# !/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. r""" The VQLS model. """ import logging import os from functools import partial from tqdm import tqdm from typing import Optional, List, Tuple, Callable, Union # import cv2 import numpy as np import paddle from paddle.io import Dataset, DataLoader import paddle_quantum as pq from paddle_quantum.ansatz import Circuit from paddle_quantum.gate import * from paddle_quantum.state import * from paddle_quantum.linalg import * import warnings warnings.filterwarnings("ignore", category=Warning) pq.set_backend("state_vector") def _nextPowerOf2(n: int): count = 0 if n and not (n & (n - 1)): return n while n != 0: n >>= 1 count += 1 return 1 << count def _preprocess(A: np.ndarray, b: np.ndarray): # extend input so dimensions are power of 2 fill_dim = _nextPowerOf2(len(A)) original_dim = len(A) A = np.pad(A, ((0, fill_dim-len(A)), (0, fill_dim-len(A))), 'constant', constant_values=0) # rescale b and convert to state b = np.pad(b, (0,fill_dim-len(b)), 'constant', constant_values=0) b_scale = np.linalg.norm(b) b = b / b_scale b = to_state(b) num_qubits = b.num_qubits # decompose A into 2 unitaries _, s, _ = np.linalg.svd(A) A_scale = max(np.abs(s)) A_copy = A / (A_scale + 1) u, s, v = np.linalg.svd(A_copy) z = [] z_conj = [] for idx in range(len(s)): val = s[idx] + np.sqrt(1 - s[idx] ** 2) * 1j z.append(val) z_conj.append(np.conj(val)) z = np.diag(z) z_conj = np.diag(z_conj) list_A = [] coefficients_real = [] coefficients_img = [] list_A.append(paddle.to_tensor(np.matmul(np.matmul(u, z), v))) coefficients_real.append((A_scale+1)/2) coefficients_img.append(0) list_A.append(paddle.to_tensor(np.matmul(np.matmul(u, z_conj), v))) coefficients_real.append((A_scale+1)/2) coefficients_img.append(0) return b, num_qubits, list_A, coefficients_real, coefficients_img, original_dim, b_scale def hadamard_test(phi: pq.state.State, U: paddle.Tensor, num_qubits: int) -> Tuple[paddle.Tensor, paddle.Tensor]: r""" Given unitary U and state :math:`|\phi\rangle`, it computes the expectation of U with respect to :math:`|\phi\rangle`, i.e. :math:`|\phi\rangle`. Args: phi: State which the expectation is with respect to. U: Unitary which we are taking expectation of. num_qubits: Number of qubits of phi/U. Returns: Return the real and imaginary part of the expectation value. """ # Return real part cir = Circuit(num_qubits + 1) cir.h([0]) cir.control_oracle(U, range(num_qubits + 1)) cir.h([0]) input_state = to_state(paddle.kron(zero_state(1).data, phi.data)) result_state = cir(input_state) measure = pq.loss.measure.Measure() prob_0 = measure(result_state, qubits_idx=0, desired_result='0') prob_1 = measure(result_state, qubits_idx=0, desired_result='1') real_exp = prob_0 - prob_1 # Return imaginary part cir = Circuit(num_qubits + 1) cir.h([0]) cir.sdg([0]) cir.control_oracle(U, range(num_qubits + 1)) cir.h([0]) input_state = to_state(np.kron(zero_state(1).numpy(), phi.numpy())) result_state = cir(input_state) prob_0 = measure(result_state, qubits_idx=0, desired_result='0') prob_1 = measure(result_state, qubits_idx=0, desired_result='1') img_exp = prob_0 - prob_1 return real_exp, img_exp def hadamard_overlap_test(phi: State, b: State, An: paddle.Tensor, Am: paddle.Tensor, num_qubits: int) -> Tuple[paddle.Tensor, paddle.Tensor]: r""" Given unitary Am, An and state :math:`|\phi\rangle`, b, it computes the value of :math:`\langle{b}| An |\phi\rangle\langle\phi| Am^\dagger |b\rangle`. Args: phi: State in the calculation. b: State in the calculation. Am: Unitary matrix in the calculation. An: Unitary matrix in the calculation. num_qubits: Number of qubits of the system. Returns: Return the real and imaginary part of the calculation. """ # Return the real part cir = Circuit(2*num_qubits + 1) cir.h([0]) cir.control_oracle(An, range(num_qubits + 1)) cir.control_oracle(Am.conj().T, [0]+list(range(num_qubits+1, 2*num_qubits+1))) for idx in range(num_qubits): cir.cnot([idx+1, idx+1+num_qubits]) cir.h(range(num_qubits+1)) input_state = to_state(paddle.kron(paddle.kron(zero_state(1).data, phi.data), b.data)) result_state = cir(input_state) measure = pq.loss.measure.Measure() # Calculate the result of the Overlap Circuit, obtain P_0 and P_1 as described in paper # See section IV.B. of https://arxiv.org/pdf/1303.6814.pdf bin_string = [] for i in range(2 ** num_qubits, 2 ** (num_qubits + 1)): bin_string.append(bin(i)[3:]) P_0 = paddle.zeros([1]) P_1 = paddle.zeros([1]) for i in bin_string: for j in bin_string: a = bin(int(i, base=2) & int(j, base=2))[2:].zfill(num_qubits) parity = a.count('1') % 2 if parity == 0: P_0 = P_0 + measure(result_state, desired_result='0'+i+j) P_1 = P_1 + measure(result_state, desired_result='1'+i+j) else: P_0 = P_0 - measure(result_state, desired_result='0'+i+j) P_1 = P_1 - measure(result_state, desired_result='1'+i+j) real_exp = P_0 - P_1 # Return the imaginary part cir = Circuit(2*num_qubits + 1) cir.h([0]) cir.control_oracle(An, range(num_qubits + 1)) cir.control_oracle(Am.conj().T, [0]+list(range(num_qubits+1, 2*num_qubits+1))) for idx in range(num_qubits): cir.cnot([idx+1,idx+1+num_qubits]) cir.rz(qubits_idx=0, param=-np.pi/2) cir.h(range(num_qubits+1)) result_state = cir(input_state) P_0 = paddle.zeros([1]) P_1 = paddle.zeros([1]) for i in bin_string: for j in bin_string: a = bin(int(i, base=2) & int(j, base=2))[2:].zfill(num_qubits) parity = a.count('1') % 2 if parity == 0: P_0 = P_0 + measure(result_state, desired_result='0'+i+j) P_1 = P_1 + measure(result_state, desired_result='1'+i+j) else: P_0 = P_0 - measure(result_state, desired_result='0'+i+j) P_1 = P_1 - measure(result_state, desired_result='1'+i+j) img_exp = P_0 - P_1 return real_exp, img_exp def _complex_multiplication (x_real: float, x_img: float, y_real: float, y_img: float): real = x_real*y_real - x_img*y_img img = x_real*y_img + x_img*y_real return real, img class VQLS(paddle.nn.Layer): r""" The class of the variational quantum linear solver (VQLS). Args: num_qubits: The number of qubits which the quantum circuit contains. A: List of unitaries in the decomposition of the input matrix. coefficients_real: Real part of coefficients of corresponding unitaries in the decomposition of the input matrix. coefficients_img: Imaginary part of coefficients of corresponding unitaries in the decomposition of the input matrix. b: The state which the input answer is encoded into. depth: Depth of the ansatz circuit. """ def __init__(self, num_qubits: int, A: List[paddle.Tensor], coefficients_real: List[float], coefficients_img: List[float], b: State, depth: int): super().__init__() self.num_qubits = num_qubits self.A = A self.b = b self.coefficients_real = coefficients_real self.coefficients_img = coefficients_img cir = Circuit(num_qubits) cir.complex_entangled_layer(depth=depth) self.cir = cir def forward(self) -> paddle.Tensor: r""" The forward function. Returns: Return the output of the model. """ phi = self.cir(zero_state(self.num_qubits)) numerator_real = paddle.zeros([1]) denominator_real = paddle.zeros([1]) for m in range(len(self.A)): for n in range(len(self.A)): prod_coeff_real, prod_coeff_img = _complex_multiplication(self.coefficients_real[m],-self.coefficients_img[m],self.coefficients_real[n],self.coefficients_img[n]) temp_real, temp_img = hadamard_test(phi=phi, U=self.A[m].conj().T @ self.A[n], num_qubits=self.num_qubits) temp_real, temp_img = _complex_multiplication(prod_coeff_real, prod_coeff_img, temp_real, temp_img) denominator_real = denominator_real + temp_real temp_real, temp_img = hadamard_overlap_test(phi=phi, b=self.b, An=self.A[n], Am=self.A[m], num_qubits=self.num_qubits) temp_real, temp_img = _complex_multiplication(prod_coeff_real, prod_coeff_img, temp_real, temp_img) numerator_real = numerator_real + temp_real loss = 1 - numerator_real/denominator_real return loss def _postprocess(scale: float, original_dim: int, A: np.ndarray, x: np.ndarray, b: np.ndarray) -> np.ndarray: # scale x to have the correct norm x = x[:original_dim] estimate = np.matmul(A,x) estimate_norm = np.linalg.norm(estimate) x = x * scale / estimate_norm # rotate x to have the correct phase phase = 0 for i in range(len(b)): phase = phase + b[i] / (len(b) * np.matmul(A, x)[i]) x = x * phase return x def compute(A: np.ndarray, b: np.ndarray, depth: int, iterations: int, LR: float, gamma: Optional[float]=0) -> np.ndarray: r""" Solve the linear equation Ax=b. Args: A: Input matrix. b: Input vector. depth: Depth of ansatz circuit. iterations: Number of iterations for optimization. LR: Learning rate of optimizer. gamma: Extra option to end optimization early if loss is below this value. Default to '0'. Returns: Return the vector x that solves Ax=b. Raises: ValueError: A is not a square matrix. ValueError: dimension of A and b don't match. ValueError: A is a singular matrix hence there's no unique solution. """ # check dimension of input and invertibility of A if A.shape[0] != A.shape[1]: raise ValueError("A is not a square matrix") if len(A) != len(b): raise ValueError("dimension of A and b don't match") if np.linalg.det(A) == 0: raise ValueError("A cannot be inverted") logging.basicConfig( filename='./linear_solver.log', filemode='w', format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO ) msg = f"Input parameters:" logging.info(msg) msg = f"Depth of ansatz circuit: {depth}" logging.info(msg) msg = f"Learning rate: {LR}" logging.info(msg) msg = f"Number of iterations: {iterations}" logging.info(msg) if gamma == 0: msg = f"No threshold value given." else: msg = f"Threshold value: {gamma}" logging.info(msg) msg = f"Matrix A:\n{A};" logging.info(msg) msg = f"Vector b:\n{b}" logging.info(msg) b_rescale, num_qubits, list_A, coefficients_real, coefficients_img, original_dim, scale = _preprocess(A=A,b=b) vqls = VQLS(num_qubits=num_qubits, A=list_A, coefficients_real=coefficients_real, coefficients_img=coefficients_img, b=b_rescale, depth=depth) opt = paddle.optimizer.Adam(learning_rate=LR, parameters=vqls.parameters()) for itr in tqdm(range(1, iterations + 1)): loss = vqls() loss.backward() opt.minimize(loss) opt.clear_grad() if itr % 10 == 0: msg = ( f"Iter:{itr:5d}, Loss:{loss.item(): 3.5f}" ) logging.info(msg) if loss.item()