Running FAST-VQE on Quantum Computers

This example demonstrates how to run FAST-VQE with custom estimators and samplers — including Amazon Braket’s local simulator interfaces, and real quantum hardware - specifically the Rigetti Ankaa-3 device.

We will also estimate the total shot counts and the approximate execution cost before running the on the Rigetti Ankaa-3 device.

Full Script

The script below shows the complete workflow; each section is explained afterward.

  1"""Run FAST-VQE with non-default estimators and samplers."""
  2# pyright: reportUnknownVariableType=false, reportMissingImports=false
  3# ruff: noqa
  4
  5from pathlib import Path
  6
  7import matplotlib.pyplot as plt
  8import numpy as np
  9from braket.devices import Devices
 10
 11import qrunch as qc
 12
 13
 14def plot_energies(
 15    fast_convergence: list[tuple[list[float], list[float], str]],
 16    *,
 17    outdir: Path = Path("dist"),
 18    show: bool = False,
 19) -> None:
 20    """
 21    Plot VQE energy convergence and log-scale error vs reference energy.
 22
 23    Args:
 24        fast_convergence: Sequence of energies, errors and label.
 25        outdir: Output directory where plots are written. Defaults to "dist".
 26        show: Whether to display the figures interactively after saving.
 27    """
 28    # Create figure and axis
 29    fig, ax = plt.subplots(figsize=(8, 5))
 30
 31    # Ensure output directory exists.
 32    outdir.mkdir(parents=True, exist_ok=True)
 33
 34    for fast_energies, energy_errors, label in fast_convergence:
 35        energies = np.array(fast_energies)
 36        errors = np.array(energy_errors)
 37
 38        # Prepare x-axis as 1-based iteration indices for readability.
 39        iterations = np.arange(1, len(energies) + 1)
 40
 41        # Plot error bars
 42        ax.errorbar(
 43            iterations,
 44            energies,
 45            yerr=errors,
 46            fmt="o",
 47            capsize=5,
 48            elinewidth=1.2,
 49            markeredgewidth=1.2,
 50            label=label,
 51        )
 52
 53    # Label axes and title
 54    ax.set_xlabel("Iteration")
 55    ax.set_ylabel("Total Energy [Hartree]")
 56    ax.set_title("FAST-VQE Convergence with Error Bars due to shot Noise, and emulated noise")
 57
 58    # Grid and legend
 59    ax.grid(True, linestyle="--", alpha=0.5)
 60    ax.legend()
 61
 62    # Tight layout and save plots
 63    fig.tight_layout()
 64
 65    convergence_plot = outdir / "vqe_convergence.png"
 66    fig.savefig(convergence_plot, dpi=200, bbox_inches="tight")
 67    if show:
 68        plt.show()
 69    else:
 70        # Close figures to free memory when running in batch contexts.
 71        plt.close(fig)
 72
 73
 74
 75
 76def main(
 77    *,
 78    do_local_braket: bool = True,
 79    do_noisy_sampler: bool = True,
 80    do_real_hardware: bool = False,
 81) -> tuple[float, float]:
 82    """
 83    Run FAST-VQE on LiH with different estimators and samplers.
 84
 85    Args:
 86        do_local_braket: Whether to run with the local Braket backend simulator.
 87        do_noisy_sampler: Whether to run with a noisy sampler emulating Ankaa-3.
 88        do_real_hardware: Whether to run with real quantum hardware (Ankaa-3).
 89
 90    """
 91
 92    # Build Lithium Hydride (LiH) molecular configuration.
 93    molecular_configuration = qc.build_molecular_configuration(
 94        molecule=[
 95            ("H", 0.0, 0.0, 0.0),
 96            ("Li", 1.5474, 0.0, 0.0),
 97        ],
 98        basis_set="sto3g",
 99    )
