use std::collections::{BTreeMap, HashMap};
use anyhow::{Result, bail};
use horned_owl::curie::PrefixMapping;
use horned_owl::io::ofn::writer::write as write_ofn;
use horned_owl::model::{
Build, ClassExpression, Component, Individual, MutableOntology, NamedIndividual, RcStr,
SubClassOf,
};
use horned_owl::ontology::component_mapped::RcComponentMappedOntology;
use horned_owl::ontology::set::SetOntology;
use owl_dl_core::{ClassId, IndividualId, InternalOntology, RoleId, Vocabulary};
use owl_dl_reasoner::justify::Justification;
use owl_dl_reasoner::{
Classification, DataPropertyValues, DifferentIndividuals, Disjointness, ObjectPropertyValues,
PropertyClassification, ProveEntailmentResult, Realization, SameIndividuals, SyntheticDef,
};
use serde::Serialize;
const OWL_NOTHING: &str = "http://www.w3.org/2002/07/owl#Nothing";
const SCHEMA_VERSION: u32 = 1;
#[must_use]
pub(crate) fn dropped_block(onto: &SetOntology<RcStr>) -> BTreeMap<String, u64> {
owl_dl_reasoner::dropped_axioms(onto)
.map(|d| d.by_kind().clone())
.unwrap_or_default()
}
pub(crate) fn trusted_sat_risk(stats: &owl_dl_reasoner::ClassificationStats) -> usize {
if matches!(
stats.fragment,
owl_dl_reasoner::FragmentClassification::OutOfFragment
) {
stats.hyper_refuted_pairs
} else {
0
}
}
#[derive(Serialize)]
pub(crate) struct ClassifyJson {
pub(crate) schema_version: u32,
pub(crate) consistent: bool,
pub(crate) incomplete: bool,
pub(crate) trusted_sat_refutations: usize,
pub(crate) completeness_guaranteed: bool,
pub(crate) unsatisfiable: Vec<String>,
pub(crate) equivalent_groups: Vec<Vec<String>>,
pub(crate) direct_subsumptions: Vec<[String; 2]>,
pub(crate) dropped: BTreeMap<String, u64>,
}
#[derive(Serialize)]
pub(crate) struct ConsistentJson {
pub(crate) schema_version: u32,
pub(crate) consistent: bool,
pub(crate) incomplete: bool,
pub(crate) dropped: BTreeMap<String, u64>,
}
#[derive(Serialize)]
pub(crate) struct VerifyElViolationJson {
pub(crate) axiom_index: usize,
pub(crate) note: String,
}
#[derive(Serialize)]
pub(crate) struct VerifyElJson {
pub(crate) schema_version: u32,
pub(crate) verdict: &'static str,
pub(crate) domain_size: usize,
pub(crate) axioms_checked: usize,
pub(crate) violations: Vec<VerifyElViolationJson>,
pub(crate) unresolved: Vec<String>,
pub(crate) dropped: BTreeMap<String, u64>,
}
#[derive(Serialize)]
pub(crate) struct IndividualTypesJson {
pub(crate) iri: String,
pub(crate) types: Vec<String>,
pub(crate) direct_types: Vec<String>,
}
#[derive(Serialize)]
pub(crate) struct RealizeJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) witness_prune_active: bool,
pub(crate) individuals: Vec<IndividualTypesJson>,
pub(crate) dropped: BTreeMap<String, u64>,
}
#[derive(Serialize)]
pub(crate) struct DisjointJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) disjoint_classes: Vec<[String; 2]>,
pub(crate) disjoint_object_properties: Vec<[String; 2]>,
pub(crate) disjoint_data_properties: Vec<[String; 2]>,
}
#[derive(Serialize)]
pub(crate) struct SatExprJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) satisfiable: bool,
}
#[derive(Serialize)]
pub(crate) struct SubclassExprJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) entailed: bool,
}
#[derive(Serialize)]
pub(crate) struct InstancesExprJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) instances: Vec<String>,
}
#[must_use]
pub(crate) fn build_sat_expr_json(v: owl_dl_reasoner::CeVerdict) -> SatExprJson {
SatExprJson {
schema_version: SCHEMA_VERSION,
incomplete: v.incomplete(),
satisfiable: v.holds(),
}
}
#[must_use]
pub(crate) fn build_subclass_expr_json(v: owl_dl_reasoner::CeVerdict) -> SubclassExprJson {
SubclassExprJson {
schema_version: SCHEMA_VERSION,
incomplete: v.incomplete(),
entailed: v.holds(),
}
}
#[must_use]
pub(crate) fn build_instances_expr_json(r: &owl_dl_reasoner::CeInstances) -> InstancesExprJson {
let mut instances = r.individuals().to_vec();
instances.sort();
InstancesExprJson {
schema_version: SCHEMA_VERSION,
incomplete: r.incomplete(),
instances,
}
}
#[must_use]
pub(crate) fn build_classify_json(
h: &Classification,
dropped: BTreeMap<String, u64>,
) -> ClassifyJson {
let stats = h.stats();
let mut unsatisfiable: Vec<String> = h
.unsatisfiable_classes()
.into_iter()
.map(str::to_owned)
.collect();
unsatisfiable.sort();
let unsat_set: std::collections::HashSet<&str> =
unsatisfiable.iter().map(String::as_str).collect();
let mut groups: Vec<Vec<String>> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for c in h.classes() {
if seen.contains(c) || unsat_set.contains(c.as_str()) {
continue;
}
let mut group: Vec<String> = h
.equivalent_classes(c)
.into_iter()
.filter(|e| !unsat_set.contains(e))
.map(str::to_owned)
.collect();
if !group.iter().any(|g| g == c) {
group.push(c.clone());
}
group.sort();
group.dedup();
for g in &group {
seen.insert(g.clone());
}
if group.len() > 1 {
groups.push(group);
}
}
groups.sort();
let mut direct_subsumptions: Vec<[String; 2]> = Vec::new();
for c in h.classes() {
for sup in h.taxonomy_direct_subsumers(c) {
direct_subsumptions.push([c.clone(), sup.to_owned()]);
}
}
direct_subsumptions.sort();
direct_subsumptions.dedup();
ClassifyJson {
schema_version: SCHEMA_VERSION,
consistent: !stats.inconsistent,
incomplete: stats.timed_out_pairs > 0,
trusted_sat_refutations: trusted_sat_risk(&stats),
completeness_guaranteed: h.completeness_guaranteed(),
unsatisfiable,
equivalent_groups: groups,
direct_subsumptions,
dropped,
}
}
#[must_use]
pub(crate) fn build_consistent_json(
consistent: bool,
incomplete: bool,
dropped: BTreeMap<String, u64>,
) -> ConsistentJson {
ConsistentJson {
schema_version: SCHEMA_VERSION,
consistent,
incomplete,
dropped,
}
}
#[must_use]
pub(crate) fn build_verify_el_json(
verdict: &'static str,
domain_size: usize,
axioms_checked: usize,
violations: Vec<VerifyElViolationJson>,
unresolved: Vec<String>,
dropped: BTreeMap<String, u64>,
) -> VerifyElJson {
VerifyElJson {
schema_version: SCHEMA_VERSION,
verdict,
domain_size,
axioms_checked,
violations,
unresolved,
dropped,
}
}
#[must_use]
pub(crate) fn build_realize_json(r: &Realization, dropped: BTreeMap<String, u64>) -> RealizeJson {
let mut individuals: Vec<IndividualTypesJson> = r
.individuals()
.iter()
.map(|ind| {
let mut types: Vec<String> = r.entailed_types(ind).to_vec();
types.sort();
let mut direct_types: Vec<String> = r.most_specific_types(ind).to_vec();
direct_types.sort();
IndividualTypesJson {
iri: ind.clone(),
types,
direct_types,
}
})
.collect();
individuals.sort_by(|a, b| a.iri.cmp(&b.iri));
RealizeJson {
schema_version: SCHEMA_VERSION,
incomplete: r.incomplete(),
witness_prune_active: r.witness_prune_active(),
individuals,
dropped,
}
}
#[derive(Serialize)]
pub(crate) struct PropHierSide {
pub(crate) equivalent_groups: Vec<Vec<String>>,
pub(crate) direct_subsumptions: Vec<[String; 2]>,
}
#[derive(Serialize)]
pub(crate) struct PropHierJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) object_properties: PropHierSide,
pub(crate) data_properties: PropHierSide,
}
fn side(c: &PropertyClassification) -> PropHierSide {
let mut ds: Vec<[String; 2]> = c
.direct_subsumptions()
.iter()
.map(|(a, b)| [a.clone(), b.clone()])
.collect();
ds.sort();
let mut eg: Vec<Vec<String>> = c.equivalent_groups().to_vec();
eg.sort();
PropHierSide {
equivalent_groups: eg,
direct_subsumptions: ds,
}
}
#[must_use]
pub(crate) fn build_prophier_json(
obj: &PropertyClassification,
data: &PropertyClassification,
) -> PropHierJson {
PropHierJson {
schema_version: SCHEMA_VERSION,
incomplete: false,
object_properties: side(obj),
data_properties: side(data),
}
}
#[must_use]
pub(crate) fn build_disjoint_json(
classes: &Disjointness,
obj: Vec<(String, String)>,
data: Vec<(String, String)>,
) -> DisjointJson {
let to_arr = |v: Vec<(String, String)>| {
let mut a: Vec<[String; 2]> = v.into_iter().map(|(x, y)| [x, y]).collect();
a.sort();
a
};
let mut dc: Vec<[String; 2]> = classes
.pairs()
.iter()
.map(|(x, y)| [x.clone(), y.clone()])
.collect();
dc.sort();
DisjointJson {
schema_version: SCHEMA_VERSION,
incomplete: classes.incomplete(),
disjoint_classes: dc,
disjoint_object_properties: to_arr(obj),
disjoint_data_properties: to_arr(data),
}
}
#[derive(Serialize)]
pub(crate) struct IndividualsJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) same_groups: Vec<Vec<String>>,
pub(crate) different_pairs: Vec<[String; 2]>,
}
#[must_use]
pub(crate) fn build_individuals_json(
same: &SameIndividuals,
different: &DifferentIndividuals,
) -> IndividualsJson {
let mut same_groups: Vec<Vec<String>> = same
.groups()
.iter()
.map(|g| {
let mut g = g.clone();
g.sort();
g
})
.collect();
same_groups.sort();
let mut different_pairs: Vec<[String; 2]> = different
.pairs()
.iter()
.map(|(a, b)| {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
[lo.clone(), hi.clone()]
})
.collect();
different_pairs.sort();
IndividualsJson {
schema_version: SCHEMA_VERSION,
incomplete: same.incomplete() || different.incomplete(),
same_groups,
different_pairs,
}
}
#[derive(Serialize)]
pub(crate) struct PropertyValuesJson {
pub(crate) schema_version: u32,
pub(crate) incomplete: bool,
pub(crate) object_property_values: Vec<[String; 3]>,
pub(crate) data_property_values: Vec<[String; 5]>,
}
#[must_use]
pub(crate) fn build_property_values_json(
obj: &ObjectPropertyValues,
data: &DataPropertyValues,
) -> PropertyValuesJson {
let mut object_property_values: Vec<[String; 3]> = obj
.triples()
.iter()
.map(|(s, p, o)| [s.clone(), p.clone(), o.clone()])
.collect();
object_property_values.sort();
let mut data_property_values: Vec<[String; 5]> = data
.quints()
.iter()
.map(|(s, p, lex, dt, lang)| [s.clone(), p.clone(), lex.clone(), dt.clone(), lang.clone()])
.collect();
data_property_values.sort();
PropertyValuesJson {
schema_version: SCHEMA_VERSION,
incomplete: obj.incomplete() || data.incomplete(),
object_property_values,
data_property_values,
}
}
#[derive(Serialize)]
pub(crate) struct JustifyJson {
pub(crate) schema_version: u32,
pub(crate) status: String, pub(crate) enumeration_complete: bool,
pub(crate) minimal: bool, pub(crate) laconic: bool,
pub(crate) justifications: Vec<JustificationJson>,
}
#[derive(Serialize)]
pub(crate) struct JustificationJson {
pub(crate) ofn: String, }
fn axioms_to_ofn_doc(axioms: &[Component<RcStr>], pm: &PrefixMapping) -> String {
let mut so: SetOntology<RcStr> = SetOntology::new();
for ax in axioms {
so.insert(ax.clone());
}
let cmo: RcComponentMappedOntology = so.into();
let mut buf: Vec<u8> = Vec::new();
write_ofn(&mut buf, &cmo, Some(pm)).expect(
"writing a justification's axioms (no OntologyID, single ontology) to an in-memory \
buffer cannot fail",
);
String::from_utf8(buf).expect("horned-owl's OFN writer emits valid UTF-8")
}
#[must_use]
pub(crate) fn build_justify_json(
justs: &[Justification<RcStr>],
pm: &PrefixMapping,
laconic: bool,
enumeration_complete: bool,
) -> JustifyJson {
let minimal = justs.iter().all(|j| j.minimal_guaranteed);
let justifications = justs
.iter()
.map(|j| JustificationJson {
ofn: axioms_to_ofn_doc(&j.axioms, pm),
})
.collect();
JustifyJson {
schema_version: SCHEMA_VERSION,
status: if justs.is_empty() {
"not-entailed"
} else {
"entailed"
}
.to_owned(),
enumeration_complete,
minimal,
laconic,
justifications,
}
}
#[derive(Serialize)]
pub(crate) struct ProveJson {
pub(crate) schema_version: u32,
pub(crate) entailed: bool,
pub(crate) has_proof: bool,
pub(crate) proof: Option<ProofNodeJson>,
pub(crate) justification_fallback: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct ProofNodeJson {
pub(crate) conclusion: String,
pub(crate) rule: String,
pub(crate) axioms: Vec<String>,
pub(crate) premises: Vec<ProofNodeJson>,
}
fn class_id_to_class_expression(
id: ClassId,
vocab: &Vocabulary,
defs: &HashMap<ClassId, SyntheticDef>,
build: &Build<RcStr>,
) -> Result<ClassExpression<RcStr>> {
let idx = id.index() as usize;
if idx < vocab.num_classes() {
return Ok(ClassExpression::Class(build.class(vocab.class_iri(id))));
}
if let Some(def) = defs.get(&id) {
return synthetic_def_to_class_expression(def, vocab, defs, build);
}
bail!(
"internal error rendering proof JSON: class id {idx} has no vocabulary entry and no \
synthetic_defs record (proof-rendering invariant violated — every synthetic ClassId a \
saturator proof can mention should be registered in synthetic_defs at construction \
time; refusing to fabricate an opaque class into the proof output)"
);
}
fn synthetic_def_to_class_expression(
def: &SyntheticDef,
vocab: &Vocabulary,
defs: &HashMap<ClassId, SyntheticDef>,
build: &Build<RcStr>,
) -> Result<ClassExpression<RcStr>> {
Ok(match def {
SyntheticDef::TseitinConj(bodies) => {
let mut parts: Vec<ClassExpression<RcStr>> = bodies
.iter()
.map(|&b| class_id_to_class_expression(b, vocab, defs, build))
.collect::<Result<Vec<_>>>()?;
if parts.len() == 1 {
parts.remove(0)
} else {
ClassExpression::ObjectIntersectionOf(parts)
}
}
SyntheticDef::ExistMarkerOneWay { role, body }
| SyntheticDef::ExistMarkerEquiv { role, body } => ClassExpression::ObjectSomeValuesFrom {
ope: role_id_to_ope(*role, vocab, build),
bce: Box::new(class_id_to_class_expression(*body, vocab, defs, build)?),
},
SyntheticDef::NominalKey(ind) => ClassExpression::ObjectOneOf(vec![Individual::Named(
individual_id_to_named(*ind, vocab, build),
)]),
SyntheticDef::MaxKey { n, role } => ClassExpression::ObjectMaxCardinality {
n: *n,
ope: role_id_to_ope(*role, vocab, build),
bce: Box::new(ClassExpression::ObjectIntersectionOf(vec![])), },
SyntheticDef::ForallKey { role, members } => {
let mems: Vec<Individual<RcStr>> = members
.iter()
.map(|&ind| Individual::Named(individual_id_to_named(ind, vocab, build)))
.collect();
ClassExpression::ObjectAllValuesFrom {
ope: role_id_to_ope(*role, vocab, build),
bce: Box::new(ClassExpression::ObjectOneOf(mems)),
}
}
SyntheticDef::DKey(iri_suffix) => {
ClassExpression::Class(build.class(format!("urn:rustdl-dkey:{iri_suffix}")))
}
})
}
fn role_id_to_ope(
role: RoleId,
vocab: &Vocabulary,
build: &Build<RcStr>,
) -> horned_owl::model::ObjectPropertyExpression<RcStr> {
horned_owl::model::ObjectPropertyExpression::ObjectProperty(
build.object_property(vocab.role_iri(role)),
)
}
fn individual_id_to_named(
id: IndividualId,
vocab: &Vocabulary,
build: &Build<RcStr>,
) -> NamedIndividual<RcStr> {
build.named_individual(vocab.individual_iri(id))
}
fn derived_fact_to_component(
fact: &owl_dl_reasoner::DerivedFact,
vocab: &Vocabulary,
defs: &HashMap<ClassId, SyntheticDef>,
build: &Build<RcStr>,
) -> Result<Component<RcStr>> {
Ok(match fact {
owl_dl_reasoner::DerivedFact::Sub(s, p) => Component::SubClassOf(SubClassOf {
sub: class_id_to_class_expression(*s, vocab, defs, build)?,
sup: class_id_to_class_expression(*p, vocab, defs, build)?,
}),
owl_dl_reasoner::DerivedFact::Exist(s, r, t) => Component::SubClassOf(SubClassOf {
sub: class_id_to_class_expression(*s, vocab, defs, build)?,
sup: ClassExpression::ObjectSomeValuesFrom {
ope: role_id_to_ope(*r, vocab, build),
bce: Box::new(class_id_to_class_expression(*t, vocab, defs, build)?),
},
}),
owl_dl_reasoner::DerivedFact::Unsat(c) => Component::SubClassOf(SubClassOf {
sub: class_id_to_class_expression(*c, vocab, defs, build)?,
sup: ClassExpression::Class(build.class(OWL_NOTHING)),
}),
})
}
fn build_proof_node_json(
node: &owl_dl_reasoner::ProofNode,
internal: &InternalOntology,
defs: &HashMap<ClassId, SyntheticDef>,
pm: &PrefixMapping,
build: &Build<RcStr>,
) -> Result<ProofNodeJson> {
let vocab = &internal.vocabulary;
let conclusion_component = derived_fact_to_component(&node.conclusion, vocab, defs, build)?;
let conclusion = axioms_to_ofn_doc(std::slice::from_ref(&conclusion_component), pm);
let axioms: Vec<String> = node
.axiom_refs
.iter()
.filter_map(|r| internal.axioms.get(r.0))
.map(|ax| {
let component = owl_dl_core::axiom_to_component(ax, internal, build);
axioms_to_ofn_doc(std::slice::from_ref(&component), pm)
})
.collect();
let premises = node
.premises
.iter()
.map(|p| build_proof_node_json(p, internal, defs, pm, build))
.collect::<Result<Vec<_>>>()?;
Ok(ProofNodeJson {
conclusion,
rule: node.rule.to_string(),
axioms,
premises,
})
}
pub(crate) fn build_prove_json(
result: &ProveEntailmentResult,
internal: &InternalOntology,
pm: &PrefixMapping,
) -> Result<ProveJson> {
Ok(match result {
ProveEntailmentResult::SaturatorProof(data) => {
let build: Build<RcStr> = Build::new_rc();
let proof = build_proof_node_json(
&data.root,
internal,
&data.trace.synthetic_defs,
pm,
&build,
)?;
ProveJson {
schema_version: SCHEMA_VERSION,
entailed: true,
has_proof: true,
proof: Some(proof),
justification_fallback: None,
}
}
ProveEntailmentResult::JustificationFallback(j) => ProveJson {
schema_version: SCHEMA_VERSION,
entailed: true,
has_proof: false,
proof: None,
justification_fallback: Some(axioms_to_ofn_doc(&j.axioms, pm)),
},
ProveEntailmentResult::NotEntailed => ProveJson {
schema_version: SCHEMA_VERSION,
entailed: false,
has_proof: false,
proof: None,
justification_fallback: None,
},
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use horned_owl::io::ParserConfiguration;
use horned_owl::io::ofn::reader::read as read_ofn;
use horned_owl::model::RcStr;
use horned_owl::ontology::set::SetOntology;
use std::io::Cursor;
fn classify_ofn(src: &str) -> owl_dl_reasoner::Classification {
let (onto, _): (SetOntology<RcStr>, _) = read_ofn(
&mut Cursor::new(src.to_owned()),
ParserConfiguration::default(),
)
.unwrap();
owl_dl_reasoner::classify(&onto).unwrap()
}
#[test]
fn classify_json_is_sorted_and_carries_verdict() {
let h = classify_ofn(
r"Prefix(:=<http://ex/#>)
Ontology(<http://ex/>
Declaration(Class(:A)) Declaration(Class(:B)) Declaration(Class(:C))
SubClassOf(:B :A) SubClassOf(:C :B))",
);
let j = build_classify_json(&h, BTreeMap::new());
assert_eq!(j.schema_version, 1);
assert!(j.consistent);
assert!(!j.incomplete);
assert!(j.unsatisfiable.is_empty());
assert!(
j.direct_subsumptions
.contains(&["http://ex/#B".to_owned(), "http://ex/#A".to_owned()])
);
assert!(
j.direct_subsumptions
.contains(&["http://ex/#C".to_owned(), "http://ex/#B".to_owned()])
);
let mut sorted = j.direct_subsumptions.clone();
sorted.sort();
assert_eq!(j.direct_subsumptions, sorted);
}
}