fxtensor-salmon 0.1.0__py3-none-any.whl
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.
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import json
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
class FXTensor:
|
|
6
|
+
def __init__(self, profile, data=None):
|
|
7
|
+
self.profile = profile
|
|
8
|
+
domain_profile = profile[0]
|
|
9
|
+
codomain_profile = profile[1]
|
|
10
|
+
self.shape = tuple(domain_profile) + tuple(codomain_profile)
|
|
11
|
+
if data is None:
|
|
12
|
+
self.data = np.zeros(self.shape)
|
|
13
|
+
else:
|
|
14
|
+
# Ensure the provided data shape matches the profile
|
|
15
|
+
assert data.shape == self.shape, "Data shape must match profile shape"
|
|
16
|
+
self.data = data
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def from_json(json_data):
|
|
20
|
+
"""Creates an FXTensor instance from a JSON object."""
|
|
21
|
+
profile = json_data["profile"]
|
|
22
|
+
domain_profile = profile[0]
|
|
23
|
+
codomain_profile = profile[1]
|
|
24
|
+
shape = tuple(domain_profile) + tuple(codomain_profile)
|
|
25
|
+
data = np.zeros(shape)
|
|
26
|
+
for strand in json_data["strands"]:
|
|
27
|
+
from_idx = tuple(strand["from"])
|
|
28
|
+
to_idx = tuple(strand["to"])
|
|
29
|
+
full_idx = from_idx + to_idx
|
|
30
|
+
data[full_idx] = strand["weight"]
|
|
31
|
+
return FXTensor(profile, data=data)
|
|
32
|
+
|
|
33
|
+
def to_json(self):
|
|
34
|
+
"""Converts the FXTensor instance to a JSON serializable dictionary."""
|
|
35
|
+
profile = self.profile
|
|
36
|
+
strands = []
|
|
37
|
+
k = len(self.profile[0]) # Number of dimensions in the domain
|
|
38
|
+
for idx in np.ndindex(self.data.shape):
|
|
39
|
+
if self.data[idx] != 0:
|
|
40
|
+
from_idx = list(idx[:k])
|
|
41
|
+
to_idx = list(idx[k:])
|
|
42
|
+
weight = float(self.data[idx])
|
|
43
|
+
strands.append({"from": from_idx, "to": to_idx, "weight": weight})
|
|
44
|
+
return {"profile": profile, "strands": strands}
|
|
45
|
+
|
|
46
|
+
def save_to_file(self, filename):
|
|
47
|
+
"""Saves the tensor to a JSON file."""
|
|
48
|
+
with open(filename, 'w') as f:
|
|
49
|
+
json.dump(self.to_json(), f, indent=2)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def load_from_file(filename):
|
|
53
|
+
"""Loads a tensor from a JSON file."""
|
|
54
|
+
with open(filename, 'r') as f:
|
|
55
|
+
json_data = json.load(f)
|
|
56
|
+
return FXTensor.from_json(json_data)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def from_strands(profile, strands):
|
|
60
|
+
"""Creates a tensor from a dictionary of strands (string representation)."""
|
|
61
|
+
tensor = FXTensor(profile)
|
|
62
|
+
for strand_str, weight in strands.items():
|
|
63
|
+
# Safely evaluate the string representation of the list
|
|
64
|
+
strand = ast.literal_eval(strand_str)
|
|
65
|
+
# Adjust for 1-based indexing if necessary
|
|
66
|
+
domain_idx = tuple(x - 1 for x in strand[0])
|
|
67
|
+
codomain_idx = tuple(x - 1 for x in strand[1])
|
|
68
|
+
idx = domain_idx + codomain_idx
|
|
69
|
+
tensor.data[idx] = weight
|
|
70
|
+
return tensor
|
|
71
|
+
|
|
72
|
+
def to_original_dict(self):
|
|
73
|
+
"""Converts the tensor back to the original dictionary format with 1-based indexing."""
|
|
74
|
+
profile = self.profile
|
|
75
|
+
strands = {}
|
|
76
|
+
total_domain_dims = len(self.profile[0])
|
|
77
|
+
for idx in np.ndindex(self.data.shape):
|
|
78
|
+
weight = self.data[idx]
|
|
79
|
+
if weight != 0:
|
|
80
|
+
domain_idx = [d + 1 for d in idx[:total_domain_dims]]
|
|
81
|
+
codomain_idx = [c + 1 for c in idx[total_domain_dims:]]
|
|
82
|
+
strand_key = str([domain_idx, codomain_idx])
|
|
83
|
+
strands[strand_key] = weight
|
|
84
|
+
return {"profile": profile, "strands": strands}
|
|
85
|
+
|
|
86
|
+
def composition(self, other):
|
|
87
|
+
"""Performs tensor composition (matrix multiplication for tensors)."""
|
|
88
|
+
if self.profile[1] != other.profile[0]:
|
|
89
|
+
raise ValueError("Tensors are not composable: codomain of self must match domain of other.")
|
|
90
|
+
|
|
91
|
+
# Axes to contract over
|
|
92
|
+
self_codomain_axes = list(range(len(self.profile[0]), len(self.shape)))
|
|
93
|
+
other_domain_axes = list(range(len(other.profile[0])))
|
|
94
|
+
|
|
95
|
+
result_data = np.tensordot(self.data, other.data, axes=(self_codomain_axes, other_domain_axes))
|
|
96
|
+
result_profile = [self.profile[0], other.profile[1]]
|
|
97
|
+
return FXTensor(result_profile, data=result_data)
|
|
98
|
+
|
|
99
|
+
def tensor_product(self, other):
|
|
100
|
+
"""Performs the tensor product of two FXTensors."""
|
|
101
|
+
dx, cx = self.profile
|
|
102
|
+
dy, cy = other.profile
|
|
103
|
+
|
|
104
|
+
# Reshape self and other to be broadcastable
|
|
105
|
+
self_reshaped_shape = tuple(dx) + (1,) * len(dy) + tuple(cx) + (1,) * len(cy)
|
|
106
|
+
other_reshaped_shape = (1,) * len(dx) + tuple(dy) + (1,) * len(cx) + tuple(cy)
|
|
107
|
+
|
|
108
|
+
self_reshaped = self.data.reshape(self_reshaped_shape)
|
|
109
|
+
other_reshaped = other.data.reshape(other_reshaped_shape)
|
|
110
|
+
|
|
111
|
+
# Broadcasting multiplication
|
|
112
|
+
result_data = self_reshaped * other_reshaped
|
|
113
|
+
|
|
114
|
+
# New profile for the resulting tensor
|
|
115
|
+
result_profile = [dx + dy, cx + cy]
|
|
116
|
+
return FXTensor(result_profile, data=result_data)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def unit_tensor(list_x):
|
|
120
|
+
"""Creates a unit tensor (identity)."""
|
|
121
|
+
profile = [list_x, list_x]
|
|
122
|
+
shape = tuple(list_x) + tuple(list_x)
|
|
123
|
+
data = np.identity(np.prod(list_x)).reshape(shape)
|
|
124
|
+
return FXTensor(profile, data=data)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def delta(list_x):
|
|
128
|
+
"""Creates a delta tensor (copying)."""
|
|
129
|
+
domain = list_x
|
|
130
|
+
codomain = list_x + list_x
|
|
131
|
+
profile = [domain, codomain]
|
|
132
|
+
shape = tuple(domain) + tuple(codomain)
|
|
133
|
+
data = np.zeros(shape)
|
|
134
|
+
|
|
135
|
+
# Create an identity matrix of size prod(list_x) x prod(list_x)
|
|
136
|
+
identity_matrix = np.identity(np.prod(list_x))
|
|
137
|
+
# Reshape it to the desired tensor shape
|
|
138
|
+
data = identity_matrix.reshape(tuple(list_x) + tuple(list_x))
|
|
139
|
+
|
|
140
|
+
# This is a more efficient way to create the delta tensor
|
|
141
|
+
# The previous loop was complex; this creates the same result
|
|
142
|
+
temp_data = np.zeros(tuple(list_x) + tuple(list_x))
|
|
143
|
+
for idx in np.ndindex(*tuple(list_x)):
|
|
144
|
+
temp_data[idx + idx] = 1
|
|
145
|
+
|
|
146
|
+
final_data = temp_data.reshape(shape)
|
|
147
|
+
|
|
148
|
+
return FXTensor(profile, data=final_data)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def exclamation(list_x):
|
|
153
|
+
"""Creates an exclamation tensor (discarding)."""
|
|
154
|
+
profile = [list_x, []]
|
|
155
|
+
shape = tuple(list_x)
|
|
156
|
+
data = np.ones(shape)
|
|
157
|
+
return FXTensor(profile, data=data)
|
|
158
|
+
|
|
159
|
+
def is_markov(self):
|
|
160
|
+
"""Checks if the tensor represents a Markov kernel."""
|
|
161
|
+
codomain_axes = tuple(range(len(self.profile[0]), len(self.shape)))
|
|
162
|
+
if not codomain_axes: # Handle case where codomain is empty
|
|
163
|
+
return np.all(self.data == 1.0)
|
|
164
|
+
row_sums = np.sum(self.data, axis=codomain_axes)
|
|
165
|
+
return np.allclose(row_sums, 1)
|
|
166
|
+
|
|
167
|
+
def partial_composition(self, other, concat_start_index):
|
|
168
|
+
"""Performs partial composition."""
|
|
169
|
+
codomain_profile = self.profile[1]
|
|
170
|
+
c_part = codomain_profile[concat_start_index - 1:]
|
|
171
|
+
unit_c = FXTensor.unit_tensor(c_part)
|
|
172
|
+
tp = other.tensor_product(unit_c)
|
|
173
|
+
return self.composition(tp)
|
|
174
|
+
|
|
175
|
+
def jointification(self, other):
|
|
176
|
+
"""Creates a joint state from two tensors."""
|
|
177
|
+
if self.profile[0] != [] or other.profile[0] != self.profile[1]:
|
|
178
|
+
raise ValueError("Tensors are not suitable for jointification")
|
|
179
|
+
a = self.profile[1]
|
|
180
|
+
b = other.profile[1]
|
|
181
|
+
|
|
182
|
+
# Reshape self.data to be broadcastable with other.data
|
|
183
|
+
self_expanded = self.data.reshape(tuple(a) + (1,) * len(b))
|
|
184
|
+
|
|
185
|
+
result_data = self_expanded * other.data
|
|
186
|
+
result_profile = [[], a + b]
|
|
187
|
+
return FXTensor(result_profile, data=result_data)
|
|
188
|
+
|
|
189
|
+
def conditionalization(self, concat_start_index):
|
|
190
|
+
"""Creates a conditional probability distribution."""
|
|
191
|
+
if self.profile[0] != []:
|
|
192
|
+
raise ValueError("Tensor must have empty domain for conditionalization")
|
|
193
|
+
codomain = self.profile[1]
|
|
194
|
+
start_B = concat_start_index - 1
|
|
195
|
+
A_sizes = codomain[0:start_B]
|
|
196
|
+
B_sizes = codomain[start_B:]
|
|
197
|
+
|
|
198
|
+
# Sum over the dimensions of B to get the marginal distribution of A
|
|
199
|
+
sum_axes = tuple(range(len(A_sizes), len(A_sizes) + len(B_sizes)))
|
|
200
|
+
marginal_A = np.sum(self.data, axis=sum_axes)
|
|
201
|
+
|
|
202
|
+
# Expand marginal_A to be broadcastable with the original data
|
|
203
|
+
marginal_A_expanded_shape = tuple(A_sizes) + (1,) * len(B_sizes)
|
|
204
|
+
marginal_A_expanded = marginal_A.reshape(marginal_A_expanded_shape)
|
|
205
|
+
|
|
206
|
+
# Avoid division by zero
|
|
207
|
+
# Where marginal_A is zero, the conditional probability is also zero.
|
|
208
|
+
result_data = np.divide(self.data, marginal_A_expanded, out=np.zeros_like(self.data), where=marginal_A_expanded!=0)
|
|
209
|
+
|
|
210
|
+
result_profile = [A_sizes, B_sizes]
|
|
211
|
+
return FXTensor(result_profile, data=result_data)
|
|
212
|
+
|
|
213
|
+
def first_marginalization(self, concat_start_index):
|
|
214
|
+
"""Marginalizes out the second part of the codomain."""
|
|
215
|
+
codomain = self.profile[1]
|
|
216
|
+
start_B = concat_start_index - 1
|
|
217
|
+
A_sizes = codomain[0:start_B]
|
|
218
|
+
B_sizes = codomain[start_B:]
|
|
219
|
+
|
|
220
|
+
unit_A = FXTensor.unit_tensor(A_sizes)
|
|
221
|
+
excl_B = FXTensor.exclamation(B_sizes)
|
|
222
|
+
|
|
223
|
+
tp = unit_A.tensor_product(excl_B)
|
|
224
|
+
result = self.composition(tp)
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
def second_marginalization(self, concat_start_index):
|
|
228
|
+
"""Marginalizes out the first part of the codomain."""
|
|
229
|
+
codomain = self.profile[1]
|
|
230
|
+
start_B = concat_start_index - 1
|
|
231
|
+
A_sizes = codomain[0:start_B]
|
|
232
|
+
B_sizes = codomain[start_B:]
|
|
233
|
+
|
|
234
|
+
excl_A = FXTensor.exclamation(A_sizes)
|
|
235
|
+
unit_B = FXTensor.unit_tensor(B_sizes)
|
|
236
|
+
|
|
237
|
+
tp = excl_A.tensor_product(unit_B)
|
|
238
|
+
result = self.composition(tp)
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def swap(list_a, list_b):
|
|
243
|
+
"""Creates a swap tensor."""
|
|
244
|
+
domain = list_a + list_b
|
|
245
|
+
codomain = list_b + list_a
|
|
246
|
+
profile = [domain, codomain]
|
|
247
|
+
|
|
248
|
+
# Create a permutation of axes
|
|
249
|
+
len_a = len(list_a)
|
|
250
|
+
len_b = len(list_b)
|
|
251
|
+
|
|
252
|
+
# The permutation will move the block of axes for 'a' to the end
|
|
253
|
+
# and the block for 'b' to the beginning.
|
|
254
|
+
# Original axes: 0, 1, ..., len_a-1, len_a, ..., len_a+len_b-1
|
|
255
|
+
# New axes: len_a, ..., len_a+len_b-1, 0, 1, ..., len_a-1
|
|
256
|
+
permutation = list(range(len_a, len_a + len_b)) + list(range(len_a))
|
|
257
|
+
|
|
258
|
+
# Create an identity tensor and transpose its domain axes
|
|
259
|
+
identity_data = np.identity(np.prod(domain)).reshape(tuple(domain) * 2)
|
|
260
|
+
result_data = np.transpose(identity_data, permutation + [i + len(domain) for i in range(len(domain))])
|
|
261
|
+
|
|
262
|
+
# This is a more direct way to create the swap tensor
|
|
263
|
+
shape = tuple(domain) + tuple(codomain)
|
|
264
|
+
data = np.zeros(shape)
|
|
265
|
+
|
|
266
|
+
# Iterate over the domain indices
|
|
267
|
+
from_indices = np.ndindex(*tuple(domain))
|
|
268
|
+
|
|
269
|
+
for from_idx in from_indices:
|
|
270
|
+
idx_a = from_idx[:len_a]
|
|
271
|
+
idx_b = from_idx[len_a:]
|
|
272
|
+
to_idx = idx_b + idx_a
|
|
273
|
+
full_idx = from_idx + to_idx
|
|
274
|
+
data[full_idx] = 1
|
|
275
|
+
|
|
276
|
+
return FXTensor(profile, data=data)
|
|
277
|
+
|
|
278
|
+
def __repr__(self):
|
|
279
|
+
"""Provides a string representation of the FXTensor instance."""
|
|
280
|
+
return f"FXTensor(profile={self.profile}, shape={self.data.shape})"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fxtensor-salmon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markov Category Module
|
|
5
|
+
Author-email: あなたの名前 <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/yourusername/fxtensor-salmon
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.20.0
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# FXTensor
|
|
26
|
+
|
|
27
|
+
マルコフ・テンソルを計算するモジュールです。
|
|
28
|
+
|
|
29
|
+
## インストール
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install fxtensor-salmon
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 使い方
|
|
36
|
+
|
|
37
|
+
JSON定義から`FXTensor`オブジェクトを作成する方法は以下の通りです。
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import numpy as np
|
|
41
|
+
from fxtensor import FXTensor
|
|
42
|
+
|
|
43
|
+
json_data = {
|
|
44
|
+
"profile": [[2], [2]],
|
|
45
|
+
"strands": [
|
|
46
|
+
{"from": [0], "to": [0], "weight": 0.3},
|
|
47
|
+
{"from": [0], "to": [1], "weight": 0.7},
|
|
48
|
+
{"from": [1], "to": [0], "weight": 0.5},
|
|
49
|
+
{"from": [1], "to": [1], "weight": 0.5}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# JSONデータからテンソルを作成
|
|
54
|
+
stochastic_map = FXTensor.from_json(json_data)
|
|
55
|
+
|
|
56
|
+
print(stochastic_map)
|
|
57
|
+
# 出力予測: FXTensor(profile=[[2], [2]], shape=(2, 2))
|
|
58
|
+
|
|
59
|
+
print(stochastic_map.data)
|
|
60
|
+
# 出力予測:
|
|
61
|
+
# [[0.3 0.7]
|
|
62
|
+
# [0.5 0.5]]
|
|
63
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fxtensor-salmon/__init__.py,sha256=eEnjtY_Pf_Nsbnn2q460MFGDp9EAG_PutqKpQmZrbTE,11346
|
|
2
|
+
fxtensor_salmon-0.1.0.dist-info/licenses/LICENSE,sha256=rmOa-IRuWs26NLtZ6quE5QGhCVHAwR9iTE1ryJbG6F4,1061
|
|
3
|
+
fxtensor_salmon-0.1.0.dist-info/METADATA,sha256=OYmzR7ej81NQPc4WaYfRFpHWHnvE6Kler0D01SiMgOQ,2485
|
|
4
|
+
fxtensor_salmon-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
fxtensor_salmon-0.1.0.dist-info/top_level.txt,sha256=Epjf4gCswIl7Z-ChBL5L91XTCLYs1H0dRC2mQ-ocFpU,16
|
|
6
|
+
fxtensor_salmon-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Tadachika Oki
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fxtensor-salmon
|