PennyLane
Install
Install

Related materials

  • Related contentHow to estimate the resource requirements of quantum algorithms
  • Related contentQubit and gate trade-offs in qubitized Quantum Phase Estimation

Contents

  1. The Vibronic Hamiltonian
    1. Defining the Hamiltonian
  2. Constructing Circuits for Single Time-Step
  3. Conclusions
  4. References
  5. About the author

Downloads

  • Download Python script
  • Download Notebook
  • View on GitHub
  1. Demos/
  2. Algorithms/
  3. Quantifying Resource Requirements for Vibronic Dynamics Simulation

Quantifying Resource Requirements for Vibronic Dynamics Simulation

Diksha Dhawan

Diksha Dhawan

Published: May 05, 2024. Last updated: September 22, 2025.

Standard quantum chemistry often relies on the adiabatic Born-Oppenheimer approximation, assuming nuclei move on a single potential energy surface (PES) well-separated from others. But in the world of photochemistry, where molecules absorb light, and break bonds, this approximation breaks down. The timescales of electronic and nuclear motion become comparable, giving rise to vibronic coupling. This coupling, drives critical processes like photosynthesis, and solar cell efficiency and accurately simulating this requires a Hamiltonian that treats the dynamics of electrons and nuclei simultaneously.

However, modeling such dynamics is computationally demanding. Before we attempt to run such complex algorithms on future fault-tolerant hardware, we need to answer a fundamental question: Is this algorithm actually feasible?

In this demo, we assume the role of an algorithm architect. We will construct a simulation workflow for the vibronic Hamiltonian from the ground up, utilizing the modular building blocks within PennyLane’s estimator. By constructing this pipeline ourselves, we can perform a feasibility analysis, determining exactly what resources are required to simulate these complex non-adiabatic processes on future hardware.

../../_images/long_image.png

The Vibronic Hamiltonian

To perform this feasibility check, we must first define the system. We use the vibronic coupling Hamiltonian, which describes a set of \(N\) electronic states interacting with \(M\) vibrational modes.

Unlike standard electronic structure problems, this model mixes discrete electronic levels with continuous vibrational motion, and the Hamiltonian takes the form:

\[H = T + V,\]

where \(T\) represents the kinetic energy of the nuclei, and is diagonal in the electronic subspace while \(V\) contains off-diagonal electronic couplings.

To simulate the time evolution \(U(t) = e^{-iHt}\), we employ a product formula (Trotterization) approach as outlined in D. Motlagh et al. [1]. A key challenge in Trotterization is decomposing the Hamiltonian efficiently. Here, we use the fragmentation scheme proposed in the reference, grouping the terms \(\ket{j} \bra{i} \otimes V_{ji}\) such that they differ by a fixed bitstring \(m\). The original grouping method results in N different fragments, which can be viewed as blocks of the potential energy matrix as shown in Figure:1. Each of these fragments can then be block-diagonalized by using only Clifford gates and implemented as a sequence of evolutions controlled by the corresponding electronic states.

../../_images/vibronic_fragments.png

Figure 1: Fragmentation of the Vibronic Hamiltonian into N blocks based on bitstring.¶

Defining the Hamiltonian

With the algorithmic approach defined, the next step is to instantiate the system we wish to benchmark.

While access to the full Hamiltonian coefficients would allow us to further optimize costs by leveraging the commutativity of electronic parts, we can still derive a reliable baseline using only structural parameters. By defining the number of modes, electronic states, and grid size, we can map out the cost topology of a real system without needing to generate and store the full Hamiltonian.

As a case study, we select (NO):math:_4-Anth, a molecule created by introducing four N-oxyl radical fragments to anthracene. Proposed for its theoretically record-breaking singlet fission speed [2], we use this system to define our simulation parameters:

num_modes = 19  # Number of vibrational modes
num_states = 5  # Number of electronic states
k_grid = 4  # Number of qubits per mode (discretization)
taylor_degree = 2  # Truncate to Quadratic Vibronic Coupling

