pylea 0.1.0__tar.gz

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 (100) hide show
  1. pylea-0.1.0/LICENSE +21 -0
  2. pylea-0.1.0/PKG-INFO +99 -0
  3. pylea-0.1.0/README.md +55 -0
  4. pylea-0.1.0/pylea/__init__.py +376 -0
  5. pylea-0.1.0/pylea/compile/__init__.py +1 -0
  6. pylea-0.1.0/pylea/compile/_order_worker.py +31 -0
  7. pylea-0.1.0/pylea/compile/cost.py +209 -0
  8. pylea-0.1.0/pylea/compile/lower.py +191 -0
  9. pylea-0.1.0/pylea/compile/network.py +139 -0
  10. pylea-0.1.0/pylea/compile/order.py +405 -0
  11. pylea-0.1.0/pylea/compile/plan.py +158 -0
  12. pylea-0.1.0/pylea/compile/planner.py +169 -0
  13. pylea-0.1.0/pylea/draw/__init__.py +172 -0
  14. pylea-0.1.0/pylea/draw/coordinates.py +192 -0
  15. pylea-0.1.0/pylea/draw/dependency.py +169 -0
  16. pylea-0.1.0/pylea/draw/glyph.py +345 -0
  17. pylea-0.1.0/pylea/draw/layout.py +526 -0
  18. pylea-0.1.0/pylea/draw/sketch.py +303 -0
  19. pylea-0.1.0/pylea/draw/svg.py +136 -0
  20. pylea-0.1.0/pylea/draw/tikz.py +124 -0
  21. pylea-0.1.0/pylea/eval/__init__.py +1 -0
  22. pylea-0.1.0/pylea/eval/backend.py +404 -0
  23. pylea-0.1.0/pylea/eval/interp.py +235 -0
  24. pylea-0.1.0/pylea/eval/kernels.py +194 -0
  25. pylea-0.1.0/pylea/examples/__init__.py +13 -0
  26. pylea-0.1.0/pylea/examples/bioassay.py +129 -0
  27. pylea-0.1.0/pylea/examples/birthday.py +35 -0
  28. pylea-0.1.0/pylea/examples/disease.py +46 -0
  29. pylea-0.1.0/pylea/examples/electoral_college.py +92 -0
  30. pylea-0.1.0/pylea/examples/german_tank.py +133 -0
  31. pylea-0.1.0/pylea/examples/holdem.py +265 -0
  32. pylea-0.1.0/pylea/examples/monty_hall.py +46 -0
  33. pylea-0.1.0/pylea/examples/percolation.py +101 -0
  34. pylea-0.1.0/pylea/examples/secret_santa.py +51 -0
  35. pylea-0.1.0/pylea/examples/simple.py +288 -0
  36. pylea-0.1.0/pylea/examples/sprinkler.py +92 -0
  37. pylea-0.1.0/pylea/ir/__init__.py +33 -0
  38. pylea-0.1.0/pylea/ir/bits.py +23 -0
  39. pylea-0.1.0/pylea/ir/body.py +207 -0
  40. pylea-0.1.0/pylea/ir/dependency.py +225 -0
  41. pylea-0.1.0/pylea/ir/inline.py +140 -0
  42. pylea-0.1.0/pylea/ir/prim.py +296 -0
  43. pylea-0.1.0/pylea/ir/program.py +329 -0
  44. pylea-0.1.0/pylea/ir/reference.py +84 -0
  45. pylea-0.1.0/pylea/sample/__init__.py +58 -0
  46. pylea-0.1.0/pylea/sample/_exact_worker.py +79 -0
  47. pylea-0.1.0/pylea/sample/certify.py +479 -0
  48. pylea-0.1.0/pylea/sample/collapse.py +145 -0
  49. pylea-0.1.0/pylea/sample/cost_model.py +201 -0
  50. pylea-0.1.0/pylea/sample/estimate.py +103 -0
  51. pylea-0.1.0/pylea/sample/packed.py +192 -0
  52. pylea-0.1.0/pylea/sample/pilot.py +242 -0
  53. pylea-0.1.0/pylea/sample/portfolio.py +421 -0
  54. pylea-0.1.0/pylea/sample/query.py +256 -0
  55. pylea-0.1.0/pylea/sample/run.py +479 -0
  56. pylea-0.1.0/pylea/sample/schedule.py +261 -0
  57. pylea-0.1.0/pylea/sample/sweep.py +622 -0
  58. pylea-0.1.0/pylea/semantics/__init__.py +1 -0
  59. pylea-0.1.0/pylea/semantics/likelihood_table.py +278 -0
  60. pylea-0.1.0/pylea/semantics/matrices.py +133 -0
  61. pylea-0.1.0/pylea/simplify/__init__.py +44 -0
  62. pylea-0.1.0/pylea/simplify/absorb.py +93 -0
  63. pylea-0.1.0/pylea/simplify/boolean.py +220 -0
  64. pylea-0.1.0/pylea/simplify/common.py +580 -0
  65. pylea-0.1.0/pylea/simplify/cse.py +96 -0
  66. pylea-0.1.0/pylea/simplify/dce.py +97 -0
  67. pylea-0.1.0/pylea/simplify/driver.py +152 -0
  68. pylea-0.1.0/pylea/simplify/fold.py +622 -0
  69. pylea-0.1.0/pylea/simplify/implication.py +238 -0
  70. pylea-0.1.0/pylea/simplify/inline.py +48 -0
  71. pylea-0.1.0/pylea/simplify/lift.py +315 -0
  72. pylea-0.1.0/pylea/simplify/mask.py +176 -0
  73. pylea-0.1.0/pylea/simplify/observe.py +173 -0
  74. pylea-0.1.0/pylea/syntax/__init__.py +1 -0
  75. pylea-0.1.0/pylea/syntax/choice.py +99 -0
  76. pylea-0.1.0/pylea/syntax/comparison.py +72 -0
  77. pylea-0.1.0/pylea/syntax/compiled.py +251 -0
  78. pylea-0.1.0/pylea/syntax/dpp.py +1366 -0
  79. pylea-0.1.0/pylea/syntax/encoding.py +108 -0
  80. pylea-0.1.0/pylea/syntax/enum.py +576 -0
  81. pylea-0.1.0/pylea/syntax/estimate.py +86 -0
  82. pylea-0.1.0/pylea/syntax/expectation.py +159 -0
  83. pylea-0.1.0/pylea/syntax/flip.py +57 -0
  84. pylea-0.1.0/pylea/syntax/integer.py +224 -0
  85. pylea-0.1.0/pylea/syntax/method.py +161 -0
  86. pylea-0.1.0/pylea/syntax/observe.py +73 -0
  87. pylea-0.1.0/pylea/syntax/pmf.py +85 -0
  88. pylea-0.1.0/pylea/syntax/prob.py +82 -0
  89. pylea-0.1.0/pylea/syntax/tuple.py +177 -0
  90. pylea-0.1.0/pylea/syntax/variable.py +244 -0
  91. pylea-0.1.0/pylea/utility/__init__.py +1 -0
  92. pylea-0.1.0/pylea/utility/bif_to_dpp.py +646 -0
  93. pylea-0.1.0/pylea/utility/dft_to_dpp.py +363 -0
  94. pylea-0.1.0/pylea.egg-info/PKG-INFO +99 -0
  95. pylea-0.1.0/pylea.egg-info/SOURCES.txt +98 -0
  96. pylea-0.1.0/pylea.egg-info/dependency_links.txt +1 -0
  97. pylea-0.1.0/pylea.egg-info/requires.txt +25 -0
  98. pylea-0.1.0/pylea.egg-info/top_level.txt +1 -0
  99. pylea-0.1.0/pyproject.toml +60 -0
  100. pylea-0.1.0/setup.cfg +4 -0
