linkml 1.5.8__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,39 +1,41 @@
1
- """Generate OWL ontology corresponding to information model
2
-
3
- model classes are translated to OWL classes, slots to OWL properties.
4
- """
1
+ """Generate OWL ontology representation of a LinkML schema."""
5
2
  import logging
6
3
  import os
4
+ from collections import defaultdict
7
5
  from dataclasses import dataclass, field
8
6
  from enum import Enum, unique
9
- from typing import Optional, TextIO, Union
7
+ from typing import List, Mapping, Optional, Set, Tuple, Union
10
8
 
11
9
  import click
10
+ from linkml_runtime import SchemaView
12
11
  from linkml_runtime.linkml_model.meta import (
12
+ AnonymousClassExpression,
13
+ AnonymousSlotExpression,
14
+ AnonymousTypeExpression,
13
15
  ClassDefinition,
14
16
  ClassDefinitionName,
15
17
  Definition,
16
- Element,
17
- ElementName,
18
18
  EnumDefinition,
19
19
  EnumDefinitionName,
20
- SchemaDefinition,
21
20
  SlotDefinition,
22
21
  SlotDefinitionName,
23
22
  TypeDefinition,
24
23
  TypeDefinitionName,
25
24
  )
26
25
  from linkml_runtime.utils.formatutils import camelcase, underscore
27
- from rdflib import OWL, RDF, BNode, Graph, Literal, URIRef
26
+ from linkml_runtime.utils.introspection import package_schemaview
27
+ from rdflib import OWL, RDF, XSD, BNode, Graph, Literal, URIRef
28
28
  from rdflib.collection import Collection
29
- from rdflib.namespace import DCTERMS, RDFS, SKOS
29
+ from rdflib.namespace import RDFS, SKOS
30
30
  from rdflib.plugin import Parser as rdflib_Parser
31
31
  from rdflib.plugin import plugins as rdflib_plugins
32
32
 
33
- from linkml import METAMODEL_NAMESPACE, METAMODEL_NAMESPACE_NAME
33
+ from linkml import METAMODEL_NAMESPACE_NAME
34
34
  from linkml._version import __version__
35
35
  from linkml.utils.generator import Generator, shared_arguments
36
36
 
37
+ OWL_TYPE = URIRef ## RDFS.Literal or OWL.Thing
38
+
37
39
 
38
40
  @unique
39
41
  class MetadataProfile(Enum):
@@ -44,6 +46,21 @@ class MetadataProfile(Enum):
44
46
 
45
47
  linkml = "linkml"
46
48
  rdfs = "rdfs"
49
+ ols = "ols"
50
+
51
+ @staticmethod
52
+ def list():
53
+ return list(map(lambda c: c.value, MetadataProfile))
54
+
55
+
56
+ @unique
57
+ class OWLProfile(Enum):
58
+ """
59
+ An enumeration of OWL Profiles
60
+ """
61
+
62
+ dl = "dl"
63
+ full = "full"
47
64
 
48
65
  @staticmethod
49
66
  def list():
@@ -61,12 +78,16 @@ class OwlSchemaGenerator(Generator):
61
78
 
62
79
  `OWL Generator Docs <https://linkml.io/linkml/generators/owl>`_
63
80
 
64
- Attributes:
65
- type_objects if True, represent TypeDefinitions as objects; if False, as literals
66
- metaclasses if True, include OWL representations of ClassDefinition, SlotDefinition, etc. Introduces punning
67
- """
81
+ .
82
+
83
+ - LinkML ClassDefinitions are translated to OWL Classes
84
+ - LinkML SlotDefinitions are translated to OWL Properties
85
+ - LinkML Enumerations are translated to OWL Classes
86
+ - LinkML TypeDefinitions are translated to OWL Datatypes
68
87
 
69
- schema: Union[str, TextIO, SchemaDefinition] = None
88
+ The translation aims to be as faithful as possible. But note that OWL is open-world,
89
+ whereas LinkML is closed-world
90
+ """
70
91
 
71
92
  # ClassVars
72
93
  generatorname = os.path.basename(__file__)
@@ -75,148 +96,168 @@ class OwlSchemaGenerator(Generator):
75
96
  x.name for x in rdflib_plugins(None, rdflib_Parser) if "/" not in str(x.name)
76
97
  ]
77
98
  file_extension = "owl"
78
- visits_are_sorted = True
79
- uses_schemaloader = True
80
- requires_metamodel = True
99
+ uses_schemaloader = False
100
+
101
+ ontology_uri_suffix: str = None
102
+ """Suffix to add to the schema name to create the ontology URI, e.g. .owl.ttl"""
81
103
 
82
104
  # ObjectVars
83
105
  metadata_profile: MetadataProfile = None
84
- ontology_uri_suffix: str = None
106
+ """Deprecated - use metadata_profiles."""
107
+
108
+ metadata_profiles: List[MetadataProfile] = field(default_factory=lambda: [])
109
+ """By default, use the linkml metadata profile,
110
+ this allows for overrides."""
111
+
85
112
  metaclasses: bool = field(default_factory=lambda: True)
113
+ """if True, include OWL representations of ClassDefinition, SlotDefinition, etc. Introduces punning"""
114
+
86
115
  add_ols_annotations: bool = field(default_factory=lambda: True)
87
116
  graph: Optional[Graph] = None
117
+ """Mutable graph that is being built up during OWL generation."""
118
+
88
119
  top_value_uri: Optional[URIRef] = None
120
+ """If metaclasses=True, then this property is used to connect object shadows to literals"""
121
+
89
122
  type_objects: bool = field(default_factory=lambda: True)
90
- assert_equivalent_classes: bool = False
123
+ """if True, represents types as classes (and thus all slots are object properties);
124
+ typed object classes effectively shadow the main xsd literal types.
125
+ The purpose of this is to allow a uniform ObjectProperty representation for all slots,
126
+ without having to commit to being either Data or Object property (OWL-DL does not
127
+ allow a property to be both."""
128
+
129
+ assert_equivalent_classes: bool = field(default_factory=lambda: False)
130
+ """If True, assert equivalence between definition_uris and class_uris"""
131
+
132
+ use_native_uris: bool = field(default_factory=lambda: True)
133
+ """If True, use the definition_uris, otherwise use class_uris."""
134
+
135
+ mixins_as_expressions: bool = None
136
+ """EXPERIMENTAL: If True, use OWL existential restrictions to represent mixins"""
137
+
138
+ slot_is_literal_map: Mapping[str, Set[bool]] = field(default_factory=lambda: defaultdict(set))
139
+ """DEPRECATED: use node_owltypes"""
140
+
141
+ node_owltypes: Mapping[Union[BNode, URIRef], Set[OWL_TYPE]] = field(
142
+ default_factory=lambda: defaultdict(set)
143
+ )
144
+ """rdfs:Datatype, owl:Thing"""
145
+
146
+ simplify: bool = field(default_factory=lambda: True)
147
+ """Reduce complex expressions to simpler forms"""
148
+
149
+ target_profile: OWLProfile = field(default_factory=lambda: OWLProfile.dl)
150
+ """Target OWL profile. Currently the only distinction is between DL and Full"""
91
151
 
92
- def visit_schema(self, output: Optional[str] = None, **_):
93
- owl_id = self.schema.id
152
+ metamodel_schemaview: SchemaView = field(
153
+ default_factory=lambda: package_schemaview("linkml_runtime.linkml_model.meta")
154
+ )
155
+
156
+ def as_graph(self) -> Graph:
157
+ """
158
+ Generate an rdflib Graph from the schema
159
+
160
+ :return:
161
+ """
162
+ sv = self.schemaview
163
+ schema = sv.schema
164
+ owl_id = schema.id
94
165
  if self.ontology_uri_suffix:
