"""Qiskit-based phase estimation circuit builder.
This module provides the Qiskit-specific implementation of the standard QPE circuit builder,
extending the base StandardQpeCircuitBuilder with Qiskit QuantumCircuit support.
"""
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, qasm3
from qiskit.synthesis.qft.qft_decompose_full import synth_qft_full
from qdk_chemistry.algorithms.phase_estimation.circuit_builder.base import (
IterativeQpeCircuitBuilder,
QpeCircuitBuilderSettings,
StandardQpeCircuitBuilder,
)
from qdk_chemistry.algorithms.phase_estimation.circuit_builder.iterative_builder import (
_validate_iteration_inputs,
)
from qdk_chemistry.data import AlgorithmRef, Circuit, QubitOperator
from qdk_chemistry.utils import Logger
__all__: list[str] = ["QiskitIterativeQpeCircuitBuilder", "QiskitStandardQpeCircuitBuilder"]
class QiskitStandardQpeCircuitBuilderSettings(QpeCircuitBuilderSettings):
"""Settings for the Qiskit Standard Phase Estimation Builder."""
def __init__(self):
"""Initialize the settings for the Qiskit Standard Phase Estimation Builder."""
super().__init__()
self._set_default("qft_do_swaps", "bool", True, "Whether to apply swap gates in the QFT.")
[docs]
class QiskitStandardQpeCircuitBuilder(StandardQpeCircuitBuilder):
"""Qiskit-based standard (QFT-based) phase estimation circuit builder.
Extends StandardQpeCircuitBuilder to add support for Qiskit QuantumCircuit objects.
Constructs the full QPE circuit (state prep, controlled unitaries, inverse QFT,
measurements) without executing it.
"""
[docs]
def __init__(
self,
num_bits: int = -1,
qft_do_swaps: bool = True,
controlled_circuit_mapper: AlgorithmRef | None = None,
unitary_builder: AlgorithmRef | None = None,
):
"""Initialize QiskitStandardQpeCircuitBuilder with the given settings.
Args:
num_bits: The number of phase bits to estimate. Default to -1; user needs to set a valid value.
qft_do_swaps: Whether to apply swap gates in the QFT. Defaults to True.
controlled_circuit_mapper: Optional algorithm reference for the controlled circuit mapper.
unitary_builder: Optional algorithm reference for the unitary builder.
"""
Logger.trace_entering()
super().__init__(num_bits=num_bits)
self._settings = QiskitStandardQpeCircuitBuilderSettings()
self._settings.set("num_bits", num_bits)
self._settings.set("qft_do_swaps", qft_do_swaps)
if unitary_builder is not None:
self._settings.set("unitary_builder", unitary_builder)
if controlled_circuit_mapper is not None:
self._settings.set("controlled_circuit_mapper", controlled_circuit_mapper)
def _run_impl(
self,
state_preparation: Circuit,
qubit_hamiltonian: QubitOperator,
) -> list[Circuit]:
"""Build the standard QPE circuit.
Args:
state_preparation: The circuit that prepares the initial state.
qubit_hamiltonian: The qubit operator for which to estimate the phase.
Returns:
A list containing a single standard QPE circuit.
"""
Logger.trace_entering()
circuit = self.build_circuit(state_preparation, qubit_hamiltonian)
Logger.info("Built standard QPE circuit using Qiskit.")
return [circuit]
[docs]
def build_circuit(
self,
state_preparation: Circuit,
qubit_hamiltonian: QubitOperator,
) -> Circuit:
"""Build the standard QPE circuit using Qiskit.
Args:
state_preparation: The circuit that prepares the initial state.
qubit_hamiltonian: The qubit operator for which to estimate the phase.
Returns:
The constructed QPE quantum circuit.
"""
Logger.trace_entering()
num_bits = self._settings.get("num_bits")
if num_bits <= 0:
raise ValueError(f"num_bits must be a positive integer. Got {num_bits}.")
# Determine unitary ancilla qubit count from the compiled controlled-circuit.
# The compiled Qiskit circuit may have additional decomposition ancillas
# beyond the logical count (e.g., for multi-controlled gate synthesis).
num_system = qubit_hamiltonian.num_qubits
probe_circuit, _ = self._create_controlled_circuit(qubit_hamiltonian=qubit_hamiltonian, power=1)
num_unitary_ancilla = probe_circuit.get_qiskit_circuit().num_qubits - 1 - num_system
phase = QuantumRegister(num_bits, "phase")
system = QuantumRegister(num_system, "system")
registers = [phase, system]
if num_unitary_ancilla > 0:
unitary_ancilla = QuantumRegister(num_unitary_ancilla, "unitary_ancilla")
registers.append(unitary_ancilla)
else:
unitary_ancilla = None
classical = ClassicalRegister(num_bits, "c")
registers.append(classical)
qc = QuantumCircuit(*registers)
Logger.debug(f"Creating traditional QPE circuit with {num_bits} ancilla qubits and measurements.")
state_prep = state_preparation.get_qiskit_circuit()
if state_prep.num_qubits != num_system:
raise ValueError(
"state_preparation must prepare the same number of system qubits as the Hamiltonian "
f"(expected {num_system}, received {state_prep.num_qubits}).",
)
qc.compose(state_prep, qubits=system, inplace=True)
for idx in range(num_bits):
qc.h(phase[idx])
target_qubits = list(system) + (list(unitary_ancilla) if unitary_ancilla else [])
for phase_idx in range(num_bits):
power = 2**phase_idx
self._append_controlled_unitary(
circuit=qc,
qubit_hamiltonian=qubit_hamiltonian,
control_qubit=phase[phase_idx],
target_qubits=target_qubits,
power=power,
)
inverse_qft = synth_qft_full(
num_bits, do_swaps=self._settings.get("qft_do_swaps"), inverse=True, name="Inverse QFT"
)
qc.compose(inverse_qft.to_gate(), qubits=phase, inplace=True)
qc.measure(phase, classical)
Logger.debug(f"Completed standard QPE circuit with {qc.num_qubits} qubits.")
return Circuit(qasm3.dumps(qc))
def _append_controlled_unitary(
self,
circuit: QuantumCircuit,
qubit_hamiltonian: QubitOperator,
control_qubit: int,
target_qubits: list,
*,
power: int,
) -> None:
"""Apply the controlled unitary to the circuit.
Args:
circuit: The quantum circuit to modify.
qubit_hamiltonian: The qubit operator for which to estimate the phase.
control_qubit: The control qubit.
target_qubits: List of target qubits.
power: The power to which the controlled unitary is raised.
"""
ctrl_unitary_circuit, _ = self._create_controlled_circuit(qubit_hamiltonian=qubit_hamiltonian, power=power)
cu_circuit = ctrl_unitary_circuit.get_qiskit_circuit()
mapping = [control_qubit, *target_qubits]
circuit.compose(cu_circuit, qubits=mapping, inplace=True)
[docs]
def name(self) -> str:
"""Return the name of the builder algorithm."""
return "qiskit_standard"
class QiskitIterativeQpeCircuitBuilderSettings(QpeCircuitBuilderSettings):
"""Settings for the Qiskit Iterative Phase Estimation Builder."""
def __init__(self):
"""Initialize the settings for the Qiskit Iterative Phase Estimation Builder."""
super().__init__()
self._set_default("phase_correction", "double", 0.0, "The accumulated phase feedback from prior iterations.")
self._set_default(
"num_iteration", "int", -1, "The specific iteration to build. Default to -1 to build all iterations."
)
[docs]
class QiskitIterativeQpeCircuitBuilder(IterativeQpeCircuitBuilder):
"""Qiskit-based iterative phase estimation circuit builder.
Extends IterativeQpeCircuitBuilder to add support for Qiskit QuantumCircuit objects.
"""
[docs]
def __init__(
self,
num_bits: int = -1,
phase_correction: float = 0.0,
num_iteration: int = -1,
controlled_circuit_mapper: AlgorithmRef | None = None,
unitary_builder: AlgorithmRef | None = None,
):
"""Initialize QiskitIterativeQpeCircuitBuilder with the given settings.
Args:
num_bits: The number of phase bits to estimate. Default to -1; user needs to set a valid value.
phase_correction: The accumulated phase feedback from prior iterations. Default to 0.0.
num_iteration: The specific iteration to build. Default to -1 (build all iterations).
controlled_circuit_mapper: AlgorithmRef | None = None,
unitary_builder: AlgorithmRef | None = None,
"""
Logger.trace_entering()
super().__init__(num_bits=num_bits)
self._settings = QiskitIterativeQpeCircuitBuilderSettings()
self._settings.set("num_bits", num_bits)
self._settings.set("phase_correction", phase_correction)
self._settings.set("num_iteration", num_iteration)
if unitary_builder is not None:
self._settings.set("unitary_builder", unitary_builder)
if controlled_circuit_mapper is not None:
self._settings.set("controlled_circuit_mapper", controlled_circuit_mapper)
def _run_impl(
self,
state_preparation: Circuit,
qubit_hamiltonian: QubitOperator,
) -> list[Circuit]:
"""Build IQPE iteration circuits using Qiskit.
Uses settings ``phase_correction`` (default 0.0) and ``num_iteration``
(default -1). When ``num_iteration`` is negative, all iteration circuits
are returned. When positive, only the circuit for that single iteration
(0-based) is returned.
Args:
state_preparation: The circuit that prepares the initial state.
qubit_hamiltonian: The qubit operator for which to build circuits.
Returns:
A list of quantum circuits, one per phase bit iteration (or a single-element
list when ``num_iteration`` is set to a specific iteration index).
Raises:
ValueError: If ``num_iteration`` >= ``num_bits``.
"""
Logger.trace_entering()
num_bits = self._settings.get("num_bits")
if num_bits <= 0:
raise ValueError(f"num_bits must be a positive integer. Got {num_bits}.")
phase_correction = self._settings.get("phase_correction")
num_iteration = self._settings.get("num_iteration")
if num_iteration >= num_bits:
raise ValueError(f"num_iteration ({num_iteration}) must be less than num_bits ({num_bits}).")
iterations = [num_iteration] if num_iteration >= 0 else range(num_bits)
circuits: list[Circuit] = []
for iteration in iterations:
circuit = self._create_iteration_circuit(
state_preparation=state_preparation,
qubit_hamiltonian=qubit_hamiltonian,
iteration=iteration,
total_iterations=num_bits,
phase_correction=phase_correction,
)
circuits.append(circuit)
Logger.info(f"Built {len(circuits)} iteration circuit(s) with phase_correction={phase_correction}.")
return circuits
def _create_iteration_circuit(
self,
state_preparation: Circuit,
qubit_hamiltonian: QubitOperator,
*,
iteration: int,
total_iterations: int,
phase_correction: float = 0.0,
) -> Circuit:
"""Construct a single IQPE iteration circuit using Qiskit.
Args:
state_preparation: Trial-state preparation circuit that prepares the initial state on the system qubits.
qubit_hamiltonian: The qubit operator for which to estimate the phase.
iteration: Current iteration index (0-based), where 0 corresponds to the most-significant bit.
total_iterations: Total number of phase bits to measure across all iterations.
phase_correction: Feedback phase angle to apply before controlled unitary, defaults to 0.0.
Returns:
A quantum circuit implementing one IQPE iteration.
"""
_validate_iteration_inputs(iteration, total_iterations)
power = 2 ** (total_iterations - iteration - 1)
ctrl_unitary_circuit, _ = self._create_controlled_circuit(qubit_hamiltonian, power)
if state_preparation.get_qiskit_circuit() and ctrl_unitary_circuit.get_qiskit_circuit():
return self._create_circuit_from_qiskit(state_preparation, ctrl_unitary_circuit, phase_correction)
raise RuntimeError("Failed to create iteration circuit without circuit interoperable Qiskit circuits.")
def _create_circuit_from_qiskit(
self, state_preparation: Circuit, controlled_unitary_circuit: Circuit, phase_correction: float
) -> Circuit:
"""Create a Circuit object from Qiskit QuantumCircuit objects.
Args:
state_preparation: Circuit object containing a Qiskit QuantumCircuit for state preparation.
controlled_unitary_circuit: Circuit object containing a Qiskit QuantumCircuit for the controlled unitary.
phase_correction: Feedback phase angle to apply before controlled unitary.
Returns:
A Circuit object representing the IQPE iteration.
"""
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, qasm3 # noqa: PLC0415
state_prep_qc = state_preparation.get_qiskit_circuit()
ctrl_unitary_qc = controlled_unitary_circuit.get_qiskit_circuit()
num_system = state_prep_qc.num_qubits
num_unitary_ancilla = ctrl_unitary_qc.num_qubits - 1 - num_system
phase = QuantumRegister(1, "phase")
system_target = QuantumRegister(num_system, "system")
registers = [phase, system_target]
if num_unitary_ancilla > 0:
unitary_ancilla = QuantumRegister(num_unitary_ancilla, "unitary_ancilla")
registers.append(unitary_ancilla)
else:
unitary_ancilla = None
classical = ClassicalRegister(1, "c")
registers.append(classical)
circuit = QuantumCircuit(*registers)
circuit.append(state_prep_qc.to_gate(), system_target)
control = phase[0]
target_qubits = list(system_target) + (list(unitary_ancilla) if unitary_ancilla else [])
circuit.h(control)
# Apply phase correction if provided
if phase_correction:
circuit.rz(phase_correction, control)
# Append the controlled unitary circuit
circuit.append(ctrl_unitary_qc.to_gate(), [control, *target_qubits])
circuit.h(control)
circuit.measure(control, classical[0])
return Circuit(qasm=qasm3.dumps(circuit))
[docs]
def name(self) -> str:
"""Return the name of the builder algorithm."""
return "qiskit_iterative"