Quickstart#
uv sync --all-extras
uv run python examples/minimal_example.py
The script below is examples/minimal_example.py.
1. Topology and circuit#
from qiskit import QuantumCircuit
from tn_noise_learning import Topology
topology = Topology.square(2, 2) # sites 0..3, row-major; edges (0,1) (0,2) (1,3) (2,3)
circuit = QuantumCircuit(4)
circuit.cz(0, 1)
circuit.cz(2, 3)
Topology.chain(n) and Topology.from_edges([...]) are the other constructors. Site integers are the qubit
order of the dataset and of the Qiskit circuit. The circuit is the ideal operation; the noise that follows it
is what gets learned. Gates may act on any pair of qubits, not only on topology edges.
2. Data#
from tn_noise_learning import NoiseDataset, sample_shots, two_qubit_depolarizing
truth = two_qubit_depolarizing(topology, p=0.05, edges=[(0, 1), (2, 3)])
sample_shots(truth, circuit, num_settings=300, shots_per_setting=100, seed=0).save("example.zarr")
data = NoiseDataset.load("example.zarr")
train, test = data.split(test_fraction=0.1, seed=0)
A NoiseDataset holds, per shot and per qubit, the label of the prepared state and of the measured effect, plus
the single-qubit states[j] and povms[j] those labels refer to, the ideal circuit (OpenQASM 2), and optionally
the ideal channel. Build one from your own experiment by filling those arrays; see
NoiseDataset for the zarr layout.
3. Model and training#
from tn_noise_learning import LPDO, Trainer, TrainingConfig
model = LPDO.random(topology, bond_dim=4, kraus_dim=4, seed=0)
history = Trainer(TrainingConfig(epochs=30, batch_size=250)).fit(model, train, test)
fit warm-starts the random model onto the trace-preserving manifold with L-BFGS-B, then runs Adam on
NLL + tp_weight · TP². It records per-epoch metrics, stops early when the test loss stops improving, and writes
the best epoch's tensors back into model. Every knob lives in
TrainingConfig; for example
import optax
TrainingConfig(
learning_rate=optax.cosine_decay_schedule(1e-2, decay_steps=5000),
tp_weight=lambda step: 1000.0 if step < 500 else 100.0,
metrics={"bond_dim": lambda m: m.bond_dim},
)
4. Results#
history.plot("history.png")
model.save("model.zarr")
The figure shows train/test loss, NLL, TP penalty, trace (with the 2**n reference) and, because the dataset
carries the ideal channel, the squared normalised distance to it. LPDO.load("model.zarr") restores the model.