Ejemplo n.º 1
0
    def generate(self, n_confs=100, rrmsd=None, k1=9, k2=3, th_lrmsd=6):
        '''
		This program is for generating new conformations.
		'''
        self.n_confs = n_confs
        self.rrmsd = rrmsd
        self.k1 = k1
        self.k2 = k2
        self.th_lrmsd = th_lrmsd
        self.eigval = self.allresidue.getEigvals()[6:]
        self.eigvec = self.allresidue.getArray().T[6:]
        self.nresil = len(self.eigvec[0]) / 3 - self.nresir

        n_confs = int(n_confs)
        n_atoms = self.allatom.numAtoms()
        initial = self.atoms.getCoords()

        print "number of total atoms for complex: ", n_atoms

        ind = self.Bset()
        array = self.allatom._getArray().T[6:]
        confs = []
        '''
		#check
		x, Rrmsd, Lrmsd = self.rejectsampling(ind)

		coords =  np.zeros(3*(self.nresil+self.nresir))

		for j in range(2*k):
			coords +=  x[j]/np.sqrt(self.eigval[ind[j]]) * self.eigvec[ind[j]]

		print ("#%d Rrmsd=%.3f Lrmsd=%.10f" %(1+1, Rrmsd, Lrmsd))
		print np.sqrt( np.sum(coords[0:3*self.nresir]**2)/self.nresir ), np.sqrt( np.sum(coords[3*self.nresir:]**2)/self.nresil )


		print array[0][0:50], np.linalg.norm(array[0]), self.eigvec[0][0:10]
		#checkend
'''

        for i in range(n_confs):

            x, Rrmsd, Lrmsd = self.rejectsampling(ind)
            coords = np.zeros(3 * n_atoms)

            for j in range(k1 + k2):

                coords += x[j] / np.sqrt(self.eigval[ind[j]]) * array[ind[j]]

            confs.append(coords.reshape((n_atoms, 3)))

            print("#%d Rrmsd=%.3f Lrmsd=%.3f" % (i + 1, Rrmsd, Lrmsd))

        ensemble = Ensemble('Conformations along {0}'.format(self.allatom))

        ensemble.setCoords(initial)

        ensemble.addCoordset(np.array(confs) + initial)

        return ensemble
Ejemplo n.º 2
0
 def __getitem__(self, index):
 
     if self._closed: 
         raise ValueError('I/O operation on closed file')
         
     if isinstance(index, int):
         return self.getFrame(index)
         
     elif isinstance(index, (slice, list, np.ndarray)):
         if isinstance(index, slice):
             ens = Ensemble('{0:s} ({1[0]:d}:{1[1]:d}:{1[2]:d})'.format(
                                 self._title, index.indices(len(self))))
         else:
             ens = Ensemble('{0:s} slice'.format(self._title))
         ens.setCoords(self.getCoords())
         if self._weights is not None:
             ens.setWeights(self._weights.copy())
         ens.addCoordset(self.getCoordsets(index))
         return ens
         
     else:
         raise IndexError('invalid index')
Ejemplo n.º 3
0
def calcOneWayAdaptiveANM(a, b, n_steps, **kwargs):
    """Runs one-way adaptivate ANM. """

    n_modes = kwargs.pop('n_modes', 20)

    coordsA, coordsB, title, atoms, weights, maskA, maskB, rmsd = checkInput(
        a, b, **kwargs)
    coordsA = coordsA.copy()

    LOGGER.timeit('_prody_calcAdaptiveANM')
    n = 0
    resetFmin = True
    defvecs = []
    rmsds = [rmsd]
    ensemble = Ensemble(title + '_aANM')
    ensemble.setAtoms(atoms)
    ensemble.setCoords(coordsB)
    ensemble.setWeights(weights)
    ensemble.addCoordset(coordsA.copy())
    while n < n_steps:
        LOGGER.info('\nStarting cycle {0} with initial structure {1}'.format(
            n + 1, title))
        n_modes = calcStep(coordsA,
                           coordsB,
                           n_modes,
                           ensemble,
                           defvecs,
                           rmsds,
                           mask=maskA,
                           resetFmin=resetFmin,
                           **kwargs)
        n += 1
        resetFmin = False
        if n_modes == 0:
            LOGGER.report('One-way Adaptive ANM converged in %.2fs.',
                          '_prody_calcAdaptiveANM')
            break

    return ensemble
