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,358 @@
1
+ """Third-order finite-model semantics: satisfaction where an argument can be a property.
2
+
3
+ :mod:`semantics.secondorder` quantifies over relations on the domain. This
4
+ evaluator adds the level above: a predicate whose ARGUMENT is a relation —
5
+ ``Positive(G)``, ``Essence(G, x)``, ``Positive(λx. ¬G(x))`` — and quantification
6
+ over such predicates. It reuses :class:`semantics.tarski.Structure` and
7
+ :func:`semantics.tarski.term_value` unchanged; what changes is that an argument
8
+ slot may now be filled by a relation rather than an individual, so a "tuple in
9
+ the extension" may have relations among its components.
10
+
11
+ **Slots, not just arities.** At second order a bound predicate variable is fully
12
+ described by its arity. At third order it is not: ``Positive`` and ``G`` can both
13
+ have arity 1 and mean entirely different things, because ``G``'s slot holds an
14
+ individual and ``Positive``'s holds a property. What each slot holds is inferred
15
+ by :func:`unicode_logic_kit.fol.nodes.analyse_signatures` and threaded through the
16
+ evaluation, which is why this module's quantifier case enumerates over a
17
+ SIGNATURE where the second-order one enumerates over an arity.
18
+
19
+ **Cost.** An individual slot ranges over the ``n`` domain elements; a property
20
+ slot of arity ``j`` over the ``2 ** (n ** j)`` relations of that arity. A
21
+ predicate of a given signature is any subset of the product of its slots, so a
22
+ monadic third-order predicate over a domain of ``n`` has ``2 ** (2 ** n)``
23
+ interpretations: 16 for ``n = 2``, 256 for 3, 65 536 for 4, about 4·10⁹ for 5.
24
+ Small, then suddenly not — which is what :data:`MAX_INTERPRETATIONS` guards, by
25
+ raising a clear error rather than hanging.
26
+
27
+ **Scope.** Two-valued, classical, constant-domain. Łukasiewicz and modal nodes
28
+ are rejected by name, as in the second-order evaluator — a third-order MODAL
29
+ formula is not evaluated here but exported by
30
+ :mod:`unicode_logic_kit.hol.ho_modal`. λ-abstractions are NOT rejected here,
31
+ unlike at second order: in argument position a λ is precisely how an anonymous
32
+ property is written, and it is evaluated to its extension. A λ anywhere else
33
+ still has no reading.
34
+ """
35
+
36
+ from itertools import product
37
+ from typing import Any, FrozenSet, Iterable, List, Mapping, Optional, Tuple
38
+
39
+ from ..fol.nodes import (
40
+ Node,
41
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
42
+ SortedQuantifier, SecondOrderQuantifier,
43
+ Lambda, LambdaVar, PredicateTerm, Variable,
44
+ analyse_signatures,
45
+ )
46
+ from ..fol._ho_nodes import INDIVIDUAL, Signatures
47
+ from .tarski import (
48
+ Structure, term_value, _atom_value, _extend,
49
+ _FORALL, _EXISTS, _FUZZY_TYPES,
50
+ )
51
+
52
+ #: A relation: a (frozen)set of argument tuples. A component of one of those
53
+ #: tuples is an individual at first/second order, and may be another relation at
54
+ #: third order — which is the whole difference.
55
+ Relation = FrozenSet[Tuple[Any, ...]]
56
+ PredBinding = Mapping[str, Relation]
57
+
58
+ #: Safety cap on one quantifier's enumeration. See the module docstring for how
59
+ #: fast the count grows; raise this attribute if you really mean to enumerate
60
+ #: more.
61
+ MAX_INTERPRETATIONS = 1 << 20 # ~1.05 million
62
+
63
+
64
+ def slot_values(kind, domain: Tuple[Any, ...]) -> Tuple[Any, ...]:
65
+ """Return everything one argument slot can hold.
66
+
67
+ ``kind`` is :data:`unicode_logic_kit.fol._ho_nodes.INDIVIDUAL` — in which case
68
+ the answer is the domain — or ``("p", j)``, in which case it is every
69
+ ``j``-ary relation on the domain, i.e. every subset of ``domain ** j``.
70
+ """
71
+ if kind == INDIVIDUAL:
72
+ return domain
73
+ arity = kind[1]
74
+ base = list(product(domain, repeat=arity))
75
+ return tuple(
76
+ frozenset(base[i] for i in range(len(base)) if (mask >> i) & 1)
77
+ for mask in range(1 << len(base))
78
+ )
79
+
80
+
81
+ def all_interpretations(signature: Tuple[Any, ...],
82
+ domain: Tuple[Any, ...]) -> Iterable[Relation]:
83
+ """Yield every interpretation of a predicate with the given argument ``signature``.
84
+
85
+ An interpretation is any subset of the product of the slots' value sets, so
86
+ the count is ``2 ** prod(len(slot_values(kind)) for kind in signature)``. An
87
+ empty signature (a propositional variable) has the single-element product
88
+ ``{()}``, hence exactly the two interpretations false and true — matching
89
+ the second-order evaluator's arity-0 case.
90
+ """
91
+ columns = [slot_values(kind, domain) for kind in signature]
92
+ base = list(product(*columns)) if columns else [()]
93
+ for mask in range(1 << len(base)):
94
+ yield frozenset(base[i] for i in range(len(base)) if (mask >> i) & 1)
95
+
96
+
97
+ def interpretation_count(signature: Tuple[Any, ...], domain_size: int) -> int:
98
+ """How many interpretations :func:`all_interpretations` would yield.
99
+
100
+ Computed without enumerating anything, so a caller (and the evaluator's own
101
+ guard) can refuse an impossible enumeration before starting it.
102
+ """
103
+ width = 1
104
+ for kind in signature:
105
+ width *= (domain_size if kind == INDIVIDUAL
106
+ else 1 << (domain_size ** kind[1]))
107
+ return 1 << width
108
+
109
+
110
+ def satisfies_to(formula: Node,
111
+ structure: Structure,
112
+ assignment: Optional[Mapping[str, Any]] = None,
113
+ pred_binding: Optional[PredBinding] = None,
114
+ signatures: Optional[Signatures] = None) -> bool:
115
+ """Return whether ``structure`` satisfies ``formula`` (third-order Tarski).
116
+
117
+ ``assignment`` maps object-variable names to individuals; ``pred_binding``
118
+ maps a bound predicate name to its current interpretation. ``signatures``
119
+ is the argument-slot analysis; it is computed from ``formula`` when omitted,
120
+ and threaded unchanged through the recursion so every occurrence of a symbol
121
+ is read at the same type.
122
+
123
+ The recursion differs from :func:`semantics.secondorder.satisfies_so` in
124
+ exactly two places, both about arguments:
125
+
126
+ - an **atom's** arguments are evaluated with :func:`argument_value`, so an
127
+ argument may be a :class:`~unicode_logic_kit.fol.nodes.PredicateTerm` or a
128
+ λ-abstraction and evaluate to a RELATION. When every argument is an
129
+ ordinary term the atom is handed to the first-order
130
+ :func:`semantics.tarski._atom_value` unchanged, so ``=`` / ``≠`` and the
131
+ arithmetic fallbacks behave exactly as they do at first order;
132
+ - a **predicate quantifier** enumerates over the bound symbol's SIGNATURE
133
+ rather than its arity, which is what makes ``∀Positive`` range over
134
+ predicates of properties instead of over relations on the domain.
135
+
136
+ Raises:
137
+ ValueError: on a Łukasiewicz node, an unknown quantifier or node type,
138
+ or when a quantifier's enumeration would exceed
139
+ :data:`MAX_INTERPRETATIONS`.
140
+ NotImplementedError: on a modal node (use the Kripke evaluator, or
141
+ export a third-order modal formula with ``hol.ho_modal``), or on a
142
+ λ outside argument position.
143
+ """
144
+ if assignment is None:
145
+ assignment = {}
146
+ if pred_binding is None:
147
+ pred_binding = {}
148
+ if signatures is None:
149
+ signatures = analyse_signatures([formula])
150
+
151
+ if isinstance(formula, Atom):
152
+ return _atom_truth(formula, structure, assignment, pred_binding, signatures)
153
+
154
+ if isinstance(formula, Not):
155
+ return not satisfies_to(formula.formula, structure, assignment,
156
+ pred_binding, signatures)
157
+ if isinstance(formula, And):
158
+ return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
159
+ and satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
160
+ if isinstance(formula, Or):
161
+ return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
162
+ or satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
163
+ if isinstance(formula, Xor):
164
+ return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
165
+ != satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
166
+ if isinstance(formula, Implies):
167
+ return ((not satisfies_to(formula.left, structure, assignment, pred_binding, signatures))
168
+ or satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
169
+ if isinstance(formula, Iff):
170
+ return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
171
+ == satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
172
+
173
+ if isinstance(formula, Quantifier):
174
+ return _object_quantifier(formula.type, formula.variable.name,
175
+ structure.domain, formula.formula, structure,
176
+ assignment, pred_binding, signatures)
177
+ if isinstance(formula, SortedQuantifier):
178
+ return _object_quantifier(formula.type, formula.variable.name,
179
+ structure.sort_universe(formula.sort),
180
+ formula.formula, structure, assignment,
181
+ pred_binding, signatures)
182
+ if isinstance(formula, SecondOrderQuantifier):
183
+ return _predicate_quantifier(formula, structure, assignment,
184
+ pred_binding, signatures)
185
+
186
+ if isinstance(formula, _FUZZY_TYPES):
187
+ raise ValueError(
188
+ f"Cannot evaluate Łukasiewicz node {type(formula).__name__} with the "
189
+ "two-valued third-order evaluator; use the fuzzy evaluator instead.")
190
+ if isinstance(formula, Lambda):
191
+ raise NotImplementedError(
192
+ "A λ-abstraction denotes a PROPERTY, not a truth value; it has a "
193
+ "reading here only in argument position (Positive(λx. ¬G(x))).")
194
+ if type(formula).__name__ in ("Box", "Diamond"):
195
+ raise NotImplementedError(
196
+ f"Modal node {type(formula).__name__} is out of scope for the "
197
+ "third-order evaluator; use the Kripke evaluator, or export the "
198
+ "formula with hol.ho_modal for a higher-order prover.")
199
+ raise ValueError(f"satisfies_to: unsupported node type {type(formula).__name__}.")
200
+
201
+
202
+ def _extension_in(structure: Structure, name: str, arity: int) -> Iterable[Tuple[Any, ...]]:
203
+ """The extension of the predicate ``name/arity`` in ``structure``.
204
+
205
+ The structure's table for it. A missing table is the empty relation, matching
206
+ the first-order convention -- unless the name is a SORT of the structure: a sort
207
+ and the unary predicate of that name are one symbol, so a unary ``name`` that
208
+ the structure knows only as a sort is that sort's universe, one 1-tuple per
209
+ member. This is the reading :func:`semantics.tarski._atom_value` gives an atom
210
+ ``S(t)``, and the laws a sort must satisfy are checked here as they are there
211
+ (an :class:`~unicode_logic_kit.semantics.tarski.IllegalStructureError` for an
212
+ empty sort, or one that disagrees with the predicate table of its name).
213
+ """
214
+ extension = structure.predicates.get((name, arity))
215
+ if arity == 1 and name in structure.sorts:
216
+ universe = structure.sort_universe(name)
217
+ if extension is None:
218
+ return {(d,) for d in universe}
219
+ return extension if extension is not None else ()
220
+
221
+
222
+ def argument_value(node: Node, structure: Structure,
223
+ assignment: Mapping[str, Any],
224
+ pred_binding: PredBinding,
225
+ signatures: Signatures) -> Any:
226
+ """Evaluate a node in ARGUMENT position — to an individual or to a relation.
227
+
228
+ A :class:`~unicode_logic_kit.fol.nodes.PredicateTerm` denotes the relation its
229
+ name stands for: the current binding if the name is bound, otherwise the
230
+ structure's table for it (a missing table is the empty relation, matching
231
+ the first-order convention, except that a unary name the structure knows as
232
+ a sort denotes that sort -- see :func:`_extension_in`). A λ-abstraction
233
+ denotes its EXTENSION, computed
234
+ by evaluating the body at every tuple of individuals its binders range over.
235
+ Anything else is an ordinary term.
236
+ """
237
+ if isinstance(node, PredicateTerm):
238
+ if node.name in pred_binding:
239
+ return pred_binding[node.name]
240
+ arity = signatures.arity.get(node.name, 1)
241
+ return frozenset(_extension_in(structure, node.name, arity))
242
+ if isinstance(node, Lambda):
243
+ names: List[str] = []
244
+ body: Node = node
245
+ while isinstance(body, Lambda):
246
+ names.append(body.param.name)
247
+ body = body.body
248
+ body = _lambda_vars_as_variables(body)
249
+ extension = []
250
+ for values in product(structure.domain, repeat=len(names)):
251
+ local = dict(assignment)
252
+ local.update(zip(names, values))
253
+ if satisfies_to(body, structure, local, pred_binding, signatures):
254
+ extension.append(values)
255
+ return frozenset(extension)
256
+ return term_value(node, structure, assignment)
257
+
258
+
259
+ def _lambda_vars_as_variables(node: Node) -> Node:
260
+ """Turn every LambdaVar in ``node`` back into an ordinary object Variable.
261
+
262
+ The parser resolves a λ binder's occurrences to LambdaVar, which the
263
+ first-order term evaluator has no reading for -- correctly, since a
264
+ λ-abstraction is not a term it can evaluate. Here the binder IS being
265
+ interpreted: its variables are about to range over the domain through the
266
+ assignment, which is exactly what an object variable does. Converting them
267
+ is sound including under nesting, because an inner λ's own binder is
268
+ re-bound by name on the way in, so the shadowing the LambdaVars encoded is
269
+ reproduced by the assignment.
270
+ """
271
+ if isinstance(node, LambdaVar):
272
+ return Variable(node.name)
273
+ return node.map_children(_lambda_vars_as_variables)
274
+
275
+
276
+ def _atom_truth(atom: Atom, structure: Structure,
277
+ assignment: Mapping[str, Any], pred_binding: PredBinding,
278
+ signatures: Signatures) -> bool:
279
+ """Truth of an atom, consulting ``pred_binding`` and allowing property arguments.
280
+
281
+ An atom whose arguments are all ordinary terms and whose predicate is not
282
+ currently bound is delegated to the first-order
283
+ :func:`semantics.tarski._atom_value` verbatim — so nothing about first-order
284
+ satisfaction is re-implemented, and ``=`` / ``≠`` are never treated as
285
+ bindable.
286
+ """
287
+ higher_order = any(isinstance(a, (PredicateTerm, Lambda)) for a in atom.args)
288
+ if atom.predicate in pred_binding and atom.predicate not in ("=", "≠"):
289
+ values = tuple(argument_value(a, structure, assignment, pred_binding, signatures)
290
+ for a in atom.args)
291
+ return values in pred_binding[atom.predicate]
292
+ if not higher_order:
293
+ return _atom_value(atom, structure, assignment)
294
+ values = tuple(argument_value(a, structure, assignment, pred_binding, signatures)
295
+ for a in atom.args)
296
+ extension = _extension_in(structure, atom.predicate, len(atom.args))
297
+ return values in extension
298
+
299
+
300
+ def _object_quantifier(qtype: str, var_name: str, universe: Iterable[Any],
301
+ body: Node, structure: Structure,
302
+ assignment: Mapping[str, Any], pred_binding: PredBinding,
303
+ signatures: Signatures) -> bool:
304
+ """Evaluate an object-level ∀/∃, threading the predicate binding and signatures."""
305
+ if qtype in _FORALL:
306
+ return all(satisfies_to(body, structure, _extend(assignment, var_name, d),
307
+ pred_binding, signatures) for d in universe)
308
+ if qtype in _EXISTS:
309
+ return any(satisfies_to(body, structure, _extend(assignment, var_name, d),
310
+ pred_binding, signatures) for d in universe)
311
+ raise ValueError(f"Unknown quantifier type: {qtype!r}")
312
+
313
+
314
+ def _predicate_quantifier(node: SecondOrderQuantifier, structure: Structure,
315
+ assignment: Mapping[str, Any],
316
+ pred_binding: PredBinding,
317
+ signatures: Signatures) -> bool:
318
+ """Evaluate ∀P / ∃P by enumerating every interpretation of P's SIGNATURE.
319
+
320
+ The signature — not the arity — is what decides the range: an arity-1 ``P``
321
+ whose slot holds an individual ranges over the ``2 ** n`` subsets of the
322
+ domain (the second-order case), while an arity-1 ``P`` whose slot holds a
323
+ monadic property ranges over the ``2 ** (2 ** n)`` sets of such properties.
324
+ Both fall out of the same enumeration.
325
+ """
326
+ signature = signatures.slots.get(node.predicate)
327
+ if signature is None:
328
+ signature = tuple(INDIVIDUAL for _ in range(node.arity))
329
+ count = interpretation_count(signature, len(structure.domain))
330
+ if count > MAX_INTERPRETATIONS:
331
+ raise ValueError(
332
+ f"Predicate quantifier {node.type}{node.predicate} with signature "
333
+ f"{signature} over a {len(structure.domain)}-element domain would "
334
+ f"enumerate {count} interpretations, above MAX_INTERPRETATIONS = "
335
+ f"{MAX_INTERPRETATIONS}. Shrink the domain (or raise "
336
+ "thirdorder.MAX_INTERPRETATIONS).")
337
+ candidates = all_interpretations(signature, structure.domain)
338
+ if node.type in _FORALL:
339
+ return all(satisfies_to(node.formula, structure, assignment,
340
+ _extend(pred_binding, node.predicate, candidate),
341
+ signatures)
342
+ for candidate in candidates)
343
+ if node.type in _EXISTS:
344
+ return any(satisfies_to(node.formula, structure, assignment,
345
+ _extend(pred_binding, node.predicate, candidate),
346
+ signatures)
347
+ for candidate in candidates)
348
+ raise ValueError(f"Unknown predicate quantifier type: {node.type!r}")
349
+
350
+
351
+ def holds_to(formula: Node, structure: Structure) -> bool:
352
+ """Convenience: ``satisfies_to(formula, structure)`` for a closed formula.
353
+
354
+ Reads as "structure satisfies the third-order sentence": the empty
355
+ assignment and empty predicate binding are what a formula with no free
356
+ object or predicate variables needs.
357
+ """
358
+ return satisfies_to(formula, structure)
@@ -0,0 +1,85 @@
1
+ """Continuous t-norms for the fuzzy (BL / Hájek basic-logic) family.
2
+
3
+ A *t-norm* ``T`` fixes the semantics of the **strong** fuzzy connectives over the
4
+ real interval ``[0, 1]``:
5
+
6
+ - strong conjunction ``⊗`` is ``T`` itself;
7
+ - strong disjunction ``⊕`` is the dual *t-conorm* ``S(x, y) = 1 − T(1−x, 1−y)``;
8
+ - implication ``→`` is the *residuum* ``R(x, y) = sup {z : T(x, z) ≤ y}``;
9
+ - negation ``¬x`` is ``R(x, 0)``;
10
+ - biconditional ``↔`` is ``min(R(x, y), R(y, x))``.
11
+
12
+ The *weak* connectives ``∧`` / ``∨`` are always ``min`` / ``max`` (the lattice meet /
13
+ join), independent of the t-norm, and so are the quantifiers ``∀`` (infimum) / ``∃``
14
+ (supremum). Three continuous t-norms — whose ordinal sums generate every
15
+ continuous t-norm (the Mostert–Shields theorem) — ship here:
16
+
17
+ - **Łukasiewicz** ``T(x, y) = max(0, x + y − 1)`` (the default; involutive negation);
18
+ - **Gödel** ``T(x, y) = min(x, y)`` (idempotent; a relative pseudo-complement negation);
19
+ - **product** ``T(x, y) = x · y`` (strict; Goguen residuum ``y / x``).
20
+
21
+ Public API: :class:`TNorm`, the instances :data:`LUKASIEWICZ`, :data:`GODEL`,
22
+ :data:`PRODUCT`, the registry :data:`TNORMS`, and :func:`get_tnorm`.
23
+ """
24
+
25
+ from dataclasses import dataclass
26
+ from typing import Callable
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class TNorm:
31
+ """A t-norm and the strong connective operations it induces over ``[0, 1]``.
32
+
33
+ ``conj`` / ``disj`` / ``impl`` / ``neg`` are the strong ⊗ / ⊕ / → / ¬; ``equiv``
34
+ is derived as ``min(x→y, y→x)``. (The weak ∧/∨ are min/max regardless and live
35
+ in the evaluator, not here.)
36
+ """
37
+
38
+ name: str
39
+ conj: Callable[[float, float], float]
40
+ disj: Callable[[float, float], float]
41
+ impl: Callable[[float, float], float]
42
+ neg: Callable[[float], float]
43
+
44
+ def equiv(self, x: float, y: float) -> float:
45
+ """Residuated biconditional ``min(x → y, y → x)``."""
46
+ return min(self.impl(x, y), self.impl(y, x))
47
+
48
+
49
+ LUKASIEWICZ = TNorm(
50
+ "lukasiewicz",
51
+ conj=lambda x, y: max(0.0, x + y - 1.0),
52
+ disj=lambda x, y: min(1.0, x + y),
53
+ impl=lambda x, y: min(1.0, 1.0 - x + y),
54
+ neg=lambda x: 1.0 - x,
55
+ )
56
+
57
+ GODEL = TNorm(
58
+ "godel",
59
+ conj=lambda x, y: min(x, y),
60
+ disj=lambda x, y: max(x, y),
61
+ impl=lambda x, y: 1.0 if x <= y else y,
62
+ neg=lambda x: 1.0 if x <= 0.0 else 0.0,
63
+ )
64
+
65
+ PRODUCT = TNorm(
66
+ "product",
67
+ conj=lambda x, y: x * y,
68
+ disj=lambda x, y: x + y - x * y,
69
+ impl=lambda x, y: 1.0 if x <= y else y / x,
70
+ neg=lambda x: 1.0 if x <= 0.0 else 0.0,
71
+ )
72
+
73
+ #: The three built-in t-norms by name (``"lukasiewicz"``, ``"godel"``,
74
+ #: ``"product"``) — what :func:`get_tnorm` looks up and what the fuzzy
75
+ #: evaluator accepts as a ``tnorm=`` argument.
76
+ TNORMS = {t.name: t for t in (LUKASIEWICZ, GODEL, PRODUCT)}
77
+
78
+
79
+ def get_tnorm(name: str) -> TNorm:
80
+ """Return the :class:`TNorm` for ``name`` (``"lukasiewicz"`` / ``"godel"`` / ``"product"``)."""
81
+ try:
82
+ return TNORMS[name]
83
+ except KeyError:
84
+ raise ValueError(
85
+ f"unknown t-norm {name!r}; use one of {sorted(TNORMS)}.") from None
@@ -0,0 +1,201 @@
1
+ """Truth tables for propositional formulas — classical, Kleene K3, and Priest LP.
2
+
3
+ A truth table enumerates every assignment of truth values to the formula's atoms and
4
+ records the formula's value (and whether that value is *designated*) under each. The
5
+ three logics share the strong-Kleene connective tables and differ only in the value
6
+ set and the designated values:
7
+
8
+ - **classical** — values ``{0, 1}``; designated ``{1}``.
9
+ - **K3** (strong Kleene) — values ``{0, ½, 1}``; designated ``{1}``.
10
+ - **LP** (Priest) — values ``{0, ½, 1}``; designated ``{½, 1}`` (paraconsistent).
11
+
12
+ Restricted to ``{0, 1}`` the strong-Kleene tables coincide with the classical ones,
13
+ so all three reuse :func:`unicode_logic_kit.semantics.manyvalued.kleene_value`.
14
+
15
+ Each distinct atom (by surface form — ``P`` and ``P(a)`` are different columns) is a
16
+ propositional variable, except the nullary atoms ``$true`` and ``$false`` (``⊤`` and
17
+ ``⊥``): those are the constants truth and falsity, they get no column, and their
18
+ value is ``1`` and ``0`` in every row. Quantified formulas have no finite truth table
19
+ and are rejected. A column is named by the text the atom prints as, so two different
20
+ atoms that print alike (the numeral ``1`` and a constant named ``1``, a free variable
21
+ ``x`` and a constant named ``x``) would be one column: such a formula is refused by name,
22
+ and so is a sorted constant (``alice:Human``), whose sort is a fact that a truth table
23
+ has no way to state.
24
+
25
+ Public API: :func:`truth_table`, :class:`TruthTable`, and the convenience predicates
26
+ :func:`is_tautology`, :func:`is_contradiction`, :func:`is_satisfiable_tt`.
27
+ """
28
+
29
+ import html
30
+ from dataclasses import dataclass
31
+ from itertools import product
32
+ from typing import Dict, List, Tuple
33
+
34
+ from ..fol.nodes import Node, Atom, Quantifier, SortedQuantifier
35
+ from ..fol._atom_keys import AtomKeys, atom_key
36
+ from ..fol._so_nodes import SecondOrderQuantifier
37
+ from ..fol._truth_constants import truth_value as _truth_value
38
+ from .manyvalued import _kleene_value, DESIGNATED
39
+
40
+
41
+ # Value sets and designated sets per logic (classical added to the K3/LP table).
42
+ _VALUES = {
43
+ "classical": (1.0, 0.0),
44
+ "K3": (1.0, 0.5, 0.0),
45
+ "LP": (1.0, 0.5, 0.0),
46
+ }
47
+ _DESIGNATED = {"classical": frozenset({1.0}), **DESIGNATED}
48
+
49
+ # Display glyphs.
50
+ _GLYPH_BOOL = {1.0: "T", 0.0: "F"}
51
+ _GLYPH_MANY = {1.0: "1", 0.5: "½", 0.0: "0"}
52
+
53
+
54
+ def _collect_atoms(formula: Node) -> List[str]:
55
+ """Return the distinct atom surface-forms in ``formula``, in first-occurrence order.
56
+
57
+ Raises ValueError on a quantified formula (no finite truth table exists).
58
+ """
59
+ if isinstance(formula, (Quantifier, SortedQuantifier, SecondOrderQuantifier)):
60
+ raise ValueError(
61
+ "truth_table: quantified formulas have no finite truth table; "
62
+ "use the finite model finder or an evaluator instead."
63
+ )
64
+ order: List[str] = []
65
+ seen = set()
66
+
67
+ def walk(node: Node) -> None:
68
+ if isinstance(node, (Quantifier, SortedQuantifier, SecondOrderQuantifier)):
69
+ raise ValueError(
70
+ "truth_table: quantified formulas have no finite truth table."
71
+ )
72
+ if isinstance(node, Atom):
73
+ if _truth_value(node) is not None:
74
+ return # `$true` / `$false` are constants, not columns
75
+ key = atom_key(node)
76
+ if key not in seen:
77
+ seen.add(key)
78
+ order.append(key)
79
+ return
80
+ for child in node._child_nodes():
81
+ walk(child)
82
+
83
+ walk(formula)
84
+ return order
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class TruthTable:
89
+ """A computed truth table: atom columns, value rows, and the designated flags.
90
+
91
+ ``atoms`` are the column headers (atom surface-forms). ``rows`` is a tuple of
92
+ ``(assignment, value, designated)`` where ``assignment`` is the tuple of atom
93
+ values aligned with ``atoms``, ``value`` is the formula's value, and
94
+ ``designated`` says whether ``value`` is designated under ``logic``.
95
+ """
96
+
97
+ formula: Node
98
+ atoms: Tuple[str, ...]
99
+ rows: Tuple[Tuple[Tuple[float, ...], float, bool], ...]
100
+ logic: str
101
+
102
+ @property
103
+ def is_tautology(self) -> bool:
104
+ """True iff the formula is designated under every assignment."""
105
+ return all(d for _, _, d in self.rows)
106
+
107
+ @property
108
+ def is_contradiction(self) -> bool:
109
+ """True iff the formula is designated under no assignment."""
110
+ return not any(d for _, _, d in self.rows)
111
+
112
+ @property
113
+ def is_satisfiable(self) -> bool:
114
+ """True iff the formula is designated under some assignment."""
115
+ return any(d for _, _, d in self.rows)
116
+
117
+ def _rows_and_glyphs(self) -> Tuple[List[str], Dict[float, str]]:
118
+ """Column headers (atoms + the formula) and the value-to-glyph map for ``logic``.
119
+
120
+ Shared by :meth:`render` (Markdown) and :meth:`_repr_html_` (HTML) so the
121
+ two renderers walk ``self.rows`` with the same headers and the same glyphs
122
+ and can never quietly disagree.
123
+ """
124
+ glyph = _GLYPH_BOOL if self.logic == "classical" else _GLYPH_MANY
125
+ head = list(self.atoms) + [self.formula.to_unicode_str()]
126
+ return head, glyph
127
+
128
+ def render(self) -> str:
129
+ """Render the truth table as a GitHub-flavoured Markdown table."""
130
+ head, glyph = self._rows_and_glyphs()
131
+ lines = ["| " + " | ".join(head) + " |",
132
+ "|" + "|".join(["---"] * len(head)) + "|"]
133
+ for assignment, value, _ in self.rows:
134
+ cells = [glyph[v] for v in assignment] + [glyph[value]]
135
+ lines.append("| " + " | ".join(cells) + " |")
136
+ return "\n".join(lines)
137
+
138
+ def __str__(self) -> str:
139
+ return self.render()
140
+
141
+ def _repr_html_(self) -> str:
142
+ """Jupyter/IPython rich-display hook: the truth table as an HTML ``<table>``.
143
+
144
+ Built directly from ``self.atoms``/``self.rows`` via :meth:`_rows_and_glyphs`
145
+ — NOT by round-tripping ``render()``'s Markdown through a Markdown-to-HTML
146
+ parser, since no such parser is a dependency of this kit (and none is added
147
+ for this). Column/atom labels come from user-chosen predicate and constant
148
+ names, so they are HTML-escaped; the glyphs themselves are always one of the
149
+ fixed strings in ``_GLYPH_BOOL``/``_GLYPH_MANY`` and need no escaping.
150
+ """
151
+ head, glyph = self._rows_and_glyphs()
152
+ header_cells = "".join(f"<th>{html.escape(h)}</th>" for h in head)
153
+ body_rows = []
154
+ for assignment, value, _ in self.rows:
155
+ cells = [glyph[v] for v in assignment] + [glyph[value]]
156
+ row_html = "".join(f"<td>{c}</td>" for c in cells)
157
+ body_rows.append(f"<tr>{row_html}</tr>")
158
+ return (f"<table><thead><tr>{header_cells}</tr></thead>"
159
+ f"<tbody>{''.join(body_rows)}</tbody></table>")
160
+
161
+
162
+ def truth_table(formula: Node, logic: str = "classical") -> TruthTable:
163
+ """Compute the :class:`TruthTable` of a propositional ``formula`` under ``logic``.
164
+
165
+ Args:
166
+ formula: a quantifier-free formula; each distinct atom is a propositional
167
+ variable.
168
+ logic: ``"classical"`` (values {0,1}), ``"K3"`` or ``"LP"`` (values {0,½,1}).
169
+
170
+ Raises:
171
+ ValueError: on an unknown logic or a quantified formula.
172
+ NotImplementedError: a sorted constant, or two different atoms that print alike.
173
+ """
174
+ if logic not in _VALUES:
175
+ raise ValueError(f"truth_table: unknown logic {logic!r} "
176
+ f"(use 'classical', 'K3', or 'LP').")
177
+ atoms = _collect_atoms(formula)
178
+ AtomKeys("truth_table", "refuse").letters([formula])
179
+ values = _VALUES[logic]
180
+ designated = _DESIGNATED[logic]
181
+ rows = []
182
+ for assignment in product(values, repeat=len(atoms)):
183
+ valuation: Dict[str, float] = dict(zip(atoms, assignment))
184
+ value = _kleene_value(formula, valuation, None, None)
185
+ rows.append((assignment, value, value in designated))
186
+ return TruthTable(formula, tuple(atoms), tuple(rows), logic)
187
+
188
+
189
+ def is_tautology(formula: Node, logic: str = "classical") -> bool:
190
+ """True iff ``formula`` is designated under every assignment (a logical truth)."""
191
+ return truth_table(formula, logic).is_tautology
192
+
193
+
194
+ def is_contradiction(formula: Node, logic: str = "classical") -> bool:
195
+ """True iff ``formula`` is designated under no assignment."""
196
+ return truth_table(formula, logic).is_contradiction
197
+
198
+
199
+ def is_satisfiable_tt(formula: Node, logic: str = "classical") -> bool:
200
+ """True iff ``formula`` is designated under some assignment."""
201
+ return truth_table(formula, logic).is_satisfiable