Skip to content

Instantly share code, notes, and snippets.

@Cryoris
Created February 4, 2021 15:39
Show Gist options
  • Save Cryoris/1bc894e684f381e07a131196bf86a4da to your computer and use it in GitHub Desktop.
Save Cryoris/1bc894e684f381e07a131196bf86a4da to your computer and use it in GitHub Desktop.

Ordered circuit parameters

This document discusses (1) facilitated assigning of parameter values to a circuit, and (2) adding a maintained order to circuit parameters.

Current situation

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})

Desired features

The status quo has a few shortcomings, mainly in UX

  • you have to carry along the instance of the Parameters
  • 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

  1. 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})
  2. 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])
  3. Keep the circuit parameters sorted by insertion, for consistency with anonymous assigning and to be able to check the order of parameters.

Use cases

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
    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, ...])
    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
    grad = Gradient().convert(expectation)
    hess = Hessian().convert(expectation)
    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.

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
    x = Parameter('x')
    circuit = QuantumCircuit(1)
    circuit.rx(x, 0)
    circuit.assign_parameters({'x': 1})
    or even for a full parametervector, 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.

Proposal

The proposal is split in three parts, which can be implemented independently.

  1. 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.

  2. 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.

  3. 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 of circuit.parameters by an intermediate object that implements List methods and deprecated Set methods.

Implementation Details

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.

Transferring from set to list

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.

Maintaining parameter order

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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment