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_jand durationtime_stepmeansexp(-i * time_step * sum_j c_j P_j), approximated by one application of the chosen product formula unless the terms commute. Thepauli_evolutionmodule text below describes the exact number-projector andXX + YYforms. - Compact labels such as
x0z2index 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, asparse_pauli_labelaccepts. -
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 ofP.
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 thePauliEvolutionGate:"lie_trotter"(QiskitLieTrotter) or"suzuki_trotter"(QiskitSuzukiTrotter). -
evolution_synthesis_options(Mapping | None, default:None) –Default
None. Keyword arguments passed to that Qiskit synthesis class.
Raises:
-
ValueError–If a
"number_projector"block lackssupportorangle, 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 forappend_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_qubitsqubits.
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
opsis 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_qubitsis 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_jonnum_qubitsqubits, 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 |