100    # Build ground state problem.
101    problem_builder = qc.problem_builder_creator().ground_state().standard().create()
102    ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
103
104    # Excitation gate estimator (Kvantifys proprietary chemistry tailored state vector simulator)
105    excitation_gate_estimator = (
106        qc
107        .estimator_creator()
108        .excitation_gate()  # Narrow the estimator type to an excitation gate estimator
109        .with_parallel_setting("parallel")  # Configure parallelization behavior: "serial" or "parallel"
110        .with_spin_particle_conservation()  # Configure to conserve alpha and beta electrons separately
111        .choose_estimator_error_mitigator()  # Start sub-choice: Choose estimator error mitigator
112        .symmetry_adapted()  # Perform the sub-choice selection - Here we pick the symmetry adapted error mitigator
113        .create(with_shot_counter=True)  # Create the estimator instance
114    )
115
116    # Excitation gate sampler (Kvantifys proprietary chemistry tailored state vector simulator)
117    excitation_gate_sampler = (
118        qc
119        .sampler_creator()
120        .excitation_gate()  # Narrow the sampler type to an excitation gate sampler
121        .with_parallel_setting("parallel")  # Configure parallelization behavior: "serial" or "parallel"
122        .with_spin_particle_conservation()  # Configure to conserve alpha and beta electrons separately
123        .create(with_shot_counter=True)  # Create the sampler instance
124    )
125
126    # A FAST gate selector using the excitation gate sampler.
127    excitation_gate_gate_selector = (
128        qc
129        .gate_selector_creator()
130        .fast()  # Narrow the gate selector type to a FAST gate selector
131        .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
132        .with_sampler(excitation_gate_sampler)  # Configure to use the excitation gate sampler
133        .create()  # Create the gate selector instance
134    )
135
136    # Build the user-configured VQE instance with the excitation_gate_estimator
137    # and gate selector that uses the excitation_gate_sampler
138    excitation_gate_fast_vqe_calculator = (
139        qc
140        .calculator_creator()  # Start creating a VQE instance
141        .vqe()  # Narrow to Variational quantum eigensolver (VQE)
142        .iterative()  # Narrow to the iterative VQE
143        .standard()  # Narrow to the standard FAST-VQE
144        .with_options(  # Configure to use the user-defined iterative VQE options
145            options=qc.options.IterativeVqeOptions(max_iterations=10)
146        )
147        .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
148        .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
149        .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
150        .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
151        .with_gate_selector(excitation_gate_gate_selector)  # Configure to use the gate selector
152        .create()  # Create the calculator instance
153    )
154    excitation_gate_result = excitation_gate_fast_vqe_calculator.calculate(ground_state_problem)
155
156    print("result from excitation gate estimator and sampler:")  # noqa: T201
157    print(excitation_gate_result)  # noqa: T201
158
159    print("Total number of shots used in estimator", excitation_gate_estimator.total_shots)  # noqa: T201
160    estimated_estimator_cost = excitation_gate_estimator.total_braket_price(device=Devices.Rigetti.Ankaa3)
161    print("Estimated cost in dollars for Estimator on Devices.Rigetti.Ankaa3: ", estimated_estimator_cost)  # noqa: T201
162
163    print("Total number of shots used in sampler", excitation_gate_sampler.total_shots)  # noqa: T201
164    estimated_sampler_cost = excitation_gate_sampler.total_braket_price(device=Devices.Rigetti.Ankaa3)
165    print("Estimated cost in dollars for Sampler on Devices.Rigetti.Ankaa3: ", estimated_sampler_cost)  # noqa: T201
166
167    local_braket_result = None
168    if do_local_braket:
169        # An Amazon Local Braket backend estimator
170        local_braket_estimator = (
171            qc
172            .estimator_creator()
173            .backend()  # Narrow the estimator type to a backend estimator
174            .choose_backend()  # Start sub-choice: Choose backend
175            .local_amazon_braket()  # Perform the sub-choice selection - Here we pick the local Braket state vector simulator backend
176            .create(with_shot_counter=True)  # Create the estimator instance
177        )
178
179        # An Amazon Local Braket backend sampler
180        local_braket_sampler = (
181            qc
182            .sampler_creator()
183            .backend()  # Narrow the sampler type to a backend sampler
184            .choose_backend()  # Start sub-choice: Choose backend
185            .local_amazon_braket()  # Perform the sub-choice selection - Here we pick the local Braket simulator backend
186            .create(with_shot_counter=True)  # Create the estimator instance
187        )
188
189        # A FAST gate selector using the local braket sampler.
190        local_braket_gate_selector = (
191            qc
192            .gate_selector_creator()
193            .fast()  # Narrow the gate selector type to a FAST gate selector
194            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
195            .with_sampler(local_braket_sampler)  # Configure to use the local braket sampler
196            .create()  # Create the gate selector instance
197        )
198
199        # Create the calculator instance using the local_braket_estimator
200        # and gate selector that uses the local_braket_sampler
201        local_braket_fast_vqe_calculator = (
202            qc
203            .calculator_creator()  # Start creating a calculator instance
204            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
205            .iterative()  # Narrow to the iterative VQE
206            .standard()  # Narrow to the standard FAST-VQE
207            .with_options(  # Configure to use the user-defined iterative VQE options
208                options=qc.options.IterativeVqeOptions(max_iterations=10)
209            )
210            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
211            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
212            .with_estimator(local_braket_estimator)  # Configure to use the local braket estimator
213            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
214            .with_gate_selector(local_braket_gate_selector)  # Configure to use the gate selector
215            .create()  # Create the calculator instance
216        )
217        local_braket_result = local_braket_fast_vqe_calculator.calculate(ground_state_problem)
218
219        print("Total number of shots used in estimator", local_braket_estimator.total_shots)  # noqa: T201
220        estimated_estimator_cost = local_braket_estimator.total_braket_price(device=Devices.Rigetti.Ankaa3)
221        print("Estimated cost in dollars for Estimator on Devices.Rigetti.Ankaa3: ", estimated_estimator_cost)  # noqa: T201
222
223        print("Total number of shots used in sampler", local_braket_sampler.total_shots)  # noqa: T201
224        estimated_sampler_cost = local_braket_sampler.total_braket_price(device=Devices.Rigetti.Ankaa3)
225        print("Estimated cost in dollars for Sampler on Devices.Rigetti.Ankaa3: ", estimated_sampler_cost)  # noqa: T201
226
227        print("result from local braket estimator and sampler:")  # noqa: T201
228        print(local_braket_result)  # noqa: T201
229
230    noisy_result = None
231    if do_noisy_sampler:
232
233        # Create a real Ankaa-3 Quantum Processor hardware backend.
234        backend = qc.backend_creator().amazon_braket(device=Devices.Rigetti.Ankaa3).create()
235
236        # Get device data from the backend - from the Ankaa-3 Quantum Processor
237        ankaa3_device_data = backend.get_device_data()
238
239        # An Amazon Local Braket backend simulator sampler
240        # that emulate the noise from the Ankaa-3 Quantum Processor
241        # using the density matrix simulator.
242        noisy_sampler = (
243            qc
244            .sampler_creator()
245            .backend()  # Narrow the estimator type to a backend estimator
246            .choose_backend()  # Start sub-choice: Choose backend
247            .local_amazon_braket(
248                device_to_simulate=ankaa3_device_data
249            )  # Perform the sub-choice selection - Here the local braket emulating Ankaa-3
250            .choose_sampler_error_mitigator()  # Start sub-choice: Choose sampler error mitigator
251            .hamming_weight_post_selection()  # Configure the error mitigator to use hamming weight post selection.
252            .create()  # Create the estimator instance
253        )
254
255        # A FAST gate selector using the noisy sampler that emulate an Ankaa-3 Quantum Processor.
256        noisy_gate_selector = (
257            qc
258            .gate_selector_creator()
259            .fast()  # Narrow the gate selector type to a FAST gate selector
260            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
261            .with_sampler(noisy_sampler)  # Configure to use the local braket sampler
262            .create()  # Create the gate selector instance
263        )
264
265        # Build the user-configured VQE calculator instance with the excitation gate estimator
266        # and gate selector that uses the noisy sampler that emulate an Ankaa-3 Quantum Processor.
267        noisy_fast_vqe_calculator = (
268            qc
269            .calculator_creator()  # Start creating a calculator instance
270            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
271            .iterative()  # Narrow to the iterative VQE
272            .standard()  # Narrow to the standard FAST-VQE
273            .with_options(  # Configure to use the user-defined iterative VQE options
274                options=qc.options.IterativeVqeOptions(max_iterations=10)
275            )
276            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
277            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
278            .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
279            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
280            .with_gate_selector(noisy_gate_selector)  # Configure to use the gate selector
281            .create()  # Create the calculator instance
282        )
283        noisy_result = noisy_fast_vqe_calculator.calculate(ground_state_problem)
284        print("result from the local braket estimator and noisy sampler:")  # noqa: T201
285        print(noisy_result)  # noqa: T201
286
287    real_hardware_braket_result = None
288    if do_real_hardware:
289        # A real hardware sampler using the Amazon Braket backend
290        # dispatching to an Ankaa-3 Quantum Processor
291        real_hardware_braket_sampler = (
292            qc
293            .sampler_creator()
294            .backend()  # Narrow the estimator type to a backend estimator
295            .choose_backend()  # Start sub-choice: Choose backend
296            .amazon_braket(
297                device=Devices.Rigetti.Ankaa3
298            )  # Perform the sub-choice selection - Here we pick the Rigetti Ankaa3
299            .choose_sampler_error_mitigator()  # Start sub-choice: Choose sampler error mitigator
300            .hamming_weight_post_selection()  # Configure the error mitigator to use hamming weight post selection.
301            .create()  # Create the estimator instance
302        )
303
304        # A FAST gate selector using the Ankaa-3 Quantum Processor for the sampler.
305        real_hardware_braket_gate_selector = (
306            qc
307            .gate_selector_creator()
308            .fast()  # Narrow the gate selector type to a FAST gate selector
309            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
310            .with_sampler(real_hardware_braket_sampler)  # Configure to use the local braket sampler
311            .create()  # Create the gate selector instance
312        )
313
314        # Build the user-configured VQE calculator instance with the excitation gate estimator
315        # and gate selector that uses the Ankaa-3 Quantum Processor for the sampler.
316        real_hardware_braket_fast_vqe_calculator = (
317            qc
318            .calculator_creator()  # Start creating a calculator instance
319            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
320            .iterative()  # Narrow to the iterative VQE
321            .standard()  # Narrow to the standard FAST-VQE
322            .with_options(  # Configure to use the user-defined iterative VQE options
323                options=qc.options.IterativeVqeOptions(max_iterations=10)
324            )
325            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
326            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
327            .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
328            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
329            .with_gate_selector(real_hardware_braket_gate_selector)  # Configure to use the gate selector
330            .create()  # Create the calculator instance
331        )
332        real_hardware_braket_result = real_hardware_braket_fast_vqe_calculator.calculate(ground_state_problem)
333
334        print("result from the excitation gate estimator and real quantum hardware sampler:")  # noqa: T201
335        print(real_hardware_braket_result)  # noqa: T201
336
337    fast_convergence: list[tuple[list[float], list[float], str]] = []
338
339    labels = ["Excitation Gate Simulator"]
340    results = [excitation_gate_result]
341    if do_local_braket and local_braket_result is not None:
342        labels.append("Local Braket Simulator")
343        results.append(local_braket_result)
344
345    if do_noisy_sampler and noisy_result is not None:
346        labels.append("Noisy Simulator")
347        results.append(noisy_result)
348
349    if do_real_hardware and real_hardware_braket_result is not None:
350        labels.append("Real Hardware")
351        results.append(real_hardware_braket_result)
352
353    for result, label in zip(results, labels, strict=True):
354        fast_energies = result.total_energy_per_macro_iteration_with_initial_energy_and_final_energy.values
355        energy_errors = result.total_energy_per_macro_iteration_with_initial_energy_and_final_energy.errors
356        fast_convergence.append((fast_energies, energy_errors, label))
357
358    plot_energies(
359        fast_convergence=fast_convergence,
360        show=True,
361    )
362
363    return excitation_gate_result.total_energy.value, excitation_gate_result.total_energy.error
364
365
366if __name__ == "__main__":
367    main()

