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,53 @@
|
|
|
1
|
+
"""Exact probabilistic logic — no sampling, no floats, every result an exact Fraction.
|
|
2
|
+
|
|
3
|
+
Two independent, complementary readings of "probabilistic first-order-ish
|
|
4
|
+
logic", both decided EXACTLY (rational arithmetic throughout — a probability
|
|
5
|
+
enters as a :class:`~fractions.Fraction` and every bound/query result leaves
|
|
6
|
+
as one; nothing in this package ever samples, approximates, or routes a
|
|
7
|
+
probability through ``float``):
|
|
8
|
+
|
|
9
|
+
- :mod:`unicode_logic_kit.prob.nilsson` — **Nilsson's probabilistic entailment**
|
|
10
|
+
(Nilsson, *Probabilistic Logic*, Artificial Intelligence 28, 1986). Given
|
|
11
|
+
probability INTERVAL constraints on a set of propositional formulas
|
|
12
|
+
(unconditional ``lower ≤ P(φ) ≤ upper``, or conditional
|
|
13
|
+
``lower ≤ P(ψ|φ) ≤ upper``), :func:`~unicode_logic_kit.prob.nilsson.entailment_bounds`
|
|
14
|
+
computes the tightest interval those constraints entail for a conclusion —
|
|
15
|
+
an exact linear program over one probability variable per possible world,
|
|
16
|
+
solved with :mod:`z3`'s ``Optimize``. Classical entailment is the corner
|
|
17
|
+
case where every constraint pins probability 1 and the bounds collapse to
|
|
18
|
+
exactly ``(1, 1)``.
|
|
19
|
+
|
|
20
|
+
- :mod:`unicode_logic_kit.prob.distribution` — **Sato's distribution semantics**
|
|
21
|
+
(Sato, *A Statistical Learning Method for Logic Programs*, ICLP 1995), the
|
|
22
|
+
semantic core of ProbLog (De Raedt, Kimmig & Toivonen, IJCAI 2007). Given a
|
|
23
|
+
set of independent Bernoulli-distributed ground facts and a DEFINITE logic
|
|
24
|
+
program (rules with a single positive-atom head and a positive-atom-only
|
|
25
|
+
body) over a finite Herbrand universe, :func:`~unicode_logic_kit.prob.distribution.query`
|
|
26
|
+
sums the weight of every "total choice" of facts whose resulting (unique)
|
|
27
|
+
least Herbrand model satisfies a query — exactly, over ``2^k`` choices for
|
|
28
|
+
``k`` relevant facts (with correctness-preserving dependency-cone pruning
|
|
29
|
+
cutting ``k`` down to the facts that could actually matter).
|
|
30
|
+
|
|
31
|
+
Both modules refuse loudly, with ``ValueError``, rather than silently
|
|
32
|
+
approximate: a quantified formula in :mod:`nilsson` (no finite space of
|
|
33
|
+
possible worlds), or negation/disjunctive-heads/existentials/free variables in
|
|
34
|
+
a :mod:`distribution` rule (outside the definite-clause fragment the
|
|
35
|
+
semantics is defined over) — the kit-wide convention that an unsupported
|
|
36
|
+
fragment is an error, never a quiet best-effort guess.
|
|
37
|
+
|
|
38
|
+
Both are PROPOSITIONAL / FINITE-DOMAIN in scope: :mod:`nilsson` evaluates over
|
|
39
|
+
the ``2^n`` truth-value assignments to a formula's distinct atoms (no
|
|
40
|
+
quantifiers at all); :mod:`distribution` grounds over the finite constants
|
|
41
|
+
occurring in the program and goal (no function symbols, no infinite Herbrand
|
|
42
|
+
universe). Both document the resulting exponential blow-up honestly and
|
|
43
|
+
expose an explicit, overridable brake on it (``max_atoms`` /
|
|
44
|
+
``max_choice_facts``) rather than hiding a silent cutoff.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .nilsson import ProbConstraint, ProbBounds, entailment_bounds
|
|
48
|
+
from .distribution import ProbFact, ProbProgram, query
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"ProbConstraint", "ProbBounds", "entailment_bounds",
|
|
52
|
+
"ProbFact", "ProbProgram", "query",
|
|
53
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""In-house Reduced Ordered BDD (ROBDD) engine — exact weighted model counting.
|
|
2
|
+
|
|
3
|
+
A minimal, self-contained Boolean-function representation used by
|
|
4
|
+
:mod:`unicode_logic_kit.prob.distribution`'s ``method="compile"`` evaluation
|
|
5
|
+
route (kept local, mirroring the sibling module's own "no cross-module
|
|
6
|
+
dependency" convention — this file has no dependency on ``distribution.py``
|
|
7
|
+
or ``nilsson.py``; the dependency runs the other way).
|
|
8
|
+
|
|
9
|
+
**Representation.** A node is ``(var_index, low, high)``: the Shannon
|
|
10
|
+
cofactors of the function when its ``var_index``-th variable is false
|
|
11
|
+
(``low``) and true (``high``). Two sentinel integers, :data:`FALSE` = 0 and
|
|
12
|
+
:data:`TRUE` = 1, are the terminal nodes; every internal node gets an integer
|
|
13
|
+
id >= 2. Nodes are canonicalised through a *unique table*
|
|
14
|
+
(:class:`BDDManager._unique`) keyed by ``(var_index, low, high)``, with the
|
|
15
|
+
standard ROBDD reduction rule applied on construction: ``low == high``
|
|
16
|
+
collapses to that shared child directly (the node is redundant — the
|
|
17
|
+
variable doesn't matter) rather than being allocated. Canonicalisation means
|
|
18
|
+
structural equality is REFERENCE (id) equality: two BDD node ids are equal
|
|
19
|
+
iff the Boolean functions they represent are identical, for any input order
|
|
20
|
+
of construction — this is what lets the least-fixpoint loop in
|
|
21
|
+
``distribution.py`` detect "no change" by a plain ``!=`` on node ids.
|
|
22
|
+
|
|
23
|
+
**Fixed variable order, no dynamic reordering.** The order is whatever order
|
|
24
|
+
the caller assigns indices in (``distribution.py`` uses the pruned
|
|
25
|
+
relevant-fact list order) and never changes — this is a CORRECTNESS tool, not
|
|
26
|
+
a competitive weighted-model-counting engine; a bad order can blow up node
|
|
27
|
+
counts where a good order collapses them, and no attempt is made here to
|
|
28
|
+
find (or shuffle into) a good one. The :data:`max_bdd_nodes`-style brake
|
|
29
|
+
(``max_nodes`` below) exists precisely because worst-case Boolean functions
|
|
30
|
+
have exponentially many ROBDD nodes under ANY fixed order — weighted model
|
|
31
|
+
counting is #P-hard, so no algorithm (fixed-order ROBDDs included) escapes
|
|
32
|
+
that in general; this module refuses loudly rather than silently grinding.
|
|
33
|
+
|
|
34
|
+
**AND / OR** are the standard memoized recursive Shannon-expansion ``apply``:
|
|
35
|
+
recurse on the pair's earlier (smaller-index) top variable, combine the
|
|
36
|
+
cofactored results, and re-canonicalise via the unique table. **NOT** is
|
|
37
|
+
deliberately NOT built as ``ite(f, FALSE, TRUE)`` through that same
|
|
38
|
+
general two-argument machinery; it is its own single-argument memoized
|
|
39
|
+
recursion whose base case is exactly a 0/1-terminal swap (``NOT(FALSE) =
|
|
40
|
+
TRUE``, ``NOT(TRUE) = FALSE``) and whose recursive case swaps the same two
|
|
41
|
+
terminals arbitrarily far down the DAG, memoized per node id so any node's
|
|
42
|
+
negation is built (or looked up) once no matter how many times it is asked
|
|
43
|
+
for — cheaper, and simpler to verify correct, than routing every negation
|
|
44
|
+
through the ternary apply used for AND/OR.
|
|
45
|
+
|
|
46
|
+
Public API: :data:`FALSE`, :data:`TRUE`, :class:`BDDManager`,
|
|
47
|
+
:func:`weighted_model_count`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from fractions import Fraction
|
|
51
|
+
from typing import Dict, List, Sequence, Tuple
|
|
52
|
+
|
|
53
|
+
__all__ = ["FALSE", "TRUE", "BDDManager", "weighted_model_count"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
FALSE: int = 0
|
|
57
|
+
TRUE: int = 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BDDManager:
|
|
61
|
+
"""Owns one ROBDD's node universe over a fixed set of ``num_vars`` Boolean variables.
|
|
62
|
+
|
|
63
|
+
``variable(i)`` returns the (canonical, cached) single-variable node for
|
|
64
|
+
variable ``i``; :meth:`AND`, :meth:`OR`, :meth:`NOT` combine existing
|
|
65
|
+
nodes into new canonical ones. ``max_nodes`` bounds the TOTAL node count
|
|
66
|
+
(the two terminals plus every internal node ever allocated) — the same
|
|
67
|
+
style of loud, explicit, overridable brake
|
|
68
|
+
:func:`~unicode_logic_kit.prob.distribution.query`'s ``max_choice_facts``
|
|
69
|
+
already is for the enumeration route, here for the compiled route's own
|
|
70
|
+
failure mode (node blow-up instead of choice-count blow-up).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, num_vars: int, *, max_nodes: int = 100_000):
|
|
74
|
+
if num_vars < 0:
|
|
75
|
+
raise ValueError(f"BDDManager: num_vars must be >= 0, got {num_vars}.")
|
|
76
|
+
if max_nodes < 2:
|
|
77
|
+
raise ValueError(f"BDDManager: max_nodes must be >= 2 (room for both terminals), got {max_nodes}.")
|
|
78
|
+
self.num_vars = num_vars
|
|
79
|
+
self.max_nodes = max_nodes
|
|
80
|
+
# Internal node id `n` (n >= 2) is stored at self._nodes[n - 2]; ids 0
|
|
81
|
+
# and 1 are the FALSE/TRUE terminals and are never stored here.
|
|
82
|
+
self._nodes: List[Tuple[int, int, int]] = []
|
|
83
|
+
self._unique: Dict[Tuple[int, int, int], int] = {}
|
|
84
|
+
self._and_memo: Dict[Tuple[int, int], int] = {}
|
|
85
|
+
self._or_memo: Dict[Tuple[int, int], int] = {}
|
|
86
|
+
self._not_memo: Dict[int, int] = {}
|
|
87
|
+
self._var_nodes: List[int] = [self._make_node(i, FALSE, TRUE) for i in range(num_vars)]
|
|
88
|
+
|
|
89
|
+
# -- construction -------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def variable(self, index: int) -> int:
|
|
92
|
+
"""Return the canonical node for "variable ``index`` is true" alone."""
|
|
93
|
+
if not (0 <= index < self.num_vars):
|
|
94
|
+
raise ValueError(f"BDDManager: variable index {index} out of range [0, {self.num_vars}).")
|
|
95
|
+
return self._var_nodes[index]
|
|
96
|
+
|
|
97
|
+
def node(self, node_id: int) -> Tuple[int, int, int]:
|
|
98
|
+
"""Return ``(var_index, low, high)`` for an internal node id (never a terminal)."""
|
|
99
|
+
if self.is_terminal(node_id):
|
|
100
|
+
raise ValueError(f"BDDManager.node: {node_id} is a terminal, not an internal node id.")
|
|
101
|
+
return self._nodes[node_id - 2]
|
|
102
|
+
|
|
103
|
+
def is_terminal(self, node_id: int) -> bool:
|
|
104
|
+
return node_id == FALSE or node_id == TRUE
|
|
105
|
+
|
|
106
|
+
def size(self) -> int:
|
|
107
|
+
"""Total node count so far, terminals included (what ``max_nodes`` bounds)."""
|
|
108
|
+
return len(self._nodes) + 2
|
|
109
|
+
|
|
110
|
+
def _make_node(self, var: int, low: int, high: int) -> int:
|
|
111
|
+
"""Canonical constructor: reduction rule, then unique-table dedup, then the brake."""
|
|
112
|
+
if low == high:
|
|
113
|
+
return low
|
|
114
|
+
key = (var, low, high)
|
|
115
|
+
existing = self._unique.get(key)
|
|
116
|
+
if existing is not None:
|
|
117
|
+
return existing
|
|
118
|
+
new_id = len(self._nodes) + 2
|
|
119
|
+
if new_id >= self.max_nodes:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"BDDManager: node count would exceed max_nodes={self.max_nodes}. "
|
|
122
|
+
"Weighted model counting is #P-hard -- worst-case Boolean functions have "
|
|
123
|
+
"exponentially many ROBDD nodes under any fixed variable order, so no "
|
|
124
|
+
"algorithm escapes that blow-up in general; reduce the program, tighten "
|
|
125
|
+
"the goal (which tightens dependency-cone pruning), or pass a larger "
|
|
126
|
+
"max_bdd_nodes explicitly if that blow-up is intended."
|
|
127
|
+
)
|
|
128
|
+
self._nodes.append(key)
|
|
129
|
+
self._unique[key] = new_id
|
|
130
|
+
return new_id
|
|
131
|
+
|
|
132
|
+
def _top_var(self, node: int) -> int:
|
|
133
|
+
"""The node's own variable index, or ``num_vars`` (past every real variable) for a terminal."""
|
|
134
|
+
return self.num_vars if self.is_terminal(node) else self._nodes[node - 2][0]
|
|
135
|
+
|
|
136
|
+
def _cofactor(self, node: int, var: int) -> Tuple[int, int]:
|
|
137
|
+
"""``(node|var=False, node|var=True)``: the node's own children if it tests ``var``, else unchanged."""
|
|
138
|
+
if self.is_terminal(node) or self._nodes[node - 2][0] != var:
|
|
139
|
+
return node, node
|
|
140
|
+
_, low, high = self._nodes[node - 2]
|
|
141
|
+
return low, high
|
|
142
|
+
|
|
143
|
+
# -- Boolean combinators --------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def AND(self, a: int, b: int) -> int:
|
|
146
|
+
"""Conjunction of two nodes; memoized recursive Shannon-expansion apply."""
|
|
147
|
+
if a == FALSE or b == FALSE:
|
|
148
|
+
return FALSE
|
|
149
|
+
if a == TRUE:
|
|
150
|
+
return b
|
|
151
|
+
if b == TRUE:
|
|
152
|
+
return a
|
|
153
|
+
if a == b:
|
|
154
|
+
return a
|
|
155
|
+
key = (a, b) if a <= b else (b, a)
|
|
156
|
+
cached = self._and_memo.get(key)
|
|
157
|
+
if cached is not None:
|
|
158
|
+
return cached
|
|
159
|
+
top = min(self._top_var(a), self._top_var(b))
|
|
160
|
+
a_lo, a_hi = self._cofactor(a, top)
|
|
161
|
+
b_lo, b_hi = self._cofactor(b, top)
|
|
162
|
+
result = self._make_node(top, self.AND(a_lo, b_lo), self.AND(a_hi, b_hi))
|
|
163
|
+
self._and_memo[key] = result
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def OR(self, a: int, b: int) -> int:
|
|
167
|
+
"""Disjunction of two nodes; memoized recursive Shannon-expansion apply."""
|
|
168
|
+
if a == TRUE or b == TRUE:
|
|
169
|
+
return TRUE
|
|
170
|
+
if a == FALSE:
|
|
171
|
+
return b
|
|
172
|
+
if b == FALSE:
|
|
173
|
+
return a
|
|
174
|
+
if a == b:
|
|
175
|
+
return a
|
|
176
|
+
key = (a, b) if a <= b else (b, a)
|
|
177
|
+
cached = self._or_memo.get(key)
|
|
178
|
+
if cached is not None:
|
|
179
|
+
return cached
|
|
180
|
+
top = min(self._top_var(a), self._top_var(b))
|
|
181
|
+
a_lo, a_hi = self._cofactor(a, top)
|
|
182
|
+
b_lo, b_hi = self._cofactor(b, top)
|
|
183
|
+
result = self._make_node(top, self.OR(a_lo, b_lo), self.OR(a_hi, b_hi))
|
|
184
|
+
self._or_memo[key] = result
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def NOT(self, a: int) -> int:
|
|
188
|
+
"""Negation, via a dedicated memoized unary 0/1-terminal-swap recursion (see module docstring)."""
|
|
189
|
+
if a == FALSE:
|
|
190
|
+
return TRUE
|
|
191
|
+
if a == TRUE:
|
|
192
|
+
return FALSE
|
|
193
|
+
cached = self._not_memo.get(a)
|
|
194
|
+
if cached is not None:
|
|
195
|
+
return cached
|
|
196
|
+
var, low, high = self._nodes[a - 2]
|
|
197
|
+
result = self._make_node(var, self.NOT(low), self.NOT(high))
|
|
198
|
+
self._not_memo[a] = result
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def weighted_model_count(manager: BDDManager, root: int, weights: Sequence[Fraction]) -> Fraction:
|
|
203
|
+
"""Exact weighted model count of ``root``, ``weights[i]`` the weight of "variable ``i`` true".
|
|
204
|
+
|
|
205
|
+
``WMC(node) = p_v * WMC(high) + (1 - p_v) * WMC(low)``, terminals
|
|
206
|
+
``TRUE -> 1``, ``FALSE -> 0``, memoized bottom-up over the node DAG so
|
|
207
|
+
every SHARED node is priced exactly once. That sharing is the entire
|
|
208
|
+
performance claim of the compiled route: a chain/tree/diamond-shaped
|
|
209
|
+
program collapses to ``O(#nodes)`` here instead of the ``O(2^k)`` exact
|
|
210
|
+
weights the enumeration route sums for the same answer. Arithmetic is
|
|
211
|
+
exact ``Fraction`` throughout, never a float.
|
|
212
|
+
"""
|
|
213
|
+
memo: Dict[int, Fraction] = {FALSE: Fraction(0), TRUE: Fraction(1)}
|
|
214
|
+
|
|
215
|
+
def rec(node: int) -> Fraction:
|
|
216
|
+
cached = memo.get(node)
|
|
217
|
+
if cached is not None:
|
|
218
|
+
return cached
|
|
219
|
+
var, low, high = manager.node(node)
|
|
220
|
+
p = weights[var]
|
|
221
|
+
value = p * rec(high) + (Fraction(1) - p) * rec(low)
|
|
222
|
+
memo[node] = value
|
|
223
|
+
return value
|
|
224
|
+
|
|
225
|
+
return rec(root)
|