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,855 @@
|
|
|
1
|
+
# [1] https://doi.org/10.1002/jcc.23481
|
|
2
|
+
# Exploring transition state structures for intramolecular pathways
|
|
3
|
+
# by the artificial force induced reaction method
|
|
4
|
+
# Maeda, Morokuma et al, 2013
|
|
5
|
+
# [2] https://pubs.acs.org/doi/10.1021/ct200290m
|
|
6
|
+
# Finding Reaction Pathways of Type A + B → X: Toward Systematic
|
|
7
|
+
# Prediction of Reaction Mechanisms
|
|
8
|
+
# Maeda, Morokuma, 2011
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import itertools as it
|
|
12
|
+
import logging
|
|
13
|
+
from functools import reduce
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from pprint import pformat
|
|
17
|
+
import shutil
|
|
18
|
+
import traceback
|
|
19
|
+
from typing import Callable, Dict, List, Tuple, Optional
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import NDArray
|
|
23
|
+
from rmsd import kabsch_rmsd
|
|
24
|
+
from scipy.spatial.distance import pdist
|
|
25
|
+
from scipy.optimize import least_squares
|
|
26
|
+
|
|
27
|
+
from pysisyphus import logger as pysis_logger
|
|
28
|
+
from pysisyphus.calculators.AFIR import AFIR, CovRadiiSumZero
|
|
29
|
+
from pysisyphus.calculators import HardSphere
|
|
30
|
+
from pysisyphus.config import AFIR_RMSD_THRESH, OUT_DIR_DEFAULT
|
|
31
|
+
from pysisyphus.constants import BOHR2ANG
|
|
32
|
+
from pysisyphus.cos.NEB import NEB
|
|
33
|
+
from pysisyphus.drivers.opt import run_opt
|
|
34
|
+
from pysisyphus.elem_data import COVALENT_RADII as CR
|
|
35
|
+
from pysisyphus.Geometry import Geometry
|
|
36
|
+
from pysisyphus.helpers import pick_image_inds, check_for_end_sign
|
|
37
|
+
from pysisyphus.helpers_pure import to_sets
|
|
38
|
+
from pysisyphus.intcoords.helpers import get_bond_difference
|
|
39
|
+
from pysisyphus.intcoords.setup import get_pair_covalent_radii
|
|
40
|
+
from pysisyphus.intcoords.setup_fast import find_bonds
|
|
41
|
+
from pysisyphus.optimizers.FIRE import FIRE
|
|
42
|
+
from pysisyphus.xyzloader import make_xyz_str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
logger = pysis_logger.getChild("afir")
|
|
46
|
+
logger.setLevel(logging.DEBUG)
|
|
47
|
+
file_handler = logging.FileHandler("afir.log", mode="w", delay=True)
|
|
48
|
+
logger.addHandler(file_handler)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
AFIR_BOND_FACTOR = 1.2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class AFIRPath:
|
|
56
|
+
atoms: tuple
|
|
57
|
+
cart_coords: np.ndarray
|
|
58
|
+
energies: np.ndarray
|
|
59
|
+
forces: np.ndarray
|
|
60
|
+
charge: int
|
|
61
|
+
mult: int
|
|
62
|
+
opt_is_converged: Optional[bool] = None
|
|
63
|
+
gamma: Optional[float] = None
|
|
64
|
+
path_indices: Optional[List[int]] = None
|
|
65
|
+
|
|
66
|
+
def compatible(self, other):
|
|
67
|
+
check = ("atoms", "charge", "mult")
|
|
68
|
+
return all([getattr(self, name) == getattr(other, name) for name in check])
|
|
69
|
+
|
|
70
|
+
def __add__(self, other):
|
|
71
|
+
"""This assumes that the first item in other.cart_coords is the same as the
|
|
72
|
+
last item of self.cart_coords."""
|
|
73
|
+
assert self.compatible(other)
|
|
74
|
+
|
|
75
|
+
def conc(name):
|
|
76
|
+
return np.concatenate(
|
|
77
|
+
(getattr(self, name), getattr(other, name)[1:]), axis=0
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
cart_coords = conc("cart_coords")
|
|
81
|
+
energies = conc("energies")
|
|
82
|
+
forces = conc("forces")
|
|
83
|
+
|
|
84
|
+
if self.path_indices is None:
|
|
85
|
+
path_indices = [0] * len(self.cart_coords)
|
|
86
|
+
else:
|
|
87
|
+
path_indices = self.path_indices
|
|
88
|
+
path_indices += [path_indices[-1] + 1] * (len(other.cart_coords) - 1)
|
|
89
|
+
|
|
90
|
+
return AFIRPath(
|
|
91
|
+
atoms=self.atoms,
|
|
92
|
+
cart_coords=cart_coords,
|
|
93
|
+
energies=energies,
|
|
94
|
+
forces=forces,
|
|
95
|
+
charge=self.charge,
|
|
96
|
+
mult=self.mult,
|
|
97
|
+
path_indices=path_indices,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def dump_trj(self, fn):
|
|
101
|
+
geom = Geometry(self.atoms, self.cart_coords[0])
|
|
102
|
+
xyzs = [geom.as_xyz(cart_coords=cc) for cc in self.cart_coords]
|
|
103
|
+
with open(fn, "w") as handle:
|
|
104
|
+
handle.write("\n".join(xyzs))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def analyze_afir_path(energies):
|
|
108
|
+
energies = np.array(energies)
|
|
109
|
+
max_ind = energies.argmax()
|
|
110
|
+
|
|
111
|
+
local_minima = list()
|
|
112
|
+
local_maxima = list()
|
|
113
|
+
stationary_points = list()
|
|
114
|
+
for i, en in enumerate(energies[1:-1], 1):
|
|
115
|
+
prev_en = energies[i - 1]
|
|
116
|
+
next_en = energies[i + 1]
|
|
117
|
+
if is_minimum := prev_en > en < next_en:
|
|
118
|
+
local_minima.append(i)
|
|
119
|
+
if is_ts := prev_en < en > next_en:
|
|
120
|
+
local_maxima.append(i)
|
|
121
|
+
if is_minimum or is_ts:
|
|
122
|
+
stationary_points.append(i)
|
|
123
|
+
|
|
124
|
+
if max_ind in stationary_points:
|
|
125
|
+
ts_ind = np.where(stationary_points == max_ind)[0][0]
|
|
126
|
+
prev_min_ind = stationary_points[ts_ind - 1]
|
|
127
|
+
next_min_ind = stationary_points[ts_ind + 1]
|
|
128
|
+
sp_inds = [prev_min_ind, max_ind, next_min_ind]
|
|
129
|
+
else:
|
|
130
|
+
sp_inds = list()
|
|
131
|
+
return sp_inds
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
##########################
|
|
135
|
+
# Multi-component AFIR #
|
|
136
|
+
# Helper #
|
|
137
|
+
##########################
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def generate_random_union(geoms, offset=2.0, rng=None):
|
|
141
|
+
"""Unite fragments into one Geometry with random fragment orientations.
|
|
142
|
+
|
|
143
|
+
Center, rotate and translate from origin acoording to approximate radius
|
|
144
|
+
and an offset.
|
|
145
|
+
Displace along +x, -x, +y, -y, +z, -z.
|
|
146
|
+
|
|
147
|
+
Results for > 3 fragments don't look so pretty ;).
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
axis_inds = (0, 0, 1, 1, 2, 2)
|
|
151
|
+
axis_inds_num = len(axis_inds)
|
|
152
|
+
axis_translations = np.zeros(axis_inds_num)
|
|
153
|
+
randomized = list()
|
|
154
|
+
for i, geom in enumerate(geoms):
|
|
155
|
+
i_mod = i % axis_inds_num
|
|
156
|
+
geom = geom.copy()
|
|
157
|
+
geom.center()
|
|
158
|
+
geom.rotate(rng=rng)
|
|
159
|
+
axis = axis_inds[i_mod]
|
|
160
|
+
step_size = axis_translations[i_mod] + geom.approximate_radius() + offset
|
|
161
|
+
axis_translations[i_mod] = step_size
|
|
162
|
+
step = np.zeros(3)
|
|
163
|
+
# Alternate between negative and positive direction along x/y/z
|
|
164
|
+
step[axis] = (-1) ** i * step_size
|
|
165
|
+
geom.coords3d += step[None, :]
|
|
166
|
+
randomized.append(geom)
|
|
167
|
+
union = reduce(lambda geom1, geom2: geom1 + geom2, randomized)
|
|
168
|
+
return union
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def generate_random_union_ref(geoms, rng=None, opt_kwargs=None):
|
|
172
|
+
"""Unite fragments into one Geometry with random fragment orientations."""
|
|
173
|
+
|
|
174
|
+
geoms = [geom.copy() for geom in geoms]
|
|
175
|
+
|
|
176
|
+
if rng is None:
|
|
177
|
+
rng = np.random.default_rng()
|
|
178
|
+
|
|
179
|
+
if opt_kwargs is None:
|
|
180
|
+
opt_kwargs = {}
|
|
181
|
+
|
|
182
|
+
# Random rotations
|
|
183
|
+
for geom in geoms:
|
|
184
|
+
geom.center()
|
|
185
|
+
geom.rotate(rng=rng)
|
|
186
|
+
|
|
187
|
+
# HardSphere optimization to fix overlapping fragments
|
|
188
|
+
#
|
|
189
|
+
# Set up fragment lists.
|
|
190
|
+
fragments = list()
|
|
191
|
+
for geom in geoms:
|
|
192
|
+
geom_inds = np.arange(len(geom.atoms))
|
|
193
|
+
try:
|
|
194
|
+
geom_inds += fragments[-1][-1] + 1
|
|
195
|
+
except IndexError:
|
|
196
|
+
pass
|
|
197
|
+
fragments.append(geom_inds.tolist())
|
|
198
|
+
union = reduce(lambda geom1, geom2: geom1 + geom2, geoms)
|
|
199
|
+
calc = HardSphere(union, frags=fragments, permutations=False, kappa=1.0)
|
|
200
|
+
union.set_calculator(calc)
|
|
201
|
+
_opt_kwargs = {
|
|
202
|
+
"max_step": 0.2,
|
|
203
|
+
"max_cycles": 500,
|
|
204
|
+
}
|
|
205
|
+
_opt_kwargs.update(opt_kwargs)
|
|
206
|
+
opt = FIRE(union, **_opt_kwargs)
|
|
207
|
+
opt.run()
|
|
208
|
+
if not opt.is_converged:
|
|
209
|
+
union = None
|
|
210
|
+
else:
|
|
211
|
+
# Remove HardSphere calculator
|
|
212
|
+
union.clear()
|
|
213
|
+
del union.calculator
|
|
214
|
+
|
|
215
|
+
return union
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def prepare_mc_afir(geoms, rng=None, **kwargs):
|
|
219
|
+
"""Wrapper for generate_random_union(_ref)."""
|
|
220
|
+
union = generate_random_union(geoms, rng=rng, **kwargs)
|
|
221
|
+
|
|
222
|
+
# Set up list of fragments
|
|
223
|
+
i = 0
|
|
224
|
+
fragments = list()
|
|
225
|
+
for geom in geoms:
|
|
226
|
+
atom_num = len(geom.atoms)
|
|
227
|
+
fragments.append(np.arange(atom_num) + i)
|
|
228
|
+
i += atom_num
|
|
229
|
+
|
|
230
|
+
afir_kwargs = {
|
|
231
|
+
"fragment_indices": fragments,
|
|
232
|
+
}
|
|
233
|
+
broken_bonds = []
|
|
234
|
+
return union, afir_kwargs, broken_bonds
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
###########################
|
|
238
|
+
# Single-component AFIR #
|
|
239
|
+
# Helper #
|
|
240
|
+
###########################
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def decrease_distance(coords3d, m, n, frac=0.8):
|
|
244
|
+
c3d_m = coords3d[m]
|
|
245
|
+
c3d_n = coords3d[n]
|
|
246
|
+
dist_vec = c3d_n - c3d_m
|
|
247
|
+
step = (1 - frac) / 2 * dist_vec
|
|
248
|
+
c3d_new = coords3d.copy()
|
|
249
|
+
c3d_new[m] += step
|
|
250
|
+
c3d_new[n] -= step
|
|
251
|
+
return c3d_new
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def lstsqs_with_reference(coords3d, ref_coords3d, freeze_atoms=None):
|
|
255
|
+
"""Least-squares w.r.t. reference coordinates while keeping some
|
|
256
|
+
atoms frozen."""
|
|
257
|
+
|
|
258
|
+
if freeze_atoms is None:
|
|
259
|
+
freeze_atoms = []
|
|
260
|
+
else:
|
|
261
|
+
freeze_atoms = list(freeze_atoms)
|
|
262
|
+
ref_dists = pdist(ref_coords3d)
|
|
263
|
+
|
|
264
|
+
mask = np.ones_like(coords3d[:, 0], dtype=bool)
|
|
265
|
+
mask[freeze_atoms] = False
|
|
266
|
+
# All atoms w/o the frozen atoms
|
|
267
|
+
coords = coords3d[mask].flatten()
|
|
268
|
+
x0 = coords
|
|
269
|
+
|
|
270
|
+
coords_full = coords3d.copy()
|
|
271
|
+
|
|
272
|
+
def fun(x):
|
|
273
|
+
# Consider all distances, including distances to the fixed atoms 'm' and 'n'.
|
|
274
|
+
coords_full[mask] = x.reshape(-1, 3)
|
|
275
|
+
dists = pdist(coords_full)
|
|
276
|
+
return dists - ref_dists
|
|
277
|
+
|
|
278
|
+
res = least_squares(fun, x0)
|
|
279
|
+
opt_coords = res.x
|
|
280
|
+
coords_full[mask] = opt_coords.reshape(-1, 3)
|
|
281
|
+
return res, coords_full
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def weight_function(atoms, coords3d, i, j, p=6):
|
|
285
|
+
cr_sum = sum([CR[atoms[k].lower()] for k in (i, j)])
|
|
286
|
+
r_ij = np.linalg.norm(coords3d[i] - coords3d[j])
|
|
287
|
+
omega = (cr_sum / r_ij) ** p
|
|
288
|
+
return omega
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def find_candidates(center, bond_sets):
|
|
292
|
+
center_set = {
|
|
293
|
+
center,
|
|
294
|
+
}
|
|
295
|
+
bonded_to_center = list()
|
|
296
|
+
for bond in bond_sets:
|
|
297
|
+
if center not in bond:
|
|
298
|
+
continue
|
|
299
|
+
bonded_to_center.append(*set(bond) - center_set)
|
|
300
|
+
return bonded_to_center
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def automatic_fragmentation(
|
|
304
|
+
atoms, coords3d, frag1, frag2, cycles=2, p=6, bond_factor=1.25
|
|
305
|
+
):
|
|
306
|
+
"""Automatic fragmentation scheme as described in SC-AFIR paper [1]."""
|
|
307
|
+
|
|
308
|
+
frag1 = set(frag1)
|
|
309
|
+
frag2 = set(frag2)
|
|
310
|
+
|
|
311
|
+
def w(m, n):
|
|
312
|
+
"""Shortcut for weight function"""
|
|
313
|
+
return weight_function(atoms, coords3d, m, n, p=p)
|
|
314
|
+
|
|
315
|
+
pairs = list(it.product(frag1, frag2))
|
|
316
|
+
weights = [w(m, n) for m, n in pairs]
|
|
317
|
+
max_weight = max(weights)
|
|
318
|
+
|
|
319
|
+
bonds = find_bonds(atoms, coords3d, bond_factor=bond_factor)
|
|
320
|
+
bond_sets = [set(bond) for bond in bonds.tolist()]
|
|
321
|
+
|
|
322
|
+
def filter_candidates(candidates, partners, max_weight):
|
|
323
|
+
to_keep = set()
|
|
324
|
+
for candidate in candidates:
|
|
325
|
+
for partner in partners:
|
|
326
|
+
if candidate == partner:
|
|
327
|
+
break
|
|
328
|
+
weight = w(candidate, partner)
|
|
329
|
+
if weight > max_weight:
|
|
330
|
+
break
|
|
331
|
+
else:
|
|
332
|
+
to_keep.add(candidate)
|
|
333
|
+
return to_keep
|
|
334
|
+
|
|
335
|
+
def grow_fragment(frag1, frag2):
|
|
336
|
+
# Find candidates that are bonded to atoms in frag1. Step 2 in [1].
|
|
337
|
+
candidates = [find_candidates(m, bond_sets) for m in frag1]
|
|
338
|
+
# Filter out candidates that are already contained in frag1
|
|
339
|
+
candidates = [c for c in it.chain(*candidates) if c not in frag1]
|
|
340
|
+
# Filter out candidates with weights that are too big. Step 3/4 in [1].
|
|
341
|
+
candidates = set(filter_candidates(candidates, frag2, max_weight))
|
|
342
|
+
return candidates
|
|
343
|
+
|
|
344
|
+
for _ in range(cycles):
|
|
345
|
+
f1_candidates = grow_fragment(frag1, frag2)
|
|
346
|
+
f2_candidates = grow_fragment(frag2, frag1)
|
|
347
|
+
|
|
348
|
+
# Step 5 in [1].
|
|
349
|
+
f1_candidates = filter_candidates(f1_candidates, f2_candidates, max_weight)
|
|
350
|
+
f2_candidates = filter_candidates(f2_candidates, f1_candidates, max_weight)
|
|
351
|
+
|
|
352
|
+
# Step 6 in [1].
|
|
353
|
+
frag1.update(f1_candidates)
|
|
354
|
+
frag2.update(f2_candidates)
|
|
355
|
+
assert frag1.isdisjoint(
|
|
356
|
+
frag2
|
|
357
|
+
), "Overlapping fragments detected!" # Sanity check
|
|
358
|
+
return frag1, frag2
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def prepare_sc_afir(geom, m, n, bond_factor=AFIR_BOND_FACTOR):
|
|
362
|
+
"""Create perturbed geometry, determine fragments and set AFIR calculator."""
|
|
363
|
+
geom = geom.copy()
|
|
364
|
+
atoms = geom.atoms
|
|
365
|
+
org_coords3d = geom.coords3d.copy()
|
|
366
|
+
|
|
367
|
+
def bond_sets(coords3d):
|
|
368
|
+
bonds = find_bonds(atoms, coords3d, bond_factor=bond_factor)
|
|
369
|
+
return set([frozenset(bond) for bond in bonds.tolist()])
|
|
370
|
+
|
|
371
|
+
org_bond_sets = bond_sets(org_coords3d)
|
|
372
|
+
|
|
373
|
+
# Move target atoms closer together along distance vector (decrease distance)
|
|
374
|
+
decr_coords3d = decrease_distance(geom.coords3d, m, n)
|
|
375
|
+
# Optimize remaining coordinates using least-squares, while keeping target
|
|
376
|
+
# atom pair fixed.
|
|
377
|
+
_, opt_coords3d = lstsqs_with_reference(decr_coords3d.copy(), geom.coords3d, (m, n))
|
|
378
|
+
# Determine fragments, using the automated fragmentation
|
|
379
|
+
frag1, frag2 = automatic_fragmentation(atoms, opt_coords3d, [m], [n])
|
|
380
|
+
logger.debug(f"Fragments for target pair [{m}, {n}]: ({frag1}, {frag2})")
|
|
381
|
+
fragment_indices = [list(frag) for frag in (frag1, frag2)]
|
|
382
|
+
# Set lstsq-optimized coordinates and created wrapped calculator
|
|
383
|
+
geom.coords3d = opt_coords3d
|
|
384
|
+
|
|
385
|
+
opt_bond_sets = bond_sets(opt_coords3d)
|
|
386
|
+
broken_bonds = org_bond_sets - opt_bond_sets
|
|
387
|
+
|
|
388
|
+
afir_kwargs = {
|
|
389
|
+
"fragment_indices": fragment_indices,
|
|
390
|
+
# If 'complete_fragments' would be True, all remaining atom indices not
|
|
391
|
+
# present in 'fragment_indices' would be assigned to a third fragment.
|
|
392
|
+
"complete_fragments": False,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
def set_atoms(inds, atom_type="X", mod_atoms=None):
|
|
396
|
+
if mod_atoms is None:
|
|
397
|
+
mod_atoms = list(atoms)
|
|
398
|
+
for i in inds:
|
|
399
|
+
mod_atoms[i] = atom_type
|
|
400
|
+
return mod_atoms
|
|
401
|
+
|
|
402
|
+
atoms_target = set_atoms((m, n))
|
|
403
|
+
atoms_fragments = set_atoms(frag1)
|
|
404
|
+
atoms_fragments = set_atoms(frag2, atom_type="Q", mod_atoms=atoms_fragments)
|
|
405
|
+
|
|
406
|
+
atoms_coords3d = {
|
|
407
|
+
"original": (atoms, org_coords3d),
|
|
408
|
+
"original w/ target atoms": (atoms_target, org_coords3d),
|
|
409
|
+
"decreased distance": (atoms, decr_coords3d),
|
|
410
|
+
"decreased distance w/ target atoms": (atoms_target, decr_coords3d),
|
|
411
|
+
"lstsq optimized": (atoms, opt_coords3d),
|
|
412
|
+
"lstsq optimized w/ target atoms": (atoms_target, opt_coords3d),
|
|
413
|
+
"lstsq optimized w/ fragments": (atoms_fragments, opt_coords3d),
|
|
414
|
+
}
|
|
415
|
+
trj = "\n".join(
|
|
416
|
+
[
|
|
417
|
+
make_xyz_str(atoms, BOHR2ANG * coords3d, comment=key)
|
|
418
|
+
for key, (atoms, coords3d) in atoms_coords3d.items()
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
return geom, afir_kwargs, broken_bonds, trj
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def determine_target_pairs(
|
|
425
|
+
atoms: Tuple[str],
|
|
426
|
+
coords3d: NDArray,
|
|
427
|
+
min_: float = 1.25,
|
|
428
|
+
max_: float = 5.0,
|
|
429
|
+
active_atoms=None,
|
|
430
|
+
) -> List[Tuple[int]]:
|
|
431
|
+
"""Determine possible target m, n atom pairs for SC-AFIR calculations."""
|
|
432
|
+
if active_atoms is None:
|
|
433
|
+
active_atoms = range(len(atoms))
|
|
434
|
+
active_atoms = set(active_atoms)
|
|
435
|
+
|
|
436
|
+
pair_cov_radii = get_pair_covalent_radii(atoms)
|
|
437
|
+
pair_dists = pdist(coords3d)
|
|
438
|
+
quots = pair_dists / pair_cov_radii
|
|
439
|
+
pair_inds = it.combinations(range(len(atoms)), 2)
|
|
440
|
+
target_pairs = list()
|
|
441
|
+
for pair_ind, quot in zip(pair_inds, quots):
|
|
442
|
+
if (min_ <= quot <= max_) and (set(pair_ind) & active_atoms):
|
|
443
|
+
target_pairs.append(pair_ind)
|
|
444
|
+
return target_pairs
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def determine_target_pairs_for_geom(geom: Geometry, **kwargs) -> List[Tuple[int]]:
|
|
448
|
+
"""Determine possible target m, n atom pairs for SC-AFIR calculations
|
|
449
|
+
from geom."""
|
|
450
|
+
target_pairs = determine_target_pairs(geom.atoms, geom.coords3d, **kwargs)
|
|
451
|
+
return target_pairs
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def coordinates_similar(
|
|
455
|
+
test_coords3d: NDArray, ref_coords3d: List[NDArray], rmsd_thresh: float = 1e-2
|
|
456
|
+
) -> Tuple[bool, int]:
|
|
457
|
+
# When the reference coordinates are an empty list.
|
|
458
|
+
if len(ref_coords3d) == 0:
|
|
459
|
+
return False, -1
|
|
460
|
+
test_centered3d = test_coords3d - test_coords3d.mean(axis=0)[None, :]
|
|
461
|
+
for i, rcoords3d in enumerate(ref_coords3d):
|
|
462
|
+
ref_centered3d = rcoords3d - rcoords3d.mean(axis=0)[None, :]
|
|
463
|
+
rmsd_ = kabsch_rmsd(test_centered3d, ref_centered3d)
|
|
464
|
+
if rmsd_ <= rmsd_thresh:
|
|
465
|
+
break
|
|
466
|
+
else:
|
|
467
|
+
return False, -1
|
|
468
|
+
return True, i
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def geom_similar(test_geom: Geometry, ref_geoms: List[Geometry], **kwargs) -> bool:
|
|
472
|
+
return coordinates_similar(
|
|
473
|
+
test_geom.coords3d, [geom.coords3d for geom in ref_geoms], **kwargs
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
########################
|
|
478
|
+
# Actual AFIR drivers #
|
|
479
|
+
########################
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def opt_afir_path(geom, calc_getter, afir_kwargs, opt_kwargs=None, out_dir=None):
|
|
483
|
+
"""Minimize geometry with AFIR calculator."""
|
|
484
|
+
if opt_kwargs is None:
|
|
485
|
+
opt_kwargs = dict()
|
|
486
|
+
if out_dir is None:
|
|
487
|
+
out_dir = "."
|
|
488
|
+
out_dir = Path(out_dir)
|
|
489
|
+
|
|
490
|
+
actual_calc = calc_getter(out_dir=out_dir / OUT_DIR_DEFAULT)
|
|
491
|
+
|
|
492
|
+
def afir_calc_getter():
|
|
493
|
+
afir_calc = AFIR(actual_calc, out_dir=out_dir, **afir_kwargs)
|
|
494
|
+
return afir_calc
|
|
495
|
+
|
|
496
|
+
_opt_kwargs = {
|
|
497
|
+
"dump": True,
|
|
498
|
+
"out_dir": out_dir,
|
|
499
|
+
"prefix": "afir",
|
|
500
|
+
"max_cycles": 125,
|
|
501
|
+
"overachieve_factor": 3,
|
|
502
|
+
"hessian_update": "flowchart",
|
|
503
|
+
}
|
|
504
|
+
logger.debug(
|
|
505
|
+
"\n".join(
|
|
506
|
+
(
|
|
507
|
+
"afir_kwargs:",
|
|
508
|
+
"\t" + pformat(afir_kwargs),
|
|
509
|
+
"opt_kwargs:",
|
|
510
|
+
"\t" + pformat(_opt_kwargs),
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
_opt_kwargs.update(opt_kwargs)
|
|
515
|
+
opt_result = run_opt(geom, afir_calc_getter, opt_key="rfo", opt_kwargs=_opt_kwargs)
|
|
516
|
+
opt = opt_result.opt
|
|
517
|
+
|
|
518
|
+
afir_path = AFIRPath(
|
|
519
|
+
atoms=geom.atoms,
|
|
520
|
+
cart_coords=np.array(opt.cart_coords),
|
|
521
|
+
energies=np.array(opt.true_energies),
|
|
522
|
+
forces=np.array(opt.true_forces),
|
|
523
|
+
opt_is_converged=opt.is_converged,
|
|
524
|
+
charge=actual_calc.charge,
|
|
525
|
+
mult=actual_calc.mult,
|
|
526
|
+
gamma=afir_kwargs["gamma"],
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return afir_path
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def run_afir_path(
|
|
533
|
+
geom,
|
|
534
|
+
calc_getter,
|
|
535
|
+
out_dir,
|
|
536
|
+
gamma_max,
|
|
537
|
+
gamma_interval: Tuple[float, float],
|
|
538
|
+
rng,
|
|
539
|
+
ignore_bonds=None,
|
|
540
|
+
bond_factor=AFIR_BOND_FACTOR,
|
|
541
|
+
afir_kwargs=None,
|
|
542
|
+
opt_kwargs=None,
|
|
543
|
+
):
|
|
544
|
+
"""Driver for AFIR minimizations with increasing gamma values."""
|
|
545
|
+
if ignore_bonds is None:
|
|
546
|
+
ignore_bonds = list()
|
|
547
|
+
if afir_kwargs is None:
|
|
548
|
+
afir_kwargs = dict()
|
|
549
|
+
if opt_kwargs is None:
|
|
550
|
+
opt_kwargs = {}
|
|
551
|
+
|
|
552
|
+
if out_dir.exists():
|
|
553
|
+
dir_contents = os.listdir(out_dir)
|
|
554
|
+
for fn in dir_contents:
|
|
555
|
+
fn = out_dir / fn
|
|
556
|
+
try:
|
|
557
|
+
os.remove(fn)
|
|
558
|
+
except IsADirectoryError:
|
|
559
|
+
shutil.rmtree(fn)
|
|
560
|
+
else:
|
|
561
|
+
os.mkdir(out_dir)
|
|
562
|
+
|
|
563
|
+
# Decreasing the distance between two atoms in SC-AFIR may lead to broken
|
|
564
|
+
# bonds for these two bonds, e.g., hydrogen atoms "that are left behind".
|
|
565
|
+
# These bonds will be formed again when the AFIR function is minimized, but
|
|
566
|
+
# we are not interested in these changes. So they can be ignored here.
|
|
567
|
+
ignore_bonds = set([frozenset(bond) for bond in ignore_bonds])
|
|
568
|
+
|
|
569
|
+
ref_calc = calc_getter()
|
|
570
|
+
ref_energy = ref_calc.get_energy(geom.atoms, geom.cart_coords)["energy"]
|
|
571
|
+
geom_backup = geom.copy()
|
|
572
|
+
|
|
573
|
+
# By using (1.0, 1.0) as interval we can directly start at gamma_max, e.g.,
|
|
574
|
+
# in SC-AFIR.
|
|
575
|
+
gamma_low, gamma_high = gamma_interval
|
|
576
|
+
assert gamma_high >= gamma_low
|
|
577
|
+
gamma_spread = gamma_high - gamma_low
|
|
578
|
+
gamma_0 = (gamma_low + (gamma_spread * rng.random(1)[0])) * gamma_max
|
|
579
|
+
gamma_inrc = 0.1 * gamma_max
|
|
580
|
+
|
|
581
|
+
afir_paths = list()
|
|
582
|
+
best_afir_path = None
|
|
583
|
+
lowest_barrier = None
|
|
584
|
+
gamma = gamma_0
|
|
585
|
+
|
|
586
|
+
# Minimize AFIR functions until gamma exceeds gamma_max.
|
|
587
|
+
logger.info(f"New AFIR run with γ_max={gamma_max:.6f} au")
|
|
588
|
+
while gamma <= gamma_max:
|
|
589
|
+
gamma_ratio = gamma / gamma_max
|
|
590
|
+
logger.info(f"AFIR run with γ={gamma:.6f} au, γ/γ_max={gamma_ratio: >6.2%}")
|
|
591
|
+
_afir_kwargs = afir_kwargs.copy()
|
|
592
|
+
_afir_kwargs["gamma"] = gamma
|
|
593
|
+
try:
|
|
594
|
+
afir_path = opt_afir_path(
|
|
595
|
+
geom,
|
|
596
|
+
calc_getter,
|
|
597
|
+
afir_kwargs=_afir_kwargs,
|
|
598
|
+
opt_kwargs=opt_kwargs,
|
|
599
|
+
out_dir=out_dir,
|
|
600
|
+
)
|
|
601
|
+
# Can happen in SC-AFIR runs when fragments comprise only hydrogens.
|
|
602
|
+
except CovRadiiSumZero:
|
|
603
|
+
logger.warning("Sum of covalent radii is 0.0!")
|
|
604
|
+
best_afir_path = None
|
|
605
|
+
break
|
|
606
|
+
except Exception:
|
|
607
|
+
logger.error(f"Optimization crashed!\n{traceback.format_exc()}")
|
|
608
|
+
best_afir_path = None
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
afir_paths.append(afir_path)
|
|
612
|
+
true_energies = afir_path.energies
|
|
613
|
+
|
|
614
|
+
# Check for changes in bond topology by comparing the result of the current
|
|
615
|
+
# minimization to the initial geometry.
|
|
616
|
+
formed, broken = get_bond_difference(geom_backup, geom, bond_factor=bond_factor)
|
|
617
|
+
formed = to_sets(formed) - ignore_bonds
|
|
618
|
+
broken = to_sets(broken) - ignore_bonds
|
|
619
|
+
if formed or broken:
|
|
620
|
+
max_energy = true_energies.max()
|
|
621
|
+
barrier = max_energy - ref_energy
|
|
622
|
+
|
|
623
|
+
# Always store the first path that leads to a change in bond topology.
|
|
624
|
+
if lowest_barrier is None:
|
|
625
|
+
best_afir_path = afir_path
|
|
626
|
+
lowest_barrier = barrier
|
|
627
|
+
# Break when a path with a lower or higher barrier is detected. This
|
|
628
|
+
# should happen in the cycle after the first change in bond toplogy was
|
|
629
|
+
# detected.
|
|
630
|
+
if barrier < lowest_barrier:
|
|
631
|
+
best_afir_path = afir_path
|
|
632
|
+
lowest_barrier = barrier
|
|
633
|
+
break
|
|
634
|
+
elif barrier > lowest_barrier:
|
|
635
|
+
break
|
|
636
|
+
else:
|
|
637
|
+
barrier = None
|
|
638
|
+
|
|
639
|
+
# Update values for next cycle
|
|
640
|
+
gamma += gamma_inrc
|
|
641
|
+
|
|
642
|
+
# reduce(lambda ap1, ap2: ap1 + ap2, afir_paths).dump_trj("dumped_trj.xyz")
|
|
643
|
+
|
|
644
|
+
# Construct TS guess from highest energy point along the AFIR path.
|
|
645
|
+
if best_afir_path:
|
|
646
|
+
guess_ind = best_afir_path.energies.argmax()
|
|
647
|
+
guess_coords = best_afir_path.cart_coords[guess_ind]
|
|
648
|
+
ts_guess = Geometry(geom.atoms, guess_coords)
|
|
649
|
+
ts_guess.dump_xyz(out_dir / "ts_guess.xyz")
|
|
650
|
+
afir_path_merged = reduce(lambda ap1, ap2: ap1 + ap2, afir_paths)
|
|
651
|
+
else:
|
|
652
|
+
ts_guess = None
|
|
653
|
+
afir_path_merged = None
|
|
654
|
+
return ts_guess, afir_path_merged
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def relax_afir_path(atoms, cart_coords, calc_getter, images=15, out_dir=None):
|
|
658
|
+
"""Sample imagef from AFIR path and do COS relaxation."""
|
|
659
|
+
image_inds = pick_image_inds(cart_coords, images=images)
|
|
660
|
+
images = [Geometry(atoms, cart_coords[i]) for i in image_inds]
|
|
661
|
+
|
|
662
|
+
# Relax last image
|
|
663
|
+
opt_kwargs = {
|
|
664
|
+
"dump": True,
|
|
665
|
+
"prefix": "last",
|
|
666
|
+
"out_dir": out_dir,
|
|
667
|
+
}
|
|
668
|
+
last_image = images[-1]
|
|
669
|
+
last_image_backup = last_image.copy()
|
|
670
|
+
run_opt(last_image, calc_getter, opt_key="rfo", opt_kwargs=opt_kwargs)
|
|
671
|
+
_, broken = get_bond_difference(last_image, last_image_backup)
|
|
672
|
+
if broken:
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
cos_kwargs = {}
|
|
676
|
+
cos = NEB(images, **cos_kwargs)
|
|
677
|
+
cos_opt_kwargs = {
|
|
678
|
+
"align": True,
|
|
679
|
+
"dump": True,
|
|
680
|
+
"max_cycles": 30,
|
|
681
|
+
"out_dir": out_dir,
|
|
682
|
+
}
|
|
683
|
+
run_opt(cos, calc_getter, opt_key="lbfgs", opt_kwargs=cos_opt_kwargs)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def run_mc_afir_paths(
|
|
687
|
+
geoms: List,
|
|
688
|
+
calc_getter: Callable,
|
|
689
|
+
gamma_max: float,
|
|
690
|
+
rng,
|
|
691
|
+
N_max: int = 5,
|
|
692
|
+
gamma_interval: Tuple[float, float] = (0.0, 1.0),
|
|
693
|
+
afir_kwargs: Optional[Dict] = None,
|
|
694
|
+
opt_kwargs: Optional[Dict] = None,
|
|
695
|
+
):
|
|
696
|
+
if afir_kwargs is None:
|
|
697
|
+
afir_kwargs = dict()
|
|
698
|
+
|
|
699
|
+
N = 0
|
|
700
|
+
N_0 = 0
|
|
701
|
+
fmt = " >4d"
|
|
702
|
+
while True:
|
|
703
|
+
geom, _afir_kwargs, *_ = prepare_mc_afir(geoms, rng=rng)
|
|
704
|
+
afir_kwargs = afir_kwargs.copy()
|
|
705
|
+
afir_kwargs.update(_afir_kwargs)
|
|
706
|
+
out_dir = Path(f"out_{N:03d}")
|
|
707
|
+
|
|
708
|
+
ts_guess, afir_path = run_afir_path(
|
|
709
|
+
geom,
|
|
710
|
+
calc_getter,
|
|
711
|
+
out_dir,
|
|
712
|
+
gamma_max,
|
|
713
|
+
gamma_interval,
|
|
714
|
+
rng=rng,
|
|
715
|
+
afir_kwargs=afir_kwargs,
|
|
716
|
+
opt_kwargs=opt_kwargs,
|
|
717
|
+
)
|
|
718
|
+
ts_guess_is_new = yield N, ts_guess, afir_path
|
|
719
|
+
if ts_guess_is_new:
|
|
720
|
+
N_0 = N
|
|
721
|
+
# Allow up to consecutive N_max failures
|
|
722
|
+
if (N - N_0) > N_max:
|
|
723
|
+
break
|
|
724
|
+
logger.debug(f"{N_0=:{fmt}}, {N=:{fmt}}, {N-N_0=:{fmt}}, {N_max=:{fmt}}, ")
|
|
725
|
+
N += 1
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def run_sc_afir_paths(
|
|
729
|
+
geom: Geometry,
|
|
730
|
+
calc_getter: Callable,
|
|
731
|
+
gamma_max: float,
|
|
732
|
+
rng,
|
|
733
|
+
N_max: int = 5,
|
|
734
|
+
N_sample: int = 0,
|
|
735
|
+
gamma_interval: Tuple[float, float] = (1.0, 1.0), # Start directly with gamma_max
|
|
736
|
+
afir_kwargs: Optional[Dict] = None,
|
|
737
|
+
opt_kwargs: Optional[Dict] = None,
|
|
738
|
+
target_pairs: Optional[List] = None,
|
|
739
|
+
):
|
|
740
|
+
if target_pairs is None:
|
|
741
|
+
target_pairs = determine_target_pairs_for_geom(geom)
|
|
742
|
+
|
|
743
|
+
if afir_kwargs is None:
|
|
744
|
+
afir_kwargs = dict()
|
|
745
|
+
if opt_kwargs is None:
|
|
746
|
+
opt_kwargs = dict()
|
|
747
|
+
_opt_kwargs = opt_kwargs.copy()
|
|
748
|
+
|
|
749
|
+
i = 0
|
|
750
|
+
while len(target_pairs) > 0:
|
|
751
|
+
m, n = target_pairs.pop(0)
|
|
752
|
+
logger.info(f"Running SC-AFIR with target_pair ({m}, {n}).")
|
|
753
|
+
# _afir_kwargs will contain the automatically determined fragments
|
|
754
|
+
geom_mod, _afir_kwargs, broken_bonds, trj = prepare_sc_afir(geom, m, n)
|
|
755
|
+
afir_kwargs = afir_kwargs.copy()
|
|
756
|
+
afir_kwargs.update(_afir_kwargs)
|
|
757
|
+
|
|
758
|
+
# _opt_kwargs.update({
|
|
759
|
+
# "fragments": [[m, ], [n, ]],
|
|
760
|
+
# "monitor_frag_dists": 5,
|
|
761
|
+
# })
|
|
762
|
+
|
|
763
|
+
out_dir = Path(f"out_{i:03d}")
|
|
764
|
+
ts_guess, afir_path = run_afir_path(
|
|
765
|
+
geom_mod,
|
|
766
|
+
calc_getter,
|
|
767
|
+
out_dir,
|
|
768
|
+
gamma_max,
|
|
769
|
+
gamma_interval,
|
|
770
|
+
rng=rng,
|
|
771
|
+
ignore_bonds=broken_bonds,
|
|
772
|
+
afir_kwargs=afir_kwargs,
|
|
773
|
+
opt_kwargs=_opt_kwargs,
|
|
774
|
+
)
|
|
775
|
+
ts_guess_is_new = yield i, ts_guess, afir_path
|
|
776
|
+
i += 1
|
|
777
|
+
# Here, TS optimization & IRC integration could take place, when the TS is new.
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def run_afir_paths(
|
|
781
|
+
afir_key,
|
|
782
|
+
geoms,
|
|
783
|
+
calc_getter,
|
|
784
|
+
afir_kwargs=None,
|
|
785
|
+
opt_kwargs=None,
|
|
786
|
+
seed=None,
|
|
787
|
+
N_sample=None,
|
|
788
|
+
rmsd_thresh: float = AFIR_RMSD_THRESH,
|
|
789
|
+
**kwargs,
|
|
790
|
+
):
|
|
791
|
+
if seed is None:
|
|
792
|
+
rng = np.random.default_rng()
|
|
793
|
+
seed = rng.integers(1_000_000_000_000)
|
|
794
|
+
logger.info(f"{seed=}")
|
|
795
|
+
rng = np.random.default_rng(seed)
|
|
796
|
+
|
|
797
|
+
if afir_key == "sc":
|
|
798
|
+
assert (
|
|
799
|
+
len(geoms) == 1
|
|
800
|
+
), f"Expected only 1 geometry for SC-AFIR, but got {len(geoms)}!."
|
|
801
|
+
geoms = geoms[0]
|
|
802
|
+
|
|
803
|
+
afir_funcs = {
|
|
804
|
+
"mc": run_mc_afir_paths,
|
|
805
|
+
"sc": run_sc_afir_paths,
|
|
806
|
+
}
|
|
807
|
+
afir_func = afir_funcs[afir_key]
|
|
808
|
+
afir_coroutine = afir_func(
|
|
809
|
+
geoms,
|
|
810
|
+
calc_getter,
|
|
811
|
+
rng=rng,
|
|
812
|
+
afir_kwargs=afir_kwargs,
|
|
813
|
+
opt_kwargs=opt_kwargs,
|
|
814
|
+
**kwargs,
|
|
815
|
+
)
|
|
816
|
+
ts_guesses = list()
|
|
817
|
+
afir_paths = list()
|
|
818
|
+
stop_sign = "afir_stop"
|
|
819
|
+
|
|
820
|
+
i, ts_guess, afir_path = next(afir_coroutine)
|
|
821
|
+
while True:
|
|
822
|
+
prefix = f"{i:03d}"
|
|
823
|
+
logger.info(f"AFIR run {prefix}")
|
|
824
|
+
# Check if we found enough TS guesses
|
|
825
|
+
if N_sample and (len(ts_guesses) >= N_sample):
|
|
826
|
+
break
|
|
827
|
+
|
|
828
|
+
# Check similarity of TS guess to previously found TS guesses
|
|
829
|
+
try:
|
|
830
|
+
rmsds = [ts_guess.rmsd(guess) for guess in ts_guesses]
|
|
831
|
+
min_rmsd = min(rmsds)
|
|
832
|
+
except AttributeError: # Raised when ts_guess is None
|
|
833
|
+
min_rmsd = None
|
|
834
|
+
except ValueError:
|
|
835
|
+
min_rmsd = rmsd_thresh if (ts_guess is not None) else None
|
|
836
|
+
|
|
837
|
+
if ts_guess_is_new := ((min_rmsd is not None) and (min_rmsd >= rmsd_thresh)):
|
|
838
|
+
logger.debug(f"rmsds different enough! {min_rmsd=:.6f} au")
|
|
839
|
+
ts_guesses.append(ts_guess)
|
|
840
|
+
afir_paths.append(afir_path)
|
|
841
|
+
afir_path.dump_trj(f"{prefix}_afir_path_trj.xyz")
|
|
842
|
+
ts_guess.dump_xyz(f"{prefix}_ts_guess.xyz")
|
|
843
|
+
elif min_rmsd:
|
|
844
|
+
logger.debug(f"rmsds too similar! {min_rmsd=:.6f} au)")
|
|
845
|
+
|
|
846
|
+
sign = check_for_end_sign(add_signs=(stop_sign,))
|
|
847
|
+
if sign == stop_sign:
|
|
848
|
+
break
|
|
849
|
+
logger.info(f"{len(ts_guesses)=: >5d}")
|
|
850
|
+
try:
|
|
851
|
+
i, ts_guess, afir_path = afir_coroutine.send(ts_guess_is_new)
|
|
852
|
+
except StopIteration:
|
|
853
|
+
break
|
|
854
|
+
|
|
855
|
+
return ts_guesses, afir_paths
|