##
# File: RosettaMRParserListener.py
# Date: 04-Mar-2022
#
# Updates:
# Generated from RosettaMRParser.g4 by ANTLR 4.9
""" ParserLister class for ROSETTA MR files.
    @author: Masashi Yokochi
"""
import sys
import re
import copy
import itertools

import numpy as np

from antlr4 import ParseTreeListener

try:
    from wwpdb.utils.nmr.mr.RosettaMRParser import RosettaMRParser
    from wwpdb.utils.nmr.mr.ParserListenerUtil import (checkCoordinates,
                                                       getTypeOfDihedralRestraint,
                                                       REPRESENTATIVE_MODEL_ID,
                                                       DIST_RESTRAINT_RANGE,
                                                       DIST_RESTRAINT_ERROR,
                                                       ANGLE_RESTRAINT_RANGE,
                                                       ANGLE_RESTRAINT_ERROR,
                                                       RDC_RESTRAINT_RANGE,
                                                       RDC_RESTRAINT_ERROR)
    from wwpdb.utils.nmr.ChemCompUtil import ChemCompUtil
    from wwpdb.utils.nmr.BMRBChemShiftStat import BMRBChemShiftStat
    from wwpdb.utils.nmr.NEFTranslator.NEFTranslator import (NEFTranslator,
                                                             ISOTOPE_NUMBERS_OF_NMR_OBS_NUCS)
except ImportError:
    from nmr.mr.RosettaMRParser import RosettaMRParser
    from nmr.mr.ParserListenerUtil import (checkCoordinates,
                                           getTypeOfDihedralRestraint,
                                           REPRESENTATIVE_MODEL_ID,
                                           DIST_RESTRAINT_RANGE,
                                           DIST_RESTRAINT_ERROR,
                                           ANGLE_RESTRAINT_RANGE,
                                           ANGLE_RESTRAINT_ERROR,
                                           RDC_RESTRAINT_RANGE,
                                           RDC_RESTRAINT_ERROR)
    from nmr.ChemCompUtil import ChemCompUtil
    from nmr.BMRBChemShiftStat import BMRBChemShiftStat
    from nmr.NEFTranslator.NEFTranslator import (NEFTranslator,
                                                 ISOTOPE_NUMBERS_OF_NMR_OBS_NUCS)


DIST_RANGE_MIN = DIST_RESTRAINT_RANGE['min_inclusive']
DIST_RANGE_MAX = DIST_RESTRAINT_RANGE['max_inclusive']

DIST_ERROR_MIN = DIST_RESTRAINT_ERROR['min_exclusive']
DIST_ERROR_MAX = DIST_RESTRAINT_ERROR['max_exclusive']


ANGLE_RANGE_MIN = ANGLE_RESTRAINT_RANGE['min_inclusive']
ANGLE_RANGE_MAX = ANGLE_RESTRAINT_RANGE['max_inclusive']

ANGLE_ERROR_MIN = ANGLE_RESTRAINT_ERROR['min_exclusive']
ANGLE_ERROR_MAX = ANGLE_RESTRAINT_ERROR['max_exclusive']


RDC_RANGE_MIN = RDC_RESTRAINT_RANGE['min_inclusive']
RDC_RANGE_MAX = RDC_RESTRAINT_RANGE['max_inclusive']

RDC_ERROR_MIN = RDC_RESTRAINT_ERROR['min_exclusive']
RDC_ERROR_MAX = RDC_RESTRAINT_ERROR['max_exclusive']


