Skip to content

fix(pair-tab): validate uniform distance grid to avoid silently wrong potentials - #5908

Open
hanaol wants to merge 10 commits into
deepmodeling:masterfrom
hanaol:fix/pairtab-nonuniform-grid
Open

fix(pair-tab): validate uniform distance grid to avoid silently wrong potentials#5908
hanaol wants to merge 10 commits into
deepmodeling:masterfrom
hanaol:fix/pairtab-nonuniform-grid

Conversation

@hanaol

@hanaol hanaol commented Jul 26, 2026

Copy link
Copy Markdown

Summary

PairTab (used for use_srtab and ZBL tab_file short-range tables) assumes the distance grid is uniformly spaced, but never validates it. A non-uniform table is silently accepted and produces incorrect potentials with no error or warning.

Problem

PairTab.reinit() infers a single stride from the first two rows only:

self.hh = self.vdata[1][0] - self.vdata[0][0]

self.hh is then baked into the spline coefficients and stored in tab_info, and the C++ inference kernel (source/lib/src/pair_tab.cc) maps distance to table index purely as idx = floor((r - rmin) / hh) . No per-row distances are kept, so a non-uniform grid is impossible to represent.

Also, the uniform-grid requirement is documented (doc/model/pairtab.md: the table is defined "on an evenly discretized grid"), but nothing enforced it.

Fix

Validate in reinit() that every distance interval matches the inferred stride, and raise a ValueError otherwise. The check runs before any spline is built, so a bad table is rejected up front instead of mis-indexed later.

Tests

Added TestPairTabGridSpacing in
source/tests/common/dpmodel/test_pairtab_preprocess.py:

test_non_uniform_grid — a table with a changing stride now raises ValueError.
test_uniform_grid — a uniformly spaced table is still accepted.

Summary by CodeRabbit

  • Bug Fixes
    • Pairwise tables now validate the loaded distance grid before updating internal state.
    • Non-uniform, duplicate, or descending grids now fail fast with a clear ValueError advising to regrid.
    • Evenly spaced uniform grids still initialize with the expected spacing.
  • Tests
    • Added unit coverage for non-uniform, duplicate, descending, and uniform distance grids (including small-grid edge cases).
    • Added verification that a failed reinitialization preserves the previously serialized state.

… potentials

Signed-off-by: hanaol <ho0950@princeton.edu>
@dosubot dosubot Bot added the bug label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

PairTab.reinit validates radial-grid ordering and uniform spacing before updating instance state. Tests cover invalid grids, failed reinitialization state preservation, fine-grid handling, and successful spacing inference.

Changes

Pair table grid validation

