##############################################################################
#    This file is part of mdciao.
#    
#    Copyright 2025 Charité Universitätsmedizin Berlin and the Authors
#
#    Authors: Guillermo Pérez-Hernandez
#    Contributors:
#
#    mdciao is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    mdciao is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with mdciao.  If not, see <https://www.gnu.org/licenses/>.
##############################################################################


my_frag_colors=[
         'magenta',
         'yellow',
         'lime',
         'maroon',
         'navy',
         'olive',
         'orange',
         'purple',
         'teal',
]

from os import path as _path, mkdir as _mkdir
import numpy as _np
import mdtraj as _md
from matplotlib import pyplot as _plt, rcParams as _rcParams, colors as _mplcolors

from pandas import DataFrame as _DF
from pandas import ExcelWriter as _ExcelWriter

from inspect import signature as _signature
from fnmatch import filter as _filter
from textwrap import wrap as _twrap
from itertools import product as _iterpd

import mdciao.contacts as _mdcctcs
import mdciao.fragments as _mdcfrg
import mdciao.nomenclature as _mdcnomenc
import mdciao.pdb as _mdcpdb
import mdciao.sites as _mdcsites
import mdciao.plots as _mdcplots

import mdciao.utils as _mdcu
from mdciao.utils.str_and_dict import _kwargs_subs

def _offer_to_create_dir(output_dir):
    r"""
    Offer to create a directory if it does not
    exist. Does nothing if it already exists

    Parameters
    ----------
    output_dir : str

    Returns
    -------

    """
    if not _path.isdir(output_dir):
        answer = input("\nThe directory '%s' does not exist. Create it on the fly [y/n]?\nDefault [y]: " % output_dir)
        if len(answer) == 0 or answer.lower().startswith("y"):
            _mkdir(output_dir)
        else:
            print("Stopping. Please check your variable 'output_dir' and try again")
            return

def _parse_consensus_option(option, consensus_type,
                            top, fragments,
                            return_Labeler=False,
                            accept_guess=False,
                            **LabelerConsensus_kwargs):
    r"""

    Frankenstein method to hide complexity away fom the command-line tools
    while making them usable at the API-level

    Internally, it instantiates a :obj:`LabelerConsensus` object to use
    its :obj:`LabelerConsensus.top2labels` method
    
    A guess is performed on-the-fly using :obj:`guess_by_nomenclature`
    to better align :obj:`top` to the :obj:`LabelerConsensus`.

    Parameters
    ----------
    option : the option that was passed as argument.
        There's four usecases:
         * None, str(None)
           Nothing happens, a residx2conlab map
           full of Nones is returned
         * str
          The needed identifier to instantiate an
          `LabelerGPCR`, `LabelerCGN` or `LabelerKLIFS` object.
         * a `LabelerConsensus` object
          An already instantiated `LabelerGPCR`,
          `LabelerCGN` or `LabelerKLIFS` object.
          The method then does nothing. Usecase are repeated
          calls to any of the methods in `mdciao.cli`
          without each call instantiating its own `LabelerConsensus`
        * iterable
          An  iterable (list, dict, array) mapping
          residue indices of 'top` to consensus labels.
          Has to have len(option)==top.n_residues
          This allows the user to create the map
          with LabelerConsensus.top2labels() beforehand
    consensus_type : str
        Either "CGN", "GPCR", "KLIFS"
    top : :obj:`mdtraj.Topology`
    fragments : iterable of iterables of ints
        How the :obj:`top` is fragmented. Helps
        to identify what part of :obj:`top`
        to align to the consensus sequence
    return_Labeler : bool, default is False
        Whether to return the object itself. Won't
        have an affect if the `option` was an
        iterable (4th case)
    accept_guess : bool, default is False
        Accept the guess generated by
        :obj:`guess_by_nomenclature` without asking
        questions
    LabelerConsensus_kwargs : opt
        Keyword arguments of for the :obj:`LabelerConsensus`

    Returns
    -------
    map, LC

    """
    if isinstance(option, str) or option is None:
        if str(option).lower() == 'none':
            map_out = [None for __ in range(top.n_residues)]
            LC_out = None
        else:
            LC_out = {"GPCR": _mdcnomenc.LabelerGPCR,
                      "CGN": _mdcnomenc.LabelerCGN,
                      "KLIFS": _mdcnomenc.LabelerKLIFS}[consensus_type](option, **LabelerConsensus_kwargs)

    #todo add a class check here instead of failing later on
    else:
        LC_out = option

    if isinstance(LC_out, _mdcnomenc.LabelerConsensus):
        answer = _mdcnomenc.guess_by_nomenclature(LC_out, top,
                                                  fragments=fragments, nomenclature_name=consensus_type,
                                                  accept_guess=accept_guess,
                                                  # verbose=True
                                                  )
        if answer is None:
            print("No fragments belonging to the %s nomenclature could be guessed based\n"
                  "on your fragments, this might be a weird case."%consensus_type)
            map_out = [None]*top.n_residues
        else:
            restrict_to_residxs = _np.hstack([fragments[ii] for ii in answer])
            map_out = LC_out.top2labels(top,
                                        min_seqID_rate=0,  # We need give-up the re-guessing here,
                                        # because explicitely done it before with an option for
                                        # interactivity
                                        restrict_to_residxs=restrict_to_residxs,
                                        autofill_consensus=True,
                                        fragments=fragments,
                                        #    verbose=True,
                                        )
        print()

    elif _mdcu.lists.is_iterable(LC_out):
        assert len(LC_out)==top.n_residues, ValueError("If a mapping residue index -> consensus label is passed, it has to have be of length(map) == `top.n_residues`, but I got %u != %u"%(len(LC_out), top.n_residues))
        map_out, LC_out = LC_out, None

    if not return_Labeler:
        return map_out
    else:
        return map_out, LC_out

#TODO test
#TODO document
def _parse_consensus_options_and_return_fragment_defs(option_dict, top,
                                                      fragments_as_residue_idxs,
                                                      accept_guess=False,
                                                      save_nomenclature_files=False,
                                                      verbose=True):
    r"""

    The consensus frags will be inferred
    from true ConsensusLabelers objects or from lists of consensus labels

    Parameters
    ----------
    option_dict
    top
    fragments_as_residue_idxs
    accept_guess
    save_nomenclature_files
    verbose

    Returns
    -------

    """
    consensus_frags, consensus_maps, consensus_labelers = {}, {}, {}
    for key, option in option_dict.items():
        map_CL, CL = _parse_consensus_option(option, key, top, fragments_as_residue_idxs,
                                           return_Labeler=True,
                                           accept_guess=accept_guess,
                                           write_to_disk=save_nomenclature_files)
        consensus_maps[key] = map_CL
        if CL is not None:
            consensus_labelers[key] = CL
            empty_map = all([lab is None for lab in map_CL])
            if not empty_map:
                if verbose or not accept_guess:
                    print("Mapping the %s fragments onto your topology:"%key)
                consensus_frags.update(CL.top2frags(top,
                                                  input_dataframe=CL.most_recent_alignment,
                                                  fragments=fragments_as_residue_idxs,
                                                  verbose=verbose or not accept_guess))
                if not accept_guess:
                    input("Hit enter to continue!\n")
        elif not all([str(val).lower()=="none" for val in map_CL]):
            consensus_frags.update(_mdcnomenc.conlabs2confrags(map_CL, replace_GPCR_frags=[key=="GPCR"]))
    _mdcu.lists.assert_no_intersection(list(consensus_frags.values()),"consensus fragment")

    return consensus_frags, consensus_maps, consensus_labelers

def _parse_fragment_naming_options(fragment_names, fragments):
    r"""
    Helper method for the CLTs to understand what/how the user wants
    the fragments to be named

    Parameters
    ----------
    fragment_names : None, str or list
        If str, we assume it comes directly from the
        command line option --fragment_names,
        see :obj:`parsers._parser_add_fragment_names. Can be different
        things:
        * "auto" : fragment names will be named frag0,frag1,frag2 ... as needed
        * None, "None","none": fragment names will be None
        * comma-separated values, with as many values
        as fragments are in `fragments`
        If list, we do nothing (for compatibility with API use of CLI tools)
    fragments: list
        existing fragment definitions (iterables of residue indices)
         to apply the :obj:`fragment_names` to.
         Typically, :obj:`fragments` come from a call to :obj:`get_fragments`

    Returns
    -------
    fragment_names : list of strings
    """
    #TODO fragment naming should be handled at the object level?

    if isinstance(fragment_names,(list, _np.ndarray)):
        assert len(fragment_names) == len(
            fragments), "Mismatch between nr. fragments and fragment names %s vs %s (%s)" % (
            len(fragments), len(fragment_names), fragment_names)
        return fragment_names
    if fragment_names == 'auto':
        fragment_names = ['frag%u' % ii for ii in range(len(fragments))]
    elif str(fragment_names).lower()=="none" or fragment_names=="":
        fragment_names = [None for __ in fragments]
    else:
        #TODO get rid of this danger nonsens
        assert isinstance(fragment_names, str), "Argument --names invalid: %s" % fragment_names
        fragment_names = [ff.strip(" ") for ff in fragment_names.split(",")]
        assert len(fragment_names) == len(
            fragments), "Mismatch between nr. fragments and fragment names %s vs %s (%s)" % (
            len(fragments), len(fragment_names), fragment_names)
        return fragment_names


    return fragment_names

# TODO mix and match with the color options of flareplots
def _parse_coloring_options(color_option, n,
                            default_color="tab:blue",
                            color_cycle=my_frag_colors
                            ):
    r"""
    Helper function to parse user input and return a color list

    Parameters
    ----------
    color_option: str, list, bool, or None
       * str : return a list of len n with this color as each entry
       * list : assert len(list)>=len(n) and return it the first n-entries of it
       * bool : True  : create a list of len n that repeats :obj:`color_cycle`
                        as needed
       * bool : False : create a list of len n with :obj:`default_color` as entries
                        (same as :obj:`color_option` had equal to :obj:`default_color`)
       * None : same as false
    n : int
        Wanted number of colors
    default_color: str
        Any color matplotlib understands
    color_cycle: iterable of matplotlib colors

    Returns
    -------
    colors

    """
    assert isinstance(color_cycle,list)

    if str(color_option).lower()=="none":
        color_option = False

    if isinstance(color_option, bool):
        if not color_option:
            colors = [default_color for __ in range(n)]
        else:
            vec_idxs = _np.mod(_np.arange(n), len(my_frag_colors))
            colors = _np.array(color_cycle)[vec_idxs].tolist()
    elif isinstance(color_option, str):
        color_option = color_option.split(",")
        if len(color_option)==1:
            colors = [color_option[0] for __ in range(n)]
    elif isinstance(color_option,list):
        if len(color_option)<n:
            raise ValueError("Not enough input values %s for expected output of size n %u"%(color_option,n))
        else:
            colors = color_option[:n]

    return colors

# TODO Consider putting the figure instantiation also here
def _manage_timedep_ploting_and_saving_options(ctc_grp,
                                               fn,
                                               myfig,
                                               plot_timedep=True,
                                               separate_N_ctcs=False,
                                               title=None,
                                               savefigs=True,
                                               savetrajs=False,
                                               ):
    r"""
    Towards a common function for saving/managing timedep files
    for neighborhoods, sites, and interfaces

    Parameters
    ----------
    ctc_grp : :obj:`mdciao.contacts.ContactGroup`
    fn : :obj:`mdciao.utils.str_and_dict.FilenameGenerator`
    myfig :obj:`matplotlib.figure.Figure`
    plot_timedep : bool, default is True
    separate_N_ctcs : bool, default is True
    t_unit : str or None, default is None
    savefigs : bool, default is True
    savetrajs : bool, default is False
    Returns
    -------

    """
    lastname = ""
    # TODO manage interface and sites appropiately
    if ctc_grp.is_neighborhood:
        lastname = "%s"%ctc_grp.anchor_res_and_fragment_str.replace('*', "")

    if title is None:
        title = fn.output_desc #TODO consider using lastname
    fname_timedep = ('%s.%s.time_trace@%2.1f_Ang.%s' % (fn.output_desc,
                                                        lastname,
                                                        fn.ctc_cutoff_Ang,
                                                        fn.graphic_ext)).replace("..", ".")

    fname_N_ctcs = ('%s.%s.time_trace@%2.1f_Ang.N_ctcs.%s' % (fn.output_desc,
                                                              lastname,
                                                              fn.ctc_cutoff_Ang,
                                                              fn.graphic_ext)).replace("..", ".")


    # Differentiate the type of figures we can have
    if len(myfig) == 0:
        fnames = []
        print("No figures of time-traces were produced because only 1 frame was provided")
    elif len(myfig) == 1:
        if plot_timedep:
            fnames = [fname_timedep]
        else:
            fnames = [fname_N_ctcs]
    elif len(myfig) == 2:
        fnames = [fname_timedep, fname_N_ctcs]

    if savefigs:
        for iname, ifig in zip(fnames, myfig):
            fname = _path.join(fn.output_dir, iname)
            ifig.axes[0].set_title("%s" % title) # TODO consider firstname lastname
            ifig.savefig(fname, bbox_inches="tight", dpi=fn.graphic_dpi)
            #_plt.close(ifig)
            print(fname)

    # even if no figures were produced or saved, we can still save the trajs
    if savetrajs:
        ctc_grp.save_trajs(fn.output_desc, fn.table_ext, fn.output_dir, t_unit=fn.t_unit, verbose=True)
        if separate_N_ctcs:
            ctc_grp.save_trajs(fn.output_desc, fn.table_ext, fn.output_dir, t_unit=fn.t_unit, verbose=True,
                               ctc_cutoff_Ang=fn.ctc_cutoff_Ang)
        print()

