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-2024.
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.FileType;
49  import ffx.potential.bonded.Atom;
50  import ffx.potential.bonded.Bond;
51  import ffx.potential.parameters.AtomType;
52  import ffx.potential.parameters.BondType;
53  import ffx.potential.parameters.ForceField;
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  import org.apache.commons.configuration2.CompositeConfiguration;
70  import org.jogamp.vecmath.Vector3d;
71  
72  /**
73   * The XYZFilter class parses TINKER Cartesian coordinate (*.XYZ) files.
74   *
75   * @author Michael J. Schnieders
76   * @since 1.0
77   */
78  public class XYZFilter extends SystemFilter {
79  
80    private static final Logger logger = Logger.getLogger(XYZFilter.class.getName());
81    private BufferedReader bufferedReader = null;
82    private int snapShot;
83    private String remarkLine;
84  
85    /**
86     * Constructor for XYZFilter.
87     *
88     * @param files a {@link java.util.List} object.
89     * @param system a {@link ffx.potential.MolecularAssembly} object.
90     * @param forceField a {@link ffx.potential.parameters.ForceField} object.
91     * @param properties a {@link org.apache.commons.configuration2.CompositeConfiguration}
92     *     object.
93     */
94    public XYZFilter(List<File> files, MolecularAssembly system, ForceField forceField,
95        CompositeConfiguration properties) {
96      super(files, system, forceField, properties);
97      this.fileType = FileType.XYZ;
98    }
99  
100   /**
101    * Constructor for XYZFilter.
102    *
103    * @param file a {@link java.io.File} object.
104    * @param system a {@link ffx.potential.MolecularAssembly} object.
105    * @param forceField a {@link ffx.potential.parameters.ForceField} object.
106    * @param properties a {@link org.apache.commons.configuration2.CompositeConfiguration}
107    *     object.
108    */
109   public XYZFilter(File file, MolecularAssembly system, ForceField forceField,
110       CompositeConfiguration properties) {
111     super(file, system, forceField, properties);
112     this.fileType = FileType.XYZ;
113   }
114 
115   /**
116    * readOnto
117    *
118    * @param newFile a {@link java.io.File} object.
119    * @param oldSystem a {@link ffx.potential.MolecularAssembly} object.
120    * @return a boolean.
121    */
122   public static boolean readOnto(File newFile, MolecularAssembly oldSystem) {
123     if (newFile == null || !newFile.exists() || oldSystem == null) {
124       return false;
125     }
126     try (BufferedReader br = new BufferedReader(new FileReader(newFile))) {
127       String data = br.readLine();
128       if (data == null) {
129         return false;
130       }
131       String[] tokens = data.trim().split(" +");
132       int num_atoms = parseInt(tokens[0]);
133       if (num_atoms != oldSystem.getAtomList().size()) {
134         return false;
135       }
136 
137       br.mark(10000);
138       data = br.readLine();
139       if (!readPBC(data, oldSystem)) {
140         br.reset();
141       }
142 
143       double[][] d = new double[num_atoms][3];
144       for (int i = 0; i < num_atoms; i++) {
145         if (!br.ready()) {
146           return false;
147         }
148         data = br.readLine();
149         if (data == null) {
150           logger.warning(format(" Check atom %d.", (i + 1)));
151           return false;
152         }
153         tokens = data.trim().split(" +");
154         if (tokens.length < 6) {
155           logger.warning(format(" Check atom %d.", (i + 1)));
156           return false;
157         }
158         d[i][0] = parseDouble(tokens[2]);
159         d[i][1] = parseDouble(tokens[3]);
160         d[i][2] = parseDouble(tokens[4]);
161       }
162       List<Atom> atoms = oldSystem.getAtomList();
163       for (Atom a : atoms) {
164         int index = a.getIndex() - 1;
165         a.setXYZ(d[index]);
166       }
167       oldSystem.center();
168       oldSystem.setFile(newFile);
169       return true;
170     } catch (Exception e) {
171       return false;
172     }
173   }
174 
175   private static boolean firstTokenIsInteger(String data) {
176     if (data == null) {
177       return false;
178     }
179 
180     // Check for a blank line.
181     data = data.trim();
182     if (data.isEmpty()) {
183       return false;
184     }
185 
186     // Check if the first token in an integer.
187     try {
188       String[] tokens = data.split(" +");
189       parseInt(tokens[0]);
190       return true;
191     } catch (NumberFormatException e) {
192       return false;
193     }
194   }
195 
196   /**
197    * Attempt to parse the String as unit cell parameters.
198    *
199    * @param data The String to parse.
200    * @return false if the first token in the String is an integer and true otherwise.
201    */
202   private static boolean readPBC(String data, MolecularAssembly activeMolecularAssembly) {
203     if (firstTokenIsInteger(data)) {
204       return false;
205     }
206 
207     String[] tokens = data.trim().split(" +");
208     if (tokens.length == 6) {
209       CompositeConfiguration config = activeMolecularAssembly.getProperties();
210       double a = parseDouble(tokens[0]);
211       double b = parseDouble(tokens[1]);
212       double c = parseDouble(tokens[2]);
213       double alpha = parseDouble(tokens[3]);
214       double beta = parseDouble(tokens[4]);
215       double gamma = parseDouble(tokens[5]);
216       config.setProperty("a-axis", a);
217       config.setProperty("b-axis", b);
218       config.setProperty("c-axis", c);
219       config.setProperty("alpha", alpha);
220       config.setProperty("beta", beta);
221       config.setProperty("gamma", gamma);
222 
223       Crystal crystal = activeMolecularAssembly.getCrystal();
224       if (crystal != null) {
225         crystal.changeUnitCellParameters(a, b, c, alpha, beta, gamma);
226       }
227     }
228     return true;
229   }
230 
231   /** {@inheritDoc} */
232   @Override
233   public void closeReader() {
234     if (bufferedReader != null) {
235       try {
236         bufferedReader.close();
237       } catch (IOException ex) {
238         logger.warning(format(" Exception in closing XYZ filter: %s", ex));
239       }
240     }
241   }
242 
243   @Override
244   public int countNumModels() {
245     File xyzFile = activeMolecularAssembly.getFile();
246     int nAtoms = activeMolecularAssembly.getAtomArray().length;
247     Pattern crystInfoPattern = Pattern.compile(
248         "^ *(?:[0-9]+\\.[0-9]+ +){3}(?:-?[0-9]+\\.[0-9]+ +){2}(?:-?[0-9]+\\.[0-9]+) *$");
249 
250     try (BufferedReader br = new BufferedReader(new FileReader(xyzFile))) {
251       String line = br.readLine();
252       int nSnaps = 0;
253       // For each header line, will read either X or X+1 lines, where X is the number of atoms.
254       while (line != null) {
255         assert parseInt(line.trim().split("\\s+")[0]) == nAtoms;
256         // Read either the crystal information *or* the first line of the snapshot.
257         line = br.readLine();
258         Matcher m = crystInfoPattern.matcher(line);
259         if (m.matches()) {
260           // If this is crystal information, move onto the first line of the snapshot.
261           br.readLine();
262         }
263         // Read lines 2-X of the XYZ.
264         for (int i = 1; i < nAtoms; i++) {
265           br.readLine();
266         }
267 
268         ++nSnaps;
269         line = br.readLine();
270       }
271       return nSnaps;
272     } catch (Exception ex) {
273       logger.log(Level.WARNING,
274           String.format(" Exception reading trajectory file %s: %s", xyzFile, ex));
275       return 1;
276     }
277   }
278 
279   /** {@inheritDoc} */
280   @Override
281   public OptionalDouble getLastReadLambda() {
282     String[] tokens = remarkLine.split("\\s+");
283     int nTokens = tokens.length;
284     for (int i = 0; i < (nTokens - 1); i++) {
285       if (tokens[i].equals("Lambda:")) {
286         return OptionalDouble.of(Double.parseDouble(tokens[i + 1]));
287       }
288     }
289     return OptionalDouble.empty();
290   }
291 
292   @Override
293   public String[] getRemarkLines() {
294     return new String[] {remarkLine};
295   }
296 
297   @Override
298   public int getSnapshot() {
299     return snapShot;
300   }
301 
302   /**
303    * {@inheritDoc}
304    *
305    * <p>Parse the XYZ File
306    */
307   @Override
308   public boolean readFile() {
309     File xyzFile = activeMolecularAssembly.getFile();
310 
311     if (forceField == null) {
312       logger.warning(format(" No force field is associated with %s.", xyzFile.toString()));
313       return false;
314     }
315     try (BufferedReader br = new BufferedReader(new FileReader(xyzFile))) {
316       String data = br.readLine();
317       // Read blank lines at the top of the file
318       while (data != null && data.trim().isEmpty()) {
319         data = br.readLine();
320       }
321       if (data == null) {
322         return false;
323       }
324       String[] tokens = data.trim().split(" +", 2);
325       int numberOfAtoms = parseInt(tokens[0]);
326       if (numberOfAtoms < 1) {
327         return false;
328       }
329       if (tokens.length == 2) {
330         getActiveMolecularSystem().setName(tokens[1]);
331       }
332       logger.info(format(" Opening %s with %d atoms\n", xyzFile.getName(), numberOfAtoms));
333       remarkLine = data.trim();
334 
335       // The header line is reasonable. Check for periodic box dimensions.
336       br.mark(10000);
337       data = br.readLine();
338       if (!readPBC(data, activeMolecularAssembly)) {
339         br.reset();
340       }
341 
342       // Prepare to parse atom lines.
343       HashMap<Integer, Integer> labelHash = new HashMap<>();
344       int[] label = new int[numberOfAtoms];
345       int[][] bonds = new int[numberOfAtoms][8];
346       double[][] d = new double[numberOfAtoms][3];
347       boolean renumber = false;
348       atomList = new ArrayList<>();
349       // Loop over the expected number of atoms.
350       for (int i = 0; i < numberOfAtoms; i++) {
351         if (!br.ready()) {
352           return false;
353         }
354         data = br.readLine();
355         if (data == null) {
356           logger.warning(
357               format(" Check atom %d in %s.", (i + 1), activeMolecularAssembly.getFile().getName()));
358           return false;
359         }
360         tokens = data.trim().split(" +");
361         if (tokens.length < 6) {
362           logger.warning(
363               format(" Check atom %d in %s.", (i + 1), activeMolecularAssembly.getFile().getName()));
364           return false;
365         }
366         // Valid number of tokens, so try to parse this line.
367         label[i] = parseInt(tokens[0]);
368         // Check for valid atom numbering, or flag for re-numbering.
369         if (label[i] != i + 1) {
370           renumber = true;
371         }
372         String atomName = tokens[1];
373         d[i][0] = parseDouble(tokens[2]);
374         d[i][1] = parseDouble(tokens[3]);
375         d[i][2] = parseDouble(tokens[4]);
376         int type = parseInt(tokens[5]);
377         AtomType atomType = forceField.getAtomType(Integer.toString(type));
378         if (atomType == null) {
379           StringBuilder message = new StringBuilder("Check atom type ");
380           message.append(type).append(" for Atom ").append(i + 1);
381           message.append(" in ").append(activeMolecularAssembly.getFile().getName());
382           logger.warning(message.toString());
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 }