def test_row_eliminate(): """ Test elemination of element in U[i, j] by rotating in i-1 and i. """ dim = 3 u_generator = numpy.random.random((dim, dim)) + 1j * numpy.random.random( (dim, dim)) u_generator = u_generator - numpy.conj(u_generator).T # make sure the generator is actually antihermitian assert numpy.allclose(-1 * u_generator, numpy.conj(u_generator).T) unitary = scipy.linalg.expm(u_generator) # eliminate U[2, 0] by rotating in 1, 2 gmat = givens_matrix_elements(unitary[1, 0], unitary[2, 0], which='right') givens_rotate(unitary, gmat, 1, 2, which='row') assert numpy.isclose(unitary[2, 0], 0.0) # eliminate U[1, 0] by rotating in 0, 1 gmat = givens_matrix_elements(unitary[0, 0], unitary[1, 0], which='right') givens_rotate(unitary, gmat, 0, 1, which='row') assert numpy.isclose(unitary[1, 0], 0.0) # eliminate U[2, 1] by rotating in 1, 2 gmat = givens_matrix_elements(unitary[1, 1], unitary[2, 1], which='right') givens_rotate(unitary, gmat, 1, 2, which='row') assert numpy.isclose(unitary[2, 1], 0.0)
def test_col_eliminate(): """ Test elimination by rotating in the column space. Left multiplication of inverse givens """ dim = 3 u_generator = numpy.random.random((dim, dim)) + 1j * numpy.random.random( (dim, dim)) u_generator = u_generator - numpy.conj(u_generator).T # make sure the generator is actually antihermitian assert numpy.allclose(-1 * u_generator, numpy.conj(u_generator).T) unitary = scipy.linalg.expm(u_generator) # eliminate U[1, 0] by rotation in rows [0, 1] and # mixing U[1, 0] and U[0, 0] unitary_original = unitary.copy() gmat = givens_matrix_elements(unitary[0, 0], unitary[1, 0], which='right') vec = numpy.array([[unitary[0, 0]], [unitary[1, 0]]]) fullgmat = create_givens(gmat, 0, 1, 3) zeroed_unitary = fullgmat.dot(unitary) givens_rotate(unitary, gmat, 0, 1) assert numpy.isclose(unitary[1, 0], 0.0) assert numpy.allclose(unitary.real, zeroed_unitary.real) assert numpy.allclose(unitary.imag, zeroed_unitary.imag) # eliminate U[2, 0] by rotating columns [0, 1] and # mixing U[2, 0] and U[2, 1]. unitary = unitary_original.copy() gmat = givens_matrix_elements(unitary[2, 0], unitary[2, 1], which='left') vec = numpy.array([[unitary[2, 0]], [unitary[2, 1]]]) assert numpy.isclose((gmat.dot(vec))[0, 0], 0.0) assert numpy.isclose((vec.T.dot(gmat.T))[0, 0], 0.0) fullgmat = create_givens(gmat, 0, 1, 3) zeroed_unitary = unitary.dot(fullgmat.T) # because col takes g[0, 0] * col_i + g[0, 1].conj() * col_j -> col_i # this is equivalent ot left multiplication by gmat.T givens_rotate(unitary, gmat.conj(), 0, 1, which='col') assert numpy.isclose(zeroed_unitary[2, 0], 0.0) assert numpy.allclose(unitary, zeroed_unitary)
def test_givens_inverse(): r""" The Givens rotation in OpenFermion is defined as $$ \begin{pmatrix} \cos(\theta) & -e^{i \varphi} \sin(\theta) \\ \sin(\theta) & e^{i \varphi} \cos(\theta) \end{pmatrix}. $$ confirm numerically its hermitian conjugate is it's inverse """ a = numpy.random.random() + 1j * numpy.random.random() b = numpy.random.random() + 1j * numpy.random.random() ab_rotation = givens_matrix_elements(a, b, which='right') assert numpy.allclose(ab_rotation.dot(numpy.conj(ab_rotation).T), numpy.eye(2)) assert numpy.allclose( numpy.conj(ab_rotation).T.dot(ab_rotation), numpy.eye(2))
def optimal_givens_decomposition( qubits: Sequence[cirq.Qid], unitary: numpy.ndarray) -> Iterable[cirq.Operation]: r""" Implement a circuit that provides the unitary that is generated by single-particle fermion generators .. math:: U(v) = exp(log(v)_{p,q}(a_{p}^{\dagger}a_{q} - a_{q}^{\dagger}a_{p}) This can be used for implementing an exact single-body basis rotation Args: qubits: Sequence of qubits to apply the operations over. The qubits should be ordered in linear physical order. unitary: """ N = unitary.shape[0] right_rotations = [] left_rotations = [] for i in range(1, N): if i % 2 == 1: for j in range(0, i): # eliminate U[N - j, i - j] by mixing U[N - j, i - j], # U[N - j, i - j - 1] by right multiplication # of a givens rotation matrix in column [i - j, i - j + 1] gmat = givens_matrix_elements(unitary[N - j - 1, i - j - 1], unitary[N - j - 1, i - j - 1 + 1], which='left') right_rotations.append((gmat.T, (i - j - 1, i - j))) givens_rotate(unitary, gmat.conj(), i - j - 1, i - j, which='col') else: for j in range(1, i + 1): # elimination of U[N + j - i, j] by mixing U[N + j - i, j] and # U[N + j - i - 1, j] by left multiplication # of a givens rotation that rotates row space # [N + j - i - 1, N + j - i gmat = givens_matrix_elements(unitary[N + j - i - 1 - 1, j - 1], unitary[N + j - i - 1, j - 1], which='right') left_rotations.append((gmat, (N + j - i - 2, N + j - i - 1))) givens_rotate(unitary, gmat, N + j - i - 2, N + j - i - 1, which='row') new_left_rotations = [] for (left_gmat, (i, j)) in reversed(left_rotations): phase_matrix = numpy.diag([unitary[i, i], unitary[j, j]]) matrix_to_decompose = left_gmat.conj().T.dot(phase_matrix) new_givens_matrix = givens_matrix_elements(matrix_to_decompose[1, 0], matrix_to_decompose[1, 1], which='left') new_phase_matrix = matrix_to_decompose.dot(new_givens_matrix.T) # check if T_{m,n}^{-1}D = D T. # coverage: ignore if not numpy.allclose(new_phase_matrix.dot(new_givens_matrix.conj()), matrix_to_decompose): raise GivensTranspositionError("Failed to shift the phase matrix " "from right to left") # coverage: ignore unitary[i, i], unitary[j, j] = new_phase_matrix[0, 0], new_phase_matrix[1, 1] new_left_rotations.append((new_givens_matrix.conj(), (i, j))) phases = numpy.diag(unitary) rotations = [] ordered_rotations = [] for (gmat, (i, j)) in list(reversed(new_left_rotations)) + list( map(lambda x: (x[0].conj().T, x[1]), reversed(right_rotations))): ordered_rotations.append((gmat, (i, j))) # if this throws the impossible has happened # coverage: ignore if not numpy.isclose(gmat[0, 0].imag, 0.0): raise GivensMatrixError( "Givens matrix does not obey our convention that all elements " "in the first column are real") if not numpy.isclose(gmat[1, 0].imag, 0.0): raise GivensMatrixError( "Givens matrix does not obey our convention that all elements " "in the first column are real") # coverage: ignore theta = numpy.arcsin(numpy.real(gmat[1, 0])) phi = numpy.angle(gmat[1, 1]) rotations.append((i, j, theta, phi)) for op in reversed(rotations): i, j, theta, phi = cast(Tuple[int, int, float, float], op) if not numpy.isclose(phi, 0.0): yield cirq.Z(qubits[j])**(phi / numpy.pi) yield Ryxxy(-theta).on(qubits[i], qubits[j]) for idx, phase in enumerate(phases): yield cirq.Z(qubits[idx])**(numpy.angle(phase) / numpy.pi)
def gaussian_givens_decomposition( qubits: Sequence[cirq.Qid], orthogonal_matrix: numpy.ndarray) -> Iterable[cirq.Operation]: r""" Implement a circuit that provides the unitary that is generated by quadratic fermion generators .. math:: U(Q) = exp(-(1/4) [log(Q)]_{j,k} \gamma_j \gamma_k). This can be used for implementing an exact fermionic Gaussian unitary from any orthogonal 2n x 2n matrix Q. Args: qubits: Sequence of qubits to apply the operations over. The qubits should be ordered in linear physical order. orthogonal_matrix: 2n x 2n orthogonal matrix which defines the linear transformation, .. math:: U(Q)^\dagger \gamma_j U(Q) = \sum_{k=0}^{2n-1} Q_{j,k} \gamma_k. """ N = orthogonal_matrix.shape[0] assert N % 2 == 0 current_matrix = numpy.copy(orthogonal_matrix) right_rotations = [] left_rotations = [] for i in range(1, N): if i % 2 == 1: for j in range(0, i): # eliminate U[N - j, i - j] by mixing U[N - j, i - j], # U[N - j, i - j - 1] by right multiplication # of a givens rotation matrix in column [i - j, i - j + 1] gmat = givens_matrix_elements(current_matrix[N - j - 1, i - j - 1], current_matrix[N - j - 1, i - j - 1 + 1], which='left') right_rotations.append((gmat.T, (i - j - 1, i - j))) givens_rotate(current_matrix, gmat.conj(), i - j - 1, i - j, which='col') else: for j in range(1, i + 1): # elimination of U[N + j - i, j] by mixing U[N + j - i, j] and # U[N + j - i - 1, j] by left multiplication # of a givens rotation that rotates row space # [N + j - i - 1, N + j - i gmat = givens_matrix_elements(current_matrix[N + j - i - 1 - 1, j - 1], current_matrix[N + j - i - 1, j - 1], which='right') left_rotations.append((gmat, (N + j - i - 2, N + j - i - 1))) givens_rotate(current_matrix, gmat, N + j - i - 2, N + j - i - 1, which='row') new_left_rotations = [] for (left_gmat, (i, j)) in reversed(left_rotations): phase_matrix = numpy.diag([current_matrix[i, i], current_matrix[j, j]]) matrix_to_decompose = left_gmat.conj().T.dot(phase_matrix) new_givens_matrix = givens_matrix_elements(matrix_to_decompose[1, 0], matrix_to_decompose[1, 1], which='left') new_phase_matrix = matrix_to_decompose.dot(new_givens_matrix.T) # check if T_{m,n}^{-1}D = D T. # coverage: ignore if not numpy.allclose(new_phase_matrix.dot(new_givens_matrix.conj()), matrix_to_decompose): raise GivensTranspositionError("Failed to shift the phase matrix " "from right to left") # coverage: ignore current_matrix[i, i], current_matrix[j, j] = (new_phase_matrix[0, 0], new_phase_matrix[1, 1]) new_left_rotations.append((new_givens_matrix.conj(), (i, j))) phases = numpy.diag(current_matrix) rotations = [] ordered_rotations = [] for (gmat, (i, j)) in list(reversed(new_left_rotations)) + list( map(lambda x: (x[0].conj().T, x[1]), reversed(right_rotations))): ordered_rotations.append((gmat, (i, j))) """Commented out because orthogonal matrices must be real, so this "impossible" happening is impossibly impossible. # if this throws the impossible has happened # coverage: ignore if not numpy.isclose(gmat[0, 0].imag, 0.0): raise GivensMatrixError( "Givens matrix does not obey our convention that all elements " "in the first column are real") if not numpy.isclose(gmat[1, 0].imag, 0.0): raise GivensMatrixError( "Givens matrix does not obey our convention that all elements " "in the first column are real") # coverage: ignore """ theta = numpy.arcsin(numpy.real(gmat[1, 0])) rotations.append((i, j, theta)) for op in reversed(rotations): i, j, theta = cast(Tuple[int, int, float], op) if not numpy.isclose(theta, 0.0): if i % 2 == 0: yield cirq.Z(qubits[i // 2])**(theta / numpy.pi) else: p = (i - 1) // 2 yield cirq.ops.XXPowGate(exponent=theta / numpy.pi).on( qubits[p], qubits[p + 1]) # final layer of Pauli gates to implement phases, which must be \pm 1 since # orthogonal matrices are real # we use the F_2n/stabilizer representation of Pauli operators here final_pauli_gate = numpy.zeros(N) n = N // 2 for p in range(n): if numpy.isclose(phases[2 * p], phases[2 * p + 1]): if numpy.isclose(phases[2 * p], -1.0): final_pauli_gate[p + n] += 1 else: final_pauli_gate[p] += 1 if numpy.isclose(phases[2 * p], 1.0): for q in range(p + 1, n): final_pauli_gate[q + n] += 1 else: for q in range(p, n): final_pauli_gate[q + n] += 1 for p in range(n): if final_pauli_gate[p] % 2 == 0 and final_pauli_gate[p + n] % 2 == 1: yield cirq.Z(qubits[p]) elif final_pauli_gate[p] % 2 == 1 and final_pauli_gate[p + n] % 2 == 0: yield cirq.X(qubits[p]) elif final_pauli_gate[p] % 2 == 1 and final_pauli_gate[p + n] % 2 == 1: yield cirq.Y(qubits[p])