compiled-knowledge 4.0.0a5__cp313-cp313-macosx_10_13_universal2.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.
Potentially problematic release.
This version of compiled-knowledge might be problematic. Click here for more details.
- ck/__init__.py +0 -0
- ck/circuit/__init__.py +13 -0
- ck/circuit/circuit.c +38749 -0
- ck/circuit/circuit.cpython-313-darwin.so +0 -0
- ck/circuit/circuit_py.py +807 -0
- ck/circuit/tmp_const.py +74 -0
- ck/circuit_compiler/__init__.py +2 -0
- ck/circuit_compiler/circuit_compiler.py +26 -0
- ck/circuit_compiler/cython_vm_compiler/__init__.py +1 -0
- ck/circuit_compiler/cython_vm_compiler/_compiler.c +17373 -0
- ck/circuit_compiler/cython_vm_compiler/_compiler.cpython-313-darwin.so +0 -0
- ck/circuit_compiler/cython_vm_compiler/cython_vm_compiler.py +96 -0
- ck/circuit_compiler/interpret_compiler.py +223 -0
- ck/circuit_compiler/llvm_compiler.py +388 -0
- ck/circuit_compiler/llvm_vm_compiler.py +546 -0
- ck/circuit_compiler/named_circuit_compilers.py +57 -0
- ck/circuit_compiler/support/__init__.py +0 -0
- ck/circuit_compiler/support/circuit_analyser.py +81 -0
- ck/circuit_compiler/support/input_vars.py +148 -0
- ck/circuit_compiler/support/llvm_ir_function.py +234 -0
- ck/example/__init__.py +53 -0
- ck/example/alarm.py +366 -0
- ck/example/asia.py +28 -0
- ck/example/binary_clique.py +32 -0
- ck/example/bow_tie.py +33 -0
- ck/example/cancer.py +37 -0
- ck/example/chain.py +38 -0
- ck/example/child.py +199 -0
- ck/example/clique.py +33 -0
- ck/example/cnf_pgm.py +39 -0
- ck/example/diamond_square.py +68 -0
- ck/example/earthquake.py +36 -0
- ck/example/empty.py +10 -0
- ck/example/hailfinder.py +539 -0
- ck/example/hepar2.py +628 -0
- ck/example/insurance.py +504 -0
- ck/example/loop.py +40 -0
- ck/example/mildew.py +38161 -0
- ck/example/munin.py +22982 -0
- ck/example/pathfinder.py +53674 -0
- ck/example/rain.py +39 -0
- ck/example/rectangle.py +161 -0
- ck/example/run.py +30 -0
- ck/example/sachs.py +129 -0
- ck/example/sprinkler.py +30 -0
- ck/example/star.py +44 -0
- ck/example/stress.py +64 -0
- ck/example/student.py +43 -0
- ck/example/survey.py +46 -0
- ck/example/triangle_square.py +54 -0
- ck/example/truss.py +49 -0
- ck/in_out/__init__.py +3 -0
- ck/in_out/parse_ace_lmap.py +216 -0
- ck/in_out/parse_ace_nnf.py +288 -0
- ck/in_out/parse_net.py +480 -0
- ck/in_out/parser_utils.py +185 -0
- ck/in_out/pgm_pickle.py +42 -0
- ck/in_out/pgm_python.py +268 -0
- ck/in_out/render_bugs.py +111 -0
- ck/in_out/render_net.py +177 -0
- ck/in_out/render_pomegranate.py +184 -0
- ck/pgm.py +3494 -0
- ck/pgm_circuit/__init__.py +1 -0
- ck/pgm_circuit/marginals_program.py +352 -0
- ck/pgm_circuit/mpe_program.py +237 -0
- ck/pgm_circuit/pgm_circuit.py +75 -0
- ck/pgm_circuit/program_with_slotmap.py +234 -0
- ck/pgm_circuit/slot_map.py +35 -0
- ck/pgm_circuit/support/__init__.py +0 -0
- ck/pgm_circuit/support/compile_circuit.py +83 -0
- ck/pgm_circuit/target_marginals_program.py +103 -0
- ck/pgm_circuit/wmc_program.py +323 -0
- ck/pgm_compiler/__init__.py +2 -0
- ck/pgm_compiler/ace/__init__.py +1 -0
- ck/pgm_compiler/ace/ace.py +252 -0
- ck/pgm_compiler/factor_elimination.py +383 -0
- ck/pgm_compiler/named_pgm_compilers.py +63 -0
- ck/pgm_compiler/pgm_compiler.py +19 -0
- ck/pgm_compiler/recursive_conditioning.py +226 -0
- ck/pgm_compiler/support/__init__.py +0 -0
- ck/pgm_compiler/support/circuit_table/__init__.py +9 -0
- ck/pgm_compiler/support/circuit_table/circuit_table.c +16042 -0
- ck/pgm_compiler/support/circuit_table/circuit_table.cpython-313-darwin.so +0 -0
- ck/pgm_compiler/support/circuit_table/circuit_table_py.py +269 -0
- ck/pgm_compiler/support/clusters.py +556 -0
- ck/pgm_compiler/support/factor_tables.py +398 -0
- ck/pgm_compiler/support/join_tree.py +275 -0
- ck/pgm_compiler/support/named_compiler_maker.py +33 -0
- ck/pgm_compiler/variable_elimination.py +89 -0
- ck/probability/__init__.py +0 -0
- ck/probability/empirical_probability_space.py +47 -0
- ck/probability/probability_space.py +568 -0
- ck/program/__init__.py +3 -0
- ck/program/program.py +129 -0
- ck/program/program_buffer.py +180 -0
- ck/program/raw_program.py +61 -0
- ck/sampling/__init__.py +0 -0
- ck/sampling/forward_sampler.py +211 -0
- ck/sampling/marginals_direct_sampler.py +113 -0
- ck/sampling/sampler.py +62 -0
- ck/sampling/sampler_support.py +232 -0
- ck/sampling/uniform_sampler.py +66 -0
- ck/sampling/wmc_direct_sampler.py +169 -0
- ck/sampling/wmc_gibbs_sampler.py +147 -0
- ck/sampling/wmc_metropolis_sampler.py +159 -0
- ck/sampling/wmc_rejection_sampler.py +113 -0
- ck/utils/__init__.py +0 -0
- ck/utils/iter_extras.py +153 -0
- ck/utils/map_list.py +128 -0
- ck/utils/map_set.py +128 -0
- ck/utils/np_extras.py +51 -0
- ck/utils/random_extras.py +64 -0
- ck/utils/tmp_dir.py +94 -0
- ck_demos/__init__.py +0 -0
- ck_demos/ace/__init__.py +0 -0
- ck_demos/ace/copy_ace_to_ck.py +15 -0
- ck_demos/ace/demo_ace.py +44 -0
- ck_demos/all_demos.py +88 -0
- ck_demos/circuit/__init__.py +0 -0
- ck_demos/circuit/demo_circuit_dump.py +22 -0
- ck_demos/circuit/demo_derivatives.py +43 -0
- ck_demos/circuit_compiler/__init__.py +0 -0
- ck_demos/circuit_compiler/compare_circuit_compilers.py +32 -0
- ck_demos/circuit_compiler/show_llvm_program.py +26 -0
- ck_demos/pgm/__init__.py +0 -0
- ck_demos/pgm/demo_pgm_dump.py +18 -0
- ck_demos/pgm/demo_pgm_dump_stress.py +18 -0
- ck_demos/pgm/demo_pgm_string_rendering.py +15 -0
- ck_demos/pgm/show_examples.py +25 -0
- ck_demos/pgm_compiler/__init__.py +0 -0
- ck_demos/pgm_compiler/compare_pgm_compilers.py +50 -0
- ck_demos/pgm_compiler/demo_compiler_dump.py +50 -0
- ck_demos/pgm_compiler/demo_factor_elimination.py +47 -0
- ck_demos/pgm_compiler/demo_join_tree.py +25 -0
- ck_demos/pgm_compiler/demo_marginals_program.py +53 -0
- ck_demos/pgm_compiler/demo_mpe_program.py +55 -0
- ck_demos/pgm_compiler/demo_pgm_compiler.py +38 -0
- ck_demos/pgm_compiler/demo_recursive_conditioning.py +33 -0
- ck_demos/pgm_compiler/demo_variable_elimination.py +33 -0
- ck_demos/pgm_compiler/demo_wmc_program.py +29 -0
- ck_demos/pgm_inference/__init__.py +0 -0
- ck_demos/pgm_inference/demo_inferencing_basic.py +188 -0
- ck_demos/pgm_inference/demo_inferencing_mpe_cancer.py +45 -0
- ck_demos/pgm_inference/demo_inferencing_wmc_and_mpe_sprinkler.py +154 -0
- ck_demos/pgm_inference/demo_inferencing_wmc_student.py +110 -0
- ck_demos/programs/__init__.py +0 -0
- ck_demos/programs/demo_program_buffer.py +24 -0
- ck_demos/programs/demo_program_multi.py +24 -0
- ck_demos/programs/demo_program_none.py +19 -0
- ck_demos/programs/demo_program_single.py +23 -0
- ck_demos/programs/demo_raw_program_interpreted.py +21 -0
- ck_demos/programs/demo_raw_program_llvm.py +21 -0
- ck_demos/sampling/__init__.py +0 -0
- ck_demos/sampling/check_sampler.py +71 -0
- ck_demos/sampling/demo_marginal_direct_sampler.py +40 -0
- ck_demos/sampling/demo_uniform_sampler.py +38 -0
- ck_demos/sampling/demo_wmc_direct_sampler.py +40 -0
- ck_demos/utils/__init__.py +0 -0
- ck_demos/utils/compare.py +88 -0
- ck_demos/utils/convert_network.py +45 -0
- ck_demos/utils/sample_model.py +216 -0
- ck_demos/utils/stop_watch.py +384 -0
- compiled_knowledge-4.0.0a5.dist-info/METADATA +50 -0
- compiled_knowledge-4.0.0a5.dist-info/RECORD +167 -0
- compiled_knowledge-4.0.0a5.dist-info/WHEEL +5 -0
- compiled_knowledge-4.0.0a5.dist-info/licenses/LICENSE.txt +21 -0
- compiled_knowledge-4.0.0a5.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes as ct
|
|
4
|
+
from typing import Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ck.program.raw_program import RawProgram, RawProgramFunction
|
|
9
|
+
from ck.utils.np_extras import DTypeNumeric, NDArrayNumeric
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProgramBuffer:
|
|
13
|
+
"""
|
|
14
|
+
A ProgramBuffer wraps a RawProgram with pre-allocated input, tmp, and out buffers.
|
|
15
|
+
The buffers are numpy arrays.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, program: RawProgram):
|
|
19
|
+
self._raw_program: RawProgram = program
|
|
20
|
+
|
|
21
|
+
# Allocate the buffers
|
|
22
|
+
self._array_vars = np.zeros(self.number_of_vars, dtype=self.dtype)
|
|
23
|
+
self._array_tmps = np.zeros(self.number_of_tmps, dtype=self.dtype)
|
|
24
|
+
self._array_outs = np.zeros(self.number_of_results, dtype=self.dtype)
|
|
25
|
+
|
|
26
|
+
# Access the c-buffers
|
|
27
|
+
ptr_type = ct.POINTER(np.ctypeslib.as_ctypes_type(self.dtype))
|
|
28
|
+
self._c_array_vars = self._array_vars.ctypes.data_as(ptr_type)
|
|
29
|
+
self._c_array_tmps = self._array_tmps.ctypes.data_as(ptr_type)
|
|
30
|
+
self._c_array_outs = self._array_outs.ctypes.data_as(ptr_type)
|
|
31
|
+
|
|
32
|
+
# Keep a direct reference to the internal callable.
|
|
33
|
+
self._function: RawProgramFunction = program.function
|
|
34
|
+
|
|
35
|
+
def clone(self) -> ProgramBuffer:
|
|
36
|
+
"""
|
|
37
|
+
Take a copy of this program buffer, with the same raw program
|
|
38
|
+
and same input and output values, but with new memory allocation
|
|
39
|
+
for the buffers.
|
|
40
|
+
"""
|
|
41
|
+
clone = ProgramBuffer(self._raw_program)
|
|
42
|
+
clone[:] = self._array_vars
|
|
43
|
+
clone.results[:] = self._array_outs
|
|
44
|
+
return clone
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def raw_program(self) -> RawProgram:
|
|
48
|
+
"""
|
|
49
|
+
What is the wrapped Program.
|
|
50
|
+
"""
|
|
51
|
+
return self._raw_program
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def dtype(self) -> DTypeNumeric:
|
|
55
|
+
"""
|
|
56
|
+
What is the numpy data type of values.
|
|
57
|
+
This is the same as numpy and ctypes `dtype`.
|
|
58
|
+
"""
|
|
59
|
+
return self._raw_program.dtype
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def number_of_vars(self) -> int:
|
|
63
|
+
"""
|
|
64
|
+
How many input values are there to the function.
|
|
65
|
+
Each input value relates to a circuit VarNode, as
|
|
66
|
+
per method `var_indices`.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
the number of input values.
|
|
70
|
+
"""
|
|
71
|
+
return self._raw_program.number_of_vars
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def number_of_tmps(self) -> int:
|
|
75
|
+
"""
|
|
76
|
+
How many temporary values are there to the function.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
the number of temporary values.
|
|
80
|
+
"""
|
|
81
|
+
return self._raw_program.number_of_tmps
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def number_of_results(self) -> int:
|
|
85
|
+
"""
|
|
86
|
+
How many output values are there from the function.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
the number of output values.
|
|
90
|
+
"""
|
|
91
|
+
return self._raw_program.number_of_results
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def var_indices(self) -> Sequence[int]:
|
|
95
|
+
"""
|
|
96
|
+
Get the circuit `VarNode.index` for each function input.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
a list of the circuit VarNode indices, co-indexed
|
|
100
|
+
with the function input values.
|
|
101
|
+
"""
|
|
102
|
+
return self._raw_program.var_indices
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def vars(self) -> NDArrayNumeric:
|
|
106
|
+
"""
|
|
107
|
+
Return the input variables as a numpy array.
|
|
108
|
+
Writing to the returned array will write to the input slots of the program buffer.
|
|
109
|
+
|
|
110
|
+
Warning:
|
|
111
|
+
the array is backed by the program buffer memory, not a copy.
|
|
112
|
+
"""
|
|
113
|
+
return self._array_vars
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def results(self) -> NDArrayNumeric:
|
|
117
|
+
"""
|
|
118
|
+
Return the results as a numpy array.
|
|
119
|
+
|
|
120
|
+
Warning:
|
|
121
|
+
the array is backed by the program buffer memory, not a copy.
|
|
122
|
+
"""
|
|
123
|
+
return self._array_outs
|
|
124
|
+
|
|
125
|
+
def compute(self) -> NDArrayNumeric:
|
|
126
|
+
"""
|
|
127
|
+
Compute and return the results, as per `self.results`.
|
|
128
|
+
|
|
129
|
+
Warning:
|
|
130
|
+
the array is backed by the program buffer memory, not a copy.
|
|
131
|
+
"""
|
|
132
|
+
self._function(self._c_array_vars, self._c_array_tmps, self._c_array_outs)
|
|
133
|
+
return self._array_outs
|
|
134
|
+
|
|
135
|
+
def __setitem__(self, idx: int | slice, value: float) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Set the value of the indexed input variable.
|
|
138
|
+
"""
|
|
139
|
+
self._array_vars[idx] = value
|
|
140
|
+
|
|
141
|
+
def __getitem__(self, idx: int | slice) -> NDArrayNumeric:
|
|
142
|
+
"""
|
|
143
|
+
Get the value of the indexed input variable.
|
|
144
|
+
"""
|
|
145
|
+
return self._array_vars[idx]
|
|
146
|
+
|
|
147
|
+
def __len__(self) -> int:
|
|
148
|
+
"""
|
|
149
|
+
Number of input variables.
|
|
150
|
+
"""
|
|
151
|
+
return len(self._array_vars)
|
|
152
|
+
|
|
153
|
+
def __getstate__(self):
|
|
154
|
+
"""
|
|
155
|
+
Support for pickle.
|
|
156
|
+
"""
|
|
157
|
+
return {
|
|
158
|
+
'_raw_program': self._raw_program,
|
|
159
|
+
'_array_vars': self._array_vars,
|
|
160
|
+
'_array_tmps': self._array_tmps,
|
|
161
|
+
'_array_outs': self._array_outs,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def __setstate__(self, state):
|
|
165
|
+
"""
|
|
166
|
+
Support for pickle.
|
|
167
|
+
"""
|
|
168
|
+
self._raw_program = state['_raw_program']
|
|
169
|
+
self._array_vars = state['_array_vars']
|
|
170
|
+
self._array_tmps = state['_array_tmps']
|
|
171
|
+
self._array_outs = state['_array_outs']
|
|
172
|
+
|
|
173
|
+
# Access the c-buffers
|
|
174
|
+
ptr_type = ct.POINTER(self.dtype)
|
|
175
|
+
self._c_array_vars = self._array_vars.ctypes.data_as(ptr_type)
|
|
176
|
+
self._c_array_tmps = self._array_tmps.ctypes.data_as(ptr_type)
|
|
177
|
+
self._c_array_outs = self._array_outs.ctypes.data_as(ptr_type)
|
|
178
|
+
|
|
179
|
+
# Keep a direct reference to the internal callable.
|
|
180
|
+
self._function: RawProgramFunction = self._raw_program.function
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable, Sequence
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import ctypes as ct
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from ck.utils.np_extras import NDArrayNumeric, DTypeNumeric
|
|
9
|
+
|
|
10
|
+
# RawProgramFunction is a function of three ctypes arrays, returning nothing.
|
|
11
|
+
# Args:
|
|
12
|
+
# [0]: input parameter values,
|
|
13
|
+
# [1]: working memory,
|
|
14
|
+
# [2]: output values.
|
|
15
|
+
RawProgramFunction = Callable[[ct.POINTER, ct.POINTER, ct.POINTER], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RawProgram:
|
|
20
|
+
"""
|
|
21
|
+
Fields:
|
|
22
|
+
function: is a function of three ctypes arrays, returning nothing.
|
|
23
|
+
dtype: the numpy data type of the array values.
|
|
24
|
+
number_of_vars: the number of input parameter values (first function argument).
|
|
25
|
+
number_of_tmps: the number of working memory values (second function argument).
|
|
26
|
+
number_of_results: the number of result values (third function argument).
|
|
27
|
+
var_indices: maps the index of inputs (from 0 to self.number_of_vars - 1) to the index
|
|
28
|
+
of the corresponding circuit var.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
function: RawProgramFunction
|
|
32
|
+
dtype: DTypeNumeric
|
|
33
|
+
number_of_vars: int
|
|
34
|
+
number_of_tmps: int
|
|
35
|
+
number_of_results: int
|
|
36
|
+
var_indices: Sequence[int]
|
|
37
|
+
|
|
38
|
+
def __call__(self, in_vars: NDArrayNumeric | Sequence[int | float]) -> NDArrayNumeric:
|
|
39
|
+
"""
|
|
40
|
+
Call the raw program as a function from an array to an array.
|
|
41
|
+
"""
|
|
42
|
+
array_vars: NDArrayNumeric
|
|
43
|
+
if isinstance(vars, np.ndarray):
|
|
44
|
+
array_vars = in_vars
|
|
45
|
+
else:
|
|
46
|
+
array_vars = np.array(in_vars, dtype=self.dtype)
|
|
47
|
+
if array_vars.shape != (self.number_of_vars,):
|
|
48
|
+
raise ValueError(f'input array incorrect shape: got {array_vars.shape} expected ({self.number_of_vars},)')
|
|
49
|
+
if array_vars.dtype != self.dtype:
|
|
50
|
+
raise ValueError(f'input array incorrect dtype: got {array_vars.dtype} expected {self.dtype}')
|
|
51
|
+
|
|
52
|
+
array_tmps: NDArrayNumeric = np.zeros(self.number_of_tmps, dtype=self.dtype)
|
|
53
|
+
array_outs: NDArrayNumeric = np.zeros(self.number_of_results, dtype=self.dtype)
|
|
54
|
+
|
|
55
|
+
ptr_type = ct.POINTER(np.ctypeslib.as_ctypes_type(self.dtype))
|
|
56
|
+
c_array_vars = array_vars.ctypes.data_as(ptr_type)
|
|
57
|
+
c_array_tmps = array_tmps.ctypes.data_as(ptr_type)
|
|
58
|
+
c_array_outs = array_outs.ctypes.data_as(ptr_type)
|
|
59
|
+
|
|
60
|
+
self.function(c_array_vars, c_array_tmps, c_array_outs)
|
|
61
|
+
return array_outs
|
ck/sampling/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines a Sampler for Bayesian networks using
|
|
3
|
+
the Forward Sampling method.
|
|
4
|
+
"""
|
|
5
|
+
import random
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Optional, Iterator, Tuple, Sequence, Dict, List, Set
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ck.pgm import PGM, RandomVariable, PotentialFunction, Instance, Indicator
|
|
12
|
+
from ck.probability.probability_space import Condition, check_condition, dtype_for_state_indexes
|
|
13
|
+
from ck.sampling.sampler import Sampler
|
|
14
|
+
from ck.sampling.sampler_support import YieldF
|
|
15
|
+
from ck.utils.np_extras import NDArrayStates, DTypeStates
|
|
16
|
+
from ck.utils.random_extras import Random
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ForwardSampler(Sampler):
|
|
20
|
+
"""
|
|
21
|
+
A ForwardSampler operates directly on a Bayesian network PGM.
|
|
22
|
+
It does not compile the PGM but directly uses the CPTs.
|
|
23
|
+
|
|
24
|
+
It determines the parent random variables to sample (recursively)
|
|
25
|
+
and samples them in a 'forward' order.
|
|
26
|
+
|
|
27
|
+
Conditioning is implemented by rejecting samples incompatible with the condition,
|
|
28
|
+
thus if the probability of the condition is low, the sampler will reject
|
|
29
|
+
many samples, which may be slow.
|
|
30
|
+
|
|
31
|
+
When unconditioned, this sampler can be very efficient.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
pgm: PGM,
|
|
37
|
+
rvs: Optional[RandomVariable | Sequence[RandomVariable]] = None,
|
|
38
|
+
condition: Condition = (),
|
|
39
|
+
rand: Random = random,
|
|
40
|
+
check_is_bayesian_network: bool = True
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Construct a forward sampler for the given PGM.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
pgm: is the model to be sampled. It is assumed to be a proper Bayesian network.
|
|
47
|
+
rvs: the list of random variables to sample; the yielded state vectors are
|
|
48
|
+
co-indexed with rvs; if None, then the WMC rvs are used; if rvs is a single
|
|
49
|
+
random variable, then single samples are yielded.
|
|
50
|
+
condition: is a collection of zero or more conditioning indicators.
|
|
51
|
+
rand: provides the stream of random numbers.
|
|
52
|
+
check_is_bayesian_network: is a Boolean flag. If true, then the
|
|
53
|
+
constructor will raise an exception if pgm.check_is_bayesian_network()
|
|
54
|
+
is false.
|
|
55
|
+
"""
|
|
56
|
+
if check_is_bayesian_network and not pgm.check_is_bayesian_network():
|
|
57
|
+
raise ValueError('the given PGM is not a Bayesian network')
|
|
58
|
+
|
|
59
|
+
condition: Sequence[Indicator] = check_condition(condition)
|
|
60
|
+
|
|
61
|
+
self._yield_f: YieldF
|
|
62
|
+
if rvs is None:
|
|
63
|
+
# all input rvs
|
|
64
|
+
rvs = pgm.rvs
|
|
65
|
+
self._yield_f = lambda x: x.tolist()
|
|
66
|
+
elif rvs in pgm.rvs:
|
|
67
|
+
# a single rv
|
|
68
|
+
rvs = (rvs,)
|
|
69
|
+
self._yield_f = lambda x: x.item()
|
|
70
|
+
else:
|
|
71
|
+
# a sequence of rvs
|
|
72
|
+
self._yield_f = lambda x: x.tolist()
|
|
73
|
+
|
|
74
|
+
super().__init__(rvs, condition)
|
|
75
|
+
|
|
76
|
+
# Create a map from rv_idx to its factor.
|
|
77
|
+
# This assumes a Bayesian network structure - one factor per rv.
|
|
78
|
+
rv_factors = {
|
|
79
|
+
factor.rvs[0].idx: factor
|
|
80
|
+
for factor in pgm.factors
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Infer sampling order of random variables.
|
|
84
|
+
# Infer the mapping from random variables to yielded samples.
|
|
85
|
+
# Get a _SampleRV for each rv in rvs, and for pre-requisites.
|
|
86
|
+
# This assumes a Bayesian network structure - no directed loops.
|
|
87
|
+
sample_rvs_map: Dict[int, _SampleRV] = {} # map from rv index to _SampleRV object
|
|
88
|
+
sample_rvs_list: List[_SampleRV] = [] # sequence of _SampleRV objects for sampling order
|
|
89
|
+
output_sample_rvs: Sequence[_SampleRV] = tuple(
|
|
90
|
+
_get_sample_rv(rv.idx, rv_factors, sample_rvs_map, sample_rvs_list)
|
|
91
|
+
for rv in rvs
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Add constraints to sample rvs, based on conditions.
|
|
95
|
+
ind: Indicator
|
|
96
|
+
for ind in condition:
|
|
97
|
+
sample_rv: _SampleRV = _get_sample_rv(ind.rv_idx, rv_factors, sample_rvs_map, sample_rvs_list)
|
|
98
|
+
constraints = sample_rv.constraints
|
|
99
|
+
if constraints is None:
|
|
100
|
+
constraints = sample_rv.constraints = set()
|
|
101
|
+
constraints.add(ind.state_idx)
|
|
102
|
+
|
|
103
|
+
# Store needed data.
|
|
104
|
+
self._dtype: DTypeStates = dtype_for_state_indexes(pgm.rvs)
|
|
105
|
+
self._rand: Random = rand
|
|
106
|
+
self._sample_rvs_list: Sequence[_SampleRV] = sample_rvs_list
|
|
107
|
+
self._output_sample_rvs: Sequence[_SampleRV] = output_sample_rvs
|
|
108
|
+
|
|
109
|
+
def __iter__(self) -> Iterator[Instance] | Iterator[int]:
|
|
110
|
+
yield_f = self._yield_f
|
|
111
|
+
output_sample_rvs = self._output_sample_rvs
|
|
112
|
+
|
|
113
|
+
# Allocate working memory for yielded result
|
|
114
|
+
instance: NDArrayStates = np.zeros(len(output_sample_rvs), dtype=self._dtype)
|
|
115
|
+
|
|
116
|
+
# Allocate working memory for sampled states
|
|
117
|
+
states: NDArrayStates = np.zeros(len(self._sample_rvs_list), dtype=self._dtype)
|
|
118
|
+
|
|
119
|
+
while True:
|
|
120
|
+
success = False
|
|
121
|
+
while not success:
|
|
122
|
+
success = self._set_sample(states)
|
|
123
|
+
|
|
124
|
+
for i, sample_rv in enumerate(output_sample_rvs):
|
|
125
|
+
instance[i] = states[sample_rv.val_idx]
|
|
126
|
+
|
|
127
|
+
yield yield_f(instance)
|
|
128
|
+
|
|
129
|
+
def _set_sample(self, states: NDArrayStates) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Set the states array with a sample.
|
|
132
|
+
The states array is co-indexed with self._sample_rvs_list.
|
|
133
|
+
If the sample has zero probability (i.e. it should be rejected)
|
|
134
|
+
then False is returned, otherwise True is returned.
|
|
135
|
+
"""
|
|
136
|
+
sample_rvs_list = self._sample_rvs_list
|
|
137
|
+
rand = self._rand
|
|
138
|
+
|
|
139
|
+
for sample_rv in sample_rvs_list:
|
|
140
|
+
state = sample_rv.sample(rand, states)
|
|
141
|
+
|
|
142
|
+
# Check that a sample was possible (i.e., positive conditioned
|
|
143
|
+
# probability for some state of `sample_rv`).
|
|
144
|
+
if state is None:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Check for possible rejection based on conditioning
|
|
148
|
+
if sample_rv.constraints is not None and state not in sample_rv.constraints:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
states[sample_rv.val_idx] = state
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass
|
|
156
|
+
class _SampleRV:
|
|
157
|
+
rv: RandomVariable
|
|
158
|
+
function: PotentialFunction
|
|
159
|
+
val_idx: int
|
|
160
|
+
parent_val_indexes: Sequence[int]
|
|
161
|
+
constraints: Optional[Set[int]]
|
|
162
|
+
|
|
163
|
+
def sample(self, rand: Random, states: NDArrayStates) -> Optional[int]:
|
|
164
|
+
"""
|
|
165
|
+
Return a random state index for the random variable, using
|
|
166
|
+
the appropriate distribution over possible states,
|
|
167
|
+
conditioned on the parent states that can be found in 'states'.
|
|
168
|
+
|
|
169
|
+
The function returns None if there was no state with a non-zero probability.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
state index from this random variable, or None if no state with a non-zero probability.
|
|
173
|
+
"""
|
|
174
|
+
num_states: int = len(self.rv)
|
|
175
|
+
function: PotentialFunction = self.function
|
|
176
|
+
parent_val_indexes: Sequence[int] = self.parent_val_indexes
|
|
177
|
+
|
|
178
|
+
x: float = rand.random() # uniform variate in [0, 1)
|
|
179
|
+
|
|
180
|
+
# Get the cumulative CPD, conditioned on parent states.
|
|
181
|
+
parent_states: Tuple[int, ...] = tuple(states.item(i) for i in parent_val_indexes)
|
|
182
|
+
total: float = 0
|
|
183
|
+
for state in range(num_states):
|
|
184
|
+
total += function[(state,) + parent_states]
|
|
185
|
+
if x < total:
|
|
186
|
+
return state
|
|
187
|
+
|
|
188
|
+
if total <= 0:
|
|
189
|
+
# No state with positive probability
|
|
190
|
+
return None
|
|
191
|
+
else:
|
|
192
|
+
return num_states - 1
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _get_sample_rv(rv_idx: int, rv_factors, sample_rvs_map, sample_rvs_list) -> _SampleRV:
|
|
196
|
+
"""
|
|
197
|
+
Get a _SampleRV for the indexed random variable.
|
|
198
|
+
This may be called recursively for the parents of the nominated rv.
|
|
199
|
+
"""
|
|
200
|
+
sample_rv = sample_rvs_map.get(rv_idx)
|
|
201
|
+
if sample_rv is None:
|
|
202
|
+
factor = rv_factors[rv_idx]
|
|
203
|
+
rv = factor.rvs[0]
|
|
204
|
+
parent_val_indexes = tuple(
|
|
205
|
+
_get_sample_rv(parent.idx, rv_factors, sample_rvs_map, sample_rvs_list).val_idx
|
|
206
|
+
for parent in factor[1:]
|
|
207
|
+
)
|
|
208
|
+
sample_rv = _SampleRV(rv, factor.function, len(sample_rvs_map), parent_val_indexes, None)
|
|
209
|
+
sample_rvs_map[rv_idx] = sample_rv
|
|
210
|
+
sample_rvs_list.append(sample_rv)
|
|
211
|
+
return sample_rv
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typing import Collection, Iterator, Dict, Sequence
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ck.pgm import Instance
|
|
6
|
+
from ck.probability.probability_space import dtype_for_state_indexes
|
|
7
|
+
from ck.program.program_buffer import ProgramBuffer
|
|
8
|
+
from ck.program.raw_program import RawProgram
|
|
9
|
+
from ck.sampling.sampler import Sampler
|
|
10
|
+
from ck.sampling.sampler_support import SampleRV, YieldF, SamplerInfo
|
|
11
|
+
from ck.utils.np_extras import NDArray, NDArrayNumeric
|
|
12
|
+
from ck.utils.random_extras import Random
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MarginalsDirectSampler(Sampler):
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
sampler_info: SamplerInfo,
|
|
20
|
+
raw_program: RawProgram,
|
|
21
|
+
rand: Random,
|
|
22
|
+
rv_idx_to_result_offset: Dict[int, int],
|
|
23
|
+
):
|
|
24
|
+
super().__init__(sampler_info.rvs, sampler_info.condition)
|
|
25
|
+
self._yield_f: YieldF = sampler_info.yield_f
|
|
26
|
+
self._rand: Random = rand
|
|
27
|
+
self._program_buffer = ProgramBuffer(raw_program)
|
|
28
|
+
self._sample_rvs: Sequence[SampleRV] = tuple(sampler_info.sample_rvs)
|
|
29
|
+
self._chain_rvs: Sequence[SampleRV] = tuple(
|
|
30
|
+
sample_rv for sample_rv in sampler_info.sample_rvs if sample_rv.copy_index is not None)
|
|
31
|
+
self._state_dtype = dtype_for_state_indexes(self.rvs)
|
|
32
|
+
self._max_number_of_states: int = max((len(rv) for rv in self.rvs), default=0)
|
|
33
|
+
self._slots_1: Collection[int] = sampler_info.slots_1
|
|
34
|
+
|
|
35
|
+
self._marginals: Sequence[NDArrayNumeric] = tuple(
|
|
36
|
+
self._program_buffer.results[
|
|
37
|
+
rv_idx_to_result_offset[sample_rv.rv.idx]
|
|
38
|
+
:
|
|
39
|
+
rv_idx_to_result_offset[sample_rv.rv.idx] + len(sample_rv.rv)
|
|
40
|
+
]
|
|
41
|
+
for sample_rv in sampler_info.sample_rvs
|
|
42
|
+
)
|
|
43
|
+
# Set up the input slots to 0 or 1 to respect conditioning and initial Markov chain states.
|
|
44
|
+
slots: NDArray = self._program_buffer.vars
|
|
45
|
+
for slot in sampler_info.slots_0:
|
|
46
|
+
slots[slot] = 0
|
|
47
|
+
for slot in sampler_info.slots_1:
|
|
48
|
+
slots[slot] = 1
|
|
49
|
+
|
|
50
|
+
def __iter__(self) -> Iterator[Instance] | Iterator[int]:
|
|
51
|
+
yield_f = self._yield_f
|
|
52
|
+
rand = self._rand
|
|
53
|
+
sample_rvs = self._sample_rvs
|
|
54
|
+
chain_rvs = self._chain_rvs
|
|
55
|
+
program_buffer = self._program_buffer
|
|
56
|
+
slots: NDArray = program_buffer.vars
|
|
57
|
+
marginals = self._marginals
|
|
58
|
+
slots_1 = self._slots_1
|
|
59
|
+
|
|
60
|
+
# Set up working memory buffer
|
|
61
|
+
states = np.zeros(len(sample_rvs), dtype=self._state_dtype)
|
|
62
|
+
|
|
63
|
+
def compute() -> float:
|
|
64
|
+
# Compute the program results based on the current input slot values.
|
|
65
|
+
# Return the WMC.
|
|
66
|
+
return program_buffer.compute().item(-1)
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
wmc: float = compute()
|
|
70
|
+
rnd: float = rand.random() * wmc
|
|
71
|
+
|
|
72
|
+
for sample_rv in sample_rvs:
|
|
73
|
+
index: int = sample_rv.index
|
|
74
|
+
if index > 0:
|
|
75
|
+
# No need to execute the program on the first time through
|
|
76
|
+
# as it was done just before entering the loop.
|
|
77
|
+
wmc = compute()
|
|
78
|
+
|
|
79
|
+
rv_dist: NDArray = marginals[sample_rv.index]
|
|
80
|
+
|
|
81
|
+
rv_dist_sum: float = rv_dist.sum()
|
|
82
|
+
if rv_dist_sum <= 0:
|
|
83
|
+
raise RuntimeError('zero probability')
|
|
84
|
+
rv_dist *= wmc / rv_dist_sum
|
|
85
|
+
|
|
86
|
+
state_index: int = -1
|
|
87
|
+
for i in range(len(sample_rv.rv)):
|
|
88
|
+
w = rv_dist.item(i)
|
|
89
|
+
if rnd < w:
|
|
90
|
+
state_index = i
|
|
91
|
+
break
|
|
92
|
+
rnd -= w
|
|
93
|
+
assert state_index >= 0
|
|
94
|
+
|
|
95
|
+
for slot in sample_rv.slots:
|
|
96
|
+
slots[slot] = 0
|
|
97
|
+
slots[sample_rv.slots[state_index]] = 1
|
|
98
|
+
states[index] = state_index
|
|
99
|
+
|
|
100
|
+
yield yield_f(states)
|
|
101
|
+
|
|
102
|
+
# Reset the one slots for the next iteration.
|
|
103
|
+
for slot in slots_1:
|
|
104
|
+
slots[slot] = 1
|
|
105
|
+
|
|
106
|
+
# Copy chain pairs for next iteration.
|
|
107
|
+
# (This writes over any initial chain conditions from slots_1.)
|
|
108
|
+
for sample_rv in chain_rvs:
|
|
109
|
+
rv_slots = sample_rv.slots
|
|
110
|
+
prev_state_idx: int = states.item(sample_rv.copy_index)
|
|
111
|
+
for slot in rv_slots:
|
|
112
|
+
slots[slot] = 0
|
|
113
|
+
slots[rv_slots[prev_state_idx]] = 1
|
ck/sampling/sampler.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from itertools import islice
|
|
3
|
+
from typing import Sequence, Iterator
|
|
4
|
+
|
|
5
|
+
from ck.pgm import RandomVariable, Instance, Indicator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Sampler(ABC):
|
|
9
|
+
"""
|
|
10
|
+
A Sampler provides an unlimited series of samples for one or more random variables.
|
|
11
|
+
The random variables being sampled are provided as a tuple via the `rvs` property.
|
|
12
|
+
|
|
13
|
+
A Sampler will either iterate over Instance objects, where each instance is co-indexed
|
|
14
|
+
with `self.rvs`, or may iterate over single state indexes. Whether a Sampler iterates
|
|
15
|
+
over Instance objects or single state indexes is determined by the implementation.
|
|
16
|
+
If iterating over single state indexes, then `len(self.rvs) == 1`.
|
|
17
|
+
"""
|
|
18
|
+
__slots__ = ('_rvs', '_condition')
|
|
19
|
+
|
|
20
|
+
def __init__(self, rvs: Sequence[RandomVariable], condition: Sequence[Indicator]):
|
|
21
|
+
"""
|
|
22
|
+
Args:
|
|
23
|
+
rvs: a collection of the random variables being
|
|
24
|
+
sampled, co-indexed with each sample provided by `iter(self)`.
|
|
25
|
+
condition: condition on `rvs` that are compiled into the sampler.
|
|
26
|
+
"""
|
|
27
|
+
self._rvs: Sequence[RandomVariable] = tuple(rvs)
|
|
28
|
+
self._condition: Sequence[Indicator] = tuple(condition)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def rvs(self) -> Sequence[RandomVariable]:
|
|
32
|
+
"""
|
|
33
|
+
What random variables are being sampled.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
the random variables being sampled, co-indexed with each sample from `iter(self)`.
|
|
37
|
+
"""
|
|
38
|
+
return self._rvs
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def condition(self) -> Sequence[Indicator]:
|
|
42
|
+
"""
|
|
43
|
+
Condition on `self.rvs` that are compiled into the sampler.
|
|
44
|
+
"""
|
|
45
|
+
return self._condition
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def __iter__(self) -> Iterator[Instance] | Iterator[int]:
|
|
49
|
+
"""
|
|
50
|
+
An unlimited series of samples from a random process.
|
|
51
|
+
Each sample is co-indexed with the random variables provided by `self.rvs`.
|
|
52
|
+
"""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def take(self, number_of_samples: int) -> Iterator[Instance] | Iterator[int]:
|
|
56
|
+
"""
|
|
57
|
+
Take a limited number of samples from `iter(self)`.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
number_of_samples: a limit on the number of samples to provide.
|
|
61
|
+
"""
|
|
62
|
+
return islice(self, number_of_samples)
|