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
37 changes: 37 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Agent instruction of DFTD4 Rust FFI and Wrapper

## Notes to human developers

You should also create a file `CLAUDE.local.md` to place local resources:
- `DFTD4_REPO_PATH`: local of original dftd4 repository. The source code can help you understand how dftd4 works.

## DFTD4 original library

General rules
- This repository should live at `DFTD4_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 dftd4.

Important files for FFI and wrapper development:
- `include/dftd4.h`: the headers. Note that these files are also copied to this project under `dftd4/header` folder.
- `python/dftd4`: the python wrapper of dftd4. We should at least implement all major features of the certain wrapper:
- `interface.py`, corresponding to this project `dftd4/src/interface.rs`.
- `parameters.py`, corresponding to this project `dftd4/src/parameters.rs`.
- Make sure the functionalities are tested. We use `dftd4/examples/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 `dftd4/src/parameters.toml` in this project.

## The additional feature in this crate

- We support toml parsing of DFTD4 parameters. The related code is at `/dftd4/src/parsing.rs`. The related test is at `dftd4/examples/test_parsing.rs`.
- We support dynamic loading of the DFTD4 library.
- We use tags such as `api-v3_5` to reflect the API version of DFTD4 we are using.

## Naming convention

- For functions and structs that will be exposed to users, add prefix `dftd4_` for general functions, and `DFTD4` for structs.
- If some function is to be fallible, we can add suffix `_f` (`fn <func>_f -> Result<_, DFTD4Error>`).

## Header handling

