From 692dcf5de60533bf1e80d27360fdade1b486ce7d Mon Sep 17 00:00:00 2001 From: Pradnya Khalate Date: Tue, 16 Jul 2024 13:03:18 -0700 Subject: [PATCH 1/3] * Check the expected number of arguments and datatypes in Python AST bridge * Check that control argument(s) is provided if `.ctrl` attribute is used * Add bugs reported in issues #9, #670, and #1641 as tests --- python/cudaq/kernel/ast_bridge.py | 94 ++++++++++++++++--- python/tests/builder/test_kernel_builder.py | 19 ++++ python/tests/kernel/test_kernel_features.py | 54 ++++++++++- .../tests/synthesis/test_custom_operations.py | 14 +++ 4 files changed, 166 insertions(+), 15 deletions(-) diff --git a/python/cudaq/kernel/ast_bridge.py b/python/cudaq/kernel/ast_bridge.py index 0bdf48144b..8491d67838 100644 --- a/python/cudaq/kernel/ast_bridge.py +++ b/python/cudaq/kernel/ast_bridge.py @@ -1291,7 +1291,7 @@ def visit_Call(self, node): self.visit(keyword.value) namedArgs[keyword.arg] = self.popValue() - if node.func.id == "len": + if node.func.id == 'len': listVal = self.ifPointerThenLoad(self.popValue()) if cc.StdvecType.isinstance(listVal.type): self.pushValue( @@ -1305,7 +1305,7 @@ def visit_Call(self, node): self.emitFatalError( "__len__ not supported on variables of this type.", node) - if node.func.id == "range": + if node.func.id == 'range': startVal, endVal, stepVal, isDecrementing = self.__processRangeLoopIterationBounds( node.args) @@ -1361,7 +1361,7 @@ def bodyBuilder(iterVar): self.pushValue(totalSize) return - if node.func.id == "enumerate": + if node.func.id == 'enumerate': # We have to have something "iterable" on the stack, # could be coming from `range()` or an iterable like `qvector` totalSize = None @@ -1403,11 +1403,12 @@ def extractFunctor(idxVal): "could not infer enumerate tuple type ({})".format( iterable.type), node) else: - # FIXME this should be an `emitFatalError` - assert len( - self.valueStack - ) == 2, 'Error in AST processing, should have 2 values on the stack for enumerate {}'.format( - ast.unparse(node) if hasattr(ast, 'unparse') else node) + if len(self.valueStack) != 2: + msg = 'Error in AST processing, should have 2 values on the stack for enumerate {}'.format( + ast.unparse(node) if hasattr(ast, 'unparse' + ) else node) + self.emitFatalError(msg) + totalSize = self.popValue() iterable = self.popValue() arrTy = cc.PointerType.getElementType(iterable.type) @@ -1472,16 +1473,17 @@ def bodyBuilder(iterVar): complex.CreateOp(self.getComplexType(), real, imag).result) return - if node.func.id in ["h", "x", "y", "z", "s", "t"]: + if node.func.id in ['h', 'x', 'y', 'z', 's', 't']: # Here we enable application of the op on all the # provided arguments, e.g. `x(qubit)`, `x(qvector)`, `x(q, r)`, etc. numValues = len(self.valueStack) qubitTargets = [self.popValue() for _ in range(numValues)] qubitTargets.reverse() + self.checkControlAndTargetTypes([], qubitTargets) self.__applyQuantumOperation(node.func.id, [], qubitTargets) return - if node.func.id in ["ch", "cx", "cy", "cz", "cs", "ct"]: + if node.func.id in ['ch', 'cx', 'cy', 'cz', 'cs', 'ct']: # These are single target controlled quantum operations MAX_ARGS = 2 numValues = len(self.valueStack) @@ -1504,18 +1506,26 @@ def bodyBuilder(iterVar): negated_qubit_controls=negatedControlQubits) return - if node.func.id in ["rx", "ry", "rz", "r1"]: + if node.func.id in ['rx', 'ry', 'rz', 'r1']: numValues = len(self.valueStack) + if numValues < 2: + self.emitFatalError( + f'invalid number of arguments ({numValues}) passed to {node.func.id} (requires at least 2 arguments)', + node) qubitTargets = [self.popValue() for _ in range(numValues - 1)] qubitTargets.reverse() param = self.popValue() if IntegerType.isinstance(param.type): param = arith.SIToFPOp(self.getFloatType(), param).result + elif not F64Type.isinstance(param.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', node) + self.checkControlAndTargetTypes([], qubitTargets) self.__applyQuantumOperation(node.func.id, [param], qubitTargets) return - if node.func.id in ["crx", "cry", "crz", "cr1"]: + if node.func.id in ['crx', 'cry', 'crz', 'cr1']: ## These are single target, one parameter, controlled quantum operations MAX_ARGS = 3 numValues = len(self.valueStack) @@ -1529,13 +1539,16 @@ def bodyBuilder(iterVar): param = self.popValue() if IntegerType.isinstance(param.type): param = arith.SIToFPOp(self.getFloatType(), param).result + elif not F64Type.isinstance(param.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', node) # Map `crx` to `RxOp`... opCtor = getattr( quake, '{}Op'.format(node.func.id.title()[1:].capitalize())) opCtor([], [param], [control], [target]) return - if node.func.id in ["sdg", "tdg"]: + if node.func.id in ['sdg', 'tdg']: target = self.popValue() self.checkControlAndTargetTypes([], [target]) # Map `sdg` to `SOp`... @@ -1612,6 +1625,7 @@ def bodyBuilder(iterVal): if node.func.id == 'reset': target = self.popValue() + self.checkControlAndTargetTypes([], [target]) if quake.RefType.isinstance(target.type): quake.ResetOp([], target) return @@ -1638,22 +1652,39 @@ def bodyBuilder(iterVal): all_args = [ self.popValue() for _ in range(len(self.valueStack)) ] + if len(all_args) < 4: + self.emitFatalError( + f'invalid number of arguments ({len(all_args)}) passed to {node.func.id} (requires at least 4 arguments)', + node) qubitTargets = all_args[:-3] qubitTargets.reverse() + self.checkControlAndTargetTypes([], qubitTargets) params = all_args[-3:] params.reverse() for idx, val in enumerate(params): if IntegerType.isinstance(val.type): params[idx] = arith.SIToFPOp(self.getFloatType(), val).result + elif not F64Type.isinstance(val.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', + node) self.__applyQuantumOperation(node.func.id, params, qubitTargets) return if node.func.id in globalRegisteredOperations: unitary = globalRegisteredOperations[node.func.id] numTargets = int(np.log2(np.sqrt(unitary.size))) + + numValues = len(self.valueStack) + if numValues != numTargets: + self.emitFatalError( + f'invalid number of arguments ({numValues}) passed to {node.func.id} (requires {numTargets} arguments)', + node) + targets = [self.popValue() for _ in range(numTargets)] targets.reverse() + self.checkControlAndTargetTypes([], targets) globalName = f'{nvqppPrefix}{node.func.id}_generator_{numTargets}.rodata' @@ -2201,6 +2232,10 @@ def maybeProposeOpAttrFix(opName, attrName): controls = [ self.popValue() for i in range(len(node.args) - 1) ] + if not controls: + self.emitFatalError( + 'controlled operation requested without any control argument(s).', + node) negatedControlQubits = None if len(self.controlNegations): negCtrlBools = [None] * len(controls) @@ -2254,6 +2289,10 @@ def bodyBuilder(iterVal): controls = [ self.popValue() for i in range(len(self.valueStack)) ] + if not controls: + self.emitFatalError( + 'controlled operation requested without any control argument(s).', + node) opCtor = getattr(quake, '{}Op'.format(node.func.value.id.title())) self.checkControlAndTargetTypes(controls, [targetA, targetB]) @@ -2268,9 +2307,17 @@ def bodyBuilder(iterVal): ] param = controls[-1] controls = controls[:-1] + if not controls: + self.emitFatalError( + 'controlled operation requested without any control argument(s).', + node) if IntegerType.isinstance(param.type): param = arith.SIToFPOp(self.getFloatType(), param).result + elif not F64Type.isinstance(param.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', + node) opCtor = getattr(quake, '{}Op'.format(node.func.value.id.title())) self.checkControlAndTargetTypes(controls, [target]) @@ -2283,6 +2330,10 @@ def bodyBuilder(iterVal): if IntegerType.isinstance(param.type): param = arith.SIToFPOp(self.getFloatType(), param).result + elif not F64Type.isinstance(param.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', + node) opCtor = getattr(quake, '{}Op'.format(node.func.value.id.title())) self.checkControlAndTargetTypes([], [target]) @@ -2321,13 +2372,20 @@ def bodyBuilder(iterVal): if node.func.attr == 'ctrl': controls = other_args[:-3] + if not controls: + self.emitFatalError( + 'controlled operation requested without any control argument(s).', + node) params = other_args[-3:] params.reverse() for idx, val in enumerate(params): if IntegerType.isinstance(val.type): params[idx] = arith.SIToFPOp( self.getFloatType(), val).result - + elif not F64Type.isinstance(val.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', + node) negatedControlQubits = None if len(self.controlNegations): negCtrlBools = [None] * len(controls) @@ -2351,6 +2409,10 @@ def bodyBuilder(iterVal): if IntegerType.isinstance(val.type): params[idx] = arith.SIToFPOp( self.getFloatType(), val).result + elif not F64Type.isinstance(val.type): + self.emitFatalError( + 'rotational parameter must be a float, or int.', + node) self.checkControlAndTargetTypes([], [target]) if quake.VeqType.isinstance(target.type): @@ -2404,6 +2466,10 @@ def bodyBuilder(iterVal): controls = [ self.popValue() for _ in range(numValues - numTargets) ] + if not controls: + self.emitFatalError( + 'controlled operation requested without any control argument(s).', + node) negatedControlQubits = None if len(self.controlNegations): negCtrlBools = [None] * len(controls) diff --git a/python/tests/builder/test_kernel_builder.py b/python/tests/builder/test_kernel_builder.py index 7cc347d4b9..c63840117c 100644 --- a/python/tests/builder/test_kernel_builder.py +++ b/python/tests/builder/test_kernel_builder.py @@ -1420,6 +1420,25 @@ def test_builder_rotate_state(): assert '10' in counts +def test_issue_9(): + + kernel, features = cudaq.make_kernel(list) + qubits = kernel.qalloc(8) + kernel.rx(features[0], qubits[100]) + + with pytest.raises(RuntimeError) as error: + kernel([3.14]) + + +def test_issue_670(): + + kernel = cudaq.make_kernel() + qubits = kernel.qalloc(1) + kernel.ry(0.1, qubits) + + cudaq.sample(kernel) + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) diff --git a/python/tests/kernel/test_kernel_features.py b/python/tests/kernel/test_kernel_features.py index 6cf3aaad72..d94167d74e 100644 --- a/python/tests/kernel/test_kernel_features.py +++ b/python/tests/kernel/test_kernel_features.py @@ -1149,7 +1149,7 @@ def test_kernel(nQubits: int): with pytest.raises(RuntimeError) as e: test_kernel.compile() - assert 'quantum operation h on incorrect quantum type' in repr(e) + assert 'target operand 0 is not of quantum type' in repr(e) @cudaq.kernel def test_kernel(nQubits: int): @@ -1618,6 +1618,58 @@ def kd(): assert len(counts) == 2 and '0' in counts and '1' in counts +def test_issue_9(): + + @cudaq.kernel + def kernel(features: list[float]): + qubits = cudaq.qvector(8) + rx(features[0], qubits[100]) + + with pytest.raises(RuntimeError) as error: + kernel([3.14]) + + +def test_issue_1641(): + + @cudaq.kernel + def less_arguments(): + q = cudaq.qubit() + rx(3.14) + + with pytest.raises(RuntimeError) as error: + print(less_arguments) + assert 'invalid number of arguments (1) passed to rx (requires at least 2 arguments)' in repr( + error) + + @cudaq.kernel + def wrong_arguments(): + q = cudaq.qubit() + rx("random_argument", q) + + with pytest.raises(RuntimeError) as error: + print(wrong_arguments) + assert 'rotational parameter must be a float, or int' in repr(error) + + @cudaq.kernel + def wrong_type(): + q = cudaq.qubit() + x("random_argument") + + with pytest.raises(RuntimeError) as error: + print(wrong_type) + assert 'target operand 0 is not of quantum type' in repr(error) + + @cudaq.kernel + def invalid_ctrl(): + q = cudaq.qubit() + rx.ctrl(np.pi, q) + + with pytest.raises(RuntimeError) as error: + print(invalid_ctrl) + assert 'controlled operation requested without any control argument(s)' in repr( + error) + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) diff --git a/python/tests/synthesis/test_custom_operations.py b/python/tests/synthesis/test_custom_operations.py index c67b36edb5..91646df7ef 100644 --- a/python/tests/synthesis/test_custom_operations.py +++ b/python/tests/synthesis/test_custom_operations.py @@ -191,6 +191,20 @@ def test_builder_mode(): check_bell(kernel) +def test_invalid_ctrl(): + cudaq.register_operation("custom_x", np.array([0, 1, 1, 0])) + + @cudaq.kernel + def bell(): + q = cudaq.qubit() + custom_x.ctrl(q) + + with pytest.raises(RuntimeError) as error: + bell.compile() + assert 'controlled operation requested without any control argument(s)' in repr( + error) + + # leave for gdb debugging if __name__ == "__main__": loc = os.path.abspath(__file__) From b6bc20e3b5e8be2002f126a88310ef4f4bd0db81 Mon Sep 17 00:00:00 2001 From: Pradnya Khalate Date: Tue, 16 Jul 2024 14:21:55 -0700 Subject: [PATCH 2/3] * Update the tests which silently worked if control arguments were missing. --- python/tests/display/test_draw.py | 10 +++++----- python/tests/kernel/test_kernel_features.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/tests/display/test_draw.py b/python/tests/display/test_draw.py index a5295113c6..995c7108fc 100644 --- a/python/tests/display/test_draw.py +++ b/python/tests/display/test_draw.py @@ -40,11 +40,11 @@ def kernel(): r1(3.14159, q[0]) tdg(q[1]) s(q[2]) - swap.ctrl(q[0], q[2]) - swap.ctrl(q[1], q[2]) - swap.ctrl(q[0], q[1]) - swap.ctrl(q[0], q[2]) - swap.ctrl(q[1], q[2]) + swap(q[0], q[2]) + swap(q[1], q[2]) + swap(q[0], q[1]) + swap(q[0], q[2]) + swap(q[1], q[2]) swap.ctrl(q[3], q[0], q[1]) swap.ctrl(q[0], q[3], q[1], q[2]) swap.ctrl(q[1], q[0], q[3]) diff --git a/python/tests/kernel/test_kernel_features.py b/python/tests/kernel/test_kernel_features.py index d94167d74e..252ba1b9db 100644 --- a/python/tests/kernel/test_kernel_features.py +++ b/python/tests/kernel/test_kernel_features.py @@ -1199,11 +1199,11 @@ def test_kernel(nQubits: int): @cudaq.kernel def test_kernel(nQubits: int): qubits = cudaq.qvector(nQubits) - swap.ctrl(3, 4) + swap.ctrl(2, 3, 4) with pytest.raises(RuntimeError) as e: test_kernel.compile() - assert 'target operand 0 is not of quantum type' in repr(e) + assert 'control operand 0 is not of quantum type' in repr(e) @cudaq.kernel def test_kernel(nQubits: int): @@ -1487,7 +1487,7 @@ def prog(theta: float): for _ in range(5): while True: x(q) - ry.ctrl(theta, q[1]) + ry(theta, q[1]) res = mz(q[1]) if res: From f530bda5062d2cadf198a047a386952c40da893e Mon Sep 17 00:00:00 2001 From: Pradnya Khalate Date: Tue, 16 Jul 2024 14:55:05 -0700 Subject: [PATCH 3/3] * Python version gotcha - skip tests using 'list' on Python version 3.8 --- python/tests/builder/test_kernel_builder.py | 1 + python/tests/kernel/test_kernel_features.py | 1 + 2 files changed, 2 insertions(+) diff --git a/python/tests/builder/test_kernel_builder.py b/python/tests/builder/test_kernel_builder.py index c63840117c..31e3a26b3d 100644 --- a/python/tests/builder/test_kernel_builder.py +++ b/python/tests/builder/test_kernel_builder.py @@ -1420,6 +1420,7 @@ def test_builder_rotate_state(): assert '10' in counts +@skipIfPythonLessThan39 def test_issue_9(): kernel, features = cudaq.make_kernel(list) diff --git a/python/tests/kernel/test_kernel_features.py b/python/tests/kernel/test_kernel_features.py index 252ba1b9db..c1255bf45c 100644 --- a/python/tests/kernel/test_kernel_features.py +++ b/python/tests/kernel/test_kernel_features.py @@ -1618,6 +1618,7 @@ def kd(): assert len(counts) == 2 and '0' in counts and '1' in counts +@skipIfPythonLessThan39 def test_issue_9(): @cudaq.kernel