├── .github └── workflows │ ├── doc.yaml │ ├── lint.yaml │ └── tests.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc ├── _static │ └── custom.css ├── algorithm.rst ├── api.rst ├── conf.py ├── getting-started.rst └── index.rst ├── pyproject.toml ├── setup.cfg ├── src └── berny │ ├── Math.py │ ├── __init__.py │ ├── berny.py │ ├── cli.py │ ├── coords.py │ ├── geomlib.py │ ├── optimize.py │ ├── solvers.py │ ├── species-data.csv │ └── species_data.py └── tests ├── __init__.py ├── aniline.xyz ├── conftest.py ├── cyanogen.xyz ├── ethanol.xyz ├── test_coords.py ├── test_optimize.py └── water.xyz /.github/workflows/doc.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-16.04 7 | steps: 8 | - uses: actions/setup-python@v2 9 | with: 10 | python-version: 3.x 11 | - name: Install Poetry 12 | run: | 13 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 14 | echo $HOME/.poetry/bin >>$GITHUB_PATH 15 | - name: Install dependencies 16 | run: pip install "sphinx<3" sphinxcontrib-katex toml 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: sphinx-build -W -E doc doc/build 20 | - run: touch doc/build/.nojekyll 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | flake8: 5 | runs-on: ubuntu-16.04 6 | steps: 7 | - uses: actions/setup-python@v2 8 | with: 9 | python-version: 3.x 10 | - name: Install dependencies 11 | run: pip install flake8 flake8-bugbear flake8-comprehensions flake8-quotes pep8-naming 12 | - uses: actions/checkout@v2 13 | - run: flake8 14 | black: 15 | runs-on: ubuntu-16.04 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.x 21 | - name: Install dependencies 22 | run: pip install black 23 | - uses: actions/checkout@v2 24 | - run: black . --check 25 | isort: 26 | runs-on: ubuntu-16.04 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-python@v2 30 | with: 31 | python-version: 3.x 32 | - name: Install dependencies 33 | run: pip install isort 34 | - uses: actions/checkout@v2 35 | - run: isort . --check 36 | pydocstyle: 37 | runs-on: ubuntu-16.04 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-python@v2 41 | with: 42 | python-version: 3.x 43 | - name: Install dependencies 44 | run: pip install pydocstyle 45 | - uses: actions/checkout@v2 46 | - run: pydocstyle src 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | all: 5 | name: All 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: [3.6, 3.7, 3.8, 3.9] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-python@v2 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - name: Install Poetry 16 | run: | 17 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 18 | echo $HOME/.poetry/bin >>$GITHUB_PATH 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | - name: Install MOPAC 23 | run: | 24 | mkdir mopac 25 | echo mopac >>.git/info/exclude 26 | cd mopac 27 | wget -q $MOPAC_DOWNLOAD_URL $MOPAC_PASSWORD_URL 28 | unzip MOPAC2016_for_Linux_64_bit.zip 29 | cat >mopac <>$GITHUB_PATH 37 | env: 38 | MOPAC_PASSWORD_URL: ${{ secrets.MOPAC_PASSWORD_URL }} 39 | MOPAC_DOWNLOAD_URL: ${{ secrets.MOPAC_DOWNLOAD_URL }} 40 | - name: Create Python virtual environment 41 | run: | 42 | python -m venv venv 43 | echo VIRTUAL_ENV=$PWD/venv >>$GITHUB_ENV 44 | echo $PWD/venv/bin >>$GITHUB_PATH 45 | echo venv >>.git/info/exclude 46 | - name: Install dependencies 47 | run: pip install -U poetry-dynamic-versioning 48 | - uses: actions/cache@v2 49 | with: 50 | path: | 51 | ${{ env.VIRTUAL_ENV }}/bin 52 | ${{ env.VIRTUAL_ENV }}/lib/python${{ matrix.python-version }}/site-packages 53 | key: ${{ runner.os }}-${{ matrix.python-version }} 54 | - name: Build 55 | run: poetry build 56 | - name: Install 57 | run: pip install -U pyberny[test] --pre -f ./dist 58 | - name: Test 59 | run: coverage run -m pytest -v 60 | - name: Upload to Codecov 61 | run: bash <(curl -s https://codecov.io/bash) 62 | - name: Uninstall 63 | run: pip uninstall -y pyberny 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | /.coverage 4 | /.pytest_cache/ 5 | /.cache/ 6 | /dist/ 7 | /doc/build/ 8 | /htmlcov/ 9 | /poetry.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.6.3] - 2021-02-22 11 | 12 | ### Fixed 13 | 14 | - CLI 15 | 16 | [unreleased]: https://github.com/deepqmc/deepqmc/compare/0.6.3...HEAD 17 | [0.6.3]: https://github.com/deepqmc/deepqmc/releases/tag/0.6.3 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyBerny 2 | 3 | ![checks](https://img.shields.io/github/checks-status/jhrmnn/pyberny/master.svg) 4 | [![coverage](https://img.shields.io/codecov/c/github/jhrmnn/pyberny.svg)](https://codecov.io/gh/jhrmnn/pyberny) 5 | ![python](https://img.shields.io/pypi/pyversions/pyberny.svg) 6 | [![pypi](https://img.shields.io/pypi/v/pyberny.svg)](https://pypi.org/project/pyberny/) 7 | [![commits since](https://img.shields.io/github/commits-since/jhrmnn/pyberny/latest.svg)](https://github.com/jhrmnn/pyberny/releases) 8 | [![last commit](https://img.shields.io/github/last-commit/jhrmnn/pyberny.svg)](https://github.com/jhrmnn/pyberny/commits/master) 9 | [![license](https://img.shields.io/github/license/jhrmnn/pyberny.svg)](https://github.com/jhrmnn/pyberny/blob/master/LICENSE) 10 | [![code style](https://img.shields.io/badge/code%20style-black-202020.svg)](https://github.com/ambv/black) 11 | [![doi](https://img.shields.io/badge/doi-10.5281%2Fzenodo.3695037-blue)](http://doi.org/10.5281/zenodo.3695037) 12 | 13 | PyBerny is an optimizer of molecular geometries with respect to the total energy, using nuclear gradient information. 14 | 15 | In each step, it takes energy and Cartesian gradients as an input, and returns a new equilibrium structure estimate. 16 | 17 | The package implements a single optimization algorithm, which is an amalgam of several techniques, comprising the quasi-Newton method, redundant internal coordinates, an iterative Hessian approximation, a trust region scheme, and linear search. The algorithm is described in more detailed in the [documentation](https://jhrmnn.github.io/pyberny/algorithm.html). 18 | 19 | Several desirable features are missing at the moment but planned, some of them being actively worked on (help is always welcome): [crystal geometries](https://github.com/jhrmnn/pyberny/issues/5), [coordinate constraints](https://github.com/jhrmnn/pyberny/issues/14), [coordinate weighting](https://github.com/jhrmnn/pyberny/issues/32), [transition state search](https://github.com/jhrmnn/pyberny/issues/4). 20 | 21 | PyBerny is available in [PySCF](https://sunqm.github.io/pyscf/geomopt.html#pyberny), [ASE](https://wiki.fysik.dtu.dk/ase/dev/ase/optimize.html?highlight=berny#pyberny), and [QCEngine](http://docs.qcarchive.molssi.org/projects/QCEngine/en/latest/index.html?highlight=pyberny#backends). 22 | 23 | ## Installing 24 | 25 | Install and update using [Pip](https://pip.pypa.io/en/stable/quickstart/): 26 | 27 | ``` 28 | pip install -U pyberny 29 | ``` 30 | 31 | ## Example 32 | 33 | ```python 34 | from berny import Berny, geomlib 35 | 36 | optimizer = Berny(geomlib.readfile('geom.xyz')) 37 | for geom in optimizer: 38 | # get energy and gradients for geom 39 | optimizer.send((energy, gradients)) 40 | ``` 41 | 42 | ## Links 43 | 44 | - Documentation: https://jhrmnn.github.io/pyberny 45 | -------------------------------------------------------------------------------- /doc/_static/custom.css: -------------------------------------------------------------------------------- 1 | .katex { font-size: 1em; } 2 | -------------------------------------------------------------------------------- /doc/algorithm.rst: -------------------------------------------------------------------------------- 1 | Algorithm 2 | ========= 3 | 4 | The optimization algorithm implemented in PyBerny loosely follows the 5 | "standard method" (SM) described in the appendix of [BirkholzTCA16]_. What 6 | follows is a summary of that method, more detailed specification when 7 | necessary, and description of any deviations. 8 | 9 | .. todo:: Make the algorithm `fully conform `_ to the SM. 10 | 11 | Sketch of the algorithm 12 | ----------------------- 13 | 14 | 1. Form `redundant internal coordinates`_. 15 | 2. Form a diagonal Hessian guess [SwartIJQC06]_. 16 | 3. Obtain energy and Cartesian gradients for the current geometry. 17 | 4. Form the Wilson **B** matrix and its `generalized inverse`_. 18 | 5. Update the Hessian using the BFGS scheme. 19 | 6. Update trust region (Eq. 5.1.6 of [Fletcher00]_). 20 | 7. Perform linear search (Gaussian `website `__, 21 | section Examples, "If a minimum is sought..."). (Not in the SM.) 22 | 8. Project to a nonredundant subspace [PengJCC96]_. 23 | 9. Perform a quadratic RFO step [BanerjeeJPC85]_. 24 | 10. Transform back to Cartesian coordinates [PengJCC96]_. 25 | 11. If convergence is not reached (criteria from the SM), go to 3. 26 | 27 | Redundant internal coordinates 28 | ------------------------------ 29 | 30 | 1. All bonds shorter than 1.3 times the sum of covalent radii are created 31 | [PengJCC96]_. 32 | 2. If there are unconnected fragments, all bonds between unconnected fragments 33 | shorter than the sum of van der Waals radii plus *d* are created, with *d* 34 | starting at 0 and increasing by 1 angstrom, until all fragments are 35 | connected (custom scheme by JH). 36 | 3. All angles greater than 45° are created. 37 | 4. All dihedrals with 1–2–3, 2–3–4 angles both greater than 45° are created. If 38 | one of the angles is zero, so that three atoms lie on a line, they are used 39 | as a new base for a dihedral. This process is recursively repeated 40 | [PengJCC96]_. 41 | 5. In the case of a crystal, just the internal coordinate closest to the 42 | original unit cell is retained from all its periodic images. 43 | 44 | .. todo:: Implement `linear bends `_. 45 | 46 | Generalized inverse 47 | ------------------- 48 | 49 | The Wilson **B** matrix, which relates differences in the internal redundant 50 | coordinates to differences in the Cartesian coordinates, is in general 51 | non-square and non-invertible. Its generalized inverse is obtained from the 52 | pseudoinverse of :math:`\mathbf B\mathbf B^\mathrm T` (singular), which is in 53 | turn obtained via singular value decomposition and inversion of only the 54 | nonzero singular values. For invertible matrices, this procedure is equivalent 55 | to an ordinary inverse. In practice, the zero values are in fact nonzero but 56 | several orders of magnitude smaller than the true nonzero values. 57 | 58 | References 59 | ---------- 60 | 61 | .. [BirkholzTCA16] Birkholz, A. B. & Schlegel, H. B. Exploration of some 62 | refinements to geometry optimization methods. Theor. Chem. Acc. 135, (2016). 63 | DOI: `10.1007/s00214-016-1847-3 64 | `_ 65 | .. [PengJCC96] Peng, C., Ayala, P. Y., Schlegel, H. B. & Frisch, M. J. Using 66 | redundant internal coordinates to optimize equilibrium geometries and 67 | transition states. J. Comput. Chem. 17, 49–56 (1996). DOI: 68 | `10.1002/(SICI)1096-987X(19960115)17:1\<49::AID-JCC5\>3.0.CO;2-0 69 | 3.0.CO;2-0>`_ 70 | .. [SwartIJQC06] Swart, M. & Bickelhaupt, F. M. Optimization of Strong and Weak 71 | Coordinates. Int. J. Quantum Chem. 106, 2536–2544 (2006). DOI: 72 | `10.1002/qua.21049 `_ 73 | .. [Fletcher00] Fletcher, R. Practical Methods of Optimization. (Wiley, 2000). 74 | URL: 75 | https://www.wiley.com/en-us/Practical+Methods+of+Optimization%2C+2nd+Edition-p-9780471494638 76 | .. [BanerjeeJPC85] Banerjee, A., Adams, N. & Simons, J. Search for Stationary 77 | Points on Surfaces. J. Phys. Chem. 57, 52–57 (1985). DOI: 78 | `10.1021/j100247a015 `_ 79 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | This covers all supported public API. 5 | 6 | .. module:: berny 7 | 8 | .. autoclass:: Berny 9 | :members: 10 | :exclude-members: Point, send, throw 11 | 12 | .. autodata:: berny.berny.defaults 13 | 14 | .. autodata:: berny.coords.angstrom 15 | 16 | Can be imported directly as :data:`berny.angstrom`. 17 | 18 | .. autofunction:: optimize 19 | 20 | Geometry operations 21 | ------------------- 22 | 23 | .. autoclass:: Geometry 24 | :members: 25 | 26 | .. module:: berny.geomlib 27 | 28 | .. autofunction:: load 29 | 30 | .. autofunction:: loads 31 | 32 | .. autofunction:: readfile 33 | 34 | Solvers 35 | ------- 36 | 37 | All functions in this module are coroutines that satisfy the solver interface 38 | expected by :func:`~berny.optimize`. 39 | 40 | .. module:: berny.solvers 41 | 42 | .. autofunction:: MopacSolver 43 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | import toml 7 | 8 | sys.path.insert(0, os.path.abspath('../src')) 9 | with open('../pyproject.toml') as f: 10 | metadata = toml.load(f)['tool']['poetry'] 11 | 12 | project = 'PyBerny' 13 | author = ' '.join(metadata['authors'][0].split()[:-1]) 14 | release = version = ( 15 | subprocess.run(['poetry', 'version'], capture_output=True, cwd='..') 16 | .stdout.decode() 17 | .split()[1] 18 | ) 19 | description = metadata['description'] 20 | year_range = (2016, datetime.date.today().year) 21 | year_str = ( 22 | str(year_range[0]) 23 | if year_range[0] == year_range[1] 24 | else f'{year_range[0]}-{year_range[1]}' 25 | ) 26 | copyright = f'{year_str}, {author}' 27 | 28 | master_doc = 'index' 29 | extensions = [ 30 | 'sphinx.ext.githubpages', 31 | 'sphinx.ext.autodoc', 32 | 'sphinx.ext.todo', 33 | 'sphinx.ext.viewcode', 34 | 'sphinx.ext.intersphinx', 35 | 'sphinx.ext.mathjax', 36 | 'sphinx.ext.napoleon', 37 | 'sphinxcontrib.katex', 38 | ] 39 | intersphinx_mapping = { 40 | 'python': ('https://docs.python.org/3', None), 41 | } 42 | exclude_patterns = ['build', '.DS_Store'] 43 | 44 | html_theme = 'alabaster' 45 | html_theme_options = { 46 | 'description': description, 47 | 'github_button': True, 48 | 'github_user': 'jhrmnn', 49 | 'github_repo': 'pyberny', 50 | 'badge_branch': 'master', 51 | 'codecov_button': True, 52 | 'fixed_sidebar': True, 53 | 'page_width': '60em', 54 | } 55 | html_sidebars = { 56 | '**': ['about.html', 'navigation.html', 'relations.html', 'searchbox.html'] 57 | } 58 | html_static_path = ['_static'] 59 | 60 | autodoc_default_options = {'special-members': '__call__'} 61 | autodoc_mock_imports = ['numpy'] 62 | todo_include_todos = True 63 | pygments_style = 'sphinx' 64 | napoleon_numpy_docstring = False 65 | napoleon_use_ivar = True 66 | 67 | 68 | def skip_namedtuples(app, what, name, obj, skip, options): 69 | if hasattr(obj, '_source'): 70 | return True 71 | 72 | 73 | def setup(app): 74 | app.connect('autodoc-skip-member', skip_namedtuples) 75 | -------------------------------------------------------------------------------- /doc/getting-started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Dependencies 5 | ------------ 6 | 7 | Python >=3.6 with Numpy. 8 | 9 | Usage 10 | ----- 11 | 12 | The Python API consists of coroutine :class:`~berny.Berny` and function 13 | :func:`~berny.optimize`:: 14 | 15 | from berny import Berny, geomlib 16 | from berny.solvers import MopacSolver 17 | 18 | optimizer = Berny(geomlib.readfile('start.xyz')) 19 | solver = MopacSolver() 20 | next(solver) 21 | for geom in optimizer: 22 | energy, gradients = solver.send((list(geom), geom.lattice)) 23 | optimizer.send((energy, gradients)) 24 | relaxed = geom 25 | 26 | or equivalently:: 27 | 28 | from berny import Berny, geomlib, optimize 29 | from berny.solvers import MopacSolver 30 | 31 | relaxed = optimize(Berny(geomlib.readfile('start.xyz')), MopacSolver()) 32 | 33 | A different option is to use the package via a command-line or socket 34 | interface defined by the ``berny`` command: 35 | 36 | .. code:: none 37 | 38 | usage: berny [-h] [--init] [-f {xyz,aims}] [-s host port] [paramfile] 39 | 40 | positional arguments: 41 | paramfile Optional optimization parameters as JSON 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | --init Initialize Berny optimizer. 46 | -f {xyz,aims}, --format {xyz,aims} 47 | Format of geometry 48 | -s host port, --socket host port 49 | Listen on given address 50 | 51 | A call with ``--init`` corresponds to initializing the :class:`~berny.Berny` 52 | object where the geometry is taken from standard input, assuming format 53 | ``--format``. The object is then pickled to ``berny.pickle`` and the program 54 | quits. Subsequent calls to ``berny`` recover the :class:`~berny.Berny` object, 55 | read energy and gradients from the standard input (first line is energy, 56 | subsequent lines correspond to Cartesian gradients of individual atoms, all in 57 | atomic units) and write the new structure estimate to standard output. An 58 | example usage could look like this: 59 | 60 | .. code:: bash 61 | 62 | #!/bin/bash 63 | berny --init params.json current.xyz 65 | while true; do 66 | # calculate energy and gradients of current.xyz 67 | cat energy_gradients.txt | berny >next.xyz 68 | if [[ $? == 0 ]]; then # minimum reached 69 | break 70 | fi 71 | mv next.xyz current.xyz 72 | done 73 | 74 | Alternatively, one can start an optimizer server with the ``--socket`` 75 | option on a given address and port. This initiates the :class:`~berny.Berny` 76 | object and waits for connections, in which it expects to receive energy and 77 | gradients as a request (in the same format as above) and responds with a new 78 | structure estimate in a given format. Example usage would be 79 | 80 | .. code:: bash 81 | 82 | #!/bin/bash 83 | berny -s localhost 25000 -f xyz current.xyz 85 | while true; do 86 | # calculate energy and gradients of current.xyz 87 | cat energy_gradients.txt | nc localhost 25000 >next.xyz 88 | if [[ ! -s next.xyz ]]; then # minimum reached 89 | break 90 | fi 91 | mv next.xyz current.xyz 92 | done 93 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | PyBerny Documentation 2 | ===================== 3 | 4 | This Python package can optimize molecular and crystal structures 5 | with respect to total energy, using nuclear gradient information. 6 | 7 | In each step, it takes energy and Cartesian gradients as an input, and 8 | returns a new structure estimate. 9 | 10 | The algorithm is an amalgam of several techniques, comprising redundant 11 | internal coordinates, iterative Hessian estimate, trust region, line 12 | search, and coordinate weighing, mostly inspired by the optimizer in the 13 | `Gaussian `_ program. 14 | 15 | .. toctree:: 16 | 17 | getting-started 18 | algorithm 19 | api 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=0.12.3"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "pyberny" 7 | version = "0.0.0" 8 | description = "Molecular/crystal structure optimizer" 9 | readme = "README.md" 10 | authors = ["Jan Hermann "] 11 | repository = "https://github.com/jhrmnn/pyberny" 12 | documentation = "https://jhrmnn.github.io/pyberny" 13 | license = "MPL-2.0" 14 | packages = [ 15 | { include = "berny", from = "src" }, 16 | ] 17 | classifiers = [ 18 | "Environment :: Console", 19 | "Intended Audience :: Science/Research", 20 | "Operating System :: POSIX", 21 | "Topic :: Scientific/Engineering :: Chemistry", 22 | "Topic :: Scientific/Engineering :: Physics", 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.6" 27 | numpy = "^1.15" 28 | pytest = { version = "^3.6", optional = true } 29 | coverage = { version = "^4.5", optional = true } 30 | sphinx = { version = "^2.2", optional = true } 31 | sphinxcontrib-katex = { version = "^0.5.1", optional = true } 32 | toml = { version = "^0.10.0", optional = true } 33 | 34 | [tool.poetry.extras] 35 | test = ["pytest", "coverage"] 36 | doc = ["sphinx", "sphinxcontrib-katex", "toml"] 37 | 38 | [tool.poetry.dev-dependencies] 39 | flake8 = "^3.5" 40 | flake8-bugbear = { version = ">=18.8", python = "^3.6" } 41 | flake8-comprehensions = ">=1.4" 42 | flake8-quotes = "^2" 43 | black = { version = ">=20-beta.0", python = "^3.6" } 44 | pep8-naming = ">=0.7" 45 | isort = { version = "^5", python = "^3.6" } 46 | pydocstyle = ">=5" 47 | 48 | [tool.poetry.scripts] 49 | berny = "berny.cli:main" 50 | 51 | [tool.poetry-dynamic-versioning] 52 | enable = true 53 | dirty = true 54 | pattern = '^(?P\d+\.\d+\.\d+)$' 55 | 56 | [tool.black] 57 | target-version = ["py36"] 58 | skip-string-normalization = true 59 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-complexity = 12 3 | max-line-length = 80 4 | ignore = E501,W503,E741,E203,N803,N806,N802 5 | select = C,E,F,N,W,B,B9,Q0 6 | 7 | [isort] 8 | multi_line_output = 3 9 | include_trailing_comma = 1 10 | line_length = 85 11 | sections = FUTURE,STDLIB,TYPING,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 12 | known_typing = typing, typing_extensions 13 | no_lines_before = TYPING 14 | combine_as_imports = true 15 | 16 | [pydocstyle] 17 | add-ignore = D100,D104,D105,D106,D107,D202 18 | match-dir = src/berny 19 | ignore-decorators = wraps 20 | 21 | [tool:pytest] 22 | filterwarnings = 23 | ignore::PendingDeprecationWarning 24 | 25 | [coverage:run] 26 | branch = true 27 | source = berny 28 | -------------------------------------------------------------------------------- /src/berny/Math.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import numpy as np 5 | 6 | __all__ = ['fit_cubic', 'fit_quartic', 'findroot'] 7 | 8 | 9 | def rms(A): 10 | if A.size == 0: 11 | return None 12 | return np.sqrt(np.sum(A ** 2) / A.size) 13 | 14 | 15 | def pinv(A, log=lambda _: None): 16 | U, D, V = np.linalg.svd(A) 17 | thre = 1e3 18 | thre_log = 1e8 19 | gaps = D[:-1] / D[1:] 20 | try: 21 | n = np.flatnonzero(gaps > thre)[0] 22 | except IndexError: 23 | n = len(gaps) 24 | else: 25 | gap = gaps[n] 26 | if gap < thre_log: 27 | log('Pseudoinverse gap of only: {:.1e}'.format(gap)) 28 | D[n + 1 :] = 0 29 | D[: n + 1] = 1 / D[: n + 1] 30 | return U.dot(np.diag(D)).dot(V) 31 | 32 | 33 | def cross(a, b): 34 | return np.array( 35 | [ 36 | a[1] * b[2] - a[2] * b[1], 37 | a[2] * b[0] - a[0] * b[2], 38 | a[0] * b[1] - a[1] * b[0], 39 | ] 40 | ) 41 | 42 | 43 | def fit_cubic(y0, y1, g0, g1): 44 | """Fit cubic polynomial to function values and derivatives at x = 0, 1. 45 | 46 | Returns position and function value of minimum if fit succeeds. Fit does 47 | not succeeds if 48 | 49 | 1. polynomial doesn't have extrema or 50 | 2. maximum is from (0,1) or 51 | 3. maximum is closer to 0.5 than minimum 52 | """ 53 | a = 2 * (y0 - y1) + g0 + g1 54 | b = -3 * (y0 - y1) - 2 * g0 - g1 55 | p = np.array([a, b, g0, y0]) 56 | r = np.roots(np.polyder(p)) 57 | if not np.isreal(r).all(): 58 | return None, None 59 | r = sorted(x.real for x in r) 60 | if p[0] > 0: 61 | maxim, minim = r 62 | else: 63 | minim, maxim = r 64 | if 0 < maxim < 1 and abs(minim - 0.5) > abs(maxim - 0.5): 65 | return None, None 66 | return minim, np.polyval(p, minim) 67 | 68 | 69 | def fit_quartic(y0, y1, g0, g1): 70 | """Fit constrained quartic polynomial to function values and erivatives at x = 0,1. 71 | 72 | Returns position and function value of minimum or None if fit fails or has 73 | a maximum. Quartic polynomial is constrained such that it's 2nd derivative 74 | is zero at just one point. This ensures that it has just one local 75 | extremum. No such or two such quartic polynomials always exist. From the 76 | two, the one with lower minimum is chosen. 77 | """ 78 | 79 | def g(y0, y1, g0, g1, c): 80 | a = c + 3 * (y0 - y1) + 2 * g0 + g1 81 | b = -2 * c - 4 * (y0 - y1) - 3 * g0 - g1 82 | return np.array([a, b, c, g0, y0]) 83 | 84 | def quart_min(p): 85 | r = np.roots(np.polyder(p)) 86 | is_real = np.isreal(r) 87 | if is_real.sum() == 1: 88 | minim = r[is_real][0].real 89 | else: 90 | minim = r[(r == max(-abs(r))) | (r == -max(-abs(r)))][0].real 91 | return minim, np.polyval(p, minim) 92 | 93 | # discriminant of d^2y/dx^2=0 94 | D = -((g0 + g1) ** 2) - 2 * g0 * g1 + 6 * (y1 - y0) * (g0 + g1) - 6 * (y1 - y0) ** 2 95 | if D < 1e-11: 96 | return None, None 97 | else: 98 | m = -5 * g0 - g1 - 6 * y0 + 6 * y1 99 | p1 = g(y0, y1, g0, g1, 0.5 * (m + np.sqrt(2 * D))) 100 | p2 = g(y0, y1, g0, g1, 0.5 * (m - np.sqrt(2 * D))) 101 | if p1[0] < 0 and p2[0] < 0: 102 | return None, None 103 | [minim1, minval1] = quart_min(p1) 104 | [minim2, minval2] = quart_min(p2) 105 | if minval1 < minval2: 106 | return minim1, minval1 107 | else: 108 | return minim2, minval2 109 | 110 | 111 | class FindrootException(Exception): 112 | pass 113 | 114 | 115 | def findroot(f, lim): 116 | """Find root of increasing function on (-inf,lim). 117 | 118 | Assumes f(-inf) < 0, f(lim) > 0. 119 | """ 120 | d = 1.0 121 | for _ in range(1000): 122 | val = f(lim - d) 123 | if val > 0: 124 | break 125 | d = d / 2 # find d so that f(lim-d) > 0 126 | else: 127 | raise RuntimeError('Cannot find f(x) > 0') 128 | x = lim - d # initial guess 129 | dx = 1e-10 # step for numerical derivative 130 | fx = f(x) 131 | err = abs(fx) 132 | for _ in range(1000): 133 | fxpdx = f(x + dx) 134 | dxf = (fxpdx - fx) / dx 135 | x = x - fx / dxf 136 | fx = f(x) 137 | err_new = abs(fx) 138 | if err_new >= err: 139 | return x 140 | err = err_new 141 | else: 142 | raise FindrootException() 143 | -------------------------------------------------------------------------------- /src/berny/__init__.py: -------------------------------------------------------------------------------- 1 | from . import geomlib 2 | from .berny import Berny 3 | from .coords import angstrom 4 | from .geomlib import Geometry 5 | from .optimize import optimize 6 | 7 | __all__ = ['optimize', 'Berny', 'geomlib', 'Geometry', 'angstrom'] 8 | -------------------------------------------------------------------------------- /src/berny/berny.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import logging 5 | from collections import namedtuple 6 | from collections.abc import Generator 7 | from itertools import chain 8 | 9 | import numpy as np 10 | from numpy import dot, eye 11 | from numpy.linalg import norm 12 | 13 | from . import Math 14 | from .coords import InternalCoords 15 | 16 | __version__ = '0.3.2' 17 | __all__ = ['Berny'] 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | defaults = { 22 | 'gradientmax': 0.45e-3, 23 | 'gradientrms': 0.15e-3, 24 | 'stepmax': 1.8e-3, 25 | 'steprms': 1.2e-3, 26 | 'trust': 0.3, 27 | 'dihedral': True, 28 | 'superweakdih': False, 29 | } 30 | """ 31 | ``gradientmax``, ``gradientrms``, ``stepmax``, ``steprms`` 32 | Convergence criteria in atomic units ("step" refers to the step in 33 | internal coordinates, assuming radian units for angles). 34 | 35 | ``trust`` 36 | Initial trust radius in atomic units. It is the maximum RMS of the 37 | quadratic step (see below). 38 | 39 | ``dihedral`` 40 | Form dihedral angles. 41 | 42 | ``superweakdih`` 43 | Form dihedral angles containing two or more noncovalent bonds. 44 | """ 45 | 46 | 47 | OptPoint = namedtuple('OptPoint', 'q E g') 48 | 49 | 50 | class BernyAdapter(logging.LoggerAdapter): 51 | def process(self, msg, kwargs): 52 | return '{} {}'.format(self.extra['step'], msg), kwargs 53 | 54 | 55 | class Berny(Generator): 56 | """Generator that receives energy and gradients and yields the next geometry. 57 | 58 | Args: 59 | geom (:class:`~berny.Geometry`): geometry to start with 60 | debug (bool): if :data:`True`, the generator yields debug info on receiving 61 | the energy and gradients, otherwise it yields :data:`None` 62 | restart (dict): start from a state saved from previous run 63 | using ``debug=True`` 64 | maxsteps (int): abort after maximum number of steps 65 | logger (:class:`logging.Logger`): alternative logger to use 66 | params: parameters that override the :data:`~berny.berny.defaults` 67 | 68 | The Berny object is to be used as follows:: 69 | 70 | optimizer = Berny(geom) 71 | for geom in optimizer: 72 | # calculate energy and gradients (as N-by-3 matrix) 73 | debug = optimizer.send((energy, gradients)) 74 | """ 75 | 76 | class State(object): 77 | pass 78 | 79 | def __init__( 80 | self, geom, debug=False, restart=None, maxsteps=100, logger=None, **params 81 | ): 82 | self._debug = debug 83 | self._maxsteps = maxsteps 84 | self._converged = False 85 | self._n = 0 86 | self._log = BernyAdapter(logger or log, {'step': self._n}) 87 | s = self._state = Berny.State() 88 | if restart: 89 | vars(s).update(restart) 90 | return 91 | s.geom = geom 92 | s.params = dict(chain(defaults.items(), params.items())) 93 | s.trust = s.params['trust'] 94 | s.coords = InternalCoords( 95 | s.geom, dihedral=s.params['dihedral'], superweakdih=s.params['superweakdih'] 96 | ) 97 | s.H = s.coords.hessian_guess(s.geom) 98 | s.weights = s.coords.weights(s.geom) 99 | s.future = OptPoint(s.coords.eval_geom(s.geom), None, None) 100 | s.first = True 101 | for line in str(s.coords).split('\n'): 102 | self._log.info(line) 103 | 104 | def __next__(self): 105 | assert self._n <= self._maxsteps 106 | if self._n == self._maxsteps or self._converged: 107 | raise StopIteration 108 | self._n += 1 109 | return self._state.geom 110 | 111 | @property 112 | def trust(self): 113 | """Current trust radius.""" 114 | return self._state.trust 115 | 116 | @property 117 | def converged(self): 118 | """Whether the optimized has converged.""" 119 | return self._converged 120 | 121 | def send(self, energy_and_gradients): # noqa: D102 122 | self._log.extra['step'] = self._n 123 | log, s = self._log.info, self._state 124 | energy, gradients = energy_and_gradients 125 | gradients = np.array(gradients) 126 | log('Energy: {:.12}'.format(energy)) 127 | B = s.coords.B_matrix(s.geom) 128 | B_inv = B.T.dot(Math.pinv(np.dot(B, B.T), log=log)) 129 | current = OptPoint(s.future.q, energy, dot(B_inv.T, gradients.reshape(-1))) 130 | if not s.first: 131 | s.H = update_hessian( 132 | s.H, current.q - s.best.q, current.g - s.best.g, log=log 133 | ) 134 | s.trust = update_trust( 135 | s.trust, 136 | current.E - s.previous.E, # or should it be s.interpolated.E? 137 | s.predicted.E - s.interpolated.E, 138 | s.predicted.q - s.interpolated.q, 139 | log=log, 140 | ) 141 | dq = s.best.q - current.q 142 | t, E = linear_search( 143 | current.E, s.best.E, dot(current.g, dq), dot(s.best.g, dq), log=log 144 | ) 145 | s.interpolated = OptPoint( 146 | current.q + t * dq, E, current.g + t * (s.best.g - current.g) 147 | ) 148 | else: 149 | s.interpolated = current 150 | if s.trust < 1e-6: 151 | raise RuntimeError('The trust radius got too small, check forces?') 152 | proj = dot(B, B_inv) 153 | H_proj = proj.dot(s.H).dot(proj) + 1000 * (eye(len(s.coords)) - proj) 154 | dq, dE, on_sphere = quadratic_step( 155 | dot(proj, s.interpolated.g), H_proj, s.weights, s.trust, log=log 156 | ) 157 | s.predicted = OptPoint(s.interpolated.q + dq, s.interpolated.E + dE, None) 158 | dq = s.predicted.q - current.q 159 | log('Total step: RMS: {:.3}, max: {:.3}'.format(Math.rms(dq), max(abs(dq)))) 160 | q, s.geom = s.coords.update_geom( 161 | s.geom, current.q, s.predicted.q - current.q, B_inv, log=log 162 | ) 163 | s.future = OptPoint(q, None, None) 164 | s.previous = current 165 | if s.first or current.E < s.best.E: 166 | s.best = current 167 | s.first = False 168 | self._converged = is_converged( 169 | current.g, s.future.q - current.q, on_sphere, s.params, log=log 170 | ) 171 | if self._n == self._maxsteps: 172 | log('Maximum number of steps reached') 173 | if self._debug: 174 | return vars(s).copy() 175 | 176 | def throw(self, *args, **kwargs): # noqa: D102 177 | return Generator.throw(self, *args, **kwargs) 178 | 179 | 180 | def no_log(msg, **kwargs): 181 | pass 182 | 183 | 184 | def update_hessian(H, dq, dg, log=no_log): 185 | dH1 = dg[None, :] * dg[:, None] / dot(dq, dg) 186 | dH2 = H.dot(dq[None, :] * dq[:, None]).dot(H) / dq.dot(H).dot(dq) 187 | dH = dH1 - dH2 # BFGS update 188 | log('Hessian update information:') 189 | log('* Change: RMS: {:.3}, max: {:.3}'.format(Math.rms(dH), abs(dH).max())) 190 | return H + dH 191 | 192 | 193 | def update_trust(trust, dE, dE_predicted, dq, log=no_log): 194 | if dE != 0: 195 | r = dE / dE_predicted # Fletcher's parameter 196 | else: 197 | r = 1.0 198 | log("Trust update: Fletcher's parameter: {:.3}".format(r)) 199 | if r < 0.25: 200 | return norm(dq) / 4 201 | elif r > 0.75 and abs(norm(dq) - trust) < 1e-10: 202 | return 2 * trust 203 | else: 204 | return trust 205 | 206 | 207 | def linear_search(E0, E1, g0, g1, log=no_log): 208 | log('Linear interpolation:') 209 | log('* Energies: {:.8}, {:.8}'.format(E0, E1)) 210 | log('* Derivatives: {:.3}, {:.3}'.format(g0, g1)) 211 | t, E = Math.fit_quartic(E0, E1, g0, g1) 212 | if t is None or t < -1 or t > 2: 213 | t, E = Math.fit_cubic(E0, E1, g0, g1) 214 | if t is None or t < 0 or t > 1: 215 | if E0 <= E1: 216 | log('* No fit succeeded, staying in new point') 217 | return 0, E0 218 | 219 | else: 220 | log('* No fit succeeded, returning to best point') 221 | return 1, E1 222 | else: 223 | msg = 'Cubic interpolation was performed' 224 | else: 225 | msg = 'Quartic interpolation was performed' 226 | log('* {}: t = {:.3}'.format(msg, t)) 227 | log('* Interpolated energy: {:.8}'.format(E)) 228 | return t, E 229 | 230 | 231 | def quadratic_step(g, H, w, trust, log=no_log): 232 | ev = np.linalg.eigvalsh((H + H.T) / 2) 233 | rfo = np.vstack((np.hstack((H, g[:, None])), np.hstack((g, 0))[None, :])) 234 | D, V = np.linalg.eigh((rfo + rfo.T) / 2) 235 | dq = V[:-1, 0] / V[-1, 0] 236 | l = D[0] 237 | if norm(dq) <= trust: 238 | log('Pure RFO step was performed:') 239 | on_sphere = False 240 | else: 241 | 242 | def steplength(l): 243 | return norm(np.linalg.solve(l * eye(H.shape[0]) - H, g)) - trust 244 | 245 | l = Math.findroot(steplength, ev[0]) # minimization on sphere 246 | dq = np.linalg.solve(l * eye(H.shape[0]) - H, g) 247 | on_sphere = True 248 | log('Minimization on sphere was performed:') 249 | dE = dot(g, dq) + 0.5 * dq.dot(H).dot(dq) # predicted energy change 250 | log('* Trust radius: {:.2}'.format(trust)) 251 | log('* Number of negative eigenvalues: {}'.format((ev < 0).sum())) 252 | log('* Lowest eigenvalue: {:.3}'.format(ev[0])) 253 | log('* lambda: {:.3}'.format(l)) 254 | log('Quadratic step: RMS: {:.3}, max: {:.3}'.format(Math.rms(dq), max(abs(dq)))) 255 | log('* Predicted energy change: {:.3}'.format(dE)) 256 | return dq, dE, on_sphere 257 | 258 | 259 | def is_converged(forces, step, on_sphere, params, log=no_log): 260 | criteria = [ 261 | ('Gradient RMS', Math.rms(forces), params['gradientrms']), 262 | ('Gradient maximum', np.max(abs(forces)), params['gradientmax']), 263 | ] 264 | if on_sphere: 265 | criteria.append(('Minimization on sphere', False)) 266 | else: 267 | criteria.extend( 268 | [ 269 | ('Step RMS', Math.rms(step), params['steprms']), 270 | ('Step maximum', np.max(abs(step)), params['stepmax']), 271 | ] 272 | ) 273 | log('Convergence criteria:') 274 | all_matched = True 275 | for crit in criteria: 276 | if len(crit) > 2: 277 | result = crit[1] < crit[2] 278 | msg = '{:.3} {} {:.3}'.format(crit[1], '<' if result else '>', crit[2]) 279 | else: 280 | msg, result = crit 281 | msg = '{}: {}'.format(crit[0], msg) if msg else crit[0] 282 | msg = '* {} => {}'.format(msg, 'OK' if result else 'no') 283 | log(msg) 284 | if not result: 285 | all_matched = False 286 | if all_matched: 287 | log('* All criteria matched') 288 | return all_matched 289 | -------------------------------------------------------------------------------- /src/berny/cli.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import json 5 | import pickle 6 | import sys 7 | from argparse import ArgumentParser 8 | from contextlib import contextmanager 9 | from socket import AF_INET, SOCK_STREAM, socket 10 | 11 | from berny import Berny, geomlib 12 | 13 | __all__ = () 14 | 15 | 16 | @contextmanager 17 | def berny_unpickled(berny=None): 18 | picklefile = 'berny.pickle' 19 | if not berny: 20 | with open(picklefile, 'rb') as f: 21 | berny = pickle.load(f) 22 | try: 23 | yield berny 24 | except Exception: 25 | raise 26 | with open(picklefile, 'wb') as f: 27 | pickle.dump(berny, f) 28 | 29 | 30 | def handler(berny, f): 31 | energy = float(next(f)) 32 | gradients = [[float(x) for x in l.split()] for l in f if l.strip()] 33 | berny.send(energy, gradients) 34 | return next(berny) 35 | 36 | 37 | def get_berny(args): 38 | geom = geomlib.load(sys.stdin, args.format) 39 | if args.paramfile: 40 | with open(args.paramfile) as f: 41 | params = json.load(f) 42 | else: 43 | params = {} 44 | berny = Berny(geom, **params) 45 | next(berny) 46 | return berny 47 | 48 | 49 | def init(args): 50 | berny = get_berny(args) 51 | berny.geom_format = args.format 52 | with berny_unpickled(berny) as berny: 53 | pass 54 | 55 | 56 | def server(args): 57 | berny = get_berny(args) 58 | host, port = args.socket 59 | server = socket(AF_INET, SOCK_STREAM) 60 | server.bind((host, int(port))) 61 | server.listen(0) 62 | while True: 63 | sock, addr = server.accept() 64 | f = sock.makefile('r+') 65 | geom = handler(berny, f) 66 | if geom: 67 | f.write(geom.dumps(args.format)) 68 | f.flush() 69 | f.close() 70 | sock.close() 71 | if not geom: 72 | break 73 | 74 | 75 | def driver(): 76 | try: 77 | with berny_unpickled() as berny: 78 | geom = handler(berny, sys.stdin) 79 | except FileNotFoundError: 80 | sys.stderr.write('error: No pickled berny, run with --init first?\n') 81 | sys.exit(1) 82 | if not geom: 83 | sys.exit(0) 84 | geom.dump(sys.stdout, berny.geom_format) 85 | sys.exit(10) 86 | 87 | 88 | def main(): 89 | parser = ArgumentParser() 90 | arg = parser.add_argument 91 | arg('--init', action='store_true', help='Initialize Berny optimizer.') 92 | arg( 93 | '-f', 94 | '--format', 95 | choices=['xyz', 'aims'], 96 | default='xyz', 97 | help='Format of geometry', 98 | ) 99 | arg( 100 | '-s', 101 | '--socket', 102 | nargs=2, 103 | metavar=('host', 'port'), 104 | help='Listen on given address', 105 | ) 106 | arg('paramfile', nargs='?', help='Optional optimization parameters as JSON') 107 | args = parser.parse_args() 108 | if args.init: 109 | init(args) 110 | elif args.socket: 111 | server(args) 112 | else: 113 | driver() 114 | -------------------------------------------------------------------------------- /src/berny/coords.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | from __future__ import division 5 | 6 | from collections import OrderedDict 7 | from itertools import combinations, product 8 | 9 | import numpy as np 10 | from numpy import dot, pi 11 | from numpy.linalg import norm 12 | 13 | from . import Math 14 | from .species_data import get_property 15 | 16 | __all__ = () 17 | 18 | angstrom = 1 / 0.52917721092 #: 19 | 20 | 21 | class InternalCoord(object): 22 | def __init__(self, C=None): 23 | if C is not None: 24 | self.weak = sum( 25 | not C[self.idx[i], self.idx[i + 1]] for i in range(len(self.idx) - 1) 26 | ) 27 | 28 | def __eq__(self, other): 29 | self.idx == other.idx # noqa B015 30 | 31 | def __hash__(self): 32 | return hash(self.idx) 33 | 34 | def __repr__(self): 35 | args = list(map(str, self.idx)) 36 | if self.weak is not None: 37 | args.append('weak=' + str(self.weak)) 38 | return '{}({})'.format(self.__class__.__name__, ', '.join(args)) 39 | 40 | 41 | class Bond(InternalCoord): 42 | def __init__(self, i, j, **kwargs): 43 | if i > j: 44 | i, j = j, i 45 | self.i = i 46 | self.j = j 47 | self.idx = i, j 48 | InternalCoord.__init__(self, **kwargs) 49 | 50 | def hessian(self, rho): 51 | return 0.45 * rho[self.i, self.j] 52 | 53 | def weight(self, rho, coords): 54 | return rho[self.i, self.j] 55 | 56 | def center(self, ijk): 57 | return np.round(ijk[[self.i, self.j]].sum(0)) 58 | 59 | def eval(self, coords, grad=False): 60 | v = (coords[self.i] - coords[self.j]) * angstrom 61 | r = norm(v) 62 | if not grad: 63 | return r 64 | return r, [v / r, -v / r] 65 | 66 | 67 | class Angle(InternalCoord): 68 | def __init__(self, i, j, k, **kwargs): 69 | if i > k: 70 | i, j, k = k, j, i 71 | self.i = i 72 | self.j = j 73 | self.k = k 74 | self.idx = i, j, k 75 | InternalCoord.__init__(self, **kwargs) 76 | 77 | def hessian(self, rho): 78 | return 0.15 * (rho[self.i, self.j] * rho[self.j, self.k]) 79 | 80 | def weight(self, rho, coords): 81 | f = 0.12 82 | return np.sqrt(rho[self.i, self.j] * rho[self.j, self.k]) * ( 83 | f + (1 - f) * np.sin(self.eval(coords)) 84 | ) 85 | 86 | def center(self, ijk): 87 | return np.round(2 * ijk[self.j]) 88 | 89 | def eval(self, coords, grad=False): 90 | v1 = (coords[self.i] - coords[self.j]) * angstrom 91 | v2 = (coords[self.k] - coords[self.j]) * angstrom 92 | dot_product = np.dot(v1, v2) / (norm(v1) * norm(v2)) 93 | if dot_product < -1: 94 | dot_product = -1 95 | elif dot_product > 1: 96 | dot_product = 1 97 | phi = np.arccos(dot_product) 98 | if not grad: 99 | return phi 100 | if abs(phi) > pi - 1e-6: 101 | grad = [ 102 | (pi - phi) / (2 * norm(v1) ** 2) * v1, 103 | (1 / norm(v1) - 1 / norm(v2)) * (pi - phi) / (2 * norm(v1)) * v1, 104 | (pi - phi) / (2 * norm(v2) ** 2) * v2, 105 | ] 106 | else: 107 | grad = [ 108 | 1 / np.tan(phi) * v1 / norm(v1) ** 2 109 | - v2 / (norm(v1) * norm(v2) * np.sin(phi)), 110 | (v1 + v2) / (norm(v1) * norm(v2) * np.sin(phi)) 111 | - 1 / np.tan(phi) * (v1 / norm(v1) ** 2 + v2 / norm(v2) ** 2), 112 | 1 / np.tan(phi) * v2 / norm(v2) ** 2 113 | - v1 / (norm(v1) * norm(v2) * np.sin(phi)), 114 | ] 115 | return phi, grad 116 | 117 | 118 | class Dihedral(InternalCoord): 119 | def __init__(self, i, j, k, l, weak=None, angles=None, C=None, **kwargs): 120 | if j > k: 121 | i, j, k, l = l, k, j, i 122 | self.i = i 123 | self.j = j 124 | self.k = k 125 | self.l = l 126 | self.idx = (i, j, k, l) 127 | self.weak = weak 128 | self.angles = angles 129 | InternalCoord.__init__(self, **kwargs) 130 | 131 | def hessian(self, rho): 132 | return 0.005 * rho[self.i, self.j] * rho[self.j, self.k] * rho[self.k, self.l] 133 | 134 | def weight(self, rho, coords): 135 | f = 0.12 136 | th1 = Angle(self.i, self.j, self.k).eval(coords) 137 | th2 = Angle(self.j, self.k, self.l).eval(coords) 138 | return ( 139 | (rho[self.i, self.j] * rho[self.j, self.k] * rho[self.k, self.l]) ** (1 / 3) 140 | * (f + (1 - f) * np.sin(th1)) 141 | * (f + (1 - f) * np.sin(th2)) 142 | ) 143 | 144 | def center(self, ijk): 145 | return np.round(ijk[[self.j, self.k]].sum(0)) 146 | 147 | def eval(self, coords, grad=False): 148 | v1 = (coords[self.i] - coords[self.j]) * angstrom 149 | v2 = (coords[self.l] - coords[self.k]) * angstrom 150 | w = (coords[self.k] - coords[self.j]) * angstrom 151 | ew = w / norm(w) 152 | a1 = v1 - dot(v1, ew) * ew 153 | a2 = v2 - dot(v2, ew) * ew 154 | sgn = np.sign(np.linalg.det(np.array([v2, v1, w]))) 155 | sgn = sgn or 1 156 | dot_product = dot(a1, a2) / (norm(a1) * norm(a2)) 157 | if dot_product < -1: 158 | dot_product = -1 159 | elif dot_product > 1: 160 | dot_product = 1 161 | phi = np.arccos(dot_product) * sgn 162 | if not grad: 163 | return phi 164 | if abs(phi) > pi - 1e-6: 165 | g = Math.cross(w, a1) 166 | g = g / norm(g) 167 | A = dot(v1, ew) / norm(w) 168 | B = dot(v2, ew) / norm(w) 169 | grad = [ 170 | g / (norm(g) * norm(a1)), 171 | -((1 - A) / norm(a1) - B / norm(a2)) * g, 172 | -((1 + B) / norm(a2) + A / norm(a1)) * g, 173 | g / (norm(g) * norm(a2)), 174 | ] 175 | elif abs(phi) < 1e-6: 176 | g = Math.cross(w, a1) 177 | g = g / norm(g) 178 | A = dot(v1, ew) / norm(w) 179 | B = dot(v2, ew) / norm(w) 180 | grad = [ 181 | g / (norm(g) * norm(a1)), 182 | -((1 - A) / norm(a1) + B / norm(a2)) * g, 183 | ((1 + B) / norm(a2) - A / norm(a1)) * g, 184 | -g / (norm(g) * norm(a2)), 185 | ] 186 | else: 187 | A = dot(v1, ew) / norm(w) 188 | B = dot(v2, ew) / norm(w) 189 | grad = [ 190 | 1 / np.tan(phi) * a1 / norm(a1) ** 2 191 | - a2 / (norm(a1) * norm(a2) * np.sin(phi)), 192 | ((1 - A) * a2 - B * a1) / (norm(a1) * norm(a2) * np.sin(phi)) 193 | - 1 194 | / np.tan(phi) 195 | * ((1 - A) * a1 / norm(a1) ** 2 - B * a2 / norm(a2) ** 2), 196 | ((1 + B) * a1 + A * a2) / (norm(a1) * norm(a2) * np.sin(phi)) 197 | - 1 198 | / np.tan(phi) 199 | * ((1 + B) * a2 / norm(a2) ** 2 + A * a1 / norm(a1) ** 2), 200 | 1 / np.tan(phi) * a2 / norm(a2) ** 2 201 | - a1 / (norm(a1) * norm(a2) * np.sin(phi)), 202 | ] 203 | return phi, grad 204 | 205 | 206 | def get_clusters(C): 207 | nonassigned = list(range(len(C))) 208 | clusters = [] 209 | while nonassigned: 210 | queue = {nonassigned[0]} 211 | clusters.append([]) 212 | while queue: 213 | node = queue.pop() 214 | clusters[-1].append(node) 215 | nonassigned.remove(node) 216 | queue.update(n for n in np.flatnonzero(C[node]) if n in nonassigned) 217 | C = np.zeros_like(C) 218 | for cluster in clusters: 219 | for i in cluster: 220 | C[i, cluster] = True 221 | return clusters, C 222 | 223 | 224 | class InternalCoords(object): 225 | def __init__(self, geom, allowed=None, dihedral=True, superweakdih=False): 226 | self._coords = [] 227 | n = len(geom) 228 | geom = geom.supercell() 229 | dist = geom.dist(geom) 230 | radii = np.array([get_property(sp, 'covalent_radius') for sp in geom.species]) 231 | bondmatrix = dist < 1.3 * (radii[None, :] + radii[:, None]) 232 | self.fragments, C = get_clusters(bondmatrix) 233 | radii = np.array([get_property(sp, 'vdw_radius') for sp in geom.species]) 234 | shift = 0.0 235 | C_total = C.copy() 236 | while not C_total.all(): 237 | bondmatrix |= ~C_total & (dist < radii[None, :] + radii[:, None] + shift) 238 | C_total = get_clusters(bondmatrix)[1] 239 | shift += 1.0 240 | for i, j in combinations(range(len(geom)), 2): 241 | if bondmatrix[i, j]: 242 | bond = Bond(i, j, C=C) 243 | self.append(bond) 244 | for j in range(len(geom)): 245 | for i, k in combinations(np.flatnonzero(bondmatrix[j, :]), 2): 246 | ang = Angle(i, j, k, C=C) 247 | if ang.eval(geom.coords) > pi / 4: 248 | self.append(ang) 249 | if dihedral: 250 | for bond in self.bonds: 251 | self.extend( 252 | get_dihedrals( 253 | [bond.i, bond.j], 254 | geom.coords, 255 | bondmatrix, 256 | C, 257 | superweak=superweakdih, 258 | ) 259 | ) 260 | if geom.lattice is not None: 261 | self._reduce(n) 262 | 263 | def append(self, coord): 264 | self._coords.append(coord) 265 | 266 | def extend(self, coords): 267 | self._coords.extend(coords) 268 | 269 | def __iter__(self): 270 | return self._coords.__iter__() 271 | 272 | def __len__(self): 273 | return len(self._coords) 274 | 275 | @property 276 | def bonds(self): 277 | return [c for c in self if isinstance(c, Bond)] 278 | 279 | @property 280 | def angles(self): 281 | return [c for c in self if isinstance(c, Angle)] 282 | 283 | @property 284 | def dihedrals(self): 285 | return [c for c in self if isinstance(c, Dihedral)] 286 | 287 | @property 288 | def dict(self): 289 | return OrderedDict( 290 | [ 291 | ('bonds', self.bonds), 292 | ('angles', self.angles), 293 | ('dihedrals', self.dihedrals), 294 | ] 295 | ) 296 | 297 | def __repr__(self): 298 | return ''.format( 299 | ', '.join( 300 | '{}: {}'.format(name, len(coords)) for name, coords in self.dict.items() 301 | ) 302 | ) 303 | 304 | def __str__(self): 305 | ncoords = sum(len(coords) for coords in self.dict.values()) 306 | s = 'Internal coordinates:\n' 307 | s += '* Number of fragments: {}\n'.format(len(self.fragments)) 308 | s += '* Number of internal coordinates: {}\n'.format(ncoords) 309 | for name, coords in self.dict.items(): 310 | for degree, adjective in [(0, 'strong'), (1, 'weak'), (2, 'superweak')]: 311 | n = len([None for c in coords if min(2, c.weak) == degree]) 312 | if n > 0: 313 | s += '* Number of {} {}: {}\n'.format(adjective, name, n) 314 | return s.rstrip() 315 | 316 | def eval_geom(self, geom, template=None): 317 | geom = geom.supercell() 318 | q = np.array([coord.eval(geom.coords) for coord in self]) 319 | if template is None: 320 | return q 321 | swapped = [] # dihedrals swapped by pi 322 | candidates = set() # potentially swapped angles 323 | for i, dih in enumerate(self): 324 | if not isinstance(dih, Dihedral): 325 | continue 326 | diff = q[i] - template[i] 327 | if abs(abs(diff) - 2 * pi) < pi / 2: 328 | q[i] -= 2 * pi * np.sign(diff) 329 | elif abs(abs(diff) - pi) < pi / 2: 330 | q[i] -= pi * np.sign(diff) 331 | swapped.append(dih) 332 | candidates.update(dih.angles) 333 | for i, ang in enumerate(self): 334 | if not isinstance(ang, Angle) or ang not in candidates: 335 | continue 336 | # candidate angle was swapped if each dihedral that contains it was 337 | # either swapped or all its angles are candidates 338 | if all( 339 | dih in swapped or all(a in candidates for a in dih.angles) 340 | for dih in self.dihedrals 341 | if ang in dih.angles 342 | ): 343 | q[i] = 2 * pi - q[i] 344 | return q 345 | 346 | def _reduce(self, n): 347 | idxs = np.int64(np.floor(np.array(range(3 ** 3 * n)) / n)) 348 | idxs, i = np.divmod(idxs, 3) 349 | idxs, j = np.divmod(idxs, 3) 350 | k = idxs % 3 351 | ijk = np.vstack((i, j, k)).T - 1 352 | self._coords = [ 353 | coord 354 | for coord in self._coords 355 | if np.all(np.isin(coord.center(ijk), [0, -1])) 356 | ] 357 | idxs = {i for coord in self._coords for i in coord.idx} 358 | self.fragments = [frag for frag in self.fragments if set(frag) & idxs] 359 | 360 | def hessian_guess(self, geom): 361 | geom = geom.supercell() 362 | rho = geom.rho() 363 | return np.diag([coord.hessian(rho) for coord in self]) 364 | 365 | def weights(self, geom): 366 | geom = geom.supercell() 367 | rho = geom.rho() 368 | return np.array([coord.weight(rho, geom.coords) for coord in self]) 369 | 370 | def B_matrix(self, geom): 371 | geom = geom.supercell() 372 | B = np.zeros((len(self), len(geom), 3)) 373 | for i, coord in enumerate(self): 374 | _, grads = coord.eval(geom.coords, grad=True) 375 | idx = [k % len(geom) for k in coord.idx] 376 | for j, grad in zip(idx, grads): 377 | B[i, j] += grad 378 | return B.reshape(len(self), 3 * len(geom)) 379 | 380 | def update_geom(self, geom, q, dq, B_inv, log=lambda _: None): 381 | geom = geom.copy() 382 | thre = 1e-6 383 | # target = CartIter(q=q+dq) 384 | # prev = CartIter(geom.coords, q, dq) 385 | for i in range(20): 386 | coords_new = geom.coords + B_inv.dot(dq).reshape(-1, 3) / angstrom 387 | dcart_rms = Math.rms(coords_new - geom.coords) 388 | geom.coords = coords_new 389 | q_new = self.eval_geom(geom, template=q) 390 | dq_rms = Math.rms(q_new - q) 391 | q, dq = q_new, dq - (q_new - q) 392 | if dcart_rms < thre: 393 | msg = 'Perfect transformation to cartesians in {} iterations' 394 | break 395 | if i == 0: 396 | keep_first = geom.copy(), q, dcart_rms, dq_rms 397 | else: 398 | msg = 'Transformation did not converge in {} iterations' 399 | geom, q, dcart_rms, dq_rms = keep_first 400 | log(msg.format(i + 1)) 401 | log('* RMS(dcart): {:.3}, RMS(dq): {:.3}'.format(dcart_rms, dq_rms)) 402 | return q, geom 403 | 404 | 405 | def get_dihedrals(center, coords, bondmatrix, C, superweak=False): 406 | lin_thre = 5 * pi / 180 407 | neigh_l = [n for n in np.flatnonzero(bondmatrix[center[0], :]) if n not in center] 408 | neigh_r = [n for n in np.flatnonzero(bondmatrix[center[-1], :]) if n not in center] 409 | angles_l = [Angle(i, center[0], center[1]).eval(coords) for i in neigh_l] 410 | angles_r = [Angle(center[-2], center[-1], j).eval(coords) for j in neigh_r] 411 | nonlinear_l = [ 412 | n 413 | for n, ang in zip(neigh_l, angles_l) 414 | if ang < pi - lin_thre and ang >= lin_thre 415 | ] 416 | nonlinear_r = [ 417 | n 418 | for n, ang in zip(neigh_r, angles_r) 419 | if ang < pi - lin_thre and ang >= lin_thre 420 | ] 421 | linear_l = [ 422 | n for n, ang in zip(neigh_l, angles_l) if ang >= pi - lin_thre or ang < lin_thre 423 | ] 424 | linear_r = [ 425 | n for n, ang in zip(neigh_r, angles_r) if ang >= pi - lin_thre or ang < lin_thre 426 | ] 427 | assert len(linear_l) <= 1 428 | assert len(linear_r) <= 1 429 | if center[0] < center[-1]: 430 | nweak = len( 431 | [None for i in range(len(center) - 1) if not C[center[i], center[i + 1]]] 432 | ) 433 | dihedrals = [] 434 | for nl, nr in product(nonlinear_l, nonlinear_r): 435 | if nl == nr: 436 | continue 437 | weak = ( 438 | nweak + (0 if C[nl, center[0]] else 1) + (0 if C[center[0], nr] else 1) 439 | ) 440 | if not superweak and weak > 1: 441 | continue 442 | dihedrals.append( 443 | Dihedral( 444 | nl, 445 | center[0], 446 | center[-1], 447 | nr, 448 | weak=weak, 449 | angles=( 450 | Angle(nl, center[0], center[1], C=C), 451 | Angle(nl, center[-2], center[-1], C=C), 452 | ), 453 | ) 454 | ) 455 | else: 456 | dihedrals = [] 457 | if len(center) > 3: 458 | pass 459 | elif linear_l and not linear_r: 460 | dihedrals.extend(get_dihedrals(linear_l + center, coords, bondmatrix, C)) 461 | elif linear_r and not linear_l: 462 | dihedrals.extend(get_dihedrals(center + linear_r, coords, bondmatrix, C)) 463 | return dihedrals 464 | -------------------------------------------------------------------------------- /src/berny/geomlib.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | import os 5 | from io import StringIO 6 | from itertools import chain, groupby, product, repeat 7 | 8 | import numpy as np 9 | from numpy import pi 10 | from numpy.linalg import inv, norm 11 | 12 | from .species_data import get_property 13 | 14 | __version__ = '0.1.0' 15 | __all__ = ['Geometry', 'loads', 'readfile'] 16 | 17 | 18 | class Geometry(object): 19 | """ 20 | Represents a single molecule or a crystal. 21 | 22 | :param list species: list of element symbols 23 | :param list coords: list of atomic coordinates in angstroms (as 3-tuples) 24 | :param list lattice: list of lattice vectors (:data:`None` for a moleucle) 25 | 26 | Iterating over a geometry yields 2-tuples of symbols and coordinates. 27 | :func:`len` returns the number of atoms in a geometry. The class supports 28 | :func:`format` with the same available formats as :meth:`dump`. 29 | """ 30 | 31 | def __init__(self, species, coords, lattice=None): 32 | self.species = species 33 | self.coords = np.array(coords) 34 | self.lattice = np.array(lattice) if lattice is not None else None 35 | 36 | @classmethod 37 | def from_atoms(cls, atoms, lattice=None, unit=1.0): 38 | """Alternative contructor. 39 | 40 | :param list atoms: list of 2-tuples with an elemnt symbol and 41 | a coordinate 42 | :param float unit: value to multiple atomic coordiantes with 43 | :param list lattice: list of lattice vectors (:data:`None` for a moleucle) 44 | """ 45 | species = [sp for sp, _ in atoms] 46 | coords = [np.array(coord, dtype=float) * unit for _, coord in atoms] 47 | return cls(species, coords, lattice) 48 | 49 | def __repr__(self): 50 | s = repr(self.formula) 51 | if self.lattice is not None: 52 | s += ' in a lattice' 53 | return '<{} {}>'.format(self.__class__.__name__, s) 54 | 55 | def __iter__(self): 56 | for specie, coord in zip(self.species, self.coords): 57 | yield specie, coord 58 | 59 | def __len__(self): 60 | return len(self.species) 61 | 62 | @property 63 | def formula(self): 64 | """Chemical formula of the molecule or a unit cell.""" 65 | composition = sorted( 66 | (sp, len(list(g))) for sp, g in groupby(sorted(self.species)) 67 | ) 68 | return ''.join('{}{}'.format(sp, n if n > 1 else '') for sp, n in composition) 69 | 70 | def __format__(self, fmt): 71 | """Return the geometry represented as a string, delegates to :meth:`dump`.""" 72 | fp = StringIO() 73 | self.dump(fp, fmt) 74 | return fp.getvalue() 75 | 76 | dumps = __format__ 77 | 78 | def dump(self, f, fmt): 79 | """Save the geometry into a file. 80 | 81 | :param file f: file object 82 | :param str fmt: geometry format, one of ``""``, ``"xyz"``, ``"aims"``, 83 | ``"mopac"``. 84 | """ 85 | if fmt == '': 86 | f.write(repr(self)) 87 | elif fmt == 'xyz': 88 | f.write('{}\n'.format(len(self))) 89 | f.write('Formula: {}\n'.format(self.formula)) 90 | for specie, coord in self: 91 | f.write( 92 | '{:>2} {}\n'.format( 93 | specie, ' '.join('{:15.8}'.format(x) for x in coord) 94 | ) 95 | ) 96 | elif fmt == 'aims': 97 | f.write('# Formula: {}\n'.format(self.formula)) 98 | for specie, coord in self: 99 | f.write( 100 | 'atom {} {:>2}\n'.format( 101 | ' '.join('{:15.8}'.format(x) for x in coord), specie 102 | ) 103 | ) 104 | elif fmt == 'mopac': 105 | f.write('* Formula: {}\n'.format(self.formula)) 106 | for specie, coord in self: 107 | f.write( 108 | '{:>2} {}\n'.format( 109 | specie, ' '.join('{:15.8} 1'.format(x) for x in coord) 110 | ) 111 | ) 112 | else: 113 | raise ValueError('Unknown format: "{}"'.format(fmt)) 114 | 115 | def copy(self): 116 | """Make a copy of the geometry.""" 117 | return Geometry( 118 | list(self.species), 119 | self.coords.copy(), 120 | self.lattice.copy() if self.lattice is not None else None, 121 | ) 122 | 123 | def write(self, filename): 124 | """ 125 | Write the geometry into a file, delegates to :meth:`dump`. 126 | 127 | :param str filename: path that will be overwritten 128 | """ 129 | ext = os.path.splitext(filename)[1] 130 | if ext == '.xyz': 131 | fmt = 'xyz' 132 | elif ext == '.aims' or os.path.basename(filename) == 'geometry.in': 133 | fmt = 'aims' 134 | elif ext == '.mopac': 135 | fmt = 'mopac' 136 | else: 137 | raise ValueError('Unknown file extension') 138 | with open(filename, 'w') as f: 139 | self.dump(f, fmt) 140 | 141 | def super_circum(self, radius): 142 | """ 143 | Supercell dimensions such that the supercell circumsribes a sphere. 144 | 145 | :param float radius: circumscribed radius in angstroms 146 | 147 | Returns :data:`None` when geometry is not a crystal. 148 | """ 149 | if self.lattice is None: 150 | return 151 | rec_lattice = 2 * pi * inv(self.lattice.T) 152 | layer_sep = np.array( 153 | [ 154 | sum(vec * rvec / norm(rvec)) 155 | for vec, rvec in zip(self.lattice, rec_lattice) 156 | ] 157 | ) 158 | return np.array(np.ceil(radius / layer_sep + 0.5), dtype=int) 159 | 160 | def supercell(self, ranges=((-1, 1), (-1, 1), (-1, 1)), cutoff=None): 161 | """ 162 | Create a crystal supercell. 163 | 164 | :param list ranges: list of 2-tuples specifying the range of multiples 165 | of the unit-cell vectors 166 | :param float cutoff: if given, the ranges are determined such that 167 | the supercell contains a sphere with the radius qual to the cutoff 168 | 169 | Returns a copy of itself when geometry is not a crystal. 170 | """ 171 | if self.lattice is None: 172 | return self.copy() 173 | if cutoff: 174 | ranges = [(-r, r) for r in self.super_circum(cutoff)] 175 | latt_vectors = np.array( 176 | [(0, 0, 0)] 177 | + [ 178 | sum(k * vec for k, vec in zip(shift, self.lattice)) 179 | for shift in product(*[range(a, b + 1) for a, b in ranges]) 180 | if shift != (0, 0, 0) 181 | ] 182 | ) 183 | species = list(chain.from_iterable(repeat(self.species, len(latt_vectors)))) 184 | coords = (self.coords[None, :, :] + latt_vectors[:, None, :]).reshape((-1, 3)) 185 | lattice = self.lattice * np.array([b - a for a, b in ranges])[:, None] 186 | return Geometry(species, coords, lattice) 187 | 188 | def dist_diff(self, other=None): 189 | r""" 190 | Calculate distances and vectors between atoms. 191 | 192 | Args: 193 | other (:class:`~berny.Geometry`): calculate distances between two 194 | geometries if given or within a geometry if not 195 | 196 | Returns: 197 | :math:`R_{ij}:=|\mathbf R_i-\mathbf R_j|` and 198 | :math:`R_{ij\alpha}:=(\mathbf R_i)_\alpha-(\mathbf R_j)_\alpha`. 199 | """ 200 | if other is None: 201 | other = self 202 | diff = self.coords[:, None, :] - other.coords[None, :, :] 203 | dist = np.sqrt(np.sum(diff ** 2, 2)) 204 | dist[np.diag_indices(len(self))] = np.inf 205 | return dist, diff 206 | 207 | def dist(self, other=None): 208 | """Alias for the first element of :meth:`dist_diff`.""" 209 | return self.dist_diff(other)[0] 210 | 211 | def bondmatrix(self, scale=1.3): 212 | r""" 213 | Calculate the covalent connectedness matrix. 214 | 215 | :param float scale: threshold for accepting a distance as a covalent bond 216 | 217 | Returns: 218 | :math:`b_{ij}:=R_{ij}<\text{scale}\times (R_i^\text{cov}+R_j^\text{cov})`. 219 | """ 220 | dist = self.dist(self) 221 | radii = np.array([get_property(sp, 'covalent_radius') for sp in self.species]) 222 | return dist < scale * (radii[None, :] + radii[:, None]) 223 | 224 | def rho(self): 225 | r""" 226 | Calculate a measure of covalentness. 227 | 228 | Returns: 229 | :math:`\rho_{ij}:=\exp\big(-R_{ij}/(R_i^\text{cov}+R_j^\text{cov})\big)`. 230 | """ 231 | geom = self.supercell() 232 | dist = geom.dist(geom) 233 | radii = np.array([get_property(sp, 'covalent_radius') for sp in geom.species]) 234 | return np.exp(-dist / (radii[None, :] + radii[:, None]) + 1) 235 | 236 | @property 237 | def masses(self): 238 | """Numpy array of atomic masses.""" 239 | return np.array([get_property(sp, 'mass') for sp in self.species]) 240 | 241 | @property 242 | def cms(self): 243 | r"""Calculate the center of mass, :math:`\mathbf R_\text{CMS}`.""" 244 | masses = self.masses 245 | return np.sum(masses[:, None] * self.coords, 0) / masses.sum() 246 | 247 | @property 248 | def inertia(self): 249 | r"""Calculate the moment of inertia. 250 | 251 | .. math:: 252 | I_{\alpha\beta}:= 253 | \sum_im_i\big(r_i^2\delta_{\alpha\beta}-(\mathbf r_i)_\alpha(\mathbf 254 | r_i)_\beta\big),\qquad 255 | \mathbf r_i=\mathbf R_i-\mathbf R_\text{CMS} 256 | """ 257 | coords_w = np.sqrt(self.masses)[:, None] * (self.coords - self.cms) 258 | A = np.array([np.diag(np.full(3, r)) for r in np.sum(coords_w ** 2, 1)]) 259 | B = coords_w[:, :, None] * coords_w[:, None, :] 260 | return np.sum(A - B, 0) 261 | 262 | 263 | def load(fp, fmt): 264 | """ 265 | Read a geometry from a file object. 266 | 267 | :param file fp: file object 268 | :param str fmt: the format of the geometry file, can be one of ``"xyz"``, 269 | ``"aims"`` 270 | 271 | Returns :class:`~berny.Geometry`. 272 | """ 273 | if fmt == 'xyz': 274 | n = int(fp.readline()) 275 | fp.readline() 276 | species = [] 277 | coords = [] 278 | for _ in range(n): 279 | l = fp.readline().split() 280 | species.append(l[0]) 281 | coords.append([float(x) for x in l[1:4]]) 282 | return Geometry(species, coords) 283 | if fmt == 'aims': 284 | species = [] 285 | coords = [] 286 | lattice = [] 287 | while True: 288 | l = fp.readline() 289 | if l == '': 290 | break 291 | l = l.strip() 292 | if not l or l.startswith('#'): 293 | continue 294 | l = l.split() 295 | what = l[0] 296 | if what == 'atom': 297 | species.append(l[4]) 298 | coords.append([float(x) for x in l[1:4]]) 299 | elif what == 'lattice_vector': 300 | lattice.append([float(x) for x in l[1:4]]) 301 | if lattice: 302 | assert len(lattice) == 3 303 | return Geometry(species, coords, lattice) 304 | else: 305 | return Geometry(species, coords) 306 | 307 | 308 | def loads(s, fmt): 309 | """ 310 | Read a geometry from a string, delegates to :func:`load`. 311 | 312 | :param str s: string with geometry 313 | """ 314 | fp = StringIO(s) 315 | return load(fp, fmt) 316 | 317 | 318 | def readfile(path, fmt=None): 319 | """ 320 | Read a geometry from a file path, delegates to :func:`load`. 321 | 322 | :param str path: path to a geometry file 323 | :param str fmt: if not given, the format is given from the file extension 324 | """ 325 | if not fmt: 326 | ext = os.path.splitext(path)[1] 327 | if ext == '.xyz': 328 | fmt = 'xyz' 329 | if ext == '.aims' or os.path.basename(path) == 'geometry.in': 330 | fmt = 'aims' 331 | with open(path) as f: 332 | return load(f, fmt) 333 | -------------------------------------------------------------------------------- /src/berny/optimize.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | __version__ = '0.2.0' 5 | 6 | 7 | def optimize(optimizer, solver, trajectory=None): 8 | """Optimize a geometry with respect to a solver. 9 | 10 | Args: 11 | optimizer (:class:`~collections.abc.Generator`): Optimizer object with 12 | the same generator interface as :class:`~berny.Berny` 13 | solver (:class:`~collections.abc.Generator`): unprimed generator that 14 | receives geometry as a 2-tuple of a list of 2-tuples of the atom 15 | symbol and coordinate (as a 3-tuple), and of a list of lattice 16 | vectors (or :data:`None` if molecule), and yields the energy and 17 | gradients (as a :math:`N`-by-3 matrix or :math:`(N+3)`-by-3 matrix 18 | in case of a crystal geometry). 19 | 20 | See :class:`~berny.solvers.MopacSolver` for an example. 21 | trajectory (str): filename for the XYZ trajectory 22 | 23 | Returns: 24 | The optimized geometry. 25 | 26 | The function is equivalent to:: 27 | 28 | next(solver) 29 | for geom in optimizer: 30 | energy, gradients = solver.send((list(geom), geom.lattice)) 31 | optimizer.send((energy, gradients)) 32 | """ 33 | if trajectory: 34 | trajectory = open(trajectory, 'w') 35 | try: 36 | next(solver) 37 | for geom in optimizer: 38 | energy, gradients = solver.send((list(geom), geom.lattice)) 39 | if trajectory: 40 | geom.dump(trajectory, 'xyz') 41 | optimizer.send((energy, gradients)) 42 | finally: 43 | if trajectory: 44 | trajectory.close() 45 | return geom 46 | -------------------------------------------------------------------------------- /src/berny/solvers.py: -------------------------------------------------------------------------------- 1 | # Any copyright is dedicated to the Public Domain. 2 | # http://creativecommons.org/publicdomain/zero/1.0/ 3 | from __future__ import division 4 | 5 | import os 6 | import shutil 7 | import subprocess 8 | import tempfile 9 | 10 | import numpy as np 11 | 12 | from .coords import angstrom 13 | 14 | __all__ = ['MopacSolver'] 15 | 16 | 17 | def MopacSolver(cmd='mopac', method='PM7', workdir=None): 18 | """ 19 | Crate a solver that wraps `MOPAC `_. 20 | 21 | Mopac needs to be installed on the system. 22 | 23 | :param str cmd: MOPAC executable 24 | :param str method: model to calculate energy 25 | """ 26 | kcal = 1 / 627.503 27 | tmpdir = workdir or tempfile.mkdtemp() 28 | try: 29 | atoms, lattice = yield 30 | while True: 31 | mopac_input = '{} 1SCF GRADIENTS\n\n\n'.format(method) + '\n'.join( 32 | '{} {} 1 {} 1 {} 1'.format(el, *coord) for el, coord in atoms 33 | ) 34 | if lattice is not None: 35 | mopac_input += '\n' + '\n'.join( 36 | 'Tv {} 1 {} 1 {} 1'.format(*vec) for vec in lattice 37 | ) 38 | input_file = os.path.join(tmpdir, 'job.mop') 39 | with open(input_file, 'w') as f: 40 | f.write(mopac_input) 41 | subprocess.check_call([cmd, input_file]) 42 | with open(os.path.join(tmpdir, 'job.out')) as f: 43 | energy = float( 44 | next(l for l in f if 'FINAL HEAT OF FORMATION' in l).split()[5] 45 | ) 46 | next(l for l in f if 'FINAL POINT AND DERIVATIVES' in l) 47 | next(f) 48 | next(f) 49 | gradients = np.array( 50 | [ 51 | [float(next(f).split()[6]) for _ in range(3)] 52 | for _ in range(len(atoms) + (0 if lattice is None else 3)) 53 | ] 54 | ) 55 | atoms, lattice = yield energy * kcal, gradients * kcal / angstrom 56 | finally: 57 | if tmpdir != workdir: 58 | shutil.rmtree(tmpdir) 59 | 60 | 61 | def GenericSolver(f, *args, **kwargs): 62 | delta = kwargs.pop('delta', 1e-3) 63 | atoms, lattice = yield 64 | while True: 65 | energy = f(atoms, lattice, *args, **kwargs) 66 | coords = np.array([coord for _, coord in atoms]) 67 | gradients = np.zeros(coords.shape) 68 | for i_atom in range(coords.shape[0]): 69 | for i_xyz in range(3): 70 | ene = {} 71 | for step in [-2, -1, 1, 2]: 72 | coords_diff = coords.copy() 73 | coords_diff[i_atom, i_xyz] += step * delta 74 | atoms_diff = list(zip([sp for sp, _, in atoms], coords_diff)) 75 | ene[step] = f(atoms_diff, lattice, *args, **kwargs) 76 | gradients[i_atom, i_xyz] = _diff5(ene, delta) 77 | if lattice is not None: 78 | lattice_grads = np.zeros((3, 3)) 79 | for i_vec in range(3): 80 | for i_xyz in range(3): 81 | ene = {} 82 | for step in [-2, -1, 1, 2]: 83 | lattice_diff = lattice.copy() 84 | lattice_diff[i_vec, i_xyz] += step * delta 85 | ene[step] = f(atoms, lattice_diff, *args, **kwargs) 86 | lattice_grads[i_vec, i_xyz] = _diff5(ene, delta) 87 | gradients = np.vstack((gradients, lattice_grads)) 88 | atoms, lattice = yield energy, gradients / angstrom 89 | 90 | 91 | def _diff5(x, delta): 92 | return (1 / 12 * x[-2] - 2 / 3 * x[-1] + 2 / 3 * x[1] - 1 / 12 * x[2]) / delta 93 | -------------------------------------------------------------------------------- /src/berny/species-data.csv: -------------------------------------------------------------------------------- 1 | "number","name","symbol","covalent_radius","mass","vdw_radius" 2 | 1,"hydrogen","H",0.38,1.0079,1.6404493538 3 | 2,"helium","He",0.32,4.0026,1.4023196089 4 | 3,"lithium","Li",1.34,6.941,2.2013771973 5 | 4,"beryllium","Be",0.9,9.0122,2.2066689695 6 | 5,"boron","B",0.82,10.811,2.0584993504 7 | 6,"carbon","C",0.77,12.0107,1.8997461871 8 | 7,"nitrogen","N",0.75,14.0067,1.7674518844 9 | 8,"oxygen","O",0.73,15.9994,1.6880753028 10 | 9,"fluorine","F",0.71,18.9984,1.6086987211 11 | 10,"neon","Ne",0.69,20.1797,1.5399056837 12 | 11,"sodium","Na",1.54,22.9897,1.9738309967 13 | 12,"magnesium","Mg",1.3,24.305,2.2595866905 14 | 13,"aluminium","Al",1.18,26.9815,2.2913373232 15 | 14,"silicon","Si",1.11,28.0855,2.2225442858 16 | 15,"phosphorus","P",1.06,30.9738,2.1220006157 17 | 16,"sulfur","S",1.02,32.065,2.0426240341 18 | 17,"chlorine","Cl",0.99,35.453,1.9632474524 19 | 18,"argon","Ar",0.97,39.948,1.8785790987 20 | 19,"potassium","K",1.96,39.0983,1.9632474524 21 | 20,"calcium","Ca",1.74,40.078,2.4606740307 22 | 21,"scandium","Sc",1.44,44.9559,2.428923398 23 | 22,"titanium","Ti",1.36,47.867,2.3865892212 24 | 23,"vanadium","V",1.25,50.9415,2.3495468164 25 | 24,"chromium","Cr",1.27,51.9961,2.1114170715 26 | 25,"manganese","Mn",1.39,54.938,2.1008335273 27 | 26,"iron","Fe",1.25,55.845,2.2384196021 28 | 27,"cobalt","Co",1.26,58.9332,2.2119607416 29 | 28,"nickel","Ni",1.21,58.6934,2.0214569456 30 | 29,"copper","Cu",1.38,63.546,1.989706313 31 | 30,"zinc","Zn",1.31,65.39,2.1272923878 32 | 31,"gallium","Ga",1.26,69.723,2.2172525137 33 | 32,"germanium","Ge",1.22,72.64,2.2225442858 34 | 33,"arsenic","As",1.19,74.9216,2.1749183368 35 | 34,"selenium","Se",1.16,78.96,2.137875932 36 | 35,"bromine","Br",1.14,79.904,2.0796664388 37 | 36,"krypton","Kr",1.1,83.8,2.0214569456 38 | 37,"rubidium","Rb",2.11,85.4678,1.9685392245 39 | 38,"strontium","Sr",1.92,87.62,2.4024645375 40 | 39,"yttrium","Y",1.62,88.9059,2.5480411882 41 | 40,"zirconium","Zr",1.48,91.224,2.3971727654 42 | 41,"niobium","Nb",1.37,92.9064,2.241859254 43 | 42,"molybdenum","Mo",1.45,95.94,2.1690973875 44 | 43,"technetium","Tc",1.56,98,2.1569263116 45 | 44,"ruthenium","Ru",1.26,101.07,2.1142217107 46 | 45,"rhodium","Rh",1.35,102.9055,2.0902499831 47 | 46,"palladium","Pd",1.31,106.42,1.9367885919 48 | 47,"silver","Ag",1.53,107.8682,2.0214569456 49 | 48,"cadmium","Cd",1.48,112.411,2.1114170715 50 | 49,"indium","In",1.44,114.818,2.239467373 51 | 50,"tin","Sn",1.41,118.71,2.2770495385 52 | 51,"antimony","Sb",1.38,121.76,2.2627617538 53 | 52,"tellurium","Te",1.35,127.6,2.23312783 54 | 53,"iodine","I",1.33,126.9045,2.2066689695 55 | 54,"xenon","Xe",1.3,131.293,2.1590430205 56 | 55,"caesium","Cs",2.25,132.9055,2.0002898572 57 | 56,"barium","Ba",1.98,137.327,2.524175296 58 | 57,"lanthanum","La",1.69,138.9055,0 59 | 58,"cerium","Ce",,140.116,0 60 | 59,"praseodymium","Pr",,140.9077,0 61 | 60,"neodymium","Nd",,144.24,0 62 | 61,"promethium","Pm",,145,0 63 | 62,"samarium","Sm",,150.36,0 64 | 63,"europium","Eu",,151.964,0 65 | 64,"gadolinium","Gd",,157.25,0 66 | 65,"terbium","Tb",,158.9253,0 67 | 66,"dysprosium","Dy",,162.5,0 68 | 67,"holmium","Ho",,164.9303,0 69 | 68,"erbium","Er",,167.259,0 70 | 69,"thulium","Tm",,168.9342,0 71 | 70,"ytterbium","Yb",,173.04,0 72 | 71,"lutetium","Lu",1.6,174.967,0 73 | 72,"hafnium","Hf",1.5,178.49,2.2278360579 74 | 73,"tantalum","Ta",1.38,180.9479,2.1960854252 75 | 74,"tungsten","W",1.46,183.84,2.1590430205 76 | 75,"rhenium","Re",1.59,186.207,2.1272923878 77 | 76,"osmium","Os",1.28,190.23,2.0320404899 78 | 77,"iridium","Ir",1.37,192.217,2.1167088436 79 | 78,"platinum","Pt",1.28,195.078,2.0743746667 80 | 79,"gold","Au",1.44,196.9665,2.0426240341 81 | 80,"mercury","Hg",1.49,200.59,2.1061252994 82 | 81,"thallium","Tl",1.48,204.3833,2.0690828946 83 | 82,"lead","Pb",1.47,207.2,2.280753779 84 | 83,"bismuth","Bi",1.46,208.9804,2.2860455511 85 | 84,"polonium","Po",,209,2.1680390331 86 | 85,"astatine","At",,210,2.1537512484 87 | 86,"radon","Rn",1.45,222,2.2384196021 88 | 87,"francium","Fr",,223,0 89 | 88,"radium","Ra",,226,0 90 | 89,"actinium","Ac",,227,0 91 | 90,"thorium","Th",,232.0381,0 92 | 91,"protactinium","Pa",,231.0359,0 93 | 92,"uranium","U",,238.0289,0 94 | -------------------------------------------------------------------------------- /src/berny/species_data.py: -------------------------------------------------------------------------------- 1 | # Any copyright is dedicated to the Public Domain. 2 | # http://creativecommons.org/publicdomain/zero/1.0/ 3 | import csv 4 | import sys 5 | 6 | from pkg_resources import resource_string 7 | 8 | __all__ = () 9 | 10 | 11 | def get_property(idx, name): 12 | if isinstance(idx, str): 13 | return species_data[idx][name] 14 | try: 15 | return next(row[name] for row in species_data if row['number'] == idx) 16 | except StopIteration: 17 | raise KeyError('No species with number "{}"'.format(idx)) 18 | 19 | 20 | def _get_species_data(): 21 | csv_lines = resource_string(__name__, 'species-data.csv').split(b'\n') 22 | if sys.version_info[0] > 2: 23 | csv_lines = [l.decode() for l in csv_lines] 24 | reader = csv.DictReader(csv_lines, quoting=csv.QUOTE_NONNUMERIC) 25 | species_data = {row['symbol']: row for row in reader} 26 | return species_data 27 | 28 | 29 | species_data = _get_species_data() 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhrmnn/pyberny/a35cde88e00c160a34d6f197d4b59e2d7a9d7d73/tests/__init__.py -------------------------------------------------------------------------------- /tests/aniline.xyz: -------------------------------------------------------------------------------- 1 | 14 2 | Aniline 3 | H 1.5205 -0.1372 2.5286 4 | C 0.9575 -0.0905 1.5914 5 | C -0.4298 -0.1902 1.6060 6 | H -0.9578 -0.3156 2.5570 7 | C -1.1520 -0.1316 0.4215 8 | H -2.2452 -0.2104 0.4492 9 | C -0.4779 0.0324 -0.7969 10 | N -1.2191 0.2008 -2.0081 11 | H -2.0974 -0.2669 -1.9681 12 | H -0.6944 -0.0913 -2.8025 13 | C 0.9208 0.1292 -0.8109 14 | H 1.4628 0.2560 -1.7555 15 | C 1.6275 0.0685 0.3828 16 | H 2.7196 0.1470 0.3709 17 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(format='%(levelname)s:%(name)s: %(message)s') 4 | logging.getLogger('berny').setLevel('INFO') 5 | -------------------------------------------------------------------------------- /tests/cyanogen.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | 3 | N 3.545830 3.669192 7.228181 4 | C 3.601888 3.624940 6.062501 5 | C 3.671915 3.575700 4.697549 6 | N 3.727670 3.537778 3.532496 7 | -------------------------------------------------------------------------------- /tests/ethanol.xyz: -------------------------------------------------------------------------------- 1 | 9 2 | 3 | C 1.1879 -0.3829 0.0000 4 | C 0.0000 0.5526 0.0000 5 | O -1.1867 -0.2472 0.0000 6 | H -1.9237 0.3850 0.0000 7 | H 2.0985 0.2306 0.0000 8 | H 1.1184 -1.0093 0.8869 9 | H 1.1184 -1.0093 -0.8869 10 | H -0.0227 1.1812 0.8852 11 | H -0.0227 1.1812 -0.8852 12 | -------------------------------------------------------------------------------- /tests/test_coords.py: -------------------------------------------------------------------------------- 1 | from berny.coords import InternalCoords, angstrom 2 | from berny.geomlib import Geometry 3 | 4 | 5 | def test_cycle_dihedrals(): 6 | geom = Geometry.from_atoms( 7 | [ 8 | (ws[1], ws[2:5]) 9 | for ws in ( 10 | l.split() 11 | for l in """\ 12 | 1 H -0.000000000000 0.000000000000 -1.142569988888 13 | 2 O 1.784105551801 1.364934064507 -1.021376180623 14 | 3 H 2.248320553963 2.318104360291 -2.500037742933 15 | 4 H 3.285761299420 0.674554743661 -0.259576564237 16 | 5 O -1.784105551799 -1.364934064536 -1.021376180591 17 | 6 H -2.248320553963 -2.318104360291 -2.500037742933 18 | 7 H -3.285761299424 -0.674554743614 -0.259576564287 19 | 8 O 5.839754502206 -0.500682935209 1.037064691223 20 | 9 H 7.440059622286 -1.597667062287 0.565115038647 21 | 10 H 6.475526400773 0.638572472561 2.500357106648 22 | 11 O -5.839754502205 0.500682935191 1.037064691242 23 | 12 H -7.440059622286 1.597667062287 0.565115038647 24 | 13 H -6.475526400773 -0.638572472561 2.500357106648 25 | """.strip().split( 26 | '\n' 27 | ) 28 | ) 29 | ], 30 | unit=1 / angstrom, 31 | ) 32 | coords = InternalCoords(geom) 33 | assert not [dih for dih in coords.dihedrals if len(set(dih.idx)) < 4] 34 | -------------------------------------------------------------------------------- /tests/test_optimize.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pkg_resources import resource_filename 3 | 4 | from berny import Berny, geomlib, optimize 5 | from berny.solvers import MopacSolver 6 | 7 | 8 | @pytest.fixture 9 | def mopac(scope='session'): 10 | return MopacSolver() 11 | 12 | 13 | def ethanol(): 14 | return geomlib.readfile(resource_filename('tests', 'ethanol.xyz')), 5 15 | 16 | 17 | def aniline(): 18 | return geomlib.readfile(resource_filename('tests', 'aniline.xyz')), 11 19 | 20 | 21 | def cyanogen(): 22 | return geomlib.readfile(resource_filename('tests', 'cyanogen.xyz')), 4 23 | 24 | 25 | def water(): 26 | return geomlib.readfile(resource_filename('tests', 'water.xyz')), 7 27 | 28 | 29 | @pytest.mark.parametrize('test_case', [ethanol, aniline, cyanogen, water]) 30 | def test_optimize(mopac, test_case): 31 | geom, n_ref = test_case() 32 | berny = Berny(geom) 33 | optimize(berny, mopac) 34 | assert berny.converged 35 | assert berny._n == n_ref 36 | -------------------------------------------------------------------------------- /tests/water.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | 3 | O 0 0 0 4 | H 0 0.7935 0 5 | H 0 0 0.7935 6 | --------------------------------------------------------------------------------