Calculate the Ground State Energy Using the FAST-VQE

Goal

Run a FAST-VQE calculation for a prepared ground-state problem and obtain the ground-state energy.

Prerequisites

Steps

  1. Build the default FAST-VQE calculator

    The calculator_creator() is a fluent builder (see Understanding and Using Kvantify Qrunch’s Fluent Builder Pattern) that configures and returns a GroundStateProblemCalculator. By default, .vqe().iterative().standard() builds a FAST-VQE calculator with conservative defaults to avoid lengthy runs on real hardware (see Running FAST-VQE on Quantum Computers for details on how to perform a cost analysis before running on hardware)

        fast_vqe_calculator = qc.calculator_creator().vqe().iterative().standard().create()
    
  2. Use the default FAST-VQE calculator to calculate the ground state energy

    You can now calculate the result of an existing ground_state_problem:

        result = fast_vqe_calculator.calculate(ground_state_problem)
    

    The result is an GroundStateProblemCalculatorResult object that contains:

    • Electronic energy of the active electrons

    • Electronic energy of all electrons

    • Total molecular energy

    • Total electronic energy per macro iteration (list of values for each VQE iteration)

    The total_energy field is an ExpectationValue:

    print(f"Total energy = {result.total_energy:.6f}")
    
  3. Customize the VQE settings

    The most common setting to adjust is max_iterations in IterativeVqeOptions. The default is 5 iterations, which is is often insufficient for convergence but it is intentionally low to avoid long, costly runs on quantum hardware. It is also often useful to require that the VQE runs all iterations up to the maximum specified value.

        adaptive_vqe_options = qc.options.IterativeVqeOptions(
            max_iterations=100,  # Increase the maximum number of iterations
            gates_per_iteration=1,  # Add one gate per iteration (Recommended)
            force_all_iterations=True,  # Force all iterations to run, independent of convergence.
        )
    
        # Build the user-configured VQE calculator instance
        fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator
            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
            .iterative()  # Narrow: pick the iterative VQE
            .standard()  # Narrow: pick the standard iterative VQE (FAST-VQE)
            .with_options(adaptive_vqe_options)  # Use the user-defined iterative VQE options
            .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
            .create()  # Create the calculator instance
        )
    

    See the API reference for IterativeVqeOptions for the full list of options.

    You can configure additional options before calling .create():

    • Set the estimator:

      .with_estimator(my_estimator)
      

      Setting an estimator to be used for energy estimation and parameter optimization. See Create an Estimator for more details.

    • Set number of measurement shots:

      .with_estimator_shots(1000)
      

      Setting the number of shots to use in the estimator when doing parameter optimization. Setting to None uses an exact simulator evaluation.

    • Choose classical minimizer:

      .choose_minimizer()
      # .<pick-a-minimizer>(...)
      

      You can choose which classical minimizer you want to use to optimize the parameters. See Choose a Minimizer for available options.

    • Choose reminimizer:

      .choose_reminimizer()
      # .<pick-a-minimizer>(...)
      

      You can choose a classical minimizer if you want to re-optimize the parameters at the end of a VQE run. See Choose a Minimizer for available options.

    • Set stopping criteria:

      .choose_stopping_criterion()
      # .<pick-a-stopping-criterion>(...)
      

      You can choose the stopping criterion for the iterative VQE. See Choose a Stopping Criterion for available options.

    • Select a custom gate selector:

      .with_gate_selector(my_gate_selector)
      

      You can provide a custom gate selector to control which excitation gate is added at each iteration. See Create a FAST Gate Selector for how to build a gate_selector.

    • Configure data persistence:

      .choose_data_persister_manager()
      # .<pick-a-persister>(...)
      

      You can choose how and where to save/load intermediate data during the VQE run. See Choose a Data Persister Manager for available options.

    • Control inclusion of single excitations:

      .with_single_excitations(include=False)
      

      You can exclude the use of single excitation gates in the FAST-VQE ansatz.

    • Control inclusion of double excitations:

      .with_double_excitations(include=False)
      

      You can exclude the use of double excitation gates in the FAST-VQE ansatz.

OO-FAST-VQE (Orbital-Optimized FAST-VQE)

OO-FAST-VQE augments FAST-VQE with orbital optimization. During the adaptive loop an intermittent orbital optimizer keeps the orbital rotations close to the global minimum as gates are added. After the loop, an optional final orbital optimizer (potentially with basin-hopping) polishes the result. This may reduce the number of iterations needed for convergence, but at a significant increase in computational cost per iteration.

import qrunch as qc

adaptive_vqe_options = qc.options.IterativeVqeOptions(
    max_iterations=100,
)

fast_vqe_calculator = (
    qc.calculator_creator()
    .vqe()
    .iterative_with_orbital_optimization()
    .standard()
    .with_options(adaptive_vqe_options)
    .create()
)

The OO-FAST-VQE has all the same configuration options as FAST-VQE, and in addition you can also:

  • Define the intermittent orbital optimizer (runs during the adaptive loop):

    .with_orbital_optimizer(intermittent_optimizer)
    

    This optimizer runs at selected iterations during the adaptive VQE to keep orbital rotations small. Use loose convergence criteria here, as the gate parameters will shift on the next iteration. See Create an Orbital Optimizer for how to construct an optimizer.

    Note if you just want to select which estimator the orbital optimizer uses, you can use the with_orbital_optimizer.

  • Control the estimator of the intermittent orbital optimization:

    .with_orbital_optimizer_estimator(estimator)
    

    This is a simple way to specify the estimator to used for the orbital optimizer.

  • Control the estimator shots of the intermittent orbital optimization:

    .with_orbital_optimizer_estimator_shots(1000)
    

    This is a simple way to specify the number of estimator shots to used for the orbital optimizer.

  • Control when intermittent optimization runs:

    .with_intermittent_orbital_optimizer_options(
        qc.options.IntermittentOrbitalOptimizerAlgorithmOptions(
            every_nth_iteration=1,          # consider OO every iteration
            gradient_threshold=1e-2,        # below this: single Newton step instead of full OO
            skip_gradient_threshold=1e-16,  # below this: skip OO entirely
            orbital_change_threshold=1e-3,  # above this orbital change: force full OO
            force_full_after_n_non_full_steps=10,  # force full OO after 10 consecutive non-full steps
            gate_addition_threshold=1e-3,   # if gate addition changes energy above this: full OO
        )
    )
    

    The intermittent optimizer uses a three-tier strategy based on the orbital gradient norm:

    • Above gradient_threshold → a full converged orbital optimization is performed.

    • Between skip_gradient_threshold and gradient_threshold → a single Newton step is taken. This is roughly as cheap as computing the gradient norm but still updates the orbitals, preventing slow drift as new gates are added.

    • Below skip_gradient_threshold → the OO step is skipped entirely.

    Additionally, a full optimization is triggered when:

    • The orbital change from the previous step exceeds orbital_change_threshold.

    • The energy change due to a gate addition exceeds gate_addition_threshold.

    A forced full optimization after force_full_after_n_non_full_steps consecutive non-full decisions acts as a safety net. Set to 0 to disable.

    See IntermittentOrbitalOptimizerAlgorithmOptions for the full list of options.

    Alternatively, use the preset-based selection via the builder:

    .choose_intermittent_orbital_optimizer_options()
    .quick()             # or .balanced(), .accurate()
    

    Or use the class methods directly:

    .with_intermittent_orbital_optimizer_options(
        qc.options.IntermittentOrbitalOptimizerAlgorithmOptions.quick()
    )
    

    Available presets:

    • quick() — Always takes a single Newton step; never runs full OO.

    • balanced() — Single Newton step each iteration; full OO after 10 consecutive non-full steps or when a gate addition causes a large energy change.

    • accurate() — Full OO at nearly every iteration.

    See Create an Orbital Optimizer for details on each preset.

  • Define the final orbital optimizer (runs once after the adaptive loop):

    .with_final_orbital_optimizer(final_optimizer)
    

    This optimizer runs a single polishing step after the adaptive loop completes. It is recommended to use tight convergence and enable basin-hopping to especially when the intermittent optimizer was not used (setting every_nth_iteration larger than the maximum number of iterations and force_full_after_n_non_full_steps to 0).

    import qrunch as qc
    
    final_optimizer = (
        qc.orbital_optimizer_creator()
        .newton()
        .choose_gradient_calculator()
          .local_gradient(
              estimator=qc.estimator_creator().excitation_gate().create()
          )
        .with_shots(None)
        .with_options(qc.options.NewtonMinimizerOptions(
            relative_error_tolerance=1e-6,
        ))
        .with_basin_hopping_options(qc.options.OrbitalOptimizerBasinHoppingOptions(
            active=True,
        ))
        .create()
    )
    

    See Create an Orbital Optimizer for full details on basin-hopping and the distinction between intermittent and final optimizers.

  • Choose a reminimizer for final gate-parameter re-optimization:

    .choose_reminimizer()
    # .<pick-a-minimizer>(...)
    

    If set, all gate parameters are re-optimized (with orbital rotations locked) before the final orbital optimization step.

Note

choose_orbital_optimizer_stopping_criterion() is deprecated. Use .with_intermittent_orbital_optimizer_options(...) instead to control when the intermittent orbital optimizer runs.

ADAPT-VQE

To use ADAPT-VQE instead of FAST-VQE, replace the gate_selector with an ADAPT gate selector. See Create an ADAPT Gate Selector for how to build an ADAPT gate_selector.

Verify the Result

  • Check that the result.total_energy is correct

  • For simulator runs, repeated calculations should produce the same .value with .error close to zero, unless the simulator uses a finite number of shots

  • On hardware, .error will reflect shot noise from a finite number of measurements

See Also