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,525 @@
1
+ """SMILES / RDKit ``Mol`` in, a finite first-order structure out.
2
+
3
+ (The summary line above deliberately carries no cross-reference role:
4
+ autosummary truncates it at the first sentence end, and a dotted target like
5
+ ``semantics.structures.FiniteStructure`` gets cut mid-role, leaving an
6
+ unterminated backtick in the generated table.)
7
+
8
+ This is the bridge model-checking-based classification needs: a molecule
9
+ *is* a finite FOL structure — atoms are individuals, and classification is
10
+ model CHECKING a class definition against that structure — but that framing
11
+ is usually stated in prose only, with no code for the molecule side, at
12
+ most a worked example done by hand (the one this module's ethanol test
13
+ reproduces exactly). :func:`mol_to_structure` is that missing translation:
14
+ SMILES (or an already-parsed RDKit ``Mol``) in, :class:`FiniteStructure`
15
+ out, in the ChemLog vocabulary (Flügel et al. 2025, MIT licence,
16
+ https://github.com/sfluegel05/chemlog-peptides) — see
17
+ :mod:`unicode_logic_kit.chem._naming` for exactly how that vocabulary is
18
+ spelled and why, and
19
+ :data:`unicode_logic_kit.chem.signature.CHEMLOG_SIGNATURE` for the matching
20
+ declared vocabulary ``api.check`` can validate a formula against.
21
+
22
+ RDKit is OPTIONAL
23
+ ------------------
24
+ This module must stay importable without RDKit installed — the kit's
25
+ existing pattern for an optional binding (:mod:`unicode_logic_kit.atp.
26
+ cvc5_backend`) imports its dependency lazily inside the function that needs
27
+ it, not at module scope, and this module does the same via
28
+ :func:`_require_rdkit`. Install with ``pip install rdkit`` (or the
29
+ ``unicode-logic-kit[chem]`` extra, once wired into the package's own
30
+ ``pyproject.toml`` — that wiring is out of this module's scope). Calling
31
+ :func:`mol_to_structure` without RDKit installed raises :class:`ImportError`
32
+ naming exactly that install command, rather than failing on some unrelated
33
+ ``NameError`` deep inside the function body.
34
+
35
+ Domain, naming, and hydrogen counting
36
+ ---------------------------------------
37
+ The domain is every NON-hydrogen atom (ChemLog's own convention — hydrogens
38
+ are folded into per-heavy-atom hydrogen-COUNT predicates instead of being
39
+ individuals of their own, which is what keeps ``CCO`` a 3-individual
40
+ structure rather than a 9-individual one). Concretely: the input is first
41
+ normalised with ``Chem.RemoveHs`` — this folds any EXPLICITLY written
42
+ hydrogen atom (``"[H]C([H])([H])[H]"`` for methane, as opposed to the more
43
+ usual implicit ``"C"``) back into the heavy atom's implicit hydrogen count,
44
+ so the two spellings of the same molecule produce IDENTICAL structures
45
+ (hand-verified: both give the single individual ``c1`` with
46
+ ``has_4_hs(c1)``). ``RemoveHs`` also compacts the remaining atom indices
47
+ while preserving their RELATIVE order (hand-verified against
48
+ ``"C([H])(N)C(=O)O"`` — removing the explicit H at index 1 shifts every
49
+ later index down by exactly one, changing nothing else), which is what
50
+ makes the "stable name = element symbol + a running per-element index, in
51
+ input atom order" scheme deterministic: :func:`mol_to_structure` never
52
+ re-sorts or canonicalises atom order beyond what ``RemoveHs`` already does.
53
+ A residual ``H`` atom ``RemoveHs`` could not fold (an isotope-labelled
54
+ deuterium/tritium, or one RDKit keeps for stereo-perception reasons) is
55
+ refused with :class:`ValueError` rather than silently either being counted
56
+ as a heavy-atom individual (wrong: it would show up as a spurious ``h1``
57
+ individual with `h1` domain membership when the CHEMLOG-SIGNATUR contract's
58
+ own domain is "non-hydrogen atoms") or silently dropped from the hydrogen
59
+ count (wrong: an undercount no downstream consumer could detect).
60
+
61
+ "Deterministic" here means exactly one thing: calling :func:`mol_to_structure`
62
+ twice on the SAME input text (or the same already-parsed ``Mol``) always
63
+ names the same atom the same way. It is NOT a canonical, input-independent
64
+ molecule identifier — two DIFFERENT SMILES spellings of the identical
65
+ molecule can, and often do, assign the SAME name to two DIFFERENT atoms,
66
+ because the naming is a direct readout of RDKit's atom-parse order, which
67
+ itself is a readout of the order atoms are WRITTEN in the SMILES text, not
68
+ of the molecule's structure. Hand-verified: ``"CCO"`` (ethanol written
69
+ methyl-first) gives atom order C(methyl), C(methylene), O, so ``c1`` is the
70
+ METHYL carbon; ``"OCC"`` (the identical molecule, written oxygen-first)
71
+ gives atom order O, C(methylene), C(methyl), so ``c1`` is now the
72
+ METHYLENE carbon — the same name, two different atoms of the same
73
+ molecule, depending purely on which SMILES string was fed in. A caller
74
+ that needs atom identity to survive a re-serialisation, a canonicalisation
75
+ pass, or comparison across two independently-obtained SMILES strings for
76
+ "the same" molecule must not rely on these names for that — they identify
77
+ a position in ONE parse of ONE input text, nothing more.
78
+
79
+ Hydrogen count itself is ``Atom.GetTotalNumHs()`` with RDKit's own default
80
+ arguments — which, as hand-verified above, already sums implicit AND
81
+ explicit hydrogens correctly regardless of which way the input SMILES wrote
82
+ them, so no separate accounting is needed here.
83
+
84
+ Two naming schemes, chosen at the call site
85
+ ----------------------------------------------
86
+ ``naming="chemlog"`` (the default) spells every predicate the way the real
87
+ ChemLog TPTP files do (``bSINGLE``, ``has_1_hs``, ...); ``naming="paper"``
88
+ spells them the way prose write-ups of such class definitions do
89
+ (``singleBond``, ``has1H``, ...) — this is what lets the ethanol acceptance
90
+ test below reproduce a hand-written worked example VERBATIM rather than
91
+ merely "equivalently". See :mod:`unicode_logic_kit.chem._naming` for the exact
92
+ per-symbol rationale and the two schemes' full definitions, and
93
+ :mod:`unicode_logic_kit.chem.signature` for the ``CHEMLOG_TO_PAPER`` /
94
+ ``PAPER_TO_CHEMLOG`` alias tables that translate between them after the
95
+ fact.
96
+
97
+ Always-populated predicates
98
+ ------------------------------
99
+ Every predicate the chosen naming scheme's FIXED vocabulary declares (the
100
+ six atom-type letters, ``atom``, the four canonical hydrogen-count
101
+ predicates, the three canonical charge predicates, ``ChiralR``/``ChiralS``,
102
+ the four bond-type predicates, the ``bond``/``has_bond_to`` super-relations,
103
+ and the three net-charge globals) is given an EXPLICIT extension in the
104
+ returned structure — even an EMPTY one, for a molecule that does not
105
+ exhibit that feature. A sulfur-free molecule's structure still interprets
106
+ ``s/1`` (as ``∅``), because :data:`unicode_logic_kit.chem.signature.
107
+ CHEMLOG_SIGNATURE` declares ``s/1`` as legitimate ChemLog vocabulary — a
108
+ formula asking ``s(x)`` about ethanol must evaluate to "false for every
109
+ ``x``" (:meth:`FiniteStructure.holds` returning ``False``), not raise
110
+ ``KeyError`` for an "uninterpreted" symbol the vocabulary in fact declares.
111
+ A molecule that genuinely needs a predicate OUTSIDE the canonical range
112
+ (``has_4_hs`` for methane's carbon; ``charge_m2`` for a doubly-anionic atom)
113
+ still gets it — the atom loop adds such a key on demand — it is simply not
114
+ PRE-populated as an empty default the way the canonical range is.
115
+
116
+ Why five predicates are COMPUTED, not stored (and why exactly these five)
117
+ ------------------------------------------------------------------------------
118
+ :mod:`unicode_logic_kit.semantics.structures`'s own design note: some
119
+ properties of a finite structure are decidable ON it while being
120
+ inexpressible IN first-order logic OVER it. ChemLog hits this exactly
121
+ (their own external "BuildingBlock" predicate, computed in Python and
122
+ injected as ground facts) and this module follows the same pattern via
123
+ ``FiniteStructure``'s ``computed=`` mechanism, so a definition may use
124
+ these predicates without the structure materialising their extension by
125
+ brute-force domain-squared enumeration:
126
+
127
+ * ``same_fragment/2`` — "connected in the bond graph" is the textbook
128
+ FOL-inexpressible property: no fixed FOL formula (a fixed, finite
129
+ quantifier count) can express "there exists a bond-path of ANY length
130
+ between x and y", because for every candidate formula there is a large
131
+ enough disconnected pair of molecules that formula cannot tell apart from
132
+ a connected one (a compactness argument, the same one that rules out
133
+ expressing "the graph is connected" in FOL generally). Computed here as
134
+ the REFLEXIVE-symmetric-transitive closure of the bond relation (RDKit's
135
+ ``GetMolFrags``) — deliberately the full equivalence relation ("same
136
+ connected component"), not merely the bare transitive closure of the edge
137
+ relation (which would leave an isolated, bond-free atom unrelated to
138
+ itself).
139
+ * ``carbon_connected/2`` — the identical inexpressibility argument, applied
140
+ to the induced subgraph of carbon atoms and carbon–carbon bonds only
141
+ (chemically: "on the same carbon skeleton", ignoring heteroatom
142
+ branches). Both endpoints must themselves be carbon; a non-carbon atom is
143
+ ``carbon_connected`` to nothing, not even itself.
144
+ * ``in_ring/1`` — "lies on SOME cycle of the bond graph" is the same
145
+ unbounded-length-path inexpressibility as ``same_fragment``, now applied
146
+ to a cycle instead of a path. Computed via RDKit's SSSR ring perception
147
+ (``GetRingInfo``).
148
+ * ``in_ring_of_size_N/1`` (``N`` in :data:`unicode_logic_kit.chem._naming.
149
+ RING_SIZE_FAMILY`, i.e. 3..8) — for one FIXED ``N`` this predicate actually
150
+ IS FOL-expressible in principle (∃ x₁…x_N pairwise-distinct with a bond
151
+ cycling through x and all of them — a bounded quantifier count), so
152
+ strictly it does not need to be computed. It is offered as a computed
153
+ convenience anyway, for the same reason ChemLog itself precomputes ring
154
+ membership rather than making every consumer spell out an N-quantifier
155
+ cycle formula by hand: writing that axiom out is exactly the kind of
156
+ syntax burden that makes hand- or machine-written class definitions fail
157
+ on pure syntax before they are ever evaluated. A ring larger than the
158
+ family's max (a macrocycle) is still correctly reported by the general
159
+ ``in_ring/1`` (True); it simply has no ``in_ring_of_size_N`` member of
160
+ its own — a real, documented limitation, not a silent one.
161
+ * ``aromatic/1`` — RDKit's aromaticity perception is a Hückel-rule
162
+ ring-electron-counting algorithm, not a graph-local property any fixed-
163
+ quantifier FOL formula over ``bond``/``bAROMATIC`` could re-derive from
164
+ the stored bond-type facts alone even in principle (it would need to
165
+ recognise arbitrarily-sized alternating ring systems). Kept computed
166
+ (delegated to RDKit's own perception at query time) rather than
167
+ duplicated as a second, stored copy of what ``bAROMATIC`` bond membership
168
+ already encodes at the BOND level, so there is exactly one source of
169
+ truth for "is this molecule's aromaticity" and it is never at risk of
170
+ drifting out of sync with itself. Deliberately computed from the
171
+ ORIGINAL (non-Kekulized) molecule regardless of the ``aromatic=`` bond-
172
+ typing choice below — see that parameter's own note.
173
+
174
+ ``computed=False`` omits all five entirely (``FiniteStructure.computed`` is
175
+ then empty), giving a structure whose ENTIRE vocabulary is genuinely FOL-
176
+ expressible over — useful e.g. for measuring how far a ChEBI class
177
+ definition gets using only facts a plain FOL formula could in principle
178
+ re-derive on its own.
179
+
180
+ The ``aromatic=`` parameter: bond typing, not atom typing
181
+ --------------------------------------------------------------
182
+ ``aromatic=True`` (the default) keeps RDKit's own aromaticity perception in
183
+ the BOND types: a benzene ring's six bonds are all ``bAROMATIC`` and NONE
184
+ are ``bSINGLE``/``bDOUBLE``. ``aromatic=False`` Kekulizes a COPY of the
185
+ molecule first (``Chem.Kekulize(..., clearAromaticFlags=True)`` on a
186
+ ``Chem.Mol(mol)`` copy, never the original), so those same six bonds become
187
+ an alternating ``bSINGLE``/``bDOUBLE`` pattern and ``bAROMATIC`` is empty
188
+ for that ring. The atom-level ``aromatic/1`` COMPUTED predicate is
189
+ unaffected either way (hand-verified: Kekulizing a copy clears aromaticity
190
+ flags on the COPY only, never the original ``mol`` the atom-level predicate
191
+ reads from) — a deliberate decoupling: whether an atom is (chemically,
192
+ electronically) aromatic is a fact about the molecule, independent of which
193
+ bond-order REPRESENTATION was chosen to encode it.
194
+
195
+ Unsupported input is refused, never approximated
196
+ ------------------------------------------------------
197
+ An atom whose element is outside ChemLog's six-element vocabulary
198
+ (``{C, N, O, S, P, H}`` — see :mod:`unicode_logic_kit.chem._naming`) raises
199
+ :class:`ValueError` naming the element and its position, rather than
200
+ silently building an atom with no atom-type predicate at all (which would
201
+ make ``atom(x) ∧ ¬c(x) ∧ ¬n(x) ∧ …`` — a molecule with, say, a bromine —
202
+ look exactly like a modelling BUG rather than what it is: an out-of-scope
203
+ input). Likewise an unrecognised RDKit ``BondType`` (dative, quadruple, …)
204
+ is refused rather than silently dropped. An unparseable or chemically
205
+ invalid SMILES string (``Chem.MolFromSmiles`` returning ``None`` — RDKit
206
+ gives the identical ``None`` result for a syntax error and for a valence
207
+ error, e.g. a pentavalent carbon; hand-verified against both) raises
208
+ :class:`ValueError` with the offending text quoted.
209
+
210
+ The identical principle applies to stereochemistry. ``rdCIPLabeler`` (the
211
+ accurate CIP labeller ``mol_to_structure`` runs on every molecule — see
212
+ below) does not only assign the classical tetrahedral ``'R'``/``'S'``
213
+ labels this vocabulary's ``ChiralR``/``ChiralS`` predicates cover —
214
+ it also assigns lower-case ``'r'``/``'s'`` for a pseudo-asymmetric centre
215
+ (hand-verified: ``"OC(=O)[C@H](O)[C@@H](O)[C@H](O)C(=O)O"``'s middle
216
+ stereocentre gets ``_CIPCode == "s"``) and ``'M'``/``'P'`` for axial/helical
217
+ chirality (atropisomers, allenes). An atom with one of these non-``R``/
218
+ ``S`` codes DOES have defined stereochemistry RDKit could determine — so
219
+ leaving both ``ChiralR`` and ``ChiralS`` unset for it, the way an earlier
220
+ version of this function did, would silently make it indistinguishable
221
+ from an atom with NO stereocentre at all, exactly the kind of information
222
+ loss this section is about. :func:`mol_to_structure` therefore raises
223
+ :class:`ValueError` for such an atom rather than adding it.
224
+
225
+ This is deliberately NOT configurable (no ``on_unsupported_stereo="raise"|
226
+ "ignore"`` escape hatch): unlike, say, an unrecognised element — which a
227
+ caller processing a large corpus might reasonably want to skip past — this
228
+ refusal already composes cleanly with the two per-molecule call sites that
229
+ matter (:func:`unicode_logic_kit.eval.datasets.c3po.score_definition` and
230
+ :mod:`unicode_logic_kit.mcp.chem_tools`'s ``check_molecules``), both of which
231
+ already catch exactly this :class:`ValueError` and report it as ONE
232
+ molecule's error entry, never aborting the whole run — the same handling
233
+ they already give an unsupported element or bond type. Adding a second,
234
+ silent way to lose the same information a caller could instead catch
235
+ :class:`ValueError` for themselves would only invite exactly the kind of
236
+ "a bit of both" approximation this kit's own conventions refuse elsewhere;
237
+ extending the ChemLog vocabulary with new predicates for these cases is
238
+ also deliberately out of scope here — ChemLog's own predicate list is a
239
+ fixed, external vocabulary this module targets, not one this module is
240
+ free to grow on its own judgement.
241
+ """
242
+
243
+ from typing import Any, Dict, FrozenSet, Optional, Tuple
244
+
245
+ from ..semantics.structures import FiniteStructure
246
+ from . import _naming
247
+
248
+ __all__ = ["mol_to_structure"]
249
+
250
+ _RDKIT_INSTALL_HINT = (
251
+ "unicode_logic_kit.chem.mol needs RDKit: pip install rdkit "
252
+ "(or, once wired into this package's own extras: "
253
+ "pip install 'unicode-logic-kit[chem]')"
254
+ )
255
+
256
+ Individual = str
257
+ Key = Tuple[str, int]
258
+
259
+
260
+ def _require_rdkit():
261
+ """Import RDKit lazily; raise a clear, actionable :class:`ImportError` if
262
+ it is absent, rather than a bare ``ModuleNotFoundError`` pointing at the
263
+ import line inside this function (the same lazy-import-with-install-hint
264
+ shape as :meth:`unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.decide`)."""
265
+ try:
266
+ from rdkit import Chem, RDLogger
267
+ from rdkit.Chem import rdCIPLabeler
268
+ except ImportError as exc:
269
+ raise ImportError(_RDKIT_INSTALL_HINT) from exc
270
+ RDLogger.DisableLog("rdApp.*") # library-internal parse/valence warnings
271
+ return Chem, rdCIPLabeler
272
+
273
+
274
+ def mol_to_structure(
275
+ mol_or_smiles: Any, *,
276
+ naming: str = "chemlog",
277
+ aromatic: bool = True,
278
+ computed: bool = True,
279
+ ) -> FiniteStructure:
280
+ """Translate a molecule into a :class:`FiniteStructure` in the ChemLog
281
+ (or, with ``naming="paper"``, the prose-spelling) vocabulary.
282
+
283
+ Args:
284
+ mol_or_smiles: a SMILES string, or an already-parsed
285
+ ``rdkit.Chem.Mol`` (assumed already sanitised, as any ``Mol``
286
+ coming out of ``Chem.MolFromSmiles``/``MolFromMolFile``/etc. is
287
+ by default — this function does not re-sanitise one handed to
288
+ it directly).
289
+ naming: ``"chemlog"`` (default) or ``"paper"`` — see the module
290
+ docstring's "Two naming schemes" section.
291
+ aromatic: bond-typing choice — see the module docstring's
292
+ ``aromatic=`` section. Does not affect the computed
293
+ ``aromatic/1`` predicate.
294
+ computed: whether to attach the five FOL-inexpressible-or-impractical
295
+ computed predicates (``in_ring``, ``in_ring_of_size_N``,
296
+ ``aromatic``, ``same_fragment``, ``carbon_connected``) — see the
297
+ module docstring's "Why five predicates are COMPUTED" section.
298
+
299
+ Returns:
300
+ A :class:`FiniteStructure` whose domain is every non-hydrogen atom.
301
+
302
+ Raises:
303
+ ImportError: RDKit is not installed (message names the install
304
+ command).
305
+ ValueError: the SMILES text does not parse / is not a valid
306
+ molecule; ``naming`` is neither ``"chemlog"`` nor ``"paper"``; an
307
+ atom's element is outside ``{C, N, O, S, P, H}``; a bond has an
308
+ RDKit ``BondType`` this vocabulary does not cover; RDKit's own
309
+ ``RemoveHs`` could not fold away every hydrogen atom (see the
310
+ module docstring's "residual H atom" note); or an atom carries a
311
+ defined CIP stereo descriptor other than ``'R'``/``'S'`` (a
312
+ pseudo-asymmetric ``'r'``/``'s'``, or axial/helical ``'M'``/
313
+ ``'P'``) that ``ChiralR``/``ChiralS`` cannot represent (see the
314
+ module docstring's "Unsupported input is refused, never
315
+ approximated" section).
316
+ """
317
+ Chem, rdCIPLabeler = _require_rdkit()
318
+
319
+ scheme = _naming.SCHEMES.get(naming)
320
+ if scheme is None:
321
+ raise ValueError(
322
+ f"mol_to_structure: unknown naming={naming!r}, expected one of "
323
+ f"{sorted(_naming.SCHEMES)}"
324
+ )
325
+
326
+ if isinstance(mol_or_smiles, str):
327
+ mol = Chem.MolFromSmiles(mol_or_smiles)
328
+ if mol is None:
329
+ raise ValueError(
330
+ f"mol_to_structure: RDKit could not parse {mol_or_smiles!r} "
331
+ "as a molecule (invalid SMILES syntax, or a chemically "
332
+ "invalid structure such as an over-valent atom — RDKit "
333
+ "reports both identically as a parse failure)"
334
+ )
335
+ elif isinstance(mol_or_smiles, Chem.Mol):
336
+ mol = mol_or_smiles
337
+ else:
338
+ raise TypeError(
339
+ "mol_to_structure: mol_or_smiles must be a SMILES str or an "
340
+ f"rdkit.Chem.Mol, got {type(mol_or_smiles).__name__}"
341
+ )
342
+
343
+ mol = Chem.RemoveHs(mol)
344
+ for atom in mol.GetAtoms():
345
+ if atom.GetSymbol() == "H":
346
+ raise ValueError(
347
+ f"mol_to_structure: atom index {atom.GetIdx()} is a hydrogen "
348
+ "RDKit's RemoveHs could not fold away (an isotope label, or "
349
+ "one RDKit keeps for stereochemistry) — this function's "
350
+ "domain is exactly the non-hydrogen atoms and refuses to "
351
+ "either count it as one or silently drop it from a "
352
+ "hydrogen-count predicate"
353
+ )
354
+ Chem.GetSymmSSSR(mol) # ensure ring perception is initialised regardless
355
+ # of whether the caller's own Mol already had it
356
+ rdCIPLabeler.AssignCIPLabels(mol) # accurate R/S (not raw CW/CCW parity)
357
+
358
+ bond_type_key: Dict[object, str] = {
359
+ Chem.BondType.SINGLE: "SINGLE", Chem.BondType.DOUBLE: "DOUBLE",
360
+ Chem.BondType.TRIPLE: "TRIPLE", Chem.BondType.AROMATIC: "AROMATIC",
361
+ }
362
+
363
+ if aromatic:
364
+ bond_mol = mol
365
+ else:
366
+ bond_mol = Chem.Mol(mol)
367
+ Chem.Kekulize(bond_mol, clearAromaticFlags=True)
368
+
369
+ # -- domain: element symbol + a running per-element index, in the
370
+ # atom order RemoveHs left us with (see module docstring). ------------
371
+ name_by_idx: Dict[int, Individual] = {}
372
+ counters: Dict[str, int] = {}
373
+ for atom in mol.GetAtoms():
374
+ symbol = atom.GetSymbol()
375
+ letter = _naming.ELEMENT_LETTERS.get(symbol)
376
+ if letter is None:
377
+ raise ValueError(
378
+ f"mol_to_structure: unsupported element {symbol!r} at atom "
379
+ f"index {atom.GetIdx()} — the ChemLog vocabulary this "
380
+ f"function targets only types "
381
+ f"{sorted(_naming.ELEMENT_LETTERS)} atoms; silently "
382
+ "omitting this atom's type predicate would misrepresent "
383
+ "the molecule, so this is refused instead"
384
+ )
385
+ counters[letter] = counters.get(letter, 0) + 1
386
+ name_by_idx[atom.GetIdx()] = f"{letter}{counters[letter]}"
387
+
388
+ domain: Tuple[Individual, ...] = tuple(
389
+ name_by_idx[atom.GetIdx()] for atom in mol.GetAtoms())
390
+
391
+ # -- unary/binary stored extensions, canonical range pre-populated ----
392
+ unary: Dict[str, set] = {name: set() for name in (
393
+ list(_naming.ELEMENT_LETTERS.values())
394
+ + [_naming.ATOM_NAME]
395
+ + [scheme.hs_name(n) for n in _naming.CANONICAL_HS_RANGE]
396
+ + [scheme.charge_name(n) for n in _naming.CANONICAL_CHARGES]
397
+ + [_naming.CHIRAL_R_NAME, _naming.CHIRAL_S_NAME]
398
+ )}
399
+ binary_names = [scheme.bond_type_names[k] for k in _naming.BOND_KINDS] \
400
+ + [_naming.BOND_SUPER_NAME]
401
+ if scheme.has_bond_to_name:
402
+ binary_names.append(scheme.has_bond_to_name)
403
+ binary: Dict[str, set] = {name: set() for name in binary_names}
404
+
405
+ for atom in mol.GetAtoms():
406
+ idx = atom.GetIdx()
407
+ name = name_by_idx[idx]
408
+ letter = _naming.ELEMENT_LETTERS[atom.GetSymbol()]
409
+ unary.setdefault(letter, set()).add((name,))
410
+ unary.setdefault(_naming.ATOM_NAME, set()).add((name,))
411
+ hs_name = scheme.hs_name(atom.GetTotalNumHs())
412
+ unary.setdefault(hs_name, set()).add((name,))
413
+ charge_name = scheme.charge_name(atom.GetFormalCharge())
414
+ unary.setdefault(charge_name, set()).add((name,))
415
+ cip = atom.GetPropsAsDict().get("_CIPCode")
416
+ if cip == "R":
417
+ unary.setdefault(_naming.CHIRAL_R_NAME, set()).add((name,))
418
+ elif cip == "S":
419
+ unary.setdefault(_naming.CHIRAL_S_NAME, set()).add((name,))
420
+ elif cip is not None:
421
+ raise ValueError(
422
+ f"mol_to_structure: atom index {idx} ({name}) carries a "
423
+ f"defined CIP stereo descriptor {cip!r} this vocabulary "
424
+ "cannot represent — ChemLog's ChiralR/ChiralS predicates "
425
+ "only cover the classical tetrahedral 'R'/'S' labels, but "
426
+ "rdCIPLabeler also assigns lower-case 'r'/'s' for "
427
+ "pseudo-asymmetric centres and 'M'/'P' for axial/helical "
428
+ "chirality (see the module docstring's 'Unsupported input "
429
+ "is refused, never approximated' section). Silently "
430
+ "leaving both ChiralR and ChiralS unset would make this "
431
+ "atom indistinguishable from one with NO defined "
432
+ "stereochemistry at all, so this is refused instead."
433
+ )
434
+
435
+ for bond in bond_mol.GetBonds():
436
+ kind = bond_type_key.get(bond.GetBondType())
437
+ if kind is None:
438
+ raise ValueError(
439
+ "mol_to_structure: unsupported bond type "
440
+ f"{bond.GetBondType()!s} between atom indices "
441
+ f"{bond.GetBeginAtomIdx()} and {bond.GetEndAtomIdx()} — "
442
+ f"this vocabulary only covers {_naming.BOND_KINDS}"
443
+ )
444
+ a = name_by_idx[bond.GetBeginAtomIdx()]
445
+ b = name_by_idx[bond.GetEndAtomIdx()]
446
+ pred_name = scheme.bond_type_names[kind]
447
+ binary.setdefault(pred_name, set()).update({(a, b), (b, a)})
448
+ binary.setdefault(_naming.BOND_SUPER_NAME, set()).update({(a, b), (b, a)})
449
+ if scheme.has_bond_to_name:
450
+ binary.setdefault(scheme.has_bond_to_name, set()).update({(a, b), (b, a)})
451
+
452
+ extensions: Dict[Key, FrozenSet] = {}
453
+ for name, rows in unary.items():
454
+ extensions[(name, 1)] = frozenset(rows)
455
+ for name, rows in binary.items():
456
+ extensions[(name, 2)] = frozenset(rows)
457
+
458
+ net_total = Chem.GetFormalCharge(mol)
459
+ extensions[(scheme.net_charge_neutral_name, 0)] = (
460
+ frozenset({()}) if net_total == 0 else frozenset())
461
+ extensions[(_naming.NET_CHARGE_POSITIVE_NAME, 0)] = (
462
+ frozenset({()}) if net_total > 0 else frozenset())
463
+ extensions[(_naming.NET_CHARGE_NEGATIVE_NAME, 0)] = (
464
+ frozenset({()}) if net_total < 0 else frozenset())
465
+
466
+ computed_preds: Dict[Key, object] = {}
467
+ if computed:
468
+ ri = mol.GetRingInfo()
469
+ ring_sizes_by_name: Dict[Individual, FrozenSet[int]] = {}
470
+ in_ring_by_name: Dict[Individual, bool] = {}
471
+ aromatic_by_name: Dict[Individual, bool] = {}
472
+ for idx, name in name_by_idx.items():
473
+ sizes = frozenset(ri.AtomRingSizes(idx))
474
+ ring_sizes_by_name[name] = sizes
475
+ in_ring_by_name[name] = bool(sizes)
476
+ aromatic_by_name[name] = mol.GetAtomWithIdx(idx).GetIsAromatic()
477
+
478
+ fragment_by_name: Dict[Individual, int] = {}
479
+ for fid, comp in enumerate(Chem.GetMolFrags(mol, asMols=False)):
480
+ for idx in comp:
481
+ fragment_by_name[name_by_idx[idx]] = fid
482
+
483
+ carbon_idxs = {idx for idx, name in name_by_idx.items()
484
+ if mol.GetAtomWithIdx(idx).GetSymbol() == "C"}
485
+ carbon_adj: Dict[int, list] = {idx: [] for idx in carbon_idxs}
486
+ for bond in mol.GetBonds():
487
+ a_idx, b_idx = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
488
+ if a_idx in carbon_idxs and b_idx in carbon_idxs:
489
+ carbon_adj[a_idx].append(b_idx)
490
+ carbon_adj[b_idx].append(a_idx)
491
+ carbon_fragment_by_name: Dict[Individual, Optional[int]] = {}
492
+ visited: set = set()
493
+ next_cfid = 0
494
+ for start in carbon_idxs:
495
+ if start in visited:
496
+ continue
497
+ stack = [start]
498
+ visited.add(start)
499
+ while stack:
500
+ cur = stack.pop()
501
+ carbon_fragment_by_name[name_by_idx[cur]] = next_cfid
502
+ for nb in carbon_adj[cur]:
503
+ if nb not in visited:
504
+ visited.add(nb)
505
+ stack.append(nb)
506
+ next_cfid += 1
507
+ for idx, name in name_by_idx.items():
508
+ if idx not in carbon_idxs:
509
+ carbon_fragment_by_name[name] = None
510
+
511
+ computed_preds[("in_ring", 1)] = (
512
+ lambda x, _d=in_ring_by_name: _d.get(x, False))
513
+ for size in _naming.RING_SIZE_FAMILY:
514
+ def _in_ring_of_size(x, _size=size, _d=ring_sizes_by_name):
515
+ return _size in _d.get(x, frozenset())
516
+ computed_preds[(f"in_ring_of_size_{size}", 1)] = _in_ring_of_size
517
+ computed_preds[("aromatic", 1)] = (
518
+ lambda x, _d=aromatic_by_name: _d.get(x, False))
519
+ computed_preds[("same_fragment", 2)] = (
520
+ lambda x, y, _d=fragment_by_name: _d.get(x) == _d.get(y))
521
+ computed_preds[("carbon_connected", 2)] = (
522
+ lambda x, y, _d=carbon_fragment_by_name:
523
+ _d.get(x) is not None and _d.get(x) == _d.get(y))
524
+
525
+ return FiniteStructure(domain=domain, extensions=extensions, computed=computed_preds)
@@ -0,0 +1,112 @@
1
+ """The declared ChemLog vocabulary as a first-class kit signature.
2
+
3
+ Plus the alias mapping between the two spellings
4
+ :mod:`unicode_logic_kit.chem.mol` can emit. (The summary line above carries no
5
+ role and no dotted name on purpose: autosummary truncates it at the first
6
+ period, so ``:mod:`unicode_logic_kit.chem.mol``` would be cut after
7
+ ``unicode_logic_kit.`` and leave an unterminated backtick in the generated
8
+ table.)
9
+
10
+ :data:`CHEMLOG_SIGNATURE` is what makes
11
+ ``unicode_logic_kit.api.check(formula, signature=CHEMLOG_SIGNATURE)`` reject a
12
+ class definition that uses an unknown predicate or the wrong arity — an
13
+ LLM-generated class definition that fails usually fails on pure SYNTAX
14
+ rather than on chemistry, and a large share of that is exactly the kind of
15
+ vocabulary slip (wrong arity, a typo'd predicate name, a hallucinated one) a
16
+ signature check catches for free, before the definition ever reaches model
17
+ checking. It is built from
18
+ :func:`unicode_logic_kit.chem._naming.chemlog_predicate_arities` (the SAME
19
+ source :mod:`.mol` itself consults) rather than a second, hand-copied
20
+ literal list — so a predicate ``mol_to_structure(naming=
21
+ "chemlog")`` can actually produce is *by construction* always something
22
+ ``CHEMLOG_SIGNATURE`` declares, and vice versa. This module has no RDKit
23
+ dependency (:mod:`._naming` does not either) — a caller wanting only
24
+ signature validation, not molecule translation, never pays RDKit's import
25
+ cost.
26
+
27
+ What ``CHEMLOG_SIGNATURE`` does NOT cover — documented, not silent
28
+ ------------------------------------------------------------------------
29
+ Only the CANONICAL hydrogen-count (0..3) and formal-charge (-1, 0, +1)
30
+ range is declared, matching the ChemLog spec text's own literal examples
31
+ (https://github.com/sfluegel05/chemlog-peptides; see :mod:`._naming`'s
32
+ ``CANONICAL_HS_RANGE`` / ``CANONICAL_CHARGES``). A real molecule needing
33
+ ``has_4_hs`` (methane) or ``charge_m2`` (a doubly-anionic atom, e.g. one
34
+ oxygen of a deprotonated phosphate) still gets that predicate POPULATED by
35
+ ``mol_to_structure`` (its atom loop adds such keys on demand — see that
36
+ module's docstring), but ``CHEMLOG_SIGNATURE.validate()`` / ``api.check``
37
+ will then honestly report it as an UNDECLARED predicate rather than
38
+ silently accepting anything shaped like ``charge_m<N>``. This is the kit's
39
+ loud-refusal convention applied to the signature itself: widening the
40
+ declared range is a caller's explicit choice
41
+ (``CHEMLOG_SIGNATURE.merge(Signature.from_dict({"predicates": {"charge_m2": 1}}))``),
42
+ never an implicit one made silently on a caller's behalf here.
43
+
44
+ Argument sorts are deliberately left unset (every :class:`PredicateDecl` has
45
+ ``arg_sorts=None``) — ChemLog's own vocabulary is single-sorted (there is
46
+ only one kind of individual, "atom"), so there is nothing for a sort
47
+ declaration to distinguish.
48
+ """
49
+
50
+ from typing import Dict
51
+
52
+ from ..fol.signature import Signature, PredicateDecl
53
+ from . import _naming
54
+
55
+ __all__ = ["CHEMLOG_SIGNATURE", "CHEMLOG_TO_PAPER", "PAPER_TO_CHEMLOG",
56
+ "to_paper_naming", "to_chemlog_naming"]
57
+
58
+ #: The ChemLog (TPTP-style) vocabulary as a first-class kit :class:`Signature`
59
+ #: — see the module docstring for exactly what is (and is not) covered.
60
+ CHEMLOG_SIGNATURE = Signature(predicates={
61
+ name: PredicateDecl(name, arity)
62
+ for name, arity in _naming.chemlog_predicate_arities().items()
63
+ })
64
+
65
+ #: ChemLog-spelling predicate name -> the camelCase prose spelling of the
66
+ #: SAME predicate (e.g. ``"has_3_hs" -> "has3Hs"``, ``"bSINGLE" ->
67
+ #: "singleBond"``). Covers exactly the predicates :data:`CHEMLOG_SIGNATURE`
68
+ #: declares MINUS ``has_bond_to`` (the prose scheme names no second
69
+ #: bond-existence relation — see :mod:`._naming`) and minus the five
70
+ #: computed predicates (``in_ring`` etc. — scheme-invariant, so an alias
71
+ #: entry would be a no-op; see :mod:`._naming`'s closing paragraph).
72
+ CHEMLOG_TO_PAPER: Dict[str, str] = dict(_naming.CHEMLOG_TO_PAPER)
73
+
74
+ #: The inverse of :data:`CHEMLOG_TO_PAPER`.
75
+ PAPER_TO_CHEMLOG: Dict[str, str] = dict(_naming.PAPER_TO_CHEMLOG)
76
+
77
+
78
+ def to_paper_naming(chemlog_name: str) -> str:
79
+ """Translate a ChemLog-spelled predicate name to its prose spelling.
80
+
81
+ Raises :class:`KeyError` (naming the symbol) for a predicate outside
82
+ :data:`CHEMLOG_TO_PAPER`'s domain — either not part of the ChemLog
83
+ vocabulary at all, or one of the two documented exclusions above
84
+ (``has_bond_to``, or a computed predicate, which needs no translation
85
+ since it is spelled identically already).
86
+ """
87
+ try:
88
+ return CHEMLOG_TO_PAPER[chemlog_name]
89
+ except KeyError:
90
+ raise KeyError(
91
+ f"to_paper_naming: {chemlog_name!r} has no paper-prose alias "
92
+ "(either not a ChemLog predicate at all, or one of the "
93
+ "documented exclusions — see the chem.signature module "
94
+ "docstring)"
95
+ ) from None
96
+
97
+
98
+ def to_chemlog_naming(paper_name: str) -> str:
99
+ """Translate a prose-spelled predicate name to its ChemLog spelling.
100
+
101
+ Inverse of :func:`to_paper_naming`; raises :class:`KeyError` on the same
102
+ terms.
103
+ """
104
+ try:
105
+ return PAPER_TO_CHEMLOG[paper_name]
106
+ except KeyError:
107
+ raise KeyError(
108
+ f"to_chemlog_naming: {paper_name!r} has no ChemLog alias "
109
+ "(either not a recognised paper-prose predicate, or one of the "
110
+ "documented exclusions — see the chem.signature module "
111
+ "docstring)"
112
+ ) from None