Ejemplo n.º 4
0
    def __getitem__(self, index):

        if self._closed:
            raise ValueError('I/O operation on closed file')

        if isinstance(index, int):
            return self.getFrame(index)

        elif isinstance(index, (slice, list, ndarray)):
            if isinstance(index, slice):
                ens = Ensemble('{0} ({1[0]}:{1[1]}:{1[2]})'.format(
                    self._title, index.indices(len(self))))
            else:
                ens = Ensemble('{0} slice'.format(self._title))
            ens.setCoords(self.getCoords())
            if self._weights is not None:
                ens.setWeights(self._weights.copy())
            ens.addCoordset(self.getCoordsets(index))
            ens.setAtoms(self._atoms)
            return ens

        else:
            raise IndexError('invalid index')
Ejemplo n.º 5
0
def sampleModes(modes, atoms=None, n_confs=1000, rmsd=1.0):
    """Return an ensemble of randomly sampled conformations along given 
    *modes*.  If *atoms* are provided, sampling will be around its active 
    coordinate set.  Otherwise, sampling is around the 0 coordinate set.
    
    :arg modes: Modes along which sampling will be performed.
    :type modes: :class:`~.Mode`, :class:`~.ModeSet`, :class:`~.PCA`, 
                 :class:`~.ANM` or :class:`~.NMA`   
    
    :arg atoms: Atoms whose active coordinate set will be used as the initial 
        conformation.
    :type atoms: :class:`~.Atomic`  
    
    :arg n_confs: Number of conformations to generate. Default is 1000.
    :type n_steps: int 
    
    :arg rmsd: The average RMSD that the conformations will have with 
        respect to the initial conformation. Default is 1.0 A.
    :type rmsd: float 
    
    For given normal modes :math:`[u_1 u_2 ... u_m]` and their eigenvalues
    :math:`[\lambda_1 \lambda_2 ... \lambda_m]`, a new conformation 
    is sampled using the relation:
        
    .. math::
    
       R_k = R_0 + s \sum_{i=1}^{m} r_i^k \lambda^{-0.5}_i u_i 
    
    :math:`R_0` is the active coordinate set of *atoms*.
    :math:`[r_1^k r_2^k ... r_m^k]` are normally distributed random numbers 
    generated for conformation :math:`k` using :func:`numpy.random.randn`.
    
    RMSD of the new conformation from :math:`R_0` can be calculated as
     
    .. math::
        
      RMSD^k = \sqrt{ {\\left( s \sum_{i=1}^{m} r_i^k \lambda^{-0.5}_i u_i  \\right)}^{2} / N } = \\frac{s}{ \sqrt{N}} \sqrt{ \sum_{i=1}^{m} (r_i^k)^2 \lambda^{-1}_i  } 


    Average :math:`RMSD` of the generated conformations from the initial conformation is: 
        
    .. math::
        
      \\left< RMSD^k \\right> = \\frac{s}{ \sqrt{N}} \\left< \sqrt{ \sum_{i=1}^{m} (r_i^k)^2 \lambda^{-1}_i } \\right>

 
    From this relation :math:`s` scaling factor obtained using the relation 
    
    .. math::
       
       s =  \\left< RMSD^k \\right> \sqrt{N} {\\left< \sqrt{ \sum_{i=1}^{m} (r_i)^2 \lambda^{-1}_i} \\right>}^{-1}
       
     
    Note that random numbers are generated before conformations are 
    sampled, hence exact value of :math:`s` is known from this relation to
    ensure that the generated ensemble will have user given average *rmsd* 
    value. 
     
    Note that if modes are from a :class:`~.PCA`, variances are used instead of 
    inverse eigenvalues, i.e. :math:`\sigma_i \sim \lambda^{-1}_i`.
    
    |more| See also :func:`~.showEllipsoid`.
    
    .. plot::
       :context:
       :include-source:
        
       # Generate 300 conformations using ANM modes 1-3
       ensemble = sampleModes( p38_anm[:3], n_confs=500 )
       # Project these conformations onto the space spanned by these modes
       plt.figure(figsize=(5,4))
       showProjection(ensemble, p38_anm[:3], rmsd=True)
       
    .. plot::
       :context:
       :nofigs:
        
       plt.close('all')"""
       
    if not isinstance(modes, (Mode, NMA, ModeSet)):
        raise TypeError('modes must be a NMA or ModeSet instance, '
                        'not {0:s}'.format(type(modes)))
    if not modes.is3d():
        raise ValueError('modes must be from a 3-dimensional model')
    n_confs = int(n_confs)
    n_atoms = modes.numAtoms()
    initial = None
    if atoms is not None:
        if not isinstance(atoms, (Atomic)):
            raise TypeError('{0:s} is not correct type for atoms'
                            .format(type(atoms)))
        if atoms.numAtoms() != n_atoms:
            raise ValueError('number of atoms do not match')
        initial = atoms.getCoords()

    rmsd = float(rmsd)
    LOGGER.info('Parameter: rmsd = {0:.2f} A'.format(rmsd))
    n_confs = int(n_confs)
    LOGGER.info('Parameter: n_confs = {0:d}'.format(n_confs))
    
    
    if isinstance(modes, Mode):
        n_modes = 1
        variances = np.array([modes.getVariance()])
    else:
        n_modes = len(modes)
        variances = modes.getVariances()
    if np.any(variances == 0):
        raise ValueError('one or more modes has zero variance')
    randn = np.random.standard_normal((n_confs, n_modes))
    coef = ((randn ** 2 * variances).sum(1) ** 0.5).mean()
    scale = n_atoms**0.5 * rmsd / coef
    
    LOGGER.info('Modes are scaled by {0:g}.'.format(scale))
    
    confs = []
    append = confs.append
    scale = scale * variances ** 0.5
    array = modes._getArray()
    if array.ndim > 1:
        for i in range(n_confs):
            append( (array * scale * randn[i]).sum(1).reshape((n_atoms, 3)) )
    else:
        for i in range(n_confs):
            append( (array * scale * randn[i]).reshape((n_atoms, 3)) )

    ensemble = Ensemble('Conformations along {0:s}'.format(modes))
    if initial is None:
        ensemble.setCoords(np.zeros((n_atoms, 3)))
        ensemble.addCoordset(np.array(confs))
    else:    
        ensemble.setCoords(initial)
        ensemble.addCoordset(np.array(confs) + initial)
    return ensemble  
