Repository: ontodev/robot
Pull request #619 (2020-02-07 02:08:29)

Add more efficient mode for `reduce` when only considering named classes.
This is prompted by a failure GO had when trying to update to a newer ROBOT version. The `reduce` command was failing to complete on the large go-lego ontology. This additional implementation walks the reasoner taxonomy, and so is more efficient than the previous approach, but it is also limited to only considering subclass axioms between named classes (there are some corner cases where the approach of generating a name for the anonymous expressions doesn't work with the new algorithm). There is a new command-line option `--named-classes-only` to engage the faster version.


Patch:
diff --git a/docs/reduce.md b/docs/reduce.md
index 9d5fb601..aef57c25 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/docs/remove.md b/docs/remove.md
index 731f877f..1dbe0705 100644
--- a/docs/remove.md
+++ b/docs/remove.md
@@ -5,7 +5,7 @@
 1. [Overview](#overview)
 2. [Preserving the Structure (`--preserve-structure`)](#preserving-the-structure)
 2. [Selectors (`--select`)](#selectors)
-3. [Axioms (`--axioms`)](#axioms)
+3. [Axioms (`--aximos`)](#axioms)
 4. [Examples](#examples)
 
 ## Overview
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 f5d4bcae..1209804b 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 class ReduceCommand implements Command {
         "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 941b3573..8c511bde 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 @@ package org.obolibrary.robot;
 
 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 @@ import org.slf4j.LoggerFactory;
  *
  * <h2>Implementation</h2>
  *
- * Because an OWL reasoner will only return named (non-anonymous) superclasses, we add a
+ * <p>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 <code>
  * EquivalentClasses(C' C)</code>, which is later removed as a post-processing step. When performing
@@ -46,7 +32,7 @@ import org.slf4j.LoggerFactory;
  *
  * <h2>GENERAL CLASS INCLUSION AXIOMS</h2>
  *
- * We make a special additional case of redunancy, as in the following example: <code>
+ * <p>We make a special additional case of redunancy, as in the following example: <code>
  * 1. (hand and part-of some human) SubClassOf part-of some forelimb
  * 2. hand SubClassOf part-of some forelimb
  * </code> 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<String, String> getDefaultOptions() {
     Map<String, String> options = new HashMap<>();
     options.put("preserve-annotated-axioms", "false");
+    options.put("named-classes-only", "false");
     return options;
   }
 
@@ -94,6 +81,27 @@ public class ReduceOperation {
   public static void reduce(
       OWLOntology ontology, OWLReasonerFactory reasonerFactory, Map<String, String> 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 class ReduceOperation {
 
     Set<OWLSubClassOfAxiom> 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 class ReduceOperation {
     for (OWLAxiom ax : rmAxioms) {
       manager.removeAxiom(ontology, ax);
     }
+    reasoner.dispose();
   }
 
   /**
@@ -279,4 +288,93 @@ public class ReduceOperation {
     }
     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<superclass, Map<subclass, axioms>>
+    Map<OWLClass, Map<OWLClass, Set<OWLSubClassOfAxiom>>> assertions = new HashMap<>();
+    Set<OWLSubClassOfAxiom> 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<OWLClass, Set<OWLSubClassOfAxiom>> subMap = assertions.get(superclass);
+        if (!subMap.containsKey(subclass)) {
+          subMap.put(subclass, new HashSet<>());
+        }
+        Set<OWLSubClassOfAxiom> axioms = subMap.get(subclass);
+        axioms.add(ax);
+      }
+    }
+    OWLReasoner reasoner = reasonerFactory.createReasoner(ontology);
+    if (!reasoner.isConsistent()) {
+      logger.info("Ontology is not consistent!");
+      return;
+    }
+    Node<OWLClass> 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<OWLSubClassOfAxiom> nonredundant = new HashSet<>();
+    Set<Node<OWLClass>> 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<OWLClass> node,
+      OWLReasoner reasoner,
+      Map<OWLClass, Map<OWLClass, Set<OWLSubClassOfAxiom>>> assertions,
+      Set<OWLSubClassOfAxiom> nonredundant,
+      Set<Node<OWLClass>> alreadySeen) {
+    if (!alreadySeen.contains(node)) {
+      NodeSet<OWLClass> subclasses = reasoner.getSubClasses(node.getRepresentativeElement(), true);
+      for (OWLClass superclass : node.getEntities()) {
+        for (OWLClass subclass : subclasses.getFlattened()) {
+          if (assertions.containsKey(superclass)) {
+            Map<OWLClass, Set<OWLSubClassOfAxiom>> subclassAxiomsBySubclass =
+                assertions.get(superclass);
+            if (subclassAxiomsBySubclass.containsKey(subclass)) {
+              nonredundant.addAll(subclassAxiomsBySubclass.get(subclass));
+            }
+          }
+        }
+      }
+      alreadySeen.add(node);
+      for (Node<OWLClass> subclassNode : subclasses.getNodes()) {
+        findNonRedundant(subclassNode, reasoner, assertions, nonredundant, alreadySeen);
+      }
+    }
+  }
 }


Test patch:
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 001ac700..022b1be1 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 class ReduceOperationTest extends CoreTest {
 
     ReduceOperation.reduce(reasoned, reasonerFactory, options);
     assertIdentical("/without_redundant_subclasses.owl", reasoned);
+
+    OWLOntology reasoned2 = loadOntology("/redundant_subclasses.owl");
+
+    Map<String, String> options2 = new HashMap<String, String>();
+    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 class ReduceOperationTest extends CoreTest {
     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<String, String> optionsA = new HashMap<String, String>();
+    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<String, String> optionsB = new HashMap<String, String>();
+    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 00000000..ae01b02f
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test-named-only-false-reduced.ofn
@@ -0,0 +1,38 @@
+Prefix(:=<http://robot.obolibrary.org/reduce/test#>)
+Prefix(owl:=<http://www.w3.org/2002/07/owl#>)
+Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
+Prefix(xml:=<http://www.w3.org/XML/1998/namespace>)
+Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
+Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)
+
+
+Ontology(<http://robot.obolibrary.org/reduce/test>
+
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/A>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/B>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/C>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/D>))
+Declaration(ObjectProperty(<http://robot.obolibrary.org/reduce/test/r>))
+
+############################
+#   Classes
+############################
+
+# Class: <http://robot.obolibrary.org/reduce/test/A> (<http://robot.obolibrary.org/reduce/test/A>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/A> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+# Class: <http://robot.obolibrary.org/reduce/test/B> (<http://robot.obolibrary.org/reduce/test/B>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/B> <http://robot.obolibrary.org/reduce/test/A>)
+
+# Class: <http://robot.obolibrary.org/reduce/test/C> (<http://robot.obolibrary.org/reduce/test/C>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/C> <http://robot.obolibrary.org/reduce/test/B>)
+
+# Class: <http://robot.obolibrary.org/reduce/test/D> (<http://robot.obolibrary.org/reduce/test/D>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> <http://robot.obolibrary.org/reduce/test/C>)
+
+
+)
\ 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 00000000..51735e61
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test-named-only-true-reduced.ofn
@@ -0,0 +1,40 @@
+Prefix(:=<http://robot.obolibrary.org/reduce/test#>)
+Prefix(owl:=<http://www.w3.org/2002/07/owl#>)
+Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
+Prefix(xml:=<http://www.w3.org/XML/1998/namespace>)
+Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
+Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)
+
+
+Ontology(<http://robot.obolibrary.org/reduce/test>
+
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/A>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/B>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/C>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/D>))
+Declaration(ObjectProperty(<http://robot.obolibrary.org/reduce/test/r>))
+
+############################
+#   Classes
+############################
+
+# Class: <http://robot.obolibrary.org/reduce/test/A> (<http://robot.obolibrary.org/reduce/test/A>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/A> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+# Class: <http://robot.obolibrary.org/reduce/test/B> (<http://robot.obolibrary.org/reduce/test/B>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/B> <http://robot.obolibrary.org/reduce/test/A>)
+SubClassOf(<http://robot.obolibrary.org/reduce/test/B> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+# Class: <http://robot.obolibrary.org/reduce/test/C> (<http://robot.obolibrary.org/reduce/test/C>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/C> <http://robot.obolibrary.org/reduce/test/B>)
+
+# Class: <http://robot.obolibrary.org/reduce/test/D> (<http://robot.obolibrary.org/reduce/test/D>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> <http://robot.obolibrary.org/reduce/test/C>)
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+
+)
\ 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 00000000..1f29cc8a
--- /dev/null
+++ b/robot-core/src/test/resources/reduce-named-only-test.ofn
@@ -0,0 +1,41 @@
+Prefix(:=<http://robot.obolibrary.org/reduce/test#>)
+Prefix(owl:=<http://www.w3.org/2002/07/owl#>)
+Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
+Prefix(xml:=<http://www.w3.org/XML/1998/namespace>)
+Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
+Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)
+
+
+Ontology(<http://robot.obolibrary.org/reduce/test>
+
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/A>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/B>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/C>))
+Declaration(Class(<http://robot.obolibrary.org/reduce/test/D>))
+Declaration(ObjectProperty(<http://robot.obolibrary.org/reduce/test/r>))
+
+############################
+#   Classes
+############################
+
+# Class: <http://robot.obolibrary.org/reduce/test/A> (<http://robot.obolibrary.org/reduce/test/A>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/A> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+# Class: <http://robot.obolibrary.org/reduce/test/B> (<http://robot.obolibrary.org/reduce/test/B>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/B> <http://robot.obolibrary.org/reduce/test/A>)
+SubClassOf(<http://robot.obolibrary.org/reduce/test/B> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+# Class: <http://robot.obolibrary.org/reduce/test/C> (<http://robot.obolibrary.org/reduce/test/C>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/C> <http://robot.obolibrary.org/reduce/test/B>)
+
+# Class: <http://robot.obolibrary.org/reduce/test/D> (<http://robot.obolibrary.org/reduce/test/D>)
+
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> <http://robot.obolibrary.org/reduce/test/B>)
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> <http://robot.obolibrary.org/reduce/test/C>)
+SubClassOf(<http://robot.obolibrary.org/reduce/test/D> ObjectSomeValuesFrom(<http://robot.obolibrary.org/reduce/test/r> <http://robot.obolibrary.org/reduce/test/C>))
+
+
+)
\ No newline at end of file