diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index e92ace52a850..0b71689d1fae 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -162,7 +162,15 @@ def __setitem__(self, name_str, instruction): def __getitem__(self, key): if isinstance(key, Instruction): - return self._data.get(id(key), key.name) + try: + # Registered gates. + return self._data[id(key)] + except KeyError: + pass + # Built-in gates. + if key.name not in self._data: + raise KeyError(key) + return key.name return self._data[key] def __iter__(self): @@ -184,10 +192,31 @@ def __contains__(self, instruction): def register(self, instruction): """Register an instruction in the namespace""" - if instruction.name in self._data: - self[f"{instruction.name}_{id(instruction)}"] = instruction + # The second part of the condition is a nasty hack to ensure that gates that come with at + # least one parameter always have their id in the name. This is a workaround a bug, where + # gates with parameters do not contain the information required to build the gate definition + # in symbolic form (unless the parameters are all symbolic). The exporter currently + # (2021-12-01) builds gate declarations with parameters in the signature, but then ignores + # those parameters during the body, and just uses the concrete values from the first + # instance of the gate it sees, such as: + # gate rzx(_gate_p_0) _gate_q_0, _gate_q_1 { + # h _gate_q_1; + # cx _gate_q_0, _gate_q_1; + # rz(0.2) _gate_q_1; // <- note the concrete value. + # cx _gate_q_0, _gate_q_1; + # h _gate_q_1; + # } + # This then means that multiple calls to the same gate with different parameters will be + # incorrect. By forcing all gates to be defined including their id, we generate a QASM3 + # program that does what was intended, even though the output QASM3 is silly. See gh-7335. + if instruction.name in self._data or ( + isinstance(instruction, Gate) + and not all(isinstance(param, Parameter) for param in instruction.params) + ): + key = f"{instruction.name}_{id(instruction)}" else: - self[instruction.name] = instruction + key = instruction.name + self[key] = instruction # A _Scope is the structure used in the builder to store the contexts and re-mappings of bits from @@ -421,14 +450,23 @@ def build_definition(self, instruction, builder): def build_opaque_definition(self, instruction): """Builds an Opaque gate definition as a CalibrationDefinition""" - name = self.global_namespace[instruction] - quantum_arguments = [ - ast.Identifier(f"{self.gate_qubit_prefix}_{n}") for n in range(instruction.num_qubits) - ] - return ast.CalibrationDefinition(ast.Identifier(name), quantum_arguments) + # We can't do anything sensible with this yet, so it's better to loudly say that. + raise QASM3ExporterError( + "Exporting opaque instructions with pulse-level calibrations is not yet supported by" + " the OpenQASM 3 exporter. Received this instruction, which appears opaque:" + f"\n{instruction}" + ) def build_subroutine_definition(self, instruction): """Builds a SubroutineDefinition""" + if instruction.definition.parameters: + # We don't yet have the type system to store the parameter types in a symbol table, and + # we currently don't have the correct logic in place to handle parameters correctly in + # the definition. + raise QASM3ExporterError( + "Exporting subroutines with parameters is not yet supported by the OpenQASM 3" + " exporter. Received this instruction, which appears parameterized:\n{instruction}" + ) name = self.global_namespace[instruction] self.push_context(instruction.definition) quantum_arguments = [ diff --git a/releasenotes/notes/qasm3-limitations-ebfdedab3f4ab6e1.yaml b/releasenotes/notes/qasm3-limitations-ebfdedab3f4ab6e1.yaml new file mode 100644 index 000000000000..248c4a7b66e0 --- /dev/null +++ b/releasenotes/notes/qasm3-limitations-ebfdedab3f4ab6e1.yaml @@ -0,0 +1,8 @@ +--- +issues: + - | + The OpenQASM 3 export capabilities are in a beta state, and some features of + Terra's :obj:`.QuantumCircuit` are not yet supported. In particular, you + may see errors if you try to export custom subroutines with classical + parameters, and there is no provision yet for exporting pulse-calibrated + operations into OpenPulse. diff --git a/test/python/circuit/test_circuit_qasm3.py b/test/python/circuit/test_circuit_qasm3.py index e3a975a3ec6c..f1b304d55bc8 100644 --- a/test/python/circuit/test_circuit_qasm3.py +++ b/test/python/circuit/test_circuit_qasm3.py @@ -21,7 +21,7 @@ import ddt from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile -from qiskit.circuit import Parameter, Qubit, Clbit +from qiskit.circuit import Parameter, Qubit, Clbit, Instruction from qiskit.test import QiskitTestCase from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError from qiskit.qasm3.exporter import QASM3Builder @@ -29,6 +29,11 @@ from qiskit.qasm import pi +# Tests marked with this decorator should be restored after gate definition with parameters is fixed +# properly, and the dummy tests after them should be deleted. See gh-7335. +requires_fixed_parameterisation = unittest.expectedFailure + + class TestQASM3Functions(QiskitTestCase): """QASM3 module - high level functions""" @@ -63,6 +68,8 @@ def test_dump(self): class TestCircuitQASM3(QiskitTestCase): """QASM3 exporter.""" + maxDiff = 1_000_000 + def test_regs_conds_qasm(self): """Test with registers and conditionals.""" qr1 = QuantumRegister(1, "qr1") @@ -377,6 +384,7 @@ def test_custom_gate_with_bound_parameter(self): ) self.assertEqual(Exporter().dumps(circuit), expected_qasm) + @requires_fixed_parameterisation def test_custom_gate_with_params_bound_main_call(self): """Custom gate with unbound parameters that are bound in the main circuit""" parameter0 = Parameter("p0") @@ -461,6 +469,40 @@ def test_unbound_circuit(self): ) self.assertEqual(Exporter().dumps(qc), expected_qasm) + def test_unknown_parameterized_gate_called_multiple_times(self): + """Test that a parameterised gate is called correctly if the first instance of it is + generic.""" + x, y = Parameter("x"), Parameter("y") + qc = QuantumCircuit(2) + qc.rzx(x, 0, 1) + qc.rzx(y, 0, 1) + qc.rzx(0.5, 0, 1) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + "gate rzx(x) _gate_q_0, _gate_q_1 {", + " h _gate_q_1;", + " cx _gate_q_0, _gate_q_1;", + " rz(x) _gate_q_1;", + " cx _gate_q_0, _gate_q_1;", + " h _gate_q_1;", + "}", + "input float[64] x;", + "input float[64] y;", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", + "rzx(x) q[0], q[1];", + "rzx(y) q[0], q[1];", + "rzx(0.5) q[0], q[1];", + "", + ] + ) + + # Set the includes and basis gates to ensure that this gate is unknown. + exporter = Exporter(includes=[], basis_gates=("rz", "h", "cx")) + self.assertEqual(exporter.dumps(qc), expected_qasm) + def test_gate_qasm_with_ctrl_state(self): """Test with open controlled gate that has ctrl_state""" qc = QuantumCircuit(2) @@ -507,6 +549,7 @@ def test_custom_gate_collision_with_stdlib(self): ) self.assertEqual(Exporter().dumps(qc), expected_qasm) + @requires_fixed_parameterisation def test_no_include(self): """Test explicit gate declaration (no include)""" q = QuantumRegister(2, "q") @@ -553,6 +596,7 @@ def test_no_include(self): ) self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) + @requires_fixed_parameterisation def test_teleportation(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) @@ -613,6 +657,7 @@ def test_teleportation(self): ) self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) + @requires_fixed_parameterisation def test_basis_gates(self): """Teleportation with physical qubits""" qc = QuantumCircuit(3, 2) @@ -1279,6 +1324,254 @@ def test_custom_gate_used_in_loop_scope(self): self.assertEqual(dumps(qc), expected_qasm) +class TestCircuitQASM3ExporterTemporaryCasesWithBadParameterisation(QiskitTestCase): + """Test functionality that is not what we _want_, but is what we need to do while the definition + of custom gates with parameterisation does not work correctly. + + These tests are modified versions of those marked with the `requires_fixed_parameterisation` + decorator, and this whole class can be deleted once those are fixed. See gh-7335. + """ + + maxDiff = 1_000_000 + + def test_basis_gates(self): + """Teleportation with physical qubits""" + qc = QuantumCircuit(3, 2) + first_h = qc.h(1)[0] + qc.cx(1, 2) + qc.barrier() + qc.cx(0, 1) + qc.h(0) + qc.barrier() + qc.measure([0, 1], [0, 1]) + qc.barrier() + first_x = qc.x(2).c_if(qc.clbits[1], 1)[0] + qc.z(2).c_if(qc.clbits[0], 1) + + u2 = first_h.definition.data[0][0] + u3_1 = u2.definition.data[0][0] + u3_2 = first_x.definition.data[0][0] + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(pi/2, 0, pi) _gate_q_0;", + "}", + f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", + f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", + "}", + "gate h _gate_q_0 {", + f" u2_{id(u2)}(0, pi) _gate_q_0;", + "}", + f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", + "}", + "bit[2] c;", + "qubit[3] _all_qubits;", + "let q = _all_qubits[0:2];", + "h q[1];", + "cx q[1], q[2];", + "barrier q[0], q[1], q[2];", + "cx q[0], q[1];", + "h q[0];", + "barrier q[0], q[1], q[2];", + "c[0] = measure q[0];", + "c[1] = measure q[1];", + "barrier q[0], q[1], q[2];", + "if (c[1] == 1) {", + " x q[2];", + "}", + "if (c[0] == 1) {", + " z q[2];", + "}", + "", + ] + ) + self.assertEqual( + Exporter(includes=[], basis_gates=["cx", "z", "U"]).dumps(qc), + expected_qasm, + ) + + def test_teleportation(self): + """Teleportation with physical qubits""" + qc = QuantumCircuit(3, 2) + qc.h(1) + qc.cx(1, 2) + qc.barrier() + qc.cx(0, 1) + qc.h(0) + qc.barrier() + qc.measure([0, 1], [0, 1]) + qc.barrier() + qc.x(2).c_if(qc.clbits[1], 1) + qc.z(2).c_if(qc.clbits[0], 1) + + transpiled = transpile(qc, initial_layout=[0, 1, 2]) + first_h = transpiled.data[0][0] + u2 = first_h.definition.data[0][0] + u3_1 = u2.definition.data[0][0] + first_x = transpiled.data[-2][0] + u3_2 = first_x.definition.data[0][0] + first_z = transpiled.data[-1][0] + u1 = first_z.definition.data[0][0] + u3_3 = u1.definition.data[0][0] + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(pi/2, 0, pi) _gate_q_0;", + "}", + f"gate u2_{id(u2)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", + f" u3_{id(u3_1)}(pi/2, 0, pi) _gate_q_0;", + "}", + "gate h _gate_q_0 {", + f" u2_{id(u2)}(0, pi) _gate_q_0;", + "}", + f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(pi, 0, pi) _gate_q_0;", + "}", + "gate x _gate_q_0 {", + f" u3_{id(u3_2)}(pi, 0, pi) _gate_q_0;", + "}", + f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(0, 0, pi) _gate_q_0;", + "}", + f"gate u1_{id(u1)}(_gate_p_0) _gate_q_0 {{", + f" u3_{id(u3_3)}(0, 0, pi) _gate_q_0;", + "}", + "gate z _gate_q_0 {", + f" u1_{id(u1)}(pi) _gate_q_0;", + "}", + "bit[2] c;", + "h $1;", + "cx $1, $2;", + "barrier $0, $1, $2;", + "cx $0, $1;", + "h $0;", + "barrier $0, $1, $2;", + "c[0] = measure $0;", + "c[1] = measure $1;", + "barrier $0, $1, $2;", + "if (c[1] == 1) {", + " x $2;", + "}", + "if (c[0] == 1) {", + " z $2;", + "}", + "", + ] + ) + self.assertEqual(Exporter(includes=[]).dumps(transpiled), expected_qasm) + + def test_custom_gate_with_params_bound_main_call(self): + """Custom gate with unbound parameters that are bound in the main circuit""" + parameter0 = Parameter("p0") + parameter1 = Parameter("p1") + + custom = QuantumCircuit(2, name="custom") + custom.rz(parameter0, 0) + custom.rz(parameter1 / 2, 1) + + qr_all_qubits = QuantumRegister(3, "q") + qr_r = QuantumRegister(3, "r") + circuit = QuantumCircuit(qr_all_qubits, qr_r) + circuit.append(custom.to_gate(), [qr_all_qubits[0], qr_r[0]]) + + circuit.assign_parameters({parameter0: pi, parameter1: pi / 2}, inplace=True) + custom_id = id(circuit.data[0][0]) + + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + 'include "stdgates.inc";', + f"gate custom_{custom_id}(p0, p1) _gate_q_0, _gate_q_1 {{", + " rz(pi) _gate_q_0;", + " rz(pi/4) _gate_q_1;", + "}", + "qubit[6] _all_qubits;", + "let q = _all_qubits[0:2];", + "let r = _all_qubits[3:5];", + f"custom_{custom_id}(pi, pi/2) q[0], r[0];", + "", + ] + ) + self.assertEqual(Exporter().dumps(circuit), expected_qasm) + + def test_no_include(self): + """Test explicit gate declaration (no include)""" + q = QuantumRegister(2, "q") + circuit = QuantumCircuit(q) + circuit.rz(pi / 2, 0) + circuit.sx(0) + circuit.cx(0, 1) + + rz = circuit.data[0][0] + u1_1 = rz.definition.data[0][0] + u3_1 = u1_1.definition.data[0][0] + sx = circuit.data[1][0] + sdg = sx.definition.data[0][0] + u1_2 = sdg.definition.data[0][0] + u3_2 = u1_2.definition.data[0][0] + h_ = sx.definition.data[1][0] + u2_1 = h_.definition.data[0][0] + u3_3 = u2_1.definition.data[0][0] + expected_qasm = "\n".join( + [ + "OPENQASM 3;", + "gate cx c, t {", + " ctrl @ U(pi, 0, pi) c, t;", + "}", + f"gate u3_{id(u3_1)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(0, 0, pi/2) _gate_q_0;", + "}", + f"gate u1_{id(u1_1)}(_gate_p_0) _gate_q_0 {{", + f" u3_{id(u3_1)}(0, 0, pi/2) _gate_q_0;", + "}", + f"gate rz_{id(rz)}(_gate_p_0) _gate_q_0 {{", + f" u1_{id(u1_1)}(pi/2) _gate_q_0;", + "}", + f"gate u3_{id(u3_2)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(0, 0, -pi/2) _gate_q_0;", + "}", + f"gate u1_{id(u1_2)}(_gate_p_0) _gate_q_0 {{", + f" u3_{id(u3_2)}(0, 0, -pi/2) _gate_q_0;", + "}", + "gate sdg _gate_q_0 {", + f" u1_{id(u1_2)}(-pi/2) _gate_q_0;", + "}", + f"gate u3_{id(u3_3)}(_gate_p_0, _gate_p_1, _gate_p_2) _gate_q_0 {{", + " U(pi/2, 0, pi) _gate_q_0;", + "}", + f"gate u2_{id(u2_1)}(_gate_p_0, _gate_p_1) _gate_q_0 {{", + f" u3_{id(u3_3)}(pi/2, 0, pi) _gate_q_0;", + "}", + "gate h _gate_q_0 {", + f" u2_{id(u2_1)}(0, pi) _gate_q_0;", + "}", + "gate sx _gate_q_0 {", + " sdg _gate_q_0;", + " h _gate_q_0;", + " sdg _gate_q_0;", + "}", + "qubit[2] _all_qubits;", + "let q = _all_qubits[0:1];", + f"rz_{id(rz)}(pi/2) q[0];", + "sx q[0];", + "cx q[0], q[1];", + "", + ] + ) + self.assertEqual(Exporter(includes=[]).dumps(circuit), expected_qasm) + + @ddt.ddt class TestQASM3ExporterFailurePaths(QiskitTestCase): """Tests of the failure paths for the exporter.""" @@ -1306,3 +1599,31 @@ def test_disallow_for_loops_with_non_integers(self, indices): QASM3ExporterError, r"The values in QASM 3 'for' loops must all be integers.*" ): exporter.dumps(qc) + + def test_disallow_custom_subroutine_with_parameters(self): + """Test that the exporter throws an error instead of trying to export a subroutine with + parameters, while this is not supported.""" + subroutine = QuantumCircuit(1) + subroutine.rx(Parameter("x"), 0) + + qc = QuantumCircuit(1) + qc.append(subroutine.to_instruction(), [0], []) + + exporter = Exporter() + with self.assertRaisesRegex( + QASM3ExporterError, "Exporting subroutines with parameters is not yet supported" + ): + exporter.dumps(qc) + + def test_disallow_opaque_instruction(self): + """Test that the exporter throws an error instead of trying to export something into a + ``defcal`` block, while this is not supported.""" + + qc = QuantumCircuit(1) + qc.append(Instruction("opaque", 1, 0, []), [0], []) + + exporter = Exporter() + with self.assertRaisesRegex( + QASM3ExporterError, "Exporting opaque instructions .* is not yet supported" + ): + exporter.dumps(qc)