Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

QURI Parts Support

Tags: integration optimization variational

This page shows how to use Qamomile’s QURI Parts quantum SDK integration through a concrete optimization problem. In this tutorial, we use QAOA optimization for a small MaxCut instance as an example. We transpile a Qamomile qkernel to a QURI Parts circuit, then run sampling and expectation-value evaluation. QuriPartsExecutor uses Qulacs, a fast C++ state-vector simulator, by default, so the examples below run on a local CPU without any extra configuration.

The MaxCut problem

We use the same small 5-node graph from the QAOA for MaxCut tutorial so the focus stays on the QURI Parts integration. Maximizing ∑(i,j)∈E(1−sisj)/2\sum_{(i,j) \in E}(1 - s_i s_j)/2 is equivalent, up to a constant, to minimizing the antiferromagnetic Ising Hamiltonian HC=∑(i,j)∈EsisjH_C = \sum_{(i,j) \in E} s_i s_j. For unweighted MaxCut, every Jij=1J_{ij} = 1 and every hi=0h_i = 0, so we pass these coefficients directly to BinaryModel.from_ising. We use the model object for the quad / linear dictionaries passed to the QAOA qkernel and for decoding measurements back into spin values (+1/−1)(+1 / -1).

<Figure size 500x400 with 1 Axes>

Building the QAOA ansatz with @qkernel

We write reusable qkernels for the QAOA layers and the sampling ansatz. The recipe is the same as in the QAOA for MaxCut tutorial. After preparing a uniform superposition in the computational basis, we alternately apply cost and mixer layers pp times, then measure in the computational basis. The expectation-value qkernel is introduced later in the run() section so the circuit diagram below shows the sampling ansatz directly rather than a wrapper around a state-preparation helper.

qaoa_ansatz.draw(...) renders the Qamomile circuit diagram. We pass concrete values for the arguments that determine the problem structure (p, quad, linear, n) so the layered shape is visible. Meanwhile, gammas / betas are left as parameters whose values are supplied later.

<Figure size 1524.19x204 with 1 Axes>

Transpile to QURI Parts

QuriPartsTranspiler is used with transpile() the same way as any other quantum SDK. We bind the arguments that determine the problem structure and keep gammas / betas as runtime parameters.

executable.get_first_circuit() returns the generated QURI Parts circuit. The returned circuit is a QURI Parts LinearMappedParametricQuantumCircuit, and the 2p2p QAOA angles (gammas[0..p-1], betas[0..p-1]) remain available as named runtime parameters. We can confirm that with type(...) and the parameter count, then inspect the circuit directly with QURI Parts’ built-in draw_circuit.

LinearMappedParametricQuantumCircuit
qubit_count    : 5
parameter_count: 6
   ___     ___     ___     ___                     ___     ___     ___                     ___     ___     ___                                  
  | H |   |PPR|   |PPR|   |PRX|                   |PPR|   |PPR|   |PRX|                   |PPR|   |PPR|   |PRX|                                 
--|0  |---|5  |---|6  |---|11 |-------------------|16 |---|17 |---|22 |-------------------|27 |---|28 |---|33 |---------------------------------
  |___|   |   |   |_ _|   |___|                   |   |   |_ _|   |___|                   |   |   |_ _|   |___|                                 
   ___    |   |    | |     ___     ___     ___    |   |    | |     ___     ___     ___    |   |    | |     ___     ___     ___                  
  | H |   |   |    | |    |PPR|   |PPR|   |PRX|   |   |    | |    |PPR|   |PPR|   |PRX|   |   |    | |    |PPR|   |PPR|   |PRX|                 
--|1  |---|   |----| |----|7  |---|8  |---|12 |---|   |----| |----|18 |---|19 |---|23 |---|   |----| |----|29 |---|30 |---|34 |-----------------
  |___|   |___|    | |    |   |   |_ _|   |___|   |___|    | |    |   |   |_ _|   |___|   |___|    | |    |   |   |_ _|   |___|                 
   ___            _| |_   |   |    | |     ___     ___    _| |_   |   |    | |     ___     ___    _| |_   |   |    | |     ___     ___          
  | H |           |   |   |   |    | |    |PPR|   |PRX|   |   |   |   |    | |    |PPR|   |PRX|   |   |   |   |    | |    |PPR|   |PRX|         
--|2  |-----------|   |---|   |----| |----|9  |---|13 |---|   |---|   |----| |----|20 |---|24 |---|   |---|   |----| |----|31 |---|35 |---------
  |___|           |___|   |___|    | |    |   |   |___|   |___|   |___|    | |    |   |   |___|   |___|   |___|    | |    |   |   |___|         
   ___                            _| |_   |   |    ___     ___            _| |_   |   |    ___     ___            _| |_   |   |    ___     ___  
  | H |                           |   |   |   |   |PPR|   |PRX|           |   |   |   |   |PPR|   |PRX|           |   |   |   |   |PPR|   |PRX| 
--|3  |---------------------------|   |---|   |---|10 |---|14 |-----------|   |---|   |---|21 |---|25 |-----------|   |---|   |---|32 |---|36 |-
  |___|                           |___|   |___|   |   |   |___|           |___|   |___|   |   |   |___|           |___|   |___|   |   |   |___| 
   ___                                            |   |    ___                            |   |    ___                            |   |    ___  
  | H |                                           |   |   |PRX|                           |   |   |PRX|                           |   |   |PRX| 
--|4  |-------------------------------------------|   |---|15 |---------------------------|   |---|26 |---------------------------|   |---|37 |-
  |___|                                           |___|   |___|                           |___|   |___|                           |___|   |___| 

Each runtime parameter remains unbound until execution time. That means binding gammas / betas is treated as a parameter-value update on the QURI Parts side rather than a circuit rebuild. The problem structure, such as the Ising coefficients, qubit count, and number of layers, is fixed when the circuit is transpiled, leaving only the variational angles as runtime inputs.

Sampling QAOA with QuriPartsExecutor

executable.sample(executor, bindings=..., shots=...) returns a SampleJob. Calling .result() gives a SampleResult, which BinaryModel.decode_from_sampleresult decodes into a BinarySampleSet of spin variables (+1/−1)(+1 / -1). This lets us count cut edges without any additional conversion. QuriPartsExecutor() runs against the Qulacs state-vector simulator by default.

Mean energy at random init: -0.4140

Optimizing the QAOA parameters

A QAOA optimization loop reuses the same executable across many (gammas, betas) vectors. Call transpiler.transpile() once, then call executable.sample() many times. In this example, we define the sampling and decoding work as cost_fn() and optimize it with SciPy’s minimize function. The classical optimizer updates (gammas, betas) while lowering the mean sampled Ising energy. Each iteration reuses the same executable and QuriPartsExecutor.

Optimized mean energy: -0.7970
Optimal gammas       : [0.8743, -0.176, 2.0996]
Optimal betas        : [0.7128, -1.2839, 1.4911]
<Figure size 800x400 with 1 Axes>

Because the QuriPartsExecutor above was constructed with seed=42, the Qulacs sampler is reproducible, so re-running this page yields the same optimization trajectory and final energy; drop the seed argument to recover the original non-deterministic behavior. The optimizer should converge close to the ground-state energy of HCH_C on this 5-node graph. The optimized parameters from this run (opt_gammas, opt_betas) are reused throughout the rest of this page.

Expectation values with run()

In Qamomile, you write expectation values in a qkernel with qmc.expval(...). When this is transpiled to QURI Parts, it becomes an executable object that can be called with ExecutableProgram.run(executor, bindings=...). run() uses Qamomile’s recorded parameter information to bind runtime parameters before calling the QURI Parts estimator.

First build a Qamomile Hamiltonian for HC=∑(i,j)∈EZiZjH_C = \sum_{(i,j) \in E} Z_i Z_j. Then transpile the expectation-value qkernel and evaluate it at the optimized QAOA parameters.

Executable.run() expectation: -0.7065846268
sample mean energy          : -0.7970

ExecutableProgram.run(...) is the recommended route when you work through the Qamomile API. Direct executor.estimate(...) and executor.estimate_expectation(...) calls are still available when you intentionally manage QURI Parts circuits yourself. On the run() path, Qamomile binds the named runtime parameters before the estimator call, so you do not need to manage QURI Parts’ flat parameter order yourself.

The next section opens that lower-level path and shows how QURI Parts handles unbound and already-bound circuits differently.

Advanced QURI Parts features

QURI Parts exposes both parametric and non-parametric estimator paths. Qamomile normally hides that distinction behind ExecutableProgram.run(...) or executor.estimate(...), but direct estimate_expectation(...) calls are useful when you need full control over the QURI Parts circuit.

Expectation values: unbound circuits and bound circuits

QuriPartsExecutor.estimate_expectation(circuit, hamiltonian, param_values) computes expectation values with QURI Parts. It chooses between two QURI Parts estimators depending on the state of the circuit it receives:

  • Unbound parametric circuit: the circuit produced by transpile() still keeps its parameters as free variables. QURI Parts’ apply_circuit wraps it as a ParametricCircuitQuantumState, and the executor calls QURI Parts’ parametric estimator. In this case, param_values is used to bind the parameters at evaluation time.

  • Bound circuit, or a circuit with no parameters from the start: once parameters are fixed to concrete values, for example with circuit.bind_parameters([...]), the same apply_circuit call returns a GeneralCircuitQuantumState. In this case, the executor calls QURI Parts’ non-parametric estimator, and param_values is ignored.

This distinction helps when estimating computation cost. The parametric estimator is a good fit for optimization loops that evaluate the same circuit many times with different parameters, because it avoids a circuit copy at each iteration. If the parameters are already fixed to concrete values, the non-parametric estimator can be more efficient because it avoids parametric-circuit bookkeeping.

QURI Parts treats measure as a no-op at the circuit level. Therefore, the parametric circuit generated by transpiler.transpile(qaoa_ansatz, ...) can be used directly as the QAOA state preparation circuit for ∣γ,β⟩|\boldsymbol{\gamma}, \boldsymbol{\beta}\rangle. We can pass it to estimate_expectation together with the cost Hamiltonian to compute ⟨HC⟩\langle H_C \rangle without sampling noise. In a QAOA optimizer, you can keep the same circuit and replace executable.sample() plus decoding with executor.estimate(circuit, hamiltonian, params=...).

To try both paths directly, convert the Hamiltonian from the previous section to a QURI Parts operator and call estimate_expectation for each circuit variant.

unbound type           : LinearMappedParametricQuantumCircuit
unbound parameter_count: 6
circuit parameter order: ['gammas[0]', 'gammas[1]', 'gammas[2]', 'betas[0]', 'betas[1]', 'betas[2]']
bound   type           : ImmutableBoundParametricQuantumCircuit
parametric  estimator: -0.7065846268
non-param.  estimator: -0.7065846268

Both paths agree to numerical precision. They evaluate the same QAOA state against the same Ising cost Hamiltonian. The resulting noise-free expectation value at the optimized parameters should also match the sample-mean energy printed earlier within shot noise. This path selection is hidden inside Qamomile’s executor.estimate() interface, so you normally do not need to think about it. Call estimate_expectation directly only when you need to manage the QURI Parts circuit yourself.

executor.estimate(circuit, hamiltonian, params=...) is the easier method when you want to pass an existing QURI Parts circuit but keep Qamomile’s Hamiltonian object. It accepts a qamomile.observable.Hamiltonian directly, converts it automatically, and then calls estimate_expectation.

executor.estimate     : -0.7065846268

Using other QURI Parts samplers and estimators

QuriPartsExecutor() lazily creates the default Qulacs state-vector sampler and parametric estimator on first use. To swap QURI Parts’ sampler or estimator, pass it through QuriPartsTranspiler.executor(sampler=..., estimator=...), or instantiate QuriPartsExecutor(sampler=..., estimator=...) directly. The custom executor can be used anywhere executor appeared above. Swapping the sampler does not require re-transpiling the kernel. Custom samplers must return nonnegative whole measurement counts; fractional weights, including fractional outputs from ideal samplers, raise ValueError. The executable carries the circuit, while the executor carries the sampler or estimator used for execution.

As a concrete example, we build a noisy sampler with QURI Parts’ Qulacs NoiseSimulator. Then we compare the noiseless and noisy sample-mean energies at the same optimized parameters. With depolarizing noise applied to every gate, the noisy mean energy should drift away from the noise-free value. This confirms that the swapped sampler is actually being used.

noiseless sampler mean energy: -0.7970
noisy     sampler mean energy: -0.2420

Because both sampler runs return shot counts, we can compare their sampled energy distributions directly. The vertical line in each subplot marks the sample mean energy.

<Figure size 1100x400 with 2 Axes>

Depolarizing noise pushes the QAOA state toward the maximally mixed state. In that limit, every spin configuration is equally likely, and the mean of HCH_C converges to 0. The noisy mean energy is therefore closer to 0, meaning higher, than the noiseless one. Increasing error_prob or adding more noise channels pushes the noisy energy further toward 0. Setting error_prob=0.0 recovers the noiseless value within shot noise. The same pattern applies when swapping in any other QURI Parts sampler, such as a remote device, a density-matrix simulator, or a stochastic state-vector sampler. In all cases, the kernel does not need to be transpiled again.

Summary

  • QuriPartsTranspiler().transpile(kernel, bindings=..., parameters=[...]) converts the qkernel to a QURI Parts LinearMappedParametricQuantumCircuit, which can be inspected with QURI Parts’ draw_circuit.

  • QuriPartsExecutor supports executable.sample() for QAOA-style sampling, executable.run() for qkernels that return qmc.expval(...), and direct executor.estimate(...) calls for circuit-level expectation values against the Qulacs state-vector simulator by default.

  • estimate_expectation switches between QURI Parts’ parametric and non-parametric estimators depending on whether the input circuit still has free parameters. Usually, executor.estimate() lets you use this behavior without thinking about the switch.

  • Custom samplers and estimators, including QURI Parts’ NoiseSimulator-backed sampler, can be passed through transpiler.executor(...) without re-transpiling the kernel.

See also

  • CUDA-Q Support covers the same MaxCut QAOA workflow with CUDA-Q.

  • Qiskit Support covers the same workflow on Qiskit, including Aer simulators, Qiskit primitives, and native Qiskit circuit features.