View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.potential.parameters;
39  
40  import ffx.potential.bonded.Atom;
41  import ffx.potential.bonded.Bond;
42  import ffx.utilities.FFXProperty;
43  import org.w3c.dom.Document;
44  import org.w3c.dom.Element;
45  
46  import java.util.ArrayList;
47  import java.util.Arrays;
48  import java.util.Collections;
49  import java.util.Comparator;
50  import java.util.HashMap;
51  import java.util.List;
52  import java.util.Objects;
53  import java.util.logging.Level;
54  import java.util.logging.Logger;
55  
56  import static ffx.potential.parameters.ForceField.ForceFieldType.POLARIZE;
57  import static ffx.utilities.Constants.ANG_TO_NM;
58  import static ffx.utilities.PropertyGroup.PotentialFunctionParameter;
59  import static java.lang.Double.parseDouble;
60  import static java.lang.Integer.parseInt;
61  import static java.lang.String.format;
62  import static java.lang.String.valueOf;
63  import static java.lang.System.arraycopy;
64  import static org.apache.commons.math3.util.FastMath.pow;
65  
66  /**
67   * The PolarizeType class defines an isotropic atomic polarizability.
68   * <p>
69   * If this modifier is not present, then charge penetration values will be used for polarization damping, as in the HIPPO polarization model.
70   *
71   * @author Michael J. Schnieders
72   * @since 1.0
73   */
74  @FFXProperty(name = "polarize", clazz = String.class, propertyGroup = PotentialFunctionParameter, description = """ 
75      [1 integer, up to 3 reals and up to 8 integers]
76      Provides the values for a single atomic dipole polarizability parameter.
77      The initial integer modifier, if positive, gives the atom type number for which a polarizability parameter is to be defined.
78      If the first integer modifier is negative, then the parameter value to follow applies only to the specific atom whose atom number is the negative of the modifier.
79      The first real number modifier gives the value of the dipole polarizability in Ang^3.
80      The second real number modifier, if present, gives the Thole damping value.
81      A Thole value of zero implies undamped polarization.
82      The third real modifier, if present, gives a direct field damping value only used with the AMOEBA+ polarization model.
83      The remaining integer modifiers list the atom type numbers of atoms directly bonded to the current atom and which will be considered to be part of the current atom’s polarization group.
84      If the parameter is for a specific atom, then the integers defining the polarization group are ignored.
85      """)
86  public final class PolarizeType extends BaseType implements Comparator<String> {
87  
88    private static final Logger logger = Logger.getLogger(PolarizeType.class.getName());
89  
90    private static final double sixth = 1.0 / 6.0;
91    public static final double DEFAULT_DIRECT_11_SCALE = 0.0;
92    public static final double DEFAULT_DIRECT_12_SCALE = 1.0;
93    public static final double DEFAULT_DIRECT_13_SCALE = 1.0;
94    public static final double DEFAULT_DIRECT_14_SCALE = 1.0;
95    public static final double DEFAULT_POLAR_12_SCALE = 0.0;
96    public static final double DEFAULT_POLAR_13_SCALE = 0.0;
97    public static final double DEFAULT_POLAR_14_SCALE = 1.0;
98    public static final double DEFAULT_POLAR_15_SCALE = 1.0;
99    public static final double DEFAULT_POLAR_12_INTRA = 0.0;
100   public static final double DEFAULT_POLAR_13_INTRA = 0.0;
101   public static final double DEFAULT_POLAR_14_INTRA = 0.5;
102   public static final double DEFAULT_POLAR_15_INTRA = 1.0;
103   /**
104    * Thole damping factor.
105    */
106   public final double thole;
107   /**
108    * Value of polarizability scale factor.
109    */
110   public double pdamp;
111   /**
112    * Direct polarization damping.
113    */
114   public final double ddp;
115   /**
116    * Isotropic polarizability in units of Angstroms^3.
117    */
118   public final double polarizability;
119   /**
120    * Atom type number.
121    */
122   public int type;
123   /**
124    * Connected types in the polarization group of each atom (can be null).
125    */
126   public int[] polarizationGroup;
127 
128   /**
129    * PolarizeType Constructor.
130    *
131    * @param atomType          The atom type.
132    * @param polarizability    The polarizability.
133    * @param thole             The Thole damping constant.
134    * @param polarizationGroup The atom types in the polarization group.
135    */
136   public PolarizeType(int atomType, double polarizability, double thole, double ddp,
137                       int[] polarizationGroup) {
138     super(POLARIZE, Integer.toString(atomType));
139     this.type = atomType;
140     this.thole = thole;
141     this.polarizability = polarizability;
142     this.ddp = 0.0;
143     this.polarizationGroup = polarizationGroup;
144     if (thole == 0.0) {
145       pdamp = 0.0;
146     } else {
147       pdamp = pow(polarizability, sixth);
148     }
149   }
150 
151   /**
152    * Construct a PolarizeType from a reference type and updated polarizability.
153    *
154    * @param polarizeType   The reference PolarizeType.
155    * @param polarizability The updated polarizability.
156    */
157   public PolarizeType(PolarizeType polarizeType, double polarizability) {
158     this(polarizeType.type, polarizability, polarizeType.thole, polarizeType.ddp,
159         Arrays.copyOf(polarizeType.polarizationGroup, polarizeType.polarizationGroup.length));
160     // Keep pdamp fixed during titration.
161     pdamp = polarizeType.pdamp;
162   }
163 
164   /**
165    * assignPolarizationGroups.
166    *
167    * @param atoms an array of {@link ffx.potential.bonded.Atom} objects.
168    * @param ip11  an array of {@link int} objects.
169    * @param ip12  an array of {@link int} objects.
170    * @param ip13  an array of {@link int} objects.
171    */
172   public static void assignPolarizationGroups(Atom[] atoms, int[][] ip11, int[][] ip12,
173                                               int[][] ip13) {
174 
175     // Find directly connected group members for each atom.
176     List<Integer> group = new ArrayList<>();
177     List<Integer> polarizationGroup = new ArrayList<>();
178     for (Atom ai : atoms) {
179       group.clear();
180       polarizationGroup.clear();
181       int index = ai.getIndex() - 1;
182       group.add(index);
183       polarizationGroup.add(ai.getType());
184       PolarizeType polarizeType = ai.getPolarizeType();
185       if (polarizeType != null) {
186         if (polarizeType.polarizationGroup != null) {
187           for (int i : polarizeType.polarizationGroup) {
188             if (!polarizationGroup.contains(i)) {
189               polarizationGroup.add(i);
190             }
191           }
192           growGroup(polarizationGroup, group, ai);
193           Collections.sort(group);
194           ip11[index] = new int[group.size()];
195           int j = 0;
196           for (int k : group) {
197             ip11[index][j++] = k;
198           }
199         } else {
200           ip11[index] = new int[group.size()];
201           int j = 0;
202           for (int k : group) {
203             ip11[index][j++] = k;
204           }
205         }
206       } else {
207         String message = "The polarize keyword was not found for atom " + (index + 1) + " with type "
208             + ai.getType();
209         logger.severe(message);
210       }
211     }
212 
213     // Find 1-2 group relationships.
214     int nAtoms = atoms.length;
215     int[] mask = new int[nAtoms];
216     List<Integer> list = new ArrayList<>();
217     List<Integer> keep = new ArrayList<>();
218     for (int i = 0; i < nAtoms; i++) {
219       mask[i] = -1;
220     }
221     for (int i = 0; i < nAtoms; i++) {
222       list.clear();
223       for (int j : ip11[i]) {
224         list.add(j);
225         mask[j] = i;
226       }
227       keep.clear();
228       for (int j : list) {
229         Atom aj = atoms[j];
230         List<Bond> bonds = aj.getBonds();
231         for (Bond b : bonds) {
232           Atom ak = b.get1_2(aj);
233           int k = ak.getIndex() - 1;
234           if (mask[k] != i) {
235             keep.add(k);
236           }
237         }
238       }
239       list.clear();
240       for (int j : keep) {
241         for (int k : ip11[j]) {
242           list.add(k);
243         }
244       }
245       Collections.sort(list);
246       ip12[i] = new int[list.size()];
247       int j = 0;
248       for (int k : list) {
249         ip12[i][j++] = k;
250       }
251     }
252 
253     // Find 1-3 group relationships.
254     for (int i = 0; i < nAtoms; i++) {
255       mask[i] = -1;
256     }
257     for (int i = 0; i < nAtoms; i++) {
258       for (int j : ip11[i]) {
259         mask[j] = i;
260       }
261       for (int j : ip12[i]) {
262         mask[j] = i;
263       }
264       list.clear();
265       for (int j : ip12[i]) {
266         for (int k : ip12[j]) {
267           if (mask[k] != i) {
268             if (!list.contains(k)) {
269               list.add(k);
270             }
271           }
272         }
273       }
274       ip13[i] = new int[list.size()];
275       Collections.sort(list);
276       int j = 0;
277       for (int k : list) {
278         ip13[i][j++] = k;
279       }
280     }
281   }
282 
283   /**
284    * Average two PolarizeType instances. The atom types to include in the new polarizationGroup must
285    * be supplied.
286    *
287    * @param polarizeType1     The first PolarizeType.
288    * @param polarizeType2     The second PolarizeType.
289    * @param atomType          The atom type to use for the new PolarizeType.
290    * @param polarizationGroup The atom types to include in the new polarizationGroup.
291    * @return The averaged PolarizeType.
292    */
293   public static PolarizeType average(PolarizeType polarizeType1, PolarizeType polarizeType2,
294                                      int atomType, int[] polarizationGroup) {
295     if (polarizeType1 == null || polarizeType2 == null) {
296       return null;
297     }
298     double thole = (polarizeType1.thole + polarizeType2.thole) / 2.0;
299     double polarizability = (polarizeType1.polarizability + polarizeType2.polarizability) / 2.0;
300     double ddp = (polarizeType1.ddp + polarizeType2.ddp) / 2.0;
301     return new PolarizeType(atomType, polarizability, thole, ddp, polarizationGroup);
302   }
303 
304   /**
305    * Construct a PolarizeType from an input string.
306    *
307    * @param input  The overall input String.
308    * @param tokens The input String tokenized.
309    * @return a PolarizeType instance.
310    */
311   public static PolarizeType parse(String input, String[] tokens) {
312     if (tokens.length < 4) {
313       logger.log(Level.WARNING, "Invalid POLARIZE type:\n{0}", input);
314     } else {
315       try {
316         int atomType = parseInt(tokens[1]);
317         double polarizability = parseDouble(tokens[2]);
318         double thole = parseDouble(tokens[3]);
319 
320         // There might be a direct polarization damping value.
321         double ddp = 0.0;
322         int nextToken = 4;
323         try {
324           // Try to parse the next token as an Int;
325           // If there is an exception then it's a direct damping value.
326           parseInt(tokens[nextToken]);
327         } catch (NumberFormatException e) {
328           ddp = parseDouble(tokens[nextToken]);
329           nextToken = 5;
330         } catch (ArrayIndexOutOfBoundsException e) {
331           // No direct damping value.
332         }
333         int entries = tokens.length - nextToken;
334         int[] polarizationGroup = null;
335         if (entries > 0) {
336           polarizationGroup = new int[entries];
337           for (int i = nextToken; i < tokens.length; i++) {
338             polarizationGroup[i - nextToken] = parseInt(tokens[i]);
339           }
340         }
341         return new PolarizeType(atomType, polarizability, thole, ddp, polarizationGroup);
342       } catch (NumberFormatException e) {
343         String message = "Exception parsing POLARIZE type:\n" + input + "\n";
344         logger.log(Level.SEVERE, message, e);
345       }
346     }
347     return null;
348   }
349 
350   /**
351    * A recursive method that checks all atoms bonded to the seed atom for inclusion in the
352    * polarization group. The method is called on each newly found group member.
353    *
354    * @param polarizationGroup Atom types that should be included in the group.
355    * @param group             XYZ indices of current group members.
356    * @param seed              The bonds of the seed atom are queried for inclusion in the group.
357    */
358   private static void growGroup(List<Integer> polarizationGroup, List<Integer> group, Atom seed) {
359     List<Bond> bonds = seed.getBonds();
360     for (Bond bi : bonds) {
361       Atom aj = bi.get1_2(seed);
362       int tj = aj.getType();
363       boolean added = false;
364       for (int g : polarizationGroup) {
365         if (g == tj) {
366           Integer index = aj.getIndex() - 1;
367           if (!group.contains(index)) {
368             group.add(index);
369             added = true;
370             break;
371           }
372         }
373       }
374       if (added) {
375         PolarizeType polarizeType = aj.getPolarizeType();
376         for (int i : polarizeType.polarizationGroup) {
377           if (!polarizationGroup.contains(i)) {
378             polarizationGroup.add(i);
379           }
380         }
381         growGroup(polarizationGroup, group, aj);
382       }
383     }
384   }
385 
386   /**
387    * A recursive method that checks all atoms bonded to the seed atom for inclusion in the
388    * polarization group. The method is called on each newly found group member.
389    *
390    * @param group XYZ indices of current group members.
391    * @param seed  The bonds of the seed atom are queried for inclusion in the group.
392    */
393   public static void growGroup(List<Integer> group, Atom seed) {
394     List<Bond> bonds = seed.getBonds();
395     for (Bond bond : bonds) {
396       Atom atom = bond.get1_2(seed);
397       int tj = atom.getType();
398       boolean added = false;
399       PolarizeType polarizeType = seed.getPolarizeType();
400       for (int type : polarizeType.polarizationGroup) {
401         if (type == tj) {
402           Integer index = atom.getIndex() - 1;
403           if (!group.contains(index)) {
404             group.add(index);
405             added = true;
406             break;
407           }
408         }
409       }
410       if (added) {
411         growGroup(group, atom);
412       }
413     }
414   }
415 
416   /**
417    * Add an atom type to the polarization group.
418    *
419    * @param atomType The atom type to add.
420    */
421   public void add(int atomType) {
422     for (int i : polarizationGroup) {
423       if (atomType == i) {
424         return;
425       }
426     }
427     int len = polarizationGroup.length;
428     int[] newGroup = new int[len + 1];
429     arraycopy(polarizationGroup, 0, newGroup, 0, len);
430     newGroup[len] = atomType;
431     polarizationGroup = newGroup;
432   }
433 
434   /**
435    * {@inheritDoc}
436    */
437   @Override
438   public int compare(String s1, String s2) {
439     int t1 = parseInt(s1);
440     int t2 = parseInt(s2);
441     return Integer.compare(t1, t2);
442   }
443 
444   /**
445    * {@inheritDoc}
446    */
447   @Override
448   public boolean equals(Object o) {
449     if (this == o) {
450       return true;
451     }
452     if (o == null || getClass() != o.getClass()) {
453       return false;
454     }
455     PolarizeType polarizeType = (PolarizeType) o;
456     return polarizeType.type == this.type;
457   }
458 
459   /**
460    * {@inheritDoc}
461    */
462   @Override
463   public int hashCode() {
464     return Objects.hash(type);
465   }
466 
467   /**
468    * {@inheritDoc}
469    *
470    * <p>Nicely formatted polarization type.
471    */
472   @Override
473   public String toString() {
474     StringBuilder polarizeString = new StringBuilder(
475         format("polarize  %5d  %8.5f %8.5f", type, polarizability, thole));
476     if (ddp != 0.0) {
477       polarizeString.append(format(" %8.5f", ddp));
478     }
479     if (polarizationGroup != null) {
480       for (int a : polarizationGroup) {
481         polarizeString.append(format("  %5d", a));
482       }
483     }
484     return polarizeString.toString();
485   }
486 
487   /**
488    * Add constant attributes to the AmoebaMultipoleForce
489    *
490    * @param node       the AmoebaMultipoleForce element.
491    * @param forceField the ForceField for collecting constants.
492    */
493   public static void addXMLAttributes(Element node, ForceField forceField) {
494     node.setAttribute("direct11Scale", valueOf(forceField.getDouble("direct-11-scale", DEFAULT_DIRECT_11_SCALE)));
495     node.setAttribute("direct12Scale", valueOf(forceField.getDouble("direct-12-scale", DEFAULT_DIRECT_12_SCALE)));
496     node.setAttribute("direct13Scale", valueOf(forceField.getDouble("direct-13-scale", DEFAULT_DIRECT_13_SCALE)));
497     node.setAttribute("direct14Scale", valueOf(forceField.getDouble("direct-14-scale", DEFAULT_DIRECT_14_SCALE)));
498     node.setAttribute("mutual11Scale", valueOf(forceField.getDouble("mutual-11-scale", 1.0)));
499     node.setAttribute("mutual12Scale", valueOf(forceField.getDouble("mutual-12-scale", 1.0)));
500     node.setAttribute("mutual13Scale", valueOf(forceField.getDouble("mutual-13-scale", 1.0)));
501     node.setAttribute("mutual14Scale", valueOf(forceField.getDouble("mutual-14-scale", 1.0)));
502     node.setAttribute("polar12Scale", valueOf(forceField.getDouble("polar-12-scale", DEFAULT_POLAR_12_SCALE)));
503     node.setAttribute("polar13Scale", valueOf(forceField.getDouble("polar-13-scale", DEFAULT_POLAR_13_SCALE)));
504     node.setAttribute("polar14Scale", valueOf(forceField.getDouble("polar-14-scale", DEFAULT_POLAR_14_SCALE)));
505     node.setAttribute("polar15Scale", valueOf(forceField.getDouble("polar-15-scale", DEFAULT_POLAR_15_SCALE)));
506     // polar-12-intra and polar-13-intra are zero by default. Other values are not supported by FFX (or OpenMM).
507     // polar-15-intra is 1.0 by default. Other values are not supported by FFX or OpenMM.
508     node.setAttribute("polar14Intra", valueOf(forceField.getDouble("polar-14-intra", DEFAULT_POLAR_14_INTRA)));
509   }
510 
511   /**
512    * Write PolarizeType to OpenMM XML format.
513    */
514   public Element toXML(Document doc) {
515     Element node = doc.createElement("Polarize");
516     node.setAttribute("type", format("%d", type));
517     // Convert Ang^3 to nm^3
518     node.setAttribute("polarizability", format("%f", polarizability * ANG_TO_NM * ANG_TO_NM * ANG_TO_NM));
519     node.setAttribute("thole", format("%f", thole));
520     // TODO: OMM does not have support for AMOEBA+ and its use of direct polarization damping (ddp)
521     int i = 1;
522     if (polarizationGroup != null) {
523       for (int a : polarizationGroup) {
524         node.setAttribute(format("pgrp%d", i), format("%d", a));
525         i++;
526       }
527     }
528     return node;
529   }
530 
531   /**
532    * Both the type and polarization group are incremented by the same amount.
533    *
534    * @param increment The increment to add to the type.
535    */
536   void incrementType(int increment) {
537     type += increment;
538     setKey(Integer.toString(type));
539     if (polarizationGroup != null) {
540       for (int i = 0; i < polarizationGroup.length; i++) {
541         polarizationGroup[i] += increment;
542       }
543     }
544   }
545 
546   /**
547    * Add mapped known types to the polarization group of a new patch.
548    *
549    * @param typeMap a lookup between new atom types and known atom types.
550    * @return a boolean.
551    */
552   boolean patchTypes(HashMap<AtomType, AtomType> typeMap) {
553     if (polarizationGroup == null) {
554       return false;
555     }
556 
557     // Append known mapped types.
558     int len = polarizationGroup.length;
559     int added = 0;
560     for (AtomType newType : typeMap.keySet()) {
561       for (int i = 1; i < len; i++) {
562         if (polarizationGroup[i] == newType.type) {
563           AtomType knownType = typeMap.get(newType);
564           added++;
565           polarizationGroup = Arrays.copyOf(polarizationGroup, len + added);
566           polarizationGroup[len + added - 1] = knownType.type;
567         }
568       }
569     }
570 
571     return added > 0;
572   }
573 }