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

Adding QFT gate to natively reason about Quantum Fourier Transforms #11463

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

alexanderivrii
Copy link
Contributor

@alexanderivrii alexanderivrii commented Dec 28, 2023

Summary

A new "high-level" gate QftGate allows to natively add QFTs on a quantum circuit and, in particular, to defer their synthesis to the transpiler. The gates will be synthesized by the HighLevelSynthesis transpiler pass, using one of several available plugins.

A new function synth_qft_full(num_qubits, do_swaps, approximation_degree, insert_barriers, inverse, name) allows to synthesize a QFT circuit with the given number of qubits. For now the implementation is taken directly from the _build method of the existing QFT (blueprint) circuit class. The name of the method reflects that the synthesized circuit requires full connectivity. Note that another QFT synthesis method has been recently added in #11236, with the synthesized circuit following only linear (aka nearest-neighbor) connectivity.

The two methods above are wrapped into HighLevelSynthesis plugins QftSynthesisFull and QftSynthesisLine respectively. The default synthesis is set to QftSynthesisFull, thus fully coinciding with the circuits produced using the existing QFT circuit class.

In a follow-up PR I am going to replace the construction of QFT circuits by QFT gates whenever possible, such as for example when constructing QFT-based adders and multipliers.

Details and Comments

The constructor in the original QFT circuit class includes arguments num_qubits, approximation_degree, do_swaps, inverse, insert_barriers and name. Here approximation_degree allows to ignore small controlled-phase rotations, this leading to an approximate but more efficient circuit, and do_swaps allows to ignore SWAP gates at the end of the circuit. An important decision is which of the arguments above should be a part of the definition of a QftGate, and which should be parameters to its synthesis method. In this PR only num_qubits is part of the QftGate, while the rest are parameters to the synthesis method. For instance, I strongly believe that whether to include barriers in the synthesized circuit and which name to assign to the synthesized circuit have nothing to do with the definition of the QftGate. I am also inclined to think that approximation_degree and do_swaps should be synthesis parameters, i.e. instead of talking about an approximate swap-reduced QFT-gate, we are talking about a QFT-gate that can be synthesized ignoring small control-phase rotations and final swaps, especially that deciding which gates in the synthesized circuit can be ignored heavily depends on the synthesis method itself. What, however, is missing is incorporating the knowledge that in some cases in the bigger circuit we have inverse QFT-gate -- some gate U -- QFT-gate is equivalent to inverse swap-reduced QFT-gate -- some gate U -- swap-reduced QFT-gate, that is it is safe to drop swaps for both QFT gates. This is important as it leads to a smaller overall circuit, however for this to be correct both QFT gates should be synthesized in the same way. And we currently don't have high-level-synthesis API to ensure this. Alternatively we could add do_swaps to the gate definition, however I don't really like this solution, and it also does not solve the problem: in theory HighLevelSynthesis can choose different QFT-synthesis algorithms for both gates, again leading to incorrect results when ignoring swap gates. Thoughts and suggestions are welcome.

@qiskit-bot
Copy link
Collaborator

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

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

@coveralls
Copy link

coveralls commented Dec 28, 2023

Pull Request Test Coverage Report for Build 8922516018

Details

  • 71 of 73 (97.26%) changed or added relevant lines in 6 files are covered.
  • 4 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+0.03%) to 89.568%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/transpiler/passes/synthesis/high_level_synthesis.py 21 23 91.3%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 4 92.37%
Totals Coverage Status
Change from base Build 8917584107: 0.03%
Covered Lines: 61687
Relevant Lines: 68872

💛 - Coveralls

@jakelishman jakelishman added the mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library label Jan 3, 2024
@jakelishman jakelishman added the Changelog: New Feature Include in the "Added" section of the changelog label Jan 3, 2024
@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Jan 5, 2024

I had a discussion with @ShellyGarion and a discussion with @Cryoris on whether do_swaps should be a part of the definition of the QFT gate:

In the original QFT synthesis algorithm for all-to-all connectivity, the synthesized QFT circuit has a layer of swap gates at the end. These swap gates correspond to a very specific reversal permutation. Other circuits that use QFT as a building block, in many cases use this special knowledge and avoid explicitly implementing the reversal implementation with swaps when the reversal can be achieved in a different way. For instance, for circuits that have both QFT and its inverse, the two reversal permutations cancel out and hence do not need to be implemented at all. (Note that the name do_swaps is misleading: we are not talking about dropping any permutation at the end of the circuit, but about a very specific reversal permutation.) Note that other QFT synthesis algorithms (such as the one implemented in #11236) do not require the layer of SWAP gates at all.

Since the goal is to make sure that the QFT-based application circuits have count/depth as small as possible, we should investigate if in all cases the transpiler is able to automatically optimize away the reversal permutations if they were to appear in the circuits. If this is the case, then we can have the canonical QFT gate (and we do not need to change the implementation in this PR). If this is not the case, then we may want to add to a QFT gate the argument reversed_bits (or some better name): when True the gate corresponds to the "reversed QFT" (which also has precise and clear semantics). The application circuits would then be able to pick the form of the gate best suited for the application, and the synthesis plugin would need to handle the "reverse_bits" argument correctly.

@jakelishman
Copy link
Member

imo, a Gate object should represent exactly something that could be a hardware instruction, which means they should have very limited semantics, or it'll not be useful as an element. If we want to keep the reasoning at a higher abstract level in the Qiskit sense, then it should be an implementor of Operation but not Instruction - this signifies that the semantics are local to Qiskit and its compiler, and then we can extend the semantics as needed.

In other words, if it's going to be a Gate, then QFTGate should represent exactly the operation of the QFT matrix on virtual qubits, without any swaps. There shouldn't be any possibility of a do_swaps argument on the Gate object, because a Gate is a representation of a hardware operation and not of a synthesis, just like a HGate represents exactly the operation of the Hadamard matrix on qubits. It's up to a synthesis routine to decide how to insert swaps when including a QFT operation as part of a larger circuit. The concept of backend connectivity, instruction set, or surrounding operations should not have any effect on an Instruction, so a do_swaps argument shouldn't exist.


With the advent of Operation, and the move towards optimisation passes based in mathematical abstractions before we hit the gate-level requirements, I'd consider making an abstract QFT representative object that implements Operation and not Instruction, much like Clifford does. It could always be converted to a Gate later, but I'd think we might want some indication of some hardware that has a QFT primitive instruction to motivate that - as I understand, all the goals from the synthesis side are to use this as a high level object, which needn't (and imo shouldn't, but I don't feel too strongly) be a Gate.

@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Jan 10, 2024

Jake, thanks for the feedback.

In other words, if it's going to be a Gate, then QFTGate should represent exactly the operation of the QFT matrix on virtual qubits, without any swaps.

I completely agree with this, and this is how it's implemented right now. Though I still have not checked if we get the same or worse results when we substitute QFT circuits (with do_swaps=False) by QFT gates in various application circuits that use QFTs as building blocks.

Note: to avoid possible confusion, for the QFT circuit to faithfully implement the QFT matrix using the default implementation in QFT._build, the circuit must contain the reversal permutation (implemented as a layer of swaps). So what the sneaky application circuit developers did is to start using "bit-reversed QFT circuits" for building larger circuits (as these bit-reversed circuits have a more efficient implementation without that swap layer, in code this corresponds to do_swaps=False). If the Qiskit transpiler is not able to automatically remove these layers of swap gates, then we may want to provide both QFT gate and bit-reversed QFT gate , and in practice we can have one to be defined as QftGate(bit_reversed=False) and the other as QftGate(bit_reversed=True). Both have well-defined matrix semantics.


In theory I agree with you that things like QftGate should be Operations and not Instructions, but in practice we have been inheriting from Gate/Instruction whenever possible, for instance both PermutationGate and LinearFunction are gates. This automatically takes care of a large number of annoying problems that we don't fully support at the level of Operations: including drawing support, QPY, etc. Specifically about QftGates, I would like to drop-in-replace QFT circuits by QFT gates in larger circuits provided by our circuit library, and I would like this change to be backwards-compatible. Maybe I am overthinking it, but I am afraid that something on the user side would break if QftGates became non-Instructions. (Well, I also want to replace inverse-QFT-gates by annotated operations, which kind of contradicts to what I just wrote).

@mtreinish mtreinish modified the milestones: 1.0.0, 1.1.0 Jan 23, 2024
@alexanderivrii
Copy link
Contributor Author

alexanderivrii commented Feb 6, 2024

An update on how QFT-subcircuits are used within Qiskit's circuit library (thanks to @ShellyGarion and @Cryoris for all the help).

We have 4 circuits that use QFT as a building block: DraperQFTAdder, RGQFTMultiplier, PhaseEstimation, and QuadraticForm.

In each of these cases, the main circuit uses a sneaky optimization of using not the proper QFT, but the QFT with the argument do_swaps=False, which corresponds to QFT with the reversal permutation of its qubits. As a side-note, the main circuit has been also modified, so that the final operator is indeed correct. And to be pedantic, the illustrations like the one appearing for DraperQFTAdder in draper_qft_adder.py are a bit misleading since in the illustration it's not really the QFT/IQFT pair, but the QFT-with-reversal/IQFT-with-reversal pair:

a_0: ─────────■──────■────────■───────────────────────────────────────
              │      │        │                                       
a_1: ─────────┼──────┼────────┼────────■──────■───────────────────────
              │      │        │        │      │                       
a_2: ─────────┼──────┼────────┼────────┼──────┼────────■──────────────
     ┌──────┐ │P(π)  │        │        │      │        │     ┌───────┐
b_0: ┤0     ├─■──────┼────────┼────────┼──────┼────────┼─────┤0      ├
     │      │        │P(π/2)  │        │P(π)  │        │     │       │
b_1: ┤1 QFT ├────────■────────┼────────■──────┼────────┼─────┤1 IQFT ├
     │      │                 │P(π/4)         │P(π/2)  │P(π) │       │
b_2: ┤2     ├─────────────────■───────────────■────────■─────┤2      ├
     └──────┘                                                └───────┘

The optimization (removing the final reversal permutation and modifying the rest of the circuit accordingly) makes sense for he default QFT synthesis (targeting all-to-all connectivity), as it introduces the layer of swaps (aka the reversal permutation) at the end of the circuit, so the "QFT-with-reversal" is the simple circuit without this layer of SWAPs. Note however that when transpiling for general architectures, routing may insert many additional SWAPs.

But the optimization no longer makes sense for other QFT synthesis algorithms, such as the one targeting the linear-nearest neighbor connectivity, since it does not introduce a reversal permutation at the end of the circuit. Hence, for the DraperQFTAdder above it would make more sense to use the "direct" QFT/IQFT gates and not the "reversed" ones. (It would probably make even more sense to think on how to transpile the full circuit including the many CP gates in the middle, but that's not the main point I am trying to make).

In addition, it is easy to check that if in the DrapperQFTAdder above was implemented with the direct versions of QFT and IQFT (that is with the reversal permutations + other required changes), then the ElidePermutations pass would be able to remove them (essentially producing the circuit above). The same is true for the other 3 application circuits mentioned (in all of the cases the reversal permutations come in pairs; e.g. for PhaseEstimation one reversal comes from do_swaps=False and the second - from reverse_bits).

The bottom line of the discussion is that we don't really need to have the "reversed" version of QFT for the transpiler to produce optimal circuits, as long as we can properly use the ElidePermutations pass. Without using ElidePermutations there is no simple way to get rid of the extra swaps.

Hence it probably makes sense to have the new QftGate not include additional arguments (like do_swaps), corresponding to the mathematically correct QFT operator.

Though, some thought is required on how to properly combine the HighLevelSynthesis and ElidePermutations passes and recursion to enable the swap cancellation above, for instance for controlled QFT-adders, where the QFTs appear as subcircuits within the definitions of more complex gates.

@@ -206,6 +206,7 @@
UCRXGate
UCRYGate
UCRZGate
QftGate
Copy link
Contributor

Choose a reason for hiding this comment

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

Side comment: I think this should be QFT since "Qft" is not a word (compare e.g. RVGate vs. PauliGate)

"""
super().__init__(name="qft", num_qubits=num_qubits, params=[])

def __array__(self, dtype=complex):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I would support this as users might run into exponential scalings. Since this is mostly a placeholder for synthesizing an object, how about disabling this method and referring users to call Operator of the synthesized circuit?

Copy link
Member

Choose a reason for hiding this comment

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

Gate supplies a to_matrix method that just calls this, so we kind of need to supply it - there is a defined matrix for the gate.

@Cryoris
Copy link
Contributor

Cryoris commented Feb 28, 2024

I agree with the final goal of having this be an Operation and move towards a clear separation of synthesis-based objects (Clifford, Operator, ...) vs. fundamental gates (like the standard gates). As @alexanderivrii mentioned above, however, this does not only affect the QFT but also e.g. the LinearFunction or the PauliEvolutionGate. To enable an efficient QFT synthesis I would therefore think it is fine to move along as QFTGate before properly thinking synthesis objects through and changing it for all.

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.

Thanks Sasha, this is looking good. For the purposes of ElideSwaps and things like that, maybe a route along the lines of letting synthesis routines synthesise up to a permutation, then outputting a permutation gate as well? That way ElideSwaps can just pick that up and remove it, or the behaviour can generally be controlled.

"""
super().__init__(name="qft", num_qubits=num_qubits, params=[])

def __array__(self, dtype=complex):
Copy link
Member

Choose a reason for hiding this comment

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

Gate supplies a to_matrix method that just calls this, so we kind of need to supply it - there is a defined matrix for the gate.

Comment on lines +44 to +54
num_qubits = self.num_qubits
mat = np.empty((2**num_qubits, 2**num_qubits), dtype=dtype)
for i in range(2**num_qubits):
i_index = int(bin(i)[2:].zfill(num_qubits), 2)
for j in range(i, 2**num_qubits):
entry = np.exp(2 * np.pi * 1j * i * j / 2**num_qubits) / 2 ** (num_qubits / 2)
j_index = int(bin(j)[2:].zfill(num_qubits), 2)
mat[i_index, j_index] = entry
if i != j:
mat[j_index, i_index] = entry
return mat
Copy link
Member

Choose a reason for hiding this comment

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

Something like

def qft(n):
    pows = np.arange(2**n)
    return np.exp(2j * np.pi / n * np.outer(pows, pows)) * (0.5 ** (n/2))

is probably a fair bit faster than this. I think there might be some nicer np.power.outer tricks that might be faster, but it probably doesn't matter too much at the scale of matrices that we can generate.

@@ -661,6 +666,90 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **
return decomposition


class QftSynthesisFull(HighLevelSynthesisPlugin):
Copy link
Member

Choose a reason for hiding this comment

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

Similar to Julien's comment about QFTGate being preferable (it's the general Qiskit style and [not that this matters too much] the pep8 recommendation), this guy and the other plugin could be QFTSynthesisFull/QFTSynthesisLine.

Comment on lines +56 to +72
def _basic_decomposition(self):
"""Provide a specific decomposition of the QFT gate into a quantum circuit.

Returns:
QuantumCircuit: A circuit implementing the evolution.
"""
from qiskit.synthesis.qft import synth_qft_full

decomposition = synth_qft_full(num_qubits=self.num_qubits)
return decomposition

def _define(self):
"""Populate self.definition with a specific decomposition of the gate.
This is used for constructing Operators from QftGates, creating qasm
representations and more.
"""
self.definition = self._basic_decomposition()
Copy link
Member

Choose a reason for hiding this comment

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

Very minor nitpicking, but is there a need to have a separate _basic_decomposition function? Can we inline it into _define (which largely implies that it's a basic definition)?

Copy link
Member

@ShellyGarion ShellyGarion left a comment

Choose a reason for hiding this comment

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

Nice contribution @alexanderivrii. I have a few comments and suggestions.

  • Do we plan to deprecate QFT in favor of QFTGate in Qiskit 2.0 ?

  • You mentioned the use-case of adders and circuits containing QFT and QFT-inverse, do you have some test for it? or will it appear in a future PR?


Args:
num_qubits: The number of qubits on which the QFT acts.
do_swaps: Whether to include the final swaps in the QFT.
Copy link
Member

Choose a reason for hiding this comment

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

perhaps it's worth to explain this parameter better, and that it actually means "reverse qubits"
(also in qft_decompose_lnn function)


The plugin supports the following additional options:

* do_swaps (bool): Whether to include Swap gates at the end of the synthesized
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 that maybe "do swaps" can be confusing, and should be replaced by something like "reverse_qubits"?
also "cna" --> "can"


The plugin supports the following additional options:

* do_swaps (bool): whether to include Swap gates at the end of the synthesized
Copy link
Member

Choose a reason for hiding this comment

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

again, perhaps it's worth to change it to something like "reverse_qubits" ?
also: cna --> can

It is impossible ti implement the QFT approximately by ignoring
controlled-phase rotations with the angle is beneath a threshold. This is discussed
in more detail in https://arxiv.org/abs/quant-ph/9601018 or
https://arxiv.org/abs/quant-ph/0403071.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add them as references?

It is impossible ti implement the QFT approximately by ignoring
controlled-phase rotations with the angle is beneath a threshold. This is discussed
in more detail in https://arxiv.org/abs/quant-ph/9601018 or
https://arxiv.org/abs/quant-ph/0403071.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add them as references?

@mtreinish mtreinish modified the milestones: 1.1.0, 1.2.0 May 2, 2024
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 mod: circuit Related to the core of the `QuantumCircuit` class or the circuit library synthesis
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants