# Quantum simulation Source: https://www.tensorplay.cn/docs/quantum.html 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 | [batch, 2, 2, ...] | one amplitude per qubit dimension | | Flat state | [batch, 2**n] | the same amplitudes, qubit dimensions folded | | Density matrix | [batch, 2**n, 2**n] | 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") # , 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) # , 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 | tensorplay.quantum.Circuit |An ordered gate list over a register of n_qubits qubits. | | --- | --- | | tensorplay.quantum.qft_circuit | The quantum Fourier transform as a gate sequence. | | tensorplay.quantum.real_amplitudes | Y rotations interleaved with CNOT pairs, the variational starting point. | | tensorplay.quantum.efficient_su2 | Y and Z rotations interleaved with CNOT pairs. | | tensorplay.quantum.nlocal_circuit | Repeats a rotation layer and an entanglement layer over a register. | | tensorplay.quantum.entanglement_pairs | The ordered (control, target) pairs of an entanglement pattern. | | tensorplay.quantum.apply_gate | Apply the unitary mat to the qubits named by wires. | | tensorplay.quantum.controlled_matrix | mat applied only when the control qubits read control_values. | | tensorplay.quantum.apply_controlled | Apply mat on the last wires, guarded by the leading ones. | | tensorplay.quantum.apply_chadamard | Apply Hadamard on target, controlled by control. | | tensorplay.quantum.apply_shadamard | Apply the quarter-turn Y rotation on wire. | | tensorplay.quantum.apply_sswap | Apply the square root of swap between two qubits. | | tensorplay.quantum.rot_matrix | A rotation of phi radians about any axis. | | tensorplay.quantum.multirz_matrix | A rotation of angle radians about Z on every one of n_qubits. | | tensorplay.quantum.multirz_eigvals | The eigenvalues multirz_matrix() is built from, as a vector. | | tensorplay.quantum.rxx_matrix | A rotation of theta radians generated by X on both wires. | | tensorplay.quantum.ryy_matrix | A rotation of theta radians generated by Y on both wires. | | tensorplay.quantum.rzz_matrix | A rotation of theta radians generated by Z on both wires. | | tensorplay.quantum.rzx_matrix | A rotation generated by Z on the leading wire and X on the other. | | tensorplay.quantum.globalphase_matrix | The scalar e^{i angle} as a 1 by 1 operator. | | tensorplay.quantum.rccx_matrix | The relative-phase Toffoli, on [control a, control b, target]. | | tensorplay.quantum.dcx_matrix | The two-wire controlled X written the way its matrix spells it. | | tensorplay.quantum.ecr_matrix | The echoed cross-resonance gate, the entangling native of one family. | | tensorplay.quantum.rc3x_matrix | The four-wire relative-phase gate on [a, b, c, target]. | | tensorplay.quantum.xx_plus_yy_matrix | The pair mixer that trades one excitation for the other. | | tensorplay.quantum.xx_minus_yy_matrix | The pair mixer that does not touch a state of one excitation. | | tensorplay.quantum.single_excitation_matrix | A real rotation of the pair that carries exactly one excitation. | | tensorplay.quantum.sh_matrix | The shadamard gate: a quarter-turn rotation around Y. | | tensorplay.quantum.sswap_matrix | The square root of swap on [first wire, second wire]. | | tensorplay.quantum.dagger | The conjugate transpose of a gate matrix, batched over leading dims. | | tensorplay.quantum.expand_to_register | Embed a gate matrix into a register-wide unitary on n_qubits qubits. | | tensorplay.quantum.zeros_state | The all-zero computational basis state: amplitude 1 on the first entry. | | tensorplay.quantum.plus_state | The equal superposition over every basis entry of n_qubits. | | tensorplay.quantum.basis_state | The computational basis state named by index, shaped [1, 2, 2, ...]. | | tensorplay.quantum.encode_state | Amplitude-encode classical feature vectors into quantum states. | | tensorplay.quantum.global_phase | The single unit factor that turns one state or gate into the other. | | tensorplay.quantum.Lindbladian | The generator of a Lindblad master equation, as it acts on matrices. | | tensorplay.quantum.lindbladian | The Lindblad generator of a master equation. | | tensorplay.quantum.dissipator | The Lindblad dissipator of jump_ops, applied to density. | | tensorplay.quantum.mesolve | Solve the master equation over time. | | tensorplay.quantum.sesolve | Evolve a closed system, where the generator is the Hamiltonian alone. | | tensorplay.quantum.random_state | A state drawn uniformly from the unit sphere of an n_qubits register. | | tensorplay.quantum.random_unitary | A unitary drawn from the Haar measure on an n_qubits register. | | tensorplay.quantum.state_from_bases | The equal superposition of the listed basis states. | | tensorplay.quantum.product_state | The tensor product of single-qubit states, in the given qubit order. | | tensorplay.quantum.flatten | Reshape a register-shaped state [batch, 2, 2, ...] into one flat row per state. | | tensorplay.quantum.unflatten | Reshape a flat state back into the register layout [batch, 2, 2, ...]; the qubit count is read off the length if unset. | | tensorplay.quantum.norm | The Euclidean norm of each state in the batch. | | tensorplay.quantum.normalize | Rescale each state in the batch to unit length. | | tensorplay.quantum.apply_reset | Project wire onto the zero state and renormalize the register. | | tensorplay.quantum.inner | The bra-ket overlap for each pair of states in the batch. | | tensorplay.quantum.fidelity | The squared bra-ket overlap magnitude for each pair of states in the batch. | | tensorplay.quantum.to_density | The density matrix of each pure state in the batch. | | tensorplay.quantum.bloch2dens | The density matrix a Bloch vector names, (I + r.sigma)/2. | | tensorplay.quantum.bloch2sv | The state a Bloch vector names, the way sv2bloch reads one. | | tensorplay.quantum.dens2bloch | The Bloch vector of a one-qubit density matrix. | | tensorplay.quantum.sv2bloch | The Bloch vector (x, y, z) of a one-qubit state. | | tensorplay.quantum.density_to_state | The pure state whose density matrix is density, when it has one. | | tensorplay.quantum.density_mixture | The eigenstates of each density matrix, as (weight, state) pairs. | | tensorplay.quantum.state_from_counts | The state whose squared amplitudes are the histogram's shares. | | tensorplay.quantum.partial_trace | Trace density down to the qubits in keep, in the given order. | | tensorplay.quantum.probs | The measurement distribution of each state, one probability per basis entry. | | tensorplay.quantum.density_probs | The measurement distribution read off a density matrix's diagonal. | | tensorplay.quantum.density_from_counts | The density matrix a histogram of basis measurements describes. | | tensorplay.quantum.marginal_probs | The distribution of the listed qubits, marginalized over the rest. | | tensorplay.quantum.expectation | The expectation value of the Pauli word word for each state, real and per batch entry. | | tensorplay.quantum.expectation_from_density | tr(P rho) for the Pauli word word, real and per batch entry. | | tensorplay.quantum.apply_pauli_word | Apply the Pauli word word to each state, one factor at a time. | | tensorplay.quantum.apply_measurement_basis | Rotates the state so a plain histogram reads the signs of word. | | tensorplay.quantum.expectation_from_counts | Estimates