Ejemplo n.º 6
0
def traverseMode(mode, atoms, n_steps=10, rmsd=1.5):
    """Generates a trajectory along a given *mode*, which can be used to
    animate fluctuations in an external program. 
    
    :arg mode: Mode along which a trajectory will be generated.
    :type mode: :class:`~.Mode`   
    
    :arg atoms: Atoms whose active coordinate set will be used as the initial 
        conformation.
    :type atoms: :class:`~.Atomic` 
    
    :arg n_steps: Number of steps to take along each direction. 
        For example, for ``n_steps=10``, 20 conformations will be 
        generated along the first mode. Default is 10.
    :type n_steps: int 
    
    :arg rmsd: The maximum RMSD that the conformations will have with 
        respect to the initial conformation. Default is 1.5 A.
    :type rmsd: float

    :returns: :class:`~.Ensemble`
    
    For given normal mode :math:`u_i`, its eigenvalue
    :math:`\lambda_i`, number of steps :math:`n`, and maximum :math:`RMSD`  
    conformations :math:`[R_{-n} R_{-n+1} ... R_{-1} R_0 R_1 ... R_n]` are 
    generated.
    
    :math:`R_0` is the active coordinate set of *atoms*. 
    :math:`R_k = R_0 + sk\lambda_iu_i`, where :math:`s` is found using
    :math:`s = ((N (\\frac{RMSD}{n})^2) / \lambda_i^{-1}) ^{0.5}`, where
    :math:`N` is the number of atoms.
    
    
    .. plot::
       :context:
       :include-source:
        
       trajectory = traverseMode( p38_anm[0], p38_structure.select('calpha'), 
                                  n_steps=8, rmsd=1.4 )
       rmsd = calcRMSD(trajectory)
       plt.figure(figsize=(5,4))
       plt.plot(rmsd, '-o')
       plt.xlabel('Frame index')
       plt.ylabel('RMSD (A)')
       
    .. plot::
       :context:
       :nofigs:
        
       plt.close('all')"""
       
    if not isinstance(mode, VectorBase):
        raise TypeError('mode must be a Mode or Vector instance, '
                        'not {0:s}'.format(type(mode)))
    if not mode.is3d():
        raise ValueError('mode must be from a 3-dimensional model.')
    n_atoms = mode.numAtoms()
    initial = None
    if atoms is not None:
        if not isinstance(atoms, Atomic):
            raise TypeError('{0:s} is not correct type for atoms'
                            .format(type(atoms)))
        if atoms.numAtoms() != n_atoms:
            raise ValueError('number of atoms do not match')
        initial = atoms.getCoords()

    name = str(mode)
    
    rmsd = float(rmsd) + 0.000004
    LOGGER.info('Parameter: rmsd = {0:.2f} A'.format(rmsd))
    n_steps = int(n_steps)
    LOGGER.info('Parameter: n_steps = {0:d}'.format(n_steps))
    step = rmsd / n_steps
    LOGGER.info('Step size is {0:.2f} A RMSD'.format(step))
    arr = mode.getArrayNx3()
    var = mode.getVariance()
    scale = ((n_atoms * step**2) / var) **0.5
    LOGGER.info('Mode is scaled by {0:g}.'.format(scale))

    array = arr * var**0.5 * scale 
    confs_add = [initial + array]
    for s in range(1, n_steps):
        confs_add.append( confs_add[-1] + array)
    confs_sub = [initial - array]
    for s in range(1, n_steps):
        confs_sub.append( confs_sub[-1] - array)
    confs_sub.reverse()
    ensemble = Ensemble('Conformations along {0:s}'.format(name))
    ensemble.setCoords(initial)    
    ensemble.addCoordset(np.array(confs_sub + [initial] + confs_add))
    return ensemble
