Skip to content

Hamiltonian evolution

Build Qiskit circuits for Hamiltonian-evolution steps of Pauli sums, exp(-i * time_step * sum_j c_j P_j), and convert between NWQLib's compact Pauli labels and Qiskit's Pauli strings. Import the records and functions from nwqlib.subroutines.hamiltonian_evolution.

import numpy as np
from scipy.linalg import expm
from qiskit.quantum_info import Operator, SparsePauliOp
from nwqlib.subroutines.hamiltonian_evolution import (
    PauliEvolutionBlock,
    PauliEvolutionTerm,
    build_pauli_evolution_circuit,
)

hopping = tuple(
    PauliEvolutionTerm(pauli=label, coefficient=0.4)
    for label in ("x0x1", "y0y1")
)
block = PauliEvolutionBlock(terms=hopping, time_step=0.5, kind="kinetic")
circuit = build_pauli_evolution_circuit([block], num_qubits=2)
print(dict(circuit.count_ops()))
H = Operator(SparsePauliOp(["XX", "YY"], [0.4, 0.4])).data
print(np.allclose(Operator(circuit).data, expm(-0.5j * H)))
{'xx_plus_yy': 1}
True

The block 0.4 (XX + YY) becomes one XXPlusYYGate, which is exact because XX and YY commute.

Conventions

  • A default-kind block with terms c_j P_j and duration time_step means exp(-i * time_step * sum_j c_j P_j), approximated by one application of the chosen product formula unless the terms commute. The pauli_evolution module text below describes the exact number-projector and XX + YY forms.
  • Compact labels such as x0z2 index qubits explicitly, in strictly increasing order, while Qiskit dense labels put qubit 0 in the rightmost character.

Pauli records and circuits

Records of Pauli terms and Hamiltonian-evolution blocks for circuit construction.

PauliEvolutionTerm

PauliEvolutionTerm(*, pauli: str, coefficient: complex)

One Pauli term c P of an evolution block, with P given by a compact label.

Build it with keyword arguments, for example PauliEvolutionTerm(pauli="x0z2", coefficient=0.5) for 0.5 X_0 Z_2. Both arguments are required.

Parameters:

  • pauli (str) –

    Compact indexed Pauli label such as "x0z2", with strictly increasing zero-based qubit indices, as parse_pauli_label accepts.

  • coefficient (complex) –

    Coefficient in the block generator. A Hermitian generator needs it real.

PauliEvolutionBlock

PauliEvolutionBlock(*, terms: tuple[PauliEvolutionTerm, ...], time_step: float, time: float | None = None, kind: str = 'hamiltonian_evolution', support: tuple[int, ...] | None = None, angle: float | None = None, metadata: Mapping[str, Any] = dict())

One Hamiltonian-evolution step exp(-i * time_step * sum_j c_j P_j) or one of its exact special forms.

Build it with keyword arguments, for example PauliEvolutionBlock(terms=(term,), time_step=0.1), and pass a sequence of blocks to build_pauli_evolution_circuit. terms and time_step are required. For the default kind the block is synthesized as one product-formula application in term order, which is exact when the terms commute. The "number_projector" and "kinetic" kinds have the exact forms that the Pauli-evolution builders describe.

Parameters:

  • terms (tuple[PauliEvolutionTerm, ...]) –

    Ordered Pauli terms of the generator.

  • time_step (float) –

    Duration multiplying the generator for this block.

  • time (float | None, default: None ) –

    Default None. Optional schedule time, distinct from the step duration.

  • kind (str, default: 'hamiltonian_evolution' ) –

    Default "hamiltonian_evolution". Block meaning: "hamiltonian_evolution", "number_projector" or "kinetic".

  • support (tuple[int, ...] | None, default: None ) –

    Default None. Qubits of a "number_projector" block.

  • angle (float | None, default: None ) –

    Default None. Angle of a "number_projector" block.

  • metadata (Mapping[str, Any], default: dict() ) –

    Default empty. Additional construction information. It does not describe a second circuit.

pauli_terms_to_dicts

pauli_terms_to_dicts(terms: tuple[PauliEvolutionTerm, ...]) -> list[dict[str, Any]]

Return the terms as {"pauli": label, "coefficient": c} records, the input of sparse_pauli_op_from_terms.

Qiskit circuit builders for compact Pauli-evolution blocks.

