From 377ca48b90c30e0702d64c99b9b072499099af02 Mon Sep 17 00:00:00 2001 From: shun0923 Date: Tue, 30 Sep 2025 15:16:12 +0900 Subject: [PATCH 1/7] rename unitary.py to d_omega_unitary.py --- pygridsynth/{unitary.py => d_omega_unitary.py} | 0 pygridsynth/gridsynth.py | 2 +- pygridsynth/synthesis_of_cliffordT.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename pygridsynth/{unitary.py => d_omega_unitary.py} (100%) diff --git a/pygridsynth/unitary.py b/pygridsynth/d_omega_unitary.py similarity index 100% rename from pygridsynth/unitary.py rename to pygridsynth/d_omega_unitary.py diff --git a/pygridsynth/gridsynth.py b/pygridsynth/gridsynth.py index 0c6560e..5b935cb 100644 --- a/pygridsynth/gridsynth.py +++ b/pygridsynth/gridsynth.py @@ -4,6 +4,7 @@ import mpmath from .config import GridsynthConfig +from .d_omega_unitary import DOmegaUnitary from .diophantine import Result, diophantine_dyadic from .grid_op import GridOp from .mymath import MPFConvertible, RealNum, solve_quadratic, sqrt @@ -13,7 +14,6 @@ from .synthesis_of_cliffordT import decompose_domega_unitary from .tdgp import solve_TDGP from .to_upright import to_upright_ellipse_pair, to_upright_set_pair -from .unitary import DOmegaUnitary class EpsilonRegion(ConvexSet): diff --git a/pygridsynth/synthesis_of_cliffordT.py b/pygridsynth/synthesis_of_cliffordT.py index 08e9f89..64d03f7 100644 --- a/pygridsynth/synthesis_of_cliffordT.py +++ b/pygridsynth/synthesis_of_cliffordT.py @@ -1,3 +1,4 @@ +from .d_omega_unitary import DOmegaUnitary from .normal_form import NormalForm from .quantum_gate import ( HGate, @@ -10,7 +11,6 @@ w_phase, ) from .ring import OMEGA_POWER -from .unitary import DOmegaUnitary BIT_SHIFT = [0, 0, 1, 0, 2, 0, 1, 3, 3, 3, 0, 2, 2, 1, 0, 0] BIT_COUNT = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4] From 03ffbc271a4e611ab65c3fceab4ce768a465c892 Mon Sep 17 00:00:00 2001 From: shun0923 Date: Wed, 10 Dec 2025 03:59:09 +0900 Subject: [PATCH 2/7] Update files --- .pre-commit-config.yaml | 3 +- README.md | 2 +- pygridsynth/cli.py | 3 +- pygridsynth/config.py | 2 +- .../{d_omega_unitary.py => domega_unitary.py} | 155 +++++- pygridsynth/gridsynth.py | 86 +--- .../multi_qubit_unitary_approximation.py | 339 +++++++++++++ pygridsynth/mymath.py | 68 +++ pygridsynth/normal_form.py | 11 +- pygridsynth/quantum_circuit.py | 108 ++++ pygridsynth/quantum_gate.py | 173 +------ pygridsynth/synthesis_of_cliffordT.py | 20 +- pygridsynth/tdgp.py | 2 +- pygridsynth/to_upright.py | 28 +- .../two_qubit_unitary_approximation.py | 465 ++++++++++++++++++ pygridsynth/unitary_approximation.py | 313 ++++++++++++ pyproject.toml | 2 +- tests/test_main.py | 10 +- 18 files changed, 1518 insertions(+), 272 deletions(-) rename pygridsynth/{d_omega_unitary.py => domega_unitary.py} (50%) create mode 100644 pygridsynth/multi_qubit_unitary_approximation.py create mode 100644 pygridsynth/quantum_circuit.py create mode 100644 pygridsynth/two_qubit_unitary_approximation.py create mode 100644 pygridsynth/unitary_approximation.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f65c68..804979b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,10 +16,11 @@ repos: args: # compatibility with black # E203 whitespace before ':' + # E704 multiple statements on one line (def) # E741 ambiguous variable name 'I' # W503 line break before binary operator - "--max-line-length=88" - - "--ignore=E203, E741, W503" + - "--ignore=E203, E704, E741, W503" - repo: https://github.com/pre-commit/mirrors-mypy rev: 'v1.15.0' hooks: diff --git a/README.md b/README.md index 8774f7e..d89c956 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ pygridsynth [options] - `--dloop`, `-dl`: Maximum number of failed integer factoring attempts allowed during Diophantine equation solving (default: `10`). - `--floop`, `-fl`: Maximum number of failed integer factoring attempts allowed during the factoring process (default: `10`). - `--seed`: Random seed for deterministic results.(default: `0`) -- `--verbose`, `-v`: Enables detailed output. +- `--verbose`, `-v`: Verbosity level (0=silent, 1=basic, 2=detailed, 3=debug).(default: `0`) - `--time`, `-t`: Measures the execution time. - `--showgraph`, `-g`: Displays the decomposition result as a graph. - `--up-to-phase`, `-ph`: Approximates up to a phase. diff --git a/pygridsynth/cli.py b/pygridsynth/cli.py index e6fd965..9b17d04 100644 --- a/pygridsynth/cli.py +++ b/pygridsynth/cli.py @@ -15,6 +15,7 @@ "allowed during the factoring process" ), "seed": "Random seed for deterministic results", + "verbose": "Verbosity level (0=silent, 1=basic, 2=detailed, 3=debug)", } @@ -29,7 +30,7 @@ def main() -> str: parser.add_argument("--dloop", "-dl", type=int, help=helps["dl"]) parser.add_argument("--floop", "-fl", type=int, help=helps["fl"]) parser.add_argument("--seed", type=int, help=helps["seed"]) - parser.add_argument("--verbose", "-v", action="store_true") + parser.add_argument("--verbose", "-v", type=int) parser.add_argument("--time", "-t", action="store_true") parser.add_argument("--showgraph", "-g", action="store_true") parser.add_argument("--up-to-phase", "-ph", action="store_true") diff --git a/pygridsynth/config.py b/pygridsynth/config.py index 41a3287..421e522 100644 --- a/pygridsynth/config.py +++ b/pygridsynth/config.py @@ -11,7 +11,7 @@ class GridsynthConfig: floop: int = 10 dtimeout: float | None = None ftimeout: float | None = None - verbose: bool = False + verbose: int = 0 measure_time: bool = False show_graph: bool = False up_to_phase: bool = False diff --git a/pygridsynth/d_omega_unitary.py b/pygridsynth/domega_unitary.py similarity index 50% rename from pygridsynth/d_omega_unitary.py rename to pygridsynth/domega_unitary.py index 0406551..c871632 100644 --- a/pygridsynth/d_omega_unitary.py +++ b/pygridsynth/domega_unitary.py @@ -1,11 +1,14 @@ from __future__ import annotations +import string from functools import cached_property import mpmath -from .quantum_gate import HGate, QuantumCircuit, SGate, SXGate, TGate, WGate -from .ring import DOmega +from .mymath import RealNum, einsum, from_matrix_to_tensor, from_tensor_to_matrix +from .quantum_circuit import QuantumCircuit +from .quantum_gate import CxGate, HGate, SGate, SingleQubitGate, SXGate, TGate, WGate +from .ring import OMEGA, DOmega class DOmegaUnitary: @@ -167,3 +170,151 @@ def from_gates(cls, gates: str) -> DOmegaUnitary: else: raise ValueError return unitary.reduce_denomexp() + + +class DOmegaMatrix: + def __init__( + self, mat: list[list[DOmega]], wires: list[int], k: int = 0, phase: RealNum = 0 + ) -> None: + n = len(wires) + if (len(mat) != 2**n) or any(len(row) != 2**n for row in mat): + raise ValueError( + f"Matrix must be a {2**n}x{2**n} square matrix to match wires" + f"(got {len(mat)}x{len(mat[0]) if len(mat) > 0 else 0})" + ) + + self._mat: list[list[DOmega]] = mat + self._wires: list[int] = wires + self._k: int = k + self._phase: mpmath.mpf = mpmath.mpf(phase) % (2 * mpmath.mp.pi) + + @property + def mat(self) -> list[list[DOmega]]: + return self._mat + + @property + def wires(self) -> list[int]: + return self._wires + + @property + def k(self) -> int: + return self._k + + @property + def phase(self) -> mpmath.mpf: + return self._phase + + @phase.setter + def phase(self, phase: RealNum) -> None: + self._phase = mpmath.mpf(phase) % (2 * mpmath.mp.pi) + + @classmethod + def from_domega_unitary( + cls, unitary: DOmegaUnitary, wires: list[int], phase: RealNum = 0 + ) -> DOmegaMatrix: + return DOmegaMatrix(unitary.to_matrix, phase=phase, wires=wires) + + @classmethod + def from_single_qubit_gate(cls, g: SingleQubitGate) -> DOmegaMatrix: + return DOmegaMatrix.from_domega_unitary( + DOmegaUnitary.from_circuit(QuantumCircuit.from_list([g])), + wires=[g.target_qubit], + ) + + @classmethod + def from_single_qubit_circuit( + cls, circuit: QuantumCircuit, wires: list[int] + ) -> DOmegaMatrix: + return DOmegaMatrix.from_domega_unitary( + DOmegaUnitary.from_circuit(circuit), phase=circuit.phase, wires=wires + ) + + @classmethod + def from_w_gate(cls, g: WGate) -> DOmegaMatrix: + return DOmegaMatrix([[DOmega(OMEGA, 0)]], wires=[]) + + @classmethod + def from_cx_gate(cls, g: CxGate) -> DOmegaMatrix: + wires = [g.control_qubit, g.target_qubit] + mat = [[DOmega.from_int(0) for j in range(4)] for i in range(4)] + for i, j in [(0, 0), (1, 1), (2, 3), (3, 2)]: + mat[i][j] = DOmega.from_int(1) + return DOmegaMatrix(mat, wires) + + @cached_property + def to_matrix(self) -> list[list[DOmega]]: + return self._mat + + @cached_property + def to_complex_matrix(self) -> mpmath.matrix: + return mpmath.matrix( + [[x.to_complex for x in row] for row in self._mat] + ) * mpmath.exp(1.0j * self._phase) + + def __repr__(self) -> str: + return ( + f"DOmegaMatrix({repr(self._mat)}, {self._wires}, {self._k}, {self._phase})" + ) + + def __eq__(self, other: DOmegaMatrix | object) -> bool: + if isinstance(other, DOmegaMatrix): + return self._mat == other.mat and self._k == other.k + else: + return False + + def __matmul__(self, other: DOmegaMatrix) -> DOmegaMatrix: + if isinstance(other, DOmegaMatrix): + all_wires = list(set(self._wires + other.wires)) + n_total = len(all_wires) + + labels = list(string.ascii_lowercase) + if (len(self._wires) + len(other.wires)) * 2 > len(labels): + raise ValueError("Too many qubits for automatic einsum labeling") + + out_row_labels = [labels[i] for i in range(n_total)] + out_col_labels = [labels[n_total + i] for i in range(n_total)] + + self_row_labels = [out_row_labels[all_wires.index(w)] for w in self._wires] + self_col_labels = [out_col_labels[all_wires.index(w)] for w in self._wires] + + other_row_labels = [out_row_labels[all_wires.index(w)] for w in other.wires] + other_col_labels = [out_col_labels[all_wires.index(w)] for w in other.wires] + + shared_wires = set(self._wires) & set(other.wires) + for idx, w in enumerate(shared_wires): + i = self._wires.index(w) + j = other.wires.index(w) + other_row_labels[j] = labels[n_total * 2 + idx] + self_col_labels[i] = labels[n_total * 2 + idx] + + einsum_str = ( + "".join(self_row_labels + self_col_labels) + + "," + + "".join(other_row_labels + other_col_labels) + + "->" + + "".join(out_row_labels + out_col_labels) + ) + + A_tensor = from_matrix_to_tensor(self._mat, len(self._wires)) + B_tensor = from_matrix_to_tensor(other.mat, len(other.wires)) + C_tensor = from_tensor_to_matrix( + einsum(einsum_str, A_tensor, B_tensor), n_total + ) + + return DOmegaMatrix( + C_tensor, + k=self._k + other.k, + phase=self._phase + other.phase, + wires=all_wires, + ) + else: + return NotImplemented + + @classmethod + def identity(cls, wires: list[int]) -> DOmegaMatrix: + n = 2 ** len(wires) + mat = [ + [DOmega.from_int(1) if i == j else DOmega.from_int(0) for j in range(n)] + for i in range(n) + ] + return cls(mat, wires) diff --git a/pygridsynth/gridsynth.py b/pygridsynth/gridsynth.py index 5b935cb..1a25d62 100644 --- a/pygridsynth/gridsynth.py +++ b/pygridsynth/gridsynth.py @@ -4,11 +4,19 @@ import mpmath from .config import GridsynthConfig -from .d_omega_unitary import DOmegaUnitary from .diophantine import Result, diophantine_dyadic +from .domega_unitary import DOmegaUnitary from .grid_op import GridOp -from .mymath import MPFConvertible, RealNum, solve_quadratic, sqrt -from .quantum_gate import QuantumCircuit, Rz +from .mymath import ( + MPFConvertible, + RealNum, + convert_theta_and_epsilon, + dps_for_epsilon, + solve_quadratic, + sqrt, +) +from .quantum_circuit import QuantumCircuit +from .quantum_gate import Rz from .region import ConvexSet, Ellipse, Rectangle from .ring import DOmega, DRootTwo, ZOmega, ZRootTwo from .synthesis_of_cliffordT import decompose_domega_unitary @@ -110,7 +118,7 @@ def error( "Either `dps` or `epsilon` must be provided to determine precision." ) else: - dps = _dps_for_epsilon(epsilon) + dps = dps_for_epsilon(epsilon) with mpmath.workdps(dps): theta = mpmath.mpf(theta) phase = mpmath.mpf(phase) @@ -149,7 +157,7 @@ def get_synthesized_unitary( "Either `dps` or `epsilon` must be provided to determine precision." ) else: - dps = _dps_for_epsilon(epsilon) + dps = dps_for_epsilon(epsilon) with mpmath.workdps(dps): return DOmegaUnitary.from_gates(gates).to_complex_matrix @@ -226,8 +234,8 @@ def _gridsynth_exact( tdgp_sets = (epsilon_region, unit_disk, *transformed) if cfg.measure_time: - print(f"to_upright_set_pair: {time.time() - start} s") - if cfg.verbose: + print(f"time of to_upright_set_pair: {(time.time() - start) * 1000} ms") + if cfg.verbose >= 2: print("------------------") u_approx = None @@ -253,7 +261,7 @@ def _gridsynth_exact( print( "time of diophantine_dyadic: " f"{time_of_diophantine_dyadic * 1000} ms" ) - if cfg.verbose: + if cfg.verbose >= 2: print(f"{u_approx=}") print("------------------") return u_approx @@ -290,8 +298,8 @@ def _gridsynth_up_to_phase( tdgp_sets1 = (epsilon_region1, unit_disk1, *transformed1) if cfg.measure_time: - print(f"to_upright_set_pair: {time.time() - start} s") - if cfg.verbose: + print(f"time of to_upright_set_pair: {(time.time() - start) * 1000} ms") + if cfg.verbose >= 2: print("------------------") u_approx = None @@ -330,7 +338,7 @@ def _gridsynth_up_to_phase( print( "time of diophantine_dyadic: " f"{time_of_diophantine_dyadic * 1000} ms" ) - if cfg.verbose: + if cfg.verbose >= 2: print(f"{u_approx=}") print("------------------") return u_approx @@ -345,41 +353,13 @@ def gridsynth( if cfg is None: cfg = GridsynthConfig(**kwargs) elif kwargs: - warnings.warn( - "When 'cfg' is provided, 'kwargs' are ignored.", - stacklevel=2, - ) + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) if cfg.dps is None: - cfg.dps = _dps_for_epsilon(epsilon) - - if isinstance(theta, float): - warnings.warn( - ( - f"pygridsynth is synthesizing the angle {theta}. " - "Please verify that this is the intended value. " - "Using float may introduce precision errors; " - "consider using mpmath.mpf for exact precision." - ), - UserWarning, - stacklevel=2, - ) - - if isinstance(epsilon, float): - warnings.warn( - ( - f"pygridsynth is using epsilon={epsilon} as the tolerance. " - "Please verify that this is the intended value. " - "Using float may introduce precision errors; " - "consider using mpmath.mpf for exact precision." - ), - UserWarning, - stacklevel=2, - ) + cfg.dps = dps_for_epsilon(epsilon) with mpmath.workdps(cfg.dps): - theta = mpmath.mpf(theta) - epsilon = mpmath.mpf(epsilon) + theta, epsilon = convert_theta_and_epsilon(theta, epsilon, dps=cfg.dps) if cfg.up_to_phase: return _gridsynth_up_to_phase(theta, epsilon, cfg=cfg) @@ -397,13 +377,10 @@ def gridsynth_circuit( if cfg is None: cfg = GridsynthConfig(**kwargs) elif kwargs: - warnings.warn( - "When 'cfg' is provided, 'kwargs' are ignored.", - stacklevel=2, - ) + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) if cfg.dps is None: - cfg.dps = _dps_for_epsilon(epsilon) + cfg.dps = dps_for_epsilon(epsilon) with mpmath.workdps(cfg.dps): start_total = time.time() if cfg.measure_time else 0.0 @@ -419,7 +396,7 @@ def gridsynth_circuit( print( f"time of decompose_domega_unitary: {(time.time() - start) * 1000} ms" ) - print(f"total time: {(time.time() - start_total) * 1000} ms") + print(f"time of gridsynth_circuit: {(time.time() - start_total) * 1000} ms") return circuit @@ -433,13 +410,10 @@ def gridsynth_gates( if cfg is None: cfg = GridsynthConfig(**kwargs) elif kwargs: - warnings.warn( - "When 'cfg' is provided, 'kwargs' are ignored.", - stacklevel=2, - ) + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) if cfg.dps is None: - cfg.dps = _dps_for_epsilon(epsilon) + cfg.dps = dps_for_epsilon(epsilon) with mpmath.workdps(cfg.dps): circuit = gridsynth_circuit( @@ -449,9 +423,3 @@ def gridsynth_gates( cfg=cfg, ) return circuit.to_simple_str() - - -def _dps_for_epsilon(epsilon: MPFConvertible) -> int: - e = mpmath.mpf(epsilon) - k = -mpmath.log10(e) - return int(15 + 2.5 * int(mpmath.ceil(k))) # used in newsynth diff --git a/pygridsynth/multi_qubit_unitary_approximation.py b/pygridsynth/multi_qubit_unitary_approximation.py new file mode 100644 index 0000000..90ef50a --- /dev/null +++ b/pygridsynth/multi_qubit_unitary_approximation.py @@ -0,0 +1,339 @@ +import time +import warnings +from typing import Literal, overload + +import mpmath + +from .config import GridsynthConfig +from .domega_unitary import DOmegaMatrix +from .gridsynth import gridsynth_circuit +from .mymath import ( + MPFConvertible, + all_close, + convert_theta_and_epsilon, + dps_for_epsilon, + kron, +) +from .quantum_circuit import QuantumCircuit +from .quantum_gate import CxGate, HGate, QuantumGate, RzGate +from .two_qubit_unitary_approximation import approximate_two_qubit_unitary +from .unitary_approximation import approximate_one_qubit_unitary + + +def _blockZXZ( + U: mpmath.matrix, num_qubits: int, verbose: int = 0 +) -> tuple[mpmath.matrix, mpmath.matrix, mpmath.matrix, mpmath.matrix]: + n = 2**num_qubits + n_half = 2 ** (num_qubits - 1) + I = mpmath.eye(n_half) + + # upper left block + X = U[0:n_half, 0:n_half] + # lower left block + U12 = U[0:n_half, n_half:n] + + # svd: M = W*diag(S)*V.H + # X = W11*diag(S11)*Vh11 + # X = Sx*Ux + W11, S11, Vh11 = mpmath.svd_c(X) + Sx = W11 @ mpmath.diag(S11) @ W11.H + Ux = W11 @ Vh11 + + # svd: U12 = W12*diag(S12)*Vh12 + # Y = Sy*Uy + W12, S12, Vh12 = mpmath.svd_c(U12) + Sy = W12 @ mpmath.diag(S12) @ W12.H + Uy = W12 @ Vh12 + + A = (Sx + 1j * Sy) @ Ux + C = -1j * Ux.H @ Uy + B = U[n_half:n, 0:n_half] + U[n_half:n, n_half:n] @ C.H + Z = 2 * A.H @ X - I + + if verbose >= 1: + IC = mpmath.zeros(n) + IC[:n_half, :n_half] = I + IC[n_half:, n_half:] = C + + AB = mpmath.zeros(n) + AB[:n_half, :n_half] = A + AB[n_half:n, n_half:n] = B + + IZ = mpmath.zeros(n) + IZ[:n_half, :n_half] = I + Z + IZ[n_half:, n_half:] = I + Z + IZ[n_half:, :n_half] = I - Z + IZ[:n_half, n_half:] = I - Z + + print( + "Block-ZXZ correct for matrix of shape: ", + U.rows, + U.cols, + " ? ", + all_close(0.5 * AB @ IZ @ IC, U), + ) + + return A, B, Z, C + + +def _demultiplex( + M1: mpmath.matrix, M2: mpmath.matrix, verbose: int = 0 +) -> tuple[mpmath.matrix, list[mpmath.mpf], mpmath.matrix]: + eigenvalues, V = mpmath.eig(M1 @ M2.H) + D_sqrt = [mpmath.sqrt(eigenvalue) for eigenvalue in eigenvalues] + W = mpmath.diag(D_sqrt) @ V.H @ M2 + if verbose >= 1: + print( + "Demultiplexing correct? ", + all_close(V @ mpmath.diag(D_sqrt) @ W, M1), + all_close(V @ mpmath.diag(D_sqrt).conjugate() @ W, M2), + ) + return V, D_sqrt, W + + +def _bitwise_inner_product(a: int, b: int) -> int: + i = a & b + # number of set bits in i + i = i - ((i >> 1) & 0x55555555) + i = (i & 0x33333333) + ((i >> 2) & 0x33333333) + i = (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24 + return i % 2 + + +# returns M^k = (-1)^(b_(i-1)*g_(i-1)), +# where * is bitwise inner product, g = binary gray code, b = binary code. +def _genMk(n: int) -> mpmath.matrix: + Mk = mpmath.matrix(n) + for i in range(0, n): + for j in range(0, n): + Mk[i, j] = (-1) ** (_bitwise_inner_product((i), ((j) ^ (j) >> 1))) + return Mk + + +# input D as (one dimensional) array +def _decompose_cz( + D: mpmath.matrix, + control_qubits: list[int], + target_qubit: int, + inverse: bool = False, +) -> QuantumCircuit: + n = len(D) + control_qubits.reverse() + D_arg = [-2 * mpmath.arg(d) for d in D] + ar = mpmath.lu_solve(_genMk(n), D_arg) + circuit = QuantumCircuit() + + if n != 2 ** len(control_qubits): + print( + "Warning: shape mismatch for controlled Z between" + "# control bits and length of D" + ) + + if inverse: + for i in range(n - 1, -1, -1): + idx = (i ^ (i >> 1) ^ (i + 1) ^ ((i + 1) >> 1)).bit_length() - 1 + if idx == len(control_qubits): + posc = control_qubits[-1] + else: + posc = control_qubits[idx] + circuit.append(CxGate(posc, target_qubit)) + circuit.append(RzGate(ar[i].real, target_qubit)) + + else: + for i in range(0, n): + idx = (i ^ (i >> 1) ^ (i + 1) ^ ((i + 1) >> 1)).bit_length() - 1 + if idx == len(control_qubits): + posc = control_qubits[-1] + else: + posc = control_qubits[idx] + circuit.append(RzGate(ar[i].real, target_qubit)) + circuit.append(CxGate(posc, target_qubit)) + + return circuit + + +def _decompose_recursively( + U: mpmath.matrix, wires: list[int], verbose: int = 0 +) -> QuantumCircuit: + num_qubits = len(wires) + if num_qubits == 1 or num_qubits == 2: + return QuantumCircuit.from_list([QuantumGate(U, wires)]) + + n_half = 2 ** (num_qubits - 1) + I = mpmath.eye(n_half) + circuit = QuantumCircuit() + + A1, A2, B, C = _blockZXZ(U, num_qubits, verbose=verbose) + V_a, D_a, W_a = _demultiplex(A1, A2, verbose=verbose) + V_c, D_c, W_c = _demultiplex(I, C, verbose=verbose) + + V_a_circ = _decompose_recursively(V_a, wires[1:], verbose=verbose) + circuit += V_a_circ + + D_a_circ = _decompose_cz(D_a, wires[1:], wires[0]) + circuit += D_a_circ[:-1] + circuit.append(HGate(wires[0])) + + new_middle_ul = W_a @ V_c + Sz_I = kron(mpmath.matrix([[1, 0], [0, -1]]), mpmath.eye(2 ** (num_qubits - 2))) + new_middle_lr = Sz_I @ W_a @ B @ V_c @ Sz_I + V_m, D_sqrt_m, W_m = _demultiplex(new_middle_ul, new_middle_lr, verbose=verbose) + + V_m_circ = _decompose_recursively(V_m, wires[1:], verbose=verbose) + circuit += V_m_circ + + D_sqrt_m_circ = _decompose_cz(D_sqrt_m, wires[1:], wires[0]) + circuit += D_sqrt_m_circ + + W_m_circ = _decompose_recursively(W_m, wires[1:], verbose=verbose) + circuit += W_m_circ + + circuit.append(HGate(wires[0])) + + D_c_circ = _decompose_cz(D_c, wires[1:], wires[0], inverse=True) + circuit += D_c_circ[1:] + + circ_W_c = _decompose_recursively(W_c, wires[1:], verbose=verbose) + circuit += circ_W_c + + return circuit + + +@overload +def approximate_multi_qubit_unitary( + U: mpmath.matrix, + num_qubits: int, + epsilon: MPFConvertible, + cfg: GridsynthConfig | None = None, + return_domega_matrix: Literal[True] = True, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix]: ... + + +@overload +def approximate_multi_qubit_unitary( + U: mpmath.matrix, + num_qubits: int, + epsilon: MPFConvertible, + cfg: GridsynthConfig | None = None, + return_domega_matrix: Literal[False] = False, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, mpmath.matrix]: ... + + +def approximate_multi_qubit_unitary( + U: mpmath.matrix, + num_qubits: int, + epsilon: MPFConvertible, + cfg: GridsynthConfig | None = None, + return_domega_matrix: bool = False, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix | mpmath.matrix]: + if cfg is None: + cfg = GridsynthConfig(**kwargs) + elif kwargs: + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) + + if cfg.dps is None: + cfg.dps = dps_for_epsilon(epsilon) + cfg.up_to_phase = True + + if num_qubits == 1: + return approximate_one_qubit_unitary( + U, + epsilon, + wires=[0], + decompose_partially=False, + return_domega_matrix=return_domega_matrix, + scale_epsilon=scale_epsilon, + cfg=cfg, + ) + elif num_qubits == 2: + return approximate_two_qubit_unitary( + U, + epsilon, + wires=[0, 1], + decompose_partially=False, + return_domega_matrix=return_domega_matrix, + scale_epsilon=scale_epsilon, + cfg=cfg, + ) + + with mpmath.workdps(cfg.dps): + _, epsilon = convert_theta_and_epsilon("0", epsilon, dps=cfg.dps) + if scale_epsilon: + epsilon /= 18 * 4 ** (num_qubits - 2) - 3 * 2 ** (num_qubits - 1) + 3 + wires = list(range(num_qubits)) + + start = time.time() if cfg.measure_time else 0.0 + decomposed_circuit = _decompose_recursively(U, wires=wires, verbose=cfg.verbose) + if cfg.measure_time: + print("--------------------------------") + print(f"time of _decompose_recursively: {(time.time() - start) * 1000} ms") + print("--------------------------------") + diag = [1] * (2**2) + circuit = QuantumCircuit() + + U_approx = DOmegaMatrix.identity(wires=wires) + for i, c in enumerate(decomposed_circuit): + if isinstance(c, RzGate): + circuit_rz = gridsynth_circuit(c.theta, epsilon, wires=c.wires, cfg=cfg) + circuit += circuit_rz + U_approx = U_approx @ DOmegaMatrix.from_single_qubit_circuit( + circuit_rz, wires=c.wires + ) + elif isinstance(c, CxGate): + circuit.append(c) + U_approx = U_approx @ DOmegaMatrix.from_cx_gate(c) + elif isinstance(c, HGate): + circuit.append(c) + U_approx = U_approx @ DOmegaMatrix.from_single_qubit_gate(c) + elif i == len(decomposed_circuit) - 1: + c.matrix = mpmath.diag(diag) @ c.matrix + scale = mpmath.det(c.matrix) ** (-1 / 4) + circuit.phase -= mpmath.arg(scale) + U_approx.phase -= mpmath.arg(scale) + c.matrix *= scale + tmp_circuit, tmp_unitary = approximate_two_qubit_unitary( + c.matrix, + epsilon, + wires=c.wires, + decompose_partially=False, + return_domega_matrix=True, + cfg=cfg, + ) + circuit += tmp_circuit + U_approx = U_approx @ tmp_unitary + else: + c.matrix = mpmath.diag(diag) @ c.matrix + scale = mpmath.det(c.matrix) ** (-1 / 4) + circuit.phase -= mpmath.arg(scale) + U_approx.phase -= mpmath.arg(scale) + c.matrix *= scale + tmp_circuit, tmp_diag, tmp_unitary = approximate_two_qubit_unitary( + c.matrix, + epsilon, + wires=c.wires, + decompose_partially=True, + return_domega_matrix=True, + scale_epsilon=False, + cfg=cfg, + ) + U_approx = U_approx @ tmp_unitary + circuit += tmp_circuit + diag = tmp_diag + + if cfg.measure_time: + print("--------------------------------") + print( + "time of approximate_multi_qubit_unitary: " + f"{(time.time() - start) * 1000} ms" + ) + print("--------------------------------") + if return_domega_matrix: + return circuit, U_approx + else: + return circuit, U_approx.to_complex_matrix diff --git a/pygridsynth/mymath.py b/pygridsynth/mymath.py index 0e73235..49b1000 100644 --- a/pygridsynth/mymath.py +++ b/pygridsynth/mymath.py @@ -1,12 +1,54 @@ +import warnings from itertools import accumulate from typing import TypeAlias import mpmath +import numpy as np RealNum: TypeAlias = int | float | mpmath.mpf MPFConvertible: TypeAlias = RealNum | mpmath.mpf +def dps_for_epsilon(epsilon: MPFConvertible) -> int: + e = mpmath.mpf(epsilon) + k = -mpmath.log10(e) + return int(15 + 2.5 * int(mpmath.ceil(k))) # used in newsynth + + +def convert_theta_and_epsilon( + theta: MPFConvertible, epsilon: MPFConvertible, dps: int +) -> tuple[mpmath.mpf, mpmath.mpf]: + if isinstance(theta, float): + warnings.warn( + ( + f"pygridsynth is synthesizing the angle {theta}. " + "Please verify that this is the intended value. " + "Using float may introduce precision errors; " + "consider using mpmath.mpf for exact precision." + ), + UserWarning, + stacklevel=2, + ) + + if isinstance(epsilon, float): + warnings.warn( + ( + f"pygridsynth is using epsilon={epsilon} as the tolerance. " + "Please verify that this is the intended value. " + "Using float may introduce precision errors; " + "consider using mpmath.mpf for exact precision." + ), + UserWarning, + stacklevel=2, + ) + + with mpmath.workdps(dps): + theta_mpf = mpmath.mpf(theta) + epsilon_mpf = mpmath.mpf(epsilon) + + return theta_mpf, epsilon_mpf + + def SQRT2() -> mpmath.mpf: return mpmath.sqrt(2) @@ -99,3 +141,29 @@ def solve_quadratic( return (0, -b / a) else: return ((2 * c) / s2, (2 * c) / s1) + + +def trace(M: mpmath.matrix) -> mpmath.mpf: + return sum(M[i, i] for i in range(min(M.rows, M.cols))) + + +def kron(A: mpmath.matrix, B: mpmath.matrix) -> mpmath.matrix: + return mpmath.matrix(np.kron(A.tolist(), B.tolist())) + + +def all_close( + A: mpmath.matrix, B: mpmath.matrix, tol: mpmath.mpf = mpmath.mpf("1e-5") +) -> bool: + return mpmath.norm(mpmath.matrix(A - B), p="inf") < tol + + +def einsum(subscripts: str, *operands: np.ndarray) -> np.ndarray: + return np.einsum(subscripts, *operands) + + +def from_matrix_to_tensor(mat: list[list], n) -> np.ndarray: + return np.array(mat, dtype=object).reshape([2] * n * 2) + + +def from_tensor_to_matrix(mat: np.ndarray, n) -> list[list]: + return mat.reshape((2**n, 2**n)).tolist() diff --git a/pygridsynth/normal_form.py b/pygridsynth/normal_form.py index f6600e6..7389d29 100644 --- a/pygridsynth/normal_form.py +++ b/pygridsynth/normal_form.py @@ -5,15 +5,8 @@ import mpmath from .mymath import RealNum -from .quantum_gate import ( - HGate, - QuantumCircuit, - QuantumGate, - SGate, - SXGate, - TGate, - WGate, -) +from .quantum_circuit import QuantumCircuit +from .quantum_gate import HGate, QuantumGate, SGate, SXGate, TGate, WGate class Axis(Enum): diff --git a/pygridsynth/quantum_circuit.py b/pygridsynth/quantum_circuit.py new file mode 100644 index 0000000..f051677 --- /dev/null +++ b/pygridsynth/quantum_circuit.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .domega_unitary import DOmegaMatrix + +import functools +import operator +from typing import Iterable + +import mpmath + +from .mymath import RealNum +from .quantum_gate import CxGate, QuantumGate, WGate, w_phase + + +class QuantumCircuit(list): + def __init__(self, phase: RealNum = 0, args: list[QuantumGate] = []) -> None: + self._phase: mpmath.mpf = mpmath.mpf(phase) % (2 * mpmath.mp.pi) + super().__init__(args) + + @classmethod + def from_list(cls, gates: list[QuantumGate]) -> QuantumCircuit: + return cls(args=gates) + + @property + def phase(self) -> mpmath.mpf: + return self._phase + + @phase.setter + def phase(self, phase: RealNum) -> None: + self._phase = mpmath.mpf(phase) % (2 * mpmath.mp.pi) + + def __str__(self) -> str: + return f"exp(1.j * {self._phase}) * " + " * ".join(str(gate) for gate in self) + + def to_simple_str(self) -> str: + return "".join(g.to_simple_str() for g in self) + + def to_domega_matrix(self, wires: list[int]) -> DOmegaMatrix: + from .domega_unitary import DOmegaMatrix + + product = [DOmegaMatrix.identity(wires=wires)] + product[-1].phase = self.phase + crt_matrix = None + for g in self: + if isinstance(g, WGate): + if crt_matrix is not None: + product.append(crt_matrix) + crt_matrix = None + product.append(DOmegaMatrix.from_w_gate(g)) + elif isinstance(g, CxGate): + if crt_matrix is not None: + product.append(crt_matrix) + crt_matrix = None + product.append(DOmegaMatrix.from_cx_gate(g)) + else: + if crt_matrix is None: + crt_matrix = DOmegaMatrix.from_single_qubit_gate(g) + else: + if crt_matrix.wires == g.wires: + crt_matrix = crt_matrix @ DOmegaMatrix.from_single_qubit_gate(g) + else: + product.append(crt_matrix) + crt_matrix = None + crt_matrix = DOmegaMatrix.from_single_qubit_gate(g) + if crt_matrix is not None: + product.append(crt_matrix) + return functools.reduce(operator.matmul, product) + + def to_complex_matrix(self, n: int) -> mpmath.matrix: + return self.to_domega_matrix(wires=list(range(n))).to_complex_matrix + + def __add__(self, other: QuantumCircuit | list) -> QuantumCircuit: + if isinstance(other, QuantumCircuit): + return QuantumCircuit(self.phase + other.phase, list(self) + list(other)) + elif isinstance(other, list): + return QuantumCircuit(self.phase, list(self) + other) + else: + return NotImplemented + + def __radd__(self, other: QuantumCircuit | list) -> QuantumCircuit: + if isinstance(other, QuantumCircuit): + return QuantumCircuit(other.phase + self.phase, list(other) + list(self)) + elif isinstance(other, list): + return QuantumCircuit(self.phase, other + list(self)) + else: + return NotImplemented + + def __iadd__(self, other: QuantumCircuit | Iterable) -> QuantumCircuit: + if isinstance(other, QuantumCircuit): + self.phase += other.phase + super().__iadd__(list(other)) + return self + elif isinstance(other, Iterable): + super().__iadd__(other) + return self + else: + return NotImplemented + + def decompose_phase_gate(self) -> None: + self._phase %= 2 * mpmath.mp.pi + + for _ in range(round(float(self._phase / w_phase()))): + self.append(WGate()) + + self._phase = 0 diff --git a/pygridsynth/quantum_gate.py b/pygridsynth/quantum_gate.py index 0ed22a0..736f4a6 100644 --- a/pygridsynth/quantum_gate.py +++ b/pygridsynth/quantum_gate.py @@ -1,16 +1,16 @@ from __future__ import annotations -from typing import Iterable - import mpmath -from .mymath import RealNum - def cnot01() -> mpmath.matrix: return mpmath.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) +def cnot10() -> mpmath.matrix: + return mpmath.matrix([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]) + + def w_phase() -> mpmath.mpf: return mpmath.mp.pi / 4 @@ -50,30 +50,6 @@ def wires(self) -> list[int]: def __str__(self) -> str: return f"QuantumGate({self.matrix}, {self.wires})" - def to_whole_matrix(self, num_qubits: int) -> mpmath.matrix: - whole_matrix = mpmath.matrix(2**num_qubits) - m = len(self._wires) - target_qubit_sorted = sorted(self._wires, reverse=True) - - for i in range(2**m): - for j in range(2**m): - k = 0 - while k < 2**num_qubits: - i2 = 0 - j2 = 0 - for l in range(m): - t = num_qubits - target_qubit_sorted[l] - 1 - if k & 1 << t: - k += 1 << t - i2 |= (i >> l & 1) << t - j2 |= (j >> l & 1) << t - if k >= 2**num_qubits: - break - whole_matrix[k | i2, k | j2] = self._matrix[i, j] - k += 1 - - return whole_matrix - class SingleQubitGate(QuantumGate): def __init__(self, matrix: mpmath.matrix, target_qubit: int) -> None: @@ -87,23 +63,6 @@ def target_qubit(self) -> int: def __str__(self) -> str: return f"SingleQubitGate({self.matrix}, {self.target_qubit})" - def to_whole_matrix(self, num_qubits: int) -> mpmath.matrix: - whole_matrix = mpmath.matrix(2**num_qubits) - t = num_qubits - self.target_qubit - 1 - - for i in range(2): - for j in range(2): - k = 0 - while k < 2**num_qubits: - if k & 1 << t: - k += 1 << t - if k >= 2**num_qubits: - break - whole_matrix[k | (i << t), k | (j << t)] = self._matrix[i, j] - k += 1 - - return whole_matrix - class HGate(SingleQubitGate): def __init__(self, target_qubit: int) -> None: @@ -152,9 +111,6 @@ def __str__(self) -> str: def to_simple_str(self) -> str: return "W" - def to_whole_matrix(self, num_qubits: int) -> mpmath.matrix: - return self._matrix - class SXGate(SingleQubitGate): def __init__(self, target_qubit: int) -> None: @@ -170,7 +126,7 @@ def to_simple_str(self) -> str: class RzGate(SingleQubitGate): def __init__(self, theta: mpmath.mpf, target_qubit: int) -> None: - self._theta = theta + self._theta: mpmath.mpf = theta matrix = Rz(theta) super().__init__(matrix, target_qubit) @@ -190,7 +146,7 @@ def __mul__(self, other: RzGate) -> RzGate: class RxGate(SingleQubitGate): def __init__(self, theta: mpmath.mpf, target_qubit: int) -> None: - self._theta = theta + self._theta: mpmath.mpf = theta matrix = Rx(theta) super().__init__(matrix, target_qubit) @@ -223,120 +179,3 @@ def target_qubit(self) -> int: def __str__(self) -> str: return f"CxGate({self.control_qubit}, {self.target_qubit})" - - def to_whole_matrix(self, num_qubits: int) -> mpmath.matrix: - whole_matrix = mpmath.matrix(2**num_qubits) - c = num_qubits - self.control_qubit - 1 - t = num_qubits - self.target_qubit - 1 - for i0 in range(2): - for j0 in range(2): - for i1 in range(2): - for j1 in range(2): - k = 0 - while k < 2**num_qubits: - if k & (1 << min(c, t)): - k += 1 << min(c, t) - if k & (1 << max(c, t)): - k += 1 << max(c, t) - if k >= 2**num_qubits: - break - i2 = k | (i0 << c) | (i1 << t) - j2 = k | (j0 << c) | (j1 << t) - whole_matrix[i2, j2] = self._matrix[ - i0 * 2 + i1, j0 * 2 + j1 - ] - k += 1 - return whole_matrix - - -class QuantumCircuit(list): - def __init__(self, phase: RealNum = 0, args: list[QuantumGate] = []) -> None: - self._phase = mpmath.mpf(phase) % (2 * mpmath.mp.pi) - super().__init__(args) - - @classmethod - def from_list(cls, gates: list[QuantumGate]) -> QuantumCircuit: - return cls(args=gates) - - @property - def phase(self) -> mpmath.mpf: - return self._phase - - @phase.setter - def phase(self, phase: RealNum) -> None: - self._phase = mpmath.mpf(phase) % (2 * mpmath.mp.pi) - - def __str__(self) -> str: - return f"exp(1.j * {self._phase}) * " + " * ".join(str(gate) for gate in self) - - def to_simple_str(self) -> str: - return "".join(g.to_simple_str() for g in self) - - def __add__(self, other: QuantumCircuit | list) -> QuantumCircuit: - if isinstance(other, QuantumCircuit): - return QuantumCircuit(self.phase + other.phase, list(self) + list(other)) - elif isinstance(other, list): - return QuantumCircuit(self.phase, list(self) + other) - else: - return NotImplemented - - def __radd__(self, other: QuantumCircuit | list) -> QuantumCircuit: - if isinstance(other, QuantumCircuit): - return QuantumCircuit(other.phase + self.phase, list(other) + list(self)) - elif isinstance(other, list): - return QuantumCircuit(self.phase, other + list(self)) - else: - return NotImplemented - - def __iadd__(self, other: QuantumCircuit | Iterable) -> QuantumCircuit: - if isinstance(other, QuantumCircuit): - self.phase += other.phase - super().__iadd__(list(other)) - return self - elif isinstance(other, Iterable): - super().__iadd__(other) - return self - else: - return NotImplemented - - def decompose_phase_gate(self) -> None: - self._phase %= 2 * mpmath.mp.pi - - for _ in range(round(float(self._phase / w_phase()))): - self.append(WGate()) - - self._phase = 0 - - def to_matrix(self, num_qubits: int) -> mpmath.matrix: - U = mpmath.eye(2**num_qubits) * mpmath.exp(1.0j * self._phase) - for g in self: - U2 = mpmath.zeros(2**num_qubits) - if isinstance(g, WGate): - U2 = U * g.matrix - elif isinstance(g, CxGate): - for i in range(2**num_qubits): - for j in range(2**num_qubits): - for b0 in range(2): - for b1 in range(2): - t = num_qubits - g.target_qubit - 1 - c = num_qubits - g.control_qubit - 1 - j2 = j ^ b0 << c ^ b1 << t - U2[i, j2] += ( - U[i, j] - * g.matrix[ - (j >> c & 1) << 1 | (j >> t & 1), - (j2 >> c & 1) << 1 | (j2 >> t & 1), - ] - ) - elif isinstance(g, SingleQubitGate): - t = num_qubits - g.target_qubit - 1 - for i in range(2**num_qubits): - for j in range(2**num_qubits): - U2[i, j] += U[i, j] * g.matrix[j >> t & 1, j >> t & 1] - j2 = j ^ 1 << t - U2[i, j2] += U[i, j] * g.matrix[j >> t & 1, j2 >> t & 1] - else: - raise ValueError - U = U2 - - return U diff --git a/pygridsynth/synthesis_of_cliffordT.py b/pygridsynth/synthesis_of_cliffordT.py index 64d03f7..c5b7a27 100644 --- a/pygridsynth/synthesis_of_cliffordT.py +++ b/pygridsynth/synthesis_of_cliffordT.py @@ -1,15 +1,7 @@ -from .d_omega_unitary import DOmegaUnitary +from .domega_unitary import DOmegaUnitary from .normal_form import NormalForm -from .quantum_gate import ( - HGate, - QuantumCircuit, - QuantumGate, - SGate, - SXGate, - TGate, - WGate, - w_phase, -) +from .quantum_circuit import QuantumCircuit +from .quantum_gate import HGate, QuantumGate, SGate, SXGate, TGate, WGate, w_phase from .ring import OMEGA_POWER BIT_SHIFT = [0, 0, 1, 0, 2, 0, 1, 3, 3, 3, 0, 2, 2, 1, 0, 0] @@ -91,11 +83,11 @@ def decompose_domega_unitary( for _ in range(m_S): circuit.append(SGate(target_qubit=wires[0])) unitary = unitary.mul_by_S_power_from_left(-m_S) - if not up_to_phase: + if up_to_phase: + circuit.phase = m_W * w_phase() + else: for _ in range(m_W): circuit.append(WGate()) - else: - circuit.phase = m_W * w_phase() assert unitary == DOmegaUnitary.identity(), "decomposition failed..." circuit = NormalForm.from_circuit(circuit).to_circuit(wires=wires) diff --git a/pygridsynth/tdgp.py b/pygridsynth/tdgp.py index d4f9893..eb955b0 100644 --- a/pygridsynth/tdgp.py +++ b/pygridsynth/tdgp.py @@ -17,7 +17,7 @@ def solve_TDGP( bboxA: Rectangle, bboxB: Rectangle, k: int, - verbose: bool = False, + verbose: int = 0, show_graph: bool = False, ) -> Iterator[DOmega]: opG_inv = opG.inv diff --git a/pygridsynth/to_upright.py b/pygridsynth/to_upright.py index 5b57920..1ac8aa7 100644 --- a/pygridsynth/to_upright.py +++ b/pygridsynth/to_upright.py @@ -24,11 +24,11 @@ def _shift_ellipse_pair(ellipse_pair: EllipsePair, n: int) -> EllipsePair: def _step_lemma( - ellipse_pair: EllipsePair, opG_l: GridOp, opG_r: GridOp, verbose: bool = False + ellipse_pair: EllipsePair, opG_l: GridOp, opG_r: GridOp, verbose: int = 0 ) -> tuple[EllipsePair, GridOp, GridOp, bool]: A = ellipse_pair.A B = ellipse_pair.B - if verbose: + if verbose >= 3: print("-----") print(f"skew: {ellipse_pair.skew}, bias: {ellipse_pair.bias}") print( @@ -40,19 +40,19 @@ def _step_lemma( ) print("-----") if B.b < 0: - if verbose: + if verbose >= 3: print("Z") OP_Z = GridOp(ZOmega(0, 0, 0, 1), ZOmega(0, -1, 0, 0)) return _reduction(ellipse_pair, opG_l, opG_r, OP_Z) elif A.bias * B.bias < 1: - if verbose: + if verbose >= 3: print("X") OP_X = GridOp(ZOmega(0, 1, 0, 0), ZOmega(0, 0, 0, 1)) return _reduction(ellipse_pair, opG_l, opG_r, OP_X) elif ellipse_pair.bias > 33.971 or ellipse_pair.bias < 0.029437: n = round(log(ellipse_pair.bias) / log(LAMBDA.to_real) / 8) OP_S = GridOp(ZOmega(-1, 0, 1, 1), ZOmega(1, -1, 1, 0)) - if verbose: + if verbose >= 3: print(f"S ({n=})") return _reduction(ellipse_pair, opG_l, opG_r, OP_S**n) elif ellipse_pair.skew <= 15: @@ -60,7 +60,7 @@ def _step_lemma( elif ellipse_pair.bias > 5.8285 or ellipse_pair.bias < 0.17157: n = round(log(ellipse_pair.bias) / log(LAMBDA.to_real) / 4) ellipse_pair = _shift_ellipse_pair(ellipse_pair, n) - if verbose: + if verbose >= 3: print(f"sigma ({n=})") if n >= 0: OP_SIGMA_L = GridOp(ZOmega(-1, 0, 1, 1), ZOmega(0, 1, 0, 0)) ** n @@ -70,36 +70,36 @@ def _step_lemma( OP_SIGMA_R = GridOp(ZOmega(0, 0, 0, 1), ZOmega(1, 1, 1, 0)) ** (-n) return ellipse_pair, opG_l * OP_SIGMA_L, OP_SIGMA_R * opG_r, False elif 0.24410 <= A.bias <= 4.0968 and 0.24410 <= B.bias <= 4.0968: - if verbose: + if verbose >= 3: print("R") OP_R = GridOp(ZOmega(0, 0, 1, 0), ZOmega(1, 0, 0, 0)) return _reduction(ellipse_pair, opG_l, opG_r, OP_R) elif A.b >= 0 and A.bias <= 1.6969: - if verbose: + if verbose >= 3: print("K") OP_K = GridOp(ZOmega(-1, -1, 0, 0), ZOmega(0, -1, 1, 0)) return _reduction(ellipse_pair, opG_l, opG_r, OP_K) elif A.b >= 0 and B.bias <= 1.6969: - if verbose: + if verbose >= 3: print("K_conj_sq2") OP_K_conj_sq2 = GridOp(ZOmega(1, -1, 0, 0), ZOmega(0, -1, -1, 0)) return _reduction(ellipse_pair, opG_l, opG_r, OP_K_conj_sq2) elif A.b >= 0: n = max(1, floorsqrt(min(A.bias, B.bias) / 4)) - if verbose: + if verbose >= 3: print(f"A ({n=})") OP_A_n = GridOp(ZOmega(0, 0, 0, 1), ZOmega(0, 1, 0, 2 * n)) return _reduction(ellipse_pair, opG_l, opG_r, OP_A_n) else: n = max(1, floorsqrt(min(A.bias, B.bias) / 2)) - if verbose: + if verbose >= 3: print(f"B ({n=})") OP_B_n = GridOp(ZOmega(0, 0, 0, 1), ZOmega(n, 1, -n, 0)) return _reduction(ellipse_pair, opG_l, opG_r, OP_B_n) def to_upright_ellipse_pair( - ellipseA: Ellipse, ellipseB: Ellipse, verbose: bool = False + ellipseA: Ellipse, ellipseB: Ellipse, verbose: int = 0 ) -> GridOp: ellipseA_normalized = ellipseA.normalize() ellipseB_normalized = ellipseB.normalize() @@ -120,7 +120,7 @@ def to_upright_set_pair( setB: ConvexSet, opG: GridOp | None = None, show_graph: bool = False, - verbose: bool = False, + verbose: int = 0, ) -> tuple[GridOp, Ellipse, Ellipse, Rectangle, Rectangle]: if opG is None: opG = to_upright_ellipse_pair(setA.ellipse, setB.ellipse, verbose=verbose) @@ -131,7 +131,7 @@ def to_upright_set_pair( bboxB = ellipseB_upright.bbox() upA = ellipseA_upright.area / bboxA.area upB = ellipseB_upright.area / bboxB.area - if verbose: + if verbose >= 2: print(f"{upA=}, {upB=}") if show_graph: plot_sol([], ellipseA_upright, ellipseB_upright, bboxA, bboxB) diff --git a/pygridsynth/two_qubit_unitary_approximation.py b/pygridsynth/two_qubit_unitary_approximation.py new file mode 100644 index 0000000..85257cd --- /dev/null +++ b/pygridsynth/two_qubit_unitary_approximation.py @@ -0,0 +1,465 @@ +import time +import warnings +from typing import Literal, overload + +import mpmath +import numpy as np + +from .config import GridsynthConfig +from .domega_unitary import DOmegaMatrix +from .gridsynth import gridsynth_circuit +from .mymath import ( + MPFConvertible, + all_close, + convert_theta_and_epsilon, + dps_for_epsilon, + kron, + sqrt, + trace, +) +from .quantum_circuit import QuantumCircuit +from .quantum_gate import CxGate, HGate, Rx, RxGate, Rz, RzGate, SingleQubitGate, cnot01 +from .synthesis_of_cliffordT import decompose_domega_unitary +from .unitary_approximation import ( + approximate_one_qubit_unitary, + euler_decompose, + magnitude_approximate, +) + + +def _SU2SU2_to_tensor_products(U: mpmath.matrix) -> tuple[mpmath.matrix, mpmath.matrix]: + U_reshaped = mpmath.matrix( + np.array(U).reshape((2, 2, 2, 2)).transpose(0, 2, 1, 3).reshape(4, 4) + ) + u, s, vh = mpmath.svd_c(U_reshaped) + A = sqrt(s[0]) * mpmath.matrix(np.array(u[:, 0].tolist()).reshape((2, 2))) + B = sqrt(s[0]) * mpmath.matrix(np.array(vh[0, :].tolist()).reshape((2, 2))) + angle = mpmath.arg(mpmath.det(A)) / 2 + A *= mpmath.exp(-1.0j * angle) + B *= mpmath.exp(1.0j * angle) + return A, B + + +def _extract_SU2SU2_prefactors( + U: mpmath.matrix, V: mpmath.matrix +) -> tuple[mpmath.matrix, mpmath.matrix, mpmath.matrix, mpmath.matrix]: + u = E.H @ U @ E + v = E.H @ V @ E + uuT = u @ u.T + vvT = v @ v.T + uuT_real_plus_imag = mpmath.matrix( + [ + [mpmath.re(uuT[i, j]) + mpmath.im(uuT[i, j]) for j in range(4)] + for i in range(4) + ] + ) + vvT_real_plus_imag = mpmath.matrix( + [ + [mpmath.re(vvT[i, j]) + mpmath.im(vvT[i, j]) for j in range(4)] + for i in range(4) + ] + ) + _, p = mpmath.eigsy(uuT_real_plus_imag) + _, q = mpmath.eigsy(vvT_real_plus_imag) + p = p @ mpmath.diag([1, 1, 1, mpmath.sign(mpmath.det(p))]) + q = q @ mpmath.diag([1, 1, 1, mpmath.sign(mpmath.det(q))]) + + G = p @ q.T + H = v.H @ G.T @ u + AB = E @ G @ E.H + A, B = _SU2SU2_to_tensor_products(AB) + CD = E @ H @ E.H + C, D = _SU2SU2_to_tensor_products(CD) + + return A, B, C, D + + +Sy_Sy = mpmath.matrix([[0, 0, 0, -1], [0, 0, 1, 0], [0, 1, 0, 0], [-1, 0, 0, 0]]) +I_Sz = mpmath.matrix([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) +E = ( + 1 + / sqrt(2) + * mpmath.matrix( + [[1, 1.0j, 0, 0], [0, 0, 1.0j, 1], [0, 0, 1.0j, -1], [1, -1.0j, 0, 0]] + ) +) + + +def _gamma(u: mpmath.matrix) -> mpmath.matrix: + return u @ Sy_Sy @ u.T @ Sy_Sy + + +def _decompose_with_2_cnots( + U: mpmath.matrix, wires: list[int], verbose: int = 0 +) -> QuantumCircuit: + m = _gamma(U) + + eig_vals, _ = mpmath.eig(m) + angle_eig_vals = sorted([mpmath.arg(eigval) for eigval in eig_vals]) + r = angle_eig_vals[0] + s = angle_eig_vals[1] + t = angle_eig_vals[2] + diff_r_s = mpmath.fabs(mpmath.exp(1.0j * r) + mpmath.exp(1.0j * s)) + diff_r_t = mpmath.fabs(mpmath.exp(1.0j * r) + mpmath.exp(1.0j * t)) + if diff_r_s < diff_r_t: + s, t = t, s + r = mpmath.fabs(r) + s = mpmath.fabs(s) + theta = (r + s) / 2 + phi = (r - s) / 2 + + V = cnot01() @ kron(Rx(theta), Rz(phi)) @ cnot01() + A, B, C, D = _extract_SU2SU2_prefactors(U, V) + + if verbose >= 1: + print( + "_decompose_with_2_cnots correct?", + all_close(U, kron(A, B) @ V @ kron(C, D)), + ) + + gates = [ + SingleQubitGate(A, wires[0]), + SingleQubitGate(B, wires[1]), + CxGate(wires[0], wires[1]), + RxGate(theta, wires[0]), + RzGate(phi, wires[1]), + CxGate(wires[0], wires[1]), + SingleQubitGate(C, wires[0]), + SingleQubitGate(D, wires[1]), + ] + circuit = QuantumCircuit.from_list(gates) + return circuit + + +@overload +def _decompose_two_qubit_unitary( + U: mpmath.matrix, + wires: list[int] = [0, 1], + decompose_partially: Literal[True] = True, + verbose: int = 0, +) -> tuple[QuantumCircuit, list[mpmath.mpf]]: ... + + +@overload +def _decompose_two_qubit_unitary( + U: mpmath.matrix, + wires: list[int] = [0, 1], + decompose_partially: Literal[False] = False, + verbose: int = 0, +) -> QuantumCircuit: ... + + +def _decompose_two_qubit_unitary( + U: mpmath.matrix, + wires: list[int] = [0, 1], + decompose_partially: bool = False, + verbose: int = 0, +) -> tuple[QuantumCircuit, list[mpmath.mpf]] | QuantumCircuit: + if decompose_partially: + gamma_U = _gamma(U.T) + + psi = mpmath.atan2(mpmath.im(trace(gamma_U)), mpmath.re(trace(I_Sz @ gamma_U))) + Delta = mpmath.diag( + [ + mpmath.exp(-1.0j * psi / 2), + mpmath.exp(1.0j * psi / 2), + mpmath.exp(1.0j * psi / 2), + mpmath.exp(-1.0j * psi / 2), + ] + ) + Delta_inv_diag = [ + mpmath.exp(1.0j * psi / 2), + mpmath.exp(-1.0j * psi / 2), + mpmath.exp(-1.0j * psi / 2), + mpmath.exp(1.0j * psi / 2), + ] + + circuit = _decompose_with_2_cnots(U @ Delta, wires=wires) + + if verbose >= 1: + print( + "_decompose_two_qubit_unitary correct?", + all_close( + U, + mpmath.exp(1.0j * circuit.phase) + * U + @ Delta + @ mpmath.diag(Delta_inv_diag), + ), + ) + return circuit, Delta_inv_diag + else: + U2 = mpmath.exp(0.25j * mpmath.mp.pi) * U @ cnot01() + gamma_U = _gamma(U2.T) + + psi = mpmath.atan2(mpmath.im(trace(gamma_U)), mpmath.re(trace(I_Sz @ gamma_U))) + + Delta = cnot01() @ kron(mpmath.eye(2), Rz(psi)) @ cnot01() + circuit = _decompose_with_2_cnots(U2 @ Delta, wires=wires) + + circuit.phase += -0.25 * mpmath.mp.pi + circuit += [CxGate(wires[0], wires[1]), RzGate(-psi, wires[1])] + if verbose >= 1: + print( + "_decompose_two_qubit_unitary correct?", + all_close( + U, + mpmath.exp(1.0j * circuit.phase) + * kron(circuit[0].matrix, circuit[1].matrix) + @ cnot01() + @ kron(circuit[3].matrix, circuit[4].matrix) + @ cnot01() + @ kron(circuit[6].matrix, circuit[7].matrix) + @ cnot01() + @ kron(mpmath.eye(2), circuit[9].matrix), + ), + ) + return circuit + + +@overload +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: Literal[True] = True, + return_domega_matrix: Literal[True] = True, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, list[mpmath.mpf], DOmegaMatrix]: ... + + +@overload +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: Literal[True] = True, + return_domega_matrix: Literal[False] = False, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, list[mpmath.mpf], mpmath.matrix]: ... + + +@overload +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: Literal[False] = False, + return_domega_matrix: Literal[True] = True, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix]: ... + + +@overload +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: Literal[False] = False, + return_domega_matrix: Literal[False] = False, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, mpmath.matrix]: ... + + +@overload +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: Literal[False] = False, + return_domega_matrix: bool = False, + scale_epsilon: bool = True, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix | mpmath.matrix]: ... + + +def approximate_two_qubit_unitary( + U: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0, 1], + cfg: GridsynthConfig | None = None, + decompose_partially: bool = False, + return_domega_matrix: bool = False, + scale_epsilon: bool = True, + **kwargs, +) -> ( + tuple[QuantumCircuit, list[mpmath.mpf], DOmegaMatrix | mpmath.matrix] + | tuple[QuantumCircuit, DOmegaMatrix | mpmath.matrix] +): + if cfg is None: + cfg = GridsynthConfig(**kwargs) + elif kwargs: + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) + + if cfg.dps is None: + cfg.dps = dps_for_epsilon(epsilon) + cfg.up_to_phase |= decompose_partially + + with mpmath.workdps(cfg.dps): + _, epsilon = convert_theta_and_epsilon("0", epsilon, dps=cfg.dps) + if scale_epsilon: + if decompose_partially: + epsilon /= 12 + else: + epsilon /= 15 + + start = time.time() if cfg.measure_time else 0.0 + if decompose_partially: + decomposed_circuit, diag = _decompose_two_qubit_unitary( + U, + wires=wires, + decompose_partially=decompose_partially, + verbose=cfg.verbose, + ) + else: + decomposed_circuit = _decompose_two_qubit_unitary( + U, + wires=wires, + decompose_partially=decompose_partially, + verbose=cfg.verbose, + ) + if cfg.measure_time: + print("--------------------------------") + print( + "time of _decompose_two_qubit_unitary: " + f"{(time.time() - start) * 1000} ms" + ) + print("--------------------------------") + circuit = QuantumCircuit(phase=decomposed_circuit.phase) + rx_theta_approx = magnitude_approximate( + decomposed_circuit[3].theta, epsilon, cfg=cfg + ) + phase_mag_rx_theta, phi1_mag_rx_theta, theta_mag_rx_theta, phi2_mag_rx_theta = ( + euler_decompose(rx_theta_approx.to_complex_matrix) + ) + circuit_rx_theta_approx = decompose_domega_unitary( + rx_theta_approx, wires=decomposed_circuit[3].wires, up_to_phase=True + ) + rz_phi_approx = magnitude_approximate( + decomposed_circuit[4].theta, epsilon, cfg=cfg + ) + phase_mag_rz_phi, phi1_mag_rz_phi, theta_mag_rz_phi, phi2_mag_rz_phi = ( + euler_decompose(rz_phi_approx.to_complex_matrix) + ) + circuit_rz_phi_approx = decompose_domega_unitary( + rz_phi_approx, wires=decomposed_circuit[4].wires, up_to_phase=True + ) + circuit_rz_phi_approx = ( + [HGate(target_qubit=decomposed_circuit[4].target_qubit)] + + circuit_rz_phi_approx + + [HGate(target_qubit=decomposed_circuit[4].target_qubit)] + ) + + circuit.phase -= phase_mag_rx_theta + circuit.phase -= phase_mag_rz_phi + decomposed_circuit[0].matrix = decomposed_circuit[0].matrix @ Rz( + -phi1_mag_rx_theta + ) + decomposed_circuit[1].matrix = decomposed_circuit[1].matrix @ Rx( + -phi1_mag_rz_phi + ) + decomposed_circuit[6].matrix = ( + Rz(-phi2_mag_rx_theta) @ decomposed_circuit[6].matrix + ) + decomposed_circuit[7].matrix = ( + Rx(-phi2_mag_rz_phi) @ decomposed_circuit[7].matrix + ) + + U_approx = mpmath.eye(2**2) * mpmath.exp(1.0j * circuit.phase) + U_approx = DOmegaMatrix.identity(wires=wires) + U_approx.phase = circuit.phase + for i in range(len(decomposed_circuit)): + if isinstance(decomposed_circuit[i], CxGate): + cx_gate = CxGate( + control_qubit=decomposed_circuit[i].control_qubit, + target_qubit=decomposed_circuit[i].target_qubit, + ) + circuit.append(cx_gate) + U_approx = U_approx @ DOmegaMatrix.from_cx_gate(cx_gate) + else: + if i == 3: + circuit += circuit_rx_theta_approx + U_approx = U_approx @ DOmegaMatrix.from_single_qubit_circuit( + circuit_rx_theta_approx, [decomposed_circuit[i].target_qubit] + ) + elif i == 4: + circuit += circuit_rz_phi_approx + U_approx = U_approx @ DOmegaMatrix.from_single_qubit_circuit( + circuit_rz_phi_approx, [decomposed_circuit[i].target_qubit] + ) + elif decompose_partially and i in [6, 7]: + tmp_circuit, tmp_rz, tmp_unitary = approximate_one_qubit_unitary( + decomposed_circuit[i].matrix, + epsilon, + wires=decomposed_circuit[i].wires, + decompose_partially=decompose_partially, + return_domega_matrix=True, + scale_epsilon=False, + cfg=cfg, + ) + circuit += tmp_circuit + U_approx = U_approx @ tmp_unitary + + if decomposed_circuit[i].target_qubit == wires[0]: + diag[0] *= mpmath.exp(-1.0j * tmp_rz.theta / 2) + diag[1] *= mpmath.exp(-1.0j * tmp_rz.theta / 2) + diag[2] *= mpmath.exp(1.0j * tmp_rz.theta / 2) + diag[3] *= mpmath.exp(1.0j * tmp_rz.theta / 2) + elif decomposed_circuit[i].target_qubit == wires[1]: + diag[0] *= mpmath.exp(-1.0j * tmp_rz.theta / 2) + diag[1] *= mpmath.exp(1.0j * tmp_rz.theta / 2) + diag[2] *= mpmath.exp(-1.0j * tmp_rz.theta / 2) + diag[3] *= mpmath.exp(1.0j * tmp_rz.theta / 2) + elif i == 9: + circuit_rz_psi = gridsynth_circuit( + decomposed_circuit[i].theta, + epsilon, + wires=decomposed_circuit[i].wires, + cfg=cfg, + ) + circuit += circuit_rz_psi + U_approx = U_approx @ DOmegaMatrix.from_single_qubit_circuit( + circuit_rz_psi, [decomposed_circuit[i].target_qubit] + ) + + else: + tmp_circuit, tmp_unitary = approximate_one_qubit_unitary( + decomposed_circuit[i].matrix, + epsilon, + wires=decomposed_circuit[i].wires, + decompose_partially=False, + return_domega_matrix=True, + scale_epsilon=False, + cfg=cfg, + ) + circuit += tmp_circuit + U_approx = U_approx @ tmp_unitary + + if not cfg.up_to_phase: + circuit.decompose_phase_gate() + if cfg.measure_time: + print("--------------------------------") + print( + "time of approximate_two_qubit_unitary: " + f"{(time.time() - start) * 1000} ms" + ) + print("--------------------------------") + if decompose_partially: + if return_domega_matrix: + return circuit, diag, U_approx + else: + return circuit, diag, U_approx.to_complex_matrix + else: + if return_domega_matrix: + return circuit, U_approx + else: + return circuit, U_approx.to_complex_matrix diff --git a/pygridsynth/unitary_approximation.py b/pygridsynth/unitary_approximation.py new file mode 100644 index 0000000..fbcd90e --- /dev/null +++ b/pygridsynth/unitary_approximation.py @@ -0,0 +1,313 @@ +import time +import warnings +from typing import Literal, overload + +import mpmath + +from .config import GridsynthConfig +from .diophantine import Result, diophantine_dyadic +from .domega_unitary import DOmegaMatrix, DOmegaUnitary +from .gridsynth import gridsynth_circuit +from .mymath import MPFConvertible, convert_theta_and_epsilon, dps_for_epsilon, sqrt +from .normal_form import NormalForm +from .odgp import solve_scaled_ODGP +from .quantum_circuit import QuantumCircuit +from .quantum_gate import RzGate +from .region import Interval +from .ring import DRootTwo +from .synthesis_of_cliffordT import decompose_domega_unitary + + +def euler_decompose( + unitary: mpmath.matrix, +) -> tuple[mpmath.mpf, mpmath.mpf, mpmath.mpf, mpmath.mpf]: + # unitary = e^{i phase} e^{- i phi1 / 2 Z} e^{- i theta / 2 X} e^{- i phi2 / 2 Z} + # 0 <= theta <= pi, - pi <= phase < pi, - pi <= phi1 < pi, - pi <= phi2 < pi + + det_arg = mpmath.arg(mpmath.det(unitary)) + psi1 = mpmath.arg(unitary[1, 1]) + psi2 = mpmath.arg(unitary[1, 0]) + + phase = det_arg / 2 + theta = 2 * mpmath.atan2(mpmath.fabs(unitary[1, 0]), mpmath.fabs(unitary[1, 1])) + phi1 = psi1 + psi2 - det_arg + mpmath.mp.pi / 2 + phi2 = psi1 - psi2 - mpmath.mp.pi / 2 + + if phi1 >= mpmath.mp.pi: + phi1 -= 2 * mpmath.mp.pi + phase += mpmath.mp.pi + if phi1 < -mpmath.mp.pi: + phi1 += 2 * mpmath.mp.pi + phase += mpmath.mp.pi + if phi2 >= mpmath.mp.pi: + phi2 -= 2 * mpmath.mp.pi + phase += mpmath.mp.pi + if phi2 <= -mpmath.mp.pi: + phi2 += 2 * mpmath.mp.pi + phase += mpmath.mp.pi + if phase >= mpmath.mp.pi: + phase -= 2 * mpmath.mp.pi + + return (phase, phi1, theta, phi2) + + +def _generate_epsilon_interval(theta: mpmath.mpf, epsilon: mpmath.mpf) -> Interval: + l = mpmath.cos(-theta / 2 - epsilon / 2) ** 2 + r = mpmath.cos(-theta / 2 + epsilon / 2) ** 2 + if l > r: + l, r = r, l + if -epsilon <= theta <= epsilon: + r = 1 + if mpmath.mp.pi - epsilon / 2 <= theta / 2 <= mpmath.mp.pi + epsilon / 2: + l = -1 + if -mpmath.mp.pi - epsilon / 2 <= theta / 2 <= -mpmath.mp.pi + epsilon / 2: + l = -1 + + return Interval(l, r) + + +def unitary_diamond_distance(u1: mpmath.matrix, u2: mpmath.matrix) -> mpmath.mpf: + mat = u1.H @ u2 + eig_vals, _ = mpmath.eig(mat) + phases = [mpmath.arg(eig_val) for eig_val in eig_vals] + d = mpmath.fabs(mpmath.cos((phases[0] - phases[1]) / 2)) + return 2 * sqrt(1 - d**2) + + +def check(u_target: mpmath.matrix, gates: str) -> None: + t_count = gates.count("T") + h_count = gates.count("H") + u_approx = DOmegaUnitary.from_gates(gates) + e = unitary_diamond_distance(u_target, u_approx.to_complex_matrix) + print(f"{gates=}") + print(f"{t_count=}, {h_count=}") + print(f"u_approx={u_approx.to_matrix}") + print(f"{e=}") + + +def _magnitude_approximate_with_fixed_k( + odgp_sets: tuple[Interval, Interval], + k: int, + cfg: GridsynthConfig, +) -> tuple[DOmegaUnitary | None, float, float]: + start = time.time() if cfg.measure_time else 0.0 + sol = solve_scaled_ODGP(*odgp_sets, k) + time_of_solve_ODGP = time.time() - start if cfg.measure_time else 0.0 + + start = time.time() if cfg.measure_time else 0.0 + time_of_diophantine_dyadic = 0.0 + u_approx = None + for m in sol: + if m.parity == 0: + continue + z = diophantine_dyadic(m, seed=cfg.seed, loop_controller=cfg.loop_controller) + if not isinstance(z, Result): + if (z * z.conj).residue == 0: + continue + xi = 1 - DRootTwo.fromDOmega(z.conj * z) + w = diophantine_dyadic( + xi, seed=cfg.seed, loop_controller=cfg.loop_controller + ) + if not isinstance(w, Result): + z = z.reduce_denomexp() + w = w.reduce_denomexp() + if z.k > w.k: + w = w.renew_denomexp(z.k) + elif z.k < w.k: + z = z.renew_denomexp(w.k) + + k1 = (z + w).reduce_denomexp().k + k2 = (z + w.mul_by_omega()).reduce_denomexp().k + if k1 <= k2: + u_approx = DOmegaUnitary(z, w, 0) + else: + u_approx = DOmegaUnitary(z, w.mul_by_omega(), 0) + break + + time_of_diophantine_dyadic += time.time() - start if cfg.measure_time else 0.0 + return u_approx, time_of_solve_ODGP, time_of_diophantine_dyadic + + +def magnitude_approximate( + theta: MPFConvertible, + epsilon: MPFConvertible, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> DOmegaUnitary: + if cfg is None: + cfg = GridsynthConfig(**kwargs) + elif kwargs: + warnings.warn( + "When 'cfg' is provided, 'kwargs' are ignored.", + stacklevel=2, + ) + + if cfg.dps is None: + cfg.dps = dps_for_epsilon(epsilon) + + with mpmath.workdps(cfg.dps): + theta, epsilon = convert_theta_and_epsilon(theta, epsilon, dps=cfg.dps) + + epsilon_interval = _generate_epsilon_interval(theta, epsilon) + unit_interval = Interval(0, 1) + odgp_sets = (epsilon_interval, unit_interval) + + u_approx = None + k = 0 + time_of_solve_ODGP = 0.0 + time_of_diophantine_dyadic = 0.0 + while True: + u_approx, time1, time2 = _magnitude_approximate_with_fixed_k( + odgp_sets, k, cfg=cfg + ) + time_of_solve_ODGP += time1 + time_of_diophantine_dyadic += time2 + if u_approx is not None: + break + + k += 1 + + if cfg.measure_time: + print(f"time of solve_ODGP: {time_of_solve_ODGP * 1000} ms") + print( + "time of diophantine_dyadic: " f"{time_of_diophantine_dyadic * 1000} ms" + ) + if cfg.verbose >= 2: + print(f"{u_approx=}") + return u_approx + + +@overload +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: Literal[True] = True, + return_domega_matrix: Literal[True] = True, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> tuple[QuantumCircuit, RzGate, DOmegaMatrix]: ... + + +@overload +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: Literal[True] = True, + return_domega_matrix: Literal[False] = False, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> tuple[QuantumCircuit, RzGate, mpmath.matrix]: ... + + +@overload +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: Literal[False] = False, + return_domega_matrix: Literal[True] = True, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix]: ... + + +@overload +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: Literal[False] = False, + return_domega_matrix: Literal[False] = False, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> tuple[QuantumCircuit, mpmath.matrix]: ... + + +@overload +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: Literal[False] = False, + return_domega_matrix: bool = False, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> tuple[QuantumCircuit, DOmegaMatrix | mpmath.matrix]: ... + + +def approximate_one_qubit_unitary( + unitary: mpmath.matrix, + epsilon: MPFConvertible, + wires: list[int] = [0], + decompose_partially: bool = False, + return_domega_matrix: bool = False, + scale_epsilon: bool = True, + cfg: GridsynthConfig | None = None, + **kwargs, +) -> ( + tuple[QuantumCircuit, RzGate, DOmegaMatrix | mpmath.matrix] + | tuple[QuantumCircuit, DOmegaMatrix | mpmath.matrix] +): + if cfg is None: + cfg = GridsynthConfig(**kwargs) + elif kwargs: + warnings.warn("When 'cfg' is provided, 'kwargs' are ignored.", stacklevel=2) + + if cfg.dps is None: + cfg.dps = dps_for_epsilon(epsilon) + cfg.up_to_phase |= decompose_partially + + with mpmath.workdps(cfg.dps): + _, epsilon = convert_theta_and_epsilon("0", epsilon, dps=cfg.dps) + if scale_epsilon: + if decompose_partially: + epsilon /= 2 + else: + epsilon /= 3 + + phase, phi1, theta, phi2 = euler_decompose(unitary) + rx_approx = magnitude_approximate(theta, epsilon, cfg=cfg) + phase_mag, phi1_mag, theta_mag, phi2_mag = euler_decompose( + rx_approx.to_complex_matrix + ) + phase -= phase_mag + phi1 -= phi1_mag + phi2 -= phi2_mag + + circuit_rz_left = gridsynth_circuit(phi1, epsilon, wires=wires, cfg=cfg) + circuit_rx_approx = decompose_domega_unitary( + rx_approx, wires=wires, up_to_phase=cfg.up_to_phase + ) + circuit = circuit_rz_left + circuit_rx_approx + circuit.phase += phase + Rz_right = RzGate(phi2, wires[0]) + + if decompose_partially: + circuit = NormalForm.from_circuit(circuit).to_circuit(wires=wires) + U_approx = DOmegaMatrix.from_single_qubit_circuit(circuit, wires=wires) + if return_domega_matrix: + return circuit, Rz_right, U_approx + else: + return circuit, Rz_right, U_approx.to_complex_matrix + + else: + circuit_rz_right = gridsynth_circuit( + Rz_right.theta, epsilon, wires=wires, cfg=cfg + ) + circuit += circuit_rz_right + + if not cfg.up_to_phase: + circuit.decompose_phase_gate() + circuit = NormalForm.from_circuit(circuit).to_circuit(wires=wires) + U_approx = DOmegaMatrix.from_single_qubit_circuit(circuit, wires=wires) + if return_domega_matrix: + return circuit, U_approx + else: + return circuit, U_approx.to_complex_matrix diff --git a/pyproject.toml b/pyproject.toml index d239f20..05be6df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ profile = "black" [tool.flake8] max-line-length = 88 -extend-ignore = "E203, E741, W503" +extend-ignore = "E203, E704, E741, W503" [dependency-groups] dev = [ diff --git a/tests/test_main.py b/tests/test_main.py index 0570615..d898cc6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -49,7 +49,15 @@ def test_main_with_different_inputs(): def test_main_consistency_with_options(): """Test that consistent results are obtained even with options""" - test_args = ["pygridsynth", "0.39269908169", "0.01", "--dps", "30", "--verbose"] + test_args = [ + "pygridsynth", + "0.39269908169", + "0.01", + "--dps", + "30", + "--verbose", + "1", + ] results = [] # Execute 3 times with the same arguments From f2062b6f9b3b9963dd3b192c552e2be452c6c90e Mon Sep 17 00:00:00 2001 From: shun0923 Date: Wed, 10 Dec 2025 04:46:37 +0900 Subject: [PATCH 3/7] Fix bugs --- .vscode/settings.json | 37 +++++++++++++++++++++---------------- pygridsynth/__main__.py | 2 +- pygridsynth/cli.py | 7 ++++--- tests/test_main.py | 20 ++++++++++++-------- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index cbe7cee..77c6856 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,25 +4,12 @@ }, "editor.formatOnSave": true, "cSpell.words": [ + "addopts", "Amano", + "capsys", "CINV", - "Kazuyuki", - "Kliuchnikov", - "Maslov", - "Mosca", - "Matsumoto", - "Nobuyuki", - "ODGP", - "PYTHONPATH", - "Selinger", - "Shuntaro", - "TCONJ", - "TDGP", - "Vadym", - "Yamamoto", - "Yoshioka", - "addopts", "cnot", + "cnots", "decomp", "denomexp", "dloop", @@ -30,6 +17,7 @@ "droottwo", "dtimeout", "eigsy", + "eigval", "facs", "figsize", "floop", @@ -38,31 +26,48 @@ "ftimeout", "gridsynth", "isort", + "Kazuyuki", + "Kliuchnikov", + "kron", "limegreen", + "Maslov", "matplotlib", + "Matsumoto", + "Mosca", "mpmath", "mymath", "mypy", "newsynth", + "Nobuyuki", "numpy", + "ODGP", "orangered", "prec", + "prefactors", "pygridsynth", "pypi", "pytest", + "PYTHONPATH", "qubit", "qubits", "rounddiv", "selfassociate", "selfcoprime", + "Selinger", "setuptools", "showgraph", + "Shuntaro", + "TCONJ", + "TDGP", "testpaths", "unitaries", + "Vadym", "venv", "workdps", "xlabel", + "Yamamoto", "ylabel", + "Yoshioka", "zomega", "zroottwo", ], diff --git a/pygridsynth/__main__.py b/pygridsynth/__main__.py index 8ac78a9..9ae637f 100644 --- a/pygridsynth/__main__.py +++ b/pygridsynth/__main__.py @@ -1,4 +1,4 @@ from .cli import main if __name__ == "__main__": - print(main()) + main() diff --git a/pygridsynth/cli.py b/pygridsynth/cli.py index 9b17d04..77c216d 100644 --- a/pygridsynth/cli.py +++ b/pygridsynth/cli.py @@ -19,7 +19,7 @@ } -def main() -> str: +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("theta", type=str) @@ -46,7 +46,7 @@ def main() -> str: if args.dloop is not None: cfg_args["dloop"] = args.dloop if args.floop is not None: - cfg_args["floop"] = args + cfg_args["floop"] = args.floop if args.seed is not None: cfg_args["seed"] = args.seed if args.verbose: @@ -61,4 +61,5 @@ def main() -> str: cfg = GridsynthConfig(**cfg_args) gates = gridsynth_gates(theta=args.theta, epsilon=args.epsilon, cfg=cfg) - return gates + + print(gates) diff --git a/tests/test_main.py b/tests/test_main.py index d898cc6..8001c85 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ from pygridsynth.__main__ import main -def test_main_deterministic_results(): +def test_main_deterministic_results(capsys): """Test that main function returns the same result for identical inputs""" # pi/8 ≈ 0.39269908169 test_args = ["pygridsynth", "0.39269908169", "0.01"] @@ -16,8 +16,9 @@ def test_main_deterministic_results(): with patch("sys.argv", test_args): original_dps = mpmath.mp.dps try: - result = main() - results.append(str(result)) + main() + captured = capsys.readouterr() + results.append(captured.out.strip()) finally: mpmath.mp.dps = original_dps @@ -25,7 +26,7 @@ def test_main_deterministic_results(): print(f"✓ Got the same result from 5 executions: {results[0]}") -def test_main_with_different_inputs(): +def test_main_with_different_inputs(capsys): """Test that main function works correctly with different inputs""" test_cases = [ ["pygridsynth", "0.78539816339", "0.1"], # pi/4 @@ -38,7 +39,9 @@ def test_main_with_different_inputs(): with patch("sys.argv", test_args): original_dps = mpmath.mp.dps try: - result = main() + main() + captured = capsys.readouterr() + result = captured.out.strip() results.append(result) assert result is not None, f"Result is None for args {test_args}" finally: @@ -47,7 +50,7 @@ def test_main_with_different_inputs(): print(f"✓ Successfully executed with {len(test_cases)} different inputs") -def test_main_consistency_with_options(): +def test_main_consistency_with_options(capsys): """Test that consistent results are obtained even with options""" test_args = [ "pygridsynth", @@ -65,8 +68,9 @@ def test_main_consistency_with_options(): with patch("sys.argv", test_args): original_dps = mpmath.mp.dps try: - result = main() - results.append(str(result)) + main() + captured = capsys.readouterr() + results.append(captured.out.strip()) finally: mpmath.mp.dps = original_dps From 91121351b8b698e423ddc28792ac002eae9ac558 Mon Sep 17 00:00:00 2001 From: shun0923 Date: Wed, 10 Dec 2025 07:07:18 +0900 Subject: [PATCH 4/7] Add tests --- .pre-commit-config.yaml | 3 +- pygridsynth/unitary_approximation.py | 6 +- .../test_multi_qubit_unitary_approximation.py | 85 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/test_multi_qubit_unitary_approximation.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 804979b..55fd90b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,8 @@ repos: name: pytest entry: bash -c 'PYTHONPATH=./ .venv/bin/pytest tests' language: system - types: [python] + pass_filenames: false + always_run: true - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/pygridsynth/unitary_approximation.py b/pygridsynth/unitary_approximation.py index fbcd90e..6578125 100644 --- a/pygridsynth/unitary_approximation.py +++ b/pygridsynth/unitary_approximation.py @@ -59,9 +59,11 @@ def _generate_epsilon_interval(theta: mpmath.mpf, epsilon: mpmath.mpf) -> Interv if -epsilon <= theta <= epsilon: r = 1 if mpmath.mp.pi - epsilon / 2 <= theta / 2 <= mpmath.mp.pi + epsilon / 2: - l = -1 + r = 1 if -mpmath.mp.pi - epsilon / 2 <= theta / 2 <= -mpmath.mp.pi + epsilon / 2: - l = -1 + r = 1 + if mpmath.mp.pi / 2 - epsilon / 2 <= theta / 2 <= mpmath.mp.pi / 2 + epsilon / 2: + l = 0 return Interval(l, r) diff --git a/tests/test_multi_qubit_unitary_approximation.py b/tests/test_multi_qubit_unitary_approximation.py new file mode 100644 index 0000000..2f08c5c --- /dev/null +++ b/tests/test_multi_qubit_unitary_approximation.py @@ -0,0 +1,85 @@ +import mpmath +import pytest + +from pygridsynth.config import GridsynthConfig +from pygridsynth.domega_unitary import DOmegaMatrix +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) +from pygridsynth.mymath import all_close +from pygridsynth.quantum_circuit import QuantumCircuit + + +def test_approximate_one_qubit_unitary_identity(): + U = mpmath.matrix([[1, 0], [0, 1]]) + num_qubits = 1 + epsilon = "0.0001" + + circuit, approx_unitary = approximate_multi_qubit_unitary(U, num_qubits, epsilon) + + assert isinstance(circuit, QuantumCircuit) + assert isinstance(approx_unitary, mpmath.matrix) + assert len(circuit) == 0 + assert all_close(approx_unitary, U, tol=float(epsilon)) + + +def test_approximate_two_qubit_unitary(): + U = mpmath.matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) + num_qubits = 2 + epsilon = "0.0001" + + circuit, approx_unitary = approximate_multi_qubit_unitary(U, num_qubits, epsilon) + + assert isinstance(circuit, QuantumCircuit) + assert isinstance(approx_unitary, mpmath.matrix) + assert len(circuit) > 0 + assert all_close(approx_unitary, U, tol=float(epsilon)) + + +def test_approximate_three_qubit_unitary_identity(): + num_qubits = 3 + n = 2**num_qubits + U = mpmath.eye(n) + epsilon = "0.0001" + + circuit, approx_unitary = approximate_multi_qubit_unitary( + U, num_qubits, epsilon, return_domega_matrix=True + ) + + assert isinstance(circuit, QuantumCircuit) + assert isinstance(approx_unitary, DOmegaMatrix) + assert all_close(approx_unitary.to_complex_matrix, U, tol=float(epsilon)) + + +def test_approximate_multi_qubit_unitary_return_matrix_type(): + U = mpmath.matrix([[1, 0], [0, 1]]) + num_qubits = 1 + epsilon = "0.0001" + + circuit, result_matrix = approximate_multi_qubit_unitary( + U, num_qubits, epsilon, return_domega_matrix=False + ) + assert isinstance(circuit, QuantumCircuit) + assert isinstance(result_matrix, mpmath.matrix) + + circuit, result_domega = approximate_multi_qubit_unitary( + U, num_qubits, epsilon, return_domega_matrix=True + ) + assert isinstance(circuit, QuantumCircuit) + assert isinstance(result_domega, DOmegaMatrix) + + +def test_approximate_multi_qubit_unitary_config_handling(): + U = mpmath.matrix([[1, 0], [0, 1]]) + num_qubits = 1 + epsilon = "0.0001" + + custom_cfg = GridsynthConfig(dps=50, measure_time=True) + with pytest.warns( + UserWarning, match="When 'cfg' is provided, 'kwargs' are ignored." + ): + approximate_multi_qubit_unitary( + U, num_qubits, epsilon, cfg=custom_cfg, verbose=100 + ) + + approximate_multi_qubit_unitary(U, num_qubits, epsilon, verbose=1) From 77d990328a66aca63af0eebcaed62e8b525d5d5e Mon Sep 17 00:00:00 2001 From: shun0923 Date: Tue, 23 Dec 2025 14:30:27 +0900 Subject: [PATCH 5/7] Add mixed synthesis --- .vscode/settings.json | 19 + pygridsynth/mixed_synthesis.py | 389 ++++++++++++++ pygridsynth/mixed_synthesis_parallel.py | 156 ++++++ pygridsynth/mixed_synthesis_sequential.py | 109 ++++ pygridsynth/mixed_synthesis_utils.py | 488 ++++++++++++++++++ pygridsynth/mymath.py | 19 + pygridsynth/test.py | 11 + pyproject.toml | 4 + requirements-dev.txt | 5 +- tests/test_mixed_synthesis.py | 183 +++++++ .../test_multi_qubit_unitary_approximation.py | 5 + 11 files changed, 1387 insertions(+), 1 deletion(-) create mode 100644 pygridsynth/mixed_synthesis.py create mode 100644 pygridsynth/mixed_synthesis_parallel.py create mode 100644 pygridsynth/mixed_synthesis_sequential.py create mode 100644 pygridsynth/mixed_synthesis_utils.py create mode 100644 pygridsynth/test.py create mode 100644 tests/test_mixed_synthesis.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 77c6856..443e824 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,17 +5,22 @@ "editor.formatOnSave": true, "cSpell.words": [ "addopts", + "allclose", "Amano", "capsys", + "choi", "CINV", "cnot", "cnots", + "cvxpy", "decomp", "denomexp", "dloop", "domega", "droottwo", "dtimeout", + "ECOS", + "eigh", "eigsy", "eigval", "facs", @@ -24,12 +29,16 @@ "floorlog", "floorsqrt", "ftimeout", + "FWHT", + "gptm", "gridsynth", + "GUROBI", "isort", "Kazuyuki", "Kliuchnikov", "kron", "limegreen", + "linalg", "Maslov", "matplotlib", "Matsumoto", @@ -38,7 +47,10 @@ "mymath", "mypy", "newsynth", + "njit", "Nobuyuki", + "nonneg", + "numba", "numpy", "ODGP", "orangered", @@ -50,19 +62,26 @@ "PYTHONPATH", "qubit", "qubits", + "qulacs", + "randn", "rounddiv", + "scipy", "selfassociate", "selfcoprime", "Selinger", + "semidefinite", "setuptools", "showgraph", "Shuntaro", "TCONJ", "TDGP", "testpaths", + "triu", "unitaries", "Vadym", + "vecs", "venv", + "Weyl", "workdps", "xlabel", "Yamamoto", diff --git a/pygridsynth/mixed_synthesis.py b/pygridsynth/mixed_synthesis.py new file mode 100644 index 0000000..7a717fe --- /dev/null +++ b/pygridsynth/mixed_synthesis.py @@ -0,0 +1,389 @@ +""" +Mixed synthesis module. +""" + +from __future__ import annotations + +import os +from multiprocessing import Pool + +import cvxpy as cp +import mpmath +import numpy as np +import scipy + +from .mixed_synthesis_utils import ( + _diamond_norm_choi, + get_random_hermitian_operator, + unitary_to_choi, + unitary_to_gptm, +) +from .multi_qubit_unitary_approximation import approximate_multi_qubit_unitary +from .mymath import dps_for_epsilon +from .quantum_circuit import QuantumCircuit +from .quantum_gate import CxGate, HGate, SGate, SXGate, TGate, WGate # noqa: F401 + + +def _get_default_solver() -> str | None: + """ + Get the default solver with priority order. + + Returns: + Available solver name, or None if no solver is available. + """ + available_solvers = cp.installed_solvers() + + preferred_solvers = ["GUROBI", "SCS", "ECOS", "OSQP"] + + for solver_name in preferred_solvers: + if solver_name in available_solvers: + return solver_name + + if available_solvers: + return available_solvers[0] + + return None + + +def solve_LP( + A: np.ndarray, + b: np.ndarray, + eps: float, + scale: float = 1.0, + solver: str | None = None, +) -> np.ndarray | None: + """ + Solve a linear programming problem for optimal mixing probabilities. + + Args: + A: Constraint matrix. + b: Constraint vector. + eps: Error tolerance parameter. + scale: Scale factor. + solver: Solver to use (None for auto-selection). + + Returns: + Optimal solution vector, or None on failure. + """ + n, m = A.shape + x = cp.Variable(m) + r = cp.Variable(n) + + x.value = np.ones(m) / (eps * m) + objective = cp.Minimize(cp.sum(r)) + constraints = [ + cp.sum(x) * eps == 1, + x >= 0, + r >= 0, + A @ x - b / eps <= r / scale, + -(A @ x - b / eps) <= r / scale, + ] + prob = cp.Problem(objective, constraints) + + if solver is None: + solver = _get_default_solver() + if solver is None: + return None + + try: + prob.solve(solver=solver, verbose=False) + except Exception: + return None + if x.value is None: + return None + else: + x = x.value / np.sum(x.value) + return x + + +def approximate_unitary_task( + eu: list[list[complex]], num_qubits: int, eps: float, dps: int = -1 +) -> tuple[float, list[str], np.ndarray, np.ndarray, np.ndarray]: + """ + Compute approximation task for a perturbed unitary matrix. + + Args: + eu: Target unitary matrix perturbed by a Hermitian operator (as a list). + num_qubits: Number of qubits. + eps: Error tolerance parameter. + dps: Decimal precision (default: -1 for auto). + + Returns: + Tuple of (phase, gates, eu_np, eu_gptm, eu_choi). + - phase: Phase of the circuit. + - gates: List of gate strings in the circuit. + - eu_np: Approximated unitary matrix (numpy array). + - eu_gptm: GPTM representation of approximated unitary. + - eu_choi: Choi representation of approximated unitary. + """ + if dps == -1: + dps = dps_for_epsilon(eps) + with mpmath.workdps(dps): + circuit, U_approx = approximate_multi_qubit_unitary( + mpmath.matrix(eu), num_qubits, mpmath.mpf(eps), return_domega_matrix=False + ) + eu_np = np.array(U_approx.tolist(), dtype=complex) + eu_gptm = unitary_to_gptm(eu_np) + eu_choi = unitary_to_choi(eu_np) + return float(circuit.phase), [str(g) for g in circuit], eu_np, eu_gptm, eu_choi + + +def compute_optimal_mixing_probabilities( + u_gptm: np.ndarray, + num_qubits: int, + eu_gptm_list: list[np.ndarray], + eu_choi_list: list[np.ndarray], + eps: float, +) -> tuple[np.ndarray, np.ndarray] | None: + """ + Compute optimal mixing probabilities + from GPTM representation of perturbed unitaries. + + Args: + u_gptm: GPTM representation of target unitary. + num_qubits: Number of qubits. + eu_gptm_list: List of GPTM representations of perturbed unitaries. + eu_choi_list: List of Choi representations of perturbed unitaries. + eps: Error tolerance parameter. + + Returns: + Tuple of (probs_gptm, u_choi_opt), or None on failure. + - probs_gptm: Array of mixing probabilities. + - u_choi_opt: Optimal Choi matrix of the mixed unitary. + """ + A = np.array([eu_gptm.reshape(eu_gptm.size) for eu_gptm in eu_gptm_list]).T.real + b = u_gptm.reshape(2 ** (4 * num_qubits)).real + + probs_gptm = solve_LP(A, b, eps=eps, scale=1e-2 / eps) + if probs_gptm is None: + return None + else: + u_choi_opt = np.einsum("i,ijk->jk", probs_gptm, eu_choi_list) + + return probs_gptm, u_choi_opt + + +def compute_diamond_norm_error( + u_choi: np.ndarray, u_choi_opt: np.ndarray, eps: float +) -> float: + """ + Compute error using diamond norm. + + Args: + u_choi: Choi representation of target unitary. + u_choi_opt: Choi representation of mixed unitary. + eps: Error tolerance parameter. + + Returns: + Diamond norm error between the target and mixed unitaries. + """ + return _diamond_norm_choi(u_choi, u_choi_opt, scale=1e-2 / eps**2) + + +def process_unitary_approximation_parallel( + unitary: mpmath.matrix, + num_qubits: int, + eps: float, + M: int, + seed: int = 123, + dps: int = -1, +) -> tuple[ + np.ndarray, + np.ndarray, + list[QuantumCircuit], + list[np.ndarray], + list[np.ndarray], + list[np.ndarray], +]: + """ + Process unitary approximation in parallel. + + Args: + unitary: Target unitary matrix. + num_qubits: Number of qubits. + eps: Error tolerance parameter. + M: Number of Hermitian operators for perturbation. + seed: Random seed. + dps: Decimal precision (default: -1 for auto). + + Returns: + Tuple of (u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list). + - u_gptm: GPTM representation of target unitary. + - u_choi: Choi representation of target unitary. + - circuit_list: List of QuantumCircuit objects for perturbed unitaries. + - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. + - eu_gptm_list: List of GPTM representations of perturbed unitaries. + - eu_choi_list: List of Choi representations of perturbed unitaries. + """ + herm_list = get_random_hermitian_operator(2**num_qubits, M, seed=seed) + unitary_np = np.array(unitary.tolist(), dtype=complex) + + u_choi = unitary_to_choi(unitary_np) + u_gptm = unitary_to_gptm(unitary_np) + eu_list = [ + (unitary @ mpmath.matrix(scipy.linalg.expm(1j * _herm * eps))).tolist() + for _herm in herm_list + ] + + num_workers = os.cpu_count() + with Pool(processes=num_workers) as pool: + results = pool.starmap( + approximate_unitary_task, + [(eu, num_qubits, eps, dps) for eu in eu_list], + ) + circuit_list = [ + QuantumCircuit(phase=result[0], args=[eval(g) for g in result[1]]) + for result in results + ] + eu_np_list = [result[2] for result in results] + eu_gptm_list = [result[3] for result in results] + eu_choi_list = [result[4] for result in results] + return (u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list) + + +def process_unitary_approximation_sequential( + unitary: mpmath.matrix, + num_qubits: int, + eps: float, + M: int, + seed: int = 123, + dps: int = -1, +) -> tuple[ + np.ndarray, + np.ndarray, + list[QuantumCircuit], + list[np.ndarray], + list[np.ndarray], + list[np.ndarray], +]: + """ + Process unitary approximation sequentially. + + Args: + unitary: Target unitary matrix. + num_qubits: Number of qubits. + eps: Error tolerance parameter. + M: Number of Hermitian operators for perturbation. + seed: Random seed. + dps: Decimal precision (default: -1 for auto). + + Returns: + Tuple of (u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list). + - u_gptm: GPTM representation of target unitary. + - u_choi: Choi representation of target unitary. + - circuit_list: List of QuantumCircuit objects for perturbed unitaries. + - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. + - eu_gptm_list: List of GPTM representations of perturbed unitaries. + - eu_choi_list: List of Choi representations of perturbed unitaries. + """ + herm_list = get_random_hermitian_operator(2**num_qubits, M, seed=seed) + unitary_np = np.array(unitary.tolist(), dtype=complex) + u_choi = unitary_to_choi(unitary_np) + u_gptm = unitary_to_gptm(unitary_np) + eu_list = [ + (unitary @ mpmath.matrix(scipy.linalg.expm(1j * _herm * eps))).tolist() + for _herm in herm_list + ] + + circuit_list = [] + eu_np_list = [] + eu_gptm_list = [] + eu_choi_list = [] + for eu in eu_list: + result = approximate_unitary_task(eu, num_qubits, eps, dps=dps) + circuit = QuantumCircuit(phase=result[0], args=[eval(g) for g in result[1]]) + circuit_list.append(circuit) + eu_np_list.append(result[2]) + eu_gptm_list.append(result[3]) + eu_choi_list.append(result[4]) + return (u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list) + + +def mixed_synthesis_parallel( + unitary: mpmath.matrix | np.ndarray, + num_qubits: int, + eps: float, + M: int, + seed: int = 123, + dps: int = -1, +) -> tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray] | None: + """ + Compute mixed probabilities for mixed unitary synthesis (parallel version). + + Args: + unitary: Target unitary matrix. + num_qubits: Number of qubits. + eps: Error tolerance parameter. + M: Number of Hermitian operators for perturbation. + seed: Random seed. + dps: Decimal precision (default: -1 for auto). + + Returns: + Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi_opt), or None on failure. + - circuit_list: List of QuantumCircuits for perturbed unitaries. + - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. + - probs_gptm: Array of mixing probabilities. + - u_choi_opt: Optimal mixed Choi matrix. + """ + if not isinstance(unitary, mpmath.matrix): + unitary = mpmath.matrix(unitary) + + u_gptm, _, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( + process_unitary_approximation_parallel( + unitary, num_qubits, eps, M, seed=seed, dps=dps + ) + ) + result = compute_optimal_mixing_probabilities( + u_gptm, num_qubits, eu_gptm_list, eu_choi_list, eps + ) + if result is None: + return None + + probs_gptm, u_choi_opt = result + + return circuit_list, eu_np_list, probs_gptm, u_choi_opt + + +def mixed_synthesis_sequential( + unitary: mpmath.matrix | np.ndarray, + num_qubits: int, + eps: float, + M: int, + seed: int = 123, + dps: int = -1, +) -> tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray] | None: + """ + Compute mixed probabilities for mixed unitary synthesis (sequential version). + + Args: + unitary: Target unitary matrix. + num_qubits: Number of qubits. + eps: Error tolerance parameter. + M: Number of Hermitian operators for perturbation. + seed: Random seed. + dps: Decimal precision (default: -1 for auto). + + Returns: + Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi_opt), or None on failure. + - circuit_list: List of QuantumCircuits for perturbed unitaries. + - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. + - probs_gptm: Array of mixing probabilities. + - u_choi_opt: Optimal mixed Choi matrix. + """ + if not isinstance(unitary, mpmath.matrix): + unitary = mpmath.matrix(unitary) + + u_gptm, _, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( + process_unitary_approximation_sequential( + unitary, num_qubits, eps, M, seed=seed, dps=dps + ) + ) + result = compute_optimal_mixing_probabilities( + u_gptm, num_qubits, eu_gptm_list, eu_choi_list, eps + ) + if result is None: + return None + + probs_gptm, u_choi_opt = result + + return circuit_list, eu_np_list, probs_gptm, u_choi_opt diff --git a/pygridsynth/mixed_synthesis_parallel.py b/pygridsynth/mixed_synthesis_parallel.py new file mode 100644 index 0000000..371e604 --- /dev/null +++ b/pygridsynth/mixed_synthesis_parallel.py @@ -0,0 +1,156 @@ +""" +Parallel execution version of mixed synthesis. +""" + +from __future__ import annotations + +import os +import warnings +from multiprocessing import Pool +from typing import TYPE_CHECKING + +import cvxpy as cp +import numpy as np + +from .mixed_synthesis import ( + compute_diamond_norm_error, + compute_optimal_mixing_probabilities, + process_unitary_approximation_parallel, +) +from .mymath import random_su + +if TYPE_CHECKING: + from .quantum_circuit import QuantumCircuit + +warnings.filterwarnings("ignore", category=UserWarning) + + +def my_task( + args: tuple[ + int, np.ndarray, np.ndarray, int, list[np.ndarray], list[np.ndarray], float + ], +) -> tuple[int, np.ndarray | None, float | None]: + """ + Compute optimal mixing probabilities and diamond norm error for a task. + + Args: + args: Tuple of + (idx, u_gptm, u_choi, num_qubits, eu_gptm_list, eu_choi_list, eps). + - idx: Task index. + - u_gptm: GPTM representation of target unitary. + - u_choi: Choi representation of target unitary. + - num_qubits: Number of qubits. + - eu_gptm_list: List of GPTM representations of perturbed unitaries. + - eu_choi_list: List of Choi representations of perturbed unitaries. + - eps: Error tolerance parameter. + + Returns: + Tuple of (idx, probs_gptm, error). + - idx: Task index. + - probs_gptm: Array of mixing probabilities, or None on failure. + - error: Diamond norm error, or None on failure. + """ + idx, u_gptm, u_choi, num_qubits, eu_gptm_list, eu_choi_list, eps = args + result = compute_optimal_mixing_probabilities( + u_gptm, num_qubits, eu_gptm_list, eu_choi_list, eps + ) + if result is None: + return (idx, None, None) + probs_gptm, u_choi_opt = result + error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + return (idx, probs_gptm, error) + + +def main() -> list[ + tuple[ + int, + float, + list[QuantumCircuit], + list[np.ndarray], + np.ndarray | None, + float | None, + ] +]: + """ + Main processing function. + + Returns: + List of results as tuples of + (num_qubits, eps, circuits, eu_np_list, probs_gptm, error). + - num_qubits: Number of qubits. + - eps: Error tolerance parameter. + - circuits: List of QuantumCircuits for perturbed unitaries. + - eu_np_list: List of approximated unitary matrices (numpy arrays). + - probs_gptm: Array of mixing probabilities, or None on failure. + - error: Diamond norm error, or None on failure. + """ + # Parameter settings + eps_list = [1e-3, 1e-4, 1e-5, 1e-6] + num_qubits_list = [1, 2] + M_list = [16, 64] + num_trial = 2 + + # Initialize GUROBI solver + cp.Problem(cp.Minimize(cp.Variable(1)), []).solve(solver=cp.GUROBI, verbose=False) + + final_results: list[ + tuple[ + int, + float, + list[QuantumCircuit], + list[np.ndarray], + np.ndarray | None, + float | None, + ] + ] = [] + tasks = [] + idx = 0 + + for num_qubits, M in zip(num_qubits_list, M_list): + unitaries = [random_su(num_qubits) for _ in range(num_trial)] + + # Process all unitary approximations first + + for eps in eps_list: + for unitary in unitaries: + # Process unitary approximation in parallel + u_gptm, u_choi, circuits, eu_np_list, eu_gptm_list, eu_choi_list = ( + process_unitary_approximation_parallel( + unitary, num_qubits, eps, M, seed=123 + ) + ) + final_results.append( + (num_qubits, eps, circuits, eu_np_list, None, None) + ) + tasks.append( + (idx, u_gptm, u_choi, num_qubits, eu_gptm_list, eu_choi_list, eps) + ) + idx += 1 + + # Compute optimal mixing probabilities in parallel + num_workers = os.cpu_count() + with Pool(processes=num_workers) as pool: + results = pool.map(my_task, tasks) + for result in results: + idx, probs_gptm, error = result + num_qubits, eps, circuits, eu_np_list, _, _ = final_results[idx] + final_results[idx] = ( + num_qubits, + eps, + circuits, + eu_np_list, + probs_gptm, + error, + ) + + return final_results + + +if __name__ == "__main__": + results = main() + for result in results: + num_qubits, eps, circuits, eu_np_list, probs_gptm, error = result + print(f"num_qubits: {num_qubits}") + print(f"eps: {eps}") + print(f"error: {error:.4e}") + print("--------------------------------") diff --git a/pygridsynth/mixed_synthesis_sequential.py b/pygridsynth/mixed_synthesis_sequential.py new file mode 100644 index 0000000..e2ae5af --- /dev/null +++ b/pygridsynth/mixed_synthesis_sequential.py @@ -0,0 +1,109 @@ +""" +Sequential execution version of mixed synthesis. +""" + +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import cvxpy as cp +import numpy as np + +from .mixed_synthesis import ( + compute_diamond_norm_error, + compute_optimal_mixing_probabilities, + process_unitary_approximation_sequential, +) +from .mymath import random_su + +if TYPE_CHECKING: + from .quantum_circuit import QuantumCircuit + +warnings.filterwarnings("ignore", category=UserWarning) + + +def main() -> list[ + tuple[ + int, + float, + list[QuantumCircuit], + list[np.ndarray], + np.ndarray | None, + float | None, + ] +]: + """ + Main processing function. + + Returns: + List of results as tuples of + (num_qubits, eps, circuits, eu_np_list, probs_gptm, error). + - num_qubits: Number of qubits. + - eps: Error tolerance parameter. + - circuits: List of QuantumCircuits for perturbed unitaries. + - eu_np_list: List of approximated unitary matrices (numpy arrays). + - probs_gptm: Array of mixing probabilities, or None on failure. + - error: Diamond norm error, or None on failure. + """ + # Parameter settings + eps_list = [1e-3, 1e-4] + num_qubits_list = [1, 2] + M_list = [16, 64] + num_trial = 2 + + # Initialize GUROBI solver + if "GUROBI" in cp.installed_solvers(): + cp.Problem(cp.Minimize(cp.Variable(1)), []).solve( + solver=cp.GUROBI, verbose=False + ) + + final_results: list[ + tuple[ + int, + float, + list[QuantumCircuit], + list[np.ndarray], + np.ndarray | None, + float | None, + ] + ] = [] + + for num_qubits, M in zip(num_qubits_list, M_list): + unitaries = [random_su(num_qubits) for _ in range(num_trial)] + + # Process each combination of eps and unitary + for eps in eps_list: + for unitary in unitaries: + # Process unitary approximation sequentially + u_gptm, u_choi, circuits, eu_np_list, eu_gptm_list, eu_choi_list = ( + process_unitary_approximation_sequential( + unitary, num_qubits, eps, M, seed=123 + ) + ) + # Compute optimal mixing probabilities + result = compute_optimal_mixing_probabilities( + u_gptm, num_qubits, eu_gptm_list, eu_choi_list, eps + ) + if result is None: + final_results.append( + (num_qubits, eps, circuits, eu_np_list, None, None) + ) + else: + probs_gptm, u_choi_opt = result + error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + final_results.append( + (num_qubits, eps, circuits, eu_np_list, probs_gptm, error) + ) + + return final_results + + +if __name__ == "__main__": + results = main() + for result in results: + num_qubits, eps, circuits, eu_np_list, probs_gptm, error = result + print(f"num_qubits: {num_qubits}") + print(f"eps: {eps}") + print(f"error: {error:.4e}") + print("--------------------------------") diff --git a/pygridsynth/mixed_synthesis_utils.py b/pygridsynth/mixed_synthesis_utils.py new file mode 100644 index 0000000..bef10ad --- /dev/null +++ b/pygridsynth/mixed_synthesis_utils.py @@ -0,0 +1,488 @@ +""" +Utility functions for mixed synthesis. +""" + +from __future__ import annotations + +import cvxpy as cp +import numpy as np +from numba import njit + + +@njit +def vector_norm(x: np.ndarray) -> float: + """Compute Euclidean norm of a 1D array.""" + s = 0.0 + for i in range(x.shape[0]): + s += x[i] * x[i] + return np.sqrt(s) + + +@njit +def repulsive_update_numba( + points: np.ndarray, step: float, epsilon: float +) -> np.ndarray: + """ + Update points based on repulsive forces between them. + For each point i, compute repulsive force from all other points: + force = Σ_{j≠i} (x_i - x_j) / (||x_i - x_j||^2 + epsilon) + Project to tangent space, move by step, and project back to sphere. + """ + M, d = points.shape + new_points = np.empty_like(points) + for i in range(M): + force = np.zeros(d) + for j in range(M): + if i == j: + continue + diff = points[i] - points[j] + dist_sq = 0.0 + for k in range(d): + dist_sq += diff[k] * diff[k] + for k in range(d): + force[k] += diff[k] / (dist_sq + epsilon) + # Project to tangent space: subtract (force・x_i) x_i from force + dot = 0.0 + for k in range(d): + dot += force[k] * points[i][k] + tangent_force = np.empty(d) + for k in range(d): + tangent_force[k] = force[k] - dot * points[i][k] + # Update point and compute new position + x_new = np.empty(d) + for k in range(d): + x_new[k] = points[i][k] + step * tangent_force[k] + norm_val = vector_norm(x_new) + for k in range(d): + new_points[i][k] = x_new[k] / norm_val + return new_points + + +def relax_points_numba( + points: np.ndarray, + iterations: int = 100, + step: float = 0.001, + epsilon: float = 1e-6, +) -> np.ndarray: + """ + Relax point cloud using Numba-compiled repulsive_update_numba. + + Args: + points: Point cloud to relax. + iterations: Number of relaxation iterations. + step: Step size for updates. + epsilon: Small constant for numerical stability. + + Returns: + Relaxed point cloud. + """ + for _ in range(iterations): + points = repulsive_update_numba(points, step, epsilon) + return points + + +def initial_points(d: int, M: int, seed: int | None = None) -> np.ndarray: + """ + Uniformly sample M points from the unit sphere S^(d-1) in d dimensions. + + Args: + d: Dimension of the space. + M: Number of points to sample. + seed: Random seed (optional). + + Returns: + Array of shape (M, d) containing the sampled points. + """ + points = np.empty((M, d)) + if seed is not None: + np.random.seed(seed) + for i in range(M): + x = np.random.randn(d) + norm_val = np.linalg.norm(x) + points[i] = x / norm_val + return points + + +def vector_to_upper_triangular(vector: np.ndarray, d: int) -> np.ndarray: + """ + Convert a vector to an upper triangular matrix. + + Args: + vector: Vector to convert. + d: Dimension of the square matrix. + + Returns: + Upper triangular matrix (excluding diagonal). + """ + matrix = np.zeros((d, d)) + indices = np.triu_indices(d, k=1) + matrix[indices] = vector + return matrix + + +def points_to_hermitian_matrix(points: np.ndarray) -> np.ndarray: + """ + Convert points to a Hermitian matrix. + + Args: + points: Points array. + + Returns: + Hermitian matrix. + """ + hilbert_dim = int(np.sqrt(len(points) + 1)) + assert hilbert_dim**2 - 1 == len(points) + + non_diag_real = points[: (hilbert_dim * (hilbert_dim - 1)) // 2] + non_diag_imag = points[ + (hilbert_dim * (hilbert_dim - 1)) // 2 : hilbert_dim * (hilbert_dim - 1) + ] + diag = list(points[hilbert_dim * (hilbert_dim - 1) :]) + [ + -sum(points[(hilbert_dim * (hilbert_dim - 1)) :]) + ] + + mat = vector_to_upper_triangular( + non_diag_real, hilbert_dim + ) + 1j * vector_to_upper_triangular(non_diag_imag, hilbert_dim) + mat = mat + mat.conj().T + mat += np.diag(diag) + + return mat + + +def operate_M_tensor(vector: np.ndarray, M: np.ndarray) -> None: + r""" + In-place Fast Walsh-Hadamard Transform-like operation M^\otimes n @ vec. + + Args: + vector: Vector to transform (modified in-place). + M: Transformation matrix. + """ + h = 1 + d = M.shape[0] + + while h < len(vector): + # perform FWHT + for i in range(0, len(vector), h * d): + for j in range(i, i + h): + if d == 2: + x = vector[j % len(vector)] + y = vector[(j + h) % len(vector)] + vector[j % len(vector)] = M[0, 0] * x + M[0, 1] * y + vector[(j + h) % len(vector)] = M[1, 0] * x + M[1, 1] * y + elif d == 4: + x = vector[j % len(vector)] + y = vector[(j + h) % len(vector)] + z = vector[(j + 2 * h) % len(vector)] + w = vector[(j + 3 * h) % len(vector)] + + vector[j % len(vector)] = ( + M[0, 0] * x + M[0, 1] * y + M[0, 2] * z + M[0, 3] * w + ) + vector[(j + h) % len(vector)] = ( + M[1, 0] * x + M[1, 1] * y + M[1, 2] * z + M[1, 3] * w + ) + vector[(j + 2 * h) % len(vector)] = ( + M[2, 0] * x + M[2, 1] * y + M[2, 2] * z + M[2, 3] * w + ) + vector[(j + 3 * h) % len(vector)] = ( + M[3, 0] * x + M[3, 1] * y + M[3, 2] * z + M[3, 3] * w + ) + + # normalize and increment + h *= d + + +def permutate_qubit_vector(vector: np.ndarray, new_order: list[int]) -> np.ndarray: + """ + Change qubit order (replacement for qulacs permutate_qubit). + + Args: + vector: Vector of dimension 2^(n_qubit). + new_order: New qubit order (e.g., [0, 2, 1, 3] for 2 qubits). + + Returns: + Vector with reordered qubits. + """ + n_qubits = len(new_order) + dim = 2**n_qubits + + tensor = vector.reshape([2] * n_qubits) + transposed = np.transpose(tensor, new_order) + return transposed.reshape(dim) + + +def pauli_vec_to_state(pauli_vec: np.ndarray) -> np.ndarray: + """ + Convert Pauli vector to density matrix (implementation without qulacs). + + Args: + pauli_vec: Pauli vector. + + Returns: + Density matrix. + """ + n_qubit = (pauli_vec.shape[0].bit_length() - 1) // 2 + M_inv = np.array([[1, 0, 0, 1], [0, 1, -1j, 0], [0, 1, 1j, 0], [1, 0, 0, -1]]) + new_order = [2 * i for i in range(n_qubit)] + [2 * i + 1 for i in range(n_qubit)] + + rho_vec_tmp = pauli_vec.copy() + operate_M_tensor(rho_vec_tmp, M_inv) + + # Change qubit order (replacement for qulacs permutate_qubit) + rho_vec = permutate_qubit_vector(rho_vec_tmp, new_order) + + # Reshape to density matrix + rho = rho_vec.reshape(2**n_qubit, 2**n_qubit) + return rho + + +def get_random_hermitian_operator( + hilbert_dim: int, + num_herm: int = 1, + iterations: int = 100, + step: float = 0.001, + seed: int | None = None, +) -> list[np.ndarray]: + """ + Generate random Hermitian operators. + + Args: + hilbert_dim: Hilbert space dimension. + num_herm: Number of Hermitian operators to generate. + iterations: Number of relaxation iterations. + step: Step size for relaxation. + seed: Random seed (optional). + + Returns: + List of Hermitian matrices. + """ + d = hilbert_dim**2 - 1 + # Generate initial point cloud + points = initial_points(d, num_herm, seed=seed) + + # Lightweight relaxation using Numba + points_relaxed = relax_points_numba(points, iterations=iterations, step=step) + + herm_list = [] + for _points in points_relaxed: + if hilbert_dim == 2 ** (hilbert_dim.bit_length() - 1): + tmp = pauli_vec_to_state(np.concatenate(([0j], _points))) + herm = tmp / np.sqrt(2) + else: + tmp = points_to_hermitian_matrix(_points) + herm = tmp / np.sqrt(hilbert_dim) + herm_list.append(herm) + return herm_list + + +def hermitian_generalized_pauli_operators(d: int) -> list[np.ndarray]: + r""" + Construct Hermitian basis {H_k} (k=0,...,d^2-1) for d-dimensional Hilbert space + from Weyl-Heisenberg generalized Pauli operators Q_{a,b} = X^a Z^b, a,b = 0,...,d-1. + + Note: Standard Q_{a,b} are not Hermitian for d>2, so we construct Hermitian + basis from pairs Q_{a,b} and Q_{a,b}^\dagger: + H^{(R)}_{a,b} = (Q_{a,b}+Q_{a,b}^\dagger)/√2 + H^{(I)}_{a,b} = -i (Q_{a,b}-Q_{a,b}^\dagger)/√2 + + For self-adjoint cases (2a≡0, 2b≡0 mod d), we use phase correction: + H = exp(-iπ a b/d) Q_{a,b} + + Args: + d: Hilbert space dimension. + + Returns: + List of d^2 Hermitian (d,d) matrices (numpy arrays). + """ + H_ops = [] + omega = np.exp(2j * np.pi / d) + + # Define shift operator X + X = np.zeros((d, d), dtype=complex) + for j in range(d): + X[j, (j + 1) % d] = 1 + + # Define phase operator Z + Z = np.diag([omega**j for j in range(d)]) + + # Set for duplicate checking + processed = set() + + for a in range(d): + for b in range(d): + # Key for (a,b) (considering adjoint (a',b') for order independence) + key = (a, b) + # Adjoint corresponds to exponents (-a mod d, -b mod d) + a_p, b_p = (-a) % d, (-b) % d + key_partner = (a_p, b_p) + + # Skip if already processed + if key in processed or key_partner in processed: + continue + + # Q_{a,b} = X^a Z^b + Q = np.linalg.matrix_power(X, a) @ np.linalg.matrix_power(Z, b) + Q_dag = Q.conj().T # Q^\dagger + + # Self-adjoint case (up to phase): 2a≡0 and 2b≡0 (mod d) + if ((2 * a) % d == 0) and ((2 * b) % d == 0): + # Q_dag = ω^{-ab} Q, + # so phase correction φ = ω^{-ab/2} makes it Hermitian + phase = np.exp(-1j * np.pi * a * b / d) + H = phase * Q + H_ops.append(H) + processed.add(key) + else: + # Create Hermitian combinations from pairs + # Note: Q_dag = ω^{-ab} Q_{a_p,b_p}, + # but we use Q and Q_dag directly here + H_R = (Q + Q_dag) / np.sqrt(2) + H_I = -1j * (Q - Q_dag) / np.sqrt(2) + H_ops.append(H_R) + H_ops.append(H_I) + processed.add(key) + processed.add(key_partner) + + # Number of operators should be d^2 + if len(H_ops) != d**2: + raise ValueError( + f"Generated {len(H_ops)} Hermitian operators, but should be d^2 = {d**2}." + ) + + return H_ops + + +def unitary_to_gptm(U: np.ndarray) -> np.ndarray: + """ + Compute GPTM (Generalized Pauli Transfer Matrix) for unitary channel Λ(ρ) = U ρ U† + using Hermitian generalized Pauli basis {H_k}. + + Definition: + M_{ij} = (1/d) Tr[ H_j U H_i U† ] + + Args: + U: Unitary matrix of dimension (d,d). + + Returns: + GPTM matrix of size (d^2, d^2). + """ + d = U.shape[0] + H_ops = hermitian_generalized_pauli_operators(d) + num_ops = len(H_ops) + M = np.zeros((num_ops, num_ops), dtype=complex) + U_dag = U.conj().T + + for i, H_i in enumerate(H_ops): + transformed = U @ H_i @ U_dag + for j, H_j in enumerate(H_ops): + M[j, i] = np.trace(H_j @ transformed) / d + return M + + +def unitary_to_choi(unitary: np.ndarray) -> np.ndarray: + """ + Convert unitary matrix to Choi matrix. + + Args: + unitary: Unitary matrix. + + Returns: + Choi matrix. + """ + dim = unitary.shape[0] + max_entangled_vec = np.identity(dim).reshape(dim * dim) + + large_unitary = np.kron(np.identity(dim), unitary) + vec = large_unitary @ max_entangled_vec + + return np.outer(vec, vec.conj()) + + +def choi_to_unitary(choi: np.ndarray, tol: float = 1e-12) -> np.ndarray: + """ + Convert Choi matrix to unitary matrix. + + Args: + choi: Choi matrix. + tol: Tolerance for rank check. + + Returns: + Unitary matrix. + + Raises: + AssertionError: If input does not correspond to a unitary. + """ + vals, vecs = np.linalg.eigh(choi) + assert ( + np.linalg.matrix_rank(choi, tol=tol) == 1 + ), "input does not correspond to unitary." + n_qubit = (len(vals).bit_length() - 1) // 2 + + u_ret = vecs[:, np.argmax(vals)].reshape(2**n_qubit, 2**n_qubit).T * np.sqrt( + np.max(vals) + ) + u_ret /= u_ret[0, 0] / (np.abs(u_ret[0, 0])) + return u_ret + + +def _diamond_norm_choi( + choi1: np.ndarray, + choi2: np.ndarray | None = None, + scale: float = 1, + solver: str | None = None, +) -> float: + """ + Compute the diamond norm of the difference channel whose Choi matrix is J_delta. + + Args: + choi1: Choi matrix of the first channel, shape (d*d, d*d). + choi2: Choi matrix of the second channel, shape (d*d, d*d) (optional). + scale: Scaling factor. + solver: The CVXPY solver to use (None for default: cp.SCS). + + Returns: + The computed diamond norm. + """ + if choi2 is None: + dim = int(np.sqrt(choi1.shape[0])) + choi2 = unitary_to_choi(np.diag(np.ones(dim))) + + if solver is None: + solver = cp.SCS + + J_delta = (choi1 - choi2) * scale + + d = int(np.sqrt(J_delta.shape[0])) + n = d * d + # Variable for the lifted operator in the SDP: an n x n Hermitian matrix. + X = cp.Variable((n, n), hermitian=True) + # Scalar variable t that will bound the operator norm of the partial trace. + t = cp.Variable(nonneg=True) + + constraints = [] + # X must be positive semidefinite and also dominate J_delta. + constraints += [X >> 0, X - J_delta >> 0] + + # We now define the partial trace of X over the second subsystem. + # For a block-structured matrix X (with blocks of size d x d), + # the (i,j) entry of Y = Tr_B(X) is given by + # summing the (i*d+k, j*d+k) entries of X. + Y = cp.Variable((d, d), hermitian=True) + for i in range(d): + for j in range(d): + # Sum over the appropriate indices to get (Y)_{ij} + constraints.append( + Y[i, j] == cp.sum([X[i * d + k, j * d + k] for k in range(d)]) + ) + + # Impose the operator norm constraint: ||Y||_∞ ≤ t. + # This is equivalent to: t*I - Y >= 0 and t*I + Y >= 0. + constraints.append(t * np.eye(d) - Y >> 0) + constraints.append(t * np.eye(d) + Y >> 0) + + # Set up the SDP: minimize t subject to the constraints. + prob = cp.Problem(cp.Minimize(t), constraints) + prob.solve(solver=solver) + + return prob.value * 2 / scale diff --git a/pygridsynth/mymath.py b/pygridsynth/mymath.py index 49b1000..2806708 100644 --- a/pygridsynth/mymath.py +++ b/pygridsynth/mymath.py @@ -167,3 +167,22 @@ def from_matrix_to_tensor(mat: list[list], n) -> np.ndarray: def from_tensor_to_matrix(mat: np.ndarray, n) -> list[list]: return mat.reshape((2**n, 2**n)).tolist() + + +def random_su(n: int) -> mpmath.matrix: + """ + Generate a random SU(n) unitary matrix. + + Args: + n: Number of qubits. + + Returns: + Random SU(2^n) unitary matrix. + """ + dim = 2**n + z = mpmath.matrix(np.random.random_sample((dim, dim))) + 1j * mpmath.matrix( + np.random.random_sample((dim, dim)) + ) / mpmath.sqrt(2) + q, _ = mpmath.qr(z) + q /= mpmath.det(q) ** (1 / dim) + return q diff --git a/pygridsynth/test.py b/pygridsynth/test.py new file mode 100644 index 0000000..55d64b9 --- /dev/null +++ b/pygridsynth/test.py @@ -0,0 +1,11 @@ +from pygridsynth.mixed_synthesis import mixed_synthesis_sequential +from pygridsynth.mymath import random_su + +if __name__ == "__main__": + num_qubits = 2 + eps = 1e-4 + M = 64 + unitary = random_su(num_qubits) + print(unitary) + result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) + print(result) diff --git a/pyproject.toml b/pyproject.toml index 05be6df..9e716cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,10 @@ dependencies = [ "mpmath>=1.3", "setuptools", "matplotlib", + "numpy", + "scipy", + "cvxpy", + "numba", ] [project.optional-dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt index 215ac55..2b8f551 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,5 @@ -mpmath +mpmath>=1.3 numpy +scipy +cvxpy +numba diff --git a/tests/test_mixed_synthesis.py b/tests/test_mixed_synthesis.py new file mode 100644 index 0000000..766dfa8 --- /dev/null +++ b/tests/test_mixed_synthesis.py @@ -0,0 +1,183 @@ +import numpy as np +import pytest + +from pygridsynth.mixed_synthesis import ( + mixed_synthesis_parallel, + mixed_synthesis_sequential, +) +from pygridsynth.mymath import random_su + + +def test_mixed_synthesis_sequential_basic(): + """Test basic functionality of mixed_synthesis_sequential""" + num_qubits = 2 + eps = 1e-4 + M = 64 + unitary = random_su(num_qubits) + + result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) + + assert result is not None, "Result should not be None" + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert isinstance(circuit_list, list), "circuit_list should be a list" + assert len(circuit_list) == M, f"circuit_list should have {M} elements" + assert isinstance(eu_np_list, list), "eu_np_list should be a list" + assert len(eu_np_list) == M, f"eu_np_list should have {M} elements" + assert isinstance(probs_gptm, np.ndarray), "probs_gptm should be a numpy array" + assert len(probs_gptm) == M, f"probs_gptm should have {M} elements" + assert isinstance(u_choi_opt, np.ndarray), "u_choi_opt should be a numpy array" + assert np.allclose(np.sum(probs_gptm), 1.0), "Probabilities should sum to 1" + assert np.all(probs_gptm >= 0), "All probabilities should be non-negative" + + +def test_mixed_synthesis_parallel_basic(): + """Test basic functionality of mixed_synthesis_parallel""" + num_qubits = 2 + eps = 1e-4 + M = 64 + unitary = random_su(num_qubits) + + result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=123) + + assert result is not None, "Result should not be None" + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert isinstance(circuit_list, list), "circuit_list should be a list" + assert len(circuit_list) == M, f"circuit_list should have {M} elements" + assert isinstance(eu_np_list, list), "eu_np_list should be a list" + assert len(eu_np_list) == M, f"eu_np_list should have {M} elements" + assert isinstance(probs_gptm, np.ndarray), "probs_gptm should be a numpy array" + assert len(probs_gptm) == M, f"probs_gptm should have {M} elements" + assert isinstance(u_choi_opt, np.ndarray), "u_choi_opt should be a numpy array" + assert np.allclose(np.sum(probs_gptm), 1.0), "Probabilities should sum to 1" + assert np.all(probs_gptm >= 0), "All probabilities should be non-negative" + + +def test_mixed_synthesis_sequential_deterministic(): + """Test that mixed_synthesis_sequential returns consistent results with same seed""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary = random_su(num_qubits) + + results = [] + for _ in range(3): + result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) + assert result is not None + results.append(result) + + # Check that all results are identical + for i in range(1, len(results)): + circuit_list1, eu_np_list1, probs_gptm1, u_choi_opt1 = results[0] + circuit_list2, eu_np_list2, probs_gptm2, u_choi_opt2 = results[i] + + assert len(circuit_list1) == len(circuit_list2) + assert np.allclose( + probs_gptm1, probs_gptm2 + ), "Probabilities should be identical" + assert np.allclose( + u_choi_opt1, u_choi_opt2 + ), "Choi matrices should be identical" + + +def test_mixed_synthesis_parallel_deterministic(): + """Test that mixed_synthesis_parallel returns consistent results with same seed""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary = random_su(num_qubits) + + results = [] + for _ in range(3): + result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=123) + assert result is not None + results.append(result) + + # Check that all results are identical + for i in range(1, len(results)): + circuit_list1, eu_np_list1, probs_gptm1, u_choi_opt1 = results[0] + circuit_list2, eu_np_list2, probs_gptm2, u_choi_opt2 = results[i] + + assert len(circuit_list1) == len(circuit_list2) + assert np.allclose( + probs_gptm1, probs_gptm2 + ), "Probabilities should be identical" + assert np.allclose( + u_choi_opt1, u_choi_opt2 + ), "Choi matrices should be identical" + + +def test_mixed_synthesis_sequential_one_qubit(): + """Test mixed_synthesis_sequential with one qubit""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary = random_su(num_qubits) + + result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) + + assert result is not None + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert len(circuit_list) == M + assert len(eu_np_list) == M + assert len(probs_gptm) == M + assert u_choi_opt.shape == (4, 4), "Choi matrix for 1 qubit should be 4x4" + + +def test_mixed_synthesis_parallel_one_qubit(): + """Test mixed_synthesis_parallel with one qubit""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary = random_su(num_qubits) + + result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=123) + + assert result is not None + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert len(circuit_list) == M + assert len(eu_np_list) == M + assert len(probs_gptm) == M + assert u_choi_opt.shape == (4, 4), "Choi matrix for 1 qubit should be 4x4" + + +def test_mixed_synthesis_sequential_with_numpy_array(): + """Test mixed_synthesis_sequential accepts numpy array input""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary_mpmath = random_su(num_qubits) + unitary_np = np.array(unitary_mpmath.tolist(), dtype=complex) + + result = mixed_synthesis_sequential(unitary_np, num_qubits, eps, M, seed=123) + + assert result is not None + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert len(circuit_list) == M + assert len(probs_gptm) == M + + +def test_mixed_synthesis_parallel_with_numpy_array(): + """Test mixed_synthesis_parallel accepts numpy array input""" + num_qubits = 1 + eps = 1e-3 + M = 16 + unitary_mpmath = random_su(num_qubits) + unitary_np = np.array(unitary_mpmath.tolist(), dtype=complex) + + result = mixed_synthesis_parallel(unitary_np, num_qubits, eps, M, seed=123) + + assert result is not None + circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + + assert len(circuit_list) == M + assert len(probs_gptm) == M + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) + print("All tests passed successfully!") diff --git a/tests/test_multi_qubit_unitary_approximation.py b/tests/test_multi_qubit_unitary_approximation.py index 2f08c5c..8b5075a 100644 --- a/tests/test_multi_qubit_unitary_approximation.py +++ b/tests/test_multi_qubit_unitary_approximation.py @@ -83,3 +83,8 @@ def test_approximate_multi_qubit_unitary_config_handling(): ) approximate_multi_qubit_unitary(U, num_qubits, epsilon, verbose=1) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) + print("All tests passed successfully!") From 79579c8a0f32466fda88ed08de661dd9e1a58cba Mon Sep 17 00:00:00 2001 From: shun0923 Date: Tue, 23 Dec 2025 16:03:29 +0900 Subject: [PATCH 6/7] Edit README.md --- .vscode/settings.json | 3 + README.md | 154 +++++++++++++++++++++++++ examples/mixed_synthesis_parallel.py | 15 +++ examples/mixed_synthesis_sequential.py | 24 ++++ examples/multi_qubit_basic.py | 16 +++ examples/multi_qubit_domega.py | 17 +++ examples/multi_qubit_random.py | 12 ++ pygridsynth/mixed_synthesis.py | 26 +++-- pygridsynth/test.py | 11 -- tests/test_mixed_synthesis.py | 27 +++-- 10 files changed, 276 insertions(+), 29 deletions(-) create mode 100644 examples/mixed_synthesis_parallel.py create mode 100644 examples/mixed_synthesis_sequential.py create mode 100644 examples/multi_qubit_basic.py create mode 100644 examples/multi_qubit_domega.py create mode 100644 examples/multi_qubit_random.py delete mode 100644 pygridsynth/test.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 443e824..8506f48 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,7 @@ "Kazuyuki", "Kliuchnikov", "kron", + "kwargs", "limegreen", "linalg", "Maslov", @@ -46,6 +47,7 @@ "mpmath", "mymath", "mypy", + "ndarray", "newsynth", "njit", "Nobuyuki", @@ -56,6 +58,7 @@ "orangered", "prec", "prefactors", + "probs", "pygridsynth", "pypi", "pytest", diff --git a/README.md b/README.md index d89c956..d25d08b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,160 @@ gates = gridsynth_gates(theta=theta, epsilon=epsilon) print(gates) ``` +### Multi-Qubit Unitary Approximation + +`pygridsynth` provides functionality for approximating multi-qubit unitary matrices using the Clifford+T gate set. This is useful for synthesizing quantum circuits that implement arbitrary multi-qubit unitaries. + +**Basic usage:** + +```python +import mpmath + +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) + +# Define a target unitary matrix (example: 2-qubit identity) +num_qubits = 2 +U = mpmath.eye(2**num_qubits) # 4x4 identity matrix +epsilon = "1e-10" + +# Approximate the unitary +circuit, U_approx = approximate_multi_qubit_unitary(U, num_qubits, epsilon) + +print(f"Circuit length: {len(circuit)}") +print(f"Circuit: {str(circuit)}") +``` + +**Using with random unitary:** + +```python +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary +num_qubits = 2 +U = random_su(num_qubits) +epsilon = "1e-10" + +# Approximate with high precision +circuit, U_approx = approximate_multi_qubit_unitary(U, num_qubits, epsilon) +``` + +**Returning DOmegaMatrix:** + +```python +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary +num_qubits = 2 +U = random_su(num_qubits) +epsilon = "1e-10" + +# Return DOmegaMatrix instead of mpmath.matrix for more efficient representation +circuit, U_domega = approximate_multi_qubit_unitary( + U, num_qubits, epsilon, return_domega_matrix=True +) + +# Convert to complex matrix if needed +U_complex = U_domega.to_complex_matrix +``` + +**Parameters:** + +- `U`: Target unitary matrix (`mpmath.matrix`) +- `num_qubits`: Number of qubits +- `epsilon`: Error tolerance (can be `str`, `float`, or `mpmath.mpf`) +- `return_domega_matrix`: If `True`, returns `DOmegaMatrix`; if `False`, returns `mpmath.matrix` (default: `False`) +- `scale_epsilon`: Whether to scale epsilon based on the number of qubits (default: `True`) +- `cfg`: Optional `GridsynthConfig` object for advanced configuration +- `**kwargs`: Additional configuration options (ignored if `cfg` is provided) + +**Returns:** + +A tuple of `(circuit, U_approx)`: +- `circuit`: `QuantumCircuit` object representing the Clifford+T decomposition +- `U_approx`: Approximated unitary matrix (`mpmath.matrix` or `DOmegaMatrix` depending on `return_domega_matrix`) + +### Mixed Unitary Synthesis + +`pygridsynth` also provides functionality for mixed unitary synthesis, which approximates a target unitary by mixing multiple perturbed unitaries. This is useful for reducing the number of T-gates in quantum circuits. + +The library provides two versions: `mixed_synthesis_parallel` (for parallel execution) and `mixed_synthesis_sequential` (for sequential execution). + +**Basic usage with mpmath.matrix:** + +```python +from pygridsynth.mixed_synthesis import ( + compute_diamond_norm_error, + mixed_synthesis_sequential, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary matrix +num_qubits = 2 +unitary = random_su(num_qubits) + +# Parameters +eps = 1e-4 # Error tolerance +M = 64 # Number of Hermitian operators for perturbation +seed = 123 # Random seed for reproducibility + +# Compute mixed synthesis (sequential version) +result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=seed) + +if result is not None: + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result + print(f"Number of circuits: {len(circuit_list)}") + print(f"Mixing probabilities: {probs_gptm}") + error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + print(f"error: {error}") +``` + +**Using parallel version:** + +```python +import mpmath + +from pygridsynth.mixed_synthesis import mixed_synthesis_parallel + +# Generate a random SU(2^n) unitary matrix +num_qubits = 2 +unitary = mpmath.eye(2**num_qubits) + +# Parameters +eps = 1e-4 # Error tolerance +M = 64 # Number of Hermitian operators for perturbation +seed = 123 # Random seed for reproducibility + +# For faster computation with multiple cores +result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=seed) +``` + +**Parameters:** + +- `unitary`: Target unitary matrix (`mpmath.matrix` or `numpy.ndarray`) +- `num_qubits`: Number of qubits +- `eps`: Error tolerance parameter +- `M`: Number of Hermitian operators for perturbation +- `seed`: Random seed for reproducibility (default: `123`) +- `dps`: Decimal precision (default: `-1` for auto-calculation) + +**Returns:** + +A tuple of `(circuit_list, eu_np_list, probs_gptm, u_choi_opt)` or `None` on failure: +- `circuit_list`: List of `QuantumCircuit` objects for perturbed unitaries +- `eu_np_list`: List of approximated unitary matrices (numpy arrays) +- `probs_gptm`: Array of mixing probabilities +- `u_choi_opt`: Optimal mixed Choi matrix + +**Note:** The parallel version (`mixed_synthesis_parallel`) uses multiprocessing and may be faster for large `M` values, while the sequential version (`mixed_synthesis_sequential`) is more suitable for debugging or when parallel execution is not desired. + ## Contributing diff --git a/examples/mixed_synthesis_parallel.py b/examples/mixed_synthesis_parallel.py new file mode 100644 index 0000000..ed1c2d2 --- /dev/null +++ b/examples/mixed_synthesis_parallel.py @@ -0,0 +1,15 @@ +import mpmath + +from pygridsynth.mixed_synthesis import mixed_synthesis_parallel + +# Generate a random SU(2^n) unitary matrix +num_qubits = 2 +unitary = mpmath.eye(2**num_qubits) + +# Parameters +eps = 1e-4 # Error tolerance +M = 64 # Number of Hermitian operators for perturbation +seed = 123 # Random seed for reproducibility + +# For faster computation with multiple cores +result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=seed) diff --git a/examples/mixed_synthesis_sequential.py b/examples/mixed_synthesis_sequential.py new file mode 100644 index 0000000..ae61fc3 --- /dev/null +++ b/examples/mixed_synthesis_sequential.py @@ -0,0 +1,24 @@ +from pygridsynth.mixed_synthesis import ( + compute_diamond_norm_error, + mixed_synthesis_sequential, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary matrix +num_qubits = 2 +unitary = random_su(num_qubits) + +# Parameters +eps = 1e-4 # Error tolerance +M = 64 # Number of Hermitian operators for perturbation +seed = 123 # Random seed for reproducibility + +# Compute mixed synthesis (sequential version) +result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=seed) + +if result is not None: + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result + print(f"Number of circuits: {len(circuit_list)}") + print(f"Mixing probabilities: {probs_gptm}") + error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + print(f"error: {error}") diff --git a/examples/multi_qubit_basic.py b/examples/multi_qubit_basic.py new file mode 100644 index 0000000..765ec48 --- /dev/null +++ b/examples/multi_qubit_basic.py @@ -0,0 +1,16 @@ +import mpmath + +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) + +# Define a target unitary matrix (example: 2-qubit identity) +num_qubits = 2 +U = mpmath.eye(2**num_qubits) # 4x4 identity matrix +epsilon = "1e-10" + +# Approximate the unitary +circuit, U_approx = approximate_multi_qubit_unitary(U, num_qubits, epsilon) + +print(f"Circuit length: {len(circuit)}") +print(f"Circuit: {str(circuit)}") diff --git a/examples/multi_qubit_domega.py b/examples/multi_qubit_domega.py new file mode 100644 index 0000000..288c8db --- /dev/null +++ b/examples/multi_qubit_domega.py @@ -0,0 +1,17 @@ +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary +num_qubits = 2 +U = random_su(num_qubits) +epsilon = "1e-10" + +# Return DOmegaMatrix instead of mpmath.matrix for more efficient representation +circuit, U_domega = approximate_multi_qubit_unitary( + U, num_qubits, epsilon, return_domega_matrix=True +) + +# Convert to complex matrix if needed +U_complex = U_domega.to_complex_matrix diff --git a/examples/multi_qubit_random.py b/examples/multi_qubit_random.py new file mode 100644 index 0000000..e1f18d1 --- /dev/null +++ b/examples/multi_qubit_random.py @@ -0,0 +1,12 @@ +from pygridsynth.multi_qubit_unitary_approximation import ( + approximate_multi_qubit_unitary, +) +from pygridsynth.mymath import random_su + +# Generate a random SU(2^n) unitary +num_qubits = 2 +U = random_su(num_qubits) +epsilon = "1e-10" + +# Approximate with high precision +circuit, U_approx = approximate_multi_qubit_unitary(U, num_qubits, epsilon) diff --git a/pygridsynth/mixed_synthesis.py b/pygridsynth/mixed_synthesis.py index 7a717fe..bfd589c 100644 --- a/pygridsynth/mixed_synthesis.py +++ b/pygridsynth/mixed_synthesis.py @@ -306,7 +306,10 @@ def mixed_synthesis_parallel( M: int, seed: int = 123, dps: int = -1, -) -> tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray] | None: +) -> ( + tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray, np.ndarray] + | None +): """ Compute mixed probabilities for mixed unitary synthesis (parallel version). @@ -319,16 +322,18 @@ def mixed_synthesis_parallel( dps: Decimal precision (default: -1 for auto). Returns: - Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi_opt), or None on failure. + Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt), + or None on failure. - circuit_list: List of QuantumCircuits for perturbed unitaries. - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. - probs_gptm: Array of mixing probabilities. + - u_choi: Choi representation of target unitary. - u_choi_opt: Optimal mixed Choi matrix. """ if not isinstance(unitary, mpmath.matrix): unitary = mpmath.matrix(unitary) - u_gptm, _, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( + u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( process_unitary_approximation_parallel( unitary, num_qubits, eps, M, seed=seed, dps=dps ) @@ -341,7 +346,7 @@ def mixed_synthesis_parallel( probs_gptm, u_choi_opt = result - return circuit_list, eu_np_list, probs_gptm, u_choi_opt + return circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt def mixed_synthesis_sequential( @@ -351,7 +356,10 @@ def mixed_synthesis_sequential( M: int, seed: int = 123, dps: int = -1, -) -> tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray] | None: +) -> ( + tuple[list[QuantumCircuit], list[np.ndarray], np.ndarray, np.ndarray, np.ndarray] + | None +): """ Compute mixed probabilities for mixed unitary synthesis (sequential version). @@ -364,16 +372,18 @@ def mixed_synthesis_sequential( dps: Decimal precision (default: -1 for auto). Returns: - Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi_opt), or None on failure. + Tuple of (circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt), + or None on failure. - circuit_list: List of QuantumCircuits for perturbed unitaries. - eu_np_list: List of target unitary matrices perturbed by Hermitian operators. - probs_gptm: Array of mixing probabilities. + - u_choi: Choi representation of target unitary. - u_choi_opt: Optimal mixed Choi matrix. """ if not isinstance(unitary, mpmath.matrix): unitary = mpmath.matrix(unitary) - u_gptm, _, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( + u_gptm, u_choi, circuit_list, eu_np_list, eu_gptm_list, eu_choi_list = ( process_unitary_approximation_sequential( unitary, num_qubits, eps, M, seed=seed, dps=dps ) @@ -386,4 +396,4 @@ def mixed_synthesis_sequential( probs_gptm, u_choi_opt = result - return circuit_list, eu_np_list, probs_gptm, u_choi_opt + return circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt diff --git a/pygridsynth/test.py b/pygridsynth/test.py deleted file mode 100644 index 55d64b9..0000000 --- a/pygridsynth/test.py +++ /dev/null @@ -1,11 +0,0 @@ -from pygridsynth.mixed_synthesis import mixed_synthesis_sequential -from pygridsynth.mymath import random_su - -if __name__ == "__main__": - num_qubits = 2 - eps = 1e-4 - M = 64 - unitary = random_su(num_qubits) - print(unitary) - result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) - print(result) diff --git a/tests/test_mixed_synthesis.py b/tests/test_mixed_synthesis.py index 766dfa8..208f81e 100644 --- a/tests/test_mixed_synthesis.py +++ b/tests/test_mixed_synthesis.py @@ -18,7 +18,7 @@ def test_mixed_synthesis_sequential_basic(): result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) assert result is not None, "Result should not be None" - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert isinstance(circuit_list, list), "circuit_list should be a list" assert len(circuit_list) == M, f"circuit_list should have {M} elements" @@ -26,6 +26,7 @@ def test_mixed_synthesis_sequential_basic(): assert len(eu_np_list) == M, f"eu_np_list should have {M} elements" assert isinstance(probs_gptm, np.ndarray), "probs_gptm should be a numpy array" assert len(probs_gptm) == M, f"probs_gptm should have {M} elements" + assert isinstance(u_choi, np.ndarray), "u_choi should be a numpy array" assert isinstance(u_choi_opt, np.ndarray), "u_choi_opt should be a numpy array" assert np.allclose(np.sum(probs_gptm), 1.0), "Probabilities should sum to 1" assert np.all(probs_gptm >= 0), "All probabilities should be non-negative" @@ -41,7 +42,7 @@ def test_mixed_synthesis_parallel_basic(): result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=123) assert result is not None, "Result should not be None" - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert isinstance(circuit_list, list), "circuit_list should be a list" assert len(circuit_list) == M, f"circuit_list should have {M} elements" @@ -49,7 +50,13 @@ def test_mixed_synthesis_parallel_basic(): assert len(eu_np_list) == M, f"eu_np_list should have {M} elements" assert isinstance(probs_gptm, np.ndarray), "probs_gptm should be a numpy array" assert len(probs_gptm) == M, f"probs_gptm should have {M} elements" + assert isinstance(u_choi, np.ndarray), "u_choi should be a numpy array" + assert u_choi.shape == (16, 16), "Choi matrix for 2 qubits should be 16x16" assert isinstance(u_choi_opt, np.ndarray), "u_choi_opt should be a numpy array" + assert u_choi_opt.shape == ( + 16, + 16, + ), "Optimal Choi matrix for 2 qubits should be 16x16" assert np.allclose(np.sum(probs_gptm), 1.0), "Probabilities should sum to 1" assert np.all(probs_gptm >= 0), "All probabilities should be non-negative" @@ -69,8 +76,8 @@ def test_mixed_synthesis_sequential_deterministic(): # Check that all results are identical for i in range(1, len(results)): - circuit_list1, eu_np_list1, probs_gptm1, u_choi_opt1 = results[0] - circuit_list2, eu_np_list2, probs_gptm2, u_choi_opt2 = results[i] + circuit_list1, eu_np_list1, probs_gptm1, u_choi1, u_choi_opt1 = results[0] + circuit_list2, eu_np_list2, probs_gptm2, u_choi2, u_choi_opt2 = results[i] assert len(circuit_list1) == len(circuit_list2) assert np.allclose( @@ -96,8 +103,8 @@ def test_mixed_synthesis_parallel_deterministic(): # Check that all results are identical for i in range(1, len(results)): - circuit_list1, eu_np_list1, probs_gptm1, u_choi_opt1 = results[0] - circuit_list2, eu_np_list2, probs_gptm2, u_choi_opt2 = results[i] + circuit_list1, eu_np_list1, probs_gptm1, u_choi1, u_choi_opt1 = results[0] + circuit_list2, eu_np_list2, probs_gptm2, u_choi2, u_choi_opt2 = results[i] assert len(circuit_list1) == len(circuit_list2) assert np.allclose( @@ -118,7 +125,7 @@ def test_mixed_synthesis_sequential_one_qubit(): result = mixed_synthesis_sequential(unitary, num_qubits, eps, M, seed=123) assert result is not None - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert len(circuit_list) == M assert len(eu_np_list) == M @@ -136,7 +143,7 @@ def test_mixed_synthesis_parallel_one_qubit(): result = mixed_synthesis_parallel(unitary, num_qubits, eps, M, seed=123) assert result is not None - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert len(circuit_list) == M assert len(eu_np_list) == M @@ -155,7 +162,7 @@ def test_mixed_synthesis_sequential_with_numpy_array(): result = mixed_synthesis_sequential(unitary_np, num_qubits, eps, M, seed=123) assert result is not None - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert len(circuit_list) == M assert len(probs_gptm) == M @@ -172,7 +179,7 @@ def test_mixed_synthesis_parallel_with_numpy_array(): result = mixed_synthesis_parallel(unitary_np, num_qubits, eps, M, seed=123) assert result is not None - circuit_list, eu_np_list, probs_gptm, u_choi_opt = result + circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result assert len(circuit_list) == M assert len(probs_gptm) == M From 2d0393313a2cb8a107b26eae7fc028727a69ec85 Mon Sep 17 00:00:00 2001 From: shun0923 Date: Fri, 9 Jan 2026 15:07:09 +0900 Subject: [PATCH 7/7] Add useful functions --- README.md | 27 +++++----- dist/pygridsynth-1.2.0-py3-none-any.whl | Bin 32356 -> 0 bytes dist/pygridsynth-1.2.0.tar.gz | Bin 32018 -> 0 bytes examples/mixed_synthesis_sequential.py | 9 ++-- pygridsynth/mixed_synthesis.py | 18 ------- pygridsynth/mixed_synthesis_parallel.py | 5 +- pygridsynth/mixed_synthesis_sequential.py | 7 +-- pygridsynth/mixed_synthesis_utils.py | 2 +- pygridsynth/mymath.py | 59 ++++++++++++++++++++++ 9 files changed, 84 insertions(+), 43 deletions(-) delete mode 100644 dist/pygridsynth-1.2.0-py3-none-any.whl delete mode 100644 dist/pygridsynth-1.2.0.tar.gz diff --git a/README.md b/README.md index d25d08b..ee4b39c 100644 --- a/README.md +++ b/README.md @@ -227,11 +227,8 @@ The library provides two versions: `mixed_synthesis_parallel` (for parallel exec **Basic usage with mpmath.matrix:** ```python -from pygridsynth.mixed_synthesis import ( - compute_diamond_norm_error, - mixed_synthesis_sequential, -) -from pygridsynth.mymath import random_su +from pygridsynth.mixed_synthesis import mixed_synthesis_sequential +from pygridsynth.mymath import diamond_norm_error_from_choi, random_su # Generate a random SU(2^n) unitary matrix num_qubits = 2 @@ -249,7 +246,7 @@ if result is not None: circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result print(f"Number of circuits: {len(circuit_list)}") print(f"Mixing probabilities: {probs_gptm}") - error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + error = diamond_norm_error_from_choi(u_choi, u_choi_opt, eps, mixed_synthesis=True) print(f"error: {error}") ``` @@ -303,9 +300,15 @@ This project is licensed under the MIT License. ## References -- Brett Giles and Peter Selinger. Remarks on Matsumoto and Amano's normal form for single-qubit Clifford+T operators, 2019. -- Ken Matsumoto and Kazuyuki Amano. Representation of Quantum Circuits with Clifford and π/8 Gates, 2008. -- Neil J. Ross and Peter Selinger. Optimal ancilla-free Clifford+T approximation of z-rotations, 2016. -- Peter Selinger. Efficient Clifford+T approximation of single-qubit operators, 2014. -- Peter Selinger and Neil J. Ross. Exact and approximate synthesis of quantum circuits. https://www.mathstat.dal.ca/~selinger/newsynth/, 2018. -- Vadym Kliuchnikov, Dmitri Maslov, and Michele Mosca. Fast and efficient exact synthesis of single qubit unitaries generated by Clifford and T gates, 2013. +- Vadym Kliuchnikov, Kristin Lauter, Romy Minko, Adam Paetznick, Christophe Petit. "Shorter quantum circuits via single-qubit gate approximation." Quantum 7 (2023): 1208. DOI: 10.22331/q-2023-12-18-1208. +- Peter Selinger. "Efficient Clifford+T approximation of single-qubit operators." Quantum Info. Comput. 15, no. 1-2 (2015): 159-180. +- Neil J. Ross and Peter Selinger. "Optimal ancilla-free Clifford+T approximation of z-rotations." Quantum Info. Comput. 16, no. 11-12 (2016): 901-953. +- Vadym Kliuchnikov, Dmitri Maslov, and Michele Mosca. "Fast and efficient exact synthesis of single-qubit unitaries generated by Clifford and T gates." Quantum Info. Comput. 13, no. 7-8 (2013): 607-630. +- Ken Matsumoto and Kazuyuki Amano. "Representation of Quantum Circuits with Clifford and π/8 Gates." arXiv: Quantum Physics (2008). URL: https://api.semanticscholar.org/CorpusID:17327793. +- Brett Gordon Giles and Peter Selinger. "Remarks on Matsumoto and Amano's normal form for single-qubit Clifford+T operators." ArXiv abs/1312.6584 (2013). URL: https://api.semanticscholar.org/CorpusID:10077777. +- Vivek V. Shende, Igor L. Markov, and Stephen S. Bullock. "Minimal universal two-qubit controlled-NOT-based circuits." Phys. Rev. A 69, no. 6 (2004): 062321. DOI: 10.1103/PhysRevA.69.062321. +- Vivek V. Shende, Igor L. Markov, and Stephen S. Bullock. "Finding Small Two-Qubit Circuits." Proceedings of SPIE - The International Society for Optical Engineering (2004). DOI: 10.1117/12.542381. +- Vivek V. Shende, Igor L. Markov, and Stephen S. Bullock. "Smaller Two-Qubit Circuits for Quantum Communication and Computation." In Proceedings of the Conference on Design, Automation and Test in Europe - Volume 2, p. 20980. IEEE Computer Society, 2004. +- Korbinian Kottmann. "Two-qubit Synthesis." (2025). URL: https://pennylane.ai/compilation/two-qubit-synthesis. +- Anna M. Krol and Zaid Al-Ars. "Beyond quantum Shannon decomposition: Circuit construction for n-qubit gates based on block-ZXZ decomposition." Physical Review Applied 22, no. 3 (2024): 034019. DOI: 10.1103/PhysRevApplied.22.034019. +- Peter Selinger and Neil J. Ross. "Exact and approximate synthesis of quantum circuits." (2018). URL: https://www.mathstat.dal.ca/~selinger/newsynth/. diff --git a/dist/pygridsynth-1.2.0-py3-none-any.whl b/dist/pygridsynth-1.2.0-py3-none-any.whl deleted file mode 100644 index 8c5b326c48446ec3348227196dbd1cf3799aca54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32356 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