Layer / File(s) Summary
Grid validation and state commit
deepmd/utils/pair_tab.py
PairTab.reinit rejects duplicate, descending, or uneven distance grids before computing and storing derived table state.
Grid validation tests
source/tests/common/dpmodel/test_pairtab_preprocess.py
Tests cover invalid and fine grids, preservation of serialized state after failed reinitialization, and successful hh inference for 0.01 and 1e-9 spacing.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: validating pair-tab distance grids to prevent incorrect potentials.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepmd/utils/pair_tab.py`:
- Around line 64-72: Update the radial-grid validation in the pair-table
initialization around vdata and dx to explicitly reject any non-positive
distance interval before or alongside the existing uniform-spacing check.
Preserve the current ValueError behavior for uneven grids, ensure duplicate and
descending grids cannot proceed to hh-dependent division or padding, and add
regression coverage for both cases.
- Around line 64-72: Make reinit() validate the newly loaded table using local
variables before modifying self.vdata, self.rmin, self.rmax, or self.hh. Move
the non-uniform-grid check and any related validation ahead of committing
instance state, then update all instance metadata and tab_info/tab_data together
only after validation succeeds, preserving the existing object unchanged when
validation raises.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 259347aa-00a8-4fe1-b26d-3bf7c9ddeb92

📥 Commits

Reviewing files that changed from the base of the PR and between cc908a8 and 492e2e5.

📒 Files selected for processing (2)
  • deepmd/utils/pair_tab.py
  • source/tests/common/dpmodel/test_pairtab_preprocess.py

Comment thread deepmd/utils/pair_tab.py Outdated
Address review feedback on the pairwise table validation:

- A constant zero or negative distance stride passed the uniform-spacing
  check, leaving hh == 0 (division by zero) or hh < 0 with rmin > rmax in
  the padding and extrapolation arithmetic. Require a strictly increasing
  grid before checking uniformity.
- reinit() assigned vdata/rmin/rmax/hh before validating, so a failed
  reinit of a live PairTab left the new metadata next to the stale
  tab_info/tab_data. Validate locals first and commit instance state only
  once all checks pass.

Add regression tests for duplicate and descending grids, and assert that a
failed reinit leaves the serialized table unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: hanaol <ho0950@princeton.edu>
@hanaol

hanaol commented Jul 28, 2026

Copy link
Copy Markdown
Author

Thanks for the review — both findings addressed in f87eeb7, with two intentional deviations from the suggested diffs:

  1. Strictly increasing grid. Implemented as a separate check with its own error message rather than folding np.all(dx > 0) into the existing allclose condition. A descending grid with a constant negative step is evenly spaced, so reusing the "not evenly spaced" text would have misdescribed the failure. Duplicate rows and descending tables now raise "not strictly increasing"; genuinely uneven grids keep the original message.

  2. Atomic reinit(). Validation now runs against locals and instance state is committed only once every check passes. I also moved the ntypes assert inside the atomic region — otherwise a table with a bad column count would still have left the object half-updated.

Regression tests added for the duplicate-distance and descending-grid cases, plus one asserting that a failed reinit() on a live PairTab leaves the serialized table byte-for-byte unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@source/tests/common/dpmodel/test_pairtab_preprocess.py`:
- Around line 334-354: Update the test’s serialized-state snapshot by importing
copy and deep-copying the result of PairTab.serialize() before reinit(), so
expected arrays cannot alias live state. Replace np.testing.assert_allclose
comparisons for vdata, tab_info, and tab_data with exact array comparisons while
preserving the existing scalar checks; run the focused pytest case, ruff check
., and ruff format .
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cf3e420-5b67-4128-a098-f7b7e161b1da

📥 Commits

Reviewing files that changed from the base of the PR and between 492e2e5 and f87eeb7.

📒 Files selected for processing (2)
  • deepmd/utils/pair_tab.py
  • source/tests/common/dpmodel/test_pairtab_preprocess.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • deepmd/utils/pair_tab.py

Comment thread source/tests/common/dpmodel/test_pairtab_preprocess.py Outdated
hanaol and others added 3 commits July 28, 2026 15:38
serialize() returns references to the live vdata/tab_info/tab_data arrays,
so deep-copy the expected snapshot rather than aliasing it, and compare the
arrays exactly since a failed reinit must leave them untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: hanaol <ho0950@princeton.edu>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: hanaol <ho0950@princeton.edu>

@njzjz-bot njzjz-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The uniform-grid check has a scale-dependent false-negative: atol=1e-8 permits clearly non-uniform grids whose spacing is below that absolute tolerance. Please make the tolerance relative to hh (for example, use atol=0 while retaining the relative tolerance) and add a regression test with a small non-uniform step.

Reviewed by OpenClaw 2026.6.11.

Comment thread deepmd/utils/pair_tab.py Outdated
hanaol and others added 2 commits July 29, 2026 11:07
atol=1e-8 dominated the comparison for sub-nanometre grids, so intervals
differing by an order of magnitude still compared equal and the table was
encoded with the smaller stride. Drop the absolute term and rely on rtol,
which is scale-invariant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: hanaol <ho0950@princeton.edu>
@hanaol
hanaol requested a review from njzjz-bot July 29, 2026 20:05
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.44%. Comparing base (4f827cc) to head (3527afe).
⚠️ Report is 52 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5908      +/-   ##
==========================================
- Coverage   79.21%   77.44%   -1.78%     
==========================================
  Files        1069     1105      +36     
  Lines      124070   130988    +6918     
  Branches     4522     4771     +249     
==========================================
+ Hits        98278   101438    +3160     
- Misses      24171    27895    +3724     
- Partials     1621     1655      +34     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@njzjz
njzjz requested review from wanghan-iapcm and removed request for njzjz-bot August 1, 2026 13:37

