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 ffx.crystal.SpaceGroup;
41  import ffx.crystal.SpaceGroupDefinitions;
42  import ffx.crystal.Crystal;
43  import ffx.crystal.LatticeSystem;
44  import ffx.potential.Utilities;
45  import ffx.potential.bonded.Atom;
46  import ffx.potential.bonded.Bond;
47  import ffx.potential.bonded.Angle;
48  import ffx.potential.bonded.MSNode;
49  import ffx.potential.bonded.MSGroup;
50  import ffx.potential.bonded.Molecule;
51  import ffx.potential.bonded.Polymer;
52  import ffx.potential.bonded.Residue;
53  import ffx.potential.MolecularAssembly;
54  import ffx.potential.parameters.ForceField;
55  
56  import java.io.BufferedReader;
57  import java.io.BufferedWriter;
58  import java.io.File;
59  import java.io.FileWriter;
60  import java.io.IOException;
61  import java.nio.file.Path;
62  import java.nio.file.Paths;
63  import java.util.List;
64  import java.util.Arrays;
65  import java.util.ArrayList;
66  import java.util.Objects;
67  import java.util.logging.Level;
68  import java.util.logging.Logger;
69  
70  import javax.vecmath.Point3d;
71  
72  import org.apache.commons.configuration2.CompositeConfiguration;
73  
74  import org.openscience.cdk.AtomContainer;
75  import org.openscience.cdk.config.AtomTypeFactory;
76  import org.openscience.cdk.graph.rebond.RebondTool;
77  import org.openscience.cdk.interfaces.IBond;
78  import org.openscience.cdk.interfaces.IAtom;
79  import org.openscience.cdk.interfaces.IAtomType;
80  import org.openscience.cdk.isomorphism.AtomMatcher;
81  import org.openscience.cdk.isomorphism.BondMatcher;
82  import org.openscience.cdk.isomorphism.VentoFoggia;
83  import org.openscience.cdk.isomorphism.Pattern;
84  
85  import org.rcsb.cif.CifIO;
86  import org.rcsb.cif.model.Column;
87  import org.rcsb.cif.model.FloatColumn;
88  import org.rcsb.cif.schema.core.AtomSite;
89  import org.rcsb.cif.schema.core.CifCoreBlock;
90  import org.rcsb.cif.schema.core.CifCoreFile;
91  import org.rcsb.cif.schema.core.Chemical;
92  import org.rcsb.cif.schema.core.Symmetry;
93  import org.rcsb.cif.schema.core.Cell;
94  import org.rcsb.cif.schema.StandardSchemata;
95  
96  import static ffx.crystal.SpaceGroupConversions.hrConversion;
97  import static ffx.numerics.math.DoubleMath.dihedralAngle;
98  import static ffx.numerics.math.DoubleMath.dist;
99  import static ffx.potential.bonded.BondedUtils.intxyz;
100 
101 import static java.lang.String.format;
102 
103 import static org.apache.commons.io.FilenameUtils.getExtension;
104 import static org.apache.commons.io.FilenameUtils.getFullPath;
105 import static org.apache.commons.io.FilenameUtils.getName;
106 import static org.apache.commons.io.FilenameUtils.removeExtension;
107 import static org.apache.commons.math3.util.FastMath.sqrt;
108 import static org.openscience.cdk.tools.periodictable.PeriodicTable.getCovalentRadius;
109 import static org.openscience.cdk.tools.periodictable.PeriodicTable.getSymbol;
110 
111 /**
112  * The CIFFilter class parses CIF coordinate (*.CIF) files.
113  *
114  * @author Aaron J. Nessler
115  * @since 1.0
116  */
117 public class CIFFilter extends SystemFilter {
118   /**
119    * A logger for this class.
120    */
121   private static final Logger logger = Logger.getLogger(CIFFilter.class.getName());
122   /**
123    * Set bond tolerance (distance to determine if bond is made).
124    */
125   private double bondTolerance = 0.2;
126   /**
127    * The CIF file reader.
128    */
129   private final BufferedReader bufferedReader = null;
130   /**
131    * The CIF file object.
132    */
133   private CifCoreFile cifFile;
134   /**
135    * List of output files created from converted CIF file.
136    */
137   private final List<String> createdFiles = new ArrayList<>();
138   /**
139    * The current snapshot.
140    */
141   private int snapShot;
142   /**
143    * The space group name.
144    */
145   private String sgName = null;
146   /**
147    * The space group number.
148    */
149   private int sgNum = -1;
150   /**
151    * The crystallographic information for this system.
152    */
153   private Crystal crystal;
154   /**
155    * Whether to fix lattice parameters.
156    */
157   private boolean fixLattice = false;
158   /**
159    * Whether to use PDB format.
160    */
161   private boolean usePDB = false;
162   /**
163    * The number of entities in the asymmetric unit.
164    */
165   private int zPrime = -1;
166   /**
167    * Maximum atomic covalent radius for CDK Rebonder Tool
168    */
169   public static final double MAX_COVALENT_RADIUS = 2.0;
170 
171   /**
172    * Minimum bond distance for CDK Rebonder Tool
173    */
174   public static final double MIN_BOND_DISTANCE = 0.5;
175 
176   /**
177    * Constructor for CIFFilter on a single file and a single assembly.
178    *
179    * @param file Input file
180    * @param molecularAssembly Active assembly
181    * @param forceField Force field for save file.
182    * @param properties Properties for save file.
183    */
184   public CIFFilter(File file, MolecularAssembly molecularAssembly, ForceField forceField,
185       CompositeConfiguration properties, boolean saveCIF) {
186     super(file, molecularAssembly, forceField, properties);
187     this.fileType = Utilities.FileType.CIF;
188 
189     if (!saveCIF) {
190       String dir = getFullPath(molecularAssembly.getFile().getAbsolutePath()) + File.separator;
191       String cifName = removeExtension(file.getName()) + ".cif";
192       Path path = Paths.get(dir + cifName);
193       try {
194         cifFile = CifIO.readFromPath(path).as(StandardSchemata.CIF_CORE);
195       } catch (Exception ex) {
196         logger.info(" Failed to create CIF file object.\n " + ex);
197       }
198       String ext = getExtension(file.getName());
199       currentFile = new File(removeExtension(currentFile.getAbsolutePath()) + ext);
200       if(ext.equalsIgnoreCase("pdb")){
201         usePDB = true;
202       }
203     }
204   }
205 
206   /**
207    * Constructor for CIFFilter on a single file and multiple assemblies.
208    *
209    * @param file Input file
210    * @param molecularAssemblies Active assemblies
211    * @param forceField Force field for save file.
212    * @param properties Properties for save file.
213    */
214   public CIFFilter(File file, List<MolecularAssembly> molecularAssemblies, ForceField forceField,
215       CompositeConfiguration properties, boolean saveCIF) {
216     super(file, molecularAssemblies, forceField, properties);
217     this.fileType = Utilities.FileType.CIF;
218 
219     if (!saveCIF) {
220       String dir =
221           getFullPath(molecularAssemblies.get(0).getFile().getAbsolutePath()) + File.separator;
222       String cifName = removeExtension(file.getName()) + ".cif";
223       Path path = Paths.get(dir + cifName);
224       try {
225         cifFile = CifIO.readFromPath(path).as(StandardSchemata.CIF_CORE);
226       } catch (Exception ex) {
227         logger.info(" Failed to create CIF file object.\n" + ex);
228       }
229       currentFile = new File(removeExtension(currentFile.getAbsolutePath()) + ".xyz");
230     }
231   }
232 
233   /**
234    * Constructor for CIFFilter on a multiple files and a single assembly.
235    *
236    * @param files Input files
237    * @param molecularAssembly Active assembly
238    * @param forceField Force field for save file.
239    * @param properties Properties for save file.
240    */
241   public CIFFilter(List<File> files, MolecularAssembly molecularAssembly, ForceField forceField,
242       CompositeConfiguration properties, boolean saveCIF) {
243     super(files, molecularAssembly, forceField, properties);
244     this.fileType = Utilities.FileType.CIF;
245 
246     if (!saveCIF) {
247       String dir = getFullPath(molecularAssembly.getFile().getAbsolutePath()) + File.separator;
248       String cifName = removeExtension(files.get(0).getName()) + ".cif";
249       Path path = Paths.get(dir + cifName);
250       try {
251         cifFile = CifIO.readFromPath(path).as(StandardSchemata.CIF_CORE);
252       } catch (Exception ex) {
253         logger.info(" Failed to create CIF file object.\n" + ex);
254       }
255       currentFile = new File(removeExtension(currentFile.getAbsolutePath()) + ".xyz");
256     }
257   }
258 
259   /**
260    * Add bonds between atoms.
261    *
262    * @param atoms To potentially be bonded together.
263    */
264   public static int bondAtoms(Atom[] atoms, double bondTolerance) {
265     int bondCount = 0;
266     int nAtoms = atoms.length;
267     for (int i = 0; i < nAtoms; i++) {
268       Atom atom1 = atoms[i];
269       String atomIelement = getAtomElement(atom1);
270       double radiusI =
271           (getCovalentRadius(atomIelement) == null) ? 0 : getCovalentRadius(atomIelement);
272       if (radiusI == 0) {
273         logger.warning(format(" Atom %d (%s) element (%s) not determined.", i + 1, atom1.getName(),
274             atomIelement));
275         return -1;
276       }
277       double[] xyz = {atom1.getX(), atom1.getY(), atom1.getZ()};
278       for (int j = i + 1; j < nAtoms; j++) {
279         Atom atom2 = atoms[j];
280         String atomJelement = getAtomElement(atom2);
281         double radiusJ =
282             (getCovalentRadius(atomJelement) == null) ? 0 : getCovalentRadius(atomJelement);
283         if (radiusJ == 0) {
284           logger.warning(format(" Atom %d element (%s) not determined.", j + 1, atomJelement));
285           return -1;
286         }
287         double[] xyz2 = {atom2.getX(), atom2.getY(), atom2.getZ()};
288         double length = dist(xyz, xyz2);
289         double bondLength = radiusI + radiusJ + bondTolerance;
290         if (length < bondLength) {
291           bondCount++;
292           Bond bond = new Bond(atom1, atom2);
293           atom1.setBond(bond);
294           atom2.setBond(bond);
295           logger.finest(format(
296               " Bonded atom %d (%s) with atom %d (%s): bond length (%4.4f Å) < tolerance (%4.4f Å)",
297               i + 1, atomIelement, j + 1, atomJelement, length, bondLength));
298         }
299       }
300     }
301     return bondCount;
302   }
303 
304   /** {@inheritDoc} */
305   @Override
306   public void closeReader() {
307     if (bufferedReader != null) {
308       try {
309         bufferedReader.close();
310       } catch (IOException ex) {
311         logger.warning(format(" Exception in closing CIF filter: %s", ex));
312       }
313     }
314   }
315 
316   /**
317    * Finds all atoms that are bonded to one another. See potential/Utilities/collectAtoms
318    *
319    * @param seed Starting atom.
320    * @param atoms that are bonded.
321    */
322   public static void collectAtoms(Atom seed, ArrayList<Atom> atoms) {
323     logger.finest(format(" Atom: %s", seed.getName()));
324     atoms.add(seed);
325     for (Bond b : seed.getBonds()) {
326       Atom nextAtom = b.get1_2(seed);
327       if (!atoms.contains(nextAtom)) {
328         collectAtoms(nextAtom, atoms);
329       }
330     }
331   }
332 
333   /**
334    * Find the maximum index that is less than those contained in current model.
335    *
336    * @param xyzAtomLists ArrayList containing ArrayList of atoms in each entity
337    * @param currentList Entity of currently being used.
338    * @return Maximum index that is less than indices in current entity.
339    */
340   public int findMaxLessIndex(ArrayList<ArrayList<Atom>> xyzAtomLists, int currentList) {
341     // If no less indices are found, want to return 0.
342     int lessIndex = 0;
343     int minIndex = Integer.MAX_VALUE;
344     for (Atom atom : xyzAtomLists.get(currentList)) {
345       int temp = atom.getIndex();
346       if (temp < minIndex) {
347         minIndex = temp;
348       }
349     }
350     int xyzSize = xyzAtomLists.size();
351     for (int i = 0; i < xyzSize; i++) {
352       if (i != currentList) {
353         for (Atom atom : xyzAtomLists.get(i)) {
354           int temp = atom.getIndex();
355           if (temp < minIndex && temp > lessIndex) {
356             lessIndex = temp;
357           }
358         }
359       }
360     }
361     return lessIndex;
362   }
363 
364   /**
365    * Parse atom name to determine atomic element.
366    *
367    * @param atom Atom whose element we desire
368    * @return String specifying atom element.
369    */
370   public static String getAtomElement(Atom atom) {
371     return getAtomElement(atom.getName());
372   }
373 
374   /**
375    * Parse atom name to determine atomic element.
376    *
377    * @param name Name of atom whose element we desire
378    * @return String specifying atom element.
379    */
380   private static String getAtomElement(String name) {
381     String value = "";
382     try {
383       value = name.replaceAll("[()]", "").replaceAll("_", "").replaceAll("-", "")
384           .replaceAll(" +", "").split("[0-9]")[0];
385     } catch (Exception e) {
386       logger.severe(" Error extracting atom element. Please ensure the CIF is formatted correctly.\n" + e);
387     }
388     return value;
389   }
390 
391   /**
392    * Obtain a list of output files written from the conversion. Used in file not committed to Git...
393    *
394    * @return String[] containing output file names.
395    */
396   public String[] getCreatedFileNames() {
397     String[] files = new String[createdFiles.size()];
398     return createdFiles.toArray(files);
399   }
400 
401   /**
402    * Parse CIF block to determine crystal and atom information.
403    * @param block CifCoreBlock object to be interpreted.
404    * @return Atoms from CIF. (Crystal updated as global variable)
405    */
406   private Atom[] parseBlock(CifCoreBlock block){
407     // Parse CIF file "blocks"
408     logger.info("\n Block ID: " + block.getBlockHeader());
409     Chemical chemical = block.getChemical();
410     Column<String[]> nameCommon = chemical.getNameCommon();
411     Column<String[]> nameSystematic = chemical.getNameSystematic();
412     int rowCount = nameCommon.getRowCount();
413     if (rowCount > 1) {
414       logger.info(" Chemical components");
415       for (int i = 0; i < rowCount; i++) {
416         logger.info(format("  %s", nameCommon.getColumnName()));
417       }
418     } else if (rowCount > 0) {
419       logger.info(format(" Chemical component: %s", nameCommon.getColumnName()));
420     }
421 
422     // Determine the space group from CIF.
423     Symmetry symmetry = block.getSymmetry();
424     if (sgNum == -1 && sgName == null || sgName.isEmpty()) {
425       if (symmetry.getIntTablesNumber().getRowCount() > 0) {
426         sgNum = symmetry.getIntTablesNumber().get(0);
427         logger.info(format(" CIF International Tables Number: %d", sgNum));
428       }
429       if (symmetry.getSpaceGroupNameH_M().getRowCount() > 0) {
430         sgName = symmetry.getSpaceGroupNameH_M().get(0);
431         logger.info(format(" CIF Hermann–Mauguin Space Group: %s", sgName));
432       } else if (block.getSpaceGroup().getNameH_mFull().getRowCount() > 0) {
433         sgName = block.getSpaceGroup().getNameH_mFull().get(0);
434         logger.info(format(" CIF Hermann–Mauguin Space Group: %s", sgName));
435       } else if (block.getSpaceGroup().getNameH_mAlt().getRowCount() > 0) {
436         sgName = block.getSpaceGroup().getNameH_mAlt().get(0);
437         logger.info(format(" CIF Hermann–Mauguin Space Group: %s", sgName));
438       }
439     } else {
440       // Parse user defined space group.
441       if (sgNum != -1) {
442         logger.info(format(" Command line International Tables Number: %d", sgNum));
443       } else {
444         logger.info(format(" Command line space group name: %s", sgName));
445       }
446     }
447 
448     // Set space group.
449     SpaceGroup sg = null;
450     if (sgName != null) {
451       sg = SpaceGroupDefinitions.spaceGroupFactory(sgName);
452     }
453     if (sg == null) {
454       logger.finer(" Space group name not found. Attempting to use space group number.");
455       sg = SpaceGroupDefinitions.spaceGroupFactory(sgNum);
456     }
457 
458     // Fall back to P1.
459     if (sg == null) {
460       logger.warning(" The space group could not be determined from the CIF file (using P1).");
461       sg = SpaceGroupDefinitions.spaceGroupFactory(1);
462     }
463 
464     // Generate FFX Crystal for CIF file.
465     Cell cell = block.getCell();
466     if(cell.isDefined()) {
467       double a = cell.getLengthA().get(0);
468       double b = cell.getLengthB().get(0);
469       double c = cell.getLengthC().get(0);
470       double alpha = cell.getAngleAlpha().get(0);
471       double beta = cell.getAngleBeta().get(0);
472       double gamma = cell.getAngleGamma().get(0);
473       assert sg != null;
474       LatticeSystem latticeSystem = sg.latticeSystem;
475       double[] latticeParameters = {a, b, c, alpha, beta, gamma};
476       if (latticeSystem.validParameters(a, b, c, alpha, beta, gamma)) {
477         crystal = new Crystal(a, b, c, alpha, beta, gamma, sg.pdbName);
478       } else {
479         // Fix lattice parameters if they disobey space group.
480         if (fixLattice) {
481           logger.info(
482                   " Attempting to patch disagreement between lattice system and lattice parameters.");
483           boolean fixed = false;
484           // Check if Rhombohedral lattice has been named Hexagonal
485           if (latticeSystem == LatticeSystem.HEXAGONAL_LATTICE
486                   && LatticeSystem.RHOMBOHEDRAL_LATTICE.validParameters(a, b, c, alpha, beta, gamma)) {
487             crystal = hrConversion(a, b, c, alpha, beta, gamma, sg);
488             latticeSystem = crystal.spaceGroup.latticeSystem;
489             if (latticeSystem.validParameters(crystal.a, crystal.b, crystal.c, crystal.alpha,
490                     crystal.beta, crystal.gamma)) {
491               fixed = true;
492             }
493             // Check if Hexagonal lattice has been named Rhombohedral
494           } else if (latticeSystem == LatticeSystem.RHOMBOHEDRAL_LATTICE
495                   && LatticeSystem.HEXAGONAL_LATTICE.validParameters(a, b, c, alpha, beta, gamma)) {
496             crystal = hrConversion(a, b, c, alpha, beta, gamma, sg);
497             latticeSystem = crystal.spaceGroup.latticeSystem;
498             if (latticeSystem.validParameters(crystal.a, crystal.b, crystal.c, crystal.alpha,
499                     crystal.beta, crystal.gamma)) {
500               fixed = true;
501             }
502           }
503           if (!fixed) {
504             double[] newLatticeParameters = latticeSystem.fixParameters(a, b, c, alpha, beta, gamma);
505             if (newLatticeParameters == latticeParameters) {
506               logger.warning(" Conversion Failed: The proposed lattice parameters for " + sg.pdbName
507                       + " do not satisfy the " + latticeSystem + ".");
508               return null;
509             } else {
510               a = newLatticeParameters[0];
511               b = newLatticeParameters[1];
512               c = newLatticeParameters[2];
513               alpha = newLatticeParameters[3];
514               beta = newLatticeParameters[4];
515               gamma = newLatticeParameters[5];
516               crystal = new Crystal(a, b, c, alpha, beta, gamma, sg.pdbName);
517             }
518           }
519         } else {
520           logger.warning(" Conversion Failed: The proposed lattice parameters for " + sg.pdbName
521                   + " do not satisfy the " + latticeSystem + ".");
522           logger.info(" Use \"--fixLattice\" or \"--fl\" flag to attempt to fix automatically.");
523           return null;
524         }
525       }
526       activeMolecularAssembly.setName(block.getBlockHeader());
527       logger.info(" New Crystal: " + crystal);
528       activeMolecularAssembly.setCrystal(crystal);
529     }
530     // Parse CIF Atoms
531     AtomSite atomSite = block.getAtomSite();
532     Column<String[]> label = atomSite.getLabel();
533     Column<String[]> typeSymbol = atomSite.getTypeSymbol();
534     FloatColumn fractX = atomSite.getFractX();
535     FloatColumn fractY = atomSite.getFractY();
536     FloatColumn fractZ = atomSite.getFractZ();
537 
538     FloatColumn cartX = atomSite.getCartnX();
539     FloatColumn cartY = atomSite.getCartnY();
540     FloatColumn cartZ = atomSite.getCartnZ();
541 
542     int nAtoms = label.getRowCount();
543     if (nAtoms < 1) {
544       logger.warning(" CIF file did not contain coordinates.");
545       return null;
546     }
547     if (logger.isLoggable(Level.FINE)) {
548       logger.fine(format("\n Number of Atoms in CIF: %d", nAtoms));
549     }
550     Atom[] atoms = new Atom[nAtoms];
551 
552     // Define per atom information from the CIF.
553     String resName = "CIF";
554     double bfactor = 1.0;
555     char altLoc = ' ';
556     char chain = 'A';
557 //    String segID = "A";
558     String[] symbols = new String[nAtoms];
559 
560     // Loop over atoms in CIF and generate FFX Atom(s).
561     for (int i = 0; i < nAtoms; i++) {
562       double occupancy = 1.0;
563       if(atomSite.getOccupancy().isDefined()){
564         occupancy = atomSite.getOccupancy().get(i);
565       }
566       // Assigning each atom their own resID prevents comparator from treating them as the same atom.
567       if (typeSymbol.getRowCount() > 0) {
568         symbols[i] = typeSymbol.getStringData(i);
569       } else {
570         symbols[i] = getAtomElement(label.getStringData(i));
571       }
572       double x = (fractX.isDefined()) ? fractX.get(i) : cartX.get(i);
573       double y = (fractY.isDefined()) ? fractY.get(i) : cartY.get(i);
574       double z = (fractZ.isDefined()) ? fractZ.get(i) : cartZ.get(i);
575       double[] xyz = {x, y, z};
576       if (fractX.isDefined() && crystal != null) {
577         crystal.toCartesianCoordinates(xyz, xyz);
578       }
579       atoms[i] = new Atom(i + 1, label.getStringData(i), altLoc, xyz, resName, i, chain, occupancy,
580               bfactor, symbols[i]);
581       atoms[i].setHetero(true);
582       if (logger.isLoggable(Level.FINE)) {
583         logger.fine(format(
584                 " Atom (%2d) Name: " + atoms[i].getName() + " Label: " + label.getStringData(i)
585                         + " Symbol: " + symbols[i], i));
586       }
587     }
588     return atoms;
589   }
590 
591   /**
592    * {@inheritDoc}
593    *
594    * <p>Parse the CIF File
595    */
596   @Override
597   public boolean readFile() {
598     // Open the input file (with electrostatics turned off).
599     System.setProperty("mpoleterm", "false");
600     Atom[] inputAtoms = activeMolecularAssembly.getAtomArray();
601     int nInputAtoms = inputAtoms.length;
602 
603     int numHydrogen = 0;
604     for (Atom atom : inputAtoms) {
605       if (atom.isHydrogen()) {
606         numHydrogen++;
607       }
608     }
609 
610     List<MSNode> entitiesInput = activeMolecularAssembly.getNodeList();
611     int numEntitiesInput = entitiesInput.size();
612     if (logger.isLoggable(Level.FINE)) {
613       logger.fine(format(" Number of entities in input: %d", numEntitiesInput));
614       for (MSNode entity : entitiesInput) {
615         logger.fine(format(" Entity: " + entity.getName()));
616         int size = entity.getAtomList().size();
617         logger.fine(format("   Entity Size: %3d", size));
618         if (size > 0) {
619           logger.fine(format("   Entity First Atom: " + entity.getAtomList().get(0).toString()));
620         } else {
621           logger.warning(" Entity did not contain atoms.");
622         }
623       }
624     }
625 
626     for (CifCoreBlock block : cifFile.getBlocks()) {
627       Atom[] atoms = parseBlock(block);
628       logger.info(crystal.toString());
629       if(atoms == null){
630         return false;
631       }
632       if(atoms.length == 0){
633         logger.warning(" Atoms could not be obtained from CIF.");
634         return false;
635       }
636       int nAtoms = atoms.length;
637 
638       // Define assembly object to contain CIF info.
639       MolecularAssembly outputAssembly = new MolecularAssembly(block.getBlockHeader());
640       int zPrime;
641       if (this.zPrime > 0) {
642         zPrime = this.zPrime;
643       } else if (nAtoms % nInputAtoms == 0) {
644         zPrime = nAtoms / nInputAtoms;
645       } else if (nAtoms % (nInputAtoms - numHydrogen) == 0) {
646         zPrime = nAtoms / (nInputAtoms - numHydrogen);
647       } else {
648         zPrime = 1;
649       }
650 
651       if (zPrime > 1) {
652         logger.info(format(" Detected more than one copy in asymmetric unit of CIF file (Z'=%d)."
653             + " -- attempting to separate.", zPrime));
654       }
655       ArrayList<ArrayList<Atom>> xyzatoms = new ArrayList<>();
656       int atomIndex = 1;
657       int shiftIndex = 0;
658       int[] moleculeNumbers = activeMolecularAssembly.getMoleculeNumbers();
659       if(logger.isLoggable(Level.FINER)) {
660         for (int i = 0; i < moleculeNumbers.length; i++) {
661           logger.finer(format(" Molecule Number (atom %2d): %2d", i, moleculeNumbers[i]));
662         }
663       }
664       // For each entity (protein, molecule, ion, etc) in the XYZ
665       for (int i = 0; i < numEntitiesInput; i++) {
666         MSNode mol = entitiesInput.get(i);
667         logger.info(format(" Size of Entity: %2d", mol.getAtomList().size()));
668         xyzatoms.add((ArrayList<Atom>) mol.getAtomList());
669         // Set up input (XYZ) file contents as CDK variable
670         AtomContainer xyzCDKAtoms = new AtomContainer();
671         // Number of hydrogen in an entity from the XYZ.
672         int numMolHydrogen = 0;
673         int atomCount = 0;
674         for (Atom atom : xyzatoms.get(i)) {
675           if (atom.isHydrogen()) {
676             numMolHydrogen++;
677           }
678           String atomName = getSymbol(atom.getAtomType().atomicNumber);
679           xyzCDKAtoms.addAtom(
680               new org.openscience.cdk.Atom(atomName, new Point3d(atom.getXYZ(null))));
681           atomCount++;
682         }
683         logger.info(format(" Number of atoms in XYZ CDK: %2d", atomCount));
684         int numInputMolAtoms = xyzatoms.get(i).size();
685         if (logger.isLoggable(Level.FINE)) {
686           logger.fine(format(" Current entity number of atoms: %d (%d + %dH)", numInputMolAtoms,
687                   numInputMolAtoms - numMolHydrogen, numMolHydrogen));
688         }
689 
690         // Add known input bonds; a limitation is that bonds all are given a Bond order of 1.
691         List<Bond> bonds = mol.getBondList();
692         IBond.Order order = IBond.Order.SINGLE;
693         int xyzBonds = bonds.size();
694         if (xyzBonds == 0) {
695           logger.warning(" No bonds detected in input structure. Please check input.\n " +
696               "If correct, separate non-bonded entities into multiple CIFs.");
697         }
698         for (Bond xyzBond : bonds) {
699           int atom0Index = xyzBond.getAtom(0).getXyzIndex();
700           int atom1Index = xyzBond.getAtom(1).getXyzIndex();
701           if (logger.isLoggable(Level.FINER)) {
702             logger.finer(
703                 format(" Bonded atom 1: %d, Bonded atom 2: %d", atom0Index,
704                     atom1Index));
705           }
706           logger.fine(format(" Atom 0 index: %2d Atom 1 Index: %2d lessIndex: %2d Value 0: %2d Value 1: %2d",
707                   atom0Index, atom1Index, shiftIndex, atom0Index - shiftIndex - 1, atom1Index - shiftIndex - 1));
708           xyzCDKAtoms.addBond(atom0Index - shiftIndex - 1,
709               atom1Index - shiftIndex - 1, order);
710         }
711 
712         // Assign CDK atom types for the input molecule.
713         AtomTypeFactory factory = AtomTypeFactory.getInstance(
714             "org/openscience/cdk/config/data/jmol_atomtypes.txt", xyzCDKAtoms.getBuilder());
715 
716         // Assign atom types to CDK object.
717         for (IAtom atom : xyzCDKAtoms.atoms()) {
718           setAtomTypes(factory, atom);
719           try {
720             factory.configure(atom);
721           } catch (Exception ex) {
722             logger.info(" Failed to configure atoms from CIF.\n" + ex + "\n" + Arrays.toString(
723                 ex.getStackTrace()));
724           }
725         }
726 
727         ArrayList<ArrayList<Integer>> zindices = new ArrayList<>();
728         int counter = 0;
729         // Bond atoms from CIF.
730         int cifBonds = bondAtoms(atoms, bondTolerance);
731         if (logger.isLoggable(Level.FINE)) {
732           logger.fine(
733               format(" Created %d bonds between CIF atoms (%d in input).", cifBonds, xyzBonds));
734         }
735         List<Atom> atomPool = new ArrayList<>(Arrays.asList(atoms));
736 
737         try {
738           while (!atomPool.isEmpty()) {
739             ArrayList<Atom> molecule = new ArrayList<>();
740             collectAtoms(atomPool.get(0), molecule);
741             if (logger.isLoggable(Level.FINER)) {
742               logger.finer(format(" Molecule (%d) Size: %d", counter, molecule.size()));
743             }
744             ArrayList<Integer> indices = new ArrayList<>();
745             while (!molecule.isEmpty()) {
746               Atom atom = molecule.remove(0);
747               indices.add(atom.getIndex());
748               atomPool.remove(atom);
749             }
750 
751             if (logger.isLoggable(Level.FINER)) {
752               logger.finer(format(
753                   " Molecule %d: %d atoms are ready and %d remain (%d atoms in input, %d atoms in CIF). ",
754                   counter + 1, indices.size(), atomPool.size(), nInputAtoms, nAtoms));
755             }
756             zindices.add(indices);
757             counter++;
758           }
759         } catch (Exception e) {
760           logger.info(e + "\n" + Arrays.toString(e.getStackTrace()));
761           logger.warning(" Failed to separate copies within the asymmetric unit.");
762           return false;
763         }
764         zPrime = zindices.size();
765         // Set up CIF contents as CDK variable
766         AtomContainer[] cifCDKAtomsArr = new AtomContainer[zPrime];
767         AtomContainer cifCDKAtoms = new AtomContainer();
768         for (int j = 0; j < zPrime; j++) {
769           if (zPrime > 1) {
770             logger.info(format("\n Attempting entity %d of %d", j + 1, zPrime));
771           }
772           ArrayList<Integer> currentList = zindices.get(j);
773           int cifMolAtoms = currentList.size();
774           if (logger.isLoggable(Level.FINE)) {
775             logger.fine(format(" CIF atoms in current: %d", cifMolAtoms));
776           }
777           // Detect if CIF contains multiple copies (Z'>1)
778           if (cifMolAtoms % numInputMolAtoms == 0
779               || cifMolAtoms % (numInputMolAtoms - numMolHydrogen) == 0) {
780             cifCDKAtomsArr[j] = new AtomContainer();
781             for (Integer integer : currentList) {
782               cifCDKAtomsArr[j].addAtom(new org.openscience.cdk.Atom(atoms[integer-1].getSegID(),
783                   new Point3d(atoms[integer - 1].getXYZ(null))));
784             }
785             AtomContainer nullCDKAtoms = new AtomContainer();
786 
787             for (IAtom atom : cifCDKAtomsArr[j].atoms()) {
788               if (atom.toString() == null) {
789                 nullCDKAtoms.addAtom(atom);
790               }
791             }
792             cifCDKAtomsArr[j].remove(nullCDKAtoms);
793 
794             // Compute bonds for CIF molecule.
795             factory = AtomTypeFactory.getInstance(
796                 "org/openscience/cdk/config/data/jmol_atomtypes.txt",
797                 cifCDKAtomsArr[j].getBuilder());
798             for (IAtom atom : cifCDKAtomsArr[j].atoms()) {
799               setAtomTypes(factory, atom);
800               try {
801                 factory.configure(atom);
802               } catch (Exception ex) {
803                 logger.info(" Failed to configure CIF atoms.\n" + ex + "\n" + Arrays.toString(
804                     ex.getStackTrace()));
805               }
806             }
807 
808             RebondTool rebonder = new RebondTool(MAX_COVALENT_RADIUS, MIN_BOND_DISTANCE,
809                 bondTolerance);
810             try {
811               rebonder.rebond(cifCDKAtomsArr[j]);
812             } catch (Exception ex) {
813               logger.info(
814                   "Failed to rebond CIF atoms.\n" + ex + "\n" + Arrays.toString(ex.getStackTrace()));
815             }
816 
817             int cifMolBonds = cifCDKAtomsArr[j].getBondCount();
818             if (logger.isLoggable(Level.FINE)) {
819               logger.fine(format(" Number of CIF bonds: %d (%d in input)", cifMolBonds, xyzBonds));
820             }
821             // Number of bonds matches.
822             // If cifMolBonds == 0 then ion or atom with implicit hydrogens (e.g. water, methane, etc.)
823             if (cifMolBonds != 0 && cifMolBonds % xyzBonds == 0) {
824               Pattern pattern = VentoFoggia.findIdentical(
825                   xyzCDKAtoms, AtomMatcher.forElement(), BondMatcher.forAny());
826               int[] p = pattern.match(cifCDKAtomsArr[j]);
827               int pLength = p.length;
828               if (p != null && pLength == numInputMolAtoms) {
829                 // Used matched atoms to update the positions of the input file atoms.
830                 for (int k = 0; k < pLength; k++) {
831                   if (logger.isLoggable(Level.FINEST)) {
832                     logger.finest(
833                         format(" %d input %s -> CIF %s", k, xyzCDKAtoms.getAtom(k).getSymbol(),
834                             cifCDKAtomsArr[j].getAtom(p[k]).getSymbol()));
835                   }
836                   Point3d point3d = cifCDKAtomsArr[j].getAtom(p[k]).getPoint3d();
837                   xyzatoms.get(i).get(k).setXYZ(new double[]{point3d.x, point3d.y, point3d.z});
838                 }
839               } else {
840                 if (logger.isLoggable(Level.FINE)) {
841                   logger.fine(
842                       format(" Atoms from CIF (%d) and input (%d) structures don't match.", p.length,
843                           nAtoms));
844                 }
845                 continue;
846               }
847             } else if ((xyzBonds - numMolHydrogen) == 0
848                 || cifMolBonds % ((xyzBonds - numMolHydrogen)) == 0) {
849               // Hydrogen most likely missing from file. If zero, then potentially water/methane (implicit hydrogen atoms).
850               if (logger.isLoggable(Level.FINE)) {
851                 logger.info(" CIF may contain implicit hydrogen -- attempting to patch.");
852               }
853               // Match heavy atoms between CIF and input
854               Pattern pattern = VentoFoggia.findSubstructure(
855                   cifCDKAtomsArr[j], AtomMatcher.forElement(), BondMatcher.forAny());
856               int[] p = pattern.match(xyzCDKAtoms);
857               int pLength = p.length;
858               if (p != null && pLength == numInputMolAtoms - numMolHydrogen) {
859                 // Used matched atoms to update the positions of the input file atoms.
860                 for (int k = 0; k < pLength; k++) {
861                   Point3d point3d = cifCDKAtomsArr[j].getAtom(k).getPoint3d();
862                   xyzatoms.get(i).get(p[k]).setXYZ(new double[]{point3d.x, point3d.y, point3d.z});
863                 }
864                 // Add in hydrogen atoms
865                 Atom lastKnownAtom1 = null;
866                 int knownCount = 0;
867                 for (Atom hydrogen : xyzatoms.get(i)) {
868                   if (hydrogen.isHydrogen()) {
869                     Bond bond0 = hydrogen.getBonds().get(0);
870                     Atom atom1 = bond0.get1_2(hydrogen);
871                     double angle0_2 = 0;
872                     List<Angle> anglesList = hydrogen.getAngles(); // Same as bond
873                     Atom atom2 = null;
874                     switch (anglesList.size()) {
875                       case 0 ->
876                         // H-Cl No angles
877                         // Place hydrogen slightly off center of bonded atom (~1Å away).
878                           hydrogen.moveTo(new double[]{atom1.getX() - 0.6, atom1.getY() - 0.6,
879                               atom1.getZ() - 0.6});
880                       case 1 -> {
881                         // H-O=C Need torsion
882                         // H-O-H
883                         for (Angle angle : anglesList) {
884                           atom2 = angle.get1_3(hydrogen);
885                           if (atom2 != null) {
886                             angle0_2 = angle.angleType.angle[0];
887                           }
888                           if (angle0_2 != 0) {
889                             //if angle0_2 is found then no need to look for another atom.
890                             break;
891                           }
892                         }
893                         assert atom2 != null;
894                         List<Bond> bonds2 = atom2.getBonds();
895                         Atom atom3 =
896                             (bonds2.size() > 1 && atom1 == bonds2.get(0).get1_2(atom2)) ? bonds2.get(
897                                 1).get1_2(atom2) : bonds2.get(0).get1_2(atom2);
898                         double diAng = dihedralAngle(hydrogen.getXYZ(null), atom1.getXYZ(null),
899                             atom2.getXYZ(null), atom3.getXYZ(null));
900                         if (atom1 != atom3) {
901                           intxyz(hydrogen, atom1, bond0.bondType.distance, atom2, angle0_2, atom3,
902                               Math.toDegrees(diAng), 0);
903                         } else {
904                           // Likely water as Atom 3 is not unique. Since no hydrogen atoms
905                           //  are present, there isn't a third atom...
906                           double[] coord = new double[]{atom2.getX(), atom2.getY(), atom3.getZ()};
907                           double mag = sqrt(
908                               coord[0] * coord[0] + coord[1] * coord[1] + coord[2] * coord[2]);
909                           coord[0] /= mag;
910                           coord[1] /= mag;
911                           coord[2] /= mag;
912                           hydrogen.moveTo(atom1.getX() - coord[0], atom1.getY() - coord[1],
913                               atom1.getZ() - coord[2]);
914                         }
915                       }
916                       default -> {
917                         // H-C(-C)(-C)-C
918                         Atom atom2B = null;
919                         double angle0_2B = 0;
920                         Atom proposedAtom;
921                         double proposedAngle = 0;
922                         int chiral = 1;
923                         for (Angle angle : anglesList) {
924                           proposedAtom = angle.get1_3(hydrogen);
925                           if (proposedAtom != null && !proposedAtom.isHydrogen()) {
926                             proposedAngle = angle.angleType.angle[0];
927                           }
928                           if (proposedAngle != 0) {
929                             // If angle1_3 is found then no need to look for another atom.
930                             if (angle0_2 != 0) {
931                               atom2B = proposedAtom;
932                               angle0_2B = proposedAngle;
933                             } else {
934                               atom2 = proposedAtom;
935                               angle0_2 = proposedAngle;
936                             }
937                             proposedAngle = 0.0;
938                           }
939                           if (angle0_2 != 0 && angle0_2B != 0) {
940                             break;
941                           }
942                         }
943                         if (lastKnownAtom1 == null || lastKnownAtom1 != atom1) {
944                           lastKnownAtom1 = atom1;
945                           knownCount = 0;
946                         } else {
947                           knownCount++;
948                         }
949                         if (angle0_2B == 0.0) {
950                           // Hydrogen position depends on other hydrogen, use generic location
951                           chiral = 0;
952                           assert atom2 != null;
953                           List<Bond> bonds2 = atom2.getBonds();
954                           atom2B =
955                               (atom1 == bonds2.get(0).get1_2(atom2)) ? bonds2.get(1).get1_2(atom2)
956                                   : bonds2.get(0).get1_2(atom2);
957                           // Evenly space out hydrogen
958                           angle0_2B = 180.0 - 120.0 * knownCount;
959                         } else if (anglesList.size() == 2) {
960                           // Trigonal hydrogen use equipartition between selected atoms
961                           chiral = 3;
962                         } else if (knownCount == 1) {
963                           // Already created one hydrogen (chiral = 1), use other.
964                           chiral = -1;
965                         }
966                         //TODO discern whether to use chiral = 1 or -1 when angles are to three heavy atoms.
967                         intxyz(hydrogen, atom1, bond0.bondType.distance, atom2, angle0_2, atom2B,
968                             angle0_2B, chiral);
969                       }
970                     }
971                   }
972                 }
973               } else {
974                 if (logger.isLoggable(Level.FINE)) {
975                   logger.fine(" Could not match heavy atoms between CIF and input.");
976                 }
977                 if (p != null && logger.isLoggable(Level.FINE)) {
978                   logger.fine(
979                       format(" Matched %d atoms out of %d in CIF (%d in input)", pLength, nAtoms,
980                           nInputAtoms - numMolHydrogen));
981                 }
982                 continue;
983               }
984             } else {
985               logger.info(format(" CIF (%d) and input ([%d+%dH=]%d) have a different number of bonds.",
986                   cifMolBonds, xyzBonds - numMolHydrogen, numMolHydrogen, xyzBonds));
987               continue;
988             }
989             cifCDKAtoms.add(cifCDKAtomsArr[j]);
990             MSGroup molecule;
991             if (mol instanceof Polymer) {
992               molecule = new Polymer(((Polymer) mol).getChainID(), mol.getName(), true);
993             } else {
994               molecule = new Molecule(mol.getName());
995             }
996             List<Atom> atomList = new ArrayList<>();
997             for (Atom atom : xyzatoms.get(i)) {
998               if(logger.isLoggable(Level.FINER)) {
999                 logger.finer(format(" Atom Residue: %s %2d", atom.getResidueName(), atom.getResidueNumber()));
1000               }
1001               Atom molAtom;
1002               if (molecule instanceof Polymer) {
1003                 molAtom = new Atom(atomIndex++, atom.getName(), atom.getAltLoc(), atom.getXYZ(null),
1004                     atom.getResidueName(), atom.getResidueNumber(), atom.getChainID(), atom.getOccupancy(),
1005                     atom.getTempFactor(), atom.getSegID());
1006                 molAtom.setAtomType(atom.getAtomType());
1007               } else {
1008                 molAtom = new Atom(atomIndex++, atom.getName(), atom.getAtomType(), atom.getXYZ(null));
1009                 if(atom.getResidueName() != null){
1010                   molAtom.setResName(atom.getResidueName());
1011                 }
1012               }
1013               atomList.add(molAtom);
1014             }
1015             List<Bond> bondList = mol.getBondList();
1016             for (Bond bond : bondList) {
1017               Atom a1 = bond.getAtom(0);
1018               Atom a2 = bond.getAtom(1);
1019               if(logger.isLoggable(Level.FINE)) {
1020                 logger.fine(format(" Bonded atom 1: %d, Bonded atom 2: %d", a1.getXyzIndex(),
1021                     a2.getXyzIndex()));
1022               }
1023               Atom newA1 = atomList.get(a1.getIndex() - shiftIndex - 1);
1024               Atom newA2 = atomList.get(a2.getIndex() - shiftIndex - 1);
1025               Bond bond2 = new Bond(newA1, newA2);
1026               bond2.setBondType(bond.getBondType());
1027             }
1028             Residue res = null;
1029             for (Atom atom : atomList) {
1030               if(molecule instanceof Polymer){
1031                 if(res == null){
1032                   res = new Residue(atom.getResidueName(), atom.getResidueNumber(), ((Polymer) mol).getResidue(atom.getResidueNumber()).getResidueType());
1033                 } else if(res.getResidueNumber() != atom.getResidueNumber()){
1034                   if(logger.isLoggable(Level.FINER)) {
1035                     logger.finer(" Added Residue " + res.getResidueNumber() + " " + res.getName());
1036                   }
1037                   molecule.addMSNode(res);
1038                   res = new Residue(atom.getResidueName(), atom.getResidueNumber(), ((Polymer) mol).getResidue(atom.getResidueNumber()).getResidueType());
1039                 }
1040                 res.addMSNode(atom);
1041               }else {
1042                 molecule.addMSNode(atom);
1043               }
1044             }
1045             if(molecule instanceof Polymer && res != null){
1046               if(logger.isLoggable(Level.FINER)) {
1047                 logger.finer(" Added Final Residue " + res.getResidueNumber() + " " + res.getName());
1048               }
1049               molecule.addMSNode(res);
1050             }
1051             outputAssembly.addMSNode(molecule);
1052           } else {
1053             if (logger.isLoggable(Level.INFO)) {
1054               logger.info(
1055                   format(" Number of atoms in CIF (%d) molecule do not match input (%d + %dH = %d).",
1056                       cifMolAtoms, nInputAtoms - numMolHydrogen, numMolHydrogen, nInputAtoms));
1057             }
1058           }
1059         }
1060         shiftIndex += mol.getAtomList().size();
1061       }
1062       // If no atoms, then conversion has failed... use active assembly
1063       if (logger.isLoggable(Level.FINE)) {
1064         logger.fine(format("\n Output Assembly Atoms: %d", outputAssembly.getAtomList().size()));
1065       }
1066       outputAssembly.setPotential(activeMolecularAssembly.getPotentialEnergy());
1067       outputAssembly.setCrystal(crystal);
1068       outputAssembly.setForceField(activeMolecularAssembly.getForceField());
1069       outputAssembly.setFile(activeMolecularAssembly.getFile());
1070       outputAssembly.setName(activeMolecularAssembly.getName());
1071       setMolecularSystem(outputAssembly);
1072 
1073       if (outputAssembly.getAtomList().isEmpty()) {
1074         logger.info(" Atom types could not be matched. File could not be written.");
1075       } else if (!writeOutputFile()) {
1076         logger.info(" Input File could not be written.");
1077       }
1078       // Finsished with this block. Clean up/reset variables for next block.
1079       outputAssembly.destroy();
1080       sgNum = -1;
1081       sgName = null;
1082     }
1083     return true;
1084   }
1085 
1086   /**
1087    * Reads the next snapshot of an archive into the activeMolecularAssembly. After calling this
1088    * function, a BufferedReader will remain open until the <code>close</code> method is called.
1089    */
1090   @Override
1091   public boolean readNext() {
1092     return readNext(false, true);
1093   }
1094 
1095   /**
1096    * Reads the next snapshot of an archive into the activeMolecularAssembly. After calling this
1097    * function, a BufferedReader will remain open until the <code>close</code> method is called.
1098    */
1099   @Override
1100   public boolean readNext(boolean resetPosition) {
1101     return readNext(resetPosition, true);
1102   }
1103 
1104   /**
1105    * Reads the next snapshot of an archive into the activeMolecularAssembly. After calling this
1106    * function, a BufferedReader will remain open until the <code>close</code> method is called.
1107    */
1108   @Override
1109   public boolean readNext(boolean resetPosition, boolean print) {
1110     return readNext(resetPosition, print, true);
1111   }
1112 
1113   /**
1114    * Reads the next snapshot of an archive into the activeMolecularAssembly. After calling this
1115    * function, a BufferedReader will remain open until the <code>close</code> method is called.
1116    */
1117   @Override
1118   public boolean readNext(boolean resetPosition, boolean print, boolean parse) {
1119     List<CifCoreBlock> blocks = cifFile.getBlocks();
1120     CifCoreBlock currentBlock;
1121     if (!parse) {
1122       snapShot++;
1123       if (print) {
1124         logger.info(format(" Skipped Block: %d", snapShot));
1125       }
1126       return true;
1127     } else if (resetPosition) {
1128       currentBlock = blocks.get(0);
1129       snapShot = 0;
1130     } else if (++snapShot < blocks.size()) {
1131       currentBlock = blocks.get(snapShot);
1132     } else {
1133       if (print) {
1134         logger.info(" Reached end of available blocks in CIF file.");
1135       }
1136       return false;
1137     }
1138     if (print) {
1139       logger.info(" Current Block: " + currentBlock.getBlockHeader());
1140     }
1141     return true;
1142   }
1143 
1144   /**
1145    * Specify the atom types for atoms created by factory.
1146    *
1147    * @param factory Atom generator
1148    * @param atom Atom of interest
1149    */
1150   public static void setAtomTypes(AtomTypeFactory factory, IAtom atom) {
1151     String atomTypeName = atom.getAtomTypeName();
1152     if (atomTypeName == null || atomTypeName.isEmpty()) {
1153       IAtomType[] types = factory.getAtomTypes(atom.getSymbol());
1154       if (types.length > 0) {
1155         IAtomType atomType = types[0];
1156         atom.setAtomTypeName(atomType.getAtomTypeName());
1157       } else {
1158         logger.info(" No atom type found for " + atom);
1159       }
1160     }
1161   }
1162 
1163   /**
1164    * Set buffer value when bonding atoms together.
1165    *
1166    * @param bondTolerance Value added to VdW radii when determining bonding.
1167    */
1168   public void setBondTolerance(double bondTolerance) {
1169     this.bondTolerance = bondTolerance;
1170   }
1171 
1172   /**
1173    * Determine whether lattice parameters can be manipulated to follow lattice system constraints.
1174    *
1175    * @param fixLattice True if lattices should be fixed.
1176    */
1177   public void setFixLattice(boolean fixLattice) {
1178     this.fixLattice = fixLattice;
1179   }
1180 
1181   /**
1182    * Override the space group of a CIF conversion based on space group name.
1183    *
1184    * @param sgName Name of the desired space group
1185    */
1186   public void setSgName(String sgName) {
1187     this.sgName = sgName;
1188   }
1189 
1190   /**
1191    * Override the space group of a CIF conversion based on space group number.
1192    *
1193    * @param sgNum Number of the desired space group
1194    */
1195   public void setSgNum(int sgNum) {
1196     this.sgNum = sgNum;
1197   }
1198 
1199   /**
1200    * Override the number of copies in the asymmetric unit (Z').
1201    *
1202    * @param zPrime Number of the desired space group
1203    */
1204   public void setZprime(int zPrime) {
1205     this.zPrime = zPrime;
1206   }
1207 
1208   /**
1209    * Write CIF files for multiple File objects.
1210    *
1211    * @return whether conversion was successful.
1212    */
1213   public boolean writeFiles() {
1214     for (File file : files) {
1215       File saveFile = new File(removeExtension(file.getAbsolutePath()) + ".cif");
1216       if (!writeFile(saveFile, true, null)) {
1217         return false;
1218       }
1219     }
1220     return true;
1221   }
1222 
1223   /**
1224    * Save a CIF file for the given molecular assembly.
1225    *
1226    * @return whether conversion was successful.
1227    */
1228   @Override
1229   public boolean writeFile(File saveFile, boolean append) {
1230     return writeFile(saveFile, append, null);
1231   }
1232 
1233   /**
1234    * Save a CIF file for the given molecular assembly.
1235    *
1236    * @return whether conversion was successful.
1237    */
1238   @Override
1239   public boolean writeFile(File saveFile, boolean append, String[] extraLines) {
1240     try {
1241       if (!append) {
1242         SystemFilter.setVersioning(Versioning.PREFIX);
1243         saveFile = version(saveFile);
1244       }
1245       BufferedWriter bw = new BufferedWriter(new FileWriter(saveFile, append));
1246       if (extraLines != null) {
1247         for (String line : extraLines) {
1248           line = line.replaceAll("\n", " ");
1249           bw.write("### " + line + "\n");
1250         }
1251       }
1252       bw.write("\ndata_" + activeMolecularAssembly.getName());
1253       Crystal xtal = activeMolecularAssembly.getCrystal().getUnitCell();
1254       bw.write("\n_symmetry_cell_setting\t" + xtal.spaceGroup.latticeSystem.name().toLowerCase()
1255           .replaceAll("_lattice", ""));
1256       bw.write("\n_symmetry_space_group_name_H-M\t" + "'" + xtal.spaceGroup.shortName + "'");
1257       bw.write("\n_symmetry_Int_Tables_number\t" + xtal.spaceGroup.number);
1258       bw.write("\nloop_\n_symmetry_equiv_pos_site_id\n_symmetry_equiv_pos_as_xyz");
1259       int numSymOps = xtal.spaceGroup.getNumberOfSymOps();
1260       for (int i = 0; i < numSymOps; i++) {
1261         bw.write(format(
1262             "\n%d " + xtal.spaceGroup.getSymOp(i).toXYZString().toLowerCase().replaceAll(" +", ""),
1263             i));
1264       }
1265       bw.write(format("\n_cell_length_a\t%4.4f", xtal.a));
1266       bw.write(format("\n_cell_length_b\t%4.4f", xtal.b));
1267       bw.write(format("\n_cell_length_c\t%4.4f", xtal.c));
1268       bw.write(format("\n_cell_angle_alpha\t%4.4f", xtal.alpha));
1269       bw.write(format("\n_cell_angle_beta\t%4.4f", xtal.beta));
1270       bw.write(format("\n_cell_angle_gamma\t%4.4f", xtal.gamma));
1271       bw.write(format("\n_cell_volume\t%4.4f", xtal.volume));
1272       int numEntities = (zPrime < 1) ? activeMolecularAssembly.getMolecules().size() : zPrime;
1273       if (numEntities > 1) {
1274         if (zPrime < 1) {
1275           logger.info(format(" Molecules detected, guessing a Z' of %d. Set manually using --zp.",
1276               numEntities));
1277         }
1278         bw.write(format("\n_cell_formula_units_Z\t%3d", numEntities));
1279       }
1280       bw.write("\nloop_");
1281       bw.write("\n_atom_site_label");
1282       bw.write("\n_atom_site_type_symbol");
1283       bw.write("\n_atom_site_fract_x");
1284       bw.write("\n_atom_site_fract_y");
1285       bw.write("\n_atom_site_fract_z");
1286       Atom[] atoms = activeMolecularAssembly.getAtomArray();
1287       int count = 1;
1288       for (Atom atom : atoms) {
1289         String name = atom.getName();
1290         String symbol = getSymbol(atom.getAtomicNumber());
1291         if (Objects.equals(name, symbol)) {
1292           name += count++;
1293         }
1294         double[] xyzC = {atom.getX(), atom.getY(), atom.getZ()};
1295         double[] xyzF = new double[3];
1296         xtal.toFractionalCoordinates(xyzC, xyzF);
1297         bw.write(format("\n%-3s %2s %8.6f %8.6f %8.6f", name, symbol, xyzF[0], xyzF[1], xyzF[2]));
1298       }
1299       bw.write("\n#END\n");
1300       bw.close();
1301       logger.info(format("\n Wrote CIF file: %s \n", saveFile.getAbsolutePath()));
1302     } catch (Exception ex) {
1303       logger.info(format("\n Failed to write out CIF file: %s \n" + ex, saveFile.getAbsolutePath()));
1304       return false;
1305     }
1306     if (!createdFiles.contains(saveFile.getAbsolutePath())) {
1307       createdFiles.add(saveFile.getAbsolutePath());
1308     }
1309     return true;
1310   }
1311 
1312   /**
1313    * Write an output file.
1314    *
1315    * @return Whether file was written successfully.
1316    */
1317   private boolean writeOutputFile() {
1318     String dir = getFullPath(files.get(0).getAbsolutePath()) + File.separator;
1319     String fileName = removeExtension(getName(files.get(0).getAbsolutePath()));
1320     String spacegroup;
1321     if(activeMolecularAssembly.getCrystal() != null) {
1322       spacegroup = activeMolecularAssembly.getCrystal().getUnitCell().spaceGroup.shortName;
1323     }else{
1324       spacegroup = null;
1325     }
1326     List<MSNode> entities = activeMolecularAssembly.getAllBondedEntities();
1327 
1328     File saveFile;
1329     if(usePDB){
1330       // Change name for different space groups. Input cannot handle multiple space groups in same file.
1331       if (entities.size() > 1) {
1332         fileName += "_z" + entities.size();
1333       }
1334       saveFile = new File(dir + fileName + ".pdb");
1335     } else if (cifFile.getBlocks().size() > 1) {
1336       // Change name for different space groups. Input cannot handle multiple space groups in same file.
1337       if (entities.size() > 1) {
1338         fileName += "_z" + entities.size();
1339       }
1340       fileName += "_" + spacegroup.replaceAll("\\/", "");
1341       saveFile = new File(dir + fileName + ".arc");
1342     } else {
1343       // If only structure then create XYZ.
1344       saveFile = XYZFilter.version(new File(dir + fileName + ".xyz"));
1345     }
1346     ForceField ff = activeMolecularAssembly.getForceField();
1347     CompositeConfiguration properties = activeMolecularAssembly.getProperties();
1348     if(usePDB){
1349       PDBFilter pdbFilter = new PDBFilter(saveFile, activeMolecularAssembly, ff, properties);
1350       pdbFilter.writeFile(saveFile, true);
1351     }else {
1352       XYZFilter xyzFilter = new XYZFilter(saveFile, activeMolecularAssembly, ff, properties);
1353       xyzFilter.writeFile(saveFile, true);
1354     }
1355     logger.info("\n Saved output file:        " + saveFile.getAbsolutePath());
1356     writePropertiesFile(dir, fileName, spacegroup, entities);
1357     if (!createdFiles.contains(saveFile.getAbsolutePath())) {
1358       createdFiles.add(saveFile.getAbsolutePath());
1359     }
1360     return true;
1361   }
1362 
1363   /**
1364    * Write an output file.
1365    *
1366    * @return Whether file was written successfully.
1367    */
1368   public boolean writeOutputFile(MolecularAssembly ma) {
1369     setMolecularSystem(ma);
1370     return writeOutputFile();
1371   }
1372 
1373   /**
1374    * Write a properties file.
1375    * @param dir String for directory location.
1376    * @param fileName String for file name.
1377    * @param spacegroup String for space group.
1378    * @param entities List of MSNodes in assembly.
1379    */
1380   public void writePropertiesFile(String dir, String fileName, String spacegroup, List<MSNode> entities) {
1381     // Write out a property file.
1382     File propertyFile = new File(dir + fileName + ".properties");
1383     if (!propertyFile.exists()) {
1384       try {
1385         FileWriter fw = new FileWriter(propertyFile, false);
1386         BufferedWriter bw = new BufferedWriter(fw);
1387         ForceField forceField = activeMolecularAssembly.getForceField();
1388         String parameters = forceField.getString("parameters", "none");
1389         if (parameters != null && !parameters.equalsIgnoreCase("none")) {
1390           bw.write(format("parameters %s\n", parameters));
1391         }
1392         String forceFieldProperty = forceField.getString("forcefield", "none");
1393         if (forceFieldProperty != null && !forceFieldProperty.equalsIgnoreCase("none")) {
1394           bw.write(format("forcefield %s\n", forceFieldProperty));
1395         }
1396         String patch = forceField.getString("patch", "none");
1397         if (patch != null && !patch.equalsIgnoreCase("none")) {
1398           bw.write(format("patch %s\n", patch));
1399         }
1400         if(spacegroup != null) {
1401           bw.write(format("spacegroup %s\n", spacegroup));
1402         }
1403         if (entities.size() > 1) {
1404           bw.write("intermolecular-softcore true\n");
1405         }
1406         logger.info("\n Saved properties file: " + propertyFile.getAbsolutePath() + "\n");
1407         bw.close();
1408       } catch (Exception ex) {
1409         logger.info("Failed to write files.\n" + ex);
1410       }
1411     } else {
1412       logger.info("\n Property file already exists:  " + propertyFile.getAbsolutePath() + "\n");
1413     }
1414   }
1415 }