Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .claude/skills/cuda-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/test-cpu.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
21 changes: 0 additions & 21 deletions .github/workflows/test-v7_0.yml

This file was deleted.

19 changes: 17 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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 }
25 changes: 16 additions & 9 deletions libxc/src/compute_cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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))
}
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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))
}
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(())
Expand Down
32 changes: 23 additions & 9 deletions libxc/src/compute_cuda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
}
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(())
Expand Down
Loading
Loading