pylea-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benedikt Peterseim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pylea-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: pylea
3
+ Version: 0.1.0
4
+ Summary: Discrete probabilistic programming in Python
5
+ Author: Benedikt Peterseim
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://benediktpeterseim.github.io/Pylea/
8
+ Project-URL: Source, https://github.com/BenediktPeterseim/Pylea
9
+ Project-URL: Issues, https://github.com/BenediktPeterseim/Pylea/issues
10
+ Keywords: probabilistic programming,exact inference,tensor networks,Bayesian networks,fault trees
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy
23
+ Requires-Dist: scipy
24
+ Requires-Dist: cotengra
25
+ Requires-Dist: cotengrust
26
+ Provides-Extra: gpu
27
+ Requires-Dist: torch; extra == "gpu"
28
+ Provides-Extra: search
29
+ Requires-Dist: kahypar; extra == "search"
30
+ Requires-Dist: optuna; extra == "search"
31
+ Requires-Dist: cmaes; extra == "search"
32
+ Provides-Extra: docs
33
+ Requires-Dist: pdoc; extra == "docs"
34
+ Requires-Dist: jupytext; extra == "docs"
35
+ Requires-Dist: nbclient; extra == "docs"
36
+ Requires-Dist: nbconvert; extra == "docs"
37
+ Requires-Dist: ipykernel; extra == "docs"
38
+ Requires-Dist: matplotlib; extra == "docs"
39
+ Provides-Extra: test
40
+ Requires-Dist: pytest; extra == "test"
41
+ Requires-Dist: matplotlib; extra == "test"
42
+ Requires-Dist: ipython; extra == "test"
43
+ Dynamic: license-file
44
+
45
+ # Pylea
46
+
47
+ Discrete probabilistic programming in Python.
48
+
49
+ Pylea computes probabilities, expected values and probability mass functions in
50
+ probabilistic models in which every variable takes only finitely many values. A model is
51
+ written as a discrete probabilistic program (DPP): a Python function decorated with
52
+ `@DPP`, in which coin flips are drawn with `flip` and events are conditioned on with
53
+ `observe`.
54
+
55
+ ## Installation
56
+
57
+ ```
58
+ pip install pylea
59
+ ```
60
+
61
+ Pylea requires Python 3.12 or later. `pip install "pylea[gpu]"` also installs PyTorch,
62
+ for evaluation on a GPU, and `pip install "pylea[search]"` installs kahypar, optuna and
63
+ cmaes, which cotengra uses where installed when it searches for a contraction order for the
64
+ costliest programs (macOS and Linux only).
65
+
66
+ ## Example
67
+
68
+ A disease occurs in a fraction 10⁻⁴ of the population. A test for it is positive for 99%
69
+ of the patients who have the disease and for 2% of those who do not. The probability that
70
+ a patient has the disease, given one positive and one negative test result:
71
+
72
+ ```python
73
+ from pylea import DPP, Bool, flip, observe, prob
74
+
75
+
76
+ @DPP
77
+ def test(has_it: Bool) -> Bool:
78
+ positive_given_disease = flip(0.99) # true positive rate
79
+ positive_given_no_disease = flip(0.02) # false positive rate
80
+ return (has_it & positive_given_disease) | (~has_it & positive_given_no_disease)
81
+
82
+
83
+ @DPP
84
+ def has_disease() -> Bool:
85
+ x = flip(1e-4) # prevalence
86
+ observe(test(x) & ~test(x)) # one positive, one negative result
87
+ return x
88
+
89
+
90
+ print(prob(has_disease)) # 5.0512703945042225e-05
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ <https://benediktpeterseim.github.io/Pylea/>
96
+
97
+ ## Licence
98
+
99
+ MIT; see [LICENSE](https://github.com/BenediktPeterseim/Pylea/blob/main/LICENSE).
pylea-0.1.0/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Pylea
2
+
3
+ Discrete probabilistic programming in Python.
4
+
5
+ Pylea computes probabilities, expected values and probability mass functions in
6
+ probabilistic models in which every variable takes only finitely many values. A model is
7
+ written as a discrete probabilistic program (DPP): a Python function decorated with
8
+ `@DPP`, in which coin flips are drawn with `flip` and events are conditioned on with
9
+ `observe`.
10
+
11
+ ## Installation
12
+
13
+ ```
14
+ pip install pylea
15
+ ```
16
+
17
+ Pylea requires Python 3.12 or later. `pip install "pylea[gpu]"` also installs PyTorch,
18
+ for evaluation on a GPU, and `pip install "pylea[search]"` installs kahypar, optuna and
19
+ cmaes, which cotengra uses where installed when it searches for a contraction order for the
20
+ costliest programs (macOS and Linux only).
21
+
22
+ ## Example
23
+
24
+ A disease occurs in a fraction 10⁻⁴ of the population. A test for it is positive for 99%
25
+ of the patients who have the disease and for 2% of those who do not. The probability that
26
+ a patient has the disease, given one positive and one negative test result:
27
+
28
+ ```python
29
+ from pylea import DPP, Bool, flip, observe, prob
30
+
31
+
32
+ @DPP
33
+ def test(has_it: Bool) -> Bool:
34
+ positive_given_disease = flip(0.99) # true positive rate
35
+ positive_given_no_disease = flip(0.02) # false positive rate
36
+ return (has_it & positive_given_disease) | (~has_it & positive_given_no_disease)
37
+
38
+
39
+ @DPP
40
+ def has_disease() -> Bool:
41
+ x = flip(1e-4) # prevalence
42
+ observe(test(x) & ~test(x)) # one positive, one negative result
43
+ return x
44
+
45
+
46
+ print(prob(has_disease)) # 5.0512703945042225e-05
47
+ ```
48
+
49
+ ## Documentation
50
+
51
+ <https://benediktpeterseim.github.io/Pylea/>
52
+
53
+ ## Licence
54
+
55
+ MIT; see [LICENSE](https://github.com/BenediktPeterseim/Pylea/blob/main/LICENSE).
@@ -0,0 +1,376 @@
1
+ """# *Pylea:* discrete probabilistic programming in Python.
2
+
3
+ Pylea is a library for computational discrete probability theory.
4
+ Given an arbitrary probabilistic model in which every variable can
5
+ take at most finitely many values, Pylea can compute the probability
6
+ of events, expected values, probability mass functions and any other quantity
7
+ of interest.
8
+ Such a probabilistic model is written as a *discrete probabilistic program (DPP)*:
9
+ a Python function decorated with `@DPP`, using a restricted syntax in which
10
+ coin flips are drawn with `flip` and events are conditioned on using `observe`.
11
+
12
+ *Example.* A disease occurs in a fraction ``10**-4`` of the population. A test for it is positive for
13
+ 99% of the patients who have the disease and for 2% of those who do not. The DPP
14
+ `has_disease` gives the probability that a patient has the disease, given one positive and
15
+ one negative test result:
16
+
17
+ >>> from pylea import DPP, Bool, flip, observe, prob
18
+ >>> @DPP
19
+ ... def test(has_it: Bool) -> Bool:
20
+ ... positive_given_disease = flip(0.99) # true positive rate
21
+ ... positive_given_no_disease = flip(0.02) # false positive rate
22
+ ... true_positive = has_it & positive_given_disease
23
+ ... false_positive = ~has_it & positive_given_no_disease
24
+ ... return true_positive | false_positive
25
+ >>> @DPP
26
+ ... def has_disease() -> Bool:
27
+ ... x = flip(1e-4)
28
+ ... t1 = test(x)
29
+ ... t2 = test(x)
30
+ ... y = t1 & ~t2
31
+ ... observe(y)
32
+ ... return x
33
+ >>> round(prob(has_disease), 8)
34
+ 5.051e-05
35
+
36
+ A DPP calls other DPPs as ordinary functions, as `has_disease` calls `test` twice. The same
37
+ DPP is `pylea.examples.disease`. Pylea's DSL includes many more features, most of them
38
+ demonstrated in the examples and guide below.
39
+
40
+
41
+ ## Examples
42
+
43
+ Each example links to a notebook, shown with its output and available for download, and
44
+ illustrates a class of models for which Pylea can be used.
45
+
46
+ - **Fault trees**: [This notebook](examples/fault_trees.html) shows how to build
47
+ *fault trees* in Pylea. These are used in reliability engineering to
48
+ estimate the failure probability of safety-critical systems.
49
+ - **Bayesian networks**: [Bayesian networks](examples/bayesian_networks.html) can be understood
50
+ as special examples of DPPs. Pylea enables flexible queries on Bayesian networks, easy
51
+ integration with other classes of discrete probabilistic models, and fast inference.
52
+ - **Discrete Bayesian statistics**: [This example](examples/bayesian_statistics.html)
53
+ goes through examples of using Pylea for Bayesian statistics, including classic textbook problems
54
+ based on historical real-world applications.
55
+ - **Games of chance**: [This notebook](examples/games_of_chance.html) illustrates that Pylea
56
+ can be used to compute winning probabilities in games of chance using the classic
57
+ *Monty Hall problem* and *Texas hold'em poker* as examples.
58
+
59
+ `pylea.utility` converts Bayesian networks in the `.bif` format and fault trees in the
60
+ `.dft` format into DPPs. More example DPPs are in `pylea.examples`.
61
+
62
+
63
+ ## Writing a DPP
64
+
65
+ Python runs the body of a `@DPP` function once, when the function is defined, and records
66
+ each step. Probabilities are computed only when the DPP is queried. Three rules follow.
67
+
68
+ **Every variable has a type.** The type is a subclass of `Variable`: `Bool`, or a type
69
+ declared by subclassing `Enum`, `UInt` or `Tuple`, as shown below. It fixes the finite set
70
+ of values the variable can take. Every parameter of a DPP is annotated with its type, and
71
+ the DPP returns one variable or a tuple of variables.
72
+
73
+ **A random variable has no value while the body runs.** It therefore cannot be used in
74
+ Python's `if`, `while`, `and`, `or` or `not`; a `Bool` used there raises `TypeError`.
75
+ `&`, `|`, `~` and `match`, below, take their place. Loops and `if` statements over
76
+ ordinary Python values work as usual: a `for` loop over ``range(3)`` records its steps
77
+ three times, and a Python function called in the body records the steps it makes.
78
+
79
+ **A DPP takes only variables.** `flip`, `observe` and the other constructions below work
80
+ only inside the body of a `@DPP` function, and a parameter cannot be a number. A number,
81
+ such as a probability, is given to a Python function that builds the DPP:
82
+
83
+ >>> def coin(p: float) -> DPP:
84
+ ... @DPP
85
+ ... def heads() -> Bool:
86
+ ... return flip(p)
87
+ ... return heads
88
+ >>> prob(coin(0.3))
89
+ 0.3
90
+
91
+ ### Coin flips and Boolean operations
92
+
93
+ `flip(p)` is a coin flip: a `Bool` that is true with probability `p`. Each call is a new
94
+ coin flip, independent of every other, and a variable used twice has the same value both
95
+ times. `&`, `|`, `~` and `^` combine `Bool`s, `Bool.any` and `Bool.all` combine any number
96
+ of them, and `at_least` is true where at least a given number of its arguments are. A
97
+ cooling system fails if both pumps fail or at least two of its three sensors fail, and a
98
+ pump fails if its motor or the shared power supply fails:
99
+
100
+ >>> from pylea import at_least
101
+ >>> @DPP
102
+ ... def cooling_fails() -> Bool:
103
+ ... power = flip(0.001)
104
+ ... pump_a = power | flip(0.01)
105
+ ... pump_b = power | flip(0.01)
106
+ ... sensors = [flip(0.02) for _ in range(3)]
107
+ ... return (pump_a & pump_b) | at_least(2, *sensors)
108
+ >>> round(prob(cooling_fails), 6)
109
+ 0.002283
110
+
111
+ ### Calling a DPP
112
+
113
+ A DPP calls another as a function, as `has_disease` calls `test` above. Each call makes its
114
+ own coin flips, so the two calls of `test` there are two independent tests. A DPP with
115
+ several outputs returns a tuple:
116
+
117
+ >>> @DPP
118
+ ... def two_coins() -> tuple[Bool, Bool]:
119
+ ... return flip(0.5), flip(0.5)
120
+ >>> @DPP
121
+ ... def same_side() -> Bool:
122
+ ... a, b = two_coins()
123
+ ... return ~(a ^ b)
124
+ >>> prob(same_side)
125
+ 0.5
126
+
127
+ ### Conditioning
128
+
129
+ `observe(e)` conditions on the `Bool` `e` being true. A query then gives probabilities
130
+ conditional on `e`, and so does a query of any DPP calling this one.
131
+
132
+ >>> @DPP
133
+ ... def first_given_either() -> Bool:
134
+ ... a, b = flip(0.5), flip(0.5)
135
+ ... observe(a | b)
136
+ ... return a
137
+ >>> round(prob(first_given_either), 6)
138
+ 0.666667
139
+
140
+ ``observe(e, if_true=0.9, if_false=0.3)`` conditions on the report of an unreliable witness
141
+ that `e` is true: the report has probability 0.9 where `e` is true and 0.3 where it is
142
+ false.
143
+
144
+ ### Enumerations: `Enum`
145
+
146
+ An `Enum` type lists its values in `elements`:
147
+
148
+ >>> from pylea import Enum, probability_mass_function
149
+ >>> class Weather(Enum):
150
+ ... elements = ["dry", "rain", "snow"]
151
+ >>> @DPP
152
+ ... def weather() -> Weather:
153
+ ... return Weather.random({"dry": 0.7, "rain": 0.25, "snow": 0.05})
154
+
155
+ `Enum.random` draws a value with the given probabilities, `Enum.uniform` draws every value
156
+ with the same probability, and `Enum.const` is a fixed value, as in
157
+ ``Weather.const("dry")``. A variable compares with `==` and `!=` against another of its
158
+ type or against one of the `elements`. `<`, `<=`, `>` and `>=` follow the order of
159
+ `elements`, not the order of the values themselves.
160
+
161
+ `Enum.match` chooses a variable or a value for each element, and needs a key for every
162
+ element. Where every choice is a value rather than a variable, `data_type` names the type
163
+ of the result. `Bool.match` is the same, with the keys `True` and `False`.
164
+
165
+ >>> @DPP
166
+ ... def slippery() -> Bool:
167
+ ... w = weather()
168
+ ... return w.match({"dry": flip(0.01), "rain": flip(0.2), "snow": True})
169
+ >>> round(prob(slippery), 6)
170
+ 0.107
171
+
172
+ `Enum.cpt` builds a conditional probability table: for each tuple of values of the
173
+ parents, the probabilities of the elements, in the order of `elements`. The table is
174
+ called on the parent variables, as a DPP is:
175
+
176
+ >>> class Grass(Enum):
177
+ ... elements = ["dry", "damp", "wet"]
178
+ >>> grass_given_weather = Grass.cpt([Weather], {
179
+ ... ("dry",): [0.8, 0.2, 0.0],
180
+ ... ("rain",): [0.0, 0.3, 0.7],
181
+ ... ("snow",): [0.2, 0.6, 0.2]})
182
+ >>> @DPP
183
+ ... def wet_grass() -> Bool:
184
+ ... return grass_given_weather(weather()) == "wet"
185
+ >>> round(prob(wet_grass), 6)
186
+ 0.185
187
+
188
+ `Enum.map` applies an ordinary Python function to the values of one or several `Enum`
189
+ variables. `data_type` names the type of the result, which is the variable's own type
190
+ where it is left out. It stores the result for every combination of values, and raises
191
+ `ValueError` where there are too many combinations.
192
+
193
+ >>> class Die(Enum):
194
+ ... elements = [1, 2, 3, 4, 5, 6]
195
+ >>> @DPP
196
+ ... def big_product() -> Bool:
197
+ ... a, b = Die.uniform(), Die.uniform()
198
+ ... return a.map(lambda x, y: x * y > 20, b, data_type=Bool)
199
+ >>> round(prob(big_product), 6)
200
+ 0.166667
201
+
202
+ ### Integers: `UInt`
203
+
204
+ A `UInt` type holds the integers from 0 to ``2**width - 1``. `+` adds two variables of
205
+ the type, or a variable and a number. The sum is taken modulo ``2**width``, so the type
206
+ must be wide enough for the largest sum. `count` gives the number of `Bool`s that are
207
+ true; Python's `sum` does not work on variables.
208
+
209
+ >>> from pylea import UInt, count
210
+ >>> class Heads(UInt):
211
+ ... width = 4 # 0 to 15
212
+ >>> @DPP
213
+ ... def many_heads() -> Bool:
214
+ ... morning = count((flip(0.5) for _ in range(5)), data_type=Heads)
215
+ ... evening = count((flip(0.5) for _ in range(5)), data_type=Heads)
216
+ ... return morning + evening >= 7
217
+ >>> prob(many_heads)
218
+ 0.171875
219
+
220
+ ### Tuples and drawing without replacement
221
+
222
+ A `Tuple` type combines several types into one: `Tuple.of` builds a value from one
223
+ variable per type, and `Tuple.components` takes it apart again. `pick` draws one of a
224
+ list of variables, each with the same probability, and returns it with the rest of the
225
+ list; given a number `n` as well, it draws `n` in turn. The two together deal two cards
226
+ from a deck of four:
227
+
228
+ >>> from pylea import Tuple, pick
229
+ >>> class Rank(Enum):
230
+ ... elements = ["K", "A"]
231
+ >>> class Colour(Enum):
232
+ ... elements = ["red", "black"]
233
+ >>> class Card(Tuple):
234
+ ... types = (Rank, Colour)
235
+ >>> @DPP
236
+ ... def two_aces() -> Bool:
237
+ ... deck = [Card.of(Rank.const(r), Colour.const(c)) for r in Rank for c in Colour]
238
+ ... (first, second), rest = pick(deck, 2)
239
+ ... first_rank, _ = first.components()
240
+ ... second_rank, _ = second.components()
241
+ ... return (first_rank == "A") & (second_rank == "A")
242
+ >>> round(prob(two_aces), 6)
243
+ 0.166667
244
+
245
+ ## Querying a DPP
246
+
247
+ A query takes a DPP without parameters that returns what is asked about:
248
+
249
+ * `prob` gives the probability that a DPP returning one `Bool` returns true.
250
+ * `probability_mass_function` gives the probability of each value of a DPP returning one
251
+ `Enum`, as a dict.
252
+ * `expectation` gives the expected value of a DPP returning one variable whose values are
253
+ numbers, such as a `UInt` or an `Enum` of numbers.
254
+
255
+ Each is conditional on the DPP's observations. To ask about one output of a DPP with
256
+ several, or about a combination of them, write a DPP that calls it and returns what is
257
+ asked about:
258
+
259
+ >>> from pylea import expectation
260
+ >>> @DPP
261
+ ... def weather_given_wet_grass() -> Weather:
262
+ ... w = weather()
263
+ ... observe(grass_given_weather(w) == "wet")
264
+ ... return w
265
+ >>> pmf = probability_mass_function(weather_given_wet_grass)
266
+ >>> {w: round(p, 4) for w, p in pmf.items()}
267
+ {'dry': 0.0, 'rain': 0.9459, 'snow': 0.0541}
268
+ >>> @DPP
269
+ ... def heads_in_ten() -> Heads:
270
+ ... return count((flip(0.5) for _ in range(10)), data_type=Heads)
271
+ >>> round(expectation(heads_in_ten), 6)
272
+ 5.0
273
+
274
+ **An answer can be an estimate.** The `method` argument selects how a query is computed:
275
+ ``"exact"`` computes the value exactly, ``"smc"`` estimates it by sampling, and ``"auto"``,
276
+ the default, runs whichever is predicted to be faster. A sampled answer is computed to
277
+ relative error `rel_err`, 0.01 by default, and `seed` makes it reproducible. Every answer
278
+ is an `Estimate`, a float with the attributes `low` and `high`, which bound the true value
279
+ with probability at least `confidence`, 0.95 by default; for an exact answer both equal
280
+ the value. Arithmetic on an `Estimate` gives a plain float, without the bounds.
281
+
282
+ >>> p = prob(first_given_either, method="smc", rel_err=0.05, seed=0)
283
+ >>> bool(p.low <= 2 / 3 <= p.high)
284
+ True
285
+
286
+ `DPP.evaluated` gives the distribution of a DPP's outputs for each value of its inputs, as
287
+ a `LikelihoodTable`. Printed, it has one row per input value and one column per output
288
+ value:
289
+
290
+ >>> print(test.evaluated())
291
+ | False | True
292
+ --------------------
293
+ False | 0.98 | 0.02
294
+ True | 0.01 | 0.99
295
+
296
+ A table is called inside a `@DPP` function as the DPP it came from is, so a DPP can be
297
+ computed once and the result used in many later DPPs. Where the DPP observes, the
298
+ probabilities for one input value sum to the probability, on that input, that every
299
+ observation holds, rather than to 1; this is what makes calling the table the same as
300
+ calling the DPP. Printing divides them by their sum, and `LikelihoodTable.normalised`
301
+ gives a table of the divided values, as in the
302
+ [German tank problem](examples/bayesian_statistics.html).
303
+
304
+ ## Drawing a DPP
305
+
306
+ `pylea.syntax.dpp.DPP.diagram` draws a DPP as a diagram: one dot per variable, one box per
307
+ assignment, read bottom to top from the inputs to the outputs. The Boolean gates take
308
+ their usual symbols, a coin flip is a circle carrying its probability, an observation is an
309
+ eye, and a call is a box carrying the name of the DPP it calls. The diagram of `test`
310
+ above:
311
+
312
+ >>> test.diagram()
313
+ <Diagram test: 15 nodes, ... x ...>
314
+
315
+ ![The diagram of test](test.svg)
316
+
317
+ The wire entering at the bottom carries the parameter it stands for. A wire leaving at the
318
+ top is named the same way where the DPP returned a variable; this one returns an
319
+ expression, which has no name to show.
320
+
321
+ `pylea.draw.Diagram.svg` gives the markup, `pylea.draw.Diagram.tikz` a figure for a LaTeX
322
+ document, `pylea.draw.Diagram.save` a file, and `pylea.draw.Diagram.show` opens one in a
323
+ browser; a notebook renders it where it stands.
324
+
325
+ There is no nesting: a called DPP is drawn by asking it for its own diagram.
326
+ `pylea.syntax.dpp.DPP.simplified` draws what the rewrite rules of `pylea.simplify` make of
327
+ a DPP, which is how a DPP that compiles more slowly than expected is read.
328
+
329
+ `pylea.syntax.dpp.DPP.dependency_graph` draws the DPP's variables instead of its
330
+ assignments: one box per variable the DPP is given or returns, and an arrow to each
331
+ variable computed from it. For a DPP whose outputs are the variables of a Bayesian
332
+ network, that is the network:
333
+
334
+ >>> from pylea.examples.sprinkler import sprinkler_network
335
+ >>> sprinkler_network.dependency_graph()
336
+ <Diagram sprinkler_network: 4 nodes, ... x ...>
337
+
338
+ ![The dependency graph of sprinkler_network](sprinkler_network.svg)
339
+
340
+ It reads from the top down: the cloudiness is drawn first, the sprinkler state and the rain
341
+ from it, and the wetness of the grass from those two. A box carries the name the DPP
342
+ returned the variable as. What is drawn is what each variable is computed from, so two
343
+ outputs sharing an internal variable the DPP does not return are drawn apart, and
344
+ conditioning is not drawn at all.
345
+
346
+
347
+ ## Modules
348
+
349
+ * `pylea.syntax`, the language: `@DPP`, variables, and the primitives beneath.
350
+ * `pylea.semantics`, the matrices of the primitives and the semantics of a DPP.
351
+ * `pylea.ir`, the intermediate representation a DPP is traced into.
352
+ * `pylea.simplify`, the rewrite rules applied to a DPP before it is lowered.
353
+ * `pylea.compile`, the tensor network, the contraction tree, and the resulting plans.
354
+ * `pylea.eval`, running a plan on numpy or on a torch device.
355
+ * `pylea.sample`, the sampling engine and the choice between the two engines.
356
+ * `pylea.draw`, drawing a DPP as a diagram.
357
+ * `pylea.utility`, converters from `.bif` Bayesian networks and `.dft` fault trees.
358
+ * `pylea.examples`, example DPPs.
359
+ """
360
+ from pylea.semantics.likelihood_table import LikelihoodTable
361
+ from pylea.syntax.choice import pick
362
+ from pylea.syntax.dpp import DPP, Bool, at_least
363
+ from pylea.syntax.enum import Enum
364
+ from pylea.syntax.estimate import Estimate
365
+ from pylea.syntax.expectation import expectation
366
+ from pylea.syntax.flip import flip
367
+ from pylea.syntax.integer import UInt, count
368
+ from pylea.syntax.observe import observe
369
+ from pylea.syntax.pmf import probability_mass_function
370
+ from pylea.syntax.prob import prob
371
+ from pylea.syntax.tuple import Tuple
372
+ from pylea.syntax.variable import Variable
373
+
374
+ __all__ = ["DPP", "Bool", "Enum", "Tuple", "UInt", "Variable", "flip", "observe",
375
+ "at_least", "count", "pick", "prob", "probability_mass_function",
376
+ "expectation", "Estimate", "LikelihoodTable"]
@@ -0,0 +1 @@
1
+ """Compilation: a DPP becomes the plans the evaluator runs."""
@@ -0,0 +1,31 @@
1
+ """Subprocess worker running one bounded `pylea.compile.order.AdaptiveOrderFinder` search.
2
+
3
+ An `AdaptiveOrderFinder` with a `max_time` runs its search here, so that the search can be
4
+ stopped at that bound: cotengra tests its own budget only between trials, and on a wide
5
+ enough network a single trial runs far beyond it. The protocol is one pickled request on
6
+ stdin, ``((optimize, escalate_cost, max_time), (indices, output, size, kappa))``, and one
7
+ pickled response on stdout, ``{"order": order}`` or ``{"error": message}``. The finder's
8
+ settings travel rather than the finder itself, so a subclass defined anywhere still runs.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import pickle
13
+ import sys
14
+
15
+
16
+ def main() -> None:
17
+ from pylea.compile.order import AdaptiveOrderFinder
18
+
19
+ (optimize, escalate_cost, max_time), (indices, output, size, kappa) = pickle.load(
20
+ sys.stdin.buffer)
21
+ finder = AdaptiveOrderFinder(optimize, escalate_cost, max_time)
22
+ try:
23
+ response = {"order": finder.find_here(indices, output, size, kappa=kappa)}
24
+ except Exception as exc: # noqa: BLE001, reported to the parent
25
+ response = {"error": f"{type(exc).__name__}: {exc}"}
26
+ pickle.dump(response, sys.stdout.buffer)
27
+ sys.stdout.buffer.flush()
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()