diff --git a/.claude/skills/cuda-testing/SKILL.md b/.claude/skills/cuda-testing/SKILL.md index 7f2e5c6..46f1b07 100644 --- a/.claude/skills/cuda-testing/SKILL.md +++ b/.claude/skills/cuda-testing/SKILL.md @@ -13,7 +13,16 @@ GPUs, CUDA testing is manual and local-only. - `api-v7_0` for libxc v7.0 - `api-v7_1` for libxc v7.1 (released as 7.1.0–7.1.2) -For cuda tests, you also need to pass the `cuda` feature (which implies `api-v7_1`). +For cuda tests, you must pass the `cuda` feature **together with** the +matching api feature — `cuda` alone does **not** imply `api-v7_1` +(`cuda = ["dep:cudarc"]` in Cargo.toml), and the default `api-v7_0` bindings +against a v7.1+ CUDA library make every functional report `OnDevice` +(v7.1's `xc_func_init` defaults to `xc_func_info_get_default_flags()`, +which is OnDevice on CUDA builds), so the CPU-guard tests fail confusingly: + +```bash +LIBXC_DYLOAD=$LIBXC_DYLOAD_CUDA cargo test -p libxc --features cuda,api-v7_1 +``` **Known limitation for v7.0 + CUDA**: When libxc v7.0 is compiled with CUDA (`--enable-cuda`), `libxc_malloc` uses `cudaMallocManaged` instead of `malloc`. diff --git a/.github/workflows/test-cpu.yml b/.github/workflows/test-cpu.yml new file mode 100644 index 0000000..5167815 --- /dev/null +++ b/.github/workflows/test-cpu.yml @@ -0,0 +1,44 @@ +name: test-cpu + +# CPU test matrix over supported libxc versions. Each matrix leg installs a +# pinned conda-forge `libxc` package and points `LIBXC_DYLOAD` (the highest +# priority candidate of the dynamic loader) at the conda-installed library, +# so the exact version under test is exercised together with its matching +# `api-vX_Y` feature set. + +on: + push: + branches: [main] + pull_request: + +jobs: + test-cpu: + name: cpu / libxc ${{ matrix.libxc }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - libxc: "6.2.2" + features: "api-v6_2,dynamic_loading" + - libxc: "7.0.0" + features: "api-v7_0,dynamic_loading" + - libxc: "7.1.2" + features: "api-v7_1,dynamic_loading" + + steps: + - uses: actions/checkout@v4 + + - uses: conda-incubator/setup-miniconda@v3 + + - name: Install libxc ${{ matrix.libxc }} from conda-forge + run: conda install -y --override-channels -c conda-forge "libxc=${{ matrix.libxc }}" + + - uses: Swatinem/rust-cache@v2 + + - name: Run tests + run: | + CONDA_BASE=$(conda info --base) + ls -l "$CONDA_BASE/lib/"libxc* + export LIBXC_DYLOAD="$CONDA_BASE/lib/libxc.so" + cargo test -p libxc --no-default-features --features="${{ matrix.features }}" diff --git a/.github/workflows/test-v7_0.yml b/.github/workflows/test-v7_0.yml deleted file mode 100644 index a6fd087..0000000 --- a/.github/workflows/test-v7_0.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: test-v7.0 - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - - test-dynload-cpu: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: conda-incubator/setup-miniconda@v3 - - name: run tests - run: | - conda install "libxc=7.0.*" -c conda-forge - ls /usr/share/miniconda/lib - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/share/miniconda/lib - cargo test -p libxc --no-default-features --features="api-v7_0,dynamic_loading" -- --nocapture diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 03b459c..d0fc2d4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -162,11 +162,26 @@ Parses `headers/xc_funcs_v*.h` into Rust enums in `src/xc_funcs/`. ### Core Type: `LibXCFunctional` -The central type, wrapping a `*mut ffi::xc_func_type` pointer. It is split across multiple files using separate `impl` blocks: +The central type, wrapping a `*mut ffi::xc_func_type` pointer. The pointer is +owned by a private `LibXCFuncRaw` handle (which carries the `Drop` and the +`unsafe impl Send/Sync`), held by `LibXCFunctional` behind an `Arc`: + +- `Clone` is cheap and shares the C object (no FFI-level re-initialization); + the object is freed when the last handle drops. +- Sharing is lock-free: all libxc read/compute entry points take + `const xc_func_type *` and do not write through it, so concurrent + `compute_xc`/getters through clones are data-race-free (the intended rayon + pattern). +- All setters take `&mut self` **and** require `Arc` uniqueness (checked via + `Arc::get_mut`): while any clone is alive they fail with + `LibXCError::SharedError` (or panic, for non-`_f` variants). This prevents + safe code from mutating the C object while other threads compute with it. + +It is split across multiple files using separate `impl` blocks: | File | Responsibility | |------|---------------| -| `functional.rs` | Construction (`from_identifier`, `from_number`, `from_identifier_with_device`, `from_number_with_device`), info getters (`number`, `kind`, `family`, `flags`, `spin`, `dim`, `device_flag`, `is_on_device`), references, description, external parameters, thresholds, hybrid/CAM/VV10 coefficients, auxiliary functionals, Drop | +| `functional.rs` | Construction (`from_identifier`, `from_number`, `from_identifier_with_device`, `from_number_with_device`), info getters (`number`, `kind`, `family`, `flags`, `spin`, `dim`, `device_flag`, `is_on_device`), references, description, external parameters, thresholds, hybrid/CAM/VV10 coefficients, auxiliary functionals, `LibXCFuncRaw` (raw handle: Drop, Send/Sync) | | `functional_specific.rs` | Hybrid/CAM/VV10 coefficient setters | | `compute_cpu.rs` | CPU compute methods: `compute_lda`, `compute_gga`, `compute_mgga`, and unified `compute_xc` dispatch | | `compute_cuda.rs` | CUDA compute methods (gated by `cuda` feature): `cuda_compute_lda`, `cuda_compute_gga`, `cuda_compute_mgga`, and unified `cuda_compute_xc` dispatch | diff --git a/CHANGELOG.md b/CHANGELOG.md index e745a73..d7e924b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog +## v0.1.4 -- 2026-09-09 + +Enhancement: + +- `LibXCFunctional` is now `Clone`: clones are reference-counted handles to + the same underlying C functional (no FFI-level re-initialization), so one + functional can be shared across rayon workers lock-free. The unsafe + `Send`/`Sync` impls moved to the private owning handle; their soundness is + argued from libxc's `const xc_func_type *` read-side API + (`tests/general/test_clone.rs`). +- All mutating setters now additionally require `Arc` uniqueness: while any + clone of the functional is alive they fail with the new + `LibXCError::SharedError` (or panic for the non-`_f` variants). The + threshold setters (`set_dens_threshold`, `set_zeta_threshold`, + `set_sigma_threshold`, `set_tau_threshold`, `set_fhc_enforcement`) gained + fallible `_f` variants accordingly. + +Fix: + +- Graceful handling of external-parameter value getters on libxc < 7.0: the + `xc_func_get_ext_params*` getters were introduced in libxc v7.0, and 6.x + has no uniform read-back of current values. Previously this surfaced as an + opaque panic inside the dynamic-loading shim (or a segfault when a second + libxc library was also discoverable, e.g. a conda environment). Now + `ext_param_values_f`/`ext_param_map_f` return the new + `LibXCError::UnsupportedVersion` on such libraries, `describe()` degrades + to default values with a note, and `set_ext_param_map` seeds from defaults + (matching libxc 6.x semantics where setting one parameter resets the + others). + +Change: + +- `LibXCError` is now `#[non_exhaustive]`: new variants may be added in + patch releases, so downstream matches should include a wildcard (`_`) arm. +- CI: the CPU test workflow now runs a matrix over libxc 6.2.2, 7.0.0 and + 7.1.2 (conda-forge `libxc` package), pointing `LIBXC_DYLOAD` at the + conda-installed library and selecting the matching `api-vX_Y` feature set + for each leg (replaces the previous single v7.0 workflow). + ## v0.1.3 -- 2026-09-08 Fix: diff --git a/Cargo.toml b/Cargo.toml index 32d3956..71dfc46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ [workspace.package] edition = "2021" rust-version = "1.82" -version = "0.1.3" +version = "0.1.4" description = "libxc FFI bindings and wrappers" repository = "https://github.com/RESTGroup/libxc-rs" keywords = ["ffi", "chemistry"] @@ -21,4 +21,4 @@ license = "Apache-2.0" # are always unified in, and e.g. `--features api-v6_2` would still compile # the v7.0 struct layout of `xc_func_type` — which reads out of bounds when # a v6.2.2 shared library is loaded at runtime. -libxc-ffi = { path = "libxc-ffi", version = "0.1.3", default-features = false } +libxc-ffi = { path = "libxc-ffi", version = "0.1.4", default-features = false } diff --git a/libxc/src/compute_cpu.rs b/libxc/src/compute_cpu.rs index 4527ebd..da16d0a 100644 --- a/libxc/src/compute_cpu.rs +++ b/libxc/src/compute_cpu.rs @@ -167,7 +167,7 @@ impl LibXCFunctional { ))); } unsafe { - xc_lda_call(self.ptr, npoints, rho_ptr, output.as_mut_ptr(), &layout); + xc_lda_call(self.inner.ptr, npoints, rho_ptr, output.as_mut_ptr(), &layout); } Ok(layout) } @@ -188,7 +188,7 @@ impl LibXCFunctional { let (npoints, rho_ptr, layout) = self.lda_prepare(input, flags)?; let mut buffer = vec![0.0f64; layout.total_size]; unsafe { - xc_lda_call(self.ptr, npoints, rho_ptr, buffer.as_mut_ptr(), &layout); + xc_lda_call(self.inner.ptr, npoints, rho_ptr, buffer.as_mut_ptr(), &layout); } Ok((buffer, layout)) } @@ -212,7 +212,7 @@ impl LibXCFunctional { let ptrs = validate_output_ptrs(output, &LDA_OUTPUT_LABELS, npoints, dim)?; unsafe { - xc_lda_call_with_output(self.ptr, npoints, rho_ptr, &ptrs); + xc_lda_call_with_output(self.inner.ptr, npoints, rho_ptr, &ptrs); } Ok(()) } @@ -261,7 +261,7 @@ impl LibXCFunctional { ))); } unsafe { - xc_gga_call(self.ptr, npoints, rho_ptr, sigma_ptr, output.as_mut_ptr(), &layout); + xc_gga_call(self.inner.ptr, npoints, rho_ptr, sigma_ptr, output.as_mut_ptr(), &layout); } Ok(layout) } @@ -281,7 +281,7 @@ impl LibXCFunctional { let (npoints, rho_ptr, sigma_ptr, layout) = self.gga_prepare(input, flags)?; let mut buffer = vec![0.0f64; layout.total_size]; unsafe { - xc_gga_call(self.ptr, npoints, rho_ptr, sigma_ptr, buffer.as_mut_ptr(), &layout); + xc_gga_call(self.inner.ptr, npoints, rho_ptr, sigma_ptr, buffer.as_mut_ptr(), &layout); } Ok((buffer, layout)) } @@ -306,7 +306,7 @@ impl LibXCFunctional { let ptrs = validate_output_ptrs(output, &GGA_OUTPUT_LABELS, npoints, dim)?; unsafe { - xc_gga_call_with_output(self.ptr, npoints, rho_ptr, sigma_ptr, &ptrs); + xc_gga_call_with_output(self.inner.ptr, npoints, rho_ptr, sigma_ptr, &ptrs); } Ok(()) } @@ -371,7 +371,7 @@ impl LibXCFunctional { crate::layout_handling::mgga_tau_scratch(&layout, dim, npoints); unsafe { xc_mgga_call( - self.ptr, + self.inner.ptr, npoints, rho_ptr, sigma_ptr, @@ -409,7 +409,7 @@ impl LibXCFunctional { crate::layout_handling::mgga_tau_scratch(&layout, dim, npoints); unsafe { xc_mgga_call( - self.ptr, + self.inner.ptr, npoints, rho_ptr, sigma_ptr, @@ -454,7 +454,14 @@ impl LibXCFunctional { unsafe { xc_mgga_call_with_output( - self.ptr, npoints, rho_ptr, sigma_ptr, lapl_ptr, tau_ptr, &ptrs, &extra, + self.inner.ptr, + npoints, + rho_ptr, + sigma_ptr, + lapl_ptr, + tau_ptr, + &ptrs, + &extra, ); } Ok(()) diff --git a/libxc/src/compute_cuda.rs b/libxc/src/compute_cuda.rs index 0aa70ed..31b1dd7 100644 --- a/libxc/src/compute_cuda.rs +++ b/libxc/src/compute_cuda.rs @@ -185,7 +185,7 @@ impl LibXCFunctional { { let (output_base, _sync) = buffer.device_ptr_mut(stream); unsafe { - xc_lda_call(self.ptr, npoints, rho_ptr, output_base as *mut f64, &layout); + xc_lda_call(self.inner.ptr, npoints, rho_ptr, output_base as *mut f64, &layout); } } Ok((buffer, layout)) @@ -210,7 +210,7 @@ impl LibXCFunctional { } let (output_base, _sync) = output.device_ptr_mut(&stream); unsafe { - xc_lda_call(self.ptr, npoints, rho_ptr, output_base as *mut f64, &layout); + xc_lda_call(self.inner.ptr, npoints, rho_ptr, output_base as *mut f64, &layout); } Ok(layout) } @@ -238,7 +238,7 @@ impl LibXCFunctional { let ptrs = validate_cuda_output_ptrs(output, &LDA_OUTPUT_LABELS, npoints, dim, stream)?; unsafe { - xc_lda_call_with_output(self.ptr, npoints, rho_ptr, &ptrs); + xc_lda_call_with_output(self.inner.ptr, npoints, rho_ptr, &ptrs); } Ok(()) } @@ -289,7 +289,7 @@ impl LibXCFunctional { let (output_base, _sync) = buffer.device_ptr_mut(stream); unsafe { xc_gga_call( - self.ptr, + self.inner.ptr, npoints, rho_ptr, sigma_ptr, @@ -321,7 +321,14 @@ impl LibXCFunctional { } let (output_base, _sync) = output.device_ptr_mut(&stream); unsafe { - xc_gga_call(self.ptr, npoints, rho_ptr, sigma_ptr, output_base as *mut f64, &layout); + xc_gga_call( + self.inner.ptr, + npoints, + rho_ptr, + sigma_ptr, + output_base as *mut f64, + &layout, + ); } Ok(layout) } @@ -350,7 +357,7 @@ impl LibXCFunctional { let ptrs = validate_cuda_output_ptrs(output, &GGA_OUTPUT_LABELS, npoints, dim, stream)?; unsafe { - xc_gga_call_with_output(self.ptr, npoints, rho_ptr, sigma_ptr, &ptrs); + xc_gga_call_with_output(self.inner.ptr, npoints, rho_ptr, sigma_ptr, &ptrs); } Ok(()) } @@ -415,7 +422,7 @@ impl LibXCFunctional { let (output_base, _sync) = buffer.device_ptr_mut(stream); unsafe { xc_mgga_call( - self.ptr, + self.inner.ptr, npoints, rho_ptr, sigma_ptr, @@ -453,7 +460,7 @@ impl LibXCFunctional { let extra = crate::layout_handling::MggaExtraPtrs::new(); unsafe { xc_mgga_call( - self.ptr, + self.inner.ptr, npoints, rho_ptr, sigma_ptr, @@ -500,7 +507,14 @@ impl LibXCFunctional { unsafe { xc_mgga_call_with_output( - self.ptr, npoints, rho_ptr, sigma_ptr, lapl_ptr, tau_ptr, &ptrs, &extra, + self.inner.ptr, + npoints, + rho_ptr, + sigma_ptr, + lapl_ptr, + tau_ptr, + &ptrs, + &extra, ); } Ok(()) diff --git a/libxc/src/error.rs b/libxc/src/error.rs index 65127fb..3f4849d 100644 --- a/libxc/src/error.rs +++ b/libxc/src/error.rs @@ -3,7 +3,11 @@ use std::fmt; use crate::enums::LibXCSpin; /// Error types for libxc operations. +/// +/// This enum is `#[non_exhaustive]`: new variants may be added in patch +/// releases, so downstream matches must include a wildcard (`_`) arm. #[derive(Debug)] +#[non_exhaustive] pub enum LibXCError { /// An error indicating xc-functional / parameter not found. NotFound(String), @@ -13,6 +17,17 @@ pub enum LibXCError { ComputeError(String), /// Error related to parameter setting. ParamSetError { param_name: String, details: String }, + /// Tried to mutate a functional that is currently shared with clones. + /// + /// All setters require exclusive access to the underlying C object: + /// drop every clone (or wait for the parallel scope holding them to + /// finish) before setting parameters, thresholds or coefficients. + SharedError, + /// The loaded libxc library is too old for the requested operation. + /// + /// `needed` is the minimum libxc version, `found` the version of the + /// loaded library (as reported by `xc_version`). + UnsupportedVersion { needed: (i32, i32, i32), found: (i32, i32, i32) }, /// Error related to CUDA operations. #[cfg(feature = "cuda")] CudaError(String), @@ -29,6 +44,17 @@ impl fmt::Display for LibXCError { LibXCError::ParamSetError { param_name, details } => { write!(f, "parameter set error for {param_name}: {details}") }, + LibXCError::SharedError => { + write!( + f, + "cannot mutate a shared functional: all clones must be dropped before calling setters" + ) + }, + LibXCError::UnsupportedVersion { needed, found } => write!( + f, + "this operation requires libxc >= {}.{}.{}, but the loaded library is {}.{}.{}", + needed.0, needed.1, needed.2, found.0, found.1, found.2 + ), #[cfg(feature = "cuda")] LibXCError::CudaError(msg) => write!(f, "cuda error: {msg}"), } diff --git a/libxc/src/functional.rs b/libxc/src/functional.rs index a181ad0..9af0f7b 100644 --- a/libxc/src/functional.rs +++ b/libxc/src/functional.rs @@ -122,17 +122,125 @@ pub struct LibXCReference { /// [`compute_xc_with_output`]: Self::compute_xc_with_output /// [`LibXCOutputLayout`]: crate::layout_handling::LibXCOutputLayout /// -/// # Notes on parallel +/// # Cloning and sharing across threads /// -/// This struct is made to be [`Sync`] and [`Send`] available. But note we have -/// not assured safety, so not to abuse them. Using send and sync for xc -/// computation is probably okay. +/// `LibXCFunctional` is cheaply cloneable. Cloning does **not** re-initialize +/// the underlying C functional: every clone is a reference-counted handle to +/// the same `xc_func_type`, and the C object is released only when the last +/// handle is dropped. This is the recommended way to share one functional +/// with worker threads: +/// +/// ```rust +/// use libxc::prelude::{libxc_enum_items::*, *}; +/// use rayon::prelude::*; +/// use std::collections::HashMap; +/// +/// let func = LibXCFunctional::from_identifier("gga_c_pbe", Unpolarized); +/// let rho: Vec = (0..2000).map(|i| 0.01 + 0.001 * i as f64).collect(); +/// let sigma: Vec = rho.iter().map(|r| 0.2 * r).collect(); +/// +/// // evaluate per 100-point chunk in parallel, sharing `func` (no locks) +/// let energy_par: f64 = rho +/// .par_chunks(100) +/// .zip(sigma.par_chunks(100)) +/// .map(|(rho, sigma)| { +/// let input = HashMap::from([ +/// ("rho".to_string(), rho as &[f64]), +/// ("sigma".to_string(), sigma as &[f64]), +/// ]); +/// let (buf, layout) = func.compute_xc(&input, 0).unwrap(); +/// buf[layout.get("zk").unwrap()].iter().sum::() +/// }) +/// .sum(); +/// +/// // same computation, serial +/// let input = HashMap::from([ +/// ("rho".to_string(), rho.as_slice()), +/// ("sigma".to_string(), sigma.as_slice()), +/// ]); +/// let (buf, layout) = func.compute_xc(&input, 0).unwrap(); +/// let energy_ser: f64 = buf[layout.get("zk").unwrap()].iter().sum(); +/// assert!((energy_par - energy_ser).abs() < 1e-10); +/// ``` +/// +/// # Notes on parallel and soundness +/// +/// This struct is [`Send`] and [`Sync`], and the two are designed for +/// lock-free sharing: there is no interior locking, and read-side usage +/// (compute, introspection) never blocks other threads. The soundness +/// argument is the following: +/// +/// - All libxc read entry points (the `xc_{lda,gga,mgga}_*` evaluation +/// routines, info/coefficient getters) take `const xc_func_type *` and do not +/// write through it; the struct holds only configuration, no scratch buffers. +/// Concurrent reads through shared handles are therefore data-race-free. +/// - Every mutating method (`set_*`) takes `&mut self` **and** requires that no +/// clone of the functional exists, enforced internally via the reference +/// count. While any clone is alive, setters fail with +/// [`LibXCError::SharedError`] (or panic, for the non-`_f` variants); +/// dropping the clones restores mutability. This rules out safe code mutating +/// the C object on one thread while another thread computes with it. pub struct LibXCFunctional { + pub(crate) inner: Arc, +} + +impl Clone for LibXCFunctional { + fn clone(&self) -> Self { + Self { inner: Arc::clone(&self.inner) } + } +} + +/// Sole owner of the underlying libxc C object. +/// +/// This private type is where the lifetime of the raw `xc_func_type` is +/// managed. The public [`LibXCFunctional`] holds it behind an [`Arc`], so +/// cloning shares (rather than duplicates) the C object, and the destructor +/// below runs exactly once, on the thread that drops the last handle. +pub(crate) struct LibXCFuncRaw { pub(crate) ptr: *mut ffi::xc_func_type, } -unsafe impl Send for LibXCFunctional {} -unsafe impl Sync for LibXCFunctional {} +// SAFETY (Send): `xc_func_type` is a plain heap-allocated C struct, not tied +// to the thread that created it. Sending the handle to another thread only +// moves the pointer; `xc_func_end`/`xc_func_free` merely release allocator +// (and, for GPU functionals, CUDA) memory, which is valid from any thread. +// +// SAFETY (Sync): every read-side libxc entry point (the `xc_{lda,gga,mgga}_*` +// evaluation routines, all `xc_func_info_*` and coefficient getters) takes +// `const xc_func_type *` and does not write through it: libxc's compute +// dispatch (`lda.c`/`gga.c`/`mgga.c`, `xc_mix_func`) treats a functional and +// its auxiliary functionals as read-only, and `xc_func_type` carries only +// configuration (spin, thresholds, coefficients, parameters), no scratch +// buffers. Concurrent reads through shared handles are therefore +// data-race-free. All mutation is funneled through `&mut self` setters that +// first prove `Arc` uniqueness (see [`LibXCFunctional::exclusive_ptr`]), so a +// mutation can never race with reads reachable from another handle. +unsafe impl Send for LibXCFuncRaw {} +unsafe impl Sync for LibXCFuncRaw {} + +impl Drop for LibXCFuncRaw { + fn drop(&mut self) { + unsafe { + ffi::xc_func_end(self.ptr); + ffi::xc_func_free(self.ptr); + } + } +} + +/// Private helpers. +impl LibXCFunctional { + /// Raw pointer for mutating FFI calls, requiring that no clone shares + /// this functional. + /// + /// `Arc::get_mut` succeeds only when this handle is the sole one left, + /// so mutation through the returned pointer cannot race with reads or + /// writes through any other handle. + pub(crate) fn exclusive_ptr(&mut self) -> Result<*mut ffi::xc_func_type, LibXCError> { + let raw: &mut LibXCFuncRaw = + Arc::get_mut(&mut self.inner).ok_or(LibXCError::SharedError)?; + Ok(raw.ptr) + } +} /// Creation functions implementation. impl LibXCFunctional { @@ -184,7 +292,7 @@ impl LibXCFunctional { ffi::xc_func_free(ptr); return Err(LibXCError::InitError { func_id, spin }); } - Ok(Self { ptr }) + Ok(Self { inner: Arc::new(LibXCFuncRaw { ptr }) }) } } @@ -267,7 +375,7 @@ impl LibXCFunctional { return Err(LibXCError::InitError { func_id, spin }); } (*(*ptr).info).flags |= device as i32; - Ok(Self { ptr }) + Ok(Self { inner: Arc::new(LibXCFuncRaw { ptr }) }) } } @@ -295,14 +403,14 @@ impl LibXCFunctional { /// /// Intended for advanced use; the caller must not free the pointer. pub fn as_ptr(&self) -> *const ffi::xc_func_type { - self.ptr + self.inner.ptr } /// Returns a raw pointer to the underlying `xc_func_type`. /// /// Intended for advanced use; the caller must not free the pointer. pub fn info(&self) -> *const ffi::xc_func_info_type { - unsafe { ffi::xc_func_get_info(self.ptr) } + unsafe { ffi::xc_func_get_info(self.inner.ptr) } } /// Functional number (ID). @@ -450,7 +558,7 @@ impl LibXCFunctional { /// /// `LibXCFunctional.xc_func.contents.nspin` pub fn spin(&self) -> LibXCSpin { - match unsafe { (*self.ptr).nspin as u32 } { + match unsafe { (*self.inner.ptr).nspin as u32 } { ffi::XC_UNPOLARIZED => LibXCSpin::Unpolarized, ffi::XC_POLARIZED => LibXCSpin::Polarized, n => panic!("Unknown spin code: {n}"), @@ -467,7 +575,7 @@ impl LibXCFunctional { /// let dim = xc_func.dim(); /// // output: xc_dimensions { rho: 1, sigma: 1, lapl: 0, tau: 0, zk: 1, vrho: 1, ... } pub fn dim(&self) -> &ffi::xc_dimensions { - unsafe { &(*self.ptr).dim } + unsafe { &(*self.inner.ptr).dim } } /// Whether this functional has a specific flag. @@ -712,27 +820,46 @@ impl LibXCFunctional { let ext_param_number = self.n_ext_params(); if ext_param_number > 0 { let ext_param_names = self.ext_param_names(); - let ext_param_values = self.ext_param_values(); let ext_param_descriptions = self.ext_param_descriptions(); let ext_param_default = self.ext_param_default_values(); - lst.push("External Parameters".to_string()); - for i in 0..ext_param_number as usize { - if ext_param_values[i] == ext_param_default[i] { - lst.push(format!( - " - {:>9} = {:<20} {}", - ext_param_names[i], - trim_float(ext_param_values[i]), - ext_param_descriptions[i], - )); - } else { - lst.push(format!( - " - {:>9} = {:<20} {:<50} (default: {:<20})", - ext_param_names[i], - trim_float(ext_param_values[i]), - ext_param_descriptions[i], - trim_float(ext_param_default[i]) - )) - }; + // reading current values requires libxc >= 7.0; fall back to + // defaults (with a note) on older libraries + match self.ext_param_values_f() { + Ok(ext_param_values) => { + lst.push("External Parameters".to_string()); + for i in 0..ext_param_number as usize { + if ext_param_values[i] == ext_param_default[i] { + lst.push(format!( + " - {:>9} = {:<20} {}", + ext_param_names[i], + trim_float(ext_param_values[i]), + ext_param_descriptions[i], + )); + } else { + lst.push(format!( + " - {:>9} = {:<20} {:<50} (default: {:<20})", + ext_param_names[i], + trim_float(ext_param_values[i]), + ext_param_descriptions[i], + trim_float(ext_param_default[i]) + )) + }; + } + }, + Err(_) => { + lst.push( + "External Parameters (current values need libxc >= 7.0; showing defaults)" + .to_string(), + ); + for i in 0..ext_param_number as usize { + lst.push(format!( + " - {:>9} = {:<20} {}", + ext_param_names[i], + trim_float(ext_param_default[i]), + ext_param_descriptions[i], + )); + } + }, } } // references @@ -841,10 +968,35 @@ impl LibXCFunctional { /// use libxc::prelude::{libxc_enum_items::*, *}; /// let mut xc_func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); /// xc_func.set_ext_params(&[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + /// # if libxc_version().0 >= 7 { // read-back requires libxc >= 7.0 /// assert_eq!(xc_func.ext_param_values(), &[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + /// # } + /// ``` + /// + /// # Panics + /// + /// Panics on libxc < 7.0, which has no public API to read back current + /// parameter values; use [`ext_param_values_f`](Self::ext_param_values_f) + /// to avoid panicking. Setting parameters still works on those versions. pub fn ext_param_values(&self) -> Vec { + self.ext_param_values_f().unwrap() + } + + /// Current values of the external parameters (fallible). + /// + /// Fails with [`LibXCError::UnsupportedVersion`] on libxc < 7.0: the + /// `xc_func_get_ext_params*` getters were only introduced in libxc v7.0 + /// (see `versioning_xc.md`), and libxc 6.x keeps current values only in + /// per-functional storage with no uniform read-back. + pub fn ext_param_values_f(&self) -> Result, LibXCError> { + let version = crate::util::libxc_version(); + if version < (7, 0, 0) { + return Err(LibXCError::UnsupportedVersion { needed: (7, 0, 0), found: version }); + } let n = self.n_ext_params(); - (0..n).map(|i| unsafe { ffi::xc_func_get_ext_params_value(self.ptr, i as c_int) }).collect() + Ok((0..n) + .map(|i| unsafe { ffi::xc_func_get_ext_params_value(self.inner.ptr, i as c_int) }) + .collect()) } /// Returns a map of external parameter names to their (default value, @@ -892,9 +1044,11 @@ impl LibXCFunctional { /// use libxc::prelude::{libxc_enum_items::*, *}; /// let mut xc_func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); /// xc_func.set_ext_params(&[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + /// # if libxc_version().0 >= 7 { // read-back requires libxc >= 7.0 /// for (key, val) in xc_func.ext_param_map() { /// println!("{key:>10}: {val}"); /// } + /// # } /// // Output: /// // _a: 0.1 /// // _b: 0.1 @@ -904,14 +1058,28 @@ impl LibXCFunctional { /// // _m2: 0.8 /// // _omega: 0.5 /// ``` + /// + /// # Panics + /// + /// Panics on libxc < 7.0 (see + /// [`ext_param_values`](Self::ext_param_values)); + /// use [`ext_param_map_f`](Self::ext_param_map_f) to avoid panicking. pub fn ext_param_map(&self) -> IndexMap { + self.ext_param_map_f().unwrap() + } + + /// Returns a map of external parameter names to their current values + /// (fallible). + /// + /// Fails with [`LibXCError::UnsupportedVersion`] on libxc < 7.0. + pub fn ext_param_map_f(&self) -> Result, LibXCError> { let names = self.ext_param_names(); - let values = self.ext_param_values(); + let values = self.ext_param_values_f()?; let mut map = IndexMap::new(); for i in 0..names.len() { map.insert(names[i].clone(), values[i]); } - map + Ok(map) } /// Set all external parameters at once. @@ -933,8 +1101,9 @@ impl LibXCFunctional { details: format!("expected {n} parameters, got {}", params.len()), }); } + let ptr = self.exclusive_ptr()?; unsafe { - ffi::xc_func_set_ext_params(self.ptr, params.as_ptr()); + ffi::xc_func_set_ext_params(ptr, params.as_ptr()); } Ok(()) } @@ -949,9 +1118,11 @@ impl LibXCFunctional { /// let mut xc_func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); /// let update_ext_param_map = HashMap::from([("_a", 0.1), ("_d", 0.2), ("_omega", 0.58)]); /// xc_func.set_ext_param_map(update_ext_param_map.iter()); + /// # if libxc_version().0 >= 7 { // read-back requires libxc >= 7.0 /// for (key, val) in xc_func.ext_param_map() { /// println!("{key:>10}: {val}"); /// } + /// # } /// // Output: /// // _a: 0.1 /// // _b: 0.132 @@ -979,7 +1150,17 @@ impl LibXCFunctional { &mut self, param_map: impl Iterator, impl Borrow)>, ) -> Result<(), LibXCError> { - let mut map = self.ext_param_map(); + // On libxc < 7.0 current values cannot be read back; start from the + // defaults. This mirrors libxc 6.x semantics, where setting a single + // parameter by name also resets all others to their defaults. + let mut map = match self.ext_param_map_f() { + Ok(map) => map, + Err(_) => { + let names = self.ext_param_names(); + let defaults = self.ext_param_default_values(); + names.into_iter().zip(defaults).collect() + }, + }; for (key, val) in param_map.into_iter() { let (key, val) = (key.as_ref(), *val.borrow()); if !map.contains_key(key) { @@ -1005,9 +1186,11 @@ impl LibXCFunctional { /// xc_func.set_ext_param_by_name("_a", 0.1); /// xc_func.set_ext_param_by_name("_d", 0.2); /// xc_func.set_ext_param_by_name("_omega", 0.58); + /// # if libxc_version().0 >= 7 { // read-back requires libxc >= 7.0 /// for (key, val) in xc_func.ext_param_map() { /// println!("{key:>10}: {val}"); /// } + /// # } /// // Output: /// // _a: 0.1 /// // _b: 0.132 @@ -1029,8 +1212,9 @@ impl LibXCFunctional { details: "external parameter not found".to_string(), }); } + let ptr = self.exclusive_ptr()?; let c_name = CString::new(name).expect("parameter name contains null byte"); - unsafe { ffi::xc_func_set_ext_params_name(self.ptr, c_name.as_ptr(), value) }; + unsafe { ffi::xc_func_set_ext_params_name(ptr, c_name.as_ptr(), value) }; Ok(()) } } @@ -1049,48 +1233,128 @@ impl LibXCFunctional { /// println!("{:?}", xc_func.dens_threshold()); // 1e-12 /// ``` pub fn dens_threshold(&self) -> f64 { - unsafe { (*self.ptr).dens_threshold } + unsafe { (*self.inner.ptr).dens_threshold } } /// Set the density threshold. + /// + /// # Panics + /// + /// Panics if the functional is currently shared with clones (see + /// [`LibXCFunctional`] docs on cloning); use + /// [`set_dens_threshold_f`](Self::set_dens_threshold_f) to avoid panicking. pub fn set_dens_threshold(&mut self, threshold: f64) { - unsafe { ffi::xc_func_set_dens_threshold(self.ptr, threshold) } + self.set_dens_threshold_f(threshold).unwrap() + } + + /// Set the density threshold (fallible). + /// + /// Fails with [`LibXCError::SharedError`] if the functional is currently + /// shared with clones; drop them first. + pub fn set_dens_threshold_f(&mut self, threshold: f64) -> Result<(), LibXCError> { + let ptr = self.exclusive_ptr()?; + unsafe { ffi::xc_func_set_dens_threshold(ptr, threshold) } + Ok(()) } /// Zeta (spin polarization) threshold for numerical stability. pub fn zeta_threshold(&self) -> f64 { - unsafe { (*self.ptr).zeta_threshold } + unsafe { (*self.inner.ptr).zeta_threshold } } /// Set the zeta (spin polarization) threshold. + /// + /// # Panics + /// + /// Panics if the functional is currently shared with clones; use + /// [`set_zeta_threshold_f`](Self::set_zeta_threshold_f) to avoid panicking. pub fn set_zeta_threshold(&mut self, threshold: f64) { - unsafe { ffi::xc_func_set_zeta_threshold(self.ptr, threshold) } + self.set_zeta_threshold_f(threshold).unwrap() + } + + /// Set the zeta (spin polarization) threshold (fallible). + /// + /// Fails with [`LibXCError::SharedError`] if the functional is currently + /// shared with clones; drop them first. + pub fn set_zeta_threshold_f(&mut self, threshold: f64) -> Result<(), LibXCError> { + let ptr = self.exclusive_ptr()?; + unsafe { ffi::xc_func_set_zeta_threshold(ptr, threshold) } + Ok(()) } /// Sigma (reduced gradient) threshold for numerical stability. pub fn sigma_threshold(&self) -> f64 { - unsafe { (*self.ptr).sigma_threshold } + unsafe { (*self.inner.ptr).sigma_threshold } } /// Set the sigma (reduced gradient) threshold. + /// + /// # Panics + /// + /// Panics if the functional is currently shared with clones; use + /// [`set_sigma_threshold_f`](Self::set_sigma_threshold_f) to avoid + /// panicking. pub fn set_sigma_threshold(&mut self, threshold: f64) { - unsafe { ffi::xc_func_set_sigma_threshold(self.ptr, threshold) } + self.set_sigma_threshold_f(threshold).unwrap() + } + + /// Set the sigma (reduced gradient) threshold (fallible). + /// + /// Fails with [`LibXCError::SharedError`] if the functional is currently + /// shared with clones; drop them first. + pub fn set_sigma_threshold_f(&mut self, threshold: f64) -> Result<(), LibXCError> { + let ptr = self.exclusive_ptr()?; + unsafe { ffi::xc_func_set_sigma_threshold(ptr, threshold) } + Ok(()) } /// Tau (kinetic energy density) threshold for numerical stability. pub fn tau_threshold(&self) -> f64 { - unsafe { (*self.ptr).tau_threshold } + unsafe { (*self.inner.ptr).tau_threshold } } /// Set the tau (kinetic energy density) threshold. + /// + /// # Panics + /// + /// Panics if the functional is currently shared with clones; use + /// [`set_tau_threshold_f`](Self::set_tau_threshold_f) to avoid panicking. pub fn set_tau_threshold(&mut self, threshold: f64) { - unsafe { ffi::xc_func_set_tau_threshold(self.ptr, threshold) } + self.set_tau_threshold_f(threshold).unwrap() + } + + /// Set the tau (kinetic energy density) threshold (fallible). + /// + /// Fails with [`LibXCError::SharedError`] if the functional is currently + /// shared with clones; drop them first. + pub fn set_tau_threshold_f(&mut self, threshold: f64) -> Result<(), LibXCError> { + let ptr = self.exclusive_ptr()?; + unsafe { ffi::xc_func_set_tau_threshold(ptr, threshold) } + Ok(()) } /// Enable or disable Fermi hole curvature enforcement (api-v7_0+). + /// + /// # Panics + /// + /// Panics if the functional is currently shared with clones; use + /// [`set_fhc_enforcement_f`](Self::set_fhc_enforcement_f) to avoid + /// panicking. #[cfg(feature = "api-v7_0")] pub fn set_fhc_enforcement(&mut self, on: bool) { - unsafe { ffi::xc_func_set_fhc_enforcement(self.ptr, on as c_int) } + self.set_fhc_enforcement_f(on).unwrap() + } + + /// Enable or disable Fermi hole curvature enforcement (fallible, + /// api-v7_0+). + /// + /// Fails with [`LibXCError::SharedError`] if the functional is currently + /// shared with clones; drop them first. + #[cfg(feature = "api-v7_0")] + pub fn set_fhc_enforcement_f(&mut self, on: bool) -> Result<(), LibXCError> { + let ptr = self.exclusive_ptr()?; + unsafe { ffi::xc_func_set_fhc_enforcement(ptr, on as c_int) } + Ok(()) } } @@ -1121,7 +1385,7 @@ impl LibXCFunctional { if matches!(self.family(), LibXCFamily::HybGGA | LibXCFamily::HybMGGA | LibXCFamily::HybLDA) && !self.is_hyb_cam() { - Some(unsafe { ffi::xc_hyb_exx_coef(self.ptr) }) + Some(unsafe { ffi::xc_hyb_exx_coef(self.inner.ptr) }) } else { None } @@ -1154,7 +1418,7 @@ impl LibXCFunctional { let mut omega: f64 = 0.0; let mut alpha: f64 = 0.0; let mut beta: f64 = 0.0; - unsafe { ffi::xc_hyb_cam_coef(self.ptr, &mut omega, &mut alpha, &mut beta) }; + unsafe { ffi::xc_hyb_cam_coef(self.inner.ptr, &mut omega, &mut alpha, &mut beta) }; Some((omega, alpha, beta)) } else { None @@ -1185,7 +1449,7 @@ impl LibXCFunctional { #[allow(non_snake_case)] let mut nlc_C: f64 = 0.0; unsafe { - ffi::xc_nlc_coef(self.ptr, &mut nlc_b, &mut nlc_C); + ffi::xc_nlc_coef(self.inner.ptr, &mut nlc_b, &mut nlc_C); } Some((nlc_b, nlc_C)) } else { @@ -1255,24 +1519,15 @@ impl LibXCFunctional { /// /// `LibXCFunctional.get_aux_funcs(return_ids=True)` pub fn aux_funcs_by_id(&self) -> Vec<(i32, f64)> { - let n = unsafe { ffi::xc_num_aux_funcs(self.ptr) as i32 }; + let n = unsafe { ffi::xc_num_aux_funcs(self.inner.ptr) as i32 }; let mut ids = vec![0 as c_int; n as usize]; let mut weights = vec![0.0f64; n as usize]; - unsafe { ffi::xc_aux_func_ids(self.ptr, ids.as_mut_ptr()) } - unsafe { ffi::xc_aux_func_weights(self.ptr, weights.as_mut_ptr()) } + unsafe { ffi::xc_aux_func_ids(self.inner.ptr, ids.as_mut_ptr()) } + unsafe { ffi::xc_aux_func_weights(self.inner.ptr, weights.as_mut_ptr()) } ids.into_iter().zip(weights).collect() } } -impl Drop for LibXCFunctional { - fn drop(&mut self) { - unsafe { - ffi::xc_func_end(self.ptr); - ffi::xc_func_free(self.ptr); - } - } -} - impl core::fmt::Debug for LibXCFunctional { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("LibXCFunctional") diff --git a/libxc/src/functional_specific.rs b/libxc/src/functional_specific.rs index e788e6d..983f27e 100644 --- a/libxc/src/functional_specific.rs +++ b/libxc/src/functional_specific.rs @@ -47,7 +47,8 @@ impl LibXCFunctional { if let Some(details) = details { Err(LibXCError::ParamSetError { param_name: "hyb_exx_coef".to_string(), details }) } else { - unsafe { (*self.ptr).cam_alpha = hyb_exx_coef }; + let ptr = self.exclusive_ptr()?; + unsafe { (*ptr).cam_alpha = hyb_exx_coef }; Ok(()) } } @@ -90,10 +91,11 @@ impl LibXCFunctional { } // set the CAM coefficients, which is usually the canonical way to get // these parameters + let ptr = self.exclusive_ptr()?; unsafe { - (*self.ptr).cam_alpha = cam_alpha; - (*self.ptr).cam_beta = cam_beta; - (*self.ptr).cam_omega = cam_omega; + (*ptr).cam_alpha = cam_alpha; + (*ptr).cam_beta = cam_beta; + (*ptr).cam_omega = cam_omega; } // also try to set the ext_params if self.ext_param_names().iter().any(|name| name == "_alpha") { @@ -154,9 +156,10 @@ impl LibXCFunctional { } // set the VV10 coefficients, which is the canonical way to get these // parameters + let ptr = self.exclusive_ptr()?; unsafe { - (*self.ptr).nlc_b = nlc_b; - (*self.ptr).nlc_C = nlc_C; + (*ptr).nlc_b = nlc_b; + (*ptr).nlc_C = nlc_C; } // also set the ext_params for specific functionals if [LibXCFuncId::GGA_XC_VV10 as i32, LibXCFuncId::HYB_GGA_XC_LC_VV10 as i32] diff --git a/libxc/src/lib.rs b/libxc/src/lib.rs index c56fb39..f1988ba 100644 --- a/libxc/src/lib.rs +++ b/libxc/src/lib.rs @@ -53,6 +53,7 @@ pub mod prelude { pub(crate) use libxc_ffi::ffi; pub(crate) use std::collections::HashMap; pub(crate) use std::ffi::CString; + pub(crate) use std::sync::Arc; } // documentation exception diff --git a/libxc/tests/general/mod.rs b/libxc/tests/general/mod.rs index c238124..b3ec28e 100644 --- a/libxc/tests/general/mod.rs +++ b/libxc/tests/general/mod.rs @@ -1,2 +1,3 @@ +mod test_clone; mod test_functional; mod test_util; diff --git a/libxc/tests/general/test_clone.rs b/libxc/tests/general/test_clone.rs new file mode 100644 index 0000000..7929dfc --- /dev/null +++ b/libxc/tests/general/test_clone.rs @@ -0,0 +1,334 @@ +//! Tests for cloning and thread-sharing of `LibXCFunctional`. +//! +//! `LibXCFunctional` clones share the underlying C functional through +//! reference counting. These tests verify: shared identity of clones, +//! lock-free parallel evaluation (rayon and raw threads), and the +//! exclusivity requirement of all mutating setters. + +use libxc::prelude::libxc_enum_items::*; +use libxc::prelude::*; +use rayon::prelude::*; +use std::collections::HashMap; + +/// Simple PCG-style PRNG for deterministic test data (same as +/// test_functional.rs). +fn pseudo_random(seed: u64, n: usize) -> Vec { + let mut state = seed; + let mut v = Vec::with_capacity(n); + for _ in 0..n { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + v.push(((state >> 33) as f64 + 0.5) / (1u64 << 31) as f64); + } + v +} + +/// Compare two slices with relative and absolute tolerance (mirrors +/// np.allclose). +fn allclose(a: &[f64], b: &[f64], rtol: f64, atol: f64) -> bool { + if a.len() != b.len() { + return false; + } + a.iter().zip(b.iter()).all(|(&x, &y)| (x - y).abs() <= atol + rtol * y.abs()) +} + +/// Spin dimension of an input array. +fn input_dim(name: &str, spin: LibXCSpin) -> usize { + match (name, spin) { + ("rho", Unpolarized) => 1, + ("rho", Polarized) => 2, + ("sigma", Unpolarized) => 1, + ("sigma", Polarized) => 3, + ("lapl", Unpolarized) | ("tau", Unpolarized) => 1, + ("lapl", Polarized) | ("tau", Polarized) => 2, + _ => panic!("unknown input name: {name}"), + } +} + +// =========================================================================== +// Send / Sync +// =========================================================================== + +fn assert_send() {} +fn assert_sync() {} + +#[test] +fn functional_is_send_and_sync() { + assert_send::(); + assert_sync::(); + // sharing &functional with other threads (rayon) requires Sync + assert_send::<&LibXCFunctional>(); +} + +// =========================================================================== +// Clone semantics +// =========================================================================== + +#[test] +fn clone_shares_underlying_functional() { + let func = LibXCFunctional::from_identifier("gga_c_pbe", Unpolarized); + let cloned = func.clone(); + + // both handles reference the same C object (no FFI-level copy) + assert_eq!(func.as_ptr(), cloned.as_ptr()); + + // both compute the same numbers + let rho: Vec = pseudo_random(11, 40); + let sigma: Vec = pseudo_random(12, 40); + let input = HashMap::from([ + ("rho".to_string(), rho.as_slice()), + ("sigma".to_string(), sigma.as_slice()), + ]); + let (buf1, layout1) = func.compute_xc(&input, 1).unwrap(); + let (buf2, layout2) = cloned.compute_xc(&input, 1).unwrap(); + for key in ["zk", "vrho", "vsigma"] { + assert!(allclose( + &buf1[layout1.get(key).unwrap()], + &buf2[layout2.get(key).unwrap()], + 0.0, + 0.0 + )); + } + + // the C object survives as long as one handle is alive + drop(func); + let (buf3, layout3) = cloned.compute_xc(&input, 1).unwrap(); + assert!(allclose( + &buf1[layout1.get("zk").unwrap()], + &buf3[layout3.get("zk").unwrap()], + 0.0, + 0.0 + )); +} + +#[test] +fn clone_sees_parameters_set_before_cloning() { + // a fresh FFI-level copy would reset these to defaults; a shared handle + // must see the values set on the original + let mut func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); + let params = [0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]; + func.set_ext_params(¶ms); + + let cloned = func.clone(); + // reading current values back requires xc_func_get_ext_params_value, + // which libxc only exports from v7.0 on + if libxc_version().0 >= 7 { + assert_eq!(cloned.ext_param_values(), params); + assert_ne!(cloned.ext_param_values(), cloned.ext_param_default_values()); + } else { + assert!(matches!( + cloned.ext_param_values_f().unwrap_err(), + LibXCError::UnsupportedVersion { .. } + )); + } +} + +#[test] +fn cloned_functional_supports_introspection() { + let func = LibXCFunctional::from_identifier("hyb_gga_xc_b3lyp", Unpolarized); + let cloned = func.clone(); + assert_eq!(cloned.identifier(), func.identifier()); + assert_eq!(cloned.number(), func.number()); + assert_eq!(cloned.family(), func.family()); + assert_eq!(cloned.spin(), func.spin()); + assert_eq!(cloned.hyb_exx_coef(), func.hyb_exx_coef()); + assert!(!cloned.references().is_empty()); + + // describe() degrades to default ext-param values on libxc < 7.0 + assert!(!cloned.describe().is_empty()); + assert!(cloned.describe().contains("External Parameters")); +} + +// =========================================================================== +// Setter exclusivity +// =========================================================================== + +#[test] +fn setters_are_refused_while_shared() { + let mut func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); + let guard = func.clone(); + + let params = [0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]; + assert!(matches!(func.set_ext_params_f(¶ms), Err(LibXCError::SharedError))); + assert!(matches!(func.set_ext_param_by_name_f("_a", 0.1), Err(LibXCError::SharedError))); + assert!(matches!(func.set_dens_threshold_f(1e-12), Err(LibXCError::SharedError))); + assert!(matches!(func.set_zeta_threshold_f(1e-12), Err(LibXCError::SharedError))); + assert!(matches!(func.set_sigma_threshold_f(1e-12), Err(LibXCError::SharedError))); + assert!(matches!(func.set_tau_threshold_f(1e-12), Err(LibXCError::SharedError))); + #[cfg(feature = "api-v7_0")] + assert!(matches!(func.set_fhc_enforcement_f(true), Err(LibXCError::SharedError))); + + let mut hyb = LibXCFunctional::from_identifier("hyb_gga_xc_b3lyp", Unpolarized); + let _g = hyb.clone(); + assert!(matches!(hyb.set_hyb_exx_coef_f(0.5), Err(LibXCError::SharedError))); + + let mut cam = LibXCFunctional::from_identifier("hyb_gga_xc_wb97x", Unpolarized); + let _g = cam.clone(); + assert!(matches!(cam.set_cam_coef_f(0.19, 0.46, 0.33), Err(LibXCError::SharedError))); + + let mut vv10 = LibXCFunctional::from_identifier("hyb_gga_xc_wb97x_v", Unpolarized); + let _g = vv10.clone(); + assert!(matches!(vv10.set_vv10_coef_f(6.0, 0.01), Err(LibXCError::SharedError))); + + drop(guard); +} + +#[test] +#[should_panic(expected = "SharedError")] +fn non_f_setter_panics_while_shared() { + let mut func = LibXCFunctional::from_identifier("gga_c_pbe", Unpolarized); + let _guard = func.clone(); + func.set_dens_threshold(1e-12); +} + +#[test] +fn setters_work_again_after_clones_dropped() { + let mut func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); + { + let c1 = func.clone(); + let c2 = func.clone(); + assert!(func.set_ext_params_f(&[0.1; 7]).is_err()); + drop(c1); + // one clone still alive + assert!(func.set_ext_params_f(&[0.1; 7]).is_err()); + drop(c2); + } + + // unique again: mutation is allowed + let params = [0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]; + func.set_ext_params(¶ms); + if libxc_version().0 >= 7 { + assert_eq!(func.ext_param_values(), params); + } + // by-name and by-map setters work on all versions + func.set_ext_param_by_name("_a", 0.3); + func.set_ext_param_map([("_b", 0.4)].into_iter()); + func.set_dens_threshold(1e-13); + assert_eq!(func.dens_threshold(), 1e-13); + + // functional is still usable for computation afterwards + let rho: Vec = pseudo_random(21, 8); + let sigma: Vec = pseudo_random(22, 8); + let input = HashMap::from([ + ("rho".to_string(), rho.as_slice()), + ("sigma".to_string(), sigma.as_slice()), + ]); + let (_, layout) = func.compute_xc(&input, 0).unwrap(); + assert_eq!(layout.get("zk").unwrap().len(), 8); +} + +// =========================================================================== +// Parallel evaluation +// =========================================================================== + +/// Evaluate a functional in parallel chunks (sharing `&func` between rayon +/// workers, no locks) and compare with the serial result over the full grid. +fn run_parallel_compute_test(identifier: &str, spin: LibXCSpin, inputs: &[&str], seed: u64) { + const NPOINT: usize = 2048; + const CHUNK: usize = 64; + + let func = LibXCFunctional::from_identifier(identifier, spin); + let data: Vec<(&str, Vec)> = inputs + .iter() + .map(|&name| (name, pseudo_random(seed, NPOINT * input_dim(name, spin)))) + .collect(); + + // serial reference over the full grid + let input_full: HashMap = + data.iter().map(|(n, v)| (n.to_string(), v.as_slice())).collect(); + let (ser_buf, ser_layout) = func.compute_xc(&input_full, 1).unwrap(); + let zk_ser = &ser_buf[ser_layout.get("zk").unwrap()]; + + // pattern 1: borrow &func inside parallel jobs (requires Sync) + let n_chunks = NPOINT / CHUNK; + let zk_par_shared: Vec = (0..n_chunks) + .into_par_iter() + .flat_map(|c| { + let input: HashMap = data + .iter() + .map(|(n, v)| { + let d = input_dim(n, spin); + (n.to_string(), &v[c * CHUNK * d..(c + 1) * CHUNK * d]) + }) + .collect(); + let (buf, layout) = func.compute_xc(&input, 1).unwrap(); + buf[layout.get("zk").unwrap()].to_vec() + }) + .collect(); + + // pattern 2: clone the handle into each job (requires Send); all clones + // still reference the same C functional + let shared = func.clone(); + let zk_par_cloned: Vec = (0..n_chunks) + .into_par_iter() + .flat_map(|c| { + let func = shared.clone(); + let input: HashMap = data + .iter() + .map(|(n, v)| { + let d = input_dim(n, spin); + (n.to_string(), &v[c * CHUNK * d..(c + 1) * CHUNK * d]) + }) + .collect(); + let (buf, layout) = func.compute_xc(&input, 1).unwrap(); + buf[layout.get("zk").unwrap()].to_vec() + }) + .collect(); + + assert_eq!(zk_par_shared.len(), zk_ser.len()); + assert_eq!(zk_par_cloned.len(), zk_ser.len()); + assert!(allclose(&zk_par_shared, zk_ser, 1e-7, 1e-14)); + assert!(allclose(&zk_par_cloned, zk_ser, 1e-7, 1e-14)); +} + +#[test] +fn parallel_compute_lda() { + run_parallel_compute_test("lda_x", Unpolarized, &["rho"], 101); + run_parallel_compute_test("lda_x", Polarized, &["rho"], 102); +} + +#[test] +fn parallel_compute_gga() { + run_parallel_compute_test("gga_c_pbe", Unpolarized, &["rho", "sigma"], 103); + run_parallel_compute_test("gga_c_pbe", Polarized, &["rho", "sigma"], 104); +} + +#[test] +fn parallel_compute_hybrid_gga() { + // hybrid with auxiliary functionals: exercises the mix path concurrently + run_parallel_compute_test("hyb_gga_xc_b3lyp", Unpolarized, &["rho", "sigma"], 105); + run_parallel_compute_test("hyb_gga_xc_b3lyp", Polarized, &["rho", "sigma"], 106); +} + +#[test] +fn parallel_compute_mgga() { + run_parallel_compute_test("mgga_c_tpss", Unpolarized, &["rho", "sigma", "tau"], 107); + run_parallel_compute_test("mgga_c_tpss", Polarized, &["rho", "sigma", "tau"], 108); +} + +#[test] +fn clone_can_be_sent_to_os_threads() { + let func = LibXCFunctional::from_identifier("lda_x", Unpolarized); + + let handles: Vec<_> = (0..4) + .map(|_| { + let func = func.clone(); + std::thread::spawn(move || { + let rho: Vec = pseudo_random(200, 32); + let input = HashMap::from([("rho".to_string(), rho.as_slice())]); + let (buf, layout) = func.compute_xc(&input, 0).unwrap(); + buf[layout.get("zk").unwrap()].to_vec() + }) + }) + .collect(); + + let results: Vec> = handles.into_iter().map(|h| h.join().unwrap()).collect(); + for r in &results[1..] { + assert!(allclose(&results[0], r, 0.0, 0.0)); + } + + // original handle still valid after worker threads dropped their clones + let rho: Vec = pseudo_random(200, 32); + let input = HashMap::from([("rho".to_string(), rho.as_slice())]); + let (buf, layout) = func.compute_xc(&input, 0).unwrap(); + assert!(allclose(&results[0], &buf[layout.get("zk").unwrap()], 0.0, 0.0)); +} diff --git a/libxc/tests/general/test_functional.rs b/libxc/tests/general/test_functional.rs index e5bf8d9..2dd2d23 100644 --- a/libxc/tests/general/test_functional.rs +++ b/libxc/tests/general/test_functional.rs @@ -158,6 +158,45 @@ fn test_ext_params_wrong_length() { assert!(func.set_ext_params_f(&[5.0, 3.0]).is_err()); // should be 3 params } +// =========================================================================== +// test_ext_param_values_version_behavior +// =========================================================================== + +#[test] +// Reading back current ext-param values needs xc_func_get_ext_params_value, +// introduced in libxc v7.0 (see versioning_xc.md). On older libraries the +// fallible getters must return UnsupportedVersion instead of panicking, and +// setters/describe must keep working. +fn test_ext_param_values_version_behavior() { + let mut func = LibXCFunctional::from_identifier("gga_c_lypr", Unpolarized); + assert_eq!(func.n_ext_params(), 7); + + if libxc_version().0 >= 7 { + let values = func.ext_param_values_f().unwrap(); + assert_eq!(values.len(), 7); + assert_eq!(func.ext_param_map_f().unwrap().len(), 7); + func.set_ext_params(&[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + assert_eq!(func.ext_param_values(), &[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + } else { + assert!(matches!( + func.ext_param_values_f().unwrap_err(), + LibXCError::UnsupportedVersion { .. } + )); + assert!(matches!( + func.ext_param_map_f().unwrap_err(), + LibXCError::UnsupportedVersion { .. } + )); + // setters still work on libxc 6.x + func.set_ext_params(&[0.1, 0.1, 0.2, 0.3, 0.2, 0.8, 0.5]); + func.set_ext_param_by_name("_a", 0.3); + func.set_ext_param_map([("_b", 0.4)].into_iter()); + } + + // describe degrades to default values on libxc < 7.0 instead of panicking + let described = func.describe(); + assert!(described.contains("External Parameters")); +} + // =========================================================================== // test_lda_compute // ===========================================================================