Skip to content

Commit

Permalink
Mapping function plugin system (#152)
Browse files Browse the repository at this point in the history
# Description:
Closes #128 

See #128 for design and reasoning behind this PR. Needed for using
mapping functions in OTEAPI pipelines.

## Type of change:
- [ ] Bug fix.
- [x] New feature.
- [ ] Documentation update.
- [ ] Testing.
  • Loading branch information
jesper-friis authored Jan 25, 2024
2 parents 11fa3dc + 692a5a3 commit 103d67d
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 31 deletions.
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ def expected_function_triplestore(
@prefix ex: <http://example.com/onto#> .
@prefix fno: <https://w3id.org/function/ontology#> .
@prefix map: <http://emmo.info/domain-mappings#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:sum__{fid} a fno:Function ;
rdfs:label "sum_"@en ;
oteio:hasPythonFunctionName "sum_" ;
oteio:hasPythonModuleName "conftest" ;
dcterms:description "Returns the sum of `first_param` and `second_param`."@en ;
fno:expects ( ex:sum__{fid}_parameter1_first_param ex:sum__{fid}_parameter2_second_param ) ;
fno:returns ( ex:sum__{fid}_output1 ) .
Expand Down
29 changes: 29 additions & 0 deletions tests/mappings/test_cost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Test cost."""
import pytest

pytest.importorskip("rdflib")

# pylint: disable=wrong-import-position
from tripper import Triplestore

ts = Triplestore(backend="rdflib")

# Define some prefixed namespaces
CHEM = ts.bind("chem", "http://onto-ns.com/onto/chemistry#")
MOL = ts.bind("mol", "http://onto-ns.com/meta/0.1/Molecule#")
SUB = ts.bind("sub", "http://onto-ns.com/meta/0.1/Substance#")


def formula_cost(ts, input_iris, output_iri):
"""Returns a cost."""
# pylint: disable=unused-argument
return 2.72


# Add mappings from data models to ontology
ts.map(MOL.name, CHEM.Identifier, cost=3.14)
ts.map(SUB.formula, CHEM.Formula, cost=formula_cost)

# pylint: disable=protected-access
assert ts._get_cost(CHEM.Identifier) == 3.14
assert ts._get_cost(CHEM.Formula) == 2.72
9 changes: 9 additions & 0 deletions tests/test_add_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ def func(a, b):
@prefix ex: <http://example.com/ex#> .
@prefix fno: <https://w3id.org/function/ontology#> .
@prefix map: <http://emmo.info/domain-mappings#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<:func_{f_id}> a fno:Function ;
rdfs:label "func"@en ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
dcterms:description "Returns the sum of `a` and `b`."@en ;
fno:expects ( <:func_{f_id}_parameter1_a> <:func_{f_id}_parameter2_b> ) ;
fno:returns ( <:func_{f_id}_output1> ) .
Expand Down Expand Up @@ -67,13 +70,16 @@ def func(a, b):
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix emmo: <http://emmo.info/emmo#> .
@prefix ex: <http://example.com/ex#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<:func_{f_id}> a emmo:EMMO_4299e344_a321_4ef2_a744_bacfcce80afc ;
rdfs:label "func"@en ;
emmo:EMMO_36e69413_8c59_4799_946c_10b05d266e22 ex:arg1,
ex:arg2 ;
emmo:EMMO_c4bace1d_4db0_4cd3_87e9_18122bae2840 ex:sum ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
dcterms:description "Returns the sum of `a` and `b`."@en .
ex:arg1 a emmo:EMMO_194e367c_9783_4bf5_96d0_9ad597d48d9a ;
Expand All @@ -100,13 +106,16 @@ def func(a, b):
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix emmo: <http://emmo.info/emmo#> .
@prefix ex: <http://example.com/ex#> .
@prefix oteio: <http://emmo.info/oteio#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<:func_{f_id}> a emmo:EMMO_4299e344_a321_4ef2_a744_bacfcce80afc ;
rdfs:label "func"@en ;
emmo:EMMO_36e69413_8c59_4799_946c_10b05d266e22 ex:arg1,
ex:arg2 ;
emmo:EMMO_c4bace1d_4db0_4cd3_87e9_18122bae2840 ex:sum ;
oteio:hasPythonFunctionName "func" ;
oteio:hasPythonModuleName "test_add_function" ;
dcterms:description "Returns the sum of `a` and `b`."@en .
ex:arg1 a emmo:EMMO_194e367c_9783_4bf5_96d0_9ad597d48d9a ;
Expand Down
53 changes: 53 additions & 0 deletions tests/test_eval_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Test Triplestore.eval_function()"""
import pytest

from tripper import Triplestore

pytest.importorskip("rdflib")


def func(a, b):
"""Returns the sum of `a` and `b`."""
# pylint: disable=invalid-name
return a + b


ts = Triplestore(backend="rdflib")
EX = ts.bind("ex", "http://example.com/ex#")

# Test to add function in current scope
iri = ts.add_function(
func,
expects=[EX.arg1, EX.arg2],
returns=EX.sum,
standard="emmo",
)
assert ts.eval_function(func_iri=iri, args=(2, 3)) == 5


# Test to add a function from the standard library. The hashlib module
# is not expected to be imported in the current scope
iri2 = ts.add_function(
EX.shape256,
expects=[EX.Bytes],
returns=EX.ShakeVar,
func_name="shake_256",
module_name="hashlib",
)
shakevar = ts.eval_function(iri2, (b"a",))
assert shakevar.hexdigest(4) == "867e2cb0"


# Test to add a function from a pypi package.
iri3 = ts.add_function(
EX.UFloat,
expects=[EX.Uncertainty],
returns=EX.UUID,
func_name="ufloat",
module_name="uncertainties",
# package_name="uncertainties",
pypi_package_name="uncertainties==3.1.7",
)
val = ts.eval_function(iri3, args=(1, 0.1))
assert val.n == 1
assert val.s == 0.1
4 changes: 4 additions & 0 deletions tripper/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class NoSuchIRIError(NamespaceError):
"""Namespace has no such IRI."""


class CannotGetFunctionError(TriplestoreError):
"""Not able to get function documented in the triplestore."""


# === Warnings ===
class UnusedArgumentWarning(Warning):
"""Argument is unused."""
35 changes: 24 additions & 11 deletions tripper/mappings/mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pint import Quantity # remove

from tripper import DM, EMMO, FNO, MAP, RDF, RDFS
from tripper.errors import CannotGetFunctionError
from tripper.triplestore import hasAccessFunction, hasDataValue
from tripper.utils import parse_literal

Expand Down Expand Up @@ -889,17 +890,29 @@ def mapping_routes(
soAFun = dict(triplestore.subject_objects(hasAccessFunction))
soDVal = dict(triplestore.subject_objects(hasDataValue))

def getfunc(func_iri, default=None):
"""Returns callable function corresponding to `func_iri`.
Raises CannotGetFunctionError if func_iri cannot be found."""
if func_iri is None:
return None
if func_iri in function_repo and function_repo[func_iri]:
return function_repo[func_iri]
try:
return (
triplestore._get_function( # pylint: disable=protected-access
func_iri
)
)
except CannotGetFunctionError:
return default

def getcost(target, stepname):
"""Returns the cost assigned to IRI `target` for a mapping step
of type `stepname`."""
cost = soCost.get(target, default_costs[stepname])
if cost is None:
return None
return (
function_repo[cost]
if cost in function_repo
else float(parse_literal(cost))
)
if cost is None or callable(cost) or isinstance(cost, float):
return cost
return getfunc(cost, float(parse_literal(cost)))

def walk(target, visited, step):
"""Walk backward in rdf graph from `node` to sources."""
Expand All @@ -914,7 +927,7 @@ def addnode(node, steptype, stepname):
step.cost = getcost(target, stepname)
if node in soAFun:
value = value_class(
value=triplestore.function_repo[soAFun[node]],
value=getfunc(soAFun[node]),
unit=soUnit.get(node),
iri=node,
property_iri=soInst.get(node),
Expand Down Expand Up @@ -962,10 +975,10 @@ def addnode(node, steptype, stepname):
addnode(node, StepType.INV_SUBCLASSOF, "subClassOf")

for fmap in function_mappers:
for func, input_iris in fmap(triplestore)[target]:
for func_iri, input_iris in fmap(triplestore)[target]:
step.steptype = StepType.FUNCTION
step.cost = getcost(func, "function")
step.function = function_repo.get(func)
step.cost = getcost(func_iri, "function")
step.function = getfunc(func_iri)
step.join_mode = True
for input_iri in input_iris:
step0 = mappingstep_class(
Expand Down
1 change: 1 addition & 0 deletions tripper/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,4 @@ def __eq__(self, other):
EMMO = Namespace("http://emmo.info/emmo#")
MAP = Namespace("http://emmo.info/domain-mappings#")
DM = Namespace("http://emmo.info/datamodel#")
OTEIO = Namespace("http://emmo.info/oteio#")
Loading

0 comments on commit 103d67d

Please sign in to comment.