matt-lourens / hierarqcal

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

VQE on hydrogen molecule #37

Closed Gopal-Dahale closed 11 months ago

Gopal-Dahale commented 1 year ago

Purpose

Demo of the VQE algorithm on the hydrogen molecule

Details

We use hierarqcal to craft the UCCSD ansatz to perform VQE on hydrogen molecule using Qiskit. Ref: PennyLane. This PR resolves #29.

matt-lourens commented 1 year ago

Hey @Gopal-Dahale, sorry for only getting around to this until now. I just finished going through the PR, I really like it, great stuff! It's the exact kind of tutorial that I wanted.

Going through your code and trying to create the patterns myself helped me spot some necessary changes the package needs. Specifically, to provide exact parameter values to a unitary when initialized, and arity-1 functionality for the masking primitive. I have some suggestions on how we create the patterns, especially I want to use the masking primitive more. I've create a new branch, called feature/chemistry_example, where I added the above mentioned functionality. I suggest we change the PR to this branch, then we can merge before the hackathon ends and flesh out the final details.

Below I have some suggestions for the single and double fermionic pattern creations and then we can discuss what the best way to add it to the tutorial. As I mentioned in the issue, my knowledge on quantum chemistry is minimal so I'm not sure if my variable names are correct. For the FermionicSingleExcitation I suggest we use the masking primitive in the following way:

from hierarqcal import Qinit, Qcycle, Qunitary, Qmask, Qunmask

n = 3
# Create masking motifs, that makes the middle of circuit unavailable
mask_middle = Qmask(pattern="0" + "1" * (n - 2) + "0")
unmask_middle = Qunmask("previous")

# Create ladder motif
cnot_ladder = Qcycle(mapping=Qunitary("CNOT()^01"), boundary="open")
cnot_ladder_r = Qcycle(
    mapping=Qunitary("CNOT()^01"), edge_order=range(n, 1, -1), boundary="open"
)
ladder_motif = (
    cnot_ladder + Qcycle(offset=n - 1, mapping=Qunitary("Rz(x)^0")) + cnot_ladder_r
)

# Create excitation motifs
excitation1 = (
    mask_middle
    + Qcycle(mapping=Qunitary("RX(x)^0;H()^1", symbols=[-np.pi / 2]))
    + unmask_middle
    + ladder_motif
    + mask_middle
    + Qcycle(mapping=Qunitary("RX(x)^0;H()^1", symbols=[np.pi / 2]))
    + unmask_middle
)
excitation2 = (
    mask_middle
    + Qcycle(mapping=Qunitary("H()^0;RX(x1)^1", symbols=[-np.pi / 2]))
    + unmask_middle
    + ladder_motif
    + mask_middle
    + Qcycle(mapping=Qunitary("H()^0;RX(x1)^1", symbols=[np.pi / 2]))
    + unmask_middle
)

single_fermionic = excitation1 + excitation2

So the n=1 case is different from yours, so I'm not sure if that's an issue (if 1 qubit in this example makes sense). There's still some potential for improvement, but I like that this reads off the pattern you see, i.e. we mask the middle of the circuit, add the H and R layer, then unmask then do the ladder and so on. Let me know what you think!

Then for the FermionicDoubleExcitation:

 n = 4
r_locations = ["0010", "1011", "0111", "0001", "1000", "0100", "1110", "1101"]
rxn = Qunitary("RX(x)^0", symbols=[-np.pi / 2])
rxp = Qunitary("RX(x)^0", symbols=[np.pi / 2])
hadamards = Qcycle(mapping=Qunitary("H()^0"), boundary="open")

cnot_ladder = Qcycle(mapping=Qunitary("CNOT()^01"), boundary="open")
cnot_ladder_r = Qcycle(
    mapping=Qunitary("CNOT()^01"), edge_order=range(n, 1, -1), boundary="open"
)
ladder_motif = (
    cnot_ladder + Qcycle(offset=n - 1, mapping=Qunitary("Rz(x)^0")) + cnot_ladder_r
)

h_and_rs = [
    (
        Qmask(pattern=r_loc, mapping=rxn) + hadamards + Qunmask(r_loc),
        Qmask(pattern=r_loc, mapping=rxp) + hadamards + Qunmask(r_loc),
    )
    for r_loc in r_locations
]
excitations = [l1 + ladder_motif + l2 for l1, l2 in h_and_rs]
double_single_fermionic = reduce(lambda a, b: a + b, excitations)

I liked your use of reduce, and tried to combine that with the use of masks. The r_location string I use is inverted from yours, as it's the qubits where R must go and then makes those qubits unavailable, then just add a cycle of hadamards.

One question about these strings: Is there a pattern? i.e. can we generalize it for n qubits?

To create the final ansatz, we can add everything this way:

hierq = Qinit(n)+double_fermionic+Qmask("0001") +single_fermionic+ Qunmask("1111")+Qmask("1000") + single_fermionic
hierq(backend="qiskit", barriers=False).draw("mpl")

Then one possibility of specifying symbols:

factors = [-1 / 8] * 4 + [1 / 8] * 4 + [1 / 2] + [-1 / 2] + [1 / 2] + [-1 / 2]
symbols = []
i = 0
for el in hierq.get_symbols():
    if isinstance(el, Parameter):
        symbols += [el * factors[i]]
        i += 1
    else:
        symbols += [el]
hierq.set_symbols(symbols)
az = hierq(backend="qiskit", barriers=False)

