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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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
|