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

Update gate.control() to return AnnotatedOperation #11433

Merged
merged 11 commits into from Jan 31, 2024

Conversation

alexanderivrii
Copy link
Contributor

@alexanderivrii alexanderivrii commented Dec 19, 2023

Summary

Partially resolves #11082.

This PR adds a new argument annotated to all the "control" methods, that is to QuantumCircuit.control(), Gate.control() and all of the subclasses of Gate, for example to SwapGate.control() and UnitaryGate.control().

When annotated is False (which is the default), the behavior is unchanged, making the change backward-compatible. As an example, SwapGate().control(1, annotated=False) returns an object of type CSwapGate (or, more precisely, a singleton version of that), while SwapGate().control(2, annotated=False) returns an object of type ControlledGate.

When annotated is True, the control method is allowed to return an AnnotatedOperation instead, leading to a more abstract representation that avoids eagerly constructing the gate's definition and allows potential "high-level" transpiler optimizations. I am saying "is allowed" because in practice there may be cases where we wouldn't want to introduce this abstraction, for example we may decide that a singly-controlled XGate should always be a CXGate and not an annotated operation. This will make even more sense when we try to similarly extend the power and inverse methods: it makes no sense to say that the inverse of a CXGate is an annotated operation and not just a CXGate.

The plans are to follow this PR in two directions.

First, we want to introduce similar capability for inverse and power methods, however each of these comes with its own set of headaches, so I have limited this PR only to control.

Second, we want to identify where in Qiskit it's beneficial to call control() with anotated=True. I believe it would be useful to do so in various circuit-constructing utilities (for example constructing controlled QFT-based adders), but there could be more opportunities.

