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,497 @@
1
+ """A composable registry of the kit's logic-to-logic translations.
2
+
3
+ The kit has long owned a family of translation functions — the modal standard
4
+ translation, ALC→FOL / ALC→modal-K, dependence→ESO — each living in its home
5
+ module as a bare function. This module catalogues them as first-class
6
+ :class:`Comorphism` edges in a graph keyed by logic labels, with BFS path
7
+ composition: ``translate(term, "alc", "fol")`` works whether the registry has
8
+ a direct edge or has to compose ``alc→modal→fol``. (The name and the idea —
9
+ logic translations as first-class objects — are HETS'; the machinery here is
10
+ deliberately lightweight and native Python.)
11
+
12
+ Logic labels used by the default registry: ``"fol"`` (classical FOL),
13
+ ``"msfol"`` (many-sorted FOL, whose sorts are non-empty), ``"modal"`` (the
14
+ propositional modal / temporal / deontic / hybrid family), ``"qml"``
15
+ (quantified modal logic with a domain regime), ``"alc"`` (description-logic
16
+ concepts), ``"drs"`` (discourse representation structures), ``"team"``
17
+ (dependence / team-semantic sentences), ``"eso"`` (existential second-order
18
+ formulas) and ``"fuzzy"`` (Łukasiewicz many-valued logic, whose single edge is
19
+ lossy). :mod:`unicode_logic_kit.logic` is the typed surface over these labels:
20
+ ``FOL(MODAL(f))`` converts and keeps the side axioms with the term.
21
+ Third-party edges register with
22
+ :func:`register_comorphism` and become reachable from
23
+ :func:`unicode_logic_kit.api.translate` immediately.
24
+
25
+ A comorphism's ``apply`` maps a SOURCE-logic term to a TARGET-logic term; the
26
+ term type is whatever the source logic's AST is (``Node`` for the formula
27
+ families, ``Concept`` for ALC). Fragment restrictions of the underlying
28
+ functions (e.g. ``concept_to_modal`` accepts only single-role concepts)
29
+ surface as their own exceptions, unchanged.
30
+
31
+ STABILITY POLICY (:data:`DEFAULT_REGISTRY`): within a minor release line
32
+ (0.N.x) no edge — identified by its ``name`` — is removed, renamed, or has
33
+ its ``source``/``target``/``lossy`` changed; only new edges are added.
34
+ This mirrors :mod:`unicode_logic_kit.api`'s own STABILITY POLICY (see that
35
+ module's docstring) applied to the registry surface rather than the
36
+ result dataclasses, and it is what lets a caller compose
37
+ ``translate(term, a, b)`` across a minor line without re-checking that the
38
+ path it found last time still exists. ``tests/test_mcp_stability.py``
39
+ pins ``(name, source, target, lossy)`` for the current edges as a
40
+ baseline-subset check.
41
+ """
42
+
43
+ from collections import deque
44
+ from dataclasses import dataclass
45
+ from inspect import Parameter, signature
46
+ from typing import Any, Callable, Dict, FrozenSet, List, Optional, Tuple
47
+
48
+ __all__ = [
49
+ "Comorphism", "TranslationResult", "ComorphismRegistry",
50
+ "DEFAULT_REGISTRY", "GUARANTEES", "register_comorphism", "weakest_guarantee",
51
+ ]
52
+
53
+ # What a translation preserves, strongest first. An edge DECLARES one; a path
54
+ # gets the weakest on it (:func:`weakest_guarantee`).
55
+ #
56
+ # EVERY level is a statement about the PAIR (image, ``axioms``), never about the
57
+ # bare image: a translation with side conditions is faithful to its source only
58
+ # in the models that satisfy them. ``to_fol`` is the plain case — the unsorted
59
+ # image of ``(∀x:Human M(x)) → ∃x:Human M(x)`` has a model the sorted formula
60
+ # has none of (``Human`` empty), and the non-emptiness axiom is exactly what
61
+ # rules it out. So "with the axioms as separate premises" is the premise of all
62
+ # four readings below, and what distinguishes them is WHICH questions transfer:
63
+ #
64
+ # "faithful" the model classes correspond in both directions on the
65
+ # edge's own fragment (pointwise, for an edge with a free
66
+ # anchor), so every question transfers.
67
+ # "validity" a validity / entailment question transfers, but the model
68
+ # classes do not correspond (e.g. the image is a closed
69
+ # sentence that only ENCODES validity of the source).
70
+ # "satisfiability" only satisfiability transfers; a validity answer does NOT
71
+ # (e.g. a free anchor that has to be closed existentially).
72
+ # "lossy" none of the above; ``note`` must say what is dropped.
73
+ #
74
+ # ``None`` means the edge has not declared one — then a path's guarantee is
75
+ # None as well. It is NOT a synonym for "faithful": an undeclared edge makes no
76
+ # promise, and a caller that needs one must look at the edge.
77
+ #: The guarantee vocabulary, strongest first (see the comment above).
78
+ GUARANTEES: Tuple[str, ...] = ("faithful", "validity", "satisfiability", "lossy")
79
+
80
+
81
+ def weakest_guarantee(values) -> Optional[str]:
82
+ """The weakest guarantee among ``values`` (``None`` if any is undeclared).
83
+
84
+ The order is :data:`GUARANTEES`. This is what a composed path can promise:
85
+ a faithful edge after a satisfiability-only one does not repair the latter.
86
+ """
87
+ values = list(values)
88
+ if not values or any(v is None for v in values):
89
+ return None
90
+ return max(values, key=GUARANTEES.index)
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class Comorphism:
95
+ """One directed translation edge between two logic labels.
96
+
97
+ ``lossy`` marks edges that do not preserve the full source semantics; it
98
+ must agree with ``guarantee == "lossy"`` (checked in ``__post_init__``).
99
+ The one lossy default edge is ``to_msfol``, the two-valued projection of
100
+ Łukasiewicz logic. ``note`` documents conventions a consumer must know
101
+ (free anchors, fragment limits), and ``axioms`` / ``options`` are the
102
+ side-condition producer and the option names this edge reads — see
103
+ :meth:`side_axioms`.
104
+ """
105
+
106
+ name: str
107
+ source: str
108
+ target: str
109
+ apply: Callable[[Any], Any]
110
+ lossy: bool = False
111
+ note: str = ""
112
+ guarantee: Optional[str] = None
113
+ axioms: Optional[Callable[..., Tuple[Any, ...]]] = None
114
+ options: FrozenSet[str] = frozenset()
115
+
116
+ def __post_init__(self):
117
+ if self.guarantee is not None and self.guarantee not in GUARANTEES:
118
+ raise ValueError(
119
+ f"Comorphism {self.name!r}: guarantee {self.guarantee!r} is not "
120
+ f"one of {GUARANTEES}")
121
+ # The flag and the declaration must agree, or a consumer that reads only
122
+ # one of them gets a different answer than one reading the other.
123
+ if self.lossy and self.guarantee not in (None, "lossy"):
124
+ raise ValueError(
125
+ f"Comorphism {self.name!r}: lossy=True but guarantee="
126
+ f"{self.guarantee!r} — a lossy edge preserves nothing")
127
+ if self.guarantee == "lossy" and not self.lossy:
128
+ raise ValueError(
129
+ f"Comorphism {self.name!r}: guarantee='lossy' but lossy=False")
130
+ object.__setattr__(self, "options", frozenset(self.options))
131
+
132
+ def _for(self, fn: Callable, options: Dict[str, Any]) -> Dict[str, Any]:
133
+ """The subset of ``options`` that ``fn`` itself accepts.
134
+
135
+ An edge declares its option NAMES once in :attr:`options`; which of the
136
+ two callables reads them is a detail of the edge (a frame system, say,
137
+ changes the side axioms but not the translation itself). The subset is
138
+ read off the signature — ``**kwargs`` takes everything — so neither
139
+ callable is ever handed a keyword it cannot name.
140
+ """
141
+ try:
142
+ params = signature(fn).parameters
143
+ except (TypeError, ValueError): # builtins without signatures
144
+ return {}
145
+ if any(p.kind is Parameter.VAR_KEYWORD for p in params.values()):
146
+ return dict(options)
147
+ return {k: v for k, v in options.items() if k in params}
148
+
149
+ def side_axioms(self, term: Any, **options) -> Tuple[Any, ...]:
150
+ """The axioms a caller must add for this edge, computed from ``term``.
151
+
152
+ Empty when the edge declares none. The axioms are TARGET-logic terms and
153
+ are never folded into the translated term: folding them in breaks every
154
+ validity question through the edge (0.28.0 shipped exactly that bug for
155
+ subsort axioms — ``api.prove(to_fol(f, signature=sig))`` then had to
156
+ prove the axiom itself and valid formulas came back REFUTED).
157
+ """
158
+ if self.axioms is None:
159
+ return ()
160
+ return tuple(self.axioms(term, **self._for(self.axioms, options)))
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class TranslationResult:
165
+ """Outcome of a (possibly composed) translation.
166
+
167
+ ``path`` names the comorphisms applied, in order; ``lossy`` is True iff
168
+ any edge on the path is. ``note`` concatenates the edge notes so the
169
+ conventions travel with the result.
170
+
171
+ ``axioms`` are the side conditions of the whole path, already expressed in
172
+ the TARGET logic (an upstream edge's axioms are translated by the remaining
173
+ edges, because they are terms of the intermediate logic). They are SEPARATE
174
+ PREMISES, never conjoined onto ``result``::
175
+
176
+ t = api.translate(f, "msfol", "fol")
177
+ api.prove(t.result, [*premises, *t.axioms])
178
+
179
+ ``guarantee`` is the weakest one on the path (see :data:`GUARANTEES`), or
180
+ ``None`` when some edge on it declares none.
181
+ """
182
+
183
+ result: Any
184
+ source: str
185
+ target: str
186
+ path: Tuple[str, ...]
187
+ lossy: bool
188
+ note: str = ""
189
+ axioms: Tuple[Any, ...] = ()
190
+ guarantee: Optional[str] = None
191
+
192
+ def to_dict(self) -> dict:
193
+ """JSON-compatible dict; the result serialises via its own ``to_dict``
194
+ when it has one, else via ``repr``."""
195
+ if hasattr(self.result, "to_dict"):
196
+ rendered = self.result.to_dict()
197
+ else:
198
+ rendered = repr(self.result)
199
+ return {
200
+ "result": rendered,
201
+ "source": self.source,
202
+ "target": self.target,
203
+ "path": list(self.path),
204
+ "lossy": self.lossy,
205
+ "note": self.note,
206
+ "axioms": [a.to_dict() if hasattr(a, "to_dict") else repr(a)
207
+ for a in self.axioms],
208
+ "guarantee": self.guarantee,
209
+ }
210
+
211
+
212
+ class ComorphismRegistry:
213
+ """A graph of :class:`Comorphism` edges with BFS path composition."""
214
+
215
+ def __init__(self):
216
+ self._edges: Dict[Tuple[str, str], Comorphism] = {}
217
+
218
+ def register(self, comorphism: Comorphism, replace: bool = False) -> None:
219
+ """Add an edge; refuses to silently overwrite an existing one.
220
+
221
+ One edge per (source, target) pair keeps ``translate`` deterministic —
222
+ pass ``replace=True`` to intentionally swap an edge out.
223
+ """
224
+ key = (comorphism.source, comorphism.target)
225
+ if key in self._edges and not replace:
226
+ raise ValueError(
227
+ f"register: an edge {key[0]}→{key[1]} is already registered "
228
+ f"({self._edges[key].name!r}); pass replace=True to swap it.")
229
+ self._edges[key] = comorphism
230
+
231
+ def unregister(self, name: str) -> bool:
232
+ """Remove the edge registered under ``name``; True iff one existed.
233
+
234
+ The inverse of :meth:`register`, added for DYNAMIC edge providers
235
+ (the Hets bridge re-binds its ``hets:<Name>`` edges on every
236
+ re-registration and must be able to drop edges the new server no
237
+ longer offers — leaving them would keep stale closures pointing at
238
+ a dead URL). Removing an unknown name is a no-op returning False,
239
+ not an error: refresh code calls this speculatively.
240
+ """
241
+ for key, edge in list(self._edges.items()):
242
+ if edge.name == name:
243
+ del self._edges[key]
244
+ return True
245
+ return False
246
+
247
+ def edges(self) -> Tuple[Comorphism, ...]:
248
+ """All registered edges, sorted by (source, target) for determinism."""
249
+ return tuple(self._edges[k] for k in sorted(self._edges))
250
+
251
+ def find_path(self, source: str, target: str) -> List[Comorphism]:
252
+ """Shortest edge sequence from ``source`` to ``target`` (BFS).
253
+
254
+ Deterministic: neighbours are explored in sorted label order, so equal
255
+ length paths always resolve the same way. Raises ``ValueError`` when no
256
+ path exists — with the known labels in the message, because a typo in a
257
+ logic label should fail loudly.
258
+ """
259
+ if source == target:
260
+ return []
261
+ labels = sorted({l for k in self._edges for l in k})
262
+ if source not in labels or target not in labels:
263
+ raise ValueError(
264
+ f"find_path: unknown logic label in {source!r}→{target!r} "
265
+ f"(known: {labels})")
266
+ # BFS over labels; predecessors remember the edge that reached a label.
267
+ seen = {source}
268
+ queue = deque([source])
269
+ via: Dict[str, Comorphism] = {}
270
+ while queue:
271
+ here = queue.popleft()
272
+ for (s, t), edge in sorted(self._edges.items()):
273
+ if s != here or t in seen:
274
+ continue
275
+ via[t] = edge
276
+ if t == target:
277
+ path = [edge]
278
+ while path[0].source != source:
279
+ path.insert(0, via[path[0].source])
280
+ return path
281
+ seen.add(t)
282
+ queue.append(t)
283
+ raise ValueError(
284
+ f"find_path: no comorphism path {source!r}→{target!r} "
285
+ f"(edges: {[f'{c.source}→{c.target}' for c in self.edges()]})")
286
+
287
+ def carry(self, term: Any, source: str, target: str, **options) -> Any:
288
+ """Translate ``term`` along the path WITHOUT collecting side axioms.
289
+
290
+ For a term that IS a side axiom: it has to be expressed in the target
291
+ logic like any other term, but the edges' own axioms were already
292
+ collected once by the :meth:`translate` call that produced it, and
293
+ collecting them again per axiom would multiply them.
294
+ """
295
+ path = self.find_path(source, target)
296
+ accepted = {name for edge in path for name in edge.options}
297
+ unknown = sorted(set(options) - accepted)
298
+ if unknown:
299
+ raise ValueError(
300
+ f"carry: option(s) {unknown} are accepted by no edge on "
301
+ f"{source!r}→{target!r} (path {[e.name for e in path]}, "
302
+ f"accepted: {sorted(accepted)})")
303
+ for edge in path:
304
+ mine = {k: v for k, v in options.items() if k in edge.options}
305
+ term = edge.apply(term, **edge._for(edge.apply, mine))
306
+ return term
307
+
308
+ def translate(self, term: Any, source: str, target: str,
309
+ **options) -> TranslationResult:
310
+ """Apply the (composed) translation ``source``→``target`` to ``term``.
311
+
312
+ ``options`` are forwarded to the edges on the path that DECLARE them in
313
+ :attr:`Comorphism.options` (e.g. a frame system for a modal edge). An
314
+ option no edge on the path declares raises ``ValueError`` naming the
315
+ path and what it does accept — a silently ignored option would answer a
316
+ different question than the caller asked.
317
+
318
+ Side axioms accumulate along the path: each edge contributes its own
319
+ (computed from the term it is handed), and the axioms collected so far
320
+ are carried through the edge as well, since they are terms of its source
321
+ logic. An edge that cannot carry an axiom raises; the path is then not
322
+ usable for that term, rather than quietly dropping the condition.
323
+ """
324
+ path = self.find_path(source, target)
325
+ accepted = {name for edge in path for name in edge.options}
326
+ unknown = sorted(set(options) - accepted)
327
+ if unknown:
328
+ raise ValueError(
329
+ f"translate: option(s) {unknown} are accepted by no edge on "
330
+ f"{source!r}→{target!r} (path "
331
+ f"{[e.name for e in path]}, accepted: {sorted(accepted)})")
332
+ result = term
333
+ axioms: Tuple[Any, ...] = ()
334
+ for edge in path:
335
+ mine = {k: v for k, v in options.items() if k in edge.options}
336
+ fresh = edge.side_axioms(result, **mine)
337
+ for_apply = edge._for(edge.apply, mine)
338
+ try:
339
+ carried = tuple(edge.apply(a, **for_apply) for a in axioms)
340
+ except Exception as exc: # pragma: no cover - guard
341
+ raise ValueError(
342
+ f"translate: edge {edge.name!r} cannot carry a side axiom "
343
+ f"of an earlier edge on {source!r}→{target!r}; this path "
344
+ f"cannot answer the question for this term") from exc
345
+ result = edge.apply(result, **for_apply)
346
+ axioms = carried + fresh
347
+ notes = "; ".join(e.note for e in path if e.note)
348
+ return TranslationResult(
349
+ result=result, source=source, target=target,
350
+ path=tuple(e.name for e in path),
351
+ lossy=any(e.lossy for e in path), note=notes,
352
+ axioms=axioms,
353
+ # The empty path (source == target) is the identity translation.
354
+ guarantee=("faithful" if not path
355
+ else weakest_guarantee([e.guarantee for e in path])))
356
+
357
+
358
+ def _build_default_registry() -> ComorphismRegistry:
359
+ """The default edges: exactly the kit's existing translation functions.
360
+
361
+ Note what is deliberately NOT an edge: ``circumscription_entails_so`` is an
362
+ entailment decision rather than a term-to-term translation; the Isabelle /
363
+ THF / TPTP / CASL writers are serialisers into text, not into another
364
+ logic's AST, so a path never runs through them; and ``eliminate_lambdas``
365
+ and the ChemLog renamings stay inside one logic. ``qml_translate`` has its
366
+ own ``qml → fol`` edge (the propositional standard translation keeps
367
+ ``modal → fol``), because the two differ in more than the source language:
368
+ the quantified one sorts the domain into worlds and objects and needs the
369
+ sort discipline among its side axioms.
370
+ """
371
+ from .fol.modal_translation import frame_axioms, standard_translation
372
+ from .fol.qml import qml_axioms, qml_translate
373
+ from .fol._msfl_nodes import sort_axioms, subsort_axioms, to_fol
374
+ from .dl.translate import concept_to_fol, concept_to_modal, _reject_data_concept
375
+ from .drt.export import drs_to_fol
376
+ from .drt.reverse import fol_to_drs
377
+ from .semantics.team_translation import dependence_to_eso
378
+
379
+ def alc_to_fol(concept):
380
+ # Faithful with no side axioms is true of a concept WITHOUT a data
381
+ # restriction; with one the sorts and the datatype lattice are axioms
382
+ # of a knowledge base (dl.kb_to_fol(..., query=[concept])), so the edge
383
+ # refuses it by name instead of answering for a weaker theory.
384
+ _reject_data_concept(concept, "alc→fol (dl.concept_to_fol)")
385
+ return concept_to_fol(concept)
386
+
387
+ registry = ComorphismRegistry()
388
+ registry.register(Comorphism(
389
+ name="standard_translation", source="modal", target="fol",
390
+ apply=standard_translation,
391
+ guarantee="faithful",
392
+ # frame_axioms returns a list; an edge's axioms producer returns a
393
+ # tuple, because TranslationResult.axioms accumulates along a path.
394
+ axioms=lambda term, **options: tuple(frame_axioms(term, **options)),
395
+ options=frozenset({"frame", "systems", "temporal_closure"}),
396
+ note="anchored at the FREE world variable 'w' — universally close it "
397
+ "(∀w …) for validity questions, existentially for satisfiability; "
398
+ "the frame conditions of every relation the image mentions are in "
399
+ ".axioms (pass frame=/systems= to choose the systems), and so is "
400
+ "the membership of every sorted constant c:S of the formula in S "
401
+ "at every world, ∀v0 S(c, v0) — a constant is a rigid designator, "
402
+ "and without that axiom the image has a countermodel in which c "
403
+ "is no S. Take them as SEPARATE premises, never conjoined onto "
404
+ "the image",
405
+ ))
406
+ registry.register(Comorphism(
407
+ name="qml_translate", source="qml", target="fol",
408
+ apply=qml_translate,
409
+ guarantee="faithful",
410
+ # qml_axioms takes the formula as a KEYWORD and gates its output on the
411
+ # relations and sorts occurring in it, so the adapter passes it there.
412
+ axioms=lambda term, **options: tuple(qml_axioms(formula=term, **options)),
413
+ options=frozenset({"mode", "frame", "systems", "bridges",
414
+ "temporal_closure"}),
415
+ note="quantified modal logic: sorted into World/Object with the "
416
+ "existence predicate E; anchored at the free world variable 'w'. "
417
+ ".axioms carries the sort discipline, the domain regime of mode= "
418
+ "and the frame conditions — the image answers nothing without "
419
+ "them (fol.qml.qml_validity_formula bundles all of it into one "
420
+ "closed sentence instead)",
421
+ ))
422
+ registry.register(Comorphism(
423
+ name="to_fol", source="msfol", target="fol",
424
+ apply=to_fol,
425
+ guarantee="faithful",
426
+ # Sort non-emptiness and the membership of every sorted constant in its
427
+ # sort are read off the sentence (to_fol drops both: ``c:S`` becomes the
428
+ # plain ``c``); a SUBSORT hierarchy is not in the term at all, so it has
429
+ # to come from the signature.
430
+ axioms=lambda term, signature=None: (
431
+ tuple(sort_axioms(term))
432
+ + (tuple(subsort_axioms(signature)) if signature is not None else ())),
433
+ options=frozenset({"signature"}),
434
+ note="many-sorted → unsorted: sorts become unary guards and a sorted "
435
+ "constant c:S becomes the plain c. .axioms has one non-emptiness "
436
+ "sentence per sort (the kit's MSFOL convention), one membership "
437
+ "atom S(c) per sorted constant (without it the image forgets that "
438
+ "c is in S) and, with signature=, one implication per declared "
439
+ "subsort edge; all of them are SEPARATE premises — conjoining "
440
+ "them onto the image makes a prover prove the axiom and valid "
441
+ "formulas come back REFUTED. Take .axioms of the premises AND of "
442
+ "the conclusion",
443
+ ))
444
+ registry.register(Comorphism(
445
+ name="to_msfol", source="fuzzy", target="msfol",
446
+ apply=lambda term: term.to_msfol(),
447
+ lossy=True, guarantee="lossy",
448
+ note="Łukasiewicz → classical is a TWO-VALUED PROJECTION: weak and "
449
+ "strong conjunction get the same image, so it is sound only on "
450
+ "crisp {0,1} valuations and is not a validity-preserving route "
451
+ "for fuzzy logic (use semantics.fuzzy / the fuzzy Kripke route)",
452
+ ))
453
+ registry.register(Comorphism(
454
+ name="drs_to_fol", source="drs", target="fol",
455
+ apply=drs_to_fol,
456
+ guarantee="faithful",
457
+ note="a DRS is a closed box: the image is a sentence, no anchor",
458
+ ))
459
+ registry.register(Comorphism(
460
+ name="fol_to_drs", source="fol", target="drs",
461
+ apply=fol_to_drs,
462
+ guarantee="faithful",
463
+ note="the inverse of drs_to_fol ON ITS IMAGE; a formula outside that "
464
+ "image is refused by name (FolToDrsError), never approximated",
465
+ ))
466
+ registry.register(Comorphism(
467
+ name="concept_to_modal", source="alc", target="modal",
468
+ apply=concept_to_modal,
469
+ guarantee="faithful",
470
+ note="single-role concepts only (ALC with one role = modal K); "
471
+ "multi-role concepts raise",
472
+ ))
473
+ registry.register(Comorphism(
474
+ name="concept_to_fol", source="alc", target="fol",
475
+ apply=alc_to_fol,
476
+ guarantee="faithful",
477
+ note="one free individual variable 'x' — close existentially for "
478
+ "satisfiability questions; read against the EMPTY knowledge base. "
479
+ "A concept with a data restriction is refused by name "
480
+ "(UnsupportedDatatypeError): its sort and datatype axioms belong "
481
+ "to a knowledge base — ask dl.kb_to_fol(..., query=[concept])",
482
+ ))
483
+ registry.register(Comorphism(
484
+ name="dependence_to_eso", source="team", target="eso",
485
+ apply=dependence_to_eso,
486
+ guarantee="faithful",
487
+ note="sentences only (no free variables)",
488
+ ))
489
+ return registry
490
+
491
+
492
+ DEFAULT_REGISTRY = _build_default_registry()
493
+
494
+
495
+ def register_comorphism(comorphism: Comorphism, replace: bool = False) -> None:
496
+ """Register an edge in the DEFAULT registry (see :class:`ComorphismRegistry`)."""
497
+ DEFAULT_REGISTRY.register(comorphism, replace=replace)