API reference¶
Core¶
This module defines Circuit and the setting for circuit. Modernized for PyTorch Tensor Network backend integration in 2026.
- class blueqat.circuit.Circuit(n_qubits=0, ops=None)[source]¶
Store the gate operations and call the backends.
- dagger(ignore_measurement=False)[source]¶
Make Hermitian conjugate of the circuit.
If the circuit contains measurement or reset (which have no Hermitian conjugate), ValueError is raised, unless ignore_measurement is True, in which case those operations are simply dropped.
- run(backend=None, *args, **kwargs)[source]¶
Run the circuit. Passes parameters to the PyTorch-based backend.
Beyond the backend’s own arguments (
shots,returns,mode,hamiltonian,amplitude,initial, …), two arguments shape sampled results:seedFix every random draw of this run – shot sampling, mid-circuit collapse and large-
nperfect sampling – so that the same circuit and seed give the same counts. It drives a privatetorch.Generator, leaving the global RNG untouched.bit_orderLayout of the counts keys:
'q0_last'(the default, and blueqat’s long-standing order, wherekey[-1]is qubit 0) or'q0_first', wherekey[i]is qubit i, as cloud APIs report it. Keys are zero-padded ton_qubitsin either order.
- statevector(backend=None, bit_order='q0_last', **kwargs)[source]¶
Run the circuit and get a statevector as a PyTorch Tensor to keep gradients intact.
Amplitude
v[k]belongs to the basis state whose qubitqis bitqofk– qubit 0 is the least-significant bit of the index, the same convention as everywhere else in the SDK and as blueqat.BIT_ORDER reports. So for two qubits the order is |00>, |01> with qubit 0 set, |10> with qubit 1 set, |11>.Reading it the other way round is a mistake nothing catches: it gives the mirror image of the answer, and on a symmetric state – a GHZ, a W, anything permutation-invariant – the two agree, so it can go unnoticed through a whole set of examples and fail on the one that is not symmetric.
bit_order=’q0_first’ puts qubit 0 in the most-significant bit instead, as some other toolkits do. It is the same argument name and the same values that run(shots=…) and probs() take. Having it on some of the three and not the others is itself the trap, because then “I checked with run()” stops being an answer about the other two – and before this it was worse than absent here: the argument was accepted, validated, and then silently ignored, so asking for the other convention returned the default one with nothing said.
- Parameters:
backend (BackendUnion)
bit_order (str)
- Return type:
Tensor
- shots(shots, backend=None, **kwargs)[source]¶
Run the circuit and get shot counts as a result.
Accepts the same
seedandbit_orderarguments asrun().
- oneshot(backend=None, **kwargs)[source]¶
Run the circuit once and return the post-measurement statevector together with the single measured bitstring.
- depth()[source]¶
Circuit depth: length of the longest gate sequence on any qubit path, counting each expanded gate application (as in Qiskit). Barriers don’t add depth.
- Return type:
- probs(qubits=None, backend=None, bit_order='q0_last', **kwargs)[source]¶
Measurement probabilities of the circuit’s final state, optionally marginalized onto qubits (as in PennyLane’s qml.probs).
Returns a tensor of length 2**len(qubits). By default index bit j is the outcome of qubits[j]: the first listed qubit is the least-significant bit of the index, matching the SDK-wide convention and what blueqat.BIT_ORDER reports. Differentiable.
bit_order=’q0_first’ reverses that, putting the first listed qubit in the most-significant bit, which is what some other toolkits do. The name and the values are the same ones run(shots=…) takes, so the two do not have to be remembered separately.
Under noise there is no statevector to square; the probabilities are the density matrix’s diagonal, and are read from there.
- expect(hamiltonian, backend=None, **kwargs)[source]¶
Expectation value <psi|H|psi> of a Pauli-expression Hamiltonian on the circuit’s final state. Differentiable.
A Hamiltonian is a Pauli expression –
Z[0] + 0.5 * X[1], built from the operators exported at the top level – or a string ("Z[0] + 0.5*X[1]"). A dict of coefficients is not one, and neither is a matrix.The circuit may be narrower than the Hamiltonian: qubits the circuit never mentions are in |0>, which is a perfectly good state to take an expectation in, and
Circuit().expect(Z[0]) == 1rather than an error about a zero-qubit state.- Parameters:
hamiltonian (Any)
backend (BackendUnion)
- Return type:
Tensor
- exp_pauli(paulis, theta)[source]¶
Append
exp(-i * theta * P), the time evolution of a single Pauli product.paulis maps a qubit index to its Pauli letter, so the operator is stated without reference to any bit order or overall width:
Circuit().exp_pauli({0: 'X', 1: 'X', 2: 'Z', 3: 'Y'}, 0.3) # exp(-0.3i XXZY)
Since
P**2 == I, this is exactlycos(theta) - i sin(theta) P. The convention (no factor of 1/2) matchesget_time_evolution(); note that a single-qubit{q: 'Z'}is thereforerz(2 * theta)[q].theta may be a
torch.Tensor, in which case the gradient flows through. Letters are case-insensitive, and'I'entries are ignored. A product of nothing but identities is a global phase, which a statevector does not carry, so it appends no gates.
- block(name)[source]¶
Group the operations appended inside the with body into a named, nestable block (as in the sub-circuits of Shor’s algorithm):
c = Circuit(4) with c.block("QFT"): c.h[0].cphase(math.pi / 2)[0, 1] ...
Blocks change nothing about execution – every backend transparently sees the inner gates – but the structure is kept in repr(), Circuit.tree(), and survives dagger() (as a mirrored block named name + ‘†’).
- Parameters:
name (str)
- Return type:
_BlockContext
- append_block(name, subcircuit, offset=0)[source]¶
Append an existing circuit as a named block.
offset shifts every qubit index of subcircuit, so a library circuit built on qubits 0..k can be placed anywhere. Shifting resolves slice targets against subcircuit.n_qubits and preserves any nested block structure inside subcircuit.
- tree()[source]¶
A text rendering of the circuit’s nested block structure:
Circuit(4) ├─ h[0] └─ QFT ├─ cphase(1.5708)[0, 1] └─ ...
Blocks appear by name with their contents beneath them; plain gates outside any block are listed at the top level.
- Return type:
- ancilla(n=1, pos=None, stop=None, reset=True)[source]¶
Context manager allocating temporary ancilla qubit(s) for use inside the with block.
By default, appends n fresh qubits past the circuit’s current width:
- with c.ancilla() as a:
c.cx[0, a[0]]
pos/stop instead pin the ancilla range to specific qubit indices (range(pos, stop); stop defaults to pos + n):
- with c.ancilla(pos=4, stop=6, reset=True) as a:
c.cx[3, a[0]]
If reset is true (the default), a reset gate is appended for each ancilla qubit on exiting the block, so they’re back at
|0>and safe to reuse elsewhere in the circuit.
- class blueqat.circuit.BlueqatGlobalSetting[source]¶
Setting for Blueqat.
- static register_gate(name, gateclass, allow_overwrite=False)[source]¶
Register new gate to gate set.
- static unregister_gate(name)[source]¶
Unregister a gate from gate set.
- Parameters:
name (str)
- Return type:
None
- static unregister_backend(name)[source]¶
Unregister a backend.
- Parameters:
name (str)
- Return type:
None
gate module implements quantum gate operations. Modernized for PyTorch Tensor Network integration in 2026.
- class blueqat.gate.Operation(targets, params=())[source]¶
Abstract quantum circuit operation class.
- class blueqat.gate.OneQubitGate(targets, params=())[source]¶
Abstract quantum gate class for 1 qubit gate.
- class blueqat.gate.TwoQubitGate(targets, params=())[source]¶
Abstract quantum gate class for 2 qubits gate.
- property n_qargs¶
Number of qubit arguments of this gate.
- class blueqat.gate.HGate(targets, params=())[source]¶
Hadamard gate
- class blueqat.gate.IGate(targets, params=())[source]¶
Identity gate
- class blueqat.gate.Mat1Gate(targets, mat)[source]¶
Arbitrary 2x2 matrix gate
- Parameters:
mat (Tensor)
- class blueqat.gate.PhaseGate(targets, theta)[source]¶
Phase gate
- class blueqat.gate.RXGate(targets, theta)[source]¶
Rotate-X gate
- class blueqat.gate.RYGate(targets, theta)[source]¶
Rotate-Y gate
- class blueqat.gate.RZGate(targets, theta)[source]¶
Rotate-Z gate
- class blueqat.gate.SGate(targets, params=())[source]¶
S gate
- class blueqat.gate.SDagGate(targets, params=())[source]¶
Dagger of S gate
- class blueqat.gate.SXGate(targets, params=())[source]¶
sqrt(X) gate
- class blueqat.gate.SXDagGate(targets, params=())[source]¶
sqrt(X)† gate
- class blueqat.gate.TGate(targets, params=())[source]¶
T gate
- class blueqat.gate.TDagGate(targets, params=())[source]¶
Dagger of T gate
- class blueqat.gate.ToffoliGate(targets, params=())[source]¶
Toffoli (CCX) gate
- property n_qargs¶
Number of qubit arguments of this gate.
- class blueqat.gate.UGate(targets, theta, phi, lam, gamma=0.0)[source]¶
Arbitrary 1 qubit unitary gate
- class blueqat.gate.XGate(targets, params=())[source]¶
Pauli’s X gate
- class blueqat.gate.YGate(targets, params=())[source]¶
Pauli’s Y gate
- class blueqat.gate.ZGate(targets, params=())[source]¶
Pauli’s Z gate
- class blueqat.gate.CCZGate(targets, params=())[source]¶
2-Controlled Z gate
- property n_qargs¶
Number of qubit arguments of this gate.
- class blueqat.gate.CHGate(targets, params=())[source]¶
Controlled-H gate
- class blueqat.gate.CPhaseGate(targets, theta)[source]¶
Controlled Phase gate
- class blueqat.gate.CRXGate(targets, theta)[source]¶
Controlled RX gate
- class blueqat.gate.CRYGate(targets, theta)[source]¶
Controlled RY gate
- class blueqat.gate.CRZGate(targets, theta)[source]¶
Controlled RZ gate
- class blueqat.gate.CSwapGate(targets, params=())[source]¶
Controlled SWAP gate
- property n_qargs¶
Number of qubit arguments of this gate.
- class blueqat.gate.CUGate(targets, theta, phi, lam, gamma=0.0)[source]¶
Controlled-U gate
- class blueqat.gate.CXGate(targets, params=())[source]¶
Controlled-X (CNOT) gate
- class blueqat.gate.CYGate(targets, params=())[source]¶
Controlled-Y gate
- class blueqat.gate.CZGate(targets, params=())[source]¶
Controlled-Z gate
- class blueqat.gate.RXXGate(targets, theta)[source]¶
Rotate-XX gate
- class blueqat.gate.RYYGate(targets, theta)[source]¶
Rotate-YY gate
- class blueqat.gate.RZZGate(targets, theta)[source]¶
Rotate-ZZ gate
- class blueqat.gate.SwapGate(targets, params=())[source]¶
Swap gate
- class blueqat.gate.ZZGate(targets)[source]¶
ZZ gate
- class blueqat.gate.ZZDagGate(targets)[source]¶
Dagger of ZZ gate
- class blueqat.gate.ISwapGate(targets, params=())[source]¶
iSWAP gate: swaps two qubits and phases the swapped amplitudes by i.
- class blueqat.gate.ISwapDagGate(targets, params=())[source]¶
Dagger of iSWAP gate.
- class blueqat.gate.ExchangeGate(targets, theta)[source]¶
Heisenberg exchange pulse, the native primitive of exchange-only (EO) spin-qubit hardware: U(theta) = exp(-i theta/2 (SWAP - I)), i.e. identity on the triplet (symmetric) subspace and phase e^{i theta} on the singlet.
theta = J*t is the integrated pulse area (exchange integral x duration); theta = pi gives an exact SWAP, theta = pi/2 a sqrt-SWAP up to phase. Symmetric in its two qubits.
- class blueqat.gate.Barrier(targets, params=())[source]¶
Barrier: a no-op marker separating circuit sections (as in Qiskit and OpenQASM). Simulation backends treat it as the identity via its empty fallback; the QASM output backend emits a real barrier statement.
- class blueqat.gate.GateBlock(name, ops=None)[source]¶
A named group of operations, nestable to arbitrary depth.
Blocks give circuits the hierarchical structure of real algorithms (Shor = init + modular exponentiation + inverse QFT, each built from smaller blocks) without changing how they execute: every backend sees the inner operations through fallback(), so simulation, QASM output and transpilation are unaffected. The structure shows up in repr() and in Circuit.tree().
Build blocks with Circuit.block(name) (a context manager) or Circuit.append_block(name, subcircuit).
- class blueqat.gate.Measurement(targets, options)[source]¶
Measurement operation
Pauli operators, VQE and QAOA¶
Integrated Quantum Operators, Utilities, VQE, and QAOA module with PyTorch. Refactored and merged into a unified utils.py module with robust Autograd tracking.
- class blueqat.utils.Term(ops, coeff)[source]¶
- blueqat.utils.term_from_chars(chars)[source]¶
Make Pauli’s Term from chars written as ‘X’, ‘Y’, ‘Z’ or ‘I’.
- blueqat.utils.commutator(expr1, expr2)[source]¶
Returns [expr1, expr2] = expr1 * expr2 - expr2 * expr1.
- blueqat.utils.is_commutable(expr1, expr2, eps=1e-08)[source]¶
Test whether expr1 and expr2 are commutable.
- blueqat.utils.parse_hamiltonian(text)[source]¶
Parse a Pauli-expression string like
"1.5*Z[0]*Z[1] - 0.5*X0 + 2"into anExpr, without using eval (safe for untrusted input, e.g. tool calls arriving over MCP).Grammar: terms joined by
+/-; each term is an optional numeric coefficient and a product of Pauli factorsX/Y/Z/Iwith the qubit index written as[n]or directlyn.*between factors is optional. A term with no Pauli factor is a constant (times identity).
- blueqat.utils.ignore_global_phase(statevec)[source]¶
Multiply e^-iθ to statevec where θ is a phase of first non-zero element.
- Parameters:
statevec (Tensor)
- Return type:
Tensor
- blueqat.utils.gen_gray_controls(n)[source]¶
Generate an iterator which returns bit indices for constructing Gray code based controlled gate.
- blueqat.utils.random_unitary(dim, seed=None, device=None)[source]¶
A
dim x dimunitary drawn from the Haar measure.The usual recipe – QR-decompose a complex Gaussian matrix and take Q – is not Haar distributed on its own, because QR does not fix the phases of Q’s columns. Multiplying by the phases of R’s diagonal is what fixes it, and omitting that step biases the distribution in a way that quietly shifts quantities like the heavy-output probability of a random circuit.
seed uses a private generator and leaves the global RNG alone.
Returns a
torch.Tensor, as everything else in this SDK does – call.numpy()on it before mixing with NumPy, ornumpyoperations will fail on the tensor rather than converting it.
- blueqat.utils.check_unitarity(mat)[source]¶
Check whether mat is a unitary matrix.
- Parameters:
mat (Tensor)
- Return type:
- blueqat.utils.calc_u_params(mat)[source]¶
Calculate U-gate parameters from a 2x2 unitary matrix.
U(theta, phi, lam, gamma)ise^{i gamma} [[cos(t), -e^{i lam} sin(t)], [e^{i phi} sin(t), e^{i(phi+lam)} cos(t)]]witht = theta / 2.The general route reads gamma off
mat[0, 0]andphi + lamoffmat[1, 1]. An antidiagonal unitary – X and Y among them – has both of those equal to zero, andcmath.phase(0)returns0.0without complaint, so two free phases silently vanish and the reconstructed gate is a different unitary, not merely a different global phase. That case is handled separately below.
- blueqat.utils.sqrt_2x2_matrix(mat)[source]¶
Returns square root of a 2x2 matrix.
Reference: https://en.wikipedia.org/wiki/Square_root_of_a_2_by_2_matrix
- Parameters:
mat (Tensor)
- Return type:
Tensor
- class blueqat.utils.AnsatzBase(hamiltonian, n_params)[source]¶
Base class for Variational Quantum Eigensolver Ansatz using PyTorch.
- make_sparse(sparse=True, device=None)[source]¶
- Parameters:
sparse (bool)
device (device | None)
- Return type:
None
- get_energy(circuit, sampler)[source]¶
Calculate energy expectation value from circuit and sampler with Autograd support.
Whether the result carries a gradient back to circuit’s parameters depends on sampler: an exact sampler (e.g. non_sampling_sampler) keeps the autograd graph intact, while a genuinely stochastic one (e.g. one built from get_measurement_sampler) does not – real shot noise isn’t differentiable, so that is expected, not a bug. To optimize through such a sampler anyway, Vqe estimates the gradient with parameter_shift_gradient instead, which it selects on its own by default.
- class blueqat.utils.QaoaAnsatz(hamiltonian, step=1, init_circuit=None, mixer=None)[source]¶
- class blueqat.utils.VqeResult(vqe: Optional[ForwardRef('Vqe')] = None, params: Optional[torch.Tensor] = None, circuit: Optional[blueqat.circuit.Circuit] = None, loss_history: List[float] = <factory>, _probs: Optional[Dict[Tuple[int, ...], float]] = None)[source]¶
- Parameters:
- loss_history: List[float]¶
Objective value at every optimizer iteration, in order, so that a run can be checked for convergence without re-running it with another optimizer. len(loss_history) is the number of iterations actually taken (which is below max_iter when the gradient-norm tolerance stopped the loop early).
- class blueqat.utils.Vqe(ansatz, optimizer_cls=<class 'torch.optim.adam.Adam'>, optimizer_kwargs=None, sampler=None, seed=None, gradient='auto')[source]¶
- Parameters:
- blueqat.utils.expect(qubits, meas)[source]¶
Marginal probabilities of meas qubits, as gradient-carrying tensors (not plain floats) so that AnsatzBase.get_energy can backprop through them when qubits came from a differentiable circuit run.
- blueqat.utils.get_measurement_sampler(n_sample, device=None, seed=None)[source]¶
A sampler that estimates probabilities from n_sample simulated measurements.
With seed set, the sampler draws from its own generator instead of the global RNG, so a VQE run using it is reproducible. The returned callable also carries a set_seed(seed) method, which is what Vqe.run(seed=…) calls to put the whole run – initial parameters and sampling alike – under a single seed.
- blueqat.utils.pauli_expectation(hamiltonian, state, n_qubits=-1)[source]¶
<psi|H|psi>for a Pauli-expression H, without ever building H as a matrix.A Pauli product is a signed permutation of basis states, so each term costs one pass over the state. Forming the
2**n x 2**nmatrix first – whatExpr.to_matrix()does – instead costs4**n, which puts even 16 qubits out of reach. Differentiable in both the state and tensor-valued coefficients.state is either a statevector (1-D), giving
<psi|H|psi>, or a density matrix (2-D), givingTr(rho H). n_qubits defaults to the width implied by the state.
- blueqat.utils.SHIFT_RULE_GATES = frozenset({'cp', 'cphase', 'cr', 'exch', 'exchange', 'p', 'phase', 'r', 'rx', 'rxx', 'ry', 'ryy', 'rz', 'rzz'})¶
Gates whose generator has exactly two eigenvalues one apart, which is what makes the two-term shift rule exact. Controlled rotations (crx/cry/crz) have four eigenvalues and need a four-term rule, so they are refused rather than silently given a wrong gradient.
- blueqat.utils.parameter_shift_gradient(ansatz, params, energy_of_circuit)[source]¶
The energy and its gradient at params, by the parameter-shift rule.
Backpropagation cannot see through a sampler: estimating an expectation value from shots throws away the autograd graph, so shot-based VQE has no gradient to descend. The shift rule gets one from the same estimator, by evaluating it at shifted parameters instead of differentiating it.
Each gate’s own derivative is
(E(theta + pi/2) - E(theta - pi/2)) / 2, exact rather than a finite difference. Those are then chained onto params through autograd, so a parameter feeding several gates – as QAOA’s angles do – correctly sums their contributions.energy_of_circuit takes a circuit and returns its energy; the cost is two of those evaluations per parametric gate application.
- Parameters:
ansatz (AnsatzBase)
params (Tensor)
- Return type:
Tuple[Tensor, Tensor]
Backends¶
Base class and plugin registration system for Blueqat backends.
- blueqat.backends.backendbase.BIT_ORDERS: Tuple[str, ...] = ('q0_last', 'q0_first')¶
Accepted values of the bit_order= argument of Circuit.run(shots=…). “q0_last” is blueqat’s long-standing layout – the leftmost character of a counts key is the highest-numbered qubit, so key[-1] is qubit 0. “q0_first” is the reverse, key[i] is qubit i, matching qapi.blueqat.app and most cloud APIs. Either way the key is exactly n_qubits characters wide.
- blueqat.backends.backendbase.apply_bit_order(counts, n_qubits, bit_order='q0_last')[source]¶
Re-key a shot Counter, whose keys are in blueqat’s own “q0_last” order, into the requested qubit-to-character order.
Keys are zero-padded to exactly n_qubits characters first – which is only correct for “q0_last” input, since that is the order whose padding goes on the left. Without the padding, a reversed “11” cannot be told apart from “000011” (qubits 0 and 1) and “110000” (qubits 4 and 5), which is precisely the mistake that hand-rolled reversals keep making.
- class blueqat.backends.backendbase.Backend[source]¶
Abstract base class for all Blueqat simulation and compilation backends.
run has a default template-method implementation: a backend that doesn’t override run directly can instead define per-gate gate_{lowername}(self, gate, ctx) hook methods (e.g. gate_x, gate_cx), plus optionally _preprocess_run/_postprocess_run to build/consume its own ctx. See QasmOutputBackend for an example. Backends like TorchBackend that need a different execution model override run directly instead.
- blueqat.backends.backendbase.register_backend(name, backend_cls, overwrite=False)[source]¶
Register a new backend plugin dynamically.
This allows external packages (like a quimb or cuQuantum connector) to register themselves into Blueqat at runtime.
- blueqat.backends.backendbase.get_backend(name)[source]¶
Retrieve an instance of the registered backend by name.
Unified Differentiable Quantum Simulator Backend using PyTorch. Supports both pure Statevector and ultra-scalable Tensor Network contraction. Leverages opt_einsum for path optimization while executing fully via PyTorch.
- class blueqat.backends.torch_backend.TorchBackend(mode='tensornet', device=None, dtype=None)[source]¶
Unified PyTorch simulator backend supporting Autograd optimization.
- Parameters:
mode (str)
device (device | None)
dtype (dtype | None)
Exchange-only spin qubits¶
The 3-spin decoherence-free-subsystem (DFS) encoding of exchange-only qubits.
One logical qubit lives in the total-spin S=1/2 sector of 3 physical spins
(spin up = |0>, physical qubit 3i+k is spin k of logical qubit i, qubit 0 is
the least-significant statevector bit, as everywhere in this SDK):
|0_L>=|singlet(0,1)>|up(2)>|1_L>= sqrt(2/3)|T+(0,1)>|down(2)>- sqrt(1/3)|T0(0,1)>|up(2)>
Each logical state comes in two “gauge” copies, the total-Sz m=+1/2 sector above and its m=-1/2 partner; exchange acts identically on both, and any population in the fully symmetric S=3/2 quadruplet is leakage.
- blueqat.eo.encoding.codeword_basis(m='+')[source]¶
(8, 2) matrix whose columns are
|0_L>,|1_L>of the requested gauge sector (‘+’ for total Sz = +1/2, ‘-’ for -1/2).- Parameters:
m (str)
- Return type:
Tensor
- blueqat.eo.encoding.encode_state(logical_amplitudes, m='+')[source]¶
Encode a product state of logical qubits into 3n physical spins.
logical_amplitudes[i] is the (alpha, beta) pair of logical qubit i. Returns the 2**(3n) statevector (logical qubit 0’s spins are physical qubits 0..2, i.e. the least-significant bits).
- blueqat.eo.encoding.leakage(state, triple=0)[source]¶
Population outside the S=1/2 subspace of the given 3-spin triple, i.e. the weight in its fully symmetric S=3/2 quadruplet.
state is either a statevector (1-D) or a density matrix (2-D), so leakage can be read off a noisy run as well as a pure one – which is the case that matters, since it is decoherence that pushes population out of the encoded subspace in the first place.
- blueqat.eo.encoding.logical_action(unitary8, m='+', atol=1e-09)[source]¶
Extract the 2x2 logical action of a 3-spin (8x8) unitary.
Raises ValueError if the unitary leaks out of the logical subspace of the requested gauge sector (the extracted block would then be non-unitary).
- blueqat.eo.encoding.logical_fidelity(actual, target)[source]¶
Phase-insensitive gate fidelity
|tr(A^dagger T)|^2 / d^2of two equally-sized unitaries.- Parameters:
actual (Tensor)
target (Tensor)
- Return type:
- blueqat.eo.encoding.two_qubit_codeword_basis(m1, m2)[source]¶
(64, 4) basis of a 2-logical-qubit (6-spin) sector: columns are
|00_L>,|01_L>,|10_L>,|11_L>with gauge m1 for logical qubit 0 (spins 0-2) and m2 for logical qubit 1 (spins 3-5).
- blueqat.eo.encoding.two_qubit_logical_action(unitary64, m1='+', m2='+', atol=1e-09)[source]¶
Extract the 4x4 logical action of a 6-spin unitary on the encoded pair.
Analytic exchange-pulse sequences for logical gates on encoded EO qubits.
A sequence is a list of ((i, j), theta) pairs in application order, where (i, j) are physical spin indices within the logical qubits involved and theta is the exchange pulse area for Circuit().exch(theta)[i, j]. All logical gates are exact up to a global phase.
Single-qubit tables and the serial Fong-Wandzura CNOT follow the constant- amplitude constructions used in eoqrid (MIT, https://github.com/samn33/eoqrid) and Weinstein et al., Nature 615, 817 (2023); the CNOT runs on the 6-spin linear chain t0-t1-t2-c2-c1-c0 (nearest-neighbor pulses only) in 28 pulses.
- blueqat.eo.sequences.rz_sequence(phase, offset=0)[source]¶
Logical RZ(phase): a single pulse on the (0,1) pair (the singlet in
|0_L>picks up e^{i theta}, giving RZ(-theta) up to global phase).
- blueqat.eo.sequences.y_sequence(offset=0)[source]¶
Logical Y = X after Z (equal to iY, a global phase).
- blueqat.eo.sequences.ry_sequence(phase, offset=0)[source]¶
Logical RY(phase) = S RX(phase) S^dagger (applied right-to-left).
- blueqat.eo.sequences.cx_sequence(control_offset, target_offset)[source]¶
Serial Fong-Wandzura CNOT: 28 exchange pulses on the linear chain t0-t1-t2-c2-c1-c0 (control spins c*, target spins t*), exact up to a global phase and independent of both qubits’ gauge states.
- blueqat.eo.sequences.cz_sequence(control_offset, target_offset)[source]¶
Encoded CZ = (I x H) CX (I x H) on the target logical qubit.
- blueqat.eo.sequences.swap_sequence(offset_a, offset_b)[source]¶
Encoded SWAP: swap the two triples spin-by-spin (3 full-SWAP pulses).
- blueqat.eo.sequences.sequence_to_circuit(sequence, n_physical_qubits)[source]¶
Build an exchange-pulse Circuit from a sequence of ((i, j), theta).
Differentiable synthesis of logical EO gates as short exchange-pulse sequences, using PyTorch autograd (the whole pipeline – pulse areas -> exchange matrices -> logical block -> fidelity – is differentiable).
This is what allows going beyond the fixed analytic gate tables: any target SU(2) can be compiled into a few constant-amplitude pulses.
- blueqat.eo.optimizer.synthesize_1q(target, n_pulses=4, n_restarts=8, max_iter=400, fidelity_goal=0.999999999, seed=0, offset=0)[source]¶
Synthesize a logical 1-qubit gate as n_pulses exchange pulses alternating on pairs (0,1) and (1,2) of one triple.
Returns the pulse sequence in application order (compatible with sequences.sequence_to_circuit). Raises RuntimeError if no restart reaches fidelity_goal – some targets need more pulses (4 suffices for generic SU(2) with these two 120-degree-tilted rotation axes).
- blueqat.eo.optimizer.synthesize_2q(target, pairs, initial_thetas=None, n_restarts=4, max_iter=1000, fidelity_goal=0.99999999, seed=0)[source]¶
Synthesize an encoded 2-logical-qubit gate (logical qubit 0 on spins 0-2, logical qubit 1 on spins 3-5) as exchange pulses on the given pair pattern.
The loss demands a gauge-independent, gauge-preserving implementation: the logical block must equal target with one common phase in all four total-Sz sectors (leakage automatically suppresses the fidelity, so it needs no separate penalty). Note that some natural constructions are gauge-permuting instead – e.g. the 3-pulse physical triple swap realizes an encoded SWAP but exchanges the two gauge states with it – and such gates cannot (and need not) be found by this loss.
Pass initial_thetas to refine a known sequence – e.g. to re-calibrate the Fong-Wandzura angles after hardware perturbations – instead of starting from random pulses; from-scratch synthesis of long 2-qubit sequences is a hard non-convex problem and may need many restarts.
- blueqat.eo.optimizer.quantize_sequence(sequence, step)[source]¶
Snap every pulse area to the nearest multiple of step and drop pulses that round to zero – the operational constraint of constant- amplitude hardware whose pulse durations come in discrete clock ticks.
Check the result’s fidelity yourself (e.g. via encoding.logical_action); a coarse step degrades the gate.
- blueqat.eo.optimizer.THREE_PULSE_REACH = 0.8660254037844386¶
The two exchange pairs of a triple act on the logical qubit as rotations about axes 120 degrees apart – pair (0,1) about -z, pair (1,2) about
(sqrt(3)/2, 0, 1/2)– and by exactly the pulse area, measured. That fixes what three pulses can reach: the off-diagonal entry of a productRa Rb Rahas modulus(sqrt(3)/2) |sin(beta/2)|, and the outer two pulses are diagonal, so they cannot change it.
- blueqat.eo.optimizer.decompose_1q(target, offset=0, samples=720)[source]¶
A logical 1-qubit gate as three or four exchange pulses, in closed form.
Exact, and far shorter than composing the analytic tables: measured against them,
rxgoes from seven pulses to three or four andryfrom nine. On exchange-only hardware a pulse is a gate, so that is the error budget.Three suffice when
|target[0,1]| <= sqrt(3)/2, which forrx(theta)meanstheta <= 2*pi/3. Beyond it a fourth pulse is applied first to bring the target inside: measured over random unitaries outside the reach, the largest residual after the best fourth pulse is 0.829 against the 0.866 bound, so four always suffice. samples is how finely that fourth angle is searched before being refined; it is a one-dimensional minimum, not a fit over the whole sequence.
Pulse schedules: the hardware-facing time-resolved view of an exchange circuit.
to_schedule turns a sequence of exchange pulses (or a Circuit of exch gates) into a JSON-compatible dict with explicit start times, packing pulses on disjoint spin pairs in parallel (ASAP scheduling; pulses on disjoint pairs commute, so this never changes the unitary). The format is designed to be handed to pulse-level control stacks (e.g. spinQICK-style backends) or submitted through blueqat.cloud.
Schema:
{
"format": "blueqat-eo-schedule",
"version": "1",
"n_spins": 6,
"amplitude": 1.0, # exchange integral J during a pulse
"pulses": [
{"start": 0.0, "duration": 3.14159, "pair": [0, 1], "theta": 3.14159},
...
],
"total_duration": 12.56637
}
Durations are theta / amplitude (constant-amplitude pulses: the pulse area theta = J * t is what fixes the gate).
- blueqat.eo.schedule.to_schedule(source, amplitude=1.0, n_spins=0)[source]¶
Build a time-resolved pulse schedule with ASAP parallel packing.
Each pulse starts as soon as both of its spins are free; pulses touching disjoint pairs run simultaneously. Relative order of pulses sharing a spin is preserved, so the scheduled unitary equals the sequential one.
The exchange unitary is exactly 2*pi-periodic in the pulse area, so theta is canonicalized into [0, 2*pi) – a negative area (e.g. from a daggered circuit) becomes the equivalent positive-duration pulse, and pulses whose area is a multiple of 2*pi (no-ops) are dropped.
- blueqat.eo.schedule.from_schedule(schedule)[source]¶
Rebuild an exchange-pulse Circuit from a schedule dict.
Pulses are replayed in order of start time (ties broken by list order). That reproduces the original unitary exactly because overlapping pulses only ever act on disjoint pairs, which is what to_schedule guarantees. A hand-written schedule need not, so it is checked here rather than serialized into a different unitary without comment.
- blueqat.eo.schedule.schedule_stats(schedule)[source]¶
Summary numbers: pulse count, serial vs scheduled duration, speedup.
The ‘eo’ backend: transpile a logical Circuit into exchange pulses.
import blueqat.eo # registers the backend physical = Circuit(2).h[0].cx[0, 1].run(backend=’eo’)
Logical qubit i is encoded in physical spins 3i, 3i+1, 3i+2, and the output is an ordinary Circuit containing only exch pulses, runnable on any simulation backend. All logical gates are exact up to global phase.
Topology note: the emitted pulses assume any pair inside the two triples involved in a gate can be pulsed (in particular, the Fong-Wandzura CNOT’s bridge pulse connects spin 3c+2 with spin 3t+2, and the encoded SWAP pulses pair the triples spin-by-spin). This is always fine for simulation; mapping onto strict nearest-neighbor-only hardware additionally requires dot orientation assignment and spin-level SWAP routing, which is future work (cf. exchange-pulse-optimizer).
- class blueqat.eo.transpiler.EOTranspiler[source]¶
Transpiler backend converting logical circuits to exchange pulses.
- run(gates, n_qubits, *args, **kwargs)[source]¶
shortest=True solves each single-qubit gate in closed form rather than reading it out of the analytic tables.
The tables compose known sequences, which is correct and longer than necessary: measured, an rx costs seven pulses there and three or four solved directly, an ry nine. A pulse is a gate on this hardware, so that is the error budget. It is off by default because it changes the emitted pulses for circuits that already work, and a device schedule is not something to alter without being asked.
Cloud¶
API-key based access to the Blueqat cloud service (https://qapi.blueqat.app).
Credential resolution order:
An explicit configure(api_key=…) call in the current process.
The BLUEQAT_API_KEY environment variable.
The config file ~/.blueqat/config.json (written by save_api_key, created with owner-only permissions).
Importing this module registers the cloud backend, so a circuit can be submitted with the same API as local simulation:
import blueqat.cloud
Circuit(2).h[0].cx[0, 1].m[:].run(backend='cloud', shots=100) # Counter
Circuit(2).h[0].cx[0, 1].run(backend='cloud') # statevector
Circuit(2).h[0].run(backend='cloud', hamiltonian=1.0 * Z[0]) # <psi|H|psi>
Results follow the SDK’s conventions (Counter keys are q_{n-1}…q0 etc.), so switching between local and cloud backends needs no code changes. Module helpers cover the rest of the REST API: health, me, circuit_info, vqe_run, qaoa_run, hardware_status, hardware_qpus, submit_hardware_job (which requires confirm=True – real hardware, real cost).
Get an API key at https://mcp.blueqat.app/login.
- blueqat.cloud.GATEWAY_TIMEOUTS = frozenset({504, 522, 523, 524})¶
Status codes meaning “the gateway gave up waiting”, not “the work failed”. Cloudflare’s 524 in particular fires after 100 seconds of silence from the origin – a limit on how long a reply may take to start, not on how long the work may take. Treating one as a failure invites a resubmission, which for a hardware job costs another slot and more money.
- exception blueqat.cloud.CloudOutcomeUnknown[source]¶
The request’s fate is unknown: it may have succeeded.
Raised instead of a plain error when the connection or the gateway timed out, so that a caller can tell “this did not happen” apart from “I do not know whether this happened” – and does not retry the second one blindly.
- blueqat.cloud.config_path()[source]¶
Path of the persistent config file (override dir with BLUEQAT_CONFIG_DIR).
- Return type:
- blueqat.cloud.save_api_key(api_key, endpoint=None)[source]¶
Persist the API key to the config file with owner-only permissions.
- blueqat.cloud.delete_api_key()[source]¶
Remove the stored API key from the config file (if present).
- Return type:
None
- blueqat.cloud.get_api_key()[source]¶
Resolve the API key: configure() > environment > config file.
- Return type:
str | None
- blueqat.cloud.get_endpoint()[source]¶
Resolve the service endpoint: configure() > config file > default.
- Return type:
- blueqat.cloud.configure(api_key=None, endpoint=None, transport=None)[source]¶
Set session-level cloud settings (highest priority, not persisted).
transport is a callable
(method, path, payload, api_key, endpoint)returning the decoded JSON response; inject one for tests.
- blueqat.cloud.reset_configuration()[source]¶
Clear session-level settings set by configure (env/file are untouched).
- Return type:
None
- blueqat.cloud.circuit_to_gates(circuit)[source]¶
Convert a Circuit into the API’s gate-list format:
[{"gate": "h", "qubits": [0]}, {"gate": "rx", "qubits": [0], "params": [0.5]}, ...](slices and named blocks are expanded).
- blueqat.cloud.hamiltonian_to_terms(hamiltonian)[source]¶
Convert a Pauli Expr/Term into the API’s term list
[{"coeff": c, "paulis": [{"op": "X", "qubit": 0}, ...]}, ...].Identity (constant) terms can’t be sent over the wire; they are returned separately as a float to add to the expectation value locally.
- class blueqat.cloud.CloudBackend[source]¶
Backend submitting circuits to https://qapi.blueqat.app (POST /v1/circuits/run). Results are converted back to SDK conventions, so it is a drop-in replacement for the local backends.
- blueqat.cloud.me()[source]¶
The authenticated account: tier, limits and remaining quota.
- Return type:
- blueqat.cloud.circuit_info(circuit)[source]¶
Server-side circuit validation and stats without running it.
- Return type:
- blueqat.cloud.vqe_run(hamiltonian, n_qubits, layers=1)[source]¶
Run VQE on the cloud for a Pauli Hamiltonian.
- blueqat.cloud.qaoa_run(qubo, steps=1, shots=256)[source]¶
Run QAOA on the cloud for a QUBO given as
[{"i": 0, "j": 1, "value": 2.0}, ...](see the API docs).
- blueqat.cloud.submit_hardware_job(circuit, shots, qpu_id=None, confirm=False, preserve_layout=False)[source]¶
Submit a circuit to real quantum hardware.
Requires confirm=True: hardware runs cost real money and are subject to your account’s quota.
- blueqat.cloud.hardware_jobs(limit=20)[source]¶
List your recent hardware jobs, newest first.
This is the way to answer “did my submission actually land?” after a CloudOutcomeUnknown – check here before resubmitting, since a duplicate hardware job spends another slot and more money.
- blueqat.cloud.hardware_quote(shots, payer)[source]¶
What a hardware run would cost, before committing to it.
MCP server¶
MCP (Model Context Protocol) server exposing blueqat to LLM clients.
Install the optional dependency and register the server with an MCP client (Claude Desktop, Claude Code, …):
pip install blueqat[mcp]
// e.g. Claude Desktop’s config: { “mcpServers”: { “blueqat”: { “command”: “blueqat-mcp” } } }
Circuits are exchanged as OpenQASM 2.0 text (parsed with blueqat’s eval-free
parser) and Hamiltonians as Pauli-expression strings parsed by
blueqat.utils.parse_hamiltonian() – no code execution ever happens on
tool inputs.
The tool implementations below are plain functions returning JSON-compatible dicts (so they are unit-testable without an MCP client); build_server() wraps them into a FastMCP server and main() serves it over stdio.
- blueqat.mcp_server.run_circuit(qasm, shots=None, backend='tensornet')[source]¶
Run an OpenQASM 2.0 circuit and return the result.
With shots, returns measurement counts. Without, returns the full statevector for small circuits, or the largest basis-state probabilities for wide ones.
- blueqat.mcp_server.circuit_stats(qasm)[source]¶
Qubit count, depth and gate counts of an OpenQASM 2.0 circuit.
- blueqat.mcp_server.expectation_value(qasm, hamiltonian)[source]¶
<psi|H|psi> for the circuit’s final state.
hamiltonian is a Pauli expression like “1.5*Z[0]*Z[1] - 0.5*X[0] + 2” (indices as [n] or directly after the letter; * is optional).
- blueqat.mcp_server.eo_transpile(qasm)[source]¶
Transpile a logical circuit to exchange-only spin-qubit pulses (3 physical spins per logical qubit) and summarize the pulse schedule.
- blueqat.mcp_server.blueqat_info()[source]¶
Version and capability summary of this blueqat installation.
Circuit utilities¶
Parser for a practical subset of OpenQASM 2.0 (the qelib1.inc gate set) into a Circuit.
This is the reverse of Circuit.to_qasm().
- blueqat.circuit_funcs.qasm_parser.MAX_EXPONENT = 64¶
Largest exponent an angle expression may use. ** is unbounded arithmetic: 9**9**9 has no answer a machine will finish computing, and this parser reads text arriving from MCP clients, so an angle is not a place to allow that.
- blueqat.circuit_funcs.qasm_parser.look_alike_characters(text)[source]¶
Characters that are not what they look like, and what they should be.
Text pasted out of a PDF or a word processor carries characters that render identically to ASCII or to an ordinary ideograph but are different code points – full-width punctuation and digits, and the 214 Kangxi radicals, where
子(U+5B50) and⼦(U+2F26) are indistinguishable on screen. A program carrying them fails to parse for a reason nobody can see by looking.Returns
{character: what it normalizes to}for each distinct character NFKC would change.⚠ That is a wide net, and using it as a “this text is damaged” test gives false positives on perfectly good Japanese. Full-width brackets and colons are correct typography; NFKC also turns
①into1,㎡intom2,ⅣintoIVand…into..., none of which is a repair. Use always_wrong_characters to ask whether something is broken; use this one to describe what is there.
- blueqat.circuit_funcs.qasm_parser.EXTRACTION_DAMAGE_RANGES = ((12032, 12245),)¶
The one range whose members can be called damage on sight. Their Unicode names say what they are – KANGXI RADICAL CHILD, KANGXI RADICAL TALL – so a character from a radical table appearing in running prose is not something anyone chose. All 214 normalize onto an ordinary ideograph.
⚠ Even here, “safe to repair in stored text” depends on where the text came from. Extracted from a PDF, one of these is an accident. Typed by a person or produced by a model, it is what they entered, and rewriting it is rewriting them. The character alone does not settle it.
- blueqat.circuit_funcs.qasm_parser.PRESENTATION_RANGES = ((63744, 64255), (65296, 65305), (65313, 65338), (65345, 65370), (65377, 65439))¶
Look-alikes that may be exactly what someone meant. Normalize these into a search index, never in the stored text.
The compatibility ideographs are the trap. Their names say nothing – CJK COMPATIBILITY IDEOGRAPH-FA10 – and they normalize onto 塚, 晴 and 祥, which is to say onto characters that appear in people’s names. A document carrying one may be spelling somebody’s name correctly, and repairing the stored text would spell it wrong.
Full-width letters are the registered form of some company names and appear in quotations that must not be altered; half-width kana is a presentation choice. The three full-width intervals are listed separately on purpose: FF10-FF5A as one span would swallow the full-width colon, question mark and brackets, which are ordinary Japanese punctuation.
- blueqat.circuit_funcs.qasm_parser.LOOK_ALIKE_RANGES = ((12032, 12245), (63744, 64255), (65296, 65305), (65313, 65338), (65345, 65370), (65377, 65439))¶
QASM is ASCII by definition, so the question of whether a character was intended does not arise inside one.
- Type:
Both, which is what a program cares about
- blueqat.circuit_funcs.qasm_parser.extraction_damage(text)[source]¶
Kangxi radicals, which say in their own names that they are misplaced.
U+2F26 renders exactly like U+5B50 and is a different character, so a document carrying it cannot be searched for its own words.
⚠ Whether repairing the stored text is right still depends on where the text came from: extracted from a document, one of these is an accident; typed by a person or emitted by a model, it is their input. Normalizing a search index is always safe and achieves the searching either way.
- blueqat.circuit_funcs.qasm_parser.presentation_variants(text)[source]¶
Look-alikes that may be deliberate. Normalize an index, not the text.
Compatibility ideographs normalize onto characters that appear in people’s names – U+FA10, U+FA12 and U+FA1A onto 塚, 晴 and 祥 – so a document carrying one may be spelling a name correctly. Full-width letters are the registered form of some company names and appear inside quotations. Nothing here can tell an intentional one from an accident, so rewriting the stored text changes names and misquotes sources. Normalize both the index and the query instead, and leave the text as it was written.
- blueqat.circuit_funcs.qasm_parser.always_wrong_characters(text)[source]¶
Every look-alike, damage or presentation, with its repair.
The union of extraction_damage and presentation_variants. Right for a program – QASM is ASCII, so anything here is a mistake in one – and the wrong question for prose, where the two halves want opposite treatment.
Only characters NFKC actually changes are returned. Reporting one whose normalized form is itself would be naming a problem and offering the problem as its own solution; see unfixable_lookalikes.
- blueqat.circuit_funcs.qasm_parser.unfixable_lookalikes(text)[source]¶
Look-alikes that normalizing will not resolve.
Twelve compatibility ideographs – U+FA0E, FA0F, FA11, FA13, FA14, FA1F, FA21, FA23, FA24, FA27, FA28 and FA29 – normalize to themselves, so U+FA11 and U+5D0E stay different after NFKC on both sides. Variant forms of a personal name are the usual way to meet them, and they need a different answer entirely.
⚠ These are not the whole of the problem, only the part that is in range. Ordinary variant ideographs – U+9AD9 against U+9AD8, say – are not compatibility characters at all and no normalization touches them. Matching people by name does not close either way; matching them by identifier does.
Returned as
{character: itself}so that “found it” and “fixed it” stay distinguishable: calling a normalization pass a resolution here is how the same report comes back a second time.
- blueqat.circuit_funcs.qasm_parser.from_qasm(qasm, normalize=False)[source]¶
Parse an OpenQASM 2.0 program (the qelib1.inc gate set) into a Circuit.
normalize applies NFKC first, folding full-width punctuation and Kangxi radicals onto their ASCII and ideographic equivalents. It is off by default because silently rewriting input is a guess at what was meant; when parsing fails, the error says whether such characters are present, which is something no amount of looking at the text will reveal.
Defines JSON serializer and deserializer for Blueqat circuits.
- blueqat.circuit_funcs.json_serializer.serialize(c)[source]¶
Serialize Circuit into JSON-compatible dictionary.
In this implementation, the serialized circuit is automatically flattened to break down multi-target operations into atomic gates.
- Parameters:
c (Circuit)
- Return type:
CircuitJsonDictV2
- blueqat.circuit_funcs.json_serializer.deserialize(data)[source]¶
Deserialize JSON-compatible dictionary back into a Circuit object.
- Parameters:
data (CircuitJsonDictV1 | CircuitJsonDictV2)
- Return type:
This module provides a feature to convert a quantum circuit to a unitary matrix.
- blueqat.circuit_funcs.circuit_to_unitary.circuit_to_unitary(circ, *runargs, **runkwargs)[source]¶
Convert a quantum circuit into its corresponding unitary matrix representation.
This function simulates the circuit for all computational basis states to construct the full unitary matrix.
- Parameters:
circ (Circuit) – The quantum circuit to be converted.
*runargs – Positional arguments passed to circuit execution backend.
**runkwargs – Keyword arguments passed to circuit execution backend.
- Returns:
The unitary matrix representing the total circuit operation.
- Return type:
np.ndarray
This module provides a feature to flatten circuit operations by expanding multi-targets.
- blueqat.circuit_funcs.flatten.flatten(c)[source]¶
Expands slice and multiple targets into single target operations.
This function normalizes the circuit so that each gate or measurement operation applies to explicit, un-sliced single qubits (or single pairs for two-qubit gates).
- Parameters:
c (Circuit) – The quantum circuit to flatten.
- Returns:
A new flattened Circuit object.
- Return type:
- Raises:
ValueError – If an unexpected or unprocessable operation type is encountered.