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 @@ * *

Implementation

* - * Because an OWL reasoner will only return named (non-anonymous) superclasses, we add a + *

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 @@ * *

GENERAL CLASS INCLUSION AXIOMS

* - * We make a special additional case of redunancy, as in the following example: + *

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