High-Level Quantum Circuit Synthesis with Classiq
Learn how Classiq's synthesis engine lets you describe quantum algorithms at a functional level and automatically optimizes circuits for depth, width, or hardware constraints, with a complete Grover's search example.
The Problem with Writing Circuits by Hand
Most quantum programming frameworks ask you to think like a hardware engineer. You place gates. You manage qubit indices. You flatten high-level algorithms into sequences of CX, H, and T gates. As circuit depth grows, this becomes untenable. A 100-qubit quantum algorithm may require millions of gates, and optimizing that circuit for a specific target device, with its own connectivity constraints, native gate set, and error rates, is a full-time job that changes every time the hardware changes.
Classiq takes a different approach. Instead of describing how to implement a quantum circuit, you describe what the circuit should compute. The Classiq synthesis engine figures out the implementation, subject to constraints you specify: minimize depth, minimize width, target a specific QPU, or stay within a gate budget. This is the same shift that happened in classical computing when compilers replaced assembly language.
The Functional Model: QMOD
Classiq’s quantum model description language is called QMOD. It is a functional specification language, not an imperative gate programming language. A QMOD specification says:
- What quantum functions are composed in what order
- What classical parameters control their behavior
- What hardware or optimization constraints apply
You write QMOD either in its own syntax (a Python-like DSL) or through the Classiq Python SDK, which translates Python function calls into QMOD under the hood. The synthesis engine compiles QMOD into gate-level circuits that satisfy your constraints.
Key QMOD concepts:
Quantum types. Variables have types like QNum (a quantum integer register), QBit (a single qubit), and QArray (a quantum array). The type system ensures that operations are applied to appropriately typed registers.
Quantum functions. Operations like hadamard_transform, arithmetic_expression, state_preparation, and grover_operator are high-level functions. You do not specify their internal gate decompositions; the synthesizer chooses the decomposition that best fits your constraints.
Constraints. You can specify max_gate_count, max_depth, max_width, or target a named QPU like ibm_kyiv or ionq_forte. Classiq will synthesize a circuit that respects those constraints, or report that no circuit satisfying them exists.
The Python SDK Workflow
The three-line core workflow is:
from classiq import create_model, synthesize, show
model = create_model(my_quantum_function)
circuit = synthesize(model)
show(circuit)
create_model converts your quantum function specification (written with Classiq decorators) into a QMOD description. synthesize calls the Classiq cloud synthesis engine and returns an optimized QuantumProgram object. show opens the Classiq IDE in your browser with the synthesized circuit, including its resource estimates and depth analysis.
The QuantumProgram object also carries execution metadata:
from classiq import execute, set_quantum_program_execution_preferences
from classiq.execution import ExecutionPreferences, IBMBackendPreferences
# Target a specific IBM backend
prefs = ExecutionPreferences(
backend_preferences=IBMBackendPreferences(backend_name="ibm_kyiv"),
num_shots=1000
)
set_quantum_program_execution_preferences(circuit, prefs)
results = execute(circuit).result()
The same circuit specification can be retargeted to a different backend by changing the preferences; no rewriting required.
Arithmetic Operations and State Preparation
Two categories of operations illustrate Classiq’s synthesis advantage particularly well.
Quantum arithmetic. Implementing an arithmetic expression like y = x^2 mod N in quantum gates requires Toffoli decompositions, carry-propagation networks, and careful ancilla qubit management. In Classiq:
from classiq import qfunc, QNum, Output, allocate
@qfunc
def square_mod(x: QNum, y: Output[QNum]) -> None:
y |= x ** 2
The synthesizer selects from multiple arithmetic circuit families (Cuccaro, Draper, Beauregard adders) and picks the one that minimizes your specified constraint. If you ask for minimum depth, you get a width-intensive parallel adder. If you ask for minimum width, you get a depth-intensive serial adder.
State preparation. Loading a classical probability distribution into a quantum state is a subroutine in many quantum finance and machine learning algorithms. The naive implementation requires an exponential number of gates. Classiq implements state preparation using log-depth divide-and-conquer circuits:
from classiq import qfunc, QArray, QBit, Output
from classiq.qmod.symbolic import sqrt
@qfunc
def load_distribution(
probabilities: list[float],
state: Output[QArray[QBit]]
) -> None:
prepare_state(probabilities=probabilities, bound=0.01, out=state)
The bound=0.01 parameter specifies the acceptable L2 error in the prepared state. The synthesizer trades depth for accuracy based on this bound.
Constraint-Based Optimization
The power of the synthesis approach becomes visible when you compare two compilations of the same specification with different constraints.
from classiq import create_model, synthesize, Constraints, OptimizationParameter
from classiq import set_constraints
model = create_model(my_grover_function)
# Optimize for minimum depth (uses more qubits)
set_constraints(model, Constraints(optimization_parameter=OptimizationParameter.DEPTH))
shallow_circuit = synthesize(model)
# Optimize for minimum width (uses fewer qubits, deeper circuit)
set_constraints(model, Constraints(optimization_parameter=OptimizationParameter.WIDTH))
narrow_circuit = synthesize(model)
print(f"Depth-optimized: depth={shallow_circuit.transpiled_circuit.depth}, "
f"qubits={shallow_circuit.transpiled_circuit.num_qubits}")
print(f"Width-optimized: depth={narrow_circuit.transpiled_circuit.depth}, "
f"qubits={narrow_circuit.transpiled_circuit.num_qubits}")
This is qualitatively different from what Qiskit’s transpiler does. Qiskit transpiles a circuit you have already written; it optimizes the decomposition, but it cannot change the algorithmic structure. Classiq synthesizes from the specification, so it can choose fundamentally different algorithms (different adder families, different oracle decompositions) that have different depth/width tradeoffs.
Complete Example: Grover’s Search
The following is a complete Grover’s search implementation in the Classiq Python SDK. The oracle marks states where a 4-bit integer equals 11.
from classiq import (
qfunc, QNum, QBit, Output, allocate,
create_model, synthesize, show, execute,
grover_operator, hadamard_transform, Z, X,
Constraints, OptimizationParameter
)
from classiq import set_constraints
# Step 1: Define the oracle
# Marks states where x == 11 (binary 1011) by flipping the phase
@qfunc
def oracle(x: QNum, target: QBit) -> None:
# x == 11 flips the target qubit
x_is_11 = (x == 11)
target ^= x_is_11
# Step 2: Define the Grover diffusion operator (inversion about average)
@qfunc
def diffuser(x: QNum) -> None:
hadamard_transform(x)
# Flip phase of |000...0> state
within_apply(
compute=lambda: X(x),
action=lambda: Z.on_all(x)
)
hadamard_transform(x)
# Step 3: Compose the full Grover circuit
@qfunc
def grover_search(x: Output[QNum]) -> None:
allocate(4, x) # 4-qubit register, searching over 0..15
# Initial equal superposition
hadamard_transform(x)
# Optimal number of iterations for N=16, M=1 marked state:
# k ≈ (pi/4) * sqrt(N/M) ≈ 3 iterations
repeat(3, lambda: grover_operator(
oracle=lambda: oracle(x, QBit()),
space_transform=lambda: hadamard_transform(x)
))
# Step 4: Synthesize
model = create_model(grover_search)
set_constraints(model, Constraints(
optimization_parameter=OptimizationParameter.DEPTH,
max_width=20 # limit to 20 qubits total including ancillas
))
circuit = synthesize(model)
# Step 5: Inspect the synthesized circuit
print(f"Circuit depth: {circuit.transpiled_circuit.depth}")
print(f"Gate count: {circuit.transpiled_circuit.count_ops()}")
print(f"Qubit count: {circuit.transpiled_circuit.num_qubits}")
# Open interactive IDE view
show(circuit)
# Step 6: Execute on simulator
from classiq.execution import ExecutionPreferences, ClassiqBackendPreferences
prefs = ExecutionPreferences(
backend_preferences=ClassiqBackendPreferences(backend_name="aer_simulator"),
num_shots=2000
)
results = execute(circuit).result()
counts = results[0].value.counts
# The state |1011> = 11 should have the highest count
sorted_counts = sorted(counts.items(), key=lambda x: x[1], reverse=True)
print("\nTop 5 measurement outcomes:")
for state, count in sorted_counts[:5]:
print(f" |{state}> : {count} ({100*count/2000:.1f}%)")
# Verify: state 11 should dominate with ~97% probability
# (Grover's algorithm with optimal iterations)
state_11_count = counts.get("1011", 0)
print(f"\nState |1011> (x=11) probability: {state_11_count/2000:.3f}")
print(f"Expected theoretical probability: ~0.961")
Notice that the oracle is written at the level of intent: x == 11. The synthesizer handles the Toffoli decomposition, ancilla qubit management, uncomputation of temporary registers, and transpilation to the target gate set. The same oracle specification works unchanged on IBM Heron, IonQ Forte, or a local Aer simulator.
Hardware-Aware Compilation
When you target a specific QPU, Classiq applies that device’s connectivity graph and native gate set during synthesis rather than as a post-processing step. This is significant because layout-aware synthesis can find solutions that post-hoc transpilation misses; for example, choosing an adder circuit whose structure naturally matches the heavy-hex connectivity of IBM devices, avoiding SWAP overhead.
from classiq.execution import IBMBackendPreferences, ExecutionPreferences
# Retarget to IBM Kyiv (127-qubit Eagle processor, heavy-hex connectivity)
ibm_prefs = ExecutionPreferences(
backend_preferences=IBMBackendPreferences(backend_name="ibm_kyiv"),
num_shots=1000
)
# No code changes to the algorithm specification
# Synthesis re-runs with IBM-specific constraints
model_ibm = create_model(grover_search)
circuit_ibm = synthesize(model_ibm)
print(f"IBM-targeted circuit depth: {circuit_ibm.transpiled_circuit.depth}")
When to Use Classiq
Classiq excels when:
- Your algorithm has significant classical-to-quantum data loading (state preparation, arithmetic oracles)
- You want to compare depth vs width tradeoffs without rewriting circuits
- You are targeting multiple hardware backends and want to avoid hardware-specific rewrites
- Your team includes algorithm designers who are not gate-level hardware experts
It is less useful for:
- Small tutorial-scale circuits where manual gate specification is fine
- Research into novel gate decompositions where you want explicit control
- Situations where the Classiq cloud synthesis latency (seconds to minutes for large circuits) is a bottleneck
Classiq is a commercial platform with a free tier and academic licensing. The synthesis engine itself is proprietary, but the QMOD specification and Python SDK are open. For quantum teams targeting production-scale algorithms, the synthesis approach represents a meaningful shift in how quantum software is developed and maintained.
Was this tutorial helpful?