8000 Variable distance-based displacements for ATMForce by egallicc · Pull Request #4776 · openmm/openmm · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Variable distance-based displacements for ATMForce #4776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

egallicc
Copy link
Contributor

These changes allow the specification of particle displacements based on the vector distances between given particles. For example, this sets the displacement of particle 0 as the vector distance of particle 2 relative to particle 1:

atmforce->addParticle(2, 1);
atmforce->addParticle(Vec3(0, 0, 0));
atmfiorce->addParticle(Vec3(0, 0, 0));

After the coordinate transformation, the position of particle 0 is

pos[0] + (pos[2]-pos[1])

The extension serves two purposes:

The extension is backward compatible with the original fixed-lab displacements without API changes.

Questions, comments, and suggestions are welcome.

Copy link
Member
@peastman peastman left a comment

Choose a reason for hiding this comment

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

Can you format the code to be consistent with the rest of OpenMM? That includes

  • Indentation is by four spaces, not two.
  • There's always a space before {.
  • else goes on a separate line from }.
  • There shouldn't be spaces just inside parentheses.

So for example,

}else{

should become

}
else {

and

atm->addParticle( 2,  1 );

should become

atm->addParticle(2,  1);

Comment on lines 100 to 102
* atm->addParticle(2, 1);
* atm->addParticle(Vec3(0, 0, 0));
* atm->addParticle(Vec3(0, 0, 0));
Copy link
Member

Choose a reason for hiding this comment

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

Is this intended to illustrate a typical use? If so, I don't understand it. I can see displacing every particle along the same direction, to move a ligand in a direction defined in internal coordinates. But when would you move some particles in a relative direction and others in a fixed direction? And when would you ever want to specify a displacement of (0, 0, 0)?

Do you also need a distance scale? I would think you would often want to use two particles to specify a direction, but then specify the distance to move separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this example, the second and third particles are stationary. That is, they are not displaced. This is the case for most of the particles in binding applications. For example, the receptor and solvent atoms are not displaced and are entered in ATMForce with zero displacement.

True, fixed and particle distance displacements are unlikely to be used together in the same application. It is one or the other. Nevertheless, stationary particles have to be specified somehow. Maybe we should have an addParticle() method for that (no arguments).

Copy link
Member

Choose a reason for hiding this comment

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

What about a distance? Will the displacement to apply always be exactly equal to the displacement between two other particles, or will it sometimes be a fixed distance in the direction defined by two particles?

I guess this depends on what the use cases are. If the goal is to move it from one pocket to a different one, you would probably use the displacement between them. If the goal is to move it into solvent, you would probably want a fixed distance, and use the two other particles to determine which direction is "outward".

What are the intended use cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, as of now, we do not have a use case for a "direction + magnitude" displacement mode.

The immediate application of the new protocol is for the calculation of the relative binding free energy (RBFE) between two congeneric ligands that share a common core and differ in a peripheral side group attached to an anchor atom of the common core. These include macromolecular ligands that differ by the sidechain of one residue. This involves:

  • The displacement of the side group of one ligand to the position of the anchor atom of the other ligand and vice versa. For example, if the indexes of the anchor atoms of the two ligands are a1 and a2, each atom of the side group of the first ligand is assigned a displacement pos[a2] - pos[a1], which of course varies with time depending on the positions of the anchor atoms.
  • The swapping of the coordinates of the corresponding atoms of the common cores of the two ligands to preserve the structures of the bonded terms. So if atoms i and j are corresponding atoms, atom i is displaced by pos[j]-pos[i], which results in atom i taking the coordinates of atom j.

Furthermore, we envision that the new variable distance-based displacements will eventually replace the current fixed lab-frame displacements for the other two main use cases of ATMForce for molecular binding:

  • Absolute binding free energies (ABFE): where the ligand is displaced from a position in the solvent to the receptor binding site (or the other way around for dissociation). This is currently implemented as a fixed lab-frame displacement of the ligand, which could be replaced with a variable distance-based displacement between a reference atom of the ligand and a virtual particle placed in the solvent and attached to the receptor by restraints expressed in internal coordinates.
  • Relative binding free energies between unrelated ligands (scaffold hopping). These are currently implemented using a fixed lab-frame displacement similar to ABFE, except that the two ligands are displaced in opposite directions. We intend to replace these fixed displacements with variable displacements based on the distance between two reference atoms of the two ligands. Here, the user will pick the two reference atoms rather than a fixed displacem 10000 ent. But we have not tried this yet.

So, I think we will eventually stop using fixed lab-frame displacements in our work.

Copy link
Member

Choose a reason for hiding this comment

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

Will this change be sufficient to address those and all other future use cases you can imagine?

I'd much rather err on the side of generality and change the API only once, rather than keep changing incrementally. The latter is likely to produce an incoherent API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The most general coordinate transformation I can think of is one in which the coordinates of a particle are some arbitrary function of the positions of some particles:

newpos[i] = f_i( i_1, i_2, i_3, ... )

Where, for example,

f_i = pos[i] + h

would correspond to the existing fixed lab-frame displacements and

f_i = pos[i] + (pos[j] - pos[k])

to the variable displacement/coordinate swapping transformation proposed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shall we hold off on incorporating this extension in the mainline code for the time being as we gather more experience with variable displacements? A few months from now, we should have a stronger basis to formulate a solid proposal.

Copy link
Member

Choose a reason for hiding this comment

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

Here's what I suggest as an API. This will be extensible, so we can add new types of displacements in the future without breaking compatibility.

  • Create a class called ATMForce::ParticleDisplacement to describe how a particle is displaced. This is an abstract class. Subclasses describe particular types of displacements.
  • Create subclasses for the particular types of displacements we want to implement. ATMForce::FixedDisplacement will describe the current behavior of fixed lab frame displacements. Other subclasses can describe other types of displacements.
  • Add a new version of addParticle() that takes a ParticleDisplacement object, along with getParticleDisplacement() and setParticleDisplacement() methods. Your example code would become
atm->addParticle(new ATMForce::FixedDisplacement(Vec3(0, 0, 0)));
  • Mark the current addParticle(), getParticleParameters(), and setParticleParameters() methods as deprecated. They'll still work, but people will be encouraged to use the new methods instead. Internally they'll create a FixedDisplacement. If you call getParticleParameters() on a particle that uses a different displacement type, it will throw an exception.
  • The serialization code will need to be revised. Currently it records a particle like this:
particles.createChildNode("Particle").setDoubleProperty("d1x", d1[0]).setDoubleProperty("d1y", d1[1]).setDoubleProperty("d1z", d1[2])
        .setDoubleProperty("d0x", d0[0]).setDoubleProperty("d0y", d0[1]).setDoubleProperty("d0z", d0[2]);

We can add a new type property to specify which displacement class to use, and all the other properties will be specific to the displacement class. When reading old files, it will assume type="fixed".

A few months from now, we should have a stronger basis to formulate a solid proposal.

Sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A few months from now, we should have a stronger basis to formulate a solid proposal.

Sounds good.

Agreed.

Here's what I suggest as an API.

Yes, it looks like a good extensible solution. However, I am unsure how efficiently it could be implemented for GPU platforms. I guess particles can be sorted into groups depending on their transformations, and then kernels would transform the coordinates and gradients for each group. Using each transformation's type parameter should help with the grouping.

Copy link
Member

Choose a reason for hiding this comment

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

Efficiency shouldn't be a problem. Within the kernel we'll still just be handling a fixed set of hardcoded operations.

Comment on lines 8082 to 8085
vector<int> pj1Vector(cc.getPaddedNumAtoms());
vector<int> pi1Vector(cc.getPaddedNumAtoms());
vector<int> pj0Vector(cc.getPaddedNumAtoms());
vector<int> pi0Vector(cc.getPaddedNumAtoms());
Copy link
Member

Choose a reason for hiding this comment

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

Using four separate arrays like this will lead to inefficient memory access in the kernels. If you pack them into a single int4 array, it will be faster.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I will do that.

@egallicc
Copy link
Contributor Author

Can you format the code to be consistent with the rest of OpenMM? That includes

Yes, of course, I will address all of that. Thank you for reviewing.

@egallicc
Copy link
Contributor Author

A quick update on our progress. The results of tests of internal coordinate displacements look promising. We will do more to confirm the validity of the approach.

Concerning the proposed extensible API:

Add a new version of addParticle() that takes a ParticleDisplacement object, along with getParticleDisplacement() and setParticleDisplacement() methods.

Would a Force object make sense in this context? For example, a CustomExternalForce with "x+h" as an expression could be used to translate an atom by some fixed amount or just "x" to assign the coordinate of one atom to another. More complicated transformations involving the coordinates of multiple atoms could be given in terms of a CustomCompoundBondForce or similar.

I don't know how custom Forces could be used to implement the actual coordinate transformation internally. (Is it even possible?) However, all the information about the transformation can be encoded and queried using the Force objects with existing APIs—just a thought.

@peastman
Copy link
Member

I was figuring on something much simpler than that. There would still only be a small list of supported displacement types, initially perhaps only two. And each of them would be hardcoded in the kernel. The public API is more general so we can add other types in the future, but those too would probably be specialized, hardcoded types.

@peastman
Copy link
Member

It is, of course, conceivable that we could have a type that involves a custom expression for the position. We would need to figure out what the expression could depend on.

@egallicc
Copy link
Contributor Author

I have completed a draft of the new ATMForce interface proposed above, which maintains the current API while deprecating it.

It defines transformation objects: ATMTransformation (a base virtual class), and ATMFixedDisplacement and ATMVectordistanceDisplacement (derived classes), which hold the parameters for each coordinate transformation. ATMFixedDisplacement refers to the fixed lab frame displacements that we supported so far, and ATMVectordistanceDisplacement refers to the variable particle distance displacements introduced above.

To create a stationary particle, one that is not displaced, do:

atmforce->addParticle();

A particle with fixed displacement is created with something like:

atmforce->addParticle(new ATMFixedDisplacement(d));

where d is the Vec3 displacement vector.

Finally, a particle with variable particle distance displacement is created with, for example:

atmforce->addParticle(new ATMVectordistanceDisplacement(1, 0));

to set the displacement of the added particle as the vector distance between particles 1 and 0.

The new API also supports setting and getting the transformation objects of existing particles:

const ATMTransformation* transformation = getParticleTransformation(i); 

which can be queried for the corresponding parameters:

if ( transformation->getName() == "FixedDisplacement") {
   const Vec3 d1 = dynamic_cast<const ATMFixedDisplacement*>(transformation)->getFixedDisplacement1();
   const Vec3 d0 = dynamic_cast<const ATMFixedDisplacement*>(transformation)->getFixedDisplacement0();
}

and similarly for ATMVectordistanceDisplacement, which is labeled as "VectordistanceDisplacement"

Two maps, ATMTransformationType and ATMTransformationName, enable mapping from transformation names to the numerical transformation types used in serialization.

The current API:

atmforce->addParticle(d);
atmforce->getParticleParameters(i, d1, d0);
etc.

is supported but generates a deprecation warning when the first particle is created.

@egallicc
Copy link
Contributor Author
egallicc commented Jun 20, 2025

NOTES:

  • Building of the C wrappers currently fails (hence the CI build failures), complaining about conflicting declarations of addParticle. I don't know why. I assume the C wrappers support function overloading? Turning off OPENMM_BUILD_C_AND_FORTRAN_WRAPPERS lets the build proceed without problems (tested on Linux).
  • The addParticle(ATMFixedDisplacement(d)) and addParticle(ATMVectordistanceDisplacement(i,j)) Python wrappers fail at runtime, probably due to class ownership. I could not add addParticle to the STEAL_OWNERSHIP list in swigInputConfig.py because it does not appear to support overloaded functions. As a workaround, particles can be added from Python by first creating them and then adding their transformation, taking advantage that setParticleTransformation is in the STEAL_OWNERSHIP list:
p = atmforce.addParticle()
atmforce.setParticleTransformation(p, ATMFixedDisplacement(d))
  • I attempted but failed to add the ATMTransformation objects as members of the ATMForce class because I could not find a way to have SWIG assign units to the output of a member class. For example, I set
("ATMFixedDisplacement", "getFixedDisplacement1") : ("unit.nanometer", ()),

in swigInputConfig.py but something like the below did not work

("ATMForce::ATMFixedDisplacement", "getFixedDisplacement1") : ("unit.nanometer", ()),
  • The SWIG interface would not build unless I set a "precedence" in the Vec3 typemaps. In typemaps.i I did:
%typemap(typecheck, precedence=SWIG_TYPECHECK_DOUBLE_ARRAY, fragment="Py_SequenceToVec3") Vec3 { ...

I hope it is fine.

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
0