MultiOptPy 1.20.2__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.
- multioptpy/Calculator/__init__.py +0 -0
- multioptpy/Calculator/ase_calculation_tools.py +424 -0
- multioptpy/Calculator/ase_tools/__init__.py +0 -0
- multioptpy/Calculator/ase_tools/fairchem.py +28 -0
- multioptpy/Calculator/ase_tools/gamess.py +19 -0
- multioptpy/Calculator/ase_tools/gaussian.py +165 -0
- multioptpy/Calculator/ase_tools/mace.py +28 -0
- multioptpy/Calculator/ase_tools/mopac.py +19 -0
- multioptpy/Calculator/ase_tools/nwchem.py +31 -0
- multioptpy/Calculator/ase_tools/orca.py +22 -0
- multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
- multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
- multioptpy/Calculator/emt_calculation_tools.py +458 -0
- multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
- multioptpy/Calculator/lj_calculation_tools.py +314 -0
- multioptpy/Calculator/psi4_calculation_tools.py +334 -0
- multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
- multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
- multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
- multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
- multioptpy/Calculator/tblite_calculation_tools.py +352 -0
- multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
- multioptpy/Constraint/__init__.py +0 -0
- multioptpy/Constraint/constraint_condition.py +834 -0
- multioptpy/Coordinate/__init__.py +0 -0
- multioptpy/Coordinate/polar_coordinate.py +199 -0
- multioptpy/Coordinate/redundant_coordinate.py +638 -0
- multioptpy/IRC/__init__.py +0 -0
- multioptpy/IRC/converge_criteria.py +28 -0
- multioptpy/IRC/dvv.py +544 -0
- multioptpy/IRC/euler.py +439 -0
- multioptpy/IRC/hpc.py +564 -0
- multioptpy/IRC/lqa.py +540 -0
- multioptpy/IRC/modekill.py +662 -0
- multioptpy/IRC/rk4.py +579 -0
- multioptpy/Interpolation/__init__.py +0 -0
- multioptpy/Interpolation/adaptive_interpolation.py +283 -0
- multioptpy/Interpolation/binomial_interpolation.py +179 -0
- multioptpy/Interpolation/geodesic_interpolation.py +785 -0
- multioptpy/Interpolation/interpolation.py +156 -0
- multioptpy/Interpolation/linear_interpolation.py +473 -0
- multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
- multioptpy/Interpolation/spline_interpolation.py +353 -0
- multioptpy/MD/__init__.py +0 -0
- multioptpy/MD/thermostat.py +185 -0
- multioptpy/MEP/__init__.py +0 -0
- multioptpy/MEP/pathopt_bneb_force.py +443 -0
- multioptpy/MEP/pathopt_dmf_force.py +448 -0
- multioptpy/MEP/pathopt_dneb_force.py +130 -0
- multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
- multioptpy/MEP/pathopt_gpneb_force.py +512 -0
- multioptpy/MEP/pathopt_lup_force.py +113 -0
- multioptpy/MEP/pathopt_neb_force.py +225 -0
- multioptpy/MEP/pathopt_nesb_force.py +205 -0
- multioptpy/MEP/pathopt_om_force.py +153 -0
- multioptpy/MEP/pathopt_qsm_force.py +174 -0
- multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
- multioptpy/ModelFunction/__init__.py +7 -0
- multioptpy/ModelFunction/avoiding_model_function.py +29 -0
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
- multioptpy/ModelFunction/conical_model_function.py +26 -0
- multioptpy/ModelFunction/opt_meci.py +50 -0
- multioptpy/ModelFunction/opt_mesx.py +47 -0
- multioptpy/ModelFunction/opt_mesx_2.py +49 -0
- multioptpy/ModelFunction/seam_model_function.py +27 -0
- multioptpy/ModelHessian/__init__.py +0 -0
- multioptpy/ModelHessian/approx_hessian.py +147 -0
- multioptpy/ModelHessian/calc_params.py +227 -0
- multioptpy/ModelHessian/fischer.py +236 -0
- multioptpy/ModelHessian/fischerd3.py +360 -0
- multioptpy/ModelHessian/fischerd4.py +398 -0
- multioptpy/ModelHessian/gfn0xtb.py +633 -0
- multioptpy/ModelHessian/gfnff.py +709 -0
- multioptpy/ModelHessian/lindh.py +165 -0
- multioptpy/ModelHessian/lindh2007d2.py +707 -0
- multioptpy/ModelHessian/lindh2007d3.py +822 -0
- multioptpy/ModelHessian/lindh2007d4.py +1030 -0
- multioptpy/ModelHessian/morse.py +106 -0
- multioptpy/ModelHessian/schlegel.py +144 -0
- multioptpy/ModelHessian/schlegeld3.py +322 -0
- multioptpy/ModelHessian/schlegeld4.py +559 -0
- multioptpy/ModelHessian/shortrange.py +346 -0
- multioptpy/ModelHessian/swartd2.py +496 -0
- multioptpy/ModelHessian/swartd3.py +706 -0
- multioptpy/ModelHessian/swartd4.py +918 -0
- multioptpy/ModelHessian/tshess.py +40 -0
- multioptpy/Optimizer/QHAdam.py +61 -0
- multioptpy/Optimizer/__init__.py +0 -0
- multioptpy/Optimizer/abc_fire.py +83 -0
- multioptpy/Optimizer/adabelief.py +58 -0
- multioptpy/Optimizer/adabound.py +68 -0
- multioptpy/Optimizer/adadelta.py +65 -0
- multioptpy/Optimizer/adaderivative.py +56 -0
- multioptpy/Optimizer/adadiff.py +68 -0
- multioptpy/Optimizer/adafactor.py +70 -0
- multioptpy/Optimizer/adam.py +65 -0
- multioptpy/Optimizer/adamax.py +62 -0
- multioptpy/Optimizer/adamod.py +83 -0
- multioptpy/Optimizer/adamw.py +65 -0
- multioptpy/Optimizer/adiis.py +523 -0
- multioptpy/Optimizer/afire_neb.py +282 -0
- multioptpy/Optimizer/block_hessian_update.py +709 -0
- multioptpy/Optimizer/c2diis.py +491 -0
- multioptpy/Optimizer/component_wise_scaling.py +405 -0
- multioptpy/Optimizer/conjugate_gradient.py +82 -0
- multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
- multioptpy/Optimizer/coordinate_locking.py +405 -0
- multioptpy/Optimizer/dic_rsirfo.py +1015 -0
- multioptpy/Optimizer/ediis.py +417 -0
- multioptpy/Optimizer/eve.py +76 -0
- multioptpy/Optimizer/fastadabelief.py +61 -0
- multioptpy/Optimizer/fire.py +77 -0
- multioptpy/Optimizer/fire2.py +249 -0
- multioptpy/Optimizer/fire_neb.py +92 -0
- multioptpy/Optimizer/gan_step.py +486 -0
- multioptpy/Optimizer/gdiis.py +609 -0
- multioptpy/Optimizer/gediis.py +203 -0
- multioptpy/Optimizer/geodesic_step.py +433 -0
- multioptpy/Optimizer/gpmin.py +633 -0
- multioptpy/Optimizer/gpr_step.py +364 -0
- multioptpy/Optimizer/gradientdescent.py +78 -0
- multioptpy/Optimizer/gradientdescent_neb.py +52 -0
- multioptpy/Optimizer/hessian_update.py +433 -0
- multioptpy/Optimizer/hybrid_rfo.py +998 -0
- multioptpy/Optimizer/kdiis.py +625 -0
- multioptpy/Optimizer/lars.py +21 -0
- multioptpy/Optimizer/lbfgs.py +253 -0
- multioptpy/Optimizer/lbfgs_neb.py +355 -0
- multioptpy/Optimizer/linesearch.py +236 -0
- multioptpy/Optimizer/lookahead.py +40 -0
- multioptpy/Optimizer/nadam.py +64 -0
- multioptpy/Optimizer/newton.py +200 -0
- multioptpy/Optimizer/prodigy.py +70 -0
- multioptpy/Optimizer/purtubation.py +16 -0
- multioptpy/Optimizer/quickmin_neb.py +245 -0
- multioptpy/Optimizer/radam.py +75 -0
- multioptpy/Optimizer/rfo_neb.py +302 -0
- multioptpy/Optimizer/ric_rfo.py +842 -0
- multioptpy/Optimizer/rl_step.py +627 -0
- multioptpy/Optimizer/rmspropgrave.py +65 -0
- multioptpy/Optimizer/rsirfo.py +1647 -0
- multioptpy/Optimizer/rsprfo.py +1056 -0
- multioptpy/Optimizer/sadam.py +60 -0
- multioptpy/Optimizer/samsgrad.py +63 -0
- multioptpy/Optimizer/tr_lbfgs.py +678 -0
- multioptpy/Optimizer/trim.py +273 -0
- multioptpy/Optimizer/trust_radius.py +207 -0
- multioptpy/Optimizer/trust_radius_neb.py +121 -0
- multioptpy/Optimizer/yogi.py +60 -0
- multioptpy/OtherMethod/__init__.py +0 -0
- multioptpy/OtherMethod/addf.py +1150 -0
- multioptpy/OtherMethod/dimer.py +895 -0
- multioptpy/OtherMethod/elastic_image_pair.py +629 -0
- multioptpy/OtherMethod/modelfunction.py +456 -0
- multioptpy/OtherMethod/newton_traj.py +454 -0
- multioptpy/OtherMethod/twopshs.py +1095 -0
- multioptpy/PESAnalyzer/__init__.py +0 -0
- multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
- multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
- multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
- multioptpy/PESAnalyzer/pca_analysis.py +314 -0
- multioptpy/Parameters/__init__.py +0 -0
- multioptpy/Parameters/atomic_mass.py +20 -0
- multioptpy/Parameters/atomic_number.py +22 -0
- multioptpy/Parameters/covalent_radii.py +44 -0
- multioptpy/Parameters/d2.py +61 -0
- multioptpy/Parameters/d3.py +63 -0
- multioptpy/Parameters/d4.py +103 -0
- multioptpy/Parameters/dreiding.py +34 -0
- multioptpy/Parameters/gfn0xtb_param.py +137 -0
- multioptpy/Parameters/gfnff_param.py +315 -0
- multioptpy/Parameters/gnb.py +104 -0
- multioptpy/Parameters/parameter.py +22 -0
- multioptpy/Parameters/uff.py +72 -0
- multioptpy/Parameters/unit_values.py +20 -0
- multioptpy/Potential/AFIR_potential.py +55 -0
- multioptpy/Potential/LJ_repulsive_potential.py +345 -0
- multioptpy/Potential/__init__.py +0 -0
- multioptpy/Potential/anharmonic_keep_potential.py +28 -0
- multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
- multioptpy/Potential/electrostatic_potential.py +69 -0
- multioptpy/Potential/flux_potential.py +30 -0
- multioptpy/Potential/gaussian_potential.py +101 -0
- multioptpy/Potential/idpp.py +516 -0
- multioptpy/Potential/keep_angle_potential.py +146 -0
- multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
- multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
- multioptpy/Potential/keep_potential.py +99 -0
- multioptpy/Potential/mechano_force_potential.py +74 -0
- multioptpy/Potential/nanoreactor_potential.py +52 -0
- multioptpy/Potential/potential.py +896 -0
- multioptpy/Potential/spacer_model_potential.py +221 -0
- multioptpy/Potential/switching_potential.py +258 -0
- multioptpy/Potential/universal_potential.py +34 -0
- multioptpy/Potential/value_range_potential.py +36 -0
- multioptpy/Potential/void_point_potential.py +25 -0
- multioptpy/SQM/__init__.py +0 -0
- multioptpy/SQM/sqm1/__init__.py +0 -0
- multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
- multioptpy/SQM/sqm2/__init__.py +0 -0
- multioptpy/SQM/sqm2/calc_tools.py +95 -0
- multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
- multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
- multioptpy/SQM/sqm2/sqm2_core.py +303 -0
- multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
- multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
- multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
- multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
- multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
- multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
- multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
- multioptpy/Thermo/__init__.py +0 -0
- multioptpy/Thermo/normal_mode_analyzer.py +865 -0
- multioptpy/Utils/__init__.py +0 -0
- multioptpy/Utils/bond_connectivity.py +264 -0
- multioptpy/Utils/calc_tools.py +884 -0
- multioptpy/Utils/oniom.py +96 -0
- multioptpy/Utils/pbc.py +48 -0
- multioptpy/Utils/riemann_curvature.py +208 -0
- multioptpy/Utils/symmetry_analyzer.py +482 -0
- multioptpy/Visualization/__init__.py +0 -0
- multioptpy/Visualization/visualization.py +156 -0
- multioptpy/WFAnalyzer/MO_analysis.py +104 -0
- multioptpy/WFAnalyzer/__init__.py +0 -0
- multioptpy/Wrapper/__init__.py +0 -0
- multioptpy/Wrapper/autots.py +1239 -0
- multioptpy/Wrapper/ieip_wrapper.py +93 -0
- multioptpy/Wrapper/md_wrapper.py +92 -0
- multioptpy/Wrapper/neb_wrapper.py +94 -0
- multioptpy/Wrapper/optimize_wrapper.py +76 -0
- multioptpy/__init__.py +5 -0
- multioptpy/entrypoints.py +916 -0
- multioptpy/fileio.py +660 -0
- multioptpy/ieip.py +340 -0
- multioptpy/interface.py +1086 -0
- multioptpy/irc.py +529 -0
- multioptpy/moleculardynamics.py +432 -0
- multioptpy/neb.py +1267 -0
- multioptpy/optimization.py +1553 -0
- multioptpy/optimizer.py +709 -0
- multioptpy-1.20.2.dist-info/METADATA +438 -0
- multioptpy-1.20.2.dist-info/RECORD +246 -0
- multioptpy-1.20.2.dist-info/WHEEL +5 -0
- multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
- multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
- multioptpy-1.20.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compute_natural_spline_coefficients(x, y):
|
|
5
|
+
"""
|
|
6
|
+
Computes the coefficients for the natural cubic spline interpolation for data points (x, y).
|
|
7
|
+
Returns:
|
|
8
|
+
a: array of a coefficients (equals y values for each interval start)
|
|
9
|
+
b: array of b coefficients for each interval
|
|
10
|
+
c: array of c coefficients for each interval (from 0 to n-2)
|
|
11
|
+
d: array of d coefficients for each interval
|
|
12
|
+
h: array of interval lengths, where h[i] = x[i+1] - x[i]
|
|
13
|
+
"""
|
|
14
|
+
n = len(x)
|
|
15
|
+
a = np.array(y, dtype=float)
|
|
16
|
+
h = np.diff(x)
|
|
17
|
+
|
|
18
|
+
# Build the tri-diagonal system for c: second derivative coefficients.
|
|
19
|
+
A = np.zeros((n, n))
|
|
20
|
+
rhs = np.zeros(n)
|
|
21
|
+
|
|
22
|
+
# Natural spline boundary conditions: second derivatives at endpoints are 0.
|
|
23
|
+
A[0, 0] = 1.0
|
|
24
|
+
A[-1, -1] = 1.0
|
|
25
|
+
for i in range(1, n-1):
|
|
26
|
+
A[i, i-1] = h[i-1]
|
|
27
|
+
A[i, i] = 2*(h[i-1] + h[i])
|
|
28
|
+
A[i, i+1] = h[i]
|
|
29
|
+
rhs[i] = 3 * ((a[i+1]-a[i])/h[i] - (a[i]-a[i-1])/h[i-1])
|
|
30
|
+
|
|
31
|
+
# Solve for c (second derivatives at data points)
|
|
32
|
+
c_full = np.linalg.solve(A, rhs)
|
|
33
|
+
|
|
34
|
+
# Compute b and d for each segment
|
|
35
|
+
b = np.zeros(n-1)
|
|
36
|
+
d = np.zeros(n-1)
|
|
37
|
+
# For each interval [x[i], x[i+1]]
|
|
38
|
+
for i in range(n-1):
|
|
39
|
+
b[i] = (a[i+1]-a[i])/h[i] - h[i]*(2*c_full[i] + c_full[i+1])/3
|
|
40
|
+
d[i] = (c_full[i+1]-c_full[i])/(3*h[i])
|
|
41
|
+
|
|
42
|
+
# We will use c coefficients per segment as c[i] (from point i)
|
|
43
|
+
# Note: For evaluating spline on interval i, c_full[i] is used.
|
|
44
|
+
return a, b, c_full, d, h
|
|
45
|
+
|
|
46
|
+
def evaluate_spline(x, a, b, c_full, d, xi):
|
|
47
|
+
"""
|
|
48
|
+
Evaluate the natural cubic spline S(x) at a given point xi.
|
|
49
|
+
Identifies the appropriate interval and computes:
|
|
50
|
+
S(x) = a[i] + b[i]*(xi - x[i]) + c_full[i]*(xi-x[i])^2 + d[i]*(xi-x[i])^3
|
|
51
|
+
"""
|
|
52
|
+
n = len(x)
|
|
53
|
+
# Find the appropriate interval
|
|
54
|
+
if xi <= x[0]:
|
|
55
|
+
i = 0
|
|
56
|
+
elif xi >= x[-1]:
|
|
57
|
+
i = n - 2
|
|
58
|
+
else:
|
|
59
|
+
# find i such that x[i] <= xi < x[i+1]
|
|
60
|
+
i = np.searchsorted(x, xi) - 1
|
|
61
|
+
dx = xi - x[i]
|
|
62
|
+
return a[i] + b[i]*dx + c_full[i]*dx**2 + d[i]*dx**3
|
|
63
|
+
|
|
64
|
+
def evaluate_spline_deriv(x, a, b, c_full, d, xi):
|
|
65
|
+
"""
|
|
66
|
+
Evaluate the first derivative S'(x) of the spline at xi.
|
|
67
|
+
S'(x) = b[i] + 2*c_full[i]*(xi-x[i]) + 3*d[i]*(xi-x[i])^2
|
|
68
|
+
"""
|
|
69
|
+
n = len(x)
|
|
70
|
+
if xi <= x[0]:
|
|
71
|
+
i = 0
|
|
72
|
+
elif xi >= x[-1]:
|
|
73
|
+
i = n - 2
|
|
74
|
+
else:
|
|
75
|
+
i = np.searchsorted(x, xi) - 1
|
|
76
|
+
dx = xi - x[i]
|
|
77
|
+
return b[i] + 2*c_full[i]*dx + 3*d[i]*dx**2
|
|
78
|
+
|
|
79
|
+
def evaluate_spline_second_deriv(x, c_full, d, xi):
|
|
80
|
+
"""
|
|
81
|
+
Evaluate the second derivative S''(x) of the spline at xi.
|
|
82
|
+
S''(x) = 2*c_full[i] + 6*d[i]*(xi-x[i])
|
|
83
|
+
"""
|
|
84
|
+
n = len(x)
|
|
85
|
+
if xi <= x[0]:
|
|
86
|
+
i = 0
|
|
87
|
+
elif xi >= x[-1]:
|
|
88
|
+
i = n - 2
|
|
89
|
+
else:
|
|
90
|
+
i = np.searchsorted(x, xi) - 1
|
|
91
|
+
dx = xi - x[i]
|
|
92
|
+
return 2*c_full[i] + 6*d[i]*dx
|
|
93
|
+
|
|
94
|
+
def newton_method(f, df, x0, tol=1e-10, max_iter=500):
|
|
95
|
+
"""
|
|
96
|
+
Apply Newton's method to solve f(x)=0.
|
|
97
|
+
Starts from initial guess x0, and iterates until convergence.
|
|
98
|
+
"""
|
|
99
|
+
x_current = x0
|
|
100
|
+
for _ in range(max_iter):
|
|
101
|
+
f_val = f(x_current)
|
|
102
|
+
df_val = df(x_current)
|
|
103
|
+
if abs(df_val) < 1e-12:
|
|
104
|
+
break
|
|
105
|
+
x_new = x_current - f_val/df_val
|
|
106
|
+
if abs(x_new - x_current) < tol:
|
|
107
|
+
return x_new
|
|
108
|
+
x_current = x_new
|
|
109
|
+
return x_current
|
|
110
|
+
|
|
111
|
+
def find_extrema(x, a, b, c_full, d):
|
|
112
|
+
"""
|
|
113
|
+
Find local extrema for the natural spline interpolation.
|
|
114
|
+
For each spline segment [x[i], x[i+1]], we apply Newton's method to solve:
|
|
115
|
+
S'_i(x) = 0,
|
|
116
|
+
starting from the midpoint of the interval.
|
|
117
|
+
|
|
118
|
+
We then classify the extremum using the second derivative:
|
|
119
|
+
- If S''(x) < 0, it's a local maximum.
|
|
120
|
+
- If S''(x) > 0, it's a local minimum.
|
|
121
|
+
|
|
122
|
+
Returns two lists: local_maxima and local_minima.
|
|
123
|
+
Each element is a tuple (xi, S(xi)).
|
|
124
|
+
"""
|
|
125
|
+
local_maxima = []
|
|
126
|
+
local_minima = []
|
|
127
|
+
n = len(x)
|
|
128
|
+
|
|
129
|
+
for i in range(n-1):
|
|
130
|
+
def f(xi):
|
|
131
|
+
return b[i] + 2*c_full[i]*(xi - x[i]) + 3*d[i]*(xi - x[i])**2
|
|
132
|
+
def df(xi):
|
|
133
|
+
return 2*c_full[i] + 6*d[i]*(xi - x[i])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
x0 = (x[i] + x[i+1]) / 2
|
|
137
|
+
root = newton_method(f, df, x0)
|
|
138
|
+
|
|
139
|
+
if root >= x[i] - 1e-10 and root <= x[i+1] + 1e-10:
|
|
140
|
+
S_value = a[i] + b[i]*(root - x[i]) + c_full[i]*(root - x[i])**2 + d[i]*(root - x[i])**3
|
|
141
|
+
second_deriv = 2*c_full[i] + 6*d[i]*(root - x[i])
|
|
142
|
+
if second_deriv < 0:
|
|
143
|
+
local_maxima.append((root, S_value))
|
|
144
|
+
elif second_deriv > 0:
|
|
145
|
+
local_minima.append((root, S_value))
|
|
146
|
+
return local_maxima, local_minima
|
|
147
|
+
|
|
148
|
+
def spline_interpolation(x_data, y_data):
|
|
149
|
+
resolution = 100000
|
|
150
|
+
a, b, c_full, d, h = compute_natural_spline_coefficients(x_data, y_data)
|
|
151
|
+
t_fine = np.linspace(x_data[0], x_data[-1], resolution)
|
|
152
|
+
S_values = [evaluate_spline(x_data, a, b, c_full, d, t) for t in t_fine]
|
|
153
|
+
local_maxima, local_minima = find_extrema(x_data, a, b, c_full, d)
|
|
154
|
+
# distance, energy = zip(*local_maxima or *local_minima)
|
|
155
|
+
return local_maxima, local_minima
|
|
156
|
+
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from multioptpy.Utils.calc_tools import calc_path_length_list
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _cumulative_path_length(geom):
|
|
6
|
+
"""Calculates cumulative path length along the string."""
|
|
7
|
+
diffs = np.diff(geom, axis=0)
|
|
8
|
+
seg_lengths = np.linalg.norm(diffs, axis=1)
|
|
9
|
+
seg_lengths = np.nan_to_num(seg_lengths, nan=0.0)
|
|
10
|
+
return np.concatenate([[0.0], np.cumsum(seg_lengths)])
|
|
11
|
+
|
|
12
|
+
def _estimate_curvature_and_tangents(gradients, geom):
|
|
13
|
+
"""
|
|
14
|
+
Calculates curvature and tangent vectors at each node.
|
|
15
|
+
Returns:
|
|
16
|
+
curvatures: np.array (N,) - Second derivative along path
|
|
17
|
+
tangents: np.array (N, atoms*3) - Normalized tangent vectors
|
|
18
|
+
"""
|
|
19
|
+
n = len(geom)
|
|
20
|
+
gradients = gradients.reshape(n, -1)
|
|
21
|
+
geom = geom.reshape(n, -1)
|
|
22
|
+
|
|
23
|
+
# 1. Calculate Tangents (Central Difference)
|
|
24
|
+
tangents = np.zeros_like(geom)
|
|
25
|
+
|
|
26
|
+
# Internal nodes: T_i ~ (x_{i+1} - x_{i-1})
|
|
27
|
+
if n > 2:
|
|
28
|
+
vecs = geom[2:] - geom[:-2]
|
|
29
|
+
norms = np.linalg.norm(vecs, axis=1)
|
|
30
|
+
norms = np.maximum(norms, 1e-12)
|
|
31
|
+
tangents[1:-1] = vecs / norms[:, None]
|
|
32
|
+
|
|
33
|
+
# Endpoints: Forward/Backward difference
|
|
34
|
+
t_start = geom[1] - geom[0]
|
|
35
|
+
tangents[0] = t_start / max(np.linalg.norm(t_start), 1e-12)
|
|
36
|
+
|
|
37
|
+
t_end = geom[-1] - geom[-2]
|
|
38
|
+
tangents[-1] = t_end / max(np.linalg.norm(t_end), 1e-12)
|
|
39
|
+
|
|
40
|
+
# 2. Calculate Gradient Projections (Slope)
|
|
41
|
+
grad_along = np.sum(gradients * tangents, axis=1)
|
|
42
|
+
|
|
43
|
+
# 3. Calculate Curvature (Numerical differentiation of slope)
|
|
44
|
+
curvature = np.zeros(n)
|
|
45
|
+
dists = np.linalg.norm(geom[1:] - geom[:-1], axis=1) # Segment lengths
|
|
46
|
+
|
|
47
|
+
for k in range(1, n - 1):
|
|
48
|
+
ds_prev = dists[k-1]
|
|
49
|
+
ds_next = dists[k]
|
|
50
|
+
total_ds = ds_prev + ds_next
|
|
51
|
+
|
|
52
|
+
if total_ds > 1e-10:
|
|
53
|
+
# Centered finite difference of the first derivative
|
|
54
|
+
curvature[k] = (grad_along[k+1] - grad_along[k-1]) / total_ds
|
|
55
|
+
|
|
56
|
+
curvature[0] = curvature[1]
|
|
57
|
+
curvature[-1] = curvature[-2]
|
|
58
|
+
|
|
59
|
+
return curvature, tangents, grad_along
|
|
60
|
+
|
|
61
|
+
def _solve_polynomial_max(s_vals, E_vals, g_vals, gamma_vals=None):
|
|
62
|
+
"""
|
|
63
|
+
Fits a polynomial to 3 points and finds the local maximum.
|
|
64
|
+
Calculates 3rd derivative to estimate asymmetry.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
s_vals: [s_prev, 0, s_next] (relative arc lengths)
|
|
68
|
+
E_vals: [E_prev, E_curr, E_next]
|
|
69
|
+
g_vals: [g_prev, g_curr, g_next] (projected gradients)
|
|
70
|
+
gamma_vals: [c_prev, c_curr, c_next] (curvatures, optional)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
s_max: Predicted arc length of TS (relative to center), or None if invalid.
|
|
74
|
+
"""
|
|
75
|
+
# Coordinate scaling to avoid numerical issues with small s
|
|
76
|
+
scale = max(abs(s_vals[0]), abs(s_vals[2]))
|
|
77
|
+
if scale < 1e-12: return None
|
|
78
|
+
|
|
79
|
+
s = np.array(s_vals) / scale
|
|
80
|
+
E = np.array(E_vals)
|
|
81
|
+
g = np.array(g_vals) * scale # Derivative scales with 1/scale
|
|
82
|
+
|
|
83
|
+
# Construct Linear System: Ac = b
|
|
84
|
+
# E(s) = sum(c_k * s^k)
|
|
85
|
+
|
|
86
|
+
rows = []
|
|
87
|
+
rhs = []
|
|
88
|
+
|
|
89
|
+
# Degree of polynomial
|
|
90
|
+
# If gamma is provided: 3 points * 3 constraints = 9 constraints -> Degree 8 (Octic)
|
|
91
|
+
# If gamma is None: 3 points * 2 constraints = 6 constraints -> Degree 5 (Quintic)
|
|
92
|
+
use_curvature = (gamma_vals is not None)
|
|
93
|
+
degree = 8 if use_curvature else 5
|
|
94
|
+
|
|
95
|
+
if use_curvature:
|
|
96
|
+
gamma = np.array(gamma_vals) * (scale**2) # 2nd deriv scales with 1/scale^2
|
|
97
|
+
|
|
98
|
+
for i in range(3):
|
|
99
|
+
si = s[i]
|
|
100
|
+
# Energy constraint: E(si)
|
|
101
|
+
rows.append([si**k for k in range(degree + 1)])
|
|
102
|
+
rhs.append(E[i])
|
|
103
|
+
|
|
104
|
+
# Gradient constraint: E'(si)
|
|
105
|
+
row_g = [0.0] * (degree + 1)
|
|
106
|
+
for k in range(1, degree + 1):
|
|
107
|
+
row_g[k] = k * si**(k-1)
|
|
108
|
+
rows.append(row_g)
|
|
109
|
+
rhs.append(g[i])
|
|
110
|
+
|
|
111
|
+
# Curvature constraint: E''(si)
|
|
112
|
+
if use_curvature:
|
|
113
|
+
row_c = [0.0] * (degree + 1)
|
|
114
|
+
for k in range(2, degree + 1):
|
|
115
|
+
row_c[k] = k * (k-1) * si**(k-2)
|
|
116
|
+
rows.append(row_c)
|
|
117
|
+
rhs.append(gamma[i])
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Solve for coefficients
|
|
121
|
+
coeffs = np.linalg.solve(np.array(rows), np.array(rhs))
|
|
122
|
+
except np.linalg.LinAlgError:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# Find roots of the derivative (E'(s) = 0)
|
|
126
|
+
deriv_coeffs = [k * coeffs[k] for k in range(1, degree + 1)]
|
|
127
|
+
roots = np.roots(deriv_coeffs[::-1])
|
|
128
|
+
|
|
129
|
+
# Filter real roots within the interval
|
|
130
|
+
valid_roots = []
|
|
131
|
+
s_min, s_max = s[0], s[2]
|
|
132
|
+
|
|
133
|
+
for r in roots:
|
|
134
|
+
if np.isreal(r):
|
|
135
|
+
r_real = r.real
|
|
136
|
+
# Allow slightly outside for prediction, but clamp tightly for safety
|
|
137
|
+
if s_min * 1.1 <= r_real <= s_max * 1.1:
|
|
138
|
+
# Check curvature (E''(s) < 0 for maximum)
|
|
139
|
+
# Calculate 2nd derivative value at this root
|
|
140
|
+
curvature_val = 0.0
|
|
141
|
+
for k in range(2, degree + 1):
|
|
142
|
+
curvature_val += k * (k-1) * coeffs[k] * (r_real**(k-2))
|
|
143
|
+
|
|
144
|
+
if curvature_val < -1e-5: # Must be concave (maximum)
|
|
145
|
+
# --- Added: Calculate 3rd Derivative for Asymmetry Check ---
|
|
146
|
+
deriv3_val = 0.0
|
|
147
|
+
for k in range(3, degree + 1):
|
|
148
|
+
deriv3_val += k * (k-1) * (k-2) * coeffs[k] * (r_real**(k-3))
|
|
149
|
+
|
|
150
|
+
# Scale back to physical units
|
|
151
|
+
# 3rd derivative scales with 1/scale^3
|
|
152
|
+
true_deriv3 = deriv3_val / (scale**3)
|
|
153
|
+
true_curvature = curvature_val / (scale**2)
|
|
154
|
+
|
|
155
|
+
fit_type = "Octic" if use_curvature else "Quintic"
|
|
156
|
+
print(f" [{fit_type}] TS candidate found at s={r_real*scale:.4f}. "
|
|
157
|
+
f"Curvature={true_curvature:.4e}, 3rd Deriv={true_deriv3:.4e}")
|
|
158
|
+
|
|
159
|
+
# Calculate Energy at this root
|
|
160
|
+
energy_val = np.polynomial.polynomial.polyval(r_real, coeffs)
|
|
161
|
+
valid_roots.append((r_real, energy_val))
|
|
162
|
+
|
|
163
|
+
if not valid_roots:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# Return the root with the highest energy
|
|
167
|
+
best_s = max(valid_roots, key=lambda x: x[1])[0]
|
|
168
|
+
|
|
169
|
+
return best_s * scale
|
|
170
|
+
|
|
171
|
+
def distribute_geometry_by_predicted_energy(
|
|
172
|
+
geometry_list,
|
|
173
|
+
energy_list,
|
|
174
|
+
gradient_list,
|
|
175
|
+
n_points=None,
|
|
176
|
+
method="octic" # Options: 'quintic', 'octic'
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
Redistributes geometry nodes to align with predicted Transition State (TS).
|
|
180
|
+
"""
|
|
181
|
+
geom = np.asarray(geometry_list, dtype=float)
|
|
182
|
+
energies = np.asarray(energy_list, dtype=float)
|
|
183
|
+
n_old = len(geom)
|
|
184
|
+
|
|
185
|
+
if n_points is None:
|
|
186
|
+
n_points = n_old
|
|
187
|
+
|
|
188
|
+
natom = geom.shape[1]
|
|
189
|
+
geom_flat = geom.reshape(n_old, -1)
|
|
190
|
+
gradients = np.asarray(gradient_list, dtype=float).reshape(n_old, -1)
|
|
191
|
+
|
|
192
|
+
# 1. Path length calculation
|
|
193
|
+
s_cum = _cumulative_path_length(geom_flat)
|
|
194
|
+
total_length = s_cum[-1]
|
|
195
|
+
|
|
196
|
+
if total_length < 1e-12 or n_old < 3:
|
|
197
|
+
return geom.copy()
|
|
198
|
+
|
|
199
|
+
# 2. Calculate properties along the path
|
|
200
|
+
curvatures, _, projected_gradients = _estimate_curvature_and_tangents(gradients, geom)
|
|
201
|
+
|
|
202
|
+
# 3. Identify Anchors
|
|
203
|
+
anchors = [(0, 0.0), (n_points - 1, total_length)]
|
|
204
|
+
|
|
205
|
+
# Scan for local maxima
|
|
206
|
+
for i in range(1, n_old - 1):
|
|
207
|
+
if energies[i] > energies[i-1] and energies[i] > energies[i+1]:
|
|
208
|
+
|
|
209
|
+
s_prev = s_cum[i-1] - s_cum[i]
|
|
210
|
+
s_curr = 0.0
|
|
211
|
+
s_next = s_cum[i+1] - s_cum[i]
|
|
212
|
+
|
|
213
|
+
s_vals = [s_prev, s_curr, s_next]
|
|
214
|
+
E_vals = [energies[i-1], energies[i], energies[i+1]]
|
|
215
|
+
g_vals = [projected_gradients[i-1], projected_gradients[i], projected_gradients[i+1]]
|
|
216
|
+
c_vals = [curvatures[i-1], curvatures[i], curvatures[i+1]]
|
|
217
|
+
|
|
218
|
+
s_ts_local = None
|
|
219
|
+
print(f"NODE {i}: Detected local maximum at s={s_cum[i]:.4f}, E={energies[i]:.4f}")
|
|
220
|
+
# --- Attempt Method 2: Octic (9-point) ---
|
|
221
|
+
if method == "octic":
|
|
222
|
+
s_ts_local = _solve_polynomial_max(s_vals, E_vals, g_vals, c_vals)
|
|
223
|
+
|
|
224
|
+
# --- Fallback/Method 1: Quintic (6-point) ---
|
|
225
|
+
if s_ts_local is None:
|
|
226
|
+
# print(f"Node {i}: Fallback to Quintic fitting.")
|
|
227
|
+
s_ts_local = _solve_polynomial_max(s_vals, E_vals, g_vals, None)
|
|
228
|
+
|
|
229
|
+
# --- Finalize Position ---
|
|
230
|
+
if s_ts_local is not None:
|
|
231
|
+
s_ts_global = s_cum[i] + s_ts_local
|
|
232
|
+
|
|
233
|
+
# Map old index 'i' to new index 'j'
|
|
234
|
+
j = int(round(i * (n_points - 1) / (n_old - 1)))
|
|
235
|
+
|
|
236
|
+
if 0 < j < n_points - 1:
|
|
237
|
+
anchors.append((j, s_ts_global))
|
|
238
|
+
|
|
239
|
+
# 4. Process Anchors & Interpolate
|
|
240
|
+
anchors.sort(key=lambda x: x[0])
|
|
241
|
+
|
|
242
|
+
unique_anchors = [anchors[0]]
|
|
243
|
+
for k in range(1, len(anchors)):
|
|
244
|
+
curr_idx, curr_s = anchors[k]
|
|
245
|
+
prev_idx, prev_s = unique_anchors[-1]
|
|
246
|
+
|
|
247
|
+
if curr_idx > prev_idx:
|
|
248
|
+
if curr_s <= prev_s:
|
|
249
|
+
curr_s = prev_s + 1e-6
|
|
250
|
+
unique_anchors.append((curr_idx, curr_s))
|
|
251
|
+
|
|
252
|
+
if unique_anchors[-1][0] != n_points - 1:
|
|
253
|
+
unique_anchors.append((n_points - 1, total_length))
|
|
254
|
+
|
|
255
|
+
# Construct Target Grid
|
|
256
|
+
target_s = np.zeros(n_points)
|
|
257
|
+
for k in range(len(unique_anchors) - 1):
|
|
258
|
+
idx_start, s_start = unique_anchors[k]
|
|
259
|
+
idx_end, s_end = unique_anchors[k+1]
|
|
260
|
+
count = idx_end - idx_start + 1
|
|
261
|
+
target_s[idx_start : idx_end + 1] = np.linspace(s_start, s_end, count)
|
|
262
|
+
|
|
263
|
+
# Interpolate Geometry
|
|
264
|
+
new_flat_geom = np.zeros((n_points, geom_flat.shape[1]))
|
|
265
|
+
for dim in range(geom_flat.shape[1]):
|
|
266
|
+
new_flat_geom[:, dim] = np.interp(target_s, s_cum, geom_flat[:, dim])
|
|
267
|
+
|
|
268
|
+
new_geom = new_flat_geom.reshape(n_points, natom, 3)
|
|
269
|
+
new_geom[0] = geom[0]
|
|
270
|
+
new_geom[-1] = geom[-1]
|
|
271
|
+
|
|
272
|
+
return new_geom
|
|
273
|
+
|
|
274
|
+
def distribute_geometry_by_length(geometry_list, angstrom_spacing):
|
|
275
|
+
"""Distribute geometries by specified distance spacing"""
|
|
276
|
+
path_length_list = calc_path_length_list(geometry_list)
|
|
277
|
+
total_length = path_length_list[-1]
|
|
278
|
+
new_geometry_list = []
|
|
279
|
+
|
|
280
|
+
# Avoid zero length
|
|
281
|
+
if total_length < 1e-8:
|
|
282
|
+
return [geometry_list[0]]
|
|
283
|
+
|
|
284
|
+
max_steps = int(total_length // angstrom_spacing)
|
|
285
|
+
# Start point
|
|
286
|
+
new_geometry_list.append(geometry_list[0])
|
|
287
|
+
|
|
288
|
+
for i in range(1, max_steps + 1):
|
|
289
|
+
dist = i * angstrom_spacing
|
|
290
|
+
if dist >= total_length:
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
for j in range(len(path_length_list) - 1):
|
|
294
|
+
if path_length_list[j] <= dist <= path_length_list[j+1]:
|
|
295
|
+
delta_t = (dist - path_length_list[j]) / (path_length_list[j+1] - path_length_list[j])
|
|
296
|
+
new_geometry = geometry_list[j] + (geometry_list[j+1] - geometry_list[j]) * delta_t
|
|
297
|
+
new_geometry_list.append(new_geometry)
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
# Add the last point if it is far enough from the previous one,
|
|
301
|
+
# following the original specification of appending the final geometry.
|
|
302
|
+
if len(new_geometry_list) == 0 or np.linalg.norm(new_geometry_list[-1] - geometry_list[-1]) > 1e-4:
|
|
303
|
+
new_geometry_list.append(geometry_list[-1])
|
|
304
|
+
|
|
305
|
+
return new_geometry_list
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def distribute_geometry(geometry_list):
|
|
309
|
+
"""Distribute geometries evenly along the path"""
|
|
310
|
+
nnode = len(geometry_list)
|
|
311
|
+
path_length_list = calc_path_length_list(geometry_list)
|
|
312
|
+
total_length = path_length_list[-1]
|
|
313
|
+
|
|
314
|
+
if total_length < 1e-8:
|
|
315
|
+
return list(geometry_list)
|
|
316
|
+
|
|
317
|
+
node_dist = total_length / (nnode-1)
|
|
318
|
+
|
|
319
|
+
new_geometry_list = [geometry_list[0]]
|
|
320
|
+
for i in range(1, nnode-1):
|
|
321
|
+
dist = i * node_dist
|
|
322
|
+
# Search logic
|
|
323
|
+
found = False
|
|
324
|
+
for j in range(len(path_length_list)-1):
|
|
325
|
+
if path_length_list[j] <= dist <= path_length_list[j+1]:
|
|
326
|
+
delta_t = (dist - path_length_list[j]) / (path_length_list[j+1] - path_length_list[j])
|
|
327
|
+
new_geometry = geometry_list[j] + (geometry_list[j+1] - geometry_list[j]) * delta_t
|
|
328
|
+
new_geometry_list.append(new_geometry)
|
|
329
|
+
found = True
|
|
330
|
+
break
|
|
331
|
+
if not found:
|
|
332
|
+
# Safeguard for cases out of range due to numerical error
|
|
333
|
+
new_geometry_list.append(geometry_list[-1])
|
|
334
|
+
|
|
335
|
+
new_geometry_list.append(geometry_list[-1])
|
|
336
|
+
return new_geometry_list
|
|
337
|
+
|
|
338
|
+
def distribute_geometry_by_energy(geometry_list, energy_list, gradient_list=None, n_points=None, smoothing=0.1):
|
|
339
|
+
"""
|
|
340
|
+
Distribute geometries concentrating on ALL high-energy regions (Multiple Peaks)
|
|
341
|
+
using Linear Interpolation.
|
|
342
|
+
|
|
343
|
+
Improvements:
|
|
344
|
+
- Uses 'gradient_list' (if provided) to calculate Energy Curvature for precise Peak Detection.
|
|
345
|
+
- Concentrates nodes on secondary transition states as well as the global maximum.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
geometry_list: list of np.ndarray
|
|
349
|
+
energy_list: list of float
|
|
350
|
+
gradient_list: list of np.ndarray (dE/dx). Optional.
|
|
351
|
+
n_points: int, number of output nodes (default: same as input)
|
|
352
|
+
smoothing: float, weighting factor for low energy regions
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
new_geometry_list: list of np.ndarray
|
|
356
|
+
"""
|
|
357
|
+
if len(geometry_list) != len(energy_list):
|
|
358
|
+
raise ValueError("Length of geometry_list and energy_list must be the same.")
|
|
359
|
+
|
|
360
|
+
if n_points is None:
|
|
361
|
+
n_points = len(geometry_list)
|
|
362
|
+
|
|
363
|
+
path_length_list = calc_path_length_list(geometry_list)
|
|
364
|
+
total_length = path_length_list[-1]
|
|
365
|
+
|
|
366
|
+
if total_length < 1e-8:
|
|
367
|
+
return list(geometry_list)
|
|
368
|
+
|
|
369
|
+
geometry_arr = np.array(geometry_list)
|
|
370
|
+
energies = np.array(energy_list)
|
|
371
|
+
n_nodes = len(energies)
|
|
372
|
+
|
|
373
|
+
# --- 1. Calculate Global Energy Weights (Height) ---
|
|
374
|
+
E_min = np.min(energies)
|
|
375
|
+
E_max = np.max(energies)
|
|
376
|
+
|
|
377
|
+
if E_max - E_min < 1e-6:
|
|
378
|
+
w_global = np.zeros_like(energies)
|
|
379
|
+
else:
|
|
380
|
+
w_global = (energies - E_min) / (E_max - E_min)
|
|
381
|
+
|
|
382
|
+
# --- 2. Calculate Local Peak Weights (Convexity) ---
|
|
383
|
+
w_local = np.zeros_like(energies)
|
|
384
|
+
|
|
385
|
+
if n_nodes > 2:
|
|
386
|
+
# Identify Hills (Energy Higher than Neighbors)
|
|
387
|
+
E_center = energies[1:-1]
|
|
388
|
+
E_neighbors = (energies[:-2] + energies[2:]) / 2.0
|
|
389
|
+
# Boolean mask for hill regions
|
|
390
|
+
is_hill = E_center > E_neighbors
|
|
391
|
+
|
|
392
|
+
if gradient_list is not None:
|
|
393
|
+
# --- A. Use Gradients for Precision ---
|
|
394
|
+
grad_arr = np.array(gradient_list)
|
|
395
|
+
|
|
396
|
+
# Calculate Tangents
|
|
397
|
+
vecs = geometry_arr[1:] - geometry_arr[:-1]
|
|
398
|
+
vec_norms = np.linalg.norm(vecs, axis=(1,2))
|
|
399
|
+
valid = vec_norms > 1e-10
|
|
400
|
+
tangents = np.zeros_like(geometry_arr)
|
|
401
|
+
tangents[:-1][valid] = vecs[valid] / vec_norms[valid][:, np.newaxis, np.newaxis]
|
|
402
|
+
tangents[-1] = tangents[-2]
|
|
403
|
+
|
|
404
|
+
# Project Gradient -> Slope (dE/ds)
|
|
405
|
+
slopes = np.sum(grad_arr * tangents, axis=(1,2))
|
|
406
|
+
|
|
407
|
+
# Curvature ~ Change in Slope
|
|
408
|
+
slope_change = np.zeros_like(slopes)
|
|
409
|
+
slope_change[1:-1] = slopes[2:] - slopes[:-2]
|
|
410
|
+
|
|
411
|
+
# Assign weight based on curvature magnitude ONLY in Hill regions
|
|
412
|
+
peak_metric = np.abs(slope_change[1:-1])
|
|
413
|
+
w_local[1:-1][is_hill] = peak_metric[is_hill]
|
|
414
|
+
|
|
415
|
+
else:
|
|
416
|
+
# --- B. Fallback to Energy Convexity ---
|
|
417
|
+
convexity = E_center - E_neighbors
|
|
418
|
+
peak_score = np.maximum(convexity, 0.0)
|
|
419
|
+
w_local[1:-1] = peak_score
|
|
420
|
+
|
|
421
|
+
# Normalize local score
|
|
422
|
+
p_max = np.max(w_local)
|
|
423
|
+
if p_max > 1e-6:
|
|
424
|
+
w_local /= p_max
|
|
425
|
+
|
|
426
|
+
# Pad endpoints
|
|
427
|
+
w_local[0] = w_local[1]
|
|
428
|
+
w_local[-1] = w_local[-2]
|
|
429
|
+
|
|
430
|
+
# --- 3. Combine Weights ---
|
|
431
|
+
# Mix 50% Global Height + 50% Local Shape + Smoothing
|
|
432
|
+
weights = 0.5 * w_global + 0.5 * w_local + smoothing
|
|
433
|
+
|
|
434
|
+
# --- Calculate weighted cumulative distance ---
|
|
435
|
+
segment_dists = np.diff(path_length_list)
|
|
436
|
+
segment_weights = (weights[:-1] + weights[1:]) / 2.0
|
|
437
|
+
weighted_segments = segment_dists * segment_weights
|
|
438
|
+
|
|
439
|
+
cum_weighted_dist = np.concatenate(([0.0], np.cumsum(weighted_segments)))
|
|
440
|
+
total_weighted_length = cum_weighted_dist[-1]
|
|
441
|
+
|
|
442
|
+
# --- Determine new target distances ---
|
|
443
|
+
target_weighted_grid = np.linspace(0, total_weighted_length, n_points)
|
|
444
|
+
target_physical_dists = np.interp(target_weighted_grid, cum_weighted_dist, path_length_list)
|
|
445
|
+
|
|
446
|
+
# --- Coordinate redistribution using linear interpolation ---
|
|
447
|
+
new_geometry_list = []
|
|
448
|
+
|
|
449
|
+
for dist in target_physical_dists:
|
|
450
|
+
if dist <= 0:
|
|
451
|
+
new_geometry_list.append(geometry_list[0])
|
|
452
|
+
continue
|
|
453
|
+
if dist >= total_length:
|
|
454
|
+
new_geometry_list.append(geometry_list[-1])
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
found = False
|
|
458
|
+
for j in range(len(path_length_list) - 1):
|
|
459
|
+
if path_length_list[j] <= dist <= path_length_list[j+1]:
|
|
460
|
+
seg_len = path_length_list[j+1] - path_length_list[j]
|
|
461
|
+
delta_t = 0 if seg_len < 1e-10 else (dist - path_length_list[j]) / seg_len
|
|
462
|
+
|
|
463
|
+
vec = geometry_list[j+1] - geometry_list[j]
|
|
464
|
+
new_geometry = geometry_list[j] + vec * delta_t
|
|
465
|
+
new_geometry_list.append(new_geometry)
|
|
466
|
+
found = True
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
if not found:
|
|
470
|
+
new_geometry_list.append(geometry_list[-1])
|
|
471
|
+
|
|
472
|
+
return new_geometry_list
|
|
473
|
+
|