Repository: linkml/linkml

Issue:
slots in OWL output have unexpected IRIs and are not declared as properties
I'm investigating some problems with the generated OWL for the CCDH model: https://github.com/cancerDHC/ccdhmodel/issues/64

I started by following the [linkml tutorial](https://linkml.io/linkml/intro/tutorial01.html#), and I think I see some of the issues in that simple example as well:

- Create `personinfo.yaml` as in https://github.com/linkml/linkml/blob/main/examples/tutorial/tutorial01/personinfo.yaml
- Run `gen-rdf personinfo.yaml >personinfo.ttl`. Observe the `phone` slot has IRI `<https://w3id.org/linkml/examples/personinfo/person__phone>`.
- Run `gen-owl personinfo.yaml >personinfo.owl.ttl`. Some problems:
  - The `phone` slot has IRI using the main `linkml` namespace: `<https://w3id.org/linkml/phone>`
  - The `phone` slot is not declared as any particular type of OWL property, e.g., `owl:ObjectProperty`
  - The `phone` slot is used in OWL restrictions without having been declared:
     ```
     <https://w3id.org/linkml/examples/personinfo/Person> a owl:Class,
        linkml:ClassDefinition ;
        rdfs:subClassOf [ a owl:Restriction ;
            owl:maxQualifiedCardinality 1 ;
            owl:onClass linkml:String ;
            owl:onProperty linkml:phone ]
     ```

All of this seems to be the cause of parsing failures found by the Java OWL API.

Discussion:

Patch:
diff --git a/linkml/generators/owlgen.py b/linkml/generators/owlgen.py
index 61c79c06..46f5333d 100644
--- a/linkml/generators/owlgen.py
+++ b/linkml/generators/owlgen.py
@@ -9,12 +9,12 @@ 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.namespace import RDFS, SKOS, DCTERMS
 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
+    TypeDefinitionName, SlotDefinition, TypeDefinition, Element, EnumDefinitionName, Definition
 from linkml_runtime.utils.formatutils import camelcase, underscore
 from linkml.utils.generator import Generator, shared_arguments
 from linkml.utils.schemaloader import SchemaLoader
@@ -25,12 +25,25 @@ class ElementDefinition(object):
 
 
 class OwlSchemaGenerator(Generator):
+    """
+    Generates a schema-oriented OWL representation of a LinkML model
+
+    `OWL Generator Docs <https://linkml.io/linkml/generators/owl>`_
+
+    Attributes:
+        type_objects    if True, represent TypeDefinitions as objects; if False, as literals
+        metaclasses     if True, include OWL representations of ClassDefinition, SlotDefinition, etc. Introduces punning
+    """
     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:
+    def __init__(self, schema: Union[str, TextIO, SchemaDefinition], ontology_uri_suffix: str=None,
+                 type_objects=True,
+                 metaclasses=True,
+                 add_ols_annotations=True,
+                 **kwargs) -> None:
         super().__init__(schema, **kwargs)
         self.graph: Optional[Graph] = None
         self.metamodel = SchemaLoader(LOCAL_METAMODEL_YAML_FILE, importmap=kwargs.get('importmap', None),
@@ -42,6 +55,9 @@ class OwlSchemaGenerator(Generator):
         self.emit_prefixes: Set[str] = set()
         self.top_value_uri: Optional[URIRef] = None
         self.ontology_uri_suffix = ontology_uri_suffix
+        self.type_objects = type_objects
+        self.metaclasses = metaclasses
+        self.add_ols_annotations = add_ols_annotations
 
     def visit_schema(self, output: Optional[str] = None, **_):
         owl_id = self.schema.id
@@ -55,14 +71,17 @@ class OwlSchemaGenerator(Generator):
         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)
+        if self.metaclasses:
+            # add the model types; these will be instantiated under each individual visitor node
+            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")))
+        if self.type_objects:
+            # TODO: additional axioms, e.g. String subClassOf hasValue some string
+            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")))
 
     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()
@@ -72,10 +91,20 @@ class OwlSchemaGenerator(Generator):
         else:
             print(data)
 
-    def add_metadata(self, e: SchemaDefinition, uri: URIRef) -> None:
+    def add_metadata(self, e: Definition, uri: URIRef) -> None:
+        """
+        Add generic annotation properties
+
+        :param e: schema element
+        :param uri: URI representation of schema element
+        :return:
+        """
+        # TODO: use metamodel annotations to drive this rather than hardcoding
         if e.aliases is not None:
             for s in e.aliases:
                 self.graph.add((uri, SKOS.altLabel, Literal(s)))
+        if e.title is not None:
+            self.graph.add((uri, DCTERMS.title, Literal(s)))
         if e.mappings is not None:
             for m in e.mappings:
                 m_uri = self.namespaces.uri_for(m)
@@ -120,59 +149,76 @@ class OwlSchemaGenerator(Generator):
                     logging.warning(f'No URI for {m}')
 
     def visit_class(self, cls: ClassDefinition) -> bool:
+        """
+        Each ClassDefinition is represented as an OWL class
+
+        * the OWL Class will instantiate ClassDefinition, if schema.metaclasses is true
+        * the OWL Class will be annotated using the same properties as the source ClassDefinition
+        * induced slots and their ranges added as OWL restrictions; note this will be under the Open World Assumption
+        :param cls:
+        :return:
+        """
+        # To understand how the RDF-level operations here related to the OWL
+        # representation, consult https://www.w3.org/TR/owl2-mapping-to-rdf/
         self.add_mappings(cls)
         cls_uri = self._class_uri(cls.name)
         self.add_metadata(cls, cls_uri)
+        # add declaration
         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')]))
+        if self.metaclasses:
+            # instantiate metaclasses -- introduces punning
+            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))
+        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)))
+        if self.add_ols_annotations:
+            # Add annotations for browser hints. See https://www.ebi.ac.uk/ols/docs/installation-guide
+            if cls.is_a is None:
+                if len(cls.mixins) == 0:
+                    # Any class that is not a mixin and is a root serves as a potential entry point
+                    self.graph.add((self.graph.identifier,
+                                    URIRef('http://purl.obolibrary.org/obo/IAO_0000700'),
+                                    cls_uri))
+        # 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:
@@ -183,49 +229,52 @@ class OwlSchemaGenerator(Generator):
 
         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))))
