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,544 @@
|
|
|
1
|
+
"""Model-checking-oriented simplification of FOL formulas, and a bridge between
|
|
2
|
+
Count and the "distinct existential witnesses" encoding an LLM tends to write.
|
|
3
|
+
|
|
4
|
+
Motivation: an LLM translating a class definition such as "has at least 40
|
|
5
|
+
carbons" produces ``∃x_0…∃x_39 (⋀_i c(x_i) ∧ ⋀_{i<j} x_i≠x_j)`` — 40
|
|
6
|
+
existentially-quantified variables plus all ``binomial(40,2) = 780`` pairwise
|
|
7
|
+
inequalities between them. Those constraints are unnecessary for a model
|
|
8
|
+
checker that already interprets separately introduced variables as distinct,
|
|
9
|
+
and FOL definitions that rely on such predicates, or that carry trivial
|
|
10
|
+
pairwise inequality constraints, are prone to timeouts. What the shape really
|
|
11
|
+
asks for is a parameterised built-in — an "at least n carbons" counter,
|
|
12
|
+
evaluated directly by the model checker — and this kit already has one:
|
|
13
|
+
:class:`~unicode_logic_kit.fol.nodes.Count`
|
|
14
|
+
(``∃≥n x φ``), which is first-order EXPRESSIBLE (so nothing about the target
|
|
15
|
+
logic changes) but is represented and can be *evaluated* without ever
|
|
16
|
+
materialising the O(n²) encoding (see :mod:`unicode_logic_kit.semantics` for the
|
|
17
|
+
structure-checking side of that).
|
|
18
|
+
|
|
19
|
+
This module supplies the missing piece: recognising and rewriting the
|
|
20
|
+
NL-generated distinct-witnesses pattern (in both directions), plus a separate,
|
|
21
|
+
narrower simplification pass that removes exactly the redundant pairwise
|
|
22
|
+
inequalities WITHOUT requiring the whole formula to be that one clean pattern
|
|
23
|
+
— real LLM output routinely interleaves the inequalities with other content
|
|
24
|
+
(actual chemical bonds between the same variables, for instance), which
|
|
25
|
+
:func:`count_from_existential_chain` must conservatively refuse to touch.
|
|
26
|
+
|
|
27
|
+
Two independent tools, two independent soundness stories
|
|
28
|
+
----------------------------------------------------------
|
|
29
|
+
``simplify_for_checking(formula, all_different=...)``
|
|
30
|
+
A conservative rewriter that never changes what the formula MEANS. Most
|
|
31
|
+
of its rules (duplicate conjuncts/disjuncts, ``t=t``, vacuous quantifiers,
|
|
32
|
+
double negation) hold under ordinary classical FOL semantics — the
|
|
33
|
+
``removed``/`changed`` bookkeeping documents exactly what left the tree
|
|
34
|
+
and why. Its one semantics-DEPENDENT rule (``all_different=True``) removes
|
|
35
|
+
pairwise inequalities between separately ∃-bound variables under the
|
|
36
|
+
*specific, non-standard convention some model checkers adopt* —
|
|
37
|
+
separately introduced existential variables are already distinct —
|
|
38
|
+
which is NOT a theorem of ordinary FOL (a domain with one element makes
|
|
39
|
+
``∃x∃y(x≠y)`` false there). ``all_different`` therefore defaults to
|
|
40
|
+
``False``: soundness under standard semantics is the default, and that
|
|
41
|
+
convention must be opted into explicitly, one call at a time.
|
|
42
|
+
|
|
43
|
+
``count_from_existential_chain`` / ``expand_count``
|
|
44
|
+
A structural equivalence, sound under ORDINARY FOL semantics (no flag,
|
|
45
|
+
no assumption beyond a non-empty domain): ``∃x_0…∃x_{n-1} (⋀ φ(x_i) ∧
|
|
46
|
+
⋀_{i<j} x_i≠x_j)`` and ``Count("ge", n, x, φ(x))`` denote the same set of
|
|
47
|
+
structures, because the former is exactly what
|
|
48
|
+
:meth:`~unicode_logic_kit.fol.nodes.Count._expand` produces when lowering
|
|
49
|
+
the latter. ``count_from_existential_chain`` is therefore conservative in
|
|
50
|
+
a DIFFERENT sense than the flag above: it recognises only a complete,
|
|
51
|
+
exact instance of the pattern (every φ(x_i), every C(n,2) inequality,
|
|
52
|
+
nothing else) and returns ``None`` rather than guess at a rewrite that
|
|
53
|
+
would silently drop real content (e.g. a bond literal connecting two of
|
|
54
|
+
the witnesses). ``expand_count`` is the inverse direction, for backends
|
|
55
|
+
that have no ``Count`` node of their own; it does not duplicate the
|
|
56
|
+
distinct-witnesses encoding — it walks the tree and reuses
|
|
57
|
+
:meth:`Count._expand` on every ``Count`` node found (the same lowering
|
|
58
|
+
``Count.to_z3``/``to_prover9``/``to_tptp`` already use internally).
|
|
59
|
+
|
|
60
|
+
Both rewriters are purely syntactic (no ``to_z3``, no model). Semantic
|
|
61
|
+
equivalence between the two directions is checked in the test suite via
|
|
62
|
+
:func:`unicode_logic_kit.atp.z3_equivalence.formulas_are_equivalent` on small
|
|
63
|
+
instances (n≤4) — see tests/test_simplify_check.py.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
from dataclasses import dataclass, fields
|
|
67
|
+
from typing import Dict, List, Optional, Tuple
|
|
68
|
+
|
|
69
|
+
from .nodes import (
|
|
70
|
+
Node, Variable, Constant, Number, Atom, Not, And, Or, Quantifier, Count,
|
|
71
|
+
Cardinality, SortedQuantifier, SortedCount, SortedCardinality, Lambda,
|
|
72
|
+
free_variables,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"SimplifyResult", "simplify_for_checking",
|
|
77
|
+
"count_from_existential_chain", "expand_count",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
_EXISTS = ("∃", "exists")
|
|
81
|
+
_FORALL = ("∀", "forall")
|
|
82
|
+
|
|
83
|
+
# Sentinel marking a "combine the two children just pushed" step in the
|
|
84
|
+
# iterative chain walk below — distinct from any real Node by identity.
|
|
85
|
+
_COMBINE = object()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class SimplifyResult:
|
|
90
|
+
"""The result of :func:`simplify_for_checking`.
|
|
91
|
+
|
|
92
|
+
``simplified`` is the rewritten formula (structurally identical to the
|
|
93
|
+
input, down to the exact ∧/∨ bracketing, whenever nothing was removed —
|
|
94
|
+
see :func:`_rebuild_like`). ``removed`` is one human-readable description
|
|
95
|
+
per individual construct that was dropped or collapsed (so
|
|
96
|
+
``len(result.removed)`` counts, e.g., exactly how many redundant pairwise
|
|
97
|
+
inequalities a call with ``all_different=True`` removed). ``changed`` is
|
|
98
|
+
``True`` iff ``removed`` is non-empty; it never disagrees with ``removed``
|
|
99
|
+
because every rewrite rule below logs its own action before applying it —
|
|
100
|
+
there is no rule that changes the tree silently.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
simplified: Node
|
|
104
|
+
removed: Tuple[str, ...]
|
|
105
|
+
changed: bool
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Iterative flatten/rebuild over a binary ∧- or ∨-chain.
|
|
110
|
+
#
|
|
111
|
+
# Both are iterative (explicit stack), not recursive, on purpose: an n-ary
|
|
112
|
+
# conjunction as an LLM actually writes it (left- or right-nested, never
|
|
113
|
+
# balanced) recurses to depth n in the naive implementation, and n is exactly
|
|
114
|
+
# the O(n²)-inequality count this module exists to simplify (820 conjuncts for
|
|
115
|
+
# an "at least 40 carbons" formula) — close enough to Python's default
|
|
116
|
+
# recursion limit (1000) to be a real risk, not a theoretical one.
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def _chain_ops(node: Node, cls) -> List:
|
|
120
|
+
"""POSTORDER walk of a binary ``cls``-chain (``cls`` is ``And`` or ``Or``).
|
|
121
|
+
|
|
122
|
+
Returns a flat list mixing leaf Nodes (in left-to-right order) with the
|
|
123
|
+
``_COMBINE`` sentinel marking "pop two values and join them" — enough
|
|
124
|
+
information to both read off the leaves (:func:`_flatten_chain`) and
|
|
125
|
+
rebuild the exact original shape around a substitute leaf sequence
|
|
126
|
+
(:func:`_rebuild_like`), without walking the tree twice recursively.
|
|
127
|
+
"""
|
|
128
|
+
ops: List = []
|
|
129
|
+
stack = [(node, False)]
|
|
130
|
+
while stack:
|
|
131
|
+
cur, expanded = stack.pop()
|
|
132
|
+
if isinstance(cur, cls):
|
|
133
|
+
if not expanded:
|
|
134
|
+
stack.append((cur, True))
|
|
135
|
+
stack.append((cur.right, False))
|
|
136
|
+
stack.append((cur.left, False))
|
|
137
|
+
else:
|
|
138
|
+
ops.append(_COMBINE)
|
|
139
|
+
else:
|
|
140
|
+
ops.append(cur)
|
|
141
|
+
return ops
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _flatten_chain(node: Node, cls) -> List[Node]:
|
|
145
|
+
"""The leaves of a binary ``cls``-chain, left to right (``[node]`` if
|
|
146
|
+
``node`` is not itself a ``cls``)."""
|
|
147
|
+
return [op for op in _chain_ops(node, cls) if op is not _COMBINE]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _fold(cls, parts: List[Node]) -> Node:
|
|
151
|
+
"""Left-associative rebuild of ``parts`` under ``cls`` — the shape used
|
|
152
|
+
whenever the leaf sequence actually changed (so there is no "original
|
|
153
|
+
shape" left to preserve)."""
|
|
154
|
+
result = parts[0]
|
|
155
|
+
for part in parts[1:]:
|
|
156
|
+
result = cls(result, part)
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _rebuild_like(template: Node, cls, leaves) -> Node:
|
|
161
|
+
"""Rebuild EXACTLY ``template``'s bracketing, substituting ``leaves`` (an
|
|
162
|
+
iterator, consumed left to right) for its original leaf positions.
|
|
163
|
+
|
|
164
|
+
Used only when nothing was removed from the chain, so the result is
|
|
165
|
+
byte-for-byte the same shape as the input (with each leaf replaced by its
|
|
166
|
+
own, independently simplified, form) — callers that compare
|
|
167
|
+
``result.simplified`` against the input with plain ``==`` when
|
|
168
|
+
``changed`` is ``False`` get exact equality, not just equivalence.
|
|
169
|
+
"""
|
|
170
|
+
stack: List[Node] = []
|
|
171
|
+
for op in _chain_ops(template, cls):
|
|
172
|
+
if op is _COMBINE:
|
|
173
|
+
right = stack.pop()
|
|
174
|
+
left = stack.pop()
|
|
175
|
+
stack.append(cls(left, right))
|
|
176
|
+
else:
|
|
177
|
+
stack.append(next(leaves))
|
|
178
|
+
return stack[0]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# Eligibility for "drop this conjunct" — the two rules that only make sense
|
|
183
|
+
# INSIDE a conjunction (dropping a conjunct that is always true preserves
|
|
184
|
+
# meaning; dropping it from a DISJUNCTION would not: A ∨ True ≡ True, not A).
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _is_trivial_equality(node: Node) -> bool:
|
|
188
|
+
"""``t = t`` for any term ``t`` (not just a bare variable): reflexivity of
|
|
189
|
+
``=`` makes this valid regardless of what ``t`` denotes, so ``φ ∧ (t=t) ≡
|
|
190
|
+
φ`` always — no ``all_different`` assumption needed."""
|
|
191
|
+
return (isinstance(node, Atom) and node.predicate == "="
|
|
192
|
+
and len(node.args) == 2 and node.args[0] == node.args[1])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_redundant_all_diff_ne(node: Node, binders: Dict[str, str]) -> bool:
|
|
196
|
+
"""``x ≠ y`` where both ``x`` and ``y`` are separately ∃-bound variables —
|
|
197
|
+
redundant ONLY under the ``all_different=True`` convention (see the module
|
|
198
|
+
docstring). Deliberately narrow:
|
|
199
|
+
|
|
200
|
+
- both sides must be plain :class:`Variable` terms (a constant or a
|
|
201
|
+
function application on either side is excluded — the convention is
|
|
202
|
+
about how a model checker treats freshly INTRODUCED existential
|
|
203
|
+
variables, and says nothing about a comparison against a fixed
|
|
204
|
+
denotation);
|
|
205
|
+
- the two names must differ (``x ≠ x`` is not "two separate variables
|
|
206
|
+
happen to be distinct", it is an outright contradiction — leaving it
|
|
207
|
+
alone is the conservative choice, not an oversight);
|
|
208
|
+
- both must be bound by an enclosing ``∃`` specifically — a ``∀``-bound
|
|
209
|
+
variable ranges over the WHOLE domain, so "this witness is distinct
|
|
210
|
+
from that other witness" is not something the model checker's
|
|
211
|
+
convention says anything about.
|
|
212
|
+
"""
|
|
213
|
+
if not (isinstance(node, Atom) and node.predicate == "≠" and len(node.args) == 2):
|
|
214
|
+
return False
|
|
215
|
+
left, right = node.args
|
|
216
|
+
if not (isinstance(left, Variable) and isinstance(right, Variable)):
|
|
217
|
+
return False
|
|
218
|
+
if left.name == right.name:
|
|
219
|
+
return False
|
|
220
|
+
return binders.get(left.name) == "exists" and binders.get(right.name) == "exists"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _is_removable_conjunct(node: Node, all_different: bool, binders: Dict[str, str]) -> bool:
|
|
224
|
+
return _is_trivial_equality(node) or (all_different and _is_redundant_all_diff_ne(node, binders))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _describe_removed_conjunct(node: Node, all_different: bool) -> str:
|
|
228
|
+
if _is_trivial_equality(node):
|
|
229
|
+
return ("dropped trivial reflexive equality from a conjunction "
|
|
230
|
+
f"(φ ∧ (t=t) ≡ φ): {node.to_unicode_str()}")
|
|
231
|
+
left, right = node.args
|
|
232
|
+
return ("dropped redundant pairwise inequality under all_different=True "
|
|
233
|
+
f"(φ ∧ ({left.name}≠{right.name}) ≡ φ under this kit's "
|
|
234
|
+
"all_different convention, since both are separately ∃-bound "
|
|
235
|
+
f"and hence already distinct): {node.to_unicode_str()}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _dedupe_chain(parts: List[Node], op_glyph: str, removed: List[str]) -> List[Node]:
|
|
239
|
+
"""Drop later occurrences of a structurally-identical operand (idempotence
|
|
240
|
+
of ∧/∨: ``φ op φ ≡ φ``) — valid regardless of ``op_glyph`` and regardless
|
|
241
|
+
of ``all_different``, so this runs unconditionally for both And and Or."""
|
|
242
|
+
seen = set()
|
|
243
|
+
kept: List[Node] = []
|
|
244
|
+
for part in parts:
|
|
245
|
+
if part in seen:
|
|
246
|
+
removed.append(
|
|
247
|
+
f"dropped duplicate {op_glyph}-operand (idempotence: "
|
|
248
|
+
f"φ {op_glyph} φ ≡ φ): {part.to_unicode_str()}"
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
seen.add(part)
|
|
252
|
+
kept.append(part)
|
|
253
|
+
return kept
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _filter_and_conjuncts(parts: List[Node], all_different: bool,
|
|
257
|
+
binders: Dict[str, str], removed: List[str]) -> List[Node]:
|
|
258
|
+
"""Drop every removable conjunct (:func:`_is_removable_conjunct`), except
|
|
259
|
+
that when EVERY conjunct in the chain is removable, one is kept —
|
|
260
|
+
otherwise the chain would collapse to an empty (nonexistent) formula.
|
|
261
|
+
The kept one is equivalent to True anyway (it is one of the removable
|
|
262
|
+
ones), so keeping it changes nothing observable; which one is kept is an
|
|
263
|
+
arbitrary but deterministic choice (the first, by list order)."""
|
|
264
|
+
eligible = [i for i, p in enumerate(parts) if _is_removable_conjunct(p, all_different, binders)]
|
|
265
|
+
drop = set(eligible)
|
|
266
|
+
if eligible and len(eligible) == len(parts):
|
|
267
|
+
drop.discard(eligible[0])
|
|
268
|
+
kept: List[Node] = []
|
|
269
|
+
for i, part in enumerate(parts):
|
|
270
|
+
if i in drop:
|
|
271
|
+
removed.append(_describe_removed_conjunct(part, all_different))
|
|
272
|
+
continue
|
|
273
|
+
kept.append(part)
|
|
274
|
+
return kept
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# The recursive engine.
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
# Node classes that bind exactly one variable over exactly one Node-valued
|
|
282
|
+
# "body" field but are NOT a Quantifier subclass, so the dedicated Quantifier
|
|
283
|
+
# branch above never sees them — Count/Cardinality/SortedQuantifier/
|
|
284
|
+
# SortedCount/SortedCardinality all bind via a field named ``variable`` over
|
|
285
|
+
# a field named ``formula``; Lambda binds via ``param`` (a LambdaVar, not a
|
|
286
|
+
# Variable) over ``body``. Mirrors exactly the case list
|
|
287
|
+
# :func:`unicode_logic_kit.fol._msfl_nodes.free_variables` already special-cases
|
|
288
|
+
# for the same reason (a binder whose bound name is not tracked as its own
|
|
289
|
+
# scope leaks the ENCLOSING scope's variable classification into a name that,
|
|
290
|
+
# inside this node, actually refers to something else entirely).
|
|
291
|
+
#
|
|
292
|
+
# Used below to SHADOW (remove, not reclassify as "exists"/"forall") the
|
|
293
|
+
# bound name out of ``binders`` while descending into the body field only —
|
|
294
|
+
# this is deliberately not the same as adding it with a "exists" kind: the
|
|
295
|
+
# all_different convention is documented as being about separately ∃-bound
|
|
296
|
+
# Quantifier variables specifically, so these binders still never make the
|
|
297
|
+
# rule fire on THEIR OWN bound name; the fix here is purely to stop an
|
|
298
|
+
# outer, differently-scoped ∃/∀ of the identical name from being applied to
|
|
299
|
+
# occurrences that this binder has re-bound (see Finding 1 in the task brief:
|
|
300
|
+
# ``all_different=True`` could otherwise drop a ``≠`` between a variable
|
|
301
|
+
# re-bound by an enclosing Count/Cardinality/SortedQuantifier/SortedCount/
|
|
302
|
+
# SortedCardinality/Lambda and an unrelated, same-named ∃-bound variable
|
|
303
|
+
# further out — unsound, since the two occurrences do not denote the same
|
|
304
|
+
# thing).
|
|
305
|
+
_SINGLE_VAR_BINDER_FIELDS: Dict[type, Tuple[str, str]] = {
|
|
306
|
+
Count: ("variable", "formula"),
|
|
307
|
+
Cardinality: ("variable", "formula"),
|
|
308
|
+
SortedQuantifier: ("variable", "formula"),
|
|
309
|
+
SortedCount: ("variable", "formula"),
|
|
310
|
+
SortedCardinality: ("variable", "formula"),
|
|
311
|
+
Lambda: ("param", "body"),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _simplify(n: Node, binders: Dict[str, str], all_different: bool,
|
|
316
|
+
removed: List[str]) -> Node:
|
|
317
|
+
if isinstance(n, (Variable, Constant, Number, Atom)):
|
|
318
|
+
return n # leaves/terms: none of the rules below rewrite term structure
|
|
319
|
+
|
|
320
|
+
if isinstance(n, Not):
|
|
321
|
+
inner = _simplify(n.formula, binders, all_different, removed)
|
|
322
|
+
if isinstance(inner, Not):
|
|
323
|
+
# ¬¬φ ≡ φ, unconditionally (classical double negation elimination).
|
|
324
|
+
removed.append(
|
|
325
|
+
f"collapsed double negation (¬¬φ ≡ φ): ¬¬{inner.formula.to_unicode_str()}"
|
|
326
|
+
)
|
|
327
|
+
return inner.formula
|
|
328
|
+
return Not(inner)
|
|
329
|
+
|
|
330
|
+
if isinstance(n, And):
|
|
331
|
+
raw = _flatten_chain(n, And)
|
|
332
|
+
parts = [_simplify(p, binders, all_different, removed) for p in raw]
|
|
333
|
+
kept = _dedupe_chain(parts, "∧", removed)
|
|
334
|
+
kept = _filter_and_conjuncts(kept, all_different, binders, removed)
|
|
335
|
+
if kept == parts:
|
|
336
|
+
return _rebuild_like(n, And, iter(parts))
|
|
337
|
+
return _fold(And, kept)
|
|
338
|
+
|
|
339
|
+
if isinstance(n, Or):
|
|
340
|
+
raw = _flatten_chain(n, Or)
|
|
341
|
+
parts = [_simplify(p, binders, all_different, removed) for p in raw]
|
|
342
|
+
kept = _dedupe_chain(parts, "∨", removed)
|
|
343
|
+
if kept == parts:
|
|
344
|
+
return _rebuild_like(n, Or, iter(parts))
|
|
345
|
+
return _fold(Or, kept)
|
|
346
|
+
|
|
347
|
+
if isinstance(n, Quantifier):
|
|
348
|
+
if n.type in _EXISTS:
|
|
349
|
+
kind = "exists"
|
|
350
|
+
elif n.type in _FORALL:
|
|
351
|
+
kind = "forall"
|
|
352
|
+
else:
|
|
353
|
+
raise ValueError(f"simplify_for_checking: unknown quantifier type {n.type!r}")
|
|
354
|
+
new_binders = dict(binders)
|
|
355
|
+
new_binders[n.variable.name] = kind
|
|
356
|
+
body = _simplify(n.formula, new_binders, all_different, removed)
|
|
357
|
+
# ∃x φ ≡ φ / ∀x φ ≡ φ when x does not occur free in φ — valid because
|
|
358
|
+
# this kit's semantics (like Count._expand's own '≥0' case) assumes a
|
|
359
|
+
# NON-EMPTY domain throughout, so a quantifier that never inspects its
|
|
360
|
+
# variable is a no-op regardless of quantifier kind.
|
|
361
|
+
if n.variable.name not in {v.name for v in free_variables(body)}:
|
|
362
|
+
removed.append(
|
|
363
|
+
f"dropped vacuous {n.type}{n.variable.name} ({n.variable.name} "
|
|
364
|
+
"does not occur free in the body; valid on a non-empty domain): "
|
|
365
|
+
f"{n.type}{n.variable.name}"
|
|
366
|
+
)
|
|
367
|
+
return body
|
|
368
|
+
return Quantifier(n.type, n.variable, body)
|
|
369
|
+
|
|
370
|
+
# Generic structural fallback: every other node type (Implies, Iff, Xor,
|
|
371
|
+
# Count, Cardinality, Measure, and any modal/hybrid/linear/second-order/
|
|
372
|
+
# MSFL node). Recurse into Node-valued fields; leave the node's own type
|
|
373
|
+
# and every non-Node field untouched. This makes the function total
|
|
374
|
+
# (it never raises on an unfamiliar node) while never MISAPPLYING a
|
|
375
|
+
# classical rule to a same-looking but semantically different construct —
|
|
376
|
+
# e.g. Łukasiewicz LukNegation is a distinct class from Not, so double
|
|
377
|
+
# negation collapse never touches it; WeakConjunction is a distinct class
|
|
378
|
+
# from And, so it never gets the classical idempotence/trivial-conjunct
|
|
379
|
+
# treatment. A Count/Cardinality/SortedQuantifier/SortedCount/
|
|
380
|
+
# SortedCardinality/Lambda node's own bound variable is deliberately NOT
|
|
381
|
+
# added to ``binders`` here as "exists"/"forall" (none of these are a
|
|
382
|
+
# Quantifier subclass, and the all_different convention is documented as
|
|
383
|
+
# being specifically about separately ∃-bound Quantifier variables) — but
|
|
384
|
+
# its name MUST still be shadowed (removed from ``binders``) while
|
|
385
|
+
# descending into the body, or an outer ∃/∀ of the identical name would
|
|
386
|
+
# incorrectly keep classifying occurrences that this binder has re-bound
|
|
387
|
+
# to something else entirely (see ``_SINGLE_VAR_BINDER_FIELDS``).
|
|
388
|
+
binder_fields = _SINGLE_VAR_BINDER_FIELDS.get(type(n))
|
|
389
|
+
shadowed_binders = binders
|
|
390
|
+
shadow_field = None
|
|
391
|
+
if binder_fields is not None:
|
|
392
|
+
var_field, shadow_field = binder_fields
|
|
393
|
+
bound_name = getattr(n, var_field).name
|
|
394
|
+
if bound_name in binders:
|
|
395
|
+
shadowed_binders = dict(binders)
|
|
396
|
+
del shadowed_binders[bound_name]
|
|
397
|
+
|
|
398
|
+
new_kwargs = {}
|
|
399
|
+
for f in fields(n):
|
|
400
|
+
val = getattr(n, f.name)
|
|
401
|
+
field_binders = shadowed_binders if f.name == shadow_field else binders
|
|
402
|
+
if isinstance(val, Node):
|
|
403
|
+
new_kwargs[f.name] = _simplify(val, field_binders, all_different, removed)
|
|
404
|
+
elif isinstance(val, (list, tuple)):
|
|
405
|
+
new_kwargs[f.name] = type(val)(
|
|
406
|
+
_simplify(c, field_binders, all_different, removed) if isinstance(c, Node) else c
|
|
407
|
+
for c in val
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
new_kwargs[f.name] = val
|
|
411
|
+
return type(n)(**new_kwargs)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def simplify_for_checking(formula: Node, *, all_different: bool = False) -> SimplifyResult:
|
|
415
|
+
"""Simplify ``formula`` for model checking; see the module docstring for
|
|
416
|
+
the full rule list and their equivalence arguments.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
formula: any :class:`Node`.
|
|
420
|
+
all_different: if ``True``, ALSO remove pairwise inequalities between
|
|
421
|
+
separately ∃-bound variables (see
|
|
422
|
+
:func:`_is_redundant_all_diff_ne`). This is sound only under the
|
|
423
|
+
model checker's own "separately introduced existential variables
|
|
424
|
+
are already distinct" convention, NOT under standard FOL
|
|
425
|
+
semantics — leave it ``False`` (the default) unless the
|
|
426
|
+
downstream checker actually implements that convention.
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
A :class:`SimplifyResult`. Applying :func:`simplify_for_checking`
|
|
430
|
+
again to ``result.simplified`` (same ``all_different``) always yields
|
|
431
|
+
``changed=False`` — every rule above leaves no further instance of
|
|
432
|
+
its own pattern behind (checked in the test suite).
|
|
433
|
+
"""
|
|
434
|
+
removed: List[str] = []
|
|
435
|
+
simplified = _simplify(formula, {}, all_different, removed)
|
|
436
|
+
return SimplifyResult(simplified=simplified, removed=tuple(removed), changed=bool(removed))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ---------------------------------------------------------------------------
|
|
440
|
+
# Existential-chain <-> Count.
|
|
441
|
+
# ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
def count_from_existential_chain(formula: Node) -> Optional[Node]:
|
|
444
|
+
"""Recognise ``∃x_0…∃x_{n-1} (⋀_i φ(x_i) ∧ ⋀_{i<j} x_i≠x_j)`` and rewrite
|
|
445
|
+
it to ``Count("ge", n, x, φ(x))``. Returns ``None`` if ``formula`` is not
|
|
446
|
+
EXACTLY this pattern.
|
|
447
|
+
|
|
448
|
+
Requires, at the top of ``formula``:
|
|
449
|
+
|
|
450
|
+
- a chain of ``n ≥ 1`` consecutive ``∃`` quantifiers over ``n`` distinct
|
|
451
|
+
variable names (a ``∀`` anywhere in the prefix, or a shadowed/repeated
|
|
452
|
+
name, is not this pattern);
|
|
453
|
+
- a body that — once fully flattened as a ∧-chain — has EXACTLY
|
|
454
|
+
``n + n·(n-1)/2`` conjuncts: for every quantified variable exactly one
|
|
455
|
+
unary atom ``φ(x_i)`` (same predicate ``φ`` for all of them), and for
|
|
456
|
+
every unordered pair exactly one ``x_i≠x_j``, and NOTHING else.
|
|
457
|
+
|
|
458
|
+
Any deviation — a variable appearing in a further literal (a bond between
|
|
459
|
+
two of the witnesses, say), a mismatched predicate, an inequality against
|
|
460
|
+
a constant, a missing or duplicated pair, an extra or missing conjunct —
|
|
461
|
+
returns ``None`` rather than guessing: since the rewrite DISCARDS
|
|
462
|
+
everything except "n witnesses, all φ, all pairwise distinct", rewriting
|
|
463
|
+
a formula that also asserts something else about those witnesses would
|
|
464
|
+
silently drop that content. This is the CONSERVATIVE direction; the
|
|
465
|
+
all_different flag on :func:`simplify_for_checking` is the narrower,
|
|
466
|
+
non-conservative-in-scope tool for formulas that don't fit this pattern
|
|
467
|
+
exactly but still have redundant pairwise inequalities in them.
|
|
468
|
+
|
|
469
|
+
The equivalence this relies on is exactly :meth:`Count._expand`'s own
|
|
470
|
+
encoding (``∃≥n x φ`` unfolds to precisely this shape) — checked via Z3
|
|
471
|
+
equivalence for n≤4 in the test suite, in both directions.
|
|
472
|
+
"""
|
|
473
|
+
variables: List[Variable] = []
|
|
474
|
+
body = formula
|
|
475
|
+
while isinstance(body, Quantifier) and body.type in _EXISTS:
|
|
476
|
+
variables.append(body.variable)
|
|
477
|
+
body = body.formula
|
|
478
|
+
n = len(variables)
|
|
479
|
+
if n == 0:
|
|
480
|
+
return None
|
|
481
|
+
names = [v.name for v in variables]
|
|
482
|
+
if len(set(names)) != n:
|
|
483
|
+
return None
|
|
484
|
+
name_set = set(names)
|
|
485
|
+
|
|
486
|
+
parts = _flatten_chain(body, And)
|
|
487
|
+
expected = n + n * (n - 1) // 2
|
|
488
|
+
if len(parts) != expected:
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
phi_name: Optional[str] = None
|
|
492
|
+
matched_var: Dict[str, str] = {}
|
|
493
|
+
pairs_seen = set()
|
|
494
|
+
all_pairs = {frozenset((names[i], names[j])) for i in range(n) for j in range(i + 1, n)}
|
|
495
|
+
|
|
496
|
+
for part in parts:
|
|
497
|
+
if isinstance(part, Atom) and part.predicate == "≠" and len(part.args) == 2:
|
|
498
|
+
left, right = part.args
|
|
499
|
+
if not (isinstance(left, Variable) and isinstance(right, Variable)):
|
|
500
|
+
return None
|
|
501
|
+
if left.name not in name_set or right.name not in name_set or left.name == right.name:
|
|
502
|
+
return None
|
|
503
|
+
pair = frozenset((left.name, right.name))
|
|
504
|
+
if pair in pairs_seen:
|
|
505
|
+
return None
|
|
506
|
+
pairs_seen.add(pair)
|
|
507
|
+
continue
|
|
508
|
+
if (isinstance(part, Atom) and len(part.args) == 1
|
|
509
|
+
and isinstance(part.args[0], Variable)
|
|
510
|
+
and part.args[0].name in name_set):
|
|
511
|
+
v = part.args[0].name
|
|
512
|
+
if v in matched_var:
|
|
513
|
+
return None
|
|
514
|
+
if phi_name is None:
|
|
515
|
+
phi_name = part.predicate
|
|
516
|
+
elif part.predicate != phi_name:
|
|
517
|
+
return None
|
|
518
|
+
matched_var[v] = part.predicate
|
|
519
|
+
continue
|
|
520
|
+
return None # anything else — extra/foreign literal — disqualifies the rewrite
|
|
521
|
+
|
|
522
|
+
if set(matched_var) != name_set or pairs_seen != all_pairs:
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
rep = variables[0]
|
|
526
|
+
return Count("ge", Number(n), rep, Atom(phi_name, [rep]))
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def expand_count(formula: Node) -> Node:
|
|
530
|
+
"""Lower every ``Count`` node anywhere in ``formula`` to plain FOL, for
|
|
531
|
+
backends with no ``Count`` node of their own.
|
|
532
|
+
|
|
533
|
+
Reuses :meth:`Count._expand` (the same distinct-witnesses lowering
|
|
534
|
+
``Count.to_z3``/``to_prover9``/``to_tptp`` already call internally)
|
|
535
|
+
rather than duplicating it — this function only supplies the tree walk.
|
|
536
|
+
Mirrors the ``Count``-handling branch of
|
|
537
|
+
:func:`unicode_logic_kit.fol._msfl_nodes.to_fol`'s ``_reduce_nl_nodes``,
|
|
538
|
+
minus that function's other, unrelated reductions (Łukasiewicz collapse,
|
|
539
|
+
sort relativisation): this one does exactly one thing.
|
|
540
|
+
"""
|
|
541
|
+
reduced = formula.map_children(expand_count)
|
|
542
|
+
if isinstance(reduced, Count):
|
|
543
|
+
return reduced._expand()
|
|
544
|
+
return reduced
|