├── .gitignore ├── .pylintrc ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── gui ├── gui_design.py ├── gui_key_table.py ├── gui_key_table.ui ├── gui_ts_guess.py ├── gui_ts_guess.ui └── key_table_view.py ├── requirements.txt ├── saddle ├── __init__.py ├── cartesian.py ├── conf.py ├── constrain_v.py ├── coordinate_types.py ├── data │ ├── __init__.py │ ├── conf.json │ └── single_hf_template.com ├── errors.py ├── fchk.py ├── gaussianwrapper.py ├── internal.py ├── math_lib.py ├── molmod.py ├── opt.py ├── optimizer │ ├── __init__.py │ ├── errors.py │ ├── hessian_modify.py │ ├── optloop.py │ ├── path_point.py │ ├── pathloop.py │ ├── quasi_newton.py │ ├── react_point.py │ ├── secant.py │ ├── step_size.py │ ├── test │ │ ├── __init__.py │ │ ├── data │ │ │ ├── HNCS.xyz │ │ │ ├── HSCN.xyz │ │ │ ├── __init__.py │ │ │ ├── final_water.fchk │ │ │ ├── new_step_water.fchk │ │ │ ├── pathloop_hscn.fchk │ │ │ ├── water.xyz │ │ │ ├── water_new.fchk │ │ │ ├── water_new_2.fchk │ │ │ ├── water_new_3.fchk │ │ │ └── water_old.fchk │ │ ├── test_hessian_modify.py │ │ ├── test_opt_loop.py │ │ ├── test_path_point.py │ │ ├── test_pathloop.py │ │ ├── test_quasi_newton.py │ │ ├── test_react_point.py │ │ ├── test_secant.py │ │ ├── test_step_size.py │ │ └── test_trust_radius.py │ └── trust_radius.py ├── path_ri.py ├── periodic │ ├── __init__.py │ ├── constants.py │ ├── data │ │ └── elements.csv │ ├── periodic.py │ └── units.py ├── procrustes │ ├── __init__.py │ ├── procrustes.py │ └── test │ │ ├── data │ │ ├── __init__.py │ │ └── water.xyz │ │ └── test_procrustes.py ├── reduced_internal.py ├── test │ ├── __init__.py │ ├── data │ │ ├── 2h-azirine.xyz │ │ ├── __init__.py │ │ ├── ch3_hf.xyz │ │ ├── ch3f_h.xyz │ │ ├── di_water.xyz │ │ ├── ethane.xyz │ │ ├── h2o2.xyz │ │ ├── methanol.xyz │ │ ├── prd.xyz │ │ ├── rct.xyz │ │ ├── water.xyz │ │ ├── water_0.fchk │ │ └── water_1.fchk │ ├── test_cartesian.py │ ├── test_config.py │ ├── test_constrain_v.py │ ├── test_coordinates_type.py │ ├── test_gaussian_wrapper.py │ ├── test_internal.py │ ├── test_math_lib.py │ ├── test_path_ri.py │ ├── test_reduced_internal.py │ ├── test_ts_construct.py │ ├── test_utils.py │ └── test_water_fchk.py ├── ts_construct.py ├── utils.py └── work │ └── log │ └── .gitkeep ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *.log 4 | *.chk 5 | *.swp 6 | *.swo 7 | *.coverage 8 | *.ipynb 9 | saddle/work/* 10 | !saddle/work/.gitkeep 11 | !single_hf_template.com 12 | 13 | build 14 | .tox 15 | .cache 16 | .pytest_cache/ 17 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-classes = numpy, list, Element 3 | ignored-modules = numpy 4 | 5 | [MASTER] 6 | extension-pkg-whitelist=numpy 7 | 8 | [MESSAGES CONTROL] 9 | disable=W0511 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://travis-ci.com/tczorro/GOpt 2 | language: python 3 | python: 4 | - 3.6 5 | 6 | install: pip install tox-travis 7 | 8 | script: tox 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU LESSER GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | 10 | This version of the GNU Lesser General Public License incorporates 11 | the terms and conditions of version 3 of the GNU General Public 12 | License, supplemented by the additional permissions listed below. 13 | 14 | 0. Additional Definitions. 15 | 16 | As used herein, "this License" refers to version 3 of the GNU Lesser 17 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 18 | General Public License. 19 | 20 | "The Library" refers to a covered work governed by this License, 21 | other than an Application or a Combined Work as defined below. 22 | 23 | An "Application" is any work that makes use of an interface provided 24 | by the Library, but which is not otherwise based on the Library. 25 | Defining a subclass of a class defined by the Library is deemed a mode 26 | of using an interface provided by the Library. 27 | 28 | A "Combined Work" is a work produced by combining or linking an 29 | Application with the Library. The particular version of the Library 30 | with which the Combined Work was made is also called the "Linked 31 | Version". 32 | 33 | The "Minimal Corresponding Source" for a Combined Work means the 34 | Corresponding Source for the Combined Work, excluding any source code 35 | for portions of the Combined Work that, considered in isolation, are 36 | based on the Application, and not on the Linked Version. 37 | 38 | The "Corresponding Application Code" for a Combined Work means the 39 | object code and/or source code for the Application, including any data 40 | and utility programs needed for reproducing the Combined Work from the 41 | Application, but excluding the System Libraries of the Combined Work. 42 | 43 | 1. Exception to Section 3 of the GNU GPL. 44 | 45 | You may convey a covered work under sections 3 and 4 of this License 46 | without being bound by section 3 of the GNU GPL. 47 | 48 | 2. Conveying Modified Versions. 49 | 50 | If you modify a copy of the Library, and, in your modifications, a 51 | facility refers to a function or data to be supplied by an Application 52 | that uses the facility (other than as an argument passed when the 53 | facility is invoked), then you may convey a copy of the modified 54 | version: 55 | 56 | a) under this License, provided that you make a good faith effort to 57 | ensure that, in the event an Application does not supply the 58 | function or data, the facility still operates, and performs 59 | whatever part of its purpose remains meaningful, or 60 | 61 | b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from 67 | a header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | a) Give prominent notice with each copy of the object code that the 74 | Library is used in it and that the Library and its use are 75 | covered by this License. 76 | 77 | b) Accompany the object code with a copy of the GNU GPL and this license 78 | document. 79 | 80 | 4. Combined Works. 81 | 82 | You may convey a Combined Work under terms of your choice that, 83 | taken together, effectively do not restrict modification of the 84 | portions of the Library contained in the Combined Work and reverse 85 | engineering for debugging such modifications, if you also do each of 86 | the following: 87 | 88 | a) Give prominent notice with each copy of the Combined Work that 89 | the Library is used in it and that the Library and its use are 90 | covered by this License. 91 | 92 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 93 | document. 94 | 95 | c) For a Combined Work that displays copyright notices during 96 | execution, include the copyright notice for the Library among 97 | these notices, as well as a reference directing the user to the 98 | copies of the GNU GPL and this license document. 99 | 100 | d) Do one of the following: 101 | 102 | 0) Convey the Minimal Corresponding Source under the terms of this 103 | License, and the Corresponding Application Code in a form 104 | suitable for, and under terms that permit, the user to 105 | recombine or relink the Application with a modified version of 106 | the Linked Version to produce a modified Combined Work, in the 107 | manner specified by section 6 of the GNU GPL for conveying 108 | Corresponding Source. 109 | 110 | 1) Use a suitable shared library mechanism for linking with the 111 | Library. A suitable mechanism is one that (a) uses at run time 112 | a copy of the Library already present on the user's computer 113 | system, and (b) will operate properly with a modified version 114 | of the Library that is interface-compatible with the Linked 115 | Version. 116 | 117 | e) Provide Installation Information, but only if you would otherwise 118 | be required to provide such information under section 6 of the 119 | GNU GPL, and only to the extent that such information is 120 | necessary to install and execute a modified version of the 121 | Combined Work produced by recombining or relinking the 122 | Application with a modified version of the Linked Version. (If 123 | you use option 4d0, the Installation Information must accompany 124 | the Minimal Corresponding Source and Corresponding Application 125 | Code. If you use option 4d1, you must provide the Installation 126 | Information in the manner specified by section 6 of the GNU GPL 127 | for conveying Corresponding Source.) 128 | 129 | 5. Combined Libraries. 130 | 131 | You may place library facilities that are a work based on the 132 | Library side by side in a single library together with other library 133 | facilities that are not Applications and are not covered by this 134 | License, and convey such a combined library under terms of your 135 | choice, if you do both of the following: 136 | 137 | a) Accompany the combined library with a copy of the same work based 138 | on the Library, uncombined with any other library facilities, 139 | conveyed under the terms of this License. 140 | 141 | b) Give prominent notice with the combined library that part of it 142 | is a work based on the Library, and explaining where to find the 143 | accompanying uncombined form of the same work. 144 | 145 | 6. Revised Versions of the GNU Lesser General Public License. 146 | 147 | The Free Software Foundation may publish revised and/or new versions 148 | of the GNU Lesser General Public License from time to time. Such new 149 | versions will be similar in spirit to the present version, but may 150 | differ in detail to address new problems or concerns. 151 | 152 | Each version is given a distinguishing version number. If the 153 | Library as you received it specifies that a certain numbered version 154 | of the GNU Lesser General Public License "or any later version" 155 | applies to it, you have the option of following the terms and 156 | conditions either of that published version or of any later version 157 | published by the Free Software Foundation. If the Library as you 158 | received it does not specify a version number of the GNU Lesser 159 | General Public License, you may choose any version of the GNU Lesser 160 | General Public License ever published by the Free Software Foundation. 161 | 162 | If the Library as you received it specifies that a proxy can decide 163 | whether future versions of the GNU Lesser General Public License shall 164 | apply, that proxy's public statement of acceptance of any version is 165 | permanent authorization for you to choose that version for the 166 | Library. 167 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.md 3 | recursive-include saddle/data *.json *.com 4 | recursive-include saddle/periodic/data *.csv 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOpt 2 | [![Python](https://img.shields.io/badge/python-3.6-blue.svg)](https://docs.python.org/3.6/) 3 | [![Build Status](https://travis-ci.org/tczorro/gbasis.svg?branch=master)](https://github.com/tczorro/GOpt) 4 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://github.com/tczorro/GOpt/blob/master/LICENSE) 5 | [![codecov](https://codecov.io/gh/tczorro/GOpt/branch/master/graph/badge.svg?token=Yo3TmFd3sf)](https://codecov.io/gh/tczorro/GOpt) 6 | 7 | ## Warning! 8 | Right now we are rewriting `Gopt` from scratch. We are therefore no longer developing or supporting this version. The new `Gopt` should be out in a few months! 9 | 10 | ## Synopsis 11 | GOpt is a python(3.6+) package for quantum chemistry geometry optimization. It 12 | provides robust performance for both energy minimization and transition state 13 | search. 14 | 15 | ## License 16 | GOpt is distributed under the conditions of the GPL License version 3 (GPLv3) 17 | 18 | You are free to use `GOpt` for any purpose. However, `GOpt`, or any derived 19 | version, can only be (re)distributed under the same license conditions. 20 | 21 | ## Build Dependence 22 | `Numpy` 23 | `importlib_resources` 24 | 25 | ## Test Dependence 26 | `pytest` 27 | `tox` 28 | 29 | ## CI Tests 30 | ```bash 31 | tox 32 | ``` 33 | 34 | ## Installation 35 | ```bash 36 | python ./setup.py install 37 | ``` 38 | -------------------------------------------------------------------------------- /gui/gui_design.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # import horton as ht 3 | # import PyQt4 import Qt 4 | # import os 5 | 6 | # from saddle import TransitionSearch 7 | from PyQt4 import QtGui, QtCore 8 | from gui_ts_guess import Ui_MainWindow 9 | from subprocess import Popen 10 | # from key_table_view import KeyIcTable 11 | 12 | 13 | class Window(QtGui.QMainWindow): 14 | 15 | def __init__(self): 16 | super(Window, self).__init__() 17 | self.ui = Ui_MainWindow() 18 | self.ui.setupUi(self) 19 | # QtGui.QApplication.setStyle(QtGui.QStyleFactory.create("Windows")) 20 | self.reactant_path = None 21 | self.product_path = None 22 | self.lable_ratio_value = 50 23 | self.ui.reactant.clicked.connect(self.reactant_open) 24 | self.ui.reactant_text.setReadOnly(True) 25 | self.ui.product.clicked.connect(self.product_open) 26 | self.ui.ts_guess.clicked.connect(self.get_ts_guess) 27 | self.ui.product_text.setReadOnly(True) 28 | self.ui.reactant_reset.clicked.connect(self.reactant_reset) 29 | self.ui.product_reset.clicked.connect(self.product_reset) 30 | self.ui.ratio_slide_bar.valueChanged.connect(self.change_ratio) 31 | self.ui.ratio_value.returnPressed.connect(self.return_ratio) 32 | self.ui.auto_ic_select.toggle() 33 | self.auto_ic_select = True 34 | self.auto_key_ic = False 35 | self.ui.auto_ic_select.stateChanged.connect(self.change_auto_ic) 36 | self.ui.auto_key_ic.stateChanged.connect(self.change_key_ic) 37 | self.ui.tc_progressBar.setValue(0) 38 | self.ui.save_xyz.clicked.connect(self.save_xyz) 39 | self.ui.actionSaddle.triggered.connect(self.about) 40 | self.ts_mol = None 41 | self.ui.view_vmd.clicked.connect(self.view_vmd) 42 | self.output = None 43 | self.ui.select_key_ic.clicked.connect(self.open_select_key_ci) 44 | 45 | def reactant_open(self): 46 | name = QtGui.QFileDialog.getOpenFileName(self,"Open file") 47 | if name: 48 | self.reactant_path = name 49 | with open(name, "r") as file: 50 | self.ui.reactant_text.setText(file.read()) 51 | self.ui.tc_progressBar.setValue(0) 52 | 53 | 54 | def product_open(self): 55 | name = QtGui.QFileDialog.getOpenFileName(self,"Open file") 56 | if name: 57 | self.product_path = name 58 | with open(name, "r") as file: 59 | self.ui.product_text.setText(file.read()) 60 | self.ui.tc_progressBar.setValue(0) 61 | 62 | 63 | def reactant_reset(self): 64 | self.ui.reactant_text.clear() 65 | self.reactant_path = None 66 | self.ui.tc_progressBar.setValue(0) 67 | self.output = None 68 | # choice = QtGui.QMessageBox.information(self, "Extrac!", "Really", QtGui.QMessageBox.Ok) 69 | 70 | def product_reset(self): 71 | self.ui.product_text.clear() 72 | self.product_path = None 73 | self.ui.tc_progressBar.setValue(0) 74 | self.output = None 75 | 76 | def change_ratio(self): 77 | self.lable_ratio_value = self.ui.ratio_slide_bar.value() 78 | # print self.lable_ratio_value 79 | self.ui.ratio_value.setText("{0}".format(self.lable_ratio_value / 100.)) 80 | 81 | def return_ratio(self): 82 | text = self.ui.ratio_value.displayText() 83 | try: 84 | value = float(text) 85 | except ValueError: 86 | value = 0.50 87 | self.ui.ratio_value.setText("0.50") 88 | self.lable_ratio_value = value * 100 89 | value = min(max(self.lable_ratio_value, 0), 100.) 90 | self.ui.ratio_slide_bar.setValue(value) 91 | # print self.lable_ratio_value 92 | 93 | def change_auto_ic(self): 94 | self.auto_ic_select = not self.auto_ic_select 95 | # print self.auto_ic_select 96 | 97 | def change_key_ic(self): 98 | self.auto_key_ic = not self.auto_key_ic 99 | self.ui.key_ic_text.setEnabled(not self.auto_key_ic) 100 | # self.ui.select_key_ic.setEnabled(not self.auto_key_ic) 101 | 102 | def get_ts_guess(self): 103 | if self.reactant_path and self.product_path: 104 | react_mol = ht.IOData.from_file(str(self.reactant_path)) 105 | produ_mol = ht.IOData.from_file(str(self.product_path)) 106 | self.ui.tc_progressBar.setValue(20) 107 | ts_search = TransitionSearch.TransitionSearch(react_mol, produ_mol) 108 | self.return_ratio() 109 | ratio = self.lable_ratio_value / 100. 110 | ts_search.auto_ts_search() 111 | self.ui.tc_progressBar.setValue(50) 112 | if self.auto_ic_select == True: 113 | ts_search.auto_ic_select_combine() 114 | if self.auto_key_ic == True: 115 | ts_search.auto_key_ic_select() 116 | self.ui.tc_progressBar.setValue(100) 117 | self.ts_mol = ts_search # change from ts_search.ts_state to just ts_search 118 | success = QtGui.QMessageBox.information(self, "Finished", "\n\nFinished!", QtGui.QMessageBox.Ok) 119 | else: 120 | fail = QtGui.QMessageBox.warning(self, "Can't do that", '''Can't do that 121 | \nPlease select reactant and product structure first''', QtGui.QMessageBox.Ok) 122 | 123 | def save_xyz(self): 124 | name = QtGui.QFileDialog.getSaveFileName(self, "Save .xyz") 125 | if name: 126 | name = name + ".xyz" 127 | with open(name, "w") as f: 128 | print >> f, len(self.ts_mol.ts_state.numbers) 129 | print >> f, getattr(self.ts_mol.ts_state, "title", name.split("/")[-1].split(".")[0]) 130 | for i in range(len(self.ts_mol.ts_state.numbers)): 131 | n = ht.periodic[self.ts_mol.ts_state.numbers[i]].symbol 132 | x, y, z = self.ts_mol.ts_state.coordinates[i]/ht.angstrom 133 | print >> f, '%2s %15.10f %15.10f %15.10f' % (n, x, y, z) 134 | self.output = name 135 | # print self.auto_key_ic 136 | 137 | def view_vmd(self): 138 | file_name = "{}".format(self.output) 139 | arg = ["vmd",file_name] 140 | a = Popen(arg) 141 | print a 142 | # os.system("vmd {0}".format(self.output)) 143 | 144 | def open_select_key_ci(self): 145 | if self.ts_mol: 146 | molecule = self.ts_mol 147 | table_gui = KeyIcTable(molecule) 148 | table_gui.setWindowTitle("Key IC Selection --by Derrick") 149 | table_gui.exec_() 150 | # print self.ts_mol._ic_key_counter 151 | else: 152 | fail = QtGui.QMessageBox.warning(self, "Can't do that", '''Can't do that 153 | \nPlease generate transition guess structure first''', QtGui.QMessageBox.Ok) 154 | 155 | def about(self): 156 | popup = QtGui.QMessageBox.about(self, "About Saddle", '''

Saddle

157 | \n

Copyright 2016 Horton Group

\n 158 |

version 1.0 by Derrick

''') 159 | 160 | 161 | app = QtGui.QApplication(sys.argv) 162 | gui = Window() 163 | gui.setWindowTitle("Saddle --by Derrick") 164 | gui.show() 165 | sys.exit(app.exec_()) 166 | -------------------------------------------------------------------------------- /gui/gui_key_table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'gui_key_table.ui' 4 | # 5 | # Created by: PyQt4 UI code generator 4.11.4 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt4 import QtCore, QtGui 10 | 11 | try: 12 | _fromUtf8 = QtCore.QString.fromUtf8 13 | except AttributeError: 14 | def _fromUtf8(s): 15 | return s 16 | 17 | try: 18 | _encoding = QtGui.QApplication.UnicodeUTF8 19 | def _translate(context, text, disambig): 20 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 21 | except AttributeError: 22 | def _translate(context, text, disambig): 23 | return QtGui.QApplication.translate(context, text, disambig) 24 | 25 | class Ui_Dialog(object): 26 | def setupUi(self, Dialog): 27 | Dialog.setObjectName(_fromUtf8("Dialog")) 28 | Dialog.resize(451, 536) 29 | self.verticalLayout = QtGui.QVBoxLayout(Dialog) 30 | self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) 31 | self.keyic_table_widget = QtGui.QTableWidget(Dialog) 32 | self.keyic_table_widget.setEnabled(True) 33 | self.keyic_table_widget.setAlternatingRowColors(False) 34 | self.keyic_table_widget.setRowCount(0) 35 | self.keyic_table_widget.setObjectName(_fromUtf8("keyic_table_widget")) 36 | self.keyic_table_widget.setColumnCount(0) 37 | self.verticalLayout.addWidget(self.keyic_table_widget) 38 | self.return_button = QtGui.QDialogButtonBox(Dialog) 39 | self.return_button.setOrientation(QtCore.Qt.Horizontal) 40 | self.return_button.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) 41 | self.return_button.setObjectName(_fromUtf8("return_button")) 42 | self.verticalLayout.addWidget(self.return_button) 43 | 44 | self.retranslateUi(Dialog) 45 | QtCore.QObject.connect(self.return_button, QtCore.SIGNAL(_fromUtf8("accepted()")), Dialog.accept) 46 | QtCore.QObject.connect(self.return_button, QtCore.SIGNAL(_fromUtf8("rejected()")), Dialog.reject) 47 | QtCore.QMetaObject.connectSlotsByName(Dialog) 48 | 49 | def retranslateUi(self, Dialog): 50 | Dialog.setWindowTitle(_translate("Dialog", "Dialog", None)) 51 | 52 | -------------------------------------------------------------------------------- /gui/gui_key_table.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 451 10 | 536 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | true 21 | 22 | 23 | false 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | 31 | 32 | 33 | Qt::Horizontal 34 | 35 | 36 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | return_button 46 | accepted() 47 | Dialog 48 | accept() 49 | 50 | 51 | 248 52 | 254 53 | 54 | 55 | 157 56 | 274 57 | 58 | 59 | 60 | 61 | return_button 62 | rejected() 63 | Dialog 64 | reject() 65 | 66 | 67 | 316 68 | 260 69 | 70 | 71 | 286 72 | 274 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /gui/gui_ts_guess.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'gui_ts_guess.ui' 4 | # 5 | # Created by: PyQt4 UI code generator 4.11.4 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt4 import QtCore, QtGui 10 | 11 | try: 12 | _fromUtf8 = QtCore.QString.fromUtf8 13 | except AttributeError: 14 | def _fromUtf8(s): 15 | return s 16 | 17 | try: 18 | _encoding = QtGui.QApplication.UnicodeUTF8 19 | def _translate(context, text, disambig): 20 | return QtGui.QApplication.translate(context, text, disambig, _encoding) 21 | except AttributeError: 22 | def _translate(context, text, disambig): 23 | return QtGui.QApplication.translate(context, text, disambig) 24 | 25 | class Ui_MainWindow(object): 26 | def setupUi(self, MainWindow): 27 | MainWindow.setObjectName(_fromUtf8("MainWindow")) 28 | MainWindow.resize(600, 800) 29 | self.centralwidget = QtGui.QWidget(MainWindow) 30 | self.centralwidget.setObjectName(_fromUtf8("centralwidget")) 31 | self.gridLayout = QtGui.QGridLayout(self.centralwidget) 32 | self.gridLayout.setObjectName(_fromUtf8("gridLayout")) 33 | self.auto_key_ic = QtGui.QCheckBox(self.centralwidget) 34 | self.auto_key_ic.setObjectName(_fromUtf8("auto_key_ic")) 35 | self.gridLayout.addWidget(self.auto_key_ic, 6, 0, 1, 1) 36 | self.ratio_slide_bar = QtGui.QSlider(self.centralwidget) 37 | self.ratio_slide_bar.setProperty("value", 50) 38 | self.ratio_slide_bar.setOrientation(QtCore.Qt.Horizontal) 39 | self.ratio_slide_bar.setObjectName(_fromUtf8("ratio_slide_bar")) 40 | self.gridLayout.addWidget(self.ratio_slide_bar, 4, 3, 1, 1) 41 | self.view_vmd = QtGui.QPushButton(self.centralwidget) 42 | self.view_vmd.setObjectName(_fromUtf8("view_vmd")) 43 | self.gridLayout.addWidget(self.view_vmd, 11, 0, 1, 1) 44 | self.reactant = QtGui.QPushButton(self.centralwidget) 45 | self.reactant.setObjectName(_fromUtf8("reactant")) 46 | self.gridLayout.addWidget(self.reactant, 0, 0, 1, 1) 47 | self.label = QtGui.QLabel(self.centralwidget) 48 | self.label.setObjectName(_fromUtf8("label")) 49 | self.gridLayout.addWidget(self.label, 4, 1, 1, 1) 50 | self.reactant_text = QtGui.QTextEdit(self.centralwidget) 51 | self.reactant_text.setObjectName(_fromUtf8("reactant_text")) 52 | self.gridLayout.addWidget(self.reactant_text, 0, 1, 2, 3) 53 | self.auto_ic_select = QtGui.QCheckBox(self.centralwidget) 54 | self.auto_ic_select.setObjectName(_fromUtf8("auto_ic_select")) 55 | self.gridLayout.addWidget(self.auto_ic_select, 5, 0, 1, 1) 56 | self.product_reset = QtGui.QPushButton(self.centralwidget) 57 | self.product_reset.setObjectName(_fromUtf8("product_reset")) 58 | self.gridLayout.addWidget(self.product_reset, 3, 0, 1, 1) 59 | self.ratio_value = QtGui.QLineEdit(self.centralwidget) 60 | self.ratio_value.setFrame(False) 61 | self.ratio_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 62 | self.ratio_value.setObjectName(_fromUtf8("ratio_value")) 63 | self.gridLayout.addWidget(self.ratio_value, 4, 2, 1, 1) 64 | self.reactant_reset = QtGui.QPushButton(self.centralwidget) 65 | self.reactant_reset.setObjectName(_fromUtf8("reactant_reset")) 66 | self.gridLayout.addWidget(self.reactant_reset, 1, 0, 1, 1) 67 | self.product = QtGui.QPushButton(self.centralwidget) 68 | self.product.setObjectName(_fromUtf8("product")) 69 | self.gridLayout.addWidget(self.product, 2, 0, 1, 1) 70 | self.product_text = QtGui.QTextEdit(self.centralwidget) 71 | self.product_text.setObjectName(_fromUtf8("product_text")) 72 | self.gridLayout.addWidget(self.product_text, 2, 1, 2, 3) 73 | self.key_ic_text = QtGui.QTextEdit(self.centralwidget) 74 | self.key_ic_text.setObjectName(_fromUtf8("key_ic_text")) 75 | self.gridLayout.addWidget(self.key_ic_text, 5, 1, 2, 3) 76 | self.save_xyz = QtGui.QPushButton(self.centralwidget) 77 | self.save_xyz.setObjectName(_fromUtf8("save_xyz")) 78 | self.gridLayout.addWidget(self.save_xyz, 11, 2, 1, 1) 79 | self.save_saddle = QtGui.QPushButton(self.centralwidget) 80 | self.save_saddle.setObjectName(_fromUtf8("save_saddle")) 81 | self.gridLayout.addWidget(self.save_saddle, 11, 3, 1, 1) 82 | self.tc_progressBar = QtGui.QProgressBar(self.centralwidget) 83 | self.tc_progressBar.setProperty("value", 24) 84 | self.tc_progressBar.setObjectName(_fromUtf8("tc_progressBar")) 85 | self.gridLayout.addWidget(self.tc_progressBar, 8, 0, 1, 2) 86 | self.ts_guess = QtGui.QPushButton(self.centralwidget) 87 | self.ts_guess.setObjectName(_fromUtf8("ts_guess")) 88 | self.gridLayout.addWidget(self.ts_guess, 8, 2, 1, 1) 89 | self.select_key_ic = QtGui.QPushButton(self.centralwidget) 90 | self.select_key_ic.setObjectName(_fromUtf8("select_key_ic")) 91 | self.gridLayout.addWidget(self.select_key_ic, 8, 3, 1, 1) 92 | self.reactant.raise_() 93 | self.reactant_text.raise_() 94 | self.reactant_reset.raise_() 95 | self.product_text.raise_() 96 | self.product.raise_() 97 | self.product_reset.raise_() 98 | self.label.raise_() 99 | self.tc_progressBar.raise_() 100 | self.save_saddle.raise_() 101 | self.ratio_slide_bar.raise_() 102 | self.ratio_value.raise_() 103 | self.save_xyz.raise_() 104 | self.key_ic_text.raise_() 105 | self.view_vmd.raise_() 106 | self.auto_ic_select.raise_() 107 | self.auto_key_ic.raise_() 108 | self.ts_guess.raise_() 109 | self.select_key_ic.raise_() 110 | MainWindow.setCentralWidget(self.centralwidget) 111 | self.menubar = QtGui.QMenuBar(MainWindow) 112 | self.menubar.setGeometry(QtCore.QRect(0, 0, 600, 22)) 113 | self.menubar.setObjectName(_fromUtf8("menubar")) 114 | self.menuSaddle = QtGui.QMenu(self.menubar) 115 | self.menuSaddle.setObjectName(_fromUtf8("menuSaddle")) 116 | MainWindow.setMenuBar(self.menubar) 117 | self.statusbar = QtGui.QStatusBar(MainWindow) 118 | self.statusbar.setObjectName(_fromUtf8("statusbar")) 119 | MainWindow.setStatusBar(self.statusbar) 120 | self.actionAbout_Saddle = QtGui.QAction(MainWindow) 121 | self.actionAbout_Saddle.setObjectName(_fromUtf8("actionAbout_Saddle")) 122 | self.actionNice = QtGui.QAction(MainWindow) 123 | self.actionNice.setObjectName(_fromUtf8("actionNice")) 124 | self.actionAgain = QtGui.QAction(MainWindow) 125 | self.actionAgain.setObjectName(_fromUtf8("actionAgain")) 126 | self.actionThird = QtGui.QAction(MainWindow) 127 | self.actionThird.setObjectName(_fromUtf8("actionThird")) 128 | self.actionSaddle = QtGui.QAction(MainWindow) 129 | self.actionSaddle.setObjectName(_fromUtf8("actionSaddle")) 130 | self.menuSaddle.addAction(self.actionSaddle) 131 | self.menubar.addAction(self.menuSaddle.menuAction()) 132 | 133 | self.retranslateUi(MainWindow) 134 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 135 | 136 | def retranslateUi(self, MainWindow): 137 | MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None)) 138 | self.auto_key_ic.setText(_translate("MainWindow", "Auto Key IC Select", None)) 139 | self.view_vmd.setText(_translate("MainWindow", "View in VMD", None)) 140 | self.reactant.setText(_translate("MainWindow", "Reactant", None)) 141 | self.label.setText(_translate("MainWindow", "Initial Guess Ratio", None)) 142 | self.auto_ic_select.setText(_translate("MainWindow", "Auto IC Select", None)) 143 | self.product_reset.setText(_translate("MainWindow", "Reset", None)) 144 | self.ratio_value.setText(_translate("MainWindow", "0.50", None)) 145 | self.reactant_reset.setText(_translate("MainWindow", "Reset", None)) 146 | self.product.setText(_translate("MainWindow", "Product", None)) 147 | self.save_xyz.setText(_translate("MainWindow", "Save as .xyz", None)) 148 | self.save_saddle.setText(_translate("MainWindow", "Save as .saddle", None)) 149 | self.ts_guess.setText(_translate("MainWindow", "Get TS Guess", None)) 150 | self.select_key_ic.setText(_translate("MainWindow", "Select Key IC", None)) 151 | self.menuSaddle.setTitle(_translate("MainWindow", "Saddle", None)) 152 | self.actionAbout_Saddle.setText(_translate("MainWindow", "ajgdagjkalsjgda", None)) 153 | self.actionNice.setText(_translate("MainWindow", "nice", None)) 154 | self.actionAgain.setText(_translate("MainWindow", "again", None)) 155 | self.actionThird.setText(_translate("MainWindow", "third", None)) 156 | self.actionSaddle.setText(_translate("MainWindow", "Saddle", None)) 157 | 158 | -------------------------------------------------------------------------------- /gui/gui_ts_guess.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 600 10 | 800 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | Auto Key IC Select 22 | 23 | 24 | 25 | 26 | 27 | 28 | 50 29 | 30 | 31 | Qt::Horizontal 32 | 33 | 34 | 35 | 36 | 37 | 38 | View in VMD 39 | 40 | 41 | 42 | 43 | 44 | 45 | Reactant 46 | 47 | 48 | 49 | 50 | 51 | 52 | Initial Guess Ratio 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Auto IC Select 63 | 64 | 65 | 66 | 67 | 68 | 69 | Reset 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0.50 77 | 78 | 79 | false 80 | 81 | 82 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 83 | 84 | 85 | 86 | 87 | 88 | 89 | Reset 90 | 91 | 92 | 93 | 94 | 95 | 96 | Product 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | Save as .xyz 110 | 111 | 112 | 113 | 114 | 115 | 116 | Save as .saddle 117 | 118 | 119 | 120 | 121 | 122 | 123 | 24 124 | 125 | 126 | 127 | 128 | 129 | 130 | Get TS Guess 131 | 132 | 133 | 134 | 135 | 136 | 137 | Select Key IC 138 | 139 | 140 | 141 | 142 | reactant 143 | reactant_text 144 | reactant_reset 145 | product_text 146 | product 147 | product_reset 148 | label 149 | tc_progressBar 150 | save_saddle 151 | ratio_slide_bar 152 | ratio_value 153 | save_xyz 154 | key_ic_text 155 | view_vmd 156 | auto_ic_select 157 | auto_key_ic 158 | ts_guess 159 | select_key_ic 160 | 161 | 162 | 163 | 164 | 0 165 | 0 166 | 600 167 | 22 168 | 169 | 170 | 171 | 172 | Saddle 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ajgdagjkalsjgda 182 | 183 | 184 | 185 | 186 | nice 187 | 188 | 189 | 190 | 191 | again 192 | 193 | 194 | 195 | 196 | third 197 | 198 | 199 | 200 | 201 | Saddle 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /gui/key_table_view.py: -------------------------------------------------------------------------------- 1 | import sys 2 | # import os 3 | 4 | from PyQt4 import QtGui, QtCore 5 | from gui_key_table import Ui_Dialog 6 | from horton import periodic 7 | 8 | # from gui_design import Window 9 | 10 | class KeyIcTable(QtGui.QDialog): 11 | 12 | def __init__(self, mol): 13 | super(KeyIcTable, self).__init__() 14 | self.ui = Ui_Dialog() 15 | self.ui.setupUi(self) 16 | self.mol = mol 17 | self.ic_info = mol.ts_state.procedures 18 | self.atom_info = mol.ts_state.numbers 19 | self.ui.keyic_table_widget.setColumnCount(5) 20 | self.ui.keyic_table_widget.setRowCount(len(self.ic_info)) 21 | # print self.ui.keyic_table_widget.rowCount() 22 | # print len(ic_info)+1 23 | self.ui.keyic_table_widget.setHorizontalHeaderLabels(['IC Type', 'Atom1', 'Atom2', 'Atom3', 'Atom4']) 24 | self.ui.keyic_table_widget.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch) 25 | for row in range(len(self.ic_info)): 26 | info = self.ic_info[row] 27 | if info[0] == 'add_bond_length': 28 | content = QtGui.QTableWidgetItem('Bond') 29 | elif info[0] == "add_bend_angle": 30 | content = QtGui.QTableWidgetItem('Angle') 31 | elif info[0] == 'add_dihed_angle': 32 | content = QtGui.QTableWidgetItem('Normal Dihedral') 33 | elif info[0] == 'add_dihed_angle_new_dot': 34 | content = QtGui.QTableWidgetItem('New Dot Dihedral') 35 | elif info[0] == 'add_dihed_angle_new_cross': 36 | content = QtGui.QTableWidgetItem('New Cross Dihedral') 37 | 38 | self.ui.keyic_table_widget.setItem(row, 0, content) 39 | content.setFlags(QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled) 40 | content.setCheckState(QtCore.Qt.Unchecked) 41 | atoms = info[1] 42 | for i in range(len(atoms)): 43 | sym = periodic[self.atom_info[atoms[i]]].symbol 44 | index = atoms[i] 45 | item = QtGui.QTableWidgetItem('{} ({})'.format(index, sym)) 46 | item.setFlags(QtCore.Qt.ItemIsEnabled) 47 | self.ui.keyic_table_widget.setItem(row, i+1, item) 48 | for i in range(len(atoms), 5): 49 | item = QtGui.QTableWidgetItem("") 50 | item.setFlags(QtCore.Qt.ItemIsSelectable) 51 | self.ui.keyic_table_widget.setItem(row, i+1, item) 52 | 53 | self.ui.keyic_table_widget.itemClicked.connect(self.item_check) 54 | self.ui.return_button.accepted.connect(self.accept_key_ic) 55 | self.ui.return_button.rejected.connect(self.reject_key_ic) 56 | if self.mol._ic_key_counter > 0: 57 | for i in range(self.mol._ic_key_counter): 58 | self.ui.keyic_table_widget.item(i, 0).setCheckState(QtCore.Qt.Checked) 59 | for j in range(5): 60 | if self.ui.keyic_table_widget.item(i, j).text(): 61 | self.ui.keyic_table_widget.item(i, j).setBackgroundColor(QtGui.QColor(200, 200, 200)) 62 | 63 | def item_check(self, item): 64 | if item.checkState() == QtCore.Qt.Checked: 65 | row = item.row() 66 | for i in range(5): 67 | if self.ui.keyic_table_widget.item(row, i).text(): 68 | self.ui.keyic_table_widget.item(row, i).setBackgroundColor(QtGui.QColor(200, 200, 200)) 69 | elif item.checkState() != QtCore.Qt.Checked: 70 | row = item.row() 71 | for i in range(5): 72 | if self.ui.keyic_table_widget.item(row, i).text(): 73 | self.ui.keyic_table_widget.item(row, i).setBackgroundColor(QtGui.QColor(255, 255, 255)) 74 | 75 | def accept_key_ic(self): 76 | key_ic = [] 77 | self.mol._ic_key_counter = 0 78 | rows = self.ui.keyic_table_widget.rowCount() 79 | for i in range(rows): 80 | if self.ui.keyic_table_widget.item(i, 0).checkState() == QtCore.Qt.Checked: 81 | key_ic.append(i) 82 | # print key_ic 83 | self.mol._arrange_key_ic(key_ic) 84 | # print self.mol._ic_key_counter 85 | 86 | def reject_key_ic(self): 87 | pass 88 | 89 | # if __name__ == '__main__': 90 | # app = QtGui.QApplication(sys.argv) 91 | # # gui = KeyIcTable(data, atom_info) 92 | # gui.setWindowTitle("Key IC Selection --by Derrick") 93 | # gui.show() 94 | # sys.exit(app.exec_()) 95 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.15.0 2 | pytest>=3.4.0 3 | importlib_resources>=1.0.1 4 | tox>=3.6.1 5 | setuptools>=39.0.1 6 | wheel>=0.31.1 7 | scipy>=1.1.1 8 | -------------------------------------------------------------------------------- /saddle/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # GOpt: Geometry Optimization program 3 | # Copyright (C) 2015-2017 The Grape Development Team 4 | # 5 | # This file is part of Grape. 6 | # 7 | # Grape is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # Grape is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """The main Saddle Package.""" 22 | 23 | __version__ = "0.2.1" 24 | __author__ = "Derrick Yang" 25 | __license__ = "GPLv3" 26 | -------------------------------------------------------------------------------- /saddle/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # PyGopt: Python Geometry Optimization. 3 | # Copyright (C) 2011-2018 The HORTON/PyGopt Development Team 4 | # 5 | # This file is part of PyGopt. 6 | # 7 | # PyGopt is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # PyGopt is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Config file to configure file directory. 22 | 23 | Attributes 24 | ---------- 25 | LOG_DIR : Path 26 | Path obj to save/load log file 27 | WORK_DIR : Path 28 | Path obj to save/load computation input/output file 29 | """ 30 | 31 | import json 32 | from pathlib import Path, PosixPath, WindowsPath 33 | 34 | from importlib_resources import path 35 | 36 | 37 | class Config: 38 | """Config class for file directory.""" 39 | 40 | # load config contents from conf.json 41 | with path("saddle.data", "conf.json") as json_path: 42 | with json_path.open(encoding="utf-8") as json_data_f: 43 | json_data = json.load(json_data_f) 44 | 45 | # set base path 46 | with path("saddle", "") as saddle_path: 47 | base_path = saddle_path 48 | 49 | @staticmethod 50 | def _find_path(given_path: str, system="posix"): 51 | """Turn given path into a proper path for given system. 52 | 53 | Parameters 54 | ---------- 55 | given_path : str 56 | Path to be converted 57 | system : str, optional 58 | System type for getting the path, 'posix' or 'windows' 59 | 60 | Returns 61 | ------- 62 | Path 63 | Generated path obj for locating certain directory 64 | 65 | Raises 66 | ------ 67 | ValueError 68 | If system offered is not suppported 69 | """ 70 | if system == "posix": 71 | given_path = PosixPath(given_path) 72 | elif system == "windows": 73 | given_path = WindowsPath(given_path) 74 | else: 75 | raise ValueError(f"system {system} is not supported") 76 | return given_path 77 | 78 | @classmethod 79 | def get_path(cls, key: str): 80 | """Get proper path for given key. 81 | 82 | Parameters 83 | ---------- 84 | key : str 85 | key for certain type of directory path 86 | 87 | Returns 88 | ------- 89 | Path 90 | proper path obj for given path key 91 | """ 92 | try: 93 | keyword_path = cls.json_data[key] 94 | except KeyError: 95 | print(f"Given key {key} is not in conf file") 96 | keyword_path = cls._find_path(keyword_path) 97 | if not keyword_path.is_absolute(): 98 | keyword_path = cls.base_path / keyword_path 99 | return keyword_path 100 | 101 | @classmethod 102 | def set_path(cls, key: str, new_path): 103 | """Set a new path for certain key path. 104 | 105 | Parameters 106 | ---------- 107 | key : str 108 | key path to set 109 | new_path : str 110 | Preferred new path 111 | 112 | Raises 113 | ------ 114 | ValueError 115 | Given key is not supported 116 | """ 117 | if key not in cls.json_data.keys(): 118 | raise ValueError(f"Give key {key} is not in conf file") 119 | new_path = cls._find_path(new_path) 120 | if not new_path.is_absolute(): 121 | new_path = (Path() / new_path).resolve() 122 | cls.json_data[key] = str(new_path) 123 | with path("saddle.data", "conf.json") as json_path: 124 | with json_path.open(mode="w", encoding="utf-8") as json_data_f: 125 | json.dump(cls.json_data, json_data_f) 126 | 127 | @classmethod 128 | def reset_path(cls): 129 | """Reset all path to default.""" 130 | cls.json_data["work_dir"] = "work" 131 | cls.json_data["log_dir"] = "work/log" 132 | with path("saddle.data", "conf.json") as json_path: 133 | with json_path.open(mode="w", encoding="utf-8") as json_data_f: 134 | json.dump(cls.json_data, json_data_f) 135 | 136 | 137 | WORK_DIR = Config.get_path("work_dir") 138 | 139 | LOG_DIR = Config.get_path("log_dir") 140 | -------------------------------------------------------------------------------- /saddle/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Data Module init file.""" 2 | -------------------------------------------------------------------------------- /saddle/data/conf.json: -------------------------------------------------------------------------------- 1 | {"work_dir": "work", "log_dir": "work/log"} -------------------------------------------------------------------------------- /saddle/data/single_hf_template.com: -------------------------------------------------------------------------------- 1 | %chk=${title}.chk 2 | #p hf/6-31+G ${freq} SCF(XQC) nosymmetry 3 | 4 | ${title} 5 | 6 | ${charge} ${multi} 7 | ${atoms} 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /saddle/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # PyGopt: Python Geometry Optimization. 3 | # Copyright (C) 2011-2018 The HORTON/PyGopt Development Team 4 | # 5 | # This file is part of PyGopt. 6 | # 7 | # PyGopt is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # PyGopt is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Custom Exception types.""" 22 | 23 | 24 | class NotSetError(Exception): 25 | """Value not set error.""" 26 | 27 | pass 28 | 29 | 30 | class AtomsNumberError(Exception): 31 | """Atoms number error.""" 32 | 33 | pass 34 | 35 | 36 | class ICNumberError(Exception): 37 | """Internal coordinates number error.""" 38 | 39 | pass 40 | 41 | 42 | class AtomsIndexError(Exception): 43 | """Atomic index error.""" 44 | 45 | pass 46 | 47 | 48 | class NotConvergeError(Exception): 49 | """Optimization not converge error.""" 50 | 51 | pass 52 | 53 | 54 | class InputTypeError(Exception): 55 | """Input type is not desired error.""" 56 | 57 | pass 58 | 59 | 60 | class PositiveProductError(Exception): 61 | """Positive product error.""" 62 | 63 | pass 64 | 65 | 66 | class InvalidArgumentError(Exception): 67 | """Invalid Argument error.""" 68 | 69 | pass 70 | 71 | 72 | class OverIterLimitError(Exception): 73 | """Over iteration limit error.""" 74 | 75 | pass 76 | 77 | 78 | class InvalidInputError(Exception): 79 | """Invalid input error.""" 80 | 81 | pass 82 | 83 | 84 | class OptError(Exception): 85 | """Opt result error.""" 86 | 87 | pass 88 | -------------------------------------------------------------------------------- /saddle/gaussianwrapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # PyGopt: Python Geometry Optimization. 3 | # Copyright (C) 2011-2018 The HORTON/PyGopt Development Team 4 | # 5 | # This file is part of PyGopt. 6 | # 7 | # PyGopt is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # PyGopt is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """wrap over Gaussian to run gaussian calculation.""" 22 | 23 | import os 24 | from pathlib import Path 25 | from string import Template 26 | 27 | from importlib_resources import read_text 28 | 29 | import numpy as np 30 | 31 | from saddle.conf import WORK_DIR 32 | from saddle.fchk import FCHKFile 33 | from saddle.periodic.periodic import angstrom, periodic 34 | 35 | __all__ = ("GaussianWrapper",) 36 | 37 | 38 | class GaussianWrapper(object): 39 | """Gaussian wrapper class.""" 40 | 41 | counter = 0 42 | 43 | template = Template(read_text("saddle.data", "single_hf_template.com")) 44 | 45 | def __init__(self, molecule, title): 46 | """Initialize Gaussian Wrapper class. 47 | 48 | Parameters 49 | ---------- 50 | molecule : Molecule 51 | Molecule instance with coordinates and numbers 52 | title : str 53 | molecule title 54 | """ 55 | self.molecule = molecule 56 | self.title = title 57 | 58 | def run_gaussian_and_get_result( 59 | self, 60 | charge, 61 | multi, 62 | *_, 63 | coordinates=True, 64 | energy=True, 65 | gradient=False, 66 | hessian=False 67 | ): 68 | """Run gaussian calculation and get the result from .fchk file. 69 | 70 | Parameters 71 | ---------- 72 | charge : int 73 | Charge of the molecule 74 | multi : int 75 | Multiplicity of the molecule 76 | coordinates : bool, kwarg, optional 77 | get coordinates from the result 78 | energy : bool, kwarg, optional 79 | get energy from the result 80 | gradient : bool, kwarg, optional 81 | get eneryg gradient from the result 82 | hessian : bool, kwarg, optional 83 | get energy hessian from the result 84 | """ 85 | freq = "" 86 | if gradient or hessian: 87 | freq = "freq" 88 | # TODO: if gradient, use Force, if hessian, use FREQ 89 | filename = self._create_input_file(charge, multi, freq=freq) 90 | # print "gausian is going to run \n{} \n{} \n{}".format(charge, multi, 91 | # self.molecule.ic) 92 | fchk_file = self._run_gaussian(filename) 93 | assert isinstance( 94 | fchk_file, FCHKFile 95 | ), "Gaussian calculation didn't run properly" 96 | result = [None] * 4 97 | if coordinates: 98 | result[0] = fchk_file.get_coordinates() 99 | if energy: 100 | result[1] = fchk_file.get_energy() 101 | if gradient: 102 | result[2] = fchk_file.get_gradient() 103 | if hessian: 104 | result[3] = fchk_file.get_hessian() 105 | return result 106 | 107 | def create_gauss_input( 108 | self, charge, multi, freq="freq", spe_title="", path="", postfix=".com" 109 | ): 110 | """Create a gaussian style input file. 111 | 112 | Parameters 113 | ---------- 114 | charge : int 115 | molecular charge 116 | multi : int 117 | molecular multiplicity 118 | freq : str, optional 119 | frequency calculation keyward 120 | spe_title : str, optional 121 | file name 122 | path : str, optional 123 | path of the file to be created 124 | postfix : str, optional 125 | poxis of input file, .com or .gjf 126 | """ 127 | assert isinstance(path, str) or isinstance(path, Path) 128 | assert isinstance(spe_title, str) 129 | atoms = "" 130 | for i in range(len(self.molecule.numbers)): 131 | x, y, z = self.molecule.coordinates[i] / angstrom 132 | atoms += "%2s % 10.5f % 10.5f % 10.5f \n" % ( 133 | periodic[self.molecule.numbers[i]].symbol, 134 | x, 135 | y, 136 | z, 137 | ) 138 | if spe_title: 139 | filename = spe_title 140 | elif self.title: 141 | filename = self.title 142 | else: 143 | raise ValueError("file name is not specified") 144 | if path: 145 | path = os.path.join(path, filename + postfix) 146 | else: 147 | path = os.path.join(WORK_DIR, filename + postfix) 148 | with open(path, "w") as f: 149 | f.write( 150 | self.template.substitute( 151 | charge=charge, freq=freq, multi=multi, atoms=atoms, title=filename 152 | ) 153 | ) 154 | GaussianWrapper.counter += 1 155 | 156 | def _create_input_file(self, charge, multi, freq="freq"): 157 | """Create input file for gaussian.""" 158 | filename = "{0}_{1}".format(self.title, self.counter) 159 | self.create_gauss_input(charge, multi, freq=freq, spe_title=filename) 160 | return filename 161 | 162 | def _run_gaussian(self, filename, fchk=True, command_bin="g09"): 163 | """Run gaussian calculation and format the output file to .fchk.""" 164 | fchk_ob = None 165 | path = WORK_DIR 166 | os.chdir(path) 167 | os.system("{0} {1}.com".format(command_bin, filename)) 168 | if fchk: 169 | logname = "{0}.log".format(filename) 170 | if os.path.isfile(os.path.join(path, logname)) and self._log_finish_test( 171 | os.path.join(path, logname) 172 | ): 173 | os.system( 174 | "formchk {0}.chk {0}.fchk".format(os.path.join(path, filename)) 175 | ) 176 | fchk_ob = FCHKFile("{0}.fchk".format(os.path.join(path, filename))) 177 | # os.chdir(os.path.join(self.pwd, '..')) 178 | # print("change_back", self.pwd) 179 | return fchk_ob 180 | 181 | def _log_finish_test(self, logname): 182 | """Check whether the calculation run properly.""" 183 | flag = False 184 | with open(logname) as f: 185 | for line in f: 186 | if "Normal termination" in line: 187 | flag = True 188 | return flag 189 | 190 | 191 | if __name__ == "__main__": 192 | from collections import namedtuple 193 | 194 | molecule = namedtuple("molecule", "numbers, coordinates") 195 | aa = molecule([1, 3], np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])) 196 | a = GaussianWrapper(aa, "text_wrapper") 197 | print(a.template) 198 | a._create_input_file(0, 2) 199 | a._create_input_file(0, 2, "freq") 200 | -------------------------------------------------------------------------------- /saddle/math_lib.py: -------------------------------------------------------------------------------- 1 | """Mathlib module.""" 2 | 3 | from typing import Callable, Tuple 4 | 5 | import numpy as np 6 | 7 | from saddle.errors import OverIterLimitError, PositiveProductError 8 | 9 | __all__ = ("ridders_solver", "diagonalize") 10 | 11 | 12 | def ridders_solver( 13 | func: Callable[[float], float], 14 | x1: float, 15 | x2: float, 16 | iteration: int = 500, 17 | error: float = 1e-6, 18 | ) -> float: 19 | """Solver nonlinear equation with ridders solver. 20 | 21 | To find a mathematical root for a continuous function. the value of the two 22 | end should be of different sign. 23 | 24 | Parameters 25 | ---------- 26 | func : Callable[[float], float] 27 | function to find the right root 28 | x1 : float 29 | left end of interval 30 | x2 : float 31 | right end of interval 32 | iteration : int, optional 33 | numbers of iterations, default is 30 34 | error : float, optional 35 | the threshold for convergence, 36 | default is 10e-6 37 | 38 | Raises 39 | ------ 40 | OverIterLimitError 41 | Description 42 | PositiveProductError 43 | when the function value of two ends of the 44 | interval is of the same sign 45 | 46 | Returns 47 | ------- 48 | float 49 | the root for function in the interval between x1 and x2 50 | """ 51 | f1 = func(x1) 52 | f2 = func(x2) 53 | if f1 * f2 > 0: 54 | raise PositiveProductError("The two end point are of same sign") 55 | answer: float = 0 56 | if np.allclose(f1, 0): 57 | return x1 58 | elif np.allclose(f2, 0): 59 | return x2 60 | for _ in range(iteration): 61 | x3 = 0.5 * (x1 + x2) 62 | f3 = func(x3) 63 | s = np.sqrt(f3 * f3 - f1 * f2) 64 | if np.allclose(s, 0): 65 | return answer 66 | x4 = x3 + (x3 - x1) * np.sign(f1 - f2) * f3 / s 67 | if abs(x4 - answer) < error: 68 | return answer 69 | answer = x4 70 | f4 = func(x4) 71 | if np.allclose(f4, 0.0): 72 | return answer 73 | if np.sign(f4) != np.sign(f3): 74 | x1, f1 = x3, f3 75 | x2, f2 = x4, f4 76 | elif np.sign(f4) != np.sign(f1): 77 | x2, f2 = x4, f4 78 | elif np.sign(f4) != np.sign(f2): 79 | x1, f1 = x4, f4 80 | else: 81 | raise PositiveProductError("The two end point are of same sign") 82 | raise OverIterLimitError 83 | 84 | 85 | def diagonalize( 86 | matrix: "np.ndarray[float]", 87 | ) -> Tuple["np.ndarray[float]", "np.ndarray[float]"]: 88 | """Orthogonilize a given matrix my Grammian Matrix method. 89 | 90 | Arguments 91 | --------- 92 | matrix : np.ndarray[float] 93 | Given matrix to be diagonalized 94 | 95 | Returns 96 | ------- 97 | (w, v) : Tuple['np.ndarray[float]', 'np.ndarray[float]'] 98 | w is the eigenvalues of the Grammian matrix 99 | v is the eigenvectors of the Grammian matrix, each column is one vector 100 | """ 101 | product = np.dot(matrix, matrix.T) 102 | w, v = np.linalg.eigh(product) 103 | return w, v 104 | 105 | 106 | def pse_inv(matrix): 107 | """Calculate pseudo inverse of given matrix. 108 | 109 | Parameters 110 | ---------- 111 | matrix : np.ndarray(N, K) 112 | a 2-dimention numpy array 113 | 114 | Returns 115 | ------- 116 | np.ndarray(K, N) 117 | pseudo inverse of given matrix, inverse if it is revertible 118 | """ 119 | assert isinstance(matrix, np.ndarray) 120 | matrix[abs(matrix) < 1e-7] = 0 121 | shape = matrix.shape[::-1] 122 | u, s, vh = np.linalg.svd(matrix) 123 | s[abs(s) < 1e-7] = 0 124 | s[s != 0] = 1 / s[s != 0] 125 | 126 | s_mtr = np.zeros(shape) 127 | s_mtr[: len(s), : len(s)] = np.diag(s) 128 | res = np.dot(np.dot(vh.T, s_mtr), u.T) 129 | res[abs(res) < 1e-7] = 0 130 | # infunction test 131 | diff = np.dot(np.dot(matrix, res), matrix) - matrix 132 | assert np.allclose( 133 | np.max(np.abs(diff)), 0, atol=1e-7 134 | ), f"Pseudo inverse didn't converge\nMax diff: {np.max(np.abs(diff))}" 135 | return res 136 | 137 | 138 | def maximum_overlap(target_mtr, input_mtr): 139 | """Compute the rotation matrix of maximum overlap for given input matrix. 140 | 141 | Parameters 142 | ---------- 143 | target_mtr : np.ndarray(M, N) 144 | target basis 145 | input_mtr : np.ndarray(M, n) 146 | input basis to be rotate 147 | 148 | Returns 149 | ------- 150 | np.ndarray 151 | The transform matrix for input matrix to rotate to maximum overlap 152 | with target matrix. 153 | """ 154 | if target_mtr.ndim == 1 or input_mtr.ndim == 1: 155 | raise ("Input array need to be 2d array, reshape 1d array with (n, 1)") 156 | if target_mtr.shape != input_mtr.shape: 157 | raise ( 158 | f"Different shape of matrices, got {target_mtr.shape}, {input_mtr.shape}" 159 | ) 160 | outer_space = np.dot(target_mtr, input_mtr.T) 161 | u, _, v = np.linalg.svd(outer_space) 162 | return np.dot(u, v) 163 | 164 | 165 | def procrustes(original, target): 166 | if not isinstance(original, np.ndarray) or not isinstance(target, np.ndarray): 167 | raise TypeError("The input is not the right type.") 168 | if original.shape != target.shape: 169 | raise ValueError("The shape of two matrix is not the same") 170 | if original.ndim < 2 or original.shape[0] == 1: 171 | raise ValueError(f"The shape of given matrix is not valid -- {original.shape}") 172 | proj = np.dot(target, original.T) 173 | u, _, v = np.linalg.svd(proj) 174 | tf_mtr = np.dot(u, v) 175 | return np.dot(tf_mtr, original) 176 | -------------------------------------------------------------------------------- /saddle/opt.py: -------------------------------------------------------------------------------- 1 | """Transform optimization process module.""" 2 | import numpy as np 3 | 4 | from saddle.math_lib import pse_inv, ridders_solver 5 | 6 | __all__ = ("Point", "GeoOptimizer") 7 | 8 | 9 | class Point(object): 10 | """Point class for holding optimiztaion property.""" 11 | 12 | def __init__(self, gradient, hessian, ele_number): 13 | """Initialize Point class. 14 | 15 | Parameters 16 | ---------- 17 | gradient : np.ndarray(N,) 18 | Gradient of a point 19 | hessian : np.ndarray(N, N) 20 | Hessian of a point 21 | ele_number : int 22 | Number of electron in the molecule 23 | """ 24 | self.gradient = gradient 25 | self.hessian = hessian 26 | self.trust_radius = np.sqrt(ele_number) 27 | self.step = None 28 | self._ele = ele_number 29 | 30 | @property 31 | def ele(self): 32 | """int: number of electron in the system.""" 33 | return self._ele 34 | 35 | 36 | class GeoOptimizer(object): 37 | """Coordinates Transformation optimization class.""" 38 | 39 | def __init__(self): 40 | """Initialize Geo optimization class.""" 41 | self.points = [] 42 | 43 | def __getitem__(self, index): 44 | """Add slicing functionality to class.""" 45 | return self.points[index] 46 | 47 | def converge(self, index): 48 | """Check given index point converged or not. 49 | 50 | Parameters 51 | ---------- 52 | index : int 53 | The index of the point in the points list 54 | 55 | Returns 56 | ------- 57 | bool 58 | True if it converged, otherwise False 59 | """ 60 | point = self.points[index] 61 | return max(np.abs(point.gradient)) <= 1e-7 62 | 63 | @property 64 | def newest(self): 65 | """int: the length of all points.""" 66 | return len(self.points) - 1 67 | 68 | def newton_step(self, index): 69 | """Compute newtom-raphson step for certain point index. 70 | 71 | Parameters 72 | ---------- 73 | index : int 74 | index of point 75 | 76 | Returns 77 | ------- 78 | np.ndarray 79 | newton step to be taken in the next iteration 80 | """ 81 | point = self.points[index] 82 | return -np.dot(pse_inv(point.hessian), point.gradient) 83 | 84 | def add_new(self, point): 85 | """Add a new point to the Point class. 86 | 87 | Parameters 88 | ---------- 89 | point : Point 90 | new optimization point to be added to the list 91 | """ 92 | self.points.append(point) 93 | 94 | def tweak_hessian(self, index, negative=0, threshold=0.05): 95 | """Tweak eigenvalues of hessian to be positive-semi-definite. 96 | 97 | Parameters 98 | ---------- 99 | index : int 100 | index of opt point 101 | negative : int, optional 102 | number of negative eiganvalues 103 | threshold : float, optional 104 | update hessian value if the eigenvalue is too small 105 | """ 106 | point = self.points[index] 107 | w, v = np.linalg.eigh(point.hessian) 108 | negative_slice = w[:negative] 109 | positive_slice = w[negative:] 110 | negative_slice[negative_slice > -threshold] = -threshold 111 | positive_slice[positive_slice < threshold] = threshold 112 | new_hessian = np.dot(v, np.dot(np.diag(w), v.T)) 113 | point.hessian = new_hessian 114 | 115 | def trust_radius_step(self, index, negative=0): 116 | """Compute trust radius step for optimization. 117 | 118 | Parameters 119 | ---------- 120 | index : int 121 | index of opt point 122 | negative : int, optional 123 | number of negative eigenvalues 124 | 125 | Returns 126 | ------- 127 | np.ndarray 128 | new step controlled by trust radius 129 | """ 130 | point = self.points[index] 131 | c_step = self.newton_step(index) 132 | if np.linalg.norm(c_step) <= point.trust_radius: 133 | point.step = c_step 134 | return c_step 135 | w, v = np.linalg.eigh(point.hessian) 136 | # incase different step calculated from tests 137 | max_w = round(max(w), 7) 138 | 139 | def func_step(value): 140 | """Compute proper update step.""" 141 | x = w.copy() 142 | x[:negative] = x[:negative] - value 143 | x[negative:] = x[negative:] + value 144 | new_hessian_inv = np.dot(v, np.dot(np.diag(1.0 / x), v.T)) 145 | return -np.dot(new_hessian_inv, point.gradient) 146 | 147 | def func_value(value): 148 | """Compute function value difference.""" 149 | step = func_step(value) 150 | return np.linalg.norm(step) - point.trust_radius 151 | 152 | while func_value(max_w) >= 0: 153 | max_w *= 2 154 | result = ridders_solver(func_value, 0, max_w) 155 | result = round(result, 7) # incase different test result 156 | # print ("result", result) 157 | step = func_step(result) 158 | point.step = step 159 | return step 160 | 161 | def update_trust_radius(self, index): 162 | """Update trust radius for given index point.""" 163 | point = self.points[index] 164 | pre_point = self.points[index - 1] 165 | if np.linalg.norm(point.gradient) > np.linalg.norm(pre_point.gradient): 166 | point.trust_radius = pre_point.trust_radius * 0.25 167 | return 168 | g_predict = pre_point.gradient + np.dot(pre_point.hessian, pre_point.step) 169 | if np.linalg.norm(point.gradient) - np.linalg.norm(pre_point.gradient) == 0: 170 | ratio = 3.0 171 | # if the gradient change is 0, then use the set_trust_radius 172 | else: 173 | ratio = np.linalg.norm(g_predict) - np.linalg.norm(pre_point.gradient) / ( 174 | np.linalg.norm(point.gradient) - np.linalg.norm(pre_point.gradient) 175 | ) 176 | if 0.8 <= ratio <= 1.25: 177 | point.trust_radius = pre_point.trust_radius * 2.0 178 | elif 0.2 <= ratio <= 6: 179 | point.trust_radius = pre_point.trust_radius 180 | else: 181 | point.trust_radius = pre_point.trust_radius * 0.5 182 | point.trust_radius = min( 183 | max(point.trust_radius, 0.1 * np.sqrt(point.ele)), 2.0 * np.sqrt(point.ele) 184 | ) 185 | -------------------------------------------------------------------------------- /saddle/optimizer/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # GOpt: Geometry Optimization program 3 | # Copyright (C) 2015-2017 The Grape Development Team 4 | # 5 | # This file is part of Grape. 6 | # 7 | # Grape is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # Grape is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Geometry optimizer module.""" 22 | -------------------------------------------------------------------------------- /saddle/optimizer/errors.py: -------------------------------------------------------------------------------- 1 | """Optimization customed error module.""" 2 | 3 | 4 | class UpdateError(Exception): 5 | """Optimization step update error.""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /saddle/optimizer/hessian_modify.py: -------------------------------------------------------------------------------- 1 | """Modify matrix engenvalues module.""" 2 | import numpy as np 3 | 4 | 5 | def modify_hessian(matrix, neg_num, key_ic=0, pos_value=0.005, neg_value=-0.005): 6 | """Modify eigenvalues of given matrix. 7 | 8 | Parameters 9 | ---------- 10 | matrix : np.ndarray(N, N) 11 | A symmetric square matrix with well defined eigenvalues 12 | neg_num : int 13 | number of negative eigenvalue(s) preferred. 14 | key_ic : int, optional 15 | number of key internal coordinates in the system 16 | pos_value : float, optional 17 | the positive eigenvalues threshold 18 | neg_value : float, optional 19 | the negative eigenvalues threshold 20 | 21 | Returns 22 | ------- 23 | np.ndarray(N, N) 24 | New modified square matrix 25 | """ 26 | val, vec = np.linalg.eigh(matrix) 27 | value = val.copy() 28 | total = len(value) 29 | assert 0 <= neg_num <= total 30 | assert 0 <= key_ic <= total 31 | neg = np.sum([value < 0]) 32 | if neg == neg_num: 33 | value[:neg][value[:neg] > neg_value] = neg_value 34 | value[neg:][value[neg:] < pos_value] = pos_value 35 | elif neg < neg_num: 36 | diff = neg_num - neg 37 | pos_vec = vec[:, neg:] 38 | if key_ic == total: 39 | value[neg:neg_num] = neg_value 40 | else: 41 | pos_sum = np.sum((pos_vec ** 2)[:key_ic, :], axis=0) 42 | seq_ind = np.argsort(pos_sum)[::-1] 43 | value[seq_ind[:diff] + neg] = neg_value 44 | else: 45 | diff = neg - neg_num 46 | neg_vec = vec[:, :neg] 47 | if key_ic == total: 48 | value[neg_num:neg] = pos_value 49 | else: 50 | neg_sum = np.sum((neg_vec ** 2)[:key_ic, :], axis=0) 51 | seq_ind = np.argsort(neg_sum) 52 | value[seq_ind[:diff]] = pos_value 53 | value[(value > 0) & (value < pos_value)] = pos_value 54 | value[(value > neg_value) & (value < 0)] = neg_value 55 | return np.dot(np.dot(vec, np.diag(value)), vec.T) 56 | 57 | 58 | def modify_hessian_with_pos_defi( 59 | matrix, neg_num, key_ic, pos_value=0.005, neg_value=-0.005 60 | ): 61 | """Modify hessian matrix with non reduced part positive definite. 62 | 63 | Parameters 64 | ---------- 65 | matrix : np.ndarray(N, N) 66 | the original matrix to be modified 67 | neg_num : int 68 | number of negative eigenvalues 69 | key_ic : int 70 | number of key internal coordinates 71 | pos_value : float, optional 72 | positive eigenvalues threshold 73 | neg_value : TYPE, optional 74 | negative eigenvalues threshold 75 | 76 | Returns 77 | ------- 78 | np.ndarray(N, N) 79 | New modified square matrix 80 | """ 81 | assert neg_num <= key_ic 82 | matrix = matrix.copy() 83 | assert len(matrix[:, 0]) == len(matrix[0]) # make sure it is square 84 | out_mat = matrix[key_ic:, key_ic:] 85 | in_mat = matrix[:key_ic, :key_ic] 86 | out_part_mat = modify_hessian(out_mat, 0, 0, pos_value) 87 | matrix[key_ic:, key_ic:] = out_part_mat 88 | in_part_mat = modify_hessian(in_mat, neg_num, key_ic, pos_value, neg_value) 89 | matrix[:key_ic, :key_ic] = in_part_mat 90 | # out_part_mat[:key_ic, :key_ic] = in_part_mat 91 | # matrix[key_ic:, key_ic:] = out_part_mat 92 | return modify_hessian(matrix, neg_num, key_ic, pos_value, neg_value) 93 | -------------------------------------------------------------------------------- /saddle/optimizer/path_point.py: -------------------------------------------------------------------------------- 1 | """Reaction Path point instance used in optimization process.""" 2 | 3 | from copy import deepcopy 4 | 5 | import numpy as np 6 | 7 | from saddle.errors import NotSetError 8 | from saddle.math_lib import pse_inv 9 | 10 | 11 | class PathPoint: 12 | """PathPoint class for optimization.""" 13 | 14 | def __init__(self, red_int): 15 | """Initialize a PathPoint for optimization process. 16 | 17 | Parameters 18 | ---------- 19 | red_int : ReducedInternal 20 | an structure reducedinternal instance 21 | """ 22 | self._instance = red_int 23 | self._step = None 24 | self._stepsize = None 25 | self._mod_hessian = None 26 | self._step_hessian = None 27 | 28 | @property 29 | def instance(self): 30 | """ReducedInternal: The reduced internal instance correspound to this point.""" 31 | return self._instance 32 | 33 | @property 34 | def energy(self): 35 | """float: the energy of reduced internal.""" 36 | return self._instance.energy 37 | 38 | @property 39 | def x_gradient(self): 40 | """np.ndarray(3N,): the cartesian gradient of reduced internal.""" 41 | return self._instance.energy_gradient 42 | 43 | @property 44 | def x_hessian(self): 45 | """np.ndarray(3N, 3N): the cartesian Hessian of reduced internal.""" 46 | return self._instance.energy_hessian 47 | 48 | @property 49 | def b_matrix(self): 50 | """np.ndarray(3N, n): the cartesian to internal transform gradient.""" 51 | return self._instance.b_matrix 52 | 53 | @property 54 | def q_gradient(self): 55 | """np.ndarray(n,): the internal gradient of reduced internal.""" 56 | return self._instance.q_gradient 57 | 58 | @property 59 | def q_hessian(self): 60 | """np.ndarray(n, n): the internal hessian of reduced internal.""" 61 | return self._instance.q_hessian 62 | 63 | @property 64 | def vspace(self): 65 | """np.ndarray(n, 3N-6): the transform matrix from reduced to internal.""" 66 | return self._instance.vspace 67 | 68 | @property 69 | def v_gradient(self): 70 | """np.ndarray(3N-6): the vspace gradient of reduced internal.""" 71 | return self._instance.v_gradient 72 | 73 | @property 74 | def v_hessian(self): 75 | """np.ndarray(3N-6, 3N-6): the vspace Hessian of reduced internal.""" 76 | if self._mod_hessian is not None: 77 | return self._mod_hessian 78 | return self.raw_hessian 79 | 80 | @property 81 | def step_hessian(self): 82 | """np.ndarray(3N, 3N): updated and modified Hessian matrix for step calculation.""" 83 | if self._step_hessian is None: 84 | raise NotSetError("Step hessian is not set yet") 85 | return self._step_hessian 86 | 87 | @step_hessian.setter 88 | def step_hessian(self, value): 89 | """Set a new value to cartesian Hessian matrix.""" 90 | assert value.shape == self.v_hessian.shape 91 | self._step_hessian = value 92 | 93 | @v_hessian.setter 94 | def v_hessian(self, value): 95 | """Set a new to vspace hessian.""" 96 | if self._mod_hessian is not None: 97 | if self._mod_hessian.shape != value.shape: 98 | raise ValueError("The shape of input is not valid") 99 | if not np.allclose(value, value.T): 100 | raise ValueError("The input Hessian is not hermitian") 101 | print("Overwrite old mod_hessian") 102 | self._mod_hessian = value.copy() 103 | 104 | @property 105 | def key_ic_number(self): 106 | """int: number of key internal coordinates.""" 107 | return self._instance.key_ic_number 108 | 109 | @property 110 | def df(self): 111 | """int: degree of freedom of given molecule, normally 3N - 6.""" 112 | return self._instance.df 113 | 114 | @property 115 | def raw_hessian(self): 116 | """np.ndarray(3N, 3N): original cartesian hessian before modification.""" 117 | return self._instance.v_hessian 118 | 119 | @property 120 | def step(self): 121 | """np.ndarray(3N-6): new optimization step in v space.""" 122 | if self._step is not None: 123 | return self._step 124 | raise NotSetError 125 | 126 | @step.setter 127 | def step(self, value): 128 | """Set new step to instance.""" 129 | if np.linalg.norm(value) - self.stepsize > 1e-3: 130 | raise ValueError 131 | self._step = value.copy() 132 | 133 | @property 134 | def stepsize(self): 135 | """float: up-bound of optimization step.""" 136 | if self._stepsize is not None: 137 | return self._stepsize 138 | raise NotSetError 139 | 140 | @stepsize.setter 141 | def stepsize(self, value): 142 | """Set a new stepsize for point.""" 143 | assert value > 0 144 | self._stepsize = value 145 | 146 | def __repr__(self): 147 | """Show string representation of PathPoint.""" 148 | return f"PathPoint object" 149 | 150 | def run_calculation(self, *_, method): 151 | """Run calculation for PathPoint instance. 152 | 153 | Parameters 154 | ---------- 155 | method : str 156 | name of outer quantum chemistry software to run calculation. 157 | """ 158 | self._instance.energy_calculation(method) 159 | 160 | def update_coordinates_with_delta_v(self, step_v): 161 | """Update the struture from the change in Vspace. 162 | 163 | Parameters 164 | ---------- 165 | step_v : np.ndarray(3N-6) 166 | structure update changes in vspace 167 | """ 168 | # this function will change the coordinates of instance 169 | self._instance.update_to_new_structure_with_delta_v(step_v) 170 | # initialize all the private variables 171 | self._step = None 172 | self._mod_hessian = None 173 | self._stepsize = None 174 | 175 | def copy(self): 176 | """Make a deepcopy of the instance itself.""" 177 | return deepcopy(self) 178 | 179 | # TODO: rewrap the lower level function and test 180 | def fd_hessian(self, coord, *_, eps=0.001, method="g09"): 181 | """Run finite difference on hessian update. 182 | 183 | Parameters 184 | ---------- 185 | coord : np.ndarray(N, 3) 186 | coordinates of molecule 187 | eps : float, optional 188 | finite step for finite difference calculation 189 | method : str, optional 190 | name of outer quantum chemistry software for calculation 191 | 192 | Raises 193 | ------ 194 | ValueError 195 | if the key ic number is not a valid value 196 | """ 197 | if coord >= self.key_ic_number: 198 | raise ValueError( 199 | "given coordinates index is not a key internal coordinates" 200 | ) 201 | # create a perturbation 202 | unit_vec = np.zeros(self.df) 203 | unit_vec[coord] = eps 204 | new_pp = self.copy() 205 | new_pp.update_coordinates_with_delta_v(unit_vec) 206 | new_pp.run_calculation(method=method) 207 | # align vspace in finite diff 208 | new_pp.instance.align_vspace(self.instance) 209 | # calculate the finite hessian 210 | result = self._calculate_finite_diff_h(self, new_pp, eps=eps) 211 | # assgin result to the column and row 212 | self._mod_hessian[:, coord] = result 213 | self._mod_hessian[coord, :] = result 214 | 215 | @staticmethod # TODO: need test 216 | def _calculate_finite_diff_h(origin, new_point, eps): 217 | # calculate 218 | d_gv = (new_point.v_gradient - origin.v_gradient) / eps 219 | d_v = (new_point.vspace - origin.vspace) / eps 220 | d_b = (new_point.b_matrix - origin.b_matrix) / eps 221 | part1 = d_gv 222 | part2 = np.dot(np.dot(origin.b_matrix.T, d_v), origin.v_gradient) 223 | part3 = np.dot(d_b.T, origin.q_gradient) 224 | multiply = np.dot(origin.vspace.T, pse_inv(origin.b_matrix.T)) 225 | result = part1 - np.dot(multiply, (part2 + part3)) 226 | return result 227 | -------------------------------------------------------------------------------- /saddle/optimizer/pathloop.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from functools import partialmethod 3 | 4 | from saddle.errors import OptError 5 | 6 | # from saddle.optimizer.hessian_modify import modify_hessian_with_pos_defi 7 | from saddle.optimizer.optloop import OptLoop 8 | from saddle.optimizer.react_point import ReactPoint 9 | from saddle.optimizer.quasi_newton import QuasiNT 10 | from saddle.optimizer.step_size import Stepsize 11 | from saddle.optimizer.trust_radius import TrustRegion 12 | from saddle.reduced_internal import ReducedInternal 13 | 14 | 15 | class PathLoop(OptLoop): 16 | def __init__( 17 | self, 18 | init_structure, 19 | dir_vct, 20 | *_, 21 | quasi_nt, 22 | trust_rad, 23 | upd_size, 24 | method="g09", 25 | max_pt=0, 26 | ): 27 | if not isinstance(init_structure, ReducedInternal): 28 | raise TypeError( 29 | f"Improper input type \ 30 | {type(init_structure)} for {init_structure}" 31 | ) 32 | # TODO: possible momery saving mode 33 | self._point = [ReactPoint(init_structure, dir_vct)] 34 | self._quasi_nt = QuasiNT(quasi_nt) 35 | self._trust_rad = TrustRegion(trust_rad) 36 | self._upd_size = Stepsize(upd_size) 37 | 38 | # memory setting 39 | if max_pt == 0 or max_pt >= 2: 40 | self._max_pt = max_pt 41 | else: 42 | raise ValueError("max number of points is too small") 43 | 44 | # initialize step_size 45 | self._upd_size.initialize(self.new) 46 | self._neg = 0 47 | self._method = method 48 | self._flag = False 49 | 50 | @property 51 | def new(self): 52 | if len(self) < 1: 53 | raise OptError("Not enough points in OptLoop") 54 | return self[-1] 55 | 56 | @property 57 | def old(self): 58 | if len(self) < 2: 59 | raise OptError("Not enough points in OptLoop") 60 | return self[-2] 61 | 62 | def check_converge(self, cutoff=3e-4): 63 | if np.max(np.abs(self.new.sub_x_gradient)) < cutoff: 64 | return True 65 | return False 66 | 67 | def verify_new_point(self, new_point, *_, debug_fchk=None): 68 | assert isinstance(new_point, ReactPoint) 69 | if debug_fchk: 70 | # debug choices 71 | new_point._instance.energy_from_fchk(debug_fchk) 72 | else: 73 | new_point.run_calculation(method=self._method) 74 | if self._flag is True: 75 | self._flag = False 76 | return True 77 | if np.linalg.norm(new_point.sub_x_gradient) > np.linalg.norm( 78 | self.new.sub_x_gradient 79 | ): 80 | self.new.stepsize *= 0.25 81 | if self.new.stepsize <= 0.1 * self._upd_size.min_s: 82 | self.new.stepsize = self._upd_size.min_s 83 | self._flag = True 84 | return False 85 | else: 86 | return True 87 | 88 | @classmethod 89 | def opt_solver( 90 | cls, 91 | init_structure, 92 | dir_vect, 93 | *_, 94 | quasi_nt, 95 | trust_rad, 96 | upd_size, 97 | method="g09", 98 | max_pt=0, 99 | iterations=50, 100 | ): 101 | opt = cls( 102 | init_structure, 103 | dir_vect, 104 | quasi_nt=quasi_nt, 105 | trust_rad=trust_rad, 106 | upd_size=upd_size, 107 | method=method, 108 | max_pt=max_pt, 109 | ) 110 | 111 | # initiate counter, neg_num 112 | counter = 1 113 | 114 | # setup optimization loop 115 | while opt.check_converge() is False: 116 | print(counter) 117 | if counter > 1: 118 | # update trust region 119 | opt.update_trust_radius() 120 | # quasi newton method for updating hessian 121 | opt.update_hessian() 122 | # finite diff for hessian if need 123 | # opt.finite_diff_hessian() 124 | # regulate hessian 125 | opt.modify_hessian() 126 | # calculate new step 127 | opt.calculate_trust_step() 128 | # calculate new point 129 | new_point = opt.next_step_structure() 130 | while opt.verify_new_point(new_point) is False: 131 | opt.calculate_trust_step() 132 | new_point = opt.next_step_structure() 133 | # add new point to optimizer 134 | opt.add_new_point(new_point) 135 | 136 | counter += 1 137 | if counter > iterations: 138 | print("Failed to converge") 139 | break 140 | print("Geometry optimization finished") 141 | 142 | path_solver = partialmethod( 143 | opt_solver, quasi_nt="bfgs", trust_rad="trim", upd_size="energy" 144 | ) 145 | -------------------------------------------------------------------------------- /saddle/optimizer/quasi_newton.py: -------------------------------------------------------------------------------- 1 | """Quasi Newton methods module.""" 2 | import numpy as np 3 | from numpy import dot, outer 4 | from numpy.linalg import norm 5 | 6 | from saddle.optimizer.errors import UpdateError 7 | from saddle.optimizer.path_point import PathPoint 8 | from saddle.optimizer.secant import secant 9 | 10 | 11 | class QuasiNT: 12 | """Quasi Newton Methods function class.""" 13 | 14 | def __init__(self, method_name): 15 | """Retrive a Quasi-Newtom update function with method name. 16 | 17 | Parameters 18 | ---------- 19 | method_name : str 20 | Name of the quasi newton method 21 | 22 | Raises 23 | ------ 24 | ValueError 25 | Description 26 | """ 27 | if method_name not in QuasiNT._methods_dict: 28 | raise ValueError(f"{method_name} is not a valid name") 29 | self._name = method_name 30 | self._update_fcn = QuasiNT._methods_dict[method_name] 31 | 32 | @property 33 | def name(self): 34 | """str: name of quasi newtom update method.""" 35 | return self._name 36 | 37 | def update_hessian(self, old, new): 38 | """Update new point Hessian matrix based on given old point. 39 | 40 | Parameters 41 | ---------- 42 | old : PathPoint 43 | the old PathPoint in the optimization process 44 | new : PathPoint 45 | the new PathPoint in the optimization process 46 | 47 | Returns 48 | ------- 49 | np.ndarray(N, N) 50 | The approximated Hessian matrix for new PathPoint 51 | 52 | Raises 53 | ------ 54 | TypeError 55 | Given arguments are not PathPoint instances 56 | """ 57 | if not isinstance(old, PathPoint) or not isinstance(new, PathPoint): 58 | raise TypeError("Improper input type for {old} or {new}") 59 | sec = secant(new, old) 60 | return self._update_fcn(old.v_hessian, sec_y=sec, step=old.step) 61 | 62 | @staticmethod 63 | def simple_rank_one(hes, *_, sec_y, step): 64 | """Update Hessian matrix with Simple-Rank-One scheme. 65 | 66 | Parameters 67 | ---------- 68 | hes : np.ndarray(N, N) 69 | old hessian matrix 70 | sec_y : np.ndarray(N) 71 | second condition value calculated 72 | step : np.ndarray(N) 73 | optimization step 74 | 75 | Returns 76 | ------- 77 | np.ndarray(N, N) 78 | new hessian matrix updated 79 | """ 80 | QuasiNT._verify_type(hes, sec_y, step) 81 | p1 = sec_y - dot(hes, step) 82 | numer = dot(p1, step) ** 2 83 | denor = norm(p1) ** 2 * norm(step) ** 2 84 | if denor == 0 or numer / denor <= 1e-18: # in case zero division 85 | return hes.copy() 86 | update_h = hes + outer(p1, p1) / dot(p1, step) 87 | return update_h 88 | 89 | sr1 = simple_rank_one 90 | 91 | @staticmethod 92 | def powell_symmetric_broyden(hes, *_, sec_y, step): 93 | """Update Hessian matrix with Simple-Rank-One scheme. 94 | 95 | Parameters 96 | ---------- 97 | hes : np.ndarray(N, N) 98 | old hessian matrix 99 | sec_y : np.ndarray(N) 100 | second condition value calculated 101 | step : np.ndarray(N) 102 | optimization step 103 | 104 | Returns 105 | ------- 106 | np.ndarray(N, N) 107 | new hessian matrix updated 108 | """ 109 | if np.allclose(norm(step), 0): 110 | raise UpdateError 111 | QuasiNT._verify_type(hes, sec_y, step) 112 | p_x = sec_y - dot(hes, step) 113 | p2 = (outer(p_x, step) + outer(step, p_x)) / dot(step, step) 114 | p3 = (dot(step, p_x) / dot(step, step) ** 2) * outer(step, step) 115 | return hes + p2 - p3 116 | 117 | psb = powell_symmetric_broyden 118 | 119 | @staticmethod 120 | def broyden_fletcher(hes, *_, sec_y, step): 121 | """Update Hessian matrix with Boyden-Fletcher(BFGS) scheme. 122 | 123 | Parameters 124 | ---------- 125 | hes : np.ndarray(N, N) 126 | old hessian matrix 127 | sec_y : np.ndarray(N) 128 | second condition value calculated 129 | step : np.ndarray(N) 130 | optimization step 131 | 132 | Returns 133 | ------- 134 | np.ndarray(N, N) 135 | new hessian matrix updated 136 | """ 137 | bind = dot(hes, step) 138 | p2 = outer(sec_y, sec_y) / dot(sec_y, step) 139 | p3 = outer(bind, bind) / dot(step, bind) 140 | return hes + p2 - p3 141 | 142 | bfgs = broyden_fletcher 143 | 144 | @staticmethod 145 | def bofill(hes, *_, sec_y, step): 146 | """Update Hessian matrix with Bofill scheme. 147 | 148 | Parameters 149 | ---------- 150 | hes : np.ndarray(N, N) 151 | old hessian matrix 152 | sec_y : np.ndarray(N) 153 | second condition value calculated 154 | step : np.ndarray(N) 155 | optimization step 156 | 157 | Returns 158 | ------- 159 | np.ndarray(N, N) 160 | new hessian matrix updated 161 | """ 162 | p_x = sec_y - dot(hes, step) 163 | numer = norm(dot(step, p_x)) ** 2 164 | denor = norm(step) ** 2 * norm(p_x) ** 2 165 | ratio = 1 - numer / denor 166 | sr1_r = QuasiNT.sr1(hes, sec_y=sec_y, step=step) 167 | psb_r = QuasiNT.psb(hes, sec_y=sec_y, step=step) 168 | return (1 - ratio) * sr1_r + ratio * psb_r 169 | 170 | @staticmethod 171 | def _verify_type(old_hessian, secant_y, step) -> None: 172 | assert old_hessian.ndim == 2 173 | assert secant_y.ndim == 1 174 | assert step.ndim == 1 175 | 176 | # bound raw staticmethod to dict key words 177 | _methods_dict = { 178 | "sr1": sr1.__func__, 179 | "psb": psb.__func__, 180 | "bfgs": bfgs.__func__, 181 | "bofill": bofill.__func__, 182 | } 183 | -------------------------------------------------------------------------------- /saddle/optimizer/react_point.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from saddle.optimizer.path_point import PathPoint 3 | 4 | 5 | class ReactPoint(PathPoint): 6 | def __init__(self, red_int, dir_vect): 7 | super().__init__(red_int) 8 | self._raw_dir_vect = dir_vect 9 | 10 | @property 11 | def dir_vect(self): 12 | real_dir_vect = np.dot( 13 | self.b_matrix, np.dot(np.linalg.pinv(self.b_matrix), self._raw_dir_vect) 14 | ) 15 | return real_dir_vect / np.linalg.norm(real_dir_vect) 16 | 17 | @property 18 | def vspace(self): 19 | sub_space = np.outer(self.dir_vect, self.dir_vect) 20 | return self._instance.vspace - np.dot(sub_space, self._instance.vspace) 21 | 22 | @property 23 | def v_gradient(self): 24 | return np.dot(self.vspace.T, self.q_gradient) 25 | 26 | @property 27 | def v_hessian(self): 28 | if self._mod_hessian is None: 29 | return np.dot(np.dot(self.vspace.T, self.q_hessian), self.vspace) 30 | return self._mod_hessian 31 | 32 | @property 33 | def sub_q_gradient(self): 34 | return np.dot(self.vspace, self.v_gradient) 35 | 36 | @property 37 | def sub_x_gradient(self): 38 | return np.dot(self.b_matrix.T, self.sub_q_gradient) 39 | 40 | @v_hessian.setter 41 | def v_hessian(self, value): 42 | if self._mod_hessian is not None: 43 | if self._mod_hessian.shape != value.shape: 44 | raise ValueError("The shape of input is not valid") 45 | if not np.allclose(value, value.T): 46 | raise ValueError("The input Hessian is not hermitian") 47 | print("Overwrite old mod_hessian") 48 | self._mod_hessian = value.copy() 49 | -------------------------------------------------------------------------------- /saddle/optimizer/secant.py: -------------------------------------------------------------------------------- 1 | """Different secant condition method module.""" 2 | import numpy as np 3 | 4 | from saddle.math_lib import pse_inv 5 | from saddle.optimizer.path_point import PathPoint 6 | 7 | 8 | def secant(new_ob, old_ob): # need tests 9 | """Compute secant value with method mentioned in the original thesis. 10 | 11 | Parameters 12 | ---------- 13 | new_ob : PathPoint 14 | Previous step point 15 | old_ob : PathPoint 16 | Newly computed step point 17 | 18 | Returns 19 | ------- 20 | np.ndarray(3N) 21 | computed secont condition value 22 | """ 23 | assert isinstance(new_ob, PathPoint) 24 | assert isinstance(old_ob, PathPoint) 25 | delta_g = new_ob.v_gradient - old_ob.v_gradient 26 | delta_v = new_ob.vspace - old_ob.vspace 27 | delta_b = new_ob.b_matrix - old_ob.b_matrix 28 | part1 = np.dot( 29 | np.dot(new_ob.b_matrix.T, delta_v), new_ob.v_gradient 30 | ) # v_space gradient here is cartesian 31 | part2 = np.dot(delta_b.T, new_ob.q_gradient) # gradient here is internal 32 | inv_trans = np.dot(new_ob.vspace.T, pse_inv(new_ob.b_matrix.T)) 33 | result = delta_g - np.dot(inv_trans, (part1 + part2)) 34 | return result 35 | 36 | 37 | def secant_1(new_ob, old_ob): 38 | """Compute secant value with 1st type. 39 | 40 | Parameters 41 | ---------- 42 | new_ob : PathPoint 43 | Previous step point 44 | old_ob : PathPoint 45 | Newly computed step point 46 | 47 | Returns 48 | ------- 49 | np.ndarray(3N) 50 | computed secont condition value 51 | """ 52 | assert isinstance(new_ob, PathPoint) 53 | assert isinstance(old_ob, PathPoint) 54 | delta_g = new_ob.v_gradient - old_ob.v_gradient 55 | delta_v = new_ob.vspace - old_ob.vspace 56 | delta_b = new_ob.b_matrix - old_ob.b_matrix 57 | part1 = np.dot( 58 | np.dot(old_ob.b_matrix.T, delta_v), old_ob.v_gradient 59 | ) # v_space gradient here is cartesian 60 | part2 = np.dot(delta_b.T, old_ob.q_gradient) # gradient here is internal 61 | inv_trans = np.dot(old_ob.vspace.T, pse_inv(old_ob.b_matrix.T)) 62 | result = delta_g - np.dot(inv_trans, (part1 + part2)) 63 | return result 64 | 65 | 66 | def secant_2(new_ob, old_ob): 67 | """Compute secant value with 2nd type. 68 | 69 | Parameters 70 | ---------- 71 | new_ob : PathPoint 72 | Previous step point 73 | old_ob : PathPoint 74 | Newly computed step point 75 | 76 | Returns 77 | ------- 78 | np.ndarray(3N) 79 | computed secont condition value 80 | """ 81 | assert isinstance(new_ob, PathPoint) 82 | assert isinstance(old_ob, PathPoint) 83 | delta_g = new_ob.v_gradient - old_ob.v_gradient 84 | delta_v = new_ob.vspace - old_ob.vspace 85 | delta_inv_b = pse_inv(new_ob.b_matrix) - pse_inv(old_ob.b_matrix) 86 | part1 = np.dot( 87 | np.dot(old_ob.vspace.T, delta_inv_b.T), old_ob.x_gradient 88 | ) # v_space gradient here is cartesian 89 | part2 = np.dot(delta_v.T, old_ob.q_gradient) # gradient here is internal 90 | return delta_g + part1 + part2 91 | 92 | 93 | def secant_3(new_ob, old_ob): 94 | """Compute secant value with 3rd type. 95 | 96 | Parameters 97 | ---------- 98 | new_ob : PathPoint 99 | Previous step point 100 | old_ob : PathPoint 101 | Newly computed step point 102 | 103 | Returns 104 | ------- 105 | np.ndarray(3N) 106 | computed secont condition value 107 | """ 108 | assert isinstance(new_ob, PathPoint) 109 | assert isinstance(old_ob, PathPoint) 110 | delta_g = new_ob.v_gradient - old_ob.v_gradient 111 | delta_v = new_ob.vspace - old_ob.vspace 112 | delta_inv_b = pse_inv(new_ob.b_matrix) - pse_inv(old_ob.b_matrix) 113 | part1 = np.dot(np.dot(old_ob.vspace.T, delta_inv_b.T), old_ob.x_gradient) 114 | part2 = np.dot(np.dot(new_ob.vspace.T, delta_inv_b.T), new_ob.x_gradient) 115 | part3 = np.dot(delta_v.T, (old_ob.q_gradient + new_ob.q_gradient)) 116 | return delta_g + 0.5 * (part1 + part2) + 0.5 * part3 117 | -------------------------------------------------------------------------------- /saddle/optimizer/step_size.py: -------------------------------------------------------------------------------- 1 | """Step size to adjust proper stepsize for each pathpoint.""" 2 | 3 | import numpy as np 4 | from numpy.linalg import norm 5 | 6 | from saddle.optimizer.path_point import PathPoint 7 | 8 | 9 | class Stepsize: 10 | """Compute the proper size for each iteration pathpoint.""" 11 | 12 | def __init__(self, method_name): 13 | if method_name not in Stepsize._methods_dict.keys(): 14 | raise ValueError(f"{method_name} is not a valid name") 15 | self._name = method_name 16 | self._update_fcn = Stepsize._methods_dict[method_name] 17 | self._max_s = None 18 | self._min_s = None 19 | self._init_s = None 20 | self._init_flag = False 21 | 22 | @property 23 | def name(self): 24 | """str: the name of step size control method.""" 25 | return self._name 26 | 27 | @property 28 | def min_s(self): 29 | """float: the acceptable minimum stepsize.""" 30 | return self._min_s 31 | 32 | @property 33 | def max_s(self): 34 | """float: the acceptable maximum stepsize.""" 35 | return self.max_s 36 | 37 | def initialize(self, init_point, ratio=0.35): 38 | """Initialize stepsize computing process. 39 | 40 | Parameters 41 | ---------- 42 | init_point : PathPoint 43 | the first pathpoint(initial guess) of the optimization process 44 | ratio : float, optional 45 | the default ratio of the first step size of the maximum stepsize 46 | """ 47 | assert init_point.df > 0 48 | number_of_atoms = (init_point.df + 6) // 3 49 | self._max_s = np.sqrt(number_of_atoms) 50 | self._min_s = 0.1 * self._max_s 51 | self._init_s = ratio * self._max_s 52 | init_point.stepsize = self._init_s 53 | self._init_flag = True 54 | 55 | def update_step(self, old, new): 56 | """Get the new stepsize for the new pathpint structure. 57 | 58 | Parameters 59 | ---------- 60 | old : PathPoint 61 | old structure with all known information 62 | new : PathPoint 63 | new structure whose stepsize to be computed 64 | 65 | Returns 66 | ------- 67 | float 68 | proper stepsize value for desired update step 69 | 70 | Raises 71 | ------ 72 | TypeError 73 | Input args are not PathPoint instances 74 | """ 75 | if not isinstance(old, PathPoint) or not isinstance(new, PathPoint): 76 | raise TypeError("Improper input type for {old} or {new}") 77 | if self._init_flag is False: 78 | self.initialize(old) 79 | update_args = { 80 | "o_gradient": old.v_gradient, 81 | "o_hessian": old.v_hessian, 82 | "step": old.step, 83 | "diff_energy": new.energy - old.energy, 84 | "n_gradient": new.v_gradient, 85 | "df": old.df, 86 | "max_s": self._max_s, 87 | "min_s": self._min_s, 88 | "step_size": old.stepsize, 89 | } 90 | return self._update_fcn(**update_args) 91 | 92 | @staticmethod 93 | def energy_based_update( 94 | o_gradient, o_hessian, step, diff_energy, step_size, *_, min_s, max_s, **kwargs 95 | ): 96 | """Compute updated stepsize based on the energy difference between two steps. 97 | 98 | Parameters 99 | ---------- 100 | o_gradient : np.ndarray(N,) 101 | old structure cartesian gradient 102 | o_hessian : np.ndarray(N, N) 103 | old sructure cartesian Hessian 104 | step : np.ndarray(N,) 105 | previous optimization step 106 | diff_energy : float 107 | energy difference between two structure 108 | step_size : float 109 | stepsize of he old structure 110 | min_s : float 111 | minimum stepsize of the optimization process 112 | max_s : float 113 | maximum stepsize of the optimiztaion process 114 | **kwargs 115 | extra kwargs needed for optimization 116 | 117 | Returns 118 | ------- 119 | float 120 | proper stepsize value for desired update step 121 | """ 122 | delta_m = np.dot(o_gradient, step) + 0.5 * np.dot(step, np.dot(o_hessian, step)) 123 | ratio = delta_m / diff_energy 124 | if 0.6667 < ratio < 1.5: 125 | new_step_size = 2 * step_size 126 | return min(max(new_step_size, min_s), max_s) 127 | if 0.3333 < ratio < 3: 128 | return max(step_size, min_s) 129 | return min(0.25 * step_size, min_s) 130 | 131 | @staticmethod 132 | def gradient_based_update( 133 | o_gradient, 134 | o_hessian, 135 | n_gradient, 136 | step, 137 | df, 138 | step_size, 139 | *_, 140 | min_s, 141 | max_s, 142 | **kwargs, 143 | ): 144 | """Compute updated stepsize based on the gradient difference between two steps. 145 | 146 | Parameters 147 | ---------- 148 | o_gradient : np.ndarray(N,) 149 | old structure cartesian gradient 150 | o_hessian : np.ndarray(N, N) 151 | old sructure cartesian Hessian 152 | n_gradient : np.ndarray(N,) 153 | new structure cartesian gradient 154 | step : np.ndarray(N,) 155 | previous optimization step 156 | df : np.ndarray(N,) 157 | gradient difference between two structure 158 | step_size : float 159 | stepsize of he old structure 160 | min_s : float 161 | minimum stepsize of the optimization process 162 | max_s : float 163 | maximum stepsize of the optimiztaion process 164 | **kwargs 165 | extra kwargs needed for optimization 166 | 167 | Returns 168 | ------- 169 | float 170 | proper stepsize value for desired update step 171 | """ 172 | g_predict = o_gradient + np.dot(o_hessian, step) 173 | rho = (norm(g_predict) - norm(o_gradient)) / ( 174 | norm(n_gradient) - norm(o_gradient) 175 | ) 176 | diff_pred = g_predict - o_gradient 177 | diff_act = n_gradient - o_gradient 178 | cosine = np.dot(diff_pred, diff_act) / np.dot(norm(diff_pred), norm(diff_act)) 179 | p10 = np.sqrt(1.6424 / df + 1.11 / (df ** 2)) 180 | p40 = np.sqrt(0.064175 / df + 0.0946 / (df ** 2)) 181 | if 0.8 < rho < 1.25 and p10 < cosine: 182 | new_step = 2 * step_size 183 | return min(max(new_step, min_s), max_s) 184 | if 0.2 < rho < 6 and p40 < cosine: 185 | return max(step_size, min_s) 186 | return min(0.5 * step_size, min_s) 187 | 188 | _methods_dict = { 189 | "energy": energy_based_update.__func__, 190 | "gradient": gradient_based_update.__func__, 191 | } 192 | -------------------------------------------------------------------------------- /saddle/optimizer/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theochem/gopt/7d39db2195cfad893e201e655401885e3b151c43/saddle/optimizer/test/__init__.py -------------------------------------------------------------------------------- /saddle/optimizer/test/data/HNCS.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | HNCS.xyz 3 | S 1.9444 -0.2024 0.0000 4 | N -0.8946 -0.2024 0.0000 5 | C 0.3124 -0.2024 0.0000 6 | H -1.3621 0.6073 0.0000 7 | -------------------------------------------------------------------------------- /saddle/optimizer/test/data/HSCN.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | HSCN.xyz 3 | S 1.0604 -0.4082 0.0000 4 | N -1.7150 -0.2345 0.0000 5 | C -0.5690 -0.3005 0.0000 6 | H 1.2235 0.9431 0.0000 7 | -------------------------------------------------------------------------------- /saddle/optimizer/test/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Optimizer data init file.""" 2 | -------------------------------------------------------------------------------- /saddle/optimizer/test/data/water.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | water 3 | H 0.783837 -0.492236 -0.000000 4 | O -0.000000 0.062020 -0.000000 5 | H -0.783837 -0.492236 -0.000000 6 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_hessian_modify.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from unittest import TestCase 4 | from saddle.optimizer.hessian_modify import modify_hessian, modify_hessian_with_pos_defi 5 | 6 | 7 | # pylint: disable=E1101, E1133 8 | # Disable pylint on numpy.random functions 9 | class TestHessianModify(TestCase): 10 | def setUp(self): 11 | np.random.seed(16) 12 | matrix = np.random.rand(5, 5) 13 | self.herm = np.dot(matrix.T, matrix) 14 | 15 | def test_all_pos(self): 16 | _, vectors = np.linalg.eigh(self.herm) 17 | values = np.arange(-1, -6, -1) 18 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 19 | modified_m = modify_hessian(new_matrix, neg_num=0, key_ic=0) 20 | values = np.linalg.eigh(modified_m)[0] 21 | assert np.allclose(values, [0.005, 0.005, 0.005, 0.005, 0.005]) 22 | 23 | values = np.arange(2, -3, -1) 24 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 25 | modified_m = modify_hessian(new_matrix, neg_num=0, key_ic=0) 26 | values = np.linalg.eigh(modified_m)[0] 27 | assert np.allclose(values, [0.005, 0.005, 0.005, 1, 2]) 28 | 29 | values = np.arange(0.1, 1, 0.2) 30 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 31 | modified_m = modify_hessian(new_matrix, neg_num=0, key_ic=0) 32 | values = np.linalg.eigh(modified_m)[0] 33 | assert np.allclose(values, [0.1, 0.3, 0.5, 0.7, 0.9]) 34 | 35 | values = np.arange(-0.1, 0.8, 0.2) 36 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 37 | modified_m = modify_hessian(new_matrix, neg_num=0, key_ic=5) 38 | values = np.linalg.eigh(modified_m)[0] 39 | assert np.allclose(values, [0.005, 0.1, 0.3, 0.5, 0.7]) 40 | 41 | values = np.arange(0.001, 0.01, 0.002) 42 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 43 | modified_m = modify_hessian(new_matrix, neg_num=0, key_ic=0) 44 | values = np.linalg.eigh(modified_m)[0] 45 | assert np.allclose(values, [0.005, 0.005, 0.005, 0.007, 0.009]) 46 | 47 | def test_one_neg(self): 48 | _, vectors = np.linalg.eigh(self.herm) 49 | values = np.arange(1, 6) 50 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 51 | modified_m = modify_hessian(new_matrix, neg_num=1, key_ic=2) 52 | values, n_vectors = np.linalg.eigh(modified_m) 53 | assert np.allclose( 54 | n_vectors[:, 0] ** 2, 55 | np.array([0.69293252, 0.38870599, -0.41219818, -0.24030381, -0.37563137]) 56 | ** 2, 57 | ) 58 | assert np.allclose(values, [-0.005, 2, 3, 4, 5]) 59 | 60 | modified_m = modify_hessian(new_matrix, neg_num=1, key_ic=4) 61 | values, n_vectors = np.linalg.eigh(modified_m) 62 | assert np.allclose( 63 | n_vectors[:, 0] ** 2, 64 | np.array([0.29402663, -0.24170677, -0.35568675, 0.84128791, 0.14438752]) 65 | ** 2, 66 | ) 67 | assert np.allclose(values, [-0.005, 1, 3, 4, 5]) 68 | 69 | values = np.array([1, -1, -2, 3, 4]) 70 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 71 | modified_m = modify_hessian(new_matrix, neg_num=1, key_ic=1) 72 | values, n_vectors = np.linalg.eigh(modified_m) 73 | assert np.allclose( 74 | n_vectors[:, 0] ** 2, 75 | np.array([0.41754632, -0.46959165, -0.08110722, -0.42598055, 0.64583353]) 76 | ** 2, 77 | ) 78 | assert np.allclose(values, [-2, 0.005, 1, 3, 4]) 79 | 80 | values = np.array([-0.001, 2, 3, 4, 5]) 81 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 82 | modified_m = modify_hessian(new_matrix, neg_num=1, key_ic=2) 83 | values, n_vectors = np.linalg.eigh(modified_m) 84 | assert np.allclose( 85 | n_vectors[:, 0] ** 2, 86 | np.array([0.69293252, 0.38870599, -0.41219818, -0.24030381, -0.37563137]) 87 | ** 2, 88 | ) 89 | assert np.allclose(values, [-0.005, 2, 3, 4, 5]) 90 | 91 | values = np.arange(1, 6) 92 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 93 | modified_m = modify_hessian(new_matrix, neg_num=1, key_ic=5) 94 | values, _ = np.linalg.eigh(modified_m) 95 | assert np.allclose(values, [-0.005, 2, 3, 4, 5]) 96 | 97 | def test_multi_neg(self): 98 | _, vectors = np.linalg.eigh(self.herm) 99 | values = np.array([-1, -2, -3, -4, -5]) 100 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 101 | modified_m = modify_hessian(new_matrix, neg_num=2, key_ic=2) 102 | values, n_vectors = np.linalg.eigh(modified_m) 103 | assert np.allclose( 104 | n_vectors[:, 1] ** 2, 105 | np.array([0.69293252, 0.38870599, -0.41219818, -0.24030381, -0.37563137]) 106 | ** 2, 107 | ) 108 | assert np.allclose( 109 | n_vectors[:, 0] ** 2, 110 | np.array([0.26951539, 0.63812477, 0.48589596, 0.21054014, 0.48962863]) ** 2, 111 | ) 112 | assert np.allclose(values, [-5, -1, 0.005, 0.005, 0.005]) 113 | 114 | values = np.array([1, 2, -2, 0.001, 0.5]) 115 | new_matrix = np.dot(np.dot(vectors, np.diag(values)), vectors.T) 116 | modified_m = modify_hessian(new_matrix, neg_num=2, key_ic=4) 117 | values, n_vectors = np.linalg.eigh(modified_m) 118 | assert np.allclose( 119 | n_vectors[:, 0] ** 2, 120 | np.array([0.41754632, -0.46959165, -0.08110722, -0.42598055, 0.64583353]) 121 | ** 2, 122 | ) 123 | assert np.allclose( 124 | n_vectors[:, 1] ** 2, 125 | np.array([0.29402663, -0.24170677, -0.35568675, 0.84128791, 0.14438752]) 126 | ** 2, 127 | ) 128 | assert np.allclose(values, [-2, -0.005, 0.005, 0.5, 1]) 129 | 130 | def test_mody_hes_with_pos_defi(self): 131 | init_matrix = np.array( 132 | [ 133 | [0.11692545, -0.10407751, 0.22927565, 0.71404804, 0.48594549], 134 | [-0.10407751, -0.5162238, 1.35542663, 0.35645273, -0.37051617], 135 | [0.22927565, 1.35542663, -1.42547168, 0.57380311, 0.26283181], 136 | [0.71404804, 0.35645273, 0.57380311, -0.20644067, 1.01414279], 137 | [0.48594549, -0.37051617, 0.26283181, 1.01414279, -0.4687893], 138 | ] 139 | ) 140 | result_mat = modify_hessian_with_pos_defi(init_matrix, 1, 2) 141 | values = np.linalg.eigh(result_mat)[0] 142 | # calculate ref 143 | ref_ini = init_matrix.copy() 144 | val, vec = np.linalg.eigh(ref_ini[2:, 2:]) 145 | val[val < 0.005] = 0.005 146 | ref_ini[2:, 2:] = np.dot(np.dot(vec, np.diag(val)), vec.T) 147 | ref_val, vec = np.linalg.eigh(ref_ini) 148 | # val = [-1.73835521, -0.45764908, 0.01310965, 1.09467051, 1.54577737] 149 | ref_val[0] = 0.005 150 | assert np.allclose(values, np.sort(ref_val)) 151 | 152 | result_mat = modify_hessian_with_pos_defi(init_matrix, 2, 2) 153 | values = np.linalg.eigh(result_mat)[0] 154 | # calculate ref 155 | ref_ini = init_matrix.copy() 156 | val, vec = np.linalg.eigh(ref_ini[2:, 2:]) 157 | val[val < 0.005] = 0.005 158 | ref_ini[2:, 2:] = np.dot(np.dot(vec, np.diag(val)), vec.T) 159 | 160 | # calculate inner 2 * 2 matrix 161 | inner_m = init_matrix[:2, :2] 162 | val, vec = np.linalg.eigh(inner_m) 163 | val[val > -0.005] = -0.005 164 | ref_ini[:2, :2] = np.dot(np.dot(vec, np.diag(val)), vec.T) 165 | # ref_vec = np.linalg.eigh(ref_ini) 166 | 167 | ref_val, vec = np.linalg.eigh(ref_ini) 168 | # val = [-1.73835521, -0.45764908, 0.01310965, 1.09467051, 1.54577737] 169 | assert np.allclose(values, ref_val) 170 | 171 | np.random.seed(133) 172 | rand_mat = np.random.rand(5, 5) 173 | init_matrix = np.dot(rand_mat.T, rand_mat) 174 | result_mat = modify_hessian_with_pos_defi(init_matrix, 1, 3) 175 | values = np.linalg.eigh(result_mat)[0] 176 | np.allclose( 177 | values, 178 | [-0.005, 6.70424398e-02, 1.74207191e-01, 6.29035498e-01, 5.86943965e00], 179 | ) 180 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_opt_loop.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from unittest import TestCase 3 | 4 | import numpy as np 5 | from importlib_resources import path 6 | from saddle.optimizer.optloop import OptLoop 7 | from saddle.optimizer.quasi_newton import QuasiNT 8 | from saddle.optimizer.secant import secant 9 | from saddle.optimizer.trust_radius import TrustRegion 10 | from saddle.reduced_internal import ReducedInternal 11 | from saddle.utils import Utils 12 | 13 | 14 | class TestOptLoop(TestCase): 15 | def setUp(self): 16 | with path("saddle.optimizer.test.data", "water.xyz") as mol_path: 17 | mol = Utils.load_file(mol_path) 18 | red_int = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 19 | red_int.add_bond(0, 1) 20 | red_int.add_bond(1, 2) 21 | red_int.add_angle(0, 1, 2) 22 | self.mol = red_int 23 | 24 | def setup_opt(self): 25 | self.mol.select_key_ic(0) 26 | with path("saddle.optimizer.test.data", "water_old.fchk") as fchk_file: 27 | self.mol.energy_from_fchk(fchk_file) 28 | opt = OptLoop(self.mol, quasi_nt="bfgs", trust_rad="trim", upd_size="energy") 29 | opt.new.step_hessian = opt.new.v_hessian 30 | return opt 31 | 32 | def test_first_step(self): 33 | opt = self.setup_opt() 34 | opt.calculate_trust_step() 35 | # ref step 36 | r_p = opt.new 37 | ref_step = TrustRegion.trim(r_p.v_hessian, r_p.v_gradient, r_p.stepsize) 38 | assert np.allclose(ref_step, opt.new.step) 39 | 40 | def test_new_struct(self): 41 | opt = self.setup_opt() 42 | opt.calculate_trust_step() 43 | opt_new_p = opt.next_step_structure() 44 | # ref new structure 45 | ref_p = deepcopy(self.mol) 46 | ref_p.update_to_new_structure_with_delta_v(opt[0].step) 47 | 48 | assert np.allclose(opt_new_p.instance.coordinates, ref_p.coordinates) 49 | 50 | def test_verify_new_point(self): 51 | opt = self.setup_opt() 52 | opt.calculate_trust_step() 53 | 54 | # generate new point 55 | new_p = opt.next_step_structure() 56 | with path("saddle.optimizer.test.data", "new_step_water.fchk") as fchk_file: 57 | result = opt.verify_new_point(new_p, debug_fchk=fchk_file) 58 | assert result is True 59 | opt.add_new_point(new_p) 60 | assert len(opt) == 2 61 | assert np.allclose(opt.new.vspace, opt.old.vspace) 62 | assert np.allclose(opt.new.key_ic_number, opt.new.key_ic_number) 63 | result = opt.check_converge() 64 | assert result is False 65 | 66 | def test_next_loop(self): 67 | # bfgs update 68 | opt = self.setup_opt() 69 | opt.calculate_trust_step() 70 | 71 | new_p = opt.next_step_structure() 72 | # new_p._instance.create_gauss_input(title='new_step_water.com') 73 | with path("saddle.optimizer.test.data", "new_step_water.fchk") as fchk_file: 74 | result = opt.verify_new_point(new_p, debug_fchk=fchk_file) 75 | assert result is True 76 | opt.add_new_point(new_p) 77 | opt.update_trust_radius() 78 | # energy criterion update 79 | s = opt.old.step 80 | g = opt.old.v_gradient 81 | h = opt.old.v_hessian 82 | pred_e_diff = np.dot(g, s) + 0.5 * np.dot(np.dot(s.T, h), s) 83 | real_e_diff = opt.new.energy - opt.old.energy 84 | assert np.allclose((pred_e_diff / real_e_diff), 1.004, atol=1e-3) 85 | assert opt.new.stepsize == 2 * opt.old.stepsize 86 | 87 | def test_loop_hessian_update(self): 88 | opt = self.setup_opt() 89 | opt.calculate_trust_step() 90 | 91 | new_p = opt.next_step_structure() 92 | with path("saddle.optimizer.test.data", "new_step_water.fchk") as fchk_file: 93 | result = opt.verify_new_point(new_p, debug_fchk=fchk_file) 94 | assert result is True 95 | opt.add_new_point(new_p) 96 | opt.update_trust_radius() 97 | # hessian update and modify 98 | opt.update_hessian() 99 | sec_y = secant(opt.new, opt.old) 100 | ref_hes = QuasiNT.bfgs(opt.old.v_hessian, sec_y=sec_y, step=opt.old.step) 101 | assert np.allclose(ref_hes, opt.new.v_hessian) 102 | opt.modify_hessian() 103 | assert np.allclose(ref_hes, opt.new.v_hessian) 104 | # set non positive hessian 105 | opt.new.v_hessian = np.diag([-1, 1, 2]) 106 | opt.modify_hessian() 107 | assert np.allclose(np.diag([0.005, 1, 2]), opt.new.step_hessian) 108 | 109 | def test_finite_diff(self): 110 | opt = self.setup_opt() 111 | opt.calculate_trust_step() 112 | 113 | new_p = opt.next_step_structure() 114 | with path("saddle.optimizer.test.data", "new_step_water.fchk") as fchk_file: 115 | result = opt.verify_new_point(new_p, debug_fchk=fchk_file) 116 | assert result is True 117 | opt.add_new_point(new_p) 118 | opt.update_trust_radius() 119 | # hessian update and modify 120 | opt.update_hessian() 121 | opt.modify_hessian() 122 | answer = opt._judge_finite_diff() 123 | assert answer == [] 124 | opt.new.v_hessian = np.diag([-1, 1, 1]) 125 | answer = opt._judge_finite_diff() 126 | assert np.allclose(answer, [0]) 127 | opt.new.v_hessian = np.diag([1, -1, 1]) 128 | answer = opt._judge_finite_diff() 129 | assert answer == [] 130 | 131 | def test_one_complete_opt_loop(self): 132 | opt = self.setup_opt() 133 | opt.calculate_trust_step() 134 | 135 | new_p = opt.next_step_structure() 136 | with path("saddle.optimizer.test.data", "new_step_water.fchk") as fchk_file: 137 | result = opt.verify_new_point(new_p, debug_fchk=fchk_file) 138 | assert result is True 139 | opt.add_new_point(new_p) 140 | opt.update_trust_radius() 141 | # hessian update and modify 142 | opt.update_hessian() 143 | opt.modify_hessian() 144 | opt.calculate_trust_step() 145 | ref_step = -np.dot(np.linalg.pinv(opt.new.v_hessian), opt.new.v_gradient) 146 | assert np.allclose(ref_step, opt.new.step) 147 | opt.calculate_trust_step() 148 | new_point = opt.next_step_structure() 149 | with path("saddle.optimizer.test.data", "final_water.fchk") as fchk_file: 150 | opt.verify_new_point(new_point, debug_fchk=fchk_file) 151 | opt.add_new_point(new_point) 152 | assert opt.check_converge() is True 153 | 154 | def test_opt_initialize(self): 155 | opt = OptLoop(self.mol, quasi_nt="bfgs", trust_rad="trim", upd_size="energy") 156 | assert len(opt) == 1 157 | assert opt._max_pt == 0 158 | assert opt._neg == 0 159 | assert opt._upd_size._max_s == np.sqrt(3) 160 | assert opt[0].stepsize == 0.35 * np.sqrt(3) 161 | assert np.allclose(opt[0].v_hessian, np.eye(3)) 162 | 163 | opt = OptLoop( 164 | self.mol, quasi_nt="bfgs", trust_rad="trim", upd_size="energy", max_pt=2 165 | ) 166 | assert opt._max_pt == 2 167 | 168 | with self.assertRaises(ValueError): 169 | opt = OptLoop( 170 | self.mol, quasi_nt="bfgs", trust_rad="trim", upd_size="energy", max_pt=1 171 | ) 172 | 173 | with self.assertRaises(ValueError): 174 | opt = OptLoop( 175 | self.mol, 176 | quasi_nt="bfgs", 177 | trust_rad="trim", 178 | upd_size="energy", 179 | neg_num=1, 180 | ) 181 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_path_point.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.errors import NotSetError 6 | from saddle.optimizer.path_point import PathPoint 7 | from saddle.reduced_internal import ReducedInternal 8 | from saddle.utils import Utils 9 | 10 | 11 | class TestPathPoint(TestCase): 12 | def setUp(self): 13 | with path("saddle.optimizer.test.data", "water.xyz") as mol_path: 14 | mol = Utils.load_file(mol_path) 15 | red_int = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 16 | red_int.add_bond(1, 0) 17 | red_int.add_bond(1, 2) 18 | red_int.add_bond(0, 2) 19 | red_int.add_angle(0, 1, 2) 20 | red_int.add_angle(1, 0, 2) 21 | red_int.set_key_ic_number(2) 22 | self.ri = red_int 23 | self.pp = PathPoint(red_int) 24 | self.ri._energy = 5.0 25 | np.random.seed(10) 26 | self.ri._energy_gradient = np.random.rand(9) 27 | with self.assertRaises(NotSetError): 28 | self.ri.energy_hessian 29 | assert np.allclose( 30 | self.pp.q_gradient, 31 | np.dot(np.linalg.pinv(self.ri.b_matrix.T), self.pp.x_gradient), 32 | ) 33 | with self.assertRaises(NotSetError): 34 | self.pp.v_hessian 35 | self.pp._mod_hessian = np.eye(3) 36 | 37 | def test_basic_property(self): 38 | assert np.allclose(self.pp.v_hessian, np.eye(3)) 39 | step = -np.dot(np.linalg.pinv(self.pp.v_hessian), self.pp.v_gradient) 40 | assert np.allclose(self.pp.v_gradient, -step) 41 | with self.assertRaises(NotSetError): 42 | self.pp.step = step 43 | with self.assertRaises(NotSetError): 44 | self.pp.raw_hessian 45 | 46 | def test_copy_ob_property(self): 47 | step = -np.dot(np.linalg.pinv(self.pp.v_hessian), self.pp.v_gradient) 48 | new_pp = self.pp.copy() 49 | assert new_pp is not self.pp 50 | new_pp.update_coordinates_with_delta_v(step) 51 | assert new_pp._step is None 52 | assert new_pp._stepsize is None 53 | assert new_pp._mod_hessian is None 54 | with self.assertRaises(NotSetError): 55 | new_pp.energy 56 | with self.assertRaises(NotSetError): 57 | new_pp.v_gradient 58 | assert not np.allclose( 59 | new_pp._instance.coordinates, self.pp._instance.coordinates 60 | ) 61 | 62 | def test_finite_different(self): 63 | def fct(x, y, z): 64 | return x ** 2 + y ** 2 + z ** 2 65 | 66 | def grad(x, y, z): 67 | return np.array([2 * x, 2 * y, 2 * z]) 68 | 69 | # hessian = lambda x, y, z: np.eye(3) * 2 70 | p1, p2 = self._set_point(fct, grad) 71 | 72 | result = PathPoint._calculate_finite_diff_h(p1, p2, 0.001) 73 | assert np.allclose(result, [2, 0, 0]) 74 | 75 | def fct(x, y, z): 76 | return x ** 3 + 2 * y ** 3 + 3 * z ** 3 77 | 78 | def grad(x, y, z): 79 | return np.array([3 * x ** 2, 6 * y ** 2, 9 * z ** 2]) 80 | 81 | p1, p2 = self._set_point(fct, grad) 82 | result = PathPoint._calculate_finite_diff_h(p1, p2, 0.001) 83 | assert np.allclose(result, [6.003, 0, 0]) 84 | 85 | def _set_point(self, fct, grad, point=(1, 2, 3), step=(0.001, 0, 0)): 86 | class T(ReducedInternal): 87 | def __init__(self): 88 | pass 89 | 90 | _numbers = [None] * 3 91 | _ic = [None] * 4 92 | 93 | point_a = T() 94 | point_a._cc_to_ic_gradient = np.eye(3) 95 | point_a._vspace = np.eye(3) 96 | point_b = T() 97 | point_b._cc_to_ic_gradient = np.eye(3) 98 | point_b._vspace = np.eye(3) 99 | 100 | new_point = np.array(point) + np.array(step) 101 | # set init point 102 | point_a._energy = fct(*point) 103 | point_a._energy_gradient = grad(*point) 104 | point_a._internal_gradient = point_a._energy_gradient 105 | point_a._vspace_gradient = point_a._energy_gradient 106 | point_a._red_space = True 107 | point_a._non_red_space = True 108 | 109 | # set new poitn 110 | point_b._energy = fct(*new_point) 111 | point_b._energy_gradient = grad(*new_point) 112 | point_b._internal_gradient = point_b._energy_gradient 113 | point_b._vspace_gradient = point_b._energy_gradient 114 | point_b._red_space = True 115 | point_b._non_red_space = True 116 | return point_a, point_b 117 | 118 | def test_finite_diff_with_water(self): 119 | with path("saddle.optimizer.test.data", "water.xyz") as mol_path: 120 | mol = Utils.load_file(mol_path) 121 | red_int = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 122 | red_int.auto_select_ic() 123 | with path("saddle.optimizer.test.data", "water_old.fchk") as fchk_file: 124 | red_int.energy_from_fchk(fchk_file) 125 | assert red_int.energy - 75.99264142 < 1e-6 126 | wt_p1 = PathPoint(red_int=red_int) 127 | step = [0.001, 0, 0] 128 | print(wt_p1._instance.vspace) 129 | ref_vspace = np.array( 130 | [ 131 | [0.25801783, -0.66522226, 0.70064694], 132 | [-0.49526649, -0.71373819, -0.49526649], 133 | [-0.82954078, 0.21921937, 0.51361947], 134 | ] 135 | ) 136 | # incase different vspace basis error 137 | wt_p1._instance.set_vspace(ref_vspace) 138 | wt_p2 = wt_p1.copy() 139 | wt_p2.update_coordinates_with_delta_v(step) 140 | # wt_p2._instance.create_gauss_input(title='water_new') 141 | with path("saddle.optimizer.test.data", "water_new.fchk") as fchk_file_new: 142 | wt_p2._instance.energy_from_fchk(fchk_file_new) 143 | wt_p2._instance.align_vspace(wt_p1._instance) 144 | assert np.allclose(wt_p1.vspace, wt_p2.vspace) 145 | result = PathPoint._calculate_finite_diff_h(wt_p1, wt_p2, 0.001) 146 | assert np.allclose(result, wt_p1._instance.v_hessian[:, 0], atol=1e-2) 147 | 148 | def test_finite_diff_with_water_2(self): 149 | with path("saddle.optimizer.test.data", "water.xyz") as mol_path: 150 | mol = Utils.load_file(mol_path) 151 | red_int = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 152 | red_int.auto_select_ic() 153 | with path("saddle.optimizer.test.data", "water_old.fchk") as fchk_file: 154 | red_int.energy_from_fchk(fchk_file) 155 | assert red_int.energy - 75.99264142 < 1e-6 156 | red_int.select_key_ic(0) 157 | ref_v = np.array( 158 | [ 159 | [-1.00000000e00, -4.17292908e-16, 0.00000000e00], 160 | [2.10951257e-16, -4.69422035e-01, -8.82973926e-01], 161 | [3.39185671e-16, -8.82973926e-01, 4.69422035e-01], 162 | ] 163 | ) 164 | ref_v2 = np.dot(ref_v, ref_v.T) 165 | assert np.allclose(ref_v2, np.dot(red_int.vspace, red_int.vspace.T)) 166 | red_int.set_vspace(ref_v) 167 | wt_p1 = PathPoint(red_int=red_int) 168 | step = [-0.001, 0, 0] 169 | wt_p2 = wt_p1.copy() 170 | wt_p2.update_coordinates_with_delta_v(step) 171 | # fchk file is for -0.001 172 | with path("saddle.optimizer.test.data", "water_new_2.fchk") as fchk_file_new: 173 | wt_p2._instance.energy_from_fchk(fchk_file_new) 174 | 175 | wt_p2._instance.align_vspace(wt_p1._instance) 176 | assert np.allclose(wt_p1.vspace, wt_p2.vspace) 177 | result = PathPoint._calculate_finite_diff_h(wt_p1, wt_p2, -0.001) 178 | assert np.allclose(result, wt_p1._instance.v_hessian[:, 0], atol=1e-2) 179 | 180 | def test_finite_different_with_water_3(self): 181 | with path("saddle.optimizer.test.data", "water.xyz") as mol_path: 182 | mol = Utils.load_file(mol_path) 183 | red_int = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 184 | red_int.auto_select_ic() 185 | red_int.add_bond(0, 2) 186 | with path("saddle.optimizer.test.data", "water_old.fchk") as fchk_file: 187 | red_int.energy_from_fchk(fchk_file) 188 | assert red_int.energy - 75.99264142 < 1e-6 189 | red_int.select_key_ic(0) 190 | wt_p1 = PathPoint(red_int=red_int) 191 | step = [0.001, 0, 0] 192 | wt_p2 = wt_p1.copy() 193 | wt_p2.update_coordinates_with_delta_v(step) 194 | with path("saddle.optimizer.test.data", "water_new_3.fchk") as fchk_file_new: 195 | wt_p2._instance.energy_from_fchk(fchk_file_new) 196 | wt_p2._instance.align_vspace(wt_p1._instance) 197 | assert np.allclose(wt_p1.vspace, wt_p2.vspace, atol=1e-2) 198 | result = PathPoint._calculate_finite_diff_h(wt_p1, wt_p2, 0.001) 199 | assert np.allclose(result, wt_p1._instance.v_hessian[:, 0], atol=1e-2) 200 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_pathloop.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from copy import deepcopy 4 | import numpy as np 5 | from importlib_resources import path 6 | from saddle.optimizer.pathloop import PathLoop 7 | from saddle.optimizer.react_point import ReactPoint 8 | from saddle.optimizer.hessian_modify import modify_hessian_with_pos_defi 9 | from saddle.ts_construct import TSConstruct 10 | from saddle.optimizer.trust_radius import TrustRegion 11 | 12 | 13 | class TestPathLoop(TestCase): 14 | def setUp(self): 15 | with path("saddle.optimizer.test.data", "HNCS.xyz") as rct_f: 16 | with path("saddle.optimizer.test.data", "HSCN.xyz") as prd_f: 17 | ts_cons = TSConstruct.from_file(rct_f, prd_f) 18 | ts_cons.auto_generate_ts(dihed_special=True) 19 | self.ts = ts_cons.ts 20 | self.dir_vec = ts_cons.prd.ic_values - ts_cons.rct.ic_values 21 | with path("saddle.optimizer.test.data", "pathloop_hscn.fchk") as fchk_f: 22 | self.ts.energy_from_fchk(fchk_f) 23 | self.opt_ob = PathLoop( 24 | self.ts, self.dir_vec, quasi_nt="bfgs", trust_rad="trim", upd_size="energy" 25 | ) 26 | 27 | def test_init(self): 28 | assert isinstance(self.opt_ob, PathLoop) 29 | assert isinstance(self.opt_ob[0], ReactPoint) 30 | 31 | def test_first_step(self): 32 | dpcp_p = deepcopy(self.opt_ob[0]) 33 | # build self v 34 | proj_b = np.dot(self.ts.b_matrix, np.linalg.pinv(self.ts.b_matrix)) 35 | dir_vec = np.dot(proj_b, self.dir_vec) 36 | unit_dv = dir_vec / np.linalg.norm(dir_vec) 37 | proj_dv = np.outer(unit_dv, unit_dv) 38 | sub_v = self.ts.vspace - np.dot(proj_dv, self.ts.vspace) 39 | # test vspace 40 | assert np.allclose(sub_v, self.opt_ob[0].vspace) 41 | v_hessian = np.dot(np.dot(sub_v.T, self.ts.q_hessian), sub_v) 42 | # test v hessian 43 | assert np.allclose(v_hessian, self.opt_ob[0].v_hessian) 44 | new_sub_v = modify_hessian_with_pos_defi(v_hessian, 0, 0) 45 | 46 | # modify_hessian and test 47 | self.opt_ob.modify_hessian() 48 | assert np.allclose(self.opt_ob[0].step_hessian, new_sub_v) 49 | 50 | # calculate step 51 | ref_step = TrustRegion.trim( 52 | new_sub_v, self.opt_ob[0].v_gradient, self.opt_ob[0].stepsize 53 | ) 54 | self.opt_ob.calculate_trust_step() 55 | assert np.allclose(self.opt_ob[0].step, ref_step) 56 | 57 | # calculate next point 58 | new_p = self.opt_ob.next_step_structure() 59 | assert isinstance(new_p, ReactPoint) 60 | dpcp_p.update_coordinates_with_delta_v(ref_step) 61 | assert np.allclose(new_p._instance.coordinates, dpcp_p._instance.coordinates) 62 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_quasi_newton.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from unittest import TestCase 4 | 5 | from saddle.optimizer.quasi_newton import QuasiNT 6 | from saddle.optimizer.path_point import PathPoint 7 | 8 | bfgs = QuasiNT.bfgs 9 | psb = QuasiNT.psb 10 | sr1 = QuasiNT.sr1 11 | bofill = QuasiNT.bofill 12 | 13 | # , psb, sr1, bofill 14 | 15 | 16 | class TestInternal(TestCase): 17 | 18 | # def setUp(self): 19 | # self.sample_fcn = lambda x, y: x**2 + x*y + 2*y**2 20 | # self.sample_g = lambda x, y: np.array([2*x + y, x + 4*y]) 21 | # self.sample_h = lambda x, y: np.array([[2,1], [1,4]]) 22 | 23 | def _set_quadratic(self): 24 | self.sample_fcn = lambda x, y: x ** 2 + x * y + 2 * y ** 2 25 | self.sample_g = lambda x, y: np.array([2 * x + y, x + 4 * y]) 26 | self.sample_h = lambda x, y: np.array([[2, 1], [1, 4]]) 27 | start_point = np.array([2, 1]) 28 | # f_v = self.sample_fcn(*start_point) # f_v = 8 29 | self.init_step = np.array([-0.2, -0.2]) 30 | init_g = self.sample_g(*start_point) 31 | self.init_h = self.sample_h(*start_point) 32 | new_point = start_point + self.init_step 33 | new_g = self.sample_g(*new_point) 34 | self.y = new_g - init_g 35 | 36 | def _set_cubic(self): 37 | self.sample_fcn = lambda x, y: 1 / 3 * x ** 3 + x * y + 2 / 3 * y ** 3 38 | self.sample_g = lambda x, y: np.array([x ** 2 + y, x + 2 * y ** 2]) 39 | self.sample_h = lambda x, y: np.array([[2 * x, 1], [1, 4 * y]]) 40 | start_point = np.array([2, 1]) 41 | # f_v = self.sample_fcn(*start_point) # f_v = 8 42 | self.init_step = np.array([-0.2, -0.2]) 43 | init_g = self.sample_g(*start_point) 44 | self.init_h = self.sample_h(*start_point) 45 | new_point = start_point + self.init_step 46 | new_g = self.sample_g(*new_point) 47 | self.y = new_g - init_g 48 | # assert np.allclose(self.y, np.array([-0.96, -0.92])) 49 | 50 | def _set_path_points(self): 51 | class Other(PathPoint): 52 | def __init__(self): 53 | pass 54 | 55 | class Attr: 56 | pass 57 | 58 | self.p1 = Other() 59 | self.p2 = Other() 60 | start_point = np.array([2, 1]) 61 | self.p1._instance = Attr() 62 | self.p2._instance = Attr() 63 | self.p1._instance.b_matrix = np.eye(2) 64 | self.p1._instance.vspace = np.eye(2) 65 | self.p1._instance.v_gradient = self.sample_g(*start_point) 66 | self.p1._instance.q_gradient = self.p1._instance.v_gradient 67 | self.p1._instance.x_gradient = self.p1._instance.v_gradient 68 | self.p1._mod_hessian = self.sample_h(*start_point) 69 | self.p1._step = np.array([-0.2, -0.2]) 70 | new_point = np.array([1.8, 0.8]) 71 | self.p2._instance.b_matrix = np.eye(2) 72 | self.p2._instance.vspace = np.eye(2) 73 | self.p2._instance.v_gradient = self.sample_g(*new_point) 74 | self.p2._instance.q_gradient = self.p2._instance.v_gradient 75 | self.p2._instance.x_gradient = self.p2._instance.v_gradient 76 | 77 | def test_sr1_quad(self): 78 | self._set_quadratic() 79 | new_hessian = sr1(self.init_h, sec_y=self.y, step=self.init_step) 80 | assert np.allclose(new_hessian, self.init_h) 81 | assert np.allclose(new_hessian, np.array([[2, 1], [1, 4]])) 82 | 83 | def test_sr1_cubic(self): 84 | self._set_cubic() 85 | new_hessian = sr1(self.init_h, sec_y=self.y, step=self.init_step) 86 | ref_hessian = np.array([[3.93333333, 0.86666667], [0.86666667, 3.73333333]]) 87 | assert np.allclose(new_hessian, ref_hessian) 88 | 89 | def test_psb_quad(self): 90 | self._set_quadratic() 91 | new_hessian = psb(self.init_h, sec_y=self.y, step=self.init_step) 92 | assert np.allclose(new_hessian, self.init_h) 93 | assert np.allclose(new_hessian, np.array([[2, 1], [1, 4]])) 94 | 95 | def test_psb_cubic(self): 96 | self._set_cubic() 97 | new_hessian = psb(self.init_h, sec_y=self.y, step=self.init_step) 98 | ref_hessian = np.array([[3.95, 0.85], [0.85, 3.75]]) 99 | assert np.allclose(new_hessian, ref_hessian) 100 | 101 | def test_bfgs_quad(self): 102 | self._set_quadratic() 103 | new_hessian = bfgs(self.init_h, sec_y=self.y, step=self.init_step) 104 | assert np.allclose(new_hessian, self.init_h) 105 | assert np.allclose(new_hessian, np.array([[2, 1], [1, 4]])) 106 | 107 | def test_bfgs_cubic(self): 108 | self._set_cubic() 109 | new_hessian = bfgs(self.init_h, sec_y=self.y, step=self.init_step) 110 | ref_hessian = np.array([[3.95106383, 0.84893617], [0.84893617, 3.75106383]]) 111 | assert np.allclose(new_hessian, ref_hessian) 112 | 113 | def test_bofill_quad(self): 114 | self._set_quadratic() 115 | new_hessian = bofill(self.init_h, sec_y=self.y, step=self.init_step) 116 | assert np.allclose(new_hessian, self.init_h) 117 | assert np.allclose(new_hessian, np.array([[2, 1], [1, 4]])) 118 | 119 | def test_bofill_cubic(self): 120 | self._set_cubic() 121 | new_hessian = bofill(self.init_h, sec_y=self.y, step=self.init_step) 122 | ref_hessian_1 = np.array([[3.93333333, 0.86666667], [0.86666667, 3.73333333]]) 123 | ref_hessian_2 = np.array([[3.95, 0.85], [0.85, 3.75]]) 124 | ref_hessian = 0.9 * ref_hessian_1 + 0.1 * ref_hessian_2 125 | assert np.allclose(new_hessian, ref_hessian) 126 | 127 | def test_quasi_object(self): 128 | self._set_quadratic() 129 | with self.assertRaises(ValueError): 130 | qnt = QuasiNT("gibberish") 131 | qnt = QuasiNT("sr1") 132 | with self.assertRaises(TypeError): 133 | qnt.update_hessian(1, 2) 134 | # setup test points 135 | self._set_path_points() 136 | 137 | new_hessian = qnt.update_hessian(self.p1, self.p2) 138 | assert np.allclose(new_hessian, np.array([[2, 1], [1, 4]])) 139 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_react_point.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.optimizer.react_point import ReactPoint 6 | from saddle.ts_construct import TSConstruct 7 | 8 | # from saddle.utils import Utils 9 | 10 | 11 | # pylint: disable=E1101, E1133 12 | # Disable pylint on numpy.random functions 13 | class TestReactPoint(TestCase): 14 | def setUp(self): 15 | with path("saddle.test.data", "rct.xyz") as rct_path: 16 | with path("saddle.test.data", "prd.xyz") as prd_path: 17 | mol = TSConstruct.from_file(rct_path, prd_path) 18 | mol.auto_generate_ts(dihed_special=True) 19 | self.ts = mol.ts 20 | self.dir_vec = mol.rct.ic_values - mol.prd.ic_values 21 | self.r_p1 = ReactPoint(self.ts, self.dir_vec) 22 | 23 | def test_dir_vec(self): 24 | proj_b = np.dot(self.ts.b_matrix, np.linalg.pinv(self.ts.b_matrix)) 25 | # project twice 26 | dir_v = np.dot(proj_b, np.dot(proj_b, self.dir_vec)) 27 | assert np.allclose(dir_v / np.linalg.norm(dir_v), self.r_p1.dir_vect) 28 | assert np.linalg.norm(self.r_p1.dir_vect) - 1 <= 1e-8 29 | 30 | def test_sub_vspace(self): 31 | proj_dv = np.outer(self.r_p1.dir_vect, self.r_p1.dir_vect) 32 | # project twice 33 | ref_sub_vspace = self.ts.vspace - np.dot( 34 | proj_dv, np.dot(proj_dv, self.ts.vspace) 35 | ) 36 | assert np.allclose(ref_sub_vspace, self.r_p1.vspace) 37 | 38 | def test_sub_v_gradient(self): 39 | # set random numpy seed 40 | np.random.seed(101) 41 | # set random gradient value 42 | x_gradient = np.random.rand(12) 43 | self.ts._energy_gradient = x_gradient 44 | ref_sub_v_gradient = np.dot(self.r_p1.vspace.T, self.ts.q_gradient) 45 | # r_p1 and ts have the same q_gradient 46 | assert np.allclose(self.r_p1.q_gradient, self.ts.q_gradient) 47 | # r_p1 has different v_gradient 48 | assert not np.allclose(self.r_p1.v_gradient, self.ts.v_gradient) 49 | assert np.allclose(self.r_p1.v_gradient, ref_sub_v_gradient) 50 | 51 | def test_sub_v_hessian(self): 52 | np.random.seed(111) 53 | # set random gradient 54 | x_gradient = np.random.rand(12) 55 | self.ts._energy_gradient = x_gradient 56 | # set random hessian 57 | x_hessian_h = np.random.rand(12, 12) 58 | x_hessian = np.dot(x_hessian_h, x_hessian_h.T) 59 | self.ts._energy_hessian = x_hessian 60 | 61 | ref_sub_v_hessian = np.dot( 62 | self.r_p1.vspace.T, np.dot(self.ts.q_hessian, self.r_p1.vspace) 63 | ) 64 | assert np.allclose(self.r_p1.q_gradient, self.ts.q_gradient) 65 | assert np.allclose(self.r_p1._instance.q_hessian, self.ts.q_hessian) 66 | assert np.allclose(self.r_p1.v_hessian, ref_sub_v_hessian) 67 | 68 | def test_x_and_q_gradient(self): 69 | np.random.seed(212) 70 | # set random gradient 71 | x_gradient = np.random.rand(12) 72 | self.ts._energy_gradient = x_gradient 73 | # set random hessian 74 | ref_sub_q_g = np.dot(self.r_p1.vspace, self.r_p1.v_gradient) 75 | assert np.allclose(ref_sub_q_g, self.r_p1.sub_q_gradient) 76 | ref_sub_x_g = np.dot(self.r_p1.b_matrix.T, self.r_p1.sub_q_gradient) 77 | assert np.allclose(ref_sub_x_g, self.r_p1.sub_x_gradient) 78 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_secant.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from numpy.testing import assert_allclose 6 | from saddle.optimizer.path_point import PathPoint 7 | from saddle.optimizer.secant import secant, secant_1, secant_2, secant_3 8 | from saddle.reduced_internal import ReducedInternal 9 | from saddle.utils import Utils 10 | 11 | 12 | class TestSecant(unittest.TestCase): 13 | def setUp(self): 14 | with path("saddle.test.data", "water.xyz") as mol_path: 15 | mol = Utils.load_file(mol_path) 16 | self.old_ob = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 17 | self.old_ob.add_bond(0, 1) 18 | self.old_ob.add_bond(1, 2) 19 | self.old_ob.add_angle(0, 1, 2) 20 | with path("saddle.optimizer.test.data", "water_old.fchk") as fchk_file1: 21 | self.old_ob.energy_from_fchk(fchk_file1) 22 | self.new_ob = ReducedInternal(mol.coordinates, mol.numbers, 0, 1) 23 | self.new_ob.add_bond(0, 1) 24 | self.new_ob.add_bond(1, 2) 25 | self.new_ob.add_angle(0, 1, 2) 26 | with path("saddle.optimizer.test.data", "water_new.fchk") as fchk_file2: 27 | self.new_ob.energy_from_fchk(fchk_file2) 28 | self.new_ob.align_vspace(self.old_ob) 29 | assert_allclose(self.new_ob.vspace, self.old_ob.vspace, atol=1e-6) 30 | self.newp = PathPoint(self.new_ob) 31 | self.oldp = PathPoint(self.old_ob) 32 | 33 | def test_secant_condition(self): 34 | result = secant(self.newp, self.oldp) 35 | 36 | # separate calculation 37 | part1 = self.newp.v_gradient - self.oldp.v_gradient 38 | part2 = np.dot(self.newp.vspace.T, np.linalg.pinv(self.newp.b_matrix.T)) 39 | part3 = np.dot( 40 | self.newp.b_matrix.T, 41 | np.dot((self.newp.vspace - self.oldp.vspace), self.newp.v_gradient), 42 | ) 43 | part4 = np.dot( 44 | (self.newp.b_matrix - self.oldp.b_matrix).T, self.newp.q_gradient 45 | ) 46 | final = part1 - np.dot(part2, (part3 + part4)) 47 | assert np.allclose(result, final, atol=1e-6) 48 | 49 | def test_secant_condition_0(self): 50 | d_s = np.dot( 51 | self.old_ob.vspace.T, self.new_ob.ic_values - self.old_ob.ic_values 52 | ) 53 | ref = np.dot(self.old_ob.v_hessian, d_s) 54 | result = secant(self.newp, self.oldp) 55 | assert_allclose(ref, result, atol=1e-5) 56 | 57 | def test_secant_condition_1(self): 58 | d_s = np.dot( 59 | self.old_ob.vspace.T, self.new_ob.ic_values - self.old_ob.ic_values 60 | ) 61 | ref = np.dot(self.old_ob.v_hessian, d_s) 62 | result = secant_1(self.newp, self.oldp) 63 | assert_allclose(ref, result, atol=1e-5) 64 | 65 | def test_secant_condition_2(self): 66 | d_s = np.dot( 67 | self.old_ob.vspace.T, self.new_ob.ic_values - self.old_ob.ic_values 68 | ) 69 | ref = np.dot(self.old_ob.v_hessian, d_s) 70 | result = secant_2(self.newp, self.oldp) 71 | assert_allclose(ref, result, atol=1e-5) 72 | 73 | def test_secant_condition_3(self): 74 | d_s = np.dot( 75 | self.old_ob.vspace.T, self.new_ob.ic_values - self.old_ob.ic_values 76 | ) 77 | ref = np.dot(self.old_ob.v_hessian, d_s) 78 | result = secant_3(self.newp, self.oldp) 79 | assert_allclose(ref, result, atol=1e-5) 80 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_step_size.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from unittest import TestCase 4 | from saddle.optimizer.step_size import Stepsize 5 | from saddle.optimizer.path_point import PathPoint 6 | 7 | # function alias 8 | energy_based_update = Stepsize.energy_based_update 9 | gradient_based_update = Stepsize.gradient_based_update 10 | 11 | 12 | class test_update_trust_radius(TestCase): 13 | def setUp(self): 14 | "set up test function gradient and hessian" 15 | self.func = ( 16 | lambda x, y: x ** 3 + 2 * x ** 2 * y + 6 * x * y + 2 * y ** 3 + 6 * y + 10 17 | ) 18 | self.gradient = lambda x, y: np.array( 19 | [3 * x ** 2 + 4 * x * y + 6 * y, 2 * x ** 2 + 6 * x + 6 * y ** 2 + 6] 20 | ) 21 | self.hessian = lambda x, y: np.array( 22 | [[6 * x + 4 * y, 4 * x + 6], [4 * x + 6, 12 * y]] 23 | ) 24 | init = (1, 2) 25 | assert self.func(*init) == 55 26 | assert np.allclose(self.gradient(*init), np.array([23, 38])) 27 | assert np.allclose(self.hessian(*init), np.array([[14, 10], [10, 24]])) 28 | 29 | def test_step_update(self): 30 | init = np.array((2, 1)) 31 | o_g = self.gradient(*init) 32 | o_h = self.hessian(*init) 33 | step = np.array((-1, 1)) 34 | diff = self.func(*(init + step)) - self.func(*init) 35 | assert diff == 9 36 | assert np.allclose(o_g, np.array([26, 32])) 37 | assert np.allclose(o_h, np.array([[16, 14], [14, 12]])) 38 | est_diff = np.dot(o_g, step) + 0.5 * np.dot(step, np.dot(o_h, step)) 39 | assert est_diff == 6 40 | ratio = est_diff / diff 41 | assert ratio - 0.6666666666 < 1e-7 42 | stepsize = np.linalg.norm(step) 43 | new_stepsize = energy_based_update( 44 | o_g, o_h, step, diff, stepsize, min_s=1, max_s=5 45 | ) 46 | assert new_stepsize == stepsize # condition 2 47 | 48 | # assert new_stepsize == 5 49 | 50 | def test_step_update_more(self): 51 | init = np.array((8, 6)) 52 | o_g = self.gradient(*init) 53 | o_h = self.hessian(*init) 54 | step = np.array((-2, -1)) 55 | diff = self.func(*(init + step)) - self.func(*init) 56 | assert diff == -1000 57 | assert np.allclose(o_g, np.array([420, 398])) 58 | assert np.allclose(o_h, np.array([[72, 38], [38, 72]])) 59 | stepsize = np.linalg.norm(step) 60 | new_stepsize = energy_based_update( 61 | o_g, o_h, step, diff, stepsize, min_s=2, max_s=5 62 | ) 63 | assert new_stepsize == 2 * stepsize 64 | 65 | step = np.array((-6, -6)) 66 | stepsize = np.linalg.norm(step) 67 | diff = self.func(*(init + step)) - self.func(*init) 68 | new_stepsize = energy_based_update( 69 | o_g, o_h, step, diff, stepsize, min_s=2, max_s=10 70 | ) 71 | assert new_stepsize == stepsize 72 | 73 | def test_gradient_update(self): 74 | init = np.array((8, 6)) 75 | o_g = self.gradient(*init) 76 | o_h = self.hessian(*init) 77 | step = np.array((-2, -1)) 78 | stepsize = np.linalg.norm(step) 79 | n_g = self.gradient(*(init + step)) 80 | diff = n_g - o_g 81 | assert np.allclose(o_g, np.array([420, 398])) 82 | assert np.allclose(n_g, np.array([258, 264])) 83 | assert np.allclose(o_h, np.array([[72, 38], [38, 72]])) 84 | assert np.allclose(diff, [-162, -134]) 85 | pre_g = o_g + np.dot(o_h, step) 86 | assert np.allclose(pre_g, [238, 250]) 87 | new_stepsize = gradient_based_update( 88 | o_g, o_h, n_g, step, df=3, step_size=stepsize, min_s=1, max_s=5 89 | ) 90 | assert new_stepsize == 2 * stepsize 91 | 92 | step = list(map(int, (-np.dot(np.linalg.pinv(o_h), o_g)))) 93 | stepsize = np.linalg.norm(step) 94 | assert np.allclose(step, [-4, -3]) 95 | assert stepsize == 5 # step == [-4, -3] 96 | n_g = self.gradient(*(init + step)) 97 | assert np.allclose(n_g, [114, 116]) 98 | diff = n_g - o_g 99 | assert np.allclose(diff, [-306, -282]) 100 | pre_g = o_g + np.dot(o_h, step) 101 | assert np.allclose(pre_g, [18, 30]) 102 | new_stepsize = gradient_based_update( 103 | o_g, o_h, n_g, step, df=3, step_size=stepsize, min_s=1, max_s=5 104 | ) 105 | assert new_stepsize == 5 106 | 107 | def _set_path_points(self): 108 | "create a class for testing points" 109 | 110 | class Other(PathPoint): 111 | def __init__(self): 112 | pass 113 | 114 | @property 115 | def df(self): 116 | return 1 117 | 118 | class Attr: 119 | pass 120 | 121 | self.p1 = Other() 122 | self.p2 = Other() 123 | start_point = np.array([2, 1]) 124 | self.p1._instance = Attr() 125 | self.p2._instance = Attr() 126 | 127 | self.p1._instance.energy = self.func(*start_point) 128 | self.p1._instance.b_matrix = np.eye(2) 129 | self.p1._instance.vspace = np.eye(2) 130 | self.p1._instance.v_gradient = self.gradient(*start_point) 131 | self.p1._instance.q_gradient = self.p1._instance.v_gradient 132 | self.p1._instance.x_gradient = self.p1._instance.v_gradient 133 | self.p1._instance._df = self.p1 134 | self.p1._mod_hessian = self.hessian(*start_point) 135 | self.p1._step = np.array([-1, 1]) 136 | 137 | new_point = np.array([1, 2]) 138 | self.p2._instance.energy = self.func(*new_point) 139 | self.p2._instance.b_matrix = np.eye(2) 140 | self.p2._instance.vspace = np.eye(2) 141 | self.p2._instance.v_gradient = self.gradient(*new_point) 142 | self.p2._instance.q_gradient = self.p2._instance.v_gradient 143 | self.p2._instance.x_gradient = self.p2._instance.v_gradient 144 | 145 | def test_update_object(self): 146 | self._set_path_points() 147 | with self.assertRaises(ValueError): 148 | energy_ob = Stepsize("gibberish") 149 | assert np.allclose(self.p1.v_gradient, [26, 32]) 150 | assert np.allclose(self.p2.v_gradient, [23, 38]) 151 | energy_ob = Stepsize("energy") 152 | new_step = energy_ob.update_step(old=self.p1, new=self.p2) 153 | assert np.allclose(new_step, self.p1.stepsize) 154 | gradient_ob = Stepsize("gradient") 155 | new_step = gradient_ob.update_step(old=self.p1, new=self.p2) 156 | assert np.allclose(new_step, energy_ob.min_s) 157 | -------------------------------------------------------------------------------- /saddle/optimizer/test/test_trust_radius.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from unittest import TestCase 4 | from saddle.optimizer.trust_radius import TrustRegion 5 | from saddle.optimizer.path_point import PathPoint 6 | 7 | trim = TrustRegion.trim 8 | 9 | 10 | # pylint: disable=E1101, E1133 11 | # Disable pylint on numpy.random functions 12 | class TestTrustRadius(TestCase): 13 | def setUp(self): 14 | np.random.seed(199) 15 | rng_mt = np.random.rand(5, 5) 16 | self.hessian = np.dot(rng_mt, rng_mt.T) 17 | self.gradient = np.random.rand(5) 18 | 19 | def test_trim(self): 20 | step = trim(self.hessian, self.gradient, 0.02) 21 | assert np.linalg.norm(step) - 0.02 < 1e-7 22 | step = trim(self.hessian, self.gradient, 0.1) 23 | assert np.linalg.norm(step) - 1 < 1e-7 24 | step = trim(self.hessian, self.gradient, 683) 25 | assert np.linalg.norm(step) - 682.95203408 < 1e-7 26 | 27 | def test_trim_one_neg(self): 28 | hessian = np.diag([-1, 3, 5, 7, 9]) 29 | gradient = np.arange(5) 30 | step = trim(hessian, gradient, 0.766881021) 31 | ref_step = np.array([-0.0, -0.3030303, -0.37735849, -0.4109589, -0.43010753]) 32 | assert np.allclose(step, ref_step) 33 | 34 | def test_trim_all_pos(self): 35 | hessian = np.diag([2, 4, 6, 7, 11]) 36 | gradient = np.arange(1, 6) 37 | step = trim(hessian, gradient, 0.8451898886) 38 | ref_step = np.array( 39 | [-0.27027027, -0.35087719, -0.38961039, -0.45977011, -0.39370079] 40 | ) 41 | assert np.allclose(step, ref_step) 42 | 43 | def test_two_neg(self): 44 | hessian = np.diag([-2, -4, 6, 7, 11]) 45 | gradient = np.arange(1, 6) 46 | step = trim(hessian, gradient, 0.8451898886) 47 | ref_step = np.array( 48 | [0.27027027, 0.35087719, -0.38961039, -0.45977011, -0.39370079] 49 | ) 50 | assert np.allclose(step, ref_step) 51 | 52 | def _quad_func_setup(self): 53 | # function f = x^2 + 2y^2 + 3xy + 2x + 4y + 1 54 | self.gradient = lambda x, y: np.array([2 * x + 3 * y + 2, 4 * y + 3 * x + 4]) 55 | self.hessian = lambda x, y: np.array([[2, 3], [3, 4]]) 56 | 57 | def _set_path_points(self): 58 | class Other(PathPoint): 59 | def __init__(self): 60 | pass 61 | 62 | class Attr: 63 | pass 64 | 65 | self.p1 = Other() 66 | start_point = np.array([2, 1]) 67 | self.p1._instance = Attr() 68 | self.p1._instance.v_gradient = self.gradient(*start_point) 69 | self.p1._mod_hessian = self.hessian(*start_point) 70 | self.p1._stepsize = 1 71 | 72 | def test_ob_trust_raiuds(self): 73 | self._quad_func_setup() 74 | self._set_path_points() 75 | trim_ob = TrustRegion("trim") 76 | assert np.allclose(self.p1.v_gradient, np.array([9, 14])) 77 | assert np.allclose(self.p1.v_hessian, np.array([[2, 3], [3, 4]])) 78 | self.p1.step_hessian = self.p1.v_hessian 79 | step = trim_ob.calculate_trust_step(self.p1) 80 | assert np.linalg.norm(step) - 1 < 1e-6 81 | self.p1._stepsize = 1.8353865 82 | step = trim_ob.calculate_trust_step(self.p1) 83 | assert np.allclose(step, np.array([-1.28760195, -1.30794685])) 84 | 85 | self.p1._instance.v_gradient = self.gradient(0, -1) 86 | self.p1._mod_hessian = self.hessian(0, -1) 87 | self.p1._stepsize = 6 88 | step = trim_ob.calculate_trust_step(self.p1) 89 | assert np.allclose(self.p1.v_gradient, np.array([-1, 0])) 90 | assert np.linalg.norm(step) - 5 < 1e-5 91 | self.p1._stepsize = 0.2643558 92 | step = trim_ob.calculate_trust_step(self.p1) 93 | assert np.allclose(step, np.array([-0.17079935, 0.20177115])) 94 | -------------------------------------------------------------------------------- /saddle/optimizer/trust_radius.py: -------------------------------------------------------------------------------- 1 | """Compute trust radius module.""" 2 | 3 | import numpy as np 4 | 5 | from saddle.math_lib import ridders_solver 6 | from saddle.optimizer.path_point import PathPoint 7 | 8 | 9 | class TrustRegion: 10 | """Constrain optimization step to preferred stepsize.""" 11 | 12 | def __init__(self, method_name): 13 | """Initialize trust radius instance. 14 | 15 | Parameters 16 | ---------- 17 | method_name : str 18 | the name of the trust radius method 19 | 20 | Raises 21 | ------ 22 | ValueError 23 | The method name is not allowed 24 | """ 25 | if method_name not in TrustRegion._trust_radius_methods: 26 | raise ValueError(f"{method_name} is not a valid name") 27 | self._name = method_name 28 | self._update_tr = TrustRegion._trust_radius_methods[method_name] 29 | 30 | @property 31 | def name(self): 32 | """str: the name of trust radius update method.""" 33 | return self._name 34 | 35 | def calculate_trust_step(self, point): 36 | """Compute the update step conform with trust radius stepsize range. 37 | 38 | Parameters 39 | ---------- 40 | point : PathPoint 41 | the optimization structure to be compute trust step 42 | 43 | Returns 44 | ------- 45 | np.ndarray 46 | 47 | 48 | Raises 49 | ------ 50 | TypeError 51 | If the input argument is not an PathPoint instance 52 | """ 53 | if not isinstance(point, PathPoint): 54 | raise TypeError(f"Improper input type for {point}") 55 | return self._update_tr(point.step_hessian, point.v_gradient, point.stepsize) 56 | 57 | @staticmethod 58 | def trust_region_image_potential(hessian, gradient, stepsize): 59 | """Conpute proper trsut radius tep with TRIP method. 60 | 61 | Parameters 62 | ---------- 63 | hessian : np.ndarray(N, N) 64 | Cartesian hessian matrix 65 | gradient : np.ndarray(N,) 66 | Cartesian gradient array 67 | stepsize : float 68 | desired stepsize of update step 69 | 70 | Returns 71 | ------- 72 | np.ndarray(N,) 73 | Proper update step conform with stepsize 74 | """ 75 | assert stepsize > 0 76 | val, vectors = np.linalg.eigh(hessian) 77 | negative = np.sum([val < 0]) 78 | 79 | def value_func(lamd): 80 | values = val.copy() 81 | values[:negative] -= lamd 82 | values[negative:] += lamd 83 | assert np.all(values != 0) 84 | n_v = 1.0 / values 85 | new_h = np.dot(vectors, np.dot(np.diag(n_v), vectors.T)) 86 | return -np.dot(new_h, gradient) 87 | 88 | def value_compare(lamd): 89 | step = value_func(lamd) 90 | return stepsize - np.linalg.norm(step) 91 | 92 | if value_compare(0) >= 0: # inital case 93 | return value_func(0) 94 | start_value = round(np.max(np.abs(val)), 7) # need to optimized in the future 95 | if value_compare(start_value) >= 0: # initial iteration case 96 | answer = ridders_solver(value_compare, 0, start_value) 97 | # print(answer) 98 | return value_func(answer) 99 | while value_compare(start_value) < 0: 100 | # print(start_value, value_compare(start_value)) 101 | start_value *= 2 102 | if value_compare(start_value) >= 0: 103 | answer = ridders_solver(value_compare, start_value / 2, start_value) 104 | # print(answer) 105 | return value_func(answer) 106 | 107 | trim = trust_region_image_potential 108 | 109 | @staticmethod 110 | def rational_functional_optimization(hessian, gradient, stepsize): 111 | """Not implemented trust radius method.""" 112 | raise NotImplementedError 113 | 114 | rfo = rational_functional_optimization 115 | 116 | _trust_radius_methods = { 117 | "trim": trim.__func__, 118 | "rfo": rfo.__func__, 119 | } 120 | -------------------------------------------------------------------------------- /saddle/path_ri.py: -------------------------------------------------------------------------------- 1 | """Reaction path internal coordinate class.""" 2 | from copy import deepcopy 3 | 4 | import numpy as np 5 | 6 | from saddle.internal import Internal 7 | from saddle.reduced_internal import ReducedInternal 8 | 9 | 10 | class PathRI(ReducedInternal): 11 | """Reaction path reduced internal coordinates.""" 12 | 13 | def __init__(self, coordinates, numbers, charge, multi, path_vector, title=""): 14 | """Initialize reaction path instance. 15 | 16 | Parameters 17 | ---------- 18 | coordinates : np.ndarray(N, 3) 19 | Cartesian Coordinates of molecules 20 | numbers : np.ndarray(N.) 21 | Atomic numbers of system 22 | charge : int 23 | Molecular charge 24 | multi : int 25 | Molecular multiplicity 26 | path_vector : np.ndarray(n,) 27 | Reaction path direction vector 28 | title : str, optional 29 | Molecule's title name 30 | 31 | Raises 32 | ------ 33 | ValueError 34 | Shape of path_vactor is not a np.ndarray 35 | """ 36 | super().__init__(coordinates, numbers, charge, multi, title, key_ic_number=0) 37 | if len(path_vector.shape) == 1: 38 | raise ValueError("Path vector is a 1d array") 39 | self._path_vector = path_vector 40 | self._k_ic_n = 0 41 | 42 | @property 43 | def path_vector(self): 44 | """np.ndarray: reaction path vector in redundant internal coordinates.""" 45 | return self._path_vector 46 | 47 | @property 48 | def real_unit_path_vector(self): 49 | """np.ndarray: realizable reaction path unit vector in inernal coordinates.""" 50 | tfm = np.dot(self.b_matrix, np.linalg.pinv(self.b_matrix)) 51 | real_path_v = np.dot(tfm, self.path_vector) 52 | return real_path_v / np.linalg.norm(real_path_v) 53 | 54 | def set_path_vector(self, vector): 55 | """Set a new reaction path vector.""" 56 | assert isinstance(vector, np.ndarray) 57 | self._reset_v_space() 58 | self._path_vector = vector 59 | 60 | def set_key_ic_number(self, number): 61 | """Not implemeted in reaction path.""" 62 | raise NotImplementedError 63 | 64 | def select_key_ic(self, *indices): 65 | """Not implemented in reaction path.""" 66 | raise NotImplementedError 67 | 68 | @classmethod 69 | def update_to_reduced_internal(cls, internal_ob, key_ic_number=0): 70 | """Not implemented in reaction path.""" 71 | raise NotImplementedError 72 | 73 | def _svd_of_b_matrix(self, threshold=1e-3) -> "np.ndarray": # tested 74 | # b_space is n * n 75 | b_space = np.dot(self.b_matrix, self.b_matrix.T) 76 | # b_matrix shape is n * 3N 77 | values, vectors = np.linalg.eigh(b_space) 78 | # select non singular basis set 79 | vectors = vectors[:, np.abs(values) > threshold] 80 | # project out unit path vector direction 81 | real_proj_mtr = np.outer(self.real_unit_path_vector, self.real_unit_path_vector) 82 | sub_vectors = vectors - real_proj_mtr @ vectors 83 | # select non singular basis set 84 | sub_val, sub_vec = np.linalg.eigh(sub_vectors @ sub_vectors.T) 85 | basis = sub_vec[:, np.abs(sub_val) > threshold] 86 | return basis 87 | 88 | # def _reduced_perturbation(self): 89 | # tsfm = np.dot(self.b_matrix, pse_inv(self.b_matrix)) 90 | # result = np.dot(tsfm, self._path_vector.T) 91 | # assert len(result.shape) == 1 92 | # return result[:, None] 93 | 94 | # @property 95 | # def vspace(self): 96 | # """Vspace transformation matrix from internal to reduced internal 97 | 98 | # Returns 99 | # ------- 100 | # vspace : np.ndarray(K, 3N - 6) 101 | # """ 102 | # if self._red_space is None or self._non_red_space is None: 103 | # self._generate_reduce_space() 104 | # self._generate_nonreduce_space() 105 | # self._vspace = self._non_red_space.copy() 106 | # return self._vspace 107 | 108 | # TO BE determined 109 | @classmethod 110 | def update_to_path_ri(cls, internal_ob, path_vector): 111 | """Update a reduced internal coordinates to Path Reaction Internal.""" 112 | assert isinstance(internal_ob, Internal) 113 | new_ob = deepcopy(internal_ob) 114 | new_ob.__class__ = cls 115 | new_ob._path_vector = None 116 | new_ob._k_ic_n = 0 117 | new_ob.set_path_vector(path_vector) 118 | return new_ob 119 | 120 | def set_vspace(self, new_vspace: "np.ndarray") -> None: 121 | """Set vspace of system with given values. 122 | 123 | Arguments 124 | --------- 125 | new_vspace : np.ndarray(K, 3N - 6) 126 | The new value of vspace 127 | """ 128 | overlap = np.dot(new_vspace.T, self.path_vector) 129 | if np.max(np.abs(overlap)) > 1e-6: 130 | # project out the vector space 131 | no_vect_path_vspace = new_vspace - np.dot( 132 | np.outer(self.real_unit_path_vector, self.real_unit_path_vector), 133 | new_vspace, 134 | ) 135 | vals, vecs = np.linalg.eigh( 136 | np.dot(no_vect_path_vspace, no_vect_path_vspace.T) 137 | ) 138 | new_vspace = vecs[:, np.abs(vals) > 1e-5] 139 | self._vspace = new_vspace 140 | self._red_space = new_vspace[:, : self.key_ic_number] 141 | self._non_red_space = new_vspace[:, self.key_ic_number :] 142 | 143 | # @property 144 | # def vspace(self): 145 | # """Vspace transformation matrix from internal to reduced internal 146 | # 147 | # Returns 148 | # ------- 149 | # vspace : np.ndarray(K, 3N - 6) 150 | # """ 151 | # if self._vspace is None: 152 | # real_vector = self._realizable_change_in_vspace( 153 | # self._path_vector) 154 | # real_uni_vector = real_vector / np.linalg.norm(real_vector) 155 | # sub_vspace = self.pre_vspace - np.dot( 156 | # np.dot(self.pre_vspace, real_uni_vector), real_uni_vector.T) 157 | # threshold = 1e-6 # nonzero eigenvalues threshold 158 | # w, v = diagonalize(sub_vspace) 159 | # self.vspace = v[:, abs(w) > threshold] 160 | # return self._vspace 161 | # else: 162 | # return self._vspace 163 | 164 | # def pre_vspace(self): 165 | # """Vspace transformation matrix from internal to reduced internal 166 | # 167 | # Returns 168 | # ------- 169 | # vspace : np.ndarray(K, 3N - 6) 170 | # """ 171 | # if self._red_space is None or self._non_red_space is None: 172 | # self._generate_reduce_space() 173 | # self._generate_nonreduce_space() 174 | # self._pre_vspace = np.hstack((self._red_space, 175 | # self._non_red_space)) 176 | # return self._pre_vspace 177 | 178 | # def _realizable_change_in_vspace(self, change_vector): 179 | # v = self.vspace 180 | # b = self.b_matrix 181 | # b_inv = pse_inv(b) 182 | # return np.dot(v.T, np.dot(b, np.dot(b_inv, change_vector))) 183 | -------------------------------------------------------------------------------- /saddle/periodic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Grape: Because I like it. 3 | # Copyright (C) 2015-2017 The Grape Development Team 4 | # 5 | # This file is part of Grape. 6 | # 7 | # Grape is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # Grape is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Periodic module for get basic atomic property.""" 22 | # from .periodic import * 23 | # from .units import * 24 | -------------------------------------------------------------------------------- /saddle/periodic/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # HORTON: Helpful Open-source Research TOol for N-fermion systems. 3 | # Copyright (C) 2011-2016 The HORTON Development Team 4 | # 5 | # This file is part of HORTON. 6 | # 7 | # HORTON is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # HORTON is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Physicochemical constants in atomic units. 22 | 23 | These are the physical constants defined in this module (in atomic units): 24 | """ 25 | 26 | boltzmann = 3.1668154051341965e-06 27 | avogadro = 6.0221415e23 28 | lightspeed = 137.03599975303575 29 | planck = 6.2831853071795864769 30 | 31 | 32 | # automatically spice up the docstrings 33 | 34 | lines = [ 35 | " ================ ==================", 36 | " Name Value ", 37 | " ================ ==================", 38 | ] 39 | 40 | for key, value in sorted(globals().items()): 41 | if not isinstance(value, float): 42 | continue 43 | lines.append(" %16s %.10e" % (key, value)) 44 | lines.append(" ================ ==================") 45 | 46 | __doc__ += "\n".join(lines) 47 | -------------------------------------------------------------------------------- /saddle/periodic/units.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # HORTON: Helpful Open-source Research TOol for N-fermion systems. 3 | # Copyright (C) 2011-2016 The HORTON Development Team 4 | # 5 | # This file is part of HORTON. 6 | # 7 | # HORTON is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # HORTON is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | """Conversion from and to atomic units. 22 | 23 | Internally HORTON always uses atomic units. Atomic units are consistent, 24 | similar to the SI unit system: one does not need conversion factors in the 25 | middle of a computation. This choice facilitates the programming and reduces 26 | accidental bugs. 27 | 28 | References for the conversion values: 29 | 30 | * B. J. Mohr and B. N. Taylor, 31 | CODATA recommended values of the fundamental physical 32 | constants: 1998, Rev. Mod. Phys. 72(2), 351 (2000) 33 | * The NIST Reference on Constants, Units, and Uncertainty 34 | (http://physics.nist.gov/cuu/Constants/index.html) 35 | * 1 calorie = 4.184 Joules 36 | 37 | **Conventions followed by this module:** 38 | 39 | Let foo be is the value of an external unit in internal (atomic) units. The 40 | way to use this unit is as follows: ``5*foo`` litterally means `five times 41 | foo`. The result of this operation is a floating point number for this value 42 | in atomic units. 43 | 44 | **Examples:** 45 | 46 | If you want to have a distance of five angstrom in internal units: 47 | ``5*angstrom``. 48 | 49 | If you want to convert a length of 5 internal units to angstrom: 50 | ``5/angstrom``. 51 | 52 | **Remarks:** 53 | 54 | It is highly recommended to perform unit conversions only when data is read 55 | from the input or data is written to the output. It may also be useful in 56 | `input scripts` that use HORTON. Do not perform any unit conversion in other 57 | parts of the program. 58 | 59 | An often recurring question is how to convert a frequency in internal units 60 | to a spectroscopic wavenumber in inverse centimeters. This is how it can be 61 | done:: 62 | 63 | >>> from horton import centimeter, lightspeed 64 | >>> invcm = lightspeed/centimeter 65 | >>> freq = 0.00320232 66 | >>> print freq/invcm 67 | 68 | These are the conversion constants defined in this module: 69 | """ 70 | 71 | 72 | from saddle.periodic.constants import avogadro 73 | 74 | # *** Generic *** 75 | au = 1.0 76 | 77 | 78 | # *** Charge *** 79 | 80 | coulomb = 1.0 / 1.602176462e-19 81 | 82 | # *** Mass *** 83 | 84 | kilogram = 1.0 / 9.10938188e-31 85 | 86 | gram = 1.0e-3 * kilogram 87 | miligram = 1.0e-6 * kilogram 88 | unified = 1.0e-3 * kilogram / avogadro 89 | amu = unified 90 | 91 | # *** Length *** 92 | 93 | meter = 1.0 / 0.5291772083e-10 94 | 95 | decimeter = 1.0e-1 * meter 96 | centimeter = 1.0e-2 * meter 97 | milimeter = 1.0e-3 * meter 98 | micrometer = 1.0e-6 * meter 99 | nanometer = 1.0e-9 * meter 100 | angstrom = 1.0e-10 * meter 101 | picometer = 1.0e-12 * meter 102 | 103 | # *** Volume *** 104 | 105 | liter = decimeter ** 3 106 | 107 | # *** Energy *** 108 | 109 | joule = 1 / 4.35974381e-18 110 | 111 | calorie = 4.184 * joule 112 | kjmol = 1.0e3 * joule / avogadro 113 | kcalmol = 1.0e3 * calorie / avogadro 114 | electronvolt = (1.0 / coulomb) * joule 115 | rydberg = 0.5 116 | 117 | # *** Force *** 118 | 119 | newton = joule / meter 120 | 121 | # *** Angles *** 122 | 123 | deg = 0.017453292519943295 124 | rad = 1.0 125 | 126 | # *** Time *** 127 | 128 | second = 1 / 2.418884326500e-17 129 | 130 | nanosecond = 1e-9 * second 131 | femtosecond = 1e-15 * second 132 | picosecond = 1e-12 * second 133 | 134 | # *** Frequency *** 135 | 136 | hertz = 1 / second 137 | 138 | # *** Pressure *** 139 | 140 | pascal = newton / meter ** 2 141 | bar = 100000 * pascal 142 | atm = 1.01325 * bar 143 | 144 | # *** Temperature *** 145 | 146 | kelvin = 1.0 147 | 148 | # *** Dipole *** 149 | 150 | debye = 0.39343031369146675 # = 1e-21*coulomb*meter**2/second/lightspeed 151 | 152 | # *** Current *** 153 | 154 | ampere = coulomb / second 155 | 156 | 157 | # automatically spice up the docstrings 158 | 159 | lines = [ 160 | " ================ ==================", 161 | " Name Value ", 162 | " ================ ==================", 163 | ] 164 | 165 | for key, value in sorted(globals().items()): 166 | if not isinstance(value, float): 167 | continue 168 | lines.append(" %16s %.10e" % (key, value)) 169 | lines.append(" ================ ==================") 170 | 171 | __doc__ += "\n".join(lines) 172 | 173 | 174 | del lines 175 | -------------------------------------------------------------------------------- /saddle/procrustes/__init__.py: -------------------------------------------------------------------------------- 1 | """Procrusts init file.""" 2 | -------------------------------------------------------------------------------- /saddle/procrustes/procrustes.py: -------------------------------------------------------------------------------- 1 | """Procrustes module.""" 2 | import numpy as np 3 | 4 | from saddle.periodic.periodic import periodic 5 | from saddle.periodic.units import amu 6 | 7 | 8 | class Procrustes(object): 9 | """Procrustes module for aligning or rotating molecules conformations.""" 10 | 11 | def __init__(self, target, *candidates): 12 | """Initialize Procrustes instance. 13 | 14 | This class is used to align candidate(s) molecules to align with target 15 | molecule with maximum overlap. 16 | 17 | Parameters 18 | ---------- 19 | target : np.ndarray(N, 3) 20 | Coordinates of target molecule 21 | *candidates : np.ndarray(N, 3) 22 | One or more candidate molecule(s) to be rotated to be aligned with 23 | target molecule 24 | """ 25 | assert len(candidates) > 0 26 | assert all(np.array_equal(target.numbers, i.numbers) for i in candidates) 27 | self._target = target 28 | self._candidates = candidates 29 | self._target_center = Procrustes._barycenter(target.coordinates, target.numbers) 30 | 31 | @staticmethod 32 | def _barycenter(coordinates, numbers): 33 | r"""Compute bary center of given molecule. 34 | 35 | .. math:: 36 | 37 | r_{bary} = \frac{\sum_i^n m_i r_i} {\sum_i^n m_i} 38 | 39 | Parameters 40 | ---------- 41 | coordinates : np.ndarray(N, 3) 42 | Coordinates of each atomic coordinates 43 | numbers : np.ndarray(N, ) 44 | Atomic number of each atoms 45 | 46 | Returns 47 | ------- 48 | np.ndarray(3, ) 49 | Coordinates of barycenter of given molecule 50 | """ 51 | assert isinstance(coordinates, np.ndarray) 52 | assert isinstance(numbers, np.ndarray) 53 | assert numbers.ndim == 1 54 | assert coordinates.ndim == 2 55 | fac_ato_ma = np.vectorize(Procrustes._fetch_atomic_mass) 56 | atom_mass = fac_ato_ma(numbers) 57 | return np.einsum("i,ij->j", atom_mass, coordinates) / np.sum(atom_mass) 58 | 59 | @staticmethod 60 | def _fetch_atomic_mass(atomic_num): 61 | """Compute atomic mass in atomic unit. 62 | 63 | Parameters 64 | ---------- 65 | atomic_num : int 66 | Atomic number of certain atom 67 | 68 | Returns 69 | ------- 70 | float 71 | Atmoic weight in atomic unit 72 | """ 73 | return periodic[atomic_num].mass / amu 74 | 75 | @staticmethod 76 | def _move_to_center(coordinates, numbers): 77 | """Move molecules bary center to origin. 78 | 79 | Parameters 80 | ---------- 81 | coordinates : np.ndarray(N, 3) 82 | Coordinates of given molecule 83 | numbers : np.ndarray(N, ) 84 | Atomic numbers of given molecule 85 | 86 | Returns 87 | ------- 88 | np.ndarray(N, 3) 89 | New translated coordinates with bary center at the origin 90 | """ 91 | center = Procrustes._barycenter(coordinates, numbers) 92 | return coordinates - center 93 | 94 | @staticmethod 95 | def _rotate_coordinates(co1, co2): 96 | """Rotate co2 coordinates to align with co1 coordinates. 97 | 98 | Parameters 99 | ---------- 100 | co1 : np.ndarray(N, 3) 101 | Target coordinates to be aligned with 102 | co2 : np.ndarray(N, 3) 103 | Object coordinates to be rotated to align with target coords 104 | 105 | Returns 106 | ------- 107 | np.ndarray(N, 3) 108 | Rotated object coordinates with maximum overlap with target coords 109 | """ 110 | assert isinstance(co1, np.ndarray) 111 | assert isinstance(co2, np.ndarray) 112 | assert co1.shape == co2.shape 113 | if co1.ndim == 1: 114 | op = np.outer(co1, co2) 115 | else: 116 | op = np.dot(co1.T, co2) 117 | u, _, v = np.linalg.svd(op) 118 | # return np.dot(np.dot(u, v), co2.T).T 119 | return np.dot(co2, np.dot(u, v).T) 120 | 121 | def rotate_mols(self): 122 | """Rotate each candidates molecules to align with target molecule. 123 | 124 | Yields 125 | ------ 126 | np.ndarray(N, 3) 127 | Rotated molecule coordinates aligned with target molecule. 128 | """ 129 | # move target molecule center to origin 130 | adj_tar_coor = Procrustes._move_to_center( 131 | self._target.coordinates, self._target.numbers 132 | ) 133 | for i in self._candidates: 134 | # move each object molecule center to origin 135 | adj_center = Procrustes._move_to_center(i.coordinates, i.numbers) 136 | # align each object molecule with target molecule 137 | adj_can_coor = Procrustes._rotate_coordinates(adj_tar_coor, adj_center) 138 | yield adj_can_coor + self._target_center 139 | -------------------------------------------------------------------------------- /saddle/procrustes/test/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theochem/gopt/7d39db2195cfad893e201e655401885e3b151c43/saddle/procrustes/test/data/__init__.py -------------------------------------------------------------------------------- /saddle/procrustes/test/data/water.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | water 3 | H 0.783837 -0.492236 -0.000000 4 | O -0.000000 0.062020 -0.000000 5 | H -0.783837 -0.492236 -0.000000 6 | -------------------------------------------------------------------------------- /saddle/procrustes/test/test_procrustes.py: -------------------------------------------------------------------------------- 1 | """Procrustes test files.""" 2 | import unittest 3 | from collections import Iterable 4 | from copy import deepcopy 5 | 6 | from importlib_resources import path 7 | 8 | import numpy as np 9 | 10 | from saddle.periodic.periodic import periodic 11 | from saddle.periodic.units import amu 12 | from saddle.procrustes.procrustes import Procrustes 13 | from saddle.utils import Utils 14 | 15 | 16 | class test_procrustes(unittest.TestCase): 17 | """Procrustes test class.""" 18 | 19 | def test_barycenter(self): 20 | """Test barycenter of molecule.""" 21 | ori_numbers = np.array([1, 2]) 22 | m1 = periodic[1].mass / amu 23 | m2 = periodic[2].mass / amu 24 | mass_numbers = np.array([m1, m2]) 25 | assert np.allclose(mass_numbers, np.array([1, 4]), atol=1.0e-2) 26 | coordinates = np.array([[0, 1, 0], [1, 0, 0]]) 27 | result = np.einsum("i, ij -> j", mass_numbers, coordinates) 28 | assert np.allclose( 29 | Procrustes._barycenter(coordinates, ori_numbers), 30 | result / np.sum(mass_numbers), 31 | ) 32 | 33 | def test_fetch_atomic_amass(self): 34 | """Test get atomic mass.""" 35 | assert Procrustes._fetch_atomic_mass(1) - 1.007975 < 1e-5 36 | assert Procrustes._fetch_atomic_mass(6) - 12.0106 < 1e-5 37 | 38 | def test_move_center(self): 39 | """Test move the center of molecule to another place.""" 40 | numbers = np.array([1, 2]) 41 | coordinates = np.array([[0, 1, 0], [1, 0, 0]]) 42 | coor_1st = Procrustes._move_to_center(coordinates, numbers) 43 | coordinates_2 = np.array([[3, 1, 0], [4, 0, 0]]) 44 | coor_2nd = Procrustes._move_to_center(coordinates_2, numbers) 45 | assert np.allclose(coor_1st, coor_2nd) 46 | 47 | np.random.seed(10) # set random number seed 48 | extra = np.random.rand(3) 49 | coordinates_3 = coordinates + extra 50 | coor_3rd = Procrustes._move_to_center(coordinates_3, numbers) 51 | assert np.allclose(coor_1st, coor_3rd) 52 | 53 | extra_2 = np.random.rand(3) 54 | coordinates_4 = coordinates_3 + extra_2 55 | coor_4th = Procrustes._move_to_center(coordinates_4, numbers) 56 | assert np.allclose(coor_1st, coor_4th) 57 | 58 | def test_rotate_coordiantes(self): 59 | """Test rotate two coordinates to align.""" 60 | coordinates = np.array([0, 1, 0]) 61 | coordinates_2 = np.array([-1, 0, 0]) 62 | coor_2 = Procrustes._rotate_coordinates(coordinates, coordinates_2) 63 | assert np.allclose(coordinates, coor_2) 64 | 65 | coordinates_2d = np.array([[1, 1, 0], [-1, 1, 0]]) 66 | coordinates_2d_2 = np.array([[-1, 1, 0], [-1, -1, 0]]) 67 | coor_2d_2 = Procrustes._rotate_coordinates(coordinates_2d, coordinates_2d_2) 68 | assert np.allclose(coordinates_2d, coor_2d_2) 69 | 70 | coordinates_2d_3 = np.array([[-1, 0, 1], [-1, 0, -1]]) 71 | coor_2d_3 = Procrustes._rotate_coordinates(coordinates_2d, coordinates_2d_3) 72 | assert np.allclose(coordinates_2d, coor_2d_3) 73 | 74 | def test_move_and_rotate(self): 75 | """Test first move the molecule and then rotate to align.""" 76 | numbers = np.array([1, 2]) 77 | coordinates = np.array([[0, 1, 0], [1, 0, 0]]) 78 | coordinates_2 = np.array([[0, 1, 1], [1, 2, 1]]) 79 | center_1 = Procrustes._move_to_center(coordinates, numbers) 80 | center_2_tmp = Procrustes._move_to_center(coordinates_2, numbers) 81 | center_2 = Procrustes._rotate_coordinates(center_1, center_2_tmp) 82 | assert np.allclose(center_1, center_2) 83 | 84 | def test_main_function(self): 85 | """Test the main function and use case for procrustes.""" 86 | # file_path = resource_filename( 87 | # Requirement.parse('saddle'), 'data/water.xyz') 88 | with path("saddle.procrustes.test.data", "water.xyz") as file_path: 89 | water = Utils.load_file(file_path) 90 | water.coordinates = np.array([[0, 1, 0], [1, 0, 0], [-1, -1, 1]]) 91 | water_2 = deepcopy(water) 92 | water_2.coordinates = np.array([[-1, 0, 0], [0, 1, 0], [1, -1, 0]]) 93 | water_2.coordinates += 1 94 | water_3 = deepcopy(water) 95 | water_3.coordinates = np.array([[1, 0, 0], [0, -1, 0], [-1, 1, 0]]) 96 | water_3.coordinates -= 1 97 | 98 | pcs = Procrustes(water, water_2, water_3) 99 | final_xyz = pcs.rotate_mols() 100 | assert isinstance(final_xyz, Iterable) 101 | assert len(list(final_xyz)) == 2 102 | for i in final_xyz: 103 | assert np.allclose(i, water.coordinates) 104 | -------------------------------------------------------------------------------- /saddle/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # HORTON: Helpful Open-source Research TOol for N-fermion systems. 3 | # Copyright (C) 2011-2015 The HORTON Development Team 4 | # 5 | # This file is part of HORTON. 6 | # 7 | # HORTON is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU General Public License 9 | # as published by the Free Software Foundation; either version 3 10 | # of the License, or (at your option) any later version. 11 | # 12 | # HORTON is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, see 19 | # 20 | # -- 21 | -------------------------------------------------------------------------------- /saddle/test/data/2h-azirine.xyz: -------------------------------------------------------------------------------- 1 | 6 2 | 135972 3 | N -0.60290 0.52460 0.00000 4 | C 0.87460 0.15160 0.00000 5 | C -0.27170 -0.67620 0.00000 6 | H 1.38360 0.35500 0.92840 7 | H 1.38350 0.35510 -0.92850 8 | H -0.48830 -1.74360 0.00000 9 | -------------------------------------------------------------------------------- /saddle/test/data/__init__.py: -------------------------------------------------------------------------------- 1 | """Test data init file.""" 2 | -------------------------------------------------------------------------------- /saddle/test/data/ch3_hf.xyz: -------------------------------------------------------------------------------- 1 | 6 2 | Energy: -87102.9775554 3 | C -1.62284 -0.00003 0.00003 4 | H -1.70771 0.86727 0.62602 5 | H -1.70759 0.10849 -1.06411 6 | H -1.70643 -0.97596 0.43802 7 | F 1.57981 0.00004 -0.00002 8 | H 0.64049 0.00005 -0.00000 9 | -------------------------------------------------------------------------------- /saddle/test/data/ch3f_h.xyz: -------------------------------------------------------------------------------- 1 | 6 2 | Energy: -87084.6301041 3 | C 0.83618 0.00831 -0.00012 4 | H 1.20821 -0.83527 -0.56159 5 | H 1.17927 0.92522 -0.45491 6 | H 1.19564 -0.04738 1.01614 7 | F -0.56773 -0.01481 0.00017 8 | H -3.49061 0.04085 -0.00043 9 | -------------------------------------------------------------------------------- /saddle/test/data/di_water.xyz: -------------------------------------------------------------------------------- 1 | 6 2 | Energy: -4.5621945 3 | O -3.77604 -0.24186 0.07107 4 | H -2.87597 0.05663 0.27472 5 | H -4.31769 0.36554 0.60696 6 | O -4.38133 2.48204 1.67871 7 | H -3.87001 3.20521 1.27287 8 | H -4.98835 2.95652 2.26748 9 | -------------------------------------------------------------------------------- /saddle/test/data/ethane.xyz: -------------------------------------------------------------------------------- 1 | 8 2 | 3 | C -8.28239 2.29085 0.00000 4 | C -7.21239 2.29085 0.00000 5 | H -8.63905 2.93791 -0.77395 6 | H -8.63906 2.63758 0.94735 7 | H -8.63906 1.29706 -0.17339 8 | H -6.85573 3.28464 0.17339 9 | H -6.85572 1.94412 -0.94735 10 | H -6.85572 1.64379 0.77395 11 | -------------------------------------------------------------------------------- /saddle/test/data/h2o2.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | hydrogen peroxide 3 | O 0.6502 -0.3493 0.1087 4 | O -0.6502 -0.2688 0.2482 5 | H 0.9662 0.5645 0.2640 6 | H -0.9662 0.0536 -0.6209 7 | -------------------------------------------------------------------------------- /saddle/test/data/methanol.xyz: -------------------------------------------------------------------------------- 1 | 6 2 | 3 | C -4.16299 2.46622 0.00000 4 | O -3.09299 2.46622 0.00000 5 | H -4.51965 3.28595 -0.58800 6 | H -4.51966 2.56559 1.00390 7 | H -4.51966 1.54714 -0.41590 8 | H -2.76966 3.29942 0.37703 9 | -------------------------------------------------------------------------------- /saddle/test/data/prd.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | Created with Saddle 3 | O -3.6119299974 0.9694699973 -0.0802499999 4 | N -2.4607999998 0.7418799978 -0.0856600002 5 | H -0.3659099996 0.7980099989 0.9881099999 6 | S -1.5455100015 1.2415799982 1.4783299971 7 | -------------------------------------------------------------------------------- /saddle/test/data/rct.xyz: -------------------------------------------------------------------------------- 1 | 4 2 | Created with Saddle 3 | O -3.3321099957 0.8385900007 -0.3372399998 4 | N -2.0385600013 0.8989900015 0.0422300000 5 | H -3.3672300000 0.6771899997 -1.2779300009 6 | S -1.7749099988 1.1688499991 1.6193700003 7 | -------------------------------------------------------------------------------- /saddle/test/data/water.xyz: -------------------------------------------------------------------------------- 1 | 3 2 | water 3 | H 0.783837 -0.492236 -0.000000 4 | O -0.000000 0.062020 -0.000000 5 | H -0.783837 -0.492236 -0.000000 6 | -------------------------------------------------------------------------------- /saddle/test/test_cartesian.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from copy import deepcopy 3 | 4 | import numpy as np 5 | from importlib_resources import path 6 | from saddle.cartesian import Cartesian 7 | from saddle.periodic.periodic import angstrom 8 | from saddle.utils import Utils 9 | 10 | 11 | class TestCartesian(unittest.TestCase): 12 | def setUp(self): 13 | with path("saddle.test.data", "water.xyz") as mol_path: 14 | mol = Utils.load_file(mol_path) 15 | self.cartesian = Cartesian(mol.coordinates, mol.numbers, 0, 1) 16 | 17 | def test_from_file(self): 18 | with path("saddle.test.data", "water.xyz") as mol_path: 19 | mol = Cartesian.from_file(mol_path) 20 | ref_coordinates = np.array( 21 | [ 22 | [0.783837, -0.492236, -0.000000], 23 | [-0.000000, 0.062020, -0.000000], 24 | [-0.783837, -0.492236, -0.000000], 25 | ] 26 | ) 27 | assert np.allclose(mol.coordinates / angstrom, ref_coordinates) 28 | assert mol.natom == 3 29 | assert isinstance(mol, Cartesian) 30 | 31 | def test_coordinates(self): 32 | ref_coordinates = np.array( 33 | [ 34 | [0.783837, -0.492236, -0.000000], 35 | [-0.000000, 0.062020, -0.000000], 36 | [-0.783837, -0.492236, -0.000000], 37 | ] 38 | ) 39 | assert np.allclose(self.cartesian.coordinates / angstrom, ref_coordinates) 40 | 41 | def test_numbers(self): 42 | ref_numbers = np.array([1, 8, 1]) 43 | assert np.allclose(self.cartesian.numbers, ref_numbers) 44 | 45 | def test_charge_and_multi(self): 46 | ref_multi = 1 47 | ref_charge = 0 48 | assert self.cartesian.multi == ref_multi 49 | assert self.cartesian.charge == ref_charge 50 | 51 | def test_distance(self): 52 | ref_distance = np.linalg.norm( 53 | np.array([0.783837, -0.492236, -0.000000]) 54 | - np.array([-0.000000, 0.062020, -0.000000]) 55 | ) 56 | assert self.cartesian.distance(0, 1) / angstrom == ref_distance 57 | 58 | def test_angle(self): 59 | vector1 = np.array([-0.000000, 0.062020, -0.000000]) - np.array( 60 | [0.783837, -0.492236, -0.000000] 61 | ) 62 | vector2 = np.array([-0.000000, 0.062020, -0.000000]) - np.array( 63 | [-0.783837, -0.492236, -0.000000] 64 | ) 65 | ref_angle_cos = ( 66 | np.dot(vector1, vector2) / np.linalg.norm(vector1) / np.linalg.norm(vector2) 67 | ) 68 | assert np.allclose(self.cartesian.angle_cos(0, 1, 2), ref_angle_cos) 69 | assert np.allclose(self.cartesian.angle(0, 1, 2), np.arccos(ref_angle_cos)) 70 | 71 | def test_get_energy_from_fchk(self): 72 | with path("saddle.test.data", "water_1.fchk") as fchk_path: 73 | mole = deepcopy(self.cartesian) 74 | mole.energy_from_fchk(fchk_path) 75 | assert np.allclose(mole.energy, -7.599264122862e1) 76 | ref_gradient = [ 77 | 2.44329621e-17, 78 | 4.95449892e-03, 79 | -9.09914286e-03, 80 | 7.79137241e-16, 81 | -3.60443012e-16, 82 | 1.81982857e-02, 83 | -8.03570203e-16, 84 | -4.95449892e-03, 85 | -9.09914286e-03, 86 | ] 87 | assert np.allclose(mole.energy_gradient, ref_gradient) 88 | ref_coor = np.array( 89 | [ 90 | 0.00000000e00, 91 | 1.48124293e00, 92 | -8.37919685e-01, 93 | 0.00000000e00, 94 | 3.42113883e-49, 95 | 2.09479921e-01, 96 | -1.81399942e-16, 97 | -1.48124293e00, 98 | -8.37919685e-01, 99 | ] 100 | ).reshape(-1, 3) 101 | assert np.allclose(mole.coordinates, ref_coor) 102 | -------------------------------------------------------------------------------- /saddle/test/test_config.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from importlib_resources import path 4 | from pathlib import PosixPath 5 | from saddle.conf import Config 6 | 7 | 8 | class TestUtils(TestCase): 9 | def setUp(self): 10 | Config.reset_path() 11 | 12 | def test_load_const(self): 13 | work_dir = Config.get_path(key="work_dir") 14 | with path("saddle", "") as ref_path: 15 | assert work_dir == (ref_path / "work") 16 | 17 | def test_set_work(self): 18 | Config.set_path("work_dir", "/usr/local") 19 | work_dir = Config.get_path("work_dir") 20 | assert work_dir == PosixPath("/usr/local") 21 | 22 | Config.reset_path() 23 | new_work_dir = Config.get_path("work_dir") 24 | with path("saddle", "") as ref_path: 25 | assert new_work_dir == (ref_path / "work") 26 | 27 | @classmethod 28 | def tearDownClass(cls): 29 | Config.reset_path() 30 | -------------------------------------------------------------------------------- /saddle/test/test_coordinates_type.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.coordinate_types import ( 6 | BendAngle, 7 | BendCos, 8 | BondLength, 9 | ConventionDihedral, 10 | DihedralAngle, 11 | NewDihedralCross, 12 | NewDihedralDot, 13 | ) 14 | from saddle.errors import NotSetError 15 | from saddle.molmod import bend_angle 16 | from saddle.utils import Utils 17 | 18 | 19 | class Test_Coordinates_Types(TestCase): 20 | @classmethod 21 | def setup_class(self): 22 | with path("saddle.test.data", "methanol.xyz") as file_path: 23 | mol = Utils.load_file(file_path) 24 | self.molecule = mol 25 | 26 | with path("saddle.test.data", "h2o2.xyz") as file_path2: 27 | mol2 = Utils.load_file(file_path2) 28 | self.h2o2 = mol2 29 | 30 | def test_bond_length(self): 31 | bond = BondLength((0, 1), self.molecule.coordinates[[0, 1]]) 32 | assert bond.value - 2.0220069632957394 < 1e-8 33 | bond.set_new_coordinates(self.molecule.coordinates[[1, 2]]) 34 | assert bond.value - 3.3019199546607476 < 1e-8 35 | # set target 36 | bond.target = 3.0 37 | # calculate ref 38 | ref_v = (bond.value - 3.0) ** 2 39 | real_v = bond.cost_v 40 | assert np.allclose(ref_v, bond.cost_v) 41 | ref_d = (bond.cost_v - (bond.value - 1e-6 - 3) ** 2) / 1e-6 42 | real_d = bond.cost_d 43 | assert np.allclose(ref_d, bond.cost_d, atol=1e-6) 44 | ref_dd = (bond.cost_d - 2 * (bond.value - 1e-6 - 3)) / 1e-6 45 | real_dd = bond.cost_dd 46 | assert np.allclose(ref_dd, bond.cost_dd, atol=1e-6) 47 | bond.weight = 1.2 48 | assert np.allclose(real_v * 1.2, bond.cost_v) 49 | assert np.allclose(real_d * 1.2, bond.cost_d) 50 | assert np.allclose(real_dd * 1.2, bond.cost_dd) 51 | 52 | def test_bend_angle(self): 53 | angle = BendAngle((1, 0, 2), self.molecule.coordinates[[1, 0, 2]]) 54 | assert angle.value - (1.9106254499450943) < 1e-8 55 | angle.set_new_coordinates(self.molecule.coordinates[[1, 0, 3]]) 56 | assert angle.value - (1.910636062481526) < 1e-8 57 | # calculate ref value 58 | angle.target = 1.8 59 | # calculate ref v 60 | ref_v = (np.cos(angle.value) - np.cos(1.8)) ** 2 61 | real_v = angle.cost_v 62 | assert np.allclose(ref_v, angle.cost_v) 63 | # calculate ref d 64 | ref_d = (angle.cost_v - (np.cos(angle.value - 1e-6) - np.cos(1.8)) ** 2) / 1e-6 65 | real_d = angle.cost_d 66 | # print(ref_d, angle.cost_d) 67 | assert np.allclose(ref_d, angle.cost_d, atol=1e-6) 68 | # calculate ref dd 69 | ref_d2 = ( 70 | -2 * (np.cos(angle.value - 1e-6) - np.cos(1.8)) * np.sin(angle.value - 1e-6) 71 | ) 72 | ref_dd = (angle.cost_d - ref_d2) / 1e-6 73 | real_dd = angle.cost_dd 74 | assert np.allclose(ref_dd, angle.cost_dd, atol=1e-6) 75 | # test weight 76 | angle.weight = 1.2 77 | assert np.allclose(angle.cost_v, real_v * 1.2) 78 | assert np.allclose(angle.cost_d, real_d * 1.2) 79 | assert np.allclose(angle.cost_dd, real_dd * 1.2) 80 | 81 | def test_bend_cos(self): 82 | angle_cos = BendCos((1, 0, 2), self.molecule.coordinates[[1, 0, 2]]) 83 | assert angle_cos.value - (-0.3333259923254888) < 1e-8 84 | angle_cos.set_new_coordinates(self.molecule.coordinates[[1, 0, 3]]) 85 | assert angle_cos.value - (-0.3333359979295637) < 1e-8 86 | 87 | def test_dihed_angle(self): 88 | dihed_angle = DihedralAngle((2, 0, 1, 3), self.h2o2.coordinates[[2, 0, 1, 3]]) 89 | assert dihed_angle.value - 1.43966112870 < 1e-8 90 | with self.assertRaises(NotSetError): 91 | dihed_angle.target 92 | dihed_angle.target = 1.57 93 | v = dihed_angle.cost_v 94 | d = dihed_angle.cost_d 95 | dd = dihed_angle.cost_dd 96 | # calculate ref value 97 | sin_ang1 = np.sin(bend_angle(dihed_angle._coordinates[:3])) ** 2 98 | sin_ang2 = np.sin(bend_angle(dihed_angle._coordinates[1:])) ** 2 99 | ref_v = ( 100 | sin_ang1 101 | * sin_ang2 102 | * (2 - 2 * np.cos(dihed_angle.value - dihed_angle.target)) 103 | ) 104 | assert np.allclose(ref_v, v) 105 | # finite diff for g 106 | ref_v2 = ( 107 | sin_ang1 108 | * sin_ang2 109 | * (2 - 2 * np.cos((dihed_angle.value - 1e-6) - dihed_angle.target)) 110 | ) 111 | ref_d = (ref_v - ref_v2) / 1e-6 112 | assert np.allclose(ref_d, d, atol=1e-6) 113 | # finite diff for h 114 | ref_d1 = ( 115 | sin_ang1 * sin_ang2 * 2 * np.sin(dihed_angle.value - dihed_angle.target) 116 | ) 117 | assert np.allclose(ref_d1, d) 118 | ref_d2 = ( 119 | sin_ang1 120 | * sin_ang2 121 | * 2 122 | * np.sin(dihed_angle.value - 1e-6 - dihed_angle.target) 123 | ) 124 | ref_dd = (ref_d1 - ref_d2) / 1e-6 125 | assert np.allclose(ref_dd, dd, atol=1e-6) 126 | dihed_angle.weight = 1.2 127 | assert np.allclose(dd * 1.2, dihed_angle.cost_dd) 128 | assert np.allclose(d * 1.2, dihed_angle.cost_d) 129 | assert np.allclose(v * 1.2, dihed_angle.cost_v) 130 | 131 | def test_convention_dihedral(self): 132 | conv_dihed = ConventionDihedral( 133 | (2, 0, 1, 5), self.molecule.coordinates[[2, 0, 1, 5]] 134 | ) 135 | assert conv_dihed.value - 0.5000093782761452 < 1e-8 136 | conv_dihed.set_new_coordinates(self.molecule.coordinates[[3, 0, 1, 5]]) 137 | assert conv_dihed.value - 0.5000015188648903 < 1e-8 138 | 139 | def test_new_dihed_dot(self): 140 | new_dihed_dot = NewDihedralDot( 141 | (2, 0, 1, 5), self.molecule.coordinates[[2, 0, 1, 5]] 142 | ) 143 | assert new_dihed_dot.value - 0.33334848858597832 < 1e-8 144 | new_dihed_dot.set_new_coordinates(self.molecule.coordinates[[3, 0, 1, 5]]) 145 | assert new_dihed_dot.value - 0.33333649967203649 < 1e-8 146 | 147 | def test_new_dihed_cross(self): 148 | new_dihed_cross = NewDihedralCross( 149 | (2, 0, 1, 5), self.molecule.coordinates[[2, 0, 1, 5]] 150 | ) 151 | assert new_dihed_cross.value - 0.76979948283180566 < 1e-8 152 | new_dihed_cross.set_new_coordinates(self.molecule.coordinates[[3, 0, 1, 5]]) 153 | assert new_dihed_cross.value - (-0.76980062801256954) < 1e-8 154 | -------------------------------------------------------------------------------- /saddle/test/test_gaussian_wrapper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.conf import WORK_DIR 6 | from saddle.gaussianwrapper import GaussianWrapper 7 | from saddle.utils import Utils 8 | 9 | 10 | class TestGaussWrap(unittest.TestCase): 11 | 12 | with path("saddle.test", "") as test_folder: 13 | test_path = test_folder 14 | file_list = [] 15 | 16 | def setUp(self): 17 | with path("saddle.test.data", "water.xyz") as mol_path: 18 | mol = Utils.load_file(mol_path) 19 | self.gwob = GaussianWrapper(mol, title="water") 20 | with path("saddle.test.data", "") as file_path: 21 | self.save_path = file_path 22 | 23 | def test_create_ins(self): 24 | assert self.gwob.title == "water" 25 | assert isinstance(self.gwob.molecule, Utils) 26 | 27 | def test_create_input(self): 28 | self.gwob.create_gauss_input(0, 1, spe_title="test_gauss") 29 | filepath = WORK_DIR / "test_gauss.com" 30 | mol = Utils.load_file(filepath) 31 | self.file_list.append(filepath) 32 | assert np.allclose(self.gwob.molecule.coordinates, mol.coordinates) 33 | 34 | def test_create_input_gjf(self): 35 | self.gwob.create_gauss_input( 36 | 0, 1, spe_title="test_2nd_gauss", path=self.test_path, postfix=".gjf" 37 | ) 38 | filepath = self.test_path / "test_2nd_gauss.gjf" 39 | self.file_list.append(filepath) 40 | mol = Utils.load_file(filepath) 41 | assert np.allclose(self.gwob.molecule.coordinates, mol.coordinates) 42 | 43 | def test_create_input_file(self): 44 | self.gwob.title = "test_untitled" 45 | input_file = self.gwob._create_input_file(0, 1) 46 | filepath = WORK_DIR / (input_file + ".com") 47 | mol = Utils.load_file(filepath) 48 | self.file_list.append(filepath) 49 | assert np.allclose(self.gwob.molecule.coordinates, mol.coordinates) 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | for i in cls.file_list: 54 | i.unlink() 55 | -------------------------------------------------------------------------------- /saddle/test/test_math_lib.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | from numpy.testing import assert_allclose 5 | from saddle.errors import PositiveProductError 6 | from saddle.math_lib import ( 7 | diagonalize, 8 | pse_inv, 9 | ridders_solver, 10 | maximum_overlap, 11 | procrustes, 12 | ) 13 | 14 | 15 | # pylint: disable=E1101, E1133 16 | # Disable pylint on numpy.random functions 17 | class TestSolver(unittest.TestCase): 18 | def test_ridder_quadratic(self): 19 | def func(x): 20 | return x ** 2 - 4 21 | 22 | answer = ridders_solver(func, -1, 10) 23 | assert abs(answer - 2) < 1e-6 24 | 25 | try: 26 | ridders_solver(func, -3, 3) 27 | except PositiveProductError: 28 | assert True 29 | else: 30 | assert False 31 | 32 | def test_ridder_quatic(self): 33 | def func(x): 34 | return x ** 3 - x ** 2 - 2 * x 35 | 36 | answer = ridders_solver(func, -2, 0) 37 | assert answer == 0 38 | 39 | answer = ridders_solver(func, -2, -0.5) 40 | assert abs(answer - (-1)) < 1e-6 41 | 42 | answer = ridders_solver(func, -0.5, 1) 43 | assert abs(answer - 0) < 1e-6 44 | 45 | answer = ridders_solver(func, 1.5, 3) 46 | assert abs(answer - 2) < 1e-6 47 | 48 | def test_diagonalize(self): 49 | # np.random.seed(111) 50 | mtx = np.random.rand(4, 2) 51 | assert mtx.shape == (4, 2) 52 | ei_value, ei_vector = diagonalize(mtx) 53 | prd = np.dot(mtx, mtx.T) 54 | assert prd.shape == (4, 4) 55 | v, w = np.linalg.eigh(prd) 56 | assert np.allclose(ei_value, v) 57 | assert np.allclose(ei_vector, w) 58 | 59 | def test_pse_inv(self): 60 | # np.random.seed(500) 61 | mtr_1 = np.random.rand(5, 5) 62 | mtr_1_r = pse_inv(mtr_1) 63 | assert np.allclose(np.dot(mtr_1, mtr_1_r), np.eye(5)) 64 | 65 | mtr_2 = np.array([[3, 0], [0, 0]]) 66 | mtr_2_r = pse_inv(mtr_2) 67 | ref_mtr = np.array([[1 / 3, 0], [0, 0]]) 68 | assert np.allclose(mtr_2_r, ref_mtr) 69 | 70 | mtr_3 = np.array([[3, 3e-9], [1e-11, 2e-10]]) 71 | mtr_3_r = pse_inv(mtr_3) 72 | assert np.allclose(mtr_3_r, ref_mtr) 73 | 74 | def test_pse_inv_with_np(self): 75 | # np.random.seed(100) 76 | for _ in range(5): 77 | shape = np.random.randint(1, 10, 2) 78 | target_mt = np.random.rand(*shape) 79 | np_ref = np.linalg.pinv(target_mt) 80 | pse_inv_res = pse_inv(target_mt) 81 | assert np.allclose(pse_inv_res, np_ref) 82 | 83 | def test_pse_inv_close(self): 84 | # np.random.seed(200) 85 | for _ in range(5): 86 | shape = np.random.randint(1, 20, 2) 87 | rand_mt = np.random.rand(*shape) 88 | inv_mt = pse_inv(rand_mt) 89 | diff = np.dot(np.dot(rand_mt, inv_mt), rand_mt) - rand_mt 90 | assert np.allclose(np.linalg.norm(diff), 0) 91 | 92 | def test_maximum_overlap(self): 93 | # oned case 94 | array_a = np.random.rand(5).reshape(5, 1) 95 | array_a /= np.linalg.norm(array_a) 96 | array_b = np.random.rand(5).reshape(5, 1) 97 | array_b /= np.linalg.norm(array_b) 98 | tf_mtr = maximum_overlap(array_a, array_b) 99 | new_b = np.dot(tf_mtr, array_b) 100 | assert_allclose(array_a, new_b) 101 | # nd case 102 | rand_c = np.random.rand(5, 5) 103 | array_c = np.linalg.eigh(np.dot(rand_c, rand_c.T))[1] 104 | array_c /= np.linalg.norm(array_c, axis=0) 105 | rand_d = np.random.rand(5, 5) 106 | array_d = np.linalg.eigh(np.dot(rand_d, rand_d.T))[1] 107 | array_d /= np.linalg.norm(array_d, axis=0) 108 | tf_mtr = maximum_overlap(array_c, array_d) 109 | new_d = np.dot(tf_mtr, array_d) 110 | assert_allclose(array_c, new_d) 111 | 112 | def test_procrustes(self): 113 | np.random.seed(101) 114 | for _ in range(5): 115 | a = np.random.rand(3, 2) 116 | n_a, s_a, m_a = np.linalg.svd(a) 117 | a_ref = n_a[:, :2] 118 | 119 | b = np.random.rand(3, 2) 120 | n_b, s_b, m_b = np.linalg.svd(b) 121 | b_ref = n_b[:, :2] 122 | result = procrustes(a_ref, b_ref) 123 | assert np.allclose(result, b_ref) 124 | 125 | for _ in range(5): 126 | a = np.random.rand(6, 4) 127 | n_a, s_a, m_a = np.linalg.svd(a) 128 | a_ref = n_a[:, :4] 129 | 130 | b = np.random.rand(6, 4) 131 | n_b, s_b, m_b = np.linalg.svd(b) 132 | b_ref = n_b[:, :4] 133 | result = procrustes(a_ref, b_ref) 134 | assert np.allclose(result, b_ref) 135 | -------------------------------------------------------------------------------- /saddle/test/test_path_ri.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from numpy.testing import assert_allclose 6 | from saddle.math_lib import diagonalize 7 | from saddle.ts_construct import TSConstruct 8 | 9 | 10 | class TestPathRI(unittest.TestCase): 11 | @classmethod 12 | def setup_class(self): 13 | with path("saddle.test.data", "rct.xyz") as rct_path: 14 | with path("saddle.test.data", "prd.xyz") as prd_path: 15 | self.ts_mol = TSConstruct.from_file(rct_path, prd_path) 16 | self.ts_mol.auto_generate_ts(task="path") 17 | self.path_mol = self.ts_mol.ts 18 | 19 | def test_basic_property(self): 20 | assert self.path_mol.key_ic_number == 0 21 | try: 22 | self.path_mol.set_key_ic_number(2) 23 | except NotImplementedError: 24 | pass 25 | assert self.path_mol.key_ic_number == 0 26 | 27 | diff = self.ts_mol.prd.ic_values - self.ts_mol.rct.ic_values 28 | assert np.allclose(self.path_mol.path_vector, diff) 29 | 30 | # genreate proper real unit path vector 31 | real_path_v = ( 32 | self.ts_mol.ts.b_matrix @ np.linalg.pinv(self.ts_mol.ts.b_matrix) @ diff 33 | ) 34 | assert np.allclose( 35 | real_path_v / np.linalg.norm(real_path_v), 36 | self.ts_mol.ts.real_unit_path_vector, 37 | ) 38 | 39 | # dim space of v is one less than degree of freedom 40 | assert self.ts_mol.ts.vspace.shape[1] == self.ts_mol.ts.df - 1 41 | 42 | def test_v_space(self): 43 | self.path_mol._generate_reduce_space() 44 | assert np.linalg.norm(self.path_mol._red_space) - 1 < 1e-8 45 | diff = np.dot(self.path_mol.vspace.T, self.path_mol.path_vector) 46 | assert np.linalg.norm(diff) < 1e-8 47 | self.path_mol._reset_v_space() 48 | assert self.path_mol._vspace is None 49 | assert self.path_mol.key_ic_number == 0 50 | diff = np.dot(self.path_mol.vspace.T, self.path_mol.path_vector) 51 | assert np.linalg.norm(diff) < 1e-8 52 | 53 | def test_v_space_change(self): 54 | diff = np.dot(self.path_mol.vspace.T, self.path_mol.path_vector) 55 | assert np.linalg.norm(diff) < 1e-8 56 | # construct vspace 57 | c = np.random.rand(9, 5) 58 | values, vectors = np.linalg.eigh(np.dot(c, c.T)) 59 | basis_vects = vectors[:, np.abs(values) > 1e-5] 60 | # project out path_vector 61 | no_path_space = basis_vects - np.dot( 62 | np.outer( 63 | self.path_mol.real_unit_path_vector, self.path_mol.real_unit_path_vector 64 | ), 65 | basis_vects, 66 | ) 67 | assert ( 68 | np.linalg.norm(np.dot(no_path_space.T, self.path_mol.real_unit_path_vector)) 69 | < 1e-8 70 | ) 71 | w, v = diagonalize(no_path_space) 72 | new_v = v[:, np.abs(w) > 1e-5] 73 | assert ( 74 | np.linalg.norm(np.dot(new_v.T, self.path_mol.real_unit_path_vector)) < 1e-8 75 | ) 76 | # test all part are orthonormal 77 | assert_allclose(np.linalg.norm(new_v, axis=0), np.ones(c.shape[1])) 78 | 79 | # given a random vspace, generate a property one 80 | self.path_mol.set_vspace(basis_vects) 81 | assert_allclose(self.path_mol.vspace, new_v) 82 | assert_allclose( 83 | np.linalg.norm(self.path_mol.vspace, axis=0), np.ones(c.shape[1]) 84 | ) 85 | -------------------------------------------------------------------------------- /saddle/test/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.utils import Utils 6 | 7 | 8 | class TestUtils(TestCase): 9 | 10 | file_list = [] 11 | 12 | def test_load_xyz(self): 13 | with path("saddle.test.data", "water.xyz") as file_path: 14 | nums, coors, title = Utils._load_xyz(file_path) 15 | assert np.allclose(nums, np.array([1, 8, 1])) 16 | ref_coor = np.array( 17 | [ 18 | [1.481237149, -0.93019116, 0.0], 19 | [0.0, 0.11720080, 0], 20 | [-1.481237149, -0.93019116, 0.0], 21 | ] 22 | ) 23 | assert np.allclose(coors, ref_coor) 24 | assert title == "water" 25 | 26 | def test_save_xyz(self): 27 | with path("saddle.test.data", "water.xyz") as file_path: 28 | water_mol = Utils.load_file(file_path) 29 | with path("saddle.test.data", "") as file_path: 30 | new_file_name = file_path / "test_base_mole_test_file" 31 | Utils.save_file(new_file_name, water_mol) 32 | new_add_file = new_file_name.parent / (new_file_name.name + ".xyz") 33 | TestUtils.file_list.append(new_add_file) 34 | with path("saddle.test.data", "test_base_mole_test_file.xyz") as file_path: 35 | mol = Utils.load_file(file_path) 36 | ref_coor = np.array( 37 | [ 38 | [1.481237149, -0.93019116, 0.0], 39 | [0.0, 0.11720080, 0], 40 | [-1.481237149, -0.93019116, 0.0], 41 | ] 42 | ) 43 | assert np.allclose(mol.coordinates, ref_coor) 44 | assert np.allclose(mol.numbers, [1, 8, 1]) 45 | 46 | @classmethod 47 | def tearDownClass(cls): 48 | for i in cls.file_list: 49 | i.unlink() 50 | -------------------------------------------------------------------------------- /saddle/test/test_water_fchk.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | from importlib_resources import path 5 | from saddle.fchk import FCHKFile 6 | 7 | 8 | class TestFormatCheck(unittest.TestCase): 9 | def test_water_fchk(self): 10 | with path("saddle.test.data", "water_0.fchk") as fchk_path: 11 | f = FCHKFile(fchk_path) 12 | hessian = f.get_hessian() 13 | gradient = f.get_gradient() 14 | energy = f.get_energy() 15 | result_h = [ 16 | [ 17 | 3.17131718e-02, 18 | -4.18734326e-18, 19 | -1.38547897e-18, 20 | -2.93155973e-02, 21 | -3.12557821e-18, 22 | 6.43451786e-22, 23 | -2.39757454e-03, 24 | 1.03805567e-17, 25 | 2.74615253e-18, 26 | ], 27 | [ 28 | -4.18734326e-18, 29 | -1.49385417e-02, 30 | 3.33228038e-02, 31 | -3.55771944e-18, 32 | 1.40893187e-02, 33 | -3.59538416e-02, 34 | 3.09439885e-18, 35 | 8.49222729e-04, 36 | 2.63103801e-03, 37 | ], 38 | [ 39 | -1.38547897e-18, 40 | 3.33228038e-02, 41 | 1.90370396e-02, 42 | 7.51471617e-19, 43 | -3.06917656e-02, 44 | -2.56655894e-02, 45 | 3.17532757e-18, 46 | -2.63103801e-03, 47 | 6.62854966e-03, 48 | ], 49 | [ 50 | -2.93155973e-02, 51 | -3.55771944e-18, 52 | 7.51471617e-19, 53 | 5.86311945e-02, 54 | -2.01776317e-16, 55 | -1.43495471e-16, 56 | -2.93155973e-02, 57 | 2.20054701e-16, 58 | 1.46795989e-16, 59 | ], 60 | [ 61 | -3.12557821e-18, 62 | 1.40893187e-02, 63 | -3.06917656e-02, 64 | -2.01776317e-16, 65 | -2.81786371e-02, 66 | -3.73661267e-10, 67 | 2.27339208e-16, 68 | 1.40893188e-02, 69 | 3.06917656e-02, 70 | ], 71 | [ 72 | 6.43451786e-22, 73 | -3.59538416e-02, 74 | -2.56655894e-02, 75 | -1.43495471e-16, 76 | -3.73661267e-10, 77 | 5.13311785e-02, 78 | 1.46365044e-16, 79 | 3.59538420e-02, 80 | -2.56655891e-02, 81 | ], 82 | [ 83 | -2.39757454e-03, 84 | 3.09439885e-18, 85 | 3.17532757e-18, 86 | -2.93155973e-02, 87 | 2.27339208e-16, 88 | 1.46365044e-16, 89 | 3.17131718e-02, 90 | -2.48221907e-16, 91 | -1.54953678e-16, 92 | ], 93 | [ 94 | 1.03805567e-17, 95 | 8.49222729e-04, 96 | -2.63103801e-03, 97 | 2.20054701e-16, 98 | 1.40893188e-02, 99 | 3.59538420e-02, 100 | -2.48221907e-16, 101 | -1.49385417e-02, 102 | -3.33228038e-02, 103 | ], 104 | [ 105 | 2.74615253e-18, 106 | 2.63103801e-03, 107 | 6.62854966e-03, 108 | 1.46795989e-16, 109 | 3.06917656e-02, 110 | -2.56655891e-02, 111 | -1.54953678e-16, 112 | -3.33228038e-02, 113 | 1.90370396e-02, 114 | ], 115 | ] 116 | result_g = [ 117 | 1.25447800e-17, 118 | 9.54806810e-02, 119 | -5.80237759e-02, 120 | 2.57560310e-16, 121 | 1.31838984e-16, 122 | 1.16047552e-01, 123 | -2.70105090e-16, 124 | -9.54806810e-02, 125 | -5.80237759e-02, 126 | ] 127 | result_e = -75.2323588708 128 | assert np.allclose(result_h, hessian) 129 | assert np.allclose(result_g, gradient) 130 | assert np.allclose(result_e, energy) 131 | -------------------------------------------------------------------------------- /saddle/work/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theochem/gopt/7d39db2195cfad893e201e655401885e3b151c43/saddle/work/log/.gitkeep -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Setup file for installing GOpt.""" 3 | import saddle 4 | 5 | from setuptools import find_packages, setup 6 | 7 | """Setup installation dependencies.""" 8 | setup( 9 | name="saddle", 10 | version=saddle.__version__, 11 | description="Geometry optimization program for chemical reaction", 12 | license=saddle.__license__, 13 | author=saddle.__author__, 14 | author_email="yxt1991@gmail.com", 15 | package_dir={"saddle": "saddle"}, 16 | packages=find_packages(exclude=["*.test", "*.test.*", "test.*", "test"]), 17 | include_package_data=True, 18 | package_data={ 19 | "saddle": ["data/*.json", "data/*.com", "work/log/.gitkeep"], 20 | "saddle.periodic": ["data/*.csv"], 21 | }, 22 | install_requires=[ 23 | "numpy>=1.16", 24 | "pytest>=2.6", 25 | # "scipy>=1.2", 26 | "importlib_resources", 27 | ] 28 | # package_data is only useful for bdist 29 | # add to MANIFEST.in works for both bdist and sdist 30 | ) 31 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, build, flake8, black 3 | skipsdist = true 4 | ignore_errors = true 5 | 6 | [testenv] 7 | passenv = CI TRAVIS TRAVIS_* 8 | deps = 9 | pytest-cov 10 | codecov 11 | commands = 12 | pip install -e . 13 | pytest --cov-report term-missing --cov=saddle 14 | codecov 15 | 16 | [testenv:build] 17 | basepython = python3 18 | skip_install = true 19 | deps = 20 | pytest 21 | wheel 22 | setuptools 23 | commands = 24 | python setup.py -q sdist bdist_wheel 25 | pip install . 26 | pytest --pyargs saddle 27 | 28 | [testenv:flake8] 29 | basepython = python3 30 | skip_install = true 31 | deps = 32 | flake8 33 | flake8-docstrings >= 0.2.7 34 | flake8-import-order >= 0.9 35 | pydocstyle == 3.0.0 36 | flake8-colors 37 | commands = 38 | flake8 --version 39 | flake8 ./saddle ./setup.py 40 | 41 | [testenv:black] 42 | basepython = python3 43 | skip_install = true 44 | deps = 45 | black 46 | commands = 47 | black -v --check --diff ./saddle ./setup.py 48 | 49 | [flake8] 50 | max-line-length = 100 51 | exclude = */test/* 52 | ignore = 53 | # false positive error around ":" for slicing 54 | E203 55 | # numpy stype docstring ignores 56 | D107, D203, D212, D213, D402, D413 57 | # Not pep8 for operator 58 | W503 59 | format = 60 | ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s 61 | 62 | [coverage:run] 63 | omit = */test* 64 | --------------------------------------------------------------------------------