Source code for qdk_chemistry.algorithms.phase_estimation.unary_phase_estimation

r"""Unary-iteration phase estimation with a number of walk queries."""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from collections.abc import Callable

from qdk_chemistry.data import (
    AlgorithmRef,
    Circuit,
    QpeResult,
    QuantumErrorProfile,
    QubitOperator,
)
from qdk_chemistry.utils import Logger

from .base import PhaseEstimation, PhaseEstimationSettings
from .circuit_builder.unary_phase_estimation_builder import QdkUnaryQpeCircuitBuilder

__all__: list[str] = [
    "UnaryPhaseEstimation",
    "UnaryPhaseEstimationSettings",
]

# Unary iteration threads a callable through a recursive Q# operation, which cannot be
# defunctionalized, so the circuit never lowers to QIR. Only the sparse-state simulator
# interprets Q# directly and can run it.
_SUPPORTED_CIRCUIT_EXECUTOR = "qdk_sparse_state_simulator"


def _post_process_phase_estimation(
    counts: dict[str, int],
    num_bits: int,
    method: str,
    resolve_positive_branch: bool,
    eigenvalue_from_phase: Callable[[float], float],
) -> QpeResult:
    r"""Process the measured results from unary-iteration phase estimation into a QpeResult.

    Every branch phase is doubled relative to the walk phase, so a measured bin
    :math:`y` satisfies :math:`y = \pm 2\varphi \bmod 1`, where :math:`\varphi` is the walk
    phase fraction of :math:`E = \lambda \cos(2\pi\varphi)`. Since :math:`\varphi` and
    :math:`1/2 - \varphi` are observationally identical and map to :math:`E` and :math:`-E`,
    the two eigenvalue signs cannot be distinguished, and ``resolve_positive_branch`` supplies the
    missing information:

    * ``False`` (the default) returns :math:`\varphi \in [1/4, 1/2]`, hence
      :math:`\cos(2\pi\varphi) \le 0` and :math:`E \le 0` for every input.
    * ``True`` returns :math:`\varphi \in [0, 1/4]`, hence :math:`E \ge 0`.

    Args:
        counts: Measured bitstring counts, most-significant bit first.
        num_bits: Size of the phase register.
        method: Phase estimation algorithm label recorded on the result.
        resolve_positive_branch: ``True`` selects the non-negative eigenvalue branch,
            ``False`` the non-positive one, as wanted for a ground state.
        eigenvalue_from_phase: A callable mapping a walk phase fraction to a Hamiltonian eigenvalue.

    Returns:
        A :class:`~qdk_chemistry.data.QpeResult` whose ``phase_fraction`` is the measured bin,
        ``canonical_phase_fraction`` is the decoded walk phase, and ``branching``
        holds both sign candidates.

    """
    num_bins = 2**num_bits
    canonical_counts: dict[float, int] = {}
    for bitstring, count in counts.items():
        measured = int(bitstring, 2) / num_bins
        folded = min(measured, (-measured) % 1.0) / 2.0
        canonical = folded if resolve_positive_branch else 0.5 - folded
        canonical_counts[canonical] = canonical_counts.get(canonical, 0) + count

    # Ties are broken toward the smaller phase fraction so that equal counts decode
    # to the same phase regardless of the order the shots arrive in.
    canonical_phase_fraction = max(canonical_counts, key=lambda phase: (canonical_counts[phase], -phase))
    raw_energy = eigenvalue_from_phase(canonical_phase_fraction)
    mirror_energy = eigenvalue_from_phase(0.5 - canonical_phase_fraction)

    phase_fraction = 2.0 * min(canonical_phase_fraction, 0.5 - canonical_phase_fraction)
    bitstring_msb_first = format(round(phase_fraction * num_bins), f"0{num_bits}b")

    return QpeResult.from_phase_fraction(
        method=method,
        phase_fraction=phase_fraction,
        eigenvalue_from_phase=eigenvalue_from_phase,
        canonical_phase_fraction=canonical_phase_fraction,
        branching=tuple(sorted((raw_energy, mirror_energy))),
        resolved_energy=raw_energy,
        bits_msb_first=tuple(int(bit) for bit in bitstring_msb_first),
        bitstring_msb_first=bitstring_msb_first,
    )


[docs] class UnaryPhaseEstimationSettings(PhaseEstimationSettings): """Settings for the unary-iteration phase estimation algorithm."""
[docs] def __init__(self) -> None: """Initialize the settings for unary-iteration phase estimation.""" super().__init__() self._set_default( "shots", "int", 3, "The number of shots to execute the circuit.", ) self._set_default( "resolve_positive_branch", "bool", False, "Whether the doubled measured phase resolves to a positive eigenvalue rather than a negative one.", ) self.set("qpe_circuit_builder", AlgorithmRef("qpe_circuit_builder", "qdk_unary"))
[docs] class UnaryPhaseEstimation(PhaseEstimation): """Phase estimation using unary iteration over an arbitrary-length query schedule."""
[docs] def __init__(self, shots: int = 3, resolve_positive_branch: bool = False) -> None: """Initialize the unary-iteration phase estimation routine. Args: shots: The number of shots to execute the circuit. resolve_positive_branch: ``True`` selects the non-negative eigenvalue branch, ``False`` (the default) the non-positive one, as wanted for a ground state. """ Logger.trace_entering() super().__init__() self._settings = UnaryPhaseEstimationSettings() self._settings.set("shots", shots) self._settings.set("resolve_positive_branch", resolve_positive_branch)
def _run_impl( self, state_preparation: Circuit, qubit_hamiltonian: QubitOperator, *, noise: QuantumErrorProfile | None = None, ) -> QpeResult: """Run unary-iteration phase estimation for the given state preparation and Hamiltonian. Args: state_preparation: The circuit that prepares the initial state. qubit_hamiltonian: The qubit Hamiltonian for which to estimate eigenvalues. noise: The quantum error profile to simulate noise, defaults to None. Returns: A QpeResult object containing the results of the phase estimation. Raises: TypeError: If the configured circuit builder is not a unary-iteration builder, or if the configured circuit executor is not the sparse-state simulator. """ Logger.trace_entering() circuit_executor = self._create_nested("circuit_executor") if circuit_executor.name() != _SUPPORTED_CIRCUIT_EXECUTOR: raise TypeError( f"Unary-iteration phase estimation only supports the '{_SUPPORTED_CIRCUIT_EXECUTOR}' " f"circuit executor, but got '{circuit_executor.name()}' instead." ) circuit_builder = self._create_nested("qpe_circuit_builder") if not isinstance(circuit_builder, QdkUnaryQpeCircuitBuilder): raise TypeError( f"Expected qpe_circuit_builder to be an instance of QdkUnaryQpeCircuitBuilder, " f"but got {type(circuit_builder)} instead." ) # Resolve container before running the circuit unitary_builder = circuit_builder._create_nested("unitary_builder") # noqa: SLF001 unitary_rep = unitary_builder.run(qubit_hamiltonian) container = unitary_rep.get_container() _, num_bits = circuit_builder.resolve_num_queries() circuits = circuit_builder.run( state_preparation=state_preparation, qubit_hamiltonian=qubit_hamiltonian, ) execution_data = circuit_executor.run(circuits[0], shots=self._settings.get("shots"), noise=noise) counts = execution_data.bitstring_counts return _post_process_phase_estimation( counts, num_bits, method=self.name(), resolve_positive_branch=self._settings.get("resolve_positive_branch"), eigenvalue_from_phase=container.eigenvalue_from_phase, )
[docs] def name(self) -> str: """Return the algorithm name as qdk_unary.""" return "qdk_unary"