mlmm-toolkit 0.2.2.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hessian_ff/__init__.py +50 -0
- hessian_ff/analytical_hessian.py +609 -0
- hessian_ff/constants.py +46 -0
- hessian_ff/forcefield.py +339 -0
- hessian_ff/loaders.py +608 -0
- hessian_ff/native/Makefile +8 -0
- hessian_ff/native/__init__.py +28 -0
- hessian_ff/native/analytical_hessian.py +88 -0
- hessian_ff/native/analytical_hessian_ext.cpp +258 -0
- hessian_ff/native/bonded.py +82 -0
- hessian_ff/native/bonded_ext.cpp +640 -0
- hessian_ff/native/loader.py +349 -0
- hessian_ff/native/nonbonded.py +118 -0
- hessian_ff/native/nonbonded_ext.cpp +1150 -0
- hessian_ff/prmtop_parmed.py +23 -0
- hessian_ff/system.py +107 -0
- hessian_ff/terms/__init__.py +14 -0
- hessian_ff/terms/angle.py +73 -0
- hessian_ff/terms/bond.py +44 -0
- hessian_ff/terms/cmap.py +406 -0
- hessian_ff/terms/dihedral.py +141 -0
- hessian_ff/terms/nonbonded.py +209 -0
- hessian_ff/tests/__init__.py +0 -0
- hessian_ff/tests/conftest.py +75 -0
- hessian_ff/tests/data/small/complex.parm7 +1346 -0
- hessian_ff/tests/data/small/complex.pdb +125 -0
- hessian_ff/tests/data/small/complex.rst7 +63 -0
- hessian_ff/tests/test_coords_input.py +44 -0
- hessian_ff/tests/test_energy_force.py +49 -0
- hessian_ff/tests/test_hessian.py +137 -0
- hessian_ff/tests/test_smoke.py +18 -0
- hessian_ff/tests/test_validation.py +40 -0
- hessian_ff/workflows.py +889 -0
- mlmm/__init__.py +36 -0
- mlmm/__main__.py +7 -0
- mlmm/_version.py +34 -0
- mlmm/add_elem_info.py +374 -0
- mlmm/advanced_help.py +91 -0
- mlmm/align_freeze_atoms.py +601 -0
- mlmm/all.py +3535 -0
- mlmm/bond_changes.py +231 -0
- mlmm/bool_compat.py +223 -0
- mlmm/cli.py +574 -0
- mlmm/cli_utils.py +166 -0
- mlmm/default_group.py +337 -0
- mlmm/defaults.py +467 -0
- mlmm/define_layer.py +526 -0
- mlmm/dft.py +1041 -0
- mlmm/energy_diagram.py +253 -0
- mlmm/extract.py +2213 -0
- mlmm/fix_altloc.py +464 -0
- mlmm/freq.py +1406 -0
- mlmm/harmonic_constraints.py +140 -0
- mlmm/hessian_cache.py +44 -0
- mlmm/hessian_calc.py +174 -0
- mlmm/irc.py +638 -0
- mlmm/mlmm_calc.py +2262 -0
- mlmm/mm_parm.py +945 -0
- mlmm/oniom_export.py +1983 -0
- mlmm/oniom_import.py +457 -0
- mlmm/opt.py +1742 -0
- mlmm/path_opt.py +1353 -0
- mlmm/path_search.py +2299 -0
- mlmm/preflight.py +88 -0
- mlmm/py.typed +1 -0
- mlmm/pysis_runner.py +45 -0
- mlmm/scan.py +1047 -0
- mlmm/scan2d.py +1226 -0
- mlmm/scan3d.py +1265 -0
- mlmm/scan_common.py +184 -0
- mlmm/summary_log.py +736 -0
- mlmm/trj2fig.py +448 -0
- mlmm/tsopt.py +2871 -0
- mlmm/utils.py +2309 -0
- mlmm/xtb_embedcharge_correction.py +475 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
- mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
- pysisyphus/Geometry.py +1667 -0
- pysisyphus/LICENSE +674 -0
- pysisyphus/TableFormatter.py +63 -0
- pysisyphus/TablePrinter.py +74 -0
- pysisyphus/__init__.py +12 -0
- pysisyphus/calculators/AFIR.py +452 -0
- pysisyphus/calculators/AnaPot.py +20 -0
- pysisyphus/calculators/AnaPot2.py +48 -0
- pysisyphus/calculators/AnaPot3.py +12 -0
- pysisyphus/calculators/AnaPot4.py +20 -0
- pysisyphus/calculators/AnaPotBase.py +337 -0
- pysisyphus/calculators/AnaPotCBM.py +25 -0
- pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
- pysisyphus/calculators/CFOUR.py +250 -0
- pysisyphus/calculators/Calculator.py +844 -0
- pysisyphus/calculators/CerjanMiller.py +24 -0
- pysisyphus/calculators/Composite.py +123 -0
- pysisyphus/calculators/ConicalIntersection.py +171 -0
- pysisyphus/calculators/DFTBp.py +430 -0
- pysisyphus/calculators/DFTD3.py +66 -0
- pysisyphus/calculators/DFTD4.py +84 -0
- pysisyphus/calculators/Dalton.py +61 -0
- pysisyphus/calculators/Dimer.py +681 -0
- pysisyphus/calculators/Dummy.py +20 -0
- pysisyphus/calculators/EGO.py +76 -0
- pysisyphus/calculators/EnergyMin.py +224 -0
- pysisyphus/calculators/ExternalPotential.py +264 -0
- pysisyphus/calculators/FakeASE.py +35 -0
- pysisyphus/calculators/FourWellAnaPot.py +28 -0
- pysisyphus/calculators/FreeEndNEBPot.py +39 -0
- pysisyphus/calculators/Gaussian09.py +18 -0
- pysisyphus/calculators/Gaussian16.py +726 -0
- pysisyphus/calculators/HardSphere.py +159 -0
- pysisyphus/calculators/IDPPCalculator.py +49 -0
- pysisyphus/calculators/IPIClient.py +133 -0
- pysisyphus/calculators/IPIServer.py +234 -0
- pysisyphus/calculators/LEPSBase.py +24 -0
- pysisyphus/calculators/LEPSExpr.py +139 -0
- pysisyphus/calculators/LennardJones.py +80 -0
- pysisyphus/calculators/MOPAC.py +219 -0
- pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
- pysisyphus/calculators/MultiCalc.py +85 -0
- pysisyphus/calculators/NFK.py +45 -0
- pysisyphus/calculators/OBabel.py +87 -0
- pysisyphus/calculators/ONIOMv2.py +1129 -0
- pysisyphus/calculators/ORCA.py +893 -0
- pysisyphus/calculators/ORCA5.py +6 -0
- pysisyphus/calculators/OpenMM.py +88 -0
- pysisyphus/calculators/OpenMolcas.py +281 -0
- pysisyphus/calculators/OverlapCalculator.py +908 -0
- pysisyphus/calculators/Psi4.py +218 -0
- pysisyphus/calculators/PyPsi4.py +37 -0
- pysisyphus/calculators/PySCF.py +341 -0
- pysisyphus/calculators/PyXTB.py +73 -0
- pysisyphus/calculators/QCEngine.py +106 -0
- pysisyphus/calculators/Rastrigin.py +22 -0
- pysisyphus/calculators/Remote.py +76 -0
- pysisyphus/calculators/Rosenbrock.py +15 -0
- pysisyphus/calculators/SocketCalc.py +97 -0
- pysisyphus/calculators/TIP3P.py +111 -0
- pysisyphus/calculators/TransTorque.py +161 -0
- pysisyphus/calculators/Turbomole.py +965 -0
- pysisyphus/calculators/VRIPot.py +37 -0
- pysisyphus/calculators/WFOWrapper.py +333 -0
- pysisyphus/calculators/WFOWrapper2.py +341 -0
- pysisyphus/calculators/XTB.py +418 -0
- pysisyphus/calculators/__init__.py +81 -0
- pysisyphus/calculators/cosmo_data.py +139 -0
- pysisyphus/calculators/parser.py +150 -0
- pysisyphus/color.py +19 -0
- pysisyphus/config.py +133 -0
- pysisyphus/constants.py +65 -0
- pysisyphus/cos/AdaptiveNEB.py +230 -0
- pysisyphus/cos/ChainOfStates.py +725 -0
- pysisyphus/cos/FreeEndNEB.py +25 -0
- pysisyphus/cos/FreezingString.py +103 -0
- pysisyphus/cos/GrowingChainOfStates.py +71 -0
- pysisyphus/cos/GrowingNT.py +309 -0
- pysisyphus/cos/GrowingString.py +508 -0
- pysisyphus/cos/NEB.py +189 -0
- pysisyphus/cos/SimpleZTS.py +64 -0
- pysisyphus/cos/__init__.py +22 -0
- pysisyphus/cos/stiffness.py +199 -0
- pysisyphus/drivers/__init__.py +17 -0
- pysisyphus/drivers/afir.py +855 -0
- pysisyphus/drivers/barriers.py +271 -0
- pysisyphus/drivers/birkholz.py +138 -0
- pysisyphus/drivers/cluster.py +318 -0
- pysisyphus/drivers/diabatization.py +133 -0
- pysisyphus/drivers/merge.py +368 -0
- pysisyphus/drivers/merge_mol2.py +322 -0
- pysisyphus/drivers/opt.py +375 -0
- pysisyphus/drivers/perf.py +91 -0
- pysisyphus/drivers/pka.py +52 -0
- pysisyphus/drivers/precon_pos_rot.py +669 -0
- pysisyphus/drivers/rates.py +480 -0
- pysisyphus/drivers/replace.py +219 -0
- pysisyphus/drivers/scan.py +212 -0
- pysisyphus/drivers/spectrum.py +166 -0
- pysisyphus/drivers/thermo.py +31 -0
- pysisyphus/dynamics/Gaussian.py +103 -0
- pysisyphus/dynamics/__init__.py +20 -0
- pysisyphus/dynamics/colvars.py +136 -0
- pysisyphus/dynamics/driver.py +297 -0
- pysisyphus/dynamics/helpers.py +256 -0
- pysisyphus/dynamics/lincs.py +105 -0
- pysisyphus/dynamics/mdp.py +364 -0
- pysisyphus/dynamics/rattle.py +121 -0
- pysisyphus/dynamics/thermostats.py +128 -0
- pysisyphus/dynamics/wigner.py +266 -0
- pysisyphus/elem_data.py +3473 -0
- pysisyphus/exceptions.py +2 -0
- pysisyphus/filtertrj.py +69 -0
- pysisyphus/helpers.py +623 -0
- pysisyphus/helpers_pure.py +649 -0
- pysisyphus/init_logging.py +50 -0
- pysisyphus/intcoords/Bend.py +69 -0
- pysisyphus/intcoords/Bend2.py +25 -0
- pysisyphus/intcoords/BondedFragment.py +32 -0
- pysisyphus/intcoords/Cartesian.py +41 -0
- pysisyphus/intcoords/CartesianCoords.py +140 -0
- pysisyphus/intcoords/Coords.py +56 -0
- pysisyphus/intcoords/DLC.py +197 -0
- pysisyphus/intcoords/DistanceFunction.py +34 -0
- pysisyphus/intcoords/DummyImproper.py +70 -0
- pysisyphus/intcoords/DummyTorsion.py +72 -0
- pysisyphus/intcoords/LinearBend.py +105 -0
- pysisyphus/intcoords/LinearDisplacement.py +80 -0
- pysisyphus/intcoords/OutOfPlane.py +59 -0
- pysisyphus/intcoords/PrimTypes.py +286 -0
- pysisyphus/intcoords/Primitive.py +137 -0
- pysisyphus/intcoords/RedundantCoords.py +659 -0
- pysisyphus/intcoords/RobustTorsion.py +59 -0
- pysisyphus/intcoords/Rotation.py +147 -0
- pysisyphus/intcoords/Stretch.py +31 -0
- pysisyphus/intcoords/Torsion.py +101 -0
- pysisyphus/intcoords/Torsion2.py +25 -0
- pysisyphus/intcoords/Translation.py +45 -0
- pysisyphus/intcoords/__init__.py +61 -0
- pysisyphus/intcoords/augment_bonds.py +126 -0
- pysisyphus/intcoords/derivatives.py +10512 -0
- pysisyphus/intcoords/eval.py +80 -0
- pysisyphus/intcoords/exceptions.py +37 -0
- pysisyphus/intcoords/findiffs.py +48 -0
- pysisyphus/intcoords/generate_derivatives.py +414 -0
- pysisyphus/intcoords/helpers.py +235 -0
- pysisyphus/intcoords/logging_conf.py +10 -0
- pysisyphus/intcoords/mp_derivatives.py +10836 -0
- pysisyphus/intcoords/setup.py +962 -0
- pysisyphus/intcoords/setup_fast.py +176 -0
- pysisyphus/intcoords/update.py +272 -0
- pysisyphus/intcoords/valid.py +89 -0
- pysisyphus/interpolate/Geodesic.py +93 -0
- pysisyphus/interpolate/IDPP.py +55 -0
- pysisyphus/interpolate/Interpolator.py +116 -0
- pysisyphus/interpolate/LST.py +70 -0
- pysisyphus/interpolate/Redund.py +152 -0
- pysisyphus/interpolate/__init__.py +9 -0
- pysisyphus/interpolate/helpers.py +34 -0
- pysisyphus/io/__init__.py +22 -0
- pysisyphus/io/aomix.py +178 -0
- pysisyphus/io/cjson.py +24 -0
- pysisyphus/io/crd.py +101 -0
- pysisyphus/io/cube.py +220 -0
- pysisyphus/io/fchk.py +184 -0
- pysisyphus/io/hdf5.py +49 -0
- pysisyphus/io/hessian.py +72 -0
- pysisyphus/io/mol2.py +146 -0
- pysisyphus/io/molden.py +293 -0
- pysisyphus/io/orca.py +189 -0
- pysisyphus/io/pdb.py +269 -0
- pysisyphus/io/psf.py +79 -0
- pysisyphus/io/pubchem.py +31 -0
- pysisyphus/io/qcschema.py +34 -0
- pysisyphus/io/sdf.py +29 -0
- pysisyphus/io/xyz.py +61 -0
- pysisyphus/io/zmat.py +175 -0
- pysisyphus/irc/DWI.py +108 -0
- pysisyphus/irc/DampedVelocityVerlet.py +134 -0
- pysisyphus/irc/Euler.py +22 -0
- pysisyphus/irc/EulerPC.py +345 -0
- pysisyphus/irc/GonzalezSchlegel.py +187 -0
- pysisyphus/irc/IMKMod.py +164 -0
- pysisyphus/irc/IRC.py +878 -0
- pysisyphus/irc/IRCDummy.py +10 -0
- pysisyphus/irc/Instanton.py +307 -0
- pysisyphus/irc/LQA.py +53 -0
- pysisyphus/irc/ModeKill.py +136 -0
- pysisyphus/irc/ParamPlot.py +53 -0
- pysisyphus/irc/RK4.py +36 -0
- pysisyphus/irc/__init__.py +31 -0
- pysisyphus/irc/initial_displ.py +219 -0
- pysisyphus/linalg.py +411 -0
- pysisyphus/line_searches/Backtracking.py +88 -0
- pysisyphus/line_searches/HagerZhang.py +184 -0
- pysisyphus/line_searches/LineSearch.py +232 -0
- pysisyphus/line_searches/StrongWolfe.py +108 -0
- pysisyphus/line_searches/__init__.py +9 -0
- pysisyphus/line_searches/interpol.py +15 -0
- pysisyphus/modefollow/NormalMode.py +40 -0
- pysisyphus/modefollow/__init__.py +10 -0
- pysisyphus/modefollow/davidson.py +199 -0
- pysisyphus/modefollow/lanczos.py +95 -0
- pysisyphus/optimizers/BFGS.py +99 -0
- pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
- pysisyphus/optimizers/ConjugateGradient.py +98 -0
- pysisyphus/optimizers/CubicNewton.py +75 -0
- pysisyphus/optimizers/FIRE.py +113 -0
- pysisyphus/optimizers/HessianOptimizer.py +1176 -0
- pysisyphus/optimizers/LBFGS.py +228 -0
- pysisyphus/optimizers/LayerOpt.py +411 -0
- pysisyphus/optimizers/MicroOptimizer.py +169 -0
- pysisyphus/optimizers/NCOptimizer.py +90 -0
- pysisyphus/optimizers/Optimizer.py +1084 -0
- pysisyphus/optimizers/PreconLBFGS.py +260 -0
- pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
- pysisyphus/optimizers/QuickMin.py +74 -0
- pysisyphus/optimizers/RFOptimizer.py +181 -0
- pysisyphus/optimizers/RSA.py +99 -0
- pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
- pysisyphus/optimizers/SteepestDescent.py +23 -0
- pysisyphus/optimizers/StringOptimizer.py +173 -0
- pysisyphus/optimizers/__init__.py +41 -0
- pysisyphus/optimizers/closures.py +301 -0
- pysisyphus/optimizers/cls_map.py +58 -0
- pysisyphus/optimizers/exceptions.py +6 -0
- pysisyphus/optimizers/gdiis.py +280 -0
- pysisyphus/optimizers/guess_hessians.py +311 -0
- pysisyphus/optimizers/hessian_updates.py +355 -0
- pysisyphus/optimizers/poly_fit.py +285 -0
- pysisyphus/optimizers/precon.py +153 -0
- pysisyphus/optimizers/restrict_step.py +24 -0
- pysisyphus/pack.py +172 -0
- pysisyphus/peakdetect.py +948 -0
- pysisyphus/plot.py +1031 -0
- pysisyphus/run.py +2106 -0
- pysisyphus/socket_helper.py +74 -0
- pysisyphus/stocastic/FragmentKick.py +132 -0
- pysisyphus/stocastic/Kick.py +81 -0
- pysisyphus/stocastic/Pipeline.py +303 -0
- pysisyphus/stocastic/__init__.py +21 -0
- pysisyphus/stocastic/align.py +127 -0
- pysisyphus/testing.py +96 -0
- pysisyphus/thermo.py +156 -0
- pysisyphus/trj.py +824 -0
- pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
- pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
- pysisyphus/tsoptimizers/TRIM.py +59 -0
- pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
- pysisyphus/tsoptimizers/__init__.py +23 -0
- pysisyphus/wavefunction/Basis.py +239 -0
- pysisyphus/wavefunction/DIIS.py +76 -0
- pysisyphus/wavefunction/__init__.py +25 -0
- pysisyphus/wavefunction/build_ext.py +42 -0
- pysisyphus/wavefunction/cart2sph.py +190 -0
- pysisyphus/wavefunction/diabatization.py +304 -0
- pysisyphus/wavefunction/excited_states.py +435 -0
- pysisyphus/wavefunction/gen_ints.py +1811 -0
- pysisyphus/wavefunction/helpers.py +104 -0
- pysisyphus/wavefunction/ints/__init__.py +0 -0
- pysisyphus/wavefunction/ints/boys.py +193 -0
- pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
- pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
- pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
- pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
- pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
- pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
- pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
- pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
- pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
- pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
- pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
- pysisyphus/wavefunction/localization.py +458 -0
- pysisyphus/wavefunction/multipole.py +159 -0
- pysisyphus/wavefunction/normalization.py +36 -0
- pysisyphus/wavefunction/pop_analysis.py +134 -0
- pysisyphus/wavefunction/shells.py +1171 -0
- pysisyphus/wavefunction/wavefunction.py +504 -0
- pysisyphus/wrapper/__init__.py +11 -0
- pysisyphus/wrapper/exceptions.py +2 -0
- pysisyphus/wrapper/jmol.py +120 -0
- pysisyphus/wrapper/mwfn.py +169 -0
- pysisyphus/wrapper/packmol.py +71 -0
- pysisyphus/xyzloader.py +168 -0
- pysisyphus/yaml_mods.py +45 -0
- thermoanalysis/LICENSE +674 -0
- thermoanalysis/QCData.py +244 -0
- thermoanalysis/__init__.py +0 -0
- thermoanalysis/config.py +3 -0
- thermoanalysis/constants.py +20 -0
- thermoanalysis/thermo.py +1011 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import h5py
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from pysisyphus.calculators.Calculator import Calculator
|
|
8
|
+
from pysisyphus.helpers import get_tangent_trj_str
|
|
9
|
+
from pysisyphus.helpers_pure import rms
|
|
10
|
+
from pysisyphus.intcoords.helpers import get_weighted_bond_mode
|
|
11
|
+
from pysisyphus.linalg import perp_comp, make_unit_vec
|
|
12
|
+
from pysisyphus.optimizers.closures import small_lbfgs_closure
|
|
13
|
+
from pysisyphus.optimizers.restrict_step import get_scale_max
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RotationConverged(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Gaussian:
|
|
21
|
+
def __init__(self, height, center, std, N):
|
|
22
|
+
self.center = np.array(center)
|
|
23
|
+
self.height = float(height)
|
|
24
|
+
self.std = float(std)
|
|
25
|
+
self.N = np.array(N)
|
|
26
|
+
|
|
27
|
+
self.s0 = self.center.dot(self.N)
|
|
28
|
+
|
|
29
|
+
def energy(self, R, height=None):
|
|
30
|
+
if height is None:
|
|
31
|
+
height = self.height
|
|
32
|
+
|
|
33
|
+
return height * np.exp(-((R.dot(self.N) - self.s0) ** 2) / (2 * self.std**2))
|
|
34
|
+
|
|
35
|
+
def forces(self, R, height=None):
|
|
36
|
+
if height is None:
|
|
37
|
+
height = self.height
|
|
38
|
+
s_diff = R.dot(self.N) - self.s0
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
height
|
|
42
|
+
* np.exp(-(s_diff**2) / (2 * self.std**2))
|
|
43
|
+
* s_diff
|
|
44
|
+
/ self.std**2
|
|
45
|
+
* self.N
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return (
|
|
50
|
+
f"Gaussian(height={self.height:.4f}, center={self.center}, "
|
|
51
|
+
f"s0={self.s0:.4f}, std={self.std:.4f}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# [1] https://aip.scitation.org/doi/abs/10.1063/1.480097
|
|
56
|
+
# Original Dimer paper
|
|
57
|
+
# Henkelmann, 1999
|
|
58
|
+
# [2] https://doi.org/10.1063/1.1809574
|
|
59
|
+
# Comparison of TS search methods
|
|
60
|
+
# Olsen, 2004
|
|
61
|
+
# [3] https://doi.org/10.1063/1.2104507
|
|
62
|
+
# Comparison of Dimer and P-RFO
|
|
63
|
+
# Heyden, 2005
|
|
64
|
+
# [4] https://aip.scitation.org/doi/abs/10.1063/1.2815812
|
|
65
|
+
# Superlinear Dimer method
|
|
66
|
+
# Kästner, 2008
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Dimer(Calculator):
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
calculator,
|
|
73
|
+
*args,
|
|
74
|
+
N_raw=None,
|
|
75
|
+
length=0.0189,
|
|
76
|
+
rotation_max_cycles=15,
|
|
77
|
+
rotation_method="fourier",
|
|
78
|
+
rotation_thresh=1e-4,
|
|
79
|
+
rotation_tol=1,
|
|
80
|
+
rotation_max_element=0.001,
|
|
81
|
+
rotation_interpolate=True,
|
|
82
|
+
rotation_disable=False,
|
|
83
|
+
rotation_disable_pos_curv=True,
|
|
84
|
+
rotation_remove_trans=True,
|
|
85
|
+
trans_force_f_perp=True,
|
|
86
|
+
bonds=None,
|
|
87
|
+
N_hessian=None,
|
|
88
|
+
bias_rotation=False,
|
|
89
|
+
bias_translation=False,
|
|
90
|
+
bias_gaussian_dot=0.1,
|
|
91
|
+
seed=None,
|
|
92
|
+
write_orientations=True,
|
|
93
|
+
forward_hessian=True,
|
|
94
|
+
**kwargs,
|
|
95
|
+
):
|
|
96
|
+
super().__init__(*args, **kwargs)
|
|
97
|
+
|
|
98
|
+
self.logger = logging.getLogger("dimer")
|
|
99
|
+
|
|
100
|
+
self.calculator = calculator
|
|
101
|
+
self.length = float(length)
|
|
102
|
+
|
|
103
|
+
# Rotation parameters
|
|
104
|
+
self.rotation_max_cycles = int(rotation_max_cycles)
|
|
105
|
+
|
|
106
|
+
rotation_methods = {
|
|
107
|
+
"direct": self.direct_rotation,
|
|
108
|
+
"fourier": self.fourier_rotation,
|
|
109
|
+
}
|
|
110
|
+
try:
|
|
111
|
+
self.rotation_method = rotation_methods[rotation_method]
|
|
112
|
+
except KeyError as err:
|
|
113
|
+
print(
|
|
114
|
+
f"Invalid rotation_method={rotation_method}! Valid types are: "
|
|
115
|
+
f"{tuple(self.rotation_methods.keys())}"
|
|
116
|
+
)
|
|
117
|
+
raise err
|
|
118
|
+
self.rotation_thresh = float(rotation_thresh)
|
|
119
|
+
self.rotation_tol = np.deg2rad(rotation_tol)
|
|
120
|
+
self.rotation_max_element = float(rotation_max_element)
|
|
121
|
+
self.rotation_interpolate = bool(rotation_interpolate)
|
|
122
|
+
self.rotation_disable = bool(rotation_disable)
|
|
123
|
+
self.rotation_disable_pos_curv = bool(rotation_disable_pos_curv)
|
|
124
|
+
self.rotation_remove_trans = bool(rotation_remove_trans)
|
|
125
|
+
self.trans_force_f_perp = trans_force_f_perp
|
|
126
|
+
self.forward_hessian = forward_hessian
|
|
127
|
+
|
|
128
|
+
# Regarding generation of initial orientation
|
|
129
|
+
self.bonds = bonds
|
|
130
|
+
self.N_hessian = N_hessian
|
|
131
|
+
# Bias
|
|
132
|
+
self.bias_rotation = bool(bias_rotation)
|
|
133
|
+
self.bias_rotation_a = 0.0
|
|
134
|
+
self.bias_translation = bool(bias_translation)
|
|
135
|
+
self.bias_gaussian_dot = float(bias_gaussian_dot)
|
|
136
|
+
|
|
137
|
+
self.write_orientations = write_orientations
|
|
138
|
+
|
|
139
|
+
restrict_steps = {
|
|
140
|
+
"direct": get_scale_max(self.rotation_max_element),
|
|
141
|
+
"fourier": None,
|
|
142
|
+
}
|
|
143
|
+
self.restrict_step = restrict_steps[rotation_method]
|
|
144
|
+
|
|
145
|
+
self._N = None
|
|
146
|
+
self._coords0 = None
|
|
147
|
+
self._energy0 = None
|
|
148
|
+
self._f0 = None
|
|
149
|
+
self._f1 = None
|
|
150
|
+
|
|
151
|
+
self.force_evals = 0
|
|
152
|
+
self.gaussians = list()
|
|
153
|
+
|
|
154
|
+
# Set dimer direction if given
|
|
155
|
+
self.N_raw = N_raw
|
|
156
|
+
if self.N_raw is not None:
|
|
157
|
+
msg = "Setting initial orientation from given 'N_raw'"
|
|
158
|
+
if isinstance(self.N_raw, str) and Path(self.N_raw).exists():
|
|
159
|
+
fn = self.N_raw
|
|
160
|
+
self.N_raw = np.loadtxt(fn)
|
|
161
|
+
msg = f"Read initial orientation from file '{fn}'"
|
|
162
|
+
N_raw = self.N_raw.copy()
|
|
163
|
+
self.N = N_raw
|
|
164
|
+
self.log(msg)
|
|
165
|
+
self.N_init = None
|
|
166
|
+
|
|
167
|
+
if seed is None:
|
|
168
|
+
# 2**32 - 1
|
|
169
|
+
seed = np.random.randint(4294967295)
|
|
170
|
+
np.random.seed(seed)
|
|
171
|
+
msg = f"Using seed {seed} to initialize the random number generator.\n"
|
|
172
|
+
print(msg)
|
|
173
|
+
self.log(msg)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def N(self):
|
|
177
|
+
return self._N
|
|
178
|
+
|
|
179
|
+
@N.setter
|
|
180
|
+
def N(self, N_new):
|
|
181
|
+
N_new = np.array(N_new, dtype=float).flatten()
|
|
182
|
+
if self.rotation_remove_trans:
|
|
183
|
+
N_new = self.remove_translation(N_new)
|
|
184
|
+
N_new /= np.linalg.norm(N_new)
|
|
185
|
+
self._N = N_new
|
|
186
|
+
self._f1 = None
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def coords0(self):
|
|
190
|
+
return self._coords0
|
|
191
|
+
|
|
192
|
+
@coords0.setter
|
|
193
|
+
def coords0(self, coords0_new):
|
|
194
|
+
self._coords0 = coords0_new
|
|
195
|
+
self._energy0 = None
|
|
196
|
+
self._f0 = None
|
|
197
|
+
self._f1 = None
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def coords1(self):
|
|
201
|
+
return self.coords0 + self.length * self.N
|
|
202
|
+
|
|
203
|
+
@coords1.setter
|
|
204
|
+
def coords1(self, coords1_new):
|
|
205
|
+
N_new = coords1_new - self.coords0
|
|
206
|
+
self.N = N_new
|
|
207
|
+
self._f1 = None
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def energy0(self):
|
|
211
|
+
if self._energy0 is None:
|
|
212
|
+
results = self.calculator.get_energy(self.atoms, self.coords0)["energy"]
|
|
213
|
+
self._energy0 = results["energy"]
|
|
214
|
+
return self._energy0
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def f0(self):
|
|
218
|
+
if self._f0 is None:
|
|
219
|
+
results = self.calculator.get_forces(self.atoms, self.coords0)
|
|
220
|
+
self.force_evals += 1
|
|
221
|
+
self._f0 = results["forces"]
|
|
222
|
+
self._energy0 = results["energy"]
|
|
223
|
+
return self._f0
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def f1(self):
|
|
227
|
+
if self._f1 is None:
|
|
228
|
+
results = self.calculator.get_forces(self.atoms, self.coords1)
|
|
229
|
+
self.force_evals += 1
|
|
230
|
+
self._f1 = results["forces"]
|
|
231
|
+
return self._f1
|
|
232
|
+
|
|
233
|
+
@f1.setter
|
|
234
|
+
def f1(self, f1_new):
|
|
235
|
+
self._f1 = f1_new
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def f2(self):
|
|
239
|
+
"""Never calculated explicitly, but estimated from f0 and f1."""
|
|
240
|
+
return 2 * self.f0 - self.f1
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def can_bias_f1(self):
|
|
244
|
+
return (
|
|
245
|
+
self.bias_rotation
|
|
246
|
+
and (self.N_init is not None)
|
|
247
|
+
and (self.bias_rotation_a is not None)
|
|
248
|
+
and (self.bias_rotation_a > 0.0)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def should_bias_f1(self):
|
|
253
|
+
"""May lead to calculation of f0 and/or f1 if present!"""
|
|
254
|
+
return self.can_bias_f1 and self.C > 0.0
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def can_bias_f0(self):
|
|
258
|
+
return self.bias_translation and (self.N is not None)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def should_bias_f0(self):
|
|
262
|
+
"""May lead to calculation of f0 and/or f1 if present!"""
|
|
263
|
+
return self.can_bias_f0 and self.C > 0.0
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def f1_bias(self):
|
|
267
|
+
# Apply bias force to f1 if desired. Dont apply bias if N_init is not (yet)
|
|
268
|
+
# set. When N_raw was converged to a reasonable N_init we can add
|
|
269
|
+
# the bias.
|
|
270
|
+
assert self.bias_rotation_a >= 0.0, "This should not be negative!"
|
|
271
|
+
|
|
272
|
+
fN = self.bias_rotation_a * self.length * self.N.dot(self.N_init) * self.N_init
|
|
273
|
+
|
|
274
|
+
return fN
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def rot_force(self):
|
|
278
|
+
f1_perp = perp_comp(self.f1, self.N)
|
|
279
|
+
f2_perp = perp_comp(self.f2, self.N)
|
|
280
|
+
f_perp = f1_perp - f2_perp
|
|
281
|
+
|
|
282
|
+
# Don't bias rotational force if curvature is already negative
|
|
283
|
+
if self.should_bias_f1:
|
|
284
|
+
f1_bias_perp = perp_comp(self.f1_bias, self.N)
|
|
285
|
+
f_perp += f1_bias_perp
|
|
286
|
+
|
|
287
|
+
return f_perp
|
|
288
|
+
|
|
289
|
+
def curvature(self, f1, f2, N):
|
|
290
|
+
"""Curvature of the mode represented by the dimer."""
|
|
291
|
+
return (f2 - f1).dot(N) / (2 * self.length)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def C(self):
|
|
295
|
+
"""Shortcut for the curvature."""
|
|
296
|
+
return self.curvature(self.f1, self.f2, self.N)
|
|
297
|
+
|
|
298
|
+
def get_gaussian_energies(self, coords, sum_=True):
|
|
299
|
+
energies = [gauss.energy(coords) for gauss in self.gaussians]
|
|
300
|
+
if sum_:
|
|
301
|
+
energies = sum(energies)
|
|
302
|
+
return energies
|
|
303
|
+
|
|
304
|
+
def get_gaussian_forces(self, coords, sum_=True):
|
|
305
|
+
forces = [gauss.forces(coords) for gauss in self.gaussians]
|
|
306
|
+
if sum_:
|
|
307
|
+
forces = np.sum(forces, axis=0)
|
|
308
|
+
return forces
|
|
309
|
+
|
|
310
|
+
def add_gaussian(
|
|
311
|
+
self, atoms, center, N, height=0.1, std=0.0529, max_cycles=50, dot_ref=None
|
|
312
|
+
):
|
|
313
|
+
# Create new gaussian object with default height that will be
|
|
314
|
+
# refined later.
|
|
315
|
+
gaussian = Gaussian(height=height, center=center, std=std, N=N)
|
|
316
|
+
|
|
317
|
+
if dot_ref is None:
|
|
318
|
+
dot_ref = self.bias_gaussian_dot
|
|
319
|
+
|
|
320
|
+
# Calculate real forces at inflection point of new gaussian
|
|
321
|
+
infl_coords = center + gaussian.std * N
|
|
322
|
+
infl_results = self.calculator.get_forces(atoms, infl_coords)
|
|
323
|
+
self.force_evals += 1
|
|
324
|
+
infl_forces = infl_results["forces"]
|
|
325
|
+
|
|
326
|
+
assert (
|
|
327
|
+
infl_forces.dot(N) < 0
|
|
328
|
+
), "We probably overstepped the TS. See Section 2.3 in the paper."
|
|
329
|
+
|
|
330
|
+
forces = self.get_gaussian_forces(infl_coords) + infl_forces
|
|
331
|
+
|
|
332
|
+
def get_dot(height):
|
|
333
|
+
"""Dot product of forces for a given height and orientation N."""
|
|
334
|
+
tmp_forces = forces.copy()
|
|
335
|
+
tmp_forces += gaussian.forces(infl_coords, height=height)
|
|
336
|
+
dot = tmp_forces.dot(N)
|
|
337
|
+
return dot
|
|
338
|
+
|
|
339
|
+
def can_break(dot):
|
|
340
|
+
"""Convergence indicator."""
|
|
341
|
+
return abs(dot - dot_ref) <= 1e-3
|
|
342
|
+
|
|
343
|
+
def bisect(
|
|
344
|
+
min_,
|
|
345
|
+
max_,
|
|
346
|
+
):
|
|
347
|
+
for i in range(max_cycles):
|
|
348
|
+
if abs(min_ - max_) <= 1e-10:
|
|
349
|
+
raise Exception("min_ and max_ became too similar!")
|
|
350
|
+
|
|
351
|
+
# Determie value at half of the internval
|
|
352
|
+
height = min_ + (max_ - min_) / 2
|
|
353
|
+
dot = get_dot(height)
|
|
354
|
+
|
|
355
|
+
if can_break(dot):
|
|
356
|
+
break
|
|
357
|
+
|
|
358
|
+
if dot > dot_ref:
|
|
359
|
+
max_ = height
|
|
360
|
+
elif dot < dot_ref:
|
|
361
|
+
min_ = height
|
|
362
|
+
return height
|
|
363
|
+
|
|
364
|
+
# Determine appropriate height
|
|
365
|
+
grow = 2
|
|
366
|
+
min_height = 0
|
|
367
|
+
assert get_dot(0) < dot_ref
|
|
368
|
+
for i in range(max_cycles):
|
|
369
|
+
dot = get_dot(height)
|
|
370
|
+
|
|
371
|
+
if can_break(dot):
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
if 0 < dot < dot_ref:
|
|
375
|
+
min_height = height
|
|
376
|
+
height *= grow
|
|
377
|
+
elif dot < dot_ref:
|
|
378
|
+
height *= grow
|
|
379
|
+
else:
|
|
380
|
+
height = bisect(min_height, height)
|
|
381
|
+
break
|
|
382
|
+
dot = get_dot(height)
|
|
383
|
+
gaussian.height = height
|
|
384
|
+
self.gaussians.append(gaussian)
|
|
385
|
+
|
|
386
|
+
self.log(f"Added gaussian with height={height:.6f}")
|
|
387
|
+
self.log(f"There are now {len(self.gaussians)} gaussians.")
|
|
388
|
+
|
|
389
|
+
return gaussian
|
|
390
|
+
|
|
391
|
+
def get_N_raw_from_hessian(self, h5_fn, root=0):
|
|
392
|
+
with h5py.File(h5_fn, "r") as handle:
|
|
393
|
+
hessian = handle["hessian"][:]
|
|
394
|
+
w, v = np.linalg.eigh(hessian)
|
|
395
|
+
assert w[root] < -1e-3
|
|
396
|
+
N_raw = v[:, root]
|
|
397
|
+
return N_raw
|
|
398
|
+
|
|
399
|
+
def set_N_raw(self, coords):
|
|
400
|
+
self.log("No initial orientation given. Generating one.")
|
|
401
|
+
if self.bonds is not None:
|
|
402
|
+
N_raw = get_weighted_bond_mode(self.bonds, coords.reshape(-1, 3))
|
|
403
|
+
msg = "weighted bond mode"
|
|
404
|
+
elif self.N_hessian is not None:
|
|
405
|
+
N_raw = self.get_N_raw_from_hessian(self.N_hessian)
|
|
406
|
+
msg = "first imaginary mode of HDF5 Hessian"
|
|
407
|
+
else:
|
|
408
|
+
msg = "random guess"
|
|
409
|
+
N_raw = np.random.rand(coords.size)
|
|
410
|
+
self.log(f"Obtained initial orientation from {msg}.")
|
|
411
|
+
# Make N_raw translationally invariant and normalize
|
|
412
|
+
self.N = N_raw
|
|
413
|
+
# Now we keep the normalized dimer orientation
|
|
414
|
+
self.N_raw = self.N
|
|
415
|
+
|
|
416
|
+
def remove_translation(self, displacement):
|
|
417
|
+
# Average vector components over cartesian directions (x,y,z)
|
|
418
|
+
# Sum over each direction should equal zero if translationally invariant
|
|
419
|
+
average = displacement.reshape(-1, 3).mean(axis=0)
|
|
420
|
+
|
|
421
|
+
if max(abs(average)) > 1e-8:
|
|
422
|
+
self.log(
|
|
423
|
+
f"N-vector not translationally invariant. Removing average before normalization."
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
return displacement
|
|
427
|
+
# Subtract the average component along each direction to make sum zero
|
|
428
|
+
invariant_displacement = (
|
|
429
|
+
displacement.reshape(-1, 3) - average[None, :]
|
|
430
|
+
).flatten()
|
|
431
|
+
return invariant_displacement
|
|
432
|
+
|
|
433
|
+
def rotate_coords1(self, rad, theta):
|
|
434
|
+
"""Rotate dimer and produce new coords1."""
|
|
435
|
+
return self.coords0 + (self.N * np.cos(rad) + theta * np.sin(rad)) * self.length
|
|
436
|
+
|
|
437
|
+
def direct_rotation(self, optimizer, prev_step):
|
|
438
|
+
rot_step = optimizer(self.rot_force, prev_step)
|
|
439
|
+
rot_step = self.restrict_step(rot_step)
|
|
440
|
+
# Strictly speaking rot_step should be constrained to conserve the desired
|
|
441
|
+
# dimer length (coords1 - coords0)*2. This step is unconstrained.
|
|
442
|
+
# Later on we calculate the actual step between the old coords1 and the new
|
|
443
|
+
# coords1 that have been reconstrained.
|
|
444
|
+
self.coords1 = self.coords1 + rot_step
|
|
445
|
+
|
|
446
|
+
def fourier_rotation(self, optimizer, prev_step):
|
|
447
|
+
theta_dir = optimizer(self.rot_force, prev_step)
|
|
448
|
+
# Remove component that is parallel to N
|
|
449
|
+
theta_dir = theta_dir - theta_dir.dot(self.N) * self.N
|
|
450
|
+
theta = theta_dir / np.linalg.norm(theta_dir)
|
|
451
|
+
|
|
452
|
+
# Get rotated endpoint geometries. The rotation takes place in a plane
|
|
453
|
+
# spanned by N and theta. Theta is a unit vector perpendicular to N that
|
|
454
|
+
# can be formed from the perpendicular components of the forces at the
|
|
455
|
+
# endpoints.
|
|
456
|
+
|
|
457
|
+
C = self.C
|
|
458
|
+
# Derivative of the curvature, Eq. (29) in [2]
|
|
459
|
+
# (f2 - f1) or -(f1 - f2)
|
|
460
|
+
dC = 2 * (self.f0 - self.f1).dot(theta) / self.length
|
|
461
|
+
rad_trial = -0.5 * np.arctan2(dC, 2 * abs(C))
|
|
462
|
+
# self.log(f"rad_trial={rad_trial:.2f}")
|
|
463
|
+
if np.abs(rad_trial) < self.rotation_tol:
|
|
464
|
+
self.log(f"rad_trial={rad_trial:.2f} below threshold. Breaking.")
|
|
465
|
+
raise RotationConverged
|
|
466
|
+
|
|
467
|
+
# Trial rotation for finite difference calculation of rotational force
|
|
468
|
+
# and rotational curvature.
|
|
469
|
+
coords1_trial = self.rotate_coords1(rad_trial, theta)
|
|
470
|
+
f1_trial = self.calculator.get_forces(self.atoms, coords1_trial)["forces"]
|
|
471
|
+
self.force_evals += 1
|
|
472
|
+
f2_trial = 2 * self.f0 - f1_trial
|
|
473
|
+
N_trial = make_unit_vec(coords1_trial, self.coords0)
|
|
474
|
+
C_trial = self.curvature(f1_trial, f2_trial, N_trial)
|
|
475
|
+
|
|
476
|
+
b1 = 0.5 * dC
|
|
477
|
+
a1 = (C - C_trial + b1 * np.sin(2 * rad_trial)) / (1 - np.cos(2 * rad_trial))
|
|
478
|
+
a0 = 2 * (C - a1)
|
|
479
|
+
|
|
480
|
+
rad_min = 0.5 * np.arctan(b1 / a1)
|
|
481
|
+
|
|
482
|
+
# self.log(f"rad_min={rad_min:.2f}")
|
|
483
|
+
def get_C(theta_rad):
|
|
484
|
+
return a0 / 2 + a1 * np.cos(2 * theta_rad) + b1 * np.sin(2 * theta_rad)
|
|
485
|
+
|
|
486
|
+
C_min = get_C(rad_min) # lgtm [py/multiple-definition]
|
|
487
|
+
if C_min > C:
|
|
488
|
+
rad_min += np.deg2rad(90)
|
|
489
|
+
# C_min_new = get_C(rad_min)
|
|
490
|
+
# self.log("Predicted theta_min lead us to a curvature maximum "
|
|
491
|
+
# f"(C(theta)={C_min:.6f}). Adding pi/2 to theta_min. "
|
|
492
|
+
# f"(C(theta+pi/2)={C_min_new:.6f})"
|
|
493
|
+
# )
|
|
494
|
+
|
|
495
|
+
# TODO: handle cases where the curvature is still positive, but
|
|
496
|
+
# the angle is small, so the rotation is skipped.
|
|
497
|
+
# Don't do rotation for small angles
|
|
498
|
+
if np.abs(rad_min) < self.rotation_tol:
|
|
499
|
+
# self.log(f"rad_min={rad_min:.2f} below threshold. Breaking.")
|
|
500
|
+
raise RotationConverged
|
|
501
|
+
|
|
502
|
+
f1 = None
|
|
503
|
+
# Interpolate force at coords1_rot; see Eq. (12) in [4]
|
|
504
|
+
if self.rotation_interpolate:
|
|
505
|
+
f1 = (
|
|
506
|
+
np.sin(rad_trial - rad_min) / np.sin(rad_trial) * self.f1
|
|
507
|
+
+ np.sin(rad_min) / np.sin(rad_trial) * f1_trial
|
|
508
|
+
+ (1 - np.cos(rad_min) - np.sin(rad_min) * np.tan(rad_trial / 2))
|
|
509
|
+
* self.f0
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
self.coords1 = self.rotate_coords1(rad_min, theta)
|
|
513
|
+
self.f1 = f1
|
|
514
|
+
|
|
515
|
+
def do_dimer_rotations(self, rotation_thresh=None):
|
|
516
|
+
self.log("Doing dimer rotations")
|
|
517
|
+
if rotation_thresh is None:
|
|
518
|
+
rotation_thresh = self.rotation_thresh
|
|
519
|
+
self.log(f"\tThreshold norm(rot_force)={rotation_thresh:.6f}")
|
|
520
|
+
|
|
521
|
+
lbfgs = small_lbfgs_closure(gamma_mult=True)
|
|
522
|
+
try:
|
|
523
|
+
N_first = self.N
|
|
524
|
+
prev_step = None
|
|
525
|
+
for i in range(self.rotation_max_cycles): # lgtm [py/redundant-else]
|
|
526
|
+
N_cur = self.N
|
|
527
|
+
rot_force = self.rot_force
|
|
528
|
+
rms_rot_force = rms(rot_force)
|
|
529
|
+
if self.should_bias_f1:
|
|
530
|
+
C_real = self.C
|
|
531
|
+
C_bias = -self.bias_rotation_a * (self.N.dot(self.N_init)) ** 2
|
|
532
|
+
C = C_real + C_bias
|
|
533
|
+
C_str = f"C={C: .6f}, C_real={C_real: .6f}, C_bias={C_bias: .6f}"
|
|
534
|
+
else:
|
|
535
|
+
C_str = f"C={self.C: .6f}"
|
|
536
|
+
self.log(f"\t{i:02d}: rms(rot_force)={rms_rot_force:.6f} {C_str}")
|
|
537
|
+
if rms_rot_force <= rotation_thresh:
|
|
538
|
+
self.log("\trms(rot_force) is below threshold!")
|
|
539
|
+
raise RotationConverged
|
|
540
|
+
coords1_old = self.coords1
|
|
541
|
+
self.rotation_method(lbfgs, prev_step)
|
|
542
|
+
actual_step = self.coords1 - coords1_old
|
|
543
|
+
prev_step = actual_step
|
|
544
|
+
rot_deg = np.rad2deg(np.arccos(N_cur.dot(self.N)))
|
|
545
|
+
self.log(f"\t\tRotated by {rot_deg:.1f}°")
|
|
546
|
+
else:
|
|
547
|
+
msg = (
|
|
548
|
+
"\tDimer rotation did not converge in "
|
|
549
|
+
f"{self.rotation_max_cycles}"
|
|
550
|
+
)
|
|
551
|
+
except RotationConverged:
|
|
552
|
+
msg = f"\tDimer rotation converged in {i+1} cycle(s)."
|
|
553
|
+
self.log(msg)
|
|
554
|
+
# self.log("\tN after rotation:\n\t" + str(self.N))
|
|
555
|
+
self.log()
|
|
556
|
+
# Restrict to interval [-1,1] where arccos is defined
|
|
557
|
+
rot_deg = np.rad2deg(np.arccos(max(min(N_first.dot(self.N), 1.0), -1.0)))
|
|
558
|
+
self.log(
|
|
559
|
+
f"\tRotated by {rot_deg:.1f}° w.r.t. the orientation " "before rotation(s)."
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def update_orientation(self, coords):
|
|
563
|
+
# Generate random guess for the dimer orientation if not yet set
|
|
564
|
+
if self.N is None:
|
|
565
|
+
self.set_N_raw(coords)
|
|
566
|
+
|
|
567
|
+
# Refine N_raw to N_init if not yet done
|
|
568
|
+
if self.bias_rotation and self.N_init is None and self.C > 0.0:
|
|
569
|
+
# Run initial sweep with a much softer convergence threshold
|
|
570
|
+
self.log("Initial sweep to refine N_raw to N_init.")
|
|
571
|
+
self.do_dimer_rotations(10 * self.rotation_thresh)
|
|
572
|
+
if self.C > 0:
|
|
573
|
+
self.N_init = self.N
|
|
574
|
+
rot_rad = np.arccos(self.N_raw.dot(self.N_init))
|
|
575
|
+
rot_deg = np.rad2deg(rot_rad)
|
|
576
|
+
self.log(f"N_raw:\n\t{self.N_raw}")
|
|
577
|
+
self.log(f"Rotated N_raw by {rot_deg:.1f}° to N_init")
|
|
578
|
+
self.log(f"N_init:\n\t{self.N_init}")
|
|
579
|
+
C = self.C
|
|
580
|
+
self.log(f"Curvature after intial sweep is C={C:.6f}")
|
|
581
|
+
self.log("Determining proper scaling factor for bias potential.")
|
|
582
|
+
# Determine proper scaling factor for the quadratic bias potential
|
|
583
|
+
# from the current curvature.
|
|
584
|
+
scale_fact = 1.5
|
|
585
|
+
self.bias_rotation_a = scale_fact * self.C
|
|
586
|
+
self.log(f"Using a={scale_fact}*C={self.bias_rotation_a:.6f}")
|
|
587
|
+
else:
|
|
588
|
+
self.log(
|
|
589
|
+
f"Curvature after initial sweep C={self.C:.6f} is "
|
|
590
|
+
"already negative. Not setting N_init and bias_rotation_a!"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
self.do_dimer_rotations()
|
|
594
|
+
|
|
595
|
+
def get_forces(self, atoms, coords):
|
|
596
|
+
self.atoms = atoms
|
|
597
|
+
self.coords0 = coords
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
N_backup = self.N.copy()
|
|
601
|
+
except AttributeError:
|
|
602
|
+
N_backup = None
|
|
603
|
+
if not self.rotation_disable:
|
|
604
|
+
self.update_orientation(coords)
|
|
605
|
+
if (N_backup is not None) and self.rotation_disable_pos_curv and self.C > 0:
|
|
606
|
+
self.log(
|
|
607
|
+
"Rotation did not yield a negative curvature. "
|
|
608
|
+
"Restoring previous unrotated N."
|
|
609
|
+
)
|
|
610
|
+
self.N = N_backup
|
|
611
|
+
# Now we (have an updated self.N and) can do the force projections
|
|
612
|
+
N = self.N
|
|
613
|
+
# self.log(f"Orientation N:\n\t{N}")
|
|
614
|
+
# Save orientation N in human-readable format, aka _trj.xyz
|
|
615
|
+
# file in Angstrom.
|
|
616
|
+
if self.write_orientations:
|
|
617
|
+
trj_str = get_tangent_trj_str(atoms, coords, N)
|
|
618
|
+
trj_fn = self.make_fn("N_trj.xyz")
|
|
619
|
+
with open(trj_fn, "w") as handle:
|
|
620
|
+
handle.write(trj_str)
|
|
621
|
+
self.log(f"Wrote current orientation animation to '{trj_fn}'")
|
|
622
|
+
# Always save orientation in Bohr
|
|
623
|
+
N_fn = self.make_fn("N")
|
|
624
|
+
np.savetxt(N_fn, N)
|
|
625
|
+
|
|
626
|
+
energy = self.energy0
|
|
627
|
+
self.log(f"\tenergy={self.energy0:.8f} au")
|
|
628
|
+
|
|
629
|
+
f0 = self.f0
|
|
630
|
+
|
|
631
|
+
if self.should_bias_f0:
|
|
632
|
+
self.log("Biasing translation forces")
|
|
633
|
+
self.log(f"There are currently {len(self.gaussians)} gaussians present.")
|
|
634
|
+
bias_energy = self.get_gaussian_energies(coords)
|
|
635
|
+
energy += bias_energy
|
|
636
|
+
bias_forces = self.get_gaussian_forces(coords, sum_=False)
|
|
637
|
+
try:
|
|
638
|
+
bias_norms = np.linalg.norm(bias_forces, axis=1)
|
|
639
|
+
bias_norms_str = np.array2string(bias_norms, precision=4)
|
|
640
|
+
self.log(f"\tnorm(bias_forces)={bias_norms_str}")
|
|
641
|
+
except np.AxisError:
|
|
642
|
+
self.log("Skipping calculation of norm(bias_forces)")
|
|
643
|
+
f0 += np.sum(bias_forces, axis=0)
|
|
644
|
+
|
|
645
|
+
norm_f0 = np.linalg.norm(f0)
|
|
646
|
+
self.log(f"\tnorm(forces)={norm_f0:.6f}")
|
|
647
|
+
|
|
648
|
+
f_parallel = f0.dot(N) * N
|
|
649
|
+
norm_parallel = np.linalg.norm(f_parallel)
|
|
650
|
+
self.log(f"\tnorm(forces_parallel)={norm_parallel:.6f}")
|
|
651
|
+
# self.log(f"\tforce_parallel:\n\t{f_parallel}")
|
|
652
|
+
|
|
653
|
+
f_perp = f0 - f_parallel
|
|
654
|
+
norm_perp = np.linalg.norm(f_perp)
|
|
655
|
+
self.log(f"\tnorm(forces_perp)={norm_perp:.6f}")
|
|
656
|
+
# self.log(f"\tforce_perp:\n\t{f_perp}")
|
|
657
|
+
|
|
658
|
+
# Only return perpendicular component when curvature is negative
|
|
659
|
+
f_tran = -f_parallel
|
|
660
|
+
|
|
661
|
+
curv_str = "positive" if self.C > 0 else "negative"
|
|
662
|
+
|
|
663
|
+
force_str = "reversed parallel component of"
|
|
664
|
+
if (self.C < 0) or self.trans_force_f_perp:
|
|
665
|
+
f_tran += f_perp
|
|
666
|
+
force_str = "full"
|
|
667
|
+
self.log(f"Curvature is {curv_str}. Returning {force_str} f_tran.")
|
|
668
|
+
# self.log(f"\tf_tran:\n\t{f_tran}")
|
|
669
|
+
self.log()
|
|
670
|
+
|
|
671
|
+
self.calc_counter += 1
|
|
672
|
+
|
|
673
|
+
results = {"energy": energy, "forces": f_tran}
|
|
674
|
+
|
|
675
|
+
return results
|
|
676
|
+
|
|
677
|
+
def get_hessian(self, atoms, coords):
|
|
678
|
+
if not self.forward_hessian:
|
|
679
|
+
raise Exception("Actual Hessian method not forwarded by Dimer calculator!")
|
|
680
|
+
results = self.calculator.get_hessian(atoms, coords)
|
|
681
|
+
return results
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pysisyphus.calculators.Calculator import Calculator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Dummy(Calculator):
|
|
5
|
+
def raise_exception(self):
|
|
6
|
+
raise Exception(
|
|
7
|
+
"Dummy calculator does not implement any calculation capabilities."
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
def run_calculation(self, *args, **kwargs):
|
|
11
|
+
self.raise_exception()
|
|
12
|
+
|
|
13
|
+
def get_energy(self, *args, **kwargs):
|
|
14
|
+
self.raise_exception()
|
|
15
|
+
|
|
16
|
+
def get_forces(self, *args, **kwargs):
|
|
17
|
+
self.raise_exception()
|
|
18
|
+
|
|
19
|
+
def get_hessian(self, *args, **kwargs):
|
|
20
|
+
self.raise_exception()
|