Named axes¶
Value labels name the points on an axis, such as low and high.
An axis name names the wire itself, such as A or C. The same labels
can sit on several wires; the names still tell them apart. Omit names
and the tensor behaves as before.
Pass names as (domain names, codomain names). They are unique in one
tensor. names is (None, None) when
the tensor is unnamed. rename() replaces
names, and axis() returns the index with
domain axes counted first, as in get_label_index().
Marginal¶
marginal() keeps the named codomain wires
in the order given and sums out the rest, including a wire in the middle.
marginalize() does the same by listing the
wires to drop; the others stay in their original order.
For a state p: I → A ⊗ B ⊗ C, keeping B is the deterministic
wiring that discards A and C:
p ; (! ⊗ id_B ⊗ !) = p_B.
Read top to bottom. Left: p ; (! ⊗ id_B ⊗ !). The dots are
discard. Right: the marginal state on B.¶
Labels move with the kept wires. The domain is unchanged.
Permute¶
permute() reorders the codomain into the
given names. The domain stays put, and labels move with their wires.
For a state this reorders every axis. The reordering is a permutation of
the wires, drawn as crossings:
Read top to bottom. Left: p with wires A, B, C crossed
into the order C, A, B. Right: the reordered state.¶
Observe¶
observe() conditions a state on outcomes
written as name=label. Without labels, the value is an integer index.
The method moves the observed wires to the front with permute(),
applies conditionalization(), and reads
that row. It returns the posterior joint on the remaining wires, in their
original order, and the probability of the evidence. Several outcomes at
once match observing them one by one; the probability is then the joint
probability of all of them. Evidence 0 raises ValueError.
On a chain A → C → D, the A marginal of observe(C="high") is
the row of bayesian_inversion() for that
value of C.
import numpy as np
from fxtensor_salmon import FXTensor
level = ["low", "high"]
prior_data = np.array([0.25, 0.75])
given_a = np.array([
[0.8, 0.2], # A = low
[0.4, 0.6], # A = high
])
given_c = np.array([
[0.9, 0.1], # C = low
[0.3, 0.7], # C = high
])
joint = FXTensor(
[[], [level, level, level]],
data=prior_data[:, None, None] * given_a[:, :, None] * given_c[None, :, :],
names=([], ["A", "C", "D"]),
)
post, evidence = joint.observe(C="high")
assert post.names == (None, ["A", "D"])
assert post.marginal("D").names == (None, ["D"])
prior = FXTensor([[], [level]], data=prior_data, names=([], ["A"]))
kernel = FXTensor([[level], [level]], data=given_a, names=(["A"], ["C"]))
inversion = kernel.bayesian_inversion(prior)
assert np.allclose(post.marginal("A").data, inversion.data[level.index("high")])
assert np.isclose(evidence, joint.marginal("C").data[level.index("high")])
How names travel¶
composition() checks the codomain names of
self against the domain names of other when both are present.
The result keeps the domain names of self and the codomain names of
other. tensor_product() concatenates
names and raises ValueError if a name is repeated. If one factor has
axes but no names, the product is unnamed.
bayesian_inversion() swaps domain and
codomain names. conditional() and
conditionalization() split names at the
same index as the axes.
to_json() stores names under names
as [domain names, codomain names]. JSON with no names key loads
as an unnamed tensor.