Skip to main content

cuest_cudarc/
shell.rs

1//! Atomic orbital (AO) Gaussian shells.
2//!
3//! A [`CuestAOShell`] wraps a `cuestAOShell_t` handle built from a
4//! [`CuestShellSpec`]. Shells are host-side objects: they are inputs to
5//! [`CuestAOBasis::new`](crate::CuestAOBasis::new) and may be dropped as
6//! soon as the basis has been created.
7
8use crate::context::{ContextInner, CuestContext};
9use crate::error::{status_check, CuestError};
10use crate::parameters::CuestParameters;
11use crate::query::{self, AoshellAttribute};
12use cuest_ffi::ffi;
13use std::ffi::c_void;
14use std::ptr;
15use std::sync::Arc;
16
17/// Specification of one contracted Gaussian shell.
18///
19/// `exponents` and `coefficients` are the primitive lists cuEST uses
20/// *verbatim*: coefficients multiply **raw** (unnormalized) primitives, so
21/// coefficients taken from a standard basis file (e.g. `.gbs`), which are
22/// relative to *normalized* primitives, must be converted with
23/// [`CuestShellSpec::normalized`] (cuEST's documented normalization). Both
24/// lists are in atomic units.
25#[derive(Debug, Clone, PartialEq)]
26pub struct CuestShellSpec {
27    /// Angular momentum quantum number (s = 0, p = 1, d = 2, ...).
28    pub l: u64,
29    /// Whether the shell uses pure (spherical) functions instead of
30    /// Cartesian ones. All shells of a basis must agree.
31    pub pure: bool,
32    /// Primitive Gaussian exponents (all positive).
33    pub exponents: Vec<f64>,
34    /// Contraction coefficients, one per exponent, used verbatim.
35    pub coefficients: Vec<f64>,
36}
37
38impl CuestShellSpec {
39    /// Build a shell spec from primitive lists.
40    ///
41    /// Returns [`CuestError::Rust`] if the lists are empty or have different
42    /// lengths.
43    pub fn new(
44        l: u64,
45        pure: bool,
46        exponents: Vec<f64>,
47        coefficients: Vec<f64>,
48    ) -> Result<Self, CuestError> {
49        check_primitive_lists(exponents.len(), coefficients.len())?;
50        Ok(Self { l, pure, exponents, coefficients })
51    }
52
53    /// Convert the coefficients to cuEST's convention, normalized to
54    /// `normalization` (usually 1.0).
55    ///
56    /// Input coefficients are interpreted as relative to *normalized*
57    /// primitives (the convention of standard basis files); the output folds
58    /// in the primitive normalization that cuEST's verbatim convention
59    /// requires (see [`normalize_coefficients`]). Calling this twice is
60    /// therefore wrong — the result is already in cuEST's convention.
61    pub fn normalized(&self, normalization: f64) -> Result<Self, CuestError> {
62        let coefficients =
63            normalize_coefficients(self.l, &self.exponents, &self.coefficients, normalization)?;
64        Ok(Self { coefficients, ..self.clone() })
65    }
66
67    /// Number of primitives in the shell.
68    pub fn num_primitive(&self) -> u64 {
69        self.exponents.len() as u64
70    }
71}
72
73/// Normalize contraction coefficients following the cuEST convention.
74///
75/// cuEST uses contraction coefficients verbatim against raw (unnormalized)
76/// primitive Gaussians, so coefficients from standard basis files (relative
77/// to normalized primitives) must first be brought to the "finished"
78/// primitive-and-L normalized form documented by cuEST:
79///
80/// ```text
81/// d   = prod_{k=1..L} (2k - 1)
82/// c_k = c_k^raw * sqrt( 2^L * (2 e_k)^(L + 3/2) / (pi^(3/2) * d) )
83/// V   = sum_ij c_i^raw c_j^raw * ( 2 sqrt(e_i e_j) / (e_i + e_j) )^(L + 3/2)
84/// c_k^N = c_k * sqrt( normalization / V )
85/// ```
86///
87/// This mirrors `compute_normalized_coefficients` from the cuEST C examples
88/// and the formula in the cuEST documentation (`api_methods.rst`,
89/// "Primitive Basis Function Normalization").
90///
91/// Returns [`CuestError::Rust`] for invalid input: empty or mismatched lists,
92/// non-positive exponents, `L >= 9`, non-positive `normalization`, or a
93/// degenerate contraction.
94pub fn normalize_coefficients(
95    l: u64,
96    exponents: &[f64],
97    coefficients: &[f64],
98    normalization: f64,
99) -> Result<Vec<f64>, CuestError> {
100    check_primitive_lists(exponents.len(), coefficients.len())?;
101    if l >= 9 {
102        return Err(CuestError::Rust(format!("cuEST cannot normalize L = {l} (L < 9 required)")));
103    }
104    if normalization <= 0.0 || normalization.is_nan() {
105        return Err(CuestError::Rust(format!("normalization {normalization} is not positive")));
106    }
107    for (i, &e) in exponents.iter().enumerate() {
108        if e <= 0.0 || e.is_nan() {
109            return Err(CuestError::Rust(format!("exponent {i} = {e} is not positive")));
110        }
111    }
112
113    // d = prod_{k=1..L} (2k - 1); the empty product for L = 0 is 1.
114    let d: f64 = (1..=l).map(|k| (2 * k - 1) as f64).product();
115    let two_l = 2f64.powi(l as i32);
116    let pi32 = std::f64::consts::PI.powf(1.5);
117    let scale = (two_l / (pi32 * d)).sqrt();
118    let l32 = l as f64 + 1.5;
119
120    let mut normalized: Vec<f64> = (0..exponents.len())
121        .map(|k| coefficients[k] * scale * (2.0 * exponents[k]).powf(l32).sqrt())
122        .collect();
123
124    // V = self-overlap of the raw contraction.
125    let mut v = 0.0;
126    for (&ei, &ci) in exponents.iter().zip(coefficients) {
127        for (&ej, &cj) in exponents.iter().zip(coefficients) {
128            v += ci * cj * (2.0 * (ei * ej).sqrt() / (ei + ej)).powf(l32);
129        }
130    }
131    if v <= 0.0 || v.is_nan() {
132        return Err(CuestError::Rust(
133            "shell contraction has non-positive self-overlap".to_string(),
134        ));
135    }
136
137    let q = (normalization / v).sqrt();
138    for c in &mut normalized {
139        *c *= q;
140    }
141    Ok(normalized)
142}
143
144/// Validate that a shell's primitive lists are non-empty and equally long.
145///
146/// cuEST reads `exponents.len()` coefficients from both arrays, so mismatched
147/// lists would read out of bounds in the C API.
148fn check_primitive_lists(exponents_len: usize, coefficients_len: usize) -> Result<(), CuestError> {
149    if exponents_len == 0 || exponents_len != coefficients_len {
150        return Err(CuestError::Rust(format!(
151            "shell has {exponents_len} exponents but {coefficients_len} coefficients"
152        )));
153    }
154    Ok(())
155}
156
157/// Owned state of one AO shell handle.
158struct ShellInner {
159    context: Arc<ContextInner>,
160    handle: ffi::cuestAOShell_t,
161    spec: CuestShellSpec,
162    num_ao: u64,
163    num_pure: u64,
164    num_cart: u64,
165}
166
167impl Drop for ShellInner {
168    fn drop(&mut self) {
169        // Drop cannot report errors; failures only leak a host object.
170        let _ = unsafe { ffi::cuestAOShellDestroy(self.handle) };
171    }
172}
173
174/// A cuEST AO shell object (`cuestAOShell_t`).
175///
176/// See [`CuestShellSpec`] for the input data and normalization conventions.
177pub struct CuestAOShell {
178    inner: Arc<ShellInner>,
179}
180
181impl CuestAOShell {
182    /// Create a shell from `spec` on the given context.
183    ///
184    /// The spec's primitive lists are validated again here: the fields of
185    /// [`CuestShellSpec`] are public, and cuEST reads `exponents.len()`
186    /// coefficients from both arrays.
187    pub fn new(context: &CuestContext, spec: &CuestShellSpec) -> Result<Self, CuestError> {
188        check_primitive_lists(spec.exponents.len(), spec.coefficients.len())?;
189        let context_inner = context.inner().clone();
190        let params = CuestParameters::create(ffi::CUEST_AOSHELL_PARAMETERS)?;
191        let mut handle: ffi::cuestAOShell_t = ptr::null_mut();
192        status_check(
193            unsafe {
194                ffi::cuestAOShellCreate(
195                    context_inner.handle(),
196                    spec.pure as i32,
197                    spec.l,
198                    spec.num_primitive(),
199                    spec.exponents.as_ptr(),
200                    spec.coefficients.as_ptr(),
201                    params.raw(),
202                    &mut handle,
203                )
204            },
205            "cuestAOShellCreate",
206        )?;
207
208        match query_new_attributes(&context_inner, handle) {
209            Ok((num_ao, num_pure, num_cart)) => Ok(Self {
210                inner: Arc::new(ShellInner {
211                    context: context_inner,
212                    handle,
213                    spec: spec.clone(),
214                    num_ao,
215                    num_pure,
216                    num_cart,
217                }),
218            }),
219            Err(err) => {
220                let _ = unsafe { ffi::cuestAOShellDestroy(handle) };
221                Err(err)
222            },
223        }
224    }
225
226    /// The specification this shell was created from.
227    pub fn spec(&self) -> &CuestShellSpec {
228        &self.inner.spec
229    }
230
231    /// Angular momentum quantum number (L).
232    pub fn l(&self) -> u64 {
233        self.inner.spec.l
234    }
235
236    /// Whether the shell uses pure (spherical) functions.
237    pub fn is_pure(&self) -> bool {
238        self.inner.spec.pure
239    }
240
241    /// Number of primitive Gaussians.
242    pub fn num_primitive(&self) -> u64 {
243        self.inner.spec.num_primitive()
244    }
245
246    /// Number of basis functions of this shell (pure or Cartesian as set).
247    pub fn num_ao(&self) -> u64 {
248        self.inner.num_ao
249    }
250
251    /// Number of pure (spherical) AOs.
252    pub fn num_pure(&self) -> u64 {
253        self.inner.num_pure
254    }
255
256    /// Number of Cartesian AOs.
257    pub fn num_cart(&self) -> u64 {
258        self.inner.num_cart
259    }
260
261    /// Query an attribute through the typed attribute mechanism.
262    pub fn query<A: AoshellAttribute>(&self) -> Result<A::Value, CuestError> {
263        query::query_attribute(
264            self.inner.context.handle(),
265            ffi::CUEST_AOSHELL,
266            self.inner.handle as *const c_void,
267            A::ID,
268        )
269    }
270
271    /// The raw cuEST shell handle (escape hatch).
272    pub fn raw_handle(&self) -> ffi::cuestAOShell_t {
273        self.inner.handle
274    }
275
276    pub(crate) fn context(&self) -> &Arc<ContextInner> {
277        &self.inner.context
278    }
279}
280
281impl Clone for CuestAOShell {
282    fn clone(&self) -> Self {
283        Self { inner: self.inner.clone() }
284    }
285}
286
287impl std::fmt::Debug for CuestAOShell {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        f.debug_struct("CuestAOShell")
290            .field("l", &self.l())
291            .field("pure", &self.is_pure())
292            .field("num_primitive", &self.num_primitive())
293            .field("num_ao", &self.num_ao())
294            .finish_non_exhaustive()
295    }
296}
297
298// SAFETY: see the "Thread safety" section of the crate documentation.
299unsafe impl Send for CuestAOShell {}
300
301fn query_new_attributes(
302    context: &ContextInner,
303    handle: ffi::cuestAOShell_t,
304) -> Result<(u64, u64, u64), CuestError> {
305    let query = |id: std::ffi::c_int| {
306        query::query_attribute::<u64>(
307            context.handle(),
308            ffi::CUEST_AOSHELL,
309            handle as *const c_void,
310            id,
311        )
312    };
313    Ok((
314        query(ffi::CUEST_AOSHELL_NUM_AO.0 as std::ffi::c_int)?,
315        query(ffi::CUEST_AOSHELL_NUM_PURE.0 as std::ffi::c_int)?,
316        query(ffi::CUEST_AOSHELL_NUM_CART.0 as std::ffi::c_int)?,
317    ))
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    /// Norm of a normalized primitive: `|N_k g_k| = 1` where `g_k` is the raw
325    /// primitive `x^l e^{-e r^2}`.
326    fn primitive_norm(l: u64, e: f64) -> f64 {
327        let d: f64 = (1..=l).map(|k| (2 * k - 1) as f64).product();
328        (2f64.powi(l as i32) * (2.0 * e).powf(l as f64 + 1.5)
329            / (std::f64::consts::PI.powf(1.5) * d))
330            .sqrt()
331    }
332
333    /// Overlap of two normalized primitives.
334    fn primitive_overlap(l: u64, ei: f64, ej: f64) -> f64 {
335        (2.0 * (ei * ej).sqrt() / (ei + ej)).powf(l as f64 + 1.5)
336    }
337
338    /// Self-overlap of `sum_k c_k phi_k` where `phi_k` are *normalized*
339    /// primitives (the convention of standard basis files).
340    fn self_overlap_normalized_primitives(l: u64, exponents: &[f64], coefficients: &[f64]) -> f64 {
341        let mut v = 0.0;
342        for (&ei, &ci) in exponents.iter().zip(coefficients) {
343            for (&ej, &cj) in exponents.iter().zip(coefficients) {
344                v += ci * cj * primitive_overlap(l, ei, ej);
345            }
346        }
347        v
348    }
349
350    /// Self-overlap of `sum_k c_k g_k` where `g_k` are *raw* primitives
351    /// (cuEST's convention: coefficients are used verbatim). The overlap of
352    /// two raw primitives is that of the normalized ones, divided by
353    /// `N_i N_j`.
354    fn self_overlap_raw_primitives(l: u64, exponents: &[f64], coefficients: &[f64]) -> f64 {
355        let mut v = 0.0;
356        for (&ei, &ci) in exponents.iter().zip(coefficients) {
357            for (&ej, &cj) in exponents.iter().zip(coefficients) {
358                v += ci * cj * primitive_overlap(l, ei, ej)
359                    / (primitive_norm(l, ei) * primitive_norm(l, ej));
360            }
361        }
362        v
363    }
364
365    #[test]
366    fn cpu_normalize_coefficients() {
367        // H 1s STO-3G coefficients as published: relative to *normalized*
368        // primitives, with the contraction ~unit-normalized (to the basis
369        // file's precision).
370        let exponents = [3.42525091, 0.62391373, 0.16885540];
371        let published = [0.15432897, 0.53532814, 0.44463454];
372        let nearly_one = self_overlap_normalized_primitives(0, &exponents, &published);
373        assert!(
374            (nearly_one - 1.0).abs() < 1e-4,
375            "published coefficients are ~normalized: {nearly_one}"
376        );
377
378        // Conversion yields cuEST-convention coefficients whose contraction
379        // (coefficients against *raw* primitives) has the requested norm.
380        let normalized = normalize_coefficients(0, &exponents, &published, 1.0).unwrap();
381        assert!((self_overlap_raw_primitives(0, &exponents, &normalized) - 1.0).abs() < 1e-12);
382
383        // A p shell, with a non-unit normalization factor.
384        let normalized = normalize_coefficients(1, &exponents, &published, 2.5).unwrap();
385        assert!((self_overlap_raw_primitives(1, &exponents, &normalized) - 2.5).abs() < 1e-12);
386
387        // Scaling the input coefficients only rescales the output.
388        let scaled: Vec<f64> = published.iter().map(|c| c * 3.0).collect();
389        let normalized_scaled = normalize_coefficients(1, &exponents, &scaled, 1.0).unwrap();
390        assert!(
391            (self_overlap_raw_primitives(1, &exponents, &normalized_scaled) - 1.0).abs() < 1e-12
392        );
393    }
394
395    #[test]
396    fn cpu_normalize_coefficients_errors() {
397        let exps = [1.0];
398        assert!(normalize_coefficients(0, &[], &[], 1.0).is_err());
399        assert!(normalize_coefficients(0, &exps, &[1.0, 2.0], 1.0).is_err());
400        assert!(normalize_coefficients(9, &exps, &[1.0], 1.0).is_err());
401        assert!(normalize_coefficients(0, &exps, &[1.0], 0.0).is_err());
402        assert!(normalize_coefficients(0, &[-1.0], &[1.0], 1.0).is_err());
403    }
404
405    #[test]
406    fn cpu_check_primitive_lists() {
407        assert!(check_primitive_lists(3, 3).is_ok());
408        assert!(check_primitive_lists(0, 0).is_err());
409        assert!(check_primitive_lists(2, 3).is_err());
410    }
411
412    #[test]
413    fn cpu_shell_spec() {
414        let spec = CuestShellSpec::new(0, true, vec![1.0, 2.0], vec![0.5, 0.5]).unwrap();
415        assert_eq!(spec.num_primitive(), 2);
416        let normalized = spec.normalized(1.0).unwrap();
417        assert_eq!(normalized.l, 0);
418        assert_eq!(normalized.coefficients.len(), 2);
419        assert_ne!(normalized.coefficients, spec.coefficients);
420        assert!(CuestShellSpec::new(0, true, vec![], vec![]).is_err());
421        assert!(CuestShellSpec::new(0, true, vec![1.0], vec![1.0, 2.0]).is_err());
422    }
423}