From 51afe026b667dcfde740f7d2ca34c8c725a16d5a Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 12:57:37 +0000 Subject: [PATCH 01/29] write workflow parser --- pyiron_ontology/parser.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index c040565..be7e6bc 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -52,7 +52,6 @@ def get_triples( hasSourceFunction=None, hasUnits=None, inheritsPropertiesFrom=None, - update_query=True, ): if hasSourceFunction is None: hasSourceFunction = EX.hasSourceFunction @@ -82,8 +81,6 @@ def get_triples( graph.add((label, inheritsPropertiesFrom, EX[d["connection"]])) for t in _get_triples_from_restrictions(d, EX): graph.add(_parse_triple(t, EX, label=label, data=data)) - if update_query: - inherit_properties(graph, EX) return graph @@ -128,7 +125,7 @@ def _parse_triple(triples, EX, label=None, data=None): return subj, pred, obj -def inherit_properties(graph, NS, n=None): +def _inherit_properties(graph, NS, n=None): update_query = ( f"PREFIX ns: <{NS}>", f"PREFIX rdfs: <{RDFS}>", @@ -165,3 +162,36 @@ def validate_values(graph): (instance, on_property, some_values_from) ) return missing_triples + + +def parse_workflow( + workflow, + EX, + graph=None, + inherit_properties=True, + hasNode=None, + hasSourceFunction=None, + hasUnits=None, + inheritsPropertiesFrom=None, +): + if hasNode is None: + hasNode = EX.hasNode + if hasSourceFunction is None: + hasSourceFunction = EX.hasSourceFunction + if hasUnits is None: + hasUnits = EX.hasUnits + if inheritsPropertiesFrom is None: + inheritsPropertiesFrom = EX.inheritsPropertiesFrom + if graph is None: + graph = Graph() + workflow_label = EX[workflow.label] + graph.add((workflow_label, RDFS.label, Literal(workflow.label))) + for key, value in workflow.children.items(): + data = get_inputs_and_outputs(value) + graph.add((workflow_label, hasNode, EX[data["label"]])) + graph += get_triples( + data, EX, hasSourceFunction, hasUnits, inheritsPropertiesFrom + ) + if inherit_properties: + _inherit_properties(graph, EX) + return graph From 77b7de06ab9349eeed8c09b7559708bd8c45b1ca Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 13:14:48 +0000 Subject: [PATCH 02/29] append workflow name --- pyiron_ontology/parser.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 8b40dd1..e775c82 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -49,10 +49,13 @@ def get_inputs_and_outputs(node): def get_triples( data, NS, + workflow_namespace=None, hasSourceFunction=None, hasUnits=None, inheritsPropertiesFrom=None, ): + if workflow_namespace is None: + workflow_namespace = "" if hasSourceFunction is None: hasSourceFunction = NS.hasSourceFunction if hasUnits is None: @@ -60,21 +63,22 @@ def get_triples( if inheritsPropertiesFrom is None: inheritsPropertiesFrom = NS.inheritsPropertiesFrom graph = Graph() + full_label = workflow_namespace + "." + data["label"] # Triple already exists - label_def_triple = (NS[data["label"]], hasSourceFunction, NS[data["function"]]) + label_def_triple = (NS[full_label], hasSourceFunction, NS[data["function"]]) if len(list(graph.triples(label_def_triple))) > 0: return graph graph.add(label_def_triple) for io_ in ["inputs", "outputs"]: for key, d in data[io_].items(): - full_key = data["label"] + f".{io_}." + key + full_key = full_label + f".{io_}." + key label = NS[full_key] graph.add((label, RDFS.label, Literal(full_key))) if d.get("uri", None) is not None: graph.add((label, RDF.type, d["uri"])) if d.get("value", NOT_DATA) is not NOT_DATA: graph.add((label, RDF.value, Literal(d["value"]))) - graph.add((label, NS[io_[:-1] + "Of"], NS[data["label"]])) + graph.add((label, NS[io_[:-1] + "Of"], NS[full_label])) if d.get("units", None) is not None: graph.add((label, hasUnits, NS[d["units"]])) if d.get("connection", None) is not None: From 13f1c5a56d8a28567c8585035c48d3f92e8521aa Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 13:33:40 +0000 Subject: [PATCH 03/29] add namespace everywhere --- pyiron_ontology/parser.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index e775c82..686e1b9 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -56,6 +56,8 @@ def get_triples( ): if workflow_namespace is None: workflow_namespace = "" + else: + workflow_namespace += "." if hasSourceFunction is None: hasSourceFunction = NS.hasSourceFunction if hasUnits is None: @@ -63,7 +65,7 @@ def get_triples( if inheritsPropertiesFrom is None: inheritsPropertiesFrom = NS.inheritsPropertiesFrom graph = Graph() - full_label = workflow_namespace + "." + data["label"] + full_label = workflow_namespace + data["label"] # Triple already exists label_def_triple = (NS[full_label], hasSourceFunction, NS[data["function"]]) if len(list(graph.triples(label_def_triple))) > 0: @@ -82,9 +84,9 @@ def get_triples( if d.get("units", None) is not None: graph.add((label, hasUnits, NS[d["units"]])) if d.get("connection", None) is not None: - graph.add((label, inheritsPropertiesFrom, NS[d["connection"]])) + graph.add((label, inheritsPropertiesFrom, NS[workflow_namespace + d["connection"]])) for t in _get_triples_from_restrictions(d, NS): - graph.add(_parse_triple(t, NS, label=label, data=data)) + graph.add(_parse_triple(t, NS, ns=full_label, label=label)) return graph @@ -115,7 +117,7 @@ def restriction_to_triple(restrictions): return triples -def _parse_triple(triples, NS, label=None, data=None): +def _parse_triple(triples, NS, ns, label=None): if len(triples) == 2: subj, pred, obj = label, triples[0], triples[1] elif len(triples) == 3: @@ -123,7 +125,7 @@ def _parse_triple(triples, NS, label=None, data=None): else: raise ValueError("Triple must have 2 or 3 elements") if obj.startswith("inputs.") or obj.startswith("outputs."): - obj = data["label"] + "." + obj + obj = ns + obj if not isinstance(obj, URIRef): obj = NS[obj] return subj, pred, obj @@ -192,9 +194,14 @@ def parse_workflow( graph.add((workflow_label, RDFS.label, Literal(workflow.label))) for key, value in workflow.children.items(): data = get_inputs_and_outputs(value) - graph.add((workflow_label, hasNode, NS[data["label"]])) + graph.add((workflow_label, hasNode, NS[workflow.label + "." + data["label"]])) graph += get_triples( - data, NS, hasSourceFunction, hasUnits, inheritsPropertiesFrom + data=data, + NS=NS, + workflow_namespace=workflow.label, + hasSourceFunction=hasSourceFunction, + hasUnits=hasUnits, + inheritsPropertiesFrom=inheritsPropertiesFrom, ) if inherit_properties: _inherit_properties(graph, NS) From a8dd4bfb31e443577db7c4664686ee442146ba39 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 13:48:29 +0000 Subject: [PATCH 04/29] restore dot --- pyiron_ontology/parser.py | 10 ++++++++-- tests/unit/test_parser.py | 18 +++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 686e1b9..84d2710 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -84,7 +84,13 @@ def get_triples( if d.get("units", None) is not None: graph.add((label, hasUnits, NS[d["units"]])) if d.get("connection", None) is not None: - graph.add((label, inheritsPropertiesFrom, NS[workflow_namespace + d["connection"]])) + graph.add( + ( + label, + inheritsPropertiesFrom, + NS[workflow_namespace + d["connection"]], + ) + ) for t in _get_triples_from_restrictions(d, NS): graph.add(_parse_triple(t, NS, ns=full_label, label=label)) return graph @@ -125,7 +131,7 @@ def _parse_triple(triples, NS, ns, label=None): else: raise ValueError("Triple must have 2 or 3 elements") if obj.startswith("inputs.") or obj.startswith("outputs."): - obj = ns + obj + obj = ns + "." + obj if not isinstance(obj, URIRef): obj = NS[obj] return subj, pred, obj diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index f0a9dc3..a1b83c6 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -4,7 +4,7 @@ from pyiron_ontology.parser import ( get_inputs_and_outputs, get_triples, - inherit_properties, + _inherit_properties, validate_values, ) from pyiron_workflow import Workflow @@ -84,19 +84,15 @@ def test_parser(self): def test_triples(self): speed = calculate_speed() data = get_inputs_and_outputs(speed) - graph = get_triples(data, NS) + graph = get_triples(data=data, NS=NS) self.assertGreater( len(list(graph.triples((None, NS.hasUnits, NS["meter/second"])))), 0 ) + ex_triple = (None, NS.isOutputOf, NS["calculate_speed.inputs.time"]) self.assertEqual( - len( - list( - graph.triples( - (None, NS.isOutputOf, NS["calculate_speed.inputs.time"]) - ) - ) - ), + len(list(graph.triples(ex_triple))), 1, + msg=f"Triple {ex_triple} not found {graph.serialize(format='turtle')}", ) self.assertEqual( len(list(graph.triples((NS.subject, NS.predicate, NS.object)))), 1 @@ -110,8 +106,8 @@ def get_graph(wf): graph.add((NS.Multiplication, RDF.type, OWL.Class)) for value in wf.children.values(): data = get_inputs_and_outputs(value) - graph += get_triples(data, NS) - inherit_properties(graph, NS) + graph += get_triples(data=data, NS=NS) + _inherit_properties(graph, NS) DeductiveClosure(OWLRL_Semantics).expand(graph) return graph From 2860463b180922658d8e3613b90e7299e0b6a974 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 15:07:21 +0000 Subject: [PATCH 05/29] Add tests --- tests/unit/test_parser.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index a1b83c6..df5e187 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -1,11 +1,12 @@ import unittest from owlrl import DeductiveClosure, OWLRL_Semantics -from rdflib import Graph, OWL, RDF +from rdflib import Graph, OWL, RDF, RDFS, Literal from pyiron_ontology.parser import ( get_inputs_and_outputs, get_triples, _inherit_properties, validate_values, + parse_workflow, ) from pyiron_workflow import Workflow from semantikon.typing import u @@ -131,6 +132,25 @@ def test_multiple_outputs(self): self.assertEqual(data["outputs"]["a"]["value"], 1) self.assertEqual(data["outputs"]["b"]["value"], 2) + def test_parse_workflow(self): + wf = Workflow("correct_analysis") + wf.addition = add(a=1.0, b=2.0) + graph = parse_workflow(wf, NS) + self.assertEqual( + len( + list( + graph.triples( + ( + NS["correct_analysis.addition.inputs.a"], + RDFS.label, + Literal("correct_analysis.addition.inputs.a") + ) + ) + ) + ), + 1 + ) + if __name__ == "__main__": unittest.main() From 4089f86da8cc521d150a6314de6a9c66c72c7c1b Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 15:11:18 +0000 Subject: [PATCH 06/29] run black --- tests/unit/test_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index df5e187..04f96b6 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -143,12 +143,12 @@ def test_parse_workflow(self): ( NS["correct_analysis.addition.inputs.a"], RDFS.label, - Literal("correct_analysis.addition.inputs.a") + Literal("correct_analysis.addition.inputs.a"), ) ) ) ), - 1 + 1, ) From 52f3b5f298b5bed811600bcc62d40c9def13b25e Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 15:11:41 +0000 Subject: [PATCH 07/29] codacy --- pyiron_ontology/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 84d2710..620380d 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -198,7 +198,7 @@ def parse_workflow( graph = Graph() workflow_label = NS[workflow.label] graph.add((workflow_label, RDFS.label, Literal(workflow.label))) - for key, value in workflow.children.items(): + for value in workflow.children.values(): data = get_inputs_and_outputs(value) graph.add((workflow_label, hasNode, NS[workflow.label + "." + data["label"]])) graph += get_triples( From bf4e20448c2959b8463a496fe2a61c5d343377d9 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 17:40:10 +0000 Subject: [PATCH 08/29] Add activity and entity --- pyiron_ontology/parser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 620380d..3d42b0f 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,5 +1,5 @@ from semantikon.converter import parse_input_args, parse_output_args -from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL +from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV from pyiron_workflow import NOT_DATA @@ -67,15 +67,17 @@ def get_triples( graph = Graph() full_label = workflow_namespace + data["label"] # Triple already exists - label_def_triple = (NS[full_label], hasSourceFunction, NS[data["function"]]) + label_def_triple = (NS[full_label], RDF.type, PROV.Activity) if len(list(graph.triples(label_def_triple))) > 0: return graph graph.add(label_def_triple) + graph.add((NS[full_label], hasSourceFunction, NS[data["function"]])) for io_ in ["inputs", "outputs"]: for key, d in data[io_].items(): full_key = full_label + f".{io_}." + key label = NS[full_key] graph.add((label, RDFS.label, Literal(full_key))) + graph.add((label, RDF.type, PROV.Entity)) if d.get("uri", None) is not None: graph.add((label, RDF.type, d["uri"])) if d.get("value", NOT_DATA) is not NOT_DATA: From bd7549cc0c7a448533ace5cbfa51d99debcb25f6 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 17:56:13 +0000 Subject: [PATCH 09/29] current state --- pyiron_ontology/parser.py | 24 +++++++++++++----------- tests/unit/test_parser.py | 6 +++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 3d42b0f..45af60d 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -67,34 +67,34 @@ def get_triples( graph = Graph() full_label = workflow_namespace + data["label"] # Triple already exists - label_def_triple = (NS[full_label], RDF.type, PROV.Activity) + label_def_triple = (URIRef(full_label), RDF.type, PROV.Activity) if len(list(graph.triples(label_def_triple))) > 0: return graph graph.add(label_def_triple) - graph.add((NS[full_label], hasSourceFunction, NS[data["function"]])) + graph.add((URIRef(full_label), hasSourceFunction, URIRef(data["function"]))) for io_ in ["inputs", "outputs"]: for key, d in data[io_].items(): full_key = full_label + f".{io_}." + key - label = NS[full_key] + label = URIRef(full_key) graph.add((label, RDFS.label, Literal(full_key))) graph.add((label, RDF.type, PROV.Entity)) if d.get("uri", None) is not None: graph.add((label, RDF.type, d["uri"])) if d.get("value", NOT_DATA) is not NOT_DATA: graph.add((label, RDF.value, Literal(d["value"]))) - graph.add((label, NS[io_[:-1] + "Of"], NS[full_label])) + graph.add((label, NS[io_[:-1] + "Of"], URIRef(full_label))) if d.get("units", None) is not None: - graph.add((label, hasUnits, NS[d["units"]])) + graph.add((label, hasUnits, URIRef(d["units"]))) if d.get("connection", None) is not None: graph.add( ( label, inheritsPropertiesFrom, - NS[workflow_namespace + d["connection"]], + URIRef(workflow_namespace + d["connection"]), ) ) for t in _get_triples_from_restrictions(d, NS): - graph.add(_parse_triple(t, NS, ns=full_label, label=label)) + graph.add(_parse_triple(t, ns=full_label, label=label)) return graph @@ -125,7 +125,7 @@ def restriction_to_triple(restrictions): return triples -def _parse_triple(triples, NS, ns, label=None): +def _parse_triple(triples, ns, label=None): if len(triples) == 2: subj, pred, obj = label, triples[0], triples[1] elif len(triples) == 3: @@ -135,7 +135,7 @@ def _parse_triple(triples, NS, ns, label=None): if obj.startswith("inputs.") or obj.startswith("outputs."): obj = ns + "." + obj if not isinstance(obj, URIRef): - obj = NS[obj] + obj = URIRef(obj) return subj, pred, obj @@ -198,11 +198,13 @@ def parse_workflow( inheritsPropertiesFrom = NS.inheritsPropertiesFrom if graph is None: graph = Graph() - workflow_label = NS[workflow.label] + workflow_label = URIRef(workflow.label) graph.add((workflow_label, RDFS.label, Literal(workflow.label))) for value in workflow.children.values(): data = get_inputs_and_outputs(value) - graph.add((workflow_label, hasNode, NS[workflow.label + "." + data["label"]])) + graph.add( + (workflow_label, hasNode, URIRef(workflow.label + "." + data["label"])) + ) graph += get_triples( data=data, NS=NS, diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 04f96b6..3a2c611 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -1,6 +1,6 @@ import unittest from owlrl import DeductiveClosure, OWLRL_Semantics -from rdflib import Graph, OWL, RDF, RDFS, Literal +from rdflib import Graph, OWL, RDF, RDFS, Literal, URIRef from pyiron_ontology.parser import ( get_inputs_and_outputs, get_triples, @@ -87,7 +87,7 @@ def test_triples(self): data = get_inputs_and_outputs(speed) graph = get_triples(data=data, NS=NS) self.assertGreater( - len(list(graph.triples((None, NS.hasUnits, NS["meter/second"])))), 0 + len(list(graph.triples((None, NS.hasUnits, URIRef("meter/second"))))), 0 ) ex_triple = (None, NS.isOutputOf, NS["calculate_speed.inputs.time"]) self.assertEqual( @@ -141,7 +141,7 @@ def test_parse_workflow(self): list( graph.triples( ( - NS["correct_analysis.addition.inputs.a"], + URIRef("correct_analysis.addition.inputs.a"), RDFS.label, Literal("correct_analysis.addition.inputs.a"), ) From 872be09d2c587a9f42d2b66766fe19b1bfd4a1d7 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Tue, 21 Jan 2025 20:37:17 +0000 Subject: [PATCH 10/29] Use pyiron namespace --- pyiron_ontology/parser.py | 36 +++++++++++++++--------------------- tests/unit/test_parser.py | 39 ++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 45af60d..fb8fb57 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,8 +1,11 @@ from semantikon.converter import parse_input_args, parse_output_args -from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV +from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV, Namespace from pyiron_workflow import NOT_DATA +PNS = Namespace("http://pyiron.org/ontology/") + + def get_source_output(var): if not var.connected: return None @@ -48,7 +51,6 @@ def get_inputs_and_outputs(node): def get_triples( data, - NS, workflow_namespace=None, hasSourceFunction=None, hasUnits=None, @@ -59,11 +61,11 @@ def get_triples( else: workflow_namespace += "." if hasSourceFunction is None: - hasSourceFunction = NS.hasSourceFunction + hasSourceFunction = PNS.hasSourceFunction if hasUnits is None: - hasUnits = NS.hasUnits + hasUnits = PNS.hasUnits if inheritsPropertiesFrom is None: - inheritsPropertiesFrom = NS.inheritsPropertiesFrom + inheritsPropertiesFrom = PNS.inheritsPropertiesFrom graph = Graph() full_label = workflow_namespace + data["label"] # Triple already exists @@ -82,7 +84,7 @@ def get_triples( graph.add((label, RDF.type, d["uri"])) if d.get("value", NOT_DATA) is not NOT_DATA: graph.add((label, RDF.value, Literal(d["value"]))) - graph.add((label, NS[io_[:-1] + "Of"], URIRef(full_label))) + graph.add((label, PNS[io_[:-1] + "Of"], URIRef(full_label))) if d.get("units", None) is not None: graph.add((label, hasUnits, URIRef(d["units"]))) if d.get("connection", None) is not None: @@ -93,12 +95,12 @@ def get_triples( URIRef(workflow_namespace + d["connection"]), ) ) - for t in _get_triples_from_restrictions(d, NS): + for t in _get_triples_from_restrictions(d): graph.add(_parse_triple(t, ns=full_label, label=label)) return graph -def _get_triples_from_restrictions(data, NS): +def _get_triples_from_restrictions(data): triples = [] if data.get("restrictions", None) is not None: triples = restriction_to_triple(data["restrictions"]) @@ -139,9 +141,9 @@ def _parse_triple(triples, ns, label=None): return subj, pred, obj -def _inherit_properties(graph, NS, n=None): +def _inherit_properties(graph, n=None): update_query = ( - f"PREFIX ns: <{NS}>", + f"PREFIX ns: <{PNS}>", f"PREFIX rdfs: <{RDFS}>", f"PREFIX rdf: <{RDF}>", "", @@ -158,7 +160,7 @@ def _inherit_properties(graph, NS, n=None): "}", ) if n is None: - n = len(list(graph.triples((None, NS.inheritsPropertiesFrom, None)))) + n = len(list(graph.triples((None, PNS.inheritsPropertiesFrom, None)))) for _ in range(n): graph.update("\n".join(update_query)) @@ -180,7 +182,6 @@ def validate_values(graph): def parse_workflow( workflow, - NS, graph=None, inherit_properties=True, hasNode=None, @@ -189,13 +190,7 @@ def parse_workflow( inheritsPropertiesFrom=None, ): if hasNode is None: - hasNode = NS.hasNode - if hasSourceFunction is None: - hasSourceFunction = NS.hasSourceFunction - if hasUnits is None: - hasUnits = NS.hasUnits - if inheritsPropertiesFrom is None: - inheritsPropertiesFrom = NS.inheritsPropertiesFrom + hasNode = PNS.hasNode if graph is None: graph = Graph() workflow_label = URIRef(workflow.label) @@ -207,12 +202,11 @@ def parse_workflow( ) graph += get_triples( data=data, - NS=NS, workflow_namespace=workflow.label, hasSourceFunction=hasSourceFunction, hasUnits=hasUnits, inheritsPropertiesFrom=inheritsPropertiesFrom, ) if inherit_properties: - _inherit_properties(graph, NS) + _inherit_properties(graph) return graph diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 3a2c611..26a01e3 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -13,7 +13,8 @@ from rdflib import Namespace -NS = Namespace("http://example.org/") +PNS = Namespace("http://pyiron.org/ontology/") +EX = Namespace("http://example.org/") @Workflow.wrap.as_function_node("speed") @@ -23,13 +24,13 @@ def calculate_speed( ) -> u( float, units="meter/second", - triples=((NS.isOutputOf, "inputs.time"), (NS.subject, NS.predicate, NS.object)), + triples=((EX.isOutputOf, "inputs.time"), (EX.subject, EX.predicate, EX.object)), ): return distance / time @Workflow.wrap.as_function_node("result") -def add(a: float, b: float) -> u(float, triples=(NS.HasOperation, NS.Addition)): +def add(a: float, b: float) -> u(float, triples=(EX.HasOperation, EX.Addition)): return a + b @@ -37,8 +38,8 @@ def add(a: float, b: float) -> u(float, triples=(NS.HasOperation, NS.Addition)): def multiply(a: float, b: float) -> u( float, triples=( - (NS.HasOperation, NS.Multiplication), - (NS.inheritsPropertiesFrom, "inputs.a"), + (EX.HasOperation, EX.Multiplication), + (PNS.inheritsPropertiesFrom, "inputs.a"), ), ): return a * b @@ -49,8 +50,8 @@ def correct_analysis( a: u( float, restrictions=( - (OWL.onProperty, NS.HasOperation), - (OWL.someValuesFrom, NS.Addition), + (OWL.onProperty, EX.HasOperation), + (OWL.someValuesFrom, EX.Addition), ), ) ) -> float: @@ -62,8 +63,8 @@ def wrong_analysis( a: u( float, restrictions=( - (OWL.onProperty, NS.HasOperation), - (OWL.someValuesFrom, NS.Division), + (OWL.onProperty, EX.HasOperation), + (OWL.someValuesFrom, EX.Division), ), ) ) -> float: @@ -85,30 +86,30 @@ def test_parser(self): def test_triples(self): speed = calculate_speed() data = get_inputs_and_outputs(speed) - graph = get_triples(data=data, NS=NS) + graph = get_triples(data=data) self.assertGreater( - len(list(graph.triples((None, NS.hasUnits, URIRef("meter/second"))))), 0 + len(list(graph.triples((None, PNS.hasUnits, URIRef("meter/second"))))), 0 ) - ex_triple = (None, NS.isOutputOf, NS["calculate_speed.inputs.time"]) + ex_triple = (None, EX.isOutputOf, URIRef("calculate_speed.inputs.time")) self.assertEqual( len(list(graph.triples(ex_triple))), 1, msg=f"Triple {ex_triple} not found {graph.serialize(format='turtle')}", ) self.assertEqual( - len(list(graph.triples((NS.subject, NS.predicate, NS.object)))), 1 + len(list(graph.triples((EX.subject, EX.predicate, EX.object)))), 1 ) def test_correct_analysis(self): def get_graph(wf): graph = Graph() - graph.add((NS.HasOperation, RDF.type, RDF.Property)) - graph.add((NS.Addition, RDF.type, OWL.Class)) - graph.add((NS.Multiplication, RDF.type, OWL.Class)) + graph.add((EX.HasOperation, RDF.type, RDF.Property)) + graph.add((EX.Addition, RDF.type, OWL.Class)) + graph.add((EX.Multiplication, RDF.type, OWL.Class)) for value in wf.children.values(): data = get_inputs_and_outputs(value) - graph += get_triples(data=data, NS=NS) - _inherit_properties(graph, NS) + graph += get_triples(data=data) + _inherit_properties(graph) DeductiveClosure(OWLRL_Semantics).expand(graph) return graph @@ -135,7 +136,7 @@ def test_multiple_outputs(self): def test_parse_workflow(self): wf = Workflow("correct_analysis") wf.addition = add(a=1.0, b=2.0) - graph = parse_workflow(wf, NS) + graph = parse_workflow(wf) self.assertEqual( len( list( From ed384bb3160e3aac85fe6469d188b35a8d61f297 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 06:08:37 +0000 Subject: [PATCH 11/29] Make it clear that the predicate is just for parsing and does not have a meaning --- tests/unit/test_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 26a01e3..ee0673b 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -24,7 +24,7 @@ def calculate_speed( ) -> u( float, units="meter/second", - triples=((EX.isOutputOf, "inputs.time"), (EX.subject, EX.predicate, EX.object)), + triples=((EX.somehowRelatedTo, "inputs.time"), (EX.subject, EX.predicate, EX.object)), ): return distance / time @@ -90,7 +90,7 @@ def test_triples(self): self.assertGreater( len(list(graph.triples((None, PNS.hasUnits, URIRef("meter/second"))))), 0 ) - ex_triple = (None, EX.isOutputOf, URIRef("calculate_speed.inputs.time")) + ex_triple = (None, EX.somehowRelatedTo, URIRef("calculate_speed.inputs.time")) self.assertEqual( len(list(graph.triples(ex_triple))), 1, From f69dc311b6b1343b958cc58d6a617f65bdfe326b Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 06:18:58 +0000 Subject: [PATCH 12/29] Add type hints --- pyiron_ontology/parser.py | 49 +++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index fb8fb57..68c651c 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,19 +1,20 @@ from semantikon.converter import parse_input_args, parse_output_args from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV, Namespace -from pyiron_workflow import NOT_DATA +from pyiron_workflow import NOT_DATA, Workflow +from pyiron_workflow.node import Node PNS = Namespace("http://pyiron.org/ontology/") -def get_source_output(var): +def get_source_output(var: Node) -> str | None: if not var.connected: return None connection = var.connections[0] return f"{connection.owner.label}.outputs.{connection.label}" -def get_inputs_and_outputs(node): +def get_inputs_and_outputs(node: Node) -> dict: """ Read input and output arguments with their type hints and return a dictionary containing all input output information @@ -50,12 +51,12 @@ def get_inputs_and_outputs(node): def get_triples( - data, - workflow_namespace=None, - hasSourceFunction=None, - hasUnits=None, - inheritsPropertiesFrom=None, -): + data: dict, + workflow_namespace: str | None = None, + hasSourceFunction: URIRef | None = None, + hasUnits: URIRef | None = None, + inheritsPropertiesFrom: URIRef | None = None, +) -> Graph: if workflow_namespace is None: workflow_namespace = "" else: @@ -100,7 +101,7 @@ def get_triples( return graph -def _get_triples_from_restrictions(data): +def _get_triples_from_restrictions(data: dict) -> list: triples = [] if data.get("restrictions", None) is not None: triples = restriction_to_triple(data["restrictions"]) @@ -112,7 +113,7 @@ def _get_triples_from_restrictions(data): return triples -def restriction_to_triple(restrictions): +def restriction_to_triple(restrictions: tuple) -> list: triples = [] assert isinstance(restrictions, tuple) and isinstance(restrictions[0], tuple) if not isinstance(restrictions[0][0], tuple): @@ -127,7 +128,11 @@ def restriction_to_triple(restrictions): return triples -def _parse_triple(triples, ns, label=None): +def _parse_triple( + triples: tuple, + ns: str, + label: URIRef | None = None, +) -> tuple: if len(triples) == 2: subj, pred, obj = label, triples[0], triples[1] elif len(triples) == 3: @@ -141,7 +146,7 @@ def _parse_triple(triples, ns, label=None): return subj, pred, obj -def _inherit_properties(graph, n=None): +def _inherit_properties(graph: Graph, n: int | None = None): update_query = ( f"PREFIX ns: <{PNS}>", f"PREFIX rdfs: <{RDFS}>", @@ -165,7 +170,7 @@ def _inherit_properties(graph, n=None): graph.update("\n".join(update_query)) -def validate_values(graph): +def validate_values(graph: Graph) -> list: missing_triples = [] for restrictions in graph.subjects(RDF.type, OWL.Restriction): on_property = graph.value(restrictions, OWL.onProperty) @@ -181,14 +186,14 @@ def validate_values(graph): def parse_workflow( - workflow, - graph=None, - inherit_properties=True, - hasNode=None, - hasSourceFunction=None, - hasUnits=None, - inheritsPropertiesFrom=None, -): + workflow: Workflow, + graph: Graph | None = None, + inherit_properties: bool = True, + hasNode: URIRef | None = None, + hasSourceFunction: URIRef | None = None, + hasUnits: URIRef | None = None, + inheritsPropertiesFrom: URIRef | None = None, +) -> Graph: if hasNode is None: hasNode = PNS.hasNode if graph is None: From edbed79e6690d52ccb4ba8d6c31250d81af85dfe Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 06:27:30 +0000 Subject: [PATCH 13/29] replace value by node --- pyiron_ontology/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 68c651c..424e546 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -200,8 +200,8 @@ def parse_workflow( graph = Graph() workflow_label = URIRef(workflow.label) graph.add((workflow_label, RDFS.label, Literal(workflow.label))) - for value in workflow.children.values(): - data = get_inputs_and_outputs(value) + for node in workflow: + data = get_inputs_and_outputs(node) graph.add( (workflow_label, hasNode, URIRef(workflow.label + "." + data["label"])) ) From b9e6b6a9a4ab7ba226d6bd4ba74ebc3b3ffbd00d Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 06:56:11 +0000 Subject: [PATCH 14/29] add test --- pyiron_ontology/parser.py | 4 +++- tests/unit/test_parser.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 424e546..44f3828 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,6 +1,6 @@ from semantikon.converter import parse_input_args, parse_output_args from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV, Namespace -from pyiron_workflow import NOT_DATA, Workflow +from pyiron_workflow import NOT_DATA, Workflow, Macro from pyiron_workflow.node import Node @@ -26,6 +26,8 @@ def get_inputs_and_outputs(node: Node) -> dict: (dict): dictionary containing input output args, type hints, values and variable names """ + if isinstance(node, Macro): + raise NotImplementedError("Macros are not supported yet") inputs = parse_input_args(node.node_function) outputs = parse_output_args(node.node_function) if isinstance(outputs, dict): diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index ee0673b..5fed7d1 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -152,6 +152,15 @@ def test_parse_workflow(self): 1, ) + def test_macro(self): + @Workflow.wrap.as_macro_node + def operation(macro): + macro.add = add(a=1.0, b=2.0) + macro.multiply = multiply(a=macro.add, b=3.0) + return macro.multiply + wf = Workflow("macro") + wf.macro = operation() + self.assertRaises(NotImplementedError, get_inputs_and_outputs, wf.macro) if __name__ == "__main__": unittest.main() From 8513bb50a44382264668eb81ca4257aa900e267e Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 07:19:37 +0000 Subject: [PATCH 15/29] add docstring --- pyiron_ontology/parser.py | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 44f3828..4e97a3b 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -59,6 +59,58 @@ def get_triples( hasUnits: URIRef | None = None, inheritsPropertiesFrom: URIRef | None = None, ) -> Graph: + """ + Generate triples from a dictionary containing input output information. The + dictionary should be obtained from the get_inputs_and_outputs function, and + should contain the keys "inputs", "outputs", "function" and "label". Within + "inputs" and "outputs", the keys should be the variable names, and the values + should be dictionaries containing the keys "type", "value", "var_name" and + "connection". The "connection" key should contain the label of the output + variable that the input is connected to. The "type" key should contain the + URI of the type of the variable. The "value" key should contain the value of + the variable. The "var_name" key should contain the variable name. The "function" + key should contain the name of the function that the node is connected to. The + "label" key should contain the label of the node. In terms of python code, + it should look like this: + + >>> data = { + >>> "inputs": { + >>> "input1": { + >>> "type": URIRef("http://example.org/Type"), + >>> "value": 1, + >>> "triples": some_triples, + >>> "restrictions": some_restrictions, + >>> "var_name": "input1", + >>> "connection": "output1" + >>> } + >>> }, + >>> "outputs": { + >>> "output1": { + >>> "type": URIRef("http://example.org/Type"), + >>> "value": 1, + >>> "triples": other_triples, + >>> "var_name": "output1" + >>> } + >>> }, + >>> "function": "function_name", + >>> "label": "label" + >>> } + + triples should consist of a list of tuples, where each tuple contains 2 or 3 + elements. If the tuple contains 2 elements, the first element should be the + predicate and the second element should be the object, in order for the subject + to be generated from the variable name. + + Args: + data (dict): dictionary containing input output information + workflow_namespace (str): namespace of the workflow + hasSourceFunction (rdflib.URIRef): predicate for source function + hasUnits (rdflib.URIRef): predicate for units + inheritsPropertiesFrom (rdflib.URIRef): predicate for inherited properties + + Returns: + (rdflib.Graph): graph containing triples + """ if workflow_namespace is None: workflow_namespace = "" else: @@ -116,6 +168,31 @@ def _get_triples_from_restrictions(data: dict) -> list: def restriction_to_triple(restrictions: tuple) -> list: + """ + Convert restrictions to triples + + Args: + restrictions (tuple): tuple of restrictions + + Returns: + (list): list of triples + + In the semantikon notation, restrictions are given in the format: + + >>> restrictions = ( + >>> (OWL.onProperty, EX.HasSomething), + >>> (OWL.someValuesFrom, EX.Something) + >>> ) + + This tuple is internally converted to the triples: + + >>> ( + >>> (EX.HasSomethingRestriction, RDF.type, OWL.Restriction), + >>> (EX.HasSomethingRestriction, OWL.onProperty, EX.HasSomething), + >>> (EX.HasSomethingRestriction, OWL.someValuesFrom, EX.Something), + >>> (my_object, RDFS.subClassOf, EX.HasSomethingRestriction) + >>> ) + """ triples = [] assert isinstance(restrictions, tuple) and isinstance(restrictions[0], tuple) if not isinstance(restrictions[0][0], tuple): @@ -173,6 +250,15 @@ def _inherit_properties(graph: Graph, n: int | None = None): def validate_values(graph: Graph) -> list: + """ + Validate if all values required by restrictions are present in the graph + + Args: + graph (rdflib.Graph): graph to be validated + + Returns: + (list): list of missing triples + """ missing_triples = [] for restrictions in graph.subjects(RDF.type, OWL.Restriction): on_property = graph.value(restrictions, OWL.onProperty) @@ -196,6 +282,21 @@ def parse_workflow( hasUnits: URIRef | None = None, inheritsPropertiesFrom: URIRef | None = None, ) -> Graph: + """ + Generate RDF graph from a pyiron workflow object + + Args: + workflow (pyiron_workflow.workflow.Workflow): workflow object + graph (rdflib.Graph): graph to be updated + inherit_properties (bool): if True, properties are inherited + hasNode (rdflib.URIRef): predicate for node + hasSourceFunction (rdflib.URIRef): predicate for source function + hasUnits (rdflib.URIRef): predicate for units + inheritsPropertiesFrom (rdflib.URIRef): predicate for inherited properties + + Returns: + (rdflib.Graph): graph containing workflow information + """ if hasNode is None: hasNode = PNS.hasNode if graph is None: From 48558bc67d64d49b36eed6d8e8d1495beeb078dd Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 07:27:06 +0000 Subject: [PATCH 16/29] run black --- pyiron_ontology/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 4e97a3b..42bc02a 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -284,7 +284,7 @@ def parse_workflow( ) -> Graph: """ Generate RDF graph from a pyiron workflow object - + Args: workflow (pyiron_workflow.workflow.Workflow): workflow object graph (rdflib.Graph): graph to be updated From 66e469707afd6102d507a5846c2e8d111b0d9cce Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 08:29:20 +0000 Subject: [PATCH 17/29] Add URIRef to uri just in case --- pyiron_ontology/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 42bc02a..795478b 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -136,7 +136,7 @@ def get_triples( graph.add((label, RDFS.label, Literal(full_key))) graph.add((label, RDF.type, PROV.Entity)) if d.get("uri", None) is not None: - graph.add((label, RDF.type, d["uri"])) + graph.add((label, RDF.type, URIRef(d["uri"]))) if d.get("value", NOT_DATA) is not NOT_DATA: graph.add((label, RDF.value, Literal(d["value"]))) graph.add((label, PNS[io_[:-1] + "Of"], URIRef(full_label))) From 6daf91351bd3fd9576cfca7d67291d1c7d768910 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 10:09:01 +0000 Subject: [PATCH 18/29] Create pyiron specific namespace --- pyiron_ontology/parser.py | 52 +++++++++++++++------------------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 795478b..d8bc8cd 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -4,7 +4,17 @@ from pyiron_workflow.node import Node -PNS = Namespace("http://pyiron.org/ontology/") +class PNS: + BASE = Namespace("http://pyiron.org/ontology/") + hasNode = BASE["hasNode"] + hasSourceFunction = BASE["hasSourceFunction"] + hasUnits = BASE["hasUnits"] + inheritsPropertiesFrom = BASE["inheritsPropertiesFrom"] + inputOf = BASE["inputOf"] + outputOf = BASE["outputOf"] + + def __getattr__(self, name): + raise AttributeError(f"Namespace {self.BASE} has no attribute {name}") def get_source_output(var: Node) -> str | None: @@ -55,9 +65,6 @@ def get_inputs_and_outputs(node: Node) -> dict: def get_triples( data: dict, workflow_namespace: str | None = None, - hasSourceFunction: URIRef | None = None, - hasUnits: URIRef | None = None, - inheritsPropertiesFrom: URIRef | None = None, ) -> Graph: """ Generate triples from a dictionary containing input output information. The @@ -104,9 +111,6 @@ def get_triples( Args: data (dict): dictionary containing input output information workflow_namespace (str): namespace of the workflow - hasSourceFunction (rdflib.URIRef): predicate for source function - hasUnits (rdflib.URIRef): predicate for units - inheritsPropertiesFrom (rdflib.URIRef): predicate for inherited properties Returns: (rdflib.Graph): graph containing triples @@ -115,12 +119,6 @@ def get_triples( workflow_namespace = "" else: workflow_namespace += "." - if hasSourceFunction is None: - hasSourceFunction = PNS.hasSourceFunction - if hasUnits is None: - hasUnits = PNS.hasUnits - if inheritsPropertiesFrom is None: - inheritsPropertiesFrom = PNS.inheritsPropertiesFrom graph = Graph() full_label = workflow_namespace + data["label"] # Triple already exists @@ -128,7 +126,7 @@ def get_triples( if len(list(graph.triples(label_def_triple))) > 0: return graph graph.add(label_def_triple) - graph.add((URIRef(full_label), hasSourceFunction, URIRef(data["function"]))) + graph.add((URIRef(full_label), PNS.hasSourceFunction, URIRef(data["function"]))) for io_ in ["inputs", "outputs"]: for key, d in data[io_].items(): full_key = full_label + f".{io_}." + key @@ -139,14 +137,17 @@ def get_triples( graph.add((label, RDF.type, URIRef(d["uri"]))) if d.get("value", NOT_DATA) is not NOT_DATA: graph.add((label, RDF.value, Literal(d["value"]))) - graph.add((label, PNS[io_[:-1] + "Of"], URIRef(full_label))) + if io_ == "inputs": + graph.add((label, PNS.inputOf, URIRef(full_label))) + elif io_ == "outputs": + graph.add((label, PNS.outputOf, URIRef(full_label))) if d.get("units", None) is not None: - graph.add((label, hasUnits, URIRef(d["units"]))) + graph.add((label, PNS.hasUnits, URIRef(d["units"]))) if d.get("connection", None) is not None: graph.add( ( label, - inheritsPropertiesFrom, + PNS.inheritsPropertiesFrom, URIRef(workflow_namespace + d["connection"]), ) ) @@ -227,7 +228,7 @@ def _parse_triple( def _inherit_properties(graph: Graph, n: int | None = None): update_query = ( - f"PREFIX ns: <{PNS}>", + f"PREFIX ns: <{PNS.BASE}>", f"PREFIX rdfs: <{RDFS}>", f"PREFIX rdf: <{RDF}>", "", @@ -277,10 +278,6 @@ def parse_workflow( workflow: Workflow, graph: Graph | None = None, inherit_properties: bool = True, - hasNode: URIRef | None = None, - hasSourceFunction: URIRef | None = None, - hasUnits: URIRef | None = None, - inheritsPropertiesFrom: URIRef | None = None, ) -> Graph: """ Generate RDF graph from a pyiron workflow object @@ -289,16 +286,10 @@ def parse_workflow( workflow (pyiron_workflow.workflow.Workflow): workflow object graph (rdflib.Graph): graph to be updated inherit_properties (bool): if True, properties are inherited - hasNode (rdflib.URIRef): predicate for node - hasSourceFunction (rdflib.URIRef): predicate for source function - hasUnits (rdflib.URIRef): predicate for units - inheritsPropertiesFrom (rdflib.URIRef): predicate for inherited properties Returns: (rdflib.Graph): graph containing workflow information """ - if hasNode is None: - hasNode = PNS.hasNode if graph is None: graph = Graph() workflow_label = URIRef(workflow.label) @@ -306,14 +297,11 @@ def parse_workflow( for node in workflow: data = get_inputs_and_outputs(node) graph.add( - (workflow_label, hasNode, URIRef(workflow.label + "." + data["label"])) + (workflow_label, PNS.hasNode, URIRef(workflow.label + "." + data["label"])) ) graph += get_triples( data=data, workflow_namespace=workflow.label, - hasSourceFunction=hasSourceFunction, - hasUnits=hasUnits, - inheritsPropertiesFrom=inheritsPropertiesFrom, ) if inherit_properties: _inherit_properties(graph) From 66e291aa5947c8702e66e698e0cc988c5cb204e0 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 10:13:24 +0000 Subject: [PATCH 19/29] Add tests --- pyiron_ontology/parser.py | 3 --- tests/unit/test_parser.py | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index d8bc8cd..d9a4ed3 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -13,9 +13,6 @@ class PNS: inputOf = BASE["inputOf"] outputOf = BASE["outputOf"] - def __getattr__(self, name): - raise AttributeError(f"Namespace {self.BASE} has no attribute {name}") - def get_source_output(var: Node) -> str | None: if not var.connected: diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 5fed7d1..fafc166 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -7,13 +7,13 @@ _inherit_properties, validate_values, parse_workflow, + PNS, ) from pyiron_workflow import Workflow from semantikon.typing import u from rdflib import Namespace -PNS = Namespace("http://pyiron.org/ontology/") EX = Namespace("http://example.org/") @@ -162,5 +162,10 @@ def operation(macro): wf.macro = operation() self.assertRaises(NotImplementedError, get_inputs_and_outputs, wf.macro) + def test_namespace(self): + self.assertEqual(PNS.hasUnits, URIRef("http://pyiron.org/ontology/hasUnits")) + with self.assertRaises(AttributeError): + _ = PNS.ahoy + if __name__ == "__main__": unittest.main() From f67b9a67a156c7b533a6da07073329a714442219 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 13:44:12 +0000 Subject: [PATCH 20/29] Codacy --- tests/unit/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index fafc166..e6199c0 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -154,7 +154,7 @@ def test_parse_workflow(self): def test_macro(self): @Workflow.wrap.as_macro_node - def operation(macro): + def operation(macro=None): macro.add = add(a=1.0, b=2.0) macro.multiply = multiply(a=macro.add, b=3.0) return macro.multiply From 08013ccb7b754d650a8c66fa12a5f31a0b63ebfd Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 22 Jan 2025 09:35:27 -0800 Subject: [PATCH 21/29] Break apart parsing and business Signed-off-by: liamhuber --- pyiron_ontology/parser.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index d9a4ed3..fbdd2f9 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,9 +1,14 @@ +from typing import Any, TypeAlias + from semantikon.converter import parse_input_args, parse_output_args from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV, Namespace from pyiron_workflow import NOT_DATA, Workflow, Macro from pyiron_workflow.node import Node +class Placeholder: + pass + class PNS: BASE = Namespace("http://pyiron.org/ontology/") hasNode = BASE["hasNode"] @@ -164,8 +169,14 @@ def _get_triples_from_restrictions(data: dict) -> list: triples.extend([data["triples"]]) return triples +_restriction_type: TypeAlias = tuple[tuple[Any, Any], ...] -def restriction_to_triple(restrictions: tuple) -> list: +def _validate_restriction_format(restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type]) -> tuple[_restriction_type]: + return restrictions + +def restriction_to_triple( + restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type] +) -> list[tuple[Any, Any, Any]]: """ Convert restrictions to triples @@ -188,20 +199,17 @@ def restriction_to_triple(restrictions: tuple) -> list: >>> (EX.HasSomethingRestriction, RDF.type, OWL.Restriction), >>> (EX.HasSomethingRestriction, OWL.onProperty, EX.HasSomething), >>> (EX.HasSomethingRestriction, OWL.someValuesFrom, EX.Something), - >>> (my_object, RDFS.subClassOf, EX.HasSomethingRestriction) + >>> (Placeholder, RDFS.subClassOf, EX.HasSomethingRestriction) >>> ) """ - triples = [] - assert isinstance(restrictions, tuple) and isinstance(restrictions[0], tuple) - if not isinstance(restrictions[0][0], tuple): - restrictions = (restrictions,) - for r in restrictions: - assert len(r[0]) == 2 + restrictions_collection = _validate_restriction_format(restrictions) + triples: list[tuple[Any, Any, Any]] = [] + for r in restrictions_collection: label = r[0][1] + "Restriction" triples.append((label, RDF.type, OWL.Restriction)) for rr in r: triples.append((label, rr[0], rr[1])) - triples.append((RDF.type, label)) + triples.append((Placeholder, RDF.type, label)) return triples From 60104b11030174610af77ae5852405f8713a040f Mon Sep 17 00:00:00 2001 From: liamhuber Date: Wed, 22 Jan 2025 09:36:16 -0800 Subject: [PATCH 22/29] Be explicit about what's missing Signed-off-by: liamhuber --- pyiron_ontology/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index fbdd2f9..e65bdef 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -172,7 +172,7 @@ def _get_triples_from_restrictions(data: dict) -> list: _restriction_type: TypeAlias = tuple[tuple[Any, Any], ...] def _validate_restriction_format(restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type]) -> tuple[_restriction_type]: - return restrictions + raise NotImplementedError def restriction_to_triple( restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type] From 703fc889c3e1bfa041257597959beaa124ff972e Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 19:42:37 +0000 Subject: [PATCH 23/29] Implement restriction validator and Placeholder --- pyiron_ontology/parser.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index e65bdef..5d7a114 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -9,6 +9,7 @@ class Placeholder: pass + class PNS: BASE = Namespace("http://pyiron.org/ontology/") hasNode = BASE["hasNode"] @@ -169,14 +170,26 @@ def _get_triples_from_restrictions(data: dict) -> list: triples.extend([data["triples"]]) return triples -_restriction_type: TypeAlias = tuple[tuple[Any, Any], ...] +_rest_type: TypeAlias = tuple[tuple[URIRef, URIRef], ...] + +def _validate_restriction_format( + restrictions: _rest_type | tuple[_rest_type] | list[_rest_type] +) -> tuple[_rest_type]: + if not all(isinstance(r, tuple) for r in restrictions): + raise ValueError("Restrictions must be tuples of URIRefs") + elif all(isinstance(rr, URIRef) for r in restrictions for rr in r): + return (restrictions,) + elif all( + isinstance(rrr, URIRef) for r in restrictions for rr in r for rrr in rr + ): + return restrictions + else: + raise ValueError("Restrictions must be tuples of URIRefs") -def _validate_restriction_format(restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type]) -> tuple[_restriction_type]: - raise NotImplementedError def restriction_to_triple( - restrictions: _restriction_type | tuple[_restriction_type] | list[_restriction_type] -) -> list[tuple[Any, Any, Any]]: + restrictions: _rest_type | tuple[_rest_type] | list[_rest_type] +) -> list[tuple[URIRef, URIRef, URIRef]]: """ Convert restrictions to triples @@ -203,7 +216,7 @@ def restriction_to_triple( >>> ) """ restrictions_collection = _validate_restriction_format(restrictions) - triples: list[tuple[Any, Any, Any]] = [] + triples: list[tuple[URIRef | Placeholder, URIRef, URIRef]] = [] for r in restrictions_collection: label = r[0][1] + "Restriction" triples.append((label, RDF.type, OWL.Restriction)) @@ -224,6 +237,10 @@ def _parse_triple( subj, pred, obj = triples else: raise ValueError("Triple must have 2 or 3 elements") + if subj is Placeholder: + subj = label + if obj is Placeholder: + obj = label if obj.startswith("inputs.") or obj.startswith("outputs."): obj = ns + "." + obj if not isinstance(obj, URIRef): From 75b7520d65a2818eaaf3e911273f7c97b321f634 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 19:43:33 +0000 Subject: [PATCH 24/29] Remove Any because it's not used anymore --- pyiron_ontology/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 5d7a114..7ee8515 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -1,4 +1,4 @@ -from typing import Any, TypeAlias +from typing import TypeAlias from semantikon.converter import parse_input_args, parse_output_args from rdflib import Graph, Literal, RDF, RDFS, URIRef, OWL, PROV, Namespace From e0bf1e81fdbe0ad676ad4f5744ccde3b6bc7c2be Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 19:53:47 +0000 Subject: [PATCH 25/29] I don't really understand but this silences mypy --- pyiron_ontology/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 7ee8515..6b559d0 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -189,7 +189,7 @@ def _validate_restriction_format( def restriction_to_triple( restrictions: _rest_type | tuple[_rest_type] | list[_rest_type] -) -> list[tuple[URIRef, URIRef, URIRef]]: +) -> list[tuple[URIRef | type, URIRef, URIRef]]: """ Convert restrictions to triples @@ -216,7 +216,7 @@ def restriction_to_triple( >>> ) """ restrictions_collection = _validate_restriction_format(restrictions) - triples: list[tuple[URIRef | Placeholder, URIRef, URIRef]] = [] + triples: list[tuple[URIRef | type, URIRef, URIRef]] = [] for r in restrictions_collection: label = r[0][1] + "Restriction" triples.append((label, RDF.type, OWL.Restriction)) From 0232957ab0ddc2ab55b627a22d5916ad182bd24d Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 20:01:16 +0000 Subject: [PATCH 26/29] add tests for place holder --- tests/unit/test_parser.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index e6199c0..03f4ccd 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -8,6 +8,7 @@ validate_values, parse_workflow, PNS, + Placeholder, ) from pyiron_workflow import Workflow from semantikon.typing import u @@ -24,7 +25,12 @@ def calculate_speed( ) -> u( float, units="meter/second", - triples=((EX.somehowRelatedTo, "inputs.time"), (EX.subject, EX.predicate, EX.object)), + triples=( + (EX.somehowRelatedTo, "inputs.time"), + (EX.subject, EX.predicate, EX.object), + (EX.subject, EX.predicate, Placeholder), + (Placeholder, EX.predicate, EX.object), + ), ): return distance / time @@ -87,6 +93,9 @@ def test_triples(self): speed = calculate_speed() data = get_inputs_and_outputs(speed) graph = get_triples(data=data) + subj = URIRef("http://example.org/subject") + obj = URIRef("http://example.org/object") + label = URIRef("calculate_speed.outputs.speed") self.assertGreater( len(list(graph.triples((None, PNS.hasUnits, URIRef("meter/second"))))), 0 ) @@ -99,6 +108,8 @@ def test_triples(self): self.assertEqual( len(list(graph.triples((EX.subject, EX.predicate, EX.object)))), 1 ) + self.assertEqual(len(list(graph.triples((subj, EX.predicate, label)))), 1) + self.assertEqual(len(list(graph.triples((label, EX.predicate, obj)))), 1) def test_correct_analysis(self): def get_graph(wf): @@ -158,6 +169,7 @@ def operation(macro=None): macro.add = add(a=1.0, b=2.0) macro.multiply = multiply(a=macro.add, b=3.0) return macro.multiply + wf = Workflow("macro") wf.macro = operation() self.assertRaises(NotImplementedError, get_inputs_and_outputs, wf.macro) @@ -167,5 +179,6 @@ def test_namespace(self): with self.assertRaises(AttributeError): _ = PNS.ahoy + if __name__ == "__main__": unittest.main() From 895ba13a82695a3f5ccbf74e2abcf0c6a2ed8091 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 20:01:27 +0000 Subject: [PATCH 27/29] run black --- pyiron_ontology/parser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 6b559d0..9e08f09 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -170,25 +170,25 @@ def _get_triples_from_restrictions(data: dict) -> list: triples.extend([data["triples"]]) return triples + _rest_type: TypeAlias = tuple[tuple[URIRef, URIRef], ...] + def _validate_restriction_format( - restrictions: _rest_type | tuple[_rest_type] | list[_rest_type] + restrictions: _rest_type | tuple[_rest_type] | list[_rest_type], ) -> tuple[_rest_type]: if not all(isinstance(r, tuple) for r in restrictions): raise ValueError("Restrictions must be tuples of URIRefs") elif all(isinstance(rr, URIRef) for r in restrictions for rr in r): return (restrictions,) - elif all( - isinstance(rrr, URIRef) for r in restrictions for rr in r for rrr in rr - ): + elif all(isinstance(rrr, URIRef) for r in restrictions for rr in r for rrr in rr): return restrictions else: raise ValueError("Restrictions must be tuples of URIRefs") def restriction_to_triple( - restrictions: _rest_type | tuple[_rest_type] | list[_rest_type] + restrictions: _rest_type | tuple[_rest_type] | list[_rest_type], ) -> list[tuple[URIRef | type, URIRef, URIRef]]: """ Convert restrictions to triples From 7d447e5ec2d8f6bb8e58c9c23e998b5106eff9a8 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 20:08:47 +0000 Subject: [PATCH 28/29] Replace Placeholder by None --- pyiron_ontology/parser.py | 15 ++++++--------- tests/unit/test_parser.py | 5 ++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 9e08f09..1a28626 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -6,9 +6,6 @@ from pyiron_workflow.node import Node -class Placeholder: - pass - class PNS: BASE = Namespace("http://pyiron.org/ontology/") @@ -189,7 +186,7 @@ def _validate_restriction_format( def restriction_to_triple( restrictions: _rest_type | tuple[_rest_type] | list[_rest_type], -) -> list[tuple[URIRef | type, URIRef, URIRef]]: +) -> list[tuple[URIRef | None, URIRef, URIRef]]: """ Convert restrictions to triples @@ -212,17 +209,17 @@ def restriction_to_triple( >>> (EX.HasSomethingRestriction, RDF.type, OWL.Restriction), >>> (EX.HasSomethingRestriction, OWL.onProperty, EX.HasSomething), >>> (EX.HasSomethingRestriction, OWL.someValuesFrom, EX.Something), - >>> (Placeholder, RDFS.subClassOf, EX.HasSomethingRestriction) + >>> (my_object, RDFS.subClassOf, EX.HasSomethingRestriction) >>> ) """ restrictions_collection = _validate_restriction_format(restrictions) - triples: list[tuple[URIRef | type, URIRef, URIRef]] = [] + triples: list[tuple[URIRef | None, URIRef, URIRef]] = [] for r in restrictions_collection: label = r[0][1] + "Restriction" triples.append((label, RDF.type, OWL.Restriction)) for rr in r: triples.append((label, rr[0], rr[1])) - triples.append((Placeholder, RDF.type, label)) + triples.append((None, RDF.type, label)) return triples @@ -237,9 +234,9 @@ def _parse_triple( subj, pred, obj = triples else: raise ValueError("Triple must have 2 or 3 elements") - if subj is Placeholder: + if subj is None: subj = label - if obj is Placeholder: + if obj is None: obj = label if obj.startswith("inputs.") or obj.startswith("outputs."): obj = ns + "." + obj diff --git a/tests/unit/test_parser.py b/tests/unit/test_parser.py index 03f4ccd..530f381 100644 --- a/tests/unit/test_parser.py +++ b/tests/unit/test_parser.py @@ -8,7 +8,6 @@ validate_values, parse_workflow, PNS, - Placeholder, ) from pyiron_workflow import Workflow from semantikon.typing import u @@ -28,8 +27,8 @@ def calculate_speed( triples=( (EX.somehowRelatedTo, "inputs.time"), (EX.subject, EX.predicate, EX.object), - (EX.subject, EX.predicate, Placeholder), - (Placeholder, EX.predicate, EX.object), + (EX.subject, EX.predicate, None), + (None, EX.predicate, EX.object), ), ): return distance / time From 0073dfd50128ea7ca4fdfeb028a0cb1fd1967752 Mon Sep 17 00:00:00 2001 From: samwaseda Date: Wed, 22 Jan 2025 20:10:22 +0000 Subject: [PATCH 29/29] run black --- pyiron_ontology/parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyiron_ontology/parser.py b/pyiron_ontology/parser.py index 1a28626..c5541ff 100644 --- a/pyiron_ontology/parser.py +++ b/pyiron_ontology/parser.py @@ -6,7 +6,6 @@ from pyiron_workflow.node import Node - class PNS: BASE = Namespace("http://pyiron.org/ontology/") hasNode = BASE["hasNode"]