Skip to content

Commit f0ad9db

Browse files
authored
Merge pull request #118 from leila-pujal/derivatives
Speed up gradient and hessian computation Closes #99
2 parents c826871 + f78ac3b commit f0ad9db

File tree

4 files changed

+916
-97
lines changed

4 files changed

+916
-97
lines changed

gbasis/evals/_deriv.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,238 @@ def _eval_deriv_contractions(coords, orders, center, angmom_comps, alphas, prim_
140140

141141
norm = norm.T[:, :, np.newaxis]
142142
return np.tensordot(prim_coeffs, norm * zeroth_part * deriv_part, (0, 0))
143+
144+
145+
def _eval_first_second_order_deriv_contractions(
146+
coords, orders, center, angmom_comps, alphas, prim_coeffs, norm
147+
):
148+
"""Return the evaluation of direct 1st and 2nd derivative orders of a Cartesian contraction.
149+
150+
Parameters
151+
----------
152+
coords : np.ndarray(N, 3)
153+
Point in space where the derivative of the Gaussian primitive is evaluated.
154+
Coordinates must be given as a two dimensional array, even if only one point is given.
155+
orders : np.ndarray(3,)
156+
Orders of the derivative.
157+
All orders must be lower than 2.
158+
Negative orders are treated as zero orders.
159+
center : np.ndarray(3,)
160+
Center of the Gaussian primitive.
161+
angmom_comps : np.ndarray(L, 3)
162+
Components of the angular momentum, :math:`(a_x, a_y, a_z)`.
163+
Angular momentum components must be given as a two dimensional array, even if only one
164+
set of components is given.
165+
alphas : np.ndarray(K,)
166+
Values of the (square root of the) precisions of the primitives.
167+
prim_coeffs : np.ndarray(K, M)
168+
Contraction coefficients of the primitives.
169+
The coefficients always correspond to generalized contractions, i.e. two-dimensional array
170+
where the first index corresponds to the primitive and the second index corresponds to the
171+
contraction (with the same exponents and angular momentum).
172+
norm : np.ndarray(L, K)
173+
Normalization constants for the primitives in each contraction.
174+
175+
Returns
176+
-------
177+
derivative : np.ndarray(M, L, N)
178+
Evaluation of the derivative at each given coordinate.
179+
Dimension 0 corresponds to the contraction, with `M` as the number of given contractions.
180+
Dimension 1 corresponds to the angular momentum vector, ordered as in `angmom_comps`.
181+
Dimension 2 corresponds to the point at which the derivative is evaluated, ordered as in
182+
`coords`.
183+
184+
Notes
185+
-----
186+
The input is not checked. This means that you must provide the parameters as they are specified
187+
in the docstring. They must all be `numpy` arrays with the **correct shape**.
188+
189+
Pople style basis sets are not supported. If multiple angular momentum vectors (with different
190+
angular momentum) and multiple contraction coefficients are provided, it is **not assumed** that
191+
the angular momentum vector should be paired up with the contraction coefficients. In fact, each
192+
angular momentum vector will create multiple contractions according to the given coefficients.
193+
194+
"""
195+
196+
# Useful variables
197+
new_coords = coords.T - center[None, :].T
198+
gauss = np.exp(-alphas[:, None, None] * (new_coords ** 2))
199+
200+
# Filters derivative orders
201+
indices_noderiv = orders <= 0
202+
indices_first_deriv = orders == 1
203+
indices_second_deriv = orders == 2
204+
205+
# Zeroth order derivative
206+
raw_zeroth_coords = new_coords[indices_noderiv]
207+
zeroth_angmom_comps = angmom_comps.T[indices_noderiv]
208+
zeroth_gauss = gauss[:, indices_noderiv]
209+
zeroth_coords = raw_zeroth_coords[:, None, :] ** zeroth_angmom_comps[:, :, None]
210+
raw_zeroth_deriv = zeroth_coords[None, :, :, :] * zeroth_gauss[:, :, None, :]
211+
zeroth_deriv = np.prod(raw_zeroth_deriv, axis=1)
212+
213+
# Initialize first and second derivative variables with shape = zeroth_deriv.shape
214+
first_deriv = np.ones(zeroth_deriv.shape)
215+
second_deriv = np.ones(zeroth_deriv.shape)
216+
217+
# Calling 1st and 2nd derivatives functions for different combination of orders
218+
if indices_first_deriv.any():
219+
first_deriv = _first_derivative(new_coords, gauss, indices_first_deriv,
220+
angmom_comps, alphas)
221+
if indices_second_deriv.any():
222+
second_deriv = _second_derivative(new_coords, gauss, indices_second_deriv,
223+
angmom_comps, alphas)
224+
elif indices_second_deriv.any():
225+
second_deriv = _second_derivative(new_coords, gauss, indices_second_deriv,
226+
angmom_comps, alphas)
227+
if indices_first_deriv.any():
228+
first_deriv = _first_derivative(new_coords, gauss, indices_first_deriv,
229+
angmom_comps, alphas)
230+
# Combining all the derivatives
231+
norm = norm.T[:, :, np.newaxis]
232+
output = np.tensordot(prim_coeffs, norm * zeroth_deriv * first_deriv * second_deriv, (0, 0))
233+
234+
return output
235+
236+
237+
def _first_derivative(center_coords, gauss, indices_first_deriv, angmom_comps, alphas):
238+
r"""Help function for calculation of explicit first derivative order for contracted gaussian.
239+
240+
Parameters
241+
----------
242+
center_coords : np.ndarray(N, 3)
243+
Shifted coordinates center_coords = coords - center
244+
gauss : np.ndarray(K, 3, N)
245+
variable containing ..math::e^{-\alpha\left(x-X_{A}\right)^{2}}
246+
indices_first_deriv: boolean np.array(L, 3)
247+
array contaning boolean values corresponding to coordinates for first derivative
248+
angmom_comps : np.ndarray(L, 3)
249+
Components of the angular momentum, :math:`(a_x, a_y, a_z)`.
250+
Angular momentum components must be given as a two dimensional array, even if only one
251+
set of components is given.
252+
alphas : np.ndarray(K,)
253+
Values of the (square root of the) precisions of the primitives.
254+
255+
Returns
256+
-------
257+
first order derivative : np.ndarray(K, L, N)
258+
Evaluation of first derivative at each given coordinate.
259+
Dimension 0 corresponds to number of primitives.
260+
Dimension 1 corresponds to the angular momentum vector, ordered as in `angmom_comps`.
261+
Dimension 2 corresponds to the point at which the derivative is evaluated, ordered as in
262+
`coords`.
263+
264+
Notes
265+
-----
266+
This is a helper function for _eval_first_second_order_deriv_contractions() and gives an
267+
intermediate output that needs to be further processed to match general organization in Gbasis
268+
269+
"""
270+
271+
first_coords = center_coords[indices_first_deriv]
272+
first_gauss = gauss[:, indices_first_deriv]
273+
first_ang_comp = angmom_comps.T[indices_first_deriv]
274+
# Indices to filter for ang momentum at the end
275+
n_0_indices = first_ang_comp == 0
276+
277+
power_part_1 = first_ang_comp - 1
278+
# NOTE: the negative indices must be turned into zeros because (x-Xa) terms are sometimes
279+
# zero (and negative power is undefined).
280+
power_part_1[power_part_1 < 0] = 0
281+
part1 = first_coords[:, None, :] ** power_part_1[:, :, None]
282+
part2 = (2 * alphas[:, None, None]) * (first_coords ** 2)
283+
part2 = first_ang_comp[None, :, :, None] - part2[:, :, None, :]
284+
# NOTE: Using an array of ones with same shape as first_ang_comp to power part2_zero_ang_mom
285+
# variable in order to get the same shape as part2. This is done in order to make easier
286+
# to filter at the end for the angular components corresponding to n=0
287+
array_ones = np.ones(first_ang_comp.shape)
288+
part2_n_0 = -2 * alphas[:, None, None, None] * (
289+
first_coords[:, None, :] ** array_ones[:, :, None])
290+
raw_first_deriv = part1 * part2
291+
# Substitute angular components n=0 with correct derivative
292+
raw_first_deriv[:, n_0_indices, :] = part2_n_0[:, n_0_indices, :]
293+
raw_first_deriv = raw_first_deriv * first_gauss[:, :, None, :]
294+
first_deriv = np.prod(raw_first_deriv, axis=1)
295+
296+
return first_deriv
297+
298+
299+
def _second_derivative(center_coords, gauss, indices_second_deriv, angmom_comps, alphas):
300+
r"""Help function for calculation of explicit second derivative order for contracted gaussian.
301+
302+
Parameters
303+
----------
304+
center_coords : np.ndarray(N, 3)
305+
Shifted coordinates center_coords = coords - center
306+
gauss : np.ndarray(K, 3, N)
307+
variable containing ..math::e^{-\alpha\left(x-X_{A}\right)^{2}}
308+
indices_second_deriv: boolean np.array(L, 3)
309+
array contaning boolean values corresponding to coordinates for second derivative
310+
angmom_comps : np.ndarray(L, 3)
311+
Components of the angular momentum, :math:`(a_x, a_y, a_z)`.
312+
Angular momentum components must be given as a two dimensional array, even if only one
313+
set of components is given.
314+
alphas : np.ndarray(K,)
315+
Values of the (square root of the) precisions of the primitives.
316+
317+
Returns
318+
-------
319+
first order derivative : np.ndarray(K, L, N)
320+
Evaluation of second derivative at each given coordinate.
321+
Dimension 0 corresponds to number of primitives.
322+
Dimension 1 corresponds to the angular momentum vector, ordered as in `angmom_comps`.
323+
Dimension 2 corresponds to the point at which the derivative is evaluated, ordered as in
324+
`coords`.
325+
326+
Notes
327+
-----
328+
This is a helper function for _eval_first_second_order_deriv_contractions() and gives an
329+
intermediate output that needs to be further processed to match general organization in Gbasis
330+
331+
"""
332+
333+
second_coords = center_coords[indices_second_deriv]
334+
second_gauss = gauss[:, indices_second_deriv]
335+
second_ang_comp = angmom_comps.T[indices_second_deriv]
336+
# NOTE: As for the first derivative, using an array of ones with shape=second_ang_comp.shape
337+
# to match the shape of different variables, corresponding to calculations of n=0 and n=1
338+
# second derivatives, to make easier at the end to combine them.
339+
array_ones = np.ones(second_ang_comp.shape)
340+
# Indices to filter for ang momentum at the end
341+
n_1_indices = second_ang_comp == 1
342+
n_2_indices = second_ang_comp >= 2
343+
344+
# angular momentum == 0
345+
total_n_0 = ((4 * alphas[:, None, None] ** 2) * (second_coords ** 2)) - \
346+
(2 * alphas[:, None, None])
347+
raw_second_deriv = total_n_0[:, :, None, :] ** array_ones[None, :, :, None]
348+
# angular momentum == 1
349+
if any(second_ang_comp[0] == 1):
350+
total_n_1 = ((4 * alphas[:, None, None] ** 2) * (second_coords ** 3)) \
351+
- ((6 * alphas[:, None, None]) * second_coords)
352+
total_n_1 = total_n_1[:, :, None, :] ** array_ones[None, :, :, None]
353+
# Substitute angular components n=1 with correct derivative
354+
raw_second_deriv[:, n_1_indices, :] = total_n_1[:, n_1_indices, :]
355+
# angular momentum >= 2
356+
if any(second_ang_comp[0] >= 2):
357+
# Calculating ..math:: \left(x-X_{A}\right)^{n-2}
358+
power_part_1 = second_ang_comp - 2
359+
# NOTE: the negative indices must be turned into zeros because (x-Xa)
360+
# terms are sometimes zeros (and negative power is undefined).
361+
power_part_1[power_part_1 < 0] = 0
362+
part1_n_2 = second_coords[:, None, :] ** (power_part_1[:, :, None])
363+
# Calculating
364+
# ..math:: 4 \alpha^{2}\left(x-X_{A}\right)^{4}-
365+
# \alpha(4 n+2)\left(x-X_{A}\right)^{2}+n(n-1)
366+
part2_1_n_2 = (4 * alphas[:, None, None] ** 2) * (second_coords ** 4)
367+
part2_2_n_2 = alphas[:, None, None, None] * (4 * second_ang_comp[:, :, None] + 2) \
368+
* second_coords[:, None, :] ** 2
369+
part2_3_n_2 = second_ang_comp * (second_ang_comp - 1)
370+
part2_n_2 = part2_1_n_2[:, :, None, :] - part2_2_n_2 + part2_3_n_2[None, :, :, None]
371+
total_n_2 = part1_n_2[None, :, :, :] * part2_n_2
372+
# Substitute angular components n=2 with correct derivative
373+
raw_second_deriv[:, n_2_indices, :] = total_n_2[:, n_2_indices, :]
374+
raw_second_deriv = raw_second_deriv * second_gauss[:, :, None, :]
375+
second_deriv = np.prod(raw_second_deriv, axis=1)
376+
377+
return second_deriv

0 commit comments

Comments
 (0)