#![allow(clippy::doc_markdown)]
use std::collections::{BTreeMap, BTreeSet};
use horned_owl::model::{
ClassExpression, Component, DataProperty, DataRange, FacetRestriction, ForIRI,
};
use horned_owl::ontology::set::SetOntology;
use horned_owl::vocab::Facet;
use crate::Vocabulary;
use crate::ir::{ClassId, ConceptId};
use crate::ontology::Axiom;
pub fn derive_data_axioms<A: ForIRI>(
src: &SetOntology<A>,
vocab: &Vocabulary,
bot_id: ConceptId,
atomic_id: impl Fn(ClassId) -> ConceptId,
) -> Vec<Axiom> {
let mut facts = extract_facts(src);
propagate_intersection_bounds(src, &mut facts);
let mut out = Vec::new();
emit_clashes(&facts, vocab, bot_id, &atomic_id, &mut out);
emit_domain_inferences(&facts, vocab, &atomic_id, &mut out);
emit_subdataprop_transitivity(&facts, vocab, &atomic_id, &mut out);
out
}
#[allow(clippy::too_many_lines, reason = "single fixpoint with 4 facts to propagate; splitting hurts readability")]
fn propagate_intersection_bounds<A: ForIRI>(src: &SetOntology<A>, facts: &mut Facts) {
let mut inherited_from: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for ac in src {
match &ac.component {
Component::EquivalentClasses(ax) => {
let atomic_members: Vec<String> = ax.0.iter().filter_map(class_iri).collect();
let intersection_members = ax.0.iter().filter_map(|ce| {
if let ClassExpression::ObjectIntersectionOf(parts) = ce {
Some(parts)
} else {
None
}
});
for parts in intersection_members {
let part_iris: Vec<String> =
parts.iter().filter_map(class_iri).collect();
for owner in &atomic_members {
for part in &part_iris {
if owner != part {
inherited_from
.entry(owner.clone())
.or_default()
.insert(part.clone());
}
}
}
}
}
Component::SubClassOf(ax) => {
if let (Some(owner), ClassExpression::ObjectIntersectionOf(parts)) =
(class_iri(&ax.sub), &ax.sup)
{
for part in parts.iter().filter_map(class_iri) {
if owner != part {
inherited_from
.entry(owner.clone())
.or_default()
.insert(part);
}
}
}
}
_ => {}
}
}
let mut changed = true;
while changed {
changed = false;
for (owner, parents) in &inherited_from {
for parent in parents {
let parent_mins: Vec<((String, String), u32)> = facts
.class_min
.iter()
.filter(|((c, _), _)| c == parent)
.map(|((_, dp), n)| ((owner.clone(), dp.clone()), *n))
.collect();
for (key, n) in parent_mins {
let entry = facts.class_min.entry(key).or_insert(0);
if n > *entry {
*entry = n;
changed = true;
}
}
let parent_maxes: Vec<((String, String), u32)> = facts
.class_max
.iter()
.filter(|((c, _), _)| c == parent)
.map(|((_, dp), n)| ((owner.clone(), dp.clone()), *n))
.collect();
for (key, n) in parent_maxes {
let entry = facts.class_max.entry(key).or_insert(u32::MAX);
if n < *entry {
*entry = n;
changed = true;
}
}
let parent_somes: Vec<(String, String)> = facts
.class_some
.iter()
.filter(|(c, _)| c == parent)
.map(|(_, dp)| (owner.clone(), dp.clone()))
.collect();
for pair in parent_somes {
if facts.class_some.insert(pair) {
changed = true;
}
}
let parent_ranges: Vec<((String, String), Vec<IntegerRange>)> = facts
.class_int_ranges
.iter()
.filter(|((c, _), _)| c == parent)
.map(|((_, dp), rs)| ((owner.clone(), dp.clone()), rs.clone()))
.collect();
for (key, ranges) in parent_ranges {
let entry = facts.class_int_ranges.entry(key).or_default();
for r in ranges {
if !entry.iter().any(|e| e.min == r.min && e.max == r.max) {
entry.push(r);
changed = true;
}
}
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
struct IntegerRange {
min: Option<i64>,
max: Option<i64>,
}
impl IntegerRange {
const fn unbounded() -> Self {
Self { min: None, max: None }
}
fn intersect(self, other: Self) -> Self {
let min = match (self.min, other.min) {
(None, x) | (x, None) => x,
(Some(a), Some(b)) => Some(if a > b { a } else { b }),
};
let max = match (self.max, other.max) {
(None, x) | (x, None) => x,
(Some(a), Some(b)) => Some(if a < b { a } else { b }),
};
Self { min, max }
}
fn is_empty(self) -> bool {
matches!((self.min, self.max), (Some(a), Some(b)) if a > b)
}
}
#[derive(Default, Debug)]
struct Facts {
functional_dps: BTreeSet<String>,
class_min: BTreeMap<(String, String), u32>,
class_max: BTreeMap<(String, String), u32>,
domains: Vec<(String, String)>,
sub_data_property: Vec<(String, String)>,
class_some: BTreeSet<(String, String)>,
some_super: BTreeMap<String, BTreeSet<String>>,
class_int_ranges: BTreeMap<(String, String), Vec<IntegerRange>>,
}
fn extract_facts<A: ForIRI>(src: &SetOntology<A>) -> Facts {
let mut f = Facts::default();
for ac in src {
scan_component(&ac.component, &mut f);
}
f
}
fn scan_component<A: ForIRI>(c: &Component<A>, f: &mut Facts) {
use Component as C;
match c {
C::FunctionalDataProperty(ax) => {
f.functional_dps.insert(dp_iri(&ax.0));
}
C::SubDataPropertyOf(ax) => {
let sub = dpe_iri(&ax.sub);
let sup = dpe_iri(&ax.sup);
if !sub.is_empty() && !sup.is_empty() {
f.sub_data_property.push((sub, sup));
}
}
C::EquivalentDataProperties(ax) => {
let iris: Vec<String> = ax.0.iter().map(dp_iri).collect();
for i in 0..iris.len() {
for j in 0..iris.len() {
if i != j {
f.sub_data_property.push((iris[i].clone(), iris[j].clone()));
}
}
}
}
C::DataPropertyDomain(ax) => {
let dp = dpe_iri(&ax.dp);
if let Some(d) = class_iri(&ax.ce) {
f.domains.push((dp, d));
}
}
C::SubClassOf(ax) => {
scan_subclass_axiom(&ax.sub, &ax.sup, f);
}
C::EquivalentClasses(ax) => {
let atomic: Vec<String> = ax
.0
.iter()
.filter_map(class_iri)
.collect();
for c in &atomic {
for other in &ax.0 {
scan_class_for_bounds(c, other, f);
scan_class_for_existentials(c, other, f);
}
}
}
_ => {}
}
}
fn scan_subclass_axiom<A: ForIRI>(
sub: &ClassExpression<A>,
sup: &ClassExpression<A>,
f: &mut Facts,
) {
if let Some(sub_iri) = class_iri(sub) {
scan_class_for_bounds(&sub_iri, sup, f);
scan_class_for_existentials(&sub_iri, sup, f);
}
if let (Some(dp), Some(sup_iri)) = (data_some_property(sub), class_iri(sup)) {
f.some_super.entry(dp).or_default().insert(sup_iri);
}
}
fn scan_class_for_bounds<A: ForIRI>(class_iri: &str, ce: &ClassExpression<A>, f: &mut Facts) {
match ce {
ClassExpression::DataMinCardinality { n, dp, .. } => {
let key = (class_iri.to_string(), dpe_iri(dp));
let entry = f.class_min.entry(key).or_insert(0);
*entry = (*entry).max(*n);
}
ClassExpression::DataMaxCardinality { n, dp, .. } => {
let key = (class_iri.to_string(), dpe_iri(dp));
let entry = f.class_max.entry(key).or_insert(u32::MAX);
*entry = (*entry).min(*n);
}
ClassExpression::DataExactCardinality { n, dp, .. } => {
let key = (class_iri.to_string(), dpe_iri(dp));
let min_entry = f.class_min.entry(key.clone()).or_insert(0);
*min_entry = (*min_entry).max(*n);
let max_entry = f.class_max.entry(key).or_insert(u32::MAX);
*max_entry = (*max_entry).min(*n);
}
ClassExpression::ObjectIntersectionOf(parts) => {
for p in parts {
scan_class_for_bounds(class_iri, p, f);
}
}
_ => {}
}
}
fn scan_class_for_existentials<A: ForIRI>(
class_iri: &str,
ce: &ClassExpression<A>,
f: &mut Facts,
) {
match ce {
ClassExpression::DataSomeValuesFrom { dp, dr } => {
f.class_some.insert((class_iri.to_string(), dpe_iri(dp)));
if let Some(range) = parse_integer_range(dr) {
f.class_int_ranges
.entry((class_iri.to_string(), dpe_iri(dp)))
.or_default()
.push(range);
}
}
ClassExpression::DataHasValue { dp, .. } => {
f.class_some.insert((class_iri.to_string(), dpe_iri(dp)));
}
ClassExpression::DataMinCardinality { dp, n, .. } if *n >= 1 => {
f.class_some.insert((class_iri.to_string(), dpe_iri(dp)));
}
ClassExpression::DataExactCardinality { dp, n, .. } if *n >= 1 => {
f.class_some.insert((class_iri.to_string(), dpe_iri(dp)));
}
ClassExpression::ObjectIntersectionOf(parts) => {
for p in parts {
scan_class_for_existentials(class_iri, p, f);
}
}
_ => {}
}
}
fn parse_integer_range<A: ForIRI>(dr: &DataRange<A>) -> Option<IntegerRange> {
let DataRange::DatatypeRestriction(dt, facets) = dr else {
return None;
};
if dt.0.to_string() != "http://www.w3.org/2001/XMLSchema#integer" {
return None;
}
parse_integer_facets(facets)
}
fn parse_integer_facets<A: ForIRI>(facets: &[FacetRestriction<A>]) -> Option<IntegerRange> {
let mut range = IntegerRange::unbounded();
for fr in facets {
let val: i64 = fr.l.literal().parse().ok()?;
match fr.f {
Facet::MinInclusive => {
range.min = Some(range.min.map_or(val, |existing| existing.max(val)));
}
Facet::MinExclusive => {
let inclusive = val.checked_add(1)?;
range.min = Some(range.min.map_or(inclusive, |existing| existing.max(inclusive)));
}
Facet::MaxInclusive => {
range.max = Some(range.max.map_or(val, |existing| existing.min(val)));
}
Facet::MaxExclusive => {
let inclusive = val.checked_sub(1)?;
range.max = Some(range.max.map_or(inclusive, |existing| existing.min(inclusive)));
}
_ => return None,
}
}
Some(range)
}
fn closure_sub_dp(edges: &[(String, String)]) -> BTreeMap<String, BTreeSet<String>> {
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut all_dps: BTreeSet<String> = BTreeSet::new();
for (s, t) in edges {
all_dps.insert(s.clone());
all_dps.insert(t.clone());
}
for dp in &all_dps {
out.insert(dp.clone(), [dp.clone()].into_iter().collect());
}
let mut changed = true;
while changed {
changed = false;
for (s, t) in edges {
let t_supers = out.get(t).cloned().unwrap_or_default();
let entry = out.entry(s.clone()).or_default();
for sup in t_supers {
if entry.insert(sup) {
changed = true;
}
}
}
}
out
}
fn emit_clashes(
f: &Facts,
vocab: &Vocabulary,
bot_id: ConceptId,
atomic_id: &impl Fn(ClassId) -> ConceptId,
out: &mut Vec<Axiom>,
) {
for ((class_iri, dp_iri), min) in &f.class_min {
if *min >= 2
&& f.functional_dps.contains(dp_iri)
&& let Some(cid) = vocab.class_id(class_iri)
{
out.push(Axiom::SubClassOf {
sub: atomic_id(cid),
sup: bot_id,
});
}
}
for ((class_iri, dp_iri), min) in &f.class_min {
if let Some(max) = f.class_max.get(&(class_iri.clone(), dp_iri.clone()))
&& min > max
&& let Some(cid) = vocab.class_id(class_iri)
{
out.push(Axiom::SubClassOf {
sub: atomic_id(cid),
sup: bot_id,
});
}
}
for ((class_iri, dp_iri), ranges) in &f.class_int_ranges {
if ranges.len() < 2 || !f.functional_dps.contains(dp_iri) {
continue;
}
let intersection = ranges
.iter()
.copied()
.fold(IntegerRange::unbounded(), IntegerRange::intersect);
if intersection.is_empty()
&& let Some(cid) = vocab.class_id(class_iri)
{
out.push(Axiom::SubClassOf {
sub: atomic_id(cid),
sup: bot_id,
});
}
}
}
fn emit_domain_inferences(
f: &Facts,
vocab: &Vocabulary,
atomic_id: &impl Fn(ClassId) -> ConceptId,
out: &mut Vec<Axiom>,
) {
for (dp_iri, domain_iri) in &f.domains {
for (class_iri, c_dp) in &f.class_some {
if c_dp != dp_iri {
continue;
}
if class_iri == domain_iri {
continue; }
if let (Some(c_id), Some(d_id)) =
(vocab.class_id(class_iri), vocab.class_id(domain_iri))
{
out.push(Axiom::SubClassOf {
sub: atomic_id(c_id),
sup: atomic_id(d_id),
});
}
}
}
}
fn emit_subdataprop_transitivity(
f: &Facts,
vocab: &Vocabulary,
atomic_id: &impl Fn(ClassId) -> ConceptId,
out: &mut Vec<Axiom>,
) {
let closure = closure_sub_dp(&f.sub_data_property);
for (class_iri, specific_dp) in &f.class_some {
let Some(supers) = closure.get(specific_dp) else { continue };
for general_dp in supers {
let Some(super_classes) = f.some_super.get(general_dp) else { continue };
for d_iri in super_classes {
if class_iri == d_iri {
continue;
}
if let (Some(c_id), Some(d_id)) =
(vocab.class_id(class_iri), vocab.class_id(d_iri))
{
out.push(Axiom::SubClassOf {
sub: atomic_id(c_id),
sup: atomic_id(d_id),
});
}
}
}
}
for (general_dp, domain_iri) in &f.domains {
for (sub_dp, supers) in &closure {
if !supers.contains(general_dp) || sub_dp == general_dp {
continue;
}
for (class_iri, c_dp) in &f.class_some {
if c_dp != sub_dp {
continue;
}
if class_iri == domain_iri {
continue;
}
if let (Some(c_id), Some(d_id)) =
(vocab.class_id(class_iri), vocab.class_id(domain_iri))
{
out.push(Axiom::SubClassOf {
sub: atomic_id(c_id),
sup: atomic_id(d_id),
});
}
}
}
}
}
fn class_iri<A: ForIRI>(ce: &ClassExpression<A>) -> Option<String> {
if let ClassExpression::Class(c) = ce {
Some(c.0.to_string())
} else {
None
}
}
fn dp_iri<A: ForIRI>(d: &DataProperty<A>) -> String {
d.0.to_string()
}
fn dpe_iri<A: ForIRI>(d: &DataProperty<A>) -> String {
d.0.to_string()
}
fn data_some_property<A: ForIRI>(ce: &ClassExpression<A>) -> Option<String> {
match ce {
ClassExpression::DataSomeValuesFrom { dp, .. }
| ClassExpression::DataHasValue { dp, .. } => Some(dpe_iri(dp)),
ClassExpression::DataMinCardinality { dp, n, .. } if *n >= 1 => Some(dpe_iri(dp)),
ClassExpression::DataExactCardinality { dp, n, .. } if *n >= 1 => Some(dpe_iri(dp)),
_ => None,
}
}
#[allow(dead_code)]
fn _unused_datarange<A: ForIRI>(_: &DataRange<A>) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::convert::convert_ontology;
use horned_owl::io::ofn::reader::read as read_ofn;
use horned_owl::io::ParserConfiguration;
use horned_owl::model::RcStr;
use horned_owl::ontology::set::SetOntology;
use std::io::Cursor;
fn parse_str(src: &str) -> SetOntology<RcStr> {
let mut r = Cursor::new(src);
read_ofn(&mut r, ParserConfiguration::default()).unwrap().0
}
#[test]
fn extracts_functional_dp_min_clash() {
let src = r#"Prefix(:=<http://t/>)
Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
Ontology(<http://t/x>
Declaration(Class(:HasTwoAges))
Declaration(DataProperty(:age))
FunctionalDataProperty(:age)
SubClassOf(:HasTwoAges DataMinCardinality(2 :age))
)
"#;
let onto = parse_str(src);
let facts = extract_facts(&onto);
assert!(facts.functional_dps.contains("http://t/age"));
assert_eq!(
facts.class_min.get(&("http://t/HasTwoAges".to_string(), "http://t/age".to_string())),
Some(&2)
);
}
#[test]
fn derives_functional_dp_min_unsat_in_convert() {
let src = r#"Prefix(:=<http://t/>)
Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
Ontology(<http://t/x>
Declaration(Class(:HasTwoAges))
Declaration(DataProperty(:age))
FunctionalDataProperty(:age)
SubClassOf(:HasTwoAges DataMinCardinality(2 :age))
)
"#;
let onto = parse_str(src);
let mut internal = convert_ontology(&onto).unwrap();
let has_two_ages = internal.vocabulary.class_id("http://t/HasTwoAges")
.expect("HasTwoAges interned");
let bot = internal.concepts.bot();
let sub_concept = internal.concepts.atomic(has_two_ages);
let found_unsat = internal.axioms.iter().any(|ax| matches!(ax,
Axiom::SubClassOf { sub, sup } if *sub == sub_concept && *sup == bot));
assert!(found_unsat, "D4: HasTwoAges ⊑ Bot should be derived from Functional + DataMin");
}
}