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