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,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
|
+
]
|