Skip to content

Latest commit

 

History

999 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

blueqat

A Quantum Computing SDK

Documentation: https://blueqat.github.io/blueqatSDK/

Blueqat's simulator is built on PyTorch, with two selectable execution modes: a dense statevector simulator and a memory-scalable tensornet (tensor-network contraction) simulator, which is the default. Both are differentiable, so circuits with torch.Tensor parameters keep their gradients through Circuit.run().

Tutorial

https://github.com/Blueqat/Blueqat-tutorials

Examples

Runnable scripts in examples/:

  • bell_state.py -- circuit basics: statevector, single amplitude, shot sampling
  • teleportation.py -- quantum teleportation, coherent and measured versions
  • grover_search.py -- Grover's search over 8 items with oracle + diffusion
  • qft.py -- Quantum Fourier Transform vs. the DFT matrix, period readout
  • vqe_ground_state.py -- VQE with a custom AnsatzBase (not tied to QAOA)
  • maxcut_qaoa.py -- QAOA for the graph Max-Cut problem
  • numpartition_qaoa.py -- QAOA for number partitioning
  • exchange_only.py -- exchange-only spin qubits: logical circuits from pure exchange pulses
  • shor_15.py -- Shor's order finding for N=15 structured with nested named blocks

Install

git clone https://github.com/blueqat/blueqatSDK
cd blueqatSDK
pip install -e .

Circuit

from blueqat import Circuit
import math

#number of qubit is not specified
c = Circuit()

#if you want to specified the number of qubit
c = Circuit(50) #50qubits

Method Chain

# write as chain
Circuit().h[0].x[0].z[0]

# write in separately
c = Circuit().h[0]
c.x[0].z[0]

Slice

Circuit().z[1:3] # Zgate on 1,2
Circuit().x[:3] # Xgate on (0, 1, 2)
Circuit().h[:] # Hgate on all qubits
Circuit().x[1, 2] # 1qubit gate with comma

Rotation Gate

Circuit().rz(math.pi / 4)[0]

Run

from blueqat import Circuit
Circuit(20).h[:].run() # returns a torch.Tensor statevector

# Select the execution mode explicitly (tensornet is the default)
Circuit(20).h[:].run(mode="statevector")
Circuit(20).h[:].run(mode="tensornet")

Run(shots=n)

Circuit(100).x[:].run(shots=1)
# => Counter({'1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111': 1})

Large-scale circuits

The dense statevector has 2**n_qubits entries, so for large n_qubits in tensornet mode (the default), run() requires either shots= or returns="amplitude" instead of materializing the full vector:

Circuit(50).h[:].run(shots=3)
Circuit(50).h[:].run(returns="amplitude", amplitude="0" * 50)

Reproducible sampling and counts bit order

# seed= fixes every random draw of a run (shot sampling, mid-circuit collapse,
# large-n perfect sampling). It drives a private torch.Generator, so it does not
# disturb the global RNG your program uses.
c = Circuit(4).h[:]
c.run(shots=200, seed=42) == c.run(shots=200, seed=42)   # True

# Counts keys are q_{n-1}...q_0 by default (qubit 0 rightmost). Cloud APIs use
# the opposite order; bit_order= converts, always zero-padded to n_qubits.
Circuit(3).x[0].run(shots=4)                        # => Counter({'001': 4})
Circuit(3).x[0].run(shots=4, bit_order='q0_first')  # => Counter({'100': 4})

Single Amplitude

Circuit(4).h[:].run(amplitude="0101")

Reset and mid-circuit measurement

# reset[i] forces qubit i back to |0>. Any circuit containing reset is run
# shot-by-shot with a real probabilistic collapse at each measure/reset.
Circuit(2).h[0].cx[0, 1].reset[0].m[:].run(shots=100)

# Measurement keys let you tag a measurement and read it back per-shot.
Circuit().x[0].m(key="a")[0].run(shots=10, returns="samples")
# => [{'a': [1]}, {'a': [1]}, ...]

Ancilla qubits

c = Circuit(4).h[0].h[1].h[2].h[3]
with c.ancilla() as a:       # allocate a fresh qubit past the current width
    c.cx[0, a[0]]
    c.cx[0, a[0]]
with c.ancilla(pos=6, stop=8, reset=True) as a:  # or pin an explicit range
    c.cx[3, a[0]]
# a[i] is reset back to |0> on exiting the `with` block when reset=True (the default)

Expectation value of hamiltonian

