Repository: Qiskit/qiskit

Issue:
Support binding/assigning parameters by name
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?

From @Cryoris 's comment in https://github.com/Qiskit/qiskit-terra/issues/5557#issuecomment-757520462 :

> Personally I'd be fine if that'd work with a string as well, since parameter names are unique in circuits.
> ```
> partially_bound = circuit.assign_parameters({'x': value})
> ```

We should consider adding support for binding and assigning parameters via their string name. This was originally omitted for concerns over overlapping parameter names between different circuits. However, by the stage of `bind_`/`assign_`, the `Parameter` s on a given circuit are fixed and guaranteed to be unique, so there isn't really a possibility for the ambiguity that exists when composing two parameterized circuits.


Discussion:
I helped someone in Slack recently who had assumed that this would be possible, and was surprised that it wasn't, so there's an indirect +1 on this from them.
Yes, I am the one who you helped @jakelishman. Thanks for that!! I was surprised because for example in myQLM you can bind parameters by the string name. You may not have access to the parameter variable name, and only to its string name. I would like to be assigned this issue to fix this problem if possible. Thanks.
Sure @VicentePerezSoloviev, let us know if you have any questions!
This would be a most welcome improvement.
This issue [seems to be blocking](https://github.com/Qiskit/qiskit/pull/11227#issuecomment-1838056254) the high priority #11227 . As such, tagging it as high priority too.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -3075,7 +3075,7 @@ def assign_parameters(  # pylint: disable=missing-raises-doc
     ) -> Optional["QuantumCircuit"]:
         """Assign parameters to new parameters or values.
 
-        If ``parameters`` is passed as a dictionary, the keys must be :class:`.Parameter`
+        If ``parameters`` is passed as a dictionary, the keys should be :class:`.Parameter`
         instances in the current circuit. The values of the dictionary can either be numeric values
         or new parameter objects.
 
@@ -3085,6 +3085,16 @@ def assign_parameters(  # pylint: disable=missing-raises-doc
 
         The values can be assigned to the current circuit object or to a copy of it.
 
+        .. note::
+            When ``parameters`` is given as a mapping, it is permissible to have keys that are
+            strings of the parameter names; these will be looked up using :meth:`get_parameter`.
+            You can also have keys that are :class:`.ParameterVector` instances, and in this case,
+            the dictionary value should be a sequence of values of the same length as the vector.
+
+            If you use either of these cases, you must leave the setting ``flat_input=False``;
+            changing this to ``True`` enables the fast path, where all keys must be
+            :class:`.Parameter` instances.
+
         Args:
             parameters: Either a dictionary or iterable specifying the new parameter values.
             inplace: If False, a copy of the circuit with the bound parameters is returned.
@@ -3092,7 +3102,9 @@ def assign_parameters(  # pylint: disable=missing-raises-doc
             flat_input: If ``True`` and ``parameters`` is a mapping type, it is assumed to be
                 exactly a mapping of ``{parameter: value}``.  By default (``False``), the mapping
                 may also contain :class:`.ParameterVector` keys that point to a corresponding
-                sequence of values, and these will be unrolled during the mapping.
+                sequence of values, and these will be unrolled during the mapping, or string keys,
+                which will be converted to :class:`.Parameter` instances using
+                :meth:`get_parameter`.
             strict: If ``False``, any parameters given in the mapping that are not used in the
                 circuit will be ignored.  If ``True`` (the default), an error will be raised
                 indicating a logic error.
@@ -3270,9 +3282,8 @@ def map_calibration(qubits, parameters, schedule):
         )
         return None if inplace else target
 
-    @staticmethod
     def _unroll_param_dict(
-        parameter_binds: Mapping[Parameter, ParameterValueType]
+        self, parameter_binds: Mapping[Parameter, ParameterValueType]
     ) -> Mapping[Parameter, ParameterValueType]:
         out = {}
         for parameter, value in parameter_binds.items():
@@ -3283,6 +3294,8 @@ def _unroll_param_dict(
                         f" but was assigned to {len(value)} values."
                     )
                 out.update(zip(parameter, value))
+            elif isinstance(parameter, str):
+                out[self.get_parameter(parameter)] = value
             else:
                 out[parameter] = value
         return out


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
@@ -244,6 +244,18 @@ def test_bind_parameters_allow_unknown(self):
         c = a.bind({a: 1, b: 1}, allow_unknown_parameters=True)
         self.assertEqual(c, a.bind({a: 1}))
 
+    def test_assign_parameters_by_name(self):
+        """Test that parameters can be assigned by name as well as value."""
+        a = Parameter("a")
+        b = Parameter("b")
+        c = Parameter("c")
+        qc = QuantumCircuit(2, global_phase=a * 2)
+        qc.rx(b + 0.125 * c, 0)
+
+        self.assertEqual(
+            qc.assign_parameters({a: 1, b: 2, c: 3}), qc.assign_parameters({"a": 1, "b": 2, "c": 3})
+        )
+
     def test_bind_parameters_custom_definition_global_phase(self):
         """Test that a custom gate with a parametrised `global_phase` is assigned correctly."""
         x = Parameter("x")
@@ -536,7 +548,7 @@ def test_raise_if_assigning_params_not_in_circuit(self):
         with self.assertRaises(CircuitError):
             qc.assign_parameters({z: [3, 4, 5]})
         with self.assertRaises(CircuitError):
-            qc.assign_parameters({"a_str": 6})
+            qc.assign_parameters({6: 6})
         with self.assertRaises(CircuitError):
             qc.assign_parameters({None: 7})