pub const OWL2RL_RULES: [(&str, &str); 9] = [
(
"prp-spo1",
r#"CONSTRUCT { ?x ?p2 ?y }
WHERE {
?p1 <http://www.w3.org/2000/01/rdf-schema#subPropertyOf> ?p2 .
?x ?p1 ?y .
FILTER (?p1 != ?p2)
}"#,
),
(
"prp-inv1",
r#"CONSTRUCT { ?y ?p2 ?x }
WHERE {
?p1 <http://www.w3.org/2002/07/owl#inverseOf> ?p2 .
?x ?p1 ?y .
}"#,
),
(
"prp-inv2",
r#"CONSTRUCT { ?y ?p1 ?x }
WHERE {
?p1 <http://www.w3.org/2002/07/owl#inverseOf> ?p2 .
?x ?p2 ?y .
}"#,
),
(
"cax-sco",
r#"CONSTRUCT { ?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?c2 }
WHERE {
?c1 <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?c2 .
?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?c1 .
FILTER (?c1 != ?c2)
FILTER (!isBlank(?c2))
}"#,
),
(
"prp-dom",
r#"CONSTRUCT { ?x <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?c }
WHERE {
?p <http://www.w3.org/2000/01/rdf-schema#domain> ?c .
?x ?p ?y .
FILTER (!isBlank(?c))
}"#,
),
(
"prp-rng",
r#"CONSTRUCT { ?y <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?c }
WHERE {
?p <http://www.w3.org/2000/01/rdf-schema#range> ?c .
?x ?p ?y .
FILTER (!isBlank(?y))
FILTER (!isBlank(?c))
}"#,
),
(
"prp-trp",
r#"CONSTRUCT { ?x ?p ?z }
WHERE {
?p <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#TransitiveProperty> .
?x ?p ?y .
?y ?p ?z .
FILTER (?x != ?z)
}"#,
),
(
"prp-symp",
r#"CONSTRUCT { ?y ?p ?x }
WHERE {
?p <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#SymmetricProperty> .
?x ?p ?y .
}"#,
),
(
"eq-cls",
r#"CONSTRUCT {
?c1 <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?c2 .
?c2 <http://www.w3.org/2000/01/rdf-schema#subClassOf> ?c1 .
}
WHERE {
?c1 <http://www.w3.org/2002/07/owl#equivalentClass> ?c2 .
FILTER (?c1 != ?c2)
}"#,
),
];
pub const INFERRED_GRAPH: &str = "urn:bra0:inferred";
#[cfg(feature = "standalone")]
pub fn materialize_owl2rl(store: &mut crate::store_oxigraph::OxigraphStore) -> Result<usize, Box<dyn std::error::Error>> {
use crate::store::KgStore;
let mut total_new = 0usize;
let mut iteration = 0;
const MAX_ITERATIONS: usize = 50;
loop {
iteration += 1;
if iteration > MAX_ITERATIONS {
return Err(format!("OWL 2 RL fixpoint did not converge after {} iterations", MAX_ITERATIONS).into());
}
let mut new_this_round = 0usize;
for (_name, rule) in &OWL2RL_RULES {
let result = store.sparql_construct(rule)?;
if !result.is_empty() {
let loaded = store.load_turtle(&result, None)?;
new_this_round += loaded;
}
}
if new_this_round == 0 {
break;
}
total_new += new_this_round;
}
Ok(total_new)
}
#[cfg(test)]
#[cfg(feature = "standalone")]
mod tests {
use super::*;
use crate::store::KgStore;
use crate::store_oxigraph::OxigraphStore;
#[test]
fn test_subclass_materialization() {
let mut store = OxigraphStore::new_memory().unwrap();
let ttl = r#"
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix : <http://example.org/> .
:Cat rdfs:subClassOf :Animal .
:Animal rdfs:subClassOf :LivingThing .
:felix a :Cat .
"#;
store.load_turtle(ttl, None).unwrap();
let new_triples = materialize_owl2rl(&mut store).unwrap();
assert!(new_triples > 0, "Should materialize at least felix a :Animal");
let result = store.sparql_query(
"ASK { <http://example.org/felix> a <http://example.org/Animal> }"
).unwrap();
assert_eq!(result, "true");
let result = store.sparql_query(
"ASK { <http://example.org/felix> a <http://example.org/LivingThing> }"
).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_inverse_of_materialization() {
let mut store = OxigraphStore::new_memory().unwrap();
let ttl = r#"
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix : <http://example.org/> .
:hasParent owl:inverseOf :hasChild .
:alice :hasParent :bob .
"#;
store.load_turtle(ttl, None).unwrap();
materialize_owl2rl(&mut store).unwrap();
let result = store.sparql_query(
"ASK { <http://example.org/bob> <http://example.org/hasChild> <http://example.org/alice> }"
).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_symmetric_property() {
let mut store = OxigraphStore::new_memory().unwrap();
let ttl = r#"
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://example.org/> .
:friendOf rdf:type owl:SymmetricProperty .
:alice :friendOf :bob .
"#;
store.load_turtle(ttl, None).unwrap();
materialize_owl2rl(&mut store).unwrap();
let result = store.sparql_query(
"ASK { <http://example.org/bob> <http://example.org/friendOf> <http://example.org/alice> }"
).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_transitive_property() {
let mut store = OxigraphStore::new_memory().unwrap();
let ttl = r#"
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix : <http://example.org/> .
:partOf rdf:type owl:TransitiveProperty .
:wheel :partOf :car .
:car :partOf :fleet .
"#;
store.load_turtle(ttl, None).unwrap();
materialize_owl2rl(&mut store).unwrap();
let result = store.sparql_query(
"ASK { <http://example.org/wheel> <http://example.org/partOf> <http://example.org/fleet> }"
).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_ontoledgy_materialization_performance() {
let ontology = std::fs::read_to_string(
std::env::var("BRA0_META")
.unwrap_or_else(|_| "/Users/admin/Documents/bra0_meta".to_string())
+ "/ontologies/enterprise/ontoledgy_v1.0-draft.ttl"
);
let ontology = match ontology {
Ok(o) => o,
Err(_) => { eprintln!("SKIP: ontoLEDGY not found"); return; }
};
let mut store = OxigraphStore::new_memory().unwrap();
store.load_turtle(&ontology, None).unwrap();
let start = std::time::Instant::now();
let new_triples = materialize_owl2rl(&mut store).unwrap();
let elapsed = start.elapsed();
eprintln!("ontoLEDGY: {} new triples materialized in {:?}", new_triples, elapsed);
assert!(elapsed.as_millis() < 500, "Materialization took {:?} — must be < 500ms", elapsed);
assert!(new_triples > 0, "Should materialize at least some triples from OWL axioms");
}
}