latest (dev)
Copy
Latest development documentation · Updated 2026-10-15
Quantum simulation
tensorplay.quantum simulates quantum circuits on the tensor hardware. A
state is a tensor of amplitudes, gates are matrices applied to the qubits they
act on, and measurement turns amplitudes into a distribution you can evaluate
exactly or sample.
import tensorplay as tp
from tensorplay import quantum as tq
bell = tq.Circuit(2)
bell.h(0)
bell.cx(0, 1)
state = bell()
tq.probs(state) # [[0.5, 0., 0., 0.5]] — |00> and |11>
tq.expectation(state, "ZZ") # [1.] — the pair is perfectly correlated
tq.measure(state, shots=1000)
Layout
A state is shaped [batch, 2, 2, ...] — one dimension of size 2 per qubit,
after the batch. Qubit 0 leads, so qubit 0 is the most significant bit of the
amplitude index: applying X to wire 0 of |00> gives |10>, which is index 2.
Layout |
Shape |
Meaning |
|---|---|---|
State |
|
one amplitude per qubit dimension |
Flat state |
|
the same amplitudes, qubit dimensions folded |
Density matrix |
|
row index the ket, column index the bra |
Complex dtypes are tensorplay.complex64 and tensorplay.complex128; a state
keeps whichever one it was created with. A batch dimension of 1 is always
present, so a single state is a valid batch of one.
Qubit 0 is the most significant bit throughout — in marginal_probs, in
measure with a wires list, and in the argument index of a Pauli word.
Two-qubit and wider gates read their arguments in the order given: the first
wire named is the first index bit of the gate matrix, which is what makes the
control the control. apply_cnot(state, 1, 0) drives wire 0 from wire 1, and
apply_mcx(state, [0, 2, 1]) uses wires 0 and 2 as controls and flips wire 1.
States
tq.zeros_state(3) # |000>
tq.plus_state(2) # (|00> + |01> + |10> + |11>) / 2
tq.basis_state(5, 3) # |101> on 3 qubits
tq.state_from_bases([0, 3], 2) # (|00> + |11>) / sqrt(2)
tq.product_state([a, b]) # the tensor product of per-qubit vectors
tq.random_state(3) # uniform on the sphere of the register
tq.encode_state([x, y, z]) # amplitude-encode a classical vector
normalize rescales to unit norm, norm reports it, and inner is the
conjugate overlap between two states. to_density and density_to_state cross
between the ket and density views, density_mixture decomposes a density
matrix into its eigenstates and their probabilities, and partial_trace drops
qubits from a density matrix.
global_phase answers the question that comes up whenever two operators are
computed two different ways: do they differ by a phase and nothing else? It
reads that one factor off the whole overlap, so no single entry decides it, and
raises if the two are not the same operator up to a phase.
phase = tq.global_phase(fused_gate, dense_product) # 1.0 if they are the same
Gates
apply_gate is the primitive: a matrix applied to a list of wires. Everything
else — the named gates, controlled gates, circuits — is a layer on top of it.
state = tq.zeros_state(2)
state = tq.apply_h(state, 0) # Hadamard on wire 0
state = tq.apply_cnot(state, 0, 1) # control wire 0, target wire 1
state = tq.apply_gate(state, tq.rot_y_matrix(0.3), [1])
Parameter-free gates: apply_x, apply_y, apply_z, apply_i, apply_h,
apply_s, apply_sdg, apply_sx, apply_t, apply_tdg. Parameterized gates
take the parameter as the last argument: apply_rx, apply_ry, apply_rz,
apply_p, apply_u2, apply_u3, apply_rot (any axis), apply_multirz (one
angle on a list of wires), apply_global_phase (scales the whole state).
Two-qubit gates: apply_cx/apply_cnot, apply_cy, apply_cz, apply_swap,
apply_iswap, apply_dcx (the same controlled X with the target written
first), apply_ecr (echoed cross resonance), the controlled square roots
apply_cs, apply_csdg and apply_csx, and the controlled parameter gates
apply_cp, apply_crx, apply_cry, apply_crz. Wider gates:
apply_toffoli/apply_ccx (three wires), apply_ccz (two controls and a phase),
apply_rccx (the relative-phase Toffoli), apply_rc3x (its four-wire version),
apply_cswap (control plus two targets), and apply_mcx, which takes a control
list and flips the remaining wire.
The chemistry pair gates mix two wires while leaving the register’s excitation
count alone, which is what a particle-number-preserving ansatz needs:
apply_xx_plus_yy, apply_xx_minus_yy and apply_single_excitation, each
taking the pair of wires and an angle.
random_unitary draws a gate uniformly from all the unitaries on a register,
which is what a test or a baseline average wants rather than a matrix of random
entries: the distribution is uniform over the operators themselves, phase and
all.
Two-body rotations take a pair of wires and an angle the same way:
apply_rxx, apply_ryy, apply_rzz and apply_rzx. Each is
exp(-i * angle * P / 2) for a two-wire Pauli product P — ZZ is diagonal, so
it only adds a phase, which is the gate an Ising Hamiltonian is written with. The
first three treat the pair alike; apply_rzx puts Z on the wire named first.
state = tq.apply_rzz(state, [0, 1], 0.4) # a phase on each basis state
state = tq.apply_rxx(state, [0, 1], 0.4) # mixes |01> and |10>
The matching matrix builders are available on their own — h_matrix,
rot_x_matrix, swap_matrix, mcx_matrix and the rest — so a gate can be
built, inspected, or composed without applying it. controlled_matrix wraps any
single-qubit unitary as a control, pauli_matrix builds a Pauli word, and
dagger is the conjugate transpose.
Control readings
A control decides whether the gate fires by what it reads, and all ones is only
the default. control_values says what each control has to hold, in wire order:
tq.apply_mcx(tq.zeros_state(3), [0, 1, 2]) # fires on |11.>
tq.apply_mcx(tq.zeros_state(3), [0, 1, 2], control_values=[0, 0]) # fires when both are 0
apply_controlled(state, mat, wires, control_values=...) is the same idea for
an arbitrary operator, with the controls leading the wire list.
Batched parameters
A gate matrix may carry one entry per batch item, which gives every state in the batch its own parameter:
theta = tp.tensor([0.0, 3.14159]) # per batch item
states = tq.zeros_state(1, batch=2)
tq.apply_ry(states, 0, theta) # first qubit stays 0, second flips
Circuits
A Circuit is an ordered gate list over a fixed register. It holds no state:
calling it starts from |0...0>, and apply threads a state you provide
through the same gates.
qc = tq.Circuit(3)
qc.h(0)
qc.rx(1, 0.4)
qc.cx(0, 1)
qc.barrier() # a marker for transpilation; changes nothing
qc.cx(1, 2)
state = qc() # from |000>
state = qc(some_state) # from a state you choose
state = qc(batch=64) # 64 copies, all following the same gates
add takes any gate name as a string, so gates without a method are still
reachable; gates() returns the recorded list, and len(qc) counts it. The
methods cover the named gates — h, x, y, z, s, sdg, t, tdg,
sx, rx, ry, rz, p, u3, cx, cz, cp, crx, cry, crz,
swap, iswap, cswap, ccx and mcx.
unitary() multiplies the gates into the dense register-wide matrix, and
inverse() returns the circuit that undoes this one — reversed order, with each
gate replaced by its adjoint. qft_circuit(n) builds the Fourier transform as a
gate sequence rather than a dense matrix.
qc.unitary().shape # (8, 8) on 3 qubits
qc.inverse()(qc()) # back to |000>
Ansatz templates
Layer templates lay out a trainable circuit instead of hand by hand.
nlocal_circuit is the general form: reps blocks of “rotate every wire, then
entangle”, a closing rotation layer, and one trainable angle per rotation gate.
tq.real_amplitudes(3, reps=2) # RY rotations and CNOT pairs
tq.efficient_su2(3, reps=2) # RY and RZ rotations, CNOT pairs
tq.nlocal_circuit(3, ("ry", "p"), reps=1, entanglement="circular")
The entanglement pattern is one of "linear", "reverse_linear",
"circular" or "full"; entanglement_pairs(n, pattern) returns the pairs
directly. Angles arrive as module parameters in (-pi, pi), so the template
can go straight to an optimizer:
qc = tq.real_amplitudes(3, reps=2)
loss = tq.expectation(qc.state(), "ZI")
loss.backward()
len(list(qc.parameters())) # 9: three wires, two blocks, one closing layer
Measurement
probs is the squared amplitude of every basis entry — the exact measurement
distribution.
tq.probs(state) # [batch, 2**n]
tq.marginal_probs(state, [0, 2]) # the listed qubits, in the order given
tq.expectation(state, "XIZ") # <psi|P|psi>, real, per batch item
expectation applies the Pauli word one factor at a time, so its cost grows
with the state rather than with its square. apply_pauli_word gives P|psi>
itself.
measure samples the distribution, where the state already lives:
counts = tq.measure(state, shots=4096) # [batch, 2**n]
counts = tq.measure(state, shots=1024, wires=[0]) # [batch, 2]
Sampling walks the cumulative distribution with one uniform per shot, so a
sampled batch never leaves the device. sample_indices returns the raw draws
and counts_from_indices tallies them if you want the two steps separately.
Reading values back out of shots
A Pauli word that touches an X or a Y wire cannot be read off a plain
histogram until the wires are turned: apply_measurement_basis puts the state
into the frame where measuring in Z reports each factor’s eigenvalue. The
histogram then averages those signs, which is expectation_from_counts.
readable = tq.apply_measurement_basis(state, "XZ")
counts = tq.measure(readable, shots=4096, wires=[0, 2])
tq.expectation_from_counts(counts, "XZ", wires=[0, 2]) # [batch]
Words that agree on every wire where both of them act can share one such
rotation: each is a sub-word of the group’s product, so a single histogram
reads all of them. commuting_groups finds the groups, joint_expectation
runs them and returns one estimate per word — fewer settings means fewer shots
for the same noise.
tq.commuting_groups(["XZ", "XI", "ZZ"]) # [['XZ', 'XI'], ['ZZ']]
tq.joint_expectation(state, ["XZ", "XI", "ZZ"], shots=4096) # [batch, 3]
A readout that lies
A device reports a bit that was prepared wrong now and then, and the two
directions usually differ. A readout matrix records that: row p is what gets
reported for a prepared p, so it is a distribution over the reported bit.
noisy = tq.readout_error_matrix(0.02) # 2% in both directions
worse = tq.readout_error_matrix(0.02, 0.05) # 2% of 1s, 5% of 0s
measure takes one and decides every shot’s reading on its own, which is how a
real readout behaves:
counts = tq.measure(state, shots=4096, readout=noisy)
Give one matrix per wire as [n, 2, 2] when the wires were calibrated apart.
apply_readout_error applies the same map to a histogram or an exact
distribution, so an offline histogram can be corrected — or biased, which is
what it is usually wanted for.
Reading a state back out of a histogram
A histogram in the computational basis is the diagonal of a density matrix and
nothing else. density_from_counts hands that diagonal back, which is the state
the measurements cannot rule out and no more: every off-diagonal term is a phase,
and a basis measurement never sees a phase.
rho = tq.density_from_counts(counts) # populations, no phases
state = tq.state_from_counts(counts) # the square roots of them
state_from_counts names the member of that family a histogram leaves free whose
amplitudes are real and non-negative, which is the one overlapping the measured
state the most. Comparing it with a theory compares populations; the phases come
from histograms taken in rotated bases, which is what joint_expectation reads.
An outcome that never came out of a finite number of shots stays out. Pass
pseudocount= to lift every outcome off the floor before normalizing, which
makes the density matrix full rank for a tomography fit — and makes the answer
wrong by that much, which is what it says on the tin.
Noise
Channels act on density matrices, given as Kraus operators. Each one is a set
of local matrices K_r, and applying it means rho -> sum_r K_r rho K_r^dagger.
rho = tq.to_density(state)
rho = tq.apply_bit_flip(rho, 0.01, wire=0)
rho = tq.apply_phase_flip(rho, 0.02, wire=1)
rho = tq.apply_amplitude_damping(rho, 0.05, wire=2)
rho = tq.apply_depolarizing(rho, 0.01, wire=0)
bit_flip_kraus, phase_flip_kraus, amplitude_damping_kraus and
depolarizing_kraus return the operator sets, and apply_channel takes any set
you build yourself, on any list of wires. Every builder takes n_qubits=: one
wire by default, and more wires as the kind of error the name says. bit_flip
and phase_flip are then correlated — every named wire flips together or none
does — depolarizing draws a uniform non-identity Pauli word over the whole
register, and amplitude_damping damps every wire independently:
kraus = tq.bit_flip_kraus(0.1) # one wire: X with chance 0.1
both = tq.bit_flip_kraus(0.1, 2) # two wires: X X together, or nothing
spread = tq.depolarizing_kraus(0.1, 2) # 15 errors, each with chance 0.1/15
relax = tq.amplitude_damping_kraus(0.05, 2) # 4 operators, one per way to decay
rho = tq.apply_channel(rho, both, [0, 1])
A chance can be a differentiable tensor, so a noise rate is a learned parameter:
pauli_kraus({"XX": p, "ZI": 0.05}) keeps the gradient on p the way the
single-wire builders always have.
Because every Kraus operator is local, the cost stays linear in the register size: no channel superoperator is ever built.
pauli_kraus builds the set from a noise model written down the way noise
models are: which errors happen with what chance, with the rest of the chance
left to nothing happening.
kraus = tq.pauli_kraus({"XX": 0.02, "ZI": 0.01, "IX": 0.005})
rho = tq.apply_channel(rho, kraus, [0, 1]) # 96.5% of nothing happening
Every named word must span the same number of wires, and one named on a single wire of a wider register still arrives as an operator on the whole of it. A set of uniform chances over every word is the depolarizing channel; a named set is the general one, and this is the way to write a two-qubit noise model at all.
A system that does not close
A channel applies all at once. Noise that acts over time is the generator
d rho / dt = -i [H, rho] + sum_r (L_r rho L_r^dagger - 1/2 {L_r^dagger L_r, rho}),
and Lindbladian is that generator, held as an object and applied as often as an
integration needs.
generator = tq.Lindbladian(h2, jump_ops=["XI", ("IZ", 0.05)])
final = generator.evolve(rho, time=2.0)
The generator itself is never built. Written out densely it is 2**n times the
size of rho squared – on ten qubits, a matrix of a trillion entries – so
each term is applied on its own and the integral is formed by composition alone.
A jump operator is a Pauli word, a (wires, matrix) pair, or a full-register
matrix, and a bare Pauli word means a rate of one. A rate can be written next to
it, which is the short way to say how fast the error happens:
tq.Lindbladian(h2, [("XI", 0.02), ("ZI", 0.01)]) # Pauli errors at rates
tq.Lindbladian(h2, [([0], 0.05 ** 0.5 * excitation), n_qubits=4])
A word of identities and Zs is a sign pattern rather than a general matrix, so
its whole dissipator is one factor per entry of rho and costs a single pass. A
stack of jump operators is read one at a time, the same way.
A jump carries its rate in its own size — sqrt(gamma) * L is an error happening
at rate gamma, which is the master equation’s own form of the statement — so
amplitude_damping_kraus(gamma)[1] is the sqrt(gamma) |0><1| this wants.
A channel is not a generator, and handing one in as if it were is the one way
to get a plausible answer that is quietly wrong. A channel is the exponential of
a generator: summing dissipators over its Kraus operators applies the channel
once and subtracts the state once per operator, which is not the exponential of
anything. The population decay comes out right and the coherence decay does not,
by a few percent, and nothing looks amiss. So a stack handed in here is read as a
list of jumps, and a channel is applied to a density matrix with apply_channel
instead — which is where a channel belongs.
Two routes integrate the generator, and both keep the trace:
generator.evolve(rho, 2.0, method="rk4") # fixed fourth-order steps
generator.evolve(rho, 2.0, method="taylor") # a truncated series of the span
"rk4" is the one that can follow a Hamiltonian that moves with time, given as a
callable of t, and the one that reports intermediate times. "taylor" sums a
series of the whole span, which costs several times less per unit of time
covered and lands closer to the exact answer — the default wherever it applies,
short spans and long ones alike.
No span is out of reach for the series. Its terms climb to about exp(span)
while the sum they add to stays the size of the state, so one sum can only carry
so far before it cancels itself away in the rounding; past that point the span
is covered by several sums of a size one can carry, in a row. Advancing by a
then by b is advancing by a + b, each half applied to the matrix the one
before left, and the propagator is still never built — a propagator can only be
squared as an operator, and this generator is never built to square.
What an experiment wants is the observables along the way:
states, values = generator.trajectory(
rho, 2.0, n_sample=101, observables=[h2, "IZ"]
)
values is [time, observable, batch], real. Without a sample count,
mesolve answers with the final matrix on its own; sesolve is the closed-system
counterpart, which advances a state rather than its square and so costs 2**n
per step instead of 4**n.
tq.sesolve(state, h2, time=2.0, order=2, n_sample=51, observables=[h2])
tq.mesolve(rho, h2, None, time=2.0, n_sample=51, observables=[h2]) # same answer
Hamiltonians and time evolution
A Hamiltonian is a weighted sum of Pauli words.
h2 = tq.Hamiltonian([
(-1.0523732, "II"),
(0.3979374, "IZ"),
(-0.3979374, "ZI"),
(-0.1809312, "XX"),
])
tq.energy(state, h2) # <psi|H|psi>, real, per batch item
h2.matrix() # the dense matrix, on the wires the words span
Every Pauli word squares to the identity, so exp(-i * t * c * P) is a
rotation about that word, cos(t * c) I - i sin(t * c) P, with a closed form.
pauli_rotation_matrix builds it, and trotter_evolve applies one rotation per
term in sequence:
evolved = tq.trotter_evolve(state, h2, time=1.5, order=4)
The split is exact for commuting terms. Otherwise the order is what decides how
carefully the non-commuting pieces are dropped, and each step is order times
the work: a first-order product has an error that falls with the step, the
palindromic second-order one with the square of it, and the fourth-order one –
that scheme composed three times with uneven weights – with the fourth power.
For small registers, matrix_exp_evolve gives the exact propagator from the
dense matrix instead, and sesolve picks between the routes.
Gradients
Every gate is differentiable in its parameters, so a circuit is differentiable
in whatever tensors you hand it — tq.energy included. Registering the
parameters as nn.Parameter is all it takes to optimize a circuit: a parameter
handed to a gate becomes one of the circuit’s own, named after the gate it
belongs to.
import tensorplay.nn as nn
h2 = tq.Hamiltonian([(-1.0523732, "II"), (0.3979374, "IZ"),
(-0.3979374, "ZI"), (-0.1809312, "XX")])
qc = tq.Circuit(2)
theta0 = nn.Parameter(tp.tensor(0.2))
theta1 = nn.Parameter(tp.tensor(-0.4))
qc.ry(0, theta0)
qc.ry(1, theta1)
qc.cx(0, 1)
optimizer = tp.optim.SGD(qc.parameters(), lr=0.05)
loss = tq.energy(qc(), h2).sum()
loss.backward()
optimizer.step()
Inside a recorded graph a gate is evaluated as a permutation, a batched matrix multiply, and the inverse permutation — all differentiable primitives, with the gradient carried through them. Outside one, the compiled kernel applies the gate in a single pass over the amplitudes.
The measurement side stays compiled either way. expectation, probs and
marginal_probs read the amplitudes in one pass and are linear in |psi|², so
inside a graph they hand the backward the arithmetic they would have done
anyway: the gradient of a Pauli word is that word applied to the state, and the
gradient of a distribution is the state times the weights the forward already
computed. A trained circuit therefore pays for one pass forward and one pass per
non-identity factor backward, rather than rebuilding a rotated copy of the state
and threading a graph through it.
Gradients without a graph
Two situations call for something else. One is a value that is not built out of differentiable primitives at all: an energy estimated from a finite number of measurements has nothing to backpropagate through. The other is a circuit deep enough that backpropagating through every rotation costs more than evaluating the whole thing twice.
sample_energy answers the first — the Hamiltonian measured with as few
measurement settings as its terms allow, optionally through a readout matrix so
the estimate carries the error a device would add:
value = tq.sample_energy(state, h2, shots=4000)
param_shift answers the second, and answers the first too. For a value that
depends on one angle through exp(-i θ G), the derivative is a difference of
the value at two displaced angles, and it is exact — independent of how deep the
circuit is and of what sits in the middle of it, a sampler included:
def energy(angles):
return tq.energy(tq.apply_cnot(
tq.apply_ry(tq.apply_ry(tq.zeros_state(2, batch=angles.shape[0]),
0, angles[:, 0]),
1, angles[:, 1]), 0, 1), h2)
angles = tp.tensor([[0.2, -0.4]])
gradients = tq.param_shift(energy, angles) # no graph anywhere
sampled = lambda a: tq.sample_energy(build(a), h2, 2000)
noisy = tq.param_shift(sampled, angles) # a value with no graph either
Every angle gate here is a rotation about half a Pauli word, whose eigenvalues
are ±1/2, so the default displacement of π/2 and weight of one half are the
rule for all of them. A generator whose eigenvalues differ by ω instead wants
π / (2 ω) and a weight of ω / (2 sin(ω shift)); both are arguments, so the
rule follows the gate rather than assuming it.
The rule reaches an angle that enters through one rotation and stops there. A controlled rotation is not one exponential — where the control is unset its generator reads zero — so the value carries half the frequency as well as the whole one, and no single displacement reaches both. Those angles want the differentiated gradient, which covers them.
One run, three ways
The three routes are the same run to a variational search: the same circuit, the same Hamiltonian, the same optimizer, and a different way of getting the direction to walk in.
h2 = tq.Hamiltonian([(-1.0523732, "II"), (0.3979374, "IZ"),
(-0.3979374, "ZI"), (-0.1809312, "XX")])
exact = float(tp.linalg.eigvalsh(h2.matrix())[0]) # -1.868555
def circuit(angles): # 3 layers, 12 angles
state = tq.zeros_state(2)
for layer in range(3):
for qubit in range(2):
state = tq.apply_ry(state, qubit, angles[0, 4 * layer + qubit])
state = tq.apply_rz(state, qubit, angles[0, 4 * layer + qubit + 1])
state = tq.apply_cnot(state, 0, 1)
return state
energy = lambda a: tq.energy(circuit(a), h2)
theta = nn.Parameter(tp.full((1, 12), 0.4))
optimizer = tp.optim.Adam([theta], lr=0.1)
for step in range(80):
optimizer.zero_grad()
energy(theta).backward() # through the circuit
# theta.backward(tq.param_shift(energy, theta.detach())) # by displacing
# theta.backward(tq.param_shift(
# lambda a: tq.sample_energy(circuit(a), h2, 4000),
# theta.detach())) # and in shots
optimizer.step()
Backpropagating lands about two thousandths above the exact ground energy, the displaced rule about three millionths — it is exact, so what is left is the optimizer’s own step size — and four thousand shots per evaluation about a hundred-thousandth, which is where the shot noise puts it.
Precision
A register’s own dtype is what a whole circuit runs at: a double-precision state stays double through every gate, and a single-precision one stays single even when a matrix arrives wider. The amplitude a gate produces is only as precise as the register holding it, so the place to decide is where the state is built. An angle written as a plain number is read in double precision on the way to the matrix, which costs nothing and keeps a double-precision register from being fed a single-precision matrix.
wide = tq.zeros_state(2, dtype=tp.complex128) # this is what decides
tq.apply_ry(wide, 0, 0.3) # double-precision amplitudes
tq.apply_ry(tq.zeros_state(2), 0, 0.3) # single, whatever the angle says
One qubit as a point in the ball
A single qubit is a point of the unit ball, and reading a simulation off in those coordinates is what makes a comparison readable: the distance between two states is the angle between their vectors, a mixed state is inside rather than on, and a fit lands somewhere you can look at.
vector = tq.sv2bloch(state) # (x, y, z), real, per batch entry
tq.dens2bloch(density) # the same reading off a mixed state
tq.bloch2dens(vector) # back to the matrix it names
tq.bloch2sv(vector) # and the state, when there is one
A normalized state is on the surface; how far inside a mixed one sits says how mixed it is, being twice the larger eigenvalue less one. The reading is real whatever the state’s own precision, and the reverse is exact for a mixed vector.
Reference
|
An ordered gate list over a register of |
|
The quantum Fourier transform as a gate sequence. |
|
Y rotations interleaved with CNOT pairs, the variational starting point. |
|
Y and Z rotations interleaved with CNOT pairs. |
|
Repeats a rotation layer and an entanglement layer over a register. |
|
The ordered |
|
Apply the unitary |
|
|
|
Apply |
|
Apply Hadamard on |
|
Apply the quarter-turn Y rotation on |
|
Apply the square root of swap between two qubits. |
|
A rotation of |
|
A rotation of |
|
The eigenvalues |
|
A rotation of |
|
A rotation of |
|
A rotation of |
|
A rotation generated by |
|
The scalar |
|
The relative-phase Toffoli, on |
|
The two-wire controlled X written the way its matrix spells it. |
|
The echoed cross-resonance gate, the entangling native of one family. |
|
The four-wire relative-phase gate on |
|
The pair mixer that trades one excitation for the other. |
|
The pair mixer that does not touch a state of one excitation. |
|
A real rotation of the pair that carries exactly one excitation. |
|
The |
|
The square root of swap on |
|
The conjugate transpose of a gate matrix, batched over leading dims. |
|
Embed a gate matrix into a register-wide unitary on |
|
The all-zero computational basis state: amplitude 1 on the first entry. |
|
The equal superposition over every basis entry of |
|
The computational basis state named by |
|
Amplitude-encode classical feature vectors into quantum states. |
|
The single unit factor that turns one state or gate into the other. |
|
The generator of a Lindblad master equation, as it acts on matrices. |
|
The Lindblad generator of a master equation. |
|
The Lindblad dissipator of |
|
Solve the master equation over |
|
Evolve a closed system, where the generator is the Hamiltonian alone. |
|
A state drawn uniformly from the unit sphere of an |
|
A unitary drawn from the Haar measure on an |
|
The equal superposition of the listed basis states. |
|
The tensor product of single-qubit states, in the given qubit order. |
|
Reshape a register-shaped state |
|
Reshape a flat state back into the register layout |
|
The Euclidean norm of each state in the batch. |
|
Rescale each state in the batch to unit length. |
|
Project |
|
The bra-ket overlap for each pair of states in the batch. |
|
The squared bra-ket overlap magnitude for each pair of states in the batch. |
|
The density matrix of each pure state in the batch. |
|
The density matrix a Bloch vector names, |
|
The state a Bloch vector names, the way |
|
The Bloch vector of a one-qubit density matrix. |
|
The Bloch vector |
|
The pure state whose density matrix is |
|
The eigenstates of each density matrix, as |
|
The state whose squared amplitudes are the histogram's shares. |
|
Trace |
|
The measurement distribution of each state, one probability per basis entry. |
|
The measurement distribution read off a density matrix's diagonal. |
|
The density matrix a histogram of basis measurements describes. |
|
The distribution of the listed qubits, marginalized over the rest. |
|
The expectation value of the Pauli word |
|
|
|
Apply the Pauli word |
|
Rotates the state so a plain histogram reads the signs of |
|
Estimates |
|
Groups Pauli words that one basis rotation can measure together. |
|
Estimates several Pauli words from as few measurement settings as possible. |
|
Draw |
|
Tally sampled basis indices into per-basis counts for each state in the batch. |
|
The computational basis labels of |
|
Join wire indices into a string key, |
|
Split a dotted wire key back into a list of ints. |
|
Sample |
|
The two outcomes one wire reports for each bit it was handed. |
|
Reports a histogram the way a noisy readout would have. |
|
Apply a local channel to the wires of a density matrix. |
|
Apply bit-flip noise to one wire of a density matrix. |
|
Apply phase-flip noise to one wire of a density matrix. |
|
Apply amplitude damping to one wire of a density matrix. |
|
Apply depolarizing noise to one wire of a density matrix. |
|
Kraus operators of the bit-flip channel: X flips every named wire together. |
|
Kraus operators of the phase-flip channel: Z rotates every named wire together. |
|
Kraus operators of amplitude damping: each qubit relaxes to 0 at rate |
|
Kraus operators of the depolarizing channel: a random Pauli error on |
|
Kraus operators of a Pauli channel: named errors with their chances. |
|
A weighted sum of Pauli words. |
|
The dense matrix of a Pauli word. |
|
The Hamiltonian expectation value for each state in the batch, real. |
|
Advance the state by |
|
Advance the state by the exact |
|
The gradient of a value built from one angle per parameter, by shifting. |
|
|
Where to go next
Complex numbers — the dtype behind every state.
Autograd — how gradients reach a circuit’s parameters.
Help improve this page
Found an error, an unclear step, or a missing example?
Quantization - tensorplay.ao.quantization
tensorplay.ao is the model optimization toolkit, and quantization is its largest part: it converts a float model to run arithmetic on 8-bit integers (roughly a 4x shrink in memory for weights), trading a controlled amoun
Reproducibility
If an operation depends on randomness or on non-deterministic kernels (for example some parallel reductions, or GPU kernels), two runs may not produce bit-identical results. TensorPlay provides a set of helpers to reques

