use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use strixonomy_catalog::{ClassHierarchy, SubclassEdge};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasonerWarning {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_profile: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferredHierarchy {
pub edges: Vec<SubclassEdge>,
pub unsatisfiable: Vec<String>,
pub combined: ClassHierarchy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassificationResult {
pub profile_used: String,
pub consistent: bool,
pub unsatisfiable: Vec<String>,
pub inferred: InferredHierarchy,
pub new_inferences: Vec<SubclassEdge>,
pub warnings: Vec<ReasonerWarning>,
pub duration_ms: u64,
pub subsumption_count: usize,
pub inferred_axiom_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub equivalences: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsistencyResult {
pub consistent: bool,
pub unsatisfiable: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<ConsistencyDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsistencyDetail {
pub consistent: bool,
pub complete: bool,
pub ontology_consistent: bool,
pub abox_clashes: Vec<String>,
pub unsatisfiable: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealizationEntry {
pub individual_iri: String,
pub types: Vec<String>,
pub most_specific: Vec<String>,
pub asserted: Vec<String>,
pub inferred: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RealizationResult {
pub profile_used: String,
pub individuals: Vec<RealizationEntry>,
pub duration_ms: u64,
#[serde(default)]
pub truncated: bool,
#[serde(default)]
pub entailment_errors: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceCheckResult {
pub individual_iri: String,
pub class_iri: String,
pub entailed: bool,
pub profile_used: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferredClassAssertion {
pub individual_iri: String,
pub class_iri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferredObjectPropertyAssertion {
pub subject_iri: String,
pub property_iri: String,
pub object_iri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SameAsCluster {
pub individuals: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferredAssertions {
pub class_assertions: Vec<InferredClassAssertion>,
pub object_property_assertions: Vec<InferredObjectPropertyAssertion>,
pub same_as_clusters: Vec<SameAsCluster>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasonerSnapshot {
pub profile_used: String,
pub consistent: bool,
pub unsatisfiable: Vec<String>,
pub inferred: InferredHierarchy,
pub new_inferences: Vec<SubclassEdge>,
pub warnings: Vec<ReasonerWarning>,
pub duration_ms: u64,
pub classified_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub consistency: Option<ConsistencyDetail>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub realization: Option<RealizationResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inferred_assertions: Option<InferredAssertions>,
}
impl From<ClassificationResult> for ReasonerSnapshot {
fn from(result: ClassificationResult) -> Self {
Self {
profile_used: result.profile_used,
consistent: result.consistent,
unsatisfiable: result.unsatisfiable,
inferred: result.inferred,
new_inferences: result.new_inferences,
warnings: result.warnings,
duration_ms: result.duration_ms,
classified_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
consistency: None,
realization: None,
inferred_assertions: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplanationRequest {
pub class_iri: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplanationStep {
pub index: usize,
pub rule: String,
pub display: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject_iri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub object_iri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplanationResult {
pub class_iri: String,
pub steps: Vec<ExplanationStep>,
pub text: String,
}
const OWL_NOTHING: &str = "http://www.w3.org/2002/07/owl#Nothing";
pub fn expand_named_unsatisfiable(reported: &[String], hierarchy: &ClassHierarchy) -> Vec<String> {
let mut unsat: BTreeSet<String> = reported.iter().cloned().collect();
for edge in &hierarchy.edges {
if edge.parent == OWL_NOTHING {
unsat.insert(edge.child.clone());
}
}
let mut changed = true;
while changed {
changed = false;
for edge in &hierarchy.edges {
if unsat.contains(&edge.parent) && unsat.insert(edge.child.clone()) {
changed = true;
}
}
}
unsat.remove(OWL_NOTHING);
unsat.into_iter().collect()
}
pub fn build_inferred_hierarchy(
taxonomy_edges: &[(String, String)],
unsatisfiable: &[String],
asserted: &ClassHierarchy,
) -> InferredHierarchy {
let asserted_set: BTreeSet<(String, String)> =
asserted.edges.iter().map(|e| (e.child.clone(), e.parent.clone())).collect();
let mut inferred_edges = Vec::new();
for (child, parent) in taxonomy_edges {
let pair = (child.clone(), parent.clone());
if !asserted_set.contains(&pair) {
inferred_edges.push(SubclassEdge { child: child.clone(), parent: parent.clone() });
}
}
let mut combined_edges = asserted.edges.clone();
let mut combined_set = asserted_set;
for edge in &inferred_edges {
let pair = (edge.child.clone(), edge.parent.clone());
if combined_set.insert(pair) {
combined_edges.push(edge.clone());
}
}
let combined = hierarchy_from_edges(combined_edges);
let expanded = expand_named_unsatisfiable(unsatisfiable, &combined);
InferredHierarchy { edges: inferred_edges, unsatisfiable: expanded, combined }
}
pub fn new_inferences(asserted: &ClassHierarchy, inferred: &[SubclassEdge]) -> Vec<SubclassEdge> {
let asserted_set: BTreeSet<(String, String)> =
asserted.edges.iter().map(|e| (e.child.clone(), e.parent.clone())).collect();
inferred
.iter()
.filter(|e| !asserted_set.contains(&(e.child.clone(), e.parent.clone())))
.cloned()
.collect()
}
pub fn hierarchy_from_edges(edges: Vec<SubclassEdge>) -> ClassHierarchy {
let mut parents: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
for edge in &edges {
parents.entry(edge.child.clone()).or_default().push(edge.parent.clone());
children.entry(edge.parent.clone()).or_default().push(edge.child.clone());
}
for list in parents.values_mut() {
list.sort();
list.dedup();
}
for list in children.values_mut() {
list.sort();
list.dedup();
}
ClassHierarchy { edges, parents, children }
}
pub fn taxonomy_to_iri_edges(
ontology: &ontologos_core::Ontology,
taxonomy: &ontologos_core::Taxonomy,
) -> Result<Vec<(String, String)>, String> {
taxonomy
.subsumptions
.iter()
.map(|(sub, sup)| {
let child = entity_iri(ontology, *sub)?;
let parent = entity_iri(ontology, *sup)?;
Ok((child, parent))
})
.collect()
}
pub fn taxonomy_equivalence_clusters(
ontology: &ontologos_core::Ontology,
taxonomy: &ontologos_core::Taxonomy,
) -> Result<Vec<Vec<String>>, String> {
let mut out = Vec::new();
for cluster in &taxonomy.equivalences {
if cluster.len() < 2 {
continue;
}
let mut iris = Vec::with_capacity(cluster.len());
for id in cluster {
iris.push(entity_iri(ontology, *id)?);
}
iris.sort();
iris.dedup();
if iris.len() >= 2 {
out.push(iris);
}
}
Ok(out)
}
pub fn entity_iri(
ontology: &ontologos_core::Ontology,
id: ontologos_core::EntityId,
) -> Result<String, String> {
let entity = ontology.entity(id).map_err(|e| e.to_string())?;
ontology.resolve_iri(entity.iri).map(|s| s.to_string()).map_err(|e| e.to_string())
}
pub fn unsatisfiable_iris(
ontology: &ontologos_core::Ontology,
taxonomy: &ontologos_core::Taxonomy,
) -> Result<Vec<String>, String> {
taxonomy.unsatisfiable.iter().map(|id| entity_iri(ontology, *id)).collect()
}
pub fn detect_unsatisfiable_classes(
ontology: &ontologos_core::Ontology,
) -> Result<Vec<String>, String> {
use ontologos_el::ElClassifier;
let taxonomy =
ElClassifier::new().classify(ontology).map_err(|e| format!("unsat detection: {e}"))?;
unsatisfiable_iris(ontology, &taxonomy)
}
#[cfg(test)]
mod tests {
use super::*;
use strixonomy_catalog::SubclassEdge;
#[test]
fn expand_named_unsatisfiable_includes_descendants_of_nothing() {
let hierarchy = hierarchy_from_edges(vec![
SubclassEdge { child: "http://ex/B".into(), parent: OWL_NOTHING.into() },
SubclassEdge { child: "http://ex/Invalid".into(), parent: "http://ex/B".into() },
]);
let expanded = expand_named_unsatisfiable(&[OWL_NOTHING.to_string()], &hierarchy);
assert!(expanded.iter().any(|i| i == "http://ex/B"));
assert!(expanded.iter().any(|i| i == "http://ex/Invalid"));
assert!(!expanded.iter().any(|i| i == OWL_NOTHING));
}
}