fix(pair-tab): validate uniform distance grid to avoid silently wrong potentials - #5908
fix(pair-tab): validate uniform distance grid to avoid silently wrong potentials#5908hanaol wants to merge 10 commits into
Conversation
… potentials Signed-off-by: hanaol <ho0950@princeton.edu>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesPair table grid validation
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
deepmd/utils/pair_tab.pysource/tests/common/dpmodel/test_pairtab_preprocess.py
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>
|
Thanks for the review — both findings addressed in f87eeb7, with two intentional deviations from the suggested diffs:
Regression tests added for the duplicate-distance and descending-grid cases, plus one asserting that a failed |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
deepmd/utils/pair_tab.pysource/tests/common/dpmodel/test_pairtab_preprocess.py
🚧 Files skipped from review as they are similar to previous changes (1)
- deepmd/utils/pair_tab.py
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
left a comment
There was a problem hiding this comment.
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.
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>
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
wanghan-iapcm
left a comment
There was a problem hiding this comment.
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).
Fixes pylint no-explicit-dtype failure flagged by pre-commit.ci. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@wanghan-iapcm @njzjz-bot Follow-up commits address the node-position issue:
Summary of what changed in
Added All 12 tests in |
wanghan-iapcm
left a comment
There was a problem hiding this comment.
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_gridassertsassertAlmostEqual(tab.hh, 1e-9)with the defaultplaces=7, which passes for anyhhbelow 5e-8, zero included. The rounded-text test compares toplaces=6on a value of 0.006. Neither would noticeself.hhbeing wrong in the way described inline.np.testing.assert_allclose(tab.hh, expected, rtol=1e-6)in both would make them real, and onceself.hhishh_refthe rounded-text one can assert against6.0 / 999exactly.doc/model/pairtab.mdstill only says the file must be readable bynumpy.loadtxt. This PR turns a silent assumption into a hardValueErrorat 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.
| self.vdata = vdata | ||
| self.rmin = rmin | ||
| self.rmax = rmax | ||
| self.hh = hh |
There was a problem hiding this comment.
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.
Summary
PairTab(used foruse_srtaband ZBLtab_fileshort-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.hhis 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 asidx = 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 aValueErrorotherwise. The check runs before any spline is built, so a bad table is rejected up front instead of mis-indexed later.Tests
Added
TestPairTabGridSpacinginsource/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
ValueErroradvising to regrid.