Ejemplo n.º 7
0
def sampleModes(modes, atoms=None, n_confs=1000, rmsd=1.0):
    """Returns an ensemble of randomly sampled conformations along given
    *modes*.  If *atoms* are provided, sampling will be around its active
    coordinate set.  Otherwise, sampling is around the 0 coordinate set.

    :arg modes: modes along which sampling will be performed
    :type modes: :class:`.Mode`, :class:`.ModeSet`, :class:`.PCA`,
                 :class:`.ANM` or :class:`.NMA`

    :arg atoms: atoms whose active coordinate set will be used as the initial
        conformation
    :type atoms: :class:`.Atomic`

    :arg n_confs: number of conformations to generate, default is 1000
    :type n_steps: int

    :arg rmsd: average RMSD that the conformations will have with
        respect to the initial conformation, default is 1.0 Å
    :type rmsd: float

    :returns: :class:`.Ensemble`

    For given normal modes :math:`[u_1 u_2 ... u_m]` and their eigenvalues
    :math:`[\\lambda_1 \\lambda_2 ... \\lambda_m]`, a new conformation
    is sampled using the relation:

    .. math::

       R_k = R_0 + s \\sum_{i=1}^{m} r_i^k \\lambda^{-0.5}_i u_i

    :math:`R_0` is the active coordinate set of *atoms*.
    :math:`[r_1^k r_2^k ... r_m^k]` are normally distributed random numbers
    generated for conformation :math:`k` using :func:`numpy.random.randn`.

    RMSD of the new conformation from :math:`R_0` can be calculated as

    .. math::

      RMSD^k = \\sqrt{ {\\left( s \\sum_{i=1}^{m} r_i^k \\lambda^{-0.5}_i u_i  \\right)}^{2} / N } = \\frac{s}{ \\sqrt{N}} \\sqrt{ \\sum_{i=1}^{m} (r_i^k)^2 \\lambda^{-1}_i  }


    Average :math:`RMSD` of the generated conformations from the initial conformation is:

    .. math::

      \\left< RMSD^k \\right> = \\frac{s}{ \\sqrt{N}} \\left< \\sqrt{ \\sum_{i=1}^{m} (r_i^k)^2 \\lambda^{-1}_i } \\right>


    From this relation :math:`s` scaling factor obtained using the relation

    .. math::

       s =  \\left< RMSD^k \\right> \\sqrt{N} {\\left< \\sqrt{ \\sum_{i=1}^{m} (r_i)^2 \\lambda^{-1}_i} \\right>}^{-1}


    Note that random numbers are generated before conformations are
    sampled, hence exact value of :math:`s` is known from this relation to
    ensure that the generated ensemble will have user given average *rmsd*
    value.

    Note that if modes are from a :class:`.PCA`, variances are used instead of
    inverse eigenvalues, i.e. :math:`\\sigma_i \\sim \\lambda^{-1}_i`.

    See also :func:`.showEllipsoid`."""

    if not isinstance(modes, (Mode, NMA, ModeSet)):
        raise TypeError('modes must be a NMA or ModeSet instance, '
                        'not {0}'.format(type(modes)))
    if not modes.is3d():
        raise ValueError('modes must be from a 3-dimensional model')
    n_confs = int(n_confs)
    n_atoms = modes.numAtoms()
    initial = None
    if atoms is not None:
        if isinstance(atoms, Atomic):
            if atoms.numAtoms() != n_atoms:
                raise ValueError('number of atoms do not match')
            initial = atoms.getCoords()
        elif isinstance(atoms, np.ndarray):
            initial = atoms
        else:
            raise TypeError('{0} is not correct type for atoms'
                            .format(type(atoms)))

    rmsd = float(rmsd)
    LOGGER.info('Parameter: rmsd = {0:.2f} A'.format(rmsd))
    n_confs = int(n_confs)
    LOGGER.info('Parameter: n_confs = {0}'.format(n_confs))

    if isinstance(modes, Mode):
        n_modes = 1
        variances = np.array([modes.getVariance()])
        magnitudes = np.array([abs(modes)])
    else:
        n_modes = len(modes)
        variances = modes.getVariances()
        magnitudes = np.array([abs(mode) for mode in modes])

    if np.any(variances == 0):
        raise ValueError('one or more modes has zero variance')
    randn = np.random.standard_normal((n_confs, n_modes))
    coef = ((randn ** 2 * variances).sum(1) ** 0.5).mean()
    scale = n_atoms**0.5 * rmsd / coef

    LOGGER.info('Modes are scaled by {0}.'.format(scale))

    confs = []
    append = confs.append
    scale = scale / magnitudes * variances ** 0.5

    array = modes._getArray()
    if array.ndim > 1:
        for i in range(n_confs):
            append((array * scale * randn[i]).sum(1).reshape((n_atoms, 3)))
    else:
        for i in range(n_confs):
            append((array * scale * randn[i]).reshape((n_atoms, 3)))

    ensemble = Ensemble('Conformations along {0}'.format(modes))
    if initial is None:
        ensemble.setCoords(np.zeros((n_atoms, 3)))
        ensemble.addCoordset(np.array(confs))
    else:
        ensemble.setCoords(initial)
        ensemble.addCoordset(np.array(confs) + initial)
    return ensemble