def _color_schemes(istr):
    r"""
    Choose or generate a color scheme

    Parameters
    ----------
    istr : str
        * colorname,
        * csv colorname list,
        * color scheme name, currently
         * "P" : ["red", "purple", "gold", "darkorange"]
         * "H" : ["m", "darkgreen", "darkorange", "navy"],
        * "auto" :obj:`matplotlib` prop_cycle

    Returns
    -------
        list of colorlike strings

    """
    if "," in istr:
        return istr.split(",")
    elif _mplcolors.is_color_like(istr):
        return [istr]
    else:
        return {"p": ["red", "purple", "gold", "darkorange"],
                "h": ["m", "darkgreen", "darkorange", "navy"],
                "auto":  _plt.rcParams['axes.prop_cycle'].by_key()["color"]}[str(istr).lower()]

def _load_any_geom(geom):
    r"""
    Helper method for command-line-tools to create a :obj:`~mdtraj.Trajectory`
    from either filenames or :obj:`mdtraj.Trajectory` (i.e. do nothing)
    Parameters
    ----------
    geom : str or :obj:`~mdtraj.Trajectory`

    Returns
    -------
    outgeom : :obj:`~mdtraj.Trajectory`
    """
    if isinstance(geom, str):
        outgeom = _md.load(geom)
    else:
        outgeom = geom

    return outgeom

def _trajsNtop2xtcsNrefgeom(trajectories,topology):
    r"""
    Inform about trajs and load necessary tops in different scenarios

    Parameters
    ----------
    trajectories: check get_trajectories_from_input
    topology : str, top

    Returns
    -------
    xtcs, refgeom
    xtcs : whatever get_trajectories_from_input returns
    refgeom : :obj:`mdtraj.Trajectory` object

    """
    # Inform about trajectories
    xtcs = _mdcu.str_and_dict.get_trajectories_from_input(trajectories)
    if topology is None:
        # TODO in case the xtc[0] is a pdb/grofile, it will be read one more time later
        refgeom = _load_any_geom(xtcs[0])[0]
    else:
        refgeom = _load_any_geom(topology)
    return xtcs,refgeom

def _fragment_overview(a,labtype):
    r"""
    provide the CLTs GPCR_overview and CGN_overview and KLIFS_overview

    Parameters
    ----------
    a : :obj:`argparse.Namespace` object
        Contains the arguments used by the user
    labtype : srt, "GPCR", "CGN", "KLIFS"
        lets the code know which :obj:`LabelerConsensus` to use

    Returns
    -------
    None
    """

    class_dict = {"GPCR":_mdcnomenc.LabelerGPCR,
                  "KLIFS":_mdcnomenc.LabelerKLIFS,
                  "CGN": _mdcnomenc.LabelerCGN}


    if labtype in class_dict.keys():
        val = a.input_
        if _path.exists(val):
            format = "%s"
        else:
            if labtype == "KLIFS":
                format = _signature(class_dict[labtype]).parameters["format"].default
            else:
                format = _signature(class_dict[labtype].__bases__[0]).parameters["format"].default
        obj = class_dict[labtype](val,
                                  format=format,
                                  write_to_disk=a.write_to_disk)
    else:
        raise ValueError("Don't know the consensus type %s, only 'GPCR', 'CGN', 'KLIFS'"%labtype)

    if a.topology is not None:
        top = _md.load(a.topology).top
        map_conlab = obj.top2labels(top,
                                    autofill_consensus=a.fill_gaps)
        obj.top2frags(top,input_dataframe=obj.most_recent_alignment)
        _mdcu.residue_and_atom.parse_and_list_AAs_input(a.AAs, top, map_conlab)
        if str(a.labels).lower() != "none":
            labels = [aa.strip(" ") for aa in a.labels.split(",")]
            conlab2residx = obj.conlab2residx(top, map=map_conlab)
            for lab in labels:
                for match in _filter(list(conlab2residx.keys()),lab):
                    idx = conlab2residx[match]
                    rr = top.residue(idx)
                    print(idx,rr, map_conlab[idx])

        if a.print_conlab:
            for ii, ilab in enumerate(map_conlab):
                print(ii, top.residue(ii), ilab)
    else:
        for key, frag in obj.fragments.items():
            print("fragment %s with %u AAs:"%(key, len(frag)))

            idf = _DF.from_dict({"residue"  : frag,
                                 "consensus": obj.fragments_as_conlabs[key]})

            n_warp = 10
            if len(idf)/n_warp > 5:
                n_warp = _np.floor(len(idf)/6).astype(int)
            textblocks = [['%-25s'%iline for iline in idf.loc[idxs].to_string().splitlines()] for idxs in _mdcu.lists.re_warp(_np.arange(len(idf)),n_warp)]
            for ii in range(len(textblocks[0])):
                line = ''
                for tb in textblocks:
                    try:
                        line += ' | %s'%tb[ii]
                    except IndexError as E:
                        pass
                print(line)
            