@wanghan-iapcm wanghan-iapcm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnosis here is exactly right and I want to say so first. hh is inferred from the first two rows, baked into the spline coefficients and tab_info, and then both evaluators index purely on it -- source/lib/src/pair_tab.cc does uu = (rr - rmin) * hi; int idx = uu; with no validation whatsoever -- so a non-uniform table cannot be represented and was being silently mis-indexed. Enforcing what doc/model/pairtab.md has always documented ("on an evenly discretized grid") is the right call, and reinit() is the right place: it is the only path that reads a file, and the checks run before _check_table_upper_boundary(), so they only ever inspect the user's raw table and never the padding rows the code appends itself. The "compute into locals, commit afterwards" refactor is a good instinct too.

I checked the regression coverage by running your new tests against unfixed code rather than reasoning about it: 5 of the 7 fail pre-fix, and the only two that pass are the intended accept-cases; all 11 pass after. test_duplicate_distances is a nice one -- pre-fix it does not raise at all, it just emits RuntimeWarning: divide by zero encountered in scalar divide from rcut_idx = int(np.ceil(self.rcut / self.hh - ...)) with hh == 0 and carries on, which is the silent corruption this PR is about. I also confirmed nothing in-tree breaks: examples/water/zbl/H2O_tab_potential.txt and the zbl_tab_potential fixtures sit ~1e-13 relative against a 1e-5 tolerance, examples/water/d3/dftd3.txt ~1e-14, and every mocked grid in the existing tests is uniform to ~1e-16. And since deepmd/tf/utils/pair_tab.py is a pure alias re-export, the fix correctly lands in the one shared reader that tf, tf2, pt, pt_expt, dpmodel and jax all go through.

One thing I would like resolved before this merges, inline below: the uniformity check measures a different quantity than the one the consumers actually depend on, and that makes it both too strict and too lax in ways that are easy to demonstrate.

A few smaller notes I am recording rather than asking you to act on. The Table file section of doc/model/pairtab.md -- the part a user reads when authoring a table -- does not state the even-grid requirement anywhere (only the Theory footnote does), so someone who hits the new ValueError has nothing to look up; a sentence there would pay for itself. deserialize() assigns vdata/hh/tab_info/tab_data directly and never routes through reinit(), so a model serialized from a bad table before this fix still loads silently -- I think ingest-time is the right gate and the serialized vdata is post-extrapolation anyway, so this is a note, not a request. The atomicity guarantee is narrower than test_failed_reinit_keeps_state suggests: after self.vdata = vdata the method still runs _check_table_upper_boundary() and _make_data(), and a CubicSpline failure there would leave new vdata beside stale tab_data. And reinit()'s docstring gained two raise paths but no numpydoc Raises section (there is one to copy in deepmd/utils/version.py).

Comment thread deepmd/utils/pair_tab.py Outdated
hanaol and others added 2 commits August 2, 2026 12:08
Fixes pylint no-explicit-dtype failure flagged by pre-commit.ci.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hanaol

hanaol commented Aug 31, 2026

Copy link
Copy Markdown
Author

@wanghan-iapcm @njzjz-bot Follow-up commits address the node-position issue:

  • 040569ed fix(pair-tab): use a scale-aware tolerance for the grid spacing check
  • 2aa67040 fix(pair-tab): validate grid by absolute node position, not interval
  • 3527afe7 fix(pair-tab): add explicit dtype to np.arange in grid validation

Summary of what changed in deepmd/utils/pair_tab.py:

  • Validation now checks each row's distance against rmin + hh_ref * i (with hh_ref = (rmax - rmin) / (n - 1)) rather than bounding each dx[i] against hh, so it measures the quantity the consumers (pair_tab.cc's index arithmetic, _make_data()'s spline) actually rely on.
  • hh_ref is used only for validation and kept separate from self.hh (still vdata[1][0] - vdata[0][0]), so tab_info and existing model behavior are unchanged.
  • Tolerance is 1e-2 * abs(hh_ref), and the error message now names the first offending row.
  • This also resolves the earlier atol=1e-8 false-acceptance case ([0, 1e-10, 1.1e-9, 2.1e-9, 3.1e-9]), still covered by test_non_uniform_fine_grid.

