3535
3636import torch
3737import numpy as np
38+ import math
39+ from scipy .special import gamma
3840
3941def rho_gaussian (q , Q , sigma ):
4042 """
4143 Args:
4244 * q (Tensor(ndof) ) - coordinate of the current point of interest
4345 * Q (Tensor(ntraj, ndof)) - coordinates of all trajectories
44- * sigma (Tensor(ntraj, ndof) ) - width parameter for each trajectory
46+ * sigma (float ) - width parameter for each trajectory
4547
4648 Returns:
4749 Tensor(1) - probability density at the point of interest
4850
4951 """
5052 _SQRT_2PI = torch .sqrt (torch .tensor (2.0 * torch .pi ))
51-
53+
5254 ntraj , ndof = Q .shape [0 ], Q .shape [1 ]
5355 return torch .sum ( (1.0 / ntraj ) * torch .prod ( torch .exp ( - 0.5 * (q - Q )** 2 / sigma ** 2 )/ (sigma * _SQRT_2PI ), 1 , False ) )
5456
5557
5658def rho_lorentzian (q , Q , sigma ):
5759 """
5860 Args:
59- * q (Tensor(ndof) ) - coordinate of the current point of interest
60- * Q (Tensor(ntraj, ndof)) - coordinates of all trajectories
61- * sigma (Tensor(ntraj, ndof) ) - width parameter for each trajectory
61+ * q (Tensor(..., ndof) ) - coordinate of the current point of interest
62+ * Q (Tensor(..., ntraj, ndof)) - coordinates of all trajectories
63+ * sigma (float ) - width parameter for each trajectory
6264
6365 Returns:
6466 Tensor(1) - probability density at the point of interest
6567 """
66- ntraj , ndof = Q .shape [0 ], Q .shape [1 ]
67- y = torch .sum ( (1.0 / ntraj ) * torch .prod ( (1.0 / torch .pi ) * sigma / ( (q - Q )** 2 + sigma ** 2 ), 1 , False ) )
68- return y
68+ # Old way
69+ #ntraj, ndof = Q.shape[0], Q.shape[1]
70+ #y = torch.sum( (1.0/ntraj) * torch.prod( (1.0/torch.pi) * sigma/( (q-Q)**2 + sigma**2 ), 1, False) )
71+
72+ # New way
73+ #ntraj, ndof = Q.shape[-2], Q.shape[-1]
74+ #norm = gamma(ndof / 2) / (math.pi**(ndof / 2) * sigma**(ndof - 1))
75+ #
76+ #LZ = sigma/( ((q-Q)**2).sum(axis=-1) + sigma**2)
77+ #f = torch.tensor( (1.0/ntraj) * norm * LZ.sum(axis=0), requires_grad=True)
78+
79+ ntraj , ndof = Q .shape
80+
81+ gamma_term = torch .exp (torch .lgamma (torch .tensor (ndof / 2.0 , dtype = Q .dtype , device = Q .device )))
82+ norm = gamma_term / (math .pi ** ( 1 + ndof / 2 ) * sigma ** (ndof - 1 ))
83+
84+ r2 = ((q - Q )** 2 ).sum (dim = 1 ) # shape: (ntraj,)
85+ LZ = sigma / (r2 + sigma ** 2 ) # shape: (ntraj,)
86+
87+ f = (1.0 / ntraj ) * norm * LZ .sum () # no tensor wrapping
88+
89+
90+ return f
91+
92+
93+ def rho_mv_cauchy (q , Q , sigma ):
94+ """
95+ Multivariate Cauchy: https://en.wikipedia.org/wiki/Cauchy_distribution
96+
97+ Args:
98+ * q (Tensor(..., ndof) ) - coordinate of the current point of interest
99+ * Q (Tensor(..., ntraj, ndof)) - coordinates of all trajectories
100+ * sigma (float) - width parameter for each trajectory
101+
102+ Returns:
103+ Tensor(1) - probability density at the point of interest
104+ """
105+
106+ ntraj , ndof = Q .shape
107+ # Normalization constant
108+ num = torch .lgamma (torch .tensor ((ndof + 1 ) / 2 ))
109+ denom = (
110+ torch .lgamma (torch .tensor (0.5 )) +
111+ 0.5 * ndof * math .log (math .pi ) +
112+ ndof * math .log (sigma )
113+ )
114+ norm = torch .exp (num - denom )
115+
116+ r2 = ((q - Q )** 2 ).sum (dim = 1 ) # shape: (ntraj,)
117+ LZ = (torch .tensor (1.0 ) + r2 / sigma ** 2 ) ** (- (ndof + 1 ) / 2 ) # shape: (ntraj,)
118+
119+ f = (1.0 / ntraj ) * norm * LZ .sum () # no tensor wrapping
120+
121+ return f
122+
69123
70124
71125def quantum_potential_original (Q , sigma , mass , TBF ):
72126 """
73127 Args:
74128 * Q (Tensor(ntraj, ndof)) - coordinates of all trajectories
75- * sigma (Tensor(ndof) ) - width parameters for each trajectory
76- * mass ( Tensor(1, ndof)) - masses of all DOFs, same for all trajectories
129+ * sigma (float ) - width parameters for each trajectory
130+ * mass ( Tensor( ndof)) - masses of all DOFs, same for all trajectories
77131 * TBF (object) - basis function reference (`rho_gaussian` or `rho_lorentzian`)
78132
79133 Returns:
80134 Tensor(1) - quantum potential summed over all trajectory points
81135 """
82136
83- ntraj , ndof = Q .shape [0 ], Q .shape [1 ]
137+ ntraj , ndof = Q .shape [- 2 ], Q .shape [- 1 ]
84138 U = torch .zeros ( (1 ,), requires_grad = True )
85139 for k in range (ntraj ):
86140 f = TBF (Q [k ], Q , sigma );
87141 [deriv1 ] = torch .autograd .grad (f , [Q ], create_graph = True , retain_graph = True );
88142 for i in range (ndof ):
89143 [deriv2 ] = torch .autograd .grad (deriv1 [k ,i ], [Q ], create_graph = True , retain_graph = True );
90- u = - (0.25 / mass [0 , i ])* ( deriv2 [k , i ]/ f - 0.5 * (deriv1 [k ,i ]/ f )** 2 );
144+ u = - (0.25 / mass [i ])* ( deriv2 [k , i ]/ f - 0.5 * (deriv1 [k ,i ]/ f )** 2 );
91145 U = U + u
92146 return U
93147
@@ -97,30 +151,76 @@ def quantum_potential_original_gen(q, Q, sigma, mass, TBF):
97151 Args:
98152 * Q (Tensor(ntraj, ndof)) - coordinates of all trajectories
99153 * sigma (Tensor(ndof)) - width parameters for each trajectory
100- * mass ( Tensor(1, ndof)) - masses of all DOFs, same for all trajectories
154+ * mass ( Tensor(ndof)) - masses of all DOFs, same for all trajectories
101155 * TBF (object) - basis function reference (`rho_gaussian` or `rho_lorentzian`)
102156
103157 Returns:
104158 Tensor(1) - quantum potential summed over all trajectory points
105159 """
106160
107- ntraj , ndof = Q .shape [0 ], Q .shape [1 ]
161+ ntraj , ndof = Q .shape [- 2 ], Q .shape [- 1 ]
108162 U = torch .zeros ( (1 ,), requires_grad = True )
109163 f = TBF (q , Q , sigma );
110164 [deriv1 ] = torch .autograd .grad (f , [q ], create_graph = True , retain_graph = True );
111165 for i in range (ndof ):
112166 [deriv2 ] = torch .autograd .grad (deriv1 [i ], [q ], create_graph = True , retain_graph = True );
113- u = - (0.25 / mass [0 , i ])* ( deriv2 [i ]/ f - 0.5 * (deriv1 [i ]/ f )** 2 );
167+ u = - (0.25 / mass [i ])* ( deriv2 [i ]/ f - 0.5 * (deriv1 [i ]/ f )** 2 );
114168 U = U + u
115169 return U
116170
117171
172+ def quantum_potential (Q , sigma , mass , TBF ):
173+ """
174+ Compute quantum potential in a partially vectorized way.
118175
176+ ### BUT, HOPEFULLY, THIS IS CORRECT ###
119177
120- def quantum_potential (Q , sigma , mass , TBF ):
178+ Args:
179+ Q (Tensor): shape (ntraj, ndof), requires_grad=True
180+ sigma (float): scalar width
181+ mass (Tensor): shape (ndof,)
182+ TBF (function): q, Q, sigma → scalar
183+
184+ Returns:
185+ Tensor(1,) — scalar quantum potential
186+ """
187+
188+ ntraj , ndof = Q .shape
189+ assert Q .requires_grad , "Q must have requires_grad=True"
190+
191+ U = 0.0
192+ for k in range (ntraj ):
193+ # Compute scalar density f_k = rho(q_k; Q)
194+ f_k = TBF (Q [k ], Q , sigma ) # scalar
195+
196+ # First derivative: grad f_k w.r.t Q → shape: (ntraj, ndof)
197+ dfk = torch .autograd .grad (f_k , Q , create_graph = True , retain_graph = True )[0 ]
198+
199+ # Get ∂f_k/∂Q[k, i] across i
200+ dfk_k = dfk [k ] # shape: (ndof,)
201+
202+ # Vectorized second derivatives: loop over i, but vectorized
203+ d2fk_k = torch .zeros (ndof , dtype = Q .dtype , device = Q .device )
204+ for i in range (ndof ):
205+ grad_i = torch .autograd .grad (dfk [k , i ], Q , create_graph = True , retain_graph = True )[0 ]
206+ d2fk_k [i ] = grad_i [k , i ] # ∂²f_k / ∂Q[k,i]²
207+
208+ # Compute quantum potential contribution from trajectory k
209+ f_k_safe = f_k + 1e-12 # prevent divide-by-zero
210+ term1 = d2fk_k / f_k_safe
211+ term2 = 0.5 * (dfk_k / f_k_safe ) ** 2
212+ U_k = - 0.25 / mass * (term1 - term2 ) # shape: (ndof,)
213+ U += U_k .sum ()
214+
215+ return U
216+
217+
218+ def quantum_potential_old (Q , sigma , mass , TBF ):
121219 """
122220 Compute quantum potential in a fully vectorized way.
123221
222+ ### THIS WAS INCORRECT MATHEMATICALLY ###
223+
124224 Args:
125225 Q (Tensor): shape (ntraj, ndof), requires_grad=True
126226 sigma (Tensor): shape (ntraj, ndof) or (ndof,)
@@ -261,7 +361,7 @@ def init_variables(ntraj, opt):
261361 q .requires_grad_ (True )
262362 p .requires_grad_ (True )
263363
264- masses = torch .tensor ([[ mass , mass ] ])
364+ masses = torch .tensor ([mass , mass ])
265365
266366 return q , p , masses
267367
@@ -272,7 +372,7 @@ def md( q, p, mass_mat, params ):
272372 Args:
273373 * q (Tensor(ntraj, ndof)) - coordinates of all trajectories
274374 * p (Tensor(ntraj, ndof)) - momenta of all trajectories
275- * mass_mat (Tensor(1, ndof)) - masses for all DOFs (same for all trajectories)
375+ * mass_mat (Tensor(ndof)) - masses for all DOFs (same for all trajectories)
276376 * params:
277377 - nsteps (int) - how many steps to do
278378 - dt (float) - integration timestep [in a.u.]
0 commit comments