-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Separate State Preparation from Initialize #7666
Conversation
Pull Request Test Coverage Report for Build 2042467059
💛 - Coveralls |
""" | ||
# call to generate the circuit that takes the desired vector to zero | ||
disentangling_circuit = self.gates_to_uncompute() | ||
self._stateprep = StatePreparation(params, num_qubits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This stores the params
inside another object. How well (relative to the current Initialize
) will this handle a user mutating Instruction.params
from under us once the instruction has been initialised?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you give me a more specific example? I didn't explicitly check this but the params handling should just be the same if I moved everything over correctly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't do any testing, but I was thinking that I could potentially do something pathological like
from qiskit.extensions import Initialize
import numpy as np
init = Initialize(np.array([1, 1]) * np.sqrt(0.5))
init.params = np.array([1, -1]) * np.sqrt(0.5)
and bad things would probably happen - in the new form, the params
won't propagate through to StatePreparation
. That said, it's highly likely that Initialize
is already broken with respect to mutation of params
in some form or another, and if so, it's not really your responsibility to fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running this example didn't throw any errors for me so either nothing bad is happening or bad things are happening silently 🤷♀️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would assume this just does nothing, which is probably not the right behavior. I think there are two solutions to this: (1) either we add a params
setter/getter which forwards the values to self._stateprep
or (2) only create the StatePreparation
within _define
.
(1) couples Initialize
and StatePrep
very strongly but they are also almost the same so maybe that's the easier solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't throw an error just on the code I wrote, but I was thinking about things that depend on params
. Things like how init.definition
behaves - both before and after this change, will it do the right thing if the parameters are mutated between object creation and first definition? My guess is that in the original code it will, and the new code it won't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah ok I understand what you mean now. I just checked with mutating params and the Initialize
instruction params get updated but the StatePreparation
doesn't.
I think I prefer option 1 from @Cryoris as we need to call self._stateprep.num_qubits
to get the constructor to work properly in Initialize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setter/getter is the best way to handle things at the moment, probably. It's still horribly broken (if I did init.params[2] = -init.params[2]
, it'd exhibit the same behaviour as it does now, even with a setter), but that's a problem that's fundamental to the way we treat params
across the board in Terra, and which we hope to improve in the push for new classical instructions.
qubits (QuantumRegister or int): | ||
* QuantumRegister: A list of qubits to be initialized [Default: None]. | ||
* int: Index of qubit to be initialized [Default: None]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if it's None
? The type hint is also inaccurate, since None
is a valid input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how best to document this. If the number of qubits in the circ matches exactly the number needed to append the initialize instruction then the user doesn't have to pass anything for this arg. But if the circ is longer they do need to pass this arg to specify which qubits to append the instruction to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think some form of the explanation you wrote is fine. Like
If given, a sequence of qubits or qubit specifiers to perform the initialisation on. If not given or
None
, then the initialisation will be applied to all the qubits in the circuit.
def prepare_state(self, state, qubits=None): | ||
r"""Prpare qubits in a specific state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This docstring (and similar for Initialize
) could probably do with referencing each other, and explaining the difference between them.
class StatePreparation(Instruction): | ||
"""Complex amplitude state preparation. | ||
|
||
Class that implements the (complex amplitude) state preparation of some | ||
flexible collection of qubit registers. | ||
""" | ||
|
||
def __init__(self, params, num_qubits=None): | ||
"""Prepare state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class should at least explain that it's only correct state prep from an initial state of |00...0>
, but it might even be better (and true to the original issue) if it accepted two possible arguments: start_state
and end_state
, where start_state
defaults to |00...0>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could leave specifying a start_state
for a different PR since that's a new feature and this merely splits Initialize
into two 😄
But this definitely should make clear that it only works if you start from |0...>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to leave start_state
for another PR.
For Abby (via slack): the idea is that the current algorithm implements a gate sequence that takes you from |00...0>
to the state you specified, so it's only correct state prep if the relevant registers start off in |00...0>
. That wasn't an issue when it wasn't split, because Initialize
called reset
on the qubits, so it always started in |0>
.
It's quite easy to extend this class to work from an arbitrary starting state; you generate the |0> -> |\psi>
sequence for both start_state
and end_state
, and then just apply the adjoint of the start_state
sequence before the end_state
sequence, so you prep |initial> -> |0> -> |final>
. It's not necessarily especially efficient, but the transpiler may well synthesis it into something better later.
def test_decompose_with_int(self): | ||
"""Test initialize with int arg decomposes to a StatePreparation and reset""" | ||
qc = QuantumCircuit(2) | ||
qc.initialize(2) | ||
decom_circ = qc.decompose() | ||
decom_circ2 = decom_circ.decompose() | ||
dag1 = circuit_to_dag(decom_circ) | ||
dag2 = circuit_to_dag(decom_circ2) | ||
|
||
self.assertEqual(len(dag1.op_nodes()), 3) | ||
self.assertEqual(dag1.op_nodes()[0].name, "reset") | ||
self.assertEqual(dag1.op_nodes()[1].name, "reset") | ||
self.assertEqual(dag1.op_nodes()[2].name, "state_preparation") | ||
|
||
self.assertEqual(len(dag2.op_nodes()), 3) | ||
self.assertEqual(dag2.op_nodes()[0].name, "reset") | ||
self.assertEqual(dag2.op_nodes()[1].name, "reset") | ||
self.assertEqual(dag2.op_nodes()[2].name, "x") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't hurt to have at least one test that StatePreparation
does essentially the same thing, but without the initial Reset
.
dag1 = circuit_to_dag(decom_circ) | ||
dag2 = circuit_to_dag(decom_circ2) | ||
|
||
self.assertEqual(len(dag1.op_nodes()), 3) | ||
self.assertEqual(dag1.op_nodes()[0].name, "reset") | ||
self.assertEqual(dag1.op_nodes()[1].name, "reset") | ||
self.assertEqual(dag1.op_nodes()[2].name, "state_preparation") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to convert to a DAG
here? Can you compare based on QuantumCircuit.data
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i think it should work comparing qc.data
, is there a particular reason not to do the check by converting to a dag? I was mainly just following the style we use for other decompose tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason I mentioned it was because it looked odd to me, and I didn't immediately know the iteration order of DAGCircuit.op_nodes
- there's no required total ordering in a DAG (only a partial ordering, but you can make a total order by sequencing the wires, I think).
class StatePreparation(Instruction): | ||
"""Complex amplitude state preparation. | ||
|
||
Class that implements the (complex amplitude) state preparation of some | ||
flexible collection of qubit registers. | ||
""" | ||
|
||
def __init__(self, params, num_qubits=None): | ||
"""Prepare state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could leave specifying a start_state
for a different PR since that's a new feature and this merely splits Initialize
into two 😄
But this definitely should make clear that it only works if you start from |0...>
actual_sv = Statevector.from_instruction(qc) | ||
self.assertTrue(desired_sv == actual_sv) | ||
|
||
def test_decompose_with_int(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think these tests are necessary, they seem to check that this operation is not the same as Initialize
which is a very specific thing. Instead it might be better to test properties specific to StatePreparation
, e.g. if the initial state of the qubits is not 0, the final state is not correct 🙂
Or what happens if you call qc.stateprep
where the qubits
arguments and state
argument are incompatible?
docs are looking better now thanks @jakelishman 🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One last round of comments, then I think this is good to go!
): | ||
r""" | ||
Args: | ||
params (str, list, int or Statevector): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you remove the type hints from the docstring, now that they are in the signature? 🙂
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
Co-authored-by: Julien Gacon <gaconju@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I flicked through quickly and it looked fine to me. If Julien's happy with the changes since I last looked, this can be merged.
self._params_arg = params | ||
self._inverse = inverse | ||
self._name = "state_preparation_dg" if self._inverse else "state_preparation" | ||
self._label = f"{label} Dg" if self._inverse else label |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause StatePreparation().inverse().inverse()
to have a label "StatePreparation Dg Dg"
if you care about that sort of thing (I probably wouldn't bother).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're missing a reno and one test case, then we can merge from my side! The test case would be checking that the params as integer raises correctly if the number of qubits is (1) missing or (2) too small.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks for all the effort!
Summary
fixes #6246
Included in this PR:
Initialize
to a newStatePreparation
classInitialize
now does the reset step and then appends aStatePreparation
instructionget_num_qubits
added toStatePreparation
classNotes
This PR could provide a solution to this SE query: https://quantumcomputing.stackexchange.com/questions/24535/is-it-possible-to-create-controlled-instructions-in-qiskit