Skip to content

Fix electrode charge sync for split K-space - #8

Open
HanswithCMY wants to merge 2 commits into
ChiahsinChu:masterfrom
HanswithCMY:fix/sync-electrode-q-kspace
Open

Fix electrode charge sync for split K-space#8
HanswithCMY wants to merge 2 commits into
ChiahsinChu:masterfrom
HanswithCMY:fix/sync-electrode-q-kspace

Conversation

@HanswithCMY

Copy link
Copy Markdown
Collaborator

Fix dynamic electrode charge synchronization in verlet/split/kspace and verlet/split/dplr

Problem

verlet/split/dplr separates R-space and K-space into distinct MPI
partitions. The previous implementation transferred atom->q from R-space to
K-space only in rk_setup(), which runs at initialization and after neighbor
list rebuilding.

This is incorrect for dynamic electrode fixes such as electrode/conq and
electrode/conp: they update electrode charges at every MD step during
PRE_FORCE. Consequently, R-space pair interactions used the current charges,
whereas K-space PPPM could use charges from the previous neighbor rebuild.

Fix

Move charge synchronization to VerletSplitKSpace::r2k_comm(), which is called
after PRE_FORCE and before the K-space calculation. Coordinates and charges
are now transferred together every step.

The old rk_setup() charge transfer is retained as commented archived code to
document the previous behavior and its limitation.

Validation

A direct R-space/K-space charge trace was recorded for a 30-step
electrode/conq trajectory with neigh_modify every 10.

  • Previous behavior: K-space sum(q^2) was piecewise constant and updated
    only after neighbor rebuilding (every 10 steps in this case). At each such
    transfer, its charge state was one integration step behind the current
    R-space state, because the transfer occurred before the subsequent
    PRE_FORCE charge update. The maximum per-atom R/K charge mismatch was
    1.46 e.
  • Fixed behavior: R-space and K-space charge traces overlap exactly;
    maximum per-atom mismatch is zero to printed precision.
q2_trace_comparison

Comparison of the R-space and K-space charge traces. With the previous implementation, K-space charges remained stale between neighbor-list rebuilds. After per-step synchronization in r2k_comm(), the two partitions have identical charge states at every MD step.

Copilot AI lite review requested due to automatic review settings August 28, 2026 09:50
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c8c61f03-dfbe-4c40-9461-c9dd0744ee00


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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Synchronize dynamic electrode charges before split K-space computation

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Scatter updated electrode charges with coordinates after each PRE_FORCE phase.
• Prevent split K-space PPPM from using charges stale since the last neighbor rebuild.
Diagram

sequenceDiagram
  participant Loop as MD Loop
  participant Fix as Electrode Fix
  participant R as R-space Atoms
  participant Comm as R2K Comm
  participant K as K-space PPPM
  Loop->>Fix: Run PRE_FORCE
  Fix->>R: Update charges
  Loop->>Comm: Synchronize state
  Comm->>R: Read coordinates and charges
  Comm->>K: Scatter current state
  Loop->>K: Compute long-range forces
Loading
High-Level Assessment

Synchronizing charges inside r2k_comm() is the appropriate approach because it is already the per-step communication boundary after PRE_FORCE and transfers coordinates using the same partition counts and displacements. Retaining the scatter in rk_setup() would preserve stale charges between neighbor rebuilds, while adding an electrode-specific synchronization hook would couple the integrator to particular fixes.

Files changed (1) +16 / -8

Bug fix (1) +16 / -8
verlet_split_kspace.cppScatter current charges during per-step R-to-K synchronization +16/-8

Scatter current charges during per-step R-to-K synchronization

• Moves effective charge synchronization from neighbor-rebuild setup into 'r2k_comm()', immediately after 'PRE_FORCE', so split K-space calculations receive current dynamic electrode charges alongside coordinates. The former one-time scatter is retained as commented historical context explaining why rebuild-only synchronization is insufficient.

src/lmp/verlet_split_kspace.cpp

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

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

🟢 Approval recommended

The functional change is straightforward and localized to the intended communication path, with only a minor comment typo to address.

Pull request overview

This PR fixes a correctness issue in split R-space/K-space runs where K-space PPPM could use stale electrode charges by synchronizing atom->q from R-space to K-space every timestep, after PRE_FORCE (when electrode fixes update charges).

Changes:

  • Move charge transfer from one-time rk_setup() logic to per-step VerletSplitKSpace::r2k_comm().
  • Synchronize coordinates and charges together via MPI_Scatterv() in r2k_comm() to ensure consistent electrostatic state on K-space ranks.
  • Leave the old rk_setup() charge scatter as commented “archived” code with an explanatory note.
