diff --git a/src/pyqasm/maps/gates.py b/src/pyqasm/maps/gates.py index fbf3d7ec..33ddbbcf 100644 --- a/src/pyqasm/maps/gates.py +++ b/src/pyqasm/maps/gates.py @@ -1167,3 +1167,48 @@ def map_qasm_inv_op_to_callable(op_name: str): InversionOp.INVERT_ROTATION, ) 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. + + 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 + while c > 0 and ctrl_op_name in CTRL_GATE_MAP: + ctrl_op_name = CTRL_GATE_MAP[ctrl_op_name] + 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: + 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" + ) diff --git a/src/pyqasm/visitor.py b/src/pyqasm/visitor.py index c64d56e6..8da003e7 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 +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 @@ -630,7 +634,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, @@ -642,7 +646,10 @@ 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: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Broadcasts the application of a gate onto multiple sets of target qubits. @@ -657,11 +664,17 @@ def _broadcast_gate_operation( List of all executed gates. """ result = [] + if ctrls is None: + ctrls = [] 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]]): + 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: @@ -672,19 +685,26 @@ 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: - qubit_name, qubit_id = qubit.name.name, qubit.indices[0][0].value # type: ignore - qubit_node = self._module._qubit_depths[(qubit_name, qubit_id)] + 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)] 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)] + 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)] 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: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Visit a gate operation element. @@ -710,8 +730,17 @@ 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: - qasm_func, op_qubit_count = map_qasm_op_to_callable(operation.name.name) + 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( @@ -726,20 +755,37 @@ def _visit_basic_gate_operation( # pylint: disable=too-many-locals 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)) - self._update_qubit_depth_for_gate(unrolled_targets) + 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 + for g in self._broadcast_gate_operation( + unrolled_gate_function, unrolled_targets, None + ) + for g2 in self._visit_basic_gate_operation(g, False, ctrls) + ] + ) + else: + result.extend( + 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 + self, + operation: qasm3_ast.QuantumGate, + inverse: bool = False, + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a custom gate operation element recursively. @@ -756,6 +802,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] = ( @@ -803,7 +851,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)) else: # TODO: add control flow support raise_qasm3_error( @@ -818,7 +866,10 @@ 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: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumGate]: """Visit an external gate operation element. @@ -837,15 +888,17 @@ 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 - self._visit_custom_gate_operation(operation, inverse=inverse) + 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=inverse) + 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) @@ -855,16 +908,23 @@ 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)] + 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)) + ) + ) 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), ) ] @@ -879,7 +939,10 @@ 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: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[qasm3_ast.QuantumPhase]: """Visit a phase operation element. @@ -891,6 +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], # type: ignore + arguments=[operation.argument], + ), # type: ignore + inverse, + ctrls[:-1], + ) evaluated_arg = Qasm3ExprEvaluator.evaluate_expression(operation.argument)[0] if inverse: @@ -914,44 +996,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. - """ - power_value, inverse_value = 1, False - - 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) - elif modifier_name == qasm3_ast.GateModifierName.inv: - inverse_value = not inverse_value - 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 (power_value, inverse_value) - - def _visit_generic_gate_operation( - self, operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase] + def _visit_generic_gate_operation( # pylint: disable=too-many-branches + self, + operation: Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase], + ctrls: Optional[list[qasm3_ast.IndexedIdentifier]] = None, ) -> list[Union[qasm3_ast.QuantumGate, qasm3_ast.QuantumPhase]]: """Visit a gate operation element. @@ -961,8 +1009,10 @@ def _visit_generic_gate_operation( Returns: None """ - power_value, inverse_value = self._collapse_gate_modifiers(operation) - operation = copy.deepcopy(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 ( @@ -979,18 +1029,89 @@ 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 + + operation.qubits = self._get_op_bits( # type: ignore + 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 + for modifier in operation.modifiers: + modifier_name = modifier.modifier + if modifier_name == qasm3_ast.GateModifierName.pow and modifier.argument is not None: + 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, + ]: + 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( + "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] + + # TODO: assert ctrl_qubits are single qubits + ctrl_arg_ind += count + ctrls.extend(ctrl_qubits) # type: ignore + if modifier_name == qasm3_ast.GateModifierName.negctrl: + negctrls.extend(ctrl_qubits) + + power_value, inverse_value = abs(exponent), exponent < 0 + + operation.qubits = operation.qubits[ctrl_arg_ind:] + operation.modifiers = [] + + # apply pow(int) via duplication + if not isinstance(power_value, int): + raise_qasm3_error( + "Power modifiers with non-integer arguments are unsupported in gate " + f"operation {operation}", + span=operation.span, + ) + + # 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): - result.extend(self._visit_phase_operation(operation, inverse_value)) + result.extend(self._visit_phase_operation(operation, inverse_value, ctrls)) elif operation.name.name in self._external_gates: - result.extend(self._visit_external_gate_operation(operation, inverse_value)) + result.extend(self._visit_external_gate_operation(operation, inverse_value, ctrls)) elif operation.name.name in self._custom_gates: - result.extend(self._visit_custom_gate_operation(operation, inverse_value)) + result.extend(self._visit_custom_gate_operation(operation, inverse_value, ctrls)) else: - result.extend(self._visit_basic_gate_operation(operation, inverse_value)) + result.extend(self._visit_basic_gate_operation(operation, inverse_value, ctrls)) + + # negctrl -> ctrl conversion + negs = [ + qasm3_ast.QuantumGate([], qasm3_ast.Identifier("x"), [], [ctrl]) for ctrl in negctrls + ] + result = negs + result + negs # type: ignore if self._check_only: return [] diff --git a/tests/qasm3/test_depth.py b/tests/qasm3/test_depth.py index eeb8a31d..0cc17bc7 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; diff --git a/tests/qasm3/test_gates.py b/tests/qasm3/test_gates.py index 92a38638..3a6feb33 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; @@ -370,44 +388,221 @@ 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[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 == 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") + + +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_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[0] == true) { + 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[i], q[i+1]; + } + """ + result = loads(qasm3_string) + result.unroll() + 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_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; 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") - - -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() + 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") + + +@pytest.mark.parametrize( + "test", + [ + ( + """ + OPENQASM 3.0; + include "stdgates.inc"; + qubit[2] q; + 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())