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.
Files changed (372) hide show
  1. hessian_ff/__init__.py +50 -0
  2. hessian_ff/analytical_hessian.py +609 -0
  3. hessian_ff/constants.py +46 -0
  4. hessian_ff/forcefield.py +339 -0
  5. hessian_ff/loaders.py +608 -0
  6. hessian_ff/native/Makefile +8 -0
  7. hessian_ff/native/__init__.py +28 -0
  8. hessian_ff/native/analytical_hessian.py +88 -0
  9. hessian_ff/native/analytical_hessian_ext.cpp +258 -0
  10. hessian_ff/native/bonded.py +82 -0
  11. hessian_ff/native/bonded_ext.cpp +640 -0
  12. hessian_ff/native/loader.py +349 -0
  13. hessian_ff/native/nonbonded.py +118 -0
  14. hessian_ff/native/nonbonded_ext.cpp +1150 -0
  15. hessian_ff/prmtop_parmed.py +23 -0
  16. hessian_ff/system.py +107 -0
  17. hessian_ff/terms/__init__.py +14 -0
  18. hessian_ff/terms/angle.py +73 -0
  19. hessian_ff/terms/bond.py +44 -0
  20. hessian_ff/terms/cmap.py +406 -0
  21. hessian_ff/terms/dihedral.py +141 -0
  22. hessian_ff/terms/nonbonded.py +209 -0
  23. hessian_ff/tests/__init__.py +0 -0
  24. hessian_ff/tests/conftest.py +75 -0
  25. hessian_ff/tests/data/small/complex.parm7 +1346 -0
  26. hessian_ff/tests/data/small/complex.pdb +125 -0
  27. hessian_ff/tests/data/small/complex.rst7 +63 -0
  28. hessian_ff/tests/test_coords_input.py +44 -0
  29. hessian_ff/tests/test_energy_force.py +49 -0
  30. hessian_ff/tests/test_hessian.py +137 -0
  31. hessian_ff/tests/test_smoke.py +18 -0
  32. hessian_ff/tests/test_validation.py +40 -0
  33. hessian_ff/workflows.py +889 -0
  34. mlmm/__init__.py +36 -0
  35. mlmm/__main__.py +7 -0
  36. mlmm/_version.py +34 -0
  37. mlmm/add_elem_info.py +374 -0
  38. mlmm/advanced_help.py +91 -0
  39. mlmm/align_freeze_atoms.py +601 -0
  40. mlmm/all.py +3535 -0
  41. mlmm/bond_changes.py +231 -0
  42. mlmm/bool_compat.py +223 -0
  43. mlmm/cli.py +574 -0
  44. mlmm/cli_utils.py +166 -0
  45. mlmm/default_group.py +337 -0
  46. mlmm/defaults.py +467 -0
  47. mlmm/define_layer.py +526 -0
  48. mlmm/dft.py +1041 -0
  49. mlmm/energy_diagram.py +253 -0
  50. mlmm/extract.py +2213 -0
  51. mlmm/fix_altloc.py +464 -0
  52. mlmm/freq.py +1406 -0
  53. mlmm/harmonic_constraints.py +140 -0
  54. mlmm/hessian_cache.py +44 -0
  55. mlmm/hessian_calc.py +174 -0
  56. mlmm/irc.py +638 -0
  57. mlmm/mlmm_calc.py +2262 -0
  58. mlmm/mm_parm.py +945 -0
  59. mlmm/oniom_export.py +1983 -0
  60. mlmm/oniom_import.py +457 -0
  61. mlmm/opt.py +1742 -0
  62. mlmm/path_opt.py +1353 -0
  63. mlmm/path_search.py +2299 -0
  64. mlmm/preflight.py +88 -0
  65. mlmm/py.typed +1 -0
  66. mlmm/pysis_runner.py +45 -0
  67. mlmm/scan.py +1047 -0
  68. mlmm/scan2d.py +1226 -0
  69. mlmm/scan3d.py +1265 -0
  70. mlmm/scan_common.py +184 -0
  71. mlmm/summary_log.py +736 -0
  72. mlmm/trj2fig.py +448 -0
  73. mlmm/tsopt.py +2871 -0
  74. mlmm/utils.py +2309 -0
  75. mlmm/xtb_embedcharge_correction.py +475 -0
  76. mlmm_toolkit-0.2.2.dev0.dist-info/METADATA +1159 -0
  77. mlmm_toolkit-0.2.2.dev0.dist-info/RECORD +372 -0
  78. mlmm_toolkit-0.2.2.dev0.dist-info/WHEEL +5 -0
  79. mlmm_toolkit-0.2.2.dev0.dist-info/entry_points.txt +2 -0
  80. mlmm_toolkit-0.2.2.dev0.dist-info/licenses/LICENSE +674 -0
  81. mlmm_toolkit-0.2.2.dev0.dist-info/top_level.txt +4 -0
  82. pysisyphus/Geometry.py +1667 -0
  83. pysisyphus/LICENSE +674 -0
  84. pysisyphus/TableFormatter.py +63 -0
  85. pysisyphus/TablePrinter.py +74 -0
  86. pysisyphus/__init__.py +12 -0
  87. pysisyphus/calculators/AFIR.py +452 -0
  88. pysisyphus/calculators/AnaPot.py +20 -0
  89. pysisyphus/calculators/AnaPot2.py +48 -0
  90. pysisyphus/calculators/AnaPot3.py +12 -0
  91. pysisyphus/calculators/AnaPot4.py +20 -0
  92. pysisyphus/calculators/AnaPotBase.py +337 -0
  93. pysisyphus/calculators/AnaPotCBM.py +25 -0
  94. pysisyphus/calculators/AtomAtomTransTorque.py +154 -0
  95. pysisyphus/calculators/CFOUR.py +250 -0
  96. pysisyphus/calculators/Calculator.py +844 -0
  97. pysisyphus/calculators/CerjanMiller.py +24 -0
  98. pysisyphus/calculators/Composite.py +123 -0
  99. pysisyphus/calculators/ConicalIntersection.py +171 -0
  100. pysisyphus/calculators/DFTBp.py +430 -0
  101. pysisyphus/calculators/DFTD3.py +66 -0
  102. pysisyphus/calculators/DFTD4.py +84 -0
  103. pysisyphus/calculators/Dalton.py +61 -0
  104. pysisyphus/calculators/Dimer.py +681 -0
  105. pysisyphus/calculators/Dummy.py +20 -0
  106. pysisyphus/calculators/EGO.py +76 -0
  107. pysisyphus/calculators/EnergyMin.py +224 -0
  108. pysisyphus/calculators/ExternalPotential.py +264 -0
  109. pysisyphus/calculators/FakeASE.py +35 -0
  110. pysisyphus/calculators/FourWellAnaPot.py +28 -0
  111. pysisyphus/calculators/FreeEndNEBPot.py +39 -0
  112. pysisyphus/calculators/Gaussian09.py +18 -0
  113. pysisyphus/calculators/Gaussian16.py +726 -0
  114. pysisyphus/calculators/HardSphere.py +159 -0
  115. pysisyphus/calculators/IDPPCalculator.py +49 -0
  116. pysisyphus/calculators/IPIClient.py +133 -0
  117. pysisyphus/calculators/IPIServer.py +234 -0
  118. pysisyphus/calculators/LEPSBase.py +24 -0
  119. pysisyphus/calculators/LEPSExpr.py +139 -0
  120. pysisyphus/calculators/LennardJones.py +80 -0
  121. pysisyphus/calculators/MOPAC.py +219 -0
  122. pysisyphus/calculators/MullerBrownSympyPot.py +51 -0
  123. pysisyphus/calculators/MultiCalc.py +85 -0
  124. pysisyphus/calculators/NFK.py +45 -0
  125. pysisyphus/calculators/OBabel.py +87 -0
  126. pysisyphus/calculators/ONIOMv2.py +1129 -0
  127. pysisyphus/calculators/ORCA.py +893 -0
  128. pysisyphus/calculators/ORCA5.py +6 -0
  129. pysisyphus/calculators/OpenMM.py +88 -0
  130. pysisyphus/calculators/OpenMolcas.py +281 -0
  131. pysisyphus/calculators/OverlapCalculator.py +908 -0
  132. pysisyphus/calculators/Psi4.py +218 -0
  133. pysisyphus/calculators/PyPsi4.py +37 -0
  134. pysisyphus/calculators/PySCF.py +341 -0
  135. pysisyphus/calculators/PyXTB.py +73 -0
  136. pysisyphus/calculators/QCEngine.py +106 -0
  137. pysisyphus/calculators/Rastrigin.py +22 -0
  138. pysisyphus/calculators/Remote.py +76 -0
  139. pysisyphus/calculators/Rosenbrock.py +15 -0
  140. pysisyphus/calculators/SocketCalc.py +97 -0
  141. pysisyphus/calculators/TIP3P.py +111 -0
  142. pysisyphus/calculators/TransTorque.py +161 -0
  143. pysisyphus/calculators/Turbomole.py +965 -0
  144. pysisyphus/calculators/VRIPot.py +37 -0
  145. pysisyphus/calculators/WFOWrapper.py +333 -0
  146. pysisyphus/calculators/WFOWrapper2.py +341 -0
  147. pysisyphus/calculators/XTB.py +418 -0
  148. pysisyphus/calculators/__init__.py +81 -0
  149. pysisyphus/calculators/cosmo_data.py +139 -0
  150. pysisyphus/calculators/parser.py +150 -0
  151. pysisyphus/color.py +19 -0
  152. pysisyphus/config.py +133 -0
  153. pysisyphus/constants.py +65 -0
  154. pysisyphus/cos/AdaptiveNEB.py +230 -0
  155. pysisyphus/cos/ChainOfStates.py +725 -0
  156. pysisyphus/cos/FreeEndNEB.py +25 -0
  157. pysisyphus/cos/FreezingString.py +103 -0
  158. pysisyphus/cos/GrowingChainOfStates.py +71 -0
  159. pysisyphus/cos/GrowingNT.py +309 -0
  160. pysisyphus/cos/GrowingString.py +508 -0
  161. pysisyphus/cos/NEB.py +189 -0
  162. pysisyphus/cos/SimpleZTS.py +64 -0
  163. pysisyphus/cos/__init__.py +22 -0
  164. pysisyphus/cos/stiffness.py +199 -0
  165. pysisyphus/drivers/__init__.py +17 -0
  166. pysisyphus/drivers/afir.py +855 -0
  167. pysisyphus/drivers/barriers.py +271 -0
  168. pysisyphus/drivers/birkholz.py +138 -0
  169. pysisyphus/drivers/cluster.py +318 -0
  170. pysisyphus/drivers/diabatization.py +133 -0
  171. pysisyphus/drivers/merge.py +368 -0
  172. pysisyphus/drivers/merge_mol2.py +322 -0
  173. pysisyphus/drivers/opt.py +375 -0
  174. pysisyphus/drivers/perf.py +91 -0
  175. pysisyphus/drivers/pka.py +52 -0
  176. pysisyphus/drivers/precon_pos_rot.py +669 -0
  177. pysisyphus/drivers/rates.py +480 -0
  178. pysisyphus/drivers/replace.py +219 -0
  179. pysisyphus/drivers/scan.py +212 -0
  180. pysisyphus/drivers/spectrum.py +166 -0
  181. pysisyphus/drivers/thermo.py +31 -0
  182. pysisyphus/dynamics/Gaussian.py +103 -0
  183. pysisyphus/dynamics/__init__.py +20 -0
  184. pysisyphus/dynamics/colvars.py +136 -0
  185. pysisyphus/dynamics/driver.py +297 -0
  186. pysisyphus/dynamics/helpers.py +256 -0
  187. pysisyphus/dynamics/lincs.py +105 -0
  188. pysisyphus/dynamics/mdp.py +364 -0
  189. pysisyphus/dynamics/rattle.py +121 -0
  190. pysisyphus/dynamics/thermostats.py +128 -0
  191. pysisyphus/dynamics/wigner.py +266 -0
  192. pysisyphus/elem_data.py +3473 -0
  193. pysisyphus/exceptions.py +2 -0
  194. pysisyphus/filtertrj.py +69 -0
  195. pysisyphus/helpers.py +623 -0
  196. pysisyphus/helpers_pure.py +649 -0
  197. pysisyphus/init_logging.py +50 -0
  198. pysisyphus/intcoords/Bend.py +69 -0
  199. pysisyphus/intcoords/Bend2.py +25 -0
  200. pysisyphus/intcoords/BondedFragment.py +32 -0
  201. pysisyphus/intcoords/Cartesian.py +41 -0
  202. pysisyphus/intcoords/CartesianCoords.py +140 -0
  203. pysisyphus/intcoords/Coords.py +56 -0
  204. pysisyphus/intcoords/DLC.py +197 -0
  205. pysisyphus/intcoords/DistanceFunction.py +34 -0
  206. pysisyphus/intcoords/DummyImproper.py +70 -0
  207. pysisyphus/intcoords/DummyTorsion.py +72 -0
  208. pysisyphus/intcoords/LinearBend.py +105 -0
  209. pysisyphus/intcoords/LinearDisplacement.py +80 -0
  210. pysisyphus/intcoords/OutOfPlane.py +59 -0
  211. pysisyphus/intcoords/PrimTypes.py +286 -0
  212. pysisyphus/intcoords/Primitive.py +137 -0
  213. pysisyphus/intcoords/RedundantCoords.py +659 -0
  214. pysisyphus/intcoords/RobustTorsion.py +59 -0
  215. pysisyphus/intcoords/Rotation.py +147 -0
  216. pysisyphus/intcoords/Stretch.py +31 -0
  217. pysisyphus/intcoords/Torsion.py +101 -0
  218. pysisyphus/intcoords/Torsion2.py +25 -0
  219. pysisyphus/intcoords/Translation.py +45 -0
  220. pysisyphus/intcoords/__init__.py +61 -0
  221. pysisyphus/intcoords/augment_bonds.py +126 -0
  222. pysisyphus/intcoords/derivatives.py +10512 -0
  223. pysisyphus/intcoords/eval.py +80 -0
  224. pysisyphus/intcoords/exceptions.py +37 -0
  225. pysisyphus/intcoords/findiffs.py +48 -0
  226. pysisyphus/intcoords/generate_derivatives.py +414 -0
  227. pysisyphus/intcoords/helpers.py +235 -0
  228. pysisyphus/intcoords/logging_conf.py +10 -0
  229. pysisyphus/intcoords/mp_derivatives.py +10836 -0
  230. pysisyphus/intcoords/setup.py +962 -0
  231. pysisyphus/intcoords/setup_fast.py +176 -0
  232. pysisyphus/intcoords/update.py +272 -0
  233. pysisyphus/intcoords/valid.py +89 -0
  234. pysisyphus/interpolate/Geodesic.py +93 -0
  235. pysisyphus/interpolate/IDPP.py +55 -0
  236. pysisyphus/interpolate/Interpolator.py +116 -0
  237. pysisyphus/interpolate/LST.py +70 -0
  238. pysisyphus/interpolate/Redund.py +152 -0
  239. pysisyphus/interpolate/__init__.py +9 -0
  240. pysisyphus/interpolate/helpers.py +34 -0
  241. pysisyphus/io/__init__.py +22 -0
  242. pysisyphus/io/aomix.py +178 -0
  243. pysisyphus/io/cjson.py +24 -0
  244. pysisyphus/io/crd.py +101 -0
  245. pysisyphus/io/cube.py +220 -0
  246. pysisyphus/io/fchk.py +184 -0
  247. pysisyphus/io/hdf5.py +49 -0
  248. pysisyphus/io/hessian.py +72 -0
  249. pysisyphus/io/mol2.py +146 -0
  250. pysisyphus/io/molden.py +293 -0
  251. pysisyphus/io/orca.py +189 -0
  252. pysisyphus/io/pdb.py +269 -0
  253. pysisyphus/io/psf.py +79 -0
  254. pysisyphus/io/pubchem.py +31 -0
  255. pysisyphus/io/qcschema.py +34 -0
  256. pysisyphus/io/sdf.py +29 -0
  257. pysisyphus/io/xyz.py +61 -0
  258. pysisyphus/io/zmat.py +175 -0
  259. pysisyphus/irc/DWI.py +108 -0
  260. pysisyphus/irc/DampedVelocityVerlet.py +134 -0
  261. pysisyphus/irc/Euler.py +22 -0
  262. pysisyphus/irc/EulerPC.py +345 -0
  263. pysisyphus/irc/GonzalezSchlegel.py +187 -0
  264. pysisyphus/irc/IMKMod.py +164 -0
  265. pysisyphus/irc/IRC.py +878 -0
  266. pysisyphus/irc/IRCDummy.py +10 -0
  267. pysisyphus/irc/Instanton.py +307 -0
  268. pysisyphus/irc/LQA.py +53 -0
  269. pysisyphus/irc/ModeKill.py +136 -0
  270. pysisyphus/irc/ParamPlot.py +53 -0
  271. pysisyphus/irc/RK4.py +36 -0
  272. pysisyphus/irc/__init__.py +31 -0
  273. pysisyphus/irc/initial_displ.py +219 -0
  274. pysisyphus/linalg.py +411 -0
  275. pysisyphus/line_searches/Backtracking.py +88 -0
  276. pysisyphus/line_searches/HagerZhang.py +184 -0
  277. pysisyphus/line_searches/LineSearch.py +232 -0
  278. pysisyphus/line_searches/StrongWolfe.py +108 -0
  279. pysisyphus/line_searches/__init__.py +9 -0
  280. pysisyphus/line_searches/interpol.py +15 -0
  281. pysisyphus/modefollow/NormalMode.py +40 -0
  282. pysisyphus/modefollow/__init__.py +10 -0
  283. pysisyphus/modefollow/davidson.py +199 -0
  284. pysisyphus/modefollow/lanczos.py +95 -0
  285. pysisyphus/optimizers/BFGS.py +99 -0
  286. pysisyphus/optimizers/BacktrackingOptimizer.py +113 -0
  287. pysisyphus/optimizers/ConjugateGradient.py +98 -0
  288. pysisyphus/optimizers/CubicNewton.py +75 -0
  289. pysisyphus/optimizers/FIRE.py +113 -0
  290. pysisyphus/optimizers/HessianOptimizer.py +1176 -0
  291. pysisyphus/optimizers/LBFGS.py +228 -0
  292. pysisyphus/optimizers/LayerOpt.py +411 -0
  293. pysisyphus/optimizers/MicroOptimizer.py +169 -0
  294. pysisyphus/optimizers/NCOptimizer.py +90 -0
  295. pysisyphus/optimizers/Optimizer.py +1084 -0
  296. pysisyphus/optimizers/PreconLBFGS.py +260 -0
  297. pysisyphus/optimizers/PreconSteepestDescent.py +7 -0
  298. pysisyphus/optimizers/QuickMin.py +74 -0
  299. pysisyphus/optimizers/RFOptimizer.py +181 -0
  300. pysisyphus/optimizers/RSA.py +99 -0
  301. pysisyphus/optimizers/StabilizedQNMethod.py +248 -0
  302. pysisyphus/optimizers/SteepestDescent.py +23 -0
  303. pysisyphus/optimizers/StringOptimizer.py +173 -0
  304. pysisyphus/optimizers/__init__.py +41 -0
  305. pysisyphus/optimizers/closures.py +301 -0
  306. pysisyphus/optimizers/cls_map.py +58 -0
  307. pysisyphus/optimizers/exceptions.py +6 -0
  308. pysisyphus/optimizers/gdiis.py +280 -0
  309. pysisyphus/optimizers/guess_hessians.py +311 -0
  310. pysisyphus/optimizers/hessian_updates.py +355 -0
  311. pysisyphus/optimizers/poly_fit.py +285 -0
  312. pysisyphus/optimizers/precon.py +153 -0
  313. pysisyphus/optimizers/restrict_step.py +24 -0
  314. pysisyphus/pack.py +172 -0
  315. pysisyphus/peakdetect.py +948 -0
  316. pysisyphus/plot.py +1031 -0
  317. pysisyphus/run.py +2106 -0
  318. pysisyphus/socket_helper.py +74 -0
  319. pysisyphus/stocastic/FragmentKick.py +132 -0
  320. pysisyphus/stocastic/Kick.py +81 -0
  321. pysisyphus/stocastic/Pipeline.py +303 -0
  322. pysisyphus/stocastic/__init__.py +21 -0
  323. pysisyphus/stocastic/align.py +127 -0
  324. pysisyphus/testing.py +96 -0
  325. pysisyphus/thermo.py +156 -0
  326. pysisyphus/trj.py +824 -0
  327. pysisyphus/tsoptimizers/RSIRFOptimizer.py +56 -0
  328. pysisyphus/tsoptimizers/RSPRFOptimizer.py +182 -0
  329. pysisyphus/tsoptimizers/TRIM.py +59 -0
  330. pysisyphus/tsoptimizers/TSHessianOptimizer.py +463 -0
  331. pysisyphus/tsoptimizers/__init__.py +23 -0
  332. pysisyphus/wavefunction/Basis.py +239 -0
  333. pysisyphus/wavefunction/DIIS.py +76 -0
  334. pysisyphus/wavefunction/__init__.py +25 -0
  335. pysisyphus/wavefunction/build_ext.py +42 -0
  336. pysisyphus/wavefunction/cart2sph.py +190 -0
  337. pysisyphus/wavefunction/diabatization.py +304 -0
  338. pysisyphus/wavefunction/excited_states.py +435 -0
  339. pysisyphus/wavefunction/gen_ints.py +1811 -0
  340. pysisyphus/wavefunction/helpers.py +104 -0
  341. pysisyphus/wavefunction/ints/__init__.py +0 -0
  342. pysisyphus/wavefunction/ints/boys.py +193 -0
  343. pysisyphus/wavefunction/ints/boys_table_N_64_xasym_27.1_step_0.01.npy +0 -0
  344. pysisyphus/wavefunction/ints/cart_gto3d.py +176 -0
  345. pysisyphus/wavefunction/ints/coulomb3d.py +25928 -0
  346. pysisyphus/wavefunction/ints/diag_quadrupole3d.py +10036 -0
  347. pysisyphus/wavefunction/ints/dipole3d.py +8762 -0
  348. pysisyphus/wavefunction/ints/int2c2e3d.py +7198 -0
  349. pysisyphus/wavefunction/ints/int3c2e3d_sph.py +65040 -0
  350. pysisyphus/wavefunction/ints/kinetic3d.py +8240 -0
  351. pysisyphus/wavefunction/ints/ovlp3d.py +3777 -0
  352. pysisyphus/wavefunction/ints/quadrupole3d.py +15054 -0
  353. pysisyphus/wavefunction/ints/self_ovlp3d.py +198 -0
  354. pysisyphus/wavefunction/localization.py +458 -0
  355. pysisyphus/wavefunction/multipole.py +159 -0
  356. pysisyphus/wavefunction/normalization.py +36 -0
  357. pysisyphus/wavefunction/pop_analysis.py +134 -0
  358. pysisyphus/wavefunction/shells.py +1171 -0
  359. pysisyphus/wavefunction/wavefunction.py +504 -0
  360. pysisyphus/wrapper/__init__.py +11 -0
  361. pysisyphus/wrapper/exceptions.py +2 -0
  362. pysisyphus/wrapper/jmol.py +120 -0
  363. pysisyphus/wrapper/mwfn.py +169 -0
  364. pysisyphus/wrapper/packmol.py +71 -0
  365. pysisyphus/xyzloader.py +168 -0
  366. pysisyphus/yaml_mods.py +45 -0
  367. thermoanalysis/LICENSE +674 -0
  368. thermoanalysis/QCData.py +244 -0
  369. thermoanalysis/__init__.py +0 -0
  370. thermoanalysis/config.py +3 -0
  371. thermoanalysis/constants.py +20 -0
  372. thermoanalysis/thermo.py +1011 -0
