Repository: Qiskit/qiskit

Issue:
QASM instruction not parameterized for two-qubit Pauli rotation gates
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: 0.19.0.dev0+0ee0824
- **Python version**: 3.9.6
- **Operating system**: Windows 11

### What is the current behavior?

The following snippet
```python
from qiskit import QuantumCircuit
from qiskit.circuit.library.standard_gates import RZXGate
import numpy as np

qc = QuantumCircuit(2)
qc.append(RZXGate(np.pi), [0, 1])
qc.append(RZXGate(np.pi / 2), [0, 1])

qc.qasm()
```
Produces
```
OPENQASM 2.0;
include "qelib1.inc";
gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(pi) q1; cx q0,q1; h q1; }
gate rzx_1491288775456(param0) q0,q1 { h q1; cx q0,q1; rz(pi/2) q1; cx q0,q1; h q1; }
qreg q[2];
rzx(pi) q[0],q[1];
rzx_1491288775456(pi/2) q[0],q[1];
```
As you can see, instead of creating a single `rzx` instruction that uses `param0`, it creates a single `rzx` for each value present in the circuit.

### Steps to reproduce the problem
This happens with at least all other `RPPGate`s, where `PP` is placeholder for some Pauli matrices. I haven't tested it with other parameterized gates.

### What is the expected behavior?
I think the output QASM string should look like:
```
OPENQASM 2.0;
include "qelib1.inc";
gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; }
qreg q[2];
rzx(pi) q[0],q[1];
rzx(pi/2) q[0],q[1];
```

I tested it and using that string to build a `QuantumCircuit` gives the intended circuit.

```python
string = """OPENQASM 2.0;
include "qelib1.inc";
gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; }
qreg q[2];
rzx(pi) q[0],q[1];
rzx(pi/2) q[0],q[1];"""

qc = QuantumCircuit.from_qasm_str(string)

qc.decompose().draw()
```

Output:

```

q_0: ───────■─────────────■──────────────■───────────────■───────
     ┌───┐┌─┴─┐┌───────┐┌─┴─┐┌───┐┌───┐┌─┴─┐┌─────────┐┌─┴─┐┌───┐
q_1: ┤ H ├┤ X ├┤ Rz(π) ├┤ X ├┤ H ├┤ H ├┤ X ├┤ Rz(π/2) ├┤ X ├┤ H ├
     └───┘└───┘└───────┘└───┘└───┘└───┘└───┘└─────────┘└───┘└───┘
```





Discussion:
I have tried to export this program into OpenQASM3. The result is:
```
OPENQASM 3;
include "stdgates.inc";
gate rzx(param_0) q_0, q_1 {
  h q_1;
  cx q_0, q_1;
  rz(pi) q_1;
  cx q_0, q_1;
  h q_1;
}
qubit[2] _q;
let q = _q[0] || _q[1];
rzx(pi) q[0], q[1];
rzx(pi/2) q[0], q[1];
```
There is only one `rzx` definition. However, the parameter is not used in the body. Therefore, the program is not correct.
The problem I mentioned above, i.e., not using parameters in a gate's body, is the same as this [example](https://github.com/Qiskit/qiskit-terra/blob/main/test/python/circuit/test_circuit_qasm3.py#L492-L506):
```
                "gate u3(param_0, param_1, param_2) q_0 {",
                "  U(0, 0, pi/2) q_0;",
                "}",
                "gate u1(param_0) q_0 {",
                "  u3(0, 0, pi/2) q_0;",
                "}",
                "gate rz(param_0) q_0 {",
                "  u1(pi/2) q_0;",
                "}",
                "gate sdg q_0 {",
                "  u1(-pi/2) q_0;",
                "}",
                "gate u2(param_0, param_1) q_0 {",
                "  u3(pi/2, 0, pi) q_0;",
                "}",
```
Therefore, the authors should be aware of this problem. There are probably some documents, issues, pull requests, or code comments that explain the design choices or the difficulties leading to the current status. I have not found them, though.

Resolving patch:
diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py
--- a/qiskit/circuit/library/generalized_gates/permutation.py
+++ b/qiskit/circuit/library/generalized_gates/permutation.py
@@ -145,10 +145,6 @@ def __init__(
             )
         pattern = np.array(pattern)
 
-        # This is needed to support qasm()
-        self._qasm_name = "permutation__" + "_".join([str(n) for n in pattern]) + "_"
-        self._qasm_definition = None
-
         super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern])
 
     def __array__(self, dtype=None):
@@ -183,21 +179,12 @@ def inverse(self):
 
         return PermutationGate(pattern=_inverse_pattern(self.pattern))
 
-    def qasm(self):
-        """The qasm for a permutation."""
-
-        if not self._qasm_definition:
-
-            # pylint: disable=cyclic-import
-            from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap
-
-            # This qasm should be identical to the one produced when permutation
-            # was a circuit rather than a gate.
-            swaps = _get_ordered_swap(self.pattern)
-            gates_def = "".join(["swap q" + str(i) + ",q" + str(j) + "; " for i, j in swaps])
-            qubit_list = ",".join(["q" + str(n) for n in range(len(self.pattern))])
-            self._qasm_definition = (
-                "gate " + self._qasm_name + " " + qubit_list + " { " + gates_def + "}"
-            )
+    def _qasm2_decomposition(self):
+        # pylint: disable=cyclic-import
+        from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap
 
-        return self._qasmif(self._qasm_name)
+        name = f"permutation__{'_'.join(str(n) for n in self.pattern)}_"
+        out = QuantumCircuit(self.num_qubits, name=name)
+        for i, j in _get_ordered_swap(self.pattern):
+            out.swap(i, j)
+        return out.to_gate()
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -1620,7 +1620,7 @@ def qasm(
         if self.num_parameters > 0:
             raise QasmError("Cannot represent circuits with unbound parameters in OpenQASM 2.")
 
-        existing_gate_names = [
+        existing_gate_names = {
             "barrier",
             "measure",
             "reset",
@@ -1666,12 +1666,11 @@ def qasm(
             "c3x",
             "c3sx",  # This is the Qiskit gate name, but the qelib1.inc name is 'c3sqrtx'.
             "c4x",
-        ]
-
-        existing_composite_circuits: list[Instruction] = []
+        }
 
-        string_temp = self.header + "\n"
-        string_temp += self.extension_lib + "\n"
+        # Mapping of instruction name to a pair of the source for a definition, and an OQ2 string
+        # that includes the `gate` or `opaque` statement that defines the gate.
+        gates_to_define: OrderedDict[str, tuple[Instruction, str]] = OrderedDict()
 
         regless_qubits = [bit for bit in self.qubits if not self.find_bit(bit).registers]
         regless_clbits = [bit for bit in self.clbits if not self.find_bit(bit).registers]
@@ -1691,72 +1690,51 @@ def qasm(
             for name, register in register_escaped_names.items()
             for (idx, bit) in enumerate(register)
         }
-        for name, reg in register_escaped_names.items():
-            string_temp += (
-                f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n"
-            )
-
+        register_definitions_qasm = "".join(
+            f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n"
+            for name, reg in register_escaped_names.items()
+        )
+        instruction_calls = []
         for instruction in self._data:
             operation = instruction.operation
             if operation.name == "measure":
                 qubit = instruction.qubits[0]
                 clbit = instruction.clbits[0]
-                string_temp += "{} {} -> {};\n".format(
-                    operation.qasm(),
-                    bit_labels[qubit],
-                    bit_labels[clbit],
-                )
+                instruction_qasm = f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};"
             elif operation.name == "reset":
-                string_temp += f"reset {bit_labels[instruction.qubits[0]]};\n"
+                instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};"
             elif operation.name == "barrier":
                 qargs = ",".join(bit_labels[q] for q in instruction.qubits)
-                string_temp += "barrier;\n" if not qargs else f"barrier {qargs};\n"
+                instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};"
             else:
-                # Check instructions names or label are valid
-                escaped = _qasm_escape_name(operation.name, "gate_")
-                if escaped != operation.name:
-                    operation = operation.copy(name=escaped)
-
-                # decompose gate using definitions if they are not defined in OpenQASM2
-                if operation.name not in existing_gate_names:
-                    op_qasm_name = None
-                    if operation.name == "permutation":
-                        op_qasm_name = getattr(operation, "_qasm_name", None)
-                    if op_qasm_name:
-                        operation = operation.copy(name=op_qasm_name)
-
-                    if operation not in existing_composite_circuits:
-                        if operation.name in [
-                            operation.name for operation in existing_composite_circuits
-                        ]:
-                            # append operation id to name of operation copy to make it unique
-                            operation = operation.copy(name=f"{operation.name}_{id(operation)}")
-
-                        existing_composite_circuits.append(operation)
-
-                        # Strictly speaking, the code below does not work for operations that
-                        # do not have the "definition" method but require a complex (recursive)
-                        # "_qasm_definition". Fortunately, right now we do not have any such operations.
-                        if getattr(operation, "definition", None) is not None:
-                            _add_sub_instruction_to_existing_composite_circuits(
-                                operation, existing_gate_names, existing_composite_circuits
-                            )
+                operation = _qasm2_define_custom_operation(
+                    operation, existing_gate_names, gates_to_define
+                )
 
                 # Insert qasm representation of the original instruction
-                string_temp += "{} {};\n".format(
-                    operation.qasm(),
-                    ",".join([bit_labels[j] for j in instruction.qubits + instruction.clbits]),
+                bits_qasm = ",".join(
+                    bit_labels[j] for j in itertools.chain(instruction.qubits, instruction.clbits)
                 )
-
-        # insert gate definitions
-        string_temp = _insert_composite_gate_definition_qasm(
-            string_temp, existing_composite_circuits, self.extension_lib
+                instruction_qasm = f"{operation.qasm()} {bits_qasm};"
+            instruction_calls.append(instruction_qasm)
+        instructions_qasm = "".join(f"{call}\n" for call in instruction_calls)
+        gate_definitions_qasm = "".join(f"{qasm}\n" for _, qasm in gates_to_define.values())
+
+        out = "".join(
+            (
+                self.header,
+                "\n",
+                self.extension_lib,
+                "\n",
+                gate_definitions_qasm,
+                register_definitions_qasm,
+                instructions_qasm,
+            )
         )
 
         if filename:
             with open(filename, "w+", encoding=encoding) as file:
-                file.write(string_temp)
-            file.close()
+                file.write(out)
 
         if formatted:
             if not HAS_PYGMENTS:
@@ -1766,12 +1744,11 @@ def qasm(
                     pip_install="pip install pygments",
                 )
             code = pygments.highlight(
-                string_temp, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle)
+                out, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle)
             )
             print(code)
             return None
-        else:
-            return string_temp
+        return out
 
     def draw(
         self,
@@ -4886,28 +4863,100 @@ def _compare_parameters(param1: Parameter, param2: Parameter) -> int:
     return _standard_compare(param1.name, param2.name)
 
 
-def _add_sub_instruction_to_existing_composite_circuits(
-    instruction: Instruction,
-    existing_gate_names: list[str],
-    existing_composite_circuits: list[Instruction],
-) -> None:
-    """Recursively add undefined sub-instructions in the definition of the given
-    instruction to existing_composite_circuit list.
-    """
-    for sub_instruction in instruction.definition:
-        sub_operation = sub_instruction.operation
-        # Check instructions names are valid
-        escaped = _qasm_escape_name(sub_operation.name, "gate_")
-        if escaped != sub_operation.name:
-            sub_operation = sub_operation.copy(name=escaped)
-        if (
-            sub_operation.name not in existing_gate_names
-            and sub_operation not in existing_composite_circuits
-        ):
-            existing_composite_circuits.insert(0, sub_operation)
-            _add_sub_instruction_to_existing_composite_circuits(
-                sub_operation, existing_gate_names, existing_composite_circuits
+# Used by the OQ2 exporter.  Just needs to have enough parameters to support the largest standard
+# (non-controlled) gate in our standard library.  We have to use the same `Parameter` instances each
+# time so the equality comparisons will work.
+_QASM2_FIXED_PARAMETERS = [Parameter("param0"), Parameter("param1"), Parameter("param2")]
+
+
+def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_define):
+    """Extract a custom definition from the given operation, and append any necessary additional
+    subcomponents' definitions to the ``gates_to_define`` ordered dictionary.
+
+    Returns a potentially new :class:`.Instruction`, which should be used for the
+    :meth:`~.Instruction.qasm` call (it may have been renamed)."""
+    from qiskit.circuit import library as lib  # pylint: disable=cyclic-import
+
+    if operation.name in existing_gate_names:
+        return operation
+
+    # Check instructions names or label are valid
+    escaped = _qasm_escape_name(operation.name, "gate_")
+    if escaped != operation.name:
+        operation = operation.copy(name=escaped)
+
+    # These are built-in gates that are known to be safe to construct by passing the correct number
+    # of `Parameter` instances positionally, and have no other information.  We can't guarantee that
+    # if they've been subclassed, though.  This is a total hack; ideally we'd be able to inspect the
+    # "calling" signatures of Qiskit `Gate` objects to know whether they're safe to re-parameterise.
+    known_good_parameterized = {
+        lib.PhaseGate,
+        lib.RGate,
+        lib.RXGate,
+        lib.RXXGate,
+        lib.RYGate,
+        lib.RYYGate,
+        lib.RZGate,
+        lib.RZXGate,
+        lib.RZZGate,
+        lib.XXMinusYYGate,
+        lib.XXPlusYYGate,
+        lib.UGate,
+        lib.U1Gate,
+        lib.U2Gate,
+        lib.U3Gate,
+    }
+
+    # In known-good situations we want to use a manually parametrised object as the source of the
+    # definition, but still continue to return the given object as the call-site object.
+    if type(operation) in known_good_parameterized:
+        parameterized_operation = type(operation)(*_QASM2_FIXED_PARAMETERS[: len(operation.params)])
+    elif hasattr(operation, "_qasm2_decomposition"):
+        new_op = operation._qasm2_decomposition()
+        parameterized_operation = operation = new_op.copy(
+            name=_qasm_escape_name(new_op.name, "gate_")
+        )
+    else:
+        parameterized_operation = operation
+
+    # If there's an _equal_ operation in the existing circuits to be defined, then our job is done.
+    previous_definition_source, _ = gates_to_define.get(operation.name, (None, None))
+    if parameterized_operation == previous_definition_source:
+        return operation
+
+    # Otherwise, if there's a naming clash, we need a unique name.
+    if operation.name in gates_to_define:
+        new_name = f"{operation.name}_{id(operation)}"
+        operation = operation.copy(name=new_name)
+    else:
+        new_name = operation.name
+
+    if parameterized_operation.params:
+        parameters_qasm = (
+            "(" + ",".join(f"param{i}" for i in range(len(parameterized_operation.params))) + ")"
+        )
+    else:
+        parameters_qasm = ""
+    qubits_qasm = ",".join(f"q{i}" for i in range(parameterized_operation.num_qubits))
+    parameterized_definition = getattr(parameterized_operation, "definition", None)
+    if parameterized_definition is None:
+        gates_to_define[new_name] = (
+            parameterized_operation,
+            f"opaque {new_name}{parameters_qasm} {qubits_qasm};",
+        )
+    else:
+        statements = []
+        qubit_labels = {bit: f"q{i}" for i, bit in enumerate(parameterized_definition.qubits)}
+        for instruction in parameterized_definition.data:
+            new_operation = _qasm2_define_custom_operation(
+                instruction.operation, existing_gate_names, gates_to_define
             )
+            bits_qasm = ",".join(qubit_labels[q] for q in instruction.qubits)
+            statements.append(f"{new_operation.qasm()} {bits_qasm};")
+        body_qasm = " ".join(statements)
+        definition_qasm = f"gate {new_name}{parameters_qasm} {qubits_qasm} {{ {body_qasm} }}"
+        gates_to_define[new_name] = (parameterized_operation, definition_qasm)
+    return operation
 
 
 def _qasm_escape_name(name: str, prefix: str) -> str:
@@ -4940,72 +4989,6 @@ def _make_unique(name: str, already_defined: collections.abc.Set[str]) -> str:
     return name
 
 
-def _get_composite_circuit_qasm_from_instruction(instruction: Instruction) -> str:
-    """Returns OpenQASM string composite circuit given an instruction.
-    The given instruction should be the result of composite_circuit.to_instruction()."""
-
-    if instruction.definition is None:
-        raise ValueError(f'Instruction "{instruction.name}" is not defined.')
-
-    gate_parameters = ",".join(["param%i" % num for num in range(len(instruction.params))])
-    qubit_parameters = ",".join(["q%i" % num for num in range(instruction.num_qubits)])
-    composite_circuit_gates = ""
-
-    definition = instruction.definition
-    definition_bit_labels = {
-        bit: idx for bits in (definition.qubits, definition.clbits) for idx, bit in enumerate(bits)
-    }
-    for sub_instruction in definition:
-        sub_operation = sub_instruction.operation
-        escaped = _qasm_escape_name(sub_operation.name, "gate_")
-        if escaped != sub_operation.name:
-            sub_operation = sub_operation.copy(name=escaped)
-
-        gate_qargs = ",".join(
-            "q%i" % index
-            for index in [definition_bit_labels[qubit] for qubit in sub_instruction.qubits]
-        )
-        composite_circuit_gates += f"{sub_operation.qasm()} {gate_qargs}; "
-
-    if composite_circuit_gates:
-        composite_circuit_gates = composite_circuit_gates.rstrip(" ")
-
-    if gate_parameters:
-        qasm_string = "gate {}({}) {} {{ {} }}".format(
-            instruction.name,
-            gate_parameters,
-            qubit_parameters,
-            composite_circuit_gates,
-        )
-    else:
-        qasm_string = "gate {} {} {{ {} }}".format(
-            instruction.name,
-            qubit_parameters,
-            composite_circuit_gates,
-        )
-
-    return qasm_string
-
-
-def _insert_composite_gate_definition_qasm(
-    string_temp: str, existing_composite_circuits: list[Instruction], extension_lib: str
-) -> str:
-    """Insert composite gate definition QASM code right after extension library in the header"""
-
-    gate_definition_string = ""
-
-    # Generate gate definition string
-    for instruction in existing_composite_circuits:
-        if hasattr(instruction, "_qasm_definition"):
-            qasm_string = instruction._qasm_definition
-        else:
-            qasm_string = _get_composite_circuit_qasm_from_instruction(instruction)
-        gate_definition_string += "\n" + qasm_string
-
-    string_temp = string_temp.replace(extension_lib, f"{extension_lib}{gate_definition_string}")
-    return string_temp
-
-
 def _bit_argument_conversion(specifier, bit_sequence, bit_set, type_) -> list[Bit]:
     """Get the list of bits referred to by the specifier ``specifier``.
 
diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py
--- a/qiskit/extensions/unitary.py
+++ b/qiskit/extensions/unitary.py
@@ -21,7 +21,6 @@
 from qiskit.circuit import QuantumRegister, Qubit
 from qiskit.circuit.exceptions import CircuitError
 from qiskit.circuit._utils import _compute_control_matrix
-from qiskit.circuit.quantumcircuit import _qasm_escape_name
 from qiskit.circuit.library.standard_gates import U3Gate
 from qiskit.extensions.quantum_initializer import isometry
 from qiskit.quantum_info.operators.predicates import matrix_equal
@@ -87,8 +86,6 @@ def __init__(self, data, label=None):
         if input_dim != output_dim or 2**num_qubits != input_dim:
             raise ExtensionError("Input matrix is not an N-qubit operator.")
 
-        self._qasm_name = None
-        self._qasm_definition = None
         # Store instruction params
         super().__init__("unitary", num_qubits, [data], label=label)
 
@@ -170,41 +167,12 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None):
             base_gate=self.copy(),
         )
 
-    def qasm(self):
-        """The qasm for a custom unitary gate
-        This is achieved by adding a custom gate that corresponds to the definition
-        of this gate. It gives the gate a random name if one hasn't been given to it.
-        """
-
-        # give this unitary a name
-        self._qasm_name = (
-            _qasm_escape_name(self.label, "gate_") if self.label else "unitary" + str(id(self))
-        )
-
-        qubit_to_qasm = {bit: f"p{i}" for i, bit in enumerate(self.definition.qubits)}
-        gates_def = ""
-        for instruction in self.definition.data:
-
-            curr_gate = "\t{} {};\n".format(
-                instruction.operation.qasm(),
-                ",".join(qubit_to_qasm[qubit] for qubit in instruction.qubits),
-            )
-            gates_def += curr_gate
-
-        # name of gate + params + {definition}
-        overall = (
-            "gate "
-            + self._qasm_name
-            + " "
-            + ",".join(qubit_to_qasm[qubit] for qubit in self.definition.qubits)
-            + " {\n"
-            + gates_def
-            + "}"
-        )
-
-        self._qasm_definition = overall
-
-        return self._qasmif(self._qasm_name)
+    def _qasm2_decomposition(self):
+        """Return an unparameterized version of ourselves, so the OQ2 exporter doesn't choke on the
+        non-standard things in our `params` field."""
+        out = self.definition.to_gate()
+        out.name = self.name
+        return out
 
     def validate_parameter(self, parameter):
         """Unitary gate parameter has to be an ndarray."""


Test patch:
diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py
--- a/test/python/circuit/test_circuit_qasm.py
+++ b/test/python/circuit/test_circuit_qasm.py
@@ -19,7 +19,7 @@
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
 from qiskit.test import QiskitTestCase
 from qiskit.circuit import Parameter, Qubit, Clbit, Gate
-from qiskit.circuit.library import C3SXGate, CCZGate, CSGate, CSdgGate, RZXGate
+from qiskit.circuit.library import C3SXGate, CCZGate, CSGate, CSdgGate, PermutationGate
 from qiskit.qasm.exceptions import QasmError
 
 # Regex pattern to match valid OpenQASM identifiers
@@ -308,16 +308,73 @@ def test_csdggate_qasm(self):
     def test_rzxgate_qasm(self):
         """Test that RZX dumps definition as a non-qelib1 gate."""
         qc = QuantumCircuit(2)
-        qc.append(RZXGate(0), qc.qubits, [])
+        qc.rzx(0, 0, 1)
+        qc.rzx(pi / 2, 1, 0)
         qasm = qc.qasm()
         expected = """OPENQASM 2.0;
 include "qelib1.inc";
-gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(0) q1; cx q0,q1; h q1; }
+gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; }
 qreg q[2];
 rzx(0) q[0],q[1];
