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,580 @@
1
+ """Three-valued (Kleene K3 / Priest LP) evaluation of classical FOL formulas.
2
+
3
+ The strong-Kleene tables interpret the classical connectives over the chain
4
+ ``{0.0, 0.5, 1.0}`` — read as *false*, *undefined/both*, *true*::
5
+
6
+ ¬x = 1 − x
7
+ a ∧ b = min(a, b)
8
+ a ∨ b = max(a, b)
9
+ a → b = max(1 − a, b) (material implication)
10
+ a ↔ b = min(a → b, b → a)
11
+ a ⊕ b = min(max(a, b), 1 − min(a, b)) (a ∨ b) ∧ ¬(a ∧ b)
12
+
13
+ These are the *same* truth functions for K3 and LP; the two logics differ only
14
+ in which values count as **designated** (truth-preserved):
15
+
16
+ K3 designates {1.0} — only plain truth
17
+ LP designates {0.5, 1.0} — "true" or "both" (Priest's paraconsistent LP)
18
+
19
+ That single choice produces the headline contrasts: the law of excluded middle
20
+ ``P ∨ ¬P`` is K3-invalid but LP-valid, and LP is paraconsistent — ``P, ¬P`` does
21
+ *not* entail an arbitrary ``Q`` — while K3 fails non-contradiction validity.
22
+
23
+ This module operates on **classical** AST nodes (Atom, Not, And, Or, Xor,
24
+ Implies, Iff, Quantifier) parsed by ``MSFLParser()`` — no new grammar. Truth
25
+ values are the floats ``0.0``, ``0.5`` and ``1.0``. A *valuation* maps a ground
26
+ atom's key (the text it prints as, with every constant written by its name, e.g.
27
+ ``'P'`` or ``'P(a)'``, also for a constant named ``a`` that the text of the formula
28
+ writes in quotes, ``P('a')``, which is read as the same key) to one of those three values.
29
+ The nullary atoms ``$true`` and
30
+ ``$false`` (``⊤`` and ``⊥``) are not letters of the formula but the constants truth and falsity: they have the value
31
+ ``1.0`` and ``0.0`` under every valuation, and an enumeration does not vary them.
32
+
33
+ Quantifiers are read substitutionally over a finite ``domain`` of constant
34
+ names, exactly as in the fuzzy evaluator: ``∀`` is the minimum and ``∃`` the
35
+ maximum of the body's value over the domain (∀ = "all", ∃ = "some"). A domain
36
+ is required whenever a quantifier is present.
37
+
38
+ A variable that is free in a formula that is decided together with a quantifier (the
39
+ deciders :func:`is_valid`, :func:`is_satisfiable` and :func:`entails`, and their
40
+ counterparts in :mod:`~unicode_logic_kit.semantics.matrix`) is a PARAMETER: one unknown
41
+ element of the domain, the same in every formula of the problem. The decider tries each
42
+ assignment of the domain's elements to the problem's free variables: a formula is valid
43
+ (a consequence) when it is so under every assignment and satisfiable when it is so
44
+ under some. ``∀y P(y) → P(x)`` is therefore LP-valid over ``{a, b}`` (the parameter ``x``
45
+ is ``a`` or ``b``), ``P(x) ⊢ ∃y P(y)`` is a consequence (whichever element ``x`` is, ``P``
46
+ of it is one of the disjuncts), and ``P(x), Q(y) ⊢ ∀z (P(z) ∧ Q(z))`` is not. Where no
47
+ quantifier is present the domain says nothing about a variable, and the atom ``P(x)`` is a
48
+ letter of its own, exactly as ``P(a)`` is. The evaluator (:func:`kleene_value`) takes the
49
+ valuation as it stands: the atom ``P(x)`` of a free variable is one more key of it.
50
+
51
+ An atom is a letter named by the text it prints as. Two different atoms that print alike
52
+ (the numeral ``1`` and a constant named ``1``, a free variable ``x`` and a constant named
53
+ ``x``) would be read as one letter, so a problem that has such a pair is refused by name.
54
+
55
+ Łukasiewicz (fuzzy), sorted, lambda, and modal nodes carry no classical
56
+ three-valued reading here and raise ``NotImplementedError``. A sorted constant
57
+ (``alice:Human``) is refused the same way wherever it stands, inside an atom as well:
58
+ that ``alice`` lies in ``Human`` is a fact about the sort which the three truth
59
+ values have no statement of, and reading ``Human(alice)`` as unrelated to it would
60
+ answer another question.
61
+
62
+ Parse inputs with ``MSFLParser()`` (classical propositional / FOL).
63
+ """
64
+
65
+ from itertools import product
66
+ from typing import Collection, Dict, Iterator, List, Optional, Sequence, Set, Tuple
67
+
68
+ from ..fol._atom_keys import AtomKeys, atom_key, find_own_key
69
+ from ..fol._free_parameters import free_parameter_names
70
+ from ..fol.nodes import (
71
+ Node, Variable, Constant, Number, Function, Atom,
72
+ Not, And, Or, Xor, Implies, Iff, Quantifier,
73
+ SortedQuantifier, SortedConstant,
74
+ LukNegation, WeakConjunction, WeakDisjunction,
75
+ StrongConjunction, StrongDisjunction, LukImplication, LukEquivalence,
76
+ Lambda, LambdaVar, Application,
77
+ Box, Diamond, Always, Eventually, Next, Until,
78
+ Historically, Once, Previous, Since, Knows, Believes,
79
+ )
80
+ from ..fol._truth_constants import truth_value as _truth_value
81
+
82
+ # The three truth values, smallest first: false < undefined/both < true.
83
+ FALSE = 0.0
84
+ UNDEFINED = 0.5
85
+ TRUE = 1.0
86
+ TRUTH_VALUES = (FALSE, UNDEFINED, TRUE)
87
+
88
+ # Designated (truth-preserved) value sets keyed by logic name.
89
+ # K3 — only plain truth is designated.
90
+ # LP — Priest's Logic of Paradox: both "true" and "both" are designated.
91
+ #: Designated (truth-preserved) value sets, keyed by logic name: ``"K3"``
92
+ #: designates plain truth alone, ``"LP"`` (Priest's Logic of Paradox)
93
+ #: designates both "true" and "both".
94
+ DESIGNATED: Dict[str, frozenset] = {
95
+ "K3": frozenset({TRUE}),
96
+ "LP": frozenset({UNDEFINED, TRUE}),
97
+ }
98
+
99
+ # Safety cap for the validity/satisfiability/entailment enumeration: those
100
+ # routines visit 3 ** (#distinct ground atoms) assignments. Past this many the
101
+ # enumeration is hopeless in practice, so it raises a clear error instead of
102
+ # hanging. Raise this module attribute if you genuinely need a larger run.
103
+ MAX_MODELS = 3_000_000
104
+
105
+ # Łukasiewicz / sorted / lambda / modal node groups, rejected with a clear
106
+ # NotImplementedError: none has a strong-Kleene three-valued reading here.
107
+ _FUZZY_NODES = (
108
+ LukNegation, WeakConjunction, WeakDisjunction,
109
+ StrongConjunction, StrongDisjunction, LukImplication, LukEquivalence,
110
+ )
111
+ _SORTED_NODES = (SortedQuantifier, SortedConstant)
112
+ _LAMBDA_NODES = (Lambda, LambdaVar, Application)
113
+ _MODAL_NODES = (Box, Diamond, Always, Eventually, Next, Until,
114
+ Historically, Once, Previous, Since, Knows, Believes)
115
+
116
+
117
+ def _snap(x: float) -> float:
118
+ """Snap a raw number to the nearest of {0.0, 0.5, 1.0}, or reject it.
119
+
120
+ Tiny floating drift around the three legal values is tolerated and snapped;
121
+ anything genuinely outside ``[0, 1]`` or far from a legal value raises
122
+ ``ValueError`` so a malformed valuation is caught early rather than silently
123
+ distorting a result.
124
+ """
125
+ for v in TRUTH_VALUES:
126
+ if abs(x - v) < 1e-9:
127
+ return v
128
+ raise ValueError(
129
+ f"Three-valued truth value must be one of {{0.0, 0.5, 1.0}}; got {x!r}."
130
+ )
131
+
132
+
133
+ def _ground_term(node: Node, var_name: str, const_name: str) -> Node:
134
+ """Replace every free ``Variable(var_name)`` in a *term* with ``Constant(const_name)``.
135
+
136
+ Operates on term-position nodes (Variable, Constant, Number, Function).
137
+ Returns a new node; the input is never mutated.
138
+ """
139
+ if isinstance(node, Variable):
140
+ return Constant(const_name) if node.name == var_name else node
141
+ if isinstance(node, Function):
142
+ return Function(node.name,
143
+ [_ground_term(a, var_name, const_name) for a in node.args])
144
+ # Constant, Number and anything else carry no free Variable.
145
+ return node
146
+
147
+
148
+ def _ground(node: Node, var_name: str, const_name: str) -> Node:
149
+ """Substitute free ``Variable(var_name)`` with ``Constant(const_name)`` throughout.
150
+
151
+ Recurses the classical formula structure, stopping at an inner quantifier
152
+ that rebinds the same variable name (the inner binding shadows ours).
153
+ Returns a new node; the input is never mutated.
154
+ """
155
+ if isinstance(node, Atom):
156
+ return Atom(node.predicate,
157
+ [_ground_term(a, var_name, const_name) for a in node.args])
158
+ if isinstance(node, Not):
159
+ return Not(_ground(node.formula, var_name, const_name))
160
+ if isinstance(node, (And, Or, Xor, Implies, Iff)):
161
+ return type(node)(_ground(node.left, var_name, const_name),
162
+ _ground(node.right, var_name, const_name))
163
+ if isinstance(node, Quantifier):
164
+ if node.variable.name == var_name:
165
+ return node # inner binder shadows the variable we are grounding
166
+ return Quantifier(node.type, node.variable,
167
+ _ground(node.formula, var_name, const_name))
168
+ # Term-position leaves pass through the term grounder.
169
+ return _ground_term(node, var_name, const_name)
170
+
171
+
172
+ def _eval_quantifier(qtype: str, var_name: str, body: Node,
173
+ valuation: Dict[str, float], domain: Set[str],
174
+ keys: Optional[AtomKeys] = None) -> float:
175
+ """Evaluate a quantifier substitutionally: ∀ = min, ∃ = max over the domain."""
176
+ if not domain:
177
+ raise ValueError(
178
+ "Cannot evaluate a quantifier over an empty domain; provide at "
179
+ "least one constant name."
180
+ )
181
+ values = [
182
+ _kleene_value(_ground(body, var_name, d), valuation, domain, keys)
183
+ for d in domain
184
+ ]
185
+ if qtype in ("∀", "forall"):
186
+ return min(values)
187
+ if qtype in ("∃", "exists"):
188
+ return max(values)
189
+ raise ValueError(f"Unknown quantifier type: {qtype!r}")
190
+
191
+
192
+ def kleene_value(formula: Node,
193
+ valuation: Dict[str, float],
194
+ domain: Optional[Set[str]] = None) -> float:
195
+ """Strong-Kleene three-valued truth value of a classical formula.
196
+
197
+ Args:
198
+ formula: a classical FOL formula node (Atom, Not, And, Or, Xor, Implies,
199
+ Iff, Quantifier). Build it with ``MSFLParser()``.
200
+ valuation: maps a ground atom's key — the text it prints as, with every
201
+ constant written by its name, e.g. ``'P'`` or ``'P(a)'`` — to a
202
+ value in ``{0.0, 0.5, 1.0}``. A key written as the text of the atom as a
203
+ formula (``atom.to_unicode_str()``, ``"P('a')"``) is read as the same key. A
204
+ missing key raises ``KeyError``. The atom of a free variable (``'P(x)'``) is one
205
+ more key.
206
+ domain: a set of constant-name strings over which quantifiers range
207
+ (∀ = min, ∃ = max). Required whenever a ``Quantifier`` is present.
208
+
209
+ Returns:
210
+ The truth value as one of ``0.0``, ``0.5`` or ``1.0``.
211
+
212
+ Raises:
213
+ KeyError: a ground atom's key is absent from the valuation.
214
+ ValueError: a quantifier lacks a domain (or the domain is empty), or a
215
+ valuation entry is not snappable to ``{0.0, 0.5, 1.0}``, or the valuation holds
216
+ both spellings of one atom (its key and its text as a formula) with different
217
+ values.
218
+ NotImplementedError: the node is a Łukasiewicz, sorted, lambda or modal
219
+ construct, which has no strong-Kleene three-valued reading here; a
220
+ sorted constant inside an atom is refused the same way; so is a pair of
221
+ different atoms that print alike (the numeral ``1`` and a constant named
222
+ ``1``, a free variable ``x`` and a constant named ``x``), which one key of the
223
+ valuation could not tell apart.
224
+ TypeError: the node is a bare term, not a formula, or otherwise
225
+ unsupported.
226
+ """
227
+ return _kleene_value(formula, valuation, domain, AtomKeys("manyvalued.kleene_value", "refuse"))
228
+
229
+
230
+ def _kleene_value(formula: Node, valuation: Dict[str, float],
231
+ domain: Optional[Set[str]], keys: Optional[AtomKeys]) -> float:
232
+ """The body of :func:`kleene_value`; ``keys`` records and checks the key of every atom
233
+ reached, and is ``None`` where the atoms were checked already."""
234
+ # --- Atoms (the base case) --------------------------------------------
235
+ if isinstance(formula, Atom):
236
+ # The truth constants ``$true`` / ``$false`` are the top and the bottom value
237
+ # (1.0 and 0.0) whatever the valuation says; they are not letters.
238
+ constant = _truth_value(formula)
239
+ if constant is not None:
240
+ return TRUE if constant else FALSE
241
+ found = (find_own_key(valuation, formula) if keys is None
242
+ else keys.find(valuation, formula))
243
+ if found is None:
244
+ key = atom_key(formula)
245
+ raise KeyError(
246
+ f"No truth value for ground atom {key!r} in the valuation. "
247
+ f"Provide valuation[{key!r}] as one of 0.0, 0.5 or 1.0."
248
+ )
249
+ return _snap(float(valuation[found]))
250
+
251
+ # --- Negation ----------------------------------------------------------
252
+ if isinstance(formula, Not):
253
+ x = _kleene_value(formula.formula, valuation, domain, keys)
254
+ return 1.0 - x
255
+
256
+ # --- Binary classical connectives -------------------------------------
257
+ if isinstance(formula, (And, Or, Xor, Implies, Iff)):
258
+ a = _kleene_value(formula.left, valuation, domain, keys)
259
+ b = _kleene_value(formula.right, valuation, domain, keys)
260
+ if isinstance(formula, And):
261
+ return min(a, b)
262
+ if isinstance(formula, Or):
263
+ return max(a, b)
264
+ if isinstance(formula, Implies):
265
+ return max(1.0 - a, b) # material: ¬a ∨ b
266
+ if isinstance(formula, Iff):
267
+ a_to_b = max(1.0 - a, b)
268
+ b_to_a = max(1.0 - b, a)
269
+ return min(a_to_b, b_to_a)
270
+ # Xor: (a ∨ b) ∧ ¬(a ∧ b) = min(max(a, b), 1 − min(a, b))
271
+ return min(max(a, b), 1.0 - min(a, b))
272
+
273
+ # --- Quantifiers -------------------------------------------------------
274
+ if isinstance(formula, Quantifier):
275
+ if domain is None:
276
+ raise ValueError(
277
+ "Evaluating a Quantifier requires a 'domain' (a set of "
278
+ "constant-name strings)."
279
+ )
280
+ return _eval_quantifier(formula.type, formula.variable.name,
281
+ formula.formula, valuation, set(domain), keys)
282
+
283
+ # --- Rejected node classes (informative errors) -----------------------
284
+ _reject_if_unsupported(formula)
285
+
286
+
287
+ def _reject_if_unsupported(formula: Node) -> None:
288
+ """Raise the error kleene_value uses for a node with no three-valued reading.
289
+
290
+ Shared by :func:`kleene_value` and the compiled enumerator (:func:`_compile`)
291
+ so both reject the same node classes with identical messages. The supported
292
+ nodes (Atom, Not, And, Or, Xor, Implies, Iff, Quantifier) are dispatched by
293
+ the caller before this is reached, so reaching here is always an error.
294
+ """
295
+ if isinstance(formula, _FUZZY_NODES):
296
+ raise NotImplementedError(
297
+ f"{type(formula).__name__} (a Łukasiewicz operator) has no "
298
+ "strong-Kleene three-valued reading; parse classical input with "
299
+ "MSFLParser() so connectives carry classical semantics."
300
+ )
301
+ if isinstance(formula, _SORTED_NODES):
302
+ raise NotImplementedError(
303
+ f"{type(formula).__name__} (a sorted construct) is not supported by "
304
+ "the three-valued evaluator; the K3/LP evaluator is unsorted."
305
+ )
306
+ if isinstance(formula, _LAMBDA_NODES):
307
+ raise NotImplementedError(
308
+ f"{type(formula).__name__} (a lambda-calculus construct) has no "
309
+ "truth value; beta-reduce and eliminate lambdas first."
310
+ )
311
+ if isinstance(formula, _MODAL_NODES):
312
+ raise NotImplementedError(
313
+ f"{type(formula).__name__} (a modal/temporal/epistemic operator) is "
314
+ "not supported by the three-valued evaluator; it is non-modal."
315
+ )
316
+ if isinstance(formula, (Variable, Constant, Number, Function)):
317
+ raise TypeError(
318
+ f"{type(formula).__name__} is a term, not a formula; the "
319
+ "three-valued evaluator can only score a formula's truth value."
320
+ )
321
+
322
+ raise TypeError(
323
+ f"kleene_value: unsupported node type {type(formula).__name__}"
324
+ )
325
+
326
+
327
+ def _designated_set(logic: str) -> frozenset:
328
+ """Look up the designated-value set for a logic name, with a clear error."""
329
+ try:
330
+ return DESIGNATED[logic]
331
+ except KeyError:
332
+ raise ValueError(
333
+ f"Unknown logic {logic!r}; choose one of {sorted(DESIGNATED)}."
334
+ )
335
+
336
+
337
+ def _atom_keys(*formulas: Node, route: str = "manyvalued") -> List[str]:
338
+ """Distinct ground-atom keys across the formulas, in first-seen order.
339
+
340
+ The key is the text of each atom with every constant written by its name — these are the
341
+ independent variables enumerated over ``{0.0, 0.5, 1.0}``. A sorted constant, and
342
+ two different atoms that print alike, are refused by name (``route`` names the
343
+ caller in the message).
344
+ """
345
+ return AtomKeys(route, "refuse").letters(formulas)
346
+
347
+
348
+ def _instantiate(node: Node, domain: Set[str]) -> Node:
349
+ """Replace every quantifier by the ∧/∨ of its domain instances.
350
+
351
+ ``∀x φ`` becomes the conjunction and ``∃x φ`` the disjunction of ``φ`` with
352
+ the bound variable grounded to each domain element. The result is
353
+ quantifier-free, so its atom set is exactly the ground atoms an assignment
354
+ must fix. Used only to discover assignment variables — the actual value is
355
+ computed by ``kleene_value`` so the two stay in lock-step.
356
+ """
357
+ if isinstance(node, Atom):
358
+ return node
359
+ if isinstance(node, Not):
360
+ return Not(_instantiate(node.formula, domain))
361
+ if isinstance(node, (And, Or, Xor, Implies, Iff)):
362
+ return type(node)(_instantiate(node.left, domain),
363
+ _instantiate(node.right, domain))
364
+ if isinstance(node, Quantifier):
365
+ instances = [
366
+ _instantiate(_ground(node.formula, node.variable.name, d), domain)
367
+ for d in domain
368
+ ]
369
+ combine = And if node.type in ("∀", "forall") else Or
370
+ result = instances[0]
371
+ for inst in instances[1:]:
372
+ result = combine(result, inst)
373
+ return result
374
+ return node
375
+
376
+
377
+ def _compile(node: Node, index: Dict[str, int]):
378
+ """Compile a quantifier-free classical formula into ``fn(values) -> float``.
379
+
380
+ ``values`` is a tuple of truth values positionally aligned with ``index``
381
+ (atom key → tuple position). The returned closure computes the strong-Kleene
382
+ value with no per-assignment AST walk and no per-atom key rendering — those
383
+ happen once, here, at compile time. It is the enumeration
384
+ fast path; it is exhaustively cross-checked against :func:`kleene_value` in the
385
+ test suite (they must agree on every assignment), and uses the SAME strong-
386
+ Kleene truth functions and the SAME rejection (:func:`_reject_if_unsupported`).
387
+ """
388
+ if isinstance(node, Atom):
389
+ constant = _truth_value(node)
390
+ if constant is not None:
391
+ value = TRUE if constant else FALSE
392
+ return lambda values: value
393
+ i = index[atom_key(node)]
394
+ return lambda values: values[i]
395
+ if isinstance(node, Not):
396
+ inner = _compile(node.formula, index)
397
+ return lambda values: 1.0 - inner(values)
398
+ if isinstance(node, (And, Or, Xor, Implies, Iff)):
399
+ fa = _compile(node.left, index)
400
+ fb = _compile(node.right, index)
401
+ if isinstance(node, And):
402
+ return lambda values: min(fa(values), fb(values))
403
+ if isinstance(node, Or):
404
+ return lambda values: max(fa(values), fb(values))
405
+ if isinstance(node, Implies):
406
+ return lambda values: max(1.0 - fa(values), fb(values))
407
+ if isinstance(node, Iff):
408
+ return lambda values: min(max(1.0 - fa(values), fb(values)),
409
+ max(1.0 - fb(values), fa(values)))
410
+ # Xor: (a ∨ b) ∧ ¬(a ∧ b) = min(max(a, b), 1 − min(a, b))
411
+ return lambda values: min(max(fa(values), fb(values)),
412
+ 1.0 - min(fa(values), fb(values)))
413
+ # A Quantifier never reaches here (callers instantiate it away); any other
414
+ # node is rejected exactly as kleene_value would reject it.
415
+ _reject_if_unsupported(node)
416
+
417
+
418
+ def _parameter_instances(formulas: Sequence[Node],
419
+ domain: Optional[Collection[str]]) -> Iterator[List[Node]]:
420
+ """The formulas of a problem under each assignment of its free variables to the domain.
421
+
422
+ A variable that is free in some formula of a problem that also has a quantifier is a
423
+ parameter of the problem: ONE element of the ``domain`` the quantifiers range over, the
424
+ same in every formula. Each assignment of the domain's elements to the free variables
425
+ gives the formulas with those variables replaced by the constants of the elements
426
+ (a quantifier that binds the name keeps it: only free occurrences are replaced), and
427
+ the problem is decided under every assignment (valid, a consequence) or under some
428
+ (satisfiable). Where nothing makes the domain the range of a variable (no quantifier,
429
+ no domain, no free variable) the formulas come out once, unchanged.
430
+
431
+ Raises:
432
+ ValueError: there are more assignments than :data:`MAX_MODELS`.
433
+ """
434
+ formulas = list(formulas)
435
+ names: Tuple[str, ...] = ()
436
+ if domain and any(formula.count(Quantifier) for formula in formulas):
437
+ names = free_parameter_names(formulas)
438
+ if not names or not domain:
439
+ yield formulas
440
+ return
441
+ elements = sorted(set(domain), key=str)
442
+ total = len(elements) ** len(names)
443
+ if total > MAX_MODELS:
444
+ raise ValueError(
445
+ f"The {len(names)} free variable(s) {list(names)} are parameters over a domain of "
446
+ f"{len(elements)} elements: {len(elements)}**{len(names)} = {total} assignments, "
447
+ f"above MAX_MODELS = {MAX_MODELS}. Shrink the domain, bind the variables with "
448
+ "quantifiers, or replace them by constants.")
449
+ for chosen in product(elements, repeat=len(names)):
450
+ instance = formulas
451
+ for name, element in zip(names, chosen):
452
+ instance = [_ground(formula, name, element) for formula in instance]
453
+ yield instance
454
+
455
+
456
+ def _prepare_enumeration(formulas: Sequence[Node], domain: Optional[Set[str]],
457
+ route: str = "manyvalued"):
458
+ """Ground quantifiers, collect ground-atom keys, and compile each formula.
459
+
460
+ Returns ``(keys, compiled)`` with ``compiled[i](values) == kleene_value(
461
+ formulas[i], dict(zip(keys, values)), domain=domain)`` for every assignment.
462
+ Raises ValueError if a quantified formula is given without a domain, or if the
463
+ ``3 ** len(keys)`` enumeration would exceed :data:`MAX_MODELS`.
464
+ """
465
+ grounded: List[Node] = []
466
+ for formula in formulas:
467
+ if formula.count(Quantifier):
468
+ if not domain:
469
+ raise ValueError(
470
+ "Enumerating a quantified formula requires a non-empty 'domain'."
471
+ )
472
+ grounded.append(_instantiate(formula, set(domain)))
473
+ else:
474
+ grounded.append(formula)
475
+ keys = _atom_keys(*grounded, route=route)
476
+ total = 3 ** len(keys)
477
+ if total > MAX_MODELS:
478
+ raise ValueError(
479
+ f"Three-valued enumeration would visit 3**{len(keys)} = {total} "
480
+ f"assignments, above MAX_MODELS = {MAX_MODELS}. Reduce the number of "
481
+ "distinct ground atoms, or raise manyvalued.MAX_MODELS if you really "
482
+ "want to wait."
483
+ )
484
+ index = {key: i for i, key in enumerate(keys)}
485
+ compiled = [_compile(g, index) for g in grounded]
486
+ return keys, compiled
487
+
488
+
489
+ def is_valid(formula: Node, logic: str = "K3",
490
+ domain: Optional[Set[str]] = None) -> bool:
491
+ """True iff the formula is designated under *every* three-valued assignment.
492
+
493
+ Enumerates all ``3**n`` assignments of the formula's ``n`` distinct ground
494
+ atoms to ``{0.0, 0.5, 1.0}`` (after grounding quantifiers over ``domain``)
495
+ and checks the value is designated for ``logic`` in each. ``n`` is the number
496
+ of ground atoms; cost is exponential in ``n``, so keep formulas (and the
497
+ domain) small. Each assignment is scored by a compiled evaluator (built once
498
+ from the formula); above :data:`MAX_MODELS` assignments a ValueError is raised
499
+ rather than hanging.
500
+
501
+ A variable that is free in a quantified formula is a parameter: one element of ``domain``,
502
+ and the formula is valid when it is designated under every assignment of the domain's
503
+ elements to its free variables as well (see the module docstring).
504
+
505
+ Args:
506
+ formula: a classical FOL formula node (built with ``MSFLParser()``).
507
+ logic: ``"K3"`` (designate {1.0}) or ``"LP"`` (designate {0.5, 1.0}).
508
+ domain: constant names for any quantifiers; required if quantified.
509
+
510
+ Raises:
511
+ NotImplementedError: a sorted construct, or two different atoms that print alike
512
+ (see the module docstring).
513
+ """
514
+ designated = _designated_set(logic)
515
+ for instance in _parameter_instances([formula], domain):
516
+ keys, (evaluate,) = _prepare_enumeration(instance, domain, "manyvalued.is_valid")
517
+ if not all(evaluate(values) in designated
518
+ for values in product(TRUTH_VALUES, repeat=len(keys))):
519
+ return False
520
+ return True
521
+
522
+
523
+ def is_satisfiable(formula: Node, logic: str = "K3",
524
+ domain: Optional[Set[str]] = None) -> bool:
525
+ """True iff *some* three-valued assignment designates the formula.
526
+
527
+ Enumerates the same ``3**n`` assignments as :func:`is_valid` and returns
528
+ True as soon as one yields a value designated for ``logic``. A variable that is free in a
529
+ quantified formula is a parameter over ``domain`` (see :func:`is_valid`): the formula is
530
+ satisfiable when it is under some assignment of the domain's elements to its free
531
+ variables.
532
+ """
533
+ designated = _designated_set(logic)
534
+ for instance in _parameter_instances([formula], domain):
535
+ keys, (evaluate,) = _prepare_enumeration(instance, domain, "manyvalued.is_satisfiable")
536
+ if any(evaluate(values) in designated
537
+ for values in product(TRUTH_VALUES, repeat=len(keys))):
538
+ return True
539
+ return False
540
+
541
+
542
+ def entails(premises: Sequence[Node], conclusion: Node, logic: str = "K3",
543
+ domain: Optional[Set[str]] = None) -> bool:
544
+ """Designation-preserving entailment: every model of the premises models the conclusion.
545
+
546
+ True iff for every three-valued assignment that designates *all* premises,
547
+ the conclusion is also designated (for ``logic``). The enumeration ranges
548
+ over the ground atoms of the premises *and* the conclusion together.
549
+
550
+ This is where K3 and LP diverge sharply: under LP the explosion inference
551
+ ``entails([P, ¬P], Q, "LP")`` is False (at P=0.5, Q=0.0 both premises are
552
+ designated yet Q is not), so LP is paraconsistent; under K3 it holds
553
+ vacuously because ``P`` and ``¬P`` are never both designated.
554
+
555
+ A variable that is free in some formula of a problem that has a quantifier is a parameter:
556
+ ONE element of ``domain``, the same in the premises and the conclusion, and the
557
+ conclusion must follow under every assignment of the domain's elements to the free
558
+ variables. ``∀y P(y) ⊢ P(x)`` holds in LP over ``{a, b}``, ``P(x) ⊢ ∃y P(y)`` holds, and
559
+ ``P(x), Q(y) ⊢ ∀z (P(z) ∧ Q(z))`` does not.
560
+
561
+ Args:
562
+ premises: a sequence of classical formula nodes.
563
+ conclusion: a classical formula node.
564
+ logic: ``"K3"`` or ``"LP"``.
565
+ domain: constant names for any quantifiers; required if quantified.
566
+
567
+ Raises:
568
+ NotImplementedError: a sorted construct, or two different atoms that print alike
569
+ (see the module docstring).
570
+ """
571
+ designated = _designated_set(logic)
572
+ premises = list(premises)
573
+ for instance in _parameter_instances([*premises, conclusion], domain):
574
+ keys, compiled = _prepare_enumeration(instance, domain, "manyvalued.entails")
575
+ *premise_fns, conclusion_fn = compiled
576
+ for values in product(TRUTH_VALUES, repeat=len(keys)):
577
+ if all(f(values) in designated for f in premise_fns):
578
+ if conclusion_fn(values) not in designated:
579
+ return False
580
+ return True