def residue_neighborhoods(residues,
                          trajectories,
                          topology=None,
                          res_idxs=False,
                          ctc_cutoff_Ang=4.5,
                          stride=1,
                          ctc_control=6,
                          n_nearest=4,
                          scheme="closest-heavy",
                          min_freq=0.01,
                          chunksize_in_frames=2000,
                          n_smooth_hw=0,
                          sort=True,
                          pbc=True,
                          ylim_Ang=15,
                          fragments="lig_resSeq+",
                          fragment_names="auto",
                          fragment_colors=None,
                          graphic_ext=".pdf",
                          table_ext=".dat",
                          GPCR_UniProt=None,
                          CGN_UniProt=None,
                          KLIFS_string=None,
                          output_dir='.',
                          output_desc='neighborhood',
                          t_unit='ns',
                          curve_color="auto",
                          background=True,
                          graphic_dpi=150,
                          short_AA_names=False,
                          allow_same_fragment_ctcs=True,
                          save_nomenclature_files=False,
                          plot_timedep=True,
                          n_cols=4,
                          distro=False,
                          n_jobs=1,
                          separate_N_ctcs=False,
                          accept_guess=False,
                          switch_off_Ang=None,
                          plot_atomtypes=False,
                          no_disk=False,
                          savefigs=True,
                          savetabs=True,
                          savetrajs=False,
                          figures=True,
                          naive_bonds=False,
                          progressbar=True,
                          ):
    r"""Per-residue neighborhoods based on contact frequencies between pairs
    of residues.

    A neighborhood is a :obj:`mdciao.contacts.ContactGroup`-object containing a set of
    :obj:`mdciao.contacts.ContactPair`-objects with a shared residue,
    called the `anchor_residue`.

    The contact frequencies will be printed, plotted and saved. The residue-residue
    distance time-traces used for their computation will be also returned

    Note
    ----
    The time-independent figures (e.g. "neighborhood.overall@3.5_Ang.pdf") are always shown
    whereas the time-dependent figures (e.g. "neighborhood.GDP395.time_trace@3.5_Ang.pdf")
    are never shown, because the number of time-traces becomes very high very quickly.
    It's easier to look at them with an outside viewer.


    The user may be prompted when necessary,
    although this behaviour can be turned off with :obj:`accept_guess`

    Input can be from disk and/or from memory (see below).

    Can be parallelized up to the number of used trajectories.

    Many other optional parameters are exposed to allow fine-tuning of the
    computing, plotting, printing, and saving. Additional information can be regarding nomenclature,
    fragmentation heuristics and/or naming and or/coloring, residue labeling, time-trace
    averaging, data-streaming,

    Parameters
    ----------
    residues : int, iterable of ints or str
        The residue(s) for which the neighborhood will be computed.
        This input is pretty flexible wrt to strings and numbers,
        which are interpreted as sequence indices unless
        :obj:`res_idxs` is True
        Valid inputs are:
         * residues = [1,10,11,12]
         * residues = '1,10,11,12'
         * residues = '1,10-12'
         * residues = [1]
         * residues = 1
         * residues = '1'
         * residues = '1,10-12,GLU*,GDP*,E30'
         Please refer to :obj:`mdciao.utils.residue_and_atom.rangeexpand_residues2residxs`
         for more info
    trajectories : str, :obj:`mdtraj.Trajectory` or lists thereof
        The MD-trajectories to calculate the frequencies from.
        This input is pretty flexible. For more info check
        :obj:`mdciao.utils.str_and_dict.get_trajectories_from_input`.
        Accepted values are:
         * pattern, e.g. "*.ext"
         * one string containing a filename
         * list of filenames
         * one :obj:`mdtraj.Trajectory` object
         * list of :obj:`mdtraj.Trajectory` objects
         * list mixing filenames and :obj:`mdtraj.Trajectory` objects
    topology : str or :obj:`~mdtraj.Trajectory`, default is None
        The topology associated with the :obj:`trajectories`
        If None, the topology of the first :obj:`trajectory` will
        be used, i.e. when no :obj:`topology` is passed, the first
        :obj:`trajectory` has to be either a .gro or .pdb file, or
        an :obj:`~mdtraj.Trajectory` object
    Other Parameters
    ----------------
    res_idxs : bool, default is False
        Whether the indices of :obj:`residues` should be understood as
         * zero-indexed, residue serial indices or
         * residue sequence, e.g. 30 in GLU30, this is called 'resSeq'
         in an :obj:`mdtraj.core.Residue`-object
    ctc_cutoff_Ang : float, default is 4.5
        Any residue-residue distance is considered a contact
        if d<=ctc_cutoff_Ang
    stride : int, default is 1
        Stride the input data by this number of frames
    ctc_control : int or float, default is 6
        Control the number of reported contacts. Can be an
        integer (keep the first n contacts) or a float
        representing a fraction [0,1] of the total number of
        contacts. Default is 6.
    n_nearest : int, default is 4
        Exclude these many bonded neighbors for each residue
    min_freq : float, default is 0.01
        Do not show frequencies smaller than this. If you
        notice the output being truncated at values too far
        above this value, you need to increase the :obj:`ctc_control`
        parameter.
    scheme : str, default is 'closest-heavy'
        Type of scheme for computing distance between
        residues. Choices are {'ca', 'closest', 'closest-
        heavy', 'sidechain', 'sidechain-heavy'}. See
        :obj:`mdtraj.compute_distances` documentation for more info
    chunksize_in_frames : int, default is 2000
        Stream through the trajectories in chunks
        of this size.
    n_smooth_hw: int, default is 0
        Plots of the time-traces will be smoothed using a window
        of 2*n_smooth_hw
    sort : bool, default is True
        Sort the input :obj:`residues` according to their indices
    pbc : bool, default is True
        Use periodic boundary conditions, i.e. the minimum image convention,
        to compute distances.
    ylim_Ang : float, default is 15
        Limit in Angstrom of the y-axis of the time-traces.
        Default is 15. Switch to any other float or 'auto'
        for automatic scaling
    fragments : str, list, None, default is "lig_resSeq+"
        Topology fragments. There exist several input modes:

        * Name of a fragmentation heuristic, e.g.
          "lig_resSeq+", which is the default of
          and usually yields good results. See
          :obj:`mdciao.fragments.get_fragments`
          for more info on defaults and other heuristics.
        * List of len N that can mix different possibilities:

          * iterable of integers (lists or np.arrays, e.g. np.arange(20,30)
          * ranges expressed as integer strings, "20-30"
          * ranges expressed as residue descriptors ["GLU30-LEU40"]

        * "consensus" : use things like "TM*" or "G.H*", i.e.
          GPCR or CGN-sub-subunit labels.

        Numeric expressions are interpreted as zero-indexed and unique
        residue serial indices, i.e. 30-40 does not necessarily equate
        "GLU30-LEU40" unless serial and sequence index coincide.
        If there's more than one "GLU30", the user gets asked to
        disambiguate. The resulting fragments need not cover
        all of the topology, they only need to not overlap.
    fragment_names : string, list of strings, or None. Default is "auto".
        The default "auto" names fragments "frag0", "frag1" up
        however many fragments there are. If string and not "auto",
        it has to be a list of comma-separated values, with enough
        names for however many fragments there are.
        mdciao will use :obj:`mdciao.utils.str_and_dict.replace4latex`
        to try to generate LaTeX expressions from stuff like "Galpha".
        You can use None or "" to avoid using fragment names.
    fragment_colors : None, boolean or list, default is None
        Assign colors to fragments. These colors will be used
        to color-code the frequency bars. If True, colors
        will be automatically selected, otherwise picked
        from the list. Use with cautions, plots
        get shrill quickly
    graphic_ext : str, default is ".pdf"
        The extension (=format) of the saved figures
    table_ext : str, default is ".dat"
        The extension (=format) of the saved tables
    GPCR_UniProt : str or :obj:`mdciao.nomenclature.LabelerGPCR`, default is None
        For GPCR nomenclature. If str, e.g. "adrb2_human".
        will try to locate a local filename or do a web lookup in the GPCRdb.
        If :obj:`mdciao.nomenclature.LabelerGPCR`, use this object directly
        (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references.
        Please note the difference between UniProt Accession Code
        and UniProt entry name
        as explained `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    CGN_UniProt : str or :obj:`mdciao.nomenclature.LabelerCGN`, default is None
        For CGN (G-alpha Numbering definitions) nomenclature. If str, e.g. "gnas2_human",
        try to locate local filenames "gnas2_human.xlsx" or do web lookups
        in the GPCRdb. If :obj:`mdciao.nomenclature.LabelerCGN`, use this object directly
        (allows for object re-use when in API mode)
        See :obj:`mdciao.nomenclature` for more info and references.
    KLIFS_string : str or :obj:`mdciao.nomenclature.LabelerKLIFS`, default is None
        String for kinase KLIFS nomenclature. First, try to locate a local
        file that directly has the `KLIFS_string` as a name. If that fails, then
        combine the filename-format expected by :obj:`mdciao.nomenclature.LabelerKLIFS`
        with `KLIFS_string` to construct a filename and check again.
        If that doesn't work, then go online to contact the KLIFS database.

        For the online lookup in the KLIFS database, the string
        has to be formatted "key:value", which ultimately leads to a given KLIFS entry.
        Acceptable keys and values for `KLIFS_string` are:
            * "UniProtAC", e.g. "UniProtAC:P31751"
            * "kinase_ID", e.g. "kinase_ID:2"
            * "structure_ID", e.g. "structure_ID:1904", e.g. "P31751",
        Please check the documentation on :obj:`mdciao.nomenclature.LabelerKLIFS`
        for a more elaborate explanation on when to pick one of these key:value
        pairs.

        Finally, if `KLIFS_string` is an :obj:`mdciao.nomenclature.LabelerKLIFS`,
        use this object directly (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references. Alos, please note
        the difference between UniProt Accession Code
        and UniProt entry name as explained
        `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    output_dir : str, default is '.'
        directory to which the results are written.
    output_desc : str, default is 'neighborhood'
        Descriptor for output files.
    t_unit : str, default is 'ns'
        Unit used for the temporal axis.
    curve_color : str, default is 'auto'
        Type of color used for the curves. Alternatives are "P" or "H"
    background : bool, or color-like, (str, hex, rgb), default is True
        When smoothing, the original curve can
        appear in the background in different colors
        * True:  use a fainted version of :obj:`color`
        * False: don't plot any background
        * color-like: use this color for the background,
          can be: str, hex, rgba, anything
          `matplotlib.pyplot.colors` understands
    graphic_dpi : int, default is 150
        Dots per Inch (DPI) of the graphic output. Only has
        an effect for bitmap outputs.
    short_AA_names : bool, default is False
        Use one-letter aminoacid names when possible, e.g.
        K145 insted of Lys145.
    allow_same_fragment_ctcs : bool, default is True
        Allow contacts whithin the same fragment.
    save_nomenclature_files : bool, default is False
        Save available nomenclature definitions to disk so
        that they can be accessed locally in later uses.
    plot_timedep : bool, default is True
        Plot and save time-traces of the contacts
    n_cols : int, default is 4
        number of columns of the overall plot.
    distro : bool, default is False
        Plot distance distributions instead of contact bar
        plots
    n_jobs : int, default is 1
        Number of processors to use. The parallelization is
        done over trajectories and not over contacts, beyond
        n_jobs>n_trajs parallelization will not have any
        effect.
    separate_N_ctcs : bool, default is False
        Separate the plot with the total number contacts
        from the time-trace plot.
    accept_guess : bool, default is False
        Accept mdciao's guesses regarding fragment
        identification using nomenclature labels
    switch_off_Ang : NoneType, default is None
        Use a linear switchoff instead of a crisp one.
    plot_atomtypes : bool, default is False
        Add the atom-types to the frequency bars by
        'hatching' them. '--' is sidechain-sidechain '|' is
        backbone-backbone '\' is backbone-sidechain '/' is
        sidechain-backbone. See Fig XX for an example
    savefigs : bool, default is True
        Save the figures
    savetabs : bool, default is True
        Save the frequency tables
    savetrajs : bool, default is False
        Save the timetraces
    no_disk : bool, default is False
        If True, don't save any files at all:
        figs, tables, trajs, nomenclature
    figures : bool, default is True
        Draw figures
    naive_bonds : bool, default is False
        If :obj:`top` doesn't automatically yield
        a list bonds between residues, build naive
        (=linear) bonds using :obj:`mdciao.utils.bonds.top2residue_bond_matrix_naive`
        These bonds are needed to exclude bonded neighbors
        using :obj:`n_nearest`
    progressbar : bool, default is True
        Report progress as the computation advances.

    Returns
    -------
    neighborhoods : dict
        Keyed by unique, zero-indexed residue indices, valued with
        :obj:`mdciao.contacts.ContactGroup` objects
        If no contacts have been found, returns None.

    """

    # Input control residues
    if residues is None:
        print("You have to provide some residue input via the --residues option")
        return None

    _offer_to_create_dir(output_dir)
    xtcs, refgeom = _trajsNtop2xtcsNrefgeom(trajectories, topology)
    fn = _mdcu.str_and_dict.FilenameGenerator(output_desc, ctc_cutoff_Ang, output_dir,
                                              graphic_ext, table_ext, graphic_dpi, t_unit)
    if no_disk:
        savetrajs = False
        savefigs  = False
        savetabs = False
        save_nomenclature_files = False

    # More input control
    ylim_Ang=float(ylim_Ang)
    print("Will compute contact frequencies for (%u items):\n%s"
          "\n with a stride of %u frames" % (len(xtcs),_mdcu.str_and_dict.inform_about_trajectories(xtcs, only_show_first_and_last=15), stride))

    fragments_as_residue_idxs = _mdcfrg.fragments._fragments_strings_to_fragments(fragments, refgeom.top, verbose=True)[0]
    fragment_names = _parse_fragment_naming_options(fragment_names, fragments_as_residue_idxs)
    fragment_colors = _parse_coloring_options(fragment_colors,len(fragment_names))


    mid_string = "\nWill compute neighborhoods for the residues\n" \
                 "%s\nexcluding %u nearest neighbors" \
                 "\n" % (residues,n_nearest)
    res_idxs_list, consensus_maps, consensus_frags = _res_resolver(residues, refgeom.top, fragments_as_residue_idxs,
                                                                   midstring=mid_string, GPCR_UniProt=GPCR_UniProt,
                                                                   CGN_UniProt=CGN_UniProt, KLIFS_string=KLIFS_string,
                                                                   save_nomenclature_files=save_nomenclature_files,
                                                                   accept_guess=accept_guess,
                                                                   fragment_names=fragment_names,
                                                                   interpret_as_res_idxs=res_idxs, sort=sort)

    top2confrag = _np.full(refgeom.top.n_residues, None)
    for key, val in consensus_frags.items():
        top2confrag[val] = key

    # Create a neighborlist
    if n_nearest == 0:
        nl = [[]]*refgeom.top.n_residues
    else:
        try:
            nl = _mdcu.bonds.bonded_neighborlist_from_top(refgeom.top, n=n_nearest)
        except ValueError as e:
            if naive_bonds:
                print("Creating a naive linear bond-list: residue 'n' will be considered bonded to its \n"
                      "adjacent 'n+1' and 'n-1' residues. Additionally, these bond-breaks will enforced:")
                print(" * between non-protein residues")
                print(" * between the above '%s' fragment definitions"%fragments)
                mat = _mdcu.bonds.top2residue_bond_matrix_naive(refgeom.top, fragments=fragments_as_residue_idxs)
                nl = _mdcu.bonds.neighborlists_from_adjacency_matrix(mat, n_nearest)
            else:
                print(e)
                raise ValueError("You can create a naive bond-list between adjacent residues with the\n"
                                 "option naive_bonds. This might lead to artifacts, use this option at your own risk!")


    # Use it to prune the contact indices
    ctc_idxs = _np.vstack(
        [[_np.sort([val, ii]) for ii in range(refgeom.top.n_residues) if ii not in nl[val] and ii != val] for val in
         res_idxs_list])

    # Prune duplicates in the ctc_idxs (the res_idxs_list with itself generates duplicates)
    assert (ctc_idxs[:,0]<ctc_idxs[:,1]).all()
    ctc_idxs = _np.unique(ctc_idxs, axis=0)

    # Can we have same-fragment contacts
    if not allow_same_fragment_ctcs:
        fragment_idxs = [[_mdcu.lists.in_what_fragment(idx, fragments_as_residue_idxs) for idx in pair] for pair in ctc_idxs]
        ctc_idxs = [ctc_idxs[ii] for (ii,pair) in enumerate(fragment_idxs) if pair[0]!=pair[1]]

    print(f"\nPerforming a first pass on {len(ctc_idxs)} residue pairs to compute lower bounds "
          f"on residue-residue distances via residue-COM distances:")
    lb_cutoff_buffer_Ang = 2.5
    idx_of_lower_lower_bounds = _mdcctcs.trajs2lower_bounds(xtcs, refgeom.top, ctc_idxs,
                                                            stride=stride,
                                                            chunksize=chunksize_in_frames,
                                                            n_jobs=n_jobs,
                                                            progressbar=progressbar,
                                                            lb_cutoff_Ang=ctc_cutoff_Ang + lb_cutoff_buffer_Ang,
                                                            periodic=pbc,
                                                            )
    idx_of_lower_lower_bounds = _np.unique(_np.hstack(idx_of_lower_lower_bounds))
    ctc_idxs_small = _np.array(ctc_idxs)[idx_of_lower_lower_bounds]
    if len(ctc_idxs_small)==0:
        print("No residues have any neighbors at %2.1f Ang. No output produced." % ctc_cutoff_Ang)
        return {idx : None for idx in res_idxs_list}
    print(f"\nReduced to only {len(ctc_idxs_small)} residue pairs for the computation of actual residue-residue distances:")
    ctcs_trajs, time_arrays, at_pair_trajs = _mdcctcs.trajs2ctcs(xtcs, refgeom.top, ctc_idxs_small, stride=stride,
                                                                 chunksize=chunksize_in_frames,
                                                                 return_times_and_atoms=True,
                                                                 consolidate=False,
                                                                 n_jobs=n_jobs,
                                                                 scheme=scheme,
                                                                 periodic=pbc,
                                                                 progressbar=progressbar,
                                                                 )
    print() # to make sure we don't overwrite output
    actcs = _np.vstack(ctcs_trajs)
    df = _mdcctcs.contacts._data2DataFrame(actcs, ctc_idxs_small, refgeom.top, ctc_cutoff_Ang,
                                           fragments_as_residue_idxs, fragment_names,
                                           top2confrag, list(consensus_maps.values()),
                                           keep_max_buffer_Ang=lb_cutoff_buffer_Ang)
    neighborhood_DFs = {}
    for ii in res_idxs_list:
        idf = _mdcctcs.contacts._DataFrame2NeighborhoodDF(df, ii)
        neighborhood_DFs[ii]=idf
        # For debugging you can get an overview of the DF before the CG is created
        #  n_ctcs = _mdcu.lists._get_n_ctcs_from_freqs(ctc_control, idf.freq)[0]
        # _mdcctcs.contacts._contact_fraction_informer(_np.min([n_ctcs, _np.sum(idf.freq>0)]),
        #                                             idf[idf.freq > 0].freq.values, or_frac=.9)
        #_mdcctcs.contacts._prettyprintDF(idf)

    # Create the neighborhoods as groups of ContactPair objects
    neighborhoods = {}
    empty_CGs = []
    for res_idx, idf in neighborhood_DFs.items():
        CPs = []
        n_ctcs = _mdcu.lists._get_n_ctcs_from_freqs(ctc_control, idf.freq, min_freq=min_freq)[0]
        for ii, irow in idf[:n_ctcs].iterrows():
            CPs.append(_mdcctcs.ContactPair([irow.residx1, irow.residx2],
                                            [itraj[:, irow.ctc_idx] for itraj in ctcs_trajs],
                                            time_arrays,
                                            top=refgeom.top,
                                            anchor_residue_idx=res_idx,
                                            consensus_labels=[irow.GRN1, irow.GRN2],
                                            trajs=xtcs,
                                            fragment_idxs=[irow.frag1, irow.frag2],
                                            consensus_fragnames=[irow.GFN1, irow.GFN2],
                                            fragment_names=[irow.fragname1, irow.fragname2],
                                            fragment_colors=[fragment_colors[irow.frag1], fragment_colors[irow.frag2]],
                                            atom_pair_trajs=[itraj[:, [irow.ctc_idx * 2, irow.ctc_idx * 2 + 1]] for
                                                             itraj in
                                                             at_pair_trajs],
                                            ))
        try:
            neighborhoods[res_idx] = _mdcctcs.ContactGroup(CPs, neighbors_excluded=n_nearest,
                                                           max_cutoff_Ang=ctc_cutoff_Ang)
            print()
            print(f"{neighborhoods[res_idx].anchor_res_and_fragment_str_short}:")
            _mdcctcs.contacts._contact_fraction_informer(len(CPs),idf[idf.freq > 0].freq.values, ctc_cutoff_Ang, or_frac=.9)
            ndf = neighborhoods[res_idx].frequency_dataframe(ctc_cutoff_Ang)
            ndf.index +=1
            print(ndf.round({"freq": 2, "sum": 2}).to_string(justify="center"))

        except NotImplementedError as e:
            print(e)
            empty_CGs.append(res_idx)
            neighborhoods[res_idx] = None
    if len(empty_CGs) == len(neighborhood_DFs):
        print("No residues have any neighbors at %2.1f Ang. No output produced." % ctc_cutoff_Ang)
        return
    elif len(empty_CGs)>0:
        print("The following residues have no neighbors at %2.1f Ang, their frequency histograms will be empty"%ctc_cutoff_Ang)
        print("\n".join([str(refgeom.top.residue(ii)) for ii in empty_CGs]))


    if any([savetabs,savefigs,savetrajs]):
        print("The following files have been created:")

    if figures:
        overall_fig = _mdcplots.CG_panels(n_cols, neighborhoods, ctc_cutoff_Ang,
                                  distro=distro,
                                  short_AA_names=short_AA_names,
                                  plot_atomtypes=plot_atomtypes,
                                  switch_off_Ang=switch_off_Ang)
        if savefigs:
            overall_fig.savefig(fn.fullpath_overall_fig, dpi=graphic_dpi)
            print(fn.fullpath_overall_fig)

    neighborhoods = {key:val for key, val in neighborhoods.items() if val is not None}
    # TODO undecided about this
    # TODO this code is repeated in sites...can we abstract this oafa?
    if savetabs:
        for CG in neighborhoods.values():
            fname = fn.fname_per_residue_table(CG.anchor_res_and_fragment_str)
            CG.frequency_table(ctc_cutoff_Ang,
                               fname,
                               switch_off_Ang=switch_off_Ang,
                               write_interface=False,
                               atom_types=True,
                               # AA_format="long",
                               )
            print(fname)

    if figures and (plot_timedep or separate_N_ctcs):
        # TODO make a method out of this to use in all CLTs
        # TODO perhaps use https://github.com/python-attrs/attrs
        # to avoid boilerplate
        # Thi is very ugly
        for CG in neighborhoods.values():
            # TODO this plot_N_ctcs and skip_timedep is very bad, but ATM my only chance without major refactor
            # TODO perhaps it would be better to bury dt in the plotting directly?
            myfig = CG.plot_timedep_ctcs(color_scheme=_color_schemes(curve_color),
                                         ctc_cutoff_Ang=ctc_cutoff_Ang,
                                         switch_off_Ang=switch_off_Ang,
                                         dt=_mdcu.str_and_dict.tunit2tunit["ps"][t_unit],
                                         background=background,
                                         n_smooth_hw=n_smooth_hw,
                                         plot_N_ctcs=True,
                                         pop_N_ctcs=separate_N_ctcs,
                                         shorten_AAs=short_AA_names,
                                         skip_timedep=not plot_timedep,
                                         t_unit=t_unit,
                                         ylim_Ang=ylim_Ang,
                                         )

            # One title for all axes on top
            title = CG.anchor_res_and_fragment_str
            if short_AA_names:
                title = CG.anchor_res_and_fragment_str_short
            title = _mdcu.str_and_dict.latex_superscript_fragments(title)
            if n_nearest >0:
                title += "\n%u nearest bonded neighbors excluded" % (n_nearest)
            _manage_timedep_ploting_and_saving_options(CG, fn, myfig,
                                                       plot_timedep=plot_timedep,
                                                       separate_N_ctcs=separate_N_ctcs,
                                                       title=title,
                                                       savefigs=savefigs,
                                                       savetrajs=savetrajs
                                                       )

    return neighborhoods