+rzx(pi/2) q[1],q[0];
 """
         self.assertEqual(qasm, expected)
 
+    def test_ecrgate_qasm(self):
+        """Test that ECR dumps its definition as a non-qelib1 gate."""
+        qc = QuantumCircuit(2)
+        qc.ecr(0, 1)
+        qc.ecr(1, 0)
+        qasm = qc.qasm()
+        expected = """OPENQASM 2.0;
+include "qelib1.inc";
+gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; }
+gate ecr q0,q1 { rzx(pi/4) q0,q1; x q0; rzx(-pi/4) q0,q1; }
+qreg q[2];
+ecr q[0],q[1];
+ecr q[1],q[0];
+"""
+        self.assertEqual(qasm, expected)
+
+    def test_unitary_qasm(self):
+        """Test that UnitaryGate can be dumped to OQ2 correctly."""
+        qc = QuantumCircuit(1)
+        qc.unitary([[1, 0], [0, 1]], 0)
+        qasm = qc.qasm()
+        expected = """OPENQASM 2.0;
+include "qelib1.inc";
+gate unitary q0 { u3(0,0,0) q0; }
+qreg q[1];
+unitary q[0];
+"""
+        self.assertEqual(qasm, expected)
+
+    def test_multiple_unitary_qasm(self):
+        """Test that multiple UnitaryGate instances can all dump successfully."""
+        custom = QuantumCircuit(1, name="custom")
+        custom.unitary([[1, 0], [0, -1]], 0)
+
+        qc = QuantumCircuit(2)
+        qc.unitary([[1, 0], [0, 1]], 0)
+        qc.unitary([[0, 1], [1, 0]], 1)
+        qc.append(custom.to_gate(), [0], [])
+        qasm = qc.qasm()
+        expected = re.compile(
+            r"""OPENQASM 2.0;
+include "qelib1.inc";
+gate unitary q0 { u3\(0,0,0\) q0; }
+gate (?P<u1>unitary_[0-9]*) q0 { u3\(pi,-pi/2,pi/2\) q0; }
+gate (?P<u2>unitary_[0-9]*) q0 { u3\(0,pi/2,pi/2\) q0; }
+gate custom q0 { (?P=u2) q0; }
+qreg q\[2\];
+unitary q\[0\];
+(?P=u1) q\[1\];
+custom q\[0\];
+""",
+            re.MULTILINE,
+        )
+        self.assertRegex(qasm, expected)
+
     def test_unbound_circuit_raises(self):
         """Test circuits with unbound parameters raises."""
         qc = QuantumCircuit(1)
@@ -365,9 +422,9 @@ def test_circuit_qasm_with_mcx_gate_variants(self):
         # param0 for "gate mcuq(param0) is not used inside the definition
         expected_qasm = """OPENQASM 2.0;
 include "qelib1.inc";
-gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }
 gate mcu1(param0) q0,q1,q2,q3,q4,q5 { cu1(pi/16) q4,q5; cx q4,q3; cu1(-pi/16) q3,q5; cx q4,q3; cu1(pi/16) q3,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; }
 gate mcx_gray q0,q1,q2,q3,q4,q5 { h q5; mcu1(pi) q0,q1,q2,q3,q4,q5; h q5; }
+gate mcx q0,q1,q2,q3 { h q3; p(pi/8) q0; p(pi/8) q1; p(pi/8) q2; p(pi/8) q3; cx q0,q1; p(-pi/8) q1; cx q0,q1; cx q1,q2; p(-pi/8) q2; cx q0,q2; p(pi/8) q2; cx q1,q2; p(-pi/8) q2; cx q0,q2; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q1,q3; p(pi/8) q3; cx q2,q3; p(-pi/8) q3; cx q0,q3; h q3; }
 gate mcx_recursive q0,q1,q2,q3,q4,q5,q6 { mcx q0,q1,q2,q6; mcx q3,q4,q6,q5; mcx q0,q1,q2,q6; mcx q3,q4,q6,q5; }
 gate mcx_vchain q0,q1,q2,q3,q4,q5,q6,q7,q8 { rccx q0,q1,q6; rccx q2,q6,q7; rccx q3,q7,q8; ccx q4,q8,q5; rccx q3,q7,q8; rccx q2,q6,q7; rccx q0,q1,q6; }
 qreg q[9];
@@ -463,9 +520,6 @@ def test_circuit_qasm_with_invalid_identifiers(self):
         gate2 = custom2.to_gate()
         gate2.name = "invalid[name]"
 
-        # Unitary gate, for which qasm string is produced by internal method
-        qc.unitary([[0, 1], [1, 0]], 0, label="[valid?]")
-
         # Append gates
         qc.append(gate, [0])
         qc.append(gate2, [1, 0])
