├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── labs ├── Astrochem │ └── CH3Spec_student.pdf ├── Atomic_radius │ └── Atom_radius_student.pdf ├── Basis_Sets │ └── Basis_Sets_student.ipynb ├── Bond_Breaking │ ├── Bond_Breaking_student.ipynb │ ├── orbital_helper.py │ ├── ozone.png │ └── restricted.png ├── CationPi │ ├── CationPi_student.pdf │ └── CationPi_student_ChemCompute.pdf ├── Getting_Started │ ├── WebMO_setup.pdf │ └── tutorial_student.pdf ├── Hartree_Fock │ └── HF_student.ipynb ├── Machine_Learning │ ├── Machine_Learning_Student.ipynb │ └── data │ │ ├── ani1_test.pd │ │ ├── ani1_train.pd │ │ └── h2o_contour.png ├── Microwave_Spectroscopy │ └── Microwave_Spectroscopy_student.ipynb ├── PIB │ ├── Box1D_student.pdf │ └── Box1D_student_ChemCompute.pdf ├── Polarity │ └── Polar_student.pdf ├── Symmetry │ └── Symmetry_student.pdf ├── Symmetry_Adapted_Perturbation_Theory │ ├── sapt0_student.ipynb │ └── water2510.png ├── spectroscopic_constants │ └── spectroscopic_constants_student.ipynb └── water_MO │ └── waterMO_student.pdf └── media ├── psi-3po.jpg └── psi4edubanner.png /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by [contacting the project 59 | team](mailto:armcdona@calpoly.edu?subject=[Psi4EDU%20GitHub]%20Psi4Education%20Breach%20of%20Community%20Conduct%20Report). 60 | All complaints will be reviewed and investigated and will result in a response 61 | that is deemed necessary and appropriate to the circumstances. The project team 62 | is obligated to maintain confidentiality with regard to the reporter of an 63 | incident. Further details of specific enforcement policies may be posted 64 | separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | 77 | 78 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Psi4Education 2 | ============================= 3 | 4 | We're so happy to hear that you're interested in contributing content 5 | to the Psi4Education project! 6 | 7 | Because we're developing educational materials for use in real college 8 | classrooms, we have the unique challenge of keeping the bulk of our content 9 | (instructor materials, answer keys, etc.) "private" so that students actually 10 | have the opportunity to work on these exercises for themselves. To meet this 11 | challenge, our organization has adopted a two-repository solution: 12 | * `psi4education/psi4education`: Public-facing repository with all materials 13 | necessary for students, and 14 | * `psi4education/psi4education-instructors`: Private repository with all 15 | instructor and student-centered materials. 16 | 17 | To avoid accidentally making instructor materials publicly available in his 18 | repository (`psi4education/psi4education`), we do not accept pull requests or 19 | any other content contributions here. Instead, please contact one of the 20 | repository maintainers, [Prof. Ashley 21 | Ringer-McDonald](mailto:armcdona@calpoly.edu?subject=[p4edu%20GitHub]%20Psi4Education%20Instructor%20Access) 22 | or [Dr. Dominic 23 | Sirianni](mailto:sirianni.dom@gmail.com?subject=[p4edu%20GitHub]%20Psi4Education%20Instructor%20Access) 24 | letting us know your affiliation, institutional e-mail address, GitHub 25 | username, and a brief description of the course or courses where you would use 26 | the Psi4Education resources, and we will invite you to collaborate on the 27 | `psi4education/psi4education-instructor` repository. 28 | 29 | Looking forward to hearing from you! 30 | 31 | -- The Psi4Education Developers 32 | 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # ***NOTICE*** 2 | 3 | Since this repository is meant to be public for student access, we do not 4 | accept any content contributions to this repository. If you are interested in 5 | contributing to our project, please see our [contribution 6 | guidelines](https://github.com/Psi4Education/psi4education/blob/master/.github/CONTRIBUTING.md) 7 | for further details. 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | *.ipynb_checkpoints 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Unit test / coverage reports 31 | htmlcov/ 32 | .tox/ 33 | .coverage* 34 | .cache 35 | nosetests.xml 36 | coverage.xml 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # Psi4 output files 57 | *.py.dat 58 | *.out 59 | *clean 60 | *timer.dat 61 | *.swp 62 | *.npz 63 | Test/ 64 | output.dat 65 | ijk.dat 66 | timer.dat 67 | output.default.180.npz 68 | *DS_Store 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2019, The Psi4Education Developers. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the Psi4NumPy Developers nor the names of any 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Psi4Education banner logo
4 | 5 |
6 |
7 |

