Hamiltonian Time Evolution

The TimeEvolution program performs Hamiltonian time evolution simulation — simulating real-time quantum dynamics under a given Hamiltonian. Divi supports multiple Trotterization techniques out of the box: ExactTrotterization (full Trotter-Suzuki decomposition) and QDrift (randomized term sampling for shallower circuits on large Hamiltonians).

It supports three output modes:

  • Probability mode: te.results contains measured basis-state probabilities (dict[str, float]).

  • Observable mode: te.results contains the estimated expectation value (float).

  • Multi-observable mode: te.results contains one expectation value per observable (list[float]), measured from a single shared evolution.

Basic Usage

Use probability mode when you want a final-state distribution:

import math
import pennylane as qp
from divi.backends import MaestroSimulator
from divi.qprog import TimeEvolution

backend = MaestroSimulator(shots=5000)

te = TimeEvolution(
    hamiltonian=qp.PauliX(0) + qp.PauliX(1),
    time=math.pi / 2,
    backend=backend,
)
te.run()

probs = te.results
print(probs)
print(f"Circuits executed: {te.total_circuit_count}")

Observable Mode

Provide observable=... to estimate expectation values after evolution:

import pennylane as qp
from divi.backends import MaestroSimulator
from divi.qprog import TimeEvolution

backend = MaestroSimulator(shots=5000)

te = TimeEvolution(
    hamiltonian=qp.PauliX(0) + qp.PauliZ(0),
    time=0.6,
    n_steps=8,
    observable=qp.PauliZ(0),
    backend=backend,
)
te.run()
print(f"<Z0> = {te.results:.6f}")

Tip

On sampling backends, pass shot_distribution="weighted" to focus the shot budget on the observable’s dominant terms. See Adaptive Shot Allocation for the full list of strategies.

Multi-Observable Mode

Pass a list (or tuple) to observable= to estimate several expectation values from a single evolution. te.results becomes a list[float] in the same order as the input observables, and the cost is shared across them:

import math
import pennylane as qp
from divi.backends import MaestroSimulator
from divi.qprog import TimeEvolution

backend = MaestroSimulator(shots=5000)

te = TimeEvolution(
    hamiltonian=qp.PauliX(0) + qp.PauliX(1),
    time=math.pi / 2,
    observable=[qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(0) @ qp.PauliZ(1)],
    backend=backend,
)
te.run()

z0, z1, zz = te.results
print(f"<Z0>   = {z0:+.4f}")
print(f"<Z1>   = {z1:+.4f}")
print(f"<Z0Z1> = {zz:+.4f}")

Two cost-saving mechanisms apply automatically:

  • Shared evolution: the e^{-iHt} body is built and executed once per measurement group, not once per observable.

  • Cross-observable QWC grouping: MeasurementStage unions the Pauli terms of every observable and groups commuting terms into shared shot batches. In the example above all three observables are diagonal in the Z basis, so they collapse into a single measurement group.

When a quantum error mitigation protocol is set, the savings extend further; see Multi-Observable Programs.

QDrift Trotterization

For large Hamiltonians, you can use QDrift to sample terms and average over multiple Hamiltonian samples:

import pennylane as qp
from divi.backends import MaestroSimulator
from divi.hamiltonians import QDrift
from divi.qprog import SuperpositionState, TimeEvolution

backend = MaestroSimulator(shots=5000)
qdrift = QDrift(
    keep_fraction=0.5,
    sampling_budget=2,
    n_hamiltonians_per_iteration=4,
    sampling_strategy="weighted",
    seed=7,
)

te = TimeEvolution(
    hamiltonian=0.7 * qp.PauliZ(0) + 0.4 * qp.PauliX(0) + 0.2 * qp.PauliZ(1),
    trotterization_strategy=qdrift,
    time=0.8,
    initial_state=SuperpositionState(),
    backend=backend,
)
te.run()
print(te.results)

Note

Multi-sample QDrift with observable on an expectation-value backend is currently not supported. Use a shot-based backend or run in probability mode for multi-sample averaging.

Time Evolution Trajectory

Use TimeEvolutionTrajectory to simulate dynamics at multiple time points in parallel. It creates one TimeEvolution program per time point, runs them via ProgramEnsemble (with optional circuit batching), and collects results into a time-ordered mapping.

import math
import numpy as np
import pennylane as qp
from divi.backends import MaestroSimulator
from divi.qprog import TimeEvolutionTrajectory

backend = MaestroSimulator(shots=5000)

trajectory = TimeEvolutionTrajectory(
    hamiltonian=qp.PauliX(0),
    time_points=np.linspace(0.01, math.pi, 20).tolist(),
    observable=qp.PauliZ(0),
    backend=backend,
)
trajectory.create_programs()
trajectory.run(blocking=True)

results = trajectory.aggregate_results()
# results: {0.01: 0.9996, 0.166: 0.944, ..., 3.14: 0.998}

trajectory.visualize_results()   # plots ⟨Z⟩ vs time

The trajectory supports all the same options as TimeEvolution (trotterization_strategy, n_steps, order, initial_state).

Batching across time points

Each time point becomes its own program in a ProgramEnsemble, so the trajectory inherits the ensemble’s circuit-batching machinery. The default BatchConfig() already merges every program’s submission into a single backend call:

from divi.qprog import BatchConfig

# Default — single merged call for all time points
trajectory.run(blocking=True, batch_config=BatchConfig())

For dense trajectories (e.g. 512 time points) on QoroService, set max_concurrent_programs=-1 so all time points run concurrently and the entire ensemble submits as one cloud job, reducing overhead significantly:

trajectory.run(
    blocking=True,
    batch_config=BatchConfig(max_concurrent_programs=-1),
)

See Circuit Batching for the full batching reference.

Next Steps