@@ -475,13 +529,9 @@ def test_circuit_qasm_with_invalid_identifiers(self):
             [
                 "OPENQASM 2.0;",
                 'include "qelib1.inc";',
-                "gate gate__valid__ p0 {",
-                "	u3(pi,-pi/2,pi/2) p0;",
-                "}",
                 "gate gate_A___ q0 { x q0; u(0,0,pi) q0; }",
                 "gate invalid_name_ q0,q1 { x q0; gate_A___ q1; }",
                 "qreg q[2];",
-                "gate__valid__ q[0];",
                 "gate_A___ q[0];",
                 "invalid_name_ q[1],q[0];",
                 "",
@@ -492,7 +542,7 @@ def test_circuit_qasm_with_invalid_identifiers(self):
         self.assertEqual(expected_qasm, qc.qasm())
 
         # Check instruction names were not changed by qasm()
-        names = ["unitary", "A[$]", "invalid[name]"]
+        names = ["A[$]", "invalid[name]"]
         for idx, instruction in enumerate(qc._data):
             self.assertEqual(instruction.operation.name, names[idx])
 
@@ -604,7 +654,6 @@ def test_circuit_raises_on_single_bit_condition(self):
 
     def test_circuit_qasm_with_permutations(self):
         """Test circuit qasm() method with Permutation gates."""
-        from qiskit.circuit.library import PermutationGate
 
         qc = QuantumCircuit(4)
         qc.append(PermutationGate([2, 1, 0]), [0, 1, 2])
@@ -616,6 +665,30 @@ def test_circuit_qasm_with_permutations(self):
 permutation__2_1_0_ q[0],q[1],q[2];\n"""
         self.assertEqual(qc.qasm(), expected_qasm)
 
+    def test_multiple_permutation(self):
+        """Test that multiple PermutationGates can be added to a circuit."""
+        custom = QuantumCircuit(3, name="custom")
+        custom.append(PermutationGate([2, 1, 0]), [0, 1, 2])
+        custom.append(PermutationGate([0, 1, 2]), [0, 1, 2])
+
+        qc = QuantumCircuit(4)
+        qc.append(PermutationGate([2, 1, 0]), [0, 1, 2], [])
+        qc.append(PermutationGate([1, 2, 0]), [0, 1, 2], [])
+        qc.append(custom.to_gate(), [1, 3, 2], [])
+        qasm = qc.qasm()
+        expected = """OPENQASM 2.0;
+include "qelib1.inc";
+gate permutation__2_1_0_ q0,q1,q2 { swap q0,q2; }
+gate permutation__1_2_0_ q0,q1,q2 { swap q1,q2; swap q0,q2; }
+gate permutation__0_1_2_ q0,q1,q2 {  }
+gate custom q0,q1,q2 { permutation__2_1_0_ q0,q1,q2; permutation__0_1_2_ q0,q1,q2; }
+qreg q[4];
+permutation__2_1_0_ q[0],q[1],q[2];
+permutation__1_2_0_ q[0],q[1],q[2];
+custom q[1],q[3],q[2];
+"""
+        self.assertEqual(qasm, expected)
+
     def test_circuit_qasm_with_reset(self):
         """Test circuit qasm() method with Reset."""
         qc = QuantumCircuit(2)
@@ -628,6 +701,73 @@ def test_circuit_qasm_with_reset(self):
 reset q[1];\n"""
         self.assertEqual(qc.qasm(), expected_qasm)
 
+    def test_nested_gate_naming_clashes(self):
+        """Test that gates that have naming clashes but only appear in the body of another gate
+        still get exported correctly."""
+
+        # pylint: disable=missing-class-docstring
+
+        class Inner(Gate):
+            def __init__(self, param):
+                super().__init__("inner", 1, [param])
+
+            def _define(self):
+                self._definition = QuantumCircuit(1)
+                self._definition.rx(self.params[0], 0)
+
+        class Outer(Gate):
+            def __init__(self, param):
+                super().__init__("outer", 1, [param])
+
+            def _define(self):
+                self._definition = QuantumCircuit(1)
+                self._definition.append(Inner(self.params[0]), [0], [])
+
+        qc = QuantumCircuit(1)
+        qc.append(Outer(1.0), [0], [])
+        qc.append(Outer(2.0), [0], [])
+        qasm = qc.qasm()
+
+        expected = re.compile(
+            r"""OPENQASM 2\.0;
+include "qelib1\.inc";
+gate inner\(param0\) q0 { rx\(1\.0\) q0; }
+gate outer\(param0\) q0 { inner\(1\.0\) q0; }
+gate (?P<inner1>inner_[0-9]*)\(param0\) q0 { rx\(2\.0\) q0; }
+gate (?P<outer1>outer_[0-9]*)\(param0\) q0 { (?P=inner1)\(2\.0\) q0; }
+qreg q\[1\];
+outer\(1\.0\) q\[0\];
+(?P=outer1)\(2\.0\) q\[0\];
+""",
+            re.MULTILINE,
+        )
+        self.assertRegex(qasm, expected)
+
+    def test_opaque_output(self):
+        """Test that gates with no definition are exported as `opaque`."""
+        custom = QuantumCircuit(1, name="custom")
+        custom.append(Gate("my_c", 1, []), [0])
+
+        qc = QuantumCircuit(2)
+        qc.append(Gate("my_a", 1, []), [0])
+        qc.append(Gate("my_a", 1, []), [1])
+        qc.append(Gate("my_b", 2, [1.0]), [1, 0])
+        qc.append(custom.to_gate(), [0], [])
+        qasm = qc.qasm()
+        expected = """OPENQASM 2.0;
+include "qelib1.inc";
+opaque my_a q0;
+opaque my_b(param0) q0,q1;
+opaque my_c q0;
+gate custom q0 { my_c q0; }
+qreg q[2];
+my_a q[0];
+my_a q[1];
+my_b(1.0) q[1],q[0];
+custom q[0];
+"""
+        self.assertEqual(qasm, expected)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py
--- a/test/python/circuit/test_unitary.py
+++ b/test/python/circuit/test_unitary.py
@@ -208,7 +208,7 @@ def test_qasm_unitary_only_one_def(self):
         cr = ClassicalRegister(1, "c0")
         qc = QuantumCircuit(qr, cr)
         matrix = numpy.array([[1, 0], [0, 1]])
-        unitary_gate = UnitaryGate(matrix, label="custom_gate")
+        unitary_gate = UnitaryGate(matrix)
 
         qc.x(qr[0])
         qc.append(unitary_gate, [qr[0]])
@@ -217,13 +217,11 @@ def test_qasm_unitary_only_one_def(self):
         expected_qasm = (
             "OPENQASM 2.0;\n"
             'include "qelib1.inc";\n'
-            "gate custom_gate p0 {\n"
-            "\tu3(0,0,0) p0;\n"
-            "}\n"
+            "gate unitary q0 { u3(0,0,0) q0; }\n"
             "qreg q0[2];\ncreg c0[1];\n"
             "x q0[0];\n"
-            "custom_gate q0[0];\n"
-            "custom_gate q0[1];\n"
+            "unitary q0[0];\n"
+            "unitary q0[1];\n"
         )
         self.assertEqual(expected_qasm, qc.qasm())
 
@@ -234,7 +232,7 @@ def test_qasm_unitary_twice(self):
         cr = ClassicalRegister(1, "c0")
         qc = QuantumCircuit(qr, cr)
         matrix = numpy.array([[1, 0], [0, 1]])
-        unitary_gate = UnitaryGate(matrix, label="custom_gate")
+        unitary_gate = UnitaryGate(matrix)
 
         qc.x(qr[0])
         qc.append(unitary_gate, [qr[0]])
@@ -243,13 +241,11 @@ def test_qasm_unitary_twice(self):
         expected_qasm = (
             "OPENQASM 2.0;\n"
             'include "qelib1.inc";\n'
-            "gate custom_gate p0 {\n"
-            "\tu3(0,0,0) p0;\n"
-            "}\n"
+            "gate unitary q0 { u3(0,0,0) q0; }\n"
             "qreg q0[2];\ncreg c0[1];\n"
             "x q0[0];\n"
-            "custom_gate q0[0];\n"
-            "custom_gate q0[1];\n"
+            "unitary q0[0];\n"
+            "unitary q0[1];\n"
         )
         self.assertEqual(expected_qasm, qc.qasm())
         self.assertEqual(expected_qasm, qc.qasm())
@@ -260,7 +256,7 @@ def test_qasm_2q_unitary(self):
         cr = ClassicalRegister(1, "c0")
         qc = QuantumCircuit(qr, cr)
         matrix = numpy.asarray([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]])
