From 2132a996e15c376112075d7738abc3712169f0eb Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Tue, 10 Dec 2024 19:36:57 -0600 Subject: [PATCH 1/2] remove linalg --- qbraid_qir/qasm3/convert.py | 11 +- qbraid_qir/qasm3/linalg.py | 324 --------------------------------- qbraid_qir/qasm3/maps.py | 37 ++-- tests/qasm3_qir/test_linalg.py | 97 ---------- 4 files changed, 24 insertions(+), 445 deletions(-) delete mode 100644 qbraid_qir/qasm3/linalg.py delete mode 100644 tests/qasm3_qir/test_linalg.py diff --git a/qbraid_qir/qasm3/convert.py b/qbraid_qir/qasm3/convert.py index 294a65f..a63c063 100644 --- a/qbraid_qir/qasm3/convert.py +++ b/qbraid_qir/qasm3/convert.py @@ -26,6 +26,7 @@ def qasm3_to_qir( program: Union[openqasm3.ast.Program, str], name: Optional[str] = None, + external_gates: Optional[list[str]] = None, **kwargs, ) -> Module: """Converts an OpenQASM 3 program to a PyQIR module. @@ -33,11 +34,13 @@ def qasm3_to_qir( Args: program (openqasm3.ast.Program or str): The OpenQASM 3 program to convert. name (str, optional): Identifier for created QIR module. Auto-generated if not provided. + external_gates (list[str], optional): A list of custom gate names that are not natively + recognized by pyqasm but should be treated as valid during program unrolling. Keyword Args: initialize_runtime (bool): Whether to perform quantum runtime environment initialization, - default `True`. - record_output (bool): Whether to record output calls for registers, default `True` + defaults to `True`. + record_output (bool): Whether to record output calls for registers, defaults to `True`. Returns: The QIR ``pyqir.Module`` representation of the input OpenQASM 3 program. @@ -52,8 +55,6 @@ def qasm3_to_qir( elif not isinstance(program, str): raise TypeError("Input quantum program must be of type openqasm3.ast.Program or str.") - external_gates: list[str] = kwargs.get("external_gates", []) - qasm3_module = pyqasm.loads(program) qasm3_module.unroll(external_gates=external_gates) if name is None: @@ -62,7 +63,7 @@ def qasm3_to_qir( final_module = QasmQIRModule(name, qasm3_module, llvm_module) - visitor = QasmQIRVisitor(**kwargs) + visitor = QasmQIRVisitor(external_gates=external_gates, **kwargs) final_module.accept(visitor) err = llvm_module.verify() diff --git a/qbraid_qir/qasm3/linalg.py b/qbraid_qir/qasm3/linalg.py deleted file mode 100644 index a85de13..0000000 --- a/qbraid_qir/qasm3/linalg.py +++ /dev/null @@ -1,324 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of qbraid-qir -# -# Qbraid-qir is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for qbraid-qir, as per Section 15 of the GPL v3. - -# pylint: disable=too-many-locals - -""" -Module for linear algebra functions necessary for gate decomposition. - -""" -from __future__ import annotations - -import cmath -import functools -import math -from typing import TYPE_CHECKING - -import numpy as np - -if TYPE_CHECKING: - from numpy.typing import DTypeLike, NDArray - - -def _helper_svd( - mat: NDArray[np.float64], -) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: - """ - Helper function to perform SVD on a matrix. - """ - if mat.size == 0: - return np.zeros((0, 0), dtype=mat.dtype), np.array([]), np.zeros((0, 0), dtype=mat.dtype) - return np.linalg.svd(mat) - - -def _merge_dtypes(dtype1: DTypeLike, dtype2: DTypeLike) -> np.dtype: - return (np.zeros(0, dtype=dtype1) + np.zeros(0, dtype=dtype2)).dtype - - -def _block_diag(*blocks: NDArray[np.float64]) -> NDArray[np.float64]: - """ - Helper function to perform block diagonalization. - """ - n = sum(b.shape[0] for b in blocks) - dtype = functools.reduce(_merge_dtypes, (b.dtype for b in blocks)) - - result = np.zeros((n, n), dtype=dtype) - i = 0 - for b in blocks: - j = i + b.shape[0] - result[i:j, i:j] = b - i = j - - return result - - -def _orthogonal_diagonalize( - symmetric_matrix: NDArray[np.float64], diagonal_matrix: NDArray[np.float64] -) -> NDArray[np.float64]: - """ - Find orthogonal matrix that diagonalizes symmetric_matrix and diagonal_matrix. - """ - - def similar_singular(i: int, j: int) -> bool: - return np.allclose(diagonal_matrix[i, i], diagonal_matrix[j, j]) - - ranges = [] - start = 0 - while start < diagonal_matrix.shape[0]: - past = start + 1 - while past < diagonal_matrix.shape[0] and similar_singular(start, past): - past += 1 - ranges.append((start, past)) - start = past - - p = np.zeros(symmetric_matrix.shape, dtype=np.float64) - for start, end in ranges: - block = symmetric_matrix[start:end, start:end] - _, res = np.linalg.eigh(block) - p[start:end, start:end] = res - - return p - - -def orthogonal_bidiagonalize( - mat1: NDArray[np.float64], mat2: NDArray[np.float64] -) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """ - Find orthogonal matrices that diagonalize mat1 and mat2. - """ - atol = 1e-9 - base_left, base_diag, base_right = _helper_svd(mat1) - base_diag = np.diag(base_diag) - - dim = base_diag.shape[0] - rank = dim - while rank > 0 and np.all(np.less_equal(np.abs(base_diag[rank - 1, rank - 1]), atol)): - rank -= 1 - base_diag = base_diag[:rank, :rank] - - semi_corrected = np.dot(np.dot(base_left.T, np.real(mat2)), base_right.T) - - overlap = semi_corrected[:rank, :rank] - overlap_adjust = _orthogonal_diagonalize(overlap, base_diag) - - extra = semi_corrected[rank:, rank:] - extra_left_adjust, _, extra_right_adjust = _helper_svd(extra) - - left_adjust = _block_diag(overlap_adjust, extra_left_adjust) - right_adjust = _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 _kronecker_fator( - mat: NDArray[np.complex128], -) -> tuple[float, NDArray[np.complex128], NDArray[np.complex128]]: - """ - Split U = kron(A, B) to A and B. - """ - a, b = max(((i, j) for i in range(4) for j in range(4)), key=lambda t: abs(mat[t])) - - f1 = np.zeros((2, 2), dtype=mat.dtype) - f2 = np.zeros((2, 2), dtype=mat.dtype) - for i in range(2): - for j in range(2): - f1[(a >> 1) ^ i, (b >> 1) ^ j] = mat[a ^ (i << 1), b ^ (j << 1)] - f2[(a & 1) ^ i, (b & 1) ^ j] = mat[a ^ i, b ^ j] - - with np.errstate(divide="ignore", invalid="ignore"): - f1 /= np.sqrt(np.linalg.det(f1)) or 1 - f2 /= np.sqrt(np.linalg.det(f2)) or 1 - - g = mat[a, b] / (f1[a >> 1, b >> 1] * f2[a & 1, b & 1]) - if np.real(g) < 0: - f1 *= -1 - g *= -1 - - return g, f1, f2 - - -def _so4_to_su2( - mat: NDArray[np.complex128], -) -> tuple[NDArray[np.complex128], NDArray[np.complex128]]: - """ - Decompose SO(4) matrix to SU(2) matrices. - """ - magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt(0.5) - - magic_conj_t = np.conj(magic.T) - - ab = np.dot(np.dot(magic, mat), magic_conj_t) - _, a, b = _kronecker_fator(ab) - - return a, b - - -def _kak_canonicalize_vector( - x: float, y: float, z: float -) -> dict[str, tuple[NDArray[np.complex128], NDArray[np.complex128]]]: - """ - Canonicalize vector for KAK decomposition. - """ - phase = [complex(1)] - left = [np.eye(2, dtype=np.complex128)] * 2 - right = [np.eye(2, dtype=np.complex128)] * 2 - v = [x, y, z] - - flippers = [ - np.array([[0, 1], [1, 0]]) * 1j, - np.array([[0, -1j], [1j, 0]]) * 1j, - np.array([[1, 0], [0, -1]]) * 1j, - ] - - swappers = [ - np.array([[1, -1j], [1j, -1]]) * 1j * np.sqrt(0.5), - np.array([[1, 1], [1, -1]]) * 1j * np.sqrt(0.5), - np.array([[0, 1 - 1j], [1 + 1j, 0]]) * 1j * np.sqrt(0.5), - ] - - def shift(k: int, step: int) -> None: - v[k] += step * np.pi / 2 - phase[0] *= 1j**step - right[0] = np.dot(flippers[k] ** (step % 4), right[0]) - right[1] = np.dot(flippers[k] ** (step % 4), right[1]) - - def negate(k1: int, k2: int) -> None: - v[k1] *= -1 - v[k2] *= -1 - phase[0] *= -1 - s = flippers[3 - k1 - k2] - left[1] = np.dot(left[1], s) - right[1] = np.dot(s, right[1]) - - def swap(k1: int, k2: int) -> None: - v[k1], v[k2] = v[k2], v[k1] - s = swappers[3 - k1 - k2] - left[0] = np.dot(left[0], s) - left[1] = np.dot(left[1], s) - right[0] = np.dot(s, right[0]) - right[1] = np.dot(s, right[1]) - - def canonical_shift(k: int) -> None: - while v[k] <= -np.pi / 4: - shift(k, +1) - while v[k] > np.pi / 4: - shift(k, -1) - - def sort() -> None: - if abs(v[0]) < abs(v[1]): - swap(0, 1) - if abs(v[1]) < abs(v[2]): - swap(1, 2) - if abs(v[0]) < abs(v[1]): - swap(0, 1) - - canonical_shift(0) - canonical_shift(1) - canonical_shift(2) - sort() - - if v[0] < 0: - negate(0, 2) - if v[1] < 0: - negate(1, 2) - canonical_shift(2) - - atol = 1e-9 - if v[0] > np.pi / 4 - atol and v[2] < 0: - shift(0, -1) - negate(0, 2) - - return { - "single_qubit_operations_after": (left[1], left[0]), - "single_qubit_operations_before": (right[1], right[0]), - } - - -def _deconstruct_matrix_to_angles(mat: NDArray[np.complex128]) -> tuple[float, float, float]: - """ - Decompose matrix into angles. - """ - - def _phase_matrix(angle: float) -> NDArray[np.complex128]: - return np.diag([1, np.exp(1j * angle)]) - - def _rotation_matrix(angle: float) -> NDArray[np.float64]: - c, s = np.cos(angle), np.sin(angle) - return np.array([[c, -s], [s, c]]) - - right_phase = cmath.phase(mat[0, 1] * np.conj(mat[0, 0])) + np.pi - mat = np.dot(mat, _phase_matrix(-right_phase)) - - bottom_phase = cmath.phase(mat[1, 0] * np.conj(mat[0, 0])) - mat = np.dot(_phase_matrix(-bottom_phase), mat) - - rotation = math.atan2(abs(mat[1, 0]), abs(mat[0, 0])) - mat = np.dot(_rotation_matrix(-rotation), mat) - - diagonal_phase = cmath.phase(mat[1, 1] * np.conj(mat[0, 0])) - - return right_phase + diagonal_phase, rotation * 2, bottom_phase - - -def so_bidiagonalize( - mat: NDArray[np.complex128], -) -> tuple[NDArray[np.float64], NDArray[np.complex128], NDArray[np.float64]]: - """ - Find special orthogonal L and R so that L @ mat @ R is diagonal. - """ - left, right = orthogonal_bidiagonalize(np.real(mat), np.imag(mat)) - with np.errstate(divide="ignore", invalid="ignore"): - if np.linalg.det(left) < 0: - left[0, :] *= -1 - if np.linalg.det(right) < 0: - right[:, 0] *= -1 - - diag = np.dot(np.dot(left, mat), right) - - return left, np.diag(diag), right - - -def kak_decomposition_angles(mat: NDArray[np.complex128]) -> list[list[float]]: - """ - Decompose matrix into KAK decomposition, return all angles. - """ - kak_magic = np.array([[1, 0, 0, 1j], [0, 1j, 1, 0], [0, 1j, -1, 0], [1, 0, 0, -1j]]) * np.sqrt( - 0.5 - ) - - kak_magic_dag = np.conjugate(np.transpose(kak_magic)) - - left, d, right = so_bidiagonalize(kak_magic_dag @ mat @ kak_magic) - - a1, a0 = _so4_to_su2(left.T) - b1, b0 = _so4_to_su2(right.T) - - kak_gama = np.array([[1, 1, 1, 1], [1, 1, -1, -1], [-1, 1, -1, 1], [1, -1, -1, 1]]) * 0.25 - - _, x, y, z = (kak_gama @ np.angle(d).reshape(-1, 1)).flatten() - - 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]) - - pre_phase00, rotation00, post_phase00 = _deconstruct_matrix_to_angles(b1) - pre_phase01, rotation01, post_phase01 = _deconstruct_matrix_to_angles(b0) - pre_phase10, rotation10, post_phase10 = _deconstruct_matrix_to_angles(a1) - pre_phase11, rotation11, post_phase11 = _deconstruct_matrix_to_angles(a0) - - return [ - [rotation00, post_phase00, pre_phase00], - [rotation01, post_phase01, pre_phase01], - [rotation10, post_phase10, pre_phase10], - [rotation11, post_phase11, pre_phase11], - ] diff --git a/qbraid_qir/qasm3/maps.py b/qbraid_qir/qasm3/maps.py index cfa3f6e..ef6a9ce 100644 --- a/qbraid_qir/qasm3/maps.py +++ b/qbraid_qir/qasm3/maps.py @@ -12,13 +12,13 @@ Module mapping supported QASM gates/operations to pyqir functions. """ -from typing import Union +from typing import Callable, Union import numpy as np import pyqir +from pyqasm.linalg import kak_decomposition_angles from .exceptions import Qasm3ConversionError -from .linalg import kak_decomposition_angles def id_gate(builder, qubits): @@ -462,7 +462,7 @@ def prx_gate(builder, theta, phi, qubit): } -def map_qasm_op_to_pyqir_callable(op_name: str): +def map_qasm_op_to_pyqir_callable(op_name: str) -> tuple[Callable, int]: """ Map a QASM operation to a PyQIR callable. @@ -471,23 +471,22 @@ def map_qasm_op_to_pyqir_callable(op_name: str): Returns: tuple: A tuple containing the PyQIR callable and the number of qubits the operation acts on. + + Raises: + Qasm3ConversionError: If the QASM operation is unsupported or undeclared. """ - try: - return PYQIR_ONE_QUBIT_OP_MAP[op_name], 1 - except KeyError: - pass - try: - return PYQIR_ONE_QUBIT_ROTATION_MAP[op_name], 1 - except KeyError: - pass - try: - return PYQIR_TWO_QUBIT_OP_MAP[op_name], 2 - except KeyError: - pass - try: - return PYQIR_THREE_QUBIT_OP_MAP[op_name], 3 - except KeyError as exc: - raise Qasm3ConversionError(f"Unsupported / undeclared QASM operation: {op_name}") from exc + qasm_op_mappings: list[tuple[dict, int]] = [ + (PYQIR_ONE_QUBIT_OP_MAP, 1), + (PYQIR_ONE_QUBIT_ROTATION_MAP, 1), + (PYQIR_TWO_QUBIT_OP_MAP, 2), + (PYQIR_THREE_QUBIT_OP_MAP, 3), + ] + + for mapping, qubits in qasm_op_mappings: + if op_name in mapping: + return mapping[op_name], qubits + + raise Qasm3ConversionError(f"Unsupported / undeclared QASM operation: {op_name}") CONSTANTS_MAP = { diff --git a/tests/qasm3_qir/test_linalg.py b/tests/qasm3_qir/test_linalg.py deleted file mode 100644 index c97a58c..0000000 --- a/tests/qasm3_qir/test_linalg.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (C) 2024 qBraid -# -# This file is part of qbraid-qir -# -# Qbraid-qir is free software released under the GNU General Public License v3 -# or later. You can redistribute and/or modify it under the terms of the GPL v3. -# See the LICENSE file in the project root or . -# -# THERE IS NO WARRANTY for qbraid-qir, as per Section 15 of the GPL v3. - -""" -Module containing unit tests for linalg.py functions. - -""" -import numpy as np - -from qbraid_qir.qasm3.linalg import ( - _block_diag, - _helper_svd, - _kak_canonicalize_vector, - _orthogonal_diagonalize, - _so4_to_su2, - kak_decomposition_angles, - orthogonal_bidiagonalize, -) - - -def test_kak_canonicalize_vector(): - """Test _kak_canonicalize_vector function.""" - x, y, z = -1, -2, -1 - result = _kak_canonicalize_vector(x, y, z) - assert result["single_qubit_operations_before"][0][0][0] == -np.sqrt(2) / 2 * 1j - - x, y, z = 1, 2, 1 - result = _kak_canonicalize_vector(x, y, z) - assert result["single_qubit_operations_before"][0][0][0] == -np.sqrt(2) / 2 - - -def test_helper_svd(): - """Test _helper_svd function.""" - mat = np.random.rand(4, 4) - u, s, vh = _helper_svd(mat) - assert np.allclose(np.dot(u, np.dot(np.diag(s), vh)), mat) - - mat_empty = np.array([[]]) - u, s, vh = _helper_svd(mat_empty) - assert u.shape == (0, 0) - assert vh.shape == (0, 0) - assert len(s) == 0 - - -def test_block_diag(): - """Test block diagonalization of matrices.""" - a = np.random.rand(2, 2) - b = np.random.rand(3, 3) - res = _block_diag(a, b) - - assert res.shape == (5, 5) - assert np.allclose(res[:2, :2], a) - assert np.allclose(res[2:, 2:], b) - - -def test_orthogonal_diagonalize(): - """Test orthogonal diagonalization of matrices.""" - mat1 = np.eye(3) - mat2 = np.diag([1, 2, 3]) - p = _orthogonal_diagonalize(mat1, mat2) - - assert np.allclose(np.dot(p.T, np.dot(mat1, p)), np.eye(3)) - - -def test_orthogonal_bidiagonalize(): - """Test orthogonal bidiagonalization of matrices.""" - mat1 = np.random.rand(4, 4) - mat2 = np.random.rand(4, 4) - left, right = orthogonal_bidiagonalize(mat1, mat2) - - assert left.shape == (4, 4) - assert right.shape == (4, 4) - - -def test_so4_to_su2(): - """Test SO4 to SU2 conversion.""" - mat = np.eye(4) - a, b = _so4_to_su2(mat) - - assert a.shape == (2, 2) - assert b.shape == (2, 2) - - -def test_kak_decomposition_angles(): - """Test KAK decomposition angles.""" - mat = np.eye(4) - angles = kak_decomposition_angles(mat) - - assert len(angles) == 4 - assert all(len(a) == 3 for a in angles) From 30ccf0bd839f13a27059dce047f71bb04f27d048 Mon Sep 17 00:00:00 2001 From: Ryan Hill Date: Tue, 10 Dec 2024 19:38:48 -0600 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acf391..b7e96e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,3 +35,4 @@ Types of changes: - Updated `pyqasm` and `qbraid` dependencies ([#183](https://github.com/qBraid/qbraid-qir/pull/183)) ### 👋 Deprecations +- Removed `qbraid_qir.qasm3.linag` module in favor of `pyqasm.linalg` ([#190](https://github.com/qBraid/qbraid-qir/pull/190))