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