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,741 @@
|
|
|
1
|
+
"""Bounded enumeration of finite Kripke models — a semantic refutation oracle for
|
|
2
|
+
the modal fragment the labelled tableau cannot decide.
|
|
3
|
+
|
|
4
|
+
:mod:`unicode_logic_kit.atp.modal_tableau` has no proof rule for the temporal
|
|
5
|
+
*closure* operators (Always ``Ⓖ`` / Eventually ``Ⓕ`` / Until ``Ⓤ`` /
|
|
6
|
+
Historically ``⒣`` / Once ``⒫`` / Previous ``⒴`` / Since ``⒮`` — see
|
|
7
|
+
``modal_tableau._TEMPORAL_CLOSURE``): it marks a labelled formula built from one
|
|
8
|
+
of them INERT and reports ``"unknown"`` for any branch that only stays open
|
|
9
|
+
because of it. The quantified-modal-logic route (``fol.qml``) is sound but
|
|
10
|
+
proof-only (Z3 over the standard translation never REFUTES). Net effect: an
|
|
11
|
+
INVALID temporal formula such as ``Ⓕ P → P`` stays "unknown" — nothing in the
|
|
12
|
+
kit ever refutes it.
|
|
13
|
+
|
|
14
|
+
This module closes that gap for **refutation only**, the same way
|
|
15
|
+
:mod:`unicode_logic_kit.semantics.modelfinder` closes the analogous gap for plain
|
|
16
|
+
FOL: it exhaustively enumerates finite :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`\\ s
|
|
17
|
+
of increasing world-count and hands each one to the EXISTING, already-tested
|
|
18
|
+
evaluator, :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`, as an
|
|
19
|
+
oracle. Soundness is by construction: a returned countermodel is one
|
|
20
|
+
``satisfies_modal`` itself confirms falsifies the formula at world 0, so this
|
|
21
|
+
module contains no new semantic rule that could disagree with the one every
|
|
22
|
+
other Kripke-based route in the kit already relies on.
|
|
23
|
+
|
|
24
|
+
**What is enumerated.** For each relation family the formula actually uses
|
|
25
|
+
(``"alethic"`` for Box/Diamond, ``"K:"+agent`` / ``"B:"+agent`` /
|
|
26
|
+
``"Say:"+agent`` / ``"Want:"+agent`` for Knows/Believes/Says/Wants,
|
|
27
|
+
``"deontic"`` for Obligatory/Permitted, ``"temporal"`` for every temporal
|
|
28
|
+
operator — the exact naming convention documented in
|
|
29
|
+
:mod:`unicode_logic_kit.semantics.kripke`), every relation over ``{0, …, n-1}``
|
|
30
|
+
satisfying that family's frame condition is enumerated (the SAME frame-name
|
|
31
|
+
vocabulary as :mod:`unicode_logic_kit.atp.modal_tableau`: K/T/D/KD/B/KB/K4/K45/
|
|
32
|
+
S4/S5/KD45 for ``frame=``, plus per-family overrides via ``systems=``), crossed
|
|
33
|
+
with every valuation of the formula's ground atoms over those worlds. ``n``
|
|
34
|
+
ranges over ``1 .. max_worlds``. Each candidate model is checked with
|
|
35
|
+
``satisfies_modal(formula, model, 0)``; the first candidate where it comes back
|
|
36
|
+
``False`` is returned as the countermodel.
|
|
37
|
+
|
|
38
|
+
**Many-sorted ground formulas.** ``satisfies_modal`` relativizes a formula with a
|
|
39
|
+
sorted constant (``Mortal(socrates:Human)``) before it reads an atom, so the atom
|
|
40
|
+
it looks up is ``Mortal(socrates)``; the enumerator relativizes first too, and
|
|
41
|
+
varies THAT key. A sorted constant ``c:S`` is an element of ``S`` at every world
|
|
42
|
+
(``semantics.kripke``'s module docstring), so the guard atom ``S(c)`` is not
|
|
43
|
+
varied: it is true at every world of every candidate, and a countermodel this
|
|
44
|
+
search returns is therefore a model the many-sorted routes (``qml_is_valid``,
|
|
45
|
+
``api.prove``) would consider. A sorted QUANTIFIER still makes ``satisfies_modal``
|
|
46
|
+
ask for object domains, which these propositional models do not carry, and is
|
|
47
|
+
reported ``unsupported``.
|
|
48
|
+
|
|
49
|
+
**Three-way honesty, not two.** A search that finds no countermodel does NOT
|
|
50
|
+
mean the formula is valid — it means one of two different things, and
|
|
51
|
+
:class:`EnumSearchResult` keeps them apart instead of collapsing them into a
|
|
52
|
+
single ``None``:
|
|
53
|
+
|
|
54
|
+
- ``exhausted=True`` — EVERY candidate up to ``max_worlds`` was checked and
|
|
55
|
+
none refuted the formula. This is a genuine, documentable statement ("no
|
|
56
|
+
countermodel with ≤ ``max_worlds`` worlds, under this frame, exists"), but it
|
|
57
|
+
is still **not a validity proof**: modal logic has no small-model property
|
|
58
|
+
bounding countermodels to 3 worlds in general, so a larger, unexplored model
|
|
59
|
+
might still refute the formula.
|
|
60
|
+
- ``exhausted=False`` (with ``model=None``) — the ``max_models`` candidate
|
|
61
|
+
budget ran out before the ``max_worlds`` search space was fully covered, or
|
|
62
|
+
``max_atoms`` rejected the formula up front. Weaker than "exhausted": the
|
|
63
|
+
small worlds were not even fully explored.
|
|
64
|
+
- ``timed_out=True`` (with ``model=None``) — the wall-clock ``timeout`` ran out
|
|
65
|
+
first. The clock is read before every candidate model, while the relations
|
|
66
|
+
and valuations of a world count are built, and while the next world count's
|
|
67
|
+
relations are enumerated, so a search ends within the cost of one candidate
|
|
68
|
+
(or of 256 edge sets of the relation enumeration) of its deadline however
|
|
69
|
+
large the space is; a single evaluation of the formula in one model is not
|
|
70
|
+
interrupted.
|
|
71
|
+
|
|
72
|
+
A formula outside the propositional/ground modal fragment ``satisfies_modal``
|
|
73
|
+
itself understands (a first-order quantifier with no per-world domain, an
|
|
74
|
+
unassigned hybrid nominal, a Lewis counterfactual, …) makes the evaluator raise
|
|
75
|
+
— caught here and reported as ``unsupported`` rather than silently skipped or
|
|
76
|
+
misreported as a bound. So is a formula in which two different atoms print alike (the
|
|
77
|
+
numeral ``1`` and a constant named ``1``, a free variable ``x`` and a constant named ``x``)
|
|
78
|
+
or two different agents are named alike: a valuation and a relation family are keyed by the
|
|
79
|
+
written form, so the pair would be ONE key of every candidate and "no countermodel" would be
|
|
80
|
+
said of a formula that has one.
|
|
81
|
+
|
|
82
|
+
**Determinism.** Every enumeration order (relations by bitmask over a
|
|
83
|
+
fixed, sorted pair list; valuations by bitmask over a fixed, sorted atom list;
|
|
84
|
+
worlds by increasing count) is fixed and free of hashing/set-iteration
|
|
85
|
+
nondeterminism, so two calls with identical arguments return bit-for-bit the
|
|
86
|
+
same model.
|
|
87
|
+
|
|
88
|
+
**Cost.** The candidate space is
|
|
89
|
+
``sum_{n=1..max_worlds} (worlds^worlds-per-family-relation-count) * 2^(atoms * n)``
|
|
90
|
+
— it grows fast in both the number of distinct relation families the formula
|
|
91
|
+
mixes and its atom count. ``max_atoms`` and ``max_models`` are the two knobs
|
|
92
|
+
that keep a pathological formula from hanging; both give an honest
|
|
93
|
+
``exhausted=False`` rather than either blocking forever or answering wrong. The
|
|
94
|
+
third, ``timeout`` (milliseconds), is the only one that bounds the TIME rather
|
|
95
|
+
than the size of the search; it gives ``timed_out=True``.
|
|
96
|
+
|
|
97
|
+
Public API: :class:`EnumSearchResult`, :func:`modal_enum_search`,
|
|
98
|
+
:func:`modal_enum_countermodel`, :class:`KripkeEnumBackend`, plus the witness
|
|
99
|
+
converters :func:`kripke_model_to_dict` / :func:`kripke_model_from_dict`
|
|
100
|
+
(the ``"data"`` payload of every ``"kripke"`` witness dict).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
import itertools
|
|
104
|
+
from dataclasses import dataclass
|
|
105
|
+
from functools import lru_cache
|
|
106
|
+
from typing import Dict, FrozenSet, Optional, Sequence, Tuple
|
|
107
|
+
|
|
108
|
+
from .._deadline import instant as _instant, passed as _passed
|
|
109
|
+
from ..fol.nodes import (
|
|
110
|
+
Node, Atom,
|
|
111
|
+
Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
|
|
112
|
+
Next, Always, Eventually, Until, Historically, Once, Previous, Since,
|
|
113
|
+
EverybodyKnows, DistributedKnowledge, CommonKnowledge,
|
|
114
|
+
sort_axioms, sort_membership_axioms,
|
|
115
|
+
)
|
|
116
|
+
from ..fol._atom_keys import AtomKeys, refuse_alike_agents
|
|
117
|
+
from ..fol._msfl_nodes import key_text
|
|
118
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
119
|
+
from ..semantics.kripke import KripkeModel, satisfies_modal
|
|
120
|
+
from ..fol.frames import (
|
|
121
|
+
FRAMES as _FRAMES, resolve_frame, holds_on_finite_frame,
|
|
122
|
+
)
|
|
123
|
+
from .protocol import ProverBackend, Verdict, REFUTED, UNKNOWN
|
|
124
|
+
|
|
125
|
+
# Relation-name convention — must match semantics.kripke.KripkeModel exactly
|
|
126
|
+
# (see that module's docstring, "Relation-name convention").
|
|
127
|
+
_ALETHIC = "alethic"
|
|
128
|
+
_DEONTIC = "deontic"
|
|
129
|
+
_TEMPORAL = "temporal"
|
|
130
|
+
_KNOWS_PREFIX = "K:"
|
|
131
|
+
_BELIEVES_PREFIX = "B:"
|
|
132
|
+
_SAYS_PREFIX = "Say:"
|
|
133
|
+
_WANTS_PREFIX = "Want:"
|
|
134
|
+
|
|
135
|
+
#: Node types whose satisfaction is defined over the "temporal" relation.
|
|
136
|
+
_TEMPORAL_TYPES = (Next, Always, Eventually, Until, Historically, Once, Previous, Since)
|
|
137
|
+
|
|
138
|
+
#: Frame-condition-catching exceptions from satisfies_modal — anything here on
|
|
139
|
+
#: a candidate means the FORMULA (not the candidate model) is out of scope for
|
|
140
|
+
#: this propositional/ground enumerator; see the module docstring's "A formula
|
|
141
|
+
#: outside the propositional/ground modal fragment" paragraph.
|
|
142
|
+
_UNSUPPORTED_EXC = (NotImplementedError, ValueError, TypeError, KeyError)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _agent_key(agent: Node) -> str:
|
|
146
|
+
"""Relation-key suffix for an epistemic/doxastic/assertive/bouletic agent term.
|
|
147
|
+
|
|
148
|
+
Mirrors the identically-named private helper in ``semantics.kripke`` and
|
|
149
|
+
``atp.modal_tableau`` (each module keeps its own copy rather than share a
|
|
150
|
+
private cross-module import for a two-line function): the agent's
|
|
151
|
+
``.name`` if it has one (a Constant or Variable), else its rendered form.
|
|
152
|
+
"""
|
|
153
|
+
return getattr(agent, "name", None) or key_text(agent)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _collect(formula: Node) -> Tuple[Tuple[str, ...], Tuple[str, ...]]:
|
|
157
|
+
"""Scan ``formula`` for its ground-atom keys and the relation families it uses.
|
|
158
|
+
|
|
159
|
+
Returns ``(atoms, families)``, both sorted tuples (deterministic order):
|
|
160
|
+
``atoms`` are the ``key_text`` keys (every constant written by its bare name) the
|
|
161
|
+
valuation must cover;
|
|
162
|
+
``families`` are the relation NAMES (in the ``semantics.kripke`` convention)
|
|
163
|
+
the enumerator must build a relation for — one entry per distinct alethic /
|
|
164
|
+
epistemic-per-agent / doxastic-per-agent / assertive-per-agent /
|
|
165
|
+
bouletic-per-agent / deontic / temporal operator family actually present.
|
|
166
|
+
A formula with no modal operators at all yields an empty ``families``
|
|
167
|
+
tuple (the propositional case: only world 0's valuation matters).
|
|
168
|
+
"""
|
|
169
|
+
atoms: set = set()
|
|
170
|
+
families: set = set()
|
|
171
|
+
for node in formula.walk():
|
|
172
|
+
if isinstance(node, Atom):
|
|
173
|
+
if _truth_value(node) is None: # `$true` / `$false` are not varied
|
|
174
|
+
atoms.add(key_text(node))
|
|
175
|
+
elif isinstance(node, (Box, Diamond)):
|
|
176
|
+
families.add(_ALETHIC)
|
|
177
|
+
elif isinstance(node, Knows):
|
|
178
|
+
families.add(_KNOWS_PREFIX + _agent_key(node.agent))
|
|
179
|
+
elif isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
|
|
180
|
+
# Every group member's "K:"+agent relation is searched and frame-
|
|
181
|
+
# checked. Dropping it would fix the relation empty on every
|
|
182
|
+
# candidate, which under e.g. S5 is not even a legal frame and
|
|
183
|
+
# "refutes" the valid E_{a} P → P.
|
|
184
|
+
families.update(_KNOWS_PREFIX + _agent_key(member) for member in node.group)
|
|
185
|
+
elif isinstance(node, Believes):
|
|
186
|
+
families.add(_BELIEVES_PREFIX + _agent_key(node.agent))
|
|
187
|
+
elif isinstance(node, Says):
|
|
188
|
+
families.add(_SAYS_PREFIX + _agent_key(node.agent))
|
|
189
|
+
elif isinstance(node, Wants):
|
|
190
|
+
families.add(_WANTS_PREFIX + _agent_key(node.agent))
|
|
191
|
+
elif isinstance(node, (Obligatory, Permitted)):
|
|
192
|
+
families.add(_DEONTIC)
|
|
193
|
+
elif isinstance(node, _TEMPORAL_TYPES):
|
|
194
|
+
families.add(_TEMPORAL)
|
|
195
|
+
return tuple(sorted(atoms)), tuple(sorted(families))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _check_frame(frame: str, systems: Optional[Dict[str, str]]) -> None:
|
|
199
|
+
"""Resolve every frame name this search will use, raising if any is unknown.
|
|
200
|
+
|
|
201
|
+
The enumerator carries every condition in the shared registry that a
|
|
202
|
+
FINITE frame check can decide — every first-order condition (a condition
|
|
203
|
+
on a finite relation is directly checkable), and, via each one's finite
|
|
204
|
+
structural characterisation (:func:`~unicode_logic_kit.fol.frames.holds_on_finite_frame`),
|
|
205
|
+
Löb, McKinsey and Grz too — which is more than the labelled tableau's
|
|
206
|
+
rule set, so it validates frames itself rather than borrowing the
|
|
207
|
+
tableau's stricter check. There is nothing left for this function to
|
|
208
|
+
refuse by condition: it only resolves each name, which still raises
|
|
209
|
+
``ValueError`` for one that names no known system or Geach spec.
|
|
210
|
+
"""
|
|
211
|
+
names = [frame]
|
|
212
|
+
for fam, sys in (systems or {}).items():
|
|
213
|
+
if fam not in ("epistemic", "doxastic", "deontic", "temporal"):
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"kripke_enum: unknown system family {fam!r} (use epistemic / "
|
|
216
|
+
"doxastic / deontic / temporal).")
|
|
217
|
+
names.append(sys)
|
|
218
|
+
for name in names:
|
|
219
|
+
try:
|
|
220
|
+
resolve_frame(name)
|
|
221
|
+
except ValueError as exc:
|
|
222
|
+
raise ValueError(f"kripke_enum: {exc}") from None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _conditions_for(relname: str, frame: str, systems: Optional[Dict[str, str]]) -> Tuple[str, ...]:
|
|
226
|
+
"""Frame conditions for a relation name, mirroring ``modal_tableau._Ctx.conds``.
|
|
227
|
+
|
|
228
|
+
``"alethic"`` uses ``frame`` directly; ``"deontic"``/``"temporal"`` and the
|
|
229
|
+
``"K:"``/``"B:"`` families use their ``systems[...]`` override (default
|
|
230
|
+
``"KD"`` for deontic, ``"K"`` for the rest) — the exact same defaulting
|
|
231
|
+
``atp.modal_tableau`` applies, so a formula decided by both routes is
|
|
232
|
+
decided over the SAME frame. ``"Say:"``/``"Want:"`` agents and any other
|
|
233
|
+
family get no frame condition (plain K): Says/Wants are documented as
|
|
234
|
+
non-factive/non-veridical K-modalities with no ``systems`` entry in
|
|
235
|
+
``modal_tableau`` either.
|
|
236
|
+
"""
|
|
237
|
+
systems = systems or {}
|
|
238
|
+
if relname == _ALETHIC:
|
|
239
|
+
return resolve_frame(frame)
|
|
240
|
+
if relname == _DEONTIC:
|
|
241
|
+
return resolve_frame(systems.get("deontic", "KD"))
|
|
242
|
+
if relname == _TEMPORAL:
|
|
243
|
+
return resolve_frame(systems.get("temporal", "K"))
|
|
244
|
+
if relname.startswith(_KNOWS_PREFIX):
|
|
245
|
+
return resolve_frame(systems.get("epistemic", "K"))
|
|
246
|
+
if relname.startswith(_BELIEVES_PREFIX):
|
|
247
|
+
return resolve_frame(systems.get("doxastic", "K"))
|
|
248
|
+
return ()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _holds_conditions(edges: FrozenSet[Tuple[int, int]], n: int, conditions: Tuple[str, ...]) -> bool:
|
|
252
|
+
"""True iff the edge set ``edges`` over ``range(n)`` satisfies every condition.
|
|
253
|
+
|
|
254
|
+
Delegates to :func:`unicode_logic_kit.fol.frames.holds_on_finite_frame`, the
|
|
255
|
+
checker the correspondence tests use as well. Delegating matters for
|
|
256
|
+
SOUNDNESS, not tidiness: this function used to test the five conditions it
|
|
257
|
+
knew and IGNORE any other, so a frame class it did not recognise silently
|
|
258
|
+
widened to the ones it did — and a countermodel the named system excludes
|
|
259
|
+
would have been reported as if it refuted the formula. An unknown
|
|
260
|
+
condition now raises instead — Löb/Grz/McKinsey are no exception: each
|
|
261
|
+
is decided by its own finite structural characterisation, not raised.
|
|
262
|
+
"""
|
|
263
|
+
return all(holds_on_finite_frame(cond, edges, n) for cond in conditions)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@lru_cache(maxsize=None)
|
|
267
|
+
def _valid_relations(n: int, conditions: Tuple[str, ...]) -> Tuple[FrozenSet[Tuple[int, int]], ...]:
|
|
268
|
+
"""Every edge set over ``range(n) x range(n)`` satisfying ``conditions``, in bitmask order.
|
|
269
|
+
|
|
270
|
+
Deterministic and cached (families sharing the same conditions — e.g. two
|
|
271
|
+
K-system epistemic agents — reuse the same enumeration and the same cache
|
|
272
|
+
entry). Brute-force over ``2**(n*n)`` subsets, which is why callers keep
|
|
273
|
+
``max_worlds`` small (n=3 already means 512 subsets to filter per family).
|
|
274
|
+
"""
|
|
275
|
+
pairs = [(a, b) for a in range(n) for b in range(n)]
|
|
276
|
+
out = []
|
|
277
|
+
for mask in range(1 << len(pairs)):
|
|
278
|
+
edges = frozenset(p for i, p in enumerate(pairs) if (mask >> i) & 1)
|
|
279
|
+
if _holds_conditions(edges, n, conditions):
|
|
280
|
+
out.append(edges)
|
|
281
|
+
return tuple(out)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
#: The world counts up to which :func:`_valid_relations_until` uses :func:`_valid_relations`
|
|
285
|
+
#: even under a deadline: ``2 ** (n * n)`` masks are a few hundred, so it cannot overrun.
|
|
286
|
+
_UNTIMED_RELATION_WORLDS = 3
|
|
287
|
+
|
|
288
|
+
#: The relation sets that :func:`_valid_relations_until` completed under a deadline.
|
|
289
|
+
_TIMED_RELATIONS: Dict[Tuple[int, Tuple[str, ...]], Tuple[FrozenSet[Tuple[int, int]], ...]] = {}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _valid_relations_until(n: int, conditions: Tuple[str, ...],
|
|
293
|
+
deadline: Optional[float]) -> Optional[Tuple[FrozenSet[Tuple[int, int]], ...]]:
|
|
294
|
+
""":func:`_valid_relations`, or ``None`` if ``deadline`` (a ``perf_counter`` instant) passes first.
|
|
295
|
+
|
|
296
|
+
Without a deadline, and for a world count too small to matter, this is
|
|
297
|
+
:func:`_valid_relations` itself. Past that the ``2 ** (n * n)`` masks are enumerated here,
|
|
298
|
+
reading the clock every 256 of them; a completed enumeration is kept, so a later call with
|
|
299
|
+
the same ``n`` and ``conditions`` does not repeat it, and an interrupted one leaves nothing
|
|
300
|
+
behind.
|
|
301
|
+
"""
|
|
302
|
+
if deadline is None or n <= _UNTIMED_RELATION_WORLDS:
|
|
303
|
+
return _valid_relations(n, conditions)
|
|
304
|
+
key = (n, conditions)
|
|
305
|
+
if key in _TIMED_RELATIONS:
|
|
306
|
+
return _TIMED_RELATIONS[key]
|
|
307
|
+
pairs = [(a, b) for a in range(n) for b in range(n)]
|
|
308
|
+
out = []
|
|
309
|
+
for mask in range(1 << len(pairs)):
|
|
310
|
+
if not mask & 0xFF and _passed(deadline):
|
|
311
|
+
return None
|
|
312
|
+
edges = frozenset(p for i, p in enumerate(pairs) if (mask >> i) & 1)
|
|
313
|
+
if _holds_conditions(edges, n, conditions):
|
|
314
|
+
out.append(edges)
|
|
315
|
+
_TIMED_RELATIONS[key] = tuple(out)
|
|
316
|
+
return _TIMED_RELATIONS[key]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _scan_sorted(formula: Node) -> Tuple[Node, Tuple[str, ...]]:
|
|
320
|
+
"""The formula the evaluator will read, and the atom keys a legal model fixes true.
|
|
321
|
+
|
|
322
|
+
``satisfies_modal`` relativizes a many-sorted formula before it reads an atom,
|
|
323
|
+
so ``Mortal(socrates:Human)`` is looked up under the key ``Mortal(socrates)``
|
|
324
|
+
— NOT under the key the unrelativized formula prints. The atoms the
|
|
325
|
+
enumerator varies must be the atoms the evaluator reads, so a sorted formula
|
|
326
|
+
is relativized HERE, once, before :func:`_collect` scans it.
|
|
327
|
+
|
|
328
|
+
A sorted constant ``c:S`` also denotes an element of ``S`` at every world
|
|
329
|
+
(it is a rigid designator; see the ``semantics.kripke`` module docstring), so
|
|
330
|
+
the guard atom ``S(c)`` is not a free atom to enumerate: it is true in every
|
|
331
|
+
world of every candidate. The second result lists those keys, sorted.
|
|
332
|
+
|
|
333
|
+
A formula without any many-sorted node is returned UNCHANGED, with no fixed
|
|
334
|
+
keys, so every unsorted caller sees exactly the search it always did.
|
|
335
|
+
"""
|
|
336
|
+
if not sort_axioms(formula):
|
|
337
|
+
return formula, ()
|
|
338
|
+
fixed = tuple(sorted({key_text(atom)
|
|
339
|
+
for atom in sort_membership_axioms(formula)}))
|
|
340
|
+
return formula._relativize([]), fixed
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
#: The number of valuations up to which one world count's valuations are held in a list
|
|
344
|
+
#: and reused for every relation choice (see :func:`modal_enum_search`).
|
|
345
|
+
_CACHED_VALUATIONS = 1 << 14
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _bitmask_tuples(count: int, n: int):
|
|
349
|
+
"""Yield every tuple of ``n`` integers of ``range(count)``, the first slowest.
|
|
350
|
+
|
|
351
|
+
The order of ``itertools.product(range(count), repeat=n)``, produced one tuple at a time:
|
|
352
|
+
``product`` first copies its input into a tuple, which for the ``2 ** m`` bitmasks of ``m``
|
|
353
|
+
atoms is more memory than a machine has long before ``m`` is large enough to matter.
|
|
354
|
+
"""
|
|
355
|
+
if n == 0:
|
|
356
|
+
yield ()
|
|
357
|
+
return
|
|
358
|
+
for head in range(count):
|
|
359
|
+
for tail in _bitmask_tuples(count, n - 1):
|
|
360
|
+
yield (head,) + tail
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _valuations(atoms: Tuple[str, ...], n: int, fixed: Tuple[str, ...] = ()):
|
|
364
|
+
"""Yield every valuation ``{world: frozenset(atoms true there)}`` over ``range(n)``.
|
|
365
|
+
|
|
366
|
+
Deterministic bitmask order: for ``n`` worlds and ``m`` atoms, each of the
|
|
367
|
+
``(2**m)**n`` combinations is produced by ``itertools.product`` over a
|
|
368
|
+
per-world bitmask in ``range(2**m)``, world 0 varying slowest — the same
|
|
369
|
+
fixed order every call, so re-running a search reproduces the same model.
|
|
370
|
+
The atom keys in ``fixed`` are true at every world of every valuation and are
|
|
371
|
+
not enumerated.
|
|
372
|
+
"""
|
|
373
|
+
m = len(atoms)
|
|
374
|
+
for combo in _bitmask_tuples(1 << m, n):
|
|
375
|
+
yield {
|
|
376
|
+
w: frozenset(atoms[i] for i in range(m) if (bits >> i) & 1) | frozenset(fixed)
|
|
377
|
+
for w, bits in enumerate(combo)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@dataclass(frozen=True)
|
|
382
|
+
class EnumSearchResult:
|
|
383
|
+
"""The outcome of one :func:`modal_enum_search` call.
|
|
384
|
+
|
|
385
|
+
Fields:
|
|
386
|
+
|
|
387
|
+
``model``
|
|
388
|
+
a verified :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`
|
|
389
|
+
falsifying the searched formula at world 0, or ``None`` if none was
|
|
390
|
+
found (whether because the space was exhausted, the budget ran out,
|
|
391
|
+
or the formula is unsupported — see ``exhausted``/``unsupported``).
|
|
392
|
+
``exhausted``
|
|
393
|
+
``True`` iff ``model is None`` because EVERY candidate up to
|
|
394
|
+
``max_worlds`` was checked and none refuted the formula — a genuine
|
|
395
|
+
"no countermodel with ≤ max_worlds worlds" statement, but NEVER a
|
|
396
|
+
validity proof (see the module docstring). Always ``False`` when
|
|
397
|
+
``model`` is not ``None`` or ``unsupported`` is not ``None``.
|
|
398
|
+
``checked``
|
|
399
|
+
the number of distinct candidate models actually run through
|
|
400
|
+
``satisfies_modal`` (informational; bounded by ``max_models``).
|
|
401
|
+
``unsupported``
|
|
402
|
+
``None`` if the formula is within the propositional/ground modal
|
|
403
|
+
fragment ``satisfies_modal`` understands; otherwise the
|
|
404
|
+
``"ExceptionType: message"`` the evaluator raised on the formula,
|
|
405
|
+
naming exactly why it is out of scope.
|
|
406
|
+
``detail``
|
|
407
|
+
a short free-text explanation of which of the above happened.
|
|
408
|
+
``timed_out``
|
|
409
|
+
``True`` iff ``model is None`` because the ``timeout`` of the call
|
|
410
|
+
passed before the search was complete. Always ``False`` for a search
|
|
411
|
+
without a ``timeout``, for a model that was found and for
|
|
412
|
+
``exhausted=True``.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
model: Optional[KripkeModel]
|
|
416
|
+
exhausted: bool
|
|
417
|
+
checked: int
|
|
418
|
+
unsupported: Optional[str] = None
|
|
419
|
+
detail: str = ""
|
|
420
|
+
timed_out: bool = False
|
|
421
|
+
|
|
422
|
+
def to_dict(self) -> dict:
|
|
423
|
+
"""Serialise to a JSON-compatible dict (the model, if any, as worlds/relations/valuation)."""
|
|
424
|
+
return {
|
|
425
|
+
"found": self.model is not None,
|
|
426
|
+
"exhausted": self.exhausted,
|
|
427
|
+
"checked": self.checked,
|
|
428
|
+
"unsupported": self.unsupported,
|
|
429
|
+
"detail": self.detail,
|
|
430
|
+
"timed_out": self.timed_out,
|
|
431
|
+
"model": kripke_model_to_dict(self.model) if self.model is not None else None,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def kripke_model_to_dict(model: KripkeModel) -> dict:
|
|
436
|
+
"""Render a :class:`KripkeModel` as a JSON-compatible dict.
|
|
437
|
+
|
|
438
|
+
The shape is the structured ``"data"`` payload of every ``"kripke"``
|
|
439
|
+
witness dict a :class:`~unicode_logic_kit.atp.protocol.Verdict` carries:
|
|
440
|
+
``{"worlds": [...], "relations": {name: [[w, w'], ...]}, "valuation":
|
|
441
|
+
{str(world): [atom_key, ...]}}``, plus ``"nominals"`` / ``"domains"``
|
|
442
|
+
keys ONLY when the model actually has them (the purely propositional
|
|
443
|
+
models this module enumerates never do). Valuation/domain keys are
|
|
444
|
+
``str(world)`` because JSON object keys must be strings;
|
|
445
|
+
:func:`kripke_model_from_dict` resolves them back against the typed
|
|
446
|
+
``"worlds"`` list.
|
|
447
|
+
"""
|
|
448
|
+
data = {
|
|
449
|
+
"worlds": sorted(model.worlds),
|
|
450
|
+
"relations": {
|
|
451
|
+
name: sorted([list(edge) for edge in edges])
|
|
452
|
+
for name, edges in model.relations.items()
|
|
453
|
+
},
|
|
454
|
+
"valuation": {
|
|
455
|
+
str(world): sorted(atoms) for world, atoms in model.valuation.items()
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
if model.nominals:
|
|
459
|
+
data["nominals"] = dict(sorted(model.nominals.items()))
|
|
460
|
+
if model.domains is not None:
|
|
461
|
+
data["domains"] = {
|
|
462
|
+
str(world): sorted(individuals, key=repr)
|
|
463
|
+
for world, individuals in model.domains.items()
|
|
464
|
+
}
|
|
465
|
+
return data
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def kripke_model_from_dict(data: dict) -> KripkeModel:
|
|
469
|
+
"""Rebuild a :class:`KripkeModel` from :func:`kripke_model_to_dict` output.
|
|
470
|
+
|
|
471
|
+
The inverse of :func:`kripke_model_to_dict` up to the container copying
|
|
472
|
+
:class:`KripkeModel`'s constructor performs anyway: worlds, relations,
|
|
473
|
+
valuation, and (when present) nominals and per-world domains all round
|
|
474
|
+
trip. Valuation/domain keys arrive as ``str(world)`` and are mapped back
|
|
475
|
+
to the typed world via the ``"worlds"`` list; a key matching no world is
|
|
476
|
+
kept verbatim rather than guessed at (``KripkeModel`` treats an unknown
|
|
477
|
+
valuation world as simply never queried).
|
|
478
|
+
"""
|
|
479
|
+
worlds = list(data["worlds"])
|
|
480
|
+
by_str = {str(world): world for world in worlds}
|
|
481
|
+
relations = {
|
|
482
|
+
name: [tuple(edge) for edge in edges]
|
|
483
|
+
for name, edges in data.get("relations", {}).items()
|
|
484
|
+
}
|
|
485
|
+
valuation = {
|
|
486
|
+
by_str.get(key, key): set(atoms)
|
|
487
|
+
for key, atoms in data.get("valuation", {}).items()
|
|
488
|
+
}
|
|
489
|
+
domains = None
|
|
490
|
+
if "domains" in data:
|
|
491
|
+
domains = {
|
|
492
|
+
by_str.get(key, key): set(individuals)
|
|
493
|
+
for key, individuals in data["domains"].items()
|
|
494
|
+
}
|
|
495
|
+
return KripkeModel(worlds, relations, valuation,
|
|
496
|
+
domains=domains, nominals=data.get("nominals"))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def modal_enum_search(formula: Node, *, frame: str = "K",
|
|
500
|
+
systems: Optional[Dict[str, str]] = None,
|
|
501
|
+
max_worlds: int = 3, max_atoms: Optional[int] = None,
|
|
502
|
+
max_models: int = 200000,
|
|
503
|
+
timeout: Optional[float] = None) -> EnumSearchResult:
|
|
504
|
+
"""Exhaustively search finite Kripke models of increasing size for a countermodel.
|
|
505
|
+
|
|
506
|
+
Enumerates every relation (per family, per the ``frame``/``systems`` frame
|
|
507
|
+
conditions — see :func:`_conditions_for`) and every atom valuation over
|
|
508
|
+
``n = 1 .. max_worlds`` worlds, in a fixed deterministic order, and checks
|
|
509
|
+
each candidate with :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`
|
|
510
|
+
(the existing, already-tested evaluator — this function adds no new
|
|
511
|
+
semantic rule, only the search). The first candidate that falsifies
|
|
512
|
+
``formula`` at world 0 is returned immediately.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
formula: the (already premise-folded, if applicable — see
|
|
516
|
+
:class:`KripkeEnumBackend`) formula to search for a countermodel of.
|
|
517
|
+
frame: the alethic frame name for ``"alethic"`` relations (Box/Diamond)
|
|
518
|
+
— any name in :data:`unicode_logic_kit.fol.frames.FRAMES`, including
|
|
519
|
+
``"GL"``/``"S4.1"``/``"Grz"`` (decided via their finite structural
|
|
520
|
+
characterisation — see the module docstring) and a Scott–Lemmon
|
|
521
|
+
spec like ``"G(1,1,1,1)"``.
|
|
522
|
+
systems: per-family frame overrides for deontic/temporal/epistemic/
|
|
523
|
+
doxastic relations (``{"epistemic": "S5", ...}``), same convention
|
|
524
|
+
and same defaults (KD for deontic, K for the rest) as
|
|
525
|
+
``modal_tableau``.
|
|
526
|
+
max_worlds: the largest world-count searched (inclusive). Every ``n``
|
|
527
|
+
from 1 up to this is tried in order, smallest countermodels first.
|
|
528
|
+
max_atoms: if given, a formula using more than this many distinct
|
|
529
|
+
ground atoms is rejected up front (``exhausted=False``,
|
|
530
|
+
``model=None``) rather than attempting a valuation space of
|
|
531
|
+
``2**(atoms*n)`` per world-count.
|
|
532
|
+
max_models: the total number of candidate models actually evaluated
|
|
533
|
+
(across every ``n``) before giving up. Bounds the worst case
|
|
534
|
+
combinatorially, at the cost of an honest ``exhausted=False``
|
|
535
|
+
instead of a complete search.
|
|
536
|
+
timeout: a wall-clock limit in milliseconds, counted from the start of
|
|
537
|
+
the call (default ``None``: no limit, the search is exactly the one
|
|
538
|
+
it was without the argument). The clock is read before every
|
|
539
|
+
candidate model and while the relations and valuations of a world
|
|
540
|
+
count are built, so the search returns within the cost of one
|
|
541
|
+
candidate of its deadline; the result has ``timed_out=True``,
|
|
542
|
+
``exhausted=False`` and no model.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
An :class:`EnumSearchResult` — see its docstring for how to read
|
|
546
|
+
``model``/``exhausted``/``unsupported`` together.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ValueError: if ``frame`` or a ``systems`` entry names an unknown frame
|
|
550
|
+
or system family. ``"GL"``/``"S4.1"``/``"Grz"`` are NOT refused —
|
|
551
|
+
each has a finite structural characterisation
|
|
552
|
+
(:func:`unicode_logic_kit.fol.frames.holds_on_finite_frame`) that
|
|
553
|
+
this bounded enumerator decides directly, unlike the routes that
|
|
554
|
+
emit a single first-order sentence over frames of every
|
|
555
|
+
cardinality (``fol.qml``, ``fol.modal_translation``, ``atp.fitch``,
|
|
556
|
+
the labelled tableau), which still refuse them.
|
|
557
|
+
"""
|
|
558
|
+
_check_frame(frame, systems)
|
|
559
|
+
try:
|
|
560
|
+
scanned, fixed = _scan_sorted(formula)
|
|
561
|
+
except _UNSUPPORTED_EXC + (RuntimeError,) as exc:
|
|
562
|
+
# relativizing walks the whole tree and meets a node it has no rule for
|
|
563
|
+
# (a Łukasiewicz operator under a sorted binder): the evaluator refuses
|
|
564
|
+
# that formula too, so report it the way an evaluator refusal is reported.
|
|
565
|
+
return EnumSearchResult(
|
|
566
|
+
model=None, exhausted=False, checked=0,
|
|
567
|
+
unsupported=f"{type(exc).__name__}: {exc}",
|
|
568
|
+
detail=("a many-sorted formula could not be relativized — it is "
|
|
569
|
+
"outside the propositional/ground modal fragment this "
|
|
570
|
+
"enumerator supports"),
|
|
571
|
+
)
|
|
572
|
+
try:
|
|
573
|
+
AtomKeys("modal_enum_search").letters([scanned])
|
|
574
|
+
refuse_alike_agents([scanned], "modal_enum_search")
|
|
575
|
+
except NotImplementedError as exc:
|
|
576
|
+
# A valuation and a relation family are named by text: two different atoms (or two
|
|
577
|
+
# different agents) that print alike would be ONE key of every candidate model, and a
|
|
578
|
+
# search that read them as one would report "no countermodel" for a formula that has one.
|
|
579
|
+
return EnumSearchResult(
|
|
580
|
+
model=None, exhausted=False, checked=0,
|
|
581
|
+
unsupported=f"{type(exc).__name__}: {exc}",
|
|
582
|
+
detail=("two different atoms (or agents) of the formula are written alike, so a "
|
|
583
|
+
"model keyed by the written form could not tell them apart — the formula "
|
|
584
|
+
"is outside the fragment this enumerator supports"),
|
|
585
|
+
)
|
|
586
|
+
atoms, families = _collect(scanned)
|
|
587
|
+
# the membership keys are fixed true, not varied: they cost no search space
|
|
588
|
+
atoms = tuple(a for a in atoms if a not in fixed)
|
|
589
|
+
|
|
590
|
+
if max_atoms is not None and len(atoms) > max_atoms:
|
|
591
|
+
return EnumSearchResult(
|
|
592
|
+
model=None, exhausted=False, checked=0, unsupported=None,
|
|
593
|
+
detail=(f"formula uses {len(atoms)} ground atoms, exceeding "
|
|
594
|
+
f"max_atoms={max_atoms}; search skipped rather than build "
|
|
595
|
+
"an oversized valuation space"),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
conditions = {fam: _conditions_for(fam, frame, systems) for fam in families}
|
|
599
|
+
checked = 0
|
|
600
|
+
deadline = _instant(timeout)
|
|
601
|
+
|
|
602
|
+
def out_of_time() -> EnumSearchResult:
|
|
603
|
+
return EnumSearchResult(
|
|
604
|
+
model=None, exhausted=False, checked=checked, unsupported=None,
|
|
605
|
+
detail=(f"the {timeout} ms limit passed after {checked} models; the search "
|
|
606
|
+
f"up to max_worlds={max_worlds} did not complete"),
|
|
607
|
+
timed_out=True)
|
|
608
|
+
|
|
609
|
+
for n in range(1, max_worlds + 1):
|
|
610
|
+
rel_lists = []
|
|
611
|
+
for fam in families:
|
|
612
|
+
family_relations = _valid_relations_until(n, conditions[fam], deadline)
|
|
613
|
+
if family_relations is None:
|
|
614
|
+
return out_of_time()
|
|
615
|
+
rel_lists.append(family_relations)
|
|
616
|
+
# A valuation list that is small is built once and reused for every relation
|
|
617
|
+
# choice; a large one is generated again for each, instead of held in memory.
|
|
618
|
+
valuations = (list(_valuations(atoms, n, fixed))
|
|
619
|
+
if (1 << len(atoms)) ** n <= _CACHED_VALUATIONS else None)
|
|
620
|
+
rel_choices = itertools.product(*rel_lists) if rel_lists else [()]
|
|
621
|
+
for rel_choice in rel_choices:
|
|
622
|
+
relations = dict(zip(families, rel_choice))
|
|
623
|
+
for valuation in (valuations if valuations is not None
|
|
624
|
+
else _valuations(atoms, n, fixed)):
|
|
625
|
+
if _passed(deadline):
|
|
626
|
+
return out_of_time()
|
|
627
|
+
if checked >= max_models:
|
|
628
|
+
return EnumSearchResult(
|
|
629
|
+
model=None, exhausted=False, checked=checked, unsupported=None,
|
|
630
|
+
detail=(f"candidate budget exhausted after {checked} models "
|
|
631
|
+
f"(max_models={max_models}); the search up to "
|
|
632
|
+
f"max_worlds={max_worlds} did not complete"),
|
|
633
|
+
)
|
|
634
|
+
model = KripkeModel(worlds=range(n), relations=relations, valuation=valuation)
|
|
635
|
+
try:
|
|
636
|
+
holds = satisfies_modal(formula, model, 0)
|
|
637
|
+
except _UNSUPPORTED_EXC as exc:
|
|
638
|
+
return EnumSearchResult(
|
|
639
|
+
model=None, exhausted=False, checked=checked,
|
|
640
|
+
unsupported=f"{type(exc).__name__}: {exc}",
|
|
641
|
+
detail=("satisfies_modal has no rule for a construct in "
|
|
642
|
+
"this formula — it is outside the propositional/"
|
|
643
|
+
"ground modal fragment this enumerator supports"),
|
|
644
|
+
)
|
|
645
|
+
checked += 1
|
|
646
|
+
if not holds:
|
|
647
|
+
return EnumSearchResult(
|
|
648
|
+
model=model, exhausted=False, checked=checked, unsupported=None,
|
|
649
|
+
detail=(f"countermodel found with {n} world(s) after "
|
|
650
|
+
f"checking {checked} candidate(s)"),
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
return EnumSearchResult(
|
|
654
|
+
model=None, exhausted=True, checked=checked, unsupported=None,
|
|
655
|
+
detail=(f"search space fully enumerated up to max_worlds={max_worlds} "
|
|
656
|
+
f"({checked} candidates checked); no countermodel exists at or "
|
|
657
|
+
"below that size — NOT a validity proof, larger models are "
|
|
658
|
+
"unexplored"),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def modal_enum_countermodel(formula: Node, *, frame: str = "K",
|
|
663
|
+
systems: Optional[Dict[str, str]] = None,
|
|
664
|
+
max_worlds: int = 3, max_atoms: Optional[int] = None,
|
|
665
|
+
max_models: int = 200000,
|
|
666
|
+
timeout: Optional[float] = None) -> Optional[KripkeModel]:
|
|
667
|
+
"""Return a verified Kripke countermodel for ``formula``, or ``None``.
|
|
668
|
+
|
|
669
|
+
Thin convenience wrapper over :func:`modal_enum_search` for callers who
|
|
670
|
+
only want the model (or its absence) and not the exhausted/unsupported/
|
|
671
|
+
checked detail — e.g. a differential test against
|
|
672
|
+
:func:`unicode_logic_kit.atp.modal_tableau.modal_countermodel`. ``None`` here
|
|
673
|
+
conflates "exhausted" and "budget hit" and "timed out" and "unsupported";
|
|
674
|
+
use :func:`modal_enum_search` directly to tell them apart (this is exactly
|
|
675
|
+
what :class:`KripkeEnumBackend` does for its ``reason``/``detail`` fields).
|
|
676
|
+
"""
|
|
677
|
+
return modal_enum_search(formula, frame=frame, systems=systems,
|
|
678
|
+
max_worlds=max_worlds, max_atoms=max_atoms,
|
|
679
|
+
max_models=max_models, timeout=timeout).model
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class KripkeEnumBackend(ProverBackend):
|
|
683
|
+
"""Refutation-only semantic backend: bounded finite-Kripke-model enumeration.
|
|
684
|
+
|
|
685
|
+
Fills exactly the gap ``modal-tableau`` and ``qml`` leave open: neither can
|
|
686
|
+
REFUTE a temporal-closure formula (``modal-tableau`` has no rule for
|
|
687
|
+
Ⓖ/Ⓕ/Ⓤ/⒣/⒫/⒴/⒮ and reports it ``UNKNOWN/unsupported``; ``qml`` is sound but
|
|
688
|
+
proof-only). This backend decides such a formula by exhaustive finite-model
|
|
689
|
+
search (:func:`modal_enum_search`) using the SAME evaluator
|
|
690
|
+
(``satisfies_modal``) both of those routes ultimately answer to, so a
|
|
691
|
+
REFUTED verdict here can never contradict a PROVED verdict from either.
|
|
692
|
+
|
|
693
|
+
Like :class:`unicode_logic_kit.atp.protocol.ModelFinderBackend`, this is
|
|
694
|
+
ONE-SIDED: finding a countermodel REFUTES; exhausting the search space
|
|
695
|
+
proves NOTHING about validity (modal logic has no small-model property
|
|
696
|
+
bounding countermodels to ``max_worlds``), so that case is reported
|
|
697
|
+
``UNKNOWN`` with ``reason="bound_hit"`` — the same reason
|
|
698
|
+
``ModelFinderBackend`` uses for "no countermodel up to the size bound",
|
|
699
|
+
since ``max_worlds`` is exactly such a size bound (see
|
|
700
|
+
``atp.protocol``'s own reason-vocabulary docstring, which lists
|
|
701
|
+
``max_worlds`` as a ``bound_hit`` example). The ``detail`` field still says
|
|
702
|
+
whether the bound was hit by a COMPLETE search (``exhausted``) or by the
|
|
703
|
+
``max_models``/``max_atoms`` budget cutting a search short — see
|
|
704
|
+
:class:`EnumSearchResult`. The call's ``timeout`` is one more bound: a search
|
|
705
|
+
it ended is ``UNKNOWN`` with ``reason="timeout"``.
|
|
706
|
+
|
|
707
|
+
``premises`` are folded into the LOCAL consequence goal
|
|
708
|
+
``(∧ premises) → formula``, exactly like ``ModalTableauBackend`` and
|
|
709
|
+
``QmlBackend``.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
name = "kripke-enum"
|
|
713
|
+
logics = frozenset({"modal"})
|
|
714
|
+
external = False
|
|
715
|
+
|
|
716
|
+
def available(self) -> bool:
|
|
717
|
+
return True
|
|
718
|
+
|
|
719
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
720
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
721
|
+
import time
|
|
722
|
+
from .protocol import _implication
|
|
723
|
+
|
|
724
|
+
goal = _implication(formula, premises)
|
|
725
|
+
start = time.perf_counter()
|
|
726
|
+
result = modal_enum_search(goal, timeout=timeout, **options)
|
|
727
|
+
elapsed = time.perf_counter() - start
|
|
728
|
+
|
|
729
|
+
if result.unsupported is not None:
|
|
730
|
+
return Verdict(UNKNOWN, self.name, logic="modal", reason="unsupported",
|
|
731
|
+
wall_time=elapsed,
|
|
732
|
+
detail=f"{result.detail} ({result.unsupported})")
|
|
733
|
+
if result.model is not None:
|
|
734
|
+
return Verdict(REFUTED, self.name, logic="modal", wall_time=elapsed,
|
|
735
|
+
countermodel={"kind": "kripke", "repr": repr(result.model),
|
|
736
|
+
"data": kripke_model_to_dict(result.model)})
|
|
737
|
+
if result.timed_out:
|
|
738
|
+
return Verdict(UNKNOWN, self.name, logic="modal", reason="timeout",
|
|
739
|
+
wall_time=elapsed, detail=result.detail)
|
|
740
|
+
return Verdict(UNKNOWN, self.name, logic="modal", reason="bound_hit",
|
|
741
|
+
wall_time=elapsed, detail=result.detail)
|