95
166
  owl_id = f"{owl_id}{self.ontology_uri_suffix}"
167
+ mergeimports = self.mergeimports
96
168
  base = URIRef(owl_id)
97
- self.graph = Graph(identifier=base)
169
+ graph = Graph(identifier=base)
170
+ self.graph = graph
98
171
  for prefix in self.metamodel.schema.emit_prefixes:
99
172
  self.graph.bind(prefix, self.metamodel.namespaces[prefix])
100
- for pfx in self.schema.prefixes.values():
173
+ for pfx in schema.prefixes.values():
101
174
  self.graph.namespace_manager.bind(pfx.prefix_prefix, URIRef(pfx.prefix_reference))
102
-
103
- self.graph.add((base, RDF.type, OWL.Ontology))
104
- self._add_element_properties(base, self.schema)
105
-
106
- if self.metaclasses:
107
- # add the model types; these will be instantiated under each individual visitor node
108
- for name in [
109
- "class_definition",
110
- "type_definition",
111
- "slot_definition",
112
- "subset_definition",
113
- ]:
114
- self._add_metamodel_class(name)
115
-
116
- # add value placeholder
117
- if self.type_objects:
118
- # TODO: additional axioms, e.g. String subClassOf hasValue some string
119
- self.top_value_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME]["topValue"]
120
- self.graph.add((self.top_value_uri, RDF.type, OWL.DatatypeProperty))
121
- self.graph.add((self.top_value_uri, RDFS.label, Literal("value")))
122
-
123
- def end_schema(self, output: Optional[str] = None, **_) -> None:
175
+ graph.add((base, RDF.type, OWL.Ontology))
176
+
177
+ for cls in sv.all_classes(imports=mergeimports).values():
178
+ self.add_class(cls)
179
+ for a in cls.attributes.values():
180
+ self.add_slot(a, attribute=True)
181
+ for slot in sv.all_slots(imports=mergeimports, attributes=False).values():
182
+ self.add_slot(slot, attribute=False)
183
+ for typ in sv.all_types(imports=mergeimports).values():
184
+ self.add_type(typ)
185
+ for enm in sv.all_enums(imports=mergeimports).values():
186
+ self.add_enum(enm)
187
+
188
+ self.add_metadata(schema, base)
189
+ return graph
190
+
191
+ def serialize(self, **kwargs) -> str:
192
+ self.as_graph()
124
193
  data = self.graph.serialize(
125
194
  format="turtle" if self.format in ["owl", "ttl"] else self.format
126
- ).decode()
127
- if output:
128
- with open(output, "w", encoding="UTF-8") as outf:
129
- outf.write(data)
130
- else:
131
- print(data)
195
+ )
196
+ return data
132
197
 
133
198
  def add_metadata(self, e: Definition, uri: URIRef) -> None:
134
199
  """
135
- Add generic annotation properties
200
+ Add generic annotation properties.
136
201
 
137
202
  :param e: schema element
138
203
  :param uri: URI representation of schema element
139
204
  :return:
140
205
  """
141
- metadata_profile = self.metadata_profile
142
- # TODO: use metamodel annotations to drive this rather than hardcoding
143
- if e.aliases is not None:
144
- for s in e.aliases:
145
- self.graph.add((uri, SKOS.altLabel, Literal(s)))
146
- if e.title is not None:
147
- self.graph.add((uri, DCTERMS.title, Literal(e.title)))
148
- if e.description is not None:
149
- if metadata_profile is None or metadata_profile == MetadataProfile.linkml:
150
- prop = SKOS.definition
151
- elif metadata_profile == MetadataProfile.rdfs:
152
- prop = RDFS.comment
153
- else:
154
- raise ValueError(f"Cannot handle metadata profile: {metadata_profile}")
155
- self.graph.add((uri, prop, Literal(e.description)))
156
- if e.mappings is not None:
157
- for m in e.mappings:
158
- m_uri = self.namespaces.uri_for(m)
159
- if m_uri is not None:
160
- self.graph.add((uri, SKOS.exactMatch, m_uri))
161
- else:
162
- logging.warning(f"No URI for {m}")
163
- if e.exact_mappings is not None:
164
- for m in e.exact_mappings:
165
- m_uri = self.namespaces.uri_for(m)
166
- if m_uri is not None:
167
- self.graph.add((uri, SKOS.exactMatch, m_uri))
168
- else:
169
- logging.warning(f"No URI for {m}")
170
- if e.close_mappings is not None:
171
- for m in e.close_mappings:
172
- m_uri = self.namespaces.uri_for(m)
173
- if m_uri is not None:
174
- self.graph.add((uri, SKOS.closeMatch, m_uri))
175
- else:
176
- logging.warning(f"No URI for {m}")
177
- if e.narrow_mappings is not None:
178
- for m in e.narrow_mappings:
179
- m_uri = self.namespaces.uri_for(m)
180
- if m_uri is not None:
181
- self.graph.add((uri, SKOS.narrowMatch, m_uri))
182
- else:
183
- logging.warning(f"No URI for {m}")
184
- if e.broad_mappings is not None:
185
- for m in e.broad_mappings:
186
- m_uri = self.namespaces.uri_for(m)
187
- if m_uri is not None:
188
- self.graph.add((uri, SKOS.broadMatch, m_uri))
189
- else:
190
- logging.warning(f"No URI for {m}")
191
- if e.related_mappings is not None:
192
- for m in e.related_mappings:
193
- m_uri = self.namespaces.uri_for(m)
194
- if m_uri is not None:
195
- self.graph.add((uri, SKOS.relatedMatch, m_uri))
206
+
207
+ msv = self.metamodel_schemaview
208
+ this_sv = self.schemaview
209
+ sn_mappings = msv.slot_name_mappings()
210
+
211
+ for metaslot_name, metaslot_value in vars(e).items():
212
+ if not metaslot_value:
213
+ continue
214
+ metaslot_name = sn_mappings.get(metaslot_name).name
215
+ metaslot = msv.induced_slot(metaslot_name, e.class_name)
216
+ metaslot_curie = msv.get_uri(metaslot, native=False, expand=False)
217
+ if metaslot_curie.startswith("linkml:"):
218
+ # only mapped properties
219
+ continue
220
+ metaslot_uri = URIRef(msv.get_uri(metaslot, native=False, expand=True))
221
+ if metaslot_name == "description" and self.has_profile(MetadataProfile.rdfs):
222
+ metaslot_uri = RDFS.comment
223
+ metaslot_range = metaslot.range
224
+ if not isinstance(metaslot_value, list):
225
+ metaslot_value = [metaslot_value]
226
+ for v in metaslot_value:
227
+ if metaslot_range in msv.all_types():
228
+ if metaslot_range == "uri":
229
+ obj = URIRef(v)
230
+ elif metaslot_range == "uriorcurie":
231
+ obj = URIRef(this_sv.expand_curie(v))
232
+ else:
233
+ obj = Literal(v)
234
+ elif metaslot_range in msv.all_subsets():
235
+ obj = Literal(v) # TODO
236
+ elif metaslot_range in msv.all_classes():
237
+ continue
238
+ # if isinstance(v, str):
239
+ # obj = URIRef(msv.expand_curie(v))
240
+ # else:
241
+ # logging.debug(f"Skipping {uri} {metaslot_uri} => {v}")
196
242
  else:
197
- logging.warning(f"No URI for {m}")
243
+ obj = Literal(v)
244
+ self.graph.add((uri, metaslot_uri, obj))
245
+
198
246
  for k, v in e.annotations.items():
