fxtensor-salmon 1.2.2__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fxtensor_salmon-1.2.2/src/fxtensor_salmon.egg-info → fxtensor_salmon-1.4.0}/PKG-INFO +82 -1
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/README.md +76 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/pyproject.toml +7 -1
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/core.py +3 -1
- fxtensor_salmon-1.4.0/src/fxtensor_salmon/exchangeability.py +328 -0
- fxtensor_salmon-1.4.0/src/fxtensor_salmon/independence.py +284 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0/src/fxtensor_salmon.egg-info}/PKG-INFO +82 -1
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/SOURCES.txt +5 -0
- fxtensor_salmon-1.4.0/src/fxtensor_salmon.egg-info/requires.txt +15 -0
- fxtensor_salmon-1.4.0/tests/test_doc_examples.py +17 -0
- fxtensor_salmon-1.4.0/tests/test_exchangeability.py +187 -0
- fxtensor_salmon-1.4.0/tests/test_independence.py +212 -0
- fxtensor_salmon-1.2.2/src/fxtensor_salmon.egg-info/requires.txt +0 -8
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/LICENSE +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/setup.cfg +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/__init__.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/discopy.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/io.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/operations.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/profile.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/special.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/dependency_links.txt +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/top_level.txt +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_almost_surely.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_axioms.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_discopy.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_fxtensor.py +0 -0
- {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_inference.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fxtensor_salmon
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Markov Tensor Module
|
|
5
5
|
Author-email: Tadachika Oki <sakesnare@gmail.com>
|
|
6
6
|
License: Copyright 2025 Tadachika Oki
|
|
@@ -12,6 +12,7 @@ License: Copyright 2025 Tadachika Oki
|
|
|
12
12
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
13
13
|
|
|
14
14
|
Project-URL: Homepage, https://github.com/salmonsnare/fxtensor-salmon
|
|
15
|
+
Project-URL: Documentation, https://salmonsnare.github.io/fxtensor-salmon/
|
|
15
16
|
Classifier: Programming Language :: Python :: 3
|
|
16
17
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
18
|
Classifier: Operating System :: OS Independent
|
|
@@ -25,12 +26,21 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
25
26
|
Requires-Dist: ruff; extra == "dev"
|
|
26
27
|
Provides-Extra: discopy
|
|
27
28
|
Requires-Dist: discopy<2,>=1.2; extra == "discopy"
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: sphinx>=7.4; extra == "docs"
|
|
31
|
+
Requires-Dist: furo>=2024.8.6; extra == "docs"
|
|
32
|
+
Requires-Dist: discopy<2,>=1.2; python_version >= "3.10" and extra == "docs"
|
|
28
33
|
Dynamic: license-file
|
|
29
34
|
|
|
30
35
|
# FXTensor
|
|
31
36
|
|
|
32
37
|
FXTensor is a Python library for tensor-based computations, particularly suited for modeling probabilistic systems and processes inspired by category theory. It leverages NumPy for efficient numerical computations. The library primarily supports labeled indices for enhanced readability while maintaining compatibility with unlabeled numeric indices.
|
|
33
38
|
|
|
39
|
+
## Documentation
|
|
40
|
+
|
|
41
|
+
- [English](https://salmonsnare.github.io/fxtensor-salmon/en/)
|
|
42
|
+
- [日本語](https://salmonsnare.github.io/fxtensor-salmon/ja/)
|
|
43
|
+
|
|
34
44
|
## Core Concepts
|
|
35
45
|
|
|
36
46
|
An FXTensor is defined by its `profile` and `data`, with optional string labels to make tensors more intuitive and meaningful.
|
|
@@ -173,6 +183,67 @@ Model a weather system with states “Sunny” or “Rainy.”
|
|
|
173
183
|
p_sunny = sunny_tomorrow.data[sunny_idx] # 0.8
|
|
174
184
|
```
|
|
175
185
|
|
|
186
|
+
### Conditional independence: weather, umbrella, and the forecast
|
|
187
|
+
|
|
188
|
+
The forecast drives both the weather you meet and whether you carry an umbrella. Given the forecast they are conditionally independent. The marginal on weather and umbrella still couples them.
|
|
189
|
+
|
|
190
|
+
The API orders the joint as weather ⊗ umbrella ⊗ forecast (`X ⊗ Y ⊗ Z`, with `Z` last). The figure draws the same variables as `X ⊗ Z ⊗ Y` and is read bottom to top. Left: `X ⊥ Y | Z` is the copy–discard equation. Right: `conditional_product` is `p_Z ; copy_Z ; (p(x|z) ⊗ id_Z ⊗ q(y|z))`. Bottom: when `Z` is trivial, the joint is the product of the marginals.
|
|
191
|
+
|
|
192
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/conditional_independence.svg" alt="Conditional independence X ⊥ Y | Z and conditional_product">
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
forecast = ['Sunny', 'Rainy']
|
|
196
|
+
weather = ['Dry', 'Wet']
|
|
197
|
+
umbrella = ['Leave', 'Take']
|
|
198
|
+
p_wf = FXTensor([[], [weather, forecast]], data=np.array([
|
|
199
|
+
[0.45, 0.05], # Dry
|
|
200
|
+
[0.05, 0.45], # Wet
|
|
201
|
+
]))
|
|
202
|
+
p_uf = FXTensor([[], [umbrella, forecast]], data=np.array([
|
|
203
|
+
[0.40, 0.10], # Leave
|
|
204
|
+
[0.10, 0.40], # Take
|
|
205
|
+
]))
|
|
206
|
+
joint = FXTensor.conditional_product(p_wf, p_uf, 2)
|
|
207
|
+
assert joint.labels == (None, [weather, umbrella, forecast])
|
|
208
|
+
assert joint.is_conditionally_independent(2, 3)
|
|
209
|
+
weather_umbrella = joint.marginalization(3)
|
|
210
|
+
assert not weather_umbrella.is_independent(2)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Exchangeability: a coin with an unknown bias
|
|
214
|
+
|
|
215
|
+
A prior on the bias of a coin makes the sequence of flips exchangeable. The flips are not independent, unless that prior is a point mass.
|
|
216
|
+
|
|
217
|
+
A state `p: I → X^{⊗n}` is exchangeable at position `k` (1-based) when swapping the `k`-th and `(k+1)`-th wires leaves it unchanged: `p ; (id^{⊗(k-1)} ⊗ swap(X,X) ⊗ id^{⊗(n-k-1)}) = p`, with `(k-1) + 2 + (n-k-1) = n`. A sequence `p_n: I → X^{⊗n}` is Kolmogorov consistent when discarding the `n`-th wire recovers `p_{n-1}`: `p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}`.
|
|
218
|
+
|
|
219
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/exchangeability.svg" alt="p ; (id^{⊗(k-1)} ⊗ swap(X,X) ⊗ id^{⊗(n-k-1)}) = p">
|
|
220
|
+
|
|
221
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/kolmogorov_consistency.svg" alt="p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}">
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
bias = ["fair", "heads"]
|
|
225
|
+
side = ["H", "T"]
|
|
226
|
+
prior = FXTensor([[], [bias]], data=np.array([0.5, 0.5]))
|
|
227
|
+
kernel = FXTensor(
|
|
228
|
+
[[bias], [side]],
|
|
229
|
+
data=np.array([
|
|
230
|
+
[0.5, 0.5], # fair
|
|
231
|
+
[0.9, 0.1], # heads
|
|
232
|
+
]),
|
|
233
|
+
)
|
|
234
|
+
flips = FXTensor.mixture_of_iid(prior, kernel, 3)
|
|
235
|
+
assert flips.labels == (None, [side, side, side])
|
|
236
|
+
assert flips.is_exchangeable()
|
|
237
|
+
sequence = [FXTensor.mixture_of_iid(prior, kernel, n) for n in range(1, 5)]
|
|
238
|
+
assert FXTensor.is_kolmogorov_consistent(sequence)
|
|
239
|
+
pair = flips.marginalization(3)
|
|
240
|
+
assert not pair.is_independent(2)
|
|
241
|
+
|
|
242
|
+
point = FXTensor([[], [bias]], data=np.array([0.0, 1.0]))
|
|
243
|
+
fixed = FXTensor.mixture_of_iid(point, kernel, 3)
|
|
244
|
+
assert fixed.is_independent(2)
|
|
245
|
+
```
|
|
246
|
+
|
|
176
247
|
## Advanced Example: Multidimensional System (Labeled)
|
|
177
248
|
|
|
178
249
|
Model **Season** (Spring, Summer, Other) and **Weather** (Sunny, Rainy) given **Location** (Urban, Rural).
|
|
@@ -398,6 +469,14 @@ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, ea
|
|
|
398
469
|
| `partial_composition` | Compose only some output wires | Keep a prefix of the outputs and feed the suffix into another kernel |
|
|
399
470
|
| `jointification` | Joint of two states | Combine two independent states (states only) |
|
|
400
471
|
| `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
|
|
472
|
+
| `is_conditionally_independent` | `X ⊥ Y` given `Z`, codomain `X ⊗ Y ⊗ Z` | Test a state or kernel against the copy–discard equation |
|
|
473
|
+
| `conditional_product` | Joint on `X ⊗ Y ⊗ Z` from joints on `X ⊗ Z` and `Y ⊗ Z` | Build a conditionally independent joint (states or kernels that share a domain) |
|
|
474
|
+
| `is_independent` | `X ⊥ Y` when `Z` is empty | Special case of conditional independence |
|
|
475
|
+
| `is_exchangeable` | Invariance of `I → X^{⊗n}` under adjacent swaps | Test whether swapping the `k`-th and `(k+1)`-th factors leaves a state unchanged |
|
|
476
|
+
| `symmetrize` | Average over permutations of the factors | Turn a joint state into an exchangeable one |
|
|
477
|
+
| `is_kolmogorov_consistent` | `p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}` | Check that discarding the `n`-th wire recovers the previous state |
|
|
478
|
+
| `iid` | `p ⊗ … ⊗ p` | Build an i.i.d. state or kernel |
|
|
479
|
+
| `mixture_of_iid` | `prior ; copy_Θ^{(n)} ; kernel^{⊗n}` | Mix i.i.d. sequences (exchangeable, and independent only for a Dirac prior) |
|
|
401
480
|
| `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
|
|
402
481
|
| `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
|
|
403
482
|
| `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
|
|
@@ -554,3 +633,5 @@ pytest
|
|
|
554
633
|
- [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
|
|
555
634
|
- [4] [T. Fritz, A synthetic approach to Markov kernels, conditional independence and theorems on sufficient statistics (arXiv:1908.07021)](https://arxiv.org/abs/1908.07021)
|
|
556
635
|
- [5] [T. Fritz, T. Gonda, A. Lorenzin, P. Perrone, D. Stein, Absolute continuity, supports and idempotent splitting in categorical probability (arXiv:2308.00651)](https://arxiv.org/abs/2308.00651)
|
|
636
|
+
- [6] [T. Fritz, T. Gonda, P. Perrone, De Finetti's theorem in categorical probability (arXiv:2105.02639)](https://arxiv.org/abs/2105.02639)
|
|
637
|
+
- [7] [F. Vaccarino, Statistical models as natural transformations (arXiv:2609.22929)](https://arxiv.org/abs/2609.22929)
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
FXTensor is a Python library for tensor-based computations, particularly suited for modeling probabilistic systems and processes inspired by category theory. It leverages NumPy for efficient numerical computations. The library primarily supports labeled indices for enhanced readability while maintaining compatibility with unlabeled numeric indices.
|
|
4
4
|
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- [English](https://salmonsnare.github.io/fxtensor-salmon/en/)
|
|
8
|
+
- [日本語](https://salmonsnare.github.io/fxtensor-salmon/ja/)
|
|
9
|
+
|
|
5
10
|
## Core Concepts
|
|
6
11
|
|
|
7
12
|
An FXTensor is defined by its `profile` and `data`, with optional string labels to make tensors more intuitive and meaningful.
|
|
@@ -144,6 +149,67 @@ Model a weather system with states “Sunny” or “Rainy.”
|
|
|
144
149
|
p_sunny = sunny_tomorrow.data[sunny_idx] # 0.8
|
|
145
150
|
```
|
|
146
151
|
|
|
152
|
+
### Conditional independence: weather, umbrella, and the forecast
|
|
153
|
+
|
|
154
|
+
The forecast drives both the weather you meet and whether you carry an umbrella. Given the forecast they are conditionally independent. The marginal on weather and umbrella still couples them.
|
|
155
|
+
|
|
156
|
+
The API orders the joint as weather ⊗ umbrella ⊗ forecast (`X ⊗ Y ⊗ Z`, with `Z` last). The figure draws the same variables as `X ⊗ Z ⊗ Y` and is read bottom to top. Left: `X ⊥ Y | Z` is the copy–discard equation. Right: `conditional_product` is `p_Z ; copy_Z ; (p(x|z) ⊗ id_Z ⊗ q(y|z))`. Bottom: when `Z` is trivial, the joint is the product of the marginals.
|
|
157
|
+
|
|
158
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/conditional_independence.svg" alt="Conditional independence X ⊥ Y | Z and conditional_product">
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
forecast = ['Sunny', 'Rainy']
|
|
162
|
+
weather = ['Dry', 'Wet']
|
|
163
|
+
umbrella = ['Leave', 'Take']
|
|
164
|
+
p_wf = FXTensor([[], [weather, forecast]], data=np.array([
|
|
165
|
+
[0.45, 0.05], # Dry
|
|
166
|
+
[0.05, 0.45], # Wet
|
|
167
|
+
]))
|
|
168
|
+
p_uf = FXTensor([[], [umbrella, forecast]], data=np.array([
|
|
169
|
+
[0.40, 0.10], # Leave
|
|
170
|
+
[0.10, 0.40], # Take
|
|
171
|
+
]))
|
|
172
|
+
joint = FXTensor.conditional_product(p_wf, p_uf, 2)
|
|
173
|
+
assert joint.labels == (None, [weather, umbrella, forecast])
|
|
174
|
+
assert joint.is_conditionally_independent(2, 3)
|
|
175
|
+
weather_umbrella = joint.marginalization(3)
|
|
176
|
+
assert not weather_umbrella.is_independent(2)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Exchangeability: a coin with an unknown bias
|
|
180
|
+
|
|
181
|
+
A prior on the bias of a coin makes the sequence of flips exchangeable. The flips are not independent, unless that prior is a point mass.
|
|
182
|
+
|
|
183
|
+
A state `p: I → X^{⊗n}` is exchangeable at position `k` (1-based) when swapping the `k`-th and `(k+1)`-th wires leaves it unchanged: `p ; (id^{⊗(k-1)} ⊗ swap(X,X) ⊗ id^{⊗(n-k-1)}) = p`, with `(k-1) + 2 + (n-k-1) = n`. A sequence `p_n: I → X^{⊗n}` is Kolmogorov consistent when discarding the `n`-th wire recovers `p_{n-1}`: `p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}`.
|
|
184
|
+
|
|
185
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/exchangeability.svg" alt="p ; (id^{⊗(k-1)} ⊗ swap(X,X) ⊗ id^{⊗(n-k-1)}) = p">
|
|
186
|
+
|
|
187
|
+
<img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/main/docs/_static/figures/kolmogorov_consistency.svg" alt="p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}">
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
bias = ["fair", "heads"]
|
|
191
|
+
side = ["H", "T"]
|
|
192
|
+
prior = FXTensor([[], [bias]], data=np.array([0.5, 0.5]))
|
|
193
|
+
kernel = FXTensor(
|
|
194
|
+
[[bias], [side]],
|
|
195
|
+
data=np.array([
|
|
196
|
+
[0.5, 0.5], # fair
|
|
197
|
+
[0.9, 0.1], # heads
|
|
198
|
+
]),
|
|
199
|
+
)
|
|
200
|
+
flips = FXTensor.mixture_of_iid(prior, kernel, 3)
|
|
201
|
+
assert flips.labels == (None, [side, side, side])
|
|
202
|
+
assert flips.is_exchangeable()
|
|
203
|
+
sequence = [FXTensor.mixture_of_iid(prior, kernel, n) for n in range(1, 5)]
|
|
204
|
+
assert FXTensor.is_kolmogorov_consistent(sequence)
|
|
205
|
+
pair = flips.marginalization(3)
|
|
206
|
+
assert not pair.is_independent(2)
|
|
207
|
+
|
|
208
|
+
point = FXTensor([[], [bias]], data=np.array([0.0, 1.0]))
|
|
209
|
+
fixed = FXTensor.mixture_of_iid(point, kernel, 3)
|
|
210
|
+
assert fixed.is_independent(2)
|
|
211
|
+
```
|
|
212
|
+
|
|
147
213
|
## Advanced Example: Multidimensional System (Labeled)
|
|
148
214
|
|
|
149
215
|
Model **Season** (Spring, Summer, Other) and **Weather** (Sunny, Rainy) given **Location** (Urban, Rural).
|
|
@@ -369,6 +435,14 @@ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, ea
|
|
|
369
435
|
| `partial_composition` | Compose only some output wires | Keep a prefix of the outputs and feed the suffix into another kernel |
|
|
370
436
|
| `jointification` | Joint of two states | Combine two independent states (states only) |
|
|
371
437
|
| `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
|
|
438
|
+
| `is_conditionally_independent` | `X ⊥ Y` given `Z`, codomain `X ⊗ Y ⊗ Z` | Test a state or kernel against the copy–discard equation |
|
|
439
|
+
| `conditional_product` | Joint on `X ⊗ Y ⊗ Z` from joints on `X ⊗ Z` and `Y ⊗ Z` | Build a conditionally independent joint (states or kernels that share a domain) |
|
|
440
|
+
| `is_independent` | `X ⊥ Y` when `Z` is empty | Special case of conditional independence |
|
|
441
|
+
| `is_exchangeable` | Invariance of `I → X^{⊗n}` under adjacent swaps | Test whether swapping the `k`-th and `(k+1)`-th factors leaves a state unchanged |
|
|
442
|
+
| `symmetrize` | Average over permutations of the factors | Turn a joint state into an exchangeable one |
|
|
443
|
+
| `is_kolmogorov_consistent` | `p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}` | Check that discarding the `n`-th wire recovers the previous state |
|
|
444
|
+
| `iid` | `p ⊗ … ⊗ p` | Build an i.i.d. state or kernel |
|
|
445
|
+
| `mixture_of_iid` | `prior ; copy_Θ^{(n)} ; kernel^{⊗n}` | Mix i.i.d. sequences (exchangeable, and independent only for a Dirac prior) |
|
|
372
446
|
| `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
|
|
373
447
|
| `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
|
|
374
448
|
| `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
|
|
@@ -525,3 +599,5 @@ pytest
|
|
|
525
599
|
- [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
|
|
526
600
|
- [4] [T. Fritz, A synthetic approach to Markov kernels, conditional independence and theorems on sufficient statistics (arXiv:1908.07021)](https://arxiv.org/abs/1908.07021)
|
|
527
601
|
- [5] [T. Fritz, T. Gonda, A. Lorenzin, P. Perrone, D. Stein, Absolute continuity, supports and idempotent splitting in categorical probability (arXiv:2308.00651)](https://arxiv.org/abs/2308.00651)
|
|
602
|
+
- [6] [T. Fritz, T. Gonda, P. Perrone, De Finetti's theorem in categorical probability (arXiv:2105.02639)](https://arxiv.org/abs/2105.02639)
|
|
603
|
+
- [7] [F. Vaccarino, Statistical models as natural transformations (arXiv:2609.22929)](https://arxiv.org/abs/2609.22929)
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "fxtensor_salmon"
|
|
10
|
-
version = "1.
|
|
10
|
+
version = "1.4.0"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name="Tadachika Oki", email="sakesnare@gmail.com" },
|
|
13
13
|
]
|
|
@@ -33,9 +33,15 @@ dev = [
|
|
|
33
33
|
discopy = [
|
|
34
34
|
"discopy>=1.2,<2",
|
|
35
35
|
]
|
|
36
|
+
docs = [
|
|
37
|
+
"sphinx>=7.4",
|
|
38
|
+
"furo>=2024.8.6",
|
|
39
|
+
"discopy>=1.2,<2; python_version>='3.10'",
|
|
40
|
+
]
|
|
36
41
|
|
|
37
42
|
[project.urls]
|
|
38
43
|
Homepage = "https://github.com/salmonsnare/fxtensor-salmon"
|
|
44
|
+
Documentation = "https://salmonsnare.github.io/fxtensor-salmon/"
|
|
39
45
|
|
|
40
46
|
[tool.setuptools.packages.find]
|
|
41
47
|
where = ["src"]
|
|
@@ -2,13 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
from typing import List, Tuple, Union, Optional
|
|
4
4
|
|
|
5
|
+
from .exchangeability import ExchangeabilityMixin
|
|
6
|
+
from .independence import IndependenceMixin
|
|
5
7
|
from .io import IOMixin
|
|
6
8
|
from .operations import OperationsMixin
|
|
7
9
|
from .profile import _constructor_profile, _parse_profile
|
|
8
10
|
from .special import SpecialMixin
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class FXTensor(IOMixin, OperationsMixin, SpecialMixin):
|
|
13
|
+
class FXTensor(IOMixin, OperationsMixin, SpecialMixin, IndependenceMixin, ExchangeabilityMixin):
|
|
12
14
|
"""A tensor with support for string-labeled dimensions and tensor operations."""
|
|
13
15
|
|
|
14
16
|
_RTOL = 1e-05
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Exchangeability and Kolmogorov consistency for finite states.
|
|
2
|
+
|
|
3
|
+
A state ``p: I → X^{⊗n}`` is exchangeable at position ``k`` (1-based) when
|
|
4
|
+
|
|
5
|
+
``p ; (id^{⊗(k-1)} ⊗ swap(X, X) ⊗ id^{⊗(n-k-1)}) = p``.
|
|
6
|
+
|
|
7
|
+
Swapping the ``k``-th and ``(k+1)``-th wires leaves ``p`` unchanged.
|
|
8
|
+
Those wires use ``(k-1) + 2 + (n-k-1) = n`` factors.
|
|
9
|
+
|
|
10
|
+
A sequence ``p_n: I → X^{⊗n}`` is Kolmogorov consistent when
|
|
11
|
+
|
|
12
|
+
``p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}``.
|
|
13
|
+
|
|
14
|
+
Discarding the ``n``-th wire of ``p_n`` yields ``p_{n-1}``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import itertools
|
|
20
|
+
import warnings
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SYMMETRIZE_WARN_ABOVE = 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _tolerances(cls, atol, rtol):
|
|
29
|
+
if atol is None:
|
|
30
|
+
atol = cls._ATOL
|
|
31
|
+
if rtol is None:
|
|
32
|
+
rtol = cls._RTOL
|
|
33
|
+
return atol, rtol
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_block(block):
|
|
37
|
+
"""Return the dimension list of one factor ``X``."""
|
|
38
|
+
if isinstance(block, tuple):
|
|
39
|
+
block = list(block)
|
|
40
|
+
if not isinstance(block, list) or not block:
|
|
41
|
+
raise ValueError("block must be a non-empty list of positive dimensions")
|
|
42
|
+
dims = []
|
|
43
|
+
for axis in block:
|
|
44
|
+
if isinstance(axis, bool) or not isinstance(axis, int) or axis <= 0:
|
|
45
|
+
raise ValueError("block must be a non-empty list of positive dimensions")
|
|
46
|
+
dims.append(axis)
|
|
47
|
+
return dims
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _factors(codomain, block):
|
|
51
|
+
"""Split a codomain into ``n`` copies of ``X``.
|
|
52
|
+
|
|
53
|
+
``block is None`` means each axis is one factor (the default of one
|
|
54
|
+
axis). Every axis must then have the same size. Otherwise ``block`` is
|
|
55
|
+
the dimension list of ``X`` and must tile the codomain.
|
|
56
|
+
"""
|
|
57
|
+
codomain = list(codomain)
|
|
58
|
+
if block is None:
|
|
59
|
+
if not codomain:
|
|
60
|
+
return [], 0
|
|
61
|
+
size = codomain[0]
|
|
62
|
+
if any(axis != size for axis in codomain):
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"codomain factors are not the same type X; pass block to group axes"
|
|
65
|
+
)
|
|
66
|
+
return [size], len(codomain)
|
|
67
|
+
dims = _parse_block(block)
|
|
68
|
+
width = len(dims)
|
|
69
|
+
if len(codomain) % width != 0:
|
|
70
|
+
raise ValueError("codomain length is not a multiple of the block")
|
|
71
|
+
count = len(codomain) // width
|
|
72
|
+
for index in range(count):
|
|
73
|
+
piece = codomain[index * width:(index + 1) * width]
|
|
74
|
+
if piece != dims:
|
|
75
|
+
raise ValueError("codomain factors are not the same type X")
|
|
76
|
+
return dims, count
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _require_state(tensor, method):
|
|
80
|
+
if tensor._profile[0]:
|
|
81
|
+
raise ValueError(f"{method} expects a state I → X^{{⊗n}}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tensor_all(cls, factors):
|
|
85
|
+
acc = factors[0]
|
|
86
|
+
for factor in factors[1:]:
|
|
87
|
+
acc = acc.tensor_product(factor)
|
|
88
|
+
return acc
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _adjacent_swap_morphism(cls, x_dims, k, n):
|
|
92
|
+
"""``id^{⊗(k-1)} ⊗ swap(X, X) ⊗ id^{⊗(n-k-1)}``.
|
|
93
|
+
|
|
94
|
+
``k`` is 1-based. The three blocks use ``(k-1) + 2 + (n-k-1) = n``
|
|
95
|
+
factors of ``X``.
|
|
96
|
+
"""
|
|
97
|
+
if not (1 <= k <= n - 1):
|
|
98
|
+
raise ValueError("swap position k is out of range")
|
|
99
|
+
factors = []
|
|
100
|
+
left = k - 1
|
|
101
|
+
right = n - k - 1
|
|
102
|
+
if left:
|
|
103
|
+
factors.append(cls.identity_tensor(list(x_dims) * left))
|
|
104
|
+
factors.append(cls.swap(list(x_dims), list(x_dims)))
|
|
105
|
+
if right:
|
|
106
|
+
factors.append(cls.identity_tensor(list(x_dims) * right))
|
|
107
|
+
return _tensor_all(cls, factors)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _discard_last_morphism(cls, x_dims, n):
|
|
111
|
+
"""``id^{⊗(n-1)} ⊗ !`` on ``X^{⊗n}``, discarding the ``n``-th wire."""
|
|
112
|
+
if n < 1:
|
|
113
|
+
raise ValueError("n must be positive")
|
|
114
|
+
factors = []
|
|
115
|
+
if n - 1:
|
|
116
|
+
factors.append(cls.identity_tensor(list(x_dims) * (n - 1)))
|
|
117
|
+
factors.append(cls.exclamation(list(x_dims)))
|
|
118
|
+
return _tensor_all(cls, factors)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _same_data(left, right, atol, rtol):
|
|
122
|
+
if left.shape != right.shape:
|
|
123
|
+
return False
|
|
124
|
+
return bool(np.allclose(left, right, rtol=rtol, atol=atol))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _permutation_axes(perm, width):
|
|
128
|
+
order = []
|
|
129
|
+
for block_index in perm:
|
|
130
|
+
start = block_index * width
|
|
131
|
+
order.extend(range(start, start + width))
|
|
132
|
+
return order
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ExchangeabilityMixin:
|
|
136
|
+
"""Mixin that adds exchangeability and Kolmogorov consistency.
|
|
137
|
+
|
|
138
|
+
States are morphisms ``I → X^{⊗n}``. ``X`` is one codomain axis unless
|
|
139
|
+
``block`` gives the dimension list of a multi-axis factor.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def is_exchangeable(self, block=None, atol=None, rtol=None):
|
|
143
|
+
"""Return whether this state is invariant under every adjacent swap.
|
|
144
|
+
|
|
145
|
+
The state ``p: I → X^{⊗n}`` is exchangeable at position ``k``
|
|
146
|
+
(1-based) when
|
|
147
|
+
|
|
148
|
+
``p ; (id^{⊗(k-1)} ⊗ swap(X, X) ⊗ id^{⊗(n-k-1)}) = p``.
|
|
149
|
+
|
|
150
|
+
Swapping the ``k``-th and ``(k+1)``-th wires leaves ``p`` unchanged.
|
|
151
|
+
The identity wires and the two swapped wires use
|
|
152
|
+
``(k-1) + 2 + (n-k-1) = n`` factors. Adjacent transpositions
|
|
153
|
+
generate the symmetric group, so this checks ``k = 1 .. n-1``.
|
|
154
|
+
``n < 2`` is vacuously true.
|
|
155
|
+
|
|
156
|
+
``block`` is the dimension list of one factor ``X``. The default,
|
|
157
|
+
``None``, takes each codomain axis as one factor; those axes must
|
|
158
|
+
share a size. ``swap`` carries no labels, so the comparison is on
|
|
159
|
+
``data`` via ``numpy.allclose``. ``atol`` and ``rtol`` default to
|
|
160
|
+
``FXTensor._ATOL`` and ``FXTensor._RTOL``.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
ValueError
|
|
165
|
+
If ``self`` is not a state, or the codomain is not ``X^{⊗n}``.
|
|
166
|
+
"""
|
|
167
|
+
_require_state(self, "is_exchangeable")
|
|
168
|
+
x_dims, count = _factors(self._profile[1], block)
|
|
169
|
+
if count < 2:
|
|
170
|
+
return True
|
|
171
|
+
atol, rtol = _tolerances(type(self), atol, rtol)
|
|
172
|
+
cls = type(self)
|
|
173
|
+
for k in range(1, count):
|
|
174
|
+
swapped = self.composition(_adjacent_swap_morphism(cls, x_dims, k, count))
|
|
175
|
+
if not _same_data(swapped.data, self.data, atol, rtol):
|
|
176
|
+
return False
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
def symmetrize(self, block=None):
|
|
180
|
+
"""Return the average of this state over every permutation of ``X``.
|
|
181
|
+
|
|
182
|
+
The result has the same profile and labels and satisfies
|
|
183
|
+
:meth:`is_exchangeable` for the same ``block``. Averaging again
|
|
184
|
+
changes nothing, and an exchangeable state is left unchanged.
|
|
185
|
+
``n > 8`` warns: the average runs through ``n!`` permutations.
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
------
|
|
189
|
+
ValueError
|
|
190
|
+
If ``self`` is not a state, or the codomain is not ``X^{⊗n}``.
|
|
191
|
+
"""
|
|
192
|
+
_require_state(self, "symmetrize")
|
|
193
|
+
x_dims, count = _factors(self._profile[1], block)
|
|
194
|
+
if count > _SYMMETRIZE_WARN_ABOVE:
|
|
195
|
+
warnings.warn(
|
|
196
|
+
f"symmetrize averages {count}! permutations; n={count} is above 8",
|
|
197
|
+
stacklevel=2,
|
|
198
|
+
)
|
|
199
|
+
if count < 2:
|
|
200
|
+
return self.copy()
|
|
201
|
+
width = len(x_dims)
|
|
202
|
+
total = np.zeros(self.data.shape, dtype=float)
|
|
203
|
+
n_perms = 0
|
|
204
|
+
for perm in itertools.permutations(range(count)):
|
|
205
|
+
total += np.transpose(self.data, _permutation_axes(perm, width))
|
|
206
|
+
n_perms += 1
|
|
207
|
+
averaged = total / n_perms
|
|
208
|
+
return self._spawn(self._profile[0], self._profile[1], self._labels, averaged)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def is_kolmogorov_consistent(cls, states, block=None, atol=None, rtol=None):
|
|
212
|
+
"""Return whether discarding the last factor reproduces the previous state.
|
|
213
|
+
|
|
214
|
+
``states`` is the list ``[p_1, p_2, ..., p_N]`` with
|
|
215
|
+
``p_n: I → X^{⊗n}`` (so ``states[0]`` is ``p_1``). The sequence is
|
|
216
|
+
Kolmogorov consistent when, for every ``n ≥ 2``,
|
|
217
|
+
|
|
218
|
+
``p_n ; (id^{⊗(n-1)} ⊗ !) = p_{n-1}``.
|
|
219
|
+
|
|
220
|
+
``!`` is :meth:`exclamation` on the last factor: discarding the
|
|
221
|
+
``n``-th wire of ``p_n`` yields ``p_{n-1}``. A list of length 0 or
|
|
222
|
+
1 is consistent. ``block`` selects ``X`` as in
|
|
223
|
+
:meth:`is_exchangeable`; the default is one axis, and
|
|
224
|
+
``states[n-1]`` must then have ``n`` axes of that size. Values are
|
|
225
|
+
compared on ``data`` with ``numpy.allclose``. ``atol`` and ``rtol``
|
|
226
|
+
default to ``FXTensor._ATOL`` and ``FXTensor._RTOL``.
|
|
227
|
+
|
|
228
|
+
Raises
|
|
229
|
+
------
|
|
230
|
+
ValueError
|
|
231
|
+
If an entry is not a state ``I → X^{⊗n}`` for that position, or
|
|
232
|
+
the factors are not a common type ``X``.
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
sequence = list(states)
|
|
236
|
+
except TypeError as exc:
|
|
237
|
+
raise ValueError("states must be a sequence of FXTensor states") from exc
|
|
238
|
+
for state in sequence:
|
|
239
|
+
if not isinstance(state, cls):
|
|
240
|
+
raise ValueError("states must be a sequence of FXTensor states")
|
|
241
|
+
_require_state(state, "is_kolmogorov_consistent")
|
|
242
|
+
if not sequence:
|
|
243
|
+
return True
|
|
244
|
+
parsed = [_factors(state._profile[1], block) for state in sequence]
|
|
245
|
+
x_dims = parsed[0][0]
|
|
246
|
+
if not x_dims:
|
|
247
|
+
raise ValueError("codomain must be X^{⊗n} for a non-empty X")
|
|
248
|
+
for index, (dims, count) in enumerate(parsed):
|
|
249
|
+
n = index + 1
|
|
250
|
+
if dims != x_dims or count != n:
|
|
251
|
+
raise ValueError(f"states[{index}] must be a state I → X^{{⊗{n}}}")
|
|
252
|
+
if len(sequence) < 2:
|
|
253
|
+
return True
|
|
254
|
+
atol, rtol = _tolerances(cls, atol, rtol)
|
|
255
|
+
for n in range(2, len(sequence) + 1):
|
|
256
|
+
discarded = sequence[n - 1].composition(
|
|
257
|
+
_discard_last_morphism(cls, x_dims, n)
|
|
258
|
+
)
|
|
259
|
+
if not _same_data(discarded.data, sequence[n - 2].data, atol, rtol):
|
|
260
|
+
return False
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def iid(cls, p, n):
|
|
265
|
+
"""Return the tensor product of ``n`` copies of ``p``.
|
|
266
|
+
|
|
267
|
+
Labels are taken from ``p``. For a state ``I → X`` the result is
|
|
268
|
+
``I → X^{⊗n}``. ``n=1`` returns a copy of ``p``.
|
|
269
|
+
|
|
270
|
+
Raises
|
|
271
|
+
------
|
|
272
|
+
ValueError
|
|
273
|
+
If ``p`` is not an ``FXTensor``, or ``n`` is not a positive integer.
|
|
274
|
+
"""
|
|
275
|
+
if not isinstance(p, cls):
|
|
276
|
+
raise ValueError("p must be an FXTensor")
|
|
277
|
+
if isinstance(n, bool) or not isinstance(n, int) or n < 1:
|
|
278
|
+
raise ValueError("n must be a positive integer")
|
|
279
|
+
result = p.copy()
|
|
280
|
+
for _ in range(n - 1):
|
|
281
|
+
result = result.tensor_product(p)
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
def mixture_of_iid(cls, prior, kernel, n):
|
|
286
|
+
"""Return ``prior ; copy_Θ^{(n)} ; kernel^{⊗n}``.
|
|
287
|
+
|
|
288
|
+
``prior`` is a state ``I → Θ`` and ``kernel`` is a kernel
|
|
289
|
+
``Θ → X``. The result is a state ``I → X^{⊗n}``: one draw from the
|
|
290
|
+
prior, copied, then ``n`` independent samples from the kernel. It is
|
|
291
|
+
exchangeable and, as ``n`` varies, Kolmogorov consistent. Codomain
|
|
292
|
+
labels are the codomain labels of ``kernel``, repeated ``n`` times.
|
|
293
|
+
|
|
294
|
+
Raises
|
|
295
|
+
------
|
|
296
|
+
ValueError
|
|
297
|
+
If ``prior`` is not a state, the profiles do not meet, or ``n``
|
|
298
|
+
is not a positive integer.
|
|
299
|
+
"""
|
|
300
|
+
if not isinstance(prior, cls) or not isinstance(kernel, cls):
|
|
301
|
+
raise ValueError("prior and kernel must be FXTensor values")
|
|
302
|
+
if prior._profile[0]:
|
|
303
|
+
raise ValueError("prior must be a state I → Θ")
|
|
304
|
+
if list(prior._profile[1]) != list(kernel._profile[0]):
|
|
305
|
+
raise ValueError("prior codomain must match the domain of kernel")
|
|
306
|
+
if isinstance(n, bool) or not isinstance(n, int) or n < 1:
|
|
307
|
+
raise ValueError("n must be a positive integer")
|
|
308
|
+
if prior._labels is not None and prior._labels[1]:
|
|
309
|
+
theta = prior._labels[1]
|
|
310
|
+
else:
|
|
311
|
+
theta = list(prior._profile[1])
|
|
312
|
+
copied = prior.composition(cls.copy_tensor(theta, n))
|
|
313
|
+
kernels = kernel.copy()
|
|
314
|
+
for _ in range(n - 1):
|
|
315
|
+
kernels = kernels.tensor_product(kernel)
|
|
316
|
+
result = copied.composition(kernels)
|
|
317
|
+
if (
|
|
318
|
+
result._labels is None
|
|
319
|
+
and kernel._labels is not None
|
|
320
|
+
and kernel._labels[1]
|
|
321
|
+
):
|
|
322
|
+
result = result._spawn(
|
|
323
|
+
result._profile[0],
|
|
324
|
+
result._profile[1],
|
|
325
|
+
([], list(kernel._labels[1]) * n),
|
|
326
|
+
result.data,
|
|
327
|
+
)
|
|
328
|
+
return result
|