We use bindgen (python script at `scripts/generate_ffi.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.
172 changes: 172 additions & 0 deletions .claude/commands/version-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
description: Update dftd4-rs bindings to support a new dftd4 version
argument-hint: <version> (e.g., v4.1.0)
---

You are performing a version update of the dftd4-rs bindings to support dftd4 **$ARGUMENTS**.

Read the CLAUDE.local.md file to get `DFTD4_REPO_PATH`, then follow the procedure below. Use `DFTD4_REPO_PATH` as the path to the upstream dftd4 repository throughout.

## Step 0: Gather upstream changes

Determine the previous version by checking the last entry in the `api_versions` list in `dftd4/scripts/generate_ffi.py`. Then diff the upstream repo:

```bash
cd $DFTD4_REPO_PATH
git fetch --tags
git checkout $ARGUMENTS
git diff v<PREVIOUS>..$ARGUMENTS -- include/dftd4.h # header changes (CRITICAL)
git diff v<PREVIOUS>..$ARGUMENTS -- python/dftd4/interface.py # Python wrapper changes
git diff v<PREVIOUS>..$ARGUMENTS -- assets/parameters.toml # parameter DB changes
git diff v<PREVIOUS>..$ARGUMENTS -- src/ --stat # Fortran source changes
```

From the header diff, identify:
- New `DFTD4_API_SUFFIX__V_X_Y` macro (defines the version tag)
- New function declarations (tagged with version suffix)
- New opaque types (e.g., `dftd4_model` subtype)
- 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: `dftd4/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 dftd4/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: `dftd4/Cargo.toml`

Add the new feature extending the previous one:
```toml
api-vX_Y = ["api-v<PREVIOUS>"]
```

Rules:
- Must extend the **previous** feature (cumulative chain)
- Do **not** update the `default` feature — leave it for manual editing

## 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<ReturnType, DFTD4Error> {
let mut error = DFTD4Error::new();
unsafe { ffi::dftd4_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 `DFTD4XyzDampingParam` 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 `DFTD4Param`
3. Implement `DFTD4ParamAPI` for the new struct
4. Add `impl_damping_param_builder!` macro invocation
5. Update `dftd4_load_param`/`dftd4_load_param_f` match arms with `#[cfg]` gating

### Pattern C: New model variant

If a new model constructor is added (e.g., `dftd4_new_d4s_model`):
1. Add `new_variant`/`new_variant_f` and `custom_variant`/`custom_variant_f` on `DFTD4Model`
2. Gate with `#[cfg(feature = "api-vX_Y")]`

## Step 5: Update parameters module (if parameters changed)

If `assets/parameters.toml` changed upstream:
1. Copy: `cp $DFTD4_REPO_PATH/assets/parameters.toml dftd4/src/parameters.toml`
2. Update `parameters.rs`:
- Add variant to `D4MethodParams` and `D4DefaultParams`
- Add to `DFTD4DampingParamEnum`
- 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: `dftd4/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 in `dftd4/examples/`.

## Step 9: Update dftd4-src (if build system changed)

```bash
git diff v<PREVIOUS>..$ARGUMENTS -- CMakeLists.txt meson.build
```
Update `dftd4-src/external_deps/` and `build.rs` if needed.

## Step 10: Update CI

Update `.github/workflows/test-dftd4.yml` to use the new feature version:
- Replace `api-v<PREVIOUS>` 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

```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 (except `ffi_dynamic/mod.rs`)
- Naming: `dftd4_` prefix for functions, `DFTD4` prefix for structs
- Fallible variants use `_f` suffix returning `Result<_, DFTD4Error>`
- Feature flags are cumulative: each version extends the previous
8 changes: 5 additions & 3 deletions .claude/rules/agent-and-maintaince.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
- Multi-co-author format (note no extra newline between co-authors):
```
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: glm-5 <service@zhipuai.cn>
Co-authored-by: glm-5.1 <service@zhipuai.cn>
```
- 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
- 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.
- 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.1`, which can be inferred by Claude Code's `/model` property.

- 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.
41 changes: 41 additions & 0 deletions .claude/rules/version-update-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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 <version>` command.

## FFI generation rules

- **Never manually edit** `ffi_static.rs` or any file in `ffi_dynamic/` (except `ffi_dynamic/mod.rs`). They are auto-generated by `dftd4/scripts/generate_ffi.py`.
- The only way to change FFI bindings is to update the header (`dftd4.h`) and re-run `generate_ffi.py`.
- `generate_ffi.py` reads the `DFTD4_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-v3_0` → `api-v3_1` → `api-v3_2` → `api-v3_3` → `api-v3_4` → `api-v3_5` → `api-v4_0` → ... Each version extends the previous.
- A new version feature must always depend on the immediately preceding version feature.
- 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: `dftd4_` prefix (e.g., `dftd4_load_param`)
- Structs exposed to users: `DFTD4` prefix (e.g., `DFTD4Model`, `DFTD4RationalDampingParam`)
- Fallible variants: add `_f` suffix, returning `Result<_, DFTD4Error>` (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 `dftd4/src/interface.rs`.
- Each wrapper follows the error-check pattern: create `DFTD4Error`, call unsafe FFI, check `error.check()`.
- New opaque C types 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 |
|---------|-----------|---------------|-----------|-------------|
| v3.0 | `V_3_0` | Core: version, error, structure, model (D4), rational damping, dispersion, delete | `dftd4_error`, `dftd4_structure`, `dftd4_model`, `dftd4_param` | `api-v3_0` |
| v3.1 | `V_3_1` | Custom D4 model, properties | — | `api-v3_1` |
| v3.2 | `V_3_2` | Pairwise dispersion | — | `api-v3_2` |
| v3.3 | `V_3_3` | (Reserved) | — | `api-v3_3` |
| v3.4 | `V_3_4` | (Reserved) | — | `api-v3_4` |
| v3.5 | `V_3_5` | Numerical hessian | — | `api-v3_5` |
| v4.0 | `V_4_0` | D4S model (new + custom) | — | `api-v4_0` |
| v4.2 | `V_4_2` | Realspace cutoff setters | — | `api-v4_2` |
4 changes: 2 additions & 2 deletions .github/workflows/test-dftd4.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
ls /usr/share/miniconda/lib
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/share/miniconda/lib
export DFTD4_DEV=1
cargo test --no-default-features --features="api-v4_0" -- --nocapture
cargo test --no-default-features --features="api-v4_0" --examples -- --nocapture
cargo test --no-default-features --features="api-v4_2" -- --nocapture
cargo test --no-default-features --features="api-v4_2" --examples -- --nocapture

test-dynamic-loading:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions dftd4-src/external_deps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ endif()
if(DFTD4_VER)
message(STATUS "User specified version of library (dftd4): ${DFTD4_VER}")
else()
set(DFTD4_VER v4.1.0)
set(DFTD4_VER v4.2.0)
message(STATUS "Download version of library (dftd4): ${DFTD4_VER}")
endif()

ExternalProject_Add(dftd4
GIT_REPOSITORY ${DFTD4_SRC}
GIT_TAG v4.1.0
GIT_TAG v4.2.0
PREFIX ${PROJECT_BINARY_DIR}/deps
CMAKE_ARGS
-GNinja
Expand Down
1 change: 1 addition & 0 deletions dftd4/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ api-v3_3 = ["api-v3_2"]
api-v3_4 = ["api-v3_3"]
api-v3_5 = ["api-v3_4"]
api-v4_0 = ["api-v3_5"]
api-v4_2 = ["api-v4_0"]

[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
Expand Down
32 changes: 32 additions & 0 deletions dftd4/examples/test_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,34 @@ fn test_error_model() {
assert!(param.is_err_and(|e| e.to_string().contains("not known")));
}

#[cfg(feature = "api-v4_2")]
fn test_smooth_realspace_cutoff() {
// Check smooth cutoff support changes dispersion energy
let numbers = vec![6, 6, 6, 1, 1, 1, 1];
#[rustfmt::skip]
let positions = vec![
0.00000000000000, 0.00000000000000, -1.79755622305860,
0.00000000000000, 0.00000000000000, 0.95338756106749,
0.00000000000000, 0.00000000000000, 3.22281255790261,
-0.96412815539807, -1.66991895015711, -2.53624948351102,
-0.96412815539807, 1.66991895015711, -2.53624948351102,
1.92825631079613, 0.00000000000000, -2.53624948351102,
0.00000000000000, 0.00000000000000, 5.23010455462158,
];

let param = DFTD4Param::load_rational_damping("pbe", false);

// Get reference energy with default cutoffs
let mut model = DFTD4Model::new(&numbers, &positions, None, None, None);
let ref_energy = model.get_dispersion(&param, false).energy;

// Set tight cutoffs with smoothing, should change the energy
model.set_realspace_cutoff_smooth(4.0, 4.0, 30.0, 2.0, 2.0);
let res_energy = model.get_dispersion(&param, false).energy;

assert_ne!(ref_energy, res_energy);
}

#[test]
fn test() {
test_rational_damping_noargs();
Expand All @@ -437,6 +465,8 @@ fn test() {
#[cfg(feature = "api-v3_1")]
test_properties();
test_error_model();
#[cfg(feature = "api-v4_2")]
test_smooth_realspace_cutoff();
}

fn main() {
Expand All @@ -453,4 +483,6 @@ fn main() {
#[cfg(feature = "api-v3_1")]
test_properties();
test_error_model();
#[cfg(feature = "api-v4_2")]
test_smooth_realspace_cutoff();
}
19 changes: 19 additions & 0 deletions dftd4/header/dftd4.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#define DFTD4_API_SUFFIX__V_3_4
#define DFTD4_API_SUFFIX__V_3_5
#define DFTD4_API_SUFFIX__V_4_0
#define DFTD4_API_SUFFIX__V_4_2

/// Error handle class
typedef struct _dftd4_error* dftd4_error;
Expand Down Expand Up @@ -145,6 +146,24 @@ dftd4_custom_d4s_model(dftd4_error /* error */,
DFTD4_API_ENTRY void DFTD4_API_CALL
dftd4_delete_model(dftd4_model* /* disp */) DFTD4_API_SUFFIX__V_3_0;

/// Set realspace cutoffs (quantities in Bohr)
DFTD4_API_ENTRY void DFTD4_API_CALL
dftd4_set_model_realspace_cutoff(dftd4_error /* error */,
dftd4_model /* disp */,
double /* disp2 */,
double /* disp3 */,
double /* cn */) DFTD4_API_SUFFIX__V_4_2;

/// Set realspace cutoffs with smoothing widths (quantities in Bohr)
DFTD4_API_ENTRY void DFTD4_API_CALL
dftd4_set_model_realspace_cutoff_smooth(dftd4_error /* error */,
dftd4_model /* disp */,
double /* disp2 */,
double /* disp3 */,
double /* cn */,
double /* width2 */,
double /* width3 */) DFTD4_API_SUFFIX__V_4_2;

/*
* Damping parameter class
**/
Expand Down
Loading
Loading