From 6242e57c747cfa3be6a4922018e28997bba7ee10 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:21:16 -0700 Subject: [PATCH 1/9] upgrade --- poetry.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index 71dc9eaec..9366f624d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -146,11 +146,11 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "babel" -version = "2.9.1" +version = "2.10.1" description = "Internationalization utilities" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pytz = ">=2015.7" @@ -784,13 +784,14 @@ python-versions = ">=3.6" [[package]] name = "linkml" -version = "1.2.7" +version = "1.2.8" description = "Linked Open Data Modeling Language" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] +antlr4-python3-runtime = ">=4.9,<4.10" argparse = ">=1.4.0" click = ">=7.0" graphviz = ">=0.10.1" @@ -1059,11 +1060,11 @@ test = ["pytest (>=7.1)", "pytest-cov (>=3.0)", "codecov (>=2.1)"] [[package]] name = "notebook" -version = "6.4.10" +version = "6.4.11" description = "A web-based notebook environment for interactive computing" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] argon2-cffi = "*" @@ -1085,7 +1086,7 @@ traitlets = ">=4.2.1" [package.extras] docs = ["sphinx", "nbsphinx", "sphinxcontrib-github-alt", "sphinx-rtd-theme", "myst-parser"] json-logging = ["json-logging"] -test = ["pytest", "coverage", "requests", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] +test = ["pytest", "coverage", "requests", "testpath", "nbval", "selenium", "pytest-cov", "requests-unixsocket"] [[package]] name = "numpy" @@ -2273,8 +2274,8 @@ attrs = [ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] babel = [ - {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, - {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, + {file = "Babel-2.10.1-py3-none-any.whl", hash = "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2"}, + {file = "Babel-2.10.1.tar.gz", hash = "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"}, ] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, @@ -2714,8 +2715,8 @@ jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.1.0.tar.gz", hash = "sha256:d5f41bc1713795385f718d44dcba47e1e1473c6289f28a95aa6b2c0782ee372a"}, ] linkml = [ - {file = "linkml-1.2.7-py3-none-any.whl", hash = "sha256:177d82ebee9db3054ea8d8e44ccaf239e36ead31c516f5dfeed8a232a2b7e935"}, - {file = "linkml-1.2.7.tar.gz", hash = "sha256:ccd7c34db8ac960ac153bb7acdf509d79abef64d3bafe9c9d68fc828df5141e5"}, + {file = "linkml-1.2.8-py3-none-any.whl", hash = "sha256:9de4ebf98708bff9bebd98713fab55c30beaf1c5d1311737f31c29573ac79953"}, + {file = "linkml-1.2.8.tar.gz", hash = "sha256:3da432b706108d5e3ebbce0df06b7e45bfea5b7c97559fefd6bcf754ed184250"}, ] linkml-dataops = [ {file = "linkml_dataops-0.1.0-py3-none-any.whl", hash = "sha256:193cf7f659e5f07946d2c2761896910d5f7151d91282543b1363801f68307f4c"}, @@ -2877,15 +2878,14 @@ networkx = [ {file = "networkx-2.8.tar.gz", hash = "sha256:4a52cf66aed221955420e11b3e2e05ca44196b4829aab9576d4d439212b0a14f"}, ] notebook = [ - {file = "notebook-6.4.10-py3-none-any.whl", hash = "sha256:49cead814bff0945fcb2ee07579259418672ac175d3dc3d8102a4b0a656ed4df"}, - {file = "notebook-6.4.10.tar.gz", hash = "sha256:2408a76bc6289283a8eecfca67e298ec83c67db51a4c2e1b713dd180bb39e90e"}, + {file = "notebook-6.4.11-py3-none-any.whl", hash = "sha256:b4a6baf2eba21ce67a0ca11a793d1781b06b8078f34d06c710742e55f3eee505"}, + {file = "notebook-6.4.11.tar.gz", hash = "sha256:709b1856a564fe53054796c80e17a67262071c86bfbdfa6b96aaa346113c555a"}, ] numpy = [ {file = "numpy-1.22.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75"}, {file = "numpy-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab"}, {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e"}, {file = "numpy-1.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4"}, - {file = "numpy-1.22.3-cp310-cp310-win32.whl", hash = "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430"}, {file = "numpy-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4"}, {file = "numpy-1.22.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce"}, {file = "numpy-1.22.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe"}, From fc2fbecac92b03cbcc9f3d55d608282dd0553ef1 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:21:57 -0700 Subject: [PATCH 2/9] gap-filling, fixes #34 --- .../sqldb/sql_implementation.py | 21 +++++++++++--- .../ubergraph/ubergraph_implementation.py | 28 +++++++++++++++++-- tests/test_implementations/test_sqldb.py | 9 +++++- tests/test_implementations/test_ubergraph.py | 9 +++++- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/oaklib/implementations/sqldb/sql_implementation.py b/src/oaklib/implementations/sqldb/sql_implementation.py index d2e954122..b3f97b00f 100644 --- a/src/oaklib/implementations/sqldb/sql_implementation.py +++ b/src/oaklib/implementations/sqldb/sql_implementation.py @@ -2,7 +2,7 @@ from abc import ABC from collections import defaultdict from dataclasses import dataclass -from typing import List, Any, Iterable, Optional, Type, Dict, Union, Tuple +from typing import List, Any, Iterable, Optional, Type, Dict, Union, Tuple, Iterator from linkml_runtime import SchemaView from linkml_runtime.utils.introspection import package_schemaview @@ -10,7 +10,7 @@ from oaklib.implementations.sqldb.model import Statements, Edge, HasSynonymStatement, \ HasTextDefinitionStatement, ClassNode, IriNode, RdfsLabelStatement, DeprecatedNode, EntailedEdge, \ ObjectPropertyNode, AnnotationPropertyNode, NamedIndividualNode, HasMappingStatement -from oaklib.interfaces.basic_ontology_interface import RELATIONSHIP_MAP, PRED_CURIE, ALIAS_MAP +from oaklib.interfaces.basic_ontology_interface import RELATIONSHIP_MAP, PRED_CURIE, ALIAS_MAP, RELATIONSHIP from oaklib.interfaces.mapping_provider_interface import MappingProviderInterface from oaklib.interfaces.obograph_interface import OboGraphInterface from oaklib.interfaces.relation_graph_interface import RelationGraphInterface @@ -20,6 +20,7 @@ from oaklib.datamodels import obograph, ontology_metadata import oaklib.datamodels.validation_datamodel as vdm from oaklib.datamodels.vocabulary import SYNONYM_PREDICATES, omd_slots, LABEL_PREDICATE, IN_SUBSET +from oaklib.utilities.graph.networkx_bridge import transitive_reduction_by_predicate from sqlalchemy import select, text, exists from sqlalchemy.orm import sessionmaker, aliased from sqlalchemy import create_engine @@ -381,5 +382,17 @@ def _check_slot(self, slot_name: str, class_name: str = 'Class') -> Iterable[vdm ) yield result - - + def gap_fill_relationships(self, seed_curies: List[CURIE], predicates: List[PRED_CURIE] = None) -> Iterator[RELATIONSHIP]: + seed_curies = tuple(seed_curies) + q = self.session.query(EntailedEdge).filter(EntailedEdge.subject.in_(seed_curies)) + q = q.filter(EntailedEdge.object.in_(seed_curies)) + if predicates: + q = q.filter(EntailedEdge.predicate.in_(tuple(predicates))) + rels = [] + print(q) + for row in q: + print(f'ROW={row}') + if row.subject != row.object: + rels.append((row.subject, row.predicate, row.object)) + for rel in transitive_reduction_by_predicate(rels): + yield rel diff --git a/src/oaklib/implementations/ubergraph/ubergraph_implementation.py b/src/oaklib/implementations/ubergraph/ubergraph_implementation.py index 17d1e4d8b..3ec3c6165 100644 --- a/src/oaklib/implementations/ubergraph/ubergraph_implementation.py +++ b/src/oaklib/implementations/ubergraph/ubergraph_implementation.py @@ -2,17 +2,18 @@ from collections import defaultdict from dataclasses import dataclass from enum import Enum -from typing import Iterable, Tuple, List, Union, Optional +from typing import Iterable, Tuple, List, Union, Optional, Iterator from oaklib.datamodels import obograph -from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation +from oaklib.implementations.sparql.sparql_implementation import SparqlImplementation, _sparql_values from oaklib.implementations.sparql.sparql_query import SparqlQuery -from oaklib.interfaces.basic_ontology_interface import RELATIONSHIP_MAP +from oaklib.interfaces.basic_ontology_interface import RELATIONSHIP_MAP, RELATIONSHIP from oaklib.interfaces.mapping_provider_interface import MappingProviderInterface from oaklib.interfaces.obograph_interface import OboGraphInterface from oaklib.interfaces.relation_graph_interface import RelationGraphInterface from oaklib.interfaces.search_interface import SearchInterface from oaklib.types import CURIE, PRED_CURIE +from oaklib.utilities.graph.networkx_bridge import transitive_reduction_by_predicate from rdflib import RDFS, RDF, OWL @@ -219,6 +220,27 @@ def descendants(self, start_curies: Union[CURIE, List[CURIE]], predicates: List[ for row in bindings: yield self.uri_to_curie(row['s']['value']) + def gap_fill_relationships(self, seed_curies: List[CURIE], predicates: List[PRED_CURIE] = None) -> Iterator[RELATIONSHIP]: + # TODO: compare with https://api.triplydb.com/s/_mZ9q_-rg + query_uris = [self.curie_to_sparql(curie) for curie in seed_curies] + where = [f'?s ?p ?o', + _sparql_values('s', query_uris), + _sparql_values('o', query_uris)] + if predicates: + pred_uris = [self.curie_to_sparql(pred) for pred in predicates] + where.append(_sparql_values('p', pred_uris)) + query = SparqlQuery(select=['?s ?p ?o'], + where=where) + bindings = self._query(query.query_str()) + # TODO: remove redundancy + rels = [] + for row in bindings: + rels.append( (self.uri_to_curie(row['s']['value']), + self.uri_to_curie(row['p']['value']), + self.uri_to_curie(row['o']['value']))) + for rel in transitive_reduction_by_predicate(rels): + yield rel + diff --git a/tests/test_implementations/test_sqldb.py b/tests/test_implementations/test_sqldb.py index 691b0cfee..22390247b 100644 --- a/tests/test_implementations/test_sqldb.py +++ b/tests/test_implementations/test_sqldb.py @@ -11,7 +11,7 @@ from oaklib.utilities.obograph_utils import graph_as_dict from oaklib.datamodels.vocabulary import IS_A, PART_OF, LABEL_PREDICATE -from tests import OUTPUT_DIR, INPUT_DIR, CELLULAR_COMPONENT, VACUOLE, CYTOPLASM +from tests import OUTPUT_DIR, INPUT_DIR, CELLULAR_COMPONENT, VACUOLE, CYTOPLASM, NUCLEUS, PHOTOSYNTHETIC_MEMBRANE, HUMAN DB = INPUT_DIR / 'go-nucleus.db' TEST_OUT = OUTPUT_DIR / 'go-nucleus.saved.owl' @@ -177,3 +177,10 @@ def test_search(self): self.assertIn('GO:0005622', oi.basic_search('intracellular')) self.assertEqual(list(oi.basic_search('protoplasm')), ['GO:0005622']) self.assertEqual(list(oi.basic_search('protoplasm', SearchConfiguration(include_aliases=False))), []) + + def test_gap_fill(self): + oi = self.oi + rels = list(oi.gap_fill_relationships([NUCLEUS, PHOTOSYNTHETIC_MEMBRANE, CELLULAR_COMPONENT, HUMAN], + predicates=[IS_A, PART_OF])) + for rel in rels: + print(rel) \ No newline at end of file diff --git a/tests/test_implementations/test_ubergraph.py b/tests/test_implementations/test_ubergraph.py index 3c0a83f23..5fde2dce2 100644 --- a/tests/test_implementations/test_ubergraph.py +++ b/tests/test_implementations/test_ubergraph.py @@ -6,7 +6,8 @@ from oaklib.interfaces.search_interface import SearchConfiguration from oaklib.datamodels.vocabulary import IS_A, PART_OF -from tests import OUTPUT_DIR, INPUT_DIR, VACUOLE, DIGIT, CYTOPLASM, CELLULAR_COMPONENT, CELL, SHAPE +from tests import OUTPUT_DIR, INPUT_DIR, VACUOLE, DIGIT, CYTOPLASM, CELLULAR_COMPONENT, CELL, SHAPE, NEURON, \ + PHOTORECEPTOR_OUTER_SEGMENT TEST_ONT = INPUT_DIR / 'go-nucleus.obo' TEST_OUT = OUTPUT_DIR / 'go-nucleus.saved.owl' @@ -115,6 +116,12 @@ def test_ancestor_graph(self): else: assert CELL in node_ids + def test_gap_fill(self): + oi = self.oi + rels = list(oi.gap_fill_relationships([NEURON, PHOTORECEPTOR_OUTER_SEGMENT], predicates=[IS_A, PART_OF])) + for rel in rels: + print(rel) + def test_extract_triples(self): oi = self.oi for t in oi.extract_triples([SHAPE]): From 97a6b56d99044bb2f0009992d45a5c9bd5775446 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:22:34 -0700 Subject: [PATCH 3/9] transitive reduction --- src/oaklib/interfaces/subsetter_interface.py | 5 +-- src/oaklib/utilities/graph/networkx_bridge.py | 31 ++++++++++++++-- tests/__init__.py | 1 + .../test_obograph_datamodel.py | 1 + .../test_text_annotator_datamodel.py | 27 ++++++++++++++ tests/test_utilities/test_networkx_bridge.py | 36 ++++++++++++++++++- 6 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/test_datamodels/test_text_annotator_datamodel.py diff --git a/src/oaklib/interfaces/subsetter_interface.py b/src/oaklib/interfaces/subsetter_interface.py index cca274199..e45556369 100644 --- a/src/oaklib/interfaces/subsetter_interface.py +++ b/src/oaklib/interfaces/subsetter_interface.py @@ -1,6 +1,6 @@ from abc import ABC from dataclasses import dataclass -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Iterator from oaklib.interfaces.basic_ontology_interface import BasicOntologyInterface, RELATIONSHIP_MAP, RELATIONSHIP from oaklib.types import CURIE, LABEL, URI, PRED_CURIE @@ -43,10 +43,11 @@ def extract_subset_ontology(self, seed_curies: List[CURIE], strategy: SubsetStra """ raise NotImplementedError - def gap_fill_relationships(self, seed_curies: List[CURIE]) -> List[RELATIONSHIP]: + def gap_fill_relationships(self, seed_curies: List[CURIE], predicates: List[PRED_CURIE] = None) -> Iterator[RELATIONSHIP]: """ :param seed_curies: + :param predicates: :return: """ raise NotImplementedError diff --git a/src/oaklib/utilities/graph/networkx_bridge.py b/src/oaklib/utilities/graph/networkx_bridge.py index 7f189ff33..4350f2c0b 100644 --- a/src/oaklib/utilities/graph/networkx_bridge.py +++ b/src/oaklib/utilities/graph/networkx_bridge.py @@ -6,7 +6,7 @@ """ try: # Python <= 3.9 - from collections import Iterable + from collections import Iterable, defaultdict except ImportError: # Python > 3.9 from collections.abc import Iterable @@ -20,7 +20,7 @@ def relationships_to_multi_digraph(relationships: Iterable[RELATIONSHIP], revers Converts an OBOGraph to NetworkX :param relationships: - :param reverse: treat subject as the networkx parent + :param reverse: treat subject as the networkx parent (default true) :return: """ g = nx.MultiDiGraph() @@ -30,3 +30,30 @@ def relationships_to_multi_digraph(relationships: Iterable[RELATIONSHIP], revers else: g.add_edge(rel[0], rel[2], predicate=rel[1]) return g + +def transitive_reduction(relationships: Iterable[RELATIONSHIP]) -> Iterable[RELATIONSHIP]: + relationships = list(relationships) + tuples = [(o, s) for s, p, o in relationships] + g = nx.DiGraph(tuples) + reduced = nx.transitive_reduction(g) + for r in relationships: + s, p, o = r + if (o, s) in reduced.edges: + yield r + +def transitive_reduction_by_predicate(relationships: Iterable[RELATIONSHIP]) -> Iterable[RELATIONSHIP]: + relationships = list(relationships) + tuples_dict = defaultdict(list) + rels_dict = defaultdict(list) + for s, p, o in relationships: + if o != s: + tuples_dict[p].append((o, s)) + rels_dict[p].append((s, p, o)) + for p, tuples in tuples_dict.items(): + print(f'p={p} tuples={tuples}') + g = nx.DiGraph(tuples) + reduced = nx.transitive_reduction(g) + for r in rels_dict[p]: + s, _, o = r + if (o, s) in reduced.edges: + yield r \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 1b6abdee1..37bb74570 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -20,6 +20,7 @@ def output_path(fn: str) -> str: CELLULAR_COMPONENT = 'GO:0005575' CELL = 'CL:0000000' SHAPE = 'PATO:0000052' +PHOTORECEPTOR_OUTER_SEGMENT = 'GO:0001750' NUCLEUS = 'GO:0005634' NUCLEAR_ENVELOPE = 'GO:0005635' diff --git a/tests/test_datamodels/test_obograph_datamodel.py b/tests/test_datamodels/test_obograph_datamodel.py index 2a48bf2a2..535d6f652 100644 --- a/tests/test_datamodels/test_obograph_datamodel.py +++ b/tests/test_datamodels/test_obograph_datamodel.py @@ -28,6 +28,7 @@ def test_introspect(self): """ sv = package_schemaview(obograph.__name__) assert 'id' in sv.all_slots() + assert 'label' in sv.all_slots() ## TODO: consider changing assert 'Node' in sv.all_classes() assert 'Edge' in sv.all_classes() diff --git a/tests/test_datamodels/test_text_annotator_datamodel.py b/tests/test_datamodels/test_text_annotator_datamodel.py new file mode 100644 index 000000000..93056287e --- /dev/null +++ b/tests/test_datamodels/test_text_annotator_datamodel.py @@ -0,0 +1,27 @@ +import unittest + +from linkml_runtime.dumpers import yaml_dumper +from linkml_runtime.utils.introspection import package_schemaview +from oaklib.datamodels import text_annotator +from tests import output_path, NUCLEUS + + +class TestTextAnnotatorDatamodel(unittest.TestCase): + + def test_create(self): + """ + Tests the creation of an example instance of the OboGraph datamodel + """ + ann = text_annotator.TextAnnotation(subject_start=1, subject_end=7, match_string='nucleus', object_id=NUCLEUS) + yaml_dumper.dump(ann, output_path('example.text_annotation.yaml')) + + def test_introspect(self): + """ + Tests ability to introspect the schema and examine the schema elements + """ + sv = package_schemaview(text_annotator.__name__) + assert 'subject_start' in sv.all_slots() + assert 'TextAnnotation' in sv.all_classes() + + + diff --git a/tests/test_utilities/test_networkx_bridge.py b/tests/test_utilities/test_networkx_bridge.py index 3fad48f23..c9cfe9dc1 100644 --- a/tests/test_utilities/test_networkx_bridge.py +++ b/tests/test_utilities/test_networkx_bridge.py @@ -1,9 +1,11 @@ import logging import unittest +from oaklib.datamodels.vocabulary import IS_A, PART_OF from oaklib.implementations.pronto.pronto_implementation import ProntoImplementation from oaklib.resource import OntologyResource -from oaklib.utilities.graph.networkx_bridge import relationships_to_multi_digraph +from oaklib.utilities.graph.networkx_bridge import relationships_to_multi_digraph, transitive_reduction, \ + transitive_reduction_by_predicate import networkx as nx from tests import OUTPUT_DIR, INPUT_DIR @@ -36,6 +38,38 @@ def test_all_paths(self): # print(path) #assert len(paths) > 10 + def test_reduction(self): + rels = [('a', IS_A, 'b'), ('b', IS_A, 'c'), ('a', IS_A, 'c')] + reduced = list(transitive_reduction(rels)) + #for r in reduced: + # print(r) + self.assertEqual(len(reduced), 2) + self.assertCountEqual(reduced, + [('a', 'rdfs:subClassOf', 'b'), + ('b', 'rdfs:subClassOf', 'c')]) + rels = list(self.oi.all_relationships()) + reduced = list(transitive_reduction([rel for rel in rels if rel[1] == IS_A])) + for r in reduced: + logging.info(r) + reduced = list(transitive_reduction([rel for rel in rels if rel[1] == PART_OF])) + + def test_reduction_by_predicate(self): + rels = [('a', IS_A, 'b'), ('b', IS_A, 'c'), ('a', IS_A, 'c')] + reduced = list(transitive_reduction_by_predicate(rels)) + #for r in reduced: + # print(r) + self.assertEqual(len(reduced), 2) + self.assertCountEqual(reduced, + [('a', 'rdfs:subClassOf', 'b'), + ('b', 'rdfs:subClassOf', 'c')]) + rels = [('a', IS_A, 'b'), ('b', IS_A, 'c'), ('a', PART_OF, 'c')] + reduced = list(transitive_reduction_by_predicate(rels)) + self.assertEqual(len(reduced), 3) + rels = list(self.oi.all_relationships()) + reduced = list(transitive_reduction_by_predicate(rels)) + for r in reduced: + logging.info(r) + From 439069d27157d8ccc4a38bb7f7bd6abe6cabe583 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:33:24 -0700 Subject: [PATCH 4/9] CLI --- src/oaklib/cli.py | 20 +++++++++++++++++++ src/oaklib/interfaces/subsetter_interface.py | 11 +++++++++- src/oaklib/utilities/graph/networkx_bridge.py | 1 - tests/test_implementations/test_sqldb.py | 12 ++++++++--- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/oaklib/cli.py b/src/oaklib/cli.py index edb2fe93e..35c7c3b01 100644 --- a/src/oaklib/cli.py +++ b/src/oaklib/cli.py @@ -369,6 +369,26 @@ def ancestors(terms, predicates, output: str): else: raise NotImplementedError(f'Cannot execute this using {impl} of type {type(impl)}') +@main.command() +@click.argument("terms", nargs=-1) +@predicates_option +@output_option +def spanners(terms, predicates, output: str): + """ + List all spanning relationships + """ + impl = settings.impl + if isinstance(impl, SubsetterInterface): + actual_predicates = _process_predicates_arg(predicates) + curies = list(terms) + logging.info(f'Ancestor seed: {curies}') + rels = impl.gap_fill_relationships(curies, predicates=actual_predicates) + for rel in rels: + # TODO: turn to graph + print(rel) + else: + raise NotImplementedError(f'Cannot execute this using {impl} of type {type(impl)}') + @main.command() @click.argument("terms", nargs=-1) @predicates_option diff --git a/src/oaklib/interfaces/subsetter_interface.py b/src/oaklib/interfaces/subsetter_interface.py index e45556369..7283f5e27 100644 --- a/src/oaklib/interfaces/subsetter_interface.py +++ b/src/oaklib/interfaces/subsetter_interface.py @@ -45,9 +45,18 @@ def extract_subset_ontology(self, seed_curies: List[CURIE], strategy: SubsetStra def gap_fill_relationships(self, seed_curies: List[CURIE], predicates: List[PRED_CURIE] = None) -> Iterator[RELATIONSHIP]: """ + Given a term subset as a list of curies, find all non-redundant relationships connecting them + + This assumes relation-graph entailed edges, so currently only implemented for ubergraph and sqlite + + First the subset of all entailed edges conforming to the predicate profile connecting any pair of terms in the + subset is selected + + Then naive transitive reduction on a per predicate basis is performed. This may yield edges that are formally + redundant, but these are still assumed to be useful for the user :param seed_curies: - :param predicates: + :param predicates: if specified, only consider relationships using these predicates :return: """ raise NotImplementedError diff --git a/src/oaklib/utilities/graph/networkx_bridge.py b/src/oaklib/utilities/graph/networkx_bridge.py index 4350f2c0b..194111b5a 100644 --- a/src/oaklib/utilities/graph/networkx_bridge.py +++ b/src/oaklib/utilities/graph/networkx_bridge.py @@ -50,7 +50,6 @@ def transitive_reduction_by_predicate(relationships: Iterable[RELATIONSHIP]) -> tuples_dict[p].append((o, s)) rels_dict[p].append((s, p, o)) for p, tuples in tuples_dict.items(): - print(f'p={p} tuples={tuples}') g = nx.DiGraph(tuples) reduced = nx.transitive_reduction(g) for r in rels_dict[p]: diff --git a/tests/test_implementations/test_sqldb.py b/tests/test_implementations/test_sqldb.py index 22390247b..2fff4dc90 100644 --- a/tests/test_implementations/test_sqldb.py +++ b/tests/test_implementations/test_sqldb.py @@ -180,7 +180,13 @@ def test_search(self): def test_gap_fill(self): oi = self.oi - rels = list(oi.gap_fill_relationships([NUCLEUS, PHOTOSYNTHETIC_MEMBRANE, CELLULAR_COMPONENT, HUMAN], + rels = list(oi.gap_fill_relationships([NUCLEUS, VACUOLE, CELLULAR_COMPONENT, HUMAN], predicates=[IS_A, PART_OF])) - for rel in rels: - print(rel) \ No newline at end of file + #for rel in rels: + # print(rel) + self.assertEqual(len(rels), 4) + self.assertCountEqual(rels, + [('GO:0005773', 'rdfs:subClassOf', 'GO:0005575'), + ('GO:0005634', 'rdfs:subClassOf', 'GO:0005575'), + ('GO:0005773', 'BFO:0000050', 'GO:0005575'), + ('GO:0005634', 'BFO:0000050', 'GO:0005575')]) \ No newline at end of file From a5d788257ddb16dd79ec3b2d9dfabb49bc843c21 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:43:34 -0700 Subject: [PATCH 5/9] temp pausing ubergraph tests --- tests/test_implementations/test_ubergraph.py | 1 + tests/test_utilities/test_networkx_bridge.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_implementations/test_ubergraph.py b/tests/test_implementations/test_ubergraph.py index 5fde2dce2..e9fb8f83e 100644 --- a/tests/test_implementations/test_ubergraph.py +++ b/tests/test_implementations/test_ubergraph.py @@ -14,6 +14,7 @@ ICMBO = 'GO:0043231' +@unittest.skip('Server down') class TestUbergraphImplementation(unittest.TestCase): def setUp(self) -> None: diff --git a/tests/test_utilities/test_networkx_bridge.py b/tests/test_utilities/test_networkx_bridge.py index c9cfe9dc1..d42c8b2fe 100644 --- a/tests/test_utilities/test_networkx_bridge.py +++ b/tests/test_utilities/test_networkx_bridge.py @@ -51,7 +51,7 @@ def test_reduction(self): reduced = list(transitive_reduction([rel for rel in rels if rel[1] == IS_A])) for r in reduced: logging.info(r) - reduced = list(transitive_reduction([rel for rel in rels if rel[1] == PART_OF])) + #reduced = list(transitive_reduction([rel for rel in rels if rel[1] == PART_OF])) def test_reduction_by_predicate(self): rels = [('a', IS_A, 'b'), ('b', IS_A, 'c'), ('a', IS_A, 'c')] From c85e63545022b51c45a883da7849ed09ddf187b7 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 19:45:48 -0700 Subject: [PATCH 6/9] CLI --- src/oaklib/implementations/sqldb/sql_implementation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/oaklib/implementations/sqldb/sql_implementation.py b/src/oaklib/implementations/sqldb/sql_implementation.py index b3f97b00f..1863496b5 100644 --- a/src/oaklib/implementations/sqldb/sql_implementation.py +++ b/src/oaklib/implementations/sqldb/sql_implementation.py @@ -10,6 +10,7 @@ from oaklib.implementations.sqldb.model import Statements, Edge, HasSynonymStatement, \ HasTextDefinitionStatement, ClassNode, IriNode, RdfsLabelStatement, DeprecatedNode, EntailedEdge, \ ObjectPropertyNode, AnnotationPropertyNode, NamedIndividualNode, HasMappingStatement +from oaklib.interfaces import SubsetterInterface from oaklib.interfaces.basic_ontology_interface import RELATIONSHIP_MAP, PRED_CURIE, ALIAS_MAP, RELATIONSHIP from oaklib.interfaces.mapping_provider_interface import MappingProviderInterface from oaklib.interfaces.obograph_interface import OboGraphInterface @@ -39,7 +40,8 @@ def get_range_xsd_type(sv: SchemaView, rng: str) -> Optional[URIorCURIE]: @dataclass -class SqlImplementation(RelationGraphInterface, OboGraphInterface, ValidatorInterface, SearchInterface, MappingProviderInterface, ABC): +class SqlImplementation(RelationGraphInterface, OboGraphInterface, ValidatorInterface, SearchInterface, + SubsetterInterface, MappingProviderInterface, ABC): """ A :class:`OntologyInterface` implementation that wraps a SQL Relational Database @@ -389,9 +391,7 @@ def gap_fill_relationships(self, seed_curies: List[CURIE], predicates: List[PRED if predicates: q = q.filter(EntailedEdge.predicate.in_(tuple(predicates))) rels = [] - print(q) for row in q: - print(f'ROW={row}') if row.subject != row.object: rels.append((row.subject, row.predicate, row.object)) for rel in transitive_reduction_by_predicate(rels): From 5b8a7cf4f35d0b9d702fe2d725f07738d892c112 Mon Sep 17 00:00:00 2001 From: cmungall Date: Thu, 21 Apr 2022 20:57:26 -0700 Subject: [PATCH 7/9] removing prints --- tests/test_implementations/test_ubergraph.py | 2 -- tests/test_utilities/test_taxon_constraints.py | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/test_implementations/test_ubergraph.py b/tests/test_implementations/test_ubergraph.py index e9fb8f83e..85c82c5b3 100644 --- a/tests/test_implementations/test_ubergraph.py +++ b/tests/test_implementations/test_ubergraph.py @@ -32,8 +32,6 @@ def test_relationships(self): def test_entailed_relationships(self): ont = self.oi rels = list(ont.entailed_outgoing_relationships_by_curie(VACUOLE)) - for rel in rels: - print(rel) self.assertIn((IS_A, VACUOLE), rels) self.assertIn((IS_A, ICMBO), rels) self.assertIn((IS_A, CELLULAR_COMPONENT), rels) diff --git a/tests/test_utilities/test_taxon_constraints.py b/tests/test_utilities/test_taxon_constraints.py index bf7f95389..a2332168e 100644 --- a/tests/test_utilities/test_taxon_constraints.py +++ b/tests/test_utilities/test_taxon_constraints.py @@ -41,7 +41,7 @@ def test_never_in(self): never, only, _ = all_term_taxon_constraints(oi, NUCLEAR_ENVELOPE, predicates=[IS_A]) self.assertCountEqual([], never) st = get_term_with_taxon_constraints(oi, NUCLEAR_ENVELOPE, include_redundant=True) - print(yaml_dumper.dumps(st)) + #print(yaml_dumper.dumps(st)) never = [tc for tc in st.never_in if not tc.redundant] assert all(tc.redundant_with_only_in for tc in never) only = [tc for tc in st.only_in if not tc.redundant] @@ -70,7 +70,7 @@ def test_photosynthetic_membrane(self): oi = self.oi t = PHOTOSYNTHETIC_MEMBRANE st = get_term_with_taxon_constraints(oi, t, include_redundant=True) - print(yaml_dumper.dumps(st)) + #print(yaml_dumper.dumps(st)) self.assertCountEqual(st.never_in, []) self.assertEqual(5, len(st.only_in)) never = set([tc.taxon.id for tc in st.never_in]) @@ -88,7 +88,7 @@ def test_nuclear_envelope(self): oi = self.oi t = NUCLEAR_ENVELOPE st = get_term_with_taxon_constraints(oi, t, include_redundant=True) - print(yaml_dumper.dumps(st)) + #print(yaml_dumper.dumps(st)) self.assertEqual(2, len(st.never_in)) self.assertEqual(3, len(st.only_in)) never = set([tc.taxon.id for tc in st.never_in]) @@ -131,7 +131,7 @@ def make_tcs(term: str, only: List[TAXON_CURIE], never: List[TAXON_CURIE], [DICTYOSTELIUM], [DICTYOSTELIUM_DISCOIDEUM], [FUNGI_OR_DICTYOSTELIUM])) - print(yaml_dumper.dumps(st)) + #print(yaml_dumper.dumps(st)) self.assertFalse(st.unsatisfiable) self.assertFalse(st.never_in[0].redundant_with_only_in) self.assertTrue(st.only_in[0].redundant) @@ -229,7 +229,7 @@ def test_taxon_subclass(self): def test_unsatisfiable(self): fake_oi = self.fake_oi st = get_term_with_taxon_constraints(fake_oi, PHOTOSYNTHETIC_MEMBRANE) - print(yaml_dumper.dumps(st)) + #print(yaml_dumper.dumps(st)) def test_all(self): oi = self.oi @@ -238,9 +238,9 @@ def test_all(self): st = get_term_with_taxon_constraints(oi, t) logging.info(yaml_dumper.dumps(st)) desc = get_taxon_constraints_description(oi, st) - print(desc) + #print(desc) def test_parser(self): with open(GAIN_LOSS_FILE) as file: for st in parse_gain_loss_file(file): - print(yaml_dumper.dumps(st)) + logging.info(yaml_dumper.dumps(st)) From 52f7466188f31dbaf4ac61c94392feaafc3c0929 Mon Sep 17 00:00:00 2001 From: cmungall Date: Fri, 22 Apr 2022 08:02:12 -0700 Subject: [PATCH 8/9] restore ubergraph tests --- tests/test_implementations/test_ubergraph.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_implementations/test_ubergraph.py b/tests/test_implementations/test_ubergraph.py index 85c82c5b3..a5038a068 100644 --- a/tests/test_implementations/test_ubergraph.py +++ b/tests/test_implementations/test_ubergraph.py @@ -1,7 +1,6 @@ import logging import unittest -from linkml_runtime.dumpers import yaml_dumper from oaklib.implementations.ubergraph.ubergraph_implementation import UbergraphImplementation from oaklib.interfaces.search_interface import SearchConfiguration from oaklib.datamodels.vocabulary import IS_A, PART_OF @@ -14,7 +13,6 @@ ICMBO = 'GO:0043231' -@unittest.skip('Server down') class TestUbergraphImplementation(unittest.TestCase): def setUp(self) -> None: @@ -117,11 +115,16 @@ def test_ancestor_graph(self): def test_gap_fill(self): oi = self.oi - rels = list(oi.gap_fill_relationships([NEURON, PHOTORECEPTOR_OUTER_SEGMENT], predicates=[IS_A, PART_OF])) + rels = list(oi.gap_fill_relationships([NEURON, PHOTORECEPTOR_OUTER_SEGMENT, CELLULAR_COMPONENT], predicates=[IS_A, PART_OF])) for rel in rels: - print(rel) + logging.info(rel) + self.assertEqual(rels, + [('GO:0001750', 'BFO:0000050', 'CL:0000540'), + ('GO:0001750', 'BFO:0000050', 'GO:0005575'), + ('GO:0001750', 'rdfs:subClassOf', 'GO:0005575')]) + def test_extract_triples(self): oi = self.oi for t in oi.extract_triples([SHAPE]): - print(t) + logging.info(t) From 1b30ab8e8b2d6cb1974c05b3272506d4b39c6e66 Mon Sep 17 00:00:00 2001 From: cmungall Date: Fri, 22 Apr 2022 09:44:20 -0700 Subject: [PATCH 9/9] added --fill-gaps to viz command, restored ubergraph tests --- .gitignore | 9 ++ notebooks/Command-Line-Examples.ipynb | 81 ++++++++++++++++++ notebooks/output/ug-gap-fill1.png | Bin 0 -> 27857 bytes src/oaklib/cli.py | 39 ++++----- .../ubergraph/ubergraph_implementation.py | 17 +++- src/oaklib/interfaces/obograph_interface.py | 15 ++++ tests/test_cli.py | 26 +++++- 7 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 notebooks/output/ug-gap-fill1.png diff --git a/.gitignore b/.gitignore index 6b9c41986..96fa3b2de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,12 @@ __pycache__ docs/_build/ docs/datamodels/*/*.md tests/output/ + +dist/ +db/ + +notebooks/output/*json +notebooks/output/*tsv +notebooks/input/go.db + + diff --git a/notebooks/Command-Line-Examples.ipynb b/notebooks/Command-Line-Examples.ipynb index 24cab4c5a..cfaf4481f 100644 --- a/notebooks/Command-Line-Examples.ipynb +++ b/notebooks/Command-Line-Examples.ipynb @@ -105,6 +105,51 @@ "The default ubergraph endpoint is used, but you could also set up your own" ] }, + { + "cell_type": "markdown", + "id": "8dbaac3c", + "metadata": {}, + "source": [ + "### Graphy operations" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ca33e9c1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CL:0000000 ! cell\r\n", + "CL:0000540 ! neuron\r\n", + "CL:0002319 ! neural cell\r\n", + "GO:0001750 ! photoreceptor outer segment\r\n", + "GO:0005575 ! cellular_component\r\n", + "UBERON:0001062 ! anatomical entity\r\n", + "BFO:0000040 ! material entity\r\n", + "CARO:0000000 ! anatomical entity\r\n", + "CL:0000003 ! native cell\r\n", + "CL:0000211 ! electrically active cell\r\n", + "CL:0000255 ! eukaryotic cell\r\n", + "CL:0000393 ! electrically responsive cell\r\n", + "CL:0000404 ! electrically signaling cell\r\n", + "CL:0000548 ! animal cell\r\n", + "CL:0002371 ! somatic cell\r\n", + "BFO:0000002 ! continuant\r\n", + "BFO:0000004 ! independent continuant\r\n", + "CARO:0030000 ! biological entity\r\n", + "GO:0110165 ! cellular anatomical entity\r\n" + ] + } + ], + "source": [ + "# all is-a (i.e. subClassOf between named classes) ancestors of a seed set\n", + "!runoak -i ubergraph: ancestors GO:0001750 CL:0000540 -p i" + ] + }, { "cell_type": "code", "execution_count": 40, @@ -121,6 +166,7 @@ } ], "source": [ + "# visualize starting from a seed set of terms\n", "!runoak -i ubergraph: viz --no-view GO:0001750 CL:0000540 -p i,BFO:0000050 -o output/ug-test1.png -O png" ] }, @@ -132,6 +178,41 @@ "![img](output/ug-test1.png)" ] }, + { + "cell_type": "markdown", + "id": "70dea17b", + "metadata": {}, + "source": [ + "### Gap Filling" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1a9d16e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(node:2377) [DEP0128] DeprecationWarning: Invalid 'main' field in '/Users/cjm/repos/obographviz/node_modules/node-getopt/package.json' of './lib'. Please either fix that or report it to the module author\r\n", + "(Use `node --trace-deprecation ...` to show where the warning was created)\r\n" + ] + } + ], + "source": [ + "!runoak -i ubergraph: viz --gap-fill GO:0001750 CL:0000540 CL:0000000 NCBITaxon:9606 NCBITaxon:1 -p i,p -o output/ug-gap-fill1.png -O png" + ] + }, + { + "cell_type": "markdown", + "id": "695bb837", + "metadata": {}, + "source": [ + "![img](output/ug-gap-fill1.png)" + ] + }, { "cell_type": "markdown", "id": "24d6a47e", diff --git a/notebooks/output/ug-gap-fill1.png b/notebooks/output/ug-gap-fill1.png new file mode 100644 index 0000000000000000000000000000000000000000..d43250e77e7dcf5abecacd56acd00314f1b2e1c1 GIT binary patch literal 27857 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 obograph.Graph: + relationships = list(relationships) + edges = [obograph.Edge(sub=s, pred=p, obj=o) for s, p, o in relationships] + node_ids = set() + for rel in relationships: + node_ids.update(list(rel)) + nodes = {} + for s, p, o in self._from_subjects_chunked(list(node_ids), [RDFS.label], object_is_literal=True): + nodes[s] = obograph.Node(id=s, label=o) + logging.info(f'NUM EDGES: {len(edges)}') + return obograph.Graph(id='query', + nodes=list(nodes.values()), edges=edges) + def ancestors(self, start_curies: Union[CURIE, List[CURIE]], predicates: List[PRED_CURIE] = None) -> Iterable[CURIE]: # TODO: DRY query_uris = [self.curie_to_sparql(curie) for curie in start_curies] diff --git a/src/oaklib/interfaces/obograph_interface.py b/src/oaklib/interfaces/obograph_interface.py index 3ccf39e37..2422f0c9d 100644 --- a/src/oaklib/interfaces/obograph_interface.py +++ b/src/oaklib/interfaces/obograph_interface.py @@ -190,6 +190,21 @@ def subgraph(self, start_curies: Union[CURIE, List[CURIE]], predicates: List[PRE g = self._merge_graphs([up_graph, down_graph]) return g + def relationships_to_graph(self, relationships: Iterable[RELATIONSHIP]) -> Graph: + """ + Generates an OboGraph from a list of relationships + + :param relationships: + :return: + """ + relationships = list(relationships) + node_ids = set() + for rel in relationships: + node_ids.update(list(rel)) + edges = [Edge(sub=s, pred=p, obj=o) for s, p, o in relationships] + nodes = [self.node(id) for id in node_ids] + return Graph(id='query', + nodes=list(nodes), edges=edges) def as_obograph(self) -> Graph: """ diff --git a/tests/test_cli.py b/tests/test_cli.py index 0676eae93..99e89499f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,15 @@ import logging import unittest +from linkml_runtime.loaders import yaml_loader, json_loader from oaklib.cli import search, main +from oaklib.datamodels import obograph +from oaklib.datamodels.vocabulary import IN_TAXON +from oaklib.implementations.pronto.pronto_implementation import ProntoImplementation +from oaklib.resource import OntologyResource -from tests import OUTPUT_DIR, INPUT_DIR, NUCLEUS, NUCLEAR_ENVELOPE, ATOM, INTERNEURON, BACTERIA, EUKARYOTA +from tests import OUTPUT_DIR, INPUT_DIR, NUCLEUS, NUCLEAR_ENVELOPE, ATOM, INTERNEURON, BACTERIA, EUKARYOTA, VACUOLE, \ + CELLULAR_COMPONENT, HUMAN, MAMMALIA from click.testing import CliRunner TEST_ONT = INPUT_DIR / 'go-nucleus.obo' @@ -46,6 +52,24 @@ def test_obograph_local(self): #assert 'GO:0016020 ! membrane' not in out assert 'GO:0043226 ! organelle' not in out + def test_gap_fill(self): + result = self.runner.invoke(main, ['-i', str(TEST_DB), 'viz', '--gap-fill', + '-p', f'i,p,{IN_TAXON}', + NUCLEUS, VACUOLE, CELLULAR_COMPONENT, + '-O', 'json', '-o', str(TEST_OUT)]) + out = result.stdout + err = result.stderr + self.assertEqual(0, result.exit_code) + with open(TEST_OUT) as file: + contents = "\n".join(file.readlines()) + self.assertIn(NUCLEUS, contents) + self.assertIn(VACUOLE, contents) + self.assertIn(CELLULAR_COMPONENT, contents) + # TODO: parse json to check it conforms + #g = json_loader.loads(contents, target_class=obograph.Graph) + + + ## MAPPINGS def test_mappings_local(self):