199
- if ":" in k:
200
- k_curie = k
201
- try:
202
- k_uri = self.namespaces.uri_for(k_curie)
203
- except ValueError:
204
- try:
205
- k_uri = self.metamodel.namespaces.uri_for(k_curie)
206
- except ValueError as me:
207
- logging.info("Ignoring namespace error: " + str(me))
208
- finally:
209
- if k_uri is not None:
210
- self.graph.add((uri, k_uri, Literal(v.value)))
211
- # use default schema prefix
247
+ if ":" not in k:
248
+ default_prefix = this_sv.schema.default_prefix
249
+ if default_prefix in this_sv.schema.prefixes:
250
+ default_prefix = this_sv.schema.prefixes[default_prefix].prefix_reference
251
+ k = default_prefix + k
252
+ k_uri = this_sv.expand_curie(k)
212
253
  else:
213
- try:
214
- k_uri = self.namespaces.uri_for(underscore(k))
215
- self.graph.add((uri, k_uri, Literal(v.value)))
216
- except Exception as e:
217
- logging.info("Ignoring annotation error: " + str(e))
254
+ k_uri = this_sv.expand_curie(k)
255
+ if k_uri == k:
256
+ k_uri = None
257
+ if k_uri:
258
+ self.graph.add((uri, URIRef(k_uri), Literal(v.value)))
218
259
 
219
- def visit_class(self, cls: ClassDefinition) -> bool:
260
+ def add_class(self, cls: ClassDefinition) -> bool:
220
261
  """
221
262
  Each ClassDefinition is represented as an OWL class
222
263
 
@@ -228,8 +269,8 @@ class OwlSchemaGenerator(Generator):
228
269
  """
229
270
  # To understand how the RDF-level operations here related to the OWL
230
271
  # representation, consult https://www.w3.org/TR/owl2-mapping-to-rdf/
272
+ sv = self.schemaview
231
273
  cls_uri = self._class_uri(cls.name)
232
- self.add_mappings(cls)
233
274
  self.add_metadata(cls, cls_uri)
234
275
  # add declaration
235
276
  self.graph.add((cls_uri, RDF.type, OWL.Class))
@@ -239,24 +280,20 @@ class OwlSchemaGenerator(Generator):
239
280
  (
240
281
  cls_uri,
241
282
  RDF.type,
242
- self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
243
- camelcase("class definition")
244
- ],
283
+ ClassDefinition.class_class_uri,
245
284
  )
246
285
  )
247
- self._add_element_properties(cls_uri, cls)
286
+ # self._add_element_properties(cls_uri, cls)
248
287
 
249
288
  # Parent classes
250
289
  if cls.is_a:
251
290
  self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(cls.is_a)))
252
- if cls.mixin:
253
- self.graph.add((cls_uri, RDFS.subClassOf, METAMODEL_NAMESPACE.mixin))
254
291
  for mixin in sorted(cls.mixins):
255
- self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(mixin)))
256
- if cls.name in self.synopsis.applytorefs:
257
- for appl in sorted(self.synopsis.applytorefs[cls.name].classrefs):
258
- self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(appl)))
259
- if self.add_ols_annotations:
292
+ parent = self._class_uri(mixin)
293
+ if self.mixins_as_expressions:
294
+ parent = self._some_values_from(self._metaslot_uri("mixin"), parent)
295
+ self.graph.add((cls_uri, RDFS.subClassOf, parent))
296
+ if self.has_profile(MetadataProfile.ols):
260
297
  # Add annotations for browser hints. See https://www.ebi.ac.uk/ols/docs/installation-guide
261
298
  if cls.is_a is None:
262
299
  if len(cls.mixins) == 0:
@@ -269,125 +306,269 @@ class OwlSchemaGenerator(Generator):
269
306
  )
270
307
  )
271
308
 
272
- if cls.class_uri and self.assert_equivalent_classes:
273
- eq_class_uri = self.namespaces.uri_for(cls.class_uri)
274
- if str(eq_class_uri) != cls.definition_uri:
275
- self.graph.add((cls_uri, OWL.equivalentClass, eq_class_uri))
276
- self.graph.remove((cls_uri, SKOS.exactMatch, eq_class_uri))
277
-
278
- # If defining slots, we generate an equivalentClass entry
279
- # equ_node = BNode()
280
- # self.graph.add((cls_uri, OWL.equivalentClass, equ_node))
281
- # self.graph.add((equ_node, RDF.type, OWL.Class))
282
- #
283
- # elts = []
284
- # if cls.is_a:
285
- # elts.append(self._class_uri(cls.is_a))
286
- # if cls.mixin:
287
- # self.graph.add((cls_uri, RDFS.subClassOf, META_NS.mixin))
288
- # for mixin in cls.mixins:
289
- # self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(mixin)))
290
- # if cls.name in self.synopsis.applytorefs:
291
- # for appl in self.synopsis.applytorefs[cls.name].classrefs:
292
- # self.graph.add((cls_uri, RDFS.subClassOf, self._class_uri(appl)))
293
- #
294
- # for slotname in cls.defining_slots:
295
- # restr_node = BNode()
296
- # slot = self.schema.slots[slotname]
297
- #
298
- # self.graph.add((restr_node, RDF.type, OWL.Restriction))
299
- # self.graph.add((restr_node, OWL.onProperty, self._prop_uri(slotname)))
300
- # self._add_cardinality(restr_node, slot)
301
- # # TODO: fix this
302
- # # self.graph.add((restr_node, OWL.someValuesFrom, self._build_range(slot)))
303
- # elts.append(restr_node)
304
- #
305
- # coll_bnode = BNode()
306
- # Collection(self.graph, coll_bnode, elts)
307
- # self.graph.add((equ_node, OWL.intersectionOf, coll_bnode))
308
-
309
- # TODO: see whether unions belong
310
- # if cls.union_of:
311
- # union_node = BNode()
312
- # Collection(self.graph, union_coll, [self.class_uri(union_node) for union_node in cls.union_of])
313
- # self.graph.add((union_node, OWL.unionOf, union_coll))
314
- # self.graph.add((cls_uri, RDFS.subClassOf, union_node))
315
-
316
- for sn in sorted(self.own_slot_names(cls)):
317
- # Defining_slots are covered above
318
- slot = self.schema.slots[sn]
319
- slot_node = BNode()
320
- self.graph.add((cls_uri, RDFS.subClassOf, slot_node))
321
-
322
- if self._range_is_datatype(slot):
323
- cardinality_on = OWL.onDataRange
309
+ if cls.class_uri:
310
+ mapped_uri = sv.get_uri(cls, expand=True, native=not self.use_native_uris)
311
+ if cls_uri != mapped_uri:
312
+ p = OWL.equivalentClass if self.assert_equivalent_classes else SKOS.exactMatch
313
+ self.graph.add((URIRef(cls_uri), p, URIRef(mapped_uri)))
314
+ superclass_expr = self.transform_class_expression(cls)
315
+ if superclass_expr:
316
+ ixn_listnodes = []
317
+ if isinstance(superclass_expr, BNode):
318
+ ixn_listnodes = list(self.graph.objects(superclass_expr, OWL.intersectionOf))
319
+ if self.simplify and ixn_listnodes:
320
+ # simplify
321
+ if len(ixn_listnodes) > 1:
322
+ raise AssertionError
323
+ for x in Collection(self.graph, ixn_listnodes[0]):
324
+ self.graph.add((cls_uri, RDFS.subClassOf, x))
325
+ self._remove_list(ixn_listnodes[0])
326
+ self.graph.remove((superclass_expr, OWL.intersectionOf, ixn_listnodes[0]))
324
327
  else:
