Propagator

The Propagator algorithm in QDK/Chemistry converts a time-dependent Hamiltonian over a finite time interval into a single effective (time-independent) Hamiltonian. Following QDK/Chemistry’s algorithm design principles, it takes a TimeDependentQubitHamiltonian and a time interval as input and produces a QubitOperator as output.

Overview

Many quantum-chemistry workflows need to simulate how a system evolves under a Hamiltonian that changes with time — for example, a molecule driven by a laser pulse. The time-dependent nature of these systems makes them challenging to simulate directly. The standard approach is to:

  1. Divide the total evolution into short time steps.

  2. For each step, compute an effective Hamiltonian that approximates the time-dependent Hamiltonian over that interval.

  3. Feed that effective Hamiltonian into a time-evolution routine (a HamiltonianUnitaryBuilder) to produce the quantum circuit for that step.

Step 2 is what a propagator does. Given an interval \([t_1, t_2]\) and a time-dependent Hamiltonian \(H(t)\), the propagator returns a time-independent \(H_\text{eff}\) that best represents the evolution during that interval.

The propagator’s output is divided by the step length \(\delta t = t_2 - t_1\), so that the downstream unitary builder applies \(U(t_2, t_1) \approx \exp(-\mathrm{i}\,\delta t\,H_\text{eff})\). This convention keeps propagator and builder responsibilities strictly separated.

Typical workflow

A propagator is not usually called directly. Instead, an EvolutionCircuitBuilder (e.g., EulerEvolutionCircuitBuilder) creates one internally from its propagator setting and calls it once per time step. The typical sequence within each step is:

  1. The circuit builder passes the TimeDependentQubitHamiltonian and the current interval \([t_1, t_2]\) to the propagator

  2. The propagator returns an effective QubitOperator

  3. The HamiltonianUnitaryBuilder implements the effective evolution as a UnitaryRepresentation

  4. A CircuitMapper converts the unitary into executable gates

The EulerEvolutionCircuitBuilder orchestrates this loop for every time step and combines the per-step circuits into a single evolution circuit.

Using the Propagator

Note

This algorithm is currently available only in the Python API.

This section demonstrates how to create, configure, and use a propagator. Propagators are typically used as a nested algorithm within a HamiltonianSimulation, but can also be created and called independently.

Input requirements

The Propagator requires the following inputs:

TimeDependentQubitHamiltonian

A TimeDependentQubitHamiltonian describing how the Hamiltonian varies with time. Currently the only supported container type is DrivenContainer, which represents Hamiltonians of the form \(H(t) = H_0 + f(t)\,H_1\) where \(f(t)\) is a user-supplied drive function.

Time interval

Two floats t_start and t_end defining the interval over which the effective Hamiltonian is computed.

Creating a propagator

from qdk_chemistry.algorithms import create

propagator = create("propagator", "magnus")

Configuring settings

Settings vary by implementation. See Available implementations below for implementation-specific options.

propagator.settings().set("order", 1)

Running the propagator

import numpy as np
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import DrivenQubitHamiltonian, LatticeGraph
from qdk_chemistry.utils.model_hamiltonians import create_ising_hamiltonian

# 1. Build a driven Hamiltonian: H(t) = H0 + sin(2πt)·H1
lattice = LatticeGraph.chain(4)
h0 = create_ising_hamiltonian(lattice, j=1.0, h=0.0)  # ZZ coupling
h1 = create_ising_hamiltonian(lattice, j=0.0, h=0.5)  # Transverse X field
td_hamiltonian = DrivenQubitHamiltonian(h0, h1, drive=lambda t: np.sin(2 * np.pi * t))

# 2. Create the propagator and compute the effective Hamiltonian
propagator = create("propagator", "magnus")
h_eff = propagator.run(td_hamiltonian, t_start=0.0, t_end=0.1)

print(f"Effective Hamiltonian has {len(h_eff.pauli_strings)} Pauli terms")

When used as a nested algorithm inside an EvolutionCircuitBuilder, the propagator is configured via the propagator setting:

from qdk_chemistry.algorithms import create
from qdk_chemistry.data import AlgorithmRef

# Configure propagator as a nested algorithm inside an evolution circuit builder
propagator_ref = AlgorithmRef("propagator", "magnus", order=1)

euler_builder = create(
    "evolution_circuit_builder",
    "euler",
    propagator=propagator_ref,
    total_time=1.0,
    dt=0.1,
)

Available implementations

QDK/Chemistry’s Propagator provides a unified interface for computing effective Hamiltonians. You can discover available implementations programmatically:

from qdk_chemistry.algorithms import available

print(available("propagator"))

Time-averaged propagator

Factory name: "magnus"

This is the default (and currently only) propagator. It computes the time-averaged Hamiltonian over each interval. For a driven Hamiltonian \(H(t) = H_0 + f(t)\,H_1\) the result is:

\[H_\text{eff} = H_0 + \bar{f}\,H_1, \qquad \bar{f} = \frac{1}{\delta t}\int_{t_1}^{t_2} f(t')\,\mathrm{d}t'\]

where the drive integral is evaluated by numerical quadrature (scipy.integrate.quad).

This is the leading-order term of the Magnus expansion. For sufficiently smooth \(H(t)\), it has \(O(\delta t^3)\) local error and second-order global accuracy over a fixed evolution interval.

Settings

Setting

Type

Description

order

int

Expansion order. Default: 1. Only order 1 (time averaging) is currently implemented; higher values raise NotImplementedError.

Supported Hamiltonian types

Only DrivenContainer Hamiltonians (\(H_0 + f(t)\,H_1\)) are supported. Passing any other container type raises NotImplementedError.

Further reading