Repository: ontodev/robot (java)

Issue:
Relax operation is currently incomplete
See also #7 

Currently relax only inspects and materializes SomeValuesFrom. This is problematic in the following scenario:

Given:

```
'craniofacial suture' EquivalentTo 'fibrous joint' and connects exactly 2 'head bone'
```

with no other logical axioms for craniofacial suture. Assume also that Elk is used.

The axiom will be invisible to Elk, to `reason` will produce no superclasses.

Currently, when we pass this to `relax` it produces nothing, because the QCR pattern is not handled. Also we do not relax named superclasses.

Possible candidates for relax to produce.

 1. SubClassOf 'fibrous joint'
 2. SubClassOf connects some 'head bone'
 3. SubClassOf connects exactly 2 'head bone'

These are *not* guaranteed complete. To get complete inferences a DL reasoner must be used. However, we assume this is not possible here so we use relaxation to produce useful if incomplete final axioms, either directly, or as input for EL reasoning.

All of these are valid. 1 and 2 are required for producing a complete basic existential graph.


Pull request description:
Relax operation now unwinds cardinality constraints,
as well as adding trivial named superclasses. Fixes #135

Patch:
diff --git a/examples/relaxed.owl b/examples/relaxed.owl
index 6b0bdb3f..51f766cd 100644
--- a/examples/relaxed.owl
+++ b/examples/relaxed.owl
@@ -55,6 +55,7 @@
                 </owl:intersectionOf>
             </owl:Class>
         </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="http://purl.obolibrary.org/obo/GO_0005840"/>
         <rdfs:subClassOf>
             <owl:Restriction>
                 <owl:onProperty rdf:resource="http://purl.obolibrary.org/obo/BFO_0000050"/>
@@ -183,6 +184,7 @@
                 </owl:intersectionOf>
             </owl:Class>
         </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="http://purl.obolibrary.org/obo/GO_0005575"/>
         <rdfs:subClassOf>
             <owl:Restriction>
                 <owl:onProperty rdf:resource="http://purl.obolibrary.org/obo/BFO_0000050"/>
@@ -208,6 +210,7 @@
                 </owl:intersectionOf>
             </owl:Class>
         </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="http://purl.obolibrary.org/obo/GO_0005575"/>
         <rdfs:subClassOf>
             <owl:Restriction>
                 <owl:onProperty rdf:resource="http://purl.obolibrary.org/obo/BFO_0000050"/>
@@ -233,6 +236,7 @@
                 </owl:intersectionOf>
             </owl:Class>
         </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="http://purl.obolibrary.org/obo/GO_0005575"/>
         <rdfs:subClassOf>
             <owl:Restriction>
                 <owl:onProperty rdf:resource="http://purl.obolibrary.org/obo/BFO_0000050"/>
@@ -258,6 +262,7 @@
                 </owl:intersectionOf>
             </owl:Class>
         </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="http://purl.obolibrary.org/obo/GO_0005575"/>
         <rdfs:subClassOf>
             <owl:Restriction>
                 <owl:onProperty rdf:resource="http://purl.obolibrary.org/obo/BFO_0000050"/>
@@ -279,5 +284,5 @@
 
 
 
-<!-- Generated by the OWL API (version 0.0.1-SNAPSHOT) https://github.com/owlcs/owlapi -->
+<!-- Generated by the OWL API (version 4.2.6) https://github.com/owlcs/owlapi -->
 
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 d6b4ac03..d361cf26 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/RelaxOperation.java
@@ -10,19 +10,27 @@ 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.OWLCardinalityRestriction;
 import org.semanticweb.owlapi.model.OWLClass;
 import org.semanticweb.owlapi.model.OWLClassExpression;
 import org.semanticweb.owlapi.model.OWLDataFactory;
 import org.semanticweb.owlapi.model.OWLEquivalentClassesAxiom;
+import org.semanticweb.owlapi.model.OWLObjectCardinalityRestriction;
 import org.semanticweb.owlapi.model.OWLObjectIntersectionOf;
+import org.semanticweb.owlapi.model.OWLObjectPropertyExpression;
 import org.semanticweb.owlapi.model.OWLObjectSomeValuesFrom;
 import org.semanticweb.owlapi.model.OWLOntology;
 import org.semanticweb.owlapi.model.OWLOntologyManager;
+import org.semanticweb.owlapi.model.OWLQuantifiedObjectRestriction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Add additional SubClassOf axioms that are relaxed forms of equivalence axioms
+ * Add additional SubClassOf axioms that are relaxed forms of equivalence axioms.
+ * 
+ * This is a form of incomplete reasoning that is useful when either axioms are
+ * invisible to EL reasoners, or to produce an existential subclass graph that
+ * has SomeValuesFrom superclasses materialized.
  *
  * <h2>Motivation</h2>
  *