In our model, we truncate the interaction terms for potential energy fragment to linear and quadratic terms only. This approximation, known as the Quadratic Vibronic Coupling (QVC) model, captures the dominant physical effects while simplifying the potential energy circuits significantly.

Constructing Circuits for Single Time-Step

Next step is to define what the circuit will look like for evolving a single time step. Based on the term-based fragmentation scheme, the single Trotter step is composed of two distinct types of quantum circuits interleaved together:

  1. Kinetic Energy Fragment: This implements the nuclear kinetic energy. Since the kinetic operator depends on momentum \(P\), this circuit uses the Quantum Fourier Transform (QFT) to switch to the momentum basis, applies a phase rotation, and then switches back.

  2. Potential Energy Fragments: These implement the interaction terms. For each fragment, the algorithm loads coefficients using a QROM (Quantum Read-Only Memory), computes the vibrational monomial product using quantum arithmetic, and applies a phase gradient.

Let’s first see what the circuit will look for the kinetic energy fragment:

import pennylane.estimator as qre


def kinetic_circuit(mode_wires, phase_wires, scratch_wires, coeff_wires):
    ops = []
    num_phase_wires = len(phase_wires)
    k_grid = len(mode_wires[0])
    for mode in range(num_modes):
        ops.append(qre.AQFT(order=1, num_wires=k_grid, wires=mode_wires[mode]))

    for i in range(num_modes):
        ops.append(
            qre.OutOfPlaceSquare(
                register_size=k_grid, wires=scratch_wires + mode_wires[i]
            )
        )

        for j in range(2 * k_grid):
            ctrl_wire = [scratch_wires[j]]
            target_wires = (
                coeff_wires[: len(phase_wires) - j]
                + phase_wires[: len(phase_wires) - j]
            )
            ops.append(
                qre.Controlled(
                    qre.SemiAdder(max_register_size=num_phase_wires - j),
                    num_ctrl_wires=1,
                    num_zero_ctrl=0,
                    wires=target_wires + ctrl_wire,
                )
            )

        ops.append(
            qre.Adjoint(
                qre.OutOfPlaceSquare(
                    register_size=k_grid, wires=scratch_wires + mode_wires[i]
                )
            )
        )

    for mode in range(num_modes):
        ops.append(
            qre.Adjoint(
                qre.AQFT(order=1, num_wires=k_grid, wires=mode_wires[mode])
            )
        )

    return qre.Prod(ops)

Similarly, we can define the structure for the potential energy fragments. For a QVC model truncated to quadratic terms, each fragment will implement a monomial of degree up to 2

\[V_{ji} = \sum_{r} c_{r} Q_{r} + \sum_{r} \tilde{c}_{r} Q_{r}^2,\]

where \(c_r\) and \(\tilde{c}_{r}\) are the linear and quadratic coupling coefficients respectively. The exponential of each term is implemented by the modular circuit shown in Figure 2.

../../_images/circuit_diagram.png

Figure 2: Circuit for implementing the exponential of a monomial term in the potential energy fragment.¶

The circuit executes the following steps:

  • Loading the coefficients from QROM controlled by the electronic state.

  • Computing the monomial product by using a square operation for quadratic terms.

  • Accumulating the phase by decomposing the multiplication into a series of controlled adders that add the product directly onto the phase gradient register.

  • Uncomputing the intermediate steps to clean up ancilla wires.

We can define this circuit using two different segments, one for linear terms and one for quadratic terms:

def linear_circuit(num_states, elec_wires, phase_wires, coeff_wires, scratch_wires):
    ops = []
    ops.append(
        qre.QROM(
            num_bitstrings=num_states,
            size_bitstring=len(phase_wires),
            restored=False,
            wires=elec_wires + coeff_wires,
        )
    )

    for i in range(k_grid):
        ctrl_wire = [scratch_wires[i]]
        target_wires = (
            coeff_wires[: len(phase_wires) - i] + phase_wires[: len(phase_wires) - i]
        )
        ops.append(
            qre.Controlled(
                qre.SemiAdder(max_register_size=len(phase_wires) - i),
                num_ctrl_wires=1,
                num_zero_ctrl=0,
                wires=target_wires + ctrl_wire,
            )
        )

    ops.append(
        qre.Adjoint(
            qre.QROM(
                num_bitstrings=num_states,
                size_bitstring=len(phase_wires),
                restored=False,
                wires=elec_wires + coeff_wires,
            )
        )
    )
    return qre.Prod(ops)


