Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flb/get descendants #405

Merged
merged 10 commits into from
Apr 28, 2022
69 changes: 68 additions & 1 deletion ontopy/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
If desirable some of this may be moved back into owlready2.
"""
# pylint: disable=too-many-lines,fixme,arguments-differ,protected-access
from typing import TYPE_CHECKING, Union
import os
import itertools
import inspect
Expand All @@ -17,7 +18,6 @@
import tempfile
import types
import pathlib
from typing import Union
from collections import defaultdict
from collections.abc import Iterable

Expand Down Expand Up @@ -47,6 +47,9 @@

from ontopy.ontograph import OntoGraph # FIXME: deprecate...

if TYPE_CHECKING:
from typing import List


# Default annotations to look up
DEFAULT_LABEL_ANNOTATIONS = [
Expand Down Expand Up @@ -1270,6 +1273,60 @@ def addancestors(entity, counter, subject):
return ancestors.difference(classes)
return ancestors

def get_descendants(
self,
classes: "Union[List, ThingClass]",
common: bool = False,
generations: int = -1,
all_descendants: bool = False,
francescalb marked this conversation as resolved.
Show resolved Hide resolved
):
francescalb marked this conversation as resolved.
Show resolved Hide resolved
"""Return descendants/subclasses of all classes in `classes`.
Arguments:
- classes: to be provided as list.
- common: only return common descendants.
francescalb marked this conversation as resolved.
Show resolved Hide resolved
- generations: Include this number of descendant levels.
- all_descendants: Include all descendants.
Returns:
A list of descendants including required generation.
francescalb marked this conversation as resolved.
Show resolved Hide resolved
If 'common'=True, the common descendants are returned
within the specified number of generations.
If 'generations' is not given and 'common' is True all
descendants will be checked.
'generations' defaults to 1 if 'common' not True.
"""
if (common) & (generations == -1):
all_descendants = True
elif generations == -1:
generations = 1
francescalb marked this conversation as resolved.
Show resolved Hide resolved

if not isinstance(classes, list):
francescalb marked this conversation as resolved.
Show resolved Hide resolved
classes = [classes]

descendants = {name: [] for name in classes}

def _children_recursively(num, newentity, parent, descendants):
"""Helper function to get all children up to generation."""
for child in self.get_children_of(newentity):
descendants[parent].append(child)
if num < generations:
_children_recursively(num + 1, child, parent, descendants)

if generations == 0:
return set()

if all_descendants is True:
for entity in classes:
descendants[entity] = entity.descendants()

francescalb marked this conversation as resolved.
Show resolved Hide resolved
else:
for entity in classes:
_children_recursively(1, entity, entity, descendants)

results = [val for _, val in descendants.items()]
francescalb marked this conversation as resolved.
Show resolved Hide resolved
if common is True:
return set.intersection(*map(set, results))
return set(flatten(results))

def get_wu_palmer_measure(self, cls1, cls2):
"""Return Wu-Palmer measure for semantic similarity.

Expand Down Expand Up @@ -1343,3 +1400,13 @@ def __hash__(self):
def __eq__(self, other):
"""For now blank nodes always compare true against each other."""
return isinstance(other, BlankNode)


def flatten(items):
"""Yield items from any nested iterable."""
for item in items:
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
for sub_item in flatten(item):
yield sub_item
else:
yield item
71 changes: 71 additions & 0 deletions tests/test_generation_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from typing import TYPE_CHECKING
import pytest

if TYPE_CHECKING:
from ontopy.ontology import Ontology


def test_descendants(emmo: "Ontology", repo_dir: "Path") -> None:
from ontopy import get_ontology
from ontopy.utils import LabelDefinitionError

ontopath = repo_dir / "tests" / "testonto" / "testontology.ttl"

onto = get_ontology(ontopath).load()

# Test that default gives one generation
assert onto.get_descendants(onto.Tree) == {
onto.EvergreenTree,
onto.DesiduousTree,
}
assert onto.get_descendants(onto.Tree, generations=1) == {
onto.EvergreenTree,
onto.DesiduousTree,
}
# Test that asking for 0 generations returns empty set
assert onto.get_descendants(onto.Tree, generations=0) == set()
# Check that more than one generation works
assert onto.get_descendants(onto.Tree, generations=2) == {
onto.EvergreenTree,
onto.DesiduousTree,
onto.Avocado,
onto.Spruce,
}
# Check that no error is generated if one of the subclasses do not have enough children for all given generations
assert onto.get_descendants(onto.Tree, generations=3) == {
onto.EvergreenTree,
onto.DesiduousTree,
onto.Avocado,
onto.Spruce,
onto.EngelmannSpruce,
onto.NorwaySpruce,
}
assert onto.get_descendants(onto.Tree, generations=4) == {
onto.EvergreenTree,
onto.DesiduousTree,
onto.Avocado,
onto.Spruce,
onto.EngelmannSpruce,
onto.NorwaySpruce,
}
# Check that descendants of a list is returned correctly
assert onto.get_descendants([onto.Tree, onto.NaturalDye]) == {
onto.EvergreenTree,
onto.DesiduousTree,
onto.Avocado,
onto.ShingledHedgehogMushroom,
}
# Check that common descendants within the number of generations are found
# With all descentants if number of generations not given
assert onto.get_descendants(
[onto.Tree, onto.NaturalDye], common=True, generations=2
) == {onto.Avocado}
assert (
onto.get_descendants(
[onto.Tree, onto.NaturalDye], common=True, generations=1
)
== set()
)
assert onto.get_descendants([onto.Tree, onto.NaturalDye], common=True) == {
onto.Avocado
}
56 changes: 56 additions & 0 deletions tests/testonto/testontology.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@prefix : <http://emmo.info/testontology#> .
@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#> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
@base <http://emmo.info/testontology> .

<http://emmo.info/testontology> rdf:type owl:Ontology ;
owl:versionIRI <http://emmo.info/testonto/0.1.0/testontology> .


# Annotations
skos:prefLabel rdf:type owl:AnnotationProperty .
skos:altLabel rdf:type owl:AnnotationProperty .


:Tree rdf:type owl:Class ;
rdfs:subClassOf owl:Thing ;
skos:prefLabel "Tree"@en .

:NaturalDye rdf:type owl:Class ;
rdfs:subClassOf owl:Thing ;
skos:prefLabel "NaturalDye"@en .



:EvergreenTree rdf:type owl:Class ;
rdfs:subClassOf :Tree ;
skos:prefLabel "EvergreenTree"@en .

:DesiduousTree rdf:type owl:Class ;
rdfs:subClassOf :Tree ;
skos:prefLabel "DesiduousTree"@en .

:Spruce rdf:type owl:Class ;
rdfs:subClassOf :EvergreenTree ;
skos:prefLabel "Spruce"@en .

:Avocado rdf:type owl:Class ;
rdfs:subClassOf :EvergreenTree ;
rdfs:subClassOf :NaturalDye ;
skos:prefLabel "Avocado"@en .

:NorwaySpruce rdf:type owl:Class ;
rdfs:subClassOf :Spruce ;
skos:prefLabel "NorwaySpruce"@en .

:EngelmannSpruce rdf:type owl:Class ;
rdfs:subClassOf :Spruce ;
skos:prefLabel "EngelmannSpruce"@en .

:ShingledHedgehogMushroom rdf:type owl:Class ;
rdfs:subClassOf :NaturalDye ;
skos:prefLabel "ShingledHedgehogMushroom"@en .