# This class defines a complete listener for a parse tree produced by RosettaMRParser.
class RosettaMRParserListener(ParseTreeListener):

    # __verbose = None
    # __lfh = None
    __debug = False

    distRestraints = 0      # ROSETTA: Distance restraints
    angRestraints = 0       # ROSETTA: Angle restraints
    dihedRestraints = 0     # ROSETTA: Dihedral angle restraints
    rdcRestraints = 0       # ROSETTA: Residual dipolar coupling restraints
    geoRestraints = 0       # ROSETTA: Coodinate geometry restraints

    # CCD accessing utility
    __ccU = None

    # BMRB chemical shift statistics
    __csStat = None

    # NEFTranslator
    __nefT = None

    # CIF reader
    # __cR = None
    __hasCoord = False

    # data item name for model ID in 'atom_site' category
    # __modelNumName = None

    # data item names for auth_asym_id, auth_seq_id, auth_atom_id in 'atom_site' category
    # __authAsymId = None
    # __authSeqId = None
    # __authAtomId = None
    # __altAuthAtomId = None

    # polymer sequences in the coordinate file generated by NmrDpUtility.__extractCoordPolymerSequence()
    __hasPolySeq = False
    __polySeq = None
    __altPolySeq = None
    __coordAtomSite = None
    __coordUnobsRes = None
    __labelToAuthSeq = None
    __authToLabelSeq = None
    __preferAuthSeq = True

    # current restraint subtype
    __cur_subtype = None

    # stack of function
    stackFuncs = None

    # collection of atom selection
    atomSelectionSet = None

    # current nested restraint type
    __cur_nest = None

    warningMessage = ''

    def __init__(self, verbose=True, log=sys.stdout, cR=None, polySeq=None,
                 representativeModelId=REPRESENTATIVE_MODEL_ID,
                 coordAtomSite=None, coordUnobsRes=None,
                 labelToAuthSeq=None, authToLabelSeq=None,
                 ccU=None, csStat=None, nefT=None):
        # self.__verbose = verbose
        # self.__lfh = log
        # self.__cR = cR
        self.__hasCoord = cR is not None

        if self.__hasCoord:
            ret = checkCoordinates(verbose, log, cR, polySeq,
                                   representativeModelId,
                                   coordAtomSite, coordUnobsRes,
                                   labelToAuthSeq, authToLabelSeq)
            # self.__modelNumName = ret['model_num_name']
            # self.__authAsymId = ret['auth_asym_id']
            # self.__authSeqId = ret['auth_seq_id']
            # self.__authAtomId = ret['auth_atom_id']
            # self.__altAuthAtomId = ret['alt_auth_atom_id']
            self.__polySeq = ret['polymer_sequence']
            self.__altPolySeq = ret['alt_polymer_sequence']
            self.__coordAtomSite = ret['coord_atom_site']
            self.__coordUnobsRes = ret['coord_unobs_res']
            self.__labelToAuthSeq = ret['label_to_auth_seq']
            self.__authToLabelSeq = ret['auth_to_label_seq']

        self.__hasPolySeq = self.__polySeq is not None and len(self.__polySeq) > 0

        # CCD accessing utility
        self.__ccU = ChemCompUtil(verbose, log) if ccU is None else ccU

        # BMRB chemical shift statistics
        self.__csStat = BMRBChemShiftStat(verbose, log, self.__ccU) if csStat is None else csStat

        # NEFTranslator
        self.__nefT = NEFTranslator(verbose, log, self.__ccU, self.__csStat) if nefT is None else nefT

        self.concat_resnum_chain_pat = re.compile(r'^(\d+)(\S+)$')

    def setDebugMode(self, debug):
        self.__debug = debug

    # Enter a parse tree produced by RosettaMRParser#rosetta_mr.
    def enterRosetta_mr(self, ctx: RosettaMRParser.Rosetta_mrContext):  # pylint: disable=unused-argument
        pass

    # Exit a parse tree produced by RosettaMRParser#rosetta_mr.
    def exitRosetta_mr(self, ctx: RosettaMRParser.Rosetta_mrContext):  # pylint: disable=unused-argument
        if len(self.warningMessage) == 0:
            self.warningMessage = None
        else:
            self.warningMessage = self.warningMessage[0:-1]
            self.warningMessage = '\n'.join(set(self.warningMessage.split('\n')))

    # Enter a parse tree produced by RosettaMRParser#atom_pair_restraints.
    def enterAtom_pair_restraints(self, ctx: RosettaMRParser.Atom_pair_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'dist'

    # Exit a parse tree produced by RosettaMRParser#atom_pair_restraints.
    def exitAtom_pair_restraints(self, ctx: RosettaMRParser.Atom_pair_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#atom_pair_restraint.
    def enterAtom_pair_restraint(self, ctx: RosettaMRParser.Atom_pair_restraintContext):  # pylint: disable=unused-argument
        self.distRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#atom_pair_restraint.
    def exitAtom_pair_restraint(self, ctx: RosettaMRParser.Atom_pair_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name(0)).upper()
        seqId2 = int(str(ctx.Integer(1)))
        atomId2 = str(ctx.Simple_name(1)).upper()

        dstFunc = self.validateDistanceRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1)
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2)

        if len(self.atomSelectionSet) < 2:
            return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2 in itertools.product(self.atomSelectionSet[0],
                                              self.atomSelectionSet[1]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} id={self.distRestraints} "
                      f"atom1={atom1} atom2={atom2} {dstFunc}")

    def validateDistanceRange(self, weight):
        """ Validate distance value range.
        """

        target_value = None
        lower_limit = None
        upper_limit = None
        lower_linear_limit = None
        upper_linear_limit = None

        firstFunc = None
        srcFunc = None

        level = 0
        while self.stackFuncs:
            func = self.stackFuncs.pop()
            if func is not None:
                if firstFunc is None:
                    firstFunc = copy.copy(func)
                if func['name'] in ('SCALARWEIGHTEDFUNC', 'SUMFUNC'):
                    continue
                if 'func_types' in firstFunc:
                    firstFunc['func_types'].append(func['name'])
                if srcFunc is None:
                    srcFunc = copy.copy(func)
                if 'target_value' in func:
                    target_value = func['target_value']
                    del srcFunc['target_value']
                if 'lower_limit' in func:
                    lower_limit = func['lower_limit']
                    del srcFunc['lower_limit']
                if 'upper_limit' in func:
                    upper_limit = func['upper_limit']
                    del srcFunc['upper_limit']
                if 'lower_linear_limit' in func:
                    lower_linear_limit = func['lower_linear_limit']
                    del srcFunc['lower_linear_limit']
                if 'upper_linear_limit' in func:
                    upper_linear_limit = func['upper_linear_limit']
                    del srcFunc['upper_linear_limit']
                level += 1

        if srcFunc is None:  # errors are already caught
            return None

        if level > 1:
            self.warningMessage += f"[Unsupported data] {self.__getCurrentRestraint()}"\
                f"Too complex constraint function {firstFunc} can not be converted to NEF/NMR-STAR data.\n"
            return None

        if target_value is None and lower_limit is None and upper_limit is None\
           and lower_linear_limit is None and upper_linear_limit is None:
            self.warningMessage += f"[Unsupported data] {self.__getCurrentRestraint()}"\
                f"The constraint function {srcFunc} can not be converted to NEF/NMR-STAR data.\n"
            return None

        validRange = True
        dstFunc = {'weight': weight}

        if target_value is not None:
            if DIST_ERROR_MIN < target_value < DIST_ERROR_MAX:
                dstFunc['target_value'] = f"{target_value:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the target value='{target_value}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if lower_limit is not None:
            if DIST_ERROR_MIN < lower_limit < DIST_ERROR_MAX:
                dstFunc['lower_limit'] = f"{lower_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower limit value='{lower_limit}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if upper_limit is not None:
            if DIST_ERROR_MIN < upper_limit < DIST_ERROR_MAX:
                dstFunc['upper_limit'] = f"{upper_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper limit value='{upper_limit}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if lower_linear_limit is not None:
            if DIST_ERROR_MIN < lower_linear_limit < DIST_ERROR_MAX:
                dstFunc['lower_linear_limit'] = f"{lower_linear_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower linear limit value='{lower_linear_limit}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if upper_linear_limit is not None:
            if DIST_ERROR_MIN < upper_linear_limit < DIST_ERROR_MAX:
                dstFunc['upper_linear_limit'] = f"{upper_linear_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper linear limit value='{upper_linear_limit}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if not validRange:
            return None

        if target_value is not None:
            if DIST_RANGE_MIN <= target_value <= DIST_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the target value='{target_value}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if lower_limit is not None:
            if DIST_RANGE_MIN <= lower_limit <= DIST_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower limit value='{lower_limit}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if upper_limit is not None:
            if DIST_RANGE_MIN <= upper_limit <= DIST_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper limit value='{upper_limit}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if lower_linear_limit is not None:
            if DIST_RANGE_MIN <= lower_linear_limit <= DIST_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower linear limit value='{lower_linear_limit}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if upper_linear_limit is not None:
            if DIST_RANGE_MIN <= upper_linear_limit <= DIST_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper linear limit value='{upper_linear_limit}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        return dstFunc

    def assignCoordPolymerSequence(self, seqId, atomId=None, fixedChainId=None):
        """ Assign polymer sequences of the coordinates.
        """

        chainAssign = []

        for ps in self.__polySeq:
            chainId = ps['chain_id']
            if fixedChainId is not None and chainId != fixedChainId:
                continue
            if seqId in ps['seq_id']:
                cifCompId = ps['comp_id'][ps['seq_id'].index(seqId)]
                if atomId is None\
                   or (atomId is not None and len(self.__nefT.get_valid_star_atom(cifCompId, atomId)[0]) > 0):
                    chainAssign.append((chainId, seqId, cifCompId))

        if len(chainAssign) == 0:
            for ps in self.__polySeq:
                chainId = ps['chain_id']
                if fixedChainId is not None and chainId != fixedChainId:
                    continue
                seqKey = (chainId, seqId)
                if seqKey in self.__authToLabelSeq:
                    _, seqId = self.__authToLabelSeq[seqKey]
                    if seqId in ps['seq_id']:
                        cifCompId = ps['comp_id'][ps['seq_id'].index(seqId)]
                        if atomId is None\
                           or (atomId is not None and len(self.__nefT.get_valid_star_atom(cifCompId, atomId)[0]) > 0):
                            chainAssign.append((chainId, seqId, cifCompId))

        if len(chainAssign) == 0 and self.__altPolySeq is not None:
            for ps in self.__altPolySeq:
                chainId = ps['chain_id']
                if fixedChainId is not None and chainId != fixedChainId:
                    continue
                if seqId in ps['auth_seq_id']:
                    cifCompId = ps['comp_id'][ps['auth_seq_id'].index(seqId)]
                    cifSeqId = ps['seq_id'][ps['auth_seq_id'].index(seqId)]
                    chainAssign.append(chainId, cifSeqId, cifCompId)

        if len(chainAssign) == 0:
            if atomId is not None:
                self.warningMessage += f"[Atom not found] {self.__getCurrentRestraint()}"\
                    f"{seqId}:{atomId} is not present in the coordinates.\n"
            else:
                self.warningMessage += f"[Atom not found] {self.__getCurrentRestraint()}"\
                    f"{seqId} is not present in the coordinates.\n"

        return chainAssign

    def selectCoordAtoms(self, chainAssign, seqId, atomId, allowAmbig=True, subtype_name=None):
        """ Select atoms of the coordinates.
        """

        atomSelection = []

        for chainId, cifSeqId, cifCompId in chainAssign:
            seqKey, coordAtomSite = self.getCoordAtomSiteOf(chainId, cifSeqId, self.__hasCoord)

            _atomId = self.__nefT.get_valid_star_atom(cifCompId, atomId)[0]
            lenAtomId = len(_atomId)
            if lenAtomId == 0:
                self.warningMessage += f"[Invalid atom nomenclature] {self.__getCurrentRestraint()}"\
                    f"{seqId}:{atomId} is invalid atom nomenclature.\n"
                continue
            if lenAtomId > 1 and not allowAmbig:
                self.warningMessage += f"[Invalid atom selection] {self.__getCurrentRestraint()}"\
                    f"Ambiguous atom selection '{seqId}:{atomId}' is not allowed as {subtype_name} restraint.\n"
                continue

            for cifAtomId in _atomId:
                atomSelection.append({'chain_id': chainId, 'seq_id': cifSeqId, 'comp_id': cifCompId, 'atom_id': cifAtomId})

                self.testCoordAtomIdConsistency(chainId, cifSeqId, cifCompId, cifAtomId, seqKey, coordAtomSite)

        if len(atomSelection) > 0:
            self.atomSelectionSet.append(atomSelection)

    def selectCoordResidues(self, chainAssign, seqId):
        """ Select residues of the coordinates.
        """

        atomSelection = []

        for chainId, cifSeqId, cifCompId in chainAssign:
            if cifSeqId == seqId:
                atomSelection.append({'chain_id': chainId, 'seq_id': cifSeqId, 'comp_id': cifCompId})

        if len(atomSelection) > 0:
            self.atomSelectionSet.append(atomSelection)

    def testCoordAtomIdConsistency(self, chainId, seqId, compId, atomId, seqKey, coordAtomSite):
        if not self.__hasCoord:
            return

        found = False

        if coordAtomSite is not None:
            if atomId in coordAtomSite['atom_id']:
                found = True
            elif 'alt_atom_id' in coordAtomSite and atomId in coordAtomSite['alt_atom_id']:
                found = True
                # self.__authAtomId = 'auth_atom_id'
            elif self.__preferAuthSeq:
                _seqKey, _coordAtomSite = self.getCoordAtomSiteOf(chainId, seqId, asis=False)
                if _coordAtomSite is not None:
                    if atomId in _coordAtomSite['atom_id']:
                        found = True
                        self.__preferAuthSeq = False
                        # self.__authSeqId = 'label_seq_id'
                        seqKey = _seqKey
                    elif 'alt_atom_id' in _coordAtomSite and atomId in _coordAtomSite['alt_atom_id']:
                        found = True
                        self.__preferAuthSeq = False
                        # self.__authSeqId = 'label_seq_id'
                        # self.__authAtomId = 'auth_atom_id'
                        seqKey = _seqKey

        elif self.__preferAuthSeq:
            _seqKey, _coordAtomSite = self.getCoordAtomSiteOf(chainId, seqId, asis=False)
            if _coordAtomSite is not None:
                if atomId in _coordAtomSite['atom_id']:
                    found = True
                    self.__preferAuthSeq = False
                    # self.__authSeqId = 'label_seq_id'
                    seqKey = _seqKey
                elif 'alt_atom_id' in _coordAtomSite and atomId in _coordAtomSite['alt_atom_id']:
                    found = True
                    self.__preferAuthSeq = False
                    # self.__authSeqId = 'label_seq_id'
                    # self.__authAtomId = 'auth_atom_id'
                    seqKey = _seqKey

        if found:
            return

        if self.__ccU.updateChemCompDict(compId):
            cca = next((cca for cca in self.__ccU.lastAtomList if cca[self.__ccU.ccaAtomId] == atomId), None)
            if cca is not None and seqKey not in self.__coordUnobsRes:
                self.warningMessage += f"[Atom not found] {self.__getCurrentRestraint()}"\
                    f"{chainId}:{seqId}:{compId}:{atomId} is not present in the coordinates.\n"

    def getCoordAtomSiteOf(self, chainId, seqId, cifCheck=True, asis=True):
        seqKey = (chainId, seqId)
        coordAtomSite = None
        if cifCheck:
            preferAuthSeq = self.__preferAuthSeq if asis else not self.__preferAuthSeq
            if preferAuthSeq:
                if seqKey in self.__coordAtomSite:
                    coordAtomSite = self.__coordAtomSite[seqKey]
            else:
                if seqKey in self.__labelToAuthSeq:
                    seqKey = self.__labelToAuthSeq[seqKey]
                    if seqKey in self.__coordAtomSite:
                        coordAtomSite = self.__coordAtomSite[seqKey]
        return seqKey, coordAtomSite

    # Enter a parse tree produced by RosettaMRParser#angle_restraints.
    def enterAngle_restraints(self, ctx: RosettaMRParser.Angle_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'ang'

    # Exit a parse tree produced by RosettaMRParser#angle_restraints.
    def exitAngle_restraints(self, ctx: RosettaMRParser.Angle_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#angle_restraint.
    def enterAngle_restraint(self, ctx: RosettaMRParser.Angle_restraintContext):  # pylint: disable=unused-argument
        self.angRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#angle_restraint.
    def exitAngle_restraint(self, ctx: RosettaMRParser.Angle_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name(0)).upper()
        seqId2 = int(str(ctx.Integer(1)))
        atomId2 = str(ctx.Simple_name(1)).upper()
        seqId3 = int(str(ctx.Integer(2)))
        atomId3 = str(ctx.Simple_name(2)).upper()

        dstFunc = self.validateAngleRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2)
        chainAssign3 = self.assignCoordPolymerSequence(seqId3, atomId3)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0 or len(chainAssign3) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1, False, 'an angle')
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2, False, 'an angle')
        self.selectCoordAtoms(chainAssign3, seqId3, atomId3, False, 'an angle')

        if len(self.atomSelectionSet) < 3:
            return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2, atom3 in itertools.product(self.atomSelectionSet[0],
                                                     self.atomSelectionSet[1],
                                                     self.atomSelectionSet[2]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} id={self.dihedRestraints} "
                      f"atom1={atom1} atom2={atom2} atom3={atom3} {dstFunc}")

    def validateAngleRange(self, weight):
        """ Validate angle value range.
        """

        target_value = None
        lower_limit = None
        upper_limit = None
        lower_linear_limit = None
        upper_linear_limit = None

        firstFunc = None
        srcFunc = None

        level = 0
        while self.stackFuncs:
            func = self.stackFuncs.pop()
            if func is not None:
                if firstFunc is None:
                    firstFunc = copy.copy(func)
                if func['name'] in ('SCALARWEIGHTEDFUNC', 'SUMFUNC'):
                    continue
                if 'func_types' in firstFunc:
                    firstFunc['func_types'].append(func['name'])
                if srcFunc is None:
                    srcFunc = copy.copy(func)
                if 'target_value' in func:
                    target_value = func['target_value']
                    del srcFunc['target_value']
                if 'lower_limit' in func:
                    lower_limit = func['lower_limit']
                    del srcFunc['lower_limit']
                if 'upper_limit' in func:
                    upper_limit = func['upper_limit']
                    del srcFunc['upper_limit']
                if 'lower_linear_limit' in func:
                    lower_linear_limit = func['lower_linear_limit']
                    del srcFunc['lower_linear_limit']
                if 'upper_linear_limit' in func:
                    upper_linear_limit = func['upper_linear_limit']
                    del srcFunc['upper_linear_limit']
                level += 1

        if srcFunc is None:  # errors are already caught
            return None

        if level > 1:
            self.warningMessage += f"[Unsupported data] {self.__getCurrentRestraint()}"\
                f"Too complex constraint function {firstFunc} can not be converted to NEF/NMR-STAR data.\n"
            return None

        if target_value is None and lower_limit is None and upper_limit is None\
           and lower_linear_limit is None and upper_linear_limit is None:
            self.warningMessage += f"[Unsupported data] {self.__getCurrentRestraint()}"\
                f"The constraint function {srcFunc} can not be converted to NEF/NMR-STAR data.\n"
            return None

        validRange = True
        dstFunc = {'weight': weight}

        if target_value is not None:
            if ANGLE_ERROR_MIN < target_value < ANGLE_ERROR_MAX:
                dstFunc['target_value'] = f"{target_value:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the target value='{target_value}' must be within range {ANGLE_RESTRAINT_ERROR}.\n"

        if lower_limit is not None:
            if ANGLE_ERROR_MIN < lower_limit < ANGLE_ERROR_MAX:
                dstFunc['lower_limit'] = f"{lower_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower limit value='{lower_limit}' must be within range {ANGLE_RESTRAINT_ERROR}.\n"

        if upper_limit is not None:
            if ANGLE_ERROR_MIN < upper_limit < ANGLE_ERROR_MAX:
                dstFunc['upper_limit'] = f"{upper_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper limit value='{upper_limit}' must be within range {ANGLE_RESTRAINT_ERROR}.\n"

        if lower_linear_limit is not None:
            if ANGLE_ERROR_MIN < lower_linear_limit < ANGLE_ERROR_MAX:
                dstFunc['lower_linear_limit'] = f"{lower_linear_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower linear limit value='{lower_linear_limit}' must be within range {ANGLE_RESTRAINT_ERROR}.\n"

        if upper_linear_limit is not None:
            if ANGLE_ERROR_MIN < upper_linear_limit < ANGLE_ERROR_MAX:
                dstFunc['upper_linear_limit'] = f"{upper_linear_limit:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper linear limit value='{upper_linear_limit}' must be within range {ANGLE_RESTRAINT_ERROR}.\n"

        if not validRange:
            return None

        if target_value is not None:
            if ANGLE_RANGE_MIN <= target_value <= ANGLE_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the target value='{target_value}' should be within range {ANGLE_RESTRAINT_RANGE}.\n"

        if lower_limit is not None:
            if ANGLE_RANGE_MIN <= lower_limit <= ANGLE_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower limit value='{lower_limit}' should be within range {ANGLE_RESTRAINT_RANGE}.\n"

        if upper_limit is not None:
            if ANGLE_RANGE_MIN <= upper_limit <= ANGLE_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper limit value='{upper_limit}' should be within range {ANGLE_RESTRAINT_RANGE}.\n"

        if lower_linear_limit is not None:
            if ANGLE_RANGE_MIN <= lower_linear_limit <= ANGLE_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the lower linear limit value='{lower_linear_limit}' should be within range {ANGLE_RESTRAINT_RANGE}.\n"

        if upper_linear_limit is not None:
            if ANGLE_RANGE_MIN <= upper_linear_limit <= ANGLE_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"{srcFunc}, the upper linear limit value='{upper_linear_limit}' should be within range {ANGLE_RESTRAINT_RANGE}.\n"

        return dstFunc

    # Enter a parse tree produced by RosettaMRParser#dihedral_restraints.
    def enterDihedral_restraints(self, ctx: RosettaMRParser.Dihedral_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'dihed'

    # Exit a parse tree produced by RosettaMRParser#dihedral_restraints.
    def exitDihedral_restraints(self, ctx: RosettaMRParser.Dihedral_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#dihedral_restraint.
    def enterDihedral_restraint(self, ctx: RosettaMRParser.Dihedral_restraintContext):  # pylint: disable=unused-argument
        self.dihedRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#dihedral_restraint.
    def exitDihedral_restraint(self, ctx: RosettaMRParser.Dihedral_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name(0)).upper()
        seqId2 = int(str(ctx.Integer(1)))
        atomId2 = str(ctx.Simple_name(1)).upper()
        seqId3 = int(str(ctx.Integer(2)))
        atomId3 = str(ctx.Simple_name(2)).upper()
        seqId4 = int(str(ctx.Integer(3)))
        atomId4 = str(ctx.Simple_name(3)).upper()

        dstFunc = self.validateAngleRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2)
        chainAssign3 = self.assignCoordPolymerSequence(seqId3, atomId3)
        chainAssign4 = self.assignCoordPolymerSequence(seqId4, atomId4)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0\
           or len(chainAssign3) == 0 or len(chainAssign4) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1, False, 'a dihedral angle')
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2, False, 'a dihedral angle')
        self.selectCoordAtoms(chainAssign3, seqId3, atomId3, False, 'a dihedral angle')
        self.selectCoordAtoms(chainAssign4, seqId4, atomId4, False, 'a dihedral angle')

        if len(self.atomSelectionSet) < 4:
            return

        compId = self.atomSelectionSet[0][0]['comp_id']
        peptide, nucleotide, carbohydrate = self.__csStat.getTypeOfCompId(compId)

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2, atom3, atom4 in itertools.product(self.atomSelectionSet[0],
                                                            self.atomSelectionSet[1],
                                                            self.atomSelectionSet[2],
                                                            self.atomSelectionSet[3]):
            if self.__debug:
                angleName = getTypeOfDihedralRestraint(peptide, nucleotide, carbohydrate,
                                                       [atom1, atom2, atom3, atom4])
                print(f"subtype={self.__cur_subtype} id={self.dihedRestraints} angleName={angleName} "
                      f"atom1={atom1} atom2={atom2} atom3={atom3} atom4={atom4} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#dihedral_pair_restraints.
    def enterDihedral_pair_restraints(self, ctx: RosettaMRParser.Dihedral_pair_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'dihed'

    # Exit a parse tree produced by RosettaMRParser#dihedral_pair_restraints.
    def exitDihedral_pair_restraints(self, ctx: RosettaMRParser.Dihedral_pair_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#dihedral_pair_restraint.
    def enterDihedral_pair_restraint(self, ctx: RosettaMRParser.Dihedral_pair_restraintContext):  # pylint: disable=unused-argument
        self.dihedRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#dihedral_pair_restraint.
    def exitDihedral_pair_restraint(self, ctx: RosettaMRParser.Dihedral_pair_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name(0)).upper()
        seqId2 = int(str(ctx.Integer(1)))
        atomId2 = str(ctx.Simple_name(1)).upper()
        seqId3 = int(str(ctx.Integer(2)))
        atomId3 = str(ctx.Simple_name(2)).upper()
        seqId4 = int(str(ctx.Integer(3)))
        atomId4 = str(ctx.Simple_name(3)).upper()

        seqId5 = int(str(ctx.Integer(4)))
        atomId5 = str(ctx.Simple_name(4)).upper()
        seqId6 = int(str(ctx.Integer(5)))
        atomId6 = str(ctx.Simple_name(5)).upper()
        seqId7 = int(str(ctx.Integer(6)))
        atomId7 = str(ctx.Simple_name(6)).upper()
        seqId8 = int(str(ctx.Integer(7)))
        atomId8 = str(ctx.Simple_name(7)).upper()

        dstFunc = self.validateAngleRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2)
        chainAssign3 = self.assignCoordPolymerSequence(seqId3, atomId3)
        chainAssign4 = self.assignCoordPolymerSequence(seqId4, atomId4)
        chainAssign5 = self.assignCoordPolymerSequence(seqId5, atomId5)
        chainAssign6 = self.assignCoordPolymerSequence(seqId6, atomId6)
        chainAssign7 = self.assignCoordPolymerSequence(seqId7, atomId7)
        chainAssign8 = self.assignCoordPolymerSequence(seqId8, atomId8)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0\
           or len(chainAssign3) == 0 or len(chainAssign4) == 0\
           or len(chainAssign5) == 0 or len(chainAssign6) == 0\
           or len(chainAssign7) == 0 or len(chainAssign8) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign3, seqId3, atomId3, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign4, seqId4, atomId4, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign5, seqId5, atomId5, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign6, seqId6, atomId6, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign7, seqId7, atomId7, False, 'a dihedral angle pair')
        self.selectCoordAtoms(chainAssign8, seqId8, atomId8, False, 'a dihedral angle pair')

        if len(self.atomSelectionSet) < 8:
            return

        compId = self.atomSelectionSet[0][0]['comp_id']
        peptide, nucleotide, carbohydrate = self.__csStat.getTypeOfCompId(compId)

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2, atom3, atom4 in itertools.product(self.atomSelectionSet[0],
                                                            self.atomSelectionSet[1],
                                                            self.atomSelectionSet[2],
                                                            self.atomSelectionSet[3]):
            if self.__debug:
                angleName = getTypeOfDihedralRestraint(peptide, nucleotide, carbohydrate,
                                                       [atom1, atom2, atom3, atom4])
                print(f"subtype={self.__cur_subtype} id={self.dihedRestraints} angleName={angleName} "
                      f"atom1={atom1} atom2={atom2} atom3={atom3} atom4={atom4}")

        compId = self.atomSelectionSet[4][0]['comp_id']
        peptide, nucleotide, carbohydrate = self.__csStat.getTypeOfCompId(compId)

        for atom1, atom2, atom3, atom4 in itertools.product(self.atomSelectionSet[4],
                                                            self.atomSelectionSet[5],
                                                            self.atomSelectionSet[6],
                                                            self.atomSelectionSet[7]):
            if self.__debug:
                angleName = getTypeOfDihedralRestraint(peptide, nucleotide, carbohydrate,
                                                       [atom1, atom2, atom3, atom4])
                print(f"subtype={self.__cur_subtype} id={self.dihedRestraints} angleName={angleName} "
                      f"atom5={atom1} atom6={atom2} atom7={atom3} atom8={atom4} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#coordinate_restraints.
    def enterCoordinate_restraints(self, ctx: RosettaMRParser.Coordinate_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#coordinate_restraints.
    def exitCoordinate_restraints(self, ctx: RosettaMRParser.Coordinate_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#coordinate_restraint.
    def enterCoordinate_restraint(self, ctx: RosettaMRParser.Coordinate_restraintContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#coordinate_restraint.
    def exitCoordinate_restraint(self, ctx: RosettaMRParser.Coordinate_restraintContext):
        atomId1 = str(ctx.Simple_name(0)).upper()
        _seqId1 = str(ctx.Simple_name(1)).upper()
        atomId2 = str(ctx.Simple_name(2)).upper()
        _seqId2 = str(ctx.Simple_name(3)).upper()

        cartX = float(str(ctx.Float(0)))
        cartY = float(str(ctx.Float(1)))
        cartZ = float(str(ctx.Float(2)))

        if _seqId1.isdecimal():
            seqId1 = int(_seqId1)
            fixedChainId1 = None
        else:
            g = self.concat_resnum_chain_pat.search(_seqId1).groups()
            seqId1 = int(g[0])
            fixedChainId1 = g[1]

        if _seqId2.isdecimal():
            seqId2 = int(_seqId2)
            fixedChainId2 = None
        else:
            g = self.concat_resnum_chain_pat.search(_seqId2).groups()
            seqId2 = int(g[0])
            fixedChainId2 = g[1]

        dstFunc = self.validateDistanceRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1, fixedChainId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2, fixedChainId2)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1)
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2, False, 'a coordinate')  # refAtom

        if len(self.atomSelectionSet) < 2:
            return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2 in itertools.product(self.atomSelectionSet[0],
                                              self.atomSelectionSet[1]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (Coordinate) id={self.geoRestraints} "
                      f"atom={atom1} refAtom={atom2} coord=({cartX}, {cartY}, {cartZ}) {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#local_coordinate_restraints.
    def enterLocal_coordinate_restraints(self, ctx: RosettaMRParser.Local_coordinate_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#local_coordinate_restraints.
    def exitLocal_coordinate_restraints(self, ctx: RosettaMRParser.Local_coordinate_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#local_coordinate_restraint.
    def enterLocal_coordinate_restraint(self, ctx: RosettaMRParser.Local_coordinate_restraintContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#local_coordinate_restraint.
    def exitLocal_coordinate_restraint(self, ctx: RosettaMRParser.Local_coordinate_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name(0)).upper()
        seqId234 = int(str(ctx.Integer(1)))
        atomId2 = str(ctx.Simple_name(1)).upper()
        atomId3 = str(ctx.Simple_name(2)).upper()
        atomId4 = str(ctx.Simple_name(3)).upper()

        cartX = float(str(ctx.Float(0)))
        cartY = float(str(ctx.Float(1)))
        cartZ = float(str(ctx.Float(2)))

        dstFunc = self.validateDistanceRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId234, atomId2)
        chainAssign3 = self.assignCoordPolymerSequence(seqId234, atomId3)
        chainAssign4 = self.assignCoordPolymerSequence(seqId234, atomId4)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0\
           or len(chainAssign3) == 0 or len(chainAssign4) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1)
        self.selectCoordAtoms(chainAssign2, seqId234, atomId2, False, 'a local coordinate')  # originAtom1
        self.selectCoordAtoms(chainAssign3, seqId234, atomId3, False, 'a local coordinate')  # originAtom2
        self.selectCoordAtoms(chainAssign4, seqId234, atomId4, False, 'a local coordiante')  # originAtom3

        if len(self.atomSelectionSet) < 4:
            return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, atom2, atom3, atom4 in itertools.product(self.atomSelectionSet[0],
                                                            self.atomSelectionSet[1],
                                                            self.atomSelectionSet[2],
                                                            self.atomSelectionSet[3]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (LocalCoordinate) id={self.geoRestraints} "
                      f"atom={atom1} originAtom1={atom2} originAtom2={atom3} originAtom3={atom4} "
                      f"localCoord=({cartX}, {cartY}, {cartZ}) {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#site_restraints.
    def enterSite_restraints(self, ctx: RosettaMRParser.Site_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#site_restraints.
    def exitSite_restraints(self, ctx: RosettaMRParser.Site_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#site_restraint.
    def enterSite_restraint(self, ctx: RosettaMRParser.Site_restraintContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#site_restraint.
    def exitSite_restraint(self, ctx: RosettaMRParser.Site_restraintContext):
        seqId1 = int(str(ctx.Integer()))
        atomId1 = str(ctx.Simple_name(0)).upper()
        opposingChainId = str(ctx.Simple_name(1)).upper()

        dstFunc = self.validateDistanceRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)

        if len(chainAssign1) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1)

        if len(self.atomSelectionSet) < 1:
            return

        ps = next((ps for ps in self.__polySeq if ps['chain_id'] == opposingChainId), None)

        if ps is None:
            self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                f"The opposing chain {opposingChainId!r} is not found in the coordinates.\n"
            return

        for atom1 in self.atomSelectionSet[0]:
            chainId = atom1['chain_id']
            if chainId == opposingChainId:
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"The selected atom {chainId}:{atom1['seq_id']}:{atom1['comp_id']}:{atom1['atom_id']} "\
                    f"must not in the opposing chain {opposingChainId!r}.\n"
                return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1 in self.atomSelectionSet[0]:
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (Site) id={self.geoRestraints} "
                      f"atom={atom1} opposingChainId={opposingChainId} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#site_residues_restraints.
    def enterSite_residues_restraints(self, ctx: RosettaMRParser.Site_residues_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#site_residues_restraints.
    def exitSite_residues_restraints(self, ctx: RosettaMRParser.Site_residues_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#site_residues_restraint.
    def enterSite_residues_restraint(self, ctx: RosettaMRParser.Site_residues_restraintContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#site_residues_restraint.
    def exitSite_residues_restraint(self, ctx: RosettaMRParser.Site_residues_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        atomId1 = str(ctx.Simple_name()).upper()
        seqId2 = int(str(ctx.Integer(1)))
        seqId3 = int(str(ctx.Integer(2)))

        dstFunc = self.validateDistanceRange(1.0)

        if dstFunc is None:
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)

        if len(chainAssign1) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1)

        if len(self.atomSelectionSet) < 1:
            return

        chainAssign2 = self.assignCoordPolymerSequence(seqId2)
        chainAssign3 = self.assignCoordPolymerSequence(seqId3)

        if len(chainAssign2) == 0 or len(chainAssign3) == 0:
            return

        self.selectCoordResidues(chainAssign2, seqId2)
        self.selectCoordResidues(chainAssign3, seqId3)

        if len(self.atomSelectionSet) < 3:
            return

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for atom1, res2, res3 in itertools.product(self.atomSelectionSet[0],
                                                   self.atomSelectionSet[1],
                                                   self.atomSelectionSet[2]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (Site-Residue) id={self.geoRestraints} "
                      f"atom1={atom1} residue2={res2} residue3={res3} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#min_residue_atomic_distance_restraints.
    def enterMin_residue_atomic_distance_restraints(self, ctx: RosettaMRParser.Min_residue_atomic_distance_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#min_residue_atomic_distance_restraints.
    def exitMin_residue_atomic_distance_restraints(self, ctx: RosettaMRParser.Min_residue_atomic_distance_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#min_residue_atomic_distance_restraint.
    def enterMin_residue_atomic_distance_restraint(self, ctx: RosettaMRParser.Min_residue_atomic_distance_restraintContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#min_residue_atomic_distance_restraint.
    def exitMin_residue_atomic_distance_restraint(self, ctx: RosettaMRParser.Min_residue_atomic_distance_restraintContext):
        seqId1 = int(str(ctx.Integer(0)))
        seqId2 = int(str(ctx.Integer(1)))
        target_value = float(str(ctx.Float()))

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0:
            return

        self.selectCoordResidues(chainAssign1, seqId1)
        self.selectCoordResidues(chainAssign2, seqId2)

        if len(self.atomSelectionSet) < 2:
            return

        dstFunc = {}
        validRange = True

        if DIST_ERROR_MIN < target_value < DIST_ERROR_MAX:
            dstFunc['target_value'] = f"{target_value:.3f}"
        else:
            validRange = False
            self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                f"The target value='{target_value}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if not validRange:
            return

        if DIST_RANGE_MIN <= target_value <= DIST_RANGE_MAX:
            pass
        else:
            self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                f"The target value='{target_value}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for res1, res2 in itertools.product(self.atomSelectionSet[0],
                                            self.atomSelectionSet[1]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (MinResidueAtomicDistance) id={self.geoRestraints} "
                      f"resudue1={res1} residue2={res2} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#big_bin_restraints.
    def enterBig_bin_restraints(self, ctx: RosettaMRParser.Big_bin_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'dihed'

    # Exit a parse tree produced by RosettaMRParser#big_bin_restraints.
    def exitBig_bin_restraints(self, ctx: RosettaMRParser.Big_bin_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#big_bin_restraint.
    def enterBig_bin_restraint(self, ctx: RosettaMRParser.Big_bin_restraintContext):  # pylint: disable=unused-argument
        self.dihedRestraints += 1

        self.stackFuncs = []
        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#big_bin_restraint.
    def exitBig_bin_restraint(self, ctx: RosettaMRParser.Big_bin_restraintContext):
        seqId = int(str(ctx.Simple_name()))
        binChar = str(ctx.Simple_name())
        sDev = float(str(ctx.Float()))

        if not self.__hasPolySeq:
            return

        chainAssign = self.assignCoordPolymerSequence(seqId)

        if len(chainAssign) == 0:
            return

        self.selectCoordResidues(chainAssign, seqId)

        if len(self.atomSelectionSet) < 1:
            return

        if binChar not in ('O', 'G', 'E', 'A', 'B'):
            self.warningMessage += f"[Enum mismatch] {self.__getCurrentRestraint()}"\
                f"The BigBin identifier '{binChar}' must be one of {('O', 'G', 'E', 'A', 'B')}.\n"
            return

        dstFunc = {}
        validRange = True

        if DIST_ERROR_MIN < sDev < DIST_ERROR_MAX:
            dstFunc['standard_deviation'] = f"{sDev:.3f}"
        else:
            validRange = False
            self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                f"The 'sdev={sDev}' must be within range {DIST_RESTRAINT_ERROR}.\n"

        if not validRange:
            return

        if DIST_RANGE_MIN <= sDev <= DIST_RANGE_MAX:
            pass
        else:
            self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                f"The 'sdev={sDev}' should be within range {DIST_RESTRAINT_RANGE}.\n"

        if self.__cur_nest is not None:
            if self.__debug:
                print(f"NESTED: {self.__cur_nest}")

        for res in self.atomSelectionSet[0]:
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (BigBin) id={self.geoRestraints} "
                      f"residue={res} binChar={binChar} {dstFunc}")

    # Enter a parse tree produced by RosettaMRParser#nested_restraints.
    def enterNested_restraints(self, ctx: RosettaMRParser.Nested_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Exit a parse tree produced by RosettaMRParser#nested_restraints.
    def exitNested_restraints(self, ctx: RosettaMRParser.Nested_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#nested_restraint.
    def enterNested_restraint(self, ctx: RosettaMRParser.Nested_restraintContext):
        n = 0
        while ctx.any_restraint(n):
            n += 1

        self.__cur_nest = {}

        if ctx.MultiConstraint():
            self.__cur_nest['type'] = 'multi'
        elif ctx.AmbiguousConstraint():
            self.__cur_nest['type'] = 'ambig'
        else:
            k = int(str(ctx.Integer()))
            self.__cur_nest['type'] = f"{k}of{n}"

        self.__cur_nest['id'] = -1
        self.__cur_nest['size'] = n

    # Exit a parse tree produced by RosettaMRParser#nested_restraint.
    def exitNested_restraint(self, ctx: RosettaMRParser.Nested_restraintContext):  # pylint: disable=unused-argument
        self.__cur_nest = None

    # Enter a parse tree produced by RosettaMRParser#any_restraint.
    def enterAny_restraint(self, ctx: RosettaMRParser.Any_restraintContext):  # pylint: disable=unused-argument
        self.__cur_nest['id'] = self.__cur_nest['id'] + 1

    # Exit a parse tree produced by RosettaMRParser#any_restraint.
    def exitAny_restraint(self, ctx: RosettaMRParser.Any_restraintContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#func_type_def.
    def enterFunc_type_def(self, ctx: RosettaMRParser.Func_type_defContext):  # pylint: disable=unused-argument
        pass

    # Exit a parse tree produced by RosettaMRParser#func_type_def.
    def exitFunc_type_def(self, ctx: RosettaMRParser.Func_type_defContext):
        """
        (CIRCULARHARMONIC | HARMONIC | SIGMOID | SQUARE_WELL) Float Float |
        BOUNDED Float Float Float Float? Simple_name? |
        PERIODICBOUNDED Float Float Float Float Float? Simple_name? |
        OFFSETPERIODICBOUNDED Float Float Float Float Float Float? Simple_name? |
        (AMBERPERIODIC | CHARMMPERIODIC | FLAT_HARMONIC | TOPOUT) Float Float Float |
        (CIRCULARSIGMOIDAL | LINEAR_PENALTY) Float Float Float Float |
        CIRCULARSPLINE Float+ |
        GAUSSIANFUNC Float Float Simple_name (WEIGHT Float)? |
        SOGFUNC Integer (Float Float Float)+ |
        (MIXTUREFUNC | KARPLUS | SOEDINGFUNC) Float Float Float Float Float Float |
        CONSTANTFUNC Float |
        IDENTITY |
        SCALARWEIGHTEDFUNC Float func_type_def |
        SUMFUNC Integer func_type_def+ |
        SPLINE Simple_name (Float Float Float | NONE Float Float Float (Simple_name Float*)+) // histogram_file_path can not be evaluated
        FADE Float Float Float Float Float? |
        SQUARE_WELL2 Float Float Float DEGREES? |
        ETABLE Float Float Float* |
        USOG Integer (Float Float Float Float)+ |
        SOG Integer (Float Float Float Float Float Float)+;
        """

        func = {}
        valid = True

        if ctx.CIRCULARHARMONIC() or ctx.HARMONIC() or ctx.SIGMOID() or ctx.SQUARE_WELL():
            x0 = float(str(ctx.Float(0)))

            func['x0'] = x0

            if ctx.CIRCULARHARMONIC():  # x0 sd
                funcType = 'CIRCULARHARMONIC'

                sd = float(str(ctx.Float(1)))

                func['sd'] = sd

                if sd <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

                func['target_value'] = x0
                func['lower_limit'] = x0 - sd
                func['upper_limit'] = x0 + sd

            elif ctx.HARMONIC():  # x0 sd
                funcType = 'HARMONIC'

                sd = float(str(ctx.Float(1)))

                func['sd'] = sd

                if sd <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

                func['target_value'] = x0
                func['lower_limit'] = x0 - sd
                func['upper_limit'] = x0 + sd

            elif ctx.SIGMOID():  # x0 m
                funcType = 'SIGMOID'

                m = float(str(ctx.Float(1)))

                func['m'] = m

                if m > 0.0:
                    func['upper_linear_limit'] = x0
                else:
                    func['lower_lienar_limit'] = x0

            else:  # x0 depth
                funcType = 'SQUARE_WELL'

                depth = float(str(ctx.Float(1)))

                func['depth'] = depth

                if depth > 0.0:
                    func['lower_linear_limit'] = x0
                elif depth < 0.0:
                    func['upper_linear_limit'] = x0

            func['name'] = funcType

        elif ctx.BOUNDED():  # lb ub sd rswitch tag
            funcType = 'BOUNDED'
            lb = float(str(ctx.Float(0)))
            ub = float(str(ctx.Float(1)))
            sd = float(str(ctx.Float(2)))
            rswitch = 0.5

            func['name'] = funcType
            func['lb'] = lb
            func['ub'] = ub
            func['sd'] = sd

            if lb > ub:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} lower boundary 'lb={lb}' must be less than or equal to upper boundary 'ub={ub}'.\n"
            if sd <= 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

            if ctx.Float(3):
                rswitch = float(str(ctx.Float(3)))

                func['rswitch'] = rswitch

                if rswitch < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} additional value for switching from the upper limit to the upper linear limit 'rswitch={rswitch}' must not be a negative value.\n"

            if ctx.Simple_name(0):
                func['tag'] = str(ctx.Simple_name(0))

            func['lower_limit'] = lb
            func['upper_limit'] = ub
            func['upper_linear_limit'] = ub + rswitch
            func['lower_linear_limit'] = lb - rswitch

        elif ctx.PERIODICBOUNDED():  # period lb ub sd rswitch tag
            funcType = 'PERIODICBOUNDED'

            period = float(str(ctx.Float(0)))
            lb = float(str(ctx.Float(1)))
            ub = float(str(ctx.Float(2)))
            sd = float(str(ctx.Float(3)))
            rswitch = 0.5

            func['name'] = funcType
            func['period'] = period
            func['lb'] = lb
            func['ub'] = ub
            func['sd'] = sd

            if period < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'period={period}' must not be a negative value.\n"
            if lb > ub:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} lower boundary 'lb={lb}' must be less than or equal to upper boundary 'ub={ub}'.\n"
            if sd <= 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

            if ctx.Float(4):
                rswitch = float(str(ctx.Float(4)))

                func['rswitch'] = rswitch

                if rswitch < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} additional value for switching from the upper limit to the upper linear limit 'rswitch={rswitch}' must not be a negative value.\n"

            if ctx.Simple_name(0):
                func['tag'] = str(ctx.Simple_name(0))

            func['lower_limit'] = lb
            func['upper_limit'] = ub
            func['upper_linear_limit'] = ub + rswitch
            func['lower_linear_limit'] = lb - rswitch

        elif ctx.OFFSETPERIODICBOUNDED():  # offset period lb ub sd rswitch tag
            funcType = 'OFFSETPERIODICBOUNDED'

            offset = float(str(ctx.Float(0)))
            period = float(str(ctx.Float(1)))
            lb = float(str(ctx.Float(2)))
            ub = float(str(ctx.Float(3)))
            sd = float(str(ctx.Float(4)))
            rswitch = 0.5

            func['name'] = funcType
            func['offset'] = offset
            func['period'] = period
            func['lb'] = lb
            func['ub'] = ub
            func['sd'] = sd

            if period < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'period={period}' must not be a negative value.\n"
            if lb > ub:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} lower boundary 'lb={lb}' must be less than or equal to upper boundary 'ub={ub}'.\n"
            if sd <= 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

            if ctx.Float(5):
                rswitch = float(str(ctx.Float(5)))

                func['rswitch'] = rswitch

                if rswitch < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} additional value for switching from the upper limit to the upper linear limit 'rswitch={rswitch}' must not be a negative value.\n"

            if ctx.Simple_name(0):
                func['tag'] = str(ctx.Simple_name(0))

            func['lower_limit'] = lb + offset
            func['upper_limit'] = ub + offset
            func['upper_linear_limit'] = ub + rswitch + offset
            func['lower_linear_limit'] = lb - rswitch + offset

        elif ctx.AMBERPERIODIC() or ctx.CHARMMPERIODIC():  # x0 n_period k
            funcType = 'AMBERPERIODIC' if ctx.AMBERPERIODIC() else 'CHARMMPERIODIC'
            x0 = float(str(ctx.Float(0)))
            n_period = float(str(ctx.Float(1)))
            k = float(str(ctx.Float(2)))

            func['name'] = funcType
            func['x0'] = x0
            func['n_period'] = n_period
            func['k'] = k

            if period < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} periodicity 'n_period={n_period}' must not be a negative value.\n"

        elif ctx.FLAT_HARMONIC() or ctx.TOPOUT():
            funcType = 'FLAT_HARMONIC' if ctx.FLAT_HARMONIC() else 'TOPOUT'

            if ctx.FLAT_HARMONIC():  # x0 sd tol
                x0 = float(str(ctx.Float(0)))
                sd = float(str(ctx.Float(1)))
                tol = float(str(ctx.Float(2)))

                func['x0'] = x0
                func['sd'] = sd
                func['tol'] = tol

                if sd <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"
                if tol < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} tolerance 'tol={tol}' must not be a negative value.\n"

                func['target_value'] = x0
                func['lower_limit'] = x0 - tol - sd
                func['upper_limit'] = x0 + tol + sd

            else:  # weight x0 limit
                weight = float(str(ctx.Float(0)))
                x0 = float(str(ctx.Float(1)))
                limit = float(str(ctx.Float(2)))

                func['weight'] = weight
                func['x0'] = x0
                func['limit'] = limit

                if weight < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} 'weight={weight}' must not be a negative value.\n"
                if limit <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} 'limit={limit}' must be a positive value.\n"

                func['target_value'] = x0
                func['lower_limit'] = x0 - limit
                func['upper_limit'] = x0 + limit

            func['name'] = funcType

        elif ctx.CIRCULARSIGMOIDAL() or ctx.LINEAR_PENALTY():
            funcType = 'CIRCULARSIGMOIDAL' if ctx.CIRCULARSIGMOIDAL() else 'LINEAR_PENALTY'

            if ctx.CIRCULARSIGMOIDAL():  # xC m o1 o2
                xC = float(str(ctx.Float(0)))
                m = float(str(ctx.Float(1)))
                o1 = float(str(ctx.Float(2)))
                o2 = float(str(ctx.Float(3)))

                func['xC'] = xC
                func['m'] = m
                func['o1'] = o1
                func['o2'] = o2

                if m < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} periodicity 'm={m}' must not be a negative value.\n"

            else:  # x0 depth width slope
                x0 = float(str(ctx.Float(0)))
                depth = float(str(ctx.Float(1)))
                width = float(str(ctx.Float(2)))
                slope = float(str(ctx.Float(3)))

                func['x0'] = x0
                func['depth'] = depth
                func['width'] = width
                func['slope'] = slope

                if width < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} 'width={width}' must not be a negative value.\n"
                if slope < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} 'slope={slope}' must not be a negative value.\n"

                func['lower_linear_limit'] = x0 - width
                func['upper_linear_limit'] = x0 + width

            func['name'] = funcType

        elif ctx.CIRCULARSPLINE():  # weight [36 energy values]
            funcType = 'CIRCULARSPLINE'
            weight = float(str(ctx.Float(0)))

            func['name'] = funcType
            func['weight'] = weight

            if weight < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'weight={weight}' must not be a negative value.\n"

            if ctx.Float(36):
                func['energy'] = []
                for i in range(36):
                    func['energy'].append(float(str(ctx.Float(i + 1))))
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires consecutive 36 energy values, following the first weight value.\n"

        elif ctx.GAUSSIANFUNC():  # mean sd tag WEIGHT weight
            funcType = 'GAUSSIANFUNC'
            mean = float(str(ctx.Float(0)))
            sd = float(str(ctx.Float(1)))

            func['name'] = funcType
            func['mean'] = mean
            func['sd'] = sd
            func['tag'] = str(ctx.Simple_name(0))

            if sd <= 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

            if ctx.WEIGHT():
                weight = float(str(ctx.Float(2)))

                func['weight'] = weight

                if weight < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} 'weight={weight}' must not be a negative value.\n"

            func['target_value'] = mean
            func['lower_limit'] = mean - sd
            func['upper_limit'] = mean + sd

        elif ctx.SOGFUNC():  # n_funcs [mean1 sdev1 weight1 [mean2 sdev2 weight2 [...]]]
            funcType = 'SOGFUNC'
            n_funcs = int(str(ctx.Integer()))

            func['name'] = funcType
            func['n_funcs'] = n_funcs

            if n_funcs <= 0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} the number of Gaussian functions 'n_funcs={n_funcs}' must be a positive value.\n"
            elif ctx.Float(n_funcs * 3 - 1):
                func['mean'] = []
                func['sdev'] = []
                func['weight'] = []
                for n in range(n_funcs):
                    p = n * 3
                    mean = float(str(ctx.Float(p)))
                    sdev = float(str(ctx.Float(p + 1)))
                    weight = float(str(ctx.Float(p + 2)))

                    func['mean'].append(mean)
                    func['sdev'].append(sdev)
                    func['weight'].append(weight)

                    if n_funcs == 1:
                        func['target_value'] = mean
                        func['lower_limit'] = mean - sdev
                        func['upper_limit'] = mean + sdev

                    if sdev <= 0.0:
                        valid = False
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            f"{funcType} standard deviation 'sdev={sdev}' of {n+1}th function must be a positive value.\n"
                    if weight < 0.0:
                        valid = False
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            f"{funcType} 'weight={weight}' of {n+1}th function must not be a negative value.\n"
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires consecutive 3 parameters (mean, sdev, weight) for each Gaussian function after the first 'n_funcs' value.\n"

        elif ctx.MIXTUREFUNC() or ctx.KARPLUS() or ctx.SOEDINGFUNC():
            if ctx.MIXTUREFUNC():  # anchor gaussian_param exp_param mixture_param bg_mean bg_sd
                funcType = 'MIXTUREFUNC'
                anchor = float(str(ctx.Float(0)))
                gaussian_param = float(str(ctx.Float(1)))
                exp_param = float(str(ctx.Float(2)))
                mixture_param = float(str(ctx.Float(3)))
                bg_mean = float(str(ctx.Float(4)))
                bg_sd = float(str(ctx.Float(5)))

                func['name'] = funcType
                func['anchor'] = anchor
                func['gaussian_param'] = gaussian_param
                func['exp_param'] = exp_param
                func['mixture_param'] = mixture_param
                func['bg_mean'] = bg_mean
                func['bg_sd'] = bg_sd

                if gaussian_param <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation of a Gaussian distribution 'gaussian_param={gaussian_param}' must be a positive value.\n"
                if exp_param <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} rate at which the exponential distribution drops off 'exp_param={exp_param}' must be a positive value.\n"
                if mixture_param <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} mixture of the Gaussian and Exponential functions 'mixture_param={mixture_param}' that make up g(r) function must be a positive value.\n"
                if bg_sd <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation 'bg_sd={bg_sd}' of h(r) function must be a positive value.\n"

            elif ctx.KARPLUS():  # A B C D x0 sd
                funcType = 'KARPLUS'
                A = float(str(ctx.Float(0)))
                B = float(str(ctx.Float(1)))
                C = float(str(ctx.Float(2)))
                D = float(str(ctx.Float(3)))
                x0 = float(str(ctx.Float(4)))
                sd = float(str(ctx.Float(5)))

                func['A'] = A
                func['B'] = B
                func['C'] = C
                func['D'] = D
                func['x0'] = x0
                func['sd'] = sd

                if sd <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation 'sd={sd}' must be a positive value.\n"

            else:  # w1 mean1 sd1 w2 mean2 sd2
                funcType = 'SOEDINGFUNC'
                w1 = float(str(ctx.Float(0)))
                mean1 = float(str(ctx.Float(1)))
                sd1 = float(str(ctx.Float(2)))
                w2 = float(str(ctx.Float(3)))
                mean2 = float(str(ctx.Float(4)))
                sd2 = float(str(ctx.Float(5)))

                func['w1'] = w1
                func['mean1'] = mean1
                func['sd1'] = sd1
                func['w2'] = w2
                func['mean2'] = mean2
                func['sd2'] = sd2

                if w1 < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} weight of the 1st Gaussian function 'w1={w1}' must not be a negative value.\n"
                if w2 < 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} weight of the 2nd Gaussian function 'w2={w2}' must not be a negative value.\n"
                if sd1 <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation of the 1st Gaussian function 'sd1={sd1}' must be a positive value.\n"
                if sd2 <= 0.0:
                    valid = False
                    self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                        f"{funcType} standard deviation of the 2nd Gaussian function 'sd2={sd2}' must be a positive value.\n"

        elif ctx.CONSTANTFUNC():  # return_val
            funcType = 'CONSTANTFUNC'
            return_val = float(str(ctx.Float(0)))

            func['name'] = funcType
            func['return_val'] = return_val

        elif ctx.IDENTITY():
            func['name'] = 'IDENTITY'

        elif ctx.SCALARWEIGHTEDFUNC():  # weight func_type_def
            funcType = 'SCALARWEIGHTEDFUNC'
            weight = float(str(ctx.Float(0)))

            func['name'] = funcType
            func['weight'] = weight

            if weight < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'weight={weight}' of {n+1}th function must not be a negative value.\n"

            func['func_types'] = []

        elif ctx.SUMFUNC():  # n_funcs Func_Type1 Func_Def1 [Func_Type2 Func_Def2 [...]]
            funcType = 'SUMFUNC'
            n_funcs = int(str(ctx.Integer()))

            func['name'] = funcType
            func['n_funcs'] = n_funcs

            if n_funcs <= 0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} the number of functions 'n_funcs={n_funcs}' must be a positive value.\n"
            elif ctx.func_type_def(n_funcs - 1):
                pass
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires {n_funcs} function definitions after the first 'n_funcs' value.\n"

            func['func_types'] = []

        elif ctx.SPLINE():  # description (NONE) experimental_value weight bin_size (x_axis val*)+
            funcType = 'SPLINE'
            description = str(ctx.Simple_name(0))
            experimental_value = float(str(ctx.Float(0)))
            weight = float(str(ctx.Float(1)))
            bin_size = float(str(ctx.Float(2)))

            func['name'] = funcType
            func['description'] = description
            func['experimental_value'] = experimental_value
            func['weight'] = weight
            func['bin_size'] = bin_size

            if weight < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'weight={weight}' must not be a negative value.\n"
            if bin_size <= 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'bin_size={bin_size}' must be a positive value.\n"

        elif ctx.FADE():  # lb ub d wd [ wo ]
            funcType = 'FADE'
            lb = float(str(ctx.Float(0)))
            ub = float(str(ctx.Float(1)))
            d = float(str(ctx.Float(2)))
            wd = float(str(ctx.Float(3)))
            wo = 0.0

            func['name'] = funcType
            func['lb'] = lb
            func['ub'] = ub
            func['d'] = d  # fade zone
            func['wd'] = wd  # well depth

            if lb > ub:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} lower boundary 'lb={lb}' must be less than or equal to upper boundary 'ub={ub}'.\n"

            if d < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} fade zone 'd={d}' must not be a negative value.\n"

            if ctx.Float(4):
                wo = float(str(ctx.Float(4)))

                func['wo'] = wo  # well offset

        elif ctx.SQUARE_WELL2():  # x0 width depth [DEGREES]
            funcType = 'SQUARE_WELL2'
            x0 = float(str(ctx.Float(0)))
            width = float(str(ctx.Float(2)))
            depth = float(str(ctx.Float(1)))

            func['name'] = funcType
            func['x0'] = x0
            func['width'] = width
            func['depth'] = depth

            if weight < 0.0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'weight={weight}' must not be a negative value.\n"

            if ctx.DEGREES():
                func['unit'] = 'degrees'
                func['lower_linear_limit'] = x0 - width
                func['upper_linear_limit'] = x0 + width
            else:
                func['unit'] = 'radians'
                func['lower_linear_limit'] = np.degrees(x0 - width)
                func['upper_linear_limit'] = np.degrees(x0 + width)

        elif ctx.ETABLE():  # min max [many numbers]
            funcType = 'ETABLE'
            _min = float(str(ctx.Float(0)))
            _max = float(str(ctx.Float(1)))

            func['name'] = funcType
            func['min'] = _min
            func['max'] = _max

            if _min > _max:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} 'min={_min}' must be less than or equal to 'max={_max}'.\n"

            if ctx.Float(2):
                pass
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires parameters after the first 'min' and the second 'max' values.\n"

        elif ctx.USOG():  # num_gaussians mean1 sd1 mean2 sd2...
            funcType = 'USOG'
            num_gaussians = int(str(ctx.Integer()))

            func['name'] = funcType
            func['num_gaussians'] = num_gaussians

            if num_gaussians <= 0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} the number of Gaussian functions 'num_gaussians={num_gaussians}' must be a positive value.\n"
            elif ctx.Float(num_gaussians * 2 - 1):
                func['mean'] = []
                func['sd'] = []
                for n in range(num_gaussians):
                    p = n * 2
                    mean = float(str(ctx.Float(p)))
                    sd = float(str(ctx.Float(p + 1)))

                    func['mean'].append(mean)
                    func['sd'].append(sd)

                    if num_gaussians == 1:
                        func['target_value'] = mean
                        func['lower_limit'] = mean - sd
                        func['upper_limit'] = mean + sd

                    if sd <= 0.0:
                        valid = False
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            f"{funcType} standard deviation 'sd={sd}' of {n+1}th function must be a positive value.\n"
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires consecutive 2 parameters (mean, sd) for each Gaussian function after the first 'num_gaussians' value.\n"

        elif ctx.SOG():  # num_gaussians mean1 sd1 weight1 mean2 sd2 weight2...
            funcType = 'SOG'
            num_gaussians = int(str(ctx.Integer()))

            func['name'] = funcType
            func['num_gaussians'] = num_gaussians

            if num_gaussians <= 0:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} the number of Gaussian functions 'num_gaussians={num_gaussians}' must be a positive value.\n"
            elif ctx.Float(num_gaussians * 3 - 1):
                func['mean'] = []
                func['sd'] = []
                func['weight'] = []
                for n in range(num_gaussians):
                    p = n * 3
                    mean = float(str(ctx.Float(p)))
                    sd = float(str(ctx.Float(p + 1)))
                    weight = float(str(ctx.Float(p + 2)))

                    func['mean'].append(mean)
                    func['sd'].append(sd)
                    func['weight'].append(weight)

                    if num_gaussians == 1:
                        func['target_value'] = mean
                        func['lower_limit'] = mean - sd
                        func['upper_limit'] = mean + sd

                    if sd <= 0.0:
                        valid = False
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            f"{funcType} standard deviation 'sd={sd}' of {n+1}th function must be a positive value.\n"
                    if weight < 0.0:
                        valid = False
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            f"{funcType} 'weight={weight}' of {n+1}th function must not be a negative value.\n"
            else:
                valid = False
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    f"{funcType} requires consecutive 3 parameters (mean, sd, weight) for each Gaussian function after the first 'num_gaussians' value.\n"

        if valid:
            self.stackFuncs.append(func)

    # Enter a parse tree produced by RosettaMRParser#rdc_restraints.
    def enterRdc_restraints(self, ctx: RosettaMRParser.Rdc_restraintsContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'rdc'

    # Exit a parse tree produced by RosettaMRParser#rdc_restraints.
    def exitRdc_restraints(self, ctx: RosettaMRParser.Rdc_restraintsContext):  # pylint: disable=unused-argument
        pass

    # Enter a parse tree produced by RosettaMRParser#rdc_restraint.
    def enterRdc_restraint(self, ctx: RosettaMRParser.Rdc_restraintContext):  # pylint: disable=unused-argument
        self.rdcRestraints += 1

        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#rdc_restraint.
    def exitRdc_restraint(self, ctx: RosettaMRParser.Rdc_restraintContext):
        try:
            seqId1 = int(str(ctx.Integer(0)))
            atomId1 = str(ctx.Simple_name(0)).upper()
            seqId2 = int(str(ctx.Integer(1)))
            atomId2 = str(ctx.Simple_name(1)).upper()
            target_value = float(str(ctx.Float()))
        except ValueError:
            self.rdcRestraints -= 1
            return

        validRange = True
        dstFunc = {'weight': 1.0}

        if target_value is not None:
            if RDC_ERROR_MIN < target_value < RDC_ERROR_MAX:
                dstFunc['target_value'] = f"{target_value:.3f}"
            else:
                validRange = False
                self.warningMessage += f"[Range value error] {self.__getCurrentRestraint()}"\
                    f"The target value='{target_value}' must be within range {RDC_RESTRAINT_ERROR}.\n"

        if not validRange:
            return

        if target_value is not None:
            if RDC_RANGE_MIN < target_value < RDC_RANGE_MAX:
                pass
            else:
                self.warningMessage += f"[Range value warning] {self.__getCurrentRestraint()}"\
                    f"The target value='{target_value}' should be within range {RDC_RESTRAINT_RANGE}.\n"

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1, atomId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2, atomId2)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, atomId1, False, 'an RDC')
        self.selectCoordAtoms(chainAssign2, seqId2, atomId2, False, 'an RDC')

        if len(self.atomSelectionSet) < 2:
            return

        if not self.areUniqueCoordAtoms('an RDC'):
            return

        chain_id_1 = self.atomSelectionSet[0][0]['chain_id']
        seq_id_1 = self.atomSelectionSet[0][0]['seq_id']
        comp_id_1 = self.atomSelectionSet[0][0]['comp_id']
        atom_id_1 = self.atomSelectionSet[0][0]['atom_id']

        chain_id_2 = self.atomSelectionSet[1][0]['chain_id']
        seq_id_2 = self.atomSelectionSet[1][0]['seq_id']
        comp_id_2 = self.atomSelectionSet[1][0]['comp_id']
        atom_id_2 = self.atomSelectionSet[1][0]['atom_id']

        if (atom_id_1[0] not in ISOTOPE_NUMBERS_OF_NMR_OBS_NUCS) or (atom_id_2[0] not in ISOTOPE_NUMBERS_OF_NMR_OBS_NUCS):
            self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                f"Non-magnetic susceptible spin appears in RDC vector; "\
                f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, "\
                f"{chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
            return

        if chain_id_1 != chain_id_2:
            self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                f"Found inter-chain RDC vector; "\
                f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, {chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
            return

        if abs(seq_id_1 - seq_id_2) > 1:
            self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                f"Found inter-residue RDC vector; "\
                f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, {chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
            return

        if abs(seq_id_1 - seq_id_2) == 1:

            if self.__csStat.peptideLike(comp_id_1) and self.__csStat.peptideLike(comp_id_2) and\
               ((seq_id_1 < seq_id_2 and atom_id_1 == 'C' and atom_id_2 in ('N', 'H')) or (seq_id_1 > seq_id_2 and atom_id_1 in ('N', 'H') and atom_id_2 == 'C')):
                pass

            else:
                self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                    "Found inter-residue RDC vector; "\
                    f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, {chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
                return

        elif atom_id_1 == atom_id_2:
            self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                "Found zero RDC vector; "\
                f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, {chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
            return

        else:

            if self.__ccU.updateChemCompDict(comp_id_1):  # matches with comp_id in CCD

                if not any(b for b in self.__ccU.lastBonds
                           if ((b[self.__ccU.ccbAtomId1] == atom_id_1 and b[self.__ccU.ccbAtomId2] == atom_id_2)
                               or (b[self.__ccU.ccbAtomId1] == atom_id_2 and b[self.__ccU.ccbAtomId2] == atom_id_1))):

                    if self.__nefT.validate_comp_atom(comp_id_1, atom_id_1) and self.__nefT.validate_comp_atom(comp_id_2, atom_id_2):
                        self.warningMessage += f"[Invalid data] {self.__getCurrentRestraint()}"\
                            "Found an RDC vector over multiple covalent bonds; "\
                            f"({chain_id_1}:{seq_id_1}:{comp_id_1}:{atom_id_1}, {chain_id_2}:{seq_id_2}:{comp_id_2}:{atom_id_2}).\n"
                        return

        for atom1, atom2 in itertools.product(self.atomSelectionSet[0],
                                              self.atomSelectionSet[1]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (CS-ROSETTA: RDC) id={self.rdcRestraints} "
                      f"atom1={atom1} atom2={atom2} {dstFunc}")

    def areUniqueCoordAtoms(self, subtype_name):
        """ Check whether atom selection sets are uniquely assigned.
        """

        for _atomSelectionSet in self.atomSelectionSet:

            if len(_atomSelectionSet) < 2:
                continue

            for atom1, atom2 in itertools.combinations(_atomSelectionSet, _atomSelectionSet):
                if atom1['chain_id'] != atom2['chain_id']:
                    continue
                if atom1['seq_id'] != atom2['seq_id']:
                    continue
                self.warningMessage += f"[Invalid atom selection] {self.__getCurrentRestraint()}"\
                    f"Ambiguous atom selection '{atom1['chain_id']}:{atom1['seq_id']}:{atom1['atom_id']} or "\
                    f"{atom2['atom_id']}' is not allowed as {subtype_name} restraint.\n"
                return False

        return True

    # Enter a parse tree produced by RosettaMRParser#disulfide_bond_linkages.
    def enterDisulfide_bond_linkages(self, ctx: RosettaMRParser.Disulfide_bond_linkagesContext):  # pylint: disable=unused-argument
        self.__cur_subtype = 'geo'

    # Exit a parse tree produced by RosettaMRParser#disulfide_bond_linkages.
    def exitDisulfide_bond_linkages(self, ctx: RosettaMRParser.Disulfide_bond_linkagesContext):
        pass

    # Enter a parse tree produced by RosettaMRParser#disulfide_bond_linkage.
    def enterDisulfide_bond_linkage(self, ctx: RosettaMRParser.Disulfide_bond_linkageContext):  # pylint: disable=unused-argument
        self.geoRestraints += 1

        self.atomSelectionSet = []

    # Exit a parse tree produced by RosettaMRParser#disulfide_bond_linkage.
    def exitDisulfide_bond_linkage(self, ctx: RosettaMRParser.Disulfide_bond_linkageContext):
        try:
            seqId1 = int(str(ctx.Integer(0)))
            seqId2 = int(str(ctx.Integer(1)))
        except ValueError:
            self.geoRestraints -= 1
            return

        if not self.__hasPolySeq:
            return

        chainAssign1 = self.assignCoordPolymerSequence(seqId1)
        chainAssign2 = self.assignCoordPolymerSequence(seqId2)

        if len(chainAssign1) == 0 or len(chainAssign2) == 0:
            return

        self.selectCoordAtoms(chainAssign1, seqId1, 'SG')
        self.selectCoordAtoms(chainAssign2, seqId2, 'SG')

        if len(self.atomSelectionSet) < 2:
            return

        for atom1 in self.atomSelectionSet[0]:
            if atom1['comp_id'] != 'CYS':
                self.warningMessage += f"[Invalid atom selection] {self.__getCurrentRestraint()}"\
                    f"Failed to select a Cystein residue for disulfide bond between '{seqId1}' and '{seqId2}'.\n"

        for atom2 in self.atomSelectionSet[1]:
            if atom2['comp_id'] != 'CYS':
                self.warningMessage += f"[Invalid atom selection] {self.__getCurrentRestraint()}"\
                    f"Failed to select a Cystein residue for disulfide bond between '{seqId1}' and '{seqId2}'.\n"

        for atom1, atom2 in itertools.product(self.atomSelectionSet[0],
                                              self.atomSelectionSet[1]):
            if self.__debug:
                print(f"subtype={self.__cur_subtype} (CS-ROSETTA: disulfide bond linkage) id={self.geoRestraints} "
                      f"atom1={atom1} atom2={atom2}")

    def __getCurrentRestraint(self):
        if self.__cur_subtype == 'dist':
            return f"[Check the {self.distRestraints}th row of distance restraints] "
        if self.__cur_subtype == 'ang':
            return f"[Check the {self.angRestraints}th row of angle restraints] "
        if self.__cur_subtype == 'dihed':
            return f"[Check the {self.dihedRestraints}th row of dihedral angle restraints] "
        if self.__cur_subtype == 'rdc':
            return f"[Check the {self.rdcRestraints}th row of residual dipolar coupling restraints] "
        if self.__cur_subtype == 'geo':
            return f"[Check the {self.geoRestraints}th row of coordinate geometry restraints] "
        return ''

    def getContentSubtype(self):
        """ Return content subtype of ROSETTA MR file.
        """

        contentSubtype = {'dist_restraint': self.distRestraints,
                          'ang_restraint': self.angRestraints,
                          'dihed_restraint': self.dihedRestraints,
                          'rdc_restraint': self.rdcRestraints,
                          'geo_restraint': self.geoRestraints
                          }

        return {k: 1 for k, v in contentSubtype.items() if v > 0}


# del RosettaMRParser
