This document discusses (1) facilitated assigning of parameter values to a circuit, and (2) adding a maintained order to circuit parameters.
A circuit can have free parameters in qubit gates, defined with the Parameter
class.
from qiskit.circuit import QuantumCircuit, Parameter
x = Parameter('x')
circuit = QuantumCircuit(1)
circuit.rx(x, 0)
We support many operations, such as transpilation, with free parameters. Before execution,
however, all free parameters must be bound. This is currently done using a dictionary
with {parameter_instance: value}
.
bound_circuit = circuit.bind_parameters({x: 0.23})
For convenience and handling of arrays, there exists the ParameterVector
class that can be
used to define parameter vectors of given length.
from qiskit.circuit import ParameterVector, QuantumCircuit
x = ParameterVector('x', 3)
circuit = QuantumCircuit(1)
circuit.rx(x[0], 0)
circuit.ry(x[1], 0)
circuit.rz(x[2], 0)
bound_circuit = circuit.bind_parameters({x: [0.1, 0.2, 0.3]})
# or bound_circuit = circuit.bind_parameters({x[0]: 0.1, x[1]: 0.2, x[2]: 0.3})
Note that the keys of the dictionary have to be the same instance as the parameter object used in the gate call. E.g. the following does not work:
x = Parameter('x')
circuit = QuantumCircuit(1)
circuit.rx(x, 0)
# in some other part of the program
circuit.bind_parameters({Parameter('x'): 0.23})
The status quo has a few shortcomings, mainly in UX
- you have to carry along the instance of the
Parameter
s - playing with circuits and assigning "by eye/name" if you look at the circuit diagram is not easily doable
- you may only keep the parameter values in an array if you have a single parameter vector, which is incompatible with extending/composing circuits
- to compute gradients and hessians of expectation values/circuit you always have to pass the parameters with respect to which you derive, otherwise the order of the vector/matrix entries are undefined
- to ensure reproducibility of any algorithm that uses a classical optimization routine you have to sort the parameters anyways (currently manually)
To improve the handling of parameters, we suggest the following features
- Allow assigning values via name instead of instance. This is possible since parameter names are already required to be unique.
circuit.bind_parameters({'x': 0.23})
- Allow assigning a list of values to a circuit to bind all parameters anonymously, i.e. without specifying the parameters they belong to.
circuit.bind_parameters([0.1, 0.2, 0.3])
- Keep the circuit parameters sorted by insertion, for consistency with anonymous assigning and to be able to check the order of parameters.
From optimization algorithms:
- Pretty much all classical optimization routines handle parameters as lists of values; including the numerical optimization routines used in algos like the VQE or QAOA. Since parameters are not sorted, running each optimization run might optimize the values in a different order, which leads to different results. The algorithms currently manually ensure that the parameters are sorted in a consistent manner -- though this might not be the order the user added them to the circuit, which has already led to some confusion in the past. If the parameters were (insertion) sorted by default and one could assign a plain vector, all this logic would fall away and circuits could naturally interact with optimizers. This works e.g. nicely in pennylane.
From gradients:
- Constructing gradients or hessian currently works like this
the parameters have to be carried along and passed to the gradients, since this specifies the order of the vector or matrix entries. It would be a lot more convenient to just call
ansatz = # some ansatz with parameters [x, y, z, ...] expectation = # some expectation based on ansatz grad = Gradient().convert(expectation, params=[x, y, z, ...]) hess = Hessian().convert(expectation, params=[x, y, z, ...])
but this would also require some sorting, which might not be what users expect. This can make comparison to other frameworks and validation difficult; where the usual order is by insertion.grad = Gradient().convert(expectation) hess = Hessian().convert(expectation)
Consistency:
- Some library circuits already have an
ordered_parameters
attribute, which exists for (1) reproducibility, (2) backward compatibility with some old variational forms (and (3) convenience). If we had sorted parameters, this attribute would no longer be needed (also mentioned in #5557).
From Qiskit/qiskit#5557:
- it would be more convenient to type
or even for a full parametervector,
x = Parameter('x') circuit = QuantumCircuit(1) circuit.rx(x, 0) circuit.assign_parameters({'x': 1})
x = ParameterVector('x', 10)
and{'x': [0, 1, 2, ...}
. This can be particularly useful if (1) the circuit is passed around or (2) users are playing with circuits and interact a lot by plotting.
The proposal is split in three parts, which can be implemented independently.
-
Allow assigning parameters by name. This is feasible since parameter names are required to be unique. For now, this will only be supported for single parameters and now parameter vectors. This only touches the
{bind,assign}_parameters
method of the circuit. -
Allow assigning parameters anonymously by vector. The order of parameters is insertion ordered, by first appearance. This is consistent with how the internal parameter table is constructed and the insertion order of Python dictionaries. This only touches the
{bind,assign}_parameters
method of the circuit. -
Let
circuit.parameters
return a ordered container instead of a set, and preserve this order throughout different circuit operations, such as converting to a dag and back, or transpiling. We start off by ensuring only that the parameter order is consistent after circuit construction and then gradually extend the operations that keep the order consistent. On the first change, this replaces the return type ofcircuit.parameters
by an intermediate object that implementsList
methods and deprecatedSet
methods.
Step 1 poses no particular issues.
Neither does Step 2, although it will be confusing that assigning by array is supported while
circuit.parameters
does still return a set.
Step 3 will be discussed in more detail here, in particular how to transfer from returning a set to a list and how circuit operations affect parameter order.
Currently, circuit.parameters
returns a set. Sets are unsorted and are therefore not consistent
with assigning parameters by array. Imagine e.g. a user assigning per array and then wanting to
verify in which order the parameters are bound.
We can gracefully change from a set to a list (with unique elements) by returning an intermediate
object that implements the List
interface, as well as the Set
methods but deprecates latter.
Since the parameters are internally stored as keys of a dictionary, we'll call this object a
ParameterView
.
Composing circuits
The order of composed circuits is the order of the equivalent circuit constructed from scratch, with all operations of the circuit on the front added first.
a = # circuit with parameters [x1, x2]
b = # circuit with parameters [x3, x4]
result = a.compose(b) # [x1, x2, x3, x4]
result = a.compose(b, front=True) # [x3, x4, x1, x2]
Appending circuits
a = # circuit with parameters [x1, x2]
b = # circuit with parameters [x3, x4]
a.append(b, qubits) # [x1, x2, x3, x4]
Converting to DAG and back
Converting to a DAG and back maintains the parameter order. This requires storing the order of parameters in the DAG.
a = # circuit with parameters [x1, x2, x3]
result = dag_to_circuit(circuit_to_dag(a)) # still [x1, x2, x3]