C贸digo fuente para qiskit.circuit.library.arithmetic.linear_amplitude_function

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""A class implementing a (piecewise-) linear function on qubit amplitudes."""

from __future__ import annotations
import numpy as np
from qiskit.circuit import QuantumCircuit

from .piecewise_linear_pauli_rotations import PiecewiseLinearPauliRotations


[documentos]class LinearAmplitudeFunction(QuantumCircuit): r"""A circuit implementing a (piecewise) linear function on qubit amplitudes. An amplitude function :math:`F` of a function :math:`f` is a mapping .. math:: F|x\rangle|0\rangle = \sqrt{1 - \hat{f}(x)} |x\rangle|0\rangle + \sqrt{\hat{f}(x)} |x\rangle|1\rangle. for a function :math:`\hat{f}: \{ 0, ..., 2^n - 1 \} \rightarrow [0, 1]`, where :math:`|x\rangle` is a :math:`n` qubit state. This circuit implements :math:`F` for piecewise linear functions :math:`\hat{f}`. In this case, the mapping :math:`F` can be approximately implemented using a Taylor expansion and linearly controlled Pauli-Y rotations, see [1, 2] for more detail. This approximation uses a ``rescaling_factor`` to determine the accuracy of the Taylor expansion. In general, the function of interest :math:`f` is defined from some interval :math:`[a,b]`, the ``domain`` to :math:`[c,d]`, the ``image``, instead of :math:`\{ 1, ..., N \}` to :math:`[0, 1]`. Using an affine transformation we can rescale :math:`f` to :math:`\hat{f}`: .. math:: \hat{f}(x) = \frac{f(\phi(x)) - c}{d - c} with .. math:: \phi(x) = a + \frac{b - a}{2^n - 1} x. If :math:`f` is a piecewise linear function on :math:`m` intervals :math:`[p_{i-1}, p_i], i \in \{1, ..., m\}` with slopes :math:`\alpha_i` and offsets :math:`\beta_i` it can be written as .. math:: f(x) = \sum_{i=1}^m 1_{[p_{i-1}, p_i]}(x) (\alpha_i x + \beta_i) where :math:`1_{[a, b]}` is an indication function that is 1 if the argument is in the interval :math:`[a, b]` and otherwise 0. The breakpoints :math:`p_i` can be specified by the ``breakpoints`` argument. References: [1]: Woerner, S., & Egger, D. J. (2018). Quantum Risk Analysis. `arXiv:1806.06893 <http://arxiv.org/abs/1806.06893>`_ [2]: Gacon, J., Zoufal, C., & Woerner, S. (2020). Quantum-Enhanced Simulation-Based Optimization. `arXiv:2005.10780 <http://arxiv.org/abs/2005.10780>`_ """ def __init__( self, num_state_qubits: int, slope: float | list[float], offset: float | list[float], domain: tuple[float, float], image: tuple[float, float], rescaling_factor: float = 1, breakpoints: list[float] | None = None, name: str = "F", ) -> None: r""" Args: num_state_qubits: The number of qubits used to encode the variable :math:`x`. slope: The slope of the linear function. Can be a list of slopes if it is a piecewise linear function. offset: The offset of the linear function. Can be a list of offsets if it is a piecewise linear function. domain: The domain of the function as tuple :math:`(x_\min{}, x_\max{})`. image: The image of the function as tuple :math:`(f_\min{}, f_\max{})`. rescaling_factor: The rescaling factor to adjust the accuracy in the Taylor approximation. breakpoints: The breakpoints if the function is piecewise linear. If None, the function is not piecewise. name: Name of the circuit. """ if not hasattr(slope, "__len__"): slope = [slope] if not hasattr(offset, "__len__"): offset = [offset] # ensure that the breakpoints include the first point of the domain if breakpoints is None: breakpoints = [domain[0]] else: if not np.isclose(breakpoints[0], domain[0]): breakpoints = [domain[0]] + breakpoints _check_sizes_match(slope, offset, breakpoints) _check_sorted_and_in_range(breakpoints, domain) self._domain = domain self._image = image self._rescaling_factor = rescaling_factor # do rescalings a, b = domain c, d = image mapped_breakpoints = [] mapped_slope = [] mapped_offset = [] for i, point in enumerate(breakpoints): mapped_breakpoint = (point - a) / (b - a) * (2**num_state_qubits - 1) mapped_breakpoints += [mapped_breakpoint] # factor (upper - lower) / (2^n - 1) is for the scaling of x to [l,u] # note that the +l for mapping to [l,u] is already included in # the offsets given as parameters mapped_slope += [slope[i] * (b - a) / (2**num_state_qubits - 1)] mapped_offset += [offset[i]] # approximate linear behavior by scaling and contracting around pi/4 slope_angles = np.zeros(len(breakpoints)) offset_angles = np.pi / 4 * (1 - rescaling_factor) * np.ones(len(breakpoints)) for i in range(len(breakpoints)): slope_angles[i] = np.pi * rescaling_factor * mapped_slope[i] / 2 / (d - c) offset_angles[i] += np.pi * rescaling_factor * (mapped_offset[i] - c) / 2 / (d - c) # use PWLPauliRotations to implement the function pwl_pauli_rotation = PiecewiseLinearPauliRotations( num_state_qubits, mapped_breakpoints, 2 * slope_angles, 2 * offset_angles, name=name ) super().__init__(*pwl_pauli_rotation.qregs, name=name) self.append(pwl_pauli_rotation.to_gate(), self.qubits)
[documentos] def post_processing(self, scaled_value: float) -> float: r"""Map the function value of the approximated :math:`\hat{f}` to :math:`f`. Args: scaled_value: A function value from the Taylor expansion of :math:`\hat{f}(x)`. Returns: The ``scaled_value`` mapped back to the domain of :math:`f`, by first inverting the transformation used for the Taylor approximation and then mapping back from :math:`[0, 1]` to the original domain. """ # revert the mapping applied in the Taylor approximation value = scaled_value - 1 / 2 + np.pi / 4 * self._rescaling_factor value *= 2 / np.pi / self._rescaling_factor # map the value from [0, 1] back to the original domain value *= self._image[1] - self._image[0] value += self._image[0] return value
def _check_sorted_and_in_range(breakpoints, domain): if breakpoints is None: return # check if sorted if not np.all(np.diff(breakpoints) > 0): raise ValueError("Breakpoints must be unique and sorted.") if breakpoints[0] < domain[0] or breakpoints[-1] > domain[1]: raise ValueError("Breakpoints must be included in domain.") def _check_sizes_match(slope, offset, breakpoints): size = len(slope) if len(offset) != size: raise ValueError(f"Size mismatch of slope ({size}) and offset ({len(offset)}).") if breakpoints is not None: if len(breakpoints) != size: raise ValueError( f"Size mismatch of slope ({size}) and breakpoints ({len(breakpoints)})." )