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,331 @@
1
+ """Relevant logic — the basic affixing system **B**, via simplified Routley–Meyer semantics.
2
+
3
+ Relevance logics reject the classical "paradoxes of material implication": in **B**
4
+ an implication ``A → B`` is a genuine entailment claim, so ``P → (Q → P)`` (an
5
+ irrelevant antecedent) and ``(P ∧ ¬P) → Q`` (ex falso quodlibet) are *not* theorems.
6
+ The semantics implemented here is the Priest–Sylvan **simplified** Routley–Meyer
7
+ semantics (Priest & Sylvan 1992, "Simplified semantics for basic relevant logics",
8
+ *J. Philosophical Logic*; Priest, *Introduction to Non-Classical Logic*, 2nd ed.,
9
+ ch. 10), which is sound and complete for B.
10
+
11
+ An interpretation is ``⟨W, N, *, R, V⟩``:
12
+
13
+ - ``W`` — a set of worlds, with ``N ⊆ W`` a **nonempty** set of *normal* worlds;
14
+ - ``*`` — the Routley star, an involution on worlds (``w** = w``), interpreting ``¬``;
15
+ - ``R`` — a ternary relation sourced only at NON-normal worlds
16
+ (``R ⊆ (W∖N) × W × W``), interpreting ``→`` at non-normal worlds;
17
+ - ``V`` — a valuation mapping each propositional atom to the set of worlds where
18
+ it holds.
19
+
20
+ Truth at a world ``w``:
21
+
22
+ - ``w ⊨ p`` iff ``w ∈ V(p)``
23
+ - ``w ⊨ A ∧ B`` iff ``w ⊨ A`` and ``w ⊨ B``
24
+ - ``w ⊨ A ∨ B`` iff ``w ⊨ A`` or ``w ⊨ B``
25
+ - ``w ⊨ ¬A`` iff ``w* ⊭ A`` (Routley star)
26
+ - ``w ⊨ A → B``, w ∈ N iff for ALL ``x ∈ W``: ``x ⊨ A`` ⇒ ``x ⊨ B``
27
+ - ``w ⊨ A → B``, w ∉ N iff for all ``x, y`` with ``R(w, x, y)``: ``x ⊨ A`` ⇒ ``y ⊨ B``
28
+
29
+ ``A ↔ B`` abbreviates ``(A → B) ∧ (B → A)``. A formula is **valid** iff it is true
30
+ at every normal world of every interpretation. Formulas are the classical
31
+ propositional nodes of the default ``MSFLParser()`` — ``And``, ``Or``, ``Not``,
32
+ ``Implies``, ``Iff`` over nullary ``Atom``s — reused with relevant semantics,
33
+ exactly as :mod:`.intuitionistic` reuses classical syntax over Kripke models.
34
+
35
+ Public API: :class:`RelevantModel`, :func:`rel_satisfies`,
36
+ :func:`rel_countermodel`, :func:`rel_valid`.
37
+ """
38
+
39
+ from collections.abc import Mapping as MappingABC
40
+ from dataclasses import dataclass, field
41
+ from itertools import chain, combinations, permutations, product
42
+ from typing import Dict, FrozenSet, Iterator, List, Mapping, Optional, Tuple
43
+
44
+ from ..fol.nodes import Node, Atom, Not, And, Or, Implies, Iff
45
+ from ..fol._msfl_nodes import key_text
46
+ from ..fol._truth_constants import refuse_truth_constants
47
+
48
+
49
+ class _FrozenMap(MappingABC):
50
+ """An immutable, hashable mapping: dict-like lookup over a frozen tuple of pairs."""
51
+
52
+ __slots__ = ("_dict", "_pairs")
53
+
54
+ def __init__(self, mapping):
55
+ items = mapping.items() if isinstance(mapping, MappingABC) else mapping
56
+ self._dict: Dict = dict(items)
57
+ # Sorted by key so equal mappings hash equally regardless of insertion order.
58
+ self._pairs: Tuple = tuple(sorted(self._dict.items(), key=lambda kv: kv[0]))
59
+
60
+ def __getitem__(self, key):
61
+ return self._dict[key]
62
+
63
+ def __iter__(self) -> Iterator:
64
+ return iter(self._dict)
65
+
66
+ def __len__(self) -> int:
67
+ return len(self._dict)
68
+
69
+ def __hash__(self) -> int:
70
+ return hash(self._pairs)
71
+
72
+ def __repr__(self) -> str:
73
+ return repr(self._dict)
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class RelevantModel:
78
+ """A simplified Routley–Meyer interpretation ``⟨W, N, *, R, V⟩`` for the logic B.
79
+
80
+ Plain containers are welcome — everything is copied and frozen in
81
+ ``__post_init__`` (the constructor ergonomics of
82
+ :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`),
83
+ and the frozen model is hashable.
84
+
85
+ Args:
86
+ worlds: the worlds ``W`` (an iterable of names; order kept, duplicates
87
+ collapse).
88
+ normal: the **nonempty** set ``N ⊆ W`` of normal worlds.
89
+ star: the Routley star, a mapping ``w → w*`` that must be total on ``W``
90
+ and involutive (``w** = w``). ``None`` (the default) means the
91
+ identity. Stored hashable (a frozen mapping) with dict-like lookup:
92
+ ``model.star["w0"]``.
93
+ R: the ternary relation — a set of triples ``(w, x, y)`` whose source
94
+ ``w`` must be NON-normal (``R ⊆ (W∖N) × W × W``). Consulted only when
95
+ evaluating ``→`` at a non-normal world. Defaults to empty.
96
+ valuation: maps an atom key (its rendered form, e.g. ``"P"``) to the set
97
+ of worlds where the atom holds. A missing key is false everywhere.
98
+ Dict-like lookup: ``model.valuation.get("P", frozenset())``.
99
+
100
+ Raises ValueError from ``__post_init__`` if ``normal`` is empty or not a
101
+ subset of ``worlds``, if ``star`` is not a total involution on ``worlds``,
102
+ if some ``R``-triple is sourced at a normal world or mentions an unknown
103
+ world, or if the valuation mentions an unknown world.
104
+ """
105
+
106
+ worlds: Tuple[str, ...]
107
+ normal: FrozenSet[str]
108
+ star: Optional[Mapping[str, str]] = None
109
+ R: FrozenSet[Tuple[str, str, str]] = frozenset()
110
+ valuation: Mapping[str, FrozenSet[str]] = field(default_factory=dict)
111
+
112
+ def __post_init__(self):
113
+ worlds = tuple(dict.fromkeys(self.worlds)) # dedupe, keep order
114
+ wset = frozenset(worlds)
115
+ normal = frozenset(self.normal)
116
+ star = (_FrozenMap({w: w for w in worlds}) if self.star is None
117
+ else _FrozenMap(self.star))
118
+ relation = frozenset(tuple(t) for t in self.R)
119
+ val_in = self.valuation
120
+ val_items = val_in.items() if isinstance(val_in, MappingABC) else val_in
121
+ valuation = _FrozenMap({key: frozenset(ws) for key, ws in val_items})
122
+
123
+ if not normal:
124
+ raise ValueError("RelevantModel: the set N of normal worlds must be nonempty.")
125
+ if not normal <= wset:
126
+ raise ValueError(
127
+ f"RelevantModel: normal worlds {sorted(normal - wset)} are not in worlds.")
128
+ if frozenset(star) != wset or not frozenset(star.values()) <= wset:
129
+ raise ValueError(
130
+ "RelevantModel: star must be a total map W → W; got domain "
131
+ f"{sorted(star)} with values {sorted(set(star.values()))} over "
132
+ f"worlds {sorted(wset)}.")
133
+ for w in worlds:
134
+ if star[star[w]] != w:
135
+ raise ValueError(
136
+ f"RelevantModel: star is not an involution (w** = w fails): "
137
+ f"{w!r}* = {star[w]!r} but {star[w]!r}* = {star[star[w]]!r}.")
138
+ for triple in relation:
139
+ if len(triple) != 3:
140
+ raise ValueError(f"RelevantModel: R entries must be triples; got {triple!r}.")
141
+ a, b, c = triple
142
+ if a not in wset or b not in wset or c not in wset:
143
+ raise ValueError(f"RelevantModel: R{triple!r} mentions a world not in W.")
144
+ if a in normal:
145
+ raise ValueError(
146
+ "RelevantModel: R must be sourced at NON-normal worlds only "
147
+ f"(R ⊆ (W∖N) × W × W); R{triple!r} starts at the normal world {a!r}.")
148
+ for key, ws in valuation.items():
149
+ if not ws <= wset:
150
+ raise ValueError(
151
+ f"RelevantModel: valuation for {key!r} mentions worlds "
152
+ f"{sorted(set(ws) - wset)} not in W.")
153
+
154
+ object.__setattr__(self, "worlds", worlds)
155
+ object.__setattr__(self, "normal", normal)
156
+ object.__setattr__(self, "star", star)
157
+ object.__setattr__(self, "R", relation)
158
+ object.__setattr__(self, "valuation", valuation)
159
+
160
+
161
+ def _reject_non_propositional(formula: Node) -> None:
162
+ """Raise TypeError unless ``formula`` is ∧ ∨ ¬ → ↔ over nullary atoms.
163
+
164
+ The B semantics interprets exactly the classical propositional connectives;
165
+ quantifiers, modalities, lambda terms, Xor and non-nullary atoms are rejected
166
+ (there is no agreed relevant reading for them here).
167
+ """
168
+ if not isinstance(formula, Node):
169
+ raise TypeError(
170
+ f"relevant: expected a formula Node, got {type(formula).__name__}.")
171
+ refuse_truth_constants(
172
+ [formula], "relevant",
173
+ "relevant logic has two truths and two falsities, an additive pair and a "
174
+ "multiplicative pair, which differ in B, and TPTP's $true / $false names "
175
+ "neither pair", error=TypeError)
176
+ for node in formula.walk():
177
+ if isinstance(node, Atom):
178
+ if node.args:
179
+ raise TypeError(
180
+ "relevant: only nullary propositional atoms are supported; "
181
+ f"got the atom {node.to_unicode_str()!r} with arguments.")
182
+ elif not isinstance(node, (Not, And, Or, Implies, Iff)):
183
+ raise TypeError(
184
+ f"relevant: unsupported node {type(node).__name__} in "
185
+ f"{formula.to_unicode_str()!r}; the B semantics interprets only "
186
+ "the propositional connectives ∧ ∨ ¬ → ↔ over nullary atoms "
187
+ "(no quantifiers, modalities, or lambda terms).")
188
+
189
+
190
+ def _sat(model: RelevantModel, world: str, formula: Node) -> bool:
191
+ """The simplified Routley–Meyer truth clauses (input already validated)."""
192
+ if isinstance(formula, Atom):
193
+ return world in model.valuation.get(key_text(formula), frozenset())
194
+ if isinstance(formula, And):
195
+ return _sat(model, world, formula.left) and _sat(model, world, formula.right)
196
+ if isinstance(formula, Or):
197
+ return _sat(model, world, formula.left) or _sat(model, world, formula.right)
198
+ if isinstance(formula, Not):
199
+ # w ⊨ ¬A iff w* ⊭ A (Routley star).
200
+ return not _sat(model, model.star[world], formula.formula)
201
+ if isinstance(formula, Iff):
202
+ # A ↔ B is defined as (A → B) ∧ (B → A), with the relevant →.
203
+ return (_sat(model, world, Implies(formula.left, formula.right))
204
+ and _sat(model, world, Implies(formula.right, formula.left)))
205
+ if isinstance(formula, Implies):
206
+ if world in model.normal:
207
+ # Normal world: w ⊨ A → B iff EVERY world satisfying A satisfies B.
208
+ return all(_sat(model, x, formula.right)
209
+ for x in model.worlds if _sat(model, x, formula.left))
210
+ # Non-normal world: quantify over the R-successor pairs of w.
211
+ return all(_sat(model, y, formula.right)
212
+ for (a, x, y) in model.R
213
+ if a == world and _sat(model, x, formula.left))
214
+ raise TypeError( # unreachable after _reject_non_propositional
215
+ f"relevant: unsupported node {type(formula).__name__}.")
216
+
217
+
218
+ def rel_satisfies(model: RelevantModel, world: str, formula: Node) -> bool:
219
+ """Return whether ``formula`` is true at ``world`` in ``model``.
220
+
221
+ Implements the simplified Routley–Meyer clauses: atoms via the valuation,
222
+ ``∧``/``∨`` pointwise, ``¬A`` true at ``w`` iff ``A`` is false at ``w*``,
223
+ ``A → B`` at a *normal* world iff every world satisfying ``A`` satisfies
224
+ ``B``, and at a *non-normal* world iff ``x ⊨ A ⇒ y ⊨ B`` for every triple
225
+ ``R(w, x, y)``. ``A ↔ B`` unfolds to ``(A → B) ∧ (B → A)``.
226
+
227
+ Raises TypeError for non-propositional input (quantifiers, modalities,
228
+ lambdas, non-nullary atoms) and ValueError for an unknown world.
229
+ """
230
+ _reject_non_propositional(formula)
231
+ if world not in model.worlds:
232
+ raise ValueError(
233
+ f"rel_satisfies: unknown world {world!r}; the model's worlds are "
234
+ f"{model.worlds}.")
235
+ return _sat(model, world, formula)
236
+
237
+
238
+ def _atom_keys(formula: Node) -> List[str]:
239
+ """Distinct atom keys in ``formula``, in first-seen order (the atoms are nullary here,
240
+ so a key is the name of the predicate)."""
241
+ keys: List[str] = []
242
+ seen = set()
243
+ for node in formula.walk():
244
+ if isinstance(node, Atom):
245
+ key = key_text(node)
246
+ if key not in seen:
247
+ seen.add(key)
248
+ keys.append(key)
249
+ return keys
250
+
251
+
252
+ def _powerset(items) -> Iterator[tuple]:
253
+ """Every subset of ``items`` as a tuple, smallest first."""
254
+ seq = list(items)
255
+ return chain.from_iterable(combinations(seq, r) for r in range(len(seq) + 1))
256
+
257
+
258
+ def _involutions(worlds: Tuple[str, ...]) -> Iterator[Dict[str, str]]:
259
+ """Every involutive permutation of ``worlds`` (each ``w** = w``) as a dict."""
260
+ for perm in permutations(worlds):
261
+ mapping = dict(zip(worlds, perm))
262
+ if all(mapping[mapping[w]] == w for w in worlds):
263
+ yield mapping
264
+
265
+
266
+ def rel_countermodel(formula: Node,
267
+ max_worlds: int = 2) -> Optional[Tuple[RelevantModel, str]]:
268
+ """Return ``(model, world)`` where ``formula`` fails at a normal world, or None.
269
+
270
+ EXHAUSTIVE search over every simplified Routley–Meyer interpretation with
271
+ ``|W| ≤ max_worlds`` worlds and exactly ONE normal world ``"w0"``: every
272
+ involution for ``*``, every ``R ⊆ (W∖N) × W × W``, and every valuation of the
273
+ formula's atoms. The returned countermodel is verified with
274
+ :func:`rel_satisfies` before it is returned (the search's failure check *is*
275
+ that call), so a non-None result definitively refutes validity in B.
276
+
277
+ One normal world is WLOG for countermodel *existence*: if ``φ`` fails at a
278
+ normal world ``w0`` of any interpretation, demote every other normal world
279
+ ``w`` to non-normal and add the triples ``R(w, x, x)`` for all ``x`` — the
280
+ demoted →-clause with those triples coincides with the normal-world clause,
281
+ the other clauses never mention ``N``, so every truth value is preserved and
282
+ ``w0`` (relabelled first) still refutes ``φ``. The search enumerates ALL
283
+ ``R``, so it covers that transformed interpretation.
284
+
285
+ The space is EXPONENTIAL: about ``inv(n) · 2^((n-1)·n²) · 2^(n·a)``
286
+ interpretations for ``n`` worlds and ``a`` atoms. ``max_worlds=2`` with ≤ 3
287
+ atoms is ~2·16·64 ≈ 2000 models (well under a second); ``max_worlds=3`` with
288
+ 3 atoms is already ≈ 5·10⁸ — keep the bound tiny.
289
+ """
290
+ _reject_non_propositional(formula)
291
+ atoms = _atom_keys(formula)
292
+ for n in range(1, max_worlds + 1):
293
+ worlds = tuple(f"w{i}" for i in range(n))
294
+ w0 = worlds[0]
295
+ normal = frozenset({w0})
296
+ non_normal = worlds[1:]
297
+ triples = [(w, x, y) for w in non_normal for x in worlds for y in worlds]
298
+ world_subsets = [frozenset(s) for s in _powerset(worlds)]
299
+ for star in _involutions(worlds):
300
+ for r_subset in _powerset(triples):
301
+ relation = frozenset(r_subset)
302
+ for choice in product(world_subsets, repeat=len(atoms)):
303
+ model = RelevantModel(worlds, normal, star, relation,
304
+ dict(zip(atoms, choice)))
305
+ # rel_satisfies IS the verification: a False here certifies
306
+ # that the model refutes the formula at its normal world.
307
+ if not rel_satisfies(model, w0, formula):
308
+ return model, w0
309
+ return None
310
+
311
+
312
+ def rel_valid(formula: Node, max_worlds: int = 2) -> bool:
313
+ """Return True iff no B-countermodel to ``formula`` is found within the bound.
314
+
315
+ HONEST CONTRACT (mirroring
316
+ :func:`~unicode_logic_kit.semantics.intuitionistic.int_valid`'s first-order
317
+ contract): ``False`` is *definitive* — it is backed by an explicit,
318
+ :func:`rel_satisfies`-verified countermodel from :func:`rel_countermodel`,
319
+ so the formula is certainly not a theorem of B. ``True`` means only "no
320
+ countermodel with at most ``max_worlds`` worlds": B is decidable, but this
321
+ search is *bounded*, so a non-theorem whose smallest refuting interpretation
322
+ needs more worlds than the bound is (spuriously) reported valid — for a
323
+ certified positive verdict use
324
+ :func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_relevant`, whose
325
+ proof battery is a real validity proof over EVERY interpretation satisfying
326
+ B's frame conditions (N nonempty, the Routley star a total involution, R
327
+ sourced only at non-normal worlds) — not just those with at most
328
+ ``max_worlds`` worlds. Raising ``max_worlds`` never turns a ``False`` into a
329
+ ``True``; it is exponentially more expensive (see :func:`rel_countermodel`).
330
+ """
331
+ return rel_countermodel(formula, max_worlds) is None