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.
Files changed (28) hide show
  1. {fxtensor_salmon-1.2.2/src/fxtensor_salmon.egg-info → fxtensor_salmon-1.4.0}/PKG-INFO +82 -1
  2. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/README.md +76 -0
  3. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/pyproject.toml +7 -1
  4. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/core.py +3 -1
  5. fxtensor_salmon-1.4.0/src/fxtensor_salmon/exchangeability.py +328 -0
  6. fxtensor_salmon-1.4.0/src/fxtensor_salmon/independence.py +284 -0
  7. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0/src/fxtensor_salmon.egg-info}/PKG-INFO +82 -1
  8. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/SOURCES.txt +5 -0
  9. fxtensor_salmon-1.4.0/src/fxtensor_salmon.egg-info/requires.txt +15 -0
  10. fxtensor_salmon-1.4.0/tests/test_doc_examples.py +17 -0
  11. fxtensor_salmon-1.4.0/tests/test_exchangeability.py +187 -0
  12. fxtensor_salmon-1.4.0/tests/test_independence.py +212 -0
  13. fxtensor_salmon-1.2.2/src/fxtensor_salmon.egg-info/requires.txt +0 -8
  14. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/LICENSE +0 -0
  15. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/setup.cfg +0 -0
  16. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/__init__.py +0 -0
  17. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/discopy.py +0 -0
  18. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/io.py +0 -0
  19. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/operations.py +0 -0
  20. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/profile.py +0 -0
  21. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon/special.py +0 -0
  22. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/dependency_links.txt +0 -0
  23. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/src/fxtensor_salmon.egg-info/top_level.txt +0 -0
  24. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_almost_surely.py +0 -0
  25. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_axioms.py +0 -0
  26. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_discopy.py +0 -0
  27. {fxtensor_salmon-1.2.2 → fxtensor_salmon-1.4.0}/tests/test_fxtensor.py +0 -0
  28. {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.2.2
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.2.2"
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