Skip to content

fix(dpmodel): guard empty magnetic loss masks#5798

Merged
njzjz merged 2 commits into
deepmodeling:masterfrom
njzjz:codex/code-scan-5637
Jul 18, 2026
Merged

fix(dpmodel): guard empty magnetic loss masks#5798
njzjz merged 2 commits into
deepmodeling:masterfrom
njzjz:codex/code-scan-5637

Conversation

@njzjz

@njzjz njzjz commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

  • replace zero magnetic-mask denominators with Array API-safe nonzero denominators
  • make all-empty magnetic-force masks contribute finite zero instead of NaN for both MSE and MAE losses
  • preserve the existing display semantics when the magnetic-force label itself is absent
  • add backend-neutral NumPy and Torch Array API regression coverage

Root cause and fix

EnergySpinLoss masks non-magnetic atoms out of the force-magnitude residual, then divides its reductions by n_valid * 3. When a batch contains no magnetic atoms, both numerator and denominator are zero, producing NaN. Multiplying that value by a zero prefactor or a missing-label flag does not recover it because 0 * NaN is still NaN.

The implementation now constructs:

safe_n_valid = xp.where(n_valid > 0, n_valid, xp.ones_like(n_valid))

and uses it for every global magnetic-force MSE/MAE reduction. The masked numerator is already zero for an empty set, so the contribution becomes exactly zero. Guarding the denominator itself is important: array backends may evaluate both branches of a later where, and an unselected divide-by-zero can still produce invalid values or gradients. The expression is compatible with Array API namespaces and JAX tracing; non-empty batches retain the exact previous formula.

This PR intentionally does not change the native PyTorch loss or redefine its missing-label display metrics. When find_force_mag == 0, the total loss is finite zero while display_if_exist continues to report NaN for the unavailable magnetic metric.

Why existing tests missed this

Existing spin-loss mask tests always selected at least one magnetic atom in each batch. They covered partial masks and all-magnetic batches, but never the global zero denominator.

The new backend-neutral regression parameterizes:

  • mse and mae;
  • float32 and float64; and
  • find_force_mag present and absent.

It verifies a finite zero total loss in all eight combinations, zero magnetic metrics for a present-but-empty label, and the existing NaN display behavior for an absent label. A separate pt_expt test exercises the same shared dpmodel implementation through the Torch Array API namespace.

On the previous implementation, all eight backend-neutral cases fail with NaN. With the fix, the combined new and related targeted tests pass.

Validation

  • old backend-neutral regression: 8 failed with NaN as expected
  • fixed NumPy and pt_expt targeted tests: 20 passed
  • array_api_strict MSE/MAE empty-mask scenarios passed
  • JAX was not installed locally; the denominator expression was kept free of Python data-dependent branching for JIT compatibility
  • a broader consistent-loss run produced 35 passed / 45 skipped plus 2 unrelated pre-existing MAE parity failures because the shared editable native-PT install came from another worktree; neither failure exercises the empty-mask path
  • ruff format --check .
  • ruff check .
  • git diff --check

Closes #5637.

Coding agent: Codex
Codex version: codex-cli 0.144.1
Model: gpt-5.6-sol
Reasoning effort: xhigh

Summary by CodeRabbit

  • Bug Fixes

    • Fixed magnetic-force loss calculations when no magnetic atoms are present.
    • Prevented NaN or invalid loss values in both MSE and MAE modes.
    • Ensured magnetic-force metrics and overall loss report finite zero values for empty magnetic masks.
    • Aligned results across supported computational backends.
  • Tests

    • Added regression coverage across supported precisions and loss configurations.

Use Array API-safe nonzero denominators for magnetic-force loss reductions so an all-empty mask contributes finite zero instead of NaN. Add backend-neutral NumPy and Torch namespace regressions across loss functions, precisions, and label-presence flags.

Coding-Agent: Codex
Codex-Version: codex-cli 0.144.1
Model: gpt-5.6-sol
Reasoning-Effort: xhigh
Copilot AI review requested due to automatic review settings July 14, 2026 19:35

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@dosubot dosubot Bot added the bug label Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Failed to post review comments.

GitHub was unavailable or timed out while CodeRabbit was posting the review. Please request a new review later if the pull request still needs one. Use @coderabbitai full review to retry the review.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0ec66315-e15a-41e0-8159-7290e0f2886f