-        unitary_gate = UnitaryGate(matrix, label="custom_gate")
+        unitary_gate = UnitaryGate(matrix)
 
         qc.x(qr[0])
         qc.append(unitary_gate, [qr[0], qr[1]])
@@ -269,29 +265,25 @@ def test_qasm_2q_unitary(self):
         expected_qasm = (
             "OPENQASM 2.0;\n"
             'include "qelib1.inc";\n'
-            "gate custom_gate p0,p1 {\n"
-            "\tu(pi,-pi/2,pi/2) p0;\n"
-            "\tu(pi,pi/2,-pi/2) p1;\n"
-            "}\n"
+            "gate unitary q0,q1 { u(pi,-pi/2,pi/2) q0; u(pi,pi/2,-pi/2) q1; }\n"
             "qreg q0[2];\n"
             "creg c0[1];\n"
             "x q0[0];\n"
-            "custom_gate q0[0],q0[1];\n"
-            "custom_gate q0[1],q0[0];\n"
+            "unitary q0[0],q0[1];\n"
+            "unitary q0[1],q0[0];\n"
         )
         self.assertEqual(expected_qasm, qc.qasm())
 
     def test_qasm_unitary_noop(self):
-        """Test that an identifier unitary can be converted to OpenQASM 2"""
+        """Test that an identity unitary can be converted to OpenQASM 2"""
         qc = QuantumCircuit(QuantumRegister(3, "q0"))
-        qc.unitary(numpy.eye(8), qc.qubits, label="unitary_identity")
+        qc.unitary(numpy.eye(8), qc.qubits)
         expected_qasm = (
             "OPENQASM 2.0;\n"
             'include "qelib1.inc";\n'
-            "gate unitary_identity p0,p1,p2 {\n"
-            "}\n"
+            "gate unitary q0,q1,q2 {  }\n"
             "qreg q0[3];\n"
-            "unitary_identity q0[0],q0[1],q0[2];\n"
+            "unitary q0[0],q0[1],q0[2];\n"
         )
         self.assertEqual(expected_qasm, qc.qasm())