Skip to content

H₂ Molecule Homework Assignment

View on GitHub

Quantum Software Development Journey: From Theory to Application with Classiq - Part 3

  • Similarly to what we have done in class, in this exercise we will implement the VQE on H2 molecule.

  • This time instead of using the built-in methods and functions (such as Molecule and MoleculeProblem) to difne and solve the problem, you will be provided with a two qubits Hamiltonian.

Submission

  • Submit the completed Jupyter notebook and report via GitHub. Ensure all files are correctly named and organized.

  • Use the Typeform link provided in the submission folder to confirm your submission.

Additional Resources

Important Dates

  • Assignment Release: 22.5.2024

  • Submission Deadline: 3.6.2024 (7 A.M GMT+3)


Happy coding and good luck!

# !pip install -U classiq
# import classiq
# classiq.authenticate()

Part 1

Given the following Hamiltonian:

\[\hat{H} = -1.0523 \cdot (I \otimes I) + 0.3979 \cdot (I \otimes Z) - 0.3979 \cdot (Z \otimes I) - 0.0112 \cdot (Z \otimes Z) + 0.1809 \cdot (X \otimes X)\]

Complete the following code

from typing import List

from classiq import *

HAMILTONIAN = QConstant(
    "HAMILTONIAN",
    List[PauliTerm],
    [
        PauliTerm(pauli=[Pauli.I, Pauli.I], coefficient=-1.0523),
        PauliTerm(pauli=[Pauli.I, Pauli.Z], coefficient=0.3979),
        PauliTerm(pauli=[Pauli.Z, Pauli.I], coefficient=-0.3979),
        PauliTerm(pauli=[Pauli.Z, Pauli.Z], coefficient=-0.0112),
        PauliTerm(pauli=[Pauli.X, Pauli.X], coefficient=0.1809),
    ],
)  # TODO: Complete Hamiltonian
@qfunc
def main(q: Output[QArray[QBit]], angles: CArray[CReal, 3]) -> None:
    # TODO: Create an ansatz which allows each qubit to have arbitrary rotation
    allocate(2, q)
    repeat(q.len, lambda i: U(angles[0], angles[1], angles[2], 0, q[i]))


@cfunc
def cmain() -> None:
    res = vqe(
        HAMILTONIAN,  # TODO: complete the missing argument
        False,
        [],
        optimizer=Optimizer.COBYLA,
        max_iteration=1000,
        tolerance=0.001,
        step_size=0,
        skip_compute_variance=False,
        alpha_cvar=1.0,
    )
    save({"result": res})


qmod = create_model(
    main, classical_execution_function=cmain
)  # TODO: complete the line, use classical_execution_function
qprog = synthesize(qmod)
# show(qprog)
execution = execute(qprog)
res = execution.result()
# execution.open_in_ide()
# print(res)
vqe_result = res[0].value  # TODO: complete the line
print(f"Optimal energy: {vqe_result.energy}")
print(f"Optimal parameters: {vqe_result.optimal_parameters}")
print(f"Eigenstate: {vqe_result.eigenstate}")
Optimal energy: -1.0736828125
Optimal parameters: {'angles_0': 6.723999211067887, 'angles_1': -4.787549480657028, 'angles_2': 0.01835345840038345}
Eigenstate: {'11': (0.05846339666834283+0j), '01': (0.2275034340400162+0j), '10': (0.18619336387207788+0j), '00': (0.9540210951546092+0j)}

Does it similar to the optimal energy we calculated in class? \ Does it similar to the total energy we calculated in class?

No! The value in the session was derived, according to different contributions, associated with the two-atom H_2 molecule, having taken into account various ways of communication through potential between them (atoms being effectively the qubits). This problem doesn't cover for those. Qubits are not talking to each other!

Part 2

Now, we want to have a more interesting ansatz in our main.
Add one line of code to the main function you created in Part 1 that creates entanglement between the two qubits.
Which gate should you use?

@qfunc
def main(q: Output[QArray[QBit]], angles: CArray[CReal, 3]) -> None:
    # TODO: Create an ansatz which allows each qubit to have arbitrary rotation

    allocate(2, q)
    repeat(q.len, lambda i: U(angles[0], angles[1], angles[2], 0, q[i]))
    CX(q[0], q[1])


@cfunc
def cmain() -> None:
    res = vqe(
        HAMILTONIAN,  # TODO: complete the missing argument
        False,
        [],
        optimizer=Optimizer.COBYLA,
        max_iteration=1000,
        tolerance=0.001,
        step_size=0,
        skip_compute_variance=False,
        alpha_cvar=1.0,
    )
    save({"result": res})


qmod = create_model(
    main, classical_execution_function=cmain
)  # TODO: complete the line, use classical_execution_function
qprog = synthesize(qmod)
# show(qprog)
execution = execute(qprog)
res = execution.result()
# execution.open_in_ide()
vqe_result = res[0].value  # TODO: complete the line
print(f"Optimal energy: {vqe_result.energy}")
print(f"Optimal parameters: {vqe_result.optimal_parameters}")
print(f"Eigenstate: {vqe_result.eigenstate}")
Optimal energy: -1.8497781249999998
Optimal parameters: {'angles_0': -2.9205748677970176, 'angles_1': -0.5854301692243206, 'angles_2': -3.1640158800495723}
Eigenstate: {'11': (0.11048543456039805+0j), '10': (0.12303137303143455+0j), '01': (0.9862333648787187+0j)}

Does it similar to the optimal energy we calculated in class? \ Does it similar to the total energy we calculated in class? \ What can we learn about the provided form this result Hamiltonian?

Evidently, the value of the optimal energy here bears a striking resemblence to the value of the problem discussed in the session. Almost surprisingly so, even though running the cell more than once introduces minimal differences. The entangling two-qubit gate CNOT is responsible for the conversation between the qubits, and that is the reason behind this similarity in values. Nevertheless, it's quite surprising admittedly to be able to simulate the impact of those sophisticated potential terms + the solid ansatz by a CNOT!