def interface(
        trajectories,
        topology=None,
        fragments='lig_resSeq+',
        interface_selection_1=None,
        interface_selection_2=None,
        AA_selection=None,
        GPCR_UniProt="None",
        CGN_UniProt="None",
        KLIFS_string=None,
        chunksize_in_frames=2000,
        ctc_cutoff_Ang=4.5,
        curve_color="auto",
        fragment_names=None,
        graphic_dpi=150,
        graphic_ext=".pdf",
        background=True,
        ctc_control=50,
        n_smooth_hw=0,
        pbc=True,
        output_desc="interface",
        output_dir=".",
        short_AA_names=False,
        stride=1,
        t_unit="ns",
        plot_timedep=True,
        accept_guess=False,
        n_jobs=1,
        n_nearest=0,
        sort_by_av_ctcs=True,
        scheme="closest-heavy",
        separate_N_ctcs=False,
        table_ext="dat",
        title=None,
        min_freq=.10,
        contact_matrix=True,
        cmap='binary',
        flareplot=True,
        save_nomenclature_files=False,
        no_disk=False,
        savefigs=True,
        savetabs=True,
        savetrajs=False,
        figures=True,
        self_interface=False,
        n_repframes=1,
        progressbar=True,
):
    r"""Contact-frequencies between two groups of residues

    The two groups of residues can be defined directly:

     * by using specific residue indices or ranges
     * by using defined molecular fragments,
       e.g. chains defined in the topology or pdb-file.
     * by guessing molecular fragments, using some
       fragmentation heuristic.
     * by guessing molecular fragments, using a consensus
       nomenclature like GPCR, CGN or KLIFS generic residue
       numbering.
    The fragment definition and the fragment selection
    are separate, i.e. there might be six chains but
    one can specify to compute the interface between
    chains [0,1] vs [2,3]. Read more in the
    documentation for `fragments`, `interface_selection_1`,
    and `interface_selection_2`.

    One can further refine the fragment selection
    at the level of single aminoacids (AAs) using
    `AA_selection`. This can fine-tune the residues
    of interest if the fragment definitions are too broad.
    See the docstring for more info.

    Typically, the two groups of residues conforming both
    sides of the interface, also called interface members,
    do not share common residues, because the members
    belong to different molecular units. For example,
    in a receptor--G-protein complex, one partner is
    the receptor and the other partner is the G-protein.

    Note
    ----
    If your definitions of `interface_selection_1` and
    `interface_selection_2` lead to some overlap between
    the interface members (see below), mdciao's default
    is to ignore contact pairs within the same fragment.
    E.g., in the context of a GPCR, computing
    "TM3" vs "TM*" ("TM3" vs "all TMs") won't include
    TM3-TM3 contacts by default. To include these
    (or equivalent) contacts set `self_interface` = True.

    Another example could be computing the interface of the
    C-terminus of a receptor with the entire receptor,
    where it might be useful to include the contacts of
    the C-terminus with itself.

    When using `self_interface` = True, it's advisable to
    increase `n_nearest`, since otherwise neighboring
    residues of the shared set (the TM3-TM3 or the Cterm-Cterm)
    will always appear as formed.

    See the documentation on `fragments`,
    `interface_selection_1`, `interface_selection_2`,
    `AA_selection`, `n_nearest` and `self_interface`.


    Finally, the interface strength, defined as the
    per-residue sum of contacts participating in
    the interface, is written as the
    `bfactor <http://www.wwpdb.org/documentation/file-format-content/format33/sect9.html#ATOM>`_
    in a .pdb file called (for the default `ctc_cutoff_Ang`=4)
    'interface.overall@4.0_Ang.as_bfactors.pdb'. You
    can see an example of how to use this file (e.g. with
    VMD) in the online documentation. The structures, i.e.
    frames, in that .pdb-file are chosen using the
    method :obj:`mdciao.contacts.ContactGroup.repframes` .
    See below the parameter `n_repframes` for more info.

    Parameters
    ----------
    trajectories : str, :obj:`mdtraj.Trajectory` or lists thereof
        The MD-trajectories to calculate the frequencies from.
        This input is pretty flexible. For more info check
        :obj:`mdciao.utils.str_and_dict.get_trajectories_from_input`.
        Accepted values are:
         * pattern, e.g. "*.ext"
         * one string containing a filename
         * list of filenames
         * one :obj:`mdtraj.Trajectory` object
         * list of :obj:`mdtraj.Trajectory` objects
         * list mixing filenames and :obj:`mdtraj.Trajectory` objects
    topology : str or :obj:`~mdtraj.Trajectory`, default is None
        The topology associated with the :obj:`trajectories`
        If None, the topology of the first :obj:`trajectory` will
        be used, i.e. when no :obj:`topology` is passed, the first
        :obj:`trajectory` has to be either a .gro or .pdb file, or
        an :obj:`~mdtraj.Trajectory` object
    fragments : str, list, None, default is "lig_resSeq+"
        How to fragment the topology. Will be used for:

         * tagging of residues, e.g. "GLU30@frag1"
         * disambiguation of residues, e.g. more than one
           "GLU30" exists.
         * grouping of residues in graphical
           representations, e.g. flareplots
         * defining the interface fragments
        There exist several input modes:

         * A single string with the name of a
           fragmentation heuristic, e.g.
           "lig_resSeq+", which is the default
           and usually yields good results. See
           :obj:`mdciao.fragments.get_fragments`
           for more info on defaults and other heuristics.
         * A list of definitions. Each entry of this list can be:

          * an iterable of integers (lists or np.arrays, e.g. np.arange(20,30)
          * a range expressed as an integer string, "20-30"
          * a ranges expressed as residue descriptors "GLU30-LEU40"
          * A special string, "consensus", to use consensus
          subdomains, like "TM1" or "G.H5", as fragment definitions.

        Numeric expressions are interpreted as zero-indexed, unique
        residue serial indices, i.e. 30-40 does not necessarily equate
        "GLU30-LEU40" unless serial and sequence index coincide.
        If there's more than one "GLU30", the user gets asked to
        disambiguate.

        Please note, since fragment definiton and fragment selection are
        separate, one can use consensus definitions to define the interface
        regardless of having passed "consensus" here. I.e., you can
        use `fragments='chains'` to divide the topology for representation
        and residue-tagging purposes but then define the interface as:

        >>> interface_selection_1="TM3"
        >>> interface_selection_2="TM2"

        to compute the interface of TM3 vs TM2 in a GPCR. For
        this mode of selection to work, the only condition is that the consensus
        labels have been provided via `GPCR_Uniprot`,
        `CGN_UniProt` or `KLIFS_string` (see below).
    interface_selection_1 : str or list, default is None
        Selection of the `fragments` that belong to one
        side of the interface. Strings can be CSVs
        and include:
         * ranges, e.g. '1,3-4'
         * wildcards, e.g. "TM*" or "G.H.??"
         * exclusions, e.g. "TM*,-TM6" (all TMs except TM6)
        The default (None) is to prompt the user for
        information, except when:
         * `fragments` yielded only one fragment that
           **doesn't** cover the whole topology. Then
           all othe residues are put into a second
           fragment and then the interface is computed
           between these two fragments.
         * `fragments` yielded just two fragments. Then
           the interface is computed between these two fragments.
    interface_selection_2 : str or list, default is None
        Selection of the `fragments` that belong to the other
        side of the interface. Strings can be CSVs
        and include:
         * ranges, e.g. '1,3-4'
         * wildcards, e.g. "TM*" or "G.H.??"
         * exclusions, e.g. "TM*,-TM6" (all TMs except TM6)
        The default (None) is to prompt the user for
        information, except when:
         * `fragments` yielded only one fragment that
           **doesn't** cover the whole topology. Then
           all othe residues are put into a second
           fragment and then the interface is computed
           between these two fragments.
         * `fragments` yielded just two fragments. Then
           the interface is computed between these two fragments.
    AA_selection : str or list, default is None
        Whatever the fragment definition and fragment selection
        has been, one can further refine the list of
        potential residue pairs by making a selection at
        the level of single aminoacids (AAs).
        E.g., if (like above) one has selected the interface
        to be "TM3" vs "TM2",

        >>> interface_selection_1="TM3"
        >>> interface_selection_2="TM2"

        but wants to select only some regions of those helices,
        one can pass here an `AA_selection`.
        This can be a string or a list of two items:

         * A string leads to a boolean "or" selection, i.e. keep
           residue pair [ii,jj] if either ii **or** jj
           match `AA_selection`. E.g.

           >>> AA_selection = "3.45-3.55"

           is equivalent of "3.45-3.55" vs "TM2" contacts
         * A list of with two items (each a string expression)
           leads to a boolean "and" selection, i.e. keep
           residue pair [ii,jj] if ii **and** jj
           match `AA_selection`. E.g.

           >>> AA_selection = ["3.45-3.55","2.45-2.55"]

           is equivalent of "3.45-3.55" vs "2.45-2.55" contacts.

        The strings for the selection are interpreted by
        :obj:`~mdciao.utils.residue_and_atom.rangeexpand_residues2residxs`,
        so read there for more info on what expressions are allowed,
        like mixed descriptors and wildcards, eg: "GLU*,ARG*,GDP*,LEU394,GLU30-ARG50".
        are valid.

        Finally, CSVs are interpreted as boolean "or", i.e.:

        >>> AA_selection = "GLU30,TRP50"

        will select pairs that contain GLU30 **or** TRP50. If you
        are sure about your residue pair selection, i.e. you
        have a very specific list of residue-pairs you want
        to compute, use :obj:`mdciao.cli.sites`.

    GPCR_UniProt : str or :obj:`mdciao.nomenclature.LabelerGPCR`, default is None
        For GPCR nomenclature. If str, e.g. "adrb2_human".
        will try to locate a local filename or do a web lookup in the GPCRdb.
        If :obj:`mdciao.nomenclature.LabelerGPCR`, use this object directly
        (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references.
        Please note the difference between UniProt Accession Code
        and UniProt entry name
        as explained `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    CGN_UniProt : str or :obj:`mdciao.nomenclature.LabelerCGN`, default is None
        For CGN (G-alpha Numbering definitions) nomenclature. If str, e.g. "gnas2_human",
        try to locate local filenames "gnas2_human.xlsx" or do web lookups
        in the GPCRdb. If :obj:`mdciao.nomenclature.LabelerCGN`, use this object directly
        (allows for object re-use when in API mode)
        See :obj:`mdciao.nomenclature` for more info and references.
    KLIFS_string : str or :obj:`mdciao.nomenclature.LabelerKLIFS`, default is None
        String for kinase KLIFS nomenclature. First, try to locate a local
        file that directly has the `KLIFS_string` as a name. If that fails, then
        combine the filename-format expected by :obj:`mdciao.nomenclature.LabelerKLIFS`
        with `KLIFS_string` to construct a filename and check again.
        If that doesn't work, then go online to contact the KLIFS database.

        For the online lookup in the KLIFS database, the string
        has to be formatted "key:value", which ultimately leads to a given KLIFS entry.
        Acceptable keys and values for `KLIFS_string` are:
            * "UniProtAC", e.g. "UniProtAC:P31751"
            * "kinase_ID", e.g. "kinase_ID:2"
            * "structure_ID", e.g. "structure_ID:1904", e.g. "P31751",
        Please check the documentation on :obj:`mdciao.nomenclature.LabelerKLIFS`
        for a more elaborate explanation on when to pick one of these key:value
        pairs.

        Finally, if `KLIFS_string` is an :obj:`mdciao.nomenclature.LabelerKLIFS`,
        use this object directly (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references. Alos, please note
        the difference between UniProt Accession Code
        and UniProt entry name as explained
        `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    chunksize_in_frames : int, default is 2000
        Stream through the trajectories in chunks
        of this size.
    ctc_cutoff_Ang : float, default is 4.5
        Any residue-residue distance is considered a contact
        if d<=ctc_cutoff_Ang
    curve_color : str, default is 'auto'
        Type of color used for the curves. Alternatives are
        "P" or "H"
    fragment_names : string, list of strings, or None. Default is None.
        Default is not to use fragment names. Otherwise, you can pass
        a string of comma-separated values or a list of fragment names.
        You have to provide as many names as there are fragments. The
        special string "auto" names fragments "frag0", "frag1" up
        to the number of fragments.
        mdciao will use :obj:`mdciao.utils.str_and_dict.replace4latex`
        to try to generate LaTeX expressions from stuff like "Galpha".
    graphic_dpi : int, default is 150
        Dots per Inch (DPI) of the graphic output. Only has
        an effect for bitmap outputs.
    graphic_ext : str, default is '.pdf'
        The extension (=format) of the saved figures
    background : bool, or color-like, (str, hex, rgb), default is True
        When smoothing, the original curve can
        appear in the background in different colors
        * True:  use a fainted version of :obj:`color`
        * False: don't plot any background
        * color-like: use this color for the background,
          can be: str, hex, rgba, anything
          `matplotlib.pyplot.colors` understands
    ctc_control : int, default is 50
        Control the number of reported contacts. Can be an
        integer (keep the first n contacts) or a float
        representing a fraction [0,1] of the total number of
        contacts. Default is 50.
    n_smooth_hw : int, default is 0
        Plots of the time-traces will be smoothed using a
        window of 2*n_smooth_hw
    pbc : bool, default is True
        Use periodic boundary conditions, i.e. the minimum image convention,
        to compute distances.
    output_desc : str, default is 'interface'
        Descriptor for output files.
    output_dir : str, default is '.'
        Directory to which the results are written.
    short_AA_names : bool, default is False
        Use one-letter aminoacid names when possible, e.g.
        K145 instead of Lys145.
    stride : int, default is 1
        Stride the input data by this number of frames
    t_unit : str, default is 'ns'
        Unit used for the temporal axis.
    plot_timedep : bool, default is True
        Plot and save time-traces of the contacts
    accept_guess : bool, default is False
        Accept mdciao's guesses regarding fragment
        identification using nomenclature labels
    n_jobs : int, default is 1
        Number of processors to use. The parallelization is
        done over trajectories and not over contacts, beyond
        n_jobs>n_trajs parallelization will not have any
        effect.
    n_nearest : int, default is 0
        Exclude these many bonded neighbors for each
        residue. Usually, the chosen molecular
        fragments belong to different chains and
        don't share any bonds, so this parameter
        has no effect. However, if you choose
        to compare molecular fragments that
        are bonded (e.g. the C-terminus with
        the rest of the molecule), there's
        one pair that'll be bonded across the
        fragment-boundary, yielding one contact
        that's always formed. Setting :obj:`n_nearest`
        to 1 will delete this contact.
    sort_by_av_ctcs : bool, default is True
        When presenting the results summarized by residue,
        sort by sum of frequencies (~average number of
        contacts). Default is True.
    scheme : str, default is 'closest-heavy'
        Type of scheme for computing distance between
        residues. Choices are {'ca', 'closest', 'closest-
        heavy', 'sidechain', 'sidechain-heavy'}. See
        :obj:`mdtraj.compute_distances` documentation for more info
    separate_N_ctcs : bool, default is False
        Separate the plot with the total number contacts
        from the time-trace plot.
    table_ext : str, default is "dat"
        The extension (=format) of the saved tables
    title : NoneType, default is None
        Name of the system. Used for figure titles (not
        filenames) Defaults to :obj:`output_desc` if None is given
    min_freq : float, default is 0.1
        Do not show frequencies smaller than this. If you
        notice the output being truncated at values too far
        above this value, you need to increase the :obj:`ctc_control`
        parameter.
    contact_matrix : bool, default is True
        Produce a plot of the interface contact matrix
    cmap : str, default is 'binary'
        The colormap for the contact matrix. Default is
        'binary' which is black and white, but you can
        choose anything from here:
        https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html
    flareplot : bool, default is True
        Produce a flare plot of interface the contact
        matrix. Regardless of the :obj:`graphic_ext`,
        the flareplot will always be in .pdf-format,
        unless :obj:`graphic_ext` is 'svg'.
    save_nomenclature_files : bool, default is False
        Save available nomenclature definitions to disk so
        that they can be accessed locally in later uses.
    no_disk : bool, default is False
        If True, don't save any files at all:
        figs, tables, trajs, nomenclature
    savefigs : bool, default is True
        Save the figures
    savetabs : bool, default is True
        Save the frequency tables
    savetrajs : bool, default is False
        Save the timetraces
    figures : bool, default is True
        Draw figures
    self_interface : bool, default is False
        Allow the interface members to share
        residues
    n_repframes : int, default is 1
        The number of representative frames
        to write to the .pdb file 'interface.overall@4.0_Ang.as_bfactors.pdb',
        where the interface strength has been stored
        as bfactor. A "representative frame" means, in this context,
        a frame where the pairs of residues participating
        in the interface are, on average over all pairs,
        at a distance close to the most-likely the residue-residue
        distances over all data. This has some caveats,
        expressed in the documentation of :obj:`mdciao.contacts.ContactGroup.repframes`.
        To check what frames have been chosen as
        representative, it is better to run mdciao in API
        mode and call :obj:`mdciao.contacts.ContactGroup.n_repframes`
        with `show_violins=True`. A value of 0 means don't write
        any such .pdb file. Max value of 50 frames is enforced.
    progressbar : bool, default is True
        Report progress as the computation advances.


    Returns
    -------
    CG_interface : :obj:`mdciao.contacts.ContactGroup`
        The object containing the :obj:`mdciao.contacts.ContactPair`
        objects tha conform the interface.
        If no contacts have been found, returns None.

    """
    if str(title).lower()=="none":
        title = output_desc

    _offer_to_create_dir(output_dir)
    xtcs, refgeom = _trajsNtop2xtcsNrefgeom(trajectories,topology)
    fn = _mdcu.str_and_dict.FilenameGenerator(output_desc,ctc_cutoff_Ang,output_dir,
                                              graphic_ext, table_ext, graphic_dpi,t_unit)
    if no_disk:
        savetrajs = False
        savefigs  = False
        savetabs = False
        save_nomenclature_files = False

    print("Will compute contact frequencies for trajectories:\n%s"
          "\n with a stride of %u frames" % (_mdcu.str_and_dict.inform_about_trajectories(xtcs, only_show_first_and_last=15), stride))

    fragments_as_residue_idxs, fragment_names, _, consensus_labelers, consensus_maps, consensus_frags, top2confrag = _parse_fragdefs_fragnames_consensus(
        refgeom.top, fragments, fragment_names, GPCR_UniProt, CGN_UniProt, KLIFS_string, accept_guess, save_nomenclature_files)
    if fragment_names is None or all([fn is None for fn in fragment_names]): #adapt to _parse_fragment_naming_options
        fragments_as_residue_idxs_d = {str(ii) : val for ii, val in enumerate(fragments_as_residue_idxs)}
    else:
        fragments_as_residue_idxs_d = {key : val for key, val in zip(fragment_names, fragments_as_residue_idxs)}
        fragments_as_residue_idxs_d.update({str(ii) : val for ii, val in enumerate(fragments_as_residue_idxs)})
    if len(fragments_as_residue_idxs)==2 and interface_selection_1 is None and interface_selection_2 is None:
        interface_selection_1, interface_selection_2 =[0], [1]
    else:
        fragments_as_residue_idxs_d.update(consensus_frags)

    intf_frags_as_residxs, \
        intf_frags_as_str_or_keys = _mdcfrg.frag_dict_2_frag_groups(fragments_as_residue_idxs_d, ng=2,
                                                                    answers=[interface_selection_1, interface_selection_2],
                                                                    )
    intf_frags_as_residxs = [_np.unique(ifrg) for ifrg in intf_frags_as_residxs]
    intersect = list(set(intf_frags_as_residxs[0]).intersection(intf_frags_as_residxs[1]))
    ctc_idxs = _mdcu.lists.unique_product_w_intersection(intf_frags_as_residxs[0], intf_frags_as_residxs[1])
    last_n_ctcs = len(ctc_idxs)
    if len(intersect)>0 and not self_interface:
        ctc_idxs = [pair for pair in ctc_idxs if not _np.isin(pair, intersect).all()]
        if len(ctc_idxs)!=last_n_ctcs:
            print()
            print(f"\nExcluding contacts within the same members of the interface reduces from {last_n_ctcs} to {len(ctc_idxs)} residue pairs. "
                  f"Use 'self_interface=True' to keep these {last_n_ctcs-len(ctc_idxs)} discarded pairs.")
            last_n_ctcs = len(ctc_idxs)

    # Once the ctc_idxs are computed and contain the self-contacts, we can
    # remove the overlap between the interface definitions
    # leaving the residues appearing in both sets (the intersection) in the first
    # intf_frags_as_residx only
    intf_frags_as_residxs[-1] = list(set(intf_frags_as_residxs[-1]).difference(intersect))

    # Create a neighborlist
    if n_nearest>0:
        nl = _mdcu.bonds.bonded_neighborlist_from_top(refgeom.top, n=n_nearest)
        ctc_idxs = _np.vstack([(ii,jj) for ii,jj in ctc_idxs if jj not in nl[ii]])
        if len(ctc_idxs)!=last_n_ctcs:
            print(f"\nExcluding contacts between {n_nearest} nearest neighbors reduces from {last_n_ctcs} to {len(ctc_idxs)} residue pairs. "
                  f"Use 'n_nearest' to control this ({last_n_ctcs-len(ctc_idxs)} residue pairs discarded).")
            last_n_ctcs=len(ctc_idxs)

    print("\nWill look for contacts in the interface between fragments\n%s\nand\n%s. "%
          ('\n'.join(_twrap(', '.join(['%s' % gg for gg in intf_frags_as_str_or_keys[0]]))),
           '\n'.join(_twrap(', '.join(['%s' % gg for gg in intf_frags_as_str_or_keys[1]])))))

    # Sub-select at the AA-level #TODO consider making method out of this
    if AA_selection is not None:
        if isinstance(AA_selection, str):
            lambda_sel = lambda pair, sel: _np.isin(pair, sel).any()
        elif isinstance(AA_selection, list) and len(AA_selection)==2:
            lambda_sel = lambda pair, sel: _np.isin(pair, sel).all()
            AA_selection = ",".join(AA_selection)
        else:
            raise ValueError(f"'AA_selection'  as to be a sting or a list of len 2, "
                             f"but your input is a {type(AA_selection).__name__} of len {len(AA_selection)}.")
        sel = _mdcu.residue_and_atom.rangeexpand_residues2residxs(AA_selection,
                                                                  fragments_as_residue_idxs,
                                                                  refgeom.top,
                                                                  fragment_names=fragment_names,
                                                                  additional_resnaming_dicts=consensus_maps)
        ctc_idxs = [pair for pair in ctc_idxs if lambda_sel(pair, sel)]
        if len(ctc_idxs)!=last_n_ctcs:
            print(f"\nExcluding residue pairs not involving residues '{AA_selection}' ({len(sel)} AAs) "
                  f"reduces from {last_n_ctcs} to {len(ctc_idxs)} residue pairs.")
            last_n_ctcs = len(ctc_idxs)
    print(f"\nPerforming a first pass on the {last_n_ctcs} group_1-group_2 residue pairs to compute lower bounds "
          f"on residue-residue distances via residue-COM distances.")
    lb_cutoff_buffer_Ang = 2.5
    idx_of_lower_lower_bounds = _mdcctcs.trajs2lower_bounds(xtcs, refgeom.top, ctc_idxs,
                                                            stride=stride,
                                                            chunksize=chunksize_in_frames,
                                                            n_jobs=n_jobs,
                                                            progressbar=progressbar,
                                                            lb_cutoff_Ang=ctc_cutoff_Ang+ lb_cutoff_buffer_Ang, # This buffer allows for some debugging before truncating to ctc_control
                                                            periodic=pbc,
                                                            )
    ctc_idxs_intf = _np.array(ctc_idxs)[_np.unique(_np.hstack(idx_of_lower_lower_bounds))]
    if len(ctc_idxs_intf)==0:
        print("No contacts found at %2.1f Ang. No output produced." % ctc_cutoff_Ang)
        return
    print(f"Reduced to only {len(ctc_idxs_intf)} (from {last_n_ctcs}) residue pairs for the computation of actual residue-residue distances:")
    ctcs, times, at_pair_trajs = _mdcctcs.trajs2ctcs(xtcs, refgeom.top, ctc_idxs_intf,
                                                     stride=stride, return_times_and_atoms=True,
                                                     consolidate=False,
                                                     chunksize=chunksize_in_frames,
                                                     n_jobs=n_jobs,
                                                     progressbar=progressbar,
                                                     scheme=scheme,
                                                     periodic=pbc
                                                     )

    # Stack all data
    actcs = _np.vstack(ctcs)
    df = _mdcctcs.contacts._data2DataFrame(actcs, ctc_idxs_intf, refgeom.top, ctc_cutoff_Ang,
                                           fragments_as_residue_idxs, fragment_names,
                                           top2confrag, list(consensus_maps.values()),
                                           keep_max_buffer_Ang=lb_cutoff_buffer_Ang)
    # The reporting is
    n_ctcs =  _mdcu.lists._get_n_ctcs_from_freqs(ctc_control,df.freq, min_freq=min_freq)[0]

    # Report n_ctcs and frequency summary leaving in the freqs < min_freq
    _mdcctcs.contacts._contact_fraction_informer(_np.min([n_ctcs, _np.sum(df.freq>min_freq)]),
                                                 df[df.freq>0].freq.values, ctc_cutoff_Ang, or_frac=.9, )
    # Take freqs < min_freq out now
    df = df[df.freq>min_freq][:n_ctcs]

    ctc_objs = []
    for ii, irow in df.iterrows():
        ctc_objs.append(_mdcctcs.ContactPair([irow.residx1, irow.residx2],
                                             [itraj[:, irow.ctc_idx] for itraj in ctcs],
                                             times,
                                             top=refgeom.top,
                                             consensus_labels=[irow.GRN1, irow.GRN2],
                                             trajs=xtcs,
                                             fragment_idxs=[irow.frag1, irow.frag2],
                                             fragment_names=[irow.fragname1, irow.fragname2],
                                             consensus_fragnames=[irow.GFN1, irow.GFN2],
                                             atom_pair_trajs=[itraj[:, [irow.ctc_idx * 2, irow.ctc_idx * 2 + 1]] for
                                                              itraj in
                                                              at_pair_trajs]
                                             ))


    ctc_grp_intf = _mdcctcs.ContactGroup(ctc_objs,
                                         max_cutoff_Ang=ctc_cutoff_Ang,
                                         interface_fragments=intf_frags_as_residxs,
                                         name=title)
    print()
    idf = ctc_grp_intf.frequency_dataframe(ctc_cutoff_Ang)
    idf.index += 1
    print(idf.round({"freq": 2, "sum": 2}).to_string(justify="center"))
    print()
    dfs = ctc_grp_intf.frequency_sum_per_residue_names(ctc_cutoff_Ang,
                                                       list_by_interface=True,
                                                       return_as_dataframe=True,
                                                       sort_by=["freq" if sort_by_av_ctcs else None][0])
    for idf in dfs:
        idf.index += 1
        print(idf.round({"freq":2}))
        print()

    if any([savetabs,savefigs,savetrajs]):
        print("The following files have been created:")

    if savetabs:
        ctc_grp_intf.frequency_table(ctc_cutoff_Ang, fn.fullpath_overall_excel, sort_by_freq=sort_by_av_ctcs)
        print(fn.fullpath_overall_excel)
        ctc_grp_intf.frequency_table(ctc_cutoff_Ang, fn.fullpath_overall_dat, atom_types=True)
        print(fn.fullpath_overall_dat)
        if n_repframes>0:
            n_repframes = _np.min((n_repframes,50))
            repframes_geom = ctc_grp_intf.repframes(ctc_cutoff_Ang=ctc_cutoff_Ang, return_traj=True, n_frames=n_repframes, verbose=False)[-1]
            repframes_geom = _md.Trajectory([geom.xyz[0] for geom in repframes_geom], topology=repframes_geom[0].top,
                                            unitcell_angles=[geom.unitcell_angles[0] for geom in repframes_geom],
                                            unitcell_lengths=[geom.unitcell_lengths[0] for geom in repframes_geom],
                                            time=[geom.time[0] for geom in repframes_geom])

            ctc_grp_intf.frequency_to_bfactor(ctc_cutoff_Ang, fn.fullpath_pdb, repframes_geom,
                                          # interface_sign=True,
                                          verbose=False
                                          )
        print(fn.fullpath_pdb)

    if figures:
        n_cols = 1
        n_rows = 2
        panelsize = 4
        panelsize2font = 3.5
        fudge = 7
        histofig, histoax = _plt.subplots(n_rows, n_cols, sharex=True, sharey=False,
                                          figsize=(n_cols * panelsize * _np.ceil(ctc_grp_intf.n_ctcs/fudge),
                                                  n_rows * panelsize),
                                          )

        # One loop for the histograms
        _rcParams["font.size"] = panelsize * panelsize2font
        ctc_grp_intf.plot_freqs_as_bars(ctc_cutoff_Ang,
                                        title,
                                        ax=histoax[0],
                                        xlim=_np.min((n_ctcs, ctc_grp_intf.n_ctcs)),
                                        label_fontsize_factor=panelsize2font / panelsize,
                                        shorten_AAs=short_AA_names,
                                        lower_cutoff_val=min_freq,
                                        total_freq=df.freq.sum()
                                        )

        ctc_grp_intf.plot_frequency_sums_as_bars(ctc_cutoff_Ang,
                                                 title,
                                                 ax=histoax[1],
                                                 list_by_interface=True,
                                                 label_fontsize_factor=panelsize2font / panelsize,
                                                 lower_cutoff_val=.05,
                                                 shorten_AAs=short_AA_names,
                                                 sort_by=["freq" if sort_by_av_ctcs else None][0],
                                                 )
        histofig.tight_layout(h_pad=2, w_pad=0, pad=0)

        # TODO bury this in plots?
        if contact_matrix:
            cmat_fig, iax = ctc_grp_intf.plot_interface_frequency_matrix(ctc_cutoff_Ang,
                                                                     colorbar=True,
                                                                     grid=True,
                                                                     cmap=cmap)

            iax.set_title("'%s'  as contact matrix" % _mdcu.str_and_dict.replace4latex(title),
                          fontsize=iax.get_xticklabels()[0].get_fontsize() * 2)
            cmat_fig.tight_layout()


        if savefigs:
            histofig.savefig(fn.fullpath_overall_fig, dpi=graphic_dpi, bbox_inches="tight")
            print(fn.fullpath_overall_fig)
            cmat_fig.savefig(fn.fullpath_matrix)
            print(fn.fullpath_matrix)

        if flareplot:
            consensus_maps_ = []
            for key in ["GPCR", "CGN", "KLIFS"]:
                if key in consensus_labelers.keys():
                    consensus_maps_.append(consensus_labelers[key])
                elif key in consensus_maps.keys():
                    consensus_maps_.append(consensus_maps[key])
            ifig, iax, _ = ctc_grp_intf.plot_freqs_as_flareplot(ctc_cutoff_Ang,
                                                                consensus_maps=consensus_maps_,
                                                                SS=ctc_grp_intf.repframes(ctc_cutoff_Ang=ctc_cutoff_Ang,
                                                                                          return_traj=True, n_frames=1,
                                                                                          verbose=False)[-1][0],
                                                                fragment_names=[None if all([fn is None for fn in fragment_names]) else fragment_names][0],
                                                                fragments=fragments_as_residue_idxs,
                                                                )
            ifig.tight_layout()
            if savefigs:
                ifig.savefig(fn.fullpath_flare_vec, bbox_inches="tight")
                print(fn.fullpath_flare_vec)

        if plot_timedep or separate_N_ctcs:
            myfig = ctc_grp_intf.plot_timedep_ctcs(color_scheme=_color_schemes(curve_color),
                                                   ctc_cutoff_Ang=ctc_cutoff_Ang,
                                                   dt=_mdcu.str_and_dict.tunit2tunit["ps"][t_unit],
                                                   background=background,
                                                   n_smooth_hw=n_smooth_hw,
                                                   plot_N_ctcs=True,
                                                   pop_N_ctcs=separate_N_ctcs,
                                                   shorten_AAs=short_AA_names,
                                                   skip_timedep=not plot_timedep,
                                                   t_unit=t_unit)
            _manage_timedep_ploting_and_saving_options(ctc_grp_intf, fn, myfig,
                                                       plot_timedep=plot_timedep,
                                                       separate_N_ctcs=separate_N_ctcs,
                                                       savefigs=savefigs,
                                                       savetrajs=savetrajs
                                                       )

    return ctc_grp_intf


