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 ffx.numerics.math.DoubleMath;
41  import ffx.potential.MolecularAssembly;
42  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
43  import ffx.potential.bonded.AminoAcidUtils.SideChainType;
44  import ffx.potential.parameters.AtomType;
45  import ffx.potential.parameters.BioType;
46  import ffx.potential.parameters.BondType;
47  import ffx.potential.parameters.ForceField;
48  import ffx.utilities.StringUtils;
49  
50  import java.io.Serial;
51  import java.util.ArrayList;
52  import java.util.Arrays;
53  import java.util.Comparator;
54  import java.util.List;
55  import java.util.Optional;
56  import java.util.logging.Level;
57  import java.util.logging.Logger;
58  import java.util.stream.Collectors;
59  
60  import static ffx.numerics.math.DoubleMath.length;
61  import static ffx.numerics.math.DoubleMath.normalize;
62  import static ffx.numerics.math.DoubleMath.scale;
63  import static ffx.numerics.math.DoubleMath.sub;
64  import static ffx.potential.bonded.Bond.logNoBondType;
65  import static ffx.potential.bonded.NamingUtils.nameAcetylCap;
66  import static java.lang.String.format;
67  import static java.lang.System.arraycopy;
68  import static org.apache.commons.math3.util.FastMath.abs;
69  import static org.apache.commons.math3.util.FastMath.cos;
70  import static org.apache.commons.math3.util.FastMath.max;
71  import static org.apache.commons.math3.util.FastMath.sin;
72  import static org.apache.commons.math3.util.FastMath.sqrt;
73  import static org.apache.commons.math3.util.FastMath.toRadians;
74  
75  /**
76   * Utilities for placing atoms.
77   *
78   * @author Michael Schnieders
79   * @since 1.0
80   */
81  public class BondedUtils {
82  
83    private static final Logger logger = Logger.getLogger(BondedUtils.class.getName());
84    private static final double eps = 0.0000001d;
85  
86    /**
87     * Checks if atom a1 is bonded to atom a2.
88     *
89     * @param a1 An Atom.
90     * @param a2 Another Atom.
91     * @return If a1 is bonded to a2.
92     */
93    public static boolean atomAttachedToAtom(Atom a1, Atom a2) {
94      assert a1 != a2;
95      return a1.getBonds().stream().anyMatch((Bond b) -> b.get1_2(a1) == a2);
96    }
97  
98    /**
99     * Build a bond between two atoms.
100    *
101    * @param a1 The first atom.
102    * @param a2 The second atom.
103    * @param forceField The force field to use.
104    * @param bondList The list of bonds to add to.
105    * @return The new bond.
106    */
107   public static Bond buildBond(Atom a1, Atom a2, ForceField forceField, List<Bond> bondList) {
108     Bond bond = new Bond(a1, a2);
109     BondType bondType = forceField.getBondType(a1.getAtomType(), a2.getAtomType());
110     if (bondType == null) {
111       logNoBondType(a1, a2, forceField);
112     } else {
113       bond.setBondType(bondType);
114     }
115     if (bondList != null) {
116       bondList.add(bond);
117     }
118     return bond;
119   }
120 
121   /**
122    * Build a heavy atom.
123    *
124    * @param residue The residue to add the atom to.
125    * @param atomName The name of the atom.
126    * @param bondedTo The atom the heavy atom is bonded to.
127    * @param key The atom type key.
128    * @param forceField The force field to use.
129    * @param bondList The list of bonds to add to.
130    * @return The heavy atom.
131    * @throws ffx.potential.bonded.BondedUtils.MissingHeavyAtomException if any.
132    */
133   public static Atom buildHeavy(MSGroup residue, String atomName, Atom bondedTo,
134       int key, ForceField forceField, List<Bond> bondList)
135       throws MissingHeavyAtomException, MissingAtomTypeException {
136     Atom atom = (Atom) residue.getAtomNode(atomName);
137     AtomType atomType = findAtomType(key, forceField);
138     if (atomType == null) {
139       Residue res = (Residue) residue;
140       throw new MissingAtomTypeException(res, atom);
141     }
142     if (atom == null) {
143       throw new MissingHeavyAtomException(atomName, atomType, bondedTo);
144     }
145     atom.setAtomType(atomType);
146     if (bondedTo != null) {
147       buildBond(atom, bondedTo, forceField, bondList);
148     }
149     return atom;
150   }
151 
152   /**
153    * Build a heavy atom.
154    *
155    * @param residue The residue to add the atom to.
156    * @param atomName The name of the atom.
157    * @param ia The first atom.
158    * @param bond The bond length.
159    * @param ib The second atom.
160    * @param angle1 The angle.
161    * @param ic The third atom.
162    * @param angle2 The second angle (either a bond angle or a dihedral angle).
163    * @param chiral The chiral flag.
164    * @param lookUp The BioType key.
165    * @param forceField The force field to use.
166    * @return The new atom.
167    */
168   public static Atom buildHeavy(MSGroup residue, String atomName, Atom ia, double bond,
169       Atom ib, double angle1, Atom ic, double angle2, int chiral, int lookUp,
170       ForceField forceField) {
171     AtomType atomType = findAtomType(lookUp, forceField);
172     return buildHeavyAtom(residue, atomName, ia, bond, ib, angle1, ic, angle2, chiral, atomType);
173   }
174 
175   /**
176    * Build a heavy atom.
177    *
178    * @param residue The residue to add the atom to.
179    * @param atomName The name of the atom.
180    * @param ia The first atom.
181    * @param bond The bond length.
182    * @param ib The second atom.
183    * @param angle1 The angle.
184    * @param ic The third atom.
185    * @param angle2 The second angle (either a bond angle or a dihedral angle).
186    * @param chiral The chiral flag.
187    * @param lookUp The BioType key.
188    * @param forceField The force field to use.
189    * @param bondList A list of bonds to add to.
190    * @return The new heavy atom.
191    */
192   public static Atom buildHeavy(MSGroup residue, String atomName, Atom ia, double bond, Atom ib,
193       double angle1, Atom ic, double angle2, int chiral, int lookUp, ForceField forceField,
194       List<Bond> bondList) {
195     AtomType atomType = findAtomType(lookUp, forceField);
196     return buildHeavyAtom(residue, atomName, ia, bond, ib, angle1, ic, angle2, chiral, atomType,
197         forceField, bondList);
198   }
199 
200   /**
201    * Build a heavy atom.
202    *
203    * @param residue The residue to add the atom to.
204    * @param atomName The name of the atom.
205    * @param ia The first atom.
206    * @param bond The bond length.
207    * @param ib The second atom.
208    * @param angle1 The angle.
209    * @param ic The third atom.
210    * @param angle2 The second angle (either a bond angle or a dihedral angle).
211    * @param chiral The chiral flag.
212    * @param forceField The force field to use.
213    * @param bondList A list of bonds to add to.
214    * @return The new heavy atom.
215    */
216   public static Atom buildHeavy(MSGroup residue, SideChainType atomName, Atom ia, double bond,
217       Atom ib, double angle1, Atom ic, double angle2, int chiral, ForceField forceField,
218       List<Bond> bondList) {
219     AtomType atomType = findAtomType(atomName.getType(), forceField);
220     return buildHeavyAtom(residue, atomName.name(), ia, bond, ib, angle1, ic, angle2, chiral,
221         atomType, forceField, bondList);
222   }
223 
224   /**
225    * Build a hydrogen atom.
226    *
227    * @param residue The residue to add the atom to.
228    * @param atomName The name of the atom.
229    * @param ia The first atom.
230    * @param bond The bond length.
231    * @param ib The second atom.
232    * @param angle1 The angle.
233    * @param ic The third atom.
234    * @param angle2 The second angle (either a bond angle or a dihedral angle).
235    * @param chiral The chiral flag.
236    * @param lookUp The BioType key.
237    * @param forceField The force field to use.
238    * @param bondList A list of bonds to add to.
239    * @return The new hydrogen atom.
240    */
241   public static Atom buildH(MSGroup residue, String atomName, Atom ia, double bond, Atom ib,
242       double angle1, Atom ic, double angle2, int chiral, int lookUp, ForceField forceField,
243       List<Bond> bondList) {
244     AtomType atomType = findAtomType(lookUp, forceField);
245     return buildHydrogenAtom(residue, atomName, ia, bond, ib, angle1, ic, angle2, chiral,
246         atomType, forceField, bondList);
247   }
248 
249   /**
250    * Build a hydrogen atom.
251    *
252    * @param residue The residue to add the atom to.
253    * @param atomName The name of the atom.
254    * @param ia The first atom.
255    * @param bond The bond length.
256    * @param ib The second atom.
257    * @param angle1 The angle.
258    * @param ic The third atom.
259    * @param angle2 The second angle (either a bond angle or a dihedral angle).
260    * @param chiral The chiral flag.
261    * @param forceField The force field to use.
262    * @param bondList A list of bonds to add to.
263    * @return The new hydrogen atom.
264    */
265   public static Atom buildH(MSGroup residue, SideChainType atomName, Atom ia, double bond,
266       Atom ib, double angle1, Atom ic, double angle2, int chiral, ForceField forceField,
267       List<Bond> bondList) {
268     AtomType atomType = findAtomType(atomName.getType(), forceField);
269     return buildHydrogenAtom(residue, atomName.name(), ia, bond, ib, angle1, ic, angle2,
270         chiral, atomType, forceField, bondList);
271   }
272 
273   /**
274    * Build a hydrogen atom.
275    *
276    * @param residue The residue to add the atom to.
277    * @param atomName The name of the atom.
278    * @param ia The first atom.
279    * @param bond The bond length.
280    * @param ib The second atom.
281    * @param angle1 The angle.
282    * @param ic The third atom.
283    * @param angle2 The second angle (either a bond angle or a dihedral angle).
284    * @param chiral The chiral flag.
285    * @param atomType The atom type.
286    * @param forceField The force field to use.
287    * @param bondList A list of bonds to add to.
288    * @return The new hydrogen atom.
289    */
290   public static Atom buildHydrogenAtom(MSGroup residue, String atomName, Atom ia, double bond,
291       Atom ib, double angle1, Atom ic, double angle2, int chiral, AtomType atomType,
292       ForceField forceField, List<Bond> bondList) {
293     if (atomType == null) {
294       return null;
295     }
296     Atom atom = (Atom) residue.getAtomNode(atomName);
297     // It may be a Deuterium
298     if (atom == null) {
299       String dAtomName = atomName.replaceFirst("H", "D");
300       atom = (Atom) residue.getAtomNode(dAtomName);
301     }
302 
303     // Basic checking for unspecified H atoms attached to water.
304     if (atom == null && residue instanceof Molecule molecule) {
305       if (StringUtils.looksLikeWater(molecule.getName())) {
306         atom = (Atom) molecule.getAtomNode("H");
307         if (atom == null) {
308           atom = (Atom) molecule.getAtomNode("D");
309         }
310         if (atom != null) {
311           atom.setName(atomName);
312         }
313       }
314     }
315 
316     if (atom == null) {
317       boolean buildDeuterium = forceField.getBoolean("build-deuterium", false);
318       if (buildDeuterium && atomName.startsWith("H")) {
319         atomName = atomName.replaceFirst("H", "D");
320       }
321       String resName = ia.getResidueName();
322       int resSeq = ia.getResidueNumber();
323       Character chainID = ia.getChainID();
324       Character altLoc = ia.getAltLoc();
325       String segID = ia.getSegID();
326       double occupancy = ia.getOccupancy();
327       double tempFactor = ia.getTempFactor();
328       atom = new Atom(0, atomName, altLoc, new double[3], resName, resSeq, chainID,
329           occupancy, tempFactor, segID, true);
330       residue.addMSNode(atom);
331       intxyz(atom, ia, bond, ib, angle1, ic, angle2, chiral);
332     }
333     atom.setAtomType(atomType);
334     buildBond(ia, atom, forceField, bondList);
335     return atom;
336   }
337 
338   /**
339    * findAtomType.
340    *
341    * @param key The BioType key.
342    * @param forceField a {@link ForceField} object.
343    * @return a {@link ffx.potential.parameters.AtomType} object.
344    */
345   public static AtomType findAtomType(int key, ForceField forceField) {
346     BioType bioType = forceField.getBioType(Integer.toString(key));
347     if (bioType != null) {
348       AtomType atomType = forceField.getAtomType(Integer.toString(bioType.atomType));
349       if (atomType != null) {
350         return atomType;
351       } else {
352         logger.severe(format("The atom type %s was not found for biotype %s.",
353             bioType.atomType, bioType));
354       }
355     }
356     return null;
357   }
358 
359   /**
360    * Finds all Atoms belonging to a Residue of a given atomic number.
361    *
362    * @param residue Residue to search in.
363    * @param element Atomic number to search for.
364    * @return A list of matching Atoms.
365    */
366   public static List<Atom> findAtomsOfElement(Residue residue, int element) {
367     return residue.getAtomList().stream()
368         .filter((Atom a) -> a.getAtomType().atomicNumber == element)
369         .collect(Collectors.toList());
370   }
371 
372   /**
373    * Finds Atoms bonded to a given Atom that match a certain atomic number.
374    *
375    * @param atom Atom to search from.
376    * @param element Atomic number to search for.
377    * @return Bonded atoms of an element.
378    */
379   public static List<Atom> findBondedAtoms(Atom atom, int element) {
380     return findBondedAtoms(atom, null, element);
381   }
382 
383   /**
384    * Finds Atoms bonded to a given Atom that match a certain atomic number that do not match an
385    * excluded atom.
386    *
387    * @param atom Atom to search from.
388    * @param toExclude Atom to exclude from search.
389    * @param element Atomic number to search for.
390    * @return Bonded atoms of an element.
391    */
392   public static List<Atom> findBondedAtoms(Atom atom, Atom toExclude, int element) {
393     return atom.getBonds().stream()
394         .map((Bond b) -> b.get1_2(atom))
395         .filter((Atom a) -> a != toExclude)
396         .filter((Atom a) -> a.getAtomType().atomicNumber == element)
397         .collect(Collectors.toList());
398   }
399 
400   /**
401    * Finds the backbone nitrogen of a residue.
402    *
403    * @param residue Amino acid residue to search for.
404    * @return backbone nitrogen.
405    */
406   public static Atom findNitrogenAtom(Residue residue) {
407     assert residue.getResidueType() == Residue.ResidueType.AA;
408 
409     // Will filter out amide N from NME caps at the end of the method.
410     List<Atom> nitrogenCandidates = new ArrayList<>(2);
411 
412     switch (residue.getAminoAcid3()) {
413       case LYS, LYD -> {
414         /* For lysine: find the nitrogen bonded to a carbon that does not have two protons. */
415         List<Atom> nitrogenList = findAtomsOfElement(residue, 7);
416         for (Atom nitrogen : nitrogenList) {
417           List<Atom> carbons = findBondedAtoms(nitrogen, 6);
418           if (carbons.size() == 2) {
419             nitrogenCandidates.add(nitrogen);
420           } else if (findBondedAtoms(carbons.get(0), 1).size() < 2) {
421             nitrogenCandidates.add(nitrogen);
422           }
423         }
424         if (nitrogenCandidates.isEmpty()) {
425           throw new IllegalArgumentException(
426               format(" Could not identify N atom of residue %s!", residue));
427         }
428       }
429       // Arginine and histidine can be handled very similarly.
430       case ARG, HIS, HIE, HID -> {
431         /*
432          * Easiest to the carbon bonded to all the side-chain nitrogen atoms,
433          * then find the nitrogen not thus bonded.
434          */
435         List<Atom> nitrogenList = findAtomsOfElement(residue, 7);
436         Atom commonC = findAtomsOfElement(residue, 6).stream()
437             .filter((Atom carbon) -> findBondedAtoms(carbon, 7).size() >= 2)
438             .findAny().get();
439         nitrogenCandidates = nitrogenList.stream()
440             .filter((Atom nitr) -> !atomAttachedToAtom(nitr, commonC))
441             .collect(Collectors.toList());
442       }
443       case ASN, GLN -> {
444         /*
445          * Find a bonded carbon that is not bonded to an oxygen. Both N and ND/NE have an attached
446          * carbonyl carbon. Only N will have CA attached.
447          */
448         List<Atom> nitrogenList = findAtomsOfElement(residue, 7);
449         for (Atom nitrogen : nitrogenList) {
450           List<Atom> bondedCarbs = findBondedAtoms(nitrogen, 6);
451           for (Atom carbon : bondedCarbs) {
452             if (!hasAttachedAtom(carbon, 8)) {
453               nitrogenCandidates.add(nitrogen);
454             }
455           }
456         }
457         if (nitrogenCandidates.isEmpty()) {
458           throw new IllegalArgumentException(
459               format(" Could not identify N atom of residue %s!", residue));
460         }
461       }
462       case TRP -> {
463         /*
464          * For tryptophan: If at an N-terminus, there will be only one bonded carbon. Else, one
465          * carbon will be a carbonyl carbon.
466          */
467         List<Atom> nitrogenList = findAtomsOfElement(residue, 7);
468         for (Atom nitrogen : nitrogenList) {
469           List<Atom> bondedCarbs = findBondedAtoms(nitrogen, 6);
470           if (bondedCarbs.size() == 1) {
471             nitrogenCandidates.add(nitrogen);
472           }
473           for (Atom carbon : bondedCarbs) {
474             if (hasAttachedAtom(carbon, 8)) {
475               nitrogenCandidates.add(nitrogen);
476             }
477           }
478         }
479         if (nitrogenCandidates.isEmpty()) {
480           throw new IllegalArgumentException(
481               format(" Could not identify N atom of residue %s!", residue));
482         }
483       }
484       case ACE -> {
485         return null;
486       }
487       /* All others should only have one nitrogen atom. */
488       default -> nitrogenCandidates = findAtomsOfElement(residue, 7);
489     }
490 
491     switch (nitrogenCandidates.size()) {
492       case 0 -> {
493         logger.warning(
494             " Did not find any atoms that might be the amide nitrogen for residue " + residue);
495         return null;
496       }
497       case 1 -> {
498         return nitrogenCandidates.get(0);
499       }
500       case 2 -> {
501         logger.fine(format(
502             " Probable NME C-terminal cap attached to residue %s, some atom names may be duplicated!",
503             residue));
504         Atom N = null;
505         for (Atom nitro : nitrogenCandidates) {
506           nitro.setName("N");
507           Optional<Atom> capMethyl = findBondedAtoms(nitro, 6).stream()
508               .filter((Atom carb) -> findBondedAtoms(carb, 1).size() == 3)
509               .findAny();
510           if (capMethyl.isPresent()) {
511             findBondedAtoms(nitro, 1).get(0).setName("H");
512             Atom theCap = capMethyl.get();
513             theCap.setName("CH3");
514             List<Atom> capHydrogenList = findBondedAtoms(theCap, 1);
515             for (int i = 0; i < 3; i++) {
516               capHydrogenList.get(i).setName(format("H%d", i + 1));
517             }
518           } else {
519             N = nitro;
520           }
521         }
522         return N;
523       }
524 //      default -> throw new IllegalArgumentException(
525 //          format(" Could not definitely identify amide nitrogen for residue %s", residue));
526       default -> {
527         if(logger.isLoggable(Level.FINE)){
528           logger.warning(format(" Nitrogen could not be mapped for amide nitrogen of residue %s ", residue));
529         }
530         return null;
531       }
532     }
533   }
534 
535   /**
536    * Find the O4' of a nucleic acid Residue. This is fairly unique in standard nucleotides, as O4' is
537    * the only ether oxygen (bonded to two carbons).
538    *
539    * @param residue Residue to find O4' of.
540    * @return O4'.
541    */
542   public static Optional<Atom> findNucleotideO4s(Residue residue) {
543     assert residue.getResidueType() == Residue.ResidueType.NA;
544     return findAtomsOfElement(residue, 8).stream()
545         .filter(o -> findBondedAtoms(o, 6).size() == 2)
546         .findAny();
547   }
548 
549   /**
550    * Finds the alpha carbon of a residue, and handles any C-terminal ACE caps while at it.
551    *
552    * @param residue Find the alpha carbon of.
553    * @param N The residue's backbone nitrogen.
554    * @return The alpha carbon.
555    */
556   public static Atom getAlphaCarbon(Residue residue, Atom N) {
557     List<Atom> resAtoms = residue.getAtomList();
558     List<Atom> caCandidates = findBondedAtoms(N, 6).stream().filter(resAtoms::contains).toList();
559 
560     if (residue.getAminoAcid3() == AminoAcid3.PRO) {
561       Atom CA = null;
562       Atom CD = null;
563       Atom aceC = null;
564       for (Atom caCand : caCandidates) {
565         if (hasAttachedAtom(caCand, 8)) {
566           aceC = caCand;
567         } else {
568           List<Atom> attachedH = findBondedAtoms(caCand, 1);
569           if (attachedH.size() == 1) {
570             CA = caCand;
571           } else if (attachedH.size() == 2) {
572             CD = caCand;
573           } else {
574             throw new IllegalArgumentException(format(" Error in parsing proline %s", residue));
575           }
576         }
577       }
578       assert CA != null && CD != null;
579       if (aceC != null) {
580         nameAcetylCap(residue, aceC);
581       }
582       return CA;
583     }
584     if (caCandidates.size() == 1) {
585       return caCandidates.get(0);
586     } else {
587       Atom CA = null;
588       Atom aceC = null;
589       for (Atom caCand : caCandidates) {
590         if (hasAttachedAtom(caCand, 8)) {
591           aceC = caCand;
592         } else {
593           CA = caCand;
594         }
595       }
596       nameAcetylCap(residue, aceC);
597       return CA;
598     }
599   }
600 
601   /**
602    * Checks if there is an Atom of a given atomic number bonded to the provided Atom.
603    *
604    * @param atom Atom to search from.
605    * @param element Atomic number to search for.
606    * @return If bonded atoms of given element exist.
607    */
608   public static boolean hasAttachedAtom(Atom atom, int element) {
609     return atom.getBonds().stream()
610         .map((Bond b) -> b.get1_2(atom))
611         .anyMatch((Atom a) -> a.getAtomType().atomicNumber == element);
612   }
613 
614   /**
615    * This routine was derived from a similar routine in TINKER.
616    *
617    * @param atom The atom to be placed.
618    * @param ia The first atom.
619    * @param bond The bond length.
620    * @param ib The second atom.
621    * @param angle1 The angle.
622    * @param ic The third atom.
623    * @param angle2 The second angle (either a bond angle or a dihedral angle).
624    * @param chiral The chiral flag.
625    */
626   public static void intxyz(
627       Atom atom, Atom ia, double bond, Atom ib, double angle1, Atom ic, double angle2, int chiral) {
628     double[] xa = new double[3];
629     xa = (ia == null) ? null : ia.getXYZ(xa);
630     double[] xb = new double[3];
631     xb = (ib == null) ? null : ib.getXYZ(xb);
632     double[] xc = new double[3];
633     xc = (ic == null) ? null : ic.getXYZ(xc);
634     atom.moveTo(determineIntxyz(xa, bond, xb, angle1, xc, angle2, chiral));
635   }
636 
637   /**
638    * Re-number atoms, especially if missing atoms were created.
639    *
640    * @param molecularAssembly The molecular assembly to renumber atoms for.
641    */
642   public static void numberAtoms(MolecularAssembly molecularAssembly) {
643     int index = 1;
644     for (Atom a : molecularAssembly.getAtomArray()) {
645       a.setXyzIndex(index++);
646     }
647     index--;
648     if (logger.isLoggable(Level.INFO)) {
649       logger.info(format(" Total number of atoms: %d\n", index));
650     }
651 
652     Polymer[] polymers = molecularAssembly.getChains();
653     if (polymers != null) {
654       for (Polymer p : polymers) {
655         List<Residue> residues = p.getResidues();
656         for (Residue r : residues) {
657           r.reOrderAtoms();
658         }
659       }
660     }
661     List<MSNode> molecules = molecularAssembly.getMolecules();
662     for (MSNode n : molecules) {
663       MSGroup m = (MSGroup) n;
664       m.reOrderAtoms();
665     }
666     List<MSNode> water = molecularAssembly.getWater();
667     for (MSNode n : water) {
668       MSGroup m = (MSGroup) n;
669       m.reOrderAtoms();
670     }
671     List<MSNode> ions = molecularAssembly.getIons();
672     for (MSNode n : ions) {
673       MSGroup m = (MSGroup) n;
674       m.reOrderAtoms();
675     }
676   }
677 
678   /**
679    * Sorts toCompare by distance to the reference Atom, returning a sorted array.
680    *
681    * @param reference Atom to compare distances to.
682    * @param toCompare Atoms to sort by distance (not modified).
683    * @return Sorted array of atoms in toCompare.
684    */
685   public static Atom[] sortAtomsByDistance(Atom reference, List<Atom> toCompare) {
686     Atom[] theAtoms = toCompare.toArray(new Atom[0]);
687     sortAtomsByDistance(reference, theAtoms);
688     return theAtoms;
689   }
690 
691   /**
692    * In-place sorts toCompare by distance to the reference Atom. Modifies toCompare.
693    *
694    * @param reference Atom to compare distances to.
695    * @param toCompare Atoms to sort (in-place) by distance.
696    */
697   public static void sortAtomsByDistance(Atom reference, Atom[] toCompare) {
698     final double[] refXYZ = reference.getXYZ(new double[3]);
699     Arrays.sort(
700         toCompare,
701         Comparator.comparingDouble(
702             a -> {
703               double[] atXYZ = a.getXYZ(new double[3]);
704               return DoubleMath.dist2(refXYZ, atXYZ);
705             }));
706   }
707 
708   /**
709    * Builds a heavy atom.
710    *
711    * @param residue The residue to add the atom to.
712    * @param atomName The name of the atom.
713    * @param ia The first atom.
714    * @param bond The bond length.
715    * @param ib The second atom.
716    * @param angle1 The angle.
717    * @param ic The third atom.
718    * @param angle2 The second angle (either a bond angle or a dihedral angle).
719    * @param chiral The chiral flag.
720    * @param atomType The atom type.
721    * @return The new heavy atom.
722    */
723   private static Atom buildHeavyAtom(MSGroup residue, String atomName, Atom ia, double bond,
724       Atom ib, double angle1, Atom ic, double angle2, int chiral, AtomType atomType) {
725     Atom atom = (Atom) residue.getAtomNode(atomName);
726     if (atomType == null) {
727       return null;
728     }
729     if (atom == null) {
730       String resName = ia.getResidueName();
731       int resSeq = ia.getResidueNumber();
732       Character chainID = ia.getChainID();
733       Character altLoc = ia.getAltLoc();
734       String segID = ia.getSegID();
735       double occupancy = ia.getOccupancy();
736       double tempFactor = ia.getTempFactor();
737       atom =
738           new Atom(
739               0,
740               atomName,
741               altLoc,
742               new double[3],
743               resName,
744               resSeq,
745               chainID,
746               occupancy,
747               tempFactor,
748               segID,
749               true);
750       residue.addMSNode(atom);
751       intxyz(atom, ia, bond, ib, angle1, ic, angle2, chiral);
752     }
753     atom.setAtomType(atomType);
754     return atom;
755   }
756 
757   /**
758    * Builds a heavy atom.
759    *
760    * @param residue The residue to add the atom to.
761    * @param atomName The name of the atom.
762    * @param ia The first atom.
763    * @param bond The bond length.
764    * @param ib The second atom.
765    * @param angle1 The angle.
766    * @param ic The third atom.
767    * @param angle2 The second angle (either a bond angle or a dihedral angle).
768    * @param chiral The chiral flag.
769    * @param atomType The atom type.
770    * @param forceField The force field to use.
771    * @param bondList A list of bonds to add to.
772    * @return The new heavy atom.
773    */
774   private static Atom buildHeavyAtom(MSGroup residue, String atomName, Atom ia, double bond,
775       Atom ib, double angle1, Atom ic, double angle2, int chiral, AtomType atomType,
776       ForceField forceField, List<Bond> bondList) {
777     Atom atom =
778         buildHeavyAtom(residue, atomName, ia, bond, ib, angle1, ic, angle2, chiral, atomType);
779     buildBond(ia, atom, forceField, bondList);
780     return atom;
781   }
782 
783   /**
784    * This routine was derived from a similar routine in TINKER. It determines at what coordinates an
785    * atom would be placed without moving or calling any atoms, relying solely upon coordinates.
786    * Passed arrays are copied into local arrays to avoid any over-writing of the passed arrays.
787    *
788    * <p>The chiral argument is 0 if angle2 is a dihedral. Else, if angle2 is the atom-ia-ic angle:
789    * -1 indicates left-hand-rule placement. +1 indicates right-hand-rule placement. +3 indicates
790    * trigonal planar placement.
791    *
792    * <p>Chiral +3 replaces the angle1 and angle2 constraints with a planarity constraint, and
793    * minimized, equipartitioned deviation from angle1 and angle2.
794    *
795    * @param ia a double[] of atomic coordinates.
796    * @param bond The bond length.
797    * @param ib a double[] of atomic coordinates.
798    * @param angle1 The angle.
799    * @param ic a double[] of atomic coordinates.
800    * @param angle2 The second angle (either a bond angle or a dihedral angle).
801    * @param chiral The chiral flag (0, 1, -1, or 3).
802    * @return A double[] with XYZ coordinates at which an atom would be placed.
803    */
804   public static double[] determineIntxyz(double[] ia, double bond, double[] ib, double angle1,
805       double[] ic, double angle2, int chiral) {
806     if (ia != null && !Double.isFinite(bond)) {
807       throw new IllegalArgumentException(
808           String.format(" Passed bond length is non-finite %f", bond));
809     } else if (ib != null && !Double.isFinite(angle1)) {
810       throw new IllegalArgumentException(String.format(" Passed angle is non-finite %f", angle1));
811     } else if (ic != null && !Double.isFinite(angle2)) {
812       throw new IllegalArgumentException(
813           String.format(" Passed dihedral/improper is non-finite %f", angle2));
814     }
815 
816     if (chiral == 3) {
817       double[] negChiral = determineIntxyz(ia, bond, ib, angle1, ic, angle2, -1);
818       double[] posChiral = determineIntxyz(ia, bond, ib, angle1, ic, angle2, 1);
819       double[] displacement = new double[3];
820       double dispMag = 0;
821 
822       for (int i = 0; i < 3; i++) {
823         // First, draw the midpoint between the positive- and negative- chiral solutions.
824         displacement[i] = 0.5 * (posChiral[i] + negChiral[i]);
825         // Second, take the displacement from a2 to this midpoint.
826         displacement[i] -= ia[i];
827         // Third, accumulate into the vector magnitude.
828         dispMag += (displacement[i] * displacement[i]);
829       }
830 
831       dispMag = sqrt(dispMag);
832       double extend = bond / dispMag;
833       assert extend > 0.999; // Should be >= 1.0, with slight machine-precision tolerance.
834 
835       double[] outXYZ = new double[3];
836       for (int i = 0; i < 3; i++) {
837         displacement[i] *= extend;
838         outXYZ[i] = displacement[i] + ia[i];
839       }
840       return outXYZ;
841     }
842 
843     angle1 = toRadians(angle1);
844     angle2 = toRadians(angle2);
845     double zcos0 = cos(angle1);
846     double zcos1 = cos(angle2);
847     double zsin0 = sin(angle1);
848     double zsin1 = sin(angle2);
849     double[] ret = new double[3];
850     double[] x = new double[3];
851 
852     // No partners
853     if (ia == null) {
854       x[0] = x[1] = x[2] = 0.0;
855     } else if (ib == null) {
856       double[] xa = new double[3];
857       arraycopy(ia, 0, xa, 0, ia.length);
858       // One partner - place on the z-axis
859       x[0] = xa[0];
860       x[1] = xa[1];
861       x[2] = xa[2] + bond;
862     } else if (ic == null) {
863       double[] xa = new double[3];
864       double[] xb = new double[3];
865       double[] xab = new double[3];
866       for (int i = 0; i < ia.length; i++) {
867         xa[i] = ia[i];
868         xb[i] = ib[i];
869       }
870       // Two partners - place in the xz-plane
871       sub(xa, xb, xab);
872       double rab = length(xab);
873       normalize(xab, xab);
874       double cosb = xab[2];
875       double sinb = sqrt(xab[0] * xab[0] + xab[1] * xab[1]);
876       double cosg, sing;
877       if (sinb == 0.0d) {
878         cosg = 1.0d;
879         sing = 0.0d;
880       } else {
881         cosg = xab[1] / sinb;
882         sing = xab[0] / sinb;
883       }
884       double xtmp = bond * zsin0;
885       double ztmp = rab - bond * zcos0;
886       x[0] = xb[0] + xtmp * cosg + ztmp * sing * sinb;
887       x[1] = xb[1] - xtmp * sing + ztmp * cosg * sinb;
888       x[2] = xb[2] + ztmp * cosb;
889     } else if (chiral == 0) {
890       double[] xa = new double[3];
891       double[] xb = new double[3];
892       double[] xc = new double[3];
893       double[] xab = new double[3];
894       double[] xbc = new double[3];
895       double[] xt = new double[3];
896       double[] xu = new double[3];
897       for (int i = 0; i < ia.length; i++) {
898         xa[i] = ia[i];
899         xb[i] = ib[i];
900         xc[i] = ic[i];
901       }
902       // General case - with a dihedral
903       sub(xa, xb, xab);
904       normalize(xab, xab);
905       sub(xb, xc, xbc);
906       normalize(xbc, xbc);
907       xt[0] = xab[2] * xbc[1] - xab[1] * xbc[2];
908       xt[1] = xab[0] * xbc[2] - xab[2] * xbc[0];
909       xt[2] = xab[1] * xbc[0] - xab[0] * xbc[1];
910       double cosine = xab[0] * xbc[0] + xab[1] * xbc[1] + xab[2] * xbc[2];
911       double sine = sqrt(max(1.0d - cosine * cosine, eps));
912       if (abs(cosine) >= 1.0d) {
913         logger.warning("Undefined Dihedral");
914       }
915       scale(xt, 1.0d / sine, xt);
916       xu[0] = xt[1] * xab[2] - xt[2] * xab[1];
917       xu[1] = xt[2] * xab[0] - xt[0] * xab[2];
918       xu[2] = xt[0] * xab[1] - xt[1] * xab[0];
919       x[0] = xa[0] + bond * (xu[0] * zsin0 * zcos1 + xt[0] * zsin0 * zsin1 - xab[0] * zcos0);
920       x[1] = xa[1] + bond * (xu[1] * zsin0 * zcos1 + xt[1] * zsin0 * zsin1 - xab[1] * zcos0);
921       x[2] = xa[2] + bond * (xu[2] * zsin0 * zcos1 + xt[2] * zsin0 * zsin1 - xab[2] * zcos0);
922     } else if (abs(chiral) == 1) {
923       double[] xa = new double[3];
924       double[] xb = new double[3];
925       double[] xc = new double[3];
926       double[] xba = new double[3];
927       double[] xac = new double[3];
928       double[] xt = new double[3];
929       for (int i = 0; i < ia.length; i++) {
930         xa[i] = ia[i];
931         xb[i] = ib[i];
932         xc[i] = ic[i];
933       }
934       sub(xb, xa, xba);
935       normalize(xba, xba);
936       sub(xa, xc, xac);
937       normalize(xac, xac);
938       xt[0] = xba[2] * xac[1] - xba[1] * xac[2];
939       xt[1] = xba[0] * xac[2] - xba[2] * xac[0];
940       xt[2] = xba[1] * xac[0] - xba[0] * xac[1];
941       double cosine = xba[0] * xac[0] + xba[1] * xac[1] + xba[2] * xac[2];
942       double sine2 = max(1.0d - cosine * cosine, eps);
943       if (abs(cosine) >= 1.0d) {
944         logger.warning("Defining Atom Colinear");
945       }
946       double a = (-zcos1 - cosine * zcos0) / sine2;
947       double b = (zcos0 + cosine * zcos1) / sine2;
948       double c = (1.0d + a * zcos1 - b * zcos0) / sine2;
949       if (c > eps) {
950         c = chiral * sqrt(c);
951       } else if (c < -eps) {
952         c =
953             sqrt(
954                 (a * xac[0] + b * xba[0]) * (a * xac[0] + b * xba[0])
955                     + (a * xac[1] + b * xba[1]) * (a * xac[1] + b * xba[1])
956                     + (a * xac[2] + b * xba[2]) * (a * xac[2] + b * xba[2]));
957         a /= c;
958         b /= c;
959         c = 0.0d;
960       } else {
961         c = 0.0d;
962       }
963       x[0] = xa[0] + bond * (a * xac[0] + b * xba[0] + c * xt[0]);
964       x[1] = xa[1] + bond * (a * xac[1] + b * xba[1] + c * xt[1]);
965       x[2] = xa[2] + bond * (a * xac[2] + b * xba[2] + c * xt[2]);
966     }
967     arraycopy(x, 0, ret, 0, ret.length);
968     return ret;
969   }
970 
971   /** This exception is thrown when an atom type could not be assigned. */
972   public static class MissingAtomTypeException extends Exception {
973 
974     @Serial
975     private static final long serialVersionUID = 1L;
976 
977     public final Residue residue;
978     public final Atom atom;
979 
980     public MissingAtomTypeException(Residue residue, Atom atom) {
981       this.residue = residue;
982       this.atom = atom;
983     }
984 
985     @Override
986     public String toString() {
987       StringBuilder sb = new StringBuilder();
988       sb.append(format(" Atom %s", atom));
989       if (residue != null) {
990         sb.append(format("\n of residue %c-%s", residue.getChainID(), residue));
991       }
992       sb.append("\n could not be assigned an atom type.\n");
993       return sb.toString();
994     }
995   }
996 
997   /** This exception is thrown when a heavy atom is not found. */
998   public static class MissingHeavyAtomException extends Exception {
999 
1000     @Serial
1001     private static final long serialVersionUID = 1L;
1002 
1003     public final String atomName;
1004     public final AtomType atomType;
1005     final Atom bondedTo;
1006 
1007     public MissingHeavyAtomException(String atomName, AtomType atomType, Atom bondedTo) {
1008       this.atomName = atomName;
1009       this.atomType = atomType;
1010       this.bondedTo = bondedTo;
1011     }
1012 
1013     @Override
1014     public String toString() {
1015       StringBuilder sb = new StringBuilder();
1016       if (atomType != null) {
1017         sb.append(format("\n An atom of type\n %s\n", atomType));
1018       } else {
1019         sb.append(format("\n Atom %s", atomName));
1020       }
1021       sb.append(" was not found");
1022       if (bondedTo != null) {
1023         sb.append(format(" bonded to atom %s ", bondedTo));
1024       }
1025       sb.append(".\n");
1026       return sb.toString();
1027     }
1028   }
1029 }