Repository: ontodev/robot
Pull request #168 (2017-05-10 18:34:20)

made reason command -e take one of 3 options for Chris' new equivalen…
…ce class detection method. Tests to go with it


Patch:
diff --git a/examples/asserted-equiv.owl b/examples/asserted-equiv.owl
new file mode 100644
index 00000000..3a3fe8c6
--- /dev/null
+++ b/examples/asserted-equiv.owl
@@ -0,0 +1,9 @@
+Prefix: : <http://purl.obolibrary.org/obo/TEST_>
+Ontology: <http://purl.obolibrary.org/obo/test1>
+
+## A==B
+## Will also trivially infer B==A, but this is the same.
+Class: A
+  EquivalentTo: B
+
+Class: B
diff --git a/examples/inferred-equiv.owl b/examples/inferred-equiv.owl
new file mode 100644
index 00000000..d9e6be81
--- /dev/null
+++ b/examples/inferred-equiv.owl
@@ -0,0 +1,15 @@
+Prefix: : <http://purl.obolibrary.org/obo/TEST_>
+Ontology: <http://purl.obolibrary.org/obo/test1>
+
+## A and B will be inferred to be equivalent.
+## This is a new inference, not originally asserted.
+## This is a red flag as such equivalencies are often unintentional
+Class: A
+  EquivalentTo: C and D
+
+Class: B
+  EquivalentTo: C and D
+
+Class: C
+
+Class: D
diff --git a/pom.xml b/pom.xml
index e31c7309..e0eb7061 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,7 +104,7 @@
                 <configuration>
                     <links>
                         <link>https://commons.apache.org/proper/commons-cli/javadocs/api-1.2/</link>
-                        <link>http://owlcs.github.io/owlapi/apidocs_4/</link>
+                        <link>http://owlapi.sourceforge.net/javadoc/</link>
                     </links>
                 </configuration>
             </plugin>
diff --git a/robot-command/src/main/java/org/obolibrary/robot/ReasonCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ReasonCommand.java
index d3a1668f..d17d44f7 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/ReasonCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/ReasonCommand.java
@@ -45,7 +45,9 @@ public class ReasonCommand implements Command {
         o.addOption("x", "exclude-duplicate-axioms",     true,
                 "do not add an axiom if it exists in import chain");
         o.addOption("e", "equivalent-classes-allowed",     true,
-                "if false then detecting equivalent classes exits with error");
+                "if none then detecting equivalent classes exits with error, if all then " +
+                    "all equivalent classes are allowed, and if asserted-only then only detecting " +
+                    "equivalent classes which are already asserted are allowed, otherwise exit");
         o.addOption("i", "input",     true, "reason ontology from a file");
         o.addOption("I", "input-iri", true, "reason ontology from an IRI");
         o.addOption("o", "output",    true, "save reasoned ontology to a file");
diff --git a/robot-command/src/main/java/org/obolibrary/robot/ValidateProfileCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ValidateProfileCommand.java
index 11a66f6b..254ce25c 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/ValidateProfileCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/ValidateProfileCommand.java
@@ -7,14 +7,13 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Options;
 import org.semanticweb.owlapi.model.OWLOntology;
 import org.semanticweb.owlapi.profiles.OWLProfile;
