@@ -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