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-2024.
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.numerics.Potential;
41  import ffx.numerics.optimization.LBFGS;
42  import ffx.numerics.optimization.LineSearch;
43  import ffx.numerics.optimization.OptimizationListener;
44  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
45  import ffx.potential.bonded.Angle;
46  import ffx.potential.bonded.AngleTorsion;
47  import ffx.potential.bonded.Atom;
48  import ffx.potential.bonded.Bond;
49  import ffx.potential.bonded.ImproperTorsion;
50  import ffx.potential.bonded.OutOfPlaneBend;
51  import ffx.potential.bonded.PiOrbitalTorsion;
52  import ffx.potential.bonded.Residue;
53  import ffx.potential.bonded.Rotamer;
54  import ffx.potential.bonded.StretchBend;
55  import ffx.potential.bonded.StretchTorsion;
56  import ffx.potential.bonded.Torsion;
57  import ffx.potential.bonded.TorsionTorsion;
58  import ffx.potential.bonded.UreyBradley;
59  import ffx.potential.parameters.MultipoleType.MultipoleFrameDefinition;
60  import ffx.potential.parameters.SoluteType.SOLUTE_RADII_TYPE;
61  import ffx.utilities.Constants;
62  
63  import java.util.Arrays;
64  import java.util.HashMap;
65  import java.util.List;
66  import java.util.logging.Level;
67  import java.util.logging.Logger;
68  
69  import static ffx.potential.bonded.AminoAcidUtils.AA_CB;
70  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.ASH;
71  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.ASP;
72  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.CYD;
73  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.CYS;
74  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.GLH;
75  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.GLU;
76  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.HID;
77  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.HIE;
78  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.HIS;
79  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.LYD;
80  import static ffx.potential.bonded.AminoAcidUtils.AminoAcid3.LYS;
81  import static ffx.potential.bonded.BondedUtils.findAtomType;
82  import static ffx.potential.parameters.MultipoleType.assignAxisAtoms;
83  import static java.lang.String.format;
84  import static org.apache.commons.math3.util.FastMath.log;
85  
86  /**
87   * Utilities for interpolating between Amino Acid protonation and tautomer states.
88   *
89   * @author Michael Schnieders
90   * @author Andrew Thiel
91   * @since 1.0
92   */
93  public class TitrationUtils {
94  
95    private static final Logger logger = Logger.getLogger(TitrationUtils.class.getName());
96  
97    private static final double LOG10 = log(10.0);
98  
99    private static final MultipoleType aspZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
100       new int[] {0, 140, 139}, MultipoleFrameDefinition.ZTHENX, false);
101   private static final MultipoleType ashZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
102       new int[] {0, 144, 143}, MultipoleFrameDefinition.ZTHENX, false);
103 
104   private static final MultipoleType gluZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
105       new int[] {0, 158, 157}, MultipoleFrameDefinition.ZTHENX, false);
106   private static final MultipoleType glhZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
107       new int[] {0, 164, 163}, MultipoleFrameDefinition.ZTHENX, false);
108 
109   private static final MultipoleType hieZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
110       new int[] {0, 130, 129}, MultipoleFrameDefinition.ZTHENX, false);
111   private static final MultipoleType hidZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
112       new int[] {0, 126, 124}, MultipoleFrameDefinition.ZTHENX, false);
113 
114   private static final MultipoleType lydZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
115       new int[] {0, 200, 198}, MultipoleFrameDefinition.ZTHENX, false);
116 
117   private static final MultipoleType cydZeroMultipoleType = new MultipoleType(MultipoleType.zeroM,
118       new int[] {0, 49, 43}, MultipoleFrameDefinition.ZTHENX, false);
119 
120   private static final PolarizeType zeroPolarizeType = new PolarizeType(0, 0.0, 0.39, 0.0,
121       new int[] {0});
122 
123   private static final SoluteType zeroSoluteType = new SoluteType(0, 1.0);
124 
125   private static final AtomType dummyHydrogenAtomType = new AtomType(0, 0, "H", "\"Dummy Hydrogen\"",
126       1, 1.0080, 1);
127 
128   private static final BondType zeroBondType = new BondType(new int[] {0, 0}, 0.0, 1.0);
129   private static final AngleType zeroAngleType = new AngleType(new int[] {0, 0, 0}, 0.0,
130       new double[] {0.0});
131   private static final StretchBendType zeroStretchBendType = new StretchBendType(new int[] {0, 0, 0},
132       new double[] {0.0, 0.0});
133   private static final OutOfPlaneBendType zeroOutOfPlaneBendType = new OutOfPlaneBendType(
134       new int[] {0, 0, 0, 0}, 0.0);
135   private static final TorsionType zeroTorsionType = new TorsionType(new int[] {0, 0, 0, 0},
136       new double[] {0.0}, new double[] {0.0}, new int[] {0});
137   private static final PiOrbitalTorsionType zeroPiOrbitalTorsionType = new PiOrbitalTorsionType(
138       new int[] {0, 0}, 0.0);
139 
140   private double proteinDielectric;
141   private boolean tanhCorrection = false;
142 
143   enum AspStates {
144     ASP, ASH1, ASH2
145   }
146 
147   /** Constant <code>AspartateAtomNames</code> */
148   private enum AspartateAtomNames {
149     CB(0, 0, 0, 0), HB2(1, 1, 1, 0), HB3(1, 1, 1, 0), CG(2, 2, 2, 0), OD1(3, 4, 3, 0), OD2(3, 3, 4,
150         0), HD1(-1, 5, -1, 1), HD2(-1, -1, 5, -1);
151 
152     /**
153      * Biotype offset relative to the CB biotype for charged aspartate (ASP).
154      */
155     private final int offsetASP;
156 
157     /**
158      * Biotype offset relative to the CB biotype for neutral aspartic acid protonated on OD1 (ASH1).
159      * <p>
160      * This is set to negative -1 for the OD2 hydrogen.
161      */
162     private final int offsetASH1;
163 
164     /**
165      * Biotype offset relative to the CB biotype for neutral aspartic acid protonated on OD2 (ASH2).
166      * <p>
167      * This is set to negative -1 for the OD1 hydrogen.
168      */
169     private final int offsetASH2;
170 
171     private final int tautomerDirection;
172 
173     public int getOffset(AspStates state) {
174       if (state == AspStates.ASP) {
175         return offsetASP;
176       } else if (state == AspStates.ASH1) {
177         return offsetASH1;
178       } else {
179         return offsetASH2;
180       }
181     }
182 
183     /**
184      * Init the Histidine atom names.
185      *
186      * @param offsetASP Biotype relative to the CB biotype for ASP.
187      * @param offsetASH1 Biotype relative to the CB biotype for ASH.
188      * @param offsetASH2 Biotype relative to the CB biotype for ASH.
189      */
190     AspartateAtomNames(int offsetASP, int offsetASH1, int offsetASH2, int tautomerDirection) {
191       this.offsetASP = offsetASP;
192       this.offsetASH1 = offsetASH1;
193       this.offsetASH2 = offsetASH2;
194       this.tautomerDirection = tautomerDirection;
195     }
196   }
197 
198   enum GluStates {
199     GLU, GLH1, GLH2
200   }
201 
202   /** Constant <code>GlutamateAtomNames</code> */
203   private enum GlutamateAtomNames {
204     CB(0, 0, 0, 0), HB2(1, 1, 1, 0), HB3(1, 1, 1, 0), CG(2, 2, 2, 0), HG2(3, 3, 3, 0), HG3(3, 3, 3,
205         0), CD(4, 4, 4, 0), OE1(5, 6, 5, 0), OE2(5, 5, 6, 0), HE1(-1, 7, -1, 1), HE2(-1, -1, 7, -1);
206 
207     /**
208      * Biotype offset relative to the CB biotype for charged Glutamate (GLU).
209      */
210     private final int offsetGLU;
211 
212     /**
213      * Biotype offset relative to the CB biotype for neutral Glutamate acid protonated on OE1
214      * (GLU1).
215      * <p>
216      * This is set to negative -1 for the OE2 hydrogen.
217      */
218     private final int offsetGLH1;
219 
220     /**
221      * Biotype offset relative to the CB biotype for neutral Glutamate acid protonated on OE2
222      * (GLU2).
223      * <p>
224      * This is set to negative -1 for the OE1 hydrogen.
225      */
226     private final int offsetGLH2;
227 
228     private final int tautomerDirection;
229 
230     public int getOffset(GluStates state) {
231       if (state == GluStates.GLU) {
232         return offsetGLU;
233       } else if (state == GluStates.GLH1) {
234         return offsetGLH1;
235       } else {
236         return offsetGLH2;
237       }
238     }
239 
240     /**
241      * Init the Glutamate atom names.
242      *
243      * @param offsetGLU Biotype relative to the CB biotype for GLU.
244      * @param offsetGLH1 Biotype relative to the CB biotype for GLH.
245      * @param offsetGLH2 Biotype relative to the CB biotype for GLH.
246      */
247     GlutamateAtomNames(int offsetGLU, int offsetGLH1, int offsetGLH2, int tautomerDirection) {
248       this.offsetGLU = offsetGLU;
249       this.offsetGLH1 = offsetGLH1;
250       this.offsetGLH2 = offsetGLH2;
251       this.tautomerDirection = tautomerDirection;
252     }
253   }
254 
255   public enum LysStates {
256     LYD, LYS
257   }
258 
259   /** Constant <code>lysineAtoms</code> */
260   public enum LysineAtomNames {
261     CB(0, 0), HB2(1, 1), HB3(1, 1), CG(2, 2), HG2(3, 3), HG3(3, 3), CD(4, 4), HD2(5, 5), HD3(5,
262         5), CE(6, 6), HE2(7, 7), HE3(7, 7), NZ(8, 8), HZ1(9, 9), HZ2(9, 9), HZ3(9, -1);
263 
264     /**
265      * Biotype offset relative to the CB biotype for LYS.
266      */
267     private final int offsetLYS;
268 
269     /**
270      * Biotype offset relative to the CB biotype for LYD.
271      */
272     private final int offsetLYD;
273 
274     public int getOffsetLYS(LysStates state) {
275       if (state == LysStates.LYS) {
276         return offsetLYS;
277       } else {
278         return offsetLYD;
279       }
280     }
281 
282     /**
283      * Init the Lysine atom names.
284      *
285      * @param offsetLYS Biotype offset relative to the CB biotype for LYS.
286      * @param offsetLYD Biotype offset relative to the CB biotype for LYD.
287      */
288     LysineAtomNames(int offsetLYS, int offsetLYD) {
289       this.offsetLYS = offsetLYS;
290       this.offsetLYD = offsetLYD;
291     }
292   }
293 
294   public enum HisStates {
295     HIS, HID, HIE
296   }
297 
298   /** Constant <code>HistidineAtoms</code> */
299   public enum HistidineAtomNames {
300     // HIS, HID, HIE
301     CB(0, 0, 0, 0), HB2(1, 1, 1, 0), HB3(1, 1, 1, 0), CG(2, 2, 2, 0), ND1(3, 3, 3,
302         0), // No HD1 proton for HIE; HIE HD1 offset is -1.
303     HD1(4, 4, -1, -1), CD2(5, 5, 4, 0), HD2(6, 6, 5, 0), CE1(7, 7, 6, 0), HE1(8, 8, 7, 0), NE2(9, 9,
304         8, 0), // No HE2 proton for HID; HID HE2 offset is -1
305     HE2(10, -1, 9, 1);
306 
307     /**
308      * Biotype offset relative to the CB biotype for charged histidine (HIS).
309      */
310     private final int offsetHIS;
311 
312     /**
313      * Biotype offset relative to the CB biotype for neutral histidine protonated on the delta
314      * nitrogren (HID).
315      * <p>
316      * This is set to negative -1 for the epsilon hydrogen.
317      */
318     private final int offsetHID;
319 
320     /**
321      * Biotype offset relative to the CB biotype for neutral histidine protonated the epsilon
322      * nitrogen (HIE).
323      * <p>
324      * This is set to negative -1 for the delta hydrogen.
325      */
326     private final int offsetHIE;
327 
328     private final int tautomerDirection;
329 
330     public int getOffsetHIS(HisStates state) {
331       if (state == HisStates.HIS) {
332         return offsetHIS;
333       } else if (state == HisStates.HID) {
334         return offsetHID;
335       } else {
336         return offsetHIE;
337       }
338     }
339 
340     /**
341      * Init the Histidine atom names.
342      *
343      * @param offsetHIS Biotype relative to the CB biotype for HIS.
344      * @param offsetHID Biotype relative to the CB biotype for HID.
345      * @param offsetHIE Biotype relative to the CB biotype for HIE.
346      */
347     HistidineAtomNames(int offsetHIS, int offsetHID, int offsetHIE, int tautomerDirection) {
348       this.offsetHIS = offsetHIS;
349       this.offsetHID = offsetHID;
350       this.offsetHIE = offsetHIE;
351       this.tautomerDirection = tautomerDirection;
352     }
353   }
354 
355   public enum CysStates {
356     CYS, CYD
357   }
358 
359   /** Constant <code>CysteineAtoms</code> */
360   public enum CysteineAtomNames {
361     CB(0, 0), HB2(1, 1), HB3(1, 1), SG(2, 2), HG(3, -1);
362 
363     /**
364      * Biotype offset relative to the CB biotype for neutral cysteine (CYS).
365      */
366     private final int offsetCYS;
367 
368     /**
369      * Biotype offset relative to the CB biotype for negatively charged cysteine (CYD).
370      * <p>
371      * This is set to negative -1 for the gamma hydrogen.
372      */
373     private final int offsetCYD;
374 
375     public int getOffsetCYS(CysStates state) {
376       if (state == CysStates.CYS) {
377         return offsetCYS;
378       } else {
379         return offsetCYD;
380       }
381     }
382 
383     /**
384      * Init the Cysteine atom names.
385      *
386      * @param offsetCYS Biotype relative to the CB biotype for CYS.
387      * @param offsetCYD Biotype relative to the CB biotype for CYD.
388      */
389     CysteineAtomNames(int offsetCYS, int offsetCYD) {
390       this.offsetCYS = offsetCYS;
391       this.offsetCYD = offsetCYD;
392     }
393   }
394 
395   /**
396    * Lysine atom types.
397    */
398   private final int nLysAtomNames = LysineAtomNames.values().length;
399   private final int nLysStates = LysStates.values().length;
400   private final AtomType[][] lysAtomTypes = new AtomType[nLysAtomNames][nLysStates];
401   private final MultipoleType[][] lysMultipoleTypes = new MultipoleType[nLysAtomNames][nLysStates];
402   private final PolarizeType[][] lysPolarizeTypes = new PolarizeType[nLysAtomNames][nLysStates];
403   private final VDWType[][] lysVDWTypes = new VDWType[nLysAtomNames][nLysStates];
404   private final SoluteType[][] lysSoluteTypes = new SoluteType[nLysAtomNames][nLysStates];
405 
406   /**
407    * Histidine atom types.
408    */
409   private final int nHisAtomNames = HistidineAtomNames.values().length;
410   private final int nHisStates = HisStates.values().length;
411   private final AtomType[][] hisAtomTypes = new AtomType[nHisAtomNames][nHisStates];
412   private final MultipoleType[][] hisMultipoleTypes = new MultipoleType[nHisAtomNames][nHisStates];
413   private final PolarizeType[][] hisPolarizeTypes = new PolarizeType[nHisAtomNames][nHisStates];
414   private final VDWType[][] hisVDWTypes = new VDWType[nHisAtomNames][nHisStates];
415   private final SoluteType[][] hisSoluteTypes = new SoluteType[nHisAtomNames][nHisStates];
416 
417   /**
418    * Aspartic acid atom types.
419    */
420   private final int nAspAtomNames = AspartateAtomNames.values().length;
421   private final int nAspStates = AspStates.values().length;
422   private final AtomType[][] aspAtomTypes = new AtomType[nAspAtomNames][nAspStates];
423   private final MultipoleType[][] aspMultipoleTypes = new MultipoleType[nAspAtomNames][nAspStates];
424   private final PolarizeType[][] aspPolarizeTypes = new PolarizeType[nAspAtomNames][nAspStates];
425   private final VDWType[][] aspVDWTypes = new VDWType[nAspAtomNames][nAspStates];
426   private final SoluteType[][] aspSoluteTypes = new SoluteType[nAspAtomNames][nAspStates];
427 
428   /**
429    * Glutamic acid atom types.
430    */
431   private final int nGluAtomNames = GlutamateAtomNames.values().length;
432   private final int nGluStates = GluStates.values().length;
433   private final AtomType[][] gluAtomTypes = new AtomType[nGluAtomNames][nGluStates];
434   private final MultipoleType[][] gluMultipoleTypes = new MultipoleType[nGluAtomNames][nGluStates];
435   private final PolarizeType[][] gluPolarizeTypes = new PolarizeType[nGluAtomNames][nGluStates];
436   private final VDWType[][] gluVDWTypes = new VDWType[nGluAtomNames][nGluStates];
437   private final SoluteType[][] gluSoluteTypes = new SoluteType[nGluAtomNames][nGluStates];
438 
439   /**
440    * Cystine atom types.
441    */
442   private final int nCysAtomNames = CysteineAtomNames.values().length;
443   private final int nCysStates = CysStates.values().length;
444   private final AtomType[][] cysAtomTypes = new AtomType[nCysAtomNames][nCysStates];
445   private final MultipoleType[][] cysMultipoleTypes = new MultipoleType[nCysAtomNames][nCysStates];
446   private final PolarizeType[][] cysPolarizeTypes = new PolarizeType[nCysAtomNames][nCysStates];
447   private final VDWType[][] cysVDWTypes = new VDWType[nCysAtomNames][nCysStates];
448   private final SoluteType[][] cysSoluteTypes = new SoluteType[nCysAtomNames][nCysStates];
449 
450   private final ForceField forceField;
451   private final SOLUTE_RADII_TYPE soluteRadiiType;
452   private final boolean updateBondedTerms;
453 
454   private final HashMap<AminoAcid3, Double> rotamerPhBiasMap = new HashMap<>();
455 
456   public TitrationUtils(ForceField forceField) {
457     this.forceField = forceField;
458 
459     String gkRadius = forceField.getString("GK_RADIUS", "SOLUTE");
460     SOLUTE_RADII_TYPE tempType;
461     try {
462       tempType = SOLUTE_RADII_TYPE.valueOf(gkRadius.trim().toUpperCase());
463     } catch (Exception e) {
464       tempType = SOLUTE_RADII_TYPE.SOLUTE;
465     }
466     soluteRadiiType = tempType;
467 
468     updateBondedTerms = forceField.getBoolean("TITRATION_UPDATE_BONDED_TERMS", true);
469 
470     // Populate the Lysine types.
471     constructLYSState(AA_CB[LYS.ordinal()], LysStates.LYS);
472     constructLYSState(AA_CB[LYD.ordinal()], LysStates.LYD);
473     checkParameterTypes("LYS", lysAtomTypes, lysPolarizeTypes, lysMultipoleTypes, lysVDWTypes);
474 
475     // Populate the Histidine types.
476     constructHISState(AA_CB[HIS.ordinal()], HisStates.HIS);
477     constructHISState(AA_CB[HID.ordinal()], HisStates.HID);
478     constructHISState(AA_CB[HIE.ordinal()], HisStates.HIE);
479     checkParameterTypes("HIS", hisAtomTypes, hisPolarizeTypes, hisMultipoleTypes, hisVDWTypes);
480 
481     // Populate the Aspartic acid types.
482     constructASPState(AA_CB[ASP.ordinal()], AspStates.ASP);
483     constructASPState(AA_CB[ASH.ordinal()], AspStates.ASH1); // First ASH Tautomer
484     constructASPState(AA_CB[ASH.ordinal()], AspStates.ASH2); // Second ASH Tautomer
485     checkParameterTypes("ASP", aspAtomTypes, aspPolarizeTypes, aspMultipoleTypes, aspVDWTypes);
486 
487     // Populate the Glutamic acid types.
488     constructGLUState(AA_CB[GLU.ordinal()], GluStates.GLU);
489     constructGLUState(AA_CB[GLH.ordinal()], GluStates.GLH1); // First GLH Tautomer
490     constructGLUState(AA_CB[GLH.ordinal()], GluStates.GLH2); // Second GLH Tautomer
491     checkParameterTypes("GLU", gluAtomTypes, gluPolarizeTypes, gluMultipoleTypes, gluVDWTypes);
492 
493     // Populate the Cystine types.
494     constructCYSState(AA_CB[CYS.ordinal()], CysStates.CYS);
495     constructCYSState(AA_CB[CYD.ordinal()], CysStates.CYD);
496     checkParameterTypes("CYS", cysAtomTypes, cysPolarizeTypes, cysMultipoleTypes, cysVDWTypes);
497   }
498 
499   public TitrationUtils(ForceField forceField, double proteinDielectric, boolean tanhCorrection){
500     this(forceField);
501     this.proteinDielectric = proteinDielectric;
502     this.tanhCorrection = tanhCorrection;
503     //String fModA = forceField.getProperties().getString("fModA");
504     //this.fModA = Double.parseDouble(fModA);
505     //logger.info("ASP fMOD = " + fModA);
506     //String fModG = forceField.getProperties().getString("fModG");
507     //this.fModG = Double.parseDouble(fModG);
508     //String fModL = forceField.getProperties().getString("fModL");
509     //this.fModL = Double.parseDouble(fModL);
510     //String fModHE = forceField.getProperties().getString("fModHE");
511     //this.fModHE = Double.parseDouble(fModHE);
512     //String fModHD = forceField.getProperties().getString("fModHD");
513     //this.fModHD = Double.parseDouble(fModHD);
514   }
515 
516   public boolean testResidueTypes(Residue residue) {
517 
518     boolean testPassed = true;
519     int nStates = 1;
520     AminoAcid3 aminoAcid3 = residue.getAminoAcid3();
521     switch (aminoAcid3) {
522       case ASP, ASH, ASD, GLU, GLH, GLD, HIS, HID, HIE -> nStates = 3;
523       case CYS, CYD, LYS, LYD -> nStates = 2;
524       default -> logger.info(format(" Only one state for atom %s.", aminoAcid3));
525     }
526 
527     List<Atom> atomList = residue.getSideChainAtoms();
528     int nAtoms = atomList.size();
529     int[][][] axisAtomIndices = new int[nAtoms][nStates][];
530     AtomType[][] atomTypes = new AtomType[nAtoms][nStates];
531     MultipoleType[][] multipoleTypes = new MultipoleType[nAtoms][nStates];
532 
533     AtomType[] initialAtomTypes = new AtomType[nAtoms];
534     MultipoleType[] initialMultipoleTypes = new MultipoleType[nAtoms];
535     // Store initial state
536     for (int i = 0; i < nAtoms; i++) {
537       Atom atom = atomList.get(i);
538       initialAtomTypes[i] = atom.getAtomType();
539       initialMultipoleTypes[i] = atom.getMultipoleType();
540     }
541 
542     // Load information for each state.
543     for (int state = 0; state < nStates; state++) {
544       // Load AtomType and MultipoleType instances for each atom for this state.
545       for (int i = 0; i < nAtoms; i++) {
546         Atom atom = atomList.get(i);
547         String atomName = atom.getName();
548         switch (aminoAcid3) {
549           case ASP:
550           case ASH:
551           case ASD:
552             int index = AspartateAtomNames.valueOf(atomName).ordinal();
553             atom.setAtomType(aspAtomTypes[index][state]);
554             atom.setMultipoleType(aspMultipoleTypes[index][state]);
555             break;
556           case CYS:
557           case CYD:
558             index = CysteineAtomNames.valueOf(atomName).ordinal();
559             atom.setAtomType(cysAtomTypes[index][state]);
560             atom.setMultipoleType(cysMultipoleTypes[index][state]);
561             break;
562           case GLU:
563           case GLH:
564           case GLD:
565             index = GlutamateAtomNames.valueOf(atomName).ordinal();
566             atom.setAtomType(gluAtomTypes[index][state]);
567             atom.setMultipoleType(gluMultipoleTypes[index][state]);
568             break;
569           case HIS:
570           case HID:
571           case HIE:
572             index = HistidineAtomNames.valueOf(atomName).ordinal();
573             atom.setAtomType(hisAtomTypes[index][state]);
574             atom.setMultipoleType(hisMultipoleTypes[index][state]);
575             break;
576           case LYS:
577           case LYD:
578             index = LysineAtomNames.valueOf(atomName).ordinal();
579             atom.setAtomType(lysAtomTypes[index][state]);
580             atom.setMultipoleType(lysMultipoleTypes[index][state]);
581             break;
582           default:
583             logger.info(format(" Only one state for atom %s.", atom));
584         }
585         atomTypes[i][state] = atom.getAtomType();
586         multipoleTypes[i][state] = atom.getMultipoleType();
587       }
588       // Assign axis atoms for each atom for this state.
589       for (int i = 0; i < nAtoms; i++) {
590         Atom atom = atomList.get(i);
591         assignAxisAtoms(atom);
592         axisAtomIndices[i][state] = atom.getAxisAtomIndices();
593       }
594     }
595 
596     // Check the local multipole frames.
597     for (int i = 0; i < nAtoms; i++) {
598       Atom atom = atomList.get(i);
599       int[] referenceIndices = axisAtomIndices[i][0];
600       AtomType referenceAtomType = atomTypes[i][0];
601       MultipoleType referenceMultipoleType = multipoleTypes[i][0];
602       for (int state = 1; state < nStates; state++) {
603         int[] stateIndices = axisAtomIndices[i][state];
604         AtomType stateAtomType = atomTypes[i][state];
605         MultipoleType stateMultipoleType = multipoleTypes[i][state];
606         if (referenceMultipoleType.frameDefinition != stateMultipoleType.frameDefinition) {
607           logger.warning(format(" Local frame definition is inconsistent for atom %s", atom));
608           logger.warning(format("  %s\n  %s", referenceAtomType, referenceMultipoleType));
609           logger.warning(format("  %s\n  %s", stateAtomType, stateMultipoleType));
610           testPassed = false;
611           continue;
612         }
613         if (Arrays.compare(referenceIndices, stateIndices) != 0) {
614           // Atom order does not matter for BISECTOR.
615           if (referenceMultipoleType.frameDefinition == MultipoleFrameDefinition.BISECTOR) {
616             if (referenceIndices[0] == stateIndices[1] && referenceIndices[1] == stateIndices[0]) {
617               continue;
618             }
619           }
620           logger.warning(format(" Local frame atom indices are inconsistent for atom %s", atom));
621           logger.warning(
622               format("  %s %s\n  %s", referenceAtomType, Arrays.toString(referenceIndices),
623                   referenceMultipoleType));
624           logger.warning(format("  %s %s\n  %s", stateAtomType, Arrays.toString(stateIndices),
625               stateMultipoleType));
626           testPassed = false;
627         }
628       }
629     }
630 
631     // Revert initial state
632     for (int i = 0; i < nAtoms; i++) {
633       Atom atom = atomList.get(i);
634       atom.setAtomType(initialAtomTypes[i]);
635       atom.setMultipoleType(initialMultipoleTypes[i]);
636     }
637 
638     return testPassed;
639   }
640 
641   /**
642    * Update force field parameters for the side-chain atoms of the given residue based on the rotamer
643    * amino acid type.
644    *
645    * @param residue Residue to update.
646    * @param rotamer Rotamer that contains the amino acid residue identity.
647    */
648   public void updateResidueParameters(Residue residue, Rotamer rotamer) {
649     if (!rotamer.isTitrating) {
650       return;
651     }
652 
653     AminoAcid3 aminoAcid3 = residue.getAminoAcid3();
654     switch (aminoAcid3) {
655       case ASH, ASP -> {
656         // Assume ASP types
657         int aspIndex = AspStates.ASP.ordinal();
658         if (rotamer.aminoAcid3 == ASH) {
659           // Use ASH2 types
660           aspIndex = AspStates.ASH2.ordinal();
661         }
662         for (AspartateAtomNames atomName : AspartateAtomNames.values()) {
663           if (atomName.name().equals("HD1")) {
664             // Skip the HD1 atom name (used only for ASD during constant pH).
665             // This atom should not be present in the residue for ASH/ASP rot opt.
666             continue;
667           }
668           int atomIndex = atomName.ordinal();
669           Atom atom = (Atom) residue.getAtomNode(atomName.name());
670           if (atom == null) {
671             logger.severe(" Atom is null for " + atomName);
672             return;
673           }
674           atom.setAtomType(aspAtomTypes[atomIndex][aspIndex]);
675           atom.setMultipoleType(aspMultipoleTypes[atomIndex][aspIndex]);
676           atom.setPolarizeType(aspPolarizeTypes[atomIndex][aspIndex]);
677           atom.setVDWType(aspVDWTypes[atomIndex][aspIndex]);
678           atom.setSoluteType(aspSoluteTypes[atomIndex][aspIndex]);
679         }
680       }
681       case GLU, GLH -> {
682         // Assume GLU types
683         int gluIndex = GluStates.GLU.ordinal();
684         if (rotamer.aminoAcid3 == GLH) {
685           // Use GLH2 types
686           gluIndex = GluStates.GLH2.ordinal();
687         }
688         for (GlutamateAtomNames atomName : GlutamateAtomNames.values()) {
689           if (atomName.name().equals("HE1")) {
690             // Skip the HE1 atom name (used only for GLD during constant pH).
691             // This atom should not be present in the residue for GLH/GLU rot opt.
692             continue;
693           }
694           int atomIndex = atomName.ordinal();
695           Atom atom = (Atom) residue.getAtomNode(atomName.name());
696           if (atom == null) {
697             logger.severe(" Atom is null for " + atomName);
698             return;
699           }
700           atom.setAtomType(gluAtomTypes[atomIndex][gluIndex]);
701           atom.setMultipoleType(gluMultipoleTypes[atomIndex][gluIndex]);
702           atom.setPolarizeType(gluPolarizeTypes[atomIndex][gluIndex]);
703           atom.setVDWType(gluVDWTypes[atomIndex][gluIndex]);
704           atom.setSoluteType(gluSoluteTypes[atomIndex][gluIndex]);
705         }
706       }
707       case LYS, LYD -> {
708         // Assume LYS types
709         int lysIndex = LysStates.LYS.ordinal();
710         if (rotamer.aminoAcid3 == LYD) {
711           // Use LYD types
712           lysIndex = LysStates.LYD.ordinal();
713         }
714         for (LysineAtomNames atomName : LysineAtomNames.values()) {
715           int atomIndex = atomName.ordinal();
716           Atom atom = (Atom) residue.getAtomNode(atomName.name());
717           if (atom == null) {
718             logger.severe(" Atom is null for " + atomName);
719             return;
720           }
721           atom.setAtomType(lysAtomTypes[atomIndex][lysIndex]);
722           atom.setMultipoleType(lysMultipoleTypes[atomIndex][lysIndex]);
723           atom.setPolarizeType(lysPolarizeTypes[atomIndex][lysIndex]);
724           atom.setVDWType(lysVDWTypes[atomIndex][lysIndex]);
725           atom.setSoluteType(lysSoluteTypes[atomIndex][lysIndex]);
726         }
727       }
728       case CYS, CYD -> {
729         // Assume CYS types
730         int cysIndex = CysStates.CYS.ordinal();
731         if (rotamer.aminoAcid3 == CYD) {
732           // Use CYD types
733           cysIndex = CysStates.CYD.ordinal();
734         }
735         for (CysteineAtomNames atomName : CysteineAtomNames.values()) {
736           int atomIndex = atomName.ordinal();
737           Atom atom = (Atom) residue.getAtomNode(atomName.name());
738           if (atom == null) {
739             logger.severe(" Atom is null for " + atomName);
740             return;
741           }
742           atom.setAtomType(cysAtomTypes[atomIndex][cysIndex]);
743           atom.setMultipoleType(cysMultipoleTypes[atomIndex][cysIndex]);
744           atom.setPolarizeType(cysPolarizeTypes[atomIndex][cysIndex]);
745           atom.setVDWType(cysVDWTypes[atomIndex][cysIndex]);
746           atom.setSoluteType(cysSoluteTypes[atomIndex][cysIndex]);
747         }
748       }
749       case HIS, HIE, HID -> {
750         // Assume HIS types.
751         int hisIndex = switch (rotamer.aminoAcid3) {
752           case HIE -> HisStates.HIE.ordinal();
753           case HID -> HisStates.HID.ordinal();
754           default -> HisStates.HIS.ordinal();
755         };
756         for (HistidineAtomNames atomName : HistidineAtomNames.values()) {
757           int atomIndex = atomName.ordinal();
758           Atom atom = (Atom) residue.getAtomNode(atomName.name());
759           if (atom == null) {
760             logger.severe(" Atom is null for " + atomName);
761             return;
762           }
763           atom.setAtomType(hisAtomTypes[atomIndex][hisIndex]);
764           atom.setMultipoleType(hisMultipoleTypes[atomIndex][hisIndex]);
765           atom.setPolarizeType(hisPolarizeTypes[atomIndex][hisIndex]);
766           atom.setVDWType(hisVDWTypes[atomIndex][hisIndex]);
767           atom.setSoluteType(hisSoluteTypes[atomIndex][hisIndex]);
768         }
769       }
770       default -> logger.severe(
771           format(" No support for titrating residue %s with rotamer %s.", residue, rotamer));
772     }
773 
774     // Update local frame defining atoms now that the AtomType and MultipoleType values are set.
775     for (Atom atom : residue.getSideChainAtoms()) {
776       assignAxisAtoms(atom);
777     }
778 
779     // Should bonded terms be updated.
780     if (!updateBondedTerms) {
781       return;
782     }
783 
784     // Update Bond force field terms.
785     for (Bond bond : residue.getBondList()) {
786       AtomType a1 = bond.getAtom(0).getAtomType();
787       AtomType a2 = bond.getAtom(1).getAtomType();
788       BondType bondType = forceField.getBondType(a1, a2);
789       if (bondType == null) {
790         bondType = zeroBondType;
791       }
792       bond.setBondType(bondType);
793     }
794 
795     // Update Angle force field terms.
796     for (Angle angle : residue.getAngleList()) {
797       AtomType a1 = angle.getAtom(0).getAtomType();
798       AtomType a2 = angle.getAtom(1).getAtomType();
799       AtomType a3 = angle.getAtom(2).getAtomType();
800       AngleType angleType = forceField.getAngleType(a1, a2, a3);
801       if (angleType == null) {
802         angleType = zeroAngleType;
803       }
804       angle.setAngleType(angleType);
805     }
806 
807     // Update Stretch-Bend force field terms.
808     for (StretchBend stretchBend : residue.getStretchBendList()) {
809       AtomType a1 = stretchBend.getAtom(0).getAtomType();
810       AtomType a2 = stretchBend.getAtom(1).getAtomType();
811       AtomType a3 = stretchBend.getAtom(2).getAtomType();
812       StretchBendType stretchBendType = forceField.getStretchBendType(a1, a2, a3);
813       if (stretchBendType == null) {
814         stretchBendType = zeroStretchBendType;
815       }
816       stretchBend.setStretchBendType(stretchBendType);
817     }
818 
819     // Update Out-of-Plane Bend force field terms.
820     for (OutOfPlaneBend outOfPlaneBend : residue.getOutOfPlaneBendList()) {
821       AtomType a4 = outOfPlaneBend.getFourthAtom().getAtomType();
822       AtomType a0 = outOfPlaneBend.getFirstAngleAtom().getAtomType();
823       AtomType a1 = outOfPlaneBend.getTrigonalAtom().getAtomType();
824       AtomType a2 = outOfPlaneBend.getLastAngleAtom().getAtomType();
825       OutOfPlaneBendType outOfPlaneBendType = forceField.getOutOfPlaneBendType(a4, a0, a1, a2);
826       if (outOfPlaneBendType == null) {
827         outOfPlaneBendType = zeroOutOfPlaneBendType;
828       }
829       outOfPlaneBend.setOutOfPlaneBendType(outOfPlaneBendType);
830     }
831 
832     // Update torsion force field terms.
833     for (Torsion torsion : residue.getTorsionList()) {
834       AtomType a1 = torsion.getAtom(0).getAtomType();
835       AtomType a2 = torsion.getAtom(1).getAtomType();
836       AtomType a3 = torsion.getAtom(2).getAtomType();
837       AtomType a4 = torsion.getAtom(3).getAtomType();
838       TorsionType torsionType = forceField.getTorsionType(a1, a2, a3, a4);
839       if (torsionType == null) {
840         torsionType = zeroTorsionType;
841       }
842       torsion.setTorsionType(torsionType);
843     }
844 
845     // Update Pi-Orbital Torsion force field terms.
846     for (PiOrbitalTorsion piOrbitalTorsion : residue.getPiOrbitalTorsionList()) {
847       Bond middleBond = piOrbitalTorsion.getMiddleBond();
848       AtomType a1 = middleBond.getAtom(0).getAtomType();
849       AtomType a2 = middleBond.getAtom(1).getAtomType();
850       PiOrbitalTorsionType piOrbitalTorsionType = forceField.getPiOrbitalTorsionType(a1, a2);
851       if (piOrbitalTorsionType == null) {
852         piOrbitalTorsionType = zeroPiOrbitalTorsionType;
853       }
854       piOrbitalTorsion.setPiOrbitalTorsionType(piOrbitalTorsionType);
855     }
856 
857     // The following terms are not supported yet.
858     List<ImproperTorsion> improperTorsions = residue.getImproperTorsionList();
859     if (improperTorsions != null && !improperTorsions.isEmpty()) {
860       logger.severe(
861           " Improper torsions are not supported yet for pH-dependent rotamer optimization.");
862     }
863 
864     List<StretchTorsion> stretchTorsions = residue.getStretchTorsionList();
865     if (stretchTorsions != null && !stretchTorsions.isEmpty()) {
866       logger.severe(
867           " Stretch-torsions are not supported yet for pH-dependent rotamer optimization.");
868     }
869 
870     List<AngleTorsion> angleTorsions = residue.getAngleTorsionList();
871     if (angleTorsions != null && !angleTorsions.isEmpty()) {
872       logger.severe(" Angle-torsions are not supported yet for pH-dependent rotamer optimization.");
873     }
874 
875     List<TorsionTorsion> torsionTorsions = residue.getTorsionTorsionList();
876     if (torsionTorsions != null && !torsionTorsions.isEmpty()) {
877       logger.severe(
878           " Torsion-torsions are not supported yet for pH-dependent rotamer optimization.");
879     }
880 
881     List<UreyBradley> ureyBradleys = residue.getUreyBradleyList();
882     if (ureyBradleys != null && !ureyBradleys.isEmpty()) {
883       logger.severe(" Urey-Bradleys are not supported yet for pH-dependent rotamer optimization.");
884     }
885 
886   }
887 
888   public double[] getMultipole(Atom atom, double titrationLambda, double tautomerLambda,
889       double[] multipole) {
890     /*
891     Step 1: retrieve the atomName from atom instance.
892     Step 2: retrieve the oridnal from the atom instance + residueType
893      */
894 
895     AminoAcid3 aminoAcid3;
896     try {
897       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
898     } catch (Exception exception) {
899       return multipole;
900     }
901     String atomName = atom.getName();
902 
903     switch (aminoAcid3) {
904       case LYS:
905         int atomIndex = LysineAtomNames.valueOf(atomName).ordinal();
906         MultipoleType lysM = lysMultipoleTypes[atomIndex][LysStates.LYS.ordinal()];
907         MultipoleType lydM = lysMultipoleTypes[atomIndex][LysStates.LYD.ordinal()];
908         double[] lys = lysM.getMultipole();
909         double[] lyd = lydM.getMultipole();
910         for (int i = 0; i < multipole.length; i++) {
911           multipole[i] = titrationLambda * lys[i] + (1.0 - titrationLambda) * lyd[i];
912         }
913         break;
914       case CYS:
915         atomIndex = CysteineAtomNames.valueOf(atomName).ordinal();
916         MultipoleType cysM = cysMultipoleTypes[atomIndex][CysStates.CYS.ordinal()];
917         MultipoleType cydM = cysMultipoleTypes[atomIndex][CysStates.CYD.ordinal()];
918         double[] cys = cysM.getMultipole();
919         double[] cyd = cydM.getMultipole();
920         for (int i = 0; i < multipole.length; i++) {
921           multipole[i] = titrationLambda * cys[i] + (1.0 - titrationLambda) * cyd[i];
922         }
923         break;
924       case HIS:
925         atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
926         MultipoleType hisM = hisMultipoleTypes[atomIndex][HisStates.HIS.ordinal()];
927         MultipoleType hidM = hisMultipoleTypes[atomIndex][HisStates.HID.ordinal()];
928         MultipoleType hieM = hisMultipoleTypes[atomIndex][HisStates.HIE.ordinal()];
929         double[] his = hisM.getMultipole();
930         double[] hid = hidM.getMultipole();
931         double[] hie = hieM.getMultipole();
932         for (int i = 0; i < multipole.length; i++) {
933           multipole[i] =
934               titrationLambda * his[i] + (1.0 - titrationLambda) * (tautomerLambda * hie[i]
935                   + (1 - tautomerLambda) * hid[i]);
936         }
937         break;
938       case ASD:
939         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
940         MultipoleType aspM = aspMultipoleTypes[atomIndex][AspStates.ASP.ordinal()];
941         MultipoleType ash1M = aspMultipoleTypes[atomIndex][AspStates.ASH1.ordinal()];
942         MultipoleType ash2M = aspMultipoleTypes[atomIndex][AspStates.ASH2.ordinal()];
943         double[] asp = aspM.getMultipole();
944         double[] ash1 = ash1M.getMultipole();
945         double[] ash2 = ash2M.getMultipole();
946         for (int i = 0; i < multipole.length; i++) {
947           multipole[i] =
948               titrationLambda * (tautomerLambda * ash1[i] + (1 - tautomerLambda) * ash2[i])
949                   + (1.0 - titrationLambda) * asp[i];
950         }
951         break;
952       case GLD:
953         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
954         MultipoleType gluM = gluMultipoleTypes[atomIndex][GluStates.GLU.ordinal()];
955         MultipoleType glh1M = gluMultipoleTypes[atomIndex][GluStates.GLH1.ordinal()];
956         MultipoleType glh2M = gluMultipoleTypes[atomIndex][GluStates.GLH2.ordinal()];
957         double[] glu = gluM.getMultipole();
958         double[] glh1 = glh1M.getMultipole();
959         double[] glh2 = glh2M.getMultipole();
960         for (int i = 0; i < multipole.length; i++) {
961           multipole[i] =
962               titrationLambda * (tautomerLambda * glh1[i] + (1 - tautomerLambda) * glh2[i])
963                   + (1.0 - titrationLambda) * glu[i];
964         }
965         break;
966       default:
967         return multipole;
968     }
969     return multipole;
970   }
971 
972   public double[] getMultipoleTitrationDeriv(Atom atom, double titrationLambda,
973       double tautomerLambda, double[] multipole) {
974     AminoAcid3 aminoAcid3;
975     try {
976       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
977     } catch (Exception exception) {
978       return multipole;
979     }
980     String atomName = atom.getName();
981     switch (aminoAcid3) {
982       case LYS:
983         int atomIndex = LysineAtomNames.valueOf(atomName).ordinal();
984         double[] lys = lysMultipoleTypes[atomIndex][LysStates.LYS.ordinal()].getMultipole();
985         double[] lyd = lysMultipoleTypes[atomIndex][LysStates.LYD.ordinal()].getMultipole();
986         for (int i = 0; i < multipole.length; i++) {
987           multipole[i] = lys[i] - lyd[i];
988         }
989         break;
990       case CYS:
991         atomIndex = CysteineAtomNames.valueOf(atomName).ordinal();
992         double[] cys = cysMultipoleTypes[atomIndex][CysStates.CYS.ordinal()].getMultipole();
993         double[] cyd = cysMultipoleTypes[atomIndex][CysStates.CYD.ordinal()].getMultipole();
994         for (int i = 0; i < multipole.length; i++) {
995           multipole[i] = cys[i] - cyd[i];
996         }
997         break;
998       case HIS:
999         atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
1000         double[] his = hisMultipoleTypes[atomIndex][HisStates.HIS.ordinal()].getMultipole();
1001         double[] hid = hisMultipoleTypes[atomIndex][HisStates.HID.ordinal()].getMultipole();
1002         double[] hie = hisMultipoleTypes[atomIndex][HisStates.HIE.ordinal()].getMultipole();
1003         for (int i = 0; i < multipole.length; i++) {
1004           multipole[i] = his[i] - (tautomerLambda * hie[i] + (1 - tautomerLambda) * hid[i]);
1005         }
1006         break;
1007       case ASD:
1008         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
1009         double[] asp = aspMultipoleTypes[atomIndex][AspStates.ASP.ordinal()].getMultipole();
1010         double[] ash1 = aspMultipoleTypes[atomIndex][AspStates.ASH1.ordinal()].getMultipole();
1011         double[] ash2 = aspMultipoleTypes[atomIndex][AspStates.ASH2.ordinal()].getMultipole();
1012         for (int i = 0; i < multipole.length; i++) {
1013           multipole[i] = (tautomerLambda * ash1[i] + (1 - tautomerLambda) * ash2[i]) - asp[i];
1014         }
1015         break;
1016       case GLD:
1017         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
1018         double[] glu = gluMultipoleTypes[atomIndex][GluStates.GLU.ordinal()].getMultipole();
1019         double[] glh1 = gluMultipoleTypes[atomIndex][GluStates.GLH1.ordinal()].getMultipole();
1020         double[] glh2 = gluMultipoleTypes[atomIndex][GluStates.GLH2.ordinal()].getMultipole();
1021         for (int i = 0; i < multipole.length; i++) {
1022           multipole[i] = (tautomerLambda * glh1[i] + (1 - tautomerLambda) * glh2[i]) - glu[i];
1023         }
1024         break;
1025       default:
1026         return multipole;
1027     }
1028     return multipole;
1029   }
1030 
1031   public double[] getMultipoleTautomerDeriv(Atom atom, double titrationLambda, double tautomerLambda,
1032       double[] multipole) {
1033     AminoAcid3 aminoAcid3;
1034     try {
1035       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
1036     } catch (Exception exception) {
1037       return multipole;
1038     }
1039     String atomName = atom.getName();
1040     switch (aminoAcid3) {
1041       case HIS:
1042         int atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
1043         double[] his = hisMultipoleTypes[atomIndex][HisStates.HIS.ordinal()].getMultipole();
1044         double[] hid = hisMultipoleTypes[atomIndex][HisStates.HID.ordinal()].getMultipole();
1045         double[] hie = hisMultipoleTypes[atomIndex][HisStates.HIE.ordinal()].getMultipole();
1046         for (int i = 0; i < multipole.length; i++) {
1047           multipole[i] = (1.0 - titrationLambda) * (hie[i] - hid[i]);
1048         }
1049         break;
1050       case ASD:
1051         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
1052         double[] asp = aspMultipoleTypes[atomIndex][AspStates.ASP.ordinal()].getMultipole();
1053         double[] ash1 = aspMultipoleTypes[atomIndex][AspStates.ASH1.ordinal()].getMultipole();
1054         double[] ash2 = aspMultipoleTypes[atomIndex][AspStates.ASH2.ordinal()].getMultipole();
1055         for (int i = 0; i < multipole.length; i++) {
1056           multipole[i] = titrationLambda * (ash1[i] - ash2[i]);
1057         }
1058         break;
1059       case GLD:
1060         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
1061         double[] glu = gluMultipoleTypes[atomIndex][GluStates.GLU.ordinal()].getMultipole();
1062         double[] glh1 = gluMultipoleTypes[atomIndex][GluStates.GLH1.ordinal()].getMultipole();
1063         double[] glh2 = gluMultipoleTypes[atomIndex][GluStates.GLH2.ordinal()].getMultipole();
1064         for (int i = 0; i < multipole.length; i++) {
1065           multipole[i] = titrationLambda * (glh1[i] - glh2[i]);
1066         }
1067         break;
1068       case LYS: // No tautomers for LYS.
1069       case CYS: // No tautomers for CYS.
1070       default:
1071         return multipole;
1072     }
1073     return multipole;
1074   }
1075 
1076   public double getPolarizability(Atom atom, double titrationLambda, double tautomerLambda,
1077       double defaultPolarizability) {
1078     AminoAcid3 aminoAcid3;
1079     try {
1080       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
1081     } catch (Exception exception) {
1082       return defaultPolarizability;
1083     }
1084     String atomName = atom.getName();
1085     switch (aminoAcid3) {
1086       case LYS:
1087         int atomIndex = LysineAtomNames.valueOf(atomName).ordinal();
1088         double lys = lysPolarizeTypes[atomIndex][LysStates.LYS.ordinal()].polarizability;
1089         double lyd = lysPolarizeTypes[atomIndex][LysStates.LYD.ordinal()].polarizability;
1090         return titrationLambda * lys + (1.0 - titrationLambda) * lyd;
1091       case CYS:
1092         atomIndex = CysteineAtomNames.valueOf(atomName).ordinal();
1093         double cys = cysPolarizeTypes[atomIndex][CysStates.CYS.ordinal()].polarizability;
1094         double cyd = cysPolarizeTypes[atomIndex][CysStates.CYD.ordinal()].polarizability;
1095         return titrationLambda * cys + (1.0 - titrationLambda) * cyd;
1096       case HIS:
1097         atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
1098         double his = hisPolarizeTypes[atomIndex][HisStates.HIS.ordinal()].polarizability;
1099         double hid = hisPolarizeTypes[atomIndex][HisStates.HID.ordinal()].polarizability;
1100         double hie = hisPolarizeTypes[atomIndex][HisStates.HIE.ordinal()].polarizability;
1101         return titrationLambda * his + (1.0 - titrationLambda) * (tautomerLambda * hie
1102             + (1.0 - tautomerLambda) * hid);
1103       case ASD:
1104         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
1105         double asp = aspPolarizeTypes[atomIndex][AspStates.ASP.ordinal()].polarizability;
1106         double ash1 = aspPolarizeTypes[atomIndex][AspStates.ASH1.ordinal()].polarizability;
1107         double ash2 = aspPolarizeTypes[atomIndex][AspStates.ASH2.ordinal()].polarizability;
1108         return titrationLambda * (tautomerLambda * ash1 + (1.0 - tautomerLambda) * ash2)
1109             + (1.0 - titrationLambda) * asp;
1110       case GLD:
1111         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
1112         double glu = gluPolarizeTypes[atomIndex][GluStates.GLU.ordinal()].polarizability;
1113         double glh1 = gluPolarizeTypes[atomIndex][GluStates.GLH1.ordinal()].polarizability;
1114         double glh2 = gluPolarizeTypes[atomIndex][GluStates.GLH2.ordinal()].polarizability;
1115         return titrationLambda * (tautomerLambda * glh1 + (1.0 - tautomerLambda) * glh2)
1116             + (1.0 - titrationLambda) * glu;
1117       default:
1118         return defaultPolarizability;
1119     }
1120   }
1121 
1122   public double getPolarizabilityTitrationDeriv(Atom atom, double titrationLambda,
1123       double tautomerLambda) {
1124     AminoAcid3 aminoAcid3;
1125     try {
1126       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
1127     } catch (Exception exception) {
1128       return 0.0;
1129     }
1130     String atomName = atom.getName();
1131     switch (aminoAcid3) {
1132       case LYS:
1133         int atomIndex = LysineAtomNames.valueOf(atomName).ordinal();
1134         double lys = lysPolarizeTypes[atomIndex][LysStates.LYS.ordinal()].polarizability;
1135         double lyd = lysPolarizeTypes[atomIndex][LysStates.LYD.ordinal()].polarizability;
1136         return lys - lyd;
1137       case CYS:
1138         atomIndex = CysteineAtomNames.valueOf(atomName).ordinal();
1139         double cys = cysPolarizeTypes[atomIndex][CysStates.CYS.ordinal()].polarizability;
1140         double cyd = cysPolarizeTypes[atomIndex][CysStates.CYD.ordinal()].polarizability;
1141         return cys - cyd;
1142       case HIS:
1143         atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
1144         double his = hisPolarizeTypes[atomIndex][HisStates.HIS.ordinal()].polarizability;
1145         double hid = hisPolarizeTypes[atomIndex][HisStates.HID.ordinal()].polarizability;
1146         double hie = hisPolarizeTypes[atomIndex][HisStates.HIE.ordinal()].polarizability;
1147         return his - (tautomerLambda * hie + (1.0 - tautomerLambda) * hid);
1148       case ASD:
1149         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
1150         double asp = aspPolarizeTypes[atomIndex][AspStates.ASP.ordinal()].polarizability;
1151         double ash1 = aspPolarizeTypes[atomIndex][AspStates.ASH1.ordinal()].polarizability;
1152         double ash2 = aspPolarizeTypes[atomIndex][AspStates.ASH2.ordinal()].polarizability;
1153         return (tautomerLambda * ash1 + (1.0 - tautomerLambda) * ash2) - asp;
1154       case GLD:
1155         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
1156         double glu = gluPolarizeTypes[atomIndex][GluStates.GLU.ordinal()].polarizability;
1157         double glh1 = gluPolarizeTypes[atomIndex][GluStates.GLH1.ordinal()].polarizability;
1158         double glh2 = gluPolarizeTypes[atomIndex][GluStates.GLH2.ordinal()].polarizability;
1159         return (tautomerLambda * glh1 + (1.0 - tautomerLambda) * glh2) - glu;
1160       default:
1161         return 0.0;
1162     }
1163   }
1164 
1165   public double getPolarizabilityTautomerDeriv(Atom atom, double titrationLambda,
1166       double tautomerLambda) {
1167     AminoAcid3 aminoAcid3;
1168     try {
1169       aminoAcid3 = atom.getMSNode(Residue.class).getAminoAcid3();
1170     } catch (Exception exception) {
1171       return 0.0;
1172     }
1173     String atomName = atom.getName();
1174     switch (aminoAcid3) {
1175       case HIS:
1176         int atomIndex = HistidineAtomNames.valueOf(atomName).ordinal();
1177         double his = hisPolarizeTypes[atomIndex][HisStates.HIS.ordinal()].polarizability;
1178         double hid = hisPolarizeTypes[atomIndex][HisStates.HID.ordinal()].polarizability;
1179         double hie = hisPolarizeTypes[atomIndex][HisStates.HIE.ordinal()].polarizability;
1180         return (1.0 - titrationLambda) * (hie - hid);
1181       case ASD:
1182         atomIndex = AspartateAtomNames.valueOf(atomName).ordinal();
1183         double asp = aspPolarizeTypes[atomIndex][AspStates.ASP.ordinal()].polarizability;
1184         double ash1 = aspPolarizeTypes[atomIndex][AspStates.ASH1.ordinal()].polarizability;
1185         double ash2 = aspPolarizeTypes[atomIndex][AspStates.ASH2.ordinal()].polarizability;
1186         return titrationLambda * (ash1 - ash2);
1187       case GLD:
1188         atomIndex = GlutamateAtomNames.valueOf(atomName).ordinal();
1189         double glu = gluPolarizeTypes[atomIndex][GluStates.GLU.ordinal()].polarizability;
1190         double glh1 = gluPolarizeTypes[atomIndex][GluStates.GLH1.ordinal()].polarizability;
1191         double glh2 = gluPolarizeTypes[atomIndex][GluStates.GLH2.ordinal()].polarizability;
1192         return titrationLambda * (glh1 - glh2);
1193       case LYS: // No tautomers for LYS.
1194       case CYS: // No tautomers for LYS.
1195       default:
1196         return 0.0;
1197     }
1198   }
1199 
1200   public static boolean isTitratingHydrogen(AminoAcid3 aminoAcid3, Atom atom) {
1201     boolean isTitratingHydrogen = false;
1202     String atomName = atom.getName();
1203     switch (aminoAcid3) {
1204       case ASD -> {
1205         if (atomName.equals(AspartateAtomNames.HD1.name()) || atomName.equals(
1206             AspartateAtomNames.HD2.name())) {
1207           isTitratingHydrogen = true;
1208         }
1209       }
1210       case GLD -> {
1211         if (atomName.equals(GlutamateAtomNames.HE1.name()) || atomName.equals(
1212             GlutamateAtomNames.HE2.name())) {
1213           isTitratingHydrogen = true;
1214         }
1215       }
1216       case HIS -> {
1217         if (atomName.equals(HistidineAtomNames.HD1.name()) || atomName.equals(
1218             HistidineAtomNames.HE2.name())) {
1219           isTitratingHydrogen = true;
1220         }
1221       }
1222       case LYS -> {
1223         if (atomName.equals(LysineAtomNames.HZ3.name())) {
1224           isTitratingHydrogen = true;
1225         }
1226       }
1227       case CYS -> {
1228         if (atomName.equals(CysteineAtomNames.HG.name())) {
1229           isTitratingHydrogen = true;
1230         }
1231       }
1232     }
1233     return isTitratingHydrogen;
1234   }
1235 
1236   /**
1237    * Used to keep track of heavy atoms with changing polarizability. Only affects carboxylic oxygen
1238    * and sulfur.
1239    *
1240    * @param aminoAcid3 The amino acid type.
1241    * @param atom The atom to check.
1242    * @return True if the atom is a heavy atom with changing polarizability.
1243    */
1244 
1245   public static boolean isTitratingHeavy(AminoAcid3 aminoAcid3, Atom atom) {
1246     boolean isTitratingHeavy = false;
1247     String atomName = atom.getName();
1248     switch (aminoAcid3) {
1249       case ASD -> {
1250         if (atomName.equals(AspartateAtomNames.OD1.name()) || atomName.equals(
1251             AspartateAtomNames.OD2.name())) {
1252           isTitratingHeavy = true;
1253         }
1254       }
1255       case GLD -> {
1256         if (atomName.equals(GlutamateAtomNames.OE1.name()) || atomName.equals(
1257             GlutamateAtomNames.OE2.name())) {
1258           isTitratingHeavy = true;
1259         }
1260       }
1261       case CYS -> {
1262         if (atomName.equals(CysteineAtomNames.SG.name())) {
1263           isTitratingHeavy = true;
1264         }
1265       }
1266     }
1267     return isTitratingHeavy;
1268   }
1269 
1270   public static int getTitratingHydrogenDirection(AminoAcid3 aminoAcid3, Atom atom) {
1271     int tautomerDirection = 0;
1272     String atomName = atom.getName();
1273     switch (aminoAcid3) {
1274       case ASD -> {
1275         if (atomName.equals(AspartateAtomNames.HD1.name())) {
1276           tautomerDirection = AspartateAtomNames.HD1.tautomerDirection;
1277         } else if (atomName.equals(AspartateAtomNames.HD2.name())) {
1278           tautomerDirection = AspartateAtomNames.HD2.tautomerDirection;
1279         }
1280       }
1281       case GLD -> {
1282         if (atomName.equals(GlutamateAtomNames.HE1.name())) {
1283           tautomerDirection = GlutamateAtomNames.HE1.tautomerDirection;
1284         } else if (atomName.equals(GlutamateAtomNames.HE2.name())) {
1285           tautomerDirection = GlutamateAtomNames.HE2.tautomerDirection;
1286         }
1287       }
1288       case HIS -> {
1289         if (atomName.equals(HistidineAtomNames.HD1.name())) {
1290           tautomerDirection = HistidineAtomNames.HD1.tautomerDirection;
1291         } else if (atomName.equals(HistidineAtomNames.HE2.name())) {
1292           tautomerDirection = HistidineAtomNames.HE2.tautomerDirection;
1293         }
1294       }
1295     }
1296     return tautomerDirection;
1297   }
1298 
1299   private void constructHISState(int biotypeCB, HisStates hisState) {
1300     int state = hisState.ordinal();
1301     for (HistidineAtomNames atomName : HistidineAtomNames.values()) {
1302       int index = atomName.ordinal();
1303       int offset = atomName.getOffsetHIS(hisState);
1304       if (offset < 0) {
1305         hisAtomTypes[index][state] = dummyHydrogenAtomType;
1306         // Zero out the MultipoleType and PolarizeType.
1307         if (hisState == HisStates.HID) {
1308           hisMultipoleTypes[index][state] = hidZeroMultipoleType;
1309         } else if (hisState == HisStates.HIE) {
1310           hisMultipoleTypes[index][state] = hieZeroMultipoleType;
1311         } else {
1312           logger.severe(" Error constructing HIS states.");
1313         }
1314         hisPolarizeTypes[index][state] = zeroPolarizeType;
1315         hisVDWTypes[index][state] = forceField.getVDWType(Integer.toString(0));
1316         hisSoluteTypes[index][state] = zeroSoluteType;
1317       } else {
1318         int biotype = biotypeCB + offset;
1319         hisAtomTypes[index][state] = findAtomType(biotype, forceField);
1320         String key = hisAtomTypes[index][state].getKey();
1321         hisMultipoleTypes[index][state] = forceField.getMultipoleTypeBeginsWith(key);
1322         hisPolarizeTypes[index][state] = forceField.getPolarizeType(key);
1323         int atomClass = hisAtomTypes[index][state].atomClass;
1324         hisVDWTypes[index][state] = forceField.getVDWType("" + atomClass);
1325         hisSoluteTypes[index][state] = getSoluteType(forceField, hisAtomTypes[index][state],
1326             hisVDWTypes[index][state]);
1327         if (hisMultipoleTypes[index][state] == null || hisPolarizeTypes[index][state] == null
1328             || hisSoluteTypes[index][state] == null) {
1329           logger.severe(format(" Titration parameters could not be assigned for His atom %s.\n %s\n",
1330               atomName, hisAtomTypes[index][state]));
1331         }
1332       }
1333     }
1334   }
1335 
1336   private void constructLYSState(int biotypeCB, LysStates lysState) {
1337     int state = lysState.ordinal();
1338     for (LysineAtomNames atomName : LysineAtomNames.values()) {
1339       int index = atomName.ordinal();
1340       int offset = atomName.getOffsetLYS(lysState);
1341       if (offset < 0) {
1342         // Set the AtomType to null.
1343         lysAtomTypes[index][state] = dummyHydrogenAtomType;
1344         // Zero out the MultipoleType and PolarizeType.
1345         lysMultipoleTypes[index][state] = lydZeroMultipoleType;
1346         lysPolarizeTypes[index][state] = zeroPolarizeType;
1347         lysVDWTypes[index][state] = forceField.getVDWType(Integer.toString(0));
1348         lysSoluteTypes[index][state] = zeroSoluteType;
1349       } else {
1350         int biotype = biotypeCB + offset;
1351         lysAtomTypes[index][state] = findAtomType(biotype, forceField);
1352         String key = lysAtomTypes[index][state].getKey();
1353         lysMultipoleTypes[index][state] = forceField.getMultipoleTypeBeginsWith(key);
1354         lysPolarizeTypes[index][state] = forceField.getPolarizeType(key);
1355         int atomClass = lysAtomTypes[index][state].atomClass;
1356         lysVDWTypes[index][state] = forceField.getVDWType("" + atomClass);
1357         lysSoluteTypes[index][state] = getSoluteType(forceField, lysAtomTypes[index][state],
1358             lysVDWTypes[index][state]);
1359         if (lysMultipoleTypes[index][state] == null || lysPolarizeTypes[index][state] == null
1360             || lysSoluteTypes[index][state] == null) {
1361           logger.severe(
1362               format(" Titration parameters could not be assigned for Lys atom %s.\n %s\n", atomName,
1363                   lysAtomTypes[index][state]));
1364         }
1365       }
1366     }
1367   }
1368 
1369   private void constructASPState(int biotypeCB, AspStates aspState) {
1370     int state = aspState.ordinal();
1371     for (AspartateAtomNames atomName : AspartateAtomNames.values()) {
1372       int index = atomName.ordinal();
1373       int offset = atomName.getOffset(aspState);
1374       if (offset < 0) {
1375         // Set the AtomType to null.
1376         aspAtomTypes[index][state] = dummyHydrogenAtomType;
1377         // Zero out the MultipoleType and PolarizeType.
1378         if (aspState == AspStates.ASP) {
1379           aspMultipoleTypes[index][state] = aspZeroMultipoleType;
1380         } else {
1381           aspMultipoleTypes[index][state] = ashZeroMultipoleType;
1382         }
1383         aspPolarizeTypes[index][state] = zeroPolarizeType;
1384         aspVDWTypes[index][state] = forceField.getVDWType(Integer.toString(0));
1385         aspSoluteTypes[index][state] = zeroSoluteType;
1386       } else {
1387         int biotype = biotypeCB + offset;
1388         aspAtomTypes[index][state] = findAtomType(biotype, forceField);
1389         String key = aspAtomTypes[index][state].getKey();
1390         aspMultipoleTypes[index][state] = forceField.getMultipoleTypeBeginsWith(key);
1391         aspPolarizeTypes[index][state] = forceField.getPolarizeType(key);
1392         int atomClass = aspAtomTypes[index][state].atomClass;
1393         aspVDWTypes[index][state] = forceField.getVDWType("" + atomClass);
1394         aspSoluteTypes[index][state] = getSoluteType(forceField, aspAtomTypes[index][state],
1395             aspVDWTypes[index][state]);
1396         if (aspMultipoleTypes[index][state] == null || aspPolarizeTypes[index][state] == null
1397             || aspSoluteTypes[index][state] == null) {
1398           logger.severe(
1399               format(" Titration parameters could not be assigned for Asp atom %s.\n %s\n", atomName,
1400                   aspAtomTypes[index][state]));
1401         }
1402       }
1403     }
1404   }
1405 
1406   private void constructGLUState(int biotypeCB, GluStates gluState) {
1407     int state = gluState.ordinal();
1408     for (GlutamateAtomNames atomName : GlutamateAtomNames.values()) {
1409       int index = atomName.ordinal();
1410       int offset = atomName.getOffset(gluState);
1411       if (offset < 0) {
1412         // Set the AtomType to null.
1413         gluAtomTypes[index][state] = dummyHydrogenAtomType;
1414         // Zero out the MultipoleType and PolarizeType.
1415         if (gluState == GluStates.GLU) {
1416           gluMultipoleTypes[index][state] = gluZeroMultipoleType;
1417         } else {
1418           gluMultipoleTypes[index][state] = glhZeroMultipoleType;
1419         }
1420         gluPolarizeTypes[index][state] = zeroPolarizeType;
1421         gluVDWTypes[index][state] = forceField.getVDWType(Integer.toString(0));
1422         gluSoluteTypes[index][state] = zeroSoluteType;
1423       } else {
1424         int biotype = biotypeCB + offset;
1425         gluAtomTypes[index][state] = findAtomType(biotype, forceField);
1426         String key = gluAtomTypes[index][state].getKey();
1427         gluMultipoleTypes[index][state] = forceField.getMultipoleTypeBeginsWith(key);
1428         gluPolarizeTypes[index][state] = forceField.getPolarizeType(key);
1429         int atomClass = gluAtomTypes[index][state].atomClass;
1430         gluVDWTypes[index][state] = forceField.getVDWType("" + atomClass);
1431         gluSoluteTypes[index][state] = getSoluteType(forceField, gluAtomTypes[index][state],
1432             gluVDWTypes[index][state]);
1433         if (gluMultipoleTypes[index][state] == null || gluPolarizeTypes[index][state] == null
1434             || gluSoluteTypes[index][state] == null) {
1435           logger.severe(
1436               format(" Titration parameters could not be assigned for Glu atom %s.\n %s\n", atomName,
1437                   gluAtomTypes[index][state]));
1438         }
1439       }
1440     }
1441   }
1442 
1443   private void constructCYSState(int biotypeCB, CysStates cysState) {
1444     int state = cysState.ordinal();
1445     for (CysteineAtomNames atomName : CysteineAtomNames.values()) {
1446       int index = atomName.ordinal();
1447       int offset = atomName.getOffsetCYS(cysState);
1448       if (offset < 0) {
1449         // Set the AtomType to null.
1450         cysAtomTypes[index][state] = dummyHydrogenAtomType;
1451         // Zero out the MultipoleType and PolarizeType.
1452         cysMultipoleTypes[index][state] = cydZeroMultipoleType;
1453         cysPolarizeTypes[index][state] = zeroPolarizeType;
1454         cysVDWTypes[index][state] = forceField.getVDWType(Integer.toString(0));
1455         cysSoluteTypes[index][state] = zeroSoluteType;
1456       } else {
1457         int biotype = biotypeCB + offset;
1458         cysAtomTypes[index][state] = findAtomType(biotype, forceField);
1459         String key = cysAtomTypes[index][state].getKey();
1460         cysMultipoleTypes[index][state] = forceField.getMultipoleTypeBeginsWith(key);
1461         // This is an edge case since the CB/HB atom types have more than 1 matching multipole
1462         if (cysMultipoleTypes[index][state] == null) {
1463           if (cysState == CysStates.CYS) {
1464             if (atomName == CysteineAtomNames.CB) {
1465               cysMultipoleTypes[index][state] = forceField.getMultipoleType(key + " 8 45");
1466             } else {
1467               // HB2 & HB3
1468               cysMultipoleTypes[index][state] = forceField.getMultipoleType(key + " 43 8");
1469             }
1470           } else {
1471             if (atomName == CysteineAtomNames.CB) {
1472               cysMultipoleTypes[index][state] = forceField.getMultipoleType(key + " 8 49");
1473             } else {
1474               // HB2 & HB3
1475               cysMultipoleTypes[index][state] = forceField.getMultipoleType(key + " 43 8");
1476             }
1477           }
1478         }
1479         cysPolarizeTypes[index][state] = forceField.getPolarizeType(key);
1480         int atomClass = cysAtomTypes[index][state].atomClass;
1481         cysVDWTypes[index][state] = forceField.getVDWType("" + atomClass);
1482         cysSoluteTypes[index][state] = getSoluteType(forceField, cysAtomTypes[index][state],
1483             cysVDWTypes[index][state]);
1484         if (cysMultipoleTypes[index][state] == null || cysPolarizeTypes[index][state] == null
1485             || cysSoluteTypes[index][state] == null) {
1486           logger.severe(
1487               format(" Titration parameters could not be assigned for Cys atom %s.\n %s\n", atomName,
1488                   cysAtomTypes[index][state]));
1489         }
1490       }
1491     }
1492   }
1493 
1494   private void checkParameterTypes(String label, AtomType[][] atomTypes,
1495       PolarizeType[][] polarizeTypes, MultipoleType[][] multipoleTypes, VDWType[][] vdwTypes) {
1496     int states = multipoleTypes.length;
1497     int types = multipoleTypes[0].length;
1498     StringBuilder sb = new StringBuilder();
1499     for (int t = 0; t < types; t++) {
1500       MultipoleFrameDefinition frame0 = multipoleTypes[0][t].frameDefinition;
1501       double eps0 = vdwTypes[0][t].wellDepth;
1502       double rad0 = vdwTypes[0][t].radius;
1503       sb.append(format("\n %s Type %d\n", label, t));
1504       sb.append(format(" %s\n  %s\n  %s\n  %s\n", atomTypes[0][t], polarizeTypes[0][t],
1505           multipoleTypes[0][t], vdwTypes[0][t]));
1506       for (int s = 1; s < states; s++) {
1507         sb.append(format(" %s\n  %s\n  %s\n  %s\n", atomTypes[s][t], polarizeTypes[s][t],
1508             multipoleTypes[s][t], vdwTypes[s][t]));
1509         MultipoleFrameDefinition frame = multipoleTypes[s][t].frameDefinition;
1510 
1511         if (!frame0.equals(frame)) {
1512           sb.append("\n Incompatible multipole frames:\n");
1513           sb.append(format(" %s\n  %s\n  %s\n", atomTypes[0][t], polarizeTypes[0][t],
1514               multipoleTypes[0][t]));
1515           sb.append(format(" %s\n  %s\n  %s\n", atomTypes[s][t], polarizeTypes[s][t],
1516               multipoleTypes[s][t]));
1517         }
1518 
1519         if (atomTypes[0][t].atomicNumber != 1) {
1520           double epsS = vdwTypes[s][t].wellDepth;
1521           double radS = vdwTypes[s][t].radius;
1522           if (epsS != eps0 || radS != rad0) {
1523             sb.append("\n Incompatible vdW types:\n");
1524             sb.append(format(" %s\n  %s\n", atomTypes[0][t], vdwTypes[0][t]));
1525             sb.append(format(" %s\n  %s\n", atomTypes[s][t], vdwTypes[s][t]));
1526           }
1527         }
1528       }
1529     }
1530 
1531     if (logger.isLoggable(Level.FINE)) {
1532       logger.fine(sb.toString());
1533     }
1534   }
1535 
1536   private SoluteType getSoluteType(ForceField forceField, AtomType atomType, VDWType vdwType) {
1537     SoluteType soluteType = SoluteType.getCensusSoluteType(atomType.atomicNumber);
1538     switch (soluteRadiiType) {
1539       case SOLUTE:
1540         SoluteType type = SoluteType.getFitSoluteType(forceField, atomType.type);
1541         if (type != null) {
1542           soluteType = type;
1543         }
1544         break;
1545       case VDW:
1546         type = SoluteType.getVDWSoluteType(vdwType);
1547         if (type != null) {
1548           soluteType = type;
1549         }
1550         break;
1551     }
1552     if (soluteType == null) {
1553       logger.severe(
1554           format(" No solute type (%s) for %d:\n  \"%s\"\n  %s", soluteRadiiType, atomType.type,
1555               atomType, vdwType));
1556     }
1557     return soluteType;
1558   }
1559 
1560   public void setRotamerPhBias(double temperature, double pH) {
1561     /*
1562      * Set ASH pH bias as sum of Fmod and acidostat energy
1563      */
1564     rotamerPhBiasMap.put(ASH, 0.0);
1565 
1566     /*
1567      * Set ASP pH bias as sum of Fmod and acidostat energy
1568      */
1569     double acidostat = LOG10 * Constants.R * temperature * (Titration.ASHtoASP.pKa - pH);
1570     double fMod = Titration.ASHtoASP.freeEnergyDiff;
1571     if(tanhCorrection){
1572       fMod = Titration.ASHtoASP.freeEnergyDiffGK;
1573       if(proteinDielectric == 2.0){
1574         fMod = Titration.ASHtoASP.freeEnergyDiffGK2;
1575       }
1576     } else if(proteinDielectric == 2.0){
1577       fMod = Titration.ASHtoASP.freeEnergyDiff2;
1578     }
1579     rotamerPhBiasMap.put(ASP, acidostat - fMod);
1580 
1581 
1582     /*
1583      * Set ASH pH bias as sum of Fmod and acidostat energy
1584      */
1585     rotamerPhBiasMap.put(GLH, 0.0);
1586 
1587     /*
1588      * Set GLU pH bias as sum of Fmod and acidostat energy
1589      */
1590     acidostat = LOG10 * Constants.R * temperature * (Titration.GLHtoGLU.pKa - pH);
1591     fMod = Titration.GLHtoGLU.freeEnergyDiff;
1592     if(tanhCorrection){
1593       fMod = Titration.GLHtoGLU.freeEnergyDiffGK;
1594       if(proteinDielectric == 2.0){
1595         fMod = Titration.GLHtoGLU.freeEnergyDiffGK2;
1596       }
1597     } else if(proteinDielectric == 2.0){
1598       fMod = Titration.GLHtoGLU.freeEnergyDiff2;
1599     }
1600     rotamerPhBiasMap.put(GLU, acidostat - fMod);
1601 
1602 
1603     /*
1604      * Set LYS pH bias as sum of Fmod and acidostat energy
1605      */
1606     rotamerPhBiasMap.put(LYS, 0.0);
1607 
1608     /*
1609      * Set LYD pH bias as sum of Fmod and acidostat energy
1610      */
1611     acidostat = LOG10 * Constants.R * temperature * (Titration.LYStoLYD.pKa - pH);
1612     fMod = Titration.LYStoLYD.freeEnergyDiff;
1613     if(tanhCorrection){
1614       fMod = Titration.LYStoLYD.freeEnergyDiffGK;
1615       if(proteinDielectric == 2.0){
1616         fMod = Titration.LYStoLYD.freeEnergyDiffGK2;
1617       }
1618     } else if(proteinDielectric == 2.0){
1619       fMod = Titration.LYStoLYD.freeEnergyDiff2;
1620     }
1621     rotamerPhBiasMap.put(LYD, acidostat - fMod);
1622 
1623     /*
1624      * Set LYS pH bias as sum of Fmod and acidostat energy
1625      */
1626     rotamerPhBiasMap.put(CYS, 0.0);
1627 
1628     /*
1629      * Set LYD pH bias as sum of Fmod and acidostat energy
1630      */
1631     acidostat = LOG10 * Constants.R * temperature * (Titration.CYStoCYD.pKa - pH);
1632     fMod = Titration.CYStoCYD.freeEnergyDiff;
1633     if(tanhCorrection){
1634       fMod = Titration.CYStoCYD.freeEnergyDiffGK;
1635       if(proteinDielectric == 2.0){
1636         fMod = Titration.CYStoCYD.freeEnergyDiffGK2;
1637       }
1638     } else if(proteinDielectric == 2.0){
1639       fMod = Titration.CYStoCYD.freeEnergyDiff2;
1640     }
1641     rotamerPhBiasMap.put(CYD, acidostat - fMod);
1642 
1643     /*
1644      * Set HIS pH bias as sum of Fmod and acidostat energy
1645      */
1646     rotamerPhBiasMap.put(HIS, 0.0);
1647 
1648     /*
1649      * Set HID pH bias as sum of Fmod and acidostat energy
1650      */
1651     acidostat = LOG10 * Constants.R * temperature * (Titration.HIStoHID.pKa - pH);
1652     fMod = Titration.HIStoHID.freeEnergyDiff;
1653     if(tanhCorrection){
1654       fMod = Titration.HIStoHID.freeEnergyDiffGK;
1655       if(proteinDielectric == 2.0){
1656         fMod = Titration.HIStoHID.freeEnergyDiffGK2;
1657       }
1658     } else if(proteinDielectric == 2.0){
1659       fMod = Titration.HIStoHID.freeEnergyDiff2;
1660     }
1661     rotamerPhBiasMap.put(HID, acidostat - fMod);
1662 
1663     /*
1664      * Set HIE pH bias as sum of Fmod and acidostat energy
1665      */
1666     acidostat = LOG10 * Constants.R * temperature * (Titration.HIStoHIE.pKa - pH);
1667     fMod = Titration.HIStoHIE.freeEnergyDiff;
1668     if(tanhCorrection){
1669       fMod = Titration.HIStoHIE.freeEnergyDiffGK;
1670       if(proteinDielectric == 2.0){
1671         fMod = Titration.HIStoHIE.freeEnergyDiffGK2;
1672       }
1673     } else if(proteinDielectric == 2.0){
1674       fMod = Titration.HIStoHIE.freeEnergyDiff2;
1675     }
1676     rotamerPhBiasMap.put(HIE, acidostat - fMod);
1677   }
1678 
1679   public double getRotamerPhBias(AminoAcid3 AA3) {
1680     return rotamerPhBiasMap.getOrDefault(AA3, 0.0);
1681   }
1682 
1683   public double getTotalRotamerPhBias(Rotamer[] rotamers) {
1684     double total = 0.0;
1685     for (Rotamer r : rotamers) {
1686       if (r.isTitrating) {
1687         total += getRotamerPhBias(r.aminoAcid3);
1688       }
1689     }
1690     return total;
1691   }
1692 
1693   /**
1694    * Predict pKa from a set of residue fractions (deprotonated / (deprotonated + protonated)). This method minimizes
1695    * the L2 loss between the the measured residue fractions and various pKa/Hill-coefficient values to get pKa/Hill-
1696    * coefficient predictions.
1697    *
1698    * @param pHScale pH values at which the residue fractions were measured
1699    * @param residueFractions a sorted array of residue fractions (deprotonated / (deprotonated + protonated))
1700    * @return {n, pKa}
1701    */
1702   public static double[] predictHillCoeffandPka(double[] pHScale, double[] residueFractions) {
1703 
1704     // Potentials for n and pKa, since LGBFS optimizer gradient is only for 1D array of gradients
1705     Potential hendersonHasselbach = new Potential() {
1706       static final double logb10 = Math.log(10);
1707 
1708       @Override
1709       public double energy(double[] x) {
1710         return leastSquaresLoss(residueFractions, getValues(x));
1711       }
1712 
1713       public double leastSquaresLoss(double[] residueFractions, double[] guessedFractions) {
1714         double loss = 0.0;
1715         for (int i = 0; i < residueFractions.length; i++) {
1716           loss += Math.pow(residueFractions[i] - guessedFractions[i], 2);
1717         }
1718         return loss;
1719       }
1720 
1721       public double[] getValues(double[] x) {
1722         double[] values = new double[pHScale.length];
1723         for (int i = 0; i < pHScale.length; i++) {
1724           values[i] = 1 / (1 + Math.pow(10, x[0] * (x[1] - pHScale[i])));
1725         }
1726         return values;
1727       }
1728 
1729       public void gradient(double[] x, double[] gradient) {
1730         // reset gradient
1731         Arrays.fill(gradient, 0.0);
1732         double[] values = getValues(x);
1733         for (int i = 0; i < pHScale.length; i++) {
1734           // term = 10^(n(pKa - pH))
1735           double term = Math.pow(10, x[0] * (x[1] - pHScale[i]));
1736 
1737           // d = (1 + term)^2
1738           double d = (1 + term) * (1 + term);
1739 
1740           // -sum(d(cost)/dn) across pH values
1741           gradient[0] += 2*(residueFractions[i] - values[i]) * logb10 * (x[1] - pHScale[i]) * term / d;
1742 
1743           // -sum(d(cost)/dpKa) across pH values
1744           gradient[1] += 2*(residueFractions[i] - values[i]) * x[0] * logb10 * term / d;
1745         }
1746       }
1747 
1748       @Override
1749       public double energyAndGradient(double[] x, double[] g) {
1750         gradient(x, g);
1751         return energy(x);
1752       }
1753 
1754       @Override
1755       public double[] getAcceleration(double[] acceleration) {
1756         return new double[0];
1757       }
1758 
1759       @Override
1760       public double[] getCoordinates(double[] parameters) {
1761         return new double[0];
1762       }
1763 
1764       @Override
1765       public STATE getEnergyTermState() {
1766         return null;
1767       }
1768 
1769       @Override
1770       public void setEnergyTermState(STATE state) {}
1771       @Override
1772       public double[] getMass() {
1773         return new double[0];
1774       }
1775 
1776       @Override
1777       public int getNumberOfVariables() {
1778         return 0;
1779       }
1780 
1781       @Override
1782       public double[] getPreviousAcceleration(double[] previousAcceleration) {
1783         return new double[0];
1784       }
1785 
1786       @Override
1787       public double[] getScaling() {
1788         return null;
1789       }
1790 
1791       @Override
1792       public void setScaling(double[] scaling) {}
1793       @Override
1794       public double getTotalEnergy() {
1795         return 0;
1796       }
1797 
1798       @Override
1799       public VARIABLE_TYPE[] getVariableTypes() {
1800         return new VARIABLE_TYPE[0];
1801       }
1802 
1803       @Override
1804       public double[] getVelocity(double[] velocity) {
1805         return new double[0];
1806       }
1807 
1808       @Override
1809       public void setAcceleration(double[] acceleration) {}
1810       @Override
1811       public void setPreviousAcceleration(double[] previousAcceleration) { }
1812       @Override
1813       public void setVelocity(double[] velocity) {}
1814     };
1815 
1816     // Call L-BFGS optimizer on hendersonHasselbach least squares potential
1817     int n = 2;
1818     double[] x = new double[]{1.0, 7.0}; // initial guess pKa
1819     int m = 3;
1820     double energy = hendersonHasselbach.energy(x);
1821     double[] grad = new double[n];
1822     hendersonHasselbach.energyAndGradient(x, grad);
1823     hendersonHasselbach.setScaling(new double[]{1.0, 1.0});
1824     double eps = 1e-5;
1825     int maxIterations = 100;
1826     OptimizationListener listener = new OptimizationListener() {
1827       @Override
1828       public boolean optimizationUpdate(int iter, int nBFGS, int nFunctionEvals, double gradientRMS,
1829                                         double coordinateRMS, double f, double df, double angle,
1830                                         LineSearch.LineSearchResult info) {
1831         return true;
1832       }
1833     };
1834 
1835     int statuspKa = LBFGS.minimize(n, m, x, energy, grad, eps, maxIterations,
1836             hendersonHasselbach, listener);
1837 
1838     return new double[]{x[0], x[1]};
1839   }
1840 
1841   /**
1842    * Amino acid protonation reactions. Constructors below specify intrinsic pKa and reference free
1843    * energy of protonation, obtained via BAR on capped monomers. pKa values from Thurlkill, Richard
1844    * L., et al. "pK values of the ionizable groups of proteins." Protein science 15.5 (2006):
1845    * 1214-1218.
1846    * <p>
1847    * HIS to HID/HIE pKa values from Bashford, Donald, et al. "Electrostatic calculations of
1848    * side-chain pKa values in myoglobin and comparison with NMR data for histidines." Biochemistry
1849    * 32.31 (1993): 8045-8056.
1850    * <p>
1851    * ASP value from Grimsley, Gerald R., J. Martin Scholtz, and C. Nick Pace.
1852    * "A summary of the measured pK values of the ionizable groups in folded proteins."
1853    * Protein Science 18.1 (2009): 247-251.
1854    * <p>
1855    * 2023 Fmod values
1856    * ASP: -71.10
1857    * GLU: -83.40
1858    * LYS: 41.77
1859    * CYS: -66.2
1860    * HID: 40.20
1861    * HIE: 37.44
1862    * <p>
1863    * March 2024 Fmod values from R. Gogal
1864    * ASP: -70.35
1865    * GLU: -81.90
1866    * LYS: 41.45
1867    * CYS: -59.5
1868    * HID: 40.20
1869    * HIE: 37.44
1870    */
1871   public enum Titration {
1872 
1873     ASHtoASP(3.94, -70.35, -35.35,-45.39, -23.15, 12.730, -107.430, 166.369, 0.0, AminoAcid3.ASH, AminoAcid3.ASP),
1874     ASH1toASH2(Double.NaN, 0.00,0,0,0, 0.0, -35.305, 35.305, 0.0, AminoAcid3.ASH, AminoAcid3.ASH),
1875     GLHtoGLU(4.25, -81.90, -39.71, -55.74, -26.3,28.024, -131.270, 189.980, 0.0,  AminoAcid3.GLH, AminoAcid3.GLU),
1876     GLH1toGLH2(Double.NaN, 0,0,0,0.00, 0.0, -29.395, 29.395, 0.0, AminoAcid3.GLH, AminoAcid3.GLH),
1877     LYStoLYD(10.40, 41.35, 20.0, 41.31, 19.7,6.752, -78.804, 26.894, 0.0, AminoAcid3.LYS, AminoAcid3.LYD),
1878     //TYRtoTYD(10.07, 34.961, 0.0, AminoAcidUtils.AminoAcid3.TYR, AminoAcidUtils.AminoAcid3.TYD),
1879     CYStoCYD(8.55, -53.1, 0.0, 0.0, 0.0,44.247, -183.990, 226.710, 0.0, AminoAcid3.CYS, AminoAcid3.CYD), //HE2 is the proton that is lost
1880     HIStoHID(7.00, 40.20, 20.92, 36.65, 18.83, 0.0, -64.317, 30.350, 0.0,  AminoAcid3.HIS, AminoAcid3.HID), //HD1 is the proton that is lost
1881     HIStoHIE(6.60, 37.44, 18.14, 34.65, 17.95,0.0, -62.931, 32.000, 0.0,AminoAcid3.HIS, AminoAcid3.HIE),
1882     HIDtoHIE(Double.NaN, 0.00, 0.0, 0.0, 0.0,0.0, -36.830, 34.325, 0.0,AminoAcid3.HID, AminoAcid3.HIE);
1883     //TerminalNH3toNH2(8.23, 0.0, 00.00, AminoAcidUtils.AminoAcid3.UNK, AminoAcidUtils.AminoAcid3.UNK),
1884     //TerminalCOOHtoCOO(3.55, 0.0, 00.00, AminoAcidUtils.AminoAcid3.UNK, AminoAcidUtils.AminoAcid3.UNK);
1885 
1886 
1887     public final double pKa;
1888     // Free energy differences used in rotamer optimization
1889     public final double freeEnergyDiff;
1890     public final double freeEnergyDiff2;
1891     public final double freeEnergyDiffGK;
1892     public final double freeEnergyDiffGK2;
1893     public final double cubic;
1894     public final double quadratic;
1895     public final double linear;
1896     public final double offset;
1897     public final AminoAcid3 protForm;
1898     public final AminoAcid3 deprotForm;
1899 
1900     /** Invoked by Enum; use the factory method to obtain instances. */
1901 
1902     Titration(double pKa, double freeEnergyDiff,double freeEnergyDiff2, double freeEnergyDiffGK,
1903               double freeEnergyDiffGK2, double cubic, double quadratic, double linear, double offset,
1904         AminoAcid3 protForm, AminoAcid3 deprotForm) {
1905       this.pKa = pKa;
1906       this.freeEnergyDiff = freeEnergyDiff;
1907       this.freeEnergyDiff2 = freeEnergyDiff2;
1908       this.freeEnergyDiffGK = freeEnergyDiffGK;
1909       this.freeEnergyDiffGK2 = freeEnergyDiffGK2;
1910       this.cubic = cubic;
1911       this.quadratic = quadratic;
1912       this.linear = linear;
1913       this.offset = offset;
1914       this.protForm = protForm;
1915       this.deprotForm = deprotForm;
1916     }
1917   }
1918 }