Skip to content
Open
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
93 changes: 48 additions & 45 deletions bin/soml2owl.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import yaml
import argparse
from rdflib import Graph, Namespace, RDF, RDFS, OWL, XSD, Literal, BNode

# Recursive creation of rdf:parseType = "Collection"
def collection_from_list(g, *rest):
node_union = BNode()
g.add((node_union, RDF.first, rest[0]))
Expand All @@ -11,51 +11,45 @@ def collection_from_list(g, *rest):
g.add((node_union, RDF.rest, RDF.nil))
return node_union

# Load SOML schema
def load_soml_schema(file_path):
with open(file_path, "r", encoding="utf-8") as file:
return yaml.safe_load(file)

# Initialize RDF graph
def initialize_graph():
def initialize_graph(base):
g = Graph()
bsdd = Namespace("http://bsdd.buildingsmart.org/def#")
base_ont = Namespace(base)
schema = Namespace("https://schema.org/")
g.bind("bsdd", bsdd)
g.bind("base_ont", base_ont)
g.bind("schema", schema)
g.bind("owl", OWL)
g.bind("rdfs", RDFS)
g.bind("xsd", XSD)
return g, bsdd, schema
return g, base_ont, schema

# Handle unionOf for properties with multiple class ranges
def merge_object_property_ranges(graph):
updated_graph = Graph()
updated_graph += graph

# Find all owl:ObjectProperty with multiple rdfs:range values
properties_to_update = {}
for s, p, o in graph.triples((None, RDF.type, OWL.ObjectProperty)):
for s, _, _ in graph.triples((None, RDF.type, OWL.ObjectProperty)):
ranges = list(graph.objects(s, RDFS.range))
if len(ranges) > 1:
properties_to_update[s] = ranges

# Remove old range declarations and replace with unionOf restriction
for prop, ranges in properties_to_update.items():
for r in ranges:
updated_graph.remove((prop, RDFS.range, r))

# Create an owl:unionOf restriction

union_bnode = BNode()
restriction_bnode = BNode()
updated_graph.add((restriction_bnode, RDF.type, OWL.Restriction))
updated_graph.add((prop, RDFS.range, restriction_bnode))
updated_graph.add((restriction_bnode, OWL.onProperty, prop))
updated_graph.add((restriction_bnode, OWL.unionOf, collection_from_list(updated_graph, *ranges)))

updated_graph.add((prop, RDFS.range, restriction_bnode))

return updated_graph

# Mapping of SOML types to RDF types
def get_rdf_type(soml_type, bsdd):
def get_rdf_type(soml_type, base_ont):
xsd_mapping = {
"string": XSD.string,
"int": XSD.integer,
Expand All @@ -66,63 +60,72 @@ def get_rdf_type(soml_type, bsdd):
}
if soml_type in xsd_mapping:
return xsd_mapping[soml_type]
return bsdd[soml_type] if soml_type[0].isupper() else XSD.string
return base_ont[soml_type] if soml_type[0].isupper() else XSD.string

# Convert types to OWL enumerations
def convert_types(g, bsdd, types):
def convert_types(g, base_ont, types):
for type_name, type_data in types.items():
type_uri = bsdd[type_name]
type_uri = base_ont[type_name]
g.add((type_uri, RDF.type, RDFS.Datatype))
list_items = [Literal(v["name"]) for v in type_data["values"]]
list_bnode = BNode()

equivalent_class_bnode = BNode()
g.add((type_uri, OWL.equivalentClass, equivalent_class_bnode))
g.add((equivalent_class_bnode, RDF.type, RDFS.Datatype))
g.add((equivalent_class_bnode, OWL.oneOf, collection_from_list(g, *list_items)))

# Convert objects to OWL classes and properties
def convert_objects(g, bsdd, schema, objects):
def convert_objects(g, base_ont, schema, objects):
for obj_name, obj_data in objects.items():
obj_uri = bsdd[obj_name]
obj_uri = base_ont[obj_name]
g.add((obj_uri, RDF.type, OWL.Class))
g.add((obj_uri, RDFS.comment, Literal(obj_data["label"])))
for prop_name, prop_data in obj_data.get("props", {}).items():
prop_uri = bsdd[prop_name]
prop_range = get_rdf_type(prop_data.get("range", "string"), bsdd)
is_object_property = isinstance(prop_range, (str, Namespace)) and str(prop_range).startswith(str(bsdd))
prop_uri = base_ont[prop_name]
prop_range = get_rdf_type(prop_data.get("range", "string"), base_ont)
is_object_property = isinstance(prop_range, (str, Namespace)) and str(prop_range).startswith(str(base_ont))
prop_type = OWL.ObjectProperty if is_object_property else OWL.DatatypeProperty
g.add((prop_uri, RDF.type, prop_type))
g.add((prop_uri, RDFS.comment, Literal(prop_data["label"])))
g.add((prop_uri, RDFS.range, prop_range))
g.add((prop_uri, schema.domainIncludes, obj_uri))

