Repository: ontodev/robot
Pull request #1236 (2025-01-19 21:14:58)

Add `--clean-obo` option to `convert`.
Resolves [#995]

- [x] `docs/` have been added/updated
- [x] tests have been added/updated
- [x] `mvn verify` says all tests pass
- [x] `mvn site` says all JavaDocs correct
- [x] `CHANGELOG.md` has been updated

This PR adds a `--clean-obo` option to the `convert` command, as discussed in #995.

The option accepts a space-separated list of keywords that dictate how OBO output should be tweaked:

* `drop-extra-labels`: if an entity has more than one `rdfs:label` annotation, keep the first label only;
* `drop-extra-definitions`: likewise, but for `IAO:0000115` definitions;
* `drop-extra-comments`: likewise, but for `rdfs:comment` annotations;
* `merge-comments`: alternative to `drop-extra-comments`: merge `rdfs:comment` annotations into a single annotation, instead of keeping only the first one;
* `drop-untranslatable-axioms`: drop axioms that cannot be represented in the OBO format instead of putting them in a `owl-axioms` header tag;
* `drop-gci-axioms`: drop general concept inclusion axioms.

In addition, the option accepts the following shortcut keywords:

* `strict`: equivalent to `drop-extra-labels drop-extra-definitions drop-extra-comments`;
* `simple`: equivalent to `strict drop-untranslatable-axioms drop-gci-axioms`;
* `true`: equivalent to `simple`.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1373e11..aaf7467e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-## Fixed
+### Added
+- Add `--clean-obo` option to [`convert`] [#995]
+
+### Fixed
 - Update owl-diff dependency for stable ordering and to avoid large string creation [#1227]
 - Improve disambiguation of properties in QuotedEntityChecker [#1226]
 - Skip "non-robot" columns in templates for the purposes of axiom annotations [#1216]
diff --git a/docs/convert.md b/docs/convert.md
index 9d870167..71245898 100644
--- a/docs/convert.md
+++ b/docs/convert.md
@@ -35,10 +35,38 @@ By default, the OBO writer strictly enforces [document structure rules](http://o
 
 As a document is converted to OBO, you may see `ERROR MASKING ERROR` exceptions. This does not indicate failure, but it should be noted that these axioms will not be translated to OBO format. Rather, they will be included in the ontology header under `owl-axioms`. See [Untranslatable OWL axioms](http://owlcollab.github.io/oboformat/doc/obo-syntax.html#5.0.4) for more details.
 
-You can choose to keep these in the file, or remove them with:
-```
-grep -v ^owl-axioms
-```
+The OBO output can be fine-tuned with the `--clean-obo` option. That option takes a space-separated list of keywords that each enables a customization of the OBO output. Available keywords are:
+  - `drop-extra-labels`: forcefully drop supernumerary `rdfs:label` annotation, to make the ontology compliant with the OBO specification (which dictates that a class can only have one label).
+  - `drop-extra-definitions`: likewise, but for `IAO:0000115` annotations (definitions).
+  - `drop-extra-comments`: likewise, but for `rdfs:comment` annotations.
+  - `merge-comments`: merge `rdfs:comment` annotations, when there are more than one, into a single annotation (alternative to `drop-extra-comments`).
+  - `drop-untranslatable-axioms`: drop axioms that cannot be represented in OBO format, instead of writing them into the aforementioned `owl-axioms` header tag.
+  - `drop-gci-axioms`: drop axioms that represent General Concept Inclusions, even if they can be legally represented in OBO format.
+
+In addition, the following special keywords are also accepted:
+  - `strict`: equivalent to `drop-extra-labels drop-extra-definitions drop-extra-comments`, to force the production of a valid OBO file by dropping supernumerary annotations as needed.
+  - `true`: alias for `strict`.
+  - `simple`: equivalent to `strict drop-untranslatable-axioms drop-gci-axioms`, to force the production of an OBO file that is not only valid, but also free of any `owl-axioms` header tag and GCI axioms (which, while perfectly valid with respect to the OBO specification, are not always handled correctly by all OBO parsers).
+
+#### Examples
+
+Convert a file to OBO and ensure the resulting file is compliant with the OBO specification, dropping supernumerary annotations if necessary:
+
+    robot convert -i cl_module.ofn \
+      --clean-obo strict \
+      --output results/cl_module-strict.obo
+
+Likewise, but with merging comments into a single one instead of dropping the supernumerary comments:
+
+    robot convert -i cl_module.ofn \
+      --clean-obo "strict merge-comments" \
+      --output results/cl_module-strict-mergedcomments.obo
+
+Convert a file to a simple variant of the OBO format (without any `owl-axioms` tag and GCI axioms):
+
+    robot convert -i cl_module.ofn \
+      --clean-obo simple \
+      --output results/cl_module-simple.obo
 
 ---
 
diff --git a/docs/examples/cl_module-simple.obo b/docs/examples/cl_module-simple.obo
new file mode 100644
index 00000000..b4456c68
--- /dev/null
+++ b/docs/examples/cl_module-simple.obo
@@ -0,0 +1,145 @@
+format-version: 1.2
+ontology: cl
+
+[Term]
+id: CL:0000000
+name: cell
+def: "A material entity of anatomical origin (part of or deriving from an organism) that has as its parts a maximally connected cell compartment surrounded by a plasma membrane." [CARO:mah]
+comment: The definition of cell is intended to represent all cells, and thus a cell is defined as a material entity and not an anatomical structure, which implies that it is part of an organism (or the entirety of one).
+is_a: UBERON:0000061 ! anatomical structure
+
+[Term]
+id: CL:0000113
+name: mononuclear phagocyte
+def: "A vertebrate phagocyte with a single nucleus." [GOC:add, GOC:tfm, ISBN:0781735149]
+is_a: CL:0000842 ! mononuclear cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000235
+name: macrophage
+def: "A mononuclear phagocyte present in variety of tissues, typically differentiated from monocytes, capable of phagocytosing a variety of extracellular particulate material, including immune complexes, microorganisms, and dead cells." [GO_REF:0000031, GOC:add, GOC:tfm, PMID:16213494, PMID:1919437]
+comment: Morphology: Diameter 30_M-80 _M, abundant cytoplasm, low N/C ratio, eccentric nucleus. Irregular shape with pseudopods, highly adhesive. Contain vacuoles and phagosomes, may contain azurophilic granules; markers: Mouse & Human: CD68, in most cases CD11b. Mouse: in most cases F4/80+; role or process: immune, antigen presentation, & tissue remodelling; lineage: hematopoietic, myeloid.
+synonym: "histiocyte" EXACT []
+is_a: CL:0000113 ! mononuclear phagocyte
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000583
+name: alveolar macrophage
+def: "A tissue-resident macrophage found in the alveoli of the lungs. Ingests small inhaled particles resulting in degradation and presentation of the antigen to immunocompetent cells. Markers include F4/80-positive, CD11b-/low, CD11c-positive, CD68-positive, sialoadhesin-positive, dectin-1-positive, MR-positive, CX3CR1-negative." [GO_REF:0000031, GOC:ana, GOC:dsd, GOC:tfm, MESH:D016676]
+comment: Markers: Mouse: F4/80mid, CD11b-/low, CD11c+, CD68+, sialoadhesin+, dectin-1+, MR+, CX3CR1-.
+synonym: "dust cell" EXACT []
+synonym: "MF.Lu" RELATED []
+xref: FMA:83023
+is_a: CL:0000235 ! macrophage
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000738
+name: leukocyte
+def: "An achromatic cell of the myeloid or lymphoid lineages capable of ameboid movement, found in blood or other tissue." [GOC:add, GOC:tfm, ISBN:978-0-323-05290-0]
+synonym: "immune cell" RELATED []
+synonym: "leucocyte" EXACT []
+synonym: "white blood cell" EXACT []
+is_a: CL:0000988 ! hematopoietic cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000842
+name: mononuclear cell
+def: "A leukocyte with a single non-segmented nucleus in the mature form." [GOC:add]
+synonym: "mononuclear leukocyte" EXACT []
+synonym: "peripheral blood mononuclear cell" NARROW []
+is_a: CL:0000738 ! leukocyte
+intersection_of: CL:0000738 ! leukocyte
+intersection_of: bearer_of PATO:0001407 ! mononucleate
+relationship: bearer_of PATO:0001407 ! mononucleate
+relationship: has_part GO:0005634 ! nucleus
+
+[Term]
+id: CL:0000988
+name: hematopoietic cell
+def: "A cell of a hematopoietic lineage." [GO_REF:0000031, GOC:add]
+synonym: "haematopoietic cell" EXACT []
+synonym: "hemopoietic cell" EXACT []
+is_a: CL:0000000 ! cell
+
+[Term]
+id: GO:0005634
+name: nucleus
+namespace: cellular_component
+def: "A membrane-bounded organelle of eukaryotic cells in which chromosomes are housed and replicated. In most cells, the nucleus contains all of the cell's chromosomes except the organellar chromosomes, and is the site of RNA synthesis and processing. In some species, or in specialized cell types, RNA metabolism or DNA replication may be absent." [GOC:go_curators]
+synonym: "cell nucleus" EXACT []
+synonym: "horsetail nucleus" NARROW [GOC:al, GOC:mah, GOC:vw, PMID:15030757]
+is_a: UBERON:0000061 ! anatomical structure
+relationship: has_part UBERON:0000061 ! anatomical structure
+
+[Term]
+id: PATO:0001407
+name: mononucleate
+namespace: quality
+def: "A nucleate quality inhering in a bearer by virtue of the bearer's having one nucleus." [Biology-online:Biology-online]
+subset: cell_quality
+subset: mpath_slim
+subset: value_slim
+
+[Term]
+id: PATO:0010006
+name: cell morphology
+namespace: quality
+def: "A quality of a single cell inhering in the bearer by virtue of the bearer's size or shape or structure." [https://orcid.org/0000-0002-7073-9172]
+comment: Use this term for morphologies that can *only* inhere in a cell, e.g. morphological qualities inhering in a cell by virtue of the presence, location or shape of one or more cell parts.
+property_value: http://purl.org/dc/terms/contributor https://orcid.org/0000-0002-7073-9172
+creation_date: 2021-01-23T11:31:53Z
+
+[Term]
+id: UBERON:0000061
+name: anatomical structure
+namespace: uberon
+def: "Material anatomical entity that is a single connected structure with inherent 3D shape generated by coordinated expression of the organism's own genome." [CARO:0000003]
+synonym: "biological structure" EXACT []
+synonym: "connected biological structure" EXACT [CARO:0000003]
+is_a: UBERON:0000465 ! material anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0000465
+name: material anatomical entity
+namespace: uberon
+def: "Anatomical entity that has mass." [http://orcid.org/0000-0001-9114-8737]
+is_a: UBERON:0001062 ! anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0001062
+name: anatomical entity
+namespace: uberon
+def: "Biological entity that is either an individual member of a biological species or constitutes the structural organization of an individual member of a biological species." [FMA:62955, http://orcid.org/0000-0001-9114-8737]
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Typedef]
+id: bearer_of
+name: has characteristic
+namespace: external
+def: "Inverse of characteristic_of" []
+xref: RO:0000053
+is_inverse_functional: true
+
+[Typedef]
+id: has_part
+name: has part
+namespace: external
+def: "a core relation that holds between a whole and its part" []
+subset: http://purl.obolibrary.org/obo/valid_for_go_annotation_extension
+subset: http://purl.obolibrary.org/obo/valid_for_go_ontology
+subset: http://purl.obolibrary.org/obo/valid_for_gocam
+xref: BFO:0000051
+is_transitive: true
+
diff --git a/docs/examples/cl_module-strict-mergedcomments.obo b/docs/examples/cl_module-strict-mergedcomments.obo
new file mode 100644
index 00000000..72296343
--- /dev/null
+++ b/docs/examples/cl_module-strict-mergedcomments.obo
@@ -0,0 +1,147 @@
+format-version: 1.2
+ontology: cl
+owl-axioms: Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\nPrefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)\nPrefix(xml:=<http://www.w3.org/XML/1998/namespace>)\nPrefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)\nPrefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)\n\n\nOntology(\nDeclaration(Class(<http://purl.obolibrary.org/obo/CL_0000000>))\nDeclaration(Class(<http://purl.obolibrary.org/obo/PATO_0010006>))\nDeclaration(ObjectProperty(<http://purl.obolibrary.org/obo/RO_0000053>))\n\n\nSubClassOf(ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0010006>) <http://purl.obolibrary.org/obo/CL_0000000>)\n)
+
+[Term]
+id: CL:0000000
+name: cell
+def: "A material entity of anatomical origin (part of or deriving from an organism) that has as its parts a maximally connected cell compartment surrounded by a plasma membrane." [CARO:mah]
+comment: The definition of cell is intended to represent all cells, and thus a cell is defined as a material entity and not an anatomical structure, which implies that it is part of an organism (or the entirety of one).
+is_a: UBERON:0000061 ! anatomical structure
+relationship: has_part GO:0005634 {gci_filler="PATO:0001407", gci_relation="bearer_of"} ! nucleus
+
+[Term]
+id: CL:0000113
+name: mononuclear phagocyte
+def: "A vertebrate phagocyte with a single nucleus." [GOC:add, GOC:tfm, ISBN:0781735149]
+is_a: CL:0000842 ! mononuclear cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000235
+name: macrophage
+def: "A mononuclear phagocyte present in variety of tissues, typically differentiated from monocytes, capable of phagocytosing a variety of extracellular particulate material, including immune complexes, microorganisms, and dead cells." [GO_REF:0000031, GOC:add, GOC:tfm, PMID:16213494, PMID:1919437]
+comment: Morphology: Diameter 30_M-80 _M, abundant cytoplasm, low N/C ratio, eccentric nucleus. Irregular shape with pseudopods, highly adhesive. Contain vacuoles and phagosomes, may contain azurophilic granules; markers: Mouse & Human: CD68, in most cases CD11b. Mouse: in most cases F4/80+; role or process: immune, antigen presentation, & tissue remodelling; lineage: hematopoietic, myeloid.
+synonym: "histiocyte" EXACT []
+is_a: CL:0000113 ! mononuclear phagocyte
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000583
+name: alveolar macrophage
+def: "A tissue-resident macrophage found in the alveoli of the lungs. Ingests small inhaled particles resulting in degradation and presentation of the antigen to immunocompetent cells. Markers include F4/80-positive, CD11b-/low, CD11c-positive, CD68-positive, sialoadhesin-positive, dectin-1-positive, MR-positive, CX3CR1-negative." [GO_REF:0000031, GOC:ana, GOC:dsd, GOC:tfm, MESH:D016676]
+comment: Markers: Mouse: F4/80mid, CD11b-/low, CD11c+, CD68+, sialoadhesin+, dectin-1+, MR+, CX3CR1-. The marker set MSR1, FABP4 can identify the Human cell type alveolar macrophage in the Lung with a confidence of 0.80 (NS-Forest FBeta value). {xref="https://doi.org/10.5281/zenodo.11165918"}
+synonym: "dust cell" EXACT []
+synonym: "MF.Lu" RELATED []
+xref: FMA:83023
+is_a: CL:0000235 ! macrophage
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000738
+name: leukocyte
+def: "An achromatic cell of the myeloid or lymphoid lineages capable of ameboid movement, found in blood or other tissue." [GOC:add, GOC:tfm, ISBN:978-0-323-05290-0]
+synonym: "immune cell" RELATED []
+synonym: "leucocyte" EXACT []
+synonym: "white blood cell" EXACT []
+is_a: CL:0000988 ! hematopoietic cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000842
+name: mononuclear cell
+def: "A leukocyte with a single non-segmented nucleus in the mature form." [GOC:add]
+synonym: "mononuclear leukocyte" EXACT []
+synonym: "peripheral blood mononuclear cell" NARROW []
+is_a: CL:0000738 ! leukocyte
+intersection_of: CL:0000738 ! leukocyte
+intersection_of: bearer_of PATO:0001407 ! mononucleate
+relationship: bearer_of PATO:0001407 ! mononucleate
+relationship: has_part GO:0005634 ! nucleus
+
+[Term]
+id: CL:0000988
+name: hematopoietic cell
+def: "A cell of a hematopoietic lineage." [GO_REF:0000031, GOC:add]
+synonym: "haematopoietic cell" EXACT []
+synonym: "hemopoietic cell" EXACT []
+is_a: CL:0000000 ! cell
+
+[Term]
+id: GO:0005634
+name: nucleus
+namespace: cellular_component
+def: "A membrane-bounded organelle of eukaryotic cells in which chromosomes are housed and replicated. In most cells, the nucleus contains all of the cell's chromosomes except the organellar chromosomes, and is the site of RNA synthesis and processing. In some species, or in specialized cell types, RNA metabolism or DNA replication may be absent." [GOC:go_curators]
+synonym: "cell nucleus" EXACT []
+synonym: "horsetail nucleus" NARROW [GOC:al, GOC:mah, GOC:vw, PMID:15030757]
+is_a: UBERON:0000061 ! anatomical structure
+relationship: has_part UBERON:0000061 ! anatomical structure
+
+[Term]
+id: PATO:0001407
+name: mononucleate
+namespace: quality
+def: "A nucleate quality inhering in a bearer by virtue of the bearer's having one nucleus." [Biology-online:Biology-online]
+subset: cell_quality
+subset: mpath_slim
+subset: value_slim
+
+[Term]
+id: PATO:0010006
+name: cell morphology
+namespace: quality
+def: "A quality of a single cell inhering in the bearer by virtue of the bearer's size or shape or structure." [https://orcid.org/0000-0002-7073-9172]
+comment: Use this term for morphologies that can *only* inhere in a cell, e.g. morphological qualities inhering in a cell by virtue of the presence, location or shape of one or more cell parts.
+property_value: http://purl.org/dc/terms/contributor https://orcid.org/0000-0002-7073-9172
+creation_date: 2021-01-23T11:31:53Z
+
+[Term]
+id: UBERON:0000061
+name: anatomical structure
+namespace: uberon
+def: "Material anatomical entity that is a single connected structure with inherent 3D shape generated by coordinated expression of the organism's own genome." [CARO:0000003]
+synonym: "biological structure" EXACT []
+synonym: "connected biological structure" EXACT [CARO:0000003]
+is_a: UBERON:0000465 ! material anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0000465
+name: material anatomical entity
+namespace: uberon
+def: "Anatomical entity that has mass." [http://orcid.org/0000-0001-9114-8737]
+is_a: UBERON:0001062 ! anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0001062
+name: anatomical entity
+namespace: uberon
+def: "Biological entity that is either an individual member of a biological species or constitutes the structural organization of an individual member of a biological species." [FMA:62955, http://orcid.org/0000-0001-9114-8737]
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Typedef]
+id: bearer_of
+name: has characteristic
+namespace: external
+def: "Inverse of characteristic_of" []
+xref: RO:0000053
+is_inverse_functional: true
+
+[Typedef]
+id: has_part
+name: has part
+namespace: external
+def: "a core relation that holds between a whole and its part" []
+subset: http://purl.obolibrary.org/obo/valid_for_go_annotation_extension
+subset: http://purl.obolibrary.org/obo/valid_for_go_ontology
+subset: http://purl.obolibrary.org/obo/valid_for_gocam
+xref: BFO:0000051
+is_transitive: true
+
diff --git a/docs/examples/cl_module-strict.obo b/docs/examples/cl_module-strict.obo
new file mode 100644
index 00000000..05a9e733
--- /dev/null
+++ b/docs/examples/cl_module-strict.obo
@@ -0,0 +1,147 @@
+format-version: 1.2
+ontology: cl
+owl-axioms: Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\nPrefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)\nPrefix(xml:=<http://www.w3.org/XML/1998/namespace>)\nPrefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)\nPrefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)\n\n\nOntology(\nDeclaration(Class(<http://purl.obolibrary.org/obo/CL_0000000>))\nDeclaration(Class(<http://purl.obolibrary.org/obo/PATO_0010006>))\nDeclaration(ObjectProperty(<http://purl.obolibrary.org/obo/RO_0000053>))\n\n\nSubClassOf(ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0010006>) <http://purl.obolibrary.org/obo/CL_0000000>)\n)
+
+[Term]
+id: CL:0000000
+name: cell
+def: "A material entity of anatomical origin (part of or deriving from an organism) that has as its parts a maximally connected cell compartment surrounded by a plasma membrane." [CARO:mah]
+comment: The definition of cell is intended to represent all cells, and thus a cell is defined as a material entity and not an anatomical structure, which implies that it is part of an organism (or the entirety of one).
+is_a: UBERON:0000061 ! anatomical structure
+relationship: has_part GO:0005634 {gci_filler="PATO:0001407", gci_relation="bearer_of"} ! nucleus
+
+[Term]
+id: CL:0000113
+name: mononuclear phagocyte
+def: "A vertebrate phagocyte with a single nucleus." [GOC:add, GOC:tfm, ISBN:0781735149]
+is_a: CL:0000842 ! mononuclear cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000235
+name: macrophage
+def: "A mononuclear phagocyte present in variety of tissues, typically differentiated from monocytes, capable of phagocytosing a variety of extracellular particulate material, including immune complexes, microorganisms, and dead cells." [GO_REF:0000031, GOC:add, GOC:tfm, PMID:16213494, PMID:1919437]
+comment: Morphology: Diameter 30_M-80 _M, abundant cytoplasm, low N/C ratio, eccentric nucleus. Irregular shape with pseudopods, highly adhesive. Contain vacuoles and phagosomes, may contain azurophilic granules; markers: Mouse & Human: CD68, in most cases CD11b. Mouse: in most cases F4/80+; role or process: immune, antigen presentation, & tissue remodelling; lineage: hematopoietic, myeloid.
+synonym: "histiocyte" EXACT []
+is_a: CL:0000113 ! mononuclear phagocyte
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000583
+name: alveolar macrophage
+def: "A tissue-resident macrophage found in the alveoli of the lungs. Ingests small inhaled particles resulting in degradation and presentation of the antigen to immunocompetent cells. Markers include F4/80-positive, CD11b-/low, CD11c-positive, CD68-positive, sialoadhesin-positive, dectin-1-positive, MR-positive, CX3CR1-negative." [GO_REF:0000031, GOC:ana, GOC:dsd, GOC:tfm, MESH:D016676]
+comment: Markers: Mouse: F4/80mid, CD11b-/low, CD11c+, CD68+, sialoadhesin+, dectin-1+, MR+, CX3CR1-.
+synonym: "dust cell" EXACT []
+synonym: "MF.Lu" RELATED []
+xref: FMA:83023
+is_a: CL:0000235 ! macrophage
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000738
+name: leukocyte
+def: "An achromatic cell of the myeloid or lymphoid lineages capable of ameboid movement, found in blood or other tissue." [GOC:add, GOC:tfm, ISBN:978-0-323-05290-0]
+synonym: "immune cell" RELATED []
+synonym: "leucocyte" EXACT []
+synonym: "white blood cell" EXACT []
+is_a: CL:0000988 ! hematopoietic cell
+property_value: RO:0002175 NCBITaxon:9606
+
+[Term]
+id: CL:0000842
+name: mononuclear cell
+def: "A leukocyte with a single non-segmented nucleus in the mature form." [GOC:add]
+synonym: "mononuclear leukocyte" EXACT []
+synonym: "peripheral blood mononuclear cell" NARROW []
+is_a: CL:0000738 ! leukocyte
+intersection_of: CL:0000738 ! leukocyte
+intersection_of: bearer_of PATO:0001407 ! mononucleate
+relationship: bearer_of PATO:0001407 ! mononucleate
+relationship: has_part GO:0005634 ! nucleus
+
+[Term]
+id: CL:0000988
+name: hematopoietic cell
+def: "A cell of a hematopoietic lineage." [GO_REF:0000031, GOC:add]
+synonym: "haematopoietic cell" EXACT []
+synonym: "hemopoietic cell" EXACT []
+is_a: CL:0000000 ! cell
+
+[Term]
+id: GO:0005634
+name: nucleus
+namespace: cellular_component
+def: "A membrane-bounded organelle of eukaryotic cells in which chromosomes are housed and replicated. In most cells, the nucleus contains all of the cell's chromosomes except the organellar chromosomes, and is the site of RNA synthesis and processing. In some species, or in specialized cell types, RNA metabolism or DNA replication may be absent." [GOC:go_curators]
+synonym: "cell nucleus" EXACT []
+synonym: "horsetail nucleus" NARROW [GOC:al, GOC:mah, GOC:vw, PMID:15030757]
+is_a: UBERON:0000061 ! anatomical structure
+relationship: has_part UBERON:0000061 ! anatomical structure
+
+[Term]
+id: PATO:0001407
+name: mononucleate
+namespace: quality
+def: "A nucleate quality inhering in a bearer by virtue of the bearer's having one nucleus." [Biology-online:Biology-online]
+subset: cell_quality
+subset: mpath_slim
+subset: value_slim
+
+[Term]
+id: PATO:0010006
+name: cell morphology
+namespace: quality
+def: "A quality of a single cell inhering in the bearer by virtue of the bearer's size or shape or structure." [https://orcid.org/0000-0002-7073-9172]
+comment: Use this term for morphologies that can *only* inhere in a cell, e.g. morphological qualities inhering in a cell by virtue of the presence, location or shape of one or more cell parts.
+property_value: http://purl.org/dc/terms/contributor https://orcid.org/0000-0002-7073-9172
+creation_date: 2021-01-23T11:31:53Z
+
+[Term]
+id: UBERON:0000061
+name: anatomical structure
+namespace: uberon
+def: "Material anatomical entity that is a single connected structure with inherent 3D shape generated by coordinated expression of the organism's own genome." [CARO:0000003]
+synonym: "biological structure" EXACT []
+synonym: "connected biological structure" EXACT [CARO:0000003]
+is_a: UBERON:0000465 ! material anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0000465
+name: material anatomical entity
+namespace: uberon
+def: "Anatomical entity that has mass." [http://orcid.org/0000-0001-9114-8737]
+is_a: UBERON:0001062 ! anatomical entity
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Term]
+id: UBERON:0001062
+name: anatomical entity
+namespace: uberon
+def: "Biological entity that is either an individual member of a biological species or constitutes the structural organization of an individual member of a biological species." [FMA:62955, http://orcid.org/0000-0001-9114-8737]
+property_value: RO:0002175 NCBITaxon:33090
+property_value: RO:0002175 NCBITaxon:33208
+property_value: RO:0002175 NCBITaxon:4751
+
+[Typedef]
+id: bearer_of
+name: has characteristic
+namespace: external
+def: "Inverse of characteristic_of" []
+xref: RO:0000053
+is_inverse_functional: true
+
+[Typedef]
+id: has_part
+name: has part
+namespace: external
+def: "a core relation that holds between a whole and its part" []
+subset: http://purl.obolibrary.org/obo/valid_for_go_annotation_extension
+subset: http://purl.obolibrary.org/obo/valid_for_go_ontology
+subset: http://purl.obolibrary.org/obo/valid_for_gocam
+xref: BFO:0000051
+is_transitive: true
+
diff --git a/docs/examples/cl_module.ofn b/docs/examples/cl_module.ofn
new file mode 100644
index 00000000..2c856510
--- /dev/null
+++ b/docs/examples/cl_module.ofn
@@ -0,0 +1,285 @@
+Prefix(:=<http://purl.obolibrary.org/obo/cl.owl#>)
+Prefix(owl:=<http://www.w3.org/2002/07/owl#>)
+Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
+Prefix(xml:=<http://www.w3.org/XML/1998/namespace>)
+Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
+Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)
+
+
+Ontology(<http://purl.obolibrary.org/obo/cl.owl>
+
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000000>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000113>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000235>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000583>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000738>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000842>))
+Declaration(Class(<http://purl.obolibrary.org/obo/CL_0000988>))
+Declaration(Class(<http://purl.obolibrary.org/obo/GO_0005634>))
+Declaration(Class(<http://purl.obolibrary.org/obo/PATO_0001407>))
+Declaration(Class(<http://purl.obolibrary.org/obo/PATO_0010006>))
+Declaration(Class(<http://purl.obolibrary.org/obo/UBERON_0000061>))
+Declaration(Class(<http://purl.obolibrary.org/obo/UBERON_0000465>))
+Declaration(Class(<http://purl.obolibrary.org/obo/UBERON_0001062>))
+Declaration(ObjectProperty(<http://purl.obolibrary.org/obo/BFO_0000051>))
+Declaration(ObjectProperty(<http://purl.obolibrary.org/obo/RO_0000053>))
+Declaration(AnnotationProperty(<http://purl.obolibrary.org/obo/IAO_0000115>))
+Declaration(AnnotationProperty(<http://purl.obolibrary.org/obo/IAO_0000233>))
+Declaration(AnnotationProperty(<http://purl.obolibrary.org/obo/OMO_0002000>))
+Declaration(AnnotationProperty(<http://purl.obolibrary.org/obo/RO_0002175>))
+Declaration(AnnotationProperty(<http://purl.org/dc/terms/contributor>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#creation_date>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#hasDbXref>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#id>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#inSubset>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#shorthand>))
+Declaration(AnnotationProperty(<http://www.geneontology.org/formats/oboInOwl#source>))
+Declaration(AnnotationProperty(<https://w3id.org/sssom/mapping_justification>))
+############################
+#   Annotation Properties
+############################
+
+# Annotation Property: <http://purl.obolibrary.org/obo/IAO_0000115> (definition)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/IAO_0000115> "The official definition, explaining the meaning of a class or property. Shall be Aristotelian, formalized and normalized. Can be augmented with colloquial definitions.")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/IAO_0000115> "definition")
+
+# Annotation Property: <http://purl.obolibrary.org/obo/RO_0002175> (present in taxon)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/RO_0002175> "S present_in_taxon T if some instance of T has some S. This does not means that all instances of T have an S - it may only be certain life stages or sexes that have S")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/OMO_0002000> <http://purl.obolibrary.org/obo/RO_0002175> "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX owl: <http://www.w3.org/2002/07/owl#>
+PREFIX in_taxon: <http://purl.obolibrary.org/obo/RO_0002162>
+PREFIX present_in_taxon: <http://purl.obolibrary.org/obo/RO_0002175>
+CONSTRUCT {
+  in_taxon: a owl:ObjectProperty .
+  ?witness rdfs:label ?label .
+  ?witness rdfs:subClassOf ?x .
+  ?witness rdfs:subClassOf [
+    a owl:Restriction ;
+    owl:onProperty in_taxon: ;
+    owl:someValuesFrom ?taxon
+  ] .
+}
+WHERE {
+  ?x present_in_taxon: ?taxon .
+  BIND(IRI(CONCAT(
+    \"http://purl.obolibrary.org/obo/RO_0002175#\",
+    MD5(STR(?x)),
+    \"-\",
+    MD5(STR(?taxon))
+  )) as ?witness)
+  BIND(CONCAT(STR(?x), \" in taxon \", STR(?taxon)) AS ?label)
+}")
+AnnotationAssertion(rdfs:comment <http://purl.obolibrary.org/obo/RO_0002175> "The SPARQL expansion for this relation introduces new named classes into the ontology. For this reason it is likely that the expansion should only be performed during a QC pipeline; the expanded output should usually not be included in a published version of the ontology.")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/RO_0002175> "present in taxon")
+AnnotationAssertion(rdfs:seeAlso <http://purl.obolibrary.org/obo/RO_0002175> <https://github.com/obophenotype/uberon/wiki/Taxon-constraints>)
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#hasDbXref> (has cross-reference)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://www.geneontology.org/formats/oboInOwl#hasDbXref> "An annotation property that links an ontology entity or a statement to a prefixed identifier or URI.")
+AnnotationAssertion(<http://purl.org/dc/terms/contributor> <http://www.geneontology.org/formats/oboInOwl#hasDbXref> <https://orcid.org/0000-0002-7356-1779>)
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasDbXref> "database_cross_reference")
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasDbXref> "has cross-reference")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> (has exact synonym)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> "An alternative label for a class or property which has the exact same meaning than the preferred name/primary label.")
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> "has exact synonym")
+AnnotationAssertion(rdfs:seeAlso <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> "https://github.com/information-artifact-ontology/ontology-metadata/issues/20")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> (has narrow synonym)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> "An alternative label for a class or property which has a more specific meaning than the preferred name/primary label.")
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> "has narrow synonym")
+AnnotationAssertion(rdfs:seeAlso <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> "https://github.com/information-artifact-ontology/ontology-metadata/issues/19")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> (has_obo_namespace)
+
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> "has_obo_namespace")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> (has related synonym)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> "An alternative label for a class or property that has been used synonymously with the primary term name, but the usage is not strictly correct.")
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> "has related synonym")
+AnnotationAssertion(rdfs:seeAlso <http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> "https://github.com/information-artifact-ontology/ontology-metadata/issues/21")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#inSubset> (in_subset)
+
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#inSubset> "in_subset")
+
+# Annotation Property: <http://www.geneontology.org/formats/oboInOwl#shorthand> (shorthand)
+
+AnnotationAssertion(rdfs:label <http://www.geneontology.org/formats/oboInOwl#shorthand> "shorthand")
+
+
+############################
+#   Object Properties
+############################
+
+# Object Property: <http://purl.obolibrary.org/obo/BFO_0000051> (has part)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/BFO_0000051> "a core relation that holds between a whole and its part")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> <http://purl.obolibrary.org/obo/BFO_0000051> "BFO:0000051")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/BFO_0000051> "external")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/BFO_0000051> "has_part")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/valid_for_go_annotation_extension>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/valid_for_go_ontology>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/valid_for_gocam>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#shorthand> <http://purl.obolibrary.org/obo/BFO_0000051> "has_part")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/BFO_0000051> "has part")
+TransitiveObjectProperty(<http://purl.obolibrary.org/obo/BFO_0000051>)
+
+# Object Property: <http://purl.obolibrary.org/obo/RO_0000053> (has characteristic)
+
+AnnotationAssertion(<http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/RO_0000053> "Inverse of characteristic_of")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> <http://purl.obolibrary.org/obo/RO_0000053> "RO:0000053")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/RO_0000053> "external")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/RO_0000053> "bearer_of")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#shorthand> <http://purl.obolibrary.org/obo/RO_0000053> "bearer_of")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/RO_0000053> "has characteristic")
+InverseFunctionalObjectProperty(<http://purl.obolibrary.org/obo/RO_0000053>)
+
+
+
+############################
+#   Classes
+############################
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000000> (cell)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "CARO:mah") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000000> "A material entity of anatomical origin (part of or deriving from an organism) that has as its parts a maximally connected cell compartment surrounded by a plasma membrane.")
+AnnotationAssertion(rdfs:comment <http://purl.obolibrary.org/obo/CL_0000000> "The definition of cell is intended to represent all cells, and thus a cell is defined as a material entity and not an anatomical structure, which implies that it is part of an organism (or the entirety of one).")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000000> "cell")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000000> <http://purl.obolibrary.org/obo/UBERON_0000061>)
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000113> (mononuclear phagocyte)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:add") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:tfm") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "ISBN:0781735149") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000113> "A vertebrate phagocyte with a single nucleus.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/CL_0000113> <http://purl.obolibrary.org/obo/NCBITaxon_9606>)
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000113> "mononuclear phagocyte")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000113> <http://purl.obolibrary.org/obo/CL_0000842>)
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000235> (macrophage)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:add") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:tfm") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GO_REF:0000031") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "PMID:16213494") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "PMID:1919437") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000235> "A mononuclear phagocyte present in variety of tissues, typically differentiated from monocytes, capable of phagocytosing a variety of extracellular particulate material, including immune complexes, microorganisms, and dead cells.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/CL_0000235> <http://purl.obolibrary.org/obo/NCBITaxon_9606>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000235> "histiocyte")
+AnnotationAssertion(rdfs:comment <http://purl.obolibrary.org/obo/CL_0000235> "Morphology: Diameter 30_M-80 _M, abundant cytoplasm, low N/C ratio, eccentric nucleus. Irregular shape with pseudopods, highly adhesive. Contain vacuoles and phagosomes, may contain azurophilic granules; markers: Mouse & Human: CD68, in most cases CD11b. Mouse: in most cases F4/80+; role or process: immune, antigen presentation, & tissue remodelling; lineage: hematopoietic, myeloid.")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000235> "macrophage")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000235> <http://purl.obolibrary.org/obo/CL_0000113>)
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000583> (alveolar macrophage)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:ana") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:dsd") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:tfm") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GO_REF:0000031") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "MESH:D016676") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000583> "A tissue-resident macrophage found in the alveoli of the lungs. Ingests small inhaled particles resulting in degradation and presentation of the antigen to immunocompetent cells. Markers include F4/80-positive, CD11b-/low, CD11c-positive, CD68-positive, sialoadhesin-positive, dectin-1-positive, MR-positive, CX3CR1-negative.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/CL_0000583> <http://purl.obolibrary.org/obo/NCBITaxon_9606>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> <http://purl.obolibrary.org/obo/CL_0000583> "FMA:83023")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000583> "dust cell")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> <http://purl.obolibrary.org/obo/CL_0000583> "MF.Lu")
+AnnotationAssertion(rdfs:comment <http://purl.obolibrary.org/obo/CL_0000583> "Markers: Mouse: F4/80mid, CD11b-/low, CD11c+, CD68+, sialoadhesin+, dectin-1+, MR+, CX3CR1-.")
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "https://doi.org/10.5281/zenodo.11165918") rdfs:comment <http://purl.obolibrary.org/obo/CL_0000583> "The marker set MSR1, FABP4 can identify the Human cell type alveolar macrophage in the Lung with a confidence of 0.80 (NS-Forest FBeta value).")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000583> "alveolar macrophage")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000583> <http://purl.obolibrary.org/obo/CL_0000235>)
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000738> (leukocyte)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:add") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:tfm") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "ISBN:978-0-323-05290-0") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000738> "An achromatic cell of the myeloid or lymphoid lineages capable of ameboid movement, found in blood or other tissue.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/CL_0000738> <http://purl.obolibrary.org/obo/NCBITaxon_9606>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000738> "leucocyte")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000738> "white blood cell")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasRelatedSynonym> <http://purl.obolibrary.org/obo/CL_0000738> "immune cell")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000738> "leukocyte")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000738> <http://purl.obolibrary.org/obo/CL_0000988>)
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000842> (mononuclear cell)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:add") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000842> "A leukocyte with a single non-segmented nucleus in the mature form.")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000842> "mononuclear leukocyte")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> <http://purl.obolibrary.org/obo/CL_0000842> "peripheral blood mononuclear cell")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000842> "mononuclear cell")
+EquivalentClasses(<http://purl.obolibrary.org/obo/CL_0000842> ObjectIntersectionOf(<http://purl.obolibrary.org/obo/CL_0000738> ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0001407>)))
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000842> <http://purl.obolibrary.org/obo/CL_0000738>)
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000842> ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/GO_0005634>))
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000842> ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0001407>))
+
+# Class: <http://purl.obolibrary.org/obo/CL_0000988> (hematopoietic cell)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:add") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GO_REF:0000031") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/CL_0000988> "A cell of a hematopoietic lineage.")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000988> "haematopoietic cell")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/CL_0000988> "hemopoietic cell")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/CL_0000988> "hematopoietic cell")
+SubClassOf(<http://purl.obolibrary.org/obo/CL_0000988> <http://purl.obolibrary.org/obo/CL_0000000>)
+
+# Class: <http://purl.obolibrary.org/obo/GO_0005634> (nucleus)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:go_curators") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/GO_0005634> "A membrane-bounded organelle of eukaryotic cells in which chromosomes are housed and replicated. In most cells, the nucleus contains all of the cell's chromosomes except the organellar chromosomes, and is the site of RNA synthesis and processing. In some species, or in specialized cell types, RNA metabolism or DNA replication may be absent.")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/GO_0005634> "cell nucleus")
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:al") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:mah") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "GOC:vw") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "PMID:15030757") <http://www.geneontology.org/formats/oboInOwl#hasNarrowSynonym> <http://purl.obolibrary.org/obo/GO_0005634> "horsetail nucleus")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/GO_0005634> "cellular_component")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/GO_0005634> "GO:0005634")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/GO_0005634> "nucleus")
+SubClassOf(<http://purl.obolibrary.org/obo/GO_0005634> <http://purl.obolibrary.org/obo/UBERON_0000061>)
+SubClassOf(<http://purl.obolibrary.org/obo/GO_0005634> ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/UBERON_0000061>))
+
+# Class: <http://purl.obolibrary.org/obo/PATO_0001407> (mononucleate)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "Biology-online:Biology-online") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/PATO_0001407> "A nucleate quality inhering in a bearer by virtue of the bearer's having one nucleus.")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/PATO_0001407> "quality")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/PATO_0001407> "PATO:0001407")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/PATO_0001407> <http://purl.obolibrary.org/obo/pato#cell_quality>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/PATO_0001407> <http://purl.obolibrary.org/obo/pato#mpath_slim>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#inSubset> <http://purl.obolibrary.org/obo/PATO_0001407> <http://purl.obolibrary.org/obo/pato#value_slim>)
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/PATO_0001407> "mononucleate")
+
+# Class: <http://purl.obolibrary.org/obo/PATO_0010006> (cell morphology)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "https://orcid.org/0000-0002-7073-9172") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/PATO_0010006> "A quality of a single cell inhering in the bearer by virtue of the bearer's size or shape or structure.")
+AnnotationAssertion(<http://purl.org/dc/terms/contributor> <http://purl.obolibrary.org/obo/PATO_0010006> <https://orcid.org/0000-0002-7073-9172>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#creation_date> <http://purl.obolibrary.org/obo/PATO_0010006> "2021-01-23T11:31:53Z")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/PATO_0010006> "quality")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/PATO_0010006> "PATO:0010006")
+AnnotationAssertion(rdfs:comment <http://purl.obolibrary.org/obo/PATO_0010006> "Use this term for morphologies that can *only* inhere in a cell, e.g. morphological qualities inhering in a cell by virtue of the presence, location or shape of one or more cell parts.")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/PATO_0010006> "cell morphology")
+
+# Class: <http://purl.obolibrary.org/obo/UBERON_0000061> (anatomical structure)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "CARO:0000003") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/UBERON_0000061> "Material anatomical entity that is a single connected structure with inherent 3D shape generated by coordinated expression of the organism's own genome.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000061> <http://purl.obolibrary.org/obo/NCBITaxon_33090>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000061> <http://purl.obolibrary.org/obo/NCBITaxon_33208>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000061> <http://purl.obolibrary.org/obo/NCBITaxon_4751>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/UBERON_0000061> "biological structure")
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "CARO:0000003") <http://www.geneontology.org/formats/oboInOwl#hasExactSynonym> <http://purl.obolibrary.org/obo/UBERON_0000061> "connected biological structure")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/UBERON_0000061> "uberon")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/UBERON_0000061> "UBERON:0000061")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/UBERON_0000061> "anatomical structure")
+SubClassOf(<http://purl.obolibrary.org/obo/UBERON_0000061> <http://purl.obolibrary.org/obo/UBERON_0000465>)
+
+# Class: <http://purl.obolibrary.org/obo/UBERON_0000465> (material anatomical entity)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "http://orcid.org/0000-0001-9114-8737") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/UBERON_0000465> "Anatomical entity that has mass.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000465> <http://purl.obolibrary.org/obo/NCBITaxon_33090>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000465> <http://purl.obolibrary.org/obo/NCBITaxon_33208>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0000465> <http://purl.obolibrary.org/obo/NCBITaxon_4751>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/UBERON_0000465> "uberon")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/UBERON_0000465> "UBERON:0000465")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/UBERON_0000465> "material anatomical entity")
+SubClassOf(<http://purl.obolibrary.org/obo/UBERON_0000465> <http://purl.obolibrary.org/obo/UBERON_0001062>)
+
+# Class: <http://purl.obolibrary.org/obo/UBERON_0001062> (anatomical entity)
+
+AnnotationAssertion(Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "FMA:62955") Annotation(<http://www.geneontology.org/formats/oboInOwl#hasDbXref> "http://orcid.org/0000-0001-9114-8737") <http://purl.obolibrary.org/obo/IAO_0000115> <http://purl.obolibrary.org/obo/UBERON_0001062> "Biological entity that is either an individual member of a biological species or constitutes the structural organization of an individual member of a biological species.")
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0001062> <http://purl.obolibrary.org/obo/NCBITaxon_33090>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0001062> <http://purl.obolibrary.org/obo/NCBITaxon_33208>)
+AnnotationAssertion(<http://purl.obolibrary.org/obo/RO_0002175> <http://purl.obolibrary.org/obo/UBERON_0001062> <http://purl.obolibrary.org/obo/NCBITaxon_4751>)
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#hasOBONamespace> <http://purl.obolibrary.org/obo/UBERON_0001062> "uberon")
+AnnotationAssertion(<http://www.geneontology.org/formats/oboInOwl#id> <http://purl.obolibrary.org/obo/UBERON_0001062> "UBERON:0001062")
+AnnotationAssertion(rdfs:label <http://purl.obolibrary.org/obo/UBERON_0001062> "anatomical entity")
+
+
+SubClassOf(ObjectIntersectionOf(<http://purl.obolibrary.org/obo/CL_0000000> ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0001407>)) ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/BFO_0000051> <http://purl.obolibrary.org/obo/GO_0005634>))
+SubClassOf(ObjectSomeValuesFrom(<http://purl.obolibrary.org/obo/RO_0000053> <http://purl.obolibrary.org/obo/PATO_0010006>) <http://purl.obolibrary.org/obo/CL_0000000>)
+)
\ No newline at end of file
diff --git a/robot-command/src/main/java/org/obolibrary/robot/CommandLineHelper.java b/robot-command/src/main/java/org/obolibrary/robot/CommandLineHelper.java
index 01dccc93..823528e0 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/CommandLineHelper.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/CommandLineHelper.java
@@ -638,6 +638,10 @@ public class CommandLineHelper {
     // Determine if OBO structure should be enforced
     boolean checkOBO = CommandLineHelper.getBooleanValue(line, "check", true);
 
+    // And if we should produce "clean" OBO output
+    EnumSet<OBOWriteOption> cleanOBO =
+        OBOWriteOption.fromString(line.getOptionValue("clean-obo", "false"));
+
     // Determine if prefixes should be added to the header of output
     // Create a map of these to include in output (or an empty map)
     Map<String, String> addPrefixes = getAddPrefixes(line);
@@ -664,7 +668,8 @@ public class CommandLineHelper {
           String formatName = FilenameUtils.getExtension(path);
           thisDf = IOHelper.getFormat(formatName);
         }
-        ioHelper.saveOntology(ontology, thisDf, IRI.create(new File(path)), addPrefixes, checkOBO);
+        ioHelper.saveOntology(
+            ontology, thisDf, IRI.create(new File(path)), addPrefixes, checkOBO, cleanOBO);
       } catch (IllegalArgumentException e) {
         // Exception from getFormat -- invalid format
         throw new IllegalArgumentException(
diff --git a/robot-command/src/main/java/org/obolibrary/robot/ConvertCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ConvertCommand.java
index 6dea9abe..ec136f74 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/ConvertCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/ConvertCommand.java
@@ -43,6 +43,7 @@ public class ConvertCommand implements Command {
     o.addOption("o", "output", true, "save ontology to a file");
     o.addOption("f", "format", true, "the format: obo, owl, ttl, owx, omn, ofn, json");
     o.addOption("c", "check", true, "if false, ignore OBO document structure checks");
+    o.addOption(null, "clean-obo", true, "options for clean OBO output (false|true|strict|simple)");
     options = o;
   }
 
diff --git a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
index 24fc21dd..2cbacf18 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
@@ -34,6 +34,7 @@ import org.geneontology.obographs.core.model.GraphDocument;
 import org.geneontology.obographs.owlapi.FromOwl;
 import org.geneontology.obographs.owlapi.OboGraphJsonDocumentFormat;
 import org.obolibrary.obo2owl.OWLAPIOwl2Obo;
+import org.obolibrary.obo2owl.Obo2OWLConstants;
 import org.obolibrary.oboformat.model.FrameStructureException;
 import org.obolibrary.oboformat.model.OBODoc;
 import org.obolibrary.oboformat.writer.OBOFormatWriter;
@@ -45,6 +46,7 @@ import org.semanticweb.owlapi.model.*;
 import org.semanticweb.owlapi.rdf.rdfxml.renderer.IllegalElementNameException;
 import org.semanticweb.owlapi.rdf.rdfxml.renderer.XMLWriterPreferences;
 import org.semanticweb.owlapi.util.DefaultPrefixManager;
+import org.semanticweb.owlapi.vocab.OWLRDFVocabulary;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.yaml.snakeyaml.Yaml;
@@ -850,12 +852,37 @@ public class IOHelper {
       Map<String, String> addPrefixes,
       boolean checkOBO)
       throws IOException {
+    return saveOntology(
+        ontology, format, ontologyIRI, addPrefixes, checkOBO, EnumSet.noneOf(OBOWriteOption.class));
+  }
+
+  /**
+   * Save an ontology in the given format to an IRI, with option to add prefixes and options to
+   * ignore OBO document checks and produce a "clean" OBO output.
+   *
+   * @param ontology the ontology to save
+   * @param format the ontology format to use
+   * @param ontologyIRI the IRI to save the ontology to
+   * @param addPrefixes map of prefixes to add to header
+   * @param checkOBO if false, ignore OBO document checks
+   * @param cleanOBO optional parameters for OBO output
+   * @return the saved ontology
+   * @throws IOException on any problem
+   */
+  public OWLOntology saveOntology(
+      final OWLOntology ontology,
+      OWLDocumentFormat format,
+      IRI ontologyIRI,
+      Map<String, String> addPrefixes,
+      boolean checkOBO,
+      EnumSet<OBOWriteOption> cleanOBO)
+      throws IOException {
     // Determine the format if not provided
     logger.debug("Saving ontology as {} with to IRI {}", format, ontologyIRI);
     XMLWriterPreferences.getInstance().setUseNamespaceEntities(getXMLEntityFlag());
     // If saving in compressed format, get byte data then save to gzip
     if (ontologyIRI.toString().endsWith(".gz")) {
-      byte[] data = getOntologyFileData(ontology, format, checkOBO);
+      byte[] data = getOntologyFileData(ontology, format, checkOBO, cleanOBO);
       saveCompressedOntology(data, ontologyIRI);
       return ontology;
     }
@@ -873,7 +900,7 @@ public class IOHelper {
     if (addPrefixes != null && !addPrefixes.isEmpty()) {
       addPrefixes(format, addPrefixes);
     }
-    saveOntologyFile(ontology, format, ontologyIRI, checkOBO);
+    saveOntologyFile(ontology, format, ontologyIRI, checkOBO, cleanOBO);
     return ontology;
   }
 
@@ -1613,17 +1640,22 @@ public class IOHelper {
   }
 
   /**
-   * Given an ontology, a document format, and a boolean indicating to check OBO formatting, return
-   * the ontology file in the OWLDocumentFormat as a byte array.
+   * Given an ontology, a document format, a boolean indicating to check OBO formatting, and a set
+   * of options for OBO output, return the ontology file in the OWLDocumentFormat as a byte array.
    *
    * @param ontology OWLOntology to save
    * @param format OWLDocumentFormat to save in
-   * @param checkOBO boolean indiciating to check OBO formatting
+   * @param checkOBO boolean indicating to check OBO formatting
+   * @param cleanOBO optional parameters for OBO output
    * @return byte array of formatted ontology data
    * @throws IOException on any problem
    */
   private byte[] getOntologyFileData(
-      final OWLOntology ontology, OWLDocumentFormat format, boolean checkOBO) throws IOException {
+      final OWLOntology ontology,
+      OWLDocumentFormat format,
+      boolean checkOBO,
+      EnumSet<OBOWriteOption> cleanOBO)
+      throws IOException {
     byte[] data;
     // first handle any non-official output formats.
     // currently this is just OboGraphs JSON format
@@ -1632,9 +1664,8 @@ public class IOHelper {
       GraphDocument gd = fromOwl.generateGraphDocument(ontology);
       String doc = OgJsonGenerator.render(gd);
       data = doc.getBytes();
-    } else if (format instanceof OBODocumentFormat && !checkOBO) {
-      OWLAPIOwl2Obo bridge = new OWLAPIOwl2Obo(ontology.getOWLOntologyManager());
-      OBODoc oboOntology = bridge.convert(ontology);
+    } else if (format instanceof OBODocumentFormat && (!checkOBO || !cleanOBO.isEmpty())) {
+      OBODoc oboOntology = makeCleanOBODocument(ontology, cleanOBO);
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(baos))) {
         OBOFormatWriter oboWriter = new OBOFormatWriter();
@@ -1727,17 +1758,23 @@ public class IOHelper {
   }
 
   /**
-   * Given an ontology, a format, an IRI to save to, and a boolean indiciating to check OBO
-   * formatting, save the ontology in the given format to a file at the IRI.
+   * Given an ontology, a format, an IRI to save to, a boolean indicating to check OBO formatting
+   * and a set of options for OBO output, save the ontology in the given format to a file at the
+   * IRI.
    *
    * @param ontology OWLOntology to save
    * @param format OWLDocumentFormat to save in
    * @param ontologyIRI IRI to save to
    * @param checkOBO boolean indicating to check OBO formatting
+   * @param cleanOBO optional parameters for OBO output
    * @throws IOException on any problem
    */
   private void saveOntologyFile(
-      final OWLOntology ontology, OWLDocumentFormat format, IRI ontologyIRI, boolean checkOBO)
+      final OWLOntology ontology,
+      OWLDocumentFormat format,
+      IRI ontologyIRI,
+      boolean checkOBO,
+      EnumSet<OBOWriteOption> cleanOBO)
       throws IOException {
     // first handle any non-official output formats.
     // currently this is just OboGraphs JSON format
@@ -1756,6 +1793,13 @@ public class IOHelper {
       mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
       ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter();
       writer.writeValue(new FileOutputStream(outfile), gd);
+    } else if (format instanceof OBODocumentFormat && !cleanOBO.isEmpty()) {
+      // OBO format can be handled natively by the OWLAPI ontology manager, but in "clean" mode we
+      // need to call the OBO converter ourselves
+      OBODoc oboDoc = makeCleanOBODocument(ontology, cleanOBO);
+      OBOFormatWriter oboWriter = new OBOFormatWriter();
+      oboWriter.setCheckStructure(checkOBO);
+      oboWriter.write(oboDoc, new File(ontologyIRI.toURI()));
     } else {
       // use native save functionality
       try {
@@ -1812,4 +1856,52 @@ public class IOHelper {
       }
     }
   }
+
+  /**
+   * Convert an ontology to an OBO document, tweaking the ontology as needed to produce a "clean"
+   * output.
+   *
+   * <p>Note that apart from the removal of "untranslatable axioms", which only affects the OBO
+   * document and leaves the original ontology untouched, all operations performed here irreversibly
+   * modify the ontology.
+   *
+   * @param ontology the ontology to convert
+   * @param options option set dictating what should be cleaned in the ontology
+   * @return the resulting OBO document
+   */
+  private OBODoc makeCleanOBODocument(OWLOntology ontology, EnumSet<OBOWriteOption> options) {
+    if (options.contains(OBOWriteOption.DROP_GCI_AXIOMS)) {
+      Set<OWLClassAxiom> gciAxioms = ontology.getGeneralClassAxioms();
+      if (!gciAxioms.isEmpty()) {
+        ontology.getOWLOntologyManager().removeAxioms(ontology, gciAxioms);
+        logger.warn("Removing {} GCI axioms", gciAxioms.size());
+      }
+    }
+
+    Set<IRI> dropExtraProperties = new HashSet<>();
+    if (options.contains(OBOWriteOption.DROP_EXTRA_LABELS)) {
+      dropExtraProperties.add(OWLRDFVocabulary.RDFS_LABEL.getIRI());
+    }
+    if (options.contains(OBOWriteOption.DROP_EXTRA_DEFINITIONS)) {
+      dropExtraProperties.add(Obo2OWLConstants.Obo2OWLVocabulary.IRI_IAO_0000115.getIRI());
+    }
+    if (options.contains(OBOWriteOption.DROP_EXTRA_COMMENTS)) {
+      dropExtraProperties.add(OWLRDFVocabulary.RDFS_COMMENT.getIRI());
+    }
+    if (!dropExtraProperties.isEmpty()) {
+      OntologyHelper.removeExtraAnnotations(ontology, dropExtraProperties);
+    }
+
+    if (options.contains(OBOWriteOption.MERGE_COMMENTS)) {
+      Set<IRI> mergeProperties = new HashSet<>();
+      mergeProperties.add(OWLRDFVocabulary.RDFS_COMMENT.getIRI());
+      OntologyHelper.mergeExtraAnnotations(ontology, mergeProperties);
+    }
+
+    // Dropping the unstranslatable axioms is delegated to the OWLAPI's built-in feature
+    OWLAPIOwl2Obo oboConverter = new OWLAPIOwl2Obo(ontology.getOWLOntologyManager());
+    oboConverter.setDiscardUntranslatable(
+        options.contains(OBOWriteOption.DROP_UNSTRANSLATABLE_AXIOMS));
+    return oboConverter.convert(ontology);
+  }
 }
diff --git a/robot-core/src/main/java/org/obolibrary/robot/OBOWriteOption.java b/robot-core/src/main/java/org/obolibrary/robot/OBOWriteOption.java
new file mode 100644
index 00000000..ea7f4ff1
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/OBOWriteOption.java
@@ -0,0 +1,82 @@
+package org.obolibrary.robot;
+
+import java.util.EnumSet;
+
+/** Possible behaviors when writing an ontology into the OBO format. */
+public enum OBOWriteOption {
+
+  /** For each entity, drop all supernumerary labels beyond the first one. */
+  DROP_EXTRA_LABELS,
+
+  /** For each entity, drop all supernumerary definitions beyond the first one. */
+  DROP_EXTRA_DEFINITIONS,
+
+  /** For each entity, drop all supernumerary comments beyond the first one. */
+  DROP_EXTRA_COMMENTS,
+
+  /** For each entity, merge all comments (if more than one) into a single comment. */
+  MERGE_COMMENTS,
+
+  /**
+   * Drop axioms that cannot be represented in OBO format, instead of writing them in a {@code
+   * owl-axioms} header tag.
+   */
+  DROP_UNSTRANSLATABLE_AXIOMS,
+
+  /** Drop general concept inclusion axioms. */
+  DROP_GCI_AXIOMS;
+
+  /**
+   * Gets a set of OBOWriteOption from a space-separated list of keywords.
+   *
+   * @param s String to turn into a option set
+   * @return the corresponding option set
+   */
+  public static EnumSet<OBOWriteOption> fromString(String s) {
+    EnumSet<OBOWriteOption> set = EnumSet.noneOf(OBOWriteOption.class);
+    for (String item : s.split(" ")) {
+      switch (item) {
+        case "drop-extra-labels":
+          set.add(DROP_EXTRA_LABELS);
+          break;
+
+        case "drop-extra-definitions":
+          set.add(DROP_EXTRA_DEFINITIONS);
+          break;
+
+        case "drop-extra-comments":
+          set.remove(MERGE_COMMENTS);
+          set.add(DROP_EXTRA_COMMENTS);
+          break;
+
+        case "merge-comments":
+          set.remove(DROP_EXTRA_COMMENTS);
+          set.add(MERGE_COMMENTS);
+          break;
+
+        case "drop-unstranslatable-axioms":
+          set.add(DROP_UNSTRANSLATABLE_AXIOMS);
+          break;
+
+        case "drop-gci-axioms":
+          set.add(DROP_GCI_AXIOMS);
+          break;
+
+        case "simple":
+          // "simple" is a shortcut for all the options above except "merge-comments"
+          set.add(DROP_UNSTRANSLATABLE_AXIOMS);
+          set.add(DROP_GCI_AXIOMS);
+          // Fall-through
+
+        case "strict":
+        case "true":
+          // "strict" and "true" are shortcuts for all the "drop-extra-*" options
+          set.add(DROP_EXTRA_LABELS);
+          set.add(DROP_EXTRA_DEFINITIONS);
+          set.add(DROP_EXTRA_COMMENTS);
+          break;
+      }
+    }
+    return set;
+  }
+}
diff --git a/robot-core/src/main/java/org/obolibrary/robot/OntologyHelper.java b/robot-core/src/main/java/org/obolibrary/robot/OntologyHelper.java
index d0376bc3..62b490ed 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/OntologyHelper.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/OntologyHelper.java
@@ -1780,4 +1780,160 @@ public class OntologyHelper {
       manager.addAxiom(ontology, cleanedAxiom);
     }
   }
+
+  /**
+   * Removes supernumerary annotations on all entities in the given ontology.
+   *
+   * <p>This method iterates over all entities (excluding imported entities) of the given ontology;
+   * if an entity is found to have more than one annotation for each of the indicated properties,
+   * all annotations beyond the first one are removed.
+   *
+   * @param ontology OWLOntology from which to remove the supernumary annotations
+   * @param properties set of annotation property IRIs to remove
+   */
+  public static void removeExtraAnnotations(OWLOntology ontology, Set<IRI> properties) {
+    Set<OWLAnnotationAssertionAxiom> axiomsToRemove = new HashSet<>();
+    for (OWLEntity ent : ontology.getSignature(Imports.EXCLUDED)) {
+      IRI entity = ent.getIRI();
+      Map<IRI, Set<OWLAnnotationAssertionAxiom>> annotsByProperty =
+          getAnnotationAssertions(ontology, entity, properties);
+      for (IRI property : properties) {
+        Set<OWLAnnotationAssertionAxiom> axioms = annotsByProperty.get(property);
+        if (axioms != null && axioms.size() > 1) {
+          OWLAnnotationAssertionAxiom axiomToKeep = null;
+          // If only one annotation is language-agnostic, we keep this one
+          int axiomsWithoutLanguageTag = 0;
+          for (OWLAnnotationAssertionAxiom ax : axioms) {
+            OWLAnnotationValue value = ax.getValue();
+            // Non-literal annotations count as language-agnostic
+            if (!value.isLiteral() || !value.asLiteral().get().hasLang()) {
+              axiomsWithoutLanguageTag += 1;
+              axiomToKeep = ax;
+            }
+          }
+          if (axiomsWithoutLanguageTag != 1) {
+            // Can't use language tags to pick up the annotation to keep, so sort them and pick
+            // whichever comes out first
+            axiomToKeep = sortAxioms(axioms).get(0);
+          }
+          axioms.remove(axiomToKeep);
+          axiomsToRemove.addAll(axioms);
+          logger.warn(
+              "Removing {} extra <{}> annotation(s) on <{}>", axioms.size(), property, entity);
+        }
+      }
+    }
+    if (!axiomsToRemove.isEmpty()) {
+      ontology.getOWLOntologyManager().removeAxioms(ontology, axiomsToRemove);
+    }
+  }
+
+  /**
+   * Merges supernumerary annotations on all entities in the given ontology, using a single space
+   * character between each annotation value.
+   *
+   * @param ontology OWLOntology in which to merge supernumerary annotations
+   * @param properties set of annotation property IRIs to merge
+   */
+  public static void mergeExtraAnnotations(OWLOntology ontology, Set<IRI> properties) {
+    mergeExtraAnnotations(ontology, properties, " ");
+  }
+
+  /**
+   * Merges supernumerary annotations on all entities in the given ontology, using a custom
+   * separator between each annotation value.
+   *
+   * <p>This method iterates over all entities (excluding imported entities) of the given ontology;
+   * if an entity is found to have more than one annotation for each of the indicated properties,
+   * they are merged into a single annotation.
+   *
+   * @param ontology OWLOntology in which to merge supernumerary annotations
+   * @param properties set of annotation property IRIs to merge
+   * @param separator Separator string to insert between each annotation value (may be {@code null})
+   */
+  public static void mergeExtraAnnotations(
+      OWLOntology ontology, Set<IRI> properties, String separator) {
+    Set<OWLAnnotationAssertionAxiom> axiomsToRemove = new HashSet<>();
+    Set<OWLAnnotationAssertionAxiom> axiomsToAdd = new HashSet<>();
+    OWLDataFactory factory = ontology.getOWLOntologyManager().getOWLDataFactory();
+    for (OWLEntity ent : ontology.getSignature(Imports.EXCLUDED)) {
+      IRI entity = ent.getIRI();
+      Map<IRI, Set<OWLAnnotationAssertionAxiom>> annotsByProperty =
+          getAnnotationAssertions(ontology, entity, properties);
+      for (IRI property : properties) {
+        Set<OWLAnnotationAssertionAxiom> axioms = annotsByProperty.get(property);
+        if (axioms != null && axioms.size() > 1) {
+          // Check that all annotations have literal values only
+          int nonLiteralValues = 0;
+          for (OWLAnnotationAssertionAxiom ax : axioms) {
+            if (!ax.getValue().isLiteral()) {
+              nonLiteralValues += 1;
+            }
+          }
+          if (nonLiteralValues != 0) {
+            logger.warn("Cannot merge non-literal <{}> annotations on <{}>", property, entity);
+            continue;
+          }
+          // Build the merged annotation axiom
+          StringBuilder sb = new StringBuilder();
+          Set<OWLAnnotation> axiomAnnotations = new HashSet<>();
+          boolean first = true;
+          for (OWLAnnotationAssertionAxiom ax : sortAxioms(axioms)) {
+            if (!first && separator != null) {
+              sb.append(separator);
+            } else {
+              first = false;
+            }
+            sb.append(getValue(ax));
+            axiomAnnotations.addAll(ax.getAnnotations());
+          }
+          axiomsToAdd.add(
+              factory.getOWLAnnotationAssertionAxiom(
+                  factory.getOWLAnnotationProperty(property),
+                  entity,
+                  factory.getOWLLiteral(sb.toString()),
+                  axiomAnnotations));
+          axiomsToRemove.addAll(axioms);
+          logger.warn("Merging {} <{}> annotations on <{}>", axioms.size(), property, ent.getIRI());
+        }
+      }
+    }
+
+    if (!axiomsToRemove.isEmpty()) {
+      ontology.getOWLOntologyManager().removeAxioms(ontology, axiomsToRemove);
+      ontology.getOWLOntologyManager().addAxioms(ontology, axiomsToAdd);
+    }
+  }
+
+  /**
+   * Gets all annotation assertion axioms on a given entity that uses one of the indicated
+   * annotation properties.
+   *
+   * @param ontology OWLOntology from which to get the annotation axioms
+   * @param entity the subject for which to retrieve the annotations
+   * @param properties the annotation properties to look for
+   * @return a map associating property IRIs to annotation assertion axioms
+   */
+  private static Map<IRI, Set<OWLAnnotationAssertionAxiom>> getAnnotationAssertions(
+      OWLOntology ontology, IRI entity, Set<IRI> properties) {
+    Map<IRI, Set<OWLAnnotationAssertionAxiom>> annotsByProperty = new HashMap<>();
+    for (OWLAnnotationAssertionAxiom ax : ontology.getAnnotationAssertionAxioms(entity)) {
+      IRI property = ax.getProperty().getIRI();
+      annotsByProperty.computeIfAbsent(property, k -> new HashSet<>()).add(ax);
+    }
+    return annotsByProperty;
+  }
+
+  /**
+   * Turns a set of annotation axioms into an ordered list.
+   *
+   * @param axioms set of axioms to order
+   * @return an ordered list
+   */
+  private static List<OWLAnnotationAssertionAxiom> sortAxioms(
+      Set<OWLAnnotationAssertionAxiom> axioms) {
+    List<OWLAnnotationAssertionAxiom> asList = new ArrayList<>(axioms);
+    asList.sort((a, b) -> a.compareTo(b));
+    return asList;
+  }
 }


Test patch:
diff --git a/robot-core/src/test/java/org/obolibrary/robot/OntologyHelperTest.java b/robot-core/src/test/java/org/obolibrary/robot/OntologyHelperTest.java
index c3385610..c2d63d6a 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/OntologyHelperTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/OntologyHelperTest.java
@@ -3,6 +3,7 @@ package org.obolibrary.robot;
 import static org.junit.Assert.assertEquals;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -139,4 +140,49 @@ public class OntologyHelperTest extends CoreTest {
     int after = ontology.getClassesInSignature().size();
     TestCase.assertEquals(8, after);
   }
+
+  /**
+   * Test dropping of supernumerary annotations.
+   *
+   * @throws IOException on issue loading ontology
+   */
+  @Test
+  public void testDropExtraAnnotations() throws IOException {
+    OWLOntology ontology = loadOntology("/extra-annotations.ofn");
+    OWLDataFactory df = ontology.getOWLOntologyManager().getOWLDataFactory();
+    OWLAnnotationProperty prop = df.getRDFSLabel();
+    OntologyHelper.removeExtraAnnotations(ontology, Collections.singleton(prop.getIRI()));
+
+    // For :test1, the "Test 1" label should be preserved because it is lexicographically before
+    // "Test one"
+    IRI testIRI = IRI.create(base + "extra-annotations.owl#test1");
+    Set<String> actual = OntologyHelper.getAnnotationStrings(ontology, prop, testIRI);
+    Set<String> expected = Collections.singleton("Test 1");
+    assertEquals(expected, actual);
+
+    // For :test2, the "Test two" label should be preserved because it has no language tag, even
+    // though "Test deux" comes first lexicographically
+    testIRI = IRI.create(base + "extra-annotations.owl#test2");
+    actual = OntologyHelper.getAnnotationStrings(ontology, prop, testIRI);
+    expected = Collections.singleton("Test two");
+    assertEquals(expected, actual);
+  }
+
+  /**
+   * Test merging of supernumerary annotations.
+   *
+   * @throws IOException on issue loading ontology
+   */
+  @Test
+  public void testMergeExtraAnnotations() throws IOException {
+    OWLOntology ontology = loadOntology("/extra-annotations.ofn");
+    OWLDataFactory df = ontology.getOWLOntologyManager().getOWLDataFactory();
+    OWLAnnotationProperty prop = df.getRDFSLabel();
+    OntologyHelper.mergeExtraAnnotations(ontology, Collections.singleton(prop.getIRI()));
+
+    IRI testIRI = IRI.create(base + "extra-annotations.owl#test1");
+    Set<String> actual = OntologyHelper.getAnnotationStrings(ontology, prop, testIRI);
+    Set<String> expected = Collections.singleton("Test 1 Test one");
+    assertEquals(expected, actual);
+  }
 }
diff --git a/robot-core/src/test/resources/extra-annotations.ofn b/robot-core/src/test/resources/extra-annotations.ofn
new file mode 100644
index 00000000..7252876e
--- /dev/null
+++ b/robot-core/src/test/resources/extra-annotations.ofn
@@ -0,0 +1,32 @@
+Prefix(:=<https://github.com/ontodev/robot/robot-core/src/test/resources/extra-annotations.owl#>)
+Prefix(obo:=<http://purl.obolibrary.org/obo/>)
+Prefix(owl:=<http://www.w3.org/2002/07/owl#>)
+Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)
+Prefix(xml:=<http://www.w3.org/XML/1998/namespace>)
+Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)
+Prefix(foaf:=<http://xmlns.com/foaf/0.1/>)
+Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)
+
+
+Ontology(<https://github.com/ontodev/robot/robot-core/src/test/resources/extra-annotations.owl>
+
+Declaration(Class(:test1))
+Declaration(Class(:test2))
+
+
+############################
+#   Classes
+############################
+
+# Class: :test1 (Test 1)
+
+AnnotationAssertion(rdfs:label :test1 "Test 1")
+AnnotationAssertion(rdfs:label :test1 "Test one")
+
+# Class: :test2 (Test 2)
+
+AnnotationAssertion(rdfs:label :test2 "Test deux"@fr)
+AnnotationAssertion(rdfs:label :test2 "Test two")
+
+
+)