ujrZO%Qc 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 diff --git a/dist/pygridsynth-1.2.0.tar.gz b/dist/pygridsynth-1.2.0.tar.gz deleted file mode 100644 index 6c1bd891aecb402bf2966c346f65ffb22a43d455..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32018 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 F{|nHnqM`r* diff --git a/examples/mixed_synthesis_sequential.py b/examples/mixed_synthesis_sequential.py index ae61fc3..0b9b1a2 100644 --- a/examples/mixed_synthesis_sequential.py +++ b/examples/mixed_synthesis_sequential.py @@ -1,8 +1,5 @@ -from pygridsynth.mixed_synthesis import ( - compute_diamond_norm_error, - mixed_synthesis_sequential, -) -from pygridsynth.mymath import random_su +from pygridsynth.mixed_synthesis import mixed_synthesis_sequential +from pygridsynth.mymath import diamond_norm_error_from_choi, random_su # Generate a random SU(2^n) unitary matrix num_qubits = 2 @@ -20,5 +17,5 @@ circuit_list, eu_np_list, probs_gptm, u_choi, u_choi_opt = result print(f"Number of circuits: {len(circuit_list)}") print(f"Mixing probabilities: {probs_gptm}") - error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + error = diamond_norm_error_from_choi(u_choi, u_choi_opt, eps, mixed_synthesis=True) print(f"error: {error}") diff --git a/pygridsynth/mixed_synthesis.py b/pygridsynth/mixed_synthesis.py index bfd589c..c7c37ab 100644 --- a/pygridsynth/mixed_synthesis.py +++ b/pygridsynth/mixed_synthesis.py @@ -13,7 +13,6 @@ import scipy from .mixed_synthesis_utils import ( - _diamond_norm_choi, get_random_hermitian_operator, unitary_to_choi, unitary_to_gptm, @@ -163,23 +162,6 @@ def compute_optimal_mixing_probabilities( return probs_gptm, u_choi_opt -def compute_diamond_norm_error( - u_choi: np.ndarray, u_choi_opt: np.ndarray, eps: float -) -> float: - """ - Compute error using diamond norm. - - Args: - u_choi: Choi representation of target unitary. - u_choi_opt: Choi representation of mixed unitary. - eps: Error tolerance parameter. - - Returns: - Diamond norm error between the target and mixed unitaries. - """ - return _diamond_norm_choi(u_choi, u_choi_opt, scale=1e-2 / eps**2) - - def process_unitary_approximation_parallel( unitary: mpmath.matrix, num_qubits: int, diff --git a/pygridsynth/mixed_synthesis_parallel.py b/pygridsynth/mixed_synthesis_parallel.py index 371e604..ec8a054 100644 --- a/pygridsynth/mixed_synthesis_parallel.py +++ b/pygridsynth/mixed_synthesis_parallel.py @@ -13,11 +13,10 @@ import numpy as np from .mixed_synthesis import ( - compute_diamond_norm_error, compute_optimal_mixing_probabilities, process_unitary_approximation_parallel, ) -from .mymath import random_su +from .mymath import diamond_norm_error_from_choi, random_su if TYPE_CHECKING: from .quantum_circuit import QuantumCircuit @@ -57,7 +56,7 @@ def my_task( if result is None: return (idx, None, None) probs_gptm, u_choi_opt = result - error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + error = diamond_norm_error_from_choi(u_choi, u_choi_opt, eps, mixed_synthesis=True) return (idx, probs_gptm, error) diff --git a/pygridsynth/mixed_synthesis_sequential.py b/pygridsynth/mixed_synthesis_sequential.py index e2ae5af..ab2a2f7 100644 --- a/pygridsynth/mixed_synthesis_sequential.py +++ b/pygridsynth/mixed_synthesis_sequential.py @@ -11,11 +11,10 @@ import numpy as np from .mixed_synthesis import ( - compute_diamond_norm_error, compute_optimal_mixing_probabilities, process_unitary_approximation_sequential, ) -from .mymath import random_su +from .mymath import diamond_norm_error_from_choi, random_su if TYPE_CHECKING: from .quantum_circuit import QuantumCircuit @@ -91,7 +90,9 @@ def main() -> list[ ) else: probs_gptm, u_choi_opt = result - error = compute_diamond_norm_error(u_choi, u_choi_opt, eps) + error = diamond_norm_error_from_choi( + u_choi, u_choi_opt, eps, mixed_synthesis=True + ) final_results.append( (num_qubits, eps, circuits, eu_np_list, probs_gptm, error) ) diff --git a/pygridsynth/mixed_synthesis_utils.py b/pygridsynth/mixed_synthesis_utils.py index bef10ad..cf045b5 100644 --- a/pygridsynth/mixed_synthesis_utils.py +++ b/pygridsynth/mixed_synthesis_utils.py @@ -426,7 +426,7 @@ def choi_to_unitary(choi: np.ndarray, tol: float = 1e-12) -> np.ndarray: return u_ret -def _diamond_norm_choi( +def diamond_norm_choi( choi1: np.ndarray, choi2: np.ndarray | None = None, scale: float = 1, diff --git a/pygridsynth/mymath.py b/pygridsynth/mymath.py index 2806708..021656f 100644 --- a/pygridsynth/mymath.py +++ b/pygridsynth/mymath.py @@ -5,6 +5,8 @@ import mpmath import numpy as np +from .mixed_synthesis_utils import diamond_norm_choi, unitary_to_choi + RealNum: TypeAlias = int | float | mpmath.mpf MPFConvertible: TypeAlias = RealNum | mpmath.mpf @@ -143,6 +145,14 @@ def solve_quadratic( return ((2 * c) / s2, (2 * c) / s1) +def mpmath_matrix_to_numpy(M: mpmath.matrix) -> np.ndarray: + return np.array(M.tolist(), dtype=complex) + + +def numpy_matrix_to_mpmath(M: np.ndarray) -> mpmath.matrix: + return mpmath.matrix(M.tolist()) + + def trace(M: mpmath.matrix) -> mpmath.mpf: return sum(M[i, i] for i in range(min(M.rows, M.cols))) @@ -186,3 +196,52 @@ def random_su(n: int) -> mpmath.matrix: q, _ = mpmath.qr(z) q /= mpmath.det(q) ** (1 / dim) return q + + +def diamond_norm_error( + u: np.ndarray | mpmath.matrix, + u_opt: np.ndarray | mpmath.matrix, + eps: MPFConvertible, +) -> float: + """ + Compute error using diamond norm. + + Args: + u: Target unitary. + u_opt: Mixed unitary. + eps: Error tolerance parameter. + + Returns: + Diamond norm error between the target and mixed unitaries. + """ + if isinstance(u, mpmath.matrix): + u = mpmath_matrix_to_numpy(u) + if isinstance(u_opt, mpmath.matrix): + u_opt = mpmath_matrix_to_numpy(u_opt) + if isinstance(eps, mpmath.mpf): + eps = float(eps) + u_choi = unitary_to_choi(u) + u_choi_opt = unitary_to_choi(u_opt) + return diamond_norm_error_from_choi(u_choi, u_choi_opt, eps, mixed_synthesis=False) + + +def diamond_norm_error_from_choi( + u_choi: np.ndarray, + u_choi_opt: np.ndarray, + eps: float, + mixed_synthesis: bool = False, +) -> float: + """ + Compute error using diamond norm. + + Args: + u_choi: Choi representation of target unitary. + u_choi_opt: Choi representation of mixed unitary. + eps: Error tolerance parameter. + mixed_synthesis: Whether the error is for mixed synthesis. + + Returns: + Diamond norm error between the target and mixed unitaries. + """ + scale = 1e-2 / eps**2 if mixed_synthesis else 1e-2 / eps + return diamond_norm_choi(u_choi, u_choi_opt, scale=scale)