-import org.semanticweb.owlapi.profiles.OWL2Profile;
 import org.semanticweb.owlapi.profiles.OWLProfileReport;
 import org.semanticweb.owlapi.profiles.Profiles;
 
 public class ValidateProfileCommand implements Command  {
-
+	
 	private final Options options;
-
+	
 	public ValidateProfileCommand() {
         Options o = CommandLineHelper.getCommonOptions();
         o.addOption("o", "output", true, "save validation report to a file");
@@ -46,48 +45,43 @@ public class ValidateProfileCommand implements Command  {
 
 	@Override
 	public void main(String[] args) {
-		try {
-			execute(null, args);
-		} catch (Exception e) {
-			CommandLineHelper.handleException(getUsage(), getOptions(), e);
-		}
-	}
+        try {
+            execute(null, args);
+        } catch (Exception e) {
+            CommandLineHelper.handleException(getUsage(), getOptions(), e);
+        }
+    }
 
 	@Override
 	public CommandState execute(CommandState state, String[] args)
 			throws Exception {
 		CommandLine line = CommandLineHelper
-			.getCommandLine(getUsage(), getOptions(), args);
+				.getCommandLine(getUsage(), getOptions(), args);
 		if (line == null) {
 			return null;
 		}
 		IOHelper ioHelper = CommandLineHelper.getIOHelper(line);
-		state = CommandLineHelper.updateInputOntology(ioHelper, state, line);
-		OWLOntology ontology = state.getOntology();
-		String profile = CommandLineHelper.getRequiredValue(
-			line, "profile", "A --profile is required");
-		profile = profile.toUpperCase();
+        state = CommandLineHelper.updateInputOntology(ioHelper, state, line);
+        OWLOntology ontology = state.getOntology();
+		String profile = CommandLineHelper.getOptionalValue(line, "profile").toUpperCase();
 		final OWLProfile owlProfile;
 		switch(profile) {
-			case "DL": owlProfile = Profiles.OWL2_DL; break;
-			case "EL": owlProfile = Profiles.OWL2_EL; break;
-			case "RL": owlProfile = Profiles.OWL2_RL; break;
-			case "QL": owlProfile = Profiles.OWL2_QL; break;
-			case "FULL": owlProfile = new OWL2Profile(); break; // #162
-			default: throw new Exception("Invalid profile: " + profile);
+		case "DL": owlProfile = Profiles.OWL2_DL; break;
+		case "EL": owlProfile = Profiles.OWL2_EL; break;
+		case "RL": owlProfile = Profiles.OWL2_RL; break;
+		case "QL": owlProfile = Profiles.OWL2_QL; break;
+		case "FULL": owlProfile = Profiles.OWL2_FULL; break;
+		default: owlProfile = null;
 		}
 		OWLProfileReport report = owlProfile.checkOntology(ontology);
 		File outputFile = CommandLineHelper.getOutputFile(line);
 		if (outputFile != null) {
-			FileWriter writer = new FileWriter(outputFile);
-			writer.write(report.toString());
-			writer.close();
-		} else {
+	        FileWriter writer = new FileWriter(outputFile);
+	        writer.write(report.toString());
+	        writer.close();
+	    } else {
 			System.out.println(report.toString());
 		}
-		if (!report.isInProfile()) {
-			throw new Exception("Ontology is not in profile " + profile);
-		}
 		return state;
 	}
 
diff --git a/robot-core/src/main/java/org/obolibrary/robot/MirrorOperation.java b/robot-core/src/main/java/org/obolibrary/robot/MirrorOperation.java
index fcd150f8..2f20ebe8 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/MirrorOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/MirrorOperation.java
@@ -36,11 +36,11 @@ public class MirrorOperation {
     /**
      * Mirrors ontologies on local filesystem
      *
-     * @param rootOntology The ontology to mirror.
-     * @param baseFolder The folder.
-     * @param catalogFile The catalog file to write to.
-     * @throws IOException on most problems.
-     * @throws OWLOntologyStorageException if the ontology cannot be saved.
+     * @param ontologies
+     * @param baseFolder
+     * @param catalogFile
+     * @throws IOException
+     * @throws OWLOntologyStorageException
      */
     public static void mirror(OWLOntology rootOntology,
             File baseFolder, File catalogFile) throws IOException, OWLOntologyStorageException {
@@ -94,9 +94,9 @@ public class MirrorOperation {
     /**
      * Writes a catalog-v001.xml file
      *
-     * @param catalogFile The catalog File to write to.
-     * @param iriMap The IRIs to write.
-     * @throws IOException on any problem.
+     * @param path
+     * @param iriMap
+     * @throws IOException
      */
     public static void writeCatalog(File catalogFile, Map<IRI,String> iriMap) throws IOException {
         List<String> lines = new ArrayList<String>();
diff --git a/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java b/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java
index 55286935..b3a6c04e 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java
@@ -83,7 +83,6 @@ public class QueryOperation {
      * @param dsg The graph to query over.
      * @param query The SPARQL query string.
      * @param output The file to write to.
-     * @param outputFormat The file format.
      * @throws FileNotFoundException if output file is not found
      */
     public static void runQuery(DatasetGraph dsg, String query, File output, Lang outputFormat)
diff --git a/robot-core/src/main/java/org/obolibrary/robot/ReasonOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ReasonOperation.java
index 11b6b44f..d1ede95c 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/ReasonOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/ReasonOperation.java
@@ -1,35 +1,16 @@
 package org.obolibrary.robot;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import org.geneontology.reasoner.ExpressionMaterializingReasoner;
-import org.obolibrary.robot.exceptions.IncoherentRBoxException;
-import org.obolibrary.robot.exceptions.IncoherentTBoxException;
-import org.obolibrary.robot.exceptions.InconsistentOntologyException;
 import org.obolibrary.robot.exceptions.OntologyLogicException;
+import org.obolibrary.robot.reason.EquivalentClassReasoning;
+import org.obolibrary.robot.reason.EquivalentClassReasoningMode;
+import org.semanticweb.owlapi.model.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.semanticweb.owlapi.apibinding.OWLManager;
-import org.semanticweb.owlapi.model.IRI;
-import org.semanticweb.owlapi.model.OWLAnnotationValue;
-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.OWLImportsDeclaration;
-import org.semanticweb.owlapi.model.OWLNamedObject;
-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.RemoveImport;
 import org.semanticweb.owlapi.model.parameters.Imports;
 import org.semanticweb.owlapi.reasoner.InferenceType;
 import org.semanticweb.owlapi.reasoner.Node;
@@ -39,6 +20,8 @@ import org.semanticweb.owlapi.util.InferredAxiomGenerator;
 import org.semanticweb.owlapi.util.InferredOntologyGenerator;
 import org.semanticweb.owlapi.util.InferredSubClassAxiomGenerator;
 
+import static org.obolibrary.robot.reason.EquivalentClassReasoningMode.ALL;
+
 /**
  * Reason over an ontology and add axioms.
  *
@@ -51,7 +34,7 @@ public class ReasonOperation {
     private static final Logger logger =
             LoggerFactory.getLogger(ReasonOperation.class);
 
- 
+
 
     /**
      * Return a map from option name to default option value,
@@ -65,7 +48,7 @@ public class ReasonOperation {
         options.put("create-new-ontology", "false");
         options.put("annotate-inferred-axioms", "false");
         options.put("exclude-duplicate-axioms", "false");
-        options.put("equivalent-classes-allowed", "true");
+        options.put("equivalent-classes-allowed", ALL.written());
 
         return options;
     }
@@ -101,12 +84,12 @@ public class ReasonOperation {
             OWLReasonerFactory reasonerFactory,
             Map<String, String> options) throws OWLOntologyCreationException, OntologyLogicException {
         logger.info("Ontology has {} axioms.", ontology.getAxioms().size());
-        
+
         logger.info("Fetching labels...");
 
-        Function<OWLNamedObject, String> labelFunc = 
+        Function<OWLNamedObject, String> labelFunc =
                 OntologyHelper.getLabelFunction(ontology, false);
-        
+
         int seconds;
         long elapsedTime;
         long startTime = System.currentTimeMillis();
@@ -117,35 +100,18 @@ public class ReasonOperation {
         logger.info("Starting reasoning...");
         OWLReasoner reasoner = reasonerFactory.createReasoner(ontology);
         ReasonerHelper.validate(reasoner);
- 
+
         logger.info("Precomputing class hierarchy...");
         reasoner.precomputeInferences(InferenceType.CLASS_HIERARCHY);
 
-        boolean isEquivalentsAllowed = OptionsHelper.optionIsTrue(options, "equivalent-classes-allowed");
-        int nEquivs = 0;
+        EquivalentClassReasoningMode mode = EquivalentClassReasoningMode.from(options.getOrDefault("equivalent-classes-allowed", ""));
         logger.info("Finding equivalencies...");
-        for (OWLClass c : ontology.getClassesInSignature()) {
-            Set<OWLClass> equivs = 
-                    reasoner.getEquivalentClasses(c).getEntitiesMinus(c);
-            if (equivs.size() > 0) {
-                nEquivs++;
-                String msg = 
-                        "Equivalency: " + c +  " == " + equivs + " // "+
-                                labelFunc.apply(c) + " == " +
-                                equivs.stream().map(labelFunc).collect(Collectors.toList());
-               logger.info(msg);
-                if (!isEquivalentsAllowed) {
-                    logger.error(msg);
-                }
-            }
-        }
 
-        if (!isEquivalentsAllowed) {
-            if (nEquivs > 0) {
-                logger.error("Equivalent classes are not allowed. Exiting with error");
-                logger.error("Run with '--equivalent-classes-allowed true' to change behavior");
-                System.exit(1); // TODO: https://github.com/ontodev/robot/issues/40
-            }
+        EquivalentClassReasoning equivalentReasoning = new EquivalentClassReasoning(ontology, reasoner, mode);
+        boolean passesEquivalenceTests = equivalentReasoning.reason();
+        equivalentReasoning.logReport(logger);
+        if(!passesEquivalenceTests) {
+            System.exit(1);
         }
         // cache the complete set of asserted axioms at the initial state.
         // we will later use this if the -x option is passed, to avoid
diff --git a/robot-core/src/main/java/org/obolibrary/robot/exceptions/IncoherentRBoxException.java b/robot-core/src/main/java/org/obolibrary/robot/exceptions/IncoherentRBoxException.java
index dafb29d3..19d81e27 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/exceptions/IncoherentRBoxException.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/exceptions/IncoherentRBoxException.java
@@ -18,7 +18,7 @@ public class IncoherentRBoxException extends OntologyLogicException {
     private static final long serialVersionUID = 2608045757804483323L;
 
     /**
-     * @param unsatisfiableProperties The unsatisfiable properties.
+     * @param unsatisfiableProperties
      */
     public IncoherentRBoxException(Set<OWLObjectProperty> unsatisfiableProperties) {
     }
diff --git a/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoning.java b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoning.java
new file mode 100644
index 00000000..908ab18c
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoning.java
@@ -0,0 +1,152 @@
+package org.obolibrary.robot.reason;
+
+import com.google.common.collect.Sets;
+import org.semanticweb.owlapi.model.*;
+import org.semanticweb.owlapi.model.parameters.AxiomAnnotations;
+import org.semanticweb.owlapi.model.parameters.Imports;
+import org.semanticweb.owlapi.reasoner.OWLReasoner;
+import org.slf4j.Logger;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Created by edouglass on 5/9/17.
+ *
+ * All the pieces that are needed to perform reasoning on equivalent classes in an OWLOntology
+ */
+public class EquivalentClassReasoning {
+
+    /**
+     * The OWLOntolgoy to reason over
+     */
+    private OWLOntology ontology;
+
+    /**
+     * The particular fail mode for dealing with an equivalent class.
+     */
+    private EquivalentAxiomReasoningTest reasoningTest;
+
+    /**
+     * Reasoner to use on the OWLOntolgoy in finding equivalent classes
+     */
+    private OWLReasoner reasoner;
+
+    /**
+     * The mode that represents a particular equivalent class reasoning strategy, once found
+     */
+    private EquivalentClassReasoningMode mode;
+
+    private OWLDataFactory dataFactory;
+
+    /**
+     * Growable Set of Equivalent Class Axioms that are found by the reasoner that also fail the test in the above
+     * reasoningTest. These are Equivalent Classes that are not allowed.
+     */
+    private Set<OWLEquivalentClassesAxiom> failingAxioms;
+
+    /**
+     * Growable set of found Equivalenct Class Axioms. As the reasoner finds equivalent classes, they are added here
+     */
+    private Set<OWLEquivalentClassesAxiom> foundEquivalentAxioms;
+
+    /**
+     * Creates a new instance that will reason over the given ontology with the strategy specified by the
+     * EquivalentClassReasoningMode
+     * @param ontology OWLOntology to reason equivalence classes over
+     * @param reasoner OWLReasoner that will reason over the ontology
+     * @param mode One of ALL, NONE, or ASSERTED_ONLY, representing a strategy to deal with found equivalent classes
+     */
+    public EquivalentClassReasoning(OWLOntology ontology, OWLReasoner reasoner, EquivalentClassReasoningMode mode) {
+        this.ontology = ontology;
+        this.reasoner = reasoner;
+        dataFactory = ontology.getOWLOntologyManager().getOWLDataFactory();
+        this.reasoningTest = createTest(mode, ontology);
+        failingAxioms = Sets.newHashSet();
+        foundEquivalentAxioms = Sets.newHashSet();
+        this.mode = mode;
+    }
+
+    /**
+     * Uses the reasoner to find all equivalent classes in the ontology, and then adds the axioms that are not allowed
+     * the the list of failed axioms. Once reasoning has occurred, a report can be logged with #logReport
+     * @return True if there were no failing equivalence axioms found, False if any failed the test
+     */
+    public boolean reason() {
+
+        for(OWLClass c : ontology.getClassesInSignature()) {
+            Set<OWLClass> equivalentClasses = reasoner.getEquivalentClasses(c).getEntitiesMinus(c);
+            foundEquivalentAxioms.addAll(this.createEquivalenceAxioms(c, equivalentClasses));
+        }
+
+        for(OWLEquivalentClassesAxiom axiom : foundEquivalentAxioms) {
+            if(reasoningTest.fails(axiom)) {
+                failingAxioms.add(axiom);
+            }
+        }
+        return failingAxioms.isEmpty();
+    }
+
+    /**
+     * Uses the provided logger to print out the results of the reasoning operation.
+     * @param logger an SLF4J logger instance
+     */
+    public void logReport(Logger logger) {
+        if(!foundEquivalentAxioms.isEmpty()) {
+            logger.info("Inferred the following equivalent classes");
+            for (OWLEquivalentClassesAxiom anAxiom : foundEquivalentAxioms) {
+                logger.info(equivAxiomToString(anAxiom));
+            }
+        }
+
+        if(!failingAxioms.isEmpty()) {
+            logger.error(mode.getExplanation());
+            for(OWLEquivalentClassesAxiom axiom : failingAxioms) {
+                logger.error(equivAxiomToString(axiom));
+            }
+        }
+    }
+
+    private Set<OWLEquivalentClassesAxiom> createEquivalenceAxioms(OWLClass c, Set<OWLClass> equivalentClasses) {
+        Set<OWLEquivalentClassesAxiom> equivalenceAxioms = new HashSet<>();
+        for(OWLClass ec : equivalentClasses) {
+            equivalenceAxioms.add(dataFactory.getOWLEquivalentClassesAxiom(c, ec));
+        }
+        return equivalenceAxioms;
+    }
+
+    /**
+     * This is how we define the failure behavior given a Reasoning Mode.
+     * @param mode one of the enum instances that define how reasoning behavior should be over equivalent axioms
+     * @param ontology Ontology to reason over
+     * @return The EquivalentAxiomReasoningTest instance with the implementation of how to detect a failing Equivalence Axiom
+     */
+    private static EquivalentAxiomReasoningTest createTest(EquivalentClassReasoningMode mode, OWLOntology ontology) {
+        EquivalentAxiomReasoningTest test = null;
+        switch (mode) {
+            case ALL:
+                test = axiom -> false;
+                break;
+            case NONE:
+                test = axiom -> true;
+                break;
+            case ASSERTED_ONLY:
+                test = axiom -> !ontology.containsAxiom(axiom, Imports.INCLUDED, AxiomAnnotations.IGNORE_AXIOM_ANNOTATIONS);
+                break;
+        }
+        return test;
+    }
+
+    private static String equivAxiomToString(OWLEquivalentClassesAxiom axiom) {
+        String equivalentClassesString = "Equivalence: ";
+        Iterator<OWLClass> classIterator = axiom.getNamedClasses().iterator();
+        while (classIterator.hasNext()) {
+            equivalentClassesString += classIterator.next().toString();
+            if (classIterator.hasNext()) {
+                equivalentClassesString += " == ";
+            }
+        }
+        return equivalentClassesString;
+    }
+}
diff --git a/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoningMode.java b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoningMode.java
new file mode 100644
index 00000000..cd0cc16b
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentClassReasoningMode.java
@@ -0,0 +1,84 @@
+package org.obolibrary.robot.reason;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ *
+ * Created by edouglass on 5/5/17.
+ * Enum of the three different equivalent class reasoning strategies. ALL means all equivalent classes detected
+ * are allowed. NONE means any equivalent class should fail, and ASSERTED_ONLY means that only if the inferred
+ * equivalence class has been previously asserted directly in the ontology is the equivalence class allowed.
+ *
+ * This is also how the command line options are detected. For backwards compatibility, the old "true" is a synonym for
+ * ALL, and "false" is a synonym for NONE. The #validates(String) method detects if the given String is a synonym or
+ * the uncapitalized name of the enum instance. For example, either "all" or "true" would validate to the instance ALL.
+ *
+ * Lastly, one of the enum instances can easily be returned from a string with the static #from(String) method. This is
+ * like a constructor. If the given string validates to any of the enum instances, then that instance is returned. By
+ * default ALL is returned. This is how a Mode instance should be retrieved from a string (say from the command line).
+ */
+public enum EquivalentClassReasoningMode {
+
+    /**
+     * Allows all equivalent classes in the reason step. This is the default
+     */
+    ALL("All equivalent class axioms are allowed", "true"),
+    /**
+     * Does not allow any detected equivalent classes.
+     */
+    NONE("No equivalent class axioms are allowed", "false"),
+    /**
+     * Only allows directly asserted equivalent classes. If there are equivalent classes inferred that do are not
+     * already asserted in the ontology, then this is considered an error.
+     */
+    ASSERTED_ONLY("Only equivalent classes that have been asserted are allowed. Inferred equivalencies are forbidden.");
+
+    private static EquivalentClassReasoningMode DEFAULT = ALL;
+
+    private List<String> synonyms;
+
+    private String explanation;
+
+    EquivalentClassReasoningMode(String explanation, String... synonyms) {
+        this.synonyms = Lists.newArrayList(synonyms);
+        this.explanation = explanation;
+    }
+
+    public String getExplanation() {
+        return explanation;
+    }
+
+    /**
+     * The command-line written form of the mode
+     */
+    public String written() {
+        return name().replace('_', '-').toLowerCase();
+    }
+
+    /**
+     * Checks if the given string counts as this mode. A string validates to a mode if s is the written form or
+     * one of the synonyms of this Mode.
+     * @return True if the given string s is valid, False if not.
+     */
+    public boolean validates(String s) {
+        return s.equals(this.written()) || synonyms.contains(s);
+    }
+
+    /**
+     * Canonical way to find make a Mode instance from a String (say from a command line argument). Tries to validate
+     * the string against one of the instances and if a mode instance is found, it's returned. If no match is found,
+     * ALL is returned by default.
+     * @param s String to match to a Mode instance.
+     * @return Mode enum instance that matches the String, or ALL by default.
+     */
+    public static EquivalentClassReasoningMode from(String s) {
+        for(EquivalentClassReasoningMode mode : EquivalentClassReasoningMode.values()) {
+            if(mode.validates(s)) {
+                return mode;
+            }
+        }
+        return DEFAULT;
+    }
+}


Test patch:
diff --git a/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentAxiomReasoningTest.java b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentAxiomReasoningTest.java
new file mode 100644
index 00000000..3a0e7535
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/reason/EquivalentAxiomReasoningTest.java
@@ -0,0 +1,18 @@
+package org.obolibrary.robot.reason;
+
+import org.semanticweb.owlapi.model.OWLAxiom;
+
+/**
+ * Created by edouglass on 5/9/17.
+ *
+ * Performs a test on an OWLAxiom and states if it "fails". This will be up to the implementation
+ */
+public interface EquivalentAxiomReasoningTest {
+
+    /**
+     * Returns true if the given axiom fails the type of Test this Reasoning Test is. For example,
+     * if no equivalent axioms are allowed, any axiom here should return false.
+     */
+    boolean fails(OWLAxiom axiom);
+
+}
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 98242810..55a19f3b 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/ReasonOperationTest.java
@@ -3,6 +3,7 @@ package org.obolibrary.robot;
 import static org.junit.Assert.*;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -47,8 +48,7 @@ public class ReasonOperationTest extends CoreTest {
     @Test
     public void testELK() throws IOException, OWLOntologyCreationException, OntologyLogicException {
         OWLOntology reasoned = loadOntology("/simple.owl");
-        OWLReasonerFactory reasonerFactory = new org.semanticweb
-                .elk.owlapi.ElkReasonerFactory();
+        OWLReasonerFactory reasonerFactory = new ElkReasonerFactory();
         ReasonOperation.reason(reasoned, reasonerFactory);
         assertIdentical("/simple_elk.owl", reasoned);
     }
@@ -173,7 +173,7 @@ public class ReasonOperationTest extends CoreTest {
         OWLOntology reasoned = loadOntology("/redundant_subclasses.owl");
         OWLReasonerFactory reasonerFactory = new org.semanticweb
                 .elk.owlapi.ElkReasonerFactory();
-        ReasonOperation.reason(reasoned, reasonerFactory, null);
+        ReasonOperation.reason(reasoned, reasonerFactory, Collections.emptyMap());
         assertIdentical("/redundant_subclasses.owl", reasoned);
 
         Map<String, String> options = new HashMap<String, String>();
diff --git a/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningModeTest.java b/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningModeTest.java
new file mode 100644
index 00000000..7749c429
--- /dev/null
+++ b/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningModeTest.java
@@ -0,0 +1,46 @@
+package org.obolibrary.robot.reason;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.obolibrary.robot.reason.EquivalentClassReasoningMode.*;
+
+/**
+ * Created by edouglass on 5/8/17.
+ * Tests for EquivalentClassReasoningMode
+ */
+public class EquivalentClassReasoningModeTest {
+
+    @Test
+    public void testWritten() {
+        assertEquals("all", ALL.written());
+        assertEquals("none", NONE.written());
+        assertEquals("asserted-only", ASSERTED_ONLY.written());
+    }
+
+    @Test
+    public void testValidateWrittenName() {
+        assertTrue(ALL.validates("all"));
+        assertTrue(NONE.validates("none"));
+        assertTrue(ASSERTED_ONLY.validates("asserted-only"));
+        assertFalse(ASSERTED_ONLY.validates("asserted_only"));
+    }
+
+    @Test
+    public void testValidatesSynonyms() {
+        assertTrue(ALL.validates("true"));
+        assertTrue(NONE.validates("false"));
+    }
+
+    @Test
+    public void testFromDefault() {
+        assertEquals(ALL, from("foo"));
+    }
+
+    @Test
+    public void testFrom() {
+        assertEquals(ALL, from("all"));
+        assertEquals(NONE, from("false"));
+        assertEquals(ASSERTED_ONLY, from("asserted-only"));
+    }
+}
diff --git a/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningTest.java b/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningTest.java
new file mode 100644
index 00000000..38291bba
--- /dev/null
+++ b/robot-core/src/test/java/org/obolibrary/robot/reason/EquivalentClassReasoningTest.java
@@ -0,0 +1,64 @@
+package org.obolibrary.robot.reason;
+
+import org.junit.Test;
+import org.obolibrary.robot.CoreTest;
+import org.semanticweb.elk.owlapi.ElkReasonerFactory;
+import org.semanticweb.owlapi.model.OWLOntology;
+import org.semanticweb.owlapi.reasoner.OWLReasoner;
+import org.semanticweb.owlapi.reasoner.OWLReasonerFactory;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+/**
+ * Created by edouglass on 5/9/17.
+ *
+ */
+public class EquivalentClassReasoningTest extends CoreTest{
+
+    @Test
+    public void testReasonAllEquivalencesAllowed() throws IOException {
+        OWLOntology inferred = loadOntology("/inferred-equiv.owl");
+        OWLReasonerFactory reasonerFactory = new ElkReasonerFactory();
+        OWLReasoner reasoner = reasonerFactory.createReasoner(inferred);
+        EquivalentClassReasoning allAllowedReasoning = new EquivalentClassReasoning(
+            inferred, reasoner, EquivalentClassReasoningMode.ALL);
+
+        assertTrue(allAllowedReasoning.reason());
+    }
+
+    @Test
+    public void testReasonNoEquivalencesAllowed() throws IOException {
+        OWLOntology inferred = loadOntology("/inferred-equiv.owl");
+        OWLReasonerFactory reasonerFactory = new ElkReasonerFactory();
+        OWLReasoner reasoner = reasonerFactory.createReasoner(inferred);
+        EquivalentClassReasoning noneAllowedReasoning = new EquivalentClassReasoning(
+            inferred, reasoner, EquivalentClassReasoningMode.NONE);
+
+        assertFalse(noneAllowedReasoning.reason());
+    }
+
+    @Test
+    public void testReasonOnlyAssertedAllowedOnInferred() throws IOException {
+        OWLOntology inferred = loadOntology("/inferred-equiv.owl");
+        OWLReasonerFactory reasonerFactory = new ElkReasonerFactory();
+        OWLReasoner reasoner = reasonerFactory.createReasoner(inferred);
+        EquivalentClassReasoning assertedOnlyReasoning = new EquivalentClassReasoning(
+            inferred, reasoner, EquivalentClassReasoningMode.ASSERTED_ONLY);
+
+        assertFalse(assertedOnlyReasoning.reason());
+    }
+
+    @Test
+    public void testReasonOnlyAssertedAllowedOnAsserted() throws IOException {
+        OWLOntology inferred = loadOntology("/asserted-equiv.owl");
+        OWLReasonerFactory reasonerFactory = new ElkReasonerFactory();
+        OWLReasoner reasoner = reasonerFactory.createReasoner(inferred);
+        EquivalentClassReasoning assertedOnlyReasoning = new EquivalentClassReasoning(
+            inferred, reasoner, EquivalentClassReasoningMode.ASSERTED_ONLY);
+
+        assertTrue(assertedOnlyReasoning.reason());
+    }
+
+}
diff --git a/robot-core/src/test/resources/asserted-equiv.owl b/robot-core/src/test/resources/asserted-equiv.owl
new file mode 100644
index 00000000..3a3fe8c6
--- /dev/null
+++ b/robot-core/src/test/resources/asserted-equiv.owl
@@ -0,0 +1,9 @@
+Prefix: : <http://purl.obolibrary.org/obo/TEST_>
+Ontology: <http://purl.obolibrary.org/obo/test1>
+
+## A==B
+## Will also trivially infer B==A, but this is the same.
+Class: A
+  EquivalentTo: B
+
+Class: B
diff --git a/robot-core/src/test/resources/inferred-equiv.owl b/robot-core/src/test/resources/inferred-equiv.owl
new file mode 100644
index 00000000..d9e6be81
--- /dev/null
+++ b/robot-core/src/test/resources/inferred-equiv.owl
@@ -0,0 +1,15 @@
+Prefix: : <http://purl.obolibrary.org/obo/TEST_>
+Ontology: <http://purl.obolibrary.org/obo/test1>
+
+## A and B will be inferred to be equivalent.
+## This is a new inference, not originally asserted.
+## This is a red flag as such equivalencies are often unintentional
+Class: A
+  EquivalentTo: C and D
+
+Class: B
+  EquivalentTo: C and D
+
+Class: C
+
+Class: D