From f734f0f0082fb3dfba8ac7734dc5a19adf3bfa95 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 9 Jan 2025 12:04:06 -0800 Subject: [PATCH 01/20] simplify pow/inv collapse --- src/pyqasm/visitor.py | 10 ++++------ tests/qasm3/test_gates.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 7267c77f..194664d1 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -933,17 +933,15 @@ def _collapse_gate_modifiers( Returns: tuple[Any, Any]: The power and inverse values of the gate operation. """ - power_value, inverse_value = 1, False + exponent = 1 for modifier in operation.modifiers: modifier_name = modifier.modifier if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] - if current_power < 0: - inverse_value = not inverse_value - power_value = power_value * abs(current_power) + exponent *= current_power elif modifier_name == qasm3_ast.GateModifierName.inv: - inverse_value = not inverse_value + exponent *= -1 elif modifier_name in [ qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl, @@ -953,7 +951,7 @@ def _collapse_gate_modifiers( err_type=NotImplementedError, span=operation.span, ) - return (power_value, inverse_value) + return (abs(exponent), exponent < 0) def _visit_generic_gate_operation( self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 048b1c11..ccc5f060 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -370,6 +370,19 @@ def test_inv_gate_modifier(): check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 0, 1]], "ccx") +def test_ctrl_gate_modifier(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + ctrl @ x q; + """ + result = loads(qasm3_string) + result.unroll() + print(result) + assert False + + def test_nested_gate_modifiers(): qasm3_string = """ OPENQASM 3; From 548431497dc6c486b35955364e3bfbdbecee2fa0 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 03:05:50 +0000 Subject: [PATCH 02/20] basic ctrl modifiers --- src/pyqasm/maps.py | 37 +++++++++ src/pyqasm/visitor.py | 158 +++++++++++++++++++++++--------------- tests/qasm3/test_gates.py | 33 ++++---- 3 files changed, 149 insertions(+), 79 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index 89752a72..ad22af2e 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1236,6 +1236,43 @@ def map_qasm_inv_op_to_callable(op_name: str): raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") +CTRL_GATE_MAP = { + "x": "cx", + "y": "cy", + "z": "cz", + "rx": "crx", + "ry": "cry", + "rz": "crz", + "p": "cp", + "h": "ch", + "u": "cu", + "swap": "cswap", + "cx": "ccx" +} + + +def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): + """ + Map a controlled QASM operation to a callable. + """ + + ctrl_op_name = op_name + while ctrl_count > 0 and ctrl_op_name in CTRL_GATE_MAP: + ctrl_op_name = CTRL_GATE_MAP[ctrl_op_name] + ctrl_count -= 1 + + if ctrl_count == 0: + if ctrl_op_name in ONE_QUBIT_OP_MAP: + return ONE_QUBIT_OP_MAP[ctrl_op_name], 1 + if ctrl_op_name in TWO_QUBIT_OP_MAP: + return TWO_QUBIT_OP_MAP[ctrl_op_name], 2 + if ctrl_op_name in THREE_QUBIT_OP_MAP: + return THREE_QUBIT_OP_MAP[ctrl_op_name], 3 + + # TODO: decompose controls if not built in + raise ValidationError(f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controlls") + + # pylint: disable=inconsistent-return-statements def qasm_variable_type_cast(openqasm_type, var_name, base_size, rhs_value): """Cast the variable type to the type to match, if possible. diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 194664d1..ddbd620f 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -32,6 +32,7 @@ CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS, SWITCH_BLACKLIST_STMTS, + map_qasm_ctrl_op_to_callable, map_qasm_inv_op_to_callable, map_qasm_op_to_callable, ) @@ -647,7 +648,8 @@ def _unroll_multiple_target_qubits( return qubit_subsets def _broadcast_gate_operation( - self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]] + self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumGate]: """Broadcasts the application of a gate onto multiple sets of target qubits. @@ -663,7 +665,7 @@ def _broadcast_gate_operation( """ result = [] for targets in all_targets: - result.extend(gate_function(*targets)) + result.extend(gate_function(*ctrls, *targets)) return result def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]): @@ -689,7 +691,9 @@ def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedI qubit_node.depth = max_involved_depth def _visit_basic_gate_operation( # pylint: disable=too-many-locals - self, operation: qasm3_ast.QuantumGate, inverse: bool = False + self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + ctrls: list[qasm3_ast.IndexedIdentifier] = [], + negctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -713,10 +717,19 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ValidationError: If the number of qubits is invalid. """ + print("HII") + print(inverse, negctrls, ctrls, operation) logger.debug("Visiting basic gate operation '%s'", str(operation)) inverse_action = None if not inverse: - qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) + if len(negctrls) > 0: + raise_qasm3_error(f"Negctrl is an unsupported in gate operation: {operation}", err_type=NotImplementedError, span=operation.span) + + if len(ctrls) > 0: + qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable(operation.name.name, len(ctrls)) + op_qubit_count = op_qubit_total_count - len(ctrls) + else: + qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) else: # in basic gates, inverse action only affects the rotation gates qasm_func, op_qubit_count, inverse_action = map_qasm_inv_op_to_callable( @@ -729,12 +742,19 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals op_parameters = self._get_op_parameters(operation) if inverse_action == InversionOp.INVERT_ROTATION: op_parameters = [-1 * param for param in op_parameters] - + result = [] - 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 inverse: + result.extend([ + g2 + for g in self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, []) + for g2 in self._visit_basic_gate_operation(g, False, ctrls, negctrls) + ]) + else: + result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls + negctrls)) self._update_qubit_depth_for_gate(unrolled_targets) @@ -744,7 +764,9 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals return result def _visit_custom_gate_operation( - self, operation: qasm3_ast.QuantumGate, inverse: bool = False + self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + ctrls: list[qasm3_ast.IndexedIdentifier] = [], + negctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -808,7 +830,7 @@ def _visit_custom_gate_operation( gate_op_copy.modifiers.append( qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None) ) - result.extend(self._visit_generic_gate_operation(gate_op_copy)) + result.extend(self._visit_generic_gate_operation(gate_op_copy, ctrls, negctrls)) else: # TODO: add control flow support raise_qasm3_error( @@ -823,7 +845,9 @@ def _visit_custom_gate_operation( return result def _visit_external_gate_operation( - self, operation: qasm3_ast.QuantumGate, inverse: bool = False + self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + ctrls: list[qasm3_ast.IndexedIdentifier] = [], + negctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -845,12 +869,12 @@ def _visit_external_gate_operation( if gate_name in self._custom_gates: # Ignore result, this is just for validation - self._visit_custom_gate_operation(operation, inverse=inverse) + self._visit_custom_gate_operation(operation, inverse, ctrls, negctrls) # 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) + self._visit_basic_gate_operation(operation, inverse, ctrls, negctrls) # 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) @@ -884,7 +908,9 @@ def gate_function(*qubits): return result def _visit_phase_operation( - self, operation: qasm3_ast.QuantumPhase, inverse: bool = False + self, operation: qasm3_ast.QuantumPhase, inverse: bool = False, + ctrls: list[qasm3_ast.IndexedIdentifier] = [], + negctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -919,42 +945,10 @@ def _visit_phase_operation( return [operation] - def _collapse_gate_modifiers( - self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase] - ) -> tuple: - """Collapse the gate modifiers of a gate operation. - Some analysis is required to get this result. - The basic idea is that any power operation is multiplied and inversions are toggled. - The placement of the inverse operation does not matter. - - Args: - operation (qasm3_ast.QuantumGate): The gate operation to collapse modifiers for. - - Returns: - tuple[Any, Any]: The power and inverse values of the gate operation. - """ - exponent = 1 - - for modifier in operation.modifiers: - modifier_name = modifier.modifier - if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: - current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] - exponent *= current_power - elif modifier_name == qasm3_ast.GateModifierName.inv: - exponent *= -1 - elif modifier_name in [ - qasm3_ast.GateModifierName.ctrl, - qasm3_ast.GateModifierName.negctrl, - ]: - raise_qasm3_error( - f"Controlled modifier gates not yet supported in gate operation {operation}", - err_type=NotImplementedError, - span=operation.span, - ) - return (abs(exponent), exponent < 0) - def _visit_generic_gate_operation( - self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase] + self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], + ctrls: list[qasm3_ast.IndexedIdentifier] = [], + negctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -964,9 +958,6 @@ def _visit_generic_gate_operation( Returns: None """ - power_value, inverse_value = self._collapse_gate_modifiers(operation) - operation = copy.deepcopy(operation) - # only needs to be done once for a gate operation if ( len(operation.qubits) > 0 @@ -982,19 +973,62 @@ def _visit_generic_gate_operation( self._function_qreg_transform_map[-1], ) ) - # Applying the inverse first and then the power is same as - # apply the power first and then inverting the result - result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] - for _ in range(power_value): - if isinstance(operation, qasm3_ast.QuantumPhase): - result.extend(self._visit_phase_operation(operation, inverse_value)) - elif 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)) + + # ctrl / pow / inv modifiers commute. so group them. + exponent = 1 + ctrl_arg_ind = 0 + for modifier in operation.modifiers: + modifier_name = modifier.modifier + if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: + current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] + exponent *= current_power + elif modifier_name == qasm3_ast.GateModifierName.inv: + exponent *= -1 + elif modifier_name in [qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl]: + count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument, const_expr=True, reqd_type=int)[0] + if count is None: count = 1 + if count <= 0: + raise_qasm3_error( + f"Controlled modifiers must have positive integer arguments in gate operation {operation}", + span = operation.span + ) + ctrl_qubits = operation.qubits[ctrl_arg_ind:ctrl_arg_ind+count] + # TODO: assert ctrl_qubits are single qubits + ctrl_arg_ind += count + if modifier_name == qasm3_ast.GateModifierName.ctrl: + ctrls.extend(ctrl_qubits) + else: + negctrls.extend(ctrl_qubits) else: - result.extend(self._visit_basic_gate_operation(operation, inverse_value)) + raise_qasm3_error( + f"Unsupported modifier in gate operation {operation}", + err_type=NotImplementedError, + span=operation.span, + ) + + power_value, inverse_value = abs(exponent), exponent < 0 + operation = copy.deepcopy(operation) + operation.qubits = operation.qubits[ctrl_arg_ind:] + operation.modifiers = [] + + # get controlled? inverted? operation + result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] + if isinstance(operation, qasm3_ast.QuantumPhase): + result = self._visit_phase_operation(operation, inverse_value, ctrls, negctrls) + elif operation.name.name in self._external_gates: + result = self._visit_external_gate_operation(operation, inverse_value, ctrls, negctrls) + elif operation.name.name in self._custom_gates: + result = self._visit_custom_gate_operation(operation, inverse_value, ctrls, negctrls) + else: + result = self._visit_basic_gate_operation(operation, inverse_value, ctrls, negctrls) + + # apply pow(int) via duplication + if isinstance(power_value, int): + result *= power_value + else: + raise_qasm3_error(f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}") + if self._check_only: return [] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index ccc5f060..0c6c5fbf 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -374,13 +374,13 @@ def test_ctrl_gate_modifier(): qasm3_string = """ OPENQASM 3.0; include "stdgates.inc"; - qubit q; - ctrl @ x q; + qubit[2] q; + pow(3) @ ctrl @ inv @ x q[0], q[1]; """ result = loads(qasm3_string) result.unroll() - print(result) - assert False + assert result.num_qubits == 2 + check_two_qubit_gate_op(result.unrolled_ast, 3, [[0, 1], [0, 1], [0, 1]], "cx") def test_nested_gate_modifiers(): @@ -408,19 +408,18 @@ def test_nested_gate_modifiers(): def test_unsupported_modifiers(): # TO DO : add implementations, but till then we have tests - for modifier in ["ctrl", "negctrl"]: - with pytest.raises( - NotImplementedError, - match=r"Controlled modifier gates not yet supported .*", - ): - loads( - f""" - OPENQASM 3; - include "stdgates.inc"; - qubit[2] q; - {modifier} @ h q[0], q[1]; - """ - ).validate() + with pytest.raises( + NotImplementedError, + match=r"Negctrl is an unsupported in gate operation.*", + ): + loads( + f""" + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + negctrl @ h q[0], q[1]; + """ + ).validate() @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) From 8ba12da5c248069b72c365aacfa17e038be5b479 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 04:50:41 +0000 Subject: [PATCH 03/20] fix stacking ctrl bug --- src/pyqasm/maps.py | 11 +++++------ src/pyqasm/visitor.py | 11 +++++------ tests/qasm3/test_gates.py | 12 ++++++++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index ad22af2e..62b825d0 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1256,12 +1256,11 @@ def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): Map a controlled QASM operation to a callable. """ - ctrl_op_name = op_name - while ctrl_count > 0 and ctrl_op_name in CTRL_GATE_MAP: + ctrl_op_name, c = op_name, ctrl_count + while c > 0 and ctrl_op_name in CTRL_GATE_MAP: ctrl_op_name = CTRL_GATE_MAP[ctrl_op_name] - ctrl_count -= 1 - - if ctrl_count == 0: + c -= 1 + if c == 0: if ctrl_op_name in ONE_QUBIT_OP_MAP: return ONE_QUBIT_OP_MAP[ctrl_op_name], 1 if ctrl_op_name in TWO_QUBIT_OP_MAP: @@ -1270,7 +1269,7 @@ def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): return THREE_QUBIT_OP_MAP[ctrl_op_name], 3 # TODO: decompose controls if not built in - raise ValidationError(f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controlls") + raise ValidationError(f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controls") # pylint: disable=inconsistent-return-statements diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index ddbd620f..38bcdcbe 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -636,7 +636,7 @@ def _unroll_multiple_target_qubits( 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: + if len(op_qubits) <= 0 or 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, @@ -717,13 +717,11 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ValidationError: If the number of qubits is invalid. """ - print("HII") - print(inverse, negctrls, ctrls, operation) logger.debug("Visiting basic gate operation '%s'", str(operation)) inverse_action = None if not inverse: if len(negctrls) > 0: - raise_qasm3_error(f"Negctrl is an unsupported in gate operation: {operation}", err_type=NotImplementedError, span=operation.span) + raise_qasm3_error(f"Negctrl is an unsupported in gate operation: {operation} NEG {negctrls}", err_type=NotImplementedError, span=operation.span) if len(ctrls) > 0: qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable(operation.name.name, len(ctrls)) @@ -974,6 +972,7 @@ def _visit_generic_gate_operation( ) ) + ctrls, negctrls = copy.deepcopy(ctrls), copy.deepcopy(negctrls) # ctrl / pow / inv modifiers commute. so group them. exponent = 1 ctrl_arg_ind = 0 @@ -985,9 +984,9 @@ def _visit_generic_gate_operation( elif modifier_name == qasm3_ast.GateModifierName.inv: exponent *= -1 elif modifier_name in [qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl]: - count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument, const_expr=True, reqd_type=int)[0] + count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] if count is None: count = 1 - if count <= 0: + if not isinstance(count, int) or count <= 0: raise_qasm3_error( f"Controlled modifiers must have positive integer arguments in gate operation {operation}", span = operation.span diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 0c6c5fbf..6633b7d3 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -374,13 +374,17 @@ def test_ctrl_gate_modifier(): qasm3_string = """ OPENQASM 3.0; include "stdgates.inc"; - qubit[2] q; - pow(3) @ ctrl @ inv @ x q[0], q[1]; + qubit[4] q; + ctrl @ z q[0], q[1]; + ctrl @ ctrl @ x q[0], q[1], q[2]; + ctrl(2) @ x q[1], q[2], q[3]; """ result = loads(qasm3_string) result.unroll() - assert result.num_qubits == 2 - check_two_qubit_gate_op(result.unrolled_ast, 3, [[0, 1], [0, 1], [0, 1]], "cx") + assert result.num_qubits == 4 + print(dumps(result)) + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") + check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], 'ccx') def test_nested_gate_modifiers(): From 1f6066a24e134493c33d314dc91a4ebdc9e31b3a Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 04:58:09 +0000 Subject: [PATCH 04/20] fix func scope bug --- src/pyqasm/visitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 38bcdcbe..2586ed36 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -956,6 +956,9 @@ def _visit_generic_gate_operation( Returns: None """ + operation = copy.deepcopy(operation) + ctrls, negctrls = copy.deepcopy(ctrls), copy.deepcopy(negctrls) + # only needs to be done once for a gate operation if ( len(operation.qubits) > 0 @@ -972,7 +975,6 @@ def _visit_generic_gate_operation( ) ) - ctrls, negctrls = copy.deepcopy(ctrls), copy.deepcopy(negctrls) # ctrl / pow / inv modifiers commute. so group them. exponent = 1 ctrl_arg_ind = 0 @@ -1007,7 +1009,6 @@ def _visit_generic_gate_operation( power_value, inverse_value = abs(exponent), exponent < 0 - operation = copy.deepcopy(operation) operation.qubits = operation.qubits[ctrl_arg_ind:] operation.modifiers = [] From 9fe78bc09bc35b66f6c00baa7550a391734b2bd1 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 05:28:11 +0000 Subject: [PATCH 05/20] nested ctrl test --- src/pyqasm/maps.py | 12 ++--- src/pyqasm/visitor.py | 93 ++++++++++++++++++++++++++------------- tests/qasm3/test_gates.py | 21 +++++---- 3 files changed, 81 insertions(+), 45 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index 62b825d0..a0a0e0b8 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1247,15 +1247,15 @@ def map_qasm_inv_op_to_callable(op_name: str): "h": "ch", "u": "cu", "swap": "cswap", - "cx": "ccx" + "cx": "ccx", } def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): """ - Map a controlled QASM operation to a callable. + Map a controlled QASM operation to a callable. """ - + ctrl_op_name, c = op_name, ctrl_count while c > 0 and ctrl_op_name in CTRL_GATE_MAP: ctrl_op_name = CTRL_GATE_MAP[ctrl_op_name] @@ -1267,9 +1267,11 @@ def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): return TWO_QUBIT_OP_MAP[ctrl_op_name], 2 if ctrl_op_name in THREE_QUBIT_OP_MAP: return THREE_QUBIT_OP_MAP[ctrl_op_name], 3 - + # TODO: decompose controls if not built in - raise ValidationError(f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controls") + raise ValidationError( + f"Unsupported controlled QASM operation: {op_name} with {ctrl_count} controls" + ) # pylint: disable=inconsistent-return-statements diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 2586ed36..3f1e9f77 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -648,8 +648,10 @@ def _unroll_multiple_target_qubits( return qubit_subsets def _broadcast_gate_operation( - self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]], - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + self, + gate_function: Callable, + all_targets: list[list[qasm3_ast.IndexedIdentifier]], + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumGate]: """Broadcasts the application of a gate onto multiple sets of target qubits. @@ -691,9 +693,11 @@ def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedI qubit_node.depth = max_involved_depth def _visit_basic_gate_operation( # pylint: disable=too-many-locals - self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + self, + operation: qasm3_ast.QuantumGate, + inverse: bool = False, ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [] + negctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -720,11 +724,17 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals logger.debug("Visiting basic gate operation '%s'", str(operation)) inverse_action = None if not inverse: - if len(negctrls) > 0: - raise_qasm3_error(f"Negctrl is an unsupported in gate operation: {operation} NEG {negctrls}", err_type=NotImplementedError, span=operation.span) + if len(negctrls) > 0: + raise_qasm3_error( + f"Negctrl is an unsupported in gate operation: {operation} NEG {negctrls}", + err_type=NotImplementedError, + span=operation.span, + ) if len(ctrls) > 0: - qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable(operation.name.name, len(ctrls)) + qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable( + operation.name.name, len(ctrls) + ) op_qubit_count = op_qubit_total_count - len(ctrls) else: qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) @@ -740,19 +750,27 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals op_parameters = self._get_op_parameters(operation) if inverse_action == InversionOp.INVERT_ROTATION: op_parameters = [-1 * param for param in op_parameters] - + result = [] unrolled_targets = self._unroll_multiple_target_qubits(operation, op_qubit_count) unrolled_gate_function = partial(qasm_func, *op_parameters) if inverse: - result.extend([ - g2 - for g in self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, []) - for g2 in self._visit_basic_gate_operation(g, False, ctrls, negctrls) - ]) + result.extend( + [ + g2 + for g in self._broadcast_gate_operation( + unrolled_gate_function, unrolled_targets, [] + ) + for g2 in self._visit_basic_gate_operation(g, False, ctrls, negctrls) + ] + ) else: - result.extend(self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls + negctrls)) + result.extend( + self._broadcast_gate_operation( + unrolled_gate_function, unrolled_targets, ctrls + negctrls + ) + ) self._update_qubit_depth_for_gate(unrolled_targets) @@ -762,9 +780,11 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals return result def _visit_custom_gate_operation( - self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + self, + operation: qasm3_ast.QuantumGate, + inverse: bool = False, ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [] + negctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -843,9 +863,11 @@ def _visit_custom_gate_operation( return result def _visit_external_gate_operation( - self, operation: qasm3_ast.QuantumGate, inverse: bool = False, + self, + operation: qasm3_ast.QuantumGate, + inverse: bool = False, ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [] + negctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -906,9 +928,11 @@ def gate_function(*qubits): return result def _visit_phase_operation( - self, operation: qasm3_ast.QuantumPhase, inverse: bool = False, + self, + operation: qasm3_ast.QuantumPhase, + inverse: bool = False, ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [] + negctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -944,9 +968,10 @@ def _visit_phase_operation( return [operation] def _visit_generic_gate_operation( - self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], + self, + operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [] + negctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -958,7 +983,7 @@ def _visit_generic_gate_operation( """ operation = copy.deepcopy(operation) ctrls, negctrls = copy.deepcopy(ctrls), copy.deepcopy(negctrls) - + # only needs to be done once for a gate operation if ( len(operation.qubits) > 0 @@ -985,16 +1010,20 @@ def _visit_generic_gate_operation( exponent *= current_power elif modifier_name == qasm3_ast.GateModifierName.inv: exponent *= -1 - elif modifier_name in [qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl]: + elif modifier_name in [ + qasm3_ast.GateModifierName.ctrl, + qasm3_ast.GateModifierName.negctrl, + ]: count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] - if count is None: count = 1 + if count is None: + count = 1 if not isinstance(count, int) or count <= 0: raise_qasm3_error( f"Controlled modifiers must have positive integer arguments in gate operation {operation}", - span = operation.span + span=operation.span, ) - ctrl_qubits = operation.qubits[ctrl_arg_ind:ctrl_arg_ind+count] - # TODO: assert ctrl_qubits are single qubits + ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] + # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count if modifier_name == qasm3_ast.GateModifierName.ctrl: ctrls.extend(ctrl_qubits) @@ -1011,7 +1040,7 @@ def _visit_generic_gate_operation( operation.qubits = operation.qubits[ctrl_arg_ind:] operation.modifiers = [] - + # get controlled? inverted? operation result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] if isinstance(operation, qasm3_ast.QuantumPhase): @@ -1027,8 +1056,10 @@ def _visit_generic_gate_operation( if isinstance(power_value, int): result *= power_value else: - raise_qasm3_error(f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}") - + raise_qasm3_error( + f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}" + ) + if self._check_only: return [] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 6633b7d3..2e5cd8d9 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -382,32 +382,35 @@ def test_ctrl_gate_modifier(): result = loads(qasm3_string) result.unroll() assert result.num_qubits == 4 - print(dumps(result)) check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") - check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], 'ccx') + check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], "ccx") def test_nested_gate_modifiers(): qasm3_string = """ OPENQASM 3; include "stdgates.inc"; - qubit[2] q; + qubit[3] q; gate custom2 p, q{ - y p; + x p; z q; + ctrl @ x q, p; } gate custom p, q { pow(1) @ custom2 p, q; } - pow(1) @ inv @ pow(2) @ custom q; - pow(-1) @ custom q; + pow(1) @ inv @ pow(2) @ custom q[0], q[1]; + ctrl @ pow(-1) @ custom q[0], q[1], q[2]; """ result = loads(qasm3_string) result.unroll() - assert result.num_qubits == 2 + assert result.num_qubits == 3 assert result.num_clbits == 0 - check_single_qubit_gate_op(result.unrolled_ast, 3, [1, 1, 1], "z") - check_single_qubit_gate_op(result.unrolled_ast, 3, [0, 0, 0], "y") + check_single_qubit_gate_op(result.unrolled_ast, 2, [1, 1, 1], "z") + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0, 0], "x") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 2]], "cz") + check_two_qubit_gate_op(result.unrolled_ast, 3, [[1, 0], [1, 0], [0, 1]], "cx") + check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 2, 1]], "ccx") def test_unsupported_modifiers(): From 969a51e9697426eb961956d03fa0a3378f9dfd45 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 06:49:59 +0000 Subject: [PATCH 06/20] remove neg ctrl --- src/pyqasm/visitor.py | 55 ++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 3f1e9f77..10f94b04 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -696,8 +696,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -724,13 +723,6 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals logger.debug("Visiting basic gate operation '%s'", str(operation)) inverse_action = None if not inverse: - if len(negctrls) > 0: - raise_qasm3_error( - f"Negctrl is an unsupported in gate operation: {operation} NEG {negctrls}", - err_type=NotImplementedError, - span=operation.span, - ) - if len(ctrls) > 0: qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable( operation.name.name, len(ctrls) @@ -762,13 +754,13 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals for g in self._broadcast_gate_operation( unrolled_gate_function, unrolled_targets, [] ) - for g2 in self._visit_basic_gate_operation(g, False, ctrls, negctrls) + for g2 in self._visit_basic_gate_operation(g, False, ctrls) ] ) else: result.extend( self._broadcast_gate_operation( - unrolled_gate_function, unrolled_targets, ctrls + negctrls + unrolled_gate_function, unrolled_targets, ctrls ) ) @@ -783,8 +775,7 @@ def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -848,7 +839,7 @@ def _visit_custom_gate_operation( gate_op_copy.modifiers.append( qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None) ) - result.extend(self._visit_generic_gate_operation(gate_op_copy, ctrls, negctrls)) + result.extend(self._visit_generic_gate_operation(gate_op_copy, ctrls)) else: # TODO: add control flow support raise_qasm3_error( @@ -866,8 +857,7 @@ def _visit_external_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -889,12 +879,12 @@ def _visit_external_gate_operation( if gate_name in self._custom_gates: # Ignore result, this is just for validation - self._visit_custom_gate_operation(operation, inverse, ctrls, negctrls) + self._visit_custom_gate_operation(operation, inverse, ctrls) # 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, ctrls, negctrls) + self._visit_basic_gate_operation(operation, inverse, ctrls) # 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) @@ -931,8 +921,7 @@ def _visit_phase_operation( self, operation: qasm3_ast.QuantumPhase, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -970,8 +959,7 @@ def _visit_phase_operation( def _visit_generic_gate_operation( self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], - ctrls: list[qasm3_ast.IndexedIdentifier] = [], - negctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: list[qasm3_ast.IndexedIdentifier] = [] ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -981,8 +969,7 @@ def _visit_generic_gate_operation( Returns: None """ - operation = copy.deepcopy(operation) - ctrls, negctrls = copy.deepcopy(ctrls), copy.deepcopy(negctrls) + operation, ctrls = copy.deepcopy(operation), copy.deepcopy(ctrls) # only needs to be done once for a gate operation if ( @@ -1010,10 +997,7 @@ def _visit_generic_gate_operation( exponent *= current_power elif modifier_name == qasm3_ast.GateModifierName.inv: exponent *= -1 - elif modifier_name in [ - qasm3_ast.GateModifierName.ctrl, - qasm3_ast.GateModifierName.negctrl, - ]: + elif modifier_name == qasm3_ast.GateModifierName.ctrl: count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] if count is None: count = 1 @@ -1025,13 +1009,10 @@ def _visit_generic_gate_operation( ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count - if modifier_name == qasm3_ast.GateModifierName.ctrl: - ctrls.extend(ctrl_qubits) - else: - negctrls.extend(ctrl_qubits) + ctrls.extend(ctrl_qubits) else: raise_qasm3_error( - f"Unsupported modifier in gate operation {operation}", + f"Negctrl is an unsupported in gate operation: {operation}", err_type=NotImplementedError, span=operation.span, ) @@ -1044,13 +1025,13 @@ def _visit_generic_gate_operation( # get controlled? inverted? operation result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] if isinstance(operation, qasm3_ast.QuantumPhase): - result = self._visit_phase_operation(operation, inverse_value, ctrls, negctrls) + result = self._visit_phase_operation(operation, inverse_value, ctrls) elif operation.name.name in self._external_gates: - result = self._visit_external_gate_operation(operation, inverse_value, ctrls, negctrls) + result = self._visit_external_gate_operation(operation, inverse_value, ctrls) elif operation.name.name in self._custom_gates: - result = self._visit_custom_gate_operation(operation, inverse_value, ctrls, negctrls) + result = self._visit_custom_gate_operation(operation, inverse_value, ctrls) else: - result = self._visit_basic_gate_operation(operation, inverse_value, ctrls, negctrls) + result = self._visit_basic_gate_operation(operation, inverse_value, ctrls) # apply pow(int) via duplication if isinstance(power_value, int): From 9fe6ae94a953025d0d7f4b477c696d727f068e26 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 10 Jan 2025 07:13:18 +0000 Subject: [PATCH 07/20] future work --- src/pyqasm/visitor.py | 2 ++ tests/qasm3/test_gates.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 10f94b04..4e4abe26 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -894,6 +894,7 @@ def _visit_external_gate_operation( self._push_context(Context.GATE) + # TODO: add ctrl @ support + testing modifiers = [] if inverse: modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)] @@ -932,6 +933,7 @@ def _visit_phase_operation( Returns: list[qasm3_ast.Statement]: The unrolled quantum phase operation. """ + # TODO: phase = ctrl @ gphase. unify this w/ existing ctrl support logger.debug("Visiting phase operation '%s'", str(operation)) evaluated_arg = Qasm3ExprEvaluator.evaluate_expression(operation.argument)[0] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 2e5cd8d9..a51d3354 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -378,12 +378,15 @@ def test_ctrl_gate_modifier(): ctrl @ z q[0], q[1]; ctrl @ ctrl @ x q[0], q[1], q[2]; ctrl(2) @ x q[1], q[2], q[3]; + ctrl @ gphase(0.1) q[0]; """ result = loads(qasm3_string) result.unroll() assert result.num_qubits == 4 check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], "ccx") + print(dumps(result)) + assert False def test_nested_gate_modifiers(): From b14a7a6fe04f707a16e56eeb5a62748d1ad287d3 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Thu, 9 Jan 2025 23:13:53 -0800 Subject: [PATCH 08/20] revert test --- tests/qasm3/test_gates.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index a51d3354..2e5cd8d9 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -378,15 +378,12 @@ def test_ctrl_gate_modifier(): ctrl @ z q[0], q[1]; ctrl @ ctrl @ x q[0], q[1], q[2]; ctrl(2) @ x q[1], q[2], q[3]; - ctrl @ gphase(0.1) q[0]; """ result = loads(qasm3_string) result.unroll() assert result.num_qubits == 4 check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], "ccx") - print(dumps(result)) - assert False def test_nested_gate_modifiers(): From 8cbb786677855a9b82b2e5599dfe8f427d52fbc4 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 08:05:48 +0000 Subject: [PATCH 09/20] negctrl support --- src/pyqasm/visitor.py | 14 +++++++------- tests/qasm3/test_gates.py | 26 ++++++++++++-------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 4e4abe26..c14e13e8 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -972,6 +972,7 @@ def _visit_generic_gate_operation( None """ operation, ctrls = copy.deepcopy(operation), copy.deepcopy(ctrls) + negctrls = [] # only needs to be done once for a gate operation if ( @@ -999,7 +1000,7 @@ def _visit_generic_gate_operation( exponent *= current_power elif modifier_name == qasm3_ast.GateModifierName.inv: exponent *= -1 - elif modifier_name == qasm3_ast.GateModifierName.ctrl: + elif modifier_name in [qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl]: count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] if count is None: count = 1 @@ -1012,12 +1013,8 @@ def _visit_generic_gate_operation( # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count ctrls.extend(ctrl_qubits) - else: - raise_qasm3_error( - f"Negctrl is an unsupported in gate operation: {operation}", - err_type=NotImplementedError, - span=operation.span, - ) + if modifier_name == qasm3_ast.GateModifierName.negctrl: + negctrls.extend(ctrl_qubits) power_value, inverse_value = abs(exponent), exponent < 0 @@ -1042,6 +1039,9 @@ def _visit_generic_gate_operation( raise_qasm3_error( f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}" ) + + negs = [qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls] + result = negs + result + negs if self._check_only: return [] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 2e5cd8d9..e288c22b 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -413,20 +413,18 @@ def test_nested_gate_modifiers(): check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 2, 1]], "ccx") -def test_unsupported_modifiers(): - # TO DO : add implementations, but till then we have tests - with pytest.raises( - NotImplementedError, - match=r"Negctrl is an unsupported in gate operation.*", - ): - loads( - f""" - OPENQASM 3; - include "stdgates.inc"; - qubit[2] q; - negctrl @ h q[0], q[1]; - """ - ).validate() +def test_negctrl_gate_modifier(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + negctrl @ z q[0], q[1]; + """ + result = loads(qasm3_string) + result.unroll() + assert result.num_qubits == 2 + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) From 2184911c71f4a1736d7e0a42dc18a6a8abac16d8 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 00:49:15 -0800 Subject: [PATCH 10/20] fix depth calculation --- src/pyqasm/visitor.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c14e13e8..c776f8b6 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -764,11 +764,11 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ) ) - self._update_qubit_depth_for_gate(unrolled_targets) - + self._update_qubit_depth_for_gate(unrolled_targets) if self._check_only: return [] + return result def _visit_custom_gate_operation( @@ -1021,25 +1021,29 @@ def _visit_generic_gate_operation( operation.qubits = operation.qubits[ctrl_arg_ind:] operation.modifiers = [] - # get controlled? inverted? operation - result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] - if isinstance(operation, qasm3_ast.QuantumPhase): - result = self._visit_phase_operation(operation, inverse_value, ctrls) - elif operation.name.name in self._external_gates: - result = self._visit_external_gate_operation(operation, inverse_value, ctrls) - elif operation.name.name in self._custom_gates: - result = self._visit_custom_gate_operation(operation, inverse_value, ctrls) - else: - result = self._visit_basic_gate_operation(operation, inverse_value, ctrls) + + # apply pow(int) via duplication - if isinstance(power_value, int): - result *= power_value - else: + if not isinstance(power_value, int): raise_qasm3_error( f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}" ) + + # get controlled? inverted? operation x power times + result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] + for _ in range(power_value): + if isinstance(operation, qasm3_ast.QuantumPhase): + r = self._visit_phase_operation(operation, inverse_value, ctrls) + elif operation.name.name in self._external_gates: + r = self._visit_external_gate_operation(operation, inverse_value, ctrls) + elif operation.name.name in self._custom_gates: + r = self._visit_custom_gate_operation(operation, inverse_value, ctrls) + else: + r = self._visit_basic_gate_operation(operation, inverse_value, ctrls) + result.extend(r) + # negctrl -> ctrl conversion negs = [qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls] result = negs + result + negs From abe4777f6409d438447fe9d065de1e3d79f74b05 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 00:55:03 -0800 Subject: [PATCH 11/20] ctrl depth calc --- src/pyqasm/visitor.py | 8 ++++---- tests/qasm3/test_depth.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c776f8b6..2177ac48 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -670,7 +670,7 @@ def _broadcast_gate_operation( result.extend(gate_function(*ctrls, *targets)) return result - def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]]): + def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]], ctrls: list[qasm3_ast.IndexedIdentifier]): """Updates the depth of the circuit after applying a broadcasted gate. Args: @@ -681,13 +681,13 @@ def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedI """ for qubit_subset in all_targets: max_involved_depth = 0 - for qubit in qubit_subset: + for qubit in qubit_subset + ctrls: 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: + for qubit in qubit_subset + ctrls: 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 @@ -764,7 +764,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ) ) - self._update_qubit_depth_for_gate(unrolled_targets) + self._update_qubit_depth_for_gate(unrolled_targets, ctrls) if self._check_only: return [] diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index 8b8ef287..caaac75e 100644 --- a/tests/qasm3/test_depth.py +++ b/tests/qasm3/test_depth.py @@ -109,6 +109,19 @@ def test_inv_gate_depth(): assert result.depth() == 5 +def test_ctrl_depth(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[3] q; + ctrl @ x q[0], q[1]; + ctrl @ x q[0], q[2]; + """ + result = loads(qasm3_string) + result.unroll() + assert result.depth() == 2 + + def test_qubit_depth_with_unrelated_measure_op(): qasm3_string = """ OPENQASM 3; From 6ec412a0debe53ed3d0095568d14d7f8307c430c Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 09:37:05 +0000 Subject: [PATCH 12/20] review fixes --- src/pyqasm/maps.py | 7 ++++ src/pyqasm/visitor.py | 67 +++++++++++++++++++++++++++------------ tests/qasm3/test_gates.py | 59 ++++++++++++++++++++++++++++------ 3 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/pyqasm/maps.py b/src/pyqasm/maps.py index a0a0e0b8..b20a6c2f 100644 --- a/src/pyqasm/maps.py +++ b/src/pyqasm/maps.py @@ -1254,6 +1254,13 @@ def map_qasm_inv_op_to_callable(op_name: str): def map_qasm_ctrl_op_to_callable(op_name: str, ctrl_count: int): """ Map a controlled QASM operation to a callable. + + Args: + op_name (str): The QASM operation name. + ctrl_count (int): The number of control qubits. + + Returns: + tuple: A tuple containing the callable and the number of qubits the operation acts on. """ ctrl_op_name, c = op_name, ctrl_count diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 2177ac48..9f99d370 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -670,7 +670,11 @@ def _broadcast_gate_operation( result.extend(gate_function(*ctrls, *targets)) return result - def _update_qubit_depth_for_gate(self, all_targets: list[list[qasm3_ast.IndexedIdentifier]], ctrls: list[qasm3_ast.IndexedIdentifier]): + def _update_qubit_depth_for_gate( + self, + all_targets: list[list[qasm3_ast.IndexedIdentifier]], + ctrls: list[qasm3_ast.IndexedIdentifier], + ): """Updates the depth of the circuit after applying a broadcasted gate. Args: @@ -696,7 +700,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -748,6 +752,8 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals unrolled_gate_function = partial(qasm_func, *op_parameters) if inverse: + # for convenience, we recur and handle the ctrl @'s to the unrolled no inverse case + # instead of trying to map the inverted callable to a ctrl'd version result.extend( [ g2 @@ -759,23 +765,20 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals ) else: result.extend( - self._broadcast_gate_operation( - unrolled_gate_function, unrolled_targets, ctrls - ) + self._broadcast_gate_operation(unrolled_gate_function, unrolled_targets, ctrls) ) self._update_qubit_depth_for_gate(unrolled_targets, ctrls) if self._check_only: return [] - return result def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -857,7 +860,7 @@ def _visit_external_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -922,7 +925,7 @@ def _visit_phase_operation( self, operation: qasm3_ast.QuantumPhase, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -961,7 +964,7 @@ def _visit_phase_operation( def _visit_generic_gate_operation( self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], - ctrls: list[qasm3_ast.IndexedIdentifier] = [] + ctrls: list[qasm3_ast.IndexedIdentifier] = [], ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -996,17 +999,38 @@ def _visit_generic_gate_operation( for modifier in operation.modifiers: modifier_name = modifier.modifier if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: - current_power = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] + try: + current_power = Qasm3ExprEvaluator.evaluate_expression( + modifier.argument, reqd_type=qasm3_ast.IntType + )[0] + except ValidationError: + raise_qasm3_error( + f"Power modifier argument must be an integer in gate operation {operation}", + span=operation.span, + ) exponent *= current_power elif modifier_name == qasm3_ast.GateModifierName.inv: exponent *= -1 - elif modifier_name in [qasm3_ast.GateModifierName.ctrl, qasm3_ast.GateModifierName.negctrl]: - count = Qasm3ExprEvaluator.evaluate_expression(modifier.argument)[0] + elif modifier_name in [ + qasm3_ast.GateModifierName.ctrl, + qasm3_ast.GateModifierName.negctrl, + ]: + try: + count = Qasm3ExprEvaluator.evaluate_expression( + modifier.argument, const_expr=True + )[0] + except ValidationError: + raise_qasm3_error( + "Controlled modifier arguments must be compile-time constants " + f"in gate operation {operation}", + span=operation.span, + ) if count is None: count = 1 if not isinstance(count, int) or count <= 0: raise_qasm3_error( - f"Controlled modifiers must have positive integer arguments in gate operation {operation}", + "Controlled modifier argument must be a positive integer " + f"in gate operation {operation}", span=operation.span, ) ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] @@ -1021,13 +1045,12 @@ def _visit_generic_gate_operation( operation.qubits = operation.qubits[ctrl_arg_ind:] operation.modifiers = [] - - - # apply pow(int) via duplication if not isinstance(power_value, int): raise_qasm3_error( - f"Power modifiers with non-integer arguments are unsupported in gate operation {operation}" + "Power modifiers with non-integer arguments are unsupported in gate " + f"operation {operation}", + span=operation.span, ) # get controlled? inverted? operation x power times @@ -1041,10 +1064,12 @@ def _visit_generic_gate_operation( r = self._visit_custom_gate_operation(operation, inverse_value, ctrls) else: r = self._visit_basic_gate_operation(operation, inverse_value, ctrls) - result.extend(r) - + result.extend(r) + # negctrl -> ctrl conversion - negs = [qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls] + negs = [ + qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls + ] result = negs + result + negs if self._check_only: diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index e288c22b..5903f3e6 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -386,6 +386,20 @@ def test_ctrl_gate_modifier(): check_three_qubit_gate_op(result.unrolled_ast, 2, [[0, 1, 2], [1, 2, 3]], "ccx") +def test_negctrl_gate_modifier(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + negctrl @ z q[0], q[1]; + """ + result = loads(qasm3_string) + result.unroll() + assert result.num_qubits == 2 + check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x") + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") + + def test_nested_gate_modifiers(): qasm3_string = """ OPENQASM 3; @@ -413,18 +427,45 @@ def test_nested_gate_modifiers(): check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 2, 1]], "ccx") -def test_negctrl_gate_modifier(): - qasm3_string = """ +@pytest.mark.parametrize( + "test", + [ + ( + """ OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; - negctrl @ z q[0], q[1]; - """ - result = loads(qasm3_string) - result.unroll() - assert result.num_qubits == 2 - check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x") - check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") + h q; + bit b; + b = measure q[0]; + ctrl(b+1) @ x q[0], q[1]; + """, + "Controlled modifier arguments must be compile-time constants.*", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + ctrl(1.5) @ x q[0], q[1]; + """, + "Controlled modifier argument must be a positive integer.*", + ), + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + pow(1.5) @ x q; + """, + "Power modifier argument must be an integer.*", + ), + ], +) +def test_modifier_arg_error(test): + qasm3_string, error_message = test + with pytest.raises(ValidationError, match=error_message): + loads(qasm3_string).validate() @pytest.mark.parametrize("test_name", CUSTOM_GATE_INCORRECT_TESTS.keys()) From 096053a6284d49d7e06366e57aad8a87fd7b07af Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Tue, 14 Jan 2025 23:05:58 +0000 Subject: [PATCH 13/20] ctrl test suite --- src/pyqasm/visitor.py | 8 ++-- tests/qasm3/test_gates.py | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 9f99d370..45d92c12 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -686,14 +686,14 @@ def _update_qubit_depth_for_gate( for qubit_subset in all_targets: max_involved_depth = 0 for qubit in qubit_subset + ctrls: - 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_id = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] # type: ignore + qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] qubit_node.num_gates += 1 max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) for qubit in qubit_subset + ctrls: - 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_id = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] # type: ignore + qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] qubit_node.depth = max_involved_depth def _visit_basic_gate_operation( # pylint: disable=too-many-locals diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 5903f3e6..de063a1c 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -399,6 +399,83 @@ def test_negctrl_gate_modifier(): check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x") check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") +def test_ctrl_in_custom_gate(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[3] q; + gate custom a, b, c { + ctrl @ x a, b; + ctrl(2) @ x a, b, c; + } + custom q[0], q[1], q[2]; + """ + result = loads(qasm3_string) + result.unroll() + assert result.num_qubits == 3 + assert result.num_clbits == 0 + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx") + check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 1, 2]], "ccx") + +def test_ctrl_in_subroutine(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + def f(qubit a, qubit b) { + ctrl @ x a, b; + return; + } + qubit[2] q; + f(q[0], q[1]); + """ + + result = loads(qasm3_string) + result.unroll() + assert result.num_qubits == 2 + assert result.num_clbits == 0 + check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx") + +def test_ctrl_in_if_block(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit b; + b = measure q[0]; + if(b == 1) { + ctrl @ x q[0], q[1]; + } + """ + expected_qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + bit[1] b; + b[0] = measure q[0]; + if (b == 1) { + cx q[0], q[1]; + } + """ + result = loads(qasm3_string) + result.unroll() + check_unrolled_qasm(dumps(result), expected_qasm) + +def test_ctrl_in_for_loop(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[4] q; + + for int i in [0:2]{ + ctrl @ x q[0], q[i+1]; + } + """ + # TODO: breaks if the index of the ctrl is not a compile-time constant + result = loads(qasm3_string) + result.unroll() + assert result.num_qubits == 4 + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (0, 2), (0, 3)], "cx") + def test_nested_gate_modifiers(): qasm3_string = """ From 2025082a4492565bd204243039a5eb6e4ad68a2d Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 17 Jan 2025 01:39:19 -0800 Subject: [PATCH 14/20] todo for unroll ctrl --- src/pyqasm/visitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 45d92c12..906d06c1 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1033,6 +1033,7 @@ def _visit_generic_gate_operation( f"in gate operation {operation}", span=operation.span, ) + # TODO: unroll ctrl qubits ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count From 503b456fa168af2bff3f42d4d096b54e9c90833e Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Fri, 17 Jan 2025 02:00:52 -0800 Subject: [PATCH 15/20] fix for loop --- src/pyqasm/visitor.py | 5 +++++ tests/qasm3/test_gates.py | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c08a0ae5..f991b77d 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -987,6 +987,10 @@ def _visit_generic_gate_operation( ) ) + operation.qubits = self._get_op_bits( + operation, reg_size_map=self._global_qreg_size_map, qubits=True + ) + # ctrl / pow / inv modifiers commute. so group them. exponent = 1 ctrl_arg_ind = 0 @@ -1029,6 +1033,7 @@ def _visit_generic_gate_operation( ) # TODO: unroll ctrl qubits ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] + # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count ctrls.extend(ctrl_qubits) diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index e21cd309..30877a94 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -468,14 +468,13 @@ def test_ctrl_in_for_loop(): qubit[4] q; for int i in [0:2]{ - ctrl @ x q[0], q[i+1]; + ctrl @ x q[i], q[i+1]; } """ - # TODO: breaks if the index of the ctrl is not a compile-time constant result = loads(qasm3_string) result.unroll() assert result.num_qubits == 4 - check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (0, 2), (0, 3)], "cx") + check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") def test_nested_gate_modifiers(): From ee5e478b86e51fc17e609a3a1d97d40a3ac6124a Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Mon, 20 Jan 2025 18:54:04 +0000 Subject: [PATCH 16/20] unroll ctrl bits --- src/pyqasm/visitor.py | 1 - tests/qasm3/test_gates.py | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index f991b77d..d6b66310 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -1031,7 +1031,6 @@ def _visit_generic_gate_operation( f"in gate operation {operation}", span=operation.span, ) - # TODO: unroll ctrl qubits ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] # TODO: assert ctrl_qubits are single qubits diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 30877a94..a583ce33 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -458,7 +458,6 @@ def test_ctrl_in_if_block(): """ result = loads(qasm3_string) result.unroll() - print(result) check_unrolled_qasm(dumps(result), expected_qasm) def test_ctrl_in_for_loop(): @@ -476,6 +475,24 @@ def test_ctrl_in_for_loop(): assert result.num_qubits == 4 check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") +def test_ctrl_unroll(): + qasm3_string = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] a; + qubit b; + ctrl (2) @ x a, b[0]; + """ + expected_qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] a; + qubit[1] b; + ccx a[0], a[1], b[0]; + """ + result = loads(qasm3_string) + result.unroll() + check_unrolled_qasm(dumps(result), expected_qasm) def test_nested_gate_modifiers(): qasm3_string = """ From f4aa3ad3c726b027a685b20c2157b72dffc11e64 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Mon, 20 Jan 2025 19:14:42 +0000 Subject: [PATCH 17/20] ctrl gphase = p --- src/pyqasm/visitor.py | 9 ++++++++- tests/qasm3/test_gates.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index d6b66310..4c8f3b39 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -930,9 +930,16 @@ def _visit_phase_operation( Returns: list[qasm3_ast.Statement]: The unrolled quantum phase operation. """ - # TODO: phase = ctrl @ gphase. unify this w/ existing ctrl support logger.debug("Visiting phase operation '%s'", str(operation)) + if len(ctrls) > 0: + return self._visit_basic_gate_operation(qasm3_ast.QuantumGate( + modifiers=[qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls)-1))], + name=qasm3_ast.Identifier("p"), + qubits=ctrls[0:1], + arguments=[operation.argument] + ), inverse, ctrls[:-1]) + evaluated_arg = Qasm3ExprEvaluator.evaluate_expression(operation.argument)[0] if inverse: evaluated_arg = -1 * evaluated_arg diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index a583ce33..4693af9d 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -494,6 +494,24 @@ def test_ctrl_unroll(): result.unroll() check_unrolled_qasm(dumps(result), expected_qasm) +def test_ctrl_gphase_eq_p(): + qasm3_str_gphase = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit a; + ctrl @ gphase(1) a; + """ + qasm3_str_p = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit a; + p(1) a; + """ + result_gphase, result_p = loads(qasm3_str_gphase), loads(qasm3_str_p) + result_gphase.unroll() + result_p.unroll() + check_unrolled_qasm(dumps(result_gphase), dumps(result_p)) + def test_nested_gate_modifiers(): qasm3_string = """ OPENQASM 3; From b71aae990f5500dad4585b56cf0cc2a0da919e25 Mon Sep 17 00:00:00 2001 From: Alvan Caleb Arulandu Date: Mon, 20 Jan 2025 14:40:29 -0500 Subject: [PATCH 18/20] ctrl external gates --- src/pyqasm/visitor.py | 8 +++++--- tests/qasm3/test_gates.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 4c8f3b39..5999b88b 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -881,7 +881,7 @@ def _visit_external_gate_operation( 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, ctrls) + self._visit_basic_gate_operation(operation) # 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) @@ -894,14 +894,16 @@ def _visit_external_gate_operation( # TODO: add ctrl @ support + testing modifiers = [] if inverse: - modifiers = [qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)] + modifiers.append(qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)) + if len(ctrls) > 0: + modifiers.append(qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls))) if len(ctrls) > 0 else None) def gate_function(*qubits): return [ qasm3_ast.QuantumGate( modifiers=modifiers, name=qasm3_ast.Identifier(gate_name), - qubits=list(qubits), + qubits=ctrls + list(qubits), arguments=list(op_parameters), ) ] diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 4693af9d..818f5dcc 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -185,6 +185,24 @@ def test_qasm_u3_gates_external_with_multiple_qubits(): check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 1], "u3") +def test_qasm_u3_gates_external_with_ctrl(): + qasm3_string = """ + OPENQASM 3; + include "stdgates.inc"; + qubit[2] q; + ctrl @ u3(0.5, 0.5, 0.5) q[0], q[1]; + """ + expected_qasm = """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + ctrl(1) @ u3(0.5, 0.5, 0.5) q[0], q[1]; + """ + result = loads(qasm3_string) + result.unroll(external_gates=["u3"]) + check_unrolled_qasm(dumps(result), expected_qasm) + + def test_qasm_u2_gates(): qasm3_string = """ OPENQASM 3; From ffc4bb72599afb81a9b9be074c9461c7962d6e4c Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 12 Feb 2025 16:22:56 +0530 Subject: [PATCH 19/20] fix formatting --- src/pyqasm/maps/gates.py | 2 +- src/pyqasm/visitor.py | 81 +++++++++++++++++++++++++++------------ tests/qasm3/test_gates.py | 9 ++++- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index 876399ff..33ddbbcf 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -1,4 +1,3 @@ - # Copyright (C) 2025 qBraid # # This file is part of PyQASM @@ -1169,6 +1168,7 @@ def map_qasm_inv_op_to_callable(op_name: str): ) raise ValidationError(f"Unsupported / undeclared QASM operation: {op_name}") + CTRL_GATE_MAP = { "x": "cx", "y": "cy", diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index a32f09b2..184bd431 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -29,7 +29,11 @@ from pyqasm.expressions import Qasm3ExprEvaluator from pyqasm.maps import SWITCH_BLACKLIST_STMTS from pyqasm.maps.expressions import ARRAY_TYPE_MAP, CONSTANTS_MAP, MAX_ARRAY_DIMENSIONS -from pyqasm.maps.gates import map_qasm_inv_op_to_callable, map_qasm_op_to_callable, map_qasm_ctrl_op_to_callable +from pyqasm.maps.gates import ( + map_qasm_ctrl_op_to_callable, + map_qasm_inv_op_to_callable, + map_qasm_op_to_callable, +) from pyqasm.subroutines import Qasm3SubroutineProcessor from pyqasm.transformer import Qasm3Transformer from pyqasm.validator import Qasm3Validator @@ -645,7 +649,7 @@ def _broadcast_gate_operation( self, gate_function: Callable, all_targets: list[list[qasm3_ast.IndexedIdentifier]], - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Broadcasts the application of a gate onto multiple sets of target qubits. @@ -660,6 +664,8 @@ def _broadcast_gate_operation( List of all executed gates. """ result = [] + if ctrls is None: + ctrls = [] for targets in all_targets: result.extend(gate_function(*ctrls, *targets)) return result @@ -680,13 +686,15 @@ def _update_qubit_depth_for_gate( for qubit_subset in all_targets: max_involved_depth = 0 for qubit in qubit_subset + ctrls: - qubit_id = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] # type: ignore + _qid_ = qubit.indices[0][0] + qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] qubit_node.num_gates += 1 max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) for qubit in qubit_subset + ctrls: - qubit_id = Qasm3ExprEvaluator.evaluate_expression(qubit.indices[0][0])[0] # type: ignore + _qid_ = qubit.indices[0][0] + qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] qubit_node.depth = max_involved_depth @@ -694,7 +702,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -720,6 +728,9 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals """ logger.debug("Visiting basic gate operation '%s'", str(operation)) inverse_action = None + if ctrls is None: + ctrls = [] + if not inverse: if len(ctrls) > 0: qasm_func, op_qubit_total_count = map_qasm_ctrl_op_to_callable( @@ -752,7 +763,7 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals [ g2 for g in self._broadcast_gate_operation( - unrolled_gate_function, unrolled_targets, [] + unrolled_gate_function, unrolled_targets, None ) for g2 in self._visit_basic_gate_operation(g, False, ctrls) ] @@ -772,7 +783,7 @@ def _visit_custom_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -789,6 +800,8 @@ def _visit_custom_gate_operation( None """ logger.debug("Visiting custom gate operation '%s'", str(operation)) + if ctrls is None: + ctrls = [] gate_name: str = operation.name.name gate_definition: qasm3_ast.QuantumGateDefinition = self._custom_gates[gate_name] op_qubits: list[qasm3_ast.IndexedIdentifier] = ( @@ -854,7 +867,7 @@ def _visit_external_gate_operation( self, operation: qasm3_ast.QuantumGate, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -873,6 +886,8 @@ def _visit_external_gate_operation( logger.debug("Visiting external gate operation '%s'", str(operation)) gate_name: str = operation.name.name + if ctrls is None: + ctrls = [] if gate_name in self._custom_gates: # Ignore result, this is just for validation @@ -896,7 +911,13 @@ def _visit_external_gate_operation( if inverse: modifiers.append(qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.inv, None)) if len(ctrls) > 0: - modifiers.append(qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls))) if len(ctrls) > 0 else None) + modifiers.append( + qasm3_ast.QuantumGateModifier( + qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls)) + ) + if len(ctrls) > 0 + else None + ) def gate_function(*qubits): return [ @@ -921,7 +942,7 @@ def _visit_phase_operation( self, operation: qasm3_ast.QuantumPhase, inverse: bool = False, - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -933,14 +954,25 @@ def _visit_phase_operation( list[qasm3_ast.Statement]: The unrolled quantum phase operation. """ logger.debug("Visiting phase operation '%s'", str(operation)) + if ctrls is None: + ctrls = [] if len(ctrls) > 0: - return self._visit_basic_gate_operation(qasm3_ast.QuantumGate( - modifiers=[qasm3_ast.QuantumGateModifier(qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls)-1))], - name=qasm3_ast.Identifier("p"), - qubits=ctrls[0:1], - arguments=[operation.argument] - ), inverse, ctrls[:-1]) + return self._visit_basic_gate_operation( + qasm3_ast.QuantumGate( + modifiers=[ + qasm3_ast.QuantumGateModifier( + qasm3_ast.GateModifierName.ctrl, + qasm3_ast.IntegerLiteral(len(ctrls) - 1), + ) + ], + name=qasm3_ast.Identifier("p"), + qubits=ctrls[0:1], + arguments=[operation.argument], + ), + inverse, + ctrls[:-1], + ) evaluated_arg = Qasm3ExprEvaluator.evaluate_expression(operation.argument)[0] if inverse: @@ -964,10 +996,10 @@ def _visit_phase_operation( return [operation] - def _visit_generic_gate_operation( + def _visit_generic_gate_operation( # pylint: disable=too-many-branches self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], - ctrls: list[qasm3_ast.IndexedIdentifier] = [], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -979,6 +1011,8 @@ def _visit_generic_gate_operation( """ operation, ctrls = copy.deepcopy(operation), copy.deepcopy(ctrls) negctrls = [] + if ctrls is None: + ctrls = [] # only needs to be done once for a gate operation if ( @@ -1041,7 +1075,7 @@ def _visit_generic_gate_operation( span=operation.span, ) ctrl_qubits = operation.qubits[ctrl_arg_ind : ctrl_arg_ind + count] - + # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count ctrls.extend(ctrl_qubits) @@ -1065,14 +1099,13 @@ def _visit_generic_gate_operation( result: list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]] = [] for _ in range(power_value): if isinstance(operation, qasm3_ast.QuantumPhase): - r = self._visit_phase_operation(operation, inverse_value, ctrls) + result.extend(self._visit_phase_operation(operation, inverse_value, ctrls)) elif operation.name.name in self._external_gates: - r = self._visit_external_gate_operation(operation, inverse_value, ctrls) + result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls)) elif operation.name.name in self._custom_gates: - r = self._visit_custom_gate_operation(operation, inverse_value, ctrls) + result.extend(self._visit_custom_gate_operation(operation, inverse_value, ctrls)) else: - r = self._visit_basic_gate_operation(operation, inverse_value, ctrls) - result.extend(r) + result.extend(self._visit_basic_gate_operation(operation, inverse_value, ctrls)) # negctrl -> ctrl conversion negs = [ diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 2bcef1ec..3a6feb33 100644 --- a/tests/qasm3/test_gates.py +++ b/tests/qasm3/test_gates.py @@ -417,6 +417,7 @@ def test_negctrl_gate_modifier(): check_single_qubit_gate_op(result.unrolled_ast, 2, [0, 0], "x") check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cz") + def test_ctrl_in_custom_gate(): qasm3_string = """ OPENQASM 3.0; @@ -435,6 +436,7 @@ def test_ctrl_in_custom_gate(): check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx") check_three_qubit_gate_op(result.unrolled_ast, 1, [[0, 1, 2]], "ccx") + def test_ctrl_in_subroutine(): qasm3_string = """ OPENQASM 3.0; @@ -446,13 +448,14 @@ def f(qubit a, qubit b) { qubit[2] q; f(q[0], q[1]); """ - + result = loads(qasm3_string) result.unroll() assert result.num_qubits == 2 assert result.num_clbits == 0 check_two_qubit_gate_op(result.unrolled_ast, 1, [[0, 1]], "cx") + def test_ctrl_in_if_block(): qasm3_string = """ OPENQASM 3.0; @@ -478,6 +481,7 @@ def test_ctrl_in_if_block(): result.unroll() check_unrolled_qasm(dumps(result), expected_qasm) + def test_ctrl_in_for_loop(): qasm3_string = """ OPENQASM 3.0; @@ -493,6 +497,7 @@ def test_ctrl_in_for_loop(): assert result.num_qubits == 4 check_two_qubit_gate_op(result.unrolled_ast, 3, [(0, 1), (1, 2), (2, 3)], "cx") + def test_ctrl_unroll(): qasm3_string = """ OPENQASM 3.0; @@ -512,6 +517,7 @@ def test_ctrl_unroll(): result.unroll() check_unrolled_qasm(dumps(result), expected_qasm) + def test_ctrl_gphase_eq_p(): qasm3_str_gphase = """ OPENQASM 3.0; @@ -530,6 +536,7 @@ def test_ctrl_gphase_eq_p(): result_p.unroll() check_unrolled_qasm(dumps(result_gphase), dumps(result_p)) + def test_nested_gate_modifiers(): qasm3_string = """ OPENQASM 3; From 0b029e8b20e1bcb0e0646803713b23b83ec5b3ae Mon Sep 17 00:00:00 2001 From: TheGupta2012 Date: Wed, 12 Feb 2025 16:33:48 +0530 Subject: [PATCH 20/20] fix types --- src/pyqasm/visitor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index 184bd431..8da003e7 100644 --- a/src/pyqasm/visitor.py +++ b/src/pyqasm/visitor.py @@ -686,6 +686,7 @@ def _update_qubit_depth_for_gate( for qubit_subset in all_targets: max_involved_depth = 0 for qubit in qubit_subset + ctrls: + assert isinstance(qubit.indices[0], list) _qid_ = qubit.indices[0][0] qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] @@ -693,6 +694,7 @@ def _update_qubit_depth_for_gate( max_involved_depth = max(max_involved_depth, qubit_node.depth + 1) for qubit in qubit_subset + ctrls: + assert isinstance(qubit.indices[0], list) _qid_ = qubit.indices[0][0] qubit_id = Qasm3ExprEvaluator.evaluate_expression(_qid_)[0] # type: ignore qubit_node = self._module._qubit_depths[(qubit.name.name, qubit_id)] @@ -915,8 +917,6 @@ def _visit_external_gate_operation( qasm3_ast.QuantumGateModifier( qasm3_ast.GateModifierName.ctrl, qasm3_ast.IntegerLiteral(len(ctrls)) ) - if len(ctrls) > 0 - else None ) def gate_function(*qubits): @@ -967,9 +967,9 @@ def _visit_phase_operation( ) ], name=qasm3_ast.Identifier("p"), - qubits=ctrls[0:1], + qubits=ctrls[0:1], # type: ignore arguments=[operation.argument], - ), + ), # type: ignore inverse, ctrls[:-1], ) @@ -1030,7 +1030,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches ) ) - operation.qubits = self._get_op_bits( + operation.qubits = self._get_op_bits( # type: ignore operation, reg_size_map=self._global_qreg_size_map, qubits=True ) @@ -1078,7 +1078,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches # TODO: assert ctrl_qubits are single qubits ctrl_arg_ind += count - ctrls.extend(ctrl_qubits) + ctrls.extend(ctrl_qubits) # type: ignore if modifier_name == qasm3_ast.GateModifierName.negctrl: negctrls.extend(ctrl_qubits) @@ -1111,7 +1111,7 @@ def _visit_generic_gate_operation( # pylint: disable=too-many-branches negs = [ qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls ] - result = negs + result + negs + result = negs + result + negs # type: ignore if self._check_only: return []