The final circuit can then be created:

ansatz = QuantumCircuit(4)
ansatz.compose(hf_state, inplace=True)
ansatz.barrier()
ansatz.compose(az,inplace=True)
ansatz.draw('mpl', fold=50)

And the rest of the code I tested with the simulation and it seems to work. When you add/ try it out be sure to pull and switch to the new feature/chemistry_example branch as that is where I changed some core.py functionality

Let me know what you think about building the patterns this way, and if you see even further "automation"/ "simplification". I think we're close to something awesome!

What I'm going to try and do on this branch is add some different functionality for specifying symbols/Paramaters, so that that factors part is less hard coded. I also need to test the changes I made :D (you'll see there's two commits)

But we can merge the PR (to this branch) and close the issue before that (for the hackathon), I think just try and incorporate the use of masking the way I did above. But I'm happy to assign the issue to you, you deserve the bounty :)

I'm available for a chat tomorrow if you want me to talk you through some of the code I just shared, let me know if you're interested!

Gopal-Dahale commented 1 year ago

One question about these strings: Is there a pattern? i.e. can we generalize it for n qubits?

As per the docs of PennyLane, UCCSD makes use of the Fermionic single and double excitations. For single, there have to be at least 2 wires and for double at least 4. I can add an assertion for this. The ansatz can have any number of qubits but the single qubits gates are placed on 2 (single) and 4(double) wires only.

Question: You want me to pull and switch to feature/chemistry_example and then update the code and then the PR should target to feature/chemistry_example branch of hierarqcal?

matt-lourens commented 1 year ago

As per the docs of PennyLane, UCCSD makes use of the Fermionic single and double excitations. For single, there have to be at least 2 wires and for double at least 4. I can add an assertion for this. The ansatz can have any number of qubits but the single qubits gates are placed on 2 (single) and 4(double) wires only.

Cool thanks, no need to add an assertion, just wanted to make sure I understand. So the strings such as "1101" will always be like that, irrespective the number of qubits?

Question: You want me to pull and switch to feature/chemistry_example and then update the code and then the PR should target to feature/chemistry_example branch of hierarqcal?

Yes, exactly! One other thing I forgot to mention is to just specify at the top of the notebook the requirements for running it. I think it's just these:

pip install qiskit[nature]
pip install --upgrade pyscf
Gopal-Dahale commented 1 year ago

So the strings such as "1101" will always be like that, irrespective the number of qubits?

Yes. If you look at the docs of FermionicDoubleExcitation, they code for layers 1 to 8 in separate functions but the single qubits gates have fixed pattern and that's why I made a list of strings to ease of iteration.

matt-lourens commented 1 year ago

Hey @Gopal-Dahale,

I refined some things for the double and single ansatz creation. I've added a new commit to that feature/quantum_chemistry branch, to create the ansatzes this way, you just need to pull the new code. I think we should create the ansatze something along the following lines: Single:

from hierarqcal import Qinit, Qcycle, Qunitary, Qmask, Qunmask
# Motif to mask all qubits except outer two
mask_middle = Qmask(pattern="0!0")
unmask = Qunmask("previous")
hrx_layer = mask_middle + Qcycle(mapping=Qunitary("RX(x)^0;H()^1")) + unmask
hrx_layer_r = mask_middle + Qcycle(mapping=Qunitary("RX(x)^1;H()^0")) + unmask

# Create ladder motif
cnot_ladder = Qcycle(mapping=Qunitary("CNOT()^01"), boundary="open")
cnot_ladder_r = Qcycle(mapping=Qunitary("CNOT()^01"), edge_order=[-1], boundary="open")
rz_last = Qmask("*1", mapping = Qunitary("Rz(x)^0")) + Qunmask("previous") # TODO replace with Qpivot once it's ready
ladder = cnot_ladder + rz_last + cnot_ladder_r

# Create two excitations TODO term?
excitation1 = hrx_layer + ladder + hrx_layer
excitation2 = hrx_layer_r + ladder + hrx_layer_r

plot

single_fermionic = excitation1 + excitation2
hierq = Qinit(5) + single_fermionic # you can change to Qinit(3) or Qinit(4)
hierq(backend="qiskit", barriers=False).draw("mpl")

Double:

r_locations = ["0010", "1011", "0111", "0001", "1000", "0100", "1110", "1101"]
rx = Qunitary("RX(x)^0")
hadamards = Qcycle(mapping=Qunitary("H()^0"), boundary="open")
h_and_rs = [Qmask(pattern=r_loc, mapping=rx) + hadamards + Qunmask(r_loc) for r_loc in r_locations]
excitations = [layer + ladder + layer for layer in h_and_rs]
double_fermionic = reduce(lambda a, b: a + b, excitations)

Both:

nq = 4 
hierq = Qinit(nq)+double_fermionic+Qmask("0001") +single_fermionic+ Qunmask("1111")+Qmask("1000") + single_fermionic
hierq(backend="qiskit", barriers=False).draw("mpl")

What I like about this is that it reads easily and uses many of the package's functionality. It also reuses "motifs", so we only define what a ladder is once and then we can use it from there. We also use Qinit only use once, so the ladders and masking is all based on what happens before. You'll see I didn't provide any symbols/parameters. I think the easiest is just to specify them manually for now in a block along the lines:

symbols = # correct symbols in order
hierq.set_symbols(symbols)
az = hierq(backend="qiskit", barriers=False)