From 4e2a2172357cbab316d1205ab503f625b74e138a Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Mon, 23 Sep 2024 18:06:34 -0700 Subject: [PATCH 01/15] 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 b05f5324..3847f3ae 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 eea09723..bf34c14e 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 00000000..1eb04ff6 --- /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 491935b2bfffece6561eef6255b1bf3423e5b6fd Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 12:08:31 -0700 Subject: [PATCH 02/15] 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 12376e59..9093a46c 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 1eb04ff6..6c29dd4b 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 00000000..8d166a18 --- /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 03/15] 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 8d166a18..c3d615a0 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 04/15] 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 c3d615a0..6e500269 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 05/15] 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 00000000..a9e8bf34 --- /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 06/15] 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 a592b66e..00d9af81 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 45257e4092dfdff590a0333a74b65059588a094b Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 17:42:12 -0700 Subject: [PATCH 07/15] Adding Block ZXZ pass, including Extract Diagonal Pass --- bqskit/passes/processing/extract_diagonal.py | 156 +++++++ bqskit/passes/synthesis/__init__.py | 6 +- bqskit/passes/synthesis/bzxz.py | 454 +++++++++++++++++++ bqskit/passes/synthesis/qsd.py | 76 ++-- 4 files changed, 663 insertions(+), 29 deletions(-) create mode 100644 bqskit/passes/processing/extract_diagonal.py create mode 100644 bqskit/passes/synthesis/bzxz.py diff --git a/bqskit/passes/processing/extract_diagonal.py b/bqskit/passes/processing/extract_diagonal.py new file mode 100644 index 00000000..96178566 --- /dev/null +++ b/bqskit/passes/processing/extract_diagonal.py @@ -0,0 +1,156 @@ +"""This module implements the ExtractDiagonalPass.""" +from __future__ import annotations + +from bqskit.compiler.passdata import PassData +from bqskit.compiler.basepass import BasePass +from bqskit.ir.operation import Operation +from bqskit.ir.circuit import Circuit +from bqskit.ir.gates import VariableUnitaryGate, DiagonalGate +from bqskit.ir.gates.constant import CNOTGate +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator +from bqskit.ir.opt.cost.generator import CostFunctionGenerator +from typing import Any + + +theorized_bounds = [0, 0, 3, 14, 61, 252] +def construct_linear_ansatz(num_qudits: int): + theorized_num = theorized_bounds[num_qudits] + circuit = Circuit(num_qudits) + circuit.append_gate(DiagonalGate(num_qudits), tuple(range(num_qudits))) + for i in range(num_qudits): + circuit.append_gate(VariableUnitaryGate(1), (i,)) + for _ in range(theorized_num // (num_qudits - 1)): + # Apply n - 1 linear CNOTs + for i in range(num_qudits - 1): + circuit.append_gate(CNOTGate(), (i, i+1)) + circuit.append_gate(VariableUnitaryGate(1), (i,)) + circuit.append_gate(VariableUnitaryGate(1), (i+1,)) + return circuit + + +class ExtractDiagonalPass(BasePass): + """ + A pass that attempts to extract a diagonal matrix from a unitary matrix. + + https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1269020 + + While there is a known algorithm for 2-qubit gates, we utilize + synthesis methods instead to scale to wider qubit gates. + + As a heuristic, we attempt to extrac the diagonal using a linear chain + ansatz of CNOT gates. We have found that up to 5 qubits, this ansatz + does succeed for most unitaries with fewer CNOTs than the theoretical + minimum number of CNOTs (utilizing the power of the Diagonal Gate in front). + """ + + def __init__( + self, + success_threshold: float = 1e-8, + cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), + instantiate_options: dict[str, Any] = {}, + ) -> None: + self.success_threshold = success_threshold + self.cost = cost + self.instantiate_options: dict[str, Any] = { + 'cost_fn_gen': self.cost, + 'min_iters': 0, + 'diff_tol_r':1e-4, + 'multistarts': 16, + 'method': 'qfactor' + } + self.instantiate_options.update(instantiate_options) + super().__init__() + + + async def decompose( + self, + op: Operation, + cost:CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), + target: UnitaryMatrix = None, + success_threshold: float = 1e-14, + instantiate_options: dict[str, Any] = {}) -> tuple[Operation | None, + Circuit]: + ''' + Return the circuit that is generated from one levl of QSD. + ''' + + circ = Circuit(op.gate.num_qudits) + + if op.gate.num_qudits == 2: + # For now just try for 2 qubit + circ.append_gate(DiagonalGate(op.gate.num_qudits), (0,1)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (0,)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (1,)) + circ.append_gate(CNOTGate(), (0, 1)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (0,)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (1,)) + circ.append_gate(CNOTGate(), (0, 1)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (0,)) + circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (1,)) + elif op.gate.num_qudits == 3: + circ = construct_linear_ansatz(op.gate.num_qudits) + else: + circ = construct_linear_ansatz(op.gate.num_qudits) + + instantiated_circ = circ.instantiate(target=target, + **instantiate_options) + + + if cost.calc_cost(instantiated_circ, target) < success_threshold: + print("Success") + diag_op = instantiated_circ.pop((0,0)) + return diag_op, instantiated_circ + + default_circ = Circuit(op.gate.num_qudits) + default_circ.append_gate(op.gate, + tuple(range(op.gate.num_qudits)), op.params) + return None, default_circ + + + async def run(self, circuit: Circuit, data: PassData) -> None: + """Synthesize `utry`, see :class:`SynthesisPass` for more.""" + num_ops = 0 + print(circuit.gate_counts) + + j = circuit.count(VariableUnitaryGate(4)) + + while j > 1: + # Find last Unitary + all_ops = list(circuit.operations_with_cycles(reverse=True)) + found = False + for cyc, op in all_ops: + if isinstance(op.gate, VariableUnitaryGate) and op.gate.num_qudits in [2,3,4]: + print("Replacing op at cyc", cyc) + if found: + merge_op = op + merge_pt = (cyc, op.location[0]) + merge_location = op.location + break + else: + num_ops += 1 + gate = op + pt = (cyc, op.location[0]) + found = True + # print(self.cost.calc_cost(circuit, data.target)) + # print(f"Decomposing {num_ops}th gate", flush=True) + diag_op, circ = await self.decompose(gate, + cost=self.cost, + target=gate.get_unitary(), + success_threshold=self.success_threshold, + instantiate_options=self.instantiate_options) + + print(self.cost.calc_cost(circuit, data.target)) + circuit.replace_with_circuit(pt, circ, as_circuit_gate=True) + print(self.cost.calc_cost(circuit, data.target)) + j -= 1 + # Commute Diagonal into next op + if diag_op: + print(self.cost.calc_cost(circuit, data.target)) + new_mat = diag_op.get_unitary() @ merge_op.get_unitary() + circuit.replace_gate(merge_pt, merge_op.gate, merge_location, merge_op.gate.calc_params(new_mat)) + print(self.cost.calc_cost(circuit, data.target)) + print("Inserted Diagonal") + + print(circuit.gate_counts) + circuit.unfold_all() \ No newline at end of file diff --git a/bqskit/passes/synthesis/__init__.py b/bqskit/passes/synthesis/__init__.py index bf34c14e..33253cbc 100644 --- a/bqskit/passes/synthesis/__init__.py +++ b/bqskit/passes/synthesis/__init__.py @@ -1,12 +1,13 @@ """This package implements synthesis passes and synthesis related classes.""" from __future__ import annotations +from bqskit.passes.synthesis.bzxz import BlockZXZPass from bqskit.passes.synthesis.diagonal import WalshDiagonalSynthesisPass from bqskit.passes.synthesis.leap import LEAPSynthesisPass 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.qsd import FullQSDPass, MGDPass, QSDPass from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass from bqskit.passes.synthesis.synthesis import SynthesisPass from bqskit.passes.synthesis.target import SetTargetPass @@ -21,4 +22,7 @@ 'PermutationAwareSynthesisPass', 'WalshDiagonalSynthesisPass', 'FullQSDPass', + 'MGDPass', + 'QSDPass', + 'BlockZXZPass', ] diff --git a/bqskit/passes/synthesis/bzxz.py b/bqskit/passes/synthesis/bzxz.py new file mode 100644 index 00000000..1496eab2 --- /dev/null +++ b/bqskit/passes/synthesis/bzxz.py @@ -0,0 +1,454 @@ +"""This module implements the Full Block ZXZ Decomposition. Additionally, it +defines the Block ZXZ Decomposition for a single pass. """ +from __future__ import annotations + +import logging +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.processing.scan import ScanningGateRemovalPass +from bqskit.passes.processing.treescan import TreeScanningGateRemovalPass +from bqskit.passes.synthesis.qsd import MGDPass, QSDPass +from bqskit.passes.processing.extract_diagonal import ExtractDiagonalPass +from bqskit.ir.circuit import Circuit + +from bqskit.compiler.basepass import BasePass +from bqskit.ir.operation import Operation +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.ir.circuit import Circuit +from bqskit.ir.gates.parameterized.mcry import MCRYGate +from bqskit.ir.gates.parameterized.mcrz import MCRZGate +from bqskit.ir.gates.constant import IdentityGate, HGate, ZGate +from bqskit.ir.gates.parameterized import RZGate +from bqskit.ir.gates.constant import CNOTGate +from bqskit.ir.location import CircuitLocation, CircuitLocationLike +from bqskit.ir.gates import CircuitGate +from bqskit.runtime import get_runtime +from scipy.linalg import svd, block_diag, eig +import numpy as np + + + +_logger = logging.getLogger(__name__) + +class FullBlockZXZPass(BasePass): + """ + A pass performing a full Block ZXZ decomposition + + 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, + perform_extract: bool = True, + instantiate_options: dict[str, Any] = {}, + ) -> None: + """ + Perform the full Block ZXZ decomposition. + + 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) + perform_extract (bool): Whether or not to perform the diagonal + extraction pass. (Default: True) + 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.perform_scan = perform_scan + self.perform_extract = perform_extract + if tree_depth > 0: + self.scan = TreeScanningGateRemovalPass( + start_from_left=start_from_left, + instantiate_options=instantiation_options, + tree_depth=tree_depth) + else: + self.scan = ScanningGateRemovalPass( + start_from_left=start_from_left, + instantiate_options=instantiation_options) + self.bzxz = BlockZXZPass(min_qudit_size=min_qudit_size) + self.mgd = MGDPass(inverse = True) + self.diag = ExtractDiagonalPass() + + async def run(self, circuit: Circuit, data: PassData) -> None: + ''' + Perform succesive rounds of Block ZXZ decomposition until the + circuit is fully decomposed with no VariableUnitaryGates larger + then `min_qudit_size`. + + At the end, attempt to extrac the diagonal gate and + commute through the circuit to find optimal CNOT counts. + ''' + passes = [] + start_num = max(x.num_qudits for x in circuit.operations()) + + for _ in range(self.min_qudit_size, start_num): + passes.append(self.bzxz) + if self.perform_scan: + passes.append(self.scan) + + if self.perform_extract: + passes.append(self.diag) + + # Once we have commuted the diagonal gate, we can break down the + # multiplexed gates + for _ in range(self.min_qudit_size, start_num): + passes.append(self.mgd) + if self.perform_scan: + passes.append(self.scan) + + await Workflow(passes).run(circuit, data) + +def left_shift(loc: CircuitLocationLike): + return loc[1:] + loc[0:1] + +class BlockZXZPass(BasePass): + """ + A pass performing one round of decomposition from the Block ZXZ algorithm. + + References: + Krol, Anna M., and Zaid Al-Ars. + "Highly Efficient Decomposition of n-Qubit Quantum Gates + Based on Block-ZXZ Decomposition." + arXiv preprint arXiv:2403.13692 (2024). + https://arxiv.org/html/2403.13692v1 + """ + + 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 widht > min_qudit_size + """ + self.min_qudit_size = min_qudit_size + + def initial_decompose(U: UnitaryMatrix) -> tuple[UnitaryMatrix, + UnitaryMatrix, + UnitaryMatrix, + UnitaryMatrix]: + ''' + This function decomposes the a given unitary U into 2 controlled + unitaries (B, and C), 1 multiplexed unitary (A) and 2 Hadamard gates. + + We return the A1 and A2 (sub-unitaries of the multipelexed unitary A), + as well as the sub unitary B and C. + + By sub-unitary, we mean the unitary that will be applied to the bottom + n - 1 qubits given the value of the top qubit 0. For a controlled + unitary, that sub-unitary is either the Identity (if qubit 0 is 0) or a + unitary on n-1 qubits if qubit 0 is 1. + + This follows equations 5-9 in the paper. + ''' + # To solve for A1, A2, and C, we must divide U into 4 submatrices. + # The upper left block is X, the upper right block is Y, the lower left + # block is U_21, and the lower right block is U_22. + + X = U[0:len(U) // 2, 0:len(U) // 2] + Y = U[0:len(U) // 2, len(U) // 2:] + U_21 = U[len(U) // 2:, 0:len(U) // 2] + U_22 = U[len(U) // 2:, len(U) // 2:] + + # We let X = V_X @ s_X @ W_X where V_X and W_X are unitary and s_X is + # a diagonal matrix of singular values. We use the SVD to find these + # matrices. We do the same decomposition for Y. + V_X, s_X, W_XH = svd(X) + V_Y, s_Y, W_YH = svd(Y) + + # We can then define S_X and S_Y as V_X @ s_X @ V_X^H and + # V_Y @ s_Y @ V_Y^H + S_X = V_X @ np.diag(s_X) @ V_X.conj().T + S_Y = V_Y @ np.diag(s_Y) @ V_Y.conj().T + + # We define U_X and U_Y as V_X^H @ U @ W_X and V_Y^H @ U @ W_Y + U_X = V_X @ W_XH + U_Y = V_Y @ W_YH + + # Now, to perform the decomposition as defined in Section 4.1 + # We can set C dagger as i(U_Y^H @ U_X) + CH = 1j * U_Y.conj().T @ U_X + + # We then set A_1 as S_X @ U_X and A_2 as U_21 + U_22 @ (iU_Y^H @ U_X) + A_1 = UnitaryMatrix((S_X + 1j * S_Y) @ U_X) + A_2 = UnitaryMatrix(U_21 + U_22 @ (1j * U_Y.conj().T @ U_X)) + + # Finally, we can set B as 2(A_1^H @ U) - I + I = np.eye(len(U) / 2) + B = UnitaryMatrix((2 * (A_1.conj().T @ X)) - I) + + return A_1, A_2, B, UnitaryMatrix(CH.conj().T) + + @staticmethod + def demultiplex(U_1: UnitaryMatrix, + U_2: UnitaryMatrix) -> tuple[UnitaryMatrix, + np.ndarray, + UnitaryMatrix]: + ''' + Demultiplex U that is block_diag(U_1, U_2) to + 2 Unitary Matrices V and W and a diagonal matrix D. + + The Diagonal Matrix D corresponds to a Multiplexed Rz gate acting on + the most significant qubit. + + We return the Unitary Matrices V and W and the parameters for the + corresponding MCRZ gate. + + ''' + # U can be decomposed into U = (I otimes V )(D otimes D†)(I otimes W ) + + # We can find V,D^2 by performing an eigen decomposition of + # U_1 @ U_2† + d2, V = eig(U_1 @ U_2.conj().T) + d = np.sqrt(d2) + D = np.diag(d) + + # We can then multiply to solve for W + W = D @ V.conj().T @ U_2 + + # We can use d to find the parameters for the MCRZ gate. + # Note than because and Rz does exp(-i * theta / 2), we must + # multiply by -2. + d_params = np.angle(d) * -2 + + return UnitaryMatrix(V), d_params, UnitaryMatrix(W) + + @staticmethod + def decompose_mpz_one_level(params, + num_qudits, + reverse=False, + drop_last_cnot=False) -> Circuit: + ''' + Decompose MPZ one level. + + Args: + params (np.ndarray): The parameters for the original MCRZ gate + num_qudits (int): The number of qudits in the MCRZ gate + reverse (bool): Whether to reverse the order of the gates (you can + decompose the gates in either order to get the same result) + drop_last_cnot (bool): Whether to drop the last CNOT gate (only + should be set to True if you are doing section 5.2 optimization) + + Returns: + Circuit: The circuit that decomposes the MCRZ gate + ''' + if (num_qudits >= 3): + # Remove 1 qubit, last qubit is controlled + new_gate = MCRZGate(num_qudits - 1, num_qudits-2) + else: + new_gate = RZGate() + + left_params, right_params = MCRYGate.get_decomposition(params) + circ = Circuit(num_qudits) + new_gate_location = list(range(1, num_qudits)) + cx_location = (0, num_qudits - 1) + + ops = [Operation(new_gate, new_gate_location, left_params), + Operation(CNOTGate(), cx_location), + Operation(new_gate, new_gate_location, right_params), + Operation(CNOTGate(), cx_location)] + + if drop_last_cnot: + ops.pop() + + if reverse: + ops.reverse() + + for op in ops: + circ.append(op) + + return circ + + @staticmethod + def decompose_mpz_two_levels(params, num_qudits, reverse=False, remove_last_cnot=True) -> Circuit: + ''' + We decompose a multiplexed RZ gate 2 levels deep. This allows you + to remove the last CNOT gate in the context of the Block ZXZ + decomposition. This is shown in section 5.2 of the paper. + + Args: + params (np.ndarray): The parameters for the original MCRZ gate + num_qudits (int): The number of qudits in the MCRZ gate + reverse (bool): Whether to reverse the order of the gates (you can + decompose the gates in either order to get the same result) + drop_last_cnot (bool): Whether to drop the last CNOT gate (only + should be set to True if you are doing section 5.2 optimization) + + Returns: + Circuit: The circuit that decomposes the MCRZ gate + ''' + # Get params for first decomposition of the MCRZ gate + left_params, right_params = MCRYGate.get_decomposition(params) + + + # Decompose the MCRZ gate into 2 MCRZ gates, dropping the last CNOT + # Also Reverse the circuit for the right side in order to do the + # optimization in section 5.2 + circ_left = BlockZXZPass.decompose_mpz_one_level(left_params, + num_qudits - 1, + reverse, + drop_last_cnot=True) + + circ_right = BlockZXZPass.decompose_mpz_one_level(right_params, + num_qudits - 1, + not reverse, + drop_last_cnot=True) + + # Now, construct the circuit. + # This will generate the original MCRZ gate with the target qubit + # set as qubit num_qudits - 1 + circ = Circuit(num_qudits) + cx_location_big = (0, num_qudits - 1) + + ops = [circ_left, + Operation(CNOTGate(), cx_location_big), + circ_right, + Operation(CNOTGate(), cx_location_big)] + + # We can remove the last CNOT gate as per section 5.2 + if remove_last_cnot: + ops.pop() + + if reverse: + ops.reverse() + + for op in ops: + if isinstance(op, Operation): + circ.append(op) + else: + circ.append_circuit(op, list(range(1, num_qudits))) + + return circ + + + @staticmethod + def zxz(orig_u: UnitaryMatrix) -> Circuit: + ''' + Return the circuit that is generated from one levl of + Block ZXZ decomposition. + ''' + + # First calculate the A, B, and C matrices for the initial decomp + A_1, A_2, B, C = BlockZXZPass.initial_decompose(orig_u) + + # Now decompose thee multiplexed A gate and the controlled C gate + I = IdentityGate(orig_u.num_qudits - 1).get_unitary() + VA, AZ_params, WA = BlockZXZPass.demultiplex(A_1, A_2) + VC, CZ_params, WC = BlockZXZPass.demultiplex(I, C) + + # Now calculate optimized B_tilde (Section 5.2) and decompose + # We merge in WA and VC into B and then merge in the additional + # CZ gates from the optimization + small_I = IdentityGate(orig_u.num_qudits - 2).get_unitary() + Z = ZGate(1).get_unitary() + B_tilde_1 = WA @ VC + B_tilde_2 = np.kron(Z, small_I) @ WA @ B @ VC @ np.kron(Z, small_I) + VB, BZ_params, WB = BlockZXZPass.demultiplex(B_tilde_1, B_tilde_2) + + # Define circuit locations + # We let the target qubit be qubit 0 (top qubit in diagram) + # The select qubits are the remaining qubits + controlled_qubit = 0 + select_qubits = list(range(1, orig_u.num_qudits)) + all_qubits = list(range(0, orig_u.num_qudits)) + + # z_gate_circ = BlockZXZPass.decompose_mpz_two_levels(BZ_params, orig_u.num_qudits, False, False) + # assert(np.allclose(z_gate_circ.get_unitary(), z_gate.get_unitary(BZ_params))) + + # Construct Circuit + circ = Circuit(orig_u.num_qudits) + + # Add WC gate + wc_gate, wc_params = QSDPass.create_unitary_gate(WC) + circ.append_gate(wc_gate, CircuitLocation(select_qubits), wc_params) + + + # Add decomposed MCRZ gate circuit. + # Since the MCRZ gate circuit sets the target qubit as qubit + # num_qudits - 1, we must shift the qubits to the left + shifted_qubits = left_shift(all_qubits) + circ.append_circuit( + BlockZXZPass.decompose_mpz_two_levels(CZ_params, orig_u.num_qudits), + shifted_qubits) + + + # Add the decomposed B-tilde gates WB and a Hadamard + combo_1_gate, combo_1_params = QSDPass.create_unitary_gate(WB) + circ.append_gate(combo_1_gate, CircuitLocation(select_qubits), + combo_1_params) + circ.append_gate(HGate(), CircuitLocation((controlled_qubit, ))) + + # The central MCRZ_gate. We set the target to the controlled qubit, + # so there is no need to shift + z_gate = MCRZGate(len(all_qubits), 0) + circ.append_gate(z_gate, CircuitLocation(all_qubits), BZ_params) + + # Now add the decomposed B-tilde gates VB and a Hadamard + combo_2_gate, combo_2_params = QSDPass.create_unitary_gate(VB) + circ.append_gate(combo_2_gate, CircuitLocation(select_qubits), + combo_2_params) + circ.append_gate(HGate(), CircuitLocation((controlled_qubit, ))) + + + # Add the decomposed MCRZ gate circuit again on shifted qubits + circ.append_circuit( + BlockZXZPass.decompose_mpz_two_levels(AZ_params, + orig_u.num_qudits, + True), + shifted_qubits) + + va_gate, va_params = QSDPass.create_unitary_gate(VA) + circ.append_gate(va_gate, CircuitLocation(select_qubits), va_params) + + + # assert np.allclose(orig_u, circ.get_unitary()) + return circ + + async def run(self, circuit: Circuit, data: PassData) -> None: + """Perform a single level of the Block ZXZ decomposition.""" + unitaries, pts, locations = QSDPass.get_variable_unitary_pts( + circuit, self.min_qudit_size + ) + + if len(unitaries) > 0: + circs = await get_runtime().map(BlockZXZPass.zxz, unitaries) + # Do bulk replace + 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 diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py index 6c29dd4b..1df6256f 100644 --- a/bqskit/passes/synthesis/qsd.py +++ b/bqskit/passes/synthesis/qsd.py @@ -12,7 +12,7 @@ from bqskit.compiler.passdata import PassData from bqskit.compiler.workflow import Workflow from bqskit.ir.circuit import Circuit -from bqskit.ir.circuit import CircuitLocation +from bqskit.ir.circuit import CircuitLocation, CircuitPoint from bqskit.ir.gates import CircuitGate from bqskit.ir.gates.constant import CNOTGate from bqskit.ir.gates.parameterized import RYGate @@ -30,7 +30,6 @@ _logger = logging.getLogger(__name__) - class FullQSDPass(BasePass): """ A pass performing one round of decomposition from the QSD algorithm. @@ -197,22 +196,6 @@ async def run(self, circuit: Circuit, data: PassData) -> None: 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. @@ -240,6 +223,28 @@ def __init__( """ self.min_qudit_size = min_qudit_size + @staticmethod + def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: + ''' + Return the Permutation Matrix that shifts the qubits down by 1 qubit. + ''' + 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) + + @staticmethod + def shift_up_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: + ''' + Return the Permutation Matrix that shifts the qubits down by 1 qubit. + ''' + 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) + + @staticmethod def create_unitary_gate(u: UnitaryMatrix) -> tuple[ VariableUnitaryGate, @@ -305,8 +310,8 @@ def create_multiplexed_circ( 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) + shift_up = QSDPass.shift_up_unitary(u.num_qudits, u.num_qudits - 1) + shift_down = QSDPass.shift_down_unitary(u.num_qudits, u.num_qudits - 1) return shift_up @ u @ shift_down @staticmethod @@ -353,19 +358,27 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: circ_2, CircuitLocation(list(range(u.num_qudits))), ) return circ_1 - - async def run(self, circuit: Circuit, data: PassData) -> None: + + @staticmethod + def get_variable_unitary_pts(circuit: Circuit, min_qudit_size: int) -> tuple[ + list[UnitaryMatrix], + list[CircuitPoint], + list[CircuitLocation] + ]: + ''' + Get all VariableUnitary Gates in the circuit wider than + `min_qudit_size` and return their unitaries, points, and locations. + ''' unitaries = [] pts = [] locations = [] num_ops = 0 all_ops = list(circuit.operations_with_cycles(reverse=True)) - 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 + op.num_qudits > min_qudit_size and isinstance(op.gate, VariableUnitaryGate) ): num_ops += 1 @@ -373,6 +386,17 @@ async def run(self, circuit: Circuit, data: PassData) -> None: pts.append((cyc, op.location[0])) locations.append(op.location) + return unitaries, pts, locations + + + async def run(self, circuit: Circuit, data: PassData) -> None: + ''' + Perform a single pass of Quantum Shannon Decomposition on the circuit. + ''' + unitaries, pts, locations = QSDPass.get_variable_unitary_pts( + circuit, self.min_qudit_size + ) + if len(unitaries) > 0: circs = await get_runtime().map(QSDPass.qsd, unitaries) circ_gates = [CircuitGate(x) for x in circs] @@ -383,8 +407,4 @@ async def run(self, circuit: Circuit, data: PassData) -> None: circuit.batch_replace(pts, circ_ops) circuit.unfold_all() - dist = circuit.get_unitary().get_distance_from(initial_utry) - - assert dist < 1e-5 - circuit.unfold_all() From ae8bfce0e718f5d6e2f2af26b3013d6275e6df29 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Thu, 26 Sep 2024 17:45:34 -0700 Subject: [PATCH 08/15] 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 a9e8bf34..63bb40b9 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 8bc99448df427c7b244692ac4f094d678e436fc6 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 11:49:09 -0700 Subject: [PATCH 09/15] Moving decomposition to MGDPass, and adding comments --- bqskit/passes/__init__.py | 8 + bqskit/passes/processing/extract_diagonal.py | 146 +++++--- bqskit/passes/synthesis/__init__.py | 6 +- bqskit/passes/synthesis/bzxz.py | 369 +++++++------------ bqskit/passes/synthesis/qsd.py | 273 ++++++++++---- 5 files changed, 449 insertions(+), 353 deletions(-) diff --git a/bqskit/passes/__init__.py b/bqskit/passes/__init__.py index 3847f3ae..ee0015a8 100644 --- a/bqskit/passes/__init__.py +++ b/bqskit/passes/__init__.py @@ -291,12 +291,16 @@ from bqskit.passes.search.heuristics.astar import AStarHeuristic from bqskit.passes.search.heuristics.dijkstra import DijkstraHeuristic from bqskit.passes.search.heuristics.greedy import GreedyHeuristic +from bqskit.passes.synthesis.bzxz import BlockZXZPass +from bqskit.passes.synthesis.bzxz import FullBlockZXZPass from bqskit.passes.synthesis.diagonal import WalshDiagonalSynthesisPass from bqskit.passes.synthesis.leap import LEAPSynthesisPass 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.qsd import MGDPass +from bqskit.passes.synthesis.qsd import QSDPass from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass from bqskit.passes.synthesis.synthesis import SynthesisPass from bqskit.passes.synthesis.target import SetTargetPass @@ -334,6 +338,10 @@ 'LEAPSynthesisPass', 'QSearchSynthesisPass', 'FullQSDPass', + 'QSDPass', + 'MGDPass', + 'BlockZXZPass', + 'FullBlockZXZPass', 'QFASTDecompositionPass', 'QPredictDecompositionPass', 'CompressPass', diff --git a/bqskit/passes/processing/extract_diagonal.py b/bqskit/passes/processing/extract_diagonal.py index 96178566..898509b1 100644 --- a/bqskit/passes/processing/extract_diagonal.py +++ b/bqskit/passes/processing/extract_diagonal.py @@ -1,20 +1,36 @@ """This module implements the ExtractDiagonalPass.""" from __future__ import annotations -from bqskit.compiler.passdata import PassData +from typing import Any + from bqskit.compiler.basepass import BasePass -from bqskit.ir.operation import Operation +from bqskit.compiler.passdata import PassData from bqskit.ir.circuit import Circuit -from bqskit.ir.gates import VariableUnitaryGate, DiagonalGate +from bqskit.ir.gates import DiagonalGate +from bqskit.ir.gates import VariableUnitaryGate from bqskit.ir.gates.constant import CNOTGate -from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix +from bqskit.ir.operation import Operation from bqskit.ir.opt.cost.functions import HilbertSchmidtResidualsGenerator from bqskit.ir.opt.cost.generator import CostFunctionGenerator -from typing import Any +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix theorized_bounds = [0, 0, 3, 14, 61, 252] -def construct_linear_ansatz(num_qudits: int): + + +def construct_linear_ansatz(num_qudits: int) -> Circuit: + """ + Generate a linear ansatz for extracting the diagonal of a unitary. + + This ansatz consists of a `num_qudits` width Diagonal Gate followed + by a ladder of CNOTs and single qubit gates. Right now, we try to use + one fewer CNOT than the theorized minimum number of CNOTs to represent + the unitary. + + This ansatz is simply a heuristic and does not have theoretical + backing. However, we see that for unitaries up to 5 qubits, this + ansatz does succeed most of the time with a threshold of 1e-8. + """ theorized_num = theorized_bounds[num_qudits] circuit = Circuit(num_qudits) circuit.append_gate(DiagonalGate(num_qudits), tuple(range(num_qudits))) @@ -23,9 +39,9 @@ def construct_linear_ansatz(num_qudits: int): for _ in range(theorized_num // (num_qudits - 1)): # Apply n - 1 linear CNOTs for i in range(num_qudits - 1): - circuit.append_gate(CNOTGate(), (i, i+1)) + circuit.append_gate(CNOTGate(), (i, i + 1)) circuit.append_gate(VariableUnitaryGate(1), (i,)) - circuit.append_gate(VariableUnitaryGate(1), (i+1,)) + circuit.append_gate(VariableUnitaryGate(1), (i + 1,)) return circuit @@ -45,41 +61,56 @@ class ExtractDiagonalPass(BasePass): """ def __init__( - self, - success_threshold: float = 1e-8, - cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), - instantiate_options: dict[str, Any] = {}, - ) -> None: + self, + qudit_size: int = 2, + success_threshold: float = 1e-8, + cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), + instantiate_options: dict[str, Any] = {}, + ) -> None: + # We only support diagonal extraction for 2-5 qubits + assert qudit_size >= 2 and qudit_size <= 5 + self.qudit_size = qudit_size self.success_threshold = success_threshold self.cost = cost self.instantiate_options: dict[str, Any] = { 'cost_fn_gen': self.cost, 'min_iters': 0, - 'diff_tol_r':1e-4, + 'diff_tol_r': 1e-4, 'multistarts': 16, - 'method': 'qfactor' + 'method': 'qfactor', } self.instantiate_options.update(instantiate_options) super().__init__() - async def decompose( self, - op: Operation, - cost:CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), - target: UnitaryMatrix = None, + op: Operation, + target: UnitaryMatrix, + cost: CostFunctionGenerator = HilbertSchmidtResidualsGenerator(), success_threshold: float = 1e-14, - instantiate_options: dict[str, Any] = {}) -> tuple[Operation | None, - Circuit]: - ''' - Return the circuit that is generated from one levl of QSD. - ''' - + instantiate_options: dict[str, Any] = {}, + ) -> tuple[ + Operation | None, + Circuit, + ]: + """ + Return the circuit that is generated from one levl of QSD. + + Args: + op (Operation): The VariableUnitaryGate Operation to decompose. + target (UnitaryMatrix): The target unitary. + cost (CostFunctionGenerator): The cost function generator to + determine if we have succeeded in decomposing the gate. + success_threshold (float): The threshold for the cost function. + instantiate_options (dict[str, Any]): The options to pass to the + instantiate method. + """ + circ = Circuit(op.gate.num_qudits) if op.gate.num_qudits == 2: # For now just try for 2 qubit - circ.append_gate(DiagonalGate(op.gate.num_qudits), (0,1)) + circ.append_gate(DiagonalGate(op.gate.num_qudits), (0, 1)) circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (0,)) circ.append_gate(VariableUnitaryGate(op.gate.num_qudits - 1), (1,)) circ.append_gate(CNOTGate(), (0, 1)) @@ -93,35 +124,39 @@ async def decompose( else: circ = construct_linear_ansatz(op.gate.num_qudits) - instantiated_circ = circ.instantiate(target=target, - **instantiate_options) - + instantiated_circ = circ.instantiate( + target=target, + **instantiate_options, + ) if cost.calc_cost(instantiated_circ, target) < success_threshold: - print("Success") - diag_op = instantiated_circ.pop((0,0)) + diag_op = instantiated_circ.pop((0, 0)) return diag_op, instantiated_circ - + default_circ = Circuit(op.gate.num_qudits) - default_circ.append_gate(op.gate, - tuple(range(op.gate.num_qudits)), op.params) + default_circ.append_gate( + op.gate, + tuple(range(op.gate.num_qudits)), op.params, + ) return None, default_circ - async def run(self, circuit: Circuit, data: PassData) -> None: """Synthesize `utry`, see :class:`SynthesisPass` for more.""" num_ops = 0 - print(circuit.gate_counts) - j = circuit.count(VariableUnitaryGate(4)) + num_gates_to_consider = circuit.count( + VariableUnitaryGate(self.qudit_size), + ) - while j > 1: + while num_gates_to_consider > 1: # Find last Unitary all_ops = list(circuit.operations_with_cycles(reverse=True)) found = False for cyc, op in all_ops: - if isinstance(op.gate, VariableUnitaryGate) and op.gate.num_qudits in [2,3,4]: - print("Replacing op at cyc", cyc) + if ( + isinstance(op.gate, VariableUnitaryGate) + and op.gate.num_qudits in [2, 3, 4] + ): if found: merge_op = op merge_pt = (cyc, op.location[0]) @@ -132,25 +167,22 @@ async def run(self, circuit: Circuit, data: PassData) -> None: gate = op pt = (cyc, op.location[0]) found = True - # print(self.cost.calc_cost(circuit, data.target)) - # print(f"Decomposing {num_ops}th gate", flush=True) - diag_op, circ = await self.decompose(gate, - cost=self.cost, - target=gate.get_unitary(), - success_threshold=self.success_threshold, - instantiate_options=self.instantiate_options) - - print(self.cost.calc_cost(circuit, data.target)) + diag_op, circ = await self.decompose( + gate, + cost=self.cost, + target=gate.get_unitary(), + success_threshold=self.success_threshold, + instantiate_options=self.instantiate_options, + ) + circuit.replace_with_circuit(pt, circ, as_circuit_gate=True) - print(self.cost.calc_cost(circuit, data.target)) - j -= 1 + num_gates_to_consider -= 1 # Commute Diagonal into next op if diag_op: - print(self.cost.calc_cost(circuit, data.target)) new_mat = diag_op.get_unitary() @ merge_op.get_unitary() - circuit.replace_gate(merge_pt, merge_op.gate, merge_location, merge_op.gate.calc_params(new_mat)) - print(self.cost.calc_cost(circuit, data.target)) - print("Inserted Diagonal") + circuit.replace_gate( + merge_pt, merge_op.gate, merge_location, + VariableUnitaryGate.get_params(new_mat), + ) - print(circuit.gate_counts) - circuit.unfold_all() \ No newline at end of file + circuit.unfold_all() diff --git a/bqskit/passes/synthesis/__init__.py b/bqskit/passes/synthesis/__init__.py index 33253cbc..4246d49f 100644 --- a/bqskit/passes/synthesis/__init__.py +++ b/bqskit/passes/synthesis/__init__.py @@ -2,12 +2,15 @@ from __future__ import annotations from bqskit.passes.synthesis.bzxz import BlockZXZPass +from bqskit.passes.synthesis.bzxz import FullBlockZXZPass from bqskit.passes.synthesis.diagonal import WalshDiagonalSynthesisPass from bqskit.passes.synthesis.leap import LEAPSynthesisPass 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, MGDPass, QSDPass +from bqskit.passes.synthesis.qsd import FullQSDPass +from bqskit.passes.synthesis.qsd import MGDPass +from bqskit.passes.synthesis.qsd import QSDPass from bqskit.passes.synthesis.qsearch import QSearchSynthesisPass from bqskit.passes.synthesis.synthesis import SynthesisPass from bqskit.passes.synthesis.target import SetTargetPass @@ -25,4 +28,5 @@ 'MGDPass', 'QSDPass', 'BlockZXZPass', + 'FullBlockZXZPass', ] diff --git a/bqskit/passes/synthesis/bzxz.py b/bqskit/passes/synthesis/bzxz.py index 1496eab2..1fdabf2f 100644 --- a/bqskit/passes/synthesis/bzxz.py +++ b/bqskit/passes/synthesis/bzxz.py @@ -1,42 +1,47 @@ -"""This module implements the Full Block ZXZ Decomposition. Additionally, it -defines the Block ZXZ Decomposition for a single pass. """ +""" +This module implements the Full Block ZXZ Decomposition. + +Additionally, it defines the Block ZXZ Decomposition for a single pass. +""" from __future__ import annotations import logging from typing import Any -from bqskit.compiler.basepass import BasePass +import numpy as np +from scipy.linalg import eig +from scipy.linalg import svd +from bqskit.compiler.basepass import BasePass from bqskit.compiler.passdata import PassData from bqskit.compiler.workflow import Workflow -from bqskit.passes.processing.scan import ScanningGateRemovalPass -from bqskit.passes.processing.treescan import TreeScanningGateRemovalPass -from bqskit.passes.synthesis.qsd import MGDPass, QSDPass -from bqskit.passes.processing.extract_diagonal import ExtractDiagonalPass -from bqskit.ir.circuit import Circuit - -from bqskit.compiler.basepass import BasePass -from bqskit.ir.operation import Operation -from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix from bqskit.ir.circuit import Circuit +from bqskit.ir.gates import CircuitGate +from bqskit.ir.gates.constant import CNOTGate +from bqskit.ir.gates.constant import HGate +from bqskit.ir.gates.constant import IdentityGate +from bqskit.ir.gates.constant import ZGate +from bqskit.ir.gates.parameterized import RZGate from bqskit.ir.gates.parameterized.mcry import MCRYGate from bqskit.ir.gates.parameterized.mcrz import MCRZGate -from bqskit.ir.gates.constant import IdentityGate, HGate, ZGate -from bqskit.ir.gates.parameterized import RZGate -from bqskit.ir.gates.constant import CNOTGate -from bqskit.ir.location import CircuitLocation, CircuitLocationLike -from bqskit.ir.gates import CircuitGate +from bqskit.ir.location import CircuitLocation +from bqskit.ir.operation import Operation +from bqskit.passes.processing.extract_diagonal import ExtractDiagonalPass +from bqskit.passes.processing.scan import ScanningGateRemovalPass +from bqskit.passes.processing.treescan import TreeScanningGateRemovalPass +from bqskit.passes.synthesis.qsd import MGDPass +from bqskit.passes.synthesis.qsd import QSDPass +from bqskit.qis.unitary.unitary import RealVector +from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix from bqskit.runtime import get_runtime -from scipy.linalg import svd, block_diag, eig -import numpy as np - _logger = logging.getLogger(__name__) + class FullBlockZXZPass(BasePass): """ - A pass performing a full Block ZXZ decomposition + A pass performing a full Block ZXZ decomposition. References: C.C. Paige, M. Wei, @@ -81,98 +86,100 @@ def __init__( self.start_from_left = start_from_left self.min_qudit_size = min_qudit_size - instantiation_options = {"method":"qfactor"} + instantiation_options = {'method': 'qfactor'} instantiation_options.update(instantiate_options) self.perform_scan = perform_scan self.perform_extract = perform_extract + 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) - else: - self.scan = ScanningGateRemovalPass( - start_from_left=start_from_left, - instantiate_options=instantiation_options) + start_from_left=start_from_left, + instantiate_options=instantiation_options, + tree_depth=tree_depth, + ) self.bzxz = BlockZXZPass(min_qudit_size=min_qudit_size) - self.mgd = MGDPass(inverse = True) - self.diag = ExtractDiagonalPass() + self.mgd = MGDPass() + self.diag = ExtractDiagonalPass(qudit_size=min_qudit_size) async def run(self, circuit: Circuit, data: PassData) -> None: - ''' - Perform succesive rounds of Block ZXZ decomposition until the - circuit is fully decomposed with no VariableUnitaryGates larger - then `min_qudit_size`. - - At the end, attempt to extrac the diagonal gate and - commute through the circuit to find optimal CNOT counts. - ''' - passes = [] + """ + Perform succesive rounds of Block ZXZ decomposition until the circuit is + fully decomposed with no VariableUnitaryGates larger then + `min_qudit_size`. + + At the end, attempt to extrac the diagonal gate and commute through the + circuit to find optimal CNOT counts. + """ + 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.bzxz) if self.perform_scan: passes.append(self.scan) - + if self.perform_extract: passes.append(self.diag) - # Once we have commuted the diagonal gate, we can break down the + # Once we have commuted the diagonal gate, we can break down the # multiplexed gates - for _ in range(self.min_qudit_size, start_num): + for _ in range(1, start_num): passes.append(self.mgd) if self.perform_scan: passes.append(self.scan) await Workflow(passes).run(circuit, data) -def left_shift(loc: CircuitLocationLike): - return loc[1:] + loc[0:1] class BlockZXZPass(BasePass): """ A pass performing one round of decomposition from the Block ZXZ algorithm. References: - Krol, Anna M., and Zaid Al-Ars. - "Highly Efficient Decomposition of n-Qubit Quantum Gates - Based on Block-ZXZ Decomposition." + Krol, Anna M., and Zaid Al-Ars. + "Highly Efficient Decomposition of n-Qubit Quantum Gates + Based on Block-ZXZ Decomposition." arXiv preprint arXiv:2403.13692 (2024). https://arxiv.org/html/2403.13692v1 """ 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 widht > min_qudit_size - """ - self.min_qudit_size = min_qudit_size - - def initial_decompose(U: UnitaryMatrix) -> tuple[UnitaryMatrix, - UnitaryMatrix, - UnitaryMatrix, - UnitaryMatrix]: - ''' + 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 widht > min_qudit_size + """ + self.min_qudit_size = min_qudit_size + + @staticmethod + def initial_decompose(U: UnitaryMatrix) -> tuple[ + UnitaryMatrix, + UnitaryMatrix, + UnitaryMatrix, + UnitaryMatrix, + ]: + """ This function decomposes the a given unitary U into 2 controlled - unitaries (B, and C), 1 multiplexed unitary (A) and 2 Hadamard gates. + unitaries (B, and C), 1 multiplexed unitary (A) and 2 Hadamard gates. We return the A1 and A2 (sub-unitaries of the multipelexed unitary A), as well as the sub unitary B and C. By sub-unitary, we mean the unitary that will be applied to the bottom - n - 1 qubits given the value of the top qubit 0. For a controlled + n - 1 qubits given the value of the top qubit 0. For a controlled unitary, that sub-unitary is either the Identity (if qubit 0 is 0) or a unitary on n-1 qubits if qubit 0 is 1. This follows equations 5-9 in the paper. - ''' + """ # To solve for A1, A2, and C, we must divide U into 4 submatrices. # The upper left block is X, the upper right block is Y, the lower left # block is U_21, and the lower right block is U_22. @@ -182,13 +189,13 @@ def initial_decompose(U: UnitaryMatrix) -> tuple[UnitaryMatrix, U_21 = U[len(U) // 2:, 0:len(U) // 2] U_22 = U[len(U) // 2:, len(U) // 2:] - # We let X = V_X @ s_X @ W_X where V_X and W_X are unitary and s_X is + # We let X = V_X @ s_X @ W_X where V_X and W_X are unitary and s_X is # a diagonal matrix of singular values. We use the SVD to find these # matrices. We do the same decomposition for Y. V_X, s_X, W_XH = svd(X) V_Y, s_Y, W_YH = svd(Y) - # We can then define S_X and S_Y as V_X @ s_X @ V_X^H and + # We can then define S_X and S_Y as V_X @ s_X @ V_X^H and # V_Y @ s_Y @ V_Y^H S_X = V_X @ np.diag(s_X) @ V_X.conj().T S_Y = V_Y @ np.diag(s_Y) @ V_Y.conj().T @@ -206,30 +213,33 @@ def initial_decompose(U: UnitaryMatrix) -> tuple[UnitaryMatrix, A_2 = UnitaryMatrix(U_21 + U_22 @ (1j * U_Y.conj().T @ U_X)) # Finally, we can set B as 2(A_1^H @ U) - I - I = np.eye(len(U) / 2) + I = np.eye(len(U) // 2) B = UnitaryMatrix((2 * (A_1.conj().T @ X)) - I) return A_1, A_2, B, UnitaryMatrix(CH.conj().T) - @staticmethod - def demultiplex(U_1: UnitaryMatrix, - U_2: UnitaryMatrix) -> tuple[UnitaryMatrix, - np.ndarray, - UnitaryMatrix]: - ''' - Demultiplex U that is block_diag(U_1, U_2) to - 2 Unitary Matrices V and W and a diagonal matrix D. + @staticmethod + def demultiplex( + U_1: UnitaryMatrix, + U_2: UnitaryMatrix, + ) -> tuple[ + UnitaryMatrix, + RealVector, + UnitaryMatrix, + ]: + """ + Demultiplex U that is block_diag(U_1, U_2) to 2 Unitary Matrices V and W + and a diagonal matrix D. - The Diagonal Matrix D corresponds to a Multiplexed Rz gate acting on - the most significant qubit. + The Diagonal Matrix D corresponds to a Multiplexed Rz gate acting on the + most significant qubit. We return the Unitary Matrices V and W and the parameters for the corresponding MCRZ gate. - - ''' + """ # U can be decomposed into U = (I otimes V )(D otimes D†)(I otimes W ) - # We can find V,D^2 by performing an eigen decomposition of + # We can find V,D^2 by performing an eigen decomposition of # U_1 @ U_2† d2, V = eig(U_1 @ U_2.conj().T) d = np.sqrt(d2) @@ -239,126 +249,16 @@ def demultiplex(U_1: UnitaryMatrix, W = D @ V.conj().T @ U_2 # We can use d to find the parameters for the MCRZ gate. - # Note than because and Rz does exp(-i * theta / 2), we must + # Note than because and Rz does exp(-i * theta / 2), we must # multiply by -2. - d_params = np.angle(d) * -2 + d_params: list[float] = list(np.angle(d) * -2) return UnitaryMatrix(V), d_params, UnitaryMatrix(W) - @staticmethod - def decompose_mpz_one_level(params, - num_qudits, - reverse=False, - drop_last_cnot=False) -> Circuit: - ''' - Decompose MPZ one level. - - Args: - params (np.ndarray): The parameters for the original MCRZ gate - num_qudits (int): The number of qudits in the MCRZ gate - reverse (bool): Whether to reverse the order of the gates (you can - decompose the gates in either order to get the same result) - drop_last_cnot (bool): Whether to drop the last CNOT gate (only - should be set to True if you are doing section 5.2 optimization) - - Returns: - Circuit: The circuit that decomposes the MCRZ gate - ''' - if (num_qudits >= 3): - # Remove 1 qubit, last qubit is controlled - new_gate = MCRZGate(num_qudits - 1, num_qudits-2) - else: - new_gate = RZGate() - - left_params, right_params = MCRYGate.get_decomposition(params) - circ = Circuit(num_qudits) - new_gate_location = list(range(1, num_qudits)) - cx_location = (0, num_qudits - 1) - - ops = [Operation(new_gate, new_gate_location, left_params), - Operation(CNOTGate(), cx_location), - Operation(new_gate, new_gate_location, right_params), - Operation(CNOTGate(), cx_location)] - - if drop_last_cnot: - ops.pop() - - if reverse: - ops.reverse() - - for op in ops: - circ.append(op) - - return circ - - @staticmethod - def decompose_mpz_two_levels(params, num_qudits, reverse=False, remove_last_cnot=True) -> Circuit: - ''' - We decompose a multiplexed RZ gate 2 levels deep. This allows you - to remove the last CNOT gate in the context of the Block ZXZ - decomposition. This is shown in section 5.2 of the paper. - - Args: - params (np.ndarray): The parameters for the original MCRZ gate - num_qudits (int): The number of qudits in the MCRZ gate - reverse (bool): Whether to reverse the order of the gates (you can - decompose the gates in either order to get the same result) - drop_last_cnot (bool): Whether to drop the last CNOT gate (only - should be set to True if you are doing section 5.2 optimization) - - Returns: - Circuit: The circuit that decomposes the MCRZ gate - ''' - # Get params for first decomposition of the MCRZ gate - left_params, right_params = MCRYGate.get_decomposition(params) - - - # Decompose the MCRZ gate into 2 MCRZ gates, dropping the last CNOT - # Also Reverse the circuit for the right side in order to do the - # optimization in section 5.2 - circ_left = BlockZXZPass.decompose_mpz_one_level(left_params, - num_qudits - 1, - reverse, - drop_last_cnot=True) - - circ_right = BlockZXZPass.decompose_mpz_one_level(right_params, - num_qudits - 1, - not reverse, - drop_last_cnot=True) - - # Now, construct the circuit. - # This will generate the original MCRZ gate with the target qubit - # set as qubit num_qudits - 1 - circ = Circuit(num_qudits) - cx_location_big = (0, num_qudits - 1) - - ops = [circ_left, - Operation(CNOTGate(), cx_location_big), - circ_right, - Operation(CNOTGate(), cx_location_big)] - - # We can remove the last CNOT gate as per section 5.2 - if remove_last_cnot: - ops.pop() - - if reverse: - ops.reverse() - - for op in ops: - if isinstance(op, Operation): - circ.append(op) - else: - circ.append_circuit(op, list(range(1, num_qudits))) - - return circ - - @staticmethod def zxz(orig_u: UnitaryMatrix) -> Circuit: - ''' - Return the circuit that is generated from one levl of - Block ZXZ decomposition. - ''' + """Return the circuit that is generated from one levl of Block ZXZ + decomposition.""" # First calculate the A, B, and C matrices for the initial decomp A_1, A_2, B, C = BlockZXZPass.initial_decompose(orig_u) @@ -366,15 +266,18 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: # Now decompose thee multiplexed A gate and the controlled C gate I = IdentityGate(orig_u.num_qudits - 1).get_unitary() VA, AZ_params, WA = BlockZXZPass.demultiplex(A_1, A_2) - VC, CZ_params, WC = BlockZXZPass.demultiplex(I, C) + VC, CZ_params, WC = BlockZXZPass.demultiplex(I, C) # Now calculate optimized B_tilde (Section 5.2) and decompose - # We merge in WA and VC into B and then merge in the additional + # We merge in WA and VC into B and then merge in the additional # CZ gates from the optimization small_I = IdentityGate(orig_u.num_qudits - 2).get_unitary() Z = ZGate(1).get_unitary() - B_tilde_1 = WA @ VC - B_tilde_2 = np.kron(Z, small_I) @ WA @ B @ VC @ np.kron(Z, small_I) + B_tilde_1 = UnitaryMatrix(WA @ VC) + B_tilde_2 = UnitaryMatrix( + np.kron(Z, small_I) @ + WA @ B @ VC @ np.kron(Z, small_I), + ) VB, BZ_params, WB = BlockZXZPass.demultiplex(B_tilde_1, B_tilde_2) # Define circuit locations @@ -384,9 +287,6 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: select_qubits = list(range(1, orig_u.num_qudits)) all_qubits = list(range(0, orig_u.num_qudits)) - # z_gate_circ = BlockZXZPass.decompose_mpz_two_levels(BZ_params, orig_u.num_qudits, False, False) - # assert(np.allclose(z_gate_circ.get_unitary(), z_gate.get_unitary(BZ_params))) - # Construct Circuit circ = Circuit(orig_u.num_qudits) @@ -394,21 +294,27 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: wc_gate, wc_params = QSDPass.create_unitary_gate(WC) circ.append_gate(wc_gate, CircuitLocation(select_qubits), wc_params) - - # Add decomposed MCRZ gate circuit. + # Add decomposed MCRZ gate circuit. # Since the MCRZ gate circuit sets the target qubit as qubit # num_qudits - 1, we must shift the qubits to the left - shifted_qubits = left_shift(all_qubits) + shifted_qubits = all_qubits[1:] + all_qubits[0:1] circ.append_circuit( - BlockZXZPass.decompose_mpz_two_levels(CZ_params, orig_u.num_qudits), - shifted_qubits) - + MGDPass.decompose_mpx_two_levels( + decompose_ry=False, + params=CZ_params, + num_qudits=orig_u.num_qudits, + drop_last_cnot=True + ), + shifted_qubits, + ) # Add the decomposed B-tilde gates WB and a Hadamard combo_1_gate, combo_1_params = QSDPass.create_unitary_gate(WB) - circ.append_gate(combo_1_gate, CircuitLocation(select_qubits), - combo_1_params) - circ.append_gate(HGate(), CircuitLocation((controlled_qubit, ))) + circ.append_gate( + combo_1_gate, CircuitLocation(select_qubits), + combo_1_params, + ) + circ.append_gate(HGate(), CircuitLocation((controlled_qubit,))) # The central MCRZ_gate. We set the target to the controlled qubit, # so there is no need to shift @@ -417,38 +323,45 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: # Now add the decomposed B-tilde gates VB and a Hadamard combo_2_gate, combo_2_params = QSDPass.create_unitary_gate(VB) - circ.append_gate(combo_2_gate, CircuitLocation(select_qubits), - combo_2_params) - circ.append_gate(HGate(), CircuitLocation((controlled_qubit, ))) - + circ.append_gate( + combo_2_gate, CircuitLocation(select_qubits), + combo_2_params, + ) + circ.append_gate(HGate(), CircuitLocation((controlled_qubit,))) # Add the decomposed MCRZ gate circuit again on shifted qubits circ.append_circuit( - BlockZXZPass.decompose_mpz_two_levels(AZ_params, - orig_u.num_qudits, - True), - shifted_qubits) - + MGDPass.decompose_mpx_two_levels( + decompose_ry=False, + params=AZ_params, + num_qudits=orig_u.num_qudits, + reverse=True, + drop_last_cnot=True + ), + shifted_qubits, + ) + va_gate, va_params = QSDPass.create_unitary_gate(VA) circ.append_gate(va_gate, CircuitLocation(select_qubits), va_params) - # assert np.allclose(orig_u, circ.get_unitary()) return circ async def run(self, circuit: Circuit, data: PassData) -> None: """Perform a single level of the Block ZXZ decomposition.""" unitaries, pts, locations = QSDPass.get_variable_unitary_pts( - circuit, self.min_qudit_size + circuit, self.min_qudit_size, ) if len(unitaries) > 0: circs = await get_runtime().map(BlockZXZPass.zxz, unitaries) # Do bulk replace 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 + circuit.unfold_all() diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py index 1df6256f..94bd71f0 100644 --- a/bqskit/passes/synthesis/qsd.py +++ b/bqskit/passes/synthesis/qsd.py @@ -12,7 +12,8 @@ from bqskit.compiler.passdata import PassData from bqskit.compiler.workflow import Workflow from bqskit.ir.circuit import Circuit -from bqskit.ir.circuit import CircuitLocation, CircuitPoint +from bqskit.ir.circuit import CircuitLocation +from bqskit.ir.circuit import CircuitPoint from bqskit.ir.gates import CircuitGate from bqskit.ir.gates.constant import CNOTGate from bqskit.ir.gates.parameterized import RYGate @@ -30,6 +31,7 @@ _logger = logging.getLogger(__name__) + class FullQSDPass(BasePass): """ A pass performing one round of decomposition from the QSD algorithm. @@ -96,7 +98,7 @@ def __init__( # 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.mgd = MGDPass(decompose_twice=False) self.perform_scan = perform_scan async def run(self, circuit: Circuit, data: PassData) -> None: @@ -128,64 +130,202 @@ class MGDPass(BasePass): https://arxiv.org/pdf/quant-ph/0406176.pdf """ + def __init__(self, decompose_twice: bool = True) -> None: + """ + The MGDPass decomposes all MCRY and MCRZ gates in a circuit. + + Args: + decompose_twice (bool): Whether to decompose the MCRZ gate twice. + This will save 2 CNOT gates in the decomposition. If false, + the pass will only decompose one level. (Default: True) + """ + self.decompose_twice = decompose_twice + + @staticmethod + def decompose_mpx_one_level( + decompose_ry: bool, + params: RealVector, + num_qudits: int, + reverse: bool =False, + drop_last_cnot: bool =False, + ) -> Circuit: + """ + Decompose Multiplexed Gate one level. + + Args: + params (RealVector): The parameters for the original MCRZ gate + num_qudits (int): The number of qudits in the MCRZ gate + reverse (bool): Whether to reverse the order of the gates (you can + decompose the gates in either order to get the same result) + drop_last_cnot (bool): Whether to drop the last CNOT gate. This + should be set if you are doing a 2 level decomposition to save 2 + CNOT gates. + + Returns: + Circuit: The circuit that decomposes the MCRZ gate + """ + + new_gate: MCRZGate | MCRYGate | RZGate | RYGate = RZGate() + if decompose_ry: + new_gate = RYGate() + + if (num_qudits >= 3): + if decompose_ry: + new_gate = MCRYGate(num_qudits - 1, num_qudits - 2) + else: + # Remove 1 qubit, last qubit is controlled + new_gate = MCRZGate(num_qudits - 1, num_qudits - 2) + + left_params, right_params = MCRYGate.get_decomposition(params) + circ = Circuit(num_qudits) + new_gate_location = list(range(1, num_qudits)) + cx_location = (0, num_qudits - 1) + + ops = [ + Operation(new_gate, new_gate_location, left_params), + Operation(CNOTGate(), cx_location), + Operation(new_gate, new_gate_location, right_params), + Operation(CNOTGate(), cx_location), + ] + + if drop_last_cnot: + ops.pop() + + if reverse: + ops.reverse() + + for op in ops: + circ.append(op) + + return circ + @staticmethod - def decompose(op: Operation) -> Circuit: + def decompose_mpx_two_levels( + decompose_ry: bool, + params: RealVector, + num_qudits: int, + reverse: bool =False, + drop_last_cnot: bool = False, + ) -> Circuit: """ - Return the decomposed circuit from one operation of a multiplexed gate. + We decompose a multiplexed RZ gate 2 levels deep. This allows you to + remove 2 CNOTs as per Figure 2 in + https://arxiv.org/pdf/quant-ph/0406176.pdf. + + Furthermore, in the context of the Block ZXZ decomposition, you can + set `drop_last_cnot` to True. This CNOT gets merged into a central gate, + which saves another 2 CNOTs. This is shown in section 5.2 of + https://arxiv.org/pdf/2403.13692v1.pdf. Args: - op (Operation): The operation to decompose. + decompose_ry (bool): Whether to decompose the MCRY gate + params (RealVector): The parameters for the original MCR gate + num_qudits (int): The number of qudits in the MCR gate + reverse (bool): Whether to reverse the order of the gates (you can + decompose the gates in either order to get the same result) + drop_last_cnot (bool): Whether to drop the last CNOT gate (only + should be set to True if you are doing section 5.2 optimization) Returns: - Circuit: The decomposed circuit. + Circuit: The circuit that decomposes the MCR gate """ - # 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 num_qudits <= 2: + # If you have less than 3 qubits, just decompose one level + return MGDPass.decompose_mpx_one_level(decompose_ry, + params, + num_qudits, + reverse) + + # Get params for first decomposition of the MCRZ gate + left_params, right_params = MCRYGate.get_decomposition(params) + + # Decompose the MCRZ gate into 2 MCRZ gates, dropping the last CNOT + # Also Reverse the circuit for the right side in order to do the + # optimization in section 5.2 + circ_left = MGDPass.decompose_mpx_one_level( + decompose_ry, + left_params, + num_qudits - 1, + reverse, + drop_last_cnot=True, + ) + + circ_right = MGDPass.decompose_mpx_one_level( + decompose_ry, + right_params, + num_qudits - 1, + not reverse, + drop_last_cnot=True, ) - 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(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) - circ.append_gate(CNOTGate(), cx_location) + + # Now, construct the circuit. + # This will generate the original MCRZ gate with the target qubit + # set as qubit num_qudits - 1 + circ = Circuit(num_qudits) + cx_location_big = (0, num_qudits - 1) + + ops: list[Circuit | Operation] = [ + circ_left, + Operation(CNOTGate(), cx_location_big), + circ_right, + Operation(CNOTGate(), cx_location_big), + ] + + # We can remove the last CNOT gate as per section 5.2 + if drop_last_cnot: + ops.pop() + + if reverse: + ops.reverse() + + for op in ops: + if isinstance(op, Operation): + circ.append(op) + else: + circ.append_circuit(op, list(range(1, num_qudits))) 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 = [] + ops: list[Operation] = [] + pts: list[CircuitPoint] = [] + locations: list[CircuitLocation] = [] 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: + ops.append(op) + pts.append(CircuitPoint((cyc, op.location[0]))) + # Adjust location based on current target, move target to last + # qudit + loc = list(op.location) + loc = ( + loc[0:op.gate.target_qubit] + + loc[(op.gate.target_qubit + 1):] + + [loc[op.gate.target_qubit]] + ) + locations.append(loc) + + if len(ops) > 0: # Do a bulk QSDs -> circs - circs = [MGDPass.decompose(gate) for gate in gates] + if self.decompose_twice: + circs = [ + MGDPass.decompose_mpx_two_levels( + isinstance(op.gate, MCRYGate), + op.params, + op.num_qudits) for op in ops + ] + else: + circs = [ + MGDPass.decompose_mpx_one_level( + isinstance(op.gate, MCRYGate), + op.params, + op.num_qudits) for op in ops + ] circ_gates = [CircuitGate(x) for x in circs] circ_ops = [ Operation(x, locations[i], x._circuit.params) @@ -196,6 +336,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None: circuit.unfold_all() + class QSDPass(BasePass): """ A pass performing one round of decomposition from the QSD algorithm. @@ -224,10 +365,12 @@ def __init__( self.min_qudit_size = min_qudit_size @staticmethod - def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: - ''' - Return the Permutation Matrix that shifts the qubits down by 1 qubit. - ''' + def shift_down_unitary( + num_qudits: int, + end_qubits: int, + ) -> PermutationMatrix: + """Return the Permutation Matrix that shifts the qubits down by 1 + qubit.""" 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)) @@ -235,16 +378,17 @@ def shift_down_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: return PermutationMatrix.from_qubit_location(num_qudits, final_qudits) @staticmethod - def shift_up_unitary(num_qudits: int, end_qubits: int) -> PermutationMatrix: - ''' - Return the Permutation Matrix that shifts the qubits down by 1 qubit. - ''' + def shift_up_unitary( + num_qudits: int, + end_qubits: int, + ) -> PermutationMatrix: + """Return the Permutation Matrix that shifts the qubits down by 1 + qubit.""" 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) - @staticmethod def create_unitary_gate(u: UnitaryMatrix) -> tuple[ VariableUnitaryGate, @@ -358,20 +502,17 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: circ_2, CircuitLocation(list(range(u.num_qudits))), ) return circ_1 - + @staticmethod - def get_variable_unitary_pts(circuit: Circuit, min_qudit_size: int) -> tuple[ - list[UnitaryMatrix], - list[CircuitPoint], - list[CircuitLocation] - ]: - ''' - Get all VariableUnitary Gates in the circuit wider than - `min_qudit_size` and return their unitaries, points, and locations. - ''' - unitaries = [] - pts = [] - locations = [] + def get_variable_unitary_pts( + circuit: Circuit, + min_qudit_size: int, + ) -> tuple[list[UnitaryMatrix], list[CircuitPoint], list[CircuitLocation]]: + """Get all VariableUnitary Gates in the circuit wider than + `min_qudit_size` and return their unitaries, points, and locations.""" + unitaries: list[UnitaryMatrix] = [] + pts: list[CircuitPoint] = [] + locations: list[CircuitLocation] = [] num_ops = 0 all_ops = list(circuit.operations_with_cycles(reverse=True)) @@ -383,18 +524,16 @@ def get_variable_unitary_pts(circuit: Circuit, min_qudit_size: int) -> tuple[ ): num_ops += 1 unitaries.append(op.get_unitary()) - pts.append((cyc, op.location[0])) + pts.append(CircuitPoint((cyc, op.location[0]))) locations.append(op.location) return unitaries, pts, locations - async def run(self, circuit: Circuit, data: PassData) -> None: - ''' - Perform a single pass of Quantum Shannon Decomposition on the circuit. - ''' + """Perform a single pass of Quantum Shannon Decomposition on the + circuit.""" unitaries, pts, locations = QSDPass.get_variable_unitary_pts( - circuit, self.min_qudit_size + circuit, self.min_qudit_size, ) if len(unitaries) > 0: From f844bcff64333d6d847b6c5b60bc17447432d785 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 11:55:55 -0700 Subject: [PATCH 10/15] tox --- bqskit/passes/synthesis/bzxz.py | 11 ++++------ bqskit/passes/synthesis/qsd.py | 37 ++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/bqskit/passes/synthesis/bzxz.py b/bqskit/passes/synthesis/bzxz.py index 1fdabf2f..39e80506 100644 --- a/bqskit/passes/synthesis/bzxz.py +++ b/bqskit/passes/synthesis/bzxz.py @@ -17,12 +17,9 @@ from bqskit.compiler.workflow import Workflow from bqskit.ir.circuit import Circuit from bqskit.ir.gates import CircuitGate -from bqskit.ir.gates.constant import CNOTGate from bqskit.ir.gates.constant import HGate from bqskit.ir.gates.constant import IdentityGate from bqskit.ir.gates.constant import ZGate -from bqskit.ir.gates.parameterized import RZGate -from bqskit.ir.gates.parameterized.mcry import MCRYGate from bqskit.ir.gates.parameterized.mcrz import MCRZGate from bqskit.ir.location import CircuitLocation from bqskit.ir.operation import Operation @@ -301,10 +298,10 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: circ.append_circuit( MGDPass.decompose_mpx_two_levels( decompose_ry=False, - params=CZ_params, + params=CZ_params, num_qudits=orig_u.num_qudits, - drop_last_cnot=True - ), + drop_last_cnot=True, + ), shifted_qubits, ) @@ -336,7 +333,7 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: params=AZ_params, num_qudits=orig_u.num_qudits, reverse=True, - drop_last_cnot=True + drop_last_cnot=True, ), shifted_qubits, ) diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py index 94bd71f0..d37b18f3 100644 --- a/bqskit/passes/synthesis/qsd.py +++ b/bqskit/passes/synthesis/qsd.py @@ -146,8 +146,8 @@ def decompose_mpx_one_level( decompose_ry: bool, params: RealVector, num_qudits: int, - reverse: bool =False, - drop_last_cnot: bool =False, + reverse: bool = False, + drop_last_cnot: bool = False, ) -> Circuit: """ Decompose Multiplexed Gate one level. @@ -204,15 +204,15 @@ def decompose_mpx_two_levels( decompose_ry: bool, params: RealVector, num_qudits: int, - reverse: bool =False, + reverse: bool = False, drop_last_cnot: bool = False, ) -> Circuit: """ - We decompose a multiplexed RZ gate 2 levels deep. This allows you to - remove 2 CNOTs as per Figure 2 in + We decompose a multiplexed RZ gate 2 levels deep. This allows you to + remove 2 CNOTs as per Figure 2 in https://arxiv.org/pdf/quant-ph/0406176.pdf. - - Furthermore, in the context of the Block ZXZ decomposition, you can + + Furthermore, in the context of the Block ZXZ decomposition, you can set `drop_last_cnot` to True. This CNOT gets merged into a central gate, which saves another 2 CNOTs. This is shown in section 5.2 of https://arxiv.org/pdf/2403.13692v1.pdf. @@ -232,10 +232,12 @@ def decompose_mpx_two_levels( if num_qudits <= 2: # If you have less than 3 qubits, just decompose one level - return MGDPass.decompose_mpx_one_level(decompose_ry, - params, - num_qudits, - reverse) + return MGDPass.decompose_mpx_one_level( + decompose_ry, + params, + num_qudits, + reverse, + ) # Get params for first decomposition of the MCRZ gate left_params, right_params = MCRYGate.get_decomposition(params) @@ -292,7 +294,6 @@ async def run(self, circuit: Circuit, data: PassData) -> None: ops: list[Operation] = [] pts: list[CircuitPoint] = [] locations: list[CircuitLocation] = [] - num_ops = 0 all_ops = list(circuit.operations_with_cycles(reverse=True)) # Gather all of the multiplexed operations @@ -308,7 +309,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None: + loc[(op.gate.target_qubit + 1):] + [loc[op.gate.target_qubit]] ) - locations.append(loc) + locations.append(CircuitLocation(loc)) if len(ops) > 0: # Do a bulk QSDs -> circs @@ -317,15 +318,17 @@ async def run(self, circuit: Circuit, data: PassData) -> None: MGDPass.decompose_mpx_two_levels( isinstance(op.gate, MCRYGate), op.params, - op.num_qudits) for op in ops - ] + op.num_qudits, + ) for op in ops + ] else: circs = [ MGDPass.decompose_mpx_one_level( isinstance(op.gate, MCRYGate), op.params, - op.num_qudits) for op in ops - ] + op.num_qudits, + ) for op in ops + ] circ_gates = [CircuitGate(x) for x in circs] circ_ops = [ Operation(x, locations[i], x._circuit.params) From f10aa6cf2aaa30b548f6ced06f48a2bf0e736205 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 12:06:00 -0700 Subject: [PATCH 11/15] Adding test --- bqskit/passes/synthesis/bzxz.py | 2 +- tests/passes/synthesis/test_bzxz.py | 43 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/passes/synthesis/test_bzxz.py diff --git a/bqskit/passes/synthesis/bzxz.py b/bqskit/passes/synthesis/bzxz.py index 39e80506..4f707f13 100644 --- a/bqskit/passes/synthesis/bzxz.py +++ b/bqskit/passes/synthesis/bzxz.py @@ -152,7 +152,7 @@ def __init__( Args: min_qudit_size (int): Performs a decomposition on all gates - with widht > min_qudit_size + with width > min_qudit_size """ self.min_qudit_size = min_qudit_size diff --git a/tests/passes/synthesis/test_bzxz.py b/tests/passes/synthesis/test_bzxz.py new file mode 100644 index 00000000..bbdaa03d --- /dev/null +++ b/tests/passes/synthesis/test_bzxz.py @@ -0,0 +1,43 @@ +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 BlockZXZPass, FullBlockZXZPass +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 TestBZXZ: + def test_small_qubit_bzxz(compiler: Compiler) -> None: + circuit = create_random_unitary_circ(4) + utry = circuit.get_unitary() + bzxz = BlockZXZPass(min_qudit_size=2) + circuit = compiler.compile(circuit, [bzxz]) + dist = circuit.get_unitary().get_distance_from(utry) + assert dist <= 1e-5 + + def test_full_bzxz_no_extract(compiler: Compiler) -> None: + circuit = create_random_unitary_circ(5) + utry = circuit.get_unitary() + bzxz = FullBlockZXZPass(min_qudit_size=2, perform_scan=False, + perform_extract=False) + circuit = compiler.compile(circuit, [bzxz]) + dist = circuit.get_unitary().get_distance_from(utry) + print(dist) + print(circuit.gate_counts) + assert dist <= 1e-5 \ No newline at end of file From fdb81d765740c3513814f21a9bbe7911ce38610d Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 12:08:16 -0700 Subject: [PATCH 12/15] tox --- tests/passes/synthesis/test_bzxz.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/tests/passes/synthesis/test_bzxz.py b/tests/passes/synthesis/test_bzxz.py index bbdaa03d..02993682 100644 --- a/tests/passes/synthesis/test_bzxz.py +++ b/tests/passes/synthesis/test_bzxz.py @@ -5,25 +5,29 @@ from bqskit.compiler import Compiler from bqskit.ir.circuit import Circuit from bqskit.ir.gates.parameterized import VariableUnitaryGate -from bqskit.passes import BlockZXZPass, FullBlockZXZPass +from bqskit.passes import BlockZXZPass +from bqskit.passes import FullBlockZXZPass from bqskit.qis import UnitaryMatrix -def create_random_unitary_circ(num_qudits: int): - ''' - Create a Circuit with a random VariableUnitaryGate. - ''' +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) - 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 TestBZXZ: - def test_small_qubit_bzxz(compiler: Compiler) -> None: + def test_small_qubit_bzxz(self, compiler: Compiler) -> None: circuit = create_random_unitary_circ(4) utry = circuit.get_unitary() bzxz = BlockZXZPass(min_qudit_size=2) @@ -31,13 +35,15 @@ def test_small_qubit_bzxz(compiler: Compiler) -> None: dist = circuit.get_unitary().get_distance_from(utry) assert dist <= 1e-5 - def test_full_bzxz_no_extract(compiler: Compiler) -> None: + def test_full_bzxz_no_extract(self, compiler: Compiler) -> None: circuit = create_random_unitary_circ(5) utry = circuit.get_unitary() - bzxz = FullBlockZXZPass(min_qudit_size=2, perform_scan=False, - perform_extract=False) + bzxz = FullBlockZXZPass( + min_qudit_size=2, perform_scan=False, + perform_extract=False, + ) circuit = compiler.compile(circuit, [bzxz]) dist = circuit.get_unitary().get_distance_from(utry) print(dist) print(circuit.gate_counts) - assert dist <= 1e-5 \ No newline at end of file + assert dist <= 1e-5 From 779c8ab170b976d8866f7d70b2bf08c3fd02eadc Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 14:22:29 -0700 Subject: [PATCH 13/15] Renaming MCR to MPR --- bqskit/passes/synthesis/bzxz.py | 16 +++++----- bqskit/passes/synthesis/qsd.py | 54 ++++++++++++++++----------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/bqskit/passes/synthesis/bzxz.py b/bqskit/passes/synthesis/bzxz.py index 4f707f13..e5a33160 100644 --- a/bqskit/passes/synthesis/bzxz.py +++ b/bqskit/passes/synthesis/bzxz.py @@ -20,7 +20,7 @@ from bqskit.ir.gates.constant import HGate from bqskit.ir.gates.constant import IdentityGate from bqskit.ir.gates.constant import ZGate -from bqskit.ir.gates.parameterized.mcrz import MCRZGate +from bqskit.ir.gates.parameterized.mprz import MPRZGate from bqskit.ir.location import CircuitLocation from bqskit.ir.operation import Operation from bqskit.passes.processing.extract_diagonal import ExtractDiagonalPass @@ -232,7 +232,7 @@ def demultiplex( most significant qubit. We return the Unitary Matrices V and W and the parameters for the - corresponding MCRZ gate. + corresponding MPRZ gate. """ # U can be decomposed into U = (I otimes V )(D otimes D†)(I otimes W ) @@ -245,7 +245,7 @@ def demultiplex( # We can then multiply to solve for W W = D @ V.conj().T @ U_2 - # We can use d to find the parameters for the MCRZ gate. + # We can use d to find the parameters for the MPRZ gate. # Note than because and Rz does exp(-i * theta / 2), we must # multiply by -2. d_params: list[float] = list(np.angle(d) * -2) @@ -291,8 +291,8 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: wc_gate, wc_params = QSDPass.create_unitary_gate(WC) circ.append_gate(wc_gate, CircuitLocation(select_qubits), wc_params) - # Add decomposed MCRZ gate circuit. - # Since the MCRZ gate circuit sets the target qubit as qubit + # Add decomposed MPRZ gate circuit. + # Since the MPRZ gate circuit sets the target qubit as qubit # num_qudits - 1, we must shift the qubits to the left shifted_qubits = all_qubits[1:] + all_qubits[0:1] circ.append_circuit( @@ -313,9 +313,9 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: ) circ.append_gate(HGate(), CircuitLocation((controlled_qubit,))) - # The central MCRZ_gate. We set the target to the controlled qubit, + # The central MPRZ_gate. We set the target to the controlled qubit, # so there is no need to shift - z_gate = MCRZGate(len(all_qubits), 0) + z_gate = MPRZGate(len(all_qubits), 0) circ.append_gate(z_gate, CircuitLocation(all_qubits), BZ_params) # Now add the decomposed B-tilde gates VB and a Hadamard @@ -326,7 +326,7 @@ def zxz(orig_u: UnitaryMatrix) -> Circuit: ) circ.append_gate(HGate(), CircuitLocation((controlled_qubit,))) - # Add the decomposed MCRZ gate circuit again on shifted qubits + # Add the decomposed MPRZ gate circuit again on shifted qubits circ.append_circuit( MGDPass.decompose_mpx_two_levels( decompose_ry=False, diff --git a/bqskit/passes/synthesis/qsd.py b/bqskit/passes/synthesis/qsd.py index d37b18f3..55aeba11 100644 --- a/bqskit/passes/synthesis/qsd.py +++ b/bqskit/passes/synthesis/qsd.py @@ -19,8 +19,8 @@ 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.mpry import MPRYGate +from bqskit.ir.gates.parameterized.mprz import MPRZGate from bqskit.ir.operation import Operation from bqskit.passes.processing import ScanningGateRemovalPass from bqskit.passes.processing import TreeScanningGateRemovalPass @@ -116,7 +116,7 @@ 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 + A pass performing one round of decomposition of the MPRY and MPRZ gates in a circuit. References: @@ -132,10 +132,10 @@ class MGDPass(BasePass): def __init__(self, decompose_twice: bool = True) -> None: """ - The MGDPass decomposes all MCRY and MCRZ gates in a circuit. + The MGDPass decomposes all MPRY and MPRZ gates in a circuit. Args: - decompose_twice (bool): Whether to decompose the MCRZ gate twice. + decompose_twice (bool): Whether to decompose the MPRZ gate twice. This will save 2 CNOT gates in the decomposition. If false, the pass will only decompose one level. (Default: True) """ @@ -153,8 +153,8 @@ def decompose_mpx_one_level( Decompose Multiplexed Gate one level. Args: - params (RealVector): The parameters for the original MCRZ gate - num_qudits (int): The number of qudits in the MCRZ gate + params (RealVector): The parameters for the original MPRZ gate + num_qudits (int): The number of qudits in the MPRZ gate reverse (bool): Whether to reverse the order of the gates (you can decompose the gates in either order to get the same result) drop_last_cnot (bool): Whether to drop the last CNOT gate. This @@ -162,21 +162,21 @@ def decompose_mpx_one_level( CNOT gates. Returns: - Circuit: The circuit that decomposes the MCRZ gate + Circuit: The circuit that decomposes the MPRZ gate """ - new_gate: MCRZGate | MCRYGate | RZGate | RYGate = RZGate() + new_gate: MPRZGate | MPRYGate | RZGate | RYGate = RZGate() if decompose_ry: new_gate = RYGate() if (num_qudits >= 3): if decompose_ry: - new_gate = MCRYGate(num_qudits - 1, num_qudits - 2) + new_gate = MPRYGate(num_qudits - 1, num_qudits - 2) else: # Remove 1 qubit, last qubit is controlled - new_gate = MCRZGate(num_qudits - 1, num_qudits - 2) + new_gate = MPRZGate(num_qudits - 1, num_qudits - 2) - left_params, right_params = MCRYGate.get_decomposition(params) + left_params, right_params = MPRYGate.get_decomposition(params) circ = Circuit(num_qudits) new_gate_location = list(range(1, num_qudits)) cx_location = (0, num_qudits - 1) @@ -218,16 +218,16 @@ def decompose_mpx_two_levels( https://arxiv.org/pdf/2403.13692v1.pdf. Args: - decompose_ry (bool): Whether to decompose the MCRY gate - params (RealVector): The parameters for the original MCR gate - num_qudits (int): The number of qudits in the MCR gate + decompose_ry (bool): Whether to decompose the MPRY gate + params (RealVector): The parameters for the original MPR gate + num_qudits (int): The number of qudits in the MPR gate reverse (bool): Whether to reverse the order of the gates (you can decompose the gates in either order to get the same result) drop_last_cnot (bool): Whether to drop the last CNOT gate (only should be set to True if you are doing section 5.2 optimization) Returns: - Circuit: The circuit that decomposes the MCR gate + Circuit: The circuit that decomposes the MPR gate """ if num_qudits <= 2: @@ -239,10 +239,10 @@ def decompose_mpx_two_levels( reverse, ) - # Get params for first decomposition of the MCRZ gate - left_params, right_params = MCRYGate.get_decomposition(params) + # Get params for first decomposition of the MPRZ gate + left_params, right_params = MPRYGate.get_decomposition(params) - # Decompose the MCRZ gate into 2 MCRZ gates, dropping the last CNOT + # Decompose the MPRZ gate into 2 MPRZ gates, dropping the last CNOT # Also Reverse the circuit for the right side in order to do the # optimization in section 5.2 circ_left = MGDPass.decompose_mpx_one_level( @@ -262,7 +262,7 @@ def decompose_mpx_two_levels( ) # Now, construct the circuit. - # This will generate the original MCRZ gate with the target qubit + # This will generate the original MPRZ gate with the target qubit # set as qubit num_qudits - 1 circ = Circuit(num_qudits) cx_location_big = (0, num_qudits - 1) @@ -290,7 +290,7 @@ def decompose_mpx_two_levels( return circ async def run(self, circuit: Circuit, data: PassData) -> None: - """Decompose all MCRY and MCRZ gates in the circuit one level.""" + """Decompose all MPRY and MPRZ gates in the circuit one level.""" ops: list[Operation] = [] pts: list[CircuitPoint] = [] locations: list[CircuitLocation] = [] @@ -298,7 +298,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None: # Gather all of the multiplexed operations for cyc, op in all_ops: - if isinstance(op.gate, MCRYGate) or isinstance(op.gate, MCRZGate): + if isinstance(op.gate, MPRYGate) or isinstance(op.gate, MPRZGate): ops.append(op) pts.append(CircuitPoint((cyc, op.location[0]))) # Adjust location based on current target, move target to last @@ -316,7 +316,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None: if self.decompose_twice: circs = [ MGDPass.decompose_mpx_two_levels( - isinstance(op.gate, MCRYGate), + isinstance(op.gate, MPRYGate), op.params, op.num_qudits, ) for op in ops @@ -324,7 +324,7 @@ async def run(self, circuit: Circuit, data: PassData) -> None: else: circs = [ MGDPass.decompose_mpx_one_level( - isinstance(op.gate, MCRYGate), + isinstance(op.gate, MPRYGate), op.params, op.num_qudits, ) for op in ops @@ -438,7 +438,7 @@ def create_multiplexed_circ( # 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) + z_gate = MPRZGate(len(all_qubits), u1.num_qudits) # Create right gate right_gate, right_params = QSDPass.create_unitary_gate(UnitaryMatrix(V)) @@ -482,7 +482,7 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: assert (len(theta_y) == u.shape[0] / 2) # Create the multiplexed circuit - # This generates 2 circuits that multipex U,V with an MCRY gate + # This generates 2 circuits that multipex U,V with an MPRY gate controlled_qubit = u.num_qudits - 1 select_qubits = list(range(0, u.num_qudits - 1)) all_qubits = list(range(u.num_qudits)) @@ -498,7 +498,7 @@ def qsd(orig_u: UnitaryMatrix) -> Circuit: ], select_qubits, ) - gate_2 = MCRYGate(u.num_qudits, controlled_qubit) + gate_2 = MPRYGate(u.num_qudits, controlled_qubit) circ_1.append_gate(gate_2, CircuitLocation(all_qubits), 2 * theta_y) circ_1.append_circuit( From 6e024147ea48600137ecc91adc24d15d0ee32b0c Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 15:54:00 -0700 Subject: [PATCH 14/15] Fixing Grad calc --- bqskit/ir/gates/parameterized/diagonal.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bqskit/ir/gates/parameterized/diagonal.py b/bqskit/ir/gates/parameterized/diagonal.py index 63bb40b9..8518edc9 100644 --- a/bqskit/ir/gates/parameterized/diagonal.py +++ b/bqskit/ir/gates/parameterized/diagonal.py @@ -51,16 +51,17 @@ def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: """ self.check_parameters(params) - mat = np.eye(2 ** self.num_qudits, dtype=np.complex128) + grad = np.zeros( + ( + len(params), 2 ** self.num_qudits, + 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]) + for i, ind in enumerate(range(1, 2 ** self.num_qudits)): + grad[ind][i][i] = 1j * np.exp(1j * params[ind]) - return np.array( - [ - mat, - ], dtype=np.complex128, - ) + return grad def optimize(self, env_matrix: npt.NDArray[np.complex128]) -> list[float]: """ From 63a6795155eac401bb0b0e27123afa81e77e07d2 Mon Sep 17 00:00:00 2001 From: Justin Kalloor Date: Fri, 27 Sep 2024 16:01:46 -0700 Subject: [PATCH 15/15] Fixing grad --- bqskit/ir/gates/parameterized/diagonal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bqskit/ir/gates/parameterized/diagonal.py b/bqskit/ir/gates/parameterized/diagonal.py index 8518edc9..e83f3006 100644 --- a/bqskit/ir/gates/parameterized/diagonal.py +++ b/bqskit/ir/gates/parameterized/diagonal.py @@ -59,7 +59,7 @@ def get_grad(self, params: RealVector = []) -> npt.NDArray[np.complex128]: ) for i, ind in enumerate(range(1, 2 ** self.num_qudits)): - grad[ind][i][i] = 1j * np.exp(1j * params[ind]) + grad[i][ind][ind] = 1j * np.exp(1j * params[i]) return grad