Skip to content

Commit cd7bf85

Browse files
committed
feat: add get_dl_expressivity() for computing ontology DL expressivity
Resolves #220. Computes the standard DL name (e.g. "ALC", "SHIQ(D)") for a loaded SyncOntology by delegating feature detection to OWLAPI's DLExpressivityChecker (already bundled for the Java reasoners) and assembling the canonical letter string in Python, since DLExpressivityChecker.getDescriptionLogicName() returns internal debug labels rather than standard DL notation on the bundled OWLAPI version. Exposed both as a top-level owlapy.get_dl_expressivity(ontology) function and as SyncOntology.get_dl_expressivity(), matching existing library conventions.
1 parent eb92772 commit cd7bf85

4 files changed

Lines changed: 132 additions & 0 deletions

File tree

owlapy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .converter import owl_expression_to_sparql, owl_expression_to_sparql_with_confusion_matrix
2+
from .expressivity import get_dl_expressivity
23
from .owl_reasoner_rdflib import RDFLibReasoner
34
from .parser import dl_to_owl_expression, manchester_to_owl_expression
45
from .render import owl_expression_to_dl, owl_expression_to_manchester
@@ -9,5 +10,6 @@
910
'owl_expression_to_dl', 'owl_expression_to_manchester',
1011
'dl_to_owl_expression', 'manchester_to_owl_expression',
1112
'owl_expression_to_sparql', 'owl_expression_to_sparql_with_confusion_matrix',
13+
'get_dl_expressivity',
1214
'RDFLibReasoner'
1315
]

owlapy/expressivity.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Compute the DL (Description Logic) expressivity of a loaded ontology.
2+
3+
Feature detection is delegated to OWLAPI's ``DLExpressivityChecker``
4+
(already bundled for the Java reasoners in ``owlapy/jar_dependencies/``),
5+
since its subsumption handling (e.g. full existential subsuming limited
6+
existential, qualified cardinality subsuming unqualified) is mature and
7+
battle-tested. The canonical DL name (e.g. ``"ALC"``, ``"SHIQ(D)"``) is
8+
assembled here rather than via OWLAPI's own ``getDescriptionLogicName()``,
9+
whose ``Construct.toString()`` returns internal debug labels
10+
(``"RRESTR"``, ``"CINT"``, ...) instead of standard DL letters.
11+
"""
12+
from owlapy.owl_ontology import SyncOntology
13+
14+
__all__ = ['get_dl_expressivity']
15+
16+
17+
def get_dl_expressivity(ontology: SyncOntology) -> str:
18+
"""Compute the DL expressivity name of an ontology, e.g. ``"ALCHN(D)"``.
19+
20+
Args:
21+
ontology: A :class:`~owlapy.owl_ontology.SyncOntology` instance.
22+
23+
Returns:
24+
The standard Description Logic name for the ontology's expressivity.
25+
26+
Raises:
27+
TypeError: If ``ontology`` is not backed by OWLAPI (only
28+
:class:`~owlapy.owl_ontology.SyncOntology` is supported, since
29+
it is the only ontology backend that exposes RBox axioms).
30+
"""
31+
if not isinstance(ontology, SyncOntology):
32+
raise TypeError(
33+
"get_dl_expressivity() requires a SyncOntology (OWLAPI-backed); "
34+
f"got {type(ontology).__name__}. Load the ontology with "
35+
"owlapy.owl_ontology.SyncOntology instead."
36+
)
37+
38+
# noinspection PyUnresolvedReferences
39+
from org.semanticweb.owlapi.util import DLExpressivityChecker
40+
41+
checker = DLExpressivityChecker([ontology.owlapi_ontology])
42+
constructs = {str(c.name()) for c in checker.getConstructs()}
43+
44+
has_complex_negation = "CONCEPT_COMPLEX_NEGATION" in constructs
45+
has_union = "CONCEPT_UNION" in constructs
46+
has_full_existential = "FULL_EXISTENTIAL" in constructs
47+
has_role_hierarchy = "ROLE_HIERARCHY" in constructs
48+
has_transitive = "ROLE_TRANSITIVE" in constructs
49+
has_complex_roles = "ROLE_REFLEXIVITY_CHAINS" in constructs or "ROLE_COMPLEX" in constructs
50+
has_nominals = "NOMINALS" in constructs
51+
has_inverse = "ROLE_INVERSE" in constructs
52+
has_functional = "F" in constructs
53+
has_unqualified_cardinality = "N" in constructs
54+
has_qualified_cardinality = "Q" in constructs
55+
has_datatypes = "D" in constructs
56+
57+
# S is shorthand for ALC + transitive roles and replaces the AL[C] prefix.
58+
if has_transitive:
59+
name = "S"
60+
else:
61+
name = "AL"
62+
if has_complex_negation:
63+
name += "C"
64+
else:
65+
# U and E are redundant once C is present (De Morgan's laws).
66+
if has_union:
67+
name += "U"
68+
if has_full_existential:
69+
name += "E"
70+
71+
if has_role_hierarchy:
72+
name += "H"
73+
if has_complex_roles:
74+
name += "R"
75+
if has_nominals:
76+
name += "O"
77+
if has_inverse:
78+
name += "I"
79+
80+
# Q (qualified cardinality) subsumes N (unqualified); show only one.
81+
if has_qualified_cardinality:
82+
name += "Q"
83+
elif has_unqualified_cardinality:
84+
name += "N"
85+
86+
# F (functional) is redundant once N/Q is present (functional == <=1).
87+
if has_functional and not (has_unqualified_cardinality or has_qualified_cardinality):
88+
name += "F"
89+
90+
if has_datatypes:
91+
name += "(D)"
92+
93+
return name

owlapy/owl_ontology.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,11 @@ def get_tbox_axioms(self, include_imports_closure: bool = True) -> Iterable[OWLA
13681368
def get_owlapi_ontology(self):
13691369
return self.owlapi_ontology
13701370

1371+
def get_dl_expressivity(self) -> str:
1372+
"""Compute the DL expressivity name of this ontology, e.g. "ALCHN(D)"."""
1373+
from owlapy.expressivity import get_dl_expressivity
1374+
return get_dl_expressivity(self)
1375+
13711376
def get_ontology_id(self) -> OWLOntologyID:
13721377
return self.mapper.map_(self.owlapi_ontology.getOntologyID())
13731378

tests/test_expressivity.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
3+
from owlapy import get_dl_expressivity
4+
from owlapy.expressivity import get_dl_expressivity as get_dl_expressivity_direct
5+
from owlapy.owl_ontology import SyncOntology
6+
7+
8+
class TestExpressivity(unittest.TestCase):
9+
10+
def test_father_ontology_expressivity(self):
11+
onto = SyncOntology("KGs/Family/father.owl")
12+
self.assertEqual(get_dl_expressivity(onto), "ALC")
13+
14+
def test_biopax_ontology_expressivity(self):
15+
onto = SyncOntology("KGs/Biopax/biopax.owl")
16+
self.assertEqual(get_dl_expressivity(onto), "ALCHN(D)")
17+
18+
def test_top_level_and_submodule_exports_agree(self):
19+
onto = SyncOntology("KGs/Family/father.owl")
20+
self.assertEqual(get_dl_expressivity(onto), get_dl_expressivity_direct(onto))
21+
22+
def test_sync_ontology_convenience_method(self):
23+
onto = SyncOntology("KGs/Family/father.owl")
24+
self.assertEqual(onto.get_dl_expressivity(), "ALC")
25+
26+
def test_rejects_non_syncontology(self):
27+
with self.assertRaises(TypeError):
28+
get_dl_expressivity("not an ontology")
29+
30+
31+
if __name__ == "__main__":
32+
unittest.main()

0 commit comments

Comments
 (0)