From 301930cbbf0ffb44e3d2a0c724ef981b06bd2ba2 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 15:32:03 -0700 Subject: [PATCH 01/16] Adding Multiplexed Gates --- bqskit/ir/gates/parameterized/__init__.py | 4 + bqskit/ir/gates/parameterized/mcry.py | 148 ++++++++++++++++++++++ bqskit/ir/gates/parameterized/mcrz.py | 130 +++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 bqskit/ir/gates/parameterized/mcry.py create mode 100644 bqskit/ir/gates/parameterized/mcrz.py diff --git a/bqskit/ir/gates/parameterized/__init__.py b/bqskit/ir/gates/parameterized/__init__.py index 546520b60..a592b66ef 100644 --- a/bqskit/ir/gates/parameterized/__init__.py +++ b/bqskit/ir/gates/parameterized/__init__.py @@ -11,6 +11,8 @@ from bqskit.ir.gates.parameterized.crz import CRZGate from bqskit.ir.gates.parameterized.cu import CUGate from bqskit.ir.gates.parameterized.fsim import FSIMGate +from bqskit.ir.gates.parameterized.mcry import MCRYGate +from bqskit.ir.gates.parameterized.mcrz import MCRZGate from bqskit.ir.gates.parameterized.pauli import PauliGate from bqskit.ir.gates.parameterized.pauliz import PauliZGate from bqskit.ir.gates.parameterized.phasedxz import PhasedXZGate @@ -41,6 +43,8 @@ 'CRZGate', 'CUGate', 'FSIMGate', + 'MCRYGate', + 'MCRZGate', 'PauliGate', 'PauliZGate', 'PhasedXZGate', diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py new file mode 100644 index 000000000..d38e35bdb --- /dev/null +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -0,0 +1,148 @@ +"""This module implements the MCRYGate.""" +from __future__ import annotations + +import numpy as np +import numpy.typing as npt + +from bqskit.ir.gates.qubitgate import QubitGate +from bqskit.qis.unitary.differentiable import DifferentiableUnitary +from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary +from bqskit.qis.unitary.unitary import RealVector +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.utils.cachedclass import CachedClass +import logging + +def get_indices(index: int, target_qudit, num_qudits): + """ + Get indices for the matrix based on the target qubit. + """ + shift_qubit = num_qudits - target_qudit - 1 + shift = 2 ** shift_qubit + # Split into two parts around target qubit + # 100 | 111 + left = index // shift + right = index % shift + + # Now, shift left by one spot to + # make room for the target qubit + left *= (shift * 2) + # Now add 0 * new_ind and 1 * new_ind to get indices + return left + right, left + shift + right + +class MCRYGate( + QubitGate, + DifferentiableUnitary, + CachedClass, + LocallyOptimizableUnitary +): + """ + A gate representing a multiplexed Y rotation. A multiplexed Y rotation + uses n - 1 qubits as select qubits and applies a Y rotation to the target. + If the target qubit is the last qubit, then the unitary is block diagonal. + Each block is a 2x2 RY matrix with parameter theta. + + Since there are n - 1 select qubits, there are 2^(n-1) parameters (thetas). + + We allow the target qubit to be specified to any qubit, and the other qubits + maintain their order. Qubit 0 is the most significant qubit. + + See this paper: https://arxiv.org/pdf/quant-ph/0406176 + """ + + _qasm_name = 'mcry' + + def __init__(self, num_qudits: int, controlled_qubit: int) -> None: + self._num_qudits = num_qudits + # 1 param for each configuration of the selec qubits + self._num_params = 2 ** (num_qudits - 1) + self.controlled_qubit = controlled_qubit + super().__init__() + + def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: + """Return the unitary for this gate, see :class:`Unitary` for more.""" + self.check_parameters(params) + + matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + for i, param in enumerate(params): + cos = np.cos(param / 2) + sin = np.sin(param / 2) + + # Now, get indices based on target qubit. + # i corresponds to the configuration of the + # select qubits (e.g 5 = 101). Now, the + # target qubit is 0,1 for both the row and col + # indices. So, if i = 5 and the target_qubit is 2 + # Then the rows/cols are 1001 and 1101 + x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + + matrix[x1, x1] = cos + matrix[x2, x2] = cos + matrix[x2, x1] = sin + matrix[x1, x2] = -1 * sin + + return UnitaryMatrix(matrix) + + def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: + """ + Return the gradient for this gate. + See :class:`DifferentiableUnitary` for more info. + """ + self.check_parameters(params) + + matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + for i, param in enumerate(params): + dcos = -np.sin(param / 2) / 2 + dsin = -1j * np.cos(param / 2) / 2 + + # Again, get indices based on target qubit. + x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + + matrix[x1, x1] = dcos + matrix[x2, x2] = dcos + matrix[x2, x1] = dsin + matrix[x1, x2] = -1 * dsin + + return UnitaryMatrix(matrix) + + + def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: + """ + Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. + """ + self.check_env_matrix(env_matrix) + thetas = [0] * self.num_params + + for i in range(self.num_params): + x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + a = np.real(env_matrix[x1, x1] + env_matrix[x2, x2]) + b = np.real(env_matrix[x2, x1] - env_matrix[x1, x2]) + theta = 2 * np.arccos(a / np.sqrt(a ** 2 + b ** 2)) + theta *= -1 if b > 0 else 1 + thetas[i] = theta + + return thetas + + @staticmethod + def get_decomposition(params: RealVector = []) -> tuple[RealVector, RealVector]: + ''' + Get the corresponding parameters for one level of decomposition + of a multiplexed gate. This is used in the decomposition of both + the MCRY and MCRZ gates. See :class:`MGDPass` for more info. + ''' + new_num_params = len(params) // 2 + left_params = np.zeros(new_num_params) + right_params = np.zeros(new_num_params) + for i in range(len(left_params)): + left_param = (params[i] + params[i + new_num_params]) / 2 + right_param = (params[i] - params[i + new_num_params]) / 2 + left_params[i] = left_param + right_params[i] = right_param + + return left_params, right_params + + @property + def name(self) -> str: + """The name of this gate, with the number of qudits appended.""" + base_name = getattr(self, '_name', self.__class__.__name__) + return f"{base_name}_{self.num_qudits}" \ No newline at end of file diff --git a/bqskit/ir/gates/parameterized/mcrz.py b/bqskit/ir/gates/parameterized/mcrz.py new file mode 100644 index 000000000..912814f5c --- /dev/null +++ b/bqskit/ir/gates/parameterized/mcrz.py @@ -0,0 +1,130 @@ +"""This module implements the MCRZGate.""" +from __future__ import annotations + +import numpy as np +import numpy.typing as npt + +from bqskit.ir.gates.qubitgate import QubitGate +from bqskit.qis.unitary.differentiable import DifferentiableUnitary +from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary +from bqskit.qis.unitary.unitary import RealVector +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.utils.cachedclass import CachedClass +from bqskit.ir.gates.parameterized.mcry import get_indices +from typing import Any + +class MCRZGate( + QubitGate, + DifferentiableUnitary, + CachedClass, + LocallyOptimizableUnitary +): + """ + A gate representing a multiplexed Z rotation. A multiplexed Z rotation + uses n - 1 qubits as select qubits and applies a Z rotation to the target. + If the target qubit is the last qubit, then the unitary is block diagonal. + Each block is a 2x2 RZ matrix with parameter theta. + + Since there are n - 1 select qubits, there are 2^(n-1) parameters (thetas). + + We allow the target qubit to be specified to any qubit, and the other qubits + maintain their order. Qubit 0 is the most significant qubit. + + + Why is 0 the MSB? Typically, in the QSD diagram, we see the block drawn + with qubit 0 at the top and qubit n-1 at the bottom. Then, the decomposition + slowly moves from the bottom to the top. + + See this paper: https://arxiv.org/pdf/quant-ph/0406176 + """ + + _qasm_name = 'mcrz' + + def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: + ''' + Create a new MCRZGate with `num_qudits` qubits and + `target_qubit` as the target qubit. We then have 2^(n-1) parameters + for this gate. + + For Example: + `num_qudits` = 3, `target_qubit` = 1 + + Then, the select qubits are 0 and 2 with 0 as the MSB. + + If the input vector is |0x0> then the selection is 00, and + RZ(theta_0) is applied to the target qubit. + + If the input vector is |1x0> then the selection is 01, and + RZ(theta_1) is applied to the target qubit. + ''' + self._num_qudits = num_qudits + # 1 param for each configuration of the selec qubits + self._num_params = 2 ** (num_qudits - 1) + # By default, the controlled qubit is the last qubit + if target_qubit == -1: + target_qubit = num_qudits - 1 + self.target_qubit = target_qubit + super().__init__() + + def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: + """Return the unitary for this gate, see :class:`Unitary` for more.""" + self.check_parameters(params) + matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + for i, param in enumerate(params): + pos = np.exp(1j * param / 2) + neg = np.exp(-1j * param / 2) + + # Get correct indices based on target qubit + # See :class:`mcry` for more info + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) + + matrix[x1, x1] = neg + matrix[x2, x2] = pos + + return UnitaryMatrix(matrix) + + def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: + """ + Return the gradient for this gate. + + See :class:`DifferentiableUnitary` for more info. + """ + self.check_parameters(params) + matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + for i, param in enumerate(params): + dpos = 1j / 2 * np.exp(1j * param / 2) + dneg = -1j / 2 * np.exp(-1j * param / 2) + + # Again, get indices based on target qubit. + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) + + matrix[x1, x1] = dpos + matrix[x2, x2] = dneg + + return UnitaryMatrix(matrix) + + + def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: + """ + Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. + """ + self.check_env_matrix(env_matrix) + thetas = [0] * self.num_params + + for i in range(self.num_params): + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) + # Optimize each RZ independently from indices + # Taken from QFACTOR repo + a = np.angle(env_matrix[x1, x1]) + b = np.angle(env_matrix[x2, x2]) + # print(thetas) + thetas[i] = a - b + + return thetas + + @property + def name(self) -> str: + """The name of this gate, with the number of qudits appended.""" + base_name = getattr(self, '_name', self.__class__.__name__) + return f"{base_name}_{self.num_qudits}" \ No newline at end of file From 73103c13b938150a8ca5d44823efc4d3bbcc55e3 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 16:11:39 -0700 Subject: [PATCH 02/16] Add tests for MCR gates, fixing comment on MCRY gate --- bqskit/ir/gates/parameterized/mcry.py | 5 ++- tests/ir/gates/parameterized/test_mcry.py | 52 ++++++++++++++++++++++ tests/ir/gates/parameterized/test_mcrz.py | 53 +++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 tests/ir/gates/parameterized/test_mcry.py create mode 100644 tests/ir/gates/parameterized/test_mcrz.py diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py index d38e35bdb..f150fb703 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -51,10 +51,13 @@ class MCRYGate( _qasm_name = 'mcry' - def __init__(self, num_qudits: int, controlled_qubit: int) -> None: + def __init__(self, num_qudits: int, controlled_qubit: int = -1) -> None: self._num_qudits = num_qudits # 1 param for each configuration of the selec qubits self._num_params = 2 ** (num_qudits - 1) + # By default, the controlled qubit is the last qubit + if target_qubit == -1: + target_qubit = num_qudits - 1 self.controlled_qubit = controlled_qubit super().__init__() diff --git a/tests/ir/gates/parameterized/test_mcry.py b/tests/ir/gates/parameterized/test_mcry.py new file mode 100644 index 000000000..e3e6aa1cc --- /dev/null +++ b/tests/ir/gates/parameterized/test_mcry.py @@ -0,0 +1,52 @@ +"""This module tests the U1Gate class.""" +from __future__ import annotations + +import numpy as np +from scipy.linalg import block_diag +from hypothesis import given +from hypothesis.strategies import floats, integers, lists + +from bqskit.ir.gates.parameterized import MCRYGate, RYGate +from bqskit.ir.gates.constant import PermutationGate + +@given(lists(elements=floats(allow_nan=False, allow_infinity=False, width=32), min_size=2, max_size=16)) +def test_get_unitary(thetas: list[float]) -> None: + ''' + Test the get_unitary method of the MCRYGate class. + Use the default target qubit. + ''' + # Assert that len(thetas) is a power of 2 + assert (len(thetas) & (len(thetas) - 1)) == 0 + + # There are 2 ** (n - 1) parameters + num_qudits = np.log2(len(thetas)) + 1 + mcry = MCRYGate(num_qudits=num_qudits) + block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + assert dist < 1e-7 + +@given(integers(min_value=0, max_value=4)) +def test_get_unitary_target_select(target_qubit: int) -> None: + ''' + Test the get_unitary method of the MCRYGate class when + the target qubit is set. + ''' + # Create an MCRY gate with 6 qubits and random parameters + mcry = MCRYGate(num_qudits=6, target_qubit=target_qubit) + thetas = list(np.random.rand(2 ** 5) * 2 * np.pi) + + # Create the block diagonal matrix + block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + + # Apply a permutation to the block diagonal matrix + # Swap the target qubit with the last qubit + perm = np.arange(6) + perm[-1], perm[target_qubit] = perm[target_qubit], perm[-1] + perm_gate = PermutationGate(6, perm) + + full_utry = perm_gate.get_unitary() @ blocked_unitary + + dist = mcry.get_unitary(thetas).get_distance_from(full_utry) + assert dist < 1e-7 \ No newline at end of file diff --git a/tests/ir/gates/parameterized/test_mcrz.py b/tests/ir/gates/parameterized/test_mcrz.py new file mode 100644 index 000000000..bedc20822 --- /dev/null +++ b/tests/ir/gates/parameterized/test_mcrz.py @@ -0,0 +1,53 @@ +"""This module tests the U1Gate class.""" +from __future__ import annotations + +import numpy as np +from scipy.linalg import block_diag + +from hypothesis import given +from hypothesis.strategies import floats, integers, lists + +from bqskit.ir.gates.parameterized import MCRZGate, RZGate +from bqskit.ir.gates.constant import PermutationGate + +@given(lists(elements=floats(allow_nan=False, allow_infinity=False, width=32), min_size=2, max_size=16)) +def test_get_unitary(thetas: list[float]) -> None: + ''' + Test the get_unitary method of the MCRZGate class. + Use the default target qubit. + ''' + # Assert that len(thetas) is a power of 2 + assert (len(thetas) & (len(thetas) - 1)) == 0 + + # There are 2 ** (n - 1) parameters + num_qudits = np.log2(len(thetas)) + 1 + mcry = MCRZGate(num_qudits=num_qudits) + block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + assert dist < 1e-7 + +@given(integers(min_value=0, max_value=4)) +def test_get_unitary_target_select(target_qubit: int) -> None: + ''' + Test the get_unitary method of the MCRZGate class when + the target qubit is set. + ''' + # Create an MCRY gate with 6 qubits and random parameters + mcry = MCRZGate(num_qudits=6, target_qubit=target_qubit) + thetas = list(np.random.rand(2 ** 5) * 2 * np.pi) + + # Create the block diagonal matrix + block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + + # Apply a permutation to the block diagonal matrix + # Swap the target qubit with the last qubit + perm = np.arange(6) + perm[-1], perm[target_qubit] = perm[target_qubit], perm[-1] + perm_gate = PermutationGate(6, perm) + + full_utry = perm_gate.get_unitary() @ blocked_unitary + + dist = mcry.get_unitary(thetas).get_distance_from(full_utry) + assert dist < 1e-7 \ No newline at end of file From ceb33a6387ab21bf93a78962409e6b19e4428a26 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 16:53:26 -0700 Subject: [PATCH 03/16] Fixing gates and tests --- bqskit/ir/gates/parameterized/mcry.py | 4 ++-- tests/ir/gates/parameterized/test_mcry.py | 27 ++++++++++++--------- tests/ir/gates/parameterized/test_mcrz.py | 29 +++++++++++++---------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py index f150fb703..f603da2c7 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -51,14 +51,14 @@ class MCRYGate( _qasm_name = 'mcry' - def __init__(self, num_qudits: int, controlled_qubit: int = -1) -> None: + def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: self._num_qudits = num_qudits # 1 param for each configuration of the selec qubits self._num_params = 2 ** (num_qudits - 1) # By default, the controlled qubit is the last qubit if target_qubit == -1: target_qubit = num_qudits - 1 - self.controlled_qubit = controlled_qubit + self.controlled_qubit = target_qubit super().__init__() def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: diff --git a/tests/ir/gates/parameterized/test_mcry.py b/tests/ir/gates/parameterized/test_mcry.py index e3e6aa1cc..7f5f5f997 100644 --- a/tests/ir/gates/parameterized/test_mcry.py +++ b/tests/ir/gates/parameterized/test_mcry.py @@ -15,11 +15,10 @@ def test_get_unitary(thetas: list[float]) -> None: Test the get_unitary method of the MCRYGate class. Use the default target qubit. ''' - # Assert that len(thetas) is a power of 2 - assert (len(thetas) & (len(thetas) - 1)) == 0 - + # Ensure that len(thetas) is a power of 2 # There are 2 ** (n - 1) parameters - num_qudits = np.log2(len(thetas)) + 1 + num_qudits = int(np.log2(len(thetas))) + 1 + thetas = thetas[:2 ** (num_qudits - 1)] mcry = MCRYGate(num_qudits=num_qudits) block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) @@ -33,20 +32,26 @@ def test_get_unitary_target_select(target_qubit: int) -> None: the target qubit is set. ''' # Create an MCRY gate with 6 qubits and random parameters - mcry = MCRYGate(num_qudits=6, target_qubit=target_qubit) - thetas = list(np.random.rand(2 ** 5) * 2 * np.pi) + num_qudits = 6 + mcry = MCRYGate(num_qudits=num_qudits, target_qubit=target_qubit) + thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) # Create the block diagonal matrix block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - # Apply a permutation to the block diagonal matrix + # Apply a permutation transformation + # to the block diagonal matrix # Swap the target qubit with the last qubit - perm = np.arange(6) - perm[-1], perm[target_qubit] = perm[target_qubit], perm[-1] - perm_gate = PermutationGate(6, perm) + # perm = np.arange(num_qudits) + perm = list(range(num_qudits)) + for i in range(target_qubit, num_qudits): + perm[i] = i + 1 + perm[-1] = target_qubit + + perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary() @ blocked_unitary + full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() dist = mcry.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7 \ No newline at end of file diff --git a/tests/ir/gates/parameterized/test_mcrz.py b/tests/ir/gates/parameterized/test_mcrz.py index bedc20822..3290a7658 100644 --- a/tests/ir/gates/parameterized/test_mcrz.py +++ b/tests/ir/gates/parameterized/test_mcrz.py @@ -16,11 +16,10 @@ def test_get_unitary(thetas: list[float]) -> None: Test the get_unitary method of the MCRZGate class. Use the default target qubit. ''' - # Assert that len(thetas) is a power of 2 - assert (len(thetas) & (len(thetas) - 1)) == 0 - + # Ensure that len(thetas) is a power of 2 # There are 2 ** (n - 1) parameters - num_qudits = np.log2(len(thetas)) + 1 + num_qudits = int(np.log2(len(thetas))) + 1 + thetas = thetas[:2 ** (num_qudits - 1)] mcry = MCRZGate(num_qudits=num_qudits) block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) @@ -33,21 +32,27 @@ def test_get_unitary_target_select(target_qubit: int) -> None: Test the get_unitary method of the MCRZGate class when the target qubit is set. ''' - # Create an MCRY gate with 6 qubits and random parameters - mcry = MCRZGate(num_qudits=6, target_qubit=target_qubit) - thetas = list(np.random.rand(2 ** 5) * 2 * np.pi) + # Create an MCRZ gate with 6 qubits and random parameters + num_qudits = 6 + mcry = MCRZGate(num_qudits=num_qudits, target_qubit=target_qubit) + thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) # Create the block diagonal matrix block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - # Apply a permutation to the block diagonal matrix + # Apply a permutation transformation + # to the block diagonal matrix # Swap the target qubit with the last qubit - perm = np.arange(6) - perm[-1], perm[target_qubit] = perm[target_qubit], perm[-1] - perm_gate = PermutationGate(6, perm) + # perm = np.arange(num_qudits) + perm = list(range(num_qudits)) + for i in range(target_qubit, num_qudits): + perm[i] = i + 1 + perm[-1] = target_qubit + + perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary() @ blocked_unitary + full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() dist = mcry.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7 \ No newline at end of file From 62c0b62147ddc0b1a46e4f771fa844f233750f6c Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 17:13:31 -0700 Subject: [PATCH 04/16] Fixing gradient calculation and running tox --- bqskit/ir/gates/parameterized/mcry.py | 78 ++++++++++++++-------- bqskit/ir/gates/parameterized/mcrz.py | 58 ++++++++++------ testing_mcr_gates.py | 80 +++++++++++++++++++++++ tests/ir/gates/parameterized/test_mcry.py | 41 ++++++++---- tests/ir/gates/parameterized/test_mcrz.py | 42 +++++++----- 5 files changed, 224 insertions(+), 75 deletions(-) create mode 100644 testing_mcr_gates.py diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py index f603da2c7..38bf47653 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -10,12 +10,14 @@ from bqskit.qis.unitary.unitary import RealVector from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix from bqskit.utils.cachedclass import CachedClass -import logging -def get_indices(index: int, target_qudit, num_qudits): - """ - Get indices for the matrix based on the target qubit. - """ + +def get_indices( + index: int, + target_qudit: int, + num_qudits: int, +) -> tuple[int, int]: + """Get indices for the matrix based on the target qubit.""" shift_qubit = num_qudits - target_qudit - 1 shift = 2 ** shift_qubit # Split into two parts around target qubit @@ -23,60 +25,70 @@ def get_indices(index: int, target_qudit, num_qudits): left = index // shift right = index % shift - # Now, shift left by one spot to + # Now, shift left by one spot to # make room for the target qubit left *= (shift * 2) # Now add 0 * new_ind and 1 * new_ind to get indices return left + right, left + shift + right + class MCRYGate( QubitGate, DifferentiableUnitary, CachedClass, - LocallyOptimizableUnitary + LocallyOptimizableUnitary, ): """ A gate representing a multiplexed Y rotation. A multiplexed Y rotation uses n - 1 qubits as select qubits and applies a Y rotation to the target. If the target qubit is the last qubit, then the unitary is block diagonal. - Each block is a 2x2 RY matrix with parameter theta. + Each block is a 2x2 RY matrix with parameter theta. Since there are n - 1 select qubits, there are 2^(n-1) parameters (thetas). We allow the target qubit to be specified to any qubit, and the other qubits - maintain their order. Qubit 0 is the most significant qubit. + maintain their order. Qubit 0 is the most significant qubit. See this paper: https://arxiv.org/pdf/quant-ph/0406176 """ _qasm_name = 'mcry' - def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: + def __init__( + self, + num_qudits: int, + target_qubit: int = -1, + ) -> None: self._num_qudits = num_qudits # 1 param for each configuration of the selec qubits self._num_params = 2 ** (num_qudits - 1) # By default, the controlled qubit is the last qubit if target_qubit == -1: target_qubit = num_qudits - 1 - self.controlled_qubit = target_qubit + self.target_qubit = target_qubit super().__init__() def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: """Return the unitary for this gate, see :class:`Unitary` for more.""" self.check_parameters(params) - matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + matrix = np.zeros( + ( + 2 ** self.num_qudits, + 2 ** self.num_qudits, + ), dtype=np.complex128, + ) for i, param in enumerate(params): cos = np.cos(param / 2) sin = np.sin(param / 2) # Now, get indices based on target qubit. - # i corresponds to the configuration of the - # select qubits (e.g 5 = 101). Now, the + # i corresponds to the configuration of the + # select qubits (e.g 5 = 101). Now, the # target qubit is 0,1 for both the row and col # indices. So, if i = 5 and the target_qubit is 2 # Then the rows/cols are 1001 and 1101 - x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) matrix[x1, x1] = cos matrix[x2, x2] = cos @@ -88,36 +100,45 @@ def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: """ Return the gradient for this gate. + See :class:`DifferentiableUnitary` for more info. """ self.check_parameters(params) - matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + orig_utry = self.get_unitary(params).numpy + grad = [] + + # For each parameter, calculate the derivative + # with respect to that parameter for i, param in enumerate(params): dcos = -np.sin(param / 2) / 2 dsin = -1j * np.cos(param / 2) / 2 # Again, get indices based on target qubit. - x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) + + matrix = orig_utry.copy() matrix[x1, x1] = dcos matrix[x2, x2] = dcos matrix[x2, x1] = dsin matrix[x1, x2] = -1 * dsin - return UnitaryMatrix(matrix) + grad.append(matrix) + return np.array(grad) def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: """ Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. """ self.check_env_matrix(env_matrix) - thetas = [0] * self.num_params + thetas: list[float] = [0] * self.num_params for i in range(self.num_params): - x1, x2 = get_indices(i, self.controlled_qubit, self.num_qudits) + x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) a = np.real(env_matrix[x1, x1] + env_matrix[x2, x2]) b = np.real(env_matrix[x2, x1] - env_matrix[x1, x2]) theta = 2 * np.arccos(a / np.sqrt(a ** 2 + b ** 2)) @@ -127,12 +148,17 @@ def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: return thetas @staticmethod - def get_decomposition(params: RealVector = []) -> tuple[RealVector, RealVector]: - ''' - Get the corresponding parameters for one level of decomposition - of a multiplexed gate. This is used in the decomposition of both + def get_decomposition(params: RealVector = []) -> tuple[ + RealVector, + RealVector, + ]: + """ + Get the corresponding parameters for one level of decomposition of a + multiplexed gate. + + This is used in the decomposition of both the MCRY and MCRZ gates. See :class:`MGDPass` for more info. - ''' + """ new_num_params = len(params) // 2 left_params = np.zeros(new_num_params) right_params = np.zeros(new_num_params) @@ -148,4 +174,4 @@ def get_decomposition(params: RealVector = []) -> tuple[RealVector, RealVector]: def name(self) -> str: """The name of this gate, with the number of qudits appended.""" base_name = getattr(self, '_name', self.__class__.__name__) - return f"{base_name}_{self.num_qudits}" \ No newline at end of file + return f'{base_name}_{self.num_qudits}' diff --git a/bqskit/ir/gates/parameterized/mcrz.py b/bqskit/ir/gates/parameterized/mcrz.py index 912814f5c..02439d012 100644 --- a/bqskit/ir/gates/parameterized/mcrz.py +++ b/bqskit/ir/gates/parameterized/mcrz.py @@ -4,31 +4,31 @@ import numpy as np import numpy.typing as npt +from bqskit.ir.gates.parameterized.mcry import get_indices from bqskit.ir.gates.qubitgate import QubitGate from bqskit.qis.unitary.differentiable import DifferentiableUnitary from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary from bqskit.qis.unitary.unitary import RealVector from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix from bqskit.utils.cachedclass import CachedClass -from bqskit.ir.gates.parameterized.mcry import get_indices -from typing import Any + class MCRZGate( QubitGate, DifferentiableUnitary, CachedClass, - LocallyOptimizableUnitary + LocallyOptimizableUnitary, ): """ A gate representing a multiplexed Z rotation. A multiplexed Z rotation uses n - 1 qubits as select qubits and applies a Z rotation to the target. If the target qubit is the last qubit, then the unitary is block diagonal. - Each block is a 2x2 RZ matrix with parameter theta. + Each block is a 2x2 RZ matrix with parameter theta. Since there are n - 1 select qubits, there are 2^(n-1) parameters (thetas). We allow the target qubit to be specified to any qubit, and the other qubits - maintain their order. Qubit 0 is the most significant qubit. + maintain their order. Qubit 0 is the most significant qubit. Why is 0 the MSB? Typically, in the QSD diagram, we see the block drawn @@ -40,11 +40,14 @@ class MCRZGate( _qasm_name = 'mcrz' - def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: - ''' - Create a new MCRZGate with `num_qudits` qubits and - `target_qubit` as the target qubit. We then have 2^(n-1) parameters - for this gate. + def __init__( + self, + num_qudits: int, + target_qubit: int = -1, + ) -> None: + """ + Create a new MCRZGate with `num_qudits` qubits and `target_qubit` as the + target qubit. We then have 2^(n-1) parameters for this gate. For Example: `num_qudits` = 3, `target_qubit` = 1 @@ -56,7 +59,7 @@ def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: If the input vector is |1x0> then the selection is 01, and RZ(theta_1) is applied to the target qubit. - ''' + """ self._num_qudits = num_qudits # 1 param for each configuration of the selec qubits self._num_params = 2 ** (num_qudits - 1) @@ -69,7 +72,12 @@ def __init__(self, num_qudits: int, target_qubit: int = -1) -> None: def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: """Return the unitary for this gate, see :class:`Unitary` for more.""" self.check_parameters(params) - matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + matrix = np.zeros( + ( + 2 ** self.num_qudits, + 2 ** self.num_qudits, + ), dtype=np.complex128, + ) for i, param in enumerate(params): pos = np.exp(1j * param / 2) neg = np.exp(-1j * param / 2) @@ -90,27 +98,37 @@ def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: See :class:`DifferentiableUnitary` for more info. """ self.check_parameters(params) - matrix = np.zeros((2 ** self.num_qudits, 2 ** self.num_qudits), dtype=np.complex128) + orig_utry = self.get_unitary(params).numpy + grad = [] + + # For each parameter, calculate the derivative + # with respect to that parameter for i, param in enumerate(params): - dpos = 1j / 2 * np.exp(1j * param / 2) - dneg = -1j / 2 * np.exp(-1j * param / 2) + dcos = -np.sin(param / 2) / 2 + dsin = -1j * np.cos(param / 2) / 2 # Again, get indices based on target qubit. x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) - matrix[x1, x1] = dpos - matrix[x2, x2] = dneg + matrix = orig_utry.copy() - return UnitaryMatrix(matrix) + matrix[x1, x1] = dcos + matrix[x2, x2] = dcos + matrix[x2, x1] = dsin + matrix[x1, x2] = -1 * dsin + grad.append(matrix) + + return np.array(grad) def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: """ Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. """ self.check_env_matrix(env_matrix) - thetas = [0] * self.num_params + thetas: list[float] = [0] * self.num_params for i in range(self.num_params): x1, x2 = get_indices(i, self.target_qubit, self.num_qudits) @@ -127,4 +145,4 @@ def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: def name(self) -> str: """The name of this gate, with the number of qudits appended.""" base_name = getattr(self, '_name', self.__class__.__name__) - return f"{base_name}_{self.num_qudits}" \ No newline at end of file + return f'{base_name}_{self.num_qudits}' diff --git a/testing_mcr_gates.py b/testing_mcr_gates.py new file mode 100644 index 000000000..7da791b0d --- /dev/null +++ b/testing_mcr_gates.py @@ -0,0 +1,80 @@ +import numpy as np +from scipy.linalg import block_diag + +from bqskit.ir.gates.parameterized import MCRYGate, RYGate, MCRZGate, RZGate +from bqskit.ir.gates.constant import PermutationGate + +def test_get_unitary_mcry(thetas: list[float]) -> None: + ''' + Test the get_unitary method of the MCRYGate class. + Use the default target qubit. + ''' + # Ensure that len(thetas) is a power of 2 + # There are 2 ** (n - 1) parameters + num_qudits = int(np.log2(len(thetas))) + 1 + thetas = thetas[:2 ** (num_qudits - 1)] + + mcry = MCRYGate(num_qudits=num_qudits) + block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + assert dist < 1e-7 + +def test_get_unitary_mcrz(thetas: list[float]) -> None: + ''' + Test the get_unitary method of the MCRYGate class. + Use the default target qubit. + ''' + # Ensure that len(thetas) is a power of 2 + # There are 2 ** (n - 1) parameters + num_qudits = int(np.log2(len(thetas))) + 1 + thetas = thetas[:2 ** (num_qudits - 1)] + + mcry = MCRZGate(num_qudits=num_qudits) + block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + assert dist < 1e-7 + +def test_get_unitary_target_select_mcry(target_qubit: int) -> None: + ''' + Test the get_unitary method of the MCRYGate class when + the target qubit is set. + ''' + # Create an MCRY gate with 6 qubits and random parameters + num_qudits = 6 + mcry = MCRYGate(num_qudits=num_qudits, target_qubit=target_qubit) + thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) + + # Create the block diagonal matrix + block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] + blocked_unitary = block_diag(*block_unitaries) + + # Apply a permutation transformation + # to the block diagonal matrix + # Swap the target qubit with the last qubit + # perm = np.arange(num_qudits) + perm = list(range(num_qudits)) + for i in range(target_qubit, num_qudits): + perm[i] = i + 1 + perm[-1] = target_qubit + + perm_gate = PermutationGate(num_qudits, perm) + + full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() + + dist = mcry.get_unitary(thetas).get_distance_from(full_utry) + assert dist < 1e-7 + + + + +for num_params in [2,4,8, 20]: + params = np.random.rand(num_params) * 2 * np.pi + test_get_unitary_mcry(params) + test_get_unitary_mcrz(params) + +np.printoptions(precision=3, threshold=np.inf, linewidth=np.inf) + +for target_qubit in [0,1,2,3,4,5]: + test_get_unitary_target_select_mcry(target_qubit) \ No newline at end of file diff --git a/tests/ir/gates/parameterized/test_mcry.py b/tests/ir/gates/parameterized/test_mcry.py index 7f5f5f997..bf7887b29 100644 --- a/tests/ir/gates/parameterized/test_mcry.py +++ b/tests/ir/gates/parameterized/test_mcry.py @@ -2,19 +2,32 @@ from __future__ import annotations import numpy as np -from scipy.linalg import block_diag from hypothesis import given -from hypothesis.strategies import floats, integers, lists +from hypothesis.strategies import floats +from hypothesis.strategies import integers +from hypothesis.strategies import lists +from scipy.linalg import block_diag -from bqskit.ir.gates.parameterized import MCRYGate, RYGate from bqskit.ir.gates.constant import PermutationGate +from bqskit.ir.gates.parameterized import MCRYGate +from bqskit.ir.gates.parameterized import RYGate + -@given(lists(elements=floats(allow_nan=False, allow_infinity=False, width=32), min_size=2, max_size=16)) +@given( + lists( + elements=floats( + allow_nan=False, + allow_infinity=False, + width=32, + ), min_size=2, max_size=16, + ), +) def test_get_unitary(thetas: list[float]) -> None: - ''' + """ Test the get_unitary method of the MCRYGate class. + Use the default target qubit. - ''' + """ # Ensure that len(thetas) is a power of 2 # There are 2 ** (n - 1) parameters num_qudits = int(np.log2(len(thetas))) + 1 @@ -25,12 +38,11 @@ def test_get_unitary(thetas: list[float]) -> None: dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) assert dist < 1e-7 + @given(integers(min_value=0, max_value=4)) def test_get_unitary_target_select(target_qubit: int) -> None: - ''' - Test the get_unitary method of the MCRYGate class when - the target qubit is set. - ''' + """Test the get_unitary method of the MCRYGate class when the target qubit + is set.""" # Create an MCRY gate with 6 qubits and random parameters num_qudits = 6 mcry = MCRYGate(num_qudits=num_qudits, target_qubit=target_qubit) @@ -40,7 +52,7 @@ def test_get_unitary_target_select(target_qubit: int) -> None: block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - # Apply a permutation transformation + # Apply a permutation transformation # to the block diagonal matrix # Swap the target qubit with the last qubit # perm = np.arange(num_qudits) @@ -49,9 +61,10 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm[i] = i + 1 perm[-1] = target_qubit - perm_gate = PermutationGate(num_qudits, perm) + perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() + full_utry = perm_gate.get_unitary().conj( + ).T @ blocked_unitary @ perm_gate.get_unitary() dist = mcry.get_unitary(thetas).get_distance_from(full_utry) - assert dist < 1e-7 \ No newline at end of file + assert dist < 1e-7 diff --git a/tests/ir/gates/parameterized/test_mcrz.py b/tests/ir/gates/parameterized/test_mcrz.py index 3290a7658..392ae7a23 100644 --- a/tests/ir/gates/parameterized/test_mcrz.py +++ b/tests/ir/gates/parameterized/test_mcrz.py @@ -2,20 +2,32 @@ from __future__ import annotations import numpy as np -from scipy.linalg import block_diag - from hypothesis import given -from hypothesis.strategies import floats, integers, lists +from hypothesis.strategies import floats +from hypothesis.strategies import integers +from hypothesis.strategies import lists +from scipy.linalg import block_diag -from bqskit.ir.gates.parameterized import MCRZGate, RZGate from bqskit.ir.gates.constant import PermutationGate +from bqskit.ir.gates.parameterized import MCRZGate +from bqskit.ir.gates.parameterized import RZGate + -@given(lists(elements=floats(allow_nan=False, allow_infinity=False, width=32), min_size=2, max_size=16)) +@given( + lists( + elements=floats( + allow_nan=False, + allow_infinity=False, + width=32, + ), min_size=2, max_size=16, + ), +) def test_get_unitary(thetas: list[float]) -> None: - ''' + """ Test the get_unitary method of the MCRZGate class. + Use the default target qubit. - ''' + """ # Ensure that len(thetas) is a power of 2 # There are 2 ** (n - 1) parameters num_qudits = int(np.log2(len(thetas))) + 1 @@ -26,12 +38,11 @@ def test_get_unitary(thetas: list[float]) -> None: dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) assert dist < 1e-7 + @given(integers(min_value=0, max_value=4)) def test_get_unitary_target_select(target_qubit: int) -> None: - ''' - Test the get_unitary method of the MCRZGate class when - the target qubit is set. - ''' + """Test the get_unitary method of the MCRZGate class when the target qubit + is set.""" # Create an MCRZ gate with 6 qubits and random parameters num_qudits = 6 mcry = MCRZGate(num_qudits=num_qudits, target_qubit=target_qubit) @@ -41,7 +52,7 @@ def test_get_unitary_target_select(target_qubit: int) -> None: block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - # Apply a permutation transformation + # Apply a permutation transformation # to the block diagonal matrix # Swap the target qubit with the last qubit # perm = np.arange(num_qudits) @@ -50,9 +61,10 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm[i] = i + 1 perm[-1] = target_qubit - perm_gate = PermutationGate(num_qudits, perm) + perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() + full_utry = perm_gate.get_unitary().conj( + ).T @ blocked_unitary @ perm_gate.get_unitary() dist = mcry.get_unitary(thetas).get_distance_from(full_utry) - assert dist < 1e-7 \ No newline at end of file + assert dist < 1e-7 From 2a53462cea24ce24931caff43f90a4e2b0fe1fc0 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 17:16:30 -0700 Subject: [PATCH 05/16] Removing extra test file --- testing_mcr_gates.py | 80 -------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 testing_mcr_gates.py diff --git a/testing_mcr_gates.py b/testing_mcr_gates.py deleted file mode 100644 index 7da791b0d..000000000 --- a/testing_mcr_gates.py +++ /dev/null @@ -1,80 +0,0 @@ -import numpy as np -from scipy.linalg import block_diag - -from bqskit.ir.gates.parameterized import MCRYGate, RYGate, MCRZGate, RZGate -from bqskit.ir.gates.constant import PermutationGate - -def test_get_unitary_mcry(thetas: list[float]) -> None: - ''' - Test the get_unitary method of the MCRYGate class. - Use the default target qubit. - ''' - # Ensure that len(thetas) is a power of 2 - # There are 2 ** (n - 1) parameters - num_qudits = int(np.log2(len(thetas))) + 1 - thetas = thetas[:2 ** (num_qudits - 1)] - - mcry = MCRYGate(num_qudits=num_qudits) - block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] - blocked_unitary = block_diag(*block_unitaries) - dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) - assert dist < 1e-7 - -def test_get_unitary_mcrz(thetas: list[float]) -> None: - ''' - Test the get_unitary method of the MCRYGate class. - Use the default target qubit. - ''' - # Ensure that len(thetas) is a power of 2 - # There are 2 ** (n - 1) parameters - num_qudits = int(np.log2(len(thetas))) + 1 - thetas = thetas[:2 ** (num_qudits - 1)] - - mcry = MCRZGate(num_qudits=num_qudits) - block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] - blocked_unitary = block_diag(*block_unitaries) - dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) - assert dist < 1e-7 - -def test_get_unitary_target_select_mcry(target_qubit: int) -> None: - ''' - Test the get_unitary method of the MCRYGate class when - the target qubit is set. - ''' - # Create an MCRY gate with 6 qubits and random parameters - num_qudits = 6 - mcry = MCRYGate(num_qudits=num_qudits, target_qubit=target_qubit) - thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) - - # Create the block diagonal matrix - block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] - blocked_unitary = block_diag(*block_unitaries) - - # Apply a permutation transformation - # to the block diagonal matrix - # Swap the target qubit with the last qubit - # perm = np.arange(num_qudits) - perm = list(range(num_qudits)) - for i in range(target_qubit, num_qudits): - perm[i] = i + 1 - perm[-1] = target_qubit - - perm_gate = PermutationGate(num_qudits, perm) - - full_utry = perm_gate.get_unitary().conj().T @ blocked_unitary @ perm_gate.get_unitary() - - dist = mcry.get_unitary(thetas).get_distance_from(full_utry) - assert dist < 1e-7 - - - - -for num_params in [2,4,8, 20]: - params = np.random.rand(num_params) * 2 * np.pi - test_get_unitary_mcry(params) - test_get_unitary_mcrz(params) - -np.printoptions(precision=3, threshold=np.inf, linewidth=np.inf) - -for target_qubit in [0,1,2,3,4,5]: - test_get_unitary_target_select_mcry(target_qubit) \ No newline at end of file From 4e2a2172357cbab316d1205ab503f625b74e138a Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 18:06:34 -0700 Subject: [PATCH 06/16] Initial QSD Commit --- bqskit/passes/__init__.py | 2 + bqskit/passes/synthesis/__init__.py | 2 + bqskit/passes/synthesis/qsd.py | 334 ++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 bqskit/passes/synthesis/qsd.py diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py index b05f53246..3847f3ae3 100644 --- a/bqskit/passes/__init__.py +++ b/bqskit/passes/__init__.py @@ -296,6 +296,7 @@ from bqskit.passes.synthesis.pas import PermutationAwareSynthesisPass from bqskit.passes.synthesis.qfast import QFASTDecompositionPass from bqskit.passes.synthesis.qpredict import QPredictDecompositionPass +from bqskit.passes.synthesis.qsd import FullQSDPass from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass from bqskit.passes.synthesis.synthesis import SynthesisPass from bqskit.passes.synthesis.target import SetTargetPass @@ -332,6 +333,7 @@ 'WalshDiagonalSynthesisPass', 'LEAPSynthesisPass', 'QSearchSynthesisPass', + 'FullQSDPass', 'QFASTDecompositionPass', 'QPredictDecompositionPass', 'CompressPass', diff --git a/bqskit/passes/synthesis/__init__.py b/bqskit/passes/synthesis/__init__.py index eea097231..bf34c14e9 100644 --- a/bqskit/passes/synthesis/__init__.py +++ b/bqskit/passes/synthesis/__init__.py @@ -6,6 +6,7 @@ from bqskit.passes.synthesis.pas import PermutationAwareSynthesisPass from bqskit.passes.synthesis.qfast import QFASTDecompositionPass from bqskit.passes.synthesis.qpredict import QPredictDecompositionPass +from bqskit.passes.synthesis.qsd import FullQSDPass from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass from bqskit.passes.synthesis.synthesis import SynthesisPass from bqskit.passes.synthesis.target import SetTargetPass @@ -19,4 +20,5 @@ 'SetTargetPass', 'PermutationAwareSynthesisPass', 'WalshDiagonalSynthesisPass', + 'FullQSDPass', ] diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py new file mode 100644 index 000000000..1eb04ff68 --- /dev/null +++ b/bqskit/passes/synthesis/qsd.py @@ -0,0 +1,334 @@ +"""This module implements the Quantum Shannon Decomposition""" +from __future__ import annotations + +import logging +import numpy as np +from scipy.linalg import cossin, diagsvd, schur +from typing import Any + +from bqskit.compiler.basepass import BasePass + +from bqskit.compiler.passdata import PassData +from bqskit.compiler.workflow import Workflow +from bqskit.passes.alias import PassAlias +from bqskit.passes.processing import ScanningGateRemovalPass, TreeScanningGateRemovalPass +from bqskit.ir.gates.parameterized.mcry import MCRYGate +from bqskit.ir.gates.parameterized.mcrz import MCRZGate +from bqskit.ir.gates.parameterized import RYGate, RZGate, VariableUnitaryGate +from bqskit.ir.operation import Operation +from bqskit.ir.gates.constant import CNOTGate +from bqskit.ir.circuit import Circuit, CircuitLocation +from bqskit.ir.gates import CircuitGate +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.qis.unitary.unitary import RealVector +from bqskit.runtime import get_runtime +from bqskit.qis.permutation import PermutationMatrix + +_logger = logging.getLogger(__name__) + + +class FullQSDPass(PassAlias): + """ + A pass performing one round of decomposition from the QSD algorithm. + + Important: This pass runs on VariableUnitaryGates only. Make sure to convert + any gates you want to decompose to VariableUnitaryGates before running this pass. + + Additionally, ScanningGateRemovalPass will operate on the context of the + entire circuit. If your circuit is large, it is best to set `perform_scan` to False + + References: + C.C. Paige, M. Wei, + History and generality of the CS decomposition, + Linear Algebra and its Applications, + Volumes 208–209, + 1994, + Pages 303-326, + ISSN 0024-3795, + https://doi.org/10.1016/0024-3795(94)90446-4. + """ + + def __init__( + self, + min_qudit_size: int = 2, + perform_scan: bool = False, + start_from_left: bool = True, + tree_depth: int = 0, + instantiate_options: dict[str, Any] = {}, + ) -> None: + """ + Construct a single level of the QSDPass. + Args: + min_qudit_size (int): Performs QSD until the circuit only has + VariableUnitaryGates with a number of qudits less than or equal + to this value. (Default: 2) + perform_scan (bool): Whether or not to perform the scanning + gate removal pass. (Default: False) + start_from_left (bool): Determines where the scan starts + attempting to remove gates from. If True, scan goes left + to right, otherwise right to left. (Default: True) + tree_depth (int): The depth of the tree to use in the + TreeScanningGateRemovalPass. If set to 0, we will instead + use the ScanningGateRemovalPass. (Default: 0) + instantiate_options (dict): The options to pass to the + scanning gate removal pass. (Default: {}) + """ + self.start_from_left = start_from_left + self.min_qudit_size = min_qudit_size + instantiation_options = {"method":"qfactor"} + instantiation_options.update(instantiate_options) + self.scan: TreeScanningGateRemovalPass | ScanningGateRemovalPass = ScanningGateRemovalPass(start_from_left=start_from_left, + instantiate_options=instantiation_options) + if tree_depth > 0: + self.scan = TreeScanningGateRemovalPass(start_from_left=start_from_left, instantiate_options=instantiation_options, tree_depth=tree_depth) + # Instantiate the helper QSD pass + self.qsd = QSDPass(min_qudit_size=min_qudit_size) + # Instantiate the helper Multiplex Gate Decomposition pass + self.mgd = MGDPass() + self.perform_scan = perform_scan + + async def run(self, circuit: Circuit, data: PassData) -> None: + """ Run a round of QSD, Multiplex Gate Decomposition, + and Scanning Gate Removal (optionally) until you reach the desired qudit size gates.""" + passes: list[BasePass] = [] + start_num = max(x.num_qudits for x in circuit.operations()) + for _ in range(self.min_qudit_size, start_num): + passes.append(self.qsd) + if self.perform_scan: + passes.append(self.scan) + passes.append(self.mgd) + await Workflow(passes).run(circuit, data) + + +class MGDPass(BasePass): + """ + A pass performing one round of decomposition of the MCRY and MCRZ gates in a circuit. + + References: + C.C. Paige, M. Wei, + History and generality of the CS decomposition, + Linear Algebra and its Applications, + Volumes 208–209, + 1994, + Pages 303-326, + ISSN 0024-3795, + https://arxiv.org/pdf/quant-ph/0406176.pdf + """ + + @staticmethod + def decompose(op: Operation) -> Circuit: + ''' + Return the decomposed circuit from one operation of a + multiplexed gate. + + Args: + op (Operation): The operation to decompose. + + Returns: + Circuit: The decomposed circuit. + ''' + + # Final level of decomposition decomposes to RY or RZ gate + gate: MCRYGate | MCRZGate | RYGate | RZGate = MCRZGate(op.num_qudits - 1, + op.num_qudits - 2) + if (op.num_qudits > 2): + if isinstance(op.gate, MCRYGate): + gate = MCRYGate(op.num_qudits - 1, op.num_qudits - 2) + elif (isinstance(op.gate, MCRYGate)): + gate = RYGate() + else: + gate = RZGate() + + + left_params, right_params = MCRYGate.get_decomposition(op.params) + + # Construct Circuit + circ = Circuit(gate.num_qudits) + new_gate_location = list(range(1, gate.num_qudits)) + cx_location = (0, gate.num_qudits - 1) + circ.append_gate(gate, new_gate_location, left_params) + circ.append_gate(CNOTGate(), cx_location) + circ.append_gate(gate, new_gate_location, right_params) + circ.append_gate(CNOTGate(), cx_location) + + return circ + + async def run(self, circuit: Circuit, data: PassData) -> None: + """Decompose all MCRY and MCRZ gates in the circuit one level.""" + gates = [] + pts = [] + locations = [] + num_ops = 0 + all_ops = list(circuit.operations_with_cycles(reverse=True)) + + # Gather all of the multiplexed operations + for cyc, op in all_ops: + if isinstance(op.gate, MCRYGate) or isinstance(op.gate, MCRZGate): + num_ops += 1 + gates.append(op) + pts.append((cyc, op.location[0])) + locations.append(op.location) + + if len(gates) > 0: + # Do a bulk QSDs -> circs + circs = [MGDPass.decompose(gate) for gate in gates] + circ_gates = [CircuitGate(x) for x in circs] + circ_ops = [Operation(x, locations[i], x._circuit.params) for i,x in enumerate(circ_gates)] + circuit.batch_replace(pts, circ_ops) + circuit.unfold_all() + + circuit.unfold_all() + +def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: + top_qubits = num_qudits - end_qubits + now_bottom_qubits = list(reversed(range(top_qubits))) + now_top_qubits = list(range(num_qudits - end_qubits, num_qudits)) + final_qudits = now_top_qubits + now_bottom_qubits + return PermutationMatrix.from_qubit_location(num_qudits, final_qudits) + +def shift_up_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: + bottom_qubits = list(range(end_qubits)) + top_qubits = list(reversed(range(end_qubits, num_qudits))) + final_qudits = top_qubits + bottom_qubits + return PermutationMatrix.from_qubit_location(num_qudits, final_qudits) + + + +class QSDPass(BasePass): + """ + A pass performing one round of decomposition from the QSD algorithm. + + This decomposition takes each unitary of size n and decomposes it into a circuit + with 4 VariableUnitaryGates of size n - 1 and 3 multiplexed rotation gates. + + Important: This pass runs on VariableUnitaryGates only. + + References: + https://arxiv.org/pdf/quant-ph/0406176 + """ + + def __init__( + self, + min_qudit_size: int = 4, + ) -> None: + """ + Construct a single level of the QSDPass. + Args: + min_qudit_size (int): Performs a decomposition on all gates + with width > min_qudit_size + """ + self.min_qudit_size = min_qudit_size + + @staticmethod + def create_unitary_gate(u: UnitaryMatrix) -> tuple[VariableUnitaryGate, RealVector]: + """ + Create a VariableUnitaryGate from a UnitaryMatrix. + """ + gate = VariableUnitaryGate(u.num_qudits) + params = np.concatenate((np.real(u).flatten(), np.imag(u).flatten())) + return gate, params + + @staticmethod + def create_multiplexed_circ(us: list[UnitaryMatrix], select_qubits: list[int], controlled_qubit: int) -> Circuit: + ''' + Takes a list of 2 unitaries of size n. Returns a circuit that + decomposes the unitaries into a circuit with 2 unitaries of size n-1 and a + multiplexed controlled gate. + + Args: + us (list[UnitaryMatrix]): The unitaries to decompose + select_qubits (list[int]): The qubits to use as select qubits + controlled_qubit (int): The qubit to use as the controlled qubit + + Returns: + Circuit: The circuit that decomposes the unitaries + + Using this paper: https://arxiv.org/pdf/quant-ph/0406176.pdf. Thm 12 + + ''' + u1 = us[0] + u2 = us[1] + assert(u1.num_qudits == u2.num_qudits) + all_qubits = list(range(len(select_qubits) + 1)) + # Use Schur Decomposition to split Us into V, D, and W matrices + D_2, V = schur(u1._utry @ u2.dagger._utry) + D = np.sqrt(np.diag(np.diag(D_2))) # D^2 will be diagonal since u1u2h is unitary + # Calculate W @ U1 + left_mat = D @ V.conj().T @ u2._utry + left_gate, left_params = QSDPass.create_unitary_gate(UnitaryMatrix(left_mat)) + + # Create Multi Controlled Z Gate + z_params: RealVector = np.array(-2 * np.angle(np.diag(D)).flatten()) + z_gate = MCRZGate(len(all_qubits), u1.num_qudits) + + # Create right gate + right_gate, right_params = QSDPass.create_unitary_gate(UnitaryMatrix(V)) + + circ = Circuit(u1.num_qudits + 1) + circ.append_gate(left_gate, CircuitLocation(select_qubits), left_params) + circ.append_gate(z_gate, CircuitLocation(all_qubits), z_params) + circ.append_gate(right_gate, CircuitLocation(select_qubits), right_params) + return circ + + @staticmethod + def mod_unitaries(u: UnitaryMatrix) -> UnitaryMatrix: + ''' + Apply a permutation transform to the unitaries to the rest of the circuit. + ''' + shift_up = shift_up_unitary(u.num_qudits, u.num_qudits - 1) + shift_down = shift_down_unitary(u.num_qudits, u.num_qudits - 1) + return shift_up @ u @ shift_down + + @staticmethod + def qsd(orig_u: UnitaryMatrix) -> Circuit: + ''' + Perform the Quantum Shannon Decomposition on a unitary matrix. + Args: + orig_u (UnitaryMatrix): The unitary matrix to decompose + + Returns: + Circuit: The circuit that decomposes the unitary + ''' + + # Shift the unitary qubits down by one + u = QSDPass.mod_unitaries(orig_u) + + # Perform CS Decomposition to solve for multiplexed unitaries and theta_y + (u1, u2), theta_y, (v1h, v2h) = cossin(u._utry, p=u.shape[0]/2, q=u.shape[1]/2, separate=True) + assert(len(theta_y) == u.shape[0] / 2) + + # Create the multiplexed circuit + # This generates 2 circuits that multipex U and V with an MCRY gate in between + controlled_qubit = u.num_qudits - 1 + select_qubits = list(range(0, u.num_qudits - 1)) + all_qubits = list(range(u.num_qudits)) + circ_1 = QSDPass.create_multiplexed_circ([UnitaryMatrix(v1h), UnitaryMatrix(v2h)], select_qubits, controlled_qubit) + circ_2 = QSDPass.create_multiplexed_circ([UnitaryMatrix(u1), UnitaryMatrix(u2)], select_qubits, controlled_qubit) + gate_2 = MCRYGate(u.num_qudits, controlled_qubit) + + circ_1.append_gate(gate_2, CircuitLocation(all_qubits), 2 * theta_y) + circ_1.append_circuit(circ_2, CircuitLocation(list(range(u.num_qudits)))) + return circ_1 + + async def run(self, circuit: Circuit, data: PassData) -> None: + unitaries = [] + pts = [] + locations = [] + num_ops = 0 + all_ops = list(circuit.operations_with_cycles(reverse=True)) + # Gather all of the unitaries + for cyc, op in all_ops: + if op.num_qudits > self.min_qudit_size and not (isinstance(op.gate, MCRYGate) or isinstance(op.gate, MCRZGate)): + num_ops += 1 + unitaries.append(op.get_unitary()) + pts.append((cyc, op.location[0])) + locations.append(op.location) + + if len(unitaries) > 0: + circs = await get_runtime().map(QSDPass.qsd, unitaries) + circ_gates = [CircuitGate(x) for x in circs] + circ_ops = [Operation(x, locations[i], x._circuit.params) for i,x in enumerate(circ_gates)] + circuit.batch_replace(pts, circ_ops) + circuit.unfold_all() + + circuit.unfold_all() \ No newline at end of file From ea7c8ce1e99849af850e67f5d547f712135e2e70 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Tue, 24 Sep 2024 11:51:54 -0700 Subject: [PATCH 07/16] Fixing docstring error --- bqskit/ir/gates/parameterized/mcry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py index 38bf47653..b359ab5a7 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -157,7 +157,7 @@ def get_decomposition(params: RealVector = []) -> tuple[ multiplexed gate. This is used in the decomposition of both - the MCRY and MCRZ gates. See :class:`MGDPass` for more info. + the MCRY and MCRZ gates. """ new_num_params = len(params) // 2 left_params = np.zeros(new_num_params) From 5281ff6f41a4874b4f91be6fa017b1ea541ce8db Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Tue, 24 Sep 2024 11:55:29 -0700 Subject: [PATCH 08/16] tox --- bqskit/ir/gates/parameterized/mcry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mcry.py index b359ab5a7..4246e3968 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mcry.py @@ -156,8 +156,7 @@ def get_decomposition(params: RealVector = []) -> tuple[ Get the corresponding parameters for one level of decomposition of a multiplexed gate. - This is used in the decomposition of both - the MCRY and MCRZ gates. + This is used in the decomposition of both the MCRY and MCRZ gates. """ new_num_params = len(params) // 2 left_params = np.zeros(new_num_params) From 491935b2bfffece6561eef6255b1bf3423e5b6fd Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:08:31 -0700 Subject: [PATCH 09/16] Adding a test and formatting QSD. Additionally, I make TreeScan extend ScanningGate to signify that it is an extension of the same function. --- bqskit/passes/processing/treescan.py | 4 +- bqskit/passes/synthesis/qsd.py | 284 ++++++++++++++++----------- tests/passes/synthesis/test_qsd.py | 59 ++++++ 3 files changed, 231 insertions(+), 116 deletions(-) create mode 100644 tests/passes/synthesis/test_qsd.py diff --git a/bqskit/passes/processing/treescan.py b/bqskit/passes/processing/treescan.py index 12376e59d..9093a46c1 100644 --- a/bqskit/passes/processing/treescan.py +++ b/bqskit/passes/processing/treescan.py @@ -5,12 +5,12 @@ from typing import Any from typing import Callable -from bqskit.compiler.basepass import BasePass from bqskit.compiler.passdata import PassData from bqskit.ir.circuit import Circuit from bqskit.ir.operation import Operation from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator from bqskit.ir.opt.cost.generator import CostFunctionGenerator +from bqskit.passes.processing.scan import ScanningGateRemovalPass from bqskit.runtime import get_runtime from bqskit.utils.typing import is_integer from bqskit.utils.typing import is_real_number @@ -18,7 +18,7 @@ _logger = logging.getLogger(__name__) -class TreeScanningGateRemovalPass(BasePass): +class TreeScanningGateRemovalPass(ScanningGateRemovalPass): """ The TreeScanningGateRemovalPass class. diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py index 1eb04ff68..6c29dd4b2 100644 --- a/bqskit/passes/synthesis/qsd.py +++ b/bqskit/passes/synthesis/qsd.py @@ -1,41 +1,47 @@ -"""This module implements the Quantum Shannon Decomposition""" +"""This module implements the Quantum Shannon Decomposition.""" from __future__ import annotations import logging -import numpy as np -from scipy.linalg import cossin, diagsvd, schur from typing import Any -from bqskit.compiler.basepass import BasePass +import numpy as np +from scipy.linalg import cossin +from scipy.linalg import schur +from bqskit.compiler.basepass import BasePass from bqskit.compiler.passdata import PassData from bqskit.compiler.workflow import Workflow -from bqskit.passes.alias import PassAlias -from bqskit.passes.processing import ScanningGateRemovalPass, TreeScanningGateRemovalPass +from bqskit.ir.circuit import Circuit +from bqskit.ir.circuit import CircuitLocation +from bqskit.ir.gates import CircuitGate +from bqskit.ir.gates.constant import CNOTGate +from bqskit.ir.gates.parameterized import RYGate +from bqskit.ir.gates.parameterized import RZGate +from bqskit.ir.gates.parameterized import VariableUnitaryGate from bqskit.ir.gates.parameterized.mcry import MCRYGate from bqskit.ir.gates.parameterized.mcrz import MCRZGate -from bqskit.ir.gates.parameterized import RYGate, RZGate, VariableUnitaryGate from bqskit.ir.operation import Operation -from bqskit.ir.gates.constant import CNOTGate -from bqskit.ir.circuit import Circuit, CircuitLocation -from bqskit.ir.gates import CircuitGate -from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.passes.processing import ScanningGateRemovalPass +from bqskit.passes.processing import TreeScanningGateRemovalPass +from bqskit.qis.permutation import PermutationMatrix from bqskit.qis.unitary.unitary import RealVector +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix from bqskit.runtime import get_runtime -from bqskit.qis.permutation import PermutationMatrix _logger = logging.getLogger(__name__) -class FullQSDPass(PassAlias): +class FullQSDPass(BasePass): """ A pass performing one round of decomposition from the QSD algorithm. Important: This pass runs on VariableUnitaryGates only. Make sure to convert - any gates you want to decompose to VariableUnitaryGates before running this pass. + any gates you want to decompose to VariableUnitaryGates before running this + pass. Additionally, ScanningGateRemovalPass will operate on the context of the - entire circuit. If your circuit is large, it is best to set `perform_scan` to False + entire circuit. If your circuit is large, it is best to set `perform_scan` + to False. References: C.C. Paige, M. Wei, @@ -49,47 +55,54 @@ class FullQSDPass(PassAlias): """ def __init__( - self, - min_qudit_size: int = 2, - perform_scan: bool = False, - start_from_left: bool = True, - tree_depth: int = 0, - instantiate_options: dict[str, Any] = {}, - ) -> None: - """ - Construct a single level of the QSDPass. - Args: - min_qudit_size (int): Performs QSD until the circuit only has - VariableUnitaryGates with a number of qudits less than or equal - to this value. (Default: 2) - perform_scan (bool): Whether or not to perform the scanning - gate removal pass. (Default: False) - start_from_left (bool): Determines where the scan starts - attempting to remove gates from. If True, scan goes left - to right, otherwise right to left. (Default: True) - tree_depth (int): The depth of the tree to use in the - TreeScanningGateRemovalPass. If set to 0, we will instead - use the ScanningGateRemovalPass. (Default: 0) - instantiate_options (dict): The options to pass to the - scanning gate removal pass. (Default: {}) - """ - self.start_from_left = start_from_left - self.min_qudit_size = min_qudit_size - instantiation_options = {"method":"qfactor"} - instantiation_options.update(instantiate_options) - self.scan: TreeScanningGateRemovalPass | ScanningGateRemovalPass = ScanningGateRemovalPass(start_from_left=start_from_left, - instantiate_options=instantiation_options) - if tree_depth > 0: - self.scan = TreeScanningGateRemovalPass(start_from_left=start_from_left, instantiate_options=instantiation_options, tree_depth=tree_depth) - # Instantiate the helper QSD pass - self.qsd = QSDPass(min_qudit_size=min_qudit_size) - # Instantiate the helper Multiplex Gate Decomposition pass - self.mgd = MGDPass() - self.perform_scan = perform_scan + self, + min_qudit_size: int = 2, + perform_scan: bool = False, + start_from_left: bool = True, + tree_depth: int = 0, + instantiate_options: dict[str, Any] = {}, + ) -> None: + """ + Construct a single level of the QSDPass. + + Args: + min_qudit_size (int): Performs QSD until the circuit only has + VariableUnitaryGates with a number of qudits less than or equal + to this value. (Default: 2) + perform_scan (bool): Whether or not to perform the scanning + gate removal pass. (Default: False) + start_from_left (bool): Determines where the scan starts + attempting to remove gates from. If True, scan goes left + to right, otherwise right to left. (Default: True) + tree_depth (int): The depth of the tree to use in the + TreeScanningGateRemovalPass. If set to 0, we will instead + use the ScanningGateRemovalPass. (Default: 0) + instantiate_options (dict): The options to pass to the + scanning gate removal pass. (Default: {}) + """ + self.start_from_left = start_from_left + self.min_qudit_size = min_qudit_size + instantiation_options = {'method': 'qfactor'} + instantiation_options.update(instantiate_options) + self.scan = ScanningGateRemovalPass( + start_from_left=start_from_left, + instantiate_options=instantiation_options, + ) + if tree_depth > 0: + self.scan = TreeScanningGateRemovalPass( + start_from_left=start_from_left, + instantiate_options=instantiation_options, + tree_depth=tree_depth, + ) + # Instantiate the helper QSD pass + self.qsd = QSDPass(min_qudit_size=min_qudit_size) + # Instantiate the helper Multiplex Gate Decomposition pass + self.mgd = MGDPass() + self.perform_scan = perform_scan async def run(self, circuit: Circuit, data: PassData) -> None: - """ Run a round of QSD, Multiplex Gate Decomposition, - and Scanning Gate Removal (optionally) until you reach the desired qudit size gates.""" + """Run a round of QSD, Multiplex Gate Decomposition, and Scanning Gate + Removal (optionally) until you reach the desired qudit size gates.""" passes: list[BasePass] = [] start_num = max(x.num_qudits for x in circuit.operations()) for _ in range(self.min_qudit_size, start_num): @@ -102,7 +115,8 @@ async def run(self, circuit: Circuit, data: PassData) -> None: class MGDPass(BasePass): """ - A pass performing one round of decomposition of the MCRY and MCRZ gates in a circuit. + A pass performing one round of decomposition of the MCRY and MCRZ gates in a + circuit. References: C.C. Paige, M. Wei, @@ -117,20 +131,21 @@ class MGDPass(BasePass): @staticmethod def decompose(op: Operation) -> Circuit: - ''' - Return the decomposed circuit from one operation of a - multiplexed gate. + """ + Return the decomposed circuit from one operation of a multiplexed gate. Args: op (Operation): The operation to decompose. - + Returns: Circuit: The decomposed circuit. - ''' + """ # Final level of decomposition decomposes to RY or RZ gate - gate: MCRYGate | MCRZGate | RYGate | RZGate = MCRZGate(op.num_qudits - 1, - op.num_qudits - 2) + gate: MCRYGate | MCRZGate | RYGate | RZGate = MCRZGate( + op.num_qudits - 1, + op.num_qudits - 2, + ) if (op.num_qudits > 2): if isinstance(op.gate, MCRYGate): gate = MCRYGate(op.num_qudits - 1, op.num_qudits - 2) @@ -139,13 +154,13 @@ def decompose(op: Operation) -> Circuit: else: gate = RZGate() - - left_params, right_params = MCRYGate.get_decomposition(op.params) + left_params, right_params = MCRYGate.get_decomposition(op.params) # Construct Circuit - circ = Circuit(gate.num_qudits) - new_gate_location = list(range(1, gate.num_qudits)) - cx_location = (0, gate.num_qudits - 1) + circ = Circuit(op.gate.num_qudits) + new_gate_location = list(range(1, op.gate.num_qudits)) + cx_location = (0, op.gate.num_qudits - 1) + # print(type(gate), gate.num_qudits, new_gate_location) circ.append_gate(gate, new_gate_location, left_params) circ.append_gate(CNOTGate(), cx_location) circ.append_gate(gate, new_gate_location, right_params) @@ -171,14 +186,18 @@ async def run(self, circuit: Circuit, data: PassData) -> None: if len(gates) > 0: # Do a bulk QSDs -> circs - circs = [MGDPass.decompose(gate) for gate in gates] + circs = [MGDPass.decompose(gate) for gate in gates] circ_gates = [CircuitGate(x) for x in circs] - circ_ops = [Operation(x, locations[i], x._circuit.params) for i,x in enumerate(circ_gates)] + circ_ops = [ + Operation(x, locations[i], x._circuit.params) + for i, x in enumerate(circ_gates) + ] circuit.batch_replace(pts, circ_ops) circuit.unfold_all() circuit.unfold_all() + def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: top_qubits = num_qudits - end_qubits now_bottom_qubits = list(reversed(range(top_qubits))) @@ -186,6 +205,7 @@ def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: final_qudits = now_top_qubits + now_bottom_qubits return PermutationMatrix.from_qubit_location(num_qudits, final_qudits) + def shift_up_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: bottom_qubits = list(range(end_qubits)) top_qubits = list(reversed(range(end_qubits, num_qudits))) @@ -193,13 +213,13 @@ def shift_up_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: return PermutationMatrix.from_qubit_location(num_qudits, final_qudits) - class QSDPass(BasePass): """ A pass performing one round of decomposition from the QSD algorithm. - This decomposition takes each unitary of size n and decomposes it into a circuit - with 4 VariableUnitaryGates of size n - 1 and 3 multiplexed rotation gates. + This decomposition takes each unitary of size n and decomposes it + into a circuit with 4 VariableUnitaryGates of size n - 1 and 3 multiplexed + rotation gates. Important: This pass runs on VariableUnitaryGates only. @@ -208,54 +228,61 @@ class QSDPass(BasePass): """ def __init__( - self, - min_qudit_size: int = 4, - ) -> None: - """ - Construct a single level of the QSDPass. - Args: - min_qudit_size (int): Performs a decomposition on all gates - with width > min_qudit_size - """ - self.min_qudit_size = min_qudit_size - - @staticmethod - def create_unitary_gate(u: UnitaryMatrix) -> tuple[VariableUnitaryGate, RealVector]: + self, + min_qudit_size: int = 4, + ) -> None: """ - Create a VariableUnitaryGate from a UnitaryMatrix. + Construct a single level of the QSDPass. + + Args: + min_qudit_size (int): Performs a decomposition on all gates + with width > min_qudit_size """ + self.min_qudit_size = min_qudit_size + + @staticmethod + def create_unitary_gate(u: UnitaryMatrix) -> tuple[ + VariableUnitaryGate, + RealVector, + ]: + """Create a VariableUnitaryGate from a UnitaryMatrix.""" gate = VariableUnitaryGate(u.num_qudits) params = np.concatenate((np.real(u).flatten(), np.imag(u).flatten())) return gate, params @staticmethod - def create_multiplexed_circ(us: list[UnitaryMatrix], select_qubits: list[int], controlled_qubit: int) -> Circuit: - ''' - Takes a list of 2 unitaries of size n. Returns a circuit that - decomposes the unitaries into a circuit with 2 unitaries of size n-1 and a + def create_multiplexed_circ( + us: list[UnitaryMatrix], + select_qubits: list[int], + ) -> Circuit: + """ + Takes a list of 2 unitaries of size n. Returns a circuit that decomposes + the unitaries into a circuit with 2 unitaries of size n-1 and a multiplexed controlled gate. Args: us (list[UnitaryMatrix]): The unitaries to decompose select_qubits (list[int]): The qubits to use as select qubits controlled_qubit (int): The qubit to use as the controlled qubit - + Returns: Circuit: The circuit that decomposes the unitaries - + Using this paper: https://arxiv.org/pdf/quant-ph/0406176.pdf. Thm 12 - - ''' + """ u1 = us[0] u2 = us[1] - assert(u1.num_qudits == u2.num_qudits) + assert (u1.num_qudits == u2.num_qudits) all_qubits = list(range(len(select_qubits) + 1)) # Use Schur Decomposition to split Us into V, D, and W matrices - D_2, V = schur(u1._utry @ u2.dagger._utry) - D = np.sqrt(np.diag(np.diag(D_2))) # D^2 will be diagonal since u1u2h is unitary + D_2, V = schur(u1._utry @ u2.dagger._utry) + # D^2 will be diagonal since u1u2h is unitary + D = np.sqrt(np.diag(np.diag(D_2))) # Calculate W @ U1 left_mat = D @ V.conj().T @ u2._utry - left_gate, left_params = QSDPass.create_unitary_gate(UnitaryMatrix(left_mat)) + left_gate, left_params = QSDPass.create_unitary_gate( + UnitaryMatrix(left_mat), + ) # Create Multi Controlled Z Gate z_params: RealVector = np.array(-2 * np.angle(np.diag(D)).flatten()) @@ -267,14 +294,17 @@ def create_multiplexed_circ(us: list[UnitaryMatrix], select_qubits: list[int], c circ = Circuit(u1.num_qudits + 1) circ.append_gate(left_gate, CircuitLocation(select_qubits), left_params) circ.append_gate(z_gate, CircuitLocation(all_qubits), z_params) - circ.append_gate(right_gate, CircuitLocation(select_qubits), right_params) + circ.append_gate( + right_gate, CircuitLocation( + select_qubits, + ), right_params, + ) return circ @staticmethod def mod_unitaries(u: UnitaryMatrix) -> UnitaryMatrix: - ''' - Apply a permutation transform to the unitaries to the rest of the circuit. - ''' + """Apply a permutation transform to the unitaries to the rest of the + circuit.""" shift_up = shift_up_unitary(u.num_qudits, u.num_qudits - 1) shift_down = shift_down_unitary(u.num_qudits, u.num_qudits - 1) return shift_up @ u @ shift_down @@ -285,7 +315,7 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: Perform the Quantum Shannon Decomposition on a unitary matrix. Args: orig_u (UnitaryMatrix): The unitary matrix to decompose - + Returns: Circuit: The circuit that decomposes the unitary ''' @@ -293,21 +323,35 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: # Shift the unitary qubits down by one u = QSDPass.mod_unitaries(orig_u) - # Perform CS Decomposition to solve for multiplexed unitaries and theta_y - (u1, u2), theta_y, (v1h, v2h) = cossin(u._utry, p=u.shape[0]/2, q=u.shape[1]/2, separate=True) - assert(len(theta_y) == u.shape[0] / 2) + # Perform CS Decomp to solve for multiplexed unitaries and theta_y + (u1, u2), theta_y, (v1h, v2h) = cossin( + u._utry, p=u.shape[0] / 2, q=u.shape[1] / 2, separate=True, + ) + assert (len(theta_y) == u.shape[0] / 2) # Create the multiplexed circuit - # This generates 2 circuits that multipex U and V with an MCRY gate in between + # This generates 2 circuits that multipex U,V with an MCRY gate controlled_qubit = u.num_qudits - 1 select_qubits = list(range(0, u.num_qudits - 1)) all_qubits = list(range(u.num_qudits)) - circ_1 = QSDPass.create_multiplexed_circ([UnitaryMatrix(v1h), UnitaryMatrix(v2h)], select_qubits, controlled_qubit) - circ_2 = QSDPass.create_multiplexed_circ([UnitaryMatrix(u1), UnitaryMatrix(u2)], select_qubits, controlled_qubit) + circ_1 = QSDPass.create_multiplexed_circ( + [ + UnitaryMatrix(v1h), UnitaryMatrix(v2h), + ], + select_qubits, + ) + circ_2 = QSDPass.create_multiplexed_circ( + [ + UnitaryMatrix(u1), UnitaryMatrix(u2), + ], + select_qubits, + ) gate_2 = MCRYGate(u.num_qudits, controlled_qubit) circ_1.append_gate(gate_2, CircuitLocation(all_qubits), 2 * theta_y) - circ_1.append_circuit(circ_2, CircuitLocation(list(range(u.num_qudits)))) + circ_1.append_circuit( + circ_2, CircuitLocation(list(range(u.num_qudits))), + ) return circ_1 async def run(self, circuit: Circuit, data: PassData) -> None: @@ -316,9 +360,14 @@ async def run(self, circuit: Circuit, data: PassData) -> None: locations = [] num_ops = 0 all_ops = list(circuit.operations_with_cycles(reverse=True)) - # Gather all of the unitaries + + initial_utry = circuit.get_unitary() + # Gather all of the VariableUnitary unitaries for cyc, op in all_ops: - if op.num_qudits > self.min_qudit_size and not (isinstance(op.gate, MCRYGate) or isinstance(op.gate, MCRZGate)): + if ( + op.num_qudits > self.min_qudit_size + and isinstance(op.gate, VariableUnitaryGate) + ): num_ops += 1 unitaries.append(op.get_unitary()) pts.append((cyc, op.location[0])) @@ -327,8 +376,15 @@ async def run(self, circuit: Circuit, data: PassData) -> None: if len(unitaries) > 0: circs = await get_runtime().map(QSDPass.qsd, unitaries) circ_gates = [CircuitGate(x) for x in circs] - circ_ops = [Operation(x, locations[i], x._circuit.params) for i,x in enumerate(circ_gates)] + circ_ops = [ + Operation(x, locations[i], x._circuit.params) + for i, x in enumerate(circ_gates) + ] circuit.batch_replace(pts, circ_ops) circuit.unfold_all() - circuit.unfold_all() \ No newline at end of file + dist = circuit.get_unitary().get_distance_from(initial_utry) + + assert dist < 1e-5 + + circuit.unfold_all() diff --git a/tests/passes/synthesis/test_qsd.py b/tests/passes/synthesis/test_qsd.py new file mode 100644 index 000000000..8d166a187 --- /dev/null +++ b/tests/passes/synthesis/test_qsd.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import numpy as np + +from bqskit.compiler import Compiler +from bqskit.ir.circuit import Circuit +from bqskit.ir.gates.parameterized import VariableUnitaryGate +from bqskit.passes import FullQSDPass +from bqskit.qis import UnitaryMatrix + + +def create_random_unitary_circ(num_qudits: int): + ''' + Create a Circuit with a random VariableUnitaryGate. + ''' + circuit = Circuit(num_qudits) + utry = UnitaryMatrix.random(num_qudits) + utry_params = np.concatenate((np.real(utry._utry).flatten(), + np.imag(utry._utry).flatten())) + circuit.append_gate(VariableUnitaryGate(num_qudits), + list(range(num_qudits)), + utry_params) + return circuit + +class TestQSD: + def test_three_qubit_qsd(self, compiler: Compiler) -> None: + circuit = create_random_unitary_circ(3) + utry = circuit.get_unitary() + # Run one pass of QSD + qsd = FullQSDPass(min_qudit_size=2, perform_scan=False) + circuit = compiler.compile(circuit, [qsd]) + dist = circuit.get_unitary().get_distance_from(utry) + assert circuit.count(VariableUnitaryGate(2)) == 4 + assert circuit.count(VariableUnitaryGate(3)) == 0 + assert dist <= 1e-5 + + def test_four_qubit_qubit(self, compiler: Compiler) -> None: + circuit = create_random_unitary_circ(4) + utry = circuit.get_unitary() + # Run two passes of QSD + qsd = FullQSDPass(min_qudit_size=2, perform_scan=False) + circuit = compiler.compile(circuit, [qsd]) + dist = circuit.get_unitary().get_distance_from(utry) + assert circuit.count(VariableUnitaryGate(2)) == 16 + assert circuit.count(VariableUnitaryGate(3)) == 0 + assert circuit.count(VariableUnitaryGate(4)) == 0 + assert dist <= 1e-5 + + def test_five_qubit_qsd(self, compiler: Compiler) -> None: + circuit = create_random_unitary_circ(5) + utry = circuit.get_unitary() + # Run two passes of QSD + qsd = FullQSDPass(min_qudit_size=3, perform_scan=False) + circuit = compiler.compile(circuit, [qsd]) + dist = circuit.get_unitary().get_distance_from(utry) + assert circuit.count(VariableUnitaryGate(3)) == 16 + assert circuit.count(VariableUnitaryGate(4)) == 0 + assert circuit.count(VariableUnitaryGate(5)) == 0 + assert dist <= 1e-5 \ No newline at end of file From 58a0c89a17a844488e8bbddc8c3b8d5535e6930e Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:14:49 -0700 Subject: [PATCH 10/16] tox --- tests/passes/synthesis/test_qsd.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/passes/synthesis/test_qsd.py b/tests/passes/synthesis/test_qsd.py index 8d166a187..c3d615a0f 100644 --- a/tests/passes/synthesis/test_qsd.py +++ b/tests/passes/synthesis/test_qsd.py @@ -10,18 +10,21 @@ def create_random_unitary_circ(num_qudits: int): - ''' - Create a Circuit with a random VariableUnitaryGate. - ''' + """Create a Circuit with a random VariableUnitaryGate.""" circuit = Circuit(num_qudits) utry = UnitaryMatrix.random(num_qudits) - utry_params = np.concatenate((np.real(utry._utry).flatten(), - np.imag(utry._utry).flatten())) - circuit.append_gate(VariableUnitaryGate(num_qudits), - list(range(num_qudits)), - utry_params) + utry_params = np.concatenate(( + np.real(utry._utry).flatten(), + np.imag(utry._utry).flatten(), + )) + circuit.append_gate( + VariableUnitaryGate(num_qudits), + list(range(num_qudits)), + utry_params, + ) return circuit + class TestQSD: def test_three_qubit_qsd(self, compiler: Compiler) -> None: circuit = create_random_unitary_circ(3) @@ -56,4 +59,4 @@ def test_five_qubit_qsd(self, compiler: Compiler) -> None: assert circuit.count(VariableUnitaryGate(3)) == 16 assert circuit.count(VariableUnitaryGate(4)) == 0 assert circuit.count(VariableUnitaryGate(5)) == 0 - assert dist <= 1e-5 \ No newline at end of file + assert dist <= 1e-5 From fbe455016c6c9dc7e8e94d7558a1ad823714e04e Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:15:36 -0700 Subject: [PATCH 11/16] tox --- tests/passes/synthesis/test_qsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/passes/synthesis/test_qsd.py b/tests/passes/synthesis/test_qsd.py index c3d615a0f..6e5002693 100644 --- a/tests/passes/synthesis/test_qsd.py +++ b/tests/passes/synthesis/test_qsd.py @@ -9,7 +9,7 @@ from bqskit.qis import UnitaryMatrix -def create_random_unitary_circ(num_qudits: int): +def create_random_unitary_circ(num_qudits: int) -> Circuit: """Create a Circuit with a random VariableUnitaryGate.""" circuit = Circuit(num_qudits) utry = UnitaryMatrix.random(num_qudits) From ffe61c30e759763007e66a4f939c8b9f3b23227e Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:23:13 -0700 Subject: [PATCH 12/16] Adding a Diagonal Gate to bqskit to help with ZXZ decomposition. --- bqskit/ir/gates/parameterized/diagonal.py | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 bqskit/ir/gates/parameterized/diagonal.py diff --git a/bqskit/ir/gates/parameterized/diagonal.py b/bqskit/ir/gates/parameterized/diagonal.py new file mode 100644 index 000000000..a9e8bf34f --- /dev/null +++ b/bqskit/ir/gates/parameterized/diagonal.py @@ -0,0 +1,81 @@ +"""This module implements a general Diagonal Gate.""" +from __future__ import annotations + +import numpy as np +import numpy.typing as npt + +from bqskit.ir.gates.qubitgate import QubitGate +from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary +from bqskit.qis.unitary.unitary import RealVector +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.utils.cachedclass import CachedClass + + +class DiagonalGate( + QubitGate, + CachedClass, + LocallyOptimizableUnitary +): + """ + A gate representing a general diagonal unitary. + The top-left element is fixed to 1, and the rest are set to exp(i * theta). + + This gate is used to optimize the Block ZXZ decomposition of a unitary. + """ + _qasm_name = 'diag' + + def __init__(self, + num_qudits: int = 2): + self._num_qudits = num_qudits + # 1 parameter per diagonal element, removing one for global phase + self._num_params = 2 ** num_qudits - 1 + + + def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: + """Return the unitary for this gate, see :class:`Unitary` for more.""" + self.check_parameters(params) + + mat = np.eye(2 ** self.num_qudits, dtype=np.complex128) + + for i in range(1, 2 ** self.num_qudits): + mat[i][i] = np.exp(1j * params[i - 1]) + + return UnitaryMatrix(mat) + + def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: + """ + Return the gradient for this gate. + See :class:`DifferentiableUnitary` for more info. + """ + self.check_parameters(params) + + mat = np.eye(2 ** self.num_qudits, dtype=np.complex128) + + for i in range(1, 2 ** self.num_qudits): + mat[i][i] = 1j * np.exp(1j * params[i - 1]) + + return np.array( + [ + mat + ], dtype=np.complex128, + ) + + + def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: + """ + Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. + """ + self.check_env_matrix(env_matrix) + thetas = [0.0] * self.num_params + + base = env_matrix[0, 0] + if base == 0: + base = np.max(env_matrix[0, :]) + + for i in range(1, 2 ** self.num_qudits): + # Optimize each angle independently + a = np.angle(env_matrix[i, i] / base) + thetas[i - 1] = -1 *a + + return thetas \ No newline at end of file From 6d01be05360aea18a842317fffe42a3e813b803f Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:24:55 -0700 Subject: [PATCH 13/16] Adding init file --- bqskit/ir/gates/parameterized/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bqskit/ir/gates/parameterized/__init__.py b/bqskit/ir/gates/parameterized/__init__.py index a592b66ef..00d9af819 100644 --- a/bqskit/ir/gates/parameterized/__init__.py +++ b/bqskit/ir/gates/parameterized/__init__.py @@ -10,6 +10,7 @@ from bqskit.ir.gates.parameterized.cry import CRYGate from bqskit.ir.gates.parameterized.crz import CRZGate from bqskit.ir.gates.parameterized.cu import CUGate +from bqskit.ir.gates.parameterized.diagonal import DiagonalGate from bqskit.ir.gates.parameterized.fsim import FSIMGate from bqskit.ir.gates.parameterized.mcry import MCRYGate from bqskit.ir.gates.parameterized.mcrz import MCRZGate @@ -42,6 +43,7 @@ 'CRYGate', 'CRZGate', 'CUGate', + 'DiagonalGate', 'FSIMGate', 'MCRYGate', 'MCRZGate', From ae8bfce0e718f5d6e2f2af26b3013d6275e6df29 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 17:45:34 -0700 Subject: [PATCH 14/16] tox --- bqskit/ir/gates/parameterized/diagonal.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bqskit/ir/gates/parameterized/diagonal.py b/bqskit/ir/gates/parameterized/diagonal.py index a9e8bf34f..63bb40b9f 100644 --- a/bqskit/ir/gates/parameterized/diagonal.py +++ b/bqskit/ir/gates/parameterized/diagonal.py @@ -14,23 +14,24 @@ class DiagonalGate( QubitGate, CachedClass, - LocallyOptimizableUnitary + LocallyOptimizableUnitary, ): """ - A gate representing a general diagonal unitary. - The top-left element is fixed to 1, and the rest are set to exp(i * theta). + A gate representing a general diagonal unitary. The top-left element is + fixed to 1, and the rest are set to exp(i * theta). This gate is used to optimize the Block ZXZ decomposition of a unitary. """ _qasm_name = 'diag' - def __init__(self, - num_qudits: int = 2): + def __init__( + self, + num_qudits: int = 2, + ): self._num_qudits = num_qudits # 1 parameter per diagonal element, removing one for global phase self._num_params = 2 ** num_qudits - 1 - def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: """Return the unitary for this gate, see :class:`Unitary` for more.""" self.check_parameters(params) @@ -45,6 +46,7 @@ def get_unitary(self, params: RealVector = []) -> UnitaryMatrix: def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: """ Return the gradient for this gate. + See :class:`DifferentiableUnitary` for more info. """ self.check_parameters(params) @@ -56,14 +58,14 @@ def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: return np.array( [ - mat + mat, ], dtype=np.complex128, ) - def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: """ Return the optimal parameters with respect to an environment matrix. + See :class:`LocallyOptimizableUnitary` for more info. """ self.check_env_matrix(env_matrix) @@ -76,6 +78,6 @@ def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: for i in range(1, 2 ** self.num_qudits): # Optimize each angle independently a = np.angle(env_matrix[i, i] / base) - thetas[i - 1] = -1 *a + thetas[i - 1] = -1 * a - return thetas \ No newline at end of file + return thetas From 4ff99ffead035b0fc7f22223dd85ad95d2ad716d Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 14:13:19 -0700 Subject: [PATCH 15/16] Renaming from MCRX to MPRX --- bqskit/ir/gates/parameterized/__init__.py | 8 +++---- .../gates/parameterized/{mcry.py => mpry.py} | 8 +++---- .../gates/parameterized/{mcrz.py => mprz.py} | 10 ++++----- .../{test_mcry.py => test_mpry.py} | 22 +++++++++---------- .../{test_mcrz.py => test_mprz.py} | 22 +++++++++---------- 5 files changed, 35 insertions(+), 35 deletions(-) rename bqskit/ir/gates/parameterized/{mcry.py => mpry.py} (97%) rename bqskit/ir/gates/parameterized/{mcrz.py => mprz.py} (95%) rename tests/ir/gates/parameterized/{test_mcry.py => test_mpry.py} (73%) rename tests/ir/gates/parameterized/{test_mcrz.py => test_mprz.py} (73%) diff --git a/bqskit/ir/gates/parameterized/__init__.py b/bqskit/ir/gates/parameterized/__init__.py index a592b66ef..34b51aee5 100644 --- a/bqskit/ir/gates/parameterized/__init__.py +++ b/bqskit/ir/gates/parameterized/__init__.py @@ -11,8 +11,8 @@ from bqskit.ir.gates.parameterized.crz import CRZGate from bqskit.ir.gates.parameterized.cu import CUGate from bqskit.ir.gates.parameterized.fsim import FSIMGate -from bqskit.ir.gates.parameterized.mcry import MCRYGate -from bqskit.ir.gates.parameterized.mcrz import MCRZGate +from bqskit.ir.gates.parameterized.mpry import MPRYGate +from bqskit.ir.gates.parameterized.mprz import MPRZGate from bqskit.ir.gates.parameterized.pauli import PauliGate from bqskit.ir.gates.parameterized.pauliz import PauliZGate from bqskit.ir.gates.parameterized.phasedxz import PhasedXZGate @@ -43,8 +43,8 @@ 'CRZGate', 'CUGate', 'FSIMGate', - 'MCRYGate', - 'MCRZGate', + 'MPRYGate', + 'MPRZGate', 'PauliGate', 'PauliZGate', 'PhasedXZGate', diff --git a/bqskit/ir/gates/parameterized/mcry.py b/bqskit/ir/gates/parameterized/mpry.py similarity index 97% rename from bqskit/ir/gates/parameterized/mcry.py rename to bqskit/ir/gates/parameterized/mpry.py index 4246e3968..373a90fa7 100644 --- a/bqskit/ir/gates/parameterized/mcry.py +++ b/bqskit/ir/gates/parameterized/mpry.py @@ -1,4 +1,4 @@ -"""This module implements the MCRYGate.""" +"""This module implements the MPRYGate.""" from __future__ import annotations import numpy as np @@ -32,7 +32,7 @@ def get_indices( return left + right, left + shift + right -class MCRYGate( +class MPRYGate( QubitGate, DifferentiableUnitary, CachedClass, @@ -52,7 +52,7 @@ class MCRYGate( See this paper: https://arxiv.org/pdf/quant-ph/0406176 """ - _qasm_name = 'mcry' + _qasm_name = 'mpry' def __init__( self, @@ -156,7 +156,7 @@ def get_decomposition(params: RealVector = []) -> tuple[ Get the corresponding parameters for one level of decomposition of a multiplexed gate. - This is used in the decomposition of both the MCRY and MCRZ gates. + This is used in the decomposition of both the MPRY and MPRZ gates. """ new_num_params = len(params) // 2 left_params = np.zeros(new_num_params) diff --git a/bqskit/ir/gates/parameterized/mcrz.py b/bqskit/ir/gates/parameterized/mprz.py similarity index 95% rename from bqskit/ir/gates/parameterized/mcrz.py rename to bqskit/ir/gates/parameterized/mprz.py index 02439d012..2dc86ecd1 100644 --- a/bqskit/ir/gates/parameterized/mcrz.py +++ b/bqskit/ir/gates/parameterized/mprz.py @@ -1,10 +1,10 @@ -"""This module implements the MCRZGate.""" +"""This module implements the MPRZGate.""" from __future__ import annotations import numpy as np import numpy.typing as npt -from bqskit.ir.gates.parameterized.mcry import get_indices +from bqskit.ir.gates.parameterized.mpry import get_indices from bqskit.ir.gates.qubitgate import QubitGate from bqskit.qis.unitary.differentiable import DifferentiableUnitary from bqskit.qis.unitary.optimizable import LocallyOptimizableUnitary @@ -13,7 +13,7 @@ from bqskit.utils.cachedclass import CachedClass -class MCRZGate( +class MPRZGate( QubitGate, DifferentiableUnitary, CachedClass, @@ -38,7 +38,7 @@ class MCRZGate( See this paper: https://arxiv.org/pdf/quant-ph/0406176 """ - _qasm_name = 'mcrz' + _qasm_name = 'mprz' def __init__( self, @@ -46,7 +46,7 @@ def __init__( target_qubit: int = -1, ) -> None: """ - Create a new MCRZGate with `num_qudits` qubits and `target_qubit` as the + Create a new MPRZGate with `num_qudits` qubits and `target_qubit` as the target qubit. We then have 2^(n-1) parameters for this gate. For Example: diff --git a/tests/ir/gates/parameterized/test_mcry.py b/tests/ir/gates/parameterized/test_mpry.py similarity index 73% rename from tests/ir/gates/parameterized/test_mcry.py rename to tests/ir/gates/parameterized/test_mpry.py index bf7887b29..20d0d4ce0 100644 --- a/tests/ir/gates/parameterized/test_mcry.py +++ b/tests/ir/gates/parameterized/test_mpry.py @@ -1,4 +1,4 @@ -"""This module tests the U1Gate class.""" +"""This module tests the MPRYGate class.""" from __future__ import annotations import numpy as np @@ -9,7 +9,7 @@ from scipy.linalg import block_diag from bqskit.ir.gates.constant import PermutationGate -from bqskit.ir.gates.parameterized import MCRYGate +from bqskit.ir.gates.parameterized import MPRYGate from bqskit.ir.gates.parameterized import RYGate @@ -24,7 +24,7 @@ ) def test_get_unitary(thetas: list[float]) -> None: """ - Test the get_unitary method of the MCRYGate class. + Test the get_unitary method of the MPRYGate class. Use the default target qubit. """ @@ -32,20 +32,20 @@ def test_get_unitary(thetas: list[float]) -> None: # There are 2 ** (n - 1) parameters num_qudits = int(np.log2(len(thetas))) + 1 thetas = thetas[:2 ** (num_qudits - 1)] - mcry = MCRYGate(num_qudits=num_qudits) + MPRy = MPRYGate(num_qudits=num_qudits) block_unitaries = [RYGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + dist = MPRy.get_unitary(thetas).get_distance_from(blocked_unitary) assert dist < 1e-7 @given(integers(min_value=0, max_value=4)) def test_get_unitary_target_select(target_qubit: int) -> None: - """Test the get_unitary method of the MCRYGate class when the target qubit + """Test the get_unitary method of the MPRYGate class when the target qubit is set.""" - # Create an MCRY gate with 6 qubits and random parameters + # Create an MPRY gate with 6 qubits and random parameters num_qudits = 6 - mcry = MCRYGate(num_qudits=num_qudits, target_qubit=target_qubit) + MPRy = MPRYGate(num_qudits=num_qudits, target_qubit=target_qubit) thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) # Create the block diagonal matrix @@ -63,8 +63,8 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary().conj( - ).T @ blocked_unitary @ perm_gate.get_unitary() + full_utry = (perm_gate.get_unitary().conj().T + @ blocked_unitary @ perm_gate.get_unitary()) - dist = mcry.get_unitary(thetas).get_distance_from(full_utry) + dist = MPRy.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7 diff --git a/tests/ir/gates/parameterized/test_mcrz.py b/tests/ir/gates/parameterized/test_mprz.py similarity index 73% rename from tests/ir/gates/parameterized/test_mcrz.py rename to tests/ir/gates/parameterized/test_mprz.py index 392ae7a23..5c7cc9f9a 100644 --- a/tests/ir/gates/parameterized/test_mcrz.py +++ b/tests/ir/gates/parameterized/test_mprz.py @@ -1,4 +1,4 @@ -"""This module tests the U1Gate class.""" +"""This module tests the MPRZGate class.""" from __future__ import annotations import numpy as np @@ -9,7 +9,7 @@ from scipy.linalg import block_diag from bqskit.ir.gates.constant import PermutationGate -from bqskit.ir.gates.parameterized import MCRZGate +from bqskit.ir.gates.parameterized import MPRZGate from bqskit.ir.gates.parameterized import RZGate @@ -24,7 +24,7 @@ ) def test_get_unitary(thetas: list[float]) -> None: """ - Test the get_unitary method of the MCRZGate class. + Test the get_unitary method of the MPRZGate class. Use the default target qubit. """ @@ -32,20 +32,20 @@ def test_get_unitary(thetas: list[float]) -> None: # There are 2 ** (n - 1) parameters num_qudits = int(np.log2(len(thetas))) + 1 thetas = thetas[:2 ** (num_qudits - 1)] - mcry = MCRZGate(num_qudits=num_qudits) + MPRy = MPRZGate(num_qudits=num_qudits) block_unitaries = [RZGate().get_unitary([theta]) for theta in thetas] blocked_unitary = block_diag(*block_unitaries) - dist = mcry.get_unitary(thetas).get_distance_from(blocked_unitary) + dist = MPRy.get_unitary(thetas).get_distance_from(blocked_unitary) assert dist < 1e-7 @given(integers(min_value=0, max_value=4)) def test_get_unitary_target_select(target_qubit: int) -> None: - """Test the get_unitary method of the MCRZGate class when the target qubit + """Test the get_unitary method of the MPRZGate class when the target qubit is set.""" - # Create an MCRZ gate with 6 qubits and random parameters + # Create an MPRZ gate with 6 qubits and random parameters num_qudits = 6 - mcry = MCRZGate(num_qudits=num_qudits, target_qubit=target_qubit) + MPRy = MPRZGate(num_qudits=num_qudits, target_qubit=target_qubit) thetas = list(np.random.rand(2 ** (num_qudits - 1)) * 2 * np.pi) # Create the block diagonal matrix @@ -63,8 +63,8 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm_gate = PermutationGate(num_qudits, perm) - full_utry = perm_gate.get_unitary().conj( - ).T @ blocked_unitary @ perm_gate.get_unitary() + full_utry = (perm_gate.get_unitary().conj().T + @ blocked_unitary @ perm_gate.get_unitary()) - dist = mcry.get_unitary(thetas).get_distance_from(full_utry) + dist = MPRy.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7 From 44cc84f8dce1247743f93fa31789b71702118f2a Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 14:13:39 -0700 Subject: [PATCH 16/16] tox --- tests/ir/gates/parameterized/test_mpry.py | 6 ++++-- tests/ir/gates/parameterized/test_mprz.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/ir/gates/parameterized/test_mpry.py b/tests/ir/gates/parameterized/test_mpry.py index 20d0d4ce0..f16e6827b 100644 --- a/tests/ir/gates/parameterized/test_mpry.py +++ b/tests/ir/gates/parameterized/test_mpry.py @@ -63,8 +63,10 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm_gate = PermutationGate(num_qudits, perm) - full_utry = (perm_gate.get_unitary().conj().T - @ blocked_unitary @ perm_gate.get_unitary()) + full_utry = ( + perm_gate.get_unitary().conj().T + @ blocked_unitary @ perm_gate.get_unitary() + ) dist = MPRy.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7 diff --git a/tests/ir/gates/parameterized/test_mprz.py b/tests/ir/gates/parameterized/test_mprz.py index 5c7cc9f9a..b19c5d925 100644 --- a/tests/ir/gates/parameterized/test_mprz.py +++ b/tests/ir/gates/parameterized/test_mprz.py @@ -63,8 +63,10 @@ def test_get_unitary_target_select(target_qubit: int) -> None: perm_gate = PermutationGate(num_qudits, perm) - full_utry = (perm_gate.get_unitary().conj().T - @ blocked_unitary @ perm_gate.get_unitary()) + full_utry = ( + perm_gate.get_unitary().conj().T + @ blocked_unitary @ perm_gate.get_unitary() + ) dist = MPRy.get_unitary(thetas).get_distance_from(full_utry) assert dist < 1e-7