fxtensor-salmon 1.2.0__tar.gz → 1.2.1__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.1}/PKG-INFO +18 -1
  2. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/README.md +17 -0
  3. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/pyproject.toml +1 -1
  4. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/operations.py +50 -0
  5. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1/src/fxtensor_salmon.egg-info}/PKG-INFO +18 -1
  6. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon.egg-info/SOURCES.txt +1 -0
  7. fxtensor_salmon-1.2.1/tests/test_almost_surely.py +158 -0
  8. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/LICENSE +0 -0
  9. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/setup.cfg +0 -0
  10. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/__init__.py +0 -0
  11. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/core.py +0 -0
  12. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/discopy.py +0 -0
  13. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/io.py +0 -0
  14. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/profile.py +0 -0
  15. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon/special.py +0 -0
  16. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon.egg-info/dependency_links.txt +0 -0
  17. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon.egg-info/requires.txt +0 -0
  18. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/src/fxtensor_salmon.egg-info/top_level.txt +0 -0
  19. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/tests/test_axioms.py +0 -0
  20. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/tests/test_discopy.py +0 -0
  21. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/tests/test_fxtensor.py +0 -0
  22. {fxtensor_salmon-1.2.0 → fxtensor_salmon-1.2.1}/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.1
4
4
  Summary: Markov Tensor Module
5
5
  Author-email: Tadachika Oki <sakesnare@gmail.com>
6
6
  License: Copyright 2025 Tadachika Oki
@@ -383,6 +383,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
383
383
  | `jointification` | Joint of two states | Combine two independent states (states only) |
384
384
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
385
385
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
386
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
387
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
386
388
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
387
389
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
388
390
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -494,6 +496,20 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
494
496
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
495
497
  ```
496
498
 
499
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
500
+
501
+ `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.
502
+
503
+ ```python
504
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
505
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
506
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
507
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
508
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
509
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
510
+ assert not g.is_absolutely_continuous(f)
511
+ ```
512
+
497
513
  ### Probabilistic Properties
498
514
 
499
515
  - `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 +529,4 @@ pytest
513
529
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
514
530
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
515
531
  - [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)
532
+ - [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)
@@ -354,6 +354,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
354
354
  | `jointification` | Joint of two states | Combine two independent states (states only) |
355
355
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
356
356
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
357
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
358
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
357
359
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
358
360
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
359
361
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -465,6 +467,20 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
465
467
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
466
468
  ```
467
469
 
470
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
471
+
472
+ `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.
473
+
474
+ ```python
475
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
476
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
477
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
478
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
479
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
480
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
481
+ assert not g.is_absolutely_continuous(f)
482
+ ```
483
+
468
484
  ### Probabilistic Properties
469
485
 
470
486
  - `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 +500,4 @@ pytest
484
500
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
485
501
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
486
502
  - [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)
503
+ - [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.1"
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.1
4
4
  Summary: Markov Tensor Module
5
5
  Author-email: Tadachika Oki <sakesnare@gmail.com>
6
6
  License: Copyright 2025 Tadachika Oki
@@ -383,6 +383,8 @@ The `fxtensor-salmon` library is designed based on the **Markov Category**, a fr
383
383
  | `jointification` | Joint of two states | Combine two independent states (states only) |
384
384
  | `conditional` | Kernel `A → X ⊗ Y` → `A ⊗ X → Y` | Conditionalize a kernel (generalizes `conditionalization`) |
385
385
  | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
386
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
387
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
386
388
  | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
387
389
  | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
388
390
  | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
@@ -494,6 +496,20 @@ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
494
496
  assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
495
497
  ```
496
498
 
499
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
500
+
501
+ `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.
502
+
503
+ ```python
504
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
505
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
506
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
507
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
508
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
509
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
510
+ assert not g.is_absolutely_continuous(f)
511
+ ```
512
+
497
513
  ### Probabilistic Properties
498
514
 
499
515
  - `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 +529,4 @@ pytest
513
529
  - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
514
530
  - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
515
531
  - [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)
532
+ - [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