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.
- pylea-0.1.0/LICENSE +21 -0
- pylea-0.1.0/PKG-INFO +99 -0
- pylea-0.1.0/README.md +55 -0
- pylea-0.1.0/pylea/__init__.py +376 -0
- pylea-0.1.0/pylea/compile/__init__.py +1 -0
- pylea-0.1.0/pylea/compile/_order_worker.py +31 -0
- pylea-0.1.0/pylea/compile/cost.py +209 -0
- pylea-0.1.0/pylea/compile/lower.py +191 -0
- pylea-0.1.0/pylea/compile/network.py +139 -0
- pylea-0.1.0/pylea/compile/order.py +405 -0
- pylea-0.1.0/pylea/compile/plan.py +158 -0
- pylea-0.1.0/pylea/compile/planner.py +169 -0
- pylea-0.1.0/pylea/draw/__init__.py +172 -0
- pylea-0.1.0/pylea/draw/coordinates.py +192 -0
- pylea-0.1.0/pylea/draw/dependency.py +169 -0
- pylea-0.1.0/pylea/draw/glyph.py +345 -0
- pylea-0.1.0/pylea/draw/layout.py +526 -0
- pylea-0.1.0/pylea/draw/sketch.py +303 -0
- pylea-0.1.0/pylea/draw/svg.py +136 -0
- pylea-0.1.0/pylea/draw/tikz.py +124 -0
- pylea-0.1.0/pylea/eval/__init__.py +1 -0
- pylea-0.1.0/pylea/eval/backend.py +404 -0
- pylea-0.1.0/pylea/eval/interp.py +235 -0
- pylea-0.1.0/pylea/eval/kernels.py +194 -0
- pylea-0.1.0/pylea/examples/__init__.py +13 -0
- pylea-0.1.0/pylea/examples/bioassay.py +129 -0
- pylea-0.1.0/pylea/examples/birthday.py +35 -0
- pylea-0.1.0/pylea/examples/disease.py +46 -0
- pylea-0.1.0/pylea/examples/electoral_college.py +92 -0
- pylea-0.1.0/pylea/examples/german_tank.py +133 -0
- pylea-0.1.0/pylea/examples/holdem.py +265 -0
- pylea-0.1.0/pylea/examples/monty_hall.py +46 -0
- pylea-0.1.0/pylea/examples/percolation.py +101 -0
- pylea-0.1.0/pylea/examples/secret_santa.py +51 -0
- pylea-0.1.0/pylea/examples/simple.py +288 -0
- pylea-0.1.0/pylea/examples/sprinkler.py +92 -0
- pylea-0.1.0/pylea/ir/__init__.py +33 -0
- pylea-0.1.0/pylea/ir/bits.py +23 -0
- pylea-0.1.0/pylea/ir/body.py +207 -0
- pylea-0.1.0/pylea/ir/dependency.py +225 -0
- pylea-0.1.0/pylea/ir/inline.py +140 -0
- pylea-0.1.0/pylea/ir/prim.py +296 -0
- pylea-0.1.0/pylea/ir/program.py +329 -0
- pylea-0.1.0/pylea/ir/reference.py +84 -0
- pylea-0.1.0/pylea/sample/__init__.py +58 -0
- pylea-0.1.0/pylea/sample/_exact_worker.py +79 -0
- pylea-0.1.0/pylea/sample/certify.py +479 -0
- pylea-0.1.0/pylea/sample/collapse.py +145 -0
- pylea-0.1.0/pylea/sample/cost_model.py +201 -0
- pylea-0.1.0/pylea/sample/estimate.py +103 -0
- pylea-0.1.0/pylea/sample/packed.py +192 -0
- pylea-0.1.0/pylea/sample/pilot.py +242 -0
- pylea-0.1.0/pylea/sample/portfolio.py +421 -0
- pylea-0.1.0/pylea/sample/query.py +256 -0
- pylea-0.1.0/pylea/sample/run.py +479 -0
- pylea-0.1.0/pylea/sample/schedule.py +261 -0
- pylea-0.1.0/pylea/sample/sweep.py +622 -0
- pylea-0.1.0/pylea/semantics/__init__.py +1 -0
- pylea-0.1.0/pylea/semantics/likelihood_table.py +278 -0
- pylea-0.1.0/pylea/semantics/matrices.py +133 -0
- pylea-0.1.0/pylea/simplify/__init__.py +44 -0
- pylea-0.1.0/pylea/simplify/absorb.py +93 -0
- pylea-0.1.0/pylea/simplify/boolean.py +220 -0
- pylea-0.1.0/pylea/simplify/common.py +580 -0
- pylea-0.1.0/pylea/simplify/cse.py +96 -0
- pylea-0.1.0/pylea/simplify/dce.py +97 -0
- pylea-0.1.0/pylea/simplify/driver.py +152 -0
- pylea-0.1.0/pylea/simplify/fold.py +622 -0
- pylea-0.1.0/pylea/simplify/implication.py +238 -0
- pylea-0.1.0/pylea/simplify/inline.py +48 -0
- pylea-0.1.0/pylea/simplify/lift.py +315 -0
- pylea-0.1.0/pylea/simplify/mask.py +176 -0
- pylea-0.1.0/pylea/simplify/observe.py +173 -0
- pylea-0.1.0/pylea/syntax/__init__.py +1 -0
- pylea-0.1.0/pylea/syntax/choice.py +99 -0
- pylea-0.1.0/pylea/syntax/comparison.py +72 -0
- pylea-0.1.0/pylea/syntax/compiled.py +251 -0
- pylea-0.1.0/pylea/syntax/dpp.py +1366 -0
- pylea-0.1.0/pylea/syntax/encoding.py +108 -0
- pylea-0.1.0/pylea/syntax/enum.py +576 -0
- pylea-0.1.0/pylea/syntax/estimate.py +86 -0
- pylea-0.1.0/pylea/syntax/expectation.py +159 -0
- pylea-0.1.0/pylea/syntax/flip.py +57 -0
- pylea-0.1.0/pylea/syntax/integer.py +224 -0
- pylea-0.1.0/pylea/syntax/method.py +161 -0
- pylea-0.1.0/pylea/syntax/observe.py +73 -0
- pylea-0.1.0/pylea/syntax/pmf.py +85 -0
- pylea-0.1.0/pylea/syntax/prob.py +82 -0
- pylea-0.1.0/pylea/syntax/tuple.py +177 -0
- pylea-0.1.0/pylea/syntax/variable.py +244 -0
- pylea-0.1.0/pylea/utility/__init__.py +1 -0
- pylea-0.1.0/pylea/utility/bif_to_dpp.py +646 -0
- pylea-0.1.0/pylea/utility/dft_to_dpp.py +363 -0
- pylea-0.1.0/pylea.egg-info/PKG-INFO +99 -0
- pylea-0.1.0/pylea.egg-info/SOURCES.txt +98 -0
- pylea-0.1.0/pylea.egg-info/dependency_links.txt +1 -0
- pylea-0.1.0/pylea.egg-info/requires.txt +25 -0
- pylea-0.1.0/pylea.egg-info/top_level.txt +1 -0
- pylea-0.1.0/pyproject.toml +60 -0
- 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
|
+

|
|
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
|
+

|
|
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()
|