def test_expand_one(self, tol): """Test that a 1 qubit gate correctly expands to 3 qubits.""" # test applied to wire 0 res = pu.expand(U, [0], 3) expected = np.kron(np.kron(U, I), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 1 res = pu.expand(U, [1], 3) expected = np.kron(np.kron(I, U), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 2 res = pu.expand(U, [2], 3) expected = np.kron(np.kron(I, I), U) assert np.allclose(res, expected, atol=tol, rtol=0)
def _matrix(*params): theta = params[0] pauli_word = params[1] if not PauliRot._check_pauli_word(pauli_word): raise ValueError( 'The given Pauli word "{}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z".format(pauli_word)) # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]) multi_Z_rot_matrix = MultiRZ._matrix(theta, len(non_identity_gates)) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( np.kron, [ PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates ], ) return expand( conjugation_matrix.T.conj() @ multi_Z_rot_matrix @ conjugation_matrix, non_identity_wires, list(range(len(pauli_word))), )
def test_expand_one_wires_list(self, tol): """Test that a 1 qubit gate correctly expands to 3 qubits.""" # test applied to wire 0 res = pu.expand(U, [0], [0, 4, 9]) expected = np.kron(np.kron(U, I), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 4 res = pu.expand(U, [4], [0, 4, 9]) expected = np.kron(np.kron(I, U), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 9 res = pu.expand(U, [9], [0, 4, 9]) expected = np.kron(np.kron(I, I), U) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_three_nonconsecutive_ascending_wires(self, tol): """Test that a 3 qubit gate on non-consecutive but ascending wires correctly expands to 4 qubits.""" # test applied to wire 0,2,3 res = pu.expand(U_toffoli, [0, 2, 3], 4) expected = ( np.kron(SWAP, np.kron(I, I)) @ np.kron(I, U_toffoli) @ np.kron(SWAP, np.kron(I, I)) ) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 0,1,3 res = pu.expand(U_toffoli, [0, 1, 3], 4) expected = ( np.kron(np.kron(I, I), SWAP) @ np.kron(U_toffoli, I) @ np.kron(np.kron(I, I), SWAP) ) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_three_nonconsecutive_nonascending_wires(self, tol): """Test that a 3 qubit gate on non-consecutive non-ascending wires correctly expands to 4 qubits""" # test applied to wire 3, 1, 2 res = pu.expand(U_toffoli, [3, 1, 2], 4) # change the control qubit on the Toffoli gate rows = np.array([0, 4, 1, 5, 2, 6, 3, 7]) expected = np.kron(I, U_toffoli[:, rows][rows]) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 3, 0, 2 res = pu.expand(U_toffoli, [3, 0, 2], 4) # change the control qubit on the Toffoli gate rows = np.array([0, 4, 1, 5, 2, 6, 3, 7]) expected = (np.kron(SWAP, np.kron(I, I)) @ np.kron( I, U_toffoli[:, rows][rows]) @ np.kron(SWAP, np.kron(I, I))) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_two_reversed_wires(self, tol): """Test that a 2 qubit gate on reversed consecutive wires correctly expands to 4 qubits.""" # CNOT with target on wire 1 res = pu.expand(CNOT, [1, 0], 4) rows = np.array([0, 2, 1, 3]) expected = np.kron(np.kron(CNOT[:, rows][rows], I), I) assert np.allclose(res, expected, atol=tol, rtol=0)
def test_expand_two_consecutive_wires(self, tol): """Test that a 2 qubit gate on consecutive wires correctly expands to 4 qubits.""" # test applied to wire 0+1 res = pu.expand(U2, [0, 1], 4) expected = np.kron(np.kron(U2, I), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 1+2 res = pu.expand(U2, [1, 2], 4) expected = np.kron(np.kron(I, U2), I) assert np.allclose(res, expected, atol=tol, rtol=0) # test applied to wire 2+3 res = pu.expand(U2, [2, 3], 4) expected = np.kron(np.kron(I, I), U2) assert np.allclose(res, expected, atol=tol, rtol=0)
def _matrix(cls, *params): pauli_word = params[1] if not PauliRot._check_pauli_word(pauli_word): raise ValueError( f'The given Pauli word "{pauli_word}" contains characters that are not allowed.' " Allowed characters are I, X, Y and Z") theta = params[0] interface = qml.math.get_interface(theta) if interface == "tensorflow": theta = qml.math.cast_like(theta, 1j) # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): exp = qml.math.exp(-1j * theta / 2) iden = qml.math.eye(2**len(pauli_word)) if interface == "torch": # Use convert_like to ensure that the tensor is put on the correct # Torch device iden = qml.math.convert_like(iden, theta) return exp * iden return qml.math.array(exp * iden, like=interface) # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]) multi_Z_rot_matrix = MultiRZ._matrix(theta, len(non_identity_gates)) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( qml.math.kron, [ PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates ], ) return expand( qml.math.dot( qml.math.conj(conjugation_matrix), qml.math.dot(multi_Z_rot_matrix, conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), )
def generator(self): if self._generator is None: pauli_word = self.parameters[1] # Simplest case is if the Pauli is the identity matrix if pauli_word == "I" * len(pauli_word): self._generator = [np.eye(2**len(pauli_word)), -1 / 2] return self._generator # We first generate the matrix excluding the identity parts and expand it afterwards. # To this end, we have to store on which wires the non-identity parts act non_identity_wires, non_identity_gates = zip( *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]) # get MultiRZ's generator multi_Z_rot_generator = qml.math.diag( pauli_eigs(len(non_identity_gates))) # now we conjugate with Hadamard and RX to create the Pauli string conjugation_matrix = functools.reduce( qml.math.kron, [ PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates ], ) self._generator = [ expand( qml.math.dot( qml.math.conj(qml.math.T(conjugation_matrix)), qml.math.dot(multi_Z_rot_generator, conjugation_matrix), ), non_identity_wires, list(range(len(pauli_word))), ), -1 / 2, ] return self._generator
def test_expand_invalid_matrix(self): """test exception raised if incorrect sized matrix provided/""" with pytest.raises(ValueError, match="Matrix parameter must be of size"): pu.expand(U, [0, 1], 4)
def _construct_metric_tensor(self, *, diag_approx=False): """Construct metric tensor subcircuits for qubit circuits. Constructs a set of quantum circuits for computing a block-diagonal approximation of the Fubini-Study metric tensor on the parameter space of the variational circuit represented by the QNode, using the Quantum Geometric Tensor. If the parameter appears in a gate :math:`G`, the subcircuit contains all gates which precede :math:`G`, and :math:`G` is replaced by the variance value of its generator. Args: diag_approx (bool): iff True, use the diagonal approximation Raises: QuantumFunctionError: if a metric tensor cannot be generated because no generator was defined """ # pylint: disable=too-many-statements, too-many-branches self._metric_tensor_subcircuits = {} for queue, curr_ops, param_idx, _ in self.circuit.iterate_layers(): obs = [] scale = [] Ki_matrices = [] KiKj_matrices = [] Ki_ev = [] KiKj_ev = [] V = None # for each operation in the layer, get the generator and convert it to a variance for n, op in enumerate(curr_ops): gen, s = op.generator w = op.wires if gen is None: raise QuantumFunctionError( "Can't generate metric tensor, operation {}" "has no defined generator".format(op)) # get the observable corresponding to the generator of the current operation if isinstance(gen, np.ndarray): # generator is a Hermitian matrix variance = var(qml.Hermitian(gen, w, do_queue=False)) if not diag_approx: Ki_matrices.append((n, expand(gen, w, self.num_wires))) elif issubclass(gen, Observable): # generator is an existing PennyLane operation variance = var(gen(w, do_queue=False)) if not diag_approx: if issubclass(gen, qml.PauliX): mat = np.array([[0, 1], [1, 0]]) elif issubclass(gen, qml.PauliY): mat = np.array([[0, -1j], [1j, 0]]) elif issubclass(gen, qml.PauliZ): mat = np.array([[1, 0], [0, -1]]) Ki_matrices.append((n, expand(mat, w, self.num_wires))) else: raise QuantumFunctionError( "Can't generate metric tensor, generator {}" "has no corresponding observable".format(gen)) obs.append(variance) scale.append(s) if not diag_approx: # In order to compute the block diagonal portion of the metric tensor, # we need to compute 'second order' <psi|K_i K_j|psi> terms. for i, j in itertools.product(range(len(Ki_matrices)), repeat=2): # compute the matrices representing all K_i K_j terms obs1 = Ki_matrices[i] obs2 = Ki_matrices[j] KiKj_matrices.append( ((obs1[0], obs2[0]), obs1[1] @ obs2[1])) V = np.identity(2**self.num_wires, dtype=np.complex128) # generate the unitary operation to rotate to # the shared eigenbasis of all observables for _, term in Ki_matrices: _, S = linalg.eigh(V.conj().T @ term @ V) V = np.round(V @ S, 15) V = V.conj().T # calculate the eigenvalues for # each observable in the shared eigenbasis for idx, term in Ki_matrices: eigs = np.diag(V @ term @ V.conj().T).real Ki_ev.append((idx, eigs)) for idx, term in KiKj_matrices: eigs = np.diag(V @ term @ V.conj().T).real KiKj_ev.append((idx, eigs)) self._metric_tensor_subcircuits[param_idx] = { "queue": queue, "observable": obs, "Ki_expectations": Ki_ev, "KiKj_expectations": KiKj_ev, "eigenbasis_matrix": V, "result": None, "scale": scale, }
def test_expand_invalid_wires(self): """test exception raised if unphysical subsystems provided.""" with pytest.raises( ValueError, match="Invalid target subsystems provided in 'wires' argument." ): pu.expand(U2, [-1, 5], 4)
def construct_metric_tensor(self, args, **kwargs): """Create metric tensor subcircuits for each parameter. If the parameter appears in a gate :math:`G`, the subcircuit contains all gates which precede :math:`G`, and :math:`G` is replaced by the variance value of its generator. Args: args (tuple): Represent the free parameters passed to the circuit. Here we are not concerned with their values, but with their structure. Each free param is replaced with a :class:`~.variable.Variable` instance. Keyword Args: diag_approx (bool): If ``True``, forces the diagonal approximation. Default is ``False``. .. note:: Additional keyword arguments may be passed to the quantum circuit function, however PennyLane does not support differentiating with respect to keyword arguments. Instead, keyword arguments are useful for providing data or 'placeholders' to the quantum circuit function. """ # pylint: disable=too-many-statements diag_approx = kwargs.pop("diag_approx", False) if not self.ops or not self.cache: # construct the circuit self.construct(args, kwargs) for queue, curr_ops, param_idx, _ in self.circuit.iterate_layers(): obs = [] scale = [] Ki_matrices = [] KiKj_matrices = [] Ki_ev = [] KiKj_ev = [] V = None # for each operator, get the generator # and convert it to a variance for n, op in enumerate(curr_ops): gen, s = op.generator w = op.wires if gen is None: raise QuantumFunctionError("Can't generate metric tensor, operation {}" "has no defined generator".format(op)) # get the observable corresponding # to the generator of the current operation if isinstance(gen, np.ndarray): # generator is a Hermitian matrix variance = qml.var(qml.Hermitian(gen, w)) if not diag_approx: Ki_matrices.append((n, expand(gen, w, self.num_wires))) elif issubclass(gen, qml.operation.Observable): # generator is an existing PennyLane operation variance = qml.var(gen(w)) if not diag_approx: if issubclass(gen, qml.ops.PauliX): mat = np.array([[0, 1], [1, 0]]) elif issubclass(gen, qml.ops.PauliY): mat = np.array([[0, -1j], [1j, 0]]) elif issubclass(gen, qml.ops.PauliZ): mat = np.array([[1, 0], [0, -1]]) Ki_matrices.append((n, expand(mat, w, self.num_wires))) else: raise QuantumFunctionError( "Can't generate metric tensor, generator {}" "has no corresponding observable".format(gen) ) obs.append(variance) scale.append(s) if not diag_approx: # In order to compute the block diagonal portion of the metric tensor, # we need to compute 'second order' <psi|K_i K_j|psi> terms. for i, j in itertools.product(range(len(Ki_matrices)), repeat=2): # compute the matrices representing all K_i K_j terms obs1 = Ki_matrices[i] obs2 = Ki_matrices[j] KiKj_matrices.append(((obs1[0], obs2[0]), obs1[1] @ obs2[1])) V = np.identity(2**self.num_wires, dtype=np.complex128) # generate the unitary operation to rotate to # the shared eigenbasis of all observables for _, term in Ki_matrices: _, S = linalg.eigh(V.conj().T @ term @ V) V = np.round(V @ S, 15) V = V.conj().T # calculate the eigenvalues for # each observable in the shared eigenbasis for idx, term in Ki_matrices: eigs = np.diag(V @ term @ V.conj().T).real Ki_ev.append((idx, eigs)) for idx, term in KiKj_matrices: eigs = np.diag(V @ term @ V.conj().T).real KiKj_ev.append((idx, eigs)) self._metric_tensor_subcircuits[tuple(param_idx)] = { "queue": queue, "observable": obs, "Ki_expectations": Ki_ev, "KiKj_expectations": KiKj_ev, "eigenbasis_matrix": V, "result": None, "scale": scale, }