Feedback regarding the approach is welcome. I am explicitly tagging @jakelishman and @mtreinish (especially that this is based on Matthew's offline suggestion).

@qiskit-bot
Copy link
Collaborator

One or more of the the following people are requested to review this:

  • @Cryoris
  • @Qiskit/terra-core
  • @ajavadia

@coveralls
Copy link

coveralls commented Dec 20, 2023

Pull Request Test Coverage Report for Build 7730318921

Warning: This coverage report may be inaccurate.

We've detected an issue with your CI configuration that might affect the accuracy of this pull request's coverage report.
To ensure accuracy in future PRs, please see these guidelines.
A quick fix for this PR: rebase it; your next report should be accurate.

  • -14 of 113 (87.61%) changed or added relevant lines in 19 files are covered.
  • 22 unchanged lines in 5 files lost coverage.
  • Overall coverage increased (+0.007%) to 89.588%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/circuit/library/generalized_gates/unitary.py 7 8 87.5%
qiskit/circuit/library/standard_gates/u1.py 13 14 92.86%
qiskit/transpiler/passes/synthesis/high_level_synthesis.py 3 5 60.0%
qiskit/circuit/annotated_operation.py 2 5 40.0%
qiskit/circuit/library/standard_gates/x.py 26 29 89.66%
qiskit/circuit/library/generalized_gates/mcmt.py 1 5 20.0%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 2 91.94%
qiskit/primitives/containers/estimator_pub.py 2 97.47%
qiskit/primitives/containers/sampler_pub.py 2 96.92%
crates/qasm2/src/parse.rs 6 97.62%
qiskit/primitives/containers/bindings_array.py 10 92.31%
Totals Coverage Status
Change from base Build 7728099338: 0.007%
Covered Lines: 59358
Relevant Lines: 66257

💛 - Coveralls

@alexanderivrii alexanderivrii changed the title [WIP] Update gate.control() to return AnnotatedOperation Update gate.control() to return AnnotatedOperation Dec 25, 2023
@1ucian0 1ucian0 added this to the 1.0.0 milestone Jan 11, 2024
Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this looks good. I've only done a high level review, quick skim over the code so far. But before I dive in line by line I had a couple of high level questions:

  • Do you think there is a time in the future where we want to have annotated=True by default?
  • For concrete gates that have a defined controlled variant (e.g. X -> CX) is there an advantage to calling XGate().control(annotated=True)? I assume this opens up opportunities for optimization by having things defined abstractly with AnnotatedOperation.

I'm just wondering if there are potentially different defaults we want here as a breaking change for the 1.0.0 release? Like I'm wondering if doing the base Gate.control default to annotated=True makes more sense. From a backwards compatibility PoV what you have here is better, but I'm just curious of your thoughts on the longer term plan with this.

@alexanderivrii
Copy link
Contributor Author

These are really great questions.

Longer-term I would indeed like to see Gate.control, QuantumCircuit.control and UnitaryGate.control return AnnotatedOperations by default.

I have now run local tests to see what would break if we were to set annotated=True in the three methods above, and there are 76 failing tests (a bit more than I have hoped). On a quick inspection:

  • Some of these are related to QPY and should hopefully be fixed by Fix qpy support for Annotated Operations #11505.
  • Some of these are related to visualization. Most of the visualization problems were solved in Add drawer support for AnnotatedOperations #11202 and Fix text drawer AnnotatedOperations #11466, but apparently there are still some corner-cases remaining.
  • Many of these are related to testing controlled gates, and fail because the returned object has no definition, label, inverse or is not an instance of ControlledGate. Though the inverse would be fixed by the follow-up PR on inverse gates. A simple fix would be to change the tests for controlled gates that explicitly mention label, definition or inverse to call control with annotated=False.
  • And there might be a few more problems (out of these 76 ones). I will take a deeper look.

I am up to making this breaking change for Qiskit 1.0 provided that we can quickly resolve all the problems above.

Regarding controlled gates with defined controlled variants. At the moment I can't think of a convincing use-case when having an X-gate with a control modifier representing a single control might be better than just having a CX-gate (or an MCX-gate in case of multiple controls), however I do agree that this possibly opens a door to some optimizations.

Update: after thinking about the above, I think that always returning a CX/MCX-gate for X.control() might indeed be a better option, as this does not prevent the possibility of a transpiler pass that translates MCX-gates to AnnotatedGates (if the goal is to maybe combine gates with similar control modifiers).

@alexanderivrii
Copy link
Contributor Author

I have taken a close look at all tests that would fail if we changed the default to control(annotated=True).

As I wrote before, most of the failing tests pertain to testing controlled gates, as annotated operations do not have methods label or definition. These tests would be easy to adapt.

There are failing tests related to the Unroller pass as it does not support circuits with AnnotatedOperations (as these have no definition), however the pass is being deprecated, so these tests can probably be simply removed.

There are some failing visualization tests: thanks to #11202 the annotated operations with control modifiers are printed very nicely and quite similarly to controlled gates, but still there are some minor differences in circuit_test_drawer: the following code

ghz_circuit = QuantumCircuit(3, name="ghz")
ghz_circuit.h(0)
ghz_circuit.cx(0, 1)
ghz_circuit.cx(1, 2)
ghz = ghz_circuit.to_gate()
cghz = ghz.control(1, ctrl_state="0")
circuit = QuantumCircuit(4)
circuit.append(cghz, [3, 1, 0, 2])

used to print

     ┌──────┐
q_0: ┤1     ├
     │      │
q_1: ┤0 ghz ├
     │      │
q_2: ┤2     ├
     └──┬───┘
q_3: ───o────

and now it's exactly the same picture except that the first letter in ghz is capitalized. There are also some difference with (not) printing parameters of the base operation.

The two remaining failing tests are test.python.circuit.test_controlled_gate.TestControlledGate.test_assign_nested_controlled_cu and test.python.circuit.test_circuit_operations.TestCircuitOperations.test_control and they point to a real problem for annotated gates over parameterized gates (and I now recall that @jakelishman mentioned at some point that this may be an issue). For example the following code

theta = Parameter("t")
qc_c = QuantumCircuit(2)
qc_c.crx(theta, 1, 0)
custom_gate = qc_c.to_gate().control()
qc = QuantumCircuit(3)
qc.append(custom_gate, [0, 1, 2])
assigned = qc.assign_parameters({theta: 0.5})

does not currently assign parameters to the base operation of an annotated operation.

mtreinish
mtreinish previously approved these changes Jan 30, 2024
Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this LGTM, I think you're right that even with a potentially breaking change in qiskit 1.0 the limitations around AnnotatedOperation (primarily the QPY support still needed) is enough of a reason to be conservative here and default to the existing behavior. We can revisit this for qiskit 2.0.0 if we want to potentially move everything to be a annotated operation.

I left a few inline comments but nothing major and I'm fine merging this as is.

num_ctrl_qubits: int = 1,
label: str | None = None,
ctrl_state: int | str | None = None,
annotated: bool = False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be?

Suggested change
annotated: bool = False,
annotated: bool = True,

Not that it matters much, but we're always returning an annotated op here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I have also simplified the returned gate: instead of returning a control-annotated operation on top of the current annotated operation, we return an annotated operation with the current set of modifiers extended with the new control modifiers.

label: str | None = None,
ctrl_state: int | str | None = None,
annotated: bool = False,
):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Suggested change
):
) -> AnnotatedOperation:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 148 to 158
if not annotated:
if ctrl_state is None: # TODO add ctrl state implementation by adding X gates
gate = MCMT(
self.gate, self.num_ctrl_qubits + num_ctrl_qubits, self.num_target_qubits
)
else:
gate = super().control(num_ctrl_qubits, label, ctrl_state, annotated=annotated)
else:
gate = AnnotatedOperation(
self, ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably have done to save some code and reduce duplication:

Suggested change
if not annotated:
if ctrl_state is None: # TODO add ctrl state implementation by adding X gates
gate = MCMT(
self.gate, self.num_ctrl_qubits + num_ctrl_qubits, self.num_target_qubits
)
else:
gate = super().control(num_ctrl_qubits, label, ctrl_state, annotated=annotated)
else:
gate = AnnotatedOperation(
self, ControlModifier(num_ctrl_qubits=num_ctrl_qubits, ctrl_state=ctrl_state)
)
if not annotated and ctrl_state is None:
gate = MCMT(
self.gate, self.num_ctrl_qubits + num_ctrl_qubits, self.num_target_qubits
)
else:
gate = super().control(num_ctrl_qubits, label, ctrl_state, annotated=annotated)

I think this applies pretty much everwhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I absolutely love this suggestion. Not only this makes the code shorter and more readable, it also seems a more correct way to apply the control method. Changed across the board in 0783fbb.

Copy link
Member

@mtreinish mtreinish 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 the updates

@mtreinish mtreinish added this pull request to the merge queue Jan 31, 2024
Merged via the queue into Qiskit:main with commit 0364786 Jan 31, 2024
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Update Gate.control method to return AnnotatedOperation
5 participants