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.bonded;
39  
40  import static ffx.potential.bonded.AminoAcidUtils.AA1toAA3;
41  import static ffx.potential.bonded.NucleicAcidUtils.NA1toNA3;
42  import static java.lang.System.arraycopy;
43  
44  import ffx.potential.bonded.AminoAcidUtils.AminoAcid1;
45  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
46  import ffx.potential.bonded.NucleicAcidUtils.NucleicAcid1;
47  import ffx.potential.bonded.NucleicAcidUtils.NucleicAcid3;
48  import ffx.potential.parameters.ForceField;
49  import ffx.potential.parameters.TitrationUtils;
50  
51  import java.io.Serial;
52  import java.util.ArrayList;
53  import java.util.Comparator;
54  import java.util.HashMap;
55  import java.util.List;
56  import java.util.Objects;
57  import java.util.logging.Level;
58  import java.util.logging.Logger;
59  import org.jogamp.java3d.Canvas3D;
60  import org.jogamp.java3d.J3DGraphics2D;
61  import org.jogamp.java3d.Material;
62  import org.jogamp.java3d.Node;
63  import org.jogamp.vecmath.Color3f;
64  import org.jogamp.vecmath.Point2d;
65  import org.jogamp.vecmath.Point3d;
66  
67  /**
68   * The Residue class represents individual amino acids or nucleic acid bases.
69   *
70   * @author Michael J. Schnieders
71   * @author Jacob M. Litman
72   * @since 1.0
73   */
74  public class Residue extends MSGroup implements Comparable<Residue> {
75  
76    @Serial
77    private static final long serialVersionUID = 1L;
78  
79    private static final Logger logger = Logger.getLogger(Residue.class.getName());
80    /** Compare residues first on seg ID, then residue number, then residue type, then name. */
81    private static final Comparator<Residue> resComparator = Comparator.comparing(Residue::getSegID)
82        .thenComparingInt(Residue::getResidueNumber).thenComparing(Residue::getResidueType)
83        .thenComparing(Residue::getName);
84  
85    /** Constant <code>NA3Color</code> */
86    private static final HashMap<NucleicAcid3, Color3f> NA3Color = new HashMap<>();
87    /** Constant <code>AA3Color</code> */
88    private static final HashMap<AminoAcid3, Color3f> AA3Color = new HashMap<>();
89    /** Constant <code>SSTypeColor</code> */
90    private static final HashMap<SSType, Color3f> SSTypeColor = new HashMap<>();
91    /** Constant <code>Ramachandran="new String[17]"</code> */
92    public static String[] Ramachandran = new String[17];
93  
94    private static final Point3d point3d = new Point3d();
95    private static final Point2d point2d = new Point2d();
96  
97    static {
98      NA3Color.put(NucleicAcid3.ADE, RendererCache.RED);
99      NA3Color.put(NucleicAcid3.CYT, RendererCache.MAGENTA);
100     NA3Color.put(NucleicAcid3.GUA, RendererCache.BLUE);
101     NA3Color.put(NucleicAcid3.URI, RendererCache.YELLOW);
102     NA3Color.put(NucleicAcid3.DAD, RendererCache.RED);
103     NA3Color.put(NucleicAcid3.DCY, RendererCache.MAGENTA);
104     NA3Color.put(NucleicAcid3.DGU, RendererCache.BLUE);
105     NA3Color.put(NucleicAcid3.DTY, RendererCache.ORANGE);
106     NA3Color.put(NucleicAcid3.MP1, RendererCache.GREEN);
107     NA3Color.put(NucleicAcid3.DP2, RendererCache.GREEN);
108     NA3Color.put(NucleicAcid3.TP3, RendererCache.GREEN);
109     NA3Color.put(NucleicAcid3.UNK, RendererCache.CYAN);
110 
111     AA3Color.put(AminoAcid3.ALA, RendererCache.GRAY);
112     AA3Color.put(AminoAcid3.ARG, RendererCache.BLUE);
113     AA3Color.put(AminoAcid3.ASN, RendererCache.BLUE);
114     AA3Color.put(AminoAcid3.ASP, RendererCache.RED);
115     AA3Color.put(AminoAcid3.CYS, RendererCache.YELLOW);
116     AA3Color.put(AminoAcid3.GLN, RendererCache.BLUE);
117     AA3Color.put(AminoAcid3.GLU, RendererCache.RED);
118     AA3Color.put(AminoAcid3.GLH, RendererCache.RED);
119     AA3Color.put(AminoAcid3.GLD, RendererCache.RED);
120     AA3Color.put(AminoAcid3.GLY, RendererCache.GRAY);
121     AA3Color.put(AminoAcid3.ILE, RendererCache.GRAY);
122     AA3Color.put(AminoAcid3.LEU, RendererCache.GRAY);
123     AA3Color.put(AminoAcid3.LYS, RendererCache.BLUE);
124     AA3Color.put(AminoAcid3.MET, RendererCache.YELLOW);
125     AA3Color.put(AminoAcid3.PHE, RendererCache.GREEN);
126     AA3Color.put(AminoAcid3.PRO, RendererCache.ORANGE);
127     AA3Color.put(AminoAcid3.SER, RendererCache.BLUE);
128     AA3Color.put(AminoAcid3.THR, RendererCache.BLUE);
129     AA3Color.put(AminoAcid3.TRP, RendererCache.GREEN);
130     AA3Color.put(AminoAcid3.TYR, RendererCache.GREEN);
131     AA3Color.put(AminoAcid3.VAL, RendererCache.GRAY);
132     AA3Color.put(AminoAcid3.HIS, RendererCache.BLUE);
133     AA3Color.put(AminoAcid3.HIE, RendererCache.BLUE);
134     AA3Color.put(AminoAcid3.HID, RendererCache.BLUE);
135     AA3Color.put(AminoAcid3.ORN, RendererCache.ORANGE);
136     AA3Color.put(AminoAcid3.AIB, RendererCache.ORANGE);
137     AA3Color.put(AminoAcid3.PCA, RendererCache.ORANGE);
138     AA3Color.put(AminoAcid3.FOR, RendererCache.RED);
139     AA3Color.put(AminoAcid3.ACE, RendererCache.RED);
140     AA3Color.put(AminoAcid3.NH2, RendererCache.BLUE);
141     AA3Color.put(AminoAcid3.NME, RendererCache.BLUE);
142     AA3Color.put(AminoAcid3.UNK, RendererCache.MAGENTA);
143 
144     SSTypeColor.put(SSType.NONE, RendererCache.WHITE);
145     SSTypeColor.put(SSType.SHEET, RendererCache.PINK);
146     SSTypeColor.put(SSType.HELIX, RendererCache.BLUE);
147     SSTypeColor.put(SSType.TURN, RendererCache.YELLOW);
148 
149     Ramachandran[0] = "Default (Extended)       [-135.0  135.0]";
150     Ramachandran[1] = "Alpha Helix (R)          [ -57.0  -47.0]";
151     Ramachandran[2] = "Alpha Helix (L)          [  57.0   47.0]";
152     Ramachandran[3] = "3-10 Helix               [ -49.0  -26.0]";
153     Ramachandran[4] = "Pi Helix                 [ -57.0  -70.0]";
154     Ramachandran[5] = "Polyproline II Helix     [ -79.0  149.0]";
155     Ramachandran[6] = "Parallel Beta Strand     [-119.0  113.0]";
156     Ramachandran[7] = "Antiparallel Beta Strand [-139.0  135.0]";
157     Ramachandran[8] = "Beta-Hairpin 2' (i+1)    [  90.0 -170.0]";
158     Ramachandran[9] = "Beta-Hairpin 2' (i+2)    [ -80.0  -10.0]";
159     Ramachandran[10] = "Beta-Hairpin 1' (i+1)    [  57.0   47.0]";
160     Ramachandran[11] = "Beta-Hairpin 1' (i+2)    [  57.0   47.0]";
161     Ramachandran[12] = "Beta-Hairpin 1  (i+1)    [ -57.0  -47.0]";
162     Ramachandran[13] = "Beta-Hairpin 1  (i+2)    [ -57.0  -47.0]";
163     Ramachandran[14] = "Beta-Hairpin 1  (i+3)    [  90.0 -170.0]";
164     Ramachandran[15] = "Beta-Hairpin 3' (i+1)    [  57.0   47.0]";
165     Ramachandran[16] = "Beta-Hairpin 3' (i+2)    [ -80.0  -10.0]";
166   }
167 
168   /** Residue type. */
169   ResidueType residueType;
170   /** The residue number of this residue in a chain. */
171   private int resNumber;
172   /** Possibly redundant PDB chain ID. */
173   private Character chainID;
174   /** Unique segID. */
175   private String segID;
176   /** The rotamers for this residue. */
177   private Rotamer[] rotamers = null;
178   /** The current rotamer in use. */
179   private Rotamer currentRotamer = null;
180   /** Short string describing this residue. */
181   private String shortString = null;
182   /** 3-letter amino acid code. */
183   private AminoAcid3 aa;
184   /** 3-letter nucleic acid code. */
185   private NucleicAcid3 na;
186   /**
187    * If this is set, then ASP, GLU, LYS and HIS Rotamers will be titratable.
188    */
189   private TitrationUtils titrationUtils = null;
190 
191   /**
192    * These arrays store default coordinates for certain atoms in nucleic acid Residues. C1', O4', and
193    * C4' are the critical sugar atoms off which every other atom is drawn when applyRotamer is
194    * called. The backbone corrections, however, move these atoms, so they must be reverted to their
195    * original coordinates each time applyRotamer is called.
196    *
197    * <p>O3' North and South coordinates are technically non-essential, as they could be derived from
198    * C1', O4', C4', and a given sugar pucker, however, it is much less computationally expensive to
199    * calculate them once and then store them.
200    *
201    * <p>TODO: Add O3' coordinates for the DNA C3'-exo configuration.
202    */
203   private double[] O3sNorthCoords, O3sSouthCoords, C1sCoords, O4sCoords, C4sCoords;
204 
205   /**
206    * Default Constructor where num is this Residue's position in the Polymer.
207    *
208    * @param num The residue number.
209    * @param residueType The residue type.
210    */
211   public Residue(int num, ResidueType residueType) {
212     super();
213     resNumber = num;
214     this.residueType = residueType;
215     assignResidueType();
216   }
217 
218   /**
219    * Constructor for Residue.
220    *
221    * @param name The residue name.
222    * @param residueType The residue type.
223    */
224   public Residue(String name, ResidueType residueType) {
225     super(name);
226     this.residueType = residueType;
227     assignResidueType();
228   }
229 
230   /**
231    * Name is the residue's 3 letter abbreviation and num is its position in the Polymer.
232    *
233    * @param name The residue name.
234    * @param num The residue number.
235    * @param residueType The residue type.
236    */
237   public Residue(String name, int num, ResidueType residueType) {
238     this(name, residueType);
239     resNumber = num;
240   }
241 
242   /**
243    * Name is the residue's 3 letter abbreviation and num is its position in the Polymer.
244    *
245    * @param name The residue name.
246    * @param resNumber The residue number.
247    * @param residueType The residue type.
248    * @param chainID The chain ID.
249    * @param segID The segment ID.
250    */
251   public Residue(String name, int resNumber, ResidueType residueType, Character chainID,
252       String segID) {
253     this(name, residueType);
254     this.resNumber = resNumber;
255     this.chainID = chainID;
256     this.segID = segID;
257   }
258 
259   /**
260    * As above, with atoms being a MSNode with this Residue's atoms as child nodes
261    *
262    * @param name The residue name.
263    * @param resNumber The residue number.
264    * @param residueType The residue type.
265    * @param atoms a {@link ffx.potential.bonded.MSNode} object.
266    * @param forceField the ForceField to use when created bonded terms.
267    */
268   public Residue(String name, int resNumber, MSNode atoms, ResidueType residueType,
269       ForceField forceField) {
270     super(name, atoms);
271     this.resNumber = resNumber;
272     this.residueType = residueType;
273     assignResidueType();
274     finalize(true, forceField);
275   }
276 
277   @Override
278   public void setName(String name) {
279     super.setName(name);
280     if (residueType != null) {
281       assignResidueType();
282     }
283     for (Atom atom : getAtomList()) {
284       atom.setResName(name);
285     }
286   }
287 
288   /**
289    * {@inheritDoc}
290    *
291    * <p>Allows adding Atoms to the Residue.
292    */
293   @Override
294   public MSNode addMSNode(MSNode o) {
295     Atom currentAtom = null;
296     if (o instanceof Atom newAtom) {
297       Character newAlt = newAtom.getAltLoc();
298       MSNode atoms = getAtomNode();
299       currentAtom = (Atom) atoms.contains(newAtom);
300       if (currentAtom == null) {
301         currentAtom = newAtom;
302         currentAtom.setResName(getName());
303         currentAtom.setResidueNumber(resNumber);
304         atoms.add(newAtom);
305         setFinalized(false);
306       } else {
307         // Allow overwriting of the root alternate conformer (' ' or 'A').
308         Character currentAlt = currentAtom.getAltLoc();
309         if (currentAlt.equals(' ') || currentAlt.equals('A')) {
310           if (!newAlt.equals(' ') && !newAlt.equals('A')) {
311             newAtom.setXyzIndex(currentAtom.getXyzIndex());
312             atoms.remove(currentAtom);
313             currentAtom = newAtom;
314 
315             currentAtom.setResName(getName());
316             currentAtom.setResidueNumber(resNumber);
317 
318             atoms.add(currentAtom);
319             setFinalized(false);
320           }
321         }
322       }
323     } else {
324       logger.warning("Only an Atom can be added to a Residue.");
325     }
326     return currentAtom;
327   }
328 
329   @Override
330   public int compareTo(Residue o) {
331     if (this.equals(o)) {
332       return 0;
333     }
334     return resComparator.compare(this, o);
335   }
336 
337   /** {@inheritDoc} */
338   @Override
339   public void drawLabel(Canvas3D canvas, J3DGraphics2D g2d, Node node) {
340     if (RendererCache.labelResidues) {
341       double[] d = getCenter();
342       point3d.x = d[0];
343       point3d.y = d[1];
344       point3d.z = d[2];
345       RendererCache.getScreenCoordinate(canvas, node, point3d, point2d);
346       g2d.drawString(getName(), (float) point2d.x, (float) point2d.y);
347     }
348     if (RendererCache.labelAtoms) {
349       super.drawLabel(canvas, g2d, node);
350     }
351   }
352 
353   /** {@inheritDoc} */
354   @Override
355   public boolean equals(Object o) {
356     if (this == o) {
357       return true;
358     }
359     if (o == null || getClass() != o.getClass()) {
360       return false;
361     }
362     Residue residue = (Residue) o;
363     return Objects.equals(segID, residue.segID)
364         && Objects.equals(getResidueNumber(), residue.getResidueNumber())
365         && residueType == residue.residueType && Objects.equals(getName(), residue.getName());
366   }
367 
368   /**
369    * {@inheritDoc}
370    *
371    * <p>The Finalize method should be called once all atoms have been added to the Residue. Geometry
372    * objects (Bonds, Angles, etc) are then formed, followed by a determination of under-constrained
373    * (Dangling) atoms.
374    */
375   @Override
376   public void finalize(boolean finalizeGeometry, ForceField forceField) {
377     setFinalized(false);
378     getAtomNode().setName("Atoms (" + getAtomList().size() + ")");
379     if (finalizeGeometry) {
380       assignBondedTerms(forceField);
381       removeLeaves();
382     }
383     setCenter(getMultiScaleCenter(false));
384     setFinalized(true);
385   }
386 
387   public void setTitrationUtils(TitrationUtils titrationUtils) {
388     this.titrationUtils = titrationUtils;
389   }
390 
391   public TitrationUtils getTitrationUtils() {
392     return titrationUtils;
393   }
394 
395   /**
396    * getAminoAcid3.
397    *
398    * @return a {@link AminoAcid3} object.
399    */
400   public AminoAcid3 getAminoAcid3() {
401     if (this.residueType != ResidueType.AA) {
402       throw new IllegalArgumentException(
403           String.format(" This residue is " + "not an amino acid: %s", this));
404     }
405     return aa;
406   }
407 
408   /**
409    * Returns a list of backbone atoms; for our purposes, nucleic acid backbone atoms are those of the
410    * nucleobase.
411    *
412    * @return List of backbone (or nucleobase) atoms.
413    */
414   public List<Atom> getBackboneAtoms() {
415     List<Atom> atoms = getAtomList();
416     List<Atom> ret;
417     switch (residueType) {
418       case NA -> {
419         ret = new ArrayList<>(atoms);
420         for (Atom atom : atoms) {
421           String name = atom.getName().toUpperCase();
422           if (name.contains("'") || name.equals("P") || name.startsWith("OP") || name.equals("H5T")
423               || name.equals("H3T")) {
424             ret.remove(atom);
425           }
426         }
427         return ret;
428       }
429       case AA -> {
430         ret = new ArrayList<>();
431         tryAddAtom(ret, "N");
432         tryAddAtom(ret, "CA");
433         tryAddAtom(ret, "C");
434         tryAddAtom(ret, "O");
435         tryAddAtom(ret, "OXT"); // C-terminal residues
436         tryAddAtom(ret, "OT2"); // Probably alternate name for OXT.
437         tryAddAtom(ret, "H1"); // N-terminal residues
438         tryAddAtom(ret, "H2");
439         tryAddAtom(ret, "H3");
440         tryAddAtom(ret, "H");
441         tryAddAtom(ret, "HA");
442         tryAddAtom(ret, "HA2"); // Glycines
443         tryAddAtom(ret, "HA3");
444         return ret;
445       }
446       default -> {
447         return new ArrayList<>(1); // Return empty list.
448       }
449     }
450   }
451 
452   /**
453    * Returns this Residues Parent Polymer name.
454    *
455    * @return a {@link java.lang.Character} object.
456    */
457   public Character getChainID() {
458     return chainID;
459   }
460 
461   /**
462    * Setter for the field <code>chainID</code>.
463    *
464    * @param c a {@link java.lang.Character} object.
465    */
466   public void setChainID(Character c) {
467     chainID = c;
468 
469     for (Atom atom : getAtomList()) {
470       atom.setChainID(c);
471     }
472   }
473 
474   /**
475    * Setter for the field <code>segID</code>.
476    *
477    * @param segID The segID to use.
478    */
479   public void setSegID(String segID) {
480     this.segID = segID;
481 
482     for (Atom atom : getAtomList()) {
483       atom.setSegID(segID);
484     }
485   }
486 
487   /**
488    * Returns the Residue bonded to this Residue at this Residue's 3' or C-terminal end. Any use of
489    * this method to add Residues to a sliding window or similar MUST not add that residue if that
490    * residue has no Rotamers, as several algorithms (such as the distance matrix) assume that all
491    * Residues being optimized have Rotamers.
492    *
493    * @return The next Residue.
494    */
495   public Residue getNextResidue() {
496     switch (residueType) {
497       case AA -> {
498         Atom carbon = (Atom) getAtomNode("C");
499         if (carbon == null) {
500           return null;
501         }
502         List<Bond> bonds = carbon.getBonds();
503         for (Bond b : bonds) {
504           Atom other = b.get1_2(carbon);
505           if (other.getName().equalsIgnoreCase("N")) {
506             return (Residue) other.getParent().getParent();
507           }
508         }
509       }
510       case NA -> {
511         Atom oxygen = (Atom) getAtomNode("O3'");
512         if (oxygen == null) {
513           return null;
514         }
515         List<Bond> bonds = oxygen.getBonds();
516         for (Bond b : bonds) {
517           Atom other = b.get1_2(oxygen);
518           if (other.getName().equalsIgnoreCase("P")) {
519             return (Residue) other.getParent().getParent();
520           }
521         }
522       }
523       default -> {
524         return null;
525       }
526     }
527     return null;
528     // Will generally indicate that you passed in a chain-terminal residue.
529   }
530 
531   /**
532    * getNucleicAcid3.
533    *
534    * @return a {@link NucleicAcid3} object.
535    */
536   public NucleicAcid3 getNucleicAcid3() {
537     if (this.residueType != ResidueType.NA) {
538       throw new IllegalArgumentException(
539           String.format(" This residue is not a nucleic acid: %s", this));
540     }
541     return na;
542   }
543 
544   /**
545    * Returns the NucleicAcid3 corresponding to this Residue, with additional robust checking for 1-
546    * or 2-letter names.
547    *
548    * @param matchShortName Try to match 1- or 2-letter names (e.g. A to ADE).
549    * @return a {@link NucleicAcid3} object.
550    */
551   public NucleicAcid3 getNucleicAcid3(boolean matchShortName) {
552     NucleicAcid3 na3 = getNucleicAcid3();
553     if (na3 == NucleicAcidUtils.NucleicAcid3.UNK && matchShortName) {
554       switch (getName()) {
555         case "A" -> {
556           return NucleicAcid3.ADE;
557         }
558         case "C" -> {
559           return NucleicAcid3.CYT;
560         }
561         case "G" -> {
562           return NucleicAcid3.GUA;
563         }
564         case "T" -> {
565           return NucleicAcid3.THY;
566         }
567         case "U" -> {
568           return NucleicAcid3.URI;
569         }
570         case "DA" -> {
571           return NucleicAcid3.DAD;
572         }
573         case "DC" -> {
574           return NucleicAcid3.DCY;
575         }
576         case "DG" -> {
577           return NucleicAcid3.DGU;
578         }
579         case "DT" -> {
580           return NucleicAcid3.DTY;
581         }
582         case "DU" -> throw new IllegalArgumentException(
583             " No NucleicAcid3 enum exists for DU (presumed to be deoxy-uracil)!");
584       }
585     }
586     return na3;
587   }
588 
589   /**
590    * Returns the Residue bonded to this Residue at this Residue's 5' or N-terminal end. Any use of
591    * this method to add Residues to a sliding window or similar MUST not add that residue if that
592    * residue has no Rotamers, as several algorithms (such as the distance matrix) assume that all
593    * Residues being optimized have Rotamers.
594    *
595    * @return The previous Residue.
596    */
597   public Residue getPreviousResidue() {
598     switch (residueType) {
599       case AA -> {
600         Atom nitrogen = (Atom) getAtomNode("N");
601         if (nitrogen == null) {
602           return null;
603         }
604         List<Bond> bonds = nitrogen.getBonds();
605         for (Bond b : bonds) {
606           Atom other = b.get1_2(nitrogen);
607           if (other.getName().equalsIgnoreCase("C")) {
608             return (Residue) other.getParent().getParent();
609           }
610         }
611       }
612       case NA -> {
613         Atom phosphate = (Atom) getAtomNode("P");
614         if (phosphate == null) {
615           return null;
616         }
617         List<Bond> bonds = phosphate.getBonds();
618         for (Bond b : bonds) {
619           Atom other = b.get1_2(phosphate);
620           if (other.getName().equalsIgnoreCase("O3'")) {
621             return (Residue) other.getParent().getParent();
622           }
623         }
624       }
625       default -> {
626         return null;
627       }
628     }
629     return null;
630     // Will generally indicate that you passed in a chain-starting residue.
631   }
632 
633   /**
634    * Returns a reference Atom for a Residue, primarily intended for rough distance calculations. This
635    * atom should be roughly centrally located within the residue, and be invariant.
636    *
637    * @return A reference Atom.
638    */
639   public Atom getReferenceAtom() {
640     Atom atom = null;
641     switch (this.getResidueType()) {
642       case AA -> atom = (Atom) this.getAtomNode("CA");
643       case NA -> {
644         // If pyrimidine, atom will be N1.  Else, if purine,
645         // N1 will return null, so grab N9.
646         atom = (Atom) this.getAtomNode("N1");
647         if (atom == null) {
648           atom = (Atom) this.getAtomNode("N9");
649         }
650       }
651       default -> {
652       }
653     }
654     if (atom == null) {
655       atom = (Atom) this.getAtomNode(0);
656     }
657     return atom;
658   }
659 
660   /**
661    * Returns this Residue's sequence number.
662    *
663    * @return The sequence number.
664    */
665   public int getResidueNumber() {
666     return resNumber;
667   }
668 
669   /**
670    * Getter for the field <code>residueType</code>.
671    *
672    * @return a {@link ResidueType} object.
673    */
674   public ResidueType getResidueType() {
675     return residueType;
676   }
677 
678   /**
679    * Get the current rotamer.
680    *
681    * @return a {@link ffx.potential.bonded.Rotamer} object.
682    */
683   public Rotamer getRotamer() {
684     return currentRotamer;
685   }
686 
687   /**
688    * Set the current rotamer.
689    *
690    * @param rotamer a {@link ffx.potential.bonded.Rotamer} object.
691    */
692   public void setRotamer(Rotamer rotamer) {
693     this.currentRotamer = rotamer;
694   }
695 
696   /**
697    * Return all currently set rotamers.
698    *
699    * @return All current rotamers for this residue.
700    */
701   public Rotamer[] getRotamers() {
702     return rotamers;
703   }
704 
705   /**
706    * Resets the rotamers for this residue, potentially incorporating the original coordinates if
707    * RotamerLibrary's original coordinates rotamer flag has been set.
708    * <p>
709    * Any rotamers that were set previously are deleted.
710    *
711    * @param library Rotamer library to use
712    * @return An array of Rotamer.
713    */
714   public Rotamer[] setRotamers(RotamerLibrary library) {
715 
716     // Obtain rotamers for this residue from the RotamerLibrary.
717     Rotamer[] libRotamers = library.getRotamers(this, titrationUtils);
718     rotamers = libRotamers;
719 
720     // Add the original coordinates as a rotamer if requested
721     // (unless the library has no rotamers for this residue).
722     if (library.getUsingOrigCoordsRotamer() && rotamers != null) {
723       // Define the current coordinates as a new rotamer.
724       Rotamer[] originalRotamers = Rotamer.defaultRotamerFactory(this, titrationUtils);
725       int nOrig = originalRotamers.length;
726       // Add the original rotamers to those from the library and cache the result.
727       int libRots = libRotamers.length;
728       rotamers = new Rotamer[libRots + nOrig];
729       // First rotamers are the original conformation.
730       arraycopy(originalRotamers, 0, rotamers, 0, nOrig);
731       // Copy in the library.
732       arraycopy(libRotamers, 0, rotamers, nOrig, libRots);
733     }
734 
735     return rotamers;
736   }
737 
738   /**
739    * Sets the original coordinate rotamers for titratable residues
740    * RotamerLibrary's original coordinates rotamer flag has been set.
741    * <p>
742    * Any rotamers that were set previously are deleted.
743    *
744    * @return An array of Rotamer.
745    */
746   public Rotamer[] setRotamers(){
747     Rotamer[] originalRotamers = Rotamer.defaultRotamerFactory(this, titrationUtils);
748     int nOrig = originalRotamers.length;
749     rotamers = new Rotamer[nOrig];
750     arraycopy(originalRotamers, 0, rotamers, 0, nOrig);
751     return rotamers;
752   }
753 
754   /**
755    * Getter for the field <code>segID</code>.
756    *
757    * @return a {@link java.lang.String} object.
758    */
759   public String getSegID() {
760     return segID;
761   }
762 
763   /**
764    * Returns a list of side chain atoms; for our purposes, nucleic acid side chain atoms are the
765    * sugar and the phosphate.
766    *
767    * @return List of side chain (or nucleic backbone) atoms.
768    */
769   public List<Atom> getSideChainAtoms() {
770     List<Atom> atoms = getAtomList();
771     List<Atom> ret;
772     switch (residueType) {
773       case NA -> {
774         ret = new ArrayList<>();
775         for (Atom atom : atoms) {
776           String name = atom.getName().toUpperCase();
777           /*
778            * Very conveniently for our purposes, the entire sugar is
779            * denoted with ' at the end. Note that we add the side chain
780            * atoms for NAs, instead of removing the backbone.
781            */
782           if (name.contains("'") || name.equals("P") || name.startsWith("OP")) {
783             ret.add(atom);
784           }
785         }
786         return ret;
787       }
788       case AA -> {
789         ret = new ArrayList<>(atoms);
790         for (Atom atom : atoms) {
791           String name = atom.getName().toUpperCase();
792           if (name.equals("N") || name.equals("H") || name.equals("H1") || name.equals("H2")
793               || name.equals("H3") || name.equals("CA") || name.startsWith("HA") || name.equals("C")
794               || name.equals("O") || name.equals("OXT") || name.equals("OT2")) {
795             ret.remove(atom);
796           }
797         }
798         return ret;
799       }
800       default -> {
801         return null;
802       }
803     }
804   }
805 
806   /**
807    * Returns a list of atoms liable to change during dead-end elimination repacking. For ordinary
808    * amino acids: side chain atoms. For ordinary nucleic acids: sugar/phosphate backbone atoms.
809    * MultiResidue over-rides this to return all atoms (as backbone atom types are non-constant).
810    *
811    * @return Atoms changeable during DEE.
812    */
813   public List<Atom> getVariableAtoms() {
814     return getSideChainAtoms();
815   }
816 
817   /** {@inheritDoc} */
818   @Override
819   public int hashCode() {
820     return Objects.hash(segID, getResidueNumber(), residueType, getName());
821   }
822 
823   /**
824    * Initializes this (presumably nucleic acid) Residue's C1s, O4s, C4s, O3sNorth, and O3sSouth
825    * default coordinates based on default PDB atom locations; to preserve rotamer independence, this
826    * must be called before any NA rotamers are applied.
827    */
828   public void initializeDefaultAtomicCoordinates() {
829     if (residueType != ResidueType.NA) {
830       return;
831     }
832     boolean isDeoxy;
833     try {
834       isDeoxy = switch (NucleicAcid3.valueOf(this.getName())) {
835         case DAD, DCY, DGU, DTY -> true;
836         default -> false;
837       };
838       C1sCoords = new double[3];
839       ((Atom) getAtomNode("C1'")).getXYZ(C1sCoords);
840       O4sCoords = new double[3];
841       ((Atom) getAtomNode("O4'")).getXYZ(O4sCoords);
842       C4sCoords = new double[3];
843       ((Atom) getAtomNode("C4'")).getXYZ(C4sCoords);
844 
845       /*
846        With the place flag set false, applySugarPucker returns
847        hypothetical O3' coordinates based on default atom positions and
848        the supplied sugar pucker.
849       */
850       O3sNorthCoords = RotamerLibrary.applySugarPucker(this,
851           RotamerLibrary.NucleicSugarPucker.C3_ENDO, isDeoxy, false);
852       O3sSouthCoords = RotamerLibrary.applySugarPucker(this,
853           RotamerLibrary.NucleicSugarPucker.C2_ENDO, isDeoxy, false);
854     } catch (Exception e) {
855       logger.log(Level.WARNING, toString(), e);
856     }
857   }
858 
859   /**
860    * {@inheritDoc}
861    *
862    * <p>Prints "Residue Number: x" to stdout.
863    */
864   @Override
865   public void print() {
866     logger.info(" " + this);
867     for (Atom a : getAtomNode().getAtomList()) {
868       a.print();
869     }
870   }
871 
872   /**
873    * revertState.
874    *
875    * @param state a {@link ffx.potential.bonded.ResidueState} object.
876    */
877   public void revertState(ResidueState state) {
878     List<Atom> atomList = getAtomList();
879     for (Atom atom : atomList) {
880       atom.moveTo(state.getAtomCoords(atom));
881     }
882   }
883 
884   /** {@inheritDoc} */
885   @Override
886   public void setColor(RendererCache.ColorModel newColorModel, Color3f color, Material mat) {
887     // If Color by Residue, pass this Residue's Color
888     if (newColorModel == RendererCache.ColorModel.RESIDUE) {
889       switch (residueType) {
890         case AA -> color = AA3Color.get(aa);
891         case NA -> color = NA3Color.get(na);
892         default -> color = null;
893       }
894       if (color == null) {
895         return;
896       }
897       mat = RendererCache.materialFactory(color);
898     } else if (newColorModel == RendererCache.ColorModel.STRUCTURE) {
899       color = SSTypeColor.get(SSType.NONE);
900       mat = RendererCache.materialFactory(color);
901     }
902     super.setColor(newColorModel, color, mat);
903   }
904 
905   /**
906    * setNumber
907    *
908    * @param n The residue number.
909    */
910   public void setNumber(int n) {
911     resNumber = n;
912     for (Atom atom : getAtomList()) {
913       atom.setResidueNumber(n);
914     }
915   }
916 
917   /**
918    * storeCoordinateArray.
919    *
920    * @return an array of {@link double} objects.
921    */
922   public double[][] storeCoordinateArray() {
923     List<Atom> atomList = getAtomList();
924     int nAtoms = atomList.size();
925     double[][] coords = new double[nAtoms][3];
926     int i = 0;
927     for (Atom atom : atomList) {
928       atom.getXYZ(coords[i++]);
929     }
930     return coords;
931   }
932 
933   /**
934    * storeState.
935    *
936    * @return a {@link ffx.potential.bonded.ResidueState} object.
937    */
938   public ResidueState storeState() {
939     return new ResidueState(this, this);
940   }
941 
942   /**
943    * Formats this residue with some optional inclusions.
944    *
945    * <p>[residue type]-[chain ID]ResNumber-Name.
946    *
947    * @param addResType Include the residue type
948    * @param addChainID Include the chain ID.
949    * @return A descriptive string.
950    */
951   public String toFormattedString(boolean addResType, boolean addChainID) {
952     StringBuilder sb = new StringBuilder();
953     if (addResType) {
954       sb.append(residueType.toString()).append("-");
955     }
956     if (addChainID) {
957       sb.append(chainID);
958     }
959     sb.append(this);
960     return sb.toString();
961   }
962 
963   /**
964    * A descriptive string based on a given rotamer.
965    *
966    * <p>[chain ID]ResNumber-RotamerName.
967    *
968    * @param rotamer The rotamer to use.
969    * @return A descriptive string.
970    */
971   public String toString(Rotamer rotamer) {
972     return "" + chainID + resNumber + "-" + rotamer.getName();
973   }
974 
975   /** {@inheritDoc} */
976   @Override
977   public String toString() {
978     if (shortString == null) {
979       shortString = resNumber + "-" + getName();
980     }
981     return shortString;
982   }
983 
984   /**
985    * Add an array of rotamers to this Residue's cached array of rotamers.
986    *
987    * @param rotamers The rotamers to add.
988    */
989   void addRotamers(Rotamer[] rotamers) {
990     for (Rotamer rotamer : rotamers) {
991       addRotamer(rotamer);
992     }
993   }
994 
995   /**
996    * Add a rotamer to this Residue's cached array of rotamers.
997    *
998    * @param rotamer The rotamer to add.
999    */
1000   void addRotamer(Rotamer rotamer) {
1001     if (rotamers != null) {
1002       Rotamer[] libRotamers = rotamers;
1003       int nRots = libRotamers.length;
1004       rotamers = new Rotamer[nRots + 1];
1005       arraycopy(libRotamers, 0, rotamers, 0, nRots);
1006       rotamers[rotamers.length - 1] = rotamer;
1007     } else {
1008       rotamers = new Rotamer[1];
1009       rotamers[0] = rotamer;
1010     }
1011   }
1012 
1013   /**
1014    * deleteAtom
1015    *
1016    * @param atomToDelete a {@link ffx.potential.bonded.Atom} object.
1017    */
1018   void deleteAtom(Atom atomToDelete) {
1019     MSNode atoms = getAtomNode();
1020     if (atoms.contains(atomToDelete) != null) {
1021       logger.info(" The following atom is being deleted from the model:\n" + atomToDelete);
1022       atoms.remove(atomToDelete);
1023     }
1024   }
1025 
1026   /**
1027    * Returns the position of this (presumably nucleic acid) Residue's default O3' coordinates given a
1028    * North pucker.
1029    *
1030    * @return a new double[] with default XYZ coordinates for O3' in a North pucker.
1031    */
1032   double[] getO3sNorth() {
1033     double[] ret = new double[3];
1034     arraycopy(O3sNorthCoords, 0, ret, 0, ret.length);
1035     return ret;
1036   }
1037 
1038   /**
1039    * Returns the position of this (presumably nucleic acid) Residue's default O3' coordinates given a
1040    * South pucker.
1041    *
1042    * @return a new double[] with default XYZ coordinates for O3' in a South pucker.
1043    */
1044   double[] getO3sSouth() {
1045     double[] ret = new double[3];
1046     arraycopy(O3sSouthCoords, 0, ret, 0, ret.length);
1047     return ret;
1048   }
1049 
1050   /**
1051    * Returns the position of this (presumably nucleic acid) Residue's original C1' coordinates.
1052    *
1053    * @return a new double[] with original XYZ coordinates for C1'.
1054    */
1055   double[] getC1sCoords() {
1056     double[] ret = new double[3];
1057     arraycopy(C1sCoords, 0, ret, 0, ret.length);
1058     return ret;
1059   }
1060 
1061   /**
1062    * Returns the position of this (presumably nucleic acid) Residue's original O4' coordinates.
1063    *
1064    * @return a new double[] with original XYZ coordinates for O4'.
1065    */
1066   double[] getO4sCoords() {
1067     double[] ret = new double[3];
1068     arraycopy(O4sCoords, 0, ret, 0, ret.length);
1069     return ret;
1070   }
1071 
1072   /**
1073    * Returns the position of this (presumably nucleic acid) Residue's original C4' coordinates.
1074    *
1075    * @return a new double[] with original XYZ coordinates for C4'.
1076    */
1077   double[] getC4sCoords() {
1078     double[] ret = new double[3];
1079     arraycopy(C4sCoords, 0, ret, 0, ret.length);
1080     return ret;
1081   }
1082 
1083   /**
1084    * Uses a name to add an Atom to a List<Atom> if the Atom exists for this residue.
1085    *
1086    * @param atList List to add to.
1087    * @param name Atom to add.
1088    * @return If atom exists.
1089    */
1090   private boolean tryAddAtom(List<Atom> atList, String name) {
1091     try {
1092       Atom at = (Atom) getAtomNode(name);
1093       if (at != null) {
1094         atList.add(at);
1095         return true;
1096       } else {
1097         return false;
1098       }
1099     } catch (Exception ex) {
1100       return false;
1101     }
1102   }
1103 
1104   private void assignResidueType() {
1105     String name = getName().toUpperCase();
1106     switch (residueType) {
1107       case AA -> {
1108         aa = null;
1109         try {
1110           if (name.length() >= 2) {
1111             aa = AminoAcid3.valueOf(name);
1112           } else if (name.length() == 1) {
1113             AminoAcid1 aa1 = AminoAcid1.valueOf(name);
1114             aa = AA1toAA3.get(aa1);
1115           }
1116         } catch (Exception e) {
1117           logger.fine(String.format("Exception assigning AA3 for residue: %s", name));
1118           aa = AminoAcid3.UNK;
1119         }
1120       }
1121       case NA -> {
1122         na = null;
1123         try {
1124           if (name.length() >= 2) {
1125             na = NucleicAcid3.parse(name);
1126           } else if (name.length() == 1) {
1127             NucleicAcid1 na1 = NucleicAcid1.valueOf(name);
1128             na = NA1toNA3.get(na1);
1129           }
1130         } catch (Exception e) {
1131           na = NucleicAcid3.UNK;
1132         }
1133       }
1134     }
1135   }
1136 
1137   public enum SSType {
1138     NONE, HELIX, SHEET, TURN
1139   }
1140 
1141   /**
1142    * Residue type [NA, AA, UNK].
1143    */
1144   public enum ResidueType {
1145     NA, AA, UNK
1146   }
1147 }