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

Explanation

  1. Import 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. import qrunch as qc loads the public and stable API. 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, time, logging, and Path.

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.

  1. Logging


qc.setup_logger(logging.INFO)

In this code snippet, we set up logging to provide INFO 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.

  1. 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.

  1. 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.

  1. 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, 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. 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.

  1. 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 have more options than the standard problem builder, as it needs 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 the 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 usefull, 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 build the ground state problem.

  1. 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.

  1. 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 alot 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 the QDK.

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

And the

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.