Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 143 additions & 41 deletions src/main/java/org/stanford/ncbo/owlapi/wrapper/OntologyParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -501,7 +503,9 @@ private void escapeXMLLiterals(OWLOntology target) {
}
}

private void generateGroundTriplesForAxioms(Set<OWLAxiom> allAxioms, OWLDataFactory fact, OWLOntology sourceOnt) {
private void generateTreeViewEdges(Set<OWLAxiom> 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);
Expand All @@ -520,53 +524,151 @@ private void generateGroundTriplesForAxioms(Set<OWLAxiom> 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<OWLAxiom> 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);
}
}
}
}
}
}

/*
* 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<OWLAxiom> allAxioms;
private final OWLDataFactory fact;

RelationshipVisitor(OWLClass subject, Set<OWLAxiom> 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<OWLClass> namedClasses(OWLClassExpression filler) {
if (!filler.isAnonymous()) {
return Collections.singleton(filler.asOWLClass());
}
if (filler instanceof OWLObjectIntersectionOf) {
Set<OWLClass> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> rels, String s, String p, String o) {
return rels.contains(NS + s + " " + NS + p + " " + NS + o);
}

@Test
public void extractsAllShapes() throws Exception {
Set<String> 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"));
}
}
79 changes: 79 additions & 0 deletions src/test/resources/repo/input/relations/relations.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@prefix : <http://example.org/relations#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

<http://example.org/relations> 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 ) ] ] .