File summaries
File Description
src/lmp/verlet_split_kspace.cpp Adds per-step R→K scatter of atom->q (aligned with atom->x scatter) and updates comments to reflect new synchronization point.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/lmp/verlet_split_kspace.cpp Outdated
// MPI_Scatterv(master ? atom->q : nullptr, qsize, qdisp, MPI_DOUBLE,
// atom->q, n, MPI_DOUBLE, 0, block);
// Disabled because electrode fixes may update atom->q every PRE_FORCE;
// transferring only at reneighbouring leaves K-space PPPM with stale charges.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
5 3 2 0
View the top 3 failed test(s) by shortest run time
tests.tf.test_data_modifier.TestChargeDipoleElectrodeModifier::test_electrode_charge
Stack Traces | 25.5s run time
self = <tf.test_data_modifier.TestChargeDipoleElectrodeModifier testMethod=test_electrode_charge>

    def test_electrode_charge(self):
        # setup charge
        charge = np.array(self.sys_charge_map)[self.atype]
        charge = np.tile(charge, [1, 1])
        # add wfcc
        all_coord, all_charge, _dipole = self.dm_test._extend_system(
            self.coord, self.box, self.atype, charge
        )
    
        t_positions = torch.tensor(all_coord.reshape(-1, 3), requires_grad=True)
        t_box = torch.tensor(self.box.reshape(3, 3), requires_grad=True)
        t_charges = torch.tensor(all_charge.reshape(-1))
    
>       pairs, ds, buffer_scales = vesin_nblist(t_positions, t_box, self.dm_test.rcut)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../tests/tf/test_data_modifier.py:246: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

positions = tensor([[ 0.0000,  0.0000, 16.5567],
        [ 2.8115,  0.0000, 16.5567],
        [ 5.6229,  0.0000, 16.5567],
       ...7],
        [ 2.5214, 12.3570, 26.9059],
        [ 2.5134,  4.8776, 26.7682]], dtype=torch.float64, requires_grad=True)
box = tensor([[16.8690,  0.0000,  0.0000],
        [ 0.0000, 14.6090,  0.0000],
        [ 0.0000,  0.0000, 76.8870]], dtype=torch.float64, requires_grad=True)
rcut = 3.0

    def vesin_nblist(
        positions: torch.Tensor,
        box: torch.Tensor,
        rcut: float,
    ):
        """
        Build neighbor list using the Vesin library.
    
        Parameters
        ----------
        positions : torch.Tensor
            Atomic positions
        box : Optional[torch.Tensor]
            Simulation box vectors
        rcut : float
            Cutoff radius
    
        Returns
        -------
        tuple
            Tuple containing (pairs, ds, buffer_scales)
        """
        if NeighborList is None:
>           raise ImportError(
                "vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function."
            )
E           ImportError: vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function.

.../hostedtoolcache/Python/3.11.16.../x64/lib/python3.11.../site-packages/torch_admp/nblist.py:116: ImportError
tests.tf.test_data_modifier.TestChargeDipoleElectrodeModifier::test_train
Stack Traces | 31.2s run time
self = <tf.test_data_modifier.TestChargeDipoleElectrodeModifier testMethod=test_train>

    def test_train(self):
        input_json = "energy.json"
        init_model = None
        restart = None
        init_frz_model = None
        output_file = "out.json"
    
        root_dir = Path(__file__).parent
    
        os.chdir(self.data_dir)
    
        # train energy model
        tf.reset_default_graph()
>       train(
            INPUT=input_json,
            init_model=init_model,
            restart=restart,
            init_frz_model=init_frz_model,
            mpi_log="master",
            log_level=2,
            output=output_file,
            log_path=None,
        )

.../tests/tf/test_data_modifier.py:220: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/entrypoints/train.py:186: in train
    _do_work(jdata, run_opt, is_compress)
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/entrypoints/train.py:264: in _do_work
    model.build(train_data, stop_batch, origin_type_map=origin_type_map)
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/train/trainer.py:212: in build
    self.model.data_stat(data)
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/model/ener.py:139: in data_stat
    all_stat = make_stat_input(data, self.data_stat_nbatch, merge_sys=False)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../deepmd/utils/model_stat.py:51: in make_stat_input
    stat_data = data.get_batch(sys_idx=ii)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../deepmd/utils/data_system.py:460: in get_batch
    b_data = self.get_batch_standard(sys_idx)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../deepmd/utils/data_system.py:487: in get_batch_standard
    b_data = self.data_systems[self.pick_idx].get_batch(
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../deepmd/utils/data.py:286: in get_batch
    self._load_batch_set(self.dirs[self.set_count % self.get_numb_set()])
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../deepmd/utils/data.py:435: in _load_batch_set
    self.modifier.modify_data(self.batch_set, self)
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/modifier/dipole_charge_electrode.py:285: in modify_data
    tot_e, tot_f, tot_v = self.eval(coord, box, atype)
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/modifier/dipole_charge_electrode.py:186: in eval
    e, f, v = self._eval_polarisable_electrode(
.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../tf/modifier/dipole_charge_electrode.py:81: in _eval_polarisable_electrode
    pairs, ds, buffer_scales = vesin_nblist(t_positions, t_box, self.rcut)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

positions = tensor([[11.0819,  5.9915, 41.7300],
        [23.1045, 28.3360, 38.7302],
        [15.1515, -6.5686, 43.8911],
       ...7],
        [ 2.5214, 12.3570, 26.9059],
        [ 2.5134,  4.8776, 26.7682]], dtype=torch.float64, requires_grad=True)
box = tensor([[16.8690,  0.0000,  0.0000],
        [ 0.0000, 14.6090,  0.0000],
        [ 0.0000,  0.0000, 76.8870]], dtype=torch.float64, requires_grad=True)
rcut = 3.0

    def vesin_nblist(
        positions: torch.Tensor,
        box: torch.Tensor,
        rcut: float,
    ):
        """
        Build neighbor list using the Vesin library.
    
        Parameters
        ----------
        positions : torch.Tensor
            Atomic positions
        box : Optional[torch.Tensor]
            Simulation box vectors
        rcut : float
            Cutoff radius
    
        Returns
        -------
        tuple
            Tuple containing (pairs, ds, buffer_scales)
        """
        if NeighborList is None:
>           raise ImportError(
                "vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function."
            )
E           ImportError: vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function.

.../hostedtoolcache/Python/3.11.16......................................./x64/lib/python3.11.../site-packages/torch_admp/nblist.py:116: ImportError
tests.tf.test_data_modifier.TestChargeDipoleElectrodeModifier::test_consistency
Stack Traces | 75.8s run time
self = <tf.test_data_modifier.TestChargeDipoleElectrodeModifier testMethod=test_consistency>

    def test_consistency(self):
        # e, f, v
        out_ref = self.dm_ref.eval(self.coord, self.box, self.atype)
>       out_test = self.dm_test.eval(self.coord, self.box, self.atype, electrode=False)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.../tests/tf/test_data_modifier.py:198: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.11.16........./x64/lib/python3.11.../tf/modifier/dipole_charge_electrode.py:186: in eval
    e, f, v = self._eval_polarisable_electrode(
.../hostedtoolcache/Python/3.11.16........./x64/lib/python3.11.../tf/modifier/dipole_charge_electrode.py:81: in _eval_polarisable_electrode
    pairs, ds, buffer_scales = vesin_nblist(t_positions, t_box, self.rcut)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

positions = tensor([[11.0819,  5.9915, 41.7300],
        [23.1045, 28.3360, 38.7302],
        [15.1515, -6.5686, 43.8911],
       ...7],
        [ 2.5214, 12.3570, 26.9059],
        [ 2.5134,  4.8776, 26.7682]], dtype=torch.float64, requires_grad=True)
box = tensor([[16.8690,  0.0000,  0.0000],
        [ 0.0000, 14.6090,  0.0000],
        [ 0.0000,  0.0000, 76.8870]], dtype=torch.float64, requires_grad=True)
rcut = 3.0

    def vesin_nblist(
        positions: torch.Tensor,
        box: torch.Tensor,
        rcut: float,
    ):
        """
        Build neighbor list using the Vesin library.
    
        Parameters
        ----------
        positions : torch.Tensor
            Atomic positions
        box : Optional[torch.Tensor]
            Simulation box vectors
        rcut : float
            Cutoff radius
    
        Returns
        -------
        tuple
            Tuple containing (pairs, ds, buffer_scales)
        """
        if NeighborList is None:
>           raise ImportError(
                "vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function."
            )
E           ImportError: vesin[torch] is required for vesin_nblist. Please install vesin with torch support to use this function.

.../hostedtoolcache/Python/3.11.16........./x64/lib/python3.11.../site-packages/torch_admp/nblist.py:116: ImportError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copilot AI review requested due to automatic review settings August 29, 2026 07:21

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

🟢 Approval recommended

The change is localized and correctly synchronizes atom->q in the same collective communication phase as coordinates, matching the intended timing after PRE_FORCE and before K-space computation.

Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants