Operations

Indices that split a codomain (start_B, concat_start_index) are 1-based positions along the codomain factors.

Composition and tensor product

composition() contracts the receiver’s codomain with the argument’s domain via numpy.tensordot. The profiles must match or it raises ValueError. Labels are kept only when both operands have labels.

tensor_product() concatenates domains and codomains and multiplies the broadcast arrays. Labels are dropped when one operand is unlabeled and has at least one axis. An unlabeled scalar (no axes) does not wipe the other tensor’s labels.

import numpy as np
from fxtensor_salmon import FXTensor

f = FXTensor([[["a", "b"]], [["x", "y"]]], data=np.array([[0.2, 0.8], [0.6, 0.4]]))
g = FXTensor.identity_tensor([2])
assert f.tensor_product(g).labels == (None, None)
scalar = FXTensor([[], []], data=np.array(1.0))
assert f.tensor_product(scalar).labels == f.labels

left = FXTensor([[2], [2]], data=np.eye(2))
right = FXTensor([[3], [1]], data=np.ones((3, 1)))
try:
    left.composition(right)
except ValueError:
    pass
else:
    raise AssertionError("composition requires a matching codomain and domain")
Sequential composition, read top to bottom

Sequential composition, f ; g.

Tensor product of two morphisms

Tensor product, f ⊗ g.

Marginalization

marginalization() sums codomain axes from start_B through the end and keeps the labels of the surviving prefix. start_B equal to len(codomain) + 1 sums nothing and returns the same object. start_B == 1 sums the whole codomain. The valid range is 1 through len(codomain) + 1.

import numpy as np
from fxtensor_salmon import FXTensor

location_labels = ["Urban", "Rural"]
season_labels = ["Spring", "Summer", "Other"]
weather_labels = ["Sunny", "Rainy"]
process_data = np.array(
    [
        [[0.2, 0.1], [0.3, 0.1], [0.2, 0.1]],
        [[0.1, 0.2], [0.2, 0.2], [0.1, 0.2]],
    ]
)
process_tensor = FXTensor(
    [[location_labels], [season_labels, weather_labels]],
    data=process_data,
)
season_tensor = process_tensor.marginalization(start_B=2)
assert season_tensor.labels == ([location_labels], [season_labels])
assert np.allclose(
    season_tensor.data,
    [
        [0.3, 0.4, 0.3],
        [0.3, 0.4, 0.3],
    ],
)

state = FXTensor(
    [[], [2, 3]],
    data=np.array(
        [
            [0.1, 0.2, 0.3],
            [0.15, 0.05, 0.2],
        ]
    ),
)
assert state.marginalization(3) is state
marginal = state.marginalization(2)
assert marginal.profile == [[], [2]]
assert np.allclose(marginal.data, [0.6, 0.4])
assert state.marginalization(1).profile == [[], []]

discard_prefix() sums the codomain axes before start_B and keeps the suffix, including its labels. start_B == 1 returns the same object.

import numpy as np
from fxtensor_salmon import FXTensor

labeled = FXTensor(
    [[], [["a", "b"], ["x", "y"]]],
    data=np.array(
        [
            [0.1, 0.2],
            [0.3, 0.4],
        ]
    ),
)
assert labeled.discard_prefix(1) is labeled
prefix = labeled.discard_prefix(2)
assert prefix.labels == (None, [["x", "y"]])
assert np.allclose(prefix.data, [0.4, 0.6])
kept = labeled.marginalization(2)
assert kept.labels == (None, [["a", "b"]])
assert np.allclose(kept.data, [0.3, 0.7])

first_marginalization() and second_marginalization() implement the same splits by composing with identity and exclamation(). Those constructors are numeric, so the results are unlabeled. first drops the suffix that starts at concat_start_index; second drops the prefix before it.

import numpy as np
from fxtensor_salmon import FXTensor

labeled = FXTensor(
    [[], [["a", "b"], ["x", "y"]]],
    data=np.array(
        [
            [0.1, 0.2],
            [0.3, 0.4],
        ]
    ),
)
first = labeled.first_marginalization(2)
second = labeled.second_marginalization(2)
assert first.labels == (None, None)
assert second.labels == (None, None)
assert np.allclose(first.data, labeled.marginalization(2).data)
assert np.allclose(second.data, labeled.discard_prefix(2).data)

Partial composition

For f: A → B ⊗ C and g: C → D, f.partial_composition(g, k) keeps the codomain prefix before the 1-based index k and feeds the suffix into g. k == 1 feeds the whole codomain, which matches composition() on the values. The implementation builds a numeric identity on the kept prefix, so labels on the result are cleared.

import numpy as np
from fxtensor_salmon import FXTensor

f = FXTensor(
    [[2], [2, 2]],
    data=np.array(
        [
            [[1.0, 0.0], [0.0, 1.0]],
            [[0.0, 1.0], [1.0, 0.0]],
        ]
    ),
)
g = FXTensor(
    [[2], [2]],
    data=np.array(
        [
            [0.2, 0.8],
            [0.6, 0.4],
        ]
    ),
)
partial = f.partial_composition(g, 2)
assert partial.profile == [[2], [2, 2]]
assert partial.labels == (None, None)
assert np.allclose(
    partial.data,
    [
        [[0.2, 0.8], [0.6, 0.4]],
        [[0.6, 0.4], [0.2, 0.8]],
    ],
)

whole = FXTensor([[2], [2]], data=np.array([[0.2, 0.8], [0.6, 0.4]]))
nxt = FXTensor([[2], [2]], data=np.array([[0.3, 0.7], [0.9, 0.1]]))
assert np.allclose(whole.partial_composition(nxt, 1).data, whole.composition(nxt).data)

Jointification

jointification() is the tensor product of two states. Both domains must be empty. Labels are kept only when both states are labeled; the domain label group of a state is None.

import numpy as np
from fxtensor_salmon import FXTensor

location_labels = ["Urban", "Rural"]
traffic_labels = ["Low", "High"]
location_state = FXTensor([[], [location_labels]], data=np.array([0.6, 0.4]))
traffic_state = FXTensor([[], [traffic_labels]], data=np.array([0.7, 0.3]))
joint_state = location_state.jointification(traffic_state)
assert joint_state.labels == (None, [location_labels, traffic_labels])
assert np.allclose(
    joint_state.data,
    [
        [0.42, 0.18],
        [0.28, 0.12],
    ],
)

px = FXTensor([[], [["a", "b"]]], data=np.array([0.3, 0.7]))
py = FXTensor([[], [["x", "y", "z"]]], data=np.array([0.2, 0.3, 0.5]))
assert np.allclose(px.jointification(py).data, px.tensor_product(py).data)

kernel = FXTensor([[2], [2]], data=np.eye(2))
try:
    location_state.jointification(kernel)
except ValueError:
    pass
else:
    raise AssertionError("jointification requires two states")