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# ruff: noqa # ruff: ignore[noqa-comments]
  3
  4import logging
  5import time
  6from pathlib import Path
  7
  8import matplotlib.pyplot as plt
  9import numpy as np
 10
 11import qrunch as qc
 12
 13qc.setup_logger(logging.INFO)
 14
 15
 16def plot_energy_and_time(
 17    active_spaces: list[int],
 18    energies: list[float],
 19    durations: list[float],
 20    *,
 21    outdir: Path = Path("dist"),
 22    show: bool = False,
 23) -> None:
 24    """
 25    Create and save plots for energy and runtime vs. active space size.
 26
 27    Args:
 28        active_spaces: List of active spatial orbital counts.
 29        energies: Total energies (Hartree) corresponding to active_spaces.
 30        durations: Wall-clock durations in seconds for each run.
 31        outdir: Output directory for images and CSV.
 32        show: Whether to display the figures interactively after saving.
 33
 34    """
 35    outdir.mkdir(parents=True, exist_ok=True)
 36
 37    x = np.asarray(active_spaces, dtype=float)
 38    e = np.asarray(energies, dtype=float)
 39    t = np.asarray(durations, dtype=float)
 40
 41    # Plot 1: Energy (Hartree) vs active space
 42    fig1 = plt.figure(figsize=(6.0, 4.0))
 43    plt.plot(x, e, "-o", label="Total energy (Hartree)", color="darkgreen")
 44    for xi, yi in zip(x, e, strict=True):
 45        plt.annotate(f"{yi:.6f}", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
 46    plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
 47    plt.ylabel("Total energy [Hartree]")
 48    plt.title("FAST-VQE-in-DFT - Energy vs. active space")
 49    plt.grid(visible=True, alpha=0.3)
 50    plt.tight_layout()
 51    energy_png = outdir / "energy_vs_active_space.png"
 52    fig1.savefig(energy_png, dpi=200)
 53
 54    # Plot 2: Time (s) vs active space
 55    fig2 = plt.figure(figsize=(6.0, 4.0))
 56    plt.plot(x, t, "-o", label="Wall time (s)", color="darkgreen")
 57    for xi, yi in zip(x, t, strict=True):
 58        plt.annotate(f"{yi:.2f}s", (xi, yi), textcoords="offset points", xytext=(0, 6), ha="center")
 59    plt.xlabel("Number of active spatial orbitals (number of qubits are 2 times the number of active spatial orbitals)")
 60    plt.ylabel("Wall time [s]")
 61    plt.title("FAST-VQE-in-DFT - Runtime vs. active space")
 62    plt.grid(visible=True, alpha=0.3)
 63    plt.tight_layout()
 64    time_png = outdir / "time_vs_active_space.png"
 65    fig2.savefig(time_png, dpi=200)
 66
 67    if show:
 68        plt.show()
 69    else:
 70        # Close figures to free memory when running in batch contexts.
 71        plt.close(fig1)
 72        plt.close(fig2)
 73
 74
 75
 76
 77def main(max_number_of_active_spatial_orbitals: int = 15, data_path: Path | None = None) -> list[float]:
 78    """Run FAST-VQE on LiH as the first script."""
 79
 80    if data_path is None:
 81        directory = Path.cwd() / "data"
 82    else:
 83        directory = data_path
 84
 85    cube_generator = (
 86        qc
 87        .cube_file_generator_creator()  # Start creating a cube file generator
 88        .projective_embedding()  # Narrow: pick Projective Embedding cube file generator
 89        .with_base_file_path(  # Configure to use a specific base file path
 90            path=Path("data")  # Directory where cube files will be saved
 91        )
 92        .with_embedding_components(  # Configure which components to generate
 93            components=["total"]  # Generate total density cube files for the embedded region.
 94        )
 95        .with_environment_components(  # Configure which components to generate
 96            components=["alpha"]  # Generate alpha density cube files for the environment region.
 97        )
 98        .with_overwriting_policy(  # Configure the file overwriting policy
 99            policy="rename"  # Rename files if filename already exist
100        )
101        .create()  # Create the cube file generator instance
102    )
103
104    # Build molecular configuration for the transition state of the
105    # malonaldehyde proton transfer reaction.
106    # We embed the two oxygen atoms and the hydrogen atom
107    # involved in the proton transfer reaction.
108    # The geometry was grabbed from the supplementary materials of the paper:
109    # Tikhonov D. Accurate Tunneling Splittings for Proton Transfer in Malonaldehyde
110    # and Formic Acid Dimer Using the One-Dimensional Schrödinger Equation.
111    # ChemRxiv. 2021; doi:10.26434/chemrxiv.14394080.v1
112    # Corresponding to
113    # MA/b3lyp-d3-def2-nvp/def-svp/scan/120pm
114
115    molecular_configuration = qc.build_molecular_configuration(
116        molecule=[
117            ("C", -0.00915141819825, 1.12354123465448, 0.00106040539915),
118            ("C", 1.15788296226156, 0.34671243078737, 0.00000758720919),
119            ("C", -1.22030979400856, 0.41042248215324, 0.00013737820145),
120            ("O", 1.12051212277652, -0.92879932667030, -0.00173637506493),
121            ("O", -1.25284335513586, -0.86184055825304, -0.00162501071734),
122            ("H", -0.06011523023948, -1.14355253030268, -0.00207685087878),
123            ("H", 0.02269677013415, 2.21233410585677, 0.00263410783148),
124            ("H", 2.15529643150533, 0.82074207219421, 0.00068827649219),
125            ("H", -2.18850348909542, 0.94364908957994, 0.00091048152759),
126        ],
127        basis_set="cc-pvdz",
128        spin_difference=0,
129        charge=0,
130        units="angstrom",
131        embedded_atoms=[3, 4, 5],  # Indices of the atoms to be treated with high-level method (0-based indexing)
132    )
133
134    adaptive_vqe_options = qc.options.IterativeVqeOptions(
135        max_iterations=100,  # Increase the maximum number of iterations
136    )
137
138    # Build the user-configured FAST-VQE calculator
139    fast_vqe_calculator = (
140        qc
141        .calculator_creator()  # Start creating a calculator
142        .vqe()  # Narrow: pick Variational quantum eigensolver (VQE)
143        .iterative()  # Use the iterative VQE
144        .standard()  # Narrow: pick the standard iterative VQE (FAST-VQE)
145        .with_options(adaptive_vqe_options)  # Use the user-defined iterative VQE options
146        .choose_minimizer()  # Start sub-choice: Choose the gate parameter minimizer
147        .quick_default()  # Perform the sub-choice selection - Here we pick the greedy and quick minimizer
148        .choose_data_persister_manager()  # Start sub-choice: Choose the data persister manager
149        .file_persister(  # Perform the sub-choice selection - Here we pick file persister
150            directory=directory,  # Directory where data files will be saved
151            extension=".qdk",  # File extension for the data files
152            do_save=True,  # Whether to save data to files
153            load_policy="fallback",  # Load data if available, otherwise compute
154            overwriting_policy="overwrite",  # Overwrite if filename already exist
155            padding_width=3,  # Padding width for file numbering (001, 002, ...)
156        )
157        .create()  # Create the calculator instance
158    )
159
160    energies: list[float] = []
161    active_spaces: list[int] = []
162    durations: list[float] = []
163    for number_of_active_spatial_orbitals in range(6, max_number_of_active_spatial_orbitals, 2):
164        # Build the ground state problem.
165        problem_builder = (
166            qc
167            .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 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.

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

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

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

  9. Make the plots

        plot_energy_and_time(
            energies=energies,
            active_spaces=active_spaces,
            durations=durations,
            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_projective_embedding.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.