Performing a Projection Based Wavefunction-in-DFT Embedding Study

In this example, we plot the FAST Variational Quantum Eigensolver (FAST-VQE) calculation result as a function of the number of active orbitals.

The theory behind projection-based embedding is described in the Projection-Based Wavefunction-in-DFT Embedding.

Full Script

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

  1"""Demonstrate Projection based wavefunction in DFT embedding (Projective-Embedding) on malonaldehyde."""
  2# pyright: reportUnknownVariableType=false, reportMissingImports=false
  3# ruff: noqa
  4
  5import logging
  6import time
  7from pathlib import Path
  8
  9import matplotlib.pyplot as plt
 10import numpy as np
 11
 12import qrunch as qc
 13
 14qc.setup_logger(logging.INFO)
 15
 16
 17def plot_energy_and_time(
 18    active_spaces: list[int],
 19    energies: list[float],
 20    durations: list[float],
 21    *,
 22    outdir: Path = Path("dist"),
 23    show: bool = False,
 24) -> None:
 25    """
 26    Create and save plots for energy and runtime vs. active space size.
 27
 28    Args:
 29        active_spaces: List of active spatial orbital counts.
 30        energies: Total energies (Hartree) corresponding to active_spaces.
 31        durations: Wall-clock durations in seconds for each run.
 32        outdir: Output directory for images and CSV.
 33        show: Whether to display the figures interactively after saving.
 34
 35    """
 36    outdir.mkdir(parents=True, exist_ok=True)
 37
 38    x = np.asarray(active_spaces, dtype=float)
 39    e = np.asarray(energies, dtype=float)
 40    t = np.asarray(durations, dtype=float)
 41
 42    # Plot 1: Energy (Hartree) vs active space
 43    fig1 = plt.figure(figsize=(6.0, 4.0))
 44    plt.plot(x, e, "-o", label="Total energy (Hartree)", color="darkgreen")
 45    for xi, yi in zip(x, e, strict=True):
 46        plt.annotate(f"{yi:.6f}", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
 47    plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
 48    plt.ylabel("Total energy [Hartree]")
 49    plt.title("FAST-VQE-in-DFT - Energy vs. active space")
 50    plt.grid(visible=True, alpha=0.3)
 51    plt.tight_layout()
 52    energy_png = outdir / "energy_vs_active_space.png"
 53    fig1.savefig(energy_png, dpi=200)
 54
 55    # Plot 2: Time (s) vs active space
 56    fig2 = plt.figure(figsize=(6.0, 4.0))
 57    plt.plot(x, t, "-o", label="Wall time (s)", color="darkgreen")
 58    for xi, yi in zip(x, t, strict=True):
 59        plt.annotate(f"{yi:.2f}s", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
 60    plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
 61    plt.ylabel("Wall time [s]")
 62    plt.title("FAST-VQE-in-DFT - Runtime vs. active space")
 63    plt.grid(visible=True, alpha=0.3)
 64    plt.tight_layout()
 65    time_png = outdir / "time_vs_active_space.png"
 66    fig2.savefig(time_png, dpi=200)
 67
 68    if show:
 69        plt.show()
 70    else:
 71        # Close figures to free memory when running in batch contexts.
 72        plt.close(fig1)
 73        plt.close(fig2)
 74
 75
 76
 77
 78def main(max_number_of_active_spatial_orbitals: int = 15, data_path: Path | None = None) -> list[float]:
 79    """Run FAST-VQE on LiH as the first script."""
 80
 81    if data_path is None:
 82        directory = Path.cwd() / "data"
 83    else:
 84        directory = data_path
 85
 86    cube_generator = (
 87        qc
 88        .cube_file_generator_creator()  # Start creating a cube file generator
 89        .projective_embedding()  # Narrow: pick Projective Embedding cube file generator
 90        .with_base_file_path(  # Configure to use a specific base file path
 91            path=Path("data")  # Directory where cube files will be saved
 92        )
 93        .with_embedding_components(  # Configure which components to generate
 94            components=["total"]  # Generate total density cube files for the embedded region.
 95        )
 96        .with_environment_components(  # Configure which components to generate
 97            components=["alpha"]  # Generate alpha density cube files for the environment region.
 98        )
 99        .with_overwriting_policy(  # Configure the file overwriting policy
100            policy="rename"  # Rename files if filename already exist
101        )
102        .create()  # Create the cube file generator instance
103    )
104
105    # Build molecular configuration for the transition state of the
106    # malonaldehyde proton transfer reaction.
107    # We embed the two oxygen atoms and the hydrogen atom
108    # involved in the proton transfer reaction.
109    # The geometry was grabbed from the supplementary materials of the paper:
110    # Tikhonov D. Accurate Tunneling Splittings for Proton Transfer in Malonaldehyde
111    # and Formic Acid Dimer Using the One-Dimensional Schrödinger Equation.
112    # ChemRxiv. 2021; doi:10.26434/chemrxiv.14394080.v1
113    # Corresponding to
114    # MA/b3lyp-d3-def2-nvp/def-svp/scan/120pm
115
116    molecular_configuration = qc.build_molecular_configuration(
117        molecule=[
118            ("C", -0.00915141819825, 1.12354123465448, 0.00106040539915),
119            ("C", 1.15788296226156, 0.34671243078737, 0.00000758720919),
120            ("C", -1.22030979400856, 0.41042248215324, 0.00013737820145),
121            ("O", 1.12051212277652, -0.92879932667030, -0.00173637506493),
122            ("O", -1.25284335513586, -0.86184055825304, -0.00162501071734),
123            ("H", -0.06011523023948, -1.14355253030268, -0.00207685087878),
124            ("H", 0.02269677013415, 2.21233410585677, 0.00263410783148),
125            ("H", 2.15529643150533, 0.82074207219421, 0.00068827649219),
126            ("H", -2.18850348909542, 0.94364908957994, 0.00091048152759),
127        ],
128        basis_set="cc-pvdz",
129        spin_difference=0,
130        charge=0,
131        units="angstrom",
132        embedded_atoms=[3, 4, 5],  # Indices of the atoms to be treated with high-level method (0-based indexing)
133    )
134
135    adaptive_vqe_options = qc.options.IterativeVqeOptions(
136        max_iterations=100,  # Increase the maximum number of iterations
137    )
138
139    # Build the user-configured FAST-VQE calculator
140    fast_vqe_calculator = (
141        qc
142        .calculator_creator()  # Start creating a calculator
143        .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
144        .iterative()  # Use the iterative VQE
145        .standard()  # Narrow: pick the standard iterative VQE (FAST-VQE)
146        .with_options(adaptive_vqe_options)  # Use the user-defined iterative VQE options
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        .choose_data_persister_manager()  # Start sub-choice: Choose the data persister manager
150        .file_persister(  # Perform the sub-choice selection - Here we pick file persister
151            directory=directory,  # Directory where data files will be saved
152            extension=".qdk",  # File extension for the data files
153            do_save=True,  # Whether to save data to files
154            load_policy="fallback",  # Load data if available, otherwise compute
155            overwriting_policy="overwrite",  # Overwrite if filename already exist
156            padding_width=3,  # Padding width for file numbering (001, 002, ...)
157        )
158        .create()  # Create the calculator instance
159    )
160
161    energies: list[float] = []
162    active_spaces: list[int] = []
163    durations: list[float] = []
164    for number_of_active_spatial_orbitals in range(6, max_number_of_active_spatial_orbitals, 2):
165        # Build the ground state problem.
166        problem_builder = (
167            qc
168            .problem_builder_creator()  # Start creating a problem builder
169            .ground_state()  # Narrow: pick ground state problem
170            .projective_embedding()  # Narrow: pick Projective Embedding ground state problem
171            .choose_full_system_solver()  # Start sub-choice: Choose the full system solver
172            .dft(  # Perform the sub-choice selection - Here we pick DFT as the full system solver
173                options=qc.options.DftCalculatorOptions(
174                    functional="b3lyp",  # The exchange-correlation functional (default is "b3lyp")
175                    convergence_tolerance=1e-9,  # Energy convergence tolerance
176                    verbose=10,  # Set the SCF verbosity level
177                )
178            )
179            .choose_localizer()  # Start sub-choice: Choose the orbital localizer
180            .pipek_mezey(  # Perform the sub-choice selection - Here we pick Pipek-Mezey localizer
181                population_method="meta-lowdin",
182                options=qc.options.LocalizationOptions(
183                    max_iterations=20,  # The max. number of iterations in each macro iteration.
184                    convergence_tolerance=1e-6,  # Converge threshold.
185                ),
186            )
187            .choose_orbital_assigner()  # Start sub-choice: Choose the orbital assigner
188            .total_weight(  # Perform the sub-choice selection - Here we pick total weight orbital assigner
189                assignment_tolerance="kmeans_midpoint"  # Cluster weights into two clusters using a minimal k-means.
190            )
191            .choose_embedded_orbital_calculator()  # Start sub-choice: Choose the embedded orbital calculator
192            .moller_plesset_2(  # Perform the sub-choice selection - Here we pick MP2 embedded orbital calculator.
193                options=qc.options.MollerPlesset2CalculatorOptions(
194                    verbose=10,  # Set the verbosity level
195                    frozen_virtual_threshold=100.0,  # Disregard virtual orbitals with large occupation numbers.
196                )
197            )
198            .choose_projector_builder()  # Start sub-choice: Choose the projector builder
199            .manby(  # Perform the sub-choice selection - Here we pick Manby's level shift projector
200                level_shift_parameter=1e5  # The level shift parameter
201            )
202            .choose_data_persister_manager()  # Start sub-choice: Choose the data persister manager
203            .file_persister(  # Perform the sub-choice selection - Here we pick file persister
204                directory=directory,  # Directory where data files will be saved
205                extension=".qdk",  # File extension for the data files
206                do_save=True,  # Whether to save data to files
207                load_policy="fallback",  # Load data if available, otherwise compute
208                overwriting_policy="rename",  # Rename files if filename already exist
209                padding_width=3,  # Padding width for file numbering (001, 002, ...)
210            )
211            .with_cube_generator(
212                cube_generator=cube_generator,
213            )
214            .add_problem_modifier()  # Add a problem modifier
215            .active_space(  # Specify the problem modifier to add - Here we pick the active space modifier
216                number_of_active_spatial_orbitals=number_of_active_spatial_orbitals,
217                number_of_active_alpha_electrons=number_of_active_spatial_orbitals
218                // 2,  # Number of active alpha electrons
219            )
220            .create()  # Create the problem builder instance
221        )
222        ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
223
224        start = time.perf_counter()
225        result = fast_vqe_calculator.calculate(ground_state_problem)
226        end = time.perf_counter()
227
228        energies.append(result.total_energy.value)
229        active_spaces.append(number_of_active_spatial_orbitals)
230        durations.append(float(end - start))
231
232    plot_energy_and_time(
233        energies=energies,
234        active_spaces=active_spaces,
235        durations=durations,
236        show=True,
237    )
238
239    return energies
240
241
242if __name__ == "__main__":
243    main()

Explanation

  1. Import Kvantify Qrunch

    import logging
    import time
    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 minor versions. In addition, we use matplotlib, numpy, time, logging, and Path (from pathlib.

    In this example, we import time to measure the execution time of different parts of the code, and logging to provide informative messages during execution.

  2. Logging

    qc.setup_logger(logging.INFO)
    

    In this code snippet, we set up logging to provide informative messages during execution. This is particularly useful for tracking the complex workflow of the projection-based embedding process. Other logging levels include DEBUG, CRITICAL. See Using the Logger for more details.

  3. Plotting

    def plot_energy_and_time(
        active_spaces: list[int],
        energies: list[float],
        durations: list[float],
        *,
        outdir: Path = Path("dist"),
        show: bool = False,
    ) -> None:
        """
        Create and save plots for energy and runtime vs. active space size.
    
        Args:
            active_spaces: List of active spatial orbital counts.
            energies: Total energies (Hartree) corresponding to active_spaces.
            durations: Wall-clock durations in seconds for each run.
            outdir: Output directory for images and CSV.
            show: Whether to display the figures interactively after saving.
    
        """
        outdir.mkdir(parents=True, exist_ok=True)
    
        x = np.asarray(active_spaces, dtype=float)
        e = np.asarray(energies, dtype=float)
        t = np.asarray(durations, dtype=float)
    
        # Plot 1: Energy (Hartree) vs active space
        fig1 = plt.figure(figsize=(6.0, 4.0))
        plt.plot(x, e, "-o", label="Total energy (Hartree)", color="darkgreen")
        for xi, yi in zip(x, e, strict=True):
            plt.annotate(f"{yi:.6f}", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
        plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
        plt.ylabel("Total energy [Hartree]")
        plt.title("FAST-VQE-in-DFT - Energy vs. active space")
        plt.grid(visible=True, alpha=0.3)
        plt.tight_layout()
        energy_png = outdir / "energy_vs_active_space.png"
        fig1.savefig(energy_png, dpi=200)
    
        # Plot 2: Time (s) vs active space
        fig2 = plt.figure(figsize=(6.0, 4.0))
        plt.plot(x, t, "-o", label="Wall time (s)", color="darkgreen")
        for xi, yi in zip(x, t, strict=True):
            plt.annotate(f"{yi:.2f}s", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
        plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
        plt.ylabel("Wall time [s]")
        plt.title("FAST-VQE-in-DFT - Runtime vs. active space")
        plt.grid(visible=True, alpha=0.3)
        plt.tight_layout()
        time_png = outdir / "time_vs_active_space.png"
        fig2.savefig(time_png, dpi=200)
    
        if show:
            plt.show()
        else:
            # Close figures to free memory when running in batch contexts.
            plt.close(fig1)
            plt.close(fig2)
    
    
    

    In this code snippet, we define a function to plot the results of the projection-based embedding calculation.

    This include the FAST-VQE-in-DFT energies as a function of the number of active orbitals, and the execution time as a function of the number of active orbitals.

  4. Cube file generation

        cube_generator = (
            qc
            .cube_file_generator_creator()  # Start creating a cube file generator
            .projective_embedding()  # Narrow: pick Projective Embedding cube file generator
            .with_base_file_path(  # Configure to use a specific base file path
                path=Path("data")  # Directory where cube files will be saved
            )
            .with_embedding_components(  # Configure which components to generate
                components=["total"]  # Generate total density cube files for the embedded region.
            )
            .with_environment_components(  # Configure which components to generate
                components=["alpha"]  # Generate alpha density cube files for the environment region.
            )
            .with_overwriting_policy(  # Configure the file overwriting policy
                policy="rename"  # Rename files if filename already exist
            )
            .create()  # Create the cube file generator instance
        )
    

    This code snippet sets up a cube file generator to generate cube files for the electron density.

    This may be useful for visualizing the electron density of the embedded region and the environment.

  5. Molecular configuration

        # Build molecular configuration for the transition state of the
        # malonaldehyde proton transfer reaction.
        # We embed the two oxygen atoms and the hydrogen atom
        # involved in the proton transfer reaction.
        # The geometry was grabbed from the supplementary materials of the paper:
        # Tikhonov D. Accurate Tunneling Splittings for Proton Transfer in Malonaldehyde
        # and Formic Acid Dimer Using the One-Dimensional Schrödinger Equation.
        # ChemRxiv. 2021; doi:10.26434/chemrxiv.14394080.v1
        # Corresponding to
        # MA/b3lyp-d3-def2-nvp/def-svp/scan/120pm
    
        molecular_configuration = qc.build_molecular_configuration(
            molecule=[
                ("C", -0.00915141819825, 1.12354123465448, 0.00106040539915),
                ("C", 1.15788296226156, 0.34671243078737, 0.00000758720919),
                ("C", -1.22030979400856, 0.41042248215324, 0.00013737820145),
                ("O", 1.12051212277652, -0.92879932667030, -0.00173637506493),
                ("O", -1.25284335513586, -0.86184055825304, -0.00162501071734),
                ("H", -0.06011523023948, -1.14355253030268, -0.00207685087878),
                ("H", 0.02269677013415, 2.21233410585677, 0.00263410783148),
                ("H", 2.15529643150533, 0.82074207219421, 0.00068827649219),
                ("H", -2.18850348909542, 0.94364908957994, 0.00091048152759),
            ],
            basis_set="cc-pvdz",
            spin_difference=0,
            charge=0,
            units="angstrom",
            embedded_atoms=[3, 4, 5],  # Indices of the atoms to be treated with high-level method (0-based indexing)
        )
    

    In this code snippet, we define the molecular configuration for the transition state of the Malonaldehyde proton transfer reaction. We set the molecular geometry, basis set, charge, and spin difference.

    Specially for the projection-based embedding, we also define the embedded atoms (via embedded_atoms), which are the atoms that belong to the embedded region.

    In this case, the two oxygen atoms and the hydrogen atom involved in the proton transfer reaction.

    We use indexing starting from 0 for the atoms. So 3 corresponds to the 4th atom in the XYZ file. That is, the first oxygen atom.

  1. The calculator

        adaptive_vqe_options = qc.options.IterativeVqeOptions(
            max_iterations=100,  # Increase the maximum number of iterations
        )
    
        # Build the user-configured FAST-VQE calculator
        fast_vqe_calculator = (
            qc
            .calculator_creator()  # Start creating a calculator
            .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
            .iterative()  # Use 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
            .choose_data_persister_manager()  # Start sub-choice: Choose the data persister manager
            .file_persister(  # Perform the sub-choice selection - Here we pick file persister
                directory=directory,  # Directory where data files will be saved
                extension=".qdk",  # File extension for the data files
                do_save=True,  # Whether to save data to files
                load_policy="fallback",  # Load data if available, otherwise compute
                overwriting_policy="overwrite",  # Overwrite if filename already exist
                padding_width=3,  # Padding width for file numbering (001, 002, ...)
            )
            .create()  # Create the calculator instance
        )
    

    We build the FAST-VQE calculator with options suitable for projection-based embedding.

    Here we set a maximum of 100 iterations, and we use a file_persister, in case we want to restart the calculation later.

  2. The problem builder

            # Build the ground state problem.
            problem_builder = (
                qc
                .problem_builder_creator()  # Start creating a problem builder
                .ground_state()  # Narrow: pick ground state problem
                .projective_embedding()  # Narrow: pick Projective Embedding ground state problem
                .choose_full_system_solver()  # Start sub-choice: Choose the full system solver
                .dft(  # Perform the sub-choice selection - Here we pick DFT as the full system solver
                    options=qc.options.DftCalculatorOptions(
                        functional="b3lyp",  # The exchange-correlation functional (default is "b3lyp")
                        convergence_tolerance=1e-9,  # Energy convergence tolerance
                        verbose=10,  # Set the SCF verbosity level
                    )
                )
                .choose_localizer()  # Start sub-choice: Choose the orbital localizer
                .pipek_mezey(  # Perform the sub-choice selection - Here we pick Pipek-Mezey localizer
                    population_method="meta-lowdin",
                    options=qc.options.LocalizationOptions(
                        max_iterations=20,  # The max. number of iterations in each macro iteration.
                        convergence_tolerance=1e-6,  # Converge threshold.
                    ),
                )
                .choose_orbital_assigner()  # Start sub-choice: Choose the orbital assigner
                .total_weight(  # Perform the sub-choice selection - Here we pick total weight orbital assigner
                    assignment_tolerance="kmeans_midpoint"  # Cluster weights into two clusters using a minimal k-means.
                )
                .choose_embedded_orbital_calculator()  # Start sub-choice: Choose the embedded orbital calculator
                .moller_plesset_2(  # Perform the sub-choice selection - Here we pick MP2 embedded orbital calculator.
                    options=qc.options.MollerPlesset2CalculatorOptions(
                        verbose=10,  # Set the verbosity level
                        frozen_virtual_threshold=100.0,  # Disregard virtual orbitals with large occupation numbers.
                    )
                )
                .choose_projector_builder()  # Start sub-choice: Choose the projector builder
                .manby(  # Perform the sub-choice selection - Here we pick Manby's level shift projector
                    level_shift_parameter=1e5  # The level shift parameter
                )
                .choose_data_persister_manager()  # Start sub-choice: Choose the data persister manager
                .file_persister(  # Perform the sub-choice selection - Here we pick file persister
                    directory=directory,  # Directory where data files will be saved
                    extension=".qdk",  # File extension for the data files
                    do_save=True,  # Whether to save data to files
                    load_policy="fallback",  # Load data if available, otherwise compute
                    overwriting_policy="rename",  # Rename files if filename already exist
                    padding_width=3,  # Padding width for file numbering (001, 002, ...)
                )
                .with_cube_generator(
                    cube_generator=cube_generator,
                )
                .add_problem_modifier()  # Add a problem modifier
                .active_space(  # Specify the problem modifier to add - Here we pick the active space modifier
                    number_of_active_spatial_orbitals=number_of_active_spatial_orbitals,
                    number_of_active_alpha_electrons=number_of_active_spatial_orbitals
                    // 2,  # Number of active alpha electrons
                )
                .create()  # Create the problem builder instance
            )
            ground_state_problem = problem_builder.build_unrestricted(molecular_configuration)
    

    We build the projection-based embedding problem builder. This problem builder has more options than the standard problem builder, as it needs more to handle the embedding process.

    The example shows some common options, but all these are optional. You can easily use

    problem_builder = (
       qc.problem_builder_creator()  # Start creating a problem builder
       .ground_state()               # Narrow: pick ground state problem
       .projective_embedding()       # Narrow: pick Projective Embedding ground state problem
       .create()                     # Create the problem builder instance
    )
    

    However, as a teaching exercise, we show how to set some of the more common options.

    First, we specify that the full system solver is DFT, and we use the B3LYP functional. We also specify the convergence tolerance and the verbosity.

    Next, we specify the orbital localization method. Here, we use the Pipek-Mezey method, which is a common choice for projection-based embedding.

    Once the occupied orbitals are localized, they must be assigned to either the embedded region or the environment. This is done by specifying the orbital assigner.

    In this example, we use the total_weight assigner, which assigns orbitals based on the total weight of the orbital on the embedded atoms. We use a clustering algorithm for the assignment.

    In the space of the local occupied orbitals and the virtual orbitals, we run a MP2 calculation as the embedded orbital calculator to obtain the natural orbitals in the embedded region, that will be used to run the FAST-VQE calculation. An alternative would be to use canonical Hartree-Fock (HF) orbitals. Again, we set the verbosity and also a frozen_virtual_threshold.

    In order to not mix the occupied orbitals of the embedded region and the environment, we use a projection operator. Here we use the manby projector.

    We also use a file persister, in case we want to restart the calculation later. This is useful, as the code will try to reuse as much data as possible from previous runs. So if you change the embedded orbital calculator from MP2 to HF, you do not need to redo the full-system DFT calculation, or the orbital localization.

    We include a cube file generator, to generate cube files for the electron density. This can be useful for visualizing the electron density of the embedded region and the environment.

    Finally, we define the active space sizes we want to run the FAST-VQE calculation for and create the instance, and, at last, build the unrestricted ground state problem.

  3. The FAST-VQE calculation

            start = time.perf_counter()
            result = fast_vqe_calculator.calculate(ground_state_problem)
            end = time.perf_counter()
    

    We run the FAST-VQE calculation, with timings.

  4. Make the plots

        # Plot the convergence and error relative to FCI reference.
        plot_energies(fast_energies, reference_energy=fci_result.total_energy.value, show=True)
    

    We plot the results of the projection-based embedding calculation.

Running the Example

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

$ python run_projective_embedding.py

You should see a lot of output like how the molecular orbitals are assigned to either the embedded region or the environment.

Note that the logging may change between different versions of Kvantify Qrunch.

Example terminal output
$ python run_estimators_and_samplers_example.py
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): TotalWeightBasedOrbitalAssigner:
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): MO index   Embedded weight      Environment weight   Assignment
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 0          0.948513             0.051487             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 1          0.948579             0.051421             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 2          0.019310             0.980690
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 3          0.019591             0.980409
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 4          0.006344             0.993656
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 5          0.500060             0.499940             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 6          0.494727             0.505273             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 7          0.074848             0.925152
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 8          0.099742             0.900258
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 9          0.718247             0.281753             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 10         0.104632             0.895368
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 11         0.075698             0.924302
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 12         0.099099             0.900901
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 13         0.634234             0.365766             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 14         0.151014             0.848986
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 15         0.569419             0.430581             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 16         0.633778             0.366222             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 17         0.717875             0.282125             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 18         0.573391             0.426609             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): Alternative assignment_tolerance values:
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): - largest_gap_midpoint    : 0.322870  -> embeds 10/19
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): TotalWeightBasedOrbitalAssigner:
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): MO index   Embedded weight      Environment weight   Assignment
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 0          0.948513             0.051487             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 1          0.948582             0.051418             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 2          0.019310             0.980690
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 3          0.019591             0.980409
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 4          0.006344             0.993656
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 5          0.500059             0.499941             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 6          0.494728             0.505272             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 7          0.074847             0.925153
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 8          0.099742             0.900258
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 9          0.718247             0.281753             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 10         0.104631             0.895369
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 11         0.075698             0.924302
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 12         0.099099             0.900901
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 13         0.634235             0.365765             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 14         0.151014             0.848986
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 15         0.569419             0.430581             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 16         0.633778             0.366222             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 17         0.717870             0.282130             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): 18         0.573392             0.426608             Embedded
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): Alternative assignment_tolerance values:
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): - largest_gap_midpoint    : 0.322871  -> embeds 10/19
Example terminal output
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): Starting active space problem modification.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): Active space Inactive energy = -130.62088772937943 Hartree
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch): Active space modification completed resulting in:
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of active orbitals = 16.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of active alpha electrons = 8.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of active beta electrons = 8.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of inactive orbitals = 45.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of inactive alpha electrons = 2.
2025-10-14 19:50:53 [INFO]  (kvantify.qrunch):     Number of inactive beta electrons = 2.

You should see 2 plots:

FAST-VQE convergence as a function of active space FAST-VQE timing plot

You should also see the requested cube files generated:

  • data_embedding_total.cube

  • data_environment_alpha.cube

There should also be a number of restart files in the data directory.

Re-Running the Example

Re-running the example

$ python run_projective_embedding.py

You should experience that the calculation is much faster and with logging you will find lines like:

Example terminal output
2025-10-14 20:46:10 [INFO]  (kvantify.qrunch): Data loaded from persister for key full_system_mean_field_result.
2025-10-14 20:46:10 [INFO]  (kvantify.qrunch): Data loaded from persister for key local_molecular_orbitals.
2025-10-14 20:46:10 [INFO]  (kvantify.qrunch): Loaded the ground state CAS modified problem.
2025-10-14 20:46:11 [INFO]  (kvantify.qrunch): Loaded adaptive VQE results.

Indicating that it found and reused data from the previous run. Naturally, in that case the timings will be much shorter as it does not need to actually do the FAST-VQE calculation again.