diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9cd146f..15b792d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated `duplicate_exact_syonym` [`report`] query to be case-insensitive and ignore synoyms annotated as abbreviation or acronym synonym types [#1179] +- Add `--enforce-obo-format`, `--exclude-named-classes` and `--include-subclass-of` features to relax command [#1060, #1183] ### Fixed - '--annotate-with-source true' does not work with extract --method subset [#1160] diff --git a/docs/examples/relaxed-enforced-obo.owl b/docs/examples/relaxed-enforced-obo.owl new file mode 100644 index 000000000..9ff2582eb --- /dev/null +++ b/docs/examples/relaxed-enforced-obo.owl @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + R + + + + + + + + + + + + + + A (Named Equivalent) + + + + + + + + A1 + + + + + + + + + + + + + + + + + + + B (SubClass example) + + + + + + + + B1 + + + + + + + + B2 + + + + + + + + + + + + + + + + + + + + C (Equivalent Simple Existential) + + + + + + + + C1 (Equivalent Simple Existential) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + E (Complex existential) + + + + + + + + E1 + + + + + + + + E2 + + + + + + + + E3 + + + + + + + + E4 + + + + + + + + E5 + + + + + + + + E6 + + + + + + + diff --git a/docs/examples/relaxed-exclude-named.owl b/docs/examples/relaxed-exclude-named.owl new file mode 100644 index 000000000..b3f54787b --- /dev/null +++ b/docs/examples/relaxed-exclude-named.owl @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + R + + + + + + + + + + + + + + A (Named Equivalent) + + + + + + + + A1 + + + + + + + + + + + + + + + + + + + B (SubClass example) + + + + + + + + B1 + + + + + + + + B2 + + + + + + + + + + + + + + + + + + + + C (Equivalent Simple Existential) + + + + + + + + C1 (Equivalent Simple Existential) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E (Complex existential) + + + + + + + + E1 + + + + + + + + E2 + + + + + + + + E3 + + + + + + + + E4 + + + + + + + + E5 + + + + + + + + E6 + + + + + + + diff --git a/docs/examples/relaxed-include-subclass.owl b/docs/examples/relaxed-include-subclass.owl new file mode 100644 index 000000000..cf990a5b0 --- /dev/null +++ b/docs/examples/relaxed-include-subclass.owl @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + R + + + + + + + + + + + + + + A (Named Equivalent) + + + + + + + + A1 + + + + + + + + + + + + + + + + + + + + + + + + + + B (SubClass example) + + + + + + + + B1 + + + + + + + + B2 + + + + + + + + + + + + + + + + + + + + C (Equivalent Simple Existential) + + + + + + + + C1 (Equivalent Simple Existential) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E (Complex existential) + + + + + + + + E1 + + + + + + + + E2 + + + + + + + + E3 + + + + + + + + E4 + + + + + + + + E5 + + + + + + + + E6 + + + + + + + diff --git a/docs/examples/relaxed2.owl b/docs/examples/relaxed2.owl new file mode 100644 index 000000000..6b228849b --- /dev/null +++ b/docs/examples/relaxed2.owl @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + R + + + + + + + + + + + + + E5 + + + + + + + + + + + + + + C (Equivalent Simple Existential) + + + + + + + + E2 + + + + + + + + E4 + + + + + + + + E6 + + + + + + + + B1 + + + + + + + + C1 (Equivalent Simple Existential) + + + + + + + + + + + + + + + + + + + B (SubClass example) + + + + + + + + + A1 + + + + + + + + B2 + + + + + + + + E1 + + + + + + + + A (Named Equivalent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + E (Complex existential) + + + + + + + + E3 + + + + + + + diff --git a/docs/relax.md b/docs/relax.md index 7ebceea48..8a56e2e6f 100644 --- a/docs/relax.md +++ b/docs/relax.md @@ -1,5 +1,14 @@ # Relax +## Contents + +1. [Overview](#overview) +1. [Exclude named classes from relax (`--exclude-named-classes`)](#exclude-named-classes) +1. [Relax subclass of axioms in addition to equivalents (`--include-subclass-of`)](#include-subclass-of) +1. [Enforce OBO format (`--enforce-obo-format`)](#enforce-obo-format) + +## Overview + Robot can be used to relax Equivalence Axioms to weaker SubClassOf axioms. The resulting axioms will be redundant with the stronger equivalence axioms, but may be useful for applications that only consume SubClassOf axioms Example: @@ -58,6 +67,75 @@ Running `reduce` will remove the redundant axiom (5), leaving the following axio This SubClassOf graph is complete and non-redundant, and can be used for intuitive visualization and browsing of the ontology +## Note about normalization of Qualified Number restrictions + +For convenience of downstream processing, `relax` rewrites expressions of the kind `:R min 1 :A` or `:R min 2 :A` to `:R some :A`. This is safe, because `:R some :A` is implied by any cardinality restriction > 0. + + + +## Exclude named classes (`--exclude-named-classes/-x`) + +By default, axioms of the form: + +``` +:A EquivalentTo :B +``` + +where `:A` and `:B` are named classes are not relaxed to be: + +``` +:A SubClassOf :B +:B SubClassOf :A +``` + +In some cases, this may be desired; in these cases, the `-x/--exclude-named-classes` can be set to `false`. + +Example to ensure that named classes are not relaxed (this is the default): + + robot relax --input relaxed2.owl --exclude-named-classes true --output results/relaxed-exclude-named.owl + + + +## Relax subclass of axioms (`--include-subclass-of/-s`) + +By default, relax is only concerned with relaxing `EquivalentClasses` axioms. However, some of the magic of the `relax` commmand is the simplification of complex conjunctive expressions, for example, as described above: + +``` +finger EquivalentTo digit and 'part of' some hand +``` +is relaxed to: +``` +finger SubClassOf digit +finger SubClassOf 'part of' some hand +``` + +In many cases it makes sense to also relax `SubClassOf` axioms this way. For example: + +``` +finger SubClassOf: digit and 'part of' some hand +``` + +can be relaxed to: + +``` +finger SubClassOf digit +finger SubClassOf 'part of' some hand +``` + +This can be achieved by setting the `--include-subclass-of` option to `true`. + +Example: + + robot relax --input relaxed2.owl --include-subclass-of true --output results/relaxed-include-subclass.owl + + + +## Enforce OBO format (`--enforce-obo-format/-x`) + +OBO format is a widely used representation for OBO ontologies. Its "graphy" nature lends itself as a simple intermediate towards graph-like representation of ontologies, such as obo-graphs JSON. For many use cases, we do not wish to assert non-graphy expressions such as `:R only :B` or `:A or :B`, expressions with nested sub-expressions such as `:R some (:S some B)` or similar. In cases where we only want to assert expressions that are simple existential restrictions, we can use the `--enforce-obo-format` option. This will process complex expressions (that potentially include _some_ complex and some _simple_ sub-expression), but only assert relaxed statements if they correspind to simple subexpression. + +Example: + robot relax --input relaxed2.owl --enforce-obo-format true --output results/relaxed-enforced-obo.owl diff --git a/robot-command/src/main/java/org/obolibrary/robot/RelaxCommand.java b/robot-command/src/main/java/org/obolibrary/robot/RelaxCommand.java index 423d809ac..655530fa4 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/RelaxCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/RelaxCommand.java @@ -1,6 +1,5 @@ package org.obolibrary.robot; -import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.semanticweb.owlapi.model.OWLOntology; @@ -24,6 +23,21 @@ public RelaxCommand() { Options o = CommandLineHelper.getCommonOptions(); o.addOption("i", "input", true, "relax ontology from a file"); o.addOption("I", "input-iri", true, "relax ontology from an IRI"); + o.addOption( + null, + "enforce-obo-format", + true, + "if true, only axioms allowed in OBO format are asserted as a consequence of relax."); + o.addOption( + null, + "exclude-named-classes", + true, + "if true, equivalent class axioms between named classes are ignored during processing."); + o.addOption( + "s", + "include-subclass-of", + true, + "if true, equivalent class axioms between named classes are ignored during processing."); o.addOption("o", "output", true, "save relaxed ontology to a file"); options = o; } @@ -100,15 +114,13 @@ public CommandState execute(CommandState state, String[] args) throws Exception state = CommandLineHelper.updateInputOntology(ioHelper, state, line); OWLOntology ontology = state.getOntology(); - // Override default reasoner options with command-line options - Map relaxOptions = RelaxOperation.getDefaultOptions(); - for (String option : relaxOptions.keySet()) { - if (line.hasOption(option)) { - relaxOptions.put(option, line.getOptionValue(option)); - } - } + boolean enforceOboFormat = CommandLineHelper.getBooleanValue(line, "enforce-obo-format", false); + boolean excludeNamedClasses = + CommandLineHelper.getBooleanValue(line, "exclude-named-classes", true); + boolean includeSubclassOf = + CommandLineHelper.getBooleanValue(line, "include-subclass-of", false); - RelaxOperation.relax(ontology, relaxOptions); + RelaxOperation.relax(ontology, enforceOboFormat, excludeNamedClasses, includeSubclassOf); CommandLineHelper.maybeSaveOutput(line, ontology); diff --git a/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java b/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java index ab05ba031..51138dce4 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java @@ -17,6 +17,7 @@ import org.semanticweb.owlapi.model.OWLObjectSomeValuesFrom; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.OWLOntologyManager; +import org.semanticweb.owlapi.model.OWLSubClassOfAxiom; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,6 +90,34 @@ public static Map getDefaultOptions() { * @param options A map of options for the operation */ public static void relax(OWLOntology ontology, Map options) { + relax(ontology); + } + + /** + * Replace EquivalentClass axioms with weaker SubClassOf axioms. + * + * @param ontology The OWLOntology to relax + */ + public static void relax(OWLOntology ontology) { + relax(ontology, false, false, false); + } + + /** + * Replace EquivalentClass axioms with weaker SubClassOf axioms. + * + * @param ontology The OWLOntology to relax + * @param enforceOboFormat if true, only axioms allowed in OBO format are asserted as a + * consequence of relax + * @param excludeNamedClasses if true, equivalent class axioms between named classes are ignored + * during processing + * @param includeSubclassOf if true, equivalent class axioms between named classes are ignored + * during processing + */ + public static void relax( + OWLOntology ontology, + boolean enforceOboFormat, + boolean excludeNamedClasses, + boolean includeSubclassOf) { OWLOntologyManager manager = OWLManager.createOWLOntologyManager(); OWLDataFactory dataFactory = manager.getOWLDataFactory(); @@ -106,21 +135,28 @@ public static void relax(OWLOntology ontology, Map options) { OWLClass c = (OWLClass) x; // ax = EquivalentClasses(x y1 y2 ...) for (OWLClassExpression y : ax.getClassExpressionsMinus(c)) { - // limited structural reasoning: - // return (P some Z), if: - // - y is of the form (P some Z) - // - y is of the form ((P some Z) and ...), - // or any level of nesting - for (OWLObjectSomeValuesFrom svf : getSomeValuesFromAncestor(y, dataFactory)) { - newAxioms.add(dataFactory.getOWLSubClassOfAxiom(c, svf)); - } - for (OWLClass z : getNamedAncestors(y)) { - newAxioms.add(dataFactory.getOWLSubClassOfAxiom(c, z)); + // if we are excluding equivalents between named classes, skip + if (!y.isAnonymous() && excludeNamedClasses) { + continue; } + relaxExpression(c, y, newAxioms, enforceOboFormat, dataFactory); } } } } + if (includeSubclassOf) { + Set subClassAxioms = ontology.getAxioms(AxiomType.SUBCLASS_OF); + for (OWLSubClassOfAxiom ax : subClassAxioms) { + OWLClassExpression subClass = ax.getSubClass(); + OWLClassExpression superClass = ax.getSuperClass(); + // we only relax in cases where the subclass is a named class + // and the superclass a complex expression + if (!subClass.isAnonymous() && superClass.isAnonymous()) { + OWLClass namedSubClass = (OWLClass) subClass; + relaxExpression(namedSubClass, superClass, newAxioms, enforceOboFormat, dataFactory); + } + } + } // remove redundant axiom for (OWLAxiom ax : newAxioms) { @@ -129,6 +165,26 @@ public static void relax(OWLOntology ontology, Map options) { } } + private static void relaxExpression( + OWLClass namedSubClass, + OWLClassExpression anonymousSuperClass, + Set newAxioms, + boolean enforceOboFormat, + OWLDataFactory dataFactory) { + // limited structural reasoning: + // return (P some Z), if: + // - y is of the form (P some Z) + // - y is of the form ((P some Z) and ...), + // or any level of nesting + for (OWLObjectSomeValuesFrom svf : + getSomeValuesFromAncestor(anonymousSuperClass, enforceOboFormat, dataFactory)) { + newAxioms.add(dataFactory.getOWLSubClassOfAxiom(namedSubClass, svf)); + } + for (OWLClass z : getNamedAncestors(anonymousSuperClass)) { + newAxioms.add(dataFactory.getOWLSubClassOfAxiom(namedSubClass, z)); + } + } + /** * Given an OWLClassExpression y, return a set of OWLObjectSomeValuesFrom objects (p some v), * where (p some v) is a superclass of y. @@ -140,11 +196,13 @@ public static void relax(OWLOntology ontology, Map options) { * @return the set of OWLObjectSomeValuesFrom objects */ private static Set getSomeValuesFromAncestor( - OWLClassExpression x, OWLDataFactory dataFactory) { + OWLClassExpression x, boolean enforceOboFormat, OWLDataFactory dataFactory) { Set svfs = new HashSet<>(); if (x instanceof OWLObjectSomeValuesFrom) { OWLObjectSomeValuesFrom svf = (OWLObjectSomeValuesFrom) x; - svfs.add(svf); + if (!enforceOboFormat || isOboFormatConformant(svf)) { + svfs.add(svf); + } } else if (x instanceof OWLObjectCardinalityRestriction) { OWLObjectCardinalityRestriction ocr = (OWLObjectCardinalityRestriction) x; @@ -152,18 +210,24 @@ private static Set getSomeValuesFromAncestor( OWLObjectPropertyExpression p = ocr.getProperty(); if (ocr.getCardinality() > 0) { OWLObjectSomeValuesFrom svf = dataFactory.getOWLObjectSomeValuesFrom(p, filler); - svfs.add(svf); + if (!enforceOboFormat || isOboFormatConformant(svf)) { + svfs.add(svf); + } } } else if (x instanceof OWLObjectIntersectionOf) { for (OWLClassExpression op : ((OWLObjectIntersectionOf) x).getOperands()) { - svfs.addAll(getSomeValuesFromAncestor(op, dataFactory)); + svfs.addAll(getSomeValuesFromAncestor(op, enforceOboFormat, dataFactory)); } } return svfs; } + private static boolean isOboFormatConformant(OWLObjectSomeValuesFrom svf) { + return !svf.getProperty().isAnonymous() && !svf.getFiller().isAnonymous(); + } + /** * Given an OWLClassExpression y, return a set of named classes c, such that c is a superclass of * y,