├── .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 | [](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 |
--------------------------------------------------------------------------------