Repository: Qiskit/qiskit

Issue:
ParameterTable expects Instructions to be used only once within a circuit
See the bug reported in #3008 . The implementation of `ParameterTable` and the associated binding machinery operate under the assumption that a given `Instruction` instance will appear in only one gate in a circuit, but this isn't guaranteed.

e.g. a user could write:
```
>>> import qiskit as qk
>>> p = qk.circuit.Parameter('p')
>>> qc = qk.QuantumCircuit(2)
>>> rz = qk.extensions.standard.RZGate(p)
>>> qc.append(rz, [0], [])
>>> qc.append(rz, [1], [])
>>> print(qc)
        ┌───────┐
q_0: |0>┤ Rz(p) ├
        ├───────┤
q_1: |0>┤ Rz(p) ├
        └───────┘
>>> qc.bind_parameters({p: 3})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 875, in bind_parameters
    new_circuit._bind_parameter(parameter, value)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 897, in _bind_parameter
    instr.params[param_index] = instr.params[param_index].bind({parameter: value})
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/parameterexpression.py", line 68, in bind
    self._raise_if_passed_unknown_parameters(parameter_values.keys())
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/parameterexpression.py", line 136, in _raise_if_passed_unknown_parameters
    [str(p) for p in unknown_parameters]))
qiskit.exceptions.QiskitError: "Cannot bind Parameters (['p']) not present in expression."
```

Users of `Instruction.repeat` would see the same problem.

#3013 worked around this by forcing deepcopies of instructions when broadcasting, but this doesn't resolve the general problem.


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -355,24 +355,8 @@ def append(self, instruction, qargs=None, cargs=None):
         expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []]
 
         instructions = InstructionSet()
-
-        # When broadcasting was handled by decorators (prior to #2282), append
-        # received multiple distinct instruction instances, one for each expanded
-        # arg. With broadcasting as part of QuantumCircuit.append, the
-        # instruction instance is constructed before append is called. However,
-        # (at least) ParameterTable expects instruction instances to be unique
-        # within a circuit, so make instruction deepcopies for expanded_args[1:].
-
-        first_instruction = True
         for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs):
-            if first_instruction:
-                instructions.add(
-                    self._append(instruction, qarg, carg), qarg, carg)
-                first_instruction = False
-            else:
-                instructions.add(
-                    self._append(deepcopy(instruction), qarg, carg), qarg, carg)
-
+            instructions.add(self._append(instruction, qarg, carg), qarg, carg)
         return instructions
 
     def _append(self, instruction, qargs, cargs):
@@ -410,7 +394,9 @@ def _append(self, instruction, qargs, cargs):
 
                 for parameter in param.parameters:
                     if parameter in current_parameters:
-                        self._parameter_table[parameter].append((instruction, param_index))
+                        if not self._check_dup_param_spec(self._parameter_table[parameter],
+                                                          instruction, param_index):
+                            self._parameter_table[parameter].append((instruction, param_index))
                     else:
                         if parameter.name in {p.name for p in current_parameters}:
                             raise QiskitError(
@@ -419,6 +405,12 @@ def _append(self, instruction, qargs, cargs):
 
         return instruction
 
+    def _check_dup_param_spec(self, parameter_spec_list, instruction, param_index):
+        for spec in parameter_spec_list:
+            if spec[0] is instruction and spec[1] == param_index:
+                return True
+        return False
+
     def add_register(self, *regs):
         """Add registers."""
         if not regs:


Test patch:
diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py
--- a/test/python/circuit/test_extensions_standard.py
+++ b/test/python/circuit/test_extensions_standard.py
@@ -942,16 +942,6 @@ def test_z_reg_inv(self):
         self.assertEqual(instruction_set.qargs[1], [self.qr[1]])
         self.assertEqual(instruction_set.instructions[2].params, [])
 
-    def test_broadcast_does_not_duplicate_instructions(self):
-        self.circuit.rz(0, range(6))
-        instructions = [inst for inst, qargs, cargs in self.circuit.data]
-        self.assertEqual(len(set(id(inst) for inst in instructions)), 6)
-
-        self.circuit.data = []
-        self.circuit.rz(0, self.qr)
-        instructions = [inst for inst, qargs, cargs in self.circuit.data]
-        self.assertEqual(len(set(id(inst) for inst in instructions)), 3)
-
 
 class TestStandard2Q(QiskitTestCase):
     """Standard Extension Test. Gates with two Qubits"""
diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py
--- a/test/python/circuit/test_parameters.py
+++ b/test/python/circuit/test_parameters.py
@@ -161,6 +161,19 @@ def test_raise_if_assigning_params_not_in_circuit(self):
         qc.u1(x, qr[0])
         self.assertRaises(QiskitError, qc.bind_parameters, {x: 1, y: 2})
 
+    def test_gate_multiplicity_binding(self):
+        """Test binding when circuit contains multiple references to same gate"""
+        from qiskit.extensions.standard import RZGate
+        qc = QuantumCircuit(1)
+        theta = Parameter('theta')
+        gate = RZGate(theta)
+        qc.append(gate, [0], [])
+        qc.append(gate, [0], [])
+        qc2 = qc.bind_parameters({theta: 1.0})
+        self.assertEqual(len(qc2._parameter_table), 0)
+        for gate, _, _ in qc2.data:
+            self.assertEqual(float(gate.params[0]), 1.0)
+
     def test_circuit_generation(self):
         """Test creating a series of circuits parametrically"""
         theta = Parameter('θ')