fxtensor-salmon 1.2.0__tar.gz → 1.2.2__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 (22) hide show
  1. {fxtensor_salmon-1.2.0/src/fxtensor_salmon.egg-info → fxtensor_salmon-1.2.2}/PKG-INFO +42 -1
  2. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/README.md +41 -0
  3. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/pyproject.toml +1 -1
  4. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/operations.py +50 -0
  5. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2/src/fxtensor_salmon.egg-info}/PKG-INFO +42 -1
  6. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon.egg-info/SOURCES.txt +1 -0
  7. fxtensor_salmon-1.2.2/tests/test_almost_surely.py +158 -0
  8. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/LICENSE +0 -0
  9. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/setup.cfg +0 -0
  10. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/__init__.py +0 -0
  11. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/core.py +0 -0
  12. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/discopy.py +0 -0
  13. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/io.py +0 -0
  14. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/profile.py +0 -0
  15. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon/special.py +0 -0
  16. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon.egg-info/dependency_links.txt +0 -0
  17. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon.egg-info/requires.txt +0 -0
  18. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/src/fxtensor_salmon.egg-info/top_level.txt +0 -0
  19. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/tests/test_axioms.py +0 -0
  20. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/tests/test_discopy.py +0 -0
  21. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/tests/test_fxtensor.py +0 -0
  22. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.2}/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.0
3
+ Version: 1.2.2
4
4
  Summary: Markov Tensor Module
5
5
  Author-email: Tadachika Oki <sakesnare@gmail.com>
6
6
  License: Copyright 2025 Tadachika Oki
@@ -371,6 +371,22 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
371
371
  - **Objects**: State spaces, represented in `FXTensor` as `domain` or `codomain` (e.g., `[['Urban', 'Rural']]` or `[[2]]`).
372
372
  - **Morphisms**: Markov kernels (probabilistic transitions), represented by `FXTensor` instances with profile and data.
373
373
 
