diff --git a/docs/reduce.md b/docs/reduce.md index 9d5fb6015..aef57c257 100644 --- a/docs/reduce.md +++ b/docs/reduce.md @@ -7,3 +7,12 @@ ROBOT can be used to remove redundant subClassOf axioms: --output results/reduced.owl See [reason](/reason) for details on supported reasoners (EMR is not supported in `reduce`). + +Available options for `reduce`: +* `--preserve-annotated-axioms`: if set to true, axioms that have axiom annotations will not be removed, even if found to be redundant (default `false`). +* `--named-classes-only`: if set to true, only subclass axioms between named classes will be checked for redundancy. Anonymous class expressions will be ignored (default `false`). + +### Warning + +Reciprocal subclass axioms (e.g. `A SubClassOf B`, `B SubClassOf A`), entailing equivalence between `A` and `B`, may be removed by `reduce`. In this case it is important to +assert an equivalence axiom (`A EquivalentTo B`) using the `reason` command before running reduce. \ No newline at end of file diff --git a/robot-command/src/main/java/org/obolibrary/robot/ReduceCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ReduceCommand.java index f5d4bcaea..1209804bf 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/ReduceCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/ReduceCommand.java @@ -29,6 +29,8 @@ public ReduceCommand() { "preserve-annotated-axioms", true, "preserve annotated axioms when removing redundant subclass axioms"); + o.addOption( + "c", "named-classes-only", true, "only reduce subclass axioms between named classes"); o.addOption("i", "input", true, "reduce ontology from a file"); o.addOption("I", "input-iri", true, "reduce ontology from an IRI"); o.addOption("o", "output", true, "save reduceed ontology to a file"); diff --git a/robot-core/src/main/java/org/obolibrary/robot/ReduceOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ReduceOperation.java index 941b35731..8c511bde4 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/ReduceOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/ReduceOperation.java @@ -2,26 +2,12 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import org.semanticweb.owlapi.apibinding.OWLManager; -import org.semanticweb.owlapi.model.AxiomType; -import org.semanticweb.owlapi.model.IRI; -import org.semanticweb.owlapi.model.OWLAxiom; -import org.semanticweb.owlapi.model.OWLClass; -import org.semanticweb.owlapi.model.OWLClassExpression; -import org.semanticweb.owlapi.model.OWLDataFactory; -import org.semanticweb.owlapi.model.OWLObjectPropertyCharacteristicAxiom; -import org.semanticweb.owlapi.model.OWLOntology; -import org.semanticweb.owlapi.model.OWLOntologyCreationException; -import org.semanticweb.owlapi.model.OWLOntologyManager; -import org.semanticweb.owlapi.model.OWLSubClassOfAxiom; +import org.semanticweb.owlapi.model.*; import org.semanticweb.owlapi.model.parameters.Imports; import org.semanticweb.owlapi.reasoner.Node; +import org.semanticweb.owlapi.reasoner.NodeSet; import org.semanticweb.owlapi.reasoner.OWLReasoner; import org.semanticweb.owlapi.reasoner.OWLReasonerFactory; import org.slf4j.Logger; @@ -38,7 +24,7 @@ * *
Because an OWL reasoner will only return named (non-anonymous) superclasses, we add a
* pre-processing step, where for each class C appearing in either LHS or RHS of a SubClassOf
* expression, if C is anonymous, we create a named class C' and add a temporary axioms
* EquivalentClasses(C' C)
, which is later removed as a post-processing step. When performing
@@ -46,7 +32,7 @@
*
*
+ * We make a special additional case of redunancy, as in the following example:
* 1. (hand and part-of some human) SubClassOf part-of some forelimb
* 2. hand SubClassOf part-of some forelimb
*
Here we treat axiom 1 as redundant, but this is not detected by the algorithm above,
@@ -68,6 +54,7 @@ public class ReduceOperation {
public static Map getDefaultOptions() {
Map options = new HashMap<>();
options.put("preserve-annotated-axioms", "false");
+ options.put("named-classes-only", "false");
return options;
}
@@ -94,6 +81,27 @@ public static void reduce(OWLOntology ontology, OWLReasonerFactory reasonerFacto
public static void reduce(
OWLOntology ontology, OWLReasonerFactory reasonerFactory, Map options)
throws OWLOntologyCreationException {
+ boolean preserveAnnotatedAxioms =
+ OptionsHelper.optionIsTrue(options, "preserve-annotated-axioms");
+ boolean namedClassesOnly = OptionsHelper.optionIsTrue(options, "named-classes-only");
+ if (namedClassesOnly) {
+ reduceNamedOnly(ontology, reasonerFactory, preserveAnnotatedAxioms);
+ } else {
+ reduceAllClassExpressions(ontology, reasonerFactory, preserveAnnotatedAxioms);
+ }
+ }
+
+ /**
+ * Remove redundant SubClassOf axioms.
+ *
+ * @param ontology The ontology to reduce.
+ * @param reasonerFactory The reasoner factory to use.
+ * @param preserveAnnotatedAxioms Whether to not remove redundant, but annotated, axioms.
+ * @throws OWLOntologyCreationException on ontology problem
+ */
+ private static void reduceAllClassExpressions(
+ OWLOntology ontology, OWLReasonerFactory reasonerFactory, boolean preserveAnnotatedAxioms)
+ throws OWLOntologyCreationException {
OWLOntologyManager manager = OWLManager.createOWLOntologyManager();
OWLDataFactory dataFactory = manager.getOWLDataFactory();
@@ -166,7 +174,7 @@ public static void reduce(
Set rmAxioms = new HashSet<>();
for (OWLSubClassOfAxiom ax : assertedSubClassAxioms) {
- if (OptionsHelper.optionIsTrue(options, "preserve-annotated-axioms")) {
+ if (preserveAnnotatedAxioms) {
if (ax.getAnnotations().size() > 0) {
logger.debug("Protecting axiom with annotations: " + ax);
continue;
@@ -254,6 +262,7 @@ public static void reduce(
for (OWLAxiom ax : rmAxioms) {
manager.removeAxiom(ontology, ax);
}
+ reasoner.dispose();
}
/**
@@ -279,4 +288,93 @@ private static OWLClass mapClass(
}
return rxmap.get(x);
}
+
+ /**
+ * Remove redundant SubClassOf axioms, only considering named classes. When only considering named
+ * classes, a somewhat more efficient algorithm can be used.
+ *
+ * @param ontology The ontology to reduce.
+ * @param reasonerFactory The reasoner factory to use.
+ * @param preserveAnnotatedAxioms Whether to not remove redundant, but annotated, axioms.
+ */
+ private static void reduceNamedOnly(
+ OWLOntology ontology, OWLReasonerFactory reasonerFactory, boolean preserveAnnotatedAxioms) {
+ // Map>
+ Map>> assertions = new HashMap<>();
+ Set assertedSubClassAxioms = ontology.getAxioms(AxiomType.SUBCLASS_OF);
+ for (OWLSubClassOfAxiom ax : assertedSubClassAxioms) {
+ if (!ax.getSubClass().isAnonymous() && !ax.getSuperClass().isAnonymous()) {
+ OWLClass subclass = ax.getSubClass().asOWLClass();
+ OWLClass superclass = ax.getSuperClass().asOWLClass();
+ if (!assertions.containsKey(superclass)) {
+ assertions.put(superclass, new HashMap<>());
+ }
+ Map> subMap = assertions.get(superclass);
+ if (!subMap.containsKey(subclass)) {
+ subMap.put(subclass, new HashSet<>());
+ }
+ Set axioms = subMap.get(subclass);
+ axioms.add(ax);
+ }
+ }
+ OWLReasoner reasoner = reasonerFactory.createReasoner(ontology);
+ if (!reasoner.isConsistent()) {
+ logger.info("Ontology is not consistent!");
+ return;
+ }
+ Node unsatisfiableClasses = reasoner.getUnsatisfiableClasses();
+ if (unsatisfiableClasses.getSize() > 1) {
+ logger.info(
+ "There are {} unsatisfiable classes in the ontology.", unsatisfiableClasses.getSize());
+ for (OWLClass cls : unsatisfiableClasses) {
+ if (!cls.isOWLNothing()) {
+ logger.info(" unsatisfiable: " + cls.getIRI());
+ }
+ }
+ }
+ Set nonredundant = new HashSet<>();
+ Set> alreadySeen = new HashSet<>();
+ findNonRedundant(reasoner.getTopClassNode(), reasoner, assertions, nonredundant, alreadySeen);
+ OWLOntologyManager manager = ontology.getOWLOntologyManager();
+ for (OWLSubClassOfAxiom ax : assertedSubClassAxioms) {
+ if (!ax.getSubClass().isAnonymous() && !ax.getSuperClass().isAnonymous()) {
+ if (preserveAnnotatedAxioms) {
+ if (ax.getAnnotations().size() > 0) {
+ logger.debug("Protecting axiom with annotations: " + ax);
+ continue;
+ }
+ }
+ if (!nonredundant.contains(ax)) {
+ manager.removeAxiom(ontology, ax);
+ }
+ }
+ }
+ reasoner.dispose();
+ }
+
+ private static void findNonRedundant(
+ Node node,
+ OWLReasoner reasoner,
+ Map>> assertions,
+ Set nonredundant,
+ Set> alreadySeen) {
+ if (!alreadySeen.contains(node)) {
+ NodeSet subclasses = reasoner.getSubClasses(node.getRepresentativeElement(), true);
+ for (OWLClass superclass : node.getEntities()) {
+ for (OWLClass subclass : subclasses.getFlattened()) {
+ if (assertions.containsKey(superclass)) {
+ Map> subclassAxiomsBySubclass =
+ assertions.get(superclass);
+ if (subclassAxiomsBySubclass.containsKey(subclass)) {
+ nonredundant.addAll(subclassAxiomsBySubclass.get(subclass));
+ }
+ }
+ }
+ }
+ alreadySeen.add(node);
+ for (Node subclassNode : subclasses.getNodes()) {
+ findNonRedundant(subclassNode, reasoner, assertions, nonredundant, alreadySeen);
+ }
+ }
+ }
}
diff --git a/robot-core/src/test/java/org/obolibrary/robot/ReduceOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/ReduceOperationTest.java
index 001ac700c..022b1be15 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/ReduceOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/ReduceOperationTest.java
@@ -29,6 +29,16 @@ public void testRemoveRedundantSubClassAxiomsPreserveAnnotated()
ReduceOperation.reduce(reasoned, reasonerFactory, options);
assertIdentical("/without_redundant_subclasses.owl", reasoned);
+
+ OWLOntology reasoned2 = loadOntology("/redundant_subclasses.owl");
+
+ Map options2 = new HashMap();
+ options2.put("remove-redundant-subclass-axioms", "true");
+ options2.put("preserve-annotated-axioms", "true");
+ options2.put("named-classes-only", "true");
+
+ ReduceOperation.reduce(reasoned2, reasonerFactory, options2);
+ assertIdentical("/without_redundant_subclasses.owl", reasoned2);
}
@Test
@@ -156,4 +166,22 @@ public void testReduceDomainCase() throws IOException, OWLOntologyCreationExcept
ReduceOperation.reduce(reasoned, reasonerFactory, options);
assertIdentical("/reduce-domain-test.owl", reasoned);
}
+
+ /** Test reduce only named classes vs. including expressions */
+ @Test
+ public void testReducedNamedOnly() throws OWLOntologyCreationException, IOException {
+ OWLReasonerFactory reasonerFactory = new org.semanticweb.elk.owlapi.ElkReasonerFactory();
+
+ OWLOntology ontologyA = loadOntology("/reduce-named-only-test.ofn");
+ Map optionsA = new HashMap();
+ optionsA.put("named-classes-only", "true");
+ ReduceOperation.reduce(ontologyA, reasonerFactory, optionsA);
+ assertIdentical("/reduce-named-only-test-named-only-true-reduced.ofn", ontologyA);
+
+ OWLOntology ontologyB = loadOntology("/reduce-named-only-test.ofn");
+ Map optionsB = new HashMap();
+ optionsB.put("named-classes-only", "false");
+ ReduceOperation.reduce(ontologyB, reasonerFactory, optionsB);
+ assertIdentical("/reduce-named-only-test-named-only-false-reduced.ofn", ontologyB);
+ }
}
diff --git a/robot-core/src/test/resources/reduce-named-only-test-named-only-false-reduced.ofn b/robot-core/src/test/resources/reduce-named-only-test-named-only-false-reduced.ofn
new file mode 100644
index 000000000..ae01b02f7
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test-named-only-false-reduced.ofn
@@ -0,0 +1,38 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+SubClassOf( ObjectSomeValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+
+# Class: ()
+
+SubClassOf( )
+
+# Class: ()
+
+SubClassOf( )
+
+
+)
\ No newline at end of file
diff --git a/robot-core/src/test/resources/reduce-named-only-test-named-only-true-reduced.ofn b/robot-core/src/test/resources/reduce-named-only-test-named-only-true-reduced.ofn
new file mode 100644
index 000000000..51735e61d
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test-named-only-true-reduced.ofn
@@ -0,0 +1,40 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+SubClassOf( ObjectSomeValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+SubClassOf( ObjectSomeValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+
+# Class: ()
+
+SubClassOf( )
+SubClassOf( ObjectSomeValuesFrom( ))
+
+
+)
\ No newline at end of file
diff --git a/robot-core/src/test/resources/reduce-named-only-test.ofn b/robot-core/src/test/resources/reduce-named-only-test.ofn
new file mode 100644
index 000000000..1f29cc8a3
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test.ofn
@@ -0,0 +1,41 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+SubClassOf( ObjectSomeValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+SubClassOf( ObjectSomeValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+
+# Class: ()
+
+SubClassOf( )
+SubClassOf( )
+SubClassOf( ObjectSomeValuesFrom( ))
+
+
+)
\ No newline at end of file