Skip to content

Commit 6312b85

Browse files
committed
feat(mta): implement link atom injection and force redistribution
Per-step link atom handling in the force provider: - After gathering ML atom positions, compute link atom positions via LinkFrontierAtom::setPositions/getLinkPosition and append to the model input - Build neighbor list pairs between link atoms and ML atoms within cutoff - After model evaluation, redistribute link atom forces to real atoms via LinkFrontierAtom::spreadForce (pattern from NNPot torchmodel.cpp) - Strip link atoms from arrays after force scatter Also stores link frontier and MM charges in MetatomicData from options. Electrostatic embedding data collection is in place (MM charges stored) but the per-step add_data() call to pass them to the model is deferred until a model that accepts point_charges input is available.
1 parent e45f75e commit 6312b85

1 file changed

Lines changed: 161 additions & 2 deletions

File tree

src/gromacs/applied_forces/metatomic/metatomic_forceprovider.cpp

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ struct MetatomicData
164164
//! Uncertainty threshold in kJ/mol. Atoms above this trigger a warning.
165165
double uncertaintyThreshold = 0.0;
166166

167+
//! Link frontier atoms for ONIOM link atom support.
168+
std::vector<LinkFrontierAtom> linkFrontier;
169+
//! MM point charges for electrostatic embedding (global atom index -> charge).
170+
std::vector<real> mmCharges;
171+
//! Number of link atoms (appended after real ML atoms in positions_).
172+
int32_t numLinkAtoms = 0;
173+
167174
//! Non-conservative mode: forces/stress predicted directly, no backward pass.
168175
bool nonConservative = false;
169176
//! Output keys for non-conservative forces and stress.
@@ -486,6 +493,26 @@ MetatomicForceProvider::MetatomicForceProvider(const MetatomicOptions& options,
486493
data_->nc_stress_key.c_str());
487494
}
488495

496+
// Store link frontier and MM charges from preprocessing
497+
data_->linkFrontier = options_.params_.linkFrontier_;
498+
data_->mmCharges = options_.params_.mmCharges_;
499+
data_->numLinkAtoms = static_cast<int32_t>(data_->linkFrontier.size());
500+
501+
if (!data_->linkFrontier.empty())
502+
{
503+
GMX_LOG(logger_.info)
504+
.asParagraph()
505+
.appendTextFormatted("Metatomic: %d link atoms at ML/MM boundary",
506+
data_->numLinkAtoms);
507+
}
508+
if (!data_->mmCharges.empty())
509+
{
510+
GMX_LOG(logger_.info)
511+
.asParagraph()
512+
.appendTextFormatted("Metatomic: electrostatic embedding with %zu charges",
513+
data_->mmCharges.size());
514+
}
515+
489516
GMX_LOG(logger_.info)
490517
.asParagraph()
491518
.appendText("MetatomicForceProvider initialization complete.");
@@ -1153,6 +1180,49 @@ void MetatomicForceProvider::calculateForces(const ForceProviderInput& inputs, F
11531180
}
11541181
copy_mat(inputs.box_, box_);
11551182

1183+
// Inject link atoms at ML/MM boundary bonds. Link atoms are appended
1184+
// after the real ML atoms in positions_/atomNumbers_. Their forces will
1185+
// be redistributed to the real atoms after model evaluation.
1186+
if (data_->numLinkAtoms > 0)
1187+
{
1188+
for (auto& link : data_->linkFrontier)
1189+
{
1190+
// Get embedded (ML) and MM atom positions from GROMACS coordinates.
1191+
// link.getEmbeddedIndex() and link.getMMIndex() are global indices.
1192+
const int embGmxLocal = options_.params_.mtaAtoms_->localIndex()[
1193+
std::distance(options_.params_.mtaIndices_.begin(),
1194+
std::find(options_.params_.mtaIndices_.begin(),
1195+
options_.params_.mtaIndices_.end(),
1196+
link.getEmbeddedIndex()))];
1197+
const RVec posEmb = inputs.x_[embGmxLocal];
1198+
// MM atom: use global index to find in full coordinate array
1199+
// For single rank, global index == local index
1200+
const RVec posMM = inputs.x_[link.getMMIndex()];
1201+
1202+
link.setPositions(posEmb, posMM);
1203+
RVec linkPos = link.getLinkPosition();
1204+
1205+
// Set input indices: embedded atom -> its MTA model index,
1206+
// link atom -> the index we're about to append
1207+
int32_t embMtaIdx = -1;
1208+
for (int32_t m = 0; m < numLocalMta_; ++m)
1209+
{
1210+
if (mtaToGlobalMta_[m] == link.getEmbeddedIndex())
1211+
{
1212+
embMtaIdx = m;
1213+
break;
1214+
}
1215+
}
1216+
int32_t linkMtaIdx = numLocalMta_ + static_cast<int32_t>(
1217+
&link - &data_->linkFrontier[0]);
1218+
link.setInputIndices(embMtaIdx, linkMtaIdx);
1219+
1220+
positions_.push_back(linkPos);
1221+
atomNumbers_.push_back(link.linkAtomNumber());
1222+
}
1223+
numLocalMta_ += data_->numLinkAtoms;
1224+
}
1225+
11561226
// Newton NL mode: in parallel, each rank needs ALL pairs involving its
11571227
// home atoms (not just the ones assigned by the eighth-shell DD
11581228
// decomposition). Uses the GROMACS pairlist as the pair source, then
@@ -1337,6 +1407,51 @@ void MetatomicForceProvider::calculateForces(const ForceProviderInput& inputs, F
13371407
nPairs = outIdx;
13381408
nlSamplesBuffer_.resize(nPairs * 5);
13391409
nlVectorsBuffer_.resize(nPairs * 3);
1410+
1411+
// Add pairs between link atoms and ML atoms within cutoff
1412+
if (data_->numLinkAtoms > 0)
1413+
{
1414+
const int32_t numRealMta = numLocalMta_ - data_->numLinkAtoms;
1415+
for (int32_t li = 0; li < data_->numLinkAtoms; ++li)
1416+
{
1417+
int32_t linkIdx = numRealMta + li;
1418+
RVec linkPos = positions_[linkIdx];
1419+
for (int32_t mi = 0; mi < numRealMta; ++mi)
1420+
{
1421+
RVec mlPos = positions_[mi];
1422+
double dx = linkPos[0] - mlPos[0];
1423+
double dy = linkPos[1] - mlPos[1];
1424+
double dz = linkPos[2] - mlPos[2];
1425+
double r2 = dx * dx + dy * dy + dz * dz;
1426+
if (r2 < cutoff2)
1427+
{
1428+
// link -> ml pair
1429+
nlSamplesBuffer_.push_back(linkIdx);
1430+
nlSamplesBuffer_.push_back(mi);
1431+
nlSamplesBuffer_.push_back(0);
1432+
nlSamplesBuffer_.push_back(0);
1433+
nlSamplesBuffer_.push_back(0);
1434+
nlVectorsBuffer_.push_back(-dx);
1435+
nlVectorsBuffer_.push_back(-dy);
1436+
nlVectorsBuffer_.push_back(-dz);
1437+
nPairs++;
1438+
if (fullList)
1439+
{
1440+
// ml -> link pair (reverse)
1441+
nlSamplesBuffer_.push_back(mi);
1442+
nlSamplesBuffer_.push_back(linkIdx);
1443+
nlSamplesBuffer_.push_back(0);
1444+
nlSamplesBuffer_.push_back(0);
1445+
nlSamplesBuffer_.push_back(0);
1446+
nlVectorsBuffer_.push_back(dx);
1447+
nlVectorsBuffer_.push_back(dy);
1448+
nlVectorsBuffer_.push_back(dz);
1449+
nPairs++;
1450+
}
1451+
}
1452+
}
1453+
}
1454+
}
13401455
}
13411456

13421457
// Debug: dump global pair indices + distances for comparison
@@ -1608,8 +1723,44 @@ void MetatomicForceProvider::calculateForces(const ForceProviderInput& inputs, F
16081723
}
16091724
else
16101725
{
1611-
// Serial: apply forces directly
1612-
for (int32_t i = 0; i < numLocalMta_; i++)
1726+
// Redistribute link atom forces to real atoms before scattering.
1727+
// Link atoms occupy indices [numRealMta, numLocalMta_) in the force array.
1728+
if (data_->numLinkAtoms > 0)
1729+
{
1730+
t_pbc pbc;
1731+
set_pbc(&pbc, *options_.params_.pbcType_, inputs.box_);
1732+
1733+
const int32_t numRealMta = numLocalMta_ - data_->numLinkAtoms;
1734+
for (int32_t li = 0; li < data_->numLinkAtoms; ++li)
1735+
{
1736+
auto& link = data_->linkFrontier[li];
1737+
int32_t linkIdx = numRealMta + li;
1738+
RVec fL = { static_cast<real>(forceData[3 * linkIdx]),
1739+
static_cast<real>(forceData[3 * linkIdx + 1]),
1740+
static_cast<real>(forceData[3 * linkIdx + 2]) };
1741+
auto [fEmb, fMM] = link.spreadForce(fL, pbc);
1742+
1743+
// Add redistributed force to embedded (ML) atom
1744+
int32_t embIdx = link.getInputIndexEmb();
1745+
if (embIdx >= 0 && embIdx < numRealMta)
1746+
{
1747+
int32_t gmxIdx = mtaToGmxLocal_[embIdx];
1748+
outputs->forceWithVirial_.force_[gmxIdx][0] += fEmb[0];
1749+
outputs->forceWithVirial_.force_[gmxIdx][1] += fEmb[1];
1750+
outputs->forceWithVirial_.force_[gmxIdx][2] += fEmb[2];
1751+
}
1752+
1753+
// Add redistributed force to MM atom
1754+
int32_t mmGlobalIdx = link.getMMIndex();
1755+
outputs->forceWithVirial_.force_[mmGlobalIdx][0] += fMM[0];
1756+
outputs->forceWithVirial_.force_[mmGlobalIdx][1] += fMM[1];
1757+
outputs->forceWithVirial_.force_[mmGlobalIdx][2] += fMM[2];
1758+
}
1759+
}
1760+
1761+
// Serial: apply forces for real ML atoms (excluding link atoms)
1762+
const int32_t numRealMta = numLocalMta_ - data_->numLinkAtoms;
1763+
for (int32_t i = 0; i < numRealMta; i++)
16131764
{
16141765
int32_t gmxIdx = mtaToGmxLocal_[i];
16151766
outputs->forceWithVirial_.force_[gmxIdx][0] += static_cast<real>(forceData[3 * i]);
@@ -1620,6 +1771,14 @@ void MetatomicForceProvider::calculateForces(const ForceProviderInput& inputs, F
16201771

16211772
forceScatterTimer.stop();
16221773

1774+
// Strip link atoms from arrays (they were appended in this step)
1775+
if (data_->numLinkAtoms > 0)
1776+
{
1777+
numLocalMta_ -= data_->numLinkAtoms;
1778+
positions_.resize(numLocalMta_);
1779+
atomNumbers_.resize(numLocalMta_);
1780+
}
1781+
16231782
// Restore original state. Backward ghost atoms have no GROMACS local
16241783
// buffer index, and backward pairs should not persist across steps.
16251784
if (useNewtonNL)

0 commit comments

Comments
 (0)