Performing a Standard FAST-VQE Convergence Study

In this example, we plot the convergence of a FAST Variational Quantum Eigensolver (FAST-VQE) calculation.

Full Script

The script below shows the full script, which is then explained section by section.

  1"""Demonstrate FAST-VQE convergence plotting on LiH molecule."""
  2# pyright: reportUnknownVariableType=false, reportMissingImports=false
  3# ruff: noqa
  4
  5from pathlib import Path
  6
  7import matplotlib.pyplot as plt
  8import numpy as np
  9
 10import qrunch as qc
 11
 12
 13def plot_energies(
 14    fast_energies: list[float],
 15    reference_energy: float,
 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_energies: Sequence of total energies per VQE iteration.
 25        reference_energy: Reference total energy (e.g., FCI) to compare against.
 26        outdir: Output directory where plots are written. Defaults to "dist".
 27        show: Whether to display the figures interactively after saving.
 28    """
 29    # Convert to a NumPy array for safe numeric operations and validation.
 30    energies = np.asarray(list(fast_energies), dtype=float)
 31
 32    # Basic validation to help catch silent failures early.
 33    if energies.size == 0:
 34        msg = "fast_energies must contain at least one value."
 35        raise ValueError(msg)
 36    if not np.isfinite(energies).all():
 37        msg = "fast_energies contains non-finite values."
 38        raise ValueError(msg)
 39    if not np.isfinite(reference_energy):
 40        msg = "reference_energy must be finite."
 41        raise ValueError(msg)
 42
 43    # Prepare x-axis as 1-based iteration indices for readability.
 44    iterations = np.arange(1, energies.size + 1, dtype=int)
 45
 46    # Ensure output directory exists.
 47    outdir.mkdir(parents=True, exist_ok=True)
 48
 49    # -----------------------------
 50    # Figure 1: Energies vs Iteration
 51    # -----------------------------
 52    fig1 = plt.figure()
 53    plt.plot(iterations, energies, "-*", color="darkgreen", label="FAST-VQE")  # points to visualize steps
 54    plt.axhline(reference_energy, linestyle="--", color="black", label="Reference")
 55    plt.xlabel("Iteration number")
 56    plt.ylabel("Total energy [Hartree]")
 57    plt.title("FAST-VQE convergence")
 58    plt.legend()
 59    plt.tight_layout()
 60
 61    convergence_plot = outdir / "vqe_convergence.png"
 62    fig1.savefig(convergence_plot, dpi=200, bbox_inches="tight")
 63
 64    # -----------------------------
 65    # Figure 2: |Energy - Reference| (log scale)
 66    # -----------------------------
 67    # Use absolute error; add a tiny epsilon to avoid log(0) if we hit the reference exactly.
 68    eps = np.finfo(float).eps
 69    abs_error = np.abs(energies - reference_energy) + eps
 70
 71    fig2 = plt.figure()
 72    plt.semilogy(iterations, abs_error, "-*", color="darkgreen", label="|E - E_ref|")
 73    plt.xlabel("Iteration number")
 74    plt.ylabel("Absolute error [Hartree]")
 75    plt.title("FAST-VQE error vs reference")
 76    plt.legend()
 77    plt.tight_layout()
 78
 79    error_convergence_plot = outdir / "vqe_error_semilogy.png"
 80    fig2.savefig(error_convergence_plot, dpi=200, bbox_inches="tight")
 81
 82    if show:
 83        plt.show()
 84    else:
 85        # Close figures to free memory when running in batch contexts.
 86        plt.close(fig1)
 87        plt.close(fig2)
 88
 89
 90
 91
 92def main() -> list[float]:
 93    """Run FAST-VQE on LiH as the first script."""
 94    path_to_molecule_xyz_file = Path().absolute() / "qrunch/demo_scripts/lih.xyz"
 95    # Build Lithium Hydride (LiH) molecular configuration from xyz file.
 96    molecular_configuration = qc.build_molecular_configuration(
 97        molecule=Path(path_to_molecule_xyz_file),
 98        basis_set="sto3g",
 99        spin_difference=0,
100        charge=0,
101        units="angstrom",
102    )
103
104    # Build the ground state problem.
105    problem_builder = qc.problem_builder_creator().ground_state().standard().create()
106    ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
107
108    adaptive_vqe_options = qc.options.IterativeVqeOptions(
109        max_iterations=100,  # Increase the maximum number of iterations
110        gates_per_iteration=1,  # Add one gate per iteration (Recommended)
111        force_all_iterations=True,  # Force all iterations to run, independent of convergence.
112    )
113
114    # Build the user-configured VQE calculator instance
115    fast_vqe_calculator = (
116        qc
117        .calculator_creator()  # Start creating a calculator
118        .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
119        .iterative()  # Narrow: pick the iterative VQE
120        .standard()  # Narrow: pick the standard iterative VQE (FAST-VQE)
121        .with_options(adaptive_vqe_options)  # Use the user-defined iterative VQE options
122        .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
123        .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
124        .create()  # Create the calculator instance
125    )
126
127    result = fast_vqe_calculator.calculate(ground_state_problem)
128
129    # We can get a nice timings report.
130    print(qc.get_execution_times_report())  # noqa: T201
131
132    # Extract FAST-VQE energies for plotting
133    fast_energies = result.total_energy_per_macro_iteration_with_initial_energy_and_final_energy.values
134
135    # Build a Full Configuration Interaction (FCI) calculator
136    full_configuration_interaction_calculator = (
137        qc
138        .calculator_creator()  # Start creating a calculator
139        .configuration_interaction()  # Narrow: pick Configuration Interaction (CI)
140        .standard()  # Narrow: pick standard CI (not paired version)
141        .create()  # Create the calculator instance
142    )
143
144    # Calculate the FCI result for reference energy
145    fci_result = full_configuration_interaction_calculator.calculate(ground_state_problem)
146
147    # Plot the convergence and error relative to FCI reference.
148    plot_energies(fast_energies, reference_energy=fci_result.total_energy.value, show=True)
149
150    return fast_energies
151
152
153if __name__ == "__main__":
154    main()

The script requires an XYZ file specifying the molecule: Download lih.xyz

Explanation

  1. Import Kvantify Qrunch

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

    This code snippet loads the python packages we need. The public and stable Kvantify Qrunch API is loaded as import qrunch as qc. This is the only import you need for most tasks, and the only one that is guaranteed to be supported across versions. In addition, we use matplotlib, numpy, and Path (from pathlib).

  2. The plotting method

    def plot_energies(
        fast_energies: list[float],
        reference_energy: float,
        *,
        outdir: Path = Path("dist"),
        show: bool = False,
    ) -> None:
        """
        Plot VQE energy convergence and log-scale error vs reference energy.
    
        Args:
            fast_energies: Sequence of total energies per VQE iteration.
            reference_energy: Reference total energy (e.g., FCI) to compare against.
            outdir: Output directory where plots are written. Defaults to "dist".
            show: Whether to display the figures interactively after saving.
        """
        # Convert to a NumPy array for safe numeric operations and validation.
        energies = np.asarray(list(fast_energies), dtype=float)
    
        # Basic validation to help catch silent failures early.
        if energies.size == 0:
            msg = "fast_energies must contain at least one value."
            raise ValueError(msg)
        if not np.isfinite(energies).all():
            msg = "fast_energies contains non-finite values."
            raise ValueError(msg)
        if not np.isfinite(reference_energy):
            msg = "reference_energy must be finite."
            raise ValueError(msg)
    
        # Prepare x-axis as 1-based iteration indices for readability.
        iterations = np.arange(1, energies.size + 1, dtype=int)
    
        # Ensure output directory exists.
        outdir.mkdir(parents=True, exist_ok=True)
    
        # -----------------------------
        # Figure 1: Energies vs Iteration
        # -----------------------------
        fig1 = plt.figure()
        plt.plot(iterations, energies, "-*", color="darkgreen", label="FAST-VQE")  # points to visualize steps
        plt.axhline(reference_energy, linestyle="--", color="black", label="Reference")
        plt.xlabel("Iteration number")
        plt.ylabel("Total energy [Hartree]")
        plt.title("FAST-VQE convergence")
        plt.legend()
        plt.tight_layout()
    
        convergence_plot = outdir / "vqe_convergence.png"
        fig1.savefig(convergence_plot, dpi=200, bbox_inches="tight")
    
        # -----------------------------
        # Figure 2: |Energy - Reference| (log scale)
        # -----------------------------
        # Use absolute error; add a tiny epsilon to avoid log(0) if we hit the reference exactly.
        eps = np.finfo(float).eps
        abs_error = np.abs(energies - reference_energy) + eps
    
        fig2 = plt.figure()
        plt.semilogy(iterations, abs_error, "-*", color="darkgreen", label="|E - E_ref|")
        plt.xlabel("Iteration number")
        plt.ylabel("Absolute error [Hartree]")
        plt.title("FAST-VQE error vs reference")
        plt.legend()
        plt.tight_layout()
    
        error_convergence_plot = outdir / "vqe_error_semilogy.png"
        fig2.savefig(error_convergence_plot, dpi=200, bbox_inches="tight")
    
        if show:
            plt.show()
        else:
            # Close figures to free memory when running in batch contexts.
            plt.close(fig1)
            plt.close(fig2)
    
    
    

    This code plot the convergence of the VQE energy and the error relative to the reference full configuration interaction (FCI) energy.

    The code generates two PNG files under dist/:

    • vqe_convergence.png

    • vqe_error_semilogy.png

  3. Molecule from XYZ file

        # Build Lithium Hydride (LiH) molecular configuration from xyz file.
        molecular_configuration = qc.build_molecular_configuration(
            molecule=Path(path_to_molecule_xyz_file),
            basis_set="sto3g",
            spin_difference=0,
            charge=0,
            units="angstrom",
        )
    

    We load LiH from an XYZ file, set the STO-3G basis, neutral charge, zero spin difference, and specify that the unit is ångström.

  4. Build the ground-state problem

        # Build the ground state problem.
        problem_builder = qc.problem_builder_creator().ground_state().standard().create()
        ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
    

    We create a standard ground-state problem using the problem builder. Here we use the unrestricted variant for LiH (spin up and spin down electrons are allowed to occupy different spatial orbitals).

  5. Configure and create the FAST-VQE calculator

        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
        )
    

    We specify that we want a maximum of 100 iterations, one gate per iteration, and we force all iterations to be executed, independent of any stopping criteria.

    We then build the adaptive VQE calculator with the specified options, where we choose a quick default gate parameter optimization method, optimizing only the parameter of the last gate in every iteration.

  6. Run and extract energies

        result = fast_vqe_calculator.calculate(ground_state_problem)
    

    We run the VQE calculation, and here we also print a timing report for profiling:

        # We can get a nice timings report.
        print(qc.get_execution_times_report())  # noqa: T201
    
  7. Compute a high-accuracy FCI reference

        # Build a Full Configuration Interaction (FCI) calculator
        full_configuration_interaction_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator
            .configuration_interaction()  # Narrow: pick Configuration Interaction (CI)
            .standard()  # Narrow: pick standard CI (not paired version)
            .create()  # Create the calculator instance
        )
    
        # Calculate the FCI result for reference energy
        fci_result = full_configuration_interaction_calculator.calculate(ground_state_problem)
    

    We build a FCI calculator and calculate the FCI energy for the same problem.

  8. Make the plots

        # Build a Full Configuration Interaction (FCI) calculator
        full_configuration_interaction_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator
            .configuration_interaction()  # Narrow: pick Configuration Interaction (CI)
            .standard()  # Narrow: pick standard CI (not paired version)
            .create()  # Create the calculator instance
        )
    
        # Calculate the FCI result for reference energy
        fci_result = full_configuration_interaction_calculator.calculate(ground_state_problem)
    

    We plot the VQE convergence and the error relative to the FCI reference, by calling the plotting function defined above.

Running the Example

After saving the script as run_vqe_convergence.py, you can run it directly from the command line:

$ python run_vqe_convergence.py

You should see 2 plots:

FAST-VQE convergence plot FAST-VQE convergence plot, as the error relative to FCI