@@ -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 (full)
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