NMR Restraints

Introduction

NMR constraints (restraints if you prefer) are typically used as input for structure generation programs. Commonly you will generate several sets of structures, each from a separate set of restraints, and use the new structures to improve the restraint set for the next round. Clearly you need to store the restraints used each time, so that you can keep track of which input data were used to generate which structures. To achieve this, each restraint set is stored under a separate NmrConstraintStore object. The idea is that you create the restraint sets and modify them only until you start using them for structure generation. The restraint sets do however remain modifiable, so it is up to you not to modify them when they should not be.

The different types of NMR Constraint are stored in different AbstractConstraintLists that are contained within the NmrConstraintStore and in turn contain SingleConstaints. There are different types of list, e.g. DistanceConstraintList, which contains DistanceConstraints. Most SingleConstraints are described by a targetValue, an upperLimit, a lowerLimit and an error. DistanceConstraints, HBondConstrains, JCouplingConstraints, and RdcConstraints all contain a series of DistanceConstraintItems HBondConstraintsItems etc. that represent alternative assignments of the Constraint, and that are linked to a pair of FixedResonances. ChemShiftConstraints and CsaConstraints have no ConstraintItems and are linked to a single FixedResonance. Finally DihedralConstraints are linked directly to four FixedResonances that identify them, and have DihedralConstraintItems that represent alternative dihedral angle intervals rather than alternative assignments.

It is practical to describe Nmr Constraints using links to Resonances instead of the more traditional links to Atoms. For one thing restraints to unassigned atoms could be used by some programs for network anchoring, or as free-floating pieces of molecule included in the calculation. For another thing restraints that involve prochiral groups need exactly the same kind of bookkeeping as assignment to prochiral groups, which is done through resonances. Since assignments can change we cannot however link the constraints through normal resonances, and so the NmrConstraint package contain a parallel set of objects for assignment. FixedResonances are the NmrConstraint equivalent of Resonances. They are linked to Atoms through FixedAtomSets and FixedResonanceSets that behave exactly the same way as the AtomSets and resonanceSets in the Nmr package (see the Resonance Assignment section).

Setup

The following examples assume that you have loaded the demonstration project, so that we can look at pre-existing data. If you have not already done so, make sure that you load such a CCPN project. This can be done as follows, remembering to change the location of the project directory to that it is appropriate to your system:

from memops.general.Io import loadProject

rootProject = loadProject('/home/user/myProjDirName')

Next we will find some objects to work with the below examples:

# There may be a pause on issuing this command

# while the NMR data is loaded from disk

nmrProject = rootProject.currentNmrProject

Constraint Stores

NmrConstraintStores must be linked to the NmrProject that they arise from, but they are children of the overall MemopsRoot project. They are created as follows:

nmrConstraintStore = rootProject.newNmrConstraintStore(nmrProject=nmrProject)

Distance Constraints

To have something to work with we shall make a DistanceConstraintList. To get one, rather that specifying constraints by hand, we can use an existing utility that will create one from a 3D NOESY peak list. Once we have the constraints we can look through the list and see what values they contain. Firstly we fetch the appropriate peakList.

# Find a peak list

nnoesy = nmrProject.findFirstExperiment(name='N-NOESY')

spectrum = nnoesy.findFirstDataSource()

peakList = spectrum.findFirstPeakList()

Next we define Python function that converts a peak intensity into target, lower and upper distance bounds. Note that this function works on the relative intensity of our peak (compared to the average) to get a distance estimate.

def getDists(relIntensity):

 dist = relIntensity ** (-1/6.0)

 dist *= 3.2

 return dist, dist*0.8, dist*1.2

print getDists(1)

print getDists(10)

print getDists(0.10)

Finally we call the utility function to make the constraints and admire the results.

from ccpnmr.analysis.core import ConstraintBasic

distConstraintList = ConstraintBasic.makeDistConstraints(peakList, nmrConstraintStore,

                                                        distanceFunction=getDists)

# Print distances

for constraint in distConstraintList.sortedConstraints():

 print constraint.lowerLimit, constraint.targetValue, constraint.upperLimit

All AbstractConstraintLists of whatever type are contained inside the NmrConstraintStore and can be accessed in the normal way

allConstraintLists = nmrConstraintStore.constraintLists

for constraintList in allConstraintLists:

 print constraintList.serial, constraintList.className

If you only want one particular type of list, you should do

allDistanceConstrantLists = nmrConstraintStore.findAllConstraintLists(className='DistanceConstraintList')

We can have a look at the constraint list and see how many items (atom pair possibilities) it has.

print len(distConstraintList.constraints)

constraint = distConstraintList.findFirstConstraint()

print constraint

print constraint.items

If we take a look at the first contraint item we see that it links to two (fixed) resonances. The atoms corresponding to these resonances are what is actually restrained in a structure calculation.

fixedResonances = constraint.findFirstItem().resonances

print fixedResonances

# Pint out the resonance's assignment

from ccpnmr.analysis.core import AssignmentBasic

for resonance in fixedResonances:

 print AssignmentBasic.makeResonanceGuiName(resonance)

