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