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