A block with generator G = sum_j c_j P_j and time_step dt approximates exp(-idtG) by Qiskit's PauliEvolutionGate with the selected Lie-Trotter or Suzuki synthesis, applied in term order. The result is exact when the terms commute. Two block kinds have exact special forms. A number_projector block applies exp(-iangle(prod_q n_q - I/2s)) on its support of s qubits, with n_q = (I - Z_q)/2. A kinetic block made of an XX and a YY term with equal real coefficient c on the same pair applies exp(-idtc*(XX + YY)) as one XXPlusYYGate, which is exact because XX and YY commute. The CX laws below mirror Qiskit's multi-controlled phase synthesis and NWQLib's diagonal synthesis for the pinned Qiskit version. They have no paper source and are checked against transpiled circuits by the QHD routing tests.

append_number_projector_phase

append_number_projector_phase(circuit: QuantumCircuit, support: Sequence[int], angle: float) -> None

Append exp(-i angle (prod n_q - I/2**s)) exactly.

The explicit global phase angle / 2**s removes the identity component I/2**s of the projector's Pauli expansion. The compiler separately records the omitted physical identity contribution. For the structured diagonal provider, an exact-zero angle emits no gate, and QHD omits zero-angle blocks before calling this builder or pricing them.

apply_pauli_rotation

apply_pauli_rotation(circuit: QuantumCircuit, pauli_label: str, angle: float, *, control: int | None = None) -> None

Append the Pauli rotation exp(-i angle P / 2) to a circuit, optionally controlled on a separate qubit.

The general case maps each X to Z with H and each Y to Z with S^dagger then H, collects the parity of the support onto its last qubit with a CX ladder, applies RZ(angle), and undoes the ladder and the basis change. RZ(angle) = exp(-i*angle*Z/2) carries no extra global phase, so replacing it by CRZ gives exactly the controlled rotation.

Parameters:

  • circuit (QuantumCircuit) –

    Circuit to append to, in place.

  • pauli_label (str) –

    Compact label of P, such as "x0z2".

  • angle (float) –

    Rotation angle.

  • control (int | None, default: None ) –

    Default None. Index of a control qubit that is not in the support of P.

Raises:

  • ValueError –

    If the control qubit is in the support or outside the circuit.

append_pauli_evolution_block

append_pauli_evolution_block(circuit: QuantumCircuit, block: PauliEvolutionBlock | Mapping[str, Any], *, evolution_synthesis: PauliEvolutionSynthesis = 'lie_trotter', evolution_synthesis_options: Mapping[str, Any] | None = None) -> None

Append the circuit of one Pauli-evolution block to circuit, in place.

A "number_projector" block uses its exact phase circuit, an exact c (XX + YY) "kinetic" block one XXPlusYYGate, and any other block one Qiskit PauliEvolutionGate on all qubits of the circuit.

Parameters:

  • circuit (QuantumCircuit) –

    Circuit to append to.

  • block (PauliEvolutionBlock | Mapping) –

    The block, or a mapping with the same fields.

  • evolution_synthesis (str, default: 'lie_trotter' ) –

    Default "lie_trotter". Product formula of the PauliEvolutionGate: "lie_trotter" (Qiskit LieTrotter) or "suzuki_trotter" (Qiskit SuzukiTrotter).

  • evolution_synthesis_options (Mapping | None, default: None ) –

    Default None. Keyword arguments passed to that Qiskit synthesis class.

Raises:

  • ValueError –

    If a "number_projector" block lacks support or angle, or the synthesis name is not supported.

build_pauli_evolution_circuit

build_pauli_evolution_circuit(blocks: Sequence[PauliEvolutionBlock | Mapping[str, Any]], *, num_qubits: int, evolution_synthesis: PauliEvolutionSynthesis = 'lie_trotter', evolution_synthesis_options: Mapping[str, Any] | None = None) -> QuantumCircuit

Build a Qiskit circuit that applies Pauli-evolution blocks in order.

Parameters:

  • blocks (Sequence[PauliEvolutionBlock | Mapping]) –

    Blocks in the order they act.

  • num_qubits (int) –

    Number of circuit qubits.

  • evolution_synthesis (str, default: 'lie_trotter' ) –

    Default "lie_trotter". Product formula for blocks of the default kind, as for append_pauli_evolution_block.

  • evolution_synthesis_options (Mapping | None, default: None ) –

    Default None. Keyword arguments of that Qiskit synthesis class.

Returns:

  • circuit ( QuantumCircuit ) –

    The circuit on num_qubits qubits.

Examples:

0.5 Z_0 and 0.3 X_1 commute, so one Lie-Trotter step of length 0.7 equals exp(-0.7i (0.5 Z_0 + 0.3 X_1)):