325
- cardinality_on = OWL.onClass
326
- slot_uri = self.namespaces.uri_for(slot.slot_uri)
327
- if slot_uri == "rdf:type":
328
- logging.warning(
329
- "rdflib may have issues serializing rdf:type with turtle serializer"
328
+ self.graph.add((cls_uri, RDFS.subClassOf, superclass_expr))
329
+
330
+ def transform_class_expression(
331
+ self, cls: Union[ClassDefinition, AnonymousClassExpression]
332
+ ) -> BNode:
333
+ graph = self.graph
334
+ sv = self.schemaview
335
+ if isinstance(cls, ClassDefinition):
336
+ own_slots = list(cls.slot_usage.values()) + list(cls.attributes.values())
337
+ else:
338
+ own_slots = []
339
+ own_slots.extend(cls.slot_conditions.values())
340
+ # sort by name
341
+ own_slots.sort(key=lambda x: x.name)
342
+ owl_exprs = []
343
+ if cls.any_of:
344
+ owl_exprs.append(
345
+ self._union_of([self.transform_class_expression(x) for x in cls.any_of])
346
+ )
347
+ if cls.exactly_one_of:
348
+ sub_exprs = [self.transform_class_expression(x) for x in cls.exactly_one_of]
349
+ if isinstance(cls, ClassDefinition):
350
+ cls_uri = self._class_uri(cls.name)
351
+ listnode = BNode()
352
+ Collection(graph, listnode, sub_exprs)
353
+ graph.add((cls_uri, OWL.disjointUnionOf, listnode))
354
+ else:
355
+ sub_sub_exprs = []
356
+ for i, x in enumerate(sub_exprs):
357
+ rest = sub_exprs[0:i] + sub_exprs[i + 1 :]
358
+ neg_expr = self._complement_of_union_of(rest)
359
+ sub_sub_exprs.append(self._intersection_of([x, neg_expr]))
360
+ sub_exprs.append(self._union_of(sub_sub_exprs))
361
+ if cls.all_of:
362
+ owl_exprs.append(
363
+ self._intersection_of([self.transform_class_expression(x) for x in cls.all_of])
364
+ )
365
+ if cls.none_of:
366
+ owl_exprs.append(
367
+ self._complement_of_union_of(
368
+ [self.transform_class_expression(x) for x in cls.any_of]
330
369
  )
331
- if slot.required:
332
- if slot.multivalued:
333
- # intersectionOf(restriction(slot only type) restriction(slot some type)
334
- restr1 = BNode()
335
- self.graph.add((restr1, RDF.type, OWL.Restriction))
336
- self.graph.add((restr1, OWL.allValuesFrom, self._range_uri(slot)))
337
- self.graph.add((restr1, OWL.onProperty, slot_uri))
338
-
339
- restr2 = BNode()
340
- self.graph.add((restr2, RDF.type, OWL.Restriction))
341
- self.graph.add((restr2, OWL.someValuesFrom, self._range_uri(slot)))
342
- self.graph.add((restr2, OWL.onProperty, slot_uri))
343
-
344
- coll_bnode = BNode()
345
- Collection(self.graph, coll_bnode, [restr1, restr2])
346
- self.graph.add((slot_node, OWL.intersectionOf, coll_bnode))
347
- self.graph.add((slot_node, RDF.type, OWL.Class))
370
+ )
371
+ for slot in own_slots:
372
+ x = self.transform_class_slot_expression(cls, slot, slot)
373
+ if not x:
374
+ range = sv.schema.default_range
375
+ if range:
376
+ if range in sv.all_types():
377
+ x = self._type_uri(range)
378
+ elif range in sv.all_enums():
379
+ x = self._enum_uri(range)
380
+ elif range in sv.all_classes():
381
+ x = self._enum_uri(range)
382
+ else:
383
+ raise ValueError(f"Unknown range {range}")
384
+ # x = self._class_uri(range)
348
385
  else:
349
- # restriction(slot exactly 1 type)
350
- self.graph.add((slot_node, RDF.type, OWL.Restriction))
351
- self.graph.add((slot_node, OWL.qualifiedCardinality, Literal(1)))
352
- self.graph.add((slot_node, OWL.onProperty, slot_uri))
353
- self.graph.add((slot_node, cardinality_on, self._range_uri(slot)))
386
+ x = OWL.Thing
387
+ slot_uri = self._prop_uri(slot)
388
+ if slot.name in sv.all_slots():
389
+ top_slot = sv.get_slot(slot.name)
354
390
  else:
355
- if slot.multivalued:
356
- # restriction(slot only type)
357
- self.graph.add((slot_node, RDF.type, OWL.Restriction))
358
- self.graph.add((slot_node, OWL.allValuesFrom, self._range_uri(slot)))
359
- self.graph.add((slot_node, OWL.onProperty, slot_uri))
360
- else:
361
- # intersectionOf(restriction(slot only type) restriction(slot max 1 type))
362
- self.graph.add((slot_node, RDF.type, OWL.Restriction))
363
- self.graph.add((slot_node, cardinality_on, self._range_uri(slot)))
364
- self.graph.add((slot_node, OWL.maxQualifiedCardinality, Literal(1)))
365
- self.graph.add((slot_node, OWL.onProperty, slot_uri))
366
-
367
- return True
368
-
369
- def visit_slot(self, slot_name: str, slot: SlotDefinition) -> None:
370
- """Add a slot definition per slot
371
-
372
- Note: visit_slot may be called multiple times for the same slot_uri, as the same slot_uri can be used:
373
- * when the schema declares `attributes`
374
- * when `slot_usage` induces additional slots
391
+ top_slot = slot
392
+ avf = BNode()
393
+ graph.add((avf, RDF.type, OWL.Restriction))
394
+ graph.add((avf, OWL.allValuesFrom, x))
395
+ graph.add((avf, OWL.onProperty, slot_uri))
396
+ owl_exprs.append(avf)
397
+ min_card_expr = BNode()
398
+ owl_exprs.append(min_card_expr)
399
+ min_card = 1 if slot.required or top_slot.required else 0
400
+ graph.add((min_card_expr, RDF.type, OWL.Restriction))
401
+ graph.add((min_card_expr, OWL.minCardinality, Literal(min_card)))
402
+ graph.add((min_card_expr, OWL.onProperty, slot_uri))
403
+ if not slot.multivalued and not top_slot.multivalued:
404
+ max_card_expr = BNode()
405
+ owl_exprs.append(max_card_expr)
406
+ graph.add((max_card_expr, RDF.type, OWL.Restriction))
407
+ graph.add((max_card_expr, OWL.maxCardinality, Literal(1)))
408
+ graph.add((max_card_expr, OWL.onProperty, slot_uri))
409
+ return self._intersection_of(owl_exprs)
410
+
411
+ def transform_class_slot_expression(
412
+ self,
413
+ cls: Optional[Union[ClassDefinition, AnonymousClassExpression]],
414
+ slot: Union[SlotDefinition, AnonymousSlotExpression],
415
+ main_slot: SlotDefinition = None,
416
+ ) -> Union[BNode, URIRef]:
417
+ """
418
+ Take a ClassExpression and SlotExpression combination and transform to a node.
375
419
 
376
- @param slot_name:
377
- @param slot:
378
- @return:
420
+ :param cls:
421
+ :param slot:
422
+ :param main_slot:
423
+ :return:
379
424
  """
425
+ sv = self.schemaview
426
+ if main_slot is None:
427
+ if not isinstance(slot, SlotDefinition):
428
+ raise ValueError(f"Must pass main slot for {slot}")
429
+ main_slot = slot
430
+
431
+ owl_exprs = []
432
+
433
+ if slot.any_of:
434
+ owl_exprs.append(
435
+ self._union_of(
436
+ [self.transform_class_slot_expression(cls, x, main_slot) for x in slot.any_of]
437
+ )
438
+ )
439
+ if slot.all_of:
440
+ owl_exprs.append(
441
+ self._intersection_of(
442
+ [self.transform_class_slot_expression(cls, x, main_slot) for x in slot.all_of]
443
+ )
444
+ )
445
+ if slot.none_of:
446
+ owl_exprs.append(
447
+ self._complement_of_union_of(
448
+ [self.transform_class_slot_expression(cls, x, main_slot) for x in slot.none_of]
449
+ )
450
+ )
451
+ if slot.exactly_one_of:
452
+ owl_exprs.append(
453
+ self._exactly_one_of(
454
+ [
455
+ self.transform_class_slot_expression(cls, x, main_slot)
456
+ for x in slot.exactly_one_of
457
+ ]
458
+ )
459
+ )
460
+ range = slot.range
461
+ # if not range and not owl_exprs:
462
+ # range = sv.schema.default_range
463
+ owl_types = set()
464
+ if range:
465
+ if range in sv.all_types(imports=True):
466
+ self.slot_is_literal_map[main_slot.name].add(True)
467
+ owl_types.add(RDFS.Literal)
468
+ typ = sv.get_type(range)
469
+ if self.type_objects:
470
+ # TODO
471
+ owl_exprs.append(self._type_uri(typ.name))
472
+ else:
473
+ owl_exprs.append(self._type_uri(typ.name))
474
+ elif range in self.schema.enums:
475
+ # TODO: enums fill this in
476
+ owl_exprs.append(self._enum_uri(EnumDefinitionName(range)))
477
+ else:
478
+ owl_types.add(OWL.Thing)
479
+ self.slot_is_literal_map[main_slot.name].add(False)
480
+ owl_exprs.append(self._class_uri(ClassDefinitionName(range)))
481
+ constraints_exprs, constraints_owltypes = self.add_constraints(slot)
482
+ owl_types.update(constraints_owltypes)
483
+ owl_exprs.extend(constraints_exprs)
484
+ # constraints = {
485
+ # XSD.minInclusive: slot.minimum_value,
486
+ # XSD.maxInclusive: slot.maximum_value,
487
+ # XSD.pattern: slot.pattern, # TODO: map between ECMAScript and XSD regular expressions
488
+ # }
489
+ # for constraint_prop, constraint_val in constraints.items():
490
+ # if constraint_val is not None:
491
+ # owl_types.add(RDFS.Literal)
492
+ # dr = BNode()
493
+ # graph.add((dr, RDF.type, RDFS.Datatype))
494
+ # if isinstance(constraint_val, float):
495
+ # graph.add((dr, OWL.onDatatype, XSD.float))
496
+ # elif isinstance(constraint_val, int):
497
+ # graph.add((dr, OWL.onDatatype, XSD.integer))
498
+ # else:
499
+ # graph.add((dr, OWL.onDatatype, XSD.string))
500
+ # listnode = BNode()
501
+ # x = BNode()
502
+ # Collection(graph, listnode, [x])
503
+ # graph.add((dr, OWL.withRestrictions, listnode))
504
+ # graph.add((x, constraint_prop, Literal(constraint_val)))
505
+ # owl_exprs.append(dr)
506
+ # constraints = {k: v for k, v in constraints.items() if v is not None}
507
+ # if False and constraints:
508
+ # owl_types.add(RDFS.Literal)
509
+ # dr = BNode()
510
+ # listnode = BNode()
511
+ # graph.add((dr, RDF.type, RDFS.Datatype))
512
+ # # TODO: allow for mixing of constraints
513
+ # #if isinstance(v, int):
514
+ # # dt = XSD.integer
515
+ # #elif isinstance(v, float):
516
+ # # dt = XSD.decimal
517
+ # #else:
518
+ # # logging.warning(f"Unknown datatype for {v}")
519
+ # # dt = XSD.integer
520
+ # graph.add((dr, OWL.onDatatype, XSD.integer))
521
+ # graph.add((dr, OWL.withRestrictions, listnode))
522
+ # dr_exprs = []
523
+ # for prop, val in constraints.items():
524
+ # x = BNode()
525
+ # graph.add((x, prop, Literal(val)))
526
+ # dr_exprs.append(x)
527
+ # Collection(graph, listnode, dr_exprs)
528
+ # owl_exprs.append(dr)
529
+ this_expr = self._intersection_of(owl_exprs, owl_types=owl_types)
530
+ self.node_owltypes[this_expr].update(self._get_owltypes(owl_types, owl_exprs))
531
+ return this_expr
532
+
533
+ def add_constraints(
534
+ self,
535
+ element: Union[
536
+ SlotDefinition, AnonymousSlotExpression, TypeDefinition, AnonymousTypeExpression
537
+ ],
538
+ ) -> Tuple[List[BNode], Set[OWL_TYPE]]:
539
+ owl_types = set()
540
+ owl_exprs = []
541
+ graph = self.graph
542
+ constraints = {
543
+ XSD.minInclusive: element.minimum_value,
544
+ XSD.maxInclusive: element.maximum_value,
545
+ XSD.pattern: element.pattern, # TODO: map between ECMAScript and XSD regular expressions
546
+ }
547
+ for constraint_prop, constraint_val in constraints.items():
548
+ if constraint_val is not None:
549
+ owl_types.add(RDFS.Literal)
550
+ dr = BNode()
551
+ graph.add((dr, RDF.type, RDFS.Datatype))
552
+ if isinstance(constraint_val, float):
553
+ graph.add((dr, OWL.onDatatype, XSD.float))
554
+ elif isinstance(constraint_val, int):
555
+ graph.add((dr, OWL.onDatatype, XSD.integer))
556
+ else:
557
+ graph.add((dr, OWL.onDatatype, XSD.string))
558
+ listnode = BNode()
559
+ x = BNode()
560
+ Collection(graph, listnode, [x])
561
+ graph.add((dr, OWL.withRestrictions, listnode))
562
+ graph.add((x, constraint_prop, Literal(constraint_val)))
563
+ owl_exprs.append(dr)
564
+ return owl_exprs, owl_types
565
+
566
+ def add_slot(self, slot: SlotDefinition, attribute=False) -> None:
380
567
  # determine if this is a slot that has been induced by slot_usage; if so
381
568
  # the meaning of the slot is context-specific and should not be used for
382
569
  # global properties
383
- if slot.alias is not None and slot.alias != slot.name and slot.alias in self.schema.slots:
384
- logging.debug(
385
- f"SKIPPING slot induced by slot_usage: {slot.alias} // {slot.name} // {slot}"
386
- )
387
- return
388
570
 
389
- slot_uri = self._prop_uri(slot.name)
390
- # logging.error(f'SLOT_URI={slot_uri}')
571
+ slot_uri = self._prop_uri(slot)
391
572
 
392
573
  # Slots may be modeled as Object or Datatype Properties
393
574
  # if type_objects is True, then ALL slots are ObjectProperties
@@ -398,53 +579,54 @@ class OwlSchemaGenerator(Generator):
398
579
  (
399
580
  slot_uri,
400
581
  RDF.type,
401
- self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
402
- camelcase("slot definition")
403
- ],
582
+ SlotDefinition.class_class_uri,
404
583
  )
405
584
  )
406
585
 
407
- slots_with_same_uri = [
408
- s.name
409
- for s in self.schema.slots.values()
410
- if slot_uri == self._prop_uri(s.name) and not s.usage_slot_name
411
- ]
412
- if len(slots_with_same_uri) > 1:
413
- logging.error(
414
- f"Multiple slots with URI: {slot_uri}: {slots_with_same_uri}; consider giving each a unique slot_uri"
415
- )
416
- return
586
+ if attribute:
587
+ n = 0
588
+ for c in self.schemaview.all_classes().values():
589
+ for a in c.attributes.values():
590
+ att_uri = self.schemaview.get_uri(a, native=False, expand=True)
591
+ if slot_uri == URIRef(att_uri):
592
+ n += 1
593
+ if n > 1:
594
+ logging.warning(f"Ambiguous attribute: {slot.name} {slot_uri}")
595
+ return
417
596
 
418
- self.add_mappings(slot)
419
597
  self.add_metadata(slot, slot_uri)
420
- self._add_element_properties(slot_uri, slot)
421
598
 
422
- self.graph.add((slot_uri, RDFS.range, self._range_uri(slot)))
599
+ if attribute:
600
+ return
601
+
602
+ range_expr = self.transform_class_slot_expression(None, slot)
603
+ if range_expr:
604
+ self.graph.add((slot_uri, RDFS.range, range_expr))
423
605
  if slot.domain:
424
606
  self.graph.add((slot_uri, RDFS.domain, self._class_uri(slot.domain)))
425
607
  if slot.inverse:
426
608
  self.graph.add((slot_uri, OWL.inverseOf, self._prop_uri(slot.inverse)))
427
- if slot.symmetric:
428
- self.graph.add((slot_uri, RDF.type, OWL.SymmetricProperty))
609
+ characteristics = {
610
+ "symmetric": OWL.SymmetricProperty,
611
+ "asymmetric": OWL.AsymmetricProperty,
612
+ "transitive": OWL.TransitiveProperty,
613
+ "reflexive": OWL.ReflexiveProperty,
614
+ "irreflexive": OWL.IrreflexiveProperty,
615
+ }
616
+ for metaslot, uri in characteristics.items():
617
+ if getattr(slot, metaslot, False):
618
+ self.graph.add((slot_uri, RDF.type, uri))
429
619
 
430
- # Parent slots. Note that is_a and mixin both map to subPropertyOf,
431
- # and are not distinguishable
432
- # TODO: consider annotating axiom to indicate if this is is-a or mixin
433
620
  if slot.is_a:
434
621
  self.graph.add((slot_uri, RDFS.subPropertyOf, self._prop_uri(slot.is_a)))
435
622
  for mixin in slot.mixins:
436
623
  self.graph.add((slot_uri, RDFS.subPropertyOf, self._prop_uri(mixin)))
437
- if slot.name in self.synopsis.applytorefs:
438
- for appl in self.synopsis.applytorefs[slot.name].slotrefs:
439
- self.graph.add((slot_uri, RDFS.subClassOf, self._prop_uri(appl)))
440
624
 
441
- def visit_type(self, typ: TypeDefinition) -> None:
625
+ def add_type(self, typ: TypeDefinition) -> None:
442
626
  type_uri = self._type_uri(typ.name)
443
- if not self.type_objects:
444
- return False
445
627
  if typ.from_schema == "https://w3id.org/linkml/types":
446
628
  return
447
- self.graph.add((type_uri, RDF.type, OWL.Class))
629
+
448
630
  if self.metaclasses:
449
631
  self.graph.add(
450
632
  (
@@ -455,18 +637,43 @@ class OwlSchemaGenerator(Generator):
455
637
  ],
456
638
  )
457
639
  )
458
- self._add_element_properties(type_uri, typ)
459
- if typ.typeof:
460
- self.graph.add((type_uri, RDFS.subClassOf, self._type_uri(typ.typeof)))
640
+ # self._add_element_properties(type_uri, typ)
641
+ if self.type_objects:
642
+ self.graph.add((type_uri, RDF.type, OWL.Class))
643
+ if typ.typeof:
644
+ self.graph.add((type_uri, RDFS.subClassOf, self._type_uri(typ.typeof)))
645
+ else:
646
+ if not self.top_value_uri:
647
+ self.top_value_uri = self.metamodel.namespaces[METAMODEL_NAMESPACE_NAME][
648
+ "topValue"
649
+ ]
650
+ self.graph.add((self.top_value_uri, RDF.type, OWL.DatatypeProperty))
651
+ self.graph.add((self.top_value_uri, RDFS.label, Literal("value")))
652
+ restr = BNode()
653
+ self.graph.add((restr, RDF.type, OWL.Restriction))
654
+ self.graph.add((restr, OWL.qualifiedCardinality, Literal(1)))
655
+ self.graph.add((restr, OWL.onProperty, self.top_value_uri))
656
+ self.graph.add((restr, OWL.onDataRange, self._type_uri(typ.name)))
657
+ self.graph.add((type_uri, RDFS.subClassOf, restr))
461
658
  else:
462
- restr = BNode()
463
- self.graph.add((restr, RDF.type, OWL.Restriction))
464
- self.graph.add((restr, OWL.qualifiedCardinality, Literal(1)))
465
- self.graph.add((restr, OWL.onProperty, self.top_value_uri))
466
- self.graph.add((restr, OWL.onDataRange, self.namespaces.uri_for(typ.uri)))
467
- self.graph.add((type_uri, RDFS.subClassOf, restr))
468
-
469
- def visit_enum(self, e: EnumDefinition) -> None:
659
+ self.graph.add((type_uri, RDF.type, RDFS.Datatype))
660
+ eq_conjunctions = []
661
+ if typ.typeof and type_uri != self._type_uri(typ.typeof):
662
+ # self.graph.add((type_uri, OWL.equivalentClass, self._type_uri(typ.typeof)))
663
+ eq_conjunctions.append(self._type_uri(typ.typeof))
664
+ # self.graph.add((type_uri, RDFS.subClassOf, self._type_uri(typ.typeof)))
665
+ if typ.uri and type_uri != URIRef(self.schemaview.expand_curie(typ.uri)):
666
+ eq_conjunctions.append(URIRef(self.schemaview.expand_curie(typ.uri)))
667
+ # self.graph.add(
668
+ # (type_uri, OWL.equivalentClass, URIRef(self.schemaview.expand_curie(typ.uri)))
669
+ # )
670
+ constraints_exprs, _ = self.add_constraints(typ)
671
+ eq_conjunctions.extend(constraints_exprs)
672
+ ixn = self._intersection_of(eq_conjunctions)
673
+ if ixn:
674
+ self.graph.add((type_uri, OWL.equivalentClass, ixn))
675
+
676
+ def add_enum(self, e: EnumDefinition) -> None:
470
677
  g = self.graph
471
678
  enum_uri = self._enum_uri(e.name)
472
679
  g.add((enum_uri, RDF.type, OWL.Class))
@@ -480,15 +687,12 @@ class OwlSchemaGenerator(Generator):
480
687
  ],
481
688
  )
482
689
  )
483
- self._add_element_properties(enum_uri, e)
484
690
  pv_uris = []
485
691
  for pv in e.permissible_values.values():
486
692
  if pv.meaning:
487
- pv_uri = self.namespaces.uri_for(pv.meaning)
693
+ pv_uri = URIRef(self.schemaview.expand_curie(pv.meaning))
488
694
  else:
489
695
  pv_uri = enum_uri + "#" + pv.text.replace(" ", "+")
490
- # pv_uri = self.namespaces.uri_for(downcase(pv.text))
491
- # pv_uri = None
492
696
  pv_uris.append(pv_uri)
493
697
  if pv_uri:
494
698
  g.add((pv_uri, RDF.type, OWL.Class))
@@ -500,71 +704,120 @@ class OwlSchemaGenerator(Generator):
500
704
  pv_uri,
501
705
  )
502
706
  )
503
- self._add_element_properties(pv_uri, pv)
707
+ # self._add_element_properties(pv_uri, pv)
504
708
  if self.metaclasses:
505
709
  g.add((pv_uri, RDF.type, enum_uri))
506
710
  if all([pv is not None for pv in pv_uris]):
507
- union_bnode = BNode()
508
- Collection(self.graph, union_bnode, pv_uris)
509
- self.graph.add((enum_uri, OWL.unionOf, union_bnode))
510
-
511
- def _add_element_properties(self, uri: URIRef, el: Element) -> None:
512
- metamodel = self.metamodel
513
- for k, v in el.__dict__.items():
514
- if k in metamodel.schema.slots:
515
- defining_slot = self.metamodel.schema.slots[k]
516
- if v is not None and (
517
- "owl" in defining_slot.in_subset or "OwlProfile" in defining_slot.in_subset
518
- ):
519
- if isinstance(v, list):
520
- ve = v
521
- elif isinstance(v, dict):
522
- ve = v.values()
523
- else:
524
- ve = [v]
525
- for e in ve:
526
- if isinstance(
527
- e,
528
- (
529
- ClassDefinition,
530
- SlotDefinition,
531
- TypeDefinition,
532
- EnumDefinition,
533
- ),
534
- ):
535
- return
536
- if k == "name" and isinstance(el, SlotDefinition) and el.alias is not None:
537
- prop_uri = RDFS.label
538
- e = el.alias
539
- else:
540
- prop_uri = URIRef(
541
- self.metamodel.namespaces.uri_for(defining_slot.slot_uri)
542
- )
543
- object = self._as_rdf_element(e, defining_slot)
544
- if object is not None:
545
- self.graph.add((uri, prop_uri, object))
546
-
547
- def _as_rdf_element(
548
- self, element: Union[ElementName, Element], parent_slot: SlotDefinition
549
- ) -> Optional[Union[URIRef, Literal]]:
550
- if parent_slot.range in self.metamodel.schema.types:
551
- return Literal(element)
552
- elif parent_slot.range in self.metamodel.schema.enums:
553
- return Literal(element)
554
- elif parent_slot.range in self.metamodel.schema.classes:
555
- if parent_slot.inlined:
556
- element_name = element.name
557
- else:
558
- element_name = element
559
- if element_name in self.metamodel.schema.classes:
560
- return self._class_uri(element_name)
561
- elif element_name in self.metamodel.schema.types:
562
- return self._type_uri(element_name)
563
- elif element_name in self.metamodel.schema.enums:
564
- return self._enum_uri(element_name)
565
- else:
566
- logging.warning(f"Unknown range {parent_slot.range}")
711
+ self._union_of(pv_uris, node=enum_uri)
712
+
713
+ def has_profile(self, profile: MetadataProfile, default=False) -> bool:
714
+ """
715
+ Determine if a metadata profile is active.
716
+
717
+ :param profile: profile to check
718
+ :param default: True if the configured profiles include the specified profile
719
+ :return:
720
+ """
721
+ if default and not self.metadata_profile and not self.metadata_profiles:
722
+ return True
723
+ return profile in self.metadata_profiles or profile == self.metadata_profile
724
+
725
+ def _get_owltypes(
726
+ self, current: Set[OWL_TYPE], exprs: List[Union[BNode, URIRef]]
727
+ ) -> Set[OWL_TYPE]:
728
+ """
729
+ Gets the OWL types of specified expressions plus current owl types.
730
+
731
+ :param current:
732
+ :param exprs:
733
+ :return:
734
+ """
735
+ owltypes = set()
736
+ for x in exprs:
737
+ x_owltypes = self.node_owltypes.get(x, None)
738
+ if x_owltypes:
739
+ owltypes.update(x_owltypes)
740
+ owltypes.update(current)
741
+ if len(owltypes) > 1:
742
+ logging.warning(f"Multiple owl types {owltypes}")
743
+ # if self.target_profile == OWLProfile.dl:
744
+ return owltypes
745
+
746
+ def _remove_list(self, listnode: BNode) -> None:
747
+ graph = self.graph
748
+ triples = list(graph.triples((listnode, None, None)))
749
+ while triples:
750
+ t = triples.pop(0)
751
+ subj, pred, obj = t
752
+ if pred not in [RDF.first, RDF.rest]:
753
+ continue
754
+ graph.remove(t)
755
+ if isinstance(obj, BNode):
756
+ triples.extend(graph.triples((obj, None, None)))
757
+
758
+ def _some_values_from(self, property: URIRef, filler: Union[URIRef, BNode]) -> BNode:
759
+ node = BNode()
760
+ self.graph.add((node, RDF.type, OWL.Restriction))
761
+ self.graph.add((node, OWL.onProperty, property))
762
+ self.graph.add((node, OWL.someValuesFrom, filler))
763
+ return node
764
+
765
+ def _complement_of_union_of(
766
+ self, exprs: List[Union[BNode, URIRef]], **kwargs
767
+ ) -> Optional[Union[BNode, URIRef]]:
768
+ if not exprs:
769
+ raise ValueError("Must pass at least one")
770
+ neg_expr = BNode()
771
+ self.graph.add((neg_expr, OWL.complementOf, self._union_of(exprs)), **kwargs)
772
+ return neg_expr
773
+
774
+ def _intersection_of(
775
+ self, exprs: List[Union[BNode, URIRef]], **kwargs
776
+ ) -> Optional[Union[BNode, URIRef]]:
777
+ return self._boolean_expression(exprs, OWL.intersectionOf, **kwargs)
778
+
779
+ def _union_of(
780
+ self, exprs: List[Union[BNode, URIRef]], **kwargs
781
+ ) -> Optional[Union[BNode, URIRef]]:
782
+ return self._boolean_expression(exprs, OWL.unionOf, **kwargs)
783
+
784
+ def _exactly_one_of(self, exprs: List[Union[BNode, URIRef]]) -> Optional[Union[BNode, URIRef]]:
785
+ if not exprs:
786
+ raise ValueError("Must pass at least one")
787
+ if len(exprs) == 1:
788
+ return exprs[0]
789
+ sub_exprs = []
790
+ for i, x in enumerate(exprs):
791
+ rest = exprs[0:i] + exprs[i + 1 :]
792
+ neg_expr = self._complement_of_union_of(rest)
793
+ sub_exprs.append(self._intersection_of([x, neg_expr]))
794
+ return self._union_of(sub_exprs)
795
+
796
+ def _boolean_expression(
797
+ self,
798
+ exprs: List[Union[BNode, URIRef]],
799
+ predicate: URIRef,
800
+ node: Optional[URIRef] = None,
801
+ owl_types: Set[OWL_TYPE] = None,
802
+ ) -> Optional[Union[BNode, URIRef]]:
803
+ graph = self.graph
804
+ if len(exprs) == 0:
567
805
  return None
806
+ elif len(exprs) == 1:
807
+ return exprs[0]
808
+ else:
809
+ if node is None:
810
+ node = BNode()
811
+ listnode = BNode()
812
+ Collection(graph, listnode, exprs)
813
+ graph.add((node, predicate, listnode))
814
+ if owl_types is None:
815
+ owl_types = set()
816
+ owl_types = owl_types.union(self._get_owltypes(set(), exprs))
817
+ if len(owl_types) == 1:
818
+ if RDFS.Literal in owl_types:
819
+ graph.add((node, RDF.type, RDFS.Datatype))
820
+ return node
568
821
 
569
822
  def _range_is_datatype(self, slot: SlotDefinition) -> bool:
570
823
  if self.type_objects:
@@ -585,26 +838,53 @@ class OwlSchemaGenerator(Generator):
585
838
  else:
586
839
  return self._class_uri(ClassDefinitionName(slot.range))
587
840
 
588
- def _class_uri(self, cn: ClassDefinitionName) -> URIRef:
589
- c = self.schema.classes[cn]
590
- return URIRef(c.definition_uri)
841
+ def _class_uri(self, cn: Union[str, ClassDefinitionName]) -> URIRef:
842
+ c = self.schemaview.get_class(cn)
843
+ return URIRef(self.schemaview.get_uri(c, expand=True, native=self.use_native_uris))
591
844
 
592
- def _enum_uri(self, en: EnumDefinitionName) -> URIRef:
593
- e = self.schema.enums[en]
845
+ def _enum_uri(self, en: Union[str, EnumDefinitionName]) -> URIRef:
846
+ sv = self.schemaview
847
+ e = sv.get_enum(en)
848
+ uri = e.enum_uri
849
+ if not uri:
850
+ uri = f"{sv.schema.default_prefix}:{camelcase(en)}"
594
851
  # TODO: allow control over URIs
595
- return URIRef(e.definition_uri)
852
+ return URIRef(self.schemaview.expand_curie(uri))
596
853
 
597
- def _prop_uri(self, pn: SlotDefinitionName) -> URIRef:
598
- p = self.schema.slots.get(pn, None)
599
- if p is not None and p.slot_uri is not None:
600
- return self.namespaces.uri_for(p.slot_uri)
854
+ def _prop_uri(self, pn: Union[SlotDefinition, SlotDefinitionName]) -> URIRef:
855
+ if isinstance(pn, SlotDefinition):
856
+ p = pn
601
857
  else:
602
- logging.error(self.schema.slots)
603
- raise Exception(f"No slot_uri for {pn} // {p}")
604
-
605
- def _type_uri(self, tn: TypeDefinitionName) -> URIRef:
606
- t = self.schema.types[tn]
607
- return URIRef(t.definition_uri)
858
+ p = self.schemaview.get_slot(pn, attributes=True)
859
+ if not p:
860
+ raise ValueError(f"No such slot or attribute: {pn}")
861
+ try:
862
+ return URIRef(self.schemaview.get_uri(p, expand=True, native=self.use_native_uris))
863
+ except (KeyError, ValueError):
864
+ # TODO: fix this upstream in schemaview
865
+ default_prefix = self.schemaview.schema.default_prefix or ""
866
+ return URIRef(self.schemaview.expand_curie(f"{default_prefix}:{underscore(p.name)}"))
867
+
868
+ def _type_uri(self, tn: TypeDefinitionName, native: bool = None) -> URIRef:
869
+ if native is None:
870
+ # never use native unless type shadowing with objects is enabled
871
+ native = self.type_objects
872
+ if native:
873
+ # UGLY HACK: Currently schemaview does not camelcase types
874
+ e = self.schemaview.get_element(tn, imports=True)
875
+ if e.from_schema is not None:
876
+ schema = next(
877
+ sc for sc in self.schemaview.schema_map.values() if sc.id == e.from_schema
878
+ )
879
+ pfx = schema.default_prefix
880
+ if pfx == "linkml":
881
+ return URIRef(self.schemaview.expand_curie(f"{pfx}:{camelcase(tn)}"))
882
+ t = self.schemaview.get_type(tn)
883
+ expanded = self.schemaview.get_uri(t, expand=True, native=native)
884
+ if expanded.startswith("xsd:"):
885
+ # TODO: fix upstream in schemaview; default_curi_maps is different on windows
886
+ return XSD[expanded[4:]]
887
+ return URIRef(expanded)
608
888
 
609
889
  def _add_metamodel_class(self, cname: str) -> None:
610
890
  metac = self.metamodel.schema.classes[cname]
@@ -613,30 +893,33 @@ class OwlSchemaGenerator(Generator):
613
893
  # self._add_element_properties(metac_uri, metac)
614
894
 
615
895
  def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
896
+ sv = self.schemaview
897
+ if slot.range is None:
898
+ range = self.schemaview.schema.default_range
899
+ else:
900
+ range = slot.range
616
901
  if self.type_objects:
617
902
  return OWL.ObjectProperty
618
- elif slot.range in self.schema.classes:
903
+ is_literal_vals = self.slot_is_literal_map[slot.name]
904
+ if len(is_literal_vals) > 1:
905
+ logging.warning(f"Ambiguous type for: {slot.name}")
906
+ if range is None:
907
+ if not is_literal_vals:
908
+ logging.warning(f"Guessing type for {slot.name}")
909
+ return OWL.ObjectProperty
910
+ if (list(is_literal_vals))[0]:
911
+ return OWL.DatatypeProperty
912
+ else:
913
+ return OWL.ObjectProperty
914
+ elif range in sv.all_classes():
619
915
  return OWL.ObjectProperty
620
- elif slot.range in self.schema.enums:
916
+ elif range in sv.all_enums():
621
917
  return OWL.ObjectProperty
622
- elif slot.range in self.schema.types:
918
+ elif range in sv.all_types():
623
919
  return OWL.DatatypeProperty
624
920
  else:
625
921
  raise Exception(f"Unknown range: {slot.range}")
626
922
 
627
- # @Deprecated
628
- def is_slot_object_property(self, slot: SlotDefinition) -> bool:
629
- if self.type_objects:
630
- return True
631
- elif slot.range in self.schema.classes:
632
- return True
633
- elif slot.range in self.schema.enums:
634
- return True
635
- elif slot.range in self.schema.types:
636
- return False
637
- else:
638
- raise Exception(f"Unknown range: {slot.range}")
639
-
640
923
 
641
924
  @shared_arguments(OwlSchemaGenerator)
642
925
  @click.command()
@@ -650,13 +933,13 @@ class OwlSchemaGenerator(Generator):
650
933
  )
651
934
  @click.option(
652
935
  "--type-objects/--no-type-objects",
653
- default=True,
936
+ default=False,
654
937
  show_default=True,
655
938
  help="If true, will model linkml types as objects, not literals",
656
939
  )
657
940
  @click.option(
658
941
  "--metaclasses/--no-metaclasses",
659
- default=True,
942
+ default=False,
660
943
  show_default=True,
661
944
  help="If true, include linkml metamodel classes as metaclasses. Note this introduces punning in OWL-DL",
662
945
  )
@@ -695,11 +978,11 @@ def cli(yamlfile, metadata_profile: str, **kwargs):
695
978
  For more info, see: https://linkml.io/linkml/generators/owl
696
979
  """
697
980
  if metadata_profile is not None:
698
- metadata_profile = MetadataProfile(metadata_profile)
981
+ metadata_profiles = [MetadataProfile(metadata_profile)]
699
982
  else:
700
- metadata_profile = MetadataProfile.linkml
983
+ metadata_profiles = [MetadataProfile.linkml]
701
984
  print(
702
- OwlSchemaGenerator(yamlfile, metadata_profile=metadata_profile, **kwargs).serialize(
985
+ OwlSchemaGenerator(yamlfile, metadata_profiles=metadata_profiles, **kwargs).serialize(
703
986
  **kwargs
704
987
  )
705
988
  )