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,208 @@
1
+ """Satisfiability / validity checks and model (counterexample) extraction via Z3.
2
+
3
+ **Many-sorted (MSFOL) soundness.** ``Node.to_z3()`` relativises a sorted
4
+ quantifier/constant/count to plain classical FOL (``fol.nodes.to_fol``,
5
+ auto-run by ``SortedQuantifier.to_z3`` etc.) but that relativisation alone
6
+ does NOT guarantee a sort's universe is non-empty — and MSFOL, by
7
+ convention, never gives a sort an empty one (see the classical-reasoning
8
+ guide's many-sorted section). Left unaddressed, Z3 is free to interpret a
9
+ sort predicate as always-false, which can turn a genuinely valid many-sorted
10
+ formula invalid (or a genuinely unsatisfiable one satisfiable) via a
11
+ spurious empty-sort "model" no legal MSFOL structure would ever be. The
12
+ relativisation likewise reads a sorted constant ``c:S`` as the plain constant
13
+ ``c``, so it forgets that ``c`` lies in ``S``: ``∀x:Human Mortal(x)`` would
14
+ not entail ``Mortal(socrates:Human)``, because ``socrates`` could be outside
15
+ ``Human``. Every function below closes both gaps the same way: it collects
16
+ ``unicode_logic_kit.fol._msfl_nodes.sort_axioms(formula)`` — one ``∃x S(x)`` per
17
+ sort and one ``S(c)`` per sorted constant — and adds them as their own extra,
18
+ UNCONDITIONAL, never-negated assertions on the solver — never folded inside
19
+ ``to_z3()`` itself, which is polarity-blind and shared with every OTHER caller
20
+ (see that function's docstring for why). For an unsorted formula the axiom
21
+ list is empty, so the Z3 query — and every one of these functions' return
22
+ values — is unchanged by it.
23
+ """
24
+
25
+ from collections import Counter
26
+ from typing import Callable, Dict, List, Optional, Sequence, Tuple
27
+
28
+ from ..fol._fol_nodes import kit_name_of_z3_symbol, z3_variable_name
29
+ from ..fol._msfl_nodes import sort_axioms
30
+ from ..fol.nodes import Node, Z3Env
31
+ from z3 import Solver, sat, unsat, Not
32
+
33
+
34
+ def declaration_keys(entries: Sequence[Tuple[str, int, str]]) -> List[str]:
35
+ """One key per declaration of a model, ``(name, arity, range)`` each.
36
+
37
+ A name that only one declaration carries is its own key, so a model of
38
+ ``P(a)`` still reads ``{"P": ..., "a": ...}``. When several declarations
39
+ share a name (``P(a)`` next to ``P(a, b)``, a function ``f`` next to a
40
+ predicate ``f``, a proposition ``a`` next to a constant ``a``) each is keyed
41
+ by its name and arity, ``"P/1"`` and ``"P/2"``, and when a name and an arity
42
+ still do not tell two apart (a predicate and a function of one name and
43
+ arity) by the sort the declaration returns as well, ``"P/1:Bool"`` and
44
+ ``"P/1:S"``. The keys are pairwise distinct; a key that would meet another
45
+ symbol's own name gets a ``#2``, ``#3``, ... suffix.
46
+
47
+ Shared by every route that reads a Z3 model back into a dict and by the
48
+ cvc5 route, so a countermodel names its symbols the same way whichever
49
+ solver found it.
50
+ """
51
+ by_name = Counter(name for name, _, _ in entries)
52
+ by_arity = Counter((name, arity) for name, arity, _ in entries)
53
+ keys: List[Optional[str]] = [name if by_name[name] == 1 else None for name, _, _ in entries]
54
+ taken = {key for key in keys if key is not None} # a symbol's own name comes first
55
+ for i, (name, arity, range_text) in enumerate(entries):
56
+ if keys[i] is not None:
57
+ continue
58
+ key = (f"{name}/{arity}" if by_arity[(name, arity)] == 1
59
+ else f"{name}/{arity}:{range_text}")
60
+ candidate, n = key, 1
61
+ while candidate in taken:
62
+ n += 1
63
+ candidate = f"{key}#{n}"
64
+ taken.add(candidate)
65
+ keys[i] = candidate
66
+ return keys # type: ignore[return-value]
67
+
68
+
69
+ def separate_variables(entries: Sequence[Tuple[str, int, str]],
70
+ is_variable: Sequence[bool]) -> List[Tuple[str, int, str]]:
71
+ """Give each variable among the ``(name, arity, range)`` entries the name it is reported under.
72
+
73
+ A free variable ``x`` is a symbol of the model next to the constants. It is reported
74
+ under its own name, ``x``, unless a constant of that name is declared too: then the
75
+ constant keeps ``x`` and the variable is ``x!v``, so that the two are told apart and the
76
+ constant is always found under its plain name. If ``x!v`` is itself the name of another
77
+ symbol (a constant spelled ``x!v``, another variable), the variable is ``x!v!v``, and so on:
78
+ a variable never takes the name of a symbol that is not it, so that no constant loses
79
+ its plain name to a variable. A constant is a symbol of no arguments whose range is not
80
+ ``Bool`` (a proposition is a predicate): the sort ``S``, or a numeric sort.
81
+ """
82
+ names = {name for name, _, _ in entries}
83
+ constants = {name for (name, arity, range_text), variable in zip(entries, is_variable)
84
+ if not variable and arity == 0 and range_text != "Bool"}
85
+ reported = []
86
+ for (name, arity, range_text), variable in zip(entries, is_variable):
87
+ if variable and name in constants:
88
+ name = z3_variable_name(name)
89
+ while name in names:
90
+ name = z3_variable_name(name)
91
+ names.add(name)
92
+ reported.append((name, arity, range_text))
93
+ return reported
94
+
95
+
96
+ def _kit_entry(decl, ranges: Sequence[str] = ("S",)) -> Tuple[Tuple[str, int, str], bool]:
97
+ """The ``(name, arity, range)`` entry of a declaration of a model, with the kit's name, and
98
+ whether the declaration is a variable. Only a constant of one of the sorts ``ranges`` (the
99
+ uninterpreted sort ``S``, or the numeric sort of the arithmetic route) is written with the
100
+ marks of :func:`~unicode_logic_kit.fol._fol_nodes.kit_name_of_z3_symbol`; every other name is
101
+ the declaration's own."""
102
+ name, arity, range_text = str(decl.name()), decl.arity(), str(decl.range())
103
+ if arity == 0 and range_text in ranges:
104
+ name, variable = kit_name_of_z3_symbol(name)
105
+ return (name, arity, range_text), variable
106
+ return (name, arity, range_text), False
107
+
108
+
109
+ def model_assignment(model, skip: Optional[Callable] = None,
110
+ ranges: Sequence[str] = ("S",)) -> Dict[str, str]:
111
+ """Read a satisfying ``z3.ModelRef`` into ``{key: interpretation}``.
112
+
113
+ Keys are the kit's names of the symbols, told apart by arity (and result sort) only
114
+ where one name is declared more than once — see :func:`declaration_keys`. A constant
115
+ is under its plain name; a free variable (a symbol of its own, see
116
+ :class:`~unicode_logic_kit.fol.nodes.Z3Env`) is under its name too, and as ``x!v`` when a
117
+ constant ``x`` is declared as well (:func:`separate_variables`).
118
+ ``skip(decl)`` leaves out a declaration (the tracking booleans of an
119
+ ``assert_and_track`` call, say) before the keys are chosen, so a skipped
120
+ symbol never makes another one's key longer. ``ranges`` are the sorts of the constants
121
+ and variables of the model: ``("S",)``, or the numeric sort of the arithmetic route.
122
+ """
123
+ decls = [d for d in model.decls() if skip is None or not skip(d)]
124
+ entries = [_kit_entry(d, ranges) for d in decls]
125
+ keys = declaration_keys(separate_variables([e for e, _ in entries], [v for _, v in entries]))
126
+ return {key: str(model[d]) for key, d in zip(keys, decls)}
127
+
128
+
129
+ def _add_sort_axioms(solver: Solver, *formulas: Node, env: Optional[Z3Env] = None) -> None:
130
+ """Assert ``sort_axioms(*formulas)`` on ``solver``, unconditionally.
131
+
132
+ Shared by every function in this module so they can never drift apart on
133
+ how the axioms are added (see the module docstring). A no-op — literally
134
+ zero calls to ``solver.add`` — when none of ``formulas`` is many-sorted.
135
+ ``env`` is the environment the formulas themselves were translated with.
136
+ """
137
+ for axiom in sort_axioms(*formulas):
138
+ solver.add(axiom.to_z3(env) if env is not None else axiom.to_z3())
139
+
140
+
141
+ def is_satisfiable(formula: Node, timeout: int = 10000) -> bool:
142
+ """Return True if the formula has a model (Z3 reports sat).
143
+
144
+ A Z3 ``unknown`` result (e.g. on hard quantified formulas hitting the
145
+ timeout) is treated as not-known-satisfiable and returns False. A
146
+ many-sorted ``formula``'s sorts are asserted non-empty, and each of its
147
+ sorted constants is asserted to lie in its sort, alongside it — see the
148
+ module docstring.
149
+ """
150
+ solver = Solver()
151
+ solver.set("timeout", timeout)
152
+ solver.set("random_seed", 42)
153
+ env = Z3Env()
154
+ solver.add(formula.to_z3(env))
155
+ _add_sort_axioms(solver, formula, env=env)
156
+ return solver.check() == sat
157
+
158
+
159
+ def is_valid(formula: Node, timeout: int = 10000) -> bool:
160
+ """Return True if the formula is valid, i.e. its negation is unsatisfiable.
161
+
162
+ A Z3 ``unknown`` result (e.g. on hard quantified formulas hitting the
163
+ timeout) is treated as not-known-valid and returns False. A many-sorted
164
+ ``formula``'s sorts are asserted non-empty and its sorted constants are
165
+ asserted to lie in their sorts, as extra, UNNEGATED premises alongside the
166
+ negated goal — see the module docstring; this is what makes ``is_valid``
167
+ agree with ``semantics.modelfinder`` on many-sorted input instead of
168
+ exploiting an empty-sort or constant-outside-its-sort loophole modelfinder
169
+ never considers a legal structure. (Asserted outside the negation: were
170
+ ``S(c)`` negated with the goal it would be one more thing to prove.)
171
+ """
172
+ solver = Solver()
173
+ solver.set("timeout", timeout)
174
+ solver.set("random_seed", 42)
175
+ env = Z3Env()
176
+ goal = Not(formula.to_z3(env))
177
+ _add_sort_axioms(solver, formula, env=env)
178
+ solver.add(goal)
179
+ return solver.check() == unsat
180
+
181
+
182
+ def get_model(formula: Node, timeout: int = 10000):
183
+ """Return a satisfying assignment as a dict, or None if unsat/unknown.
184
+
185
+ The dict maps each Z3 declaration name (constants, uninterpreted
186
+ functions/predicates) to the string form of its interpretation. A name that
187
+ is declared more than once in the formula (``P(a)`` next to ``P(a, b)``, a
188
+ function and a predicate of one name) is two symbols, and each is reported
189
+ under its own key, ``"P/1"`` and ``"P/2"`` (see :func:`declaration_keys`);
190
+ a name declared once keeps its plain name. For an
191
+ invalid equivalence or entailment, ``get_model(Not(...))`` yields the
192
+ concrete counterexample. Returns None when the formula is unsatisfiable or
193
+ Z3 cannot decide it within the timeout. Every many-sorted node's sort
194
+ anywhere in ``formula`` — including under a ``Not(...)`` a caller wraps
195
+ it in — is asserted non-empty, and every sorted constant anywhere in it is
196
+ asserted to lie in its sort, alongside it, so a returned model is always
197
+ a legal MSFOL structure (see the module docstring); the returned dict may
198
+ therefore also carry an interpretation for a sort's own predicate.
199
+ """
200
+ solver = Solver()
201
+ solver.set("timeout", timeout)
202
+ solver.set("random_seed", 42)
203
+ env = Z3Env()
204
+ solver.add(formula.to_z3(env))
205
+ _add_sort_axioms(solver, formula, env=env)
206
+ if solver.check() != sat:
207
+ return None
208
+ return model_assignment(solver.model())
@@ -0,0 +1,88 @@
1
+ """Molecules as finite FOL structures — the model-checking side.
2
+
3
+ A chemical class definition written in FOL classifies a molecule by MODEL
4
+ CHECKING that formula against the molecule, represented as a finite FOL
5
+ structure (atoms = individuals). The formula side of such a pipeline is
6
+ what usually gets documented in detail; the molecule-to-structure side
7
+ tends to exist only as a single worked prose example (ethanol) — no code.
8
+ This subpackage is that missing translation, in the vocabulary of ChemLog
9
+ (Flügel et al. 2025, MIT licence,
10
+ https://github.com/sfluegel05/chemlog-peptides):
11
+
12
+ >>> from unicode_logic_kit.chem import mol_to_structure, CHEMLOG_SIGNATURE
13
+ >>> ethanol = mol_to_structure("CCO") # SMILES -> FiniteStructure
14
+ >>> ethanol.individuals_with("c") # the two carbons
15
+ ('c1', 'c2')
16
+ >>> from unicode_logic_kit import api
17
+ >>> api.check("∀x (c(x) → atom(x))", signature=CHEMLOG_SIGNATURE).ok
18
+ True
19
+
20
+ Scope
21
+ -----
22
+ * :mod:`.mol` — :func:`mol_to_structure`: SMILES or an ``rdkit.Chem.Mol`` in,
23
+ a :class:`~unicode_logic_kit.semantics.structures.FiniteStructure` out. RDKit
24
+ is an OPTIONAL dependency (``pip install rdkit``); importing this
25
+ subpackage never requires it, only calling :func:`mol_to_structure` does.
26
+ * :mod:`.signature` — :data:`CHEMLOG_SIGNATURE`, the same vocabulary as a
27
+ first-class kit :class:`~unicode_logic_kit.fol.signature.Signature`
28
+ (for ``api.check(formula, signature=CHEMLOG_SIGNATURE)``), plus the
29
+ :data:`CHEMLOG_TO_PAPER` / :data:`PAPER_TO_CHEMLOG` alias tables between
30
+ ChemLog's own TPTP-file spelling and the prose spelling of the identical
31
+ vocabulary.
32
+
33
+ Why this exists (the bottlenecks this pipeline hits)
34
+ ----------------------------------------------------------
35
+ Three problems recur in a pipeline of this shape, and this subpackage
36
+ speaks to each directly: model-checking timeouts are the major bottleneck
37
+ (addressed by
38
+ :class:`~unicode_logic_kit.semantics.structures.FiniteStructure`'s
39
+ candidate-set indexing, which this module's structures inherit for free);
40
+ over-general formulas, whose precision cost stays invisible until it is
41
+ measured against a corpus (addressed elsewhere, by tighter translation, not
42
+ by this subpackage); and failures on pure syntax, which dominate the rest
43
+ (addressed by :data:`CHEMLOG_SIGNATURE` catching an unknown predicate or
44
+ wrong arity before a definition ever reaches model checking).
45
+
46
+ What this subpackage does NOT do
47
+ --------------------------------------
48
+ It does not translate natural-language class definitions to FOL (that is
49
+ the rest of the kit — parsing, ``api.check``, the prover backends), and it
50
+ does not itself run model checking (that is
51
+ :mod:`unicode_logic_kit.semantics.model_eval` / the Tarskian evaluator over
52
+ the :class:`FiniteStructure` this subpackage builds). It is exactly the one
53
+ missing piece: molecule in, structure out, in a vocabulary the rest of the
54
+ kit already knows how to check formulas against.
55
+ """
56
+
57
+ from .mol import mol_to_structure
58
+ from .cache import StructureCache, StructureBuildError
59
+ from .signature import (
60
+ CHEMLOG_SIGNATURE, CHEMLOG_TO_PAPER, PAPER_TO_CHEMLOG,
61
+ to_paper_naming, to_chemlog_naming,
62
+ )
63
+
64
+ from ..fol.spans import SpanMap
65
+ from .interop import (
66
+ parse_chemlog_tptp, to_kit_names, to_chemlog_names,
67
+ CHEMLOG_TO_KIT, KIT_TO_CHEMLOG,
68
+ rename_with_spans, to_chemlog_names_with_spans,
69
+ )
70
+
71
+ __all__ = [
72
+ "mol_to_structure",
73
+ "StructureCache", "StructureBuildError",
74
+ "CHEMLOG_SIGNATURE", "CHEMLOG_TO_PAPER", "PAPER_TO_CHEMLOG",
75
+ "to_paper_naming", "to_chemlog_naming",
76
+ # TPTP interop: the kit's importer inverts TPTP's case convention, so a
77
+ # formula read from ChemLog TPTP needs its chemical vocabulary renamed
78
+ # back before it lines up with a molecule structure.
79
+ "parse_chemlog_tptp", "to_kit_names", "to_chemlog_names",
80
+ "CHEMLOG_TO_KIT", "KIT_TO_CHEMLOG",
81
+ # Span propagation (opt-in; see unicode_logic_kit.mcp.chem_tools's module
82
+ # docstring for the consumer): rename_with_spans/to_chemlog_names_with_spans
83
+ # carry a caller-supplied SpanMap across the rename this module always
84
+ # performs, since that rename reconstructs every node (see
85
+ # interop.rename_with_spans's own docstring) and would otherwise sever
86
+ # the link between a parsed AST node and the source span it came from.
87
+ "SpanMap", "rename_with_spans", "to_chemlog_names_with_spans",
88
+ ]
@@ -0,0 +1,284 @@
1
+ """Shared naming-scheme machinery for :mod:`unicode_logic_kit.chem` — PRIVATE.
2
+
3
+ The ChemLog vocabulary this subpackage targets (Flügel et al. 2025, MIT
4
+ licence, https://github.com/sfluegel05/chemlog-peptides) is spelled two
5
+ different ways by the two kinds of source that describe it:
6
+
7
+ * **running prose** (as in a worked ethanol example) writes ``has3Hs`` /
8
+ ``has2Hs`` / ``has1H`` (note the irregular singular at exactly one) and
9
+ ``singleBond``;
10
+ * the actual **ChemLog TPTP source files** write ``has_1_hs`` / ``has_2_hs``
11
+ / ``has_3_hs`` and ``bSINGLE``.
12
+
13
+ :mod:`unicode_logic_kit.chem.mol` has to pick ONE of these as what it actually
14
+ emits (silently supporting "a bit of both" would be exactly the kind of
15
+ unprincipled approximation this kit refuses), and
16
+ :mod:`unicode_logic_kit.chem.signature` has to publish a mapping between the
17
+ two so a caller who needs the other spelling is not stuck. Both modules
18
+ import this one so the naming rules are declared in exactly ONE place: were
19
+ ``signature``'s alias table hand-written separately from what ``mol``
20
+ actually produces, the two would be free to drift out of sync with no test
21
+ able to catch it short of re-deriving both by hand. This module has NO
22
+ RDKit dependency (it deals in strings only) — it must stay importable
23
+ without RDKit installed, because :mod:`unicode_logic_kit.chem.signature`
24
+ (which needs it) explicitly must be.
25
+
26
+ **The default scheme is ``"chemlog"``** (the TPTP-file spelling), because
27
+ that is the vocabulary :mod:`unicode_logic_kit.chem.signature`'s
28
+ ``CHEMLOG_SIGNATURE`` is built to validate against and because it is
29
+ internally uniform (one snake_case convention throughout) where the prose
30
+ spelling is not (see ``_hs_name_paper`` below). ``"paper"`` exists
31
+ specifically so a caller can reproduce the literal prose spelling — see
32
+ ``mol.py``'s ethanol acceptance test.
33
+
34
+ Naming decisions spelled out (so a reviewer does not have to reverse-engineer
35
+ them from the code):
36
+
37
+ * **Hydrogen count.** ChemLog: ``has_{n}_hs`` uniformly. ``"paper"``:
38
+ ``has{n}H`` at ``n == 1``, ``has{n}Hs`` otherwise — this is a
39
+ GENERALISATION of the literal ``has3Hs``/``has2Hs``/``has1H`` (the only
40
+ three values ethanol exhibits) to arbitrary ``n``; the prose spelling
41
+ never shows ``n == 0`` or ``n > 3``, so extending the same
42
+ singular-at-one rule to those is the only assumption made here, not a
43
+ literal quote.
44
+ * **Formal charge.** IDENTICAL under both schemes: ``charge0`` at zero,
45
+ ``charge_p{n}``/``charge_m{n}`` (``n = abs(charge)``) otherwise. The
46
+ prose spelling only ever demonstrates ``charge0``; the ChemLog spec gives
47
+ ``charge0, charge_m1, charge_p1 (bzw. chargeN)`` for the ChemLog side. The
48
+ parenthetical generic form is read here as "continue the sign-letter
49
+ pattern established by charge_m1/charge_p1 to any n" rather than as a
50
+ THIRD, inconsistent bare ``chargeN`` spelling that would collide with the
51
+ sign-letter convention already fixed at n=1 — the latter reading would
52
+ leave the sign undetermined for exactly the cases (n>1) it is supposed to
53
+ cover.
54
+ * **Bond types.** ChemLog: ``bSINGLE``/``bDOUBLE``/``bTRIPLE``/``bAROMATIC``
55
+ (all four are literal ChemLog spec text). ``"paper"``: only
56
+ ``singleBond`` is literally attested; ``doubleBond``/``tripleBond``/
57
+ ``aromaticBond`` are this module's generalisation of that one example by
58
+ analogy.
59
+ * **Net-charge globals.** ``NetChargePositive``/``NetChargeNegative`` are
60
+ spelled IDENTICALLY in the ChemLog spec text and in prose (both
61
+ CamelCase) — no alias needed. Only the neutral flag differs:
62
+ ``net_charge_neutral`` (ChemLog, snake_case — matching the spec text
63
+ verbatim, inconsistent as that is with the sibling predicates being
64
+ CamelCase) vs. ``NetChargeNeutral`` (the ``"paper"`` scheme, CamelCase,
65
+ matching its siblings and the literal prose spelling).
66
+ * **Element letters, ``atom``, ``ChiralR``/``ChiralS``, the ``bond``
67
+ super-relation.** Spelled identically by both sources — no alias table
68
+ entry is needed for these beyond the identity pairing (included below for
69
+ completeness, so a caller can look ANY known predicate up in one table
70
+ without having to special-case "well, THIS one doesn't need translating").
71
+ * **``has_bond_to``.** ChemLog-only: the spec text lists it as a second name
72
+ for the same bond-existence super-relation ``bond`` alongside ``bond``
73
+ itself (their exact relationship undocumented beyond "binaere: ... bond
74
+ (Oberrelation), has_bond_to" in the spec text) — both are populated as
75
+ IDENTICAL extensions under the ``"chemlog"`` scheme. The prose spelling
76
+ never mentions a second name at all, so the ``"paper"`` scheme simply
77
+ omits it; it has no alias-table counterpart (deliberately absent, not an
78
+ oversight — see :mod:`unicode_logic_kit.chem.signature`).
79
+
80
+ The computed predicates (:mod:`unicode_logic_kit.chem.mol`'s ``in_ring`` /
81
+ ``in_ring_of_size_N`` / ``aromatic`` / ``same_fragment`` / ``carbon_
82
+ connected``) are this module's OWN addition beyond either source's literal
83
+ vocabulary, so they are scheme-INVARIANT (same spelling always) and are not
84
+ part of the alias table below — an identity mapping would carry no
85
+ information a lookup failure doesn't already convey by raising.
86
+ """
87
+
88
+ from dataclasses import dataclass
89
+ from typing import Callable, Dict, Optional, Tuple
90
+
91
+ __all__: Tuple[str, ...] = () # private module — nothing meant for reuse outside chem/
92
+
93
+ # -----------------------------------------------------------------------
94
+ # Fixed, scheme-invariant vocabulary
95
+ # -----------------------------------------------------------------------
96
+
97
+ #: RDKit element symbol -> ChemLog atom-type letter (identical in both
98
+ #: schemes, and in the literal worked ethanol example: M(c), M(o)).
99
+ #:
100
+ #: The first six are ChemLog's own: its published vocabulary is a peptide
101
+ #: one, and peptides are built from C/N/O/S/P/H. The halogens below are this
102
+ #: module's extension, on the same footing as the computed predicates
103
+ #: described at the end of the module docstring -- an addition beyond either
104
+ #: source's literal vocabulary, spelled by the same rule (lower-cased
105
+ #: element symbol) and therefore scheme-invariant.
106
+ #:
107
+ #: They are here because the vocabulary has to cover the molecules it is
108
+ #: pointed at, not just the ones its source paper was about. ChEBI classes
109
+ #: such as ``organohalogenCompound`` are *defined* by a halogen; without
110
+ #: these letters ``mol_to_structure`` refuses the molecule outright, and a
111
+ #: refusal is not a chemical statement -- every such class would be
112
+ #: unanswerable rather than answered. Astatine is included for closure of
113
+ #: the group even though it is vanishingly rare in ChEBI: leaving out one
114
+ #: member would make the vocabulary's boundary an accident of frequency.
115
+ ELEMENT_LETTERS: Dict[str, str] = {
116
+ "C": "c", "N": "n", "O": "o", "S": "s", "P": "p", "H": "h",
117
+ "F": "f", "Cl": "cl", "Br": "br", "I": "i", "At": "at",
118
+ }
119
+
120
+ ATOM_NAME = "atom"
121
+ CHIRAL_R_NAME = "ChiralR"
122
+ CHIRAL_S_NAME = "ChiralS"
123
+ BOND_SUPER_NAME = "bond"
124
+ NET_CHARGE_POSITIVE_NAME = "NetChargePositive"
125
+ NET_CHARGE_NEGATIVE_NAME = "NetChargeNegative"
126
+
127
+ #: The four RDKit bond-order "kinds" this vocabulary covers, by a
128
+ #: scheme-independent short key (NOT an RDKit ``BondType`` — this module has
129
+ #: no RDKit dependency; ``mol.py`` maps ``rdkit.Chem.BondType`` values onto
130
+ #: these keys itself).
131
+ BOND_KINDS: Tuple[str, ...] = ("SINGLE", "DOUBLE", "TRIPLE", "AROMATIC")
132
+
133
+ #: The hydrogen-count / formal-charge values every structure ``mol.py``
134
+ #: builds interprets EXPLICITLY (even as an empty extension) regardless of
135
+ #: whether the particular molecule exhibits them — see ``mol.py``'s module
136
+ #: docstring, "always-populated predicates", for why: a formula asking
137
+ #: ``s(x)`` of a sulfur-free molecule must get "false everywhere", not a
138
+ #: ``KeyError`` for an uninterpreted symbol that the ChemLog vocabulary in
139
+ #: fact declares. These two ranges match the ChemLog spec text's OWN
140
+ #: examples verbatim (``has_0_hs``..``has_3_hs``; ``charge0``, ``charge_m1``,
141
+ #: ``charge_p1``) rather than some wider guess — a molecule that genuinely
142
+ #: needs e.g. ``has_4_hs`` (methane's carbon) or ``charge_m2`` still gets
143
+ #: that predicate populated (``mol.py``'s atom loop adds keys on demand), it
144
+ #: is just not PRE-populated as an empty default the way 0..3 / -1/0/1 are.
145
+ CANONICAL_HS_RANGE: Tuple[int, ...] = (0, 1, 2, 3)
146
+ CANONICAL_CHARGES: Tuple[int, ...] = (0, -1, 1)
147
+
148
+ #: Concrete ring sizes ``mol.py`` exposes as their own unary predicate
149
+ #: (``in_ring_of_size_3`` .. ``in_ring_of_size_8``), alongside the fully
150
+ #: general ``in_ring`` (any size). Range chosen to cover the ring sizes that
151
+ #: actually recur in organic/bio-organic chemistry — cyclopropane through
152
+ #: cyclooctane, which already includes every common aromatic and
153
+ #: heteroaromatic ring (5- and 6-membered). A macrocycle bigger than 8 is
154
+ #: still correctly reported by ``in_ring`` (True) but has no
155
+ #: ``in_ring_of_size_N`` member of its own — a real but narrow limitation,
156
+ #: not a silent one: nothing claims a size-12 ring is a size-8 ring.
157
+ RING_SIZE_FAMILY: Tuple[int, ...] = (3, 4, 5, 6, 7, 8)
158
+
159
+
160
+ def _hs_name_chemlog(n: int) -> str:
161
+ return f"has_{n}_hs"
162
+
163
+
164
+ def _hs_name_paper(n: int) -> str:
165
+ return f"has{n}H" if n == 1 else f"has{n}Hs"
166
+
167
+
168
+ def _charge_name(n: int) -> str:
169
+ if n == 0:
170
+ return "charge0"
171
+ return f"charge_{'p' if n > 0 else 'm'}{abs(n)}"
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class NamingScheme:
176
+ """One consistent spelling of the ChemLog vocabulary.
177
+
178
+ ``bond_type_names`` maps each :data:`BOND_KINDS` key to that scheme's
179
+ predicate name; ``has_bond_to_name`` is ``None`` under the ``"paper"``
180
+ scheme (see the module docstring's "``has_bond_to``" bullet).
181
+ """
182
+
183
+ key: str
184
+ hs_name: Callable[[int], str]
185
+ charge_name: Callable[[int], str]
186
+ net_charge_neutral_name: str
187
+ bond_type_names: Dict[str, str]
188
+ has_bond_to_name: Optional[str]
189
+
190
+
191
+ CHEMLOG = NamingScheme(
192
+ key="chemlog",
193
+ hs_name=_hs_name_chemlog,
194
+ charge_name=_charge_name,
195
+ net_charge_neutral_name="net_charge_neutral",
196
+ bond_type_names={
197
+ "SINGLE": "bSINGLE", "DOUBLE": "bDOUBLE",
198
+ "TRIPLE": "bTRIPLE", "AROMATIC": "bAROMATIC",
199
+ },
200
+ has_bond_to_name="has_bond_to",
201
+ )
202
+
203
+ PAPER = NamingScheme(
204
+ key="paper",
205
+ hs_name=_hs_name_paper,
206
+ charge_name=_charge_name,
207
+ net_charge_neutral_name="NetChargeNeutral",
208
+ bond_type_names={
209
+ "SINGLE": "singleBond", "DOUBLE": "doubleBond",
210
+ "TRIPLE": "tripleBond", "AROMATIC": "aromaticBond",
211
+ },
212
+ has_bond_to_name=None,
213
+ )
214
+
215
+ #: ``naming=`` argument -> scheme, consulted by ``mol.mol_to_structure``.
216
+ SCHEMES: Dict[str, NamingScheme] = {"chemlog": CHEMLOG, "paper": PAPER}
217
+
218
+
219
+ def chemlog_predicate_arities() -> Dict[str, int]:
220
+ """Every predicate ``mol_to_structure(naming="chemlog")`` may populate,
221
+ mapped to its arity — the vocabulary
222
+ :data:`unicode_logic_kit.chem.signature.CHEMLOG_SIGNATURE` declares.
223
+
224
+ Includes the computed predicates (present when ``computed=True``, the
225
+ default) alongside the stored ones, since both are equally part of what
226
+ a formula checked against ``CHEMLOG_SIGNATURE`` is allowed to mention —
227
+ :class:`~unicode_logic_kit.fol.signature.Signature` has no notion of
228
+ "stored vs. computed", only "declared vs. not".
229
+ """
230
+ out: Dict[str, int] = {}
231
+ for letter in ELEMENT_LETTERS.values():
232
+ out[letter] = 1
233
+ out[ATOM_NAME] = 1
234
+ for n in CANONICAL_HS_RANGE:
235
+ out[CHEMLOG.hs_name(n)] = 1
236
+ for n in CANONICAL_CHARGES:
237
+ out[CHEMLOG.charge_name(n)] = 1
238
+ out[CHIRAL_R_NAME] = 1
239
+ out[CHIRAL_S_NAME] = 1
240
+ for kind in BOND_KINDS:
241
+ out[CHEMLOG.bond_type_names[kind]] = 2
242
+ out[BOND_SUPER_NAME] = 2
243
+ assert CHEMLOG.has_bond_to_name is not None
244
+ out[CHEMLOG.has_bond_to_name] = 2
245
+ out[CHEMLOG.net_charge_neutral_name] = 0
246
+ out[NET_CHARGE_POSITIVE_NAME] = 0
247
+ out[NET_CHARGE_NEGATIVE_NAME] = 0
248
+ # computed
249
+ out["in_ring"] = 1
250
+ for size in RING_SIZE_FAMILY:
251
+ out[f"in_ring_of_size_{size}"] = 1
252
+ out["aromatic"] = 1
253
+ out["same_fragment"] = 2
254
+ out["carbon_connected"] = 2
255
+ return out
256
+
257
+
258
+ def _build_chemlog_to_paper() -> Dict[str, str]:
259
+ """The alias table :mod:`unicode_logic_kit.chem.signature` publishes,
260
+ derived from the two :class:`NamingScheme` objects above rather than
261
+ hand-duplicated, so it cannot drift from what ``mol.py`` actually emits
262
+ (see the module docstring's opening paragraph)."""
263
+ out: Dict[str, str] = {}
264
+ for letter in ELEMENT_LETTERS.values():
265
+ out[letter] = letter
266
+ out[ATOM_NAME] = ATOM_NAME
267
+ out[CHIRAL_R_NAME] = CHIRAL_R_NAME
268
+ out[CHIRAL_S_NAME] = CHIRAL_S_NAME
269
+ out[BOND_SUPER_NAME] = BOND_SUPER_NAME
270
+ out[NET_CHARGE_POSITIVE_NAME] = NET_CHARGE_POSITIVE_NAME
271
+ out[NET_CHARGE_NEGATIVE_NAME] = NET_CHARGE_NEGATIVE_NAME
272
+ out[CHEMLOG.net_charge_neutral_name] = PAPER.net_charge_neutral_name
273
+ for n in CANONICAL_HS_RANGE:
274
+ out[CHEMLOG.hs_name(n)] = PAPER.hs_name(n)
275
+ for n in CANONICAL_CHARGES:
276
+ out[CHEMLOG.charge_name(n)] = PAPER.charge_name(n)
277
+ for kind in BOND_KINDS:
278
+ out[CHEMLOG.bond_type_names[kind]] = PAPER.bond_type_names[kind]
279
+ # has_bond_to deliberately excluded: no paper-scheme counterpart exists.
280
+ return out
281
+
282
+
283
+ CHEMLOG_TO_PAPER: Dict[str, str] = _build_chemlog_to_paper()
284
+ PAPER_TO_CHEMLOG: Dict[str, str] = {v: k for k, v in CHEMLOG_TO_PAPER.items()}