Skip to content
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

Merged
merged 49 commits into from
Mar 25, 2022

Conversation

javabster
Copy link
Contributor

@javabster javabster commented Feb 15, 2022

Summary

fixes #6246

Included in this PR:

  • State preparation code (minus the resets) has been moved from Initialize to a new StatePreparation class
  • Initialize now does the reset step and then appends a StatePreparation instruction
  • Static method get_num_qubits added to StatePreparation class
  • New tests added to check initialise decomposition to reset + stateprep

Notes

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

@javabster javabster requested a review from a team as a code owner February 15, 2022 22:39
@coveralls
Copy link

coveralls commented Feb 15, 2022

Pull Request Test Coverage Report for Build 2042467059

  • 196 of 205 (95.61%) changed or added relevant lines in 4 files are covered.
  • 3 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+0.006%) to 83.719%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/extensions/quantum_initializer/initializer.py 14 15 93.33%
qiskit/circuit/library/data_preparation/state_preparation.py 179 187 95.72%
Files with Coverage Reduction New Missed Lines %
qiskit/pulse/library/waveform.py 3 89.36%
Totals Coverage Status
Change from base Build 2042159833: 0.006%
Covered Lines: 53014
Relevant Lines: 63324

💛 - Coveralls

qiskit/extensions/quantum_initializer/initializer.py Outdated Show resolved Hide resolved
"""
# call to generate the circuit that takes the desired vector to zero
disentangling_circuit = self.gates_to_uncompute()
self._stateprep = StatePreparation(params, num_qubits)
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

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 🤷‍♀️

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

qiskit/extensions/quantum_initializer/initializer.py Outdated Show resolved Hide resolved
qiskit/extensions/quantum_initializer/initializer.py Outdated Show resolved Hide resolved
qiskit/extensions/quantum_initializer/initializer.py Outdated Show resolved Hide resolved
Comment on lines 377 to 379
qubits (QuantumRegister or int):
* QuantumRegister: A list of qubits to be initialized [Default: None].
* int: Index of qubit to be initialized [Default: None].
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Comment on lines 363 to 364
def prepare_state(self, state, qubits=None):
r"""Prpare qubits in a specific state.
Copy link
Member

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.

Comment on lines 30 to 38
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
Copy link
Member

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

Copy link
Contributor

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

Copy link
Member

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.

Comment on lines 450 to 467
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")
Copy link
Member

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.

Comment on lines 456 to 462
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")
Copy link
Member

@jakelishman jakelishman Feb 16, 2022

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?

Copy link
Contributor Author

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

Copy link
Member

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

qiskit/extensions/quantum_initializer/state_preparation.py Outdated Show resolved Hide resolved
Comment on lines 30 to 38
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
Copy link
Contributor

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

qiskit/extensions/quantum_initializer/state_preparation.py Outdated Show resolved Hide resolved
test/python/circuit/test_initializer.py Outdated Show resolved Hide resolved
actual_sv = Statevector.from_instruction(qc)
self.assertTrue(desired_sv == actual_sv)

def test_decompose_with_int(self):
Copy link
Contributor

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?

@javabster
Copy link
Contributor Author

docs are looking better now thanks @jakelishman 🎉

Screenshot 2022-02-22 at 6 07 45 PM

Screenshot 2022-02-22 at 6 08 19 PM

Copy link
Contributor

@Cryoris Cryoris left a 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):
Copy link
Contributor

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? 🙂

jakelishman
jakelishman previously approved these changes Mar 25, 2022
Copy link
Member

@jakelishman jakelishman left a 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
Copy link
Member

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

Copy link
Contributor

@Cryoris Cryoris left a 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.

Cryoris
Cryoris previously approved these changes Mar 25, 2022
Copy link
Contributor

@Cryoris Cryoris left a 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!

@jakelishman jakelishman added Changelog: New Feature Include in the "Added" section of the changelog automerge labels Mar 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Initialize should be composed of Resets + StatePreparation
4 participants