Skip to content

Commit

Permalink
Add QuantumCircuit.get_parameter to retrieve by name (#11431)
Browse files Browse the repository at this point in the history
* 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`.

* Update qiskit/circuit/parametertable.py

Co-authored-by: Julien Gacon <gaconju@gmail.com>

* Fix typos

Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>

* Fix blank space

---------

Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com>
  • Loading branch information
3 people authored Jan 8, 2024
1 parent 733e2a3 commit 3263355
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 7 deletions.
20 changes: 16 additions & 4 deletions qiskit/circuit/parametertable.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Look-up table for variable parameters in QuantumCircuit.
"""
import operator
import typing
from collections.abc import MappingView, MutableMapping, MutableSet


Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -165,7 +166,18 @@ 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.
If the parameter is not present, return the ``default`` value.
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
Expand All @@ -181,7 +193,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)
Expand Down
90 changes: 89 additions & 1 deletion qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,85 @@ def _update_parameter_table(self, instruction: Instruction):
# 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`
Retrieve 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]:
...
Expand Down Expand Up @@ -1545,6 +1624,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
Expand All @@ -1564,7 +1648,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`
Retrieve 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 55 additions & 2 deletions test/python/circuit/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand All @@ -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")
Expand Down

0 comments on commit 3263355

Please sign in to comment.