matt-lourens / hierarqcal

Generate hierarchical quantum circuits for Neural Architecture Search.
https://matt-lourens.github.io/hierarqcal/
BSD 3-Clause "New" or "Revised" License
43 stars 15 forks source link

Implement a N - qubit QFT with one line of code. #26

Closed matt-lourens closed 1 year ago

matt-lourens commented 1 year ago

Summary

The Quantum Fourier Transform has nice symmetry in it's circuit representation, as seen here from Nielsen and Chuang Chapter 5: image

This amends itself well to being implementable with HierarQcal. We will however need a new primitive operation called Qpivot which given a 2 qubit unitary, cycles though each qubit and connects it to one pivot qubit (pivot acts as target, more detail below). The gist of this issue is implementing the new Qpivot primitive and showcasing it's usage in the examples notebook with QFT, which I think we can do in essentially one line.

Example usage

It will go something like this:

N = 5
h_top = Qpivot(pattern="1*", mapping=u_h)
controlled_rs = Qpivot(pattern="1*", mapping=u_cr, share_weights=False)
hcr = h_top + controlled_rs
qft = Qinit(N)  + (hcr + Qmask("1*"))*(N)

The Qpivot primitive is very similar to the Qmask primitive so use that as a reference (see Qmask examples in quickstart.ipynb), and I'm happy to explain in more detail how Qmask works to make the development easier. Qpivot will receive a pattern string, where '1' indicates the pivot qubit and 0 the control. The star is a wild card which gets filled with '0''s based on the number of available qubits. 1* pivots to the top qubit, *1 to the bottom, *1* to the middle, 1*1*1 has 3 pivots which can be connected based on nearest neighbour or the normal cycle pattern, something similar is already implemented in Qmask which I can take you through. For one qubit unitaries (such as the h_top for the Hadamard) the unitary gets placed only on pivot qubits. You can ignore the N>2-qubit unitary case, I will implement that logic but it will be the same idea as Qmask.

Here's some examples of the directed graphs corresponding the pivot primitive (qubits are nodes and 2-qubit unitaries edges):

Pivot "1*" pattern on 8 qubits

pivot_1

Pivot "*1" pattern on 8 qubits

pivot_8

Aaron-Robertson commented 1 year ago

Sounds like a blast, I'd like to take this one @matt-lourens!

matt-lourens commented 1 year ago

That's Great, thanks @Aaron-Robertson!

Let me know if anything is unclear, I'm happy to elaborate more on the issue or package.

Aaron-Robertson commented 1 year ago

@matt-lourens I walked through the examples and code but I'm still not completely clear, so perhaps better we sync up! I'm happy to take notes while we do if you like, since I noticed a few TODOs around documenting Qmask.

matt-lourens commented 1 year ago

Hey @Aaron-Robertson sure thing, let's do call and sync up, are you free sometime tomorrow? I'm in SAST time, and can do a call the morning somewhere between 09:00 and 13:00. Let me know what time would work for you.

Qmask does a lot of things, so getting to grip with it does take time. The Qpivot will only relate to a small part of the Qmask functionality, specifically the Qmask functionality that generates edges for arity=2 mappings. The rest of Qmask you can essentially "ignore". So there won't be a Qunpivot nor a Qpivot_base. The reason being that Qpivot won't make any qubits unavailable, so there won't be qubits to make available again. I should've been more clear on what the relation to Qmask is, and that is only in the way that edges gets generated based on a pattern for 2 qubit unitaries.

The outline for Qpivot will look something like:

class Qpivot(Qmotif):
    """
    """

    def __init__(self, pattern="1*", **kwargs):
        # Initalize Qpivot, has a pattern attribute
        pass

    def __call__(self, Qc_l, *args, **kwargs):
        # Check if arity==2
            # Get pivot pattern function based on the logic from Qmask_base.get_mask_pattern_fn 
                # The relevant logic from Qmask_base.get_mask_pattern_fn is lines 731-753 (the if any("*") block)
            # Set source and pivot qubits by calling pivot pattern function
            # Generate edges based on connection type, nearest_cicle, nearest_tower, cycle
                # The logic of this will be similar to Qmask lines 849-881
                # You can ignore the other Qmask code, the true case  for the if on line 844 goes into the mask functionality that most mimics the qpivot one
            # If connection type is cycle use stride, step and offset to generate the edges
                # The stride
        return self

    def __eq__(self, other):
        # Equality check
        pass

I hope that makes things a bit clearer.

Stepping through the code:

u = Qunitary(V2, 0, 2)
hierq = Qinit(8) + Qmask("1*1*1", mapping=u)
circuit = hierq(backend="qiskit")
circuit.draw("mpl")

u = Qunitary(V2, 0, 2)
hierq = Qinit(8) + Qmask("*1", mapping=u)
circuit = hierq(backend="qiskit")
circuit.draw("mpl")

Should also help, specifically if you have a breakpoint on line 849 if self.connection_type == "nearest_circle": and check what happens based on different connection types, it might make things clearer. It is that logic of Qmask where edge generation is similar. The other part is the pattern string from 731-753 (the if any("*") block

Aaron-Robertson commented 1 year ago

@matt-lourens That clarifies most of my concerns, so thanks! I can be available from 12-13, but will go ahead with the stub as provided. Fortunately all I'd done was use a find and replace on mask as a way to walk through the code. The only functionality I worked on was the pattern matching, which should work as desired (though we can discuss custom pivot functions-which I don't imagine we'll want based on that description-and base patterns). I'll push up the updates shortly, and look forward to chatting tomorrow!

matt-lourens commented 1 year ago

Hey @Aaron-Robertson 12 SAST works for me, do you have discord? I'm on the unitary fund server, you can reach me at: Mattx2#9155

matt-lourens commented 1 year ago

Fixed with #40