Explanation

  1. Imports

    from pathlib import Path
    
    import matplotlib.pyplot as plt
    import numpy as np
    from braket.devices import Devices
    
    import qrunch as qc
    

    We import the public Kvantify Qrunch API via import qrunch as qc (stable user interface) and, since we will query device pricing and eventually run on the device, we also import Devices from braket.devices. In addition, we use matplotlib, numpy, and Path (from pathlib).

  2. The plotting method

    def plot_energies(
        fast_convergence: list[tuple[list[float], list[float], str]],
        *,
        outdir: Path = Path("dist"),
        show: bool = False,
    ) -> None:
        """
        Plot VQE energy convergence and log-scale error vs reference energy.
    
        Args:
            fast_convergence: Sequence of energies, errors and label.
            outdir: Output directory where plots are written. Defaults to "dist".
            show: Whether to display the figures interactively after saving.
        """
        # Create figure and axis
        fig, ax = plt.subplots(figsize=(8, 5))
    
        # Ensure output directory exists.
        outdir.mkdir(parents=True, exist_ok=True)
    
        for fast_energies, energy_errors, label in fast_convergence:
            energies = np.array(fast_energies)
            errors = np.array(energy_errors)
    
            # Prepare x-axis as 1-based iteration indices for readability.
            iterations = np.arange(1, len(energies) + 1)
    
            # Plot error bars
            ax.errorbar(
                iterations,
                energies,
                yerr=errors,
                fmt="o",
                capsize=5,
                elinewidth=1.2,
                markeredgewidth=1.2,
                label=label,
            )
    
        # Label axes and title
        ax.set_xlabel("Iteration")
        ax.set_ylabel("Total Energy [Hartree]")
        ax.set_title("FAST-VQE Convergence with Error Bars due to shot Noise, and emulated noise")
    
        # Grid and legend
        ax.grid(True, linestyle="--", alpha=0.5)
        ax.legend()
    
        # Tight layout and save plots
        fig.tight_layout()
    
        convergence_plot = outdir / "vqe_convergence.png"
        fig.savefig(convergence_plot, dpi=200, bbox_inches="tight")
        if show:
            plt.show()
        else:
            # Close figures to free memory when running in batch contexts.
            plt.close(fig)
    
    
    

    This code plots the convergence of the VQE energy with the error as error bars.

    The code generates a PNG file under dist/ as vqe_convergence.png

  3. Build the LiH ground-state problem

        # Build Lithium Hydride (LiH) molecular configuration.
        molecular_configuration = qc.build_molecular_configuration(
            molecule=[
                ("H", 0.0, 0.0, 0.0),
                ("Li", 1.5474, 0.0, 0.0),
            ],
            basis_set="sto3g",
        )
        # Build ground state problem.
        problem_builder = qc.problem_builder_creator().ground_state().standard().create()
        ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
    

    We construct a lithium hydride (LiH) molecule using the minimal STO-3G basis.

  4. Create an excitation-gate estimator

        # Excitation gate estimator (Kvantifys proprietary chemistry tailored state vector simulator)
        excitation_gate_estimator = (
            qc
            .estimator_creator()
            .excitation_gate()  # Narrow the estimator type to an excitation gate estimator
            .with_parallel_setting("parallel")  # Configure parallelization behavior: "serial" or "parallel"
            .with_spin_particle_conservation()  # Configure to conserve alpha and beta electrons separately
            .choose_estimator_error_mitigator()  # Start sub-choice: Choose estimator error mitigator
            .symmetry_adapted()  # Perform the sub-choice selection - Here we pick the symmetry adapted error mitigator
            .create(with_shot_counter=True)  # Create the estimator instance
        )
    

    The excitation-gate estimator is Kvantify’s proprietary chemistry-aware state-vector simulator. It supports parallel execution and can enforce separate conservation of α and β electrons. We additionally attach a symmetry-adapted error mitigator and we enable shot counting for later cost analysis. This is the default estimator, but we here define it explicitly for clarity.

    The estimator is the object that can evaluate expectation values of quantum circuits. In this case the estimator is used to evaluate the expectation value of the Hamiltonian (energy) of the quantum state prepared by the VQE iterative ansatz.

  5. Create an excitation-gate sampler

        # Excitation gate sampler (Kvantifys proprietary chemistry tailored state vector simulator)
        excitation_gate_sampler = (
            qc
            .sampler_creator()
            .excitation_gate()  # Narrow the sampler type to an excitation gate sampler
            .with_parallel_setting("parallel")  # Configure parallelization behavior: "serial" or "parallel"
            .with_spin_particle_conservation()  # Configure to conserve alpha and beta electrons separately
            .create(with_shot_counter=True)  # Create the sampler instance
        )
    

    Like the estimator, it runs in parallel mode, conserves spin, and maintains an internal shot counter.

    The sampler is the object that provide a set of bitstrings representing measurement outcomes, and associated counts. In this case, this means sampling the quantum state prepared by the VQE iterative ansatz, where each bitstring represents a Slater determinant (or occupation number vector).

  6. Build the FAST gate selector

        # A FAST gate selector using the excitation gate sampler.
        excitation_gate_gate_selector = (
            qc
            .gate_selector_creator()
            .fast()  # Narrow the gate selector type to a FAST gate selector
            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
            .with_sampler(excitation_gate_sampler)  # Configure to use the excitation gate sampler
            .create()  # Create the gate selector instance
        )
    

    A FAST gate selector controls which excitation gates are added during the iterative VQE procedure. Here it is configured to use the excitation-gate sampler and 10000 shots per iteration. Here we use the default FAST gate selector that uses the Heuristic Gradient metric to select gates. Note that the FAST gate selector only require a sampler, not an estimator. See https://arxiv.org/abs/2303.07417 for details.

  7. Assemble and run the excitation-gate-based FAST-VQE calculator

        # Build the user-configured VQE instance with the excitation_gate_estimator
        # and gate selector that uses the excitation_gate_sampler
        excitation_gate_fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a VQE instance
            .vqe()  # Narrow to Variational quantum eigensolver (VQE)
            .iterative()  # Narrow to the iterative VQE
            .standard()  # Narrow to the standard FAST-VQE
            .with_options(  # Configure to use the user-defined iterative VQE options
                options=qc.options.IterativeVqeOptions(max_iterations=10)
            )
            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
            .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
            .with_gate_selector(excitation_gate_gate_selector)  # Configure to use the gate selector
            .create()  # Create the calculator instance
        )
        excitation_gate_result = excitation_gate_fast_vqe_calculator.calculate(ground_state_problem)
    

    We combine the estimator, and gate selector into a user-configured iterative VQE calculator. It uses up to 10 iterations, a “quick default” greedy minimizer, and 100000 estimator shots per energy evaluation. The last line performs the ground state energy calculation.

  1. Estimate total shots and cost

        print("Total number of shots used in estimator", excitation_gate_estimator.total_shots)  # noqa: T201
        estimated_estimator_cost = excitation_gate_estimator.total_braket_price(device=Devices.Rigetti.Ankaa3)
        print("Estimated cost in dollars for Estimator on Devices.Rigetti.Ankaa3: ", estimated_estimator_cost)  # noqa: T201
    
        print("Total number of shots used in sampler", excitation_gate_sampler.total_shots)  # noqa: T201
        estimated_sampler_cost = excitation_gate_sampler.total_braket_price(device=Devices.Rigetti.Ankaa3)
        print("Estimated cost in dollars for Sampler on Devices.Rigetti.Ankaa3: ", estimated_sampler_cost)  # noqa: T201
    

    Both estimator and sampler expose their total shot counts and we can estimate cloud-hardware pricing via .total_braket_price(device=...). This provides a convenient cost estimate for experiments on, for example, Rigetti Ankaa-3.

  2. Create a local Braket backend estimator

        # An Amazon Local Braket backend estimator
        local_braket_estimator = (
            qc
            .estimator_creator()
            .backend()  # Narrow the estimator type to a backend estimator
            .choose_backend()  # Start sub-choice: Choose backend
            .local_amazon_braket()  # Perform the sub-choice selection - Here we pick the local Braket state vector simulator backend
            .create(with_shot_counter=True)  # Create the estimator instance
        )

        # An Amazon Local Braket backend sampler
        local_braket_sampler = (
            qc
            .sampler_creator()
            .backend()  # Narrow the sampler type to a backend sampler
            .choose_backend()  # Start sub-choice: Choose backend
            .local_amazon_braket()  # Perform the sub-choice selection - Here we pick the local Braket simulator backend
            .create(with_shot_counter=True)  # Create the estimator instance
        )

Here we construct an estimator and sampler that uses a local Braket simulator backend. The prefix``local_`` indicates that the calculation runs locally on the user’s machine. We enable shot counting for later cost analysis.

  1. Build and run a FAST-VQE with the local Braket backends

        # A FAST gate selector using the local braket sampler.
        local_braket_gate_selector = (
            qc
            .gate_selector_creator()
            .fast()  # Narrow the gate selector type to a FAST gate selector
            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
            .with_sampler(local_braket_sampler)  # Configure to use the local braket sampler
            .create()  # Create the gate selector instance
        )

        # Create the calculator instance using the local_braket_estimator
        # and gate selector that uses the local_braket_sampler
        local_braket_fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator instance
            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
            .iterative()  # Narrow to the iterative VQE
            .standard()  # Narrow to the standard FAST-VQE
            .with_options(  # Configure to use the user-defined iterative VQE options
                options=qc.options.IterativeVqeOptions(max_iterations=10)
            )
            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
            .with_estimator(local_braket_estimator)  # Configure to use the local braket estimator
            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
            .with_gate_selector(local_braket_gate_selector)  # Configure to use the gate selector
            .create()  # Create the calculator instance
        )
        local_braket_result = local_braket_fast_vqe_calculator.calculate(ground_state_problem)

Similar to the excitation-gate-based VQE, we build a gate selector that uses the local Braket sampler. Then we assemble an iterative VQE, configured to use the local Braket estimator, and create the FAST-VQE calculator.

Finally, we run the FAST-VQE calculator again, this time with the local Braket estimator and sampler.

  1. Evaluate cost for the local Braket run

        print("Total number of shots used in estimator", local_braket_estimator.total_shots)  # noqa: T201
        estimated_estimator_cost = local_braket_estimator.total_braket_price(device=Devices.Rigetti.Ankaa3)
        print("Estimated cost in dollars for Estimator on Devices.Rigetti.Ankaa3: ", estimated_estimator_cost)  # noqa: T201

        print("Total number of shots used in sampler", local_braket_sampler.total_shots)  # noqa: T201
        estimated_sampler_cost = local_braket_sampler.total_braket_price(device=Devices.Rigetti.Ankaa3)
        print("Estimated cost in dollars for Sampler on Devices.Rigetti.Ankaa3: ", estimated_sampler_cost)  # noqa: T201

As before, we print shot statistics and estimated dollar costs. The cost estimates should be close to the cost estimated using the excitation-gate estimator and sampler. Any differences arise from the error that arises from the limited shot counts.

  1. Create a noisy sampler


        # Create a real Ankaa-3 Quantum Processor hardware backend.
        backend = qc.backend_creator().amazon_braket(device=Devices.Rigetti.Ankaa3).create()

        # Get device data from the backend - from the Ankaa-3 Quantum Processor
        ankaa3_device_data = backend.get_device_data()

        # An Amazon Local Braket backend simulator sampler
        # that emulate the noise from the Ankaa-3 Quantum Processor
        # using the density matrix simulator.
        noisy_sampler = (
            qc
            .sampler_creator()
            .backend()  # Narrow the estimator type to a backend estimator
            .choose_backend()  # Start sub-choice: Choose backend
            .local_amazon_braket(
                device_to_simulate=ankaa3_device_data
            )  # Perform the sub-choice selection - Here the local braket emulating Ankaa-3
            .choose_sampler_error_mitigator()  # Start sub-choice: Choose sampler error mitigator
            .hamming_weight_post_selection()  # Configure the error mitigator to use hamming weight post selection.
            .create()  # Create the estimator instance
        )

Next, we create a noisy sampler that uses the Rigetti Aspen-M-3 device as a noise model. You can think of this as a simulator that emulates the noise characteristics of the real device, using a density matrix simulator.

The cost for running sampling on real hardware is typically much smaller than running the estimator on real hardware, as shown by the cost estimates.

To create the noisy sampler, we first create a backend object, and then query the backend object for the device data, which is used to emulate the behaviour of that device.

Note that this time we choose the hamming_weight_post_selection as the sampler error mitigator.

This error mitigator removes all states with an incorrect Hamming weight, i.e., an incorrect number of 1’s in the bitstrings, which means an incorrect number of alpha and beta electrons in the occupation number vector (or Slater determinant).

This is required as the noisy sampler will produce bitstrings that do not conserve the number of alpha and beta electrons, due to noise.

  1. Build and run a FAST-VQE with the noisy sampler

        # A FAST gate selector using the noisy sampler that emulate an Ankaa-3 Quantum Processor.
        noisy_gate_selector = (
            qc
            .gate_selector_creator()
            .fast()  # Narrow the gate selector type to a FAST gate selector
            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
            .with_sampler(noisy_sampler)  # Configure to use the local braket sampler
            .create()  # Create the gate selector instance
        )

        # Build the user-configured VQE calculator instance with the excitation gate estimator
        # and gate selector that uses the noisy sampler that emulate an Ankaa-3 Quantum Processor.
        noisy_fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator instance
            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
            .iterative()  # Narrow to the iterative VQE
            .standard()  # Narrow to the standard FAST-VQE
            .with_options(  # Configure to use the user-defined iterative VQE options
                options=qc.options.IterativeVqeOptions(max_iterations=10)
            )
            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
            .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
            .with_gate_selector(noisy_gate_selector)  # Configure to use the gate selector
            .create()  # Create the calculator instance
        )
        noisy_result = noisy_fast_vqe_calculator.calculate(ground_state_problem)
        print("result from the local braket estimator and noisy sampler:")  # noqa: T201
        print(noisy_result)  # noqa: T201

