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,750 @@
|
|
|
1
|
+
"""Logical generality analysis — an early-warning signal for over-general
|
|
2
|
+
class definitions, computed WITHOUT any molecule database.
|
|
3
|
+
|
|
4
|
+
Motivation: an LLM translates chemical class definitions to FOL, and
|
|
5
|
+
classification is then model-checking a definition against real molecule
|
|
6
|
+
structures. A learned class definition that is too general matches far too
|
|
7
|
+
much, and the precision cost is invisible until it is measured against a
|
|
8
|
+
corpus. Two definitions of exactly the shape such a translation produces
|
|
9
|
+
show why, and both are concrete enough to serve as this module's
|
|
10
|
+
calibration cases:
|
|
11
|
+
|
|
12
|
+
molecule <=> net_charge_neutral
|
|
13
|
+
organicMolecularEntity <=> ?[A1]: (molecule & c(A1))
|
|
14
|
+
|
|
15
|
+
The right-hand side of the first is satisfied by ANY neutrally-charged
|
|
16
|
+
structure, however tiny; the second by any neutral structure with a single
|
|
17
|
+
carbon. A real acyl-CoA has on the order of 50 atoms — if its class
|
|
18
|
+
definition is already satisfied by a 3-atom structure, the definition is
|
|
19
|
+
under-determined, and this can be SEEN before a single real molecule is
|
|
20
|
+
checked. That is the whole point of this module: it never touches a molecule
|
|
21
|
+
database, ChemLog, or RDKit. It only asks the kit's own finite model finder
|
|
22
|
+
(:mod:`unicode_logic_kit.semantics.modelfinder`) "what is the SMALLEST finite
|
|
23
|
+
structure that satisfies this formula", and the kit's own prover chain
|
|
24
|
+
(:mod:`unicode_logic_kit.api`) "does this subclass body actually add anything
|
|
25
|
+
over its superclass body". Both questions are purely syntactic/semantic
|
|
26
|
+
properties of the FORMULA — no corpus required.
|
|
27
|
+
|
|
28
|
+
Three tools, matching the three questions a definition author should ask:
|
|
29
|
+
|
|
30
|
+
* :func:`minimal_model_size` / :func:`generality_report` — "how small a
|
|
31
|
+
structure already satisfies this?" A thin, HONEST wrapper around
|
|
32
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model`: that finder
|
|
33
|
+
already searches domain sizes ``1, 2, 3, …`` in ascending order and returns
|
|
34
|
+
the first (hence smallest) satisfying interpretation, so "minimal model
|
|
35
|
+
size" needs no search logic of its own here — just an honest reading of
|
|
36
|
+
what the finder already does, and an honest report of what it does NOT
|
|
37
|
+
prove (see the calibration discipline below).
|
|
38
|
+
|
|
39
|
+
* :func:`is_vacuous_specialisation` / :func:`strictly_stronger` — "does the
|
|
40
|
+
subclass body actually narrow down the superclass body, or does it
|
|
41
|
+
(possibly after being dressed up differently) mean the same thing?" A
|
|
42
|
+
"specialisation" that is logically equivalent to what it specialises has
|
|
43
|
+
added nothing — exactly the kind of silent redundancy an
|
|
44
|
+
auxiliary-predicate-inheritance mechanism (each helper predicate is
|
|
45
|
+
optimised only in the context of the class that introduces it, then
|
|
46
|
+
inherited by every subclass verbatim) can produce without anyone noticing.
|
|
47
|
+
|
|
48
|
+
**Calibration discipline (read before using the "underdetermined" verdict).**
|
|
49
|
+
A small minimal model size is an INDICATION of under-determination, never a
|
|
50
|
+
PROOF: a class can legitimately be satisfiable by a small structure (a
|
|
51
|
+
definition that is genuinely about a 2-atom functional group, say). This
|
|
52
|
+
module therefore refuses to invent an absolute threshold ("smaller than 5 is
|
|
53
|
+
bad") — :func:`generality_report`'s verdict is only ever RELATIVE to an
|
|
54
|
+
``expected_min_size`` the CALLER supplies (e.g. from the real ChEBI class's
|
|
55
|
+
typical atom count); without one, the report states the fact (the size) and
|
|
56
|
+
makes no judgement at all. This mirrors the kit-wide rule that an "unknown"
|
|
57
|
+
or "not judged" outcome must never be silently reported as a negative
|
|
58
|
+
finding.
|
|
59
|
+
|
|
60
|
+
**Boundedness (the other honesty axis).** Finite-model search is inherently
|
|
61
|
+
bounded here, for two independent reasons documented in
|
|
62
|
+
:mod:`unicode_logic_kit.semantics.modelfinder` and inherited verbatim by this
|
|
63
|
+
module: (1) the search only goes up to ``max_size`` — a formula whose
|
|
64
|
+
smallest model is bigger is reported the same way as a genuinely
|
|
65
|
+
unsatisfiable one (``size=None, exhausted=True``); this module does NOT
|
|
66
|
+
attempt to disambiguate the two, because doing so would require a
|
|
67
|
+
decision procedure this kit does not have (FOL satisfiability is
|
|
68
|
+
undecidable). (2) a domain size whose interpretation space exceeds
|
|
69
|
+
``max_candidates`` is SKIPPED rather than exhaustively searched — so, in
|
|
70
|
+
principle, a skipped smaller size could also have been satisfying, and the
|
|
71
|
+
"minimal" size this module reports is only minimal among the SIZES ACTUALLY
|
|
72
|
+
SEARCHED. Both are the modelfinder's own pre-existing, tested contract; nothing
|
|
73
|
+
here works around or hides them.
|
|
74
|
+
|
|
75
|
+
**Solver tri-state discipline.** :func:`strictly_stronger` /
|
|
76
|
+
:func:`is_vacuous_specialisation` route both entailment directions through
|
|
77
|
+
:func:`unicode_logic_kit.api.prove`'s backend chain, which is itself already
|
|
78
|
+
tri-state (PROVED / REFUTED / UNKNOWN). This module keeps that third value
|
|
79
|
+
alive end to end: an entailment direction the chain could not decide within
|
|
80
|
+
its budget is reported as ``None``, and — per the Kleene-logic combination
|
|
81
|
+
documented on :class:`StrictlyStrongerResult` — propagates to an honest
|
|
82
|
+
``"undecided"`` classification rather than a guessed one, EXCEPT in the one
|
|
83
|
+
case where the algebra makes a guess unnecessary: once one conjunct of
|
|
84
|
+
"sub |= sup AND NOT sup |= sub" is definitely known to be False, the whole
|
|
85
|
+
conjunction is False regardless of the other, undecided, conjunct — that is
|
|
86
|
+
not a guess, it is deduction, and reporting it as ``None`` would itself be
|
|
87
|
+
the dishonest choice (silently downgrading a proven answer to "unknown").
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from dataclasses import dataclass
|
|
91
|
+
from typing import List, Optional, Sequence, Tuple
|
|
92
|
+
|
|
93
|
+
from ..fol.nodes import (
|
|
94
|
+
Node, Variable, Constant, Number, Function, Atom,
|
|
95
|
+
Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
96
|
+
)
|
|
97
|
+
from ..fol.signature import Signature
|
|
98
|
+
from ..semantics.modelfinder import MAX_CANDIDATES, find_model
|
|
99
|
+
from ..semantics.tarski import Structure
|
|
100
|
+
from ..atp.protocol import PROVED, REFUTED
|
|
101
|
+
from .explain import explain_countermodel
|
|
102
|
+
|
|
103
|
+
__all__ = [
|
|
104
|
+
"MinimalModelResult", "minimal_model_size",
|
|
105
|
+
"GeneralityReport", "generality_report",
|
|
106
|
+
"StrictlyStrongerResult", "strictly_stronger",
|
|
107
|
+
"VacuousSpecialisationResult", "is_vacuous_specialisation",
|
|
108
|
+
"NO_SMALL_MODEL_FOUND", "REPORTED_ONLY", "UNDERDETERMINED", "MEETS_EXPECTATION",
|
|
109
|
+
"EQUIVALENT", "STRICTLY_STRONGER", "NOT_A_SPECIALISATION", "UNDECIDED",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# generality_report verdict vocabulary — see the module docstring's
|
|
114
|
+
# "Calibration discipline" for why there is no absolute-threshold verdict.
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
NO_SMALL_MODEL_FOUND = "no_small_model_found" # no model up to max_size — NOT a claim of unsatisfiability
|
|
118
|
+
REPORTED_ONLY = "reported_only" # a size was found, but no expected_min_size to judge it against
|
|
119
|
+
UNDERDETERMINED = "underdetermined" # size < expected_min_size — an INDICATION, not a proof
|
|
120
|
+
MEETS_EXPECTATION = "meets_expectation" # size >= expected_min_size — no warning raised (not a positive guarantee either)
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# is_vacuous_specialisation classification vocabulary
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
EQUIVALENT = "equivalent" # sub_body <=> sup_body: the "specialisation" adds nothing
|
|
127
|
+
STRICTLY_STRONGER = "strictly_stronger" # sub_body |= sup_body, sup_body does NOT |= sub_body: a genuine narrowing
|
|
128
|
+
NOT_A_SPECIALISATION = "not_a_specialisation" # sub_body does NOT even entail sup_body — the premise of the question is false
|
|
129
|
+
UNDECIDED = "undecided" # the backend chain could not settle enough of the two directions
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# minimal_model_size
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class MinimalModelResult:
|
|
138
|
+
"""Outcome of :func:`minimal_model_size`.
|
|
139
|
+
|
|
140
|
+
``size`` is the domain size of the smallest satisfying structure found,
|
|
141
|
+
or ``None`` iff no satisfying structure was found up to ``max_size_tried``
|
|
142
|
+
(``exhausted`` is then ``True`` — read as "no small satisfying structure
|
|
143
|
+
found", NEVER as "unsatisfiable": see the module docstring's
|
|
144
|
+
"Boundedness" section for the two independent reasons a genuinely
|
|
145
|
+
satisfiable formula can still come back this way). ``model`` is the
|
|
146
|
+
witness structure itself (a
|
|
147
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure`) when one was found,
|
|
148
|
+
else ``None``.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
size: Optional[int]
|
|
152
|
+
model: Optional[Structure]
|
|
153
|
+
exhausted: bool
|
|
154
|
+
max_size_tried: int
|
|
155
|
+
|
|
156
|
+
def to_dict(self) -> dict:
|
|
157
|
+
return {
|
|
158
|
+
"size": self.size,
|
|
159
|
+
"model": ({"kind": "finite_structure", "repr": repr(self.model)}
|
|
160
|
+
if self.model is not None else None),
|
|
161
|
+
"exhausted": self.exhausted,
|
|
162
|
+
"max_size_tried": self.max_size_tried,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# all_different — the ChemLog convention, as a formula transformation
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
#
|
|
170
|
+
# The model finder searches under plain FOL semantics, and there is no place
|
|
171
|
+
# to hand it a semantics switch: it evaluates with
|
|
172
|
+
# unicode_logic_kit.semantics.tarski.satisfies, which has no all_different
|
|
173
|
+
# reading. So the convention is applied where it CAN be applied exactly — to
|
|
174
|
+
# the formula, before the search — by making the distinctness the convention
|
|
175
|
+
# leaves implicit explicit as ≠ atoms. The two are the same statement:
|
|
176
|
+
# "separately introduced existential variables denote distinct individuals"
|
|
177
|
+
# IS the conjunction of those inequalities.
|
|
178
|
+
#
|
|
179
|
+
# The pairs are chosen to mirror
|
|
180
|
+
# unicode_logic_kit.semantics.model_eval's OWN reading of all_different, not a
|
|
181
|
+
# wider one: only existentials in an ANCESTOR/DESCENDANT relationship in the
|
|
182
|
+
# syntax tree (∃y somewhere inside the matrix ∃x quantifies over) are made
|
|
183
|
+
# distinct — two existentials in SIBLING positions (the two sides of an ∧)
|
|
184
|
+
# may still coincide there, and so they may here. Any drift between the two
|
|
185
|
+
# readings would mean the model checker and the generality analysis disagree
|
|
186
|
+
# about what the same formula says.
|
|
187
|
+
|
|
188
|
+
_NEGATIVE_NODES = (Not, Implies, Iff, Xor)
|
|
189
|
+
_COMPARISONS = frozenset({"=", "≠", "<", ">", "≤", "≥"})
|
|
190
|
+
|
|
191
|
+
#: Both spellings of the existential quantifier's ``type``. The kit's own
|
|
192
|
+
#: parsers emit ``"∃"``, hand-built ASTs (and several importers) use
|
|
193
|
+
#: ``"exists"``, and :class:`~unicode_logic_kit.fol.nodes.Quantifier` normalises
|
|
194
|
+
#: neither — so matching only one of them here would silently read a formula
|
|
195
|
+
#: as having no existentials at all, and answer an all_different question
|
|
196
|
+
#: under plain semantics without saying so. Same pair as
|
|
197
|
+
#: ``semantics.model_eval._EXISTS``.
|
|
198
|
+
_EXISTS = ("exists", "∃")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _is_exists(node: Node) -> bool:
|
|
202
|
+
return isinstance(node, Quantifier) and node.type in _EXISTS
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _with_all_different(formula: Node, enclosing: Tuple[str, ...] = ()) -> Node:
|
|
206
|
+
"""``formula`` with the all_different convention written out as ≠ atoms.
|
|
207
|
+
|
|
208
|
+
Each ``∃v`` gains, INSIDE its own scope, one ``u ≠ v`` for every
|
|
209
|
+
existential ``u`` it is nested in. Placing the atom inside the binder is
|
|
210
|
+
the whole difficulty: conjoining ``u ≠ v`` to the formula as a whole
|
|
211
|
+
would leave both variables FREE there, and the model finder reads a free
|
|
212
|
+
variable as a parameter: an unknown element of its own, which has nothing
|
|
213
|
+
to do with the bound variables of that name. The constraint would then say
|
|
214
|
+
"two parameters differ": it would leave the existentials it was written
|
|
215
|
+
for free to coincide, and only force the domain to have two elements.
|
|
216
|
+
|
|
217
|
+
A formula with no nested existentials comes back unchanged, so the
|
|
218
|
+
convention costs nothing where it says nothing.
|
|
219
|
+
"""
|
|
220
|
+
if _is_exists(formula):
|
|
221
|
+
name = formula.variable.name
|
|
222
|
+
inner = _with_all_different(formula.formula, enclosing + (name,))
|
|
223
|
+
for outer in enclosing:
|
|
224
|
+
inner = And(inner, Atom("≠", [Variable(outer), Variable(name)]))
|
|
225
|
+
return Quantifier(formula.type, formula.variable, inner)
|
|
226
|
+
return formula.map_children(lambda child: _with_all_different(child, enclosing))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _closed_form_size(formula: Node) -> Optional[int]:
|
|
230
|
+
"""The minimal model size under all_different, computed rather than
|
|
231
|
+
searched — or ``None`` when the formula is outside the fragment where
|
|
232
|
+
that is provable.
|
|
233
|
+
|
|
234
|
+
**Fragment**: a chain of existential quantifiers over a matrix built only
|
|
235
|
+
from atoms, ∧ and ∨, with no comparison atom (``= ≠ < > ≤ ≥``) and no
|
|
236
|
+
:class:`~unicode_logic_kit.fol.nodes.Number` anywhere.
|
|
237
|
+
|
|
238
|
+
**Claim**: for such a formula with ``n`` existentially bound variables,
|
|
239
|
+
the smallest structure satisfying it under all_different has exactly
|
|
240
|
+
``max(n, 1)`` individuals.
|
|
241
|
+
|
|
242
|
+
**Proof.** (≥) Every pair of the ``n`` variables stands in an
|
|
243
|
+
ancestor/descendant relation, so the convention makes them pairwise
|
|
244
|
+
distinct and any model has at least ``n`` individuals; a domain is
|
|
245
|
+
non-empty, so at least 1. (≤) Take ``D = {d_1, …, d_n}``, assign
|
|
246
|
+
``v_i ↦ d_i``, interpret every predicate of arity ``k`` as ``D^k``, every
|
|
247
|
+
function as the constant ``d_1``, every constant as ``d_1``. Every atom
|
|
248
|
+
of the matrix is then true — every argument denotes an individual of
|
|
249
|
+
``D``, and the predicate holds of every tuple — and a matrix built from
|
|
250
|
+
true atoms by ∧ and ∨ alone is true. ∎
|
|
251
|
+
|
|
252
|
+
The fragment conditions are exactly the proof's load-bearing
|
|
253
|
+
assumptions, which is why each is checked rather than assumed: a
|
|
254
|
+
negation would break "every atom true ⇒ matrix true"; an equality atom
|
|
255
|
+
would be made true between DISTINCT individuals by the all-tuples
|
|
256
|
+
interpretation, contradicting the assignment; a ``Number`` denotes an
|
|
257
|
+
individual outside ``D``; a universal quantifier ranges over ``D`` and
|
|
258
|
+
can fail. Out of fragment, the caller falls back to search — which is
|
|
259
|
+
correct but exponential, and for the formulas this matters for (a
|
|
260
|
+
ChEBI class definition binds up to two dozen atoms) will not finish.
|
|
261
|
+
Verified against that search on the fragment in
|
|
262
|
+
``tests/test_generality.py``.
|
|
263
|
+
"""
|
|
264
|
+
count = 0
|
|
265
|
+
node = formula
|
|
266
|
+
while isinstance(node, Quantifier):
|
|
267
|
+
if not _is_exists(node):
|
|
268
|
+
return None
|
|
269
|
+
count += 1
|
|
270
|
+
node = node.formula
|
|
271
|
+
for part in node.walk():
|
|
272
|
+
if isinstance(part, (Quantifier, Number)) or isinstance(part, _NEGATIVE_NODES):
|
|
273
|
+
return None
|
|
274
|
+
if isinstance(part, Atom) and part.predicate in _COMPARISONS:
|
|
275
|
+
return None
|
|
276
|
+
if not isinstance(part, (Atom, And, Or, Variable, Constant, Function)):
|
|
277
|
+
return None
|
|
278
|
+
return max(count, 1)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _saturated_witness(formula: Node, size: int) -> Structure:
|
|
282
|
+
"""The structure the closed form's (≤) direction constructs: ``size``
|
|
283
|
+
individuals, every predicate holding of every tuple, every function and
|
|
284
|
+
constant denoting the first individual."""
|
|
285
|
+
domain = tuple(f"d{i}" for i in range(1, size + 1))
|
|
286
|
+
predicates = {}
|
|
287
|
+
functions = {}
|
|
288
|
+
constants = {}
|
|
289
|
+
for part in formula.walk():
|
|
290
|
+
if isinstance(part, Atom):
|
|
291
|
+
arity = len(part.args)
|
|
292
|
+
predicates[(part.predicate, arity)] = (
|
|
293
|
+
True if arity == 0
|
|
294
|
+
else {tuple(t) for t in _tuples(domain, arity)})
|
|
295
|
+
elif isinstance(part, Function):
|
|
296
|
+
functions[(part.name, len(part.args))] = (
|
|
297
|
+
lambda *_args, first=domain[0]: first)
|
|
298
|
+
elif isinstance(part, Constant):
|
|
299
|
+
constants[part.name] = domain[0]
|
|
300
|
+
return Structure(domain, constants=constants, functions=functions,
|
|
301
|
+
predicates=predicates)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _tuples(domain: Tuple[str, ...], arity: int):
|
|
305
|
+
from itertools import product
|
|
306
|
+
return product(domain, repeat=arity)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def minimal_model_size(
|
|
310
|
+
formula: Node, *,
|
|
311
|
+
signature: Optional[Signature] = None,
|
|
312
|
+
max_size: int = 6,
|
|
313
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
314
|
+
all_different: bool = False,
|
|
315
|
+
) -> MinimalModelResult:
|
|
316
|
+
"""Search for the SMALLEST finite structure satisfying ``formula``.
|
|
317
|
+
|
|
318
|
+
This is a thin, honesty-preserving wrapper around
|
|
319
|
+
:func:`unicode_logic_kit.semantics.modelfinder.find_model`: that finder
|
|
320
|
+
already enumerates domain sizes ``1, 2, 3, …, max_size`` in ascending
|
|
321
|
+
order and returns the structure for the FIRST size at which one is found
|
|
322
|
+
— which is, by construction, the smallest size (among those actually
|
|
323
|
+
searched, see below) at which ``formula`` is satisfiable. No separate
|
|
324
|
+
search loop is implemented here; duplicating that logic would risk it
|
|
325
|
+
silently diverging from the finder's own (tested) enumeration order.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
formula: the definitional BODY to test (e.g. the right-hand side of
|
|
329
|
+
a ``class <=> body`` definition) — not the whole biconditional,
|
|
330
|
+
which would trivially be satisfiable by choosing the class
|
|
331
|
+
predicate's extension to match whatever the body denotes. A free
|
|
332
|
+
variable is a PARAMETER, matching
|
|
333
|
+
:mod:`~unicode_logic_kit.semantics.modelfinder`'s own reading: one
|
|
334
|
+
unknown element, the same wherever the variable occurs, and a
|
|
335
|
+
model the search finds interprets it as the constant of its name.
|
|
336
|
+
So ``P(x) ∧ ¬P(y)`` has a model of size 2 (``x`` and ``y`` two
|
|
337
|
+
elements) and is not read as ``∀x ∀y (P(x) ∧ ¬P(y))``, which has
|
|
338
|
+
none. (The closed-form witness of ``all_different`` has no entry
|
|
339
|
+
for a free variable: every element satisfies the formula there.)
|
|
340
|
+
signature: if given, ``formula`` is validated against it first
|
|
341
|
+
(:meth:`~unicode_logic_kit.fol.signature.Signature.validate`) and a
|
|
342
|
+
formula using any undeclared predicate/function/constant is
|
|
343
|
+
REFUSED with ``ValueError`` before any search runs — a
|
|
344
|
+
generality analysis over a symbol that is not even part of the
|
|
345
|
+
real vocabulary (e.g. a typo'd helper-predicate name) would be
|
|
346
|
+
meaningless, and this kit does not silently search anyway when
|
|
347
|
+
the input itself looks wrong.
|
|
348
|
+
max_size: the largest domain size tried (inclusive). Bigger costs
|
|
349
|
+
more; see ``max_candidates`` for the per-size cutoff.
|
|
350
|
+
max_candidates: forwarded to :func:`~unicode_logic_kit.semantics.modelfinder.find_model`
|
|
351
|
+
— a domain size whose interpretation space would exceed this is
|
|
352
|
+
SKIPPED rather than exhaustively enumerated. This means the
|
|
353
|
+
"minimal" size reported here is minimal among the sizes that
|
|
354
|
+
were actually searched, not provably minimal overall — see the
|
|
355
|
+
module docstring's "Boundedness" section. Formulas built only
|
|
356
|
+
from unary predicates and 0-ary facts (the module docstring's
|
|
357
|
+
calibration cases, and this module's own tests) stay far under
|
|
358
|
+
the default budget for every ``max_size`` this module defaults to;
|
|
359
|
+
a formula with several binary predicates can hit the skip much
|
|
360
|
+
sooner, since a binary predicate's interpretation space grows as
|
|
361
|
+
``2**(k**2)``.
|
|
362
|
+
|
|
363
|
+
all_different: read the formula under ChemLog's convention —
|
|
364
|
+
separately introduced existential variables denote PAIRWISE
|
|
365
|
+
DISTINCT individuals (see
|
|
366
|
+
:mod:`unicode_logic_kit.semantics.model_eval`, whose
|
|
367
|
+
ancestor/descendant reading of "separately introduced" this
|
|
368
|
+
mirrors exactly). Default ``False``: plain FOL semantics.
|
|
369
|
+
|
|
370
|
+
This matters more than it looks. Under plain semantics a
|
|
371
|
+
definition built only from ∃, ∧ and ∨ — which is what an LLM
|
|
372
|
+
writes for a chemical class — is satisfied by a ONE-element
|
|
373
|
+
structure in which every predicate holds, whatever it says: the
|
|
374
|
+
measurement is constant 1 and discriminates nothing. Under the
|
|
375
|
+
convention it measures what the definition actually demands,
|
|
376
|
+
namely how many DISTINCT atoms must exist. Implemented as a
|
|
377
|
+
formula transformation (the implicit distinctness written out as
|
|
378
|
+
≠ atoms) rather than a search-time switch, because the finder
|
|
379
|
+
evaluates with :func:`~unicode_logic_kit.semantics.tarski.satisfies`,
|
|
380
|
+
which has no such reading — and answered in CLOSED FORM, without
|
|
381
|
+
any search, for the fragment where that is provable (see
|
|
382
|
+
:func:`_closed_form_size`); a two-dozen-variable class definition
|
|
383
|
+
is not reachable by enumeration at all.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
A :class:`MinimalModelResult`. Any exception the underlying finder
|
|
387
|
+
or evaluator raises for an out-of-fragment node (e.g. a modal or
|
|
388
|
+
fuzzy operator :func:`~unicode_logic_kit.semantics.tarski.satisfies`
|
|
389
|
+
has no rule for) propagates UNCHANGED — this module adds no
|
|
390
|
+
swallowing of its own, per the kit's loud-refusal convention.
|
|
391
|
+
"""
|
|
392
|
+
if signature is not None:
|
|
393
|
+
violations = signature.validate(formula)
|
|
394
|
+
if violations:
|
|
395
|
+
extra = f" (and {len(violations) - 1} more)" if len(violations) > 1 else ""
|
|
396
|
+
raise ValueError(
|
|
397
|
+
"generality.minimal_model_size: formula uses vocabulary outside "
|
|
398
|
+
f"the given signature -- {violations[0]}{extra}. Fix the formula "
|
|
399
|
+
"or the signature before searching for a model: a generality "
|
|
400
|
+
"analysis over an undeclared symbol would be meaningless."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
searched = formula
|
|
404
|
+
if all_different:
|
|
405
|
+
size = _closed_form_size(formula)
|
|
406
|
+
if size is not None:
|
|
407
|
+
return MinimalModelResult(
|
|
408
|
+
size=size, model=_saturated_witness(formula, size),
|
|
409
|
+
exhausted=False, max_size_tried=max(max_size, size))
|
|
410
|
+
searched = _with_all_different(formula)
|
|
411
|
+
|
|
412
|
+
model = find_model([searched], max_size=max_size, max_candidates=max_candidates)
|
|
413
|
+
if model is None:
|
|
414
|
+
return MinimalModelResult(size=None, model=None, exhausted=True,
|
|
415
|
+
max_size_tried=max_size)
|
|
416
|
+
return MinimalModelResult(size=len(model.domain), model=model, exhausted=False,
|
|
417
|
+
max_size_tried=max_size)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _describe_model(model: Structure) -> str:
|
|
421
|
+
"""A short, deterministic English rendering of a witness structure.
|
|
422
|
+
|
|
423
|
+
Delegates to :func:`unicode_logic_kit.eval.explain.explain_countermodel`,
|
|
424
|
+
which already renders an arbitrary
|
|
425
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure` as a few plain
|
|
426
|
+
sentences (domain, constants, every predicate's extension) — the
|
|
427
|
+
rendering logic is identical whether the structure is framed as a
|
|
428
|
+
countermodel of a failed entailment (its original purpose) or, as here,
|
|
429
|
+
as a minimal SATISFYING witness; reimplementing that formatting here
|
|
430
|
+
would just duplicate already-tested code for no behavioural difference.
|
|
431
|
+
"""
|
|
432
|
+
return explain_countermodel(model)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
# generality_report
|
|
437
|
+
# ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
@dataclass(frozen=True)
|
|
440
|
+
class GeneralityReport:
|
|
441
|
+
"""Outcome of :func:`generality_report`.
|
|
442
|
+
|
|
443
|
+
``verdict`` is one of the module's four verdict constants
|
|
444
|
+
(:data:`NO_SMALL_MODEL_FOUND`, :data:`REPORTED_ONLY`,
|
|
445
|
+
:data:`UNDERDETERMINED`, :data:`MEETS_EXPECTATION`) — see the module
|
|
446
|
+
docstring's "Calibration discipline" for why :data:`UNDERDETERMINED` is
|
|
447
|
+
only ever reached RELATIVE to a caller-supplied ``expected_min_size``,
|
|
448
|
+
never from an absolute size alone. ``narrative`` is the same judgement
|
|
449
|
+
spelled out as one or two plain-English sentences, including the
|
|
450
|
+
relevant caveat every time (never a bare verdict word with no context).
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
name: str
|
|
454
|
+
formula: Node
|
|
455
|
+
minimal_model: MinimalModelResult
|
|
456
|
+
expected_min_size: Optional[int]
|
|
457
|
+
verdict: str
|
|
458
|
+
narrative: str
|
|
459
|
+
witness_description: Optional[str]
|
|
460
|
+
|
|
461
|
+
def to_dict(self) -> dict:
|
|
462
|
+
return {
|
|
463
|
+
"name": self.name,
|
|
464
|
+
"formula": self.formula.to_dict(),
|
|
465
|
+
"minimal_model": self.minimal_model.to_dict(),
|
|
466
|
+
"expected_min_size": self.expected_min_size,
|
|
467
|
+
"verdict": self.verdict,
|
|
468
|
+
"narrative": self.narrative,
|
|
469
|
+
"witness_description": self.witness_description,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def generality_report(
|
|
474
|
+
name: str, formula: Node, *,
|
|
475
|
+
expected_min_size: Optional[int] = None,
|
|
476
|
+
signature: Optional[Signature] = None,
|
|
477
|
+
max_size: int = 6,
|
|
478
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
479
|
+
) -> GeneralityReport:
|
|
480
|
+
"""Report on how easily ``formula`` (a class's definitional body) is
|
|
481
|
+
satisfied, optionally judged against ``expected_min_size``.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
name: a label for the class this ``formula`` defines (e.g.
|
|
485
|
+
``"acylCoA"``) — carried through purely for a readable report,
|
|
486
|
+
never inspected.
|
|
487
|
+
formula: as in :func:`minimal_model_size`.
|
|
488
|
+
expected_min_size: the caller's own expectation of how large a
|
|
489
|
+
REAL instance of this class must be (e.g. the typical atom
|
|
490
|
+
count of the real ChEBI class) — the one number this module
|
|
491
|
+
will not invent for you (see the module docstring). Omitted
|
|
492
|
+
(``None``) means: report the fact, make no judgement.
|
|
493
|
+
signature / max_size / max_candidates: forwarded to
|
|
494
|
+
:func:`minimal_model_size` verbatim.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
A :class:`GeneralityReport`. Every verdict branch names its own
|
|
498
|
+
caveat in ``narrative`` — this module never emits a bare "bad" or
|
|
499
|
+
"good" without repeating why that is not a proof either way.
|
|
500
|
+
"""
|
|
501
|
+
minimal = minimal_model_size(formula, signature=signature, max_size=max_size,
|
|
502
|
+
max_candidates=max_candidates)
|
|
503
|
+
witness = _describe_model(minimal.model) if minimal.model is not None else None
|
|
504
|
+
|
|
505
|
+
if minimal.size is None:
|
|
506
|
+
verdict = NO_SMALL_MODEL_FOUND
|
|
507
|
+
narrative = (
|
|
508
|
+
f"No structure of size <= {max_size} satisfies this definition. This "
|
|
509
|
+
"is honestly UNKNOWN territory, not a finding of overgenerality (if "
|
|
510
|
+
"anything the opposite direction) and not a proof of "
|
|
511
|
+
"unsatisfiability either -- FOL satisfiability is undecidable, and "
|
|
512
|
+
"the search is bounded by max_size and max_candidates (see "
|
|
513
|
+
"minimal_model_size's docstring)."
|
|
514
|
+
)
|
|
515
|
+
elif expected_min_size is None:
|
|
516
|
+
verdict = REPORTED_ONLY
|
|
517
|
+
narrative = (
|
|
518
|
+
f"The smallest structure satisfying '{name}' found within the "
|
|
519
|
+
f"search bound has {minimal.size} individual(s). No "
|
|
520
|
+
"expected_min_size was supplied, so no generality judgement is "
|
|
521
|
+
"made -- a bare 'small is bad' threshold is deliberately not "
|
|
522
|
+
"built into this module (see its docstring)."
|
|
523
|
+
)
|
|
524
|
+
elif minimal.size < expected_min_size:
|
|
525
|
+
verdict = UNDERDETERMINED
|
|
526
|
+
narrative = (
|
|
527
|
+
f"The smallest structure satisfying '{name}' has {minimal.size} "
|
|
528
|
+
f"individual(s), below the expected minimum of {expected_min_size}. "
|
|
529
|
+
"This is an INDICATION of possible under-determination (a "
|
|
530
|
+
"structure this small can hardly represent the intended real-world "
|
|
531
|
+
"instances) -- NOT a proof: a class can legitimately be satisfiable "
|
|
532
|
+
"by a small structure, and this module has no way to distinguish "
|
|
533
|
+
"that from a genuinely too-permissive definition on its own."
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
verdict = MEETS_EXPECTATION
|
|
537
|
+
narrative = (
|
|
538
|
+
f"The smallest structure satisfying '{name}' has {minimal.size} "
|
|
539
|
+
f"individual(s), at or above the expected minimum of "
|
|
540
|
+
f"{expected_min_size}. No generality warning is raised here -- "
|
|
541
|
+
"this does not positively CONFIRM the definition is correctly "
|
|
542
|
+
"restrictive either; it only means this particular early-warning "
|
|
543
|
+
"check found nothing to flag."
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return GeneralityReport(
|
|
547
|
+
name=name, formula=formula, minimal_model=minimal,
|
|
548
|
+
expected_min_size=expected_min_size, verdict=verdict,
|
|
549
|
+
narrative=narrative, witness_description=witness,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
# strictly_stronger / is_vacuous_specialisation
|
|
555
|
+
# ---------------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
def _entails(
|
|
558
|
+
premise: Node, conclusion: Node, *,
|
|
559
|
+
timeout: int, backends: Optional[Sequence[str]],
|
|
560
|
+
) -> Tuple[Optional[bool], Optional[dict], Optional[str]]:
|
|
561
|
+
"""Tri-state answer to "does ``premise`` entail ``conclusion``?"
|
|
562
|
+
|
|
563
|
+
Routes through :func:`unicode_logic_kit.api.prove` (imported lazily —
|
|
564
|
+
``api`` itself imports from this package at module scope elsewhere in
|
|
565
|
+
the kit, so importing it at THIS module's top level would risk a
|
|
566
|
+
circular import the moment something wires this module into
|
|
567
|
+
``unicode_logic_kit.eval``'s own ``__init__``; every other entry point in
|
|
568
|
+
this file that needs ``api`` imports it the same way, immediately before
|
|
569
|
+
use). Returns ``(entails, countermodel, backend)``:
|
|
570
|
+
|
|
571
|
+
* ``(True, None, name)`` — the chain PROVED it; ``name`` is the
|
|
572
|
+
backend that closed it.
|
|
573
|
+
* ``(False, cm, name)`` — the chain REFUTED it; ``cm`` is the
|
|
574
|
+
Verdict-layer countermodel witness dict (same shape as
|
|
575
|
+
``Verdict.countermodel`` / ``CountermodelResult.model``).
|
|
576
|
+
* ``(None, None, name)`` — neither: the chain's final UNKNOWN verdict
|
|
577
|
+
(``name`` is ``"chain"`` unless a single backend was requested via
|
|
578
|
+
``backends``) — an honest "could not decide within budget", never
|
|
579
|
+
silently read as either PROVED or REFUTED.
|
|
580
|
+
"""
|
|
581
|
+
from .. import api # lazy: see the docstring above for why
|
|
582
|
+
|
|
583
|
+
kwargs = {} if backends is None else {"backends": list(backends)}
|
|
584
|
+
verdict = api.prove(conclusion, [premise], timeout=timeout, **kwargs)
|
|
585
|
+
if verdict.status == PROVED:
|
|
586
|
+
return True, None, verdict.backend
|
|
587
|
+
if verdict.status == REFUTED:
|
|
588
|
+
return False, verdict.countermodel, verdict.backend
|
|
589
|
+
return None, None, verdict.backend
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _kleene_and(a: Optional[bool], b: Optional[bool]) -> Optional[bool]:
|
|
593
|
+
"""Three-valued (Kleene strong) conjunction: ``False`` short-circuits
|
|
594
|
+
even when the OTHER operand is ``None`` (unknown) — this is the one
|
|
595
|
+
place :class:`StrictlyStrongerResult` legitimately reports a definite
|
|
596
|
+
answer despite one entailment direction being undecided; see the module
|
|
597
|
+
docstring's "Solver tri-state discipline"."""
|
|
598
|
+
if a is False or b is False:
|
|
599
|
+
return False
|
|
600
|
+
if a is True and b is True:
|
|
601
|
+
return True
|
|
602
|
+
return None
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@dataclass(frozen=True)
|
|
606
|
+
class StrictlyStrongerResult:
|
|
607
|
+
"""Tri-state answer to ``sub_body |= sup_body AND NOT sup_body |= sub_body``.
|
|
608
|
+
|
|
609
|
+
``forward`` / ``backward`` are the two entailment directions
|
|
610
|
+
(``sub_body |= sup_body`` and ``sup_body |= sub_body`` respectively),
|
|
611
|
+
each ``True`` (PROVED) / ``False`` (REFUTED, with a witness) / ``None``
|
|
612
|
+
(the backend chain could not decide within ``timeout``) — see
|
|
613
|
+
:func:`unicode_logic_kit.api.prove`'s own tri-state contract, which this
|
|
614
|
+
is a direct read-out of.
|
|
615
|
+
|
|
616
|
+
``stronger`` combines them via :func:`_kleene_and` applied to
|
|
617
|
+
``(forward, not backward)`` — see the module docstring's "Solver
|
|
618
|
+
tri-state discipline" for why this can be a definite ``False`` even when
|
|
619
|
+
``forward`` itself is ``None`` (once ``backward`` is PROVED ``True``,
|
|
620
|
+
``NOT backward`` is definitely ``False``, and ``anything AND False`` is
|
|
621
|
+
``False`` regardless of the unknown operand — a deduction, not a guess).
|
|
622
|
+
|
|
623
|
+
``countermodel`` witnesses ``backward is False`` (a structure where
|
|
624
|
+
``sup_body`` holds but ``sub_body`` does not — the genuine-narrowing
|
|
625
|
+
witness); ``forward_countermodel`` witnesses ``forward is False`` (a
|
|
626
|
+
structure where ``sub_body`` holds but ``sup_body`` does not — meaning
|
|
627
|
+
``sub_body`` was never even a specialisation of ``sup_body`` to begin
|
|
628
|
+
with). Each is populated only when its corresponding direction was
|
|
629
|
+
actually REFUTED, ``None`` otherwise — never a guessed placeholder.
|
|
630
|
+
"""
|
|
631
|
+
|
|
632
|
+
stronger: Optional[bool]
|
|
633
|
+
forward: Optional[bool]
|
|
634
|
+
backward: Optional[bool]
|
|
635
|
+
forward_backend: Optional[str]
|
|
636
|
+
backward_backend: Optional[str]
|
|
637
|
+
countermodel: Optional[dict] = None
|
|
638
|
+
forward_countermodel: Optional[dict] = None
|
|
639
|
+
|
|
640
|
+
def to_dict(self) -> dict:
|
|
641
|
+
return {
|
|
642
|
+
"stronger": self.stronger,
|
|
643
|
+
"forward": self.forward,
|
|
644
|
+
"backward": self.backward,
|
|
645
|
+
"forward_backend": self.forward_backend,
|
|
646
|
+
"backward_backend": self.backward_backend,
|
|
647
|
+
"countermodel": self.countermodel,
|
|
648
|
+
"forward_countermodel": self.forward_countermodel,
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def strictly_stronger(
|
|
653
|
+
sub_body: Node, sup_body: Node, *,
|
|
654
|
+
timeout: int = 10000,
|
|
655
|
+
backends: Optional[Sequence[str]] = None,
|
|
656
|
+
) -> StrictlyStrongerResult:
|
|
657
|
+
"""Decide whether ``sub_body`` is a genuine (non-vacuous) logical
|
|
658
|
+
specialisation of ``sup_body``: ``sub_body |= sup_body`` AND
|
|
659
|
+
``sup_body`` does NOT ``|= sub_body``, each direction decided through
|
|
660
|
+
:func:`unicode_logic_kit.api.prove`'s backend chain.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
sub_body / sup_body: the two definitional bodies to compare (e.g. a
|
|
664
|
+
subclass's and its superclass's ``<=>`` right-hand sides).
|
|
665
|
+
timeout: forwarded to ``api.prove`` for EACH of the two entailment
|
|
666
|
+
checks (so the total wall-clock budget is up to ``2 * timeout``).
|
|
667
|
+
backends: forwarded to ``api.prove`` verbatim; ``None`` (the
|
|
668
|
+
default) uses the kit's full default chain for the detected
|
|
669
|
+
logic. Restricting this to a single, deliberately weak backend
|
|
670
|
+
(e.g. ``["z3"]`` with a tiny ``timeout``) is how a caller can
|
|
671
|
+
deliberately force the honest ``None``/undecided branch — see
|
|
672
|
+
``tests/test_generality.py`` for exactly that use.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
A :class:`StrictlyStrongerResult` — see its docstring for the
|
|
676
|
+
Kleene-logic combination behind ``.stronger``.
|
|
677
|
+
"""
|
|
678
|
+
fwd, fwd_cm, fwd_backend = _entails(sub_body, sup_body, timeout=timeout,
|
|
679
|
+
backends=backends)
|
|
680
|
+
bwd, bwd_cm, bwd_backend = _entails(sup_body, sub_body, timeout=timeout,
|
|
681
|
+
backends=backends)
|
|
682
|
+
not_bwd = {True: False, False: True, None: None}[bwd]
|
|
683
|
+
stronger = _kleene_and(fwd, not_bwd)
|
|
684
|
+
return StrictlyStrongerResult(
|
|
685
|
+
stronger=stronger, forward=fwd, backward=bwd,
|
|
686
|
+
forward_backend=fwd_backend, backward_backend=bwd_backend,
|
|
687
|
+
countermodel=bwd_cm if bwd is False else None,
|
|
688
|
+
forward_countermodel=fwd_cm if fwd is False else None,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@dataclass(frozen=True)
|
|
693
|
+
class VacuousSpecialisationResult:
|
|
694
|
+
"""Outcome of :func:`is_vacuous_specialisation`.
|
|
695
|
+
|
|
696
|
+
``classification`` is one of :data:`EQUIVALENT` (the "specialisation"
|
|
697
|
+
means the same thing as what it specialises — vacuous), :data:`STRICTLY_STRONGER`
|
|
698
|
+
(a genuine, machine-checked narrowing), :data:`NOT_A_SPECIALISATION`
|
|
699
|
+
(``sub_body`` does not even entail ``sup_body`` — the premise that this
|
|
700
|
+
IS a subclass/superclass pair is itself false), or :data:`UNDECIDED`
|
|
701
|
+
(the backend chain could not settle enough of the two directions to
|
|
702
|
+
place this in one of the other three — see ``detail`` for exactly what
|
|
703
|
+
WAS decided; in particular, a caller that only cares whether ``sub_body``
|
|
704
|
+
genuinely narrows ``sup_body`` should read ``detail.stronger`` directly
|
|
705
|
+
rather than only this coarser label, since ``detail.stronger`` can be a
|
|
706
|
+
definite ``False`` in one edge case where this classification still
|
|
707
|
+
says :data:`UNDECIDED` — see :class:`StrictlyStrongerResult`).
|
|
708
|
+
"""
|
|
709
|
+
|
|
710
|
+
classification: str
|
|
711
|
+
detail: StrictlyStrongerResult
|
|
712
|
+
|
|
713
|
+
def to_dict(self) -> dict:
|
|
714
|
+
return {"classification": self.classification, "detail": self.detail.to_dict()}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def is_vacuous_specialisation(
|
|
718
|
+
sub_body: Node, sup_body: Node, *,
|
|
719
|
+
timeout: int = 10000,
|
|
720
|
+
backends: Optional[Sequence[str]] = None,
|
|
721
|
+
) -> VacuousSpecialisationResult:
|
|
722
|
+
"""Is ``sub_body`` (a subclass's definitional body) a VACUOUS
|
|
723
|
+
specialisation of ``sup_body`` (its superclass's) — i.e. logically
|
|
724
|
+
EQUIVALENT to it, so the "specialisation" adds nothing?
|
|
725
|
+
|
|
726
|
+
Built directly on :func:`strictly_stronger` (see its docstring for the
|
|
727
|
+
two entailment directions this classification is derived from):
|
|
728
|
+
|
|
729
|
+
* both directions PROVED -> :data:`EQUIVALENT`
|
|
730
|
+
* forward PROVED, backward REFUTED -> :data:`STRICTLY_STRONGER`
|
|
731
|
+
* forward REFUTED -> :data:`NOT_A_SPECIALISATION`
|
|
732
|
+
* anything else -> :data:`UNDECIDED`
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
sub_body / sup_body / timeout / backends: as in
|
|
736
|
+
:func:`strictly_stronger`.
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
A :class:`VacuousSpecialisationResult`.
|
|
740
|
+
"""
|
|
741
|
+
detail = strictly_stronger(sub_body, sup_body, timeout=timeout, backends=backends)
|
|
742
|
+
if detail.forward is True and detail.backward is True:
|
|
743
|
+
classification = EQUIVALENT
|
|
744
|
+
elif detail.forward is True and detail.backward is False:
|
|
745
|
+
classification = STRICTLY_STRONGER
|
|
746
|
+
elif detail.forward is False:
|
|
747
|
+
classification = NOT_A_SPECIALISATION
|
|
748
|
+
else:
|
|
749
|
+
classification = UNDECIDED
|
|
750
|
+
return VacuousSpecialisationResult(classification=classification, detail=detail)
|