Qiskit / qiskit

Qiskit is an open-source SDK for working with quantum computers at the level of extended quantum circuits, operators, and primitives.
https://www.ibm.com/quantum/qiskit
Apache License 2.0
5.22k stars 2.36k forks source link

Better support for working with composite parameterized circuits #10665

Open nonhermitian opened 1 year ago

nonhermitian commented 1 year ago

What should we add?

There are currently several issues that make working with composite parameterized circuits challenging in Qiskit.

First, circuits from the circuit library use hard-coded parameter names by default. That means the following fails:


U = EfficientSU2(2)
V = EfficientSU2(2)
U.compose(V.inverse())

because the user did not implicitly know that they need to set the variables names themselves. Rather, the parameter names should be dynamic, like the circuit names are, so that there is no name conflicts by default.

Second, there is no absolute ordering to the way in which parameters get assigned to a composite parameterized circuit as the ordering depends on the alphabetical ordering of the parameter names. That is to say the following yield different insertion orders for the same operations:


U = EfficientSU2(2, parameter_prefix='a')
V = EfficientSU2(2, parameter_prefix='b')

U.compose(V.inverse())

U = EfficientSU2(2, parameter_prefix='b')
V = EfficientSU2(2, parameter_prefix='a')

U.compose(V.inverse())

This is very tricky to deal with as the natural assumption is that indexing would be done by insertion order that is to say that in the first example, one could index the full set of parameters like [a0, a1,...,b0, b1, ...] and the second [b0, b1,..., a0, a1,...].

The way around this is to reassign variable names such that the alphabetical order matches the insertion order. However, this is a bit unsatisfying as : a) The user needs to know to do this, b) without a convention, it makes indexing difficult as one needs to know the renamed parameters each time. For a single user this is fine, for making routines for others to consume this is challenging. It is even more challenging because whatever the convention is, it must satisfy the alphabetical ordering that Qiskit currently uses. E.g a conversion like x0, x1, x2, ... x10,... fails since x2 comes after x10 in the alphabetical sorting. Having a convention would allow for deterministic partial binding by sub-circuit, which is likely a handy thing to have.

Finally, there is no good documentation on parameterized circuits in Qiskit save for this short section: https://qiskit.org/documentation/tutorials/circuits_advanced/01_advanced_circuits.html#Parameterized-circuits that was last updated 3 years ago.

Cryoris commented 1 year ago

Dynamic naming, like circuits, sounds like a good improvement 👍🏻 I've also stumbled over trying to append multiple parameterized circuit from the library and having to manually set the parameter-prefix.

We discussed the order of parameters a lot around the PR #5759, and we agreed that insertion-order would be the most intuitive. However, this would come with a big potential caveat around circuit storing and reloading. For example, assume you build the circuit

a, b = Parameter("a"), Parameter("b")
circuit = QuantumCircuit(2)
circuit.rx(a, 0)
circuit.ry(b, 1)

then the order would be a - b.

Problem number 1 is converting this circuit to DAG representation and back. As there is no unique ordering of these gates and adding them in any order leads to the same circuit, converted circuit could have inserted the gates in the opposite order. This problem could be fixed by explicilty storing the parameters.

Problem 2 is essentially the same, but worse: Assume you get a dumped circuit in e.g. QPY format, which after loading prints as

     ┌───────┐
q_0: ┤ Rx(a) ├
     ├───────┤
q_1: ┤ Ry(b) ├
     └───────┘

While the order a - b seems intuitive, there is no guarantee it was actually saved as such.

Sorting alphabetically resolves these issues as it is always unique. It's a bit less intuitive, but safer. This is something we should clearly point out in the linked tutorial, maybe along with some general "parameter gymnastics" that users might have to do 🙂

nonhermitian commented 1 year ago

I think this all gets solved if the order is maintained somewhere. The case of single parameters is a bit artificial in my mind, and more useful scenarios are basically storing the order that multiple vectors of parameters are appended to the circuit.

jakelishman commented 1 year ago

Copying in some of my related comments from private Slack discussion:

"Insertion order" becomes especially tricky when two parameters are first encountered in an expression like a - b - there’s no easy way to determine the "insertion order" from that, so if we’re doing things magically, it’s best to have a pure order (i.e. one that depends only on the parameters themselves).

For compound parameterised circuits: these issues are part of why I’ve been pushing so heavily to move parameters from being internals of objects to being arguments managed by the CircuitInstruction context object. One reason is memory usage, but the other major reason is that then we have probably defined an internal binding scope for parameters. At the moment, there’s a bunch of hacks throughout Qiskit surrounding the Parameter object to attempt to deal with the fact that it inherently must live in a completely flat global namespace that’s shared between all circuits, since there’s no concept of scoping.

Let’s say I construct a circuit like this:

from qiskit.circuit import QuantumCircuit, Parameter
a = Parameter("a")
b = Parameter("b")
qc = QuantumCirucit(1)
qc.rz(b + a, 0)

Which of b or a got inserted first?

Technically it’s possible to track this all the way through ParameterExpression, but I worry about increasing the memory usage of that, and insert-related things always open up lots of places for fragility in the order getting lost.

jakelishman commented 1 year ago

For the root issue: my intention (which I need to write up in design docs ahead of the next release) is that new-style runtime classical variables will be declared as scoped resources, analogous to ClassicalRegister now. When combined with the move of parameters from being internal state of the inner scope objects, that will mean that there is both a clear and obvious order defined for parameters (declaration order), and also that there'll be scopes between different objects of the same type, so the created EfficientSU2 will just declare itself as taking some arguments by which it defines itself, and it won't need to care if those names clash with names in the outer circuit, because they'll be in inner scopes.

I'll write up more details, and a comment on how circuits will cope with declaring closure variables, etc, in a design doc.