tn_noise_learning#
Tensor-network learning of quantum noise channels.
Pipeline: Topology → LPDO →
NoiseDataset → Trainer →
History.
PAULI_EIGENSTATES
module-attribute
#
PAULI_EIGENSTATES = array(
[
[[1, 0], [0, 0]],
[[0, 0], [0, 1]],
[[0.5, 0.5], [0.5, 0.5]],
[[0.5, -0.5], [-0.5, 0.5]],
[[0.5, -0.5j], [0.5j, 0.5]],
[[0.5, 0.5j], [-0.5j, 0.5]],
],
dtype=complex,
)
States |0>, |1>, |+>, |−>, |+i>, |−i>; effect 2 * basis + bit is outcome bit of basis Z, X or Y.
Batch #
Bases: NamedTuple
One training batch: per-site state and effect matrices, each of shape (num_qubits, batch, 2, 2).
History
dataclass
#
History(
metrics: dict[str, list[float]] = dict(),
params: list[list[ndarray]] = list(),
best_epoch: int | None = None,
num_sites: int | None = None,
)
Metric curves and optional parameter snapshots recorded by Trainer.
| PARAMETER | DESCRIPTION |
|---|---|
metrics
|
one list per metric name, one value per epoch. |
params
|
Kraus tensors after each epoch, filled only when |
best_epoch
|
1-based epoch whose parameters were written back to the model.
TYPE:
|
num_sites
|
number of qubits, used to draw the trace-preserving reference
TYPE:
|
append #
append(
values: dict[str, Any],
tensors: list[ndarray] | None = None,
) -> None
Record one epoch.
Source code in src/tn_noise_learning/history.py
54 55 56 57 58 59 | |
load
classmethod
#
load(path: str | Path) -> History
Read a history written by save.
Source code in src/tn_noise_learning/history.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
plot #
plot(path: str | Path | None = None) -> Figure
Plot the metric curves with plot_history.
Source code in src/tn_noise_learning/history.py
89 90 91 92 93 | |
save #
save(path: str | Path) -> None
Write to a zarr store at path.
Source code in src/tn_noise_learning/history.py
61 62 63 64 65 66 67 68 69 | |
LPDO
dataclass
#
LPDO(topology: Topology, tensors: list[ndarray])
Quantum channel as an LPDO on a topology.
| PARAMETER | DESCRIPTION |
|---|---|
topology
|
site connectivity.
TYPE:
|
tensors
|
one Kraus tensor per site in canonical axis order
|
__post_init__ #
__post_init__() -> None
Validate tensor count, axis counts, physical dimensions and bond sizes.
Source code in src/tn_noise_learning/lpdo.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
bond_size #
bond_size(site: int, neighbor: int) -> int
Size of the bond leg of site pointing at neighbor.
Source code in src/tn_noise_learning/lpdo.py
138 139 140 | |
distance #
distance(other: LPDO) -> float
Squared normalised Frobenius distance ‖Λ − Λ'‖_F² / 4**num_sites.
Source code in src/tn_noise_learning/lpdo.py
194 195 196 | |
from_group
classmethod
#
from_group(group: Group) -> LPDO
Read an LPDO written by to_group.
Source code in src/tn_noise_learning/lpdo.py
214 215 216 217 218 219 | |
from_network
classmethod
#
from_network(
topology: Topology, kraus_tn: TensorNetwork
) -> LPDO
Rebuild from a Kraus network with the index names of kraus_inds.
Source code in src/tn_noise_learning/lpdo.py
164 165 166 167 168 169 170 171 172 173 | |
identity
classmethod
#
identity(topology: Topology) -> LPDO
Identity channel: one Kraus operator 𝟙 per site, all bonds of size one.
Source code in src/tn_noise_learning/lpdo.py
153 154 155 156 157 158 159 160 161 162 | |
kraus_network #
kraus_network() -> qtn.TensorNetwork
Tensor network of the Kraus half, tagged I{j} and V.
Source code in src/tn_noise_learning/lpdo.py
175 176 177 178 179 180 181 182 | |
load
classmethod
#
load(path: str | Path) -> LPDO
Load from a zarr store written by save.
Source code in src/tn_noise_learning/lpdo.py
225 226 227 228 | |
network #
network() -> qtn.TensorNetwork
Full LPDO network with open legs kn, k (ket) and bn, b (bra) per site.
Source code in src/tn_noise_learning/lpdo.py
184 185 186 | |
random
classmethod
#
random(
topology: Topology,
bond_dim: int,
kraus_dim: int,
seed: int | None = None,
) -> LPDO
Complex Gaussian Kraus tensors scaled so that the expected trace is 2**num_sites.
Source code in src/tn_noise_learning/lpdo.py
142 143 144 145 146 147 148 149 150 151 | |
save #
save(path: str | Path) -> None
Save to a zarr store at path.
Source code in src/tn_noise_learning/lpdo.py
221 222 223 | |
to_dense #
to_dense() -> np.ndarray
Superoperator matrix Σ_m K_m ⊗ conj(K_m) with site 0 most significant (small systems only).
Source code in src/tn_noise_learning/lpdo.py
198 199 200 201 202 203 204 205 | |
to_group #
to_group(group: Group) -> None
Write topology and tensors into an open zarr group.
Source code in src/tn_noise_learning/lpdo.py
207 208 209 210 211 212 | |
trace #
trace() -> float
Trace of the channel's Choi-like network, 2**num_sites for a trace-preserving channel.
Source code in src/tn_noise_learning/lpdo.py
188 189 190 191 192 | |
NoiseDataset
dataclass
#
NoiseDataset(
inputs: ndarray,
outcomes: ndarray,
states: ndarray,
povms: ndarray,
circuit: QuantumCircuit | None = None,
ideal_channel: LPDO | None = None,
)
Shots of a process-tomography experiment on product states with product measurements.
| PARAMETER | DESCRIPTION |
|---|---|
inputs
|
prepared-state labels, shape
TYPE:
|
outcomes
|
measured-effect labels, shape
TYPE:
|
states
|
single-qubit input states per qubit, shape
TYPE:
|
povms
|
single-qubit effects per qubit, shape
TYPE:
|
circuit
|
ideal circuit applied before the noise channel, or
TYPE:
|
ideal_channel
|
ground-truth channel, if known, to report the distance to during training.
TYPE:
|
__len__ #
__len__() -> int
Number of recorded shots.
Source code in src/tn_noise_learning/dataset.py
110 111 112 | |
__post_init__ #
__post_init__() -> None
Normalise dtypes and validate shapes and label ranges.
Source code in src/tn_noise_learning/dataset.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
batches #
batches(
batch_size: int,
*,
shuffle: bool = True,
seed: int = 0,
drop_last: bool = True,
) -> Iterator[Batch]
Iterate over batches; with drop_last every batch has exactly batch_size shots (one JIT trace).
Source code in src/tn_noise_learning/dataset.py
131 132 133 134 135 136 137 138 139 140 | |
gather #
gather(indices: ndarray) -> Batch
State and effect matrices of the given shots, shape (num_qubits, len(indices), 2, 2) each.
Source code in src/tn_noise_learning/dataset.py
126 127 128 129 | |
load
classmethod
#
load(path: str | Path) -> NoiseDataset
Read a dataset written by save.
Source code in src/tn_noise_learning/dataset.py
157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
save #
save(path: str | Path) -> None
Write the dataset to a zarr store at path (overwriting).
Source code in src/tn_noise_learning/dataset.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 | |
split #
split(
test_fraction: float = 0.1, seed: int = 0
) -> tuple[NoiseDataset, NoiseDataset]
Random disjoint (train, test) split.
Source code in src/tn_noise_learning/dataset.py
118 119 120 121 122 123 124 | |
subset #
subset(indices: ndarray) -> NoiseDataset
Dataset restricted to the given shots; states, povms, circuit and ideal channel are shared.
Source code in src/tn_noise_learning/dataset.py
114 115 116 | |
Topology
dataclass
#
Topology(
num_sites: int, edges: tuple[tuple[int, int], ...]
)
Graph of qubit sites 0..num_sites-1 and the edges that carry virtual bonds.
Site integers are the qubit order of the dataset and of the Qiskit circuit. Edges are
stored normalised as sorted (i, j) pairs with i < j, without duplicates.
| PARAMETER | DESCRIPTION |
|---|---|
num_sites
|
number of qubits.
TYPE:
|
edges
|
pairs of sites connected by a bond. |
__post_init__ #
__post_init__() -> None
Validate sites and normalise the edge list.
Source code in src/tn_noise_learning/topology.py
43 44 45 46 47 48 49 50 51 52 53 54 | |
chain
classmethod
#
chain(num_sites: int) -> Topology
Open 1D chain 0 - 1 - ... - (num_sites - 1).
Source code in src/tn_noise_learning/topology.py
56 57 58 59 | |
degree #
degree(site: int) -> int
Number of bonds attached to site.
Source code in src/tn_noise_learning/topology.py
79 80 81 | |
from_edges
classmethod
#
from_edges(
edges: Iterable[tuple[int, int]],
num_sites: int | None = None,
) -> Topology
Custom topology from an edge list; num_sites defaults to the largest site index plus one.
Source code in src/tn_noise_learning/topology.py
67 68 69 70 71 72 73 | |
neighbors #
neighbors(site: int) -> tuple[int, ...]
Sites bonded to site, in ascending order.
Source code in src/tn_noise_learning/topology.py
75 76 77 | |
square
classmethod
#
square(rows: int, cols: int) -> Topology
Open 2D square lattice, with site (r, c) mapped row-major to r * cols + c.
Source code in src/tn_noise_learning/topology.py
61 62 63 64 65 | |
Trainer #
Trainer(config: TrainingConfig | None = None)
Fit an LPDO to a NoiseDataset.
Source code in src/tn_noise_learning/trainer.py
143 144 | |
fit #
fit(
model: LPDO,
train: NoiseDataset,
test: NoiseDataset | None = None,
ideal_channel: LPDO | None = None,
) -> History
Train model in place and return the training history.
| PARAMETER | DESCRIPTION |
|---|---|
model
|
initial LPDO; on return it holds the parameters of the best epoch.
TYPE:
|
train
|
training shots; its circuit is applied inside the loss.
TYPE:
|
test
|
held-out shots; split off
TYPE:
|
ideal_channel
|
ground truth to report
TYPE:
|
Source code in src/tn_noise_learning/trainer.py
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
TrainingConfig
dataclass
#
TrainingConfig(
epochs: int = 50,
batch_size: int = 256,
learning_rate: ScalarOrSchedule = (
lambda: optax.exponential_decay(
0.01, 200, 0.8, end_value=0.0001
)
)(),
optimizer: Callable[
[ScalarOrSchedule], GradientTransformation
] = optax.adam,
tp_weight: Schedule = 100.0,
tp_warmstart_steps: int = 500,
tp_project_every: int | None = None,
tp_project_steps: int = 50,
extra_loss_terms: dict[
str, tuple[LossTerm, Schedule]
] = dict(),
metrics: dict[str, Metric] = dict(),
monitor: str = "test_loss",
patience: int = 10,
tolerance: float = 0.0001,
test_fraction: float = 0.1,
seed: int = 0,
snapshot_params: bool = False,
log_every: int = 1,
contract_optimize: str = "auto-hq",
)
Hyper-parameters of Trainer.
| PARAMETER | DESCRIPTION |
|---|---|
epochs
|
maximum number of passes over the training set.
TYPE:
|
batch_size
|
shots per gradient step; every batch has exactly this size.
TYPE:
|
learning_rate
|
constant, optax schedule, or callable of the step index; defaults to an exponential decay
from
TYPE:
|
optimizer
|
factory turning the learning rate into an optax optimizer.
TYPE:
|
tp_weight
|
weight of the trace-preservation penalty, constant or callable of the step index. The trained
trace exceeds
TYPE:
|
tp_warmstart_steps
|
L-BFGS-B steps moving the initial model onto the trace-preserving manifold (0 disables).
TYPE:
|
tp_project_every
|
re-run the L-BFGS-B projection every this many steps (
TYPE:
|
tp_project_steps
|
L-BFGS-B steps per projection.
TYPE:
|
extra_loss_terms
|
|
metrics
|
|
monitor
|
metric used for early stopping and model selection.
TYPE:
|
patience
|
epochs without improvement of
TYPE:
|
tolerance
|
minimum decrease of
TYPE:
|
test_fraction
|
held-out fraction used when
TYPE:
|
seed
|
seed for the train/test split and batch shuffling.
TYPE:
|
snapshot_params
|
record the Kraus tensors after every epoch in the history.
TYPE:
|
log_every
|
log one line every this many epochs.
TYPE:
|
contract_optimize
|
cotengra path optimizer for the loss contraction.
TYPE:
|
apply_circuit #
apply_circuit(
states_tn: TensorNetwork, circuit: QuantumCircuit
) -> qtn.TensorNetwork
Evolve a raw state network (legs p, q) to U ρ U† with legs k, b, one tensor per site.
Source code in src/tn_noise_learning/circuits.py
123 124 125 126 127 128 | |
circuit_network #
circuit_network(
circuit: QuantumCircuit,
) -> qtn.TensorNetwork
One tensor per site mapping (p_j, q_j) to (k_j, b_j) such that the whole network is U · U†.
Only one- and two-qubit unitary gates are supported; barriers are skipped.
Source code in src/tn_noise_learning/circuits.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
depolarizing #
depolarizing(topology: Topology, p: float) -> LPDO
Single-qubit depolarizing channel (1 − p) ρ + (p / 3) Σ_{P≠I} P ρ P on every site.
Source code in src/tn_noise_learning/channels.py
79 80 81 | |
full_network #
full_network(
kraus_tn: TensorNetwork, topology: Topology
) -> qtn.TensorNetwork
Join a Kraus network with its conjugate; works on JAX tracers inside a jitted loss.
Source code in src/tn_noise_learning/lpdo.py
89 90 91 92 | |
negative_log_likelihood #
negative_log_likelihood(
lpdo_tn: TensorNetwork, probs: ndarray
) -> jnp.ndarray
-mean(log p) over the batch, the KL divergence up to a data-dependent constant.
Source code in src/tn_noise_learning/losses.py
67 68 69 | |
outcome_distribution #
outcome_distribution(
channel: LPDO,
circuit: QuantumCircuit | None,
state_labels: Labels,
basis_labels: Labels,
*,
states: ndarray,
povms: ndarray,
optimize: str = "auto-hq",
) -> np.ndarray
Joint probability of every bit string for one setting, shape (2,) * num_qubits with qubit 0 first.
Source code in src/tn_noise_learning/simulate.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
pauli_channel #
pauli_channel(
topology: Topology, probs: Iterable[float] | ndarray
) -> LPDO
Independent single-qubit Pauli channels ρ → Σ_P p_P P ρ P.
| PARAMETER | DESCRIPTION |
|---|---|
topology
|
site connectivity (all bonds have size one).
TYPE:
|
probs
|
|
Source code in src/tn_noise_learning/channels.py
65 66 67 68 69 70 71 72 73 74 75 76 | |
pauli_eigenstates #
pauli_eigenstates(num_sites: int) -> np.ndarray
The six Pauli eigenstates on every site, shape (num_sites, 6, 2, 2).
Source code in src/tn_noise_learning/channels.py
49 50 51 | |
plot_history #
plot_history(
history: History, path: str | Path | None = None
) -> Figure
One panel per metric: loss (train and test), NLL, TP penalty, trace, distance to ideal, then custom metrics.
Source code in src/tn_noise_learning/plotting.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
probabilities #
probabilities(
lpdo_tn: TensorNetwork,
states: ndarray | Array,
povms: ndarray | Array,
circuit_tn: TensorNetwork | None = None,
optimize: str = "auto-hq",
) -> jnp.ndarray
Transition probabilities Tr[E Λ(U ρ U†)] for a batch of (state, effect) pairs, clipped from below.
Source code in src/tn_noise_learning/losses.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
product_state_network #
product_state_network(
states: ndarray | Array, *, raw: bool = True
) -> qtn.TensorNetwork
Batched product states states[j, exp] of shape (num_sites, batch, 2, 2) as site tensors.
Each tensor has legs (exp, ket_j, bra_j): p{j}, q{j} with raw=True (to be evolved by a circuit),
otherwise k{j}, b{j}.
Source code in src/tn_noise_learning/circuits.py
111 112 113 114 115 116 117 118 119 120 | |
project_trace_preserving #
project_trace_preserving(model: LPDO, steps: int) -> LPDO
Minimise the trace-preservation penalty alone with L-BFGS-B, starting from model.
Source code in src/tn_noise_learning/trainer.py
100 101 102 103 104 105 106 107 108 109 110 111 | |
sample_shots #
sample_shots(
channel: LPDO,
circuit: QuantumCircuit | None = None,
*,
num_settings: int = 100,
shots_per_setting: int = 100,
seed: int | None = None,
states: ndarray | None = None,
povms: ndarray | None = None,
settings: Sequence[Setting] | None = None,
) -> NoiseDataset
Sample a dataset from channel applied after circuit.
| PARAMETER | DESCRIPTION |
|---|---|
channel
|
ground-truth channel; stored in the dataset as
TYPE:
|
circuit
|
ideal circuit applied before the channel (
TYPE:
|
num_settings
|
number of random
TYPE:
|
shots_per_setting
|
shots drawn per setting.
TYPE:
|
seed
|
seed for settings, shots and the final shuffle.
TYPE:
|
states
|
input states per qubit, shape
TYPE:
|
povms
|
effects per qubit as consecutive basis pairs; default Pauli eigenstates.
TYPE:
|
settings
|
explicit settings, overriding
TYPE:
|
Source code in src/tn_noise_learning/simulate.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
tp_penalty #
tp_penalty(
lpdo_tn: TensorNetwork, probs: ndarray | None = None
) -> jnp.ndarray
Squared relative distance of the output-traced channel from the identity, ‖Tr_out Λ − 𝟙‖_F² / 2**n.
Squaring keeps the gradient proportional to the violation, so large weights act as a smooth quadratic
constraint. The likelihood alone rewards inflating the channel's trace, which this term counteracts; at
weight w the equilibrium trace exceeds 2**n by a factor 1 + 1/(2w).
Source code in src/tn_noise_learning/losses.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
two_qubit_depolarizing #
two_qubit_depolarizing(
topology: Topology,
p: float,
edges: Iterable[tuple[int, int]] | None = None,
) -> LPDO
Two-qubit depolarizing channel (1 − p) ρ + p · Tr[ρ] 𝟙/4 on pairwise-disjoint edges, identity elsewhere.
| PARAMETER | DESCRIPTION |
|---|---|
topology
|
site connectivity.
TYPE:
|
p
|
depolarizing probability; the sixteen two-qubit Paulis get weights
TYPE:
|
edges
|
edges that carry the channel (default: all topology edges). They must be topology edges and must not share sites. |
Source code in src/tn_noise_learning/channels.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |