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