├── .coveragerc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── LICENSE.ASE ├── LICENSE.Cython ├── LICENSE.NumPy ├── LICENSE.SciPy └── README ├── README.md ├── environment.yml ├── pyproject.toml ├── requirements.txt ├── sella ├── __init__.py ├── eigensolvers.py ├── force_match.pyx ├── hessian_update.py ├── internal.py ├── linalg.py ├── optimize │ ├── __init__.py │ ├── irc.py │ ├── optimize.py │ ├── restricted_step.py │ └── stepper.py ├── peswrapper.py ├── py.typed ├── samd.py └── utilities │ ├── __init__.py │ ├── blas.pxd │ ├── blas.pyx │ ├── math.pxd │ └── math.pyx ├── setup.cfg ├── setup.py └── tests ├── integration ├── test_morse_cluster.py └── test_tip3p_cluster.py ├── internal └── test_get_internal.py ├── test_eigensolvers.py ├── test_hessian_update.py ├── test_linalg.py ├── test_peswrapper.py ├── test_utils ├── __init__.py ├── matrix_factory.py ├── poly_factory.py └── test_poly_factory.py └── utilities ├── math_wrappers.pyx └── test_math.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | plugins = Cython.Coverage 3 | source = sella 4 | omit = 5 | tests 6 | *__init__.py 7 | *.pxd 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | name: Build wheels on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-13, macos-14] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.11" 23 | 24 | - name: Install cibuildwheel 25 | run: python3 -m pip install cibuildwheel==2.16.5 26 | 27 | - name: Build wheels 28 | run: python3 -m cibuildwheel --output-dir wheelhouse 29 | 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: cibw-${{ matrix.os }}-${{ strategy.job-index }} 33 | path: ./wheelhouse/*.whl 34 | 35 | make_sdist: 36 | name: Make SDist 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Set up Python 42 | uses: actions/setup-python@v4 43 | with: 44 | python-version: "3.11" 45 | 46 | - name: Install dependencies 47 | run: | 48 | python3 -m pip install --upgrade pip build setuptools cython numpy scipy ase jax jaxlib 49 | python3 -m pip install -r requirements.txt 50 | 51 | - name: Build SDist 52 | run: python3 -m build --sdist 53 | 54 | - uses: actions/upload-artifact@v4 55 | with: 56 | name: cibw-sdist 57 | path: dist/*.tar.gz 58 | 59 | upload: 60 | needs: [build, make_sdist] 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/download-artifact@v4 65 | with: 66 | pattern: cibw-* 67 | path: dist 68 | merge-multiple: true 69 | 70 | - name: Set up Python 71 | uses: actions/setup-python@v4 72 | with: 73 | python-version: "3.11" 74 | 75 | - name: Upload with twine 76 | env: 77 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 78 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 79 | run: | 80 | ls -l dist/* 81 | python3 -m pip install twine 82 | twine upload dist/* 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | __pycache__ 4 | *.so 5 | *.o 6 | *.egg 7 | *.egg-info 8 | *.c 9 | .*.swp 10 | *.html 11 | *.traj 12 | .coverage 13 | build 14 | *venv 15 | .eggs/.*txt 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | 5 | * Internal coordinate optimization implemented 6 | * Invoke with `Sella(..., internal=True)` 7 | * Automatically constructs dummy atoms when necessary 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 National Technology & Engineering Solutions of Sandia, 2 | LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the 3 | U.S. Government retains certain rights in this software. 4 | 5 | GNU LESSER GENERAL PUBLIC LICENSE 6 | Version 3, 29 June 2007 7 | 8 | Copyright (C) 2007 Free Software Foundation, Inc. 9 | Everyone is permitted to copy and distribute verbatim copies 10 | of this license document, but changing it is not allowed. 11 | 12 | 13 | This version of the GNU Lesser General Public License incorporates 14 | the terms and conditions of version 3 of the GNU General Public 15 | License, supplemented by the additional permissions listed below. 16 | 17 | 0. Additional Definitions. 18 | 19 | As used herein, "this License" refers to version 3 of the GNU Lesser 20 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 21 | General Public License. 22 | 23 | "The Library" refers to a covered work governed by this License, 24 | other than an Application or a Combined Work as defined below. 25 | 26 | An "Application" is any work that makes use of an interface provided 27 | by the Library, but which is not otherwise based on the Library. 28 | Defining a subclass of a class defined by the Library is deemed a mode 29 | of using an interface provided by the Library. 30 | 31 | A "Combined Work" is a work produced by combining or linking an 32 | Application with the Library. The particular version of the Library 33 | with which the Combined Work was made is also called the "Linked 34 | Version". 35 | 36 | The "Minimal Corresponding Source" for a Combined Work means the 37 | Corresponding Source for the Combined Work, excluding any source code 38 | for portions of the Combined Work that, considered in isolation, are 39 | based on the Application, and not on the Linked Version. 40 | 41 | The "Corresponding Application Code" for a Combined Work means the 42 | object code and/or source code for the Application, including any data 43 | and utility programs needed for reproducing the Combined Work from the 44 | Application, but excluding the System Libraries of the Combined Work. 45 | 46 | 1. Exception to Section 3 of the GNU GPL. 47 | 48 | You may convey a covered work under sections 3 and 4 of this License 49 | without being bound by section 3 of the GNU GPL. 50 | 51 | 2. Conveying Modified Versions. 52 | 53 | If you modify a copy of the Library, and, in your modifications, a 54 | facility refers to a function or data to be supplied by an Application 55 | that uses the facility (other than as an argument passed when the 56 | facility is invoked), then you may convey a copy of the modified 57 | version: 58 | 59 | a) under this License, provided that you make a good faith effort to 60 | ensure that, in the event an Application does not supply the 61 | function or data, the facility still operates, and performs 62 | whatever part of its purpose remains meaningful, or 63 | 64 | b) under the GNU GPL, with none of the additional permissions of 65 | this License applicable to that copy. 66 | 67 | 3. Object Code Incorporating Material from Library Header Files. 68 | 69 | The object code form of an Application may incorporate material from 70 | a header file that is part of the Library. You may convey such object 71 | code under terms of your choice, provided that, if the incorporated 72 | material is not limited to numerical parameters, data structure 73 | layouts and accessors, or small macros, inline functions and templates 74 | (ten or fewer lines in length), you do both of the following: 75 | 76 | a) Give prominent notice with each copy of the object code that the 77 | Library is used in it and that the Library and its use are 78 | covered by this License. 79 | 80 | b) Accompany the object code with a copy of the GNU GPL and this license 81 | document. 82 | 83 | 4. Combined Works. 84 | 85 | You may convey a Combined Work under terms of your choice that, 86 | taken together, effectively do not restrict modification of the 87 | portions of the Library contained in the Combined Work and reverse 88 | engineering for debugging such modifications, if you also do each of 89 | the following: 90 | 91 | a) Give prominent notice with each copy of the Combined Work that 92 | the Library is used in it and that the Library and its use are 93 | covered by this License. 94 | 95 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 96 | document. 97 | 98 | c) For a Combined Work that displays copyright notices during 99 | execution, include the copyright notice for the Library among 100 | these notices, as well as a reference directing the user to the 101 | copies of the GNU GPL and this license document. 102 | 103 | d) Do one of the following: 104 | 105 | 0) Convey the Minimal Corresponding Source under the terms of this 106 | License, and the Corresponding Application Code in a form 107 | suitable for, and under terms that permit, the user to 108 | recombine or relink the Application with a modified version of 109 | the Linked Version to produce a modified Combined Work, in the 110 | manner specified by section 6 of the GNU GPL for conveying 111 | Corresponding Source. 112 | 113 | 1) Use a suitable shared library mechanism for linking with the 114 | Library. A suitable mechanism is one that (a) uses at run time 115 | a copy of the Library already present on the user's computer 116 | system, and (b) will operate properly with a modified version 117 | of the Library that is interface-compatible with the Linked 118 | Version. 119 | 120 | e) Provide Installation Information, but only if you would otherwise 121 | be required to provide such information under section 6 of the 122 | GNU GPL, and only to the extent that such information is 123 | necessary to install and execute a modified version of the 124 | Combined Work produced by recombining or relinking the 125 | Application with a modified version of the Linked Version. (If 126 | you use option 4d0, the Installation Information must accompany 127 | the Minimal Corresponding Source and Corresponding Application 128 | Code. If you use option 4d1, you must provide the Installation 129 | Information in the manner specified by section 6 of the GNU GPL 130 | for conveying Corresponding Source.) 131 | 132 | 5. Combined Libraries. 133 | 134 | You may place library facilities that are a work based on the 135 | Library side by side in a single library together with other library 136 | facilities that are not Applications and are not covered by this 137 | License, and convey such a combined library under terms of your 138 | choice, if you do both of the following: 139 | 140 | a) Accompany the combined library with a copy of the same work based 141 | on the Library, uncombined with any other library facilities, 142 | conveyed under the terms of this License. 143 | 144 | b) Give prominent notice with the combined library that part of it 145 | is a work based on the Library, and explaining where to find the 146 | accompanying uncombined form of the same work. 147 | 148 | 6. Revised Versions of the GNU Lesser General Public License. 149 | 150 | The Free Software Foundation may publish revised and/or new versions 151 | of the GNU Lesser General Public License from time to time. Such new 152 | versions will be similar in spirit to the present version, but may 153 | differ in detail to address new problems or concerns. 154 | 155 | Each version is given a distinguishing version number. If the 156 | Library as you received it specifies that a certain numbered version 157 | of the GNU Lesser General Public License "or any later version" 158 | applies to it, you have the option of following the terms and 159 | conditions either of that published version or of any later version 160 | published by the Free Software Foundation. If the Library as you 161 | received it does not specify a version number of the GNU Lesser 162 | General Public License, you may choose any version of the GNU Lesser 163 | General Public License ever published by the Free Software Foundation. 164 | 165 | If the Library as you received it specifies that a proxy can decide 166 | whether future versions of the GNU Lesser General Public License shall 167 | apply, that proxy's public statement of acceptance of any version is 168 | permanent authorization for you to choose that version for the 169 | Library. 170 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include sella *.pyx 3 | -------------------------------------------------------------------------------- /NOTICE/LICENSE.ASE: -------------------------------------------------------------------------------- 1 | ASE is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU Lesser General Public License as published by 3 | the Free Software Foundation, either version 2.1 of the License, or 4 | (at your option) any later version. 5 | 6 | ASE is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU Lesser General Public License for more details. 10 | 11 | You should have received a copy of the GNU Lesser General Public License 12 | along with ASE. If not, see . 13 | -------------------------------------------------------------------------------- /NOTICE/LICENSE.Cython: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /NOTICE/LICENSE.NumPy: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, NumPy Developers. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of the NumPy Developers nor the names of any 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | 34 | The NumPy repository and source distributions bundle several libraries that are 35 | compatibly licensed. We list these here. 36 | 37 | Name: Numpydoc 38 | Files: doc/sphinxext/numpydoc/* 39 | License: 2-clause BSD 40 | For details, see doc/sphinxext/LICENSE.txt 41 | 42 | Name: scipy-sphinx-theme 43 | Files: doc/scipy-sphinx-theme/* 44 | License: 3-clause BSD, PSF and Apache 2.0 45 | For details, see doc/scipy-sphinx-theme/LICENSE.txt 46 | 47 | Name: lapack-lite 48 | Files: numpy/linalg/lapack_lite/* 49 | License: 3-clause BSD 50 | For details, see numpy/linalg/lapack_lite/LICENSE.txt 51 | 52 | Name: tempita 53 | Files: tools/npy_tempita/* 54 | License: BSD derived 55 | For details, see tools/npy_tempita/license.txt 56 | 57 | Name: dragon4 58 | Files: numpy/core/src/multiarray/dragon4.c 59 | License: One of a kind 60 | For license text, see numpy/core/src/multiarray/dragon4.c 61 | -------------------------------------------------------------------------------- /NOTICE/LICENSE.SciPy: -------------------------------------------------------------------------------- 1 | Copyright (c) 2001, 2002 Enthought, Inc. 2 | All rights reserved. 3 | 4 | Copyright (c) 2003-2019 SciPy Developers. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | a. Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | b. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | c. Neither the name of Enthought nor the names of the SciPy Developers 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS 24 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 25 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 30 | THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | 33 | 34 | SciPy bundles a number of libraries that are compatibly licensed. We list 35 | these here. 36 | 37 | Name: Numpydoc 38 | Files: doc/sphinxext/numpydoc/* 39 | License: 2-clause BSD 40 | For details, see doc/sphinxext/LICENSE.txt 41 | 42 | Name: scipy-sphinx-theme 43 | Files: doc/scipy-sphinx-theme/* 44 | License: 3-clause BSD, PSF and Apache 2.0 45 | For details, see doc/sphinxext/LICENSE.txt 46 | 47 | Name: Six 48 | Files: scipy/_lib/six.py 49 | License: MIT 50 | For details, see the header inside scipy/_lib/six.py 51 | 52 | Name: Decorator 53 | Files: scipy/_lib/decorator.py 54 | License: 2-clause BSD 55 | For details, see the header inside scipy/_lib/decorator.py 56 | 57 | Name: ID 58 | Files: scipy/linalg/src/id_dist/* 59 | License: 3-clause BSD 60 | For details, see scipy/linalg/src/id_dist/doc/doc.tex 61 | 62 | Name: L-BFGS-B 63 | Files: scipy/optimize/lbfgsb/* 64 | License: BSD license 65 | For details, see scipy/optimize/lbfgsb/README 66 | 67 | Name: SuperLU 68 | Files: scipy/sparse/linalg/dsolve/SuperLU/* 69 | License: 3-clause BSD 70 | For details, see scipy/sparse/linalg/dsolve/SuperLU/License.txt 71 | 72 | Name: ARPACK 73 | Files: scipy/sparse/linalg/eigen/arpack/ARPACK/* 74 | License: 3-clause BSD 75 | For details, see scipy/sparse/linalg/eigen/arpack/ARPACK/COPYING 76 | 77 | Name: Qhull 78 | Files: scipy/spatial/qhull/* 79 | License: Qhull license (BSD-like) 80 | For details, see scipy/spatial/qhull/COPYING.txt 81 | 82 | Name: Cephes 83 | Files: scipy/special/cephes/* 84 | License: 3-clause BSD 85 | Distributed under 3-clause BSD license with permission from the author, 86 | see https://lists.debian.org/debian-legal/2004/12/msg00295.html 87 | 88 | Cephes Math Library Release 2.8: June, 2000 89 | Copyright 1984, 1995, 2000 by Stephen L. Moshier 90 | 91 | This software is derived from the Cephes Math Library and is 92 | incorporated herein by permission of the author. 93 | 94 | All rights reserved. 95 | 96 | Redistribution and use in source and binary forms, with or without 97 | modification, are permitted provided that the following conditions are met: 98 | * Redistributions of source code must retain the above copyright 99 | notice, this list of conditions and the following disclaimer. 100 | * Redistributions in binary form must reproduce the above copyright 101 | notice, this list of conditions and the following disclaimer in the 102 | documentation and/or other materials provided with the distribution. 103 | * Neither the name of the nor the 104 | names of its contributors may be used to endorse or promote products 105 | derived from this software without specific prior written permission. 106 | 107 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 108 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 109 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 110 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 111 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 112 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 113 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 114 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 115 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 116 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 117 | 118 | Name: Faddeeva 119 | Files: scipy/special/Faddeeva.* 120 | License: MIT 121 | Copyright (c) 2012 Massachusetts Institute of Technology 122 | 123 | Permission is hereby granted, free of charge, to any person obtaining 124 | a copy of this software and associated documentation files (the 125 | "Software"), to deal in the Software without restriction, including 126 | without limitation the rights to use, copy, modify, merge, publish, 127 | distribute, sublicense, and/or sell copies of the Software, and to 128 | permit persons to whom the Software is furnished to do so, subject to 129 | the following conditions: 130 | 131 | The above copyright notice and this permission notice shall be 132 | included in all copies or substantial portions of the Software. 133 | 134 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 135 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 136 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 137 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 138 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 139 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 140 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 141 | 142 | Name: qd 143 | Files: scipy/special/cephes/dd_*.[ch] 144 | License: modified BSD license ("BSD-LBNL-License.doc") 145 | This work was supported by the Director, Office of Science, Division 146 | of Mathematical, Information, and Computational Sciences of the 147 | U.S. Department of Energy under contract numbers DE-AC03-76SF00098 and 148 | DE-AC02-05CH11231. 149 | 150 | Copyright (c) 2003-2009, The Regents of the University of California, 151 | through Lawrence Berkeley National Laboratory (subject to receipt of 152 | any required approvals from U.S. Dept. of Energy) All rights reserved. 153 | 154 | 1. Redistribution and use in source and binary forms, with or 155 | without modification, are permitted provided that the following 156 | conditions are met: 157 | 158 | (1) Redistributions of source code must retain the copyright 159 | notice, this list of conditions and the following disclaimer. 160 | 161 | (2) Redistributions in binary form must reproduce the copyright 162 | notice, this list of conditions and the following disclaimer in 163 | the documentation and/or other materials provided with the 164 | distribution. 165 | 166 | (3) Neither the name of the University of California, Lawrence 167 | Berkeley National Laboratory, U.S. Dept. of Energy nor the names 168 | of its contributors may be used to endorse or promote products 169 | derived from this software without specific prior written 170 | permission. 171 | 172 | 2. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 173 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 174 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 175 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 176 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 177 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 178 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 179 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 180 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 181 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 182 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 183 | 184 | 3. You are under no obligation whatsoever to provide any bug fixes, 185 | patches, or upgrades to the features, functionality or performance of 186 | the source code ("Enhancements") to anyone; however, if you choose to 187 | make your Enhancements available either publicly, or directly to 188 | Lawrence Berkeley National Laboratory, without imposing a separate 189 | written license agreement for such Enhancements, then you hereby grant 190 | the following license: a non-exclusive, royalty-free perpetual license 191 | to install, use, modify, prepare derivative works, incorporate into 192 | other computer software, distribute, and sublicense such enhancements 193 | or derivative works thereof, in binary and source code form. 194 | -------------------------------------------------------------------------------- /NOTICE/README: -------------------------------------------------------------------------------- 1 | This directory contains licenses of Sella's software dependencies. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/zadorlab/sella) 2 | 3 | # Sella 4 | 5 | Sella is a utility for finding first order saddle points 6 | 7 | An example script 8 | ```python 9 | #!/usr/bin/env python3 10 | 11 | from ase.build import fcc111, add_adsorbate 12 | from ase.calculators.emt import EMT 13 | 14 | from sella import Sella, Constraints 15 | 16 | # Set up your system as an ASE atoms object 17 | slab = fcc111('Cu', (5, 5, 6), vacuum=7.5) 18 | add_adsorbate(slab, 'Cu', 2.0, 'bridge') 19 | 20 | # Optionally, create and populate a Constraints object. 21 | cons = Constraints(slab) 22 | for atom in slab: 23 | if atom.position[2] < slab.cell[2, 2] / 2.: 24 | cons.fix_translation(atom.index) 25 | 26 | # Set up your calculator 27 | slab.calc = EMT() 28 | 29 | # Set up a Sella Dynamics object 30 | dyn = Sella( 31 | slab, 32 | constraints=cons, 33 | trajectory='test_emt.traj', 34 | ) 35 | 36 | dyn.run(1e-3, 1000) 37 | ``` 38 | 39 | If you are using Sella or you wish to use Sella, let me know! 40 | 41 | ## Documentation 42 | 43 | For more information on how to use Sella, please check the [wiki](https://github.com/zadorlab/sella/wiki). 44 | 45 | ## Support 46 | 47 | If you need help using Sella, please visit our [gitter support channel](https://gitter.im/zadorlab/sella), 48 | or open a GitHub issue. 49 | 50 | ## How to cite 51 | 52 | If you use our code in publications, please cite the revelant work(s). (1) is recommended when Sella is used for solids or in heterogeneous catalysis, (3) is recommended for molecular systems. 53 | 54 | 1. Hermes, E., Sargsyan, K., Najm, H. N., Zádor, J.: Accelerated saddle point refinement through full exploitation of partial Hessian diagonalization. Journal of Chemical Theory and Computation, 2019 15 6536-6549. https://pubs.acs.org/doi/full/10.1021/acs.jctc.9b00869 55 | 2. Hermes, E. D., Sagsyan, K., Najm, H. N., Zádor, J.: A geodesic approach to internal coordinate optimization. The Journal of Chemical Physics, 2021 155 094105. https://aip.scitation.org/doi/10.1063/5.0060146 56 | 3. Hermes, E. D., Sagsyan, K., Najm, H. N., Zádor, J.: Sella, an open-source automation-friendly molecular saddle point optimizer. Journal of Chemical Theory and Computation, 2022 18 6974–6988. https://pubs.acs.org/doi/10.1021/acs.jctc.2c00395 57 | 58 | ## Acknowledgments 59 | 60 | This work was supported by the U.S. Department of Energy, Office of Science, Basic Energy Sciences, Chemical Sciences, Geosciences and Biosciences Division, as part of the Computational Chemistry Sciences Program. 61 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: sella 2 | channels: 3 | - defaults 4 | - conda-forge 5 | dependencies: 6 | - python >= 3.6 7 | - scipy >=1.1.0 8 | - cython 9 | - ase 10 | - jax >= 0.2.3 11 | - jaxlib >= 0.1.56 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 54.2.0", 4 | "setuptools_scm >= 2.0.0, <3", 5 | "cython >= 0.29.23", 6 | "numpy", 7 | "scipy" 8 | ] 9 | 10 | build-backend = "setuptools.build_meta" 11 | 12 | [tool.cibuildwheel] 13 | skip = [ 14 | # skip pypy 15 | "pp*", 16 | # skip 32-bit archs 17 | "*-win32", 18 | "*-manylinux_i686", 19 | # skip musllinux 20 | "*-musllinux*", 21 | ] 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.14.0 2 | scipy>=1.1.0 3 | ase>=3.18.0 4 | jax>=0.4.20 5 | jaxlib>=0.4.20 6 | -------------------------------------------------------------------------------- /sella/__init__.py: -------------------------------------------------------------------------------- 1 | import jax 2 | 3 | from .optimize import IRC, Sella 4 | from .internal import Internals, Constraints 5 | 6 | jax.config.update("jax_enable_x64", True) 7 | 8 | __all__ = ['IRC', 'Sella'] 9 | -------------------------------------------------------------------------------- /sella/eigensolvers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from scipy.linalg import eigh, solve 4 | 5 | from sella.utilities.math import modified_gram_schmidt 6 | from .hessian_update import symmetrize_Y 7 | 8 | 9 | def exact(A, gamma=None, P=None): 10 | if isinstance(A, np.ndarray): 11 | lams, vecs = eigh(A) 12 | else: 13 | n, _ = A.shape 14 | if P is None: 15 | P = np.eye(n) 16 | vecs_P = np.eye(n) 17 | else: 18 | _, vecs_P, _ = exact(P) 19 | 20 | # Construct numerical version of A in case it is a LinearOperator. 21 | # This should be more or less exact if A is a numpy array already. 22 | B = np.zeros((n, n)) 23 | for i in range(n): 24 | v = vecs_P[i] 25 | B += np.outer(v, A.dot(v)) 26 | B = 0.5 * (B + B.T) 27 | lams, vecs = eigh(B) 28 | return lams, vecs, lams[np.newaxis, :] * vecs 29 | 30 | 31 | def rayleigh_ritz(A, gamma, P, B=None, v0=None, vref=None, vreftol=0.99, 32 | method='jd0', maxiter=None): 33 | n, _ = A.shape 34 | 35 | if B is None: 36 | B = np.eye(n) 37 | 38 | if maxiter is None: 39 | maxiter = 2 * n + 1 40 | 41 | if gamma <= 0: 42 | return exact(A, gamma, P) 43 | 44 | if v0 is not None: 45 | V = modified_gram_schmidt(v0.reshape((-1, 1))) 46 | else: 47 | P_lams, P_vecs, _ = exact(P, 0) 48 | nneg = max(1, np.sum(P_lams < 0)) 49 | V = modified_gram_schmidt(P_vecs[:, :nneg]) 50 | v0 = V[:, 0] 51 | 52 | AV = A.dot(V) 53 | 54 | symm = 2 55 | seeking = 0 56 | while True: 57 | Atilde = V.T @ (symmetrize_Y(V, AV, symm=symm)) 58 | lams, vecs = eigh(Atilde, V.T @ B @ V) 59 | nneg = max(1, np.sum(lams < 0)) 60 | # Rotate our subspace V to be diagonal in A. 61 | # This is not strictly necessary but it makes our lives easier later 62 | AV = AV @ vecs 63 | V = V @ vecs 64 | vecs = np.eye(V.shape[1]) 65 | if V.shape[1] >= maxiter: 66 | return lams, V, AV 67 | 68 | Ytilde = symmetrize_Y(V, AV, symm=symm) 69 | R = (Ytilde @ vecs[:, :nneg] 70 | - B @ V @ vecs[:, :nneg] * lams[np.newaxis, :nneg]) 71 | Rnorm = np.linalg.norm(R, axis=0) 72 | 73 | # a hack for the optbench.org eigensolver convergence test 74 | if vref is not None: 75 | x0 = V @ vecs[:, 0] 76 | print(np.abs(x0 @ vref)) 77 | if np.abs(x0 @ vref) > vreftol: 78 | print("Dot product between your v0 and the final answer:", 79 | np.abs(v0 @ x0) / np.linalg.norm(v0)) 80 | return lams, V, AV 81 | 82 | # Loop over all Ritz values of interest 83 | for seeking, (rinorm, thetai) in enumerate(zip(Rnorm, lams)): 84 | # Take the first Ritz value that is not converged, and use it 85 | # to extend V 86 | if V.shape[1] == 1 or rinorm >= gamma * np.abs(thetai): 87 | ri = R[:, seeking] 88 | thetai = lams[seeking] 89 | break 90 | # If they all seem converged, then we are done 91 | else: 92 | return lams, V, AV 93 | 94 | t = expand(V, Ytilde, P, B, lams, vecs, thetai, method, seeking) 95 | t /= np.linalg.norm(t) 96 | if np.linalg.norm(t - V @ V.T @ t) < 1e-2: # pragma: no cover 97 | # Do Lanczos instead 98 | t = ri / np.linalg.norm(ri) 99 | 100 | t = modified_gram_schmidt(t[:, np.newaxis], V) 101 | 102 | # Davidson failed to find a new search direction 103 | if t.shape[1] == 0: # pragma: no cover 104 | # Do Lanczos instead 105 | for rj in R.T: 106 | t = modified_gram_schmidt(rj[:, np.newaxis], V) 107 | if t.shape[1] == 1: 108 | break 109 | else: 110 | t = modified_gram_schmidt(np.random.normal(size=(n, 1)), V) 111 | if t.shape[1] == 0: 112 | return lams, V, AV 113 | 114 | V = np.hstack([V, t]) 115 | AV = np.hstack([AV, A.dot(t)]) 116 | 117 | 118 | def expand(V, Y, P, B, lams, vecs, shift, method='jd0', seeking=0): 119 | d, n = V.shape 120 | R = Y @ vecs - B @ V @ vecs * lams[np.newaxis, :] 121 | Pshift = P - shift * B 122 | if method == 'lanczos': 123 | return R[:, seeking] 124 | elif method == 'gd': 125 | return np.linalg.solve(Pshift, R[:, seeking]) 126 | elif method == 'jd0_alt': 127 | vi = V @ vecs[:, seeking] 128 | Pprojr = solve(Pshift, R[:, seeking]) 129 | Pprojv = solve(Pshift, vi) 130 | alpha = vi.T @ Pprojr / (vi.T @ Pprojv) 131 | return Pprojv * alpha - Pprojr 132 | elif method == 'jd0': 133 | vi = V @ vecs[:, seeking] 134 | Aaug = np.block([[Pshift, vi[:, np.newaxis]], [vi, 0]]) 135 | raug = np.zeros(d + 1) 136 | raug[:d] = R[:, seeking] 137 | z = solve(Aaug, -raug) 138 | return z[:d] 139 | elif method == 'mjd0_alt': 140 | Pprojr = solve(Pshift, R[:, seeking]) 141 | PprojV = solve(Pshift, V @ vecs) 142 | alpha = solve((V @ vecs).T @ PprojV, (V @ vecs).T @ Pprojr) 143 | return solve(Pshift, ((V @ vecs) @ alpha - R[:, seeking])) 144 | elif method == 'mjd0': 145 | Vrot = V @ vecs 146 | Aaug = np.block([[Pshift, Vrot], [Vrot.T, np.zeros((n, n))]]) 147 | raug = np.zeros(d + n) 148 | raug[:d] = R[:, seeking] 149 | z = solve(Aaug, -raug) 150 | return z[:d] 151 | else: # pragma: no cover 152 | raise ValueError("Unknown diagonalization method {}".format(method)) 153 | -------------------------------------------------------------------------------- /sella/force_match.pyx: -------------------------------------------------------------------------------- 1 | from cython.view cimport array as cvarray 2 | 3 | cimport numpy as np 4 | from scipy.linalg.cython_blas cimport dcopy, dgemv 5 | from scipy.linalg.cython_lapack cimport dgels 6 | 7 | from libc.stdlib cimport malloc, free 8 | from libc.math cimport sqrt, exp 9 | from libc.string cimport memset 10 | 11 | import numpy as np 12 | from ase.data import covalent_radii 13 | from scipy.optimize import minimize, brute 14 | 15 | # We are fitting an approximate force field to a given gradient in order to predict 16 | # the initial Hessian matrix for saddle point optimization. The fitted parameters of 17 | # this force field are categorized as being linear or nonlinear. Only the nonlinear 18 | # parameters are explicitly optimized using scipy.optimize -- the linear parameters 19 | # are directly solved for at each iteration of the optimizer using a least squares fit. 20 | # 21 | # dFlin is the gradient of the atomic forces with respect to the LINEAR parameters. 22 | # It is used in the least squares fit. 23 | # 24 | # dFlin[NDOF x NLINEAR] 25 | # 26 | # dFnonlin is the second derivative of the atomic forces with respect to the LINEAR parameters 27 | # *and* the NONLINEAR parameters. Once we know the linear parameters from the least squares 28 | # fit, we calculate from dFnonlin the first order derivative of the atomic forces with respect 29 | # to the NONLINEAR parameters only. This is what goes into the gradient of our cost function. 30 | # 31 | # dFnonlin[NDOF x NNONLINEAR x NLINEAR] 32 | 33 | cdef int ONE = 1 34 | cdef int THREE = 3 35 | cdef double D_ZERO = 0. 36 | cdef double D_ONE = 1. 37 | cdef double D_TWO = 2. 38 | 39 | cdef double[:, :, :] dFlin 40 | cdef double[:, :, :, :] dFnonlin 41 | 42 | cdef struct s_pair_interactions: 43 | int nint 44 | int* npairs 45 | int** indices 46 | double** coords 47 | 48 | ctypedef s_pair_interactions pair_interactions 49 | 50 | cdef pair_interactions lj_interactions 51 | cdef pair_interactions buck_interactions 52 | cdef pair_interactions morse_interactions 53 | cdef pair_interactions bond_interactions 54 | 55 | cdef void init_pair_interactions(pair_interactions* pi, dict data): 56 | cdef int i 57 | cdef int j 58 | cdef int k 59 | cdef int npairs 60 | 61 | cdef int nint = len(data) 62 | 63 | pi[0].nint = nint 64 | pi[0].npairs = malloc(sizeof(int*) * nint) 65 | pi[0].indices = malloc(sizeof(int*) * nint) 66 | pi[0].coords = malloc(sizeof(double*) * nint) 67 | 68 | for i, (atom_types, pair_data) in enumerate(data.items()): 69 | npairs = len(pair_data) 70 | pi[0].npairs[i] = npairs 71 | 72 | pi[0].indices[i] = malloc(sizeof(int) * npairs * 2) 73 | pi[0].coords[i] = malloc(sizeof(double) * npairs * 3) 74 | 75 | for j, (indices, coords) in enumerate(pair_data): 76 | for k in range(3): 77 | pi[0].coords[i][3 * j + k] = coords[k] 78 | if k < 2: 79 | pi[0].indices[i][2 * j + k] = indices[k] 80 | 81 | cdef void free_pair_interactions(pair_interactions* pi): 82 | cdef int i 83 | 84 | for i in range(pi[0].nint): 85 | free(pi[0].indices[i]) 86 | free(pi[0].coords[i]) 87 | 88 | free(pi[0].npairs) 89 | free(pi[0].indices) 90 | free(pi[0].coords) 91 | 92 | #cdef struct s_pair_interaction: 93 | # int i 94 | # int j 95 | # double[:, :] xij 96 | # 97 | #ctypedef s_pair_interaction pair_interaction 98 | 99 | def force_match(atoms, types=['buck', 'bond']): 100 | cdef int i 101 | cdef int j 102 | cdef int k 103 | cdef int a 104 | cdef int natoms = len(atoms) 105 | cdef int ndof = 3 * natoms 106 | cdef int nlin = 0 107 | cdef int nnonlin = 0 108 | cdef int nbuck = 0 109 | cdef int nbond = 0 110 | cdef int nlj = 0 111 | cdef int nmorse = 0 112 | cdef int info 113 | cdef bint do_lj 114 | cdef bint do_buck 115 | cdef bint do_morse 116 | cdef bint do_bond 117 | 118 | global dFlin 119 | global dFnonlin 120 | global lj_interactions 121 | global buck_interactions 122 | global bond_interactions 123 | 124 | enumbers = atoms.get_atomic_numbers() 125 | rmax = np.max(atoms.get_all_distances()) 126 | rmin = np.min(atoms.get_all_distances() + rmax * np.eye(natoms)) 127 | rcut = 3 * rmin 128 | #rcut = 9.5 129 | pos = atoms.get_positions() 130 | 131 | 132 | if np.any(atoms.pbc): 133 | cell = atoms.get_cell() 134 | latt_len = np.sqrt((cell**2).sum(1)) 135 | V = abs(np.linalg.det(cell)) 136 | n = atoms.pbc * np.array(np.ceil(rcut * np.prod(latt_len) / 137 | (V * latt_len)), dtype=int) 138 | tvecs = [] 139 | for i in range(-n[0], n[0] + 1): 140 | latt_a = i * cell[0] 141 | for j in range(-n[1], n[1] + 1): 142 | latt_ab = latt_a + j * cell[1] 143 | for k in range(-n[2], n[2] + 1): 144 | tvecs.append(latt_ab + k * cell[2]) 145 | tvecs = np.array(tvecs) 146 | else: 147 | tvecs = np.array([[0., 0., 0.]]) 148 | 149 | cdef double[:] rij = cvarray(shape=(3,), itemsize=sizeof(double), format='d') 150 | cdef double[:, :] pos_mv = memoryview(pos) 151 | cdef double[:, :] tvecs_mv = memoryview(tvecs) 152 | cdef int nt = len(tvecs) 153 | cdef int ti 154 | cdef double dij2 155 | cdef double dij 156 | cdef double rcut2 = rcut * rcut 157 | 158 | rij_t = np.zeros(3, dtype=np.float64) 159 | cdef double[:] rij_t_mv = memoryview(rij_t) 160 | 161 | do_lj = 'lj' in types 162 | do_buck = 'buck' in types 163 | do_morse = 'morse' in types 164 | do_bond = 'bond' in types 165 | 166 | vdw_data = {} 167 | cov_data = {} 168 | ff_data = {'lj': {}, 169 | 'buck': {}, 170 | 'morse': {}, 171 | 'bond': {}, 172 | } 173 | x0 = [] 174 | brute_range = [] 175 | for i in range(natoms): 176 | for j in range(i, natoms): 177 | for k in range(3): 178 | rij[k] = pos_mv[j, k] - pos_mv[i, k] 179 | for ti in range(nt): 180 | if (i == j) and tvecs_mv[ti, 0] == 0. and tvecs_mv[ti, 1] == 0. and tvecs_mv[ti, 2] == 0.: 181 | continue 182 | dij2 = 0. 183 | for k in range(3): 184 | rij_t_mv[k] = rij[k] + tvecs_mv[ti, k] 185 | dij2 += rij_t_mv[k] * rij_t_mv[k] 186 | if dij2 > rcut2: 187 | continue 188 | dij = sqrt(dij2) 189 | eij = tuple(sorted(enumbers[[i, j]])) 190 | 191 | if do_lj: 192 | vdw_x = ff_data['lj'].get(eij) 193 | if vdw_x is None: 194 | vdw_x = [] 195 | ff_data['lj'][eij] = vdw_x 196 | nlj += 1 197 | vdw_x.append(((i, j), rij_t.copy())) 198 | 199 | if do_buck: 200 | vdw_x = ff_data['buck'].get(eij) 201 | if vdw_x is None: 202 | vdw_x = [] 203 | ff_data['buck'][eij] = vdw_x 204 | nbuck += 1 205 | x0.append(2.5) 206 | brute_range.append((0.1, 10.0)) 207 | vdw_x.append(((i, j), rij_t.copy())) 208 | 209 | if do_morse: 210 | vdw_x = ff_data['morse'].get(eij) 211 | if vdw_x is None: 212 | vdw_x = [] 213 | ff_data['morse'][eij] = vdw_x 214 | nmorse += 1 215 | x0.append(2.5) 216 | brute_range.append((0.1, 10.0)) 217 | vdw_x.append(((i, j), rij_t.copy())) 218 | 219 | if do_bond: 220 | rcov = np.sum(covalent_radii[list(eij)]) 221 | if dij > 1.5 * rcov: 222 | continue 223 | cov_x = ff_data['bond'].get(eij) 224 | if cov_x is None: 225 | cov_x = [] 226 | ff_data['bond'][eij] = cov_x 227 | nbond += 1 228 | x0.append(rcov) 229 | brute_range.append((0.5 * rcov, 2 * rcov)) 230 | cov_x.append(((i, j), rij_t.copy())) 231 | 232 | nlin = 2 * nlj + 2 * nbuck + 2 * nmorse + nbond 233 | nnonlin = nbuck + nmorse + nbond 234 | 235 | init_pair_interactions(&lj_interactions, ff_data['lj']) 236 | init_pair_interactions(&buck_interactions, ff_data['buck']) 237 | init_pair_interactions(&morse_interactions, ff_data['morse']) 238 | init_pair_interactions(&bond_interactions, ff_data['bond']) 239 | 240 | dFlin = cvarray(shape=(natoms, 3, nlin), itemsize=sizeof(double), format='d') 241 | dFnonlin = cvarray(shape=(natoms, 3, nnonlin, nlin), itemsize=sizeof(double), format='d') 242 | 243 | memset(&dFlin[0, 0, 0], 0, sizeof(double) * ndof * nlin) 244 | memset(&dFnonlin[0, 0, 0, 0], 0, sizeof(double) * ndof * nnonlin * nlin) 245 | 246 | constraints = [] 247 | if atoms.constraints: 248 | constraints = atoms.constraints 249 | atoms.constraints = [] 250 | ftrue = atoms.get_forces() 251 | atoms.constraints = constraints 252 | 253 | bounds = [] 254 | bounds += [(0., None)] * nbuck 255 | bounds += [(1., None)] * nmorse 256 | bounds += [(0., None)] * nbond 257 | 258 | if nnonlin < 5: 259 | x0 = brute(objective, brute_range, args=(ndof, nlin, natoms, ftrue), Ns=10, disp=True) 260 | else: 261 | x0 = np.array(x0, dtype=np.float64) 262 | 263 | print(objective(x0, ndof, nlin, natoms, ftrue)) 264 | 265 | res = minimize(objective, x0, method='L-BFGS-B', jac=True, options={'gtol': 1e-10, 'ftol': 1e-8}, 266 | args=(ndof, nlin, natoms, ftrue, True), bounds=bounds) 267 | print(res) 268 | nonlinpars = res['x'] 269 | 270 | linpars = objective(nonlinpars, ndof, nlin, natoms, ftrue, False, True) 271 | print(linpars, nonlinpars) 272 | 273 | hess = calc_hess(linpars, nonlinpars, natoms) 274 | 275 | free_pair_interactions(&lj_interactions) 276 | free_pair_interactions(&buck_interactions) 277 | free_pair_interactions(&morse_interactions) 278 | free_pair_interactions(&bond_interactions) 279 | 280 | return hess 281 | 282 | def objective(pars, int ndof, int nlin, int natoms, np.ndarray[np.float_t, ndim=2] ftrue, bint grad=False, bint ret_linpars=False): 283 | global dFlin 284 | global dFnonlin 285 | global lj_interactions 286 | global buck_interactions 287 | global morse_interactions 288 | global bond_interactions 289 | 290 | cdef int i 291 | cdef int j 292 | cdef int k 293 | cdef int l 294 | cdef int a 295 | cdef int b 296 | cdef int linstart = 0 297 | cdef int nonlinstart = 0 298 | cdef int nnonlin = len(pars) 299 | cdef int info 300 | cdef int Asize 301 | cdef int nonlin_size 302 | 303 | cdef double chisq 304 | 305 | cdef double[:] pars_c = memoryview(pars) 306 | 307 | cdef double rho 308 | cdef double r0 309 | 310 | xij_np = np.zeros(3, dtype=np.float64) 311 | cdef double* xij = np.PyArray_DATA(xij_np) 312 | 313 | memset(&dFlin[0, 0, 0], 0, sizeof(double) * ndof * nlin) 314 | memset(&dFnonlin[0, 0, 0, 0], 0, sizeof(double) * ndof * nnonlin * nlin) 315 | 316 | for j in range(lj_interactions.nint): 317 | for k in range(lj_interactions.npairs[j]): 318 | a = lj_interactions.indices[j][2 * k] 319 | b = lj_interactions.indices[j][2 * k + 1] 320 | dcopy(&THREE, &lj_interactions.coords[j][3 * k], &ONE, xij, &ONE) 321 | lj(a, b, linstart, xij) 322 | linstart += 2 323 | 324 | for j in range(buck_interactions.nint): 325 | for k in range(buck_interactions.npairs[j]): 326 | rho = pars[nonlinstart] 327 | a = buck_interactions.indices[j][2 * k] 328 | b = buck_interactions.indices[j][2 * k + 1] 329 | dcopy(&THREE, &buck_interactions.coords[j][3 * k], &ONE, xij, &ONE) 330 | buck(a, b, linstart, nonlinstart, xij, rho) 331 | linstart += 2 332 | nonlinstart += 1 333 | 334 | for j in range(morse_interactions.nint): 335 | for k in range(morse_interactions.npairs[j]): 336 | rho = pars[nonlinstart] 337 | a = morse_interactions.indices[j][2 * k] 338 | b = morse_interactions.indices[j][2 * k + 1] 339 | dcopy(&THREE, &morse_interactions.coords[j][3 * k], &ONE, xij, &ONE) 340 | morse(a, b, linstart, nonlinstart, xij, rho) 341 | linstart += 2 342 | nonlinstart += 1 343 | 344 | for j in range(bond_interactions.nint): 345 | for k in range(bond_interactions.npairs[j]): 346 | r0 = pars[nonlinstart] 347 | a = bond_interactions.indices[j][2 * k] 348 | b = bond_interactions.indices[j][2 * k + 1] 349 | dcopy(&THREE, &bond_interactions.coords[j][3 * k], &ONE, xij, &ONE) 350 | bond(a, b, linstart, nonlinstart, xij, r0) 351 | linstart += 1 352 | nonlinstart += 1 353 | 354 | linpars, _, _, _ = np.linalg.lstsq(np.asarray(dFlin).reshape((ndof, nlin)), ftrue.ravel(), rcond=None) 355 | fapprox = np.einsum('ijk,k', dFlin, linpars) 356 | df = fapprox - ftrue 357 | chisq = np.sum(df * df) 358 | 359 | if ret_linpars: 360 | return linpars 361 | 362 | if not grad: 363 | return chisq 364 | 365 | dchisq = 2 * np.einsum('ij,ijkl,l', df, dFnonlin, linpars) 366 | 367 | return chisq, dchisq 368 | 369 | def calc_hess(linpars, nonlinpars, natoms): 370 | global lj_interactions 371 | global buck_interactions 372 | global morse_interactions 373 | global bond_interactions 374 | 375 | cdef int i 376 | cdef int j 377 | cdef int k 378 | cdef int l 379 | cdef int a 380 | cdef int b 381 | cdef int linstart = 0 382 | cdef int nonlinstart = 0 383 | cdef int nlin = len(linpars) 384 | cdef int nnonlin = len(nonlinpars) 385 | 386 | cdef double C6 387 | cdef double C12 388 | cdef double A 389 | cdef double B 390 | cdef double D 391 | cdef double K 392 | cdef double r0 393 | cdef double rho 394 | 395 | xij_np = np.zeros(3, dtype=np.float64) 396 | cdef double* xij = np.PyArray_DATA(xij_np) 397 | 398 | hess_np = np.zeros((natoms, 3, natoms, 3)) 399 | cdef double[:, :, :, :] hess = memoryview(hess_np) 400 | 401 | for i in range(lj_interactions.nint): 402 | for j in range(lj_interactions.npairs[i]): 403 | C6 = linpars[linstart] 404 | C12 = linpars[linstart + 1] 405 | a = lj_interactions.indices[i][2 * j] 406 | b = lj_interactions.indices[i][2 * j + 1] 407 | dcopy(&THREE, &lj_interactions.coords[i][3 * j], &ONE, xij, &ONE) 408 | lj_hess(a, b, C6, C12, xij, hess) 409 | linstart += 2 410 | 411 | for i in range(buck_interactions.nint): 412 | for j in range(buck_interactions.npairs[i]): 413 | A = linpars[linstart] 414 | C6 = linpars[linstart + 1] 415 | rho = nonlinpars[nonlinstart] 416 | 417 | a = buck_interactions.indices[i][2 * j] 418 | b = buck_interactions.indices[i][2 * j + 1] 419 | dcopy(&THREE, &buck_interactions.coords[i][3 * j], &ONE, xij, &ONE) 420 | buck_hess(a, b, A, C6, rho, xij, hess) 421 | linstart += 2 422 | nonlinstart += 1 423 | 424 | for i in range(morse_interactions.nint): 425 | for j in range(morse_interactions.npairs[i]): 426 | A = linpars[linstart] 427 | B = linpars[linstart + 1] 428 | rho = nonlinpars[nonlinstart] 429 | 430 | a = morse_interactions.indices[i][2 * j] 431 | b = morse_interactions.indices[i][2 * j + 1] 432 | dcopy(&THREE, &morse_interactions.coords[i][3 * j], &ONE, xij, &ONE) 433 | morse_hess(a, b, A, B, rho, xij, hess) 434 | linstart += 2 435 | nonlinstart += 1 436 | 437 | for i in range(bond_interactions.nint): 438 | for j in range(bond_interactions.npairs[i]): 439 | K = linpars[linstart] 440 | r0 = nonlinpars[nonlinstart] 441 | 442 | a = bond_interactions.indices[i][2 * j] 443 | b = bond_interactions.indices[i][2 * j + 1] 444 | dcopy(&THREE, &bond_interactions.coords[i][3 * j], &ONE, xij, &ONE) 445 | bond_hess(a, b, K, r0, xij, hess) 446 | linstart += 1 447 | nonlinstart += 1 448 | 449 | return hess_np.reshape((3 * natoms, 3 * natoms)) 450 | 451 | cdef void update_hess(int i, int j, double* xij, double[:, :, :, :] hess, double diag, double rest) nogil: 452 | cdef int k 453 | cdef int a 454 | cdef double hessterm 455 | 456 | for k in range(3): 457 | hessterm = diag + rest * xij[k] * xij[k] 458 | hess[i, k, i, k] += hessterm 459 | hess[i, k, j, k] -= hessterm 460 | 461 | hess[j, k, i, k] -= hessterm 462 | hess[j, k, j, k] += hessterm 463 | for a in range(k + 1, 3): 464 | hessterm = rest * xij[k] * xij[a] 465 | hess[i, k, i, a] += hessterm 466 | hess[i, k, j, a] -= hessterm 467 | hess[j, k, i, a] -= hessterm 468 | hess[j, k, j, a] += hessterm 469 | 470 | hess[i, a, i, k] += hessterm 471 | hess[i, a, j, k] -= hessterm 472 | hess[j, a, i, k] -= hessterm 473 | hess[j, a, j, k] += hessterm 474 | 475 | 476 | cdef void lj(int i, int j, int linstart, double* xij) nogil: 477 | cdef double dij 478 | cdef double dij2 479 | cdef double dij8 480 | cdef double dij14 481 | cdef double f6 482 | cdef double f12 483 | cdef int k 484 | 485 | global dFlin 486 | 487 | dij2 = 0. 488 | for k in range(3): 489 | dij2 += xij[k] * xij[k] 490 | 491 | dij = sqrt(dij2) 492 | dij8 = dij2 * dij2 * dij2 * dij2 493 | dij14 = dij8 * dij2 * dij2 * dij2 494 | 495 | f6 = 6 / dij8 496 | f12 = -12 / dij14 497 | 498 | for k in range(3): 499 | dFlin[i, k, linstart] += f6 * xij[k] 500 | dFlin[i, k, linstart + 1] += f12 * xij[k] 501 | 502 | for k in range(3): 503 | dFlin[j, k, linstart] += -f6 * xij[k] 504 | dFlin[j, k, linstart + 1] += -f12 * xij[k] 505 | 506 | cdef void lj_hess(int i, int j, double C6, double C12, double* xij, double[:, :, :, :] hess) nogil: 507 | cdef double dij2 508 | cdef double dij8 509 | cdef double dij14 510 | cdef double dij10 511 | cdef double dij16 512 | cdef double diag 513 | cdef double rest 514 | cdef int k 515 | cdef int a 516 | 517 | 518 | dij2 = 0. 519 | for k in range(3): 520 | dij2 += xij[k] * xij[k] 521 | 522 | dij8 = dij2 * dij2 * dij2 * dij2 523 | dij10 = dij8 * dij2 524 | dij14 = dij10 * dij2 * dij2 525 | dij16 = dij8 * dij8 526 | 527 | diag = -12 * C12 / dij14 + 6 * C6 / dij8 528 | rest = 168 * C12 / dij16 - 48 * C6 / dij10 529 | 530 | update_hess(i, j, xij, hess, diag, rest) 531 | 532 | cdef void buck(int i, int j, int linstart, int nonlinstart, 533 | double* xij, double B) nogil: 534 | cdef double dij 535 | cdef double dij2 536 | cdef double dij8 537 | cdef double expterm 538 | cdef double fexp 539 | cdef double f6 540 | cdef double dB 541 | cdef int k 542 | 543 | global dFlin 544 | global dFnonlin 545 | 546 | dij2 = 0. 547 | for k in range(3): 548 | dij2 += xij[k] * xij[k] 549 | 550 | dij = sqrt(dij2) 551 | dij8 = dij2 * dij2 * dij2 * dij2 552 | 553 | expterm = exp(-B * dij) 554 | fexp = -B * expterm / dij 555 | dB = B * expterm - expterm / dij 556 | 557 | f6 = 6. / dij8 558 | 559 | for k in range(3): 560 | dFlin[i, k, linstart] += fexp * xij[k] 561 | dFlin[i, k, linstart + 1] += f6 * xij[k] 562 | 563 | dFlin[j, k, linstart] -= fexp * xij[k] 564 | dFlin[j, k, linstart + 1] -= f6 * xij[k] 565 | 566 | dFnonlin[i, k, nonlinstart, linstart] += dB * xij[k] 567 | dFnonlin[j, k, nonlinstart, linstart] -= dB * xij[k] 568 | 569 | cdef void buck_hess(int i, int j, double A, double C6, double B, double* xij, double[:, :, :, :] hess) nogil: 570 | cdef double dij 571 | cdef double dij2 572 | cdef double dij3 573 | cdef double dij8 574 | cdef double dij10 575 | cdef double expterm 576 | cdef double diag 577 | cdef double rest 578 | 579 | cdef int k 580 | 581 | dij2 = 0. 582 | for k in range(3): 583 | dij2 += xij[k] * xij[k] 584 | dij = sqrt(dij2) 585 | dij3 = dij * dij2 586 | 587 | dij8 = dij2 * dij2 * dij2 * dij2 588 | dij10 = dij8 * dij2 589 | 590 | expterm = exp(-B * dij) 591 | 592 | diag = 6 * C6 / dij8 - A * B * expterm / dij 593 | rest = -48 * C6 / dij10 + A * B * expterm / dij3 + A * B * B * expterm / dij2 594 | 595 | update_hess(i, j, xij, hess, diag, rest) 596 | 597 | cdef void morse(int i, int j, int linstart, int nonlinstart, double* xij, double rho) nogil: 598 | cdef double dij 599 | cdef double dij2 600 | cdef double expterm 601 | cdef double expterm2 602 | cdef double f_att 603 | cdef double f_rep 604 | cdef double drho_att 605 | cdef double drho_rep 606 | cdef int k 607 | 608 | global dFlin 609 | global dFnonlin 610 | 611 | dij2 = 0. 612 | for k in range(3): 613 | dij2 += xij[k] * xij[k] 614 | 615 | dij = sqrt(dij2) 616 | 617 | expterm = exp(-rho * dij) 618 | expterm2 = expterm * expterm 619 | f_rep = -2 * rho * expterm2 / dij 620 | f_att = rho * expterm / dij 621 | drho_rep = 2 * expterm2 * (2 * rho * dij - 1) / dij 622 | drho_att = expterm * (1 - rho * dij) / dij 623 | 624 | for k in range(3): 625 | dFlin[i, k, linstart] += f_rep * xij[k] 626 | dFlin[i, k, linstart + 1] += f_att * xij[k] 627 | 628 | dFlin[j, k, linstart] -= f_rep * xij[k] 629 | dFlin[j, k, linstart + 1] -= f_att * xij[k] 630 | 631 | dFnonlin[i, k, nonlinstart, linstart] += drho_rep * xij[k] 632 | dFnonlin[i, k, nonlinstart, linstart + 1] += drho_att * xij[k] 633 | 634 | dFnonlin[j, k, nonlinstart, linstart] -= drho_rep * xij[k] 635 | dFnonlin[j, k, nonlinstart, linstart + 1] -= drho_att * xij[k] 636 | 637 | cdef void morse_hess(int i, int j, double A, double B, double rho, double* xij, double[:, :, :, :] hess) nogil: 638 | cdef double dij 639 | cdef double dij2 640 | cdef double expterm 641 | cdef double expterm2 642 | cdef double diag 643 | cdef double rest 644 | 645 | cdef int k 646 | 647 | dij2 = 0. 648 | for k in range(3): 649 | dij2 += xij[k] * xij[k] 650 | dij = sqrt(dij2) 651 | dij3 = dij2 * dij 652 | 653 | expterm = exp(-rho * dij) 654 | expterm2 = expterm * expterm 655 | 656 | diag = (B * expterm - 2 * A * expterm2) / dij 657 | rest = rho * ((2 * A * expterm2 - B * expterm) / dij + rho * (4 * A * expterm2 - B * expterm)) / dij2 658 | 659 | update_hess(i, j, xij, hess, diag, rest) 660 | 661 | cdef void bond(int i, int j, int linstart, int nonlinstart, double* xij, double r0) nogil: 662 | cdef double dij 663 | cdef double dij2 664 | cdef double fbond 665 | cdef double dr0 666 | cdef int k 667 | 668 | global dFlin 669 | global dFnonlin 670 | 671 | dij2 = 0. 672 | for k in range(3): 673 | dij2 += xij[k] * xij[k] 674 | 675 | dij = sqrt(dij2) 676 | 677 | fbond = 2 * (dij - r0) / dij 678 | dr0 = -2 / dij 679 | 680 | for k in range(3): 681 | dFlin[i, k, linstart] += fbond * xij[k] 682 | dFlin[j, k, linstart] -= fbond * xij[k] 683 | 684 | dFnonlin[i, k, nonlinstart, linstart] += dr0 * xij[k] 685 | dFnonlin[j, k, nonlinstart, linstart] -= dr0 * xij[k] 686 | 687 | cdef void bond_hess(int i, int j, double K, double r0, double* xij, double[:, :, :, :] hess) nogil: 688 | cdef double dij 689 | cdef double dij2 690 | cdef double dij3 691 | cdef double diag 692 | cdef double rest 693 | cdef int a 694 | 695 | dij2 = 0 696 | for a in range(3): 697 | dij2 += xij[a] * xij[a] 698 | dij = sqrt(dij2) 699 | dij3 = dij2 * dij 700 | 701 | diag = 2 * K * (dij - r0) / dij 702 | rest = 2 * K * r0 / dij3 703 | 704 | update_hess(i, j, xij, hess, diag, rest) 705 | -------------------------------------------------------------------------------- /sella/hessian_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | 5 | import numpy as np 6 | 7 | from scipy.linalg import eigh, lstsq, solve 8 | 9 | 10 | def symmetrize_Y2(S, Y): 11 | _, nvecs = S.shape 12 | dY = np.zeros_like(Y) 13 | YTS = Y.T @ S 14 | dYTS = np.zeros_like(YTS) 15 | STS = S.T @ S 16 | for i in range(1, nvecs): 17 | RHS = np.linalg.lstsq(STS[:i, :i], 18 | YTS[i, :i].T - YTS[:i, i] - dYTS[:i, i], 19 | rcond=None)[0] 20 | dY[:, i] = -S[:, :i] @ RHS 21 | dYTS[i, :] = -STS[:, :i] @ RHS 22 | return dY 23 | 24 | 25 | def symmetrize_Y(S, Y, symm): 26 | if symm is None or S.shape[1] == 1: 27 | return Y 28 | elif symm == 0: 29 | return Y + S @ lstsq(S.T @ S, np.tril(S.T @ Y - Y.T @ S, -1).T)[0] 30 | elif symm == 1: 31 | return Y + Y @ lstsq(S.T @ Y, np.tril(S.T @ Y - Y.T @ S, -1).T)[0] 32 | elif symm == 2: 33 | return Y + symmetrize_Y2(S, Y) 34 | else: # pragma: no cover 35 | raise ValueError("Unknown symmetrization method {}".format(symm)) 36 | 37 | 38 | def update_H(B, S, Y, method='TS-BFGS', symm=2, lams=None, vecs=None): 39 | if len(S.shape) == 1: 40 | if np.linalg.norm(S) < 1e-8: 41 | return B 42 | S = S[:, np.newaxis] 43 | if len(Y.shape) == 1: 44 | Y = Y[:, np.newaxis] 45 | 46 | Ytilde = symmetrize_Y(S, Y, symm) 47 | 48 | if B is None: 49 | # Approximate B as a scaled identity matrix, where the 50 | # scalar is the average Ritz value from S.T @ Y 51 | thetas, _ = eigh(S.T @ Ytilde) 52 | lam0 = np.exp(np.average(np.log(np.abs(thetas)))) 53 | d, _ = S.shape 54 | B = lam0 * np.eye(d) 55 | 56 | if lams is None or vecs is None: 57 | lams, vecs = eigh(B) 58 | 59 | if method == 'BFGS_auto': 60 | # Default to TS-BFGS, and only use BFGS if B and S.T @ Y are 61 | # both positive definite 62 | method = 'TS-BFGS' 63 | if np.all(lams > 0): 64 | lams_STY, vecs_STY = eigh(S.T @ Ytilde, S.T @ S) 65 | if np.all(lams_STY > 0): 66 | method = 'BFGS' 67 | 68 | if method == 'BFGS': 69 | Bplus = _MS_BFGS(B, S, Ytilde) 70 | elif method == 'TS-BFGS': 71 | Bplus = _MS_TS_BFGS(B, S, Ytilde, lams, vecs) 72 | elif method == 'PSB': 73 | Bplus = _MS_PSB(B, S, Ytilde) 74 | elif method == 'DFP': 75 | Bplus = _MS_DFP(B, S, Ytilde) 76 | elif method == 'SR1': 77 | Bplus = _MS_SR1(B, S, Ytilde) 78 | elif method == 'Greenstadt': 79 | Bplus = _MS_Greenstadt(B, S, Ytilde) 80 | else: # pragma: no cover 81 | raise ValueError('Unknown update method {}'.format(method)) 82 | 83 | Bplus += B 84 | Bplus -= np.tril(Bplus.T - Bplus, -1).T 85 | 86 | return Bplus 87 | 88 | 89 | def _MS_BFGS(B, S, Y): 90 | return Y @ solve(Y.T @ S, Y.T) - B @ S @ solve(S.T @ B @ S, S.T @ B) 91 | 92 | 93 | def _MS_TS_BFGS(B, S, Y, lams, vecs): 94 | J = Y - B @ S 95 | X1 = S.T @ Y @ Y.T 96 | absBS = vecs @ (np.abs(lams[:, np.newaxis]) * (vecs.T @ S)) 97 | X2 = S.T @ absBS @ absBS.T 98 | U = lstsq((X1 + X2) @ S, X1 + X2)[0].T 99 | UJT = U @ J.T 100 | return (UJT + UJT.T) - U @ (J.T @ S) @ U.T 101 | 102 | 103 | def _MS_PSB(B, S, Y): 104 | J = Y - B @ S 105 | U = solve(S.T @ S, S.T).T 106 | UJT = U @ J.T 107 | return (UJT + UJT.T) - U @ (J.T @ S) @ U.T 108 | 109 | 110 | def _MS_DFP(B, S, Y): 111 | J = Y - B @ S 112 | U = solve(S.T @ Y, Y.T).T 113 | UJT = U @ J.T 114 | return (UJT + UJT.T) - U @ (J.T @ S) @ U.T 115 | 116 | 117 | def _MS_SR1(B, S, Y): 118 | YBS = Y - B @ S 119 | return YBS @ solve(YBS.T @ S, YBS.T) 120 | 121 | 122 | def _MS_Greenstadt(B, S, Y): 123 | J = Y - B @ S 124 | MS = B @ S 125 | U = solve(S.T @ MS, MS.T).T 126 | UJT = U @ J.T 127 | return (UJT + UJT.T) - U @ (J.T @ S) @ U.T 128 | 129 | 130 | # Not a symmetric update, so not available my default 131 | def _MS_Powell(B, S, Y): # pragma: no cover 132 | return (Y - B @ S) @ S.T 133 | -------------------------------------------------------------------------------- /sella/linalg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from typing import List 4 | from itertools import product 5 | import numpy as np 6 | 7 | from sella.hessian_update import update_H 8 | 9 | from scipy.sparse.linalg import LinearOperator 10 | from scipy.linalg import eigh 11 | 12 | 13 | class NumericalHessian(LinearOperator): 14 | dtype = np.dtype('float64') 15 | 16 | def __init__(self, func, x0, g0, eta, threepoint=False, Uproj=None): 17 | self.func = func 18 | self.x0 = x0.copy() 19 | self.g0 = g0.copy() 20 | self.eta = eta 21 | self.threepoint = threepoint 22 | self.calls = 0 23 | self.Uproj = Uproj 24 | 25 | self.ntrue = len(self.x0) 26 | 27 | if self.Uproj is not None: 28 | ntrue, n = self.Uproj.shape 29 | assert ntrue == self.ntrue 30 | else: 31 | n = self.ntrue 32 | 33 | self.shape = (n, n) 34 | 35 | self.Vs = np.empty((self.ntrue, 0), dtype=self.dtype) 36 | self.AVs = np.empty((self.ntrue, 0), dtype=self.dtype) 37 | 38 | def _matvec(self, v): 39 | self.calls += 1 40 | 41 | if self.Uproj is not None: 42 | v = self.Uproj @ v.ravel() 43 | 44 | # Since the sign of v is arbitrary, we choose a "canonical" direction 45 | # for the finite displacement. Essentially, we always displace in a 46 | # descent direction, unless the displacement vector is orthogonal 47 | # to the gradient. In that case, we choose a displacement in the 48 | # direction which brings the current coordinates projected onto 49 | # the displacement vector closer to "0". If the displacement 50 | # vector is orthogonal to both the gradient and the coordinate 51 | # vector, then choose whatever direction makes the first nonzero 52 | # element of the displacement positive. 53 | # 54 | # Note that these are completely arbitrary criteria for choosing 55 | # displacement direction. We are just trying to be as consistent 56 | # as possible for numerical stability and reproducibility reasons. 57 | 58 | vdotg = v.ravel() @ self.g0 59 | vdotx = v.ravel() @ self.x0 60 | sign = 1. 61 | if abs(vdotg) > 1e-4: 62 | sign = 2. * (vdotg < 0) - 1. 63 | elif abs(vdotx) > 1e-4: 64 | sign = 2. * (vdotx < 0) - 1. 65 | else: 66 | for vi in v.ravel(): 67 | if vi > 1e-4: 68 | sign = 1. 69 | break 70 | elif vi < -1e-4: 71 | sign = -1. 72 | break 73 | 74 | vnorm = np.linalg.norm(v) * sign 75 | _, gplus = self.func(self.x0 + self.eta * v.ravel() / vnorm) 76 | if self.threepoint: 77 | fminus, gminus = self.func(self.x0 - self.eta * v.ravel() / vnorm) 78 | Av = vnorm * (gplus - gminus) / (2 * self.eta) 79 | else: 80 | Av = vnorm * (gplus - self.g0) / self.eta 81 | 82 | self.Vs = np.hstack((self.Vs, v.reshape((self.ntrue, -1)))) 83 | self.AVs = np.hstack((self.AVs, Av.reshape((self.ntrue, -1)))) 84 | 85 | if self.Uproj is not None: 86 | Av = self.Uproj.T @ Av 87 | 88 | return Av 89 | 90 | def __add__(self, other): 91 | return MatrixSum(self, other) 92 | 93 | def _transpose(self): 94 | return self 95 | 96 | 97 | class MatrixSum(LinearOperator): 98 | def __init__(self, *matrices): 99 | # This makes sure that if matrices of different dtypes are 100 | # provided, we use the most general type for the sum. 101 | 102 | # For example, if two matrices are provided with the detypes 103 | # np.int64 and np.float64, then this MatrixSum object will be 104 | # np.float64. 105 | self.dtype = sorted([mat.dtype for mat in matrices], reverse=True)[0] 106 | self.shape = matrices[0].shape 107 | 108 | mnum = None 109 | self.matrices = [] 110 | for matrix in matrices: 111 | assert matrix.dtype <= self.dtype 112 | assert matrix.shape == self.shape, (matrix.shape, self.shape) 113 | if isinstance(matrix, np.ndarray): 114 | if mnum is None: 115 | mnum = np.zeros(self.shape, dtype=self.dtype) 116 | mnum += matrix 117 | else: 118 | self.matrices.append(matrix) 119 | 120 | if mnum is not None: 121 | self.matrices.append(mnum) 122 | 123 | def _matvec(self, v): 124 | w = np.zeros_like(v, dtype=self.dtype) 125 | for matrix in self.matrices: 126 | w += matrix.dot(v) 127 | return w 128 | 129 | def _transpose(self): 130 | return MatrixSum(*[mat.T for mat in self.matrices]) 131 | 132 | def __add__(self, other): 133 | return MatrixSum(*self.matrices, other) 134 | 135 | 136 | class ApproximateHessian(LinearOperator): 137 | def __init__( 138 | self, 139 | dim: int, 140 | ncart: int, 141 | B0: np.ndarray = None, 142 | update_method: str = 'TS-BFGS', 143 | symm: int = 2, 144 | initialized: bool = False, 145 | ) -> None: 146 | """A wrapper object for the approximate Hessian matrix.""" 147 | self.dim = dim 148 | self.ncart = ncart 149 | self.shape = (self.dim, self.dim) 150 | self.dtype = np.float64 151 | self.update_method = update_method 152 | self.symm = symm 153 | self.initialized = initialized 154 | 155 | self.set_B(B0) 156 | 157 | def set_B(self, target): 158 | if target is None: 159 | self.B = None 160 | self.evals = None 161 | self.evecs = None 162 | self.initialized = False 163 | return 164 | elif np.isscalar(target): 165 | target = target * np.eye(self.dim) 166 | else: 167 | self.initialized = True 168 | assert target.shape == self.shape 169 | self.B = target 170 | self.evals, self.evecs = eigh(self.B) 171 | 172 | def update(self, dx, dg): 173 | """Perform a quasi-Newton update on B""" 174 | if self.B is None: 175 | B = np.zeros(self.shape, dtype=self.dtype) 176 | else: 177 | B = self.B.copy() 178 | if not self.initialized: 179 | self.initialized = True 180 | dx_cart = dx[:self.ncart] 181 | dg_cart = dg[:self.ncart] 182 | B[:self.ncart, :self.ncart] = update_H( 183 | None, dx_cart, dg_cart, method=self.update_method, 184 | symm=self.symm, lams=None, vecs=None 185 | ) 186 | self.set_B(B) 187 | return 188 | 189 | self.set_B(update_H(B, dx, dg, method=self.update_method, 190 | symm=self.symm, lams=self.evals, vecs=self.evecs)) 191 | 192 | def project(self, U): 193 | """Project B into the subspace defined by U.""" 194 | m, n = U.shape 195 | assert m == self.dim 196 | 197 | if self.B is None: 198 | Bproj = None 199 | else: 200 | Bproj = U.T @ self.B @ U 201 | 202 | return ApproximateHessian(n, 0, Bproj, self.update_method, 203 | self.symm) 204 | 205 | def asarray(self): 206 | if self.B is not None: 207 | return self.B 208 | return np.eye(self.dim) 209 | 210 | def _matvec(self, v): 211 | if self.B is None: 212 | return v 213 | return self.B @ v 214 | 215 | def _rmatvec(self, v): 216 | return self.matvec(v) 217 | 218 | def _matmat(self, X): 219 | if self.B is None: 220 | return X 221 | return self.B @ X 222 | 223 | def _rmatmat(self, X): 224 | return self.matmat(X) 225 | 226 | def __add__(self, other): 227 | initialized = self.initialized 228 | if isinstance(other, ApproximateHessian): 229 | other = other.B 230 | initialized = initialized and other.initialized 231 | if not self.initialized or other is None: 232 | tot = None 233 | initialized = False 234 | else: 235 | tot = self.B + other 236 | return ApproximateHessian( 237 | self.dim, self.ncart, tot, self.update_method, self.symm, 238 | initialized=initialized, 239 | ) 240 | 241 | 242 | class SparseInternalJacobian(LinearOperator): 243 | dtype = np.float64 244 | 245 | def __init__( 246 | self, 247 | natoms: int, 248 | indices: List[List[int]], 249 | vals: List[List[np.ndarray]], 250 | ) -> None: 251 | self.natoms = natoms 252 | self.indices = indices 253 | self.vals = vals 254 | self.nints = len(self.indices) 255 | self.shape = (self.nints, 3 * self.natoms) 256 | 257 | def asarray(self) -> np.ndarray: 258 | B = np.zeros((self.nints, self.natoms, 3)) 259 | for Bi, idx, vals in zip(B, self.indices, self.vals): 260 | for j, v in zip(idx, vals): 261 | Bi[j] += v 262 | return B.reshape(self.shape) 263 | 264 | def _matvec(self, v: np.ndarray) -> np.ndarray: 265 | vi = v.reshape((self.natoms, 3)) 266 | w = np.zeros(self.nints) 267 | for i in range(self.nints): 268 | for j, val in zip(self.indices[i], self.vals[i]): 269 | w[i] += vi[j] @ val 270 | return w 271 | 272 | def _rmatvec(self, v: np.ndarray) -> np.ndarray: 273 | w = np.zeros((self.natoms, 3)) 274 | for vi, indices, vals in zip(v, self.indices, self.vals): 275 | for j, val in zip(indices, vals): 276 | w[j] += vi * val 277 | return w.ravel() 278 | 279 | 280 | class SparseInternalHessian(LinearOperator): 281 | dtype = np.float64 282 | 283 | def __init__( 284 | self, 285 | natoms: int, 286 | indices: List[int], 287 | vals: np.ndarray, 288 | ) -> None: 289 | self.natoms = natoms 290 | self.shape = (3 * self.natoms, 3 * self.natoms) 291 | self.indices = indices 292 | self.vals = vals 293 | 294 | def asarray(self) -> np.ndarray: 295 | H = np.zeros((self.natoms, 3, self.natoms, 3)) 296 | for (a, i), (b, j) in product(enumerate(self.indices), repeat=2): 297 | H[i, :, j, :] += self.vals[a, :, b, :] 298 | return H.reshape(self.shape) 299 | 300 | def _matvec(self, v: np.ndarray) -> np.ndarray: 301 | vi = v.reshape((self.natoms, 3)) 302 | w = np.zeros_like(vi) 303 | for (a, i), (b, j) in product(enumerate(self.indices), repeat=2): 304 | w[i] += self.vals[a, :, b, :] @ vi[j, :] 305 | return w.ravel() 306 | 307 | def _rmatvec(self, v: np.ndarray) -> np.ndarray: 308 | return self._matvec(v) 309 | 310 | 311 | class SparseInternalHessians: 312 | def __init__( 313 | self, 314 | hessians: List[SparseInternalHessian], 315 | ndof: int 316 | ): 317 | self.hessians = hessians 318 | self.natoms = ndof // 3 319 | self.shape = (len(self.hessians), ndof, ndof) 320 | 321 | def asarray(self) -> np.ndarray: 322 | return np.array([hess.asarray() for hess in self.hessians]) 323 | 324 | def ldot(self, v: np.ndarray) -> np.ndarray: 325 | M = np.zeros((self.natoms, 3, self.natoms, 3)) 326 | for vi, hess in zip(v, self.hessians): 327 | for (a, i), (b, j) in product(enumerate(hess.indices), repeat=2): 328 | M[i, :, j, :] += vi * hess.vals[a, :, b, :] 329 | return M.reshape(self.shape[1:]) 330 | 331 | def rdot(self, v: np.ndarray) -> np.ndarray: 332 | M = np.zeros(self.shape[:2]) 333 | for row, hessian in zip(M, self.hessians): 334 | row[:] = hessian @ v 335 | return M 336 | 337 | def ddot(self, u: np.ndarray, v: np.ndarray) -> np.ndarray: 338 | w = np.zeros(self.shape[0]) 339 | for i, hessian in enumerate(self.hessians): 340 | w[i] = u @ hessian @ v 341 | return w 342 | -------------------------------------------------------------------------------- /sella/optimize/__init__.py: -------------------------------------------------------------------------------- 1 | from .optimize import Sella 2 | from .irc import IRC 3 | -------------------------------------------------------------------------------- /sella/optimize/irc.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Optional, Union, Dict, Any 3 | 4 | import numpy as np 5 | from scipy.linalg import eigh 6 | 7 | from ase import Atoms 8 | from ase.io.trajectory import Trajectory, TrajectoryWriter 9 | from ase.optimize.optimize import Optimizer 10 | 11 | from sella.peswrapper import PES 12 | from .restricted_step import IRCTrustRegion 13 | from .stepper import QuasiNewtonIRC 14 | 15 | 16 | class IRCInnerLoopConvergenceFailure(RuntimeError): 17 | pass 18 | 19 | 20 | class IRC(Optimizer): 21 | def __init__( 22 | self, 23 | atoms: Atoms, 24 | logfile: str = '-', 25 | trajectory: Optional[Union[str, TrajectoryWriter]] = None, 26 | master: Optional[bool] = None, 27 | ninner_iter: int = 10, 28 | irctol: float = 1e-2, 29 | dx: float = 0.1, 30 | eta: float = 1e-4, 31 | gamma: float = 0.1, 32 | peskwargs: Optional[Dict[str, Any]] = None, 33 | keep_going: bool = False, 34 | **kwargs 35 | ): 36 | Optimizer.__init__( 37 | self, 38 | atoms, 39 | restart=None, 40 | logfile=logfile, 41 | trajectory=trajectory, 42 | master=master, 43 | ) 44 | self.ninner_iter = ninner_iter 45 | self.irctol = irctol 46 | self.dx = dx 47 | if peskwargs is None: 48 | self.peskwargs = dict(gamma=gamma) 49 | 50 | if 'masses' not in self.atoms.arrays: 51 | try: 52 | self.atoms.set_masses('most_common') 53 | except ValueError: 54 | warnings.warn("The version of ASE that is installed does not " 55 | "contain the most common isotope masses, so " 56 | "Earth-abundance-averaged masses will be used " 57 | "instead!") 58 | self.atoms.set_masses('defaults') 59 | 60 | self.sqrtm = np.repeat(np.sqrt(self.atoms.get_masses()), 3) 61 | 62 | self.pes = PES(atoms, eta=eta, proj_trans=False, proj_rot=False, 63 | **kwargs) 64 | 65 | self.lastrun = None 66 | self.x0 = self.pes.get_x().copy() 67 | self.v0ts: Optional[np.ndarray] = None 68 | self.H0: Optional[np.ndarray] = None 69 | self.peslast = None 70 | self.xi = 1. 71 | self.first = True 72 | self.keep_going = keep_going 73 | 74 | def irun( 75 | self, 76 | fmax: float = 0.05, 77 | fmax_inner: float = 0.01, 78 | steps: Optional[int] = None, 79 | direction: str = 'forward', 80 | ): 81 | if direction not in ['forward', 'reverse']: 82 | raise ValueError('direction must be one of "forward" or ' 83 | '"reverse"!') 84 | 85 | if self.v0ts is None: 86 | # Initial diagonalization 87 | self.pes.kick(0, True, **self.peskwargs) 88 | self.H0 = self.pes.get_H().asarray().copy() 89 | Hw = self.H0 / np.outer(self.sqrtm, self.sqrtm) 90 | _, vecs = eigh(Hw) 91 | self.v0ts = self.dx * vecs[:, 0] / self.sqrtm 92 | 93 | # force v0ts to be the direction where the first non-zero 94 | # component is positive 95 | if self.v0ts[np.nonzero(self.v0ts)[0][0]] < 0: 96 | self.v0ts *= -1 97 | 98 | self.pescurr = self.pes.curr.copy() 99 | self.peslast = self.pes.last.copy() 100 | else: 101 | # Or, restore from last diagonalization for new direction 102 | self.pes.set_x(self.x0) 103 | self.pes.curr = self.pescurr.copy() 104 | self.pes.last = self.peslast.copy() 105 | self.pes.set_H(self.H0.copy(), initialized=True) 106 | 107 | if direction == 'forward': 108 | self.d1 = self.v0ts.copy() 109 | elif direction == 'reverse': 110 | self.d1 = -self.v0ts.copy() 111 | 112 | self.first = True 113 | self.fmax_inner = min(fmax, fmax_inner) 114 | return Optimizer.irun(self, fmax, steps) 115 | 116 | def run(self, *args, **kwargs): 117 | for converged in self.irun(*args, **kwargs): 118 | pass 119 | return converged 120 | 121 | def step(self): 122 | if self.first: 123 | self.pes.kick(self.d1) 124 | self.first = False 125 | for n in range(self.ninner_iter): 126 | s, smag = IRCTrustRegion( 127 | self.pes, 128 | 0, 129 | self.dx, 130 | method=QuasiNewtonIRC, 131 | sqrtm=self.sqrtm, 132 | d1=self.d1, 133 | W=self.get_W(), 134 | ).get_s() 135 | 136 | bound_clip = abs(smag - self.dx) < 1e-8 137 | self.d1 += s 138 | 139 | self.pes.kick(s) 140 | g1 = self.pes.get_g() 141 | 142 | d1m = self.d1 * self.sqrtm 143 | d1m /= np.linalg.norm(d1m) 144 | g1m = g1 / self.sqrtm 145 | 146 | g1m_proj = g1m - d1m * (d1m @ g1m) 147 | fmax = np.linalg.norm( 148 | (g1m_proj * self.sqrtm).reshape((-1, 3)), axis=1 149 | ).max() 150 | 151 | g1m /= np.linalg.norm(g1m) 152 | if bound_clip and fmax < self.fmax_inner: 153 | break 154 | elif self.converged(): 155 | break 156 | else: 157 | if self.keep_going: 158 | warnings.warn( 159 | 'IRC inner loop failed to converge! The trajectory is no ' 160 | 'longer a trustworthy IRC.' 161 | ) 162 | else: 163 | raise IRCInnerLoopConvergenceFailure 164 | 165 | self.d1 *= 0. 166 | 167 | def converged(self, forces=None): 168 | return self.pes.converged(self.fmax)[0] and self.pes.H.evals[0] > 0 169 | 170 | def get_W(self): 171 | return np.diag(1. / np.sqrt(np.repeat(self.atoms.get_masses(), 3))) 172 | -------------------------------------------------------------------------------- /sella/optimize/optimize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import warnings 4 | from time import localtime, strftime 5 | from typing import Union, Callable, Optional 6 | 7 | import numpy as np 8 | from ase import Atoms 9 | from ase.optimize.optimize import Optimizer 10 | from ase.utils import basestring 11 | from ase.io.trajectory import Trajectory 12 | 13 | from .restricted_step import get_restricted_step 14 | from sella.peswrapper import PES, InternalPES 15 | from sella.internal import Internals, Constraints 16 | 17 | _default_kwargs = dict( 18 | minimum=dict( 19 | delta0=1e-1, 20 | sigma_inc=1.15, 21 | sigma_dec=0.90, 22 | rho_inc=1.035, 23 | rho_dec=100, 24 | method='rfo', 25 | eig=False 26 | ), 27 | saddle=dict( 28 | delta0=0.1, 29 | sigma_inc=1.15, 30 | sigma_dec=0.65, 31 | rho_inc=1.035, 32 | rho_dec=5.0, 33 | method='prfo', 34 | eig=True 35 | ) 36 | ) 37 | 38 | 39 | class Sella(Optimizer): 40 | def __init__( 41 | self, 42 | atoms: Atoms, 43 | restart: bool = None, 44 | logfile: str = '-', 45 | trajectory: Union[str, Trajectory] = None, 46 | master: bool = None, 47 | delta0: float = None, 48 | sigma_inc: float = None, 49 | sigma_dec: float = None, 50 | rho_dec: float = None, 51 | rho_inc: float = None, 52 | order: int = 1, 53 | eig: bool = None, 54 | eta: float = 1e-4, 55 | method: str = None, 56 | gamma: float = 0.1, 57 | threepoint: bool = False, 58 | constraints: Constraints = None, 59 | constraints_tol: float = 1e-5, 60 | v0: np.ndarray = None, 61 | internal: Union[bool, Internals] = False, 62 | append_trajectory: bool = False, 63 | rs: str = None, 64 | nsteps_per_diag: int = 3, 65 | diag_every_n: Optional[int] = None, 66 | hessian_function: Optional[Callable[[Atoms], np.ndarray]] = None, 67 | **kwargs 68 | ): 69 | if order == 0: 70 | default = _default_kwargs['minimum'] 71 | else: 72 | default = _default_kwargs['saddle'] 73 | 74 | if trajectory is not None: 75 | if isinstance(trajectory, basestring): 76 | mode = "a" if append_trajectory else "w" 77 | trajectory = Trajectory(trajectory, mode=mode, 78 | atoms=atoms, master=master) 79 | 80 | asetraj = None 81 | self.peskwargs = kwargs.copy() 82 | self.user_internal = internal 83 | self.initialize_pes( 84 | atoms, 85 | trajectory, 86 | order, 87 | eta, 88 | constraints, 89 | v0, 90 | internal, 91 | hessian_function, 92 | **kwargs 93 | ) 94 | 95 | if rs is None: 96 | rs = 'mis' if internal else 'ras' 97 | self.rs = get_restricted_step(rs) 98 | Optimizer.__init__(self, atoms, restart=restart, 99 | logfile=logfile, trajectory=asetraj, 100 | master=master) 101 | 102 | if delta0 is None: 103 | delta0 = default['delta0'] 104 | if rs in ['mis', 'ras']: 105 | self.delta = delta0 106 | else: 107 | self.delta = delta0 * self.pes.get_Ufree().shape[1] 108 | 109 | if sigma_inc is None: 110 | self.sigma_inc = default['sigma_inc'] 111 | else: 112 | self.sigma_inc = sigma_inc 113 | 114 | if sigma_dec is None: 115 | self.sigma_dec = default['sigma_dec'] 116 | else: 117 | self.sigma_dec = sigma_dec 118 | 119 | if rho_inc is None: 120 | self.rho_inc = default['rho_inc'] 121 | else: 122 | self.rho_inc = rho_inc 123 | 124 | if rho_dec is None: 125 | self.rho_dec = default['rho_dec'] 126 | else: 127 | self.rho_dec = rho_dec 128 | 129 | if method is None: 130 | self.method = default['method'] 131 | else: 132 | self.method = method 133 | 134 | if eig is None: 135 | self.eig = default['eig'] 136 | else: 137 | self.eig = eig 138 | 139 | self.ord = order 140 | self.eta = eta 141 | self.delta_min = self.eta 142 | self.constraints_tol = constraints_tol 143 | self.diagkwargs = dict(gamma=gamma, threepoint=threepoint) 144 | self.rho = 1. 145 | 146 | if self.ord != 0 and not self.eig: 147 | warnings.warn("Saddle point optimizations with eig=False will " 148 | "most likely fail!\n Proceeding anyway, but you " 149 | "shouldn't be optimistic.") 150 | 151 | self.initialized = False 152 | self.xi = 1. 153 | self.nsteps_per_diag = nsteps_per_diag 154 | self.nsteps_since_diag = 0 155 | self.diag_every_n = np.inf if diag_every_n is None else diag_every_n 156 | 157 | def initialize_pes( 158 | self, 159 | atoms: Atoms, 160 | trajectory: str = None, 161 | order: int = 1, 162 | eta: float = 1e-4, 163 | constraints: Constraints = None, 164 | v0: np.ndarray = None, 165 | internal: Union[bool, Internals] = False, 166 | hessian_function: Optional[Callable[[Atoms], np.ndarray]] = None, 167 | **kwargs 168 | ): 169 | if internal: 170 | if isinstance(internal, Internals): 171 | auto_find_internals = False 172 | if constraints is not None: 173 | raise ValueError( 174 | "Internals object and Constraint object cannot both " 175 | "be provided to Sella. Instead, you must pass the " 176 | "Constraints object to the constructor of the " 177 | "Internals object." 178 | ) 179 | else: 180 | auto_find_internals = True 181 | internal = Internals(atoms, cons=constraints) 182 | self.internal = internal.copy() 183 | self.constraints = None 184 | self.pes = InternalPES( 185 | atoms, 186 | internals=internal, 187 | trajectory=trajectory, 188 | eta=eta, 189 | v0=v0, 190 | auto_find_internals=auto_find_internals, 191 | hessian_function=hessian_function, 192 | **kwargs 193 | ) 194 | else: 195 | self.internal = None 196 | if constraints is None: 197 | constraints = Constraints(atoms) 198 | self.constraints = constraints 199 | self.pes = PES( 200 | atoms, 201 | constraints=constraints, 202 | trajectory=trajectory, 203 | eta=eta, 204 | v0=v0, 205 | hessian_function=hessian_function, 206 | **kwargs 207 | ) 208 | self.trajectory = self.pes.traj 209 | 210 | def _predict_step(self): 211 | if not self.initialized: 212 | self.pes.get_g() 213 | if self.eig: 214 | if self.pes.hessian_function is not None: 215 | self.pes.calculate_hessian() 216 | else: 217 | self.pes.diag(**self.diagkwargs) 218 | self.nsteps_since_diag = -1 219 | self.initialized = True 220 | 221 | self.pes.cons.disable_satisfied_inequalities() 222 | self.pes._update_basis() 223 | self.pes.save() 224 | all_valid = False 225 | x0 = self.pes.get_x() 226 | while not all_valid: 227 | s, smag = self.rs( 228 | self.pes, self.ord, self.delta, method=self.method 229 | ).get_s() 230 | self.pes.set_x(x0 + s) 231 | all_valid = self.pes.cons.validate_inequalities() 232 | self.pes._update_basis() 233 | self.pes.restore() 234 | self.pes._update_basis() 235 | return s, smag 236 | 237 | def step(self): 238 | s, smag = self._predict_step() 239 | 240 | # Determine if we need to call the eigensolver, then step 241 | if self.nsteps_since_diag >= self.diag_every_n: 242 | ev = True 243 | elif self.eig and self.nsteps_since_diag >= self.nsteps_per_diag: 244 | if self.pes.H.evals is None: 245 | ev = True 246 | else: 247 | Unred = self.pes.get_Unred() 248 | ev = (self.pes.get_HL().project(Unred) 249 | .evals[:self.ord] > 0).any() 250 | else: 251 | ev = False 252 | 253 | if ev: 254 | self.nsteps_since_diag = 0 255 | else: 256 | self.nsteps_since_diag += 1 257 | 258 | rho = self.pes.kick(s, ev, **self.diagkwargs) 259 | 260 | # Check for bad internals, and if found, reset PES object. 261 | # This skips the trust radius update. 262 | if self.internal and self.pes.int.check_for_bad_internals(): 263 | self.initialize_pes( 264 | atoms=self.pes.atoms, 265 | trajectory=self.pes.traj, 266 | order=self.ord, 267 | eta=self.pes.eta, 268 | constraints=self.constraints, 269 | v0=None, # TODO: use leftmost eigenvector from old H 270 | internal=self.user_internal, 271 | hessian_function=self.pes.hessian_function, 272 | ) 273 | self.initialized = False 274 | self.rho = 1 275 | return 276 | 277 | # Update trust radius 278 | if rho is None: 279 | pass 280 | elif rho < 1./self.rho_dec or rho > self.rho_dec: 281 | self.delta = max(smag * self.sigma_dec, self.delta_min) 282 | elif 1./self.rho_inc < rho < self.rho_inc: 283 | self.delta = max(self.sigma_inc * smag, self.delta) 284 | self.rho = rho 285 | if self.rho is None: 286 | self.rho = 1. 287 | 288 | def converged(self, forces=None): 289 | return self.pes.converged(self.fmax)[0] 290 | 291 | def log(self, forces=None): 292 | if self.logfile is None: 293 | return 294 | _, fmax, cmax = self.pes.converged(self.fmax) 295 | e = self.pes.get_f() 296 | T = strftime("%H:%M:%S", localtime()) 297 | name = self.__class__.__name__ 298 | buf = " " * len(name) 299 | if self.nsteps == 0: 300 | self.logfile.write(buf + "{:>4s} {:>8s} {:>15s} {:>12s} {:>12s} " 301 | "{:>12s} {:>12s}\n" 302 | .format("Step", "Time", "Energy", "fmax", 303 | "cmax", "rtrust", "rho")) 304 | self.logfile.write("{} {:>3d} {:>8s} {:>15.6f} {:>12.4f} {:>12.4f} " 305 | "{:>12.4f} {:>12.4f}\n" 306 | .format(name, self.nsteps, T, e, fmax, cmax, 307 | self.delta, self.rho)) 308 | self.logfile.flush() 309 | -------------------------------------------------------------------------------- /sella/optimize/restricted_step.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union, List 2 | 3 | import numpy as np 4 | import inspect 5 | 6 | from sella.peswrapper import PES, InternalPES 7 | from .stepper import get_stepper, BaseStepper, NaiveStepper 8 | 9 | 10 | # Classes for restricted step (e.g. trust radius, max atom displacement, etc) 11 | class BaseRestrictedStep: 12 | synonyms: List[str] = [] 13 | 14 | def __init__( 15 | self, 16 | pes: Union[PES, InternalPES], 17 | order: int, 18 | delta: float, 19 | method: str = 'qn', 20 | tol: float = 1e-15, 21 | maxiter: int = 1000, 22 | d1: Optional[np.ndarray] = None, 23 | W: Optional[np.ndarray] = None, 24 | ): 25 | self.pes = pes 26 | self.delta = delta 27 | self.d1 = d1 28 | g0 = self.pes.get_g() 29 | 30 | if W is None: 31 | W = np.eye(len(g0)) 32 | 33 | self.scons = self.pes.get_scons() 34 | # TODO: Should this be HL instead of H? 35 | g = g0 + self.pes.get_H() @ self.scons 36 | 37 | if inspect.isclass(method) and issubclass(method, BaseStepper): 38 | stepper = method 39 | else: 40 | stepper = get_stepper(method.lower()) 41 | 42 | if self.cons(self.scons) - self.delta > 1e-8: 43 | self.P = self.pes.get_Unred().T 44 | dx = self.P @ self.scons 45 | self.stepper = NaiveStepper(dx) 46 | self.scons[:] *= 0 47 | else: 48 | self.P = self.pes.get_Ufree().T @ W 49 | d1 = self.d1 50 | if d1 is not None: 51 | d1 = np.linalg.lstsq(self.P.T, d1, rcond=None)[0] 52 | self.stepper = stepper( 53 | self.P @ g, 54 | self.pes.get_HL().project(self.P.T), 55 | order, 56 | d1=d1, 57 | ) 58 | 59 | self.tol = tol 60 | self.maxiter = maxiter 61 | 62 | def cons(self, s, dsda=None): 63 | raise NotImplementedError 64 | 65 | def eval(self, alpha): 66 | s, dsda = self.stepper.get_s(alpha) 67 | stot = self.P.T @ s + self.scons 68 | val, dval = self.cons(stot, self.P.T @ dsda) 69 | return stot, val, dval 70 | 71 | def get_s(self): 72 | alpha = self.stepper.alpha0 73 | 74 | s, val, dval = self.eval(alpha) 75 | if val < self.delta: 76 | assert val > 0. 77 | return s, val 78 | err = val - self.delta 79 | 80 | lower = self.stepper.alphamin 81 | upper = self.stepper.alphamax 82 | 83 | for niter in range(self.maxiter): 84 | if abs(err) <= self.tol: 85 | break 86 | 87 | if np.nextafter(lower, upper) >= upper: 88 | break 89 | 90 | if err * self.stepper.slope > 0: 91 | upper = alpha 92 | else: 93 | lower = alpha 94 | 95 | a1 = alpha - err / dval 96 | if np.isnan(a1) or a1 <= lower or a1 >= upper or niter > 4: 97 | a2 = (lower + upper) / 2. 98 | if np.isinf(a2): 99 | alpha = alpha + max(1, 0.5 * alpha) * np.sign(a2) 100 | else: 101 | alpha = a2 102 | else: 103 | alpha = a1 104 | 105 | s, val, dval = self.eval(alpha) 106 | err = val - self.delta 107 | else: 108 | raise RuntimeError("Restricted step failed to converge!") 109 | 110 | assert val > 0 111 | return s, self.delta 112 | 113 | @classmethod 114 | def match(cls, name): 115 | return name in cls.synonyms 116 | 117 | 118 | class TrustRegion(BaseRestrictedStep): 119 | synonyms = [ 120 | 'tr', 121 | 'trust region', 122 | 'trust-region', 123 | 'trust radius', 124 | 'trust-radius', 125 | ] 126 | 127 | def cons(self, s, dsda=None): 128 | val = np.linalg.norm(s) 129 | if dsda is None: 130 | return val 131 | 132 | dval = dsda @ s / val 133 | return val, dval 134 | 135 | 136 | class IRCTrustRegion(TrustRegion): 137 | synonyms = [] 138 | 139 | def __init__(self, *args, sqrtm=None, **kwargs): 140 | assert sqrtm is not None 141 | self.sqrtm = sqrtm 142 | TrustRegion.__init__(self, *args, **kwargs) 143 | assert self.d1 is not None 144 | 145 | def cons(self, s, dsda=None): 146 | s = (s + self.d1) * self.sqrtm 147 | if dsda is not None: 148 | dsda = dsda * self.sqrtm 149 | return TrustRegion.cons(self, s, dsda) 150 | 151 | 152 | class RestrictedAtomicStep(BaseRestrictedStep): 153 | synonyms = ['ras', 'restricted atomic step'] 154 | 155 | def __init__(self, pes, *args, **kwargs): 156 | if pes.int is not None: 157 | raise ValueError( 158 | "Internal coordinates are not compatible with " 159 | f"the {self.__class__.__name__} trust region method." 160 | ) 161 | BaseRestrictedStep.__init__(self, pes, *args, **kwargs) 162 | 163 | def cons(self, s, dsda=None): 164 | s_mat = s.reshape((-1, 3)) 165 | s_norms = np.linalg.norm(s_mat, axis=1) 166 | index = np.argmax(s_norms) 167 | val = s_norms[index] 168 | 169 | if dsda is None: 170 | return val 171 | 172 | dsda_mat = dsda.reshape((-1, 3)) 173 | dval = dsda_mat[index] @ s_mat[index] / val 174 | return val, dval 175 | 176 | 177 | class MaxInternalStep(BaseRestrictedStep): 178 | synonyms = ['mis', 'max internal step'] 179 | 180 | def __init__( 181 | self, pes, *args, wx=1., wb=1., wa=1., wd=1., wo=1., **kwargs 182 | ): 183 | if pes.int is None: 184 | raise ValueError( 185 | "Internal coordinates are required for the " 186 | "{self.__class__.__name__} trust region method" 187 | ) 188 | self.wx = wx 189 | self.wb = wb 190 | self.wa = wa 191 | self.wd = wd 192 | self.wo = wo 193 | BaseRestrictedStep.__init__(self, pes, *args, **kwargs) 194 | 195 | def cons(self, s, dsda=None): 196 | w = np.array( 197 | [self.wx] * self.pes.int.ntrans 198 | + [self.wb] * self.pes.int.nbonds 199 | + [self.wa] * self.pes.int.nangles 200 | + [self.wd] * self.pes.int.ndihedrals 201 | + [self.wo] * self.pes.int.nother 202 | + [self.wx] * self.pes.int.nrotations 203 | ) 204 | assert len(w) == len(s) 205 | 206 | sw = np.abs(s * w) 207 | idx = np.argmax(np.abs(sw)) 208 | val = sw[idx] 209 | 210 | if dsda is None: 211 | return val 212 | return val, np.sign(s[idx]) * dsda[idx] * w[idx] 213 | 214 | 215 | _all_restricted_step = [TrustRegion, RestrictedAtomicStep, MaxInternalStep] 216 | 217 | 218 | def get_restricted_step(name): 219 | for rs in _all_restricted_step: 220 | if rs.match(name): 221 | return rs 222 | raise ValueError("Unknown restricted step name: {}".format(name)) 223 | -------------------------------------------------------------------------------- /sella/optimize/stepper.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple, Type, List 2 | 3 | import numpy as np 4 | from scipy.linalg import eigh 5 | 6 | from sella.linalg import ApproximateHessian 7 | 8 | 9 | # Classes for optimization algorithms (e.g. MMF, Newton, RFO) 10 | class BaseStepper: 11 | alpha0: Optional[float] = None 12 | alphamin: Optional[float] = None 13 | alphamax: Optional[float] = None 14 | # Whether the step size increases or decreases with increasing alpha 15 | slope: Optional[float] = None 16 | synonyms: List[str] = [] 17 | 18 | def __init__( 19 | self, 20 | g: np.ndarray, 21 | H: ApproximateHessian, 22 | order: int = 0, 23 | d1: Optional[np.ndarray] = None, 24 | ) -> None: 25 | self.g = g 26 | self.H = H 27 | self.order = order 28 | self.d1 = d1 29 | self._stepper_init() 30 | 31 | @classmethod 32 | def match(cls, name: str) -> bool: 33 | return name in cls.synonyms 34 | 35 | def _stepper_init(self) -> None: 36 | raise NotImplementedError # pragma: no cover 37 | 38 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 39 | raise NotImplementedError # pragma: no cover 40 | 41 | 42 | class NaiveStepper(BaseStepper): 43 | synonyms = [] # No synonyms, we don't want someone using this accidentally 44 | alpha0 = 0.5 45 | alphamin = 0. 46 | alphamax = 1. 47 | slope = 1. 48 | 49 | def __init__(self, dx: np.ndarray) -> None: 50 | self.dx = dx 51 | 52 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 53 | return alpha * self.dx, self.dx 54 | 55 | 56 | class QuasiNewton(BaseStepper): 57 | alpha0 = 0. 58 | alphamin = 0. 59 | alphamax = np.inf 60 | slope = -1 61 | synonyms = [ 62 | 'qn', 63 | 'quasi-newton', 64 | 'quasi newton', 65 | 'quasi-newton', 66 | 'newton', 67 | 'mmf', 68 | 'minimum mode following', 69 | 'minimum-mode following', 70 | 'dimer', 71 | ] 72 | 73 | def _stepper_init(self) -> None: 74 | self.L = np.abs(self.H.evals) 75 | self.L[:self.order] *= -1 76 | 77 | self.V = self.H.evecs 78 | self.Vg = self.V.T @ self.g 79 | 80 | self.ones = np.ones_like(self.L) 81 | self.ones[:self.order] = -1 82 | 83 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 84 | denom = self.L + alpha * self.ones 85 | sproj = self.Vg / denom 86 | s = -self.V @ sproj 87 | dsda = self.V @ (sproj / denom) 88 | return s, dsda 89 | 90 | 91 | class QuasiNewtonIRC(QuasiNewton): 92 | synonyms = [] 93 | 94 | def _stepper_init(self) -> None: 95 | QuasiNewton._stepper_init(self) 96 | self.Vd1 = self.V.T @ self.d1 97 | 98 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 99 | denom = np.abs(self.L) + alpha 100 | sproj = -(self.Vg + alpha * self.Vd1) / denom 101 | s = self.V @ sproj 102 | dsda = -self.V @ ((sproj + self.Vd1) / denom) 103 | return s, dsda 104 | 105 | 106 | class RationalFunctionOptimization(BaseStepper): 107 | alpha0 = 1. 108 | alphamin = 0. 109 | alphamax = 1. 110 | slope = 1. 111 | synonyms = ['rfo', 'rational function optimization'] 112 | 113 | def _stepper_init(self) -> None: 114 | self.A = np.block([ 115 | [self.H.asarray(), self.g[:, np.newaxis]], 116 | [self.g, 0] 117 | ]) 118 | 119 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 120 | A = self.A * alpha 121 | A[:-1, :-1] *= alpha 122 | L, V = eigh(A) 123 | s = V[:-1, self.order] * alpha / V[-1, self.order] 124 | 125 | dAda = self.A.copy() 126 | dAda[:-1, :-1] *= 2 * alpha 127 | 128 | V1 = np.delete(V, self.order, 1) 129 | L1 = np.delete(L, self.order) 130 | dVda = V1 @ ((V1.T @ dAda @ V[:, self.order]) 131 | / (L1 - L[self.order])) 132 | 133 | dsda = (V[:-1, self.order] / V[-1, self.order] 134 | + (alpha / V[-1, self.order]) * dVda[:-1] 135 | - (V[:-1, self.order] * alpha 136 | / V[-1, self.order]**2) * dVda[-1]) 137 | return s, dsda 138 | 139 | 140 | class PartitionedRationalFunctionOptimization(RationalFunctionOptimization): 141 | synonyms = ['prfo', 'p-rfo', 'partitioned rational function optimization'] 142 | 143 | def _stepper_init(self) -> None: 144 | self.Vmax = self.H.evecs[:, :self.order] 145 | self.Vmin = self.H.evecs[:, self.order:] 146 | 147 | self.max = RationalFunctionOptimization( 148 | self.Vmax.T @ self.g, 149 | self.H.project(self.Vmax), 150 | order=self.Vmax.shape[1], 151 | ) 152 | 153 | self.min = RationalFunctionOptimization( 154 | self.Vmin.T @ self.g, 155 | self.H.project(self.Vmin), 156 | order=0, 157 | ) 158 | 159 | def get_s(self, alpha: float) -> Tuple[np.ndarray, np.ndarray]: 160 | smax, dsmaxda = self.max.get_s(alpha) 161 | smin, dsminda = self.min.get_s(alpha) 162 | 163 | s = self.Vmax @ smax + self.Vmin @ smin 164 | dsda = self.Vmax @ dsmaxda + self.Vmin @ dsminda 165 | return s, dsda 166 | 167 | 168 | _all_steppers = [ 169 | QuasiNewton, 170 | RationalFunctionOptimization, 171 | PartitionedRationalFunctionOptimization, 172 | ] 173 | 174 | 175 | def get_stepper(name: str) -> Type[BaseStepper]: 176 | for stepper in _all_steppers: 177 | if stepper.match(name): 178 | return stepper 179 | raise ValueError("Unknown stepper name: {}".format(name)) 180 | -------------------------------------------------------------------------------- /sella/peswrapper.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Callable 2 | 3 | import numpy as np 4 | from scipy.linalg import eigh 5 | from scipy.integrate import LSODA 6 | from ase import Atoms 7 | from ase.utils import basestring 8 | from ase.visualize import view 9 | from ase.calculators.singlepoint import SinglePointCalculator 10 | from ase.io.trajectory import Trajectory 11 | 12 | from sella.utilities.math import modified_gram_schmidt 13 | from sella.hessian_update import symmetrize_Y 14 | from sella.linalg import NumericalHessian, ApproximateHessian 15 | from sella.eigensolvers import rayleigh_ritz 16 | from sella.internal import Internals, Constraints, DuplicateInternalError 17 | 18 | 19 | class PES: 20 | def __init__( 21 | self, 22 | atoms: Atoms, 23 | H0: np.ndarray = None, 24 | constraints: Constraints = None, 25 | eigensolver: str = 'jd0', 26 | trajectory: Union[str, Trajectory] = None, 27 | eta: float = 1e-4, 28 | v0: np.ndarray = None, 29 | proj_trans: bool = None, 30 | proj_rot: bool = None, 31 | hessian_function: Callable[[Atoms], np.ndarray] = None, 32 | ) -> None: 33 | self.atoms = atoms 34 | if constraints is None: 35 | constraints = Constraints(self.atoms) 36 | if proj_trans is None: 37 | if constraints.internals['translations']: 38 | proj_trans = False 39 | else: 40 | proj_trans = True 41 | if proj_trans: 42 | try: 43 | constraints.fix_translation() 44 | except DuplicateInternalError: 45 | pass 46 | 47 | if proj_rot is None: 48 | if np.any(atoms.pbc): 49 | proj_rot = False 50 | else: 51 | proj_rot = True 52 | if proj_rot: 53 | try: 54 | constraints.fix_rotation() 55 | except DuplicateInternalError: 56 | pass 57 | self.cons = constraints 58 | self.eigensolver = eigensolver 59 | 60 | if trajectory is not None: 61 | if isinstance(trajectory, basestring): 62 | self.traj = Trajectory(trajectory, 'w', self.atoms) 63 | else: 64 | self.traj = trajectory 65 | else: 66 | self.traj = None 67 | 68 | self.eta = eta 69 | self.v0 = v0 70 | 71 | self.neval = 0 72 | self.curr = dict( 73 | x=None, 74 | f=None, 75 | g=None, 76 | ) 77 | self.last = self.curr.copy() 78 | 79 | # Internal coordinate specific things 80 | self.int = None 81 | self.dummies = None 82 | 83 | self.dim = 3 * len(atoms) 84 | self.ncart = self.dim 85 | if H0 is None: 86 | self.set_H(None, initialized=False) 87 | else: 88 | self.set_H(H0, initialized=True) 89 | 90 | self.savepoint = dict(apos=None, dpos=None) 91 | self.first_diag = True 92 | 93 | self.hessian_function = hessian_function 94 | 95 | apos = property(lambda self: self.atoms.positions.copy()) 96 | dpos = property(lambda self: None) 97 | 98 | def save(self): 99 | self.savepoint = dict(apos=self.apos, dpos=self.dpos) 100 | 101 | def restore(self): 102 | apos = self.savepoint['apos'] 103 | dpos = self.savepoint['dpos'] 104 | assert apos is not None 105 | self.atoms.positions = apos 106 | if dpos is not None: 107 | self.dummies.positions = dpos 108 | 109 | # Position getter/setter 110 | def set_x(self, target): 111 | diff = target - self.get_x() 112 | self.atoms.positions = target.reshape((-1, 3)) 113 | return diff, diff, self.curr.get('g', np.zeros_like(diff)) 114 | 115 | def get_x(self): 116 | return self.apos.ravel().copy() 117 | 118 | # Hessian getter/setter 119 | def get_H(self): 120 | return self.H 121 | 122 | def set_H(self, target, *args, **kwargs): 123 | self.H = ApproximateHessian( 124 | self.dim, self.ncart, target, *args, **kwargs 125 | ) 126 | 127 | # Hessian of the constraints 128 | def get_Hc(self): 129 | return self.cons.hessian().ldot(self.curr['L']) 130 | 131 | # Hessian of the Lagrangian 132 | def get_HL(self): 133 | return self.get_H() - self.get_Hc() 134 | 135 | # Getters for constraints and their derivatives 136 | def get_res(self): 137 | return self.cons.residual() 138 | 139 | def get_drdx(self): 140 | return self.cons.jacobian() 141 | 142 | def _calc_basis(self): 143 | drdx = self.get_drdx() 144 | U, S, VT = np.linalg.svd(drdx) 145 | ncons = np.sum(S > 1e-6) 146 | Ucons = VT[:ncons].T 147 | Ufree = VT[ncons:].T 148 | Unred = np.eye(self.dim) 149 | return drdx, Ucons, Unred, Ufree 150 | 151 | def write_traj(self): 152 | if self.traj is not None: 153 | self.traj.write() 154 | 155 | def eval(self): 156 | self.neval += 1 157 | f = self.atoms.get_potential_energy() 158 | g = -self.atoms.get_forces().ravel() 159 | self.write_traj() 160 | return f, g 161 | 162 | def _calc_eg(self, x): 163 | self.save() 164 | self.set_x(x) 165 | 166 | f, g = self.eval() 167 | 168 | self.restore() 169 | return f, g 170 | 171 | def get_scons(self): 172 | """Returns displacement vector for linear constraint correction.""" 173 | Ucons = self.get_Ucons() 174 | 175 | scons = -Ucons @ np.linalg.lstsq( 176 | self.get_drdx() @ Ucons, 177 | self.get_res(), 178 | rcond=None, 179 | )[0] 180 | return scons 181 | 182 | def _update(self, feval=True): 183 | x = self.get_x() 184 | new_point = True 185 | if self.curr['x'] is not None and np.all(x == self.curr['x']): 186 | if feval and self.curr['f'] is None: 187 | new_point = False 188 | else: 189 | return False 190 | drdx, Ucons, Unred, Ufree = self._calc_basis() 191 | 192 | if feval: 193 | f, g = self.eval() 194 | else: 195 | f = None 196 | g = None 197 | 198 | if new_point: 199 | self.last = self.curr.copy() 200 | 201 | self.curr['x'] = x 202 | self.curr['f'] = f 203 | self.curr['g'] = g 204 | self._update_basis() 205 | return True 206 | 207 | def _update_basis(self): 208 | drdx, Ucons, Unred, Ufree = self._calc_basis() 209 | self.curr['drdx'] = drdx 210 | self.curr['Ucons'] = Ucons 211 | self.curr['Unred'] = Unred 212 | self.curr['Ufree'] = Ufree 213 | 214 | if self.curr['g'] is None: 215 | L = None 216 | else: 217 | L = np.linalg.lstsq(drdx.T, self.curr['g'], rcond=None)[0] 218 | self.curr['L'] = L 219 | 220 | def _update_H(self, dx, dg): 221 | if self.last['x'] is None or self.last['g'] is None: 222 | return 223 | self.H.update(dx, dg) 224 | 225 | def get_f(self): 226 | self._update() 227 | return self.curr['f'] 228 | 229 | def get_g(self): 230 | self._update() 231 | return self.curr['g'].copy() 232 | 233 | def get_Unred(self): 234 | self._update(False) 235 | return self.curr['Unred'] 236 | 237 | def get_Ufree(self): 238 | self._update(False) 239 | return self.curr['Ufree'] 240 | 241 | def get_Ucons(self): 242 | self._update(False) 243 | return self.curr['Ucons'] 244 | 245 | def diag(self, gamma=0.1, threepoint=False, maxiter=None): 246 | if self.curr['f'] is None: 247 | self._update(feval=True) 248 | 249 | Ufree = self.get_Ufree() 250 | nfree = Ufree.shape[1] 251 | 252 | P = self.get_HL().project(Ufree) 253 | 254 | if P.B is None or self.first_diag: 255 | v0 = self.v0 256 | if v0 is None: 257 | v0 = self.get_g() @ Ufree 258 | else: 259 | v0 = None 260 | 261 | if P.B is None: 262 | P = np.eye(nfree) 263 | else: 264 | P = P.asarray() 265 | 266 | Hproj = NumericalHessian(self._calc_eg, self.get_x(), self.get_g(), 267 | self.eta, threepoint, Ufree) 268 | Hc = self.get_Hc() 269 | rayleigh_ritz(Hproj - Ufree.T @ Hc @ Ufree, gamma, P, v0=v0, 270 | method=self.eigensolver, 271 | maxiter=maxiter) 272 | 273 | # Extract eigensolver iterates 274 | Vs = Hproj.Vs 275 | AVs = Hproj.AVs 276 | 277 | # Re-calculate Ritz vectors 278 | Atilde = Vs.T @ symmetrize_Y(Vs, AVs, symm=2) - Vs.T @ Hc @ Vs 279 | _, X = eigh(Atilde) 280 | 281 | # Rotate Vs and AVs into X 282 | Vs = Vs @ X 283 | AVs = AVs @ X 284 | 285 | # Update the approximate Hessian 286 | self.H.update(Vs, AVs) 287 | 288 | self.first_diag = False 289 | 290 | # FIXME: temporary functions for backwards compatibility 291 | def get_projected_forces(self): 292 | """Returns Nx3 array of atomic forces orthogonal to constraints.""" 293 | g = self.get_g() 294 | Ufree = self.get_Ufree() 295 | return -((Ufree @ Ufree.T) @ g).reshape((-1, 3)) 296 | 297 | def converged(self, fmax, cmax=1e-5): 298 | fmax1 = np.linalg.norm(self.get_projected_forces(), axis=1).max() 299 | cmax1 = np.linalg.norm(self.get_res()) 300 | conv = (fmax1 < fmax) and (cmax1 < cmax) 301 | return conv, fmax1, cmax1 302 | 303 | def wrap_dx(self, dx): 304 | return dx 305 | 306 | def get_df_pred(self, dx, g, H): 307 | if H is None: 308 | return None 309 | return g.T @ dx + (dx.T @ H @ dx) / 2. 310 | 311 | def kick(self, dx, diag=False, **diag_kwargs): 312 | x0 = self.get_x() 313 | f0 = self.get_f() 314 | g0 = self.get_g() 315 | B0 = self.H.asarray() 316 | 317 | dx_initial, dx_final, g_par = self.set_x(x0 + dx) 318 | 319 | df_pred = self.get_df_pred(dx_initial, g0, B0) 320 | dg_actual = self.get_g() - g_par 321 | df_actual = self.get_f() - f0 322 | if df_pred is None: 323 | ratio = None 324 | else: 325 | ratio = df_actual / df_pred 326 | 327 | self._update_H(dx_final, dg_actual) 328 | 329 | if diag: 330 | if self.hessian_function is not None: 331 | self.calculate_hessian() 332 | else: 333 | self.diag(**diag_kwargs) 334 | 335 | return ratio 336 | 337 | def calculate_hessian(self): 338 | assert self.hessian_function is not None 339 | self.H.set_B(self.hessian_function(self.atoms)) 340 | 341 | 342 | class InternalPES(PES): 343 | def __init__( 344 | self, 345 | atoms: Atoms, 346 | internals: Internals, 347 | *args, 348 | H0: np.ndarray = None, 349 | iterative_stepper: int = 0, 350 | auto_find_internals: bool = True, 351 | **kwargs 352 | ): 353 | self.int_orig = internals 354 | new_int = internals.copy() 355 | if auto_find_internals: 356 | new_int.find_all_bonds() 357 | new_int.find_all_angles() 358 | new_int.find_all_dihedrals() 359 | new_int.validate_basis() 360 | 361 | PES.__init__( 362 | self, 363 | atoms, 364 | *args, 365 | constraints=new_int.cons, 366 | H0=None, 367 | proj_trans=False, 368 | proj_rot=False, 369 | **kwargs 370 | ) 371 | 372 | self.int = new_int 373 | self.dummies = self.int.dummies 374 | self.dim = len(self.get_x()) 375 | self.ncart = self.int.ndof 376 | if H0 is None: 377 | # Construct guess hessian and zero out components in 378 | # infeasible subspace 379 | B = self.int.jacobian() 380 | P = B @ np.linalg.pinv(B) 381 | H0 = P @ self.int.guess_hessian() @ P 382 | self.set_H(H0, initialized=False) 383 | else: 384 | self.set_H(H0, initialized=True) 385 | 386 | # Flag used to indicate that new internal coordinates are required 387 | self.bad_int = None 388 | self.iterative_stepper = iterative_stepper 389 | 390 | dpos = property(lambda self: self.dummies.positions.copy()) 391 | 392 | def _set_x_iterative(self, target): 393 | pos0 = self.atoms.positions.copy() 394 | dpos0 = self.dummies.positions.copy() 395 | pos1 = None 396 | dpos1 = None 397 | x0 = self.get_x() 398 | dx_initial = target - x0 399 | g0 = np.linalg.lstsq( 400 | self.int.jacobian(), 401 | self.curr.get('g', np.zeros_like(dx_initial)), 402 | rcond=None, 403 | )[0] 404 | for _ in range(10): 405 | dx = np.linalg.lstsq( 406 | self.int.jacobian(), 407 | self.wrap_dx(target - self.get_x()), 408 | rcond=None, 409 | )[0].reshape((-1, 3)) 410 | if np.sqrt((dx**2).sum() / len(dx)) < 1e-6: 411 | break 412 | self.atoms.positions += dx[:len(self.atoms)] 413 | self.dummies.positions += dx[len(self.atoms):] 414 | if pos1 is None: 415 | pos1 = self.atoms.positions.copy() 416 | dpos1 = self.dummies.positions.copy() 417 | else: 418 | print('Iterative stepper failed!') 419 | if self.iterative_stepper == 2: 420 | self.atoms.positions = pos0 421 | self.dummies.positions = dpos0 422 | return 423 | self.atoms.positions = pos1 424 | self.dummies.positions = dpos1 425 | dx_final = self.get_x() - x0 426 | g_final = self.int.jacobian() @ g0 427 | return dx_initial, dx_final, g_final 428 | 429 | # Position getter/setter 430 | def set_x(self, target): 431 | if self.iterative_stepper: 432 | res = self._set_x_iterative(target) 433 | if res is not None: 434 | return res 435 | dx = target - self.get_x() 436 | 437 | t0 = 0. 438 | Binv = np.linalg.pinv(self.int.jacobian()) 439 | y0 = np.hstack((self.apos.ravel(), self.dpos.ravel(), 440 | Binv @ dx, 441 | Binv @ self.curr.get('g', np.zeros_like(dx)))) 442 | ode = LSODA(self._q_ode, t0, y0, t_bound=1., atol=1e-6) 443 | 444 | while ode.status == 'running': 445 | ode.step() 446 | y = ode.y 447 | t0 = ode.t 448 | self.bad_int = self.int.check_for_bad_internals() 449 | if self.bad_int is not None: 450 | print('Bad internals found!') 451 | break 452 | if ode.nfev > 1000: 453 | view(self.atoms + self.dummies) 454 | raise RuntimeError("Geometry update ODE is taking too long " 455 | "to converge!") 456 | 457 | if ode.status == 'failed': 458 | raise RuntimeError("Geometry update ODE failed to converge!") 459 | 460 | nxa = 3 * len(self.atoms) 461 | nxd = 3 * len(self.dummies) 462 | y = y.reshape((3, nxa + nxd)) 463 | self.atoms.positions = y[0, :nxa].reshape((-1, 3)) 464 | self.dummies.positions = y[0, nxa:].reshape((-1, 3)) 465 | B = self.int.jacobian() 466 | dx_final = t0 * B @ y[1] 467 | g_final = B @ y[2] 468 | dx_initial = t0 * dx 469 | return dx_initial, dx_final, g_final 470 | 471 | def get_x(self): 472 | return self.int.calc() 473 | 474 | # Hessian of the constraints 475 | def get_Hc(self): 476 | D_cons = self.cons.hessian().ldot(self.curr['L']) 477 | B_int = self.int.jacobian() 478 | Binv_int = np.linalg.pinv(B_int) 479 | B_cons = self.cons.jacobian() 480 | L_int = self.curr['L'] @ B_cons @ Binv_int 481 | D_int = self.int.hessian().ldot(L_int) 482 | Hc = Binv_int.T @ (D_cons - D_int) @ Binv_int 483 | return Hc 484 | 485 | def get_drdx(self): 486 | # dr/dq = dr/dx dx/dq 487 | return PES.get_drdx(self) @ np.linalg.pinv(self.int.jacobian()) 488 | 489 | def _calc_basis(self, internal=None, cons=None): 490 | if internal is None: 491 | internal = self.int 492 | if cons is None: 493 | cons = self.cons 494 | B = internal.jacobian() 495 | Ui, Si, VTi = np.linalg.svd(B) 496 | nnred = np.sum(Si > 1e-6) 497 | Unred = Ui[:, :nnred] 498 | Vnred = VTi[:nnred].T 499 | Siinv = np.diag(1 / Si[:nnred]) 500 | drdxnred = cons.jacobian() @ Vnred @ Siinv 501 | drdx = drdxnred @ Unred.T 502 | Uc, Sc, VTc = np.linalg.svd(drdxnred) 503 | ncons = np.sum(Sc > 1e-6) 504 | Ucons = Unred @ VTc[:ncons].T 505 | Ufree = Unred @ VTc[ncons:].T 506 | return drdx, Ucons, Unred, Ufree 507 | 508 | def eval(self): 509 | f, g_cart = PES.eval(self) 510 | Binv = np.linalg.pinv(self.int.jacobian()) 511 | return f, g_cart @ Binv[:len(g_cart)] 512 | 513 | def update_internals(self, dx): 514 | self._update(True) 515 | 516 | nold = 3 * (len(self.atoms) + len(self.dummies)) 517 | 518 | # FIXME: Testing to see if disabling this works 519 | #if self.bad_int is not None: 520 | # for bond in self.bad_int['bonds']: 521 | # self.int_orig.forbid_bond(bond) 522 | # for angle in self.bad_int['angles']: 523 | # self.int_orig.forbid_angle(angle) 524 | 525 | # Find new internals, constraints, and dummies 526 | new_int = self.int_orig.copy() 527 | new_int.find_all_bonds() 528 | new_int.find_all_angles() 529 | new_int.find_all_dihedrals() 530 | new_int.validate_basis() 531 | new_cons = new_int.cons 532 | 533 | # Calculate B matrix and its inverse for new and old internals 534 | Blast = self.int.jacobian() 535 | B = new_int.jacobian() 536 | Binv = np.linalg.pinv(B) 537 | Dlast = self.int.hessian() 538 | D = new_int.hessian() 539 | 540 | # # Projection matrices 541 | # P2 = B[:, nold:] @ Binv[nold:, :] 542 | 543 | # Update the info in self.curr 544 | x = new_int.calc() 545 | g = -self.atoms.get_forces().ravel() @ Binv[:3*len(self.atoms)] 546 | drdx, Ucons, Unred, Ufree = self._calc_basis( 547 | internal=new_int, 548 | cons=new_cons, 549 | ) 550 | L = np.linalg.lstsq(drdx.T, g, rcond=None)[0] 551 | 552 | # Update H using old data where possible. For new (dummy) atoms, 553 | # use the guess hessian info. 554 | H = self.get_H().asarray() 555 | Hcart = Blast.T @ H @ Blast 556 | Hcart += Dlast.ldot(self.curr['g']) 557 | Hnew = Binv.T[:, :nold] @ (Hcart - D.ldot(g)) @ Binv 558 | self.dim = len(x) 559 | self.set_H(Hnew) 560 | 561 | self.int = new_int 562 | self.cons = new_cons 563 | 564 | self.curr.update(x=x, g=g, drdx=drdx, Ufree=Ufree, 565 | Unred=Unred, Ucons=Ucons, L=L, B=B, Binv=Binv) 566 | 567 | def get_df_pred(self, dx, g, H): 568 | if H is None: 569 | return None 570 | Unred = self.get_Unred() 571 | dx_r = dx @ Unred 572 | # dx_r = self.wrap_dx(dx) @ Unred 573 | g_r = g @ Unred 574 | H_r = Unred.T @ H @ Unred 575 | return g_r.T @ dx_r + (dx_r.T @ H_r @ dx_r) / 2. 576 | 577 | # FIXME: temporary functions for backwards compatibility 578 | def get_projected_forces(self): 579 | """Returns Nx3 array of atomic forces orthogonal to constraints.""" 580 | g = self.get_g() 581 | Ufree = self.get_Ufree() 582 | B = self.int.jacobian() 583 | return -((Ufree @ Ufree.T) @ g @ B).reshape((-1, 3)) 584 | 585 | def wrap_dx(self, dx): 586 | return self.int.wrap(dx) 587 | 588 | # x setter aux functions 589 | def _q_ode(self, t, y): 590 | nxa = 3 * len(self.atoms) 591 | nxd = 3 * len(self.dummies) 592 | x, dxdt, g = y.reshape((3, nxa + nxd)) 593 | 594 | dydt = np.zeros((3, nxa + nxd)) 595 | dydt[0] = dxdt 596 | 597 | self.atoms.positions = x[:nxa].reshape((-1, 3)).copy() 598 | self.dummies.positions = x[nxa:].reshape((-1, 3)).copy() 599 | 600 | D = self.int.hessian() 601 | Binv = np.linalg.pinv(self.int.jacobian()) 602 | D_tmp = -Binv @ D.rdot(dxdt) 603 | dydt[1] = D_tmp @ dxdt 604 | dydt[2] = D_tmp @ g 605 | 606 | return dydt.ravel() 607 | 608 | def kick(self, dx, diag=False, **diag_kwargs): 609 | ratio = PES.kick(self, dx, diag=diag, **diag_kwargs) 610 | 611 | # FIXME: Testing to see if this works 612 | #if self.bad_int is not None: 613 | # self.update_internals(dx) 614 | # self.bad_int = None 615 | 616 | return ratio 617 | 618 | def write_traj(self): 619 | if self.traj is not None: 620 | energy = self.atoms.calc.results['energy'] 621 | forces = np.zeros((len(self.atoms) + len(self.dummies), 3)) 622 | forces[:len(self.atoms)] = self.atoms.calc.results['forces'] 623 | atoms_tmp = self.atoms + self.dummies 624 | atoms_tmp.calc = SinglePointCalculator(atoms_tmp, energy=energy, 625 | forces=forces) 626 | self.traj.write(atoms_tmp) 627 | 628 | def _update(self, feval=True): 629 | if not PES._update(self, feval=feval): 630 | return 631 | 632 | B = self.int.jacobian() 633 | Binv = np.linalg.pinv(B) 634 | self.curr.update(B=B, Binv=Binv) 635 | return True 636 | 637 | def _convert_cartesian_hessian_to_internal( 638 | self, 639 | Hcart: np.ndarray, 640 | ) -> np.ndarray: 641 | ncart = 3 * len(self.atoms) 642 | # Get Jacobian and calculate redundant and non-redundant spaces 643 | B = self.int.jacobian()[:, :ncart] 644 | Ui, Si, VTi = np.linalg.svd(B) 645 | nnred = np.sum(Si > 1e-6) 646 | Unred = Ui[:, :nnred] 647 | Ured = Ui[:, nnred:] 648 | 649 | # Calculate inverse Jacobian in non-redundant space 650 | Bnred_inv = VTi[:nnred].T @ np.diag(1 / Si[:nnred]) 651 | 652 | # Convert Cartesian Hessian to non-redundant internal Hessian 653 | Hcart_coupled = self.int.hessian().ldot(self.get_g())[:ncart, :ncart] 654 | Hcart_corr = Hcart - Hcart_coupled 655 | Hnred = Bnred_inv.T @ Hcart_corr @ Bnred_inv 656 | 657 | # Find eigenvalues of non-redundant internal Hessian 658 | lnred, _ = np.linalg.eigh(Hnred) 659 | 660 | # The redundant part of the Hessian will be initialized to the 661 | # geometric mean of the non-redundant eigenvalues 662 | lnred_mean = np.exp(np.log(np.abs(lnred)).mean()) 663 | 664 | # finish reconstructing redundant internal Hessian 665 | return Unred @ Hnred @ Unred.T + lnred_mean * Ured @ Ured.T 666 | 667 | def _convert_internal_hessian_to_cartesian( 668 | self, 669 | Hint: np.ndarray, 670 | ) -> np.ndarray: 671 | B = self.int.jacobian() 672 | return B.T @ Hint @ B + self.int.hessian().ldot(self.get_g()) 673 | 674 | def calculate_hessian(self): 675 | assert self.hessian_function is not None 676 | self.H.set_B(self._convert_cartesian_hessian_to_internal( 677 | self.hessian_function(self.atoms) 678 | )) 679 | -------------------------------------------------------------------------------- /sella/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sella/samd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import division 4 | 5 | import numpy as np 6 | from ase.units import kB 7 | 8 | def T_linear(i, T0, Tf, n): 9 | return T0 + i * (Tf - T0) / (n - 1) 10 | 11 | def T_exp(i, T0, Tf, n): 12 | return T0 * (Tf / T0)**(i/n) 13 | 14 | def bdp(func, x0, ngen, T0, Tf, dt, tau, *args, schedule=T_linear, v0=None, **kwargs): 15 | d = len(x0) 16 | 17 | x = x0.copy() 18 | f, g = func(x, *args, **kwargs) 19 | 20 | if v0 is None: 21 | v = np.random.normal(scale=np.sqrt(2 * T0), size=d) 22 | else: 23 | v = v0.copy() 24 | 25 | edttau = np.exp(-dt / tau) 26 | edttau2 = np.exp(-dt / (2 * tau)) 27 | 28 | for i in range(ngen): 29 | old_f = f 30 | old_g = g.copy() 31 | 32 | x += dt * v - 0.5 * dt**2 * g 33 | f, g = func(x, *args, **kwargs) 34 | 35 | v -= 0.5 * dt * (g + old_g) 36 | 37 | T = schedule(i, T0, Tf, ngen) 38 | K_target = d * T / 2. 39 | K = np.sum(v**2) / 2. 40 | R = np.random.normal(size=d) 41 | alpha2 = edttau + K * (1 - edttau) * np.sum(R**2) / (d * K) + 2 * edttau2 * np.sqrt(K_target * (1 - edttau) / (d * K)) * R[0] 42 | v *= np.sqrt(alpha2) 43 | print(np.average(v**2) / kB, T / kB) 44 | return x 45 | 46 | def velocity_rescaling(func, x0, ngen, T0, Tf, dt, *args, schedule=T_linear, v0=None, **kwargs): 47 | d = len(x0) 48 | 49 | x = x0.copy() 50 | f, g = func(x, *args, **kwargs) 51 | 52 | if v0 is None: 53 | v = np.random.normal(scale=np.sqrt(2 * T0), size=d) 54 | else: 55 | v = v0.copy() 56 | 57 | for i in range(ngen): 58 | old_f = f 59 | old_g = g.copy() 60 | 61 | x += dt * v - 0.5 * dt**2 * g 62 | f, g = func(x, *args, **kwargs) 63 | 64 | v -= 0.5 * dt * (g + old_g) 65 | 66 | T = schedule(i, T0, Tf, ngen) 67 | K_target = d * T / 2. 68 | K = np.sum(v**2) / 2. 69 | 70 | v *= np.sqrt(K_target / K) 71 | print(np.average(v**2) / kB, T / kB) 72 | 73 | return x 74 | 75 | def csvr(func, x0, ngen, T0, Tf, dt, *args, schedule=T_linear, v0=None, **kwargs): 76 | d = len(x0) 77 | 78 | x = x0.copy() 79 | f, g = func(x, *args, **kwargs) 80 | 81 | if v0 is None: 82 | v = np.random.normal(scale=np.sqrt(2 * T0), size=d) 83 | else: 84 | v = v0.copy() 85 | 86 | for i in range(ngen): 87 | old_f = f 88 | old_g = g.copy() 89 | 90 | x += dt * v - 0.5 * dt**2 * g 91 | f, g = func(x, *args, **kwargs) 92 | 93 | v -= 0.5 * dt * (g + old_g) 94 | 95 | T = schedule(i, T0, Tf, ngen) 96 | K_target = np.random.gamma(d/2, T) 97 | K = np.sum(v**2) / 2. 98 | 99 | v *= np.sqrt(K_target / K) 100 | print(np.average(v**2) / kB, T / kB) 101 | 102 | return x 103 | -------------------------------------------------------------------------------- /sella/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zadorlab/sella/4d5412b8ac854038cdc6b100b437eba7cbf96f56/sella/utilities/__init__.py -------------------------------------------------------------------------------- /sella/utilities/blas.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | from libc.math cimport INFINITY 4 | 5 | cdef double my_dasum(double[:] x) nogil 6 | 7 | cdef int my_dcopy(double[:] x, double[:] y) nogil except -1 8 | 9 | cdef double my_ddot(double[:] x, double[:] y) nogil except INFINITY 10 | 11 | cdef double my_dnrm2(double[:] x) nogil 12 | 13 | cdef void my_dscal(double alpha, double[:] x) nogil 14 | 15 | cdef int my_dswap(double[:] x, double[:] y) nogil except -1 16 | 17 | cdef int my_daxpy(double scale, double[:] x, double[:] y) nogil except -1 18 | 19 | cdef int my_dgemv(double[:, :] A, double[:] x, double[:] y, 20 | double alpha=?, double beta=?) nogil except -1 21 | 22 | cdef int my_dgemm(double[:, :] A, double[:, :] B, double[:, :] C, 23 | double alpha=?, double beta=?) nogil except -1 24 | 25 | cdef int my_dger(double[:, :] A, double[:] x, double[:] y, 26 | double alpha=?) nogil except -1 27 | 28 | -------------------------------------------------------------------------------- /sella/utilities/blas.pyx: -------------------------------------------------------------------------------- 1 | from libc.math cimport INFINITY 2 | from scipy.linalg.cython_blas cimport (dasum, daxpy, dcopy, ddot, dnrm2, 3 | dscal, dswap, dgemv, dgemm, dger) 4 | 5 | cdef double my_dasum(double[:] x) nogil: 6 | """Returns sum(x)""" 7 | cdef int n = x.shape[0] 8 | cdef int sdx = x.strides[0] >> 3 9 | return dasum(&n, &x[0], &sdx) 10 | 11 | cdef int my_daxpy(double scale, double[:] x, double[:] y) nogil except -1: 12 | """Evaluates y = y + scale * x""" 13 | cdef int n = len(x) 14 | if len(y) != n: 15 | return -1 16 | cdef int sdx = x.strides[0] >> 3 17 | cdef int sdy = y.strides[0] >> 3 18 | daxpy(&n, &scale, &x[0], &sdx, &y[0], &sdy) 19 | return 0 20 | 21 | cdef int my_dcopy(double[:] x, double[:] y) nogil except -1: 22 | """Copies contents of x into y""" 23 | cdef int n = x.shape[0] 24 | if n != y.shape[0]: 25 | return -1 26 | cdef int sdx = x.strides[0] >> 3 27 | cdef int sdy = y.strides[0] >> 3 28 | dcopy(&n, &x[0], &sdx, &y[0], &sdy) 29 | return 0 30 | 31 | cdef double my_ddot(double[:] x, double[:] y) nogil except INFINITY: 32 | """Calculators dot(x, y)""" 33 | cdef int n = x.shape[0] 34 | if n != y.shape[0]: 35 | return INFINITY 36 | cdef int sdx = x.strides[0] >> 3 37 | cdef int sdy = y.strides[0] >> 3 38 | return ddot(&n, &x[0], &sdx, &y[0], &sdy) 39 | 40 | cdef double my_dnrm2(double[:] x) nogil: 41 | """Calculators 2-norm of x""" 42 | cdef int n = x.shape[0] 43 | cdef int sdx = x.strides[0] >> 3 44 | return dnrm2(&n, &x[0], &sdx) 45 | 46 | cdef void my_dscal(double alpha, double[:] x) nogil: 47 | """Multiplies x by scalar alpha""" 48 | cdef int n = x.shape[0] 49 | cdef int sdx = x.strides[0] >> 3 50 | dscal(&n, &alpha, &x[0], &sdx) 51 | 52 | cdef int my_dswap(double[:] x, double[:] y) nogil except -1: 53 | """Swaps contents of x and y""" 54 | cdef int n = x.shape[0] 55 | if n != y.shape[0]: 56 | return -1 57 | cdef int sdx = x.strides[0] >> 3 58 | cdef int sdy = y.strides[0] >> 3 59 | dswap(&n, &x[0], &sdx, &y[0], &sdy) 60 | return 0 61 | 62 | cdef int my_dgemv(double[:, :] A, double[:] x, double[:] y, 63 | double alpha=1., double beta=1.) nogil except -1: 64 | """Evaluates y = alpha * A @ x + beta * y""" 65 | cdef int ma, na, nx, my 66 | ma, na = A.shape[:2] 67 | nx = x.shape[0] 68 | my = y.shape[0] 69 | if ma != my: 70 | return -1 71 | if na != nx: 72 | return -1 73 | cdef int lda = A.strides[0] >> 3 74 | cdef int sda = A.strides[1] >> 3 75 | cdef int sdx = x.strides[0] >> 3 76 | cdef int sdy = y.strides[0] >> 3 77 | cdef char* trans = 'N' 78 | if lda > sda: 79 | lda, sda = sda, lda 80 | ma, na = na, ma 81 | trans = 'T' 82 | dgemv(trans, &ma, &na, &alpha, &A[0, 0], &sda, &x[0], &sdx, &beta, &y[0], 83 | &sdy) 84 | return 0 85 | 86 | cdef int my_dgemm(double[:, :] A, double[:, :] B, double[:, :] C, 87 | double alpha=1., double beta=1.) nogil except -1: 88 | """Evaluates C = alpha * A @ B + beta * C""" 89 | cdef int ma, na, mb, nb, mc, nc 90 | ma, na = A.shape[:2] 91 | mb, nb = B.shape[:2] 92 | mc, nc = C.shape[:2] 93 | if na != mb or ma != mc or nb != nc: 94 | return -1 95 | 96 | cdef int ldc = C.strides[0] >> 3 97 | cdef int sdc = C.strides[1] >> 3 98 | 99 | if ldc > sdc: 100 | return my_dgemm(B.T, A.T, C.T, alpha=alpha, beta=beta) 101 | 102 | cdef char* transa = 'N' 103 | cdef char* transb = 'N' 104 | 105 | cdef int lda = A.strides[0] >> 3 106 | cdef int sda = A.strides[1] >> 3 107 | cdef int ldb = B.strides[0] >> 3 108 | cdef int sdb = B.strides[1] >> 3 109 | 110 | if (lda > sda): 111 | transa = 'T' 112 | lda, sda = sda, lda 113 | 114 | if (ldb > sdb): 115 | transb = 'T' 116 | ldb, sdb = sdb, ldb 117 | 118 | dgemm(transa, transb, &mc, &nc, &na, &alpha, &A[0, 0], &sda, &B[0, 0], 119 | &sdb, &beta, &C[0, 0], &sdc) 120 | return 0 121 | 122 | cdef int my_dger(double[:, :] A, double[:] x, double[:] y, 123 | double alpha=1.) nogil except -1: 124 | """Evaluates A = A + alpha * outer(x, y)""" 125 | cdef int lda = A.strides[0] >> 3 126 | cdef int sda = A.strides[1] >> 3 127 | cdef int ma, na 128 | ma, na = A.shape[:2] 129 | cdef int mx = x.shape[0] 130 | cdef int ny = y.shape[0] 131 | if ma != mx or na != ny: 132 | return -1 133 | 134 | if lda > sda: 135 | lda, sda = sda, lda 136 | x, y = y, x 137 | ma, na = na, ma 138 | 139 | cdef int sdx = x.strides[0] >> 3 140 | cdef int sdy = y.strides[0] >> 3 141 | 142 | 143 | dger(&ma, &na, &alpha, &x[0], &sdx, &y[0], &sdy, &A[0, 0], &sda) 144 | return 0 145 | -------------------------------------------------------------------------------- /sella/utilities/math.pxd: -------------------------------------------------------------------------------- 1 | # cython: language_level=3 2 | 3 | cdef int normalize(double[:] x) nogil 4 | 5 | cdef int vec_sum(double[:] x, double[:] y, double[:] z, double scale=?) nogil 6 | 7 | cdef void cross(double[:] x, double[:] y, double[:] z) nogil 8 | 9 | cdef void symmetrize(double* X, size_t n, size_t lda) nogil 10 | 11 | cdef void skew(double[:] x, double[:, :] Y, double scale=?) nogil 12 | 13 | cdef int mgs(double[:, :] X, double[:, :] Y=?, double eps1=?, 14 | double eps2=?, int maxiter=?) nogil 15 | 16 | cdef int mppi(int n, int m, double[:, :] A, double[:, :] U, double[:, :] VT, 17 | double[:] s, double[:, :] Ainv, double[:] work, 18 | double eps=?) nogil 19 | -------------------------------------------------------------------------------- /sella/utilities/math.pyx: -------------------------------------------------------------------------------- 1 | from libc.math cimport sqrt, fabs, INFINITY 2 | from libc.string cimport memset 3 | from scipy.linalg.cython_blas cimport daxpy, dnrm2, dcopy, dgemv, ddot, dger 4 | from scipy.linalg.cython_lapack cimport dgesvd 5 | 6 | import numpy as np 7 | 8 | cdef int UNITY = 1 9 | 10 | cdef double DUNITY = 1. 11 | cdef double DZERO = 0. 12 | 13 | 14 | cdef inline int normalize(double[:] x) nogil: 15 | """Normalizes a vector in place""" 16 | cdef int n = len(x) 17 | cdef int sdx = x.strides[0] >> 3 18 | cdef double norm = dnrm2(&n, &x[0], &sdx) 19 | cdef int i 20 | for i in range(n): 21 | x[i] /= norm 22 | return 0 23 | 24 | 25 | cdef inline int vec_sum(double[:] x, double[:] y, double[:] z, 26 | double scale=1.) nogil: 27 | """Evaluates z[:] = x[:] + scale * y[:]""" 28 | cdef int n = len(x) 29 | if len(y) != n: 30 | return -1 31 | if len(z) != n: 32 | return -1 33 | cdef int sdx = x.strides[0] >> 3 34 | cdef int sdy = y.strides[0] >> 3 35 | cdef int sdz = z.strides[0] >> 3 36 | dcopy(&n, &x[0], &sdx, &z[0], &sdz) 37 | daxpy(&n, &scale, &y[0], &sdy, &z[0], &sdz) 38 | 39 | 40 | cdef inline void cross(double[:] x, double[:] y, double[:] z) nogil: 41 | """Evaluates z[:] = x[:] cross y[:]""" 42 | z[0] = x[1] * y[2] - y[1] * x[2] 43 | z[1] = x[2] * y[0] - y[2] * x[0] 44 | z[2] = x[0] * y[1] - y[0] * x[1] 45 | 46 | 47 | cdef inline void symmetrize(double* X, size_t n, size_t lda) nogil: 48 | """Symmetrizes matrix X by populating the lower triangle with the 49 | contents of the upper triangle""" 50 | cdef size_t i, j 51 | for i in range(max(0, n - 1)): 52 | for j in range(i + 1, n): 53 | X[j * lda + i] = X[i * lda + j] 54 | 55 | 56 | cdef inline void skew(double[:] x, double[:, :] Y, double scale=1.) nogil: 57 | """Fillx matrix Y with the elements of vectors scale * x such that 58 | Y becomes a skew-symmetric matrix""" 59 | # Safer than memset, because this might be a slice of a larger array 60 | cdef int i, j 61 | for i in range(3): 62 | for j in range(3): 63 | Y[i, j] = 0. 64 | 65 | Y[2, 1] = scale * x[0] 66 | Y[0, 2] = scale * x[1] 67 | Y[1, 0] = scale * x[2] 68 | 69 | Y[1, 2] = -Y[2, 1] 70 | Y[2, 0] = -Y[0, 2] 71 | Y[0, 1] = -Y[1, 0] 72 | 73 | 74 | cdef int mgs(double[:, :] X, double[:, :] Y=None, double eps1=1e-15, 75 | double eps2=1e-6, int maxiter=100) nogil: 76 | """Orthonormalizes X in-place against itself and Y. To accomplish this, Y 77 | is first orthonormalized in-place.""" 78 | 79 | cdef int n = X.shape[0] 80 | cdef int nx = X.shape[1] 81 | cdef int sdx = X.strides[0] >> 3 82 | 83 | cdef int ny 84 | cdef int sdy 85 | if Y is None: 86 | ny = 0 87 | sdy = -1 88 | else: 89 | if Y.shape[0] != n: 90 | return -1 91 | ny = Y.shape[1] 92 | sdy = Y.strides[0] >> 3 93 | 94 | cdef double norm, normtot, dot 95 | 96 | # Now orthonormalize X 97 | cdef int m = 0 98 | cdef int niter 99 | for i in range(nx): 100 | if i != m: 101 | X[:, m] = X[:, i] 102 | norm = dnrm2(&n, &X[0, m], &sdx) 103 | for k in range(n): 104 | X[k, m] /= norm 105 | for niter in range(maxiter): 106 | normtot = 1. 107 | for j in range(ny): 108 | dot = -ddot(&n, &Y[0, j], &sdy, &X[0, m], &sdx) 109 | daxpy(&n, &dot, &Y[0, j], &sdy, &X[0, m], &sdx) 110 | norm = dnrm2(&n, &X[0, m], &sdx) 111 | normtot *= norm 112 | if normtot < eps2: 113 | break 114 | for k in range(n): 115 | X[k, m] /= norm 116 | if normtot < eps2: 117 | break 118 | for j in range(m): 119 | dot = -ddot(&n, &X[0, j], &sdx, &X[0, m], &sdx) 120 | daxpy(&n, &dot, &X[0, j], &sdx, &X[0, m], &sdx) 121 | norm = dnrm2(&n, &X[0, m], &sdx) 122 | normtot *= norm 123 | if normtot < eps2: 124 | break 125 | for k in range(n): 126 | X[k, m] /= norm 127 | if normtot < eps2: 128 | break 129 | elif 0. <= 1. - normtot <= eps1: 130 | m += 1 131 | break 132 | else: 133 | return -2 134 | 135 | # Just for good measure, zero out any leftover bits of X 136 | for i in range(m, nx): 137 | for k in range(n): 138 | X[k, i] = 0. 139 | 140 | return m 141 | 142 | 143 | def modified_gram_schmidt(Xin, Yin=None, eps1=1.e-15, eps2=1.e-6, 144 | maxiter=100): 145 | if Xin.shape[1] == 0: 146 | return Xin 147 | 148 | if Yin is not None: 149 | Yout = Yin.copy() 150 | ny = mgs(Yout, None, eps1=eps1, eps2=eps2, maxiter=maxiter) 151 | Yout = Yout[:, :ny] 152 | else: 153 | Yout = None 154 | 155 | Xout = Xin.copy() 156 | nx = mgs(Xout, Yout, eps1=eps1, eps2=eps2, maxiter=maxiter) 157 | if nx < 0: 158 | raise RuntimeError("MGS failed.") 159 | return Xout[:, :nx] 160 | 161 | 162 | cdef int mppi(int n, int m, double[:, :] A, double[:, :] U, double[:, :] VT, 163 | double[:] s, double[:, :] Ainv, double[:] work, 164 | double eps=1e-6) nogil: 165 | """Computes the Moore-Penrose pseudoinverse of A and stores the result in 166 | Ainv. This is done using singular value decomposition. Additionally, saves 167 | the right singular values in VT. A is then populated with the columns of 168 | VT that correspond to the null space of the singular vectors.""" 169 | if A.shape[0] < n: 170 | return -1 171 | if A.shape[1] < m: 172 | return -1 173 | 174 | cdef int minnm = min(n, m) 175 | cdef int lda = A.strides[0] >> 3 176 | cdef int ldu = U.strides[0] >> 3 177 | cdef int ldvt = VT.strides[0] >> 3 178 | 179 | cdef int ns = s.shape[0] 180 | if ns < minnm: 181 | return -1 182 | 183 | cdef int lwork = work.shape[0] 184 | cdef int info 185 | 186 | dgesvd('A', 'S', &m, &n, &A[0, 0], &lda, &s[0], &VT[0, 0], &ldvt, 187 | &U[0, 0], &ldu, &work[0], &lwork, &info) 188 | if info != 0: 189 | return -1 190 | 191 | memset(&Ainv[0, 0], 0, Ainv.shape[0] * Ainv.shape[1] * sizeof(double)) 192 | 193 | cdef int i 194 | cdef double sinv 195 | cdef int incvt = VT.strides[1] >> 3 196 | cdef int ldainv = Ainv.strides[0] >> 3 197 | cdef int nsing = 0 198 | 199 | # Evaluate the pseudo-inverse 200 | for i in range(minnm): 201 | if fabs(s[i]) < eps: 202 | continue 203 | nsing += 1 204 | sinv = 1. / s[i] 205 | dger(&n, &m, &sinv, &U[0, i], &ldu, &VT[i, 0], &incvt, 206 | &Ainv[0, 0], &ldainv) 207 | 208 | # Populate the basis matrices 209 | cdef int inca = A.strides[1] >> 3 210 | for i in range(m): 211 | dcopy(&m, &VT[i, 0], &incvt, &A[0, i], &lda) 212 | 213 | for i in range(m - nsing): 214 | dcopy(&m, &A[0, nsing + i], &lda, &VT[0, i], &ldvt) 215 | 216 | return nsing 217 | 218 | 219 | def pseudo_inverse(double[:, :] A, double eps=1e-6): 220 | cdef int n, m, minnm, maxnm 221 | n, m = A.shape[:2] 222 | minnm = min(n, m) 223 | maxnm = max(n, m) 224 | 225 | U = np.zeros((n, n), dtype=np.float64) 226 | VT = np.zeros((m, m), dtype=np.float64) 227 | s = np.zeros(min(n, m), dtype=np.float64) 228 | Ainv = np.zeros((m, n), dtype=np.float64) 229 | work = np.zeros(2 * max(3 * minnm + maxnm, 5 * minnm, 1)) 230 | 231 | nsing = mppi(n, m, A, U, VT, s, Ainv, work, eps=eps) 232 | 233 | if nsing == -1: 234 | raise RuntimeError("mmpi failed!") 235 | 236 | return U, s, VT, Ainv, nsing 237 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md 3 | 4 | [tool:pytest] 5 | testpaths = 6 | tests 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | 5 | import numpy as np 6 | 7 | from setuptools import setup, Extension, find_packages 8 | 9 | VERSION = '2.3.5' 10 | 11 | debug = '--debug' in sys.argv or '-g' in sys.argv 12 | 13 | try: 14 | from Cython.Build import cythonize 15 | except ImportError: 16 | use_cython = False 17 | 18 | else: 19 | use_cython = True 20 | 21 | cy_suff = '.pyx' if use_cython else '.c' 22 | 23 | cy_files = [ 24 | ['force_match'], 25 | ['utilities', 'blas'], 26 | ['utilities', 'math'], 27 | ] 28 | 29 | macros = [] 30 | if debug: 31 | macros.append(('CYTHON_TRACE_NOGIL', '1')) 32 | 33 | ext_modules = [] 34 | for cy_file in cy_files: 35 | ext_modules.append(Extension('.'.join(['sella', *cy_file]), 36 | [os.path.join('sella', *cy_file) + cy_suff], 37 | define_macros=macros, 38 | include_dirs=[np.get_include()])) 39 | 40 | if use_cython: 41 | compdir = dict(linetrace=debug, boundscheck=debug, language_level=3, 42 | wraparound=False, cdivision=True) 43 | ext_modules = cythonize(ext_modules, compiler_directives=compdir) 44 | 45 | with open('README.md', 'r') as f: 46 | long_description = f.read() 47 | 48 | with open('requirements.txt', 'r') as f: 49 | install_requires = f.read().strip().split() 50 | 51 | setup(name='Sella', 52 | version=VERSION, 53 | author='Eric Hermes', 54 | author_email='ehermes@sandia.gov', 55 | long_description=long_description, 56 | long_description_content_type='text/markdown', 57 | packages=find_packages(), 58 | ext_modules=ext_modules, 59 | include_dirs=[np.get_include()], 60 | classifiers=['Development Status :: 4 - Beta', 61 | 'Environment :: Console', 62 | 'Intended Audience :: Science/Research', 63 | 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', 64 | 'Natural Language :: English', 65 | 'Operating System :: OS Independent', 66 | 'Programming Language :: Python :: 3.8', 67 | 'Programming Language :: Cython', 68 | 'Topic :: Scientific/Engineering :: Chemistry', 69 | 'Topic :: Scientific/Engineering :: Mathematics', 70 | 'Topic :: Scientific/Engineering :: Physics'], 71 | python_requires='>=3.8', 72 | install_requires=install_requires, 73 | ) 74 | -------------------------------------------------------------------------------- /tests/integration/test_morse_cluster.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | from ase import Atoms 5 | from ase.calculators.morse import MorsePotential 6 | from ase.units import kB 7 | 8 | from sella import Sella, Constraints 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "internal,order", 13 | [ 14 | (False, 0), 15 | (False, 1), 16 | (True, 0), 17 | (True, 1), 18 | ], 19 | ) 20 | def test_morse_cluster(internal, order, trajectory=None): 21 | rng = np.random.RandomState(4) 22 | 23 | nat = 4 24 | atoms = Atoms(['Xe'] * nat, rng.normal(size=(nat, 3), scale=3.0)) 25 | # parameters from DOI: 10.1515/zna-1987-0505 26 | atoms.calc = MorsePotential(alpha=226.9 * kB, r0=4.73, rho0=4.73*1.099) 27 | 28 | cons = Constraints(atoms) 29 | cons.fix_translation() 30 | cons.fix_rotation() 31 | 32 | opt = Sella( 33 | atoms, 34 | order=order, 35 | internal=internal, 36 | trajectory=trajectory, 37 | gamma=1e-3, 38 | constraints=cons, 39 | ) 40 | opt.run(fmax=1e-3) 41 | 42 | Ufree = opt.pes.get_Ufree() 43 | np.testing.assert_allclose(opt.pes.get_g() @ Ufree, 0, atol=5e-3) 44 | opt.pes.diag(gamma=1e-16) 45 | H = opt.pes.get_HL().project(Ufree) 46 | assert np.sum(H.evals < 0) == order, H.evals 47 | -------------------------------------------------------------------------------- /tests/integration/test_tip3p_cluster.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | import pytest 3 | import numpy as np 4 | 5 | from ase import Atoms 6 | from ase.build import molecule 7 | from ase.calculators.tip3p import TIP3P, rOH, angleHOH 8 | 9 | from sella import Sella, Constraints, Internals 10 | from sella.internal import DuplicateConstraintError 11 | 12 | water = molecule('H2O') 13 | water.set_distance(0, 1, rOH) 14 | water.set_distance(0, 2, rOH) 15 | water.set_angle(1, 0, 2, angleHOH) 16 | a = 3.106162559099496 17 | rng = np.random.RandomState(0) 18 | 19 | atoms_ref = Atoms() 20 | for offsets in product(*((0, 1),) * 3): 21 | atoms = water.copy() 22 | for axis in ['x', 'y', 'z']: 23 | atoms.rotate(rng.random() * 360, axis) 24 | atoms.positions += a * np.asarray(offsets) 25 | atoms_ref += atoms 26 | 27 | 28 | @pytest.mark.parametrize("internal,order", 29 | [(True, 0), 30 | (False, 0), 31 | (True, 1), 32 | (False, 1), 33 | ]) 34 | def test_water_dimer(internal, order): 35 | internal = True 36 | order = 0 37 | rng = np.random.RandomState(1) 38 | 39 | atoms = atoms_ref.copy() 40 | atoms.calc = TIP3P() 41 | atoms.rattle(0.01, rng=rng) 42 | 43 | nwater = len(atoms) // 3 44 | cons = Constraints(atoms) 45 | for i in range(nwater): 46 | cons.fix_bond((3 * i, 3 * i + 1), target=rOH) 47 | cons.fix_bond((3 * i, 3 * i + 2), target=rOH) 48 | cons.fix_angle((3 * i + 1, 3 * i, 3 * i + 2), target=angleHOH) 49 | 50 | # Remove net translation and rotation 51 | try: 52 | cons.fix_translation() 53 | except DuplicateConstraintError: 54 | pass 55 | try: 56 | cons.fix_rotation() 57 | except DuplicateConstraintError: 58 | pass 59 | 60 | sella_kwargs = dict( 61 | order=order, 62 | trajectory='test.traj', 63 | eta=1e-6, 64 | delta0=1e-2, 65 | ) 66 | if internal: 67 | sella_kwargs['internal'] = Internals( 68 | atoms, cons=cons, allow_fragments=True 69 | ) 70 | else: 71 | sella_kwargs['constraints'] = cons 72 | opt = Sella(atoms, **sella_kwargs) 73 | 74 | opt.delta = 0.05 75 | opt.run(fmax=1e-3) 76 | print("First run done") 77 | 78 | atoms.rattle() 79 | opt.run(fmax=1e-3) 80 | 81 | Ufree = opt.pes.get_Ufree() 82 | g = opt.pes.get_g() @ Ufree 83 | np.testing.assert_allclose(g, 0, atol=1e-3) 84 | opt.pes.diag(gamma=1e-16) 85 | H = opt.pes.get_HL().project(Ufree) 86 | assert np.sum(H.evals < 0) == order, H.evals 87 | -------------------------------------------------------------------------------- /tests/internal/test_get_internal.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | from ase.build import molecule 5 | 6 | from sella.internal import Internals 7 | 8 | 9 | def res(pos: np.ndarray, internal: Internals) -> np.ndarray: 10 | internal.atoms.positions = pos.reshape((-1, 3)) 11 | return internal.calc() 12 | 13 | 14 | def jacobian(pos: np.ndarray, internal: Internals) -> np.ndarray: 15 | internal.atoms.positions = pos.reshape((-1, 3)) 16 | return internal.jacobian() 17 | 18 | 19 | def hessian(pos: np.ndarray, internal: Internals) -> np.ndarray: 20 | internal.atoms.positions = pos.reshape((-1, 3)) 21 | return internal.hessian() 22 | 23 | 24 | @pytest.mark.parametrize("name", ['CH4', 'C6H6', 'C2H6']) 25 | def test_get_internal(name: str) -> None: 26 | atoms = molecule(name) 27 | internal = Internals(atoms) 28 | internal.find_all_bonds() 29 | internal.find_all_angles() 30 | internal.find_all_dihedrals() 31 | jac = internal.jacobian() 32 | hess = internal.hessian() 33 | 34 | x0 = atoms.positions.ravel().copy() 35 | x = x0.copy() 36 | dx = 1e-4 37 | 38 | jac_numer = np.zeros_like(jac) 39 | hess_numer = np.zeros_like(hess) 40 | for i in range(len(x)): 41 | x[i] += dx 42 | atoms.positions = x.reshape((-1, 3)) 43 | res_plus = internal.calc() 44 | jac_plus = internal.jacobian() 45 | x[i] = x0[i] - dx 46 | atoms.positions = x.reshape((-1, 3)) 47 | res_minus = internal.calc() 48 | jac_minus = internal.jacobian() 49 | x[i] = x0[i] 50 | atoms.positions = x.reshape((-1, 3)) 51 | jac_numer[:, i] = (internal.wrap(res_plus - res_minus)) / (2 * dx) 52 | hess_numer[:, i, :] = (jac_plus - jac_minus) / (2 * dx) 53 | np.testing.assert_allclose(jac, jac_numer, rtol=1e-7, atol=1e-7) 54 | np.testing.assert_allclose(hess, hess_numer, rtol=1e-7, atol=1e-7) 55 | -------------------------------------------------------------------------------- /tests/test_eigensolvers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | 5 | from sella.eigensolvers import exact, rayleigh_ritz 6 | from sella.linalg import NumericalHessian 7 | 8 | from test_utils import poly_factory, get_matrix 9 | 10 | 11 | @pytest.mark.parametrize("dim,order,eta,threepoint", 12 | [(10, 4, 1e-6, True), 13 | (10, 4, 1e-6, False)]) 14 | def test_exact(dim, order, eta, threepoint): 15 | rng = np.random.RandomState(1) 16 | 17 | tol = dict(atol=1e-4, rtol=eta**2) 18 | 19 | poly = poly_factory(dim, order, rng=rng) 20 | x = rng.normal(size=dim) 21 | 22 | _, g, h = poly(x) 23 | 24 | H = NumericalHessian(lambda x: poly(x)[:2], g0=g, x0=x, 25 | eta=eta, threepoint=threepoint) 26 | 27 | l1, V1, AV1 = exact(h) 28 | l2, V2, AV2 = exact(H) 29 | 30 | np.testing.assert_allclose(l1, l2, **tol) 31 | np.testing.assert_allclose(np.abs(V1.T @ V2), np.eye(dim), **tol) 32 | np.testing.assert_allclose(h @ V1, AV1, **tol) 33 | np.testing.assert_allclose(h @ V2, AV2, **tol) 34 | 35 | P = h + get_matrix(dim, dim, rng=rng) * 1e-3 36 | l3, V3, AV3 = exact(H, P=P) 37 | 38 | np.testing.assert_allclose(l1, l3, **tol) 39 | np.testing.assert_allclose(np.abs(V1.T @ V2), np.eye(dim), **tol) 40 | 41 | 42 | @pytest.mark.parametrize("dim,order,eta,threepoint,gamma,method,maxiter", 43 | [(10, 4, 1e-6, False, 0., 'jd0', None), 44 | (10, 4, 1e-6, False, 1e-32, 'jd0', 3), 45 | (10, 4, 1e-6, True, 1e-1, 'jd0', None), 46 | (10, 4, 1e-6, False, 1e-1, 'jd0', None), 47 | (10, 4, 1e-6, False, 1e-1, 'lanczos', None), 48 | (10, 4, 1e-6, False, 1e-1, 'gd', None), 49 | (10, 4, 1e-6, False, 1e-1, 'jd0_alt', None), 50 | (10, 4, 1e-6, False, 1e-1, 'mjd0_alt', None), 51 | (10, 4, 1e-6, False, 1e-1, 'mjd0', None), 52 | ]) 53 | def test_rayleigh_ritz(dim, order, eta, threepoint, gamma, method, maxiter): 54 | rng = np.random.RandomState(1) 55 | 56 | tol = dict(atol=1e-4, rtol=eta**2) 57 | 58 | poly = poly_factory(dim, order, rng=rng) 59 | x = rng.normal(size=dim) 60 | 61 | _, g, h = poly(x) 62 | H = NumericalHessian(lambda x: poly(x)[:2], g0=g, x0=x, 63 | eta=eta, threepoint=threepoint) 64 | 65 | l1, V1, AV1 = rayleigh_ritz(H, gamma, np.eye(dim), method=method, 66 | maxiter=maxiter) 67 | np.testing.assert_allclose(l1, np.linalg.eigh(V1.T @ AV1)[0], **tol) 68 | 69 | v0 = rng.normal(size=dim) 70 | rayleigh_ritz(H, gamma, np.eye(dim), method=method, v0=v0, 71 | maxiter=maxiter, vref=np.linalg.eigh(h)[1][:, 0]) 72 | -------------------------------------------------------------------------------- /tests/test_hessian_update.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | from test_utils import get_matrix 5 | 6 | from sella.hessian_update import update_H 7 | 8 | 9 | @pytest.mark.parametrize("dim,subdim,method,symm, pd", 10 | [(10, 1, 'TS-BFGS', 2, False), 11 | (10, 2, 'TS-BFGS', 0, False), 12 | (10, 2, 'TS-BFGS', 1, False), 13 | (10, 2, 'TS-BFGS', 2, False), 14 | (10, 2, 'BFGS', 2, False), 15 | (10, 2, 'PSB', 2, False), 16 | (10, 2, 'DFP', 2, False), 17 | (10, 2, 'SR1', 2, False), 18 | (10, 2, 'Greenstadt', 2, False), 19 | (10, 2, 'BFGS_auto', 2, False), 20 | (10, 2, 'BFGS_auto', 2, True), 21 | ]) 22 | def test_update_H(dim, subdim, method, symm, pd): 23 | rng = np.random.RandomState(1) 24 | 25 | tol = dict(atol=1e-6, rtol=1e-6) 26 | 27 | B = get_matrix(dim, dim, pd, True, rng=rng) 28 | H = get_matrix(dim, dim, pd, True, rng=rng) 29 | 30 | S = get_matrix(dim, subdim, rng=rng) 31 | Y = H @ S 32 | 33 | B1 = update_H(None, S, Y, method=method, symm=symm) 34 | np.testing.assert_allclose(B1 @ S, Y, **tol) 35 | 36 | B2 = update_H(B, S, Y, method=method, symm=symm) 37 | np.testing.assert_allclose(B2 @ S, Y, **tol) 38 | 39 | if subdim == 1: 40 | B3 = update_H(B, S.ravel(), Y.ravel(), method=method, symm=symm) 41 | np.testing.assert_allclose(B2, B3, **tol) 42 | 43 | B4 = update_H(B, S.ravel() / 1e12, Y.ravel() / 1e12, method=method, 44 | symm=symm) 45 | np.testing.assert_allclose(B, B4, atol=0, rtol=0) 46 | -------------------------------------------------------------------------------- /tests/test_linalg.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | from scipy.stats import ortho_group 5 | 6 | from sella.linalg import NumericalHessian 7 | 8 | from test_utils import poly_factory 9 | 10 | 11 | @pytest.mark.parametrize("dim,subdim,order,threepoint", 12 | [(3, None, 1, False), 13 | (3, None, 1, True), 14 | (5, 3, 2, True), 15 | (10, None, 4, True), 16 | (10, 6, 4, False)]) 17 | def test_NumericalHessian(dim, subdim, order, threepoint, eta=1e-6, atol=1e-4): 18 | rng = np.random.RandomState(2) 19 | tol = dict(rtol=atol, atol=eta**2) 20 | 21 | x = rng.normal(size=dim) 22 | 23 | poly1 = poly_factory(dim, order, rng) 24 | _, g1, h1 = poly1(x) 25 | 26 | poly2 = poly_factory(dim, order, rng) 27 | _, g2, h2 = poly2(x) 28 | 29 | if subdim is None: 30 | U = None 31 | subdim = dim 32 | g1proj = g1 33 | xproj = x 34 | else: 35 | U = ortho_group.rvs(dim, random_state=rng)[:, :subdim] 36 | h1 = U.T @ h1 @ U 37 | h2 = U.T @ h2 @ U 38 | g1proj = U.T @ g1 39 | xproj = U.T @ x 40 | 41 | Hkwargs = dict(x0=x, eta=eta, threepoint=threepoint, Uproj=U) 42 | 43 | H1 = NumericalHessian(lambda x: poly1(x)[:2], g0=g1, **Hkwargs) 44 | 45 | # M1: some random matrix 46 | M1 = rng.normal(size=(subdim, subdim)) 47 | 48 | H2 = H1 + NumericalHessian(lambda x: poly2(x)[:2], g0=g2, **Hkwargs) + M1 49 | H3 = h1 + h2 + M1 50 | 51 | # Make first column orthogonal to g1 52 | M1[:, 0] = xproj - g1proj * (xproj @ g1proj) / (g1proj @ g1proj) 53 | 54 | # Make second column orthogonal to g1 and x 55 | M1[:, 1] -= M1[:, 0] * (M1[:, 1] @ M1[:, 0]) / (M1[:, 0] @ M1[:, 0]) 56 | M1[:, 1] -= g1proj * (M1[:, 1] @ g1proj) / (g1proj @ g1proj) 57 | 58 | np.testing.assert_allclose(H2.T.dot(M1), H3.T @ M1, **tol) 59 | -------------------------------------------------------------------------------- /tests/test_peswrapper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | 5 | from ase.build import molecule 6 | from ase.calculators.emt import EMT 7 | 8 | from sella.peswrapper import PES, InternalPES 9 | 10 | @pytest.mark.parametrize("name,traj,cons", 11 | [("CH4", "CH4.traj", None), 12 | ("CH4", None, dict(bonds=((0, 1)))), 13 | ("C6H6", None, None), 14 | ]) 15 | def test_PES(name, traj, cons): 16 | tol = dict(atol=1e-6, rtol=1e-6) 17 | 18 | atoms = molecule(name) 19 | 20 | # EMT is *not* appropriate for molecules like this, but this is one 21 | # of the few calculators that is guaranteed to be available to all 22 | # users, and we don't need to use a physical PES to test this. 23 | atoms.calc = EMT() 24 | for MyPES in [PES, InternalPES]: 25 | pes = PES(atoms, trajectory=traj) 26 | 27 | pes.kick(0., diag=True, gamma=0.1) 28 | 29 | for i in range(2): 30 | pes.kick(-pes.get_g() * 0.01) 31 | 32 | assert pes.H is not None 33 | assert not pes.converged(0.)[0] 34 | assert pes.converged(1e100) 35 | A = pes.get_Ufree().T @ pes.get_Ucons() 36 | np.testing.assert_allclose(A, 0, **tol) 37 | 38 | pes.kick(-pes.get_g() * 0.001, diag=True, gamma=0.1) 39 | -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .poly_factory import poly_factory 2 | from .matrix_factory import get_matrix 3 | 4 | __all__ = ["poly_factory", "get_matrix"] 5 | -------------------------------------------------------------------------------- /tests/test_utils/matrix_factory.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def get_matrix(n, m, pd=False, symm=False, rng=None): 4 | """Generates a random n-by-m matrix""" 5 | if rng is None: 6 | rng = np.random.RandomState(1) 7 | A = rng.normal(size=(n, m)) 8 | if symm: 9 | assert n == m 10 | A = 0.5 * (A + A.T) 11 | if pd: 12 | assert n == m 13 | lams, vecs = np.linalg.eigh(A) 14 | A = vecs @ (np.abs(lams)[:, np.newaxis] * vecs.T) 15 | return A 16 | -------------------------------------------------------------------------------- /tests/test_utils/poly_factory.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from itertools import permutations 4 | 5 | 6 | def poly_factory(dim, order, rng=None): 7 | """Generates a random multi-dimensional polynomial function.""" 8 | if rng is None: 9 | rng = np.random.RandomState(1) 10 | 11 | coeffs = [] 12 | for i in range(order + 1): 13 | tmp = rng.normal(size=(dim,) * i) 14 | coeff = np.zeros_like(tmp) 15 | for n, permute in enumerate(permutations(range(i))): 16 | coeff += np.transpose(tmp, permute) 17 | coeffs.append(coeff / ((n + 1) * np.math.factorial(i))) 18 | 19 | def poly(x): 20 | res = 0 21 | grad = np.zeros_like(x) 22 | hess = np.zeros((dim, dim)) 23 | for i, coeff in enumerate(coeffs): 24 | lastlast = None 25 | last = None 26 | for j in range(i): 27 | lastlast = last 28 | last = coeff 29 | coeff = coeff @ x 30 | if last is not None: 31 | grad += i * last 32 | if lastlast is not None: 33 | hess += i * (i - 1) * lastlast 34 | res += coeff 35 | return res, grad, hess 36 | 37 | return poly 38 | -------------------------------------------------------------------------------- /tests/test_utils/test_poly_factory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | 5 | from .poly_factory import poly_factory 6 | 7 | 8 | @pytest.mark.parametrize("dim,order", [(1, 1), (2, 2), (10, 5)]) 9 | def test_poly_factory(dim, order, eta=1e-6, atol=1e-4): 10 | rng = np.random.RandomState(1) 11 | 12 | tol = dict(atol=atol, rtol=eta**2) 13 | 14 | my_poly = poly_factory(dim, order) 15 | x0 = rng.normal(size=dim) 16 | f0, g0, h0 = my_poly(x0) 17 | 18 | g_numer = np.zeros_like(g0) 19 | h_numer = np.zeros_like(h0) 20 | for i in range(dim): 21 | x = x0.copy() 22 | x[i] += eta 23 | fplus, gplus, _ = my_poly(x) 24 | x[i] = x0[i] - eta 25 | fminus, gminus, _ = my_poly(x) 26 | g_numer[i] = (fplus - fminus) / (2 * eta) 27 | h_numer[i] = (gplus - gminus) / (2 * eta) 28 | 29 | np.testing.assert_allclose(g0, g_numer, **tol) 30 | np.testing.assert_allclose(h0, h_numer, **tol) 31 | -------------------------------------------------------------------------------- /tests/utilities/math_wrappers.pyx: -------------------------------------------------------------------------------- 1 | from sella.utilities.math cimport normalize, vec_sum, symmetrize, skew, mgs 2 | 3 | def wrap_normalize(x): 4 | return normalize(x) 5 | 6 | def wrap_vec_sum(x, y, z, scale): 7 | return vec_sum(x, y, z, scale) 8 | 9 | def wrap_symmetrize(X_np): 10 | cdef double[:, :] X = memoryview(X_np) 11 | cdef size_t n = len(X) 12 | cdef size_t lda = X.strides[0] >> 3 13 | return symmetrize(&X[0, 0], n, lda) 14 | 15 | def wrap_skew(x, Y, scale): 16 | return skew(x, Y, scale) 17 | 18 | def wrap_mgs(X, Y, eps1=1e-15, eps2=1e-6, maxiter=1000): 19 | return mgs(X, Y=Y, eps1=eps1, eps2=eps2, maxiter=maxiter) 20 | 21 | 22 | wrappers = {'normalize': wrap_normalize, 23 | 'vec_sum': wrap_vec_sum, 24 | 'symmetrize': wrap_symmetrize, 25 | 'skew': wrap_skew, 26 | 'mgs': wrap_mgs} 27 | 28 | -------------------------------------------------------------------------------- /tests/utilities/test_math.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | from sella.utilities.math import pseudo_inverse, modified_gram_schmidt 5 | 6 | from test_utils import get_matrix 7 | 8 | import pyximport 9 | pyximport.install(language_level=3) 10 | from math_wrappers import wrappers 11 | 12 | # TODO: figure out why m > n crashes 13 | @pytest.mark.parametrize("n,m,eps", 14 | [(3, 3, 1e-10), 15 | (100, 3, 1e-6), 16 | ]) 17 | def test_mppi(n, m, eps): 18 | rng = np.random.RandomState(1) 19 | 20 | tol = dict(atol=1e-6, rtol=1e-6) 21 | 22 | A = get_matrix(n, m, rng=rng) 23 | U1, s1, VT1, Ainv, nsing1 = pseudo_inverse(A.copy(), eps=eps) 24 | 25 | A_test = U1[:, :nsing1] @ np.diag(s1) @ VT1[:nsing1, :] 26 | np.testing.assert_allclose(A_test, A, **tol) 27 | 28 | Ainv_test = np.linalg.pinv(A) 29 | np.testing.assert_allclose(Ainv_test, Ainv, **tol) 30 | 31 | nsingB = nsing1 - 1 32 | B = U1[:, :nsingB] @ np.diag(s1[:nsingB]) @ VT1[:nsingB, :] 33 | U2, s2, VT2, Binv, nsing2 = pseudo_inverse(B.copy(), eps=eps) 34 | 35 | 36 | @pytest.mark.parametrize("n,mx,my,eps1,eps2,maxiter", 37 | [(3, 2, 1, 1e-15, 1e-6, 100), 38 | (100, 50, 25, 1e-15, 1e-6, 100), 39 | ]) 40 | def test_modified_gram_schmidt(n, mx, my, eps1, eps2, maxiter): 41 | rng = np.random.RandomState(2) 42 | 43 | tol = dict(atol=1e-6, rtol=1e-6) 44 | mgskw = dict(eps1=eps1, eps2=eps2, maxiter=maxiter) 45 | 46 | X = get_matrix(n, mx, rng=rng) 47 | 48 | Xout1 = modified_gram_schmidt(X, **mgskw) 49 | _, nxout1 = Xout1.shape 50 | 51 | np.testing.assert_allclose(Xout1.T @ Xout1, np.eye(nxout1), **tol) 52 | np.testing.assert_allclose(np.linalg.det(X.T @ X), 53 | np.linalg.det(X.T @ Xout1)**2, **tol) 54 | 55 | 56 | Y = get_matrix(n, my, rng=rng) 57 | Xout2 = modified_gram_schmidt(X, Y, **mgskw) 58 | _, nxout2 = Xout2.shape 59 | 60 | np.testing.assert_allclose(Xout2.T @ Xout2, np.eye(nxout2), **tol) 61 | np.testing.assert_allclose(Xout2.T @ Y, np.zeros((nxout2, my)), **tol) 62 | 63 | X[:, 1] = X[:, 0] 64 | 65 | Xout3 = modified_gram_schmidt(X, **mgskw) 66 | _, nxout3 = Xout3.shape 67 | assert nxout3 == nxout1 - 1 68 | 69 | np.testing.assert_allclose(Xout2.T @ Xout2, np.eye(nxout2), **tol) 70 | 71 | 72 | @pytest.mark.parametrize('rngstate,length', 73 | [(0, 1), 74 | (1, 2), 75 | (2, 10), 76 | (3, 1024), 77 | (4, 0)]) 78 | def test_normalize(rngstate, length): 79 | rng = np.random.RandomState(rngstate) 80 | x = rng.normal(size=(length,)) 81 | wrappers['normalize'](x) 82 | 83 | if length > 0: 84 | assert abs(np.linalg.norm(x) - 1.) < 1e-14 85 | 86 | @pytest.mark.parametrize('rngstate,length,scale', 87 | [(0, 1, 1.), 88 | (1, 3, 0.5), 89 | (2, 100, 4.), 90 | (3, 10, 0.), 91 | (4, 0, 1.)]) 92 | def test_vec_sum(rngstate, length, scale): 93 | rng = np.random.RandomState(rngstate) 94 | x = rng.normal(size=(length,)) 95 | y = rng.normal(size=(length,)) 96 | z = np.zeros(length) 97 | err = wrappers['vec_sum'](x, y, z, scale) 98 | assert err == 0 99 | np.testing.assert_allclose(z, x + scale * y) 100 | 101 | if length > 0: 102 | assert wrappers['vec_sum'](x, y[:length-1], z, scale) == -1 103 | assert wrappers['vec_sum'](x, y, z[:length-1], scale) == -1 104 | 105 | @pytest.mark.parametrize('rngstate,n,m', 106 | [(0, 1, 1), 107 | (1, 5, 5), 108 | (2, 3, 7), 109 | (3, 8, 4), 110 | (6, 100, 100)]) 111 | def test_symmetrize(rngstate, n, m): 112 | rng = np.random.RandomState(rngstate) 113 | X = get_matrix(n, m, rng=rng) 114 | minnm = min(n, m) 115 | Y = X[:minnm, :minnm] 116 | wrappers['symmetrize'](Y) 117 | np.testing.assert_allclose(Y, Y.T) 118 | 119 | 120 | @pytest.mark.parametrize('rngstate,scale', 121 | [(0, 1.), 122 | (1, 0.1), 123 | (2, 100.)]) 124 | def test_skew(rngstate, scale): 125 | rng = np.random.RandomState(rngstate) 126 | x = rng.normal(size=(3,)) 127 | Y = get_matrix(3, 3, rng=rng) 128 | wrappers['skew'](x, Y, scale) 129 | np.testing.assert_allclose(scale * np.cross(np.eye(3), x), Y) 130 | 131 | @pytest.mark.parametrize('rngstate,n,mx,my', 132 | [(2, 10, 2, 4)]) 133 | def test_mgs(rngstate, n, mx, my): 134 | rng = np.random.RandomState(rngstate) 135 | X = get_matrix(n, mx, rng=rng) 136 | assert wrappers['mgs'](X, None, maxiter=1) < 0 137 | X = get_matrix(n, mx, rng=rng) 138 | assert wrappers['mgs'](X, None, eps2=1e10) == 0 139 | X = get_matrix(n, mx, rng=rng) 140 | Y = get_matrix(n, my, rng=rng) 141 | assert wrappers['mgs'](X, Y, eps2=1e10) == 0 142 | Y = get_matrix(n, my, rng=rng) 143 | my2 = wrappers['mgs'](Y, None) 144 | assert my2 >= 0 145 | np.testing.assert_allclose(Y[:, :my2].T @ Y[:, :my2], np.eye(my2), 146 | atol=1e-10) 147 | X = get_matrix(n, mx, rng=rng) 148 | mx2 = wrappers['mgs'](X, Y) 149 | assert mx2 >= 0 150 | np.testing.assert_allclose(X[:, :mx2].T @ X[:, :mx2], np.eye(mx2), 151 | atol=1e-10) 152 | np.testing.assert_allclose(X[:, :mx2].T @ Y[:, :my2], np.zeros((mx2, my2)), 153 | atol=1e-10) 154 | assert wrappers['mgs'](X, Y[:n-1]) < 0 155 | 156 | --------------------------------------------------------------------------------