As an example of accessing the data, the following code will find all DistanceConstraints anywhere that may arise from a given pair of FixedResonances

distanceLists = nmrConstraintStore.findAllConstraintLists(className='DistanceConstraintList')

constraints = []

for constraintList in distanceLists:

 for constraint in constraintList.constraints:

   if constraint.findFirstItem(resonances=fixedResonances):

     constraints.append(constraint)

print constraints

For going from a Constraint to the atoms involved, there is a utility function we can use

constraint = constraints[0]

atomsList = ConstraintBasic.getConstraintAtoms(constraint)

The job is actually quite complex, given the highly ambiguous restraints that can appear, so the atomsList is by the way of being an intermediate result. atomsList is actually a list of lists of lists. The outermost list contains another list for every alternative assignment of the Constraint. The middle list contains another list for each alternative assignment of each FixedResonance in the Constraint assignment. And the innermost list contains the Atom(s) that the FixedAtomSet is linked to. So:

for list1 in atomsList:

 print 'Assign:'

 for list2 in list1:

   print list2

In this simple case we would actually be better off looking at the assignments by hand:

for fixRes in fixedResonances:

 print [x.atoms for x in fixRes.resonanceSet.atomSets]

You can link the fixedResonance to the ordinary resonances, but the link is handled in an unusual manner:

resonance = fixRes.resonance

print resonance

# Below fails

newFixRes = nmrConstraintStore.newFixedResonance(isotopeCode='13C',

                                                resonance=resonance)

# Below works

newFixRes = nmrConstraintStore.newFixedResonance(isotopeCode='13C',

                                                resonanceSerial=resonance.serial)

# or, if you prefer

fixedResonance = ConstraintBasic.makeFixedResonance(nmrConstraintStore,resonance)

As you see you can have several resonances with the same FixedResonance (not that it makes much sense):

print nmrConstraintStore.findAllFixedResonances(resonanceSerial=resonance.serial)

 

AbstractConstraints are linked to Peaks from the Nmr package, so you can identify the peaks that were used to generate a constraint. The link can be set and accessed in the normal way.

constraintPeaks = constraint.peaks

print constraintPeaks

constraint.peaks = []

print constraint.peaks

constraint.peaks = constraintPeaks

print constraint.peaks

For technical reasons the information is actually stored differently, using the ConstraintPeakContrib class. Since the behaviour is exactly the same as for a direct link, we suggest that you pay no attention to this class.

 

Dihedral Contraints

Dihedral Constraints work differently from most other Constraint types. Instead of having a single constraint interval with several alternative assignments, you have a single assignment with several alternative assignments. As an example let us make a restraint for the Psi angle of residue 1. The hardest part is actually getting hold of the correct fixedResonances. Here goes:

# Get the two residues and four atoms

chain = rootProject.findFirstMolSystem().findFirstChain()

res1 = chain.findFirstResidue(seqId=1)

res2 = chain.findFirstResidue(seqId=2)

atoms = [res1.findFirstAtom(name=x) for x in ('N', 'CA', 'C')]

atoms.append(res2.findFirstAtom(name='N'))

# Now make and assign the fixed resonances

fixedResonances = []

for atom in atoms:

 if atom.name == 'N':

   isotopeCode = '15N'

 else:

   isotopeCode = '13C'

 fixRes = nmrConstraintStore.newFixedResonance(name=atom.name,

                                               isotopeCode=isotopeCode)

 fixedResonances.append(fixRes)

 atomSet = nmrConstraintStore.newFixedAtomSet(atoms=(atom,))

 nmrConstraintStore.newFixedResonanceSet(resonances=(fixRes,),

                                         atomSets=(atomSet,))

Once you have the resonances you can make the constraint:

dihedralConstraintList = nmrConstraintStore.newDihedralConstraintList()

dihedralConstraint = dihedralConstraintList.newDihedralConstraint(resonances=fixedResonances)

print dihedralConstraint

Dihedral constraints can constrain the torsion angle to one of several intervals, which therefore have to be set separately. A diredral constraint derived from coupling constants might be e.g. '-160 < phi < -80 or  40 < phi < 80'. To set that you would do:

dihedralConstraint.newDihedralConstraintItem(targetValue=-120, upperLimit=-80, lowerLimit=-160)

dihedralConstraint.newDihedralConstraintItem(targetValue=60, upperLimit=80, lowerLimit=40)

Violations

The model also contains violation lists, to store constraint violations calculated over a set of structures. Violations in a Violation list could refer to any kind of Constraint, they simply store the amount violated, the calculated value and its error, and the fraction of structures where the constraint is violated. There is also a link to a Method object that describes how the violation was calculated - e.g. which kind of averaging was done over the structures. As an example the following would print a rough violation table, using Constraints from constraintList and violations from violationList:

# Assuming we still have our loaded structure

violationList = ConstraintBasic.getStructureViolations(constraintList, structure)

for constraint in constraintList.sortedConstraints():

  violation = violationList.findFirstViolation(constraint=constraint)

 if violation:

   if violation.violation > 0.2:

     print 'VIOL', constraint.serial, constraint.targetValue, violation.violation, violation.fractionViolated