def sites(site_inputs,
          trajectories,
          topology=None,
          ctc_cutoff_Ang=4.5,
          stride=1,
          scheme="closest-heavy",
          chunksize_in_frames=2000,
          n_smooth_hw=0,
          pbc=True,
          GPCR_UniProt="None",
          CGN_UniProt="None",
          KLIFS_string=None,
          fragments='lig_resSeq+',
          allow_partial_sites=False,
          default_fragment_index=None,
          fragment_names=None,
          output_dir='.',
          graphic_ext=".pdf",
          t_unit='ns',
          curve_color="auto",
          background=True,
          graphic_dpi=150,
          short_AA_names=False,
          save_nomenclature_files=False,
          ylim_Ang=10,
          n_jobs=1,
          accept_guess=False,
          table_ext="dat",
          output_desc="sites",
          plot_atomtypes=False,
          distro=False,
          no_disk=False,
          savefigs=True,
          savetabs=True,
          savetrajs=False,
          figures=True,
          plot_timedep=True,
          progressbar=True,
          ):
    r"""

    Compute distances between groups of contact-pairs that are
    already pre-defined as sites

    Parameters
    ----------
    site_inputs : dict or path to file, or list thereof
        Site(s) to compute. A site can be either
        a path to a site file in json format or
        directly a site dictionary. A site dictionary
        is something like

        >>> {"name": "interesting contacts",
        >>>  "pairs": {"AAresSeq": ["L394-K270",
        >>>                         "D381-Q229"]}}

        Any site containing a residue that can't be
        found in the topology will be discarded.
        The list in "pairs" can be specified as:
         * 'AAresSeq':
          >>> {"name": "interesting contacts",
          >>>  "pairs": {"AAresSeq": ["L394-K270",
          >>>                         "D381-Q229"]}}
          The 'AAresSeq' definitions are transferable to
          another system where the same aminoacids are
          present, regardless of their actual zero-indexing.
         * 'residx':
          >>> {"name": "interesting contacts",
          >>>  "pairs": {"residx":[[353,972],
          >>>                      [340,956]]}}
          The 'pairs' definitions are only transferable
          across systems as long both systems share the same
          zero-indexing scheme.
         * 'consensus'
          >>> {"name": "interesting contacts",
          >>>  "pairs": {"consensus": ["G.H5.26-6.32x32",
          >>>                          "G.H5.13-5.68x68"]}}
          The 'consensus' definitions are transferable to
          another system, even if the selected aminoacids are
          different. Please note, in order
          to use 'consenus' definitions, you need
          to pass at least one (or more) of the `GPCR_UniProt`,
          `CGN_UniProt` or `KLIFS_string` arguments, else
          there is no way to know to which residues the labels
          belong to.
        See :obj:`mdciao.sites` for more info on
        the site format.
    trajectories : str, :obj:`mdtraj.Trajectory` or lists thereof
        The MD-trajectories to calculate the frequencies from.
        This input is pretty flexible. For more info check
        :obj:`mdciao.utils.str_and_dict.get_trajectories_from_input`.
        Accepted values are:
         * pattern, e.g. "*.ext"
         * one string containing a filename
         * list of filenames
         * one :obj:`mdtraj.Trajectory` object
         * list of :obj:`mdtraj.Trajectory` objects
         * list mixing filenames and :obj:`mdtraj.Trajectory` objects
    topology : str or :obj:`~mdtraj.Trajectory`, default is None
        The topology associated with the :obj:`trajectories`
        If None, the topology of the first :obj:`trajectory` will
        be used, i.e. when no :obj:`topology` is passed, the first
        :obj:`trajectory` has to be either a .gro or .pdb file, or
        an :obj:`~mdtraj.Trajectory` object
    ctc_cutoff_Ang : float, default is 4.5
        Any residue-residue distance is considered a contact
        if d<=ctc_cutoff_Ang
    stride : int, default is 1
        Stride the input data by this number of frames
    scheme : str, default is 'closest-heavy'
        Type of scheme for computing distance between
        residues. Choices are {'ca', 'closest', 'closest-
        heavy', 'sidechain', 'sidechain-heavy'}. See mdtraj
        documentation for more info
    chunksize_in_frames : int, default is 2000
        Stream through the trajectories in chunks
        of this size.
    n_smooth_hw : int, default is 0
        Plots of the time-traces will be smoothed using a
        window of 2*n_smooth_hw
    pbc : bool, default is True
        Use periodic boundary conditions, i.e. the minimum image convention,
        to compute distances.
    GPCR_UniProt : str or :obj:`mdciao.nomenclature.LabelerGPCR`, default is None
        For GPCR nomenclature. If str, e.g. "adrb2_human".
        will try to locate a local filename or do a web lookup in the GPCRdb.
        If :obj:`mdciao.nomenclature.LabelerGPCR`, use this object directly
        (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references.
        Please note the difference between UniProt Accession Code
        and UniProt entry name
        as explained `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    CGN_UniProt : str or :obj:`mdciao.nomenclature.LabelerCGN`, default is None
        For CGN (G-alpha Numbering definitions) nomenclature. If str, e.g. "gnas2_human",
        try to locate local filenames "gnas2_human.xlsx" or do web lookups
        in the GPCRdb. If :obj:`mdciao.nomenclature.LabelerCGN`, use this object directly
        (allows for object re-use when in API mode)
        See :obj:`mdciao.nomenclature` for more info and references.
    KLIFS_string : str or :obj:`mdciao.nomenclature.LabelerKLIFS`, default is None
        String for kinase KLIFS nomenclature. First, try to locate a local
        file that directly has the `KLIFS_string` as a name. If that fails, then
        combine the filename-format expected by :obj:`mdciao.nomenclature.LabelerKLIFS`
        with `KLIFS_string` to construct a filename and check again.
        If that doesn't work, then go online to contact the KLIFS database.

        For the online lookup in the KLIFS database, the string
        has to be formatted "key:value", which ultimately leads to a given KLIFS entry.
        Acceptable keys and values for `KLIFS_string` are:
            * "UniProtAC", e.g. "UniProtAC:P31751"
            * "kinase_ID", e.g. "kinase_ID:2"
            * "structure_ID", e.g. "structure_ID:1904", e.g. "P31751",
        Please check the documentation on :obj:`mdciao.nomenclature.LabelerKLIFS`
        for a more elaborate explanation on when to pick one of these key:value
        pairs.

        Finally, if `KLIFS_string` is an :obj:`mdciao.nomenclature.LabelerKLIFS`,
        use this object directly (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references. Alos, please note
        the difference between UniProt Accession Code
        and UniProt entry name as explained
        `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    fragments : str, list, None, default is "lig_resSeq+"
        Topology fragments. There exist several input modes:

        * Name of a fragmentation heuristic, e.g.
          "lig_resSeq+", which is the default of
          and usually yields good results. See
          :obj:`mdciao.fragments.get_fragments`
          for more info on defaults and other heuristics.
        * List of len N that can mix different possibilities:

          * iterable of integers (lists or np.arrays, e.g. np.arange(20,30)
          * ranges expressed as integer strings, "20-30"
          * ranges expressed as residue descriptors ["GLU30-LEU40"]

        * "consensus" : use things like "TM*" or "G.H*", i.e.
          GPCR or CGN-sub-subunit labels.

        Numeric expressions are interpreted as zero-indexed and unique
        residue serial indices, i.e. 30-40 does not necessarily equate
        "GLU30-LEU40" unless serial and sequence index coincide.
        If there's more than one "GLU30", the user gets asked to
        disambiguate. The resulting fragments need not cover
        all of the topology, they only need to not overlap.
    default_fragment_index : NoneType, default is None
        In case a residue identified as, e.g, "GLU30", appears
        more than one time in the topology, e.g. in case of
        a dimer, pass which fragment/monomer should be chosen
        by default. The default behaviour (None)
        will prompt the user when necessary
    fragment_names : string, list of strings, or None. Default is None.
        Default is not to use fragment names. Otherwise, you can pass
        a string of comma-separated values or a list of fragment names.
        You have to provide as many names as there are fragments. The
        special string "auto" names fragments "frag0", "frag1" up
        to the number of fragments.
        mdciao will use :obj:`mdciao.utils.str_and_dict.replace4latex`
        to try to generate LaTeX expressions from stuff like "Galpha".
    allow_partial_sites : bool, default is False
        If False, a single missing residue is enough to
        discard an entire site. If True,
        the site definitions get modified
        to keep the residues that could be found
    output_dir : str, default is '.'
        directory to which the results are written
    graphic_ext : str, default is '.pdf'
        Extension of the output graphics, default is .pdf
    t_unit : str, default is 'ns'
        Unit used for the temporal axis.
    curve_color : str, default is 'auto'
        Type of color used for the curves. Alternatives are
        "P" or "H"
    background : bool, or color-like, (str, hex, rgb), default is True
        When smoothing, the original curve can
        appear in the background in different colors
        * True:  use a fainted version of :obj:`color`
        * False: don't plot any background
        * color-like: use this color for the background,
          can be: str, hex, rgba, anything
          `matplotlib.pyplot.colors` understands
    graphic_dpi : int, default is 150
        Dots per Inch (DPI) of the graphic output. Only has
        an effect for bitmap outputs.
    short_AA_names : bool, default is False
        Use one-letter aminoacid names when possible, e.g.
        K145 insted of Lys145.
    save_nomenclature_files : bool, default is False
        Save available nomenclature definitions to disk so
        that they can be accessed locally in later uses.
    ylim_Ang : int, default is 10
        Limit in Angstrom of the y-axis of the time-traces.
        Switch to any other float or 'auto' for automatic scaling
    n_jobs : int, default is 1
        Number of processors to use. The parallelization is
        done over trajectories and not over contacts, beyond
        n_jobs>n_trajs parallelization will not have any
        effect
    accept_guess : bool, default is False
        Accept mdciao's guesses regarding fragment
        identification using nomenclature labels
    table_ext : str, default is dat
        Extension for tabled files (.dat, .txt, .xlsx).
    output_desc :
        Descriptor for output files.
    plot_atomtypes : bool, default is False
        Add the atom-types to the frequency bars by
        'hatching' them. '--' is sidechain-sidechain '|' is
        backbone-backbone '\' is backbone-sidechain '/' is
        sidechain-backbone. See Fig XX for an example
    distro : bool, default is False
        Plot distance distributions instead of contact bar plots
    savefigs : bool, default is True
        Save the figures
    savetabs : bool, default is True
        Save the frequency tables
    savetrajs : bool, default is False
        Save the timetraces
    no_disk : bool, default is False
        If True, don't save any files at all:
        figs, tables, trajs, nomenclature
    figures : bool, default is True
        Draw figures
    plot_timedep : bool, default is True
        Plot time-traces of the contacts
    progressbar : bool, default is True
        Report progress as the computation advances.

    Returns
    -------
    CG_site : dictionary
        Keyed with the site name, its values are the
        :obj:`mdciao.contacts.ContactGroup`-objects,
        that conform each site

    """

    ylim_Ang = float(ylim_Ang)
    _offer_to_create_dir(output_dir)
    xtcs, refgeom = _trajsNtop2xtcsNrefgeom(trajectories, topology)
    fn = _mdcu.str_and_dict.FilenameGenerator(output_desc, ctc_cutoff_Ang, output_dir,
                                              graphic_ext, table_ext, graphic_dpi, t_unit)
    if no_disk:
        savetrajs = False
        savefigs  = False
        savetabs = False
        save_nomenclature_files = False
    site_inputs = [[site_inputs] if not isinstance(site_inputs,list) else site_inputs][0]
    print("Will compute the sites\n %s\nin the trajectories:\n%s\n with a stride of %u frames.\n" % (
        "\n ".join([_mdcsites.site2str(ss) for ss in site_inputs]),
        _mdcu.str_and_dict.inform_about_trajectories(xtcs, only_show_first_and_last=15),stride))

    fragments_as_residue_idxs, fragment_names, __, consensus_labelers, consensus_maps, consensus_frags, top2confrag = _parse_fragdefs_fragnames_consensus(
        refgeom.top, fragments, fragment_names, GPCR_UniProt, CGN_UniProt, KLIFS_string, accept_guess, save_nomenclature_files)

    sites = [_mdcsites.x2site(ff) for ff in site_inputs]
    ctc_idxs_small, site_maps = _mdcsites.sites_to_res_pairs(sites, refgeom.top,
                                                             fragments=fragments_as_residue_idxs,
                                                             default_fragment_index=default_fragment_index,
                                                             consensus_maps=[consensus_maps if len(consensus_maps)>0 else None][0],
                                                             fragment_names=fragment_names)
    if None in ctc_idxs_small:
        print("Some definitions of the 'site_inputs' contain one or more residues not found in the input topology.")
        if not allow_partial_sites:
            print("Please read the documentation for the 'allow_partial_sites' parameter.")
        ctc_idxs_small, site_maps, _sites, discarded = _mdcsites.discard_empty_sites(ctc_idxs_small,site_maps, sites, allow_partial_sites=allow_partial_sites)
        preface=True
        for ii in discarded["full"]:
            if preface:
                print("The following sites have been discarded and won't appear in the output: ")
                preface=False
            print(" * site '%s' (idx %u)"%(sites[ii]["name"],ii))
        if len(_sites)==0:
            raise ValueError("No site(s) could be constructed, please check the above messages and your review your site(s)' definitions.")
        else:
            sites = _sites

    print("These are the residues that could be found:")
    header = "  ".join(["%10s"%head for head in "residue  residx fragment  resSeq".split()+list(consensus_maps.keys())])
    print(header)
    for idx in _np.unique(ctc_idxs_small):
        print(_mdcu.residue_and_atom.residue_line("",refgeom.top.residue(idx),
                                                  _mdcu.lists.in_what_fragment(idx,fragments_as_residue_idxs),
                                                  consensus_maps=consensus_maps, table=True))
    ctcs, time_array, at_pair_trajs = _mdcctcs.trajs2ctcs(xtcs, refgeom.top, ctc_idxs_small, stride=stride,
                                                          chunksize=chunksize_in_frames,
                                                          return_times_and_atoms=True, consolidate=False, periodic=pbc,
                                                          scheme=scheme,
                                                          n_jobs=n_jobs, progressbar=progressbar)

    # Abstract each site to a group of contacts and fragments
    site_as_gc = {}
    for isite, imap in zip(sites,site_maps):
        key = isite["name"]
        site_as_gc[key] = []
        for idx in imap:
            pair = ctc_idxs_small[idx]
            consensus_labels = [_mdcnomenc.choose_between_consensus_dicts(idx, list(consensus_maps.values()), no_key=None) for idx in pair]
            fragment_idxs = [_mdcu.lists.in_what_fragment(idx, fragments_as_residue_idxs) for idx in pair]
            site_as_gc[key].append(_mdcctcs.ContactPair(pair,
                                               [itraj[:, idx] for itraj in ctcs],
                                               time_array,
                                               top=refgeom.top,
                                               consensus_labels=consensus_labels,
                                               trajs=xtcs,
                                               fragment_idxs=fragment_idxs,
                                               fragment_names=[fragment_names[idx] for idx in fragment_idxs],
                                               atom_pair_trajs=[itraj[:, [idx * 2, idx * 2 + 1]] for itraj in
                                                                at_pair_trajs],
                                               consensus_fragnames=[top2confrag[idx] for idx in pair]
                                               #colors=[fragcolors[idx] for idx in idxs]
                                               ))
        site_as_gc[key] = _mdcctcs.ContactGroup(site_as_gc[key], name="site '%s'"%key)
        print()
        print(f"{site_as_gc[key].name.capitalize()}:")
        print(site_as_gc[key].frequency_dataframe(ctc_cutoff_Ang).round({"freq": 2, "sum": 2}))
        print()
    if figures:
        overall_fig = _mdcplots.CG_panels(4, site_as_gc, ctc_cutoff_Ang,
                                   distro=distro,
                                   short_AA_names=short_AA_names,
                                   plot_atomtypes=plot_atomtypes)
        overall_fig.tight_layout(h_pad=2, w_pad=0, pad=0)

    if scheme!="closest-heavy":
        scheme_desc='%s.'%scheme
    else:
        scheme_desc=''
    if any([savetabs,savefigs,savetrajs]):
        print("The following files have been created:")

    if savefigs:
        overall_fig.savefig(fn.fullpath_overall_fig, dpi=graphic_dpi)
        print(fn.fullpath_overall_fig)
        _plt.close(overall_fig)

    for site_name, isite_nh in site_as_gc.items():
        if savetabs:
            isite_nh.frequency_table(ctc_cutoff_Ang,
                                     fn.fname_per_site_table(site_name),
                                     write_interface=False,
                                     atom_types=True,
                                     # AA_format="long",
                                     )
            print(fn.fname_per_site_table(site_name))

    if figures and plot_timedep:
        for site_name, isite_nh in site_as_gc.items():
            myfig = isite_nh.plot_timedep_ctcs(panelheight = 4,
                                               color_scheme=_color_schemes(curve_color),
                                               ctc_cutoff_Ang=ctc_cutoff_Ang,
                                               n_smooth_hw=n_smooth_hw,
                                               dt=_mdcu.str_and_dict.tunit2tunit["ps"][t_unit],
                                               t_unit=t_unit,
                                               background=background,
                                               shorten_AAs=short_AA_names,
                                               plot_N_ctcs=True,
                                               ylim_Ang=ylim_Ang,
                                               )

            _manage_timedep_ploting_and_saving_options(isite_nh, fn, myfig,
                                                       plot_timedep=True,
                                                       separate_N_ctcs=False,
                                                       title="site: %s" % site_name,
                                                       savefigs=savefigs,
                                                       savetrajs=savetrajs
                                                       )


    return site_as_gc


