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,1031 @@
|
|
|
1
|
+
"""Chemistry MCP tools — model-checking feedback for an LLM writing ChEBI FOL.
|
|
2
|
+
|
|
3
|
+
The setting (see ``unicode_logic_kit.chem``'s own module docstring for the
|
|
4
|
+
full brief): an LLM translates a ChEBI class definition to FOL;
|
|
5
|
+
classification is MODEL CHECKING that formula against a molecule represented
|
|
6
|
+
as a finite structure. The failure modes that loop runs into are what these
|
|
7
|
+
six tools exist to close, one each:
|
|
8
|
+
|
|
9
|
+
* the LLM never SEES what its formula is being checked against
|
|
10
|
+
(:func:`molecule_to_structure` — the structure as JSON, plus a headline
|
|
11
|
+
domain size / nonempty-predicate summary a caller can read without parsing
|
|
12
|
+
the whole extension table);
|
|
13
|
+
* a single positive example failing gives no diagnosis, only pass/fail
|
|
14
|
+
(:func:`check_molecule` — ``holds`` plus, on failure, the
|
|
15
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate_detailed` blame trail);
|
|
16
|
+
* a whole labelled corpus needs to be re-checked after every definition edit,
|
|
17
|
+
and a definition that is too general (matching far too much, at a precision
|
|
18
|
+
cost invisible until it is measured against a corpus) is exactly the kind
|
|
19
|
+
of thing only visible in aggregate (:func:`check_molecules` — per-molecule
|
|
20
|
+
results plus a holds/fails/unknown/errors summary);
|
|
21
|
+
* a failing conjunct alone still does not say what the molecule looks like —
|
|
22
|
+
a counterexample needs explaining, not just reporting
|
|
23
|
+
(:func:`explain_molecule_failure` — the failing conjunct PLUS the
|
|
24
|
+
molecule's atoms-by-type and bond list, so a model can see both halves of
|
|
25
|
+
the mismatch at once);
|
|
26
|
+
* redundant pairwise-inequality chains (``hasAtLeast40Carbons`` written as 40
|
|
27
|
+
nested existentials + C(40,2)=780 ``≠`` literals) are a routine timeout
|
|
28
|
+
cause (:func:`simplify_definition`
|
|
29
|
+
— applies :func:`~unicode_logic_kit.fol.simplify_check.simplify_for_checking`
|
|
30
|
+
and reports what shrank, by how much);
|
|
31
|
+
* pure-SYNTAX failures, hallucinated predicate names among them, burn a whole
|
|
32
|
+
generation attempt each (:func:`chemical_signature` — the exact
|
|
33
|
+
40-predicate ChemLog vocabulary so a generator never has to guess what it
|
|
34
|
+
is allowed to write).
|
|
35
|
+
|
|
36
|
+
Conventions (matching :mod:`unicode_logic_kit.mcp.server`, followed exactly so
|
|
37
|
+
a client that already talks to that server needs no special-casing for these
|
|
38
|
+
six tools)
|
|
39
|
+
------------------------------------------------------------------------------
|
|
40
|
+
* every tool returns a JSON-compatible dict;
|
|
41
|
+
* a bad TEXT argument (an unparseable formula, or a SMILES string that does
|
|
42
|
+
not describe a valid molecule) comes back as
|
|
43
|
+
``{"ok": False, "argument": <which input — "formula"/"smiles"/
|
|
44
|
+
"smiles[<i>]">, "errors": [...], "spec_topic": <topic>}`` — the SAME shape
|
|
45
|
+
for both, deliberately: from the calling pipeline's perspective a bad
|
|
46
|
+
formula and a bad SMILES are the identical case, "this argument did not
|
|
47
|
+
work", and a generic client checks ``result.get("ok") is False``, full
|
|
48
|
+
stop;
|
|
49
|
+
* a documented exception that is not about a bad argument (RDKit not
|
|
50
|
+
installed; a formula that mentions a predicate the structure does not
|
|
51
|
+
interpret; a node type the structural evaluator does not support) comes
|
|
52
|
+
back as ``{"error": {"type": ..., "message": ...}}``, never a traceback.
|
|
53
|
+
|
|
54
|
+
Chemical formulas MUST be written in TPTP
|
|
55
|
+
------------------------------------------
|
|
56
|
+
ChemLog's vocabulary is lower-case-led (``c``, ``bDOUBLE``, ...); in the
|
|
57
|
+
kit's own unicode surface syntax a lower-case leading letter is a VARIABLE or
|
|
58
|
+
FUNCTION, never a predicate, so a chemical class definition can only be
|
|
59
|
+
parsed as TPTP. ``dialect="tptp_bare"`` (the default on every tool that takes
|
|
60
|
+
a formula) therefore routes through
|
|
61
|
+
:func:`unicode_logic_kit.chem.parse_chemlog_tptp`, which additionally repairs
|
|
62
|
+
the three recurring LLM syntax failure modes (unbracketed
|
|
63
|
+
biimplication, an unquoted invalid predicate name, a free left-hand
|
|
64
|
+
variable — see :mod:`unicode_logic_kit.fol.tptp_repair`) BEFORE renaming the
|
|
65
|
+
chemical vocabulary back from the TPTP importer's forced-capitalised kit
|
|
66
|
+
spelling to ChemLog's own lower-case spelling — without that rename step no
|
|
67
|
+
TPTP-derived formula would ever line up with a
|
|
68
|
+
:func:`~unicode_logic_kit.chem.mol_to_structure` structure (see
|
|
69
|
+
``unicode_logic_kit.chem.interop``'s module docstring for exactly why). Passing
|
|
70
|
+
any other ``dialect`` is still honoured (useful for a formula generated
|
|
71
|
+
directly in the kit's own unicode syntax with kit-capitalised chemical
|
|
72
|
+
predicate names, e.g. ``C(x)`` for carbon) — it is parsed via
|
|
73
|
+
:func:`unicode_logic_kit.api.parse_any` and then the SAME chemical-vocabulary
|
|
74
|
+
rename is applied, so the resulting :class:`~unicode_logic_kit.fol.nodes.Node`
|
|
75
|
+
always ends up chemlog-spelled either way. It is NOT repaired in that branch
|
|
76
|
+
(only the ``tptp_bare`` path calls the repair layer — a formula in the kit's
|
|
77
|
+
own syntax is repaired by calling
|
|
78
|
+
:func:`unicode_logic_kit.fol.repair_formula` explicitly first, which is
|
|
79
|
+
deliberate: a rename that happened silently inside a CHECKING tool would
|
|
80
|
+
change which predicate was checked without the caller ever seeing it), and
|
|
81
|
+
auxiliary/class
|
|
82
|
+
predicates outside ChemLog's 40-symbol vocabulary (``carboxylicAcid``,
|
|
83
|
+
``organicMolecularEntity``, ...) are left exactly as parsed either way — they
|
|
84
|
+
have no ChemLog spelling to rename to.
|
|
85
|
+
|
|
86
|
+
Why ``all_different`` defaults to ``True`` here (unlike the underlying
|
|
87
|
+
evaluator)
|
|
88
|
+
------------------------------------------------------------------------------
|
|
89
|
+
:func:`unicode_logic_kit.semantics.model_eval.evaluate_detailed` defaults
|
|
90
|
+
``all_different=False`` (plain classical semantics: two separately
|
|
91
|
+
existentially-bound variables MAY denote the same individual). Every ChemLog
|
|
92
|
+
class definition this tool layer exists to check is written under the
|
|
93
|
+
OPPOSITE, ChemLog-specific convention — "separately introduced existential
|
|
94
|
+
variables are already pairwise distinct" — so every tool below that runs
|
|
95
|
+
model checking defaults ``all_different=True`` instead. Pass ``False``
|
|
96
|
+
explicitly to check a formula under plain FOL semantics instead.
|
|
97
|
+
|
|
98
|
+
Never a guessed False
|
|
99
|
+
-----------------------
|
|
100
|
+
Every model-checking tool exposes an optional ``budget`` (default ``None`` —
|
|
101
|
+
unbounded). If a budget is given and exhausted before a definitive answer,
|
|
102
|
+
``holds`` comes back ``None`` (never ``False``) — see
|
|
103
|
+
:mod:`unicode_logic_kit.semantics.model_eval`'s own "three-valued contract".
|
|
104
|
+
|
|
105
|
+
Spans (opt-in, ``with_spans=True`` on :func:`check_molecule`,
|
|
106
|
+
:func:`check_molecules`, :func:`explain_molecule_failure`)
|
|
107
|
+
------------------------------------------------------------------------------
|
|
108
|
+
``failing_conjunct`` names the piece of ``formula`` that made the check fail,
|
|
109
|
+
but only ever as rendered TEXT (``fc.to_unicode_str()``) — a caller has to
|
|
110
|
+
find that text again inside the ORIGINAL ``formula`` string by eye (or by a
|
|
111
|
+
substring search that can fail to be unique, or fail outright once
|
|
112
|
+
rendering has re-bracketed or re-spaced anything). ``with_spans=True`` adds
|
|
113
|
+
a ``"span"`` key beside ``failing_conjunct`` instead: ``{"start": int, "end":
|
|
114
|
+
int, "line": int, "column": int, "end_line": int, "end_column": int, "text":
|
|
115
|
+
str}`` (a JSON rendering of a :class:`~unicode_logic_kit.fol.spans.Span`) when
|
|
116
|
+
one was recovered, else ``None`` — a character range directly into the
|
|
117
|
+
``formula`` TEXT this call was given, exact and unambiguous, ready for a
|
|
118
|
+
caller to slice or highlight without re-deriving it.
|
|
119
|
+
|
|
120
|
+
This is built directly on
|
|
121
|
+
:meth:`unicode_logic_kit.fol.msflparser.MSFLParser.parse_with_spans` — a new,
|
|
122
|
+
additive method alongside ``.parse`` on the SAME instance (``.parse`` itself
|
|
123
|
+
is byte-for-byte unchanged) that returns a
|
|
124
|
+
:class:`~unicode_logic_kit.fol.spans.SpannedFormula`: ``.formula`` is the
|
|
125
|
+
identical AST ``.parse`` would build, ``.spans`` a
|
|
126
|
+
:class:`~unicode_logic_kit.fol.spans.SpanMap` from each of that AST's nodes
|
|
127
|
+
back to the slice of source text it was parsed from. ``SpanMap`` is keyed by
|
|
128
|
+
PATH (a tuple of child indices from the root — see
|
|
129
|
+
:mod:`unicode_logic_kit.fol.spans`'s module docstring), NOT by the node's own
|
|
130
|
+
value and NOT by Python ``id()``: every
|
|
131
|
+
:class:`~unicode_logic_kit.fol.nodes.Node` subclass is a frozen dataclass with
|
|
132
|
+
STRUCTURAL equality/hashing (see the kit-wide reason in ``_fol_nodes.py``),
|
|
133
|
+
so a value-keyed table would silently collapse two textually-distinct but
|
|
134
|
+
structurally-identical subformulas (``c(x) & c(x)``, two separate
|
|
135
|
+
occurrences of the same class predicate) onto a single span, and an
|
|
136
|
+
id()-keyed one goes stale the moment a node is rebuilt (which
|
|
137
|
+
``map_children``-based rewrites, including the rename below, always do).
|
|
138
|
+
This module uses the convenience ``.for_node(node)`` lookup — a node object
|
|
139
|
+
in hand, resolved by identity against whichever tree the map was last bound
|
|
140
|
+
to — rather than the PATH-keyed ``.get(path)`` a caller doing its own
|
|
141
|
+
traversal would use.
|
|
142
|
+
|
|
143
|
+
This module supplies the piece the span layer does not itself cover: every
|
|
144
|
+
``check_molecule``/``check_molecules``/``explain_molecule_failure`` call
|
|
145
|
+
renames the freshly parsed formula's chemical vocabulary to ChemLog spelling
|
|
146
|
+
before evaluating it (see "Chemical formulas MUST be written in TPTP"
|
|
147
|
+
above), and that rename reconstructs EVERY node in the tree — including
|
|
148
|
+
every ancestor of a renamed leaf, not just the renamed leaf itself, since
|
|
149
|
+
:meth:`~unicode_logic_kit.fol.nodes.Node.map_children` always allocates a new
|
|
150
|
+
object — so a fresh set of node OBJECTS exists after the rename even though
|
|
151
|
+
the tree's SHAPE (and hence every PATH into it) is unchanged
|
|
152
|
+
(:func:`~unicode_logic_kit.semantics.model_eval.evaluate_detailed` itself
|
|
153
|
+
never rebuilds a node, so identity survives evaluation, just not the rename
|
|
154
|
+
that happens first). :func:`unicode_logic_kit.chem.rename_with_spans` closes
|
|
155
|
+
exactly that gap, via :func:`unicode_logic_kit.fol.spans.project_spans` (the
|
|
156
|
+
span layer's own utility for carrying a ``SpanMap`` across a
|
|
157
|
+
shape-preserving rewrite — the rename only ever changes an ``Atom``'s
|
|
158
|
+
predicate or a ``Function``'s name, never the shape of the tree around it —
|
|
159
|
+
and re-binds the identity index to the renamed tree, so ``.for_node``
|
|
160
|
+
resolves against the SAME tree ``failing_conjunct`` is drawn from): a caller
|
|
161
|
+
sees a span on ``failing_conjunct`` whenever ``parse_with_spans`` recorded
|
|
162
|
+
one for the corresponding source node, all the way through the rename.
|
|
163
|
+
|
|
164
|
+
Only the dialects that route through ``MSFLParser`` support ``with_spans``
|
|
165
|
+
today — the kit's own surface syntax (``"unicode"``, or a specific mode name
|
|
166
|
+
such as ``"fol"``/``"msfol"``/...), NOT ``dialect="tptp_bare"`` (this
|
|
167
|
+
module's own default): TPTP is parsed by a wholly separate Lark grammar
|
|
168
|
+
(:mod:`unicode_logic_kit.fol.tptp_input`) that the span layer above does not
|
|
169
|
+
cover. This is not a theoretical restriction: the real external caller of
|
|
170
|
+
these tools (the ChEBI campaign) already calls every one of them with
|
|
171
|
+
``dialect="unicode"`` throughout its own pipeline (never ``"tptp_bare"``),
|
|
172
|
+
specifically so its `check_formula`/`diagnose`/`repair_formula` loop and its
|
|
173
|
+
model-checking calls agree on one dialect — ``with_spans=True`` lines up
|
|
174
|
+
with that exact usage, not a hypothetical one. Passing ``with_spans=True``
|
|
175
|
+
with ``dialect="tptp_bare"`` (or any other non-kit-syntax dialect) is
|
|
176
|
+
reported as ``{"error": {"type": "ValueError", ...}}`` — a caller/config
|
|
177
|
+
mismatch, not formula data being bad.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
from typing import Dict, List, Optional, Tuple
|
|
181
|
+
|
|
182
|
+
from .. import api, chem
|
|
183
|
+
from .. import simplify_for_checking, count_from_existential_chain
|
|
184
|
+
from ..fol.nodes import Node
|
|
185
|
+
from ..fol.tptp_repair import _render as _render_tptp
|
|
186
|
+
from ..semantics.model_eval import (
|
|
187
|
+
evaluate_detailed, EvalResult, UninterpretedSymbol, UnsupportedNode,
|
|
188
|
+
)
|
|
189
|
+
from ..semantics.structures import FiniteStructure
|
|
190
|
+
|
|
191
|
+
__all__ = [
|
|
192
|
+
"molecule_to_structure", "check_molecule", "check_molecules",
|
|
193
|
+
"explain_molecule_failure", "simplify_definition", "chemical_signature",
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Shared error shaping (see module docstring's "Conventions")
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
def _error(exc: Exception) -> dict:
|
|
202
|
+
"""A documented, non-argument exception as ``{"error": {...}}``.
|
|
203
|
+
|
|
204
|
+
:class:`~unicode_logic_kit.semantics.model_eval.UninterpretedSymbol`
|
|
205
|
+
additionally carries ``.symbol``/``.arity`` — surfaced here so a caller
|
|
206
|
+
can tell, without parsing the message, exactly which predicate/arity the
|
|
207
|
+
structure did not interpret (almost always a vocabulary typo, or a class
|
|
208
|
+
predicate from a definition this tool never saw).
|
|
209
|
+
"""
|
|
210
|
+
payload = {"error": {"type": type(exc).__name__, "message": str(exc)}}
|
|
211
|
+
symbol = getattr(exc, "symbol", None)
|
|
212
|
+
if symbol is not None:
|
|
213
|
+
payload["error"]["symbol"] = symbol
|
|
214
|
+
payload["error"]["arity"] = getattr(exc, "arity", None)
|
|
215
|
+
return payload
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _smiles_error(message: str, argument: str = "smiles") -> dict:
|
|
219
|
+
"""A SMILES that does not describe a valid molecule, in the SAME uniform
|
|
220
|
+
``ok=False``/``argument``/``errors`` shape as a formula parse failure —
|
|
221
|
+
see the module docstring's "Conventions" for why the two are unified."""
|
|
222
|
+
return {"ok": False, "argument": argument,
|
|
223
|
+
"errors": [{"dialect": "smiles", "message": message}],
|
|
224
|
+
"spec_topic": "chemistry"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Lowercased substring of a parse-error message -> the syntax_spec topic that
|
|
228
|
+
# explains it. A small, honest heuristic (mirrors
|
|
229
|
+
# unicode_logic_kit.mcp.server's own, kept independent rather than imported —
|
|
230
|
+
# this module owns no dependency on the general-purpose server's private
|
|
231
|
+
# helpers): it names where to LOOK, not a diagnosis, which is why the
|
|
232
|
+
# fallback is "chemistry" (the topic covering the TPTP-only, 0-ary-
|
|
233
|
+
# definition, lowercase-predicate conventions every chemical formula must
|
|
234
|
+
# follow) rather than a guess.
|
|
235
|
+
_SPEC_HINTS: Tuple[Tuple[str, str], ...] = (
|
|
236
|
+
("invalid name", "naming"),
|
|
237
|
+
("invalid variable", "naming"),
|
|
238
|
+
("invalid name/constant", "naming"),
|
|
239
|
+
("unexpected character", "naming"),
|
|
240
|
+
("not bound", "quantifiers"),
|
|
241
|
+
("free variable", "quantifiers"),
|
|
242
|
+
("incomplete formula", "operators"),
|
|
243
|
+
("unexpected token", "operators"),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _spec_topic_for(message: str) -> str:
|
|
248
|
+
low = message.lower()
|
|
249
|
+
for needle, topic in _SPEC_HINTS:
|
|
250
|
+
if needle in low:
|
|
251
|
+
return topic
|
|
252
|
+
return "chemistry"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _parse_chem(text: str, dialect: str, argument: str = "formula"
|
|
256
|
+
) -> Tuple[Optional[Node], Optional[dict]]:
|
|
257
|
+
"""Parse chemical class-definition TEXT into a ChemLog-spelled
|
|
258
|
+
:class:`~unicode_logic_kit.fol.nodes.Node` — ``(node, None)`` on success,
|
|
259
|
+
``(None, error_dict)`` on failure (see the module docstring's
|
|
260
|
+
"Conventions" for the error shape).
|
|
261
|
+
|
|
262
|
+
``dialect="tptp_bare"`` (see the module docstring's "Chemical formulas
|
|
263
|
+
MUST be written in TPTP") routes through
|
|
264
|
+
:func:`unicode_logic_kit.chem.parse_chemlog_tptp` (repair + rename, in one
|
|
265
|
+
call). Any other ``dialect`` is parsed via
|
|
266
|
+
:func:`unicode_logic_kit.api.parse_any` (no repair pass) and then renamed
|
|
267
|
+
the identical way via :func:`unicode_logic_kit.chem.to_chemlog_names`, so
|
|
268
|
+
the returned node is ChemLog-spelled either way.
|
|
269
|
+
"""
|
|
270
|
+
if dialect == "tptp_bare":
|
|
271
|
+
try:
|
|
272
|
+
node = chem.parse_chemlog_tptp(text)
|
|
273
|
+
except Exception as exc: # noqa: BLE001 — every TPTP parser raises
|
|
274
|
+
# its own exception type (TptpParsingError et al.), none of them
|
|
275
|
+
# a ValueError; catching broadly and reporting the message is
|
|
276
|
+
# exactly what unicode_logic_kit.api.parse_any's own _try() does
|
|
277
|
+
# for the identical reason.
|
|
278
|
+
message = str(exc)
|
|
279
|
+
return None, {"ok": False, "argument": argument,
|
|
280
|
+
"errors": [{"dialect": "tptp_bare",
|
|
281
|
+
"message": message}],
|
|
282
|
+
"spec_topic": _spec_topic_for(message)}
|
|
283
|
+
return node, None
|
|
284
|
+
|
|
285
|
+
parsed = api.parse_any(text, hint=dialect)
|
|
286
|
+
if not parsed.ok:
|
|
287
|
+
errors = parsed.to_dict()["errors"]
|
|
288
|
+
combined = " ".join(e.get("message", "") for e in errors)
|
|
289
|
+
return None, {"ok": False, "argument": argument, "errors": errors,
|
|
290
|
+
"spec_topic": _spec_topic_for(combined)}
|
|
291
|
+
return chem.to_chemlog_names(parsed.formula), None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Spans (opt-in) -- see the module docstring's "Spans" section for the full
|
|
296
|
+
# contract this builds on (unicode_logic_kit.fol.msflparser.MSFLParser
|
|
297
|
+
# .parse_with_spans / unicode_logic_kit.fol.spans).
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
#: Dialects with_spans=True actually supports: the kit's own MSFLParser-
|
|
301
|
+
#: backed surface syntax -- api._UNICODE_HINTS's keys (each a single fixed
|
|
302
|
+
#: mode) plus "unicode" itself (the ladder that tries them in the SAME
|
|
303
|
+
#: order api._unicode_ladder does). Deliberately reuses api's own table
|
|
304
|
+
#: rather than restating the mode list here, the same way fol.dialect_repair
|
|
305
|
+
#: reuses fol.tptp_repair's fixes instead of reimplementing them -- two
|
|
306
|
+
#: independently-maintained copies of "which modes, which order" could
|
|
307
|
+
#: silently drift, and with_spans=True must pick the identical winning mode
|
|
308
|
+
#: plain parsing would, or the span-carrying node would not even be the SAME
|
|
309
|
+
#: formula. NOT "tptp_bare" (this module's own default): TPTP is parsed by
|
|
310
|
+
#: a wholly separate Lark grammar (fol.tptp_input) with no span tracking.
|
|
311
|
+
_SPAN_DIALECTS = frozenset(api._UNICODE_HINTS) | {"unicode"}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _parse_chem_with_spans(text: str, dialect: str, argument: str = "formula"
|
|
315
|
+
) -> Tuple[Optional[Node], Optional[chem.SpanMap],
|
|
316
|
+
Optional[dict]]:
|
|
317
|
+
""":func:`_parse_chem`, plus a :class:`unicode_logic_kit.chem.SpanMap` for
|
|
318
|
+
the returned node — ``(node, spans, None)`` on success.
|
|
319
|
+
|
|
320
|
+
``(None, None, error_dict)`` on failure, where ``error_dict`` is one of
|
|
321
|
+
two shapes (see the module docstring's "Conventions" and "Spans"):
|
|
322
|
+
|
|
323
|
+
* ``{"error": {"type": "ValueError", ...}}`` — ``dialect`` is not in
|
|
324
|
+
:data:`_SPAN_DIALECTS` (a caller/config mistake: ``with_spans=True``
|
|
325
|
+
together with, most commonly, this module's own ``dialect="tptp_bare"``
|
|
326
|
+
default — not formula DATA being bad);
|
|
327
|
+
* the uniform ``{"ok": False, "argument": ..., "errors": [...],
|
|
328
|
+
"spec_topic": ...}`` shape for a genuine syntax error in ``text`` — the
|
|
329
|
+
SAME shape :func:`_parse_chem` reports for the identical input, so a
|
|
330
|
+
caller's ``result.get("ok") is False`` check behaves identically
|
|
331
|
+
whether or not it asked for spans.
|
|
332
|
+
"""
|
|
333
|
+
if dialect not in _SPAN_DIALECTS:
|
|
334
|
+
return None, None, _error(ValueError(
|
|
335
|
+
f"chem_tools: with_spans=True is not supported for dialect={dialect!r} "
|
|
336
|
+
f"yet — only the kit's own surface syntax ({sorted(_SPAN_DIALECTS)}) "
|
|
337
|
+
"has a span-tracking parser (not this module's own default, "
|
|
338
|
+
"dialect='tptp_bare'). Call again without with_spans, or with one "
|
|
339
|
+
"of the dialects above."))
|
|
340
|
+
|
|
341
|
+
from ..fol.msflparser import MSFLParser
|
|
342
|
+
|
|
343
|
+
ladder = (api._UNICODE_MODES if dialect == "unicode"
|
|
344
|
+
else ((dialect, api._UNICODE_HINTS[dialect]),))
|
|
345
|
+
spanned = None
|
|
346
|
+
message = "no unicode-surface mode accepted the text"
|
|
347
|
+
for _name, kwargs in ladder:
|
|
348
|
+
try:
|
|
349
|
+
spanned = MSFLParser(**kwargs).parse_with_spans(text)
|
|
350
|
+
break
|
|
351
|
+
except Exception as exc: # noqa: BLE001 — mirrors api._try's own catch-all
|
|
352
|
+
message = str(exc)
|
|
353
|
+
if spanned is None:
|
|
354
|
+
return None, None, {"ok": False, "argument": argument,
|
|
355
|
+
"errors": [{"dialect": dialect, "message": message}],
|
|
356
|
+
"spec_topic": _spec_topic_for(message)}
|
|
357
|
+
node, spans = chem.to_chemlog_names_with_spans(spanned.formula, spanned.spans)
|
|
358
|
+
return node, spans, None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _span_dict(span) -> Optional[dict]:
|
|
362
|
+
"""JSON rendering of a :class:`unicode_logic_kit.fol.spans.Span`, or
|
|
363
|
+
``None`` for :data:`~unicode_logic_kit.fol.spans.UNKNOWN` — a node the span
|
|
364
|
+
layer could not recover an exact source range for (see that module's
|
|
365
|
+
docstring for the two documented, narrow cases) is reported as ``None``,
|
|
366
|
+
never a guessed range."""
|
|
367
|
+
from ..fol.spans import UNKNOWN
|
|
368
|
+
|
|
369
|
+
if span is UNKNOWN:
|
|
370
|
+
return None
|
|
371
|
+
return {"start": span.start, "end": span.end, "line": span.line,
|
|
372
|
+
"column": span.column, "end_line": span.end_line,
|
|
373
|
+
"end_column": span.end_column, "text": span.text}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Structure -> readable summary helpers (shared by molecule_to_structure /
|
|
378
|
+
# explain_molecule_failure)
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
#: The atom-type letters: ChemLog's own six, then the halogens the kit adds
|
|
382
|
+
#: (see chem._naming.ELEMENT_LETTERS — not imported: that module is
|
|
383
|
+
#: documented PRIVATE, and this literal tuple is the exact list
|
|
384
|
+
#: unicode_logic_kit.mcp.syntax_spec's own "chemistry" topic already
|
|
385
|
+
#: publishes, so duplicating it here does not risk drifting from an
|
|
386
|
+
#: undocumented internal any more than that existing publication already
|
|
387
|
+
#: does). Kept in sync by tests/test_chem.py's vocabulary test.
|
|
388
|
+
_ATOM_LETTERS: Tuple[str, ...] = (
|
|
389
|
+
"c", "n", "o", "s", "p", "h", "f", "cl", "br", "i", "at")
|
|
390
|
+
|
|
391
|
+
_BOND_KINDS: Tuple[str, ...] = ("bSINGLE", "bDOUBLE", "bTRIPLE", "bAROMATIC")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _atoms_by_type(structure: FiniteStructure) -> Dict[str, List[str]]:
|
|
395
|
+
"""Domain individuals grouped by ChemLog atom-type letter — only the
|
|
396
|
+
letters this structure actually interprets (always all six for a
|
|
397
|
+
structure ``mol_to_structure`` built, per its own "always-populated
|
|
398
|
+
predicates" contract; guarded here anyway so this helper stays correct
|
|
399
|
+
for a hand-built or ``naming="paper"`` structure too, where it silently
|
|
400
|
+
returns fewer groups instead of raising)."""
|
|
401
|
+
return {letter: list(structure.individuals_with(letter))
|
|
402
|
+
for letter in _ATOM_LETTERS if structure.interprets(letter, 1)}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _bonds(structure: FiniteStructure) -> List[dict]:
|
|
406
|
+
"""Every stored bond, ONE entry per unordered pair (bond relations are
|
|
407
|
+
stored symmetrically — see chem.mol's module docstring — so naively
|
|
408
|
+
listing both directions would double-count every bond)."""
|
|
409
|
+
seen = set()
|
|
410
|
+
out: List[dict] = []
|
|
411
|
+
for kind in _BOND_KINDS:
|
|
412
|
+
if not structure.interprets(kind, 2):
|
|
413
|
+
continue
|
|
414
|
+
for a in structure.domain:
|
|
415
|
+
for b in structure.neighbors(kind, a):
|
|
416
|
+
pair = frozenset((a, b))
|
|
417
|
+
key = (kind, pair)
|
|
418
|
+
if key in seen:
|
|
419
|
+
continue
|
|
420
|
+
seen.add(key)
|
|
421
|
+
out.append({"kind": kind, "atoms": sorted(pair)})
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _atom_notes(structure: FiniteStructure, individual: str) -> List[str]:
|
|
426
|
+
"""Every OTHER unary fact about ``individual`` (hydrogen count, charge,
|
|
427
|
+
chirality, and — when ``computed=True`` — ring/aromaticity/connectivity),
|
|
428
|
+
excluding the bare element letter and ``atom`` (already carried by
|
|
429
|
+
:func:`_atoms_by_type`). Read directly off ``structure.signature()``
|
|
430
|
+
rather than a hardcoded predicate list, so it stays correct regardless of
|
|
431
|
+
naming scheme, ``computed=`` choice, or a future vocabulary extension —
|
|
432
|
+
the one thing this helper must never do is silently miss a fact a wider
|
|
433
|
+
hardcoded list happened not to anticipate."""
|
|
434
|
+
skip = set(_ATOM_LETTERS) | {"atom"}
|
|
435
|
+
held = [name for name, arity in structure.signature()
|
|
436
|
+
if arity == 1 and name not in skip
|
|
437
|
+
and structure.holds(name, (individual,))]
|
|
438
|
+
return sorted(held)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _eval_payload(result: EvalResult,
|
|
442
|
+
spans: Optional[chem.SpanMap] = None) -> dict:
|
|
443
|
+
"""The holds/exhausted/steps/witness/failing_conjunct bookkeeping shared
|
|
444
|
+
by :func:`check_molecule`, :func:`check_molecules` and
|
|
445
|
+
:func:`explain_molecule_failure` — factored out so the three tools report
|
|
446
|
+
the identical shape for the identical :class:`EvalResult` rather than
|
|
447
|
+
three independently rewritten (and driftable) renderings.
|
|
448
|
+
|
|
449
|
+
``witness``/``failing_conjunct`` are populated only when ``holds`` is
|
|
450
|
+
decisively ``True``/``False`` respectively (never both, never either when
|
|
451
|
+
``holds`` is ``None`` — the budget ran out) — see
|
|
452
|
+
:mod:`unicode_logic_kit.semantics.model_eval`'s own docstring for exactly
|
|
453
|
+
what shape of formula each can explain.
|
|
454
|
+
|
|
455
|
+
``spans``, when given (the caller asked for ``with_spans=True`` and
|
|
456
|
+
parsing succeeded with a span table — see the module docstring's
|
|
457
|
+
"Spans" section), adds a ``"span"`` key beside ``failing_conjunct``: a
|
|
458
|
+
JSON rendering of its EXTENT :class:`~unicode_logic_kit.fol.spans.Span`
|
|
459
|
+
when this exact node has one, else ``None`` (:data:`~unicode_logic_kit.fol.spans.UNKNOWN`
|
|
460
|
+
— never guessed). ``spans is None`` (the default — every caller that
|
|
461
|
+
never passes ``with_spans=True``) omits the key entirely, so the
|
|
462
|
+
returned shape is byte-identical to before this parameter existed.
|
|
463
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate_detailed` never
|
|
464
|
+
rebuilds any node (see that module's ``_find_blame``), so
|
|
465
|
+
``failing_conjunct`` is always a literal object out of the tree
|
|
466
|
+
``spans`` was built over —
|
|
467
|
+
:meth:`~unicode_logic_kit.fol.spans.SpanMap.for_node` finds it directly, by
|
|
468
|
+
identity, no re-derivation (``spans`` is already bound to that tree —
|
|
469
|
+
see :func:`_parse_chem_with_spans`, which routes it through
|
|
470
|
+
:func:`unicode_logic_kit.chem.to_chemlog_names_with_spans`).
|
|
471
|
+
"""
|
|
472
|
+
payload: dict = {"holds": result.holds, "exhausted": result.exhausted,
|
|
473
|
+
"steps": result.steps}
|
|
474
|
+
if result.holds is True:
|
|
475
|
+
payload["witness"] = result.witness
|
|
476
|
+
elif result.holds is False:
|
|
477
|
+
fc = result.failing_conjunct
|
|
478
|
+
entry = ({"unicode": fc.to_unicode_str(), "formula": fc.to_dict()}
|
|
479
|
+
if fc is not None else None)
|
|
480
|
+
if spans is not None and entry is not None:
|
|
481
|
+
entry["span"] = _span_dict(spans.for_node(fc).extent)
|
|
482
|
+
payload["failing_conjunct"] = entry
|
|
483
|
+
return payload
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _build_structure(smiles: str, include_computed: bool
|
|
487
|
+
) -> Tuple[Optional[FiniteStructure], Optional[dict]]:
|
|
488
|
+
""":func:`unicode_logic_kit.chem.mol_to_structure` in the fixed
|
|
489
|
+
``naming="chemlog"`` scheme every model-checking tool below needs (a
|
|
490
|
+
formula parsed via :func:`_parse_chem` is ALWAYS ChemLog-spelled, so the
|
|
491
|
+
structure it is checked against must be too — exposing ``naming`` here
|
|
492
|
+
would let a caller silently break that pairing, which is why it is not a
|
|
493
|
+
parameter on any tool below except :func:`molecule_to_structure` itself,
|
|
494
|
+
whose whole point is inspecting the structure directly).
|
|
495
|
+
|
|
496
|
+
``(structure, None)`` on success; ``(None, error_dict)`` on failure — an
|
|
497
|
+
``ImportError`` (RDKit missing) is the ``{"error": ...}`` shape (an
|
|
498
|
+
environment problem, not a bad argument); any other exception
|
|
499
|
+
(``mol_to_structure``'s documented ``ValueError`` — unparseable SMILES,
|
|
500
|
+
an unsupported element/bond, a residual hydrogen atom) is the uniform
|
|
501
|
+
``argument="smiles"`` shape (see the module docstring's "Conventions").
|
|
502
|
+
|
|
503
|
+
A ``smiles`` that is not even a ``str`` (an ``int``, ``None``, a list, …
|
|
504
|
+
— plausible in a batch call like :func:`check_molecules`, where one
|
|
505
|
+
caller-side mistake in a long list should not be worse than any other
|
|
506
|
+
per-item error) is caught HERE, before ever reaching
|
|
507
|
+
``mol_to_structure``, and reported in that SAME uniform
|
|
508
|
+
``argument="smiles"`` shape rather than as a raw, undocumented
|
|
509
|
+
:class:`TypeError` — ``mol_to_structure`` itself raises exactly that
|
|
510
|
+
:class:`TypeError` for a non-``str``/non-``Mol`` argument (see its own
|
|
511
|
+
docstring), which is correct for a direct caller but WRONG for this
|
|
512
|
+
tool layer's documented contract (see the module docstring's
|
|
513
|
+
"Conventions": every bad argument is ``ok=False``, never a traceback) —
|
|
514
|
+
left uncaught it would have propagated straight out of
|
|
515
|
+
:func:`check_molecules`, aborting every remaining molecule in the batch
|
|
516
|
+
over a single bad list entry.
|
|
517
|
+
"""
|
|
518
|
+
if not isinstance(smiles, str):
|
|
519
|
+
return None, _smiles_error(
|
|
520
|
+
f"smiles must be a str, got {type(smiles).__name__}")
|
|
521
|
+
try:
|
|
522
|
+
return chem.mol_to_structure(smiles, naming="chemlog",
|
|
523
|
+
computed=include_computed), None
|
|
524
|
+
except ImportError as exc:
|
|
525
|
+
return None, _error(exc)
|
|
526
|
+
except ValueError as exc:
|
|
527
|
+
return None, _smiles_error(str(exc))
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
# 1. molecule_to_structure
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
def molecule_to_structure(smiles: str, *, naming: str = "chemlog",
|
|
535
|
+
include_computed: bool = True) -> dict:
|
|
536
|
+
"""Translate a SMILES string into the finite structure a definition is
|
|
537
|
+
checked against — lets the caller SEE what it is generating formulas for.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
smiles: a SMILES string (e.g. ``"CCO"`` for ethanol).
|
|
541
|
+
naming: ``"chemlog"`` (default — ``c``, ``bSINGLE``, ``has_1_hs``,
|
|
542
|
+
...; what :func:`check_molecule` and friends require) or
|
|
543
|
+
``"paper"`` (the human-readable prose spelling — ``c``,
|
|
544
|
+
``singleBond``, ``has1H``, ...; use this only to reproduce a
|
|
545
|
+
worked example written that way, never to feed a
|
|
546
|
+
``dialect="tptp_bare"`` formula, which always comes back
|
|
547
|
+
ChemLog-spelled).
|
|
548
|
+
include_computed: attach the five ring/aromaticity/connectivity
|
|
549
|
+
predicates that need transitive closure (not first-order
|
|
550
|
+
expressible over the stored facts — see
|
|
551
|
+
:mod:`unicode_logic_kit.chem.mol`'s module docstring). ``False``
|
|
552
|
+
gives a structure whose entire vocabulary IS first-order
|
|
553
|
+
expressible over what is stored.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
On success: ``{"ok": True, "structure": <FiniteStructure.to_dict()>,
|
|
557
|
+
"domain_size": <int>, "nonempty_predicates": [...],
|
|
558
|
+
"computed_predicates": [...]}`` — ``nonempty_predicates`` (each
|
|
559
|
+
``"name/arity"``, matching ``structure``'s own key format) is the
|
|
560
|
+
quick answer to "what does this molecule actually have", without
|
|
561
|
+
scanning the full (mostly-empty, canonically-pre-populated —
|
|
562
|
+
see ``mol_to_structure``'s docstring) extension table by hand.
|
|
563
|
+
|
|
564
|
+
``naming`` outside ``{"chemlog", "paper"}`` is a caller/config
|
|
565
|
+
mistake, not molecule data — reported as
|
|
566
|
+
``{"error": {"type": "ValueError", ...}}``. An unparseable/invalid
|
|
567
|
+
SMILES is reported as ``{"ok": False, "argument": "smiles", ...}``
|
|
568
|
+
(see the module docstring's "Conventions"); RDKit not being
|
|
569
|
+
installed is reported as ``{"error": {"type": "ImportError", ...}}``
|
|
570
|
+
naming the install command.
|
|
571
|
+
"""
|
|
572
|
+
if naming not in ("chemlog", "paper"):
|
|
573
|
+
return _error(ValueError(
|
|
574
|
+
f"molecule_to_structure: unknown naming={naming!r}, expected "
|
|
575
|
+
"'chemlog' or 'paper'"))
|
|
576
|
+
if not isinstance(smiles, str):
|
|
577
|
+
# Same contract as _build_structure below (see its own docstring):
|
|
578
|
+
# a non-str smiles is a bad ARGUMENT, not molecule data, and must
|
|
579
|
+
# come back in the uniform ok=False/argument shape rather than as a
|
|
580
|
+
# raw, undocumented TypeError leaking out of mol_to_structure.
|
|
581
|
+
return _smiles_error(
|
|
582
|
+
f"smiles must be a str, got {type(smiles).__name__}")
|
|
583
|
+
try:
|
|
584
|
+
structure = chem.mol_to_structure(smiles, naming=naming,
|
|
585
|
+
computed=include_computed)
|
|
586
|
+
except ImportError as exc:
|
|
587
|
+
return _error(exc)
|
|
588
|
+
except ValueError as exc:
|
|
589
|
+
return _smiles_error(str(exc))
|
|
590
|
+
|
|
591
|
+
payload = structure.to_dict()
|
|
592
|
+
nonempty = sorted(label for label, rows in payload["extensions"].items()
|
|
593
|
+
if rows)
|
|
594
|
+
return {
|
|
595
|
+
"ok": True,
|
|
596
|
+
"structure": payload,
|
|
597
|
+
"domain_size": len(payload["domain"]),
|
|
598
|
+
"nonempty_predicates": nonempty,
|
|
599
|
+
"computed_predicates": payload["computed"],
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# ---------------------------------------------------------------------------
|
|
604
|
+
# 2. check_molecule
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
def check_molecule(formula: str, smiles: str, *, all_different: bool = True,
|
|
608
|
+
dialect: str = "tptp_bare", include_computed: bool = True,
|
|
609
|
+
budget: Optional[int] = None,
|
|
610
|
+
with_spans: bool = False) -> dict:
|
|
611
|
+
"""Does ``formula`` hold of the molecule ``smiles`` describes?
|
|
612
|
+
|
|
613
|
+
Parses ``formula`` (see the module docstring's "Chemical formulas MUST
|
|
614
|
+
be written in TPTP"), builds the molecule's structure, and model-checks
|
|
615
|
+
directly (:func:`unicode_logic_kit.semantics.model_eval.evaluate_detailed`
|
|
616
|
+
— no CNF/prenex normal form, so a negated existential or a top-level
|
|
617
|
+
disjunction never blows up the way it does for a normal-form-requiring
|
|
618
|
+
checker; see that module's own docstring).
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
formula: a class-definition formula, TEXT (see ``dialect``).
|
|
622
|
+
smiles: the molecule to check it against.
|
|
623
|
+
all_different: the ChemLog convention that separately ∃-bound
|
|
624
|
+
variables are pairwise distinct — see the module docstring for
|
|
625
|
+
why this defaults ``True`` here (opposite of the underlying
|
|
626
|
+
evaluator's own default).
|
|
627
|
+
dialect: ``"tptp_bare"`` (default) or any other
|
|
628
|
+
:func:`unicode_logic_kit.api.parse_any` hint.
|
|
629
|
+
include_computed: attach the ring/aromaticity/connectivity computed
|
|
630
|
+
predicates (see :func:`molecule_to_structure`).
|
|
631
|
+
budget: cap the evaluator's step count. ``None`` (default) is
|
|
632
|
+
unbounded. If given and exhausted, ``holds`` comes back ``None``
|
|
633
|
+
— an honest UNKNOWN, never a guessed ``False``.
|
|
634
|
+
with_spans: add a ``"span"`` byte-offset range beside
|
|
635
|
+
``failing_conjunct`` — see the module docstring's "Spans"
|
|
636
|
+
section for the shape and its current dialect restriction.
|
|
637
|
+
Default ``False``: byte-identical to every call made before this
|
|
638
|
+
parameter existed.
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
On success: ``{"ok": True, "holds": bool|None, "exhausted": bool,
|
|
642
|
+
"steps": int, "witness": {...}}`` (only when ``holds is True`` — a
|
|
643
|
+
variable->individual binding that makes the definition true) or
|
|
644
|
+
``..., "failing_conjunct": {"unicode": ..., "formula": ...}}`` (only
|
|
645
|
+
when ``holds is False`` — the specific conjunct
|
|
646
|
+
:func:`evaluate_detailed`'s blame trail drilled down to; ``None`` if
|
|
647
|
+
the formula's shape has no natural single-conjunct blame — see that
|
|
648
|
+
function's own docstring for exactly which shapes it can explain).
|
|
649
|
+
With ``with_spans=True``, ``failing_conjunct`` additionally carries
|
|
650
|
+
a ``"span"`` key — see the module docstring's "Spans" section for
|
|
651
|
+
its shape.
|
|
652
|
+
|
|
653
|
+
A parse failure or invalid SMILES is the uniform ``ok=False``
|
|
654
|
+
argument/errors shape; a formula mentioning a predicate the
|
|
655
|
+
structure does not interpret
|
|
656
|
+
(:class:`~unicode_logic_kit.semantics.model_eval.UninterpretedSymbol`
|
|
657
|
+
— almost always a vocabulary typo, or a class predicate this call
|
|
658
|
+
never defined), an unsupported node type
|
|
659
|
+
(:class:`~unicode_logic_kit.semantics.model_eval.UnsupportedNode`), a
|
|
660
|
+
free variable, or (``with_spans=True`` only) an unsupported
|
|
661
|
+
``dialect``/unavailable span layer are the ``{"error": {...}}``
|
|
662
|
+
shape.
|
|
663
|
+
"""
|
|
664
|
+
spans = None
|
|
665
|
+
if with_spans:
|
|
666
|
+
node, spans, err = _parse_chem_with_spans(formula, dialect, argument="formula")
|
|
667
|
+
else:
|
|
668
|
+
node, err = _parse_chem(formula, dialect, argument="formula")
|
|
669
|
+
if err is not None:
|
|
670
|
+
return err
|
|
671
|
+
structure, err = _build_structure(smiles, include_computed)
|
|
672
|
+
if err is not None:
|
|
673
|
+
return err
|
|
674
|
+
try:
|
|
675
|
+
result = evaluate_detailed(node, structure,
|
|
676
|
+
all_different=all_different, budget=budget)
|
|
677
|
+
except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
|
|
678
|
+
return _error(exc)
|
|
679
|
+
return {"ok": True, **_eval_payload(result, spans)}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
# ---------------------------------------------------------------------------
|
|
683
|
+
# 3. check_molecules
|
|
684
|
+
# ---------------------------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
def check_molecules(formula: str, smiles_list: List[str], *,
|
|
687
|
+
all_different: bool = True, dialect: str = "tptp_bare",
|
|
688
|
+
include_computed: bool = True,
|
|
689
|
+
budget: Optional[int] = None,
|
|
690
|
+
with_spans: bool = False) -> dict:
|
|
691
|
+
""":func:`check_molecule`, batched over a corpus — the core feedback
|
|
692
|
+
loop: one call shows which positive examples a definition actually
|
|
693
|
+
covers, without a round trip per molecule.
|
|
694
|
+
|
|
695
|
+
``formula`` is parsed ONCE; a parse failure aborts the whole call (the
|
|
696
|
+
uniform ``ok=False`` shape, ``argument="formula"``). RDKit missing
|
|
697
|
+
likewise aborts the whole call (an environment problem, not a per-
|
|
698
|
+
molecule one). Everything else is per-molecule: one bad SMILES in the
|
|
699
|
+
list does not lose the results for the rest — it gets its own
|
|
700
|
+
``{"smiles": ..., "ok": False, "error": ...}`` entry and the batch
|
|
701
|
+
continues, because seeing WHICH examples are unusable is itself part of
|
|
702
|
+
the feedback a correction loop needs.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
formula: as :func:`check_molecule`.
|
|
706
|
+
smiles_list: the molecules to check it against, in order.
|
|
707
|
+
all_different, dialect, include_computed, budget, with_spans: as
|
|
708
|
+
:func:`check_molecule`.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
``{"ok": True, "results": [...], "summary": {"total": int,
|
|
712
|
+
"holds": int, "fails": int, "unknown": int, "errors": int}}``.
|
|
713
|
+
Each ``results[i]`` is ``{"smiles": smiles_list[i], "ok": True,
|
|
714
|
+
**check_molecule-style holds/exhausted/steps/witness/
|
|
715
|
+
failing_conjunct}`` on success, or ``{"smiles": ..., "ok": False,
|
|
716
|
+
"error": <message>}`` on a per-molecule failure (bad SMILES, or the
|
|
717
|
+
same ``UninterpretedSymbol``/``UnsupportedNode``/free-variable
|
|
718
|
+
exceptions :func:`check_molecule` reports as ``{"error": ...}`` —
|
|
719
|
+
flattened to a plain message here since these entries already live
|
|
720
|
+
one level below the top-level ``ok``).
|
|
721
|
+
"""
|
|
722
|
+
spans = None
|
|
723
|
+
if with_spans:
|
|
724
|
+
node, spans, err = _parse_chem_with_spans(formula, dialect, argument="formula")
|
|
725
|
+
else:
|
|
726
|
+
node, err = _parse_chem(formula, dialect, argument="formula")
|
|
727
|
+
if err is not None:
|
|
728
|
+
return err
|
|
729
|
+
|
|
730
|
+
results: List[dict] = []
|
|
731
|
+
counts = {"holds": 0, "fails": 0, "unknown": 0, "errors": 0}
|
|
732
|
+
for smiles in (smiles_list or []):
|
|
733
|
+
structure, build_err = _build_structure(smiles, include_computed)
|
|
734
|
+
if build_err is not None:
|
|
735
|
+
if "error" in build_err and build_err["error"]["type"] == "ImportError":
|
|
736
|
+
return build_err # environment-wide: abort the whole batch
|
|
737
|
+
message = build_err["errors"][0]["message"]
|
|
738
|
+
results.append({"smiles": smiles, "ok": False, "error": message})
|
|
739
|
+
counts["errors"] += 1
|
|
740
|
+
continue
|
|
741
|
+
try:
|
|
742
|
+
result = evaluate_detailed(node, structure,
|
|
743
|
+
all_different=all_different,
|
|
744
|
+
budget=budget)
|
|
745
|
+
except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
|
|
746
|
+
results.append({"smiles": smiles, "ok": False, "error": str(exc)})
|
|
747
|
+
counts["errors"] += 1
|
|
748
|
+
continue
|
|
749
|
+
entry = {"smiles": smiles, "ok": True, **_eval_payload(result, spans)}
|
|
750
|
+
results.append(entry)
|
|
751
|
+
if result.holds is True:
|
|
752
|
+
counts["holds"] += 1
|
|
753
|
+
elif result.holds is False:
|
|
754
|
+
counts["fails"] += 1
|
|
755
|
+
else:
|
|
756
|
+
counts["unknown"] += 1
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
"ok": True,
|
|
760
|
+
"results": results,
|
|
761
|
+
"summary": {"total": len(results), **counts},
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
# ---------------------------------------------------------------------------
|
|
766
|
+
# 4. explain_molecule_failure
|
|
767
|
+
# ---------------------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
def explain_molecule_failure(formula: str, smiles: str, *,
|
|
770
|
+
all_different: bool = True,
|
|
771
|
+
dialect: str = "tptp_bare",
|
|
772
|
+
include_computed: bool = True,
|
|
773
|
+
budget: Optional[int] = None,
|
|
774
|
+
with_spans: bool = False) -> dict:
|
|
775
|
+
"""The full counterexample-explanation view for ONE molecule — the piece
|
|
776
|
+
a bare classifier does not give you (a plain model checker returns only
|
|
777
|
+
proved/not-proved, never a witness).
|
|
778
|
+
|
|
779
|
+
Runs the identical check :func:`check_molecule` does (same holds/
|
|
780
|
+
exhausted/steps/witness/failing_conjunct), plus the molecule's own
|
|
781
|
+
structure rendered for a human/LLM to read at a glance: every atom
|
|
782
|
+
grouped by element type, every other fact about each atom (hydrogen
|
|
783
|
+
count, charge, chirality, and — when ``include_computed=True`` —
|
|
784
|
+
ring/aromaticity/connectivity), and the full bond list. Reading the
|
|
785
|
+
failing conjunct against this is what lets a caller propose a concrete
|
|
786
|
+
fix (e.g. "the definition asks for a nitrogen but this molecule has
|
|
787
|
+
none") instead of just re-generating blind.
|
|
788
|
+
|
|
789
|
+
Args: as :func:`check_molecule` (including ``with_spans``).
|
|
790
|
+
|
|
791
|
+
Returns:
|
|
792
|
+
On success: ``{"ok": True, "formula_unicode": str, "domain": [...],
|
|
793
|
+
"domain_size": int, "atoms_by_type": {letter: [individuals]},
|
|
794
|
+
"atom_properties": {individual: [other unary facts]}, "bonds":
|
|
795
|
+
[{"kind": ..., "atoms": [a, b]}], **check_molecule-style holds/
|
|
796
|
+
exhausted/steps/witness/failing_conjunct}``. Same failure shapes as
|
|
797
|
+
:func:`check_molecule` for a bad formula/SMILES/vocabulary mismatch.
|
|
798
|
+
"""
|
|
799
|
+
spans = None
|
|
800
|
+
if with_spans:
|
|
801
|
+
node, spans, err = _parse_chem_with_spans(formula, dialect, argument="formula")
|
|
802
|
+
else:
|
|
803
|
+
node, err = _parse_chem(formula, dialect, argument="formula")
|
|
804
|
+
if err is not None:
|
|
805
|
+
return err
|
|
806
|
+
structure, err = _build_structure(smiles, include_computed)
|
|
807
|
+
if err is not None:
|
|
808
|
+
return err
|
|
809
|
+
try:
|
|
810
|
+
result = evaluate_detailed(node, structure,
|
|
811
|
+
all_different=all_different, budget=budget)
|
|
812
|
+
except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
|
|
813
|
+
return _error(exc)
|
|
814
|
+
|
|
815
|
+
return {
|
|
816
|
+
"ok": True,
|
|
817
|
+
"formula_unicode": node.to_unicode_str(),
|
|
818
|
+
"domain": list(structure.domain),
|
|
819
|
+
"domain_size": len(structure.domain),
|
|
820
|
+
"atoms_by_type": _atoms_by_type(structure),
|
|
821
|
+
"atom_properties": {ind: _atom_notes(structure, ind)
|
|
822
|
+
for ind in structure.domain},
|
|
823
|
+
"bonds": _bonds(structure),
|
|
824
|
+
**_eval_payload(result, spans),
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
# ---------------------------------------------------------------------------
|
|
829
|
+
# 5. simplify_definition
|
|
830
|
+
# ---------------------------------------------------------------------------
|
|
831
|
+
|
|
832
|
+
def simplify_definition(formula: str, *, all_different: bool = True,
|
|
833
|
+
dialect: str = "tptp_bare") -> dict:
|
|
834
|
+
"""Shrink a class definition for model checking, and report exactly what
|
|
835
|
+
changed — the fix for the standard timeout cause (a redundant O(n²)
|
|
836
|
+
pairwise-inequality chain, e.g. 780 literals for "at least 40
|
|
837
|
+
carbons").
|
|
838
|
+
|
|
839
|
+
Two independent, differently-scoped rewrites are applied (see
|
|
840
|
+
:mod:`unicode_logic_kit.fol.simplify_check`'s own module docstring for the
|
|
841
|
+
full soundness argument of each):
|
|
842
|
+
|
|
843
|
+
1. :func:`~unicode_logic_kit.fol.simplify_check.simplify_for_checking`
|
|
844
|
+
(``all_different=True`` by default here — see the module docstring)
|
|
845
|
+
— a conservative, EVERYWHERE-applicable pass: drops trivial ``t=t``
|
|
846
|
+
conjuncts, duplicate ∧/∨ operands, double negation, vacuous
|
|
847
|
+
quantifiers, and (under ``all_different``) a pairwise ``x≠y`` between
|
|
848
|
+
two separately ∃-bound variables, wherever in the tree it occurs —
|
|
849
|
+
even when interleaved with other content (a real bond literal between
|
|
850
|
+
the same two variables, say). This is what actually shrinks a real
|
|
851
|
+
definition, and is always applied.
|
|
852
|
+
2. :func:`~unicode_logic_kit.fol.simplify_check.count_from_existential_chain`
|
|
853
|
+
— checked against the ORIGINAL (pre-simplification) formula, because
|
|
854
|
+
it recognises only a COMPLETE, EXACT top-level instance of the
|
|
855
|
+
distinct-witnesses pattern (every witness, every C(n,2) inequality,
|
|
856
|
+
nothing else — see that function's own docstring) and the ``≠``
|
|
857
|
+
literals rule (1) already removed would make an already-simplified
|
|
858
|
+
formula fail that exact-shape test even when the original genuinely
|
|
859
|
+
matched. Reported as ``count_pattern`` — separate, informational
|
|
860
|
+
``diagnostic`` output, not folded into the main simplified formula
|
|
861
|
+
(see "Why the Count form is reported separately" below).
|
|
862
|
+
|
|
863
|
+
Why the Count form is reported separately
|
|
864
|
+
-------------------------------------------
|
|
865
|
+
TPTP (what :func:`check_molecule` reparses) has no native counting-
|
|
866
|
+
quantifier syntax — only the kit's own unicode surface syntax does. A
|
|
867
|
+
``Count`` node can therefore never be rendered back to reusable
|
|
868
|
+
``tptp_bare`` text, so folding it into the primary ``after_*`` output
|
|
869
|
+
would hand back a formula this same tool's sibling ``check_molecule``
|
|
870
|
+
could not accept. ``count_pattern`` is reported purely for information
|
|
871
|
+
(and for a caller working with a :class:`~unicode_logic_kit.fol.nodes.Node`
|
|
872
|
+
directly rather than through the TPTP round trip).
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
formula: the class definition to simplify.
|
|
876
|
+
all_different: passed to ``simplify_for_checking`` (default ``True``
|
|
877
|
+
— see the module docstring for why this differs from that
|
|
878
|
+
function's own, plain-FOL-safe default of ``False``).
|
|
879
|
+
dialect: as :func:`check_molecule`.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
``{"ok": True, "before_unicode": str, "after_unicode": str,
|
|
883
|
+
"after_tptp": str|None, "changed": bool, "removed": [str, ...],
|
|
884
|
+
"removed_count": int, "count_pattern": {"detected": bool,
|
|
885
|
+
"folded_unicode": str, "note": str} | {"detected": False}}``.
|
|
886
|
+
``after_tptp`` is ``None`` (with an explanatory
|
|
887
|
+
``after_tptp_note``) only in the — for this rule set, unreachable in
|
|
888
|
+
practice — case that the simplified formula contains a node type
|
|
889
|
+
with no native TPTP rendering (a ``Count`` node, or an operator
|
|
890
|
+
outside classical FOL reached via a non-``tptp_bare`` ``dialect``);
|
|
891
|
+
``simplify_for_checking`` itself never introduces one.
|
|
892
|
+
"""
|
|
893
|
+
node, err = _parse_chem(formula, dialect, argument="formula")
|
|
894
|
+
if err is not None:
|
|
895
|
+
return err
|
|
896
|
+
|
|
897
|
+
counted = count_from_existential_chain(node)
|
|
898
|
+
result = simplify_for_checking(node, all_different=all_different)
|
|
899
|
+
simplified = result.simplified
|
|
900
|
+
|
|
901
|
+
payload = {
|
|
902
|
+
"ok": True,
|
|
903
|
+
"before_unicode": node.to_unicode_str(),
|
|
904
|
+
"after_unicode": simplified.to_unicode_str(),
|
|
905
|
+
"changed": result.changed,
|
|
906
|
+
"removed": list(result.removed),
|
|
907
|
+
"removed_count": len(result.removed),
|
|
908
|
+
}
|
|
909
|
+
# known_names maps the TPTP importer's forced-capitalised kit spelling
|
|
910
|
+
# back to the true ChemLog original for every symbol IN the declared
|
|
911
|
+
# chemical vocabulary (chem.KIT_TO_CHEMLOG) — Node.to_tptp() lowercases
|
|
912
|
+
# a predicate's ENTIRE name, which would silently corrupt a mixed-case
|
|
913
|
+
# ChemLog name like "bDOUBLE" into "bdouble" (a different, uninterpreted
|
|
914
|
+
# symbol); this is the same case-preserving renderer
|
|
915
|
+
# repair_tptp_formula's own `repaired_text` uses internally. A symbol
|
|
916
|
+
# OUTSIDE the chemical vocabulary (an auxiliary class predicate) falls
|
|
917
|
+
# back to that renderer's own best-effort un-capitalisation, which is
|
|
918
|
+
# exact for the common case (a name that arrived as a bare, TPTP-
|
|
919
|
+
# required-lowercase functor) — see fol.tptp_repair's module docstring
|
|
920
|
+
# for the one documented, narrow residual gap.
|
|
921
|
+
try:
|
|
922
|
+
payload["after_tptp"] = _render_tptp(simplified, dict(chem.KIT_TO_CHEMLOG))
|
|
923
|
+
except NotImplementedError:
|
|
924
|
+
payload["after_tptp"] = None
|
|
925
|
+
payload["after_tptp_note"] = (
|
|
926
|
+
"the simplified formula contains a node type with no native "
|
|
927
|
+
"TPTP rendering (e.g. a Count node, or an operator outside "
|
|
928
|
+
"classical FOL) — use after_unicode for display instead.")
|
|
929
|
+
|
|
930
|
+
if counted is not None:
|
|
931
|
+
payload["count_pattern"] = {
|
|
932
|
+
"detected": True,
|
|
933
|
+
"folded_unicode": counted.to_unicode_str(),
|
|
934
|
+
"note": (
|
|
935
|
+
"the ORIGINAL formula is exactly the distinct-witnesses "
|
|
936
|
+
"pattern for this counting quantifier. Reported for "
|
|
937
|
+
"information only — TPTP has no native counting syntax, so "
|
|
938
|
+
"this folded form cannot be re-submitted to check_molecule "
|
|
939
|
+
"as tptp_bare text; after_tptp above (still all_different-"
|
|
940
|
+
"simplified) evaluates the identical set of molecules under "
|
|
941
|
+
"the all_different convention."),
|
|
942
|
+
}
|
|
943
|
+
else:
|
|
944
|
+
payload["count_pattern"] = {"detected": False}
|
|
945
|
+
return payload
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# ---------------------------------------------------------------------------
|
|
949
|
+
# 6. chemical_signature
|
|
950
|
+
# ---------------------------------------------------------------------------
|
|
951
|
+
|
|
952
|
+
# Grouping of chem.CHEMLOG_SIGNATURE's 35 declared predicates, for a caller
|
|
953
|
+
# that wants "which predicates cover charge" rather than a flat 35-entry
|
|
954
|
+
# list. Mirrors unicode_logic_kit.mcp.syntax_spec's own "chemistry" topic
|
|
955
|
+
# grouping exactly (both are hand-written against the SAME published
|
|
956
|
+
# vocabulary, chem.CHEMLOG_SIGNATURE — the assertion in
|
|
957
|
+
# chemical_signature() below is what keeps this list from silently drifting
|
|
958
|
+
# out of sync with it, rather than a shared import of chem's own PRIVATE
|
|
959
|
+
# _naming module).
|
|
960
|
+
_COMPUTED_PREDICATES: Tuple[str, ...] = (
|
|
961
|
+
"in_ring", "in_ring_of_size_3", "in_ring_of_size_4", "in_ring_of_size_5",
|
|
962
|
+
"in_ring_of_size_6", "in_ring_of_size_7", "in_ring_of_size_8",
|
|
963
|
+
"aromatic", "same_fragment", "carbon_connected",
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
_SIGNATURE_GROUPS: Dict[str, Tuple[str, ...]] = {
|
|
967
|
+
"atom_types": _ATOM_LETTERS,
|
|
968
|
+
"hydrogen_count": ("has_0_hs", "has_1_hs", "has_2_hs", "has_3_hs"),
|
|
969
|
+
"charge": ("charge0", "charge_m1", "charge_p1"),
|
|
970
|
+
"chirality": ("ChiralR", "ChiralS"),
|
|
971
|
+
"bonds": ("bSINGLE", "bDOUBLE", "bTRIPLE", "bAROMATIC", "bond",
|
|
972
|
+
"has_bond_to"),
|
|
973
|
+
"molecule_global": ("net_charge_neutral", "NetChargePositive",
|
|
974
|
+
"NetChargeNegative"),
|
|
975
|
+
"computed": _COMPUTED_PREDICATES,
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def chemical_signature() -> dict:
|
|
980
|
+
"""The full ChemLog vocabulary a chemical class definition may use
|
|
981
|
+
(https://github.com/sfluegel05/chemlog-peptides) — so a generator never
|
|
982
|
+
has to GUESS a predicate name or arity (a hallucinated predicate name is
|
|
983
|
+
a pure-SYNTAX failure that burns a whole generation attempt; an
|
|
984
|
+
unknown-predicate/wrong-arity slip is exactly what this catches before a
|
|
985
|
+
definition ever reaches model checking, via
|
|
986
|
+
``api.check(formula, signature=chem.CHEMLOG_SIGNATURE)``).
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
``{"ok": True, "signature": <chem.CHEMLOG_SIGNATURE.to_dict()>,
|
|
990
|
+
"stored_predicates": [...], "computed_predicates": [...],
|
|
991
|
+
"groups": {"atom_types": [...], "hydrogen_count": [...], "charge":
|
|
992
|
+
[...], "chirality": [...], "bonds": [...], "molecule_global": [...],
|
|
993
|
+
"computed": [...]}, "dialect_note": str}``. ``signature`` is the
|
|
994
|
+
raw, declared ``(name, arity)`` table (40 predicates — see
|
|
995
|
+
:data:`unicode_logic_kit.chem.CHEMLOG_SIGNATURE`'s own docstring for
|
|
996
|
+
exactly what is and is not covered, e.g. only the CANONICAL
|
|
997
|
+
hydrogen-count 0..3 / charge -1/0/+1 range); ``stored_predicates``/
|
|
998
|
+
``computed_predicates`` split that same set by whether
|
|
999
|
+
:func:`unicode_logic_kit.chem.mol_to_structure` materialises the
|
|
1000
|
+
extension up front or decides it algorithmically on demand (see that
|
|
1001
|
+
function's own "Why five predicates are COMPUTED" section);
|
|
1002
|
+
``groups`` is the same 40 predicates organised by chemical
|
|
1003
|
+
role, for a caller that wants "what covers charge" rather than a
|
|
1004
|
+
flat list.
|
|
1005
|
+
"""
|
|
1006
|
+
sig_dict = chem.CHEMLOG_SIGNATURE.to_dict()
|
|
1007
|
+
declared = set(sig_dict["predicates"])
|
|
1008
|
+
groups = {name: list(names) for name, names in _SIGNATURE_GROUPS.items()}
|
|
1009
|
+
grouped_names = {n for names in groups.values() for n in names}
|
|
1010
|
+
# Sanity, not a caller-facing failure mode: these groups are hand-
|
|
1011
|
+
# written against chem.CHEMLOG_SIGNATURE (see this section's own
|
|
1012
|
+
# docstring) rather than derived from it, precisely so this module never
|
|
1013
|
+
# imports chem's PRIVATE _naming — an assertion is what keeps that
|
|
1014
|
+
# duplication from silently drifting if the declared vocabulary ever
|
|
1015
|
+
# changes, per this kit's own "never approximate silently" convention.
|
|
1016
|
+
assert grouped_names <= declared, sorted(grouped_names - declared)
|
|
1017
|
+
stored = sorted(declared - set(_COMPUTED_PREDICATES))
|
|
1018
|
+
return {
|
|
1019
|
+
"ok": True,
|
|
1020
|
+
"signature": sig_dict,
|
|
1021
|
+
"stored_predicates": stored,
|
|
1022
|
+
"computed_predicates": list(_COMPUTED_PREDICATES),
|
|
1023
|
+
"groups": groups,
|
|
1024
|
+
"dialect_note": (
|
|
1025
|
+
"This vocabulary uses lowercase predicate names (c, o, n, "
|
|
1026
|
+
"bSINGLE, ...); in the kit's own unicode syntax those parse as "
|
|
1027
|
+
"variables/functions, never predicates. Write chemical class "
|
|
1028
|
+
"definitions in TPTP and pass dialect='tptp_bare' to "
|
|
1029
|
+
"check_molecule/check_molecules/explain_molecule_failure/"
|
|
1030
|
+
"simplify_definition rather than relying on auto-detection."),
|
|
1031
|
+
}
|