fxtensor-salmon 0.2.2__tar.gz → 1.3.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 (31) hide show
  1. fxtensor_salmon-1.3.0/PKG-INFO +596 -0
  2. fxtensor_salmon-1.3.0/README.md +562 -0
  3. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/pyproject.toml +29 -1
  4. fxtensor_salmon-1.3.0/src/fxtensor_salmon/__init__.py +3 -0
  5. fxtensor_salmon-1.3.0/src/fxtensor_salmon/core.py +114 -0
  6. fxtensor_salmon-1.3.0/src/fxtensor_salmon/discopy.py +362 -0
  7. fxtensor_salmon-1.3.0/src/fxtensor_salmon/independence.py +284 -0
  8. fxtensor_salmon-1.3.0/src/fxtensor_salmon/io.py +75 -0
  9. fxtensor_salmon-1.3.0/src/fxtensor_salmon/operations.py +289 -0
  10. fxtensor_salmon-1.3.0/src/fxtensor_salmon/profile.py +62 -0
  11. fxtensor_salmon-1.3.0/src/fxtensor_salmon/special.py +78 -0
  12. fxtensor_salmon-1.3.0/src/fxtensor_salmon.egg-info/PKG-INFO +596 -0
  13. fxtensor_salmon-1.3.0/src/fxtensor_salmon.egg-info/SOURCES.txt +23 -0
  14. fxtensor_salmon-1.3.0/src/fxtensor_salmon.egg-info/requires.txt +15 -0
  15. fxtensor_salmon-1.3.0/tests/test_almost_surely.py +158 -0
  16. fxtensor_salmon-1.3.0/tests/test_axioms.py +173 -0
  17. fxtensor_salmon-1.3.0/tests/test_discopy.py +169 -0
  18. fxtensor_salmon-1.3.0/tests/test_doc_examples.py +17 -0
  19. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/tests/test_fxtensor.py +142 -1
  20. fxtensor_salmon-1.3.0/tests/test_independence.py +212 -0
  21. fxtensor_salmon-1.3.0/tests/test_inference.py +173 -0
  22. fxtensor_salmon-0.2.2/PKG-INFO +0 -307
  23. fxtensor_salmon-0.2.2/README.md +0 -283
  24. fxtensor_salmon-0.2.2/src/fxtensor_salmon/__init__.py +0 -398
  25. fxtensor_salmon-0.2.2/src/fxtensor_salmon.egg-info/PKG-INFO +0 -307
  26. fxtensor_salmon-0.2.2/src/fxtensor_salmon.egg-info/SOURCES.txt +0 -10
  27. fxtensor_salmon-0.2.2/src/fxtensor_salmon.egg-info/requires.txt +0 -1
  28. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/LICENSE +0 -0
  29. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/setup.cfg +0 -0
  30. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/src/fxtensor_salmon.egg-info/dependency_links.txt +0 -0
  31. {fxtensor_salmon-0.2.2 → fxtensor_salmon-1.3.0}/src/fxtensor_salmon.egg-info/top_level.txt +0 -0
