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 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 <func>_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**.

Exception is `ffi_dynamic/mod.rs`. This file can be manually modified.
174 changes: 174 additions & 0 deletions .claude/commands/version-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
description: Update dftd3-rs bindings to support a new simple-dftd3 version
argument-hint: <version> (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<PREVIOUS>..$ARGUMENTS -- include/s-dftd3.h # header changes (CRITICAL)
git diff v<PREVIOUS>..$ARGUMENTS -- python/dftd3/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 `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<PREVIOUS>"]
```

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
- 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, DFTD3Error> {
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<PREVIOUS>..$ARGUMENTS -- CMakeLists.txt meson.build
```
Update `dftd3-src/external_deps/` and `build.rs` if needed.

## Step 10: Update CI

Update `.github/workflows/test-dftd3.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
- 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
8 changes: 4 additions & 4 deletions .claude/rules/agent-and-maintaince.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +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
- 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.
40 changes: 40 additions & 0 deletions .claude/rules/version-update-guide.md
Original file line number Diff line number Diff line change
@@ -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 <version>` 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` → `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.

## 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` |
8 changes: 4 additions & 4 deletions .github/workflows/test-dftd3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ tarpaulin-report.html
.idea
book
*.npy
*.local.*

# bindgen
blas_bindgen.h
Expand Down
1 change: 1 addition & 0 deletions dftd3/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
12 changes: 12 additions & 0 deletions dftd3/examples/test_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&param, false).energy;

model.set_realspace_cutoff_smooth(8.0, 8.0, 40.0, 4.0, 4.0);
let res_energy = model.get_dispersion(&param, false).energy;

assert!((res_energy - ref_energy).abs() > 1e-8);
}

// GCP tests
#[rstest]
#[cfg(feature = "gcp")]
Expand Down
13 changes: 12 additions & 1 deletion dftd3/header/s-dftd3.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
double* /* sigma[3][3] */) SDFTD3_API_SUFFIX__V_1_3;
Loading
Loading