From da5038a9f47ed6595188b60d7775ed830e60c3c6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 19 Dec 2023 11:45:31 +0000 Subject: [PATCH] Add `QuantumCircuit.get_parameter` to retrieve by name This allows `Parameter` instances to be retrieved from a circuit by string name. The interface is a mirror of the same functionality already added for run-time variables (`get_var`), but for the compile-time `Parameter` class. Similarly, a `has_parameter` method mirrors `has_var`. --- qiskit/circuit/parametertable.py | 18 +++- qiskit/circuit/quantumcircuit.py | 90 ++++++++++++++++++- ...ircuit-get-parameter-d33c08925b5c7d72.yaml | 7 ++ test/python/circuit/test_parameters.py | 57 +++++++++++- 4 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/circuit-get-parameter-d33c08925b5c7d72.yaml diff --git a/qiskit/circuit/parametertable.py b/qiskit/circuit/parametertable.py index f6cf022b0744..71659aa6474d 100644 --- a/qiskit/circuit/parametertable.py +++ b/qiskit/circuit/parametertable.py @@ -13,6 +13,7 @@ Look-up table for variable parameters in QuantumCircuit. """ import operator +import typing from collections.abc import MappingView, MutableMapping, MutableSet @@ -124,7 +125,7 @@ def __init__(self, mapping=None): self._table = {} self._keys = set(self._table) - self._names = {x.name for x in self._table} + self._names = {x.name: x for x in self._table} def __getitem__(self, key): return self._table[key] @@ -149,7 +150,7 @@ def __setitem__(self, parameter, refs): self._table[parameter] = refs self._keys.add(parameter) - self._names.add(parameter.name) + self._names[parameter.name] = parameter def get_keys(self): """Return a set of all keys in the parameter table @@ -165,7 +166,16 @@ def get_names(self): Returns: set: A set of all the names in the parameter table """ - return self._names + return self._names.keys() + + def parameter_from_name(self, name: str, default: typing.Any = None): + """Get a :class:`.Parameter` with references in this table by its string name, or return the + default if not present. + + Args: + name: the name of the :class:`.Parameter` + default: the object that should be returned if the parameter is missing.""" + return self._names.get(name, default) def discard_references(self, expression, key): """Remove all references to parameters contained within ``expression`` at the given table @@ -181,7 +191,7 @@ def discard_references(self, expression, key): def __delitem__(self, key): del self._table[key] self._keys.discard(key) - self._names.discard(key.name) + del self._names[key.name] def __iter__(self): return iter(self._table) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 11070059c927..0edd0d79ff35 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1486,6 +1486,85 @@ def _update_parameter_table(self, instruction: CircuitInstruction): # clear cache if new parameter is added self._parameters = None + @typing.overload + def get_parameter(self, name: str, default: T) -> Union[Parameter, T]: + ... + + # The builtin `types` module has `EllipsisType`, but only from 3.10+! + @typing.overload + def get_parameter(self, name: str, default: type(...) = ...) -> Parameter: + ... + + # We use a _literal_ `Ellipsis` as the marker value to leave `None` available as a default. + def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: + """Retrieve a compile-time parameter that is accessible in this circuit scope by name. + + Args: + name: the name of the parameter to retrieve. + default: if given, this value will be returned if the parameter is not present. If it + is not given, a :exc:`KeyError` is raised instead. + + Returns: + The corresponding parameter. + + Raises: + KeyError: if no default is given, but the parameter does not exist in the circuit. + + Examples: + Retrieve a parameter by name from a circuit:: + + from qiskit.circuit import QuantumCircuit, Parameter + + my_param = Parameter("my_param") + + # Create a parametrised circuit. + qc = QuantumCircuit(1) + qc.rx(my_param, 0) + + # We can use 'my_param' as a parameter, but let's say we've lost the Python object + # and need to retrieve it. + my_param_again = qc.get_parameter("my_param") + + assert my_param is my_param_again + + Get a variable from a circuit by name, returning some default if it is not present:: + + assert qc.get_parameter("my_param", None) is my_param + assert qc.get_parameter("unknown_param", None) is None + + See also: + :meth:`get_var` + A similar method, but for :class:`.expr.Var` run-time variables instead of + :class:`.Parameter` compile-time parameters. + """ + if (parameter := self._parameter_table.parameter_from_name(name, None)) is None: + if default is Ellipsis: + raise KeyError(f"no parameter named '{name}' is present") + return default + return parameter + + def has_parameter(self, name_or_param: str | Parameter, /) -> bool: + """Check whether a parameter object exists in this circuit. + + Args: + name_or_param: the parameter, or name of a parameter to check. If this is a + :class:`.Parameter` node, the parameter must be exactly the given one for this + function to return ``True``. + + Returns: + whether a matching parameter is assignable in this circuit. + + See also: + :meth:`QuantumCircuit.get_parameter` + Retrive the :class:`.Parameter` instance from this circuit by name. + :meth:`QuantumCircuit.has_var` + A similar method to this, but for run-time :class:`.expr.Var` variables instead of + compile-time :class:`.Parameter`\\ s. + """ + if isinstance(name_or_param, str): + return self.get_parameter(name_or_param, None) is not None + return self.get_parameter(name_or_param.name) == name_or_param + @typing.overload def get_var(self, name: str, default: T) -> Union[expr.Var, T]: ... @@ -1529,6 +1608,11 @@ def get_var(self, name: str, default: typing.Any = ...): assert qc.get_var("my_var", None) is my_var assert qc.get_var("unknown_variable", None) is None + + See also: + :meth:`get_parameter` + A similar method, but for :class:`.Parameter` compile-time parameters instead of + :class:`.expr.Var` run-time variables. """ if (out := self._current_scope().get_var(name)) is not None: return out @@ -1548,7 +1632,11 @@ def has_var(self, name_or_var: str | expr.Var, /) -> bool: whether a matching variable is accessible. See also: - :meth:`QuantumCircuit.get_var`: retrieve a named variable from a circuit. + :meth:`QuantumCircuit.get_var` + Retrive the :class:`.expr.Var` instance from this circuit by name. + :meth:`QuantumCircuit.has_parameter` + A similar method to this, but for compile-time :class:`.Parameter`\\ s instead of + run-time :class:`.expr.Var` variables. """ if isinstance(name_or_var, str): return self.get_var(name_or_var, None) is not None diff --git a/releasenotes/notes/circuit-get-parameter-d33c08925b5c7d72.yaml b/releasenotes/notes/circuit-get-parameter-d33c08925b5c7d72.yaml new file mode 100644 index 000000000000..755b5987451e --- /dev/null +++ b/releasenotes/notes/circuit-get-parameter-d33c08925b5c7d72.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + :class:`.QuantumCircuit` has two new methods, :meth:`~.QuantumCircuit.get_parameter` and + :meth:`~.QuantumCircuit.has_parameter`, which respectively retrieve a :class:`.Parameter` + instance used in the circuit by name, and return a Boolean of whether a parameter with a + matching name (or the exact instance given) are used in the circuit. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 5c6e9a15bdf9..a25082df0ab2 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -133,7 +133,7 @@ def test_duplicate_name_on_append(self): qc.rx(param_a, 0) self.assertRaises(CircuitError, qc.rx, param_a_again, 0) - def test_get_parameters(self): + def test_parameters_property(self): """Test instantiating gate with variable parameters""" from qiskit.circuit.library.standard_gates.rx import RXGate @@ -147,7 +147,7 @@ def test_get_parameters(self): self.assertIs(theta, next(iter(vparams))) self.assertEqual(rxg, next(iter(vparams[theta]))[0]) - def test_get_parameters_by_index(self): + def test_parameters_property_by_index(self): """Test getting parameters by index""" x = Parameter("x") y = Parameter("y") @@ -164,6 +164,59 @@ def test_get_parameters_by_index(self): for i, vi in enumerate(v): self.assertEqual(vi, qc.parameters[i]) + def test_get_parameter(self): + """Test the `get_parameter` method.""" + x = Parameter("x") + y = Parameter("y") + z = Parameter("z") + v = ParameterVector("v", 3) + + qc = QuantumCircuit(1) + qc.rx(x + y + z + sum(v), 0) + + self.assertIs(qc.get_parameter("x"), x) + self.assertIs(qc.get_parameter("y"), y) + self.assertIs(qc.get_parameter("z"), z) + self.assertIs(qc.get_parameter(v[1].name), v[1]) + + self.assertIsNone(qc.get_parameter("abc", None)) + self.assertEqual(qc.get_parameter("jfkdla", "not present"), "not present") + + with self.assertRaisesRegex(KeyError, "no parameter named"): + qc.get_parameter("jfklda") + + def test_get_parameter_global_phase(self): + """Test that `get_parameter` works on parameters that only appear in the global phase.""" + x = Parameter("x") + qc = QuantumCircuit(0, global_phase=x) + + self.assertIs(qc.get_parameter("x"), x) + self.assertIsNone(qc.get_parameter("y", None), None) + + def test_has_parameter(self): + """Test the `has_parameter` method.""" + x = Parameter("x") + y = Parameter("y") + z = Parameter("z") + v = ParameterVector("v", 3) + + qc = QuantumCircuit(1) + qc.rx(x + y + z + sum(v), 0) + + self.assertTrue(qc.has_parameter("x")) + self.assertTrue(qc.has_parameter("y")) + self.assertTrue(qc.has_parameter("z")) + self.assertTrue(qc.has_parameter(v[1].name)) + + self.assertFalse(qc.has_parameter("abc")) + self.assertFalse(qc.has_parameter("jfkdla")) + + self.assertTrue(qc.has_parameter(x)) + self.assertTrue(qc.has_parameter(y)) + + # This `z` should compare unequal to the first one, so it should appear absent. + self.assertFalse(qc.has_parameter(Parameter("z"))) + def test_bind_parameters_anonymously(self): """Test setting parameters by insertion order anonymously""" phase = Parameter("phase")