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,358 @@
|
|
|
1
|
+
"""Third-order finite-model semantics: satisfaction where an argument can be a property.
|
|
2
|
+
|
|
3
|
+
:mod:`semantics.secondorder` quantifies over relations on the domain. This
|
|
4
|
+
evaluator adds the level above: a predicate whose ARGUMENT is a relation —
|
|
5
|
+
``Positive(G)``, ``Essence(G, x)``, ``Positive(λx. ¬G(x))`` — and quantification
|
|
6
|
+
over such predicates. It reuses :class:`semantics.tarski.Structure` and
|
|
7
|
+
:func:`semantics.tarski.term_value` unchanged; what changes is that an argument
|
|
8
|
+
slot may now be filled by a relation rather than an individual, so a "tuple in
|
|
9
|
+
the extension" may have relations among its components.
|
|
10
|
+
|
|
11
|
+
**Slots, not just arities.** At second order a bound predicate variable is fully
|
|
12
|
+
described by its arity. At third order it is not: ``Positive`` and ``G`` can both
|
|
13
|
+
have arity 1 and mean entirely different things, because ``G``'s slot holds an
|
|
14
|
+
individual and ``Positive``'s holds a property. What each slot holds is inferred
|
|
15
|
+
by :func:`unicode_logic_kit.fol.nodes.analyse_signatures` and threaded through the
|
|
16
|
+
evaluation, which is why this module's quantifier case enumerates over a
|
|
17
|
+
SIGNATURE where the second-order one enumerates over an arity.
|
|
18
|
+
|
|
19
|
+
**Cost.** An individual slot ranges over the ``n`` domain elements; a property
|
|
20
|
+
slot of arity ``j`` over the ``2 ** (n ** j)`` relations of that arity. A
|
|
21
|
+
predicate of a given signature is any subset of the product of its slots, so a
|
|
22
|
+
monadic third-order predicate over a domain of ``n`` has ``2 ** (2 ** n)``
|
|
23
|
+
interpretations: 16 for ``n = 2``, 256 for 3, 65 536 for 4, about 4·10⁹ for 5.
|
|
24
|
+
Small, then suddenly not — which is what :data:`MAX_INTERPRETATIONS` guards, by
|
|
25
|
+
raising a clear error rather than hanging.
|
|
26
|
+
|
|
27
|
+
**Scope.** Two-valued, classical, constant-domain. Łukasiewicz and modal nodes
|
|
28
|
+
are rejected by name, as in the second-order evaluator — a third-order MODAL
|
|
29
|
+
formula is not evaluated here but exported by
|
|
30
|
+
:mod:`unicode_logic_kit.hol.ho_modal`. λ-abstractions are NOT rejected here,
|
|
31
|
+
unlike at second order: in argument position a λ is precisely how an anonymous
|
|
32
|
+
property is written, and it is evaluated to its extension. A λ anywhere else
|
|
33
|
+
still has no reading.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from itertools import product
|
|
37
|
+
from typing import Any, FrozenSet, Iterable, List, Mapping, Optional, Tuple
|
|
38
|
+
|
|
39
|
+
from ..fol.nodes import (
|
|
40
|
+
Node,
|
|
41
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
42
|
+
SortedQuantifier, SecondOrderQuantifier,
|
|
43
|
+
Lambda, LambdaVar, PredicateTerm, Variable,
|
|
44
|
+
analyse_signatures,
|
|
45
|
+
)
|
|
46
|
+
from ..fol._ho_nodes import INDIVIDUAL, Signatures
|
|
47
|
+
from .tarski import (
|
|
48
|
+
Structure, term_value, _atom_value, _extend,
|
|
49
|
+
_FORALL, _EXISTS, _FUZZY_TYPES,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
#: A relation: a (frozen)set of argument tuples. A component of one of those
|
|
53
|
+
#: tuples is an individual at first/second order, and may be another relation at
|
|
54
|
+
#: third order — which is the whole difference.
|
|
55
|
+
Relation = FrozenSet[Tuple[Any, ...]]
|
|
56
|
+
PredBinding = Mapping[str, Relation]
|
|
57
|
+
|
|
58
|
+
#: Safety cap on one quantifier's enumeration. See the module docstring for how
|
|
59
|
+
#: fast the count grows; raise this attribute if you really mean to enumerate
|
|
60
|
+
#: more.
|
|
61
|
+
MAX_INTERPRETATIONS = 1 << 20 # ~1.05 million
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def slot_values(kind, domain: Tuple[Any, ...]) -> Tuple[Any, ...]:
|
|
65
|
+
"""Return everything one argument slot can hold.
|
|
66
|
+
|
|
67
|
+
``kind`` is :data:`unicode_logic_kit.fol._ho_nodes.INDIVIDUAL` — in which case
|
|
68
|
+
the answer is the domain — or ``("p", j)``, in which case it is every
|
|
69
|
+
``j``-ary relation on the domain, i.e. every subset of ``domain ** j``.
|
|
70
|
+
"""
|
|
71
|
+
if kind == INDIVIDUAL:
|
|
72
|
+
return domain
|
|
73
|
+
arity = kind[1]
|
|
74
|
+
base = list(product(domain, repeat=arity))
|
|
75
|
+
return tuple(
|
|
76
|
+
frozenset(base[i] for i in range(len(base)) if (mask >> i) & 1)
|
|
77
|
+
for mask in range(1 << len(base))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def all_interpretations(signature: Tuple[Any, ...],
|
|
82
|
+
domain: Tuple[Any, ...]) -> Iterable[Relation]:
|
|
83
|
+
"""Yield every interpretation of a predicate with the given argument ``signature``.
|
|
84
|
+
|
|
85
|
+
An interpretation is any subset of the product of the slots' value sets, so
|
|
86
|
+
the count is ``2 ** prod(len(slot_values(kind)) for kind in signature)``. An
|
|
87
|
+
empty signature (a propositional variable) has the single-element product
|
|
88
|
+
``{()}``, hence exactly the two interpretations false and true — matching
|
|
89
|
+
the second-order evaluator's arity-0 case.
|
|
90
|
+
"""
|
|
91
|
+
columns = [slot_values(kind, domain) for kind in signature]
|
|
92
|
+
base = list(product(*columns)) if columns else [()]
|
|
93
|
+
for mask in range(1 << len(base)):
|
|
94
|
+
yield frozenset(base[i] for i in range(len(base)) if (mask >> i) & 1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def interpretation_count(signature: Tuple[Any, ...], domain_size: int) -> int:
|
|
98
|
+
"""How many interpretations :func:`all_interpretations` would yield.
|
|
99
|
+
|
|
100
|
+
Computed without enumerating anything, so a caller (and the evaluator's own
|
|
101
|
+
guard) can refuse an impossible enumeration before starting it.
|
|
102
|
+
"""
|
|
103
|
+
width = 1
|
|
104
|
+
for kind in signature:
|
|
105
|
+
width *= (domain_size if kind == INDIVIDUAL
|
|
106
|
+
else 1 << (domain_size ** kind[1]))
|
|
107
|
+
return 1 << width
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def satisfies_to(formula: Node,
|
|
111
|
+
structure: Structure,
|
|
112
|
+
assignment: Optional[Mapping[str, Any]] = None,
|
|
113
|
+
pred_binding: Optional[PredBinding] = None,
|
|
114
|
+
signatures: Optional[Signatures] = None) -> bool:
|
|
115
|
+
"""Return whether ``structure`` satisfies ``formula`` (third-order Tarski).
|
|
116
|
+
|
|
117
|
+
``assignment`` maps object-variable names to individuals; ``pred_binding``
|
|
118
|
+
maps a bound predicate name to its current interpretation. ``signatures``
|
|
119
|
+
is the argument-slot analysis; it is computed from ``formula`` when omitted,
|
|
120
|
+
and threaded unchanged through the recursion so every occurrence of a symbol
|
|
121
|
+
is read at the same type.
|
|
122
|
+
|
|
123
|
+
The recursion differs from :func:`semantics.secondorder.satisfies_so` in
|
|
124
|
+
exactly two places, both about arguments:
|
|
125
|
+
|
|
126
|
+
- an **atom's** arguments are evaluated with :func:`argument_value`, so an
|
|
127
|
+
argument may be a :class:`~unicode_logic_kit.fol.nodes.PredicateTerm` or a
|
|
128
|
+
λ-abstraction and evaluate to a RELATION. When every argument is an
|
|
129
|
+
ordinary term the atom is handed to the first-order
|
|
130
|
+
:func:`semantics.tarski._atom_value` unchanged, so ``=`` / ``≠`` and the
|
|
131
|
+
arithmetic fallbacks behave exactly as they do at first order;
|
|
132
|
+
- a **predicate quantifier** enumerates over the bound symbol's SIGNATURE
|
|
133
|
+
rather than its arity, which is what makes ``∀Positive`` range over
|
|
134
|
+
predicates of properties instead of over relations on the domain.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: on a Łukasiewicz node, an unknown quantifier or node type,
|
|
138
|
+
or when a quantifier's enumeration would exceed
|
|
139
|
+
:data:`MAX_INTERPRETATIONS`.
|
|
140
|
+
NotImplementedError: on a modal node (use the Kripke evaluator, or
|
|
141
|
+
export a third-order modal formula with ``hol.ho_modal``), or on a
|
|
142
|
+
λ outside argument position.
|
|
143
|
+
"""
|
|
144
|
+
if assignment is None:
|
|
145
|
+
assignment = {}
|
|
146
|
+
if pred_binding is None:
|
|
147
|
+
pred_binding = {}
|
|
148
|
+
if signatures is None:
|
|
149
|
+
signatures = analyse_signatures([formula])
|
|
150
|
+
|
|
151
|
+
if isinstance(formula, Atom):
|
|
152
|
+
return _atom_truth(formula, structure, assignment, pred_binding, signatures)
|
|
153
|
+
|
|
154
|
+
if isinstance(formula, Not):
|
|
155
|
+
return not satisfies_to(formula.formula, structure, assignment,
|
|
156
|
+
pred_binding, signatures)
|
|
157
|
+
if isinstance(formula, And):
|
|
158
|
+
return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
|
|
159
|
+
and satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
|
|
160
|
+
if isinstance(formula, Or):
|
|
161
|
+
return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
|
|
162
|
+
or satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
|
|
163
|
+
if isinstance(formula, Xor):
|
|
164
|
+
return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
|
|
165
|
+
!= satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
|
|
166
|
+
if isinstance(formula, Implies):
|
|
167
|
+
return ((not satisfies_to(formula.left, structure, assignment, pred_binding, signatures))
|
|
168
|
+
or satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
|
|
169
|
+
if isinstance(formula, Iff):
|
|
170
|
+
return (satisfies_to(formula.left, structure, assignment, pred_binding, signatures)
|
|
171
|
+
== satisfies_to(formula.right, structure, assignment, pred_binding, signatures))
|
|
172
|
+
|
|
173
|
+
if isinstance(formula, Quantifier):
|
|
174
|
+
return _object_quantifier(formula.type, formula.variable.name,
|
|
175
|
+
structure.domain, formula.formula, structure,
|
|
176
|
+
assignment, pred_binding, signatures)
|
|
177
|
+
if isinstance(formula, SortedQuantifier):
|
|
178
|
+
return _object_quantifier(formula.type, formula.variable.name,
|
|
179
|
+
structure.sort_universe(formula.sort),
|
|
180
|
+
formula.formula, structure, assignment,
|
|
181
|
+
pred_binding, signatures)
|
|
182
|
+
if isinstance(formula, SecondOrderQuantifier):
|
|
183
|
+
return _predicate_quantifier(formula, structure, assignment,
|
|
184
|
+
pred_binding, signatures)
|
|
185
|
+
|
|
186
|
+
if isinstance(formula, _FUZZY_TYPES):
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Cannot evaluate Łukasiewicz node {type(formula).__name__} with the "
|
|
189
|
+
"two-valued third-order evaluator; use the fuzzy evaluator instead.")
|
|
190
|
+
if isinstance(formula, Lambda):
|
|
191
|
+
raise NotImplementedError(
|
|
192
|
+
"A λ-abstraction denotes a PROPERTY, not a truth value; it has a "
|
|
193
|
+
"reading here only in argument position (Positive(λx. ¬G(x))).")
|
|
194
|
+
if type(formula).__name__ in ("Box", "Diamond"):
|
|
195
|
+
raise NotImplementedError(
|
|
196
|
+
f"Modal node {type(formula).__name__} is out of scope for the "
|
|
197
|
+
"third-order evaluator; use the Kripke evaluator, or export the "
|
|
198
|
+
"formula with hol.ho_modal for a higher-order prover.")
|
|
199
|
+
raise ValueError(f"satisfies_to: unsupported node type {type(formula).__name__}.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _extension_in(structure: Structure, name: str, arity: int) -> Iterable[Tuple[Any, ...]]:
|
|
203
|
+
"""The extension of the predicate ``name/arity`` in ``structure``.
|
|
204
|
+
|
|
205
|
+
The structure's table for it. A missing table is the empty relation, matching
|
|
206
|
+
the first-order convention -- unless the name is a SORT of the structure: a sort
|
|
207
|
+
and the unary predicate of that name are one symbol, so a unary ``name`` that
|
|
208
|
+
the structure knows only as a sort is that sort's universe, one 1-tuple per
|
|
209
|
+
member. This is the reading :func:`semantics.tarski._atom_value` gives an atom
|
|
210
|
+
``S(t)``, and the laws a sort must satisfy are checked here as they are there
|
|
211
|
+
(an :class:`~unicode_logic_kit.semantics.tarski.IllegalStructureError` for an
|
|
212
|
+
empty sort, or one that disagrees with the predicate table of its name).
|
|
213
|
+
"""
|
|
214
|
+
extension = structure.predicates.get((name, arity))
|
|
215
|
+
if arity == 1 and name in structure.sorts:
|
|
216
|
+
universe = structure.sort_universe(name)
|
|
217
|
+
if extension is None:
|
|
218
|
+
return {(d,) for d in universe}
|
|
219
|
+
return extension if extension is not None else ()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def argument_value(node: Node, structure: Structure,
|
|
223
|
+
assignment: Mapping[str, Any],
|
|
224
|
+
pred_binding: PredBinding,
|
|
225
|
+
signatures: Signatures) -> Any:
|
|
226
|
+
"""Evaluate a node in ARGUMENT position — to an individual or to a relation.
|
|
227
|
+
|
|
228
|
+
A :class:`~unicode_logic_kit.fol.nodes.PredicateTerm` denotes the relation its
|
|
229
|
+
name stands for: the current binding if the name is bound, otherwise the
|
|
230
|
+
structure's table for it (a missing table is the empty relation, matching
|
|
231
|
+
the first-order convention, except that a unary name the structure knows as
|
|
232
|
+
a sort denotes that sort -- see :func:`_extension_in`). A λ-abstraction
|
|
233
|
+
denotes its EXTENSION, computed
|
|
234
|
+
by evaluating the body at every tuple of individuals its binders range over.
|
|
235
|
+
Anything else is an ordinary term.
|
|
236
|
+
"""
|
|
237
|
+
if isinstance(node, PredicateTerm):
|
|
238
|
+
if node.name in pred_binding:
|
|
239
|
+
return pred_binding[node.name]
|
|
240
|
+
arity = signatures.arity.get(node.name, 1)
|
|
241
|
+
return frozenset(_extension_in(structure, node.name, arity))
|
|
242
|
+
if isinstance(node, Lambda):
|
|
243
|
+
names: List[str] = []
|
|
244
|
+
body: Node = node
|
|
245
|
+
while isinstance(body, Lambda):
|
|
246
|
+
names.append(body.param.name)
|
|
247
|
+
body = body.body
|
|
248
|
+
body = _lambda_vars_as_variables(body)
|
|
249
|
+
extension = []
|
|
250
|
+
for values in product(structure.domain, repeat=len(names)):
|
|
251
|
+
local = dict(assignment)
|
|
252
|
+
local.update(zip(names, values))
|
|
253
|
+
if satisfies_to(body, structure, local, pred_binding, signatures):
|
|
254
|
+
extension.append(values)
|
|
255
|
+
return frozenset(extension)
|
|
256
|
+
return term_value(node, structure, assignment)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _lambda_vars_as_variables(node: Node) -> Node:
|
|
260
|
+
"""Turn every LambdaVar in ``node`` back into an ordinary object Variable.
|
|
261
|
+
|
|
262
|
+
The parser resolves a λ binder's occurrences to LambdaVar, which the
|
|
263
|
+
first-order term evaluator has no reading for -- correctly, since a
|
|
264
|
+
λ-abstraction is not a term it can evaluate. Here the binder IS being
|
|
265
|
+
interpreted: its variables are about to range over the domain through the
|
|
266
|
+
assignment, which is exactly what an object variable does. Converting them
|
|
267
|
+
is sound including under nesting, because an inner λ's own binder is
|
|
268
|
+
re-bound by name on the way in, so the shadowing the LambdaVars encoded is
|
|
269
|
+
reproduced by the assignment.
|
|
270
|
+
"""
|
|
271
|
+
if isinstance(node, LambdaVar):
|
|
272
|
+
return Variable(node.name)
|
|
273
|
+
return node.map_children(_lambda_vars_as_variables)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _atom_truth(atom: Atom, structure: Structure,
|
|
277
|
+
assignment: Mapping[str, Any], pred_binding: PredBinding,
|
|
278
|
+
signatures: Signatures) -> bool:
|
|
279
|
+
"""Truth of an atom, consulting ``pred_binding`` and allowing property arguments.
|
|
280
|
+
|
|
281
|
+
An atom whose arguments are all ordinary terms and whose predicate is not
|
|
282
|
+
currently bound is delegated to the first-order
|
|
283
|
+
:func:`semantics.tarski._atom_value` verbatim — so nothing about first-order
|
|
284
|
+
satisfaction is re-implemented, and ``=`` / ``≠`` are never treated as
|
|
285
|
+
bindable.
|
|
286
|
+
"""
|
|
287
|
+
higher_order = any(isinstance(a, (PredicateTerm, Lambda)) for a in atom.args)
|
|
288
|
+
if atom.predicate in pred_binding and atom.predicate not in ("=", "≠"):
|
|
289
|
+
values = tuple(argument_value(a, structure, assignment, pred_binding, signatures)
|
|
290
|
+
for a in atom.args)
|
|
291
|
+
return values in pred_binding[atom.predicate]
|
|
292
|
+
if not higher_order:
|
|
293
|
+
return _atom_value(atom, structure, assignment)
|
|
294
|
+
values = tuple(argument_value(a, structure, assignment, pred_binding, signatures)
|
|
295
|
+
for a in atom.args)
|
|
296
|
+
extension = _extension_in(structure, atom.predicate, len(atom.args))
|
|
297
|
+
return values in extension
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _object_quantifier(qtype: str, var_name: str, universe: Iterable[Any],
|
|
301
|
+
body: Node, structure: Structure,
|
|
302
|
+
assignment: Mapping[str, Any], pred_binding: PredBinding,
|
|
303
|
+
signatures: Signatures) -> bool:
|
|
304
|
+
"""Evaluate an object-level ∀/∃, threading the predicate binding and signatures."""
|
|
305
|
+
if qtype in _FORALL:
|
|
306
|
+
return all(satisfies_to(body, structure, _extend(assignment, var_name, d),
|
|
307
|
+
pred_binding, signatures) for d in universe)
|
|
308
|
+
if qtype in _EXISTS:
|
|
309
|
+
return any(satisfies_to(body, structure, _extend(assignment, var_name, d),
|
|
310
|
+
pred_binding, signatures) for d in universe)
|
|
311
|
+
raise ValueError(f"Unknown quantifier type: {qtype!r}")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _predicate_quantifier(node: SecondOrderQuantifier, structure: Structure,
|
|
315
|
+
assignment: Mapping[str, Any],
|
|
316
|
+
pred_binding: PredBinding,
|
|
317
|
+
signatures: Signatures) -> bool:
|
|
318
|
+
"""Evaluate ∀P / ∃P by enumerating every interpretation of P's SIGNATURE.
|
|
319
|
+
|
|
320
|
+
The signature — not the arity — is what decides the range: an arity-1 ``P``
|
|
321
|
+
whose slot holds an individual ranges over the ``2 ** n`` subsets of the
|
|
322
|
+
domain (the second-order case), while an arity-1 ``P`` whose slot holds a
|
|
323
|
+
monadic property ranges over the ``2 ** (2 ** n)`` sets of such properties.
|
|
324
|
+
Both fall out of the same enumeration.
|
|
325
|
+
"""
|
|
326
|
+
signature = signatures.slots.get(node.predicate)
|
|
327
|
+
if signature is None:
|
|
328
|
+
signature = tuple(INDIVIDUAL for _ in range(node.arity))
|
|
329
|
+
count = interpretation_count(signature, len(structure.domain))
|
|
330
|
+
if count > MAX_INTERPRETATIONS:
|
|
331
|
+
raise ValueError(
|
|
332
|
+
f"Predicate quantifier {node.type}{node.predicate} with signature "
|
|
333
|
+
f"{signature} over a {len(structure.domain)}-element domain would "
|
|
334
|
+
f"enumerate {count} interpretations, above MAX_INTERPRETATIONS = "
|
|
335
|
+
f"{MAX_INTERPRETATIONS}. Shrink the domain (or raise "
|
|
336
|
+
"thirdorder.MAX_INTERPRETATIONS).")
|
|
337
|
+
candidates = all_interpretations(signature, structure.domain)
|
|
338
|
+
if node.type in _FORALL:
|
|
339
|
+
return all(satisfies_to(node.formula, structure, assignment,
|
|
340
|
+
_extend(pred_binding, node.predicate, candidate),
|
|
341
|
+
signatures)
|
|
342
|
+
for candidate in candidates)
|
|
343
|
+
if node.type in _EXISTS:
|
|
344
|
+
return any(satisfies_to(node.formula, structure, assignment,
|
|
345
|
+
_extend(pred_binding, node.predicate, candidate),
|
|
346
|
+
signatures)
|
|
347
|
+
for candidate in candidates)
|
|
348
|
+
raise ValueError(f"Unknown predicate quantifier type: {node.type!r}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def holds_to(formula: Node, structure: Structure) -> bool:
|
|
352
|
+
"""Convenience: ``satisfies_to(formula, structure)`` for a closed formula.
|
|
353
|
+
|
|
354
|
+
Reads as "structure satisfies the third-order sentence": the empty
|
|
355
|
+
assignment and empty predicate binding are what a formula with no free
|
|
356
|
+
object or predicate variables needs.
|
|
357
|
+
"""
|
|
358
|
+
return satisfies_to(formula, structure)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Continuous t-norms for the fuzzy (BL / Hájek basic-logic) family.
|
|
2
|
+
|
|
3
|
+
A *t-norm* ``T`` fixes the semantics of the **strong** fuzzy connectives over the
|
|
4
|
+
real interval ``[0, 1]``:
|
|
5
|
+
|
|
6
|
+
- strong conjunction ``⊗`` is ``T`` itself;
|
|
7
|
+
- strong disjunction ``⊕`` is the dual *t-conorm* ``S(x, y) = 1 − T(1−x, 1−y)``;
|
|
8
|
+
- implication ``→`` is the *residuum* ``R(x, y) = sup {z : T(x, z) ≤ y}``;
|
|
9
|
+
- negation ``¬x`` is ``R(x, 0)``;
|
|
10
|
+
- biconditional ``↔`` is ``min(R(x, y), R(y, x))``.
|
|
11
|
+
|
|
12
|
+
The *weak* connectives ``∧`` / ``∨`` are always ``min`` / ``max`` (the lattice meet /
|
|
13
|
+
join), independent of the t-norm, and so are the quantifiers ``∀`` (infimum) / ``∃``
|
|
14
|
+
(supremum). Three continuous t-norms — whose ordinal sums generate every
|
|
15
|
+
continuous t-norm (the Mostert–Shields theorem) — ship here:
|
|
16
|
+
|
|
17
|
+
- **Łukasiewicz** ``T(x, y) = max(0, x + y − 1)`` (the default; involutive negation);
|
|
18
|
+
- **Gödel** ``T(x, y) = min(x, y)`` (idempotent; a relative pseudo-complement negation);
|
|
19
|
+
- **product** ``T(x, y) = x · y`` (strict; Goguen residuum ``y / x``).
|
|
20
|
+
|
|
21
|
+
Public API: :class:`TNorm`, the instances :data:`LUKASIEWICZ`, :data:`GODEL`,
|
|
22
|
+
:data:`PRODUCT`, the registry :data:`TNORMS`, and :func:`get_tnorm`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Callable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class TNorm:
|
|
31
|
+
"""A t-norm and the strong connective operations it induces over ``[0, 1]``.
|
|
32
|
+
|
|
33
|
+
``conj`` / ``disj`` / ``impl`` / ``neg`` are the strong ⊗ / ⊕ / → / ¬; ``equiv``
|
|
34
|
+
is derived as ``min(x→y, y→x)``. (The weak ∧/∨ are min/max regardless and live
|
|
35
|
+
in the evaluator, not here.)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
conj: Callable[[float, float], float]
|
|
40
|
+
disj: Callable[[float, float], float]
|
|
41
|
+
impl: Callable[[float, float], float]
|
|
42
|
+
neg: Callable[[float], float]
|
|
43
|
+
|
|
44
|
+
def equiv(self, x: float, y: float) -> float:
|
|
45
|
+
"""Residuated biconditional ``min(x → y, y → x)``."""
|
|
46
|
+
return min(self.impl(x, y), self.impl(y, x))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
LUKASIEWICZ = TNorm(
|
|
50
|
+
"lukasiewicz",
|
|
51
|
+
conj=lambda x, y: max(0.0, x + y - 1.0),
|
|
52
|
+
disj=lambda x, y: min(1.0, x + y),
|
|
53
|
+
impl=lambda x, y: min(1.0, 1.0 - x + y),
|
|
54
|
+
neg=lambda x: 1.0 - x,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
GODEL = TNorm(
|
|
58
|
+
"godel",
|
|
59
|
+
conj=lambda x, y: min(x, y),
|
|
60
|
+
disj=lambda x, y: max(x, y),
|
|
61
|
+
impl=lambda x, y: 1.0 if x <= y else y,
|
|
62
|
+
neg=lambda x: 1.0 if x <= 0.0 else 0.0,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
PRODUCT = TNorm(
|
|
66
|
+
"product",
|
|
67
|
+
conj=lambda x, y: x * y,
|
|
68
|
+
disj=lambda x, y: x + y - x * y,
|
|
69
|
+
impl=lambda x, y: 1.0 if x <= y else y / x,
|
|
70
|
+
neg=lambda x: 1.0 if x <= 0.0 else 0.0,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
#: The three built-in t-norms by name (``"lukasiewicz"``, ``"godel"``,
|
|
74
|
+
#: ``"product"``) — what :func:`get_tnorm` looks up and what the fuzzy
|
|
75
|
+
#: evaluator accepts as a ``tnorm=`` argument.
|
|
76
|
+
TNORMS = {t.name: t for t in (LUKASIEWICZ, GODEL, PRODUCT)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_tnorm(name: str) -> TNorm:
|
|
80
|
+
"""Return the :class:`TNorm` for ``name`` (``"lukasiewicz"`` / ``"godel"`` / ``"product"``)."""
|
|
81
|
+
try:
|
|
82
|
+
return TNORMS[name]
|
|
83
|
+
except KeyError:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"unknown t-norm {name!r}; use one of {sorted(TNORMS)}.") from None
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Truth tables for propositional formulas — classical, Kleene K3, and Priest LP.
|
|
2
|
+
|
|
3
|
+
A truth table enumerates every assignment of truth values to the formula's atoms and
|
|
4
|
+
records the formula's value (and whether that value is *designated*) under each. The
|
|
5
|
+
three logics share the strong-Kleene connective tables and differ only in the value
|
|
6
|
+
set and the designated values:
|
|
7
|
+
|
|
8
|
+
- **classical** — values ``{0, 1}``; designated ``{1}``.
|
|
9
|
+
- **K3** (strong Kleene) — values ``{0, ½, 1}``; designated ``{1}``.
|
|
10
|
+
- **LP** (Priest) — values ``{0, ½, 1}``; designated ``{½, 1}`` (paraconsistent).
|
|
11
|
+
|
|
12
|
+
Restricted to ``{0, 1}`` the strong-Kleene tables coincide with the classical ones,
|
|
13
|
+
so all three reuse :func:`unicode_logic_kit.semantics.manyvalued.kleene_value`.
|
|
14
|
+
|
|
15
|
+
Each distinct atom (by surface form — ``P`` and ``P(a)`` are different columns) is a
|
|
16
|
+
propositional variable, except the nullary atoms ``$true`` and ``$false`` (``⊤`` and
|
|
17
|
+
``⊥``): those are the constants truth and falsity, they get no column, and their
|
|
18
|
+
value is ``1`` and ``0`` in every row. Quantified formulas have no finite truth table
|
|
19
|
+
and are rejected. A column is named by the text the atom prints as, so two different
|
|
20
|
+
atoms that print alike (the numeral ``1`` and a constant named ``1``, a free variable
|
|
21
|
+
``x`` and a constant named ``x``) would be one column: such a formula is refused by name,
|
|
22
|
+
and so is a sorted constant (``alice:Human``), whose sort is a fact that a truth table
|
|
23
|
+
has no way to state.
|
|
24
|
+
|
|
25
|
+
Public API: :func:`truth_table`, :class:`TruthTable`, and the convenience predicates
|
|
26
|
+
:func:`is_tautology`, :func:`is_contradiction`, :func:`is_satisfiable_tt`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import html
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from itertools import product
|
|
32
|
+
from typing import Dict, List, Tuple
|
|
33
|
+
|
|
34
|
+
from ..fol.nodes import Node, Atom, Quantifier, SortedQuantifier
|
|
35
|
+
from ..fol._atom_keys import AtomKeys, atom_key
|
|
36
|
+
from ..fol._so_nodes import SecondOrderQuantifier
|
|
37
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
38
|
+
from .manyvalued import _kleene_value, DESIGNATED
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Value sets and designated sets per logic (classical added to the K3/LP table).
|
|
42
|
+
_VALUES = {
|
|
43
|
+
"classical": (1.0, 0.0),
|
|
44
|
+
"K3": (1.0, 0.5, 0.0),
|
|
45
|
+
"LP": (1.0, 0.5, 0.0),
|
|
46
|
+
}
|
|
47
|
+
_DESIGNATED = {"classical": frozenset({1.0}), **DESIGNATED}
|
|
48
|
+
|
|
49
|
+
# Display glyphs.
|
|
50
|
+
_GLYPH_BOOL = {1.0: "T", 0.0: "F"}
|
|
51
|
+
_GLYPH_MANY = {1.0: "1", 0.5: "½", 0.0: "0"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _collect_atoms(formula: Node) -> List[str]:
|
|
55
|
+
"""Return the distinct atom surface-forms in ``formula``, in first-occurrence order.
|
|
56
|
+
|
|
57
|
+
Raises ValueError on a quantified formula (no finite truth table exists).
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(formula, (Quantifier, SortedQuantifier, SecondOrderQuantifier)):
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"truth_table: quantified formulas have no finite truth table; "
|
|
62
|
+
"use the finite model finder or an evaluator instead."
|
|
63
|
+
)
|
|
64
|
+
order: List[str] = []
|
|
65
|
+
seen = set()
|
|
66
|
+
|
|
67
|
+
def walk(node: Node) -> None:
|
|
68
|
+
if isinstance(node, (Quantifier, SortedQuantifier, SecondOrderQuantifier)):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"truth_table: quantified formulas have no finite truth table."
|
|
71
|
+
)
|
|
72
|
+
if isinstance(node, Atom):
|
|
73
|
+
if _truth_value(node) is not None:
|
|
74
|
+
return # `$true` / `$false` are constants, not columns
|
|
75
|
+
key = atom_key(node)
|
|
76
|
+
if key not in seen:
|
|
77
|
+
seen.add(key)
|
|
78
|
+
order.append(key)
|
|
79
|
+
return
|
|
80
|
+
for child in node._child_nodes():
|
|
81
|
+
walk(child)
|
|
82
|
+
|
|
83
|
+
walk(formula)
|
|
84
|
+
return order
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class TruthTable:
|
|
89
|
+
"""A computed truth table: atom columns, value rows, and the designated flags.
|
|
90
|
+
|
|
91
|
+
``atoms`` are the column headers (atom surface-forms). ``rows`` is a tuple of
|
|
92
|
+
``(assignment, value, designated)`` where ``assignment`` is the tuple of atom
|
|
93
|
+
values aligned with ``atoms``, ``value`` is the formula's value, and
|
|
94
|
+
``designated`` says whether ``value`` is designated under ``logic``.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
formula: Node
|
|
98
|
+
atoms: Tuple[str, ...]
|
|
99
|
+
rows: Tuple[Tuple[Tuple[float, ...], float, bool], ...]
|
|
100
|
+
logic: str
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def is_tautology(self) -> bool:
|
|
104
|
+
"""True iff the formula is designated under every assignment."""
|
|
105
|
+
return all(d for _, _, d in self.rows)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_contradiction(self) -> bool:
|
|
109
|
+
"""True iff the formula is designated under no assignment."""
|
|
110
|
+
return not any(d for _, _, d in self.rows)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_satisfiable(self) -> bool:
|
|
114
|
+
"""True iff the formula is designated under some assignment."""
|
|
115
|
+
return any(d for _, _, d in self.rows)
|
|
116
|
+
|
|
117
|
+
def _rows_and_glyphs(self) -> Tuple[List[str], Dict[float, str]]:
|
|
118
|
+
"""Column headers (atoms + the formula) and the value-to-glyph map for ``logic``.
|
|
119
|
+
|
|
120
|
+
Shared by :meth:`render` (Markdown) and :meth:`_repr_html_` (HTML) so the
|
|
121
|
+
two renderers walk ``self.rows`` with the same headers and the same glyphs
|
|
122
|
+
and can never quietly disagree.
|
|
123
|
+
"""
|
|
124
|
+
glyph = _GLYPH_BOOL if self.logic == "classical" else _GLYPH_MANY
|
|
125
|
+
head = list(self.atoms) + [self.formula.to_unicode_str()]
|
|
126
|
+
return head, glyph
|
|
127
|
+
|
|
128
|
+
def render(self) -> str:
|
|
129
|
+
"""Render the truth table as a GitHub-flavoured Markdown table."""
|
|
130
|
+
head, glyph = self._rows_and_glyphs()
|
|
131
|
+
lines = ["| " + " | ".join(head) + " |",
|
|
132
|
+
"|" + "|".join(["---"] * len(head)) + "|"]
|
|
133
|
+
for assignment, value, _ in self.rows:
|
|
134
|
+
cells = [glyph[v] for v in assignment] + [glyph[value]]
|
|
135
|
+
lines.append("| " + " | ".join(cells) + " |")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
return self.render()
|
|
140
|
+
|
|
141
|
+
def _repr_html_(self) -> str:
|
|
142
|
+
"""Jupyter/IPython rich-display hook: the truth table as an HTML ``<table>``.
|
|
143
|
+
|
|
144
|
+
Built directly from ``self.atoms``/``self.rows`` via :meth:`_rows_and_glyphs`
|
|
145
|
+
— NOT by round-tripping ``render()``'s Markdown through a Markdown-to-HTML
|
|
146
|
+
parser, since no such parser is a dependency of this kit (and none is added
|
|
147
|
+
for this). Column/atom labels come from user-chosen predicate and constant
|
|
148
|
+
names, so they are HTML-escaped; the glyphs themselves are always one of the
|
|
149
|
+
fixed strings in ``_GLYPH_BOOL``/``_GLYPH_MANY`` and need no escaping.
|
|
150
|
+
"""
|
|
151
|
+
head, glyph = self._rows_and_glyphs()
|
|
152
|
+
header_cells = "".join(f"<th>{html.escape(h)}</th>" for h in head)
|
|
153
|
+
body_rows = []
|
|
154
|
+
for assignment, value, _ in self.rows:
|
|
155
|
+
cells = [glyph[v] for v in assignment] + [glyph[value]]
|
|
156
|
+
row_html = "".join(f"<td>{c}</td>" for c in cells)
|
|
157
|
+
body_rows.append(f"<tr>{row_html}</tr>")
|
|
158
|
+
return (f"<table><thead><tr>{header_cells}</tr></thead>"
|
|
159
|
+
f"<tbody>{''.join(body_rows)}</tbody></table>")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def truth_table(formula: Node, logic: str = "classical") -> TruthTable:
|
|
163
|
+
"""Compute the :class:`TruthTable` of a propositional ``formula`` under ``logic``.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
formula: a quantifier-free formula; each distinct atom is a propositional
|
|
167
|
+
variable.
|
|
168
|
+
logic: ``"classical"`` (values {0,1}), ``"K3"`` or ``"LP"`` (values {0,½,1}).
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: on an unknown logic or a quantified formula.
|
|
172
|
+
NotImplementedError: a sorted constant, or two different atoms that print alike.
|
|
173
|
+
"""
|
|
174
|
+
if logic not in _VALUES:
|
|
175
|
+
raise ValueError(f"truth_table: unknown logic {logic!r} "
|
|
176
|
+
f"(use 'classical', 'K3', or 'LP').")
|
|
177
|
+
atoms = _collect_atoms(formula)
|
|
178
|
+
AtomKeys("truth_table", "refuse").letters([formula])
|
|
179
|
+
values = _VALUES[logic]
|
|
180
|
+
designated = _DESIGNATED[logic]
|
|
181
|
+
rows = []
|
|
182
|
+
for assignment in product(values, repeat=len(atoms)):
|
|
183
|
+
valuation: Dict[str, float] = dict(zip(atoms, assignment))
|
|
184
|
+
value = _kleene_value(formula, valuation, None, None)
|
|
185
|
+
rows.append((assignment, value, value in designated))
|
|
186
|
+
return TruthTable(formula, tuple(atoms), tuple(rows), logic)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_tautology(formula: Node, logic: str = "classical") -> bool:
|
|
190
|
+
"""True iff ``formula`` is designated under every assignment (a logical truth)."""
|
|
191
|
+
return truth_table(formula, logic).is_tautology
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_contradiction(formula: Node, logic: str = "classical") -> bool:
|
|
195
|
+
"""True iff ``formula`` is designated under no assignment."""
|
|
196
|
+
return truth_table(formula, logic).is_contradiction
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def is_satisfiable_tt(formula: Node, logic: str = "classical") -> bool:
|
|
200
|
+
"""True iff ``formula`` is designated under some assignment."""
|
|
201
|
+
return truth_table(formula, logic).is_satisfiable
|