+            slot = self.schema.slots[sn]
+            slot_node = BNode()
+            self.graph.add((cls_uri, RDFS.subClassOf, slot_node))
+
+            if self._range_is_datatype(slot):
+                cardinality_on = OWL.onDataRange
+            else:
+                cardinality_on = OWL.onClass
+            slot_uri = self.namespaces.uri_for(slot.slot_uri)
+            if slot_uri == 'rdf:type':
+                logging.warning(f'rdflib may have issues serializing rdf:type with turtle serializer')
+            if slot.required:
+                if slot.multivalued:
+                    #  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, slot_uri))
+
+                    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, slot_uri))
+
+                    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:
+                    #    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, slot_uri))
+                    self.graph.add((slot_node, cardinality_on, self._range_uri(slot)))
+            else:
+                if slot.multivalued:
+                    #    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, slot_uri))
+                else:
+                    #    intersectionOf(restriction(slot only type) restriction(slot max 1 type))
+                    self.graph.add((slot_node, RDF.type, OWL.Restriction))
+                    self.graph.add((slot_node, cardinality_on, self._range_uri(slot)))
+                    self.graph.add((slot_node, OWL.maxQualifiedCardinality, Literal(1)))
+                    self.graph.add((slot_node, OWL.onProperty, slot_uri))
 
         return True
 
@@ -233,19 +282,40 @@ class OwlSchemaGenerator(Generator):
     def visit_slot(self, slot_name: str, slot: SlotDefinition) -> None:
         """ Add a slot definition per slot
 
+        Note: visit_slot may be called multiple times for the same slot_uri, as the same slot_uri can be used:
+        * when the schema declares `attributes`
+        * when `slot_usage` induces additional slots
+
         @param slot_name:
         @param slot:
         @return:
         """
-        self.add_mappings(slot)
-        # Note: We use the raw name in OWL and add a subProperty arc
+        # determine if this is a slot that has been induced by slot_usage; if so the meaning of the slot is context-specific
+        # and should not be used for global properties
+        if slot.alias is not None and slot.alias != slot.name and slot.alias in self.schema.slots:
+            logging.debug(f'SKIPPING slot induced by slot_usage: {slot.alias} // {slot.name} // {slot}')
+            return
+
         slot_uri = self._prop_uri(slot.name)