Added test_uniform_grid_rounded_text_precision, which writes a real text file via np.savetxt(..., fmt="%.6f") and reads it back with np.loadtxt, so the printed-precision rounding case is now exercised through the actual text-parsing path rather than mocked arrays.

All 12 tests in test_pairtab_preprocess.py pass. Would appreciate a re-review when you get a chance.

@hanaol
hanaol requested a review from wanghan-iapcm September 1, 2026 17:16

@wanghan-iapcm wanghan-iapcm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The position-based check is the right one, and the regression test proves it: I ran the whole test file against b00aeb3 and against this head, and test_uniform_grid_rounded_text_precision fails on the old interval check with exactly the %.6f case from my earlier table, then passes here with the other eleven, including the fine-grid rejection from the bot review.

One thing still stands between the check and the PR's stated goal, and it is a consequence of advice I gave last round, so I am retracting that advice rather than just pointing at the gap. Inline below.

Two smaller things, neither blocking:

  • test_uniform_fine_grid asserts assertAlmostEqual(tab.hh, 1e-9) with the default places=7, which passes for any hh below 5e-8, zero included. The rounded-text test compares to places=6 on a value of 0.006. Neither would notice self.hh being wrong in the way described inline. np.testing.assert_allclose(tab.hh, expected, rtol=1e-6) in both would make them real, and once self.hh is hh_ref the rounded-text one can assert against 6.0 / 999 exactly.
  • doc/model/pairtab.md still only says the file must be readable by numpy.loadtxt. This PR turns a silent assumption into a hard ValueError at model construction, so the "Table file" section should say that the first column has to be strictly increasing and uniform, and mention the one-percent tolerance, so a user who hits the error can see what is expected without reading the source.

For the record, things checked and fine: every in-tree table (examples/water/zbl, examples/water/d3, the two test fixtures) passes the new check with deviations around 1e-12 of a cell, so nothing shipped is rejected; deserialize bypasses reinit, so already-serialized models are untouched; and n == 2 is handled.

Comment thread deepmd/utils/pair_tab.py
self.vdata = vdata
self.rmin = rmin
self.rmax = rmax
self.hh = hh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line that keeps the fix from closing the bug it targets, and the reason it is written this way is my comment last round, which I am withdrawing.

The check above proves every node sits within one percent of a cell of rmin + i * hh_ref. But what gets stored, and what every consumer then uses, is the first interval: tab_info[1] feeds uu = (rr - rmin) / hh in pair_tab.cc and the dpmodel/pt _pair_tabulated_inter, _make_data scales derivatives by it, rcut_idx and the padding linspace in _check_table_upper_boundary are computed from it. Nothing bounds how far this value may sit from hh_ref; the check only bounds node positions, so the first interval can be off by nearly a full percent while the table passes. A relative error in hh accumulates linearly in the index.

I reproduced it against this head: linspace(0, 1, 1001) with only row 1 moved by 9.9e-6, i.e. 0.99 of a cell, is accepted, self.hh comes out as 0.0010099, and at r = 0.995 the consumer's index arithmetic lands on cell 985 while the node is 995. Ten cells off, silently, which is precisely the failure mode in the PR title. The neighbouring comment says the consumers' rmin + i * hh is "what must stay accurate", and it is, but it is hh_ref that was made accurate, not hh.

The fix is one line: self.hh = hh_ref, and drop the first-interval hh. I asked last round to keep them separate to avoid changing tab_info for existing models; having worked through it, that caution was misplaced. For a table with a round stride the two values are bit-identical, and for a printed table hh_ref averages out the rounding of row 1 and is the better estimate, so the change to existing models is at the printing-precision level and in the right direction. The error message already reports hh_ref as the step, so this also makes what the object stores agree with what it tells the user.

A test that would have caught it: a uniform grid with a single node perturbed inside the tolerance, asserting tab.hh against the true step with rtol rather than assertAlmostEqual.

@njzjz
njzjz requested a review from njzjz-bot September 2, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants