unicode-logic-kit 0.31.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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,975 @@
1
+ """Classical (two-valued) Tarskian model theory for FOL and MSFOL.
2
+
3
+ A :class:`Structure` is a first-order "world": a non-empty domain of
4
+ individuals together with interpretations of the constant, function, and
5
+ predicate symbols (and, for MSFOL, the named sorts). :func:`satisfies`
6
+ computes the truth value of a formula in such a structure under a variable
7
+ assignment, following Tarski's recursive definition of satisfaction.
8
+
9
+ Only the classical fragment is interpreted here. Łukasiewicz (fuzzy) operators
10
+ and lambda nodes are rejected with a clear error: the former need the
11
+ many-valued evaluator, the latter must be beta-reduced / lambda-eliminated
12
+ first.
13
+
14
+ Functional style: the variable assignment dict is never mutated. When a
15
+ quantifier ranges over the domain, the binding is added to a *copy* of the
16
+ assignment for each candidate individual.
17
+
18
+ **What a many-sorted structure is.** There is ONE domain. A sort ``S`` is a
19
+ non-empty subset of it, and sorts may overlap. The sort ``S`` and the unary
20
+ predicate ``S`` are ONE symbol: an atom ``S(t)`` holds exactly when the value of
21
+ ``t`` is in the sort (a structure that names ``S`` only as a sort needs no
22
+ second table for the predicate, and one that gives both must give them the same
23
+ extension). A sorted constant ``c:S`` denotes an element of ``S``. An unsorted
24
+ constant, an unsorted variable and the value of a function may be any element of
25
+ the domain; a predicate is a relation over the whole domain. A structure that
26
+ breaks one of these laws is not a structure of the definition, and evaluating a
27
+ formula in it is an :class:`IllegalStructureError` naming the law, not a truth
28
+ value: a truth value in such a structure answers a question nobody asked. The
29
+ checks run where the evaluator reads the thing they are about (a sort when it is
30
+ read, a sorted constant when it is evaluated); :func:`check_structure` runs all
31
+ of them at once, up front.
32
+ """
33
+
34
+ import html
35
+ import operator
36
+ from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union
37
+
38
+ from ..fol.nodes import (
39
+ Node,
40
+ Variable, Constant, Number, Function,
41
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
42
+ SortedQuantifier, SortedConstant,
43
+ Count, Cardinality, SortedCount, SortedCardinality, Measure,
44
+ LukNegation, WeakConjunction, WeakDisjunction,
45
+ StrongConjunction, StrongDisjunction,
46
+ LukImplication, LukEquivalence,
47
+ LambdaVar, Lambda, Application,
48
+ )
49
+ from ..fol._fol_nodes import numeral_key
50
+ from ..fol._numeral_symbols import term_numerals
51
+ from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
52
+ from ..fol._tptp_symbols import truth_constant_word as _truth_constant_word
53
+
54
+ # Quantifier.type spellings accepted for each quantifier kind.
55
+ _FORALL = ("∀", "forall")
56
+ _EXISTS = ("∃", "exists")
57
+
58
+ #: The function symbol a :class:`Measure` term denotes. Matches ``Measure.to_z3`` and
59
+ #: ``Measure.to_prover9``, so a structure found here interprets the same symbol the
60
+ #: provers see.
61
+ _MEASURE_FUNC = ("measure", 2)
62
+
63
+ # Infix comparison predicates that are NOT equality/disequality. Their
64
+ # extensions live in Structure.predicates like any ordinary relation.
65
+ _ORDER_COMPARISONS = frozenset({"<", ">", "≤", "≥"})
66
+
67
+ #: Numeric readings of the order comparisons (see :func:`_order_value`).
68
+ _ORDER_OPS = {
69
+ "<": operator.lt, ">": operator.gt, "≤": operator.le, "≥": operator.ge,
70
+ }
71
+
72
+ # Łukasiewicz node types: two-valued Tarski cannot interpret them.
73
+ _FUZZY_TYPES = (
74
+ LukNegation, WeakConjunction, WeakDisjunction,
75
+ StrongConjunction, StrongDisjunction,
76
+ LukImplication, LukEquivalence,
77
+ )
78
+
79
+ # Lambda-calculus node types: must be eliminated before evaluation.
80
+ _LAMBDA_TYPES = (LambdaVar, Lambda, Application)
81
+
82
+
83
+ class IllegalStructureError(ValueError):
84
+ """A :class:`Structure` that is not a structure of the many-sorted definition.
85
+
86
+ Raised instead of a truth value when a formula is evaluated in a structure
87
+ that breaks one of the laws in the module docstring: a sort that is empty or
88
+ holds an element outside the domain, a name that is a sort and a unary
89
+ predicate with two different extensions, or a sorted constant whose value is
90
+ not in its sort. The message names the law and the symbols that break it.
91
+
92
+ It is also raised when a :class:`Structure` is BUILT with a table keyed in a shape
93
+ the evaluator never reads (see :class:`Structure`): such a table would be dropped
94
+ without a word and its symbol would denote the empty relation.
95
+ """
96
+
97
+
98
+ def _first_length(table: Any) -> Optional[int]:
99
+ """The one length shared by every argument tuple in ``table``, if it has one.
100
+
101
+ ``table`` is the extension of a predicate (a container of argument tuples) or the
102
+ table of a function (a mapping from argument tuples); ``None`` when it is
103
+ empty, a callable or has tuples of different lengths (a bool is a nullary
104
+ predicate's value, whose tuple has length 0).
105
+ """
106
+ if isinstance(table, bool):
107
+ return 0
108
+ try:
109
+ lengths = {len(row) for row in table}
110
+ except TypeError:
111
+ return None
112
+ return lengths.pop() if len(lengths) == 1 else None
113
+
114
+
115
+ def _table_key_problems(constants: Mapping[Any, Any], functions: Mapping[Any, Any],
116
+ predicates: Mapping[Any, Any], sorts: Mapping[Any, Any]) -> List[str]:
117
+ """Every table key of a structure that the evaluator would never read.
118
+
119
+ A function or predicate table is read under ``(name, arity)`` (``name`` a ``str``,
120
+ ``arity`` a non-negative ``int``); a constant and a sort are read under their name, a
121
+ ``str``. A table under any other key is not an extension of anything: it is dropped,
122
+ and the symbol it was meant for denotes the empty relation (a predicate) or has no
123
+ value (a function). One line per offending key, naming the table, the key and what
124
+ to write instead.
125
+ """
126
+ problems: List[str] = []
127
+ for table_name, table, consequence in (
128
+ ("functions", functions, "the function would have no value"),
129
+ ("predicates", predicates, "the predicate would denote the empty relation")):
130
+ for key, value in table.items():
131
+ if (isinstance(key, tuple) and len(key) == 2 and isinstance(key[0], str)
132
+ and isinstance(key[1], int) and not isinstance(key[1], bool) and key[1] >= 0):
133
+ continue
134
+ if isinstance(key, str):
135
+ length = _first_length(value)
136
+ arity = "arity" if length is None else str(length)
137
+ problems.append(
138
+ f"{table_name}[{key!r}] is keyed by the bare name, but a table is read "
139
+ f"under (name, arity); this one would never be read and {consequence}. "
140
+ f"Write the key as ({key!r}, {arity})")
141
+ else:
142
+ problems.append(
143
+ f"{table_name}[{key!r}] is not keyed by (name, arity), a pair of a str "
144
+ f"and a non-negative int, so it would never be read and {consequence}")
145
+ for table_name, table in (("constants", constants), ("sorts", sorts)):
146
+ for key in table:
147
+ if not isinstance(key, str):
148
+ problems.append(
149
+ f"{table_name}[{key!r}] is not keyed by a name (a str), so it would "
150
+ f"never be read")
151
+ return problems
152
+
153
+
154
+ class Structure:
155
+ """A first-order structure (model / "world") over a non-empty domain.
156
+
157
+ Args:
158
+ domain: a non-empty iterable of individuals (any hashable Python
159
+ values, e.g. ``{"alice", "bob"}`` or ``{0, 1}``). Stored as a
160
+ tuple preserving order; duplicates are dropped.
161
+ constants: maps a constant NAME (str) to an individual in the domain.
162
+ Interprets both :class:`Constant` and :class:`SortedConstant`.
163
+ :class:`Number` ``n`` defaults to the individual ``n`` unless the
164
+ name of its VALUE is overridden here (``"1"`` for ``1`` and for
165
+ ``1.0``, ``"2.5"`` for ``2.5``: one constant per value, see
166
+ :func:`~unicode_logic_kit.fol._fol_nodes.numeral_key`) — except where ``n``
167
+ is compared with a cardinality, which always reads it as the numeral
168
+ itself.
169
+ functions: maps ``(name, arity)`` to either a Python callable
170
+ ``(*args) -> individual`` or a plain dict ``{arg_tuple: individual}``.
171
+ A dict is looked up by the tuple of evaluated argument individuals.
172
+ predicates: maps ``(name, arity)`` to the relation's extension — a set
173
+ (or any container) of argument tuples of individuals. A nullary
174
+ predicate maps ``(name, 0)`` to a bool. A missing predicate denotes
175
+ the empty relation (always false), except for the order comparisons
176
+ ``< > ≤ ≥``, which fall back to arithmetic when both operands
177
+ evaluate to numbers — see :func:`_order_value`.
178
+ sorts: maps a sort name (str) to its universe — a NON-EMPTY subset of
179
+ the domain. Used by :class:`SortedQuantifier`, the sorted counting
180
+ quantifiers and cardinality terms to restrict the range, and read
181
+ by an atom ``S(t)`` over a name ``S`` that is a sort and has no
182
+ predicate table of its own: a sort and the unary predicate of the
183
+ same name are one symbol. A structure that lists both must give
184
+ them the same extension. A sorted constant ``c:S`` must denote an
185
+ element of ``S``. Anything else is an
186
+ :class:`IllegalStructureError` when the formula is evaluated.
187
+
188
+ All mapping arguments default to empty, so a bare ``Structure(domain)`` is
189
+ a valid (if symbol-free) world.
190
+
191
+ **Keys are checked when the structure is built.** ``functions`` and ``predicates``
192
+ are keyed by ``(name, arity)`` and ``constants`` and ``sorts`` by the name; the
193
+ evaluator reads a table under no other key. A table under a bare name
194
+ (``predicates={"Q": {(0,)}}``) would therefore be dropped without a word and ``Q``
195
+ would denote the empty relation, so a key of any other shape is refused with an
196
+ :class:`IllegalStructureError` that names the key and the key to write instead
197
+ (``("Q", 1)``).
198
+
199
+ The laws on ``sorts`` are checked when a sort is first read and again after
200
+ its table (or the domain, or the predicate table of the same name) is
201
+ replaced by another object; changing a set in place after the first read is
202
+ not noticed. :func:`check_structure` checks everything, every time.
203
+ """
204
+
205
+ def __init__(
206
+ self,
207
+ domain: Iterable[Any],
208
+ constants: Optional[Mapping[str, Any]] = None,
209
+ functions: Optional[Mapping[Tuple[str, int], Union[Callable, Mapping]]] = None,
210
+ predicates: Optional[Mapping[Tuple[str, int], Any]] = None,
211
+ sorts: Optional[Mapping[str, Iterable[Any]]] = None,
212
+ ):
213
+ """Build a structure, copying each mapping so later edits never leak in.
214
+
215
+ Raises:
216
+ ValueError: the domain is empty.
217
+ ~unicode_logic_kit.semantics.tarski.IllegalStructureError:
218
+ a ``functions`` or ``predicates`` table is not keyed
219
+ by ``(name, arity)`` (a bare name included), or a ``constants`` or
220
+ ``sorts`` table is not keyed by a name: the evaluator would never read
221
+ such a table, and its symbol would silently denote nothing. The
222
+ message names each offending key and what to write instead.
223
+ """
224
+ # Deduplicate while preserving order; reject an empty domain.
225
+ seen = []
226
+ for d in domain:
227
+ if d not in seen:
228
+ seen.append(d)
229
+ if not seen:
230
+ raise ValueError("Structure domain must be non-empty.")
231
+ self.domain: Tuple[Any, ...] = tuple(seen)
232
+
233
+ problems = _table_key_problems(constants or {}, functions or {},
234
+ predicates or {}, sorts or {})
235
+ if problems:
236
+ raise IllegalStructureError("Structure: " + "; ".join(problems))
237
+
238
+ self.constants: Dict[str, Any] = dict(constants or {})
239
+ self.functions: Dict[Tuple[str, int], Union[Callable, Mapping]] = dict(functions or {})
240
+ self.predicates: Dict[Tuple[str, int], Any] = dict(predicates or {})
241
+ self.sorts: Dict[str, Tuple[Any, ...]] = {
242
+ name: tuple(universe) for name, universe in (sorts or {}).items()
243
+ }
244
+ # sort name -> the objects (universe, predicate table, domain) the laws
245
+ # on that sort were last verified against; see sort_universe.
246
+ self._verified_sorts: Dict[str, Tuple[Any, Any, Any]] = {}
247
+
248
+ def __repr__(self) -> str:
249
+ """Show the domain size and the symbol tables for quick inspection."""
250
+ return (
251
+ f"Structure(domain={self.domain!r}, "
252
+ f"constants={self.constants!r}, "
253
+ f"functions={list(self.functions)!r}, "
254
+ f"predicates={list(self.predicates)!r}, "
255
+ f"sorts={self.sorts!r})"
256
+ )
257
+
258
+ def _repr_html_(self) -> str:
259
+ """Jupyter/IPython rich-display hook: an HTML summary table.
260
+
261
+ Same conservative shape as :meth:`__repr__`: the (already fully
262
+ materialised, finite) domain and constant VALUES are shown, but
263
+ functions/predicates are represented by their ``(name, arity)`` KEYS
264
+ only — an interpretation may be a plain Python callable, and it is
265
+ never invoked here, exactly as ``__repr__`` already chooses not to
266
+ dump ``self.functions``/``self.predicates`` themselves. Every value is
267
+ rendered through ``repr()`` and HTML-escaped, since a domain
268
+ individual, constant name, or sort name may be an arbitrary
269
+ user-supplied string (e.g. containing ``<``/``>``/``&``).
270
+ """
271
+ def esc(value: Any) -> str:
272
+ return html.escape(repr(value))
273
+
274
+ def keys_html(mapping: Mapping[Tuple[str, int], Any]) -> str:
275
+ return ", ".join(html.escape(f"{name}/{arity}")
276
+ for name, arity in sorted(mapping)) or "—"
277
+
278
+ constants_html = ", ".join(
279
+ f"{html.escape(name)} = {esc(value)}"
280
+ for name, value in sorted(self.constants.items())
281
+ ) or "—"
282
+ sorts_html = ", ".join(
283
+ f"{html.escape(name)} = {{{', '.join(esc(v) for v in universe)}}}"
284
+ for name, universe in sorted(self.sorts.items())
285
+ ) or "—"
286
+ rows = [
287
+ ("domain", ", ".join(esc(d) for d in self.domain) or "—"),
288
+ ("constants", constants_html),
289
+ ("functions", keys_html(self.functions)),
290
+ ("predicates", keys_html(self.predicates)),
291
+ ("sorts", sorts_html),
292
+ ]
293
+ body = "".join(f"<tr><th>{label}</th><td>{cell}</td></tr>" for label, cell in rows)
294
+ return f"<table><tbody>{body}</tbody></table>"
295
+
296
+ def sort_universe(self, sort: str) -> Tuple[Any, ...]:
297
+ """Return the universe of a named sort.
298
+
299
+ Raises:
300
+ KeyError: if the sort is undeclared. An undeclared sort is an error
301
+ rather than the empty set, since ``∀x:Undeclared φ`` vacuously
302
+ true and ``∃x:Undeclared φ`` false would silently mask a typo.
303
+ IllegalStructureError: if the sort is empty, holds an element that
304
+ is not in the domain, or is also a unary predicate of this
305
+ structure with a different extension. A vacuous ``∀`` over an
306
+ empty sort would be a truth value in a structure that is not a
307
+ structure of the definition.
308
+ """
309
+ if sort not in self.sorts:
310
+ raise KeyError(
311
+ f"Sort {sort!r} is not declared in this structure "
312
+ f"(known sorts: {sorted(self.sorts)})."
313
+ )
314
+ universe = self.sorts[sort]
315
+ predicate = self.predicates.get((sort, 1))
316
+ verified = self._verified_sorts.get(sort)
317
+ if (verified is None or verified[0] is not universe
318
+ or verified[1] is not predicate or verified[2] is not self.domain):
319
+ problems = _sort_problems(self, sort)
320
+ if problems:
321
+ raise IllegalStructureError("; ".join(problems))
322
+ self._verified_sorts[sort] = (universe, predicate, self.domain)
323
+ return universe
324
+
325
+
326
+ def _sort_problems(structure: Structure, name: str) -> List[str]:
327
+ """Every way the sort ``name`` of ``structure`` breaks the definition (none: empty list).
328
+
329
+ A sort is a NON-EMPTY subset of the domain, and a sort that is also a unary
330
+ predicate of the structure has the same extension as that predicate.
331
+ """
332
+ universe = structure.sorts[name]
333
+ problems: List[str] = []
334
+ if not universe:
335
+ problems.append(f"sort {name!r} is empty, and a sort is never empty")
336
+ outside = [d for d in universe if d not in structure.domain]
337
+ if outside:
338
+ problems.append(
339
+ f"sort {name!r} holds {outside!r}, which "
340
+ f"{'is' if len(outside) == 1 else 'are'} not in the domain"
341
+ )
342
+ declared = structure.predicates.get((name, 1))
343
+ if declared is not None:
344
+ in_sort = [d for d in structure.domain if d in universe]
345
+ in_predicate = [d for d in structure.domain if (d,) in declared]
346
+ if in_sort != in_predicate:
347
+ problems.append(
348
+ f"{name!r} is both a sort and a unary predicate, and they have "
349
+ f"different extensions: the sort holds {in_sort!r}, the "
350
+ f"predicate holds {in_predicate!r}"
351
+ )
352
+ return problems
353
+
354
+
355
+ def _undeclared_sort_message(sort: str) -> str:
356
+ """The message for a formula that uses a sort the structure does not declare."""
357
+ return f"the formula uses the sort {sort!r}, which this structure does not declare"
358
+
359
+
360
+ def _sorted_constant_problem(structure: Structure, term: SortedConstant) -> Optional[str]:
361
+ """Why ``term`` (``c:S``) is not an element of its declared sort in ``structure``, or None.
362
+
363
+ A constant the structure does not interpret has no value to be outside the
364
+ sort; a sort the structure does not declare is a different violation (see
365
+ :func:`_undeclared_sort_message`).
366
+ """
367
+ if term.name not in structure.constants or term.sort not in structure.sorts:
368
+ return None
369
+ value = structure.constants[term.name]
370
+ if value not in structure.sorts[term.sort]:
371
+ return (f"sorted constant {term.name}:{term.sort} denotes {value!r}, "
372
+ f"which is not in the sort {term.sort!r} "
373
+ f"({list(structure.sorts[term.sort])!r})")
374
+ return None
375
+
376
+
377
+ def structure_violations(structure: Structure, *formulas: Node) -> List[str]:
378
+ """Every way ``structure`` is not a structure of the many-sorted definition.
379
+
380
+ The structure's own laws are always checked: every sort in ``structure.sorts``
381
+ is a non-empty subset of the domain, and a sort that is also a unary
382
+ predicate has that predicate's extension. With ``formulas``, the laws that
383
+ depend on what is written are checked too: every sort a formula uses (a
384
+ sorted quantifier, counting quantifier, cardinality term or constant) is
385
+ declared, and every sorted constant ``c:S`` the structure interprets denotes
386
+ an element of ``S``. A constant written with two sorts must therefore lie in
387
+ both.
388
+
389
+ Returns:
390
+ One message per violation, in a stable order; an empty list for a
391
+ legal structure. Nothing is raised.
392
+ """
393
+ problems: List[str] = []
394
+ for name in structure.sorts:
395
+ problems.extend(_sort_problems(structure, name))
396
+ for formula in formulas:
397
+ for node in formula.walk():
398
+ if not isinstance(node, (SortedQuantifier, SortedCount, SortedCardinality,
399
+ SortedConstant)):
400
+ continue
401
+ if node.sort not in structure.sorts:
402
+ problems.append(_undeclared_sort_message(node.sort))
403
+ elif isinstance(node, SortedConstant):
404
+ problem = _sorted_constant_problem(structure, node)
405
+ if problem is not None:
406
+ problems.append(problem)
407
+ return list(dict.fromkeys(problems))
408
+
409
+
410
+ def check_structure(structure: Structure, *formulas: Node) -> None:
411
+ """Raise :class:`IllegalStructureError` unless ``structure`` is a structure of the definition.
412
+
413
+ The up-front, complete version of the checks the evaluator makes as it
414
+ reads sorts and sorted constants: see :func:`structure_violations` for what
415
+ is checked. The error lists every violation, not only the first.
416
+
417
+ Raises:
418
+ IllegalStructureError: if :func:`structure_violations` is not empty.
419
+ """
420
+ problems = structure_violations(structure, *formulas)
421
+ if problems:
422
+ raise IllegalStructureError("; ".join(problems))
423
+
424
+
425
+ def _refuse_numeral_constant_pair(formulas: Iterable[Node], where: str) -> None:
426
+ """Refuse formulas that hold a numeral and a constant spelled like its value.
427
+
428
+ A structure interprets a constant under its name and a numeral under the name of its
429
+ VALUE (:func:`~unicode_logic_kit.fol._fol_nodes.numeral_key`: ``1`` and ``1.0`` are
430
+ ``'1'``), in the one table ``constants``. ``Number(1)`` next to ``Constant('1')`` would
431
+ therefore be ONE entry, and a structure could not tell them apart: the evaluator and the
432
+ finite model finder (which enumerates that table) would read two symbols as one. A
433
+ numeral is a constant of its own, so the pair is refused by name, as the Z3 route, the
434
+ cvc5 route, the Prover9 writer and the TPTP writers refuse it. A number that is the bound
435
+ of a counting quantifier, or the operand a cardinality is compared with, is no individual
436
+ and is not looked up there; it clashes with nothing. ``where`` is the caller, which the
437
+ refusal names.
438
+
439
+ Raises:
440
+ NotImplementedError: a :class:`Constant` or :class:`SortedConstant` is named like
441
+ the value of a numeral of the formulas.
442
+ """
443
+ formulas = list(formulas)
444
+ numerals: Dict[str, Number] = {}
445
+ for formula in formulas:
446
+ for numeral in term_numerals(formula, counting_comparisons=True):
447
+ numerals.setdefault(numeral_key(numeral.value), numeral)
448
+ if not numerals:
449
+ return
450
+ for formula in formulas:
451
+ for node in formula.walk():
452
+ if isinstance(node, (Constant, SortedConstant)) and node.name in numerals:
453
+ raise _numeral_constant_clash(node.name, where)
454
+
455
+
456
+ def _numeral_constant_clash(name: str, where: str) -> NotImplementedError:
457
+ """The refusal of a numeral and a constant that are one entry ``name`` of a structure."""
458
+ return NotImplementedError(
459
+ f"{where}: the number {name} and the constant {name!r} would be ONE entry of a "
460
+ "structure's constants (a numeral is the constant of its value and is interpreted "
461
+ "under that name), so a structure could not tell them apart. A numeral is a constant "
462
+ "of its own, identified by its value (1 and 1.0 are one constant), and the kit "
463
+ "refuses to merge it with the constant of the same spelling. Rename the constant.")
464
+
465
+
466
+ #: The comparisons a cardinality may be an operand of (arithmetic over the natural numbers).
467
+ _COUNTING_COMPARISONS = frozenset({"=", "≠"}) | _ORDER_COMPARISONS
468
+
469
+
470
+ def _cardinality_vs_individual(where: str, predicate: str, operand: Node) -> NotImplementedError:
471
+ """The refusal of a cardinality compared with something that is not a number."""
472
+ return NotImplementedError(
473
+ f"{where}: {predicate!r} compares a cardinality with {operand.to_unicode_str()}, which "
474
+ "is not a number. A cardinality |{v : φ}| is a natural number the evaluator counts, "
475
+ "not an element of the domain, so it is only compared with a numeral or with another "
476
+ "cardinality. Compare it with a number, or quantify over the individuals instead.")
477
+
478
+
479
+ def _cardinality_as_individual(where: str, cardinality: Node, position: str) -> NotImplementedError:
480
+ """The refusal of a cardinality that stands where an individual is expected."""
481
+ return NotImplementedError(
482
+ f"{where}: the cardinality {cardinality.to_unicode_str()} is {position}. A cardinality "
483
+ "is a natural number the evaluator counts, not an element of the domain, so it has no "
484
+ "value there: it would be read as the element that shares its value. It can only be "
485
+ "compared with a number (= ≠ < > ≤ ≥ against a numeral or another cardinality). "
486
+ "Compare it, or quantify over the individuals instead.")
487
+
488
+
489
+ def _refuse_cardinality_as_individual(formulas: Iterable[Node], where: str) -> None:
490
+ """Refuse a cardinality term that does not stand as an operand of a comparison with a number.
491
+
492
+ ``|{v : φ}|`` is a natural number that the evaluator counts, not an element of the
493
+ domain. Its one reading is arithmetic: an operand of ``= ≠ < > ≤ ≥`` whose other operand
494
+ is a number (a numeral or another cardinality). Anywhere else (an argument of an ordinary
495
+ predicate or of a function, or compared with an individual) it would have to be read as
496
+ the element of the domain that happens to share its value (the count ``1`` as the element
497
+ ``1``), which is another statement and one that changes with how the domain is named. The
498
+ finite routes that state counting (the ASP and MiniZinc encodings, ``model_eval``) refuse
499
+ these forms, and so does the evaluator, by name, instead of answering for a different
500
+ question. ``where`` is the caller, which the refusal names.
501
+
502
+ Raises:
503
+ NotImplementedError: a :class:`Cardinality` or :class:`SortedCardinality` is an
504
+ argument of a predicate, a function or another term, or an operand of a
505
+ comparison whose other operand is not a number.
506
+ """
507
+ cardinalities = (Cardinality, SortedCardinality)
508
+ for formula in formulas:
509
+ readable: Set[int] = set()
510
+ for node in formula.walk():
511
+ if (isinstance(node, Atom) and node.predicate in _COUNTING_COMPARISONS
512
+ and len(node.args) == 2
513
+ and any(isinstance(a, cardinalities) for a in node.args)):
514
+ for operand in node.args:
515
+ if not isinstance(operand, cardinalities + (Number,)):
516
+ raise _cardinality_vs_individual(where, node.predicate, operand)
517
+ readable.update(id(a) for a in node.args if isinstance(a, cardinalities))
518
+ for node in formula.walk():
519
+ for child in node._child_nodes():
520
+ if isinstance(child, cardinalities) and id(child) not in readable:
521
+ if isinstance(node, Atom):
522
+ position = f"an argument of the predicate {node.predicate!r}"
523
+ elif isinstance(node, Function):
524
+ position = f"an argument of the function {node.name!r}"
525
+ else:
526
+ position = f"an operand of a {type(node).__name__}"
527
+ raise _cardinality_as_individual(where, child, position)
528
+
529
+
530
+ #: The formulas ``_check_formula_once`` has already checked, by identity. The
531
+ #: formula is kept in the table so that its identity cannot be reused while the entry stands.
532
+ _CHECKED_FORMULAS: Dict[int, Node] = {}
533
+ _CHECKED_FORMULAS_MAX = 1024
534
+
535
+
536
+ def _check_formula_once(formula: Node) -> None:
537
+ """The checks of a formula as a whole (:func:`_refuse_numeral_constant_pair` and
538
+ :func:`_refuse_cardinality_as_individual`) for the evaluator's entry, which a model finder
539
+ calls once per candidate structure with the same formula: a formula that passed is
540
+ remembered (a table of bounded size), so the check costs one lookup after the first."""
541
+ if id(formula) in _CHECKED_FORMULAS:
542
+ return
543
+ _refuse_numeral_constant_pair([formula], "semantics.tarski.satisfies")
544
+ _refuse_cardinality_as_individual([formula], "semantics.tarski.satisfies")
545
+ if len(_CHECKED_FORMULAS) >= _CHECKED_FORMULAS_MAX:
546
+ _CHECKED_FORMULAS.clear()
547
+ _CHECKED_FORMULAS[id(formula)] = formula
548
+
549
+
550
+ def term_value(term: Node, structure: Structure, assignment: Mapping[str, Any]) -> Any:
551
+ """Evaluate a term to its individual in the structure under an assignment.
552
+
553
+ - :class:`Variable` ``v`` → ``assignment[v.name]``.
554
+ - :class:`Constant` ``c`` → ``structure.constants[c.name]``.
555
+ - :class:`SortedConstant` ``c:S`` → ``structure.constants[c.name]``, which
556
+ must be an element of the sort ``S`` of the structure.
557
+ - :class:`Number` ``n`` → ``structure.constants.get(numeral_key(n.value), n.value)``
558
+ (the literal value itself by default): a numeral is a constant named by its
559
+ VALUE, so ``Number(1)`` and ``Number(1.0)`` are one constant. A comparison with a
560
+ cardinality bypasses this and reads the numeral directly — see
561
+ :func:`_operand_value`.
562
+ - :class:`Function` → the interpreted function applied to the evaluated args;
563
+ the interpretation may be a callable or a ``{arg_tuple: value}`` dict.
564
+
565
+ Raises:
566
+ KeyError: for an unassigned variable, an uninterpreted constant, or the
567
+ sort of a sorted constant that the structure does not declare.
568
+ IllegalStructureError: for a sorted constant whose value is not an
569
+ element of its sort, or whose sort breaks the laws in
570
+ :meth:`Structure.sort_universe`.
571
+ ValueError: for an uninterpreted function symbol, a dict interpretation
572
+ missing an argument tuple, or a lambda / non-term node.
573
+ """
574
+ if isinstance(term, Variable):
575
+ if term.name not in assignment:
576
+ raise KeyError(f"Variable {term.name!r} is not bound in the assignment.")
577
+ return assignment[term.name]
578
+
579
+ if isinstance(term, Constant):
580
+ if term.name not in structure.constants:
581
+ raise KeyError(
582
+ f"Constant {term.name!r} has no interpretation in the structure."
583
+ )
584
+ return structure.constants[term.name]
585
+
586
+ if isinstance(term, Number):
587
+ return structure.constants.get(numeral_key(term.value), term.value)
588
+
589
+ if isinstance(term, Function):
590
+ args = tuple(term_value(a, structure, assignment) for a in term.args)
591
+ key = (term.name, len(term.args))
592
+ if key not in structure.functions:
593
+ raise ValueError(
594
+ f"Function {term.name!r}/{len(term.args)} has no interpretation "
595
+ f"in the structure."
596
+ )
597
+ interp = structure.functions[key]
598
+ if callable(interp):
599
+ return interp(*args)
600
+ # Dict-style interpretation: look up the evaluated argument tuple.
601
+ if args not in interp:
602
+ raise ValueError(
603
+ f"Function {term.name!r}/{len(term.args)} is undefined for "
604
+ f"arguments {args!r}."
605
+ )
606
+ return interp[args]
607
+
608
+ if isinstance(term, (Cardinality, SortedCardinality)):
609
+ # |{v : φ}| counts the individuals satisfying φ. The count is a NATURAL
610
+ # NUMBER, not a domain individual: it has no value as one, and answering with
611
+ # the integer would read it as the element that shares its value. The only
612
+ # reading it has is the operand of a comparison with a number, which
613
+ # _operand_value counts itself.
614
+ raise _cardinality_as_individual(
615
+ "semantics.tarski.term_value", term, "evaluated as an individual")
616
+
617
+ if isinstance(term, Measure):
618
+ # μ(entity, dimension) is the binary function ``measure``, matching the Z3 and
619
+ # Prover9 lowerings — a structure that interprets it agrees with the provers.
620
+ args = (term_value(term.entity, structure, assignment),
621
+ term_value(term.dimension, structure, assignment))
622
+ if _MEASURE_FUNC not in structure.functions:
623
+ raise ValueError(
624
+ "Measure term μ(…) needs an interpretation for the function "
625
+ f"{_MEASURE_FUNC[0]!r}/{_MEASURE_FUNC[1]} in the structure."
626
+ )
627
+ interp = structure.functions[_MEASURE_FUNC]
628
+ if callable(interp):
629
+ return interp(*args)
630
+ if args not in interp:
631
+ raise ValueError(
632
+ f"Function {_MEASURE_FUNC[0]!r}/{_MEASURE_FUNC[1]} is undefined for "
633
+ f"arguments {args!r}."
634
+ )
635
+ return interp[args]
636
+
637
+ if isinstance(term, SortedConstant):
638
+ # Tested after the hot branches above, so an unsorted formula pays nothing
639
+ # for it. The value is the constant's, as for a plain Constant; the extra
640
+ # step is the law that it lies in the sort.
641
+ if term.name not in structure.constants:
642
+ raise KeyError(
643
+ f"Constant {term.name!r} has no interpretation in the structure."
644
+ )
645
+ value = structure.constants[term.name]
646
+ if value not in structure.sort_universe(term.sort):
647
+ raise IllegalStructureError(_sorted_constant_problem(structure, term))
648
+ return value
649
+
650
+ if isinstance(term, _LAMBDA_TYPES):
651
+ raise ValueError(
652
+ f"Cannot evaluate lambda node {type(term).__name__} as a term; "
653
+ "beta-reduce and lambda-eliminate the formula first."
654
+ )
655
+
656
+ raise ValueError(
657
+ f"term_value: {type(term).__name__} is not a term node."
658
+ )
659
+
660
+
661
+ def _witnesses(
662
+ binder: Node,
663
+ structure: Structure,
664
+ assignment: Mapping[str, Any],
665
+ ) -> Iterable[Any]:
666
+ """Yield the individuals in ``binder``'s range that satisfy its matrix.
667
+
668
+ Shared by the counting quantifiers and the cardinality terms: both bind one
669
+ variable over a matrix and differ only in what they do with the witnesses. A
670
+ sorted binder ranges over its sort's universe, an unsorted one over the domain.
671
+ """
672
+ universe = (structure.sort_universe(binder.sort)
673
+ if isinstance(binder, (SortedCount, SortedCardinality))
674
+ else structure.domain)
675
+ name = binder.variable.name
676
+ for d in universe:
677
+ if _evaluate(binder.formula, structure, _extend(assignment, name, d)):
678
+ yield d
679
+
680
+
681
+ def _is_number(value: Any) -> bool:
682
+ """Whether a term value counts as a number for an order comparison.
683
+
684
+ ``bool`` is excluded even though Python makes it an ``int`` subclass: a truth
685
+ value is not a position on a scale, and letting ``True ≥ False`` quietly
686
+ succeed would hide a modelling error rather than surface it.
687
+ """
688
+ return not isinstance(value, bool) and isinstance(value, (int, float))
689
+
690
+
691
+ def _compares_a_cardinality(atom: Atom) -> bool:
692
+ """Whether a comparison atom has a cardinality operand."""
693
+ return any(isinstance(a, (Cardinality, SortedCardinality)) for a in atom.args)
694
+
695
+
696
+ def _operand_value(term: Node, structure: Structure, assignment: Mapping[str, Any],
697
+ numeric: bool) -> Any:
698
+ """Evaluate one operand of a comparison.
699
+
700
+ Next to a cardinality (``numeric``), a :class:`Number` is the numeral it
701
+ spells, never whatever ``structure.constants`` maps its name to: the other
702
+ side is a count, so ``|{x : P(x)}| > 1`` asks about the number one. Reading
703
+ the literal through the constant table instead would let a structure that
704
+ interprets the name ``"1"`` as some other individual change the question —
705
+ and a model finder that enumerates interpretations for every numeral it sees
706
+ finds exactly such a structure, reporting a countermodel to a valid
707
+ entailment.
708
+
709
+ Next to a cardinality the other operand has to be a number too: the count of a
710
+ cardinality is the only value of one, and an individual (a constant, a variable, a
711
+ function term) is not a number, whatever integer it happens to be.
712
+
713
+ Raises:
714
+ NotImplementedError: ``numeric`` and ``term`` is neither a numeral nor a cardinality.
715
+ """
716
+ if numeric:
717
+ if isinstance(term, Number):
718
+ return term.value
719
+ if isinstance(term, (Cardinality, SortedCardinality)):
720
+ return sum(1 for _ in _witnesses(term, structure, assignment))
721
+ raise _cardinality_vs_individual("semantics.tarski", "a comparison", term)
722
+ return term_value(term, structure, assignment)
723
+
724
+
725
+ def _order_value(atom: Atom, structure: Structure, assignment: Mapping[str, Any]) -> bool:
726
+ """Truth value of a binary order comparison ``< > ≤ ≥``.
727
+
728
+ Three readings apply, in this order of precedence:
729
+
730
+ 1. A :class:`Cardinality` operand forces the **numeric** reading. A cardinality
731
+ is a natural number this evaluator computes itself, so there is no freedom
732
+ left to a structure; routing it through a relation extension would be a
733
+ category error. A :class:`Number` on the other side is read as the numeral
734
+ itself, not through ``structure.constants`` (see :func:`_operand_value`).
735
+ A cardinality compared against a non-number raises.
736
+ 2. Otherwise a **declared** extension wins. The order symbols are ordinary
737
+ relation symbols of the language, and a structure may interpret ``<`` over
738
+ its domain however it likes — that is also the reading ``to_z3`` /
739
+ ``to_prover9`` export, where the comparison is an uninterpreted relation.
740
+ 3. Otherwise, if both operands evaluate to numbers, the **numeric** reading
741
+ applies. This is what makes an undeclared order over :class:`Measure` values
742
+ behave: ``μ`` is an uninterpreted function, so a structure that maps it to
743
+ numbers without also declaring ``≥`` would otherwise fall through to the
744
+ empty relation and be silently false — the same failure mode (1) exists to
745
+ prevent for cardinalities.
746
+
747
+ Anything else is the empty relation, hence false, like any uninterpreted
748
+ predicate. Note the asymmetry between (1) and (2) is deliberate and not an
749
+ inconsistency: a cardinality *is* a number, whereas a measure's values are
750
+ whatever the structure says they are.
751
+ """
752
+ numeric = _compares_a_cardinality(atom)
753
+ left, right = (_operand_value(a, structure, assignment, numeric) for a in atom.args)
754
+
755
+ if numeric:
756
+ for value in (left, right):
757
+ if not _is_number(value):
758
+ raise ValueError(
759
+ f"Cannot compare a cardinality with {value!r}: an order "
760
+ f"comparison involving |{{v : φ}}| is numeric, so both operands "
761
+ f"must evaluate to numbers."
762
+ )
763
+ return _ORDER_OPS[atom.predicate](left, right)
764
+
765
+ key = (atom.predicate, 2)
766
+ if key in structure.predicates:
767
+ return (left, right) in structure.predicates[key]
768
+
769
+ if _is_number(left) and _is_number(right):
770
+ return _ORDER_OPS[atom.predicate](left, right)
771
+
772
+ return False
773
+
774
+
775
+ def _atom_value(atom: Atom, structure: Structure, assignment: Mapping[str, Any]) -> bool:
776
+ """Compute the truth value of an atomic formula.
777
+
778
+ Equality ``=`` is identity of the two term values; ``≠`` is non-identity. When
779
+ one side is a cardinality, a :class:`Number` on the other is read as the
780
+ numeral itself, as for the order comparisons (see :func:`_operand_value`).
781
+ A nullary predicate reads its bool from ``predicates[(name, 0)]``. A binary
782
+ order comparison ``< > ≤ ≥`` is delegated to :func:`_order_value`. Every other
783
+ predicate is true iff the tuple of argument values lies in its extension; a
784
+ missing extension is the empty relation, hence false — unless the name is a
785
+ SORT of the structure: a sort and the unary predicate of that name are one
786
+ symbol, so ``S(t)`` is then true iff the value of ``t`` is in the sort (and
787
+ when the structure lists a predicate table for ``S`` as well, it must agree
788
+ with the sort — :meth:`Structure.sort_universe` checks that).
789
+ """
790
+ if atom.predicate in ("=", "≠") and len(atom.args) == 2:
791
+ numeric = _compares_a_cardinality(atom)
792
+ left, right = (_operand_value(a, structure, assignment, numeric) for a in atom.args)
793
+ return (left == right) if atom.predicate == "=" else (left != right)
794
+
795
+ if not atom.args:
796
+ if _is_tptp_boolean_atom(atom):
797
+ return _truth_constant_word(atom) == "$true" # the truth constants, in every structure
798
+ return bool(structure.predicates.get((atom.predicate, 0), False))
799
+
800
+ if atom.predicate in _ORDER_COMPARISONS and len(atom.args) == 2:
801
+ return _order_value(atom, structure, assignment)
802
+
803
+ values = tuple(term_value(a, structure, assignment) for a in atom.args)
804
+ arity = len(atom.args)
805
+ extension = structure.predicates.get((atom.predicate, arity))
806
+ if structure.sorts and arity == 1 and atom.predicate in structure.sorts:
807
+ universe = structure.sort_universe(atom.predicate)
808
+ if extension is None:
809
+ return values[0] in universe
810
+ return extension is not None and values in extension
811
+
812
+
813
+ def _extend(assignment: Mapping[str, Any], name: str, value: Any) -> Dict[str, Any]:
814
+ """Return a copy of the assignment with ``name`` bound to ``value``.
815
+
816
+ The input mapping is never mutated (functional style).
817
+ """
818
+ extended = dict(assignment)
819
+ extended[name] = value
820
+ return extended
821
+
822
+
823
+ def satisfies(
824
+ formula: Node,
825
+ structure: Structure,
826
+ assignment: Optional[Mapping[str, Any]] = None,
827
+ ) -> bool:
828
+ """Return whether ``structure`` satisfies ``formula`` under ``assignment``.
829
+
830
+ ``assignment`` maps logical variable names to individuals; it defaults to
831
+ the empty assignment (appropriate for a sentence with no free variables).
832
+
833
+ Connectives follow the classical truth tables. ``∀x φ`` holds iff every
834
+ individual of the domain satisfies ``φ`` with ``x`` bound to it; ``∃x φ``
835
+ iff some individual does. A :class:`SortedQuantifier` ranges over the named
836
+ sort's universe instead of the whole domain.
837
+
838
+ Raises:
839
+ ValueError: on a Łukasiewicz node (use the fuzzy evaluator — Tarski is
840
+ two-valued) or a lambda node (eliminate it first), or on an unknown
841
+ quantifier type or node type.
842
+ IllegalStructureError: when ``structure`` is not a structure of the
843
+ many-sorted definition and the evaluation reads the part that is
844
+ wrong: an empty sort or one holding a non-domain element, a name
845
+ that is a sort and a unary predicate with two extensions, a sorted
846
+ constant outside its sort (see the module docstring and
847
+ :func:`check_structure`). A subclass of ``ValueError``.
848
+ NotImplementedError: when ``formula`` holds a :class:`Number` and a constant
849
+ spelled like its value (``Number(1)`` next to ``Constant('1')``, or
850
+ ``Number(1.0)`` next to ``Constant('1')``): a structure holds ONE entry
851
+ ``'1'`` for both, so it could not tell them apart, and the kit refuses to
852
+ merge a numeral with the constant of the same spelling. Also when a cardinality
853
+ ``|{v : φ}|`` is not an operand of a comparison with a number (an argument of a
854
+ predicate or a function, or compared with an individual): it is a natural number
855
+ the evaluator counts, not an element of the domain, so it has no value there.
856
+ """
857
+ _check_formula_once(formula)
858
+ return _evaluate(formula, structure, {} if assignment is None else assignment)
859
+
860
+
861
+ def _evaluate(formula: Node, structure: Structure, assignment: Mapping[str, Any]) -> bool:
862
+ """:func:`satisfies` without the check of the formula as a whole, which the entry made.
863
+
864
+ Every recursive step lands here and not on :func:`satisfies`, so a formula is checked
865
+ once, when it is handed in, and not once per subformula and quantified individual.
866
+ """
867
+ if isinstance(formula, Atom):
868
+ # `$true` / `$false` with no arguments are TPTP's DEFINED propositions (the
869
+ # kit's TPTP reader produces them), not letters of the user's: the same
870
+ # reading to_z3 and the TPTP writers give them.
871
+ if _is_tptp_boolean_atom(formula):
872
+ return _truth_constant_word(formula) == "$true"
873
+ return _atom_value(formula, structure, assignment)
874
+
875
+ if isinstance(formula, Not):
876
+ return not _evaluate(formula.formula, structure, assignment)
877
+
878
+ if isinstance(formula, And):
879
+ return (_evaluate(formula.left, structure, assignment)
880
+ and _evaluate(formula.right, structure, assignment))
881
+
882
+ if isinstance(formula, Or):
883
+ return (_evaluate(formula.left, structure, assignment)
884
+ or _evaluate(formula.right, structure, assignment))
885
+
886
+ if isinstance(formula, Xor):
887
+ return (_evaluate(formula.left, structure, assignment)
888
+ != _evaluate(formula.right, structure, assignment))
889
+
890
+ if isinstance(formula, Implies):
891
+ return ((not _evaluate(formula.left, structure, assignment))
892
+ or _evaluate(formula.right, structure, assignment))
893
+
894
+ if isinstance(formula, Iff):
895
+ return (_evaluate(formula.left, structure, assignment)
896
+ == _evaluate(formula.right, structure, assignment))
897
+
898
+ if isinstance(formula, Quantifier):
899
+ return _eval_quantifier(
900
+ formula.type, formula.variable.name, structure.domain,
901
+ formula.formula, structure, assignment,
902
+ )
903
+
904
+ if isinstance(formula, SortedQuantifier):
905
+ universe = structure.sort_universe(formula.sort)
906
+ return _eval_quantifier(
907
+ formula.type, formula.variable.name, universe,
908
+ formula.formula, structure, assignment,
909
+ )
910
+
911
+ if isinstance(formula, (Count, SortedCount)):
912
+ # ∃≥n / ∃≤n / ∃=n: count the witnesses, compare against the bound. The whole
913
+ # (finite) universe is walked — ``le`` and ``eq`` need the exact count anyway,
914
+ # and the universes here are the small ones the model search enumerates.
915
+ witnesses = sum(1 for _ in _witnesses(formula, structure, assignment))
916
+ n = formula.n.value
917
+ if formula.op == "ge":
918
+ return witnesses >= n
919
+ if formula.op == "le":
920
+ return witnesses <= n
921
+ if formula.op == "eq":
922
+ return witnesses == n
923
+ raise ValueError(f"Unknown counting-quantifier op: {formula.op!r}")
924
+
925
+ if isinstance(formula, _FUZZY_TYPES):
926
+ raise ValueError(
927
+ f"Cannot evaluate Łukasiewicz node {type(formula).__name__} with the "
928
+ "two-valued Tarskian evaluator; use the fuzzy evaluator instead."
929
+ )
930
+
931
+ if isinstance(formula, _LAMBDA_TYPES):
932
+ raise ValueError(
933
+ f"Cannot evaluate lambda node {type(formula).__name__}; beta-reduce "
934
+ "and lambda-eliminate the formula before calling satisfies."
935
+ )
936
+
937
+ raise ValueError(
938
+ f"satisfies: unsupported node type {type(formula).__name__}."
939
+ )
940
+
941
+
942
+ def _eval_quantifier(
943
+ qtype: str,
944
+ var_name: str,
945
+ universe: Iterable[Any],
946
+ body: Node,
947
+ structure: Structure,
948
+ assignment: Mapping[str, Any],
949
+ ) -> bool:
950
+ """Evaluate a quantifier over a given universe of individuals.
951
+
952
+ ``∀`` holds iff the body holds for every individual; ``∃`` iff for some.
953
+ Each candidate is bound in a fresh copy of the assignment (no mutation).
954
+ """
955
+ if qtype in _FORALL:
956
+ return all(
957
+ _evaluate(body, structure, _extend(assignment, var_name, d))
958
+ for d in universe
959
+ )
960
+ if qtype in _EXISTS:
961
+ return any(
962
+ _evaluate(body, structure, _extend(assignment, var_name, d))
963
+ for d in universe
964
+ )
965
+ raise ValueError(f"Unknown quantifier type: {qtype!r}")
966
+
967
+
968
+ def models(formula: Node, structure: Structure) -> bool:
969
+ """Convenience alias: ``satisfies(formula, structure, {})``.
970
+
971
+ Reads as "structure models formula" — the sentence is evaluated under the
972
+ empty assignment, so it is meaningful for closed formulas (no free
973
+ variables).
974
+ """
975
+ return satisfies(formula, structure, {})