Skip to content

Commit 22ea5b5

Browse files
committed
Added a suite for Bohmian dynamics in Libra
1 parent 4e34ce9 commit 22ea5b5

File tree

4 files changed

+560
-2
lines changed

4 files changed

+560
-2
lines changed

src/libra_py/dynamics/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# ***********************************************************
2-
# * Copyright (C) 2020 Alexey V. Akimov
2+
# * Copyright (C) 2020 - 2025 Alexey V. Akimov
33
# * This file is distributed under the terms of the
44
# * GNU General Public License as published by the
55
# * Free Software Foundation; either version 3 of the
66
# * License, or (at your option) any later version.
77
# * http://www.gnu.org/copyleft/gpl.txt
88
# ***********************************************************/
99

10-
__all__ = ["exact",
10+
__all__ = ["bohmian",
11+
"exact",
1112
"heom",
13+
"qtag",
1214
"tsh",
1315
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ***********************************************************
2+
# * Copyright (C) 2025 Alexey V. Akimov
3+
# * This file is distributed under the terms of the
4+
# * GNU General Public License as published by the
5+
# * Free Software Foundation; either version 3 of the
6+
# * License, or (at your option) any later version.
7+
# * http://www.gnu.org/copyleft/gpl.txt
8+
# ***********************************************************/
9+
10+
__all__ = ["compute",
11+
"plot"
12+
]
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
# *********************************************************************************
2+
# * Copyright (C) 2025 Alexey V. Akimov
3+
# *
4+
# * This file is distributed under the terms of the GNU General Public License
5+
# * as published by the Free Software Foundation, either version 3 of
6+
# * the License, or (at your option) any later version.
7+
# * See the file LICENSE in the root directory of this distribution
8+
# * or <http://www.gnu.org/licenses/>.
9+
# ***********************************************************************************
10+
"""
11+
.. module:: compute
12+
:platform: Unix
13+
:synopsis: This module implements functions for Bohmian dynamics
14+
List of functions:
15+
* rho_gaussian(q, Q, sigma)
16+
* rho_lorentzian(q, Q, sigma)
17+
* quantum_potential(Q, sigma, mass, TBF)
18+
* compute_derivatives(q, function, function_params)
19+
* compute_derivatives_hess(q, function, function_params)
20+
* init_variables(ntraj, opt)
21+
* md( q, p, mass_mat, params )
22+
23+
.. moduleauthor:: Alexey V. Akimov
24+
25+
"""
26+
27+
__author__ = "Alexey V. Akimov"
28+
__copyright__ = "Copyright 2025 Alexey V. Akimov"
29+
__credits__ = ["Alexey V. Akimov"]
30+
__license__ = "GNU-3"
31+
__version__ = "1.0"
32+
__maintainer__ = "Alexey V. Akimov"
33+
__email__ = "[email protected]"
34+
__url__ = "https://github.com/Quantum-Dynamics-Hub/libra-code"
35+
36+
import torch
37+
import numpy as np
38+
39+
def rho_gaussian(q, Q, sigma):
40+
"""
41+
Args:
42+
* q (Tensor(ndof) ) - coordinate of the current point of interest
43+
* Q (Tensor(ntraj, ndof)) - coordinates of all trajectories
44+
* sigma (Tensor(ntraj, ndof)) - width parameter for each trajectory
45+
46+
Returns:
47+
Tensor(1) - probability density at the point of interest
48+
49+
"""
50+
_SQRT_2PI = torch.sqrt(torch.tensor(2.0 * torch.pi))
51+
52+
ntraj, ndof = Q.shape[0], Q.shape[1]
53+
return torch.sum( (1.0/ntraj) * torch.prod( torch.exp( - 0.5*(q-Q)**2/sigma**2 )/(sigma * _SQRT_2PI ), 1, False) )
54+
55+
56+
def rho_lorentzian(q, Q, sigma):
57+
"""
58+
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
62+
63+
Returns:
64+
Tensor(1) - probability density at the point of interest
65+
"""
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
69+
70+
71+
def quantum_potential_orginal(Q, sigma, mass, TBF):
72+
"""
73+
Args:
74+
* 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
77+
* TBF (object) - basis function reference (`rho_gaussian` or `rho_lorentzian`)
78+
79+
Returns:
80+
Tensor(1) - quantum potential summed over all trajectory points
81+
"""
82+
83+
ntraj, ndof = Q.shape[0], Q.shape[1]
84+
U = torch.zeros( (1,), requires_grad=True)
85+
for k in range(ntraj):
86+
f = TBF(Q[k], Q, sigma);
87+
[deriv1] = torch.autograd.grad(f, [Q], create_graph=True, retain_graph=True);
88+
for i in range(ndof):
89+
[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 );
91+
U = U + u
92+
return U
93+
94+
95+
def quantum_potential(Q, sigma, mass, TBF):
96+
"""
97+
Compute quantum potential in a fully vectorized way.
98+
99+
Args:
100+
Q (Tensor): shape (ntraj, ndof), requires_grad=True
101+
sigma (Tensor): shape (ntraj, ndof) or (ndof,)
102+
mass (Tensor): shape (1, ndof)
103+
TBF (callable): basis function (e.g., rho_gaussian or rho_lorentzian)
104+
105+
Returns:
106+
Tensor(1,) — scalar total quantum potential
107+
"""
108+
ntraj, ndof = Q.shape
109+
110+
# Compute rho for each trajectory point: shape (ntraj,)
111+
f_list = torch.stack([TBF(Q[k], Q, sigma) for k in range(ntraj)], dim=0) # shape: (ntraj,)
112+
113+
# Ensure Q requires grad
114+
Q.requires_grad_(True)
115+
116+
# Compute first derivative: shape (ntraj, ndof)
117+
grad_f = torch.autograd.grad(f_list.sum(), Q, create_graph=True)[0] # shape: (ntraj, ndof)
118+
119+
# Compute second derivatives (Hessian diagonal elements)
120+
deriv2 = torch.zeros_like(Q)
121+
for i in range(ndof):
122+
grad_i = torch.autograd.grad(grad_f[:, i].sum(), Q, create_graph=True)[0] # shape: (ntraj, ndof)
123+
deriv2[:, i] = grad_i[:, i] # extract diagonal part only
124+
125+
# Expand mass to match shape (ntraj, ndof)
126+
mass_exp = mass.expand(ntraj, -1)
127+
128+
# Compute quantum potential batch-wise
129+
term1 = deriv2 / f_list.unsqueeze(1)
130+
term2 = 0.5 * (grad_f / f_list.unsqueeze(1)) ** 2
131+
U = -0.25 / mass_exp * (term1 - term2) # shape: (ntraj, ndof)
132+
133+
# Sum over trajectories and DOFs
134+
U_total = U.sum()
135+
136+
return U_total
137+
138+
139+
140+
141+
142+
def compute_derivatives(q, function, function_params):
143+
"""
144+
Args:
145+
* q (Tensor(ntraj, ndof)) - coordinates of all trajectories
146+
* function (object) - reference to PyTorch function that computes energy
147+
the functions should be called as `function(q function_params)`
148+
* function_params (dict) - parameters of the model Hamiltonian
149+
150+
Returns:
151+
* f (Tensor(0)) - energy
152+
* grad (Tensor(ntraj, ndof)) - gradients of the Hamiltonian with respect to
153+
all DOFs of all trajectories
154+
"""
155+
156+
ntraj, ndof = q.shape[0], q.shape[1]
157+
158+
# Compute the function itself
159+
f = function(q, function_params)
160+
161+
# Compute the first gradients
162+
[grad] = torch.autograd.grad(f, q, create_graph=False, retain_graph=False)
163+
164+
return f, grad
165+
166+
167+
def compute_derivatives_hess(q, function, function_params):
168+
"""
169+
Args:
170+
* q (Tensor(ntraj, ndof)) - coordinates of all trajectories
171+
* function (object) - reference to PyTorch function that computes energy
172+
the functions should be called as `function(q function_params)`
173+
* function_params (dict) - parameters of the model Hamiltonian
174+
175+
Returns:
176+
* f (Tensor(0)) - energy
177+
* grad (Tensor(ntraj, ndof)) - gradients of the Hamiltonian with respect to
178+
all DOFs of all trajectories
179+
* hess (Tensor(ntraj, ndof, ndof)) - Hessians of the Hamiltonian for all DOFs
180+
for all trajectories, but not cross-trajectory
181+
182+
Note: Hessian calculations may be quite expensive
183+
"""
184+
ntraj, ndof = q.shape[0], q.shape[1]
185+
186+
# Compute the function itself
187+
f = function(q, function_params)
188+
189+
# Compute the first gradients
190+
[grad] = torch.autograd.grad(f, q, create_graph=True, retain_graph=True)
191+
192+
# Compute the second gradients
193+
hess = torch.zeros( (ntraj, ndof, ndof) )
194+
for k in range(ntraj):
195+
for i in range(ndof):
196+
[ d2f ] = torch.autograd.grad( grad[k, i], q, create_graph=False, retain_graph=False)
197+
hess[k, i, :] = d2f[k, :]
198+
199+
return f, grad, hess
200+
201+
202+
def init_variables(ntraj, opt):
203+
"""
204+
So far, this is only good for very specific cases - the models
205+
from Wang-Martens-Zheng paper:
206+
207+
Wang, L.; Martens, C. C.; Zheng, Y. Entangled Trajectory Molecular Dynamics in Multidimensional Systems:
208+
Two-Dimensional Quantum Tunneling through the Eckart Barrier. J. Chem. Phys. 2012, 137 (3), 034113.
209+
https://doi.org/10.1063/1.4736559.
210+
211+
212+
Args:
213+
* ntraj (int) - the number of trajectories
214+
* opt (int) - the type of initial condition
215+
opt: 1 - q = (-1, 0), p = (3.0, 0.0)
216+
opt: 2 - q = (-1, 0), p = (4.0, 0.0)
217+
218+
"""
219+
220+
mass = 2000.0
221+
omega = 0.004
222+
223+
sigma_q = np.sqrt(0.5/(mass*omega))
224+
sigma_p = np.sqrt(0.5*mass*omega)
225+
226+
q_mean = torch.tensor([[-1.0, 0.0]]*ntraj)
227+
q_std = torch.tensor([[ sigma_q, sigma_q]]*ntraj)
228+
q = torch.normal(q_mean, q_std)
229+
230+
p_mean = torch.tensor([[ 3.0 , 0.0]]*ntraj)
231+
if opt == 2:
232+
p_mean = torch.tensor([[ 4.0 , 0.0]]*ntraj)
233+
p_std = torch.tensor([[ sigma_p, sigma_p]]*ntraj)
234+
p = torch.normal(p_mean, p_std)
235+
236+
q.requires_grad_(True)
237+
p.requires_grad_(True)
238+
239+
masses = torch.tensor([[mass, mass]])
240+
241+
return q, p, masses
242+
243+
244+
245+
def md( q, p, mass_mat, params ):
246+
"""
247+
Args:
248+
* q (Tensor(ntraj, ndof)) - coordinates of all trajectories
249+
* p (Tensor(ntraj, ndof)) - momenta of all trajectories
250+
* mass_mat (Tensor(1, ndof)) - masses for all DOFs (same for all trajectories)
251+
* params:
252+
- nsteps (int) - how many steps to do
253+
- dt (float) - integration timestep [in a.u.]
254+
- do_bohmian (int or Bool) - whether to include quantum potential: 0 - no, 1 - yes
255+
- prefix (string) - the name of the ".pt" file where all will be saved
256+
- ham (object) - function that defined Hamiltonian - should be called as `ham(q, ham_params)`
257+
- ham_params (dict) - parameters of the model Hamiltonian
258+
- qpot_sigmas ( Tensor(ndof)) - width paramters of the TBFs
259+
- tbf_type (object) - function that defines the type of trajectory basis functions used in computing
260+
probability density. Can be either: `rho_gaussian` or `rho_lorentzian` defined in this module
261+
If no quantum potential is used, define it as `None`
262+
263+
Returns:
264+
None, but saves the key variable in a ".pt" file
265+
"""
266+
267+
nsteps = params["nsteps"]
268+
dt = params["dt"]
269+
do_bohmian = params["do_bohmian"]
270+
ntraj = q.shape[0]
271+
ndof = q.shape[1]
272+
prefix = params["prefix"]
273+
ham = params["ham"]
274+
ham_params = params["ham_params"]
275+
sigma = params["qpot_sigmas"]
276+
tbf_type = params["tbf_type"]
277+
print_period = params["print_period"]
278+
279+
q_traj = torch.zeros( nsteps, ntraj, ndof )
280+
p_traj = torch.zeros( nsteps, ntraj, ndof )
281+
t = torch.zeros(nsteps)
282+
P = torch.zeros(nsteps)
283+
E = torch.zeros(nsteps, 4 ) # kin, pot, quantum, tot
284+
285+
print("Starting MD")
286+
E_pot, grad = compute_derivatives(q, ham, ham_params)
287+
f = -grad
288+
if do_bohmian:
289+
q_pot = quantum_potential(q, sigma, mass_mat, tbf_type)
290+
E[0,2] = q_pot.detach()/ntraj
291+
[q_force] = torch.autograd.grad( q_pot, [q], create_graph=False, retain_graph=False)
292+
f = f - q_force
293+
294+
E[0,0] = torch.sum( 0.5 * p**2/ mass_mat)/ntraj
295+
E[0,1] = E_pot.detach()/ntraj
296+
E[0,3] = E[0,0] + E[0,1] + E[0,2]
297+
P[0] = 0.0
298+
t[0] = 0.0
299+
300+
#q = q.detach().clone().requires_grad_(True)
301+
q_traj[0,:,:] = q.detach()
302+
p_traj[0,:,:] = p.detach()
303+
304+
for i in range(1,nsteps):
305+
q = q.detach().clone().requires_grad_(True)
306+
p = p.detach().clone().requires_grad_(False)
307+
308+
p = p + 0.5 * f * dt
309+
q = q + dt * p/mass_mat
310+
311+
E_pot, grad = compute_derivatives(q, ham, ham_params)
312+
f = -grad
313+
if do_bohmian:
314+
q_pot = quantum_potential(q, sigma, mass_mat, tbf_type)
315+
E[i,2] = q_pot.detach()/ntraj
316+
[q_force] = torch.autograd.grad( q_pot, [q], create_graph=False, retain_graph=False)
317+
f = f - q_force
318+
319+
p = p + 0.5 * f * dt
320+
321+
E[i,0] = torch.sum( 0.5 * p**2/ mass_mat)/ntraj
322+
E[i,1] = E_pot.detach()/ntraj
323+
E[i,3] = E[i,0] + E[i,1] + E[i,2]
324+
t[i] = i * dt
325+
q_traj[i,:,:] = q.detach()
326+
p_traj[i,:,:] = p.detach()
327+
328+
# Compute the transmission probability
329+
a = q[:,0].detach() # x-component only
330+
P[i] = a.masked_fill(a>0, 1).masked_fill(a<0, 0).sum()/ntraj # we sum up the elements that are larger than 0.0
331+
332+
if i%print_period==0:
333+
print(t[i].item(), E[i])
334+
335+
#return t, q_traj, p_traj, E, P
336+
torch.save( {"t":t, "q_traj":q_traj, "p_traj":p_traj, "E":E, "P":P }, F"{prefix}.pt" )
337+
338+
339+

0 commit comments

Comments
 (0)