From de6b82fcdc2febb7bcac82dfbd3f17d056fba6de Mon Sep 17 00:00:00 2001 From: William Galvin Date: Sun, 24 Dec 2023 23:44:29 -0800 Subject: [PATCH] Add pennylane and vcvqe modules --- .gitignore | 5 +- README.md | 6 ++ data/h2.xyz | 4 +- data/h2o2.xyz | 4 + data/n2h4.xyz | 6 ++ pennylane/README.md | 12 +++ pennylane/requirements.txt | 2 + pennylane/setup.py | 11 +++ pennylane/src/main.py | 110 +++++++++++++++++++++ vcvqe/README.md | 39 ++++++++ requirements.txt => vcvqe/requirements.txt | 0 vcvqe/setup.py | 11 +++ {src => vcvqe/src}/__init__.py | 0 {src => vcvqe/src}/givens.py | 0 {src => vcvqe/src}/hamiltonian.py | 0 {src => vcvqe/src}/main.py | 47 +++++---- {src => vcvqe/src}/utils.py | 0 {tests => vcvqe/tests}/__init__.py | 0 {tests => vcvqe/tests}/test_givens.py | 0 {tests => vcvqe/tests}/test_hamiltonian.py | 6 +- {tests => vcvqe/tests}/test_utils.py | 9 +- 21 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 README.md create mode 100644 data/h2o2.xyz create mode 100644 data/n2h4.xyz create mode 100644 pennylane/README.md create mode 100644 pennylane/requirements.txt create mode 100644 pennylane/setup.py create mode 100644 pennylane/src/main.py create mode 100644 vcvqe/README.md rename requirements.txt => vcvqe/requirements.txt (100%) create mode 100644 vcvqe/setup.py rename {src => vcvqe/src}/__init__.py (100%) rename {src => vcvqe/src}/givens.py (100%) rename {src => vcvqe/src}/hamiltonian.py (100%) rename {src => vcvqe/src}/main.py (77%) rename {src => vcvqe/src}/utils.py (100%) rename {tests => vcvqe/tests}/__init__.py (100%) rename {tests => vcvqe/tests}/test_givens.py (100%) rename {tests => vcvqe/tests}/test_hamiltonian.py (86%) rename {tests => vcvqe/tests}/test_utils.py (76%) diff --git a/.gitignore b/.gitignore index 58d54ed..dae1eba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__/ -*.npy \ No newline at end of file +.pytest_cache +*.npy +*.egg-info +structure.xyz \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef3cd35 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# DD2367-Quantum-Chemistry +This is the final project of Samuil Donchev and William Galvin for Autumn 2023 DD2367 Quantum Computing for Computer Scientisits at KTH Stockholm. + +In this project, we explore the variational quantum eigensolver (VQE) and its applications to computational chemistry, particularly in solving the electron structure problem. + +In `/paper` we present an overview of our findings, in `/pennylane` we use an off-the-shelf library and tutorial to demonstrate the VQE with different molecules, and in `/vcvqe` we present a "very classical variational quantum eigensolver" that gives a lower level, less practical window into the VQE for quantum chemistry. See their READMEs for more information and usage instructions. \ No newline at end of file diff --git a/data/h2.xyz b/data/h2.xyz index 8e2728b..a5ee9dc 100644 --- a/data/h2.xyz +++ b/data/h2.xyz @@ -1,4 +1,4 @@ 2 -H 0.0000 0.0000 -0.66 -H 0.0000 0.0000 0.66 \ No newline at end of file +H 0.0000 0.0000 -0.75 +H 0.0000 0.0000 0 \ No newline at end of file diff --git a/data/h2o2.xyz b/data/h2o2.xyz new file mode 100644 index 0000000..ef231c1 --- /dev/null +++ b/data/h2o2.xyz @@ -0,0 +1,4 @@ +O 0.0000 0.7375 -0.0528 +O 0.0000 -0.7375 -0.0528 +H 0.8190 0.8170 0.4220 +H -0.8190 -0.8170 0.4220 \ No newline at end of file diff --git a/data/n2h4.xyz b/data/n2h4.xyz new file mode 100644 index 0000000..278573a --- /dev/null +++ b/data/n2h4.xyz @@ -0,0 +1,6 @@ +N 0.0000 0.7230 -0.1123 +N 0.0000 -0.7230 -0.1123 +H -0.4470 1.0031 0.7562 +H 0.4470 -1.0031 0.7562 +H 0.9663 1.0031 0.0301 +H -0.9663 -1.0031 0.0301 \ No newline at end of file diff --git a/pennylane/README.md b/pennylane/README.md new file mode 100644 index 0000000..2b219dd --- /dev/null +++ b/pennylane/README.md @@ -0,0 +1,12 @@ +# PennyLane + +This is a slight modifaction of the code found in [this PennyLane tutorial](https://pennylane.ai/qml/demos/tutorial_vqe/). + +## Installation +1. Clone this repo and `cd` into `pennylane` +2. Run `pip install -r requirements.txt && pip install -e .` + +## Usage +``` +vqe-pennylane --file ../data/[file].xyz [--max-iter ] [--threshold ] +``` \ No newline at end of file diff --git a/pennylane/requirements.txt b/pennylane/requirements.txt new file mode 100644 index 0000000..7311e02 --- /dev/null +++ b/pennylane/requirements.txt @@ -0,0 +1,2 @@ +pennylane>=0.32 +matplotlib \ No newline at end of file diff --git a/pennylane/setup.py b/pennylane/setup.py new file mode 100644 index 0000000..7e71b3c --- /dev/null +++ b/pennylane/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + +setup( + name="vqe-pennylane", + version="0.1.0", + packages=["src"], + entry_points={ + "console_scripts": + ["vqe-pennylane = src.main:main"] + }, +) diff --git a/pennylane/src/main.py b/pennylane/src/main.py new file mode 100644 index 0000000..5b81a37 --- /dev/null +++ b/pennylane/src/main.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 + +import argparse + +from pennylane import numpy as np +import pennylane as qml + + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--file", + required=True, + help="Path to .xyz file for input molecule." + ) + + parser.add_argument( + "--max-iter", + type=int, + default=50, + help="Maximum number of optimization cycles" + ) + + parser.add_argument( + "--threshold", + type=float, + default=1e-6, + help="Convergence threshdold" + ) + + args = parser.parse_args() + + symbols, geometry = qml.qchem.read_structure(args.file) + + mol = qml.qchem.Molecule(symbols, geometry) + electrons = mol.n_electrons + H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry) + + if qubits > 8: + print(f"{qubits} qubits: This may take awhile...") + else: + print("Number of qubits = ", qubits) + + + dev = qml.device("lightning.qubit", wires=qubits) + + hf = qml.qchem.hf_state(electrons, qubits) + print("Hartree-fock basis state:", hf) + + + def circuit(param, wires): + qml.BasisState(hf, wires=wires) + singles, doubles = qml.qchem.excitations(electrons, qubits) + + for i, s in enumerate(singles): + qml.SingleExcitation(param[i], wires=s) + for i, s in enumerate(doubles): + qml.DoubleExcitation(param[i + len(singles)], wires=s) + + + @qml.qnode(dev, interface="autograd") + def cost_fn(param): + circuit(param, wires=range(qubits)) + return qml.expval(H) + + opt = qml.GradientDescentOptimizer(stepsize=0.4) + singles, doubles = qml.qchem.excitations(electrons, qubits) + theta = np.random.random((len(singles) + len(doubles)), requires_grad=True) + + + energy = [cost_fn(theta)] + angle = [theta] + + for n in range(args.max_iter): + theta, prev_energy = opt.step_and_cost(cost_fn, theta) + + energy.append(cost_fn(theta)) + angle.append(theta) + + conv = np.abs(energy[-1] - prev_energy) + + if n % 2 == 0: + print(f"Step = {n}, Energy = {energy[-1]:.8f} Ha") + + if conv <= args.threshold: + break + + print("\n" f"Final value of the ground-state energy = {energy[-1]:.8f} Ha") + print("\n" f"Optimal value of the circuit parameter:") + for i in theta: + print(f"{i: 5f}") + + + import matplotlib.pyplot as plt + + plt.plot(range(n + 2), energy, "tab:red", ls="-.") + plt.xlabel("Optimization step", fontsize=13) + plt.ylabel("Energy (Hartree)", fontsize=13) + plt.ylabel(r"$E_\mathrm{HF}$", fontsize=15) + plt.title("Calculated Energy vs Optimization step") + plt.xticks(fontsize=12) + plt.yticks(fontsize=12) + + plt.savefig("plot.png") + print("\nPlot saved at plot.png") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/vcvqe/README.md b/vcvqe/README.md new file mode 100644 index 0000000..6427b3d --- /dev/null +++ b/vcvqe/README.md @@ -0,0 +1,39 @@ +# Very Classical Variational Quantum Eigensolver (VCVQE) + +## Background + +This is a classical implementation of the Variational Quantum Eigensolver for quantum chemistry described in [this PennyLane tutorial](https://pennylane.ai/qml/demos/tutorial_vqe/). See the accompanying write-up in `../paper` for more details. + +This is considered a "classical" implementation because it takes the ideas from the VQE and implements them in classical matrix algebra, an approach that quickly becomes unmanageable as molecules grow large. For example, with a H2O molecule, this requires 14 qubits and therefor a Hamiltonian $H \in \mathbb{C}^{2^{14} \times 2^{14}}$, which is too large to even write down on a laptop. Nonetheless, it provides an instructive window into the inner workings of the VQE + +The energies calculated by this program account for only electron-electron and electron-nucleus interactions and electron kinetic energy, under the Born-Oppenheimer and Hartree-Fock approoximations. It does not incorporate nucleus-nucleus interactions, nuclear kinetic energies, or pairwise electron-electron energies. As such, **the energies calculated here do not match the energies calculatd by PySCF and PennyLane**. + +This program takes as input a molecular geometry (the 3D coordinates of the nuclei) and attempts to solve the electron structure problem. It prints a brief report of its findings. + +## Installation +1. Clone this repo and `cd` to the root directory `vcvqe` +2. Install with `pip install -r requirements.txt` and `pip install -e .` + +## Usage +Run with +``` +$ vcvqe [-h] [--random] [--no-random] [--num-H NUM_H] [--file FILE] [--max-iter MAX_ITER] [--min-iter MIN_ITER] [--threshold THRESHOLD] [--step-size STEP_SIZE] +``` +For example +``` +vcvqe --file data/h4.xyz +``` +or +``` +vcvqe --random --num-H 2 --max-iter 25 --min-iter 5 --threshold 0.001 --step-size 0.25 +``` + +This program can be used without `pip install -e .` by running `python src/main.py [args]`. + +### args +- `random`: Use a hydrogen molecules with atoms randomly placed in 3D space. Almost certainly will not be a physically observable molecule. If `random` is used, `file` should not be used and `num-H` should be used. +- `num-H`: Number of hydrogen atoms to use in random hydrogen molecule. H2 and H4 are supported, but note that H3 is not (PySCF doesn't support it). Defaults to 4 +- `file`: path to file with a molecule's `.xyz` coords. Theoretically may be any molecule, works best if it's a very small molecule (H2 or H4). +- `[max|min]-iter` max/min iterations of optimization loop to perform. Defaults to 25 and 5 +- `threshold` If two consecutuve energies are calculated with a difference not greater than threshold, break optimization loop. Defaults to 0.001. +- `step-size` Learning rate in optimization step. Defaults to 1 diff --git a/requirements.txt b/vcvqe/requirements.txt similarity index 100% rename from requirements.txt rename to vcvqe/requirements.txt diff --git a/vcvqe/setup.py b/vcvqe/setup.py new file mode 100644 index 0000000..d00fd66 --- /dev/null +++ b/vcvqe/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + +setup( + name="vcvqe", + version="0.1.0", + packages=["src"], + entry_points={ + "console_scripts": + ["vcvqe = src.main:main"] + }, +) \ No newline at end of file diff --git a/src/__init__.py b/vcvqe/src/__init__.py similarity index 100% rename from src/__init__.py rename to vcvqe/src/__init__.py diff --git a/src/givens.py b/vcvqe/src/givens.py similarity index 100% rename from src/givens.py rename to vcvqe/src/givens.py diff --git a/src/hamiltonian.py b/vcvqe/src/hamiltonian.py similarity index 100% rename from src/hamiltonian.py rename to vcvqe/src/hamiltonian.py diff --git a/src/main.py b/vcvqe/src/main.py similarity index 77% rename from src/main.py rename to vcvqe/src/main.py index a1ac9e2..b4ccb54 100644 --- a/src/main.py +++ b/vcvqe/src/main.py @@ -11,14 +11,14 @@ from jax import numpy as jnp from jax import value_and_grad -from givens import givens -from givens import hf_ground -from givens import full_wavefunction -from givens import excitations -from hamiltonian import expectation -from hamiltonian import hamiltonian -from utils import get_electrons_qubits -from utils import random_xyz +from src.givens import givens +from src.givens import hf_ground +from src.givens import full_wavefunction +from src.givens import excitations +from src.hamiltonian import expectation +from src.hamiltonian import hamiltonian +from src.utils import get_electrons_qubits +from src.utils import random_xyz def energy(H: np.ndarray, electrons: int, qubits: int, thetas: np.ndarray) -> float: @@ -47,10 +47,15 @@ def get_args() -> argparse.Namespace: parser.add_argument( "--random", - action=argparse.BooleanOptionalAction, + action="store_true", default=False, help="Specify whether to use a random molecule or supply your own. If --random is set, --num-H must be too. If not, --file must be set" ) + parser.add_argument( + "--no-random", + dest="random", + action="store_false" + ) parser.add_argument( "--num-H", @@ -92,13 +97,16 @@ def get_args() -> argparse.Namespace: help="Optimization step size aka learning rate" ) - return parser.parse_args() + return parser.parse_args(), parser -if __name__ == "__main__": +def main(): linebreak = "\n" + 100 * "=" + "\n" - args = get_args() + args, parser = get_args() + if not args.random and args.file is None: + parser.print_help() + exit(-1) with tempfile.NamedTemporaryFile() as f: if args.random: @@ -116,11 +124,11 @@ def get_args() -> argparse.Namespace: try: electrons, qubits = get_electrons_qubits(file) - except RuntimeError: - raise RuntimeError("Molecule not valid for PySCF") + except RuntimeError as e: + raise RuntimeError(f"Molecule not valid for PySCF:\n {str(e)}") + print(f"\nCreating Hamiltonian for {electrons} electrons and {qubits} spin orbitals") H = hamiltonian(file) - print(f"\nHamiltonian created for {electrons} electrons and {qubits} spin orbitals") print(linebreak) singles, doubles = excitations(electrons, qubits) @@ -142,11 +150,14 @@ def get_args() -> argparse.Namespace: print(f"\tstep: {i: 2}\t\tE: {E: .3f}\t\tDifference: {E - prev: .3f}") - if abs(E - prev) < args.threshold and i >= args.min_iter: + if abs(E - prev) < args.threshold and i >= args.min_iter - 1: break prev = E print(linebreak) - print(f"Final E converged to {E: 5f} in {i} steps\n") - \ No newline at end of file + print(f"Final E converged to {E: 5f} in {i + 1} steps\n") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/utils.py b/vcvqe/src/utils.py similarity index 100% rename from src/utils.py rename to vcvqe/src/utils.py diff --git a/tests/__init__.py b/vcvqe/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to vcvqe/tests/__init__.py diff --git a/tests/test_givens.py b/vcvqe/tests/test_givens.py similarity index 100% rename from tests/test_givens.py rename to vcvqe/tests/test_givens.py diff --git a/tests/test_hamiltonian.py b/vcvqe/tests/test_hamiltonian.py similarity index 86% rename from tests/test_hamiltonian.py rename to vcvqe/tests/test_hamiltonian.py index 0b3b7d6..0980633 100644 --- a/tests/test_hamiltonian.py +++ b/vcvqe/tests/test_hamiltonian.py @@ -1,7 +1,11 @@ import numpy as np +import os from src import hamiltonian +root = "." if os.path.isdir("data") else ".." +print(root) + def test_creation_annihilation_shape(): for n in range(8): for i in range(n): @@ -22,6 +26,6 @@ def test_creation_annihilation_anti_symmetry(): def test_hamiltonian_hermitian(): - file = "data/h2.xyz" + file = f"{root}/data/h2.xyz" H = hamiltonian.hamiltonian(file) assert np.allclose(H, H.conj().T) diff --git a/tests/test_utils.py b/vcvqe/tests/test_utils.py similarity index 76% rename from tests/test_utils.py rename to vcvqe/tests/test_utils.py index 0f7efbd..20b2069 100644 --- a/tests/test_utils.py +++ b/vcvqe/tests/test_utils.py @@ -1,7 +1,10 @@ import tempfile +import os from src import utils +root = "." if os.path.isdir("data") else ".." + def test_random_xyz(): atoms = {"H": 12, "O": 89, "Cl": 4} with tempfile.NamedTemporaryFile() as f: @@ -12,11 +15,11 @@ def test_random_xyz(): def test_get_electrons_qubits(): - file = "data/h2.xyz" + file = f"{root}/data/h2.xyz" assert utils.get_electrons_qubits(file) == (2, 4) - file = "data/h4.xyz" + file = f"{root}/data/h4.xyz" assert utils.get_electrons_qubits(file) == (4, 8) - file = "data/h2o.xyz" + file = f"{root}/data/h2o.xyz" assert utils.get_electrons_qubits(file) == (10, 14) \ No newline at end of file