compiled-knowledge 4.0.0a24__cp313-cp313-macosx_11_0_arm64.whl → 4.1.0__cp313-cp313-macosx_11_0_arm64.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/circuit/_circuit_cy.c +1 -1
- ck/circuit/_circuit_cy.cpython-313-darwin.so +0 -0
- ck/circuit/tmp_const.py +5 -4
- ck/circuit_compiler/cython_vm_compiler/_compiler.c +152 -152
- ck/circuit_compiler/cython_vm_compiler/_compiler.cpython-313-darwin.so +0 -0
- ck/circuit_compiler/interpret_compiler.py +2 -2
- ck/circuit_compiler/llvm_compiler.py +4 -4
- ck/circuit_compiler/support/circuit_analyser/_circuit_analyser_cy.c +1 -1
- ck/circuit_compiler/support/circuit_analyser/_circuit_analyser_cy.cpython-313-darwin.so +0 -0
- ck/circuit_compiler/support/input_vars.py +4 -4
- ck/circuit_compiler/support/llvm_ir_function.py +4 -4
- ck/dataset/__init__.py +1 -0
- ck/dataset/cross_table.py +334 -0
- ck/dataset/dataset.py +682 -0
- ck/dataset/dataset_builder.py +519 -0
- ck/dataset/dataset_compute.py +140 -0
- ck/dataset/dataset_from_crosstable.py +64 -0
- ck/dataset/dataset_from_csv.py +151 -0
- ck/dataset/sampled_dataset.py +96 -0
- ck/example/diamond_square.py +3 -1
- ck/example/triangle_square.py +3 -1
- ck/example/truss.py +3 -1
- ck/in_out/parse_net.py +21 -19
- ck/in_out/parser_utils.py +7 -3
- ck/learning/__init__.py +0 -0
- ck/learning/coalesce_cross_tables.py +403 -0
- ck/learning/model_from_cross_tables.py +296 -0
- ck/learning/parameters.py +117 -0
- ck/learning/train_generative_bn.py +198 -0
- ck/pgm.py +105 -92
- ck/pgm_circuit/marginals_program.py +5 -0
- ck/pgm_circuit/mpe_program.py +3 -4
- ck/pgm_circuit/pgm_circuit.py +27 -18
- ck/pgm_circuit/program_with_slotmap.py +27 -46
- ck/pgm_circuit/support/compile_circuit.py +2 -4
- ck/pgm_circuit/wmc_program.py +5 -0
- ck/pgm_compiler/support/circuit_table/_circuit_table_cy.c +1 -1
- ck/pgm_compiler/support/circuit_table/_circuit_table_cy.cpython-313-darwin.so +0 -0
- ck/probability/cross_table_probability_space.py +53 -0
- ck/probability/divergence.py +226 -0
- ck/probability/empirical_probability_space.py +1 -0
- ck/probability/probability_space.py +53 -30
- ck/program/raw_program.py +23 -16
- ck/sampling/sampler_support.py +5 -6
- ck/utils/iter_extras.py +3 -2
- ck/utils/local_config.py +16 -8
- ck_demos/dataset/__init__.py +0 -0
- ck_demos/dataset/demo_dataset_builder.py +37 -0
- ck_demos/dataset/demo_dataset_from_sampler.py +18 -0
- ck_demos/learning/__init__.py +0 -0
- ck_demos/learning/demo_bayesian_network_from_cross_tables.py +70 -0
- ck_demos/learning/demo_simple_learning.py +55 -0
- ck_demos/sampling/demo_wmc_direct_sampler.py +2 -2
- {compiled_knowledge-4.0.0a24.dist-info → compiled_knowledge-4.1.0.dist-info}/METADATA +2 -1
- {compiled_knowledge-4.0.0a24.dist-info → compiled_knowledge-4.1.0.dist-info}/RECORD +58 -37
- {compiled_knowledge-4.0.0a24.dist-info → compiled_knowledge-4.1.0.dist-info}/WHEEL +0 -0
- {compiled_knowledge-4.0.0a24.dist-info → compiled_knowledge-4.1.0.dist-info}/licenses/LICENSE.txt +0 -0
- {compiled_knowledge-4.0.0a24.dist-info → compiled_knowledge-4.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Sequence, Tuple, Dict
|
|
2
|
+
|
|
3
|
+
from ck.dataset.cross_table import CrossTable, Instance
|
|
4
|
+
from ck.pgm import RandomVariable, Indicator
|
|
5
|
+
from ck.probability.probability_space import ProbabilitySpace, Condition, check_condition
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CrossTableProbabilitySpace(ProbabilitySpace):
|
|
9
|
+
def __init__(self, cross_table: CrossTable):
|
|
10
|
+
"""
|
|
11
|
+
Enable probabilistic queries over a sample from a sample space.
|
|
12
|
+
Note that this is not necessarily an efficient approach to calculating probabilities and statistics.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
cross_table: a CrossTable to adapt to a ProbabilitySpace.
|
|
16
|
+
"""
|
|
17
|
+
self._cross_table: CrossTable = cross_table
|
|
18
|
+
self._rv_idx_to_sample_idx: Dict[int, int] = {
|
|
19
|
+
rv.idx: i
|
|
20
|
+
for i, rv in enumerate(cross_table.rvs)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def rvs(self) -> Sequence[RandomVariable]:
|
|
25
|
+
return self._cross_table.rvs
|
|
26
|
+
|
|
27
|
+
def wmc(self, *condition: Condition) -> float:
|
|
28
|
+
condition: Tuple[Indicator, ...] = check_condition(condition)
|
|
29
|
+
rvs: Sequence[RandomVariable] = self._cross_table.rvs
|
|
30
|
+
|
|
31
|
+
checks = [set() for _ in rvs]
|
|
32
|
+
for ind in condition:
|
|
33
|
+
checks[self._rv_idx_to_sample_idx[ind.rv_idx]].add(ind.state_idx)
|
|
34
|
+
for i in range(len(checks)):
|
|
35
|
+
if len(checks[i]) > 0:
|
|
36
|
+
checks[i] = set(range(len(rvs[i]))).difference(checks[i])
|
|
37
|
+
|
|
38
|
+
def satisfied(item: Tuple[Instance, float]) -> float:
|
|
39
|
+
"""
|
|
40
|
+
Return the weight of the instance, if the instance satisfies
|
|
41
|
+
the condition, else return 0.
|
|
42
|
+
"""
|
|
43
|
+
instance, weight = item
|
|
44
|
+
if any((state in check) for state, check in zip(instance, checks)):
|
|
45
|
+
return 0
|
|
46
|
+
else:
|
|
47
|
+
return weight
|
|
48
|
+
|
|
49
|
+
return sum(map(satisfied, self._cross_table.items()))
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def z(self) -> float:
|
|
53
|
+
return self._cross_table.total_weight()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module implements several divergences which measure the difference
|
|
3
|
+
between two distributions.
|
|
4
|
+
"""
|
|
5
|
+
import math
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ck.pgm import RandomVariable, rv_instances_as_indicators, PGM
|
|
11
|
+
from ck.probability.probability_space import ProbabilitySpace
|
|
12
|
+
|
|
13
|
+
_NAN: float = np.nan # Not-a-number (i.e., the result of an invalid calculation).
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def kl(p: ProbabilitySpace, q: ProbabilitySpace) -> float:
|
|
17
|
+
"""
|
|
18
|
+
Compute the Kullback-Leibler divergence between p & q,
|
|
19
|
+
where p is the true distribution.
|
|
20
|
+
|
|
21
|
+
This implementation uses logarithms, base 2.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
p: a probability space to compare to.
|
|
25
|
+
q: the other probability space.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
the Kullback–Leibler (KL) divergence of p & q, where p is
|
|
29
|
+
the true distribution.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: if `p` and `q` do not have compatible random variables.specifically:
|
|
33
|
+
* `len(self.rvs) == len(other.rvs)`
|
|
34
|
+
* `len(other.rvs[i]) == len(self.rvs[i])` for all `i`
|
|
35
|
+
* `other.rvs[i].idx == self.rvs[i].idx` for all `i`.
|
|
36
|
+
|
|
37
|
+
Warning:
|
|
38
|
+
this method will enumerate the whole probability space.
|
|
39
|
+
"""
|
|
40
|
+
if not _compatible_rvs(p.rvs, q.rvs):
|
|
41
|
+
raise ValueError('incompatible random variables')
|
|
42
|
+
|
|
43
|
+
total = 0.0
|
|
44
|
+
for x in rv_instances_as_indicators(*p.rvs):
|
|
45
|
+
p_x = p.probability(*x)
|
|
46
|
+
q_x = q.probability(*x)
|
|
47
|
+
if p_x <= 0 or q_x <= 0:
|
|
48
|
+
return _NAN
|
|
49
|
+
total += p_x * math.log2(p_x / q_x)
|
|
50
|
+
return total
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pseudo_kl(p: ProbabilitySpace, q: ProbabilitySpace) -> float:
|
|
54
|
+
"""
|
|
55
|
+
A kind of KL divergence, factored by the structure of `p`.
|
|
56
|
+
This is an experimental measure.
|
|
57
|
+
|
|
58
|
+
This implementation uses logarithms, base 2.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
p: a probability space to compare to.
|
|
62
|
+
q: the other probability space.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
the factored histogram intersection between the two probability spaces.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: if `p` and `q` do not have compatible random variables.specifically:
|
|
69
|
+
* `len(self.rvs) == len(other.rvs)`
|
|
70
|
+
* `len(other.rvs[i]) == len(self.rvs[i])` for all `i`
|
|
71
|
+
* `other.rvs[i].idx == self.rvs[i].idx` for all `i`.
|
|
72
|
+
ValueError: if not all random variable of `p` are from a single PGM, which must
|
|
73
|
+
have a Bayesian network structure.
|
|
74
|
+
"""
|
|
75
|
+
p_rvs: Sequence[RandomVariable] = p.rvs
|
|
76
|
+
q_rvs: Sequence[RandomVariable] = q.rvs
|
|
77
|
+
|
|
78
|
+
if not _compatible_rvs(p_rvs, q_rvs):
|
|
79
|
+
raise ValueError('incompatible random variables')
|
|
80
|
+
|
|
81
|
+
if len(p_rvs) == 0:
|
|
82
|
+
return _NAN
|
|
83
|
+
|
|
84
|
+
pgm: PGM = p_rvs[0].pgm
|
|
85
|
+
if any(rv.pgm is not pgm for rv in p_rvs):
|
|
86
|
+
raise ValueError('p random variables are not from a single PGM.')
|
|
87
|
+
if not pgm.is_structure_bayesian:
|
|
88
|
+
raise ValueError('p does not have Bayesian network structure.')
|
|
89
|
+
|
|
90
|
+
# Across the two spaces, corresponding random variables are equivalent;
|
|
91
|
+
# i.e., same number of states and same `idx` values. Therefore,
|
|
92
|
+
# indicators from either one space can be used in both spaces.
|
|
93
|
+
|
|
94
|
+
total: float = 0
|
|
95
|
+
for factor in pgm.factors:
|
|
96
|
+
for x in rv_instances_as_indicators(*factor.rvs): # every possible state of factor rvs
|
|
97
|
+
p_x = p.probability(*x)
|
|
98
|
+
q_x = q.probability(*x)
|
|
99
|
+
if p_x <= 0 or q_x <= 0:
|
|
100
|
+
return _NAN
|
|
101
|
+
total += p_x * math.log2(p_x / q_x)
|
|
102
|
+
return total
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def hi(p: ProbabilitySpace, q: ProbabilitySpace) -> float:
|
|
106
|
+
"""
|
|
107
|
+
Compute the histogram intersection between this probability spaces and the given other.
|
|
108
|
+
|
|
109
|
+
The histogram intersection between two probability spaces P and Q,
|
|
110
|
+
with state spaces X, is defined as:
|
|
111
|
+
HI(P, Q) = sum(min(P(x), Q(x)) for x in X)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
p: a probability space to compare to.
|
|
115
|
+
q: the other probability space.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
the histogram intersection between the two probability spaces.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
ValueError: if `p` and `q` do not have compatible random variables.specifically:
|
|
122
|
+
* `len(self.rvs) == len(other.rvs)`
|
|
123
|
+
* `len(other.rvs[i]) == len(self.rvs[i])` for all `i`
|
|
124
|
+
* `other.rvs[i].idx == self.rvs[i].idx` for all `i`.
|
|
125
|
+
|
|
126
|
+
Warning:
|
|
127
|
+
this method will enumerate the whole probability space.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
p_rvs: Sequence[RandomVariable] = p.rvs
|
|
131
|
+
q_rvs: Sequence[RandomVariable] = q.rvs
|
|
132
|
+
|
|
133
|
+
if not _compatible_rvs(p_rvs, q_rvs):
|
|
134
|
+
raise ValueError('incompatible random variables')
|
|
135
|
+
|
|
136
|
+
# Across the two spaces, corresponding random variables are equivalent;
|
|
137
|
+
# i.e., same number of states and same `idx` values. Therefore,
|
|
138
|
+
# indicators from either one space can be used in both spaces.
|
|
139
|
+
|
|
140
|
+
return sum(
|
|
141
|
+
min(p.probability(*x), q.probability(*x))
|
|
142
|
+
for x in rv_instances_as_indicators(*p_rvs)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def fhi(p: ProbabilitySpace, q: ProbabilitySpace) -> float:
|
|
147
|
+
"""
|
|
148
|
+
Compute the factored histogram intersection between this probability spaces and the given other.
|
|
149
|
+
|
|
150
|
+
The factored histogram intersection between two probability spaces P and Q,
|
|
151
|
+
with state spaces X and factorisation F, is defined as:
|
|
152
|
+
FHI(P, Q) = 1/n sum(P(Y=y) CHI(P, Q, X | Y=y)
|
|
153
|
+
where:
|
|
154
|
+
CHI(P, Q, X | Y=y) = HI(P(X | Y=y), Q(X | Y=y))
|
|
155
|
+
HI(P, Q) = sum(min(P(X=x), Q(X=x)) for x in f)
|
|
156
|
+
|
|
157
|
+
The value of _n_ is the sum ofP(Y=y) over all CPT rows. However,
|
|
158
|
+
this always equals the number of CPTs, i.e., the number of random
|
|
159
|
+
variables.
|
|
160
|
+
|
|
161
|
+
The factorisation F is taken from the `p`.
|
|
162
|
+
|
|
163
|
+
For more information about factored histogram intersection, see the publication:
|
|
164
|
+
Suresh, S., Drake, B. (2025). Sampling of Large Probabilistic Graphical Models
|
|
165
|
+
Using Arithmetic Circuits. AI 2024: Advances in Artificial Intelligence. AI 2024.
|
|
166
|
+
Lecture Notes in Computer Science, vol 15443. https://doi.org/10.1007/978-981-96-0351-0_13.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
p: a probability space to compare to.
|
|
170
|
+
q: the other probability space.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
the factored histogram intersection between the two probability spaces.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
ValueError: if `p` and `q` do not have compatible random variables.specifically:
|
|
177
|
+
* `len(self.rvs) == len(other.rvs)`
|
|
178
|
+
* `len(other.rvs[i]) == len(self.rvs[i])` for all `i`
|
|
179
|
+
* `other.rvs[i].idx == self.rvs[i].idx` for all `i`.
|
|
180
|
+
ValueError: if not all random variable of `p` are from a single PGM, which must
|
|
181
|
+
have a Bayesian network structure.
|
|
182
|
+
"""
|
|
183
|
+
p_rvs: Sequence[RandomVariable] = p.rvs
|
|
184
|
+
q_rvs: Sequence[RandomVariable] = q.rvs
|
|
185
|
+
|
|
186
|
+
if not _compatible_rvs(p_rvs, q_rvs):
|
|
187
|
+
raise ValueError('incompatible random variables')
|
|
188
|
+
|
|
189
|
+
if len(p_rvs) == 0:
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
pgm: PGM = p_rvs[0].pgm
|
|
193
|
+
if any(rv.pgm is not pgm for rv in p_rvs):
|
|
194
|
+
raise ValueError('p random variables are not from a single PGM.')
|
|
195
|
+
if not pgm.is_structure_bayesian:
|
|
196
|
+
raise ValueError('p does not have Bayesian network structure.')
|
|
197
|
+
|
|
198
|
+
# Across the two spaces, corresponding random variables are equivalent;
|
|
199
|
+
# i.e., same number of states and same `idx` values. Therefore,
|
|
200
|
+
# indicators from either one space can be used in both spaces.
|
|
201
|
+
|
|
202
|
+
# Loop over all CPTs, accumulating the total
|
|
203
|
+
total: float = 0
|
|
204
|
+
for factor in pgm.factors:
|
|
205
|
+
child: RandomVariable = factor.rvs[0]
|
|
206
|
+
parents: Sequence[RandomVariable] = factor.rvs[1:]
|
|
207
|
+
# Loop over all rows of the CPT
|
|
208
|
+
for parent_indicators in rv_instances_as_indicators(*parents):
|
|
209
|
+
p_marginal = p.marginal_distribution(child, condition=parent_indicators)
|
|
210
|
+
q_marginal = q.marginal_distribution(child, condition=parent_indicators)
|
|
211
|
+
row_hi = np.minimum(p_marginal, q_marginal).sum().item()
|
|
212
|
+
pr_row = p.probability(*parent_indicators)
|
|
213
|
+
total += pr_row * row_hi
|
|
214
|
+
|
|
215
|
+
return total / len(p_rvs)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _compatible_rvs(rvs1: Sequence[RandomVariable], rvs2: Sequence[RandomVariable]) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
The rvs are compatible if they have the same number of random variables
|
|
221
|
+
and the corresponding indicators are equal.
|
|
222
|
+
"""
|
|
223
|
+
return (
|
|
224
|
+
len(rvs1) == len(rvs2)
|
|
225
|
+
and all(len(rv1) == len(rv2) and rv1.idx == rv2.idx for rv1, rv2 in zip(rvs1, rvs2))
|
|
226
|
+
)
|
|
@@ -11,6 +11,7 @@ class EmpiricalProbabilitySpace(ProbabilitySpace):
|
|
|
11
11
|
Note that this is not necessarily an efficient approach to calculating probabilities and statistics.
|
|
12
12
|
|
|
13
13
|
This probability space treats each of the samples as equally weighted.
|
|
14
|
+
For a probability space over unequally weighted samples, consider using `CrossTableProbabilitySpace`.
|
|
14
15
|
|
|
15
16
|
Assumes:
|
|
16
17
|
len(sample) == len(rvs), for each sample in samples.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import math
|
|
2
4
|
from abc import ABC, abstractmethod
|
|
3
5
|
from itertools import chain
|
|
@@ -11,17 +13,16 @@ from ck.utils.map_set import MapSet
|
|
|
11
13
|
from ck.utils.np_extras import dtype_for_number_of_states, NDArrayFloat64, DTypeStates, NDArrayNumeric
|
|
12
14
|
|
|
13
15
|
Condition: TypeAlias = None | Indicator | Iterable[Indicator]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"""
|
|
16
|
+
"""
|
|
17
|
+
Type defining a condition. A condition is logically a set of
|
|
18
|
+
indicators, each indicator representing a random variable being in some state.
|
|
19
|
+
|
|
20
|
+
If multiple indicators of the same random variable appear in
|
|
21
|
+
a condition, then they are interpreted as
|
|
22
|
+
a disjunction, otherwise indicators are interpreted as
|
|
23
|
+
a conjunction. E.g., the condition (X=0, Y=1, Y=3) means
|
|
24
|
+
X=0 and (Y=1 or Y=3).
|
|
25
|
+
"""
|
|
25
26
|
|
|
26
27
|
_NAN: float = np.nan # Not-a-number (i.e., the result of an invalid calculation).
|
|
27
28
|
|
|
@@ -204,16 +205,19 @@ class ProbabilitySpace(ABC):
|
|
|
204
205
|
loop_rvs.append([rv[i] for i in sorted(states)])
|
|
205
206
|
reduced_space = True
|
|
206
207
|
|
|
208
|
+
best_probability = float('-inf')
|
|
209
|
+
best_states = None
|
|
210
|
+
|
|
207
211
|
# If the random variables we are looping over does not have any conditions
|
|
208
212
|
# then it is expected to be faster by using computed marginal probabilities.
|
|
209
213
|
if not reduced_space:
|
|
210
214
|
prs = self.marginal_distribution(*rvs, condition=condition)
|
|
211
|
-
best_probability = float('-inf')
|
|
212
|
-
best_states = None
|
|
213
215
|
for probability, inst in zip(prs, rv_instances(*rvs)):
|
|
214
216
|
if probability > best_probability:
|
|
215
217
|
best_probability = probability
|
|
216
218
|
best_states = inst
|
|
219
|
+
if best_states is None:
|
|
220
|
+
return _NAN, ()
|
|
217
221
|
return best_probability, best_states
|
|
218
222
|
|
|
219
223
|
else:
|
|
@@ -221,8 +225,6 @@ class ProbabilitySpace(ABC):
|
|
|
221
225
|
new_conditions = tuple(ind for ind in condition if ind.rv_idx not in rv_indexes)
|
|
222
226
|
|
|
223
227
|
# Loop over the state space of the 'loop' rvs
|
|
224
|
-
best_probability = float('-inf')
|
|
225
|
-
best_states = None
|
|
226
228
|
indicators: Tuple[Indicator, ...]
|
|
227
229
|
for indicators in _combos(loop_rvs):
|
|
228
230
|
probability = self.wmc(*(indicators + new_conditions))
|
|
@@ -230,6 +232,8 @@ class ProbabilitySpace(ABC):
|
|
|
230
232
|
best_probability = probability
|
|
231
233
|
best_states = tuple(ind.state_idx for ind in indicators)
|
|
232
234
|
condition_probability = self.wmc(*condition)
|
|
235
|
+
if best_states is None:
|
|
236
|
+
return _NAN, ()
|
|
233
237
|
return best_probability / condition_probability, best_states
|
|
234
238
|
|
|
235
239
|
def correlation(self, indicator1: Indicator, indicator2: Indicator, condition: Condition = ()) -> float:
|
|
@@ -246,6 +250,20 @@ class ProbabilitySpace(ABC):
|
|
|
246
250
|
"""
|
|
247
251
|
condition = check_condition(condition)
|
|
248
252
|
|
|
253
|
+
if indicator1.rv_idx == indicator2.rv_idx:
|
|
254
|
+
# Special case - same random variable
|
|
255
|
+
condition_groups: MapSet[int, Indicator] = _group_indicators(condition)
|
|
256
|
+
rv_idx: int = indicator1.rv_idx
|
|
257
|
+
if indicator1 not in condition_groups.get(rv_idx, (indicator1,)):
|
|
258
|
+
return _NAN
|
|
259
|
+
if indicator1 == indicator2:
|
|
260
|
+
return 1
|
|
261
|
+
else:
|
|
262
|
+
if indicator2 not in condition_groups.get(rv_idx, (indicator2,)):
|
|
263
|
+
return _NAN
|
|
264
|
+
else:
|
|
265
|
+
return 0
|
|
266
|
+
|
|
249
267
|
p1 = self.probability(indicator1, condition=condition)
|
|
250
268
|
p2 = self.probability(indicator2, condition=condition)
|
|
251
269
|
p12 = self._joint_probability(indicator1, indicator2, condition=condition)
|
|
@@ -268,12 +286,7 @@ class ProbabilitySpace(ABC):
|
|
|
268
286
|
entropy of the given random variable.
|
|
269
287
|
"""
|
|
270
288
|
condition = check_condition(condition)
|
|
271
|
-
|
|
272
|
-
for ind in rv:
|
|
273
|
-
p = self.probability(ind, condition=condition)
|
|
274
|
-
if p > 0.0:
|
|
275
|
-
e -= p * math.log2(p)
|
|
276
|
-
return e
|
|
289
|
+
return -sum(plogp(self.probability(ind, condition=condition)) for ind in rv)
|
|
277
290
|
|
|
278
291
|
def conditional_entropy(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
|
|
279
292
|
"""
|
|
@@ -310,13 +323,11 @@ class ProbabilitySpace(ABC):
|
|
|
310
323
|
joint entropy of the given random variables.
|
|
311
324
|
"""
|
|
312
325
|
condition = check_condition(condition)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
for
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
e -= p * math.log2(p)
|
|
319
|
-
return e
|
|
326
|
+
return -sum(
|
|
327
|
+
plogp(self._joint_probability(ind1, ind2, condition=condition))
|
|
328
|
+
for ind1 in rv1
|
|
329
|
+
for ind2 in rv2
|
|
330
|
+
)
|
|
320
331
|
|
|
321
332
|
def mutual_information(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
|
|
322
333
|
"""
|
|
@@ -420,8 +431,12 @@ class ProbabilitySpace(ABC):
|
|
|
420
431
|
denominator = self.joint_entropy(rv1, rv2, condition=condition)
|
|
421
432
|
return self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
|
|
422
433
|
|
|
423
|
-
def covariant_normalised_mutual_information(
|
|
424
|
-
|
|
434
|
+
def covariant_normalised_mutual_information(
|
|
435
|
+
self,
|
|
436
|
+
rv1: RandomVariable,
|
|
437
|
+
rv2: RandomVariable,
|
|
438
|
+
condition: Condition = (),
|
|
439
|
+
) -> float:
|
|
425
440
|
"""
|
|
426
441
|
Calculate the covariant normalised mutual information
|
|
427
442
|
= I(rv1; rv2) / sqrt(H(rv1) * H(rv2)).
|
|
@@ -550,6 +565,14 @@ class ProbabilitySpace(ABC):
|
|
|
550
565
|
return wmc
|
|
551
566
|
|
|
552
567
|
|
|
568
|
+
def plogp(p: float) -> float:
|
|
569
|
+
"""
|
|
570
|
+
Returns:
|
|
571
|
+
p * log2(p)
|
|
572
|
+
"""
|
|
573
|
+
return p * math.log2(p) if p > 0 else 0
|
|
574
|
+
|
|
575
|
+
|
|
553
576
|
def check_condition(condition: Condition) -> Tuple[Indicator, ...]:
|
|
554
577
|
"""
|
|
555
578
|
Make the best effort to interpret the given condition.
|
ck/program/raw_program.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Callable, Sequence
|
|
2
|
+
from typing import Callable, Sequence, TypeAlias
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import ctypes as ct
|
|
@@ -7,12 +7,14 @@ import ctypes as ct
|
|
|
7
7
|
|
|
8
8
|
from ck.utils.np_extras import NDArrayNumeric, DTypeNumeric
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
RawProgramFunction: TypeAlias = Callable[[ct.POINTER, ct.POINTER, ct.POINTER], None]
|
|
11
|
+
"""
|
|
12
|
+
RawProgramFunction is a function of three ctypes arrays, returning nothing.
|
|
13
|
+
Args:
|
|
14
|
+
[0]: input values,
|
|
15
|
+
[1]: temporary working memory,
|
|
16
|
+
[2]: output values.
|
|
17
|
+
"""
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
@dataclass
|
|
@@ -26,23 +28,28 @@ class RawProgram:
|
|
|
26
28
|
an efficient method for executing a program as buffers are reallocated for
|
|
27
29
|
each call. Alternatively, a `RawProgram` can be wrapped in a `ProgramBuffer`
|
|
28
30
|
for computationally efficient memory buffer reuse.
|
|
29
|
-
|
|
30
|
-
Fields:
|
|
31
|
-
function: is a function of three ctypes arrays, returning nothing.
|
|
32
|
-
dtype: the numpy data type of the array values.
|
|
33
|
-
number_of_vars: the number of input values (first function argument).
|
|
34
|
-
number_of_tmps: the number of working memory values (second function argument).
|
|
35
|
-
number_of_results: the number of result values (third function argument).
|
|
36
|
-
var_indices: maps the index of inputs (from 0 to self.number_of_vars - 1) to the index
|
|
37
|
-
of the corresponding circuit var.
|
|
38
31
|
"""
|
|
39
32
|
|
|
40
33
|
function: RawProgramFunction
|
|
34
|
+
"""a function of three ctypes arrays, returning nothing."""
|
|
35
|
+
|
|
41
36
|
dtype: DTypeNumeric
|
|
37
|
+
"""the numpy data type of the array values."""
|
|
38
|
+
|
|
42
39
|
number_of_vars: int
|
|
40
|
+
"""the number of input values (first function argument)."""
|
|
41
|
+
|
|
43
42
|
number_of_tmps: int
|
|
43
|
+
"""the number of working memory values (second function argument)."""
|
|
44
|
+
|
|
44
45
|
number_of_results: int
|
|
46
|
+
"""the number of result values (third function argument)."""
|
|
47
|
+
|
|
45
48
|
var_indices: Sequence[int]
|
|
49
|
+
"""
|
|
50
|
+
a map from the index of inputs (from 0 to self.number_of_vars - 1) to the index
|
|
51
|
+
of the corresponding circuit var.
|
|
52
|
+
"""
|
|
46
53
|
|
|
47
54
|
def __call__(self, var_values: NDArrayNumeric | Sequence[int | float] | int | float) -> NDArrayNumeric:
|
|
48
55
|
"""
|
ck/sampling/sampler_support.py
CHANGED
|
@@ -11,12 +11,11 @@ from ck.utils.np_extras import NDArrayStates, NDArrayNumeric
|
|
|
11
11
|
from ck.utils.random_extras import Random
|
|
12
12
|
|
|
13
13
|
YieldF: TypeAlias = Callable[[NDArrayStates], int] | Callable[[NDArrayStates], Instance]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"""
|
|
14
|
+
"""
|
|
15
|
+
Type of a yield function. Support for a sampler.
|
|
16
|
+
A yield function may be used to implement a sampler's iterator, thus
|
|
17
|
+
it provides an Instance or single state index.
|
|
18
|
+
"""
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
@dataclass
|
ck/utils/iter_extras.py
CHANGED
|
@@ -33,11 +33,12 @@ def combos(list_of_lists: Sequence[Sequence[_T]], flip=False) -> Iterable[Tuple[
|
|
|
33
33
|
Iterate over all combinations of taking one element from each of the lists.
|
|
34
34
|
|
|
35
35
|
The order of results has the first element changing most rapidly.
|
|
36
|
-
For example, given [[1,2,3],[4,5],[6,7]], combos yields the following
|
|
36
|
+
For example, given [[1,2,3],[4,5],[6,7]], combos yields the following::
|
|
37
|
+
|
|
37
38
|
(1,4,6), (2,4,6), (3,4,6), (1,5,6), (2,5,6), (3,5,6),
|
|
38
39
|
(1,4,7), (2,4,7), (3,4,7), (1,5,7), (2,5,7), (3,5,7).
|
|
39
40
|
|
|
40
|
-
If flip, then the last changes most rapidly.
|
|
41
|
+
If `flip` is true, then the last changes most rapidly.
|
|
41
42
|
"""
|
|
42
43
|
num = len(list_of_lists)
|
|
43
44
|
if num == 0:
|
ck/utils/local_config.py
CHANGED
|
@@ -12,10 +12,13 @@ other getter methods wrap `get`.
|
|
|
12
12
|
|
|
13
13
|
The `get` method will search for a value for a requested variable
|
|
14
14
|
using the following steps.
|
|
15
|
+
|
|
15
16
|
1) Check the `programmatic config` which is a dictionary that
|
|
16
|
-
|
|
17
|
+
can be directly updated.
|
|
18
|
+
|
|
17
19
|
2) Check the PYTHONPATH for a module called `config` (i.e., a
|
|
18
|
-
|
|
20
|
+
`config.py` file) for global variables defined in that module.
|
|
21
|
+
|
|
19
22
|
3) Check the system environment variables (`os.environ`).
|
|
20
23
|
|
|
21
24
|
Variable names must be a valid Python identifier. Only valid
|
|
@@ -171,8 +174,9 @@ def get_params(
|
|
|
171
174
|
are returned as a single string with `delim` as the delimiter. If
|
|
172
175
|
`delim` is not None then the default value for `sep` is '='.
|
|
173
176
|
|
|
174
|
-
For example, assume config.py contains: ABC = 123 and DEF = 456
|
|
175
|
-
then
|
|
177
|
+
For example, assume `config.py` contains: `ABC = 123` and `DEF = 456`,
|
|
178
|
+
then::
|
|
179
|
+
|
|
176
180
|
get_params('ABC') -> ('ABC', 123)
|
|
177
181
|
get_params('ABC', 'DEF') -> ('ABC', 123), ('DEF', 456)
|
|
178
182
|
get_params('ABC', sep='=') = 'ABC=123'
|
|
@@ -180,10 +184,14 @@ def get_params(
|
|
|
180
184
|
get_params('ABC;DEF', delim=';') = 'ABC=123;DEF=456'
|
|
181
185
|
get_params('ABC;DEF', sep='==', delim=';') = 'ABC==123;DEF==456'
|
|
182
186
|
|
|
183
|
-
:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
Args:
|
|
188
|
+
keys: the names of variables to access.
|
|
189
|
+
sep: the separator character between {variable} and {value}.
|
|
190
|
+
delim: the delimiter character between key-value pairs.
|
|
191
|
+
config: a Config instance to update. Default is the global config.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
the requested parameter values.
|
|
187
195
|
"""
|
|
188
196
|
if delim is not None:
|
|
189
197
|
keys = flatten(key.split(delim) for key in keys)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ck.dataset import HardDataset, SoftDataset
|
|
2
|
+
from ck.dataset.dataset_builder import DatasetBuilder, soft_dataset_from_builder, hard_dataset_from_builder
|
|
3
|
+
from ck.pgm import PGM
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
pgm = PGM()
|
|
8
|
+
x = pgm.new_rv('x', (True, False))
|
|
9
|
+
y = pgm.new_rv('y', ('yes', 'no', 'maybe'))
|
|
10
|
+
|
|
11
|
+
builder = DatasetBuilder([x, y])
|
|
12
|
+
builder.append()
|
|
13
|
+
builder.append(1, 2).weight = 3
|
|
14
|
+
builder.append(None, [0.7, 0.1, 0.2])
|
|
15
|
+
builder.append().set_states(True, 'maybe')
|
|
16
|
+
|
|
17
|
+
print('DatasetBuilder dump')
|
|
18
|
+
builder.dump()
|
|
19
|
+
print()
|
|
20
|
+
|
|
21
|
+
print('DatasetBuilder dump, showing states and custom missing values')
|
|
22
|
+
builder.dump(as_states=True, missing='?')
|
|
23
|
+
print()
|
|
24
|
+
|
|
25
|
+
print('HardDataset dump')
|
|
26
|
+
dataset: HardDataset = hard_dataset_from_builder(builder, missing=99)
|
|
27
|
+
dataset.dump()
|
|
28
|
+
print()
|
|
29
|
+
|
|
30
|
+
print('SoftDataset dump')
|
|
31
|
+
dataset: SoftDataset = soft_dataset_from_builder(builder)
|
|
32
|
+
dataset.dump()
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ck import example
|
|
2
|
+
from ck.dataset.sampled_dataset import dataset_from_sampler
|
|
3
|
+
from ck.pgm import PGM
|
|
4
|
+
from ck.pgm_circuit.wmc_program import WMCProgram
|
|
5
|
+
from ck.pgm_compiler import DEFAULT_PGM_COMPILER
|
|
6
|
+
from ck.sampling.sampler import Sampler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
pgm: PGM = example.Student()
|
|
11
|
+
sampler: Sampler = WMCProgram(DEFAULT_PGM_COMPILER(pgm)).sample_direct()
|
|
12
|
+
dataset = dataset_from_sampler(sampler, 10)
|
|
13
|
+
|
|
14
|
+
dataset.dump()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == '__main__':
|
|
18
|
+
main()
|
|
File without changes
|