Skip to content

feat(pt): add atomic_weight parameter to dipole model#2

Closed
ChiahsinChu wants to merge 5 commits into
develfrom
patch/v3.1.1/dipole_with_atomic_weight
Closed

feat(pt): add atomic_weight parameter to dipole model#2
ChiahsinChu wants to merge 5 commits into
develfrom
patch/v3.1.1/dipole_with_atomic_weight

Conversation

@ChiahsinChu

@ChiahsinChu ChiahsinChu commented Dec 11, 2025

Copy link
Copy Markdown
Owner
  • Add atomic_weight parameter to BaseAtomicModel.forward_common_atomic() to support scaling outputs with atomic weights
  • Update model interfaces in dpmodel and pt modules to pass atomic_weight parameter
  • Add tests for atomic_weight functionality in dipole model
  • This enables weighted dipole calculations where atomic contributions can be scaled differently

This change is part of the dipole_with_atomic_weight feature branch.

Summary by CodeRabbit

  • New Features

    • Optional per-atom "atomic_weight" input available across models, inference/eval, and training; when supplied, per-atom outputs are scaled by these weights with broadcasting and shape validation.
  • Tests

    • Added tests ensuring atomic-weight propagation, correct scaling of atomic outputs, and consistency across model paths.
  • Chores

    • Threaded atomic_weight through public APIs, eval, and training call paths; updated docstrings and input handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 11, 2025

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

An optional per-atom atomic_weight input was added and propagated through DP and PyTorch codepaths; when provided, atomic outputs are multiplied (broadcast over the last dimension) by the weights. Inference, training wrapper, dipole model, and tests now accept and forward the parameter.

Changes

Cohort / File(s) Change Summary
DPModel atomic model scaling
deepmd/dpmodel/atomic_model/base_atomic_model.py
Added optional atomic_weight to forward_common_atomic and call; outputs scaled by atomic_weight with shape validation and updated docstrings.
DPModel model interface propagation
deepmd/dpmodel/model/make_model.py
Threaded atomic_weight through model_call_from_call_lower, CM.call, CM.call_lower, CM.forward_common_atomic, and CM.input_type_cast; extended input/output tuples and type hints to carry atomic_weight.
PyTorch atomic model scaling
deepmd/pt/model/atomic_model/base_atomic_model.py
Added optional atomic_weight to BaseAtomicModel.forward_common_atomic and forward; atomic outputs multiplied by atomic_weight reshaped to [nf, nloc, -1]; docstrings updated.
PyTorch model interface propagation
deepmd/pt/model/model/make_model.py
Threaded atomic_weight through CM.forward_common, CM.forward_common_lower, CM.forward, and input_type_cast; passed to atomic model calls and included in returned tuples and plumbing.
PyTorch dipole model support
deepmd/pt/model/model/dipole_model.py
Added optional atomic_weight to DipoleModel.forward and DipoleModel.forward_lower; propagated into forward_common/forward_common_lower and prediction assembly paths.
Inference / evaluation
deepmd/pt/infer/deep_eval.py
DeepEval.eval, _eval_model, and _eval_model_spin accept optional atomic_weight; added validation/reshaping utility to normalize 1D/2D/3D inputs to (nframes, natoms, k); error on unsupported spin+atomic_weight combos; forward prepared atomic_weight to model.
Training wrapper
deepmd/pt/train/wrapper.py
ModelWrapper.forward gains optional atomic_weight and includes it in the public input dict used for inference and strips it before loss computation where appropriate.
Tests
source/tests/pt/model/test_dp_atomic_model.py
Added tests that pass random atomic_weight and assert per-atom energy outputs equal original energy multiplied by the weight (verifies broadcasting/weighting and cross-model consistency).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble through tensors late at night,
Tiny weights make each atom bright.
I hop and broadcast, soft and neat,
Scaling energies with gentle feet.
A twitch, a hop — the outputs right!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding atomic_weight parameter support to the dipole model across multiple layers (pt and dpmodel modules).
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02fa9d0 and 60ac1a5.

📒 Files selected for processing (1)
  • deepmd/pt/train/wrapper.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • deepmd/pt/train/wrapper.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: Test Python (1, 3.10)
  • GitHub Check: Test Python (4, 3.10)
  • GitHub Check: Test Python (3, 3.13)
  • GitHub Check: Test Python (2, 3.13)
  • GitHub Check: Test Python (5, 3.10)
  • GitHub Check: Test Python (4, 3.13)
  • GitHub Check: Test Python (2, 3.10)
  • GitHub Check: Test Python (6, 3.13)
  • GitHub Check: Test Python (6, 3.10)
  • GitHub Check: Test Python (3, 3.10)
  • GitHub Check: Test Python (1, 3.13)
  • GitHub Check: Test Python (5, 3.13)
  • GitHub Check: Test C++ (true, false, false, true)
  • GitHub Check: Test C++ (false, true, true, false)
  • GitHub Check: Test C++ (false, false, false, true)
  • GitHub Check: Analyze (c-cpp)
  • GitHub Check: Test C++ (true, true, true, false)
  • GitHub Check: Analyze (python)
  • GitHub Check: Build C++ (cuda120, cuda)

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
deepmd/pt/model/model/make_model.py (1)

131-203: Inconsistency: atomic_weight not passed through input_type_cast in PT version.

In the dpmodel version (deepmd/dpmodel/model/make_model.py), atomic_weight is passed through input_type_cast and gets dtype-casted along with other inputs. However, in this PT version, atomic_weight is passed directly without casting.

