Repository: Qiskit/qiskit-terra (python)

Issue:
Unable bind parameters in global phase
<!-- ⚠️ 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 @ c3b2d7ac
- **Python version**: 3.7.9
- **Operating system**: macOS Big Sur

### What is the current behavior?

If the global phase of a circuit contains a parameter it can currently not be bound:
```python
>>> from qiskit.circuit import QuantumCircuit, Parameter
>>> x = Parameter('x')
>>> circuit = QuantumCircuit(1, global_phase=x)
>>> circuit.bind_parameters({x: 2}).draw()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/jul/Qiskit/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 1912, in bind_parameters
    return self.assign_parameters(value_dict)
  File "/Users/jul/Qiskit/qiskit-terra/qiskit/circuit/quantumcircuit.py", line 1886, in assign_parameters
    ', '.join(map(str, params_not_in_circuit))))
qiskit.circuit.exceptions.CircuitError: 'Cannot bind parameters (x) not present in the circuit.'
```
The above bug might be due to #5648, since the low-level `_assign_parameter` seems to support global phase binding.

Also, they are not listed when calling `circuit.parameters`:
```python
>>> from qiskit.circuit import QuantumCircuit, Parameter
>>> from qiskit.opflow import StateFn
>>> x = Parameter('x')
>>> circuit = QuantumCircuit(1, global_phase=x)
>>> circuit.parameters
set()
```



Pull request description:
Fix parameter handling in global phase
<!--
⚠️ 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 #5806.

### Details and comments

Now:
```python
>>> from qiskit.circuit import QuantumCircuit, Parameter
>>> x = Parameter('x')
>>> circuit = QuantumCircuit(1, global_phase=x)
>>> circuit.parameters
{Parameter(x)}
>>> circuit.bind_parameters({x: 2}).draw()
global phase: 2

q_0:

```


Patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index e6e472a05..fb2d4b65c 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -83,7 +83,7 @@ class QuantumCircuit:
 
         name (str): the name of the quantum circuit. If not set, an
             automatically generated string will be assigned.
-        global_phase (float): The global phase of the circuit in radians.
+        global_phase (float or ParameterExpression): The global phase of the circuit in radians.
         metadata (dict): Arbitrary key value metadata to associate with the
             circuit. This gets stored as free-form data in a dict in the
             :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will
@@ -1806,7 +1806,14 @@ class QuantumCircuit:
     @property
     def parameters(self):
         """Convenience function to get the parameters defined in the parameter table."""
-        return self._parameter_table.get_keys()
+        # parameters from gates
+        params = self._parameter_table.get_keys()
+
+        # parameters in global phase
+        if isinstance(self.global_phase, ParameterExpression):
+            return params.union(self.global_phase.parameters)
+
+        return params
 
     @property
     def num_parameters(self):
@@ -1882,7 +1889,7 @@ class QuantumCircuit:
 
         # 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 param_key not in self.parameters]
         if len(params_not_in_circuit) > 0:
             raise CircuitError('Cannot bind parameters ({}) not present in the circuit.'.format(
                 ', '.join(map(str, params_not_in_circuit))))
