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.cluster;
022
023import org.biojava.nbio.alignment.Alignments;
024import org.biojava.nbio.alignment.Alignments.PairwiseSequenceAlignerType;
025import org.biojava.nbio.alignment.SimpleGapPenalty;
026import org.biojava.nbio.alignment.template.GapPenalty;
027import org.biojava.nbio.alignment.template.PairwiseSequenceAligner;
028import org.biojava.nbio.core.alignment.matrices.SubstitutionMatrixHelper;
029import org.biojava.nbio.core.alignment.template.SubstitutionMatrix;
030import org.biojava.nbio.core.exceptions.CompoundNotFoundException;
031import org.biojava.nbio.core.sequence.ProteinSequence;
032import org.biojava.nbio.core.sequence.compound.AminoAcidCompound;
033import org.biojava.nbio.structure.Atom;
034import org.biojava.nbio.structure.StructureException;
035import org.biojava.nbio.structure.align.StructureAlignment;
036import org.biojava.nbio.structure.align.StructureAlignmentFactory;
037import org.biojava.nbio.structure.align.ce.ConfigStrucAligParams;
038import org.biojava.nbio.structure.align.model.AFPChain;
039import org.biojava.nbio.structure.align.multiple.Block;
040import org.biojava.nbio.structure.align.multiple.BlockImpl;
041import org.biojava.nbio.structure.align.multiple.BlockSet;
042import org.biojava.nbio.structure.align.multiple.BlockSetImpl;
043import org.biojava.nbio.structure.align.multiple.MultipleAlignment;
044import org.biojava.nbio.structure.align.multiple.MultipleAlignmentEnsembleImpl;
045import org.biojava.nbio.structure.align.multiple.MultipleAlignmentImpl;
046import org.biojava.nbio.structure.align.multiple.util.MultipleAlignmentScorer;
047import org.biojava.nbio.structure.align.multiple.util.ReferenceSuperimposer;
048import org.biojava.nbio.structure.symmetry.core.QuatSymmetrySubunits;
049import org.biojava.nbio.structure.symmetry.internal.CESymmParameters;
050import org.biojava.nbio.structure.symmetry.internal.CeSymm;
051import org.biojava.nbio.structure.symmetry.internal.CeSymmResult;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055import java.lang.reflect.InvocationTargetException;
056import java.lang.reflect.Method;
057import java.util.*;
058import java.util.stream.Collectors;
059
060/**
061 * A SubunitCluster contains a set of equivalent {@link QuatSymmetrySubunits},
062 * the set of equivalent residues (EQR) between {@link Subunit} and a
063 * {@link Subunit} representative. It also stores the method used for
064 * clustering.
065 * <p>
066 * This class allows the comparison and merging of SubunitClusters.
067 *
068 * @author Aleix Lafita
069 * @since 5.0.0
070 *
071 */
072public class SubunitCluster {
073
074        private static final Logger logger = LoggerFactory
075                        .getLogger(SubunitCluster.class);
076
077        private List<Subunit> subunits = new ArrayList<Subunit>();
078        private List<List<Integer>> subunitEQR = new ArrayList<List<Integer>>();
079        private int representative = -1;
080
081        private SubunitClustererMethod method = SubunitClustererMethod.SEQUENCE;
082        private boolean pseudoStoichiometric = false;
083
084        /**
085         * A letter that is assigned to this cluster in stoichiometry.
086        */
087        private String alpha = "";
088
089        /**
090         * A letter that is assigned to this cluster in stoichiometry.
091         *
092         * @return alpha
093         *          String
094         */
095
096        public String getAlpha() {
097                return alpha;
098        }
099
100        /**
101         * A letter that is assigned to this cluster in stoichiometry.
102         *
103         * @param  alpha
104         *          String
105         */
106        public void setAlpha(String alpha) {
107                this.alpha = alpha;
108        }
109
110        /**
111         * A constructor from a single Subunit. To obtain a
112         * SubunitCluster with multiple Subunits, initialize different
113         * SubunitClusters and merge them.
114         *
115         * @param subunit
116         *            initial Subunit
117         */
118        public SubunitCluster(Subunit subunit) {
119
120                subunits.add(subunit);
121
122                List<Integer> identity = new ArrayList<Integer>();
123                for (int i = 0; i < subunit.size(); i++)
124                        identity.add(i);
125                subunitEQR.add(identity);
126
127                representative = 0;
128        }
129
130        /**
131         * A copy constructor with the possibility of removing subunits.
132         * No re-clustering is done.
133         *
134         * @param other
135         *            reference SubunitCluster
136         * @param subunitsToRetain
137         *            which subunits to copy to this cluster
138         */
139        public SubunitCluster(SubunitCluster other, List<Integer> subunitsToRetain) {
140                method = other.method;
141                pseudoStoichiometric = other.pseudoStoichiometric;
142                for (int i = 0; i < other.subunits.size(); i++) {
143                        if(subunitsToRetain.contains(i)) {
144                                subunits.add(other.subunits.get(i));
145                                subunitEQR.add(other.subunitEQR.get(i));
146                        }
147                }
148                representative = 0;
149                for (int i=1; i<subunits.size(); i++) {
150                        if (subunits.get(i).size() > subunits.get(representative).size()) {
151                                representative = i;
152                        }
153                }
154                setAlpha(other.getAlpha());
155        }
156
157        /**
158         * Subunits contained in the SubunitCluster.
159         *
160         * @return an unmodifiable view of the original List
161         */
162        public List<Subunit> getSubunits() {
163                return Collections.unmodifiableList(subunits);
164        }
165
166        /**
167         * Tells whether the other SubunitCluster contains exactly the same Subunit.
168         * This is checked by String equality of their residue one-letter sequences.
169         *
170         * @param other
171         *            SubunitCluster
172         * @return true if the SubunitClusters are identical, false otherwise
173         */
174        public boolean isIdenticalTo(SubunitCluster other) {
175                String thisSequence = this.subunits.get(this.representative)
176                                .getProteinSequenceString();
177                String otherSequence = other.subunits.get(other.representative)
178                                .getProteinSequenceString();
179                return thisSequence.equals(otherSequence);
180        }
181
182        /**
183         * Merges the other SubunitCluster into this one if it contains exactly the
184         * same Subunit. This is checked by {@link #isIdenticalTo(SubunitCluster)}.
185         *
186         * @param other
187         *            SubunitCluster
188         * @return true if the SubunitClusters were merged, false otherwise
189         */
190        public boolean mergeIdentical(SubunitCluster other) {
191
192                if (!isIdenticalTo(other))
193                        return false;
194
195                logger.info("SubunitClusters are identical");
196
197                this.subunits.addAll(other.subunits);
198                this.subunitEQR.addAll(other.subunitEQR);
199
200                return true;
201        }
202
203        /**
204         * Merges the other SubunitCluster into this one if their representatives
205         * sequences are similar (according to the criteria in params).
206         * <p>
207         * The sequence alignment is performed using linear {@link SimpleGapPenalty} and
208         * BLOSUM62 as scoring matrix.
209         *
210         * @param other
211         *            SubunitCluster
212         * @param params
213         *            SubunitClustererParameters, with information whether to use local
214         *            or global alignment, sequence identity and coverage thresholds.
215         *            Threshold values lower than 0.7 are not recommended.
216         *            Use {@link #mergeStructure} for lower values.
217         * @return true if the SubunitClusters were merged, false otherwise
218         * @throws CompoundNotFoundException
219         */
220
221        public boolean mergeSequence(SubunitCluster other, SubunitClustererParameters params) throws CompoundNotFoundException {
222                PairwiseSequenceAlignerType alignerType = PairwiseSequenceAlignerType.LOCAL;
223                if (params.isUseGlobalMetrics()) {
224                        alignerType = PairwiseSequenceAlignerType.GLOBAL;
225                }
226                return mergeSequence(other, params,alignerType
227                                , new SimpleGapPenalty(),
228                                SubstitutionMatrixHelper.getBlosum62());
229        }
230
231        /**
232         * Merges the other SubunitCluster into this one if their representatives
233         * sequences are similar (according to the criteria in params).
234         * <p>
235         * The sequence alignment is performed using linear {@link SimpleGapPenalty} and
236         * BLOSUM62 as scoring matrix.
237         *
238         * @param other
239         *            SubunitCluster
240         * @param params
241         *            {@link SubunitClustererParameters}, with information whether to use local
242         *            or global alignment, sequence identity and coverage thresholds.
243         *            Threshold values lower than 0.7 are not recommended.
244         *            Use {@link #mergeStructure} for lower values.
245         * @param alignerType
246         *            parameter for the sequence alignment algorithm
247         * @param gapPenalty
248         *            parameter for the sequence alignment algorithm
249         * @param subsMatrix
250         *            parameter for the sequence alignment algorithm
251         * @return true if the SubunitClusters were merged, false otherwise
252         * @throws CompoundNotFoundException
253         */
254
255        public boolean mergeSequence(SubunitCluster other, SubunitClustererParameters params,
256                                                                 PairwiseSequenceAlignerType alignerType,
257                                                                 GapPenalty gapPenalty,
258                                                                 SubstitutionMatrix<AminoAcidCompound> subsMatrix)
259                        throws CompoundNotFoundException {
260
261                // Extract the protein sequences as BioJava alignment objects
262                ProteinSequence thisSequence = this.subunits.get(this.representative)
263                                .getProteinSequence();
264                ProteinSequence otherSequence = other.subunits
265                                .get(other.representative).getProteinSequence();
266
267                // Perform the alignment with provided parameters
268                PairwiseSequenceAligner<ProteinSequence, AminoAcidCompound> aligner = Alignments
269                                .getPairwiseAligner(thisSequence, otherSequence, alignerType,
270                                                gapPenalty, subsMatrix);
271
272                double sequenceIdentity;
273                if(params.isUseGlobalMetrics()) {
274                        sequenceIdentity = aligner.getPair().getPercentageOfIdentity(true);
275                } else {
276                        sequenceIdentity = aligner.getPair().getPercentageOfIdentity(false);
277                }
278
279                if (sequenceIdentity < params.getSequenceIdentityThreshold())
280                        return false;
281
282                double sequenceCoverage = 0;
283                if(params.isUseSequenceCoverage()) {
284                        // Calculate real coverage (subtract gaps in both sequences)
285                        double gaps1 = aligner.getPair().getAlignedSequence(1)
286                                        .getNumGapPositions();
287                        double gaps2 = aligner.getPair().getAlignedSequence(2)
288                                        .getNumGapPositions();
289                        double lengthAlignment = aligner.getPair().getLength();
290                        double lengthThis = aligner.getQuery().getLength();
291                        double lengthOther = aligner.getTarget().getLength();
292                        sequenceCoverage = (lengthAlignment - gaps1 - gaps2)
293                                        / Math.max(lengthThis, lengthOther);
294
295                        if (sequenceCoverage < params.getSequenceCoverageThreshold())
296                                return false;
297                }
298
299                logger.info(String.format("SubunitClusters are similar in sequence "
300                                                + "with %.2f sequence identity and %.2f coverage", sequenceIdentity,
301                                sequenceCoverage));
302
303                // If coverage and sequence identity sufficient, merge other and this
304                List<Integer> thisAligned = new ArrayList<Integer>();
305                List<Integer> otherAligned = new ArrayList<Integer>();
306
307                // Extract the aligned residues of both Subunit
308                for (int p = 1; p < aligner.getPair().getLength() + 1; p++) {
309
310                        // Skip gaps in any of the two sequences
311                        if (aligner.getPair().getAlignedSequence(1).isGap(p))
312                                continue;
313                        if (aligner.getPair().getAlignedSequence(2).isGap(p))
314                                continue;
315
316                        int thisIndex = aligner.getPair().getIndexInQueryAt(p) - 1;
317                        int otherIndex = aligner.getPair().getIndexInTargetAt(p) - 1;
318
319                        // Only consider residues that are part of the SubunitCluster
320                        if (this.subunitEQR.get(this.representative).contains(thisIndex)
321                                        && other.subunitEQR.get(other.representative).contains(
322                                                        otherIndex)) {
323                                thisAligned.add(thisIndex);
324                                otherAligned.add(otherIndex);
325                        }
326                }
327
328                // Do a List intersection to find out which EQR columns to remove
329                List<Integer> thisRemove = new ArrayList<Integer>();
330                List<Integer> otherRemove = new ArrayList<Integer>();
331
332                for (int t = 0; t < this.subunitEQR.get(this.representative).size(); t++) {
333                        // If the index is aligned do nothing, otherwise mark as removing
334                        if (!thisAligned.contains(this.subunitEQR.get(this.representative)
335                                        .get(t)))
336                                thisRemove.add(t);
337                }
338
339                for (int t = 0; t < other.subunitEQR.get(other.representative).size(); t++) {
340                        // If the index is aligned do nothing, otherwise mark as removing
341                        if (!otherAligned.contains(other.subunitEQR.get(
342                                        other.representative).get(t)))
343                                otherRemove.add(t);
344                }
345                // Now remove unaligned columns, from end to start
346                Collections.sort(thisRemove);
347                Collections.reverse(thisRemove);
348                Collections.sort(otherRemove);
349                Collections.reverse(otherRemove);
350
351                for (int t = 0; t < thisRemove.size(); t++) {
352                        for (List<Integer> eqr : this.subunitEQR) {
353                                int column = thisRemove.get(t);
354                                eqr.remove(column);
355                        }
356                }
357
358                for (int t = 0; t < otherRemove.size(); t++) {
359                        for (List<Integer> eqr : other.subunitEQR) {
360                                int column = otherRemove.get(t);
361                                eqr.remove(column);
362                        }
363                }
364
365                // The representative is the longest sequence
366                if (this.subunits.get(this.representative).size() < other.subunits.get(
367                                other.representative).size())
368                        this.representative = other.representative + subunits.size();
369
370                this.subunits.addAll(other.subunits);
371                this.subunitEQR.addAll(other.subunitEQR);
372
373                this.method = SubunitClustererMethod.SEQUENCE;
374
375                pseudoStoichiometric = !params.isHighConfidenceScores(sequenceIdentity,sequenceCoverage);
376
377                return true;
378        }
379
380        /**
381         * Merges the other SubunitCluster into this one if their representative
382         * Atoms are structurally similar (according to the criteria in params).
383         * <p>
384         *
385         * @param other
386         *            SubunitCluster
387         * @param params
388         *            {@link SubunitClustererParameters}, with information on what alignment
389         *            algorithm to use, RMSD/TMScore and structure coverage thresholds.
390         * @return true if the SubunitClusters were merged, false otherwise
391         * @throws StructureException
392         */
393
394        public boolean mergeStructure(SubunitCluster other, SubunitClustererParameters params) throws StructureException {
395
396                StructureAlignment aligner = StructureAlignmentFactory.getAlgorithm(params.getSuperpositionAlgorithm());
397                ConfigStrucAligParams aligner_params = aligner.getParameters();
398
399                Method setOptimizeAlignment = null;
400                try {
401                        setOptimizeAlignment = aligner_params.getClass().getMethod("setOptimizeAlignment", boolean.class);
402                } catch (NoSuchMethodException e) {
403                        //alignment algorithm does not have an optimization switch, moving on
404                }
405                if (setOptimizeAlignment != null) {
406                        try {
407                                setOptimizeAlignment.invoke(aligner_params, params.isOptimizeAlignment());
408                        } catch (IllegalAccessException|InvocationTargetException e) {
409                                logger.warn("Could not set alignment optimisation switch");
410                        }
411                }
412
413                AFPChain afp = aligner.align(this.subunits.get(this.representative)
414                                .getRepresentativeAtoms(),
415                                other.subunits.get(other.representative)
416                                                .getRepresentativeAtoms());
417
418                // Convert AFPChain to MultipleAlignment for convenience
419                MultipleAlignment msa = new MultipleAlignmentEnsembleImpl(
420                                afp,
421                                this.subunits.get(this.representative).getRepresentativeAtoms(),
422                                other.subunits.get(other.representative)
423                                                .getRepresentativeAtoms(), false)
424                                .getMultipleAlignment(0);
425
426                double structureCoverage = Math.min(msa.getCoverages().get(0), msa
427                                .getCoverages().get(1));
428
429                if(params.isUseStructureCoverage() && structureCoverage < params.getStructureCoverageThreshold()) {
430                        return false;
431                }
432
433                double rmsd = afp.getTotalRmsdOpt();
434                if (params.isUseRMSD() && rmsd > params.getRMSDThreshold()) {
435                        return false;
436                }
437
438                double tmScore = afp.getTMScore();
439                if (params.isUseTMScore() && tmScore < params.getTMThreshold()) {
440                        return false;
441                }
442
443                logger.info(String.format("SubunitClusters are structurally similar with "
444                                + "%.2f RMSD %.2f coverage", rmsd, structureCoverage));
445
446                // Merge clusters
447                List<List<Integer>> alignedRes = msa.getBlock(0).getAlignRes();
448                List<Integer> thisAligned = new ArrayList<Integer>();
449                List<Integer> otherAligned = new ArrayList<Integer>();
450
451                // Extract the aligned residues of both Subunit
452                for (int p = 0; p < msa.length(); p++) {
453
454                        // Skip gaps in any of the two sequences
455                        if (alignedRes.get(0).get(p) == null)
456                                continue;
457                        if (alignedRes.get(1).get(p) == null)
458                                continue;
459
460                        int thisIndex = alignedRes.get(0).get(p);
461                        int otherIndex = alignedRes.get(1).get(p);
462
463                        // Only consider residues that are part of the SubunitCluster
464                        if (this.subunitEQR.get(this.representative).contains(thisIndex)
465                                        && other.subunitEQR.get(other.representative).contains(
466                                                        otherIndex)) {
467                                thisAligned.add(thisIndex);
468                                otherAligned.add(otherIndex);
469                        }
470                }
471
472                // Do a List intersection to find out which EQR columns to remove
473                List<Integer> thisRemove = new ArrayList<Integer>();
474                List<Integer> otherRemove = new ArrayList<Integer>();
475
476                for (int t = 0; t < this.subunitEQR.get(this.representative).size(); t++) {
477                        // If the index is aligned do nothing, otherwise mark as removing
478                        if (!thisAligned.contains(this.subunitEQR.get(this.representative)
479                                        .get(t)))
480                                thisRemove.add(t);
481                }
482
483                for (int t = 0; t < other.subunitEQR.get(other.representative).size(); t++) {
484                        // If the index is aligned do nothing, otherwise mark as removing
485                        if (!otherAligned.contains(other.subunitEQR.get(
486                                        other.representative).get(t)))
487                                otherRemove.add(t);
488                }
489
490                // Now remove unaligned columns, from end to start
491                Collections.sort(thisRemove);
492                Collections.reverse(thisRemove);
493                Collections.sort(otherRemove);
494                Collections.reverse(otherRemove);
495
496                for (int t = 0; t < thisRemove.size(); t++) {
497                        for (List<Integer> eqr : this.subunitEQR) {
498                                int column = thisRemove.get(t);
499                                eqr.remove(column);
500                        }
501                }
502
503                for (int t = 0; t < otherRemove.size(); t++) {
504                        for (List<Integer> eqr : other.subunitEQR) {
505                                int column = otherRemove.get(t);
506                                eqr.remove(column);
507                        }
508                }
509
510                // The representative is the longest sequence
511                if (this.subunits.get(this.representative).size() < other.subunits.get(
512                                other.representative).size())
513                        this.representative = other.representative + subunits.size();
514
515                this.subunits.addAll(other.subunits);
516                this.subunitEQR.addAll(other.subunitEQR);
517
518                this.method = SubunitClustererMethod.STRUCTURE;
519                pseudoStoichiometric = true;
520
521                return true;
522        }
523
524        /**
525         * Analyze the internal symmetry of the SubunitCluster and divide its
526         * {@link Subunit} into the internal repeats (domains) if they are
527         * internally symmetric.
528         *
529         * @param clusterParams {@link SubunitClustererParameters} with fields used as follows:
530         * structureCoverageThreshold
531         *            the minimum coverage of all repeats in the Subunit
532         * rmsdThreshold
533         *            the maximum allowed RMSD between the repeats
534         * minimumSequenceLength
535         *            the minimum length of the repeating units
536         * @return true if the cluster was internally symmetric, false otherwise
537         * @throws StructureException
538         */
539        public boolean divideInternally(SubunitClustererParameters clusterParams)
540                        throws StructureException {
541
542                CESymmParameters cesym_params = new CESymmParameters();
543                cesym_params.setMinCoreLength(clusterParams.getMinimumSequenceLength());
544                cesym_params.setGaps(false); // We want no gaps between the repeats
545
546                // Analyze the internal symmetry of the representative subunit
547                CeSymmResult result = CeSymm.analyze(subunits.get(representative)
548                                .getRepresentativeAtoms(), cesym_params);
549
550                if (!result.isSignificant())
551                        return false;
552
553                double rmsd = result.getMultipleAlignment().getScore(
554                                MultipleAlignmentScorer.RMSD);
555                if (rmsd > clusterParams.getRMSDThreshold())
556                        return false;
557
558                double coverage = result.getMultipleAlignment().getCoverages().get(0)
559                                * result.getNumRepeats();
560                if (coverage < clusterParams.getStructureCoverageThreshold())
561                        return false;
562
563                logger.info("SubunitCluster is internally symmetric with {} repeats, "
564                                + "{} RMSD and {} coverage", result.getNumRepeats(), rmsd,
565                                coverage);
566
567                // Divide if symmety was significant with RMSD and coverage sufficient
568                List<List<Integer>> alignedRes = result.getMultipleAlignment()
569                                .getBlock(0).getAlignRes();
570
571                List<List<Integer>> columns = new ArrayList<List<Integer>>();
572                for (int s = 0; s < alignedRes.size(); s++)
573                        columns.add(new ArrayList<Integer>(alignedRes.get(s).size()));
574
575                // Extract the aligned columns of each repeat in the Subunit
576                for (int col = 0; col < alignedRes.get(0).size(); col++) {
577
578                        // Check that all aligned residues are part of the Cluster
579                        boolean missing = false;
580                        for (int s = 0; s < alignedRes.size(); s++) {
581                                if (!subunitEQR.get(representative).contains(
582                                                alignedRes.get(s).get(col))) {
583                                        missing = true;
584                                        break;
585                                }
586                        }
587
588                        // Skip the column if any residue was not part of the cluster
589                        if (missing)
590                                continue;
591
592                        for (int s = 0; s < alignedRes.size(); s++) {
593                                columns.get(s).add(
594                                                subunitEQR.get(representative).indexOf(
595                                                                alignedRes.get(s).get(col)));
596                        }
597                }
598
599                // Divide the Subunits in their repeats
600                List<Subunit> newSubunits = new ArrayList<Subunit>(subunits.size()
601                                * columns.size());
602                List<List<Integer>> newSubunitEQR = new ArrayList<List<Integer>>(
603                                subunits.size() * columns.size());
604
605                for (int s = 0; s < subunits.size(); s++) {
606                        for (int r = 0; r < columns.size(); r++) {
607
608                                // Calculate start and end residues of the new Subunit
609                                int start = subunitEQR.get(s).get(columns.get(r).get(0));
610                                int end = subunitEQR.get(s).get(
611                                                columns.get(r).get(columns.get(r).size() - 1));
612
613                                Atom[] reprAtoms = Arrays.copyOfRange(subunits.get(s)
614                                                .getRepresentativeAtoms(), start, end + 1);
615
616                                newSubunits.add(new Subunit(reprAtoms, subunits.get(s)
617                                                .getName(), subunits.get(s).getIdentifier(), subunits
618                                                .get(s).getStructure()));
619
620                                // Recalculate equivalent residues
621                                List<Integer> eqr = new ArrayList<Integer>();
622                                for (int p = 0; p < columns.get(r).size(); p++) {
623                                        eqr.add(subunitEQR.get(s).get(columns.get(r).get(p))
624                                                        - start);
625                                }
626                                newSubunitEQR.add(eqr);
627                        }
628                }
629
630                subunits = newSubunits;
631                subunitEQR = newSubunitEQR;
632
633                // Update representative
634                for (int s = 0; s < subunits.size(); s++) {
635                        if (subunits.get(s).size() > subunits.get(representative).size())
636                                representative = s;
637                }
638
639                method = SubunitClustererMethod.STRUCTURE;
640                pseudoStoichiometric = true;
641                return true;
642        }
643
644        /**
645         * @return the number of Subunits in the cluster
646         */
647        public int size() {
648                return subunits.size();
649        }
650
651        /**
652         * @return the number of aligned residues between Subunits of the cluster
653         */
654        public int length() {
655                return subunitEQR.get(representative).size();
656        }
657
658        /**
659         * @return the {@link SubunitClustererMethod} used for clustering the
660         *         Subunits
661         */
662        public SubunitClustererMethod getClustererMethod() {
663                return method;
664        }
665
666        /**
667         * @return A List of size {@link #size()} of Atom arrays of length
668         *         {@link #length()} with the aligned Atoms for each Subunit in the
669         *         cluster
670         */
671        public List<Atom[]> getAlignedAtomsSubunits() {
672
673                List<Atom[]> alignedAtoms = Collections.emptyList();
674
675                // Loop through all subunits and add the aligned positions
676                for (int s = 0; s < subunits.size(); s++)
677                        alignedAtoms.add(getAlignedAtomsSubunit(s));
678
679                return alignedAtoms;
680        }
681
682        /**
683         * @param index
684         *            Subunit index in the Cluster
685         * @return An Atom array of length {@link #length()} with the aligned Atoms
686         *         from the selected Subunit in the Cluster
687         */
688        public Atom[] getAlignedAtomsSubunit(int index) {
689
690                Atom[] aligned = new Atom[subunitEQR.get(index).size()];
691
692                // Add only the aligned positions of the Subunit in the Cluster
693                for (int p = 0; p < subunitEQR.get(index).size(); p++) {
694                        aligned[p] = subunits.get(index).getRepresentativeAtoms()[subunitEQR
695                                        .get(index).get(p)];
696                }
697
698                return aligned;
699        }
700
701        /**
702         * The multiple alignment is calculated from the equivalent residues in the
703         * SubunitCluster. The alignment is recalculated every time the method is
704         * called (no caching).
705         *
706         * @return MultipleAlignment representation of the aligned residues in this
707         *         Subunit Cluster
708         * @throws StructureException
709         */
710        public MultipleAlignment getMultipleAlignment() throws StructureException {
711
712                // Create a multiple alignment with the atom arrays of the Subunits
713                MultipleAlignment msa = new MultipleAlignmentImpl();
714                msa.setEnsemble(new MultipleAlignmentEnsembleImpl());
715                msa.getEnsemble().setAtomArrays(
716                                subunits.stream().map(s -> s.getRepresentativeAtoms())
717                                                .collect(Collectors.toList()));
718
719                // Fill in the alignment information
720                BlockSet bs = new BlockSetImpl(msa);
721                Block b = new BlockImpl(bs);
722                b.setAlignRes(subunitEQR);
723
724                // Fill in the transformation matrices
725                new ReferenceSuperimposer(representative).superimpose(msa);
726
727                // Calculate some scores
728                MultipleAlignmentScorer.calculateScores(msa);
729
730                return msa;
731
732        }
733
734        @Override
735        public String toString() {
736                return "SubunitCluster [Size=" + size() + ", Length=" + length()
737                                + ", Representative=" + representative + ", Method=" + method
738                                + "]";
739        }
740
741        /**
742         * @return true if this cluster is considered pseudo-stoichiometric (i.e.,
743         *                 was either clustered by structure, or by sequence with low scores),
744         *         false otherwise.
745         */
746        public boolean isPseudoStoichiometric() {
747                return pseudoStoichiometric;
748        }
749
750}