@@ -0,0 +1,601 @@
1
+ # mlmm/align_freeze_atoms.py
2
+
3
+ """
4
+ align_freeze_atoms — Rigid alignment and staged “scan + relaxation” utilities for pysisyphus Geometry objects
5
+ =====================================================================
6
+
7
+ Usage (API)
8
+ -----
9
+ from mlmm.align_freeze_atoms import (
10
+ align_and_refine_pair_inplace,
11
+ align_and_refine_sequence_inplace,
12
+ align_second_to_first_kabsch_inplace,
13
+ kabsch_R_t,
14
+ scan_freeze_atoms_toward_target_inplace,
15
+ )
16
+
17
+ Examples::
18
+ >>> from mlmm.align_freeze_atoms import align_and_refine_pair_inplace
19
+ >>> result = align_and_refine_pair_inplace(g_ref, g_mob, shared_calc=mlmm_calc)
20
+ >>> result["align"]["mode"]
21
+ 'kabsch'
22
+
23
+ Description
24
+ -----
25
+ API-only utilities to co-align and refine pre-optimized `pysisyphus.Geometry` objects—typically adjacent
26
+ images along a reaction path—using `freeze_atoms`. A rigid alignment is performed first (with special
27
+ handling when 1 or 2 atoms are frozen), followed by a staged “scan toward reference + local relaxation”
28
+ that moves the frozen atoms toward the reference in small steps while relaxing the surroundings.
29
+ All updates are applied in place on the mobile geometry.
30
+
31
+ The module now targets **ML/MM-only** workflows: a shared ML/MM calculator instance (created via
32
+ `mlmm.mlmm_calc.mlmm`) must already be attached to the geometries or supplied through the
33
+ `shared_calc` keyword.
34
+
35
+ Provided functionality (concise):
36
+ - kabsch_R_t(P, Q)
37
+ Row-vector Kabsch solver for two (N, 3) point sets. Returns (R, t) minimizing ||(Q @ R + t) − P||.
38
+ - align_second_to_first_kabsch_inplace(g_ref, g_mob, *, verbose=True)
39
+ Rigidly aligns `g_mob` to `g_ref` in place. Special cases:
40
+ - freeze_atoms length = 1: translate to match the anchor; restrict rotations about that anchor;
41
+ minimizes all-atom RMSD; RMSD reported on all atoms.
42
+ - freeze_atoms length = 2: align the axis defined by the two anchors; optimize rotation around
43
+ that axis; RMSD reported on all atoms.
44
+ - otherwise: Kabsch on the union of `freeze_atoms` from both geometries (or all atoms if empty);
45
+ apply the transform to all atoms; RMSD reported on the Kabsch selection.
46
+ Returns: dict(before_A, after_A, n_used, mode) with RMSD in Å and mode ∈ {"one_anchor","two_anchor","kabsch"}.
47
+ - scan_freeze_atoms_toward_target_inplace(
48
+ g_ref, g_mob, *, step_A=0.1, per_step_cycles=50, final_cycles=200, max_steps=1000,
49
+ shared_calc=None, out_dir=Path("./result_align_refine/"), thresh="gau", verbose=True)
50
+ Moves `g_mob.freeze_atoms` toward `g_ref` by `step_A` Å per iteration. At each step the frozen atoms
51
+ are held fixed and the surroundings are relaxed with LBFGS. When the maximum remaining distance is
52
+ below one step, enforce exact coincidence of the frozen atoms and run a finishing relaxation.
53
+ Returns: dict(max_remaining_A, n_steps, converged).
54
+ - align_and_refine_pair_inplace(
55
+ g_ref, g_mob, *, shared_calc=None, out_dir=Path("./result_align_refine/"),
56
+ step_A=0.1, per_step_cycles=50, final_cycles=200, max_steps=1000,
57
+ thresh="gau", verbose=True)
58
+ High-level pair API: (1) rigid alignment, then (2) scan + relaxation toward the reference.
59
+ Returns: {"align": {...}, "scan": {...}}.
60
+ - align_and_refine_sequence_inplace(
61
+ geoms, *, shared_calc=None, out_dir=Path("./result_align_refine/"),
62
+ step_A=0.1, per_step_cycles=1000, final_cycles=1000, max_steps=10000,
63
+ thresh="gau", verbose=True)
64
+ Applies the pair procedure along [g0, g1, g2, ...] as (g0←g1), (g1←g2), ... and returns a list of per-pair results.
65
+
66
+ Outputs (& Directory Layout)
67
+ -----
68
+ - Default base directory: `./result_align_refine/`
69
+ - Pair API: uses `out_dir/` for stepwise and final LBFGS runs.
70
+ - Sequence API: creates subdirectories `pair_00/`, `pair_01/`, ... under `out_dir/`, one per adjacent pair.
71
+ - Directories are created even when optimizer dumping is disabled; LBFGS is invoked with `dump=False`.
72
+
73
+ Notes:
74
+ -----
75
+ - Units & conventions:
76
+ - User-facing distances/thresholds are in Å; internal coordinates are in bohr (conversion via `BOHR2ANG`).
77
+ - Row-vector convention for rigid transforms: points `Q` are mapped as `Q @ R + t`.
78
+ - Indices in `freeze_atoms` are 0-based.
79
+ - Calculator handling:
80
+ - A shared ML/MM calculator must already be attached to each geometry (e.g., via `mlmm(...)`) or supplied through
81
+ `shared_calc`. UMA/link-atom fallbacks are not available.
82
+ - Rigid alignment priority and RMSD reporting:
83
+ - freeze=1 → rotations about the single anchor only; RMSD minimized and reported on all atoms.
84
+ - freeze=2 → align anchor-defined axis; optimize rotation around that axis; RMSD reported on all atoms.
85
+ - else → Kabsch on the union of `freeze_atoms` (or all atoms if empty); transform applied to all atoms;
86
+ RMSD reported on the same selection used by Kabsch.
87
+ - Scan + relaxation details:
88
+ - At each step: move frozen atoms by `step_A` Å toward reference, hold them fixed, run a short LBFGS on the surroundings.
89
+ - Finalization: enforce exact coincidence of frozen atoms, then run a finishing LBFGS.
90
+ - The original `freeze_atoms` of `g_mob` is restored after the scan, even on exceptions.
91
+ - Error handling:
92
+ - `LBFGS` exceptions (`ZeroStepLength`, `OptimizationError`) are caught and logged; the procedure continues when reasonable.
93
+ - Internals:
94
+ - Uses Rodrigues-based rotations, vector-alignment, and planar projection helpers to implement axis-constrained motions.
95
+ """
96
+
97
+
98
+ from __future__ import annotations
99
+
100
+ from pathlib import Path
101
+ from typing import List, Optional, Sequence, Tuple, Dict, Any
102
+
103
+ import click
104
+ import numpy as np
105
+
106
+ # pysisyphus
107
+ from pysisyphus.optimizers.LBFGS import LBFGS
108
+ from pysisyphus.optimizers.exceptions import OptimizationError, ZeroStepLength
109
+ from pysisyphus.constants import BOHR2ANG
110
+
111
+
112
+ # =============================================================================
113
+ # Math utilities (row-vector convention: Q @ R + t)
114
+ # =============================================================================
115
+
116
+ def kabsch_R_t(P: np.ndarray, Q: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
117
+ """
118
+ Row-vector Kabsch: find R, t minimizing ||(Q @ R + t) - P|| for (N, 3).
119
+ """
120
+ P = np.asarray(P, float)
121
+ Q = np.asarray(Q, float)
122
+ if P.shape != Q.shape or P.ndim != 2 or P.shape[1] != 3:
123
+ raise ValueError("Kabsch expects P, Q with shape (N, 3).")
124
+ mu_P, mu_Q = P.mean(0), Q.mean(0)
125
+ Pc, Qc = P - mu_P, Q - mu_Q
126
+ H = Pc.T @ Qc
127
+ U, _, Vt = np.linalg.svd(H)
128
+ R = Vt.T @ U.T
129
+ if np.linalg.det(R) < 0.0:
130
+ Vt[-1] *= -1.0
131
+ R = Vt.T @ U.T
132
+ t = mu_P - mu_Q @ R
133
+ return R, t
134
+
135
+
136
+ def _rodrigues(axis_unit: np.ndarray, theta: float) -> np.ndarray:
137
+ """
138
+ 3×3 rotation matrix for a rotation of angle `theta` about `axis_unit`.
139
+ """
140
+ u = np.asarray(axis_unit, float)
141
+ n = np.linalg.norm(u)
142
+ if n < 1e-16:
143
+ return np.eye(3)
144
+ u /= n
145
+ ux, uy, uz = u
146
+ K = np.array([[0.0, -uz, uy],
147
+ [uz, 0.0, -ux],
148
+ [-uy, ux, 0.0]], float)
149
+ I = np.eye(3)
150
+ return I + np.sin(theta) * K + (1.0 - np.cos(theta)) * (K @ K)
151
+
152
+
153
+ def _rotation_align_vectors(a: np.ndarray, b: np.ndarray) -> np.ndarray:
154
+ """
155
+ 3×3 rotation (column-vector convention) that maps vector `a` to `b`.
156
+ When applying to row-vectors, use the transpose of the returned matrix.
157
+ """
158
+ a = np.asarray(a, float)
159
+ b = np.asarray(b, float)
160
+ an = np.linalg.norm(a)
161
+ bn = np.linalg.norm(b)
162
+ if an < 1e-16 or bn < 1e-16:
163
+ return np.eye(3)
164
+ a /= an
165
+ b /= bn
166
+ v = np.cross(a, b)
167
+ c = float(np.clip(a.dot(b), -1.0, 1.0))
168
+ s = np.linalg.norm(v)
169
+ if s < 1e-12:
170
+ if c > 0.0:
171
+ return np.eye(3)
172
+ # 180°: choose any axis orthogonal to `a`
173
+ tmp = np.array([1.0, 0.0, 0.0]) if abs(a[0]) <= 0.9 else np.array([0.0, 1.0, 0.0])
174
+ axis = np.cross(a, tmp)
175
+ axis /= (np.linalg.norm(axis) + 1e-16)
176
+ return _rodrigues(axis, np.pi)
177
+ axis = v / s
178
+ theta = np.arctan2(s, c)
179
+ return _rodrigues(axis, theta)
180
+
181
+
182
+ def _orth_proj_perp(u: np.ndarray) -> np.ndarray:
183
+ """
184
+ 3×3 projector onto the plane perpendicular to vector `u`.
185
+ """
186
+ u = np.asarray(u, float)
187
+ n = np.linalg.norm(u)
188
+ if n < 1e-16:
189
+ return np.eye(3)
190
+ u = u / n
191
+ return np.eye(3) - np.outer(u, u)
192
+
193
+
194
+ # =============================================================================
195
+ # Geometry utilities
196
+ # =============================================================================
197
+
198
+ def _coords3d(geom) -> np.ndarray:
199
+ """
200
+ Return (N, 3) coordinates in bohr (float).
201
+ """
202
+ return np.array(geom.coords3d, float)
203
+
204
+
205
+ def _rmsd(A: np.ndarray, B: np.ndarray) -> float:
206
+ """
207
+ Compute RMSD in Å (inputs are in bohr; conversion is applied internally).
208
+ """
209
+ A = np.asarray(A, float) * BOHR2ANG
210
+ B = np.asarray(B, float) * BOHR2ANG
211
+ return float(np.sqrt(np.mean(np.sum((A - B) ** 2, axis=1)))) if len(A) else float("nan")
212
+
213
+
214
+ def _set_all_coords_disabling_freeze(geom, coords3d_bohr: np.ndarray) -> None:
215
+ """
216
+ Temporarily clear `freeze_atoms`, set all coordinates (bohr), then restore.
217
+ """
218
+ old = np.array(getattr(geom, "freeze_atoms", []), int)
219
+ try:
220
+ geom.freeze_atoms = np.array([], int)
221
+ geom.set_coords(np.asarray(coords3d_bohr, float).reshape(-1), cartesian=True)
222
+ finally:
223
+ geom.freeze_atoms = old
224
+
225
+
226
+ def _attach_calc_if_needed(geom, shared_calc=None) -> None:
227
+ """
228
+ Ensure a calculator is present; prefer `shared_calc` when available.
229
+
230
+ ML/MM-only workflows mean no automatic fallback is provided—callers must either supply
231
+ `shared_calc` (typically `mlmm(...)`) or attach a calculator beforehand.
232
+ """
233
+ try:
234
+ has_calc = getattr(geom, "calculator", None) is not None
235
+ except Exception:
236
+ has_calc = False
237
+ if shared_calc is not None:
238
+ geom.set_calculator(shared_calc)
239
+ elif not has_calc:
240
+ raise RuntimeError(
241
+ "align_freeze_atoms requires a calculator. Provide shared_calc=mlmm(...) or attach one to each geometry."
242
+ )
243
+
244
+
245
+ def _freeze_union(g_ref, g_mob, n_atoms: Optional[int] = None) -> List[int]:
246
+ """
247
+ Union of `freeze_atoms` from `g_ref` and `g_mob` (0-based).
248
+ If `n_atoms` is given, out-of-range indices are removed. Returns [] if empty.
249
+ """
250
+ fa0 = getattr(g_ref, "freeze_atoms", np.array([], int))
251
+ fa1 = getattr(g_mob, "freeze_atoms", np.array([], int))
252
+ cand = sorted(set(int(i) for i in list(fa0) + list(fa1)))
253
+ if n_atoms is None:
254
+ return cand
255
+ good = [i for i in cand if 0 <= i < int(n_atoms)]
256
+ return good
257
+
258
+
259
+ # =============================================================================
260
+ # Rigid alignment: special handling for freeze=1/2 → Kabsch otherwise
261
+ # =============================================================================
262
+
263
+ def align_second_to_first_kabsch_inplace(g_ref, g_mob,
264
+ *, verbose: bool = True) -> Dict[str, Any]:
265
+ """
266
+ Rigidly align `g_mob` to `g_ref` (update coordinates of `g_mob` in place).
267
+
268
+ Returns a dict with keys: {before_A, after_A, n_used, mode}
269
+ - mode: "one_anchor" | "two_anchor" | "kabsch"
270
+
271
+ Behavior:
272
+ * freeze=1 atom: minimize the all-atom RMSD using rotations about the
273
+ single anchor point only.
274
+ * freeze=2 atoms: align the axis defined by the two anchors and optimize
275
+ the rotation around that axis.
276
+ * otherwise: solve Kabsch with the union of `freeze_atoms` (or all atoms
277
+ if empty) and apply the resulting transform to all atoms.
278
+ """
279
+ P = _coords3d(g_ref) # bohr
280
+ Q = _coords3d(g_mob) # bohr
281
+ if P.shape != Q.shape:
282
+ raise ValueError(f"Different atom counts: {P.shape[0]} vs {Q.shape[0]}")
283
+ N = P.shape[0]
284
+ idx = _freeze_union(g_ref, g_mob, n_atoms=N)
285
+
286
+ def _set_all(Q_new: np.ndarray) -> None:
287
+ _set_all_coords_disabling_freeze(g_mob, Q_new)
288
+
289
+ mode = "kabsch"
290
+
291
+ # ---- 1 anchor ----
292
+ if len(idx) == 1:
293
+ i = idx[0]
294
+ before = _rmsd(P, Q) # all-atom RMSD (design: constrained rotation minimizes all-atom)
295
+ p0, q0 = P[i].copy(), Q[i].copy()
296
+ Q_shift = Q + (p0 - q0) # match the anchor point
297
+ P_rel, Q_rel = P - p0, Q_shift - p0
298
+ U, _, Vt = np.linalg.svd(P_rel.T @ Q_rel)
299
+ R = Vt.T @ U.T
300
+ if np.linalg.det(R) < 0.0:
301
+ Vt[-1] *= -1.0
302
+ R = Vt.T @ U.T
303
+ Q_aln = (Q_rel @ R) + p0
304
+ _set_all(Q_aln)
305
+ after = _rmsd(P, Q_aln)
306
+ mode = "one_anchor"
307
+ if verbose:
308
+ click.echo(f"[align] one-anchor: RMSD {before:.6f} Å → {after:.6f} Å (idx={i})")
309
+ return {"before_A": before, "after_A": after, "n_used": 1, "mode": mode}
310
+
311
+ # ---- 2 anchors ----
312
+ if len(idx) == 2:
313
+ before = _rmsd(P, Q) # all-atom RMSD (design: constrained rotation minimizes all-atom)
314
+ i0, i1 = idx[0], idx[1]
315
+ p0, p1, q0, q1 = P[i0].copy(), P[i1].copy(), Q[i0].copy(), Q[i1].copy()
316
+ pm, qm = 0.5 * (p0 + p1), 0.5 * (q0 + q1)
317
+ vP, vQ = p1 - p0, q1 - q0
318
+
319
+ if np.linalg.norm(vP) < 1e-16 or np.linalg.norm(vQ) < 1e-16:
320
+ # Fallback: Kabsch
321
+ pass
322
+ else:
323
+ Q0 = Q + (pm - qm) # match midpoints
324
+ R_align = _rotation_align_vectors(vQ, vP)
325
+ Q0 = ((Q0 - pm) @ R_align.T) + pm # align axis direction (right-multiply → .T)
326
+
327
+ u = vP / (np.linalg.norm(vP) + 1e-16)
328
+ c = pm
329
+ P_perp = _orth_proj_perp(u)
330
+ A = (P - c) @ P_perp.T
331
+ B = (Q0 - c) @ P_perp.T
332
+ cross_u_B = np.cross(u, B)
333
+ s1 = float(np.sum(A * B))
334
+ s2 = float(np.sum(A * cross_u_B))
335
+ theta = np.arctan2(s2, s1) if (abs(s1) + abs(s2)) > 1e-16 else 0.0
336
+
337
+ R_axis = _rodrigues(u, theta)
338
+ Q1 = ((Q0 - c) @ R_axis.T) + c
339
+ _set_all(Q1)
340
+ after = _rmsd(P, Q1)
341
+ mode = "two_anchor"
342
+ if verbose:
343
+ click.echo(f"[align] two-anchors: RMSD {before:.6f} Å → {after:.6f} Å (idx=({i0},{i1}))")
344
+ return {"before_A": before, "after_A": after, "n_used": 2, "mode": mode}
345
+
346
+ # ---- Default: Kabsch (selected freeze atoms or all atoms) ----
347
+ if len(idx) > 0:
348
+ use = np.zeros(N, bool)
349
+ for k in idx:
350
+ if 0 <= k < N:
351
+ use[k] = True
352
+ else:
353
+ use = np.ones(N, bool)
354
+
355
+ P_sel, Q_sel = P[use], Q[use]
356
+ n_used = int(P_sel.shape[0])
357
+
358
+ # *** CHANGED: evaluate RMSD on the same selection used for Kabsch ***
359
+ before_sel = _rmsd(P_sel, Q_sel)
360
+
361
+ R, t = kabsch_R_t(P_sel, Q_sel)
362
+ Q_aln = (Q @ R) + t
363
+ _set_all(Q_aln)
364
+
365
+ after_sel = _rmsd(P_sel, Q_aln[use])
366
+
367
+ if verbose:
368
+ click.echo(f"[align] kabsch: RMSD {before_sel:.6f} Å → {after_sel:.6f} Å (used {n_used})")
369
+
370
+ return {"before_A": before_sel, "after_A": after_sel, "n_used": n_used, "mode": mode}
371
+
372
+
373
+ # =============================================================================
374
+ # Scan + relaxation (stepwise matching of `freeze_atoms` to the reference)
375
+ # =============================================================================
376
+
377
+ def scan_freeze_atoms_toward_target_inplace(
378
+ g_ref,
379
+ g_mob,
380
+ *,
381
+ step_A: float = 0.1,
382
+ per_step_cycles: int = 50,
383
+ final_cycles: int = 200,
384
+ max_steps: int = 1000,
385
+ thresh: str = "gau",
386
+ shared_calc=None,
387
+ out_dir: Path = Path("./result_align_refine/"),
388
+ verbose: bool = True,
389
+ ) -> Dict[str, Any]:
390
+ """
391
+ Move the `freeze_atoms` of `g_mob` toward the reference (`g_ref`) by `step_A` Å
392
+ per iteration. At each step, keep the frozen atoms fixed and run a short LBFGS
393
+ relaxation on the remaining atoms. Finally, enforce exact coincidence of the
394
+ frozen atoms and perform a finishing relaxation. Updates `g_mob` in place.
395
+
396
+ Returns:
397
+ dict(max_remaining_A, n_steps, converged)
398
+ """
399
+ P = _coords3d(g_ref) # bohr
400
+ Q = _coords3d(g_mob) # bohr
401
+ if P.shape != Q.shape:
402
+ raise ValueError(f"Different atom counts: {P.shape[0]} vs {Q.shape[0]}")
403
+ N = P.shape[0]
404
+
405
+ original_freeze = np.array(getattr(g_mob, "freeze_atoms", []), int)
406
+ try:
407
+ idx = _freeze_union(g_ref, g_mob, n_atoms=N)
408
+
409
+ if len(idx) == 0:
410
+ if verbose:
411
+ click.echo("[scan] freeze_atoms list is empty. Skipping scan and relaxation.")
412
+ return {"max_remaining_A": 0.0, "n_steps": 0, "converged": True}
413
+
414
+ # Attach a calculator if needed
415
+ _attach_calc_if_needed(g_mob, shared_calc)
416
+
417
+ out_dir = Path(out_dir)
418
+ out_dir.mkdir(parents=True, exist_ok=True)
419
+
420
+ step_bohr = float(step_A) / BOHR2ANG
421
+ eps = 1e-12
422
+ n_steps_done = 0
423
+ converged = False
424
+ max_remaining_A = None
425
+
426
+ for istep in range(1, max_steps + 1):
427
+ Q = _coords3d(g_mob)
428
+ d = P[idx] - Q[idx] # bohr
429
+ rem_bohr = np.linalg.norm(d, axis=1)
430
+ max_rem_bohr = float(rem_bohr.max()) if len(rem_bohr) else 0.0
431
+ max_remaining_A = max_rem_bohr * BOHR2ANG
432
+ if verbose:
433
+ click.echo(f"[scan] step {istep:03d}: max remaining = {max_remaining_A:.6f} Å")
434
+
435
+ if max_rem_bohr <= step_bohr + 1e-12:
436
+ # Final step: enforce exact coincidence
437
+ Q_new = Q.copy()
438
+ Q_new[idx] = P[idx]
439
+ _set_all_coords_disabling_freeze(g_mob, Q_new)
440
+ try:
441
+ # Finishing relaxation
442
+ g_mob.freeze_atoms = np.array(idx, int)
443
+ LBFGS(
444
+ g_mob,
445
+ out_dir=str(out_dir),
446
+ max_cycles=int(final_cycles),
447
+ print_every=100,
448
+ thresh=thresh,
449
+ dump=False,
450
+ ).run()
451
+ except (ZeroStepLength, OptimizationError) as e:
452
+ if verbose:
453
+ click.echo(f"[scan] WARNING: Exception occurred in final relaxation: {e} (continue...)", err=True)
454
+ g_mob.freeze_atoms = np.array([], int)
455
+ converged = True
456
+ n_steps_done = istep
457
+ break
458
+
459
+ # Take one step forward toward the target
460
+ move = np.zeros_like(d)
461
+ sel = rem_bohr > eps
462
+ move[sel] = (d[sel] / rem_bohr[sel, None]) * step_bohr
463
+ Q_next = Q.copy()
464
+ Q_next[idx] = Q[idx] + move
465
+
466
+ # Update coordinates → short relaxation with frozen atoms fixed
467
+ _set_all_coords_disabling_freeze(g_mob, Q_next)
468
+ try:
469
+ g_mob.freeze_atoms = np.array(idx, int)
470
+ LBFGS(
471
+ g_mob,
472
+ out_dir=str(out_dir),
473
+ max_cycles=int(per_step_cycles),
474
+ print_every=100,
475
+ thresh=thresh,
476
+ dump=False,
477
+ ).run()
478
+ except (ZeroStepLength, OptimizationError) as e:
479
+ if verbose:
480
+ click.echo(f"[scan] WARNING: Exception occurred in relaxation: {e} (continue...)", err=True)
481
+ finally:
482
+ g_mob.freeze_atoms = np.array([], int)
483
+
484
+ n_steps_done = istep
485
+ else:
486
+ if verbose:
487
+ click.echo(f"[scan] WARNING: Reached max_steps={max_steps}.", err=True)
488
+
489
+ return {"max_remaining_A": float(max_remaining_A or 0.0),
490
+ "n_steps": int(n_steps_done),
491
+ "converged": bool(converged)}
492
+ finally:
493
+ # Always restore the original freeze_atoms state
494
+ g_mob.freeze_atoms = original_freeze
495
+
496
+
497
+ # =============================================================================
498
+ # High-level API: pair / sequence
499
+ # =============================================================================
500
+
501
+ def align_and_refine_pair_inplace(
502
+ g_ref,
503
+ g_mob,
504
+ *,
505
+ shared_calc=None,
506
+ out_dir: Path = Path("./result_align_refine/"),
507
+ step_A: float = 0.1,
508
+ per_step_cycles: int = 50,
509
+ final_cycles: int = 200,
510
+ max_steps: int = 1000,
511
+ thresh: str = "gau",
512
+ verbose: bool = True,
513
+ ) -> Dict[str, Any]:
514
+ """
515
+ For a pair (g_ref, g_mob), perform:
516
+ (1) rigid alignment (special cases for freeze=1/2, otherwise Kabsch), then
517
+ (2) scan + relaxation (stepwise matching of `freeze_atoms` to the reference).
518
+ Updates `g_mob` in place.
519
+
520
+ Returns:
521
+ {
522
+ "align": {before_A, after_A, n_used, mode},
523
+ "scan": {max_remaining_A, n_steps, converged}
524
+ }
525
+ """
526
+ # Rigid alignment
527
+ align_res = align_second_to_first_kabsch_inplace(g_ref, g_mob, verbose=verbose)
528
+ # Scan + relaxation
529
+ scan_res = scan_freeze_atoms_toward_target_inplace(
530
+ g_ref, g_mob,
531
+ step_A=step_A,
532
+ per_step_cycles=per_step_cycles,
533
+ final_cycles=final_cycles,
534
+ max_steps=max_steps,
535
+ thresh=thresh,
536
+ shared_calc=shared_calc,
537
+ out_dir=out_dir,
538
+ verbose=verbose,
539
+ )
540
+ return {"align": align_res, "scan": scan_res}
541
+
542
+
543
+ def align_and_refine_sequence_inplace(
544
+ geoms: Sequence[Any],
545
+ *,
546
+ shared_calc=None,
547
+ out_dir: Path = Path("./result_align_refine/"),
548
+ step_A: float = 0.1,
549
+ per_step_cycles: int = 1000,
550
+ final_cycles: int = 1000,
551
+ max_steps: int = 10000,
552
+ thresh: str = "gau",
553
+ verbose: bool = True,
554
+ ) -> List[Dict[str, Any]]:
555
+ """
556
+ For a list [g0, g1, g2, ...], apply `align_and_refine_pair_inplace` in order:
557
+ (g0←g1), (g1←g2), ... i.e., each g_{i+1} is aligned/refined to g_i.
558
+ Returns a list of per-pair result dicts.
559
+
560
+ Intended to be used after pre-optimization in `path_search.py`.
561
+ """
562
+ geoms = list(geoms)
563
+ if len(geoms) <= 1:
564
+ return []
565
+
566
+ out_dir = Path(out_dir)
567
+ out_dir.mkdir(parents=True, exist_ok=True)
568
+
569
+ results: List[Dict[str, Any]] = []
570
+ for i in range(len(geoms) - 1):
571
+ g_ref = geoms[i]
572
+ g_mob = geoms[i + 1]
573
+ pair_out = out_dir / f"pair_{i:02d}"
574
+ pair_out.mkdir(parents=True, exist_ok=True)
575
+
576
+ if verbose:
577
+ click.echo(f"\n[align+scan] Pair {i:02d}: image {i} (ref) ← image {i+1} (mobile)")
578
+
579
+ res = align_and_refine_pair_inplace(
580
+ g_ref, g_mob,
581
+ shared_calc=shared_calc,
582
+ out_dir=pair_out,
583
+ step_A=step_A,
584
+ per_step_cycles=per_step_cycles,
585
+ final_cycles=final_cycles,
586
+ max_steps=max_steps,
587
+ thresh=thresh,
588
+ verbose=verbose,
589
+ )
590
+ results.append(res)
591
+
592
+ return results
593
+
594
+
595
+ __all__ = [
596
+ "align_second_to_first_kabsch_inplace",
597
+ "scan_freeze_atoms_toward_target_inplace",
598
+ "align_and_refine_pair_inplace",
599
+ "align_and_refine_sequence_inplace",
600
+ "kabsch_R_t",
601
+ ]