# Apply cardinality and range restrictions at the class level

# Add owl:inverseOf if inverseAlias exists
if "inverseAlias" in prop_data:
inverse_prop_uri = base_ont[prop_data["inverseAlias"]]
g.add((prop_uri, OWL.inverseOf, inverse_prop_uri))

for prop_name, prop_data in obj_data.get("props", {}).items():
restriction_bnode = BNode()
g.add((restriction_bnode, RDF.type, OWL.Restriction))
g.add((restriction_bnode, OWL.onProperty, bsdd[prop_name]))
prop_range = get_rdf_type(prop_data.get("range", "string"), bsdd)
g.add((restriction_bnode, OWL.onProperty, base_ont[prop_name]))

prop_range = get_rdf_type(prop_data.get("range", "string"), base_ont)
if "min" in prop_data and prop_data["min"] >= 1:
g.add((restriction_bnode, OWL.someValuesFrom, prop_range))
g.add((restriction_bnode, OWL.minCardinality, Literal(prop_data["min"], datatype=XSD.integer)))
if "max" in prop_data and prop_data["max"] != "inf":
g.add((restriction_bnode, OWL.maxCardinality, Literal(prop_data["max"], datatype=XSD.integer)))
else:
g.add((restriction_bnode, OWL.allValuesFrom, prop_range))

g.add((obj_uri, RDFS.subClassOf, restriction_bnode))

# Convert SOML schema to RDF graph
def convert_soml_to_turtle(soml_schema):
g, bsdd, schema = initialize_graph()
convert_types(g, bsdd, soml_schema["types"])
convert_objects(g, bsdd, schema, soml_schema["objects"])
def convert_soml_to_turtle(soml_schema, base):
g, base_ont, schema = initialize_graph(base)
convert_types(g, base_ont, soml_schema["types"])
convert_objects(g, base_ont, schema, soml_schema["objects"])
g = merge_object_property_ranges(g)
return g.serialize(format="turtle", base="http://bsdd.buildingsmart.org/def#")
return g.serialize(format="turtle", base=base)

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert SOML YAML schema to OWL Turtle ontology.")
parser.add_argument("-i", "--input", required=True, help="Path to the input YAML file")
parser.add_argument("-o", "--output", required=True, help="Path to the output TTL file")
parser.add_argument("-b", "--base", required=True, help="Base URI for the ontology")
args = parser.parse_args()

soml_schema = load_soml_schema(args.input)
turtle_output = convert_soml_to_turtle(soml_schema, args.base)

with open(args.output, "w", encoding="utf-8") as f:
f.write(turtle_output)

# Load schema and convert to Turtle
soml_schema = load_soml_schema("bsdd-graphql-soml-refact.yaml")
turtle_output = convert_soml_to_turtle(soml_schema)
print(turtle_output)
print(f"Conversion complete. RDF Turtle saved to: {args.output}")
7 changes: 5 additions & 2 deletions soml2owl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ Acknowledgement: this work was done by Nataliya Keberle (@nataschake) as part of
that has received funding from the European Union's Horizon Europe research and innovation programme under grant agreement no. 101056973.

Features
- TODO
- SOML `range` converts to `owl:allValuesFrom` restriction
- SOML `min>=1` converts to `owl:minCardinality`/`owl:someValuesFrom` pair of restrictions
- SOML `max<inf` converts to `owl:maxCardinality` restriction
- SOML `inverseAlias` converts to `owl:inverseOf` restriction

Usage:
```
python soml2owl.py bsdd-soml.yaml > bsdd-ontology.ttl
python soml2owl.py -i bsdd-soml.yaml -o bsdd-ontology.ttl -b "http://bsdd.buildingsmart.org/def#"
```
Loading