Repository: ontodev/robot
Pull request #385 (2018-10-24 14:54:21)

Exclude assertions
See #185


Patch:
diff --git a/docs/extract.md b/docs/extract.md
index d8b497b5..a809ab5d 100644
--- a/docs/extract.md
+++ b/docs/extract.md
@@ -18,6 +18,8 @@ The `--method` options fall into two groups: Syntactic Locality Module Extractor
 - BOT: use the SLME to extract a bottom module
 - MIREOT: extract a simple hierarchy of terms
 
+By default, `extract` will include imported ontologies. To exclude imported ontologies, just add `--imports exclude` for any non-MIREOT extraction method.
+
 ## Syntactic Locality Module Extractor (SLME)
 
 Each SLME module type takes a "seed" that you specify with `--term` and `--term-file` options. From the seed it builds a module with a "signature" that includes the seed plus any other terms required so that any logical entailments are preserved between entities (classes, properties and individuals) in the signature. For example, if an ontology implies that A is a subclass of B, and the seed contains A and B, then the module will also imply that A is a subclass of B. In other words, the module will contain all the axioms needed to provide the same entailments for the seed terms (and resulting signature) as the full ontology would.
@@ -36,6 +38,14 @@ For more details see:
 
 ROBOT expects any `--term` or IRI in the `--term-file` to exist in the input ontology. If none of the input terms exist, the command will fail with an [empty terms error](errors#empty-terms-error). This can be overridden by including `--force true`. 
 
+### Instances
+
+When using the SLME method of extraction, all individuals (ABox axioms) and their class types (the TBox axioms they depend on) are included by default. The `extract` command provides an `--individuals` option to specify what (if any) individuals are included in the output ontology:
+* `--individuals include`: all individuals in the input ontology and their class types (default)
+* `--individuals minimal`: only the individuals that are a type of a class in the extracted module
+* `--individuals definitions`: only the individuals that are used in logical definitions of classes
+* `--individuals exclude`: no individuals
+
 ## MIREOT
 
 The MIREOT method preserves the hierarchy of the input ontology (subclass and subproperty relationships), but does not try to preserve the full set of logical entailments. Both "upper" (ancestor) and "lower" (descendant) limits can be specified, like this:
diff --git a/robot-command/src/main/java/org/obolibrary/robot/ExtractCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ExtractCommand.java
index 5b0d2874..ce5b148d 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/ExtractCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/ExtractCommand.java
@@ -75,6 +75,8 @@ public class ExtractCommand implements Command {
     o.addOption("f", "force", true, "if true, warn on empty input terms instead of fail");
     o.addOption("a", "annotate-with-source", true, "if true, annotate terms with rdfs:isDefinedBy");
     o.addOption("s", "sources", true, "specify a mapping file of term to source ontology");
+    o.addOption("n", "individuals", true, "handle individuals (default: include)");
+    o.addOption("M", "imports", true, "handle imports (default: include)");
     options = o;
   }
 
@@ -151,17 +153,12 @@ public class ExtractCommand implements Command {
     state = CommandLineHelper.updateInputOntology(ioHelper, state, line);
     OWLOntology inputOntology = state.getOntology();
 
-    IRI outputIRI = CommandLineHelper.getOutputIRI(line);
-    if (outputIRI == null) {
-      outputIRI = inputOntology.getOntologyID().getOntologyIRI().orNull();
-    }
-
-    // Determine if terms should be annotated with isDefinedBy
-    boolean annotateSource = CommandLineHelper.getBooleanValue(line, "annotate-with-source", false);
-    String sourceMapPath = CommandLineHelper.getOptionalValue(line, "sources");
-    Map<IRI, IRI> sourceMap = new HashMap<>();
-    if (sourceMapPath != null) {
-      sourceMap = getSourceMap(ioHelper, sourceMapPath);
+    // Override default reasoner options with command-line options
+    Map<String, String> extractOptions = ExtractOperation.getDefaultOptions();
+    for (String option : extractOptions.keySet()) {
+      if (line.hasOption(option)) {
+        extractOptions.put(option, line.getOptionValue(option));
+      }
     }
 
     // Get method, make sure it has been specified
@@ -170,8 +167,6 @@ public class ExtractCommand implements Command {
             .trim()
             .toLowerCase();
 
-    boolean force = CommandLineHelper.getBooleanValue(line, "force", false);
-
     ModuleType moduleType = null;
     switch (method) {
       case "star":
@@ -186,78 +181,9 @@ public class ExtractCommand implements Command {
     }
 
     if (method.equals("mireot")) {
-      List<OWLOntology> outputOntologies = new ArrayList<>();
-      // Get terms from input (ensuring that they are in the input ontology)
-      // It's okay for any of these to return empty (allowEmpty = true)
-      // Checks for empty sets later
-      Set<IRI> upperIRIs =
-          OntologyHelper.filterExistingTerms(
-              inputOntology,
-              CommandLineHelper.getTerms(ioHelper, line, "upper-term", "upper-terms"),
-              true);
-      if (upperIRIs.size() == 0) {
-        upperIRIs = null;
-      }
-      Set<IRI> lowerIRIs =
-          OntologyHelper.filterExistingTerms(
-              inputOntology,
-              CommandLineHelper.getTerms(ioHelper, line, "lower-term", "lower-terms"),
-              true);
-      if (lowerIRIs.size() == 0) {
-        lowerIRIs = null;
-      }
-      Set<IRI> branchIRIs =
-          OntologyHelper.filterExistingTerms(
-              inputOntology,
-              CommandLineHelper.getTerms(ioHelper, line, "branch-from-term", "branch-from-terms"),
-              true);
-      if (branchIRIs.size() == 0) {
-        branchIRIs = null;
-      }
-
-      // Need branch IRIs or lower IRIs to proceed
-      if (branchIRIs == null && lowerIRIs == null) {
-        throw new IllegalArgumentException(missingMireotTermsError);
-      } else {
-        // First check for lower IRIs, upper IRIs can be null or not
-        if (lowerIRIs != null) {
-          outputOntologies.add(
-              MireotOperation.getAncestors(
-                  inputOntology, upperIRIs, lowerIRIs, null, annotateSource, sourceMap));
-          // If there are no lower IRIs, there shouldn't be any upper IRIs
-        } else if (upperIRIs != null) {
-          throw new IllegalArgumentException(missingLowerTermError);
-        }
-        // Check for branch IRIs
-        if (branchIRIs != null) {
-          outputOntologies.add(
-              MireotOperation.getDescendants(
-                  inputOntology, branchIRIs, null, annotateSource, sourceMap));
-        }
-      }
-      outputOntology = MergeOperation.merge(outputOntologies);
+      outputOntology = mireotExtract(ioHelper, inputOntology, line, extractOptions);
     } else if (moduleType != null) {
-      // upper-term, lower-term, and branch-from term should not be used
-      List<String> mireotTerms =
-          Arrays.asList(
-              CommandLineHelper.getOptionalValue(line, "upper-term"),
-              CommandLineHelper.getOptionalValue(line, "upper-terms"),
-              CommandLineHelper.getOptionalValue(line, "lower-term"),
-              CommandLineHelper.getOptionalValue(line, "lower-terms"),
-              CommandLineHelper.getOptionalValue(line, "branch-from-term"),
-              CommandLineHelper.getOptionalValue(line, "branch-from-terms"));
-      for (String mt : mireotTerms) {
-        if (mt != null) {
-          throw new IllegalArgumentException(invalidOptionError);
-        }
-      }
-      // Make sure the terms exist in the input ontology
-      Set<IRI> terms =
-          OntologyHelper.filterExistingTerms(
-              inputOntology, CommandLineHelper.getTerms(ioHelper, line), force);
-      outputOntology =
-          ExtractOperation.extract(
-              inputOntology, terms, outputIRI, moduleType, annotateSource, sourceMap);
+      outputOntology = slmeExtract(ioHelper, inputOntology, moduleType, line, extractOptions);
     } else {
       throw new Exception(invalidMethodError);
     }
@@ -277,6 +203,140 @@ public class ExtractCommand implements Command {
     return state;
   }
 
+  /**
+   * Perform a MIREOT extraction on an ontology after validating command line options.
+   *
+   * @param ioHelper IOHelper to use
+   * @param inputOntology OWLOntology to extract from
+   * @param line CommandLine with options
+   * @param extractOptions Map of extract options
+   * @return a new ontology containing extracted subset
+   * @throws IOException on problem parsing terms
+   * @throws OWLOntologyCreationException on OWLAPI issue
+   */
+  private static OWLOntology mireotExtract(
+      IOHelper ioHelper,
+      OWLOntology inputOntology,
+      CommandLine line,
+      Map<String, String> extractOptions)
+      throws Exception {
+    List<OWLOntology> outputOntologies = new ArrayList<>();
+    // Get terms from input (ensuring that they are in the input ontology)
+    // It's okay for any of these to return empty (allowEmpty = true)
+    // Checks for empty sets later
+    Set<IRI> upperIRIs =
+        OntologyHelper.filterExistingTerms(
+            inputOntology,
+            CommandLineHelper.getTerms(ioHelper, line, "upper-term", "upper-terms"),
+            true);
+    if (upperIRIs.size() == 0) {
+      upperIRIs = null;
+    }
+    Set<IRI> lowerIRIs =
+        OntologyHelper.filterExistingTerms(
+            inputOntology,
+            CommandLineHelper.getTerms(ioHelper, line, "lower-term", "lower-terms"),
+            true);
+    if (lowerIRIs.size() == 0) {
+      lowerIRIs = null;
+    }
+    Set<IRI> branchIRIs =
+        OntologyHelper.filterExistingTerms(
+            inputOntology,
+            CommandLineHelper.getTerms(ioHelper, line, "branch-from-term", "branch-from-terms"),
+            true);
+    if (branchIRIs.size() == 0) {
+      branchIRIs = null;
+    }
+
+    // Need branch IRIs or lower IRIs to proceed
+    if (branchIRIs == null && lowerIRIs == null) {
+      throw new IllegalArgumentException(missingMireotTermsError);
+    } else {
+      boolean annotateSource = OptionsHelper.optionIsTrue(extractOptions, "annotate-with-source");
+      Map<IRI, IRI> sourceMap =
+          getSourceMap(ioHelper, CommandLineHelper.getOptionalValue(line, "sources"));
+
+      // First check for lower IRIs, upper IRIs can be null or not
+      if (lowerIRIs != null) {
+        outputOntologies.add(
+            MireotOperation.getAncestors(
+                inputOntology, upperIRIs, lowerIRIs, null, annotateSource, sourceMap));
+        // If there are no lower IRIs, there shouldn't be any upper IRIs
+      } else if (upperIRIs != null) {
+        throw new IllegalArgumentException(missingLowerTermError);
+      }
+      // Check for branch IRIs
+      if (branchIRIs != null) {
+        outputOntologies.add(
+            MireotOperation.getDescendants(
+                inputOntology, branchIRIs, null, annotateSource, sourceMap));
+      }
+    }
+    // Get the output IRI and create the output ontology
+    IRI outputIRI = CommandLineHelper.getOutputIRI(line);
+    if (outputIRI == null) {
+      outputIRI = inputOntology.getOntologyID().getOntologyIRI().orNull();
+    }
+    OWLOntology outputOntology = MergeOperation.merge(outputOntologies);
+    if (outputIRI != null) {
+      outputOntology.getOWLOntologyManager().setOntologyDocumentIRI(outputOntology, outputIRI);
+    }
+    return outputOntology;
+  }
+
+  /**
+   * Perform a SLME extraction after validating command line options.
+   *
+   * @param inputOntology OWLOntology to extract from
+   * @param moduleType type of extraction
+   * @param line CommandLine with options
+   * @param extractOptions Map of extract options
+   * @return a new ontology containing extracted subset
+   * @throws IOException on issue parsing terms
+   * @throws OWLOntologyCreationException on OWLAPI issue
+   */
+  private static OWLOntology slmeExtract(
+      IOHelper ioHelper,
+      OWLOntology inputOntology,
+      ModuleType moduleType,
+      CommandLine line,
+      Map<String, String> extractOptions)
+      throws Exception {
+    // upper-term, lower-term, and branch-from term should not be used
+    List<String> mireotTerms =
+        Arrays.asList(
+            CommandLineHelper.getOptionalValue(line, "upper-term"),
+            CommandLineHelper.getOptionalValue(line, "upper-terms"),
+            CommandLineHelper.getOptionalValue(line, "lower-term"),
+            CommandLineHelper.getOptionalValue(line, "lower-terms"),
+            CommandLineHelper.getOptionalValue(line, "branch-from-term"),
+            CommandLineHelper.getOptionalValue(line, "branch-from-terms"));
+    for (String mt : mireotTerms) {
+      if (mt != null) {
+        throw new IllegalArgumentException(invalidOptionError);
+      }
+    }
+    // Make sure the terms exist in the input ontology
+    Set<IRI> terms =
+        OntologyHelper.filterExistingTerms(
+            inputOntology,
+            CommandLineHelper.getTerms(ioHelper, line),
+            OptionsHelper.optionIsTrue(extractOptions, "force"));
+
+    // Determine what to do with sources
+    Map<IRI, IRI> sourceMap =
+        getSourceMap(ioHelper, CommandLineHelper.getOptionalValue(line, "sources"));
+    // Get the output IRI
+    IRI outputIRI = CommandLineHelper.getOutputIRI(line);
+    if (outputIRI == null) {
+      outputIRI = inputOntology.getOntologyID().getOntologyIRI().orNull();
+    }
+
+    return ExtractOperation.extract(
+        inputOntology, terms, outputIRI, moduleType, extractOptions, sourceMap);
+  }
+
   /**
    * Given an IOHelper and the path to a term-to-source map, return a map of term IRI to source IRI.
    *
@@ -287,6 +347,12 @@ public class ExtractCommand implements Command {
    */
   private static Map<IRI, IRI> getSourceMap(IOHelper ioHelper, String sourceMapPath)
       throws Exception {
+    // If no source map path is specified, just return null
+    if (sourceMapPath == null) {
+      return null;
+    }
+
+    // Otherwise, use the path to get a file containing the mappings
     File sourceMapFile = new File(sourceMapPath);
     if (!sourceMapFile.exists()) {
       throw new Exception(String.format(missingFileError, sourceMapPath, "--sources"));
@@ -312,7 +378,7 @@ public class ExtractCommand implements Command {
     csv.skip(1);
 
     Map<IRI, IRI> sourceMap = new HashMap<>();
-    for (String line[] : csv) {
+    for (String[] line : csv) {
       IRI entity = ioHelper.createIRI(line[0]);
 
       // Maybe create a source IRI from a prefix
diff --git a/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java
index d05d82ab..2ba9b837 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/ExtractOperation.java
@@ -1,11 +1,13 @@
 package org.obolibrary.robot;
 
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import org.semanticweb.owlapi.apibinding.OWLManager;
 import org.semanticweb.owlapi.model.*;
 import org.semanticweb.owlapi.model.parameters.Imports;
+import org.semanticweb.owlapi.search.EntitySearcher;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import uk.ac.manchester.cs.owl.owlapi.OWLDataFactoryImpl;
@@ -28,9 +30,30 @@ public class ExtractOperation {
   /** RDFS isDefinedBy annotation property. */
   private static OWLAnnotationProperty isDefinedBy = dataFactory.getRDFSIsDefinedBy();
 
+  /** Namespace for errors. */
+  private static final String NS = "extract#";
+
+  /** Error message when an invalid argument is passed to --individuals. */
+  private static final String unknownIndividuals =
+      NS + "UNKNOWN INDIVIDUALS %s is not a valid --individuals argument";
+
+  /**
+   * Return a map from option name to default option value.
+   *
+   * @return a map with default values for all available options
+   */
+  public static Map<String, String> getDefaultOptions() {
+    Map<String, String> options = new HashMap<>();
+    options.put("individuals", "include");
+    options.put("imports", "include");
+    options.put("copy-ontology-annotations", "false");
+    options.put("annotate-with-source", "false");
+    return options;
+  }
+
   /**
    * Extract a set of terms from an ontology using the OWLAPI's SyntacticLocalityModuleExtractor
-   * (SLME). The input ontology is not changed.
+   * (SLME) with default options. The input ontology is not changed.
    *
    * @param inputOntology the ontology to extract from
    * @param terms a set of IRIs for terms to extract
@@ -42,7 +65,17 @@ public class ExtractOperation {
   public static OWLOntology extract(
       OWLOntology inputOntology, Set<IRI> terms, IRI outputIRI, ModuleType moduleType)
       throws OWLOntologyCreationException {
-    return extract(inputOntology, terms, outputIRI, moduleType, false, null);
+    return extract(inputOntology, terms, outputIRI, moduleType, getDefaultOptions(), null);
+  }
+
+  public static OWLOntology extract(
+      OWLOntology inputOntology,
+      Set<IRI> terms,
+      IRI outputIRI,
+      ModuleType moduleType,
+      Map<String, String> options)
+      throws OWLOntologyCreationException {
+    return extract(inputOntology, terms, outputIRI, moduleType, options, null);
   }
 
   /**
@@ -53,8 +86,8 @@ public class ExtractOperation {
    * @param terms a set of IRIs for terms to extract
    * @param outputIRI the OntologyIRI of the new ontology
    * @param moduleType determines the type of extraction; defaults to STAR
-   * @param annotateSource if true, annotate copied classes with rdfs:isDefinedBy
-   * @param sourceMap map of term IRI to source IRI
+   * @param options map of extract options
+   * @param sourceMap map of term IRI to source IRI, or null (only used with annotate-with-source)
    * @return a new ontology (with a new manager)
    * @throws OWLOntologyCreationException on any OWLAPI problem
    */
@@ -63,29 +96,85 @@ public class ExtractOperation {
       Set<IRI> terms,
       IRI outputIRI,
       ModuleType moduleType,
-      boolean annotateSource,
+      Map<String, String> options,
       Map<IRI, IRI> sourceMap)
       throws OWLOntologyCreationException {
+    if (options == null) {
+      options = getDefaultOptions();
+    }
+
+    String individuals = OptionsHelper.getOption(options, "individuals", "include");
+    boolean excludeInstances;
+    if (individuals.equalsIgnoreCase("exclude")
+        || individuals.equalsIgnoreCase("minimal")
+        || individuals.equalsIgnoreCase("definitions")) {
+      excludeInstances = true;
+    } else if (individuals.equalsIgnoreCase("include")) {
+      excludeInstances = false;
+    } else {
+      throw new IllegalArgumentException(String.format(unknownIndividuals, individuals));
+    }
+
+    String importsString = OptionsHelper.getOption(options, "imports", "include");
+    Imports imports;
+    if ("include".equalsIgnoreCase(importsString)) {
+      imports = Imports.INCLUDED;
+    } else {
+      imports = Imports.EXCLUDED;
+    }
     logger.debug("Extracting...");
 
     Set<OWLEntity> entities = new HashSet<>();
     for (IRI term : terms) {
-      entities.addAll(inputOntology.getEntitiesInSignature(term, Imports.INCLUDED));
+      entities.addAll(inputOntology.getEntitiesInSignature(term, imports));
     }
 
+    // Default moduleType is STAR
     ModuleType type = moduleType;
     if (type == null) {
       type = ModuleType.STAR;
     }
 
+    // Get all axioms from the ontology and its imports
+    Set<OWLAxiom> axs = new HashSet<>(inputOntology.getAxioms());
+    for (OWLOntology importedOnt : inputOntology.getImportsClosure()) {
+      axs.addAll(importedOnt.getAxioms());
+    }
+    // Maybe get an IRI
+    IRI ontIRI = inputOntology.getOntologyID().getOntologyIRI().orNull();
+
     SyntacticLocalityModuleExtractor extractor =
         new SyntacticLocalityModuleExtractor(
-            inputOntology.getOWLOntologyManager(), inputOntology, type);
+            inputOntology.getOWLOntologyManager(), ontIRI, axs, type, excludeInstances);
 
+    // Create the output with the extracted terms
     OWLOntologyManager manager = OWLManager.createOWLOntologyManager();
     OWLOntology outputOntology = manager.createOntology(extractor.extract(entities), outputIRI);
+
+    // Maybe add the axioms belonging to individuals of class types included
+    if (individuals.equalsIgnoreCase("minimal")) {
+      addMinimalIndividualAxioms(inputOntology, outputOntology, imports);
+    } else if (individuals.equalsIgnoreCase("definitions")) {
+      addDefinitionIndividualAxioms(inputOntology, outputOntology, imports);
+    } else if ("exclude".equalsIgnoreCase(individuals)) {
+      // Make sure to completely remove individuals
+      Set<OWLObject> indivs = new HashSet<>(outputOntology.getIndividualsInSignature());
+      Set<OWLAxiom> indivAxioms =
+          RelatedObjectsHelper.getCompleteAxioms(outputOntology, indivs, null);
+      manager.removeAxioms(outputOntology, indivAxioms);
+    }
+
+    // Maybe copy ontology annotations
+    boolean copyOntologyAnnotations =
+        OptionsHelper.optionIsTrue(options, "copy-ontology-annotations");
+    if (copyOntologyAnnotations) {
+      for (OWLAnnotation annotation : inputOntology.getAnnotations()) {
+        OntologyHelper.addOntologyAnnotation(outputOntology, annotation);
+      }
+    }
+
     // Maybe annotate entities with rdfs:isDefinedBy
-    if (annotateSource) {
+    if (OptionsHelper.optionIsTrue(options, "annotate-with-source")) {
       Set<OWLAnnotationAxiom> sourceAxioms = new HashSet<>();
       for (OWLEntity entity : OntologyHelper.getEntities(outputOntology)) {
         // Check if rdfs:isDefinedBy already exists
@@ -102,6 +191,89 @@ public class ExtractOperation {
     return outputOntology;
   }
 
+  /**
+   * Given an input ontology, an output ontology, and an Imports specification, copy individuals
+   * used in logical definitions from the input to the output ontology.
+   *
+   * @param inputOntology OWLOntology to copy axioms from
+   * @param outputOntology OWLOntology to copy axioms to
+   * @param imports Imports.INCLUDED or Imports.EXCLUDED
+   */
+  private static void addDefinitionIndividualAxioms(
+      OWLOntology inputOntology, OWLOntology outputOntology, Imports imports) {
+    Set<OWLIndividual> individuals = new HashSet<>();
+    Set<OWLClass> classes = outputOntology.getClassesInSignature();
+    for (OWLClass cls : classes) {
+      for (OWLClassExpression expr : EntitySearcher.getEquivalentClasses(cls, inputOntology)) {
+        if (!expr.isAnonymous()) {
+          continue;
+        }
+        individuals.addAll(expr.getIndividualsInSignature());
+      }
+
+      for (OWLClassExpression expr : EntitySearcher.getSubClasses(cls, inputOntology)) {
+        if (!expr.isAnonymous()) {
+          continue;
+        }
+        individuals.addAll(expr.getIndividualsInSignature());
+      }
+    }
+
+    addIndiviudalsAxioms(inputOntology, outputOntology, individuals, imports);
+  }
+
+  /**
+   * Given an input ontology and an output ontology, copy any individual axioms and their
+   * annotations from the input to the output ontology as long as the class type is included in the
+   * output ontology.
+   *
+   * @param inputOntology OWLOntology to copy axioms from
+   * @param outputOntology OWLOntology to copy axioms to
+   * @param imports Imports.INCLUDED or Imports.EXCLUDED
+   */
+  private static void addMinimalIndividualAxioms(
+      OWLOntology inputOntology, OWLOntology outputOntology, Imports imports) {
+    Set<OWLIndividual> individuals = new HashSet<>();
+    Set<OWLClass> classes = outputOntology.getClassesInSignature();
+    // Get the individuals for each of the included classes
+    for (OWLClass cls : classes) {
+      individuals.addAll(EntitySearcher.getIndividuals(cls, inputOntology));
+    }
+    addIndiviudalsAxioms(inputOntology, outputOntology, individuals, imports);
+  }
+
+  /**
+   * Given an input ontology, an output ontology, a set of individuals, and an Imports
+   * specification, copy the individual axioms for the set of individuals from the input to the
+   * output ontology.
+   *
+   * @param inputOntology OWLOntology to copy axioms from
+   * @param outputOntology OWLOntology to copy axioms to
+   * @param individuals Set of OWLIndividuals to copy axioms for
+   * @param imports Imports.INCLUDED or Imports.EXCLUDED
+   */
+  private static void addIndiviudalsAxioms(
+      OWLOntology inputOntology,
+      OWLOntology outputOntology,
+      Set<OWLIndividual> individuals,
+      Imports imports) {
+    if (imports == null) {
+      imports = Imports.INCLUDED;
+    }
+    Set<OWLAxiom> axioms = new HashSet<>();
+    for (OWLIndividual individual : individuals) {
+      if (individual.isNamed()) {
+        // Add axioms about named individuals (includes assertions)
+        OWLNamedIndividual namedIndividual = individual.asOWLNamedIndividual();
+        axioms.addAll(inputOntology.getAnnotationAssertionAxioms(namedIndividual.getIRI()));
+        axioms.addAll(inputOntology.getAxioms(namedIndividual, imports));
+      } else {
+        axioms.addAll(inputOntology.getAxioms(individual, imports));
+      }
+    }
+    outputOntology.getOWLOntologyManager().addAxioms(outputOntology, axioms);
+  }
+
   /**
    * Given an OWLEntity, return an OWLAnnotationAssertionAxiom indicating the source ontology with
    * rdfs:isDefinedBy.
diff --git a/robot-core/src/main/java/org/obolibrary/robot/RelatedObjectsHelper.java b/robot-core/src/main/java/org/obolibrary/robot/RelatedObjectsHelper.java
index f3ad8c26..c5641ed9 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/RelatedObjectsHelper.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/RelatedObjectsHelper.java
@@ -86,6 +86,10 @@ public class RelatedObjectsHelper {
    */
   public static Set<OWLAxiom> getCompleteAxioms(
       OWLOntology ontology, Set<OWLObject> objects, Set<Class<? extends OWLAxiom>> axiomTypes) {
+    if (axiomTypes == null) {
+      axiomTypes = new HashSet<>();
+      axiomTypes.add(OWLAxiom.class);
+    }
     Set<OWLAxiom> axioms = new HashSet<>();
     Set<IRI> iris = getIRIs(objects);
     for (OWLAxiom axiom : ontology.getAxioms()) {
@@ -137,6 +141,10 @@ public class RelatedObjectsHelper {
    */
   public static Set<OWLAxiom> getPartialAxioms(
       OWLOntology ontology, Set<OWLObject> objects, Set<Class<? extends OWLAxiom>> axiomTypes) {
+    if (axiomTypes == null) {
+      axiomTypes = new HashSet<>();
+      axiomTypes.add(OWLAxiom.class);
+    }
     Set<OWLAxiom> axioms = new HashSet<>();
     Set<IRI> iris = getIRIs(objects);
     for (OWLAxiom axiom : ontology.getAxioms()) {


Test patch:
diff --git a/robot-core/src/test/java/org/obolibrary/robot/ExtractOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/ExtractOperationTest.java
index 2e0a22ff..148a15e5 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/ExtractOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/ExtractOperationTest.java
@@ -93,6 +93,39 @@ public class ExtractOperationTest extends CoreTest {
     assertEquals(axiom, ExtractOperation.getIsDefinedBy(entity, null));
   }
 
+  /**
+   * Tests extraction with minimal individuals.
+   *
+   * @throws IOException on IO error
+   * @throws OWLOntologyCreationException on ontology error
+   */
+  @Test
+  public void testExtractMinimalIndividuals() throws IOException, OWLOntologyCreationException {
+    testExtractIndividuals("minimal", "/minimal-individuals.owl");
+  }
+
+  /**
+   * Tests extraction with definition individuals.
+   *
+   * @throws IOException on IO error
+   * @throws OWLOntologyCreationException on ontology error
+   */
+  @Test
+  public void testExtractDefinitionIndividuals() throws IOException, OWLOntologyCreationException {
+    testExtractIndividuals("definitions", "/definition-individuals.owl");
+  }
+
+  /**
+   * Tests extraction with no individuals.
+   *
+   * @throws IOException on IO error
+   * @throws OWLOntologyCreationException on ontology error
+   */
+  @Test
+  public void testExtractNoIndividuals() throws IOException, OWLOntologyCreationException {
+    testExtractIndividuals("exclude", "/no-individuals.owl");
+  }
+
   /**
    * Tests a generic non-MIREOT (i.e. SLME) extraction operation using a custom module type and a
    * pre-generated OWL file to compare against.
@@ -105,7 +138,6 @@ public class ExtractOperationTest extends CoreTest {
   public void testExtract(ModuleType moduleType, String expectedPath)
       throws IOException, OWLOntologyCreationException {
     OWLOntology simple = loadOntology("/filtered.owl");
-
     IRI outputIRI = IRI.create("http://purl.obolibrary.org/obo/uberon.owl");
 
     Set<IRI> terms =
@@ -117,4 +149,33 @@ public class ExtractOperationTest extends CoreTest {
     removeDeclarations(module);
     assertIdentical(expected, module);
   }
+
+  /**
+   * Tests extraction based on how to handle individuals. Uses TOP method.
+   *
+   * @param individuals 'exclude', 'minimal', or 'definitions'
+   * @param expectedPath path to the known-good file for comparison
+   * @throws IOException on IO error
+   * @throws OWLOntologyCreationException on ontology error
+   */
+  private void testExtractIndividuals(String individuals, String expectedPath)
+      throws IOException, OWLOntologyCreationException {
+    Map<String, String> options = ExtractOperation.getDefaultOptions();
+    options.put("individuals", individuals);
+    OWLOntology simple = loadOntology("/simple-individuals.owl");
+    IRI outputIRI =
+        IRI.create("https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl");
+
+    Set<IRI> terms =
+        Collections.singleton(
+            IRI.create(
+                "https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2"));
+    OWLOntology module =
+        ExtractOperation.extract(simple, terms, outputIRI, ModuleType.TOP, options);
+
+    OWLOntology expected = loadOntology(expectedPath);
+    removeDeclarations(expected);
+    removeDeclarations(module);
+    assertIdentical(expected, module);
+  }
 }
diff --git a/robot-core/src/test/resources/definition-individuals.owl b/robot-core/src/test/resources/definition-individuals.owl
new file mode 100644
index 00000000..19d19883
--- /dev/null
+++ b/robot-core/src/test/resources/definition-individuals.owl
@@ -0,0 +1,75 @@
+<?xml version="1.0"?>
+<rdf:RDF xmlns="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#"
+     xml:base="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"
+     xmlns:owl="http://www.w3.org/2002/07/owl#"
+     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:xml="http://www.w3.org/XML/1998/namespace"
+     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
+     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+    <owl:Ontology rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"/>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Classes
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2">
+        <owl:equivalentClass>
+            <owl:Class>
+                <owl:oneOf rdf:parseType="Collection">
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+                </owl:oneOf>
+            </owl:Class>
+        </owl:equivalentClass>
+        <rdfs:label>Test 2</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Individuals
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+    </owl:NamedIndividual>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+    </owl:NamedIndividual>
+</rdf:RDF>
+
+
+
+<!-- Generated by the OWL API (version 4.5.6) https://github.com/owlcs/owlapi -->
+
diff --git a/robot-core/src/test/resources/minimal-individuals.owl b/robot-core/src/test/resources/minimal-individuals.owl
new file mode 100644
index 00000000..80209fba
--- /dev/null
+++ b/robot-core/src/test/resources/minimal-individuals.owl
@@ -0,0 +1,73 @@
+<?xml version="1.0"?>
+<rdf:RDF xmlns="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#"
+     xml:base="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"
+     xmlns:owl="http://www.w3.org/2002/07/owl#"
+     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:xml="http://www.w3.org/XML/1998/namespace"
+     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
+     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+    <owl:Ontology rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"/>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Classes
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2">
+        <owl:equivalentClass>
+            <owl:Class>
+                <owl:oneOf rdf:parseType="Collection">
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+                </owl:oneOf>
+            </owl:Class>
+        </owl:equivalentClass>
+        <rdfs:label>Test 2</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Individuals
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual3 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual3">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2"/>
+    </owl:NamedIndividual>
+</rdf:RDF>
+
+
+
+<!-- Generated by the OWL API (version 4.5.6) https://github.com/owlcs/owlapi -->
+
diff --git a/robot-core/src/test/resources/no-individuals.owl b/robot-core/src/test/resources/no-individuals.owl
new file mode 100644
index 00000000..d3cd35ca
--- /dev/null
+++ b/robot-core/src/test/resources/no-individuals.owl
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<rdf:RDF xmlns="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#"
+     xml:base="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"
+     xmlns:owl="http://www.w3.org/2002/07/owl#"
+     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:xml="http://www.w3.org/XML/1998/namespace"
+     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
+     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+    <owl:Ontology rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"/>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Classes
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2">
+        <owl:equivalentClass>
+            <owl:Class>
+                <owl:oneOf rdf:parseType="Collection">
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+                </owl:oneOf>
+            </owl:Class>
+        </owl:equivalentClass>
+        <rdfs:label>Test 2</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Individuals
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+</rdf:RDF>
+
+
+
+<!-- Generated by the OWL API (version 4.5.6) https://github.com/owlcs/owlapi -->
+
diff --git a/robot-core/src/test/resources/simple-individuals.owl b/robot-core/src/test/resources/simple-individuals.owl
new file mode 100644
index 00000000..f36817bc
--- /dev/null
+++ b/robot-core/src/test/resources/simple-individuals.owl
@@ -0,0 +1,113 @@
+<?xml version="1.0"?>
+<rdf:RDF xmlns="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"
+     xml:base="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"
+     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:owl="http://www.w3.org/2002/07/owl#"
+     xmlns:xml="http://www.w3.org/XML/1998/namespace"
+     xmlns:xsd="http://www.w3.org/2001/XMLSchema#"
+     xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+     xmlns:obo="http://purl.obolibrary.org/obo/"
+     xmlns:foaf="http://xmlns.com/foaf/0.1/">
+    <owl:Ontology rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl"/>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Classes
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test1 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test1">
+        <rdfs:label>Test 1</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2">
+        <owl:equivalentClass>
+            <owl:Class>
+                <owl:oneOf rdf:parseType="Collection">
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1"/>
+                    <rdf:Description rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2"/>
+                </owl:oneOf>
+            </owl:Class>
+        </owl:equivalentClass>
+        <rdfs:subClassOf rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test1"/>
+        <rdfs:label>Test 2</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3">
+        <rdfs:label>Test 3</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test4 -->
+
+    <owl:Class rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test4">
+        <rdfs:subClassOf rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+        <rdfs:label>Test 4</rdfs:label>
+    </owl:Class>
+    
+
+
+    <!-- 
+    ///////////////////////////////////////////////////////////////////////////////////////
+    //
+    // Individuals
+    //
+    ///////////////////////////////////////////////////////////////////////////////////////
+     -->
+
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual1">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+    </owl:NamedIndividual>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual2">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test3"/>
+    </owl:NamedIndividual>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual3 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual3">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test2"/>
+    </owl:NamedIndividual>
+    
+
+
+    <!-- https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual4 -->
+
+    <owl:NamedIndividual rdf:about="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#individual4">
+        <rdf:type rdf:resource="https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test4"/>
+    </owl:NamedIndividual>
+</rdf:RDF>
+
+
+
+<!-- Generated by the OWL API (version 4.2.8.20170104-2310) https://github.com/owlcs/owlapi -->
+