Skip to main content

integral_core/
deriv.rs

1//! Geometric (nuclear-coordinate) first derivatives of integrals (L2).
2//!
3//! Derivatives are assembled from **shifted-angular-momentum value integrals**
4//! via the Gaussian center-derivative relation — no new recurrences. For a
5//! primitive Cartesian Gaussian `g_a` on center `A`,
6//!
7//! ```text
8//!   ∂/∂A_i g_a = 2α · g_{a+1_i} − a_i · g_{a−1_i}
9//! ```
10//!
11//! (raise / lower the angular momentum along axis `i`), with `α` the primitive
12//! exponent and `a_i` the Cartesian power along `i`. Because the operator of a
13//! one- or two-electron integral does not depend on the basis-function center,
14//! `∂/∂A_i ⟨g_a|O|…⟩ = ⟨∂/∂A_i g_a|O|…⟩`, so the derivative of any integral block
15//! is this same fixed linear combination of the value blocks evaluated at `l±1`
16//! on the differentiated index. The existing engines compute those shifted
17//! blocks; this module only combines them.
18//!
19//! ## The `2α` weight is folded by the caller (engine-agnostic)
20//!
21//! The `2α` weight is **per primitive**. Rather than bake an exponent into the
22//! combiner — which would only work for a per-primitive engine — the **raised**
23//! block passed to [`accumulate_center_derivative`] is already weighted by `2α`.
24//! Every value engine takes a `scale`, so:
25//!
26//! - the per-primitive **Rys** path evaluates the raised primitive block with
27//!   `scale = 2α` directly;
28//! - the contracted **OS/HGP** path folds `2α` into each primitive's contraction
29//!   coefficient (`coeff_p · 2α_p`) when it evaluates the raised contracted block.
30//!
31//! Both yield the same `2α`-weighted raised block, so one combiner serves both
32//! engines and the one- and two-electron paths alike.
33//!
34//! ## Convention
35//!
36//! The result is `∂/∂(center of the differentiated shell)` — the basis-function
37//! (center) derivative with respect to the position of the center the
38//! differentiated angular-momentum index sits on. The sign / which-center
39//! convention is documented above.
40
41use integral_math::am::{cart_components, cart_index, n_cart};
42
43/// Geometry of one center-derivative step: which angular-momentum index of a
44/// row-major block is being differentiated, along which Cartesian axis, and how
45/// that index is embedded in the block.
46///
47/// The block's indices are grouped as `[outer, l-index, inner]` (row-major): the
48/// `l`-index runs over `n_cart(l)` Cartesian components, `outer` is the product
49/// of all slower index dimensions and `inner` the product of all faster ones.
50#[derive(Debug, Clone, Copy)]
51pub struct AxisDeriv {
52    /// Angular momentum of the differentiated index.
53    pub l: usize,
54    /// Cartesian axis of the derivative: `0 = x`, `1 = y`, `2 = z`.
55    pub cart_axis: usize,
56    /// Product of the dimensions of all slower indices.
57    pub outer: usize,
58    /// Product of the dimensions of all faster indices.
59    pub inner: usize,
60}
61
62/// Accumulate the center-derivative of one row-major block along one Cartesian
63/// axis of one angular-momentum index (geometry in `d`).
64///
65/// The two inputs are the **value** blocks with the differentiated index raised
66/// and lowered. The raised block must **already be weighted by `2α`** (see the
67/// module docs):
68///
69/// - `raised`: `2α`-weighted, dims `outer × n_cart(l+1) × inner`,
70/// - `lowered`: dims `outer × n_cart(l-1) × inner` (pass `None` when `l == 0`).
71///
72/// For each component `a` (axis `d.cart_axis ∈ {0,1,2}`) the result is
73///
74/// ```text
75///   out[outer, a, inner] += scale · ( raised[outer, a+1_i, inner]
76///                                     − a_i · lowered[outer, a−1_i, inner] )
77/// ```
78///
79/// accumulated into `out` (dims `outer × n_cart(l) × inner`). `scale` is the
80/// contraction-coefficient product for this primitive (set/term).
81///
82/// This single primitive serves every index position: a one-electron bra
83/// (`outer = 1`, `inner = n_cart(lb)`), a ket (`outer = n_cart(la)`,
84/// `inner = 1`), and any of the four ERI indices (`outer`/`inner` the products of
85/// the dimensions before/after it).
86///
87/// # Panics
88/// Debug-asserts the input/output lengths are consistent and that `lowered` is
89/// present whenever `l > 0`.
90pub fn accumulate_center_derivative(
91    d: AxisDeriv,
92    scale: f64,
93    raised: &[f64],
94    lowered: Option<&[f64]>,
95    out: &mut [f64],
96) {
97    let AxisDeriv {
98        l,
99        cart_axis,
100        outer,
101        inner,
102    } = d;
103    debug_assert!(cart_axis < 3, "cart_axis must be 0, 1, or 2");
104    let nl = n_cart(l);
105    let np1 = n_cart(l + 1);
106    debug_assert_eq!(raised.len(), outer * np1 * inner, "raised block size");
107    debug_assert_eq!(out.len(), outer * nl * inner, "output block size");
108    if l > 0 {
109        debug_assert!(lowered.is_some(), "lowered block required for l > 0");
110        debug_assert_eq!(
111            lowered.unwrap().len(),
112            outer * n_cart(l - 1) * inner,
113            "lowered block size"
114        );
115    }
116    let nm1 = if l > 0 { n_cart(l - 1) } else { 0 };
117
118    for (a_idx, a) in cart_components(l).into_iter().enumerate() {
119        // Raise along the axis: a + 1_i lands at `r_idx` in shell l+1.
120        let mut ar = a;
121        ar[cart_axis] += 1;
122        let r_idx = cart_index(ar);
123
124        // Lower along the axis: a − 1_i in shell l-1, weighted by the power a_i.
125        let pw = a[cart_axis];
126        let l_idx = if pw >= 1 {
127            let mut al = a;
128            al[cart_axis] -= 1;
129            Some(cart_index(al))
130        } else {
131            None
132        };
133
134        for o in 0..outer {
135            let out_row = (o * nl + a_idx) * inner;
136            let r_row = (o * np1 + r_idx) * inner;
137            let l_row = l_idx.map(|li| (o * nm1 + li) * inner);
138            for s in 0..inner {
139                let mut v = raised[r_row + s];
140                if let Some(lr) = l_row {
141                    v -= (pw as f64) * lowered.unwrap()[lr + s];
142                }
143                out[out_row + s] += scale * v;
144            }
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    fn desc(l: usize, cart_axis: usize, outer: usize, inner: usize) -> AxisDeriv {
154        AxisDeriv {
155            l,
156            cart_axis,
157            outer,
158            inner,
159        }
160    }
161
162    // For l = 0 (s function) the derivative is purely the raise term. With the
163    // raised p-block already weighted by 2α, the x-derivative picks raised_x.
164    #[test]
165    fn s_function_derivative_is_raise_only() {
166        let raised = vec![10.0, 20.0, 30.0]; // 2α-weighted p block (n_cart(1)=3)
167        let mut out = vec![0.0; 1];
168        accumulate_center_derivative(desc(0, 0, 1, 1), 1.0, &raised, None, &mut out);
169        assert_eq!(out[0], 10.0);
170        let mut outy = vec![0.0; 1];
171        accumulate_center_derivative(desc(0, 1, 1, 1), 1.0, &raised, None, &mut outy);
172        assert_eq!(outy[0], 20.0);
173    }
174
175    // For a p function, ∂_x p_x = (2α·d)_xx − 1·s ; ∂_x p_y = (2α·d)_xy − 0.
176    #[test]
177    fn p_function_derivative_has_raise_and_lower() {
178        // raised = 2α-weighted d block over n_cart(2)=6: xx,xy,xz,yy,yz,zz
179        let d = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
180        // lowered = s block (n_cart(0)=1)
181        let s = vec![7.0];
182        let mut out = vec![0.0; 3]; // p block: x,y,z
183        accumulate_center_derivative(desc(1, 0, 1, 1), 1.0, &d, Some(&s), &mut out);
184        // ∂_x p_x = d_xx − 1·s  (p_x=[1,0,0], a_x=1, raise->xx idx0, lower->s)
185        assert_eq!(out[0], 1.0 - 7.0);
186        // ∂_x p_y = d_xy − 0    (p_y=[0,1,0], a_x=0, raise->xy idx1)
187        assert_eq!(out[1], 2.0);
188        // ∂_x p_z = d_xz − 0    (p_z=[0,0,1], a_x=0, raise->xz idx2)
189        assert_eq!(out[2], 3.0);
190    }
191
192    // `scale` multiplies the whole term, and repeated calls accumulate.
193    #[test]
194    fn scale_multiplies_and_accumulates() {
195        let raised = vec![10.0, 0.0, 0.0];
196        let mut out = vec![0.0; 1];
197        accumulate_center_derivative(desc(0, 0, 1, 1), 2.0, &raised, None, &mut out);
198        accumulate_center_derivative(desc(0, 0, 1, 1), 3.0, &raised, None, &mut out);
199        assert_eq!(out[0], (2.0 + 3.0) * 10.0);
200    }
201
202    // Outer indexing: a ket-style derivative with outer=2, inner=1 lands in the
203    // right rows.
204    #[test]
205    fn outer_indexing_places_correctly() {
206        // l=0 differentiated index with outer=2: raised dims 2*n_cart(1)*1 = 6.
207        let raised = vec![1.0, 0.0, 0.0, /*row1*/ 2.0, 0.0, 0.0];
208        let mut out = vec![0.0; 2];
209        accumulate_center_derivative(desc(0, 0, 2, 1), 1.0, &raised, None, &mut out);
210        assert_eq!(out, vec![1.0, 2.0]);
211    }
212
213    // Inner indexing (ket of a 1e pair): outer=1, the differentiated index is
214    // fast (inner=1) but here we exercise inner>1 to check striding.
215    #[test]
216    fn inner_indexing_places_correctly() {
217        // Differentiate an l=0 index sitting BEFORE a size-2 inner index.
218        // raised dims outer(1)*n_cart(1)(3)*inner(2) = 6, laid out [comp][inner].
219        let raised = vec![
220            11.0, 12.0, // px row over inner=2
221            21.0, 22.0, // py
222            31.0, 32.0, // pz
223        ];
224        let mut out = vec![0.0; 2];
225        accumulate_center_derivative(desc(0, 0, 1, 2), 1.0, &raised, None, &mut out);
226        assert_eq!(out, vec![11.0, 12.0]); // x-derivative picks the px row
227    }
228}