From 54839c51abcc16b94c8a6613f98d6fffca831054 Mon Sep 17 00:00:00 2001 From: ajz34 Date: Thu, 7 May 2026 18:10:43 +0800 Subject: [PATCH 1/5] update some claude instruction --- .claude/CLAUDE.md | 35 +++++++++++++++++++++++++++ .claude/rules/agent-and-maintaince.md | 8 +++--- .gitignore | 1 + readme.md | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 .claude/CLAUDE.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..a8d7c69 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,35 @@ +# Agent instruction of DFTD3 Rust FFI and Wrapper + +## Notes to human developers + +You should also create a file `CLAUDE.local.md` to place local resources: +- `DFTD3_REPO_PATH`: local of original simple-dftd3 repository. The source code can help you understand how dftd3 works. + +## DFTD3 original library + +General rules +- This repository should live at `DFTD3_REPO_PATH`, which is defined in `CLAUDE.local.md`. +- **This repository should not be modified**, unless you are going to checkout specific tags (versions) of dftd3. + +Important files for FFI and wrapper development: +- `include/s-dftd3.h`: the headers. Note that these files are also copied to this project under `dftd3/headers` folder. +- `python/dftd3`: the python wrapper of dftd3. We should at least implement all major features of the certain wrapper: + - `interface.py`, corresponding to this project `dftd3/src/interface.rs`, also `interface_gcp.rs`. + - `parameters.py`, corresponding to this project `dftd3/src/parameters.rs`. + - Make sure the functionalities are tested. We use `dftd3/example/test_interface.rs` corresponding to `test_interface.py` in the original wrapper for testing. +- `assets/parameters.toml`: the parameters file, which should be copied to `dftd3/src/assets/parameters.toml` in this project. + +## The additional feature in this crate + +- We support toml parsing of DFTD3 parameters. The related code is at `/dftd3/src/parsing.rs`. The related test is at `dftd3/example/test_parsing.rs`. +- We support dynamic loading of the DFTD3 library. +- We use tags such as `api-v0_5` to reflect the API version of DFTD3 we are using. + +## Naming convention + +- For functions and structs that will be exposed to users, add prefix `dftd3_` for general functions, and `DFTD3` for structs. +- If some function is to be fallible, we can add suffix `_f` (`fn _f -> Result<_, DFTD3Error>`). + +## Header handling + +We use bindgen (python script at `scripts/generate_bindings.py`) to generate Rust bindings for the C header files. **Not modify the generated files directly**. diff --git a/.claude/rules/agent-and-maintaince.md b/.claude/rules/agent-and-maintaince.md index 457b24d..0dec9aa 100644 --- a/.claude/rules/agent-and-maintaince.md +++ b/.claude/rules/agent-and-maintaince.md @@ -6,17 +6,17 @@ - Multi-co-author format (note no extra newline between co-authors): ``` Co-authored-by: Claude Code - Co-authored-by: glm-5 + Co-authored-by: glm-5.1 ``` - First co-author: Agent - Claude Code: noreply@anthropic.com - Second co-author: Model - qwen* (eg. qwen3.5-plus): qianwen_opensource@alibabacloud.com - - glm* (eg. glm-5): service@zhipuai.cn + - glm* (eg. glm-5.1): service@zhipuai.cn - minimax* (eg. MiniMax-M2.5): model@minimax.io - deepseek* (eg. DeepSeek-V3.2): service@deepseek.com - kimi* (eg. kimi-k2.5): growth@moonshot.cn - doubao* (eg. doubao-seed-2.0-code): doubao-llm@bytedance.com - - Model name should include the version or details, such as `qwen3.5-plus`, `glm-5`, which can be inferred by Claude Code's `/model` property. + - Model name should include the version or details, such as `qwen3.5-plus`, `glm-5.1`, which can be inferred by Claude Code's `/model` property. -- You should not git commit by yourself. Please return the code changes to the user, and let the user decide whether to commit or not. +- You should not git commit by yourself, even you are in bypass mode. Please return the code changes to the user, and let the user decide whether to commit or not. diff --git a/.gitignore b/.gitignore index 2a6f7aa..1d66cba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ tarpaulin-report.html .idea book *.npy +*.local.* # bindgen blas_bindgen.h diff --git a/readme.md b/readme.md index c25e80b..445ddb3 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ This project contains simple-dftd3 FFI bindings, wrapper and build-from-source. -Current binding of simple-dftd3: [![v1.3.1](https://img.shields.io/github/v/release/dftd3/simple-dftd3)](https://github.com/dftd3/simple-dftd3/releases/v1.3.1) +Current binding of simple-dftd3: [![v1.3.1](https://img.shields.io/github/v/release/dftd3/simple-dftd3?filter=v1.3.1)](https://github.com/dftd3/simple-dftd3/releases/v1.3.1) Source code of simple-dftd3 is available on [github](https://github.com/dftd3/simple-dftd3). From d89ab147ff6737943f3070c5ba3f8619d1a6beea Mon Sep 17 00:00:00 2001 From: ajz34 Date: Thu, 7 May 2026 18:52:40 +0800 Subject: [PATCH 2/5] Add version update guide as skill and invariant rules Split version update documentation into: - .claude/commands/version-update.md: on-demand skill (/version-update ) with the full procedural workflow for updating bindings - .claude/rules/version-update-guide.md: always-loaded invariant rules (naming conventions, feature flag rules, FFI generation constraints) Co-authored-by: Claude Code Co-authored-by: glm-5.1 --- .claude/commands/version-update.md | 170 ++++++++++++++++++++++++++ .claude/rules/version-update-guide.md | 40 ++++++ 2 files changed, 210 insertions(+) create mode 100644 .claude/commands/version-update.md create mode 100644 .claude/rules/version-update-guide.md diff --git a/.claude/commands/version-update.md b/.claude/commands/version-update.md new file mode 100644 index 0000000..0bef326 --- /dev/null +++ b/.claude/commands/version-update.md @@ -0,0 +1,170 @@ +--- +description: Update dftd3-rs bindings to support a new simple-dftd3 version +argument-hint: (e.g., v1.4.0) +--- + +You are performing a version update of the dftd3-rs bindings to support simple-dftd3 **$ARGUMENTS**. + +Read the CLAUDE.local.md file to get `DFTD3_REPO_PATH`, then follow the procedure below. Use `DFTD3_REPO_PATH` as the path to the upstream simple-dftd3 repository throughout. + +## Step 0: Gather upstream changes + +Determine the previous version by checking the last entry in the `api_versions` list in `dftd3/scripts/generate_ffi.py`. Then diff the upstream repo: + +```bash +cd $DFTD3_REPO_PATH +git fetch --tags +git checkout $ARGUMENTS +git diff v..$ARGUMENTS -- include/s-dftd3.h # header changes (CRITICAL) +git diff v..$ARGUMENTS -- python/dftd3/interface.py # Python wrapper changes +git diff v..$ARGUMENTS -- assets/parameters.toml # parameter DB changes +git diff v..$ARGUMENTS -- src/ --stat # Fortran source changes +``` + +From the header diff, identify: +- New `SDFTD3_API_SUFFIX__V_X_Y` macro (defines the version tag) +- New function declarations (tagged with version suffix) +- New opaque types (e.g., `dftd3_gcp`) +- Changed function signatures (rare) + +**Report your findings to the user before proceeding.** Summarize what's new/changed so they can confirm scope. + +## Step 1: Update generate_ffi.py + +File: `dftd3/scripts/generate_ffi.py` + +1. Add the new version to `api_versions` list (follow existing pattern): + ```python + ("V_X_Y", "api-vX_Y"), # new entry + ``` +2. Update the docstring inside `generate_static_ffi()` to list the new version. + +## Step 2: Regenerate FFI bindings + +```bash +cd dftd3/scripts && python generate_ffi.py +``` + +This auto-copies the header, runs bindgen, generates `ffi_static.rs` and all `ffi_dynamic/` files. + +**Verify** the new function appears: +- In `ffi_static.rs` with `#[cfg(feature = "api-vX_Y")]` +- In `ffi_dynamic/dyload_struct.rs`, `dyload_initializer.rs`, `dyload_compatible.rs` + +## Step 3: Update Cargo.toml features + +File: `dftd3/Cargo.toml` + +Add the new feature extending the previous one: +```toml +api-vX_Y = ["api-v"] +``` + +Rules: +- Must extend the **previous** feature (cumulative chain) +- If the version introduces a new capability category (like `gcp` for v1_3), create a separate feature and depend on it +- Consider whether to update `default` + +## Step 4: Update interface.rs (safe wrappers) + +This is the most manual step. For each new C function, add a safe Rust wrapper following these patterns: + +### Pattern A: New method on existing struct + +Add both infallible and fallible (`_f`) versions, gated by the new feature: +```rust +#[cfg(feature = "api-vX_Y")] +pub fn new_method(&self, /* args */) -> ReturnType { + self.new_method_f(/* args */).unwrap() +} + +#[cfg(feature = "api-vX_Y")] +pub fn new_method_f(&self, /* args */) -> Result { + let mut error = DFTD3Error::new(); + unsafe { ffi::dftd3_new_method(error.get_c_ptr(), /* args */) }; + match error.check() { + true => Err(error), + false => Ok(/* result */), + } +} +``` + +### Pattern B: New damping type + +1. Add a new `DFTD3XyzDampingParam` struct with `#[derive(Builder, Debug, Clone, Deserialize, Serialize)]` +2. Add `new_xyz_damping_f`/`new_xyz_damping` and `load_xyz_damping_f`/`load_xyz_damping` on `DFTD3Param` +3. Implement `DFTD3ParamAPI` for the new struct +4. Add `impl_load_param_api!` and `impl_damping_param_builder!` macro invocations +5. Update `dftd3_load_param`/`dftd3_load_param_f` match arms with `#[cfg]` gating + +### Pattern C: New opaque type + +1. Create a new `interface_xyz.rs` file +2. Add module in `lib.rs` gated by appropriate feature +3. Update `prelude` +4. Follow the `interface_gcp.rs` pattern + +## Step 5: Update parameters module (if parameters changed) + +If `assets/parameters.toml` changed upstream: +1. Copy: `cp $DFTD3_REPO_PATH/assets/parameters.toml dftd3/src/parameters.toml` +2. Update `parameters.rs`: + - Add variant to `D3MethodParams` and `D3DefaultParams` + - Add to `DFTD3DampingParamEnum` + - Update `convert_to_damping_param`, `get_variant_entry`, `get_variant_entry_for_defaults` + - Add `#[cfg(feature = "...")]` gates + +## Step 6: Update parsing module (if new damping variants) + +File: `dftd3/src/parsing.rs` + +1. Update `valid_fields_for_version()` with new variant's valid fields +2. Add `#[cfg(feature = "...")]` gating +3. Add `#[cfg(not(feature = "..."))]` error arm + +## Step 7: Update lib.rs (if new modules) + +If you added new interface files: +```rust +#[cfg(feature = "api-vX_Y")] +pub mod interface_xyz; +``` +And update `prelude`. + +## Step 8: Update examples + +Check upstream Python examples for new functionality. Add corresponding Rust examples. + +## Step 9: Update dftd3-src (if build system changed) + +```bash +git diff v..$ARGUMENTS -- CMakeLists.txt meson.build +``` +Update `dftd3-src/external_deps/` and `build.rs` if needed. + +## Step 10: Update CI + +Check if CI needs: newer s-dftd3 version, updated test matrix, new test cases. + +## Step 11: Test + +```bash +cargo build --features api-vX_Y +cargo test --all-features +cargo test --features api-vX_Y +cargo doc --all-features --no-deps +``` + +## Step 12: Update documentation + +- Update `CHANGELOG.md` +- Update `readme.md` if API surface changed +- Update feature docstring in `generate_ffi.py` + +--- + +**Important invariants** (from project rules — always follow these): +- Never manually edit `ffi_static.rs` or `ffi_dynamic/` files +- Naming: `dftd3_` prefix for functions, `DFTD3` prefix for structs +- Fallible variants use `_f` suffix returning `Result<_, DFTD3Error>` +- Feature flags are cumulative: each version extends the previous diff --git a/.claude/rules/version-update-guide.md b/.claude/rules/version-update-guide.md new file mode 100644 index 0000000..fe0fb63 --- /dev/null +++ b/.claude/rules/version-update-guide.md @@ -0,0 +1,40 @@ +# Version Update Invariants + +These are constant rules that always apply when working on version updates. For the step-by-step procedure, use the `/version-update ` command. + +## FFI generation rules + +- **Never manually edit** `ffi_static.rs` or any file in `ffi_dynamic/`. They are auto-generated by `dftd3/scripts/generate_ffi.py`. +- The only way to change FFI bindings is to update the header (`s-dftd3.h`) and re-run `generate_ffi.py`. +- `generate_ffi.py` reads the `SDFTD3_API_SUFFIX__V_X_Y` macros from the header to determine which feature gate each function belongs to. + +## Feature flag rules + +- Feature flags are **cumulative**: `api-v0_2` → `api-v0_3` → `api-v0_4` → `api-v0_5` → `api-v1_3` → ... Each version extends the previous. +- A new version feature must always depend on the immediately preceding version feature. +- If a version introduces a distinct capability (like `gcp`), create a separate feature and have the version feature depend on it. +- Static FFI respects feature flags (`#[cfg(feature = "api-vX_Y")]`). Dynamic loading ignores them — all functions are available at runtime. + +## Naming conventions + +- Functions exposed to users: `dftd3_` prefix (e.g., `dftd3_load_param`) +- Structs exposed to users: `DFTD3` prefix (e.g., `DFTD3Model`, `DFTD3RationalDampingParam`) +- Fallible variants: add `_f` suffix, returning `Result<_, DFTD3Error>` (e.g., `get_dispersion_f`) +- Infallible variants: unwrap the `_f` version (e.g., `get_dispersion`) + +## Safe wrapper patterns + +- Every new FFI function must get a safe Rust wrapper in the appropriate `interface*.rs` file. +- Each wrapper follows the error-check pattern: create `DFTD3Error`, call unsafe FFI, check `error.check()`. +- New opaque C types (e.g., `dftd3_gcp`) get their own `interface_*.rs` file, a new module in `lib.rs`, and a prelude re-export. + +## Version history reference + +| Version | Tag suffix | New functions | New types | Feature gate | +|---------|-----------|---------------|-----------|-------------| +| v0.2 | `V_0_2` | Core: version, error, structure, model, dispersion, delete | `dftd3_error`, `dftd3_structure`, `dftd3_model`, `dftd3_param` | `api-v0_2` | +| v0.3 | `V_0_3` | (Reserved) | — | `api-v0_3` | +| v0.4 | `V_0_4` | Damping constructors: zero, rational, mzero, mrational (new + load) | — | `api-v0_4` | +| v0.5 | `V_0_5` | Optimized power damping (new + load), pairwise dispersion, realspace cutoff | — | `api-v0_5` | +| v1.3 | `V_1_3` | CSO damping (new + load), GCP (load, cutoff, delete, counterpoise) | `dftd3_gcp` | `api-v1_3` (+`gcp`) | +| v1.4 | `V_1_4` | Smooth realspace cutoff | — | `api-v1_4` | From 0b3833b2f3b50fca4e0e3b8bc3e9bc4e4330fdab Mon Sep 17 00:00:00 2001 From: ajz34 Date: Thu, 7 May 2026 18:56:25 +0800 Subject: [PATCH 3/5] update rule of header handling --- .claude/CLAUDE.md | 2 + dftd3/scripts/generate_ffi.py | 193 ---------------------------------- 2 files changed, 2 insertions(+), 193 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a8d7c69..657a8fb 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -33,3 +33,5 @@ Important files for FFI and wrapper development: ## Header handling We use bindgen (python script at `scripts/generate_bindings.py`) to generate Rust bindings for the C header files. **Not modify the generated files directly**. + +Exception is `ffi_dynamic/mod.rs`. This file can be manually modified. diff --git a/dftd3/scripts/generate_ffi.py b/dftd3/scripts/generate_ffi.py index 4ef9f78..e9ffe17 100644 --- a/dftd3/scripts/generate_ffi.py +++ b/dftd3/scripts/generate_ffi.py @@ -336,199 +336,6 @@ def dyload_main(token): } -DYLOAD_MOD_TEMPLATE = """//! FFI module for dftd3 (dynamic loading). -//! -//! This module provides dynamic loading support. - -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(clippy::missing_safety_doc)] -#![allow(clippy::type_complexity)] -#![allow(clippy::too_many_arguments)] - -pub const MOD_NAME: &str = module_path!(); -pub const LIB_NAME: &str = "DFTD3"; -pub const LIB_NAME_SHOW: &str = "s-dftd3"; -pub const LIB_NAME_LINK: &str = "s-dftd3"; - -#[cfg(feature = "dynamic_loading")] -mod dynamic_loading_specific { - use super::*; - use libloading::Library; - use std::fmt::Debug; - use std::sync::OnceLock; - - use std::env::consts::{DLL_PREFIX, DLL_SUFFIX}; - - /// Detect Python interpreter path and return the corresponding lib directory. - /// Uses OnceLock pattern for lazy initialization. - static PYTHON_LIB_PATH: OnceLock> = OnceLock::new(); - - fn detect_python_lib_path() -> Option { - PYTHON_LIB_PATH.get_or_init(|| { - // 1. Check explicit environment variable first - if let Ok(python_path) = std::env::var("DFTD3_PYTHON_PATH") { - if let Some(lib_path) = extract_lib_from_python_bin(&python_path) { - return Some(lib_path); - } - } - - // 2. Try to find python in PATH - if let Ok(paths) = std::env::var("PATH") { - for path in paths.split(":") { - for python_name in ["python3", "python"] { - let python_bin = format!("{path}/{python_name}"); - if std::path::Path::new(&python_bin).exists() { - if let Some(lib_path) = extract_lib_from_python_bin(&python_bin) { - return Some(lib_path); - } - } - } - } - } - - None - }).clone() - } - - fn extract_lib_from_python_bin(python_bin: &str) -> Option { - // If python is at /path/to/bin/python, library should be at /path/to/lib/ - let bin_path = std::path::Path::new(python_bin); - if let Some(parent) = bin_path.parent() { - if let Some(base) = parent.parent() { - let lib_path = base.join("lib"); - if lib_path.exists() { - return Some(lib_path.to_string_lossy().to_string()); - } - } - } - None - } - - fn get_lib_candidates() -> Vec { - let mut candidates = vec![]; - - // User-defined candidates via environment variables - for env_var in [format!("DFTD3_DYLOAD_{LIB_NAME}").as_str(), "DFTD3_DYLOAD"] { - if let Ok(path) = std::env::var(env_var) { - candidates.extend(path.split(":").map(|s| s.to_string())); - } - } - - // LD_LIBRARY_PATH style discovery - for env_var in ["LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"] { - if let Ok(paths) = std::env::var(env_var) { - for path in paths.split(":") { - candidates.push(format!("{path}/{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}")); - } - } - } - - // Python interpreter path discovery (cached) - if let Some(lib_path) = detect_python_lib_path() { - candidates.push(format!("{lib_path}/{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}")); - } - - // Standard system candidates - candidates.extend(vec![ - format!("{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}"), - format!("{DLL_PREFIX}dftd3{DLL_SUFFIX}"), - format!("/usr/lib/{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}"), - format!("/usr/local/lib/{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}"), - format!("/lib/{DLL_PREFIX}{LIB_NAME_LINK}{DLL_SUFFIX}"), - ]); - candidates - } - - fn check_lib_loaded(lib: &DyLoadLib) -> bool { - lib.dftd3_get_version.is_some() - } - - fn panic_no_lib_found(candidates: &[S], err_msg: &str) -> ! { - panic!( - r#" -This happens in module `{MOD_NAME}`. -Unable to dynamically load the {LIB_NAME_SHOW} (`{LIB_NAME_LINK}`) shared library. -Candidates: {candidates:#?} - -Please check: -- If dynamic-loading is not desired, disable the `dynamic_loading` feature in Cargo.toml. -- Use environment variable `DFTD3_DYLOAD_{LIB_NAME}` or `DFTD3_DYLOAD` to specify the library path. -- If `lib{LIB_NAME_LINK}.so` is installed on your system. -- If `LD_LIBRARY_PATH` is set correctly. -- Python interpreter path discovery: if Python is at `/path/bin/python`, - the library is expected at `/path/lib/libs-dftd3.so`. - -Error message(s): -{err_msg} -"# - ) - } - - fn panic_condition_not_met(candidates: &[S]) -> ! { - panic!( - r#" -This happens in module `{MOD_NAME}`. -Library loaded but condition not met: `dftd3_get_version` not found. -Found libraries: {candidates:#?} - -Please check that the loaded library is a valid s-dftd3 library. -"# - ) - } - - pub unsafe fn dyload_lib() -> &'static DyLoadLib { - static LIB: OnceLock = OnceLock::new(); - - LIB.get_or_init(|| { - let candidates = get_lib_candidates(); - let (mut libraries, mut libraries_path) = (vec![], vec![]); - let mut err_msg = String::new(); - for candidate in &candidates { - match Library::new(candidate) { - Ok(l) => { - libraries.push(l); - libraries_path.push(candidate.to_string()); - }, - Err(e) => err_msg.push_str(&format!("Failed to load `{candidate}`: {e}\n")), - } - } - let lib = DyLoadLib::new(libraries, libraries_path); - if lib.__libraries.is_empty() { - panic_no_lib_found(&candidates, &err_msg); - } - if !check_lib_loaded(&lib) { - panic_condition_not_met(&lib.__libraries_path); - } - lib - }) - } -} - -#[cfg(feature = "dynamic_loading")] -pub use dynamic_loading_specific::*; - -/* #region general configuration */ - -pub(crate) mod ffi_base; -pub use ffi_base::*; - -#[cfg(feature = "dynamic_loading")] -pub(crate) mod dyload_compatible; -#[cfg(feature = "dynamic_loading")] -pub(crate) mod dyload_initializer; -#[cfg(feature = "dynamic_loading")] -pub(crate) mod dyload_struct; - -#[cfg(feature = "dynamic_loading")] -pub use dyload_compatible::*; -#[cfg(feature = "dynamic_loading")] -pub use dyload_struct::*; - -/* #endregion */ -""" - - # ## Main execution def main(): From 3ff50c42bb126e538c11f384c7158764c0ac06c7 Mon Sep 17 00:00:00 2001 From: ajz34 Date: Thu, 7 May 2026 19:27:35 +0800 Subject: [PATCH 4/5] Add api-v1_4 feature for smooth realspace cutoff Support simple-dftd3 v1.4.0 which adds dftd3_set_model_realspace_cutoff_smooth (width2, width3 smoothing parameters). Update CI workflows, FFI bindings, safe wrappers, and version-update guide. Co-Authored-By: Claude Code Co-Authored-By: glm-5.1 --- .claude/commands/version-update.md | 8 +++- .claude/rules/version-update-guide.md | 2 +- .github/workflows/test-dftd3.yml | 8 ++-- dftd3/Cargo.toml | 1 + dftd3/examples/test_interface.rs | 12 ++++++ dftd3/header/s-dftd3.h | 13 +++++- dftd3/scripts/generate_ffi.py | 5 +-- dftd3/src/ffi_dynamic/dyload_compatible.rs | 14 +++++++ dftd3/src/ffi_dynamic/dyload_initializer.rs | 5 +++ dftd3/src/ffi_dynamic/dyload_struct.rs | 11 +++++ dftd3/src/ffi_static.rs | 12 ++++++ dftd3/src/interface.rs | 45 +++++++++++++++++++++ 12 files changed, 125 insertions(+), 11 deletions(-) diff --git a/.claude/commands/version-update.md b/.claude/commands/version-update.md index 0bef326..c9ff9ac 100644 --- a/.claude/commands/version-update.md +++ b/.claude/commands/version-update.md @@ -63,7 +63,7 @@ api-vX_Y = ["api-v"] Rules: - Must extend the **previous** feature (cumulative chain) - If the version introduces a new capability category (like `gcp` for v1_3), create a separate feature and depend on it -- Consider whether to update `default` +- Do **not** update the `default` feature — leave it for manual editing ## Step 4: Update interface.rs (safe wrappers) @@ -144,7 +144,11 @@ Update `dftd3-src/external_deps/` and `build.rs` if needed. ## Step 10: Update CI -Check if CI needs: newer s-dftd3 version, updated test matrix, new test cases. +Update `.github/workflows/test-dftd3.yml` to use the new feature version: +- Replace `api-v` with `api-vX_Y` in all `cargo test` commands +- This applies to both the `test-static-linking` and `test-dynamic-loading` jobs + +Also check if other CI files need updates: newer conda-forge package version, updated test matrix, new test cases. ## Step 11: Test diff --git a/.claude/rules/version-update-guide.md b/.claude/rules/version-update-guide.md index fe0fb63..6d315af 100644 --- a/.claude/rules/version-update-guide.md +++ b/.claude/rules/version-update-guide.md @@ -10,7 +10,7 @@ These are constant rules that always apply when working on version updates. For ## Feature flag rules -- Feature flags are **cumulative**: `api-v0_2` → `api-v0_3` → `api-v0_4` → `api-v0_5` → `api-v1_3` → ... Each version extends the previous. +- Feature flags are **cumulative**: `api-v0_2` → `api-v0_3` → `api-v0_4` → `api-v0_5` → `api-v1_3` → `api-v1_4` → ... Each version extends the previous. - A new version feature must always depend on the immediately preceding version feature. - If a version introduces a distinct capability (like `gcp`), create a separate feature and have the version feature depend on it. - Static FFI respects feature flags (`#[cfg(feature = "api-vX_Y")]`). Dynamic loading ignores them — all functions are available at runtime. diff --git a/.github/workflows/test-dftd3.yml b/.github/workflows/test-dftd3.yml index 7af12da..294f461 100644 --- a/.github/workflows/test-dftd3.yml +++ b/.github/workflows/test-dftd3.yml @@ -19,8 +19,8 @@ jobs: ls /usr/share/miniconda/lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/share/miniconda/lib export DFTD3_DEV=1 - cargo test --no-default-features --features="api-v1_3" -- --nocapture - cargo test --no-default-features --features="api-v1_3" --examples -- --nocapture + cargo test --no-default-features --features="api-v1_4" -- --nocapture + cargo test --no-default-features --features="api-v1_4" --examples -- --nocapture test-dynamic-loading: runs-on: ubuntu-latest @@ -34,5 +34,5 @@ jobs: ls /usr/share/miniconda/lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/share/miniconda/lib export DFTD3_DEV=1 - cargo test --features="api-v1_3" -- --nocapture - cargo test --features="api-v1_3" --examples -- --nocapture + cargo test --features="api-v1_4" -- --nocapture + cargo test --features="api-v1_4" --examples -- --nocapture diff --git a/dftd3/Cargo.toml b/dftd3/Cargo.toml index 794dc58..d3642cb 100644 --- a/dftd3/Cargo.toml +++ b/dftd3/Cargo.toml @@ -40,6 +40,7 @@ api-v0_3 = ["api-v0_2"] api-v0_4 = ["api-v0_3"] api-v0_5 = ["api-v0_4"] api-v1_3 = ["api-v0_5", "gcp"] +api-v1_4 = ["api-v1_3"] # JSON input support for parsing module json = ["dep:serde_json"] diff --git a/dftd3/examples/test_interface.rs b/dftd3/examples/test_interface.rs index a6d1112..3a54132 100644 --- a/dftd3/examples/test_interface.rs +++ b/dftd3/examples/test_interface.rs @@ -254,6 +254,18 @@ fn test_b97d_d3_op(model: DFTD3Model, #[case] atm: bool, #[case] expected: f64) assert_abs_diff_eq!(res.energy, expected, epsilon = 1e-8); } +#[cfg(feature = "api-v1_4")] +#[rstest] +fn test_smooth_realspace_cutoff(model: DFTD3Model) { + let param = DFTD3RationalDampingParam::load_param("pbe0", true); + let ref_energy = model.get_dispersion(¶m, false).energy; + + model.set_realspace_cutoff_smooth(8.0, 8.0, 40.0, 4.0, 4.0); + let res_energy = model.get_dispersion(¶m, false).energy; + + assert!((res_energy - ref_energy).abs() > 1e-8); +} + // GCP tests #[rstest] #[cfg(feature = "gcp")] diff --git a/dftd3/header/s-dftd3.h b/dftd3/header/s-dftd3.h index b77baba..9b670e1 100644 --- a/dftd3/header/s-dftd3.h +++ b/dftd3/header/s-dftd3.h @@ -30,6 +30,7 @@ #define SDFTD3_API_SUFFIX__V_0_4 #define SDFTD3_API_SUFFIX__V_0_5 #define SDFTD3_API_SUFFIX__V_1_3 +#define SDFTD3_API_SUFFIX__V_1_4 /// Error handle class typedef struct _dftd3_error* dftd3_error; @@ -131,6 +132,16 @@ dftd3_set_model_realspace_cutoff(dftd3_error /* error */, double /* disp3 */, double /* cn */) SDFTD3_API_SUFFIX__V_0_5; +/// Set realspace cutoffs with smoothing widths (quantities in Bohr) +SDFTD3_API_ENTRY void SDFTD3_API_CALL +dftd3_set_model_realspace_cutoff_smooth(dftd3_error /* error */, + dftd3_model /* model */, + double /* disp2 */, + double /* disp3 */, + double /* cn */, + double /* width2 */, + double /* width3 */) SDFTD3_API_SUFFIX__V_1_4; + /// Delete dispersion model SDFTD3_API_ENTRY void SDFTD3_API_CALL dftd3_delete_model(dftd3_model* /* disp */) SDFTD3_API_SUFFIX__V_0_2; @@ -298,4 +309,4 @@ dftd3_get_counterpoise(dftd3_error /* error */, dftd3_gcp /* gcp */, double* /* energy */, double* /* gradient[n][3] */, - double* /* sigma[3][3] */) SDFTD3_API_SUFFIX__V_1_3; \ No newline at end of file + double* /* sigma[3][3] */) SDFTD3_API_SUFFIX__V_1_3; diff --git a/dftd3/scripts/generate_ffi.py b/dftd3/scripts/generate_ffi.py index e9ffe17..0a760d0 100644 --- a/dftd3/scripts/generate_ffi.py +++ b/dftd3/scripts/generate_ffi.py @@ -44,6 +44,7 @@ ("V_0_4", "api-v0_4"), ("V_0_5", "api-v0_5"), ("V_1_3", "api-v1_3"), + ("V_1_4", "api-v1_4"), ] # Default API version (used when no features are specified) @@ -142,6 +143,7 @@ def generate_static_ffi(token, version_map): //! - `api-v0_4`: Extends api-v0_3, adds damping parameter functions //! - `api-v0_5`: Extends api-v0_4, adds optimized power damping and pairwise dispersion //! - `api-v1_3`: Full API, adds CSO damping and GCP functions +//! - `api-v1_4`: Extends api-v1_3, adds smooth realspace cutoff //! //! Features are cumulative: enabling `api-v0_5` also enables all functions from //! earlier versions (api-v0_2, api-v0_3, api-v0_4). @@ -393,9 +395,6 @@ def main(): with open(f"{path_out}/src/ffi_dynamic/dyload_compatible.rs", "w") as f: f.write(dyload_files["dyload_compatible"]) - with open(f"{path_out}/src/ffi_dynamic/mod.rs", "w") as f: - f.write(DYLOAD_MOD_TEMPLATE) - # Run cargo fmt os.chdir(path_out) subprocess.run(["cargo", "fmt"]) diff --git a/dftd3/src/ffi_dynamic/dyload_compatible.rs b/dftd3/src/ffi_dynamic/dyload_compatible.rs index f45aff4..e75d3e8 100644 --- a/dftd3/src/ffi_dynamic/dyload_compatible.rs +++ b/dftd3/src/ffi_dynamic/dyload_compatible.rs @@ -66,6 +66,20 @@ pub unsafe fn dftd3_set_model_realspace_cutoff( dyload_lib().dftd3_set_model_realspace_cutoff.unwrap()(arg1, arg2, arg3, arg4, arg5) } +pub unsafe fn dftd3_set_model_realspace_cutoff_smooth( + arg1: dftd3_error, + arg2: dftd3_model, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, +) { + dyload_lib().dftd3_set_model_realspace_cutoff_smooth.unwrap()( + arg1, arg2, arg3, arg4, arg5, arg6, arg7, + ) +} + pub unsafe fn dftd3_delete_model(arg1: *mut dftd3_model) { dyload_lib().dftd3_delete_model.unwrap()(arg1) } diff --git a/dftd3/src/ffi_dynamic/dyload_initializer.rs b/dftd3/src/ffi_dynamic/dyload_initializer.rs index 7142930..383ef4d 100644 --- a/dftd3/src/ffi_dynamic/dyload_initializer.rs +++ b/dftd3/src/ffi_dynamic/dyload_initializer.rs @@ -29,6 +29,11 @@ impl DyLoadLib { b"dftd3_set_model_realspace_cutoff\0", ) .map(|sym| *sym), + dftd3_set_model_realspace_cutoff_smooth: get_symbol( + &libs, + b"dftd3_set_model_realspace_cutoff_smooth\0", + ) + .map(|sym| *sym), dftd3_delete_model: get_symbol(&libs, b"dftd3_delete_model\0").map(|sym| *sym), dftd3_new_zero_damping: get_symbol(&libs, b"dftd3_new_zero_damping\0").map(|sym| *sym), dftd3_load_zero_damping: get_symbol(&libs, b"dftd3_load_zero_damping\0") diff --git a/dftd3/src/ffi_dynamic/dyload_struct.rs b/dftd3/src/ffi_dynamic/dyload_struct.rs index 179b8b5..4a12d18 100644 --- a/dftd3/src/ffi_dynamic/dyload_struct.rs +++ b/dftd3/src/ffi_dynamic/dyload_struct.rs @@ -43,6 +43,17 @@ pub struct DyLoadLib { pub dftd3_set_model_realspace_cutoff: Option< unsafe extern "C" fn(arg1: dftd3_error, arg2: dftd3_model, arg3: f64, arg4: f64, arg5: f64), >, + pub dftd3_set_model_realspace_cutoff_smooth: Option< + unsafe extern "C" fn( + arg1: dftd3_error, + arg2: dftd3_model, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + ), + >, pub dftd3_delete_model: Option, pub dftd3_new_zero_damping: Option< unsafe extern "C" fn( diff --git a/dftd3/src/ffi_static.rs b/dftd3/src/ffi_static.rs index c5b66c5..acb0d2b 100644 --- a/dftd3/src/ffi_static.rs +++ b/dftd3/src/ffi_static.rs @@ -10,6 +10,7 @@ //! - `api-v0_5`: Extends api-v0_4, adds optimized power damping and pairwise //! dispersion //! - `api-v1_3`: Full API, adds CSO damping and GCP functions +//! - `api-v1_4`: Extends api-v1_3, adds smooth realspace cutoff //! //! Features are cumulative: enabling `api-v0_5` also enables all functions from //! earlier versions (api-v0_2, api-v0_3, api-v0_4). @@ -104,6 +105,17 @@ unsafe extern "C" { arg4: f64, arg5: f64, ); + #[cfg(feature = "api-v1_4")] + #[doc = " Set realspace cutoffs with smoothing widths (quantities in Bohr)"] + pub fn dftd3_set_model_realspace_cutoff_smooth( + arg1: dftd3_error, + arg2: dftd3_model, + arg3: f64, + arg4: f64, + arg5: f64, + arg6: f64, + arg7: f64, + ); #[cfg(feature = "api-v0_2")] #[doc = " Delete dispersion model"] pub fn dftd3_delete_model(arg1: *mut dftd3_model); diff --git a/dftd3/src/interface.rs b/dftd3/src/interface.rs index 66a5db2..e452229 100644 --- a/dftd3/src/interface.rs +++ b/dftd3/src/interface.rs @@ -1284,6 +1284,51 @@ impl DFTD3Model { } } + #[cfg(feature = "api-v1_4")] + /// Set realspace cutoffs with smoothing widths (in Bohr). + pub fn set_realspace_cutoff_smooth( + &self, + disp2: f64, + disp3: f64, + cn: f64, + width2: f64, + width3: f64, + ) { + self.set_realspace_cutoff_smooth_f(disp2, disp3, cn, width2, width3).unwrap() + } + + #[cfg(feature = "api-v1_4")] + /// Set realspace cutoffs with smoothing widths (in Bohr, failable). + /// + /// # See also + /// + /// [`DFTD3Model::set_realspace_cutoff_smooth`] + pub fn set_realspace_cutoff_smooth_f( + &self, + disp2: f64, + disp3: f64, + cn: f64, + width2: f64, + width3: f64, + ) -> Result<(), DFTD3Error> { + let mut error = DFTD3Error::new(); + unsafe { + ffi::dftd3_set_model_realspace_cutoff_smooth( + error.get_c_ptr(), + self.ptr, + disp2, + disp3, + cn, + width2, + width3, + ) + }; + match error.check() { + true => Err(error), + false => Ok(()), + } + } + /// Create new D3 dispersion model from structure (failable). /// /// # See also From 596fba3daca68f95f6aac8b4adb179b33f1b7b82 Mon Sep 17 00:00:00 2001 From: ajz34 Date: Thu, 7 May 2026 19:30:50 +0800 Subject: [PATCH 5/5] update readme --- readme.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 445ddb3..d2e5f20 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ This project contains simple-dftd3 FFI bindings, wrapper and build-from-source. -Current binding of simple-dftd3: [![v1.3.1](https://img.shields.io/github/v/release/dftd3/simple-dftd3?filter=v1.3.1)](https://github.com/dftd3/simple-dftd3/releases/v1.3.1) +Current binding of simple-dftd3: [![v1.4.0](https://img.shields.io/github/v/release/dftd3/simple-dftd3?filter=v1.4.0)](https://github.com/dftd3/simple-dftd3/releases/v1.4.0) Source code of simple-dftd3 is available on [github](https://github.com/dftd3/simple-dftd3). @@ -16,7 +16,7 @@ This crate contains simple-dftd3 FFI bindings and wrapper. |--|--| | Crate | [![Crate](https://img.shields.io/crates/v/dftd3.svg)](https://crates.io/crates/dftd3) | | API Document | [![API Documentation](https://docs.rs/dftd3/badge.svg)](https://docs.rs/dftd3) | -| FFI Binding | [![v1.3.1](https://img.shields.io/github/v/release/dftd3/simple-dftd3?filter=v1.3.1)](https://github.com/dftd3/simple-dftd3/releases/v1.3.1) | +| FFI Binding | [![v1.4.0](https://img.shields.io/github/v/release/dftd3/simple-dftd3?filter=v1.4.0)](https://github.com/dftd3/simple-dftd3/releases/v1.4.0) | ### Dynamic loading guide @@ -104,8 +104,8 @@ Default cargo features of `dftd3` are: - **`dynamic_loading`**: This will enable dynamic loading of `s-dftd3` library, which can be more flexible for users who do not want to perform static linking. Please place `libs-dftd3.so` in `LD_LIBRARY_PATH` (for macos, place `libs-dftd3.dylib` in `DYLD_LIBRARY_PATH`), and function symbols will be loaded at runtime. Other cargo features of `dftd3` are: -- **`gcp`**: Support of geometric counterpoise correction. Please note that this is not available in latest stable release of simple-dftd3 (at the time writing this readme, is v1.3.1). Unless you build simple-dftd3 from git repository, you may not use this feature (especially installed simple-dftd3 from conda or similar). -- **`api-v1_3`**: Corresponding to the original simple-dftd3 [v1.3](https://github.com/dftd3/simple-dftd3/releases/tag/v1.3.1). This will additionally enable versions `cso` and cargo feature `gcp`. +- **`gcp`**: Support of geometric counterpoise correction. Please note that this is not available in latest stable release of simple-dftd3 (at the time writing this readme, is v1.4.0). Unless you build simple-dftd3 from git repository, you may not use this feature (especially installed simple-dftd3 from conda or similar). +- **`api-v1_3`**: Corresponding to the original simple-dftd3 [v1.3](https://github.com/dftd3/simple-dftd3/releases/tag/v1.4.0). This will additionally enable versions `cso` and cargo feature `gcp`. - **`json`**: This will enable JSON parsing for DFTD3 parameters. Note that toml parsing is builtin, and json is an optional feature. ## Installation guide and Crate `dftd3-src` @@ -166,7 +166,7 @@ If you have not compiled `s-dftd3` library, you may try out cargo feature `build CMake configurable variables (can be defined as environment variables): - `DFTD3_SRC`: git repository source directory or URL; - - `DFTD3_VER`: version of DFT-D3 (default v1.3.1); + - `DFTD3_VER`: version of DFT-D3 (default v1.4.0); - **`static`**: This will link static libary instead of dynamic one. Please note that 1. static linking may require additional Fortran and OpenMP linking, which is not provided in this crate; 2. staticly linking LGPL-3.0 license may require your project to be GPL-3.0.