Similar to the earlier, we build a gate selector that uses the noisy sampler. Then we assemble an iterative VQE and create the FAST-VQE calculator. Finally we run FAST-VQE calculator again, this time with the noisy sampler and local Braket state vector estimator.

This yields a slightly higher energy, due to the noise in the sampler.

  1. Create a real hardware sampler

Finally, now that we have an estimate of the cost and we have emulated the behaviour of the real hardware, we can create a sampler that runs on real quantum hardware.

        # A real hardware sampler using the Amazon Braket backend
        # dispatching to an Ankaa-3 Quantum Processor
        real_hardware_braket_sampler = (
            qc
            .sampler_creator()
            .backend()  # Narrow the estimator type to a backend estimator
            .choose_backend()  # Start sub-choice: Choose backend
            .amazon_braket(
                device=Devices.Rigetti.Ankaa3
            )  # Perform the sub-choice selection - Here we pick the Rigetti Ankaa3
            .choose_sampler_error_mitigator()  # Start sub-choice: Choose sampler error mitigator
            .hamming_weight_post_selection()  # Configure the error mitigator to use hamming weight post selection.
            .create()  # Create the estimator instance
        )

Here we create a sampler that runs on the Rigetti Ankaa-3 device. We choose .hamming_weight_post_selection() as the sampler error mitigator for the sampler.

