Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more efficient mode for reduce when only considering named classes. #619

Merged
merged 1 commit into from
Feb 7, 2020
Merged
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
9 changes: 9 additions & 0 deletions docs/reduce.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
138 changes: 118 additions & 20 deletions robot-core/src/main/java/org/obolibrary/robot/ReduceOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,15 +24,15 @@
*
* <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
* reasoner tests, we can then substitute C for C'
*
* <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,
Expand All @@ -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;
}

Expand All @@ -94,6 +81,27 @@ public static void reduce(OWLOntology ontology, OWLReasonerFactory reasonerFacto
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();
Expand Down Expand Up @@ -166,7 +174,7 @@ public static void reduce(

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;
Expand Down Expand Up @@ -254,6 +262,7 @@ public static void reduce(
for (OWLAxiom ax : rmAxioms) {
manager.removeAxiom(ontology, ax);
}
reasoner.dispose();
}

/**
Expand All @@ -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<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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
Expand Down Expand Up @@ -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<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);
}
}
Original file line number Diff line number Diff line change
@@ -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>)


)
Original file line number Diff line number Diff line change
@@ -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>))


)
41 changes: 41 additions & 0 deletions robot-core/src/test/resources/reduce-named-only-test.ofn
Original file line number Diff line number Diff line change
@@ -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>))


)