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.

Files changed (167) hide show
  1. ck/__init__.py +0 -0
  2. ck/circuit/__init__.py +13 -0
  3. ck/circuit/circuit.c +38749 -0
  4. ck/circuit/circuit.cpython-313-darwin.so +0 -0
  5. ck/circuit/circuit_py.py +807 -0
  6. ck/circuit/tmp_const.py +74 -0
  7. ck/circuit_compiler/__init__.py +2 -0
  8. ck/circuit_compiler/circuit_compiler.py +26 -0
  9. ck/circuit_compiler/cython_vm_compiler/__init__.py +1 -0
  10. ck/circuit_compiler/cython_vm_compiler/_compiler.c +17373 -0
  11. ck/circuit_compiler/cython_vm_compiler/_compiler.cpython-313-darwin.so +0 -0
  12. ck/circuit_compiler/cython_vm_compiler/cython_vm_compiler.py +96 -0
  13. ck/circuit_compiler/interpret_compiler.py +223 -0
  14. ck/circuit_compiler/llvm_compiler.py +388 -0
  15. ck/circuit_compiler/llvm_vm_compiler.py +546 -0
  16. ck/circuit_compiler/named_circuit_compilers.py +57 -0
  17. ck/circuit_compiler/support/__init__.py +0 -0
  18. ck/circuit_compiler/support/circuit_analyser.py +81 -0
  19. ck/circuit_compiler/support/input_vars.py +148 -0
  20. ck/circuit_compiler/support/llvm_ir_function.py +234 -0
  21. ck/example/__init__.py +53 -0
  22. ck/example/alarm.py +366 -0
  23. ck/example/asia.py +28 -0
  24. ck/example/binary_clique.py +32 -0
  25. ck/example/bow_tie.py +33 -0
  26. ck/example/cancer.py +37 -0
  27. ck/example/chain.py +38 -0
  28. ck/example/child.py +199 -0
  29. ck/example/clique.py +33 -0
  30. ck/example/cnf_pgm.py +39 -0
  31. ck/example/diamond_square.py +68 -0
  32. ck/example/earthquake.py +36 -0
  33. ck/example/empty.py +10 -0
  34. ck/example/hailfinder.py +539 -0
  35. ck/example/hepar2.py +628 -0
  36. ck/example/insurance.py +504 -0
  37. ck/example/loop.py +40 -0
  38. ck/example/mildew.py +38161 -0
  39. ck/example/munin.py +22982 -0
  40. ck/example/pathfinder.py +53674 -0
  41. ck/example/rain.py +39 -0
  42. ck/example/rectangle.py +161 -0
  43. ck/example/run.py +30 -0
  44. ck/example/sachs.py +129 -0
  45. ck/example/sprinkler.py +30 -0
  46. ck/example/star.py +44 -0
  47. ck/example/stress.py +64 -0
  48. ck/example/student.py +43 -0
  49. ck/example/survey.py +46 -0
  50. ck/example/triangle_square.py +54 -0
  51. ck/example/truss.py +49 -0
  52. ck/in_out/__init__.py +3 -0
  53. ck/in_out/parse_ace_lmap.py +216 -0
  54. ck/in_out/parse_ace_nnf.py +288 -0
  55. ck/in_out/parse_net.py +480 -0
  56. ck/in_out/parser_utils.py +185 -0
  57. ck/in_out/pgm_pickle.py +42 -0
  58. ck/in_out/pgm_python.py +268 -0
  59. ck/in_out/render_bugs.py +111 -0
  60. ck/in_out/render_net.py +177 -0
  61. ck/in_out/render_pomegranate.py +184 -0
  62. ck/pgm.py +3494 -0
  63. ck/pgm_circuit/__init__.py +1 -0
  64. ck/pgm_circuit/marginals_program.py +352 -0
  65. ck/pgm_circuit/mpe_program.py +237 -0
  66. ck/pgm_circuit/pgm_circuit.py +75 -0
  67. ck/pgm_circuit/program_with_slotmap.py +234 -0
  68. ck/pgm_circuit/slot_map.py +35 -0
  69. ck/pgm_circuit/support/__init__.py +0 -0
  70. ck/pgm_circuit/support/compile_circuit.py +83 -0
  71. ck/pgm_circuit/target_marginals_program.py +103 -0
  72. ck/pgm_circuit/wmc_program.py +323 -0
  73. ck/pgm_compiler/__init__.py +2 -0
  74. ck/pgm_compiler/ace/__init__.py +1 -0
  75. ck/pgm_compiler/ace/ace.py +252 -0
  76. ck/pgm_compiler/factor_elimination.py +383 -0
  77. ck/pgm_compiler/named_pgm_compilers.py +63 -0
  78. ck/pgm_compiler/pgm_compiler.py +19 -0
  79. ck/pgm_compiler/recursive_conditioning.py +226 -0
  80. ck/pgm_compiler/support/__init__.py +0 -0
  81. ck/pgm_compiler/support/circuit_table/__init__.py +9 -0
  82. ck/pgm_compiler/support/circuit_table/circuit_table.c +16042 -0
  83. ck/pgm_compiler/support/circuit_table/circuit_table.cpython-313-darwin.so +0 -0
  84. ck/pgm_compiler/support/circuit_table/circuit_table_py.py +269 -0
  85. ck/pgm_compiler/support/clusters.py +556 -0
  86. ck/pgm_compiler/support/factor_tables.py +398 -0
  87. ck/pgm_compiler/support/join_tree.py +275 -0
  88. ck/pgm_compiler/support/named_compiler_maker.py +33 -0
  89. ck/pgm_compiler/variable_elimination.py +89 -0
  90. ck/probability/__init__.py +0 -0
  91. ck/probability/empirical_probability_space.py +47 -0
  92. ck/probability/probability_space.py +568 -0
  93. ck/program/__init__.py +3 -0
  94. ck/program/program.py +129 -0
  95. ck/program/program_buffer.py +180 -0
  96. ck/program/raw_program.py +61 -0
  97. ck/sampling/__init__.py +0 -0
  98. ck/sampling/forward_sampler.py +211 -0
  99. ck/sampling/marginals_direct_sampler.py +113 -0
  100. ck/sampling/sampler.py +62 -0
  101. ck/sampling/sampler_support.py +232 -0
  102. ck/sampling/uniform_sampler.py +66 -0
  103. ck/sampling/wmc_direct_sampler.py +169 -0
  104. ck/sampling/wmc_gibbs_sampler.py +147 -0
  105. ck/sampling/wmc_metropolis_sampler.py +159 -0
  106. ck/sampling/wmc_rejection_sampler.py +113 -0
  107. ck/utils/__init__.py +0 -0
  108. ck/utils/iter_extras.py +153 -0
  109. ck/utils/map_list.py +128 -0
  110. ck/utils/map_set.py +128 -0
  111. ck/utils/np_extras.py +51 -0
  112. ck/utils/random_extras.py +64 -0
  113. ck/utils/tmp_dir.py +94 -0
  114. ck_demos/__init__.py +0 -0
  115. ck_demos/ace/__init__.py +0 -0
  116. ck_demos/ace/copy_ace_to_ck.py +15 -0
  117. ck_demos/ace/demo_ace.py +44 -0
  118. ck_demos/all_demos.py +88 -0
  119. ck_demos/circuit/__init__.py +0 -0
  120. ck_demos/circuit/demo_circuit_dump.py +22 -0
  121. ck_demos/circuit/demo_derivatives.py +43 -0
  122. ck_demos/circuit_compiler/__init__.py +0 -0
  123. ck_demos/circuit_compiler/compare_circuit_compilers.py +32 -0
  124. ck_demos/circuit_compiler/show_llvm_program.py +26 -0
  125. ck_demos/pgm/__init__.py +0 -0
  126. ck_demos/pgm/demo_pgm_dump.py +18 -0
  127. ck_demos/pgm/demo_pgm_dump_stress.py +18 -0
  128. ck_demos/pgm/demo_pgm_string_rendering.py +15 -0
  129. ck_demos/pgm/show_examples.py +25 -0
  130. ck_demos/pgm_compiler/__init__.py +0 -0
  131. ck_demos/pgm_compiler/compare_pgm_compilers.py +50 -0
  132. ck_demos/pgm_compiler/demo_compiler_dump.py +50 -0
  133. ck_demos/pgm_compiler/demo_factor_elimination.py +47 -0
  134. ck_demos/pgm_compiler/demo_join_tree.py +25 -0
  135. ck_demos/pgm_compiler/demo_marginals_program.py +53 -0
  136. ck_demos/pgm_compiler/demo_mpe_program.py +55 -0
  137. ck_demos/pgm_compiler/demo_pgm_compiler.py +38 -0
  138. ck_demos/pgm_compiler/demo_recursive_conditioning.py +33 -0
  139. ck_demos/pgm_compiler/demo_variable_elimination.py +33 -0
  140. ck_demos/pgm_compiler/demo_wmc_program.py +29 -0
  141. ck_demos/pgm_inference/__init__.py +0 -0
  142. ck_demos/pgm_inference/demo_inferencing_basic.py +188 -0
  143. ck_demos/pgm_inference/demo_inferencing_mpe_cancer.py +45 -0
  144. ck_demos/pgm_inference/demo_inferencing_wmc_and_mpe_sprinkler.py +154 -0
  145. ck_demos/pgm_inference/demo_inferencing_wmc_student.py +110 -0
  146. ck_demos/programs/__init__.py +0 -0
  147. ck_demos/programs/demo_program_buffer.py +24 -0
  148. ck_demos/programs/demo_program_multi.py +24 -0
  149. ck_demos/programs/demo_program_none.py +19 -0
  150. ck_demos/programs/demo_program_single.py +23 -0
  151. ck_demos/programs/demo_raw_program_interpreted.py +21 -0
  152. ck_demos/programs/demo_raw_program_llvm.py +21 -0
  153. ck_demos/sampling/__init__.py +0 -0
  154. ck_demos/sampling/check_sampler.py +71 -0
  155. ck_demos/sampling/demo_marginal_direct_sampler.py +40 -0
  156. ck_demos/sampling/demo_uniform_sampler.py +38 -0
  157. ck_demos/sampling/demo_wmc_direct_sampler.py +40 -0
  158. ck_demos/utils/__init__.py +0 -0
  159. ck_demos/utils/compare.py +88 -0
  160. ck_demos/utils/convert_network.py +45 -0
  161. ck_demos/utils/sample_model.py +216 -0
  162. ck_demos/utils/stop_watch.py +384 -0
  163. compiled_knowledge-4.0.0a5.dist-info/METADATA +50 -0
  164. compiled_knowledge-4.0.0a5.dist-info/RECORD +167 -0
  165. compiled_knowledge-4.0.0a5.dist-info/WHEEL +5 -0
  166. compiled_knowledge-4.0.0a5.dist-info/licenses/LICENSE.txt +21 -0
  167. compiled_knowledge-4.0.0a5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,568 @@
1
+ """
2
+ An abstract class for object providing probabilities.
3
+ """
4
+ import math
5
+ from abc import ABC, abstractmethod
6
+ from typing import Sequence, Tuple, Iterable, Callable
7
+
8
+ import numpy as np
9
+
10
+ from ck.pgm import Indicator, RandomVariable, rv_instances_as_indicators, number_of_states, rv_instances, Instance
11
+ from ck.utils.iter_extras import combos as _combos
12
+ from ck.utils.map_set import MapSet
13
+ from ck.utils.np_extras import dtype_for_number_of_states, NDArrayFloat64, DTypeStates, NDArrayNumeric
14
+
15
+ # Type defining a condition.
16
+ Condition = None | Indicator | Iterable[Indicator]
17
+
18
+
19
+ class ProbabilitySpace(ABC):
20
+ """
21
+ An abstract mixin class for a class providing probabilities over a state space defined by random variables.
22
+ Each possible world of the state space is referred to as an 'instance'.
23
+ """
24
+ __slots__ = ()
25
+
26
+ @property
27
+ @abstractmethod
28
+ def rvs(self) -> Sequence[RandomVariable]:
29
+ """
30
+ Return the random variables that define the state space.
31
+ Each random variable, rv, has a length len(rv) which
32
+ is the number of states, and rv[i] is the 'indicator' for
33
+ the ith state of the random variable. Indicators must
34
+ be unique across all rvs as rv[i] indicates the
35
+ condition 'rv == i'.
36
+ """
37
+
38
+ @abstractmethod
39
+ def wmc(self, *condition: Condition) -> float:
40
+ """
41
+ Return the weight of instances matching the given condition.
42
+
43
+ If multiple indicators of the same random variable appear in
44
+ the parameter 'indicators' then they are interpreted as
45
+ a disjunction, otherwise indicators are interpreted as
46
+ a conjunction. E.g.: X=0, Y=1, Y=3 means X=0 and (Y=1 or Y=3)
47
+
48
+ Args:
49
+ condition: zero or more indicators that specify a condition.
50
+ """
51
+
52
+ @property
53
+ @abstractmethod
54
+ def z(self) -> float:
55
+ """
56
+ Return the summed weight of all instances.
57
+ This is equivalent to self.wmc(), with no arguments.
58
+ """
59
+
60
+ def probability(self, *indicators: Indicator, condition: Condition = ()) -> float:
61
+ """
62
+ Return the joint probability of the given indicators,
63
+ conditioned on any conditions, and
64
+ marginalised over any unmentioned random variables.
65
+
66
+ If multiple indicators of the same random variable appear in
67
+ the parameters 'indicators' or 'condition' then they are interpreted as
68
+ a disjunction, otherwise indicators are interpreted as
69
+ a conjunction. E.g.: X=0, Y=1, Y=3 means X=0 and (Y=1 or Y=3).
70
+
71
+ Args:
72
+ indicators: Indicators that specify which set of instances to compute probability.
73
+ condition: Indicators that specify conditions for a conditional probability.
74
+ Returns:
75
+ the probability of the given indicators, conditioned on the given conditions.
76
+ """
77
+ condition: Tuple[Indicator, ...] = check_condition(condition)
78
+ if len(condition) == 0:
79
+ z = self.z
80
+ else:
81
+ z = self.wmc(*condition)
82
+ indicators += condition # append the condition
83
+ if z > 0:
84
+ return self.wmc(*indicators) / z
85
+ else:
86
+ return np.nan
87
+
88
+ def marginal_distribution(self, *rvs: RandomVariable, condition: Condition = ()) -> NDArrayNumeric:
89
+ """
90
+ What is the marginal probability distribution over the states of the given random variables.
91
+ Assumes that no indicators of rv in rvs appear in the conditions (if supplied).
92
+
93
+ When multiple rvs are supplied, the order of instantiations is as per
94
+ `rv_instances_as_indicators(*rvs)`.
95
+
96
+ If multiple indicators of the same random variable appear in
97
+ the parameter 'condition' then they are interpreted as
98
+ a disjunction, otherwise indicators are interpreted as
99
+ a conjunction. E.g.: X=0, Y=1, Y=3 means X=0 and (Y=1 or Y=3).
100
+
101
+ This is not an efficient implementation as it will call self.probability(...)
102
+ for each possible state of the given random variable. If efficient marginal
103
+ probability calculations are required, consider using a different method.
104
+
105
+ Warning:
106
+ If the probability of each state of rv (given the condition) is
107
+ zero, then the marginal distribution is il-defined and the returned probabilities will
108
+ all be NAN.
109
+
110
+ Args:
111
+ rvs: Random variables to compute the marginal distribution over.
112
+ condition: Indicators that specify conditions for conditional probability.
113
+
114
+ Returns:
115
+ marginal probability distribution as an array co-indexed with `rv_instances_as_indicators(*rvs)`.
116
+ """
117
+ condition = check_condition(condition)
118
+
119
+ # We have to be careful of the situation where indicators of rvs appear in condition.
120
+ # If an RV has at least 1 indicator in condition then it must match it to have non-zero probability.
121
+ wmc = self._get_wmc_for_marginals(rvs, condition)
122
+
123
+ result: NDArrayFloat64 = np.fromiter(
124
+ (wmc(indicators) for indicators in rv_instances_as_indicators(*rvs)),
125
+ count=number_of_states(*rvs),
126
+ dtype=np.float64
127
+ )
128
+ _normalise_marginal(result)
129
+ return result
130
+
131
+ def map(self, *rvs: RandomVariable, condition: Condition = ()) -> Tuple[float, Instance]:
132
+ """
133
+ Determine the maximum apriori probability (MAP).
134
+
135
+ If there are tied solutions, one solution is returned, which
136
+ is selected arbitrarily.
137
+
138
+ If multiple indicators of the same random variable appear in
139
+ the parameter 'condition' then they are interpreted as
140
+ a disjunction, otherwise indicators are interpreted as
141
+ a conjunction. E.g.: X=0, Y=1, Y=3 means X=0 and (Y=1 or Y=3)
142
+
143
+ Warning:
144
+ This is not an efficient implementation as it will call `self.wmc`
145
+ for each possible state of the given random variables. If efficient MAP
146
+ probability calculations are required, consider using a different method.
147
+
148
+ Args:
149
+ rvs: random variables to find the MAP over.
150
+ condition: any conditioning indicators.
151
+
152
+ Returns:
153
+ (probability, instance) where
154
+ probability: is the MAP probability
155
+ instance: is the MAP state (co-indexed with the given rvs).
156
+ """
157
+ condition: Sequence[Indicator] = check_condition(condition)
158
+
159
+ rv_indexes = set(rv.idx for rv in rvs)
160
+ assert len(rv_indexes) == len(rvs), 'duplicated random variables not allowed'
161
+
162
+ # Group conditioning indicators by random variable.
163
+ conditions_by_rvs = MapSet()
164
+ for ind in condition:
165
+ conditions_by_rvs.get_set(ind.rv_idx).add(ind.state_idx)
166
+
167
+ # See if any MAP random variable is also conditioned.
168
+ # Reduce the state space of any conditioned MAP rv.
169
+ loop_rvs = []
170
+ reduced_space = False
171
+ for rv in rvs:
172
+ states = conditions_by_rvs.get(rv.idx)
173
+ if states is None:
174
+ loop_rvs.append(rv)
175
+ else:
176
+ loop_rvs.append([rv[i] for i in sorted(states)])
177
+ reduced_space = True
178
+
179
+ # If the random variables we are looping over does not have any conditions
180
+ # then it is expected to be faster by using computed marginal probabilities.
181
+ if not reduced_space:
182
+ prs = self.marginal_distribution(*rvs, condition=condition)
183
+ best_probability = float('-inf')
184
+ best_states = None
185
+ for probability, inst in zip(prs, rv_instances(*rvs)):
186
+ if probability > best_probability:
187
+ best_probability = probability
188
+ best_states = inst
189
+ return best_probability, best_states
190
+
191
+ else:
192
+ # Remove any condition indicators with rv in rvs.
193
+ new_conditions = tuple(ind for ind in condition if ind.rv_idx not in rv_indexes)
194
+
195
+ # Loop over the state space of the 'loop' rvs
196
+ best_probability = float('-inf')
197
+ best_states = None
198
+ inds: Tuple[Indicator, ...]
199
+ for inds in _combos(loop_rvs):
200
+ probability = self.wmc(*(inds + new_conditions))
201
+ if probability > best_probability:
202
+ best_probability = probability
203
+ best_states = tuple(ind.state_idx for ind in inds)
204
+ condition_probability = self.wmc(*condition)
205
+ return best_probability / condition_probability, best_states
206
+
207
+ def correlation(self, indicator1: Indicator, indicator2: Indicator, condition: Condition = ()) -> float:
208
+ """
209
+ What is the correlation between the two given indicators, r(indicator1, indicator2).
210
+
211
+ Args:
212
+ indicator1: a first random variable and its state.
213
+ indicator2: a second random variable and its state.
214
+ condition: any conditioning indicators.
215
+
216
+ Returns:
217
+ correlation between the two given indicators.
218
+ """
219
+ condition = check_condition(condition)
220
+
221
+ p1 = self.probability(indicator1, condition=condition)
222
+ p2 = self.probability(indicator2, condition=condition)
223
+ p12 = self._joint_probability(indicator1, indicator2, condition=condition)
224
+ d = p1 * (1.0 - p1) * p2 * (1.0 - p2)
225
+ if d == 0.0:
226
+ # As any marginal probability approaches zero, correlation approaches zero
227
+ return 0.0
228
+ else:
229
+ return (p12 - p1 * p2) / math.sqrt(d)
230
+
231
+ def entropy(self, rv: RandomVariable, condition: Condition = ()) -> float:
232
+ """
233
+ Calculate the entropy of the given random variable, H(rv).
234
+
235
+ Args:
236
+ rv: random variable to calculate the entropy for.
237
+ condition: any conditioning indicators.
238
+
239
+ Returns:
240
+ entropy of the given random variable.
241
+ """
242
+ condition = check_condition(condition)
243
+ e = 0.0
244
+ for ind in rv:
245
+ p = self.probability(ind, condition=condition)
246
+ if p > 0.0:
247
+ e -= p * math.log2(p)
248
+ return e
249
+
250
+ def conditional_entropy(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
251
+ """
252
+ Calculate the conditional entropy, H(rv1 | rv2).
253
+
254
+ Args:
255
+ rv1: random variable to calculate the entropy for.
256
+ rv2: the conditioning random variable for entropy calculation.
257
+ condition: any conditioning indicators to restrict the state space.
258
+
259
+ Returns:
260
+ entropy of rv1, conditioned on rv2.
261
+ """
262
+ condition = check_condition(condition)
263
+ e = 0.0
264
+ for ind1 in rv1:
265
+ for ind2 in rv2:
266
+ p = self._joint_probability(ind1, ind2, condition=condition)
267
+ if p > 0.0:
268
+ # if p > 0 then p2 > 0, as p <= p2
269
+ p2 = self.probability(ind2, condition=condition)
270
+ e -= p * math.log2(p / p2)
271
+ return e
272
+
273
+ def joint_entropy(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
274
+ """
275
+ Calculate the joint entropy of the two random variables, H(rv1; rv2).
276
+
277
+ Args:
278
+ rv1: a first random variable to calculate joint entropy.
279
+ rv2: a second random variable to calculate joint entropy.
280
+ condition: any conditioning indicators to restrict the state space.
281
+ Returns:
282
+ joint entropy of the given random variables.
283
+ """
284
+ condition = check_condition(condition)
285
+ e = 0.0
286
+ for ind1 in rv1:
287
+ for ind2 in rv2:
288
+ p = self._joint_probability(ind1, ind2, condition=condition)
289
+ if p > 0.0:
290
+ e -= p * math.log2(p)
291
+ return e
292
+
293
+ def mutual_information(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
294
+ """
295
+ Calculate the mutual information between two random variables, I(rv1; rv2).
296
+
297
+ Args:
298
+ rv1: a first random variable
299
+ rv2: a second random variable
300
+ condition: indicators to specify a condition restricting the state space.
301
+ Returns:
302
+ mutual_information(rv1, rv2) / denominator
303
+ """
304
+ condition = check_condition(condition)
305
+ p1s = self.marginal_distribution(rv1, condition=condition)
306
+ p2s = self.marginal_distribution(rv2, condition=condition)
307
+ info = 0.0
308
+ for ind1, p1 in zip(rv1, p1s):
309
+ for ind2, p2 in zip(rv2, p2s):
310
+ p12 = self._joint_probability(ind1, ind2, condition=condition)
311
+ if p12 > 0.0:
312
+ info += p12 * math.log2(p12 / p1 / p2)
313
+ return info
314
+
315
+ def total_correlation(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
316
+ """
317
+ Calculate the 'total correlation' measure.
318
+ total_correlation = I(rv1; rv2) / min(H(rv1), H(rv2)).
319
+ This is a normalised mutual information between two random variables.
320
+ 0 => no mutual information.
321
+ 1 => perfect mutual information.
322
+
323
+ Args:
324
+ rv1: a first random variable
325
+ rv2: a second random variable
326
+ condition: indicators to specify a condition restricting the state space.
327
+ Returns:
328
+ total correlation between the given random variables.
329
+ """
330
+ condition = check_condition(condition)
331
+ denominator = min(self.entropy(rv1), self.entropy(rv2, condition=condition))
332
+ return self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
333
+
334
+ def uncertainty(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
335
+ """
336
+ Calculate the 'uncertainty' measure, C, between two random variables
337
+ C(rv1, rv2) = I(rv1; rv2) / H(rv2)
338
+ This is a normalised mutual information between two random variables.
339
+ Note that it is not a symmetric measure; in general C(rv1, rv2) does not equal C(rv2, rv1).
340
+ 0 => no mutual information.
341
+ 1 => perfect mutual information.
342
+
343
+ Args:
344
+ rv1: a first random variable
345
+ rv2: a second random variable
346
+ condition: indicators to specify a condition restricting the state space.
347
+ Returns:
348
+ uncertainty between the given random variables.
349
+ """
350
+ condition = check_condition(condition)
351
+ denominator = self.entropy(rv2, condition=condition)
352
+ return self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
353
+
354
+ def symmetric_uncertainty(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
355
+ """
356
+ Calculate the 'symmetric uncertainty' measure.
357
+ symmetric_uncertainty = 2 * I(rv1, rv2) / (H(rv1) + H(rv2)).
358
+ This is the harmonic mean of the two uncertainty coefficients,
359
+ C(rv1, rv2) = I(rv1; rv2) / H(rv2) and C(rv2, rv1) = I(rv1; rv2) / H(rv1).
360
+ This is a normalised mutual information between two random variables.
361
+ 0 => no mutual information.
362
+ 1 => perfect mutual information.
363
+
364
+ Args:
365
+ rv1: a first random variable
366
+ rv2: a second random variable
367
+ condition: indicators to specify a condition restricting the state space.
368
+ Returns:
369
+ symmetric uncertainty between the given random variables.
370
+ """
371
+ condition = check_condition(condition)
372
+ denominator = self.entropy(rv1) + self.entropy(rv2, condition=condition)
373
+ return 2.0 * self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
374
+
375
+ def iqr(self, rv1: RandomVariable, rv2: RandomVariable, condition: Condition = ()) -> float:
376
+ """
377
+ Calculate the Information Quality Ratio (IQR).
378
+ IQR = I(rv1; rv2) / H(rv1; rv2).
379
+ Also known as 'dual total correlation'.
380
+ This is a normalised mutual information between two random variables.
381
+ 0 => no mutual information.
382
+ 1 => perfect mutual information.
383
+
384
+ Args:
385
+ rv1: a first random variable
386
+ rv2: a second random variable
387
+ condition: indicators to specify a condition restricting the state space.
388
+ Returns:
389
+ Information Quality Ratio between the given random variables.
390
+ """
391
+ condition = check_condition(condition)
392
+ denominator = self.joint_entropy(rv1, rv2, condition=condition)
393
+ return self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
394
+
395
+ def covariant_normalised_mutual_information(self, rv1: RandomVariable, rv2: RandomVariable,
396
+ condition: Condition = ()) -> float:
397
+ """
398
+ Calculate the covariant normalised mutual information
399
+ = I(rv1; rv2) / sqrt(H(rv1) * H(rv2)).
400
+ This is a normalised mutual information between two random variables.
401
+ 0 => no mutual information.
402
+ 1 => perfect mutual information.
403
+
404
+ Args:
405
+ rv1: a first random variable
406
+ rv2: a second random variable
407
+ condition: indicators to specify a condition restricting the state space.
408
+ Returns:
409
+ covariant normalised mutual information between the given random variables.
410
+ """
411
+ condition = check_condition(condition)
412
+ denominator = math.sqrt(self.entropy(rv1, condition=condition) * self.entropy(rv2, condition=condition))
413
+ return self._normalised_mutual_information(rv1, rv2, denominator, condition=condition)
414
+
415
+ def _normalised_mutual_information(
416
+ self,
417
+ rv1: RandomVariable,
418
+ rv2: RandomVariable,
419
+ denominator: float,
420
+ condition: Tuple[Indicator, ...],
421
+ ) -> float:
422
+ """
423
+ Helper function for normalised mutual information calculations.
424
+
425
+ Args:
426
+ rv1: a first random variable
427
+ rv2: a second random variable
428
+ denominator: the normalisation factor
429
+ condition: indicators to specify a condition restricting the state space.
430
+ Returns:
431
+ mutual_information(rv1, rv2) / denominator
432
+ """
433
+ if denominator == 0.0:
434
+ return 0.0
435
+ else:
436
+ return self.mutual_information(rv1, rv2, condition) / denominator
437
+
438
+ def _joint_probability(
439
+ self,
440
+ indicator1: Indicator,
441
+ indicator2: Indicator,
442
+ condition: Tuple[Indicator, ...],
443
+ ) -> float:
444
+ """
445
+ Helper function to correctly calculate a joint probability even if the two indicators
446
+ are from the same random variable.
447
+
448
+ If the indicators are from the different random variables then
449
+ probability(indicator1 and indicator2 | condition).
450
+
451
+ If the indicators are from the same random variable then
452
+ probability(indicator1 or indicator2 | condition).
453
+
454
+ Args:
455
+ indicator1: a first Indicator.
456
+ indicator2: a second Indicator
457
+ condition: indicators to specify a condition restricting the state space.
458
+ Returns:
459
+ joint probability of the two indicators, given the condition.
460
+ """
461
+ if indicator1 == indicator2:
462
+ # Ensure correct behaviour, same random variable and same states
463
+ return self.probability(indicator1, condition=condition)
464
+ elif indicator1.rv_idx == indicator2.rv_idx:
465
+ # Efficiency shortcut, same random variable but different states
466
+ return 0.0
467
+ else:
468
+ # General case, two different random variables
469
+ return self.probability(indicator1, indicator2, condition=condition)
470
+
471
+ def _get_wmc_for_marginals(
472
+ self,
473
+ rvs: Sequence[RandomVariable],
474
+ condition: Tuple[Indicator, ...],
475
+ ) -> Callable[[Sequence[Indicator]], float]:
476
+ """
477
+ Return a wmc function that is suitable for calculating marginal distributions.
478
+
479
+ This implementation is careful of the situation where indicators of rvs appear in condition.
480
+ If an RV has at least 1 indicator in condition then it must match it to have non-zero probability.
481
+
482
+ Args:
483
+ rvs: random variables to calculate marginal distributions for.
484
+ condition: indicators to specify a condition restricting the state space.
485
+ Returns:
486
+ A function from a condition, specified as a sequence of indicators, to a weighted model count.
487
+ """
488
+ if len(condition) > 0:
489
+ check_sets = []
490
+ overlap_detected = False
491
+ cond_set = set(condition)
492
+ for rv in rvs:
493
+ in_condition = set()
494
+ for ind in rv:
495
+ if ind in cond_set:
496
+ in_condition.add(ind)
497
+ cond_set.discard(ind)
498
+ overlap_detected = True
499
+ if len(in_condition) == 0:
500
+ in_condition.update(rv)
501
+ check_sets.append(in_condition)
502
+
503
+ if overlap_detected:
504
+ __wmc__condition = tuple(cond_set)
505
+
506
+ def wmc(indicators: Sequence[Indicator]) -> float:
507
+ for indicator, check_set in zip(indicators, check_sets):
508
+ if indicator not in check_set:
509
+ return 0.0
510
+ full_condition = tuple(indicators) + __wmc__condition
511
+ return self.wmc(*full_condition)
512
+ else:
513
+ __wmc__condition = tuple(condition)
514
+
515
+ def wmc(indicators: Sequence[Indicator]) -> float:
516
+ full_condition = tuple(indicators) + __wmc__condition
517
+ return self.wmc(*full_condition)
518
+ else:
519
+ def wmc(indicators: Sequence[Indicator]) -> float:
520
+ return self.wmc(*indicators)
521
+
522
+ return wmc
523
+
524
+
525
+ def check_condition(condition: Condition) -> Tuple[Indicator, ...]:
526
+ """
527
+ Make the best effort to interpret the given condition.
528
+
529
+ Args:
530
+ condition: a relaxed specification of a condition.
531
+ Returns:
532
+ a formal specification of the condition as a tuple of indicators with no duplicates.
533
+ """
534
+ if condition is None:
535
+ return ()
536
+ elif isinstance(condition, Indicator):
537
+ return (condition,)
538
+ else:
539
+ return tuple(set(condition))
540
+
541
+
542
+ def dtype_for_state_indexes(rvs: Iterable[RandomVariable]) -> DTypeStates:
543
+ """
544
+ Infer a numpy dtype to hold any state index from any given random variable.
545
+
546
+ Args:
547
+ rvs: some random variables.
548
+ Returns:
549
+ a numpy dtype.
550
+ """
551
+ return dtype_for_number_of_states(max((len(rv) for rv in rvs), default=0))
552
+
553
+
554
+ def _normalise_marginal(distribution: NDArrayFloat64) -> None:
555
+ """
556
+ Update the values in the given distribution to
557
+ properly represent a marginal distribution.
558
+
559
+ The update is made in-place.
560
+
561
+ Args:
562
+ a 1D numpy array of likelihoods.
563
+ """
564
+ total = np.sum(distribution)
565
+ if total <= 0:
566
+ distribution[:] = np.nan
567
+ elif total != 1:
568
+ distribution /= total
ck/program/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .raw_program import RawProgram
2
+ from .program_buffer import ProgramBuffer
3
+ from .program import Program
ck/program/program.py ADDED
@@ -0,0 +1,129 @@
1
+ from typing import Callable, Sequence
2
+
3
+ import numpy as np
4
+
5
+ from ck.program.raw_program import RawProgram
6
+ from ck.utils.np_extras import DTypeNumeric, NDArrayNumeric
7
+
8
+
9
+ class Program:
10
+ """
11
+ A Program wraps a RawProgram to make a convenient callable.
12
+ """
13
+
14
+ def __init__(self, raw_program: RawProgram):
15
+ self._raw_program = raw_program
16
+ self.__call = self.__get_call_method()
17
+
18
+ @property
19
+ def dtype(self) -> DTypeNumeric:
20
+ """
21
+ What is the C data type of values.
22
+ This is the same as numpy and ctypes `dtype`.
23
+ """
24
+ return self._raw_program.dtype
25
+
26
+ @property
27
+ def number_of_vars(self) -> int:
28
+ """
29
+ How many input values are there to the function.
30
+ Each input value relates to a circuit VarNode, as
31
+ per method `var_indices`.
32
+
33
+ Returns:
34
+ the number of input values.
35
+ """
36
+ return self._raw_program.number_of_vars
37
+
38
+ @property
39
+ def number_of_tmps(self) -> int:
40
+ """
41
+ How many temporary values are there to the function.
42
+
43
+ Returns:
44
+ the number of temporary values.
45
+ """
46
+ return self._raw_program.number_of_tmps
47
+
48
+ @property
49
+ def number_of_results(self) -> int:
50
+ """
51
+ How many output values are there from the function.
52
+
53
+ Returns:
54
+ the number of output values.
55
+ """
56
+ return self._raw_program.number_of_results
57
+
58
+ @property
59
+ def var_indices(self) -> Sequence[int]:
60
+ """
61
+ Get the circuit `VarNode.index` for each function input.
62
+
63
+ Returns:
64
+ a list of the circuit VarNode indices, co-indexed
65
+ with the function input values.
66
+ """
67
+ return self._raw_program.var_indices
68
+
69
+ def __call__(self, *args: float | int) -> NDArrayNumeric | int | float:
70
+ """
71
+ Call the compiled program.
72
+
73
+ Returns:
74
+ either a single value or a numpy array, depending
75
+ on the construction arguments.
76
+
77
+ Raises:
78
+ ValueError: if the given number of argument != self.number_of_vars.
79
+ """
80
+ # dynamically defined at construction time
81
+ return self.__call(*args)
82
+
83
+ def __call_single(self, *args: float) -> int | float:
84
+ """
85
+ Returns a single result of type self._dtype.value.dtype
86
+ """
87
+ return self._raw_program(args).item()
88
+
89
+ def __call_empty(self, *args: float | int) -> NDArrayNumeric:
90
+ """
91
+ Returns a numpy array result of dtype self.dtype
92
+ """
93
+ if len(args) != self.number_of_vars:
94
+ raise ValueError(f'incorrect number of arguments: expected {self.number_of_vars}, got {len(args)}')
95
+ np_out = np.zeros(0, dtype=self.dtype)
96
+ return np_out
97
+
98
+ def __call_multi(self, *args: float | int) -> NDArrayNumeric:
99
+ """
100
+ Returns a numpy array result of dtype self.dtype
101
+ """
102
+ return self._raw_program(args)
103
+
104
+ def __get_call_method(self) -> Callable:
105
+ """
106
+ Choose a call method based on self._number_of_results
107
+ """
108
+ match self.number_of_results:
109
+ case 0:
110
+ return self.__call_empty
111
+ case 1:
112
+ return self.__call_single
113
+ case _:
114
+ return self.__call_multi
115
+
116
+ def __getstate__(self):
117
+ """
118
+ Support for pickle.
119
+ """
120
+ return {
121
+ '_raw_program': self._raw_program,
122
+ }
123
+
124
+ def __setstate__(self, state):
125
+ """
126
+ Support for pickle.
127
+ """
128
+ self._raw_program = state['_raw_program']
129
+ self.__call = self.__get_call_method()