Perform structural analysis on a frame/ set of frames.

        Depending on the passed parameters a frame, a list of particular
        frames, a range of frames (from, to), or all frames can be analysed
        with this function.

        The analysis is performed on each frame and each discrete molecule in
        that frame separately. The steps are as follows:

        1. A frame is extracted and returned as a :class:`MolecularSystem`.
        2. If `swap_atoms` is set the atom ids are swapped.
        3. If `forcefield` is set the atom ids are deciphered.
        4. If `rebuild` is set the molecules in the system are rebuild.
        5. Each discrete molecule is extracted as :class:`Molecule`
        6. Each molecule is analysed with :func:`Molecule.full_analysis()`
        7. Analysis output populates the :attr:`analysis_output` dictionary.

        As the analysis of trajectories often have to be unique, many options
        are conditional.

        A side effect of this function is that the analysed frames are also
        returned to the :attr:`frames` mimicking the behaviour of the
        :func:`get_frames()`.

        Parameters
        ----------
        frames : :class:`int` or :class:`list` or :class:`touple` or :class:`str`
            Specified frame (:class:`int`), or frames (:class:`list`), or
            range (:class:`touple`), or `all`/`everything` (:class:`str`).
            (default='all')

        override : :class:`bool`
            If True, an output already storred in :attr:`analysis_output` can
            be override. (default=False)

        swap_atoms : :class:`dict`, optional
            If this kwarg is passed with an appropriate dictionary a
            :func:`pywindow.molecular.MolecularSystem.swap_atom_keys()` will
            be applied to the extracted frame.

        forcefield : :class:`str`, optional
            If this kwarg is passed with appropriate forcefield keyword a
            :func:`pywindow.molecular.MolecularSystem.decipher_atom_keys()`
            will be applied to the extracted frame.

        modular : :class:`bool`, optional
            If this kwarg is passed a
            :func:`pywindow.molecular.MolecularSystem.make_modular()`
            will be applied to the extracted frame. (default=False)

        rebuild : :class:`bool`, optional
            If this kwarg is passed a `rebuild=True` is passed to
            :func:`pywindow.molecular.MolecularSystem.make_modular()` that
            will be applied to the extracted frame. (default=False)

        ncpus : :class:`int`, optional
            If ncpus > 1, then the analysis is performed in parallel for the
            specified number of parallel jobs. Otherwise, it runs in serial.
            (default=1)

        Returns
        -------
        None : :class:`NoneType`
            The function returns `None`, the analysis output is
            returned to :attr:`analysis_output` dictionary.

def analysis(self, frames='all', ncpus=1, override=False, **kwargs):
        """
        Perform structural analysis on a frame/ set of frames.

        Depending on the passed parameters a frame, a list of particular
        frames, a range of frames (from, to), or all frames can be analysed
        with this function.

        The analysis is performed on each frame and each discrete molecule in
        that frame separately. The steps are as follows:

        1. A frame is extracted and returned as a :class:`MolecularSystem`.
        2. If `swap_atoms` is set the atom ids are swapped.
        3. If `forcefield` is set the atom ids are deciphered.
        4. If `rebuild` is set the molecules in the system are rebuild.
        5. Each discrete molecule is extracted as :class:`Molecule`
        6. Each molecule is analysed with :func:`Molecule.full_analysis()`
        7. Analysis output populates the :attr:`analysis_output` dictionary.

        As the analysis of trajectories often have to be unique, many options
        are conditional.

        A side effect of this function is that the analysed frames are also
        returned to the :attr:`frames` mimicking the behaviour of the
        :func:`get_frames()`.

        Parameters
        ----------
        frames : :class:`int` or :class:`list` or :class:`touple` or :class:`str`
            Specified frame (:class:`int`), or frames (:class:`list`), or
            range (:class:`touple`), or `all`/`everything` (:class:`str`).
            (default='all')

        override : :class:`bool`
            If True, an output already storred in :attr:`analysis_output` can
            be override. (default=False)

        swap_atoms : :class:`dict`, optional
            If this kwarg is passed with an appropriate dictionary a
            :func:`pywindow.molecular.MolecularSystem.swap_atom_keys()` will
            be applied to the extracted frame.

        forcefield : :class:`str`, optional
            If this kwarg is passed with appropriate forcefield keyword a
            :func:`pywindow.molecular.MolecularSystem.decipher_atom_keys()`
            will be applied to the extracted frame.

        modular : :class:`bool`, optional
            If this kwarg is passed a
            :func:`pywindow.molecular.MolecularSystem.make_modular()`
            will be applied to the extracted frame. (default=False)

        rebuild : :class:`bool`, optional
            If this kwarg is passed a `rebuild=True` is passed to
            :func:`pywindow.molecular.MolecularSystem.make_modular()` that
            will be applied to the extracted frame. (default=False)

        ncpus : :class:`int`, optional
            If ncpus > 1, then the analysis is performed in parallel for the
            specified number of parallel jobs. Otherwise, it runs in serial.
            (default=1)

        Returns
        -------
        None : :class:`NoneType`
            The function returns `None`, the analysis output is
            returned to :attr:`analysis_output` dictionary.

        """
        if override is True:
            self.analysis_output = {}
        if isinstance(frames, int):
            analysed_frame = self._analysis_serial(frames, ncpus, **kwargs)
            if frames not in self.analysis_output.keys():
                self.analysis_output[frames] = analysed_frame
            return analysed_frame
        else:
            frames_for_analysis = []
            if isinstance(frames, list):
                for frame in frames:
                    if frame not in self.analysis_output.keys():
                        frames_for_analysis.append(frame)
            if isinstance(frames, tuple):
                for frame in range(frames[0], frames[1]):
                    if frame not in self.analysis_output.keys():
                        frames_for_analysis.append(frame)
            if isinstance(frames, str):
                if frames in ['all', 'everything']:
                    for frame in range(0, self.no_of_frames):
                        if frame not in self.analysis_output.keys():
                            frames_for_analysis.append(frame)
            self._analysis_parallel(frames_for_analysis, ncpus, **kwargs)