def quadratic_circuit(
    num_states, elec_wires, phase_wires, coeff_wires, mode_wires, scratch_wires
):
    ops = []
    k_grid = len(mode_wires)
    ops.append(
        qre.QROM(
            num_bitstrings=num_states,
            size_bitstring=len(phase_wires),
            restored=False,
            wires=elec_wires + coeff_wires,
        )
    )

    qre.OutOfPlaceSquare(register_size=k_grid, wires=mode_wires + scratch_wires)
    for i in range(2 * k_grid):
        ctrl_wire = [scratch_wires[i]]
        target_wires = (
            coeff_wires[: len(phase_wires) - i] + phase_wires[: len(phase_wires) - i]
        )
        ops.append(
            qre.Controlled(
                qre.SemiAdder(max_register_size=len(phase_wires) - i),
                num_ctrl_wires=1,
                num_zero_ctrl=0,
                wires=target_wires + ctrl_wire,
            )
        )

    ops.append(
        qre.Adjoint(
            qre.OutOfPlaceSquare(
                register_size=k_grid, wires=mode_wires + scratch_wires
            )
        )
    )
    ops.append(
        qre.Adjoint(
            qre.QROM(
                num_bitstrings=num_states,
                size_bitstring=len(phase_wires),
                restored=False,
                wires=elec_wires + coeff_wires,
            )
        )
    )
    return qre.Prod(ops)

Finally, to ensure precise resource tracking, we explicitly label our wire registers. This avoids ambiguity about which circuit operations are mapped to which quantum registers and ensures the resource estimator captures the full width of the circuit.

def get_wire_labels(num_modes, num_states, k_grid, phase_prec):
    """Generates the wire map for the full system."""
    num_elec_qubits = (num_states - 1).bit_length()
    elec_wires = [
        f"e_{i}" for i in range(num_elec_qubits)
    ]  # Electronic State Register

    phase_wires = [
        f"pg_{i}" for i in range(phase_prec)
    ]  # Resource State For Phase Gradients
    coeff_wires = [f"c_{i}" for i in range(phase_prec)]  # Coefficient Register

    mode_wires = []
    for m in range(num_modes):
        mode_wires.append([f"m{m}_{w}" for w in range(k_grid)])  # Mode m Register

    scratch_wires = [
        f"s_{i}" for i in range(2 * k_grid)
    ]  # Scratch Space for Arithmetic

    return elec_wires, phase_wires, coeff_wires, mode_wires, scratch_wires

We now define the full circuit by assembling our fragments into a second-order Suzuki-Trotter expansion

To construct the expansion, we loop over all fragments: the kinetic fragment is added once, while the number of potential fragments is determined by the size of the electronic register. This ensures that every block of the Hamiltonian identified in our fragmentation scheme (Figure 1) is accounted for in the simulation.

Additionally, an important part of the algorithm is the preparation of phase gradient register. The size of this register is dictated by the desired precision for phase rotations in both the potential and kinetic steps, directly influencing the simulation’s accuracy.

import math
def circuit(num_modes, num_states, k_grid, taylor_degree, num_steps, phase_grad_prec=1e-6):
    fragments = []
    phase_grad_wires = int(math.ceil(math.log2(1 / phase_grad_prec)))
    elec_wires, phase_wires, coeff_wires, mode_wires, scratch_wires = (
        get_wire_labels(num_modes, num_states, k_grid, phase_grad_wires)
    )
    qre.PhaseGradient(num_wires=len(phase_wires), wires=phase_wires) # Prepare Phase Gradient State
    kinetic_fragment = kinetic_circuit(
        mode_wires, phase_wires, scratch_wires, coeff_wires
    )
    fragments.append(kinetic_fragment) # Add Kinetic Fragment

    num_fragments = 2**len(elec_wires)
    for i in range(num_fragments):
        frag_op = []
        for mode in range(num_modes):
            if taylor_degree >= 1:
                frag_op.append(
                    linear_circuit(
                        num_states, elec_wires, phase_wires, coeff_wires, scratch_wires
                    )
                )
            if taylor_degree >= 2:
                frag_op.append(
                    quadratic_circuit(
                        num_states,
                        elec_wires,
                        phase_wires,
                        coeff_wires,
                        mode_wires[mode],
                        scratch_wires,
                    )
                )
        fragments.append(qre.Prod(frag_op))

    qre.TrotterProduct(first_order_expansion=fragments, num_steps=num_steps, order=2)

Finally, we can estimate the resource requirements for this full circuit using the estimate function. To ensure the simulation reaches a total time of 100 fs with sufficient accuracy, we set the number of Trotter steps to 500. This value is chosen based on benchmark data from Motlagh et al. [1], where this step count was shown to maintain reliable accuracy for a 100 fs simulation of the (NO)$_4$-Anth system.

print(qre.estimate(circuit)(num_modes, num_states, k_grid, taylor_degree, num_steps=500))
--- Resources: ---
 Total wires: 146
   algorithmic wires: 127
   allocated wires: 19
     zero state: 19
     any state: 0
 Total gates : 5.210E+8
   'Toffoli': 6.082E+7,
   'T': 749,
   'CNOT': 2.712E+8,
   'X': 4.560E+6,
   'Z': 1,
   'S': 1,
   'Hadamard': 1.844E+8

We observe that while our results follow the same scaling and orders of magnitude reported in Motlagh et al. [1], the Toffoli gate count is slightly higher. The higher gate counts in this estimation occur because we assume a dense Hamiltonian, whereas the reference work leverages system-specific sparsity by only implementing non-zero coupling terms. These numbers can therefore be viewed as a reliable upper bound for the cost of simulating (NO):math:_4-Anth dynamics.

Conclusions

By constructing the full simulation workflow for vibronic dynamics from the ground up, we have gained critical insights into the resource requirements for simulating these complex processes on future quantum hardware. This approach allows us to break down complex algorithms into manageable building blocks, providing a transparent view of how algorithmic choices directly impact gate counts and qubit overhead. This demonstration highlights the power of modular quantum algorithm design, enabling researchers to build, analyze, and optimize complex workflows for next-generation quantum simulations. Ultimately, this framework is designed to be an open sandbox for researchers. We encourage you to use the building blocks provided here to explore alternative algorithms tailored to your specific research interests.

References

[1] (1,2,3)

Danial Motlagh et al., “Quantum Algorithm for Vibronic Dynamics: Case Study on Singlet Fission Solar Cell Design”. arXiv preprint arXiv:2411.13669 (2014).

[2]

E. Pradhan et al., “Design of the Smallest Intramolecular Singlet Fission Chromophore with the Fastest Singlet Fission”. Journal of Physical Chemistry Letters 13.48 (2022).

About the author

Diksha Dhawan
Diksha Dhawan

Diksha Dhawan

Developing Tools to Simulate Chemistry Using Quantum Computers

Total running time of the script: (0 minutes 0.192 seconds)

Share demo

Ask a question on the forum

Related Demos

How to estimate the resource requirements of quantum algorithms

Qubit and gate trade-offs in qubitized Quantum Phase Estimation

PennyLane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Built by researchers, for research. Created with ❤️ by Xanadu.

Research

  • Research
  • Performance
  • Hardware & Simulators
  • Demos
  • Quantum Compilation
  • Quantum Datasets

Education

  • Teach
  • Learn
  • Codebook
  • Coding Challenges
  • Videos
  • Glossary

Software

  • Install PennyLane
  • Features
  • Documentation
  • Catalyst Compilation Docs
  • Development Guide
  • API
  • GitHub
Stay updated with our newsletter

© Copyright 2026 | Xanadu | All rights reserved

TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.

Privacy Policy|Terms of Service|Cookie Policy|Code of Conduct