def negate(k1, k2): v[k1] *= -1 v[k2] *= -1 phase[0] *= -1 s = flippers[3 - k1 - k2] # The other axis' flipper. left[1] = combinators.dot(left[1], s) right[1] = combinators.dot(s, right[1])
def swap(k1, k2): v[k1], v[k2] = v[k2], v[k1] s = swappers[3 - k1 - k2] # The other axis' swapper. left[0] = combinators.dot(left[0], s) left[1] = combinators.dot(left[1], s) right[0] = combinators.dot(s, right[0]) right[1] = combinators.dot(s, right[1])
def negate(k1, k2): v[k1] *= -1 v[k2] *= -1 phase[0] *= -1 s = flippers[3 - k1 - k2] # The other axis' flipper. left[1] = combinators.dot(left[1], s) right[1] = combinators.dot(s, right[1])
def swap(k1, k2): v[k1], v[k2] = v[k2], v[k1] s = swappers[3 - k1 - k2] # The other axis' swapper. left[0] = combinators.dot(left[0], s) left[1] = combinators.dot(left[1], s) right[0] = combinators.dot(s, right[0]) right[1] = combinators.dot(s, right[1])
def random_bi_diagonalizable_pair( n: int, d1: Optional[int] = None, d2: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray]: u = testing.random_orthogonal(n) s = random_real_diagonal_matrix(n, d1) z = random_real_diagonal_matrix(n, d2) v = testing.random_orthogonal(n) a = combinators.dot(u, s, v) b = combinators.dot(u, z, v) return a, b
def so4_to_magic_su2s( mat: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True ) -> Tuple[np.ndarray, np.ndarray]: """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag. Mag is the magic basis matrix: 1 0 0 i 0 i 1 0 0 i -1 0 (times sqrt(0.5) to normalize) 1 0 0 -i Args: mat: A real 4x4 orthogonal matrix. rtol: Per-matrix-entry relative tolerance on equality. atol: Per-matrix-entry absolute tolerance on equality. check_preconditions: When set, the code verifies that the given matrix is from SO(4). Defaults to set. Returns: A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag is approximately equal to the given matrix. Raises: ValueError: Bad matrix. """ if check_preconditions: if mat.shape != (4, 4) or not predicates.is_special_orthogonal(mat, atol=atol, rtol=rtol): raise ValueError('mat must be 4x4 special orthogonal.') ab = combinators.dot(MAGIC, mat, MAGIC_CONJ_T) _, a, b = kron_factor_4x4_to_2x2s(ab) return a, b
def so4_to_magic_su2s( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.ndarray]: """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag. Mag is the magic basis matrix: 1 0 0 i 0 i 1 0 0 i -1 0 (times sqrt(0.5) to normalize) 1 0 0 -i Args: mat: A real 4x4 orthogonal matrix. tolerance: Per-matrix-entry tolerance on equality. Returns: A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag is approximately equal to the given matrix. Raises: ValueError: Bad matrix. """ if mat.shape != (4, 4) or not predicates.is_special_orthogonal( mat, tolerance): raise ValueError('mat must be 4x4 special orthogonal.') ab = combinators.dot(MAGIC, mat, MAGIC_CONJ_T) _, a, b = kron_factor_4x4_to_2x2s(ab, tolerance) return a, b
def kak_decomposition( mat: np.ndarray, rtol: float = DEFAULT_RTOL, atol: float = DEFAULT_ATOL) -> KakDecomposition: """Decomposes a 2-qubit unitary into 1-qubit ops and XX/YY/ZZ interactions. Args: mat: The 4x4 unitary matrix to decompose. rtol: Per-matrix-entry relative tolerance on equality. atol: Per-matrix-entry absolute tolerance on equality. Returns: A `cirq.KakDecomposition` canonicalized such that the interaction coefficients x, y, z satisfy: 0 ≤ abs(z) ≤ y ≤ x ≤ π/4 z ≠ -π/4 Raises: ValueError: Bad matrix. ArithmeticError: Failed to perform the decomposition. References: 'An Introduction to Cartan's KAK Decomposition for QC Programmers' https://arxiv.org/abs/quant-ph/0507171 """ magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) gamma = np.array([[1, 1, 1, 1], [1, 1, -1, -1], [-1, 1, -1, 1], [1, -1, -1, 1]]) * 0.25 # Diagonalize in magic basis. left, d, right = ( diagonalize.bidiagonalize_unitary_with_special_orthogonals( combinators.dot(np.conj(magic.T), mat, magic), atol=atol, rtol=rtol)) # Recover pieces. a1, a0 = so4_to_magic_su2s(left.T, atol=atol, rtol=rtol) b1, b0 = so4_to_magic_su2s(right.T, atol=atol, rtol=rtol) w, x, y, z = gamma.dot(np.vstack(np.angle(d))).flatten() g = np.exp(1j * w) # Canonicalize. inner_cannon = kak_canonicalize_vector(x, y, z) b1 = np.dot(inner_cannon.single_qubit_operations_before[0], b1) b0 = np.dot(inner_cannon.single_qubit_operations_before[1], b0) a1 = np.dot(a1, inner_cannon.single_qubit_operations_after[0]) a0 = np.dot(a0, inner_cannon.single_qubit_operations_after[1]) return KakDecomposition( interaction_coefficients=inner_cannon.interaction_coefficients, global_phase=g * inner_cannon.global_phase, single_qubit_operations_before=(b1, b0), single_qubit_operations_after=(a1, a0))
def so4_to_magic_su2s( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.ndarray]: """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag. Mag is the magic basis matrix: 1 0 0 i 0 i 1 0 0 i -1 0 (times sqrt(0.5) to normalize) 1 0 0 -i Args: mat: A real 4x4 orthogonal matrix. tolerance: Per-matrix-entry tolerance on equality. Returns: A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag is approximately equal to the given matrix. Raises: ValueError: Bad matrix. ArithmeticError: Failed to perform the decomposition to desired tolerance. """ if mat.shape != (4, 4) or not predicates.is_special_orthogonal(mat, tolerance): raise ValueError('mat must be 4x4 special orthogonal.') magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) ab = combinators.dot(magic, mat, np.conj(magic.T)) _, a, b = kron_factor_4x4_to_2x2s(ab, tolerance) # Check decomposition against desired tolerance. reconstructed = combinators.dot(np.conj(magic.T), combinators.kron(a, b), magic) if not tolerance.all_close(reconstructed, mat): raise ArithmeticError('Failed to decompose to desired tolerance.') return a, b
def so4_to_magic_su2s( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.ndarray]: """Finds 2x2 special-unitaries A, B where mat = Mag.H @ kron(A, B) @ Mag. Mag is the magic basis matrix: 1 0 0 i 0 i 1 0 0 i -1 0 (times sqrt(0.5) to normalize) 1 0 0 -i Args: mat: A real 4x4 orthogonal matrix. tolerance: Per-matrix-entry tolerance on equality. Returns: A pair (A, B) of matrices in SU(2) such that Mag.H @ kron(A, B) @ Mag is approximately equal to the given matrix. Raises: ValueError: Bad matrix. ArithmeticError: Failed to perform the decomposition to desired tolerance. """ if mat.shape != (4, 4) or not predicates.is_special_orthogonal(mat, tolerance): raise ValueError('mat must be 4x4 special orthogonal.') magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) ab = combinators.dot(magic, mat, np.conj(magic.T)) _, a, b = kron_factor_4x4_to_2x2s(ab, tolerance) # Check decomposition against desired tolerance. reconstructed = combinators.dot(np.conj(magic.T), combinators.kron(a, b), magic) if not tolerance.all_close(reconstructed, mat): raise ArithmeticError('Failed to decompose to desired tolerance.') return a, b
def kak_decomposition( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[complex, Tuple[np.ndarray, np.ndarray], Tuple[float, float, float], Tuple[np.ndarray, np.ndarray]]: """Decomposes a 2-qubit unitary into 1-qubit ops and XX/YY/ZZ interactions. Args: mat: The 4x4 unitary matrix to decompose. tolerance: Per-matrix-entry tolerance on equality. Returns: A nested tuple (g, (a1, a0), (x, y, z), (b1, b0)) containing: 0. A global phase factor. 1. The pre-operation matrices to apply to the second/firs qubit. 2. The XX/YY/ZZ weights of the non-local operation. 3. The post-operation matrices to apply to the second/firs qubit. Guarantees that the x2, y2, z2 are canonicalized to satisfy: 0 ≤ abs(z) ≤ y ≤ x ≤ π/4 z ≠ -π/4 Guarantees that the input matrix should approximately equal: g · (a1 ⊗ a0) · exp(i·(x·XX + y·YY + z·ZZ)) · (b1 ⊗ b0) Raises: ValueError: Bad matrix. ArithmeticError: Failed to perform the decomposition. References: 'An Introduction to Cartan's KAK Decomposition for QC Programmers' https://arxiv.org/abs/quant-ph/0507171 """ magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) gamma = np.array([[1, 1, 1, 1], [1, 1, -1, -1], [-1, 1, -1, 1], [1, -1, -1, 1]]) * 0.25 # Diagonalize in magic basis. left, d, right = ( diagonalize.bidiagonalize_unitary_with_special_orthogonals( combinators.dot(np.conj(magic.T), mat, magic), tolerance)) # Recover pieces. a1, a0 = so4_to_magic_su2s(left.T, tolerance) b1, b0 = so4_to_magic_su2s(right.T, tolerance) w, x, y, z = gamma.dot(np.vstack(np.angle(d))).flatten() g = np.exp(1j * w) # Canonicalize. g2, (c1, c0), (x2, y2, z2), (d1, d0) = kak_canonicalize_vector(x, y, z) return (g * g2, (a1.dot(c1), a0.dot(c0)), (x2, y2, z2), (d1.dot(b1), d0.dot(b0)))
def recompose_so4(a: np.ndarray, b: np.ndarray) -> np.ndarray: assert a.shape == (2, 2) assert b.shape == (2, 2) assert linalg.is_special_unitary(a) assert linalg.is_special_unitary(b) magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) result = np.real( combinators.dot(np.conj(magic.T), linalg.kron(a, b), magic)) assert linalg.is_orthogonal(result) return result
def bidiagonalize_unitary_with_special_orthogonals( mat: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True ) -> Tuple[np.ndarray, np.array, np.ndarray]: """Finds orthogonal matrices L, R such that L @ matrix @ R is diagonal. Args: mat: A unitary matrix. rtol: Relative numeric error threshold. atol: Absolute numeric error threshold. check_preconditions: If set, verifies that the input is a unitary matrix (to the given tolerances). Defaults to set. Returns: A triplet (L, d, R) such that L @ mat @ R = diag(d). Both L and R will be orthogonal matrices with determinant equal to 1. Raises: ValueError: Matrices don't meet preconditions (e.g. not real). """ if check_preconditions: if not predicates.is_unitary(mat, rtol=rtol, atol=atol): raise ValueError('matrix must be unitary.') # Note: Because mat is unitary, setting A = real(mat) and B = imag(mat) # guarantees that both A @ B.T and A.T @ B are Hermitian. left, right = bidiagonalize_real_matrix_pair_with_symmetric_products( np.real(mat), np.imag(mat), rtol=rtol, atol=atol, check_preconditions=check_preconditions) # Convert to special orthogonal w/o breaking diagonalization. if np.linalg.det(left) < 0: left[0, :] *= -1 if np.linalg.det(right) < 0: right[:, 0] *= -1 diag = combinators.dot(left, mat, right) return left, np.diag(diag), right
def _unitary_(self): """Returns the decomposition's two-qubit unitary matrix. U = g · (a1 ⊗ a0) · exp(i·(x·XX + y·YY + z·ZZ)) · (b1 ⊗ b0) """ before = np.kron(*self.single_qubit_operations_before) after = np.kron(*self.single_qubit_operations_after) def interaction_matrix(m: np.ndarray, c: float) -> np.ndarray: return map_eigenvalues(np.kron(m, m), lambda v: np.exp(1j * v * c)) x, y, z = self.interaction_coefficients x_mat = np.array([[0, 1], [1, 0]]) y_mat = np.array([[0, -1j], [1j, 0]]) z_mat = np.array([[1, 0], [0, -1]]) return self.global_phase * combinators.dot( after, interaction_matrix(z_mat, z), interaction_matrix(y_mat, y), interaction_matrix(x_mat, x), before)
def bidiagonalize_unitary_with_special_orthogonals( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.array, np.ndarray]: """Finds orthogonal matrices L, R such that L @ matrix @ R is diagonal. Args: mat: A unitary matrix. tolerance: Numeric error thresholds. Returns: A triplet (L, d, R) such that L @ mat @ R = diag(d). Both L and R will be orthogonal matrices with determinant equal to 1. Raises: ValueError: Matrices don't meet preconditions (e.g. not real). ArithmeticError: Failed to meet specified tolerance. """ if not predicates.is_unitary(mat, tolerance): raise ValueError('matrix must be unitary.') # Note: Because mat is unitary, setting A = real(mat) and B = imag(mat) # guarantees that both A @ B.T and A.T @ B are Hermitian. left, right = bidiagonalize_real_matrix_pair_with_symmetric_products( np.real(mat), np.imag(mat), tolerance) # Convert to special orthogonal w/o breaking diagonalization. if np.linalg.det(left) < 0: left[0, :] *= -1 if np.linalg.det(right) < 0: right[:, 0] *= -1 diag = combinators.dot(left, mat, right) # Check acceptability vs tolerances. if not predicates.is_diagonal(diag, tolerance): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return left, np.diag(diag), right
def bidiagonalize_unitary_with_special_orthogonals( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[np.ndarray, np.array, np.ndarray]: """Finds orthogonal matrices L, R such that L @ matrix @ R is diagonal. Args: mat: A unitary matrix. tolerance: Numeric error thresholds. Returns: A triplet (L, d, R) such that L @ mat @ R = diag(d). Both L and R will be orthogonal matrices with determinant equal to 1. Raises: ValueError: Matrices don't meet preconditions (e.g. not real). ArithmeticError: Failed to meet specified tolerance. """ if not predicates.is_unitary(mat, tolerance): raise ValueError('matrix must be unitary.') # Note: Because mat is unitary, setting A = real(mat) and B = imag(mat) # guarantees that both A @ B.T and A.T @ B are Hermitian. left, right = bidiagonalize_real_matrix_pair_with_symmetric_products( np.real(mat), np.imag(mat), tolerance) # Convert to special orthogonal w/o breaking diagonalization. if np.linalg.det(left) < 0: left[0, :] *= -1 if np.linalg.det(right) < 0: right[:, 0] *= -1 diag = combinators.dot(left, mat, right) # Check acceptability vs tolerances. if not predicates.is_diagonal(diag, tolerance): raise ArithmeticError('Failed to diagonalize to specified tolerance.') return left, np.diag(diag), right
def bidiagonalize_real_matrix_pair_with_symmetric_products( mat1: np.ndarray, mat2: np.ndarray, *, rtol: float = 1e-5, atol: float = 1e-8, check_preconditions: bool = True, ) -> Tuple[np.ndarray, np.ndarray]: """Finds orthogonal matrices that diagonalize both mat1 and mat2. Requires mat1 and mat2 to be real. Requires mat1.T @ mat2 to be symmetric. Requires mat1 @ mat2.T to be symmetric. Args: mat1: One of the real matrices. mat2: The other real matrix. rtol: Relative numeric error threshold. atol: Absolute numeric error threshold. check_preconditions: If set, verifies that the inputs are real, and that mat1.T @ mat2 and mat1 @ mat2.T are both symmetric. Defaults to set. Returns: A tuple (L, R) of two orthogonal matrices, such that both L @ mat1 @ R and L @ mat2 @ R are diagonal matrices. Raises: ValueError: Matrices don't meet preconditions (e.g. not real). """ if check_preconditions: if np.any(np.imag(mat1) != 0): raise ValueError('mat1 must be real.') if np.any(np.imag(mat2) != 0): raise ValueError('mat2 must be real.') if not predicates.is_hermitian( np.dot(mat1, mat2.T), rtol=rtol, atol=atol): raise ValueError('mat1 @ mat2.T must be symmetric.') if not predicates.is_hermitian( np.dot(mat1.T, mat2), rtol=rtol, atol=atol): raise ValueError('mat1.T @ mat2 must be symmetric.') # Use SVD to bi-diagonalize the first matrix. base_left, base_diag, base_right = _svd_handling_empty(np.real(mat1)) base_diag = np.diag(base_diag) # Determine where we switch between diagonalization-fixup strategies. dim = base_diag.shape[0] rank = dim while rank > 0 and tolerance.all_near_zero(base_diag[rank - 1, rank - 1], atol=atol): rank -= 1 base_diag = base_diag[:rank, :rank] # Try diagonalizing the second matrix with the same factors as the first. semi_corrected = combinators.dot(base_left.T, np.real(mat2), base_right.T) # Fix up the part of the second matrix's diagonalization that's matched # against non-zero diagonal entries in the first matrix's diagonalization # by performing simultaneous diagonalization. overlap = semi_corrected[:rank, :rank] overlap_adjust = diagonalize_real_symmetric_and_sorted_diagonal_matrices( overlap, base_diag, rtol=rtol, atol=atol, check_preconditions=check_preconditions) # Fix up the part of the second matrix's diagonalization that's matched # against zeros in the first matrix's diagonalization by performing an SVD. extra = semi_corrected[rank:, rank:] extra_left_adjust, _, extra_right_adjust = _svd_handling_empty(extra) # Merge the fixup factors into the initial diagonalization. left_adjust = combinators.block_diag(overlap_adjust, extra_left_adjust) right_adjust = combinators.block_diag(overlap_adjust.T, extra_right_adjust) left = np.dot(left_adjust.T, base_left.T) right = np.dot(base_right.T, right_adjust.T) return left, right
def shift(k, step): v[k] += step * np.pi / 2 phase[0] *= 1j**step right[0] = combinators.dot(flippers[k]**(step % 4), right[0]) right[1] = combinators.dot(flippers[k]**(step % 4), right[1])
def kak_decomposition( mat: np.ndarray, tolerance: Tolerance = Tolerance.DEFAULT ) -> Tuple[complex, Tuple[np.ndarray, np.ndarray], Tuple[float, float, float], Tuple[np.ndarray, np.ndarray]]: """Decomposes a 2-qubit unitary into 1-qubit ops and XX/YY/ZZ interactions. Args: mat: The 4x4 unitary matrix to decompose. tolerance: Per-matrix-entry tolerance on equality. Returns: A nested tuple (g, (a1, a0), (x, y, z), (b1, b0)) containing: 0. A global phase factor. 1. The pre-operation matrices to apply to the second/firs qubit. 2. The XX/YY/ZZ weights of the non-local operation. 3. The post-operation matrices to apply to the second/firs qubit. Guarantees that the x2, y2, z2 are canonicalized to satisfy: 0 ≤ abs(z) ≤ y ≤ x ≤ π/4 z ≠ -π/4 Guarantees that the input matrix should approximately equal: g · (a1 ⊗ a0) · exp(i·(x·XX + y·YY + z·ZZ)) · (b1 ⊗ b0) Raises: ValueError: Bad matrix. ArithmeticError: Failed to perform the decomposition. References: 'An Introduction to Cartan's KAK Decomposition for QC Programmers' https://arxiv.org/abs/quant-ph/0507171 """ magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) gamma = np.array([[1, 1, 1, 1], [1, 1, -1, -1], [-1, 1, -1, 1], [1, -1, -1, 1]]) * 0.25 # Diagonalize in magic basis. left, d, right = ( diagonalize.bidiagonalize_unitary_with_special_orthogonals( combinators.dot(np.conj(magic.T), mat, magic), tolerance)) # Recover pieces. a1, a0 = so4_to_magic_su2s(left.T, tolerance) b1, b0 = so4_to_magic_su2s(right.T, tolerance) w, x, y, z = gamma.dot(np.vstack(np.angle(d))).flatten() g = np.exp(1j * w) # Canonicalize. g2, (c1, c0), (x2, y2, z2), (d1, d0) = kak_canonicalize_vector(x, y, z) return ( g * g2, (a1.dot(c1), a0.dot(c0)), (x2, y2, z2), (d1.dot(b1), d0.dot(b0)) )
def shift(k, step): v[k] += step * np.pi / 2 phase[0] *= 1j**step right[0] = combinators.dot(flippers[k]**(step % 4), right[0]) right[1] = combinators.dot(flippers[k]**(step % 4), right[1])