📥 Commits

Reviewing files that changed from the base of the PR and between 3122138 and 599d6da.

📒 Files selected for processing (5)
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
  • source/tests/pt_expt/loss/test_ener_spin.py
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Build wheels for cp310-manylinux_aarch64
  • GitHub Check: Build wheels for cp311-macosx_arm64
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Install linter and run ruff check . before committing changes or the CI will fail
Format code with ruff format . before committing changes or the CI will fail

Files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
**/tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Use pytest for testing single test cases like pytest source/tests/tf/test_dp_test.py::TestDPTestEner::test_1frame -v (8-13 seconds) instead of full test suite (60+ minutes)

Files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
🧠 Learnings (10)
📚 Learning: 2026-04-11T08:01:08.364Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5391
File: source/tests/pt_expt/infer/test_deep_eval.py:1667-1691
Timestamp: 2026-04-11T08:01:08.364Z
Learning: In deepmd-kit, `get_dp_atomic_model()` is a model-level API exposed on the full model hierarchy (e.g., `make_model` CM class, `FrozenModel`, `SpinModel`) and is not available on atomic models (e.g., `DPZBLLinearEnergyAtomicModel`, `DPAtomicModel`). When writing/validating tests for ZBL or other atomic-model behavior, do not call `get_dp_atomic_model()` on atomic model instances; instead, use `isinstance` checks (or equivalent type assertions) on the atomic model.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-05-01T13:46:12.190Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5428
File: source/tests/infer/gen_model_devi.py:144-147
Timestamp: 2026-05-01T13:46:12.190Z
Learning: Do not report or fail code review findings for Ruff `RUF059` (`unused-unpacked-variable`) in Python files. This rule is explicitly ignored via `tool.ruff.lint.ignore` in `pyproject.toml` (deepmodeling/deepmd-kit, referenced line 399), and since CI does not enforce it, unused variables created via tuple/unpack assignments (e.g., `e0, f0, v0, ae0, av0 = dp.eval(...)` where some of the targets are unused) should not be flagged for this reason.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-05-02T12:55:24.164Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5430
File: source/lmp/tests/test_lammps_dpa3_pt2.py:479-479
Timestamp: 2026-05-02T12:55:24.164Z
Learning: For this repository (deepmodeling/deepmd-kit), the Ruff configuration in pyproject.toml enables only specific rule codes and does not include the flake8-bandit ('S') rules. Therefore, when reviewing Python code, do not flag or require changes for Ruff findings with codes starting with 'S' (e.g., S603 subprocess calls), since those checks are not enabled by the project's configured lint rules.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-05-02T12:55:39.794Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5430
File: source/lmp/tests/test_lammps_spin_dpa3_pt2.py:176-176
Timestamp: 2026-05-02T12:55:39.794Z
Learning: In deepmodeling/deepmd-kit, the Ruff configuration in pyproject.toml does not include the `S` (flake8-bandit) rule set in `[tool.ruff.lint].select`, so any `S*` Bandit rules (e.g., `S603`) are not enforced by CI. During code review, do not flag `S*` issues and do not request adding/removing `# noqa: S*` (or any `# noqa` targeting `S*`) annotations, since they are effectively no-ops in this repository.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-05-28T23:48:28.388Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5474
File: deepmd/pt_expt/utils/comm.py:64-67
Timestamp: 2026-05-28T23:48:28.388Z
Learning: In this repository, Ruff’s configured lint rules in pyproject.toml do not include the `BLE` (flake8-blind-except) rule group, so `BLE*` rules like `BLE001` are not enforced by CI. During code review, do not flag `BLE*` issues and do not ask authors to add `# noqa: BLE001` (or any `# noqa` targeting `BLE*`), since those suppressions target rules the project does not currently run.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-06-11T16:43:05.483Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5515
File: deepmd/dpmodel/descriptor/dpa4.py:374-376
Timestamp: 2026-06-11T16:43:05.483Z
Learning: In this repository, Ruff rule E741 ("ambiguous variable name") is configured to be ignored in pyproject.toml (tool.ruff.lint.ignore). During code review, do not raise E741 findings or request renaming single-letter variables such as `l`, `O`, or `I` on the basis of E741, since CI does not enforce this rule.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-06-11T16:43:04.825Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5515
File: deepmd/dpmodel/descriptor/dpa4_nn/norm.py:215-217
Timestamp: 2026-06-11T16:43:04.825Z
Learning: In deepmodeling/deepmd-kit, Ruff rule E741 (“ambiguous variable name”) is explicitly ignored in pyproject.toml, so during code review you should not ask for renaming of single-letter or otherwise short variables (e.g., `l`, `O`, `I`) solely on the basis of E741.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-07-07T08:20:09.299Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5708
File: deepmd/infer/deep_eval.py:96-108
Timestamp: 2026-07-07T08:20:09.299Z
Learning: In this repo (deepmodeling/deepmd-kit), Ruff rule B027 (flake8-bugbear: “empty method in abstract base class without abstractmethod”) is intentionally not enabled via pyproject.toml [tool.ruff.lint].select. During code reviews, do not raise issues for B027 findings and do not suggest adding `# noqa: B027` suppressions—let existing Ruff/pre-commit CI configuration be the source of truth.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
  • deepmd/dpmodel/loss/ener_spin.py
  • deepmd/pt/loss/ener_spin.py
  • source/tests/common/dpmodel/test_loss_ener_spin.py
  • source/tests/consistent/loss/test_ener_spin.py
📚 Learning: 2026-06-13T01:31:17.863Z
Learnt from: wanghan-iapcm
Repo: deepmodeling/deepmd-kit PR: 5522
File: source/tests/pt_expt/descriptor/test_dpa4.py:72-73
Timestamp: 2026-06-13T01:31:17.863Z
Learning: In pt_expt tests, do not require an explicit `.to(self.device)` after calling `DescrptDPA4.deserialize` (or other pt_expt `deserialize` methods). The pt_expt backend deserialization path routes through `dpmodel_setattr` and materializes tensors/buffers with `torch.as_tensor(..., device=env.DEVICE)`, so the returned module is already on the correct device when the test uses `env.DEVICE` (e.g., `self.device = env.DEVICE`). Only flag missing `.to(...)` when the tensors/modules were not created by the pt_expt `deserialize` path or are otherwise moved/assigned to a different device.

Applied to files:

  • source/tests/pt_expt/loss/test_ener_spin.py
📚 Learning: 2026-01-22T02:59:02.650Z
Learnt from: ChiahsinChu
Repo: deepmodeling/deepmd-kit PR: 5138
File: deepmd/pt/infer/deep_eval.py:178-191
Timestamp: 2026-01-22T02:59:02.650Z
Learning: In the PyTorch backend (deepmd/pt), serialization of dictionaries that include torch.Tensor objects should use pickle.dumps/pickle.loads instead of JSON, as JSON cannot serialize tensors. Ensure this pattern is followed for similar serialization needs in this directory. Also consider documenting the rationale, add security notes (deserialization should only occur with trusted data), and explore safer or more explicit serialization approaches (e.g., saving tensors/state_dict via PyTorch utilities) where applicable. Add tests to cover round-trip integrity of serialized dictionaries.

Applied to files:

  • deepmd/pt/loss/ener_spin.py
🪛 Ruff (0.15.21)
source/tests/consistent/loss/test_ener_spin.py

[warning] 272-272: Function definition does not bind loop variable pt_predict

(B023)

📝 Walkthrough

Walkthrough

EnergySpinLoss now handles empty magnetic-atom masks without invalid reductions. Both dpmodel and PyTorch paths sanitize magnetic-force loss metrics, with regression tests covering loss modes, precisions, optional magnetic forces, and cross-backend consistency.

Changes

Empty magnetic-mask handling

