probability-flow 1.4.3__tar.gz → 1.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {probability_flow-1.4.3 → probability_flow-1.4.4}/CHANGELOG.md +28 -2
- {probability_flow-1.4.3 → probability_flow-1.4.4}/PKG-INFO +1 -1
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/__init__.py +1 -1
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/graph.py +9 -1
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/independent.py +65 -6
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/serialize.py +1 -1
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/variable.py +30 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/pyproject.toml +1 -1
- {probability_flow-1.4.3 → probability_flow-1.4.4}/.gitignore +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/LICENSE +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/README.md +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/cpd.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/decompose.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/edges.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/eliminate.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/engines.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/evidence.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/factors.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/generation.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/inference.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/junction.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/logical.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/noisy.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/ordering.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/py.typed +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/query.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/reveal.py +0 -0
- {probability_flow-1.4.3 → probability_flow-1.4.4}/probability_flow/viz.py +0 -0
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.4
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`variable.set_baseline(probability)`**, for changing P(a node) with every
|
|
8
|
+
parent absent after the CPD is written.
|
|
9
|
+
|
|
10
|
+
It takes a PROBABILITY, which is the unit `set_prior` takes and the unit the
|
|
11
|
+
file format stores. There is no relative form and no `bits=` flag: a
|
|
12
|
+
`shift_baseline(0.2)` beside a `set_baseline(0.2)` is two calls that read the
|
|
13
|
+
same and mean different things, since `0.2` is a plausible probability and a
|
|
14
|
+
plausible number of bits.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A removed id could be handed out again.** Removing a variable from the
|
|
19
|
+
middle of a graph left a hole that the next auto-assigned id stepped past,
|
|
20
|
+
but removing the LAST variable simply freed its id, and the next variable
|
|
21
|
+
took it back. Anything still referring to the removed node then pointed at a
|
|
22
|
+
different proposition, silently. Ids are retired on removal now.
|
|
23
|
+
|
|
24
|
+
- **Three more silent no-ops in the additive family.** `cpd.baseline`,
|
|
25
|
+
`cpd.gates` and `Gate.weight_when_gated` all accepted an assignment, changed
|
|
26
|
+
nothing and reported nothing, because the value every query reads is a
|
|
27
|
+
derived copy. `baseline` was the worst: it read back as the number you had
|
|
28
|
+
assigned, so the object agreed with you while going on answering the old
|
|
29
|
+
question. All three raise now.
|
|
30
|
+
|
|
3
31
|
## 1.4.3
|
|
4
32
|
|
|
5
33
|
### Added
|
|
@@ -78,8 +106,6 @@
|
|
|
78
106
|
the row where both hold, where an `and` over the two would be `1.0`. Tagging
|
|
79
107
|
that `logical` would misdescribe the numbers.
|
|
80
108
|
|
|
81
|
-
## Unreleased
|
|
82
|
-
|
|
83
109
|
### Fixed
|
|
84
110
|
|
|
85
111
|
- **An evidence node's `name` is free text.** The reader used to refuse any name
|
|
@@ -39,6 +39,13 @@ class CausalGraph:
|
|
|
39
39
|
self._by_name: "dict[str, Variable]" = {}
|
|
40
40
|
self._position: "dict[int, int]" = {}
|
|
41
41
|
self._by_id: "dict[str, Variable]" = {}
|
|
42
|
+
#: EVERY ID THIS GRAPH HAS EVER ISSUED, including ones whose variable
|
|
43
|
+
#: has since been removed. `_by_id` is not enough on its own: removing
|
|
44
|
+
#: the LAST variable freed its id, and the next auto-assigned one took
|
|
45
|
+
#: it back, so a decoration or an external reference keyed to the old
|
|
46
|
+
#: `v2` would silently attach to a different proposition. Ids are the
|
|
47
|
+
#: schema every external reference uses, so a removed one is retired.
|
|
48
|
+
self._issued: "set[str]" = set()
|
|
42
49
|
#: Free-form provenance emitted at the top of a document: library
|
|
43
50
|
#: version, generator identity, and a seed where one exists. The seed is
|
|
44
51
|
#: what makes "regenerate the affected twelve" possible instead of
|
|
@@ -108,7 +115,7 @@ class CausalGraph:
|
|
|
108
115
|
# variable used to hand the reader an auto id that was already
|
|
109
116
|
# taken, so the graph could not round-trip at all.
|
|
110
117
|
n = len(self._variables)
|
|
111
|
-
while f"v{n}" in self._by_id:
|
|
118
|
+
while f"v{n}" in self._by_id or f"v{n}" in self._issued:
|
|
112
119
|
n += 1
|
|
113
120
|
variable.id = f"v{n}"
|
|
114
121
|
if variable.id in self._by_id:
|
|
@@ -125,6 +132,7 @@ class CausalGraph:
|
|
|
125
132
|
self._variables.append(variable)
|
|
126
133
|
self._by_name[variable.name] = variable
|
|
127
134
|
self._by_id[variable.id] = variable
|
|
135
|
+
self._issued.add(variable.id)
|
|
128
136
|
self._position[id(variable)] = len(self._variables) - 1
|
|
129
137
|
self._parent_list[id(variable)] = []
|
|
130
138
|
self._child_list[id(variable)] = []
|
|
@@ -101,7 +101,7 @@ class Gate:
|
|
|
101
101
|
whose polarity is ambiguous.
|
|
102
102
|
"""
|
|
103
103
|
|
|
104
|
-
__slots__ = ("source", "modulates", "
|
|
104
|
+
__slots__ = ("source", "modulates", "_weight_when_gated")
|
|
105
105
|
|
|
106
106
|
def __init__(self, source, modulates,
|
|
107
107
|
weight_when_gated: float = 0.0):
|
|
@@ -116,7 +116,18 @@ class Gate:
|
|
|
116
116
|
)
|
|
117
117
|
self.source = source
|
|
118
118
|
self.modulates = modulates
|
|
119
|
-
self.
|
|
119
|
+
self._weight_when_gated = float(weight_when_gated)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def weight_when_gated(self) -> float:
|
|
123
|
+
"""What the modulated edge carries while this gate holds, in bits. READ-ONLY.
|
|
124
|
+
|
|
125
|
+
The CPD converts it to nats once, at construction, so assigning here
|
|
126
|
+
changed nothing and reported nothing. A gate's weight is part of what
|
|
127
|
+
the gate IS, so changing it means declaring the gate again and setting
|
|
128
|
+
the CPD, which is the same amount of typing and cannot half-apply.
|
|
129
|
+
"""
|
|
130
|
+
return self._weight_when_gated
|
|
120
131
|
|
|
121
132
|
def __repr__(self) -> str:
|
|
122
133
|
kind = ("hard" if self.weight_when_gated == 0.0
|
|
@@ -224,15 +235,15 @@ class IndependentEvidence(CPD):
|
|
|
224
235
|
|
|
225
236
|
self.child = None # bound by set_cpd
|
|
226
237
|
self.parents = tuple(parents)
|
|
227
|
-
self.
|
|
228
|
-
self.
|
|
238
|
+
self._baseline = float(baseline)
|
|
239
|
+
self._gates = tuple(gates)
|
|
229
240
|
#: AUTHORED BITS, stored verbatim. Not re-derived from the nats below,
|
|
230
241
|
#: because bits -> nats -> bits does not return the number that was
|
|
231
242
|
#: written: speeding-camera carries 1.5154639175257731 and a document
|
|
232
243
|
#: must hold what its author wrote.
|
|
233
244
|
self._weights = {id(p): float(ratios.get(p, 0.0)) for p in parents}
|
|
234
245
|
|
|
235
|
-
self._log_baseline = math.log(self.
|
|
246
|
+
self._log_baseline = math.log(self._baseline / (1.0 - self._baseline))
|
|
236
247
|
#: nats, derived once. Everything below this line is nats, because
|
|
237
248
|
#: math.log is, and converting at every comparison is how a 1.4427
|
|
238
249
|
#: factor goes missing.
|
|
@@ -254,6 +265,54 @@ class IndependentEvidence(CPD):
|
|
|
254
265
|
from types import MappingProxyType
|
|
255
266
|
return MappingProxyType(self._weights)
|
|
256
267
|
|
|
268
|
+
@property
|
|
269
|
+
def gates(self):
|
|
270
|
+
"""The gates on this CPD. READ-ONLY.
|
|
271
|
+
|
|
272
|
+
`_gated` is built from these once, at construction, so replacing the
|
|
273
|
+
tuple changed nothing. Gates are declared when the CPD is written.
|
|
274
|
+
"""
|
|
275
|
+
return self._gates
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def baseline(self) -> float:
|
|
279
|
+
"""P(child) with every parent absent, as a PROBABILITY. Read-only.
|
|
280
|
+
|
|
281
|
+
`_log_baseline` holds the same number in nats and is what every query
|
|
282
|
+
reads, so `cpd.baseline = 0.2` used to change nothing, report nothing,
|
|
283
|
+
and then read back as 0.2: the object lied about itself. Use
|
|
284
|
+
`set_baseline`.
|
|
285
|
+
"""
|
|
286
|
+
return self._baseline
|
|
287
|
+
|
|
288
|
+
def set_baseline(self, probability: float) -> None:
|
|
289
|
+
"""Set the baseline, as a PROBABILITY.
|
|
290
|
+
|
|
291
|
+
ABSOLUTE, SO IT IS A PROBABILITY, which is the same unit `set_prior`
|
|
292
|
+
takes and the same unit the file format stores. The baseline is a claim
|
|
293
|
+
about the world -- what the child is worth when none of its parents
|
|
294
|
+
hold -- so it is the kind of thing you can write a sentence about, and
|
|
295
|
+
sentences about it are in probabilities.
|
|
296
|
+
|
|
297
|
+
THERE IS NO RELATIVE FORM, ON PURPOSE. A `shift_baseline(0.2)` beside
|
|
298
|
+
a `set_baseline(0.2)` is two calls that read the same and mean
|
|
299
|
+
different things, because 0.2 is a plausible probability AND a
|
|
300
|
+
plausible number of bits. That is the ambiguity a `bits=True` flag
|
|
301
|
+
would introduce, spread across two names instead of one keyword.
|
|
302
|
+
"""
|
|
303
|
+
if not (isinstance(probability, (int, float))
|
|
304
|
+
and not isinstance(probability, bool)
|
|
305
|
+
and 0.0 < probability < 1.0):
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"a baseline is a probability strictly between 0 and 1 (got "
|
|
308
|
+
f"{probability!r}). It is P(child) with every parent absent, so "
|
|
309
|
+
"0 and 1 are both excluded: a baseline of 0 is a child no "
|
|
310
|
+
"evidence can ever raise, and the log-odds of either is "
|
|
311
|
+
"infinite."
|
|
312
|
+
)
|
|
313
|
+
self._baseline = float(probability)
|
|
314
|
+
self._log_baseline = math.log(self._baseline / (1.0 - self._baseline))
|
|
315
|
+
|
|
257
316
|
def set_weight(self, parent, bits: float) -> None:
|
|
258
317
|
"""Set what one parent's edge carries, in BITS, keeping the derived form.
|
|
259
318
|
|
|
@@ -517,7 +576,7 @@ class IndependentEvidence(CPD):
|
|
|
517
576
|
kept = {id(p) for p in parents}
|
|
518
577
|
out._weights = {id(p): self.weights.get(id(p), 0.0) for p in parents}
|
|
519
578
|
out._log_w = {k: v * _LN2 for k, v in out.weights.items()}
|
|
520
|
-
out.
|
|
579
|
+
out._gates = tuple(g for g in self.gates
|
|
521
580
|
if id(g.source) in kept and id(g.modulates) in kept)
|
|
522
581
|
out._gated = {id(g.modulates): (g.source, g.weight_when_gated * _LN2)
|
|
523
582
|
for g in out.gates}
|
|
@@ -599,6 +599,36 @@ class Variable:
|
|
|
599
599
|
"""P(this | its causes). See `CausalGraph.set_cpd` for what `spec` may be."""
|
|
600
600
|
self.graph._set_cpd(self, spec)
|
|
601
601
|
|
|
602
|
+
def _additive_cpd(self, doing: str):
|
|
603
|
+
"""This node's CPD, if it is the family that has a baseline."""
|
|
604
|
+
try:
|
|
605
|
+
cpd = self.cpd
|
|
606
|
+
except KeyError:
|
|
607
|
+
# `graph._cpd` raises rather than returning None, so this is the
|
|
608
|
+
# path a caller takes before writing any numbers at all.
|
|
609
|
+
raise ValueError(
|
|
610
|
+
f"{short(self.name)!r} has no CPD yet, so there is no baseline "
|
|
611
|
+
"to set. Give it one first, with set_cpd() or set_prior()."
|
|
612
|
+
) from None
|
|
613
|
+
if not hasattr(cpd, "set_baseline"):
|
|
614
|
+
leak = " Its `leak` is the same idea for this family." \
|
|
615
|
+
if hasattr(cpd, "leak") else ""
|
|
616
|
+
raise ValueError(
|
|
617
|
+
f"{short(self.name)!r} has a {type(cpd).__name__}, which has no "
|
|
618
|
+
f"baseline to {doing}. A baseline is P(child) with every parent "
|
|
619
|
+
"absent, which only the additive family states as a single "
|
|
620
|
+
f"number.{leak}"
|
|
621
|
+
)
|
|
622
|
+
return cpd
|
|
623
|
+
|
|
624
|
+
def set_baseline(self, probability: float) -> None:
|
|
625
|
+
"""Set P(this) with every parent absent, as a PROBABILITY.
|
|
626
|
+
|
|
627
|
+
The same unit `set_prior` takes, because it is the same kind of claim:
|
|
628
|
+
what this node is worth before any of its causes do anything.
|
|
629
|
+
"""
|
|
630
|
+
self._additive_cpd("set").set_baseline(probability)
|
|
631
|
+
|
|
602
632
|
@property
|
|
603
633
|
def parents(self) -> "tuple[Variable, ...]":
|
|
604
634
|
return self.graph._parents(self)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|