From adf8c5174642a73f88e9b589f38c64cae3d25097 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Mon, 4 Nov 2024 15:15:10 +0000 Subject: [PATCH 01/10] preliminary support for externally linked gates --- pyqasm/visitor.py | 64 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 3af87331..3e48ebfa 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -52,7 +52,7 @@ class QasmVisitor: record_output (bool): If True, output of the circuit will be recorded. Defaults to True. """ - def __init__(self, module, check_only: bool = False): + def __init__(self, module, check_only: bool = False, external_gates: list[str] | None = None): self._module = module self._scope: deque = deque([{}]) self._context: deque = deque([Context.GLOBAL]) @@ -65,6 +65,7 @@ def __init__(self, module, check_only: bool = False): self._function_qreg_transform_map: deque = deque([]) # for nested functions self._global_creg_size_map: dict[str, int] = {} self._custom_gates: dict[str, qasm3_ast.QuantumGateDefinition] = {} + self._external_gates: list[str] = [] if external_gates is None else external_gates self._subroutine_defns: dict[str, qasm3_ast.SubroutineDefinition] = {} self._check_only: bool = check_only self._curr_scope: int = 0 @@ -757,6 +758,63 @@ def _visit_custom_gate_operation( return [] return result + + def _visit_external_gate_operation( + self, operation: qasm3_ast.QuantumGate, inverse: bool = False + ) -> list[qasm3_ast.QuantumGate]: + """Visit an external gate operation element. + + Args: + operation (qasm3_ast.QuantumGate): The external gate operation to visit. + inverse (bool): Whether the operation is an inverse operation. Defaults to False. + + If True, the gate operation is applied in reverse order and the + inverse modifier is appended to each gate call. + See https://openqasm.com/language/gates.html#inverse-modifier + for more clarity. + + Returns: + list[qasm3_ast.QuantumGate]: The quantum gate that was collected. + """ + + logger.debug("Visiting custom gate operation '%s'", str(operation)) + gate_name: str = operation.name.name + + gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] + op_qubits: list[qasm3_ast.IndexedIdentifier] = ( + self._get_op_bits( # type: ignore [assignment] + operation, + self._global_qreg_size_map, + ) + ) + Qasm3Validator.validate_gate_call(operation, gate_definition, len(op_qubits)) + + op_parameters = [] + if len(operation.arguments) > 0: # parametric gate + op_parameters = [qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation)] + #if inverse_action == InversionOp.INVERT_ROTATION: + # op_parameters = [-1 * param for param in op_parameters] + + self._push_context(Context.GATE) + + modifiers = [] + if inverse: + modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)] + + external_gate = qasm3_ast.QuantumGate( + modifiers=modifiers, + name=qasm3_ast.Identifier(gate_name), + qubits=op_qubits, + arguments=op_parameters + ) + + result = [external_gate] + + self._restore_context() + if self._check_only: + return [] + + return result def _collapse_gate_modifiers(self, operation: qasm3_ast.QuantumGate) -> tuple: """Collapse the gate modifiers of a gate operation. @@ -821,7 +879,9 @@ def _visit_generic_gate_operation( # apply the power first and then inverting the result result = [] for _ in range(power_value): - if operation.name.name in self._custom_gates: + if operation.name.name in self._external_gates: + result.extend(self._visit_external_gate_operation(operation, inverse_value)) + elif operation.name.name in self._custom_gates: result.extend(self._visit_custom_gate_operation(operation, inverse_value)) else: result.extend(self._visit_basic_gate_operation(operation, inverse_value)) From 232493788f4b2deee3ef88441994227bd5cedecb Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Tue, 5 Nov 2024 10:25:20 +0000 Subject: [PATCH 02/10] add docstring and error messages --- pyqasm/visitor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 3e48ebfa..c154e33c 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -50,6 +50,7 @@ class QasmVisitor: Args: initialize_runtime (bool): If True, quantum runtime will be initialized. Defaults to True. record_output (bool): If True, output of the circuit will be recorded. Defaults to True. + external_gates (list[str]): List of custom gates that should not be unrolled into their definition. """ def __init__(self, module, check_only: bool = False, external_gates: list[str] | None = None): @@ -780,6 +781,12 @@ def _visit_external_gate_operation( logger.debug("Visiting custom gate operation '%s'", str(operation)) gate_name: str = operation.name.name + if gate_name not in self._custom_gates: + raise_qasm3_error( + f"Gate '{gate_name}' is not a custom gate and can therefore not be marked as external. ", + span=operation.span + ) + gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] op_qubits: list[qasm3_ast.IndexedIdentifier] = ( self._get_op_bits( # type: ignore [assignment] @@ -791,9 +798,7 @@ def _visit_external_gate_operation( op_parameters = [] if len(operation.arguments) > 0: # parametric gate - op_parameters = [qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation)] - #if inverse_action == InversionOp.INVERT_ROTATION: - # op_parameters = [-1 * param for param in op_parameters] + op_parameters = [qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation)] self._push_context(Context.GATE) From 8723c060d74f5c8d90a39285f7f70b2eecfd898c Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Tue, 5 Nov 2024 10:51:42 +0000 Subject: [PATCH 03/10] add tests --- tests/qasm3/test_gates.py | 14 ++++++++++++++ tests/utils.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index c170fc12..76255a6d 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -27,6 +27,7 @@ ) from tests.utils import ( check_custom_qasm_gate_op, + check_custom_qasm_gate_op_with_external_gates, check_single_qubit_gate_op, check_single_qubit_rotation_op, check_three_qubit_gate_op, @@ -173,6 +174,19 @@ def test_custom_ops(test_name, request): # Check for custom gate definition check_custom_qasm_gate_op(result.unrolled_ast, gate_type) +@pytest.mark.parametrize("test_name", custom_op_tests) +def test_custom_ops_with_external_gates(test_name, request): + qasm3_string = request.getfixturevalue(test_name) + gate_type = test_name.removeprefix("Fixture_") + result = load(qasm3_string) + result.unroll(external_gates=["custom", "custom1"]) + + assert result.num_qubits == 2 + assert result.num_clbits == 0 + + # Check for custom gate definition + check_custom_qasm_gate_op_with_external_gates(result.unrolled_ast, gate_type) + def test_pow_gate_modifier(): qasm3_string = """ diff --git a/tests/utils.py b/tests/utils.py index 7c4d117f..1dd3adda 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -233,3 +233,17 @@ def check_custom_qasm_gate_op(unrolled_ast, test_type): if test_type not in test_function_map: raise ValueError(f"Unknown test type {test_type}") test_function_map[test_type](unrolled_ast) + +def check_custom_qasm_gate_op_with_external_gates(unrolled_ast, test_type): + if test_type == "simple": + check_two_qubit_gate_op(unrolled_ast, 1, [(0, 1)], "custom") + elif test_type == "nested": + check_two_qubit_gate_op(unrolled_ast, 1, [(0, 1)], "custom") + elif test_type == "complex": + #Only custom1 is external, custom2 and custom3 should be unrolled + check_single_qubit_gate_op(unrolled_ast, 1, [0], "custom1") + check_single_qubit_gate_op(unrolled_ast, 1, [0], "ry") + check_single_qubit_gate_op(unrolled_ast, 1, [0], "rz") + check_two_qubit_gate_op(unrolled_ast, 1, [[0, 1]], "cx") + else: + raise ValueError(f"Unknown test type {test_type}") \ No newline at end of file From 5bed1491a2b1d3a6b24cc7363cd699282981d244 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Tue, 5 Nov 2024 11:27:15 +0000 Subject: [PATCH 04/10] format code --- pyqasm/visitor.py | 22 ++++++++++++---------- tests/qasm3/test_gates.py | 1 + tests/utils.py | 5 +++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index e87b9ce4..a494ef65 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -50,7 +50,7 @@ class QasmVisitor: Args: initialize_runtime (bool): If True, quantum runtime will be initialized. Defaults to True. record_output (bool): If True, output of the circuit will be recorded. Defaults to True. - external_gates (list[str]): List of custom gates that should not be unrolled into their definition. + external_gates (list[str]): List of custom gates that should not be unrolled. """ def __init__(self, module, check_only: bool = False, external_gates: list[str] | None = None): @@ -760,7 +760,7 @@ def _visit_custom_gate_operation( return [] return result - + def _visit_external_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False ) -> list[qasm3_ast.QuantumGate]: @@ -778,14 +778,14 @@ def _visit_external_gate_operation( Returns: list[qasm3_ast.QuantumGate]: The quantum gate that was collected. """ - + logger.debug("Visiting custom gate operation '%s'", str(operation)) gate_name: str = operation.name.name if gate_name not in self._custom_gates: raise_qasm3_error( - f"Gate '{gate_name}' is not a custom gate and can therefore not be marked as external. ", - span=operation.span + f"Gate '{gate_name}' is not a custom gate and can not be marked as external.", + span=operation.span, ) gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] @@ -799,19 +799,21 @@ def _visit_external_gate_operation( op_parameters = [] if len(operation.arguments) > 0: # parametric gate - op_parameters = [qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation)] + op_parameters = [ + qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation) + ] self._push_context(Context.GATE) - + modifiers = [] if inverse: modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)] - + external_gate = qasm3_ast.QuantumGate( - modifiers=modifiers, + modifiers=modifiers, name=qasm3_ast.Identifier(gate_name), qubits=op_qubits, - arguments=op_parameters + arguments=op_parameters, ) result = [external_gate] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 76255a6d..b0d3e2c6 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -174,6 +174,7 @@ def test_custom_ops(test_name, request): # Check for custom gate definition check_custom_qasm_gate_op(result.unrolled_ast, gate_type) + @pytest.mark.parametrize("test_name", custom_op_tests) def test_custom_ops_with_external_gates(test_name, request): qasm3_string = request.getfixturevalue(test_name) diff --git a/tests/utils.py b/tests/utils.py index 1dd3adda..21ee7b45 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -234,16 +234,17 @@ def check_custom_qasm_gate_op(unrolled_ast, test_type): raise ValueError(f"Unknown test type {test_type}") test_function_map[test_type](unrolled_ast) + def check_custom_qasm_gate_op_with_external_gates(unrolled_ast, test_type): if test_type == "simple": check_two_qubit_gate_op(unrolled_ast, 1, [(0, 1)], "custom") elif test_type == "nested": check_two_qubit_gate_op(unrolled_ast, 1, [(0, 1)], "custom") elif test_type == "complex": - #Only custom1 is external, custom2 and custom3 should be unrolled + # Only custom1 is external, custom2 and custom3 should be unrolled check_single_qubit_gate_op(unrolled_ast, 1, [0], "custom1") check_single_qubit_gate_op(unrolled_ast, 1, [0], "ry") check_single_qubit_gate_op(unrolled_ast, 1, [0], "rz") check_two_qubit_gate_op(unrolled_ast, 1, [[0, 1]], "cx") else: - raise ValueError(f"Unknown test type {test_type}") \ No newline at end of file + raise ValueError(f"Unknown test type {test_type}") From d7668b349998c7006a0fd09750e50874d211ecb1 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Thu, 7 Nov 2024 13:14:04 +0000 Subject: [PATCH 05/10] allow all gates to be marked as external --- pyqasm/visitor.py | 17 ++++++++--------- tests/qasm3/test_gates.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 5e752245..1de7ea53 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -779,23 +779,22 @@ def _visit_external_gate_operation( list[qasm3_ast.QuantumGate]: The quantum gate that was collected. """ - logger.debug("Visiting custom gate operation '%s'", str(operation)) + logger.debug("Visiting external gate operation '%s'", str(operation)) gate_name: str = operation.name.name - if gate_name not in self._custom_gates: - raise_qasm3_error( - f"Gate '{gate_name}' is not a custom gate and can not be marked as external.", - span=operation.span, - ) - - gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] op_qubits: list[qasm3_ast.IndexedIdentifier] = ( self._get_op_bits( # type: ignore [assignment] operation, self._global_qreg_size_map, ) ) - Qasm3Validator.validate_gate_call(operation, gate_definition, len(op_qubits)) + + if gate_name in self._custom_gates: + # Ignore result, this is just for validation + self._visit_custom_gate_operation(operation, inverse=inverse) + else: + # Ignore result, this is just for validation + self._visit_basic_gate_operation(operation, inverse=inverse) op_parameters = [] if len(operation.arguments) > 0: # parametric gate diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index b0d3e2c6..b486d7e3 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -139,6 +139,21 @@ def test_qasm_u3_gates(): check_single_qubit_rotation_op(result.unrolled_ast, 1, [0], [0.5, 0.5, 0.5], "u3") +def test_qasm_u3_gates_external(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u3(0.5, 0.5, 0.5) q1[0]; + """ + result = load(qasm3_string) + result.unroll(external_gates=["u3"]) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "u3") + + def test_qasm_u2_gates(): qasm3_string = """ OPENQASM 3; From ce2aec4191513ebe1f048179c01495cbfee61d10 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Thu, 7 Nov 2024 13:38:31 +0000 Subject: [PATCH 06/10] make linter happy --- pyqasm/visitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 1de7ea53..8a4d699d 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -811,8 +811,8 @@ def _visit_external_gate_operation( external_gate = qasm3_ast.QuantumGate( modifiers=modifiers, name=qasm3_ast.Identifier(gate_name), - qubits=op_qubits, - arguments=op_parameters, + qubits=list(op_qubits), + arguments=list(op_parameters), ) result = [external_gate] From 7a03d494ec91d2229b45e451537b24dc3502c031 Mon Sep 17 00:00:00 2001 From: Sola85 Date: Mon, 11 Nov 2024 09:23:49 +0100 Subject: [PATCH 07/10] Update docstring Co-authored-by: Harshit Gupta --- pyqasm/visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 8a4d699d..e4059444 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -50,7 +50,7 @@ class QasmVisitor: Args: initialize_runtime (bool): If True, quantum runtime will be initialized. Defaults to True. record_output (bool): If True, output of the circuit will be recorded. Defaults to True. - external_gates (list[str]): List of custom gates that should not be unrolled. + external_gates (list[str]): List of gates that should not be unrolled. """ def __init__(self, module, check_only: bool = False, external_gates: list[str] | None = None): From 3d2c64ee516446a4a7cff7b74d375ccb8b283f26 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Mon, 11 Nov 2024 14:03:22 +0000 Subject: [PATCH 08/10] fix broadcasting for external gates --- pyqasm/visitor.py | 145 +++++++++++++++++++++------------ tests/qasm3/resources/gates.py | 2 +- tests/qasm3/test_depth.py | 21 +++++ tests/qasm3/test_gates.py | 15 ++++ 4 files changed, 130 insertions(+), 53 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 8a4d699d..eb65cbe2 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -17,7 +17,8 @@ import copy import logging from collections import deque -from typing import Any, Optional, Union +from functools import partial +from typing import Any, Callable, Optional, Union import numpy as np import openqasm3.ast as qasm3_ast @@ -604,6 +605,71 @@ def _visit_gate_definition(self, definition: qasm3_ast.QuantumGateDefinition) -> return [] + def _unroll_multiple_target_qubits( + self, operation: qasm3_ast.QuantumGate, gate_qubit_count: int + ) -> list[list[qasm3_ast.IndexedIdentifier]]: + """Unroll the complete list of all qubits that the given operation is applied to. + E.g. this maps 'cx q[0], q[1], q[2], q[3]' to [[q[0], q[1]], [q[2], q[3]]] + + Args: + operation (qasm3_ast.QuantumGate): The gate to be applied. + gate_qubit_count (list[int]): The number of qubits that a single gate acts on. + + Returns: + The list of all targets that the unrolled gate should act on. + """ + op_qubits = self._get_op_bits(operation, self._global_qreg_size_map) + if len(op_qubits) % gate_qubit_count != 0: + raise_qasm3_error( + f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}", + span=operation.span, + ) + qubit_subsets = [] + for i in range(0, len(op_qubits), gate_qubit_count): + # we apply the gate on the qubit subset linearly + qubit_subsets.append(op_qubits[i : i + gate_qubit_count]) + return qubit_subsets + + def _broadcast_gate_operation( + self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]] + ) -> list[qasm3_ast.QuantumGate]: + """Broadcasts the application of a gate onto multiple sets of target qubits. + + Args: + gate_function (callable): The gate that should be applied to multiple target qubits + all_targets (list[list[qasm3_ast.IndexedIdentifier]]): + The list of target of target qubits. + The length of this list indicates the number of time the gate is invoked. + Returns: + List of all executed gates. + """ + result = [] + for targets in all_targets: + result.extend(gate_function(*targets)) + return result + + def _update_qubit_depth(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]): + """Updates the depth of the circuit after applying a broadcasted gate. + + Args: + all_targes: The list of qubits on which a gate was just added. + + Returns: + None + """ + for qubit_subset in all_targets: + max_involved_depth = 0 + for qubit in qubit_subset: + qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value # type: ignore + qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] + qubit_node.num_gates += 1 + max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) + + for qubit in qubit_subset: + qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value # type: ignore + qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] + qubit_node.depth = max_involved_depth + def _visit_basic_gate_operation( # pylint: disable=too-many-locals self, operation: qasm3_ast.QuantumGate, inverse: bool = False ) -> list[qasm3_ast.QuantumGate]: @@ -629,9 +695,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ValidationError: If the number of qubits is invalid. """ - logger.debug("Visiting basic gate operation '%s'", str(operation)) - op_qubits = self._get_op_bits(operation, self._global_qreg_size_map) inverse_action = None if not inverse: qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) @@ -641,46 +705,24 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals operation.name.name ) - op_parameters = None - - if len(op_qubits) % op_qubit_count != 0: - raise_qasm3_error( - f"Invalid number of qubits {len(op_qubits)} for operation {operation.name.name}", - span=operation.span, - ) + op_parameters = [] if len(operation.arguments) > 0: # parametric gate op_parameters = self._get_op_parameters(operation) if inverse_action == InversionOp.INVERT_ROTATION: op_parameters = [-1 * param for param in op_parameters] - if self._check_only: - return [] - result = [] - for i in range(0, len(op_qubits), op_qubit_count): - # we apply the gate on the qubit subset linearly - qubit_subset = op_qubits[i : i + op_qubit_count] - unrolled_gate = [] - if op_parameters is not None: - unrolled_gate = qasm_func(*op_parameters, *qubit_subset) - else: - unrolled_gate = qasm_func(*qubit_subset) - result.extend(unrolled_gate) - # update qubit depths - max_involved_depth = 0 - for qubit in qubit_subset: - qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value # type: ignore - qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] - qubit_node.num_gates += 1 - max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) + result = [] - for qubit in qubit_subset: - qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value # type: ignore - qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] - qubit_node.depth = max_involved_depth + unrolled_targets = self._unroll_multiple_target_qubits(operation, op_qubit_count) + unrolled_gate_function = partial(qasm_func, *op_parameters) + result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets)) if self._check_only: return [] + + self._update_qubit_depth(unrolled_targets) + return result def _visit_custom_gate_operation( @@ -782,25 +824,20 @@ def _visit_external_gate_operation( logger.debug("Visiting external gate operation '%s'", str(operation)) gate_name: str = operation.name.name - op_qubits: list[qasm3_ast.IndexedIdentifier] = ( - self._get_op_bits( # type: ignore [assignment] - operation, - self._global_qreg_size_map, - ) - ) - if gate_name in self._custom_gates: # Ignore result, this is just for validation self._visit_custom_gate_operation(operation, inverse=inverse) + # Don't need to check if custom gate exists, since we just validated the call + gate_qubit_count = len(self._custom_gates[gate_name].qubits) else: # Ignore result, this is just for validation self._visit_basic_gate_operation(operation, inverse=inverse) + # Don't need to check if custom gate exists, since we just validated the call + _, gate_qubit_count = map_qasm_op_to_callable(operation.name.name) - op_parameters = [] - if len(operation.arguments) > 0: # parametric gate - op_parameters = [ - qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation) - ] + op_parameters = [ + qasm3_ast.FloatLiteral(param) for param in self._get_op_parameters(operation) + ] self._push_context(Context.GATE) @@ -808,14 +845,18 @@ def _visit_external_gate_operation( if inverse: modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)] - external_gate = qasm3_ast.QuantumGate( - modifiers=modifiers, - name=qasm3_ast.Identifier(gate_name), - qubits=list(op_qubits), - arguments=list(op_parameters), - ) + def gate_function(*qubits): + return [ + qasm3_ast.QuantumGate( + modifiers=modifiers, + name=qasm3_ast.Identifier(gate_name), + qubits=list(qubits), + arguments=list(op_parameters), + ) + ] - result = [external_gate] + all_targets = self._unroll_multiple_target_qubits(operation, gate_qubit_count) + result = self._broadcast_gate_operation(gate_function, all_targets) self._restore_context() if self._check_only: diff --git a/tests/qasm3/resources/gates.py b/tests/qasm3/resources/gates.py index 412a8551..5e6791db 100644 --- a/tests/qasm3/resources/gates.py +++ b/tests/qasm3/resources/gates.py @@ -262,7 +262,7 @@ def test_fixture(): qubit[3] q1; cx q1; // invalid application of gate, as we apply it to 3 qubits in blocks of 2 """, - "Invalid number of qubits 3 for operation .*", + "Invalid number of qubits 3 for operation cx", ), "unsupported_parameter_type": ( """ diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index edb746e0..666e152c 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -47,6 +47,27 @@ def test_gate_depth(): assert result.depth() == 5 +@pytest.mark.skip(reason="Not implemented computing depth of external gates") +def test_gate_depth_external_function(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + gate my_gate() q { + h q; + x q; + } + + qubit q; + my_gate() q; + """ + result = load(qasm3_string) + result.unroll(external_gates=["my_gate"]) + assert result.num_qubits == 1 + assert result.num_clbits == 0 + assert result.depth() == 1 + + def test_pow_gate_depth(): qasm3_string = """ OPENQASM 3; diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index b486d7e3..76cc001b 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -154,6 +154,21 @@ def test_qasm_u3_gates_external(): check_single_qubit_gate_op(result.unrolled_ast, 1, [0], "u3") +def test_qasm_u3_gates_external_with_multiple_qubits(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + + qubit[2] q1; + u3(0.5, 0.5, 0.5) q1; + """ + result = load(qasm3_string) + result.unroll(external_gates=["u3"]) + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 1], "u3") + + def test_qasm_u2_gates(): qasm3_string = """ OPENQASM 3; From c84a5021dfca8c60d4cd3b9e2f5caac267998fab Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Mon, 11 Nov 2024 14:18:10 +0000 Subject: [PATCH 09/10] fix new tests --- pyqasm/visitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 07918bda..6fc9b169 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -724,11 +724,11 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals unrolled_gate_function = partial(qasm_func, *op_parameters) result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets)) + self._update_qubit_depth(unrolled_targets) + if self._check_only: return [] - self._update_qubit_depth(unrolled_targets) - return result def _visit_custom_gate_operation( From 70e40e9ee0412bd0dc17b856a407d69c426db2c3 Mon Sep 17 00:00:00 2001 From: Tobias Schmale Date: Tue, 12 Nov 2024 11:57:46 +0000 Subject: [PATCH 10/10] incorporate minor feedback --- pyqasm/visitor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqasm/visitor.py b/pyqasm/visitor.py index 6fc9b169..090cbdc3 100644 --- a/pyqasm/visitor.py +++ b/pyqasm/visitor.py @@ -642,7 +642,9 @@ def _broadcast_gate_operation( """Broadcasts the application of a gate onto multiple sets of target qubits. Args: - gate_function (callable): The gate that should be applied to multiple target qubits + gate_function (callable): The gate that should be applied to multiple target qubits. + (All arguments of the callable should be qubits, i.e. all non-qubit arguments of the + gate should already be evaluated, e.g. using functools.partial). all_targets (list[list[qasm3_ast.IndexedIdentifier]]): The list of target of target qubits. The length of this list indicates the number of time the gate is invoked. @@ -654,7 +656,7 @@ def _broadcast_gate_operation( result.extend(gate_function(*targets)) return result - def _update_qubit_depth(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]): + def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]): """Updates the depth of the circuit after applying a broadcasted gate. Args: @@ -724,7 +726,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals unrolled_gate_function = partial(qasm_func, *op_parameters) result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets)) - self._update_qubit_depth(unrolled_targets) + self._update_qubit_depth_for_gate(unrolled_targets) if self._check_only: return [] @@ -839,7 +841,7 @@ def _visit_external_gate_operation( else: # Ignore result, this is just for validation self._visit_basic_gate_operation(operation, inverse=inverse) - # Don't need to check if custom gate exists, since we just validated the call + # Don't need to check if basic gate exists, since we just validated the call _, gate_qubit_count = map_qasm_op_to_callable(operation.name.name) op_parameters = [