Choosing another device is as simple as changing the device name.

See Choose a Backend for more options on using different backends, different quantum computer providers, etc.

  1. Build and run a FAST-VQE with the real hardware sampler

        # A FAST gate selector using the Ankaa-3 Quantum Processor for the sampler.
        real_hardware_braket_gate_selector = (
            qc
            .gate_selector_creator()
            .fast()  # Narrow the gate selector type to a FAST gate selector
            .with_shots(10_000)  # Configure to use 10,000 shots when estimating sampling
            .with_sampler(real_hardware_braket_sampler)  # Configure to use the local braket sampler
            .create()  # Create the gate selector instance
        )

        # Build the user-configured VQE calculator instance with the excitation gate estimator
        # and gate selector that uses the Ankaa-3 Quantum Processor for the sampler.
        real_hardware_braket_fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator instance
            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
            .iterative()  # Narrow to the iterative VQE
            .standard()  # Narrow to the standard FAST-VQE
            .with_options(  # Configure to use the user-defined iterative VQE options
                options=qc.options.IterativeVqeOptions(max_iterations=10)
            )
            .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
            .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
            .with_estimator(excitation_gate_estimator)  # Configure to use the excitation gate estimator
            .with_estimator_shots(100_000)  # Configure to use 10,000 shots in the estimator
            .with_gate_selector(real_hardware_braket_gate_selector)  # Configure to use the gate selector
            .create()  # Create the calculator instance
        )
        real_hardware_braket_result = real_hardware_braket_fast_vqe_calculator.calculate(ground_state_problem)

As before, we build a VQE instance, a gate selector, and the FAST-VQE calculator, that now uses the real hardware sampler to select the gate.

Finally, we run FAST-VQE calculator.

  1. Make the plot

    fast_convergence: list[tuple[list[float], list[float], str]] = []

    labels = ["Excitation Gate Simulator"]
    results = [excitation_gate_result]
    if do_local_braket and local_braket_result is not None:
        labels.append("Local Braket Simulator")
        results.append(local_braket_result)

    if do_noisy_sampler and noisy_result is not None:
        labels.append("Noisy Simulator")
        results.append(noisy_result)

    if do_real_hardware and real_hardware_braket_result is not None:
        labels.append("Real Hardware")
        results.append(real_hardware_braket_result)

    for result, label in zip(results, labels, strict=True):
        fast_energies = result.total_energy_per_macro_iteration_with_initial_energy_and_final_energy.values
        energy_errors = result.total_energy_per_macro_iteration_with_initial_energy_and_final_energy.errors
        fast_convergence.append((fast_energies, energy_errors, label))

    plot_energies(
        fast_convergence=fast_convergence,
        show=True,
    )

We plot the VQE convergence with the error bars by calling the plotting function defined above.

Running the Example

After saving the script as run_estimators_and_samplers.py, execute:

$ python run_estimators_and_samplers.py

You will see the cost estimates printed to the console:

Example terminal output
$ python run_estimators_and_samplers.py
Total number of shots used in estimator 470600000
Estimated cost in dollars for Estimator on Devices.Rigetti.Ankaa3:  424951.7999999651
Total number of shots used in sampler 100000
Estimated cost in dollars for Sampler on Devices.Rigetti.Ankaa3:  92.99999999999999

The estimated cost is from 14 October 2025, so they are most likely out of date.

You will see the VQE convergence plot saved as dist/vqe_convergence.png:

FAST-VQE convergence plot with noisy estimators