Skip to content

Commit

Permalink
Adding utilities for working with disjoint classes. (#650)
Browse files Browse the repository at this point in the history
* Adding utilities for working with disjoint classes.

At the CLI level this adds:

- disjoints command: queries all disjointness axioms
- generate-disjoints command: generates candidate disjoints

Also adds a notebook explaining this

* docs

* lint
  • Loading branch information
cmungall authored Aug 30, 2023
1 parent 8d2a124 commit 428d6e8
Show file tree
Hide file tree
Showing 11 changed files with 1,520 additions and 308 deletions.
738 changes: 556 additions & 182 deletions notebooks/Clinical/OMOP-Example.ipynb

Large diffs are not rendered by default.

311 changes: 201 additions & 110 deletions notebooks/Commands/TermsetSimilarity.ipynb

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions src/oaklib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@
logical_definition_analyzer,
logical_definition_summarizer,
)
from oaklib.utilities.axioms.disjointness_axiom_analyzer import (
DisjointnessInducerConfig,
generate_disjoint_class_expressions_axioms,
)
from oaklib.utilities.iterator_utils import chunk
from oaklib.utilities.kgcl_utilities import (
generate_change_id,
Expand Down Expand Up @@ -3420,6 +3424,82 @@ def _exclude_ldef(ldef: LogicalDefinitionAxiom) -> bool:
writer.file.close()


@main.command()
@click.argument("terms", nargs=-1)
@predicates_option
@autolabel_option
@output_type_option
@click.option(
"--named-classes-only/--no-named-classes-only",
default=False,
show_default=True,
help="Only show disjointness axioms between two named classes.",
)
@output_option
def disjoints(
terms,
predicates: str,
autolabel: bool,
output_type: str,
named_classes_only: bool,
output: str,
):
"""
Show all disjoints for a set of terms, or whole ontology.
Leave off all arguments for defaults - all terms, YAML OboGraph model
serialization:
Example:
runoak -i sqlite:obo:uberon disjoints
Note that this will include pairwise disjoints, setwise disjoints,
disjoint unions, and disjoints involving simple class expressions.
A tabular format can be easier to browse, and includes labels by default:
Example:
runoak -i sqlite:obo:uberon disjoints --autolabel -O csv
To perform this on a subset:
Example:
runoak -i sqlite:obo:cl disjoints --autolabel -O csv .desc//p=i "immune cell"
Data model:
https://w3id.org/oak/obograph
"""
impl = settings.impl
writer = _get_writer(output_type, impl, StreamingYamlWriter)
writer.output = output
writer.autolabel = autolabel
actual_predicates = _process_predicates_arg(predicates)

label_fields = [
"classIds",
"classExpressionPropertyIds",
"classExpressionFillerIds",
"unionEquivalentToFillerId",
"unionEquivalentToPropertyId",
]
if not isinstance(impl, OboGraphInterface):
raise NotImplementedError(f"Cannot execute this using {type(impl)}")
if terms == ".all":
terms = None
term_it = query_terms_iterator(terms, impl) if terms else None
dxas = impl.disjoint_class_expressions_axioms(term_it, predicates=actual_predicates)
for dxa in dxas:
if named_classes_only and dxa.classExpressions:
continue
writer.emit(dxa, label_fields=label_fields)
writer.finish()
writer.file.close()


@main.command()
@output_option
@output_type_option
Expand Down Expand Up @@ -6038,5 +6118,73 @@ def generate_logical_definitions(
writer.file.close()


@main.command()
@click.argument("terms", nargs=-1)
@autolabel_option
@output_option
@predicates_option
@output_type_option
@click.option(
"--min-descendants",
"-M",
default=3,
show_default=True,
help="Minimum number of descendants for a class to have to be considered a candidate.",
)
@click.option(
"--exclude-existing/--no-exclude-existing",
default=True,
show_default=True,
help="Do not report duplicates with existing disjointness axioms.",
)
def generate_disjoints(
terms,
predicates,
autolabel,
output,
output_type,
exclude_existing,
min_descendants,
):
"""
Generate candidate disjointness axioms.
Example:
runoak -i sqlite:obo:iao generate-disjoints -O obo
To generate spatial disjointness axioms:
runoak -i sqlite:obo:zfa generate-disjoints -O obo p i,p
"""
impl = settings.impl
writer = _get_writer(output_type, impl, StreamingYamlWriter, kgcl)
writer.output = output
writer.autolabel = autolabel
if not isinstance(impl, OboGraphInterface):
raise NotImplementedError
curies = list(query_terms_iterator(terms, impl))
actual_predicates = _process_predicates_arg(predicates)
if not actual_predicates:
actual_predicates = [IS_A]
config = DisjointnessInducerConfig(
min_descendants=min_descendants, exclude_existing=exclude_existing
)
dxas = generate_disjoint_class_expressions_axioms(
impl, curies, [actual_predicates], config=config
)
label_fields = [
"classIds",
"classExpressionPropertyIds",
"classExpressionFillerIds",
"unionEquivalentToFillerId",
"unionEquivalentToPropertyId",
]
for dxa in dxas:
writer.emit(dxa, label_fields=label_fields)
writer.finish()
writer.file.close()


if __name__ == "__main__":
main()
125 changes: 114 additions & 11 deletions src/oaklib/datamodels/obograph.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
# Auto generated from obograph.yaml by pythongen.py version: 0.9.0
# Generation date: 2023-05-13T10:22:20
# Auto generated from obograph.yaml by pythongen.py version: 0.0.1
# Generation date: 2023-08-29T10:52:15
# Schema: obographs_datamodel
#
# id: https://github.com/geneontology/obographs
# description: A data model for graph-oriented representations of ontologies. Each ontology is represented as a
# Graph, and multiple ontologies can be connected together in a GraphDocument. The principle elements
# of a Graph are Node objects and Edge objects. A Node represents an arbitrary ontology element,
# including but not limited to the core terms in the ontology. Edges represent simple relationships
# between Nodes. Nodes and Edges can both have Meta objects attached, providing additional metedata.
# Not everything in an ontology can be represented as nodes and edges. More complex axioms have
# specialized structures such as DomainRangeAxiom objects and LogicalDefinitionAxiom.
# description: A data model for graph-oriented representations of ontologies. Each ontology is represented as a Graph, and multiple ontologies can be connected together in a GraphDocument.
# The principle elements of a Graph are Node objects and Edge objects. A Node represents an arbitrary ontology element, including but not limited to the core terms in the ontology. Edges represent simple relationships between Nodes. Nodes and Edges can both have Meta objects attached, providing additional metedata.
# Not everything in an ontology can be represented as nodes and edges. More complex axioms have specialized structures such as DomainRangeAxiom objects and LogicalDefinitionAxiom.
# license: https://creativecommons.org/publicdomain/zero/1.0/

import dataclasses
import re
import sys
from dataclasses import dataclass
from typing import Any, ClassVar, Dict, List, Optional, Union

Expand Down Expand Up @@ -819,6 +814,59 @@ def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
super().__post_init__(**kwargs)


@dataclass
class DisjointClassExpressionsAxiom(Axiom):
"""
An axiom that defines a set of classes or class expressions as being mutually disjoint. Formally, there exists no
instance that instantiates more that one of the union of classIds and classExpressions.
"""

_inherited_slots: ClassVar[List[str]] = []

class_class_uri: ClassVar[URIRef] = OBOGRAPHS.DisjointClassExpressionsAxiom
class_class_curie: ClassVar[str] = "obographs:DisjointClassExpressionsAxiom"
class_name: ClassVar[str] = "DisjointClassExpressionsAxiom"
class_model_uri: ClassVar[URIRef] = OBOGRAPHS.DisjointClassExpressionsAxiom

classIds: Optional[Union[str, List[str]]] = empty_list()
classExpressions: Optional[
Union[
Union[dict, ExistentialRestrictionExpression],
List[Union[dict, ExistentialRestrictionExpression]],
]
] = empty_list()
unionEquivalentTo: Optional[str] = None
unionEquivalentToExpression: Optional[Union[dict, ExistentialRestrictionExpression]] = None

def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
if not isinstance(self.classIds, list):
self.classIds = [self.classIds] if self.classIds is not None else []
self.classIds = [v if isinstance(v, str) else str(v) for v in self.classIds]

if not isinstance(self.classExpressions, list):
self.classExpressions = (
[self.classExpressions] if self.classExpressions is not None else []
)
self.classExpressions = [
v
if isinstance(v, ExistentialRestrictionExpression)
else ExistentialRestrictionExpression(**as_dict(v))
for v in self.classExpressions
]

if self.unionEquivalentTo is not None and not isinstance(self.unionEquivalentTo, str):
self.unionEquivalentTo = str(self.unionEquivalentTo)

if self.unionEquivalentToExpression is not None and not isinstance(
self.unionEquivalentToExpression, ExistentialRestrictionExpression
):
self.unionEquivalentToExpression = ExistentialRestrictionExpression(
**as_dict(self.unionEquivalentToExpression)
)

super().__post_init__(**kwargs)


@dataclass
class PropertyChainAxiom(Axiom):
"""
Expand Down Expand Up @@ -873,7 +921,7 @@ class ScopeEnum(EnumDefinitionImpl):
)
hasRelatedSynonym = PermissibleValue(
text="hasRelatedSynonym",
description="The synonym represents something closely related in meaning than the node, but in not exact, broad, or narrow.",
description="""The synonym represents something closely related in meaning than the node, but in not exact, broad, or narrow.""",
meaning=OIO.hasRelatedSynonym,
)

Expand Down Expand Up @@ -1118,6 +1166,20 @@ class slots:
],
)

slots.disjointClassExpressionsAxioms = Slot(
uri=OBOGRAPHS.disjointClassExpressionsAxioms,
name="disjointClassExpressionsAxioms",
curie=OBOGRAPHS.curie("disjointClassExpressionsAxioms"),
model_uri=OBOGRAPHS.disjointClassExpressionsAxioms,
domain=None,
range=Optional[
Union[
Union[dict, DisjointClassExpressionsAxiom],
List[Union[dict, DisjointClassExpressionsAxiom]],
]
],
)

slots.domainRangeAxioms = Slot(
uri=OBOGRAPHS.domainRangeAxioms,
name="domainRangeAxioms",
Expand Down Expand Up @@ -1347,6 +1409,47 @@ class slots:
],
)

slots.disjointClassExpressionsAxiom__classIds = Slot(
uri=OBOGRAPHS.classIds,
name="disjointClassExpressionsAxiom__classIds",
curie=OBOGRAPHS.curie("classIds"),
model_uri=OBOGRAPHS.disjointClassExpressionsAxiom__classIds,
domain=None,
range=Optional[Union[str, List[str]]],
)

slots.disjointClassExpressionsAxiom__classExpressions = Slot(
uri=OBOGRAPHS.classExpressions,
name="disjointClassExpressionsAxiom__classExpressions",
curie=OBOGRAPHS.curie("classExpressions"),
model_uri=OBOGRAPHS.disjointClassExpressionsAxiom__classExpressions,
domain=None,
range=Optional[
Union[
Union[dict, ExistentialRestrictionExpression],
List[Union[dict, ExistentialRestrictionExpression]],
]
],
)

slots.disjointClassExpressionsAxiom__unionEquivalentTo = Slot(
uri=OBOGRAPHS.unionEquivalentTo,
name="disjointClassExpressionsAxiom__unionEquivalentTo",
curie=OBOGRAPHS.curie("unionEquivalentTo"),
model_uri=OBOGRAPHS.disjointClassExpressionsAxiom__unionEquivalentTo,
domain=None,
range=Optional[str],
)

slots.disjointClassExpressionsAxiom__unionEquivalentToExpression = Slot(
uri=OBOGRAPHS.unionEquivalentToExpression,
name="disjointClassExpressionsAxiom__unionEquivalentToExpression",
curie=OBOGRAPHS.curie("unionEquivalentToExpression"),
model_uri=OBOGRAPHS.disjointClassExpressionsAxiom__unionEquivalentToExpression,
domain=None,
range=Optional[Union[dict, ExistentialRestrictionExpression]],
)

slots.Meta_xrefs = Slot(
uri=OBOGRAPHS.xrefs,
name="Meta_xrefs",
Expand Down
36 changes: 36 additions & 0 deletions src/oaklib/datamodels/obograph.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ slots:
range: LogicalDefinitionAxiom
description: >-
A list of logical definition axioms that define the meaning of a class in terms of other classes.
disjointClassExpressionsAxioms:
multivalued: true
inlined_as_list: true
range: DisjointClassExpressionsAxiom
description: >-
A list of logical disjointness axioms that specify that a class or class expression
is disjoint from other classes or class expressions.
domainRangeAxioms:
multivalued: true
range: DomainRangeAxiom
Expand Down Expand Up @@ -583,6 +590,35 @@ classes:
see_also:
- https://github.com/geneontology/obographs/issues/89

DisjointClassExpressionsAxiom:
aliases:
- disjoint classes
description: >-
An axiom that defines a set of classes or class expressions as being mutually disjoint.
Formally, there exists no instance that instantiates more that one of the union of
classIds and classExpressions.
is_a: Axiom
attributes:
classIds:
description: >-
The set of named classes that are mutually disjoint.
multivalued: true
classExpressions:
description: >-
The set of class expressions that are mutually disjoint.
comments:
- currently restricted to existential restrictions (some values from)
range: ExistentialRestrictionExpression
multivalued: true
unionEquivalentTo:
description: >-
If present, this equates to an OWL DisjointUnion expression.
unionEquivalentToExpression:
range: ExistentialRestrictionExpression
description: >-
if present, this class expression is equivalent ot the (disjoint) union of
the classIds and classExpressions.
PropertyChainAxiom:
is_a: Axiom
description: >-
Expand Down
Loading

0 comments on commit 428d6e8

Please sign in to comment.