Skip to main content

cuest_cudarc/
oeint.rs

1//! One-electron integral plans and computation (overlap, kinetic, potential).
2
3use crate::basis::CuestAOBasis;
4use crate::context::ContextInner;
5use crate::error::{status_check, CuestError};
6use crate::pair_list::CuestAOPairList;
7use crate::parameters::CuestParameters;
8use crate::workspace::{new_descriptor, Workspace};
9use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut};
10use cuest_ffi::ffi;
11use std::ffi::c_int;
12use std::ptr;
13use std::sync::Arc;
14
15/// Precision usage mode for compute calls (cuEST "ffloat").
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17#[non_exhaustive]
18pub enum CuestFfloatUsageMode {
19    /// Use ffloat compute when it is available and expected to be faster on
20    /// this GPU architecture (the cuEST default).
21    Default,
22    /// Request ffloat (reduced-precision) compute.
23    On,
24    /// Request FP64 compute.
25    Off,
26}
27
28impl CuestFfloatUsageMode {
29    fn to_raw(self) -> ffi::cuestFfloatUsageMode_t {
30        match self {
31            Self::Default => ffi::CUEST_FFLOAT_USAGE_MODE_DEFAULT,
32            Self::On => ffi::CUEST_FFLOAT_USAGE_MODE_ON,
33            Self::Off => ffi::CUEST_FFLOAT_USAGE_MODE_OFF,
34        }
35    }
36}
37
38/// JIT usage mode for compute calls.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum CuestJitUsageMode {
42    /// Use JIT-compiled kernels. Requires the cuEST CUDA 13.x package;
43    /// requesting this on a CUDA 12 package returns an error.
44    On,
45    /// Use AOT-compiled kernels.
46    Off,
47}
48
49impl CuestJitUsageMode {
50    fn to_raw(self) -> ffi::cuestJITUsageMode_t {
51        match self {
52            Self::On => ffi::CUEST_JIT_USAGE_MODE_ON,
53            Self::Off => ffi::CUEST_JIT_USAGE_MODE_OFF,
54        }
55    }
56}
57
58/// Options for [`CuestOeIntPlan::potential_with_options`].
59///
60/// All fields are `Option`s: `None` leaves the cuEST default in place. (This
61/// matters for [`CuestJitUsageMode`]: the cuEST default is JIT on CUDA 13.x
62/// packages and AOT on CUDA 12 packages, and explicitly requesting JIT on a
63/// CUDA 12 package fails.)
64#[derive(Debug, Default, Clone)]
65pub struct CuestPotentialComputeOptions {
66    /// Precision usage mode; `None` = compute precision chosen heuristically.
67    pub ffloat_usage_mode: Option<CuestFfloatUsageMode>,
68    /// JIT usage mode; `None` = package default (JIT on CUDA 13.x).
69    pub jit_usage_mode: Option<CuestJitUsageMode>,
70}
71
72/// Owned state of one one-electron integral plan handle.
73struct PlanInner {
74    context: Arc<ContextInner>,
75    handle: ffi::cuestOEIntPlan_t,
76    /// The basis and pair list the plan was built from (cuEST requires both
77    /// to outlive the plan).
78    _basis: CuestAOBasis,
79    _pair_list: CuestAOPairList,
80    _persistent_workspace: Workspace,
81    nao: u64,
82}
83
84impl PlanInner {
85    /// Size the temporary workspace with `query` and allocate it on the
86    /// context's stream.
87    fn temporary_workspace(
88        &self,
89        query: impl FnOnce(&mut ffi::cuestWorkspaceDescriptor_t) -> ffi::cuestStatus_t,
90        name: &'static str,
91    ) -> Result<Workspace, CuestError> {
92        let mut desc = new_descriptor();
93        status_check(query(&mut desc), name)?;
94        Workspace::new(self.context.stream(), &desc)
95    }
96}
97
98impl Drop for PlanInner {
99    fn drop(&mut self) {
100        // cuEST requires the handle's device to be current for destroy.
101        // Drop cannot report errors; failures only leak GPU resources.
102        let _ = self.context.stream().context().bind_to_thread();
103        let _ = unsafe { ffi::cuestOEIntPlanDestroy(self.handle) };
104    }
105}
106
107/// A cuEST one-electron integral plan (`cuestOEIntPlan_t`).
108///
109/// Built from a basis and a pair list (both kept alive by the plan), it
110/// caches the intermediates needed to compute the overlap, kinetic, and
111/// potential matrices. All compute methods are stream-ordered: they enqueue
112/// work on the context's stream and return without synchronizing.
113pub struct CuestOeIntPlan {
114    inner: Arc<PlanInner>,
115}
116
117impl CuestOeIntPlan {
118    /// Create a plan for `basis`/`pair_list`.
119    ///
120    /// The pair list must have been built from `basis`; returns
121    /// [`CuestError::Rust`] otherwise.
122    pub fn new(basis: &CuestAOBasis, pair_list: &CuestAOPairList) -> Result<Self, CuestError> {
123        if !Arc::ptr_eq(basis.inner(), pair_list.inner().basis.inner()) {
124            return Err(CuestError::Rust(
125                "pair list was built from a different basis than the one given".to_string(),
126            ));
127        }
128        let context = basis.inner().context.clone();
129        let params = CuestParameters::create(ffi::CUEST_OEINTPLAN_PARAMETERS)?;
130        let (mut persistent_desc, mut temporary_desc) = (new_descriptor(), new_descriptor());
131        status_check(
132            unsafe {
133                ffi::cuestOEIntPlanCreateWorkspaceQuery(
134                    context.handle(),
135                    basis.raw_handle(),
136                    pair_list.raw_handle(),
137                    params.raw(),
138                    &mut persistent_desc,
139                    &mut temporary_desc,
140                    ptr::null_mut(),
141                )
142            },
143            "cuestOEIntPlanCreateWorkspaceQuery",
144        )?;
145
146        let stream = context.stream().clone();
147        let persistent = Workspace::new(&stream, &persistent_desc)?;
148        let temporary = Workspace::new(&stream, &temporary_desc)?;
149        let mut handle: ffi::cuestOEIntPlan_t = ptr::null_mut();
150        {
151            let (mut persistent_raw, _persistent_guard) = persistent.raw(&stream);
152            let (mut temporary_raw, _temporary_guard) = temporary.raw(&stream);
153            status_check(
154                unsafe {
155                    ffi::cuestOEIntPlanCreate(
156                        context.handle(),
157                        basis.raw_handle(),
158                        pair_list.raw_handle(),
159                        params.raw(),
160                        &mut persistent_raw,
161                        &mut temporary_raw,
162                        &mut handle,
163                    )
164                },
165                "cuestOEIntPlanCreate",
166            )?;
167        }
168        // The temporary workspace is no longer needed.
169
170        Ok(Self {
171            inner: Arc::new(PlanInner {
172                context,
173                handle,
174                _basis: basis.clone(),
175                _pair_list: pair_list.clone(),
176                _persistent_workspace: persistent,
177                nao: basis.num_ao(),
178            }),
179        })
180    }
181
182    /// Number of basis functions (the side of the output matrices).
183    pub fn nao(&self) -> u64 {
184        self.inner.nao
185    }
186
187    /// Compute the AO overlap matrix `S` (nao x nao) into a new device
188    /// buffer allocated on the context's stream.
189    pub fn overlap(&self) -> Result<CudaSlice<f64>, CuestError> {
190        let mut out = self.alloc_output()?;
191        self.overlap_into(&mut out)?;
192        Ok(out)
193    }
194
195    /// Compute the AO overlap matrix `S` into `out` (at least nao x nao
196    /// elements on the context's stream).
197    pub fn overlap_into(&self, out: &mut impl DevicePtrMut<f64>) -> Result<(), CuestError> {
198        self.inner.context.check_stream(out.stream(), "out")?;
199        check_output_len(self.inner.nao, out.len())?;
200        let params = CuestParameters::create(ffi::CUEST_OVERLAPCOMPUTE_PARAMETERS)?;
201        let temp = self.inner.temporary_workspace(
202            |desc| unsafe {
203                ffi::cuestOverlapComputeWorkspaceQuery(
204                    self.inner.context.handle(),
205                    self.inner.handle,
206                    params.raw(),
207                    desc,
208                    ptr::null_mut(),
209                )
210            },
211            "cuestOverlapComputeWorkspaceQuery",
212        )?;
213
214        let stream = self.inner.context.stream();
215        let (out_ptr, _out_guard) = out.device_ptr_mut(stream);
216        let (mut ws, _ws_guard) = temp.raw(stream);
217        status_check(
218            unsafe {
219                ffi::cuestOverlapCompute(
220                    self.inner.context.handle(),
221                    self.inner.handle,
222                    params.raw(),
223                    &mut ws,
224                    out_ptr as *mut f64,
225                )
226            },
227            "cuestOverlapCompute",
228        )
229    }
230
231    /// Compute the AO kinetic energy matrix `T` (nao x nao) into a new device
232    /// buffer allocated on the context's stream.
233    pub fn kinetic(&self) -> Result<CudaSlice<f64>, CuestError> {
234        let mut out = self.alloc_output()?;
235        self.kinetic_into(&mut out)?;
236        Ok(out)
237    }
238
239    /// Compute the AO kinetic energy matrix `T` into `out` (at least nao x nao
240    /// elements on the context's stream).
241    pub fn kinetic_into(&self, out: &mut impl DevicePtrMut<f64>) -> Result<(), CuestError> {
242        self.inner.context.check_stream(out.stream(), "out")?;
243        check_output_len(self.inner.nao, out.len())?;
244        let params = CuestParameters::create(ffi::CUEST_KINETICCOMPUTE_PARAMETERS)?;
245        let temp = self.inner.temporary_workspace(
246            |desc| unsafe {
247                ffi::cuestKineticComputeWorkspaceQuery(
248                    self.inner.context.handle(),
249                    self.inner.handle,
250                    params.raw(),
251                    desc,
252                    ptr::null_mut(),
253                )
254            },
255            "cuestKineticComputeWorkspaceQuery",
256        )?;
257
258        let stream = self.inner.context.stream();
259        let (out_ptr, _out_guard) = out.device_ptr_mut(stream);
260        let (mut ws, _ws_guard) = temp.raw(stream);
261        status_check(
262            unsafe {
263                ffi::cuestKineticCompute(
264                    self.inner.context.handle(),
265                    self.inner.handle,
266                    params.raw(),
267                    &mut ws,
268                    out_ptr as *mut f64,
269                )
270            },
271            "cuestKineticCompute",
272        )
273    }
274
275    /// Compute the AO potential energy matrix `V` for `charges` at `xyz`
276    /// (both on the context's stream) into a new device buffer.
277    ///
278    /// `xyz` is flat row-major coordinates (3 per charge, in Bohr), `charges`
279    /// the matching charge magnitudes. cuEST evaluates the potential with a
280    /// positive unit test charge: for nuclear attraction integrals use the
281    /// *negated* nuclear charges, e.g.
282    /// [`Xyz::nuclear_attraction_charges`](crate::Xyz::nuclear_attraction_charges).
283    pub fn potential(
284        &self,
285        xyz: &impl DevicePtr<f64>,
286        charges: &impl DevicePtr<f64>,
287    ) -> Result<CudaSlice<f64>, CuestError> {
288        self.potential_with_options(&CuestPotentialComputeOptions::default(), xyz, charges)
289    }
290
291    /// [`CuestOeIntPlan::potential`] into `out` (at least nao x nao elements).
292    pub fn potential_into(
293        &self,
294        out: &mut impl DevicePtrMut<f64>,
295        xyz: &impl DevicePtr<f64>,
296        charges: &impl DevicePtr<f64>,
297    ) -> Result<(), CuestError> {
298        self.potential_with_options_into(
299            &CuestPotentialComputeOptions::default(),
300            out,
301            xyz,
302            charges,
303        )
304    }
305
306    /// [`CuestOeIntPlan::potential`] with explicit compute options.
307    pub fn potential_with_options(
308        &self,
309        options: &CuestPotentialComputeOptions,
310        xyz: &impl DevicePtr<f64>,
311        charges: &impl DevicePtr<f64>,
312    ) -> Result<CudaSlice<f64>, CuestError> {
313        let mut out = self.alloc_output()?;
314        self.potential_with_options_into(options, &mut out, xyz, charges)?;
315        Ok(out)
316    }
317
318    /// [`CuestOeIntPlan::potential_with_options`] into `out`.
319    pub fn potential_with_options_into(
320        &self,
321        options: &CuestPotentialComputeOptions,
322        out: &mut impl DevicePtrMut<f64>,
323        xyz: &impl DevicePtr<f64>,
324        charges: &impl DevicePtr<f64>,
325    ) -> Result<(), CuestError> {
326        self.inner.context.check_stream(out.stream(), "out")?;
327        self.inner.context.check_stream(xyz.stream(), "xyz")?;
328        self.inner.context.check_stream(charges.stream(), "charges")?;
329        check_output_len(self.inner.nao, out.len())?;
330        if xyz.len() == 0 || xyz.len() % 3 != 0 {
331            return Err(CuestError::Rust(format!(
332                "`xyz` has {} coordinates; expected 3 per charge",
333                xyz.len()
334            )));
335        }
336        let num_charges = xyz.len() / 3;
337        if charges.len() != num_charges {
338            return Err(CuestError::Rust(format!(
339                "`charges` has {} entries for {num_charges} charge positions",
340                charges.len()
341            )));
342        }
343
344        let params = CuestParameters::create(ffi::CUEST_POTENTIALCOMPUTE_PARAMETERS)?;
345        if let Some(mode) = options.ffloat_usage_mode {
346            params.configure_i32(
347                ffi::CUEST_POTENTIALCOMPUTE_PARAMETERS_FFLOAT_USAGE_MODE.0 as c_int,
348                mode.to_raw().0 as i32,
349            )?;
350        }
351        if let Some(mode) = options.jit_usage_mode {
352            params.configure_i32(
353                ffi::CUEST_POTENTIALCOMPUTE_PARAMETERS_JIT_USAGE_MODE.0 as c_int,
354                mode.to_raw().0 as i32,
355            )?;
356        }
357
358        let num_charges = num_charges as u64;
359        let temp = self.inner.temporary_workspace(
360            |desc| unsafe {
361                ffi::cuestPotentialComputeWorkspaceQuery(
362                    self.inner.context.handle(),
363                    self.inner.handle,
364                    params.raw(),
365                    desc,
366                    num_charges,
367                    ptr::null(),
368                    ptr::null(),
369                    ptr::null_mut(),
370                )
371            },
372            "cuestPotentialComputeWorkspaceQuery",
373        )?;
374
375        let stream = self.inner.context.stream();
376        let (xyz_ptr, _xyz_guard) = xyz.device_ptr(stream);
377        let (charges_ptr, _charges_guard) = charges.device_ptr(stream);
378        let (out_ptr, _out_guard) = out.device_ptr_mut(stream);
379        let (mut ws, _ws_guard) = temp.raw(stream);
380        status_check(
381            unsafe {
382                ffi::cuestPotentialCompute(
383                    self.inner.context.handle(),
384                    self.inner.handle,
385                    params.raw(),
386                    &mut ws,
387                    num_charges,
388                    xyz_ptr as *const f64,
389                    charges_ptr as *const f64,
390                    out_ptr as *mut f64,
391                )
392            },
393            "cuestPotentialCompute",
394        )
395    }
396
397    /// Allocate a nao x nao zero-initialized output on the context's stream.
398    fn alloc_output(&self) -> Result<CudaSlice<f64>, CuestError> {
399        let count = (self.inner.nao * self.inner.nao) as usize;
400        Ok(self.inner.context.stream().alloc_zeros::<f64>(count)?)
401    }
402
403    /// The raw cuEST plan handle (escape hatch).
404    pub fn raw_handle(&self) -> ffi::cuestOEIntPlan_t {
405        self.inner.handle
406    }
407}
408
409impl Clone for CuestOeIntPlan {
410    fn clone(&self) -> Self {
411        Self { inner: self.inner.clone() }
412    }
413}
414
415impl std::fmt::Debug for CuestOeIntPlan {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        f.debug_struct("CuestOeIntPlan").field("nao", &self.nao()).finish_non_exhaustive()
418    }
419}
420
421// SAFETY: see the "Thread safety" section of the crate documentation.
422unsafe impl Send for CuestOeIntPlan {}
423
424/// The cuEST C API cannot check output buffer sizes; do it here.
425fn check_output_len(nao: u64, len: usize) -> Result<(), CuestError> {
426    let needed = (nao * nao) as usize;
427    if len < needed {
428        Err(CuestError::Rust(format!(
429            "output matrix has {len} elements, expected at least {needed} (nao = {nao})"
430        )))
431    } else {
432        Ok(())
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn cpu_check_output_len() {
442        assert!(check_output_len(3, 9).is_ok());
443        assert!(check_output_len(3, 10).is_ok());
444        assert!(check_output_len(3, 8).is_err());
445        assert!(check_output_len(0, 0).is_ok());
446    }
447}