Layer / File(s) Summary
Guard magnetic-force reductions
deepmd/dpmodel/loss/ener_spin.py, deepmd/pt/loss/ener_spin.py
Magnetic-force reductions use safe denominators and sanitized MSE/MAE values when no magnetic atoms are selected.
Validate empty-mask behavior
source/tests/common/dpmodel/test_loss_ener_spin.py, source/tests/consistent/loss/test_ener_spin.py, source/tests/pt_expt/loss/test_ener_spin.py
Regression tests verify finite zero losses, zero magnetic metrics, and matching dpmodel/PyTorch results across supported loss modes and precisions.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: wanghan-iapcm, iprozd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change: guarding empty magnetic masks in EnergySpinLoss.
Linked Issues check ✅ Passed The fix guards empty-mask magnetic-force denominators and adds regression tests covering empty magnetic masks and zero-valued metrics.
Out of Scope Changes check ✅ Passed All code changes stay focused on EnergySpinLoss fixes and regression coverage, with no unrelated functionality added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 78.31%. Comparing base (7d5ad38) to head (599d6da).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
deepmd/pt/loss/ener_spin.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5798      +/-   ##
==========================================
- Coverage   79.85%   78.31%   -1.54%     
==========================================
  Files        1022     1050      +28     
  Lines      117351   120601    +3250     
  Branches     4313     4354      +41     
==========================================
+ Hits        93706    94453     +747     
- Misses      22101    24586    +2485     
- Partials     1544     1562      +18     

☔ 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 iProzd and wanghan-iapcm July 14, 2026 23:03

@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.

Reviewed via the code-review skill. Correct and behavior-preserving: for a non-empty magnetic mask safe_n_valid==n_valid (loss unchanged), and for an empty mask the numerator is already 0 so the contribution is a finite 0; guarding the denominator (not wrapping the result in where) is the right approach for JAX, which evaluates both where branches. All three magnetic-force reductions are covered. The regressions genuinely fail pre-fix (verified: all 8 numpy and 4 torch cases fail on isfinite, since 0*NaN=NaN regardless of find_force_mag), across mse/mae, float32/64, numpy+torch, with correct display_if_exist semantics (0 for a present-but-empty label, NaN for an absent label).

One non-blocking cross-backend note: the separate torch.jit backend deepmd/pt/loss/ener_spin.py already guards the accumulated loss via torch.nan_to_num, but its displayed more_loss['rmse_fm']/['mae_fm'] are NOT guarded and still evaluate to NaN for an empty magnetic mask. After this merges, dpmodel/pt_expt will report 0.0 for those display metrics while pt reports NaN for the same logical case, and source/tests/consistent/loss/test_ener_spin.py never exercises n_magnetic=0, so the divergence is untested. Worth aligning pt's display metrics (and/or adding an n_magnetic=0 consistency case) in a follow-up. Separately -- and clearly out of scope here -- the same NaN class (a mask-count of 0 used as a divisor) is still open in the energy/virial 1/real_natoms reciprocals across ener.py/ener_spin.py/pt equivalents for all-ghost frames; that belongs to the broader loss-padding-mask work. LGTM for the magnetic-mask fix.

Normalize empty magnetic-force reductions before publishing PyTorch RMSE and MAE metrics, and add a cross-backend consistency regression against dpmodel.

Coding-Agent: Codex
Codex-Version: codex-cli 0.144.4
Model: gpt-5.6-sol
Reasoning-Effort: xhigh
@njzjz-bot

Copy link
Copy Markdown
Contributor

I addressed the non-blocking PyTorch consistency note in 599d6da9d.

The torch.jit loss now normalizes empty magnetic-force reductions before publishing rmse_fm and mae_fm, so a present-but-empty magnetic label reports 0.0, matching dpmodel/pt_expt instead of reporting NaN. I also added a direct cross-backend regression covering MSE RMSE, optional MSE MAE, and MAE loss metrics with n_magnetic=0.

The focused consistency test passed, including all three subcases; Ruff and formatting hooks also passed.

Coding agent: Codex
Codex version: codex-cli 0.144.4
Model: gpt-5.6-sol
Reasoning effort: xhigh

@njzjz
njzjz enabled auto-merge July 16, 2026 15:49
@njzjz
njzjz added this pull request to the merge queue Jul 16, 2026
@njzjz
njzjz removed this pull request from the merge queue due to the queue being cleared Jul 17, 2026
@njzjz
njzjz added this pull request to the merge queue Jul 17, 2026
Merged via the queue into deepmodeling:master with commit f7d8a53 Jul 18, 2026
57 checks passed
@njzjz
njzjz deleted the codex/code-scan-5637 branch July 18, 2026 02: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.

[Code scan] Guard EnergySpinLoss when batches contain no magnetic atoms

5 participants