OpenQARP philosophy¶
The Open Quantum Application Research Package (OpenQARP) is designed around a modular, compositional philosophy for building quantum algorithms. This guide introduces the core concepts and how they work together to create a flexible framework for quantum computing.
The building Blocks philosophy¶
At its core, OpenQARP treats quantum circuits as composable building blocks. Rather than working directly with low-level gate operations, you work with higher-level abstractions called Blocks that encapsulate meaningful quantum operations.
The Block Hierarchy¶
The SimpleBlock Class¶
SimpleBlock is the fundamental leaf unit of circuit construction in OpenQARP — a real subclass of the qarpx qx.SimpleBlock (direct inheritance via nanobind), so gate methods such as self.h(0) and self.cx(0, 1) are inherited natively from C++. Subclass SimpleBlock for a leaf block and CompositeBlockBase for one built out of other blocks.
There is no Block base class to inherit from. AnyBlock (an alias of the
qarpx qx.Block type) exists for annotating a value that may be any kind of
block — a type, not a base class:
from qarp.blocks import AnyBlock
def depth_of(block: AnyBlock) -> int:
return len(list(block.flatten()))
Every block (SimpleBlock or CompositeBlock alike):
Encapsulates a quantum operation that can be built into a command stream
Has a
build()method that populates the underlying qarpx C++ command bufferCan specify which qubits it targets via
target_qubitsCan be controlled by wrapping it in
ControlledBlockCan be composed with other blocks
from qarp.blocks import HEABlock
# A Hardware-Efficient Ansatz block with 2 layers
ansatz = HEABlock(
n_qubits=4,
n_layers=2,
real=False, # Use Ry-Rz rotations (complex ansatz)
linear=True, # Linear entanglement pattern
circular=False,
use_cz=False, # Use CNOT gates (not CZ)
)
ansatz.build()
# Access the underlying command stream (list of qarpx Commands)
commands = ansatz.flatten()
Some of the features of blocks:
Lazy building: Blocks are defined first, then built when needed
Symbolic parameters: Blocks can contain symbolic (variational) parameters
Dagger support: Get the adjoint via
block.dagger()Parameter substitution: Replace symbols with values using
set_symbols()
Primitive Blocks¶
OpenQARP provides many pre-built primitive blocks for common quantum operations, amongst them:
- State Preparation:
HnBlock: Applies Hadamard to the target qubitsComputationalBasisStateBlock: Prepares a specific computational basis stateDickeStateBlock: Prepares Dicke statesGHZLikeStateBlock: Prepares GHZ-like entangled states
- Ansätze (Parameterized Circuits):
HEABlock: Hardware-Efficient Ansatz with stacked rotation and entangling layersUCCBlock: Unitary Coupled Cluster for quantum chemistryQAOABlock: QAOA mixer and cost layers
- Algorithm-Specific:
QPEBlock: Quantum Phase Estimation circuitQFTBlock: Quantum Fourier TransformTrotterBlock: Trotterized time evolutionHadamardTestBlock,SWAPTestBlock: Quantum tests for overlaps
- Operator Blocks:
PauliBlock: Single Pauli string operationsBlockEncodingBlock: Block encoding for linear combinations of unitaries
Composing Blocks: The CompositeBlock¶
The CompositeBlock is how you combine multiple blocks into larger circuits. It takes a sequence of blocks and stitches them together respecting each block’s target_qubits.
from qarp.blocks import CompositeBlock, HnBlock, HEABlock, ReadoutBlock
# Create individual blocks
init = HnBlock(n_qubits=4, target_qubits=[0, 1, 2, 3])
ansatz = HEABlock(
n_qubits=4,
n_layers=2,
real=False,
linear=True,
circular=False,
use_cz=False,
target_qubits=[0, 1, 2, 3],
)
measure = ReadoutBlock(n_qubits=4, target_qubits=[0, 1, 2, 3])
# Compose them together
full_circuit = CompositeBlock(
blocks=[init, ansatz, measure],
n_qubits=4,
name="MyCircuit"
)
full_circuit.build()
Key rules for composition:
Target qubits matter: Each block’s
target_qubitsdetermines where it acts in the larger circuitOrder matters: Blocks are applied in sequence
Measurements usually go last: nothing forbids a
ReadoutBlockin the middle, but measuring and then continuing is a mid-circuit measurement — it moves the run onto the per-shot trajectory path (see below), and any primitive that consumes amplitudes will reject the circuit with aCapabilityError(see Errors)
Measurement and Classical Control¶
OpenQARP exposes three block primitives for the quantum/classical interface:
MeasureBlock— singleMeasurecommand (one qubit, one cbit)ResetBlock— projective reset of a qubit to|0⟩ConditionalBlock— wraps another block and only applies it when a classical condition (AND of(cbit, value)pairs) holds
Convenience SimpleBlock builder shortcuts (block.measure(q, c), block.reset(q)) are also available for inline use.
End-of-circuit measurement is also still supported via the bulk ReadoutBlock:
from qarp.blocks import ReadoutBlock
# Measure all 4 qubits
measurements = ReadoutBlock(n_qubits=4, target_qubits=[0, 1, 2, 3])
Any non-unitary command (Measure with a cbit, Reset, or a conditional gate) puts the simulator on the per-shot trajectory path automatically; circuits without these stay on the fast statevector path.
Executing Quantum Algorithms¶
OpenQARP separates the what (algorithm specification) from the how (execution). This is achieved through Primitive Algorithms, Targets, and Engines.
Primitive Algorithms¶
A PrimitiveAlgorithm defines a fundamental quantum measurement protocol. These are the key operations from which more complex algorithms are built.
Core primitives include:
StateVector: Exact statevector simulation (no sampling)Sampler: Shot-based sampling from circuitsPauliAveraging: Efficient expectation value estimation via Pauli groupingHadamardTest: Measures overlaps and matrix elementsSWAPTest: Alternative method for measuring overlaps
Each primitive takes:
bra: The bra state block ⟨ψ|ket: The ket state block |ψ⟩operator: The observable to measure (optional)n_shots: Number of measurement shots (optional)
Device configuration is now an engine concern, not a primitive concern: pass a
Device to the engine constructor (QarpEngine(device=...)), and every
primitive executed by that engine inherits the device’s rebase / route / noise
pipeline.
from qarp.algorithms import StateVector, PauliAveraging
from qarp.blocks import HEABlock
from qarp.operators import QubitOperator
hamiltonian = QubitOperator("Z0 Z1") + QubitOperator("X0")
ansatz = HEABlock(
n_qubits=4, n_layers=2, real=False, linear=True, circular=False, use_cz=False
)
# Exact statevector computation
sv_primitive = StateVector(ket=ansatz, operator=hamiltonian)
# Shot-based Pauli averaging
pa_primitive = PauliAveraging(ket=ansatz, operator=hamiltonian, n_shots=1000)
The Target Enum¶
The Target specifies what quantity a primitive algorithm computes:
Target.SAMPLING: Raw measurement distributionTarget.EXPECTATION_VALUE: ⟨ψ|O|ψ⟩Target.OVERLAP: ⟨φ|ψ⟩Target.TRANSITION_AMPLITUDE: ⟨φ|O|ψ⟩
Targets are inferred automatically based on what you provide (bra, ket, operator).
Engines: The Execution Backend¶
An Engine is responsible for actually executing quantum circuits. OpenQARP currently provides two qarpx-native engines:
QarpEngine: Fast statevector simulation backed by theQarpSimulator. Optionaldevice=triggers rebase + route + noise via the unified compilation pipeline.CudaqEngine: GPU execution throughqx.CudaqSimulator. Needs thecuda-quantum-cu12runtime and a qarpx build compiled with CUDA-Q support (QARP_WITH_CUDAQ=ON); see Engines.
The engine abstracts away backend-specific details:
from qarp.engines import QarpEngine
from qarp.algorithms import StateVector
import numpy as np
# Create an engine
engine = QarpEngine()
# Build and run a primitive
primitive = StateVector(ket=ansatz, operator=hamiltonian)
engine.build([primitive])
# Get the symbolic parameters from the ansatz and provide values for them
# HEABlock creates symbols like ry_0_0, rz_0_0, ry_0_1, rz_0_1, etc.
params = {symbol: np.random.uniform(0, 2 * np.pi) for symbol in ansatz.symbols}
result = engine.run(params)
Engines provide two key methods:
build(primitives): Prepare the engine with a list of primitivesrun(params): Execute with given parameter values. Keys may besympysymbols or their names — both are coerced tostrinternally.
To draw shots from an arbitrary block, build a Sampler primitive and run it through the engine.
Composite Algorithms¶
CompositeAlgorithm represents higher-level quantum algorithms that orchestrate multiple primitives and potentially classical optimization. These are the algorithms you typically want to run.
Some available composite algorithms:
VQE: Variational Quantum Eigensolver for ground statesVQD: Variational Quantum Deflation for excited statesQAOA: Quantum Approximate Optimization AlgorithmPCE: Pauli Correlation EncodingQPE: Quantum Phase EstimationSSVQE: Subspace-Search VQEAdaptVQE,AdaptVQD: Adaptive ansatz constructionMonteCarlo: Quantum Monte Carlo methods
Example: VQE¶
from qarp.algorithms import VQE, StateVector
from qarp.blocks import HEABlock
from qarp.engines import QarpEngine
from qarp.optimizers import ScipyOptimizer
from qarp.operators import QubitOperator
# Define the Hamiltonian
hamiltonian = QubitOperator("Z0 Z1") + QubitOperator("X0") + QubitOperator("X1")
# Create the ansatz
ansatz = HEABlock(
n_qubits=2, n_layers=2, real=False, linear=True, circular=False, use_cz=False
)
# Set up VQE
vqe = VQE(
operator=hamiltonian,
ket=ansatz,
primitive=StateVector(),
optimizer=ScipyOptimizer("COBYLA"),
engine=QarpEngine(),
)
# Build and run
vqe.build()
energy, optimal_params = vqe.run()
print(f"Ground state energy: {energy}")
The Data Flow¶
Understanding how data flows through OpenQARP helps clarify the architecture:
Circuit construction
BlockBlock…CompositeBlockbuilt circuit
ReadoutBlock supplies the classical mapping.
Algorithm layer
PrimitiveAlgorithm
- defines the measurement strategy (Hadamard test, Pauli averaging)
- constructs the measurement circuits
- post-processes the results
CompositeAlgorithm
- orchestrates several primitives
- drives the classical optimisation loop
- combines their results
Execution layer
Engine
- executes circuits on a backend (QarpEngine, CudaqEngine)
- substitutes parameters
- returns measurement results
Device
- transforms the circuit to the gate set and architecture
- applies the noise model
Devices and Noise¶
The Device class represents the target quantum hardware configuration. It is a passive data bundle — no methods beyond check_fits. The compilation pipeline lives on the Engine (or the standalone helper qarp.devices.compile_for_device()).
import qarpx as qx
from qarp.devices import Device, NoiseModel, Architecture, get_nearest_neighbour_architecture
# Simple device with just qubit count
simple_device = Device(n_qubits=10)
# Device with architecture constraints (linear chain on 4 qubits)
constrained_device = Device(
n_qubits=4,
architecture=Architecture(n_qubits=4, edges=[(0, 1), (1, 2), (2, 3)]),
gate_set=qx.full_gateset_1q_2q(),
)
# Or use a helper for a standard topology (2x2 nearest-neighbour grid)
grid_device = Device(
n_qubits=4,
architecture=get_nearest_neighbour_architecture(xdim=2, ydim=2),
)
# Noise model with bit-flip error
noise_model = NoiseModel.bit_flip(0.01)
# Device with noise — note ``.inner`` to unwrap to the C++ ``qx.NoiseModel``
noisy_device = Device(
n_qubits=10,
noise_model=noise_model.inner,
)
The ergonomic NoiseModel builders are depolarizing, pauli,
bit_flip, and amplitude_damping; compose them with +
to build mixed-channel noise models. See Devices for their signatures.
Putting It All Together¶
Here’s a complete example showing how all concepts connect:
from qarp.blocks import CompositeBlock, HnBlock, HEABlock
from qarp.algorithms import VQE, StateVector
from qarp.engines import QarpEngine
from qarp.optimizers import ScipyOptimizer
from qarp.operators import QubitOperator
# 1. Define the problem: a simple Hamiltonian
H = QubitOperator("Z0 Z1", 1.0) + QubitOperator("X0", 0.5) + QubitOperator("X1", 0.5)
# 2. Build the circuit using blocks
n_qubits = 2
initialization = HnBlock(n_qubits=n_qubits, target_qubits=list(range(n_qubits)))
ansatz = HEABlock(
n_qubits=n_qubits,
n_layers=3,
real=False,
linear=True,
circular=False,
use_cz=False,
target_qubits=list(range(n_qubits)),
)
# Compose into a single state preparation block
state_prep = CompositeBlock(
blocks=[initialization, ansatz],
n_qubits=n_qubits,
name="StatePreparation"
)
# 3. Choose how to measure (primitive algorithm) - using StateVector for exact computation
primitive = StateVector()
# 4. Set up the high-level algorithm
vqe = VQE(
operator=H,
ket=state_prep,
primitive=primitive,
optimizer=ScipyOptimizer("COBYLA"),
engine=QarpEngine(),
)
# 5. Build and run
vqe.build()
energy, optimal_params = vqe.run()
print(f"Ground state energy: {energy}")
Summary¶
OpenQARP’s architecture can be summarized as:
Blocks are the building units for quantum circuits
CompositeBlock composes blocks into larger circuits
ReadoutBlock bridges quantum to classical information
PrimitiveAlgorithm defines how to extract information from circuits
Target specifies what quantity to compute
Engine executes circuits on a backend
CompositeAlgorithm orchestrates full quantum algorithms
Device describes hardware constraints and noise
This separation of concerns makes OpenQARP flexible: you can swap engines, change measurement strategies, or modify ansätze without rewriting your entire algorithm.