+        #logging.error(f'SLOT_URI={slot_uri}')
+
+        # Slots may be modeled as Object or Datatype Properties
+        # if type_objects is True, then ALL slots are ObjectProperties
+        self.graph.add((slot_uri, RDF.type, self.slot_owl_type(slot)))
+        if self.metaclasses:
+            # add metaclass which this property instantiates -- induces punning
+            self.graph.add((slot_uri, RDF.type,
+                            self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][camelcase('slot definition')]))
+
+        slots_with_same_uri = [s.name for s in self.schema.slots.values() if slot_uri == self._prop_uri(s.name)]
+        if len(slots_with_same_uri) > 1:
+            logging.error(f'Multiple slots with URI: {slot_uri}: {slots_with_same_uri}; consider giving each a unique slot_uri')
+            return
+
+        self.add_mappings(slot)
         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)))
@@ -254,7 +324,9 @@ class OwlSchemaGenerator(Generator):
         if slot.symmetric:
             self.graph.add((slot_uri, RDF.type, OWL.SymmetricProperty))
 
-        # Parent slots
+        # Parent slots. Note that is_a and mixin both map to subPropertyOf,
+        # and are not distinguishable
+        # TODO: consider annotating axiom to indicate if this is is-a or mixin
         if slot.is_a:
             self.graph.add((slot_uri, RDFS.subPropertyOf, self._prop_uri(slot.is_a)))
         for mixin in slot.mixins:
@@ -265,9 +337,14 @@ class OwlSchemaGenerator(Generator):
 
     def visit_type(self, typ: TypeDefinition) -> None:
         type_uri = self._type_uri(typ.name)
+        if not self.type_objects:
+            return False
+        if typ.from_schema == 'https://w3id.org/linkml/types':
+            return
         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')]))
+        if self.metaclasses:
+            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)))
@@ -286,23 +363,29 @@ class OwlSchemaGenerator(Generator):
                 if v is not None and 'owl' in defining_slot.in_subset:
                     ve = v if isinstance(v, list) else [v]
                     for e in ve:
+                        if k == 'name' and isinstance(el, SlotDefinition) and el.alias is not None:
+                            prop_uri = RDFS.label
+                            e = el.alias
+                        else:
+                            prop_uri = URIRef(self.metamodel.namespaces.uri_for(defining_slot.slot_uri))
                         self.graph.add((uri,
-                                        URIRef(self.metamodel.namespaces.uri_for(defining_slot.slot_uri)),
+                                        prop_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_is_datatype(self, slot: SlotDefinition) -> bool:
+        if self.type_objects:
+            return False
+        else:
+            return slot.range in self.schema.types
 
     def _range_uri(self, slot: SlotDefinition) -> URIRef:
         if slot.range in self.schema.types:
-            return self._type_uri(TypeDefinitionName(slot.range))
+            typ = self.schema.types[slot.range]
+            if self.type_objects:
+                return self._type_uri(typ.name)
+            else:
+                return self.namespaces.uri_for(typ.uri)
         elif slot.range in self.schema.enums:
             # TODO: enums fill this in
             return self._enum_uri(EnumDefinitionName(slot.range))
@@ -320,10 +403,10 @@ class OwlSchemaGenerator(Generator):
 
     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)]
+        if p is not None and p.slot_uri is not None:
+            return self.namespaces.uri_for(p.slot_uri)
         else:
-            return URIRef(p.definition_uri)
+            raise Exception(f'No slot_uri for {pn} // {p}')
 
     def _type_uri(self, tn: TypeDefinitionName) -> URIRef:
         t = self.schema.types[tn]
@@ -335,20 +418,57 @@ class OwlSchemaGenerator(Generator):
         self.graph.add((metac_uri, RDF.type, OWL.Class))
         self._add_element_properties(metac_uri, metac)
 
+    def slot_owl_type(self, slot : SlotDefinition) -> URIRef:
+        if self.type_objects:
+            return OWL.ObjectProperty
+        elif slot.range in self.schema.classes:
+            return OWL.ObjectProperty
+        elif slot.range in self.schema.enums:
+            return OWL.ObjectProperty
+        elif slot.range in self.schema.types:
+            return OWL.DatatypeProperty
+        else:
+            raise Exception(f'Unknown range: {slot.range}')
 
+    # @Deprecated
     def is_slot_object_property(self, slot : SlotDefinition) -> bool:
-        return True
+        if self.type_objects:
+            return True
+        elif slot.range in self.schema.classes:
+            return True
+        elif slot.range in self.schema.enums:
+            return True
+        elif slot.range in self.schema.types:
+            return False
+        else:
+            raise Exception(f'Unknown range: {slot.range}')
 
 
 @shared_arguments(OwlSchemaGenerator)
 @click.command()
 @click.option("-o", "--output",
               help="Output file name")
