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,1200 @@
|
|
|
1
|
+
"""Minimal models via ASP — clingo enumerates, the kit's own filter decides.
|
|
2
|
+
|
|
3
|
+
:func:`nonmonotonic.minimal_models <unicode_logic_kit.semantics.nonmonotonic.minimal_models>`
|
|
4
|
+
answers "which finite models are ≤-minimal" by brute force: enumerate every
|
|
5
|
+
interpretation of the signature over a domain, keep the ones that satisfy the
|
|
6
|
+
premises, then compare every survivor against every other survivor in its
|
|
7
|
+
fixed-part group. That comparison step is correct but the enumeration step is
|
|
8
|
+
not what an ASP solver is for — clingo already ships a search procedure that
|
|
9
|
+
prunes as it grounds instead of generating every candidate and checking it
|
|
10
|
+
afterward. This module keeps the (already correct, unmodified) comparison and
|
|
11
|
+
replaces only the enumeration: :func:`asp_minimal_models` grounds ``premises``
|
|
12
|
+
into an ASP program whose answer sets are *exactly* the models of a given
|
|
13
|
+
domain size, lets clingo enumerate those, and then hands the result to
|
|
14
|
+
nonmonotonic's own ``_circ_profile`` / ``_strictly_below`` / ``_fixed_key`` —
|
|
15
|
+
imported, not reimplemented — to pick out the minimal ones.
|
|
16
|
+
|
|
17
|
+
Why reuse rather than reimplement the filter
|
|
18
|
+
---------------------------------------------
|
|
19
|
+
A second, independently-written "is this model minimal" check would need to
|
|
20
|
+
agree with the first one for this module to be trustworthy, and disagreement
|
|
21
|
+
between two independent implementations of the same non-trivial comparison
|
|
22
|
+
(componentwise subset-or-equal with at least one strict subset, grouped by a
|
|
23
|
+
shared fixed part) is exactly the kind of bug this kit's differential-testing
|
|
24
|
+
discipline exists to catch — better to make it structurally impossible by
|
|
25
|
+
sharing the code. Importing the private ``_circ_profile`` / ``_strictly_below``
|
|
26
|
+
/ ``_fixed_key`` from :mod:`~unicode_logic_kit.semantics.nonmonotonic` (that
|
|
27
|
+
module is not modified here) means the two routes can only ever disagree about
|
|
28
|
+
*which models exist* (an enumeration bug), never about *which of them are
|
|
29
|
+
minimal* (a comparison bug) — the second class of disagreement is ruled out
|
|
30
|
+
by construction, not merely tested for.
|
|
31
|
+
|
|
32
|
+
Why not ``#minimize`` over the circumscribed predicates
|
|
33
|
+
----------------------------------------------------------
|
|
34
|
+
clingo's own optimisation directive would find the models with the fewest
|
|
35
|
+
TRUE atoms among the circumscribed predicates — CARDINALITY-minimal models.
|
|
36
|
+
Circumscription wants SUBSET-minimal models: ``{a}`` is more minimal than
|
|
37
|
+
``{a, b}`` even though both have "few" elements, and a set with 3 elements
|
|
38
|
+
distributed one way is not comparable by cardinality alone to a different
|
|
39
|
+
3-element set. The two orders coincide only when every minimal set has the
|
|
40
|
+
same size, which is not true in general (see the disjunctive-fact example in
|
|
41
|
+
this module's own verification below: ``P(a) ∨ P(b)`` has minimal models
|
|
42
|
+
``{a}`` and ``{b}``, both size 1, but a formula like ``P(a) ∨ (P(b) ∧ P(c))``
|
|
43
|
+
already breaks the coincidence). Substituting cardinality-minimality for
|
|
44
|
+
subset-minimality would be exactly the silent semantic swap this kit's other
|
|
45
|
+
non-monotonic-reasoning code refuses to make elsewhere, so it is refused here
|
|
46
|
+
too: this module enumerates ALL models and filters in Python with the real
|
|
47
|
+
predicate, never ``#minimize``.
|
|
48
|
+
|
|
49
|
+
``size`` is a single domain size, not a bound
|
|
50
|
+
------------------------------------------------
|
|
51
|
+
:func:`nonmonotonic.minimal_models <unicode_logic_kit.semantics.nonmonotonic.minimal_models>`
|
|
52
|
+
takes ``max_size`` and unions the minimal models found at every size from 1 up
|
|
53
|
+
to that bound (models at different sizes are never compared against each
|
|
54
|
+
other — see that function's own docstring). :func:`asp_minimal_models` here
|
|
55
|
+
takes a single ``size`` and grounds exactly once, at that size, because
|
|
56
|
+
grounding is the expensive step an ASP solver does per call and silently
|
|
57
|
+
regrounding ``size`` times to imitate ``max_size`` would hide that cost from
|
|
58
|
+
the caller. A caller who wants the union-over-sizes behaviour gets it by
|
|
59
|
+
calling this function in a loop and concatenating the results — one line at
|
|
60
|
+
the call site, versus a hidden cost inside every call otherwise. This is a
|
|
61
|
+
genuine, deliberate difference from ``minimal_models``'s parameter of a
|
|
62
|
+
similar name; the verification below accounts for it by comparing against
|
|
63
|
+
``minimal_models(..., max_size=size)`` FILTERED to the structures whose own
|
|
64
|
+
domain has exactly ``size`` elements, which — because sizes are never compared
|
|
65
|
+
across each other in that function either — is provably the same set a
|
|
66
|
+
hypothetical single-size ``minimal_models`` would have returned.
|
|
67
|
+
|
|
68
|
+
The ASP encoding
|
|
69
|
+
-------------------
|
|
70
|
+
For a fixed domain size ``n``, ``dom(0..n-1).`` names the individuals; every
|
|
71
|
+
declared predicate, function, and constant gets a free ASP choice (a function
|
|
72
|
+
of arity ``k`` as the standard *total relation*: ``1 { f(x̄, v) : dom(v) } 1``
|
|
73
|
+
per input tuple ``x̄``, which enforces functionality — at most one result —
|
|
74
|
+
and totality — at least one — in the same rule the way
|
|
75
|
+
:mod:`~unicode_logic_kit.atp.finite_domain`'s design note also describes it).
|
|
76
|
+
Each (sub)formula then gets its own auxiliary ASP atom, defined by rules that
|
|
77
|
+
mirror its connective: ``And`` needs one rule (a conjunction of positive
|
|
78
|
+
literals is what a rule body already is), ``Or``/``Xor``/``Implies``/``Iff``
|
|
79
|
+
need two rules each (one for each way to satisfy them — plain multi-rule
|
|
80
|
+
disjunction, no aggregate), ``Not`` is default negation over its
|
|
81
|
+
already-fully-defined child (safe precisely because the child's rules are
|
|
82
|
+
grounded, and hence decided, for every combination of its free variables
|
|
83
|
+
before the parent ever refers to it — the formula tree is a DAG with no
|
|
84
|
+
cycles through negation, so this is ordinary stratified Datalog negation, not
|
|
85
|
+
anything exotic). ``∃x φ`` is a projection: a rule whose body mentions ``x``
|
|
86
|
+
but whose head does not, so grounding produces "true for some x" for free —
|
|
87
|
+
no aggregate needed. ``∀x φ`` is rewritten to ``¬∃x ¬φ`` before encoding
|
|
88
|
+
rather than given its own bespoke aggregate rule, so its correctness rides on
|
|
89
|
+
the already-tested ``Not``/``∃`` encodings instead of a third, separately-
|
|
90
|
+
fallible implementation of the same quantifier alternation. ``Count`` (the
|
|
91
|
+
one place real counting is needed) is the one aggregate this module emits:
|
|
92
|
+
``#count{x : dom(x), φ(x)} <op> n``. Symbol names in the generated program
|
|
93
|
+
are never the FOL names themselves (``p0``, ``f0``, ``k0``, …, assigned by a
|
|
94
|
+
signature scan) — sidestepping both ASP's stricter identifier syntax
|
|
95
|
+
(FOL names may contain non-ASCII letters this kit deliberately allows
|
|
96
|
+
elsewhere) and any namespace collision between a predicate and a same-spelled
|
|
97
|
+
constant, which a flat ASP atom namespace would otherwise conflate.
|
|
98
|
+
|
|
99
|
+
Fragment supported here (deliberately narrower than
|
|
100
|
+
:func:`~unicode_logic_kit.atp.finite_domain.fragment_check`'s)
|
|
101
|
+
------------------------------------------------------------------
|
|
102
|
+
``Atom``, ``Not``, ``And``, ``Or``, ``Xor``, ``Implies``, ``Iff``,
|
|
103
|
+
``Quantifier`` (unsorted ``∀``/``∃`` only), ``Count``, over
|
|
104
|
+
``Variable``/``Constant``/``Number``/``Function`` terms — exactly the
|
|
105
|
+
fragment :mod:`~unicode_logic_kit.semantics.tarski` (via
|
|
106
|
+
:mod:`~unicode_logic_kit.semantics.modelfinder`) evaluates, because that
|
|
107
|
+
evaluator is this module's own verification oracle below: supporting a
|
|
108
|
+
construct here that the oracle cannot check would mean shipping an
|
|
109
|
+
un-cross-checked code path, which is what this whole module exists to avoid.
|
|
110
|
+
Two constructs :func:`~unicode_logic_kit.atp.finite_domain.fragment_check` DOES
|
|
111
|
+
admit are refused here for exactly that reason: ``Cardinality`` (the oracle's
|
|
112
|
+
:func:`~unicode_logic_kit.semantics.tarski.term_value` can evaluate it as a
|
|
113
|
+
term, but only inside an equality/order comparison whose VALUE is an
|
|
114
|
+
arithmetic integer, not a domain individual — plumbing that through this
|
|
115
|
+
module's term encoding, which only ever produces domain-individual variables,
|
|
116
|
+
is a distinct unit of work with its own risk of a subtly wrong aggregate,
|
|
117
|
+
and it is not needed for circumscription premises, which reason about
|
|
118
|
+
predicate extensions, not counts) and ``Contrast`` (the oracle does not
|
|
119
|
+
evaluate it at all — :func:`~unicode_logic_kit.semantics.tarski.satisfies` has
|
|
120
|
+
no case for it — so there is no oracle to verify against even if this module
|
|
121
|
+
encoded it). A sentence outside this fragment raises ``ValueError`` naming
|
|
122
|
+
the offending node type rather than silently mis-encoding it.
|
|
123
|
+
|
|
124
|
+
Public API: :func:`asp_minimal_models`, :func:`asp_find_model`.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
from itertools import product
|
|
128
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
|
129
|
+
|
|
130
|
+
from ..fol.nodes import (
|
|
131
|
+
Node, Variable, Constant, Number, Function,
|
|
132
|
+
Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Count,
|
|
133
|
+
SecondOrderQuantifier,
|
|
134
|
+
)
|
|
135
|
+
from ..fol._fol_nodes import numeral_key
|
|
136
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
137
|
+
from .tarski import Structure, _FORALL, _EXISTS, _ORDER_COMPARISONS, _ORDER_OPS, _is_number
|
|
138
|
+
from ..fol._free_parameters import parameterize
|
|
139
|
+
from .modelfinder import _Signature
|
|
140
|
+
from .nonmonotonic import _circ_profile, _strictly_below, _fixed_key
|
|
141
|
+
|
|
142
|
+
__all__ = ["asp_minimal_models", "asp_find_model", "asp_holds_so"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# =============================================================================
|
|
146
|
+
# Fragment gate — see the module docstring's "Fragment supported here" section
|
|
147
|
+
# for what is admitted and, for the two admitted-elsewhere-but-not-here node
|
|
148
|
+
# types, exactly why.
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
_ALLOWED_FORMULA_TYPES = (Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Count)
|
|
152
|
+
_ALLOWED_TERM_TYPES = (Variable, Constant, Number, Function)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _silent(code, message):
|
|
156
|
+
"""clingo logger that drops its info/warning chatter about the GENERATED program
|
|
157
|
+
(e.g. "atom does not occur in any rule head" for an empty extension), the same
|
|
158
|
+
choice atp.clingo_backend makes; real errors still raise from clingo itself."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _check_fragment(sentences: Iterable[Node]) -> None:
|
|
162
|
+
"""Raise ``ValueError`` at the first node outside this module's fragment.
|
|
163
|
+
|
|
164
|
+
Walks every sentence pre-order (so a disallowed node nested inside an
|
|
165
|
+
allowed one is still caught) and additionally rejects a ``Quantifier``
|
|
166
|
+
whose ``type`` is neither an unsorted ``∀``/``forall`` nor an unsorted
|
|
167
|
+
``∃``/``exists`` spelling — the only two this module's encoder
|
|
168
|
+
recognises (the same spellings :mod:`~unicode_logic_kit.semantics.tarski`
|
|
169
|
+
recognises, imported from there rather than re-listed here so the two
|
|
170
|
+
can never drift apart).
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
ValueError: naming the first offending node type (or quantifier
|
|
174
|
+
spelling) encountered, and — for a well-known excluded
|
|
175
|
+
construct (``Cardinality``, ``Contrast``, the sorted family,
|
|
176
|
+
…) — why it is excluded (see the module docstring).
|
|
177
|
+
"""
|
|
178
|
+
for sentence in sentences:
|
|
179
|
+
for node in sentence.walk():
|
|
180
|
+
if isinstance(node, _ALLOWED_FORMULA_TYPES) or isinstance(node, _ALLOWED_TERM_TYPES):
|
|
181
|
+
if isinstance(node, Quantifier) and node.type not in _FORALL and node.type not in _EXISTS:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"asp_models: unknown quantifier spelling {node.type!r} — "
|
|
184
|
+
f"only unsorted {_FORALL + _EXISTS} are encodable here."
|
|
185
|
+
)
|
|
186
|
+
continue
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"asp_models: {type(node).__name__} is not in the fragment this "
|
|
189
|
+
"module encodes (unsorted classical FOL — Atom/Not/And/Or/Xor/"
|
|
190
|
+
"Implies/Iff/Quantifier/Count over Variable/Constant/Number/"
|
|
191
|
+
"Function terms; see the module docstring's 'Fragment supported "
|
|
192
|
+
"here' section for why Cardinality/Contrast/the sorted and modal "
|
|
193
|
+
"families are excluded)."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# The ASP encoder
|
|
199
|
+
# =============================================================================
|
|
200
|
+
|
|
201
|
+
class _AspEncoder:
|
|
202
|
+
"""Translates closed FOL sentences into a clingo program, one call at a time.
|
|
203
|
+
|
|
204
|
+
Every (sub)formula gets its own fresh auxiliary ASP atom (a Tseitin-style
|
|
205
|
+
encoding — see the module docstring's "The ASP encoding" section for the
|
|
206
|
+
per-connective rule shapes and why each is sound). State kept here is
|
|
207
|
+
purely bookkeeping for that translation: the accumulated rule TEXT, fresh
|
|
208
|
+
name counters, and the FOL-symbol-name -> ASP-identifier maps (so a FOL
|
|
209
|
+
name containing non-ASCII characters, or one that collides with a FOL
|
|
210
|
+
name in a *different* symbol class, never has to become a raw ASP
|
|
211
|
+
identifier — see the module docstring).
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, size: int):
|
|
215
|
+
self.size = size
|
|
216
|
+
self.rules: List[str] = []
|
|
217
|
+
self._var_ctr = 0
|
|
218
|
+
self._head_ctr = 0
|
|
219
|
+
self.pred_asp: Dict[Tuple[str, int], str] = {}
|
|
220
|
+
self.func_asp: Dict[Tuple[str, int], str] = {}
|
|
221
|
+
self.const_asp: Dict[str, str] = {}
|
|
222
|
+
|
|
223
|
+
def _fresh_var(self) -> str:
|
|
224
|
+
self._var_ctr += 1
|
|
225
|
+
return f"V{self._var_ctr}"
|
|
226
|
+
|
|
227
|
+
def _fresh_head(self) -> str:
|
|
228
|
+
self._head_ctr += 1
|
|
229
|
+
return f"h{self._head_ctr}"
|
|
230
|
+
|
|
231
|
+
# -- setup --------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def declare_signature(self, sig: "_Signature") -> None:
|
|
234
|
+
"""Assign a fresh, ASCII, collision-free ASP identifier to every symbol.
|
|
235
|
+
|
|
236
|
+
Enumeration order is ``sorted(...)`` of each namespace, so this is
|
|
237
|
+
deterministic given a signature — useful for reading generated
|
|
238
|
+
programs back while debugging, though nothing downstream depends on
|
|
239
|
+
the specific numbering.
|
|
240
|
+
"""
|
|
241
|
+
for i, key in enumerate(sorted(sig.predicates)):
|
|
242
|
+
self.pred_asp[key] = f"p{i}"
|
|
243
|
+
for i, key in enumerate(sorted(sig.functions)):
|
|
244
|
+
self.func_asp[key] = f"f{i}"
|
|
245
|
+
for i, name in enumerate(sorted(sig.constants)):
|
|
246
|
+
self.const_asp[name] = f"k{i}"
|
|
247
|
+
|
|
248
|
+
def emit_base_facts(self) -> None:
|
|
249
|
+
"""Emit ``dom/1`` and the free choice for every declared symbol.
|
|
250
|
+
|
|
251
|
+
A predicate of arity ``k`` gets an unconstrained choice over its
|
|
252
|
+
``domain**k`` possible tuples (arity 0 is a single choice atom, no
|
|
253
|
+
domain conditions needed). A function or constant gets the "total
|
|
254
|
+
relation" choice: for every input tuple (none, for a constant),
|
|
255
|
+
``1 { … } 1`` picks exactly one result — enforcing functionality
|
|
256
|
+
(at most one) and totality (at least one) in the same rule, the same
|
|
257
|
+
reading :mod:`~unicode_logic_kit.atp.finite_domain`'s design note
|
|
258
|
+
describes for the sibling refutation backends.
|
|
259
|
+
"""
|
|
260
|
+
self.rules.append(f"dom(0..{self.size - 1}).")
|
|
261
|
+
for (name, arity), asp in self.pred_asp.items():
|
|
262
|
+
if arity == 0:
|
|
263
|
+
self.rules.append(f"{{{asp}}}.")
|
|
264
|
+
else:
|
|
265
|
+
xs = [f"X{i}" for i in range(arity)]
|
|
266
|
+
conds = ", ".join(f"dom({x})" for x in xs)
|
|
267
|
+
self.rules.append(f"{{{asp}({','.join(xs)}) : {conds}}}.")
|
|
268
|
+
for (name, arity), asp in self.func_asp.items():
|
|
269
|
+
v = "V"
|
|
270
|
+
if arity == 0:
|
|
271
|
+
self.rules.append(f"1 {{ {asp}({v}) : dom({v}) }} 1.")
|
|
272
|
+
else:
|
|
273
|
+
xs = [f"X{i}" for i in range(arity)]
|
|
274
|
+
choice = f"1 {{ {asp}({','.join(xs + [v])}) : dom({v}) }} 1"
|
|
275
|
+
body = ", ".join(f"dom({x})" for x in xs)
|
|
276
|
+
self.rules.append(f"{choice} :- {body}.")
|
|
277
|
+
for name, asp in self.const_asp.items():
|
|
278
|
+
self.rules.append(f"1 {{ {asp}(V) : dom(V) }} 1.")
|
|
279
|
+
|
|
280
|
+
def emit_fixed_facts(self, structure: Structure, index_of: Dict[Any, int],
|
|
281
|
+
free_predicates: Set[Tuple[str, int]]) -> None:
|
|
282
|
+
"""Emit ``dom/1``, plus GROUND facts pinning ``structure``'s fixed part.
|
|
283
|
+
|
|
284
|
+
Sibling of :meth:`emit_base_facts`, used only by :func:`asp_holds_so`
|
|
285
|
+
(``emit_base_facts`` itself is untouched, so :func:`asp_find_model` /
|
|
286
|
+
:func:`asp_minimal_models` are unaffected). Every declared predicate
|
|
287
|
+
named in ``free_predicates`` — the SO-quantifier-bound ones — gets
|
|
288
|
+
exactly the same free choice rule ``emit_base_facts`` would give it
|
|
289
|
+
(the quantifier itself needs no other encoding: see
|
|
290
|
+
:func:`asp_holds_so`'s docstring). Every OTHER declared symbol
|
|
291
|
+
(predicate, function, constant) is pinned EXACTLY to its
|
|
292
|
+
interpretation in ``structure`` — ground facts, never a choice rule —
|
|
293
|
+
translating ``structure``'s own (arbitrary, hashable) domain
|
|
294
|
+
individuals to the ``0..size-1`` integers ``declare_signature``'s
|
|
295
|
+
numbering assumes elsewhere via ``index_of``.
|
|
296
|
+
|
|
297
|
+
One case is not "pin to the declared extension, or else empty": an
|
|
298
|
+
order comparison (``< > ≤ ≥``) that ``structure`` declares NO
|
|
299
|
+
extension for. :func:`~unicode_logic_kit.semantics.tarski._order_value`
|
|
300
|
+
does not read that as empty (always false) — its third, lowest-
|
|
301
|
+
priority reading is that the comparison still holds numerically
|
|
302
|
+
between two operands that themselves evaluate to numbers (see that
|
|
303
|
+
function's own docstring). Pinning such a predicate to the empty
|
|
304
|
+
relation here would silently disagree with the very evaluator
|
|
305
|
+
:func:`asp_holds_so` claims to match once the block's body compares
|
|
306
|
+
two numeric domain individuals with no declared ``<``/etc.
|
|
307
|
+
extension — see :meth:`_order_numeric_extension`, used below exactly
|
|
308
|
+
where ``emit_base_facts`` has no analogous case (a fully free choice
|
|
309
|
+
never needs this fallback).
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
structure: the structure whose fixed part is pinned.
|
|
313
|
+
index_of: ``{domain individual: 0-based index}`` for every
|
|
314
|
+
individual in ``structure.domain`` (``len(structure.domain)``
|
|
315
|
+
must equal ``self.size``).
|
|
316
|
+
free_predicates: the ``(name, arity)`` pairs to leave as a free
|
|
317
|
+
choice instead of pinning (the SO-quantifier-bound ones).
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
ValueError: a constant has no interpretation in ``structure``
|
|
321
|
+
(and does not itself parse as a numeral, mirroring
|
|
322
|
+
:func:`~unicode_logic_kit.semantics.tarski.term_value`'s own
|
|
323
|
+
fallback), a function has no interpretation in ``structure``
|
|
324
|
+
or is not TOTAL over its domain (an argument tuple with no
|
|
325
|
+
value — this encoder's function encoding is the same
|
|
326
|
+
"total relation" reading :func:`emit_base_facts`
|
|
327
|
+
free-chooses, so a partial interpretation does not fit it),
|
|
328
|
+
or a pinned value (from a predicate's declared extension, a
|
|
329
|
+
function's arguments/result, or a constant) is not itself a
|
|
330
|
+
member of ``structure.domain``. A declared PREDICATE with no
|
|
331
|
+
interpretation in ``structure`` never raises here — it
|
|
332
|
+
silently falls through to the empty relation (or, for an
|
|
333
|
+
order comparison, to :meth:`_order_numeric_extension`'s
|
|
334
|
+
numeric reading), the same "missing extension is the empty
|
|
335
|
+
relation, hence false" fallback
|
|
336
|
+
:func:`~unicode_logic_kit.semantics.tarski._atom_value` /
|
|
337
|
+
:func:`~unicode_logic_kit.semantics.tarski._order_value`
|
|
338
|
+
themselves document, not a gap.
|
|
339
|
+
"""
|
|
340
|
+
self.rules.append(f"dom(0..{self.size - 1}).")
|
|
341
|
+
|
|
342
|
+
for (name, arity), asp in self.pred_asp.items():
|
|
343
|
+
if (name, arity) in free_predicates:
|
|
344
|
+
if arity == 0:
|
|
345
|
+
self.rules.append(f"{{{asp}}}.")
|
|
346
|
+
else:
|
|
347
|
+
xs = [f"X{i}" for i in range(arity)]
|
|
348
|
+
conds = ", ".join(f"dom({x})" for x in xs)
|
|
349
|
+
self.rules.append(f"{{{asp}({','.join(xs)}) : {conds}}}.")
|
|
350
|
+
continue
|
|
351
|
+
if arity == 0:
|
|
352
|
+
if bool(structure.predicates.get((name, 0), False)):
|
|
353
|
+
self.rules.append(f"{asp}.")
|
|
354
|
+
continue
|
|
355
|
+
extension = structure.predicates.get((name, arity))
|
|
356
|
+
if extension is None:
|
|
357
|
+
extension = (self._order_numeric_extension(name, structure)
|
|
358
|
+
if arity == 2 and name in _ORDER_COMPARISONS else ())
|
|
359
|
+
for tup in extension:
|
|
360
|
+
idxs = [self._pin_value(v, index_of, "predicate", name) for v in tup]
|
|
361
|
+
self.rules.append(f"{asp}({','.join(str(i) for i in idxs)}).")
|
|
362
|
+
|
|
363
|
+
for (name, arity), asp in self.func_asp.items():
|
|
364
|
+
# Functions are never second-order-bound (SecondOrderQuantifier
|
|
365
|
+
# binds a PREDICATE name only — see the module docstring), so
|
|
366
|
+
# every function declared here is pinned.
|
|
367
|
+
key = (name, arity)
|
|
368
|
+
if key not in structure.functions:
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"asp_holds_so: function {name!r}/{arity} has no "
|
|
371
|
+
"interpretation in the given structure."
|
|
372
|
+
)
|
|
373
|
+
interp = structure.functions[key]
|
|
374
|
+
for args in product(structure.domain, repeat=arity):
|
|
375
|
+
if callable(interp):
|
|
376
|
+
value = interp(*args)
|
|
377
|
+
else:
|
|
378
|
+
if args not in interp:
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"asp_holds_so: function {name!r}/{arity} is not "
|
|
381
|
+
f"total over the given structure's domain — "
|
|
382
|
+
f"undefined for arguments {args!r} (this "
|
|
383
|
+
"encoder's function encoding requires exactly "
|
|
384
|
+
"one result per input tuple, the same 'total "
|
|
385
|
+
"relation' reading emit_base_facts free-chooses)."
|
|
386
|
+
)
|
|
387
|
+
value = interp[args]
|
|
388
|
+
idxs = [self._pin_value(a, index_of, "function", name) for a in args]
|
|
389
|
+
idxs.append(self._pin_value(value, index_of, "function", name))
|
|
390
|
+
self.rules.append(f"{asp}({','.join(str(i) for i in idxs)}).")
|
|
391
|
+
|
|
392
|
+
for name, asp in self.const_asp.items():
|
|
393
|
+
# Constants are never second-order-bound either.
|
|
394
|
+
if name in structure.constants:
|
|
395
|
+
value = structure.constants[name]
|
|
396
|
+
else:
|
|
397
|
+
# No override -- mirror tarski.term_value's own Number
|
|
398
|
+
# fallback (a Number is scanned as a constant named by its
|
|
399
|
+
# value, read as the literal itself unless overridden;
|
|
400
|
+
# see modelfinder._Signature.scan / this module's own _term).
|
|
401
|
+
try:
|
|
402
|
+
value = int(name)
|
|
403
|
+
except ValueError:
|
|
404
|
+
raise ValueError(
|
|
405
|
+
f"asp_holds_so: constant {name!r} has no "
|
|
406
|
+
"interpretation in the given structure."
|
|
407
|
+
)
|
|
408
|
+
idx = self._pin_value(value, index_of, "constant", name)
|
|
409
|
+
self.rules.append(f"{asp}({idx}).")
|
|
410
|
+
|
|
411
|
+
def _order_numeric_extension(self, name: str, structure: Structure) -> Set[Tuple[Any, Any]]:
|
|
412
|
+
"""The numeric-fallback extension of an order comparison ``structure``
|
|
413
|
+
declares no extension for — mirrors
|
|
414
|
+
:func:`~unicode_logic_kit.semantics.tarski._order_value`'s rule (3).
|
|
415
|
+
|
|
416
|
+
Computed directly over ``structure.domain`` rather than over every
|
|
417
|
+
term this encoder's callers might build: every term
|
|
418
|
+
:meth:`emit_fixed_facts` ever pins (a constant, a function result, a
|
|
419
|
+
quantified variable) is already required to be a member of
|
|
420
|
+
``structure.domain`` (:meth:`_pin_value` raises otherwise), so the
|
|
421
|
+
domain's own individuals are exactly the ``(left, right)`` value
|
|
422
|
+
pairs :func:`~unicode_logic_kit.semantics.tarski._order_value` would
|
|
423
|
+
ever see for this predicate once no declared extension applies. A
|
|
424
|
+
pair where either individual is not a number (:func:`~unicode_logic_kit.semantics.tarski._is_number`,
|
|
425
|
+
which excludes ``bool``) is simply absent from the result — the same
|
|
426
|
+
"anything else is false" fallback ``_order_value`` itself uses, not
|
|
427
|
+
an error: a non-numeric domain is a legitimate case, not a gap.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
The set of ``(x, y)`` pairs of RAW domain individuals (not yet
|
|
431
|
+
translated to ``dom/1`` indices — the caller does that via
|
|
432
|
+
:meth:`_pin_value`, same as for a declared extension) for which
|
|
433
|
+
``name``'s numeric reading holds.
|
|
434
|
+
"""
|
|
435
|
+
op = _ORDER_OPS[name]
|
|
436
|
+
return {
|
|
437
|
+
(x, y)
|
|
438
|
+
for x in structure.domain
|
|
439
|
+
for y in structure.domain
|
|
440
|
+
if _is_number(x) and _is_number(y) and op(x, y)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
def _pin_value(self, value: Any, index_of: Dict[Any, int], kind: str, name: str) -> int:
|
|
444
|
+
"""Translate one structure individual to its ``dom/1`` index, or raise."""
|
|
445
|
+
if value not in index_of:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
f"asp_holds_so: the given structure's {kind} {name!r} "
|
|
448
|
+
f"produces the value {value!r}, which is not itself a member "
|
|
449
|
+
"of the structure's own domain."
|
|
450
|
+
)
|
|
451
|
+
return index_of[value]
|
|
452
|
+
|
|
453
|
+
# -- term evaluation ------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
def _term(self, term: Node, var_of: Dict[str, str], body: List[str]) -> str:
|
|
456
|
+
"""Return the ASP variable naming ``term``'s value, extending ``body``.
|
|
457
|
+
|
|
458
|
+
A term is not a single ASP value the way it is a single Python value
|
|
459
|
+
under :func:`~unicode_logic_kit.semantics.tarski.term_value` — every
|
|
460
|
+
constant/function application here is itself a RELATION (the total-
|
|
461
|
+
relation encoding), so evaluating a term means walking it and
|
|
462
|
+
emitting one join literal per constant/function occurrence, each
|
|
463
|
+
introducing a fresh ASP variable for its result. ``body`` is mutated
|
|
464
|
+
in place (appended to) rather than returned and concatenated by every
|
|
465
|
+
caller, since a term's evaluation is always exactly one ingredient of
|
|
466
|
+
a larger rule body being built up alongside it.
|
|
467
|
+
"""
|
|
468
|
+
if isinstance(term, Variable):
|
|
469
|
+
if term.name not in var_of:
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f"asp_models: variable {term.name!r} is not bound by any "
|
|
472
|
+
"enclosing quantifier — a free variable must have been "
|
|
473
|
+
"replaced by a parameter constant (see _closed_sentences) "
|
|
474
|
+
"before encoding."
|
|
475
|
+
)
|
|
476
|
+
return var_of[term.name]
|
|
477
|
+
if isinstance(term, Constant):
|
|
478
|
+
v = self._fresh_var()
|
|
479
|
+
body.append(f"{self.const_asp[term.name]}({v})")
|
|
480
|
+
return v
|
|
481
|
+
if isinstance(term, Number):
|
|
482
|
+
# Mirrors _Signature.scan exactly: a Number is registered as a
|
|
483
|
+
# constant named by its VALUE (numeral_key: 1 and 1.0 are '1'), NOT
|
|
484
|
+
# pinned to its literal value — see modelfinder._Signature.scan and
|
|
485
|
+
# tarski.term_value. Pinning it to the literal instead would look
|
|
486
|
+
# more natural but would silently diverge from what the oracle this
|
|
487
|
+
# module is verified against actually computes, breaking the one
|
|
488
|
+
# invariant this module exists to protect.
|
|
489
|
+
v = self._fresh_var()
|
|
490
|
+
body.append(f"{self.const_asp[numeral_key(term.value)]}({v})")
|
|
491
|
+
return v
|
|
492
|
+
if isinstance(term, Function):
|
|
493
|
+
arg_vars = [self._term(a, var_of, body) for a in term.args]
|
|
494
|
+
v = self._fresh_var()
|
|
495
|
+
key = (term.name, len(term.args))
|
|
496
|
+
body.append(f"{self.func_asp[key]}({','.join(arg_vars + [v])})")
|
|
497
|
+
return v
|
|
498
|
+
raise ValueError(
|
|
499
|
+
f"asp_models: {type(term).__name__} is not an encodable term "
|
|
500
|
+
"(only Variable/Constant/Number/Function are)."
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# -- formula encoding -----------------------------------------------------
|
|
504
|
+
|
|
505
|
+
def encode(self, node: Node, var_of: Dict[str, str]) -> Tuple[str, List[str]]:
|
|
506
|
+
"""Emit rules defining ``node``'s truth; return ``(reference, free_names)``.
|
|
507
|
+
|
|
508
|
+
``reference`` is the ASP literal text a PARENT rule uses to mention
|
|
509
|
+
this node's truth value (a bare head name if ``node`` is closed, or
|
|
510
|
+
``head(V1,…)`` over its free variables' current ASP names otherwise).
|
|
511
|
+
``free_names`` is the sorted list of ``node``'s free FOL variable
|
|
512
|
+
names. ``var_of`` maps every FOL variable name currently in scope
|
|
513
|
+
(bound by an enclosing quantifier already processed) to the ASP
|
|
514
|
+
variable name standing for it — extended with a FRESH ASP name on
|
|
515
|
+
entering a ``Quantifier``/``Count``, so a shadowed inner binder using
|
|
516
|
+
the same FOL name as an outer one never captures the outer binding
|
|
517
|
+
(mirrors :func:`~unicode_logic_kit.semantics.tarski._extend`'s
|
|
518
|
+
dict-overwrite shadowing semantics, which is exactly as correct here
|
|
519
|
+
as it is there for the same reason: assignment lookup is always the
|
|
520
|
+
innermost binding).
|
|
521
|
+
|
|
522
|
+
A ``∀`` quantifier is rewritten to ``¬∃¬`` and re-dispatched through
|
|
523
|
+
this same method rather than given its own encoding — see the module
|
|
524
|
+
docstring's "The ASP encoding" section for why.
|
|
525
|
+
"""
|
|
526
|
+
if isinstance(node, Quantifier) and node.type in _FORALL:
|
|
527
|
+
return self.encode(Not(Quantifier("∃", node.variable, Not(node.formula))), var_of)
|
|
528
|
+
|
|
529
|
+
free_names = sorted(_free_var_names_local(node))
|
|
530
|
+
head_args = [var_of[n] for n in free_names]
|
|
531
|
+
head = self._fresh_head()
|
|
532
|
+
head_ref = head if not head_args else f"{head}({','.join(head_args)})"
|
|
533
|
+
dom_lits = [f"dom({v})" for v in head_args]
|
|
534
|
+
|
|
535
|
+
if isinstance(node, Atom):
|
|
536
|
+
self._encode_atom(node, var_of, head_ref, dom_lits)
|
|
537
|
+
elif isinstance(node, Not):
|
|
538
|
+
inner_ref, _ = self.encode(node.formula, var_of)
|
|
539
|
+
self._rule(head_ref, dom_lits + [f"not {inner_ref}"])
|
|
540
|
+
elif isinstance(node, And):
|
|
541
|
+
l_ref, _ = self.encode(node.left, var_of)
|
|
542
|
+
r_ref, _ = self.encode(node.right, var_of)
|
|
543
|
+
self._rule(head_ref, dom_lits + [l_ref, r_ref])
|
|
544
|
+
elif isinstance(node, Or):
|
|
545
|
+
l_ref, _ = self.encode(node.left, var_of)
|
|
546
|
+
r_ref, _ = self.encode(node.right, var_of)
|
|
547
|
+
self._rule(head_ref, dom_lits + [l_ref])
|
|
548
|
+
self._rule(head_ref, dom_lits + [r_ref])
|
|
549
|
+
elif isinstance(node, Xor):
|
|
550
|
+
l_ref, _ = self.encode(node.left, var_of)
|
|
551
|
+
r_ref, _ = self.encode(node.right, var_of)
|
|
552
|
+
self._rule(head_ref, dom_lits + [l_ref, f"not {r_ref}"])
|
|
553
|
+
self._rule(head_ref, dom_lits + [r_ref, f"not {l_ref}"])
|
|
554
|
+
elif isinstance(node, Implies):
|
|
555
|
+
l_ref, _ = self.encode(node.left, var_of)
|
|
556
|
+
r_ref, _ = self.encode(node.right, var_of)
|
|
557
|
+
self._rule(head_ref, dom_lits + [f"not {l_ref}"])
|
|
558
|
+
self._rule(head_ref, dom_lits + [r_ref])
|
|
559
|
+
elif isinstance(node, Iff):
|
|
560
|
+
l_ref, _ = self.encode(node.left, var_of)
|
|
561
|
+
r_ref, _ = self.encode(node.right, var_of)
|
|
562
|
+
self._rule(head_ref, dom_lits + [l_ref, r_ref])
|
|
563
|
+
self._rule(head_ref, dom_lits + [f"not {l_ref}", f"not {r_ref}"])
|
|
564
|
+
elif isinstance(node, Quantifier): # node.type in _EXISTS, by elimination
|
|
565
|
+
inner_var = self._fresh_var()
|
|
566
|
+
var_of2 = dict(var_of)
|
|
567
|
+
var_of2[node.variable.name] = inner_var
|
|
568
|
+
inner_ref, _ = self.encode(node.formula, var_of2)
|
|
569
|
+
self._rule(head_ref, dom_lits + [inner_ref])
|
|
570
|
+
elif isinstance(node, Count):
|
|
571
|
+
inner_var = self._fresh_var()
|
|
572
|
+
var_of2 = dict(var_of)
|
|
573
|
+
var_of2[node.variable.name] = inner_var
|
|
574
|
+
inner_ref, _ = self.encode(node.formula, var_of2)
|
|
575
|
+
op = {"ge": ">=", "le": "<=", "eq": "="}[node.op]
|
|
576
|
+
agg = f"#count{{ {inner_var} : dom({inner_var}), {inner_ref} }} {op} {node.n.value}"
|
|
577
|
+
self._rule(head_ref, dom_lits + [agg])
|
|
578
|
+
else:
|
|
579
|
+
raise ValueError(
|
|
580
|
+
f"asp_models: {type(node).__name__} reached the encoder despite "
|
|
581
|
+
"_check_fragment — this is an internal invariant violation, not "
|
|
582
|
+
"a normal unsupported-fragment report."
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return head_ref, free_names
|
|
586
|
+
|
|
587
|
+
def _rule(self, head_ref: str, body: List[str]) -> None:
|
|
588
|
+
"""Append one ``head :- body`` rule (``body`` is always non-empty here)."""
|
|
589
|
+
self.rules.append(f"{head_ref} :- {', '.join(body)}.")
|
|
590
|
+
|
|
591
|
+
def _encode_atom(self, node: Atom, var_of: Dict[str, str], head_ref: str, dom_lits: List[str]) -> None:
|
|
592
|
+
"""Emit the rule(s) defining an ``Atom``'s auxiliary head atom.
|
|
593
|
+
|
|
594
|
+
``=``/``≠`` at arity 2 get the built-in identity reading (an ASP
|
|
595
|
+
variable comparison, never a free-choice predicate); every other
|
|
596
|
+
atom — including the order comparisons ``< > ≤ ≥``, which
|
|
597
|
+
:func:`~unicode_logic_kit.semantics.modelfinder._Signature.scan`
|
|
598
|
+
registers as ORDINARY predicates, not built-ins, exactly like
|
|
599
|
+
:func:`~unicode_logic_kit.semantics.tarski._order_value`'s own
|
|
600
|
+
"a declared extension wins" reading — is a free-choice predicate
|
|
601
|
+
lookup, so no special-casing is needed for them here beyond what
|
|
602
|
+
the general branch already does.
|
|
603
|
+
"""
|
|
604
|
+
body: List[str] = list(dom_lits)
|
|
605
|
+
constant = _truth_value(node)
|
|
606
|
+
if constant is not None:
|
|
607
|
+
# `$true` holds wherever the head is defined at all, `$false` nowhere: no
|
|
608
|
+
# rule defines the head, so the solver reads it as false.
|
|
609
|
+
if constant:
|
|
610
|
+
if body:
|
|
611
|
+
self._rule(head_ref, body)
|
|
612
|
+
else:
|
|
613
|
+
self.rules.append(f"{head_ref}.")
|
|
614
|
+
return
|
|
615
|
+
if node.predicate in ("=", "≠") and len(node.args) == 2:
|
|
616
|
+
a = self._term(node.args[0], var_of, body)
|
|
617
|
+
b = self._term(node.args[1], var_of, body)
|
|
618
|
+
body.append(f"{a}{'=' if node.predicate == '=' else '!='}{b}")
|
|
619
|
+
self._rule(head_ref, body)
|
|
620
|
+
return
|
|
621
|
+
arg_vars = [self._term(a, var_of, body) for a in node.args]
|
|
622
|
+
key = (node.predicate, len(node.args))
|
|
623
|
+
if key not in self.pred_asp:
|
|
624
|
+
raise KeyError(
|
|
625
|
+
f"asp_models: predicate {node.predicate!r}/{len(node.args)} is "
|
|
626
|
+
"not in the scanned signature — internal invariant violation "
|
|
627
|
+
"(every predicate an encoded sentence uses must have been "
|
|
628
|
+
"scanned into the signature first)."
|
|
629
|
+
)
|
|
630
|
+
pred_lit = self.pred_asp[key] if not arg_vars else f"{self.pred_asp[key]}({','.join(arg_vars)})"
|
|
631
|
+
body.append(pred_lit)
|
|
632
|
+
self._rule(head_ref, body)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _free_var_names_local(node: Node) -> Set[str]:
|
|
636
|
+
"""``_free_var_names(node)`` from modelfinder, at the default (empty) bound.
|
|
637
|
+
|
|
638
|
+
A thin wrapper purely so every call site above reads as "the free
|
|
639
|
+
variables of this node" without repeating the ``frozenset()`` default
|
|
640
|
+
argument; kept local (not re-exported) since it is a one-line adapter,
|
|
641
|
+
not a new piece of logic.
|
|
642
|
+
"""
|
|
643
|
+
from .modelfinder import _free_var_names
|
|
644
|
+
return _free_var_names(node)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# =============================================================================
|
|
648
|
+
# Model decoding — clingo's answer set back into modelfinder's own shape
|
|
649
|
+
# =============================================================================
|
|
650
|
+
|
|
651
|
+
def _decode_model(model, enc: "_AspEncoder", sig: "_Signature", size: int
|
|
652
|
+
) -> Tuple[Dict[str, int], Dict[Tuple[str, int], dict], Dict[Tuple[str, int], set]]:
|
|
653
|
+
"""Turn one clingo answer set into ``(constants, functions, predicates)``.
|
|
654
|
+
|
|
655
|
+
The exact shapes :func:`~unicode_logic_kit.semantics.modelfinder._interpretations`
|
|
656
|
+
produces (domain individuals are the plain ints ``0..size-1``, NOT the
|
|
657
|
+
strings :mod:`~unicode_logic_kit.semantics.structures.FiniteStructure` uses)
|
|
658
|
+
— required for byte-identical comparison against
|
|
659
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`'s own
|
|
660
|
+
output, and for feeding straight into ``_circ_profile``/``_fixed_key``.
|
|
661
|
+
|
|
662
|
+
Raises:
|
|
663
|
+
RuntimeError: a constant's choice rule produced a count of true
|
|
664
|
+
atoms other than 1 in this answer set — the ``1 { … } 1`` choice
|
|
665
|
+
rule :meth:`_AspEncoder.emit_base_facts` emits is supposed to
|
|
666
|
+
make this impossible, so seeing it would mean the generated
|
|
667
|
+
program itself is broken, not that the caller misused anything.
|
|
668
|
+
"""
|
|
669
|
+
by_name: Dict[str, list] = {}
|
|
670
|
+
for sym in model.symbols(atoms=True):
|
|
671
|
+
by_name.setdefault(sym.name, []).append(sym)
|
|
672
|
+
|
|
673
|
+
constants: Dict[str, int] = {}
|
|
674
|
+
for name, asp in enc.const_asp.items():
|
|
675
|
+
matches = by_name.get(asp, [])
|
|
676
|
+
if len(matches) != 1:
|
|
677
|
+
raise RuntimeError(
|
|
678
|
+
f"asp_models: constant {name!r} ({asp}) has {len(matches)} "
|
|
679
|
+
"true atoms in an answer set, expected exactly 1 — the "
|
|
680
|
+
"'1 { … } 1' choice rule should make this impossible."
|
|
681
|
+
)
|
|
682
|
+
(arg,) = matches[0].arguments
|
|
683
|
+
constants[name] = arg.number
|
|
684
|
+
|
|
685
|
+
functions: Dict[Tuple[str, int], dict] = {}
|
|
686
|
+
for (name, arity), asp in enc.func_asp.items():
|
|
687
|
+
table: Dict[Tuple[int, ...], int] = {}
|
|
688
|
+
for sym in by_name.get(asp, []):
|
|
689
|
+
args = tuple(a.number for a in sym.arguments)
|
|
690
|
+
table[args[:-1]] = args[-1]
|
|
691
|
+
functions[(name, arity)] = table
|
|
692
|
+
|
|
693
|
+
predicates: Dict[Tuple[str, int], set] = {}
|
|
694
|
+
for (name, arity), asp in enc.pred_asp.items():
|
|
695
|
+
predicates[(name, arity)] = {
|
|
696
|
+
tuple(a.number for a in sym.arguments) for sym in by_name.get(asp, [])
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return constants, functions, predicates
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# =============================================================================
|
|
703
|
+
# Shared setup: signature scan + program assembly for both public functions
|
|
704
|
+
# =============================================================================
|
|
705
|
+
|
|
706
|
+
def _closed_sentences(premises: Iterable[Node]) -> List[Node]:
|
|
707
|
+
"""``premises`` with every free variable read as a PARAMETER of the problem.
|
|
708
|
+
|
|
709
|
+
A free variable is one unknown element, the same in every premise: it is replaced,
|
|
710
|
+
in all the premises together, by a constant of its own name
|
|
711
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), so ``P(x), ¬P(y)`` has a
|
|
712
|
+
model (``x`` and ``y`` are two elements) and ``P(x), ¬P(x)`` has none. A premise is
|
|
713
|
+
never closed universally: ``∀x P(x)`` is another premise than ``P(x)``. The structure
|
|
714
|
+
a call returns interprets the parameter like any constant, ``constants['x']``.
|
|
715
|
+
|
|
716
|
+
Raises:
|
|
717
|
+
NotImplementedError: a free variable has the spelling of a constant of the
|
|
718
|
+
premises (a structure holds one entry per name).
|
|
719
|
+
"""
|
|
720
|
+
sentences, _ = parameterize(list(premises), after_variables=True)
|
|
721
|
+
return sentences
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _build_program(sentences: List[Node], size: int) -> Tuple[str, "_AspEncoder", "_Signature"]:
|
|
725
|
+
"""Scan ``sentences`` for their signature and assemble the full ASP program.
|
|
726
|
+
|
|
727
|
+
Shared by :func:`asp_find_model` and :func:`asp_minimal_models` — the
|
|
728
|
+
only difference between the two callers is how many answer sets clingo
|
|
729
|
+
is asked for, not how the program is built.
|
|
730
|
+
|
|
731
|
+
Raises:
|
|
732
|
+
TypeError: ``size`` is not a plain ``int``, or a member of
|
|
733
|
+
``sentences`` still has a free variable (an internal invariant —
|
|
734
|
+
see :func:`_closed_sentences`, applied by both public callers
|
|
735
|
+
before this function ever runs).
|
|
736
|
+
ValueError: ``size < 1``, or a sentence uses a construct outside
|
|
737
|
+
this module's fragment (see :func:`_check_fragment`).
|
|
738
|
+
"""
|
|
739
|
+
if isinstance(size, bool) or not isinstance(size, int) or size < 1:
|
|
740
|
+
raise ValueError(f"asp_models: size must be an int >= 1, got {size!r}.")
|
|
741
|
+
_check_fragment(sentences)
|
|
742
|
+
|
|
743
|
+
sig = _Signature()
|
|
744
|
+
for s in sentences:
|
|
745
|
+
sig.scan(s)
|
|
746
|
+
|
|
747
|
+
enc = _AspEncoder(size)
|
|
748
|
+
enc.declare_signature(sig)
|
|
749
|
+
enc.emit_base_facts()
|
|
750
|
+
for s in sentences:
|
|
751
|
+
head_ref, free = enc.encode(s, {})
|
|
752
|
+
if free:
|
|
753
|
+
raise TypeError(
|
|
754
|
+
f"asp_models: sentence {s.to_unicode_str()!r} still has free "
|
|
755
|
+
f"variable(s) {free} after the parameters were substituted — internal "
|
|
756
|
+
"invariant violation."
|
|
757
|
+
)
|
|
758
|
+
enc.rules.append(f":- not {head_ref}.")
|
|
759
|
+
|
|
760
|
+
return "\n".join(enc.rules), enc, sig
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
# =============================================================================
|
|
764
|
+
# Public API
|
|
765
|
+
# =============================================================================
|
|
766
|
+
|
|
767
|
+
def asp_find_model(premises: Iterable[Node], size: int = 3) -> Optional[Structure]:
|
|
768
|
+
"""Return one finite :class:`~unicode_logic_kit.semantics.tarski.Structure`
|
|
769
|
+
of exactly ``size`` individuals satisfying every one of ``premises``, or
|
|
770
|
+
``None``.
|
|
771
|
+
|
|
772
|
+
The ASP analog of
|
|
773
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model`, but grounded
|
|
774
|
+
at exactly ``size`` rather than searching ``1 … max_size`` — see the
|
|
775
|
+
module docstring's "``size`` is a single domain size, not a bound"
|
|
776
|
+
section; a caller wanting the multi-size search calls this in a loop.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
premises: the sentences to satisfy together. A free variable is a
|
|
780
|
+
PARAMETER of the problem, one unknown element shared by every
|
|
781
|
+
premise (:func:`_closed_sentences`), as in
|
|
782
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model`:
|
|
783
|
+
``P(x), ¬P(y)`` has a model, ``P(x), ¬P(x)`` has none, and the
|
|
784
|
+
returned structure reports the element under the variable's name,
|
|
785
|
+
``constants['x']``.
|
|
786
|
+
size: the exact domain size to search, ``>= 1``.
|
|
787
|
+
|
|
788
|
+
Returns:
|
|
789
|
+
A structure over the domain ``0 … size-1`` satisfying every premise,
|
|
790
|
+
or ``None`` if clingo finds the grounded program unsatisfiable at
|
|
791
|
+
this size (NOT evidence of unsatisfiability at any other size, or of
|
|
792
|
+
first-order unsatisfiability — the same one-sided-boundedness every
|
|
793
|
+
finite-model search in this kit carries).
|
|
794
|
+
|
|
795
|
+
Raises:
|
|
796
|
+
ValueError: ``size < 1``, or a premise uses a construct outside this
|
|
797
|
+
module's fragment (see the module docstring).
|
|
798
|
+
NotImplementedError: a free variable has the spelling of a constant of
|
|
799
|
+
the premises.
|
|
800
|
+
"""
|
|
801
|
+
import clingo
|
|
802
|
+
|
|
803
|
+
sentences = _closed_sentences(premises)
|
|
804
|
+
program, enc, sig = _build_program(sentences, size)
|
|
805
|
+
|
|
806
|
+
ctl = clingo.Control(["1"], logger=_silent)
|
|
807
|
+
ctl.add("base", [], program)
|
|
808
|
+
ctl.ground([("base", [])])
|
|
809
|
+
|
|
810
|
+
with ctl.solve(yield_=True) as handle:
|
|
811
|
+
for model in handle:
|
|
812
|
+
constants, functions, predicates = _decode_model(model, enc, sig, size)
|
|
813
|
+
return Structure(tuple(range(size)), constants=constants,
|
|
814
|
+
functions=functions, predicates=predicates)
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def asp_minimal_models(premises: Iterable[Node], circumscribed: Optional[Set[str]] = None,
|
|
819
|
+
size: int = 3) -> List[Structure]:
|
|
820
|
+
"""Return the ≤-minimal finite models of ``premises`` at exactly ``size``.
|
|
821
|
+
|
|
822
|
+
clingo enumerates EVERY model of the grounded ``premises`` at this
|
|
823
|
+
domain size (the fragment gate, choice rules and per-connective encoding
|
|
824
|
+
are described in the module docstring); this function then applies
|
|
825
|
+
:mod:`~unicode_logic_kit.semantics.nonmonotonic`'s own, UNMODIFIED
|
|
826
|
+
minimality filter (``_fixed_key`` groups models sharing a domain,
|
|
827
|
+
constants, functions and every non-circumscribed predicate; within each
|
|
828
|
+
group, ``_circ_profile``/``_strictly_below`` keep the ones no other group
|
|
829
|
+
member's circumscribed-predicate extensions are a strict subset of) — the
|
|
830
|
+
exact same filter
|
|
831
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models` applies to
|
|
832
|
+
its own, brute-force-enumerated candidates. See the module docstring's
|
|
833
|
+
"Why reuse rather than reimplement the filter" section for why sharing
|
|
834
|
+
this code, rather than writing a second copy of the same comparison, is
|
|
835
|
+
what makes this function's answer trustworthy.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
premises: the sentences every returned model satisfies. A free
|
|
839
|
+
variable is a PARAMETER, one constant of its own name shared by
|
|
840
|
+
every premise (:func:`_closed_sentences`), exactly as
|
|
841
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`
|
|
842
|
+
reads it; the parameter belongs to the fixed part two models must
|
|
843
|
+
share to be compared.
|
|
844
|
+
circumscribed: the predicate NAMES to minimise (arity is whatever the
|
|
845
|
+
premises use it at). ``None`` (the default) minimises every
|
|
846
|
+
predicate the premises mention — the same default
|
|
847
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`
|
|
848
|
+
uses, and the same "bare name, not a ``(name, arity)`` pair"
|
|
849
|
+
convention (unlike
|
|
850
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_formula`'s
|
|
851
|
+
``CircSpec``, which additionally accepts explicit arities — not
|
|
852
|
+
needed here since every predicate this function can minimise
|
|
853
|
+
was, by construction, already found by scanning ``premises``).
|
|
854
|
+
size: the exact domain size to search, ``>= 1`` — see the module
|
|
855
|
+
docstring's "``size`` is a single domain size, not a bound"
|
|
856
|
+
section for how this differs from ``minimal_models``'s
|
|
857
|
+
``max_size``.
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
Every ≤-minimal structure (see
|
|
861
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`'s own
|
|
862
|
+
docstring for the exact ``≤`` relation) over the domain
|
|
863
|
+
``0 … size-1`` that satisfies every premise. Empty if no model
|
|
864
|
+
exists at this size at all.
|
|
865
|
+
|
|
866
|
+
Raises:
|
|
867
|
+
ValueError: ``size < 1``, or a premise uses a construct outside this
|
|
868
|
+
module's fragment (see the module docstring).
|
|
869
|
+
NotImplementedError: a free variable has the spelling of a constant of
|
|
870
|
+
the premises.
|
|
871
|
+
"""
|
|
872
|
+
import clingo
|
|
873
|
+
|
|
874
|
+
sentences = _closed_sentences(premises)
|
|
875
|
+
program, enc, sig = _build_program(sentences, size)
|
|
876
|
+
|
|
877
|
+
pred_sig = sorted(sig.predicates)
|
|
878
|
+
circ = set(circumscribed) if circumscribed is not None else {n for n, _ in pred_sig}
|
|
879
|
+
|
|
880
|
+
ctl = clingo.Control(["0"], logger=_silent)
|
|
881
|
+
ctl.add("base", [], program)
|
|
882
|
+
ctl.ground([("base", [])])
|
|
883
|
+
|
|
884
|
+
found = []
|
|
885
|
+
with ctl.solve(yield_=True) as handle:
|
|
886
|
+
for model in handle:
|
|
887
|
+
constants, functions, predicates = _decode_model(model, enc, sig, size)
|
|
888
|
+
structure = Structure(tuple(range(size)), constants=constants,
|
|
889
|
+
functions=functions, predicates=predicates)
|
|
890
|
+
found.append((constants, functions, predicates, structure))
|
|
891
|
+
|
|
892
|
+
# Group by fixed part (domain + constants + functions + non-circumscribed
|
|
893
|
+
# predicate extensions); within each group keep only the models no other
|
|
894
|
+
# member's circumscribed-predicate profile is strictly below — verbatim
|
|
895
|
+
# the same grouping loop
|
|
896
|
+
# :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models` runs over
|
|
897
|
+
# its OWN (brute-force-enumerated) `found`, so the two functions can only
|
|
898
|
+
# ever disagree about which models were found, never about which of them
|
|
899
|
+
# count as minimal.
|
|
900
|
+
groups: Dict[tuple, list] = {}
|
|
901
|
+
for entry in found:
|
|
902
|
+
constants, functions, predicates, _ = entry
|
|
903
|
+
key = _fixed_key(constants, functions, predicates, pred_sig, circ)
|
|
904
|
+
profile = _circ_profile(predicates, pred_sig, circ)
|
|
905
|
+
groups.setdefault(key, []).append((entry, profile))
|
|
906
|
+
|
|
907
|
+
result: List[Structure] = []
|
|
908
|
+
for grp in groups.values():
|
|
909
|
+
for (entry, profile) in grp:
|
|
910
|
+
if not any(_strictly_below(other, profile) for (e2, other) in grp if e2 is not entry):
|
|
911
|
+
result.append(entry[3])
|
|
912
|
+
return result
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# =============================================================================
|
|
916
|
+
# Single-block second-order checking (roadmap C24): asp_holds_so
|
|
917
|
+
# =============================================================================
|
|
918
|
+
#
|
|
919
|
+
# secondorder.satisfies_so evaluates ∀P/∃P by brute-force enumeration of every
|
|
920
|
+
# relation of P's arity — 2 ** (n ** k), doubly exponential (see that module's
|
|
921
|
+
# docstring). The exact mechanism this module already uses for an ORDINARY
|
|
922
|
+
# predicate — "a free ASP choice, propagation-pruned by clingo instead of
|
|
923
|
+
# materialised in Python" (module docstring, "The ASP encoding") — applies
|
|
924
|
+
# just as well to a SECOND-ORDER-BOUND predicate, PROVIDED the quantifier
|
|
925
|
+
# nesting is simple enough that "propagation-pruned choice + one solve" is a
|
|
926
|
+
# SOUND reduction: a single leading block of SAME-polarity SecondOrderQuantifier
|
|
927
|
+
# occurrences (∃P1…∃Pk or ∀P1…∀Pk, nothing else of that kind anywhere in the
|
|
928
|
+
# sentence). One ∃-block is an ordinary satisfiability check (NP-flavoured,
|
|
929
|
+
# exactly what clingo solves). One ∀-block reduces to a single
|
|
930
|
+
# UNSAT-of-the-negation check (co-NP-flavoured — ask whether any answer set
|
|
931
|
+
# witnesses the negation; none existing means the block holds for every
|
|
932
|
+
# choice). Genuine ALTERNATION (∀P∃Q…) is a strictly harder complexity class
|
|
933
|
+
# (Σ2p/Π2p) that plain, non-disjunctive clingo choice rules do not capture
|
|
934
|
+
# soundly — see secondorder.py's own module docstring and roadmap C24's
|
|
935
|
+
# existing_coverage for the argument — so it is refused here, loudly, rather
|
|
936
|
+
# than silently mishandled.
|
|
937
|
+
|
|
938
|
+
def _so_quantifier_chain(sentence: Node) -> Tuple[Optional[str], List[SecondOrderQuantifier]]:
|
|
939
|
+
"""Validate ``sentence``'s SecondOrderQuantifier occurrences; return its block.
|
|
940
|
+
|
|
941
|
+
Returns ``(None, [])`` if ``sentence`` has no SecondOrderQuantifier at all
|
|
942
|
+
(a purely classical sentence — :func:`asp_holds_so` accepts this as the
|
|
943
|
+
degenerate zero-quantifier case). Otherwise returns ``("forall" | "exists",
|
|
944
|
+
chain)``, ``chain`` being the block's nodes in outer-to-inner order.
|
|
945
|
+
|
|
946
|
+
"A single block" means every SecondOrderQuantifier occurrence in
|
|
947
|
+
``sentence`` — wherever it sits in the tree, including nested inside
|
|
948
|
+
ordinary classical structure such as
|
|
949
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_entails_so`'s
|
|
950
|
+
``Implies``/``And`` — is reachable from exactly ONE entry node by
|
|
951
|
+
following ``.formula`` through SecondOrderQuantifier nodes ONLY, all of
|
|
952
|
+
the SAME polarity, until reaching a body with no further
|
|
953
|
+
SecondOrderQuantifier anywhere inside it. A second, unrelated occurrence
|
|
954
|
+
(a sibling block, or one merely nested a connective away rather than
|
|
955
|
+
directly wrapping the next) is rejected exactly like true alternation —
|
|
956
|
+
both are outside the fragment :func:`asp_holds_so` soundly covers.
|
|
957
|
+
|
|
958
|
+
Raises:
|
|
959
|
+
ValueError: mixed ``∀``/``∃`` polarities, an unrecognised quantifier
|
|
960
|
+
spelling, more than one entry point (disconnected or
|
|
961
|
+
connective-separated occurrences), or a SecondOrderQuantifier
|
|
962
|
+
nested inside the block's own innermost body — naming the
|
|
963
|
+
offending sentence in every case.
|
|
964
|
+
"""
|
|
965
|
+
all_so = [n for n in sentence.walk() if isinstance(n, SecondOrderQuantifier)]
|
|
966
|
+
if not all_so:
|
|
967
|
+
return None, []
|
|
968
|
+
|
|
969
|
+
types = {n.type for n in all_so}
|
|
970
|
+
forall_seen = types & set(_FORALL)
|
|
971
|
+
exists_seen = types & set(_EXISTS)
|
|
972
|
+
unknown = types - forall_seen - exists_seen
|
|
973
|
+
if unknown:
|
|
974
|
+
raise ValueError(
|
|
975
|
+
f"asp_holds_so: unknown SecondOrderQuantifier type(s) {sorted(unknown)!r} "
|
|
976
|
+
f"in {sentence.to_unicode_str()!r} — only unsorted "
|
|
977
|
+
f"{_FORALL + _EXISTS} are supported here."
|
|
978
|
+
)
|
|
979
|
+
if forall_seen and exists_seen:
|
|
980
|
+
raise ValueError(
|
|
981
|
+
f"asp_holds_so: {sentence.to_unicode_str()!r} mixes ∀ and ∃ "
|
|
982
|
+
"second-order quantifiers (alternation) — only a single leading "
|
|
983
|
+
"block of the SAME polarity is supported here; use "
|
|
984
|
+
"secondorder.satisfies_so for alternating second-order "
|
|
985
|
+
"quantification."
|
|
986
|
+
)
|
|
987
|
+
block_type = "forall" if forall_seen else "exists"
|
|
988
|
+
|
|
989
|
+
bodies_that_are_so = {
|
|
990
|
+
id(n.formula) for n in all_so if isinstance(n.formula, SecondOrderQuantifier)
|
|
991
|
+
}
|
|
992
|
+
entry_points = [n for n in all_so if id(n) not in bodies_that_are_so]
|
|
993
|
+
if len(entry_points) != 1:
|
|
994
|
+
raise ValueError(
|
|
995
|
+
f"asp_holds_so: {sentence.to_unicode_str()!r} has "
|
|
996
|
+
f"{len(entry_points)} separate second-order-quantifier chains "
|
|
997
|
+
"(nested through a non-quantifier connective, or genuinely "
|
|
998
|
+
"scattered) — only a single leading block, with nothing else of "
|
|
999
|
+
"its kind anywhere in the sentence, is supported here; use "
|
|
1000
|
+
"secondorder.satisfies_so instead."
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
chain: List[SecondOrderQuantifier] = []
|
|
1004
|
+
cur: Node = entry_points[0]
|
|
1005
|
+
while isinstance(cur, SecondOrderQuantifier):
|
|
1006
|
+
chain.append(cur)
|
|
1007
|
+
cur = cur.formula
|
|
1008
|
+
|
|
1009
|
+
if any(isinstance(n, SecondOrderQuantifier) for n in cur.walk()):
|
|
1010
|
+
raise ValueError(
|
|
1011
|
+
f"asp_holds_so: a SecondOrderQuantifier is nested inside the "
|
|
1012
|
+
f"single block's own body in {sentence.to_unicode_str()!r} "
|
|
1013
|
+
"(quantifier alternation) — use secondorder.satisfies_so instead."
|
|
1014
|
+
)
|
|
1015
|
+
if len(chain) != len(all_so):
|
|
1016
|
+
raise ValueError( # pragma: no cover - defensive; unreachable given the checks above
|
|
1017
|
+
f"asp_holds_so: the second-order quantifiers in "
|
|
1018
|
+
f"{sentence.to_unicode_str()!r} do not form a single connected "
|
|
1019
|
+
"chain — use secondorder.satisfies_so instead."
|
|
1020
|
+
)
|
|
1021
|
+
return block_type, chain
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _replace_so_block(node: Node, entry: SecondOrderQuantifier, replacement: Node) -> Node:
|
|
1025
|
+
"""Rebuild ``node``, replacing the ONE occurrence ``entry`` (by identity) with ``replacement``.
|
|
1026
|
+
|
|
1027
|
+
``entry`` is found by object identity (``is``), not structural equality —
|
|
1028
|
+
the single node :func:`_so_quantifier_chain` identified as the block's
|
|
1029
|
+
entry point, wherever it sits in ``node``. Used to splice the block's own
|
|
1030
|
+
(already-computed) truth value back into the surrounding classical
|
|
1031
|
+
sentence — see :func:`asp_holds_so`.
|
|
1032
|
+
"""
|
|
1033
|
+
if node is entry:
|
|
1034
|
+
return replacement
|
|
1035
|
+
return node.map_children(lambda c: _replace_so_block(c, entry, replacement))
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def asp_holds_so(sentence: Node, structure: Structure) -> bool:
|
|
1039
|
+
"""Whether ``structure`` satisfies second-order ``sentence`` (ASP-grounded).
|
|
1040
|
+
|
|
1041
|
+
Mirrors :func:`~unicode_logic_kit.semantics.secondorder.holds`'s signature,
|
|
1042
|
+
and — on the fragment described below — its exact answer (verified
|
|
1043
|
+
differentially against ``secondorder.satisfies_so``/``holds`` in this
|
|
1044
|
+
module's test suite; see roadmap C24). Restricted to sentences whose
|
|
1045
|
+
:class:`~unicode_logic_kit.fol.nodes.SecondOrderQuantifier` occurrences form
|
|
1046
|
+
a SINGLE, contiguous, SAME-polarity block — see
|
|
1047
|
+
:func:`_so_quantifier_chain` for exactly what that means (the block need
|
|
1048
|
+
not be at ``sentence``'s outermost node: see
|
|
1049
|
+
:func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_entails_so`'s
|
|
1050
|
+
output, whose ``∀``-block sits inside an ``Implies``/``And``). A sentence
|
|
1051
|
+
with NO second-order quantifier at all is accepted too, checked as an
|
|
1052
|
+
ordinary closed classical formula against ``structure``.
|
|
1053
|
+
|
|
1054
|
+
THE ENCODING is a TWO-STEP evaluation, not a single whole-sentence ASP
|
|
1055
|
+
solve — a single solve over the WHOLE sentence with the block's wrapper
|
|
1056
|
+
merely stripped is UNSOUND once the block sits in a position where sign
|
|
1057
|
+
matters (e.g. the ANTECEDENT of an ``Implies``, as
|
|
1058
|
+
``circumscription_entails_so``'s own output does): negating the whole
|
|
1059
|
+
stripped sentence to test the ``∀`` case conflates the block's own
|
|
1060
|
+
polarity with the surrounding connective's, silently turning a nested
|
|
1061
|
+
``∀`` into something that answers like an ``∃`` (caught by this module's
|
|
1062
|
+
own differential tests against ``satisfies_so`` — a hand-checked
|
|
1063
|
+
``Implies(∀P(P(a)→Q(a)), Q(a))`` example disagreed before this two-step
|
|
1064
|
+
design). Instead:
|
|
1065
|
+
|
|
1066
|
+
1. The block's OWN truth value is computed in ISOLATION: its innermost
|
|
1067
|
+
body (``chain[-1].formula`` — the part with no further
|
|
1068
|
+
``SecondOrderQuantifier``) is ASP-encoded on its own — the SO-bound
|
|
1069
|
+
predicate name(s) get the ordinary free ASP choice any OTHER declared
|
|
1070
|
+
predicate gets (no per-node quantifier encoding is needed; see the
|
|
1071
|
+
module docstring's "The ASP encoding"), and every other symbol this
|
|
1072
|
+
body uses is PINNED to ``structure``'s own extension
|
|
1073
|
+
(:meth:`_AspEncoder.emit_fixed_facts`). One clingo solve then asks:
|
|
1074
|
+
for a ``∃``-block, is the body true in SOME answer set (SAT, ``:- not
|
|
1075
|
+
head.``); for a ``∀``-block, is the body's NEGATION true in NO answer
|
|
1076
|
+
set (UNSAT). This is exactly ``asp_find_model``'s own SAT-checking
|
|
1077
|
+
shape, just scoped to the block's body and a partly-pinned rather than
|
|
1078
|
+
fully-free signature.
|
|
1079
|
+
2. That single Boolean is spliced back into ``sentence`` in place of the
|
|
1080
|
+
block (:func:`_replace_so_block`, by object identity) as a fresh
|
|
1081
|
+
nullary atom whose extension in an EXTENDED copy of ``structure`` is
|
|
1082
|
+
exactly that Boolean, and the RESULT — an ordinary classical sentence,
|
|
1083
|
+
since the block is now just a 0-ary atom — is handed to
|
|
1084
|
+
:func:`~unicode_logic_kit.semantics.tarski.satisfies`, the real
|
|
1085
|
+
recursive Tarskian evaluator, which threads the surrounding
|
|
1086
|
+
``Implies``/``And``/``Not``/… polarity correctly because it is not
|
|
1087
|
+
re-derived here, just reused.
|
|
1088
|
+
|
|
1089
|
+
The block's own body may not have any free object variable — genuinely
|
|
1090
|
+
free in ``sentence`` (which must be closed) or bound by an object-level
|
|
1091
|
+
quantifier OUTSIDE the block — since step 1 evaluates it as a
|
|
1092
|
+
self-contained closed sentence; see the ``Raises`` section.
|
|
1093
|
+
|
|
1094
|
+
Requires ``structure``'s functions the block's body uses to be
|
|
1095
|
+
interpreted TOTALLY over its own domain, matching this module's own
|
|
1096
|
+
"total relation" function encoding; see
|
|
1097
|
+
:meth:`_AspEncoder.emit_fixed_facts`.
|
|
1098
|
+
|
|
1099
|
+
Args:
|
|
1100
|
+
sentence: a closed (no free object variable) second-order sentence,
|
|
1101
|
+
single-block as above; the block's own body must, once isolated,
|
|
1102
|
+
be within :func:`_check_fragment`'s classical fragment
|
|
1103
|
+
(``Atom``/``Not``/``And``/``Or``/``Xor``/``Implies``/``Iff``/
|
|
1104
|
+
``Quantifier``/``Count`` over
|
|
1105
|
+
``Variable``/``Constant``/``Number``/``Function`` terms; see the
|
|
1106
|
+
module docstring's "Fragment supported here" section) — the
|
|
1107
|
+
surrounding classical structure the block sits inside, if any,
|
|
1108
|
+
is evaluated by the FULL Tarskian evaluator instead (step 2
|
|
1109
|
+
above), so it is not limited to this narrower fragment.
|
|
1110
|
+
structure: the structure to check ``sentence`` against — an
|
|
1111
|
+
arbitrary :class:`~unicode_logic_kit.semantics.tarski.Structure`
|
|
1112
|
+
(any hashable domain, not necessarily the ``0..n-1`` integers
|
|
1113
|
+
:func:`asp_find_model`/:func:`asp_minimal_models` produce; this
|
|
1114
|
+
function builds its own index for the ASP encoding).
|
|
1115
|
+
|
|
1116
|
+
Returns:
|
|
1117
|
+
Whether ``structure`` satisfies ``sentence``.
|
|
1118
|
+
|
|
1119
|
+
Raises:
|
|
1120
|
+
ValueError: the SecondOrderQuantifier occurrences in ``sentence`` do
|
|
1121
|
+
not form a single same-polarity block (alternation, nesting,
|
|
1122
|
+
mixed polarity, or more than one chain — naming the offending
|
|
1123
|
+
sentence); the block's own body uses a construct outside
|
|
1124
|
+
:func:`_check_fragment`'s fragment, or has a free object variable
|
|
1125
|
+
(genuinely free in ``sentence``, or bound outside the block —
|
|
1126
|
+
either way, out of scope for the isolated per-block solve above);
|
|
1127
|
+
or a constant or function the block's body needs from
|
|
1128
|
+
``structure`` has no interpretation there, a pinned value is not
|
|
1129
|
+
itself a member of ``structure``'s domain, or a function is not
|
|
1130
|
+
total over it — a declared PREDICATE with no interpretation
|
|
1131
|
+
never raises (see :meth:`_AspEncoder.emit_fixed_facts`'s own
|
|
1132
|
+
``Raises``).
|
|
1133
|
+
ImportError: ``clingo`` (the optional ``asp`` extra) is not
|
|
1134
|
+
installed — never a silent fallback to the brute-force evaluator.
|
|
1135
|
+
"""
|
|
1136
|
+
import clingo
|
|
1137
|
+
from .tarski import satisfies
|
|
1138
|
+
from ..atp.sequent import _all_pred_names, _fresh_pred_name
|
|
1139
|
+
|
|
1140
|
+
block_type, chain = _so_quantifier_chain(sentence)
|
|
1141
|
+
|
|
1142
|
+
if not chain:
|
|
1143
|
+
return satisfies(sentence, structure, {})
|
|
1144
|
+
|
|
1145
|
+
entry = chain[0]
|
|
1146
|
+
body = chain[-1].formula
|
|
1147
|
+
_check_fragment([body])
|
|
1148
|
+
|
|
1149
|
+
free_vars = _free_var_names_local(body)
|
|
1150
|
+
if free_vars:
|
|
1151
|
+
raise ValueError(
|
|
1152
|
+
f"asp_holds_so: the second-order block in {sentence.to_unicode_str()!r} "
|
|
1153
|
+
f"has free object variable(s) {sorted(free_vars)} in its own body "
|
|
1154
|
+
"— either genuinely free in `sentence` (which must be closed) or "
|
|
1155
|
+
"bound by an object-level quantifier OUTSIDE the block, neither "
|
|
1156
|
+
"of which this function's isolated per-block solve supports; use "
|
|
1157
|
+
"secondorder.satisfies_so instead."
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
so_names = {(n.predicate, n.arity) for n in chain}
|
|
1161
|
+
|
|
1162
|
+
sig = _Signature()
|
|
1163
|
+
sig.scan(body)
|
|
1164
|
+
sig.predicates |= so_names # a vacuously-quantified SO predicate still needs a choice
|
|
1165
|
+
|
|
1166
|
+
index_of: Dict[Any, int] = {d: i for i, d in enumerate(structure.domain)}
|
|
1167
|
+
size = len(structure.domain)
|
|
1168
|
+
|
|
1169
|
+
enc = _AspEncoder(size)
|
|
1170
|
+
enc.declare_signature(sig)
|
|
1171
|
+
enc.emit_fixed_facts(structure, index_of, so_names)
|
|
1172
|
+
|
|
1173
|
+
target: Node = Not(body) if block_type == "forall" else body
|
|
1174
|
+
head_ref, _free = enc.encode(target, {})
|
|
1175
|
+
enc.rules.append(f":- not {head_ref}.")
|
|
1176
|
+
program = "\n".join(enc.rules)
|
|
1177
|
+
|
|
1178
|
+
ctl = clingo.Control(["1"], logger=_silent)
|
|
1179
|
+
ctl.add("base", [], program)
|
|
1180
|
+
ctl.ground([("base", [])])
|
|
1181
|
+
|
|
1182
|
+
found = False
|
|
1183
|
+
with ctl.solve(yield_=True) as handle:
|
|
1184
|
+
for _model in handle:
|
|
1185
|
+
found = True
|
|
1186
|
+
break
|
|
1187
|
+
|
|
1188
|
+
block_value = (not found) if block_type == "forall" else found
|
|
1189
|
+
|
|
1190
|
+
if entry is sentence:
|
|
1191
|
+
return block_value
|
|
1192
|
+
|
|
1193
|
+
fresh_name = _fresh_pred_name("_so_block", _all_pred_names(sentence))
|
|
1194
|
+
substituted = _replace_so_block(sentence, entry, Atom(fresh_name, ()))
|
|
1195
|
+
ext_predicates = dict(structure.predicates)
|
|
1196
|
+
ext_predicates[(fresh_name, 0)] = block_value
|
|
1197
|
+
ext_structure = Structure(structure.domain, constants=structure.constants,
|
|
1198
|
+
functions=structure.functions, predicates=ext_predicates,
|
|
1199
|
+
sorts=structure.sorts)
|
|
1200
|
+
return satisfies(substituted, ext_structure, {})
|