for a Pauli word from a histogram of measurements. | | tensorplay.quantum.commuting_groups | Groups Pauli words that one basis rotation can measure together. | | tensorplay.quantum.joint_expectation | Estimates several Pauli words from as few measurement settings as possible. | | tensorplay.quantum.sample_indices | Draw shots basis indices per row by inverse transform sampling. | | tensorplay.quantum.counts_from_indices | Tally sampled basis indices into per-basis counts for each state in the batch. | | tensorplay.quantum.gen_bitstrings | The computational basis labels of n_qubits, in flat-index order. | | tensorplay.quantum.encode_w | Join wire indices into a string key, "1.3.2" for [1, 3, 2]. | | tensorplay.quantum.decode_w | Split a dotted wire key back into a list of ints. | | tensorplay.quantum.measure | Sample shots measurements per state and tally them. | | tensorplay.quantum.readout_error_matrix | The two outcomes one wire reports for each bit it was handed. | | tensorplay.quantum.apply_readout_error | Reports a histogram the way a noisy readout would have. | | tensorplay.quantum.apply_channel | Apply a local channel to the wires of a density matrix. | | tensorplay.quantum.apply_bit_flip | Apply bit-flip noise to one wire of a density matrix. | | tensorplay.quantum.apply_phase_flip | Apply phase-flip noise to one wire of a density matrix. | | tensorplay.quantum.apply_amplitude_damping | Apply amplitude damping to one wire of a density matrix. | | tensorplay.quantum.apply_depolarizing | Apply depolarizing noise to one wire of a density matrix. | | tensorplay.quantum.bit_flip_kraus | Kraus operators of the bit-flip channel: X flips every named wire together. | | tensorplay.quantum.phase_flip_kraus | Kraus operators of the phase-flip channel: Z rotates every named wire together. | | tensorplay.quantum.amplitude_damping_kraus | Kraus operators of amplitude damping: each qubit relaxes to 0 at rate gamma. | | tensorplay.quantum.depolarizing_kraus | Kraus operators of the depolarizing channel: a random Pauli error on n_qubits. | | tensorplay.quantum.pauli_kraus | Kraus operators of a Pauli channel: named errors with their chances. | | tensorplay.quantum.Hamiltonian | A weighted sum of Pauli words. | | tensorplay.quantum.pauli_string_matrix | The dense matrix of a Pauli word. | | tensorplay.quantum.energy | The Hamiltonian expectation value for each state in the batch, real. | | tensorplay.quantum.trotter_evolve | Advance the state by exp(-i * time * H) with a Trotter product. | | tensorplay.quantum.matrix_exp_evolve | Advance the state by the exact exp(-i * time * H). | | tensorplay.quantum.param_shift | The gradient of a value built from one angle per parameter, by shifting. | | tensorplay.quantum.sample_energy | estimated from a finite number of measurements. | ## Where to go next - [Complex numbers](/docs/complex_numbers.html) — the dtype behind every state. - [Autograd](/docs/autograd.html) — how gradients reach a circuit’s parameters.