@_kwargs_subs(_mdcplots.compare_groups_of_contacts)
def compare(datasets, graphic_ext=".pdf", output_desc="freq_comparison", pop=False, **kwargs):
    r"""

    Compare contact frequencies across different sets of data


    Parameters
    ----------
    datasets : iterable (list or dict)
        The datasets to compare with each other.
        If dict, then the keys will be used as names
        for the contact groups, e.g. "WT", "MUT" etc.
        If list, then  the keys will be auto-generated.
        The entries of the list/dictionary can be:
          * :obj:`~mdciao.contacts.ContactGroup` objects.
            For these, a :obj:`ctc_cutoff_Ang` value
            needs to be passed along, otherwise frequencies
            cannot be computed on-the-fly.
          * dictionaries where the keys are residue-pairs,
            one letter-codes, no fragment info,
            as in :obj:`mdciao.contacts.ContactGroup.ctc_labels_short`
            and the values are contact frequencies
          * files generated by (or in the same format as)
            :obj:`~mdciao.contacts.ContactGroup.frequency_table`

            * ascii-files with the contact the frequencies in the first
              column and labels in the second and/or third column,
              see :obj:`~mdciao.contacts.ContactGroup.frequency_str_ASCII_file`
              and :obj:`~mdciao.utils.str_and_dict.freq_ascii2dict`
            * .xlsx files with the header in the second row,
              containing at least the column-names "label" and "freqs", see
              :obj:`~mdciao.contacts.ContactGroup.frequency_spreadsheet`
    graphic_ext : str, default is ".pdf"
        The extension for figures
    output_desc : str, default is 'freq_comparison'
        Descriptor for output files.
    pop : bool, default is True
        Use :obj:`~matplotlib.pyplot.show` to
        force the figure to be drawn.
    kwargs : dict
        Optional arguments for
        :obj:`~mdciao.plots.compare_groups_of_contacts`,
        which are listed below:

    Other Parameters
    ----------------
    %(substitute_kwargs)s

    Returns
    -------
    myfig : :obj:`~matplotlib.figure.Figure`
        Figure with the comparison plot
    freqs : dictionary
        Unified frequency dictionaries,
        including mutations and anchor
    plotted_freqs : dictionary
        Like :obj:`freqs` but sorted and purged
        according to the user-defined input options,
        s.t. it represents the plotted values
    """
    myfig, freqs, plotted_freqs = _mdcplots.compare_groups_of_contacts(datasets, **kwargs)
    myfig.tight_layout()

    output_desc=output_desc.strip(".").replace(" ","_")
    fname = "%s.%s" % (output_desc, graphic_ext.strip("."))
    print("Created files")
    myfig.savefig(fname)
    print(fname)
    fname_excel = "%s.xlsx" % output_desc
    writer = _ExcelWriter(fname_excel, engine='xlsxwriter')
    workbook = writer.book
    sheet1_name = "plotted frequencies"
    writer.sheets[sheet1_name] = workbook.add_worksheet(sheet1_name)
    offset = 0
    header = 'pairs by contact frequency'
    if "anchor" in kwargs.keys():
        header+= "(anchor was %s)"%kwargs["anchor"]
    writer.sheets[sheet1_name].write_string(0, offset,header
                                            )
    offset += 1
    _DF.from_dict(plotted_freqs).round({"freq": 2, "sum": 2}).to_excel(writer,
                                                                       sheet_name=sheet1_name,
                                                                       startrow=offset,
                                                                       startcol=0,
                                                                       )
    # offset = 0
    sheet2_name = "all frequencies"
    writer.sheets[sheet2_name] = workbook.add_worksheet(sheet2_name)
    writer.sheets[sheet2_name].write_string(offset, 0, 'pairs by contact frequency')
    _DF.from_dict(freqs).round({"freq": 2, "sum": 2}).to_excel(writer,
                                                               sheet_name=sheet2_name,
                                                               startrow=offset,
                                                               startcol=0,
                                                               )
    writer.close()
    print(fname_excel)
    if pop:
        myfig.tight_layout()
        _plt.show()

    return myfig, freqs, plotted_freqs

