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,492 @@
|
|
|
1
|
+
"""Classical-FOL normal forms: NNF, PNF, CNF, Skolemization, and a Horn check.
|
|
2
|
+
|
|
3
|
+
All entry points first call ``to_fol`` to eliminate sort annotations, so they
|
|
4
|
+
accept FOL and MSFOL inputs. Łukasiewicz (fuzzy) connectives are REJECTED with a
|
|
5
|
+
pointer instead of being silently collapsed: the collapse changes the logic (the
|
|
6
|
+
classical image of fuzzy ``P ∨ ¬P`` is valid, the fuzzy original is not), and it
|
|
7
|
+
used to leak wrong verdicts into ``is_valid_resolution``. Collapse explicitly
|
|
8
|
+
with ``to_fol(node)`` first if the classical skeleton is really wanted. Lambda
|
|
9
|
+
terms must be beta-reduced and lambda-eliminated first (``to_fol`` will raise
|
|
10
|
+
otherwise).
|
|
11
|
+
|
|
12
|
+
- ``to_nnf`` — negation normal form (eliminate → ↔ ⊕, push ¬ to atoms).
|
|
13
|
+
- ``to_pnf`` — prenex normal form (quantifier prefix + quantifier-free NNF
|
|
14
|
+
matrix), with bound variables standardised apart. Equivalence-preserving.
|
|
15
|
+
- ``to_cnf`` — prenex form whose matrix is a conjunction of clauses.
|
|
16
|
+
Equivalence-preserving.
|
|
17
|
+
- ``to_dnf`` — prenex form whose matrix is a disjunction of conjunctive
|
|
18
|
+
clauses (the dual of ``to_cnf``). Equivalence-preserving.
|
|
19
|
+
- ``to_tseitin_cnf`` — EQUISATISFIABLE CNF of the matrix via the Tseitin /
|
|
20
|
+
definitional encoding (fresh 0-ary auxiliary predicates); avoids the
|
|
21
|
+
exponential blow-up of the distributive ``to_cnf``. NOT equivalence-preserving.
|
|
22
|
+
- ``skolemize`` — prenex NNF with existentials replaced by Skolem terms over
|
|
23
|
+
the universals in scope; universal prefix retained. Satisfiability-preserving
|
|
24
|
+
(NOT equivalence-preserving).
|
|
25
|
+
- ``is_horn`` — True iff the clausal form (skolemise → drop ∀ → CNF) consists of
|
|
26
|
+
Horn clauses (each clause has at most one positive literal).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .nodes import (
|
|
30
|
+
Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
31
|
+
Variable, Constant, Number, Function, to_fol,
|
|
32
|
+
)
|
|
33
|
+
from ._msfl_nodes import _rename
|
|
34
|
+
|
|
35
|
+
_FORALL = ("∀", "forall")
|
|
36
|
+
_EXISTS = ("∃", "exists")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _unsupported_hint(n: Node) -> str:
|
|
40
|
+
"""A pointer at the right tool for a node the classical normal forms reject."""
|
|
41
|
+
from .nodes import (
|
|
42
|
+
Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
|
|
43
|
+
Next, Always, Eventually, Until, Historically, Once, Previous, Since,
|
|
44
|
+
Nominal, At, Would, Might, Cardinality, SortedCardinality,
|
|
45
|
+
SecondOrderQuantifier,
|
|
46
|
+
)
|
|
47
|
+
if isinstance(n, (Would, Might)):
|
|
48
|
+
return (" — the counterfactuals □→/◇→ have no first-order reading; use "
|
|
49
|
+
"cf_valid / cf_satisfies (sphere semantics) or "
|
|
50
|
+
"isabelle_decide_counterfactual.")
|
|
51
|
+
if isinstance(n, (Until, Since)):
|
|
52
|
+
return (" — Until/Since are not first-order definable (they need a "
|
|
53
|
+
"transitive-closure fixpoint); evaluate with satisfies_modal or "
|
|
54
|
+
"decide with isabelle_decide_modal.")
|
|
55
|
+
if isinstance(n, (Box, Diamond, Knows, Believes, Says, Wants, Obligatory,
|
|
56
|
+
Permitted, Next, Always, Eventually, Historically, Once,
|
|
57
|
+
Previous, Nominal, At)):
|
|
58
|
+
return (" — modal/temporal/hybrid operators have no classical rule here; "
|
|
59
|
+
"lower the propositional-modal fragment with "
|
|
60
|
+
"standard_translation, or use the modal tableau / "
|
|
61
|
+
"isabelle_decide_modal (is_valid_resolution and the Fitch/tableau "
|
|
62
|
+
"entry points route automatically).")
|
|
63
|
+
if isinstance(n, (Cardinality, SortedCardinality)):
|
|
64
|
+
return (" — set cardinality is second-order; evaluate with "
|
|
65
|
+
"semantics.tarski.satisfies or export to HOL via hol.secondorder.")
|
|
66
|
+
if isinstance(n, SecondOrderQuantifier):
|
|
67
|
+
return (" — second-order quantification has no sound classical "
|
|
68
|
+
"Skolemization; use the sequent calculus's SO rules, "
|
|
69
|
+
"satisfies_so, or hol.secondorder.")
|
|
70
|
+
from ._linear_nodes import Tensor, With, OPlus, LinearImplies, OfCourse, One
|
|
71
|
+
if isinstance(n, (Tensor, With, OPlus, LinearImplies, OfCourse, One)):
|
|
72
|
+
return (" — substructural (linear-logic) connectives have no classical "
|
|
73
|
+
"normal form: weakening/contraction are exactly what the collapse "
|
|
74
|
+
"would smuggle back in; decide derivability with atp.linear "
|
|
75
|
+
"(ill_prove / ill_derivable).")
|
|
76
|
+
from ._lambek_nodes import Product, Under, Over
|
|
77
|
+
if isinstance(n, (Product, Under, Over)):
|
|
78
|
+
return (" — Lambek-calculus connectives are order-sensitive; a classical "
|
|
79
|
+
"normal form would erase exactly that; decide derivability with "
|
|
80
|
+
"atp.lambek (lambek_prove / lambek_derivable).")
|
|
81
|
+
from ._team_nodes import Dependence, SlashedExists
|
|
82
|
+
if isinstance(n, (Dependence, SlashedExists)):
|
|
83
|
+
return (" — dependence/IF constructs are team-semantic (existential "
|
|
84
|
+
"second-order strength); evaluate with team_satisfies / "
|
|
85
|
+
"team_models.")
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Negation normal form
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _reject_fuzzy(node: Node, entry: str) -> None:
|
|
94
|
+
"""Raise on Łukasiewicz input: the classical collapse changes the LOGIC.
|
|
95
|
+
|
|
96
|
+
The fuzzy connectives' ``to_msfol`` image (min/max/t-norm → And/Or/…) is a
|
|
97
|
+
different logic — classical ``P ∨ ¬P`` is valid, the fuzzy original is not —
|
|
98
|
+
so collapsing silently here leaked wrong verdicts into
|
|
99
|
+
``is_valid_resolution``. The collapse stays available, explicitly:
|
|
100
|
+
``to_fol(node)`` first.
|
|
101
|
+
"""
|
|
102
|
+
from ..semantics._modal_reject import FUZZY_TYPES
|
|
103
|
+
for sub in node.walk():
|
|
104
|
+
if isinstance(sub, FUZZY_TYPES):
|
|
105
|
+
raise TypeError(
|
|
106
|
+
f"{entry}: {type(sub).__name__} is a Łukasiewicz connective — "
|
|
107
|
+
"classical normal forms (and the resolution prover on top of "
|
|
108
|
+
"them) would silently decide the WRONG logic. Evaluate with "
|
|
109
|
+
"semantics.fuzzy.evaluate or decide with "
|
|
110
|
+
"atp.z3_fuzzy.fuzzy_is_valid; collapse explicitly with "
|
|
111
|
+
"to_fol(node) first if the classical skeleton is wanted.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def to_nnf(node: Node) -> Node:
|
|
115
|
+
"""Return the negation normal form of node (after reducing to classical FOL).
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
TypeError: on Łukasiewicz (fuzzy) input — the classical collapse changes
|
|
119
|
+
the logic, so it must be requested explicitly via ``to_fol(node)``.
|
|
120
|
+
"""
|
|
121
|
+
_reject_fuzzy(node, "to_nnf")
|
|
122
|
+
return _nnf(to_fol(node))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _nnf(n: Node) -> Node:
|
|
126
|
+
if isinstance(n, Atom):
|
|
127
|
+
return n
|
|
128
|
+
if isinstance(n, Not):
|
|
129
|
+
return _nnf_neg(n.formula)
|
|
130
|
+
if isinstance(n, And):
|
|
131
|
+
return And(_nnf(n.left), _nnf(n.right))
|
|
132
|
+
if isinstance(n, Or):
|
|
133
|
+
return Or(_nnf(n.left), _nnf(n.right))
|
|
134
|
+
if isinstance(n, Implies):
|
|
135
|
+
return Or(_nnf(Not(n.left)), _nnf(n.right))
|
|
136
|
+
if isinstance(n, Iff):
|
|
137
|
+
return And(_nnf(Implies(n.left, n.right)), _nnf(Implies(n.right, n.left)))
|
|
138
|
+
if isinstance(n, Xor):
|
|
139
|
+
return Or(And(_nnf(n.left), _nnf(Not(n.right))),
|
|
140
|
+
And(_nnf(Not(n.left)), _nnf(n.right)))
|
|
141
|
+
if isinstance(n, Quantifier):
|
|
142
|
+
return Quantifier(n.type, n.variable, _nnf(n.formula))
|
|
143
|
+
raise TypeError(
|
|
144
|
+
f"to_nnf: unexpected node type {type(n).__name__}{_unsupported_hint(n)}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _nnf_neg(n: Node) -> Node:
|
|
148
|
+
"""Return the NNF of ¬n."""
|
|
149
|
+
if isinstance(n, Atom):
|
|
150
|
+
return Not(n)
|
|
151
|
+
if isinstance(n, Not):
|
|
152
|
+
return _nnf(n.formula) # ¬¬a → a
|
|
153
|
+
if isinstance(n, And):
|
|
154
|
+
return Or(_nnf_neg(n.left), _nnf_neg(n.right))
|
|
155
|
+
if isinstance(n, Or):
|
|
156
|
+
return And(_nnf_neg(n.left), _nnf_neg(n.right))
|
|
157
|
+
if isinstance(n, Implies): # ¬(a → b) ≡ a ∧ ¬b
|
|
158
|
+
return And(_nnf(n.left), _nnf_neg(n.right))
|
|
159
|
+
if isinstance(n, Iff): # ¬(a ↔ b) ≡ (a ∧ ¬b) ∨ (¬a ∧ b)
|
|
160
|
+
return Or(And(_nnf(n.left), _nnf_neg(n.right)),
|
|
161
|
+
And(_nnf_neg(n.left), _nnf(n.right)))
|
|
162
|
+
if isinstance(n, Xor): # ¬(a ⊕ b) ≡ a ↔ b
|
|
163
|
+
return _nnf(Iff(n.left, n.right))
|
|
164
|
+
if isinstance(n, Quantifier):
|
|
165
|
+
dual = "∃" if n.type in _FORALL else "∀"
|
|
166
|
+
return Quantifier(dual, n.variable, _nnf_neg(n.formula))
|
|
167
|
+
raise TypeError(
|
|
168
|
+
f"to_nnf: unexpected node type {type(n).__name__}{_unsupported_hint(n)}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Fresh-name plumbing
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _all_names(node: Node) -> set:
|
|
176
|
+
"""Collect every predicate, function, constant, and variable name in node."""
|
|
177
|
+
names = set()
|
|
178
|
+
for n in node.walk():
|
|
179
|
+
cls = type(n).__name__
|
|
180
|
+
if cls == "Atom":
|
|
181
|
+
names.add(n.predicate)
|
|
182
|
+
elif cls == "Function":
|
|
183
|
+
names.add(n.name)
|
|
184
|
+
elif cls in ("Variable", "Constant"):
|
|
185
|
+
names.add(n.name)
|
|
186
|
+
return names
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _gensym(base: str, counter: list, used: set) -> str:
|
|
190
|
+
"""Return a fresh name base+N not already in used; record it in used."""
|
|
191
|
+
while True:
|
|
192
|
+
name = f"{base}{counter[0]}"
|
|
193
|
+
counter[0] += 1
|
|
194
|
+
if name not in used:
|
|
195
|
+
used.add(name)
|
|
196
|
+
return name
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Prenex normal form
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def to_pnf(node: Node) -> Node:
|
|
204
|
+
"""Return the prenex normal form: quantifier prefix over a quantifier-free
|
|
205
|
+
NNF matrix, with all bound variables standardised apart."""
|
|
206
|
+
_reject_fuzzy(node, "to_pnf")
|
|
207
|
+
nnf = _nnf(to_fol(node))
|
|
208
|
+
std = _standardize(nnf, [0], _all_names(nnf))
|
|
209
|
+
prefix, matrix = _prenex_split(std)
|
|
210
|
+
for qtype, qvar in reversed(prefix):
|
|
211
|
+
matrix = Quantifier(qtype, qvar, matrix)
|
|
212
|
+
return matrix
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _standardize(n: Node, counter: list, used: set) -> Node:
|
|
216
|
+
"""Rename every bound variable to a globally unique fresh name."""
|
|
217
|
+
if isinstance(n, Quantifier):
|
|
218
|
+
fresh = Variable(_gensym("v", counter, used))
|
|
219
|
+
body = _rename(n.formula, n.variable, fresh)
|
|
220
|
+
return Quantifier(n.type, fresh, _standardize(body, counter, used))
|
|
221
|
+
if isinstance(n, (And, Or)):
|
|
222
|
+
return type(n)(_standardize(n.left, counter, used),
|
|
223
|
+
_standardize(n.right, counter, used))
|
|
224
|
+
if isinstance(n, Not):
|
|
225
|
+
return Not(_standardize(n.formula, counter, used))
|
|
226
|
+
return n # Atom
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _prenex_split(n: Node):
|
|
230
|
+
"""Split a standardised-apart NNF formula into (prefix, quantifier-free matrix).
|
|
231
|
+
|
|
232
|
+
prefix is a list of (quantifier_type, Variable) in outer-to-inner order.
|
|
233
|
+
Sound because bound variables are disjoint, so quantifiers hoist freely.
|
|
234
|
+
"""
|
|
235
|
+
if isinstance(n, Quantifier):
|
|
236
|
+
prefix, matrix = _prenex_split(n.formula)
|
|
237
|
+
return [(n.type, n.variable)] + prefix, matrix
|
|
238
|
+
if isinstance(n, (And, Or)):
|
|
239
|
+
pl, ml = _prenex_split(n.left)
|
|
240
|
+
pr, mr = _prenex_split(n.right)
|
|
241
|
+
return pl + pr, type(n)(ml, mr)
|
|
242
|
+
return [], n # Not(atom) or Atom
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
# Conjunctive normal form
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def to_cnf(node: Node) -> Node:
|
|
250
|
+
"""Return prenex form whose matrix is a conjunction of clauses. Equivalence-preserving."""
|
|
251
|
+
pnf = to_pnf(node)
|
|
252
|
+
prefix, matrix = _prenex_split(pnf)
|
|
253
|
+
cnf_matrix = _cnf(matrix)
|
|
254
|
+
for qtype, qvar in reversed(prefix):
|
|
255
|
+
cnf_matrix = Quantifier(qtype, qvar, cnf_matrix)
|
|
256
|
+
return cnf_matrix
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _cnf(n: Node) -> Node:
|
|
260
|
+
"""Distribute ∨ over ∧ in a quantifier-free NNF formula."""
|
|
261
|
+
if isinstance(n, (Atom, Not)):
|
|
262
|
+
return n
|
|
263
|
+
if isinstance(n, And):
|
|
264
|
+
return And(_cnf(n.left), _cnf(n.right))
|
|
265
|
+
if isinstance(n, Or):
|
|
266
|
+
return _distribute(_cnf(n.left), _cnf(n.right))
|
|
267
|
+
raise TypeError(f"to_cnf: unexpected node type {type(n).__name__}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _distribute(left: Node, right: Node) -> Node:
|
|
271
|
+
if isinstance(left, And):
|
|
272
|
+
return And(_distribute(left.left, right), _distribute(left.right, right))
|
|
273
|
+
if isinstance(right, And):
|
|
274
|
+
return And(_distribute(left, right.left), _distribute(left, right.right))
|
|
275
|
+
return Or(left, right)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Disjunctive normal form
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
def to_dnf(node: Node) -> Node:
|
|
283
|
+
"""Return prenex form whose matrix is a disjunction of conjunctive clauses.
|
|
284
|
+
|
|
285
|
+
The dual of :func:`to_cnf`: reuse the prenex prefix from ``to_pnf`` and
|
|
286
|
+
distribute ∧ over ∨ in the quantifier-free matrix (each conjunctive clause
|
|
287
|
+
is a conjunction of literals). Equivalence-preserving.
|
|
288
|
+
"""
|
|
289
|
+
pnf = to_pnf(node)
|
|
290
|
+
prefix, matrix = _prenex_split(pnf)
|
|
291
|
+
dnf_matrix = _dnf(matrix)
|
|
292
|
+
for qtype, qvar in reversed(prefix):
|
|
293
|
+
dnf_matrix = Quantifier(qtype, qvar, dnf_matrix)
|
|
294
|
+
return dnf_matrix
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _dnf(n: Node) -> Node:
|
|
298
|
+
"""Distribute ∧ over ∨ in a quantifier-free NNF formula (dual of _cnf)."""
|
|
299
|
+
if isinstance(n, (Atom, Not)):
|
|
300
|
+
return n
|
|
301
|
+
if isinstance(n, Or):
|
|
302
|
+
return Or(_dnf(n.left), _dnf(n.right))
|
|
303
|
+
if isinstance(n, And):
|
|
304
|
+
return _distribute_and(_dnf(n.left), _dnf(n.right))
|
|
305
|
+
raise TypeError(f"to_dnf: unexpected node type {type(n).__name__}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _distribute_and(left: Node, right: Node) -> Node:
|
|
309
|
+
"""Distribute ∧ over ∨ (dual of _distribute)."""
|
|
310
|
+
if isinstance(left, Or):
|
|
311
|
+
return Or(_distribute_and(left.left, right), _distribute_and(left.right, right))
|
|
312
|
+
if isinstance(right, Or):
|
|
313
|
+
return Or(_distribute_and(left, right.left), _distribute_and(left, right.right))
|
|
314
|
+
return And(left, right)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Tseitin (definitional) CNF
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
def to_tseitin_cnf(node: Node) -> Node:
|
|
322
|
+
"""Return an EQUISATISFIABLE CNF via the Tseitin/definitional encoding.
|
|
323
|
+
|
|
324
|
+
Unlike :func:`to_cnf`, this is *not* equivalence-preserving: a fresh 0-ary
|
|
325
|
+
auxiliary predicate Atom is introduced for every compound subformula of the
|
|
326
|
+
quantifier-free NNF matrix, and the defining biconditionals (``aux ↔
|
|
327
|
+
op(children)``) are expanded into clauses and conjoined with the unit clause
|
|
328
|
+
asserting the root auxiliary. This avoids the exponential blow-up of the
|
|
329
|
+
distributive :func:`to_cnf` while preserving satisfiability.
|
|
330
|
+
|
|
331
|
+
The auxiliaries are *0-ary* (propositional) definitions, so they only
|
|
332
|
+
faithfully capture a subformula whose truth value is fixed — i.e. the
|
|
333
|
+
propositional / ground case the encoding targets. They are **not** sound
|
|
334
|
+
inside a quantifier prefix: a single global ``aux`` cannot track a
|
|
335
|
+
subformula such as ``P(x)`` whose truth value varies over the domain, which
|
|
336
|
+
breaks equisatisfiability. This function therefore requires a quantifier-free
|
|
337
|
+
(after prenexing) input and raises :class:`ValueError` otherwise.
|
|
338
|
+
"""
|
|
339
|
+
pnf = to_pnf(node)
|
|
340
|
+
prefix, matrix = _prenex_split(pnf)
|
|
341
|
+
if prefix:
|
|
342
|
+
raise ValueError(
|
|
343
|
+
"to_tseitin_cnf only supports quantifier-free (propositional / "
|
|
344
|
+
"ground) formulas: the 0-ary auxiliary predicates are not sound "
|
|
345
|
+
"under a quantifier prefix. Eliminate quantifiers first (e.g. via "
|
|
346
|
+
"skolemize and grounding) or use to_cnf for an equivalence-"
|
|
347
|
+
"preserving prenex CNF."
|
|
348
|
+
)
|
|
349
|
+
used = _all_names(pnf)
|
|
350
|
+
counter = [0]
|
|
351
|
+
|
|
352
|
+
clauses = [] # accumulated CNF clauses (each a Node)
|
|
353
|
+
root = _tseitin(matrix, counter, used, clauses)
|
|
354
|
+
clauses.append(root) # unit clause asserting the root holds
|
|
355
|
+
|
|
356
|
+
cnf_matrix = clauses[0]
|
|
357
|
+
for clause in clauses[1:]:
|
|
358
|
+
cnf_matrix = And(cnf_matrix, clause)
|
|
359
|
+
return cnf_matrix
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _tseitin(n: Node, counter: list, used: set, clauses: list) -> Node:
|
|
363
|
+
"""Encode subformula n, appending its defining clauses; return its literal.
|
|
364
|
+
|
|
365
|
+
A literal (Atom or ¬Atom) is returned unchanged — it needs no auxiliary.
|
|
366
|
+
For each compound node a fresh 0-ary Atom ``aux`` is created and the clauses
|
|
367
|
+
expressing ``aux ↔ op(children)`` are appended to ``clauses``.
|
|
368
|
+
"""
|
|
369
|
+
if isinstance(n, Atom):
|
|
370
|
+
return n
|
|
371
|
+
if isinstance(n, Not):
|
|
372
|
+
# Inputs are NNF, so this wraps an atom: it is already a literal.
|
|
373
|
+
return n
|
|
374
|
+
|
|
375
|
+
if isinstance(n, And):
|
|
376
|
+
a = _tseitin(n.left, counter, used, clauses)
|
|
377
|
+
b = _tseitin(n.right, counter, used, clauses)
|
|
378
|
+
aux = Atom(_gensym("ts", counter, used), [])
|
|
379
|
+
# aux ↔ (a ∧ b):
|
|
380
|
+
# (¬aux ∨ a) ∧ (¬aux ∨ b) ∧ (¬a ∨ ¬b ∨ aux)
|
|
381
|
+
clauses.append(Or(Not(aux), a))
|
|
382
|
+
clauses.append(Or(Not(aux), b))
|
|
383
|
+
clauses.append(Or(Or(_neg(a), _neg(b)), aux))
|
|
384
|
+
return aux
|
|
385
|
+
|
|
386
|
+
if isinstance(n, Or):
|
|
387
|
+
a = _tseitin(n.left, counter, used, clauses)
|
|
388
|
+
b = _tseitin(n.right, counter, used, clauses)
|
|
389
|
+
aux = Atom(_gensym("ts", counter, used), [])
|
|
390
|
+
# aux ↔ (a ∨ b):
|
|
391
|
+
# (¬aux ∨ a ∨ b) ∧ (¬a ∨ aux) ∧ (¬b ∨ aux)
|
|
392
|
+
clauses.append(Or(Not(aux), Or(a, b)))
|
|
393
|
+
clauses.append(Or(_neg(a), aux))
|
|
394
|
+
clauses.append(Or(_neg(b), aux))
|
|
395
|
+
return aux
|
|
396
|
+
|
|
397
|
+
raise TypeError(f"to_tseitin_cnf: unexpected node type {type(n).__name__}")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _neg(lit: Node) -> Node:
|
|
401
|
+
"""Return the negation of a literal, collapsing ¬¬Atom back to Atom."""
|
|
402
|
+
if isinstance(lit, Not):
|
|
403
|
+
return lit.formula
|
|
404
|
+
return Not(lit)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
# Skolemization
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
def skolemize(node: Node) -> Node:
|
|
412
|
+
"""Return prenex NNF with existentials replaced by Skolem terms.
|
|
413
|
+
|
|
414
|
+
Each ∃-bound variable becomes a Skolem function of the universals in scope
|
|
415
|
+
(a Skolem constant if there are none); the existential quantifiers are
|
|
416
|
+
dropped and the universal prefix is retained. Satisfiability-preserving.
|
|
417
|
+
"""
|
|
418
|
+
pnf = to_pnf(node)
|
|
419
|
+
prefix, matrix = _prenex_split(pnf)
|
|
420
|
+
used = _all_names(pnf)
|
|
421
|
+
sk_counter = [0]
|
|
422
|
+
|
|
423
|
+
universals = [] # Variables of the ∀ seen so far, in order
|
|
424
|
+
kept_prefix = [] # the ∀ Variables to re-wrap
|
|
425
|
+
subst = {} # existential var name -> Skolem term
|
|
426
|
+
|
|
427
|
+
for qtype, qvar in prefix:
|
|
428
|
+
if qtype in _FORALL:
|
|
429
|
+
universals.append(qvar)
|
|
430
|
+
kept_prefix.append(qvar)
|
|
431
|
+
else:
|
|
432
|
+
fname = _gensym("sk", sk_counter, used)
|
|
433
|
+
term = Function(fname, list(universals)) if universals else Constant(fname)
|
|
434
|
+
subst[qvar.name] = term
|
|
435
|
+
|
|
436
|
+
result = matrix
|
|
437
|
+
for name, term in subst.items():
|
|
438
|
+
result = _subst_term(result, name, term)
|
|
439
|
+
for qvar in reversed(kept_prefix):
|
|
440
|
+
result = Quantifier("∀", qvar, result)
|
|
441
|
+
return result
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _subst_term(n: Node, varname: str, term: Node) -> Node:
|
|
445
|
+
"""Replace every free Variable(varname) with term inside a formula/term."""
|
|
446
|
+
if isinstance(n, Variable):
|
|
447
|
+
return term if n.name == varname else n
|
|
448
|
+
if isinstance(n, (Constant, Number)):
|
|
449
|
+
return n
|
|
450
|
+
if isinstance(n, Quantifier):
|
|
451
|
+
if n.variable.name == varname:
|
|
452
|
+
return n # shadowed (cannot happen after standardising apart, but safe)
|
|
453
|
+
return Quantifier(n.type, n.variable, _subst_term(n.formula, varname, term))
|
|
454
|
+
# Structural: Function, Atom, Not, And, Or — substitute into every child.
|
|
455
|
+
return n.map_children(lambda c: _subst_term(c, varname, term))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
# Horn check
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
def is_horn(node: Node) -> bool:
|
|
463
|
+
"""Return True iff node's clausal form consists of Horn clauses.
|
|
464
|
+
|
|
465
|
+
Syntactic/clausal definition: skolemise, drop the universal prefix, put the
|
|
466
|
+
matrix into CNF, split into clauses, and check that every clause has at most
|
|
467
|
+
one positive literal.
|
|
468
|
+
"""
|
|
469
|
+
sk = skolemize(node)
|
|
470
|
+
_, matrix = _prenex_split(sk)
|
|
471
|
+
cnf = _cnf(matrix)
|
|
472
|
+
for clause in _clauses(cnf):
|
|
473
|
+
if sum(1 for lit in clause if isinstance(lit, Atom)) > 1:
|
|
474
|
+
return False
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _conjuncts(n: Node) -> list:
|
|
479
|
+
if isinstance(n, And):
|
|
480
|
+
return _conjuncts(n.left) + _conjuncts(n.right)
|
|
481
|
+
return [n]
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _disjuncts(n: Node) -> list:
|
|
485
|
+
if isinstance(n, Or):
|
|
486
|
+
return _disjuncts(n.left) + _disjuncts(n.right)
|
|
487
|
+
return [n]
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _clauses(cnf: Node) -> list:
|
|
491
|
+
"""Split a CNF matrix into a list of clauses, each a list of literals."""
|
|
492
|
+
return [_disjuncts(c) for c in _conjuncts(cnf)]
|