@@ -53,8 +61,28 @@ import org.slf4j.LoggerFactory;
  *    ...
  *    C SubClassOf X_n
  * </code>
+ * 
+ * Additionally, if X_i is a qualified cardinality constraint,
+ * weaken to an existential
+ * 
+ * <h2>Usage</h2>
+ * 
+ * For ELK reasoners, it is recommended to use this prior to the reasoning step, as relaxation can
+ * reveal weaker forms of axioms that are outside EL.
+ * 
+ * It is recommended that the reduce operation is executed after relaxation, to remove
+ * any redundancies within the subclass graph
+ * 
+ * Note that the materialize operation may seem to make the relax step pointless; materialization
+ * produces the most specific existential parent. However, in some cases relaxation should still
+ * be performed:
+ *  - if Elk is used (see above)
+ *  - if materialization is only performed on a subset of properties
+ *  
+ * The standard sequence is: relax-materialize-reduce, or relax-reason-reduce
  *
  * @see <a href="https://github.com/ontodev/robot/issues/7">issue 7</a>
+ * @see <a href="https://github.com/ontodev/robot/issues/135">issue 135</a>
  *
  * @author <a href="mailto:cjmungall@lbl.gov">Chris Mungall</a>
  *
@@ -99,14 +127,28 @@ public class RelaxOperation {
 
         for (OWLEquivalentClassesAxiom ax : eqAxioms) {
             for (OWLClassExpression x : ax.getClassExpressions()) {
+                
+                // we only relax in cases where the equivalence is between one named
+                // and one anon expression
                 if (!x.isAnonymous()) {
                     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)) {
+                                : getSomeValuesFromAncestor(y, dataFactory)) {
                             newAxioms.add(
                               dataFactory.getOWLSubClassOfAxiom(c, svf));
                         }
+                        for (OWLClass z
+                                : getNamedAncestors(y)) {
+                            newAxioms.add(
+                              dataFactory.getOWLSubClassOfAxiom(c, z));
+                        }
                     }
                 }
             }
@@ -121,51 +163,74 @@ public class RelaxOperation {
     }
 
     /**
-     * Given an OWLClassExpression, return a set of
-     * OWLObjectSomeValuesFrom objects.
+     * Given an OWLClassExpression y, return a set of
+     * OWLObjectSomeValuesFrom objects (p some v), where
+     * (p some v) is a superclass of y
+     * 
+     * Not guaranteed to be complete
      *
      * @param x The OWLClassExpression to check.
+     * @param dataFactory 
      * @return the set of OWLObjectSomeValuesFrom objects
      */
     private static Set<OWLObjectSomeValuesFrom> getSomeValuesFromAncestor(
-            OWLClassExpression x) {
+            OWLClassExpression x, OWLDataFactory dataFactory) {
         Set<OWLObjectSomeValuesFrom> svfs = new HashSet<>();
         if (x instanceof OWLObjectSomeValuesFrom) {
             OWLObjectSomeValuesFrom svf = (OWLObjectSomeValuesFrom) x;
             svfs.add(svf);
-        } else if (x instanceof OWLObjectIntersectionOf) {
+        }
+        else if (x instanceof OWLObjectCardinalityRestriction) {
+            OWLObjectCardinalityRestriction ocr = (OWLObjectCardinalityRestriction) x;
+            OWLClassExpression filler = ocr.getFiller();
+            OWLObjectPropertyExpression p = ocr.getProperty();
+            if (ocr.getCardinality() > 0) {
+                OWLObjectSomeValuesFrom svf = 
+                        dataFactory.getOWLObjectSomeValuesFrom(p, filler);
+                svfs.add(svf);
+            }
+           
+        }
+        else if (x instanceof OWLObjectIntersectionOf) {
             for (OWLClassExpression op
                     : ((OWLObjectIntersectionOf) x).getOperands()) {
-                svfs.addAll(getSomeValuesFromAncestor(op));
+                svfs.addAll(getSomeValuesFromAncestor(op, dataFactory));
             }
         }
         return svfs;
     }
