linkml 1.8.1__py3-none-any.whl → 1.8.3__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.
- linkml/cli/__init__.py +0 -0
- linkml/cli/__main__.py +4 -0
- linkml/cli/main.py +130 -0
- linkml/generators/common/build.py +105 -0
- linkml/generators/common/ifabsent_processor.py +286 -0
- linkml/generators/common/lifecycle.py +124 -0
- linkml/generators/common/template.py +89 -0
- linkml/generators/csvgen.py +1 -1
- linkml/generators/docgen/slot.md.jinja2 +4 -0
- linkml/generators/docgen.py +1 -1
- linkml/generators/dotgen.py +1 -1
- linkml/generators/erdiagramgen.py +1 -1
- linkml/generators/excelgen.py +1 -1
- linkml/generators/golanggen.py +1 -1
- linkml/generators/golrgen.py +1 -1
- linkml/generators/graphqlgen.py +1 -1
- linkml/generators/javagen.py +1 -1
- linkml/generators/jsonldcontextgen.py +4 -5
- linkml/generators/jsonldgen.py +1 -1
- linkml/generators/jsonschemagen.py +69 -22
- linkml/generators/linkmlgen.py +1 -1
- linkml/generators/markdowngen.py +1 -1
- linkml/generators/namespacegen.py +1 -1
- linkml/generators/oocodegen.py +2 -1
- linkml/generators/owlgen.py +1 -1
- linkml/generators/plantumlgen.py +1 -1
- linkml/generators/prefixmapgen.py +1 -1
- linkml/generators/projectgen.py +1 -1
- linkml/generators/protogen.py +1 -1
- linkml/generators/pydanticgen/__init__.py +8 -3
- linkml/generators/pydanticgen/array.py +114 -194
- linkml/generators/pydanticgen/build.py +64 -25
- linkml/generators/pydanticgen/includes.py +1 -31
- linkml/generators/pydanticgen/pydanticgen.py +621 -276
- linkml/generators/pydanticgen/template.py +152 -184
- linkml/generators/pydanticgen/templates/attribute.py.jinja +9 -7
- linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -13
- linkml/generators/pydanticgen/templates/class.py.jinja +2 -2
- linkml/generators/pydanticgen/templates/footer.py.jinja +2 -10
- linkml/generators/pydanticgen/templates/module.py.jinja +2 -2
- linkml/generators/pydanticgen/templates/validator.py.jinja +0 -4
- linkml/generators/python/__init__.py +1 -0
- linkml/generators/python/python_ifabsent_processor.py +92 -0
- linkml/generators/pythongen.py +19 -23
- linkml/generators/rdfgen.py +1 -1
- linkml/generators/shacl/__init__.py +1 -3
- linkml/generators/shacl/shacl_data_type.py +1 -1
- linkml/generators/shacl/shacl_ifabsent_processor.py +89 -0
- linkml/generators/shaclgen.py +11 -5
- linkml/generators/shexgen.py +1 -1
- linkml/generators/sparqlgen.py +1 -1
- linkml/generators/sqlalchemygen.py +1 -1
- linkml/generators/sqltablegen.py +1 -1
- linkml/generators/sssomgen.py +1 -1
- linkml/generators/summarygen.py +1 -1
- linkml/generators/terminusdbgen.py +7 -4
- linkml/generators/typescriptgen.py +1 -1
- linkml/generators/yamlgen.py +1 -1
- linkml/generators/yumlgen.py +1 -1
- linkml/linter/cli.py +1 -1
- linkml/transformers/logical_model_transformer.py +117 -18
- linkml/utils/converter.py +1 -1
- linkml/utils/execute_tutorial.py +2 -0
- linkml/utils/logictools.py +142 -29
- linkml/utils/schema_builder.py +7 -6
- linkml/utils/schema_fixer.py +1 -1
- linkml/utils/sqlutils.py +1 -1
- linkml/validator/cli.py +4 -1
- linkml/validators/jsonschemavalidator.py +1 -1
- linkml/validators/sparqlvalidator.py +1 -1
- linkml/workspaces/example_runner.py +1 -1
- {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/METADATA +2 -2
- {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/RECORD +76 -68
- {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/entry_points.txt +1 -1
- linkml/generators/shacl/ifabsent_processor.py +0 -59
- linkml/utils/ifabsent_functions.py +0 -138
- {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/LICENSE +0 -0
- {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/WHEEL +0 -0
@@ -1,23 +1,67 @@
|
|
1
1
|
"""
|
2
|
-
A model transformer that transforms a schema into logical form
|
2
|
+
A model transformer that transforms a schema into logical form.
|
3
3
|
|
4
4
|
Formally, this transformer generates a logical model of the schema in which inheritance
|
5
|
-
of slots/attributes is replaced by boolean conjunctions.
|
5
|
+
of slots/attributes is replaced by boolean conjunctions, and the final form is simplified.
|
6
|
+
|
7
|
+
For example, given a slot `s`:
|
8
|
+
|
9
|
+
```
|
10
|
+
slots:
|
11
|
+
s:
|
12
|
+
range: integer
|
13
|
+
minimum_value: 1
|
14
|
+
```
|
15
|
+
|
16
|
+
Which is reused and refined by C and C (where C is_a D):
|
17
|
+
|
18
|
+
```
|
19
|
+
classes:
|
20
|
+
D:
|
21
|
+
slots:
|
22
|
+
- s
|
23
|
+
slot_usage:
|
24
|
+
s:
|
25
|
+
minimum_value: 2
|
26
|
+
C:
|
27
|
+
is_a: D
|
28
|
+
slot_usage:
|
29
|
+
s:
|
30
|
+
minimum_value: 3
|
31
|
+
```
|
32
|
+
|
33
|
+
The hierarchy unrolling step will create an attribute `s` for C that is the conjunction of usages for that
|
34
|
+
class, ancestors, and the base slot:
|
35
|
+
|
36
|
+
```
|
37
|
+
s:
|
38
|
+
range: integer
|
39
|
+
all_of:
|
40
|
+
- minimum_value: 1
|
41
|
+
- minimum_value: 2
|
42
|
+
- minimum_value: 3
|
43
|
+
```
|
44
|
+
|
45
|
+
This is then simplified using symbolic reasoning to:
|
46
|
+
|
47
|
+
```
|
48
|
+
s:
|
49
|
+
range: integer
|
50
|
+
minimum_value: 3
|
51
|
+
```
|
52
|
+
|
53
|
+
See logictools.py for the symbolic reasoning engine.
|
54
|
+
|
6
55
|
|
7
|
-
For example, given a slot s and two classes C and D, where C is_a D. If both C and D
|
8
|
-
place constraints on s (for example, C may refine the range, or make more restricted
|
9
|
-
value constraints), then an attribute is generated for C that for which the conjunction
|
10
|
-
of constraints in both classes hold.
|
11
56
|
|
12
|
-
These logical constraints are then simplified by translating to disjunctive normal form,
|
13
|
-
and applying simplification rules.
|
14
57
|
"""
|
15
58
|
|
16
59
|
import logging
|
17
60
|
from copy import deepcopy
|
18
61
|
from dataclasses import dataclass
|
19
|
-
from typing import Any, Callable, Dict, Iterator, List, Union
|
62
|
+
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
20
63
|
|
64
|
+
from linkml_runtime import SchemaView
|
21
65
|
from linkml_runtime.dumpers import json_dumper
|
22
66
|
from linkml_runtime.linkml_model import ClassDefinitionName, SchemaDefinition
|
23
67
|
from linkml_runtime.linkml_model.meta import (
|
@@ -37,6 +81,8 @@ HERITABLE_METASLOT = [
|
|
37
81
|
"range_expression",
|
38
82
|
"minimum_value",
|
39
83
|
"maximum_value",
|
84
|
+
"minimum_cardinality",
|
85
|
+
"maximum_cardinality",
|
40
86
|
"pattern",
|
41
87
|
"structured_pattern",
|
42
88
|
"required",
|
@@ -64,6 +110,8 @@ DIRECT_ROLL_DOWN = ["multivalued", "key", "identifier", "designates_type"]
|
|
64
110
|
HERITABLE_TYPE_METASLOT = [
|
65
111
|
"minimum_value",
|
66
112
|
"maximum_value",
|
113
|
+
# "minimum_cardinality",
|
114
|
+
# "maximum_cardinality",
|
67
115
|
"pattern",
|
68
116
|
"structured_pattern",
|
69
117
|
"all_of",
|
@@ -103,6 +151,7 @@ class LogicalModelTransformer(ModelTransformer):
|
|
103
151
|
>>> _ = sb.add_class("Person", slots={"age": {"range": "integer"}}, is_a="Thing")
|
104
152
|
>>> _ = sb.add_class("Organization", slots={"category": {"range": "OrganizationType"}}, is_a="Thing")
|
105
153
|
>>> _ = sb.add_enum("OrganizationType", ["commercial", "non-profit"])
|
154
|
+
>>> _ = sb.add_defaults()
|
106
155
|
>>> from linkml.transformers.logical_model_transformer import LogicalModelTransformer
|
107
156
|
>>> tr = LogicalModelTransformer()
|
108
157
|
>>> tr.set_schema(sb.schema)
|
@@ -113,8 +162,10 @@ class LogicalModelTransformer(ModelTransformer):
|
|
113
162
|
attributes:
|
114
163
|
id:
|
115
164
|
name: id
|
165
|
+
range: string
|
116
166
|
name:
|
117
167
|
name: name
|
168
|
+
range: string
|
118
169
|
age:
|
119
170
|
name: age
|
120
171
|
range: integer
|
@@ -142,6 +193,10 @@ class LogicalModelTransformer(ModelTransformer):
|
|
142
193
|
|
143
194
|
...
|
144
195
|
|
196
|
+
This has "compiled away" the hierarchy. The type of the values of 'entities' for Container can
|
197
|
+
be checked against the any_of expression, without needing to know the full class hierarchy.
|
198
|
+
This is useful when compiling to a framework that does not support inheritance.
|
199
|
+
|
145
200
|
If you are compiling to a framework that does have inheritance, then you can specify that these are
|
146
201
|
preserved:
|
147
202
|
|
@@ -173,6 +228,9 @@ class LogicalModelTransformer(ModelTransformer):
|
|
173
228
|
name: entities2
|
174
229
|
multivalued: true
|
175
230
|
any_of:
|
231
|
+
- range: Organization
|
232
|
+
- range: Person
|
233
|
+
|
176
234
|
...
|
177
235
|
|
178
236
|
Note that defaults are always applied after reasoning. So if you set a default_range to
|
@@ -211,6 +269,10 @@ class LogicalModelTransformer(ModelTransformer):
|
|
211
269
|
minimum_value: 40
|
212
270
|
maximum_value: 60
|
213
271
|
|
272
|
+
Here the slot-level constraints seem to have been "overridden". In fact, LinkML is monotonic, and there
|
273
|
+
is no overriding. In fact the constraints have been combined as a conjunction (And), and then the resulting
|
274
|
+
entailed attribute has been *simplified* (age >= 40 & age >= 0 ==> age >= 40).
|
275
|
+
|
214
276
|
The reasoning engine can also detect unsatisfiable constraints:
|
215
277
|
|
216
278
|
>>> from linkml.transformers.logical_model_transformer import UnsatisfiableAttribute
|
@@ -227,6 +289,9 @@ class LogicalModelTransformer(ModelTransformer):
|
|
227
289
|
Let's get rid of the problematic class:
|
228
290
|
|
229
291
|
>>> del sb.schema.classes["YoungAdult"]
|
292
|
+
|
293
|
+
And try a different example - we will try and override the range of the age slot with a string:
|
294
|
+
|
230
295
|
>>> _ = sb.add_class("Person2", is_a="Person", slot_usage={"age": {"range": "string"}})
|
231
296
|
>>> tr.set_schema(sb.schema)
|
232
297
|
>>> try:
|
@@ -237,8 +302,6 @@ class LogicalModelTransformer(ModelTransformer):
|
|
237
302
|
Attribute Person2.age is unsatisfiable
|
238
303
|
<BLANKLINE>
|
239
304
|
|
240
|
-
|
241
|
-
|
242
305
|
"""
|
243
306
|
|
244
307
|
preserve_class_is_a: bool = False
|
@@ -277,16 +340,24 @@ class LogicalModelTransformer(ModelTransformer):
|
|
277
340
|
a lack of range assignment.
|
278
341
|
"""
|
279
342
|
|
280
|
-
|
343
|
+
reason_over_metamodel_slots: Optional[List[str]] = None
|
344
|
+
"""
|
345
|
+
If set, only reason over the specified metamodel slots.
|
346
|
+
"""
|
347
|
+
|
348
|
+
def transform(self, tgt_schema_name: str = None, simplify=True, force_any_of=False, **kwargs) -> Any:
|
281
349
|
"""
|
282
|
-
Transform the schema
|
350
|
+
Transform the schema using logical reasoning, materializing entailed simple attributes.
|
283
351
|
|
284
352
|
:param tgt_schema_name:
|
285
353
|
:param simplify: If True (default), simplify the schema after transformation
|
354
|
+
:param force_any_of: If True, force the use of any_of expressions for range, even for singletons
|
286
355
|
:return:
|
287
356
|
"""
|
288
357
|
sv = self.schemaview
|
289
|
-
|
358
|
+
target_schemaview = SchemaView(deepcopy(self.source_schema))
|
359
|
+
target_schemaview.merge_imports()
|
360
|
+
target_schema = target_schemaview.schema
|
290
361
|
for tn, typ in target_schema.types.items():
|
291
362
|
ancs = sv.type_ancestors(tn, reflexive=False)
|
292
363
|
logging.debug(f"Unrolling type {tn}, merging {len(ancs)}")
|
@@ -312,7 +383,7 @@ class LogicalModelTransformer(ModelTransformer):
|
|
312
383
|
self._roll_down(target_schema, cn, ancs)
|
313
384
|
self.apply_defaults(target_schema)
|
314
385
|
if simplify:
|
315
|
-
self.simplify(target_schema)
|
386
|
+
self.simplify(target_schema, force_any_of=force_any_of)
|
316
387
|
if self.tidy_slots:
|
317
388
|
target_schema.slots = {}
|
318
389
|
if self.tidy_inheritance:
|
@@ -366,7 +437,8 @@ class LogicalModelTransformer(ModelTransformer):
|
|
366
437
|
:return:
|
367
438
|
"""
|
368
439
|
new_slot_dict = {}
|
369
|
-
|
440
|
+
heritable_metaslots = self.reason_over_metamodel_slots or HERITABLE_METASLOT
|
441
|
+
for p in heritable_metaslots:
|
370
442
|
pv = getattr(source, p)
|
371
443
|
if pv is not None and pv != [] and pv != {} and pv is not False:
|
372
444
|
new_slot_dict[p] = pv
|
@@ -377,7 +449,7 @@ class LogicalModelTransformer(ModelTransformer):
|
|
377
449
|
sv = self.schemaview
|
378
450
|
if source.range in sv.all_types():
|
379
451
|
typ = sv.get_type(source.range)
|
380
|
-
for p in set(HERITABLE_TYPE_METASLOT).intersection(
|
452
|
+
for p in set(HERITABLE_TYPE_METASLOT).intersection(heritable_metaslots):
|
381
453
|
pv = getattr(typ, p)
|
382
454
|
if pv is not None and pv != [] and pv != {} and pv is not False:
|
383
455
|
new_slot_dict[p] = pv
|
@@ -395,6 +467,9 @@ class LogicalModelTransformer(ModelTransformer):
|
|
395
467
|
"""
|
396
468
|
new_slot_dict = {}
|
397
469
|
for p in HERITABLE_TYPE_METASLOT:
|
470
|
+
if p in ["repr"]:
|
471
|
+
# not on anonymous types
|
472
|
+
continue
|
398
473
|
pv = getattr(source, p)
|
399
474
|
if pv is not None and pv != [] and pv != {} and pv is not False:
|
400
475
|
new_slot_dict[p] = pv
|
@@ -431,7 +506,7 @@ class LogicalModelTransformer(ModelTransformer):
|
|
431
506
|
for sx in att.all_of + att.exactly_one_of + att.none_of + att.any_of:
|
432
507
|
yield from self._collection_slot_expressions(sx, filter_function)
|
433
508
|
|
434
|
-
def simplify(self, target_schema: SchemaDefinition):
|
509
|
+
def simplify(self, target_schema: SchemaDefinition, force_any_of=False):
|
435
510
|
for cls in target_schema.classes.values():
|
436
511
|
for att in cls.attributes.values():
|
437
512
|
x = self._as_logical_expression(att)
|
@@ -441,6 +516,9 @@ class LogicalModelTransformer(ModelTransformer):
|
|
441
516
|
if logictools.is_contradiction(x):
|
442
517
|
raise UnsatisfiableAttribute(f"Attribute {cls.name}.{att.name} is unsatisfiable")
|
443
518
|
self._simplify_member_ofs(x)
|
519
|
+
if force_any_of:
|
520
|
+
if not isinstance(x, logictools.Or):
|
521
|
+
x = logictools.Or(x)
|
444
522
|
logger.debug(f"Simplified member of: {x}")
|
445
523
|
simplified_att = self._from_logical_expression(x)
|
446
524
|
for k, v in simplified_att.__dict__.items():
|
@@ -524,6 +602,10 @@ class LogicalModelTransformer(ModelTransformer):
|
|
524
602
|
def _value_var(self) -> logictools.Variable:
|
525
603
|
return logictools.Variable("value")
|
526
604
|
|
605
|
+
@property
|
606
|
+
def _length_var(self) -> logictools.Variable:
|
607
|
+
return logictools.Variable("length")
|
608
|
+
|
527
609
|
@property
|
528
610
|
def _type_var(self) -> logictools.Variable:
|
529
611
|
return logictools.Variable("type")
|
@@ -535,10 +617,15 @@ class LogicalModelTransformer(ModelTransformer):
|
|
535
617
|
exprs = []
|
536
618
|
value_var = self._value_var
|
537
619
|
type_var = self._type_var
|
620
|
+
length_var = self._length_var
|
538
621
|
if slot_expression.minimum_value is not None:
|
539
622
|
exprs.append(value_var >= slot_expression.minimum_value)
|
540
623
|
if slot_expression.maximum_value is not None:
|
541
624
|
exprs.append(value_var <= slot_expression.maximum_value)
|
625
|
+
if slot_expression.minimum_cardinality is not None:
|
626
|
+
exprs.append(length_var >= slot_expression.minimum_cardinality)
|
627
|
+
if slot_expression.maximum_cardinality is not None:
|
628
|
+
exprs.append(length_var <= slot_expression.maximum_cardinality)
|
542
629
|
if slot_expression.pattern is not None:
|
543
630
|
exprs.append(logictools.Term(MATCHES_PREDICATE, value_var, slot_expression.pattern))
|
544
631
|
if slot_expression.range:
|
@@ -576,6 +663,7 @@ class LogicalModelTransformer(ModelTransformer):
|
|
576
663
|
"none_of",
|
577
664
|
"any_of",
|
578
665
|
]:
|
666
|
+
# already accounted for
|
579
667
|
continue
|
580
668
|
v = getattr(slot_expression, p, None)
|
581
669
|
if v is not None and v != [] and v != {}:
|
@@ -624,6 +712,17 @@ class LogicalModelTransformer(ModelTransformer):
|
|
624
712
|
return AnonymousSlotExpression(maximum_value=val - 1)
|
625
713
|
else:
|
626
714
|
raise ValueError(f"Unknown predicate {expr.predicate} for {var}")
|
715
|
+
elif var == self._length_var:
|
716
|
+
if expr.predicate == ">=":
|
717
|
+
return AnonymousSlotExpression(minimum_cardinality=val)
|
718
|
+
elif expr.predicate == ">":
|
719
|
+
return AnonymousSlotExpression(minimum_cardinality=val + 1)
|
720
|
+
elif expr.predicate == "<=":
|
721
|
+
return AnonymousSlotExpression(maximum_cardinality=val)
|
722
|
+
elif expr.predicate == "<":
|
723
|
+
return AnonymousSlotExpression(maximum_cardinality=val - 1)
|
724
|
+
else:
|
725
|
+
raise ValueError(f"Unknown predicate {expr.predicate} for {var}")
|
627
726
|
elif var == self._type_var:
|
628
727
|
if expr.predicate == "in":
|
629
728
|
if val == logictools.UniversalSet():
|
linkml/utils/converter.py
CHANGED
linkml/utils/execute_tutorial.py
CHANGED
@@ -189,6 +189,8 @@ def parse_file_to_blocks(input) -> List[Block]:
|
|
189
189
|
@click.version_option(__version__, "-V", "--version")
|
190
190
|
def cli(inputs, directory):
|
191
191
|
"""
|
192
|
+
Execute a tutorial markdown file (eg. those in the /docs/intro/ directory) and
|
193
|
+
save the outputs in the given directory
|
192
194
|
|
193
195
|
Example:
|
194
196
|
|
linkml/utils/logictools.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import operator
|
2
2
|
from copy import deepcopy
|
3
3
|
from itertools import product
|
4
|
-
from typing import Any, Collection, List, Optional, Tuple
|
4
|
+
from typing import Any, Collection, List, Optional, Tuple, Type
|
5
5
|
|
6
6
|
|
7
7
|
def member_of(item: Any, collection: Collection[Any]) -> bool:
|
@@ -25,6 +25,8 @@ OPS = {
|
|
25
25
|
|
26
26
|
|
27
27
|
class Expression:
|
28
|
+
"""Base class for logic expressions"""
|
29
|
+
|
28
30
|
def __eq__(self, other: "Expression"):
|
29
31
|
if isinstance(other, Expression):
|
30
32
|
return self._ordered_str() == other._ordered_str()
|
@@ -60,6 +62,7 @@ class Expression:
|
|
60
62
|
|
61
63
|
def __contains__(self, other: Any):
|
62
64
|
return Term("contains", self, other)
|
65
|
+
return Term("contains", self, other)
|
63
66
|
|
64
67
|
def _ordered_str(self, **kwargs):
|
65
68
|
return str(self)
|
@@ -69,6 +72,8 @@ class Expression:
|
|
69
72
|
|
70
73
|
|
71
74
|
class Variable(Expression):
|
75
|
+
"""A variable in a logic expression"""
|
76
|
+
|
72
77
|
def __init__(self, name: str):
|
73
78
|
self.name = name
|
74
79
|
|
@@ -80,6 +85,8 @@ class Variable(Expression):
|
|
80
85
|
|
81
86
|
|
82
87
|
class Top(Expression):
|
88
|
+
"""True expression"""
|
89
|
+
|
83
90
|
def __str__(self):
|
84
91
|
return "T"
|
85
92
|
|
@@ -88,6 +95,8 @@ class Top(Expression):
|
|
88
95
|
|
89
96
|
|
90
97
|
class Bottom(Expression):
|
98
|
+
"""False expression"""
|
99
|
+
|
91
100
|
def __str__(self):
|
92
101
|
return "F"
|
93
102
|
|
@@ -96,6 +105,8 @@ class Bottom(Expression):
|
|
96
105
|
|
97
106
|
|
98
107
|
class Not(Expression):
|
108
|
+
"""Negation of an expression"""
|
109
|
+
|
99
110
|
def __init__(self, operand: Expression):
|
100
111
|
self.operand = operand
|
101
112
|
|
@@ -107,6 +118,8 @@ class Not(Expression):
|
|
107
118
|
|
108
119
|
|
109
120
|
class And(Expression):
|
121
|
+
"""Conjunction of expressions"""
|
122
|
+
|
110
123
|
def __init__(self, *operands: Expression):
|
111
124
|
self.operands: List[Expression] = list(operands)
|
112
125
|
|
@@ -306,6 +319,45 @@ def distribute_and_over_or(expr: Expression) -> Expression:
|
|
306
319
|
|
307
320
|
|
308
321
|
def is_contradiction(expr: Expression, apply_dnf=False) -> Optional[bool]:
|
322
|
+
"""
|
323
|
+
Check if the given expression is a contradiction.
|
324
|
+
|
325
|
+
>>> a = Variable("a")
|
326
|
+
>>> b = Variable("b")
|
327
|
+
>>> c = Variable("c")
|
328
|
+
>>> d = Variable("d")
|
329
|
+
>>> F = Bottom()
|
330
|
+
>>> T = Top()
|
331
|
+
>>> is_contradiction(T)
|
332
|
+
False
|
333
|
+
>>> is_contradiction(F)
|
334
|
+
True
|
335
|
+
>>> is_contradiction(~T)
|
336
|
+
True
|
337
|
+
>>> is_contradiction(~F)
|
338
|
+
False
|
339
|
+
>>> is_contradiction(~~T)
|
340
|
+
False
|
341
|
+
>>> is_contradiction(~~F)
|
342
|
+
True
|
343
|
+
>>> is_contradiction(F & F)
|
344
|
+
True
|
345
|
+
>>> is_contradiction(F & T)
|
346
|
+
True
|
347
|
+
>>> is_contradiction(F | T)
|
348
|
+
False
|
349
|
+
>>> is_contradiction(a & F )
|
350
|
+
True
|
351
|
+
>>> is_contradiction(a & ~a)
|
352
|
+
True
|
353
|
+
>>> assert is_contradiction(a & T) is None
|
354
|
+
>>> assert is_contradiction(a) is None
|
355
|
+
>>> assert is_contradiction(And(a)) is None
|
356
|
+
|
357
|
+
:param expr:
|
358
|
+
:param apply_dnf:
|
359
|
+
:return:
|
360
|
+
"""
|
309
361
|
if apply_dnf:
|
310
362
|
expr = to_dnf(expr)
|
311
363
|
expr = simplify(expr)
|
@@ -354,6 +406,32 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
|
|
354
406
|
"""
|
355
407
|
Two-valued logic
|
356
408
|
|
409
|
+
>>> a = Variable("a")
|
410
|
+
>>> b = Variable("b")
|
411
|
+
>>> c = Variable("c")
|
412
|
+
>>> d = Variable("d")
|
413
|
+
>>> F = Bottom()
|
414
|
+
>>> T = Top()
|
415
|
+
>>> evals_to_true(T)
|
416
|
+
True
|
417
|
+
>>> evals_to_true(F)
|
418
|
+
False
|
419
|
+
>>> evals_to_true(~T)
|
420
|
+
False
|
421
|
+
>>> evals_to_true(~F)
|
422
|
+
True
|
423
|
+
>>> evals_to_true(a & b)
|
424
|
+
False
|
425
|
+
>>> evals_to_true(a | b)
|
426
|
+
False
|
427
|
+
>>> evals_to_true(a & a)
|
428
|
+
False
|
429
|
+
|
430
|
+
Note that this does not perform unification, so the following does not evaluate to True
|
431
|
+
|
432
|
+
>>> evals_to_true(a & ~a)
|
433
|
+
False
|
434
|
+
|
357
435
|
:param expr:
|
358
436
|
:param apply_dnf:
|
359
437
|
:return:
|
@@ -363,7 +441,7 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
|
|
363
441
|
if isinstance(expr, And):
|
364
442
|
return all([evals_to_true(operand) for operand in expr.operands])
|
365
443
|
elif isinstance(expr, Not):
|
366
|
-
if is_contradiction(expr.operand) is
|
444
|
+
if is_contradiction(expr.operand) is True:
|
367
445
|
return True
|
368
446
|
else:
|
369
447
|
return False
|
@@ -378,32 +456,6 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
|
|
378
456
|
return False
|
379
457
|
|
380
458
|
|
381
|
-
def is_tautology(expr):
|
382
|
-
"""
|
383
|
-
Checks if the given propositional logic formula is a tautology.
|
384
|
-
|
385
|
-
A formula is a tautology if it is true under every possible valuation of its variables.
|
386
|
-
|
387
|
-
Args:
|
388
|
-
expr (Expression): The input formula which is an instance of Expression or its subclasses.
|
389
|
-
|
390
|
-
Returns:
|
391
|
-
bool: True if the formula is a tautology, False otherwise.
|
392
|
-
"""
|
393
|
-
if isinstance(expr, Or):
|
394
|
-
for i in range(len(expr.operands)):
|
395
|
-
for j in range(i + 1, len(expr.operands)):
|
396
|
-
if isinstance(expr.operands[i], Not) and expr.operands[i].operand == expr.operands[j]:
|
397
|
-
return True
|
398
|
-
elif isinstance(expr.operands[j], Not) and expr.operands[j].operand == expr.operands[i]:
|
399
|
-
return True
|
400
|
-
elif isinstance(expr, And):
|
401
|
-
for operand in expr.operands:
|
402
|
-
if is_tautology(operand):
|
403
|
-
return True
|
404
|
-
return False
|
405
|
-
|
406
|
-
|
407
459
|
def eliminate_redundant(expr: Expression) -> Expression:
|
408
460
|
if isinstance(expr, And):
|
409
461
|
visited = set()
|
@@ -444,6 +496,23 @@ def simplify(expr: Expression) -> Expression:
|
|
444
496
|
"""
|
445
497
|
Simplifies the given propositional logic formula.
|
446
498
|
|
499
|
+
>>> a = Variable("a")
|
500
|
+
>>> b = Variable("b")
|
501
|
+
>>> c = Variable("c")
|
502
|
+
>>> d = Variable("d")
|
503
|
+
>>> simplify(a & b)
|
504
|
+
(a & b)
|
505
|
+
>>> simplify(a | b)
|
506
|
+
(a | b)
|
507
|
+
>>> simplify(a & a)
|
508
|
+
a
|
509
|
+
>>> simplify(a | a)
|
510
|
+
a
|
511
|
+
>>> simplify(a & ~a)
|
512
|
+
F
|
513
|
+
>>> simplify(a | ~a)
|
514
|
+
T
|
515
|
+
|
447
516
|
:param expr:
|
448
517
|
:return:
|
449
518
|
"""
|
@@ -521,6 +590,30 @@ COMPOSITIONS = {
|
|
521
590
|
|
522
591
|
|
523
592
|
def _unsat(x: Expression, y: Expression) -> bool:
|
593
|
+
"""
|
594
|
+
Check if two terms are unsatisfiable when combined.
|
595
|
+
|
596
|
+
>>> a = Variable("a")
|
597
|
+
>>> b = Variable("b")
|
598
|
+
>>> _unsat( a < b, a > b)
|
599
|
+
True
|
600
|
+
>>> _unsat( a < b, a < b)
|
601
|
+
False
|
602
|
+
>>> _unsat( a <= b, b >= a)
|
603
|
+
False
|
604
|
+
>>> _unsat( a < b, a <= b)
|
605
|
+
False
|
606
|
+
>>> _unsat( a < b, b <= a)
|
607
|
+
False
|
608
|
+
>>> _unsat( IsIn(a, [1, 2]), IsIn(a, [2, 3]))
|
609
|
+
False
|
610
|
+
>>> _unsat( IsIn(a, [1, 2]), IsIn(a, [3, 4]))
|
611
|
+
True
|
612
|
+
|
613
|
+
:param x:
|
614
|
+
:param y:
|
615
|
+
:return:
|
616
|
+
"""
|
524
617
|
if isinstance(x, Term) and isinstance(y, Term):
|
525
618
|
if x.operands[0] == y.operands[0]:
|
526
619
|
xp = x.predicate
|
@@ -547,7 +640,27 @@ def _unsat(x: Expression, y: Expression) -> bool:
|
|
547
640
|
return False
|
548
641
|
|
549
642
|
|
550
|
-
def compose_operators(boolean_op, op1, v1, op2, v2) -> Optional[Tuple]:
|
643
|
+
def compose_operators(boolean_op: Type[Expression], op1: str, v1: Any, op2: str, v2: Any) -> Optional[Tuple]:
|
644
|
+
"""
|
645
|
+
Compose two expressions.
|
646
|
+
|
647
|
+
>>> assert compose_operators(And, '<', 1, '<', 2) == (operator.__lt__, 1)
|
648
|
+
>>> assert compose_operators(Or, '<', 1, '<', 2) == (operator.__lt__, 2)
|
649
|
+
>>> assert compose_operators(Or, '=', 1, '=', 2) is None
|
650
|
+
>>> assert compose_operators(And, '=', 1, '=', 2) is None
|
651
|
+
>>> assert compose_operators(And, "in", [1, 2], "in", [2, 3]) == (member_of, [2])
|
652
|
+
>>> assert compose_operators(Or, "in", [1, 2], "in", [2, 3]) == (member_of, [1, 2, 3])
|
653
|
+
>>> assert compose_operators(And, "in", [1], "in", [3]) == (member_of, [])
|
654
|
+
>>> assert compose_operators(And, "in", [1], "in", UniversalSet()) == (member_of, [1])
|
655
|
+
>>> assert compose_operators(Or, "in", [1], "in", UniversalSet()) == (member_of, UniversalSet())
|
656
|
+
|
657
|
+
:param boolean_op:
|
658
|
+
:param op1:
|
659
|
+
:param v1:
|
660
|
+
:param op2:
|
661
|
+
:param v2:
|
662
|
+
:return: New expression OR None if the expressions cannot be reduced
|
663
|
+
"""
|
551
664
|
rev_op_map = {v: k for k, v in OPS.items()}
|
552
665
|
if not (op1 in rev_op_map and op2 in rev_op_map):
|
553
666
|
return None
|
linkml/utils/schema_builder.py
CHANGED
@@ -101,12 +101,13 @@ class SchemaBuilder:
|
|
101
101
|
for k, v in slots.items():
|
102
102
|
cls.slots.append(k)
|
103
103
|
self.add_slot(SlotDefinition(k, **v), replace_if_present=replace_if_present)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
104
|
+
else:
|
105
|
+
for s in slots:
|
106
|
+
cls.slots.append(s.name if isinstance(s, SlotDefinition) else s)
|
107
|
+
if isinstance(s, str) and s in self.schema.slots:
|
108
|
+
# top-level slot already exists
|
109
|
+
continue
|
110
|
+
self.add_slot(s, replace_if_present=replace_if_present)
|
110
111
|
if slot_usage:
|
111
112
|
if isinstance(slot_usage, dict):
|
112
113
|
for k, v in slot_usage.items():
|
linkml/utils/schema_fixer.py
CHANGED
linkml/utils/sqlutils.py
CHANGED
linkml/validator/cli.py
CHANGED
@@ -64,7 +64,7 @@ def _resolve_loaders(loader_config: Iterable[Union[str, Dict[str, Dict[str, str]
|
|
64
64
|
DEPRECATED = "[DEPRECATED: only used in legacy mode]"
|
65
65
|
|
66
66
|
|
67
|
-
@click.command()
|
67
|
+
@click.command(name="validate")
|
68
68
|
@click.option(
|
69
69
|
"-s",
|
70
70
|
"--schema",
|
@@ -132,6 +132,9 @@ def cli(
|
|
132
132
|
include_range_class_descendants: bool,
|
133
133
|
include_context: bool,
|
134
134
|
):
|
135
|
+
"""
|
136
|
+
Validate data according to a LinkML Schema
|
137
|
+
"""
|
135
138
|
if legacy_mode:
|
136
139
|
from linkml.validators import jsonschemavalidator
|
137
140
|
|
@@ -114,7 +114,7 @@ class JsonSchemaDataValidator(DataValidator):
|
|
114
114
|
yield f"{best_error.message} in {best_error.json_path}"
|
115
115
|
|
116
116
|
|
117
|
-
@click.command()
|
117
|
+
@click.command(name="jsonschema")
|
118
118
|
@click.option("--module", "-m", help="Path to python datamodel module")
|
119
119
|
@click.option(
|
120
120
|
"--input-format",
|
@@ -85,7 +85,7 @@ class SparqlDataValidator(DataValidator):
|
|
85
85
|
return self.schema
|
86
86
|
|
87
87
|
|
88
|
-
@click.command()
|
88
|
+
@click.command(name="sparql")
|
89
89
|
@click.option("--named-graph", "-G", multiple=True, help="Constrain query to a named graph")
|
90
90
|
@click.option("--input", "-i", help="Input file to validate")
|
91
91
|
@click.option("--endpoint-url", "-U", help="URL of sparql endpoint")
|
@@ -255,7 +255,7 @@ class ExampleRunner:
|
|
255
255
|
return dict_obj
|
256
256
|
|
257
257
|
|
258
|
-
@click.command()
|
258
|
+
@click.command(name="examples")
|
259
259
|
@click.option("--schema", "-s", required=True, help="Path to linkml schema yaml file")
|
260
260
|
@click.option("--prefixes", "-P", help="Path to prefixes")
|
261
261
|
@click.option("--input-directory", "-e", help="folder containing positive examples that MUST pass validation")
|