+@click.option("--type-objects/--no-type-objects",
+              default=True,
+              help="If true, will model linkml types as objects, not literals")
+@click.option("--metaclasses/--no-metaclasses",
+              default=True,
+              help="If true, include linkml metamodel classes as metaclasses. Note this introduces punning in OWL-DL")
+@click.option("--add-ols-annotations/--no-add-ols-annotations",
+              default=True,
+              help="If true, auto-include annotations from https://www.ebi.ac.uk/ols/docs/installation-guide")
 @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 """
+    """ Generate an OWL representation of a LinkML model
+
+    Examples:
+
+        gen-owl --no-metaclasses --no-type-objects
+
+    For more info, see: https://linkml.io/linkml/generators/owl
+    """
     print(OwlSchemaGenerator(yamlfile, **kwargs).serialize(**kwargs))
 
 
diff --git a/sphinx/faq/why-linkml.md b/sphinx/faq/why-linkml.md
index dd07d14c..317a0bb2 100644
--- a/sphinx/faq/why-linkml.md
+++ b/sphinx/faq/why-linkml.md
@@ -153,7 +153,97 @@ Full disclosure: LinkML lead developer Harold Solbrig is also one of the authors
 
 ## Why should I use LinkML over SQL DDL?
 
-TODO
+SQL Data Description Language (DDL; or simply "CREATE TABLE"
+statements) is a means of describing the structure of a relational
+database.
+
+If you are using a relational database and have minimal lightweight
+application code that performs direct SQL queries over data, there may
+be no compelling use case for LinkML.
+
+However, most information architectures that use a SQL database also
+involve some alternative representation of the data - for example, a
+JSON representation in an API, or an object representation -- or even
+an RDF representation. It can be challenging to keep these different
+representations in sync. There are frequently excellent products that
+solve "one part" of this mapping problem -- e.g. an ORM (Object
+Relational Mapping) tool. LinkML is intended to allow you to give a
+"bigger picture" view of your model that is as independent as possible
+from underlying storage or exchange technologies, and at the same time
+can be compiled down.
+
+LinkML also allows you to specify URIs and CURIE/URI mappings for each
+element of your schema, which provides a declarative specification of
+how your data should be mapped. For example, if you are trying to
+bring together two schemas (whether via federation or via
+warehousing), if both schemas annotate the same field with the same
+URI then we know these can be merged.
+
+Even if you are not interested in mapping your SQL data model to
+anything else, it can still be a great idea to use LinkML as a data
+definition language for your schema, especially if you have a schema
+with many fields that a domain scientist or stakeholder needs to
+understand.
+
+In our opinion, SQL DDL is not a great language for data
+dictionaries. There is a lack of standard ways to even add comments or
+descriptions to fields, and it can be challenging to introspect
+these. LinkML provides a simple easy to use way to provide rich
+metadata about your fields.
+
+Compare:
+
+```sql
+CREATE TABLE sample (
+  id TEXT PRIMARY KEY,  -- unique sample id
+  individual_id TEXT FOREIGN KEY(person.id),
+  name TEXT,
+  disease TEXT,
+  src TEXT,
+  collec_location TEXT FOREIGN KEY(geoloc.id),
+  ...
+);
+```
+
+with:
+
+```yaml
+classes:
+  Sample:
+    attributes:
+      id:
+        title: identifier
+        identifier: true
+        pattern: "SAMPLEDB:SAM\\d{8}"
+        description: A unique identifier for the sample
+      name:
+        title: sample name
+        description: A human-readable name for the sample
+        range: NarrativeText
+      disease:
+        title: disease
+        description: the disease with which the patient was diagnosed
+        range: DiseaseEnum
+      src:
+        title: tissue source
+        description: the anatomical location from which the tissue was sampled
+        range: DiseaseEnum
+        todos:
+          - model this using an ontology term instead
+      collec_location:
+        title: collection location
+        description: the geocoordinates of the site where the sampling was obtained
+        notes:
+          - this should NOT be the site of processing
+        range: GeoLoc
+```
+
+Here we have a standard way of providing human-readable names for our
+columns (via [title](https://w3id.org/linkml/title)). We have a
+human-friendly textual description. Both of these could be used to
+drive dynamic tooltips in an application that collects or displays data.
+
+
 
 ## Why should I use LinkML over UML?
 
@@ -182,4 +272,7 @@ an example, and see also the
 [linkml-owl](https://github.com/linkml/linkml-owl) framework.
 
 
+## Why should I use LinkML over CSV-on-the-web?
+
+
 
diff --git a/sphinx/generators/owl.rst b/sphinx/generators/owl.rst
index d8d9ef2a..a96b93d9 100644
--- a/sphinx/generators/owl.rst
+++ b/sphinx/generators/owl.rst
@@ -17,12 +17,9 @@ rendering)
 .. seealso:: The `linkml-owl <https://github.com/linkml/linkml-owl>`_
              maps between LinkML *data* and OWL
              
-.. note:: The OWL is rendered as RDF/turte. We recommend the suffix
+.. note:: The OWL is rendered as RDF/turtle. We recommend the suffix
           ``.owl.ttl`` to distinguish from the direct RDF mapping
-             
-.. warning:: Currently imports are included in the rendered OWL, this
-             means if you import linkml/types these will also be
-             rendered, which can be confusing
+
 
 Mapping
 ^^^^^^^
@@ -32,6 +29,7 @@ Mapping
 
     - if the range of the slot is class, then an ObjectProperty is used
     - otherwise DataProperty is used
+    - Exception to the above: if ``type_objects`` is set then ObjectProperties are always used
 
 * OWL *restrictions* are used for cardinality and range constraints
 
@@ -47,8 +45,10 @@ Mapping
 * Each LinkML element is rendered as an instance of the relevant metamodel class      
 
     - This means *punning* is used
-    - In future this behavior may be configurable
-      
+    - Set ``metaclass=False`` if you do not want this behavior
+
+.. note:: The current default settings for ``metaclasses`` and ``type_objects`` may change in the future
+
 Docs
 ----
 


Test patch:
diff --git a/tests/test_issues/input/linkml_issue_384.yaml b/tests/test_issues/input/linkml_issue_384.yaml
new file mode 100644
index 00000000..df584c0b
--- /dev/null
+++ b/tests/test_issues/input/linkml_issue_384.yaml
@@ -0,0 +1,62 @@
+id: https://w3id.org/linkml/examples/personinfo
+name: personinfo
+prefixes:
+  linkml: https://w3id.org/linkml/
+  sdo: http://schema.org/
+imports:
+  - linkml:types
+default_range: string
+
+classes:
+  Thing:
+    slots:
+      - id
+      - full_name
+  Person:
+    is_a: Thing
+    class_uri: sdo:Person
+    attributes:
+      aliases:
+        multivalued: true
+      phone:
+      age:
+        description: age in years
+        range: integer
+    slot_usage:
+      parent:
+        range: Person
+  Organization:
+    is_a: Thing
+    slot_usage:
+      full_name:
+        description: name of the organization, e.g. ACME inc
+      parent:
+        range: Organization
+  GeoObject:
+    is_a: Thing
+    attributes:
+      aliases:
+        multivalued: false
+        comments:
+          - we introduce a deliberate conflict (single vs multivalied) with the aliases attribute that is local to person
+      age:
+        range: GeoAge
+        comments:
+          - we introduce a deliberate conflict (type vs class range) with the age attribute that is local to person
+
+  GeoAge:
+    attributes:
+      unit:
+      value:
+        range: float
+
+
+
+slots:
+  id:
+  full_name:
+    title: full name
+  parent:
+    range: Thing
+    multivalued: true
+
diff --git a/tests/test_issues/input/linkml_issue_387.yaml b/tests/test_issues/input/linkml_issue_387.yaml
new file mode 100644
index 00000000..0b5e96b7
--- /dev/null
+++ b/tests/test_issues/input/linkml_issue_387.yaml
@@ -0,0 +1,18 @@
+id: https://w3id.org/linkml/examples/test
+prefixes:
+  linkml: https://w3id.org/linkml/
+default_range: my_str
+default_curi_maps:
+  - semweb_context
+
+types:
+  my_str:
+    base: str
+    uri: xsd:string
+
+classes:
+  C1:
+    attributes:
+      a:
+
+
diff --git a/tests/test_issues/input/linkml_issue_388.yaml b/tests/test_issues/input/linkml_issue_388.yaml
new file mode 100644
index 00000000..5ac5e0c5
--- /dev/null
+++ b/tests/test_issues/input/linkml_issue_388.yaml
@@ -0,0 +1,35 @@
+id: https://w3id.org/linkml/examples/personinfo
+prefixes:
+  linkml: https://w3id.org/linkml/
+  this: https://example.org/this/
+  other: https://example.org/other/
+default_range: my_str
+default_prefix: this
+
+types:
+  my_str:
+    base: str
+    uri: xsd:string
+  my_int:
+    base: integer
+    uri: xsd:integer
+
+classes:
+  C1:
+    attributes:
+      a:
+        range: my_str
+        description: this-a in the context of C1
+  C2:
+    attributes:
+      a:
+        range: my_int
+        description: this-a in the context of C2
+  C3:
+    attributes:
+      a:
+        slot_uri: other:a
+        description: other-a in the context of C3
+        range: C1
+
+
diff --git a/tests/test_issues/test_linkml_issue_384.py b/tests/test_issues/test_linkml_issue_384.py
new file mode 100644
index 00000000..c222902b
--- /dev/null
+++ b/tests/test_issues/test_linkml_issue_384.py
@@ -0,0 +1,115 @@
+import os
+import unittest
+
+from linkml_runtime.linkml_model import String
+from rdflib import URIRef, Graph
+from rdflib.namespace import OWL, RDFS, RDF, XSD
+
+from linkml import METAMODEL_CONTEXT_URI
+from linkml.generators.jsonldcontextgen import ContextGenerator
+from linkml.generators.owlgen import OwlSchemaGenerator
+from linkml.generators.pythongen import PythonGenerator
+from linkml.generators.rdfgen import RDFGenerator
+from linkml.generators.yamlgen import YAMLGenerator
+from tests.utils.compare_rdf import compare_rdf
+from tests.utils.test_environment import TestEnvironmentTestCase
+from tests.test_issues.environment import env
+
+TESTFILE = 'linkml_issue_384'
+
+class IssueOWLNamespaceTestCase(TestEnvironmentTestCase):
+    """
+    Tests: https://github.com/linkml/linkml/issues/384
+    """
+    env = env
+
+
+
+    def _test_other(self, name: str) -> None:
+        infile = env.input_path(f'{name}.yaml')
+        self.env.generate_single_file(f'{name}.yaml',
+                                      lambda: YAMLGenerator(env.input_path(f'{name}.yaml'),
+                                                            importmap=env.import_map).serialize(),
+                                      value_is_returned=True)
+        self.env.generate_single_file(f'{name}.context.jsonld',
+                                      lambda: ContextGenerator(env.input_path(f'{name}.yaml'),
+                                                               importmap=env.import_map).serialize(),
+                                      value_is_returned=True)
+        #print(f'Loading: {infile}')
+        #rdfstr = RDFGenerator(infile, context=[METAMODEL_CONTEXT_URI]).serialize(context=[METAMODEL_CONTEXT_URI])
+        #print(rdfstr)
+        self.env.generate_single_file(f'{name}.ttl',
+                                      lambda: RDFGenerator(infile,
+                                                           importmap=env.import_map,
+                                                           ).serialize(context=[METAMODEL_CONTEXT_URI]),
+                                      value_is_returned=True, comparator=compare_rdf)
+        self.env.generate_single_file(f'{name}.py',
+                                      lambda: PythonGenerator(infile,
+                                                              importmap=env.import_map,
+                                                              ).serialize(),
+                                      value_is_returned=True)
+
+    def _test_owl(self, name: str, metaclasses=False, type_objects=False) -> Graph:
+        infile = env.input_path(f'{name}.yaml')
+        outpath = f'{name}-{metaclasses}-{type_objects}.owl'
+        self.env.generate_single_file(outpath,
+                                      lambda: OwlSchemaGenerator(infile,
+                                                                 mergeimports=False,
+                                                                 add_ols_annotations=True,
+                                                                 metaclasses=metaclasses,
+                                                                 type_objects=type_objects,
+                                                                 importmap=env.import_map).serialize(mergeimports=False),
+                                      value_is_returned=True,
+                                      comparator=compare_rdf)
+        g = Graph()
+        g.parse(env.expected_path(outpath), format="turtle")
+        return g
+
+    def _contains_restriction(self, g: Graph, c: URIRef, prop: URIRef, pred: URIRef, filler: URIRef) -> bool:
+        for r in g.objects(c, RDFS.subClassOf):
+            if prop in g.objects(r, OWL.onProperty):
+                if filler in g.objects(r, pred):
+                    return True
+        return False
+
+    def test_issue_owl_properties(self):
+        def uri(s) -> URIRef:
+            return URIRef(f'https://w3id.org/linkml/examples/personinfo/{s}')
+        for conf in [dict(metaclasses=False, type_objects=False),
+                     dict(metaclasses=True, type_objects=True),
+                     ]:
+            g = self._test_owl(TESTFILE, **conf)
+            Thing = uri('Thing')
+            Person = uri('Person')
+            Organization = uri('Organization')
+            parent = uri('parent')
+            age = uri('age')
+            aliases = uri('aliases')
+            full_name = uri('full_name')
+            classes = [Thing, Person, Organization]
+            props = [parent, age]
+            # if type_objects=True then the range of slots that are types will be mapped to Object
+            # representations of literals
+            if conf['type_objects']:
+                string_rep = URIRef(String.type_model_uri)
+            else:
+                string_rep = XSD.string
+            for c in classes:
+                self.assertIn((c, RDF.type, OWL.Class), g)
+            for p in props:
+                self.assertIn((p, RDF.type, OWL.ObjectProperty), g)
+            assert self._contains_restriction(g, Person, parent, OWL.allValuesFrom, Person)
+            assert self._contains_restriction(g, Organization, parent, OWL.allValuesFrom, Organization)
+            assert self._contains_restriction(g, Person, aliases, OWL.allValuesFrom, string_rep)
+            # TODO: also validate cardinality restrictions
+            #assert self._contains_restriction(g, Thing, full_name, OWL.allValuesFrom, string_rep)
+
+        #self.assertIn((A, RDF.type, OWL.Class), g)
+        #NAME = URIRef('http://example.org/name')
+        #self.assertIn((NAME, RDF.type, OWL.ObjectProperty), g)
+
+    def test_other_formats(self):
+        self._test_other(TESTFILE)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_issues/test_linkml_issue_387.py b/tests/test_issues/test_linkml_issue_387.py
new file mode 100644
index 00000000..a4503e60
--- /dev/null
+++ b/tests/test_issues/test_linkml_issue_387.py
@@ -0,0 +1,58 @@
+import os
+import unittest
+
+from rdflib import URIRef, Graph, Literal
+from rdflib.namespace import OWL, RDFS, RDF, XSD
+
+from linkml import METAMODEL_CONTEXT_URI
+from linkml.generators.jsonldcontextgen import ContextGenerator
+from linkml.generators.owlgen import OwlSchemaGenerator
+from linkml.generators.rdfgen import RDFGenerator
+from linkml.generators.yamlgen import YAMLGenerator
+from tests.utils.compare_rdf import compare_rdf
+from tests.utils.test_environment import TestEnvironmentTestCase
+from tests.test_issues.environment import env
+
+class IssueOWLNamespaceTestCase(TestEnvironmentTestCase):
+    """
+    Tests: https://github.com/linkml/linkml/issues/387
+
+    Ensure attributes have correct names in OWL
+    """
+    env = env
+
+
+    def test_name_mangling(self):
+        infile = env.input_path('linkml_issue_387.yaml')
+        outpath = env.expected_path('linkml_issue_387.owl')
+        gen = OwlSchemaGenerator(infile,
+                                 mergeimports=False,
+                                 metaclasses=False,
+                                 type_objects=False,
+                                 importmap=env.import_map)
+        self.env.generate_single_file(outpath,
+                                      lambda: gen.serialize(mergeimports=False),
+                                      value_is_returned=True,
+                                      comparator=compare_rdf)
+        g = Graph()
+        g.parse(env.expected_path(outpath), format="turtle")
+        def uri(s) -> URIRef:
+            return URIRef(f'https://w3id.org/linkml/examples/test/{s}')
+        C1 = uri('C1')
+        a = uri('a')
+        self.assertIn((C1, RDFS.label, Literal("C1")), g)
+        self.assertIn((C1, RDF.type, OWL.Class), g)
+        self.assertIn((a, RDFS.label, Literal("a")), g)
+        self.assertIn((a, RDFS.range, XSD.string), g)
+        self.assertIn((a, RDF.type, OWL.DatatypeProperty), g)
+        assert len(list(g.objects(a, RDF.type))) == 1
+        assert len(list(g.objects(C1, RDF.type))) == 1
+        schema = gen.schema
+        my_str = schema.types['my_str']
+        self.assertEqual(my_str.uri, 'xsd:string')
+        self.assertEqual(my_str.definition_uri, 'https://w3id.org/linkml/examples/test/MyStr')
+
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_issues/test_linkml_issue_388.py b/tests/test_issues/test_linkml_issue_388.py
new file mode 100644
index 00000000..d55918d3
--- /dev/null
+++ b/tests/test_issues/test_linkml_issue_388.py
@@ -0,0 +1,61 @@
+import os
+import unittest
+
+from rdflib import URIRef, Graph
+from rdflib.namespace import OWL, RDFS, RDF
+
+from linkml import METAMODEL_CONTEXT_URI
+from linkml.generators.jsonldcontextgen import ContextGenerator
+from linkml.generators.jsonschemagen import JsonSchemaGenerator
+from linkml.generators.owlgen import OwlSchemaGenerator
+from linkml.generators.rdfgen import RDFGenerator
+from linkml.generators.yamlgen import YAMLGenerator
+from tests.utils.compare_rdf import compare_rdf
+from tests.utils.test_environment import TestEnvironmentTestCase
+from tests.test_issues.environment import env
+
+class IssueOWLNamespaceTestCase(TestEnvironmentTestCase):
+    """
+    Tests: https://github.com/linkml/linkml/issues/388
+
+    Note: this test is currently for exploration, it does not yet do tests beyond ensuring conversion
+    generates no errors
+    """
+    env = env
+
+
+    def test_attribute_behavior(self):
+        name = 'linkml_issue_388'
+        infile = env.input_path(f'{name}.yaml')
+        self.env.generate_single_file(f'{name}.yaml',
+                                      lambda: YAMLGenerator(env.input_path(f'{name}.yaml'),
+                                                            importmap=env.import_map).serialize(),
+                                      value_is_returned=True)
+        self.env.generate_single_file(f'{name}.schema.json',
+                                      lambda: JsonSchemaGenerator(env.input_path(f'{name}.yaml'),
+                                                            importmap=env.import_map).serialize(),
+                                      value_is_returned=True)
+
+        self.env.generate_single_file(f'{name}.ttl',
+                                      lambda: RDFGenerator(env.input_path(f'{name}.yaml'),
+
+                                                               importmap=env.import_map).serialize(context=[METAMODEL_CONTEXT_URI],),
+                                      value_is_returned=True)
+        self.env.generate_single_file(f'{name}.owl',
+                                      lambda: OwlSchemaGenerator(env.input_path(f'{name}.yaml'),
+                                                                 metaclasses=False,
+                                                                 ).serialize(context=[METAMODEL_CONTEXT_URI]),
+                                      value_is_returned=True)
+        g = Graph()
+        g.parse(env.expected_path(f'{name}.owl'), format='turtle')
+        C1 = URIRef('https://example.org/this/C1')
+        this_a = URIRef('https://example.org/this/a')
+        other_a = URIRef('https://example.org/other/a')
+        # slot_uri refers to two attributes, ambiguous, so minimal metadata;
+        assert len(list(g.triples((this_a, None, None)))) == 1
+        self.assertIn((other_a, RDFS.range, C1), g)
+
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_prefixes/input/prefixtest.yaml b/tests/test_prefixes/input/prefixtest.yaml
index 5fcf384b..358c4540 100644
--- a/tests/test_prefixes/input/prefixtest.yaml
+++ b/tests/test_prefixes/input/prefixtest.yaml
@@ -21,6 +21,7 @@ prefixes:
   SIO: 'http://semanticscience.org/resource/SIO_'
   wd: https://www.wikidata.org/wiki/
   dbont: http://dbpedia.org/ontology/
+  rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns#
 default_prefix: prefixtest
 see_also:
   - https://example.org/
diff --git a/tests/test_prefixes/test_prefixes.py b/tests/test_prefixes/test_prefixes.py
index 743f620c..5c2de72f 100644
--- a/tests/test_prefixes/test_prefixes.py
+++ b/tests/test_prefixes/test_prefixes.py
@@ -20,11 +20,13 @@ CONTEXT_OUTPUT = env.expected_path('prefixtest.context.jsonld')
 class PrefixTestCase(unittest.TestCase):
     def test_owlgen(self):
         """ owl  """
-        owl = OwlSchemaGenerator(SCHEMA, mergeimports=False, ontology_uri_suffix='.owl.ttl').serialize()
+        owl = OwlSchemaGenerator(SCHEMA, mergeimports=False, ontology_uri_suffix='.owl.ttl', format='nt').serialize()
         with open(OWL_OUTPUT, 'w') as stream:
             stream.write(owl)
         g = Graph()
-        g.parse(OWL_OUTPUT, format="turtle")
+        # TODO: test with turtle when https://github.com/linkml/linkml/issues/163#issuecomment-906507968 is resolved
+        #g.parse(OWL_OUTPUT, format="turtle")
+        g.parse(OWL_OUTPUT, format="nt")
         # TODO: fix owlgen such that we don't have to hardcode exceptions
         self._check_triples(g,exceptions=[
             'http://schema.org/additionalName',