>>> import numpy as np
>>> from scipy.linalg import expm
>>> from qiskit.quantum_info import Operator
>>> from nwqlib.subroutines.hamiltonian_evolution import (
...     PauliEvolutionBlock, PauliEvolutionTerm,
...     build_pauli_evolution_circuit, sparse_pauli_op_from_terms)
>>> terms = (PauliEvolutionTerm(pauli="z0", coefficient=0.5),
...          PauliEvolutionTerm(pauli="x1", coefficient=0.3))
>>> block = PauliEvolutionBlock(terms=terms, time_step=0.7)
>>> circuit = build_pauli_evolution_circuit([block], num_qubits=2)
>>> H = Operator(sparse_pauli_op_from_terms(
...     [{"pauli": "z0", "coefficient": 0.5},
...      {"pauli": "x1", "coefficient": 0.3}], 2)).data
>>> print(np.allclose(Operator(circuit).data, expm(-0.7j * H)))
True

Pauli labels

Compact Pauli-label helpers for sparse Hamiltonian evolution.

A compact label such as "x0z2" names X on qubit 0 and Z on qubit 2, with strictly increasing qubit indices. Qiskit's dense label puts qubit 0 in the rightmost character, so the conversion reverses positions.

make_pauli_label

make_pauli_label(ops: Mapping[int, str]) -> str

Return a compact label such as "x0z2" from qubit-indexed Paulis.

Parameters:

  • ops (Mapping[int, str]) –

    Nonempty map from nonnegative qubit index to "X", "Y" or "Z" (either case).

Returns:

  • label ( str ) –

    Lowercase letters followed by their qubit indices, in increasing qubit order.

Raises:

  • ValueError –

    If ops is empty, an index is negative or an operator is not X, Y or Z.

Examples:

>>> from nwqlib.subroutines.hamiltonian_evolution import make_pauli_label
>>> make_pauli_label({2: "Z", 0: "X"})
'x0z2'

parse_pauli_label

parse_pauli_label(label: str) -> dict[int, str]

Parse a compact Pauli label into {qubit: op}.

The compact convention uses zero-based qubit indices and requires strictly increasing qubit order so that labels have a stable canonical form.

Parameters:

  • label (str) –

    Nonempty compact label such as "x0z2".

Returns:

  • ops ( dict[int, str] ) –

    Map from qubit index to "X", "Y" or "Z".

Raises:

  • ValueError –

    If the label is empty, malformed or not in strictly increasing qubit order.

pauli_label_to_qiskit_string

pauli_label_to_qiskit_string(label: str, num_qubits: int) -> str

Convert a compact label to Qiskit's dense Pauli string, with qubit 0 in the rightmost character.

Parameters:

  • label (str) –

    Compact label such as "x0z2".

  • num_qubits (int) –

    Positive length of the dense string.

Returns:

  • pauli ( str ) –

    Dense label, for example "IZIX" for "x0z2" on four qubits.

Raises:

  • ValueError –

    If num_qubits is not positive or the label names a qubit outside it.

sparse_pauli_op_from_terms

sparse_pauli_op_from_terms(terms: Sequence[Mapping[str, object]], num_qubits: int) -> SparsePauliOp

Build a Qiskit SparsePauliOp from compact Pauli-term records.

Parameters:

  • terms (Sequence[Mapping]) –

    Records {"pauli": label, "coefficient": c} with compact labels.

  • num_qubits (int) –

    Positive number of qubits.

Returns:

  • operator ( SparsePauliOp ) –

    sum_j c_j P_j on num_qubits qubits, or the zero operator for no terms.

Source map

Code paths are relative to nwqlib.subroutines.hamiltonian_evolution. Most of these forms have no paper equation, so their rows name the identity or dependency behind them.

Scientific step Source Code
exp(-i dt c (XX + YY)) as one XXPlusYYGate(4 dt c) XX and YY commute, and Qiskit defines XXPlusYYGate(theta) = exp(-i theta (XX + YY)/4). The parameter is formed as (2 dt c) * 2, so a finite parameter is exactly twice the stored angle of a QHD hopping block, because forming 4 dt first can overflow where theta is finite pauli_evolution.append_pauli_evolution_block, pauli_evolution._hopping_parameter
exp(-i angle (prod_q n_q - I/2**s)) with n_q = (I - Z_q)/2 A phase -angle on the all-ones basis state plus the global phase angle/2**s pauli_evolution.append_number_projector_phase
Number-projector CX count, phase-diagonal provider Shende, Bullock and Markov, quant-ph/0406176v5, Theorem 7, p. 10, with the 2**k CX of a multiplexed Rz on k select bits stated after Theorem 8, p. 11. The QHD guide states which provider each support size selects pauli_evolution.structured_number_projector_provider
Number-projector CX count, multi-controlled-phase provider Qiskit's MCPhaseGate definition and its multi-controlled X synthesis, checked against transpiled circuits pauli_evolution._mcphase_cx
Pauli rotation by basis change and a CX parity ladder Standard identity exp(-i angle P/2) stated in its docstring pauli_evolution.apply_pauli_rotation