8 | 9 | # Overview 10 | 11 | Psi4Education is the education and outreach program of the [Psi4](psicode.org) 12 | electronic structure software package. Psi4Education offers a suite of "dry" 13 | computational chemistry lab activities, suitable for classes across the chemistry 14 | curriculum. 15 | 16 | ## Getting Started 17 | 18 | All lab activities and instructional materials are provided free of charge, and 19 | can be downloaded directly as a [zipped 20 | archive](https://github.com/Psi4Education/psi4education/archive/master.zip) or 21 | by [cloning this 22 | repository](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository). 23 | If you are a faculty member or instructor and would like to receive access to 24 | instructor guides, answer keys, and other resources, please [contact 25 | us](mailto:armcdona@calpoly.edu?subject=[p4edu%20GitHub]%20Psi4Education%20Instructor%20Access). 26 | In your request, please use Psi4Education in your subject line, and let us know 27 | your affiliation, institutional e-mail address, GitHub username, and a brief 28 | description of the course or courses where you would use the Psi4Education 29 | resources. 30 | 31 | ### Required Software 32 | 33 | Since Psi4Education provides computational chemistry labs, it might be expected 34 | that there is some required software to install or download in order for you or 35 | your students to enjoy these activities. Fortunately, however, all lab 36 | activities may be performed online using either the [WebMO Demo 37 | Server](https://www.webmo.net/demo/) or JupyterHub through our partnership with 38 | [Chem Compute](https://chemcompute.org/jupyterhub). 39 | 40 | ### Available Resources 41 | 42 | All laboratory activities can be found in the 43 | [`labs/`](https://github.com/Psi4Education/psi4education/tree/master/labs) 44 | directory, within which all labs are separated into their own subdirectories. 45 | Psi4Education offers two distinct types of laboratory exercises: 46 | * WebMO labs, which utilize the WebMO graphical front-end user interface to 47 | facilitate performing quantum chemistry computations, and 48 | * Python labs, in which quantum chemistry computations, data collection, 49 | and analysis are performed using the Python programming language within the 50 | Jupyter notebook development environment. 51 | 52 | ## Contributing to Psi4Education 53 | 54 | There are several ways to become involved with Psi4Education, each of which are 55 | highly valuable, and will help Psi4Education to better serve the needs of the 56 | chemical education community at large. You can help by 57 | * [submitting an 58 | issue](https://github.com/Psi4Education/psi4education/issues/new/choose) if you 59 | find a mistake or want to request a new lab, 60 | * making your voice heard by participating in discussions on [existing 61 | issues](https://github.com/psi4education/psi4education/issues) raised on this 62 | repository, 63 | * updating/adding features to existing content, or 64 | * by submitting new content. 65 | 66 | >If you are interested in submitting a new lab activity, we would love to hear 67 | from you! Please learn how by checking out our [contribution 68 | guidelines](https://github.com/Psi4Education/psi4education/blob/master/.github/CONTRIBUTING.md). 69 | 70 | -------------------------------------------------------------------------------- /labs/Astrochem/CH3Spec_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Astrochem/CH3Spec_student.pdf -------------------------------------------------------------------------------- /labs/Atomic_radius/Atom_radius_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Atomic_radius/Atom_radius_student.pdf -------------------------------------------------------------------------------- /labs/Bond_Breaking/Bond_Breaking_student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Bond Breaking\"\"\"\n", 10 | "\n", 11 | "__authors__ = \"Victor H. Chavez\", \"Lyudmila Slipchenko\"\n", 12 | "__credits__ = [\"Victor H. Chavez\", \"Lyudmila Slipchenko\"]\n", 13 | "__email__ = [\"gonza445@purdue.edu\", \"lslipchenko@purdue.edu\"]\n", 14 | "\n", 15 | "__copyright__ = \"(c) 2008-2019, The Psi4Education Developers\"\n", 16 | "__license__ = \"BSD-3-Clause\"\n", 17 | "__date__ = \"2019-11-18\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "---\n", 25 | "## Lab 2. Bond-breaking in $H_2$" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "In this lab, you will:\n", 33 | "* Investigate the bond-breaking reaction in $H_2$ molecule.\n", 34 | "* Compare the performance of restricted and unrestricted Hartree-Fock, and Density Functional Theory for bond breaking.\n", 35 | "* Benchmark these results with respect to the Full Configuration Interaction (FCI) values obtained using the coupled cluster with single and double excitations (CCSD) calculations, which give the exact answer for the two-electron system. \n", 36 | "* Calculate the correlation energy.\n", 37 | "* Distinguish dynamic and static contributions to the correlation energy.\n", 38 | "\n", 39 | "Authors: Lyudmila Slipchenko (lslipchenko@purdue.edu; ORCID: 0000-0002-0445-2990) and Victor H. Chavez (gonza445@purdue.edu; ORCID: 0000-0003-3765-2961).\n" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "***" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 3, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "#Import modules\n", 56 | "\n", 57 | "import psi4\n", 58 | "\n", 59 | "import numpy as np\n", 60 | "import os\n", 61 | "import matplotlib.pyplot as plt\n" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "***\n", 69 | "To perform a basic calculation we use the ```psi4.energy``` function. The function needs to know what method and basis set to use, and what molecule you are interested in (if you have defined more than one geometry inside your Jupyter Notebook). Let say that we want to get the HF energy of the Helium atom. We would need to do the following:\n" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 4, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "name": "stdout", 79 | "output_type": "stream", 80 | "text": [ 81 | "The HF energy of He is -2.8551883987268125\n" 82 | ] 83 | } 84 | ], 85 | "source": [ 86 | "#Define Helium Geometry\n", 87 | "#The first line referst to the charge and spin multiplicity. \n", 88 | "\n", 89 | "he_geo = psi4.geometry(\"\"\"\n", 90 | "0 1\n", 91 | "He 0.0 0.0 0.0\n", 92 | "\"\"\")\n", 93 | "\n", 94 | "#Request the HF calculation using the correlation consistent basis set cc-pvdz. \n", 95 | "e = psi4.energy(\"HF/cc-pvdz\", molecule=he_geo)\n", 96 | "\n", 97 | "\n", 98 | "#Print the energy. The units are given in atomic units or hartrees. \n", 99 | "print(f\"The HF energy of He is {e}\")\n", 100 | "\n", 101 | "#We made us of *f-strings* which allow us to combine strings and numbers in a print statement." 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "If you were to try the Helium example on a Hydrogen atom as it is, you would find that Psi4 will throw an error. This is because when running a calculation, Psi4 defaults to a *Restricted Hartree-Fock* or *RHF*, (i.e. a system with an even number of electrons where all electrons are paired). This means that electrons of opposite spin occupy (or are \"restricted\") to the same spatial orbital. \n", 109 | "\n", 110 | "In cases like Hydrogen, where the numbers of alpha and beta spin electrons are different, we lift this restriction allowing both electrons to have different spatial orbitals. \n", 111 | "\n", 112 | "
\n", 113 | "\n", 114 | "\n", 115 | "\n", 116 | "
\n", 117 | "\n", 118 | "We need to tell Psi4 that we want an UHF calculation. This is done by setting the global option \"reference\" as \"UHF\". In the cell below, type: ```psi4.set_options({\"reference\" : \"UHF\"})```. You may need to switch between UHF and RHF many times throughout the lab. " 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "***" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "We want to produce a binding energy curve for the $H_2$ molecule using different levels of theory. The binding energy is given by:\n", 133 | "\n", 134 | " $$E_{bind} = E(H_2) - 2E(H) \\tag{1} $$\n", 135 | "\n", 136 | "For a molecule with one degree of freedom, just like the $H_2$ molecule, the potential energy surface is just a 1D curve. Notice that the second term on the right hand side of the equation is just constant that is equal to two times the energy of the Hydrogen atom. Your first task is to obtain this value for each method:\n", 137 | "\n", 138 | "### Part 1\n", 139 | "\n", 140 | "\n", 141 | "#### **1.** Calculate and store the energy of a single H atom with the methods: HF, PBE, B3LYP and CCSD. Use 6-31G** basis for all the calculation in this lab. Change the reference and multiplicity of the atom accordingly. \n", 142 | "\n", 143 | "\n", 144 | "
\n", 145 | " \n", 146 | "Hint: Notice that the first argument of `psi4.energy` is a string. You could quickly go through the calculations by creating a list with the different methods and then use them in a for loop to to run each of them. Consider that the string also contains the basis set. In order to overcome this predicament, remember that strings can be concatenated by using the `+` operator (e.g. \"HF\"+\"/cc-pvdz\"). \n", 147 | " \n", 148 | "
\n", 149 | "\n", 150 | "#### **2.** Explain the origin of errors in each method and why HF and CCSD energies are the same for the H atom. \n" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 2, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "### RESPONSE" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "---\n", 167 | "\n", 168 | "Let us now concentrate on the first term of equation 1. We require to run a series of calculations for each method at different H-H separations in Angstroms (e.g. 0.3, 0.4, 0.5, ... ,4.9, 5.0). \n", 169 | "
\n", 170 | "Hint : Given that the argument of a psi4.geometry is a string, we can take advantage of that by looking at the following example:\n", 171 | "
" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 4, 177 | "metadata": { 178 | "scrolled": true 179 | }, 180 | "outputs": [ 181 | { 182 | "name": "stdout", 183 | "output_type": "stream", 184 | "text": [ 185 | " \n", 186 | "H 0.0 0.0 0.0\n", 187 | "\n", 188 | " \n", 189 | "He 0.0 0.0 0.0\n", 190 | "\n", 191 | " \n", 192 | "Li 0.0 0.0 0.0\n", 193 | "\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "#Define string with psi4.geometry syntax. \n", 199 | "#Identify what you want to change and use a particular label that you know that won't get repeated. \n", 200 | "molecule = \"\"\" \n", 201 | "**atom1** 0.0 0.0 0.0\n", 202 | "\"\"\"\n", 203 | "\n", 204 | "#Create a list with the things that you want to go through. \n", 205 | "atoms = [ \"H\", \"He\", \"Li\" ]\n", 206 | "\n", 207 | "#Cycle through them. \n", 208 | "for atom in atoms:\n", 209 | " print(molecule.replace(\"**atom1**\", atom))" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "#### **3.** Using the previous example and the following distances, write a snippet that will calculate the energy at each separation for a **RHF** calculation. You will need to change the reference to \"RHF\" (Psi4 still thinks you want to run UHF calculations). \n", 217 | "Make sure you store the wavefunction object for each separation since it will be used later in the lab: ```energy, wfn = psi4.energy(\"method/basis\", return_wfn=True)```" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 5, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "#We use more points closer to where we would expect to have the ground state geometry to create a nice and smooth function. \n", 227 | "distances = np.zeros(20)\n", 228 | "distances[0:16] = np.linspace(0.3, 2.5, 16)\n", 229 | "distances[16:] = np.linspace(2.7, 5.0, 4)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "markdown", 234 | "metadata": {}, 235 | "source": [ 236 | "Here we are using the `numpy` library first to create an empty array filled with zeros using `np.zeros`, where the argument specifies the size of the array. The other function `np.linspace` creates a sequence of evenly spaced values within an interval. This means, we generated a linear space with 16 points from 0.3 to 2.5 and one with 4 points from 2.7 to 5.0. " 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 3, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "#RESPONSE:\n" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "---\n", 253 | "#### **4.** Calculate the energies at the same distances at the **UHF** level. You can recycle the code that you just wrote (just remember to change the name of your variables). We will need extra information that can ony be found in the output. \n", 254 | "\n", 255 | "In order to save the output to a file we require the additional option: ```psi4.core.set_output_file(\"filename.txt\", True)```.\n", 256 | "\n", 257 | "
\n", 258 | "Hint: In order to obtain the correct UHF energies, we need to set the extra following options:\n", 259 | "
" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": 7, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "psi4.set_options({'reference' : 'UHF', \n", 269 | " 'guess_mix' : True, \n", 270 | " 'guess' : \"gwh\"})" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 5, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "#RESPONSE" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "#### **5.** Store the values for $S^2$. This information is found in each outputfile close to the end of your calculation (look for Spin Contamination Metric). \n", 287 | "\n", 288 | "You can go through each of the files and copy the value, but you can also think about how can you let python automatize this process. Think carefully about the steps required for this. Given a path you would need to import the file ( `f = open(path, 'r')` ) and proceed to extract the lines ( `f.read().splitlines()` ). With those lines available, you may concentrate on determining whether or not each line contains the `S^2` string. \n", 289 | "\n", 290 | "If you require a more thorough review of parsing files. You can look at [this tutorial](https://education.molssi.org/python_scripting_cms/02-file_parsing/index.html) to learn more about file parsing. " 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 6, 296 | "metadata": {}, 297 | "outputs": [], 298 | "source": [ 299 | "#Response\n" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "metadata": {}, 305 | "source": [ 306 | "#### **6.** Make a table or plot of $S^2$ values from the UHF calculations. Explain why $S^2$ deteriorates when the H-H bond is stretched." 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 7, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "#Response" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": {}, 321 | "source": [ 322 | "---\n", 323 | "#### 7. Calculate the same potential energy surface at the DFT level. Use the PBE functional and a restricted wavefunction. " 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 8, 329 | "metadata": {}, 330 | "outputs": [], 331 | "source": [ 332 | "#RESPONSE\n" 333 | ] 334 | }, 335 | { 336 | "cell_type": "markdown", 337 | "metadata": {}, 338 | "source": [ 339 | "#### **8.** Calculate the same potential energy surface at the FCI level. \n", 340 | "For a two-electron system, the FCI results may be obtained by using the CCSD method. This is true because CCSD includes determinants that are singly and doubly excited. For a two electron system that includes all electrons available, thus CCSD includes all possible excitations in the system. \n", 341 | "\n", 342 | "#### **9.** You will need to save the output file generated by Psi4 again. From the output file, record total CCSD ampltitudes: CCSD $T_1^2$ and $T_2^2$, and the value of the largest $T_2$ amplitude for the ground state geometry and for a split geometry. " 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "Consider T as a sum of operators that that act on a reference determinant. In CCSD $T = T_1 + T_2$ where $T_1$ refers with single excitations and $T_2$ with double excitations. \n", 350 | "The values of amplitudes show a relative weight of singly and doubly excited determinants in the wavefunction. If $T_1$ and/or $T_2$ are large (generally speaking, if a particular |$T_2| > 0.1$), the wavefunction is considered to be multi-configurational, i.e., containing several important Slater determinants. In other words, this is a region where non-dynamic (static) correlation is significant. Several small $T_1$ and $T_2$ amplitudes tell about (almost always present) dynamic correlation.\n", 351 | "\n", 352 | "In each output you should look at the values of *Largest {TIA, Tia, TIjAb} Amplitudes*, where the $T$ refers to the previously mentioned operator, and the following indices refer to the orbitals used according to the notation:\n", 353 | "\n", 354 | "\n", 355 | "| | Occupied Molecular Orbitals | Virtual Molecular Orbitals | | |\n", 356 | "|-------|-----------------------------|----------------------------|---|---|\n", 357 | "| Alpha | i,j | a, b | | |\n", 358 | "| Beta | I, J | A, B | | |\n" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": 10, 364 | "metadata": {}, 365 | "outputs": [], 366 | "source": [ 367 | "#RESPONSE" 368 | ] 369 | }, 370 | { 371 | "cell_type": "markdown", 372 | "metadata": {}, 373 | "source": [ 374 | "---\n", 375 | "\n", 376 | "#### **10.** Plot on the same graph the RHF, DFT and FCI binding energies in $H_2$ versus the separation distance. Plot in kcal/mol energy units (1 Hartree = 627.5 kcal/mol)" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": 12, 382 | "metadata": {}, 383 | "outputs": [], 384 | "source": [ 385 | "#Response" 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "metadata": {}, 391 | "source": [ 392 | "#### **11.** Using your results, compare CCSD, UHF and RHF dissociation energies. " 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "metadata": {}, 399 | "outputs": [], 400 | "source": [ 401 | "#RESPONSE" 402 | ] 403 | }, 404 | { 405 | "cell_type": "markdown", 406 | "metadata": {}, 407 | "source": [ 408 | "---\n", 409 | "#### **12.** Comment on the behaviour of RHF with respect to FCI at short (around 0.7 Angstroms) and long distances. For more information, you can read paragraph 3.8.7 from Reference 1 (found below) for a discussion of RHF and UHF solutions. " 410 | ] 411 | }, 412 | { 413 | "cell_type": "markdown", 414 | "metadata": {}, 415 | "source": [ 416 | "#RESPONSE" 417 | ] 418 | }, 419 | { 420 | "cell_type": "markdown", 421 | "metadata": {}, 422 | "source": [ 423 | "---\n", 424 | "#### **13.** Plot the first two $H_2$ molecular orbitals from your RHF and UHF calculations at equilibrium , 0.7 and 5.0 Angstroms. Remember to use the appropriate global settings. Comment on qualitative changes in the shape of the orbitals.\n", 425 | "\n", 426 | "##### You may use the function `generate_orbitals` from the orbital_helper file in the same directory to plot both HOMO and HOMO for the $H_2$ molecule. The syntax is the following:\n", 427 | "```\n", 428 | "from orbital_helper import generate_orbitals\n", 429 | "x, alpha_orbitals, beta_orbitals = generate_orbitals(wfn, [1,2,3])\n", 430 | "#Where the arguments are the wavefunction object and the integer values of the orbitals.\n", 431 | "#The function returns a numpy array with the domain, and a set of lists with alpha and beta orbitals. \n", 432 | "```\n", 433 | "\n", 434 | "##### If you have the package `moly` installed. You may visualize the orbitals in 3D with the following:\n", 435 | "```\n", 436 | "import moly\n", 437 | "fig = moly.Figure(figsize=(300,500))\n", 438 | "fig.add_orbital(\"Name\", wfn, orbital_number, iso, colorscale=\"portland_r\")\n", 439 | "fig.show()\n", 440 | "```\n" 441 | ] 442 | }, 443 | { 444 | "cell_type": "code", 445 | "execution_count": 14, 446 | "metadata": {}, 447 | "outputs": [], 448 | "source": [ 449 | "from orbital_helper import generate_orbitals" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 13, 455 | "metadata": {}, 456 | "outputs": [], 457 | "source": [ 458 | "#RESPONSE" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "---\n", 466 | "#### **14.** Difference between FCI and HF energies is the correlation energy. What is the nature of the correlation energy (dynamic vs non-dynamic) in $H_2$ at equilibrium and long distances? At what distance does the non-dynamic correlation become important?" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": {}, 472 | "source": [ 473 | "#RESPONSE" 474 | ] 475 | }, 476 | { 477 | "cell_type": "markdown", 478 | "metadata": {}, 479 | "source": [ 480 | "---\n", 481 | "#### **15.** Comment on the behaviour of DFT at equilibrium and long distances. What is the reason of DFT failure for bond-breaking?" 482 | ] 483 | }, 484 | { 485 | "cell_type": "markdown", 486 | "metadata": {}, 487 | "source": [ 488 | "#RESPONSE" 489 | ] 490 | }, 491 | { 492 | "cell_type": "markdown", 493 | "metadata": {}, 494 | "source": [ 495 | "---\n", 496 | "#### **Bonus.** From the previously computed energy of a Hydrogen atom with the hybrid B3LYP functional. Compare the energy of the atom computed with HF, B3LYP and the exact energy. Do you see any discrepancy with B3LYP? If so, what is/are the reasons for such discrepancies?" 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": 14, 502 | "metadata": {}, 503 | "outputs": [], 504 | "source": [ 505 | "#RESPONSE" 506 | ] 507 | }, 508 | { 509 | "cell_type": "markdown", 510 | "metadata": {}, 511 | "source": [ 512 | "***\n", 513 | "## Part 2" 514 | ] 515 | }, 516 | { 517 | "cell_type": "markdown", 518 | "metadata": {}, 519 | "source": [ 520 | "Your friend, who is an experimental chemist, seeks your help knowing that you have expertise in running quantum chemistry simulations. Their research group has measured the singlet-triplet gap of ozone recently. They want to see if computational simulations can support their measurement. How will you measure the singlet-triplet gap in ozone?" 521 | ] 522 | }, 523 | { 524 | "cell_type": "markdown", 525 | "metadata": {}, 526 | "source": [ 527 | "Use the ideas from the previous part of this lab and the follwoing hints:\n", 528 | " \n", 529 | " **1.** Assume that the singlet and triplet ozone molecules have the same geometry. \n", 530 | " \n", 531 | " **2.** You will have to optimize the geometry of ozone to start with. Psi4 can let you import geometries from PubChem. The sytax is: `h2o_geometry = psi4.geometry(\"pubchem:water\")`. You may use the common name or its molecular formula. Alternatively, you can use a database such as [CCCBDB](https://cccbdb.nist.gov/). \n", 532 | " \n", 533 | " **3.** Use RHF/6-31G* for simulating the singlet ozone molecule. Use UHF/6-31G* for simulating the triplet ozone molecule. Use the energy difference to compute the gap. \n", 534 | " \n", 535 | " **4.** Write the electronic energies corresponding to singlet and triplet ozone molecules. the singlet-triplet gap in eV, and the $$ value for triplet ozone. Information about spin contamination is given by $$ and can be found close to the end of your calculation (look for Spin Contamination Metric). \n", 536 | " \n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " " 541 | ] 542 | }, 543 | { 544 | "cell_type": "code", 545 | "execution_count": 16, 546 | "metadata": {}, 547 | "outputs": [], 548 | "source": [ 549 | "#Response" 550 | ] 551 | }, 552 | { 553 | "cell_type": "markdown", 554 | "metadata": {}, 555 | "source": [ 556 | "---\n", 557 | "Now, compute the singlet-triplet gap between the $^1\\Delta_g$ and $^3\\Sigma_g$ states of oxygen molecule and report it in eV. Compare the singlet-triplet gap you computed in this lab with the ones availiable in CCBDB. Is it an exact match (http://cccbdb.nist.gov/stgap1.asp)?\n", 558 | "\n", 559 | "\n", 560 | "" 561 | ] 562 | }, 563 | { 564 | "cell_type": "markdown", 565 | "metadata": {}, 566 | "source": [ 567 | "##### Compare the expected $$ with observed $$ and respond: Of all the four cases you have computed so far, which one suffers the most spin contamination?" 568 | ] 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": 17, 573 | "metadata": {}, 574 | "outputs": [], 575 | "source": [ 576 | "#RESPONSE" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "---\n", 584 | "Bonus. Compute the singlet-triplet gap between $^1\\Sigma_g ^+$ and $^3\\Sigma_g ^-$ states of oxygen atom. \n", 585 | " \n", 586 | " \n", 587 | "
\n", 588 | "\n", 589 | "Hint: Start with $^1 \\Delta_g$ geometry. Use the maximum overlap method (MOM) to force the highest beta electron to occupy the second $\\pi^*$ ortibal: ```psi4.set_options({\"MOM_START\":1})``` \n", 590 | " \n", 591 | "
" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": 18, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "#Response\n" 601 | ] 602 | }, 603 | { 604 | "cell_type": "markdown", 605 | "metadata": {}, 606 | "source": [ 607 | "# ---\n", 608 | "## Further Reading:\n", 609 | "\n", 610 | "#### General:\n", 611 | "1. Szabo, A., & Ostlund, N. S. (2012). Modern quantum chemistry: introduction to advanced electronic structure theory. Courier Corporation. \n", 612 | "2. Cramer, Christopher J. Essentials of computational chemistry: theories and models. John Wiley & Sons, 2013. \n", 613 | "3. Krylov. A. Theory and Practice of Molecular Electronic Structure: [link](http://iopenshell.usc.edu/chem545/lectures2016/chem545_2016.pdf)\n", 614 | "4. Sherrill. D. Non-Dynamical (Static) Electron Correlation: Bond Breaking in Quantum Chemistry [link](https://youtu.be/coGVX7HCCQE)\n", 615 | "\n", 616 | "\n", 617 | "#### Bond stretching:\n", 618 | "1. Dutta, Antara, and C. David Sherrill. \"Full configuration interaction potential energy curves for breaking bonds to hydrogen: An assessment of single-reference correlation methods.\" The Journal of chemical physics 118.4 (2003): 1610-1619.\n", 619 | "\n", 620 | "#### Singlet-triplet gaps:\n", 621 | "1. Slipchenko, Lyudmila V., and Anna I. Krylov. \"Singlet-triplet gaps in diradicals by the spin-flip approach: A benchmark study.\" The Journal of chemical physics 117.10 (2002): 4694-4708.\n" 622 | ] 623 | }, 624 | { 625 | "cell_type": "code", 626 | "execution_count": null, 627 | "metadata": {}, 628 | "outputs": [], 629 | "source": [] 630 | } 631 | ], 632 | "metadata": { 633 | "kernelspec": { 634 | "display_name": "Python 3", 635 | "language": "python", 636 | "name": "python3" 637 | }, 638 | "language_info": { 639 | "codemirror_mode": { 640 | "name": "ipython", 641 | "version": 3 642 | }, 643 | "file_extension": ".py", 644 | "mimetype": "text/x-python", 645 | "name": "python", 646 | "nbconvert_exporter": "python", 647 | "pygments_lexer": "ipython3", 648 | "version": "3.8.5" 649 | } 650 | }, 651 | "nbformat": 4, 652 | "nbformat_minor": 4 653 | } 654 | -------------------------------------------------------------------------------- /labs/Bond_Breaking/orbital_helper.py: -------------------------------------------------------------------------------- 1 | 2 | import psi4 3 | import numpy as np 4 | 5 | def generate_orbitals(wfn, orbitals): 6 | """ 7 | Generates alpha/beta orbitals on z axis. 8 | 9 | Parameters 10 | ---------- 11 | wfn: psi4.core.Wavefunction 12 | Wavefunction from calculation 13 | orbitals: List[int], int>0 14 | List with requested orbitals 15 | 16 | Returns 17 | ------- 18 | x: np.ndarray 19 | Numpy array of domain correspoding to z axis. 20 | orbs_out_a, orbs_out_b: List[np.ndarray] 21 | List with requested orbitals on z axis. 22 | """ 23 | 24 | if 0 in orbitals: 25 | raise ValueError("Must provide integer values of orbitals. (i_orb > 0)") 26 | 27 | #Orbital Matrices 28 | Ca = wfn.Ca().clone() 29 | Cb = wfn.Cb().clone() 30 | 31 | #Create Vpot object 32 | lda_functional = psi4.driver.proc.dft.lda_functionals.functional_list["svwn"] 33 | if wfn.same_a_b_orbs() is True: 34 | functional = psi4.driver.dft.build_superfunctional_from_dictionary(lda_functional, 500000, 1, True) 35 | functional[0].allocate() 36 | functional = functional[0] 37 | Vpot = psi4.core.VBase.build(wfn.basisset(), functional, "RV") 38 | Vpot.initialize() 39 | points_func = Vpot.properties()[0] 40 | points_func.set_pointers(Ca) 41 | else: 42 | functional = psi4.driver.dft.build_superfunctional_from_dictionary(lda_functional, 500000, 1, False) 43 | functional[0].allocate() 44 | functional = functional[0] 45 | Vpot = psi4.core.VBase.build(wfn.basisset(), functional, "UV") 46 | Vpot.initialize() 47 | points_func = Vpot.properties()[0] 48 | points_func.set_pointers(Ca, Cb) 49 | 50 | Ca_np = Ca.clone().np 51 | Cb_np = Cb.clone().np 52 | nbf = len(Ca_np) 53 | 54 | a_orbs = { str(i) : np.empty((1,0)) for i in orbitals } 55 | b_orbs = { str(i) : np.empty((1,0)) for i in orbitals } 56 | x_out = np.empty((1,0)) 57 | 58 | for b in range(Vpot.nblocks()): 59 | block = Vpot.get_block(b) 60 | points_func.compute_points(block) 61 | npoints = block.npoints() 62 | 63 | lpos = np.array(block.functions_local_to_global()) 64 | x, y, z = block.x().np, block.y().np, block.z().np 65 | phi = np.array(points_func.basis_values()["PHI"])[:npoints, :lpos.shape[0]] 66 | 67 | #Range specifies number of orbitals requested 68 | for i_orb in orbitals: 69 | if len(lpos) != 0: 70 | Ca_local = Ca_np[lpos, i_orb-1] 71 | Cb_local = Cb_np[lpos, i_orb-1] 72 | orb_a = np.einsum('m, pm -> p', Ca_local, phi, optimize=True) 73 | orb_b = np.einsum('m, pm -> p', Cb_local, phi, optimize=True) 74 | 75 | #Isolate points close to z axis. 76 | mask = np.bitwise_and(np.isclose(x, 0), np.isclose(y, 0)) 77 | if i_orb == orbitals[0]: 78 | x_out = np.append(x_out, z[mask]) 79 | a_orbs[ str(i_orb) ] = np.append( a_orbs[str(i_orb)], orb_a[mask] ) 80 | b_orbs[ str(i_orb) ] = np.append( b_orbs[str(i_orb)], orb_b[mask] ) 81 | 82 | orbs_out_a = [] 83 | orbs_out_b = [] 84 | #Order points according to z 85 | indx = np.argsort(x_out) 86 | x_out = x_out[indx] 87 | for i in orbitals: 88 | orbs_out_a.append( a_orbs[str(i)][indx] ) 89 | orbs_out_b.append( b_orbs[str(i)][indx] ) 90 | 91 | 92 | return x_out, orbs_out_a, orbs_out_b -------------------------------------------------------------------------------- /labs/Bond_Breaking/ozone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Bond_Breaking/ozone.png -------------------------------------------------------------------------------- /labs/Bond_Breaking/restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Bond_Breaking/restricted.png -------------------------------------------------------------------------------- /labs/CationPi/CationPi_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/CationPi/CationPi_student.pdf -------------------------------------------------------------------------------- /labs/CationPi/CationPi_student_ChemCompute.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/CationPi/CationPi_student_ChemCompute.pdf -------------------------------------------------------------------------------- /labs/Getting_Started/WebMO_setup.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Getting_Started/WebMO_setup.pdf -------------------------------------------------------------------------------- /labs/Getting_Started/tutorial_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Getting_Started/tutorial_student.pdf -------------------------------------------------------------------------------- /labs/Hartree_Fock/HF_student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Hartree-Fock Procedure for Approximate Quantum Chemistry\"\"\"\n", 10 | "\n", 11 | "__authors__ = [\"Ashley Ringer McDonald\",\"Dominic A. Sirianni\", \"Tricia D. Shepherd\", \"Sean Garrett-Roe\"]\n", 12 | "__email__ = [\"armcdona@calpoly.edu\", \"sirianni.dom@gmail.com\", \"profshep@icloud.com\", \"sgr@pitt.edu\"]\n", 13 | "__credits__ = [\"Daniel G.A. Smith\"]\n", 14 | "__copyright__ = \"(c) 2008-2020, The Psi4Education Developers\"\n", 15 | "__license__ = \"BSD-3-Clause\"\n", 16 | "__date__ = \"2020-07-28\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "## Hartree-Fock Procedure for Approximate Quantum Chemistry" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "import psi4\n", 33 | "import numpy as np\n", 34 | "from scipy import linalg as splinalg" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "### I. Warm up with the H atom\n", 42 | "By solving the Hydrogen atom Schrödinger equation, we saw that the expression for the different\n", 43 | "energy levels is given by:\n", 44 | "\n", 45 | "$$E_{n} = -\\frac{m_e e^4}{8\\epsilon_0^2h^2n^2}=-\\frac{m_ee^4}{2(4\\pi\\epsilon_0)^2\\hbar^2n^2}\\qquad(1)$$\n", 46 | "\n", 47 | "Atomic units (a.u.) are based on fundamental quantities so that many physical constants have numerical values of 1: \n", 48 | "\n", 49 | "| Symbol | Quantity | Value in a.u. | Value in SI units\n", 50 | "|---|---|---|---|\n", 51 | "| $e$ | electron charge| 1 |$1.602\\times 10^{-19}$ C |\n", 52 | "| $m_e$ | electron mass| 1 |$9.110\\times 10^{-31}$ kg |\n", 53 | "| $\\hbar$ | angular momentum| 1 |$1.055\\times 10^{-34}$ J s |\n", 54 | "| $a_0$ | Bohr radius (atomic distance unit)| 1 |$5.292\\times 10^{-11}$ m |\n", 55 | "| $E$ | Hartree energy (atomic energy unit)| 1 |$4.360\\times 10^{-18}$ J |\n", 56 | "| $4\\pi\\epsilon_0$ | vacuum permittivity| 1 |$1.113\\times 10^{-10}$ C$^2$/J m |. \n", 57 | "\n", 58 | "#### Question: Verify that the two forms of Eq.1 agree." 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "**Answer:** " 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "#### Question: Rewrite $E_n$ in atomic units.\n" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "**Answer:** " 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "#### Calculate: In the cell below, write the formula to compute the exact H atom ground state energy in SI units and explicitly convert from SI to hartrees" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "# ==> Toy Example: The Hydrogen Atom <==\n", 96 | "\n", 97 | "# Define fundamental constants in SI units\n", 98 | "m_e = 9.1093837015e-31 # kg\n", 99 | "e = 1.602176634e-19 # C\n", 100 | "epsilon_0 = 8.8541878128e-12 # F / m\n", 101 | "h = 6.62607015e-34 # J*s\n", 102 | "n = 1\n", 103 | "\n", 104 | "# Define a.u. to SI energy conversion factor (https://en.wikipedia.org/wiki/Hartree)\n", 105 | "hartree2joules = 4.359744650e-18\n", 106 | "\n", 107 | "# Compute ground state energy of H atom in SI units using constants above\n", 108 | "E_1 = #\n", 109 | "\n", 110 | "# Convert to atomic units\n", 111 | "E_1_au = #\n", 112 | "\n", 113 | "print(f'The exact ground state energy of the H atom in SI units is: {E_1} J')\n", 114 | "print(f'The exact ground state energy of the H atom in atomic units is: {E_1_au} Eh')" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "metadata": {}, 120 | "source": [ 121 | "We obtained the Hydrogen atom energy expression above by solving the Schrödinger equation exactly. But what happens if we cannot do this?\n", 122 | "\n", 123 | "That's where Hartree-Fock molecular orbital theory comes in! Just as a test case, let's use Psi4 to compute the Hartree-Fock wavefunction and energy for the Hydrogen atom:\n", 124 | "\n", 125 | "#### Calculate: In the cell below, use psi4 to compute the exact H atom ground state energy in SI units and hartrees" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": { 132 | "scrolled": true 133 | }, 134 | "outputs": [], 135 | "source": [ 136 | "# ==> Compute H atom energy with Hartree-Fock using Psi4 <==\n", 137 | "\n", 138 | "# the H atom has a charge of 0, spin multiplicity of 2 (m_s=1/2)\n", 139 | "# and we place it at the xyz origin (0,0,0)\n", 140 | "\n", 141 | "h_atom = psi4.geometry(\"\"\"\n", 142 | "0 2\n", 143 | "H 0 0 0\n", 144 | "\"\"\")\n", 145 | "\n", 146 | "# specify the basis\n", 147 | "basis = 'd-aug-cc-pv5z'\n", 148 | "\n", 149 | "# set computation options\n", 150 | "psi4.set_options({'basis': basis,\n", 151 | " 'reference': 'rohf',\n", 152 | " 'scf_type': 'pk'})\n", 153 | "\n", 154 | "# compute energy\n", 155 | "e = psi4.energy('scf')\n", 156 | "psi4.core.clean()\n", 157 | "\n", 158 | "print(f\"The Hartree-Fock ground state energy of the H atom in SI units is: {e * psi4.constants.hartree2J} J\")\n", 159 | "print(f\"The Hartree-Fock ground state energy of the H atom in atomic units is: {e} Eh\")" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "\n", 167 | "In this lab activity, you will build and diagonalize the Fock matrix to determine the MO coefficients and energies for a molecule. We will be using the functions of the Psi4 quantum chemistry software package to compute the integrals we need. The following notebook will lead you through setting up your molecule, establishing the basis set, and forming and diagonalizing the Fock matrix. Be sure to run each cell as your proceed through the notebook.\n", 168 | "\n", 169 | "### II. The Hartree-Fock procedure\n", 170 | "The Schrödinger equation has the structure of an eigenvalue equation\n", 171 | "\n", 172 | "$$\\hat{H}|\\psi\\rangle = E|\\psi\\rangle$$\n", 173 | "\n", 174 | "In Hartree-Fock theory, this is reexpresed in terms of the Fock matrix, $F$, a matrix of wavefunction amplitudes for each MO, $C$, and the overlap matrix, $S$,\n", 175 | "\n", 176 | "$$FC = SCE.\\qquad(\\text{2})$$\n", 177 | "\n", 178 | "The Fock matrix for a closed-shell system is \n", 179 | "$$\n", 180 | "F = H + 2J - K\n", 181 | "$$\n", 182 | "where $H$ is the one electron \"core\" Hamiltonian, $J$ is the Coulomb integral matrix, and $K$ is the exchange integral matrix. The definitions of $J$ and $K$ depend on the coefficients, $C$. We see here the central premise of SCF: To get the Fock matrix, we need the coefficient matrix, but to compute the coefficient matrix we need the Fock matrix. When $S$ is not equal to the identity matrix (i.e. the basis is not orthonormal), then this is a pseudo-eigenvalue problem and is even harder to solve. This is our task.\n", 183 | "\n", 184 | "We will\n", 185 | "- review Dirac notation \n", 186 | "- introduce the overlap matrix \n", 187 | "- learn how to build an orthogonalization matrix\n", 188 | "- learn how to calculate the density\n", 189 | "- learn how to calculate the Coulomb and Exchange integral matrices\n", 190 | "- learn how to diagonalize the Fock matrix\n", 191 | "- build an iterative procedure to converge HF energy" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "### III. Orthogonalizing the AO basis set" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "Crash course in Dirac notation:\n", 206 | "\n", 207 | "The wavefunction, $\\psi(x)$ can be represented as a *column vector*, $|\\psi\\rangle$ . The complex conjugate of the wavefunction, $\\psi^*(x)$ is also represented by a vector which is the complex conjugate transpose of $|\\psi\\rangle$. \n", 208 | "\n", 209 | "\\begin{align}\n", 210 | "\\psi(x)&\\rightarrow|\\psi\\rangle\\quad\\text{column vector}\\\\\n", 211 | "\\psi^*(x)&\\rightarrow\\langle\\psi|\\quad\\text{row vector}\n", 212 | "\\end{align}\n", 213 | "\n", 214 | "The normalization of a wavefunction $\\psi(x)$ is an integral\n", 215 | "\n", 216 | "$$\\int\\psi^*(x)\\psi(x)\\;\\mathrm{d}x=1.$$\n", 217 | "\n", 218 | "In Dirac notation, it is replaced with a vector equation\n", 219 | "\n", 220 | "$$\\langle\\psi|\\psi\\rangle=1.$$\n", 221 | "\n", 222 | "The orthogonality of two wavefunctions, $\\psi_i(x)$ and $\\psi_j(x)$, which, in integral notation, is\n", 223 | "\n", 224 | "$$\\int\\psi_i^*(x)\\psi_j(x)\\;\\mathrm{d}x=0,$$\n", 225 | "\n", 226 | "becomes, in Dirac notation,\n", 227 | "\n", 228 | "$$\\langle\\psi_i|\\psi_j\\rangle=0.$$\n", 229 | " \n", 230 | "#### Calculate: Define two vectors, `phi1` and `phi2`, with two elements each, that are normalized, in the sense $\\langle\\phi_i|\\phi_i\\rangle=1$, and orthogonal in sense that $\\langle\\phi_i|\\phi_j\\rangle=0$. Recall that one defines a vector `v` with the two elements `1` and `2` through the command `v=np.array([1,2])`." 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# define a first basis vector and a second, orthogonal vector\n", 240 | "phi1 = #\n", 241 | "phi2 = #\n", 242 | "\n", 243 | "print(F'Phi1: {phi1}')\n", 244 | "print(F'Phi2: {phi2}')\n", 245 | "print()#empty line" 246 | ] 247 | }, 248 | { 249 | "cell_type": "markdown", 250 | "metadata": {}, 251 | "source": [ 252 | "Note: Numpy commands for vector operations assuming `v`= vector:\n", 253 | "\n", 254 | "inner product (scalar product) of two vectors: `v.dot(v)`\n", 255 | "\n", 256 | "complex conjugate of a vector: `v.conj()`\n", 257 | "\n", 258 | "inner product of $v^\\dagger v$: `v.conj().dot(v)`\n", 259 | "\n", 260 | "\n", 261 | "#### Calculate: Use the commands demonstrated above to show that `phi1` is normalized." 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "# calculate normalization\n", 271 | "\n", 272 | "phi1_norm = #\n", 273 | "\n", 274 | "print(F' = {phi1_norm}')" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "metadata": {}, 280 | "source": [ 281 | "#### Calculate: Similarly, enter the three remaining calculations below to show that `phi1` and `phi2` are orthonormal." 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "# calculate normalization\n", 291 | "\n", 292 | "print(F' = {}')\n", 293 | "\n", 294 | "# calculate orthogonality\n", 295 | "\n", 296 | "print(F' = {}')\n", 297 | "\n", 298 | "print(F' = {}')" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "### IV. The overlap integrals\n", 306 | "For a set of basis functions, $\\phi_i(\\tau)$, where $\\tau$ is a shorthand for all the coordinates of all the particles, we can calculate the overlap integrals between the basis functions in the following way\n", 307 | "\n", 308 | "$$S_{ij}=\\int {\\rm d}\\tau\\; \\phi_i^*(\\tau)\\phi_j(\\tau).$$\n", 309 | "\n", 310 | "#### Question: Define $S_{ij}$ using Dirac notation." 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "metadata": {}, 316 | "source": [ 317 | "**Answer:** " 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "metadata": {}, 323 | "source": [ 324 | "#### Question: For an orthonormal basis, what does the overlap integral array, `S`, look like?" 325 | ] 326 | }, 327 | { 328 | "cell_type": "markdown", 329 | "metadata": {}, 330 | "source": [ 331 | "**Answer:** " 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "metadata": {}, 337 | "source": [ 338 | "#### Calculate: the terms `S_ij` using the basis vectors `phi1` and `phi2`. " 339 | ] 340 | }, 341 | { 342 | "cell_type": "code", 343 | "execution_count": null, 344 | "metadata": {}, 345 | "outputs": [], 346 | "source": [ 347 | "# calculate the overlap (inner product) of the vectors \n", 348 | "S_11 = #\n", 349 | "S_12 = \n", 350 | "S_21 = \n", 351 | "S_22 = \n", 352 | "print('The ij elements of S:')\n", 353 | "print(S_11,S_12)\n", 354 | "print(S_21,S_22)\n", 355 | "print()\n" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "### V. Constructing the overlap matrix\n", 363 | "These overlap integrals, $S_{ij}$, can be interpreted as the elements on the $i$-th row and $j$-th column of a matrix, $S$. Let's propose a matrix, $S$, made of the overlap integrals $S_{ij}$. We can build $S$ systematically in the following way. First, make a matrix, $B$, composed of our basis vectors as columns,\n", 364 | "\n", 365 | "$$ B = \\left(\\begin{array}{ccc}|& |&|\\\\ \\phi_1 &\\phi_2&\\phi_3\\\\|&|&|\\end{array}\\right).$$\n", 366 | "\n", 367 | "We will use the symbol $\\dagger$ to indicate the complex conjugate of the transpose of a matrix. So\n", 368 | "\n", 369 | "$$B^\\dagger = (B^T)^*.$$" 370 | ] 371 | }, 372 | { 373 | "cell_type": "code", 374 | "execution_count": null, 375 | "metadata": {}, 376 | "outputs": [], 377 | "source": [ 378 | "# construct the overlap matrix from matrix of basis vectors\n", 379 | "vector_length = phi1.size #length of the vector space\n", 380 | "phi1_column = phi1.reshape(vector_length,1) #this makes phi a column vector\n", 381 | "phi2_column = phi2.reshape(vector_length,1)\n", 382 | "\n", 383 | "# put together (concatenate) the vectors into the matrix B\n", 384 | "B = np.concatenate((phi1_column,phi2_column),axis=1)\n", 385 | "print(F'The matrix B:\\n{B}')\n", 386 | "\n", 387 | "B_dagger = B.conj().T\n", 388 | "print(F'The matrix B^\\dagger:\\n{B_dagger}')" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "metadata": {}, 394 | "source": [ 395 | "Now, multiplying the rows of $B^\\dagger$ by the columns of $B$ (normal matrix multiplication) produces a matrix of the overlap integrals in the correct locations. we defined to be the matrix $S$.\n", 396 | "\n", 397 | "$$B^\\dagger B =\\left(\\begin{array}{ccc}-& \\phi_1^*&-\\\\-& \\phi_2^* &-\\\\-&\\phi_3&-\\end{array}\\right) \\left(\\begin{array}{ccc}|& |&|\\\\ \\phi_1 &\\phi_2&\\phi_3\\\\|&|&|\\end{array}\\right)\\equiv S.$$" 398 | ] 399 | }, 400 | { 401 | "cell_type": "markdown", 402 | "metadata": {}, 403 | "source": [ 404 | "Note: Numpy commands for matrix operations assuming `v` = vector, and `M` = matrix:\n", 405 | "\n", 406 | "matrix vector product: `M.dot(v)`\n", 407 | "\n", 408 | "matrix matrix product: `M.dot(M)`\n", 409 | "\n", 410 | "matrix complex conjugate: `M.conj()`\n", 411 | "\n", 412 | "matrix transpose: `M.T`" 413 | ] 414 | }, 415 | { 416 | "cell_type": "markdown", 417 | "metadata": {}, 418 | "source": [ 419 | "#### Calculate: Use `B` and `B_dagger` and the matrix rules above to calculate the matrix `S`." 420 | ] 421 | }, 422 | { 423 | "cell_type": "code", 424 | "execution_count": null, 425 | "metadata": {}, 426 | "outputs": [], 427 | "source": [ 428 | "# calculate S from matrix of basis vectors\n", 429 | "S = #\n", 430 | "print(F'The matrix of eigenvectors in columns B =\\n {B} \\n\\nand S = B^\\dagger B =\\n{S}\\n')" 431 | ] 432 | }, 433 | { 434 | "cell_type": "markdown", 435 | "metadata": {}, 436 | "source": [ 437 | "### VI. Einstein implicit summation notation\n", 438 | "Matrix multiplication is defined through\n", 439 | "\n", 440 | "$$(AB)_{pq}= \\sum_i A_{p,i}B_{i,q}\\qquad\\text{explicit summation}$$\n", 441 | "\n", 442 | "Note that there is a repeated index, $i$, in the summation. In implicit summation notation, Einstein notation, we do not write the $\\sum$ and treat the summation as understood. \n", 443 | "\n", 444 | "$$(AB)_{pq}=A_{p,i}B_{i,q}\\qquad\\text{implicit summation}$$\n", 445 | "\n", 446 | "Using implicit summation for the case at hand, $B^\\dagger B$ gives\n", 447 | "\n", 448 | "$$S_{pq}=(B^\\dagger B)_{pq}= (B^\\dagger)_{p,i}B_{i,q}\\qquad\\text{implicit summation}$$\n", 449 | "$$= (B^*)_{i,p}B_{i,q}\\qquad\\text{implicit summation}$$\n", 450 | "\n", 451 | "where $B^*$ is the complex conjugate of $B$ (no transpose). Note that the two sets of indices, $(i,p)$ and $(i,q)$, in the input matrices become one set, $(p,q)$, in the product. \n", 452 | "\n", 453 | "*(Python programming aside: There is a convenient function in `numpy` called `einsum()`, which is one of the crown jewels of the numpy library. In short, `einsum` lets you perform various combinations of multiplying, summing, and transposing matrices very efficiently. A good tutorial about `einsum` can be found at http://ajcr.net/Basic-guide-to-einsum/. To specify the operations you want to do, you use the **Ein**stein **Sum**mation convention.)*\n", 454 | "\n", 455 | "To calculate the sum \n", 456 | "\n", 457 | "$$S_{pq} = (B^*)_{i,p}B_{i,q}\\qquad\\text{implicit summation}$$\n", 458 | "\n", 459 | "use `S=np.einsum('ip,iq->pq',B.conj(),B)`, as demonstrated below. We will use `einsum()` in several places.\n", 460 | "\n", 461 | "Let's try this with a simple basis set of two (perhaps) orthonormal vectors." 462 | ] 463 | }, 464 | { 465 | "cell_type": "markdown", 466 | "metadata": {}, 467 | "source": [ 468 | "#### Question: Describe how the notation of the `np.einsum` command coorelates to the implicit summation formula written above." 469 | ] 470 | }, 471 | { 472 | "cell_type": "markdown", 473 | "metadata": {}, 474 | "source": [ 475 | "****Answer**** " 476 | ] 477 | }, 478 | { 479 | "cell_type": "markdown", 480 | "metadata": {}, 481 | "source": [ 482 | "#### Calculate: Use the function `np.einsum()` to calculate the matrix `S`, and confirm that your answer is the same as above." 483 | ] 484 | }, 485 | { 486 | "cell_type": "code", 487 | "execution_count": null, 488 | "metadata": {}, 489 | "outputs": [], 490 | "source": [ 491 | "# calculate S from Einstein sum\n", 492 | "S = #\n", 493 | "print(F'S from Einstein notation:\\n{S}')" 494 | ] 495 | }, 496 | { 497 | "cell_type": "markdown", 498 | "metadata": {}, 499 | "source": [ 500 | "#### Question: Propose a different orthonormal basis, modify `phi1` and `phi2`, and verify that `S` still has the same form. There are infinitely many choices. It isn't complex... or *is* it?!" 501 | ] 502 | }, 503 | { 504 | "cell_type": "markdown", 505 | "metadata": {}, 506 | "source": [ 507 | "#### Question: Propose what will happen to `S` if the vectors are not orthonormal. \n", 508 | "#### Calculate: Test your prediction!" 509 | ] 510 | }, 511 | { 512 | "cell_type": "markdown", 513 | "metadata": {}, 514 | "source": [ 515 | "### VII. Gaussian atomic orbital basis set" 516 | ] 517 | }, 518 | { 519 | "cell_type": "markdown", 520 | "metadata": {}, 521 | "source": [ 522 | "H$_2$O is a small but interesting molecule to use in our exploration. \n", 523 | "\n", 524 | "#### Question: Answer the following questions for the H$_2$O molecule:\n", 525 | "##### How many electrons are there in total?\n", 526 | "##### How many occupied molecular orbitals would you expect?" 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "Before we can begin to implement the HF procedure, we need to specifcy the molecule and basis set that we will be using. We will also set the memory usage for our calcluation and the output file name. " 534 | ] 535 | }, 536 | { 537 | "cell_type": "code", 538 | "execution_count": null, 539 | "metadata": {}, 540 | "outputs": [], 541 | "source": [ 542 | "# ==> Set Basic Psi4 Options <==\n", 543 | "# Memory specification\n", 544 | "psi4.set_memory('500 MB')\n", 545 | "numpy_memory = 2 # No NumPy array can exceed 2 MB in size\n", 546 | "\n", 547 | "# set output file\n", 548 | "psi4.core.set_output_file('output.dat', False)\n", 549 | "\n", 550 | "# specify the basis\n", 551 | "# basis = 'cc-pvdz'\n", 552 | "basis = 'sto-3g'\n", 553 | "\n", 554 | "\n", 555 | "# Set computation options\n", 556 | "psi4.set_options({'basis': basis,\n", 557 | " 'scf_type': 'pk',\n", 558 | " 'e_convergence': 1e-8})\n", 559 | "\n", 560 | "\n", 561 | "# ==> Define Molecule <==\n", 562 | "# Define our model of water -- \n", 563 | "# we will distort the molecule later, which may require C1 symmetry\n", 564 | "mol = psi4.geometry(\"\"\"\n", 565 | "O\n", 566 | "H 1 1.1\n", 567 | "H 1 1.1 2 104\n", 568 | "symmetry c1\n", 569 | "\"\"\")\n", 570 | "\n", 571 | "# compute energy\n", 572 | "\n", 573 | "SCF_E_psi = psi4.energy('scf')\n", 574 | "psi4.core.clean()\n", 575 | "\n", 576 | "print(f\"The Hartree-Fock ground state energy of the water is: {SCF_E_psi} Eh\")" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "Next, we need to build the wavefunction from the basis functions. We store the wavefunction in a variable called `wfn`. We use the function `nalpha()` provided by the wavefunction object we created above, `wfn`, to determine the number of orbitals with spin alpha, which will be doubly occupied orbitals for close shelled systems. We save this answer as a variable called `ndocc` (number of doubly occupied orbitals). \n", 584 | "\n", 585 | "#### Calculate: Execute the code below and confirm that the number of doubly occupied orbitals matches your expectation for the molecule you chose." 586 | ] 587 | }, 588 | { 589 | "cell_type": "code", 590 | "execution_count": null, 591 | "metadata": {}, 592 | "outputs": [], 593 | "source": [ 594 | "# ==> Compute static 1e- and 2e- quantities with Psi4 <==\n", 595 | "wfn = psi4.core.Wavefunction.build(mol, psi4.core.get_global_option('basis'))\n", 596 | "\n", 597 | "# number of spin alpha orbitals (doubly occupied for closed-shell systems)\n", 598 | "ndocc = wfn.nalpha()\n", 599 | "nbf = wfn.basisset().nbf()\n", 600 | "\n", 601 | "print(F'Number of occupied orbitals: {ndocc}')\n", 602 | "print(F'Number of basis functions: {nbf}') " 603 | ] 604 | }, 605 | { 606 | "cell_type": "markdown", 607 | "metadata": {}, 608 | "source": [ 609 | "Next we will examine the atomic orbital basis set. To do this, we have to set up a data structure, called a class, to calculate the molecular integrals. (Psi4 will do the nasty calculus for us.) We will call this data structure `mints` (Molecular INTegralS). We use the function `ao_overlap` to calculate the overlap integrals between all the AO basis functions. We cast the result to a numpy array called `S`. \n", 610 | "\n", 611 | "_(Python programming aside: `asarray()` is a special case of `array()` that does not copy arrays when compatible and converts array subclasses to base class ndarrays. https://stackoverflow.com/questions/14415741/what-is-the-difference-between-numpys-array-and-asarray-functions)_ " 612 | ] 613 | }, 614 | { 615 | "cell_type": "code", 616 | "execution_count": null, 617 | "metadata": {}, 618 | "outputs": [], 619 | "source": [ 620 | "# Construct a molecular integrals object\n", 621 | "mints = psi4.core.MintsHelper(wfn.basisset())\n", 622 | "\n", 623 | "# Overlap matrix as a psi4 Matrix object\n", 624 | "S_matrix = mints.ao_overlap()\n", 625 | "\n", 626 | "# Overlap matrix converted into an ndarray\n", 627 | "S = np.asarray(S_matrix) \n", 628 | "\n", 629 | "print(F'Shape of S is {S.shape}')" 630 | ] 631 | }, 632 | { 633 | "cell_type": "markdown", 634 | "metadata": {}, 635 | "source": [ 636 | "#### Question: Explain the shape (number of rows and columns) of `S` in terms of the AO basis set we chose." 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "metadata": {}, 642 | "source": [ 643 | "Examine the contents of `S`." 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": null, 649 | "metadata": {}, 650 | "outputs": [], 651 | "source": [ 652 | "print(S) #the full matrix may be somewhat hard to read based on the basis set" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": null, 658 | "metadata": {}, 659 | "outputs": [], 660 | "source": [ 661 | "# Look at the first few elements\n", 662 | "def peak(S,nrows=4,ncols=4):\n", 663 | " print(F'Here is a peak at the first {nrows} x {ncols} elements of the matrix:\\n{S[:nrows,:ncols]}')\n", 664 | " \n", 665 | "peak(S)" 666 | ] 667 | }, 668 | { 669 | "cell_type": "markdown", 670 | "metadata": {}, 671 | "source": [ 672 | "#### Question: Based on your observations of `S` in the AO basis, answer the following questions\n", 673 | "##### What do the diagonal elements of `S` indicate?\n", 674 | "##### What do the off-diagonal elements of `S` indicate?\n", 675 | "##### Does the Gaussian atomic orbital basis set form an orthonormal basis? " 676 | ] 677 | }, 678 | { 679 | "cell_type": "markdown", 680 | "metadata": {}, 681 | "source": [ 682 | "We can perform this test programmatically as well, with a few python tricks. Construct an array of the same size as the overlap array (`S`) that has 1's along the diagonal and 0's everywhere else. Then compare that array to the `S` array to determine if the AO basis is orthonormal." 683 | ] 684 | }, 685 | { 686 | "cell_type": "code", 687 | "execution_count": null, 688 | "metadata": {}, 689 | "outputs": [], 690 | "source": [ 691 | "# example of testing for orthonormality\n", 692 | "\n", 693 | "# define a function\n", 694 | "def isBasisOrthonormal(S):\n", 695 | " # get the number of rows of S\n", 696 | " size_S = S.shape[0] \n", 697 | " \n", 698 | " # construct an identity matrix, I -- \"eye\", get it?!? Ha ha! Math is so funny!\n", 699 | " identity_matrix = np.eye(size_S) \n", 700 | "\n", 701 | " # are all elements of S numerically close to the identity matrix? \n", 702 | " # We won't test for equality because there can be very small numerical \n", 703 | " # differences that we don't care about\n", 704 | " orthonormal_check = np.allclose(S, identity_matrix)\n", 705 | "\n", 706 | " print(F'Q:(T/F) The AO basis is orthonormal? A: {orthonormal_check}')\n", 707 | " return orthonormal_check\n", 708 | "\n", 709 | "# use the function\n", 710 | "isBasisOrthonormal(S)" 711 | ] 712 | }, 713 | { 714 | "cell_type": "markdown", 715 | "metadata": {}, 716 | "source": [ 717 | "#### Question: Does the result agree with what you determined above? Explain." 718 | ] 719 | }, 720 | { 721 | "cell_type": "markdown", 722 | "metadata": {}, 723 | "source": [ 724 | "### VIII. An orthogonalization matrix\n", 725 | "Recall that if we had used the hydrogen atom wavefunctions as our basis set, the AO wavefunctions would all be orthonormal. Since we used a basis set of Gaussian wavefuctions, this may not be the case. We will now introduce some tools to fix it!\n", 726 | "\n", 727 | "Since our AO basis set was not orthonormal, we seek to construct an orthogonalization matrix, $A$, such that ${\\bf A}^{\\dagger}{\\bf SA} = {\\bf 1}$. \n", 728 | "\n", 729 | "**Motivation:** If ${\\bf A}$ and ${\\bf S}$ were real numbers $a$ and $s$ (not matrices), this would be simple to solve. First, $a^\\dagger=a$ because a real number is the same as its hermitian transpose. By simple algebra we can solve for a,\n", 730 | "\n", 731 | "$$a^\\dagger s a=1$$\n", 732 | "$$a^\\dagger s a=a s a=a^2s=1$$\n", 733 | "$$\\Rightarrow{}a=s^{-1/2}$$ \n", 734 | "\n", 735 | "In linear algebra (with matrices instead of numbers) this is more complicated, but numpy and the mints class can take care of the details for us! Leaving out the details, we will calculate\n", 736 | "\n", 737 | "$${\\bf A}={\\bf S}^{-1/2}$$.\n", 738 | "\n", 739 | "#### Calculate: Use the function `np.linalg.inv()` to calculate the inverse of `S`, and the function `splinalg.sqrtm()` to take its (matrix) square root. Execute the code below and examine the matrix `A`." 740 | ] 741 | }, 742 | { 743 | "cell_type": "code", 744 | "execution_count": null, 745 | "metadata": {}, 746 | "outputs": [], 747 | "source": [ 748 | "# ==> Construct AO orthogonalization matrix A <==\n", 749 | "\n", 750 | "# inverse of S using np.linalg.inv\n", 751 | "# matrix square root of the inverse of S using splinalg.sqrtm\n", 752 | "\n", 753 | "A = #\n", 754 | "\n", 755 | "peak(A)" 756 | ] 757 | }, 758 | { 759 | "cell_type": "markdown", 760 | "metadata": {}, 761 | "source": [ 762 | "#### Question: What do you observe about the elements of `A`? Is the matrix real or complex? Is the matrix symmetric or not?" 763 | ] 764 | }, 765 | { 766 | "cell_type": "markdown", 767 | "metadata": {}, 768 | "source": [ 769 | "Our basis set $B$ is not orthonormal, so we want to take linear combinations of its columns to make a new basis set, $B'$, that is orthonormal. We define a new matrix, $A$, in terms of that transformation,\n", 770 | "\n", 771 | "$$B' = BA.$$\n", 772 | "\n", 773 | "The new overlap matrix will be\n", 774 | "\n", 775 | "\\begin{align}\n", 776 | " S' &= B'^\\dagger B',\\\\\n", 777 | " &= (BA)^\\dagger (BA),\\\\\n", 778 | "&= A^\\dagger B^\\dagger BA,\\\\\n", 779 | "&= A^\\dagger S A.\n", 780 | "\\end{align}\n", 781 | "The matrix $A$ makes the proper linear combination of the columns of $B$ and $A^\\dagger$ makes the linear combinations of the rows of $B^\\dagger$. This is a very common structure of transformation matrices. Because $S$ is real and symmetric, $A$ is also real and symmetric, so $A^\\dagger=A$. The transformation becomes simply\n", 782 | "\n", 783 | "$$S' = A S A.$$" 784 | ] 785 | }, 786 | { 787 | "cell_type": "markdown", 788 | "metadata": {}, 789 | "source": [ 790 | "#### Calculate: Use the orthogonalization matrix `A` to transform the overlap matrix, `S`. Check the transformed overlap matrix, `S_p`, to make sure it represents an orthonormal basis." 791 | ] 792 | }, 793 | { 794 | "cell_type": "code", 795 | "execution_count": null, 796 | "metadata": {}, 797 | "outputs": [], 798 | "source": [ 799 | "# Transform S with A and assign the result to S_p\n", 800 | "\n", 801 | "S_p = #\n", 802 | "\n", 803 | "isBasisOrthonormal(S_p)" 804 | ] 805 | }, 806 | { 807 | "cell_type": "markdown", 808 | "metadata": {}, 809 | "source": [ 810 | "#### Question: The product A S A does not take the complex conjugate transpose of A. What conditions (properties of A) make that ok?" 811 | ] 812 | }, 813 | { 814 | "cell_type": "markdown", 815 | "metadata": {}, 816 | "source": [ 817 | "### IX. The Fock Matrix Transformed to an Orthonormal Basis\n", 818 | "We can now return to Eq.2 using our orthogonalization matrix $A$. A common linear algebra trick is to \"insert one.\" In this case, the matrix $A$ times its inverse is, by definition the identity matrix, $AA^{-1}={\\bf1}$. We can put that factor of one anywhere in an equation that is useful to us, and, then, typically we regroup terms in a way we want. In this case, we insert one bewteen $FC$ and $SC$.\n", 819 | "\n", 820 | "\\begin{align}\n", 821 | "FC&=SCE\\\\\n", 822 | "F({\\bf1})C&=S({\\bf1})CE\\\\\n", 823 | "FAA^{-1}C&=SAA^{-1}CE\n", 824 | "\\end{align}\n", 825 | "Multiplying on the left by $A$ then gives\n", 826 | "\n", 827 | "$$\n", 828 | "AFAA^{-1}C=ASAA^{-1}CE\n", 829 | "$$\n", 830 | "\n", 831 | "We can recognize the transformation $S'=ASA$ on the right hand side and similarly define $F'=AFA$ on the left hand side. Lastly, we define a transformed coefficient matrix, $C'=A^{-1}C$. Our transformed Fock equation reads\n", 832 | "\n", 833 | "\\begin{align}\n", 834 | "F'C'&=S'C'E,\\\\\n", 835 | "&=C'E.\n", 836 | "\\end{align}\n", 837 | "\n", 838 | "The last line follows because $S'={\\bf1}$ in our new basis. We now have an eigenvalue problem that we can solve by matrix diagonalization.\n", 839 | "\n", 840 | "In the expression\n", 841 | "\n", 842 | "$$C'=A^{-1}C,$$\n", 843 | "\n", 844 | "the matrix $A^{-1}$ transforms the coefficients, $C$, into the orthogonalized basis set. We will also need a way to transform those coefficients, $C'$, back to the original AO basis.\n", 845 | "\n", 846 | "#### Question: Based on the definition of $C'$, propose a definition of $C$ in terms of $A$ and $C'$. Justify your equation." 847 | ] 848 | }, 849 | { 850 | "cell_type": "markdown", 851 | "metadata": {}, 852 | "source": [ 853 | "**Answer:**" 854 | ] 855 | }, 856 | { 857 | "cell_type": "markdown", 858 | "metadata": {}, 859 | "source": [ 860 | "### X. Initial guess for the Fock Matrix is the one electron Hamiltonian\n", 861 | "To get the Fock matrix, we need the coefficient matrix, but to compute the coefficient matrix we need the Fock matrix. So we start with a guess for the Fock matrix, which is the core Hamiltonian matrix." 862 | ] 863 | }, 864 | { 865 | "cell_type": "code", 866 | "execution_count": null, 867 | "metadata": {}, 868 | "outputs": [], 869 | "source": [ 870 | "# Build core Hamiltonian\n", 871 | "T = np.asarray(mints.ao_kinetic())\n", 872 | "V = np.asarray(mints.ao_potential())\n", 873 | "H = T + V" 874 | ] 875 | }, 876 | { 877 | "cell_type": "markdown", 878 | "metadata": {}, 879 | "source": [ 880 | "#### Calculate: In the cell below, use the core Hamiltonian matrix as your initial guess for the Fock matrix. Transform it with the same A matrix you used above. To calculate the eigenvalues, `vals`, and eigenvectors, `vecs`, of matrix `M` using `vals, vecs = np.linalg.eigh(M)`." 881 | ] 882 | }, 883 | { 884 | "cell_type": "code", 885 | "execution_count": null, 886 | "metadata": {}, 887 | "outputs": [], 888 | "source": [ 889 | "# Guess for the Fock matrix\n", 890 | "\n", 891 | "# Transformed Fock matrix, F_p\n", 892 | "\n", 893 | "# Diagonalize F_p for eigenvalues & eigenvectors with NumPy\n" 894 | ] 895 | }, 896 | { 897 | "cell_type": "markdown", 898 | "metadata": {}, 899 | "source": [ 900 | "#### Calculate: Display, i.e., `print`, the coefficent matrix and confirm it the correct size" 901 | ] 902 | }, 903 | { 904 | "cell_type": "markdown", 905 | "metadata": {}, 906 | "source": [ 907 | "Now that we have the coefficents in the transformed basis, we need to go back and get the coefficients in the original AO basis.\n", 908 | "\n", 909 | "#### Calculate: Use `A` and the formula you proposed previously to transform the coefficient matrix back to the AO basis. Confirm that the resulting matrix appears reasonable, i.e., similar size and magnitude" 910 | ] 911 | }, 912 | { 913 | "cell_type": "code", 914 | "execution_count": null, 915 | "metadata": {}, 916 | "outputs": [], 917 | "source": [ 918 | "# Transform the coefficient matrix back into AO basis\n" 919 | ] 920 | }, 921 | { 922 | "cell_type": "markdown", 923 | "metadata": {}, 924 | "source": [ 925 | "### XI. The Density Matrix\n", 926 | "Recall, the Fock matrix is \n", 927 | "\n", 928 | "$$\n", 929 | "F = H + 2J - K\n", 930 | "$$\n", 931 | "where $H$ is the one electron \"core\" Hamiltonian, $J$ is the Coulomb integral matrix, and $K$ is the exchange integral matrix. The HF energy can be expressed in explicit terms of one and two electron integrals\n", 932 | "$$\n", 933 | "E_{HF} = \\sum_i^{elec}\\langle i|h_i|i\\rangle + \\sum_{i>j}^{elec}[ii|jj]-[ij|ji]\n", 934 | "$$\n", 935 | "Expanding the orbitals in terms of basis functions, we find\n", 936 | "$$\n", 937 | "[ii|jj]=\\sum_{pqrs}c^*_{pi}c_{qi}c^*_{rj}c_{sj}\\int{\\rm d}\\tau\\; \\phi_p^*(1)\\phi_q(1)\\frac{1}{r_{ij}}\\phi_r^*(2)\\phi_s(2)\\qquad{\\text{(3)}}\n", 938 | "$$" 939 | ] 940 | }, 941 | { 942 | "cell_type": "markdown", 943 | "metadata": {}, 944 | "source": [ 945 | "\n", 946 | "First, look at the coefficients in Eq.3. They come in two pairs of complex-conjugates, $c^*_{pi}c_{qi}$ and $c^*_{ri}c_{si}$. The diagonal terms, when $p=q$ for example, are the probability of some basis function $p$ contributing to the MO $i$. We will sum each term over the occupied orbitals, $i$, to form the \"density matrix\"\n", 947 | "$$\n", 948 | "D_{pq}=\\sum_i^{occ} c^*_{pi}c_{qi}.\n", 949 | "$$\n", 950 | "\n", 951 | "We are going to construct the density matrix from the occupied orbitals. To get a matrix of just the occupied orbitals, use the coefficient matrix in the original AO basis, and take a slice to include all rows and just the columns that represent the occupied orbitals." 952 | ] 953 | }, 954 | { 955 | "cell_type": "code", 956 | "execution_count": null, 957 | "metadata": { 958 | "scrolled": true 959 | }, 960 | "outputs": [], 961 | "source": [ 962 | "# Grab occupied orbitals (recall: ndocc is the number of doubly occupied orbitals we found earlier)\n", 963 | "C_occ = C[:, :ndocc]\n", 964 | "print(F'The shape of C_occ is {C_occ.shape}')" 965 | ] 966 | }, 967 | { 968 | "cell_type": "markdown", 969 | "metadata": {}, 970 | "source": [ 971 | "#### Calculate: Build the density matrix, `D`, from the occupied orbitals, `C_occ`, using the function `np.einsum()`. (Recall Section VI.)" 972 | ] 973 | }, 974 | { 975 | "cell_type": "code", 976 | "execution_count": null, 977 | "metadata": {}, 978 | "outputs": [], 979 | "source": [ 980 | "# Build density matrix from occupied orbitals\n", 981 | "\n", 982 | "D = #\n", 983 | "\n", 984 | "print(F'The shape of D is {D.shape}')" 985 | ] 986 | }, 987 | { 988 | "cell_type": "markdown", 989 | "metadata": {}, 990 | "source": [ 991 | "### XII. Coulomb and Exchange Integrals and the SCF Energy\n", 992 | "\n", 993 | "The integral on the right of Eq.3 is super important. It has four indicies, $p,q,r,s$, so formally it is a tensor. It accounts for the repulsion between pairs of electrons, so it is called the electron repulsion integral tensor, $I$,\n", 994 | "$$\n", 995 | "I_{pqrs} = \\int{\\rm d}\\tau\\; \\phi_p^*(1)\\phi_q(1)\\frac{1}{r_{ij}}\\phi_r^*(2)\\phi_s(2).\n", 996 | "$$\n", 997 | "First, we can build the electron-repulsion integral (ERI) tensor, which stores the electron repulsion between the atomic orbital wavefunctions. Mints does all the work for us!" 998 | ] 999 | }, 1000 | { 1001 | "cell_type": "code", 1002 | "execution_count": null, 1003 | "metadata": {}, 1004 | "outputs": [], 1005 | "source": [ 1006 | "# Build electron repulsion integral (ERI) Tensor\n", 1007 | "I = np.asarray(mints.ao_eri())" 1008 | ] 1009 | }, 1010 | { 1011 | "cell_type": "markdown", 1012 | "metadata": {}, 1013 | "source": [ 1014 | "Eq.3 can be expressed in terms of $I$ and $D$ as\n", 1015 | "\\begin{align}\n", 1016 | "[ii|jj] &= \\sum_{pqrs}D_{pq}D_{rs}I_{pqrs},\\\\\n", 1017 | "&=\\sum_{pq}D_{pq}\\sum_{rs}D_{rs}I_{pqrs}.\n", 1018 | "\\end{align}\n", 1019 | "The term $\\sum_{rs}D_{rs}I_{pqrs}$ is the effective repulsion felt by one electron due to the other electrons in the system. This term is the Coulomb integral matrix\n", 1020 | "$$\n", 1021 | "J_{pq}=\\sum_{rs}D_{rs}I_{pqrs}.\n", 1022 | "$$" 1023 | ] 1024 | }, 1025 | { 1026 | "cell_type": "markdown", 1027 | "metadata": {}, 1028 | "source": [ 1029 | "#### Calculate: Define J in terms of the density matrix, `D`, and the electron repulsion integral tensor, `I`, using `np.einsum()`." 1030 | ] 1031 | }, 1032 | { 1033 | "cell_type": "code", 1034 | "execution_count": null, 1035 | "metadata": {}, 1036 | "outputs": [], 1037 | "source": [ 1038 | "#Define J" 1039 | ] 1040 | }, 1041 | { 1042 | "cell_type": "markdown", 1043 | "metadata": {}, 1044 | "source": [ 1045 | "Similarly, \n", 1046 | "\\begin{align}\n", 1047 | "[ij|ji] &= \\sum_{pqrs}D_{ps}D_{rq}I_{pqrs},\\\\\n", 1048 | " &= \\sum_{ps}D_{ps}\\sum_{rq}D_{rq}I_{pqrs}.\n", 1049 | "\\end{align}\n", 1050 | "corrects the repulsion due to electrons \"avoiding each other\" due to their Fermionic (antisymmetric w.r.t. exchange) character. This term is the exchange integral matrix\n", 1051 | "$$\n", 1052 | "K_{ps}=\\sum_{rq}D_{rq}I_{pqrs}.\n", 1053 | "$$" 1054 | ] 1055 | }, 1056 | { 1057 | "cell_type": "markdown", 1058 | "metadata": {}, 1059 | "source": [ 1060 | "#### Calculate: Define K in terms of the density matrix, `D`, and the electron repulsion integral tensor, `I`, using `einsum()`. " 1061 | ] 1062 | }, 1063 | { 1064 | "cell_type": "code", 1065 | "execution_count": null, 1066 | "metadata": {}, 1067 | "outputs": [], 1068 | "source": [ 1069 | "#Define K" 1070 | ] 1071 | }, 1072 | { 1073 | "cell_type": "markdown", 1074 | "metadata": {}, 1075 | "source": [ 1076 | "#### Calculate: Define F in terms of H, J, and K. (Recall Section XI.)" 1077 | ] 1078 | }, 1079 | { 1080 | "cell_type": "code", 1081 | "execution_count": null, 1082 | "metadata": {}, 1083 | "outputs": [], 1084 | "source": [ 1085 | "#Define F as a function of H, J, and K" 1086 | ] 1087 | }, 1088 | { 1089 | "cell_type": "markdown", 1090 | "metadata": {}, 1091 | "source": [ 1092 | "A more convenient form of the SCF energy is in terms of a sum over the AO basis functions\n", 1093 | "$$\n", 1094 | "E = E_{nuc} + \\sum_{pq}(H_{pq} + F_{pq})D_{pq},\n", 1095 | "$$\n", 1096 | "where $E_{nuc}$ is the nuclear repulsion, which psi4 can calculate for us as well." 1097 | ] 1098 | }, 1099 | { 1100 | "cell_type": "markdown", 1101 | "metadata": {}, 1102 | "source": [ 1103 | "#### Calculate: SCF energy based on H, F, and D using `np.einsum()`. \n", 1104 | "*(Hint: The right hand side of the equation is the sum of the product of two terms, each of which has two indices (p and q). The result, E, is a number, so it has no indices. In `einsum()` notation, this case will be represented with the indices for the matrices on the left of the `->`, and no index on the right of the `->`. For example, in the case of just one matrix, the sum of all its elements of a matrix `M` is `sum_of_m = np.einsum('pq->',M )`. In your answer below, be sure to account for any modifications required of an element-wise product of two matrices.)* " 1105 | ] 1106 | }, 1107 | { 1108 | "cell_type": "code", 1109 | "execution_count": null, 1110 | "metadata": {}, 1111 | "outputs": [], 1112 | "source": [ 1113 | "E_nuc = mol.nuclear_repulsion_energy()\n", 1114 | "SCF_E = #\n", 1115 | "print(F'Energy = {SCF_E:.8f}')" 1116 | ] 1117 | }, 1118 | { 1119 | "cell_type": "markdown", 1120 | "metadata": {}, 1121 | "source": [ 1122 | "#### Question: Based on the result of the calculation in Section VII, is this a reasonable answer? " 1123 | ] 1124 | }, 1125 | { 1126 | "cell_type": "markdown", 1127 | "metadata": {}, 1128 | "source": [ 1129 | "**Answer:** " 1130 | ] 1131 | }, 1132 | { 1133 | "cell_type": "markdown", 1134 | "metadata": {}, 1135 | "source": [ 1136 | "#### Question: Describe a procedure (i.e. identify the steps/commands) that will updated coefficients and compute a new density matrix based on the new definition of the Fock matrix. (Recall Section X)" 1137 | ] 1138 | }, 1139 | { 1140 | "cell_type": "markdown", 1141 | "metadata": {}, 1142 | "source": [ 1143 | "**Answer** " 1144 | ] 1145 | }, 1146 | { 1147 | "cell_type": "markdown", 1148 | "metadata": {}, 1149 | "source": [ 1150 | "#### Calculate: Using the procedure proposed above, calculate the updated coefficients" 1151 | ] 1152 | }, 1153 | { 1154 | "cell_type": "code", 1155 | "execution_count": null, 1156 | "metadata": {}, 1157 | "outputs": [], 1158 | "source": [ 1159 | "# Update density matrix" 1160 | ] 1161 | }, 1162 | { 1163 | "cell_type": "markdown", 1164 | "metadata": {}, 1165 | "source": [ 1166 | "You have just completed one cycle of the SCF calculation!\n", 1167 | "\n", 1168 | "\n", 1169 | "Now we will use the density matrix to build the Fock matrix. The code block below sets up a skeleton of the Hartree-Fock procedure. The basic steps are:\n", 1170 | "1. Calculate the Fock Matrix based on the density matrix previously defined from a one electron hamiltonian\n", 1171 | "2. Calculate the energy from the Fock matrix.\n", 1172 | "3. Check and see if the energy has converged by comparing the current energy to the previous energy and seeing if it is within the convergence threshold.\n", 1173 | "4. If the energy has not converged, transform the Fock matrix, and diagonalize the transformed Fock matrix to get the energy and MO coefficients. Then transform back to the original AO basis, pull the occupied orbitals, and reconstruct the density matrix. " 1174 | ] 1175 | }, 1176 | { 1177 | "cell_type": "code", 1178 | "execution_count": null, 1179 | "metadata": { 1180 | "scrolled": true 1181 | }, 1182 | "outputs": [], 1183 | "source": [ 1184 | "# ==> Nuclear Repulsion Energy <==\n", 1185 | "E_nuc = mol.nuclear_repulsion_energy()\n", 1186 | "\n", 1187 | "# ==> SCF Iterations <==\n", 1188 | "# Pre-iteration energy declarations\n", 1189 | "SCF_E = 0.0\n", 1190 | "E_old = 0.0\n", 1191 | "\n", 1192 | "# ==> Set default program options <==\n", 1193 | "# We continue recalculating the energy until it converges to the level we specify. \n", 1194 | "# The varible `E_conv` is where we set this level of convergence. We also set a \n", 1195 | "# maximum number of iterations so that if our calculation does not converge, it \n", 1196 | "# eventually stops and lets us know that it did not converge. \n", 1197 | "# Maximum SCF iterations\n", 1198 | "MAXITER = 40\n", 1199 | "# Energy convergence criterion\n", 1200 | "E_conv = 1.0e-6\n", 1201 | "\n", 1202 | "print('==> Starting SCF Iterations <==\\n')\n", 1203 | "\n", 1204 | "# Begin Iterations\n", 1205 | "for scf_iter in range(1, MAXITER + 1):\n", 1206 | " \n", 1207 | " # Build Fock matrix (Section XII)\n", 1208 | " \n", 1209 | " # \n", 1210 | "\n", 1211 | " # Compute SCF energy\n", 1212 | " \n", 1213 | " SCF_E = #\n", 1214 | " \n", 1215 | " print(F'SCF Iteration {scf_iter}: Energy = {SCF_E:.8f} dE = {SCF_E - E_old:.8f}')\n", 1216 | " \n", 1217 | " # Check to see if the energy is converged. If it is break out of the loop.\n", 1218 | " # If it is not, set the current energy E_old\n", 1219 | " \n", 1220 | " if (abs(SCF_E - E_old) < E_conv):\n", 1221 | " break\n", 1222 | " E_old = SCF_E\n", 1223 | " \n", 1224 | " # Compute new coefficient & density matrices (Section X & XI) \n", 1225 | " \n", 1226 | " # \n", 1227 | " \n", 1228 | " # MAXITER exceeded?\n", 1229 | " if (scf_iter == MAXITER):\n", 1230 | " psi4.core.clean()\n", 1231 | " raise Exception(\"Maximum number of SCF iterations exceeded.\")\n", 1232 | "\n", 1233 | "# Post iterations\n", 1234 | "print('\\nSCF converged.')\n", 1235 | "print(F'Final RHF Energy: {SCF_E:.6f} [Eh]')" 1236 | ] 1237 | }, 1238 | { 1239 | "cell_type": "markdown", 1240 | "metadata": {}, 1241 | "source": [ 1242 | "Compare your results to Psi4 by computing the energy using `psi4.energy()` in the cell below. " 1243 | ] 1244 | }, 1245 | { 1246 | "cell_type": "code", 1247 | "execution_count": null, 1248 | "metadata": { 1249 | "scrolled": true 1250 | }, 1251 | "outputs": [], 1252 | "source": [ 1253 | "# ==> Compare our SCF to Psi4 <==\n", 1254 | "# Call psi4.energy() to compute the SCF energy\n", 1255 | "SCF_E_psi = psi4.energy('SCF')\n", 1256 | "psi4.core.clean()\n", 1257 | "\n", 1258 | "# Compare our energy value to what Psi4 computes\n", 1259 | "assert psi4.compare_values(SCF_E_psi, SCF_E, 6, 'My SCF Procedure')" 1260 | ] 1261 | }, 1262 | { 1263 | "cell_type": "markdown", 1264 | "metadata": {}, 1265 | "source": [ 1266 | "#### Question: Modify the value of E_conv and describe its affect the number of iterations." 1267 | ] 1268 | }, 1269 | { 1270 | "cell_type": "markdown", 1271 | "metadata": {}, 1272 | "source": [ 1273 | "### XIII. Using Hartree-Fock to Justify Molecular Structure\n", 1274 | "\n", 1275 | "Why is CO$_2$ linear? Why is H$_2$O bent? Why is CH$_4$ tetrahedral? Why is FeF$_6$ octahedral? In general\n", 1276 | "chemistry, we used valence shell electron pair repulsion (VSEPR) theory to justify molecular structures\n", 1277 | "by invoking a _repulsion_ between both bonding and non-bonding pairs of electrons. The reality of molecular\n", 1278 | "structure is more complicated, however.\n", 1279 | "\n", 1280 | "In this section of the lab, we will use the same Hartree-Fock method that we implemented above to justify the\n", 1281 | "bent structure of water by computing the electronic energy of H$_2$O at a variety of bond angles. \n" 1282 | ] 1283 | }, 1284 | { 1285 | "cell_type": "code", 1286 | "execution_count": null, 1287 | "metadata": {}, 1288 | "outputs": [], 1289 | "source": [ 1290 | "# ==> Scanning a Bond Angle: Flexible Water <==\n", 1291 | "# Import a library to visualize energy profile\n", 1292 | "import matplotlib.pyplot as plt\n", 1293 | "%matplotlib inline\n", 1294 | "\n", 1295 | "# Define flexible water molecule using Z-matrix\n", 1296 | "flexible_water = \"\"\"\n", 1297 | "O\n", 1298 | "H 1 0.96\n", 1299 | "H 1 0.96 2 {}\n", 1300 | "\"\"\"\n", 1301 | "\n", 1302 | "# Scan over bond angle range between 90 & 180, in 5 degree increments\n", 1303 | "scan = {}\n", 1304 | "for angle in range(90, 181, 5):\n", 1305 | " # Make molecule\n", 1306 | " mol = psi4.geometry(flexible_water.format(angle))\n", 1307 | " # Call Psi4\n", 1308 | " e = psi4.energy('scf/cc-pvdz', molecule=mol)\n", 1309 | " #e = psi4.energy('scf/sto-3g', molecule=mol)\n", 1310 | "\n", 1311 | " # Save energy in dictionary\n", 1312 | " scan[angle] = e" 1313 | ] 1314 | }, 1315 | { 1316 | "cell_type": "code", 1317 | "execution_count": null, 1318 | "metadata": {}, 1319 | "outputs": [], 1320 | "source": [ 1321 | "# Visualize energy profile\n", 1322 | "x = list(scan.keys())\n", 1323 | "y = list(scan.values())\n", 1324 | "plt.plot(x,y,'ro-')\n", 1325 | "plt.xlabel('H-O-H Bond Angle ($^{\\circ}$)')\n", 1326 | "plt.ylabel('Molecular Energy ($E_h$)')\n", 1327 | "plt.show()" 1328 | ] 1329 | }, 1330 | { 1331 | "cell_type": "markdown", 1332 | "metadata": {}, 1333 | "source": [ 1334 | "Using the energy profile we generated above, justify the experimentally measured water bond angle of 104.5$^{\\circ}$ in the cell below." 1335 | ] 1336 | }, 1337 | { 1338 | "cell_type": "markdown", 1339 | "metadata": {}, 1340 | "source": [ 1341 | "**Answer**" 1342 | ] 1343 | } 1344 | ], 1345 | "metadata": { 1346 | "kernelspec": { 1347 | "display_name": "Python 3", 1348 | "language": "python", 1349 | "name": "python3" 1350 | }, 1351 | "language_info": { 1352 | "codemirror_mode": { 1353 | "name": "ipython", 1354 | "version": 3 1355 | }, 1356 | "file_extension": ".py", 1357 | "mimetype": "text/x-python", 1358 | "name": "python", 1359 | "nbconvert_exporter": "python", 1360 | "pygments_lexer": "ipython3", 1361 | "version": "3.7.6" 1362 | }, 1363 | "latex_envs": { 1364 | "LaTeX_envs_menu_present": true, 1365 | "autoclose": false, 1366 | "autocomplete": true, 1367 | "bibliofile": "biblio.bib", 1368 | "cite_by": "apalike", 1369 | "current_citInitial": 1, 1370 | "eqLabelWithNumbers": true, 1371 | "eqNumInitial": 1, 1372 | "hotkeys": { 1373 | "equation": "Ctrl-E", 1374 | "itemize": "Ctrl-I" 1375 | }, 1376 | "labels_anchors": false, 1377 | "latex_user_defs": false, 1378 | "report_style_numbering": true, 1379 | "user_envs_cfg": true 1380 | } 1381 | }, 1382 | "nbformat": 4, 1383 | "nbformat_minor": 2 1384 | } 1385 | -------------------------------------------------------------------------------- /labs/Machine_Learning/Machine_Learning_Student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Machine-learning applications in computational chemistry.\"\"\"\n", 10 | "\n", 11 | "__authors__ = \"B. G. Peyton\"\n", 12 | "__credits__ = [\"Doaa Altarawy\", \"Matthew Welborn\", \"Daniel G. A. Smith\"]\n", 13 | "__email__ = [\"bgpeyton@vt.edu\"]\n", 14 | "\n", 15 | "__copyright__ = \"(c) 2008-2020, The Psi4Education Developers\"\n", 16 | "__license__ = \"BSD-3-Clause\"\n", 17 | "__date__ = \"2020-07-13\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Machine-learning applications in computational chemistry\n", 25 | "## 0. Environment setup " 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "import psi4\n", 35 | "from sklearn.linear_model import LinearRegression\n", 36 | "from sklearn.kernel_ridge import KernelRidge\n", 37 | "from sklearn.model_selection import GridSearchCV\n", 38 | "import numpy as np\n", 39 | "import pandas as pd\n", 40 | "import matplotlib.pyplot as plt" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## 1. Introduction\n", 48 | "\n", 49 | "Machine-learning (ML) is broadly defined as any algorithm which improves by feeding more data into the algorithm. Familiar concepts such as basic linear regression can be understood as applications of ML, where more data in the regression provides a better fit to a straight line which can then be used to predict new points. This is an example of **supervised** learning, where some input values (x) and output values (y) are known from the beginning. These values compose a **training set**, a defining feature of any ML method. Unsupervised learning is also possible, and is generally used for finding patterns in correlated data; however, we will restrict our discussion to supervised learning techniques.\n", 50 | "\n", 51 | "ML has found applications across the physical sciences including (but not limited to) engineering, physics, biology, and chemistry. By utilizing training sets of molecules and their various properties, chemists can make use of ML algorithms by predicting molecular properties using only the molecular formula or structure without the need for time-consuming experiments. However, generating a robust database of reference values to train the ML model is a tedious task for experimental chemists. Computational chemistry provides a quick and consistent way to produce training data for a reference set of molecules. Once a model is trained with these reference data, properties for new molecules (or geometries of the same molecule) can be predicted at very low cost -- no experiment or electronic structure calculation required. " 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## 2. Representing a molecule\n", 59 | "The ML algorithms we will be using attempt to find a model $f({\\bf x})$ which can map a representation vector $\\bf x$ onto some target value $y$:\n", 60 | "$$f({\\bf x}) = {\\bf x}^T {\\bf w},$$\n", 61 | "\n", 62 | "$$y = f({\\bf x}) + \\epsilon$$\n", 63 | "where $\\epsilon$ is the **noise** or **error** of the model. These equations define a simple linear regression model, where we have allowed the input vector (and therefor the weights, or slope) to be of arbitrary dimension.\n", 64 | "\n", 65 | "For our purposes, $\\bf x$ is some vector **representation** of our molecule, and $y$ is some property we'd like to predict, such as the energy. We will start by calculating the energy of the water molecule during the symmetric stretching of the O-H bonds, which will be the property $y$ in our model. The following code block will calculate the energy at 31 different bond lengths and save the geometry, charges, and energy at each bond length." 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "# run across an H2O symmetric bond stretching surface\n", 75 | "# 0.5 - 2.0 Angstroms, increments of 0.05, total of 31 geometries\n", 76 | "# use H-H bond angle 104.5 degrees\n", 77 | "# save geometry, nuclear charges (same across the surface), and scf energy as lists\n", 78 | "\n", 79 | "# set basis\n", 80 | "psi4.set_options({\n", 81 | " 'basis':'sto-3g'\n", 82 | "})\n", 83 | "# initialize geometry list\n", 84 | "geoms = []\n", 85 | "# initialize charge list\n", 86 | "qs = []\n", 87 | "# initialize energy list\n", 88 | "Es = []\n", 89 | "# generate bond lengths\n", 90 | "rs = []\n", 91 | "for i in range(0,31):\n", 92 | " rs.append(0.5 + i*0.05)\n", 93 | "\n", 94 | "# loop over bond lengths\n", 95 | "for i in rs:\n", 96 | " # generate a water molecule using a Z-matrix and set the O-H bond lengths\n", 97 | " mol = psi4.geometry(\"\"\"\n", 98 | " O\n", 99 | " H 1 \"\"\" + str(i) + \"\"\"\n", 100 | " H 1 \"\"\" + str(i) + \"\"\" 2 104.5\n", 101 | " \"\"\")\n", 102 | " # save the geometry\n", 103 | " geoms.append(mol.geometry().to_array())\n", 104 | " \n", 105 | " # save the charges for all three atoms as a list\n", 106 | " q = []\n", 107 | " for a in range(0,3):\n", 108 | " q.append(mol.fZ(a))\n", 109 | " qs.append(q)\n", 110 | " \n", 111 | " # calculate and save the energy\n", 112 | " Es.append(psi4.energy('scf'))\n", 113 | " " 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "metadata": {}, 119 | "source": [ 120 | "In the next cell, the change in the energy as the length of the O-H bonds change is plotted as a slice of the potential energy surface." 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "# plot the original surface\n", 130 | "plt.plot(rs,list(Es))\n", 131 | "plt.title(\"H$_2$O Hartree-Fock symmetric O-H stretch\")\n", 132 | "plt.xlabel('r / $\\AA$')\n", 133 | "plt.ylabel('E / $E_h$')\n", 134 | "plt.show()" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "At a glance, the energy seems to be a simple function of the bond length $r$. One could formulate this as a simple regression problem and solve for a function that maps $r$ into the energy $E$. However, if one wishes to explore more than the symmetric stretch, more information about the molecule is necessary. We want to describe every bond in the molecule.\n", 142 | "\n", 143 | "To do this, we can encode the geometry of the molecule into a matrix using each atom distance $r_{ij}$ - that is, the distance between every pair of atoms. To differentiate the O-H bond length from the H-H \"bond\", we will use the atomic charge $Z$ of each atom. The resulting matrix is called a **Coulomb matrix** $\\bf C$, which takes the form:\n", 144 | "$$\n", 145 | "\\begin{align}\n", 146 | "C_{ij} = \n", 147 | "\\begin{cases}\n", 148 | "\\frac{Z_iZ_j}{r_{ij}}&\\text{for $i \\neq j$}\\\\\n", 149 | "0.5Z_i^{2.4}&\\text{for $i=j$}\\\\\n", 150 | "\\end{cases}\n", 151 | "\\end{align}\n", 152 | "$$" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# ==> Build coulomb matrices according to the above equation <==\n", 162 | " \n", 163 | "def coulomb(geom,q):\n", 164 | " '''\n", 165 | " Generates the coulomb matrix given a geometry and list of atomic charges\n", 166 | " \n", 167 | " Parameters:\n", 168 | " geom: list of lists of atomic coordinates, [[xi,yi,zi],[xj,yj,zj],...]\n", 169 | " q: list of atomic charges, [qi,qj,...]\n", 170 | " \n", 171 | " Returns:\n", 172 | " cm: np.ndarray, coulomb matrix\n", 173 | " '''\n", 174 | " natom = len(q)\n", 175 | " cm = np.zeros((natom,natom)) # ==> Fill this matrix! <==\n", 176 | " \n", 177 | " # ==> YOUR CODE HERE <==#\n", 178 | " \n", 179 | " return cm\n", 180 | "\n", 181 | "# Generate the coulomb matrix for all geometries and store them in a list\n", 182 | "couls = []\n", 183 | "for i in range(0,len(geoms)):\n", 184 | " coul = coulomb(geoms[i],qs[i])\n", 185 | " couls.append(coul)" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "print(couls[0])" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "The above cell should evaluate to:\n", 202 | "```\n", 203 | "[[73.51669472 8.46683537 8.46683537]\n", 204 | " [ 8.46683537 0.5 0.66926039]\n", 205 | " [ 8.46683537 0.66926039 0.5 ]]\n", 206 | " ```" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "**STUDENT QUESTIONS:**\n", 214 | "\n", 215 | "A. `mol.geometry()` always returns the geometry in cartesian units Bohr. How would the Coulomb matrix compare if you used different units for your bond distances?\n", 216 | "\n", 217 | "B. Why do we not scale the diagonal elements by the bond distance?\n", 218 | "\n", 219 | "**Answers:** \n", 220 | "\n", 221 | "A. \n", 222 | "\n", 223 | "B. " 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "## 2. Training a ML model for molecular energy\n", 231 | "Now we may take our Coulomb matrix elements and use them as features for a ML model. We will flatten $\\bf C$ into a 1D array $\\bf c$ (using the `numpy` function `ndarray.flatten()`), then find the optimum weights $\\bf w$ which map the features onto the energy:\n", 232 | "\n", 233 | "$$y \\approx f({\\bf x}) = {\\bf c}^T {\\bf w}.$$\n", 234 | "\n", 235 | "Finding the weights is done by calling `fit(x,y)` on a `LinearRegression` object, which was imported from the `scikit-learn` ML package at the top of this notebook. The resulting model can then be used to `predict` values for any new representations." 236 | ] 237 | }, 238 | { 239 | "cell_type": "markdown", 240 | "metadata": {}, 241 | "source": [ 242 | "**STUDENT QUESTION:** If our feature vector $\\bf c$ is a vector containing all of the elements of $\\bf C$, how many elements will there be in our weight vector $\\bf w$?\n", 243 | "\n", 244 | "**Answer:** " 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "metadata": {}, 250 | "source": [ 251 | "We will now select some points to train the ML model, and try predicting the energy for every other point. Since we know what the curve will look like, we can evaluate the accuracy of our model. Execute the following code blocks to see how the model performs." 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": null, 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "# set up a training set (X,y) - 4 evenly-spaced points on the PES\n", 261 | "trainers = [0,7,15,23] \n", 262 | "X_train = []\n", 263 | "y_train = []\n", 264 | "for t in trainers:\n", 265 | " X_train.append(couls[t].flatten())\n", 266 | " y_train.append(Es[t])\n", 267 | "\n", 268 | "# prediction set will be everything else\n", 269 | "testers = []\n", 270 | "for i in range(0,31):\n", 271 | " testers.append(i)\n", 272 | "# delete the test set!\n", 273 | "for t in sorted(trainers,reverse=True):\n", 274 | " del testers[t]\n", 275 | "# set up test set (X,y)\n", 276 | "X_test = []\n", 277 | "y_test = []\n", 278 | "for t in testers:\n", 279 | " X_test.append(couls[t].flatten())\n", 280 | " y_test.append(Es[t])\n", 281 | "\n", 282 | "# use Linear Regression from scikit-learn to train and predict\n", 283 | "reg = LinearRegression().fit(X_train,y_train)\n", 284 | "y_pred = reg.predict(X_test)" 285 | ] 286 | }, 287 | { 288 | "cell_type": "code", 289 | "execution_count": null, 290 | "metadata": {}, 291 | "outputs": [], 292 | "source": [ 293 | "# plot the true and ML surfaces\n", 294 | "plt.plot(rs,list(Es),label='Truth')\n", 295 | "plt.plot(np.asarray(rs)[testers],y_pred,label='ML')\n", 296 | "plt.legend()\n", 297 | "plt.xlabel('r / $\\AA$')\n", 298 | "plt.ylabel('E / $E_h$')\n", 299 | "plt.show()" 300 | ] 301 | }, 302 | { 303 | "cell_type": "markdown", 304 | "metadata": {}, 305 | "source": [ 306 | "This model did not perform admirably. However, `scikit-learn` provides many pre-packaged ML algorithms which are more appropriate for this job. Let's try again, but instead of linear regression we use the popular **Kernel Ridge Regression** model. This model solves a modified linear regression equation:\n", 307 | "\n", 308 | "$$\n", 309 | "y \\approx f({\\bf x}) = \\sum_i w_i k({\\bf x},{\\bf x'}_i)\n", 310 | "$$\n", 311 | "\n", 312 | "where $i$ runs over the training set (${\\bf x'}$). The regression coefficients $\\bf w$ are defined by\n", 313 | "\n", 314 | "$$\n", 315 | "{\\bf w} \\triangleq ({\\bf K}({\\bf x'},{\\bf x'}) + \\alpha{\\bf I})^{-1}{\\bf y}\n", 316 | "$$\n", 317 | "\n", 318 | "where $\\bf I$ is the identity matrix. This method differs from linear regression in two key ways. The first is the inclusion of a dampening parameter (called a **hyperparameter**), $\\alpha$, which is optimized during training and protects against over-training. The next difference is the use of a **kernel**, vector $k$ or matrix $\\bf{K}$, which measures the **similarity** of two inputs $\\bf x$ and ${\\bf x'}$ rather than using the inputs themselves. There are many possible kernel definitions, but we will choose the popular radial basis function kernel:\n", 319 | "\n", 320 | "$$\n", 321 | "{\\bf K}({\\bf x},{\\bf x'}) = exp(-\\gamma||{\\bf x} - {\\bf x'}||^2)\n", 322 | "$$\n", 323 | "\n", 324 | "where $\\gamma$ is included as an additional optimizable hyperparameter determined at training time to determine the \"width\" of the kernel. The hyperparameters are optimized by simply trying many different values, and choosing the combination which gives the best results on a subset of the training data in a process called cross-validation.\n", 325 | "(NOTE: the algorithm must never be shown the test data until final evaluation!)\n", 326 | "\n", 327 | "`scikit-learn` can build this model given the training set and a few starting parameters with just a few lines of code, shown below." 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": null, 333 | "metadata": {}, 334 | "outputs": [], 335 | "source": [ 336 | "# building the kernel ridge regression model\n", 337 | "krr = KernelRidge(kernel='rbf')\n", 338 | "# define hyperparameter grid: 12 points from 1E-12 to 1E12 for each\n", 339 | "parameters = {'alpha':np.logspace(-12,12,num=12),\n", 340 | " 'gamma':np.logspace(-12,12,num=12)}\n", 341 | "# build the model - 4-fold grid search CV w/ NMSE scoring function\n", 342 | "krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')\n", 343 | "# train the model on the training set\n", 344 | "krr_regressor.fit(X_train,y_train)\n", 345 | "# predict the test set\n", 346 | "y_pred = krr_regressor.predict(X_test)" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "metadata": {}, 352 | "source": [ 353 | "We will plot our results with `matplotlib` again, this time using an inset plot to zoom-in." 354 | ] 355 | }, 356 | { 357 | "cell_type": "code", 358 | "execution_count": null, 359 | "metadata": {}, 360 | "outputs": [], 361 | "source": [ 362 | "plt.plot(rs,list(Es),label='Truth')\n", 363 | "plt.plot(np.asarray(rs)[testers],y_pred,label='ML')\n", 364 | "\n", 365 | "ax = plt.gca()\n", 366 | "axin = ax.inset_axes([0.25,0.4,0.4,0.4])\n", 367 | "axin.plot(rs,list(Es))\n", 368 | "axin.plot(np.asarray(rs)[testers],y_pred)\n", 369 | "axin.set_xlim([0.9,1.1])\n", 370 | "axin.set_ylim([-74.965,-74.94])\n", 371 | "axin.set_alpha(0)\n", 372 | "ax.indicate_inset_zoom(axin,label='_nolegend_')\n", 373 | "\n", 374 | "plt.legend()\n", 375 | "plt.xlabel('r / $\\AA$')\n", 376 | "plt.ylabel('E / $E_h$')\n", 377 | "plt.show()" 378 | ] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "metadata": {}, 383 | "source": [ 384 | "As you can see, the inclusion of multiple optimizable parameters and the use of a kernel vastly improves the flexibility of our model. Now that we have a model that seems to work, we will try a more complex system." 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "## 4. Predicting a hypersurface\n", 392 | "Of course, we do not need to limit ourselves to the symmetric stretch. The asymmetric stretch of water, a function of both O-H bond distances, can also be explored. Complete the following code to compute the hypersurface - you should try all values of `rs` for each bond, so reduce the number of rs to 16 instead of 31 (still ranging from 0.5 to 2 Angstroms). Running the calculations may take a few minutes." 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "metadata": {}, 399 | "outputs": [], 400 | "source": [ 401 | "# ==> Generate the slice of the hypersurface where both bonds stretch independently. <==\n", 402 | "\n", 403 | "psi4.set_options({\n", 404 | " 'basis':'sto-3g'\n", 405 | "})\n", 406 | "geoms = []\n", 407 | "qs = []\n", 408 | "Es = np.zeros((len(rs),len(rs))) # store the energies in a matrix! we can flatten it later\n", 409 | "rs = [] # 16 r values!\n", 410 | "\n", 411 | "# ==> YOUR CODE HERE <==" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "Since we now want to plot the energy against two variables ($r_1$ and $r_2$), we can use a contour plot. The color will show us the deviation from the minimum at each point, $(r_1,r_2)$." 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": null, 424 | "metadata": {}, 425 | "outputs": [], 426 | "source": [ 427 | "fig,ax=plt.subplots(1,1)\n", 428 | "cp = ax.contourf(rs,rs,(Es - np.min(Es)))\n", 429 | "cbar = fig.colorbar(cp,label=\"(E - min) / $E_h$\")\n", 430 | "ax.set_title('H$_2$O hypersurface')\n", 431 | "ax.set_xlabel('r$_1$ / $\\AA$')\n", 432 | "ax.set_ylabel('r$_2$ / $\\AA$')\n", 433 | "plt.show()" 434 | ] 435 | }, 436 | { 437 | "cell_type": "markdown", 438 | "metadata": {}, 439 | "source": [ 440 | "If the calculations have run correctly, the above cell should produce the following graph:\n", 441 | "![contour](data/h2o_contour.png)\n", 442 | "We can see that there is a broad enery well, meaning that many different bond distances produce energies near the minimum. Reproducing this well is key for a useful ML model of the system. " 443 | ] 444 | }, 445 | { 446 | "cell_type": "markdown", 447 | "metadata": {}, 448 | "source": [ 449 | "In the following cells, choose a number of training points and attempt to reproduce the above hypersurface. See if you can find the lowest number of training points to still get a good fit!\n", 450 | "\n", 451 | "NOTE: you can choose the training points however you like. You may wish to try evenly-spaced points, points near the minima or maxima of the hypersurface, or even random points! (Hint: for the latter, try `np.random.randint`)" 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": null, 457 | "metadata": {}, 458 | "outputs": [], 459 | "source": [ 460 | "# ==> Generate the coulomb matrix for all geometries <==\n", 461 | "couls = []\n", 462 | "\n", 463 | "# ==> YOUR CODE HERE <==" 464 | ] 465 | }, 466 | { 467 | "cell_type": "code", 468 | "execution_count": null, 469 | "metadata": {}, 470 | "outputs": [], 471 | "source": [ 472 | "# ==> Make your training set - X and y values.\n", 473 | "# Remember: you must flatten each individual Coulomb matrix, but also the whole energy array\n", 474 | "trainers = []\n", 475 | "X_train = []\n", 476 | "y_train = []\n", 477 | "\n", 478 | "# ==> YOUR CODE HERE <==" 479 | ] 480 | }, 481 | { 482 | "cell_type": "code", 483 | "execution_count": null, 484 | "metadata": {}, 485 | "outputs": [], 486 | "source": [ 487 | "# ==> Finally, use Scikit-Learn to build a model to predict the rest of the surface <==\n", 488 | "krr = KernelRidge(kernel='rbf')\n", 489 | "parameters = {'alpha':np.logspace(-12,12,num=12),\n", 490 | " 'gamma':np.logspace(-12,12,num=12)}\n", 491 | "krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')\n", 492 | "\n", 493 | "# ==> COMPLETE THE FUNCTION CALL <==\n", 494 | "krr_regressor.fit() " 495 | ] 496 | }, 497 | { 498 | "cell_type": "markdown", 499 | "metadata": {}, 500 | "source": [ 501 | "The following cell will predict the entire surface, including the training set, to make plotting easier. We will also look at the average error across the surface. How does it compare?" 502 | ] 503 | }, 504 | { 505 | "cell_type": "code", 506 | "execution_count": null, 507 | "metadata": {}, 508 | "outputs": [], 509 | "source": [ 510 | "y_pred = krr_regressor.predict([c.flatten() for c in couls])\n", 511 | "fig,ax=plt.subplots(1,1)\n", 512 | "cp = ax.contourf(rs,rs,(y_pred.reshape(16,16) - np.min(Es)))\n", 513 | "cbar = fig.colorbar(cp,label=\"($E_{ML}$ - min) / $E_h$\")\n", 514 | "ax.set_title('H$_2$O hypersurface')\n", 515 | "ax.set_xlabel('r$_1$ / $\\AA$')\n", 516 | "ax.set_ylabel('r$_2$ / $\\AA$')\n", 517 | "plt.show()\n", 518 | "print(\"Mean prediction error across the surface: {} Hartrees\".format(np.mean(np.abs(y_pred - Es.flatten()))))" 519 | ] 520 | }, 521 | { 522 | "cell_type": "markdown", 523 | "metadata": {}, 524 | "source": [ 525 | "**STUDENT QUESTION:** The shape of the well may be reproduced, but \"chemical accuracy\" in computational chemistry is generally 1kcal/mol. Does your model reach this (on average)?\n", 526 | "\n", 527 | "**Answer:** " 528 | ] 529 | }, 530 | { 531 | "cell_type": "markdown", 532 | "metadata": {}, 533 | "source": [ 534 | "## 5. Atomization energies and the ANI-1 dataset\n", 535 | "We can also apply the same logic to many different molecules. A variant of the Coulomb matrix can be generated for molecules from the ANI-1 dataset, a collection of over 57 thousand organic molecules with up to 8 heave (non-Hydrogen) atoms and their properties. Large datasets such as these can be used to train models to predict properties for new molecules without needing to perform an expensive calculation. \n", 536 | "\n", 537 | "Below, a subset of the ANI-1 dataset is loaded into a Pandas dataframe containing the energy from an electronic structure calculation, a molecule object, the Coulomb matrix (`feature`), and the atomization energy (`AE`) for a 1000-molecule training set and 100-molecule test set. The Pandas dataframe acts as a powerful dictionary for storing large datasets." 538 | ] 539 | }, 540 | { 541 | "cell_type": "code", 542 | "execution_count": null, 543 | "metadata": {}, 544 | "outputs": [], 545 | "source": [ 546 | "data_train = pd.read_pickle('data/ani1_train.pd')\n", 547 | "data_test = pd.read_pickle('data/ani1_test.pd')\n", 548 | "\n", 549 | "data_train" 550 | ] 551 | }, 552 | { 553 | "cell_type": "markdown", 554 | "metadata": {}, 555 | "source": [ 556 | "Now, all of our data is collected into just two objects which can be indexed by name, similarly to a Python `dict` object. Another advantage of this dataframe is that every entry can be visualized within a Jupyter notebook. Feel free to explore the different molecules present." 557 | ] 558 | }, 559 | { 560 | "cell_type": "code", 561 | "execution_count": null, 562 | "metadata": {}, 563 | "outputs": [], 564 | "source": [ 565 | "data_test[\"molecule\"][1]" 566 | ] 567 | }, 568 | { 569 | "cell_type": "markdown", 570 | "metadata": {}, 571 | "source": [ 572 | "In the cells below, train a new kernel ridge regression model. Then, evaluate the error for predicting the atomization energies of the test and training sets. The error on the training sets gives us a \"lower bound\" to what we hope the error in the test set to be. If the training set performs poorly, we know our model must be revised." 573 | ] 574 | }, 575 | { 576 | "cell_type": "code", 577 | "execution_count": null, 578 | "metadata": {}, 579 | "outputs": [], 580 | "source": [ 581 | "# ==> Pandas dictionaries allow us to store features and data in one place. <==\n", 582 | "# ==> Select the appropriate dictionary keys to build the training and test sets. <==\n", 583 | "X_train = np.vstack(data_train[])\n", 584 | "y_train = data_train[].values\n", 585 | "X_test = np.vstack(data_test[])\n", 586 | "y_test = data_test[].values" 587 | ] 588 | }, 589 | { 590 | "cell_type": "code", 591 | "execution_count": null, 592 | "metadata": {}, 593 | "outputs": [], 594 | "source": [ 595 | "# ==> train the model and predict for both the test and training set <==\n", 596 | "# ==> this will take a moment <==\n", 597 | "krr = KernelRidge(kernel='rbf')\n", 598 | "parameters = {'alpha':np.logspace(-12,12,num=12),\n", 599 | " 'gamma':np.logspace(-12,12,num=12)}\n", 600 | "krr_regressor = GridSearchCV(krr,parameters,cv=4,scoring='neg_mean_squared_error')\n", 601 | "\n", 602 | "# ==> COMPLETE THE FUNCTION CALLS <==\n", 603 | "krr_regressor.fit()\n", 604 | "y_pred_test = krr_regressor.predict()\n", 605 | "y_pred_train = krr_regressor.predict()" 606 | ] 607 | }, 608 | { 609 | "cell_type": "markdown", 610 | "metadata": {}, 611 | "source": [ 612 | "Finally, we will plot the errors in a violin plot. The y-axis will tell us the error, while the width of the violin tells us the density (frequency) of points with that error. In other words, the widest point tells us the error where most of the data points fall. The minimum, maxiumum, and mean errors are marked by horizontal lines." 613 | ] 614 | }, 615 | { 616 | "cell_type": "code", 617 | "execution_count": null, 618 | "metadata": {}, 619 | "outputs": [], 620 | "source": [ 621 | "test_err = np.abs(y_test - y_pred_test)\n", 622 | "train_err = np.abs(y_train - y_pred_train)\n", 623 | "\n", 624 | "fig,ax = plt.subplots(1,1)\n", 625 | "ax.violinplot([test_err,train_err],showmeans=True)\n", 626 | "ax.set_xticks([1,2])\n", 627 | "ax.set_xticklabels([\"Test Set\",\"Training Set\"])\n", 628 | "ax.set_ylabel('ML error / $E_h$')\n", 629 | "print(\"Mean prediction error across the test set: {} Hartrees\".format(np.mean(test_err)))" 630 | ] 631 | }, 632 | { 633 | "cell_type": "markdown", 634 | "metadata": {}, 635 | "source": [ 636 | "**STUDENT QUESTION:**\n", 637 | "Are these error significant or negligible? Should the model be revised? Discuss your reasoning.\n", 638 | "\n", 639 | "**Answer:** " 640 | ] 641 | }, 642 | { 643 | "cell_type": "code", 644 | "execution_count": null, 645 | "metadata": {}, 646 | "outputs": [], 647 | "source": [] 648 | } 649 | ], 650 | "metadata": { 651 | "kernelspec": { 652 | "display_name": "Python 3", 653 | "language": "python", 654 | "name": "python3" 655 | }, 656 | "language_info": { 657 | "codemirror_mode": { 658 | "name": "ipython", 659 | "version": 3 660 | }, 661 | "file_extension": ".py", 662 | "mimetype": "text/x-python", 663 | "name": "python", 664 | "nbconvert_exporter": "python", 665 | "pygments_lexer": "ipython3", 666 | "version": "3.7.6" 667 | }, 668 | "latex_envs": { 669 | "LaTeX_envs_menu_present": true, 670 | "autoclose": false, 671 | "autocomplete": true, 672 | "bibliofile": "biblio.bib", 673 | "cite_by": "apalike", 674 | "current_citInitial": 1, 675 | "eqLabelWithNumbers": true, 676 | "eqNumInitial": 1, 677 | "hotkeys": { 678 | "equation": "Ctrl-E", 679 | "itemize": "Ctrl-I" 680 | }, 681 | "labels_anchors": false, 682 | "latex_user_defs": false, 683 | "report_style_numbering": true, 684 | "user_envs_cfg": true 685 | } 686 | }, 687 | "nbformat": 4, 688 | "nbformat_minor": 2 689 | } 690 | -------------------------------------------------------------------------------- /labs/Machine_Learning/data/ani1_test.pd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Machine_Learning/data/ani1_test.pd -------------------------------------------------------------------------------- /labs/Machine_Learning/data/ani1_train.pd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Machine_Learning/data/ani1_train.pd -------------------------------------------------------------------------------- /labs/Machine_Learning/data/h2o_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Machine_Learning/data/h2o_contour.png -------------------------------------------------------------------------------- /labs/Microwave_Spectroscopy/Microwave_Spectroscopy_student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Determining Structure from Microwave Spectroscopy\"\"\"\n", 10 | "\n", 11 | "__authors__ = \"D. Brandon Magers\"\n", 12 | "__credits__ = [\"W. D. Allen\"]\n", 13 | "__email__ = [\"bmagers@belhaven.edu\"]\n", 14 | "\n", 15 | "__copyright__ = \"(c) 2008-2020, The Psi4Education Developers\"\n", 16 | "__license__ = \"BSD-3-Clause\"\n", 17 | "__date__ = \"2020-07-13\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Determining Structure from Microwave Spectroscopy" 25 | ] 26 | }, 27 | { 28 | "attachments": { 29 | "ethenone.png": { 30 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAALQAAAEZCAIAAAHGXah3AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAIdUAACHVAQSctJ0AAGR3SURBVHhe7d13kF3lmSZw/riSTkvtNB7P2jM1O1s1NVO7OzVhXTuzu7N27RpsTAaRk0UywUQTDBgw2SYnkYQQCJBEzlHknHPOGSSRc5AJvb8+z+2PS7daqGV1i6vtt26dOvfcc77wvM/7vO93blqs66ts5qRJzb3+bW6tPF9VH3Z2vtvZ+XRVXVZVzaNzsn5b+eMbb8zu7Oz67nc/7eyc1dl5e1Ud3Gg0X+tj/bbySFV93Nn5eWen4bw8evRNVTV+Plp5ecyYmWPGvNnZOXP0aC1eVFV7zkcrb3R23lpV91XVHVV1SVUdNWLE1f1D028rr3V2auiEqppUVfs3Gu91ds5PK693dprOyfWVj1XV+/Pdisfknlbmcyw3/O//feOPfvT7//yf7V/43//77T/+8aR/+qe81Nf6beX3ta2zzjr2d9555/333/9Xv/pVXuprX93K1ltvnVY222yz5mt97CtaOeCAA/bbb7/ddttNE5tvvnnztT7Wbyu/+93vDj300KOOOurggw/ed999t99++7XXXrv5Wh/rt5W9995bKxMnTiyt/OIXv2i+1sf6bWWXXXYxFzMyL/tbbbXVGmus0Xytj/XbChR++9vfQgS022yzzXrrrffzn/+8+Vof67cVTtl44415d5NNNjGXZZdd1nCar/WxflsxnX322WfppZdeaqmlfvKTnxx44IHz04qx7LXXXqussor9X//61wCaz1a4abXVVrO/3XbbzWcrZsTBq6++un1ImxES56W+1m8rf/jDH7QS726xxRbz2UrfsfB3Xuprc8OFj/bcc8899thjhx12mM9WjMWVhx12mK1BbbvttvPTiiFo4uijjz7kkEO08pvf/GbddddtvtbH5taKUWjCdvfddzeWKNYcrd9WdI4vQlFzO+20E31ZeeWVN9poo+bLX7Y5tPLOO+8cccQRaKZ/uNpuuumma665pjiA96677vrmm282T+2xL7Vy//33f/7553aE8i9/+Uudw4I4jR07VhP8TbreeOONGTNm3Hzzzbkk1t2KFz744INnn33W/ltvvXXbbbcZSP1qbyN9991338yZMw3nww8/NOp7773X8cXee++99O+1Sy+99LTTTjvyyCPrS/q1Cy644Mknn7Tz8ccfP/744xMmTFjs7rvv1r/hPPLII9OmTct5X2mPPfaY4ej41ltvxc/uGd1www1mcc455+y444456StNEzfeeONVV11lIJ420Z00adILL7yQ/Xm3zIvNwdOt9t7dd7/4VXXd3Jp4p66iXu3svLeqLlh++ebRPtZvEyqXrm99q+ub31RtPFVV581HqaBmUQ52/du/2VGIaeL3/RRic27i048+eqKq1CyvdHY+U1U3V9VJVbXrQJt4d8yYe6oKCjdW1WlVtUuj8duBNvFRXQvq/KiRI7dvNNTLWmm+/GXrtwl17f09EEL0hY6OATfBnQ/0NPFBZ+eAR/HJRx/d+eMfn/vDH+apyu2Kf/u3jf/6r/O0l825idmzZx900EEELU/JIu342c9+lqe9rN8mXEbHDj/8cFUBNaGsP/3pT5svf9nm1oRwto24U7YBN+H68ePHU1Y7RJQsD6wJidaVrqcLZqEJ01lmmWWaL3/Z5twE7TV+F0t6koOJSAsDg3PDDTeEpfnr3/VmoTBZfPHFmy9/2ebcxAYbbGAirmT6V0xIC8stt1zz5S/bnJtwGV8oqczf+M3IdAbWhIloQnLLUyPCi/kZRWki1FJw5mkvm3MTp59+uiZUdXnKL+qZFVdcMU972dyaULnn6fw0IUVqgl/zVBPK1vkchdIDNdSGPDLgJpSXqh8rEkOwkFDID6yJM844wyhUBsceeyx3oKmBDCxGhJnOjUI9ZzhWJKqfgTWh9CJTtoINFghiIYLjzZe/bL2beOKJJyiViYhuAQbRLbfcUsgsv/zyhqPdK664onlqj32piffff99WNaRbFddaa62lczGWtYx60PWvv/66suDcc8/NJWwxFZMiSuGhVrFvFCqW/sC/8sorXa+VV155RcX08ssvO3mxt99+22uqt4cffviWW27h/5zdnykm77zzzk8//dS+tq655prF1Dda0Z7KkPNy3lfarFmz1GnqtjPPPHMxLLr99ts1ltppHm369Ok33XTTeeedx19NOI8//vjszIfNmRfzYo984xsvVNWMMWNe6uh4sqpUEpdV1dtPPNF8eYA2P+NQfL3b2flZz2N2fafu5dGjH62qG6rq1P7rsrnYgMehdlHEfZKa8Hvf61p66c/rcczs7Hysqm6v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K6Gb96NWXHHFwR0H2EFCPPTN8qYYJAxi++2332KLLYCx5pprLr300s5sXjNvNrBx7LDDDiat11133TU3LXfZZRfk3W677Xhk4403FrEB4+yzz25eM282sHGYPRoKzm233RYAzAjECPly3CBWXnnln/70p+uvv/7gjgM5kHSDDTbYZJNNSBZDCDCst956a6211tixY2U+gDlh0PEwDnq6xhprmD1K2hoBGJZbbrkllljCS9w06OMIHsIELThimdp+/vOfL7744nKvl4SPcQyRX0waQ7mjNThJSMaBxaJmSPFoHYcwzhCHdBzw6DUOWlLwGCKeUlJ4bLTRRr3GQeOHdBxficeg85SG6myOPFUg5qVANZevMs7RBjYO3WfSOutvHBIQng7FOOIXktofHoM+jvhljuPYc889Cx6D7pcyDjtzxMNLQ8GPXniQc7lXxj/xxBNb8Rgiv+jMjqHou3Ud5fiQjgMeQqZ1HWVAujcUZdiQ8lSdbOWSOjlDIfkyrdJw3LhxQ6Ef0U0jsHJhWdIZB6kFidrMIJQjgzuOrKN0nHXUsccem3UUWmQcXEPfVl999cEdh56MI4tb66jzzjsv38OIX7A167l80ad5zbzZgMeROsMiKusoY0IXzhI1Vjd4iqTWc4M7DusG8zb7BEsCJ4tKQ1TEb7bZZtb7+T5s85p5swGMA+wkS380Ayq6z1LKIMSqNYRXkVTZrGjlnQG9HT5P47jxxhvNmxdwou86ytOyjsqHHBXuCWOaO4/vi3/1OF566aW7775bfB5Tm7ig5ToGADe1rqMMwgrbEgZUxq1yNtA777xz8uTJzbb6t7mN4/7773/33XfzjeQzzzwTG8xSx73WUfazjiIb+SAIqPjLUOSdl19++bHHHrv++utt0+wcbc7j+PTTT1999dVPPvkkTz/++OOZM2fCn1yatKWKyKQQZR0lQKyjrCgNQrxgBhoR+9dee+2NN96YNWvWc889Z1blUyd9rTmO119//Zlnnnnqqaecfemll3744YeBgb3zzjtaAa8O+L4EwlJLLbVsbVb3COE4fjhu9HTMiO+9915tGsSbb76pfTPR/u233/7iiy8+//zzTzzxxE033fTee++lte5xePmDDz4waV4AYz52wxz09MEHH7zqqqtOOOEE8vDWW2/lpa80JD399NNvueWWRx55RK/QBYwthGbPnp0P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ahghxGRW2imtRj7zdmRur+sC1okpqcdVzqdBV155ZXMcf5r9SXDMnj37qaeeeuONNz766KP8rgEIJNFTTz315JNPJhBhCufTws0228zQuZ068j8zeeapg3nnCQoYMW7cuAinzIoU0QsnaIol4gou+QGM++s/h7nwwgslrObg5svmB478X05+cSF/vVMMItJnEkTKbRMGROoo3kZ++9AxbVtmJeJIBAL/nUYmrE0wIu/XM3hRVlQSU3hBm2Ghlzdre+WVVwzp5ZdffvLJJx988MHbb799+vTpEyZMmI//XZkzHCb5em35cZQcfOyxx1588cV3331XN+UnMGKeOu7Ml156CV8uu+wyPk9GUCaonUxJQelM+6aK/+TASzEsyDtvy9X/GBSNiGkK48aOHQssQaRZASK+wP3CCy/MnDkTFvrNaF999VVH4MJbjzzyyF133XXddddxj4Pvvffe008/fccddyDRtddee8899+QvpXvZF3CY0uOPPz5jxoxAENQ1/cwzzzz88MO33norLvRCAWrvvPMOz4Sx+rj55psvvfRSwUI+8t6qEGie3cfMM3NuNUFUftull5EnyWWttdYSLNdff73ZOhMj4GLYhgERwzZ4U/CUb5577jmTcpC3DNWOl/KHS+LLpMTXJZdc0uygwBEJ+PDDD3neJNknn3xi/ppwHCk0arZgdjLJeOutt/QHKU4g7zfccMMFF1wABfUVMlP+BfVTKHM0UiVDHX/88Yraa665hs/FCH8gBVx4ES6GzUw+XM7/OZmUa/ObKUA0eIjwH8lPkuqGA8Cu7CUEacLFNAIKenrooYdcfMUVV4QvnGMo5513Xj7clWLBaJrXD4kZMHkiE2eeeeZVV10lEDCI54yWCznM/FsZbUZ8KUzghSMmDgVEc7k1lP3F0ImZs/O0HhRzMVyFHKSETNilP1haZZx77rnSm9xB4VgrjgvLaITok9eN8MYbb+RCAcLHrTNiIgC1scOsH330Uczi4LPOOktKkgEX42c087KLncowwhYQMCJOrcokvZ9zzjkTJ05Mkmv28DUz5L333nvFgsGbAi5kXnbMES9QOFgkzMlHlpGItpjouu222xCMHEIUwUATidaipl2GgXgBxZDCQoM66KDZ/9fPLr/88kSNSXGnSZmO6BBBXOs4puMFgSN5U6ZMUStlJdGtHRdffLEX7rzzTk3Q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" 31 | } 32 | }, 33 | "cell_type": "markdown", 34 | "metadata": {}, 35 | "source": [ 36 | "### Introduction\n", 37 | "Scientists can learn so much more by analyzing the direct physical obervables obtained in a laboratory setting. Microwave spectroscopy is an instrumental technique that can determine the rotational constants of small molecules. From these experimental rotational constants, the Kraitchman equations can be used to determine the molecular structure to high precision of a reference molecule and isotope substituted isotopologs.\n", 38 | "\n", 39 | "$$z_i=\\sqrt{\\left ( \\frac{\\hbar}{4\\pi} \\right )\n", 40 | "\\left ( \\frac{1}{\\Delta m_i}+\\frac{1}{M} \\right )\n", 41 | "\\left ( \\frac{A^{\\circ}}{B^{\\circ}} \\right )\n", 42 | "\\left ( 1-\\frac{B^{\\circ}}{A_i} \\right )\n", 43 | "\\left ( 1-\\frac{B^{\\circ}}{B_i} \\right )\n", 44 | "\\left ( \\frac{1}{B^{\\circ}-A^{\\circ}} \\right ) }$$\n", 45 | "\n", 46 | "$$y_i=\\sqrt{\\left ( \\frac{\\hbar}{4\\pi} \\right )\n", 47 | "\\left ( \\frac{1}{\\Delta m_i}+\\frac{1}{M} \\right )\n", 48 | "\\left ( \\frac{B^{\\circ}}{A^{\\circ}} \\right )\n", 49 | "\\left ( 1-\\frac{A^{\\circ}}{A_i} \\right )\n", 50 | "\\left ( 1-\\frac{A^{\\circ}}{B_i} \\right )\n", 51 | "\\left ( \\frac{1}{A^{\\circ}-B^{\\circ}} \\right ) }$$\n", 52 | "\n", 53 | "Here $z_i$ and $y_i$ give the positive displacement of atom *i* from the center of mass in meters. The other\n", 54 | "variables are defined as follows:\n", 55 | "\n", 56 | "$\\Delta m_i \\Rightarrow $ difference of isotope mass from reference atom mass in *kg*\n", 57 | "\n", 58 | "$M \\Rightarrow $ total mass of reference molecule in *kg*\n", 59 | "\n", 60 | "$A^{\\circ} \\Rightarrow $ rotational constant A of reference molecule in $m^{-1}$\n", 61 | "\n", 62 | "$B^{\\circ} \\Rightarrow $ rotational constant B of reference molecule in $m^{-1}$\n", 63 | "\n", 64 | "$A_i \\Rightarrow $ rotational constant A of isotopolog in $m^{-1}$\n", 65 | "\n", 66 | "$B_i \\Rightarrow $ rotational constant B of isotopolog in $m^{-1}$\n", 67 | "\n", 68 | "We will use these equations to determine the precise molecular structure of ethenone with C2v symmetry pictured below.\n", 69 | "\n", 70 | "
\n", 71 | "\n", 72 | "Consider the molecule to be oriented in the *yz* plane with the *z* axis along the principle axis of rotation. This automatically means the x coordinate for each atom is 0. Assume the oxygen is displaced in the positive *z* direction.\n", 73 | "\n", 74 | "The following table contains experimental high-resolution rotational constants in MHz for isotopologs for ethenone.\n", 75 | "\n", 76 | "| | A | B | C |\n", 77 | "| :-: | :-: | :-: | :-: |\n", 78 | "| H2CCO | 282101.185 | 10293.32117 | 9915.90548 |\n", 79 | "| H2HCCO | 194305 | 9647.0664 | 9174.6457 |\n", 80 | "| H213CCO | 282112 | 9960.9659 | 9607.1276 |\n", 81 | "| H2CC18O | 287350 | 9761.2368 | 9421.1236 |\n", 82 | "\n", 83 | "The mass of the atoms and their isotopes are needed. The reference molecule (first row of the above table) contains the most abundant isotope of each atom type. The isotope masses are listed here. Source: [NIST](https://www.nist.gov/pml/atomic-weights-and-isotopic-compositions-relative-atomic-masses)\n", 84 | "\n", 85 | "| | mass in *Daltons* |\n", 86 | "| :-: | :-: |\n", 87 | "| 1H | 1.00782503223 |\n", 88 | "| 2H | 2.01410177812 |\n", 89 | "| 12C | 12. |\n", 90 | "| 13C | 13.00335483507 |\n", 91 | "| 16O | 15.99491461957 |\n", 92 | "| 18O | 17.99915961286 |\n" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "# ==> Import Psi4 & NumPy <==\n", 102 | "import psi4\n", 103 | "import numpy as np" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Part 1 - Using the Kraitchman equations\n", 111 | "Use the Kraitchman equations to find the displacement in the *z* and *y* directions of each atom except the central carbon (one connected to oxygen). Modify the code below as needed." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "# Define all constants and values needed for Kraitchman equations and convert to SI units\n", 121 | "h_bar = 1.054571817e-34 # Planck's constant in J*s\n", 122 | "RotConst_MHz = np.array([[282101.185, 10293.32117, 9915.90548],\n", 123 | " [194305,9647.0664, 9174.6457],\n", 124 | " [282112,9960.9659, 9607.1276],\n", 125 | " [287350, 9761.2368, 9421.1236]])\n", 126 | "\n", 127 | "# Convert rotational constants to SI units\n", 128 | "RotConst_Hz = \n", 129 | "\n", 130 | "# Define reference rotational constants for easier use\n", 131 | "RotConst_A_ref = RotConst_Hz[0][0]\n", 132 | "RotConst_B_ref = \n", 133 | "\n", 134 | "# Define mass constants\n", 135 | "amu_to_kg = \n", 136 | "M_ref = np.array([1.00782503223,12.,15.99491461957])\n", 137 | "M_iso = np.array([2.01410177812,13.00335483507,17.99915961286])\n", 138 | "\n", 139 | "# Determine total mass of reference molecule in kg\n", 140 | "M_total_ref =\n", 141 | "\n", 142 | "# Determine mass difference from isotopolog and reference molecule\n", 143 | "M_delta = (M_iso - M_ref) * amu_to_kg" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "# Define functions for Kraitchman equation\n", 153 | "# Each k is one of the 6 terms multiplied together within the square root in the equation provided above\n", 154 | "def kraitchman_z(M_delta, RotConst_A, RotConst_B):\n", 155 | " k1 = \n", 156 | " k2 = \n", 157 | " k3 = \n", 158 | " k4 = \n", 159 | " k5 = \n", 160 | " k6 = \n", 161 | " z = np.sqrt(k1 * k2 * k3 * k4 * k5 * k6)\n", 162 | " z *= # convert from m to Angstroms\n", 163 | " return z\n", 164 | "\n", 165 | "def kraitchman_y(M_delta, RotConst_A, RotConst_B):\n", 166 | " k1 = \n", 167 | " k2 = \n", 168 | " k3 = \n", 169 | " k4 = \n", 170 | " k5 = \n", 171 | " k6 = \n", 172 | " y = np.sqrt(k1 * k2 * k3 * k4 * k5 * k6 + 0j).real # to avoid errors when computing value that should be exactly 0\n", 173 | " y *= # convert from m to Angstroms\n", 174 | " return y" 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "metadata": { 181 | "scrolled": false 182 | }, 183 | "outputs": [], 184 | "source": [ 185 | "# Use defined functions to solve Kraitchman equations and print results\n", 186 | "# Since the Kraitchman equations only return positive displacements we have to manually assign +/- signs\n", 187 | "zcoord_H = kraitchman_z(M_delta[0], RotConst_Hz[1][0], RotConst_Hz[1][1])\n", 188 | "ycoord_H = \n", 189 | "zcoord_C = \n", 190 | "ycoord_C = \n", 191 | "zcoord_O = \n", 192 | "ycoord_O = \n", 193 | "\n", 194 | "# Build xyz coordinates and replace y & z zeros as needed\n", 195 | "xyz_coords = np.array([[0.0, ycoord_H, 0.0],\n", 196 | " [0.0, 0.0, 0.0],\n", 197 | " [0.0, 0.0, 0.0],\n", 198 | " [0.0, 0.0, 0.0], # temporarily set as zero\n", 199 | " [0.0, 0.0, 0.0]])\n", 200 | "print(\"Incomplete xyz coordinates in Angstroms\")\n", 201 | "print(xyz_coords)" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "---\n", 209 | "## Part 2 - Use center of mass to find xyz of final carbon atom\n", 210 | "Now that the xyz coordinates have been found for four of the five atoms, that information can be used to find the coordinates of the final carbon atom. The Kraitchman equations assume that the origin is located at the center of mass. So the center of mass equation can be used to find the missing information.\n", 211 | "\n", 212 | "$$y=0=\\frac{\\sum_{i=1}^{N}m_i y_i}{total mass} $$\n", 213 | "\n", 214 | "$$z=0=\\frac{\\sum_{i=1}^{N}m_i z_i}{total mass} $$\n" 215 | ] 216 | }, 217 | { 218 | "cell_type": "code", 219 | "execution_count": null, 220 | "metadata": { 221 | "scrolled": true 222 | }, 223 | "outputs": [], 224 | "source": [ 225 | "# Rearrange and solve center of mass equation for coordinates of final central carbon atom\n", 226 | "# Remember to manually assign +/- signs\n", 227 | "\n", 228 | "zcoord_C2 = \n", 229 | "\n", 230 | "xyz_coords[3,2] = zcoord_C2\n", 231 | "print(\"Final xyz coordinates in Angstroms\\n\")\n", 232 | "print(xyz_coords)" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "---\n", 240 | "## Part 3 - Bond lengths and bond angles\n", 241 | "Compute all bond lengths and the H-C-H bond angle." 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | "metadata": {}, 248 | "outputs": [], 249 | "source": [ 250 | "# bond lengths\n", 251 | "rCO = \n", 252 | "rCC = \n", 253 | "rCH = \n", 254 | "# H-C-H bond angle\n", 255 | "aHCH = \n", 256 | "\n", 257 | "# Print results\n", 258 | "print(f\"rCO = {rCO:.4f} Å\")\n", 259 | "print()\n", 260 | "print()\n", 261 | "print()" 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "---\n", 269 | "## Part 4 - Calculate Rotational Constants with Psi4\n", 270 | "Use Psi4 to determine the rotational constants from your determined xyz geometry." 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": { 277 | "scrolled": true 278 | }, 279 | "outputs": [], 280 | "source": [ 281 | "# Define molecule geometry in angstroms\n", 282 | "# Example: \"H 0.0 1.23 4.56\" for each atom\n", 283 | "\n", 284 | "mol = psi4.geometry(\"\"\"\n", 285 | "H \n", 286 | "H \n", 287 | "C \n", 288 | "C \n", 289 | "O \n", 290 | "\"\"\")\n", 291 | "\n", 292 | "# Set each atoms isotope mass\n", 293 | "mol.set_mass(0,1.00782503223)\n", 294 | "mol.set_mass()\n", 295 | "mol.set_mass()\n", 296 | "mol.set_mass()\n", 297 | "mol.set_mass()\n", 298 | "\n", 299 | "# Find rotational constants\n", 300 | "rot_conts_wn = psi4.core.Molecule.rotational_constants(mol).to_array() # in cm^-1\n", 301 | "print(rot_conts_wn,\" in cm^-1\")\n", 302 | "\n", 303 | "# convert to MHz\n", 304 | "c = # speed of light in m/s\n", 305 | "rot_conts_MHz = \n", 306 | "print(rot_conts_MHz,\" in MHz\")" 307 | ] 308 | }, 309 | { 310 | "cell_type": "markdown", 311 | "metadata": {}, 312 | "source": [ 313 | "---\n", 314 | "## Part 5 - Analyze\n", 315 | "Answer the following.\n", 316 | "\n", 317 | "1. Compare the results from Psi4 to the provided experimental literature values.\n", 318 | "2. Why does the geometry/structure of a molecule and the rotational constants have a relationship? Think about how a rotational constant is defined." 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": null, 324 | "metadata": {}, 325 | "outputs": [], 326 | "source": [ 327 | "# Write your answers below\n", 328 | "\n" 329 | ] 330 | } 331 | ], 332 | "metadata": { 333 | "kernelspec": { 334 | "display_name": "Python 3", 335 | "language": "python", 336 | "name": "python3" 337 | }, 338 | "language_info": { 339 | "codemirror_mode": { 340 | "name": "ipython", 341 | "version": 3 342 | }, 343 | "file_extension": ".py", 344 | "mimetype": "text/x-python", 345 | "name": "python", 346 | "nbconvert_exporter": "python", 347 | "pygments_lexer": "ipython3", 348 | "version": "3.6.10" 349 | } 350 | }, 351 | "nbformat": 4, 352 | "nbformat_minor": 2 353 | } 354 | -------------------------------------------------------------------------------- /labs/PIB/Box1D_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/PIB/Box1D_student.pdf -------------------------------------------------------------------------------- /labs/PIB/Box1D_student_ChemCompute.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/PIB/Box1D_student_ChemCompute.pdf -------------------------------------------------------------------------------- /labs/Polarity/Polar_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Polarity/Polar_student.pdf -------------------------------------------------------------------------------- /labs/Symmetry/Symmetry_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Symmetry/Symmetry_student.pdf -------------------------------------------------------------------------------- /labs/Symmetry_Adapted_Perturbation_Theory/sapt0_student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Intermolecular Interactions and Symmetry-Adapted Perturbation Theory\"\"\"\n", 10 | "\n", 11 | "__authors__ = \"Konrad Patkowski\"\n", 12 | "__email__ = [\"patkowsk@auburn.edu\"]\n", 13 | "\n", 14 | "__copyright__ = \"(c) 2008-2020, The Psi4Education Developers\"\n", 15 | "__license__ = \"BSD-3-Clause\"\n", 16 | "__date__ = \"2020-07-16\"" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "This lab activity is designed to teach students about weak intermolecular interactions, and the calculation and interpretation of the interaction energy between two molecules. The interaction energy can be broken down into physically meaningful contributions (electrostatics, induction, dispersion, and exchange) using symmetry-adapted perturbation theory (SAPT). In this exercise, we will calculate complete interaction energies and their SAPT decomposition using the procedures from the Psi4 software package, processing and analyzing the data with NumPy and Matplotlib.\n", 24 | "\n", 25 | "Prerequisite knowledge: the Hartree-Fock method, molecular orbitals, electron correlation and the MP2 theory. The lab also assumes all the standard Python prerequisites of all Psi4Education labs.\n", 26 | "\n", 27 | "Learning Objectives: \n", 28 | "1. Recognize and appreciate the ubiquity and diversity of intermolecular interactions.\n", 29 | "2. Compare and contrast the supermolecular and perturbative methods of calculating interaction energy.\n", 30 | "3. Analyze and interpret the electrostatic, induction, dispersion, and exchange SAPT contributions at different intermolecular separations.\n", 31 | "\n", 32 | "Author: Konrad Patkowski, Auburn University (patkowsk@auburn.edu; ORCID: 0000-0002-4468-207X)\n", 33 | "\n", 34 | "Copyright: Psi4Education Project, 2020\n", 35 | "\n", 36 | "# Weak intermolecular interactions \n", 37 | "\n", 38 | "In this activity, you will examine some properties of weak interactions between molecules. As the molecular subunits are not connected by any covalent (or ionic) bonds, we often use the term *noncovalent interactions*. Suppose we want to calculate the interaction energy between molecule A and molecule B for a certain geometry of the A-B complex (obviously, this interaction energy depends on how far apart the molecules are and how they are oriented). The simplest way of doing so is by subtraction (in the so-called *supermolecular approach*):\n", 39 | "\n", 40 | "\\begin{equation}\n", 41 | "E_{\\rm int}=E_{\\rm A-B}-E_{\\rm A}-E_{\\rm B}\n", 42 | "\\end{equation}\n", 43 | "\n", 44 | "where $E_{\\rm X}$ is the total energy of system X, computed using our favorite electronic structure theory and basis set. A negative value of $E_{\\rm int}$ means that A and B have a lower energy when they are together than when they are apart, so they do form a weakly bound complex that might be stable at least at very low temperatures. A positive value of $E_{\\rm int}$ means that the A-B complex is unbound - it is energetically favorable for A and B to go their separate ways. \n", 45 | "\n", 46 | "Let's consider a simple example of two interacting helium atoms and calculate $E_{\\rm int}$ at a few different interatomic distances $R$. You will use Psi4 to calculate the total energies that you need to perform subtraction. When you do so for a couple different $R$, you will be able to sketch the *potential energy curve* - the graph of $E_{\\rm int}(R)$ as a function of $R$.\n", 47 | "\n", 48 | "OK, but how should you pick the electronic structure method to calculate $E_{\\rm A-B}$, $E_{\\rm A}$, and $E_{\\rm B}$? Let's start with the simplest choice and try out the Hartree-Fock (HF) method. In case HF is not accurate enough, we will also try the coupled-cluster method with single, double, and perturbative triple excitations - CCSD(T). If you haven't heard about CCSD(T) before, let's just state that it is **(1)** usually very accurate (it's even called the *gold standard* of electronic structure theory) and **(2)** very expensive for larger molecules. For the basis set, let's pick the augmented correlation consistent triple-zeta (aug-cc-pVTZ) basis of Dunning which should be quite OK for both HF and CCSD(T).\n" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "# A simple Psi4 input script to compute the potential energy curve for two helium atoms\n", 58 | "\n", 59 | "%matplotlib notebook\n", 60 | "import time\n", 61 | "import numpy as np\n", 62 | "import scipy\n", 63 | "from scipy.optimize import *\n", 64 | "np.set_printoptions(precision=5, linewidth=200, threshold=2000, suppress=True)\n", 65 | "import psi4\n", 66 | "import matplotlib.pyplot as plt\n", 67 | "\n", 68 | "# Set Psi4 & NumPy Memory Options\n", 69 | "psi4.set_memory('2 GB')\n", 70 | "psi4.core.set_output_file('output.dat', False)\n", 71 | "\n", 72 | "numpy_memory = 2\n", 73 | "\n", 74 | "psi4.set_options({'basis': 'aug-cc-pVTZ',\n", 75 | " 'e_convergence': 1e-10,\n", 76 | " 'd_convergence': 1e-10,\n", 77 | " 'INTS_TOLERANCE': 1e-15})\n", 78 | "\n" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "We need to collect some data points to graph the function $E_{\\rm int}(R)$. Therefore, we set up a list of distances $R$ for which we will run the calculations (we go with 11 of them). For each distance, we need to remember three values ($E_{\\rm A-B}$, $E_{\\rm A}$, and $E_{\\rm B}$). For this purpose, we will prepare two $11\\times 3$ NumPy arrays to hold the HF and CCSD(T) results. \n" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "distances = [4.0,4.5,5.0,5.3,5.6,6.0,6.5,7.0,8.0,9.0,10.0]\n", 95 | "ehf = np.zeros((11,3))\n", 96 | "eccsdt = np.zeros((11,3))\n", 97 | "\n" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "We are almost ready to crunch some numbers! One question though: how are we going to tell Psi4 whether we want $E_{\\rm A-B}$, $E_{\\rm A}$, or $E_{\\rm B}$? \n", 105 | "We need to define three different geometries. The $E_{\\rm A-B}$ one has two helium atoms $R$ atomic units from each other - we can place one atom at $(0,0,0)$ and the other at $(0,0,R)$. The other two geometries involve one actual helium atom, with a nucleus and two electrons, and one *ghost atom* in place of the other one. A ghost atom does not have a nucleus or electrons, but it does carry the same basis functions as an actual atom - we need to calculate all energies in the same basis set, with functions centered at both $(0,0,0)$ and $(0,0,R)$, to prevent the so-called *basis set superposition error*. In Psi4, the syntax `Gh(X)` denotes a ghost atom where basis functions for atom type X are located. \n", 106 | "\n", 107 | "Using ghost atoms, we can now easily define geometries for the $E_{\\rm A}$ and $E_{\\rm B}$ calculations.\n" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "for i in range(len(distances)):\n", 117 | " dimer = psi4.geometry(\"\"\"\n", 118 | " He 0.0 0.0 0.0\n", 119 | " --\n", 120 | " He 0.0 0.0 \"\"\"+str(distances[i])+\"\"\"\n", 121 | " units bohr\n", 122 | " symmetry c1\n", 123 | " \"\"\")\n", 124 | "\n", 125 | " psi4.energy('ccsd(t)') #HF will be calculated along the way\n", 126 | " ehf[i,0] = psi4.variable('HF TOTAL ENERGY')\n", 127 | " eccsdt[i,0] = psi4.variable('CCSD(T) TOTAL ENERGY')\n", 128 | " psi4.core.clean()\n", 129 | "\n", 130 | " monomerA = psi4.geometry(\"\"\"\n", 131 | " He 0.0 0.0 0.0\n", 132 | " --\n", 133 | " Gh(He) 0.0 0.0 \"\"\"+str(distances[i])+\"\"\"\n", 134 | " units bohr\n", 135 | " symmetry c1\n", 136 | " \"\"\")\n", 137 | "\n", 138 | " psi4.energy('ccsd(t)') #HF will be calculated along the way\n", 139 | " ehf[i,1] = psi4.variable('HF TOTAL ENERGY')\n", 140 | " eccsdt[i,1] = psi4.variable('CCSD(T) TOTAL ENERGY')\n", 141 | " psi4.core.clean()\n", 142 | "\n", 143 | " monomerB = psi4.geometry(\"\"\"\n", 144 | " Gh(He) 0.0 0.0 0.0\n", 145 | " --\n", 146 | " He 0.0 0.0 \"\"\"+str(distances[i])+\"\"\"\n", 147 | " units bohr\n", 148 | " symmetry c1\n", 149 | " \"\"\")\n", 150 | "\n", 151 | " psi4.energy('ccsd(t)') #HF will be calculated along the way\n", 152 | " ehf[i,2] = psi4.variable('HF TOTAL ENERGY')\n", 153 | " eccsdt[i,2] = psi4.variable('CCSD(T) TOTAL ENERGY')\n", 154 | " psi4.core.clean()\n", 155 | "\n" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "metadata": {}, 161 | "source": [ 162 | "We have completed the $E_{\\rm A-B}$, $E_{\\rm A}$, or $E_{\\rm B}$ calculations for all 11 distances $R$ (it didn't take that long, did it?). We will now perform the subtraction to form NumPy arrays with $E_{\\rm int}(R)$ values for each method, converted from atomic units (hartrees) to kcal/mol, and graph the resulting potential energy curves using the matplotlib library. \n" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "#COMPLETE the two lines below to generate interaction energies. Convert them from atomic units to kcal/mol.\n", 172 | "einthf = \n", 173 | "eintccsdt = \n", 174 | "\n", 175 | "print ('HF PEC',einthf)\n", 176 | "print ('CCSD(T) PEC',eintccsdt)\n", 177 | "\n", 178 | "plt.plot(distances,einthf,'r+',linestyle='-',label='HF')\n", 179 | "plt.plot(distances,eintccsdt,'bo',linestyle='-',label='CCSD(T)')\n", 180 | "plt.hlines(0.0,4.0,10.0)\n", 181 | "plt.legend(loc='upper right')\n", 182 | "plt.show()\n" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "*Questions* \n", 190 | "1. Which curve makes more physical sense?\n", 191 | "2. Why does helium form a liquid at very low temperatures?\n", 192 | "3. You learned in freshman chemistry that two helium atoms do not form a molecule because there are two electrons on a bonding orbital and two electrons on an antibonding orbital. How does this information relate to the behavior of HF (which does assume a molecular orbital for every electron) and CCSD(T) (which goes beyond the molecular orbital picture)?\n", 193 | "4. When you increase the size of the interacting molecules, the CCSD(T) method quickly gets much more expensive and your calculation might take weeks instead of seconds. It gets especially expensive for the calculation of $E_{\\rm A-B}$ because A-B has more electrons than either A or B. Your friend suggests to use CCSD(T) only for the easier terms $E_{\\rm A}$ and $E_{\\rm B}$ and subtract them from $E_{\\rm A-B}$ calculated with a different, cheaper method such as HF. Why is this a really bad idea?\n", 194 | "\n", 195 | "*To answer the questions above, please double click this Markdown cell to edit it. When you are done entering your answers, run this cell as if it was a code cell, and your Markdown source will be recompiled.*\n" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "A nice feature of the supermolecular approach is that it is very easy to use - you just need to run three standard energy calculations, and modern quantum chemistry codes such as Psi4 give you a lot of methods to choose from. However, the accuracy of subtraction hinges on error cancellation, and we have to be careful to ensure that the errors do cancel between $E_{\\rm A-B}$ and $E_{\\rm A}+E_{\\rm B}$. Another drawback of the supermolecular approach is that it is not particularly rich in physical insight. All that we get is a single number $E_{\\rm int}$ that tells us very little about the underlying physics of the interaction. Therefore, one may want to find an alternative approach where $E_{\\rm int}$ is computed directly, without subtraction, and it is obtained as a sum of distinct, physically meaningful terms. Symmetry-adapted perturbation theory (SAPT) is such an alternative approach.\n", 203 | "\n", 204 | "# Symmetry-Adapted Perturbation Theory (SAPT)\n", 205 | "\n", 206 | "SAPT is a perturbation theory aimed specifically at calculating the interaction energy between two molecules. Contrary to the supermolecular approach, SAPT obtains the interaction energy directly - no subtraction of similar terms is needed. Moreover, the result is obtained as a sum of separate corrections accounting for the electrostatic, induction, dispersion, and exchange contributions to interaction energy, so the SAPT decomposition facilitates the understanding and physical interpretation of results.\n", 207 | "- *Electrostatic energy* arises from the Coulomb interaction between charge densities of isolated molecules.\n", 208 | "- *Induction energy* is the energetic effect of mutual polarization between the two molecules.\n", 209 | "- *Dispersion energy* is a consequence of intermolecular electron correlation, usually explained in terms of correlated fluctuations of electron density on both molecules.\n", 210 | "- *Exchange energy* is a short-range repulsive effect that is a consequence of the Pauli exclusion principle.\n", 211 | "\n", 212 | "In this activity, we will explore the simplest level of the SAPT theory called SAPT0 (see [Parker:2014] for the definitions of different levels of SAPT). A particular SAPT correction $E^{(nk)}$ corresponds to effects that are of $n$th order in the intermolecular interaction and $k$th order in the intramolecular electron correlation. In SAPT0, intramolecular correlation is neglected, and intermolecular interaction is included through second order:\n", 213 | "\n", 214 | "\\begin{equation}\n", 215 | "E_{\\rm int}^{\\rm SAPT0}=E^{(10)}_{\\rm elst}+E^{(10)}_{\\rm exch}+E^{(20)}_{\\rm ind,resp}+E^{(20)}_{\\rm exch-ind,resp}+E^{(20)}_{\\rm disp}+E^{(20)}_{\\rm exch-disp}+\\delta E^{(2)}_{\\rm HF}\n", 216 | "\\end{equation}\n", 217 | "\n", 218 | "In this equation, the consecutive corrections account for the electrostatic, first-order exchange, induction, exchange induction, dispersion, and exchange dispersion effects, respectively. The additional subscript ''resp'' denotes that these corrections are computed including response effects - the HF orbitals of each molecule are relaxed in the electric field generated by the other molecule. The last term $\\delta E^{(2)}_{\\rm HF}$ approximates third- and higher-order induction and exchange induction effects and is taken from a supermolecular HF calculation.\n", 219 | "\n", 220 | "Sticking to our example of two helium atoms, let's now calculate the SAPT0 interaction energy contributions using Psi4. In the results that follow, we will group $E^{(20)}_{\\rm ind,resp}$, $E^{(20)}_{\\rm exch-ind,resp}$, and $\\delta E^{(2)}_{\\rm HF}$ to define the total induction effect (including its exchange quenching), and group $E^{(20)}_{\\rm disp}$ with $E^{(20)}_{\\rm exch-disp}$ to define the total dispersion effect.\n" 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "metadata": {}, 227 | "outputs": [], 228 | "source": [ 229 | "distances = [4.0,4.5,5.0,5.3,5.6,6.0,6.5,7.0,8.0,9.0,10.0]\n", 230 | "eelst = np.zeros((11))\n", 231 | "eexch = np.zeros((11))\n", 232 | "eind = np.zeros((11))\n", 233 | "edisp = np.zeros((11))\n", 234 | "esapt = np.zeros((11))\n", 235 | "\n", 236 | "for i in range(len(distances)):\n", 237 | " dimer = psi4.geometry(\"\"\"\n", 238 | " He 0.0 0.0 0.0\n", 239 | " --\n", 240 | " He 0.0 0.0 \"\"\"+str(distances[i])+\"\"\"\n", 241 | " units bohr\n", 242 | " symmetry c1\n", 243 | " \"\"\")\n", 244 | "\n", 245 | " psi4.energy('sapt0')\n", 246 | " eelst[i] = psi4.variable('SAPT ELST ENERGY') * 627.509\n", 247 | " eexch[i] = psi4.variable('SAPT EXCH ENERGY') * 627.509\n", 248 | " eind[i] = psi4.variable('SAPT IND ENERGY') * 627.509\n", 249 | " edisp[i] = psi4.variable('SAPT DISP ENERGY') * 627.509\n", 250 | " esapt[i] = psi4.variable('SAPT TOTAL ENERGY') * 627.509\n", 251 | " psi4.core.clean()\n", 252 | "\n", 253 | "plt.close()\n", 254 | "plt.ylim(-0.2,0.4)\n", 255 | "plt.plot(distances,eelst,'r+',linestyle='-',label='SAPT0 elst')\n", 256 | "plt.plot(distances,eexch,'bo',linestyle='-',label='SAPT0 exch')\n", 257 | "plt.plot(distances,eind,'g^',linestyle='-',label='SAPT0 ind')\n", 258 | "plt.plot(distances,edisp,'mx',linestyle='-',label='SAPT0 disp')\n", 259 | "plt.plot(distances,esapt,'k*',linestyle='-',label='SAPT0 total')\n", 260 | "plt.hlines(0.0,4.0,10.0)\n", 261 | "plt.legend(loc='upper right')\n", 262 | "plt.show()\n" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "*Questions* \n", 270 | "1. What is the origin of attraction between two helium atoms?\n", 271 | "2. For the interaction of two helium atoms, which SAPT terms are *long-range* (vanish with distance like some inverse power of $R$) and which are *short-range* (vanish exponentially with $R$ just like the overlap of molecular orbitals)?\n", 272 | "3. The dispersion energy decays at large $R$ like $R^{-n}$. Find the value of $n$ by fitting a function to the five largest-$R$ results. You can use `scipy.optimize.curve_fit` to perform the fitting, but you have to define the appropriate function first.\n", 273 | "Does the optimal exponent $n$ obtained by your fit agree with what you know about van der Waals dispersion forces? Is the graph of dispersion energy shaped like the $R^{-n}$ graph for large $R$? What about intermediate $R$?\n", 274 | "\n", 275 | "*Do you know how to calculate $R^{-n}$ if you have an array with $R$ values? If not, look it up in the NumPy documentation!* \n" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": null, 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "#COMPLETE the definition of function f below.\n", 285 | "def f\n", 286 | "\n", 287 | "ndisp = scipy.optimize.curve_fit(f,distances[-5:],edisp[-5:])\n", 288 | "print (\"Optimal dispersion exponent:\",ndisp[0][0])\n" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "metadata": {}, 294 | "source": [ 295 | "# Interaction between two water molecules\n", 296 | "\n", 297 | "For the next part, you will perform the same analysis and obtain the supermolecular and SAPT0 data for the interaction of two water molecules. We now have many more degrees of freedom: in addition to the intermolecular distance $R$, we can change the relative orientation of two molecules, or even their internal geometries (O-H bond lengths and H-O-H angles). In this way, the potential energy curve becomes a multidimensional *potential energy surface*. It is hard to graph functions of more than two variables, so we will stick to the distance dependence of the interaction energies. Therefore, we will assume one particular orientation of two water molecules (a hydrogen-bonded one) and vary the intermolecular distance $R$ while keeping the orientation, and molecular geometries, constant. The geometry of the A-B complex has been defined for you, but you have to request all the necessary Psi4 calculations and extract the numbers that you need. To save time, we will downgrade the basis set to aug-cc-pVDZ and use MP2 (an approximate method that captures most of electron correlation) in place of CCSD(T).\n", 298 | "\n", 299 | "*Hints:* To prepare the geometries for the individual water molecules A and B, copy and paste the A-B geometry, but use the Gh(O2)... syntax to define the appropriate ghost atoms. Remember to run `psi4.core.clean()` after each calculation.\n" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": null, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "distances_h2o = [2.7,3.0,3.5,4.0,4.5,5.0,6.0,7.0,8.0,9.0]\n", 309 | "ehf_h2o = np.zeros((10,3))\n", 310 | "emp2_h2o = np.zeros((10,3))\n", 311 | "psi4.set_options({'basis': 'aug-cc-pVDZ'})\n", 312 | "\n", 313 | "for i in range(len(distances_h2o)):\n", 314 | " dimer = psi4.geometry(\"\"\"\n", 315 | " O1\n", 316 | " H1 O1 0.96\n", 317 | " H2 O1 0.96 H1 104.5\n", 318 | " --\n", 319 | " O2 O1 \"\"\"+str(distances_h2o[i])+\"\"\" H1 5.0 H2 0.0\n", 320 | " X O2 1.0 O1 120.0 H2 180.0\n", 321 | " H3 O2 0.96 X 52.25 O1 90.0\n", 322 | " H4 O2 0.96 X 52.25 O1 -90.0\n", 323 | " units angstrom\n", 324 | " symmetry c1\n", 325 | " \"\"\")\n", 326 | "\n", 327 | "#COMPLETE the MP2 energy calculations for A-B, A, and B, and prepare the data for the graph.\n", 328 | "#Copy and paste the A-B geometry, but use the Gh(O2)... syntax to define the appropriate ghost atoms for the A and B calculations. \n", 329 | "#Remember to run psi4.core.clean() after each calculation.\n", 330 | "\n", 331 | "print ('HF PEC',einthf_h2o)\n", 332 | "print ('MP2 PEC',eintmp2_h2o)\n", 333 | "\n", 334 | "plt.close()\n", 335 | "plt.plot(distances_h2o,einthf_h2o,'r+',linestyle='-',label='HF')\n", 336 | "plt.plot(distances_h2o,eintmp2_h2o,'bo',linestyle='-',label='MP2')\n", 337 | "plt.hlines(0.0,2.5,9.0)\n", 338 | "plt.legend(loc='upper right')\n", 339 | "plt.show()\n" 340 | ] 341 | }, 342 | { 343 | "cell_type": "code", 344 | "execution_count": null, 345 | "metadata": {}, 346 | "outputs": [], 347 | "source": [ 348 | "eelst_h2o = np.zeros((10))\n", 349 | "eexch_h2o = np.zeros((10))\n", 350 | "eind_h2o = np.zeros((10))\n", 351 | "edisp_h2o = np.zeros((10))\n", 352 | "esapt_h2o = np.zeros((10))\n", 353 | "\n", 354 | "#COMPLETE the SAPT calculations for 10 distances to prepare the data for the graph.\n", 355 | "\n", 356 | "plt.close()\n", 357 | "plt.ylim(-10.0,10.0)\n", 358 | "plt.plot(distances_h2o,eelst_h2o,'r+',linestyle='-',label='SAPT0 elst')\n", 359 | "plt.plot(distances_h2o,eexch_h2o,'bo',linestyle='-',label='SAPT0 exch')\n", 360 | "plt.plot(distances_h2o,eind_h2o,'g^',linestyle='-',label='SAPT0 ind')\n", 361 | "plt.plot(distances_h2o,edisp_h2o,'mx',linestyle='-',label='SAPT0 disp')\n", 362 | "plt.plot(distances_h2o,esapt_h2o,'k*',linestyle='-',label='SAPT0 total')\n", 363 | "plt.hlines(0.0,2.5,9.0)\n", 364 | "plt.legend(loc='upper right')\n", 365 | "plt.show()\n" 366 | ] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": {}, 371 | "source": [ 372 | "Before we proceed any further, let us check one thing about your first MP2 water-water interaction energy calculation, the one that produced `eintmp2_h2o[0]`. Here's the geometry of that complex again:\n" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": null, 378 | "metadata": {}, 379 | "outputs": [], 380 | "source": [ 381 | "#all x,y,z in Angstroms\n", 382 | "atomtypes = [\"O1\",\"H1\",\"H2\",\"O2\",\"H3\",\"H4\"]\n", 383 | "coordinates = np.array([[0.116724185090, 1.383860971547, 0.000000000000],\n", 384 | " [0.116724185090, 0.423860971547, 0.000000000000],\n", 385 | " [-0.812697549673, 1.624225775439, 0.000000000000],\n", 386 | " [-0.118596320329, -1.305864713301, 0.000000000000],\n", 387 | " [0.362842754701, -1.642971982825, -0.759061990794],\n", 388 | " [0.362842754701, -1.642971982825, 0.759061990794]])\n" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "metadata": {}, 394 | "source": [ 395 | "First, write the code to compute the four O-H bond lengths and two H-O-H bond angles in the two molecules. *(Hint: if the angles look weird, maybe they are still in radians - don't forget to convert them to degrees.)* Are the two water molecules identical?\n", 396 | "\n", 397 | "Then, check the values of the MP2 energy for these two molecules (the numbers $E_{\\rm A}$ and $E_{\\rm B}$ that you subtracted to get the interaction energy). If the molecules are the same, why are the MP2 energies close but not the same?\n", 398 | "\n", 399 | "*Hints:* The most elegant way to write this code is to define functions `distance(point1,point2)` for the distance between two points $(x_1,y_1,z_1)$ and $(x_2,y_2,z_2)$, and `angle(vec1,vec2)` for the angle between two vectors $(x_{v1},y_{v1},z_{v1})$ and $(x_{v2},y_{v2},z_{v2})$. Recall that the cosine of this angle is related to the dot product $(x_{v1},y_{v1},z_{v1})\\cdot(x_{v2},y_{v2},z_{v2})$. If needed, check the documentation on how to calculate the dot product of two NumPy vectors. \n", 400 | "\n", 401 | "When you are parsing the NumPy array with the coordinates, remember that `coordinates[k,:]` is the vector of $(x,y,z)$ values for atom number $k$, $k=0,1,2,\\ldots,N_{\\rm atoms}-1$. \n" 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": null, 407 | "metadata": {}, 408 | "outputs": [], 409 | "source": [ 410 | "\n", 411 | "#COMPLETE the distance and angle calculations below.\n", 412 | "ro1h1 = \n", 413 | "ro1h2 = \n", 414 | "ro2h3 = \n", 415 | "ro2h4 = \n", 416 | "ah1o1h2 = \n", 417 | "ah3o2h4 = \n", 418 | "print ('O-H distances: %5.3f %5.3f %5.3f %5.3f' % (ro1h1,ro1h2,ro2h3,ro2h4))\n", 419 | "print ('H-O-H angles: %6.2f %6.2f' % (ah1o1h2,ah3o2h4))\n", 420 | "print ('MP2 energy of molecule 1: %18.12f hartrees' % emp2_h2o[0,1])\n", 421 | "print ('MP2 energy of molecule 2: %18.12f hartrees' % emp2_h2o[0,2])\n" 422 | ] 423 | }, 424 | { 425 | "cell_type": "markdown", 426 | "metadata": {}, 427 | "source": [ 428 | "We can now proceed with the analysis of the SAPT0 energy components for the complex of two water molecules. *Please edit this Markdown cell to write your answers.*\n", 429 | "1. Which of the four SAPT terms are long-range, and which are short-range this time?\n", 430 | "2. For the terms that are long-range and decay with $R$ like $R^{-n}$, estimate $n$ by fitting a proper function to the 5 data points with the largest $R$, just like you did for the two interacting helium atoms (using `scipy.optimize.curve_fit`). How would you explain the power $n$ that you obtained for the electrostatic energy?\n" 431 | ] 432 | }, 433 | { 434 | "cell_type": "code", 435 | "execution_count": null, 436 | "metadata": {}, 437 | "outputs": [], 438 | "source": [ 439 | "#COMPLETE the optimizations below. \n", 440 | "nelst_h2o = \n", 441 | "nind_h2o = \n", 442 | "ndisp_h2o = \n", 443 | "print (\"Optimal electrostatics exponent:\",nelst_h2o[0][0])\n", 444 | "print (\"Optimal induction exponent:\",nind_h2o[0][0])\n", 445 | "print (\"Optimal dispersion exponent:\",ndisp_h2o[0][0])\n" 446 | ] 447 | }, 448 | { 449 | "cell_type": "markdown", 450 | "metadata": {}, 451 | "source": [ 452 | "The water molecules are polar - each one has a nonzero dipole moment, and at large distances we expect the electrostatic energy to be dominated by the dipole-dipole interaction (at short distances, when the orbitals of two molecules overlap, the multipole approximation is not valid and the electrostatic energy contains the short-range *charge penetration* effects). Let's check if this is indeed the case. In preparation for this, we first find the HF dipole moment vector for each water molecule. \n" 453 | ] 454 | }, 455 | { 456 | "cell_type": "code", 457 | "execution_count": null, 458 | "metadata": {}, 459 | "outputs": [], 460 | "source": [ 461 | "waterA = psi4.geometry(\"\"\"\n", 462 | "O 0.116724185090 1.383860971547 0.000000000000\n", 463 | "H 0.116724185090 0.423860971547 0.000000000000\n", 464 | "H -0.812697549673 1.624225775439 0.000000000000\n", 465 | "units angstrom\n", 466 | "noreorient\n", 467 | "nocom\n", 468 | "symmetry c1\n", 469 | "\"\"\")\n", 470 | "\n", 471 | "comA = waterA.center_of_mass()\n", 472 | "comA = np.array([comA[0],comA[1],comA[2]])\n", 473 | "E, wfn = psi4.energy('HF',return_wfn=True)\n", 474 | "dipoleA = np.array([psi4.variable('SCF DIPOLE X'),psi4.variable('SCF DIPOLE Y'),\n", 475 | " psi4.variable('SCF DIPOLE Z')])*0.393456 # conversion from Debye to a.u.\n", 476 | "psi4.core.clean()\n", 477 | "print(\"COM A in a.u.\",comA)\n", 478 | "print(\"Dipole A in a.u.\",dipoleA)\n", 479 | "\n", 480 | "waterB = psi4.geometry(\"\"\"\n", 481 | "O -0.118596320329 -1.305864713301 0.000000000000\n", 482 | "H 0.362842754701 -1.642971982825 -0.759061990794\n", 483 | "H 0.362842754701 -1.642971982825 0.759061990794\n", 484 | "units angstrom\n", 485 | "noreorient\n", 486 | "nocom\n", 487 | "symmetry c1\n", 488 | "\"\"\")\n", 489 | "\n", 490 | "comB = waterB.center_of_mass()\n", 491 | "comB = np.array([comB[0],comB[1],comB[2]])\n", 492 | "E, wfn = psi4.energy('HF',return_wfn=True)\n", 493 | "dipoleB = np.array([psi4.variable('SCF DIPOLE X'),psi4.variable('SCF DIPOLE Y'),\n", 494 | " psi4.variable('SCF DIPOLE Z')])*0.393456 # conversion from Debye to a.u.\n", 495 | "psi4.core.clean()\n", 496 | "print(\"COM B in a.u.\",comB)\n", 497 | "print(\"Dipole B in a.u.\",dipoleB)\n", 498 | "\n", 499 | "comA_to_comB = comB - comA\n", 500 | "print(\"Vector from COMA to COMB:\",comA_to_comB)\n", 501 | "\n" 502 | ] 503 | }, 504 | { 505 | "cell_type": "markdown", 506 | "metadata": {}, 507 | "source": [ 508 | "Our goal now is to plot the electrostatic energy from SAPT against the interaction energy between two dipoles $\\boldsymbol{\\mu_A}$ and $\\boldsymbol{\\mu_B}$:\n", 509 | "\n", 510 | "\\begin{equation}\n", 511 | "E_{\\rm dipole-dipole}=\\frac{\\boldsymbol{\\mu_A}\\cdot\\boldsymbol{\\mu_B}}{R^3}-\\frac{3(\\boldsymbol{\\mu_A}\\cdot{\\mathbf R})(\\boldsymbol{\\mu_B}\\cdot{\\mathbf R})}{R^5} \n", 512 | "\\end{equation}\n", 513 | "\n", 514 | "Program this formula in the `dipole_dipole` function below, taking ${\\mathbf R}$, $\\boldsymbol{\\mu_A}$, and $\\boldsymbol{\\mu_B}$ in atomic units and calculating the dipole-dipole interaction energy, also in atomic units (which we will later convert to kcal/mol). \n", 515 | "With your new function, we can populate the `edipdip` array of dipole-dipole interaction energies for all intermolecular separations, and plot these energies alongside the actual electrostatic energy data from SAPT. \n", 516 | "\n", 517 | "Note that ${\\mathbf R}$ is the vector from the center of mass of molecule A to the center of mass of molecule B. For the shortest intermolecular distance, the atomic coordinates are listed in the code above, so `R = comA_to_comB`. For any other distance, we obtained the geometry of the complex by shifting one water molecule away from the other along the O-O direction, so we need to shift the center of mass of the second molecule in the same way.\n" 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "execution_count": null, 523 | "metadata": {}, 524 | "outputs": [], 525 | "source": [ 526 | "#the geometries are related to each other by a shift of 1 molecule along the O-O vector:\n", 527 | "OA_to_OB = (np.array([-0.118596320329,-1.305864713301,0.000000000000])-np.array(\n", 528 | " [0.116724185090,1.383860971547,0.000000000000]))/0.529177249\n", 529 | "OA_to_OB_unit = OA_to_OB/np.sqrt(np.sum(OA_to_OB*OA_to_OB))\n", 530 | "print(\"Vector from OA to OB:\",OA_to_OB,OA_to_OB_unit)\n", 531 | "\n", 532 | "def dipole_dipole(R,dipA,dipB):\n", 533 | "#COMPLETE the definition of the dipole-dipole energy. All your data are in atomic units.\n", 534 | "\n", 535 | "edipdip = []\n", 536 | "for i in range(len(distances_h2o)):\n", 537 | " shiftlength = (distances_h2o[i]-distances_h2o[0])/0.529177249\n", 538 | " R = comA_to_comB + shiftlength*OA_to_OB_unit\n", 539 | " edipdip.append(dipole_dipole(R,dipoleA,dipoleB)*627.509)\n", 540 | "\n", 541 | "edipdip = np.array(edipdip)\n", 542 | "print (edipdip)\n", 543 | "\n", 544 | "plt.close()\n", 545 | "plt.ylim(-10.0,10.0)\n", 546 | "plt.plot(distances_h2o,eelst_h2o,'r+',linestyle='-',label='SAPT0 elst')\n", 547 | "plt.plot(distances_h2o,edipdip,'bo',linestyle='-',label='dipole-dipole')\n", 548 | "plt.hlines(0.0,2.5,9.0)\n", 549 | "plt.legend(loc='upper right')\n", 550 | "plt.show()\n" 551 | ] 552 | }, 553 | { 554 | "cell_type": "markdown", 555 | "metadata": {}, 556 | "source": [ 557 | "We clearly have a favorable dipole-dipole interaction, which results in negative (attractive) electrostatic energy. This is how the origins of hydrogen bonding might have been explained to you in your freshman chemistry class: two polar molecules have nonzero dipole moments and the dipole-dipole interaction can be strongly attractive. However, your SAPT components show you that it's not a complete explanation: the two water molecules are bound not only by electrostatics, but by two other SAPT components as well. Can you quantify the relative (percentage) contributions of electrostatics, induction, and dispersion to the overall interaction energy at the van der Waals minimum? This minimum is the second point on your curve, so, for example, `esapt_h2o[1]` is the total SAPT interaction energy.\n" 558 | ] 559 | }, 560 | { 561 | "cell_type": "code", 562 | "execution_count": null, 563 | "metadata": {}, 564 | "outputs": [], 565 | "source": [ 566 | "#now let's examine the SAPT0 contributions at the van der Waals minimum, which is the 2nd point on the curve\n", 567 | "#COMPLETE the calculation of percentages.\n", 568 | "percent_elst = \n", 569 | "percent_ind = \n", 570 | "percent_disp = \n", 571 | "print ('At the van der Waals minimum, electrostatics, induction, and dispersion')\n", 572 | "print (' contribute %5.1f, %5.1f, and %5.1f percent of interaction energy, respectively.'\n", 573 | " % (percent_elst,percent_ind,percent_disp))\n", 574 | "\n" 575 | ] 576 | }, 577 | { 578 | "cell_type": "markdown", 579 | "metadata": {}, 580 | "source": [ 581 | "You have now completed some SAPT calculations and analyzed the meaning of different corrections. Can you complete the table below to indicate whether different SAPT corrections can be positive (repulsive), negative (attractive), or both, and why?\n" 582 | ] 583 | }, 584 | { 585 | "cell_type": "code", 586 | "execution_count": null, 587 | "metadata": {}, 588 | "outputs": [], 589 | "source": [ 590 | "#Type in your answers below.\n", 591 | "#COMPLETE this table. Do not remove the comment (#) signs.\n", 592 | "#\n", 593 | "#SAPT term Positive/Negative/Both? Why?\n", 594 | "#Electrostatics\n", 595 | "#Exchange\n", 596 | "#Induction\n", 597 | "#Dispersion\n" 598 | ] 599 | }, 600 | { 601 | "cell_type": "markdown", 602 | "metadata": {}, 603 | "source": [ 604 | "# Ternary diagrams\n", 605 | "\n", 606 | "Higher levels of SAPT calculations can give very accurate interaction energies, but are more computationally expensive than SAPT0. SAPT0 is normally sufficient for qualitative accuracy and basic understanding of the interaction physics. One important use of SAPT0 is to *classify different intermolecular complexes according to the type of interaction*, and a nice way to display the results of this classification is provided by a *ternary diagram*.\n", 607 | "\n", 608 | "The relative importance of attractive electrostatic, induction, and dispersion contributions to a SAPT interaction energy for a particular structure can be marked as a point inside a triangle, with the distance to each vertex of the triangle depicting the relative contribution of a given type (the more dominant a given contribution is, the closer the point lies to the corresponding vertex). If the electrostatic contribution is repulsive, we can display the relative magnitudes of electrostatic, induction, and dispersion terms in the same way, but we need the second triangle (the left one). The combination of two triangles forms the complete diagram and we can mark lots of different points corresponding to different complexes and geometries.\n", 609 | "\n", 610 | "Let's now mark all your systems on a ternary diagram, in blue for two helium atoms and in red for two water molecules. What kinds of interaction are represented? Compare your diagram with the one pictured below, prepared for 2510 different geometries of the complex of two water molecules, with all kinds of intermolecular distances and orientations (this graph is taken from [Smith:2016]). What conclusions can you draw about the interaction of two water molecules at *any* orientation?\n" 611 | ] 612 | }, 613 | { 614 | "cell_type": "code", 615 | "execution_count": null, 616 | "metadata": { 617 | "scrolled": true 618 | }, 619 | "outputs": [], 620 | "source": [ 621 | "def ternary(sapt, title='', labeled=True, view=True, saveas=None, relpath=False, graphicsformat=['pdf']):\n", 622 | "#Adapted from the QCDB ternary diagram code by Lori Burns\n", 623 | " \"\"\"Takes array of arrays *sapt* in form [elst, indc, disp] and builds formatted\n", 624 | " two-triangle ternary diagrams. Either fully-readable or dotsonly depending\n", 625 | " on *labeled*.\n", 626 | " \"\"\"\n", 627 | " from matplotlib.path import Path\n", 628 | " import matplotlib.patches as patches\n", 629 | "\n", 630 | " # initialize plot\n", 631 | " plt.close()\n", 632 | " fig, ax = plt.subplots(figsize=(6, 3.6))\n", 633 | " plt.xlim([-0.75, 1.25])\n", 634 | " plt.ylim([-0.18, 1.02])\n", 635 | " plt.xticks([])\n", 636 | " plt.yticks([])\n", 637 | " ax.set_aspect('equal')\n", 638 | "\n", 639 | " if labeled:\n", 640 | " # form and color ternary triangles\n", 641 | " codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]\n", 642 | " pathPos = Path([(0., 0.), (1., 0.), (0.5, 0.866), (0., 0.)], codes)\n", 643 | " pathNeg = Path([(0., 0.), (-0.5, 0.866), (0.5, 0.866), (0., 0.)], codes)\n", 644 | " ax.add_patch(patches.PathPatch(pathPos, facecolor='white', lw=2))\n", 645 | " ax.add_patch(patches.PathPatch(pathNeg, facecolor='#fff5ee', lw=2))\n", 646 | "\n", 647 | " # label corners\n", 648 | " ax.text(1.0,\n", 649 | " -0.15,\n", 650 | " u'Elst (−)',\n", 651 | " verticalalignment='bottom',\n", 652 | " horizontalalignment='center',\n", 653 | " family='Times New Roman',\n", 654 | " weight='bold',\n", 655 | " fontsize=18)\n", 656 | " ax.text(0.5,\n", 657 | " 0.9,\n", 658 | " u'Ind (−)',\n", 659 | " verticalalignment='bottom',\n", 660 | " horizontalalignment='center',\n", 661 | " family='Times New Roman',\n", 662 | " weight='bold',\n", 663 | " fontsize=18)\n", 664 | " ax.text(0.0,\n", 665 | " -0.15,\n", 666 | " u'Disp (−)',\n", 667 | " verticalalignment='bottom',\n", 668 | " horizontalalignment='center',\n", 669 | " family='Times New Roman',\n", 670 | " weight='bold',\n", 671 | " fontsize=18)\n", 672 | " ax.text(-0.5,\n", 673 | " 0.9,\n", 674 | " u'Elst (+)',\n", 675 | " verticalalignment='bottom',\n", 676 | " horizontalalignment='center',\n", 677 | " family='Times New Roman',\n", 678 | " weight='bold',\n", 679 | " fontsize=18)\n", 680 | "\n", 681 | " xvals = []\n", 682 | " yvals = []\n", 683 | " cvals = []\n", 684 | " geomindex = 0 # first 11 points are He-He, the next 10 are H2O-H2O\n", 685 | " for sys in sapt:\n", 686 | " [elst, indc, disp] = sys\n", 687 | "\n", 688 | " # calc ternary posn and color\n", 689 | " Ftop = abs(indc) / (abs(elst) + abs(indc) + abs(disp))\n", 690 | " Fright = abs(elst) / (abs(elst) + abs(indc) + abs(disp))\n", 691 | " xdot = 0.5 * Ftop + Fright\n", 692 | " ydot = 0.866 * Ftop\n", 693 | " if geomindex <= 10:\n", 694 | " cdot = 'b'\n", 695 | " else:\n", 696 | " cdot = 'r'\n", 697 | " if elst > 0.:\n", 698 | " xdot = 0.5 * (Ftop - Fright)\n", 699 | " ydot = 0.866 * (Ftop + Fright)\n", 700 | " #print elst, indc, disp, '', xdot, ydot, cdot\n", 701 | "\n", 702 | " xvals.append(xdot)\n", 703 | " yvals.append(ydot)\n", 704 | " cvals.append(cdot)\n", 705 | " geomindex += 1\n", 706 | "\n", 707 | " sc = ax.scatter(xvals, yvals, c=cvals, s=15, marker=\"o\", \n", 708 | " edgecolor='none', vmin=0, vmax=1, zorder=10)\n", 709 | "\n", 710 | " # remove figure outline\n", 711 | " ax.spines['top'].set_visible(False)\n", 712 | " ax.spines['right'].set_visible(False)\n", 713 | " ax.spines['bottom'].set_visible(False)\n", 714 | " ax.spines['left'].set_visible(False)\n", 715 | "\n", 716 | " # save and show\n", 717 | " plt.show()\n", 718 | " return 1\n", 719 | "\n", 720 | "sapt = []\n", 721 | "for i in range(11):\n", 722 | " sapt.append([eelst[i],eind[i],edisp[i]])\n", 723 | "for i in range(10):\n", 724 | " sapt.append([eelst_h2o[i],eind_h2o[i],edisp_h2o[i]])\n", 725 | "idummy = ternary(sapt)\n", 726 | "from IPython.display import Image\n", 727 | "Image(filename='water2510.png')\n" 728 | ] 729 | }, 730 | { 731 | "cell_type": "markdown", 732 | "metadata": {}, 733 | "source": [ 734 | "# Some further reading:\n", 735 | "\n", 736 | "1. How is the calculation of SAPT corrections actually programmed? The Psi4NumPy projects has some tutorials on this topic: https://github.com/psi4/psi4numpy/tree/master/Tutorials/07_Symmetry_Adapted_Perturbation_Theory \n", 737 | "2. A classic (but recently updated) book on the theory of interactions between molecules: \"The Theory of Intermolecular Forces\"\n", 738 | "\t> [[Stone:2013](https://www.worldcat.org/title/theory-of-intermolecular-forces/oclc/915959704)] A. Stone, Oxford University Press, 2013\n", 739 | "3. The classic review paper on SAPT: \"Perturbation Theory Approach to Intermolecular Potential Energy Surfaces of van der Waals Complexes\"\n", 740 | "\t> [[Jeziorski:1994](http://pubs.acs.org/doi/abs/10.1021/cr00031a008)] B. Jeziorski, R. Moszynski, and K. Szalewicz, *Chem. Rev.* **94**, 1887 (1994)\n", 741 | "4. A brand new (as of 2020) review of SAPT, describing new developments and inprovements to the theory: \"Recent developments in symmetry‐adapted perturbation theory\"\n", 742 | "\t> [[Patkowski:2020](https://onlinelibrary.wiley.com/doi/abs/10.1002/wcms.1452)] K. Patkowski, *WIREs Comput. Mol. Sci.* **10**, e1452 (2020)\n", 743 | "5. The definitions and practical comparison of different levels of SAPT: \"Levels of symmetry adapted perturbation theory (SAPT). I. Efficiency and performance for interaction energies\"\n", 744 | "\t> [[Parker:2014](http://aip.scitation.org/doi/10.1063/1.4867135)] T. M. Parker, L. A. Burns, R. M. Parrish, A. G. Ryno, and C. D. Sherrill, *J. Chem. Phys.* **140**, 094106 (2014)\n", 745 | "6. An example study making use of the SAPT0 classification of interaction types, with lots of ternary diagrams in the paper and in the supporting information: \"Revised Damping Parameters for the D3 Dispersion Correction to Density Functional Theory\"\n", 746 | "\t> [[Smith:2016](https://pubs.acs.org/doi/abs/10.1021/acs.jpclett.6b00780)] D. G. A. Smith, L. A. Burns, K. Patkowski, and C. D. Sherrill, *J. Phys. Chem. Lett.* **7**, 2197 (2016).\n" 747 | ] 748 | } 749 | ], 750 | "metadata": { 751 | "anaconda-cloud": {}, 752 | "kernelspec": { 753 | "display_name": "Python 3", 754 | "language": "python", 755 | "name": "python3" 756 | }, 757 | "language_info": { 758 | "codemirror_mode": { 759 | "name": "ipython", 760 | "version": 3 761 | }, 762 | "file_extension": ".py", 763 | "mimetype": "text/x-python", 764 | "name": "python", 765 | "nbconvert_exporter": "python", 766 | "pygments_lexer": "ipython3", 767 | "version": "3.7.3" 768 | }, 769 | "latex_envs": { 770 | "LaTeX_envs_menu_present": true, 771 | "bibliofile": "biblio.bib", 772 | "cite_by": "apalike", 773 | "current_citInitial": 1, 774 | "eqLabelWithNumbers": true, 775 | "eqNumInitial": 1, 776 | "labels_anchors": false, 777 | "latex_user_defs": false, 778 | "report_style_numbering": false, 779 | "user_envs_cfg": false 780 | } 781 | }, 782 | "nbformat": 4, 783 | "nbformat_minor": 1 784 | } 785 | -------------------------------------------------------------------------------- /labs/Symmetry_Adapted_Perturbation_Theory/water2510.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/Symmetry_Adapted_Perturbation_Theory/water2510.png -------------------------------------------------------------------------------- /labs/spectroscopic_constants/spectroscopic_constants_student.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "\"\"\"Calculating Spectroscopic Constants from a Potential Energy Surface\"\"\"\n", 10 | "\n", 11 | "__authors__ = \"Ashley Ringer McDonald\"\n", 12 | "__credits__ = [\"Dominic A. Sirianni\"]\n", 13 | "__email__ = [\"armcdona@calpoly.edu\"]\n", 14 | "\n", 15 | "__copyright__ = \"(c) 2008-2019, The Psi4Education Developers\"\n", 16 | "__license__ = \"BSD-3-Clause\"\n", 17 | "__date__ = \"2019-11-18\"" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Calculating Spectroscopic Constants from a Potential Energy Surface \n", 25 | "The spectroscopic constants for a diatomic molecule can be calculated from a series of potential energies computed for different bond separations. In this lab, you will calculate the spectroscopic constants for two molecules and compare their force constants. You will also study the effect of using the harmonic approximation in determining the vibrational energy levels. " 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "To use this lab: As you work through the lab, be sure to execute each code block. If you don't, then when you try to write you own code, it will tell you the previous variable are undefined because the prior code blocks were not executed." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "First we import the electronic structure functions into our notebook. " 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "import psi4\n", 49 | "import numpy as np\n", 50 | "import matplotlib.pyplot as plt" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "Next contruct the molecule using the PSI4 input structure and define the values of the bond length, R. Here we build an HF molecule." 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "mol_tmpl = \"\"\"H\n", 67 | "F 1 **R**\"\"\"\n", 68 | "rvals = [0.8, 0.85, 0.9, 0.95, 1.0]" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "Each of the following `psi4.` functions calls uses the Psi4 program. The `psi4.geometry` function creates the molecule and the `psi4.energy` function calculates the SCF/cc-pVDZ energy." 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "molecules =[]\n", 85 | "energies = []\n", 86 | "for r in rvals:\n", 87 | " molecule = psi4.geometry(mol_tmpl.replace(\"**R**\", str(r)))\n", 88 | " molecules.append(molecule)\n", 89 | "for mol in molecules:\n", 90 | " energy = psi4.energy(\"SCF/cc-pVDZ\", molecule=mol)\n", 91 | " energies.append(energy)\n", 92 | "print(rvals)\n", 93 | "print(energies)" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "metadata": {}, 99 | "source": [ 100 | "This makes a nicer chart" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "for num, r in enumerate(rvals):\n", 110 | " print(r, ':', energies[num])" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "*What is the general trend in the energy for these bond distances? Offer an explaination as to why this seems reasonable.*" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "Student answer box: " 125 | ] 126 | }, 127 | { 128 | "cell_type": "markdown", 129 | "metadata": {}, 130 | "source": [ 131 | "Activity: *Plot the energies vs. R*" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": null, 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "#Write you code here\n" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "The next function uses the bonds distances and the energies to derive the spectroscopic constants for the molecule. These are output to a python dictionary which is called data in this example. All of the values are given in cm$^{-1}$." 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "data = psi4.diatomic.anharmonicity(rvals, energies)" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": null, 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "print(data)" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "To access an element of data in a later computation, you use the syntax: " 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "data['nu']" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "Activity: *Calculate the energies of the n vibrational levels. Recall that the vibrational energy levels are given by $$ E_n = \\omega_e \\left( n+ \\frac{1}{2} \\right) - \\omega_e x_e \\left( n + \\frac{1}{2} \\right)^2 $$ where the second term is the anharmonic correction. Assuming J=0, calculate the energy for the n=0 to n=3 energy levels, with and without the anharmonic correction. Remember, you can access the $ \\omega_e $ and $ \\omega_e x_e $ values from the `data` dictionary. Have your code print your values.*" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "#Write your code here\n" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "metadata": {}, 203 | "source": [ 204 | "Activity: *Calculate the difference between the harmonic and the anharmonic energies and print these values.*" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "# Write your code here\n" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "metadata": {}, 219 | "source": [ 220 | "*Is it more important to include anharmonicity corrections for ground state energy calculations or excited state calculations? Explain your answer.*" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "metadata": {}, 226 | "source": [ 227 | "Write your answer here: " 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "metadata": {}, 233 | "source": [ 234 | "Activity: *Calculate the $ \\Delta E $ for n=0 to n=1 and n=1 to n=2. Compare the results for the harmonic approximation and the anharmonic results.*" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "# Write you code here\n" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "*Is the spacing of the energy levels equal? Explain your answer.* " 251 | ] 252 | }, 253 | { 254 | "cell_type": "markdown", 255 | "metadata": {}, 256 | "source": [ 257 | "Write your answer here: " 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": { 263 | "collapsed": true 264 | }, 265 | "source": [ 266 | "Activity: *Calculate the force constant for the HF bond in N/m. $$ \\omega_e = \\frac{1}{2\\pi c} \\sqrt \\frac{k}{\\mu} $$ Remember that $ \\mu $ is the reduced mass of HF.*" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": null, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "# Write your code here\n" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "Activity: *Construct and plot the PES for CO. Determine $\\omega_e$, $\\omega_e x_e$, $\\mu$, and k for CO. Add additional cells to your jupyter notebook as needed.* " 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "# Write your code here:\n" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": {}, 297 | "source": [ 298 | "Question: *Based on your results, is anharmonicity a more significant factor for HF or CO?*" 299 | ] 300 | }, 301 | { 302 | "cell_type": "markdown", 303 | "metadata": {}, 304 | "source": [ 305 | "Write your answer here:" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": {}, 311 | "source": [ 312 | "Question: *Is the force constant greater for HF or CO? Do your results agree with the typical bond orders expected for HF and CO?*" 313 | ] 314 | }, 315 | { 316 | "cell_type": "markdown", 317 | "metadata": {}, 318 | "source": [ 319 | "Write your answer here:" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": null, 325 | "metadata": {}, 326 | "outputs": [], 327 | "source": [] 328 | } 329 | ], 330 | "metadata": { 331 | "anaconda-cloud": {}, 332 | "kernelspec": { 333 | "display_name": "Python 3", 334 | "language": "python", 335 | "name": "python3" 336 | }, 337 | "language_info": { 338 | "codemirror_mode": { 339 | "name": "ipython", 340 | "version": 3 341 | }, 342 | "file_extension": ".py", 343 | "mimetype": "text/x-python", 344 | "name": "python", 345 | "nbconvert_exporter": "python", 346 | "pygments_lexer": "ipython3", 347 | "version": "3.7.6" 348 | } 349 | }, 350 | "nbformat": 4, 351 | "nbformat_minor": 1 352 | } 353 | -------------------------------------------------------------------------------- /labs/water_MO/waterMO_student.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/labs/water_MO/waterMO_student.pdf -------------------------------------------------------------------------------- /media/psi-3po.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/media/psi-3po.jpg -------------------------------------------------------------------------------- /media/psi4edubanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Psi4Education/psi4education/c7f92ccd4c08b49682e95ab6d5b115171f07b4a9/media/psi4edubanner.png --------------------------------------------------------------------------------