from blueqat.utils import Z
hamiltonian = 1*Z[0]+1*Z[1]
Circuit(4).x[:].run(hamiltonian=hamiltonian)
# => -2.0

# Or the equivalent convenience method (differentiable):
Circuit(4).x[:].expect(hamiltonian)

Pauli exponentials and expectation values

from blueqat.utils import X, Y, Z

# exp(-i * theta * P) for a Pauli product P, given as {qubit: letter} so there
# is no bit-order ambiguity. P**2 == I, so this is exactly cos(t) - i sin(t) P.
Circuit().exp_pauli({0: 'X', 1: 'X', 2: 'Z', 3: 'Y'}, 0.3)   # exp(-0.3i XXZY)
Circuit().exp_pauli({5: 'Z'}, 0.3)                           # == rz(0.6)[5]

# Expectation of any Pauli expression, computed term-by-term over the
# statevector (O(terms * 2**n)) instead of building a 2**n x 2**n matrix.
Circuit(18).h[:].expect(1.0 * Z[0] * Z[1] - 0.5 * X[2])

Named gate blocks (nested subcircuits)

c = Circuit(7)
with c.block("order-finding"):
    with c.block("superposition"):
        c.h[4, 5, 6]
    with c.block("c-U^1"):
        c.cswap[4, 2, 3].cswap[4, 1, 2].cswap[4, 0, 1]
    c.append_block("IQFT", qft_circuit(3).dagger(), offset=4)

print(c.tree())      # shows the nested structure (see examples/shor_15.py)
c.run()              # backends see the plain gates -- execution is unchanged
c.dagger()           # inverts blocks as blocks ("order-finding†")

c.run(backend="draw")                     # blocks drawn as labeled boxes
c.run(backend="draw", expand_blocks=True) # ...or expanded into their gates

Probabilities, depth and gate counts

Circuit(2).h[0].cx[0, 1].probs()        # measurement probabilities (differentiable)
Circuit(2).h[0].cx[0, 1].probs([1])     # marginal on selected qubits
Circuit(2).h[0].cx[0, 1].depth()        # => 2
Circuit(2).h[0].cx[0, 1].count_ops()    # => Counter({'h': 1, 'cx': 1})

Noise and density matrices

from blueqat.noise import depolarizing, amplitude_damping, NoiseModel

# noise= switches the run onto a density-matrix simulation and applies the
# channel after every gate. depolarizing(p) is the Nielsen & Chuang form,
# (1-p)rho + p I/2**k, acting jointly on a two-qubit gate's qubits;
# depolarizing(p, per_qubit=True) instead applies the one-qubit channel to each
# of them (what papers assuming purely local noise mean -- a different map).
Circuit(2).h[0].cx[0, 1].run(noise=depolarizing(0.01))              # rho
Circuit(2).h[0].cx[0, 1].run(noise=depolarizing(0.01), shots=1000)  # counts
Circuit(2).h[0].cx[0, 1].run(noise=depolarizing(0.01), hamiltonian=h)  # Tr(rho H)

# Different rates per gate (real devices have worse two-qubit gates)
nm = NoiseModel()
nm.add(depolarizing(0.001))
nm.add(depolarizing(0.01), gates=['cx'])

# noise_scale= is the zero-noise-extrapolation knob: same circuit, more noise
values = [c.run(noise=nm, noise_scale=s, hamiltonian=h) for s in (1, 2, 3)]

# Silicon spin qubits are dephased by noise that is *constant within a shot*
# (nuclear fields, 1/f charge noise) -- not a Kraus channel, and the difference
# shows: a Hahn echo refocuses this and does nothing to phase_damping.
from blueqat.noise import QuasiStatic
c.run(quasi_static=QuasiStatic(sigma=0.4), samples=4000, seed=1)

Density matrices have 4**n entries, so this is for small circuits: about 0.4 ms/gate at 8 qubits, 9 ms at 10, 183 ms at 12.

Error correction

from blueqat.qec import repetition_code, memory_experiment, PhenomenologicalNoise

code = repetition_code(5)          # or rotated_surface_code(3)
code.check()                       # generators really commute, logicals really pair
code.logical_weight()              # brute-forced code distance, for small codes

result = memory_experiment(code, rounds=5, shots=2000, seed=1,
                           noise=PhenomenologicalNoise(p_data=0.02, p_measure=0.02))
result.logical_error_rate

# Circuit-level noise puts faults after every gate, measurement and reset, so
# a single fault can ride an ancilla's later gates onto several data qubits --
# a hook error. Which faults do depends on the interaction order, which is
# therefore an argument: on the d=3 surface code the order alone moves the
# logical error rate by nearly a factor of two.
from blueqat.qec import CircuitLevelNoise
memory_experiment(code, rounds=3, shots=6000, seed=4,
                  noise=CircuitLevelNoise(p1=0.001, p2=0.01, p_measure=0.01),
                  order=my_schedule)

Codes, syndrome circuits, decoders and experiments are separate pieces, so a decoder can be swapped or checked against a reference. The detector graph a matching decoder needs is built by injecting each error location and observing, not by hand-writing each code's geometry.

Clifford operators and randomized benchmarking

from blueqat.clifford import Clifford, random_clifford

c = Clifford.from_circuit(Circuit(2).h[0].cx[0, 1])  # a stabilizer tableau
c.then(other)          # compose (c first), exactly, with no 2**n matrix
c.inverse()            # ...and invert
c.to_circuit()         # back to gates
random_clifford(2, seed=0)   # uniform over the 11520 two-qubit Cliffords

# Randomized benchmarking: a sequence plus the ONE Clifford that undoes it
total = Clifford.identity(n)
for i in range(m):
    g = random_clifford(n, seed=i)
    circuit += g.to_circuit()
    total = total.then(g)
circuit += total.inverse().to_circuit()   # survival is exactly 1 without noise

Circuit optimization

from blueqat.optimize import optimize

optimize(Circuit(2).h[0].x[1].h[0])              # => Circuit(2).x[1]
optimize(Circuit(2).rz(0.3)[0].x[1].rz(0.4)[0])  # => rz(0.7)[0] . x[1]

# Every rewrite preserves the unitary exactly, global phase included: rz(2*pi)
# is -I and is kept; rz(4*pi) is I and is dropped. Trainable (requires_grad)
# angles are merged but never dropped, even at zero.

# For exchange-only hardware the cost is the pulse count, and optimizing the
# logical circuit first removes whole pulse sequences before they are emitted:
import blueqat.eo
logical = Circuit(2).x[0].x[0].cx[0, 1].cx[0, 1].h[1]
len(logical.run(backend='eo').ops)             # 65 pulses
len(optimize(logical).run(backend='eo').ops)   # 3

Exchange-only spin qubits (silicon quantum dots)

import blueqat.eo                      # registers the 'eo' backend
from blueqat.eo import encoding, synthesize_1q

# The native hardware primitive: a Heisenberg exchange pulse
Circuit(2).exch(math.pi)[0, 1]         # theta = pi is an exact SWAP

# Transpile a logical circuit into pure exchange pulses
# (3 spins per logical qubit; H = 3 pulses, Fong-Wandzura CNOT = 28 pulses)
physical = Circuit(2).h[0].cx[0, 1].run(backend='eo')

# Run the pulses on the encoded state and inspect the logical result
init = encoding.encode_state([(1, 0), (1, 0)])   # |00>_L
final = physical.run(initial=init)
encoding.leakage(final, 0)                       # leakage out of the code space

# Differentiable pulse synthesis: any SU(2) in 4 constant-amplitude pulses
seq = synthesize_1q(target_2x2_unitary, n_pulses=4)

# Re-calibrate a drifted 2-qubit sequence back to an exact gate
from blueqat.eo import synthesize_2q, quantize_sequence, to_schedule
refined = synthesize_2q(cx_4x4, pairs=pulse_pairs, initial_thetas=drifted)

# Discrete pulse durations (hardware clock ticks) and time-resolved schedules
seq_q = quantize_sequence(seq, step=2 * math.pi / 4096)
schedule = to_schedule(physical)   # ASAP-parallel, JSON-ready pulse schedule

MCP server (use blueqat from Claude and other LLM clients)

pip install blueqat[mcp]

Register the blueqat-mcp command with an MCP client -- e.g. Claude Desktop's config:

{ "mcpServers": { "blueqat": { "command": "blueqat-mcp" } } }

Tools: run_circuit (OpenQASM in, statevector/counts out), circuit_stats, expectation_value (Pauli-expression Hamiltonians like "1.5*Z[0]*Z[1] - 0.5*X[0]"), draw_circuit (diagram image), eo_transpile (exchange-only pulse compilation), blueqat_info. All inputs are parsed without eval -- safe for untrusted tool calls.

Cloud access (qapi.blueqat.app)

