Repository: Qiskit/qiskit

Issue:
Decompose does not propagate bound parameters
```
>>> import qiskit as qk
>>> p = qk.circuit.Parameter('p')
>>> qc = qk.QuantumCircuit(1)
>>> qc.rx(p,0)
<qiskit.circuit.instructionset.InstructionSet object at 0x1037b3668>
>>> inst = qc.to_instruction()
>>> qc2 = qk.QuantumCircuit(1)
>>> qc2.append(inst, [0])
<qiskit.circuit.instructionset.InstructionSet object at 0x124066710>
>>> print(qc2)
        ┌─────────────┐
q_0: |0>┤ Circuit0(p) ├
        └─────────────┘
>>> print(qc2.bind_parameters({p: 0.5}))
        ┌───────────────┐
q_0: |0>┤ Circuit0(0.5) ├
        └───────────────┘
>>> print(qc2.bind_parameters({p: 0.5}).decompose())
        ┌───────┐
q_0: |0>┤ Rx(p) ├
        └───────┘
```
In the normal workflow, this isn't an issue since we decompose before we bind, but both forms should work.


Discussion:
@chunfuchen pointed out that this also results in an error when trying to execute a circuit containing a parameterized instruction which was bound before assembly. e.g.
```
>>> import qiskit as qk
>>> theta = qk.circuit.Parameter('theta')
>>> sub_qc = qk.QuantumCircuit(2)
>>> sub_qc.h(0)
>>> sub_qc.cx(0,1)
>>> sub_qc.rz(theta)
>>> sub_qc.rz(theta, [0,1])
>>> print(sub_qc)
        ┌───┐     ┌───────────┐
q_0: |0>┤ H ├──■──┤ Rz(theta) ├
        └───┘┌─┴─┐├───────────┤
q_1: |0>─────┤ X ├┤ Rz(theta) ├
             └───┘└───────────┘
>>> sub_inst = sub_qc.to_instruction()
>>> unbound_qc = qk.QuantumCircuit(2,2)
>>> unbound_qc.append(sub_inst, [0, 1], [])
>>> unbound_qc.measure([0, 1], [0, 1])
>>> print(unbound_qc)
        ┌──────────────────┐┌─┐
q_0: |0>┤0                 ├┤M├───
        │  circuit4(theta) │└╥┘┌─┐
q_1: |0>┤1                 ├─╫─┤M├
        └──────────────────┘ ║ └╥┘
 c_0: 0 ═════════════════════╩══╬═
                                ║
 c_1: 0 ════════════════════════╩═

>>> bound_qc = unbound_qc.bind_parameters({theta: 0})
>>> bound_qc.parameters
set()
>>> qk.execute(bound_qc, backend=qk.BasicAer.get_backend('qasm_simulator'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/execute.py", line 218, in execute
    **run_config
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/compiler/assemble.py", line 147, in assemble
    run_config=run_config)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/compiler/assemble.py", line 333, in _expand_parameters
    'Circuit parameters: {}').format(all_bind_parameters, all_circuit_parameters))
qiskit.exceptions.QiskitError: 'Mismatch between run_config.parameter_binds and all circuit parameters. Parameter binds: [] Circuit parameters: [{Parameter(theta)}]'
```

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -1284,6 +1284,19 @@ def _bind_parameter(self, parameter, value):
         for (instr, param_index) in self._parameter_table[parameter]:
             instr.params[param_index] = instr.params[param_index].bind({parameter: value})
 