def pdb(code,
        filename=None,
        verbose=True,
        url="https://files.rcsb.org/download/",
        ):
    r""" Return a :obj:`~mdtraj.Trajectory` from a four-letter PDB code via RCSB PBB lookup

    Thinly wraps around :obj:`mdciao.pdb.pdb2traj`, which wraps around :obj:`mdtraj.load_pdb`
    and prints the corresponding citation.

    Will return None if lookup fails

    Parameters
    ----------
    code : str
        four-letter code, e.g. 3SN6
    filename : str, default is None
        if str, save to this file,
        eventually overwriting
    verbose : bool, default is False
        Be verbose
    url : str, default is 'https://files.rcsb.org/download'
        base URL for lookups

    Returns
    -------
    traj : :obj:`~mdtraj.Trajectory` or None
    """

    return _mdcpdb.pdb2traj(code, filename=filename, verbose=verbose,url=url)

def _res_resolver(res_range, top, fragments, midstring=None, GPCR_UniProt=None, CGN_UniProt=None, KLIFS_string=None,
                  save_nomenclature_files=False, accept_guess=False, **rangeexpand_residues2residxs_kwargs):

    option_dict = {"GPCR": GPCR_UniProt,
                   "CGN": CGN_UniProt,
                   "KLIFS": KLIFS_string}

    option_dict = {key : val for key, val in option_dict.items() if not str(val).lower()=="none"}
    #print(option_dict)

    consensus_frags, consensus_maps, consensus_labelers = \
        _parse_consensus_options_and_return_fragment_defs(option_dict,
                                                          top,
                                                          fragments,
                                                          verbose=True,
                                                          save_nomenclature_files=save_nomenclature_files,
                                                          accept_guess=accept_guess)

    consensus_maps = {key : consensus_maps[key] for key in option_dict.keys()}

    res_idxs_list = _mdcu.residue_and_atom.rangeexpand_residues2residxs(res_range, fragments, top,
                                                                        pick_this_fragment_by_default=None,
                                                                        additional_resnaming_dicts=[consensus_maps if len(consensus_maps)>0 else None][0],
                                                                        **rangeexpand_residues2residxs_kwargs,
                                                                        )

    if midstring is not None:
        print(midstring)

    header = "  ".join(["%10s"%head for head in "residue  residx fragment  resSeq".split()+list(consensus_maps.keys())])
    print(header)
    for idx in res_idxs_list:
        print(_mdcu.residue_and_atom.residue_line("", top.residue(idx),
                                                  _mdcu.lists.in_what_fragment(idx, fragments),
                                                  consensus_maps=consensus_maps,
                                                  table=True))
    return res_idxs_list, consensus_maps, consensus_frags