374
+ ### String diagrams
375
+
376
+ Diagrams are read top to bottom (inputs at the top, outputs at the bottom). The drawing conventions are referenced from T. Fritz (arXiv:1908.07021), implemented independently in TikZ. Boxes are morphisms, triangles are states `I → X`, a black dot branching downwards is `copy`, and a wire ending in a black dot is discard `!`. Sources are in [`docs/diagrams/`](https://github.com/salmonsnare/fxtensor-salmon/tree/v1.2.2/docs/diagrams/).
377
+
378
+ Copy is coassociative, counital and cocommutative:
379
+
380
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_associativity.svg" alt="coassociativity">
381
+
382
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_counitality.svg" alt="counitality">
383
+
384
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_commutativity.svg" alt="cocommutativity">
385
+
386
+ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, each row of a kernel sums to 1:
387
+
388
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/discard_naturality.svg" alt="f ; ! = !">
389
+
374
390
  ### Markov Category Operations
375
391
 
376
392
  | Method | Role | When to use |
@@ -383,6 +399,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
383
399
  | `jointification` | Joint of two states | Combine two independent states (states only) |
384
400
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
385
401
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
402
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
403
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
386
404
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
387
405
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
388
406
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -396,8 +414,12 @@ result = tensor1.composition(tensor2)
396
414
  assert np.allclose(result.data, [[0.78, 0.22], [0.54, 0.46]])
397
415
  ```
398
416
 
417
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/composition.svg" alt="f ; g">
418
+
399
419
  #### `tensor_product` — independent systems in parallel
400
420
 
421
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/tensor_product.svg" alt="f ⊗ g">
422
+
401
423
  Use to put two morphisms (or two states) next to each other without coupling.
402
424
 
403
425
  ```python
@@ -471,6 +493,8 @@ assert np.allclose(joint_xy.data, [[0.06, 0.09, 0.15], [0.14, 0.21, 0.35]])
471
493
 
472
494
  #### `bayesian_inversion` — posterior kernel
473
495
 
496
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/bayesian_inversion.svg" alt="p;copy;(id⊗f) = p;f;copy;(f†⊗id)">
497
+
474
498
  For a prior `p: I → X` and a kernel `f: X → Y`, `f.bayesian_inversion(p)` returns `f†: Y → X` with `f†(x|y) = p(x) f(y|x) / Σ p(x') f(y|x')` (Cho & Jacobs). It satisfies `p ; copy ; (id ⊗ f) = p ; f ; copy ; (f† ⊗ id)`. Outputs with zero evidence stay zero.
475
499
 
476
500
  ```python
@@ -494,6 +518,22 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
494
518
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
495
519
  ```
496
520
 
521
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
522
+
523
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/almost_surely_equal.svg" alt="p;copy;(id⊗f) = p;copy;(id⊗g)">
524
+
525
+ `support()` returns the 0/1 indicator of nonzero entries (same profile and labels). `f.almost_surely_equal(g, p)` checks Fritz's `p ; copy ; (id ⊗ f) = p ; copy ; (id ⊗ g)`, i.e. `f` and `g` agree on the support of `p`. `p.is_absolutely_continuous(q)` checks `p ≪ q` (zero in `q` implies zero in `p`; input-wise for kernels, cf. arXiv:2308.00651). Zero is judged with `_ATOL`; labels are not compared.
526
+
527
+ ```python
528
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
529
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
530
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
531
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
532
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
533
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
534
+ assert not g.is_absolutely_continuous(f)
535
+ ```
536
+
497
537
  ### Probabilistic Properties
498
538
 
499
539
  - `is_markov()`: Verifies if the tensor satisfies the normalization condition (sum of outputs equals 1 or 0). States (empty domain) always return `False`; check `np.isclose(state.data.sum(), 1)` for a normalized state.
@@ -513,3 +553,4 @@ pytest
513
553
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
514
554
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
515
555
  - [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
+ - [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)
@@ -342,6 +342,22 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
342
342
  - **Objects**: State spaces, represented in `FXTensor` as `domain` or `codomain` (e.g., `[['Urban', 'Rural']]` or `[[2]]`).
343
343
  - **Morphisms**: Markov kernels (probabilistic transitions), represented by `FXTensor` instances with profile and data.
344
344
 
345
+ ### String diagrams
346
+
347
+ Diagrams are read top to bottom (inputs at the top, outputs at the bottom). The drawing conventions are referenced from T. Fritz (arXiv:1908.07021), implemented independently in TikZ. Boxes are morphisms, triangles are states `I → X`, a black dot branching downwards is `copy`, and a wire ending in a black dot is discard `!`. Sources are in [`docs/diagrams/`](https://github.com/salmonsnare/fxtensor-salmon/tree/v1.2.2/docs/diagrams/).
348
+
349
+ Copy is coassociative, counital and cocommutative:
350
+
351
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_associativity.svg" alt="coassociativity">
352
+
353
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_counitality.svg" alt="counitality">
354
+
355
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_commutativity.svg" alt="cocommutativity">
356
+
357
+ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, each row of a kernel sums to 1:
358
+
359
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/discard_naturality.svg" alt="f ; ! = !">
360
+
345
361
  ### Markov Category Operations
346
362
 
347
363
  | Method | Role | When to use |
@@ -354,6 +370,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
354
370
  | `jointification` | Joint of two states | Combine two independent states (states only) |
355
371
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
356
372
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
373
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
374
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
357
375
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
358
376
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
359
377
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -367,8 +385,12 @@ result = tensor1.composition(tensor2)
367
385
  assert np.allclose(result.data, [[0.78, 0.22], [0.54, 0.46]])
368
386
  ```
369
387
 
388
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/composition.svg" alt="f ; g">
389
+
370
390
  #### `tensor_product` — independent systems in parallel
371
391
 
392
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/tensor_product.svg" alt="f ⊗ g">
393
+
372
394
  Use to put two morphisms (or two states) next to each other without coupling.
373
395
 
374
396
  ```python
@@ -442,6 +464,8 @@ assert np.allclose(joint_xy.data, [[0.06, 0.09, 0.15], [0.14, 0.21, 0.35]])
442
464
 
443
465
  #### `bayesian_inversion` — posterior kernel
444
466
 
467
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/bayesian_inversion.svg" alt="p;copy;(id⊗f) = p;f;copy;(f†⊗id)">
468
+
445
469
  For a prior `p: I → X` and a kernel `f: X → Y`, `f.bayesian_inversion(p)` returns `f†: Y → X` with `f†(x|y) = p(x) f(y|x) / Σ p(x') f(y|x')` (Cho & Jacobs). It satisfies `p ; copy ; (id ⊗ f) = p ; f ; copy ; (f† ⊗ id)`. Outputs with zero evidence stay zero.
446
470
 
447
471
  ```python
@@ -465,6 +489,22 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
465
489
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
466
490
  ```
467
491
 
492
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
493
+
494
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/almost_surely_equal.svg" alt="p;copy;(id⊗f) = p;copy;(id⊗g)">
495
+
496
+ `support()` returns the 0/1 indicator of nonzero entries (same profile and labels). `f.almost_surely_equal(g, p)` checks Fritz's `p ; copy ; (id ⊗ f) = p ; copy ; (id ⊗ g)`, i.e. `f` and `g` agree on the support of `p`. `p.is_absolutely_continuous(q)` checks `p ≪ q` (zero in `q` implies zero in `p`; input-wise for kernels, cf. arXiv:2308.00651). Zero is judged with `_ATOL`; labels are not compared.
497
+
498
+ ```python
499
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
500
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
501
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
502
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
503
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
504
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
505
+ assert not g.is_absolutely_continuous(f)
506
+ ```
507
+
468
508
  ### Probabilistic Properties
469
509
 
470
510
  - `is_markov()`: Verifies if the tensor satisfies the normalization condition (sum of outputs equals 1 or 0). States (empty domain) always return `False`; check `np.isclose(state.data.sum(), 1)` for a normalized state.
@@ -484,3 +524,4 @@ pytest
484
524
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
485
525
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
486
526
  - [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
+ - [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)
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "fxtensor_salmon"
10
- version = "1.2.0"
10
+ version = "1.2.2"
11
11
  authors = [
12
12
  { name="Tadachika Oki", email="sakesnare@gmail.com" },
13
13
  ]
@@ -152,6 +152,56 @@ class OperationsMixin:
152
152
  one = np.isclose(rows, 1, rtol=rtol, atol=atol)
153
153
  return bool(np.all(zero | one) and np.all(one.sum(axis=1) == 1))
154
154
 
155
+ def support(self) -> 'FXTensor':
156
+ """Indicator tensor of the nonzero entries (same profile and labels).
157
+
158
+ For a state ``p: I → X`` this is the support ``{x : p(x) > 0}``; for a
159
+ kernel ``f: A → X`` it is the support relation ``{(a, x) : f(x|a) > 0}``.
160
+ Entries within ``_ATOL`` of zero count as zero.
161
+ """
162
+ rtol = type(self)._RTOL
163
+ atol = type(self)._ATOL
164
+ mask = ~np.isclose(self.data, 0, rtol=rtol, atol=atol)
165
+ return self._spawn(self._profile[0], self._profile[1], self._labels, mask.astype(float))
166
+
167
+ def almost_surely_equal(self, other: 'FXTensor', prior: 'FXTensor') -> bool:
168
+ """Check ``self = other`` ``prior``-almost surely.
169
+
170
+ For ``f, g: X → Y`` and a state ``p: I → X`` this is Fritz's
171
+ ``p ; copy ; (id ⊗ f) = p ; copy ; (id ⊗ g)``; for finite tensors it
172
+ means ``f(·|x) = g(·|x)`` for every ``x`` with ``p(x) > 0``. Only
173
+ profiles are compared, not labels.
174
+ """
175
+ if self._profile != other._profile:
176
+ raise ValueError("Tensors must have the same profile")
177
+ if prior._profile[0]:
178
+ raise ValueError("prior must be a state (empty domain)")
179
+ if prior._profile[1] != self._profile[0]:
180
+ raise ValueError("prior codomain must match the domain of self")
181
+ weights = prior.data.reshape(self._profile[0] + [1] * len(self._profile[1]))
182
+ return bool(np.allclose(
183
+ weights * self.data,
184
+ weights * other.data,
185
+ rtol=type(self)._RTOL,
186
+ atol=type(self)._ATOL,
187
+ ))
188
+
189
+ def is_absolutely_continuous(self, other: 'FXTensor') -> bool:
190
+ """Check ``self ≪ other``: wherever ``other`` is zero, ``self`` is zero.
191
+
192
+ For states this is the usual absolute continuity of distributions;
193
+ for kernels ``f, g: A → X`` it holds input-wise, i.e. ``f(·|a) ≪ g(·|a)``
194
+ for every ``a`` (finite case of Fritz et al., arXiv:2308.00651).
195
+ Only profiles are compared, not labels.
196
+ """
197
+ if self._profile != other._profile:
198
+ raise ValueError("Tensors must have the same profile")
199
+ rtol = type(self)._RTOL
200
+ atol = type(self)._ATOL
201
+ other_zero = np.isclose(other.data, 0, rtol=rtol, atol=atol)
202
+ self_zero = np.isclose(self.data, 0, rtol=rtol, atol=atol)
203
+ return bool(np.all(self_zero | ~other_zero))
204
+
155
205
  def marginalization(self, start_B: int) -> 'FXTensor':
156
206
  """Marginalize out a part of the codomain by summing over it."""
157
207
  domain_len = len(self._profile[0])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fxtensor_salmon
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: Markov Tensor Module
5
5
  Author-email: Tadachika Oki <sakesnare@gmail.com>
6
6
  License: Copyright 2025 Tadachika Oki
@@ -371,6 +371,22 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
371
371
  - **Objects**: State spaces, represented in `FXTensor` as `domain` or `codomain` (e.g., `[['Urban', 'Rural']]` or `[[2]]`).
372
372
  - **Morphisms**: Markov kernels (probabilistic transitions), represented by `FXTensor` instances with profile and data.
373
373
 
374
+ ### String diagrams
375
+
376
+ Diagrams are read top to bottom (inputs at the top, outputs at the bottom). The drawing conventions are referenced from T. Fritz (arXiv:1908.07021), implemented independently in TikZ. Boxes are morphisms, triangles are states `I → X`, a black dot branching downwards is `copy`, and a wire ending in a black dot is discard `!`. Sources are in [`docs/diagrams/`](https://github.com/salmonsnare/fxtensor-salmon/tree/v1.2.2/docs/diagrams/).
377
+
378
+ Copy is coassociative, counital and cocommutative:
379
+
380
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_associativity.svg" alt="coassociativity">
381
+
382
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_counitality.svg" alt="counitality">
383
+
384
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_commutativity.svg" alt="cocommutativity">
385
+
386
+ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, each row of a kernel sums to 1:
387
+
388
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/discard_naturality.svg" alt="f ; ! = !">
389
+
374
390
  ### Markov Category Operations
375
391
 
376
392
  | Method | Role | When to use |
@@ -383,6 +399,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
383
399
  | `jointification` | Joint of two states | Combine two independent states (states only) |
384
400
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
385
401
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
402
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
403
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
386
404
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
387
405
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
388
406
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -396,8 +414,12 @@ result = tensor1.composition(tensor2)
396
414
  assert np.allclose(result.data, [[0.78, 0.22], [0.54, 0.46]])
397
415
  ```
398
416
 
417
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/composition.svg" alt="f ; g">
418
+
399
419
  #### `tensor_product` — independent systems in parallel
400
420
 
421
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/tensor_product.svg" alt="f ⊗ g">
422
+
401
423
  Use to put two morphisms (or two states) next to each other without coupling.
402
424
 
403
425
  ```python
@@ -471,6 +493,8 @@ assert np.allclose(joint_xy.data, [[0.06, 0.09, 0.15], [0.14, 0.21, 0.35]])
471
493
 
472
494
  #### `bayesian_inversion` — posterior kernel
473
495
 
496
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/bayesian_inversion.svg" alt="p;copy;(id⊗f) = p;f;copy;(f†⊗id)">
497
+
474
498
  For a prior `p: I → X` and a kernel `f: X → Y`, `f.bayesian_inversion(p)` returns `f†: Y → X` with `f†(x|y) = p(x) f(y|x) / Σ p(x') f(y|x')` (Cho & Jacobs). It satisfies `p ; copy ; (id ⊗ f) = p ; f ; copy ; (f† ⊗ id)`. Outputs with zero evidence stay zero.
475
499
 
476
500
  ```python
@@ -494,6 +518,22 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
494
518
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
495
519
  ```
496
520
 
521
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
522
+
523
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/almost_surely_equal.svg" alt="p;copy;(id⊗f) = p;copy;(id⊗g)">
524
+
525
+ `support()` returns the 0/1 indicator of nonzero entries (same profile and labels). `f.almost_surely_equal(g, p)` checks Fritz's `p ; copy ; (id ⊗ f) = p ; copy ; (id ⊗ g)`, i.e. `f` and `g` agree on the support of `p`. `p.is_absolutely_continuous(q)` checks `p ≪ q` (zero in `q` implies zero in `p`; input-wise for kernels, cf. arXiv:2308.00651). Zero is judged with `_ATOL`; labels are not compared.
526
+
527
+ ```python
528
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
529
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
530
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
531
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
532
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
533
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
534
+ assert not g.is_absolutely_continuous(f)
535
+ ```
536
+
497
537
  ### Probabilistic Properties
498
538
 
499
539
  - `is_markov()`: Verifies if the tensor satisfies the normalization condition (sum of outputs equals 1 or 0). States (empty domain) always return `False`; check `np.isclose(state.data.sum(), 1)` for a normalized state.
@@ -513,3 +553,4 @@ pytest
513
553
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
514
554
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
515
555
  - [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
+ - [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)
@@ -13,6 +13,7 @@ src/fxtensor_salmon.egg-info/SOURCES.txt
13
13
  src/fxtensor_salmon.egg-info/dependency_links.txt
14
14
  src/fxtensor_salmon.egg-info/requires.txt
15
15
  src/fxtensor_salmon.egg-info/top_level.txt
16
+ tests/test_almost_surely.py
16
17
  tests/test_axioms.py
17
18
  tests/test_discopy.py
18
19
  tests/test_fxtensor.py
@@ -0,0 +1,158 @@
1
+ """Tests for support, almost_surely_equal and is_absolutely_continuous.
2
+
3
+ References: T. Fritz (arXiv:1908.07021), almost sure equality;
4
+ T. Fritz, T. Gonda, A. Lorenzin, P. Perrone, D. Stein, "Absolute continuity,
5
+ supports and idempotent splitting in categorical probability" (arXiv:2308.00651).
6
+ """
7
+ import numpy as np
8
+ import pytest
9
+
10
+ from fxtensor_salmon import FXTensor
11
+
12
+ from test_axioms import random_kernel
13
+
14
+ X = ["x0", "x1", "x2"]
15
+ Y = ["y0", "y1"]
16
+
17
+
18
+ def joint(prior, f):
19
+ """prior ; copy ; (id ⊗ f)"""
20
+ x = prior.profile[1]
21
+ return prior.composition(FXTensor.copy_tensor(x)).composition(
22
+ FXTensor.identity_tensor(x).tensor_product(f)
23
+ )
24
+
25
+
26
+ @pytest.fixture
27
+ def prior():
28
+ # x2 has probability zero
29
+ return FXTensor([[], [X]], data=np.array([0.4, 0.6, 0.0]))
30
+
31
+
32
+ @pytest.fixture
33
+ def f():
34
+ return FXTensor([[X], [Y]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
35
+
36
+
37
+ @pytest.fixture
38
+ def g():
39
+ # differs from f only on the null point x2
40
+ return FXTensor([[X], [Y]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
41
+
42
+
43
+ class TestSupport:
44
+ def test_state(self, prior):
45
+ s = prior.support()
46
+ assert s.labels == (None, [X])
47
+ assert np.array_equal(s.data, [1.0, 1.0, 0.0])
48
+
49
+ def test_kernel_relation(self, f):
50
+ s = f.support()
51
+ assert s.profile == f.profile
52
+ assert s.labels == f.labels
53
+ assert np.array_equal(s.data, [[1, 1], [1, 1], [1, 0]])
54
+
55
+ def test_tolerance(self):
56
+ p = FXTensor([[], [2]], data=np.array([1 - 1e-12, 1e-12]))
57
+ assert np.array_equal(p.support().data, [1.0, 0.0])
58
+
59
+ def test_support_of_composition(self, prior, f):
60
+ # supp(p;f) = {y : exists x in supp(p) with f(y|x) > 0}
61
+ expected = (prior.support().data @ f.support().data) > 0
62
+ assert np.array_equal(prior.composition(f).support().data, expected.astype(float))
63
+
64
+
65
+ class TestAlmostSurelyEqual:
66
+ def test_differs_only_on_null_set(self, prior, f, g):
67
+ assert f.almost_surely_equal(g, prior)
68
+ assert f != g
69
+
70
+ def test_matches_joint_equation(self, prior, f, g):
71
+ assert joint(prior, f) == joint(prior, g)
72
+
73
+ def test_differs_on_support(self, prior, f):
74
+ h = FXTensor([[X], [Y]], data=np.array([[0.3, 0.7], [0.5, 0.5], [1.0, 0.0]]))
75
+ assert not f.almost_surely_equal(h, prior)
76
+ assert joint(prior, f) != joint(prior, h)
77
+
78
+ def test_full_support_prior_means_equality(self, f, g):
79
+ uniform = FXTensor([[], [X]], data=np.full(3, 1 / 3))
80
+ assert not f.almost_surely_equal(g, uniform)
81
+ assert f.almost_surely_equal(f.copy(), uniform)
82
+
83
+ def test_double_bayesian_inversion_is_almost_surely_equal(self, prior, g):
84
+ # with a non-full-support prior, (g†)† differs from g on x2 but agrees a.s.
85
+ inv = g.bayesian_inversion(prior)
86
+ double = inv.bayesian_inversion(prior.composition(g))
87
+ assert double != g
88
+ assert double.almost_surely_equal(g, prior)
89
+
90
+ def test_random_multi_axis(self):
91
+ p = random_kernel([], [2, 3], 40)
92
+ data = p.data.copy()
93
+ data[1, 2] = 0.0
94
+ p = FXTensor([[], [2, 3]], data=data / data.sum())
95
+ f = random_kernel([2, 3], [2], 41)
96
+ g_data = f.data.copy()
97
+ g_data[1, 2] = [0.9, 0.1]
98
+ g = FXTensor([[2, 3], [2]], data=g_data)
99
+ assert f.almost_surely_equal(g, p)
100
+ assert joint(p, f) == joint(p, g)
101
+
102
+ def test_labels_are_ignored(self, prior, f):
103
+ unlabeled = FXTensor([[3], [2]], data=f.data)
104
+ assert f.almost_surely_equal(unlabeled, prior)
105
+
106
+ def test_errors(self, prior, f):
107
+ with pytest.raises(ValueError):
108
+ f.almost_surely_equal(FXTensor([[3], [3]], data=np.eye(3)), prior)
109
+ with pytest.raises(ValueError):
110
+ f.almost_surely_equal(f, f)
111
+ with pytest.raises(ValueError):
112
+ f.almost_surely_equal(f, FXTensor([[], [2]], data=np.array([0.5, 0.5])))
113
+
114
+
115
+ class TestAbsoluteContinuity:
116
+ def test_states(self):
117
+ p = FXTensor([[], [3]], data=np.array([0.5, 0.5, 0.0]))
118
+ q = FXTensor([[], [3]], data=np.array([0.2, 0.3, 0.5]))
119
+ assert p.is_absolutely_continuous(q)
120
+ assert not q.is_absolutely_continuous(p)
121
+
122
+ def test_equivalent_to_support_inclusion(self):
123
+ rng = np.random.default_rng(50)
124
+ for _ in range(20):
125
+ a = rng.random(4) * (rng.random(4) > 0.4)
126
+ b = rng.random(4) * (rng.random(4) > 0.4)
127
+ p, q = FXTensor([[], [4]], data=a), FXTensor([[], [4]], data=b)
128
+ inclusion = np.all(p.support().data <= q.support().data)
129
+ assert p.is_absolutely_continuous(q) == inclusion
130
+
131
+ def test_kernels_inputwise(self, f, g):
132
+ assert f.is_absolutely_continuous(g)
133
+ assert not g.is_absolutely_continuous(f)
134
+
135
+ def test_reflexive_and_transitive(self):
136
+ p = FXTensor([[], [3]], data=np.array([1.0, 0.0, 0.0]))
137
+ q = FXTensor([[], [3]], data=np.array([0.5, 0.5, 0.0]))
138
+ r = FXTensor([[], [3]], data=np.full(3, 1 / 3))
139
+ assert p.is_absolutely_continuous(p)
140
+ assert p.is_absolutely_continuous(q) and q.is_absolutely_continuous(r)
141
+ assert p.is_absolutely_continuous(r)
142
+
143
+ def test_preserved_by_composition(self, f):
144
+ p = FXTensor([[], [X]], data=np.array([1.0, 0.0, 0.0]))
145
+ q = FXTensor([[], [X]], data=np.array([0.5, 0.0, 0.5]))
146
+ assert p.is_absolutely_continuous(q)
147
+ assert p.composition(f).is_absolutely_continuous(q.composition(f))
148
+
149
+ def test_almost_sure_equality_transfers(self, prior, f, g):
150
+ # if p ≪ q and f = g q-a.s., then f = g p-a.s.
151
+ p = FXTensor([[], [X]], data=np.array([1.0, 0.0, 0.0]))
152
+ assert p.is_absolutely_continuous(prior)
153
+ assert f.almost_surely_equal(g, prior)
154
+ assert f.almost_surely_equal(g, p)
155
+
156
+ def test_profile_mismatch(self):
157
+ with pytest.raises(ValueError):
158
+ FXTensor([[], [2]], data=np.ones(2)).is_absolutely_continuous(FXTensor([[], [3]], data=np.ones(3)))
File without changes