001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021package org.biojava.nbio.structure.xtal;
022
023
024import org.biojava.nbio.structure.*;
025import org.biojava.nbio.structure.contact.AtomContactSet;
026import org.biojava.nbio.structure.contact.StructureInterface;
027import org.biojava.nbio.structure.contact.StructureInterfaceList;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import javax.vecmath.Matrix4d;
032import javax.vecmath.Point3i;
033import javax.vecmath.Vector3d;
034import java.util.*;
035
036
037/**
038 * A class containing methods to find interfaces in a given crystallographic Structure by
039 * reconstructing the crystal lattice through application of symmetry operators
040 *
041 * @author Jose Duarte
042 *
043 */
044
045public class CrystalBuilder {
046
047        public static final String NCS_CHAINID_SUFFIX_CHAR = "n";
048
049        // Default number of cell neighbors to try in interface search (in 3 directions of space).
050        // In the search, only bounding box overlaps are tried, thus there's not so much overhead in adding
051        // more cells. We actually tested it and using numCells from 1 to 10 didn't change runtimes at all.
052        // Examples with interfaces in distant neighbor cells:
053        //   2nd neighbors: 3hz3, 1wqj, 2de3, 1jcd
054        //   3rd neighbors: 3bd3, 1men, 2gkp, 1wui
055        //   5th neighbors: 2ahf, 2h2z
056        //   6th neighbors: 1was (in fact interfaces appear only at 5th neighbors for it)
057        // Maybe this could be avoided by previously translating the given molecule to the first cell,
058        // BUT! some bona fide cases exist, e.g. 2d3e: it is properly placed at the origin but the molecule
059        // is enormously long in comparison with the dimensions of the unit cell, some interfaces come at the 7th neighbor.
060        // After a scan of the whole PDB (Oct 2013) using numCells=50, the highest one was 4jgc with
061        // interfaces up to the 11th neighbor. Other high ones (9th neighbors) are 4jbm and 4k3t.
062        // We set the default value to 20 to be on the safe side. Runtime does not seem to be affected at all - JD 2020-01-12
063        // Some good examples in this posting in CCP4: https://www.jiscmail.ac.uk/cgi-bin/webadmin?A2=CCP4BB;45b2755d.2001
064        // in any case the 5m3h example in the posting seems to have contacts only up to the 11th neighbor.
065        public static final int DEF_NUM_CELLS = 20;
066
067        /**
068         * Default maximum distance between two chains to be considered an interface.
069         * @see #getUniqueInterfaces(double)
070         */
071        public static final double DEFAULT_INTERFACE_DISTANCE_CUTOFF = 5.5;
072
073        public static final Matrix4d IDENTITY = new Matrix4d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
074
075
076        /**
077         * Whether to consider HETATOMs in contact calculations
078         */
079        private static final boolean INCLUDE_HETATOMS = true;
080
081        private Structure structure;
082        private PDBCrystallographicInfo crystallographicInfo;
083        private int numPolyChainsAu;
084        private int numOperatorsSg;
085        private Map<String,Matrix4d> chainNcsOps = null;
086        private Map<String,String> chainOrigNames = null;
087
088        private static final Logger logger = LoggerFactory.getLogger(CrystalBuilder.class);
089
090        private int numCells;
091
092        private ArrayList<CrystalTransform> visitedCrystalTransforms;
093        private Map<String,Map<Matrix4d,StructureInterface>> visitedNcsChainPairs = null;
094
095        private boolean searchBeyondAU;
096        private Matrix4d[] ops;
097
098        /**
099         * Special constructor for NCS-aware CrystalBuilder.
100         * The output list of interfaces will be pre-clustered by NCS-equivalence.
101         * Run {@link CrystalBuilder#expandNcsOps(Structure, Map, Map)} first to extend the AU
102         * and get the equivalence information.
103         * @param structure
104         *          NCS-extended structure
105         * @param chainOrigNames
106         *          chain names mapped to the original chain names (pre-NCS extension)
107         * @param chainNcsOps
108         *          chain names mapped to the ncs operators that was used to generate them
109         * @since 5.0.0
110         */
111        public CrystalBuilder(Structure structure, Map<String,String> chainOrigNames, Map<String,Matrix4d> chainNcsOps) {
112                this(structure);
113                this.chainOrigNames = chainOrigNames;
114                this.chainNcsOps = chainNcsOps;
115        }
116
117        public CrystalBuilder(Structure structure) {
118                this.structure = structure;
119                this.crystallographicInfo = structure.getCrystallographicInfo();
120                this.numPolyChainsAu = structure.getPolyChains().size();
121
122                this.searchBeyondAU = false;
123                if (structure.isCrystallographic()) {
124
125                        this.searchBeyondAU = true;
126
127                        // we need to check space group not null for the cases where the entry is crystallographic but
128                        // the space group is not a standard one recognized by biojava, e.g. 1mnk (SG: 'I 21')
129                        if (this.crystallographicInfo.isNonStandardSg()) {
130                                logger.warn("Space group is non-standard, will only calculate asymmetric unit interfaces.");
131                                this.searchBeyondAU = false;
132                        }
133
134                        // just in case we still check for space group null (a user pdb file could potentially be crystallographic and have no space group)
135                        if (this.crystallographicInfo.getSpaceGroup() == null) {
136                                logger.warn("Space group is null, will only calculate asymmetric unit interfaces.");
137                                this.searchBeyondAU = false;
138                        }
139
140                        // we need to check crystal cell not null for the rare cases where the entry is crystallographic but
141                        // the crystal cell is not given, e.g. 2i68, 2xkm, 4bpq
142                        if (this.crystallographicInfo.getCrystalCell() == null) {
143                                logger.warn("Could not find a crystal cell definition, will only calculate asymmetric unit interfaces.");
144                                this.searchBeyondAU = false;
145                        }
146
147                        // check for cases like 4hhb that are in a non-standard coordinate frame convention, see https://github.com/eppic-team/owl/issues/4
148                        if (this.crystallographicInfo.isNonStandardCoordFrameConvention()) {
149                                logger.warn("Non-standard coordinate frame convention, will only calculate asymmetric unit interfaces.");
150                                this.searchBeyondAU = false;
151                        }
152                }
153
154                if (this.searchBeyondAU) {
155                        // explore the crystal
156                        this.numOperatorsSg = this.crystallographicInfo.getSpaceGroup().getMultiplicity();
157                        this.ops = this.crystallographicInfo.getTransformationsOrthonormal();
158                } else {
159                        // look for contacts within structure as given
160                        this.numOperatorsSg = 1;
161                        this.ops = new Matrix4d[1];
162                        this.ops[0] = new Matrix4d(IDENTITY);
163                }
164
165                this.numCells = DEF_NUM_CELLS;
166
167        }
168
169
170        /**
171         * @return true if this CrystalBuilder is NCS-aware.
172         * @since 5.0.0
173         */
174        public boolean hasNcsOps() {
175                return chainNcsOps != null;
176        }
177
178        /**
179         * Set the number of neighboring crystal cells that will be used in the search for contacts
180         * @param numCells
181         */
182        public void setNumCells(int numCells) {
183                this.numCells = numCells;
184        }
185
186        private void initialiseVisited() {
187                visitedCrystalTransforms = new ArrayList<>();
188                if(this.hasNcsOps()) {
189                        visitedNcsChainPairs = new HashMap<>();
190                }
191        }
192
193        /**
194         * Returns the list of unique interfaces that the given Structure has upon
195         * generation of all crystal symmetry mates. An interface is defined as any pair of chains
196         * that contact, i.e. for which there is at least a pair of atoms (one from each chain) within
197         * the default cutoff distance.
198         * @return
199         * @see #DEFAULT_INTERFACE_DISTANCE_CUTOFF
200         */
201        public StructureInterfaceList getUniqueInterfaces() {
202                return getUniqueInterfaces(DEFAULT_INTERFACE_DISTANCE_CUTOFF);
203        }
204
205        /**
206         * Returns the list of unique interfaces that the given Structure has upon
207         * generation of all crystal symmetry mates. An interface is defined as any pair of chains
208         * that contact, i.e. for which there is at least a pair of atoms (one from each chain) within
209         * the given cutoff distance.
210         * @param cutoff the distance cutoff for 2 chains to be considered in contact
211         * @return
212         */
213        public StructureInterfaceList getUniqueInterfaces(double cutoff) {
214
215
216                StructureInterfaceList set = new StructureInterfaceList();
217
218                // certain structures in the PDB are not macromolecules (contain no polymeric chains at all), e.g. 1ao2
219                // with the current mmCIF parsing, those will be empty since purely non-polymeric chains are removed
220                // see commit e9562781f23da0ebf3547146a307d7edd5741090
221                if (numPolyChainsAu==0) {
222                        logger.warn("No chains present in the structure! No interfaces will be calculated");
223                        return set;
224                }
225
226                // pass the chainOrigNames map in NCS case so that StructureInterfaceList can deal with original to NCS chain names conversion
227                if (chainOrigNames!=null) {
228                        set.setChainOrigNamesMap(chainOrigNames);
229                }
230
231                // initialising the visited ArrayList for keeping track of symmetry redundancy
232                initialiseVisited();
233
234
235
236                // the isCrystallographic() condition covers 3 cases:
237                // a) entries with expMethod X-RAY/other diffraction and defined crystalCell (most usual case)
238                // b) entries with expMethod null but defined crystalCell (e.g. PDB file with CRYST1 record but no expMethod annotation)
239                // c) entries with expMethod not X-RAY (e.g. NMR) and defined crystalCell (NMR entries do have a dummy CRYST1 record "1 1 1 90 90 90 P1")
240                // d) isCrystallographic will be false if the structure is crystallographic but the space group was not recognized
241
242
243                calcInterfacesCrystal(set, cutoff);
244
245                return set;
246        }
247
248        /**
249         * Calculate interfaces between original asymmetric unit and neighboring
250         * whole unit cells, including the original full unit cell i.e. i=0,j=0,k=0
251         * @param set
252         * @param cutoff
253         */
254        private void calcInterfacesCrystal(StructureInterfaceList set, double cutoff) {
255
256
257                // initialising debugging vars
258                long start = -1;
259                long end = -1;
260                int trialCount = 0;
261                int skippedRedundant = 0;
262                int skippedAUsNoOverlap = 0;
263                int skippedChainsNoOverlap = 0;
264                int skippedSelfEquivalent = 0;
265
266                // The bounding boxes of all AUs of the unit cell
267                UnitCellBoundingBox bbGrid = new UnitCellBoundingBox(numOperatorsSg, numPolyChainsAu);;
268                // we calculate all the bounds of each of the asym units, those will then be reused and translated
269                bbGrid.setBbs(structure, ops, INCLUDE_HETATOMS);
270
271
272                // if not crystallographic there's no search to do in other cells, only chains within "AU" will be checked
273                if (!searchBeyondAU) numCells = 0;
274
275                boolean verbose = logger.isDebugEnabled();
276
277                if (verbose) {
278                        trialCount = 0;
279                        start= System.currentTimeMillis();
280                        int neighbors = (2*numCells+1)*(2*numCells+1)*(2*numCells+1)-1;
281                        int auTrials = (numPolyChainsAu*(numPolyChainsAu-1))/2;
282                        int trials = numPolyChainsAu*numOperatorsSg*numPolyChainsAu*neighbors;
283                        logger.debug("Chain clash trials within original AU: "+auTrials);
284                        logger.debug(
285                                        "Chain clash trials between the original AU and the neighbouring "+neighbors+
286                                        " whole unit cells ("+numCells+" neighbours)" +
287                                        "(2x"+numPolyChainsAu+"chains x "+numOperatorsSg+"AUs x "+neighbors+"cells) : "+trials);
288                        logger.debug("Total trials: "+(auTrials+trials));
289                }
290
291                List<Chain> polyChains = structure.getPolyChains();
292
293                for (int a=-numCells;a<=numCells;a++) {
294                        for (int b=-numCells;b<=numCells;b++) {
295                                for (int c=-numCells;c<=numCells;c++) {
296
297                                        Point3i trans = new Point3i(a,b,c);
298                                        Vector3d transOrth = new Vector3d(a,b,c);
299                                        if (a!=0 || b!=0 || c!=0) {
300                                                // we avoid doing the transformation for 0,0,0 (in case it's not crystallographic)
301                                                this.crystallographicInfo.getCrystalCell().transfToOrthonormal(transOrth);
302                                        }
303
304                                        UnitCellBoundingBox bbGridTrans = bbGrid.getTranslatedBbs(transOrth);
305
306                                        for (int n=0;n<numOperatorsSg;n++) {
307
308                                                // short-cut strategies
309                                                // 1) we skip first of all if the bounding boxes of the AUs don't overlap
310                                                if (!bbGrid.getAuBoundingBox(0).overlaps(bbGridTrans.getAuBoundingBox(n), cutoff)) {
311                                                        skippedAUsNoOverlap++;
312                                                        continue;
313                                                }
314
315                                                // 2) we check if we didn't already see its equivalent symmetry operator partner
316                                                CrystalTransform tt = new CrystalTransform(this.crystallographicInfo.getSpaceGroup(), n);
317                                                tt.translate(trans);
318                                                if (isRedundantTransform(tt)) {
319                                                        skippedRedundant++;
320                                                        continue;
321                                                }
322                                                addVisitedTransform(tt);
323
324
325                                                boolean selfEquivalent = false;
326
327                                                // 3) an operator can be "self redundant" if it is the inverse of itself (involutory, e.g. all pure 2-folds with no translation)
328                                                if (tt.isEquivalent(tt)) {
329                                                        logger.debug("Transform {} is equivalent to itself, will skip half of i-chains to j-chains comparisons", tt.toString());
330                                                        // in this case we can't skip the operator, but we can skip half of the matrix comparisons e.g. j>i
331                                                        // we set a flag and do that within the loop below
332                                                        selfEquivalent = true;
333                                                }
334
335                                                StringBuilder builder = null;
336                                                if (verbose) builder = new StringBuilder(String.valueOf(tt)).append(" ");
337
338                                                // Now that we know that boxes overlap and operator is not redundant, we have to go to the details
339                                                int contactsFound = 0;
340
341                                                for (int j=0;j<numPolyChainsAu;j++) {
342
343                                                        for (int i=0;i<numPolyChainsAu;i++) { // we only have to compare the original asymmetric unit to every full cell around
344
345                                                                if(selfEquivalent && (j>i)) {
346                                                                        // in case of self equivalency of the operator we can safely skip half of the matrix
347                                                                        skippedSelfEquivalent++;
348                                                                        continue;
349                                                                }
350                                                                // special case of original AU, we don't compare a chain to itself
351                                                                if (n==0 && a==0 && b==0 && c==0 && i==j) continue;
352
353                                                                // before calculating the AtomContactSet we check for overlap, then we save putting atoms into the grid
354                                                                if (!bbGrid.getChainBoundingBox(0,i).overlaps(bbGridTrans.getChainBoundingBox(n,j),cutoff)) {
355                                                                        skippedChainsNoOverlap++;
356                                                                        if (verbose) {
357                                                                                builder.append(".");
358                                                                        }
359                                                                        continue;
360                                                                }
361
362                                                                trialCount++;
363
364                                                                // finally we've gone through all short-cuts and the 2 chains seem to be close enough:
365                                                                // we do the calculation of contacts
366                                                                Chain chaini = polyChains.get(i);
367                                                                Chain chainj = polyChains.get(j);
368
369                                                                if (n!=0 || a!=0 || b!=0 || c!=0) {
370                                                                        Matrix4d mJCryst = new Matrix4d(ops[n]);
371                                                                        translate(mJCryst, transOrth);
372                                                                        chainj = (Chain)chainj.clone();
373                                                                        Calc.transform(chainj,mJCryst);
374                                                                }
375
376                                                                StructureInterface interf = calcContacts(chaini, chainj, cutoff, tt, builder);
377                                                                if (interf == null) {
378                                                                        continue;
379                                                                }
380
381                                                                contactsFound++;
382                                                                if(this.hasNcsOps()) {
383                                                                        StructureInterface interfNcsRef = findNcsRef(interf);
384                                                                        set.addNcsEquivalent(interf,interfNcsRef);
385                                                                } else {
386                                                                        set.add(interf);
387                                                                }
388                                                        }
389                                                }
390
391                                                if( verbose ) {
392                                                        if (a==0 && b==0 && c==0 && n==0)
393                                                                builder.append(" "+contactsFound+"("+(numPolyChainsAu*(numPolyChainsAu-1))/2+")");
394                                                        else if (selfEquivalent)
395                                                                builder.append(" "+contactsFound+"("+(numPolyChainsAu*(numPolyChainsAu+1))/2+")");
396                                                        else
397                                                                builder.append(" "+contactsFound+"("+numPolyChainsAu*numPolyChainsAu+")");
398
399                                                        logger.debug(builder.toString());
400                                                }
401                                        }
402                                }
403                        }
404                }
405
406                end = System.currentTimeMillis();
407                logger.debug("\n"+trialCount+" chain-chain clash trials done. Time "+(end-start)/1000+"s");
408                logger.debug("  skipped (not overlapping AUs)       : "+skippedAUsNoOverlap);
409                logger.debug("  skipped (not overlapping chains)    : "+skippedChainsNoOverlap);
410                logger.debug("  skipped (sym redundant op pairs)    : "+skippedRedundant);
411                logger.debug("  skipped (sym redundant self op)     : "+skippedSelfEquivalent);
412                logger.debug("Found "+set.size()+" interfaces.");
413        }
414
415
416        /**
417         * Checks whether given interface is NCS-redundant, i.e., an identical interface between NCS copies of
418         * these molecules has already been seen, and returns this (reference) interface.
419         *
420         * @param interf
421         *          StructureInterface
422         * @return  already seen interface that is NCS-equivalent to interf,
423         *          null if such interface is not found.
424         */
425        private StructureInterface findNcsRef(StructureInterface interf) {
426                if (!this.hasNcsOps()) {
427                        return null;
428                }
429                String chainIName = interf.getMoleculeIds().getFirst();
430                String iOrigName = chainOrigNames.get(chainIName);
431
432                String chainJName = interf.getMoleculeIds().getSecond();
433                String jOrigName = chainOrigNames.get(chainJName);
434
435                Matrix4d mJCryst;
436                if(this.searchBeyondAU) {
437                        mJCryst = interf.getTransforms().getSecond().getMatTransform();
438                        mJCryst = crystallographicInfo.getCrystalCell().transfToOrthonormal(mJCryst);
439                } else {
440                        mJCryst = IDENTITY;
441                }
442
443                // Let X1,...Xn be the original coords, before NCS transforms (M1...Mk)
444                // current chain i: M_i * X_i
445                // current chain j: Cn * M_j * X_j
446
447                // transformation to bring chain j near X_i: M_i^(-1) * Cn * M_j
448                // transformation to bring chain i near X_j: (Cn * M_j)^(-1) * M_i = (M_i^(-1) * Cn * M_j)^(-1)
449
450                Matrix4d mChainIInv = new Matrix4d(chainNcsOps.get(chainIName));
451                mChainIInv.invert();
452
453                Matrix4d mJNcs = new Matrix4d(chainNcsOps.get(chainJName));
454
455                Matrix4d j2iNcsOrigin = new Matrix4d(mChainIInv);
456                j2iNcsOrigin.mul(mJCryst);
457                //overall transformation to bring current chainj from its NCS origin to i's
458                j2iNcsOrigin.mul(mJNcs);
459
460                //overall transformation to bring current chaini from its NCS origin to j's
461                Matrix4d i2jNcsOrigin = new Matrix4d(j2iNcsOrigin);
462                i2jNcsOrigin.invert();
463
464                String matchChainIdsIJ = iOrigName + jOrigName;
465                String matchChainIdsJI = jOrigName + iOrigName;
466
467                // same original chain names
468                Optional<Matrix4d> matchDirect =
469                                visitedNcsChainPairs.computeIfAbsent(matchChainIdsIJ, k-> new HashMap<>()).entrySet().stream().
470                                        map(r->r.getKey()).
471                                        filter(r->r.epsilonEquals(j2iNcsOrigin,0.01)).
472                                        findFirst();
473
474                Matrix4d matchMatrix = matchDirect.orElse(null);
475                String matchChainIds = matchChainIdsIJ;
476
477                if(matchMatrix == null) {
478                        // reversed original chain names with inverted transform
479                        Optional<Matrix4d> matchInverse =
480                                        visitedNcsChainPairs.computeIfAbsent(matchChainIdsJI, k-> new HashMap<>()).entrySet().stream().
481                                        map(r->r.getKey()).
482                                        filter(r->r.epsilonEquals(i2jNcsOrigin,0.01)).
483                                        findFirst();
484                        matchMatrix = matchInverse.orElse(null);
485                        matchChainIds = matchChainIdsJI;
486                }
487
488                StructureInterface matchInterface = null;
489
490                if (matchMatrix == null) {
491                        visitedNcsChainPairs.get(matchChainIdsIJ).put(j2iNcsOrigin,interf);
492                } else {
493                        matchInterface = visitedNcsChainPairs.get(matchChainIds).get(matchMatrix);
494                }
495
496                return matchInterface;
497        }
498
499        private StructureInterface calcContacts(Chain chaini, Chain chainj, double cutoff, CrystalTransform tt, StringBuilder builder) {
500                // note that we don't consider hydrogens when calculating contacts
501                AtomContactSet graph = StructureTools.getAtomsInContact(chaini, chainj, cutoff, INCLUDE_HETATOMS);
502
503                if (graph.size()>0) {
504                        if (builder != null) builder.append("x");
505
506                        CrystalTransform transf = new CrystalTransform(this.crystallographicInfo.getSpaceGroup());
507                        StructureInterface interf = new StructureInterface(
508                                        StructureTools.getAllAtomArray(chaini), StructureTools.getAllAtomArray(chainj),
509                                        chaini.getName(), chainj.getName(),
510                                        graph,
511                                        transf, tt);
512
513                        return interf;
514
515                } else {
516                        if (builder != null) builder.append("o");
517                        return null;
518                }
519        }
520
521        private void addVisitedTransform(CrystalTransform tt) {
522                visitedCrystalTransforms.add(tt);
523        }
524
525        /**
526         * Checks whether given transformId/translation is symmetry redundant
527         * Two transformations are symmetry redundant if their matrices (4d) multiplication gives the identity, i.e.
528         * if one is the inverse of the other.
529         * @param tt
530         * @return
531         */
532        private boolean isRedundantTransform(CrystalTransform tt) {
533
534                Iterator<CrystalTransform> it = visitedCrystalTransforms.iterator();
535                while (it.hasNext()) {
536                        CrystalTransform v = it.next();
537
538                        if (tt.isEquivalent(v)) {
539
540                                logger.debug("Skipping redundant transformation: "+tt+", equivalent to "+v);
541
542                                // there's only 1 possible equivalent partner for each visited matrix
543                                // (since the equivalent is its inverse matrix and the inverse matrix is unique)
544                                // thus once the partner has been seen, we don't need to check it ever again
545                                it.remove();
546
547                                return true;
548                        }
549                }
550
551                return false;
552        }
553
554        public void translate(Matrix4d m, Vector3d translation) {
555                m.m03 = m.m03+translation.x;
556                m.m13 = m.m13+translation.y;
557                m.m23 = m.m23+translation.z;
558        }
559
560        /**
561         * Apply the NCS operators in the given Structure adding new chains as needed.
562         * All chains are (re)assigned ids of the form: original_chain_id+ncs_operator_index+{@value #NCS_CHAINID_SUFFIX_CHAR}.
563         * @param structure
564         *          the structure to expand
565         * @param chainOrigNames
566         *          new chain names mapped to the original chain names
567         * @param chainNcsOps
568         *          new chain names mapped to the ncs operators that was used to generate them
569         * @since 5.0.0
570         */
571        public static void expandNcsOps(Structure structure, Map<String,String> chainOrigNames, Map<String,Matrix4d> chainNcsOps) {
572                PDBCrystallographicInfo xtalInfo = structure.getCrystallographicInfo();
573                if (xtalInfo ==null) return;
574
575                if (xtalInfo.getNcsOperators()==null || xtalInfo.getNcsOperators().length==0)
576                        return;
577
578                List<Chain> chainsToAdd = new ArrayList<>();
579
580                Matrix4d identity = new Matrix4d();
581                identity.setIdentity();
582
583                Matrix4d[] ncsOps = xtalInfo.getNcsOperators();
584
585                for (Chain c:structure.getChains()) {
586                        String cOrigId = c.getId();
587                        String cOrigName = c.getName();
588
589                        for (int iOperator = 0; iOperator < ncsOps.length; iOperator++) {
590                                Matrix4d m = ncsOps[iOperator];
591
592                                Chain clonedChain = (Chain)c.clone();
593                                String newChainId = cOrigId+(iOperator+1)+NCS_CHAINID_SUFFIX_CHAR;
594                                String newChainName = cOrigName+(iOperator+1)+NCS_CHAINID_SUFFIX_CHAR;
595                                clonedChain.setId(newChainId);
596                                clonedChain.setName(newChainName);
597
598                                setChainIdsInResidueNumbers(clonedChain, newChainName);
599                                Calc.transform(clonedChain, m);
600
601                                chainsToAdd.add(clonedChain);
602                                c.getEntityInfo().addChain(clonedChain);
603
604                                chainOrigNames.put(newChainName,cOrigName);
605                                chainNcsOps.put(newChainName,m);
606                        }
607
608                        chainNcsOps.put(cOrigName,identity);
609                        chainOrigNames.put(cOrigName,cOrigName);
610                }
611
612                chainsToAdd.forEach(structure::addChain);
613        }
614
615        /**
616         * Auxiliary method to reset chain ids of residue numbers in a chain.
617         * Used when cloning chains and resetting their ids: one needs to take care of
618         * resetting the ids within residue numbers too.
619         * @param c
620         * @param newChainName
621         */
622        private static void setChainIdsInResidueNumbers(Chain c, String newChainName) {
623                for (Group g:c.getAtomGroups()) {
624                        g.setResidueNumber(newChainName, g.getResidueNumber().getSeqNum(), g.getResidueNumber().getInsCode());
625                }
626                for (Group g:c.getSeqResGroups()) {
627                        if (g.getResidueNumber()==null) continue;
628                        g.setResidueNumber(newChainName, g.getResidueNumber().getSeqNum(), g.getResidueNumber().getInsCode());
629                }
630        }
631
632}