diff --git a/src/main/java/org/stanford/ncbo/owlapi/wrapper/OntologyParser.java b/src/main/java/org/stanford/ncbo/owlapi/wrapper/OntologyParser.java index 57a990c..b01a626 100644 --- a/src/main/java/org/stanford/ncbo/owlapi/wrapper/OntologyParser.java +++ b/src/main/java/org/stanford/ncbo/owlapi/wrapper/OntologyParser.java @@ -16,6 +16,7 @@ import org.semanticweb.owlapi.search.EntitySearcher; import org.semanticweb.owlapi.util.AutoIRIMapper; import org.semanticweb.owlapi.util.InferredSubClassAxiomGenerator; +import org.semanticweb.owlapi.util.OWLClassExpressionVisitorAdapter; import org.semanticweb.owlapi.util.SimpleIRIMapper; import org.semanticweb.owlapi.vocab.OWLRDFVocabulary; import org.slf4j.Logger; @@ -252,7 +253,8 @@ private boolean buildOWLOntology(OWLOntology masterOntology, boolean isOBO) { IRI documentIRI = sourceOwlManager.getOntologyDocumentIRI(sourceOnt); addGroundMetadata(documentIRI, fact, sourceOnt); - generateGroundTriplesForAxioms(allAxioms, fact, sourceOnt); + generateTreeViewEdges(allAxioms, fact, sourceOnt); + generateRelationshipTriples(allAxioms, fact, sourceOnt); if (isOBO) { if (!documentIRI.toString().startsWith("owlapi:ontology")) { @@ -501,7 +503,9 @@ private void escapeXMLLiterals(OWLOntology target) { } } - private void generateGroundTriplesForAxioms(Set allAxioms, OWLDataFactory fact, OWLOntology sourceOnt) { + private void generateTreeViewEdges(Set allAxioms, OWLDataFactory fact, OWLOntology sourceOnt) { + + OWLAnnotationProperty treeView = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/treeView")); for (OWLAxiom axiom : sourceOnt.getAxioms()) { allAxioms.add(axiom); @@ -520,46 +524,51 @@ private void generateGroundTriplesForAxioms(Set allAxioms, OWLDataFact if (!some.getProperty().isAnonymous() && !some.getFiller().isAnonymous()) { String propSome = some.getProperty().asOWLObjectProperty().getIRI().toString().toLowerCase(); + IRI subClass = sc.getSubClass().asOWLClass().getIRI(); + IRI filler = some.getFiller().asOWLClass().getIRI(); + + // The OBO hierarchy is built from treeView edges. part_of/develops_from + // (and their IRIs) contribute an edge subclass -> filler; contains points + // the other way, filler -> subclass. The relationships themselves are + // emitted, for every property, by generateRelationshipTriples. + if (propSome.endsWith("part_of") + || propSome.endsWith("bfo_0000050") + || propSome.endsWith("develops_from") + || propSome.endsWith("ro_0002202")) { + allAxioms.add(fact.getOWLAnnotationAssertionAxiom(treeView, subClass, filler)); + } else if (propSome.endsWith("contains") || propSome.endsWith("ro_0001019")) { + allAxioms.add(fact.getOWLAnnotationAssertionAxiom(treeView, filler, subClass)); + } + } + } + } + } + } - if (propSome.contains("obo")) { - - if (propSome.endsWith("part_of") - || propSome.endsWith("bfo_0000050") - || propSome.endsWith("contains") - || propSome.endsWith("ro_0001019") - || propSome.endsWith("develops_from") - || propSome.endsWith("ro_0002202")) { - - OWLAnnotationProperty prop = null; - if (propSome.endsWith("contains") || propSome.endsWith("ro_0001019")) { - prop = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/obo/contains")); - OWLAxiom annAsse = fact.getOWLAnnotationAssertionAxiom(prop, some.getFiller().asOWLClass().getIRI(), sc.getSubClass().asOWLClass().getIRI()); - allAxioms.add(annAsse); - - prop = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/treeView")); - annAsse = fact.getOWLAnnotationAssertionAxiom(prop, some.getFiller().asOWLClass().getIRI(), sc.getSubClass().asOWLClass().getIRI()); - allAxioms.add(annAsse); - } else { - if (propSome.endsWith("part_of") || propSome.endsWith("bfo_0000050")) - prop = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/obo/part_of")); - else { - prop = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/obo/develops_from")); - } - - OWLAxiom annAsse = fact.getOWLAnnotationAssertionAxiom(prop, sc.getSubClass().asOWLClass().getIRI(), some.getFiller().asOWLClass().getIRI()); - allAxioms.add(annAsse); - - prop = fact.getOWLAnnotationProperty(IRI.create("http://data.bioontology.org/metadata/treeView")); - annAsse = fact.getOWLAnnotationAssertionAxiom(prop, sc.getSubClass().asOWLClass().getIRI(), some.getFiller().asOWLClass().getIRI()); - allAxioms.add(annAsse); - } - } else { - if (!some.getFiller().isAnonymous() && !sc.getSubClass().isAnonymous()) { - OWLAnnotationProperty prop = fact.getOWLAnnotationProperty(some.getProperty().asOWLObjectProperty().getIRI()); - OWLAxiom annAsse = fact.getOWLAnnotationAssertionAxiom(prop, sc.getSubClass().asOWLClass().getIRI(), some.getFiller().asOWLClass().getIRI()); - allAxioms.add(annAsse); - } - } + /* + * Emits a metadata triple for every class-to-class (or class-to-individual) + * relationship a named class participates in, for ANY object property, derived + * from SubClassOf and EquivalentClasses axioms. + * + * The right-hand side of each axiom is visited (see RelationshipVisitor) so that + * relationships nested inside intersections, and those stated via equivalences, + * are also extracted -- not just the flat shape SubClassOf(A, someValuesFrom(p, + * B)). Each relationship A --p--> filler is emitted under the property's own IRI. + */ + private void generateRelationshipTriples(Set allAxioms, OWLDataFactory fact, OWLOntology sourceOnt) { + for (OWLSubClassOfAxiom sc : sourceOnt.getAxioms(AxiomType.SUBCLASS_OF)) { + if (!sc.getSubClass().isAnonymous()) { + sc.getSuperClass().accept(new RelationshipVisitor(sc.getSubClass().asOWLClass(), allAxioms, fact)); + } + } + + for (OWLEquivalentClassesAxiom eq : sourceOnt.getAxioms(AxiomType.EQUIVALENT_CLASSES)) { + for (OWLClassExpression named : eq.getClassExpressions()) { + if (!named.isAnonymous()) { + RelationshipVisitor visitor = new RelationshipVisitor(named.asOWLClass(), allAxioms, fact); + for (OWLClassExpression other : eq.getClassExpressions()) { + if (!other.equals(named)) { + other.accept(visitor); } } } @@ -567,6 +576,99 @@ private void generateGroundTriplesForAxioms(Set allAxioms, OWLDataFact } } + /* + * Collects the relationships a fixed subject class participates in, by visiting + * the class expression on the right-hand side of a SubClassOf/EquivalentClasses + * axiom. Only intersections are traversed further; the existential shapes + * (someValuesFrom, hasValue, min/exact cardinality >= 1) emit a relationship. The + * filler of those shapes is expanded the same way -- a named filler, or the named + * classes of an intersection filler (see emitRelationshipToClass). Every other + * class-expression type is a no-op (inherited from the adapter), so unions, + * complements and allValuesFrom are, by design, neither traversed nor emitted. + */ + private class RelationshipVisitor extends OWLClassExpressionVisitorAdapter { + private final OWLClass subject; + private final Set allAxioms; + private final OWLDataFactory fact; + + RelationshipVisitor(OWLClass subject, Set allAxioms, OWLDataFactory fact) { + this.subject = subject; + this.allAxioms = allAxioms; + this.fact = fact; + } + + @Override + public void visit(OWLObjectIntersectionOf intersection) { + for (OWLClassExpression operand : intersection.getOperands()) { + operand.accept(this); + } + } + + @Override + public void visit(OWLObjectSomeValuesFrom some) { + emitRelationshipToClass(some.getProperty(), some.getFiller()); + } + + @Override + public void visit(OWLObjectHasValue hasValue) { + emitRelationshipToIndividual(hasValue.getProperty(), hasValue.getFiller()); + } + + @Override + public void visit(OWLObjectMinCardinality card) { + if (card.getCardinality() >= 1) { + emitRelationshipToClass(card.getProperty(), card.getFiller()); + } + } + + @Override + public void visit(OWLObjectExactCardinality card) { + if (card.getCardinality() >= 1) { + emitRelationshipToClass(card.getProperty(), card.getFiller()); + } + } + + /* + * Emits subject --p--> C for every named class C in the filler. A named + * filler contributes itself; an intersection filler contributes the named + * classes of its operands (recursively), since `p some (B and C1 ... Cn)` + * entails a relationship to each of B, C1, ... Cn. Anonymous fillers with no + * named class (e.g. a union, or a nested restriction) contribute nothing. + */ + private void emitRelationshipToClass(OWLObjectPropertyExpression property, OWLClassExpression filler) { + if (property.isAnonymous()) { + return; + } + OWLAnnotationProperty prop = fact.getOWLAnnotationProperty(property.asOWLObjectProperty().getIRI()); + for (OWLClass namedFiller : namedClasses(filler)) { + allAxioms.add(fact.getOWLAnnotationAssertionAxiom(prop, subject.getIRI(), namedFiller.getIRI())); + } + } + + // The named classes reachable through a (possibly nested) intersection. + private Set namedClasses(OWLClassExpression filler) { + if (!filler.isAnonymous()) { + return Collections.singleton(filler.asOWLClass()); + } + if (filler instanceof OWLObjectIntersectionOf) { + Set result = new HashSet<>(); + for (OWLClassExpression operand : ((OWLObjectIntersectionOf) filler).getOperands()) { + result.addAll(namedClasses(operand)); + } + return result; + } + return Collections.emptySet(); + } + + private void emitRelationshipToIndividual(OWLObjectPropertyExpression property, OWLIndividual filler) { + if (property.isAnonymous() || filler.isAnonymous()) { + return; + } + OWLAnnotationProperty prop = fact.getOWLAnnotationProperty(property.asOWLObjectProperty().getIRI()); + allAxioms.add(fact.getOWLAnnotationAssertionAxiom(prop, subject.getIRI(), filler.asOWLNamedIndividual().getIRI())); + } + } + /* * Parses one or more ontology files. */ diff --git a/src/test/java/org/stanford/ncbo/owlapi/wrapper/RelationshipExtractionTest.java b/src/test/java/org/stanford/ncbo/owlapi/wrapper/RelationshipExtractionTest.java new file mode 100644 index 0000000..f631993 --- /dev/null +++ b/src/test/java/org/stanford/ncbo/owlapi/wrapper/RelationshipExtractionTest.java @@ -0,0 +1,82 @@ +package org.stanford.ncbo.owlapi.wrapper; + +import org.junit.Test; +import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.model.*; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * Verifies that class-to-class (and class-to-individual) relationships are + * extracted from a range of axiom shapes -- not just SubClassOf(A, some(p, B)) -- + * and for ANY object property (no OBO privilege). The parser emits each + * relationship as a ground triple subject --p--> filler using the property's own + * IRI. Because the property is declared an object property in the export, on + * reload these triples come back as ObjectPropertyAssertion axioms, which the test + * reads directly. + */ +public class RelationshipExtractionTest { + + private static final String NS = "http://example.org/relations#"; + + private Set extractedRelationships() throws Exception { + ParserInvocation pi = new ParserInvocation( + "./src/test/resources/repo/input/relations", + "./src/test/resources/repo/output/relations", + "relations.ttl", true); + assertTrue("parse failed", new OntologyParser(pi).parse()); + + OWLOntology out = OWLManager.createOWLOntologyManager() + .loadOntologyFromOntologyDocument( + new File("./src/test/resources/repo/output/relations/owlapi.xrdf")); + + Set rels = new HashSet<>(); + for (OWLObjectPropertyAssertionAxiom ax : out.getAxioms(AxiomType.OBJECT_PROPERTY_ASSERTION)) { + if (!ax.getProperty().isAnonymous() + && ax.getSubject().isNamed() && ax.getObject().isNamed()) { + rels.add(individualOrClassIri(ax.getSubject()) + " " + + ax.getProperty().asOWLObjectProperty().getIRI() + " " + + individualOrClassIri(ax.getObject())); + } + } + return rels; + } + + private String individualOrClassIri(OWLIndividual ind) { + return ind.asOWLNamedIndividual().getIRI().toString(); + } + + private boolean rel(Set rels, String s, String p, String o) { + return rels.contains(NS + s + " " + NS + p + " " + NS + o); + } + + @Test + public void extractsAllShapes() throws Exception { + Set rels = extractedRelationships(); + + // Extracted: + assertTrue("bare some: A hasPart B", rel(rels, "A", "hasPart", "B")); + assertTrue("nested intersection: A connectedTo D", rel(rels, "A", "connectedTo", "D")); + assertTrue("equivalent + intersection: E connectedTo F", rel(rels, "E", "connectedTo", "F")); + assertTrue("hasValue: A hasColour red", rel(rels, "A", "hasColour", "red")); + assertTrue("min-cardinality >=1: A adjacentTo D", rel(rels, "A", "adjacentTo", "D")); + // someValuesFrom with an intersection filler -> a relationship to each named conjunct. + assertTrue("intersection filler: G hasPart B", rel(rels, "G", "hasPart", "B")); + assertTrue("intersection filler: G hasPart C1", rel(rels, "G", "hasPart", "C1")); + // nested intersection filler -> to each named class at any depth. + assertTrue("nested intersection filler: H hasPart B", rel(rels, "H", "hasPart", "B")); + assertTrue("nested intersection filler: H hasPart C1", rel(rels, "H", "hasPart", "C1")); + assertTrue("nested intersection filler: H hasPart C2", rel(rels, "H", "hasPart", "C2")); + + // Not extracted: + assertFalse("allValuesFrom must not extract", rel(rels, "A", "hasPart", "E")); + assertFalse("union operand C1 must not extract", rel(rels, "A", "hasPart", "C1")); + assertFalse("union operand C2 must not extract", rel(rels, "A", "hasPart", "C2")); + assertFalse("union filler operand B must not extract", rel(rels, "I", "hasPart", "B")); + assertFalse("union filler operand C1 must not extract", rel(rels, "I", "hasPart", "C1")); + } +} diff --git a/src/test/resources/repo/input/relations/relations.ttl b/src/test/resources/repo/input/relations/relations.ttl new file mode 100644 index 0000000..032e683 --- /dev/null +++ b/src/test/resources/repo/input/relations/relations.ttl @@ -0,0 +1,79 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . + + a owl:Ontology . + +# Non-OBO object properties (would be dropped by the old contains("obo") guard) +:hasPart a owl:ObjectProperty . +:connectedTo a owl:ObjectProperty . +:hasColour a owl:ObjectProperty . +:adjacentTo a owl:ObjectProperty . + +# Named classes +:A a owl:Class . +:B a owl:Class . +:C1 a owl:Class . +:C2 a owl:Class . +:D a owl:Class . +:E a owl:Class . +:F a owl:Class . + +# Named individual (filler for hasValue) +:red a owl:NamedIndividual . + +# 1) Bare someValuesFrom under SubClassOf -> A hasPart some B +:A rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasPart ; owl:someValuesFrom :B ] . + +# 2) someValuesFrom NESTED in an intersection under SubClassOf +# A subClassOf ( C1 and (connectedTo some D) and C2 ) +:A rdfs:subClassOf [ + a owl:Class ; + owl:intersectionOf ( :C1 + [ a owl:Restriction ; owl:onProperty :connectedTo ; owl:someValuesFrom :D ] + :C2 ) +] . + +# 3) EquivalentClasses with intersection + someValuesFrom -> E connectedTo some F +:E owl:equivalentClass [ + a owl:Class ; + owl:intersectionOf ( :C1 + [ a owl:Restriction ; owl:onProperty :connectedTo ; owl:someValuesFrom :F ] ) +] . + +# 4) hasValue to an individual -> A hasColour red +:A rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasColour ; owl:hasValue :red ] . + +# 5) min-cardinality (n>=1) with a named filler -> A adjacentTo some D +:A rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :adjacentTo ; owl:minQualifiedCardinality "1"^^xsd:nonNegativeInteger ; owl:onClass :D ] . + +# 6) allValuesFrom -> MUST NOT be extracted (constraint, not existential) +:A rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasPart ; owl:allValuesFrom :E ] . + +# 7) union -> MUST NOT be descended into +:A rdfs:subClassOf [ + a owl:Class ; + owl:unionOf ( [ a owl:Restriction ; owl:onProperty :hasPart ; owl:someValuesFrom :C1 ] + [ a owl:Restriction ; owl:onProperty :hasPart ; owl:someValuesFrom :C2 ] ) +] . + +# 8) someValuesFrom whose filler is an INTERSECTION containing named classes +# G subClassOf hasPart some (B and C1) -> G hasPart B, G hasPart C1 +:G a owl:Class . +:G rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasPart ; + owl:someValuesFrom [ a owl:Class ; owl:intersectionOf ( :B :C1 ) ] ] . + +# 9) NESTED intersection filler +# H subClassOf hasPart some (B and (C1 and C2)) -> H hasPart B, C1, C2 +:H a owl:Class . +:H rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasPart ; + owl:someValuesFrom [ a owl:Class ; owl:intersectionOf + ( :B [ a owl:Class ; owl:intersectionOf ( :C1 :C2 ) ] ) ] ] . + +# 10) UNION filler -> MUST NOT extract +# I subClassOf hasPart some (B or C1) -> nothing +:I a owl:Class . +:I rdfs:subClassOf [ a owl:Restriction ; owl:onProperty :hasPart ; + owl:someValuesFrom [ a owl:Class ; owl:unionOf ( :B :C1 ) ] ] .