# !/usr/bin/env python3 # Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import numpy as np import paddle from typing import Callable, Optional, Tuple, Union from math import log2 from .angles import qpp_angle_finder, qpp_angle_approximator from .laurent import Q_generation, pair_generation, laurent_generator, revise_tol from ..ansatz import Circuit from ..backend import Backend from ..base import get_dtype from ..gate import X from ..intrinsic import _get_float_dtype, _type_transform from ..linalg import unitary_random, is_unitary from ..loss import Measure from ..operator import Collapse from ..qinfo import dagger, partial_trace from ..state import State, zero_state r""" QPP circuit and related tools, see Theorem 6 in paper https://arxiv.org/abs/2209.14278 for more details. """ __all__ = ["qpp_cir", "simulation_cir", "qps", "qubitize", "purification_block_enc"] def qpp_cir( list_theta: Union[np.ndarray, paddle.Tensor], list_phi: Union[np.ndarray, paddle.Tensor], U: Union[np.ndarray, paddle.Tensor, float], ) -> Circuit: r"""Construct a quantum phase processor of QPP by `list_theta` and `list_phi`. Args: list_theta: angles for :math:`R_Y` gates. list_phi: angles for :math:`R_Z` gates. U: unitary or scalar input. Returns: a multi-qubit version of trigonometric QSP. """ complex_dtype = get_dtype() float_dtype = _get_float_dtype(complex_dtype) if not isinstance(list_theta, paddle.Tensor): list_theta = paddle.to_tensor(list_theta, dtype=float_dtype) if not isinstance(list_phi, paddle.Tensor): list_phi = paddle.to_tensor(list_phi, dtype=float_dtype) if not isinstance(U, paddle.Tensor): U = paddle.to_tensor(U, dtype=complex_dtype) if len(U.shape) == 1: U = U.cast(float_dtype) list_theta, list_phi = np.squeeze(list_theta), np.squeeze(list_phi) assert len(list_theta) == len(list_phi) n = int(log2(U.shape[0])) L = len(list_theta) - 1 cir = Circuit(n + 1) all_register = list(range(n + 1)) for i in range(L): cir.rz(0, param=list_phi[i]) cir.ry(0, param=list_theta[i]) # input the unitary if len(U.shape) == 1: cir.rz(0, param=U) elif i % 2 == 0: cir.control_oracle(U, all_register, latex_name=r"$U$") else: cir.x(0) cir.control_oracle(dagger(U), all_register, latex_name=r"$U^\dagger$") cir.x(0) cir.rz(0, param=list_phi[-1]) cir.ry(0, param=list_theta[-1]) return cir def simulation_cir( fn: Callable[[np.ndarray], np.ndarray], U: Union[np.ndarray, paddle.Tensor, float], approx: Optional[bool] = True, deg: Optional[int] = 50, length: Optional[float] = np.pi, step_size: Optional[float] = 0.00001 * np.pi, tol: Optional[float] = 1e-30, ) -> Circuit: r"""Return a QPP circuit approximating `fn`. Args: fn: function to be approximated. U: unitary input. deg: degree of approximation, defaults to be :math:`50`. approx: whether approximately find the angle of circuits, defaults to be ``True`` length: half of approximation width, defaults to be :math:`\pi`. step_size: sampling frequency of data points, defaults to be :math:`0.00001 \pi`. tol: error tolerance, defaults to be :math:`10^{-30}`. Returns: a QPP circuit approximating `fn` in Theorem 6 of paper https://arxiv.org/abs/2209.14278. """ f = laurent_generator(fn, step_size, deg, length) if np.abs(f.max_norm - 1) < 1e-2: f = f * (0.999999999 / f.max_norm) P, Q = pair_generation(f) revise_tol(tol) list_theta, list_phi = ( qpp_angle_approximator(P, Q) if approx else qpp_angle_finder(P, Q) ) return qpp_cir(list_theta, list_phi, U) def qps( U: Union[np.ndarray, paddle.Tensor], initial_state: Union[np.ndarray, paddle.Tensor, State], ) -> Tuple[float, State]: r"""Algorithm for quantum phase search, see Algorithm 1 and 2 of paper https://arxiv.org/abs/2209.14278. Args: U: target unitary initial_state: the input quantum state Returns: contains the following two elements: * an eigenphase of U * its corresponding eigenstate that has overlap with the initial state """ assert is_unitary(U) U, initial_state = _type_transform(U, "tensor"), _type_transform( initial_state, "state_vector" ) initial_state = zero_state(1).kron(initial_state) def p_func(x): target = 2.0 * np.arctan(np.sin(x) / 0.01) / np.pi return np.sqrt((1 + target) / 2) # function approximation P = laurent_generator(p_func, 0.00001 * np.pi, 160, np.pi) Q = Q_generation(P) list_theta, list_phi = qpp_angle_approximator(P, Q) # Algorithm setting T, Q, Delta = 13, 10, 0.2 Delta_bar = Delta + np.pi / (2 ** (Q + 1)) op_M = Measure() # measurement op_C = [ Collapse(0, desired_result="0"), Collapse(0, desired_result="1"), ] # collapse operator op_X = X(0) # x gate used to reset the first qubit def __measure_and_collapse(target_state: State) -> Tuple[float, State]: r"""Measure ``target_state``, and return the measurement outcome and collapsed state""" prob_dist = op_M(target_state, 0).tolist() result = np.random.choice(2, 1, p=prob_dist)[0] return result, op_C[result](target_state) def __interval_search( U: paddle.Tensor, input_state: State, interval: Tuple[int, int] ) -> Tuple[int, int]: r"""Find a smaller interval containing eigenphases from ``interval``, Algorithm 1.""" x_low, x_up = interval[0], interval[1] theta = (x_up + x_low) / 2 state = input_state.clone() # if the interval is too wide, the update rule is different if x_up - x_low > 2 * np.pi - 2 * Delta: state = qpp_cir(list_theta, list_phi, np.exp(-1j * theta).item() * U)(state) result, state = __measure_and_collapse(state) if result == 0: x_low = -Delta x_up += Delta else: x_low -= Delta x_up = Delta state = op_X(state) theta = (x_up + x_low) / 2 # begin interval search formally for _ in range(Q): state = qpp_cir(list_theta, list_phi, np.exp(-1j * theta).item() * U)(state) result, state = __measure_and_collapse(state) # update the interval according to the measurement result of the first qubit # - if the measurement result is 0, choose the RHS interval # - otherwise, choose the LHS and reset the first qubit if result == 0: x_low = theta - Delta else: x_up = theta + Delta state = op_X(state) theta = (x_up + x_low) / 2 return [x_low, x_up], state # Algorithm 2 phase_estimate = 0 p = int(np.round(1 / Delta_bar)) itr_interval = [-np.pi, np.pi] itr_state = initial_state for j in range(T): itr_interval, itr_state = __interval_search(U, itr_state, itr_interval) lamb = (itr_interval[0] + itr_interval[1]) / 2 phase_estimate += lamb * (Delta**j) # update unitary and interval, so that we can start a new interval search U = np.linalg.matrix_power(U, p) * np.exp(-1j * p * lamb) itr_interval = ((np.array(itr_interval) - lamb) * p).tolist() return phase_estimate, partial_trace(itr_state, 2**1, U.shape[0], A_or_B=1) def qubitize( block_enc: Union[np.ndarray, paddle.Tensor], num_block_qubits: int ) -> paddle.Tensor: r"""Qubitize the block encoding to keep subspace invariant using one more ancilla qubit, see paper http://arxiv.org/abs/1610.06546 for more details. Args: block_enc: the target block encoding. num_block_qubits: number of ancilla qubits used in block encoding. Returns: the qubitized version for ``block_enc``. """ if isinstance(block_enc, np.ndarray): block_enc = paddle.to_tensor(block_enc) n = int(log2(block_enc.shape[0])) cir = Circuit(n + 1) cir.h(0) cir.x(0) cir.control_oracle(block_enc, list(range(n + 1))) cir.x(0) cir.control_oracle(dagger(block_enc), list(range(n + 1))) cir.x(0) cir.h(0) num_qubits = n - num_block_qubits zero_states = zero_state(num_block_qubits, backend=Backend.DensityMatrix) zero_projector = paddle.kron( 2 * zero_states.data - paddle.eye(2**num_block_qubits), paddle.eye(2**num_qubits), ) return (cir.unitary_matrix() @ zero_projector).cast(block_enc.dtype) def purification_block_enc(num_qubits: int, num_block_qubits: int) -> paddle.Tensor: r"""Randomly generate a :math:`(n + m)`-qubit qubitized block encoding of a :math:`n`-qubit density matrix. Args: num_qubits: number of qubits :math:`n`. num_block_qubits: number of ancilla qubits :math:`m > n` used in block encoding. Returns: a :math:`2^{n + m} \times 2^{n + m}` unitary matrix that its upper-left block is a density matrix. """ assert ( num_qubits < num_block_qubits ), f"the number of block qubits need to be larger than the number of qubits {num_qubits}: received {num_block_qubits}" V = unitary_random(num_block_qubits) aux1_reg = list(range(num_block_qubits - num_qubits)) aux2_reg = list(range(num_block_qubits - num_qubits, num_block_qubits)) sys_reg = list(range(num_block_qubits, num_qubits + num_block_qubits)) cir = Circuit(num_qubits + num_block_qubits) cir.oracle(V, aux1_reg + aux2_reg) for i in range(num_qubits): cir.swap([aux2_reg[i], sys_reg[i]]) cir.oracle(dagger(V), aux1_reg + aux2_reg) zero_states = paddle.eye(2**num_block_qubits, 1) reflector = 2 * zero_states @ dagger(zero_states) - paddle.eye( 2**num_block_qubits ) cir.oracle(reflector, list(range(num_block_qubits))) return cir.unitary_matrix()