Read an OBO-format ontology — the OBO Foundry's own stanza syntax — and translate it into a vaelii corpus.
OBO is the format the life sciences standardized on: the Gene Ontology, ChEBI, Uberon,
the Disease Ontology and about two hundred others, all CC-BY, all built by curators
rather than generated. A .obo file is a header followed by stanzas:
[Term]
id: GO:0000278
name: mitotic cell cycle
namespace: biological_process
def: "The cell cycle of a eukaryotic cell..." [GOC:mah]
is_a: GO:0007049 ! cell cycle
relationship: part_of GO:0051301
[Typedef]
id: part_of
is_transitive: true
inverse_of: has_part
which is why it gets its own reader rather than going through the RDF one: the same
ontologies also publish an OWL serialization, and the OWL is generated from the OBO
by a mapping that turns every relationship: into an existential restriction — which
is precisely the OWL construct that has no Horn form. Reading the OBO directly keeps
the relations vaelii can hold.
| OBO | vaelii |
|---|---|
[Term] | a type (snake_case, named from name:) |
[Typedef] | a predicate (camelCase, named from id:) |
[Instance] | an individual |
namespace: | the context the stanza's sentences land in |
is_a: X | (genl this x) |
relationship: R X | (r this x) |
disjoint_from: X | (disjoint this x) |
intersection_of: (all plain) | both genl edges and the sufficient-condition rule |
union_of: X | (genl x this), each member |
domain: / range: | (argIsa p 1 c) / (argIsa p 2 c) |
is_transitive: and its siblings | (transitive p), (symmetric p), … |
inverse_of: Q | (inverse p q) |
holds_over_chain: A B | (implies (and (a ?x ?y) (b ?y ?z)) (p ?x ?z)) |
transitive_over: R | (implies (and (p ?x ?y) (r ?y ?z)) (p ?x ?z)) |
def: / comment: | (comment t "…") |
name: / synonym: / xref: | (label t "…"), (synonym t "…"), (xref t "…") |
relationship: means here, and what it does notrelationship: part_of GO:0051301 is read as the class-level fact (partOf mitotic_cell_cycle cell_division). That is what the OBO tooling means by it in
practice and what every browser shows, but it is not what the OWL mapping says: there
it is MitoticCellCycle ⊑ ∃part_of.CellDivision, an existential restriction on each
instance. The two agree about the ontology's shape and disagree about what follows for
an individual, and only the first has a form vaelii can store. Taking it is a
deliberate weakening, recorded here so nobody has to rediscover it from the output.
An obsolete term is dropped, with its stanza counted, unless :obsolete? true.
OBO never deletes: a retired term keeps its id, gains is_obsolete: true, and loses
its is_a edges, so importing one gives a term with no place in the hierarchy and a
name that collides with its replacement's.
Read an OBO-format ontology — the OBO Foundry's own stanza syntax — and translate it
into a vaelii corpus.
OBO is the format the life sciences standardized on: the Gene Ontology, ChEBI, Uberon,
the Disease Ontology and about two hundred others, all CC-BY, all built by curators
rather than generated. A `.obo` file is a header followed by stanzas:
[Term]
id: GO:0000278
name: mitotic cell cycle
namespace: biological_process
def: "The cell cycle of a eukaryotic cell..." [GOC:mah]
is_a: GO:0007049 ! cell cycle
relationship: part_of GO:0051301
[Typedef]
id: part_of
is_transitive: true
inverse_of: has_part
which is why it gets its own reader rather than going through the RDF one: the same
ontologies also publish an OWL serialization, and the OWL is *generated from* the OBO
by a mapping that turns every `relationship:` into an existential restriction — which
is precisely the OWL construct that has no Horn form. Reading the OBO directly keeps
the relations vaelii can hold.
## What comes across
| OBO | vaelii |
|------------------------------------|-----------------------------------------------|
| `[Term]` | a **type** (snake_case, named from `name:`) |
| `[Typedef]` | a **predicate** (camelCase, named from `id:`) |
| `[Instance]` | an **individual** |
| `namespace:` | the **context** the stanza's sentences land in |
| `is_a: X` | `(genl this x)` |
| `relationship: R X` | `(r this x)` |
| `disjoint_from: X` | `(disjoint this x)` |
| `intersection_of:` (all plain) | both `genl` edges **and** the sufficient-condition rule |
| `union_of: X` | `(genl x this)`, each member |
| `domain:` / `range:` | `(argIsa p 1 c)` / `(argIsa p 2 c)` |
| `is_transitive:` and its siblings | `(transitive p)`, `(symmetric p)`, … |
| `inverse_of: Q` | `(inverse p q)` |
| `holds_over_chain: A B` | `(implies (and (a ?x ?y) (b ?y ?z)) (p ?x ?z))` |
| `transitive_over: R` | `(implies (and (p ?x ?y) (r ?y ?z)) (p ?x ?z))` |
| `def:` / `comment:` | `(comment t "…")` |
| `name:` / `synonym:` / `xref:` | `(label t "…")`, `(synonym t "…")`, `(xref t "…")` |
## What `relationship:` means here, and what it does not
`relationship: part_of GO:0051301` is read as the **class-level fact** `(partOf
mitotic_cell_cycle cell_division)`. That is what the OBO tooling means by it in
practice and what every browser shows, but it is *not* what the OWL mapping says: there
it is `MitoticCellCycle ⊑ ∃part_of.CellDivision`, an existential restriction on each
instance. The two agree about the ontology's shape and disagree about what follows for
an individual, and only the first has a form vaelii can store. Taking it is a
deliberate weakening, recorded here so nobody has to rediscover it from the output.
**An obsolete term is dropped**, with its stanza counted, unless `:obsolete? true`.
OBO never deletes: a retired term keeps its id, gains `is_obsolete: true`, and loses
its `is_a` edges, so importing one gives a term with no place in the hierarchy and a
name that collides with its replacement's.(classify stanzas opts)Pass 1. {:entries [[id role base] …] :contexts #{namespace …} :header stanza} —
everything pass 2 needs that it cannot see one stanza at a time.
A [Term] is named from its name: and a [Typedef] from its id:, and that
asymmetry is the formats': a term's id is an opaque accession (GO:0000278) and its
name is what every tool shows, while a typedef's id is its name (part_of) and its
name: is a human phrase (part of) that would spell to the same thing anyway.
:used-relations is every relation id referenced anywhere — see relations-used for
why those have to be named even though nothing here declares them.
Pass 1. `{:entries [[id role base] …] :contexts #{namespace …} :header stanza}` —
everything pass 2 needs that it cannot see one stanza at a time.
A `[Term]` is named from its `name:` and a `[Typedef]` from its `id:`, and that
asymmetry is the formats': a term's id is an opaque accession (`GO:0000278`) and its
name is what every tool shows, while a typedef's id *is* its name (`part_of`) and its
`name:` is a human phrase (`part of`) that would spell to the same thing anyway.
`:used-relations` is every relation id referenced anywhere — see `relations-used` for
why those have to be named even though nothing here declares them.(convert! path out-dir)(convert! path out-dir opts)Convert the OBO file at path into a vaelii corpus under out-dir. Two passes: the
first names every stanza, the second writes the sentences. Returns the report map
(also written as report.edn).
Options: :obsolete? keeps obsolete terms (off — see the namespace docstring);
:functional? imports is_functional as vaelii's functional (off); :limit reads
only the first n stanzas.
Convert the OBO file at `path` into a vaelii corpus under `out-dir`. Two passes: the first names every stanza, the second writes the sentences. Returns the report map (also written as `report.edn`). Options: `:obsolete?` keeps obsolete terms (off — see the namespace docstring); `:functional?` imports `is_functional` as vaelii's `functional` (off); `:limit` reads only the first n stanzas.
The convert option that keeps each :filtered drop — see cyc/drop-flags for the
contract and plugin-test for what enforces it.
One entry, and it is the clearest case in the repo: an obsolete term is well-formed
OBO that this reader declines to import, --obsolete imports it, and the only cost of
saying yes is a term with no place in the hierarchy and a name that collides with its
replacement's.
The convert option that keeps each `:filtered` drop — see `cyc/drop-flags` for the contract and `plugin-test` for what enforces it. One entry, and it is the clearest case in the repo: an obsolete term is well-formed OBO that this reader declines to import, `--obsolete` imports it, and the only cost of saying yes is a term with no place in the hierarchy and a name that collides with its replacement's.
What each of this reader's drop reasons is — see corpus/drop-kinds. A reason
missing here counts as :unread.
The two named here would each read as a loss without a kind, and neither is one.
:obsolete is a policy: the stanza is well-formed, this reader declines to import
a term the ontology has retired, and --obsolete imports it after all. PATO retires
919 of its 2,820 stanzas, so a clean conversion of it reads as a third broken until
the kind says otherwise. :existential-intersect is a weakening: the fact is
written and only the biconditional is lost, so uo.obo reports 81 drops of 574
stanzas while losing nothing at all. What is left is :unread and every one of those
is a name the ontology uses and never declares.
What each of this reader's drop reasons **is** — see `corpus/drop-kinds`. A reason missing here counts as `:unread`. The two named here would each read as a loss without a kind, and neither is one. `:obsolete` is a **policy**: the stanza is well-formed, this reader declines to import a term the ontology has retired, and `--obsolete` imports it after all. PATO retires 919 of its 2,820 stanzas, so a clean conversion of it reads as a third broken until the kind says otherwise. `:existential-intersect` is a **weakening**: the fact is written and only the biconditional is lost, so `uo.obo` reports 81 drops of 574 stanzas while losing nothing at all. What is left is `:unread` and every one of those is a name the ontology uses and never declares.
(load-dir! kb dir)(load-dir! kb dir opts)Load the corpus at dir into kb — vaelii.foreign.corpus/load-dir! with this
format's profiles.
Load the corpus at `dir` into `kb` — `vaelii.foreign.corpus/load-dir!` with this format's `profiles`.
(name-table {:keys [entries namespaces used-relations]})The OBO id -> vaelii term map, plus a context term per namespace. Sorted by id so a
collision — two terms sharing a name:, which every large ontology has — resolves the
same way on every run.
Relations referenced but never declared are named last, from their ids. Last, so a declared term always wins a collision against one; named at all, because otherwise every fact stated with an imported relation is dropped.
The OBO id -> vaelii term map, plus a context term per namespace. Sorted by id so a collision — two terms sharing a `name:`, which every large ontology has — resolves the same way on every run. Relations referenced but never declared are named last, from their ids. Last, so a declared term always wins a collision against one; named at all, because otherwise every fact stated with an imported relation is dropped.
(obsolete? stanza)Has this stanza been retired? OBO never deletes — see the namespace docstring for what importing one would cost.
Has this stanza been retired? OBO never deletes — see the namespace docstring for what importing one would cost.
Named subsets. :ontology drops the curation trail and the lexical layer, which on
the Gene Ontology is over half the sentences and none of the inference; :taxonomy
keeps only the hierarchy and the relation declarations.
Named subsets. `:ontology` drops the curation trail and the lexical layer, which on the Gene Ontology is over half the sentences and none of the inference; `:taxonomy` keeps only the hierarchy and the relation declarations.
(quoted-value s)The "…" head of a value like def: "text" [refs] or synonym: "text" EXACT [],
or nil when the value does not start with one.
The `"…"` head of a value like `def: "text" [refs]` or `synonym: "text" EXACT []`, or nil when the value does not start with one.
This format's reader, as the seam (vaelii.impl.foreign) hands it out.
This format's reader, as the seam (`vaelii.impl.foreign`) hands it out.
(stanzas lines)The stanzas of an OBO file, as a lazy seq of {:type "Term" :tags {tag [value …]}}
maps — the header first, as a stanza of type nil.
Tag values keep their order and repeat: is_a appears once per parent, and reading
only the last would silently make the hierarchy a tree.
The stanzas of an OBO file, as a lazy seq of `{:type "Term" :tags {tag [value …]}}`
maps — the header first, as a stanza of type nil.
Tag values keep their order and repeat: `is_a` appears once per parent, and reading
only the last would silently make the hierarchy a tree.(tags stanza t)Every value of t, in file order.
Every value of `t`, in file order.
(translate stanza names default-context opts)The sentences one stanza becomes: {:context C :sentences [[strength sentence] …] :drops {reason n}}.
A sentence carries its own strength rather than the stanza doing so, because an
OBO stanza states both kinds at once: is_a is definitional and synonym is a note
somebody attached, and giving both the same strength would either make a label
irretractable or make the hierarchy defeasible.
The sentences one stanza becomes: `{:context C :sentences [[strength sentence] …]
:drops {reason n}}`.
A sentence carries its **own** strength rather than the stanza doing so, because an
OBO stanza states both kinds at once: `is_a` is definitional and `synonym` is a note
somebody attached, and giving both the same strength would either make a label
irretractable or make the hierarchy defeasible.(true-tag? stanza t)Is boolean tag t present and true? OBO writes a flag as the string, and an
absent flag and an explicit false mean the same thing.
Is boolean tag `t` present and `true`? OBO writes a flag as the string, and an absent flag and an explicit `false` mean the same thing.
(unescape s)OBO's backslash escapes. \W is a space, which is the one that surprises: a tag
value is whitespace-delimited, so a space inside one has to be written.
OBO's backslash escapes. `\W` is a space, which is the one that surprises: a tag value is whitespace-delimited, so a space inside one has to be written.
(with-stanzas path f)Open the OBO file at path, call (f stanzas), close the file after. .gz is
decompressed on the way through. The seq is only valid inside f.
Open the OBO file at `path`, call `(f stanzas)`, close the file after. `.gz` is decompressed on the way through. The seq is only valid inside `f`.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |