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.parsers;
39  
40  import ffx.crystal.Crystal;
41  import ffx.crystal.SpaceGroup;
42  import ffx.crystal.SpaceGroupDefinitions;
43  import ffx.crystal.SpaceGroupInfo;
44  import ffx.crystal.SymOp;
45  import ffx.numerics.math.DoubleMath;
46  import ffx.potential.MolecularAssembly;
47  import ffx.potential.Utilities.FileType;
48  import ffx.potential.bonded.AminoAcidUtils;
49  import ffx.potential.bonded.AminoAcidUtils.AminoAcid3;
50  import ffx.potential.bonded.Atom;
51  import ffx.potential.bonded.Bond;
52  import ffx.potential.bonded.MSNode;
53  import ffx.potential.bonded.Molecule;
54  import ffx.potential.bonded.NucleicAcidUtils;
55  import ffx.potential.bonded.Polymer;
56  import ffx.potential.bonded.Residue;
57  import ffx.potential.parameters.ForceField;
58  import ffx.utilities.Hybrid36;
59  import ffx.utilities.StringUtils;
60  import org.apache.commons.configuration2.CompositeConfiguration;
61  
62  import java.io.BufferedReader;
63  import java.io.BufferedWriter;
64  import java.io.File;
65  import java.io.FileNotFoundException;
66  import java.io.FileReader;
67  import java.io.FileWriter;
68  import java.io.IOException;
69  import java.util.ArrayList;
70  import java.util.Collections;
71  import java.util.HashMap;
72  import java.util.List;
73  import java.util.Map;
74  import java.util.Objects;
75  import java.util.OptionalDouble;
76  import java.util.Set;
77  import java.util.logging.Level;
78  import java.util.logging.Logger;
79  import java.util.regex.Matcher;
80  import java.util.regex.Pattern;
81  import java.util.stream.Collectors;
82  
83  import static ffx.potential.bonded.BondedUtils.numberAtoms;
84  import static ffx.potential.bonded.NamingUtils.renameAtomsToPDBStandard;
85  import static ffx.potential.bonded.PolymerUtils.assignAtomTypes;
86  import static ffx.potential.bonded.PolymerUtils.buildDisulfideBonds;
87  import static ffx.potential.bonded.PolymerUtils.buildMissingResidues;
88  import static ffx.potential.bonded.PolymerUtils.locateDisulfideBonds;
89  import static ffx.potential.parsers.PDBFilter.PDBFileStandard.VERSION3_2;
90  import static ffx.potential.parsers.PDBFilter.PDBFileStandard.VERSION3_3;
91  import static ffx.utilities.StringUtils.padLeft;
92  import static java.lang.Double.parseDouble;
93  import static java.lang.Integer.parseInt;
94  import static java.lang.String.format;
95  import static org.apache.commons.lang3.StringUtils.repeat;
96  import static org.apache.commons.math3.util.FastMath.min;
97  import static org.apache.commons.math3.util.FastMath.toDegrees;
98  
99  /**
100  * The PDBFilter class parses data from a Protein DataBank (*.PDB) file. The following records are
101  * recognized: ANISOU, ATOM, CONECT, CRYST1, END, HELIX, HETATM, LINK, SHEET, SSBOND, REMARK. The
102  * rest are currently ignored.
103  *
104  * @author Michael J. Schnieders
105  * @see <a href="http://www.wwpdb.org/documentation/format32/v3.2.html">PDB format 3.2</a>
106  * @since 1.0
107  */
108 public final class PDBFilter extends SystemFilter {
109 
110   private static final Logger logger = Logger.getLogger(PDBFilter.class.getName());
111   private static final Set<String> backboneNames;
112   private static final Set<String> constantPhBackboneNames;
113   private static final Set<String> naBackboneNames;
114 
115   static {
116     String[] names = {"C", "CA", "N", "O", "OXT", "OT2"};
117     backboneNames = Set.of(names);
118 
119     String[] constantPhNames = {"C", "CA", "N", "O", "OXT", "OT2", "H", "HA", "H1", "H2", "H3"};
120     constantPhBackboneNames = Set.of(constantPhNames);
121 
122     String[] naNames = {"P", "OP1", "OP2", "O5'", "C5'", "C4'", "O4'", "C3'", "O3'", "C2'", "C1'"};
123     naBackboneNames = Set.of(naNames);
124   }
125 
126   /**
127    * Map of SEQRES entries.
128    */
129   private final Map<Character, String[]> seqRes = new HashMap<>();
130   /**
131    * Map of DBREF entries.
132    */
133   private final Map<Character, int[]> dbRef = new HashMap<>();
134   /**
135    * List of altLoc characters seen in the PDB file.
136    */
137   private final List<Character> altLocs = new ArrayList<>();
138   /**
139    * List of segIDs defined for the PDB file.
140    *
141    * <p>The expectation is for chain naming from A-Z, then from 0-9. For large systems, chain names
142    * are sometimes reused due to limitations in the PDB format.
143    *
144    * <p>However, we define segIDs to always be unique. For the first A-Z,0-9 series chainID ==
145    * segID. Then, for second A-Z,0-9 series, the segID = 1A-1Z,10-19, and for the third series segID
146    * = 2A-2Z,20-29, and so on.
147    */
148   private final List<String> segIDs = new ArrayList<>();
149 
150   private final Map<Character, List<String>> segidMap = new HashMap<>();
151   /**
152    * Maps a chain to the number of insertion codes encountered in that chain.
153    */
154   private final Map<Character, Integer> insertionCodeCount = new HashMap<>();
155   /**
156    * Maps chainIDResNumInsCode to renumbered chainIDResNum. For example, residue 52A in chain C might
157    * be renumbered to residue 53, and mapped as "C52A" to "C53".
158    */
159   private final Map<String, String> pdbToNewResMap = new HashMap<>();
160   /**
161    * List of modified residues *
162    */
163   private final Map<String, String> modRes = new HashMap<>();
164   /**
165    * Keep track of ATOM record serial numbers to match them with ANISOU records.
166    */
167   private final HashMap<Integer, Atom> atoms = new HashMap<>();
168 
169   private final Map<MolecularAssembly, BufferedReader> readers = new HashMap<>();
170   /**
171    * The current altLoc - i.e., the one we are defining a chemical system for.
172    */
173   private Character currentAltLoc = 'A';
174   /**
175    * Character for the current chain ID.
176    */
177   private Character currentChainID = null;
178   /**
179    * String for the current SegID.
180    */
181   private String currentSegID = null;
182   /**
183    * Flag to indicate a mutation is requested.
184    */
185   private boolean mutate = false;
186   private List<Mutation> mutations = null;
187   private List<Integer> resNumberList = null;
188   /**
189    * Flag to indicate if missing fields should be printed (i.e. missing B-factors).
190    */
191   private boolean printMissingFields = true;
192   /**
193    * Number of symmetry operators when expanding to a P1 unit cell (-1 saves as current spacegroup).
194    */
195   private int nSymOp = -1;
196   /**
197    * Number of replicates in A lattice direction (-1 defaults to unit cell).
198    */
199   private int lValue = -1;
200   /**
201    * Number of replicates in B lattice direction (-1 defaults to unit cell).
202    */
203   private int mValue = -1;
204   /**
205    * Number of replicates in C lattice direction (-1 defaults to unit cell).
206    */
207   private int nValue = -1;
208   /**
209    * The serial field continues from the previous asymmetric unit when expanding to P1. This is not
210    * used when saving as the current spacegroup.
211    */
212   private int serialP1 = 0;
213   /**
214    * Assume current standard.
215    */
216   private PDBFileStandard fileStandard = VERSION3_3;
217   /**
218    * If false, skip logging "Saving file".
219    */
220   private boolean logWrites = true;
221   /**
222    * Keep track of the current MODEL in the file.
223    */
224   private int modelsRead = 1;
225   /**
226    * Tracks output MODEL numbers. Unused if below zero.
227    */
228   private int modelsWritten = -1;
229   /**
230    * Replicates vector dimensions if saving as expanded.
231    */
232   private int[] lmn = new int[]{1, 1, 1};
233 
234   private final File readFile;
235   private List<String> remarkLines = Collections.emptyList();
236   private double lastReadLambda = Double.NaN;
237 
238   /**
239    * If true, read in titratable residues in their fully protonated form.
240    */
241   private boolean constantPH = false;
242   /**
243    * If true, read in titratable residues in their protonated form.
244    */
245   private boolean rotamerTitration = false;
246   /**
247    * List of residue to rename for constantPH simulations.
248    */
249   private static final HashMap<AminoAcid3, AminoAcid3> constantPHResidueMap = new HashMap<>();
250 
251   static {
252     // Lysine
253     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.LYD, AminoAcidUtils.AminoAcid3.LYS);
254     // Cysteine
255     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.CYD, AminoAcidUtils.AminoAcid3.CYS);
256     // Histidine
257     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.HID, AminoAcidUtils.AminoAcid3.HIS);
258     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.HIE, AminoAcidUtils.AminoAcid3.HIS);
259     // Aspartate
260     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.ASP, AminoAcidUtils.AminoAcid3.ASD);
261     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.ASH, AminoAcidUtils.AminoAcid3.ASD);
262     // Glutamate
263     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.GLU, AminoAcidUtils.AminoAcid3.GLD);
264     constantPHResidueMap.put(AminoAcidUtils.AminoAcid3.GLH, AminoAcidUtils.AminoAcid3.GLD);
265   }
266 
267   /**
268    * List of residue to rename for rotamer titration simulations.
269    */
270   private static final HashMap<AminoAcid3, AminoAcid3> rotamerResidueMap = new HashMap<>();
271 
272   static {
273     // Lysine
274     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.LYD, AminoAcidUtils.AminoAcid3.LYS);
275     // Histidine
276     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.HID, AminoAcidUtils.AminoAcid3.HIS);
277     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.HIE, AminoAcidUtils.AminoAcid3.HIS);
278     // Aspartate
279     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.ASP, AminoAcidUtils.AminoAcid3.ASH);
280     // Glutamate
281     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.GLU, AminoAcidUtils.AminoAcid3.GLH);
282     //Cysteine
283     rotamerResidueMap.put(AminoAcidUtils.AminoAcid3.CYD, AminoAcidUtils.AminoAcid3.CYS);
284   }
285 
286   /**
287    * Constructor for PDBFilter.
288    *
289    * @param files             a {@link java.util.List} object.
290    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
291    * @param forceField        a {@link ffx.potential.parameters.ForceField} object.
292    * @param properties        a {@link org.apache.commons.configuration2.CompositeConfiguration}
293    *                          object.
294    */
295   public PDBFilter(List<File> files, MolecularAssembly molecularAssembly, ForceField forceField,
296                    CompositeConfiguration properties) {
297     super(files, molecularAssembly, forceField, properties);
298     bondList = new ArrayList<>();
299     this.fileType = FileType.PDB;
300     readFile = files.getFirst();
301   }
302 
303   /**
304    * Parse the PDB File from a URL.
305    *
306    * @param file              a {@link java.io.File} object.
307    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
308    * @param forceField        a {@link ffx.potential.parameters.ForceField} object.
309    * @param properties        a {@link org.apache.commons.configuration2.CompositeConfiguration}
310    *                          object.
311    */
312   public PDBFilter(File file, MolecularAssembly molecularAssembly, ForceField forceField,
313                    CompositeConfiguration properties) {
314     super(file, molecularAssembly, forceField, properties);
315     bondList = new ArrayList<>();
316     this.fileType = FileType.PDB;
317     readFile = file;
318   }
319 
320   /**
321    * Parse the PDB File from a URL.
322    *
323    * @param file                a {@link java.io.File} object.
324    * @param molecularAssemblies a {@link java.util.List} object.
325    * @param forceField          a {@link ffx.potential.parameters.ForceField} object.
326    * @param properties          a {@link org.apache.commons.configuration2.CompositeConfiguration}
327    *                            object.
328    */
329   public PDBFilter(File file, List<MolecularAssembly> molecularAssemblies, ForceField forceField,
330                    CompositeConfiguration properties) {
331     super(file, molecularAssemblies, forceField, properties);
332     bondList = new ArrayList<>();
333     this.fileType = FileType.PDB;
334     readFile = file;
335   }
336 
337   /**
338    * Constructor for PDBFilter with residue numbers.
339    *
340    * @param file              a {@link java.util.List} object.
341    * @param molecularAssembly a {@link ffx.potential.MolecularAssembly} object.
342    * @param forceField        a {@link ffx.potential.parameters.ForceField} object.
343    * @param properties        a {@link org.apache.commons.configuration2.CompositeConfiguration}
344    *                          object.
345    * @param resNumberList     a List of integer residue numbers for constant pH rotamer
346    *                          optimization.
347    */
348   public PDBFilter(File file, MolecularAssembly molecularAssembly, ForceField forceField,
349                    CompositeConfiguration properties, List<Integer> resNumberList) {
350     super(file, molecularAssembly, forceField, properties);
351     bondList = new ArrayList<>();
352     this.fileType = FileType.PDB;
353     this.readFile = file;
354     this.resNumberList = resNumberList;
355     //this.chainList = chainList;
356   }
357 
358 
359   /**
360    * Simple method useful for converting files to PDB format.
361    *
362    * @param atom a {@link ffx.potential.bonded.Atom} object.
363    * @return Returns a PDB ATOM or HETATM record for the passed Atom.
364    */
365   public static String toPDBAtomLine(Atom atom) {
366     StringBuilder sb;
367     if (atom.isHetero()) {
368       sb = new StringBuilder("HETATM");
369     } else {
370       sb = new StringBuilder("ATOM  ");
371     }
372     sb.append(repeat(" ", 74));
373 
374     String name = atom.getName();
375     int nameLength = name.length();
376     if (nameLength > 4) {
377       name = name.substring(0, 4);
378     } else if (nameLength == 1) {
379       name = name + "  ";
380     } else if (nameLength == 2) {
381       name = name + " ";
382     }
383     int serial = atom.getXyzIndex();
384     sb.replace(6, 16, format("%5s " + padLeft(name.toUpperCase(), 4), Hybrid36.encode(5, serial)));
385 
386     Character altLoc = atom.getAltLoc();
387     if (altLoc != null) {
388       sb.setCharAt(16, altLoc);
389     } else {
390       char blankChar = ' ';
391       sb.setCharAt(16, blankChar);
392     }
393 
394     String resName = atom.getResidueName();
395     sb.replace(17, 20, padLeft(resName.toUpperCase(), 3));
396 
397     char chain = atom.getChainID();
398     sb.setCharAt(21, chain);
399 
400     int resID = atom.getResidueNumber();
401     sb.replace(22, 26, format("%4s", Hybrid36.encode(4, resID)));
402 
403     double[] xyz = atom.getXYZ(null);
404     StringBuilder decimals = new StringBuilder();
405     for (int i = 0; i < 3; i++) {
406       try {
407         decimals.append(StringUtils.fwFpDec(xyz[i], 8, 3));
408       } catch (IllegalArgumentException ex) {
409         String newValue = StringUtils.fwFpTrunc(xyz[i], 8, 3);
410         logger.info(format(" XYZ coordinate %8.3f for atom %s overflowed PDB format and is truncated to %s.",
411             xyz[i], atom, newValue));
412         decimals.append(newValue);
413       }
414     }
415     try {
416       decimals.append(StringUtils.fwFpDec(atom.getOccupancy(), 6, 2));
417     } catch (IllegalArgumentException ex) {
418       logger.severe(format(" Occupancy %6.2f for atom %s must be between 0 and 1.", atom.getOccupancy(), atom));
419     }
420     try {
421       decimals.append(StringUtils.fwFpDec(atom.getTempFactor(), 6, 2));
422     } catch (IllegalArgumentException ex) {
423       String newValue = StringUtils.fwFpTrunc(atom.getTempFactor(), 6, 2);
424       logger.info(
425           format(" B-factor %6.2f for atom %s overflowed the PDB format and is truncated to %s.",
426               atom.getTempFactor(), atom, newValue));
427       decimals.append(newValue);
428     }
429 
430     sb.replace(30, 66, decimals.toString());
431     sb.replace(78, 80, format("%2d", 0));
432     sb.append("\n");
433     return sb.toString();
434   }
435 
436   public void setConstantPH(boolean constantPH) {
437     this.constantPH = constantPH;
438   }
439 
440   public void setRotamerTitration(boolean rotamerTitration) {
441     this.rotamerTitration = rotamerTitration;
442   }
443 
444   /**
445    * clearSegIDs
446    */
447   public void clearSegIDs() {
448     segIDs.clear();
449   }
450 
451   /**
452    * {@inheritDoc}
453    */
454   @Override
455   public void closeReader() {
456     for (MolecularAssembly system : systems) {
457       BufferedReader br = readers.get(system);
458       if (br != null) {
459         try {
460           br.close();
461         } catch (IOException ex) {
462           logger.warning(format(" Exception in closing system %s: %s", system.toString(), ex));
463         }
464       }
465     }
466   }
467 
468   @Override
469   public int countNumModels() {
470     Set<File> files = systems.stream().map(MolecularAssembly::getFile).map(File::toString).distinct()
471         .map(File::new).collect(Collectors.toSet());
472 
473     // Dangers of parallelism are minimized by: unique files/filenames, read-only access.
474     return files.parallelStream().mapToInt((File fi) -> {
475       int nModelsLocal = 0;
476       try (BufferedReader br = new BufferedReader(new FileReader(fi))) {
477         String line = br.readLine();
478         while (line != null) {
479           if (line.startsWith("MODEL")) {
480             ++nModelsLocal;
481           }
482           line = br.readLine();
483         }
484         nModelsLocal = Math.max(1, nModelsLocal);
485       } catch (IOException ex) {
486         logger.info(format(" Exception in parsing file %s: %s", fi, ex));
487       }
488       return nModelsLocal;
489     }).sum();
490   }
491 
492   /**
493    * Get the list of alternate locations encountered.
494    *
495    * @return the alternate location list.
496    */
497   public List<Character> getAltLocs() {
498     return altLocs;
499   }
500 
501   /**
502    * {@inheritDoc}
503    */
504   @Override
505   public OptionalDouble getLastReadLambda() {
506     return Double.isNaN(lastReadLambda) ? OptionalDouble.empty() : OptionalDouble.of(lastReadLambda);
507   }
508 
509   /**
510    * Returns all the remark lines found by the last readFile call.
511    *
512    * @return Remark lines from the last readFile call.
513    */
514   @Override
515   public String[] getRemarkLines() {
516     int nRemarks = remarkLines.size();
517     return remarkLines.toArray(new String[nRemarks]);
518   }
519 
520   @Override
521   public int getSnapshot() {
522     return modelsRead;
523   }
524 
525   /**
526    * Mutate residue(s) as the PDB file is being parsed.
527    *
528    * @param mutations a {@link java.util.List} object.
529    */
530   public void mutate(List<Mutation> mutations) {
531     mutate = true;
532     if (this.mutations == null) {
533       this.mutations = new ArrayList<>();
534     }
535     this.mutations.addAll(mutations);
536   }
537 
538   /**
539    * Parse the PDB File
540    */
541   @Override
542   public boolean readFile() {
543     remarkLines = new ArrayList<>();
544     // First atom is #1, to match xyz file format
545     int xyzIndex = 1;
546     setFileRead(false);
547     systems.add(activeMolecularAssembly);
548 
549     List<String> conects = new ArrayList<>();
550     List<String> links = new ArrayList<>();
551     List<String> ssbonds = new ArrayList<>();
552     List<String> structs = new ArrayList<>();
553     try {
554       for (File file : files) {
555         currentFile = file;
556         if (mutate) {
557           List<Character> chainIDs = new ArrayList<>();
558           try (BufferedReader br = new BufferedReader(new FileReader(file))) {
559             String line = br.readLine();
560             while (line != null) {
561               // Replace all tabs w/ 4x spaces
562               line = line.replaceAll("\t", "    ");
563               String identity = line;
564               if (line.length() > 6) {
565                 identity = line.substring(0, 6);
566               }
567               identity = identity.trim().toUpperCase();
568               Record record;
569               try {
570                 record = Record.valueOf(identity);
571               } catch (Exception e) {
572                 // Continue until the record is recognized.
573                 line = br.readLine();
574                 continue;
575               }
576               switch (record) {
577                 case ANISOU, HETATM, ATOM -> {
578                   char c22 = line.charAt(21);
579                   boolean idFound = false;
580                   for (Character chainID : chainIDs) {
581                     if (c22 == chainID) {
582                       idFound = true;
583                       break;
584                     }
585                   }
586                   if (!idFound) {
587                     chainIDs.add(c22);
588                   }
589                 }
590               }
591               line = br.readLine();
592             }
593             for (Mutation mtn : mutations) {
594               if (!chainIDs.contains(mtn.chainChar)) {
595                 if (chainIDs.size() == 1) {
596                   logger.warning(
597                       format(" Chain ID %c for mutation not found: only one chain %c found.",
598                           mtn.chainChar, chainIDs.getFirst()));
599                 } else {
600                   logger.warning(
601                       format(" Chain ID %c for mutation not found: mutation will not proceed.",
602                           mtn.chainChar));
603                 }
604               }
605             }
606           } catch (IOException ioException) {
607             logger.fine(format(" Exception %s in parsing file to find chain IDs", ioException));
608           }
609         }
610 
611         // Check that the current file exists and that we can read it.
612         if (currentFile == null || !currentFile.exists() || !currentFile.canRead()) {
613           return false;
614         }
615 
616         // Open the current file for parsing.
617         try (BufferedReader br = new BufferedReader(new FileReader(currentFile))) {
618           // Echo the alternate location being parsed.
619           if (currentAltLoc == 'A') {
620             logger.info(format(" Reading %s", currentFile.getName()));
621           } else {
622             logger.info(format(" Reading %s alternate location %s", currentFile.getName(), currentAltLoc));
623           }
624           activeMolecularAssembly.setAlternateLocation(currentAltLoc);
625 
626           // Reset the current chain and segID.
627           currentChainID = null;
628           currentSegID = null;
629           boolean containsInsCode = false;
630 
631           // Read the first line of the file.
632           String line = br.readLine();
633 
634           // Parse until END or ENDMDL is found, or to the end of the file.
635           while (line != null) {
636             // Replace all tabs w/ 4x spaces
637             line = line.replaceAll("\t", "    ");
638             String identity = line;
639             if (line.length() > 6) {
640               identity = line.substring(0, 6);
641             }
642             identity = identity.trim().toUpperCase();
643             Record record;
644             try {
645               record = Record.valueOf(identity);
646             } catch (Exception e) {
647               // Continue until the record is recognized.
648               line = br.readLine();
649               continue;
650             }
651 
652             // Switch on the known record.
653             switch (record) {
654               case ENDMDL:
655               case END: {
656                 // Setting "line" to null will exit the loop.
657                 line = null;
658                 continue;
659               }
660               case DBREF: {
661 // =============================================================================
662 //  1 -  6       Record name   "DBREF "
663 //  8 - 11       IDcode        idCode             ID code of this entry.
664 // 13            Character     chainID            Chain identifier.
665 // 15 - 18       Integer       seqBegin           Initial sequence number of the
666 //                                                PDB sequence segment.
667 // 19            AChar         insertBegin        Initial insertion code of the
668 //                                                PDB sequence segment.
669 // 21 - 24       Integer       seqEnd             Ending sequence number of the
670 //                                                PDB sequence segment.
671 // 25            AChar         insertEnd          Ending insertion code of the
672 //                                                PDB sequence segment.
673 // 27 - 32       LString       database           Sequence database name.
674 // 34 - 41       LString       dbAccession        Sequence database accession code.
675 // 43 - 54       LString       dbIdCode           Sequence  database identification code.
676 // 56 - 60       Integer       dbseqBegin         Initial sequence number of the
677 //                                                database seqment.
678 // 61            AChar         idbnsBeg           Insertion code of initial residue of the
679 //                                                segment, if PDB is the reference.
680 // 63 - 67       Integer       dbseqEnd           Ending sequence number of the
681 //                                                database segment.
682 // 68            AChar         dbinsEnd           Insertion code of the ending residue of
683 //                                                the segment, if PDB is the reference.
684 // =============================================================================
685                 Character chainID = line.substring(12, 13).toUpperCase().charAt(0);
686                 int seqBegin = parseInt(line.substring(14, 18).trim());
687                 int seqEnd = parseInt(line.substring(20, 24).trim());
688                 int[] seqRange = dbRef.computeIfAbsent(chainID, k -> new int[2]);
689                 seqRange[0] = seqBegin;
690                 seqRange[1] = seqEnd;
691                 break;
692               }
693               case SEQRES: {
694 // =============================================================================
695 //  1 -  6        Record name    "SEQRES"
696 //  8 - 10        Integer        serNum       Serial number of the SEQRES record for the
697 //                                            current  chain. Starts at 1 and increments
698 //                                            by one  each line. Reset to 1 for each chain.
699 // 12             Character      chainID      Chain identifier. This may be any single
700 //                                            legal  character, including a blank which is
701 //                                            is used if there is only one chain.
702 // 14 - 17        Integer        numRes       Number of residues in the chain.
703 //                                            This  value is repeated on every record.
704 // 20 - 22        Residue name   resName      Residue name.
705 // 24 - 26        Residue name   resName      Residue name.
706 // 28 - 30        Residue name   resName      Residue name.
707 // 32 - 34        Residue name   resName      Residue name.
708 // 36 - 38        Residue name   resName      Residue name.
709 // 40 - 42        Residue name   resName      Residue name.
710 // 44 - 46        Residue name   resName      Residue name.
711 // 48 - 50        Residue name   resName      Residue name.
712 // 52 - 54        Residue name   resName      Residue name.
713 // 56 - 58        Residue name   resName      Residue name.
714 // 60 - 62        Residue name   resName      Residue name.
715 // 64 - 66        Residue name   resName      Residue name.
716 // 68 - 70        Residue name   resName      Residue name.
717 // =============================================================================
718                 activeMolecularAssembly.addHeaderLine(line);
719                 Character chainID = line.substring(11, 12).toUpperCase().charAt(0);
720                 int serNum = parseInt(line.substring(7, 10).trim());
721                 String[] chain = seqRes.get(chainID);
722                 int numRes = parseInt(line.substring(13, 17).trim());
723                 if (chain == null) {
724                   chain = new String[numRes];
725                   seqRes.put(chainID, chain);
726                 }
727                 int resID = (serNum - 1) * 13;
728                 int end = line.length();
729                 for (int start = 19; start + 3 <= end; start += 4) {
730                   String res = line.substring(start, start + 3).trim();
731                   if (res.isEmpty()) {
732                     break;
733                   }
734                   chain[resID++] = res;
735                 }
736                 break;
737               }
738               case MODRES: {
739 // =============================================================================
740 //  1 -  6        Record name     "MODRES"
741 //  8 - 11        IDcode          idCode         ID code of this entry.
742 // 13 - 15        Residue name    resName        Residue name used in this entry.
743 // 17             Character       chainID        Chain identifier.
744 // 19 - 22        Integer         seqNum         Sequence number.
745 // 23             AChar           iCode          Insertion code.
746 // 25 - 27        Residue name    stdRes         Standard residue name.
747 // 30 - 70        String          comment        Description of the residue modification.
748 // =============================================================================
749                 String modResName = line.substring(12, 15).trim();
750                 String stdName = line.substring(24, 27).trim();
751                 modRes.put(modResName.toUpperCase(), stdName.toUpperCase());
752                 activeMolecularAssembly.addHeaderLine(line);
753                 break;
754               }
755               case ANISOU: {
756 // =============================================================================
757 //  1 - 6        Record name   "ANISOU"
758 //  7 - 11       Integer       serial         Atom serial number.
759 // 13 - 16       Atom          name           Atom name.
760 // 17            Character     altLoc         Alternate location indicator
761 // 18 - 20       Residue name  resName        Residue name.
762 // 22            Character     chainID        Chain identifier.
763 // 23 - 26       Integer       resSeq         Residue sequence number.
764 // 27            AChar         iCode          Insertion code.
765 // 29 - 35       Integer       u[0][0]        U(1,1)
766 // 36 - 42       Integer       u[1][1]        U(2,2)
767 // 43 - 49       Integer       u[2][2]        U(3,3)
768 // 50 - 56       Integer       u[0][1]        U(1,2)
769 // 57 - 63       Integer       u[0][2]        U(1,3)
770 // 64 - 70       Integer       u[1][2]        U(2,3)
771 // 77 - 78       LString(2)    element        Element symbol, right-justified.
772 // 79 - 80       LString(2)    charge         Charge on the atom.
773 // =============================================================================
774                 boolean deleteAnisou = properties.getBoolean("delete-anisou", false);
775                 double resetBfactors = properties.getDouble("reset-bfactors", -1.0);
776                 if (deleteAnisou || resetBfactors >= 0.0) {
777                   break;
778                 }
779                 Integer serial = Hybrid36.decode(5, line.substring(6, 11));
780                 Character altLoc = line.substring(16, 17).toUpperCase().charAt(0);
781                 if (!altLocs.contains(altLoc)) {
782                   altLocs.add(altLoc);
783                 }
784                 if (!altLoc.equals(' ') && !altLoc.equals('A') && !altLoc.equals(currentAltLoc)) {
785                   break;
786                 }
787                 double[] adp = new double[6];
788                 adp[0] = parseInt(line.substring(28, 35).trim()) * 1.0e-4;
789                 adp[1] = parseInt(line.substring(35, 42).trim()) * 1.0e-4;
790                 adp[2] = parseInt(line.substring(42, 49).trim()) * 1.0e-4;
791                 adp[3] = parseInt(line.substring(49, 56).trim()) * 1.0e-4;
792                 adp[4] = parseInt(line.substring(56, 63).trim()) * 1.0e-4;
793                 adp[5] = parseInt(line.substring(63, 70).trim()) * 1.0e-4;
794                 if (atoms.containsKey(serial)) {
795                   Atom a = atoms.get(serial);
796                   a.setAltLoc(altLoc);
797                   a.setAnisou(adp);
798                 } else {
799                   logger.info(format(" No ATOM record for ANISOU serial number %d has been found.", serial));
800                   logger.info(format(" This ANISOU record will be ignored:\n %s", line));
801                 }
802                 break;
803               }
804               case ATOM: {
805 // =============================================================================
806 //  1 -  6        Record name   "ATOM  "
807 //  7 - 11        Integer       serial       Atom serial number.
808 // 13 - 16        Atom          name         Atom name.
809 // 17             Character     altLoc       Alternate location indicator.
810 // 18 - 20        Residue name  resName      Residue name.
811 // 22             Character     chainID      Chain identifier.
812 // 23 - 26        Integer       resSeq       Residue sequence number.
813 // 27             AChar         iCode        Code for insertion of residues.
814 // 31 - 38        Real(8.3)     x            Orthogonal coordinates for X in Angstroms.
815 // 39 - 46        Real(8.3)     y            Orthogonal coordinates for Y in Angstroms.
816 // 47 - 54        Real(8.3)     z            Orthogonal coordinates for Z in Angstroms.
817 // 55 - 60        Real(6.2)     occupancy    Occupancy.
818 // 61 - 66        Real(6.2)     tempFactor   Temperature factor.
819 // 77 - 78        LString(2)    element      Element symbol, right-justified.
820 // 79 - 80        LString(2)    charge       Charge  on the atom.
821 // =============================================================================
822                 // If it's a misnamed water, it will fall through to HETATM.
823                 Integer serial = Hybrid36.decode(5, line.substring(6, 11));
824                 String name = line.substring(12, 16).trim();
825                 if (name.toUpperCase().contains("1H") || name.toUpperCase().contains("2H")
826                     || name.toUpperCase().contains("3H")) {
827                   // VERSION3_2 is presently just a placeholder for "anything non-standard".
828                   fileStandard = VERSION3_2;
829                 }
830                 Character altLoc = line.substring(16, 17).toUpperCase().charAt(0);
831                 if (!altLocs.contains(altLoc)) {
832                   altLocs.add(altLoc);
833                 }
834 
835                 if (!altLoc.equals(' ') && !altLoc.equals('A') && !altLoc.equals(currentAltLoc)) {
836                   break;
837                 }
838 
839                 String resName = line.substring(17, 20).trim();
840                 Character chainID = line.substring(21, 22).charAt(0);
841                 String segID = getSegID(chainID);
842                 int resSeq = Hybrid36.decode(4, line.substring(22, 26));
843 
844                 char insertionCode = line.charAt(26);
845                 if (insertionCode != ' ' && !containsInsCode) {
846                   containsInsCode = true;
847                   logger.warning(
848                       " FFX support for files with " + "insertion codes is experimental. "
849                           + "Residues will be renumbered to " + "eliminate insertion codes (52A "
850                           + "becomes 53, 53 becomes 54, etc)");
851                 }
852 
853                 int offset = insertionCodeCount.getOrDefault(chainID, 0);
854                 String pdbResNum = format("%c%d%c", chainID, resSeq, insertionCode);
855                 if (!pdbToNewResMap.containsKey(pdbResNum)) {
856                   if (insertionCode != ' ') {
857                     ++offset;
858                     insertionCodeCount.put(chainID, offset);
859                   }
860                   resSeq += offset;
861                   if (offset != 0) {
862                     logger.info(
863                         format(" Chain %c " + "residue %s-%s renumbered to %c %s-%d", chainID,
864                             pdbResNum.substring(1).trim(), resName, chainID, resName, resSeq));
865                   }
866                   String newNum = format("%c%d", chainID, resSeq);
867                   pdbToNewResMap.put(pdbResNum, newNum);
868                 } else {
869                   resSeq += offset;
870                 }
871 
872                 boolean printAtom = false;
873                 if (mutate) {
874                   boolean doBreak = false;
875                   for (Mutation mtn : mutations) {
876                     if (chainID == mtn.chainChar && resSeq == mtn.resID) {
877                       mtn.origResName = resName;
878                       resName = mtn.resName;
879                       String atomName = name.toUpperCase();
880 
881                       int isAA = AminoAcidUtils.getAminoAcidNumber(resName);
882                       int isNA = NucleicAcidUtils.getNucleicAcidNumber(resName);
883 
884                       if ((isNA != -1 && naBackboneNames.contains(atomName)) || (isAA != -1 && backboneNames.contains(atomName))) {
885                         printAtom = true;
886                       } else {
887                         // grab pur-pur or pyr-pyr alchem. atoms
888                         ArrayList<String> alchAtoms = mtn.getAlchemicalAtoms(false);
889                         if (alchAtoms == null) {
890                           // test to see if atom is involved in glycosyl torsion and if needs renaming (pyr-pur/pur-pyr)
891                           String newName = mtn.isNonAlchemicalAtom(atomName);
892                           if (newName != null) { // if not null -- use name it
893                             printAtom = true;
894                             if (newName.startsWith("~")) { // switch from purine to pyrmidine or v.v.
895                               // switch name and include it as an alchemical atom
896                               name = newName.substring(1);
897                               logger.info(format(" DELETING atom %d %s of %s %d in chain %s", serial, atomName, resName, resSeq, chainID));
898                             } else {
899                               // replace name but do not include as an alchemical atom
900                               name = newName;
901                             }
902                             doBreak = false;
903                           } else if (!atomName.contains("'")) {
904                             logger.info(format(" DELETING atom %d %s of %s %d in chain %s", serial, atomName, resName, resSeq, chainID));
905                             doBreak = true;
906                           } else {
907                             printAtom = true;
908                             doBreak = false;
909                           }
910                         } else {
911                           if (alchAtoms.contains(atomName) && !atomName.contains("'")) {
912                             logger.info(format(" DELETING atom %d %s of %s %d in chain %s", serial, atomName, resName, resSeq, chainID));
913                             doBreak = true;
914                           } else {
915                             printAtom = true;
916                             doBreak = false;
917                           }
918                         }
919                         break;
920                       }
921                     }
922                   }
923                   if (doBreak) {
924                     break;
925                   }
926                 }
927 
928                 if (constantPH) {
929                   AminoAcid3 aa3 = AminoAcidUtils.getAminoAcid(resName.toUpperCase());
930                   if (constantPHResidueMap.containsKey(aa3)) {
931                     String atomName = name.toUpperCase();
932                     AminoAcid3 aa3PH = constantPHResidueMap.get(aa3);
933                     resName = aa3PH.name();
934                     if (constantPhBackboneNames.contains(atomName)) {
935                       logger.info(format(" %s-%d %s", resName, resSeq, atomName));
936                     } else if (!atomName.startsWith("H")) {
937                       logger.info(format(" %s-%d %s", resName, resSeq, atomName));
938                     } else {
939                       logger.info(format(" %s-%d %s skipped", resName, resSeq, atomName));
940                       break;
941                     }
942                   }
943                 } else if (rotamerTitration) {
944                   AminoAcid3 aa3 = AminoAcidUtils.getAminoAcid(resName.toUpperCase());
945                   if (rotamerResidueMap.containsKey(aa3) && resNumberList.contains(resSeq)) {
946                     AminoAcid3 aa3rotamer = rotamerResidueMap.get(aa3);
947                     resName = aa3rotamer.name();
948                   }
949                 }
950                 double[] d = new double[3];
951                 d[0] = parseDouble(line.substring(30, 38).trim());
952                 d[1] = parseDouble(line.substring(38, 46).trim());
953                 d[2] = parseDouble(line.substring(46, 54).trim());
954                 double occupancy = 1.0;
955                 double tempFactor = 1.0;
956                 try {
957                   occupancy = parseDouble(line.substring(54, 60).trim());
958                   tempFactor = parseDouble(line.substring(60, 66).trim());
959                 } catch (NumberFormatException | StringIndexOutOfBoundsException e) {
960                   // Use default values.
961                   if (printMissingFields) {
962                     logger.info(" Missing occupancy and b-factors set to 1.0.");
963                     printMissingFields = false;
964                   } else if (logger.isLoggable(Level.FINE)) {
965                     logger.fine(" Missing occupancy and b-factors set to 1.0.");
966                   }
967                 }
968 
969                 double bfactor = properties.getDouble("reset-bfactors", -1.0);
970                 if (bfactor >= 0.0) {
971                   tempFactor = bfactor;
972                 }
973 
974                 Atom newAtom = new Atom(0, name, altLoc, d, resName, resSeq, chainID, occupancy, tempFactor, segID);
975 
976                 // Check if this is a modified residue.
977                 if (modRes.containsKey(resName.toUpperCase())) {
978                   newAtom.setModRes(true);
979                 }
980                 Atom returnedAtom = (Atom) activeMolecularAssembly.addMSNode(newAtom);
981                 if (returnedAtom != newAtom) {
982                   // A previously added atom has been retained.
983                   atoms.put(serial, returnedAtom);
984                   if (logger.isLoggable(Level.FINE)) {
985                     logger.fine(returnedAtom + " has been retained over\n" + newAtom);
986                   }
987                 } else {
988                   // The new atom has been added.
989                   atoms.put(serial, newAtom);
990                   // Check if the newAtom took the xyzIndex of a previous alternate conformer.
991                   if (newAtom.getIndex() == 0) {
992                     newAtom.setXyzIndex(xyzIndex++);
993                   }
994                   if (printAtom) {
995                     logger.info(newAtom.toString());
996                   }
997                 }
998                 break;
999               }
1000               case HETATM: {
1001 // =============================================================================
1002 //  1 - 6        Record name    "HETATM"
1003 //  7 - 11       Integer        serial        Atom serial number.
1004 // 13 - 16       Atom           name          Atom name.
1005 // 17            Character      altLoc        Alternate location indicator.
1006 // 18 - 20       Residue name   resName       Residue name.
1007 // 22            Character      chainID       Chain identifier.
1008 // 23 - 26       Integer        resSeq        Residue sequence number.
1009 // 27            AChar          iCode         Code for insertion of residues.
1010 // 31 - 38       Real(8.3)      x             Orthogonal coordinates for X.
1011 // 39 - 46       Real(8.3)      y             Orthogonal coordinates for Y.
1012 // 47 - 54       Real(8.3)      z             Orthogonal coordinates for Z.
1013 // 55 - 60       Real(6.2)      occupancy     Occupancy.
1014 // 61 - 66       Real(6.2)      tempFactor    Temperature factor.
1015 // 77 - 78       LString(2)     element       Element symbol; right-justified.
1016 // 79 - 80       LString(2)     charge        Charge on the atom.
1017 // =============================================================================
1018                 Integer serial = Hybrid36.decode(5, line.substring(6, 11));
1019                 String name = line.substring(12, 16).trim();
1020                 Character altLoc = line.substring(16, 17).toUpperCase().charAt(0);
1021                 if (!altLocs.contains(altLoc)) {
1022                   altLocs.add(altLoc);
1023                 }
1024                 if (!altLoc.equals(' ') && !altLoc.equals(currentAltLoc)) {
1025                   break;
1026                 }
1027                 // if (!altLoc.equals(' ') && !altLoc.equals('A') && !altLoc.equals(currentAltLoc)) {
1028                 //  break;
1029                 //}
1030                 String resName = line.substring(17, 20).trim();
1031                 Character chainID = line.substring(21, 22).charAt(0);
1032                 String segID = getSegID(chainID);
1033                 int resSeq = Hybrid36.decode(4, line.substring(22, 26));
1034 
1035                 char insertionCode = line.charAt(26);
1036                 if (insertionCode != ' ' && !containsInsCode) {
1037                   containsInsCode = true;
1038                   logger.warning(" FFX support for files with " + "insertion codes is experimental. "
1039                       + "Residues will be renumbered to " + "eliminate insertion codes (52A "
1040                       + "becomes 53, 53 becomes 54, etc)");
1041                 }
1042 
1043                 int offset = insertionCodeCount.getOrDefault(chainID, 0);
1044                 String pdbResNum = format("%c%d%c", chainID, resSeq, insertionCode);
1045                 if (!pdbToNewResMap.containsKey(pdbResNum)) {
1046                   if (insertionCode != ' ') {
1047                     ++offset;
1048                     insertionCodeCount.put(chainID, offset);
1049                   }
1050                   resSeq += offset;
1051                   if (offset != 0) {
1052                     logger.info(
1053                         format(" Chain %c " + "molecule %s-%s renumbered to %c %s-%d", chainID,
1054                             pdbResNum.substring(1).trim(), resName, chainID, resName, resSeq));
1055                   }
1056                   String newNum = format("%c%d", chainID, resSeq);
1057                   pdbToNewResMap.put(pdbResNum, newNum);
1058                 } else {
1059                   resSeq += offset;
1060                 }
1061 
1062                 double[] d = new double[3];
1063                 d[0] = parseDouble(line.substring(30, 38).trim());
1064                 d[1] = parseDouble(line.substring(38, 46).trim());
1065                 d[2] = parseDouble(line.substring(46, 54).trim());
1066                 double occupancy = 1.0;
1067                 double tempFactor = 1.0;
1068                 try {
1069                   occupancy = parseDouble(line.substring(54, 60).trim());
1070                   tempFactor = parseDouble(line.substring(60, 66).trim());
1071                 } catch (NumberFormatException | StringIndexOutOfBoundsException e) {
1072                   // Use default values.
1073                   if (printMissingFields) {
1074                     logger.info(" Missing occupancy and b-factors set to 1.0.");
1075                     printMissingFields = false;
1076                   } else if (logger.isLoggable(Level.FINE)) {
1077                     logger.fine(" Missing occupancy and b-factors set to 1.0.");
1078                   }
1079                 }
1080 
1081                 double bfactor = properties.getDouble("reset-bfactors", -1.0);
1082                 if (bfactor >= 0.0) {
1083                   tempFactor = bfactor;
1084                 }
1085 
1086                 Atom newAtom = new Atom(0, name, altLoc, d, resName, resSeq, chainID, occupancy, tempFactor, segID);
1087                 newAtom.setHetero(true);
1088                 // Check if this is a modified residue.
1089                 if (modRes.containsKey(resName.toUpperCase())) {
1090                   newAtom.setModRes(true);
1091                 }
1092                 Atom returnedAtom = (Atom) activeMolecularAssembly.addMSNode(newAtom);
1093                 if (returnedAtom != newAtom) {
1094                   // A previously added atom has been retained.
1095                   atoms.put(serial, returnedAtom);
1096                   if (logger.isLoggable(Level.FINE)) {
1097                     logger.fine(returnedAtom + " has been retained over\n" + newAtom);
1098                   }
1099                 } else {
1100                   // The new atom has been added.
1101                   atoms.put(serial, newAtom);
1102                   newAtom.setXyzIndex(xyzIndex++);
1103                 }
1104                 break;
1105               }
1106               case CRYST1: {
1107 // =============================================================================
1108 // The CRYST1 record presents the unit cell parameters, space group, and Z
1109 // value. If the structure was not determined by crystallographic means, CRYST1
1110 // simply provides the unitary values, with an appropriate REMARK.
1111 //
1112 //  7 - 15       Real(9.3)     a              a (Angstroms).
1113 // 16 - 24       Real(9.3)     b              b (Angstroms).
1114 // 25 - 33       Real(9.3)     c              c (Angstroms).
1115 // 34 - 40       Real(7.2)     alpha          alpha (degrees).
1116 // 41 - 47       Real(7.2)     beta           beta (degrees).
1117 // 48 - 54       Real(7.2)     gamma          gamma (degrees).
1118 // 56 - 66       LString       sGroup         Space  group.
1119 // 67 - 70       Integer       z              Z value.
1120 // =============================================================================
1121                 if (line.length() < 55) {
1122                   logger.severe(" CRYST1 record is improperly formatted.");
1123                 }
1124                 double aaxis = parseDouble(line.substring(6, 15).trim());
1125                 double baxis = parseDouble(line.substring(15, 24).trim());
1126                 double caxis = parseDouble(line.substring(24, 33).trim());
1127                 double alpha = parseDouble(line.substring(33, 40).trim());
1128                 double beta = parseDouble(line.substring(40, 47).trim());
1129                 double gamma = parseDouble(line.substring(47, 54).trim());
1130                 int limit = min(line.length(), 66);
1131                 String sg = line.substring(55, limit).trim();
1132                 properties.addProperty("a-axis", aaxis);
1133                 properties.addProperty("b-axis", baxis);
1134                 properties.addProperty("c-axis", caxis);
1135                 properties.addProperty("alpha", alpha);
1136                 properties.addProperty("beta", beta);
1137                 properties.addProperty("gamma", gamma);
1138                 properties.addProperty("spacegroup", SpaceGroupInfo.pdb2ShortName(sg));
1139                 break;
1140               }
1141               case CONECT: {
1142 // =============================================================================
1143 //  7 - 11        Integer        serial       Atom  serial number
1144 // 12 - 16        Integer        serial       Serial number of bonded atom
1145 // 17 - 21        Integer        serial       Serial number of bonded atom
1146 // 22 - 26        Integer        serial       Serial number of bonded atom
1147 // 27 - 31        Integer        serial       Serial number of bonded atom
1148 //
1149 // CONECT records involving atoms for which the coordinates are not present
1150 // in the entry (e.g., symmetry-generated) are not given.
1151 // CONECT records involving atoms for which the coordinates are missing due
1152 // to disorder, are also not provided.
1153 // =============================================================================
1154                 conects.add(line);
1155                 break;
1156               }
1157               case LINK: {
1158 // =============================================================================
1159 // The LINK records specify connectivity between residues that is not implied by
1160 // the primary structure. Connectivity is expressed in terms of the atom names.
1161 // They also include the distance associated with each linkage following the
1162 // symmetry operations at the end of each record.
1163 // 13 - 16         Atom           name1           Atom name.
1164 // 17              Character      altLoc1         Alternate location indicator.
1165 // 18 - 20         Residue name   resName1        Residue  name.
1166 // 22              Character      chainID1        Chain identifier.
1167 // 23 - 26         Integer        resSeq1         Residue sequence number.
1168 // 27              AChar          iCode1          Insertion code.
1169 // 43 - 46         Atom           name2           Atom name.
1170 // 47              Character      altLoc2         Alternate location indicator.
1171 // 48 - 50         Residue name   resName2        Residue name.
1172 // 52              Character      chainID2        Chain identifier.
1173 // 53 - 56         Integer        resSeq2         Residue sequence number.
1174 // 57              AChar          iCode2          Insertion code.
1175 // 60 - 65         SymOP          sym1            Symmetry operator atom 1.
1176 // 67 - 72         SymOP          sym2            Symmetry operator atom 2.
1177 // 74 – 78         Real(5.2)      Length          Link distance
1178 // =============================================================================
1179                 char a1 = line.charAt(16);
1180                 char a2 = line.charAt(46);
1181                 if (a1 != a2) {
1182                   // logger.info(format(" Ignoring LINK record as alternate locations do not match\n
1183                   // %s.", line));
1184                   break;
1185                 }
1186                 if (currentAltLoc == 'A') {
1187                   if ((a1 == ' ' || a1 == 'A') && (a2 == ' ' || a2 == 'A')) {
1188                     links.add(line);
1189                   }
1190                 } else if (a1 == currentAltLoc && a2 == currentAltLoc) {
1191                   links.add(line);
1192                 }
1193                 break;
1194               }
1195               case SSBOND: {
1196 // =============================================================================
1197 // The SSBOND record identifies each disulfide bond in protein and polypeptide
1198 // structures by identifying the two residues involved in the bond.
1199 // The disulfide bond distance is included after the symmetry operations at
1200 // the end of the SSBOND record.
1201 //
1202 //  8 - 10        Integer         serNum       Serial number.
1203 // 12 - 14        LString(3)      "CYS"        Residue name.
1204 // 16             Character       chainID1     Chain identifier.
1205 // 18 - 21        Integer         seqNum1      Residue sequence number.
1206 // 22             AChar           icode1       Insertion code.
1207 // 26 - 28        LString(3)      "CYS"        Residue name.
1208 // 30             Character       chainID2     Chain identifier.
1209 // 32 - 35        Integer         seqNum2      Residue sequence number.
1210 // 36             AChar           icode2       Insertion code.
1211 // 60 - 65        SymOP           sym1         Symmetry oper for 1st resid
1212 // 67 - 72        SymOP           sym2         Symmetry oper for 2nd resid
1213 // 74 – 78        Real(5.2)      Length        Disulfide bond distance
1214 //
1215 // If SG of cysteine is disordered then there are possible alternate linkages.
1216 // wwPDB practice is to put together all possible SSBOND records. This is
1217 // problematic because the alternate location identifier is not specified in
1218 // the SSBOND record.
1219 //
1220 // Notes:
1221 // SSBOND records may be invalid if chain IDs are reused.
1222 // SSBOND records are applied by FFX to all conformers.
1223 // =============================================================================
1224                 ssbonds.add(line);
1225                 break;
1226               }
1227               case HELIX: {
1228 // =============================================================================
1229 // HELIX records are used to identify the position of helices in the molecule.
1230 // Helices are named, numbered, and classified by type. The residues where the
1231 // helix begins and ends are noted, as well as the total length.
1232 //
1233 //  8 - 10        Integer        serNum        Serial number of the helix. This starts
1234 //                                             at 1  and increases incrementally.
1235 // 12 - 14        LString(3)     helixID       Helix  identifier. In addition to a serial
1236 //                                             number, each helix is given an
1237 //                                             alphanumeric character helix identifier.
1238 // 16 - 18        Residue name   initResName   Name of the initial residue.
1239 // 20             Character      initChainID   Chain identifier for the chain containing
1240 //                                             this  helix.
1241 // 22 - 25        Integer        initSeqNum    Sequence number of the initial residue.
1242 // 26             AChar          initICode     Insertion code of the initial residue.
1243 // 28 - 30        Residue  name  endResName    Name of the terminal residue of the helix.
1244 // 32             Character      endChainID    Chain identifier for the chain containing
1245 //                                             this  helix.
1246 // 34 - 37        Integer        endSeqNum     Sequence number of the terminal residue.
1247 // 38             AChar          endICode      Insertion code of the terminal residue.
1248 // 39 - 40        Integer        helixClass    Helix class (see below).
1249 // 41 - 70        String         comment       Comment about this helix.
1250 // 72 - 76        Integer        length        Length of this helix.
1251 //
1252 //                                      CLASS NUMBER
1253 // TYPE OF  HELIX                     (COLUMNS 39 - 40)
1254 // --------------------------------------------------------------
1255 // Right-handed alpha (default)                1
1256 // Right-handed omega                          2
1257 // Right-handed pi                             3
1258 // Right-handed gamma                          4
1259 // Right-handed 3 - 10                         5
1260 // Left-handed alpha                           6
1261 // Left-handed omega                           7
1262 // Left-handed gamma                           8
1263 // 2 - 7 ribbon/helix                          9
1264 // Polyproline                                10
1265 // =============================================================================
1266               }
1267               case SHEET: {
1268 // =============================================================================
1269 // SHEET records are used to identify the position of sheets in the molecule.
1270 // Sheets are both named and numbered. The residues where the sheet begins and
1271 // ends are noted.
1272 //
1273 //  8 - 10        Integer       strand         Strand  number which starts at 1 for each
1274 //                                             strand within a sheet and increases by one.
1275 // 12 - 14        LString(3)    sheetID        Sheet  identifier.
1276 // 15 - 16        Integer       numStrands     Number  of strands in sheet.
1277 // 18 - 20        Residue name  initResName    Residue  name of initial residue.
1278 // 22             Character     initChainID    Chain identifier of initial residue in strand.
1279 // 23 - 26        Integer       initSeqNum     Sequence number of initial residue in strand.
1280 // 27             AChar         initICode      Insertion code of initial residue in  strand.
1281 // 29 - 31        Residue name  endResName     Residue name of terminal residue.
1282 // 33             Character     endChainID     Chain identifier of terminal residue.
1283 // 34 - 37        Integer       endSeqNum      Sequence number of terminal residue.
1284 // 38             AChar         endICode       Insertion code of terminal residue.
1285 // 39 - 40        Integer       sense          Sense of strand with respect to previous
1286 //                                             strand in the sheet. 0 if first strand,
1287 //                                             1 if  parallel,and -1 if anti-parallel.
1288 // 42 - 45        Atom          curAtom        Registration.  Atom name in current strand.
1289 // 46 - 48        Residue name  curResName     Registration.  Residue name in current strand
1290 // 50             Character     curChainId     Registration. Chain identifier in current strand.
1291 // 51 - 54        Integer       curResSeq      Registration.  Residue sequence number
1292 //                                             in current strand.
1293 // 55             AChar         curICode       Registration. Insertion code in current strand.
1294 // 57 - 60        Atom          prevAtom       Registration.  Atom name in previous strand.
1295 // 61 - 63        Residue name  prevResName    Registration.  Residue name in previous strand.
1296 // 65             Character     prevChainId    Registration.  Chain identifier in previous strand.
1297 // 66 - 69        Integer       prevResSeq     Registration. Residue sequence number
1298 //                                             in previous strand.
1299 // 70             AChar         prevICode      Registration.  Insertion code in
1300 //                                             previous strand.
1301 // =============================================================================
1302                 structs.add(line);
1303                 break;
1304               }
1305               case MODEL: // Currently, no handling in initial read.
1306                 break;
1307               case MTRIX1: {
1308 // ================================================================================
1309 // MTRIXn (n = 1, 2, or 3) records present transformations expressing
1310 // non-crystallographic symmetry.
1311 // MTRIXn will appear only when such transformations are required to generate an
1312 // entire asymmetric unit,
1313 // such as a large viral structure.
1314 //
1315 //  8 - 10        Integer       serial         Serial number.
1316 // 11 - 20        Real(10.6)    m[n][1]        Mn1
1317 // 21 - 30        Real(10.6)    m[n][2]        Mn2
1318 // 31 - 40        Real(10.6)    m[n][3]        Mn3
1319 // 21 - 30        Real(10.6)    v[n]           Vn
1320 // 60             Integer       iGiven         1 if coordinates for the representations which are
1321 //                                              approximately related by the transformations of the
1322 //                                              molecule are contained in the entry. Otherwise, blank.
1323 // =================================================================================
1324                 StringBuilder MTRX1 = new StringBuilder(line.substring(11, 55));
1325                 properties.addProperty("MTRIX1", MTRX1);
1326                 break;
1327               }
1328               case MTRIX2: {
1329                 StringBuilder MTRX2 = new StringBuilder(line.substring(11, 55));
1330                 properties.addProperty("MTRIX2", MTRX2);
1331                 break;
1332               }
1333               case MTRIX3: {
1334                 StringBuilder MTRX3 = new StringBuilder(line.substring(11, 55));
1335                 properties.addProperty("MTRIX3", MTRX3);
1336                 break;
1337               }
1338               case REMARK: {
1339                 remarkLines.add(line.trim());
1340                 if (line.contains("Lambda:")) {
1341                   Matcher m = lambdaPattern.matcher(line);
1342                   if (m.find()) {
1343                     lastReadLambda = Double.parseDouble(m.group(1));
1344                   }
1345                 }
1346 // =================================================================================
1347 // REMARK 350: presents all transformations, both crystallographic and non-crystallographic,
1348 // needed to generate the biomolecule. These transformations operate on the coordinates in the
1349 // entry. Both author and computational descriptions of assemblies are provided, if applicable.
1350 // For strict ncs case where more than one assembly presents in asymmetric unit, only one
1351 // chain with unit matrix will reported in REMARK 350, the other chain will be generated
1352 // by rotation and translation.
1353 //
1354 // 20 - 23        Integer       serial         Serial number.
1355 // 24 - 33        Real(10.6)    m[n][1]        Mn1
1356 // 34 - 43        Real(10.6)    m[n][2]        Mn2
1357 // 44 - 53        Real(10.6)    m[n][3]        Mn3
1358 // 59 - 68        Real(10.6)    v[n]           Vn
1359 // =================================================================================
1360                 if (line.length() >= 68) {
1361                   String remarkType = line.substring(7, 10).trim();
1362                   if (remarkType.matches("\\d+") && parseInt(remarkType) == 350 && line.substring(13,
1363                       18).equalsIgnoreCase("BIOMT")) {
1364                     properties.addProperty("BIOMTn", new StringBuilder(line.substring(24, 68)));
1365                   }
1366                 }
1367                 break;
1368               }
1369               default:
1370                 break;
1371             }
1372             line = br.readLine();
1373           }
1374 
1375         } catch (FileNotFoundException fileNotFoundException) {
1376           logger.log(Level.SEVERE, " PDB file not found", fileNotFoundException);
1377         }
1378       }
1379       xyzIndex--;
1380       setFileRead(true);
1381     } catch (IOException e) {
1382       logger.exiting(PDBFilter.class.getName(), "readFile", e);
1383       return false;
1384     }
1385 
1386     // Locate disulfide bonds; bond parameters are assigned below.
1387     List<Bond> ssBondList = locateDisulfideBonds(ssbonds, activeMolecularAssembly, pdbToNewResMap);
1388 
1389     // Record the number of atoms read in from the PDB file before applying
1390     // algorithms that may build new atoms.
1391     int pdbAtoms = activeMolecularAssembly.getAtomArray().length;
1392 
1393     // Remove hydrogen atoms that are not consistent with amino acid residue protonation states (e.g., HIE & HID).
1394     removeInappropriateHydrogen();
1395 
1396     // Build missing backbone atoms in loops.
1397     buildMissingResidues(xyzIndex, activeMolecularAssembly, seqRes, dbRef);
1398 
1399     // Assign atom types. Missing side-chains atoms and missing hydrogen will be built in.
1400     bondList = assignAtomTypes(activeMolecularAssembly, fileStandard);
1401 
1402     // Assign disulfide bonds parameters and log their creation.
1403     buildDisulfideBonds(ssBondList, activeMolecularAssembly, bondList);
1404 
1405     // Finally, re-number the atoms if missing atoms were created.
1406     int currentN = activeMolecularAssembly.getAtomArray().length;
1407     boolean renumber = forceField.getBoolean("renumber-pdb", false);
1408     if (pdbAtoms != currentN) {
1409       logger.info(format(" Renumbering PDB file due to built atoms (%d vs %d)", currentN, pdbAtoms));
1410       numberAtoms(activeMolecularAssembly);
1411     } else if (renumber) {
1412       logger.info(" Renumbering PDB file due to renumber-pdb flag.");
1413       numberAtoms(activeMolecularAssembly);
1414     }
1415     return true;
1416   }
1417 
1418   /**
1419    * Removes inappropriate hydrogen atoms from residues in the active molecular assembly.
1420    * This method iterates through the residue list of the active molecular assembly and
1421    * identifies residues requiring hydrogen removal (e.g., specific residues such as "HID", "HIE", "ASP", etc.).
1422    * Residue names are defined by the C-alpha atom.
1423    */
1424   public void removeInappropriateHydrogen() {
1425     StringBuilder sb = new StringBuilder();
1426     int numRemoved = 0;
1427     for (Residue residue : activeMolecularAssembly.getResidueList()) {
1428       if (residue.getName().equals("ACE") ||
1429           residue.getName().equals("NME") ||
1430           residue.getResidueType() != Residue.ResidueType.AA) {
1431         continue;
1432       }
1433       String trueResName = residue.getAtomByName("CA", true).getResidueName();
1434       Atom atom;
1435       switch (trueResName) {
1436         case "HID", "GLU" -> // No HE2
1437             atom = residue.getAtomByName("HE2", true);
1438         case "HIE" -> // No HD1
1439             atom = residue.getAtomByName("HD1", true);
1440         case "ASP" -> // No HD2
1441             atom = residue.getAtomByName("HD2", true);
1442         case "LYD" -> // No HZ3
1443             atom = residue.getAtomByName("HZ3", true);
1444         case "CYD" -> // No HG
1445             atom = residue.getAtomByName("HG", true);
1446         default -> atom = null;
1447         // Do nothing.
1448       }
1449       if (atom != null) {
1450         MSNode atoms = residue.getAtomNode();
1451         atoms.remove(atom);
1452         residue.setName(trueResName);
1453         sb.append(" Removed ").append(atom);
1454         numRemoved++;
1455       }
1456     }
1457     if (numRemoved > 0) {
1458       logger.info(format(" Removed %d Inappropriate Hydrogen", numRemoved));
1459       logger.info(sb.toString());
1460     }
1461   }
1462 
1463   /**
1464    * {@inheritDoc}
1465    */
1466   @Override
1467   public boolean readNext() {
1468     return readNext(false);
1469   }
1470 
1471   /**
1472    * {@inheritDoc}
1473    */
1474   @Override
1475   public boolean readNext(boolean resetPosition) {
1476     return readNext(resetPosition, false);
1477   }
1478 
1479   /**
1480    * {@inheritDoc}
1481    */
1482   @Override
1483   public boolean readNext(boolean resetPosition, boolean print) {
1484     return readNext(resetPosition, print, true);
1485   }
1486 
1487   /**
1488    * {@inheritDoc}
1489    */
1490   @Override
1491   @SuppressWarnings("fallthrough")
1492   public boolean readNext(boolean resetPosition, boolean print, boolean parse) {
1493     modelsRead = resetPosition ? 1 : modelsRead + 1;
1494     if (!parse) {
1495       if (print) {
1496         logger.info(format(" Skipped Model %d.", modelsRead));
1497       }
1498       return true;
1499     }
1500     remarkLines = new ArrayList<>(remarkLines.size());
1501     // ^ is beginning of line, \\s+ means "one or more whitespace", (\\d+) means match and capture
1502     // one or more digits.
1503     Pattern modelPatt = Pattern.compile("^MODEL\\s+(\\d+)");
1504     boolean eof = true;
1505     for (MolecularAssembly system : systems) {
1506       try {
1507         BufferedReader currentReader;
1508         if (readers.containsKey(system)) {
1509           currentReader = readers.get(system);
1510           try {
1511             if (!currentReader.ready()) {
1512               currentReader = new BufferedReader(new FileReader(readFile));
1513               // Mark the start of the file.
1514               currentReader.mark(0);
1515               readers.remove(system);
1516               readers.put(system, currentReader);
1517             } else if (resetPosition) {
1518               // If the BufferedReader has been opened, and reset is requested, reset the position.
1519               currentReader.reset();
1520             }
1521           } catch (Exception exception) {
1522             // If all structures in the PDB file have been read, the currentReader may have closed.
1523             // The try block will catch this case and reset to the beginning of the file.
1524             currentReader = new BufferedReader(new FileReader(readFile));
1525             // Mark the start of the file.
1526             currentReader.mark(0);
1527             readers.remove(system);
1528             readers.put(system, currentReader);
1529           }
1530         } else {
1531           currentReader = new BufferedReader(new FileReader(readFile));
1532           // Mark the start of the file.
1533           currentReader.mark(0);
1534           readers.put(system, currentReader);
1535         }
1536 
1537         // Skip to appropriate model.
1538         String line = currentReader.readLine();
1539         while (line != null) {
1540           line = line.trim();
1541           Matcher m = modelPatt.matcher(line);
1542           if (m.find()) {
1543             int modelNum = parseInt(m.group(1));
1544             if (modelNum == modelsRead) {
1545               if (print) {
1546                 logger.log(Level.INFO, format(" Reading model %d for %s", modelNum, currentFile));
1547               }
1548               eof = false;
1549               break;
1550             }
1551           }
1552           line = currentReader.readLine();
1553         }
1554         if (eof) {
1555           if (logger.isLoggable(Level.FINEST)) {
1556             logger.log(Level.FINEST, format("\n End of file reached for %s", readFile));
1557           }
1558           currentReader.close();
1559           return false;
1560         }
1561 
1562         // Begin parsing the model.
1563         currentChainID = null;
1564         currentSegID = null;
1565         boolean modelDone = false;
1566         line = currentReader.readLine();
1567         while (line != null) {
1568           line = line.trim();
1569           String recID = line.substring(0, Math.min(6, line.length())).trim();
1570           try {
1571             Record record = Record.valueOf(recID);
1572             boolean hetatm = true;
1573             switch (record) {
1574               // =============================================================================
1575               //
1576               //  7 - 11        Integer       serial       Atom serial number.
1577               // 13 - 16        Atom          name         Atom name.
1578               // 17             Character     altLoc       Alternate location indicator.
1579               // 18 - 20        Residue name  resName      Residue name.
1580               // 22             Character     chainID      Chain identifier.
1581               // 23 - 26        Integer       resSeq       Residue sequence number.
1582               // 27             AChar         iCode        Code for insertion of residues.
1583               // 31 - 38        Real(8.3)     x            Orthogonal coordinates for X in
1584               // Angstroms.
1585               // 39 - 46        Real(8.3)     y            Orthogonal coordinates for Y in
1586               // Angstroms.
1587               // 47 - 54        Real(8.3)     z            Orthogonal coordinates for Z in
1588               // Angstroms.
1589               // 55 - 60        Real(6.2)     occupancy    Occupancy.
1590               // 61 - 66        Real(6.2)     tempFactor   Temperature factor.
1591               // 77 - 78        LString(2)    element      Element symbol, right-justified.
1592               // 79 - 80        LString(2)    charge       Charge  on the atom.
1593               // =============================================================================
1594               //         1         2         3         4         5         6         7
1595               // 123456789012345678901234567890123456789012345678901234567890123456789012345678
1596               // ATOM      1  N   ILE A  16      60.614  71.140 -10.592  1.00  7.38           N
1597               // ATOM      2  CA  ILE A  16      60.793  72.149  -9.511  1.00  6.91           C
1598               case ATOM:
1599                 hetatm = false;
1600               case HETATM:
1601                 String name = line.substring(12, 16).trim();
1602                 if (name.toUpperCase().contains("1H") || name.toUpperCase().contains("2H")
1603                     || name.toUpperCase().contains("3H")) {
1604                   // VERSION3_2 is presently just a placeholder for "anything non-standard".
1605                   fileStandard = VERSION3_2;
1606                 }
1607                 Character altLoc = line.substring(16, 17).toUpperCase().charAt(0);
1608                 if (!altLoc.equals(' ') && !altLoc.equals(currentAltLoc)) {
1609                   break;
1610                 }
1611                 // if (!altLoc.equals(' ') && !altLoc.equals('A') && !altLoc.equals(currentAltLoc)) {
1612                 //  break;
1613                 // }
1614                 String resName = line.substring(17, 20).trim();
1615                 Character chainID = line.substring(21, 22).charAt(0);
1616                 String segID = getExistingSegID(chainID);
1617                 int resSeq = Hybrid36.decode(4, line.substring(22, 26));
1618                 double[] d = new double[3];
1619                 d[0] = parseDouble(line.substring(30, 38).trim());
1620                 d[1] = parseDouble(line.substring(38, 46).trim());
1621                 d[2] = parseDouble(line.substring(46, 54).trim());
1622                 double occupancy = 1.0;
1623                 double tempFactor = 1.0;
1624                 Atom newAtom = new Atom(0, name, altLoc, d, resName, resSeq, chainID, occupancy,
1625                     tempFactor, segID);
1626                 newAtom.setHetero(hetatm);
1627                 // Check if this is a modified residue.
1628                 if (modRes.containsKey(resName.toUpperCase())) {
1629                   newAtom.setModRes(true);
1630                 }
1631 
1632                 Atom returnedAtom = activeMolecularAssembly.findAtom(newAtom);
1633                 if (returnedAtom != null) {
1634                   returnedAtom.setXYZ(d);
1635                   double[] retXYZ = new double[3];
1636                   returnedAtom.getXYZ(retXYZ);
1637                 } else {
1638                   String message = format(" Could not find atom %s in assembly", newAtom);
1639                   if (dieOnMissingAtom) {
1640                     logger.severe(message);
1641                   } else {
1642                     logger.warning(message);
1643                   }
1644                 }
1645                 break;
1646               case CRYST1:
1647                 // =============================================================================
1648                 // The CRYST1 record presents the unit cell parameters, space group, and Z
1649                 // value. If the structure was not determined by crystallographic means, CRYST1
1650                 // simply provides the unitary values, with an appropriate REMARK.
1651                 //
1652                 //  7 - 15       Real(9.3)     a              a (Angstroms).
1653                 // 16 - 24       Real(9.3)     b              b (Angstroms).
1654                 // 25 - 33       Real(9.3)     c              c (Angstroms).
1655                 // 34 - 40       Real(7.2)     alpha          alpha (degrees).
1656                 // 41 - 47       Real(7.2)     beta           beta (degrees).
1657                 // 48 - 54       Real(7.2)     gamma          gamma (degrees).
1658                 // 56 - 66       LString       sGroup         Space  group.
1659                 // 67 - 70       Integer       z              Z value.
1660                 // =============================================================================
1661                 logger.fine(" Crystal record found.");
1662                 if (line.length() < 55) {
1663                   logger.severe(" CRYST1 record is improperly formatted.");
1664                 }
1665                 double aaxis = parseDouble(line.substring(6, 15).trim());
1666                 double baxis = parseDouble(line.substring(15, 24).trim());
1667                 double caxis = parseDouble(line.substring(24, 33).trim());
1668                 double alpha = parseDouble(line.substring(33, 40).trim());
1669                 double beta = parseDouble(line.substring(40, 47).trim());
1670                 double gamma = parseDouble(line.substring(47, 54).trim());
1671                 int limit = min(line.length(), 66);
1672                 String sg = line.substring(55, limit).trim();
1673 //                properties.clearProperty("a-axis");
1674 //                properties.clearProperty("b-axis");
1675 //                properties.clearProperty("c-axis");
1676 //                properties.clearProperty("alpha");
1677 //                properties.clearProperty("beta");
1678 //                properties.clearProperty("gamma");
1679 //                properties.clearProperty("spacegroup");
1680 //
1681 //                properties.addProperty("a-axis", aaxis);
1682 //                properties.addProperty("b-axis", baxis);
1683 //                properties.addProperty("c-axis", caxis);
1684 //                properties.addProperty("alpha", alpha);
1685 //                properties.addProperty("beta", beta);
1686 //                properties.addProperty("gamma", gamma);
1687 //                properties.addProperty("spacegroup", SpaceGroupInfo.pdb2ShortName(sg));
1688                 Crystal crystal = activeMolecularAssembly.getCrystal();
1689                 SpaceGroup spaceGroup = SpaceGroupDefinitions.spaceGroupFactory(sg);
1690                 if (Objects.equals(crystal.spaceGroup.shortName, spaceGroup.shortName)) {
1691                   crystal.changeUnitCellParameters(aaxis, baxis, caxis, alpha, beta, gamma);
1692                 } else {
1693                   // TODO: Handle changes in space groups... Means recalculating force field terms.
1694                   logger.warning(format(" Original space group %s could not be changed to %s",
1695                       crystal.spaceGroup.shortName, spaceGroup.shortName));
1696                 }
1697                 break;
1698               case ENDMDL:
1699               case END: // Technically speaking, END should be at the end of the file, not end of
1700                 // the model.
1701                 logger.log(Level.FINE, format(" Model %d successfully read", modelsRead));
1702                 modelDone = true;
1703                 break;
1704               case REMARK:
1705                 remarkLines.add(line.trim());
1706                 if (line.contains("Lambda:")) {
1707                   Matcher m = lambdaPattern.matcher(line);
1708                   if (m.find()) {
1709                     lastReadLambda = Double.parseDouble(m.group(1));
1710                   }
1711                 }
1712                 break;
1713               default:
1714                 break;
1715             }
1716           } catch (Exception ex) {
1717             // Do nothing; it's not an ATOM/HETATM line.
1718           }
1719           if (modelDone) {
1720             break;
1721           }
1722           line = currentReader.readLine();
1723         }
1724         return true;
1725       } catch (IOException ex) {
1726         logger.info(
1727             format(" Exception in parsing frame %d of %s:" + " %s", modelsRead, system.toString(), ex));
1728       }
1729     }
1730     return false;
1731   }
1732 
1733   /**
1734    * Specify the alternate location.
1735    *
1736    * @param molecularAssembly The MolecularAssembly to populate.
1737    * @param altLoc            The alternate location to use.
1738    */
1739   public void setAltID(MolecularAssembly molecularAssembly, Character altLoc) {
1740     setMolecularSystem(molecularAssembly);
1741     currentAltLoc = altLoc;
1742   }
1743 
1744   /**
1745    * Sets whether this PDBFilter should log each time it saves to a file.
1746    *
1747    * @param logWrites a boolean.
1748    */
1749   public void setLogWrites(boolean logWrites) {
1750     this.logWrites = logWrites;
1751   }
1752 
1753   /**
1754    * setModelNumbering.
1755    *
1756    * @param modelsWritten the number of models written.
1757    */
1758   public void setModelNumbering(int modelsWritten) {
1759     this.modelsWritten = modelsWritten;
1760   }
1761 
1762   public void setLMN(int[] lmn) {
1763     if (lmn[0] >= 1 && lmn[1] >= 1 && lmn[2] >= 1) {
1764       this.lmn = lmn;
1765     } else {
1766       // Provided dimensions are not handled. Revert to P1.
1767       this.lmn = new int[]{1, 1, 1};
1768     }
1769   }
1770 
1771   /**
1772    * setSymOp.
1773    *
1774    * @param symOp a int.
1775    */
1776   public void setSymOp(int symOp) {
1777     this.nSymOp = symOp;
1778   }
1779 
1780   /**
1781    * Expand the current system to P1 during the save operation.
1782    *
1783    * @param file The file to write.
1784    * @return Return true on a successful write.
1785    */
1786   public boolean writeFileAsP1(File file) {
1787     // XYZ File First Line
1788     final int l = lmn[0];
1789     final int m = lmn[1];
1790     final int n = lmn[2];
1791     Crystal crystal = activeMolecularAssembly.getCrystal();
1792     int nSymOps = crystal.getUnitCell().spaceGroup.getNumberOfSymOps();
1793 
1794     if (nSymOps == 1 && l <= 1 && m <= 1 && n <= 1) {
1795       // This is a P1 system.
1796       if (!writeFile(file, false)) {
1797         logger.info(format(" Save failed for %s", activeMolecularAssembly));
1798         return false;
1799       } else {
1800         return true;
1801       }
1802     } else {
1803       Polymer[] polymers = activeMolecularAssembly.getChains();
1804       int chainCount = 0;
1805       for (Polymer polymer : polymers) {
1806         Character chainID = Polymer.CHAIN_IDS.charAt(chainCount++);
1807         polymer.setChainID(chainID);
1808         polymer.setSegID(chainID.toString());
1809       }
1810       nSymOp = 0;
1811       logWrites = false;
1812       boolean writeEnd = false;
1813       if (!writeFile(file, false, false, writeEnd)) {
1814         logger.info(format(" Save failed for %s", activeMolecularAssembly));
1815         return false;
1816       } else {
1817         for (int i = 0; i < l; i++) {
1818           for (int j = 0; j < m; j++) {
1819             for (int k = 0; k < n; k++) {
1820               lValue = i;
1821               mValue = j;
1822               nValue = k;
1823               for (int iSym = 0; iSym < nSymOps; iSym++) {
1824                 nSymOp = iSym;
1825                 for (Polymer polymer : polymers) {
1826                   Character chainID = Polymer.CHAIN_IDS.charAt(chainCount++);
1827                   polymer.setChainID(chainID);
1828                   polymer.setSegID(chainID.toString());
1829                 }
1830                 // If the last sym op to be written.
1831                 writeEnd = iSym == nSymOps - 1 && i == l - 1 && j == m - 1 && k == n - 1;
1832                 if (!writeFile(file, true, false, writeEnd)) {
1833                   logger.info(format(" Save failed for %s", activeMolecularAssembly));
1834                   return false;
1835                 }
1836               }
1837             }
1838           }
1839         }
1840       }
1841 
1842       // Reset the chainIDs.
1843       chainCount = 0;
1844       for (Polymer polymer : polymers) {
1845         Character chainID = Polymer.CHAIN_IDS.charAt(chainCount++);
1846         polymer.setChainID(chainID);
1847         polymer.setSegID(chainID.toString());
1848       }
1849 
1850     }
1851 
1852     return true;
1853   }
1854 
1855   /**
1856    * writeFile
1857    *
1858    * @param saveFile    a {@link java.io.File} object.
1859    * @param append      Whether to append to saveFile (vs over-write).
1860    * @param printLinear Ignored (remains to present a different method signature).
1861    * @param writeEnd    True if this is the final model.
1862    * @return Success of writing.
1863    */
1864   public boolean writeFile(File saveFile, boolean append, boolean printLinear, boolean writeEnd) {
1865     return writeFile(saveFile, append, Collections.emptySet(), writeEnd, true);
1866   }
1867 
1868   /**
1869    * writeFile
1870    *
1871    * @param saveFile   a {@link java.io.File} object to save to.
1872    * @param append     Whether to append to saveFile (vs over-write).
1873    * @param toExclude  A {@link java.util.Set} of {@link ffx.potential.bonded.Atom}s to exclude
1874    *                   from writing.
1875    * @param writeEnd   True if this is the final model.
1876    * @param versioning True if the file being saved to should be versioned. False if the file
1877    *                   being saved to should be overwritten.
1878    * @return Success of writing.
1879    */
1880   public boolean writeFile(File saveFile, boolean append, Set<Atom> toExclude, boolean writeEnd,
1881                            boolean versioning) {
1882     return writeFile(saveFile, append, toExclude, writeEnd, versioning, null);
1883   }
1884 
1885   /**
1886    * writeFile
1887    *
1888    * @param saveFile   a {@link java.io.File} object to save to.
1889    * @param append     Whether to append to saveFile (vs over-write).
1890    * @param toExclude  A {@link java.util.Set} of {@link ffx.potential.bonded.Atom}s to exclude
1891    *                   from writing.
1892    * @param writeEnd   True if this is the final model.
1893    * @param versioning True if the file being saved to should be versioned. False if the file
1894    *                   being saved to should be overwritten.
1895    * @param extraLines Extra comment/header lines to write.
1896    * @return Success of writing.
1897    */
1898   public boolean writeFile(File saveFile, boolean append, Set<Atom> toExclude, boolean writeEnd,
1899                            boolean versioning, String[] extraLines) {
1900     // Set standardize atom names to false in the presence of deuterium
1901     if (standardizeAtomNames) {
1902       logger.info(" Setting atom names to PDB standard.");
1903 
1904       List<Atom> deuteriumAtoms = new ArrayList<>();
1905       for (Atom atom : activeMolecularAssembly.getAtomArray()) {
1906         if (atom.getName().startsWith("D")) {
1907           String name = atom.getName().replaceFirst("D", "H");
1908           atom.setName(name);
1909           deuteriumAtoms.add(atom);
1910         }
1911       }
1912       renameAtomsToPDBStandard(activeMolecularAssembly);
1913       for (Atom atom : activeMolecularAssembly.getAtomArray()) {
1914         if (deuteriumAtoms.contains(atom) && atom.getName().startsWith("H")) {
1915           String name = atom.getName().replaceFirst("H", "D");
1916           atom.setName(name);
1917         }
1918       }
1919     }
1920 
1921     final Set<Atom> atomExclusions = toExclude == null ? Collections.emptySet() : toExclude;
1922     if (saveFile == null) {
1923       return false;
1924     }
1925     if (vdwH) {
1926       logger.info(" Saving hydrogen to van der Waals centers instead of nuclear locations.");
1927     }
1928     if (nSymOp > -1) {
1929       logger.info(format(" Saving atoms using the symmetry operator:\n%s\n",
1930           activeMolecularAssembly.getCrystal().getUnitCell().spaceGroup.getSymOp(nSymOp)
1931               .toString()));
1932     }
1933 
1934     // Create StringBuilders for ATOM, ANISOU and TER records that can be reused.
1935     StringBuilder sb = new StringBuilder("ATOM  ");
1936     StringBuilder anisouSB = new StringBuilder("ANISOU");
1937     StringBuilder terSB = new StringBuilder("TER   ");
1938     StringBuilder model = null;
1939     for (int i = 6; i < 80; i++) {
1940       sb.append(' ');
1941       anisouSB.append(' ');
1942       terSB.append(' ');
1943     }
1944 
1945     File newFile = saveFile;
1946     if (!append) {
1947       if (versioning) {
1948         newFile = version(saveFile);
1949       }
1950     } else if (modelsWritten >= 0) {
1951       model = new StringBuilder(format("MODEL     %-4d", ++modelsWritten));
1952       model.append(repeat(" ", 65));
1953     }
1954     activeMolecularAssembly.setFile(newFile);
1955     if (activeMolecularAssembly.getName() == null) {
1956       activeMolecularAssembly.setName(newFile.getName());
1957     }
1958     if (logWrites) {
1959       logger.log(Level.INFO, " Saving {0}", newFile.getName());
1960     }
1961 
1962     try (FileWriter fw = new FileWriter(newFile, append);
1963          BufferedWriter bw = new BufferedWriter(fw)) {
1964       /*
1965        Will come before CRYST1 and ATOM records, but after anything
1966        written by writeFileWithHeader (particularly X-ray refinement statistics).
1967       */
1968       String[] headerLines = activeMolecularAssembly.getHeaderLines();
1969       for (String line : headerLines) {
1970         bw.write(format("%s\n", line));
1971       }
1972       if (extraLines != null) {
1973         if (rotamerTitration && extraLines[0].contains("REMARK")) {
1974           for (String line : extraLines) {
1975             bw.write(line + "\n");
1976           }
1977         } else {
1978           for (String line : extraLines) {
1979             bw.write(format("REMARK 999 %s\n", line));
1980           }
1981         }
1982       }
1983       if (model != null) {
1984         bw.write(model.toString());
1985         bw.newLine();
1986       }
1987       // =============================================================================
1988       // The CRYST1 record presents the unit cell parameters, space group, and Z
1989       // value. If the structure was not determined by crystallographic means, CRYST1
1990       // simply provides the unitary values, with an appropriate REMARK.
1991       //
1992       //  7 - 15       Real(9.3)     a              a (Angstroms).
1993       // 16 - 24       Real(9.3)     b              b (Angstroms).
1994       // 25 - 33       Real(9.3)     c              c (Angstroms).
1995       // 34 - 40       Real(7.2)     alpha          alpha (degrees).
1996       // 41 - 47       Real(7.2)     beta           beta (degrees).
1997       // 48 - 54       Real(7.2)     gamma          gamma (degrees).
1998       // 56 - 66       LString       sGroup         Space  group.
1999       // 67 - 70       Integer       z              Z value.
2000       // =============================================================================
2001       if (nSymOp < 0) {
2002         // Write out the unit cell.
2003         Crystal crystal = activeMolecularAssembly.getCrystal();
2004         if (crystal != null && !crystal.aperiodic()) {
2005           Crystal c = crystal.getUnitCell();
2006           if (lmn[0] > 0 || lmn[1] > 0 || lmn[2] > 0) {
2007             c.a = c.a * lmn[0];
2008             c.b = c.b * lmn[1];
2009             c.c = c.c * lmn[2];
2010           }
2011           bw.write(c.toCRYST1());
2012         }
2013       } else if (nSymOp == 0) {
2014         // Write a P1 cell.
2015         Crystal crystal = activeMolecularAssembly.getCrystal();
2016         if (crystal != null && !crystal.aperiodic()) {
2017           Crystal c = crystal.getUnitCell();
2018           Crystal p1 = new Crystal((lmn[0] > 0) ? c.a * lmn[0] : c.a, (lmn[1] > 0) ? c.b * lmn[1] : c.b, (lmn[2] > 0) ? c.c * lmn[2] : c.c, c.alpha, c.beta, c.gamma, "P1");
2019           bw.write(p1.toCRYST1());
2020         }
2021       }
2022       // =============================================================================
2023       // The SSBOND record identifies each disulfide bond in protein and polypeptide
2024       // structures by identifying the two residues involved in the bond.
2025       // The disulfide bond distance is included after the symmetry operations at
2026       // the end of the SSBOND record.
2027       //
2028       //  8 - 10        Integer         serNum       Serial number.
2029       // 12 - 14        LString(3)      "CYS"        Residue name.
2030       // 16             Character       chainID1     Chain identifier.
2031       // 18 - 21        Integer         seqNum1      Residue sequence number.
2032       // 22             AChar           icode1       Insertion code.
2033       // 26 - 28        LString(3)      "CYS"        Residue name.
2034       // 30             Character       chainID2     Chain identifier.
2035       // 32 - 35        Integer         seqNum2      Residue sequence number.
2036       // 36             AChar           icode2       Insertion code.
2037       // 60 - 65        SymOP           sym1         Symmetry oper for 1st resid
2038       // 67 - 72        SymOP           sym2         Symmetry oper for 2nd resid
2039       // 74 – 78        Real(5.2)      Length        Disulfide bond distance
2040       //
2041       // If SG of cysteine is disordered then there are possible alternate linkages.
2042       // wwPDB practice is to put together all possible SSBOND records. This is
2043       // problematic because the alternate location identifier is not specified in
2044       // the SSBOND record.
2045       // =============================================================================
2046       int serNum = 1;
2047       Polymer[] polymers = activeMolecularAssembly.getChains();
2048       if (polymers != null) {
2049         for (Polymer polymer : polymers) {
2050           List<Residue> residues = polymer.getResidues();
2051           for (Residue residue : residues) {
2052             if (residue.getName().equalsIgnoreCase("CYS")) {
2053               List<Atom> cysAtoms = residue.getAtomList().stream()
2054                   .filter(a -> !atomExclusions.contains(a)).toList();
2055               Atom SG1 = null;
2056               for (Atom atom : cysAtoms) {
2057                 String atName = atom.getName().toUpperCase();
2058                 if (atName.equals("SG") || atName.equals("SH")
2059                     || atom.getAtomType().atomicNumber == 16) {
2060                   SG1 = atom;
2061                   break;
2062                 }
2063               }
2064               List<Bond> bonds = SG1.getBonds();
2065               for (Bond bond : bonds) {
2066                 Atom SG2 = bond.get1_2(SG1);
2067                 if (SG2.getAtomType().atomicNumber == 16 && !atomExclusions.contains(SG2)) {
2068                   if (SG1.getIndex() < SG2.getIndex()) {
2069                     bond.energy(false);
2070                     bw.write(format("SSBOND %3d CYS %1s %4s    CYS %1s %4s %36s %5.2f\n", serNum++,
2071                         SG1.getChainID().toString(), Hybrid36.encode(4, SG1.getResidueNumber()),
2072                         SG2.getChainID().toString(), Hybrid36.encode(4, SG2.getResidueNumber()), "",
2073                         bond.getValue()));
2074                   }
2075                 }
2076               }
2077             }
2078           }
2079         }
2080       }
2081       // =============================================================================
2082       //
2083       //  7 - 11        Integer       serial       Atom serial number.
2084       // 13 - 16        Atom          name         Atom name.
2085       // 17             Character     altLoc       Alternate location indicator.
2086       // 18 - 20        Residue name  resName      Residue name.
2087       // 22             Character     chainID      Chain identifier.
2088       // 23 - 26        Integer       resSeq       Residue sequence number.
2089       // 27             AChar         iCode        Code for insertion of residues.
2090       // 31 - 38        Real(8.3)     x            Orthogonal coordinates for X in Angstroms.
2091       // 39 - 46        Real(8.3)     y            Orthogonal coordinates for Y in Angstroms.
2092       // 47 - 54        Real(8.3)     z            Orthogonal coordinates for Z in Angstroms.
2093       // 55 - 60        Real(6.2)     occupancy    Occupancy.
2094       // 61 - 66        Real(6.2)     tempFactor   Temperature factor.
2095       // 77 - 78        LString(2)    element      Element symbol, right-justified.
2096       // 79 - 80        LString(2)    charge       Charge  on the atom.
2097       // =============================================================================
2098       //         1         2         3         4         5         6         7
2099       // 123456789012345678901234567890123456789012345678901234567890123456789012345678
2100       // ATOM      1  N   ILE A  16      60.614  71.140 -10.592  1.00  7.38           N
2101       // ATOM      2  CA  ILE A  16      60.793  72.149  -9.511  1.00  6.91           C
2102       MolecularAssembly[] molecularAssemblies = this.getMolecularAssemblyArray();
2103       int serial = 1;
2104       if (nSymOp > 0) {
2105         serial = serialP1;
2106       }
2107 
2108       // Loop over biomolecular chains
2109       if (polymers != null) {
2110         for (Polymer polymer : polymers) {
2111           currentSegID = polymer.getName();
2112           currentChainID = polymer.getChainID();
2113           sb.setCharAt(21, currentChainID);
2114           // Loop over residues
2115           List<Residue> residues = polymer.getResidues();
2116           for (Residue residue : residues) {
2117             String resName = residue.getName();
2118             if (resName.length() > 3) {
2119               resName = resName.substring(0, 3);
2120             }
2121             int resID = residue.getResidueNumber();
2122             sb.replace(17, 20, padLeft(resName.toUpperCase(), 3));
2123             sb.replace(22, 26, format("%4s", Hybrid36.encode(4, resID)));
2124             // Loop over atoms
2125             List<Atom> residueAtoms = residue.getAtomList().stream()
2126                 .filter(a -> !atomExclusions.contains(a)).collect(Collectors.toList());
2127             boolean altLocFound = false;
2128             for (Atom atom : residueAtoms) {
2129               if (mutate) {
2130                 for (Mutation mtn : mutations) {
2131                   if (resID == mtn.resID && currentChainID == mtn.chainChar) {
2132                     mtn.addTors(atom, serial);
2133                     ArrayList<String> alchAtoms = mtn.getAlchemicalAtoms(true);
2134                     if (alchAtoms != null) {
2135                       if (residue.getBackboneAtoms().contains(atom) && alchAtoms.contains(atom.getName())) {
2136                         logger.info(format(" MUTATION atom is %d chain %s", serial, currentChainID));
2137                       }
2138                     } else {
2139                       // treating pur-pyr or pyr-pur N9/N1 & C2/C4 as alchemical
2140                       if (residue.getBackboneAtoms().contains(atom)) {
2141                         logger.info(format(" MUTATION atom is %d chain %s", serial, currentChainID));
2142                       }
2143                     }
2144                   }
2145                 }
2146               }
2147               writeAtom(atom, serial++, sb, anisouSB, bw);
2148               Character altLoc = atom.getAltLoc();
2149               if (altLoc != null && !altLoc.equals(' ')) {
2150                 altLocFound = true;
2151               }
2152             }
2153             // Write out alternate conformers
2154             if (altLocFound) {
2155               for (int ma = 1; ma < molecularAssemblies.length; ma++) {
2156                 MolecularAssembly altMolecularAssembly = molecularAssemblies[ma];
2157                 Polymer altPolymer = altMolecularAssembly.getPolymer(currentChainID, currentSegID, false);
2158                 Residue altResidue = altPolymer.getResidue(resName, resID, false, Residue.ResidueType.AA);
2159                 if (altResidue == null) {
2160                   resName = AminoAcid3.UNK.name();
2161                   altResidue = altPolymer.getResidue(resName, resID, false, Residue.ResidueType.AA);
2162                 }
2163                 residueAtoms = altResidue.getAtomList().stream()
2164                     .filter(a -> !atomExclusions.contains(a)).toList();
2165                 for (Atom atom : residueAtoms) {
2166                   if (atom.getAltLoc() != null && !atom.getAltLoc().equals(' ') && !atom.getAltLoc()
2167                       .equals('A')) {
2168                     sb.replace(17, 20, padLeft(atom.getResidueName().toUpperCase(), 3));
2169                     writeAtom(atom, serial++, sb, anisouSB, bw);
2170                   }
2171                 }
2172               }
2173             }
2174           }
2175           terSB.replace(6, 11, format("%5s", Hybrid36.encode(5, serial++)));
2176           terSB.replace(12, 16, "    ");
2177           terSB.replace(16, 26, sb.substring(16, 26));
2178           bw.write(terSB.toString());
2179           bw.newLine();
2180         }
2181       }
2182       sb.replace(0, 6, "HETATM");
2183       sb.setCharAt(21, 'A');
2184 
2185       Character chainID = 'A';
2186       if (polymers != null) {
2187         chainID = polymers[0].getChainID();
2188       }
2189       activeMolecularAssembly.setChainIDAndRenumberMolecules(chainID);
2190 
2191       // Loop over molecules, ions and then water.
2192       List<MSNode> molecules = activeMolecularAssembly.getMolecules();
2193       int numMolecules = molecules.size();
2194       for (int i = 0; i < numMolecules; i++) {
2195         Molecule molecule = (Molecule) molecules.get(i);
2196         chainID = molecule.getChainID();
2197         sb.setCharAt(21, chainID);
2198         String resName = molecule.getResidueName();
2199         int resID = molecule.getResidueNumber();
2200         if (resName.length() > 3) {
2201           resName = resName.substring(0, 3);
2202         }
2203         sb.replace(17, 20, padLeft(resName.toUpperCase(), 3));
2204         sb.replace(22, 26, format("%4s", Hybrid36.encode(4, resID)));
2205         // List<Atom> moleculeAtoms = molecule.getAtomList();
2206         List<Atom> moleculeAtoms = molecule.getAtomList().stream()
2207             .filter(a -> !atomExclusions.contains(a)).collect(Collectors.toList());
2208         boolean altLocFound = false;
2209         for (Atom atom : moleculeAtoms) {
2210           writeAtom(atom, serial++, sb, anisouSB, bw);
2211           Character altLoc = atom.getAltLoc();
2212           if (altLoc != null && !altLoc.equals(' ')) {
2213             altLocFound = true;
2214           }
2215         }
2216         // Write out alternate conformers
2217         if (altLocFound) {
2218           for (int ma = 1; ma < molecularAssemblies.length; ma++) {
2219             MolecularAssembly altMolecularAssembly = molecularAssemblies[ma];
2220             MSNode altmolecule = altMolecularAssembly.getMolecules().get(i);
2221             moleculeAtoms = altmolecule.getAtomList();
2222             for (Atom atom : moleculeAtoms) {
2223               if (atom.getAltLoc() != null && !atom.getAltLoc().equals(' ') && !atom.getAltLoc()
2224                   .equals('A')) {
2225                 writeAtom(atom, serial++, sb, anisouSB, bw);
2226               }
2227             }
2228           }
2229         }
2230       }
2231 
2232       List<MSNode> ions = activeMolecularAssembly.getIons();
2233       for (int i = 0; i < ions.size(); i++) {
2234         Molecule ion = (Molecule) ions.get(i);
2235         chainID = ion.getChainID();
2236         sb.setCharAt(21, chainID);
2237         String resName = ion.getResidueName();
2238         int resID = ion.getResidueNumber();
2239         if (resName.length() > 3) {
2240           resName = resName.substring(0, 3);
2241         }
2242         sb.replace(17, 20, padLeft(resName.toUpperCase(), 3));
2243         sb.replace(22, 26, format("%4s", Hybrid36.encode(4, resID)));
2244         // List<Atom> ionAtoms = ion.getAtomList();
2245         List<Atom> ionAtoms = ion.getAtomList().stream().filter(a -> !atomExclusions.contains(a))
2246             .collect(Collectors.toList());
2247         boolean altLocFound = false;
2248         for (Atom atom : ionAtoms) {
2249           writeAtom(atom, serial++, sb, anisouSB, bw);
2250           Character altLoc = atom.getAltLoc();
2251           if (altLoc != null && !altLoc.equals(' ')) {
2252             altLocFound = true;
2253           }
2254         }
2255         // Write out alternate conformers
2256         if (altLocFound) {
2257           for (int ma = 1; ma < molecularAssemblies.length; ma++) {
2258             MolecularAssembly altMolecularAssembly = molecularAssemblies[ma];
2259             MSNode altion = altMolecularAssembly.getIons().get(i);
2260             ionAtoms = altion.getAtomList();
2261             for (Atom atom : ionAtoms) {
2262               if (atom.getAltLoc() != null && !atom.getAltLoc().equals(' ') && !atom.getAltLoc()
2263                   .equals('A')) {
2264                 writeAtom(atom, serial++, sb, anisouSB, bw);
2265               }
2266             }
2267           }
2268         }
2269       }
2270 
2271       List<MSNode> water = activeMolecularAssembly.getWater();
2272       for (int i = 0; i < water.size(); i++) {
2273         Molecule wat = (Molecule) water.get(i);
2274         chainID = wat.getChainID();
2275         sb.setCharAt(21, chainID);
2276         String resName = wat.getResidueName();
2277         int resID = wat.getResidueNumber();
2278         if (resName.length() > 3) {
2279           resName = resName.substring(0, 3);
2280         }
2281         sb.replace(17, 20, padLeft(resName.toUpperCase(), 3));
2282         sb.replace(22, 26, format("%4s", Hybrid36.encode(4, resID)));
2283         List<Atom> waterAtoms = wat.getAtomList().stream().filter(a -> !atomExclusions.contains(a))
2284             .collect(Collectors.toList());
2285         boolean altLocFound = false;
2286         for (Atom atom : waterAtoms) {
2287           writeAtom(atom, serial++, sb, anisouSB, bw);
2288           Character altLoc = atom.getAltLoc();
2289           if (altLoc != null && !altLoc.equals(' ')) {
2290             altLocFound = true;
2291           }
2292         }
2293         // Write out alternate conformers
2294         if (altLocFound) {
2295           for (int ma = 1; ma < molecularAssemblies.length; ma++) {
2296             MolecularAssembly altMolecularAssembly = molecularAssemblies[ma];
2297             MSNode altwater = altMolecularAssembly.getWater().get(i);
2298             waterAtoms = altwater.getAtomList();
2299             for (Atom atom : waterAtoms) {
2300               if (atom.getAltLoc() != null && !atom.getAltLoc().equals(' ') && !atom.getAltLoc()
2301                   .equals('A')) {
2302                 writeAtom(atom, serial++, sb, anisouSB, bw);
2303               }
2304             }
2305           }
2306         }
2307       }
2308 
2309       if (model != null) {
2310         bw.write("ENDMDL");
2311         bw.newLine();
2312       }
2313 
2314       if (writeEnd) {
2315         bw.write("END");
2316         bw.newLine();
2317       }
2318 
2319       if (nSymOp >= 0) {
2320         serialP1 = serial;
2321       }
2322 
2323     } catch (Exception e) {
2324       String message = "Exception writing to file: " + saveFile;
2325       logger.log(Level.WARNING, message, e);
2326       return false;
2327     }
2328     return true;
2329   }
2330 
2331   /**
2332    * {@inheritDoc}
2333    *
2334    * <p>Write out the Atomic information in PDB format.
2335    */
2336   @Override
2337   public boolean writeFile(File saveFile, boolean append) {
2338     return writeFile(saveFile, append, false, true);
2339   }
2340 
2341   public boolean writeFile(File saveFile, boolean append, String[] extraLines) {
2342     return writeFile(saveFile, append, Collections.emptySet(), false, !append, extraLines);
2343   }
2344 
2345   /**
2346    * Writes out the atomic information in PDB format.
2347    *
2348    * @param saveFile   The file to save information to.
2349    * @param append     True if the current data should be appended to the saveFile (as in arc
2350    *                   files).
2351    * @param versioning True if the saveFile should be versioned. False if the saveFile should be
2352    *                   overwritten.
2353    * @return Success of writing.
2354    */
2355   public boolean writeFile(File saveFile, boolean append, boolean versioning) {
2356     return writeFile(saveFile, append, Collections.emptySet(), true, versioning);
2357   }
2358 
2359   /**
2360    * writeFileWithHeader.
2361    *
2362    * @param saveFile a {@link java.io.File} object.
2363    * @param header   a {@link java.lang.String} object.
2364    * @param append   a boolean.
2365    * @return a boolean.
2366    */
2367   public boolean writeFileWithHeader(File saveFile, String header, boolean append) {
2368     // Set standardize atom names to false in the presence of deuterium
2369     if (standardizeAtomNames) {
2370       List<Atom> deuteriumAtoms = new ArrayList<>();
2371       for (Atom atom : activeMolecularAssembly.getAtomArray()) {
2372         if (atom.getName().startsWith("D")) {
2373           String name = atom.getName().replaceFirst("D", "H");
2374           atom.setName(name);
2375           deuteriumAtoms.add(atom);
2376         }
2377       }
2378 
2379       logger.info(" Setting atom names to PDB standard.");
2380       renameAtomsToPDBStandard(activeMolecularAssembly);
2381 
2382       for (Atom atom : activeMolecularAssembly.getAtomArray()) {
2383         if (deuteriumAtoms.contains(atom) && atom.getName().startsWith("H")) {
2384           String name = atom.getName().replaceFirst("H", "D");
2385           atom.setName(name);
2386         }
2387       }
2388     }
2389     activeMolecularAssembly.setFile(saveFile);
2390     activeMolecularAssembly.setName(saveFile.getName());
2391 
2392     try (FileWriter fw = new FileWriter(saveFile, append); BufferedWriter bw = new BufferedWriter(
2393         fw)) {
2394       bw.write(header);
2395       bw.newLine();
2396     } catch (Exception e) {
2397       String message = " Exception writing to file: " + saveFile;
2398       logger.log(Level.WARNING, message, e);
2399       return false;
2400     }
2401     if (writeFile(saveFile, true)) {
2402       logger.log(Level.INFO, " Wrote PDB to file {0}", saveFile.getPath());
2403       return true;
2404     } else {
2405       logger.log(Level.INFO, " Error writing to file {0}", saveFile.getPath());
2406       return false;
2407     }
2408   }
2409 
2410   /**
2411    * writeFileWithHeader.
2412    *
2413    * @param saveFile a {@link java.io.File} object.
2414    * @param header   a {@link java.lang.String} object.
2415    * @return a boolean.
2416    */
2417   public boolean writeFileWithHeader(File saveFile, String header) {
2418     return writeFileWithHeader(saveFile, header, true);
2419   }
2420 
2421   /**
2422    * writeFileWithHeader.
2423    *
2424    * @param saveFile a {@link java.io.File} object.
2425    * @param header   a {@link java.lang.StringBuilder} object.
2426    * @return a boolean.
2427    */
2428   public boolean writeFileWithHeader(File saveFile, StringBuilder header) {
2429     return writeFileWithHeader(saveFile, header.toString());
2430   }
2431 
2432   /**
2433    * Get unique SegID for possibly duplicate chain IDs.
2434    *
2435    * @param c chain ID just read.
2436    * @return a unique segID.
2437    */
2438   private String getExistingSegID(Character c) {
2439     if (c.equals(' ')) {
2440       c = 'A';
2441     }
2442 
2443     // If the chain ID has not changed, return the existing segID.
2444     if (c.equals(currentChainID)) {
2445       return currentSegID;
2446     } else {
2447       currentChainID = null;
2448     }
2449 
2450     List<String> segIDs = segidMap.get(c);
2451     if (segIDs != null) {
2452       if (segIDs.size() > 1) {
2453         if (currentSegID == null) {
2454           currentChainID = c;
2455           currentSegID = segIDs.getFirst();
2456           return segIDs.getFirst();
2457         } else if (currentSegID.length() == 1) {
2458           currentChainID = c;
2459           currentSegID = segIDs.get(1);
2460           return segIDs.get(1);
2461         } else if (currentSegID.length() == 2) {
2462           String s = currentSegID.substring(0, 1);
2463           int num = -2;
2464           try {
2465             num = Integer.parseInt(s);
2466           } catch (NumberFormatException e) {
2467             logger.severe(" SegID of length 2 does not start with an integer.");
2468           }
2469           currentChainID = c;
2470           currentSegID = segIDs.get(num + 1);
2471           return segIDs.get(num + 1);
2472         } else {
2473           logger.info(" Too many repeated chains. Using single letter for segID.");
2474         }
2475       }
2476       return segIDs.get(0);
2477     } else {
2478       logger.log(Level.INFO, format(" Creating SegID for to chain %s", c));
2479       return getSegID(c);
2480     }
2481   }
2482 
2483   /**
2484    * Convert possibly duplicate chain IDs into unique segIDs.
2485    *
2486    * @param c chain ID just read.
2487    * @return a unique segID.
2488    */
2489   private String getSegID(Character c) {
2490     if (c.equals(' ')) {
2491       c = 'A';
2492     }
2493 
2494     // If the chain ID has not changed, return the existing segID.
2495     if (c.equals(currentChainID)) {
2496       return currentSegID;
2497     }
2498 
2499     // Loop through existing segIDs to find the first one that is unused.
2500     int count = 0;
2501     for (String segID : segIDs) {
2502       if (segID.endsWith(c.toString())) {
2503         count++;
2504       }
2505     }
2506 
2507     // If the count is greater than 0, then append it.
2508     String newSegID;
2509     if (count == 0) {
2510       newSegID = c.toString();
2511     } else {
2512       newSegID = count + c.toString();
2513     }
2514     segIDs.add(newSegID);
2515     currentChainID = c;
2516     currentSegID = newSegID;
2517 
2518     if (segidMap.containsKey(c)) {
2519       segidMap.get(c).add(newSegID);
2520     } else {
2521       List<String> newChainList = new ArrayList<>();
2522       newChainList.add(newSegID);
2523       segidMap.put(c, newChainList);
2524     }
2525 
2526     return newSegID;
2527   }
2528 
2529   /**
2530    * writeAtom
2531    *
2532    * @param atom     a {@link ffx.potential.bonded.Atom} object.
2533    * @param serial   a int.
2534    * @param sb       a {@link java.lang.StringBuilder} object.
2535    * @param anisouSB a {@link java.lang.StringBuilder} object.
2536    * @param bw       a {@link java.io.BufferedWriter} object.
2537    * @throws java.io.IOException if any.
2538    */
2539   private void writeAtom(Atom atom, int serial, StringBuilder sb, StringBuilder anisouSB,
2540                          BufferedWriter bw) throws IOException {
2541     String name = atom.getName();
2542     int nameLength = name.length();
2543     if (nameLength > 4) {
2544       name = name.substring(0, 4);
2545     } else if (nameLength == 1) {
2546       name = name + "  ";
2547     } else if (nameLength == 2) {
2548       if (atom.getAtomType().valence == 0) {
2549         name = name + "  ";
2550       } else {
2551         name = name + " ";
2552       }
2553     }
2554     double[] xyz = vdwH ? atom.getRedXYZ() : atom.getXYZ(null);
2555     if (nSymOp >= 0) {
2556       Crystal crystal = activeMolecularAssembly.getCrystal().getUnitCell();
2557       SymOp symOp = crystal.spaceGroup.getSymOp(nSymOp);
2558       double[] newXYZ = new double[xyz.length];
2559       crystal.applySymOp(xyz, newXYZ, symOp);
2560       if (lValue > 0 || mValue > 0 || nValue > 0) {
2561         double[] translation = new double[]{lValue, mValue, nValue};
2562         crystal.getUnitCell().toCartesianCoordinates(translation, translation);
2563         newXYZ[0] += translation[0];
2564         newXYZ[1] += translation[1];
2565         newXYZ[2] += translation[2];
2566       }
2567       xyz = newXYZ;
2568     }
2569     sb.replace(6, 16, format("%5s " + padLeft(name.toUpperCase(), 4), Hybrid36.encode(5, serial)));
2570     Character altLoc = atom.getAltLoc();
2571     sb.setCharAt(16, Objects.requireNonNullElse(altLoc, ' '));
2572 
2573 
2574     /*
2575      * On the following code:
2576      * #1: StringBuilder.replace will allow for longer strings, expanding the StringBuilder's length if necessary.
2577      *
2578      * #2: sb was never re-initialized, so if there was overflow,
2579      * sb would continue to be > 80 characters long, resulting in broken PDB files
2580      *
2581      * #3: It may be wiser to have XYZ coordinates result in shutdown, not
2582      * truncation of coordinates. #4: Excessive B-factors aren't much of an
2583      * issue; if the B-factor is past 999.99, that's the difference between
2584      * "density extends to Venus" and "density extends to Pluto".
2585      */
2586     StringBuilder decimals = new StringBuilder();
2587     for (int i = 0; i < 3; i++) {
2588       try {
2589         decimals.append(StringUtils.fwFpDec(xyz[i], 8, 3));
2590       } catch (IllegalArgumentException ex) {
2591         String newValue = StringUtils.fwFpTrunc(xyz[i], 8, 3);
2592         logger.info(format(" XYZ %d coordinate %8.3f for atom %s "
2593             + "overflowed bounds of 8.3f string specified by PDB "
2594             + "format; truncating value to %s", i, xyz[i], atom, newValue));
2595         decimals.append(newValue);
2596       }
2597     }
2598     try {
2599       decimals.append(StringUtils.fwFpDec(atom.getOccupancy(), 6, 2));
2600     } catch (IllegalArgumentException ex) {
2601       logger.severe(
2602           format(" Occupancy %f for atom %s is impossible; " + "value must be between 0 and 1",
2603               atom.getOccupancy(), atom));
2604     }
2605     try {
2606       decimals.append(StringUtils.fwFpDec(atom.getTempFactor(), 6, 2));
2607     } catch (IllegalArgumentException ex) {
2608       String newValue = StringUtils.fwFpTrunc(atom.getTempFactor(), 6, 2);
2609       logger.info(format(" Atom temp factor %6.2f for atom %s overflowed "
2610               + "bounds of 6.2f string specified by PDB format; truncating " + "value to %s",
2611           atom.getTempFactor(), atom, newValue));
2612       decimals.append(newValue);
2613     }
2614     sb.replace(30, 66, decimals.toString());
2615 
2616     name = Atom.ElementSymbol.values()[atom.getAtomicNumber() - 1].toString();
2617     name = name.toUpperCase();
2618     if (atom.isDeuterium()) {
2619       name = "D";
2620     }
2621     sb.replace(76, 78, padLeft(name, 2));
2622     sb.replace(78, 80, format("%2d", 0));
2623     bw.write(sb.toString());
2624     bw.newLine();
2625     // =============================================================================
2626     //  1 - 6        Record name   "ANISOU"
2627     //  7 - 11       Integer       serial         Atom serial number.
2628     // 13 - 16       Atom          name           Atom name.
2629     // 17            Character     altLoc         Alternate location indicator
2630     // 18 - 20       Residue name  resName        Residue name.
2631     // 22            Character     chainID        Chain identifier.
2632     // 23 - 26       Integer       resSeq         Residue sequence number.
2633     // 27            AChar         iCode          Insertion code.
2634     // 29 - 35       Integer       u[0][0]        U(1,1)
2635     // 36 - 42       Integer       u[1][1]        U(2,2)
2636     // 43 - 49       Integer       u[2][2]        U(3,3)
2637     // 50 - 56       Integer       u[0][1]        U(1,2)
2638     // 57 - 63       Integer       u[0][2]        U(1,3)
2639     // 64 - 70       Integer       u[1][2]        U(2,3)
2640     // 77 - 78       LString(2)    element        Element symbol, right-justified.
2641     // 79 - 80       LString(2)    charge         Charge on the atom.
2642     // =============================================================================
2643     double[] anisou = atom.getAnisou(null);
2644     if (anisou != null) {
2645       anisouSB.replace(6, 80, sb.substring(6, 80));
2646       anisouSB.replace(28, 70,
2647           format("%7d%7d%7d%7d%7d%7d", (int) (anisou[0] * 1e4), (int) (anisou[1] * 1e4),
2648               (int) (anisou[2] * 1e4), (int) (anisou[3] * 1e4), (int) (anisou[4] * 1e4),
2649               (int) (anisou[5] * 1e4)));
2650       bw.write(anisouSB.toString());
2651       bw.newLine();
2652     }
2653   }
2654 
2655   /**
2656    * PDB records that are recognized.
2657    */
2658   private enum Record {
2659     ANISOU, ATOM, CONECT, CRYST1, DBREF, END, MODEL, ENDMDL, HELIX, HETATM, LINK, MTRIX1, MTRIX2, MTRIX3, MODRES, SEQRES, SHEET, SSBOND, REMARK
2660   }
2661 
2662   /**
2663    * Presently, VERSION3_3 is default, and VERSION3_2 is anything non-standard.
2664    */
2665   public enum PDBFileStandard {
2666     VERSION2_3, VERSION3_0, VERSION3_1, VERSION3_2, VERSION3_3
2667   }
2668 
2669   public static class Mutation {
2670 
2671     /**
2672      * Residue ID of the residue to mutate.
2673      */
2674     final int resID;
2675     /**
2676      * Residue name after mutation.
2677      */
2678     final String resName;
2679     /**
2680      * Character for the chain ID of the residue that will be mutated.
2681      */
2682     final char chainChar;
2683     /**
2684      * Residue name before mutation.
2685      */
2686     String origResName;
2687     /**
2688      * The 4 x,y,z glycosyl torsion atom coordinates.
2689      */
2690     double[][] glyco = new double[4][3];
2691     /**
2692      * The 4 atom indices of glycosyl torsion atoms.
2693      */
2694     int[] glycoAtomIndex = new int[4];
2695     /**
2696      * Index correction.
2697      */
2698     int indCorr;
2699 
2700     public Mutation(int resID, char chainChar, String newResName) {
2701       newResName = newResName.toUpperCase();
2702       if (newResName.length() != 3) {
2703         logger.log(Level.WARNING, format("Invalid mutation target: %s.", newResName));
2704       }
2705       int isAA = AminoAcidUtils.getAminoAcidNumber(newResName);
2706       int isNA = NucleicAcidUtils.getNucleicAcidNumber(newResName);
2707       if (isAA == -1 && isNA == -1) {
2708         logger.log(Level.WARNING, format("Invalid mutation target: %s.", newResName));
2709       }
2710       this.resID = resID;
2711       this.chainChar = chainChar;
2712       this.resName = newResName;
2713 
2714       String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2715       indCorr = alphabet.indexOf(chainChar);
2716     }
2717 
2718     /**
2719      * Check to see if an atom is involved in the mutated base's glycosyl torsion. If the mutation is a switch from
2720      * purine to pyrimidine or vice versa, it will return '~name', meaning the name should be replaced but to include it
2721      * as an alchemical atom.
2722      *
2723      * @param atomName atom name to check
2724      * @return new name to use if it is involved in glycosyl torsion, null otherwise
2725      */
2726     public String isNonAlchemicalAtom(String atomName) {
2727       if (isWtPurine()) {
2728         if (atomName.equals("N9")) {
2729           if (isMtnPyrimidine()) {
2730             return "~N1";
2731           }
2732           return atomName;
2733         } else if (atomName.equals("C4")) {
2734           if (isMtnPyrimidine()) {
2735             return "~C2";
2736           }
2737           return atomName;
2738         }
2739         return null;
2740       }
2741 
2742       if (isWtPyrimidine()) {
2743         if (atomName.equals("N1")) {
2744           if (isMtnPurine()) {
2745             // here
2746             return "~N9";
2747           }
2748           return atomName;
2749         } else if (atomName.equals("C2")) {
2750           if (isMtnPurine()) {
2751             // here
2752             return "~C4";
2753           }
2754           return atomName;
2755         }
2756         return null;
2757       }
2758 
2759       return null;
2760     }
2761 
2762     /**
2763      * Determines what atoms should be alchemical for a purine to purine or pyrimidine to pyrimidine mutation.
2764      *
2765      * @param isWriting true if writing the pdb, false if reading the pdb
2766      * @return ArrayList of alchemical atoms, null if not a pur-pur or pyr-pyr mutation
2767      */
2768     public ArrayList<String> getAlchemicalAtoms(boolean isWriting) {
2769       // Log warning that the mutation input is the same residue and return nothing so prev. functionality is not changed
2770       if (resName.equals(origResName)) {
2771         logger.severe("Desired Mutation residue is the same as the original.");
2772         return null;
2773       }
2774 
2775       boolean purpur;
2776       if (isMtnPurine() && isWtPurine()) {
2777         purpur = true;
2778       } else if (isMtnPyrimidine() && isWtPyrimidine()) {
2779         purpur = false;
2780       } else {
2781         // Return nothing so previous functionality is not changed
2782         return null;
2783       }
2784 
2785       String res;
2786 
2787       // look at the mutation residue if writing or the original (wild type) residue if reading
2788       if (isWriting) {
2789         res = resName;
2790       } else {
2791         res = origResName;
2792       }
2793 
2794       ArrayList<String> list = new ArrayList<>();
2795       if (purpur) { // purine: either A to G or G to A
2796         if (res.equals("DAD") || res.equals("DA")) { // MTN is A
2797           list.add("N6");
2798           list.add("H61");
2799           list.add("H62");
2800           list.add("H2");
2801         } else { // MTN is G
2802           list.add("H1");
2803           list.add("N2");
2804           list.add("H21");
2805           list.add("H22");
2806           list.add("O6");
2807         }
2808       } else { // pyrimidine: either T to C or C to T
2809         if (res.equals("DTY") || res.equals("DT")) { // MTN is T
2810           list.add("H3");
2811           list.add("O4");
2812           list.add("C7");
2813           list.add("H71");
2814           list.add("H72");
2815           list.add("H73");
2816         } else { // MTN is C
2817           list.add("N4");
2818           list.add("H41");
2819           list.add("H42");
2820           list.add("H5");
2821         }
2822       }
2823       return list;
2824     }
2825 
2826     /**
2827      * Determine if the mutation residue is purine.
2828      *
2829      * @return true if mutation is purine
2830      */
2831     public boolean isMtnPurine() {
2832       return resName.equals("DA") || resName.equals("DG") || resName.equals("DAD") || resName.equals("DGU");
2833     }
2834 
2835     /**
2836      * Determine if the mutation residue is pyrimidine.
2837      *
2838      * @return true if mutation is pyrimidine
2839      */
2840     public boolean isMtnPyrimidine() {
2841       return resName.equals("DC") || resName.equals("DT") || resName.equals("DCY") || resName.equals("DTY");
2842     }
2843 
2844     /**
2845      * Determine if original (wild type) residue is purine.
2846      *
2847      * @return true if original residue is purine
2848      */
2849     public boolean isWtPurine() {
2850       return origResName.equals("DA") || origResName.equals("DG") || origResName.equals("DAD") || origResName.equals("DGU");
2851     }
2852 
2853     /**
2854      * Determine if original (wild type) residue is pyrimidine.
2855      *
2856      * @return true if original residue is pyrimidine
2857      */
2858     public boolean isWtPyrimidine() {
2859       return origResName.equals("DT") || origResName.equals("DC") || origResName.equals("DTY") || origResName.equals("DCY");
2860     }
2861 
2862     public void addTors(Atom atom, int index) {
2863       String name = atom.getName();
2864       if (name.equals("O4'")) {
2865         atom.getXYZ(glyco[0]);
2866         glycoAtomIndex[0] = index - indCorr;
2867       } else if (name.equals("C1'")) {
2868         atom.getXYZ(glyco[1]);
2869         glycoAtomIndex[1] = index - indCorr;
2870       } else if (name.equals("N9") || (name.equals("N1") && isMtnPyrimidine())) {
2871         atom.getXYZ(glyco[2]);
2872         glycoAtomIndex[2] = index - indCorr;
2873       } else if ((name.equals("C4") && isMtnPurine()) || (name.equals("C2") && isMtnPyrimidine())) {
2874         atom.getXYZ(glyco[3]);
2875         glycoAtomIndex[3] = index - indCorr;
2876       }
2877     }
2878 
2879     public void calculateTorsion() {
2880       double tors = toDegrees(DoubleMath.dihedralAngle(glyco[0], glyco[1], glyco[2], glyco[3]));
2881       double delta;
2882       if (tors > 0) {
2883         delta = tors - 180.0;
2884       } else {
2885         delta = tors + 180.0;
2886       }
2887       logger.info(format("restrain-torsion %d %d %d %d  5.000  %f 1", glycoAtomIndex[0], glycoAtomIndex[1], glycoAtomIndex[2], glycoAtomIndex[3], delta));
2888     }
2889   }
2890 }