def density_matrix(state): """Calculates the density matrix representation of a state. Args: state (array[complex]): array representing a quantum state vector Returns: dm: (array[complex]): array representing the density matrix """ return state * np.conj(state).T
def BuildDensityBasisState(self, params, num=0): ''' Build density matrix associated to params and initial basis state ''' # Get device state self.ThermalQNode(params, i=Dec2nbitBin(num, self.num_spins)) state = self.device.state # Return density matrix return np.outer(state, np.conj(state))
def squared_term(a, r, phi): """Analytic expression for <N^2>""" magnitude_squared = np.abs(a)**2 squared_term = ( -magnitude_squared + magnitude_squared**2 + 2 * magnitude_squared * np.cosh(2 * r) - np.exp(-1j * phi) * a**2 * np.cosh(r) * np.sinh(r) - np.exp(1j * phi) * np.conj(a)**2 * np.cosh(r) * np.sinh(r) + np.sinh(r)**4 + np.cosh(r) * np.sinh(r) * np.sinh(2 * r)) return squared_term
def fidelity(state1, state2): """ Calculates the fidelity between two state vectors Args: state1 (array[float]): State vector representation state2 (array[float]): State vector representation Returns: float: fidelity between `state1` and `state2` """ return np.abs(np.dot(np.conj(state1), state2))
def matrix_norm(mixed_state, pure_state): """Computes the matrix one-norm of the difference between mixed and pure states. Args: - mixed_state (np.tensor): A density matrix - pure_state (np.tensor): A pure state Returns: - (float): The matrix one-norm """ return np.sum(np.abs(mixed_state - np.outer(pure_state, np.conj(pure_state))))
def cost_loc(weights): """Local version of the cost function, which tends to zero when A |x> is proportional to |b>.""" mu_sum = 0.0 for l in range(0, len(c)): for lp in range(0, len(c)): for j in range(0, n_qubits): mu_sum = mu_sum + c[l] * np.conj(c[lp]) * mu(weights, l, lp, j) mu_sum = abs(mu_sum) # Cost function C_L return 0.5 - 0.5 * mu_sum / (n_qubits * psi_norm(weights))
def test_correct_state(self, tol): """Test that the device state is correct after applying a quantum function on the device""" dev = qml.device("default.qubit.autograd", wires=2) state = dev.state expected = np.array([1, 0, 0, 0]) assert np.allclose(state, expected, atol=tol, rtol=0) @qml.qnode(dev, interface="autograd", diff_method="backprop") def circuit(): qml.Hadamard(wires=0) qml.RZ(np.pi / 4, wires=0) return qml.expval(qml.PauliZ(0)) circuit() state = dev.state amplitude = np.exp(-1j * np.pi / 8) / np.sqrt(2) expected = np.array([amplitude, 0, np.conj(amplitude), 0]) assert np.allclose(state, expected, atol=tol, rtol=0)
def fourier_function(freq_dict, x): r"""Function of the form :math:`\sum c_n e^{inx}` to construct and evaluate a Fourier series given a dictionary of frequencies and their coefficients. Args: freq_dict (dict[int, float]): Pairs of positive integer frequencies and corresponding Fourier coefficients. Returns: float: output of the function """ result = 0 # Handle 0 coefficient separately if 0 in freq_dict.keys(): result = freq_dict[0] result += sum([ coeff * np.exp(1j * freq * x) + np.conj(coeff) * np.exp(-1j * freq * x) for freq, coeff in freq_dict.items() if freq != 0 ]) return result.real
def mt(*params): state = qnode(*params) rqnode = lambda *params: np.real(qnode(*params)) iqnode = lambda *params: np.imag(qnode(*params)) rjac = qml.jacobian(rqnode)(*params) ijac = qml.jacobian(iqnode)(*params) if isinstance(rjac, tuple): out = [] for rc, ic in zip(rjac, ijac): c = rc + 1j * ic psidpsi = np.tensordot(np.conj(state), c, axes=([0], [0])) out.append( np.real( np.tensordot(np.conj(c), c, axes=([0], [0])) - np.tensordot(np.conj(psidpsi), psidpsi, axes=0))) return tuple(out) jac = rjac + 1j * ijac psidpsi = np.tensordot(np.conj(state), jac, axes=([0], [0])) return np.real( np.tensordot(np.conj(jac), jac, axes=([0], [0])) - np.tensordot(np.conj(psidpsi), psidpsi, axes=0))
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # gradient = np.zeros([6], dtype=np.float64) for i in range(6): params[i] += 0.1 forward = qnode(params) params[i] -= 0.2 backward = qnode(params) params[i] += 0.1 gradient[i] = (forward - backward) / (2 * np.sin(0.1)) @qml.qnode(dev) def fubini_qnode(params): variational_circuit(params) return qml.state() fubini = np.zeros([6, 6], dtype=np.float64) base = np.conj(fubini_qnode(params)) def fubini_calculate(): for i in range(6): for j in range(6): params[i] += np.pi / 2 params[j] += np.pi / 2 plusplus = np.abs(np.dot(base, fubini_qnode(params)))**2 params[j] -= np.pi plusminus = np.abs(np.dot(base, fubini_qnode(params)))**2 params[i] -= np.pi minusminus = np.abs(np.dot(base, fubini_qnode(params)))**2 params[j] += np.pi minusplus = np.abs(np.dot(base, fubini_qnode(params)))**2 fubini[i, j] = (-plusplus - minusminus + plusminus + minusplus) / 8 params[i] += np.pi / 2 params[j] -= np.pi / 2 fubini_calculate() natural_grad = np.matmul(np.linalg.inv(fubini), gradient) # QHACK # return natural_grad
def test_conj(t): res = fn.conj(t) assert fn.allequal(res, np.conj(t))
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # s = 0.1 gradient = np.zeros([6], dtype=np.float64) for i in range(6): params[i] += s plus_s = qnode(params) params[i] -= 2 * s minus_s = qnode(params) gradient[i] = (plus_s - minus_s) / (2 * np.sin(s)) params[i] += s @qml.qnode(dev) def my_qnode(params): variational_circuit(params) return qml.state() s = np.pi / 2 ket_state = my_qnode(params) bra_state = np.conj(ket_state) F = np.zeros([6, 6], dtype=np.float64) def get_F(qnode, params): for i in range(6): for j in range(6): params[i] += s params[j] += s pp_state = qnode(params) pp = np.abs(np.dot(bra_state, pp_state))**2 params[j] -= 2 * s pm_state = qnode(params) pm = np.abs(np.dot(bra_state, pm_state))**2 params[i] -= 2 * s params[j] += 2 * s mp_state = qnode(params) mp = np.abs(np.dot(bra_state, mp_state))**2 params[j] -= 2 * s mm_state = qnode(params) mm = np.abs(np.dot(bra_state, mm_state))**2 F[i, j] = (-pp + pm + mp - mm) / 8 params[i] += s params[j] += s get_F(my_qnode, params) natural_grad = np.matmul(np.linalg.inv(F), gradient) # QHACK # return natural_grad
class AutogradBox(qml.math.TensorBox): """Implements the :class:`~.TensorBox` API for ``pennylane.numpy`` tensors. For more details, please refer to the :class:`~.TensorBox` documentation. """ abs = wrap_output(lambda self: np.abs(self.data)) angle = wrap_output(lambda self: np.angle(self.data)) arcsin = wrap_output(lambda self: np.arcsin(self.data)) cast = wrap_output(lambda self, dtype: np.tensor(self.data, dtype=dtype)) conj = wrap_output(lambda self: np.conj(self.data)) diag = staticmethod(wrap_output(lambda values, k=0: np.diag(values, k=k))) expand_dims = wrap_output(lambda self, axis: np.expand_dims(self.data, axis=axis)) gather = wrap_output(lambda self, indices: self.data[indices]) ones_like = wrap_output(lambda self: np.ones_like(self.data)) reshape = wrap_output(lambda self, shape: np.reshape(self.data, shape)) sqrt = wrap_output(lambda self: np.sqrt(self.data)) sum = wrap_output( lambda self, axis=None, keepdims=False: np.sum(self.data, axis=axis, keepdims=keepdims) ) T = wrap_output(lambda self: self.data.T) squeeze = wrap_output(lambda self: self.data.squeeze()) @staticmethod def astensor(tensor): return np.tensor(tensor) @staticmethod @wrap_output def block_diag(values): return block_diag(*AutogradBox.unbox_list(values)) @staticmethod @wrap_output def concatenate(values, axis=0): return np.concatenate(AutogradBox.unbox_list(values), axis=axis) @staticmethod @wrap_output def dot(x, y): x, y = AutogradBox.unbox_list([x, y]) if x.ndim == 0 and y.ndim == 0: return x * y if x.ndim == 2 and y.ndim == 2: return x @ y return np.dot(x, y) @property def interface(self): return "autograd" def numpy(self): if hasattr(self.data, "_value"): # Catches the edge case where the data is an Autograd arraybox, # which only occurs during backpropagation. return self.data._value return self.data.numpy() @property def requires_grad(self): return self.data.requires_grad @wrap_output def scatter_element_add(self, index, value): size = self.data.size flat_index = np.ravel_multi_index(index, self.shape) t = [0] * size t[flat_index] = value self.data = self.data + np.array(t).reshape(self.shape) return self.data @property def shape(self): return self.data.shape @staticmethod @wrap_output def stack(values, axis=0): return np.stack(AutogradBox.unbox_list(values), axis=axis) @wrap_output def take(self, indices, axis=None): indices = self.astensor(indices) if axis is None: return self.data.flatten()[indices] fancy_indices = [slice(None)] * axis + [indices] return self.data[tuple(fancy_indices)] @staticmethod @wrap_output def where(condition, x, y): return np.where(condition, *AutogradBox.unbox_list([x, y]))
def density_matrix(state): """ Calculates the density matrix representation of a state (a state is a complex vector in the canonical basis representation). The density matrix is a Hermitian operator. """ return np.outer(np.conj(state), state)
def natural_gradient(params): """Calculate the natural gradient of the qnode() cost function. The code you write for this challenge should be completely contained within this function between the # QHACK # comment markers. You should evaluate the metric tensor and the gradient of the QNode, and then combine these together using the natural gradient definition. The natural gradient should be returned as a NumPy array. The metric tensor should be evaluated using the equation provided in the problem text. Hint: you will need to define a new QNode that returns the quantum state before measurement. Args: params (np.ndarray): Input parameters, of dimension 6 Returns: np.ndarray: The natural gradient evaluated at the input parameters, of dimension 6 """ natural_grad = np.zeros(6) # QHACK # import math # Create qnode for quantum state gradient = np.zeros([len(params)], dtype=np.float64) behaviour_matrix = np.zeros([len(params), len(params)], dtype=np.float64) # Obtain gradient def parameter_shift_term(qnode, params, shift_value, index): shifted = params.copy() shifted[index] += shift_value forward = qnode(shifted) # forward evaluation shifted[index] -= shift_value*2 backward = qnode(shifted) # backward evaluation return (forward - backward)/(2*math.sin(shift_value)) for i in range(len(params)): gradient[i] = parameter_shift_term(qnode, params, math.pi/2, i) # print(gradient) # Metric tensor # First of all, reset state dev.reset() @qml.qnode(dev) def circuit(params): variational_circuit(params) return qml.state() # Execution with original params original_params_state = np.conj(circuit(params)) dev.reset() def shifter(params, shift_value, row, col, signs): r""" This function executes the shift needed for the Hessian calculation. Args: row (int): First index of the shift col (int): Second index of the shift signs (tuple): Tuple with signs (+1,-1) os the shift to be made Returns: array: Shifted params """ shifted = params.copy() shifted[row] += shift_value*signs[0] shifted[col] += shift_value*signs[1] return shifted def magnitude_calculator(bra, ket): return pow(np.abs(np.inner(bra, ket)), 2) def parameter_behave_terms(qnode, params, shift_value, row, col): # Reference for future review: https://arxiv.org/pdf/2008.06517.pdf dev.reset() qnode(shifter(params, shift_value, row, col, [1, 1])) step_1 = magnitude_calculator(original_params_state, dev.state) dev.reset() qnode(shifter(params, shift_value, row, col, [1, -1])) step_2 = magnitude_calculator(original_params_state, dev.state) dev.reset() qnode(shifter(params, shift_value, row, col, [-1, 1])) step_3 = magnitude_calculator(original_params_state, dev.state) dev.reset() qnode(shifter(params, shift_value, row, col, [-1, -1])) step_4 = magnitude_calculator(original_params_state, dev.state) return (-step_1 + step_2 + step_3 - step_4)/8 for i in range(len(params)): for j in range(i, len(params)): behaviour_matrix[i][j] = parameter_behave_terms( circuit, params, math.pi/2, i, j) behaviour_matrix[j][i] = behaviour_matrix[i][j] # print(behaviour_matrix) # qml.metric_tenser return a block diagonal matrix. # Can be used for comparison, but it is not to be used for this task #print(np.round(qml.metric_tensor(qnode)(params), 8)) inv_behaviour_matrix = np.linalg.inv(behaviour_matrix) # print(inv_behaviour_matrix) natural_grad = inv_behaviour_matrix.dot(gradient) # QHACK # return natural_grad