-
+    
     /**
-     * Map a class expression to a class.
+     * Given an OWLClassExpression y, return a set of
+     * named classes c, 
+     * such that c is a superclass of y,
+     * 
+     * obtained by relaxing/unwinding expression, in a way
+     * that is guaranteed valid but not guaranteed complete.
+     * 
+     * This is effectively poor-mans reasoning over IntersectionOf; e.g
+     * 
+     * <pre>
+     * C SubClassOf (C and ...) 
+     * </pre>
      *
-     * @param dataFactory A datafactory for creating the mapped class expression
-     * @param rxmap A map from class expressions to classes
-     * @param x The OWLClassExpression to map
-     * @return the mapped OWLClass
+     * @param x The OWLClassExpression to unwind.
+     * @return the set of OWLClass superclasses
      */
-    private static OWLClass mapClass(OWLDataFactory dataFactory,
-            Map<OWLClassExpression, OWLClass> rxmap, OWLClassExpression x) {
-        if (!rxmap.containsKey(x)) {
-            if (x.isAnonymous()) {
-                UUID uuid = UUID.randomUUID();
-                OWLClass c = dataFactory.getOWLClass(
-                    IRI.create("urn:uuid" + uuid.toString()));
-                logger.info(c + " ==> " + x);
-                rxmap.put(x, c);
-            } else {
-                rxmap.put(x, (OWLClass) x);
+    private static Set<OWLClass> getNamedAncestors(
+            OWLClassExpression x) {
+        Set<OWLClass> cs = new HashSet<>();
+        if (!x.isAnonymous()) {
+            cs.add(x.asOWLClass());
+        } else if (x instanceof OWLObjectIntersectionOf) {
+            for (OWLClassExpression op
+                    : ((OWLObjectIntersectionOf) x).getOperands()) {
+                cs.addAll(getNamedAncestors(op));
             }
-
         }
-        return rxmap.get(x);
+        return cs;
     }
 
 
+
 }


Test patch:
diff --git a/robot-core/src/test/java/org/obolibrary/robot/MaterializeOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/MaterializeOperationTest.java
index 5571a4b9..a5d8428d 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/MaterializeOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/MaterializeOperationTest.java
@@ -27,7 +27,6 @@ public class MaterializeOperationTest extends CoreTest {
     /**
      * Test reasoning with Expression Materializing Reasoner.
      *
-     * This test effectively relaxes an equivalence axiom
      *
      * @throws IOException on file problem
      * @throws OWLOntologyCreationException on ontology problem
diff --git a/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java
index 99deb19f..8e3466d4 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java
@@ -10,6 +10,7 @@ import org.geneontology.reasoner.ExpressionMaterializingReasonerFactory;
 import org.junit.Test;
 import org.obolibrary.robot.exceptions.OntologyLogicException;
 import org.semanticweb.elk.owlapi.ElkReasonerFactory;
+import org.semanticweb.owlapi.model.OWLAxiom;
 import org.semanticweb.owlapi.model.OWLOntology;
 import org.semanticweb.owlapi.model.OWLOntologyCreationException;
 import org.semanticweb.owlapi.reasoner.OWLReasonerFactory;
@@ -118,18 +119,21 @@ public class ReasonOperationTest extends CoreTest {
         OWLReasonerFactory reasonerFactory = new org.semanticweb
             .HermiT.Reasoner.ReasonerFactory();
         Map<String, String> opts = new HashMap<>();
+        
         // see https://github.com/ontodev/robot/issues/80
         opts.put("create-new-ontology", "true");
-        // see https://github.com/ontodev/robot/issues/80
         opts.put("annotate-inferred-axioms", "true");
 
         ReasonOperation.reason(reasoned, reasonerFactory, opts);
-        assertEquals(3, reasoned.getAxiomCount());
+
+        // note that some of the inferred axioms are trivial
+        // involving owl:Thing
+        assertEquals(15, reasoned.getAxiomCount());
         //assertIdentical("/simple_hermit.owl", reasoned);
     }
 
     /**
-     * Test inferring into new ontology.
+     * Test inferring into new ontology, excluding duplicates
      *
      * @throws IOException on file problem
      * @throws OWLOntologyCreationException, OntologyLogicException on ontology problem
@@ -146,7 +150,7 @@ public class ReasonOperationTest extends CoreTest {
         opts.put("annotate-inferred-axioms", "true");
         opts.put("exclude-duplicate-axioms", "true");
         ReasonOperation.reason(reasoned, reasonerFactory, opts);
-        assertEquals(1, reasoned.getAxiomCount());
+        assertEquals(5, reasoned.getAxiomCount());
         //assertIdentical("/simple_hermit.owl", reasoned);
     }
 
diff --git a/robot-core/src/test/java/org/obolibrary/robot/RelaxOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/RelaxOperationTest.java
index 6a8df8cc..e945cae6 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/RelaxOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/RelaxOperationTest.java
@@ -14,7 +14,7 @@ import org.semanticweb.owlapi.model.OWLOntology;
 public class RelaxOperationTest extends CoreTest {
 
     /**
-     * Test removing redundant subclass axioms.
+     * Test relaxation, basic case
      *
      * @throws IOException on file problem
      */
diff --git a/robot-core/src/test/resources/relax_equivalence_axioms_expressions_materialized.obo b/robot-core/src/test/resources/relax_equivalence_axioms_expressions_materialized.obo
index e033e340..b6dff386 100644
--- a/robot-core/src/test/resources/relax_equivalence_axioms_expressions_materialized.obo
+++ b/robot-core/src/test/resources/relax_equivalence_axioms_expressions_materialized.obo
@@ -7,8 +7,34 @@ intersection_of: X:2
 intersection_of: part_of X:3
 relationship: part_of X:3
 
+!! note in the following, there is no materialized
+!! superclasses; this is because cardinality axioms
+!! are invisible to Elk
+[Term]
+id: X:4
+intersection_of: X:5
+intersection_of: has_component X:6 {cardinality="2"}
+
+[Term]
+id: X:14
+intersection_of: X:15
+intersection_of: has_component X:16 {minCardinality="2"}
+
+[Term]
+id: X:24
+intersection_of: X:25
+intersection_of: has_component X:26 {maxCardinality="2"}
+
+[Term]
+id: X:34
+intersection_of: X:35
+intersection_of: has_component X:36 {maxCardinality="0"}
+
 [Typedef]
 id: part_of
 xref: BFO:0000050
 is_transitive: true
 
+[Typedef]
+id: has_component
+xref: RO:0002180
diff --git a/robot-core/src/test/resources/relax_equivalence_axioms_relaxed.obo b/robot-core/src/test/resources/relax_equivalence_axioms_relaxed.obo
index 26fc4879..8c6223d9 100644
--- a/robot-core/src/test/resources/relax_equivalence_axioms_relaxed.obo
+++ b/robot-core/src/test/resources/relax_equivalence_axioms_relaxed.obo
@@ -2,12 +2,46 @@ ontology: relax
 
 [Term]
 id: X:1
+is_a: X:2
 intersection_of: X:2
 intersection_of: part_of X:3
 relationship: part_of X:3
 
+!! Note that relaxed axioms are generated, even
+!! though the cardinality axiom is invisible to Elk
+[Term]
+id: X:4
+is_a: X:5
+intersection_of: X:5
+intersection_of: has_component X:6 {cardinality="2"}
+relationship: has_component X:6
+
+[Term]
+id: X:14
+is_a: X:15
+intersection_of: X:15
+intersection_of: has_component X:16 {minCardinality="2"}
+relationship: has_component X:16
+
+[Term]
+id: X:24
+is_a: X:25
+intersection_of: X:25
+intersection_of: has_component X:26 {maxCardinality="2"}
+relationship: has_component X:26
+
+[Term]
+id: X:34
+is_a: X:35
+intersection_of: X:35
+intersection_of: has_component X:36 {maxCardinality="0"}
+
 [Typedef]
 id: part_of
 xref: BFO:0000050
 is_transitive: true
 
+[Typedef]
+id: has_component
+xref: RO:0002180
+
diff --git a/robot-core/src/test/resources/relax_equivalence_axioms_test.obo b/robot-core/src/test/resources/relax_equivalence_axioms_test.obo
index 7b88654f..9751e1f8 100644
--- a/robot-core/src/test/resources/relax_equivalence_axioms_test.obo
+++ b/robot-core/src/test/resources/relax_equivalence_axioms_test.obo
@@ -5,8 +5,36 @@ id: X:1
 intersection_of: X:2
 intersection_of: part_of X:3
 
+!! Note that cardinality axioms are invisible to Elk.
+!! The relax operation will unwind these into weaker
+!! but still valid SubClassOf axioms, utilizing only
+!! named classes and SomeValuesFrom expressions
+[Term]
+id: X:4
+intersection_of: X:5
+intersection_of: has_component X:6 {cardinality="2"}
+
+[Term]
+id: X:14
+intersection_of: X:15
+intersection_of: has_component X:16 {minCardinality="2"}
+
+[Term]
+id: X:24
+intersection_of: X:25
+intersection_of: has_component X:26 {maxCardinality="2"}
+
+[Term]
+id: X:34
+intersection_of: X:35
+intersection_of: has_component X:36 {maxCardinality="0"}
+
 [Typedef]
 id: part_of
 xref: BFO:0000050
 is_transitive: true
 
+[Typedef]
+id: has_component
+xref: RO:0002180
+