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.SymOp;
42  import ffx.potential.MolecularAssembly;
43  import ffx.potential.Utilities.FileType;
44  import ffx.potential.bonded.Atom;
45  import ffx.potential.bonded.Bond;
46  import ffx.potential.bonded.Residue;
47  import ffx.potential.extended.ExtendedSystem;
48  import ffx.potential.parameters.AtomType;
49  import ffx.potential.parameters.BondType;
50  import ffx.potential.parameters.ForceField;
51  import org.apache.commons.configuration2.CompositeConfiguration;
52  import org.jogamp.vecmath.Vector3d;
53  
54  import java.io.BufferedReader;
55  import java.io.BufferedWriter;
56  import java.io.File;
57  import java.io.FileNotFoundException;
58  import java.io.FileReader;
59  import java.io.FileWriter;
60  import java.io.IOException;
61  import java.util.ArrayList;
62  import java.util.HashMap;
63  import java.util.List;
64  import java.util.OptionalDouble;
65  import java.util.logging.Level;
66  import java.util.logging.Logger;
67  import java.util.regex.Matcher;
68  import java.util.regex.Pattern;
69  
70  import static ffx.potential.bonded.Bond.logNoBondType;
71  import static java.lang.Double.parseDouble;
72  import static java.lang.Integer.parseInt;
73  import static java.lang.String.format;
74  
75  /**
76   * The XYZFilter class parses TINKER Cartesian coordinate (*.XYZ) files.
77   *
78   * @author Michael J. Schnieders
79   * @since 1.0
80   */
81  public class XPHFilter extends SystemFilter {
82  
83    private static final Logger logger = Logger.getLogger(XPHFilter.class.getName());
84    private BufferedReader bufferedReader = null;
85    private int snapShot;
86    private String remarkLine;
87    private ExtendedSystem extendedSystem;
88    private MolecularAssembly system = null;
89  
90    /**
91     * Constructor for XPHFilter.
92     *
93     * @param file a {@link File} object.
94     * @param system a {@link MolecularAssembly} object.
95     * @param forceField a {@link ForceField} object.
96     * @param properties a {@link CompositeConfiguration} object.
97     */
98    public XPHFilter(File file, MolecularAssembly system, ForceField forceField,
99                     CompositeConfiguration properties, ExtendedSystem esvSystem) {
100     super(file, system, forceField, properties);
101     this.fileType = FileType.XPH;
102     extendedSystem = esvSystem;
103     this.system = system;
104   }
105 
106   public XPHFilter(SystemFilter systemFilter, ExtendedSystem esvSystem) {
107     super(systemFilter.getFile(), systemFilter.getActiveMolecularSystem(),
108             systemFilter.getActiveMolecularSystem().getForceField(),
109             systemFilter.getActiveMolecularSystem().getProperties());
110 
111     extendedSystem = esvSystem;
112   }
113 
114   /**
115    * readOnto
116    *
117    * @param newFile a {@link File} object.
118    * @param oldSystem a {@link MolecularAssembly} object.
119    * @return a boolean.
120    */
121   public static boolean readOnto(File newFile, MolecularAssembly oldSystem) {
122     if (newFile == null || !newFile.exists() || oldSystem == null) {
123       return false;
124     }
125     try (BufferedReader br = new BufferedReader(new FileReader(newFile))) {
126       String data = br.readLine();
127       if (data == null) {
128         return false;
129       }
130       String[] tokens = data.trim().split(" +");
131       int num_atoms = parseInt(tokens[0]);
132       if (num_atoms != oldSystem.getAtomList().size()) {
133         return false;
134       }
135 
136       br.mark(10000);
137       data = br.readLine();
138       if (!readPBC(data, oldSystem)) {
139         br.reset();
140       }
141 
142       double[][] d = new double[num_atoms][3];
143       for (int i = 0; i < num_atoms; i++) {
144         if (!br.ready()) {
145           return false;
146         }
147         data = br.readLine();
148         if (data == null) {
149           logger.warning(format(" Check atom %d.", (i + 1)));
150           return false;
151         }
152         tokens = data.trim().split(" +");
153         if (tokens.length < 6) {
154           logger.warning(format(" Check atom %d.", (i + 1)));
155           return false;
156         }
157         d[i][0] = parseDouble(tokens[2]);
158         d[i][1] = parseDouble(tokens[3]);
159         d[i][2] = parseDouble(tokens[4]);
160       }
161       List<Atom> atoms = oldSystem.getAtomList();
162       for (Atom a : atoms) {
163         int index = a.getIndex() - 1;
164         a.setXYZ(d[index]);
165       }
166       oldSystem.center();
167       oldSystem.setFile(newFile);
168       return true;
169     } catch (Exception e) {
170       return false;
171     }
172   }
173 
174   private static boolean firstTokenIsInteger(String data) {
175     if (data == null) {
176       return false;
177     }
178 
179     // Check for a blank line.
180     data = data.trim();
181     if (data.equals("")) {
182       return false;
183     }
184 
185     // Check if the first token in an integer.
186     try {
187       String[] tokens = data.split(" +");
188       parseInt(tokens[0]);
189       return true;
190     } catch (NumberFormatException e) {
191       return false;
192     }
193   }
194 
195   /**
196    * Attempt to parse the String as unit cell parameters.
197    *
198    * @param data The String to parse.
199    * @return false if the first token in the String is an integer and true otherwise.
200    */
201   private static boolean readPBC(String data, MolecularAssembly activeMolecularAssembly) {
202     if (firstTokenIsInteger(data)) {
203       return false;
204     }
205 
206     String[] tokens = data.trim().split(" +");
207     if (tokens.length == 6) {
208       CompositeConfiguration config = activeMolecularAssembly.getProperties();
209       double a = parseDouble(tokens[0]);
210       double b = parseDouble(tokens[1]);
211       double c = parseDouble(tokens[2]);
212       double alpha = parseDouble(tokens[3]);
213       double beta = parseDouble(tokens[4]);
214       double gamma = parseDouble(tokens[5]);
215       config.setProperty("a-axis", a);
216       config.setProperty("b-axis", b);
217       config.setProperty("c-axis", c);
218       config.setProperty("alpha", alpha);
219       config.setProperty("beta", beta);
220       config.setProperty("gamma", gamma);
221 
222       Crystal crystal = activeMolecularAssembly.getCrystal();
223       if (crystal != null) {
224         crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
225       }
226     }
227     return true;
228   }
229 
230   /** {@inheritDoc} */
231   @Override
232   public void closeReader() {
233     if (bufferedReader != null) {
234       try {
235         bufferedReader.close();
236       } catch (IOException ex) {
237         logger.warning(format(" Exception in closing XYZ filter: %s", ex));
238       }
239     }
240   }
241 
242   @Override
243   public int countNumModels(){
244     File xphFile = currentFile;
245     int nAtoms = activeMolecularAssembly.getAtomArray().length;
246     Pattern crystInfoPattern = Pattern.compile(
247             "^ *(?:[0-9]+\\.[0-9]+ +){3}(?:-?[0-9]+\\.[0-9]+ +){2}(?:-?[0-9]+\\.[0-9]+) *$");
248 
249     try (BufferedReader br = new BufferedReader(new FileReader(xphFile))) {
250       String line = br.readLine();
251       int nSnaps = 0;
252       // For each header line, will read either X or X+1 lines, where X is the number of atoms.
253       while (line != null) {
254         assert parseInt(line.trim().split("\\s+")[0]) == nAtoms;
255         // Read either the crystal information *or* the first line of the snapshot.
256         line = br.readLine();
257         Matcher m = crystInfoPattern.matcher(line);
258         if (m.matches()) {
259           // If this is crystal information, move onto the first line of the snapshot.
260           br.readLine();
261         }
262         // Read lines 2-X of the XYZ.
263         for (int i = 1; i < nAtoms; i++) {
264           br.readLine();
265         }
266 
267         String data = br.readLine();
268 
269         //Read past blanklines
270         while (data != null && data.trim().equals("")) {
271           data = br.readLine();
272         }
273 
274         // Read Past ESV
275 
276         assert data != null;
277         if(data.contains("ESV")) {
278 
279           while (data != null && !data.trim().equals("")) {
280             data = br.readLine();
281           }
282         }
283 
284         ++nSnaps;
285         line = br.readLine();
286       }
287       return nSnaps;
288     } catch (Exception ex) {
289       logger.log(Level.WARNING,
290               String.format(" Exception reading trajectory file %s: %s", xphFile, ex));
291       return -1;
292     }
293   }
294 
295   /** {@inheritDoc} */
296   @Override
297   public OptionalDouble getLastReadLambda() {
298     String[] toks = remarkLine.split("\\s+");
299     int nToks = toks.length;
300     for (int i = 0; i < (nToks - 1); i++) {
301       if (toks[i].equals("Lambda:")) {
302         return OptionalDouble.of(Double.parseDouble(toks[i + 1]));
303       }
304     }
305     return OptionalDouble.empty();
306   }
307 
308   @Override
309   public String[] getRemarkLines() {
310     return new String[] {remarkLine};
311   }
312 
313   @Override
314   public int getSnapshot() {
315     return snapShot;
316   }
317 
318   /**
319    * {@inheritDoc}
320    *
321    * <p>Parse the XYZ File
322    */
323   @Override
324   public boolean readFile() {
325     File xphFile = activeMolecularAssembly.getFile();
326 
327     if (forceField == null) {
328       logger.warning(format(" No force field is associated with %s.", xphFile.toString()));
329       return false;
330     }
331     try (BufferedReader br = new BufferedReader(new FileReader(xphFile))) {
332       String data = br.readLine();
333       // Read blank lines at the top of the file
334       while (data != null && data.trim().equals("")) {
335         data = br.readLine();
336       }
337       if (data == null) {
338         return false;
339       }
340       String[] tokens = data.trim().split(" +", 2);
341       int numberOfAtoms = parseInt(tokens[0]);
342       if (numberOfAtoms < 1) {
343         return false;
344       }
345       if (tokens.length == 2) {
346         getActiveMolecularSystem().setName(tokens[1]);
347       }
348       logger.info(format(" Opening %s with %d atoms\n", xphFile.getName(), numberOfAtoms));
349       remarkLine = data.trim();
350 
351       // The header line is reasonable. Check for periodic box dimensions.
352       br.mark(10000);
353       data = br.readLine();
354       if (!readPBC(data, activeMolecularAssembly)) {
355         br.reset();
356       }
357 
358       // Prepare to parse atom lines.
359       HashMap<Integer, Integer> labelHash = new HashMap<>();
360       int[] label = new int[numberOfAtoms];
361       int[][] bonds = new int[numberOfAtoms][8];
362       double[][] d = new double[numberOfAtoms][3];
363       boolean renumber = false;
364       atomList = new ArrayList<>();
365       // Loop over the expected number of atoms.
366       for (int i = 0; i < numberOfAtoms; i++) {
367         if (!br.ready()) {
368           return false;
369         }
370         data = br.readLine();
371         if (data == null) {
372           logger.warning(
373                   format(" Check atom %d in %s.", (i + 1), activeMolecularAssembly.getFile().getName()));
374           return false;
375         }
376         tokens = data.trim().split(" +");
377         if (tokens.length < 6) {
378           logger.warning(
379                   format(" Check atom %d in %s.", (i + 1), activeMolecularAssembly.getFile().getName()));
380           return false;
381         }
382         // Valid number of tokens, so try to parse this line.
383         label[i] = parseInt(tokens[0]);
384         // Check for valid atom numbering, or flag for re-numbering.
385         if (label[i] != i + 1) {
386           renumber = true;
387         }
388         String atomName = tokens[1];
389         d[i][0] = parseDouble(tokens[2]);
390         d[i][1] = parseDouble(tokens[3]);
391         d[i][2] = parseDouble(tokens[4]);
392         int type = parseInt(tokens[5]);
393         AtomType atomType = forceField.getAtomType(Integer.toString(type));
394         if (atomType == null) {
395           StringBuilder message = new StringBuilder("Check atom type ");
396           message.append(type).append(" for Atom ").append(i + 1);
397           message.append(" in ").append(activeMolecularAssembly.getFile().getName());
398           logger.warning(message.toString());
399           return false;
400         }
401         Atom a = new Atom(i + 1, atomName, atomType, d[i]);
402         atomList.add(a);
403         // Bond Data
404         int numberOfBonds = tokens.length - 6;
405         for (int b = 0; b < 8; b++) {
406           if (b < numberOfBonds) {
407             int bond = parseInt(tokens[6 + b]);
408             bonds[i][b] = bond;
409           } else {
410             bonds[i][b] = 0;
411           }
412         }
413       }
414 
415       // Check if this is an archive.
416       if (br.ready()) {
417         // Read past blank lines between archive files
418         data = br.readLine().trim();
419         while (data.equals("") && br.ready()) {
420           data = br.readLine().trim();
421         }
422         tokens = data.split(" +", 2);
423         if (tokens.length > 0) {
424           try {
425             int archiveNumberOfAtoms = parseInt(tokens[0]);
426             if (archiveNumberOfAtoms == numberOfAtoms) {
427               setType(FileType.ARC);
428             }
429           } catch (NumberFormatException e) {
430             //
431           }
432         }
433       }
434       // Try to renumber
435       if (renumber) {
436         for (int i = 0; i < numberOfAtoms; i++) {
437           if (labelHash.containsKey(label[i])) {
438             logger.warning(format(" Two atoms have the same index: %d.", label[i]));
439             return false;
440           }
441           labelHash.put(label[i], i + 1);
442         }
443         for (int i = 0; i < numberOfAtoms; i++) {
444           int j = -1;
445           while (j < 3 && bonds[i][++j] > 0) {
446             bonds[i][j] = labelHash.get(bonds[i][j]);
447           }
448         }
449       }
450       bondList = new ArrayList<>();
451       int[] c = new int[2];
452       for (int a1 = 1; a1 <= numberOfAtoms; a1++) {
453         int j = -1;
454         while (j < 7 && bonds[a1 - 1][++j] > 0) {
455           int a2 = bonds[a1 - 1][j];
456           if (a1 < a2) {
457             if (a2 > numberOfAtoms) {
458               logger.warning(format(" Check the bond between %d and %d in %s.", a1, a2,
459                       activeMolecularAssembly.getFile().getName()));
460               return false;
461             }
462             // Check for bidirectional connection
463             boolean bidirectional = false;
464             int k = -1;
465             while (k < 7 && bonds[a2 - 1][++k] > 0) {
466               int a3 = bonds[a2 - 1][k];
467               if (a3 == a1) {
468                 bidirectional = true;
469                 break;
470               }
471             }
472             if (!bidirectional) {
473               logger.warning(format(" Check the bond between %d and %d in %s.", a1, a2,
474                       activeMolecularAssembly.getFile().getName()));
475               return false;
476             }
477             Atom atom1 = atomList.get(a1 - 1);
478             Atom atom2 = atomList.get(a2 - 1);
479             if (atom1 == null || atom2 == null) {
480               logger.warning(format(" Check the bond between %d and %d in %s.", a1, a2,
481                       activeMolecularAssembly.getFile().getName()));
482               return false;
483             }
484             Bond bond = new Bond(atom1, atom2);
485             BondType bondType = forceField.getBondType(atom1.getAtomType(), atom2.getAtomType());
486             if (bondType == null) {
487               logNoBondType(atom1, atom2, forceField);
488             } else {
489               bond.setBondType(bondType);
490             }
491             bondList.add(bond);
492           }
493         }
494       }
495       // Read ESVs
496       while (data != null && data.equals("") && br.ready()) {
497         data = br.readLine().trim();
498       }
499 
500       if (data != null) {
501         tokens = data.split(" +", 2);
502 
503         if (tokens[0].equalsIgnoreCase("ESV")) {
504           int numOfESVs = parseInt(tokens[1]);
505           data = br.readLine().trim();
506 
507           List<Residue> residueList = extendedSystem.getExtendedResidueList();
508 
509           if (numOfESVs == residueList.size()) {
510             int switchIndex = extendedSystem.getTitratingResidueList().size();
511             for (int i = 0; i < residueList.size(); i++) {
512               tokens = data.split(" +");
513 
514               if (i < switchIndex) {
515                 extendedSystem.setTitrationLambda(residueList.get(i), parseDouble(tokens[2]));
516               } else {
517                 extendedSystem.setTautomerLambda(residueList.get(i), parseDouble(tokens[2]));
518               }
519 
520               data = br.readLine().trim();
521             }
522 
523             for (Atom atom : system.getAtomList()) {
524               int atomIndex = atom.getIndex() - 1;
525               atom.setOccupancy(extendedSystem.getTitrationLambda(atomIndex));
526               atom.setTempFactor(extendedSystem.getTautomerLambda(atomIndex));
527             }
528 
529           } else {
530             logger.severe(
531                     " Number of ESVs in archive doesn't match extended system residue list size.");
532             return false;
533           }
534         }
535       }
536       return true;
537     } catch (IOException e) {
538       logger.severe(e.toString());
539     }
540     return false;
541   }
542 
543   /** {@inheritDoc} */
544   @Override
545   public boolean readNext() {
546     return readNext(false);
547   }
548 
549   /**
550    * {@inheritDoc}
551    *
552    * <p>Reads the next snap-shot of an archive into the activeMolecularAssembly. After calling this
553    * function, a BufferedReader will remain open until the <code>close</code> method is called.
554    */
555   @Override
556   public boolean readNext(boolean resetPosition) {
557     return readNext(resetPosition, true);
558   }
559 
560   /**
561    * {@inheritDoc}
562    *
563    * <p>Reads the next snap-shot of an archive into the activeMolecularAssembly. After calling this
564    * function, a BufferedReader will remain open until the <code>close</code> method is called.
565    */
566   @Override
567   public boolean readNext(boolean resetPosition, boolean print) {
568     return readNext(resetPosition, print, true);
569   }
570 
571   /**
572    * Reads the next snap-shot of an archive into the activeMolecularAssembly. After calling this
573    * function, a BufferedReader will remain open until the <code>close</code> method is called.
574    */
575   public boolean readNext(boolean resetPosition, boolean print, boolean parse) {
576     try {
577       String data;
578       Atom[] atoms = activeMolecularAssembly.getAtomArray();
579       int nSystem = atoms.length;
580 
581       if (bufferedReader == null && !resetPosition) {
582         bufferedReader = new BufferedReader(new FileReader(currentFile));
583         // Read past the first N + 1 lines that begin with an integer.
584         for (int i = 0; i < nSystem + 1; i++) {
585           data = bufferedReader.readLine();
586           while (!firstTokenIsInteger(data)) {
587             data = bufferedReader.readLine();
588           }
589         }
590         snapShot = 1;
591       } else if (resetPosition) {
592         // Reset the reader to the beginning of the file. Do not skip reading the first entry if resetPostion is true.
593         bufferedReader = new BufferedReader(new FileReader(currentFile));
594         snapShot = 0;
595       }
596 
597       snapShot++;
598 
599       data = bufferedReader.readLine();
600 
601       // Read past blank lines
602       while (data != null && data.trim().equals("")) {
603         data = bufferedReader.readLine();
604       }
605       if (data == null) {
606         return false;
607       }
608 
609       // Read Past ESV
610       if (data.contains("ESV")) {
611         while (data != null && !data.trim().equals("")) {
612           data = bufferedReader.readLine();
613         }
614 
615         data = bufferedReader.readLine();
616 
617         if (data == null) {
618           return false;
619         }
620       }
621 
622       if (print) {
623         logger.info(format("\n Attempting to read snapshot %d.", snapShot));
624       }
625       try {
626         int nArchive = parseInt(data.trim().split(" +")[0]);
627         if (nArchive != nSystem) {
628           String message = format("Number of atoms mismatch (Archive: %d, System: %d).", nArchive,
629                   nSystem);
630           if (dieOnMissingAtom) {
631             logger.severe(message);
632           }
633           logger.warning(message);
634           return false;
635         }
636       } catch (NumberFormatException e) {
637         logger.warning(e.toString());
638         return false;
639       }
640 
641       remarkLine = data;
642 
643       // The header line is reasonable. Check for periodic box dimensions.
644       bufferedReader.mark(10000);
645       data = bufferedReader.readLine();
646       if (!readPBC(data, activeMolecularAssembly)) {
647         bufferedReader.reset();
648       }
649 
650       String[] tokens;
651       for (int i = 0; i < nSystem; i++) {
652         data = bufferedReader.readLine();
653         // Read past blank lines
654         while (data != null && data.trim().equals("")) {
655           data = bufferedReader.readLine();
656         }
657         tokens = data.trim().split(" +");
658         if (tokens.length < 6) {
659           String message = format("Check atom %d in %s.", (i + 1), currentFile.getName());
660           logger.warning(message);
661           return false;
662         }
663         double x = parseDouble(tokens[2]);
664         double y = parseDouble(tokens[3]);
665         double z = parseDouble(tokens[4]);
666         int xyzIndex = atoms[i].getIndex();
667         if (xyzIndex != i + 1) {
668           String message = format("Archive atom index %d being read onto system atom index %d.",
669                   i + 1, xyzIndex);
670           logger.warning(message);
671         }
672         atoms[i].moveTo(x, y, z);
673       }
674 
675       // Read ESVs
676       int counter = 0;
677       while (data != null && !data.contains("ESV") && bufferedReader.ready()) {
678         data = bufferedReader.readLine().trim();
679         counter++;
680         if (counter > 5) {
681           logger.severe(" Read through too many lines");
682         }
683       }
684 
685       if (data != null) {
686         tokens = data.split(" +", 2);
687         if (tokens[0].equalsIgnoreCase("ESV")) {
688           int numOfESVs = parseInt(tokens[1]);
689           data = bufferedReader.readLine().trim();
690 
691           List<Residue> residueList = extendedSystem.getExtendedResidueList();
692 
693           if (numOfESVs == residueList.size()) {
694             int switchIndex = extendedSystem.getTitratingResidueList().size();
695             for (int i = 0; i < residueList.size(); i++) {
696               tokens = data.split(" +");
697 
698               if (i < switchIndex) {
699                 extendedSystem.setTitrationLambda(residueList.get(i), parseDouble(tokens[2]));
700               } else {
701                 extendedSystem.setTautomerLambda(residueList.get(i), parseDouble(tokens[2]));
702               }
703 
704               data = bufferedReader.readLine().trim();
705             }
706           } else {
707             logger.severe(
708                     " Number of ESVs in archive doesn't match extended system residue list size.");
709             return false;
710           }
711         }
712       }
713       return true;
714 
715     } catch (FileNotFoundException e) {
716       String message = format("Exception opening file %s.", currentFile);
717       logger.log(Level.WARNING, message, e);
718     } catch (IOException e) {
719       String message = format("Exception reading from file %s.", currentFile);
720       logger.log(Level.WARNING, message, e);
721     }
722     return false;
723   }
724 
725   public ExtendedSystem getExtendedSystem() {
726     return extendedSystem;
727   }
728 
729   public void setExtendedSystem(ExtendedSystem esvSystem) {
730     this.extendedSystem = esvSystem;
731   }
732 
733   /** {@inheritDoc} */
734   @Override
735   public boolean writeFile(File saveFile, boolean append, String[] extraLines) {
736     if (saveFile == null) {
737       return false;
738     }
739 
740     File newFile = saveFile;
741     if (!append) {
742       newFile = version(saveFile);
743     }
744     activeMolecularAssembly.setFile(newFile);
745     if (activeMolecularAssembly.getName() == null) {
746       activeMolecularAssembly.setName(newFile.getName());
747     }
748 
749     try (FileWriter fw = new FileWriter(newFile,
750             append && newFile.exists()); BufferedWriter bw = new BufferedWriter(fw)) {
751       // XYZ File First Line
752       int numberOfAtoms = activeMolecularAssembly.getAtomList().size();
753       StringBuilder sb = new StringBuilder(
754               format("%7d  %s", numberOfAtoms, activeMolecularAssembly.getName()));
755       if (extraLines != null) {
756         for (String line : extraLines) {
757           line = line.replaceAll("\n", " ");
758           sb.append(" ").append(line);
759         }
760       }
761       String output = sb.append("\n").toString();
762       bw.write(output);
763 
764       Crystal crystal = activeMolecularAssembly.getCrystal();
765       if (crystal != null && !crystal.aperiodic()) {
766         Crystal uc = crystal.getUnitCell();
767         String params = format("%14.8f%14.8f%14.8f%14.8f%14.8f%14.8f\n", uc.a, uc.b, uc.c, uc.alpha,
768                 uc.beta, uc.gamma);
769         bw.write(params);
770       }
771 
772       Atom a2;
773       StringBuilder line;
774       StringBuilder[] lines = new StringBuilder[numberOfAtoms];
775       // XYZ File Atom Lines
776       List<Atom> atoms = activeMolecularAssembly.getAtomList();
777       Vector3d offset = activeMolecularAssembly.getOffset();
778       for (Atom a : atoms) {
779         if (vdwH) {
780           line = new StringBuilder(
781                   format("%7d %3s%14.8f%14.8f%14.8f%6d", a.getIndex(), a.getAtomType().name,
782                           a.getRedX() - offset.x, a.getRedY() - offset.y, a.getRedZ() - offset.z,
783                           a.getType()));
784         } else {
785           line = new StringBuilder(
786                   format("%7d %3s%14.8f%14.8f%14.8f%6d", a.getIndex(), a.getAtomType().name,
787                           a.getX() - offset.x, a.getY() - offset.y, a.getZ() - offset.z, a.getType()));
788         }
789         for (Bond b : a.getBonds()) {
790           a2 = b.get1_2(a);
791           line.append(format("%8d", a2.getIndex()));
792         }
793         lines[a.getIndex() - 1] = line.append("\n");
794       }
795 
796       StringBuilder[] esvLines = null;
797       if (extendedSystem != null) {
798         List<Residue> residueList = extendedSystem.getTitratingResidueList();
799         esvLines = new StringBuilder[extendedSystem.getExtendedResidueList().size()];
800         double ESV;
801 
802         List<Atom> residueAtoms;
803         int CAIndex = -1;
804 
805         // Titrating residues
806         for (int i = 0; i < residueList.size(); i++) {
807           ESV = extendedSystem.getTitrationLambda(residueList.get(i));
808 
809           residueAtoms = residueList.get(i).getAtomList(true);
810           for (Atom atom : residueAtoms) {
811             if (atom.getAtomType().name.equalsIgnoreCase("CA")) {
812               CAIndex = atom.getIndex();
813             }
814           }
815 
816           line = new StringBuilder(
817                   format("%7d%7d%14.8f%7s\n", i, CAIndex, ESV, residueList.get(i).getAminoAcid3()));
818 
819           esvLines[i] = line;
820 
821           CAIndex = -1;
822         }
823 
824         int offsetIndex = residueList.size();
825         residueList = extendedSystem.getTautomerizingResidueList();
826         // Tautomerizing residues
827         for (int i = 0; i < residueList.size(); i++) {
828           ESV = extendedSystem.getTautomerLambda(residueList.get(i));
829 
830           residueAtoms = residueList.get(i).getAtomList(true);
831           for (Atom atom : residueAtoms) {
832             if (atom.getAtomType().name.equalsIgnoreCase("CA")) {
833               CAIndex = atom.getIndex();
834             }
835           }
836 
837           line = new StringBuilder(format("%7d%7d%14.8f%7s\n", offsetIndex + i, CAIndex, ESV,
838                   residueList.get(i).getAminoAcid3()));
839 
840           esvLines[offsetIndex + i] = line;
841 
842           CAIndex = -1;
843         }
844 
845       }
846       try {
847         for (int i = 0; i < numberOfAtoms; i++) {
848           bw.write(lines[i].toString());
849         }
850 
851         if (esvLines != null) {
852           String esvHead = format("\n%7s%7d\n", "ESV", esvLines.length);
853           bw.write(esvHead);
854 
855           for (int i = 0; i < esvLines.length; i++) {
856             bw.write(esvLines[i].toString());
857           }
858           bw.write("\n");
859         }
860       } catch (IOException e) {
861         String message = format(" There was an unexpected error writing to %s.",
862                 getActiveMolecularSystem().toString());
863         logger.log(Level.WARNING, message, e);
864         return false;
865       }
866     } catch (IOException e) {
867       String message = format(" There was an unexpected error writing to %s.",
868               getActiveMolecularSystem().toString());
869       logger.log(Level.WARNING, message, e);
870       return false;
871     }
872     return true;
873   }
874 
875   /**
876    * writeFileAsP1
877    *
878    * @param saveFile a {@link File} object.
879    * @param append a boolean.
880    * @param crystal a {@link Crystal} object.
881    * @return a boolean.
882    */
883   public boolean writeFileAsP1(File saveFile, boolean append, Crystal crystal) {
884     return writeFileAsP1(saveFile, append, crystal, null);
885   }
886 
887   /**
888    * writeFileAsP1
889    *
890    * @param saveFile a {@link File} object.
891    * @param append a boolean.
892    * @param crystal a {@link Crystal} object.
893    * @param extraLines Additional lines to print in the header.
894    * @return a boolean.
895    */
896   public boolean writeFileAsP1(File saveFile, boolean append, Crystal crystal, String[] extraLines) {
897     if (saveFile == null) {
898       return false;
899     }
900 
901     File newFile = saveFile;
902     if (!append) {
903       newFile = version(saveFile);
904     }
905     activeMolecularAssembly.setFile(newFile);
906     activeMolecularAssembly.setName(newFile.getName());
907 
908     try (FileWriter fw = new FileWriter(newFile,
909             append && newFile.exists()); BufferedWriter bw = new BufferedWriter(fw)) {
910       int nSymm = crystal.spaceGroup.symOps.size();
911       // XYZ File First Line
912       int numberOfAtoms = activeMolecularAssembly.getAtomList().size() * nSymm;
913       StringBuilder sb = new StringBuilder(
914               format("%7d  %s", numberOfAtoms, activeMolecularAssembly.toString()));
915       if (extraLines != null) {
916         for (String line : extraLines) {
917           line = line.replaceAll("\n", " ");
918           sb.append(" ").append(line);
919         }
920       }
921       String output = sb.append("\n").toString();
922       bw.write(output);
923 
924       if (!crystal.aperiodic()) {
925         Crystal uc = crystal.getUnitCell();
926         String params = format("%14.8f%14.8f%14.8f%14.8f%14.8f%14.8f\n", uc.a, uc.b, uc.c, uc.alpha,
927                 uc.beta, uc.gamma);
928         bw.write(params);
929       }
930 
931       Atom a2;
932       StringBuilder line;
933       StringBuilder[] lines = new StringBuilder[numberOfAtoms];
934       // XYZ File Atom Lines
935       Atom[] atoms = activeMolecularAssembly.getAtomArray();
936       double[] xyz = new double[3];
937       for (int iSym = 0; iSym < nSymm; iSym++) {
938         SymOp symOp = crystal.spaceGroup.getSymOp(iSym);
939         int indexOffset = iSym * atoms.length;
940         for (Atom a : atoms) {
941           int index = a.getIndex() + indexOffset;
942           String id = a.getAtomType().name;
943           if (vdwH) {
944             a.getRedXYZ(xyz);
945           } else {
946             xyz[0] = a.getX();
947             xyz[1] = a.getY();
948             xyz[2] = a.getZ();
949           }
950           crystal.applySymOp(xyz, xyz, symOp);
951           int type = a.getType();
952           line = new StringBuilder(
953                   format("%7d %3s%14.8f%14.8f%14.8f%6d", index, id, xyz[0], xyz[1], xyz[2], type));
954           for (Bond b : a.getBonds()) {
955             a2 = b.get1_2(a);
956             line.append(format("%8d", a2.getIndex() + indexOffset));
957           }
958           lines[index - 1] = line.append("\n");
959         }
960       }
961       try {
962         for (int i = 0; i < numberOfAtoms; i++) {
963           bw.write(lines[i].toString());
964         }
965       } catch (IOException e) {
966         String message = format(" There was an unexpected error writing to %s.",
967                 getActiveMolecularSystem().toString());
968         logger.log(Level.WARNING, message, e);
969         return false;
970       }
971     } catch (IOException e) {
972       String message = format(" There was an unexpected error writing to %s.",
973               getActiveMolecularSystem().toString());
974       logger.log(Level.WARNING, message, e);
975       return false;
976     }
977     return true;
978   }
979 }