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

Added support for evaluating Manchester expression to owlready2 #296

Merged
merged 15 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pip install EMMOntoPy
- [semver]: Required for `ontoversion`-tool.
- [pydot]: Used for generating graphs.
Will be deprecated.
- [pyparsing](https://github.com/pyparsing/pyparsing): Used for parsing Manchester syntax

See [docs/docker-instructions.md](docs/docker-instructions.md) for how to build a docker image.

Expand Down
2 changes: 1 addition & 1 deletion demo/horizontal/step1_generate_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@

print("Generated metadata for the usercase ontology:")
print(f" {e.coll.count()} instances")
print(f" {len(list(e.coll.relations()))} relations")
print(f" {len(list(e.coll.get_relations()))} relations")
3 changes: 3 additions & 0 deletions docs/api_reference/ontopy/manchester.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# manchester

::: ontopy.manchester
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ pip install EMMOntoPy
- [semver]: Required for `ontoversion`-tool.
- [pydot]: Used for generating graphs.
Will be deprecated.
- [pyparsing](https://github.com/pyparsing/pyparsing): Used for parsing Manchester syntax

See [docker-instructions.md](docker-instructions.md) for how to build a docker image.

Expand Down
2 changes: 1 addition & 1 deletion emmopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
raise RuntimeError("emmopy requires Python 3.6 or later")

# Ensure emmopy is imported before owlready2...
if "owlready2" in sys.modules and "ontopy" not in sys.modules:
if "owlready2" in sys.modules and "emmopy" not in sys.modules:
raise RuntimeError("emmopy must be imported before owlready2")

# Import functions from emmopy
Expand Down
2 changes: 1 addition & 1 deletion ontopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
raise RuntimeError("ontopy requires Python 3.6 or later")

# Ensure ontopy is imported before owlready2...
if "owlready2" in sys.modules and "emmopy" not in sys.modules:
if "owlready2" in sys.modules and "ontopy" not in sys.modules:
raise RuntimeError("ontopy must be imported before owlready2")

# Monkey patch Owlready2 by injecting some methods
Expand Down
192 changes: 192 additions & 0 deletions ontopy/manchester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""Evaluate Manchester syntax

This module compiles restrictions and logical constructs in Manchester
syntax into Owlready2 classes. The main function in this module is
`manchester.evaluate()`, see its docstring for usage example.

Pyparsing is used under the hood for parsing.
"""
# pylint: disable=unused-import,wrong-import-order
import pyparsing as pp
import ontopy # noqa F401 -- ontopy must be imported before owlready2
import owlready2


GRAMMAR = None # Global cache


def manchester_expression():
"""Returns pyparsing grammar for a Manchester expression.

This function is mostly for internal use.

See also: https://www.w3.org/TR/owl2-manchester-syntax/
"""
# pylint: disable=global-statement,invalid-name,too-many-locals
global GRAMMAR
if GRAMMAR:
return GRAMMAR

# Subset of the Manchester grammar for expressions
# It is based on https://www.w3.org/TR/owl2-manchester-syntax/
# but allows logical constructs within restrictions (like Protege)
ident = pp.Word(pp.alphas + "_", pp.alphanums + "_", asKeyword=True)
uint = pp.Word(pp.nums)
alphas = pp.Word(pp.alphas)
string = pp.Word(pp.alphanums + ":")
quotedString = (
pp.QuotedString('"""', multiline=True) | pp.QuotedString('"')
)("string")
typedLiteral = pp.Combine(quotedString + "^^" + string("datatype"))
stringLanguageLiteral = pp.Combine(quotedString + "@" + alphas("language"))
stringLiteral = quotedString
numberLiteral = pp.pyparsing_common.number("number")
literal = (
typedLiteral | stringLanguageLiteral | stringLiteral | numberLiteral
)
logOp = pp.oneOf(["and", "or"], asKeyword=True)
expr = pp.Forward()
restriction = pp.Forward()
primary = pp.Keyword("not")[...] + (
restriction | ident("cls") | pp.nestedExpr("(", ")", expr)
)
objPropExpr = (
pp.Literal("inverse")
+ pp.Suppress("(")
+ ident("objProp")
+ pp.Suppress(")")
| pp.Literal("inverse") + ident("objProp")
| ident("objProp")
)
dataPropExpr = ident("dataProp")
restriction <<= (
objPropExpr + pp.Keyword("some") + expr
| objPropExpr + pp.Keyword("only") + expr
| objPropExpr + pp.Keyword("Self")
| objPropExpr + pp.Keyword("value") + ident("individual")
| objPropExpr + pp.Keyword("min") + uint + expr
| objPropExpr + pp.Keyword("max") + uint + expr
| objPropExpr + pp.Keyword("exactly") + uint + expr
| dataPropExpr + pp.Keyword("value") + literal
)
expr <<= primary + (logOp("op") + expr)[...]

GRAMMAR = expr
return expr


class ManchesterError(Exception):
"""Raised on invalid Manchester notation."""


# pylint: disable=too-many-statements
def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct:
"""Evaluate expression in Manchester syntax.

Args:
ontology: The ontology within which the expression will be evaluated.
expr: Manchester expression to be evaluated.

Returns:
An Owlready2 construct that corresponds to the expression.

Example:
>>> from ontopy.manchester import evaluate
>>> from ontopy import get_ontology
>>> emmo = get_ontology.load()

>>> restriction = evaluate(emmo, 'hasPart some Atom')
>>> cls = evaluate(emmo, 'Atom')
>>> expr = evaluate(emmo, 'Atom or Molecule')

Note:
Logical expressions (with `not`, `and` and `or`) are supported as
well as object property restrictions. For data properterties are
only value restrictions supported so far.
"""

# pylint: disable=invalid-name
def _parse_literal(r):
"""Compiles literal to Owlready2 type."""
if r.language:
v = owlready2.locstr(r.string, r.language)
elif r.number:
v = r.number
else:
v = r.string
return v

# pylint: disable=invalid-name,no-else-return,too-many-return-statements
# pylint: disable=too-many-branches
def _eval(r):
"""Recursively evaluate expression produced by pyparsing into an
Owlready2 construct."""

def fneg(x):
"""Negates the argument if `neg` is true."""
return owlready2.Not(x) if neg else x

if isinstance(r, str): # r is atomic, returns its owlready2 repr
return ontology[r]

neg = False # whether the expression starts with "not"
while r[0] == "not":
r.pop(0) # strip off the "not" and proceed
neg = not neg

if len(r) == 1: # r is either a atomic or a parenthesised
# subexpression that should be further evaluated
if isinstance(r[0], str):
return fneg(ontology[r[0]])
else:
return fneg(_eval(r[0]))
elif r.op: # r contains a logical operator: and/or
ops = {"and": owlready2.And, "or": owlready2.Or}
op = ops[r.op]
if len(r) == 3:
return op([fneg(_eval(r[0])), _eval(r[2])])
else:
arg1 = fneg(_eval(r[0]))
r.pop(0)
r.pop(0)
return op([arg1, _eval(r)])
elif r.objProp: # r is a restriction
if r[0] == "inverse":
r.pop(0)
prop = owlready2.Inverse(ontology[r[0]])
else:
prop = ontology[r[0]]
rtype = r[1]
if rtype == "Self":
return fneg(prop.has_self())
r.pop(0)
r.pop(0)
f = getattr(prop, rtype)
if rtype == "value":
return fneg(f(_eval(r)))
elif rtype in ("some", "only"):
return fneg(f(_eval(r)))
elif rtype in ("min", "max", "exactly"):
cardinality = r.pop()
return fneg(f(cardinality, _eval(r)))
else:
raise ManchesterError(f"invalid restriction type: {rtype}")
francescalb marked this conversation as resolved.
Show resolved Hide resolved
elif r.dataProp: # r is a data property restriction
prop = ontology[r[0]]
rtype = r[1]
r.pop(0)
r.pop(0)
f = getattr(prop, rtype)
if rtype == "value":
print("===", _parse_literal(r))
return f(_parse_literal(r))
else:
raise ManchesterError(
f"unimplemented data property restriction: "
f"{prop} {rtype} {r}"
)
else:
raise ManchesterError(f"invalid expression: {r}")

grammar = manchester_expression()
return _eval(grammar.parseString(expr, parseAll=True))
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Owlready2>=0.28,<0.36,!=0.32,!=0.34
packaging>=21.0<22
pydot>=1.4.1,<2
Pygments>=2.7.4,<3
pyparsing>=2.4.7
PyYAML>=5.4.1,<7
rdflib>=4.2.1,<7
semver>=2.8.1,<3
75 changes: 75 additions & 0 deletions tests/test_manchester.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from ontopy import get_ontology
from ontopy.manchester import evaluate
from owlready2 import And, Or, Not, Inverse, locstr


emmo = get_ontology().load()


def check(s, expected):
r = evaluate(emmo, s)
print(s, "-->", r)
assert repr(r) == repr(expected)


def test_manchester():
check("Item", emmo.Item)
check("not Item", Not(emmo.Item))
check("not not Item", emmo.Item)
check("not (not Item)", Not(Not(emmo.Item)))
check("hasPart some Atom", emmo.hasPart.some(emmo.Atom))
check("Atom and not Molecule", emmo.Atom & Not(emmo.Molecule))
check("Atom and (not Molecule)", emmo.Atom & Not(emmo.Molecule))
check("not Atom and Molecule", Not(emmo.Atom) & emmo.Molecule)
check("(not Atom) and Molecule", Not(emmo.Atom) & emmo.Molecule)
check("inverse hasPart some Atom", Inverse(emmo.hasPart).some(emmo.Atom))
check("inverse(hasPart) some Atom", Inverse(emmo.hasPart).some(emmo.Atom))
check("not hasPart some Atom", Not(emmo.hasPart.some(emmo.Atom)))
check("not (hasPart some Atom)", Not(emmo.hasPart.some(emmo.Atom)))
check("hasPart some (not Atom)", emmo.hasPart.some(Not(emmo.Atom)))
check("hasPart some not Atom", emmo.hasPart.some(Not(emmo.Atom)))
check("not hasPart some not Atom", Not(emmo.hasPart.some(Not(emmo.Atom))))
check(
"hasPart only (inverse hasPart some not Atom)",
emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom))),
)
check(
"hasPart only inverse hasPart some not Atom",
emmo.hasPart.only(Inverse(emmo.hasPart).some(Not(emmo.Atom))),
)
check(
"Atom and Molecule and Proton",
emmo.Atom & (emmo.Molecule & emmo.Proton),
)
check(
"Atom and (Molecule and Proton)",
emmo.Atom & (emmo.Molecule & emmo.Proton),
)
check(
"(Atom and Molecule) or Proton",
(emmo.Atom & emmo.Molecule) | emmo.Proton,
)
check(
"(Atom and Molecule) or Proton",
(emmo.Atom & emmo.Molecule) | emmo.Proton,
)
check(
"inverse(hasPart) value Universe",
Inverse(emmo.hasPart).value(emmo.Universe),
)
# literal data restriction
check('hasSymbolData value "hello"', emmo.hasSymbolData.value("hello"))
check("hasSymbolData value 42", emmo.hasSymbolData.value(42))
check("hasSymbolData value 3.14", emmo.hasSymbolData.value(3.14))
check(
'hasSymbolData value "abc"^^xsd:string',
emmo.hasSymbolData.value("abc"),
)
check(
'hasSymbolData value "hello"@en',
emmo.hasSymbolData.value(locstr("hello", "en")),
)


if __name__ == "__main__":
test_manchester()