"""Generate OWL ontology corresponding to information model
model classes are translated to OWL classes, slots to OWL properties.
"""
import os
from typing import Union, TextIO, Optional, cast
import logging
import click
from rdflib import Graph, URIRef, RDF, OWL, Literal, BNode
from rdflib.collection import Collection
from rdflib.namespace import RDFS, SKOS
from rdflib.plugin import plugins as rdflib_plugins, Parser as rdflib_Parser
from linkml import LOCAL_METAMODEL_YAML_FILE, METAMODEL_NAMESPACE_NAME, METAMODEL_NAMESPACE, METAMODEL_YAML_URI, META_BASE_URI
from linkml_runtime.linkml_model.meta import ClassDefinitionName, SchemaDefinition, ClassDefinition, SlotDefinitionName, \
TypeDefinitionName, SlotDefinition, TypeDefinition, Element, EnumDefinitionName
from linkml_runtime.utils.formatutils import camelcase, underscore
from linkml.utils.generator import Generator, shared_arguments
from linkml.utils.schemaloader import SchemaLoader
[docs]class ElementDefinition(object):
pass
[docs]class OwlSchemaGenerator(Generator):
generatorname = os.path.basename(__file__)
generatorversion = "0.1.1"
valid_formats = ['owl', 'ttl'] + [x.name for x in rdflib_plugins(None, rdflib_Parser) if '/' not in str(x.name)]
visits_are_sorted = True
def __init__(self, schema: Union[str, TextIO, SchemaDefinition], ontology_uri_suffix: str=None, **kwargs) -> None:
super().__init__(schema, **kwargs)
self.graph: Optional[Graph] = None
self.metamodel = SchemaLoader(LOCAL_METAMODEL_YAML_FILE, importmap=kwargs.get('importmap', None),
mergeimports=self.merge_imports) \
if os.path.exists(LOCAL_METAMODEL_YAML_FILE) else\
SchemaLoader(METAMODEL_YAML_URI, base_dir=META_BASE_URI, importmap=kwargs.get('importmap', None),
mergeimports=self.merge_imports)
self.metamodel.resolve()
self.emit_prefixes: Set[str] = set()
self.top_value_uri: Optional[URIRef] = None
self.ontology_uri_suffix = ontology_uri_suffix
[docs] def visit_schema(self, output: Optional[str] = None, **_):
owl_id = self.schema.id
if self.ontology_uri_suffix:
owl_id = f'{owl_id}{self.ontology_uri_suffix}'
base = URIRef(owl_id)
self.graph = Graph(identifier=base)
for prefix in self.metamodel.schema.emit_prefixes:
self.graph.bind(prefix, self.metamodel.namespaces[prefix])
self.graph.add((base, RDF.type, OWL.Ontology))
self._add_element_properties(base, self.schema)
# add the model types
for name in ['class_definition', 'type_definition', 'slot_definition', 'subset_definition']:
self._add_metamodel_class(name)
# add value placeholder
self.top_value_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]['topValue']
self.graph.add((self.top_value_uri, RDF.type, OWL.DatatypeProperty))
self.graph.add((self.top_value_uri, RDFS.label, Literal("value")))
[docs] def end_schema(self, output: Optional[str] = None, **_) -> None:
data = self.graph.serialize(format='turtle' if self.format in ['owl', 'ttl'] else self.format).decode()
if output:
with open(output, 'w') as outf:
outf.write(data)
else:
print(data)
[docs] def visit_class(self, cls: ClassDefinition) -> bool:
self.add_mappings(cls)
cls_uri = self._class_uri(cls.name)
self.add_metadata(cls, cls_uri)
self.graph.add((cls_uri, RDF.type, OWL.Class))
self.graph.add((cls_uri, RDF.type,
self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase('class definition')]))
self._add_element_properties(cls_uri, cls)
# Parent classes
# TODO: reintroduce this
# if not cls.defining_slots:
if True:
if cls.is_a:
self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(cls.is_a)))
if cls.mixin:
self.graph.add((cls_uri, RDFS.subClassOf, METAMODEL_NAMESPACE.mixin))
for mixin in sorted(cls.mixins):
self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(mixin)))
if cls.name in self.synopsis.applytorefs:
for appl in sorted(self.synopsis.applytorefs[cls.name].classrefs):
self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(appl)))
else:
raise NotImplementedError("Defining slots need to be implemented")
# If defining slots, we generate an equivalentClass entry
# equ_node = BNode()
# self.graph.add((cls_uri, OWL.equivalentClass, equ_node))
# self.graph.add((equ_node, RDF.type, OWL.Class))
#
# elts = []
# if cls.is_a:
# elts.append(self._class_uri(cls.is_a))
# if cls.mixin:
# self.graph.add((cls_uri, RDFS.subClassOf, META_NS.mixin))
# for mixin in cls.mixins:
# self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(mixin)))
# if cls.name in self.synopsis.applytorefs:
# for appl in self.synopsis.applytorefs[cls.name].classrefs:
# self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(appl)))
#
# for slotname in cls.defining_slots:
# restr_node = BNode()
# slot = self.schema.slots[slotname]
#
# self.graph.add((restr_node, RDF.type, OWL.Restriction))
# self.graph.add((restr_node, OWL.onProperty, self._prop_uri(slotname)))
# self._add_cardinality(restr_node, slot)
# # TODO: fix this
# # self.graph.add((restr_node, OWL.someValuesFrom, self._build_range(slot)))
# elts.append(restr_node)
#
# coll_bnode = BNode()
# Collection(self.graph, coll_bnode, elts)
# self.graph.add((equ_node, OWL.intersectionOf, coll_bnode))
# TODO: see whether unions belong
# if cls.union_of:
# union_node = BNode()
# Collection(self.graph, union_coll, [self.class_uri(union_node) for union_node in cls.union_of])
# self.graph.add((union_node, OWL.unionOf, union_coll))
# self.graph.add((cls_uri, RDFS.subClassOf, union_node))
for sn in sorted(self.own_slot_names(cls)):
# Defining_slots are covered above
if sn not in cls.defining_slots:
slot = self.schema.slots[sn]
# Non-inherited slots are annotation properties
if self.is_slot_object_property(slot):
slot_node = BNode()
self.graph.add((cls_uri, RDFS.subClassOf, slot_node))
# required multivalued
if slot.required:
if slot.multivalued:
# y y intersectionOf(restriction(slot only type) restriction(slot some type)
restr1 = BNode()
self.graph.add((restr1, RDF.type, OWL.Restriction))
self.graph.add((restr1, OWL.allValuesFrom, self._range_uri(slot)))
self.graph.add((restr1, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot))))
restr2 = BNode()
self.graph.add((restr2, RDF.type, OWL.Restriction))
self.graph.add((restr2, OWL.someValuesFrom, self._range_uri(slot)))
self.graph.add((restr2, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot))))
coll_bnode = BNode()
Collection(self.graph, coll_bnode, [restr1, restr2])
self.graph.add((slot_node, OWL.intersectionOf, coll_bnode))
self.graph.add((slot_node, RDF.type, OWL.Class))
else:
# y n restriction(slot exactly 1 type)
self.graph.add((slot_node, RDF.type, OWL.Restriction))
self.graph.add((slot_node, OWL.qualifiedCardinality, Literal(1)))
self.graph.add((slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot))))
self.graph.add((slot_node, OWL.onClass, self._range_uri(slot)))
else:
if slot.multivalued:
# n y restriction(slot only type)
self.graph.add((slot_node, RDF.type, OWL.Restriction))
self.graph.add((slot_node, OWL.allValuesFrom, self._range_uri(slot)))
self.graph.add((slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot))))
else:
# n n intersectionOf(restriction(slot only type) restriction(slot max 1 type))
self.graph.add((slot_node, RDF.type, OWL.Restriction))
self.graph.add((slot_node, OWL.onClass, self._range_uri(slot)))
self.graph.add((slot_node, OWL.maxQualifiedCardinality, Literal(1)))
self.graph.add((slot_node, OWL.onProperty, self._prop_uri(self.aliased_slot_name(slot))))
return True
[docs] def visit_slot(self, slot_name: str, slot: SlotDefinition) -> None:
""" Add a slot definition per slot
@param slot_name:
@param slot:
@return:
"""
self.add_mappings(slot)
# Note: We use the raw name in OWL and add a subProperty arc
slot_uri = self._prop_uri(slot.name)
self._add_element_properties(slot_uri, slot)
# Inherited slots are object or data properties
# All others are annotation properties
self.graph.add((slot_uri, RDF.type, OWL.ObjectProperty if self.is_slot_object_property(slot) else OWL.AnnotationProperty))
self.graph.add((slot_uri, RDF.type,
self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase('slot definition')]))
self.graph.add((slot_uri, RDFS.range, self._range_uri(slot)))
if slot.domain:
self.graph.add((slot_uri, RDFS.domain, self._class_uri(slot.domain)))
if slot.inverse:
self.graph.add((slot_uri, OWL.inverseOf, self._prop_uri(slot.inverse)))
if slot.symmetric:
self.graph.add((slot_uri, RDF.type, OWL.SymmetricProperty))
# Parent slots
if slot.is_a:
self.graph.add((slot_uri, RDFS.subPropertyOf, self._prop_uri(slot.is_a)))
for mixin in slot.mixins:
self.graph.add((slot_uri, RDFS.subPropertyOf, self._prop_uri(mixin)))
if slot.name in self.synopsis.applytorefs:
for appl in self.synopsis.applytorefs[slot.name].slotrefs:
self.graph.add((slot_uri, RDFS.subClassOf, self._prop_uri(appl)))
[docs] def visit_type(self, typ: TypeDefinition) -> None:
type_uri = self._type_uri(typ.name)
self.graph.add((type_uri, RDF.type, OWL.Class))
self.graph.add((type_uri, RDF.type,
self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase('type definition')]))
self._add_element_properties(type_uri, typ)
if typ.typeof:
self.graph.add((type_uri, RDFS.subClassOf, self._type_uri(typ.typeof)))
else:
restr = BNode()
self.graph.add((restr, RDF.type, OWL.Restriction))
self.graph.add((restr, OWL.qualifiedCardinality, Literal(1)))
self.graph.add((restr, OWL.onProperty, self.top_value_uri))
self.graph.add((restr, OWL.onDataRange, self.namespaces.uri_for(typ.uri)))
self.graph.add((type_uri, RDFS.subClassOf, restr))
def _add_element_properties(self, uri: URIRef, el: Element) -> None:
for k, v in el.__dict__.items():
if k in self.metamodel.schema.slots:
defining_slot = self.metamodel.schema.slots[k]
if v is not None and 'owl' in defining_slot.in_subset:
ve = v if isinstance(v, list) else [v]
for e in ve:
self.graph.add((uri,
URIRef(self.metamodel.namespaces.uri_for(defining_slot.slot_uri)),
Literal(e)))
def _add_cardinality(self, subj: Union[BNode, URIRef], slot) -> None:
""" Add cardinality restrictions to node """
if slot.required:
if slot.multivalued:
self.graph.add((subj, OWL.minCardinality, Literal(1)))
else:
self.graph.add((subj, OWL.cardinality, Literal(1)))
elif not slot.multivalued:
self.graph.add((subj, OWL.maxCardinality, Literal(1)))
def _range_uri(self, slot: SlotDefinition) -> URIRef:
if slot.range in self.schema.types:
return self._type_uri(TypeDefinitionName(slot.range))
elif slot.range in self.schema.enums:
# TODO: enums fill this in
return self._enum_uri(EnumDefinitionName(slot.range))
else:
return self._class_uri(ClassDefinitionName(slot.range))
def _class_uri(self, cn: ClassDefinitionName) -> URIRef:
c = self.schema.classes[cn]
return URIRef(c.definition_uri)
def _enum_uri(self, en: EnumDefinitionName) -> URIRef:
# TODO: enums
e = self.schema.enums[en]
return URIRef(f"http://UNKNOWN.org/{en}")
def _prop_uri(self, pn: SlotDefinitionName) -> URIRef:
p = self.schema.slots.get(pn, None)
if p is None or p.definition_uri is None:
return self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][underscore(pn)]
else:
return URIRef(p.definition_uri)
def _type_uri(self, tn: TypeDefinitionName) -> URIRef:
t = self.schema.types[tn]
return URIRef(t.definition_uri)
def _add_metamodel_class(self, cname: str) -> None:
metac = self.metamodel.schema.classes[cname]
metac_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase(metac.name)]
self.graph.add((metac_uri, RDF.type, OWL.Class))
self._add_element_properties(metac_uri, metac)
[docs] def is_slot_object_property(self, slot : SlotDefinition) -> bool:
return True
@shared_arguments(OwlSchemaGenerator)
@click.command()
@click.option("-o", "--output",
help="Output file name")
@click.option("--ontology-iri-suffix",
default='.owl.ttl',
help="Suffix to append to schema id to generate OWL Ontology IRI")
def cli(yamlfile, **kwargs):
""" Generate an OWL representation of a LinkML model """
print(OwlSchemaGenerator(yamlfile, **kwargs).serialize(**kwargs))
if __name__ == '__main__':
cli()