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,1129 @@
|
|
|
1
|
+
# [1] https://www.sciencedirect.com/science/article/pii/S0166128098004758
|
|
2
|
+
# https://doi.org/10.1016/S0166-1280(98)00475-8
|
|
3
|
+
# Dapprich, Frisch, 1998
|
|
4
|
+
# [2] https://onlinelibrary.wiley.com/doi/abs/10.1002/9783527629213.ch2
|
|
5
|
+
# Clemente, Frisch, 2010
|
|
6
|
+
#
|
|
7
|
+
# Not implemented in pysisyphus
|
|
8
|
+
#
|
|
9
|
+
# [2] https://aip.scitation.org/doi/pdf/10.1063/1.2814164?class=pdf
|
|
10
|
+
# QM/QM ONIOM EE based on Mulliken charges
|
|
11
|
+
# Hratchian, Raghavachari, 2008
|
|
12
|
+
# [3] https://aip.scitation.org/doi/full/10.1063/1.3315417<Paste>
|
|
13
|
+
# QM/QM ONIOM EE based on Löwdin charges
|
|
14
|
+
# Mayhall, Hratchian, 2010
|
|
15
|
+
# [4] https://www.frontiersin.org/articles/10.3389/fchem.2018.00089/full
|
|
16
|
+
# Overview on hybrid methods
|
|
17
|
+
# [5] https://doi.org/10.1021/jp0446332
|
|
18
|
+
# Electronic embedding charge redistribution
|
|
19
|
+
# Lin, Truhlar 2004
|
|
20
|
+
# Excited state ONIOM
|
|
21
|
+
# [5] https://aip.scitation.org/doi/pdf/10.1063/1.4972000?class=pdf
|
|
22
|
+
# [6] https://pubs.rsc.org/en/content/articlehtml/2012/pc/c2pc90007f
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
import itertools as it
|
|
26
|
+
import logging
|
|
27
|
+
from collections import namedtuple
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import scipy.sparse as sparse
|
|
31
|
+
|
|
32
|
+
from pysisyphus.calculators import (
|
|
33
|
+
Composite,
|
|
34
|
+
Gaussian16,
|
|
35
|
+
MOPAC,
|
|
36
|
+
OBabel,
|
|
37
|
+
OpenMolcas,
|
|
38
|
+
ORCA,
|
|
39
|
+
ORCA5,
|
|
40
|
+
Psi4,
|
|
41
|
+
Turbomole,
|
|
42
|
+
XTB,
|
|
43
|
+
PyXTB,
|
|
44
|
+
)
|
|
45
|
+
from pysisyphus.calculators.Calculator import Calculator
|
|
46
|
+
from pysisyphus.elem_data import COVALENT_RADII as CR
|
|
47
|
+
from pysisyphus.Geometry import Geometry
|
|
48
|
+
from pysisyphus.helpers_pure import full_expand
|
|
49
|
+
from pysisyphus.intcoords.setup import get_bond_sets
|
|
50
|
+
from pysisyphus.intcoords.setup_fast import get_bond_vec_getter
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
CALC_DICT = {
|
|
54
|
+
"composite": Composite,
|
|
55
|
+
"g16": Gaussian16,
|
|
56
|
+
"openmolcas": OpenMolcas.OpenMolcas,
|
|
57
|
+
"mopac": MOPAC,
|
|
58
|
+
"obabel": OBabel,
|
|
59
|
+
"orca": ORCA.ORCA,
|
|
60
|
+
"orca5": ORCA5.ORCA5,
|
|
61
|
+
"psi4": Psi4,
|
|
62
|
+
"turbomole": Turbomole,
|
|
63
|
+
"xtb": XTB.XTB,
|
|
64
|
+
# "pypsi4": PyPsi4,
|
|
65
|
+
"pyxtb": PyXTB.PyXTB,
|
|
66
|
+
}
|
|
67
|
+
try:
|
|
68
|
+
from pysisyphus.calculators.PySCF import PySCF
|
|
69
|
+
|
|
70
|
+
CALC_DICT["pyscf"] = PySCF
|
|
71
|
+
except (ModuleNotFoundError, ImportError, OSError):
|
|
72
|
+
# print("Error importing PySCF in ONIOMv2")
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
Link = namedtuple("Link", "ind parent_ind atom g")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_g_value(atom, parent_atom, link_atom):
|
|
80
|
+
cr, pcr, lcr = [CR[a.lower()] for a in (atom, parent_atom, link_atom)]
|
|
81
|
+
|
|
82
|
+
# Ratio between sum of CR_atom and CR_link with sum of CR_atom CR_parent_atom.
|
|
83
|
+
# See [1] Sect. 2.2 page 5.
|
|
84
|
+
g = (cr + lcr) / (cr + pcr)
|
|
85
|
+
return g
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cap_fragment(atoms, coords, fragment, link_atom="H", g=None):
|
|
89
|
+
coords3d = coords.reshape(-1, 3)
|
|
90
|
+
|
|
91
|
+
fragment_set = set(fragment)
|
|
92
|
+
|
|
93
|
+
# Determine bond(s) that connect fragment with the rest
|
|
94
|
+
bonds = get_bond_sets(atoms, coords3d)
|
|
95
|
+
bond_sets = [set(b) for b in bonds]
|
|
96
|
+
|
|
97
|
+
# Find all bonds that involve one atom of model. These bonds
|
|
98
|
+
# connect the model to the real geometry. We want to cap these
|
|
99
|
+
# bonds.
|
|
100
|
+
break_bonds = [b for b in bond_sets if len(b & fragment_set) == 1]
|
|
101
|
+
|
|
102
|
+
# Put capping atoms at every bond to break.
|
|
103
|
+
# The model fragment size corresponds to the length of the union of
|
|
104
|
+
# the model set and the atoms in break_bonds.
|
|
105
|
+
capped_frag = fragment_set.union(*break_bonds)
|
|
106
|
+
capped_inds = list(sorted(capped_frag))
|
|
107
|
+
|
|
108
|
+
# Index map between the new model geometry and the original indices
|
|
109
|
+
# in the real geometry.
|
|
110
|
+
atom_map = {
|
|
111
|
+
model_ind: real_ind
|
|
112
|
+
for real_ind, model_ind in zip(capped_inds, range(len(capped_inds)))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
links = list()
|
|
116
|
+
for bb in break_bonds:
|
|
117
|
+
to_cap = bb - fragment_set
|
|
118
|
+
assert len(to_cap) == 1
|
|
119
|
+
ind = list(bb - to_cap)[0]
|
|
120
|
+
parent_ind = tuple(to_cap)[0]
|
|
121
|
+
if g is None:
|
|
122
|
+
g = get_g_value(atoms[ind], atoms[parent_ind], link_atom)
|
|
123
|
+
link = Link(ind=ind, parent_ind=parent_ind, atom=link_atom, g=g)
|
|
124
|
+
links.append(link)
|
|
125
|
+
|
|
126
|
+
return atom_map, links
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def atom_inds_to_cart_inds(atom_inds):
|
|
130
|
+
stencil = np.array((0, 1, 2), dtype=int)
|
|
131
|
+
size_ = len(atom_inds)
|
|
132
|
+
cart_inds = np.tile(stencil, size_) + np.repeat(atom_inds, 3) * 3
|
|
133
|
+
return cart_inds
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ModelDummyCalc:
|
|
137
|
+
def __init__(self, model, cap=False): # , all_atoms, all_coords):
|
|
138
|
+
self.model = model
|
|
139
|
+
self.cap = cap
|
|
140
|
+
|
|
141
|
+
def get_energy(self, atoms, coords):
|
|
142
|
+
energy = self.model.get_energy(atoms, coords, cap=self.cap)
|
|
143
|
+
results = {"energy": energy}
|
|
144
|
+
return results
|
|
145
|
+
|
|
146
|
+
def get_forces(self, atoms, coords):
|
|
147
|
+
# if self.parent_name is not None:
|
|
148
|
+
# raise Exception("Currently, this does not work for Models with a parent!")
|
|
149
|
+
energy, forces = self.model.get_forces(atoms, coords, cap=self.cap)
|
|
150
|
+
forces_ = np.zeros((len(atoms), 3))
|
|
151
|
+
# The redistribution below only works withouth parent, as otherwise
|
|
152
|
+
# the numer of atoms on the RHS and LHS differ.
|
|
153
|
+
forces_[: len(atoms) - len(self.model.links)] = forces.reshape(-1, 3)[
|
|
154
|
+
self.model.atom_inds
|
|
155
|
+
]
|
|
156
|
+
results = {"energy": energy, "forces": forces_.flatten()}
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
# def get_hessian(self, atoms, coords):
|
|
160
|
+
# energy, hessian = self.model.get_hessian(atoms, coords, cap=False)
|
|
161
|
+
# results = {"energy": energy, "hessian": hessian}
|
|
162
|
+
# return results
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Model:
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
name,
|
|
169
|
+
calc_level,
|
|
170
|
+
calc,
|
|
171
|
+
parent_name,
|
|
172
|
+
parent_calc_level,
|
|
173
|
+
parent_calc,
|
|
174
|
+
atom_inds,
|
|
175
|
+
parent_atom_inds,
|
|
176
|
+
use_link_atoms=True,
|
|
177
|
+
):
|
|
178
|
+
|
|
179
|
+
self.name = name
|
|
180
|
+
self.calc_level = calc_level
|
|
181
|
+
self.calc = calc
|
|
182
|
+
|
|
183
|
+
self.parent_name = parent_name
|
|
184
|
+
self.parent_calc_level = parent_calc_level
|
|
185
|
+
self.parent_calc = parent_calc
|
|
186
|
+
|
|
187
|
+
self.atom_inds = list(atom_inds)
|
|
188
|
+
# parent_atom_inds may be None
|
|
189
|
+
try:
|
|
190
|
+
self.parent_atom_inds = list(parent_atom_inds)
|
|
191
|
+
except TypeError:
|
|
192
|
+
self.parent_atom_inds = None
|
|
193
|
+
|
|
194
|
+
self.use_link_atoms = use_link_atoms
|
|
195
|
+
|
|
196
|
+
self.links = list()
|
|
197
|
+
self.capped = False
|
|
198
|
+
self.J = None
|
|
199
|
+
|
|
200
|
+
self.log(f"Created model '{self}' with {len(self.atom_inds)} atoms.")
|
|
201
|
+
|
|
202
|
+
def log(self, message=""):
|
|
203
|
+
logger = logging.getLogger("calculator")
|
|
204
|
+
logger.debug(self.__str__() + " " + message)
|
|
205
|
+
|
|
206
|
+
def create_links(self, atoms, coords, debug=False):
|
|
207
|
+
self.capped = True
|
|
208
|
+
|
|
209
|
+
if self.use_link_atoms and self.parent_name is not None:
|
|
210
|
+
_, self.links = cap_fragment(atoms, coords, self.atom_inds)
|
|
211
|
+
self.capped_atom_num = len(self.atom_inds) + len(self.links)
|
|
212
|
+
for i, link in enumerate(self.links):
|
|
213
|
+
ind, parent_ind = link.ind, link.parent_ind
|
|
214
|
+
self.log(
|
|
215
|
+
f"\tCreated Link atom ({link.atom}) between {atoms[ind]}{ind} "
|
|
216
|
+
f"and {atoms[parent_ind]}{parent_ind} (g={link.g:.6f})"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if len(self.links) == 0:
|
|
220
|
+
self.log("Didn't create any link atoms!\n")
|
|
221
|
+
|
|
222
|
+
# self.J = self.get_jacobian()
|
|
223
|
+
self.J = self.get_sparse_jacobian()
|
|
224
|
+
|
|
225
|
+
if debug:
|
|
226
|
+
catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
227
|
+
geom = Geometry(catoms, ccoords)
|
|
228
|
+
geom.jmol()
|
|
229
|
+
|
|
230
|
+
def capped_atoms_coords(self, all_atoms, all_coords):
|
|
231
|
+
assert self.capped, "Did you forget to call create_links()?"
|
|
232
|
+
|
|
233
|
+
org_atom_num = len(self.atom_inds)
|
|
234
|
+
c3d = all_coords.reshape(-1, 3)
|
|
235
|
+
|
|
236
|
+
capped_atoms = [all_atoms[i] for i in self.atom_inds]
|
|
237
|
+
# Initialize empty coordinate array
|
|
238
|
+
capped_coords = np.zeros((self.capped_atom_num, 3))
|
|
239
|
+
# Copy non-capped coordinates
|
|
240
|
+
capped_coords[:org_atom_num] = c3d[self.atom_inds]
|
|
241
|
+
|
|
242
|
+
for i, link in enumerate(self.links):
|
|
243
|
+
capped_atoms.append(link.atom)
|
|
244
|
+
|
|
245
|
+
r1 = c3d[link.ind]
|
|
246
|
+
r3 = c3d[link.parent_ind]
|
|
247
|
+
r2 = r1 + link.g * (r3 - r1)
|
|
248
|
+
capped_coords[org_atom_num + i] = r2
|
|
249
|
+
return capped_atoms, capped_coords
|
|
250
|
+
|
|
251
|
+
def create_bond_vec_getters(self, atoms):
|
|
252
|
+
link_parent_inds = [link.parent_ind for link in self.links]
|
|
253
|
+
no_bonds_with = [
|
|
254
|
+
[
|
|
255
|
+
link.ind,
|
|
256
|
+
]
|
|
257
|
+
for link in self.links
|
|
258
|
+
]
|
|
259
|
+
self.log(
|
|
260
|
+
f"Model has {len(link_parent_inds)} link atom hosts: {link_parent_inds}"
|
|
261
|
+
)
|
|
262
|
+
covalent_radii = [CR[atom.lower()] for atom in atoms]
|
|
263
|
+
self.get_bond_vecs = get_bond_vec_getter(
|
|
264
|
+
atoms,
|
|
265
|
+
covalent_radii,
|
|
266
|
+
link_parent_inds,
|
|
267
|
+
no_bonds_with,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def get_jacobian(self):
|
|
271
|
+
try:
|
|
272
|
+
# Shape of Jacobian is (model + link, real). TypeError will be raised
|
|
273
|
+
# when self.parent_atom_inds is None.
|
|
274
|
+
jac_shape = (
|
|
275
|
+
len(self.atom_inds) * 3 + len(self.links) * 3,
|
|
276
|
+
len(self.parent_atom_inds) * 3,
|
|
277
|
+
)
|
|
278
|
+
except TypeError:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
J = np.zeros(jac_shape)
|
|
282
|
+
# Stencil for diagonal elements of 3x3 submatrix
|
|
283
|
+
stencil = np.array((0, 1, 2), dtype=int)
|
|
284
|
+
size_ = len(self.atom_inds)
|
|
285
|
+
|
|
286
|
+
model_rows = np.arange(size_ * 3)
|
|
287
|
+
# When more than two layers are present the inner layers aren't directly
|
|
288
|
+
# embedded in the outermost layer. This means parent_inds does not begin
|
|
289
|
+
# at 0, but with a higher index. So we need a map of the actual indices
|
|
290
|
+
# (not starting at 0) to the indices in the Jacobian which start at 0.
|
|
291
|
+
atom_inds = [self.parent_atom_inds.index(ind) for ind in self.atom_inds]
|
|
292
|
+
ind_map = {k: v for k, v in zip(self.atom_inds, atom_inds)}
|
|
293
|
+
model_cols = atom_inds_to_cart_inds(atom_inds)
|
|
294
|
+
J[model_rows, model_cols] = 1
|
|
295
|
+
|
|
296
|
+
# Link atoms
|
|
297
|
+
link_start = model_rows.max() + 1
|
|
298
|
+
for i, (ind, parent_ind, atom, g) in enumerate(self.links):
|
|
299
|
+
rows = link_start + i * 3 + stencil
|
|
300
|
+
cols = ind_map[ind] * 3 + stencil
|
|
301
|
+
|
|
302
|
+
J[rows, cols] = 1 - g
|
|
303
|
+
try:
|
|
304
|
+
parent_cols = self.parent_atom_inds.index(parent_ind) * 3 + stencil
|
|
305
|
+
J[rows, parent_cols] = g
|
|
306
|
+
# Raised when link atom is not coupled to layer above, but
|
|
307
|
+
# to a layer higher above.
|
|
308
|
+
except ValueError:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
return J
|
|
312
|
+
|
|
313
|
+
def get_sparse_jacobian(self):
|
|
314
|
+
try:
|
|
315
|
+
# Shape of Jacobian is (model + link, real). TypeError will be raised
|
|
316
|
+
# when self.parent_atom_inds is None.
|
|
317
|
+
jac_shape = (
|
|
318
|
+
len(self.atom_inds) * 3 + len(self.links) * 3,
|
|
319
|
+
len(self.parent_atom_inds) * 3,
|
|
320
|
+
)
|
|
321
|
+
except TypeError:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
# Stencil for diagonal elements of 3x3 submatrix
|
|
325
|
+
stencil = np.array((0, 1, 2), dtype=int)
|
|
326
|
+
ones = np.ones_like(stencil)
|
|
327
|
+
size_ = len(self.atom_inds)
|
|
328
|
+
|
|
329
|
+
model_rows = np.arange(size_ * 3)
|
|
330
|
+
# When more than two layers are present the inner layers aren't directly
|
|
331
|
+
# embedded in the outermost layer. This means parent_inds does not begin
|
|
332
|
+
# at 0, but with a higher index. So we need a map of the actual indices
|
|
333
|
+
# (not starting at 0) to the indices in the Jacobian which start at 0.
|
|
334
|
+
atom_inds = [self.parent_atom_inds.index(ind) for ind in self.atom_inds]
|
|
335
|
+
ind_map = {k: v for k, v in zip(self.atom_inds, atom_inds)}
|
|
336
|
+
model_cols = atom_inds_to_cart_inds(atom_inds)
|
|
337
|
+
|
|
338
|
+
jac_rows = model_rows.tolist()
|
|
339
|
+
jac_cols = model_cols.tolist()
|
|
340
|
+
jac_data = np.ones_like(jac_cols).tolist()
|
|
341
|
+
|
|
342
|
+
# Link atoms
|
|
343
|
+
link_start = model_rows.max() + 1
|
|
344
|
+
for i, (ind, parent_ind, atom, g) in enumerate(self.links):
|
|
345
|
+
rows = (link_start + i * 3 + stencil).tolist()
|
|
346
|
+
cols = (ind_map[ind] * 3 + stencil).tolist()
|
|
347
|
+
jac_rows += rows
|
|
348
|
+
jac_cols += cols
|
|
349
|
+
jac_data += (ones - g).tolist()
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
parent_cols = (
|
|
353
|
+
self.parent_atom_inds.index(parent_ind) * 3 + stencil
|
|
354
|
+
).tolist()
|
|
355
|
+
jac_rows += rows
|
|
356
|
+
jac_cols += parent_cols
|
|
357
|
+
jac_data += np.full_like(parent_cols, g, dtype=float).tolist()
|
|
358
|
+
# Raised when link atom is not coupled to layer above, but
|
|
359
|
+
# to a layer higher above.
|
|
360
|
+
except ValueError:
|
|
361
|
+
pass
|
|
362
|
+
J = sparse.csr_matrix((jac_data, (jac_rows, jac_cols)), shape=jac_shape)
|
|
363
|
+
|
|
364
|
+
return J
|
|
365
|
+
|
|
366
|
+
def get_energy(
|
|
367
|
+
self, atoms, coords, point_charges=None, parent_correction=True, cap=True
|
|
368
|
+
):
|
|
369
|
+
self.log("Energy calculation")
|
|
370
|
+
if cap:
|
|
371
|
+
catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
372
|
+
else:
|
|
373
|
+
catoms = atoms
|
|
374
|
+
ccoords = coords
|
|
375
|
+
|
|
376
|
+
prepare_kwargs = {
|
|
377
|
+
"point_charges": point_charges,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
self.log("Calculation at layer level")
|
|
381
|
+
results = self.calc.get_energy(catoms, ccoords, **prepare_kwargs)
|
|
382
|
+
energy = results["energy"]
|
|
383
|
+
|
|
384
|
+
# Calculate correction if parent layer is present and it is requested
|
|
385
|
+
if (self.parent_calc is not None) and parent_correction:
|
|
386
|
+
self.log("Calculation at parent layer level")
|
|
387
|
+
parent_results = self.parent_calc.get_energy(
|
|
388
|
+
catoms, ccoords, **prepare_kwargs
|
|
389
|
+
)
|
|
390
|
+
parent_energy = parent_results["energy"]
|
|
391
|
+
energy -= parent_energy
|
|
392
|
+
elif not parent_correction:
|
|
393
|
+
self.log("No parent correction!")
|
|
394
|
+
|
|
395
|
+
return energy
|
|
396
|
+
|
|
397
|
+
def get_forces(
|
|
398
|
+
self, atoms, coords, point_charges=None, parent_correction=True, cap=True
|
|
399
|
+
):
|
|
400
|
+
self.log("Force calculation")
|
|
401
|
+
# catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
402
|
+
if cap:
|
|
403
|
+
catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
404
|
+
else:
|
|
405
|
+
catoms = atoms
|
|
406
|
+
ccoords = coords
|
|
407
|
+
|
|
408
|
+
prepare_kwargs = {
|
|
409
|
+
"point_charges": point_charges,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
self.log("Calculation at layer level")
|
|
413
|
+
results = self.calc.get_forces(catoms, ccoords, **prepare_kwargs)
|
|
414
|
+
# These forces can contain contributions from link atoms.
|
|
415
|
+
forces = results["forces"]
|
|
416
|
+
energy = results["energy"]
|
|
417
|
+
# Redistribute link atom forces onto the two link atom hosts using the
|
|
418
|
+
# Jacobian J. Afterwards, the forces will have the shape of the parent-forces.
|
|
419
|
+
if self.J is not None:
|
|
420
|
+
# forces = forces.dot(self.J)
|
|
421
|
+
# f^T J = (J^T f)^T
|
|
422
|
+
# The transpose of the term in brackets can be ignored here, as numpy
|
|
423
|
+
# does not distinguish between f and f^T for a 1d-array.
|
|
424
|
+
forces = self.J.T @ forces
|
|
425
|
+
|
|
426
|
+
# Calculate correction if parent layer is present and it is requested
|
|
427
|
+
if (self.parent_calc is not None) and parent_correction:
|
|
428
|
+
self.log("Calculation at parent layer level")
|
|
429
|
+
parent_results = self.parent_calc.get_forces(
|
|
430
|
+
catoms, ccoords, **prepare_kwargs
|
|
431
|
+
)
|
|
432
|
+
parent_forces = parent_results["forces"]
|
|
433
|
+
parent_energy = parent_results["energy"]
|
|
434
|
+
|
|
435
|
+
# Correct energy and forces
|
|
436
|
+
energy -= parent_energy
|
|
437
|
+
forces -= self.J.T @ parent_forces
|
|
438
|
+
elif not parent_correction:
|
|
439
|
+
self.log("No parent correction!")
|
|
440
|
+
|
|
441
|
+
return energy, forces
|
|
442
|
+
|
|
443
|
+
def get_hessian(
|
|
444
|
+
self, atoms, coords, point_charges=None, parent_correction=True, cap=True
|
|
445
|
+
):
|
|
446
|
+
self.log("Hessian calculation")
|
|
447
|
+
# catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
448
|
+
if cap:
|
|
449
|
+
catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
450
|
+
else:
|
|
451
|
+
catoms = atoms
|
|
452
|
+
ccoords = coords
|
|
453
|
+
|
|
454
|
+
prepare_kwargs = {
|
|
455
|
+
"point_charges": point_charges,
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
self.log("Calculation at layer level")
|
|
459
|
+
# results = self.calc.get_hessian(catoms, ccoords, prepare_kwargs)
|
|
460
|
+
results = self.calc.get_hessian(catoms, ccoords, **prepare_kwargs)
|
|
461
|
+
hessian = results["hessian"]
|
|
462
|
+
energy = results["energy"]
|
|
463
|
+
if self.J is not None:
|
|
464
|
+
# hessian = self.J.T.dot(hessian.dot(self.J))
|
|
465
|
+
hessian = (self.J.T @ hessian) @ self.J
|
|
466
|
+
|
|
467
|
+
# Calculate correction if parent layer is present and it is requested
|
|
468
|
+
if (self.parent_calc is not None) and parent_correction:
|
|
469
|
+
self.log("Calculation at parent layer level")
|
|
470
|
+
parent_results = self.parent_calc.get_hessian(
|
|
471
|
+
catoms, ccoords, **prepare_kwargs
|
|
472
|
+
)
|
|
473
|
+
parent_hessian = parent_results["hessian"]
|
|
474
|
+
parent_energy = parent_results["energy"]
|
|
475
|
+
|
|
476
|
+
# Correct energy and hessian
|
|
477
|
+
energy -= parent_energy
|
|
478
|
+
# hessian -= self.J.T.dot(parent_hessian.dot(self.J))
|
|
479
|
+
hessian -= (self.J.T @ parent_hessian) @ self.J
|
|
480
|
+
elif not parent_correction:
|
|
481
|
+
self.log("No parent correction!")
|
|
482
|
+
|
|
483
|
+
return energy, hessian
|
|
484
|
+
|
|
485
|
+
# def get_delta_S(self, atoms, coords):
|
|
486
|
+
# self.log("ΔS calculation")
|
|
487
|
+
# catoms, ccoords = self.capped_atoms_coords(atoms, coords)
|
|
488
|
+
|
|
489
|
+
# # Parent calculator
|
|
490
|
+
# E_parent_real = self.parent_calc.get_energy(atoms, coords)["energy"]
|
|
491
|
+
# self.parent_calc.reset()
|
|
492
|
+
# E_parent_model = self.parent_calc.get_energy(catoms, ccoords)["energy"]
|
|
493
|
+
# S_low = E_parent_real - E_parent_model
|
|
494
|
+
# self.log(f"S_low={S_low:.6f} au")
|
|
495
|
+
# print(f"S_low={S_low:.6f} au")
|
|
496
|
+
# # High level calculator
|
|
497
|
+
# E_high_real = self.calc.get_energy(atoms, coords)["energy"]
|
|
498
|
+
# self.calc.reset()
|
|
499
|
+
# E_high_model = self.calc.get_energy(catoms, ccoords)["energy"]
|
|
500
|
+
# S_high = E_high_real - E_high_model
|
|
501
|
+
# self.log(f"S_high={S_high:.6f} au")
|
|
502
|
+
# print(f"S_high={S_high:.6f} au")
|
|
503
|
+
|
|
504
|
+
# delta_S = S_low - S_high
|
|
505
|
+
# self.log(f"ΔS={delta_S:.6f} au")
|
|
506
|
+
# print(f"ΔS={delta_S:.6f} au")
|
|
507
|
+
|
|
508
|
+
# return delta_S
|
|
509
|
+
|
|
510
|
+
def parse_charges(self):
|
|
511
|
+
charges = self.calc.parse_charges()
|
|
512
|
+
try:
|
|
513
|
+
parent_charges = self.parent_calc.parse_charges()
|
|
514
|
+
except AttributeError:
|
|
515
|
+
parent_charges = None
|
|
516
|
+
|
|
517
|
+
return charges, parent_charges
|
|
518
|
+
|
|
519
|
+
def as_geom(self, all_atoms, all_coords):
|
|
520
|
+
capped_atoms, capped_coords3d = self.capped_atoms_coords(all_atoms, all_coords)
|
|
521
|
+
geom = Geometry(capped_atoms, capped_coords3d)
|
|
522
|
+
dummy_calc = self.as_calculator()
|
|
523
|
+
geom.set_calculator(dummy_calc)
|
|
524
|
+
return geom
|
|
525
|
+
|
|
526
|
+
def as_calculator(self, cap=False):
|
|
527
|
+
return ModelDummyCalc(self, cap=cap)
|
|
528
|
+
|
|
529
|
+
def __str__(self):
|
|
530
|
+
# return (
|
|
531
|
+
# f"Model({self.name}, {len(self.atom_inds)} atoms, "
|
|
532
|
+
# f"level={self.calc_level}, parent_level={self.parent_calc_level})"
|
|
533
|
+
# )
|
|
534
|
+
return f"{self.name}_{self.calc_level}"
|
|
535
|
+
|
|
536
|
+
def __repr__(self):
|
|
537
|
+
return self.__str__()
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def get_embedding_charges(embedding, layer, parent_layer, coords3d):
|
|
541
|
+
# Only consider charges that belong to atoms in the parent
|
|
542
|
+
# layer. Otherwise this would result in additonal charges at
|
|
543
|
+
# the same positions as the atoms we would like to calculate.
|
|
544
|
+
if "electronic" in embedding:
|
|
545
|
+
assert (
|
|
546
|
+
len(parent_layer) == 1
|
|
547
|
+
), "Multicenter ONIOM in intermediate layer is not supported!"
|
|
548
|
+
parent_model = parent_layer[0]
|
|
549
|
+
parent_inds = parent_model.atom_inds
|
|
550
|
+
point_charges, _ = parent_model.parse_charges()
|
|
551
|
+
|
|
552
|
+
layer_inds = set(*it.chain([model.atom_inds for model in layer]))
|
|
553
|
+
# Determine indices of atoms that are in the parent layer, but
|
|
554
|
+
# not in the current layer
|
|
555
|
+
only_parent_inds = list(set(parent_inds) - layer_inds)
|
|
556
|
+
|
|
557
|
+
del_charge_inds = list()
|
|
558
|
+
all_redist_coords_charges = list()
|
|
559
|
+
# Here, redistributed and scaled charges are calculated. In the EE-RC and EE-RCD
|
|
560
|
+
# schemes the link atom parent (LAP) charges are divided by the number of bonds
|
|
561
|
+
# connected to the LAP minus 1. They are put halfway along theses bonds.
|
|
562
|
+
# See [5] for a discussion.
|
|
563
|
+
# EE-RC and EE-RCD are very similar. The block below handles calculations that
|
|
564
|
+
# are common to both methods, e.g. calculation of the redistributed charges and
|
|
565
|
+
# their coordinates.
|
|
566
|
+
#
|
|
567
|
+
# This will be executed for 'electronic_rc' and 'electronic_rcd'
|
|
568
|
+
if "electronic_rc" in embedding:
|
|
569
|
+
# Collect charges for models in a layer, e.g., for multicenter ONIOM.
|
|
570
|
+
for model in layer:
|
|
571
|
+
redist_coords_charges = list()
|
|
572
|
+
single_redist_charges = list()
|
|
573
|
+
# Determine bonds, connected to link parent.
|
|
574
|
+
link_host_bond_vecs, bonded_inds = model.get_bond_vecs(
|
|
575
|
+
coords3d, return_bonded_inds=True
|
|
576
|
+
)
|
|
577
|
+
# Determine link atoms
|
|
578
|
+
links = model.links
|
|
579
|
+
for link, bond_vecs in zip(links, link_host_bond_vecs):
|
|
580
|
+
parent_ind = link.parent_ind
|
|
581
|
+
# Presence of a link atom implies a bond.
|
|
582
|
+
assert len(bond_vecs) > 0
|
|
583
|
+
# *parent_coords, parent_charge = point_charges[link.parent_ind]
|
|
584
|
+
# parent_charge = ee_charges[parent_ind]
|
|
585
|
+
parent_charge = point_charges[parent_ind]
|
|
586
|
+
parent_coords = coords3d[parent_ind]
|
|
587
|
+
bond_num = len(bond_vecs)
|
|
588
|
+
redist_charge = parent_charge / bond_num
|
|
589
|
+
single_redist_charges.append(redist_charge)
|
|
590
|
+
# Put modified charges halfway on the bonds
|
|
591
|
+
redist_coords = parent_coords + bond_vecs / 2
|
|
592
|
+
redist_coords_charges.extend(
|
|
593
|
+
[(*coords, redist_charge) for coords in redist_coords]
|
|
594
|
+
)
|
|
595
|
+
del_charge_inds.append(parent_ind)
|
|
596
|
+
redist_coords_charges = np.array(redist_coords_charges)
|
|
597
|
+
|
|
598
|
+
# Redistributed charges and dipoles to preserve the M1-M2 bond dipoles. See [5].
|
|
599
|
+
if embedding == "electronic_rcd":
|
|
600
|
+
# Multiply all redistributed charges by 2
|
|
601
|
+
redist_coords_charges[:, -1] *= 2
|
|
602
|
+
# Substract original redistributed charge from M2 charges
|
|
603
|
+
for binds, src in zip(bonded_inds, single_redist_charges):
|
|
604
|
+
point_charges[binds] -= src
|
|
605
|
+
|
|
606
|
+
# Gather redistributed charges of separate models (centers)
|
|
607
|
+
all_redist_coords_charges.extend(redist_coords_charges)
|
|
608
|
+
|
|
609
|
+
assert len(del_charge_inds) == len(set(del_charge_inds)), (
|
|
610
|
+
"It seems that one parent hosts multiple link atoms. I did not think about "
|
|
611
|
+
"cases like that yet!"
|
|
612
|
+
)
|
|
613
|
+
# Only keep charges that are not on link atom hosts/parents
|
|
614
|
+
keep_mask = [opi for opi in only_parent_inds if opi not in del_charge_inds]
|
|
615
|
+
kept_point_charges = point_charges[keep_mask]
|
|
616
|
+
kept_coords3d = coords3d[keep_mask]
|
|
617
|
+
kept_coords_point_charges = np.concatenate(
|
|
618
|
+
(kept_coords3d, kept_point_charges[:, None]), axis=1
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# Join unmodified charges and redistributed charges
|
|
622
|
+
if len(all_redist_coords_charges) > 0:
|
|
623
|
+
kept_coords_point_charges = np.concatenate(
|
|
624
|
+
(kept_coords_point_charges, all_redist_coords_charges), axis=0
|
|
625
|
+
)
|
|
626
|
+
return kept_coords_point_charges
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class LayerCalc:
|
|
630
|
+
def __init__(self, models, total_size, parent_layer_calc=None):
|
|
631
|
+
self.models = models
|
|
632
|
+
self.parent_layer_calc = parent_layer_calc
|
|
633
|
+
self.total_size = total_size # atoms in the total system
|
|
634
|
+
|
|
635
|
+
self.models_str = ", ".join([str(model) for model in self.models])
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def mult(self):
|
|
639
|
+
mults = [model.calc.mult for model in self.models]
|
|
640
|
+
mult0 = mults[0]
|
|
641
|
+
assert all([mult == mult0 for mult in mults])
|
|
642
|
+
return mult0
|
|
643
|
+
|
|
644
|
+
@mult.setter
|
|
645
|
+
def mult(self, mult):
|
|
646
|
+
for model in self.models:
|
|
647
|
+
model.calc.mult = mult
|
|
648
|
+
|
|
649
|
+
@property
|
|
650
|
+
def charge(self):
|
|
651
|
+
charges = [model.calc.mult for model in self.models]
|
|
652
|
+
charge0 = charges[0]
|
|
653
|
+
assert all([charge == charge0 for charge in charges])
|
|
654
|
+
return charge0
|
|
655
|
+
|
|
656
|
+
@charge.setter
|
|
657
|
+
def charge(self, charge):
|
|
658
|
+
for model in self.models:
|
|
659
|
+
model.calc.charge = charge
|
|
660
|
+
|
|
661
|
+
def run_calculations(self, atoms, coords, method):
|
|
662
|
+
results = [getattr(model, method)(atoms, coords) for model in self.models]
|
|
663
|
+
return results
|
|
664
|
+
|
|
665
|
+
@staticmethod
|
|
666
|
+
def energy_from_results(model_energies, parent_energy=None):
|
|
667
|
+
energy = sum(model_energies)
|
|
668
|
+
if parent_energy is not None:
|
|
669
|
+
energy += parent_energy
|
|
670
|
+
return energy
|
|
671
|
+
|
|
672
|
+
def do_parent(self, with_parent):
|
|
673
|
+
return (self.parent_layer_calc is not None) and with_parent
|
|
674
|
+
|
|
675
|
+
def get_energy(self, atoms, coords, with_parent=True):
|
|
676
|
+
model_energies = self.run_calculations(atoms, coords, "get_energy")
|
|
677
|
+
if self.do_parent(with_parent):
|
|
678
|
+
parent_result = self.parent_layer_calc.get_energy(
|
|
679
|
+
atoms, coords, with_parent=True
|
|
680
|
+
)
|
|
681
|
+
parent_energy = parent_result["energy"]
|
|
682
|
+
else:
|
|
683
|
+
parent_energy = None
|
|
684
|
+
energy = self.energy_from_results(model_energies, parent_energy)
|
|
685
|
+
return {"energy": energy}
|
|
686
|
+
|
|
687
|
+
def get_forces(self, atoms, coords, with_parent=True):
|
|
688
|
+
full_forces = np.zeros((self.total_size, 3))
|
|
689
|
+
model_energies = list()
|
|
690
|
+
for model in self.models:
|
|
691
|
+
model_energy, model_forces = model.get_forces(
|
|
692
|
+
atoms, coords, parent_correction=True
|
|
693
|
+
)
|
|
694
|
+
model_energies.append(model_energy)
|
|
695
|
+
full_forces[model.parent_atom_inds] = model_forces.reshape(-1, 3)
|
|
696
|
+
forces = full_forces.flatten()
|
|
697
|
+
if self.do_parent(with_parent):
|
|
698
|
+
parent_result = self.parent_layer_calc.get_forces(
|
|
699
|
+
atoms, coords, with_parent=True
|
|
700
|
+
)
|
|
701
|
+
parent_energy = parent_result["energy"]
|
|
702
|
+
parent_forces = parent_result["forces"]
|
|
703
|
+
forces += parent_forces
|
|
704
|
+
else:
|
|
705
|
+
parent_energy = None
|
|
706
|
+
energy = self.energy_from_results(model_energies, parent_energy)
|
|
707
|
+
results = {
|
|
708
|
+
"energy": energy,
|
|
709
|
+
"forces": forces,
|
|
710
|
+
}
|
|
711
|
+
return results
|
|
712
|
+
|
|
713
|
+
def get_hessian(self, atoms, coords, with_parent=True):
|
|
714
|
+
hessian = np.zeros((self.total_size*3, self.total_size*3))
|
|
715
|
+
model_energies = list()
|
|
716
|
+
for model in self.models:
|
|
717
|
+
model_energy, model_hessian = model.get_hessian(
|
|
718
|
+
atoms, coords, parent_correction=True
|
|
719
|
+
)
|
|
720
|
+
model_energies.append(model_energy)
|
|
721
|
+
try:
|
|
722
|
+
inds = 3*model.parent_atom_inds[:, None] + np.arange(3)
|
|
723
|
+
hessian[inds, inds] += model_hessian
|
|
724
|
+
except TypeError:
|
|
725
|
+
hessian += model_hessian
|
|
726
|
+
if self.do_parent(with_parent):
|
|
727
|
+
parent_result = self.parent_layer_calc.get_hessian(
|
|
728
|
+
atoms, coords, with_parent=True
|
|
729
|
+
)
|
|
730
|
+
parent_energy = parent_result["energy"]
|
|
731
|
+
parent_hessian = parent_result["hessian"]
|
|
732
|
+
hessian += parent_hessian
|
|
733
|
+
else:
|
|
734
|
+
parent_energy = None
|
|
735
|
+
energy = self.energy_from_results(model_energies, parent_energy)
|
|
736
|
+
result = {
|
|
737
|
+
"energy": energy,
|
|
738
|
+
"hessian": hessian,
|
|
739
|
+
}
|
|
740
|
+
return result
|
|
741
|
+
|
|
742
|
+
def __str__(self):
|
|
743
|
+
return f"LayerCalc({self.models_str})"
|
|
744
|
+
|
|
745
|
+
def __repr__(self):
|
|
746
|
+
return self.__str__()
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class ONIOM(Calculator):
|
|
750
|
+
embeddings = {
|
|
751
|
+
"": "",
|
|
752
|
+
"electronic": "Electronic embedding",
|
|
753
|
+
"electronic_rc": "Electronic embedding with redistributed charges",
|
|
754
|
+
"electronic_rcd": "Electronic embedding with redistributed charges and dipoles",
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
def __init__(
|
|
758
|
+
self,
|
|
759
|
+
calcs,
|
|
760
|
+
models,
|
|
761
|
+
geom,
|
|
762
|
+
layers=None,
|
|
763
|
+
embedding="",
|
|
764
|
+
real_key="real",
|
|
765
|
+
use_link_atoms=True,
|
|
766
|
+
*args,
|
|
767
|
+
**kwargs,
|
|
768
|
+
):
|
|
769
|
+
"""
|
|
770
|
+
layer: list of models
|
|
771
|
+
len(layer) == 1: normal ONIOM, len(layer) >= 1: multicenter ONIOM.
|
|
772
|
+
model:
|
|
773
|
+
(sub)set of all atoms that resides in a certain layer and has
|
|
774
|
+
a certain calculator.
|
|
775
|
+
"""
|
|
776
|
+
|
|
777
|
+
super().__init__(*args, **kwargs)
|
|
778
|
+
|
|
779
|
+
if embedding is None:
|
|
780
|
+
embedding = ""
|
|
781
|
+
assert (
|
|
782
|
+
embedding in self.embeddings.keys()
|
|
783
|
+
), f"Valid embeddings are: {self.embeddings.keys()}"
|
|
784
|
+
self.embedding = embedding
|
|
785
|
+
|
|
786
|
+
assert real_key not in models, f'"{real_key}" must not be defined in "models"!'
|
|
787
|
+
assert real_key in calcs, f'"{real_key}" must be defined in "calcs"!'
|
|
788
|
+
|
|
789
|
+
self.use_link_atoms = use_link_atoms
|
|
790
|
+
|
|
791
|
+
# Expand index-lists in models
|
|
792
|
+
for model in models.values():
|
|
793
|
+
if ".." in model["inds"]:
|
|
794
|
+
model["inds"] = full_expand(model["inds"])
|
|
795
|
+
|
|
796
|
+
# When no ordering of layers is given we try to guess it from
|
|
797
|
+
# the size of the respective models. It's probably a better idea
|
|
798
|
+
# to always specify the layer ordering though ;)
|
|
799
|
+
if layers is None:
|
|
800
|
+
self.log(
|
|
801
|
+
"No explicit layer ordering specified! Determining layer "
|
|
802
|
+
"hierarchy from model sizes. This does not support multi-"
|
|
803
|
+
"center ONIOM!"
|
|
804
|
+
)
|
|
805
|
+
as_list = [(key, val) for key, val in models.items()]
|
|
806
|
+
# Determine hierarchy of models, from biggest to smallest model
|
|
807
|
+
layers = [
|
|
808
|
+
key
|
|
809
|
+
for key, val in sorted(
|
|
810
|
+
as_list, key=lambda model: -len(model[1]["inds"])
|
|
811
|
+
)
|
|
812
|
+
]
|
|
813
|
+
|
|
814
|
+
assert real_key not in layers, f'"{real_key}" must not be defined in "layers"!'
|
|
815
|
+
|
|
816
|
+
############
|
|
817
|
+
# #
|
|
818
|
+
# LAYERS #
|
|
819
|
+
# #
|
|
820
|
+
############
|
|
821
|
+
|
|
822
|
+
# Add real model and layer as they are missing right now. The real
|
|
823
|
+
# layer is always the last layer. The real layer is always calculated
|
|
824
|
+
# by the 'realkey'-calculator.
|
|
825
|
+
layers = [real_key] + layers
|
|
826
|
+
models[real_key] = {
|
|
827
|
+
"calc": real_key,
|
|
828
|
+
"inds": list(range(len(geom.atoms))),
|
|
829
|
+
}
|
|
830
|
+
self.log(f"Layer-ordering from big to small: {layers}")
|
|
831
|
+
|
|
832
|
+
# Single-model layers will be given as strings. As we also support
|
|
833
|
+
# multicenter-ONIOM there may also be layers that are given as lists
|
|
834
|
+
# that contain multiple models per layer.
|
|
835
|
+
# Now we convert the single-model layers to lists of length 1, so
|
|
836
|
+
# every layer is a list.
|
|
837
|
+
layers = [
|
|
838
|
+
[
|
|
839
|
+
layer,
|
|
840
|
+
]
|
|
841
|
+
if isinstance(layer, str)
|
|
842
|
+
else layer
|
|
843
|
+
for layer in layers
|
|
844
|
+
]
|
|
845
|
+
self.layer_num = len(layers)
|
|
846
|
+
assert self.layer_num > 1, "ONIOM with only 1 layer requested. Aborting!"
|
|
847
|
+
|
|
848
|
+
############
|
|
849
|
+
# #
|
|
850
|
+
# MODELS #
|
|
851
|
+
# #
|
|
852
|
+
############
|
|
853
|
+
|
|
854
|
+
# Create mapping between model and its parent layer. Actually
|
|
855
|
+
# this is a bit hacky right now, as the mapping should not be between
|
|
856
|
+
# model and parent layer, but between model and parent model.
|
|
857
|
+
# This way we expect the parent layer to have the same calculator
|
|
858
|
+
# throughout, so multicenter ONIOM with different calculators
|
|
859
|
+
# in all but the smallest layer (highest level) is not well defined.
|
|
860
|
+
#
|
|
861
|
+
# If multicenter ONIOM in an intermediate layer is useful may
|
|
862
|
+
# be another question to be answered ;).
|
|
863
|
+
self.model_parent_layers = dict()
|
|
864
|
+
for i, layer in enumerate(layers[1:]):
|
|
865
|
+
self.model_parent_layers.update({model: i for model in layer})
|
|
866
|
+
model_keys = list(it.chain(*layers))
|
|
867
|
+
|
|
868
|
+
cur_calc_num = 0
|
|
869
|
+
|
|
870
|
+
def get_calc(calc_key, base_name=None):
|
|
871
|
+
"""Helper function for easier generation of calculators
|
|
872
|
+
with incrementing calc_number."""
|
|
873
|
+
nonlocal cur_calc_num
|
|
874
|
+
|
|
875
|
+
kwargs = calcs[calc_key].copy()
|
|
876
|
+
type_ = kwargs.pop("type")
|
|
877
|
+
|
|
878
|
+
kwargs["calc_number"] = cur_calc_num
|
|
879
|
+
if base_name is not None:
|
|
880
|
+
kwargs["base_name"] = base_name
|
|
881
|
+
|
|
882
|
+
calc = CALC_DICT[type_](**kwargs)
|
|
883
|
+
cur_calc_num += 1
|
|
884
|
+
return calc
|
|
885
|
+
|
|
886
|
+
# Create models and required calculators.
|
|
887
|
+
self.models = list()
|
|
888
|
+
self.layers = [list() for _ in layers]
|
|
889
|
+
for model_key in model_keys:
|
|
890
|
+
model_calc_key = models[model_key]["calc"]
|
|
891
|
+
model_base_name = f"{model_key}_{model_calc_key}"
|
|
892
|
+
model_calc = get_calc(model_calc_key, base_name=model_base_name)
|
|
893
|
+
# Update parent information
|
|
894
|
+
try:
|
|
895
|
+
parent_layer_ind = self.model_parent_layers[model_key]
|
|
896
|
+
parent_layer = layers[parent_layer_ind]
|
|
897
|
+
parent_calc_keys = set(
|
|
898
|
+
[models[model_key]["calc"] for model_key in parent_layer]
|
|
899
|
+
)
|
|
900
|
+
assert len(parent_calc_keys) == 1, (
|
|
901
|
+
"It seems you are trying to run a multicenter ONIOM setup in "
|
|
902
|
+
"an intermediate layer with different calculators. This is "
|
|
903
|
+
"not supported right now."
|
|
904
|
+
)
|
|
905
|
+
parent_name = parent_layer[0]
|
|
906
|
+
parent_calc_key = models[parent_name]["calc"]
|
|
907
|
+
parent_base_name = f"{model}_parent"
|
|
908
|
+
parent_calc = get_calc(parent_calc_key, base_name=parent_base_name)
|
|
909
|
+
parent_atom_inds = models[parent_name]["inds"]
|
|
910
|
+
except KeyError:
|
|
911
|
+
parent_name = parent_calc_key = parent_calc = parent_atom_inds = None
|
|
912
|
+
parent_layer_ind = -1
|
|
913
|
+
|
|
914
|
+
model = Model(
|
|
915
|
+
name=model_key,
|
|
916
|
+
calc_level=model_calc_key,
|
|
917
|
+
calc=model_calc,
|
|
918
|
+
atom_inds=models[model_key]["inds"],
|
|
919
|
+
use_link_atoms=self.use_link_atoms,
|
|
920
|
+
#
|
|
921
|
+
parent_name=parent_name,
|
|
922
|
+
parent_calc_level=parent_calc_key,
|
|
923
|
+
parent_calc=parent_calc,
|
|
924
|
+
parent_atom_inds=parent_atom_inds,
|
|
925
|
+
)
|
|
926
|
+
self.models.append(model)
|
|
927
|
+
self.layers[parent_layer_ind + 1].append(model)
|
|
928
|
+
|
|
929
|
+
self.log("Created all ONIOM layers:")
|
|
930
|
+
for model in self.models:
|
|
931
|
+
self.log("\t" + str(model))
|
|
932
|
+
|
|
933
|
+
# Create link atoms
|
|
934
|
+
[model.create_links(geom.atoms, geom.cart_coords) for model in self.models]
|
|
935
|
+
# Create functions to calculate bond vectors with link atom hosts
|
|
936
|
+
[model.create_bond_vec_getters(geom.atoms) for model in self.models]
|
|
937
|
+
|
|
938
|
+
# And do a quick sanity check
|
|
939
|
+
assert (
|
|
940
|
+
len(self.models[0].links) == 0
|
|
941
|
+
), "There must not be any links in the 'real' layer!"
|
|
942
|
+
# Look for link atoms that appear in two adjacent layers. In such situations
|
|
943
|
+
# the higher layer is coupled to a layer two levels below. This may be a bad
|
|
944
|
+
# idea.
|
|
945
|
+
for i, (lower_model, model) in enumerate(
|
|
946
|
+
zip(self.models[:-1], self.models[1:])
|
|
947
|
+
):
|
|
948
|
+
lower_links = lower_model.links
|
|
949
|
+
links = model.links
|
|
950
|
+
same_links = [link for link in links if link in lower_links]
|
|
951
|
+
if same_links:
|
|
952
|
+
print(f"Found {len(same_links)} link(s) that appear(s) in two layers!")
|
|
953
|
+
for j, link in enumerate(same_links):
|
|
954
|
+
print(f"\t{j:02d}: {link}")
|
|
955
|
+
print(
|
|
956
|
+
f"Your current setup couples layer '{model.name}' to "
|
|
957
|
+
f"layer '{self.models[i-1].name}' two levels below! "
|
|
958
|
+
"This is probably a bad idea!"
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
self.log(
|
|
962
|
+
f"Created ONIOM calculator with {self.layer_num} layers and "
|
|
963
|
+
f"{len(self.models)} models."
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
self.layer_calcs = list()
|
|
967
|
+
for i, layer in enumerate(self.layers):
|
|
968
|
+
try:
|
|
969
|
+
parent_layer_calc = self.layer_calcs[-1]
|
|
970
|
+
except IndexError:
|
|
971
|
+
parent_layer_calc = None
|
|
972
|
+
layer_calc = LayerCalc(
|
|
973
|
+
models=layer,
|
|
974
|
+
total_size=len(geom.atoms),
|
|
975
|
+
parent_layer_calc=parent_layer_calc,
|
|
976
|
+
)
|
|
977
|
+
self.layer_calcs.append(layer_calc)
|
|
978
|
+
|
|
979
|
+
@property
|
|
980
|
+
def model_iter(self):
|
|
981
|
+
try:
|
|
982
|
+
# A layer may contain several models
|
|
983
|
+
model_iter = it.chain(*[layer for layer in self.layers])
|
|
984
|
+
except AttributeError:
|
|
985
|
+
model_iter = list()
|
|
986
|
+
return model_iter
|
|
987
|
+
|
|
988
|
+
@property
|
|
989
|
+
def mult(self):
|
|
990
|
+
mults = [model.calc.mult for model in self.model_iter]
|
|
991
|
+
mult0 = mults[0]
|
|
992
|
+
assert all([mult == mult0 for mult in mults])
|
|
993
|
+
return mult0
|
|
994
|
+
|
|
995
|
+
@mult.setter
|
|
996
|
+
def mult(self, mult):
|
|
997
|
+
for model in self.model_iter:
|
|
998
|
+
model.calc.mult = mult
|
|
999
|
+
|
|
1000
|
+
@property
|
|
1001
|
+
def charge(self):
|
|
1002
|
+
charges = [model.calc.charge for model in self.model_iter]
|
|
1003
|
+
charge0 = charges[0]
|
|
1004
|
+
return charge0
|
|
1005
|
+
|
|
1006
|
+
@charge.setter
|
|
1007
|
+
def charge(self, charge):
|
|
1008
|
+
for model in self.model_iter:
|
|
1009
|
+
model.calc.charge = charge
|
|
1010
|
+
|
|
1011
|
+
def run_calculations(self, atoms, coords, method):
|
|
1012
|
+
self.log(f"{self.embeddings[self.embedding]} ONIOM calculation")
|
|
1013
|
+
|
|
1014
|
+
all_results = list()
|
|
1015
|
+
for i, layer in enumerate(self.layers):
|
|
1016
|
+
point_charges = None
|
|
1017
|
+
# Calculate embedding charges, if required
|
|
1018
|
+
if self.embedding and (i > 0):
|
|
1019
|
+
parent_layer = self.layers[i - 1]
|
|
1020
|
+
coords3d = coords.reshape(-1, 3)
|
|
1021
|
+
point_charges = get_embedding_charges(
|
|
1022
|
+
self.embedding, layer, parent_layer, coords3d
|
|
1023
|
+
)
|
|
1024
|
+
self.log(
|
|
1025
|
+
f"Polarizing calculation in layer {i} ({layer}) by "
|
|
1026
|
+
f"charges from layer {i-1} ({self.layers[i-1]})."
|
|
1027
|
+
)
|
|
1028
|
+
ee_charge_sum = point_charges[:, -1].sum()
|
|
1029
|
+
self.log(f"sum(charges)={ee_charge_sum:.4f}")
|
|
1030
|
+
|
|
1031
|
+
# Enable for debugging
|
|
1032
|
+
# from pysisyphus.wrapper.jmol import render_geom_and_charges
|
|
1033
|
+
# if len(layer) == 1:
|
|
1034
|
+
# model = layer[0]
|
|
1035
|
+
# tmp_atoms, tmp_coords = model.capped_atoms_coords(atoms, coords)
|
|
1036
|
+
# render_geom_and_charges(
|
|
1037
|
+
# Geometry(tmp_atoms, tmp_coords), point_charges
|
|
1038
|
+
# )
|
|
1039
|
+
|
|
1040
|
+
results = [
|
|
1041
|
+
getattr(model, method)(atoms, coords, point_charges=point_charges)
|
|
1042
|
+
for model in layer
|
|
1043
|
+
]
|
|
1044
|
+
all_results.extend(results)
|
|
1045
|
+
|
|
1046
|
+
self.calc_counter += 1
|
|
1047
|
+
return all_results
|
|
1048
|
+
|
|
1049
|
+
def run_calculation(self, atoms, coords):
|
|
1050
|
+
self.log("run_calculation() called. Doing simple energy calculation!")
|
|
1051
|
+
return self.get_energy(atoms, coords)
|
|
1052
|
+
|
|
1053
|
+
def get_energy(self, atoms, coords):
|
|
1054
|
+
all_energies = self.run_calculations(atoms, coords, "get_energy")
|
|
1055
|
+
|
|
1056
|
+
energy = sum(all_energies)
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
"energy": energy,
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
def get_forces(self, atoms, coords):
|
|
1063
|
+
all_results = self.run_calculations(atoms, coords, "get_forces")
|
|
1064
|
+
|
|
1065
|
+
energies, forces_ = zip(*all_results)
|
|
1066
|
+
forces_ = [np.array(f).reshape(-1, 3) for f in forces_]
|
|
1067
|
+
energy = sum(energies)
|
|
1068
|
+
|
|
1069
|
+
forces = forces_[0] # first layer has shape of the total system
|
|
1070
|
+
for mdl, f in zip(self.models[1:], forces_[1:]):
|
|
1071
|
+
forces[mdl.parent_atom_inds] += f
|
|
1072
|
+
|
|
1073
|
+
return {
|
|
1074
|
+
"energy": energy,
|
|
1075
|
+
"forces": forces.flatten(),
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
def get_hessian(self, atoms, coords):
|
|
1079
|
+
all_results = self.run_calculations(atoms, coords, "get_hessian")
|
|
1080
|
+
|
|
1081
|
+
energies, hessians = zip(*all_results)
|
|
1082
|
+
energy = sum(energies)
|
|
1083
|
+
|
|
1084
|
+
hessian = hessians[0] # first layer has shape of the total system
|
|
1085
|
+
for mdl, h in zip(self.models[1:], hessians[1:]):
|
|
1086
|
+
inds = atom_inds_to_cart_inds(mdl.parent_atom_inds)
|
|
1087
|
+
# Keep in mind that we modify hessians[0] in place
|
|
1088
|
+
hessian[inds[:, None], inds[None, :]] += h
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
"energy": energy,
|
|
1092
|
+
"hessian": hessian,
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
def atom_inds_in_layer(self, index, exclude_inner=False):
|
|
1096
|
+
"""Returns list of atom indices in layer at index.
|
|
1097
|
+
|
|
1098
|
+
Atoms that also appear in inner layer can be excluded on request.
|
|
1099
|
+
|
|
1100
|
+
Parameters
|
|
1101
|
+
----------
|
|
1102
|
+
index : int
|
|
1103
|
+
pasd
|
|
1104
|
+
exclude_inner : bool, default=False, optional
|
|
1105
|
+
Whether to exclude atom indices that also appear in inner layers.
|
|
1106
|
+
|
|
1107
|
+
Returns
|
|
1108
|
+
-------
|
|
1109
|
+
atom_indices : list
|
|
1110
|
+
List containing the atom indices in the selected layer.
|
|
1111
|
+
"""
|
|
1112
|
+
|
|
1113
|
+
layer = self.layers[index]
|
|
1114
|
+
atom_inds = list(it.chain(*[model.atom_inds for model in layer]))
|
|
1115
|
+
if exclude_inner and (index < len(self.layers) - 1):
|
|
1116
|
+
lower_inds = self.atom_inds_in_layer(index + 1)
|
|
1117
|
+
# Drop indices that appear in inner layers
|
|
1118
|
+
atom_inds = [i for i in atom_inds if i not in lower_inds]
|
|
1119
|
+
return atom_inds
|
|
1120
|
+
|
|
1121
|
+
def get_layer_calc(self, layer_ind):
|
|
1122
|
+
return self.layer_calcs[layer_ind]
|
|
1123
|
+
|
|
1124
|
+
def calc_layer(self, atoms, coords, index, parent_correction=True):
|
|
1125
|
+
layer = self.layers[index]
|
|
1126
|
+
assert len(layer) == 1, "Multicenter not yet supported!"
|
|
1127
|
+
(model,) = layer
|
|
1128
|
+
result = model.get_forces(atoms, coords, parent_correction=parent_correction)
|
|
1129
|
+
return result
|