Source code for qdk_chemistry.utils.phase

"""Utility functions for manipulating phases produced by QPE."""

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
# TODO (DBWY): I feel like the functions in this file do not have broad user scope - can they be moved to private
# context where they're used?

from collections.abc import Iterable, Sequence

import numpy as np

from qdk_chemistry.utils import Logger

__all__ = [
    "accumulated_phase_from_bits",
    "energy_alias_candidates",
    "energy_from_phase",
    "iterative_phase_feedback_update",
    "phase_fraction_from_feedback",
    "resolve_energy_aliases",
]


[docs] def energy_from_phase(phase_fraction: float, *, evolution_time: float) -> float: """Convert a measured phase fraction to energy using ``E = angle / t``. Args: phase_fraction: Fractional phase obtained from the phase register. evolution_time: Evolution time ``t`` used in ``U = exp(-i H t)``. Returns: Energy estimate corresponding to ``phase_fraction``. """ Logger.trace_entering() angle = (phase_fraction % 1.0) * (2 * np.pi) if angle > np.pi: angle -= 2 * np.pi return float(angle / evolution_time)
[docs] def energy_alias_candidates( raw_energy: float, *, evolution_time: float, shift_range: Iterable[int] = range(-2, 3), ) -> list[float]: """Enumerate alias energies compatible with ``raw_energy``. Args: raw_energy: Energy derived from the measured phase. evolution_time: Evolution time ``t`` used by the unitary. shift_range: Integer shifts (in multiples of ``2π / t``) to explore. The default ``range(-2, 3)`` checks ``k = -2, -1, 0, 1, 2``—a pragmatic window that typically covers chemical-energy estimates because they rarely deviate by more than one or two ``2π / t`` periods from the raw measurement. Returns: Sorted list of alias energy values covering positive and negative branches. """ Logger.trace_entering() period = 2 * np.pi / evolution_time # Materialize the range in case a generator is provided and guarantee the zero shift. shifts = tuple(shift_range) if 0 not in shifts: shifts = (*shifts, 0) candidate_set: set[float] = set() for shift in shifts: candidate_set.add(float(raw_energy + period * shift)) candidate_set.add(float(-raw_energy + period * shift)) return sorted(candidate_set)
[docs] def resolve_energy_aliases( raw_energy: float, *, evolution_time: float, reference_energy: float, shift_range: Iterable[int] = range(-2, 3), ) -> float: """Select the alias energy closest to a known reference value. Args: raw_energy (float): Energy derived from the measured phase. evolution_time (float): Evolution time ``t`` used by the unitary. reference_energy (float): External reference guiding alias selection. shift_range (Iterable[int]): Integer shifts (in multiples of ``2π / t``) to explore. Use a wider window when the true value may sit multiple periods away from the raw estimate. Returns: Alias energy closest to ``reference_energy``. """ Logger.trace_entering() candidates = energy_alias_candidates(raw_energy, evolution_time=evolution_time, shift_range=shift_range) return min(candidates, key=lambda energy: abs(energy - reference_energy))
[docs] def iterative_phase_feedback_update(current_phase: float, measured_bit: int) -> float: """Update the feedback phase according to Kitaev's recursion :cite:`Kitaev1995`. Args: current_phase: Phase ``Φ(k+1)`` from the previous iteration. measured_bit: Measured classical bit ``j_k`` (0 or 1). Returns: Updated feedback phase ``Φ(k)`` for the next iteration. Raises: ValueError: If ``measured_bit`` is not 0 or 1. """ Logger.trace_entering() if measured_bit not in (0, 1): raise ValueError(f"measured_bit must be 0 or 1, received {measured_bit}.") return current_phase / 2.0 + np.pi * measured_bit / 2.0
[docs] def phase_fraction_from_feedback(phase_feedback: float) -> float: """Convert the final feedback phase ``Φ(1)`` into the phase fraction ``φ``. Args: phase_feedback: Final feedback phase angle returned by IQPE. Returns: Phase fraction corresponding to ``phase_feedback``. """ Logger.trace_entering() return float(phase_feedback / np.pi)
[docs] def accumulated_phase_from_bits(bits_msb_first: Sequence[int]) -> float: """Compute ``Φ(1)`` directly from a measured bit string (MSB to LSB). Args: bits_msb_first: Sequence of measured bits ordered from MSB to LSB. Returns: Accumulated phase angle ``Φ(1)``. """ Logger.trace_entering() phase = 0.0 for bit in reversed(bits_msb_first): phase = iterative_phase_feedback_update(phase, bit) return phase