@@ -0,0 +1,596 @@
1
+ Metadata-Version: 2.4
2
+ Name: fxtensor_salmon
3
+ Version: 1.3.0
4
+ Summary: Markov Tensor Module
5
+ Author-email: Tadachika Oki <sakesnare@gmail.com>
6
+ License: Copyright 2025 Tadachika Oki
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
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
+
14
+ Project-URL: Homepage, https://github.com/salmonsnare/fxtensor-salmon
15
+ Project-URL: Documentation, https://salmonsnare.github.io/fxtensor-salmon/
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=1.20.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: ruff; extra == "dev"
27
+ Provides-Extra: discopy
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"
33
+ Dynamic: license-file
34
+
35
+ # FXTensor
36
+
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.
38
+
39
+ ## Documentation
40
+
41
+ - [English](https://salmonsnare.github.io/fxtensor-salmon/en/)
42
+ - [日本語](https://salmonsnare.github.io/fxtensor-salmon/ja/)
43
+
44
+ ## Core Concepts
45
+
46
+ An FXTensor is defined by its `profile` and `data`, with optional string labels to make tensors more intuitive and meaningful.
47
+
48
+ - **Profile**: A pair `[domain, codomain]` specifying the dimensions of input (domain) and output (codomain) indices. For labeled tensors, e.g., `[[['a', 'b']], [['x', 'y', 'z']]]` represents a 2x3 matrix with labeled rows and columns. For unlabeled tensors, `[[2], [3]]` specifies dimensions numerically.
49
+ - **Labels (Optional)**: String labels can be assigned to each dimension, enhancing interpretability. For example, input axis labeled `['a', 'b']` and output axis labeled `['x', 'y', 'z']`. Unlabeled tensors have `labels` set to `None`.
50
+ - **Data**: A NumPy array holding the tensor’s values. Its shape must match the total number of dimensions in the profile (`len(domain) + len(codomain)`).
51
+
52
+ ## Usage Examples
53
+
54
+ ### Basic Example: Labeled Tensor
55
+
56
+ ```python
57
+ import numpy as np
58
+ from fxtensor_salmon import FXTensor
59
+
60
+ # Create a 2x3 matrix with string labels
61
+ profile = [[['a', 'b']], [['x', 'y', 'z']]]
62
+ data = np.array([
63
+ [0.1, 0.2, 0.7], # a -> x, y, z
64
+ [0.3, 0.3, 0.4] # b -> x, y, z
65
+ ])
66
+ tensor = FXTensor(profile, data=data)
67
+
68
+ # Access elements using labels
69
+ assert tensor.get_label_index(0, 'a') == 0 # Index of label 'a' on input axis
70
+ assert tensor.get_index_label(1, 2) == 'z' # Label at index 2 on output axis
71
+ ```
72
+
73
+ ### Unlabeled Tensor
74
+
75
+ ```python
76
+ # Create a 2x3 matrix with numeric indices
77
+ profile = [[2], [3]]
78
+ data = np.array([
79
+ [0.1, 0.2, 0.7],
80
+ [0.3, 0.3, 0.4]
81
+ ])
82
+ tensor = FXTensor(profile, data=data)
83
+ assert tensor.labels == (None, None) # No labels
84
+ ```
85
+
86
+ ### Creating Tensor from Strands
87
+
88
+ ```python
89
+ # Create a tensor from labeled strands
90
+ profile = [[['a', 'b']], [['x', 'y', 'z']]]
91
+ strands = {
92
+ "[[['a']], [['x']]]": 0.1,
93
+ "[[['a']], [['y']]]": 0.2,
94
+ "[[['a']], [['z']]]": 0.7,
95
+ "[[['b']], [['x']]]": 0.3,
96
+ "[[['b']], [['y']]]": 0.3,
97
+ "[[['b']], [['z']]]": 0.4
98
+ }
99
+ tensor = FXTensor.from_strands(profile, strands)
100
+ assert tensor.labels == ([['a', 'b']], [['x', 'y', 'z']])
101
+ ```
102
+
103
+ ### Labeled Tensor Composition
104
+
105
+ ```python
106
+ # P(Y|X) where X={a,b}, Y={x,y}
107
+ tensor1 = FXTensor(
108
+ [[['a', 'b']], [['x', 'y']]],
109
+ data=np.array([
110
+ [0.2, 0.8], # a -> x, y
111
+ [0.6, 0.4] # b -> x, y
112
+ ])
113
+ )
114
+
115
+ # P(Z|Y) where Y={x,y}, Z={p,q}
116
+ tensor2 = FXTensor(
117
+ [[['x', 'y']], [['p', 'q']]],
118
+ data=np.array([
119
+ [0.3, 0.7], # x -> p, q
120
+ [0.9, 0.1] # y -> p, q
121
+ ])
122
+ )
123
+
124
+ # Composition: P(Z|X) = P(Y|X) ; P(Z|Y)
125
+ result = tensor1.composition(tensor2)
126
+ assert result.labels == ([['a', 'b']], [['p', 'q']])
127
+ assert np.allclose(result.data, [
128
+ [0.2*0.3 + 0.8*0.9, 0.2*0.7 + 0.8*0.1], # a -> p, q
129
+ [0.6*0.3 + 0.4*0.9, 0.6*0.7 + 0.4*0.1], # b -> p, q
130
+ ])
131
+ ```
132
+
133
+ ### Labeled Tensor Product
134
+
135
+ ```python
136
+ # P(X) where X={a,b}
137
+ tensor1 = FXTensor(
138
+ [[], [['a', 'b']]],
139
+ data=np.array([0.3, 0.7])
140
+ )
141
+
142
+ # P(Y) where Y={x,y,z}
143
+ tensor2 = FXTensor(
144
+ [[], [['x', 'y', 'z']]],
145
+ data=np.array([0.2, 0.3, 0.5])
146
+ )
147
+
148
+ # Tensor product: P(X,Y) = P(X) ⊗ P(Y)
149
+ result = tensor1.tensor_product(tensor2)
150
+ assert result.labels == (None, [['a', 'b'], ['x', 'y', 'z']])
151
+ assert np.allclose(result.data, np.outer(
152
+ np.array([0.3, 0.7]),
153
+ np.array([0.2, 0.3, 0.5]),
154
+ ))
155
+ ```
156
+
157
+ ## Simple Example: Weather Forecast (Labeled)
158
+
159
+ Model a weather system with states “Sunny” or “Rainy.”
160
+
161
+ - **State Tensor**: Represents today’s weather probability with labels. If today is certainly sunny, the state is `[1, 0]`.
162
+
163
+ ```python
164
+ weather_states = ['Sunny', 'Rainy']
165
+ sunny_today = FXTensor([[], [weather_states]], data=np.array([1, 0]))
166
+ ```
167
+
168
+ - **Process Tensor**: Represents a weather forecast as a labeled Markov kernel.
169
+
170
+ ```python
171
+ forecast_matrix = np.array([
172
+ [0.8, 0.2], # Sunny -> Sunny: 0.8, Rainy: 0.2
173
+ [0.4, 0.6] # Rainy -> Sunny: 0.4, Rainy: 0.6
174
+ ])
175
+ forecast_tensor = FXTensor([[weather_states], [weather_states]], data=forecast_matrix)
176
+ ```
177
+
178
+ - **Composition**: Predict tomorrow’s weather by composing today’s state with the forecast.
179
+
180
+ ```python
181
+ sunny_tomorrow = sunny_today.composition(forecast_tensor)
182
+ sunny_idx = sunny_tomorrow.get_label_index(0, 'Sunny')
183
+ p_sunny = sunny_tomorrow.data[sunny_idx] # 0.8
184
+ ```
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
+ ## Advanced Example: Multidimensional System (Labeled)
214
+
215
+ Model **Season** (Spring, Summer, Other) and **Weather** (Sunny, Rainy) given **Location** (Urban, Rural).
216
+
217
+ `conditionalization` and `jointification` require a **state** (empty domain). `marginalization` also works on a kernel.
218
+
219
+ ### Kernel: P(Season, Weather | Location)
220
+
221
+ Profile `[[['Urban', 'Rural']], [['Spring', 'Summer', 'Other'], ['Sunny', 'Rainy']]]`, data shape `(2, 3, 2)`. Each location’s block sums to 1.
222
+
223
+ ```python
224
+ location_labels = ['Urban', 'Rural']
225
+ season_labels = ['Spring', 'Summer', 'Other']
226
+ weather_labels = ['Sunny', 'Rainy']
227
+ process_data = np.array([
228
+ [[0.2, 0.1], [0.3, 0.1], [0.2, 0.1]], # Urban
229
+ [[0.1, 0.2], [0.2, 0.2], [0.1, 0.2]], # Rural
230
+ ])
231
+ process_tensor = FXTensor(
232
+ [[location_labels], [season_labels, weather_labels]],
233
+ data=process_data,
234
+ )
235
+
236
+ # P(Season | Location) by summing out Weather
237
+ season_tensor = process_tensor.marginalization(start_B=2)
238
+ assert season_tensor.labels == ([['Urban', 'Rural']], [['Spring', 'Summer', 'Other']])
239
+ assert np.allclose(season_tensor.data, [
240
+ [0.3, 0.4, 0.3],
241
+ [0.3, 0.4, 0.3],
242
+ ])
243
+ ```
244
+
245
+ ### Joint state: P(Location, Season, Weather)
246
+
247
+ A state has an empty domain. Split the last axis with `conditionalization(3)` to get `P(Weather | Location, Season)`.
248
+
249
+ ```python
250
+ joint_data = np.array([
251
+ [[0.08, 0.04], [0.12, 0.04], [0.08, 0.04]], # Urban, total 0.4
252
+ [[0.06, 0.12], [0.12, 0.12], [0.06, 0.12]], # Rural, total 0.6
253
+ ])
254
+ joint = FXTensor(
255
+ [[], [location_labels, season_labels, weather_labels]],
256
+ data=joint_data,
257
+ )
258
+
259
+ cond_tensor = joint.conditionalization(concat_start_index=3)
260
+ assert cond_tensor.labels == (
261
+ [['Urban', 'Rural'], ['Spring', 'Summer', 'Other']],
262
+ [['Sunny', 'Rainy']],
263
+ )
264
+ assert cond_tensor.is_markov()
265
+ assert np.allclose(cond_tensor.data, [
266
+ [[2/3, 1/3], [0.75, 0.25], [2/3, 1/3]],
267
+ [[1/3, 2/3], [0.50, 0.50], [1/3, 2/3]],
268
+ ])
269
+ ```
270
+
271
+ ### Jointification of two states
272
+
273
+ Both arguments must be states. Empty-domain labels appear as `None`.
274
+
275
+ ```python
276
+ location_state = FXTensor([[], [location_labels]], data=np.array([0.6, 0.4]))
277
+ traffic_labels = ['Low', 'High']
278
+ traffic_state = FXTensor([[], [traffic_labels]], data=np.array([0.7, 0.3]))
279
+ joint_state = location_state.jointification(traffic_state)
280
+ assert joint_state.labels == (None, [['Urban', 'Rural'], ['Low', 'High']])
281
+ assert np.allclose(joint_state.data, [
282
+ [0.42, 0.18],
283
+ [0.28, 0.12],
284
+ ])
285
+ ```
286
+
287
+ ### Key Method Applications
288
+
289
+ #### `from_json(json_data)`
290
+
291
+ Creates a tensor from JSON data, loading profile and data to instantiate an FXTensor.
292
+
293
+ ```python
294
+ json_data = {
295
+ "profile": [[['a', 'b']], [['x', 'y']]],
296
+ "data": [[0.2, 0.8], [0.6, 0.4]]
297
+ }
298
+ tensor = FXTensor.from_json(json_data)
299
+ assert tensor.labels == ([['a', 'b']], [['x', 'y']])
300
+ ```
301
+
302
+ #### `from_strands(profile, strands)`
303
+
304
+ Creates a tensor from strands (sparse string representations of non-zero elements).
305
+
306
+ ```python
307
+ profile = [[['a', 'b']], [['x', 'y', 'z']]]
308
+ strands = {
309
+ "[[['a']], [['x']]]": 0.1,
310
+ "[[['a']], [['y']]]": 0.2,
311
+ "[[['a']], [['z']]]": 0.7,
312
+ "[[['b']], [['x']]]": 0.3,
313
+ "[[['b']], [['y']]]": 0.3,
314
+ "[[['b']], [['z']]]": 0.4
315
+ }
316
+ tensor = FXTensor.from_strands(profile, strands)
317
+ assert tensor.labels == ([['a', 'b']], [['x', 'y', 'z']])
318
+ ```
319
+
320
+ #### `identity_tensor(list_x)`
321
+
322
+ Creates an identity tensor for the given dimensions, supporting labeled or numeric inputs.
323
+
324
+ ```python
325
+ labels = [['a', 'b']]
326
+ id_tensor = FXTensor.identity_tensor(labels)
327
+ assert id_tensor.labels == ([['a', 'b']], [['a', 'b']])
328
+ ```
329
+
330
+ #### `copy_tensor(list_x, n=2)`
331
+
332
+ Creates a diagonal copy morphism `X → X^{⊗ n}`. `n=0` is discard, `n=1` is identity.
333
+
334
+ ```python
335
+ copy = FXTensor.copy_tensor([2])
336
+ assert copy.profile == [[2], [2, 2]]
337
+ assert np.array_equal(copy.data, [
338
+ [[1, 0],
339
+ [0, 0]],
340
+ [[0, 0],
341
+ [0, 1]],
342
+ ])
343
+
344
+ labeled_copy = FXTensor.copy_tensor([['a', 'b']])
345
+ assert labeled_copy.labels == ([['a', 'b']], [['a', 'b'], ['a', 'b']])
346
+ ```
347
+
348
+ #### `unit_tensor(dims)`
349
+
350
+ Creates a unit state tensor (all-ones vector) for the given dimensions.
351
+
352
+ ```python
353
+ dims = [2, 3]
354
+ unit = FXTensor.unit_tensor(dims)
355
+ assert unit.profile == [[], dims]
356
+ assert np.all(unit.data == 1)
357
+ ```
358
+
359
+ #### `delta_tensor(dims)`
360
+
361
+ Creates a delta tensor (identity matrix) for the given dimensions, used for copying.
362
+
363
+ ```python
364
+ dims = [2]
365
+ delta = FXTensor.delta_tensor(dims)
366
+ assert delta.profile == [[dims], [dims]]
367
+ ```
368
+
369
+ ## DisCoPy integration
370
+
371
+ `fxtensor-salmon` can convert labeled tensors to [DisCoPy](https://discopy.org) `Tensor` / `Box` values and interpret `discopy.markov` diagrams (Copy, Discard, Swap, Id) as `FXTensor` morphisms. This extra requires Python 3.10+ and is installed with:
372
+
373
+ ```shell
374
+ pip install fxtensor-salmon[discopy]
375
+ ```
376
+
377
+ Each profile factor is one wire. Unlabeled tensors map to `Dim`; labels are stored on Markov type names and restored when converting a `Box` back.
378
+
379
+ ```python
380
+ from discopy.markov import Copy, Discard, Id, Swap, Ty
381
+ from fxtensor_salmon import FXTensor
382
+ from fxtensor_salmon.discopy import FXTensorFunctor, from_tensor, to_box, to_tensor
383
+
384
+ kernel = FXTensor(
385
+ [[2], [2]],
386
+ data=np.array([[0.8, 0.2], [0.4, 0.6]]),
387
+ )
388
+ assert from_tensor(to_tensor(kernel)) == kernel
389
+
390
+ weather = ['Sunny', 'Rainy']
391
+ forecast = FXTensor([[weather], [weather]], data=np.array([[0.8, 0.2], [0.4, 0.6]]))
392
+ box = to_box(forecast, name="forecast")
393
+
394
+ x, y = Ty('x'), Ty('y')
395
+ F = FXTensorFunctor({x: 2, y: 3}, {})
396
+ assert F(Copy(x)) == FXTensor.copy_tensor([2])
397
+ assert F(Discard(x)) == FXTensor.exclamation([2])
398
+ assert F(Swap(x, y)) == FXTensor.swap([2], [3])
399
+ assert F(Id(x)) == FXTensor.identity_tensor([2])
400
+ ```
401
+
402
+ ## Theoretical Background: Relation to Markov Categories
403
+
404
+ The `fxtensor-salmon` library is designed based on the **Markov Category**, a framework for categorical probability theory.
405
+
406
+ ### Markov Category Basics
407
+
408
+ - **Objects**: State spaces, represented in `FXTensor` as `domain` or `codomain` (e.g., `[['Urban', 'Rural']]` or `[[2]]`).
409
+ - **Morphisms**: Markov kernels (probabilistic transitions), represented by `FXTensor` instances with profile and data.
410
+
411
+ ### String diagrams
412
+
413
+ 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/).
414
+
415
+ Copy is coassociative, counital and cocommutative:
416
+
417
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_associativity.svg" alt="coassociativity">
418
+
419
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_counitality.svg" alt="counitality">
420
+
421
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/copy_commutativity.svg" alt="cocommutativity">
422
+
423
+ Every morphism is Markov (discard is natural), `f ; ! = !` — in `FXTensor`, each row of a kernel sums to 1:
424
+
425
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/discard_naturality.svg" alt="f ; ! = !">
426
+
427
+ ### Markov Category Operations
428
+
429
+ | Method | Role | When to use |
430
+ |---|---|---|
431
+ | `composition` | Sequential composition `A→B` then `B→C` | Wire processes in series, or apply a kernel to a state |
432
+ | `tensor_product` | Monoidal product | Place independent systems side by side |
433
+ | `marginalization` | Discard a suffix of the outputs | Sum out axes you no longer need |
434
+ | `conditionalization` | Joint state → kernel | Split a joint distribution into a conditional (states only) |
435
+ | `partial_composition` | Compose only some output wires | Keep a prefix of the outputs and feed the suffix into another kernel |
436
+ | `jointification` | Joint of two states | Combine two independent states (states only) |
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
+ | `bayesian_inversion` | `f: X → Y` and prior `I → X` → `f†: Y → X` | Bayesian update / posterior kernel |
442
+ | `almost_surely_equal` | `f = g` p-a.s. | Compare kernels only where the prior has mass |
443
+ | `is_absolutely_continuous` | `p ≪ q` | Check support inclusion of states / kernels |
444
+ | `exclamation` | Discard morphism `X → I` | Build an all-ones discarding tensor |
445
+ | `copy_tensor` | Copy morphism `X → X^{⊗ n}` | Deterministic copy / diagonal (`n=0` is discard) |
446
+ | `delta_tensor` | Identity (alias of `identity_tensor`) | Build an identity kernel |
447
+
448
+ #### `composition` — sequential wiring
449
+
450
+ Use when the codomain of `f` matches the domain of `g`. Same numbers as the labeled example above: `P(Z|X) = P(Y|X) ; P(Z|Y)`.
451
+
452
+ ```python
453
+ result = tensor1.composition(tensor2)
454
+ assert np.allclose(result.data, [[0.78, 0.22], [0.54, 0.46]])
455
+ ```
456
+
457
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/composition.svg" alt="f ; g">
458
+
459
+ #### `tensor_product` — independent systems in parallel
460
+
461
+ <img src="https://raw.githubusercontent.com/salmonsnare/fxtensor-salmon/v1.2.2/docs/diagrams/tensor_product.svg" alt="f ⊗ g">
462
+
463
+ Use to put two morphisms (or two states) next to each other without coupling.
464
+
465
+ ```python
466
+ px = FXTensor([[], [['a', 'b']]], data=np.array([0.3, 0.7]))
467
+ py = FXTensor([[], [['x', 'y', 'z']]], data=np.array([0.2, 0.3, 0.5]))
468
+ pxy = px.tensor_product(py)
469
+ assert pxy.labels == (None, [['a', 'b'], ['x', 'y', 'z']])
470
+ assert np.allclose(pxy.data, [[0.06, 0.09, 0.15], [0.14, 0.21, 0.35]])
471
+ ```
472
+
473
+ #### `marginalization` — drop a suffix of the outputs
474
+
475
+ Use `start_B` (1-based) as the first codomain axis to sum out. A state with profile `[[], [2, 3]]` and `start_B=2` keeps the first axis.
476
+
477
+ ```python
478
+ state = FXTensor([[], [2, 3]], data=np.array([
479
+ [0.1, 0.2, 0.3],
480
+ [0.15, 0.05, 0.2],
481
+ ]))
482
+ marginal = state.marginalization(2)
483
+ assert marginal.profile == [[], [2]]
484
+ assert np.allclose(marginal.data, [0.6, 0.4])
485
+ ```
486
+
487
+ #### `conditionalization` — joint state to a kernel
488
+
489
+ Use only on a state. `concat_start_index` (1-based) is the first axis of the new codomain. A zero slice stays zero.
490
+
491
+ ```python
492
+ joint_state = FXTensor([[], [2, 2]], data=np.array([[0.1, 0.2], [0.0, 0.0]]))
493
+ kernel = joint_state.conditionalization(2)
494
+ assert kernel.profile == [[2], [2]]
495
+ assert kernel.is_markov()
496
+ assert np.allclose(kernel.data, [[1/3, 2/3], [0.0, 0.0]])
497
+ ```
498
+
499
+ #### `partial_composition` — compose only the trailing outputs
500
+
501
+ For `f: A → B ⊗ C` and `g: C → D`, `f.partial_composition(g, 2)` keeps `B` and composes on `C`, giving `A → B ⊗ D`.
502
+
503
+ ```python
504
+ f = FXTensor([[2], [2, 2]], data=np.array([
505
+ [[1.0, 0.0], [0.0, 1.0]],
506
+ [[0.0, 1.0], [1.0, 0.0]],
507
+ ]))
508
+ g = FXTensor([[2], [2]], data=np.array([
509
+ [0.2, 0.8],
510
+ [0.6, 0.4],
511
+ ]))
512
+ partial = f.partial_composition(g, 2)
513
+ assert partial.profile == [[2], [2, 2]]
514
+ assert np.allclose(partial.data, [
515
+ [[0.2, 0.8], [0.6, 0.4]],
516
+ [[0.6, 0.4], [0.2, 0.8]],
517
+ ])
518
+ ```
519
+
520
+ `concat_start_index=1` composes on the whole codomain (the case covered by the tests).
521
+
522
+ #### `jointification` — two states into one joint
523
+
524
+ Same numbers as the tensor product of states; both domains must be empty.
525
+
526
+ ```python
527
+ px = FXTensor([[], [['a', 'b']]], data=np.array([0.3, 0.7]))
528
+ py = FXTensor([[], [['x', 'y', 'z']]], data=np.array([0.2, 0.3, 0.5]))
529
+ joint_xy = px.jointification(py)
530
+ assert joint_xy.labels == (None, [['a', 'b'], ['x', 'y', 'z']])
531
+ assert np.allclose(joint_xy.data, [[0.06, 0.09, 0.15], [0.14, 0.21, 0.35]])
532
+ ```
533
+
534
+ #### `bayesian_inversion` — posterior kernel
535
+
536
+ <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)">
537
+
538
+ 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.
539
+
540
+ ```python
541
+ health = ['Sick', 'Healthy']
542
+ result = ['Positive', 'Negative']
543
+ prior = FXTensor([[], [health]], data=np.array([0.01, 0.99]))
544
+ test = FXTensor([[health], [result]], data=np.array([[0.9, 0.1], [0.05, 0.95]]))
545
+ posterior = test.bayesian_inversion(prior)
546
+ assert posterior.labels == ([result], [health])
547
+ assert np.isclose(posterior.data[0, 0], 2 / 13) # P(Sick | Positive)
548
+ ```
549
+
550
+ #### `conditional` — conditionalize a kernel
551
+
552
+ `f.conditional(k)` turns `f: A → X ⊗ Y` into `A ⊗ X → Y`, where `k` (1-based) is the first codomain axis of `Y`. On a state it equals `conditionalization(k)`.
553
+
554
+ ```python
555
+ f = FXTensor([[['a']], [['x0', 'x1'], ['y0', 'y1']]], data=np.array([[[0.1, 0.3], [0.2, 0.4]]]))
556
+ c = f.conditional(2)
557
+ assert c.labels == ([['a'], ['x0', 'x1']], [['y0', 'y1']])
558
+ assert np.allclose(c.data, [[[0.25, 0.75], [1/3, 2/3]]])
559
+ ```
560
+
561
+ #### `support`, `almost_surely_equal`, `is_absolutely_continuous` — a.s. reasoning
562
+
563
+ <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)">
564
+
565
+ `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.
566
+
567
+ ```python
568
+ prior = FXTensor([[], [['x0', 'x1', 'x2']]], data=np.array([0.4, 0.6, 0.0]))
569
+ f = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [1.0, 0.0]]))
570
+ g = FXTensor([[['x0', 'x1', 'x2']], [['y0', 'y1']]], data=np.array([[0.2, 0.8], [0.5, 0.5], [0.3, 0.7]]))
571
+ assert np.array_equal(prior.support().data, [1.0, 1.0, 0.0])
572
+ assert f.almost_surely_equal(g, prior) # f and g differ only on the null point x2
573
+ assert f.is_absolutely_continuous(g) # f(·|x) ≪ g(·|x) for every x
574
+ assert not g.is_absolutely_continuous(f)
575
+ ```
576
+
577
+ ### Probabilistic Properties
578
+
579
+ - `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.
580
+ - `is_deterministic()`: True if every codomain slice is a point mass (one-hot), i.e. `f ; copy = copy ; (f ⊗ f)`. Zero slices are not deterministic.
581
+ - Labeled tensors enable intuitive interpretation via `get_label_index` and `get_index_label`.
582
+
583
+ ## Testing
584
+
585
+ Tests are implemented in `tests/test_fxtensor.py` using `pytest`.
586
+
587
+ ```bash
588
+ pytest
589
+ ```
590
+
591
+ ## References
592
+ - [1] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏 A First Look -- 圏論的確率論の最良の定式化](https://m-hiyama.hatenablog.com/entry/2020/06/09/154044)
593
+ - [2] [檜山正幸のキマイラ飼育記 (はてなBlog), マルコフ圏におけるテンソル計算の手順とコツ](https://m-hiyama.hatenablog.com/entry/2021/04/05/153325)
594
+ - [3] [K. Cho and B. Jacobs, Disintegration and Bayesian Inversion via String Diagrams (arXiv:1709.00322)](https://arxiv.org/abs/1709.00322)
595
+ - [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)
596
+ - [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)