This inconsistency could cause dtype mismatches if atomic_weight is provided with a different precision than the model's global precision.

Consider adding atomic_weight to the input_type_cast method for consistency:

 def input_type_cast(
     self,
     coord: torch.Tensor,
     box: Optional[torch.Tensor] = None,
     fparam: Optional[torch.Tensor] = None,
     aparam: Optional[torch.Tensor] = None,
+    atomic_weight: Optional[torch.Tensor] = None,
 ) -> tuple[
     torch.Tensor,
     Optional[torch.Tensor],
     Optional[torch.Tensor],
     Optional[torch.Tensor],
+    Optional[torch.Tensor],
     str,
 ]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d774ede and cb2659e.

📒 Files selected for processing (6)
  • deepmd/dpmodel/atomic_model/base_atomic_model.py (5 hunks)
  • deepmd/dpmodel/model/make_model.py (11 hunks)
  • deepmd/pt/model/atomic_model/base_atomic_model.py (5 hunks)
  • deepmd/pt/model/model/dipole_model.py (4 hunks)
  • deepmd/pt/model/model/make_model.py (4 hunks)
  • source/tests/pt/model/test_dp_atomic_model.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
source/tests/pt/model/test_dp_atomic_model.py (3)
deepmd/dpmodel/atomic_model/base_atomic_model.py (1)
  • forward_common_atomic (152-235)
deepmd/dpmodel/model/make_model.py (1)
  • forward_common_atomic (352-378)
deepmd/pt/utils/utils.py (3)
  • to_numpy_array (224-224)
  • to_numpy_array (228-228)
  • to_numpy_array (231-247)
deepmd/pt/model/atomic_model/base_atomic_model.py (1)
deepmd/pt/model/network/network.py (1)
  • Tensor (36-37)
deepmd/dpmodel/model/make_model.py (2)
deepmd/pt/model/model/make_model.py (1)
  • input_type_cast (315-358)
deepmd/pd/model/model/make_model.py (1)
  • input_type_cast (306-349)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Build wheels for cp311-macosx_x86_64
  • GitHub Check: Build wheels for cp311-win_amd64
  • GitHub Check: Build wheels for cp311-win_amd64
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Test Python (1, 3.9)
  • GitHub Check: Analyze (c-cpp)
  • GitHub Check: Test Python (4, 3.9)
  • GitHub Check: Test Python (5, 3.9)
  • GitHub Check: Test Python (3, 3.12)
  • GitHub Check: Test Python (4, 3.12)
  • GitHub Check: Test Python (3, 3.9)
  • GitHub Check: Test Python (1, 3.12)
  • GitHub Check: Analyze (python)
  • GitHub Check: Build C++ (rocm, rocm)
  • GitHub Check: Build C++ (clang, clang)
  • GitHub Check: Build C library (2.14, >=2.5.0,<2.15, libdeepmd_c_cu11.tar.gz)
  • GitHub Check: Build C library (2.18, libdeepmd_c.tar.gz)
  • GitHub Check: Build C++ (cuda120, cuda)
  • GitHub Check: Build C++ (cpu, cpu)
  • GitHub Check: Build C++ (cuda, cuda)
🔇 Additional comments (14)
deepmd/pt/model/atomic_model/base_atomic_model.py (2)

282-285: LGTM on atomic_weight scaling logic.

The implementation correctly applies atomic_weight after the atom_mask, ensuring that virtual atoms remain zeroed. The scaling is properly applied before the "mask" key is added to ret_dict.


299-310: LGTM on forward method propagation.

The atomic_weight parameter is correctly added to the signature and propagated to forward_common_atomic.

deepmd/pt/model/model/dipole_model.py (2)

56-92: LGTM on forward method.

The atomic_weight parameter is correctly added and propagated to forward_common.


94-133: LGTM on forward_lower method.

The atomic_weight parameter is correctly propagated to forward_common_lower.

deepmd/dpmodel/atomic_model/base_atomic_model.py (2)

228-232: LGTM on atomic_weight scaling.

The implementation correctly applies atomic_weight after masking. The reshape with -1 allows broadcasting when the weight dimension matches the flattened output dimensions.


237-255: LGTM on call method.

The atomic_weight parameter is correctly added and propagated to forward_common_atomic.

source/tests/pt/model/test_dp_atomic_model.py (2)

76-82: LGTM on atomic_weight test in self-consistency.

Good test coverage validating that output energy is correctly scaled by atomic_weight.


111-118: LGTM on atomic_weight test in DP consistency.

Good cross-implementation validation ensuring NumPy and PyTorch versions produce consistent results when using atomic_weight.

deepmd/pt/model/model/make_model.py (2)

240-313: LGTM on forward_common_lower propagation.

The atomic_weight parameter is correctly propagated to atomic_model.forward_common_atomic.


622-639: Note: forward() does not include atomic_weight.

The base forward() method doesn't include the atomic_weight parameter, which means models using this base implementation won't expose atomic_weight through the standard PyTorch interface. This appears intentional since DipoleModel provides its own forward() override that includes atomic_weight.

deepmd/dpmodel/model/make_model.py (4)

225-283: LGTM on call method with atomic_weight integration.

The atomic_weight parameter is correctly integrated with input_type_cast and propagated to model_call_from_call_lower.


285-350: LGTM on call_lower method.

The atomic_weight parameter is correctly cast via input_type_cast and propagated to forward_common_atomic.


382-418: LGTM on input_type_cast extension.

The method properly handles atomic_weight dtype casting along with other inputs, maintaining consistency.


352-378: LGTM on forward_common_atomic.

The atomic_weight parameter is correctly propagated to the underlying atomic model.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
deepmd/pt/infer/deep_eval.py (1)

306-371: atomic_weight is undocumented in eval() and ignored when spin is provided

  • DeepEval.eval now accepts atomic_weight but the docstring doesn’t describe it (purpose, expected shape, or broadcasting rules). That will confuse users trying to adopt the new feature.
  • In the spin branch, eval() calls _eval_model_spin without passing atomic_weight, so any atomic_weight supplied by the caller is silently ignored for spinful models. This is a behavioral discrepancy compared to the non-spin path and can yield scientifically inconsistent results without warning.

I recommend:

  • Documenting atomic_weight in the eval docstring (including allowed shapes and units/semantics).
  • Either (a) extending _eval_model_spin and the spin model path to accept and apply atomic_weight as well, or (b) explicitly rejecting atomic_weight when spin is provided (e.g., ValueError) so users don’t assume it is applied when it is not.
🧹 Nitpick comments (1)
deepmd/pt/infer/deep_eval.py (1)

474-541: Clarify atomic_weight shape expectations and auto-batch behavior in _eval_model

The basic plumbing of atomic_weight in _eval_model looks correct: you reshape to (nframes, natoms, -1), convert with to_torch_tensor, and pass atomic_weight=atomic_weight_input into the model, consistent with how fparam and aparam are handled (see to_torch_tensor in deepmd/pt/utils/utils.py).

A few points worth tightening up:

  • The reshape atomic_weight.reshape(nframes, natoms, -1) implicitly constrains acceptable input shapes. It will work nicely for (nframes, natoms) or (nframes, natoms, k) arrays/tensors, but will fail for shapes like (natoms,) that one might reasonably expect to be broadcast across frames. If you intend to support only full (nframes, natoms, [dim]) inputs, this should be documented. If not, you’ll need extra logic (similar to how aparam is documented) to handle per-atom or global weights explicitly before reshaping.
  • atomic_weight is threaded into _eval_func(self._eval_model, numb_test, natoms)(...) as a trailing positional argument. That means AutoBatchSize.execute_all must be aware that there is now an additional per-frame/per-atom array in *args and split it consistently across batches. It’s probably fine, but worth confirming via tests that auto-batching with atomic_weight behaves as expected.

No immediate correctness bug for the documented use-case, but clarifying the supported shapes and verifying the auto-batch path would make this much more robust.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb2659e and 93918d8.

📒 Files selected for processing (2)
  • deepmd/pt/infer/deep_eval.py (5 hunks)
  • deepmd/pt/train/wrapper.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
deepmd/pt/infer/deep_eval.py (1)
deepmd/pt/utils/utils.py (3)
  • to_torch_tensor (251-251)
  • to_torch_tensor (255-255)
  • to_torch_tensor (258-276)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Test Python (6, 3.12)
  • GitHub Check: Build wheels for cp310-manylinux_aarch64
  • GitHub Check: Test Python (6, 3.9)
  • GitHub Check: Test Python (5, 3.9)
  • GitHub Check: Test Python (4, 3.9)
  • GitHub Check: Test Python (1, 3.9)
  • GitHub Check: Test Python (4, 3.12)
  • GitHub Check: Test Python (5, 3.12)
  • GitHub Check: Build C++ (rocm, rocm)
  • GitHub Check: Build C++ (cuda, cuda)
  • GitHub Check: Test C++ (true, true, true, false)
  • GitHub Check: Test C++ (false, true, true, false)
  • GitHub Check: Build C++ (cuda120, cuda)
  • GitHub Check: Test C++ (true, false, false, true)
  • GitHub Check: Test C++ (false, false, false, true)
  • GitHub Check: Build C++ (clang, clang)
  • GitHub Check: Build C library (2.14, >=2.5.0,<2.15, libdeepmd_c_cu11.tar.gz)
  • GitHub Check: Build C library (2.18, libdeepmd_c.tar.gz)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (c-cpp)
🔇 Additional comments (1)
deepmd/pt/train/wrapper.py (1)

154-168: atomic_weight plumbing in ModelWrapper.forward looks correct

The new atomic_weight argument is added at the end of the signature with a default of None and is consistently threaded through input_dict for both inference (self.model[task_key](**input_dict)) and training (passed as part of input_dict to the loss). This should be backward-compatible with existing call sites and enables losses/models that understand atomic_weight to consume it without impacting others.

Also applies to: 175-183

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
deepmd/pt/infer/deep_eval.py (1)

657-699: Dead code: validation never reached.

This validation block is identical to lines 534-576 in _eval_model. However, it's unreachable dead code because eval() raises an error when atomic_weight is provided with spin (lines 379-382).

This duplication should be addressed by either:

  1. Removing atomic_weight support from _eval_model_spin entirely (if spin + atomic_weight shouldn't be supported)
  2. Removing the error check in eval() and extracting the shared validation logic (if it should be supported)

See the earlier comment on lines 379-382 for detailed recommendations.

🧹 Nitpick comments (1)
deepmd/pt/infer/deep_eval.py (1)

543-557: Document ambiguous shape handling.

When natoms == nframes, a 2D input is ambiguous—it could be interpreted as either (nframes, natoms) or (natoms, nframes). The code resolves this by preferring the (nframes, natoms) interpretation (since the condition on line 546 requires shape[1] != nframes).

This is a reasonable choice, but consider adding a comment or updating the docstring to clarify this edge case for users.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4372534 and 516f1bf.

📒 Files selected for processing (1)
  • deepmd/pt/infer/deep_eval.py (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
deepmd/pt/infer/deep_eval.py (1)
deepmd/pt/utils/utils.py (3)
  • to_torch_tensor (251-251)
  • to_torch_tensor (255-255)
  • to_torch_tensor (258-276)
🪛 Ruff (0.14.8)
deepmd/pt/infer/deep_eval.py

380-382: Avoid specifying long messages outside the exception class

(TRY003)


539-541: Avoid specifying long messages outside the exception class

(TRY003)


555-557: Avoid specifying long messages outside the exception class

(TRY003)


564-566: Avoid specifying long messages outside the exception class

(TRY003)


568-570: Avoid specifying long messages outside the exception class

(TRY003)


662-664: Avoid specifying long messages outside the exception class

(TRY003)


678-680: Avoid specifying long messages outside the exception class

(TRY003)


687-689: Avoid specifying long messages outside the exception class

(TRY003)


691-693: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Build wheels for cp311-win_amd64
  • GitHub Check: Build wheels for cp311-macosx_arm64
  • GitHub Check: Build wheels for cp311-macosx_x86_64
  • GitHub Check: Build C library (2.14, >=2.5.0,<2.15, libdeepmd_c_cu11.tar.gz)
  • GitHub Check: Build C library (2.18, libdeepmd_c.tar.gz)
  • GitHub Check: Test Python (2, 3.12)
  • GitHub Check: Build C library (2.14, >=2.5.0,<2.15, libdeepmd_c_cu11.tar.gz)
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Build wheels for cp311-win_amd64
  • GitHub Check: Build wheels for cp311-macosx_arm64
  • GitHub Check: Build wheels for cp311-macosx_x86_64
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Test Python (3, 3.9)
  • GitHub Check: Test Python (1, 3.9)
  • GitHub Check: Build C++ (cuda, cuda)
  • GitHub Check: Build C++ (clang, clang)
  • GitHub Check: Analyze (c-cpp)
  • GitHub Check: Analyze (c-cpp)
🔇 Additional comments (1)
deepmd/pt/infer/deep_eval.py (1)

314-314: Well-documented parameter addition.

The atomic_weight parameter is properly added with clear documentation covering the supported shapes.

Also applies to: 344-349

Comment thread deepmd/pt/infer/deep_eval.py
Comment on lines +534 to +576
if atomic_weight is not None:
# Validate atomic_weight shape
if atomic_weight.ndim == 1:
# (natoms,) - broadcast across frames
if atomic_weight.shape[0] != natoms:
raise ValueError(
f"atomic_weight shape {atomic_weight.shape} incompatible with number of atoms {natoms}"
)
atomic_weight = np.tile(atomic_weight, (nframes, 1))
elif atomic_weight.ndim == 2:
# (nframes, natoms) or (natoms, k)
if (
atomic_weight.shape[0] == natoms
and atomic_weight.shape[1] != nframes
):
# (natoms, k) - broadcast across frames
atomic_weight = np.tile(atomic_weight, (nframes, 1, 1))
elif (
atomic_weight.shape[0] != nframes
or atomic_weight.shape[1] != natoms
):
raise ValueError(
f"atomic_weight shape {atomic_weight.shape} incompatible with nframes={nframes} and natoms={natoms}"
)
elif atomic_weight.ndim == 3:
# (nframes, natoms, k)
if (
atomic_weight.shape[0] != nframes
or atomic_weight.shape[1] != natoms
):
raise ValueError(
f"atomic_weight shape {atomic_weight.shape} incompatible with nframes={nframes} and natoms={natoms}"
)
else:
raise ValueError(
f"atomic_weight must have 1, 2, or 3 dimensions, got {atomic_weight.ndim}"
)

atomic_weight_input = to_torch_tensor(
atomic_weight.reshape(nframes, natoms, -1)
)
else:
atomic_weight_input = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated validation logic to a helper method.

This validation block is duplicated identically in _eval_model_spin (lines 657-699), violating the DRY principle. The logic is also quite complex and would benefit from being in a separate, testable method.

Extract to a helper method:

def _validate_and_reshape_atomic_weight(
    self,
    atomic_weight: Optional[np.ndarray],
    nframes: int,
    natoms: int,
) -> Optional[np.ndarray]:
    """Validate and reshape atomic_weight to (nframes, natoms, k).
    
    Parameters
    ----------
    atomic_weight : np.ndarray, optional
        Input atomic weight array
    nframes : int
        Number of frames
    natoms : int
        Number of atoms
        
    Returns
    -------
    np.ndarray, optional
        Reshaped array of shape (nframes, natoms, k) or None
        
    Raises
    ------
    ValueError
        If shape is incompatible with nframes and natoms
    """
    if atomic_weight is None:
        return None
        
    # Validation logic here...
    # (lines 536-574)
    
    return atomic_weight.reshape(nframes, natoms, -1)

Then use it in both _eval_model and _eval_model_spin:

-    if atomic_weight is not None:
-        # Validate atomic_weight shape
-        if atomic_weight.ndim == 1:
-            ...
-        atomic_weight_input = to_torch_tensor(
-            atomic_weight.reshape(nframes, natoms, -1)
-        )
-    else:
-        atomic_weight_input = None
+    reshaped_weight = self._validate_and_reshape_atomic_weight(
+        atomic_weight, nframes, natoms
+    )
+    atomic_weight_input = to_torch_tensor(reshaped_weight) if reshaped_weight is not None else None
🧰 Tools
🪛 Ruff (0.14.8)

539-541: Avoid specifying long messages outside the exception class

(TRY003)


555-557: Avoid specifying long messages outside the exception class

(TRY003)


564-566: Avoid specifying long messages outside the exception class

(TRY003)


568-570: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In deepmd/pt/infer/deep_eval.py around lines 534 to 576, the atomic_weight
validation/reshaping logic is duplicated (also in _eval_model_spin lines
~657-699); extract this into a private helper method (e.g.
_validate_and_reshape_atomic_weight(self, atomic_weight, nframes, natoms)) that
returns either None or a np.ndarray shaped (nframes, natoms, k), move all shape
checks and broadcasting logic into it (preserve all current ValueError
messages), and replace the duplicated blocks in both _eval_model and
_eval_model_spin with a call to this helper and then to_torch_tensor on its
result if not None.

@ChiahsinChu ChiahsinChu force-pushed the patch/v3.1.1/dipole_with_atomic_weight branch from 516f1bf to 0c55d75 Compare December 15, 2025 01:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
deepmd/dpmodel/model/make_model.py (1)

5-7: Missing Optional import causes NameError.

The Optional type hint is used on lines 71, 234, 295, and 362, but it's not imported from the typing module. This will cause a NameError at runtime.

Apply this diff to fix the import:

 from typing import (
     Any,
+    Optional,
 )
deepmd/pt/model/model/make_model.py (1)

5-7: Missing Optional import causes NameError.

The Optional type hint is used on lines 141, 258, and 645, but it's not imported from the typing module. This will cause a NameError at runtime.

Apply this diff to fix the import:

 from typing import (
     Any,
+    Optional,
 )
deepmd/pt/infer/deep_eval.py (1)

607-714: Dead code: atomic_weight handling in _eval_model_spin is unreachable.

Since eval() raises an error at lines 380-383 when atomic_weight is provided with spin, the _eval_model_spin method will always receive atomic_weight=None. This makes:

  • The parameter at line 616 effectively unused
  • The validation logic at lines 658-700 unreachable dead code
  • The atomic_weight_input at line 713 always None

Consider one of these options:

Option A: Remove atomic_weight from _eval_model_spin entirely

 def _eval_model_spin(
     self,
     coords: np.ndarray,
     cells: np.ndarray | None,
     atom_types: np.ndarray,
     spins: np.ndarray,
     fparam: np.ndarray | None,
     aparam: np.ndarray | None,
     request_defs: list[OutputVariableDef],
-    atomic_weight: np.ndarray | None = None,
 ) -> tuple[np.ndarray, ...]:

And remove lines 658-700 and the atomic_weight=atomic_weight_input from line 713.

Option B: Keep for future support but add a comment explaining it's reserved for future use.

deepmd/dpmodel/atomic_model/base_atomic_model.py (1)

221-232: Add validation or clarification for atomic_weight broadcasting.

The atomic_weight scaling multiplies each output by atomic_weight.reshape([_out_shape[0], _out_shape[1], -1]). This assumes the third dimension of atomic_weight matches the flattened trailing dimensions of the output. While tests show atomic_weight shape [nf, nloc, 1] for energy outputs (which have shape [nf, nloc, 1] after flattening), the code does not validate this constraint.

For outputs with different dimensions—e.g., a tensor property with shape [nf, nloc, 3, 3] flattened to [nf, nloc, 9]—broadcasting with [nf, nloc, 1] atomic_weight would fail or behave unexpectedly. Either validate that atomic_weight.shape[2] == math.prod(out_shape[2:]) for each output, or explicitly document this requirement in the docstring.

Additionally, the "mask" key added at line 232 is not scaled, which is correct since it's added after the loop completes.

deepmd/pt/model/model/dipole_model.py (1)

55-73: Fix undefined Optional — change to PEP 604 union syntax

On line 63, atomic_weight: Optional[torch.Tensor] references Optional, which is not imported. Since __future__ annotations is not enabled, this will raise a NameError at runtime.

The rest of the file already uses the PEP 604 union syntax (torch.Tensor | None on lines 59–61), so update line 63 to match:

-        atomic_weight: Optional[torch.Tensor] = None,
+        atomic_weight: torch.Tensor | None = None,

This fixes the undefined name error and keeps typing consistent throughout the file.

🧹 Nitpick comments (2)
deepmd/pt/infer/deep_eval.py (1)

535-577: Consider extracting validation logic to a helper method.

This validation/reshaping logic for atomic_weight (lines 535-577) is complex and handles multiple shape scenarios (1D, 2D, 3D). While the implementation is correct, extracting it to a helper method would:

  1. Improve readability
  2. Make the logic easier to test in isolation
  3. Enable reuse if needed elsewhere

Example helper signature:

def _validate_and_reshape_atomic_weight(
    self,
    atomic_weight: np.ndarray | None,
    nframes: int,
    natoms: int,
) -> np.ndarray | None:
    """Validate and reshape atomic_weight to (nframes, natoms, k)."""
    ...
deepmd/pt/train/wrapper.py (1)

152-200: Consider adding atomic_weight to input_dict only when not None

The new atomic_weight parameter is correctly threaded through to the model and loss, but it’s always included in input_dict, even when None. If any existing model/loss still has a strict forward(...) signature without atomic_weight, this can cause TypeError: got an unexpected keyword argument 'atomic_weight'.

To keep this wrapper backward‑compatible while still supporting the new feature, only add the key when a non‑None tensor is provided:

-        input_dict = {
-            "coord": coord,
-            "atype": atype,
-            "box": box,
-            "do_atomic_virial": do_atomic_virial,
-            "fparam": fparam,
-            "aparam": aparam,
-            "atomic_weight": atomic_weight,
-        }
+        input_dict = {
+            "coord": coord,
+            "atype": atype,
+            "box": box,
+            "do_atomic_virial": do_atomic_virial,
+            "fparam": fparam,
+            "aparam": aparam,
+        }
+        if atomic_weight is not None:
+            input_dict["atomic_weight"] = atomic_weight
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 516f1bf and 2a95032.

📒 Files selected for processing (8)
  • deepmd/dpmodel/atomic_model/base_atomic_model.py (5 hunks)
  • deepmd/dpmodel/model/make_model.py (11 hunks)
  • deepmd/pt/infer/deep_eval.py (9 hunks)
  • deepmd/pt/model/atomic_model/base_atomic_model.py (5 hunks)
  • deepmd/pt/model/model/dipole_model.py (4 hunks)
  • deepmd/pt/model/model/make_model.py (12 hunks)
  • deepmd/pt/train/wrapper.py (2 hunks)
  • source/tests/pt/model/test_dp_atomic_model.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • deepmd/pt/model/atomic_model/base_atomic_model.py
🧰 Additional context used
🧬 Code graph analysis (3)
deepmd/pt/infer/deep_eval.py (1)
deepmd/pt/utils/utils.py (3)
  • to_torch_tensor (249-249)
  • to_torch_tensor (253-253)
  • to_torch_tensor (256-274)
deepmd/pt/train/wrapper.py (2)
deepmd/pt/model/network/network.py (1)
  • Tensor (34-35)
deepmd/pd/model/network/network.py (1)
  • Tensor (26-29)
deepmd/pt/model/model/dipole_model.py (1)
deepmd/pt/model/network/network.py (1)
  • Tensor (34-35)
🪛 Ruff (0.14.8)
deepmd/pt/infer/deep_eval.py

381-383: Avoid specifying long messages outside the exception class

(TRY003)


540-542: Avoid specifying long messages outside the exception class

(TRY003)


556-558: Avoid specifying long messages outside the exception class

(TRY003)


565-567: Avoid specifying long messages outside the exception class

(TRY003)


569-571: Avoid specifying long messages outside the exception class

(TRY003)


663-665: Avoid specifying long messages outside the exception class

(TRY003)


679-681: Avoid specifying long messages outside the exception class

(TRY003)


688-690: Avoid specifying long messages outside the exception class

(TRY003)


692-694: Avoid specifying long messages outside the exception class

(TRY003)

deepmd/dpmodel/model/make_model.py

71-71: Undefined name Optional

(F821)


234-234: Undefined name Optional

(F821)


295-295: Undefined name Optional

(F821)


362-362: Undefined name Optional

(F821)

deepmd/pt/model/model/make_model.py

141-141: Undefined name Optional

(F821)


258-258: Undefined name Optional

(F821)


645-645: Undefined name Optional

(F821)

deepmd/pt/model/model/dipole_model.py

63-63: Undefined name Optional

(F821)

🔇 Additional comments (15)
deepmd/dpmodel/atomic_model/base_atomic_model.py (1)

236-254: LGTM!

The call method correctly accepts and propagates atomic_weight to forward_common_atomic.

deepmd/dpmodel/model/make_model.py (5)

48-131: LGTM!

The atomic_weight parameter is correctly threaded through model_call_from_call_lower to call_lower. The implementation is consistent with the overall design.


226-284: LGTM!

The CM.call method correctly:

  1. Receives atomic_weight from input_type_cast as aw
  2. Passes aw to model_call_from_call_lower

286-351: LGTM!

The CM.call_lower method correctly threads atomic_weight through input type casting and passes it to forward_common_atomic.


383-414: LGTM!

The input_type_cast method correctly handles atomic_weight:

  1. Casts to coord dtype alongside other parameters
  2. Returns in the extended tuple
  3. Handles precision conversion consistently with other optional parameters

353-379: LGTM!

The forward_common_atomic method correctly propagates atomic_weight to the underlying atomic model.

deepmd/pt/model/model/make_model.py (4)

133-210: LGTM!

The forward_common method correctly:

  1. Accepts atomic_weight parameter with docstring
  2. Receives aw from input_type_cast
  3. Deletes atomic_weight from local scope
  4. Passes aw to forward_common_lower

247-325: LGTM!

The forward_common_lower method correctly threads atomic_weight through input type casting to atomic_model.forward_common_atomic.


327-373: LGTM!

The input_type_cast method correctly handles atomic_weight with proper dtype conversion using PyTorch's .to() method.


637-656: LGTM!

The forward method correctly accepts and passes atomic_weight to forward_common.

deepmd/pt/infer/deep_eval.py (2)

307-398: LGTM for the public eval method.

The atomic_weight parameter is correctly added with appropriate docstring. The error raised when atomic_weight is provided with spin (lines 380-383) makes the design intent clear.


485-589: LGTM for _eval_model atomic_weight handling.

The validation logic correctly handles various input shapes:

  • 1D (natoms,) → broadcast across frames
  • 2D (nframes, natoms) or (natoms, k) → appropriate handling
  • 3D (nframes, natoms, k) → direct use

The final reshape to (nframes, natoms, -1) ensures consistent shape for the model.

source/tests/pt/model/test_dp_atomic_model.py (2)

76-82: LGTM! Good test coverage for atomic_weight scaling.

The test correctly verifies that forward_common_atomic with atomic_weight produces energy scaled by the weight. The assertion ret0["energy"] * aw.reshape(nf, nloc, -1) matches the expected scaling behavior implemented in the atomic model.


111-118: LGTM! Good cross-implementation consistency test.

The test verifies that atomic_weight produces consistent results between the DP (numpy) and PT (torch) implementations, which is important for maintaining compatibility across backends.

deepmd/pt/model/model/dipole_model.py (1)

93-117: atomic_weight propagation in forward_lower looks correct

forward_lower now accepts atomic_weight: torch.Tensor | None and passes it through to forward_common_lower(..., atomic_weight=atomic_weight, ...), which matches the pattern used in the higher‑level forward method and keeps the lower‑level path in sync with the main dipole path.

@ChiahsinChu ChiahsinChu force-pushed the patch/v3.1.1/dipole_with_atomic_weight branch from 2a95032 to 9359011 Compare December 15, 2025 03:15

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
deepmd/pt/infer/deep_eval.py (1)

315-350: Fix eval docstring vs behavior for atomic_weight with spin

In eval, when spin is provided and atomic_weight is not None, a ValueError is raised (Lines 380–383), but the docstring still says:

Note: This parameter is currently ignored when spin is provided.

That’s misleading for users.

Consider updating both the doc and the error message to reflect the actual behavior, e.g.:

-        atomic_weight
-            The atomic weight for weighted averaging of atomic contributions.
+        atomic_weight
+            The atomic weight for weighted averaging of atomic contributions
+            on non-spin models.
@@
-            - nframes x natoms x k. Different weights for each atom in each frame with k dimensions.
-            Note: This parameter is currently ignored when spin is provided.
+            - nframes x natoms x k. Different weights for each atom in each frame with k dimensions.
+            Note: This parameter is not supported together with ``spin``.
+            A :class:`ValueError` is raised if both are provided.
@@
-            if atomic_weight is not None:
-                raise ValueError(
-                    "atomic_weight is not supported when spin is provided."
-                )
+            if atomic_weight is not None:
+                raise ValueError(
+                    "atomic_weight is not supported when spin is provided; "
+                    "omit atomic_weight or disable spin."
+                )

This keeps the API contract explicit and avoids surprises.

Also applies to: 370-383

deepmd/pt/model/model/dipole_model.py (1)

55-73: Avoid breaking forward_lower callers by reordering new atomic_weight parameter

Adding atomic_weight to forward is a pure extension and safe. In forward_lower, though, the new signature is:

def forward_lower(
    self,
    extended_coord,
    extended_atype,
    nlist,
    mapping=None,
    fparam=None,
    aparam=None,
    do_atomic_virial: bool = False,
    atomic_weight: torch.Tensor | None = None,  # new
    comm_dict: dict[str, torch.Tensor] | None = None,
) -> dict[str, torch.Tensor]:

Previously, comm_dict was the positional argument immediately after do_atomic_virial. Any existing callers that do:

dipole.forward_lower(..., do_atomic_virial, comm_dict)

will now have comm_dict bound to atomic_weight instead, which is a silent but serious behavior change (likely causing shape errors or mis-scaling).

To keep backward compatibility, consider keeping comm_dict in its original position and appending atomic_weight at the end:

-    def forward_lower(
+    def forward_lower(
         self,
         extended_coord: torch.Tensor,
         extended_atype: torch.Tensor,
         nlist: torch.Tensor,
         mapping: torch.Tensor | None = None,
         fparam: torch.Tensor | None = None,
         aparam: torch.Tensor | None = None,
         do_atomic_virial: bool = False,
-        atomic_weight: torch.Tensor | None = None,
-        comm_dict: dict[str, torch.Tensor] | None = None,
+        comm_dict: dict[str, torch.Tensor] | None = None,
+        atomic_weight: torch.Tensor | None = None,
     ) -> dict[str, torch.Tensor]:
@@
-            do_atomic_virial=do_atomic_virial,
-            comm_dict=comm_dict,
-            extra_nlist_sort=self.need_sorted_nlist_for_lower(),
-            atomic_weight=atomic_weight,
+            do_atomic_virial=do_atomic_virial,
+            comm_dict=comm_dict,
+            extra_nlist_sort=self.need_sorted_nlist_for_lower(),
+            atomic_weight=atomic_weight,

Since forward_common_lower is called with keyword arguments, this change won’t affect that internal call, but will preserve existing positional uses of comm_dict.

Also applies to: 94-117

🧹 Nitpick comments (4)
deepmd/dpmodel/atomic_model/base_atomic_model.py (1)

151-160: Clarify atomic_weight shape semantics and validation behavior

The per-key scaling logic looks correct (masking then multiplying by a reshaped atomic_weight), but a couple of details are worth tightening:

  • The code enforces only atomic_weight.shape[2] == prod(out_shape[2:]), while the docstring says the full shape is nf x nloc x dim_aw. If you intend to require per-frame/per-atom weights (no broadcasting on the first two axes), consider also checking atomic_weight.shape[:2] == _out_shape[:2] for clearer failure modes. If broadcasting over frames/atoms is intended, it would help to document that explicitly.
  • The long ValueError message is a bit unwieldy; consider shortening it or moving some detail into a helper/constant if you want to appease static analysis.

Functionally this is fine; these are mostly API clarity and ergonomics points.

Also applies to: 181-183, 221-239

deepmd/pt/infer/deep_eval.py (1)

881-936: Document all accepted atomic_weight shapes and simplify error text

The new _validate_and_reshape_atomic_weight helper is a good consolidation of shape handling, but:

  • It supports shapes beyond what eval’s docstring mentions: (natoms,) and (natoms, k) (broadcast over frames) as well as (nframes, natoms) and (nframes, natoms, k). It would be helpful to either reference this helper from the public docs or expand the docstring to list these accepted patterns so users know what’s allowed.
  • The multiple ValueError messages are quite long and repetitive; you might consider shortening them a bit or factoring shared wording into a small internal helper/constant if you want to quiet static-analysis complaints.

Functionally the logic (including the tiling and nframes/natoms checks) looks sound.

deepmd/dpmodel/model/make_model.py (1)

71-72: atomic_weight propagation through dpmodel CM looks consistent

The dpmodel factory now:

  • Accepts atomic_weight in model_call_from_call_lower, CM.call, CM.call_lower, and CM.forward_common_atomic.
  • Normalizes its dtype in input_type_cast alongside box, fparam, and aparam.
  • Passes the casted aw down to atomic_model.forward_common_atomic, which owns the actual scaling logic.

This wiring is coherent and preserves backward compatibility (all new params are optional and default to None). If you want to tidy things further, you might:

  • Mention atomic_weight explicitly in the model_call_from_call_lower and input_type_cast docstrings, and
  • Align the input_type_cast return type hints to use Array | None instead of np.ndarray | None for consistency with the rest of the file.

These are style-only; behavior looks correct.

Also applies to: 226-283, 286-352, 383-414

deepmd/pt/model/model/make_model.py (1)

132-177: atomic_weight integration in PT factory is coherent and backward-compatible

On the PyTorch side:

  • forward_common and forward now take an optional atomic_weight and pass it into input_type_cast.
  • input_type_cast casts atomic_weight to the coordinate dtype and returns it as aw alongside coord, box, fparam, and aparam.
  • forward_common_lower picks up aw and passes it as atomic_weight into atomic_model.forward_common_atomic, then into fit_output_to_model_output.

All new parameters are optional and appended, so existing call sites remain valid. The design mirrors the dpmodel path and matches how DeepEval prepares a (nframes, natoms, k) tensor.

Only minor polish you might consider: expand the docstrings for forward_common / forward_common_lower slightly to mention that atomic_weight is expected to have shape (nf, nloc, dim_aw) after any broadcasting/reshaping performed at higher levels (DeepEval).

Also applies to: 247-315, 327-373, 637-656

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a95032 and 9359011.

📒 Files selected for processing (5)
  • deepmd/dpmodel/atomic_model/base_atomic_model.py (5 hunks)
  • deepmd/dpmodel/model/make_model.py (11 hunks)
  • deepmd/pt/infer/deep_eval.py (9 hunks)
  • deepmd/pt/model/model/dipole_model.py (4 hunks)
  • deepmd/pt/model/model/make_model.py (12 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
deepmd/dpmodel/model/make_model.py (2)
deepmd/pt/model/model/make_model.py (1)
  • input_type_cast (327-373)
deepmd/pd/model/model/make_model.py (1)
  • input_type_cast (303-346)
deepmd/pt/model/model/make_model.py (2)
deepmd/pt/model/network/network.py (1)
  • Tensor (34-35)
deepmd/dpmodel/model/make_model.py (1)
  • input_type_cast (383-414)
🪛 Ruff (0.14.8)
deepmd/pt/infer/deep_eval.py

381-383: Avoid specifying long messages outside the exception class

(TRY003)


912-914: Avoid specifying long messages outside the exception class

(TRY003)


922-924: Avoid specifying long messages outside the exception class

(TRY003)


928-930: Avoid specifying long messages outside the exception class

(TRY003)


932-934: Avoid specifying long messages outside the exception class

(TRY003)

deepmd/dpmodel/atomic_model/base_atomic_model.py

233-236: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Test C++ (false, false, false, true)
  • GitHub Check: Analyze (python)
  • GitHub Check: Build wheels for cp311-manylinux_x86_64
  • GitHub Check: Test C++ (true, false, false, true)
  • GitHub Check: Build C library (2.18, libdeepmd_c.tar.gz)
  • GitHub Check: Test C++ (false, false, false, true)
  • GitHub Check: Test C++ (true, true, true, false)
  • GitHub Check: Test C++ (false, true, true, false)
🔇 Additional comments (2)
deepmd/dpmodel/atomic_model/base_atomic_model.py (1)

244-262: call wrapper correctly forwards atomic_weight

The call method’s new atomic_weight parameter is threaded straight through to forward_common_atomic and preserves the previous signature as a strict extension, so existing callers remain compatible.

deepmd/pt/infer/deep_eval.py (1)

485-555: Non‑spin and spin model paths handle atomic_weight consistently

The non‑spin _eval_model path now:

  • Accepts atomic_weight,
  • Validates/reshapes it via _validate_and_reshape_atomic_weight,
  • Converts to a (nframes, natoms, k) torch tensor,
  • And passes it as atomic_weight=... to the model.

The spin path _eval_model_spin:

  • No longer takes atomic_weight,
  • Always passes atomic_weight=None into the model,
  • And eval forbids non‑None atomic_weight when spin is present.

This separation looks coherent and should keep the PT models’ signature uniform (atomic_weight kwarg always exists) while making spin behavior explicit at the wrapper level.

Also applies to: 573-637

@ChiahsinChu ChiahsinChu force-pushed the patch/v3.1.1/dipole_with_atomic_weight branch from 9359011 to 02fa9d0 Compare December 27, 2025 04:35
@ChiahsinChu ChiahsinChu closed this Jan 8, 2026
@ChiahsinChu ChiahsinChu deleted the patch/v3.1.1/dipole_with_atomic_weight branch January 8, 2026 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant