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,185 @@
1
+ """A molecule-structure cache that survives a whole campaign.
2
+
3
+ Why this is worth a module. Checking K class definitions against N molecules
4
+ rebuilds the same :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`
5
+ K times unless something remembers it — and the structure does not depend on
6
+ the formula at all. Measured on this kit (Python 3.11, rdkit 2026.03.5):
7
+ building a structure costs 0.045 ms (methane) to 1.04 ms (ATP, 31 heavy
8
+ atoms), rising linearly in the domain size, while EVALUATING a definition
9
+ against it costs 0.005–1.13 ms depending almost entirely on the FORMULA. So
10
+ the ratio build/evaluate runs from about 0.6 (a hard three-variable pattern on
11
+ a large molecule) to 58 (a definition that short-circuits immediately), and
12
+ caching turns ``K·(build + eval)`` into ``build + K·eval`` — a speed-up of
13
+ ``1 + build/eval``, i.e. **1.6× to 59×**, around 3–8× for a realistic mix.
14
+ Worth doing, and worth measuring rather than assuming, which is why
15
+ :attr:`StructureCache.hits`/:attr:`~StructureCache.misses` are part of the
16
+ type rather than something a caller has to bolt on.
17
+
18
+ **The key is the full option tuple, not the SMILES.**
19
+ :func:`~unicode_logic_kit.chem.mol_to_structure` builds a genuinely different
20
+ structure for the same molecule depending on three parameters, and a key that
21
+ omits any of them lets one call's structure silently answer another call's
22
+ question:
23
+
24
+ * ``naming`` — ``"chemlog"`` spells the carbon predicate ``c`` and the single
25
+ bond ``bSINGLE``; ``"paper"`` spells them ``c`` and ``singleBond``. A
26
+ formula checked against the wrong one fails with
27
+ :class:`~unicode_logic_kit.semantics.model_eval.UninterpretedSymbol` on every
28
+ molecule — a different failure than "does not hold", and a confusing one.
29
+ * ``aromatic`` — ``False`` Kekulizes the bond typing, so ``bAROMATIC`` is
30
+ empty where ``True`` fills it. Benzene answers differently.
31
+ * ``computed`` — ``False`` omits ``in_ring``, ``in_ring_of_size_N``,
32
+ ``aromatic``, ``same_fragment`` and ``carbon_connected`` entirely, so a
33
+ definition mentioning any of them raises instead of deciding.
34
+
35
+ The SMILES is stored **raw, never canonicalised**: individual names are a
36
+ readout of the input string's atom order (``"CCO"`` names the methyl carbon
37
+ ``c1``, ``"OCC"`` names the methylene carbon ``c1``), so canonicalising would
38
+ merge two structures whose individuals mean different atoms and mislabel every
39
+ witness. Two spellings of one molecule therefore cost two entries — the
40
+ correct trade.
41
+
42
+ What is deliberately NOT in the key: ``all_different``, the evaluation budget,
43
+ and the formula. Those decide the VERDICT, not the structure; a verdict cache
44
+ is a separate concern and this module does not pretend to be one.
45
+
46
+ Failures are cached too. A SMILES RDKit refuses will be refused identically
47
+ next time, so :class:`StructureBuildError` is stored in place of the structure
48
+ and returned on the next lookup instead of paying for the same failure again.
49
+ """
50
+
51
+ from collections import OrderedDict
52
+ from typing import Any, Dict, Iterator, Optional, Tuple
53
+
54
+ __all__ = ["StructureBuildError", "StructureCache", "CacheKey"]
55
+
56
+ #: ``(smiles, naming, aromatic, computed)`` — see the module docstring for why
57
+ #: each field is load-bearing.
58
+ CacheKey = Tuple[str, str, bool, bool]
59
+
60
+
61
+ class StructureBuildError:
62
+ """Cached in place of a structure when
63
+ :func:`~unicode_logic_kit.chem.mol_to_structure` refused a SMILES.
64
+
65
+ Deliberately NOT an exception: it is a stored VALUE, and raising it on
66
+ lookup would make a cache hit and a cache miss behave differently at the
67
+ call site. Callers test with ``isinstance`` and read :attr:`message`.
68
+ """
69
+
70
+ __slots__ = ("message",)
71
+
72
+ def __init__(self, message: str):
73
+ self.message = message
74
+
75
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
76
+ return f"StructureBuildError({self.message!r})"
77
+
78
+ def __eq__(self, other: object) -> bool:
79
+ return (isinstance(other, StructureBuildError)
80
+ and other.message == self.message)
81
+
82
+ def __hash__(self) -> int:
83
+ return hash((StructureBuildError, self.message))
84
+
85
+
86
+ class StructureCache:
87
+ """Build-or-fetch cache for molecule structures, with a bounded size.
88
+
89
+ Implements enough of the ``MutableMapping`` protocol
90
+ (``in``/``[]``/``[]=``/``del``/``len``/iteration) to be passed straight to
91
+ :func:`unicode_logic_kit.eval.datasets.c3po.score_definition`'s
92
+ ``structure_cache`` parameter, which is typed against that protocol and
93
+ was previously handed a plain ``dict``.
94
+
95
+ Eviction is least-recently-used and bounded by ``max_entries``, because a
96
+ campaign over hundreds of thousands of molecules would otherwise hold
97
+ every structure it ever built. LRU rather than "clear when full": the
98
+ campaign loop revisits the SAME molecules across definitions, so recency
99
+ is exactly the right predictor here.
100
+
101
+ Args:
102
+ max_entries: hard upper bound on stored structures. ``None`` disables
103
+ eviction (use only when the molecule set is known to be small).
104
+ """
105
+
106
+ def __init__(self, *, max_entries: Optional[int] = 100_000):
107
+ if max_entries is not None and max_entries < 1:
108
+ raise ValueError("StructureCache: max_entries must be >= 1 or None")
109
+ self.max_entries = max_entries
110
+ self._entries: "OrderedDict[CacheKey, Any]" = OrderedDict()
111
+ self.hits = 0
112
+ self.misses = 0
113
+ self.evictions = 0
114
+
115
+ # -- mapping protocol ---------------------------------------------------
116
+
117
+ def __contains__(self, key: object) -> bool:
118
+ return key in self._entries
119
+
120
+ def __getitem__(self, key: CacheKey) -> Any:
121
+ value = self._entries[key]
122
+ self._entries.move_to_end(key)
123
+ return value
124
+
125
+ def __setitem__(self, key: CacheKey, value: Any) -> None:
126
+ self._entries[key] = value
127
+ self._entries.move_to_end(key)
128
+ while self.max_entries is not None and len(self._entries) > self.max_entries:
129
+ self._entries.popitem(last=False)
130
+ self.evictions += 1
131
+
132
+ def __delitem__(self, key: CacheKey) -> None:
133
+ del self._entries[key]
134
+
135
+ def __len__(self) -> int:
136
+ return len(self._entries)
137
+
138
+ def __iter__(self) -> Iterator[CacheKey]:
139
+ return iter(self._entries)
140
+
141
+ # -- build-or-fetch -----------------------------------------------------
142
+
143
+ def structure_for(self, smiles: str, *, naming: str = "chemlog",
144
+ aromatic: bool = True, computed: bool = True) -> Any:
145
+ """The structure for ``smiles`` under these options, built once.
146
+
147
+ Returns either a
148
+ :class:`~unicode_logic_kit.semantics.structures.FiniteStructure` or a
149
+ :class:`StructureBuildError` — the latter for a SMILES
150
+ :func:`~unicode_logic_kit.chem.mol_to_structure` refused, which is
151
+ per-molecule DATA and must not stop a campaign.
152
+
153
+ :class:`ImportError` (RDKit missing) and :class:`TypeError` (a caller
154
+ passing something that is not a string) propagate untouched: those are
155
+ environment and caller bugs, and turning them into 200 000 identical
156
+ cached "errors" would bury the one thing worth fixing.
157
+ """
158
+ from .mol import mol_to_structure
159
+
160
+ key: CacheKey = (smiles, naming, aromatic, computed)
161
+ if key in self._entries:
162
+ self.hits += 1
163
+ return self[key]
164
+ self.misses += 1
165
+ try:
166
+ value: Any = mol_to_structure(smiles, naming=naming,
167
+ aromatic=aromatic, computed=computed)
168
+ except ValueError as exc:
169
+ value = StructureBuildError(f"{type(exc).__name__}: {exc}")
170
+ self[key] = value
171
+ return value
172
+
173
+ # -- reporting ----------------------------------------------------------
174
+
175
+ @property
176
+ def hit_rate(self) -> Optional[float]:
177
+ """Hits over lookups, or ``None`` before the first lookup — never a
178
+ fabricated 0.0 for a cache nobody has asked anything yet."""
179
+ total = self.hits + self.misses
180
+ return self.hits / total if total else None
181
+
182
+ def stats(self) -> Dict[str, Any]:
183
+ return {"entries": len(self._entries), "hits": self.hits,
184
+ "misses": self.misses, "evictions": self.evictions,
185
+ "hit_rate": self.hit_rate, "max_entries": self.max_entries}
@@ -0,0 +1,244 @@
1
+ """ChemLog TPTP ↔ kit AST — the name bridge between the two halves.
2
+
3
+ ChemLog's axioms and the FOL definitions LLMs generate for chemical classes
4
+ are written in TPTP, where a predicate is LOWERCASE (``c(Ac)``, ``bDOUBLE``)
5
+ and a variable is UPPERCASE. The kit's own convention is the exact inverse,
6
+ so :func:`unicode_logic_kit.fol.tptp_input.parse_tptp_formula` capitalises
7
+ every predicate on import: ``c/1`` arrives as ``C/1``, ``bSINGLE/2`` as
8
+ ``BSINGLE/2``.
9
+
10
+ That is correct and injective, but it means a formula imported from TPTP
11
+ does NOT line up with the structure :func:`unicode_logic_kit.chem.mol_to_structure`
12
+ builds, which carries ChemLog's own lowercase spelling — evaluation then
13
+ fails loudly with an uninterpreted symbol rather than silently, which is
14
+ right, but useless. This module closes the gap explicitly: it renames the
15
+ chemical vocabulary back to its ChemLog spelling after import, and forward
16
+ again before export.
17
+
18
+ **The renaming is checked for injectivity, not assumed.** Two distinct
19
+ ChemLog names that capitalise to the same kit name would silently merge two
20
+ predicates into one — the classic non-injective-lowercasing soundness bug
21
+ this kit has been bitten by before (see ``atp/tptp_ncl``'s collision check).
22
+ :data:`KIT_TO_CHEMLOG` is therefore built once at import time and the
23
+ construction raises if the mapping is not a bijection on the known
24
+ vocabulary.
25
+
26
+ Symbols OUTSIDE the chemical vocabulary — the class predicates themselves
27
+ (``amideBond``, ``carboxylicAcid``) and any auxiliary predicate a generator
28
+ invents — are deliberately left in the kit's capitalised form. They have no
29
+ ChemLog spelling to restore, the capitalisation is injective on them too,
30
+ and inventing a round-trip for them would be guessing at a convention the
31
+ source never stated.
32
+ """
33
+
34
+ from typing import Dict, Tuple
35
+
36
+ from ..fol.nodes import Node
37
+ from ..fol.spans import SpanMap, project_spans
38
+ from ..fol.tptp_input import parse_tptp_formula
39
+ from .signature import CHEMLOG_SIGNATURE
40
+
41
+ __all__ = [
42
+ "CHEMLOG_TO_KIT", "KIT_TO_CHEMLOG",
43
+ "parse_chemlog_tptp", "to_kit_names", "to_chemlog_names",
44
+ "rename_with_spans", "to_chemlog_names_with_spans",
45
+ ]
46
+
47
+
48
+ def _kit_spelling(name: str) -> str:
49
+ """How :func:`parse_tptp_formula` will render the TPTP name ``name``.
50
+
51
+ The importer upper-cases the first character and leaves the rest alone;
52
+ this mirrors that rule rather than importing the parser's private helper,
53
+ so a change there surfaces as a failing round-trip test instead of a
54
+ silent divergence.
55
+ """
56
+ return name[:1].upper() + name[1:] if name else name
57
+
58
+
59
+ def _build_maps() -> Tuple[Dict[str, str], Dict[str, str]]:
60
+ """The chemical vocabulary's forward and inverse name maps.
61
+
62
+ What the injectivity check below protects against, precisely: it checks
63
+ injectivity under :func:`_kit_spelling` — capitalise the FIRST character
64
+ only, leave the rest exactly as-is — because that is the ONE
65
+ transformation this module's own round trip ever applies: the TPTP
66
+ importer (:func:`~unicode_logic_kit.fol.tptp_input.parse_tptp_formula`)
67
+ capitalises a parsed predicate's first character on the way IN, and
68
+ :data:`KIT_TO_CHEMLOG`/:func:`to_chemlog_names` invert exactly that on
69
+ the way back out. Two ChemLog names that differ only in the case of their
70
+ FIRST character (``atom`` and ``Atom`` both become ``Atom``; there is no such
71
+ pair among the 40 predicates of the current vocabulary, which has no
72
+ function or constant symbols, but nothing stops a future addition from
73
+ introducing one) would collide under this fold and are refused here
74
+ rather than silently merged. Names that differ in the case of a later
75
+ character stay two kit names (``isA`` and ``isa``).
76
+
77
+ This check does NOT protect against a DIFFERENT, unrelated collision:
78
+ a renderer that folds a kit-capitalised name back to TPTP text by
79
+ lowercasing the WHOLE string rather than just its first character (see
80
+ :func:`to_kit_names`'s own docstring) can make two DISTINCT
81
+ kit-spelled names collapse to the SAME TPTP text on export — that is a
82
+ problem for whatever renders TPTP text to catch (a case-preserving
83
+ renderer, or a dedicated export-time collision guard), not something
84
+ this module's own import-direction check can see, since this module
85
+ only ever renames an already-parsed AST and never itself serialises one
86
+ to TPTP text.
87
+
88
+ Raises:
89
+ ValueError: two ChemLog names share a kit spelling — the mapping
90
+ would not be invertible and evaluation could silently conflate
91
+ two predicates.
92
+ """
93
+ forward: Dict[str, str] = {}
94
+ inverse: Dict[str, str] = {}
95
+ names = (list(CHEMLOG_SIGNATURE.predicates)
96
+ + list(CHEMLOG_SIGNATURE.functions)
97
+ + sorted(CHEMLOG_SIGNATURE.constants))
98
+ for name in names:
99
+ kit = _kit_spelling(name)
100
+ if kit in inverse and inverse[kit] != name:
101
+ raise ValueError(
102
+ "chem.interop: the chemical vocabulary is not injective under "
103
+ f"the TPTP importer's capitalisation — {name!r} and "
104
+ f"{inverse[kit]!r} both become {kit!r}; a formula using them "
105
+ "could not be mapped back unambiguously")
106
+ forward[name] = kit
107
+ inverse[kit] = name
108
+ return forward, inverse
109
+
110
+
111
+ _CHEMLOG_TO_KIT, _KIT_TO_CHEMLOG = _build_maps()
112
+
113
+ # Bound one at a time rather than by tuple unpacking: a `#:` comment cannot
114
+ # attach to an unpacking target, so both names would reach the API reference
115
+ # undocumented — and autodoc would fall back to describing `dict` itself.
116
+
117
+ #: ChemLog spelling -> the spelling the kit's TPTP importer produces
118
+ #: (``"bSINGLE"`` -> ``"BSINGLE"``, ``"c"`` -> ``"C"``): the importer
119
+ #: capitalises every predicate, and only these symbols are renamed back.
120
+ CHEMLOG_TO_KIT: Dict[str, str] = _CHEMLOG_TO_KIT
121
+
122
+ #: The inverse of :data:`CHEMLOG_TO_KIT`.
123
+ KIT_TO_CHEMLOG: Dict[str, str] = _KIT_TO_CHEMLOG
124
+
125
+
126
+ def _rename(node: Node, mapping: Dict[str, str]) -> Node:
127
+ """Rename predicate/function symbols throughout ``node`` by ``mapping``.
128
+
129
+ Symbols absent from ``mapping`` are left untouched — that is what keeps
130
+ class and auxiliary predicates alone (see the module docstring).
131
+ """
132
+ from ..fol.nodes import Atom, Function
133
+ from dataclasses import replace as _replace
134
+
135
+ def walk(current: Node) -> Node:
136
+ if isinstance(current, Atom):
137
+ renamed = mapping.get(current.predicate, current.predicate)
138
+ return _replace(current,
139
+ predicate=renamed,
140
+ args=tuple(walk(a) for a in current.args))
141
+ if isinstance(current, Function):
142
+ renamed = mapping.get(current.name, current.name)
143
+ return _replace(current, name=renamed,
144
+ args=tuple(walk(a) for a in current.args))
145
+ return current.map_children(walk)
146
+
147
+ return walk(node)
148
+
149
+
150
+ def rename_with_spans(node: Node, mapping: Dict[str, str],
151
+ spans: SpanMap) -> Tuple[Node, SpanMap]:
152
+ """:func:`_rename`, plus propagation of ``spans`` across the rewrite.
153
+
154
+ The rename walk (see :func:`_rename`) only ever changes an ``Atom``'s
155
+ ``predicate`` string or a ``Function``'s ``name`` string — never the
156
+ number, order, or TYPE of a node's children — so it is a
157
+ shape-preserving rewrite: :func:`unicode_logic_kit.fol.spans.project_spans`
158
+ carries ``spans`` (PATH-keyed — see that module's docstring for why a
159
+ path, not a node identity, is what stays meaningful across a rewrite that
160
+ reconstructs every node on the path to a renamed leaf) onto
161
+ ``renamed_node`` unchanged, since every path in ``spans`` still denotes
162
+ the exact same structural position in ``renamed_node`` that it did in
163
+ ``node``.
164
+
165
+ Returns:
166
+ ``(renamed_node, new_spans)`` — ``renamed_node`` is identical to
167
+ ``_rename(node, mapping)``; ``new_spans`` is a
168
+ :class:`~unicode_logic_kit.fol.spans.SpanMap` with the same path data,
169
+ already bound (via :meth:`~unicode_logic_kit.fol.spans.SpanMap.rebind`)
170
+ to ``renamed_node`` so ``new_spans.for_node(...)`` works against it.
171
+ """
172
+ renamed = _rename(node, mapping)
173
+ return renamed, project_spans(node, renamed, spans)
174
+
175
+
176
+ def to_chemlog_names(formula: Node) -> Node:
177
+ """Rename a kit-side formula's chemical symbols to ChemLog spelling.
178
+
179
+ Apply to anything that came through the TPTP importer before evaluating
180
+ it against a :func:`~unicode_logic_kit.chem.mol_to_structure` structure.
181
+ """
182
+ return _rename(formula, KIT_TO_CHEMLOG)
183
+
184
+
185
+ def to_chemlog_names_with_spans(formula: Node, spans: SpanMap) -> Tuple[Node, SpanMap]:
186
+ """:func:`to_chemlog_names`, plus propagation of ``spans`` — see
187
+ :func:`rename_with_spans`."""
188
+ return rename_with_spans(formula, KIT_TO_CHEMLOG, spans)
189
+
190
+
191
+ def to_kit_names(formula: Node) -> Node:
192
+ """The inverse of :func:`to_chemlog_names` — ChemLog spelling to kit spelling.
193
+
194
+ Apply before handing a structure-side formula to a route that expects the
195
+ kit's own capitalised naming convention. The unicode renderer
196
+ (:meth:`~unicode_logic_kit.fol.nodes.Node.to_unicode_str`) is always a safe
197
+ destination — it renders the name on the node as it is: a predicate or
198
+ function by its own name, and a constant by its own name too, in single quotes
199
+ when the bare word would not read back as that constant (``'1,2-diacyl'``).
200
+
201
+ A route that re-renders the result to TPTP TEXT (e.g. for a prover
202
+ backend) is a DIFFERENT case and is safe only if that renderer restores
203
+ a mixed-case ChemLog name exactly, by folding just the first character
204
+ back to lower case the way :func:`_kit_spelling` folded it up — not by
205
+ lowercasing the name's ENTIRE string, which corrupts a ChemLog name that
206
+ has further uppercase letters after the first one (``bSINGLE`` would
207
+ come back as ``bsingle``, a different, uninterpreted symbol). Whether a
208
+ given TPTP renderer does this correctly is that renderer's own
209
+ contract, not something this function can guarantee on its caller's
210
+ behalf — check it explicitly before trusting a
211
+ ``to_kit_names`` → (render to TPTP) round trip for chemical vocabulary,
212
+ the same way :func:`unicode_logic_kit.mcp.chem_tools.simplify_definition`
213
+ does by using a case-preserving renderer rather than assuming any
214
+ TPTP-rendering route is automatically safe.
215
+ """
216
+ return _rename(formula, CHEMLOG_TO_KIT)
217
+
218
+
219
+ def parse_chemlog_tptp(text: str, *, repair: bool = True) -> Node:
220
+ """Parse ChemLog-style TPTP into an AST that matches molecule structures.
221
+
222
+ Parses ``text`` as a bare TPTP formula and renames the chemical
223
+ vocabulary back to its ChemLog spelling, so the result can be evaluated
224
+ directly against :func:`unicode_logic_kit.chem.mol_to_structure` output.
225
+
226
+ ``repair=True`` (the default) first runs
227
+ :func:`unicode_logic_kit.fol.repair_tptp_formula`, which absorbs the
228
+ documented syntax failure modes of LLM-generated chemical FOL —
229
+ unbracketed biconditionals and predicate names that need quoting — so a
230
+ formula that a stricter parser would have rejected outright still
231
+ arrives. Free variables are REPORTED by that layer, never silently
232
+ bound: a definition written as ``className(X) <=> …`` is a semantic
233
+ error only its author can resolve.
234
+
235
+ Raises:
236
+ ValueError: the text does not parse even after repair.
237
+ """
238
+ if repair:
239
+ from ..fol.tptp_repair import repair_tptp_formula
240
+
241
+ result = repair_tptp_formula(text)
242
+ if result.ok and result.formula is not None:
243
+ return to_chemlog_names(result.formula)
244
+ return to_chemlog_names(parse_tptp_formula(text))