def residue_selection(expression,
                      top, GPCR_UniProt=None,
                      CGN_UniProt=None,
                      KLIFS_string=None,
                      save_nomenclature_files=False,
                      accept_guess=False,
                      fragments=None):
    r"""
    Find residues in an input topology using Unix filename pattern matching
    like in an 'ls' Unix operation.

    Parameters
    ----------
    expression : str
        Unix-like expressions and ranges are allowed, e.g.
        'GLU,PH*,380-394,3.50,GH.5*.', as are consensus
        descriptors if consensus labels are provided
    top : str, :obj:`~mdtraj.Trajectory`, or :obj:`~mdtraj.Topology`
        The topology to use.
    GPCR_UniProt : str or :obj:`mdciao.nomenclature.LabelerGPCR`, default is None
        For GPCR nomenclature. If str, e.g. "adrb2_human".
        will try to locate a local filename or do a web lookup in the GPCRdb.
        If :obj:`mdciao.nomenclature.LabelerGPCR`, use this object directly
        (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references.
        Please note the difference between UniProt Accession Code
        and UniProt entry name
        as explained `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .
    CGN_UniProt : str or :obj:`mdciao.nomenclature.LabelerCGN`, default is None
        For CGN (G-alpha Numbering definitions) nomenclature. If str, e.g. "gnas2_human",
        try to locate local filenames "gnas2_human.xlsx" or do web lookups
        in the GPCRdb. If :obj:`mdciao.nomenclature.LabelerCGN`, use this object directly
        (allows for object re-use when in API mode)
        See :obj:`mdciao.nomenclature` for more info and references.
    KLIFS_string : str or :obj:`mdciao.nomenclature.LabelerKLIFS`, default is None
        String for kinase KLIFS nomenclature. First, try to locate a local
        file that directly has the `KLIFS_string` as a name. If that fails, then
        combine the filename-format expected by :obj:`mdciao.nomenclature.LabelerKLIFS`
        with `KLIFS_string` to construct a filename and check again.
        If that doesn't work, then go online to contact the KLIFS database.

        For the online lookup in the KLIFS database, the string
        has to be formatted "key:value", which ultimately leads to a given KLIFS entry.
        Acceptable keys and values for `KLIFS_string` are:
            * "UniProtAC", e.g. "UniProtAC:P31751"
            * "kinase_ID", e.g. "kinase_ID:2"
            * "structure_ID", e.g. "structure_ID:1904", e.g. "P31751",
        Please check the documentation on :obj:`mdciao.nomenclature.LabelerKLIFS`
        for a more elaborate explanation on when to pick one of these key:value
        pairs.

        Finally, if `KLIFS_string` is an :obj:`mdciao.nomenclature.LabelerKLIFS`,
        use this object directly (allows for object re-use when in API mode).
        See :obj:`mdciao.nomenclature` for more info and references. Alos, please note
        the difference between UniProt Accession Code
        and UniProt entry name as explained
        `here <https://www.uniprot.org/help/difference%5Faccession%5Fentryname>`_ .

    save_nomenclature_files : bool, default is False
        Save available nomenclature definitions to disk so :
    accept_guess : bool, default is False
        Accept mdciao's guesses regarding fragment
        identification using nomenclature labels
    fragments : str, list, None, default is "lig_resSeq+"
        Topology fragments. There exist several input modes:

        * Name of a fragmentation heuristic, e.g.
          "lig_resSeq+", which is the default of
          and usually yields good results. See
          :obj:`mdciao.fragments.get_fragments`
          for more info on defaults and other heuristics.
        * List of len N that can mix different possibilities:

          * iterable of integers (lists or np.arrays, e.g. np.arange(20,30)
          * ranges expressed as integer strings, "20-30"
          * ranges expressed as residue descriptors ["GLU30-LEU40"]

        * "consensus" : use things like "TM*" or "G.H*", i.e.
          GPCR or CGN-sub-subunit labels.

        Numeric expressions are interpreted as zero-indexed and unique
        residue serial indices, i.e. 30-40 does not necessarily equate
        "GLU30-LEU40" unless serial and sequence index coincide.
        If there's more than one "GLU30", the user gets asked to
        disambiguate. The resulting fragments need not cover
        all of the topology, they only need to not overlap.

    Returns
    -------
    res_idxs_list : np.ndarray
        The residue indices of the residues
        that match the :obj:`expression`
    frags : list of integers
        Whatever fragments the user chose
    consensus_maps : dict
        Keys are currently just 'GPCR' and 'CGN'
        Values are lists of len :obj:`topology.n_residues`
        with the consensus labels. All labels
        will be None if no consensus info
        was provided

    """
    if isinstance(top,_md.Topology):
        _top = top
    else:
        _top = _load_any_geom(top).top

    if fragments is None:
        fragments = [_signature(_mdcfrg.get_fragments).parameters["method"].default]
    _frags = _mdcfrg.fragments._fragments_strings_to_fragments(_mdcu.lists.force_iterable(fragments),
                                                                   _top, verbose=True)[0]
    res_idxs_list, consensus_maps, __ = _res_resolver(expression, _top, _frags,
                                                      midstring="Your selection '%s' yields:" % expression,
                                                      GPCR_UniProt=GPCR_UniProt, CGN_UniProt=CGN_UniProt,
                                                      KLIFS_string=KLIFS_string,
                                                      save_nomenclature_files=save_nomenclature_files,
                                                      accept_guess=accept_guess,
                                                      just_inform=True)

    return res_idxs_list, _frags, consensus_maps


def fragment_overview(topology,
             methods=['all'],
             AAs=None,
             ):

    """
    Prints the fragments obtained by :obj:`get_fragments` for the available methods.

    Optionally, you can pass along a list of residue
    descriptors to be printed after the fragments have
    been shown.

    Parameters
    ----------
    topology :  :obj:`mdtraj.Topology`
        The moleculr topology to fragment
    methods : str or list of strings
        method(s) to be used for obtaining fragments
    AAs : list, default is None
        Anything that :obj:`find_AA` can understand

    Returns
    -------
    fragments_out : dict
        The result of the fragmentation schemes keyed
        by their method name

    """

    return _mdcfrg.overview(topology,methods=methods, AAs=AAs)

def _parse_fragdefs_fragnames_consensus(top, fragments, fragment_names, GPCR_UniProt, CGN_UniProt, KLIFS_string, accept_guess, save_nomenclature_files):
    r"""

    Frankenstein method to parse and handle the many fragment-definition, -naming, -selection options
    taking into account also consensus information

    Ideally, all exposed cli methods should end up using this to parse the many fragment options

    Currently, there's the highly similar (but different method in) :obj:`_res_resolver`

    Internally, uses other parsing methods as well:
    * mdciao.fragments.fragments._fragments_strings_to_fragments
    * mdciao.cli._parse_fragment_naming_options
    * mdciao.cli._parse_consensus_options_and_return_fragment_defs, which itself wraps around
      * mdciao.cli._parse_consensus_option

    Parameters
    ----------
    fragments : comes directly from the top API call (cli.interface(fragments=whatever)
    top : the topology as md.Topology
    GPCR_UniProt : comes directly from the top API call (cli.interface(GPCR_UniProt=whatever)
    CGN_UniProt : comes directly from the top API call (cli.interface(CGN_UniProt=whatever)
    KLIFS_string : comes directly from the top API call (cli.interface(KLIFS_string=whatever)
    fragment_names : comes directly from the top API call (cli.interface(fragment_names=whatever)
    accept_guess : comes directly from the top API call (cli.interface(accept_guess=whatever)
    save_nomenclature_files : boolean

    Returns
    -------
    fragments_as_residue_idxs : list
        The actual fragment definitions as residue indices of top
    fragment_names : list of strings
        The fragment names resulting from user input
    user_wants_consensus : bool
        Whether it was inferred from the value of :obj:`fragments`
        that the user wanted consensus
    consensus_frags : dictionary
        A dictionary of consensus fragments, keyed
        by consensus fragment names (ICL3, 3.5, G.H5...)
        and valued with lists of the residues of :obj:`top` in them
    consensus_labelers : dict
        List of LabelerConsensus-objects if any consensus info was given,
    consensus_maps : dict
        Dicttionary of consensus maps (anything indexable by residx),
        in case any consensus info was given, otherwise the
        lists will be full of Nones. Consensus maps result
        from calling LabelerConsensus.top2labels on `top`
        and have a len top.n_residues
    consensus_frags :
        TODO
    top2confrag : list
        Of len top.n_residues, maps each residue
        index of :obj:`top` to their consensus
        fragment name
    """
    fragments_as_residue_idxs, user_wants_consensus = _mdcfrg.fragments._fragments_strings_to_fragments(fragments, top, verbose=True,
                                                                                                        fragment_names=fragment_names)
    if user_wants_consensus and all([str(cons).lower() == 'none' for cons in [GPCR_UniProt, CGN_UniProt, KLIFS_string]]):
        raise ValueError(
            "User wants to define interface fragments using consensus labels, but no consensus labels were provided via the 'CGN_UniProt' or the 'GPCR_UniProt' arguments.")
    fragment_names = _parse_fragment_naming_options(fragment_names, fragments_as_residue_idxs)
    consensus_frags, consensus_maps, consensus_labelers = \
        _parse_consensus_options_and_return_fragment_defs({"GPCR": GPCR_UniProt,
                                                           "CGN": CGN_UniProt,
                                                           "KLIFS": KLIFS_string},
                                                          top,
                                                          fragments_as_residue_idxs,
                                                          accept_guess=accept_guess,
                                                          save_nomenclature_files=save_nomenclature_files)
    # pop out the Nones of the maps
    consensus_maps = {key : val for key, val in consensus_maps.items() if not all(_np.array(val)==None)}
    top2confrag = _np.full(top.n_residues, None)
    for key, val in consensus_frags.items():
        top2confrag[val] = key
    return fragments_as_residue_idxs, fragment_names, user_wants_consensus, consensus_labelers, consensus_maps, consensus_frags, top2confrag