Repository: Qiskit/qiskit-terra (python)

Issue:
QuantumCircuit.bind/assign_parameters do not raise if passed key of invalid type
<!-- ⚠️ 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**: master @  507d3d8a95127064a6e5836d72912d965e0ad796
- **Python version**: 3.6
- **Operating system**: osx

### What is the current behavior?

```
>>> import qiskit as qk
>>> theta = qk.circuit.Parameter('theta')
>>> qc = qk.QuantumCircuit(1)
>>> qc.rx(theta, 0)
>>> qc.draw()
     ┌───────────┐
q_0: ┤ RX(theta) ├
     └───────────┘
```

Attempting to bind a Parameter not in the circuit correctly raises:
```
>>> phi = qk.circuit.Parameter('phi')
>>> qc.bind_parameters({phi: 0.1})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2037, in bind_parameters
    return self.assign_parameters(value_dict)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2015, in assign_parameters
    bound_circuit._assign_parameter(parameter, value)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2061, in _assign_parameter
    for instr, param_index in self._parameter_table[parameter]:
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/parametertable.py", line 35, in __getitem__
    return self._table[key]
KeyError: Parameter(phi)
```

Likewise attempting to bind a different Parameter instance of the same name correctly raises:
```
>>> qc.bind_parameters({qk.circuit.Parameter('theta'): 0.1})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2037, in bind_parameters
    return self.assign_parameters(value_dict)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2015, in assign_parameters
    bound_circuit._assign_parameter(parameter, value)
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 2061, in _assign_parameter
    for instr, param_index in self._parameter_table[parameter]:
  File "/Users/kevin.krsulichibm.com/q/qiskit-terra/qiskit/circuit/parametertable.py", line 35, in __getitem__
    return self._table[key]
KeyError: Parameter(theta)
```

But attempting to bind using a non-Parameter type (like a string) silently passes:
```
>>> qc.bind_parameters({'theta': 0.1})
>>> qc.bind_parameters({'theta': 0.1}).draw()
     ┌───────────┐
q_0: ┤ RX(theta) ├
     └───────────┘
>>> qc.bind_parameters({None: 0.1}).draw()
     ┌───────────┐
q_0: ┤ RX(theta) ├
     └───────────┘
```

Likewise for `assign_parameters`:
```
>>> qc.assign_parameters({None: 0.1}).draw()
     ┌───────────┐
q_0: ┤ RX(theta) ├
     └───────────┘
```

### Steps to reproduce the problem



### What is the expected behavior?

Even the examples above which are correctly raising, are raising from the wrong place and with the wrong error type. It's expected we catch these cases here:

https://github.com/Qiskit/qiskit-terra/blob/7e40ed1174938955235a2d9d50ce2729c17c3d3e/qiskit/circuit/quantumcircuit.py#L2010

There are tests covering these cases, but they should updated to check the type of error raised and the message:

https://github.com/Qiskit/qiskit-terra/blob/44462a8b13ea6c2cce0f9c7345c26c15fb0d4ce3/test/python/circuit/test_parameters.py#L299

### Suggested solutions




Pull request:
Add proper error checking for bind/assign parameters in QuantumCircuit
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

Fixes #5612

### Details and comments

This PR adds proper error checking to `QuantumCircuit.bind_parameters` and `QuantumCircuit.assign_parameters` for these cases,
- Parameter not in the circuit
- Element of `ParameterVector` not in circuit
- Any non-`Parameter` or None as parameter name
```
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter, ParameterVector

theta = Parameter('theta')
phi = Parameter('phi')
rho = Parameter('rho')
delta = Parameter('delta')
xi = ParameterVector('xi', 5)

qc = QuantumCircuit(1)
qc.rx(theta, 0)
qc.rx(xi[2], 0)
qc.bind_parameters({theta: 3})
qc.bind_parameters({rho: 6, delta: 7, xi: [1,2,3,4,5], 'gamma': 4, None: 5})

qc.draw()
```
```
CircuitError: 'Cannot bind parameters (rho, delta, xi[0], xi[1], xi[3], xi[4], gamma, None) not present in the circuit.'
```


Patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index d04854325..ef40b553e 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -1878,10 +1878,12 @@ class QuantumCircuit:
         # unroll the parameter dictionary (needed if e.g. it contains a ParameterVector)
         unrolled_param_dict = self._unroll_param_dict(param_dict)
 
-        # check that only existing parameters are in the parameter dictionary
-        if unrolled_param_dict.keys() > self._parameter_table.keys():
+        # check that all param_dict items are in the _parameter_table for this circuit
+        params_not_in_circuit = [param_key for param_key in unrolled_param_dict
+                                 if param_key not in self._parameter_table.keys()]
+        if len(params_not_in_circuit) > 0:
             raise CircuitError('Cannot bind parameters ({}) not present in the circuit.'.format(
-                [str(p) for p in param_dict.keys() - self._parameter_table]))
+                ', '.join(map(str, params_not_in_circuit))))
 
         # replace the parameters with a new Parameter ("substitute") or numeric value ("bind")
         for parameter, value in unrolled_param_dict.items():
@@ -1912,14 +1914,15 @@ class QuantumCircuit:
     def _unroll_param_dict(self, value_dict):
         unrolled_value_dict = {}
         for (param, value) in value_dict.items():
-            if isinstance(param, ParameterExpression):
-                unrolled_value_dict[param] = value
             if isinstance(param, ParameterVector):
                 if not len(param) == len(value):
                     raise CircuitError('ParameterVector {} has length {}, which '
                                        'differs from value list {} of '
                                        'len {}'.format(param, len(param), value, len(value)))
                 unrolled_value_dict.update(zip(param, value))
+            # pass anything else except number through. error checking is done in assign_parameter
+            elif isinstance(param, (ParameterExpression, str)) or param is None:
+                unrolled_value_dict[param] = value
         return unrolled_value_dict
 
     def _assign_parameter(self, parameter, value):


Test patch:
diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py
index 5f2ccbca5..8af5cfc21 100644
--- a/test/python/circuit/test_parameters.py
+++ b/test/python/circuit/test_parameters.py
@@ -300,6 +300,7 @@ class TestParameters(QiskitTestCase):
         """Verify binding parameters which are not present in the circuit raises an error."""
         x = Parameter('x')
         y = Parameter('y')
+        z = ParameterVector('z', 3)
         qr = QuantumRegister(1)
         qc = QuantumCircuit(qr)
 
@@ -309,9 +310,12 @@ class TestParameters(QiskitTestCase):
             with self.subTest(assign_fun=assign_fun):
                 qc.p(0.1, qr[0])
                 self.assertRaises(CircuitError, getattr(qc, assign_fun), {x: 1})
-
                 qc.p(x, qr[0])
                 self.assertRaises(CircuitError, getattr(qc, assign_fun), {x: 1, y: 2})
+                qc.p(z[1], qr[0])
+                self.assertRaises(CircuitError, getattr(qc, assign_fun), {z: [3, 4, 5]})
+                self.assertRaises(CircuitError, getattr(qc, assign_fun), {'a_str': 6})
+                self.assertRaises(CircuitError, getattr(qc, assign_fun), {None: 7})
 
     def test_gate_multiplicity_binding(self):
         """Test binding when circuit contains multiple references to same gate"""