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.contact;
022
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.List;
026import java.util.Map;
027import java.util.TreeMap;
028
029import org.biojava.nbio.structure.Atom;
030import org.biojava.nbio.structure.Chain;
031import org.biojava.nbio.structure.Element;
032import org.biojava.nbio.structure.EntityInfo;
033import org.biojava.nbio.structure.Group;
034import org.biojava.nbio.structure.GroupType;
035import org.biojava.nbio.structure.ResidueNumber;
036import org.biojava.nbio.structure.Structure;
037import org.biojava.nbio.structure.asa.AsaCalculator;
038import org.biojava.nbio.structure.asa.GroupAsa;
039import org.biojava.nbio.structure.io.FileConvert;
040import org.biojava.nbio.structure.io.FileParsingParameters;
041import org.biojava.nbio.structure.io.mmcif.MMCIFFileTools;
042import org.biojava.nbio.structure.io.mmcif.SimpleMMcifParser;
043import org.biojava.nbio.structure.io.mmcif.chem.PolymerType;
044import org.biojava.nbio.structure.io.mmcif.model.AtomSite;
045import org.biojava.nbio.structure.io.mmcif.model.ChemComp;
046import org.biojava.nbio.structure.xtal.CrystalTransform;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050
051/**
052 * An interface between 2 molecules (2 sets of atoms).
053 *
054 * @author duarte_j
055 *
056 */
057public class StructureInterface implements Serializable, Comparable<StructureInterface> {
058
059        private static final long serialVersionUID = 1L;
060
061        private static final Logger logger = LoggerFactory.getLogger(StructureInterface.class);
062
063        /**
064         * Interfaces with larger inverse self contact overlap score will be considered isologous
065         */
066        private static final double SELF_SCORE_FOR_ISOLOGOUS = 0.3;
067
068        private int id;
069        private double totalArea;
070        private AtomContactSet contacts;
071        private GroupContactSet groupContacts;
072
073        private Pair<Atom[]> molecules;
074
075        /**
076         * The identifier for each of the atom arrays (usually a chain identifier, i.e. a single capital letter)
077         * Serves to identify the molecules within the Asymmetric Unit of the crystal
078         */
079        private Pair<String> moleculeIds;
080
081        /**
082         * The transformations (crystal operators) applied to each molecule (if applicable)
083         */
084        private Pair<CrystalTransform> transforms;
085
086        private Map<ResidueNumber, GroupAsa> groupAsas1;
087        private Map<ResidueNumber, GroupAsa> groupAsas2;
088
089        private StructureInterfaceCluster cluster;
090
091        /**
092         * Constructs a StructureInterface
093         * @param firstMolecule the atoms of the first molecule
094         * @param secondMolecule the atoms of the second molecule
095         * @param firstMoleculeId an identifier that identifies the first molecule within the Asymmetric Unit
096         * @param secondMoleculeId an identifier that identifies the second molecule within the Asymmetric Unit
097         * @param contacts the contacts between the 2 molecules
098         * @param firstTransf the transformation (crystal operator) applied to first molecule
099         * @param secondTransf the transformation (crystal operator) applied to second molecule
100         */
101        public StructureInterface(
102                        Atom[] firstMolecule, Atom[] secondMolecule,
103                        String firstMoleculeId, String secondMoleculeId,
104                        AtomContactSet contacts,
105                        CrystalTransform firstTransf, CrystalTransform secondTransf) {
106
107                this.molecules = new Pair<>(firstMolecule, secondMolecule);
108                this.moleculeIds = new Pair<>(firstMoleculeId,secondMoleculeId);
109                this.contacts = contacts;
110                this.transforms = new Pair<>(firstTransf, secondTransf);
111        }
112
113        /**
114         * Constructs an empty StructureInterface
115         */
116        public StructureInterface() {
117                this.groupAsas1 = new TreeMap<>();
118                this.groupAsas2 = new TreeMap<>();
119        }
120
121        public int getId() {
122                return id;
123        }
124
125        public void setId(int id) {
126                this.id = id;
127        }
128
129        /**
130         * Returns a pair of identifiers for each of the 2 member molecules that
131         * identify them uniquely in the crystal:
132         *   &lt;molecule id (asym unit id)&gt;+&lt;operator id&gt;+&lt;crystal translation&gt;
133         * @return
134         */
135        public Pair<String> getCrystalIds() {
136                return new Pair<>(
137                        moleculeIds.getFirst()+transforms.getFirst().getTransformId()+transforms.getFirst().getCrystalTranslation(),
138                        moleculeIds.getSecond()+transforms.getSecond().getTransformId()+transforms.getSecond().getCrystalTranslation());
139        }
140
141        /**
142         * Returns the total area buried upon formation of this interface,
143         * defined as: 1/2[ (ASA1u-ASA1c) + (ASA2u-ASA2u) ] , with:
144         *  <p>ASAxu = ASA of first/second unbound chain</p>
145         *  <p>ASAxc = ASA of first/second complexed chain</p>
146         * In the area calculation HETATOM groups not part of the main protein/nucleotide chain
147         * are not included.
148         * @return
149         */
150        public double getTotalArea() {
151                return totalArea;
152        }
153
154        public void setTotalArea(double totalArea) {
155                this.totalArea = totalArea;
156        }
157
158        public AtomContactSet getContacts() {
159                return contacts;
160        }
161
162        public void setContacts(AtomContactSet contacts) {
163                this.contacts = contacts;
164        }
165
166        public Pair<Atom[]> getMolecules() {
167                return molecules;
168        }
169
170        public void setMolecules(Pair<Atom[]> molecules) {
171                this.molecules = molecules;
172        }
173
174        /**
175         * Return the pair of identifiers identifying each of the 2 molecules of this interface
176         * in the asymmetry unit (usually the chain identifier if this interface is between 2 chains)
177         * @return
178         */
179        public Pair<String> getMoleculeIds() {
180                return moleculeIds;
181        }
182
183        public void setMoleculeIds(Pair<String> moleculeIds) {
184                this.moleculeIds = moleculeIds;
185        }
186
187        /**
188         * Return the 2 crystal transform operations performed on each of the
189         * molecules of this interface.
190         * @return
191         */
192        public Pair<CrystalTransform> getTransforms() {
193                return transforms;
194        }
195
196        public void setTransforms(Pair<CrystalTransform> transforms) {
197                this.transforms = transforms;
198        }
199
200        /**
201         * Set ASA annotations by passing the uncomplexed ASA values of the 2 partners.
202         * This will calculate complexed ASA and set the ASA values in the member variables.
203         * @param asas1 ASA values for atoms of partner 1
204         * @param asas2 ASA values for atoms of partner 2
205         * @param nSpherePoints the number of sphere points to be used for complexed ASA calculation
206         * @param nThreads the number of threads to be used for complexed ASA calculation
207         * @param cofactorSizeToUse the minimum size of cofactor molecule (non-chain HET atoms) that will be used in ASA calculation
208         */
209        void setAsas(double[] asas1, double[] asas2, int nSpherePoints, int nThreads, int cofactorSizeToUse) {
210
211                Atom[] atoms = getAtomsForAsa(cofactorSizeToUse);
212                AsaCalculator asaCalc = new AsaCalculator(atoms,
213                                AsaCalculator.DEFAULT_PROBE_SIZE, nSpherePoints, nThreads);
214
215                double[] complexAsas = asaCalc.calculateAsas();
216
217                if (complexAsas.length!=asas1.length+asas2.length)
218                        throw new IllegalArgumentException("The size of ASAs of complex doesn't match that of ASAs 1 + ASAs 2");
219
220
221                groupAsas1 = new TreeMap<>();
222                groupAsas2 = new TreeMap<>();
223
224                this.totalArea = 0;
225
226                for (int i=0;i<asas1.length;i++) {
227                        Group g = atoms[i].getGroup();
228
229                        if (!g.getType().equals(GroupType.HETATM) ||
230                                isInChain(g)) {
231                                // interface area should be only for protein/nucleotide but not hetatoms that are not part of the chain
232                                this.totalArea += (asas1[i] - complexAsas[i]);
233                        }
234
235                        if (!groupAsas1.containsKey(g.getResidueNumber())) {
236                                GroupAsa groupAsa = new GroupAsa(g);
237                                groupAsa.addAtomAsaU(asas1[i]);
238                                groupAsa.addAtomAsaC(complexAsas[i]);
239                                groupAsas1.put(g.getResidueNumber(), groupAsa);
240                        } else {
241                                GroupAsa groupAsa = groupAsas1.get(g.getResidueNumber());
242                                groupAsa.addAtomAsaU(asas1[i]);
243                                groupAsa.addAtomAsaC(complexAsas[i]);
244                        }
245                }
246
247                for (int i=0;i<asas2.length;i++) {
248                        Group g = atoms[i+asas1.length].getGroup();
249
250                        if (!g.getType().equals(GroupType.HETATM) ||
251                                isInChain(g)) {
252                                // interface area should be only for protein/nucleotide but not hetatoms that are not part of the chain
253                                this.totalArea += (asas2[i] - complexAsas[i+asas1.length]);
254                        }
255
256                        if (!groupAsas2.containsKey(g.getResidueNumber())) {
257                                GroupAsa groupAsa = new GroupAsa(g);
258                                groupAsa.addAtomAsaU(asas2[i]);
259                                groupAsa.addAtomAsaC(complexAsas[i+asas1.length]);
260                                groupAsas2.put(g.getResidueNumber(), groupAsa);
261                        } else {
262                                GroupAsa groupAsa = groupAsas2.get(g.getResidueNumber());
263                                groupAsa.addAtomAsaU(asas2[i]);
264                                groupAsa.addAtomAsaC(complexAsas[i+asas1.length]);
265                        }
266                }
267
268                // our interface area definition: average of bsa of both molecules
269                this.totalArea = this.totalArea/2.0;
270
271        }
272
273        protected Atom[] getFirstAtomsForAsa(int cofactorSizeToUse) {
274
275                return getAllNonHAtomArray(molecules.getFirst(), cofactorSizeToUse);
276        }
277
278        protected Atom[] getSecondAtomsForAsa(int cofactorSizeToUse) {
279
280                return getAllNonHAtomArray(molecules.getSecond(), cofactorSizeToUse);
281        }
282
283        protected Atom[] getAtomsForAsa(int cofactorSizeToUse) {
284                Atom[] atoms1 = getFirstAtomsForAsa(cofactorSizeToUse);
285                Atom[] atoms2 = getSecondAtomsForAsa(cofactorSizeToUse);
286
287                Atom[] atoms = new Atom[atoms1.length+atoms2.length];
288                for (int i=0;i<atoms1.length;i++) {
289                        atoms[i] = atoms1[i];
290                }
291                for (int i=0;i<atoms2.length;i++) {
292                        atoms[i+atoms1.length] = atoms2[i];
293                }
294
295                return atoms;
296        }
297
298        /**
299         * Returns and array of all non-Hydrogen atoms in the given molecule, including all
300         * main chain HETATOM groups. Non main-chain HETATOM groups with fewer than minSizeHetAtomToInclude
301         * non-Hydrogen atoms are not included.
302         * @param m
303         * @param minSizeHetAtomToInclude HETATOM groups (non main-chain) with fewer number of
304         * non-Hydrogen atoms are not included
305         * @return
306         */
307        private static final Atom[] getAllNonHAtomArray(Atom[] m, int minSizeHetAtomToInclude) {
308                List<Atom> atoms = new ArrayList<>();
309
310                for (Atom a:m){
311
312                        if (a.getElement()==Element.H) continue;
313
314                        Group g = a.getGroup();
315                        if (g.getType().equals(GroupType.HETATM) &&
316                                !isInChain(g) &&
317                                getSizeNoH(g)<minSizeHetAtomToInclude) {
318                                continue;
319                        }
320
321                        atoms.add(a);
322
323                }
324                return atoms.toArray(new Atom[atoms.size()]);
325        }
326
327        /**
328         * Calculates the number of non-Hydrogen atoms in the given group
329         * @param g
330         * @return
331         */
332        private static int getSizeNoH(Group g) {
333                int size = 0;
334                for (Atom a:g.getAtoms()) {
335                        if (a.getElement()!=Element.H)
336                                size++;
337                }
338                return size;
339        }
340
341        /**
342         * Returns true if the given group is part of the main chain, i.e. if it is
343         * a peptide-linked group or a nucleotide
344         * @param g
345         * @return
346         */
347        private static boolean isInChain(Group g) {
348                ChemComp chemComp = g.getChemComp();
349
350                if (chemComp==null) {
351                        logger.warn("Warning: can't determine PolymerType for group "+g.getResidueNumber()+" ("+g.getPDBName()+"). Will consider it as non-nucleotide/non-protein type.");
352                        return false;
353                }
354
355                PolymerType polyType = chemComp.getPolymerType();
356                for (PolymerType protOnlyType: PolymerType.PROTEIN_ONLY) {
357                        if (polyType==protOnlyType) return true;
358                }
359                for (PolymerType protOnlyType: PolymerType.POLYNUCLEOTIDE_ONLY) {
360                        if (polyType==protOnlyType) return true;
361                }
362
363                return false;
364        }
365
366        /**
367         * Tells whether the interface corresponds to one mediated by crystallographic symmetry,
368         * i.e. it is between symmetry-related molecules (with same chain identifier)
369         * @return
370         */
371        public boolean isSymRelated() {
372                return moleculeIds.getFirst().equals(moleculeIds.getSecond());
373        }
374
375        /**
376         * Returns true if the transformation applied to the second molecule of this interface
377         * has an infinite character (pure translation or screw rotation)
378         * and both molecules of the interface have the same asymmetric unit identifier (chain id): in such cases the
379         * interface would lead to infinite fiber-like (linear or helical) assemblies
380         * @return
381         */
382        public boolean isInfinite() {
383                return ((isSymRelated() && transforms.getSecond().getTransformType().isInfinite()));
384        }
385
386        /**
387         * Returns true if the 2 molecules of this interface are the same entity (i.e. homomeric interface), false
388         * otherwise (i.e. heteromeric interface)
389         * @return true if homomeric or if either of the entities is unknonw (null Compounds), false otherwise
390         */
391        public boolean isHomomeric() {
392                EntityInfo first = getParentChains().getFirst().getEntityInfo();
393                EntityInfo second = getParentChains().getSecond().getEntityInfo();
394                if (first==null || second==null) {
395                        logger.warn("Some compound of interface {} is null, can't determine whether it is homo/heteromeric. Consider it homomeric", getId());
396                        return true;
397                }
398                return
399                        first.getRepresentative().getId().equals(second.getRepresentative().getId());
400        }
401
402        /**
403         * Gets a map of ResidueNumbers to GroupAsas for all groups of first chain.
404         * @return
405         */
406        public Map<ResidueNumber, GroupAsa> getFirstGroupAsas() {
407                return groupAsas1;
408        }
409
410        /**
411         * Gets the GroupAsa for the corresponding residue number of first chain
412         * @param resNum
413         * @return
414         */
415        public GroupAsa getFirstGroupAsa(ResidueNumber resNum) {
416                return groupAsas1.get(resNum);
417        }
418
419        public void setFirstGroupAsa(GroupAsa groupAsa) {
420                groupAsas1.put(groupAsa.getGroup().getResidueNumber(), groupAsa);
421        }
422
423        void setFirstGroupAsas(Map<ResidueNumber, GroupAsa> firstGroupAsas) {
424                this.groupAsas1 = firstGroupAsas;
425        }
426
427        void setSecondGroupAsas(Map<ResidueNumber, GroupAsa> secondGroupAsas) {
428                this.groupAsas2 = secondGroupAsas;
429        }
430
431        /**
432         * Gets a map of ResidueNumbers to GroupAsas for all groups of second chain.
433         * @return
434         */
435        public Map<ResidueNumber, GroupAsa> getSecondGroupAsas() {
436                return groupAsas2;
437        }
438
439        public void setSecondGroupAsa(GroupAsa groupAsa) {
440                groupAsas2.put(groupAsa.getGroup().getResidueNumber(), groupAsa);
441        }
442
443        /**
444         * Gets the GroupAsa for the corresponding residue number of second chain
445         * @param resNum
446         * @return
447         */
448        public GroupAsa getSecondGroupAsa(ResidueNumber resNum) {
449                return groupAsas2.get(resNum);
450        }
451
452        /**
453         * Returns the residues belonging to the interface core, defined as those residues at
454         * the interface (BSA>0) and for which the BSA/ASA ratio is above the given bsaToAsaCutoff
455         * @param bsaToAsaCutoff
456         * @param minAsaForSurface the minimum ASA to consider a residue on the surface
457         * @return
458         */
459        public Pair<List<Group>> getCoreResidues(double bsaToAsaCutoff, double minAsaForSurface) {
460
461                List<Group> core1 = new ArrayList<Group>();
462                List<Group> core2 = new ArrayList<Group>();
463
464                for (GroupAsa groupAsa:groupAsas1.values()) {
465
466                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
467                                if (groupAsa.getBsaToAsaRatio()<bsaToAsaCutoff) {
468                                        //rim1.add(groupAsa.getGroup());
469                                } else {
470                                        core1.add(groupAsa.getGroup());
471                                }
472                        }
473                }
474                for (GroupAsa groupAsa:groupAsas2.values()) {
475
476                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
477                                if (groupAsa.getBsaToAsaRatio()<bsaToAsaCutoff) {
478                                        //rim2.add(groupAsa.getGroup());
479                                } else {
480                                        core2.add(groupAsa.getGroup());
481                                }
482                        }
483                }
484
485                return new Pair<List<Group>>(core1, core2);
486        }
487
488        /**
489         * Returns the residues belonging to the interface rim, defined as those residues at
490         * the interface (BSA>0) and for which the BSA/ASA ratio is below the given bsaToAsaCutoff
491         * @param bsaToAsaCutoff
492         * @param minAsaForSurface the minimum ASA to consider a residue on the surface
493         * @return
494         */
495        public Pair<List<Group>> getRimResidues(double bsaToAsaCutoff, double minAsaForSurface) {
496
497                List<Group> rim1 = new ArrayList<Group>();
498                List<Group> rim2 = new ArrayList<Group>();
499
500                for (GroupAsa groupAsa:groupAsas1.values()) {
501
502                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
503                                if (groupAsa.getBsaToAsaRatio()<bsaToAsaCutoff) {
504                                        rim1.add(groupAsa.getGroup());
505                                } else {
506                                        //core1.add(groupAsa.getGroup());
507                                }
508                        }
509                }
510                for (GroupAsa groupAsa:groupAsas2.values()) {
511
512                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
513                                if (groupAsa.getBsaToAsaRatio()<bsaToAsaCutoff) {
514                                        rim2.add(groupAsa.getGroup());
515                                } else {
516                                        //core2.add(groupAsa.getGroup());
517                                }
518                        }
519                }
520
521                return new Pair<List<Group>>(rim1, rim2);
522        }
523
524        /**
525         * Returns the residues belonging to the interface, i.e. the residues
526         * at the surface with BSA>0
527         * @param minAsaForSurface the minimum ASA to consider a residue on the surface
528         * @return
529         */
530        public Pair<List<Group>> getInterfacingResidues(double minAsaForSurface) {
531
532                List<Group> interf1 = new ArrayList<Group>();
533                List<Group> interf2 = new ArrayList<Group>();
534
535                for (GroupAsa groupAsa:groupAsas1.values()) {
536
537                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
538                                interf1.add(groupAsa.getGroup());
539                        }
540                }
541                for (GroupAsa groupAsa:groupAsas2.values()) {
542
543                        if (groupAsa.getAsaU()>minAsaForSurface && groupAsa.getBsa()>0) {
544                                interf2.add(groupAsa.getGroup());
545                        }
546                }
547
548                return new Pair<List<Group>>(interf1, interf2);
549        }
550
551        /**
552         * Returns the residues belonging to the surface
553         * @param minAsaForSurface the minimum ASA to consider a residue on the surface
554         * @return
555         */
556        public Pair<List<Group>> getSurfaceResidues(double minAsaForSurface) {
557                List<Group> surf1 = new ArrayList<Group>();
558                List<Group> surf2 = new ArrayList<Group>();
559
560                for (GroupAsa groupAsa:groupAsas1.values()) {
561
562                        if (groupAsa.getAsaU()>minAsaForSurface) {
563                                surf1.add(groupAsa.getGroup());
564                        }
565                }
566                for (GroupAsa groupAsa:groupAsas2.values()) {
567
568                        if (groupAsa.getAsaU()>minAsaForSurface) {
569                                surf2.add(groupAsa.getGroup());
570                        }
571                }
572
573                return new Pair<List<Group>>(surf1, surf2);
574        }
575
576        public StructureInterfaceCluster getCluster() {
577                return cluster;
578        }
579
580        public void setCluster(StructureInterfaceCluster cluster) {
581                this.cluster = cluster;
582        }
583
584        /**
585         * Calculates the contact overlap score between this StructureInterface and
586         * the given one.
587         * The two sides of the given StructureInterface need to match this StructureInterface
588         * in the sense that they must come from the same Compound (Entity), i.e.
589         * their residue numbers need to align with 100% identity, except for unobserved
590         * density residues. The SEQRES indices obtained through {@link EntityInfo#getAlignedResIndex(Group, Chain)} are
591         * used to match residues, thus if no SEQRES is present or if {@link FileParsingParameters#setAlignSeqRes(boolean)}
592         * is not used, this calculation is not guaranteed to work properly.
593         * @param other
594         * @param invert if false the comparison will be done first-to-first and second-to-second,
595         * if true the match will be first-to-second and second-to-first
596         * @return the contact overlap score, range [0.0,1.0]
597         */
598        public double getContactOverlapScore(StructureInterface other, boolean invert) {
599
600                Structure thisStruct = getParentStructure();
601                Structure otherStruct = other.getParentStructure();
602
603                if (thisStruct!=otherStruct) {
604                        // in the current implementation, comparison between different structure doesn't make much sense
605                        // and won't even work since the compounds of both will never match. We warn because it
606                        // really is not what this is intended for at the moment
607                        logger.warn("Comparing interfaces from different structures, contact overlap score will be 0");
608                        return 0;
609                }
610
611                Pair<Chain> thisChains = getParentChains();
612                Pair<Chain> otherChains = other.getParentChains();
613
614                if (thisChains.getFirst().getEntityInfo() == null || thisChains.getSecond().getEntityInfo() == null ||
615                        otherChains.getFirst().getEntityInfo() == null || otherChains.getSecond().getEntityInfo() == null ) {
616                        // this happens in cases like 2uub
617                        logger.warn("Found chains with null compounds while comparing interfaces {} and {}. Contact overlap score for them will be 0.",
618                                        this.getId(), other.getId());
619                        return 0;
620                }
621
622                Pair<EntityInfo> thisCompounds = new Pair<EntityInfo>(thisChains.getFirst().getEntityInfo(), thisChains.getSecond().getEntityInfo());
623                Pair<EntityInfo> otherCompounds = new Pair<EntityInfo>(otherChains.getFirst().getEntityInfo(), otherChains.getSecond().getEntityInfo());
624
625                if ( (  (thisCompounds.getFirst() == otherCompounds.getFirst()) &&
626                                (thisCompounds.getSecond() == otherCompounds.getSecond())   )  ||
627                         (  (thisCompounds.getFirst() == otherCompounds.getSecond()) &&
628                                (thisCompounds.getSecond() == otherCompounds.getFirst())   )    ) {
629
630                        int common = 0;
631                        GroupContactSet thisContacts = getGroupContacts();
632                        GroupContactSet otherContacts = other.getGroupContacts();
633
634                        for (GroupContact thisContact:thisContacts) {
635
636                                ResidueIdentifier first = null;
637                                ResidueIdentifier second = null;
638
639                                if (!invert) {
640                                        first = new ResidueIdentifier(thisContact.getPair().getFirst());
641
642                                        second = new ResidueIdentifier(thisContact.getPair().getSecond());
643                                } else {
644                                        first = new ResidueIdentifier(thisContact.getPair().getSecond());
645
646                                        second = new ResidueIdentifier(thisContact.getPair().getFirst());
647                                }
648
649                                if (otherContacts.hasContact(first,second)) {
650                                        common++;
651                                }
652                        }
653                        return (2.0*common)/(thisContacts.size()+otherContacts.size());
654                } else {
655                        logger.debug("Chain pairs {},{} and {},{} belong to different compound pairs, contact overlap score will be 0 ",
656                                        thisChains.getFirst().getId(),thisChains.getSecond().getId(),
657                                        otherChains.getFirst().getId(),otherChains.getSecond().getId());
658                        return 0.0;
659                }
660        }
661
662        public GroupContactSet getGroupContacts() {
663                if (groupContacts==null) {
664                        this.groupContacts  = new GroupContactSet(contacts);
665                }
666                return this.groupContacts;
667        }
668
669        /**
670         * Tell whether the interface is isologous, i.e. it is formed
671         * by the same patches of same Compound on both sides.
672         *
673         * @return true if isologous, false if heterologous
674         */
675        public boolean isIsologous() {
676                double scoreInverse = this.getContactOverlapScore(this, true);
677                logger.debug("Interface {} contact overlap score with itself inverted: {}",
678                                getId(), scoreInverse);
679                return (scoreInverse>SELF_SCORE_FOR_ISOLOGOUS);
680        }
681
682        /**
683         * Finds the parent chains by looking up the references of first atom of each side of this interface
684         * @return
685         */
686        public Pair<Chain> getParentChains() {
687                Atom[] firstMol = this.molecules.getFirst();
688                Atom[] secondMol = this.molecules.getSecond();
689                if (firstMol.length==0 || secondMol.length==0) {
690                        logger.warn("No atoms found in first or second molecule, can't get parent Chains");
691                        return null;
692                }
693
694                return new Pair<Chain>(firstMol[0].getGroup().getChain(), secondMol[0].getGroup().getChain());
695        }
696
697        /**
698         * Finds the parent compounds by looking up the references of first atom of each side of this interface
699         * @return
700         */
701        public Pair<EntityInfo> getParentCompounds() {
702                Pair<Chain> chains = getParentChains();
703                if (chains == null) {
704                        logger.warn("Could not find parents chains, compounds will be null");
705                        return null;
706                }
707                return new Pair<EntityInfo>(chains.getFirst().getEntityInfo(), chains.getSecond().getEntityInfo());
708        }
709
710        private Structure getParentStructure() {
711                Atom[] firstMol = this.molecules.getFirst();
712                if (firstMol.length==0) {
713                        logger.warn("No atoms found in first molecule, can't get parent Structure");
714                        return null;
715                }
716                return firstMol[0].getGroup().getChain().getStructure();
717        }
718
719        /**
720         * Return a String representing the 2 molecules of this interface in PDB format.
721         * If the molecule ids (i.e. chain ids) are the same for both molecules, then the second
722         * one will be replaced by the next letter in alphabet (or A for Z)
723         * @return
724         */
725        public String toPDB() {
726
727                String molecId1 = getMoleculeIds().getFirst();
728                String molecId2 = getMoleculeIds().getSecond();
729
730                if (molecId2.equals(molecId1)) {
731                        // if both chains are named equally we want to still named them differently in the output pdb file
732                        // so that molecular viewers can handle properly the 2 chains as separate entities
733                        char letter = molecId1.charAt(0);
734                        if (letter!='Z' && letter!='z') {
735                                molecId2 = Character.toString((char)(letter+1)); // i.e. next letter in alphabet
736                        } else {
737                                molecId2 = Character.toString((char)(letter-25)); //i.e. 'A' or 'a'
738                        }
739                }
740
741                StringBuilder sb = new StringBuilder();
742                for (Atom atom:this.molecules.getFirst()) {
743                        sb.append(FileConvert.toPDB(atom, molecId1));
744                }
745                sb.append("TER");
746                sb.append(System.getProperty("line.separator"));
747                for (Atom atom:this.molecules.getSecond()) {
748                        sb.append(FileConvert.toPDB(atom,molecId2));
749                }
750                sb.append("TER");
751                sb.append(System.getProperty("line.separator"));
752                sb.append("END");
753                sb.append(System.getProperty("line.separator"));
754                return sb.toString();
755        }
756
757        /**
758         * Return a String representing the 2 molecules of this interface in mmCIF format.
759         * If the molecule ids (i.e. chain ids) are the same for both molecules, then the second
760         * one will be written as chainId_operatorId (with operatorId taken from {@link #getTransforms()}
761         * @return
762         */
763        public String toMMCIF() {
764                StringBuilder sb = new StringBuilder();
765
766                String molecId1 = getMoleculeIds().getFirst();
767                String molecId2 = getMoleculeIds().getSecond();
768
769                if (isSymRelated()) {
770                        // if both chains are named equally we want to still named them differently in the output mmcif file
771                        // so that molecular viewers can handle properly the 2 chains as separate entities
772                        molecId2 = molecId2 + "_" +getTransforms().getSecond().getTransformId();
773                }
774
775                sb.append(SimpleMMcifParser.MMCIF_TOP_HEADER).append("BioJava_interface_").append(getId()).append(System.getProperty("line.separator"));
776
777                sb.append(FileConvert.getAtomSiteHeader());
778
779                // we reassign atom ids if sym related (otherwise atom ids would be duplicated and some molecular viewers can't cope with that)
780                int atomId = 1;
781                List<AtomSite> atomSites = new ArrayList<>();
782                for (Atom atom:this.molecules.getFirst()) {
783                        if (isSymRelated()) {
784                                atomSites.add(MMCIFFileTools.convertAtomToAtomSite(atom, 1, molecId1, molecId1, atomId));
785                        } else {
786                                atomSites.add(MMCIFFileTools.convertAtomToAtomSite(atom, 1, molecId1, molecId1));
787                        }
788                        atomId++;
789                }
790                for (Atom atom:this.molecules.getSecond()) {
791                        if (isSymRelated()) {
792                                atomSites.add(MMCIFFileTools.convertAtomToAtomSite(atom, 1, molecId2, molecId2, atomId));
793                        } else {
794                                atomSites.add(MMCIFFileTools.convertAtomToAtomSite(atom, 1, molecId2, molecId2));
795                        }
796                        atomId++;
797                }
798
799                sb.append(MMCIFFileTools.toMMCIF(atomSites,AtomSite.class));
800
801                return sb.toString();
802        }
803
804        @Override
805        public int compareTo(StructureInterface o) {
806                // this will sort descending on interface areas
807                return (Double.compare(o.totalArea,this.totalArea));
808        }
809
810        @Override
811        public String toString() {
812                return String.format("StructureInterface %d (%s, %.0f A, <%s; %s>)", id, moleculeIds,totalArea,transforms.getFirst().toXYZString(),transforms.getSecond().toXYZString());
813        }
814
815}