@@ -1936,25 +1943,27 @@ class QuantumCircuit:
             value (Union(ParameterExpression, float, int)): A numeric or parametric expression to
                 replace instances of ``parameter``.
         """
-        for instr, param_index in self._parameter_table[parameter]:
-            new_param = instr.params[param_index].assign(parameter, value)
-            # if fully bound, validate
-            if len(new_param.parameters) == 0:
-                instr.params[param_index] = instr.validate_parameter(new_param)
-            else:
-                instr.params[param_index] = new_param
+        # parameter might be in global phase only
+        if parameter in self._parameter_table.keys():
+            for instr, param_index in self._parameter_table[parameter]:
+                new_param = instr.params[param_index].assign(parameter, value)
+                # if fully bound, validate
+                if len(new_param.parameters) == 0:
+                    instr.params[param_index] = instr.validate_parameter(new_param)
+                else:
+                    instr.params[param_index] = new_param
 
-            self._rebind_definition(instr, parameter, value)
+                self._rebind_definition(instr, parameter, value)
 
-        if isinstance(value, ParameterExpression):
-            entry = self._parameter_table.pop(parameter)
-            for new_parameter in value.parameters:
-                if new_parameter in self._parameter_table:
-                    self._parameter_table[new_parameter].extend(entry)
-                else:
-                    self._parameter_table[new_parameter] = entry
-        else:
-            del self._parameter_table[parameter]  # clear evaluated expressions
+            if isinstance(value, ParameterExpression):
+                entry = self._parameter_table.pop(parameter)
+                for new_parameter in value.parameters:
+                    if new_parameter in self._parameter_table:
+                        self._parameter_table[new_parameter].extend(entry)
+                    else:
+                        self._parameter_table[new_parameter] = entry
+            else:
+                del self._parameter_table[parameter]  # clear evaluated expressions
 
         if (isinstance(self.global_phase, ParameterExpression) and
                 parameter in self.global_phase.parameters):
diff --git a/releasenotes/notes/bind-global-phase-a7248943488f4010.yaml b/releasenotes/notes/bind-global-phase-a7248943488f4010.yaml
new file mode 100644
index 000000000..3edd4045c
--- /dev/null
+++ b/releasenotes/notes/bind-global-phase-a7248943488f4010.yaml
@@ -0,0 +1,5 @@
+---
+fixes:
+  - |
+    Fix a bug where a free parameters only occurring in ``QuantumCircuit.global_phase`` did not
+    show upon calling ``QuantumCircuit.parameters`` and could not be bound.


Test patch:
diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py
index 311f90762..abfdcee9a 100644
--- a/test/python/circuit/test_circuit_operations.py
+++ b/test/python/circuit/test_circuit_operations.py
@@ -427,6 +427,30 @@ class TestCircuitOperations(QiskitTestCase):
         expected = np.exp(1j * phase * num) * np.identity(2)
         np.testing.assert_array_almost_equal(Operator(qc.repeat(num)).data, expected)
 
+    def test_bind_global_phase(self):
+        """Test binding global phase."""
+        x = Parameter('x')
+        circuit = QuantumCircuit(1, global_phase=x)
+        self.assertEqual(circuit.parameters, {x})
+
+        bound = circuit.bind_parameters({x: 2})
+        self.assertEqual(bound.global_phase, 2)
+        self.assertEqual(bound.parameters, set())
+
+    def test_bind_parameter_in_phase_and_gate(self):
+        """Test binding a parameter present in the global phase and the gates."""
+        x = Parameter('x')
+        circuit = QuantumCircuit(1, global_phase=x)
+        circuit.rx(x, 0)
+        self.assertEqual(circuit.parameters, {x})
+
+        ref = QuantumCircuit(1, global_phase=2)
+        ref.rx(2, 0)
+
+        bound = circuit.bind_parameters({x: 2})
+        self.assertEqual(bound, ref)
+        self.assertEqual(bound.parameters, set())
+
     def test_power(self):
         """Test taking the circuit to a power works."""
         qc = QuantumCircuit(2)
diff --git a/test/python/opflow/test_evolution.py b/test/python/opflow/test_evolution.py
index ba5580c8b..5d60a6772 100644
--- a/test/python/opflow/test_evolution.py
+++ b/test/python/opflow/test_evolution.py
@@ -100,11 +100,12 @@ class TestEvolution(QiskitOpflowTestCase):
         # wf = (Pl^Pl) + (Ze^Ze)
         wf = (op).exp_i() @ CX @ (H ^ I) @ Zero
         mean = evolution.convert(wf)
-        circuit_params = mean.to_circuit().parameters
-        # Check that the non-identity parameters are in the circuit
-        for p in thetas[1:]:
-            self.assertIn(p, circuit_params)
-        self.assertNotIn(thetas[0], circuit_params)
+        circuit = mean.to_circuit()
+        # Check that all parameters are in the circuit
+        for p in thetas:
+            self.assertIn(p, circuit.parameters)
+        # Check that the identity-parameters only exist as global phase
+        self.assertNotIn(thetas[0], circuit._parameter_table.get_keys())
 
     def test_bind_parameters(self):
         """ bind parameters test """