+            # For instructions which have already been defined (e.g. composite
+            # instructions), search the definition for instances of the
+            # parameter which also need to be bound.
+            self._rebind_definition(instr, parameter, value)
+
+    def _rebind_definition(self, instruction, parameter, value):
+        if instruction._definition:
+            for op, _, _ in instruction._definition:
+                for idx, param in enumerate(op.params):
+                    if isinstance(param, ParameterExpression) and parameter in param.parameters:
+                        op.params[idx] = param.bind({parameter: value})
+                        self._rebind_definition(op, parameter, value)
+
     def _substitute_parameters(self, parameter_map):
         """For every {existing_parameter: replacement_parameter} pair in
         parameter_map, substitute replacement for existing in all


Test patch:
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
@@ -17,8 +17,11 @@
 import pickle
 from operator import add, sub, mul, truediv
 
+from test import combine
 import numpy
 
+from ddt import ddt, data
+
 import qiskit
 from qiskit import BasicAer
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
@@ -31,6 +34,7 @@
 from qiskit.circuit.exceptions import CircuitError
 
 
+@ddt
 class TestParameters(QiskitTestCase):
     """QuantumCircuit Operations tests."""
 
@@ -434,6 +438,101 @@ def test_repeated_gates_to_dag_and_back(self):
         bound_test_qc = test_qc.bind_parameters({theta: 1})
         self.assertEqual(len(bound_test_qc.parameters), 0)
 
+    @data('gate', 'instruction')
+    def test_decompose_propagates_bound_parameters(self, target_type):
+        """Verify bind-before-decompose preserves bound values."""
+        # ref: https://github.com/Qiskit/qiskit-terra/issues/2482
+        theta = Parameter('th')
+        qc = QuantumCircuit(1)
+        qc.rx(theta, 0)
+
+        if target_type == 'gate':
+            inst = qc.to_gate()
+        elif target_type == 'instruction':
+            inst = qc.to_instruction()
+
+        qc2 = QuantumCircuit(1)
+        qc2.append(inst, [0])
+
+        bound_qc2 = qc2.bind_parameters({theta: 0.5})
+
+        self.assertEqual(qc2.parameters, {theta})
+        self.assertEqual(bound_qc2.parameters, set())
+
+        decomposed_qc2 = bound_qc2.decompose()
+
+        expected_qc2 = QuantumCircuit(1)
+        expected_qc2.rx(0.5, 0)
+
+        self.assertEqual(decomposed_qc2.parameters, set())
+        self.assertEqual(decomposed_qc2, expected_qc2)
+
+    @data('gate', 'instruction')
+    def test_decompose_propagates_deeply_bound_parameters(self, target_type):
+        """Verify bind-before-decompose preserves deeply bound values."""
+        theta = Parameter('th')
+        qc1 = QuantumCircuit(1)
+        qc1.rx(theta, 0)
+
+        if target_type == 'gate':
+            inst = qc1.to_gate()
+        elif target_type == 'instruction':
+            inst = qc1.to_instruction()
+
+        qc2 = QuantumCircuit(1)
+        qc2.append(inst, [0])
+
+        if target_type == 'gate':
+            inst = qc2.to_gate()
+        elif target_type == 'instruction':
+            inst = qc2.to_instruction()
+
+        qc3 = QuantumCircuit(1)
+        qc3.append(inst, [0])
+
+        bound_qc3 = qc3.bind_parameters({theta: 0.5})
+
+        self.assertEqual(qc3.parameters, {theta})
+        self.assertEqual(bound_qc3.parameters, set())
+
+        decomposed_qc3 = bound_qc3.decompose()
+        deep_decomposed_qc3 = decomposed_qc3.decompose()
+
+        expected_qc3 = QuantumCircuit(1)
+        expected_qc3.rx(0.5, 0)
+
+        self.assertEqual(deep_decomposed_qc3.parameters, set())
+        self.assertEqual(deep_decomposed_qc3, expected_qc3)
+
+    @data('gate', 'instruction')
+    def test_executing_parameterized_instruction_bound_early(self, target_type):
+        """Verify bind-before-execute preserves bound values."""
+        # ref: https://github.com/Qiskit/qiskit-terra/issues/2482
+
+        theta = Parameter('theta')
+
+        sub_qc = QuantumCircuit(2)
+        sub_qc.h(0)
+        sub_qc.cx(0, 1)
+        sub_qc.rz(theta, [0, 1])
+        sub_qc.cx(0, 1)
+        sub_qc.h(0)
+
+        if target_type == 'gate':
+            sub_inst = sub_qc.to_gate()
+        elif target_type == 'instruction':
+            sub_inst = sub_qc.to_instruction()
+
+        unbound_qc = QuantumCircuit(2, 1)
+        unbound_qc.append(sub_inst, [0, 1], [])
+        unbound_qc.measure(0, 0)
+
+        bound_qc = unbound_qc.bind_parameters({theta: numpy.pi/2})
+
+        shots = 1024
+        job = execute(bound_qc, backend=BasicAer.get_backend('qasm_simulator'), shots=shots)
+        self.assertDictAlmostEqual(job.result().get_counts(), {'1': shots}, 0.05 * shots)
+
 
 def _construct_circuit(param, qr):
     qc = QuantumCircuit(qr)
@@ -441,6 +540,7 @@ def _construct_circuit(param, qr):
     return qc
 
 
+@ddt
 class TestParameterExpressions(QiskitTestCase):
     """Test expressions of Parameters."""
 
@@ -620,17 +720,36 @@ def test_name_collision(self):
         with self.assertRaises(CircuitError):
             _ = x / y
 
-    def test_to_instruction_with_expression(self):
-        """Test preservation of expressions via parameterized instructions."""
+    @combine(target_type=['gate', 'instruction'],
+             order=['bind-decompose', 'decompose-bind'])
+    def test_to_instruction_with_expression(self, target_type, order):
+        """Test preservation of expressions via parameterized instructions.
+
+                  ┌───────┐┌──────────┐┌───────────┐
+        qr1_0: |0>┤ Rx(θ) ├┤ Rz(pi/2) ├┤ Ry(phi*θ) ├
+                  └───────┘└──────────┘└───────────┘
+
+                     ┌───────────┐
+        qr2_0: |0>───┤ Ry(delta) ├───
+                  ┌──┴───────────┴──┐
+        qr2_1: |0>┤ Circuit0(phi,θ) ├
+                  └─────────────────┘
+        qr2_2: |0>───────────────────
+        """
 
         theta = Parameter('θ')
         phi = Parameter('phi')
         qr1 = QuantumRegister(1, name='qr1')
         qc1 = QuantumCircuit(qr1)
+
         qc1.rx(theta, qr1)
         qc1.rz(numpy.pi/2, qr1)
         qc1.ry(theta * phi, qr1)
-        gate = qc1.to_instruction()
+
+        if target_type == 'gate':
+            gate = qc1.to_gate()
+        elif target_type == 'instruction':
+            gate = qc1.to_instruction()
 
         self.assertEqual(gate.params, [phi, theta])
 
@@ -639,21 +758,34 @@ def test_to_instruction_with_expression(self):
         qc2 = QuantumCircuit(qr2)
         qc2.ry(delta, qr2[0])
         qc2.append(gate, qargs=[qr2[1]])
+
         self.assertEqual(qc2.parameters, {delta, theta, phi})
 
-        bound_qc = qc2.decompose().bind_parameters({delta: 1, theta: 2, phi: 3})
-        self.assertEqual(float(bound_qc.data[0][0].params[0]), 1)
-        self.assertEqual(float(bound_qc.data[1][0].params[0]), 2)
-        self.assertEqual(float(bound_qc.data[2][0].params[0]), numpy.pi/2)
-        self.assertEqual(float(bound_qc.data[3][0].params[0]), 2 * 3)
+        binds = {delta: 1, theta: 2, phi: 3}
+        expected_qc = QuantumCircuit(qr2)
+        expected_qc.rx(2, 1)
+        expected_qc.rz(numpy.pi/2, 1)
+        expected_qc.ry(3 * 2, 1)
+        expected_qc.r(1, numpy.pi/2, 0)
+
+        if order == 'bind-decompose':
+            decomp_bound_qc = qc2.bind_parameters(binds).decompose()
+        elif order == 'decompose-bind':
+            decomp_bound_qc = qc2.decompose().bind_parameters(binds)
 
-    def test_to_instruction_expression_parameter_map(self):
+        self.assertEqual(decomp_bound_qc.parameters, set())
+        self.assertEqual(decomp_bound_qc, expected_qc)
+
+    @combine(target_type=['gate', 'instruction'],
+             order=['bind-decompose', 'decompose-bind'])
+    def test_to_instruction_expression_parameter_map(self, target_type, order):
         """Test preservation of expressions via instruction parameter_map."""
 
         theta = Parameter('θ')
         phi = Parameter('phi')
         qr1 = QuantumRegister(1, name='qr1')
         qc1 = QuantumCircuit(qr1)
+
         qc1.rx(theta, qr1)
         qc1.rz(numpy.pi/2, qr1)
         qc1.ry(theta * phi, qr1)
@@ -661,7 +793,10 @@ def test_to_instruction_expression_parameter_map(self):
         theta_p = Parameter('theta')
         phi_p = Parameter('phi')
 
-        gate = qc1.to_instruction(parameter_map={theta: theta_p, phi: phi_p})
+        if target_type == 'gate':
+            gate = qc1.to_gate(parameter_map={theta: theta_p, phi: phi_p})
+        elif target_type == 'instruction':
+            gate = qc1.to_instruction(parameter_map={theta: theta_p, phi: phi_p})
 
         self.assertEqual(gate.params, [phi_p, theta_p])
 
@@ -670,13 +805,23 @@ def test_to_instruction_expression_parameter_map(self):
         qc2 = QuantumCircuit(qr2)
         qc2.ry(delta, qr2[0])
         qc2.append(gate, qargs=[qr2[1]])
+
         self.assertEqual(qc2.parameters, {delta, theta_p, phi_p})
 
-        bound_qc = qc2.decompose().bind_parameters({delta: 1, theta_p: 2, phi_p: 3})
-        self.assertEqual(float(bound_qc.data[0][0].params[0]), 1)
-        self.assertEqual(float(bound_qc.data[1][0].params[0]), 2)
-        self.assertEqual(float(bound_qc.data[2][0].params[0]), numpy.pi/2)
-        self.assertEqual(float(bound_qc.data[3][0].params[0]), 2 * 3)
+        binds = {delta: 1, theta_p: 2, phi_p: 3}
+        expected_qc = QuantumCircuit(qr2)
+        expected_qc.rx(2, 1)
+        expected_qc.rz(numpy.pi/2, 1)
+        expected_qc.ry(3 * 2, 1)
+        expected_qc.r(1, numpy.pi/2, 0)
+
+        if order == 'bind-decompose':
+            decomp_bound_qc = qc2.bind_parameters(binds).decompose()
+        elif order == 'decompose-bind':
+            decomp_bound_qc = qc2.decompose().bind_parameters(binds)
+
+        self.assertEqual(decomp_bound_qc.parameters, set())
+        self.assertEqual(decomp_bound_qc, expected_qc)
 
     def test_binding_across_broadcast_instruction(self):
         """Bind a parameter which was included via a broadcast instruction."""