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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,686 @@
1
+ """Sato distribution semantics — the ProbLog core, exact, no sampling.
2
+
3
+ Sato (*A Statistical Learning Method for Logic Programs*, ICLP 1995) and
4
+ ProbLog (De Raedt, Kimmig & Toivonen, IJCAI 2007) attach an independent
5
+ probability to each of a finite set of GROUND probabilistic facts and read off
6
+ the probability of a query as a sum over "total choices". Concretely: a
7
+ :class:`ProbProgram` is a set of :class:`ProbFact`\\ s (each an independent
8
+ Bernoulli-distributed ground atom), a set of definite-clause ``rules``, and a
9
+ set of deterministic ``hard_facts``. A **total choice** ``C`` is a subset of
10
+ the probabilistic facts (the ones chosen "true"; the rest are "false"); it has
11
+ weight ``Π_{f∈C} p_f · Π_{f∉C} (1−p_f)`` — the probability, under mutual
12
+ independence, that exactly this subset comes out true. Every total choice,
13
+ together with ``hard_facts`` and ``rules``, is a plain (non-probabilistic)
14
+ DEFINITE logic program, which — being definite (no negation, no disjunctive
15
+ heads) — has a UNIQUE least Herbrand model, computed here by straightforward
16
+ forward-chaining (no stratification needed: definite programs are always
17
+ stratified trivially). :func:`query`'s result is the sum, over every total
18
+ choice whose least model satisfies the goal, of that choice's weight — exactly,
19
+ in :class:`~fractions.Fraction` arithmetic throughout (never sampled, never a
20
+ float): with ``k`` probabilistic facts this sums ``2^k`` exact weights, not an
21
+ approximation of them.
22
+
23
+ **Rule language.** A rule is Kit-AST shaped as either a bare ground
24
+ :class:`~unicode_logic_kit.fol.nodes.Atom` (a fact with an implicitly-true empty
25
+ body), or a (possibly nested) ``∀``-quantification wrapping
26
+ ``Implies(Body, Head)`` where ``Head`` is a single positive atom and ``Body``
27
+ is a conjunction of positive atoms. Negation anywhere, a non-atomic or
28
+ disjunctive head, an existential quantifier, or a variable used in the clause
29
+ but not bound by an enclosing ``∀`` are all refused with ``ValueError`` — this
30
+ is exactly the DEFINITE-clause fragment distribution semantics is defined
31
+ over; anything outside it has no unique least model to speak of, so it is
32
+ rejected loudly rather than silently coerced. ``hard_facts`` accepts the same
33
+ two shapes (ground atom, or ∀-quantified definite clause) — they differ from
34
+ ``rules`` only in being unconditionally available in EVERY total choice's
35
+ program, never gated by a probabilistic fact's inclusion.
36
+
37
+ **Grounding.** Rules are grounded over the FINITE set of constants occurring
38
+ anywhere in the program (facts, rules, hard_facts) and the goal — the
39
+ "Herbrand universe" is deliberately this finite, program-derived set, not an
40
+ open-ended one, so grounding always terminates.
41
+
42
+ **Goal language and CLOSED-WORLD NEGATION.** A goal is a ground literal, or a
43
+ conjunction/disjunction/negation of such — **NEGATION IS EVALUATED AGAINST THE
44
+ LEAST HERBRAND MODEL UNDER THE CLOSED-WORLD ASSUMPTION**: ``¬p`` is true in a
45
+ given total choice's model iff ``p`` is NOT a member of that model's (unique,
46
+ forward-chained) least fixpoint. This is the standard ProbLog convention, not
47
+ a general negation-as-failure over a possibly-non-stratified program (definite
48
+ programs need none of that machinery — the least model is unique regardless of
49
+ which atoms end up outside it). ``∀``/``∃`` in the goal are expanded, before
50
+ evaluation, into a finite ``∧``/``∨`` over the same finite constant domain
51
+ used for grounding rules.
52
+
53
+ **Truth constants.** ``⊤`` (``$true``) and ``⊥`` (``$false``) are the constants
54
+ true and false, not atoms that a rule has to derive: they have that value in
55
+ every total choice (:func:`~unicode_logic_kit.fol._truth_constants.truth_value`),
56
+ so ``P(⊤) = 1``, ``P(¬⊤) = 0`` and ``P(Rain ∧ ⊤) = P(Rain)``. In a rule body
57
+ ``⊤`` is a conjunct that always holds and ``⊥`` makes the clause never fire. As
58
+ the head of a clause, as a hard fact or as a :class:`ProbFact` a constant is
59
+ refused by name: it derives nothing, or (``⊥``) is an integrity constraint that a
60
+ definite program has no least model for.
61
+
62
+ **Correctness-preserving pruning (``prune=True``, the default).** Only
63
+ probabilistic facts that lie in the goal's GROUND DEPENDENCY CONE — reachable
64
+ by walking grounded-rule body→head edges backward from a goal atom — can
65
+ possibly change whether that goal atom is derivable; every other fact
66
+ marginalises to a factor of 1 (its own probability mass sums away: ``p_f +
67
+ (1−p_f) = 1``) and is simply never enumerated, which is why omitting it from
68
+ both the choice enumeration AND the seed set of the least-model computation
69
+ never changes the result. This is the whole of the optimization: it narrows
70
+ which facts are enumerated as 2^k choices, never which RULES fire during
71
+ forward chaining. ``max_choice_facts`` (default 16, i.e. up to 65536 choices)
72
+ is a deliberate, overridable brake on the ``2^k`` enumeration over the
73
+ (pruned, if ``prune=True``) relevant fact set.
74
+
75
+ **A second evaluation route (``method="compile"``).** :func:`query` takes a
76
+ ``method`` keyword, ``"enumerate"`` (the default; the ``2^k`` total-choice sum
77
+ described above, UNCHANGED) or ``"compile"`` — a second, algorithmically
78
+ DISTINCT but semantically IDENTICAL route that never materialises a total
79
+ choice at all. It reuses the exact same grounding, pruning and least-model
80
+ machinery, but replaces the Boolean least-fixpoint with a BDD-VALUED one over
81
+ :class:`~unicode_logic_kit.prob._bdd.BDDManager`: each derived ground atom gets
82
+ a canonical Boolean FUNCTION of the relevant facts (OR, over every grounded
83
+ rule deriving it, of the AND of that rule's body atoms' current functions),
84
+ iterated to a fixpoint by the same monotone-lattice argument as the
85
+ enumeration route's while-loop — now over canonical BDD node identity rather
86
+ than Python-set membership. The goal is composed over these atom-functions
87
+ with the identical ∧/∨/¬ structure :func:`_eval_goal` uses, and the single
88
+ resulting root is weighted-model-counted bottom-up (each SHARED node priced
89
+ once), which is why a chain/tree/diamond-shaped program collapses to
90
+ ``O(#BDD nodes)`` under ``method="compile"`` instead of the ``O(2^k)`` weights
91
+ the enumeration route sums for the same answer. Both routes MUST agree, on
92
+ every input, exactly (never a float, never a tolerance) — that identity is
93
+ the specification this module holds itself to, and the test suite tests it
94
+ directly. ``method="compile"`` is bounded by its own brake, ``max_bdd_nodes``
95
+ (default 100 000, ``max_choice_facts`` does not apply to it): weighted model
96
+ counting is #P-hard, so worst-case Boolean functions still have exponential
97
+ ROBDDs under any fixed variable order — this path still refuses loudly rather
98
+ than silently degrading, preserving the kit-wide rule, and can in fact refuse
99
+ programs the enumeration route would ALSO refuse (just via a different brake,
100
+ since the two routes' costs scale with different things) as well as accept
101
+ programs (many shared sub-derivations) the enumeration route's
102
+ ``max_choice_facts`` brake alone would never let through.
103
+
104
+ Public API: :class:`ProbFact`, :class:`ProbProgram`, :func:`query`.
105
+ """
106
+
107
+ from dataclasses import dataclass
108
+ from fractions import Fraction
109
+ from itertools import product
110
+ from typing import Dict, List, Literal, Sequence, Set, Tuple
111
+
112
+ from ..fol.nodes import (
113
+ Node, Atom, Not, And, Or, Implies, Quantifier, Variable, Constant, substitute,
114
+ )
115
+ from ..fol._atom_keys import AtomKeys
116
+ from ..fol._msfl_nodes import key_text
117
+ from ..fol._truth_constants import truth_value
118
+ from ._bdd import BDDManager, weighted_model_count, FALSE as _BDD_FALSE, TRUE as _BDD_TRUE
119
+
120
+ __all__ = ["ProbFact", "ProbProgram", "query"]
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Exact-rational helper (mirrors nilsson._as_exact_prob; kept local so this
125
+ # module has no dependency on its sibling).
126
+ # ---------------------------------------------------------------------------
127
+
128
+ def _as_exact_prob(value, label: str) -> Fraction:
129
+ """Coerce ``value`` to an exact Fraction in [0, 1]; reject float (never sampled/rounded)."""
130
+ if isinstance(value, bool) or not isinstance(value, (Fraction, int)):
131
+ raise TypeError(
132
+ f"ProbFact: {label} must be an exact Fraction (or int, coerced losslessly) "
133
+ f"— got {type(value).__name__} {value!r}. Pass Fraction({label}) explicitly; "
134
+ "a float is refused because this module never approximates a probability."
135
+ )
136
+ p = Fraction(value)
137
+ if not (Fraction(0) <= p <= Fraction(1)):
138
+ raise ValueError(f"ProbFact: {label} must satisfy 0 <= {label} <= 1; got {p}.")
139
+ return p
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # ProbFact / ProbProgram
144
+ # ---------------------------------------------------------------------------
145
+
146
+ @dataclass(frozen=True)
147
+ class ProbFact:
148
+ """One independent Bernoulli-distributed GROUND atom: ``atom`` is true with probability ``prob``.
149
+
150
+ ``atom`` must be a ground :class:`~unicode_logic_kit.fol.nodes.Atom` (no
151
+ :class:`~unicode_logic_kit.fol.nodes.Variable` anywhere in its arguments) —
152
+ distribution semantics assigns a probability to one concrete ground fact,
153
+ not a schema; a parametrised probability needs a family of ``ProbFact``\\ s
154
+ (one per grounding) or a deterministic ``∀``-rule deriving the pattern from
155
+ such a family. ``prob`` is coerced to an exact ``Fraction`` (float rejected;
156
+ see :func:`_as_exact_prob`).
157
+ """
158
+
159
+ atom: Node
160
+ prob: Fraction
161
+
162
+ def __post_init__(self):
163
+ if not isinstance(self.atom, Atom):
164
+ raise ValueError(f"ProbFact: atom must be an Atom, got {type(self.atom).__name__}.")
165
+ if truth_value(self.atom) is not None:
166
+ raise ValueError(
167
+ f"ProbFact: {self.atom.to_unicode_str()!r} is a truth constant, which is "
168
+ "true (or false) in every total choice and so has no probability of its own; "
169
+ "a probabilistic fact is a ground atom of the program."
170
+ )
171
+ if self.atom.variables():
172
+ names = sorted(v.name for v in self.atom.variables())
173
+ raise ValueError(
174
+ f"ProbFact: atom {self.atom.to_unicode_str()!r} is not ground (free "
175
+ f"variable(s) {names}) — a ProbFact assigns a probability to ONE ground "
176
+ "atom, not a schema; ground it, or express the pattern with a "
177
+ "deterministic ∀-quantified rule over a family of ground ProbFacts."
178
+ )
179
+ object.__setattr__(self, "prob", _as_exact_prob(self.prob, "prob"))
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class ProbProgram:
184
+ """A distribution-semantics program: probabilistic facts, definite rules, hard facts.
185
+
186
+ ``facts`` are the independent :class:`ProbFact`\\ s whose total choices
187
+ define the distribution. ``rules`` and ``hard_facts`` both hold definite
188
+ clauses in the shape documented in the module docstring (ground atom, or
189
+ ∀-quantified ``Implies(Body, Head)``); ``hard_facts`` are simply always
190
+ available (in every total choice), while ``rules`` are as well — the two
191
+ fields are one combined rule set for forward chaining, kept separate only
192
+ so callers can organise "background theory" apart from the rules that
193
+ mediate between probabilistic facts and derived predicates. Malformed
194
+ rules/hard_facts raise ``ValueError`` eagerly, at construction time.
195
+ """
196
+
197
+ facts: Sequence[ProbFact]
198
+ rules: Sequence[Node]
199
+ hard_facts: Sequence[Node] = ()
200
+
201
+ def __post_init__(self):
202
+ object.__setattr__(self, "facts", tuple(self.facts))
203
+ object.__setattr__(self, "rules", tuple(self.rules))
204
+ object.__setattr__(self, "hard_facts", tuple(self.hard_facts))
205
+ for f in self.facts:
206
+ if not isinstance(f, ProbFact):
207
+ raise ValueError(f"ProbProgram: facts must be ProbFact, got {type(f).__name__}.")
208
+ for r in self.rules:
209
+ _decompose_rule(r, "rule")
210
+ for hf in self.hard_facts:
211
+ _decompose_rule(hf, "hard fact")
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Definite-clause validation
216
+ # ---------------------------------------------------------------------------
217
+
218
+ def _refuse_truth_constant_head(atom: Atom, kind: str) -> None:
219
+ """Refuse a clause whose head (or whose bare fact) is a truth constant, by name.
220
+
221
+ A definite clause derives an atom. ``⊤`` is true in every total choice, so
222
+ deriving it adds nothing, and ``⊥`` as a head is an integrity constraint
223
+ (``Body → ⊥`` says the body never holds), which a definite program has no
224
+ least model for: this module has no conditioning on evidence, so it refuses
225
+ both rather than read one as an atom named ``⊥``. A truth constant in a
226
+ BODY is fine: it is read as the value it is (see :func:`_ground_definite_clauses`).
227
+ """
228
+ if truth_value(atom) is not None:
229
+ raise ValueError(
230
+ f"ProbProgram: {kind} has the truth constant {atom.to_unicode_str()!r} as its "
231
+ "head (or is that constant): a definite clause derives an atom, and a truth "
232
+ "constant is true (or false) in every total choice -- deriving it adds nothing, "
233
+ "and a clause that derives falsity is an integrity constraint this module does "
234
+ "not have. Leave the clause out, or use the constant only in a body."
235
+ )
236
+
237
+
238
+ def _decompose_rule(node: Node, kind: str) -> Tuple[Tuple[Atom, ...], Atom, Tuple[str, ...]]:
239
+ """Validate ``node`` as a definite clause; return ``(body_atoms, head, bound_var_names)``.
240
+
241
+ Accepts exactly two shapes: a bare ground-or-not Atom (empty body,
242
+ ``bound_var_names`` from any variables it uses is checked to be empty —
243
+ plain atoms are never quantifier-wrapped in this grammar), or a chain of
244
+ one-or-more ``∀`` quantifiers directly wrapping ``Implies(Body, Head)``
245
+ with ``Head`` a single positive Atom and ``Body`` a conjunction of
246
+ positive atoms. Anything else — negation anywhere, a disjunctive/negated
247
+ head, an existential in the chain, or a variable used but not bound by the
248
+ chain — raises ``ValueError`` naming ``kind`` (``"rule"`` / ``"hard fact"``).
249
+ """
250
+ if isinstance(node, Atom):
251
+ _refuse_truth_constant_head(node, kind)
252
+ if node.variables():
253
+ names = sorted(v.name for v in node.variables())
254
+ raise ValueError(
255
+ f"ProbProgram: {kind} {node.to_unicode_str()!r} is a bare atom with free "
256
+ f"variable(s) {names}; a variable must be bound by an enclosing ∀ over an "
257
+ "Implies(Body, Head) clause, or the atom must be ground."
258
+ )
259
+ return (), node, ()
260
+
261
+ bound: Set[str] = set()
262
+ cur = node
263
+ while isinstance(cur, Quantifier):
264
+ if cur.type not in ("forall", "∀"):
265
+ raise ValueError(
266
+ f"ProbProgram: {kind} uses a non-universal quantifier {cur.type!r}; only "
267
+ "ground atoms or ∀-quantified definite clauses are accepted (no ∃ — "
268
+ "distribution semantics is defined over DEFINITE clauses only)."
269
+ )
270
+ bound.add(cur.variable.name)
271
+ cur = cur.formula
272
+
273
+ if not isinstance(cur, Implies):
274
+ raise ValueError(
275
+ f"ProbProgram: {kind} must be a ground Atom or a (possibly ∀-quantified) "
276
+ f"Implies(Body, Head) definite clause; found {type(cur).__name__} under the "
277
+ "quantifier chain."
278
+ )
279
+
280
+ head = cur.right
281
+ if not isinstance(head, Atom):
282
+ raise ValueError(
283
+ f"ProbProgram: {kind} head must be a single positive Atom; found "
284
+ f"{type(head).__name__} — negation or disjunction in the head is not a "
285
+ "definite clause."
286
+ )
287
+ _refuse_truth_constant_head(head, kind)
288
+
289
+ body_atoms: List[Atom] = []
290
+
291
+ def _flatten_body(n: Node) -> None:
292
+ if isinstance(n, Atom):
293
+ body_atoms.append(n)
294
+ return
295
+ if isinstance(n, And):
296
+ _flatten_body(n.left)
297
+ _flatten_body(n.right)
298
+ return
299
+ raise ValueError(
300
+ f"ProbProgram: {kind} body must be a conjunction of positive atoms; found "
301
+ f"{type(n).__name__} — negation or disjunction in the body is not a definite "
302
+ "clause."
303
+ )
304
+
305
+ _flatten_body(cur.left)
306
+
307
+ used: Set[str] = set()
308
+ for a in body_atoms + [head]:
309
+ used |= {v.name for v in a.variables()}
310
+ free = used - bound
311
+ if free:
312
+ raise ValueError(
313
+ f"ProbProgram: {kind} has free variable(s) {sorted(free)} not bound by an "
314
+ "enclosing ∀."
315
+ )
316
+
317
+ return tuple(body_atoms), head, tuple(sorted(bound))
318
+
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # Constants, grounding, goal expansion
322
+ # ---------------------------------------------------------------------------
323
+
324
+ def _constants_in(node: Node, out: Set[str]) -> None:
325
+ """Collect every Constant name occurring anywhere in ``node`` into ``out``."""
326
+ for n in node.walk():
327
+ if isinstance(n, Constant):
328
+ out.add(n.name)
329
+
330
+
331
+ def _ground_clause(body_atoms: Tuple[Atom, ...], head: Atom, bound_vars: Tuple[str, ...],
332
+ constants: Tuple[str, ...]):
333
+ """Yield every ``(ground_body, ground_head)`` instance of a clause over ``constants``.
334
+
335
+ With no bound variables the clause is already ground and yields once
336
+ unchanged. Otherwise every combination of constants for ``bound_vars`` is
337
+ substituted in (capture-avoiding, via :func:`~unicode_logic_kit.fol.nodes.substitute`);
338
+ with an empty ``constants`` domain and at least one bound variable this
339
+ yields NOTHING (an empty-domain clause has no ground instances).
340
+ """
341
+ if not bound_vars:
342
+ yield body_atoms, head
343
+ return
344
+ for combo in product(constants, repeat=len(bound_vars)):
345
+ mapping = dict(zip(bound_vars, combo))
346
+
347
+ def sub(term: Node) -> Node:
348
+ for vname, cname in mapping.items():
349
+ term = substitute(term, Variable(vname), Constant(cname))
350
+ return term
351
+
352
+ yield tuple(sub(a) for a in body_atoms), sub(head)
353
+
354
+
355
+ def _ground_definite_clauses(nodes: Sequence[Node], kind: str, constants: Tuple[str, ...]):
356
+ """Validate+ground every clause in ``nodes``; split into always-true seeds and body-gated rules.
357
+
358
+ A grounding whose body comes out empty (a bare fact, or a clause whose
359
+ body atoms were all the truth constant ``⊤``) is a SEED — always true,
360
+ independent of any total choice. Everything else is a grounded rule
361
+ ``(ground_body, ground_head)`` for forward chaining.
362
+
363
+ A truth constant in a body is read as the value it is, not as an atom that
364
+ some rule has to derive: ``⊤`` holds in every total choice, so it is dropped
365
+ from the body, and a body that holds ``⊥`` holds in none, so the grounded
366
+ clause is dropped.
367
+ """
368
+ seeds: List[Atom] = []
369
+ grounded: List[Tuple[Tuple[Atom, ...], Atom]] = []
370
+ for node in nodes:
371
+ body_atoms, head, bound_vars = _decompose_rule(node, kind)
372
+ for g_body, g_head in _ground_clause(body_atoms, head, bound_vars, constants):
373
+ if any(truth_value(a) is False for a in g_body):
374
+ continue
375
+ g_body = tuple(a for a in g_body if truth_value(a) is None)
376
+ if g_body:
377
+ grounded.append((g_body, g_head))
378
+ else:
379
+ seeds.append(g_head)
380
+ return seeds, grounded
381
+
382
+
383
+ _GOAL_LANGUAGE = (
384
+ "query: unsupported node type {cls} in a goal; expected a ground literal, a "
385
+ "∧/∨/¬ combination of such, or a ∀/∃ quantifier over the program's finite "
386
+ "constant domain (→/↔ are not part of the goal language — encode them via "
387
+ "¬/∧/∨ if needed)."
388
+ )
389
+
390
+
391
+ def _expand_goal(node: Node, constants: Tuple[str, ...]) -> Node:
392
+ """Expand every ∀/∃ in ``node`` into a finite ∧/∨ over ``constants``; validate the rest.
393
+
394
+ Recurses through Atom/Not/And/Or unchanged (validating shape); a
395
+ Quantifier is expanded via :func:`~unicode_logic_kit.fol.nodes.substitute`
396
+ into the conjunction (∀) or disjunction (∃) of its body over every
397
+ constant, each branch recursively expanded (so nested quantifiers work).
398
+ Raises ``ValueError`` on an empty constant domain under a quantifier, or
399
+ on any node type outside the goal language.
400
+ """
401
+ if isinstance(node, Atom):
402
+ return node
403
+ if isinstance(node, Not):
404
+ return Not(_expand_goal(node.formula, constants))
405
+ if isinstance(node, And):
406
+ return And(_expand_goal(node.left, constants), _expand_goal(node.right, constants))
407
+ if isinstance(node, Or):
408
+ return Or(_expand_goal(node.left, constants), _expand_goal(node.right, constants))
409
+ if isinstance(node, Quantifier):
410
+ if not constants:
411
+ raise ValueError(
412
+ "query: cannot expand a ∀/∃ in the goal over an empty constant domain "
413
+ "(the program and goal mention no constants at all)."
414
+ )
415
+ branches = [_expand_goal(substitute(node.formula, node.variable, Constant(c)), constants)
416
+ for c in constants]
417
+ if node.type in ("forall", "∀"):
418
+ result = branches[0]
419
+ for b in branches[1:]:
420
+ result = And(result, b)
421
+ return result
422
+ if node.type in ("exists", "∃"):
423
+ result = branches[0]
424
+ for b in branches[1:]:
425
+ result = Or(result, b)
426
+ return result
427
+ raise ValueError(f"query: unknown quantifier type {node.type!r} in goal.")
428
+ raise ValueError(_GOAL_LANGUAGE.format(cls=type(node).__name__))
429
+
430
+
431
+ def _goal_atom_keys(node: Node, out: Set[str]) -> None:
432
+ """Collect the keys of every Atom leaf in a (already-expanded) goal.
433
+
434
+ A key is the text of the atom with every constant written by its name (the same
435
+ text for the facts, the rules and the goal, whatever the constants are called).
436
+ A truth constant is not an atom of the program and has no key: it is true or
437
+ false in every total choice, so it depends on no probabilistic fact.
438
+ """
439
+ if isinstance(node, Atom):
440
+ if truth_value(node) is None:
441
+ out.add(key_text(node))
442
+ return
443
+ if isinstance(node, Not):
444
+ _goal_atom_keys(node.formula, out)
445
+ return
446
+ if isinstance(node, (And, Or)):
447
+ _goal_atom_keys(node.left, out)
448
+ _goal_atom_keys(node.right, out)
449
+ return
450
+ raise ValueError(_GOAL_LANGUAGE.format(cls=type(node).__name__)) # pragma: no cover — defensive
451
+
452
+
453
+ def _eval_goal(node: Node, known_true: Set[str]) -> bool:
454
+ """Evaluate an expanded ground goal against a least-model's atom-key set.
455
+
456
+ A truth constant is the value it is in every total choice; it is never looked
457
+ up in the model.
458
+ """
459
+ if isinstance(node, Atom):
460
+ constant = truth_value(node)
461
+ if constant is not None:
462
+ return constant
463
+ return key_text(node) in known_true
464
+ if isinstance(node, Not):
465
+ return not _eval_goal(node.formula, known_true)
466
+ if isinstance(node, And):
467
+ return _eval_goal(node.left, known_true) and _eval_goal(node.right, known_true)
468
+ if isinstance(node, Or):
469
+ return _eval_goal(node.left, known_true) or _eval_goal(node.right, known_true)
470
+ raise ValueError(_GOAL_LANGUAGE.format(cls=type(node).__name__)) # pragma: no cover — defensive
471
+
472
+
473
+ def _goal_bdd(node: Node, atom_bdd: Dict[str, int], manager: BDDManager) -> int:
474
+ """Compose an expanded ground goal over per-atom BDDs — the ``method="compile"`` analog of :func:`_eval_goal`.
475
+
476
+ Same ∧/∨/¬ recursive structure, same default-``False`` (here: the BDD
477
+ ``FALSE`` terminal) treatment of an atom key absent from ``atom_bdd`` —
478
+ mirroring ``key_text(node) in known_true`` being ``False`` when the
479
+ key was never derived. A truth constant is the BDD terminal of its value.
480
+ """
481
+ if isinstance(node, Atom):
482
+ constant = truth_value(node)
483
+ if constant is not None:
484
+ return _BDD_TRUE if constant else _BDD_FALSE
485
+ return atom_bdd.get(key_text(node), _BDD_FALSE)
486
+ if isinstance(node, Not):
487
+ return manager.NOT(_goal_bdd(node.formula, atom_bdd, manager))
488
+ if isinstance(node, And):
489
+ return manager.AND(_goal_bdd(node.left, atom_bdd, manager), _goal_bdd(node.right, atom_bdd, manager))
490
+ if isinstance(node, Or):
491
+ return manager.OR(_goal_bdd(node.left, atom_bdd, manager), _goal_bdd(node.right, atom_bdd, manager))
492
+ raise ValueError(_GOAL_LANGUAGE.format(cls=type(node).__name__)) # pragma: no cover — defensive, unreachable: _expand_goal already validated node's shape
493
+
494
+
495
+ # ---------------------------------------------------------------------------
496
+ # Dependency cone (pruning) and least Herbrand model (forward chaining)
497
+ # ---------------------------------------------------------------------------
498
+
499
+ def _dependency_cone(goal_keys: Set[str],
500
+ grounded_rules: List[Tuple[Tuple[Atom, ...], Atom]]) -> Set[str]:
501
+ """Backward-reachable atom keys from ``goal_keys`` over grounded body→head edges.
502
+
503
+ A conservative (safe-to-over-include, never under-include) approximation
504
+ of "could possibly affect whether some goal atom is derivable": walks
505
+ grounded-rule edges in REVERSE (head -> its body atoms) from every goal
506
+ atom, transitively. See the module docstring's pruning-correctness note.
507
+ """
508
+ predecessors: Dict[str, Set[str]] = {}
509
+ for body_atoms, head in grounded_rules:
510
+ hk = key_text(head)
511
+ predecessors.setdefault(hk, set()).update(key_text(a) for a in body_atoms)
512
+
513
+ seen = set(goal_keys)
514
+ frontier = list(goal_keys)
515
+ while frontier:
516
+ cur = frontier.pop()
517
+ for pred in predecessors.get(cur, ()):
518
+ if pred not in seen:
519
+ seen.add(pred)
520
+ frontier.append(pred)
521
+ return seen
522
+
523
+
524
+ def _least_model(seed_keys: Set[str],
525
+ grounded_rules: List[Tuple[Tuple[Atom, ...], Atom]]) -> Set[str]:
526
+ """Forward-chain ``grounded_rules`` from ``seed_keys`` to the (unique) least Herbrand model."""
527
+ known = set(seed_keys)
528
+ changed = True
529
+ while changed:
530
+ changed = False
531
+ for body_atoms, head in grounded_rules:
532
+ hk = key_text(head)
533
+ if hk in known:
534
+ continue
535
+ if all(key_text(a) in known for a in body_atoms):
536
+ known.add(hk)
537
+ changed = True
538
+ return known
539
+
540
+
541
+ def _least_model_bdd(always_true: Set[str], relevant: Sequence["ProbFact"],
542
+ grounded_rules: List[Tuple[Tuple[Atom, ...], Atom]],
543
+ manager: BDDManager) -> Dict[str, int]:
544
+ """The ``method="compile"`` analog of :func:`_least_model`: a BDD-valued least fixpoint.
545
+
546
+ ``relevant[i]``'s own truth is exactly BDD variable ``i`` (its "chosen"
547
+ status); every other atom key's function starts at ``FALSE`` and is
548
+ repeatedly OR'd with (AND over a deriving rule's body atoms' current
549
+ functions), one grounded rule at a time, until nothing changes — same
550
+ flat while-loop shape as :func:`_least_model`, same termination argument
551
+ (a monotone, bounded-above lattice: here "bounded above" by the BDD
552
+ ``TRUE`` terminal under Boolean implication, each successful update
553
+ strictly increasing an atom's function in that order), but comparing
554
+ canonical BDD node ids instead of Python-set membership.
555
+ """
556
+ known: Dict[str, int] = {key: _BDD_TRUE for key in always_true}
557
+ for i, fact in enumerate(relevant):
558
+ key = key_text(fact.atom)
559
+ known[key] = manager.OR(known.get(key, _BDD_FALSE), manager.variable(i))
560
+
561
+ changed = True
562
+ while changed:
563
+ changed = False
564
+ for body_atoms, head in grounded_rules:
565
+ hk = key_text(head)
566
+ if known.get(hk) == _BDD_TRUE:
567
+ continue # already the constant-true function; cannot improve further
568
+ body_bdd = _BDD_TRUE
569
+ for a in body_atoms:
570
+ body_bdd = manager.AND(body_bdd, known.get(key_text(a), _BDD_FALSE))
571
+ if body_bdd == _BDD_FALSE:
572
+ break
573
+ new_value = manager.OR(known.get(hk, _BDD_FALSE), body_bdd)
574
+ if new_value != known.get(hk, _BDD_FALSE):
575
+ known[hk] = new_value
576
+ changed = True
577
+ return known
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # Public query
582
+ # ---------------------------------------------------------------------------
583
+
584
+ def query(program: ProbProgram, goal: Node, *, max_choice_facts: int = 16,
585
+ prune: bool = True, method: Literal["enumerate", "compile"] = "enumerate",
586
+ max_bdd_nodes: int = 100_000) -> Fraction:
587
+ """Return the exact distribution-semantics probability of ``goal`` under ``program``.
588
+
589
+ Sums the weight of every total choice of the (by default, pruned to the
590
+ goal's dependency cone — see the module docstring) relevant probabilistic
591
+ facts whose resulting least Herbrand model satisfies ``goal``. The result
592
+ is an exact :class:`~fractions.Fraction` — Z3 is not used anywhere in this
593
+ module; the arithmetic is plain Python ``Fraction`` products and sums over
594
+ a finite, explicitly enumerated set of choices.
595
+
596
+ ``method`` selects the evaluation ALGORITHM, never the semantics — see the
597
+ module docstring's "A second evaluation route" section. ``"enumerate"``
598
+ (the default) is the ``2^k`` total-choice sum just described, unchanged.
599
+ ``"compile"`` instead builds one shared Boolean-decision-diagram function
600
+ per derivable atom (:mod:`unicode_logic_kit.prob._bdd`) and weighted-model-
601
+ counts a single composed root; it is bounded by ``max_bdd_nodes`` rather
602
+ than ``max_choice_facts`` (the latter is ignored under ``method="compile"``),
603
+ and can therefore answer some programs ``"enumerate"`` cannot (many shared
604
+ sub-derivations, few actual BDD nodes) — while, on every program either
605
+ route CAN answer, returning bit-for-bit the same ``Fraction``.
606
+
607
+ Raises:
608
+ ValueError: on a malformed ``goal`` (outside ground-literal /
609
+ ∧/∨/¬ / ∀/∃-over-constants); under ``method="enumerate"``, if the
610
+ number of relevant probabilistic facts exceeds ``max_choice_facts``
611
+ (the choice enumeration is ``2^k`` — an explicit, overridable
612
+ brake); under ``method="compile"``, if the compiled BDD would grow
613
+ past ``max_bdd_nodes`` (weighted model counting is #P-hard — an
614
+ explicit, overridable brake on a different failure mode); an
615
+ unrecognised ``method``; or if ``program`` itself is malformed
616
+ (raised eagerly by :class:`ProbProgram` / :class:`ProbFact` at
617
+ construction time, before ``query`` is ever called); or if two different
618
+ ground atoms of the program and goal print alike (the numeral ``1`` and a
619
+ constant named ``1``, which would be ONE probabilistic variable), or an atom
620
+ holds a sorted constant.
621
+ """
622
+ constant_names: Set[str] = set()
623
+ for f in program.facts:
624
+ _constants_in(f.atom, constant_names)
625
+ for r in program.rules:
626
+ _constants_in(r, constant_names)
627
+ for hf in program.hard_facts:
628
+ _constants_in(hf, constant_names)
629
+ _constants_in(goal, constant_names)
630
+ constants = tuple(sorted(constant_names))
631
+
632
+ ground_goal = _expand_goal(goal, constants)
633
+
634
+ rule_seeds, grounded_rules = _ground_definite_clauses(program.rules, "rule", constants)
635
+ hard_seeds, hard_grounded = _ground_definite_clauses(program.hard_facts, "hard fact", constants)
636
+ always_true = {key_text(a) for a in rule_seeds + hard_seeds}
637
+ all_grounded_rules = grounded_rules + hard_grounded
638
+
639
+ # An atom is named by the text it prints as: two different ground atoms that print alike
640
+ # (the numeral 1 and a constant named 1) would be one probabilistic variable.
641
+ AtomKeys("query", "refuse", ValueError).letters(
642
+ [*(f.atom for f in program.facts), *rule_seeds, *hard_seeds,
643
+ *(a for body, head in all_grounded_rules for a in (*body, head)), ground_goal])
644
+
645
+ if prune:
646
+ goal_keys: Set[str] = set()
647
+ _goal_atom_keys(ground_goal, goal_keys)
648
+ cone = _dependency_cone(goal_keys, all_grounded_rules)
649
+ relevant = [f for f in program.facts if key_text(f.atom) in cone]
650
+ else:
651
+ relevant = list(program.facts)
652
+
653
+ if method == "enumerate":
654
+ if len(relevant) > max_choice_facts:
655
+ raise ValueError(
656
+ f"query: {len(relevant)} relevant probabilistic facts exceeds "
657
+ f"max_choice_facts={max_choice_facts}. The total-choice enumeration is "
658
+ f"O(2^k) — {len(relevant)} facts means 2**{len(relevant)} = "
659
+ f"{2 ** len(relevant)} choices. Reduce the program (or tighten the goal, "
660
+ "which tightens the dependency-cone pruning), or pass a larger "
661
+ "max_choice_facts explicitly if that blow-up is intended."
662
+ )
663
+
664
+ total = Fraction(0)
665
+ for combo in product((False, True), repeat=len(relevant)):
666
+ weight = Fraction(1)
667
+ chosen_keys = set(always_true)
668
+ for fact, is_chosen in zip(relevant, combo):
669
+ weight *= fact.prob if is_chosen else (Fraction(1) - fact.prob)
670
+ if is_chosen:
671
+ chosen_keys.add(key_text(fact.atom))
672
+ if weight == 0:
673
+ continue
674
+ known_true = _least_model(chosen_keys, all_grounded_rules)
675
+ if _eval_goal(ground_goal, known_true):
676
+ total += weight
677
+ return total
678
+
679
+ if method == "compile":
680
+ manager = BDDManager(len(relevant), max_nodes=max_bdd_nodes)
681
+ atom_bdd = _least_model_bdd(always_true, relevant, all_grounded_rules, manager)
682
+ root = _goal_bdd(ground_goal, atom_bdd, manager)
683
+ weights = [fact.prob for fact in relevant]
684
+ return weighted_model_count(manager, root, weights)
685
+
686
+ raise ValueError(f"query: unknown method {method!r}; expected 'enumerate' or 'compile'.")