Skip to content

Commit

Permalink
Merge branch 'main' into rename-basic-aer
Browse files Browse the repository at this point in the history
  • Loading branch information
ElePT authored Jan 9, 2024
2 parents 56ecc8d + d113a6a commit b9f97bf
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 39 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
153 changes: 131 additions & 22 deletions qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,10 @@ def compose(
"Cannot emit a new composed circuit while a control-flow context is active."
)

# Avoid mutating `dest` until as much of the error checking as possible is complete, to
# avoid an in-place composition getting `self` in a partially mutated state for a simple
# error that the user might want to correct in an interactive session.
dest = self if inplace else self.copy()
dest.duration = None
dest.unit = "dt"

# As a special case, allow composing some clbits onto no clbits - normally the destination
# has to be strictly larger. This allows composing final measurements onto unitary circuits.
Expand Down Expand Up @@ -997,23 +998,9 @@ def compose(
"Trying to compose with another QuantumCircuit which has more 'in' edges."
)

for gate, cals in other.calibrations.items():
dest._calibrations[gate].update(cals)

dest.global_phase += other.global_phase

if not other.data:
# Nothing left to do. Plus, accessing 'data' here is necessary
# to trigger any lazy building since we now access '_data'
# directly.
return None if inplace else dest

# The 'qubits' and 'clbits' used for 'dest'.
# Maps bits in 'other' to bits in 'dest'.
mapped_qubits: list[Qubit]
mapped_clbits: list[Clbit]

# Maps bits in 'other' to bits in 'dest'. Used only for
# adjusting bits in variables (e.g. condition and target).
edge_map: dict[Qubit | Clbit, Qubit | Clbit] = {}
if qubits is None:
mapped_qubits = dest.qubits
Expand All @@ -1025,6 +1012,10 @@ def compose(
f"Number of items in qubits parameter ({len(mapped_qubits)}) does not"
f" match number of qubits in the circuit ({len(other.qubits)})."
)
if len(set(mapped_qubits)) != len(mapped_qubits):
raise CircuitError(
f"Duplicate qubits referenced in 'qubits' parameter: '{mapped_qubits}'"
)
edge_map.update(zip(other.qubits, mapped_qubits))

if clbits is None:
Expand All @@ -1037,8 +1028,25 @@ def compose(
f"Number of items in clbits parameter ({len(mapped_clbits)}) does not"
f" match number of clbits in the circuit ({len(other.clbits)})."
)
if len(set(mapped_clbits)) != len(mapped_clbits):
raise CircuitError(
f"Duplicate clbits referenced in 'clbits' parameter: '{mapped_clbits}'"
)
edge_map.update(zip(other.clbits, dest.cbit_argument_conversion(clbits)))

for gate, cals in other.calibrations.items():
dest._calibrations[gate].update(cals)

dest.duration = None
dest.unit = "dt"
dest.global_phase += other.global_phase

if not other.data:
# Nothing left to do. Plus, accessing 'data' here is necessary
# to trigger any lazy building since we now access '_data'
# directly.
return None if inplace else dest

variable_mapper = _classical_resource_map.VariableMapper(
dest.cregs, edge_map, dest.add_register
)
Expand Down Expand Up @@ -1502,6 +1510,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 +1632,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 +1656,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 Expand Up @@ -2987,7 +3083,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.
Expand All @@ -2997,14 +3093,26 @@ 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.
If True the circuit instance itself is modified.
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.
Expand Down Expand Up @@ -3182,9 +3290,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():
Expand All @@ -3195,6 +3302,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
Expand Down
10 changes: 0 additions & 10 deletions qiskit_bot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,6 @@ notifications:
- "`@levbishop`"
"quantum_info":
- "`@ikkoham`"
"utils/run_circuits":
- "`@woodsp-ibm`"
"utils/quantum_instance":
- "`@woodsp-ibm`"
"opflow":
- "`@woodsp-ibm`"
- "`@Cryoris`"
"algorithms":
- "`@ElePT`"
- "`@woodsp-ibm`"
"circuit/library":
- "`@Cryoris`"
- "`@ajavadia`"
Expand Down
14 changes: 14 additions & 0 deletions releasenotes/notes/assign-by-name-305f2bbf89099174.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
features:
- |
:meth:`.QuantumCircuit.assign_parameters` now accepts string keys in the mapping form of input.
These names are used to look up the corresponding :class:`.Parameter` instance using
:meth:`~.QuantumCircuit.get_parameter`. This lets you do::
from qiskit.circuit import QuantumCircuit, Parameter
a = Parameter("a")
qc = QuantumCircuit(1)
qc.rx(a, 0)
qc.assign_parameters({"a": 1}) == qc.assign_parameters({a: 1})
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
:meth:`.QuantumCircuit.compose` will now correctly raise a :exc:`.CircuitError` when there are
duplicates in the ``qubits`` or ``clbits`` arguments.
11 changes: 11 additions & 0 deletions test/python/circuit/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
Instruction,
CASE_DEFAULT,
SwitchCaseOp,
CircuitError,
)
from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal
from qiskit.circuit.classical import expr
Expand Down Expand Up @@ -880,6 +881,16 @@ def test_expr_target_is_mapped(self):

self.assertEqual(dest, expected)

def test_rejects_duplicate_bits(self):
"""Test that compose rejects duplicates in either qubits or clbits."""
base = QuantumCircuit(5, 5)

attempt = QuantumCircuit(2, 2)
with self.assertRaisesRegex(CircuitError, "Duplicate qubits"):
base.compose(attempt, [1, 1], [0, 1])
with self.assertRaisesRegex(CircuitError, "Duplicate clbits"):
base.compose(attempt, [0, 1], [1, 1])


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit b9f97bf

Please sign in to comment.