import blueqat.cloud as cloud        # registers the 'cloud' backend
cloud.save_api_key("YOUR_API_KEY")   # get one at https://mcp.blueqat.app/login
# or: export BLUEQAT_API_KEY=...

c = Circuit(2).h[0].cx[0, 1]
c.m[:].run(backend='cloud', shots=100)          # counts (same conventions as local)
c.run(backend='cloud')                          # statevector
c.run(backend='cloud', hamiltonian=1.0 * Z[0])  # expectation value

cloud.hardware_status()                          # real-QPU status (public)
cloud.submit_hardware_job(c, shots=100, confirm=True)  # real hardware, real cost

Blueqat to/from QASM

Circuit().h[0].to_qasm()

#OPENQASM 2.0;
#include "qelib1.inc";
#qreg q[1];
#creg c[1];
#h q[0];

from blueqat.circuit_funcs import from_qasm
from_qasm(Circuit().h[0].to_qasm())  # parses back into an equivalent Circuit

Hamiltonian

from blueqat.utils import X, Y, Z, I

h1 = 1.23 * Z[0] + 4.56 * X[1] * Z[2]
h2 = 2.46 * Y[0] + 5.55 * Z[1] * X[2] * X[1]
hamiltonian = h1 * h1 + h2 * h2
print(hamiltonian)

Simplify the Hamiltonian

hamiltonian = hamiltonian.simplify()
print(hamiltonian)

QUBO Hamiltonian

from blueqat.utils import qubo_bit as q

hamiltonian = -3*q(0)-3*q(1)-3*q(2)-3*q(3)-3*q(4)+2*q(0)*q(1)+2*q(0)*q(2)+2*q(0)*q(3)+2*q(0)*q(4)
print(hamiltonian)

Time Evolution

import numpy as np
from blueqat import Circuit
from blueqat.utils import Z, X

hamiltonian = [1.0*Z[0], 1.0*X[0]]
a = [term.get_time_evolution() for term in hamiltonian]

time_evolution = Circuit().h[0]
for evo in a:
    evo(time_evolution, np.random.rand())

print(time_evolution)

VQE

import torch
from blueqat import Circuit
from blueqat.utils import Z, AnsatzBase, Vqe

class MyAnsatz(AnsatzBase):
    def get_circuit(self, params: torch.Tensor) -> Circuit:
        return Circuit(1).rx(params[0])[0]

hamiltonian = 1.0 * Z[0]
vqe = Vqe(MyAnsatz(hamiltonian, n_params=1))
result = vqe.run(initial_params=torch.tensor([0.1]))  # initial_params is optional
print(result.params, result.circuit.run())
print(vqe.sampler_call_count)  # 0 unless a sampler was supplied to Vqe(...)

# Without initial_params the run starts from random parameters, so repeated runs
# reach different local optima. seed= makes the whole run deterministic (initial
# parameters and, for a seedable sampler, its draws), and loss_history records
# the objective at every iteration for convergence checks.
result = Vqe(MyAnsatz(hamiltonian, n_params=1), seed=42).run()
print(len(result.loss_history), result.loss_history[-1])

Shot-based VQE (parameter-shift rule)

from blueqat.utils import get_measurement_sampler

# Sampling throws away the autograd graph, so a shot-based objective has no
# gradient to backpropagate. Vqe notices and switches to the parameter-shift
# rule, which is exact (not a finite difference) and chains correctly through
# parameters that drive many gates, as QAOA's angles do.
vqe = Vqe(ansatz, sampler=get_measurement_sampler(2000, seed=3), seed=42)
result = vqe.run()

# gradient='backprop' / 'parameter_shift' overrides the automatic choice.

QAOA

from blueqat.utils import qubo_bit as q, QaoaAnsatz, Vqe

hamiltonian = q(0)-q(1)
step = 1

vqe = Vqe(QaoaAnsatz(hamiltonian, step))
result = vqe.run()
result.circuit.run(shots=100)

# => Counter({'10': 100})

Drawing

Circuit().h[0].cx[0, 1].m[:].run(backend="draw")     # circuit diagram
Circuit().h[0].cx[0, 1].run(backend="draw_tn")        # tensor-network graph

Document

https://blueqat.github.io/blueqatSDK/ (日本語版: https://blueqat.github.io/blueqatSDK/ja/index.html)

Disclaimer

Copyright 2026 The blueqat Developers.

Releases

Packages

Used by

Contributors

Languages