Ejemplo n.º 8
0
def traverseMode(mode, atoms, n_steps=10, rmsd=1.5, **kwargs):
    """Generates a trajectory along a given *mode*, which can be used to
    animate fluctuations in an external program.

    :arg mode: mode along which a trajectory will be generated
    :type mode: :class:`.Mode`

    :arg atoms: atoms whose active coordinate set will be used as the initial
        conformation
    :type atoms: :class:`.Atomic`

    :arg n_steps: number of steps to take along each direction,
        for example, for ``n_steps=10``, 20 conformations will be
        generated along *mode* with structure *atoms* in between, 
        default is 10.
    :type n_steps: int

    :arg rmsd: maximum RMSD that the conformations will have with
        respect to the initial conformation, default is 1.5 Å
    :type rmsd: float

    :arg pos: whether to include steps in the positive mode
        direction, default is **True**
    :type pos: bool

    :arg neg: whether to include steps in the negative mode
        direction, default is **True**
    :type neg: bool

    :arg reverse: whether to reverse the direction
        default is **False**
    :type reverse: bool

    :returns: :class:`.Ensemble`

    For given normal mode :math:`u_i`, its eigenvalue
    :math:`\\lambda_i`, number of steps :math:`n`, and maximum :math:`RMSD`
    conformations :math:`[R_{-n} R_{-n+1} ... R_{-1} R_0 R_1 ... R_n]` are
    generated.

    :math:`R_0` is the active coordinate set of *atoms*.
    :math:`R_k = R_0 + sk\\lambda_iu_i`, where :math:`s` is found using
    :math:`s = ((N (\\frac{RMSD}{n})^2) / \\lambda_i^{-1}) ^{0.5}`, where
    :math:`N` is the number of atoms."""

    pos = kwargs.get('pos', True)
    neg = kwargs.get('neg', True)
    reverse = kwargs.get('reverse', False)

    if pos is False and neg is False:
        raise ValueError('pos and neg cannot both be False')

    if not isinstance(mode, VectorBase):
        raise TypeError('mode must be a Mode or Vector instance, '
                        'not {0}'.format(type(mode)))
    if not mode.is3d():
        raise ValueError('mode must be from a 3-dimensional model.')
    n_atoms = mode.numAtoms()
    initial = None
    if atoms is not None:
        if not isinstance(atoms, Atomic):
            raise TypeError('{0} is not correct type for atoms'
                            .format(type(atoms)))
        if atoms.numAtoms() != n_atoms:
            raise ValueError('number of atoms do not match')
        initial = atoms.getCoords()

    name = str(mode)

    rmsd = float(rmsd) + 0.000004
    LOGGER.info('Parameter: rmsd = {0:.2f} A'.format(rmsd))
    n_steps = int(n_steps)
    LOGGER.info('Parameter: n_steps = {0}'.format(n_steps))
    step = rmsd / n_steps
    LOGGER.info('Step size is {0:.2f} A RMSD'.format(step))
    arr = mode.getArrayNx3()
    try:
        var = mode.getVariance()
    except AttributeError:
        var = 1.
    scale = ((n_atoms * step**2) / var) ** 0.5
    LOGGER.info('Mode is scaled by {0}.'.format(scale))

    array = arr * var**0.5 * scale / abs(mode)
    confs_add = [initial + array]
    for s in range(1, n_steps):
        confs_add.append(confs_add[-1] + array)
    confs_sub = [initial - array]
    for s in range(1, n_steps):
        confs_sub.append(confs_sub[-1] - array)
    confs_sub.reverse()
    ensemble = Ensemble('Conformations along {0}'.format(name))
    ensemble.setAtoms(atoms)
    ensemble.setCoords(initial)

    conf_list = [initial]
    if pos:
        conf_list = conf_list + confs_add
    if  neg:
        conf_list = confs_sub + conf_list
    conf_array = np.array(conf_list)

    if reverse:
        conf_array = conf_array[::-1]

    ensemble.addCoordset(conf_array)
    return ensemble
Ejemplo n.º 9
0
def calcBothWaysAdaptiveANM(a, b, n_steps, **kwargs):
    """Runs both-way adaptivate ANM. """

    n_modes0 = n_modes = kwargs.pop('n_modes', 20)

    coordsA, coordsB, title, atoms, weights, maskA, maskB, rmsd = checkInput(
        a, b, **kwargs)
    coordsA = coordsA.copy()
    coordsB = coordsB.copy()

    LOGGER.timeit('_prody_calcAdaptiveANM')
    n = 0
    resetFmin = True
    defvecs = []
    rmsds = [rmsd]
    ensA = Ensemble('A')
    ensA.setCoords(coordsA)
    ensA.setWeights(weights)
    ensA.addCoordset(coordsA.copy())

    ensB = Ensemble('B')
    ensB.setCoords(coordsB.copy())
    ensB.setWeights(weights)
    ensB.addCoordset(coordsB.copy())

    while n < n_steps:
        LOGGER.info('\nStarting cycle {0} with {1}'.format(
            n + 1, getTitle(a, 'structure A')))
        n_modes = calcStep(coordsA,
                           coordsB,
                           n_modes,
                           ensA,
                           defvecs,
                           rmsds,
                           mask=maskA,
                           resetFmin=resetFmin,
                           **kwargs)
        n += 1
        resetFmin = False

        if n_modes == 0:
            break

    n = 0
    n_modes = n_modes0
    resetFmin = True
    while n < n_steps:
        LOGGER.info('\nStarting cycle {0} with structure {1}'.format(
            n + 1, getTitle(b, 'structure B')))
        n_modes = calcStep(coordsB,
                           coordsA,
                           n_modes,
                           ensB,
                           defvecs,
                           rmsds,
                           mask=maskB,
                           resetFmin=resetFmin,
                           **kwargs)
        n += 1
        resetFmin = False

        if n_modes == 0:
            LOGGER.report('Alternating Adaptive ANM converged in %.2fs.',
                          '_prody_calcAdaptiveANM')
            break

    ensemble = ensA + ensB[::-1]
    ensemble.setTitle(title + '_aANM')
    ensemble.setAtoms(atoms)
    ensemble.setCoords(ensB.getCoords())

    LOGGER.report('Both-way Adaptive ANM converged in %.2fs.',
                  '_prody_calcAdaptiveANM')

    return ensemble