Source code for ccpn.core.RestraintContribution

"""
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-11-04 20:12:04 +0000 (Thu, November 04, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

from typing import Sequence, Tuple, Dict

from ccpn.core.Project import Project
from ccpn.core.Restraint import Restraint
from ccpn.core._implementation.AbstractWrapperObject import AbstractWrapperObject
from ccpn.core.lib import CcpnSorting
from ccpn.core.lib import Pid
from ccpnmodel.ccpncore.api.ccp.nmr import NmrConstraint
from ccpn.core.lib.ContextManagers import newObject


[docs]class RestraintContribution(AbstractWrapperObject): """Restraint contribution, corresponding to a set of alternative Atom tuples with associated limits, target value, weight, and other parameters. Simple restraints will have only contribution, whereas more complex restraints can have multiple contributions with different parameters and possibly logical relationships""" #: Short class name, for PID. shortClassName = 'RC' # Attribute it necessary as subclasses must use superclass className className = 'RestraintContribution' _parentClass = Restraint #: Name of plural link to instances of class _pluralLinkName = 'restraintContributions' #: List of child classes. _childClasses = [] # Qualified name of matching API class _apiClassQualifiedName = NmrConstraint.GenericContribution._metaclass.qualifiedName() # CCPN properties @property def _apiContribution(self) -> NmrConstraint.GenericContribution: """ API Contribution matching Contribution""" return self._wrappedData @property def _parent(self) -> Restraint: """Restraint object containing restraintContribution.""" return self._project._data2Obj[self._wrappedData.constraint] restraint = _parent @property def _key(self) -> str: """id string - serial number converted to string""" return str(self._wrappedData.serial) @property def serial(self) -> int: """serial number of RestraintContribution, used in Pid and to identify the RestraintContribution. """ return self._wrappedData.serial @property def targetValue(self) -> float: """targetValue of contribution """ return self._wrappedData.targetValue @targetValue.setter def targetValue(self, value: float): self._wrappedData.targetValue = value @property def error(self) -> float: """error of contribution """ return self._wrappedData.error @error.setter def error(self, value: float): self._wrappedData.error = value @property def weight(self) -> float: """weight of contribution """ return self._wrappedData.weight @weight.setter def weight(self, value: float): self._wrappedData.weight = value @property def additionalLowerLimit(self) -> float: """additionalLowerLimit of contribution Used for potential functions that require more than one parameter, typically for parabolic-linear potentials where the additionalLowerLimit marks the transition from parabolic to linear potential""" return self._wrappedData.additionalLowerLimit @additionalLowerLimit.setter def additionalLowerLimit(self, value: float): self._wrappedData.additionalLowerLimit = value @property def lowerLimit(self) -> float: """lowerLimit of contribution """ return self._wrappedData.lowerLimit @lowerLimit.setter def lowerLimit(self, value: float): self._wrappedData.lowerLimit = value @property def upperLimit(self) -> float: """upperLimit of contribution """ return self._wrappedData.upperLimit @upperLimit.setter def upperLimit(self, value: float): self._wrappedData.upperLimit = value @property def additionalUpperLimit(self) -> float: """additionalUpperLimit of contribution. Used for potential functions that require more than one parameter, typically for parabolic-linear potentials where the additionalUpperLimit marks the transition from parabolic to linear potential""" return self._wrappedData.additionalUpperLimit @additionalUpperLimit.setter def additionalUpperLimit(self, value: float): self._wrappedData.additionalUpperLimit = value @property def scale(self) -> float: """scaling factor (relevant mainly for RDC) to be multiplied with targetValue to get scaled value """ return self._wrappedData.scale @scale.setter def scale(self, value: float): self._wrappedData.scale = value @property def isDistanceDependent(self) -> bool: """Does targetValue depend on a variable distance (where this is relevant, e.g. for Rdc) """ return self._wrappedData.isDistanceDependent @isDistanceDependent.setter def isDistanceDependent(self, value: bool): self._wrappedData.isDistanceDependent = value @property def combinationId(self) -> int: """combinationId of contribution. Contributions with the same combinationId are AND'ed together, where contributions with different combinationId (or combinationId None) are OR'ed""" return self._wrappedData.combinationId @combinationId.setter def combinationId(self, value: int): self._wrappedData.combinationId = value @property def restraintItems(self) -> Tuple[Tuple[str, ...]]: """restraint items of contribution - given as a tuple of tuples of AtomId (not Pid). Example value: (('A.127.ALA.HA','A.130.SER.H'), ('A.93.VAL.HA','A.93.TYR.H')) """ itemLength = self._wrappedData.constraint.parentList.itemLength result = [] sortkey = CcpnSorting.universalSortKey if itemLength == 1: for apiItem in self._wrappedData.items: result.append((_fixedResonance2AtomId(apiItem.resonance),)) else: for apiItem in self._wrappedData.items: atomIds = [_fixedResonance2AtomId(x) for x in apiItem.resonances] if sortkey(atomIds[0]) > sortkey(atomIds[-1]): # order so smallest string comes first # NB This assumes that assignments are either length 2 or ordered (as is so far the case) atomIds.reverse() result.append(tuple(atomIds)) # return tuple(sorted(result, key=sortkey)) @restraintItems.setter def restraintItems(self, value: Sequence[Sequence[str]]): itemLength = self._wrappedData.constraint.parentList.itemLength for ll in value: # make new items if len(ll) != itemLength: raise ValueError("RestraintItems must have length %s: %s" % (itemLength, ll)) apiContribution = self._wrappedData for item in apiContribution.items: # remove old items item.delete() fetchFixedResonance = self._parent._parent._parent._fetchFixedResonance if itemLength == 1: for ll in value: # make new items apiContribution.newSingleAtomItem( resonance=fetchFixedResonance(ll[0])) else: if itemLength == 4: func = apiContribution.newFourAtomItem else: func = apiContribution.newAtomPairItem for ll in value: # make new items func(resonances=tuple(fetchFixedResonance(x) for x in ll))
[docs] def addRestraintItem(self, restraintItem: Sequence[str], _string2Item: Dict = None): """Add a restraint item, given as aa tuple of atomId (NOT Pid). Example value: ('A.127.ALA.HA','A.130.SER.H') The optional _string2Item dictionary speeds up the generation of many restraintItems in a single operation. It must be initialised with StructureData._getTempItemMap(), and serves only for a single StructureData. On principle it must be used for a closed set of operations and then discarded, since it is a cache of underlying FixedResonance objects which are in theory mutable (don't ask)""" itemLength = self._wrappedData.constraint.parentList.itemLength if len(restraintItem) != itemLength: raise ValueError("RestraintItem must have length %s: %s" % (itemLength, restraintItem)) apiContribution = self._wrappedData fetchFixedResonance = self._parent._parent._parent._fetchFixedResonance resonances = [] if _string2Item is None: for ss in restraintItem: resonance = fetchFixedResonance(ss) resonances.append(resonance) else: for ss in restraintItem: resonance = _string2Item.get(ss) if resonance is None: resonance = fetchFixedResonance(ss, checkUniqueness=False) _string2Item[ss] = resonance resonances.append(resonance) if itemLength == 1: # make new item resonance = resonances[0] if apiContribution.findFirstItem(resonance=resonance) is None: apiContribution.newSingleAtomItem(resonance=resonance) else: raise ValueError("Cannot add RestraintItem due to clash with pre-existing item: %s" % restraintItem) elif itemLength == 4: if apiContribution.findFirstItem(resonances=resonances) is None: apiContribution.newFourAtomItem( resonances=resonances ) else: raise ValueError("Cannot add RestraintItem due to clash with pre-existing item: %s" % restraintItem) else: # Assume length 2 if apiContribution.findFirstItem(resonances=resonances) is None: apiContribution.newAtomPairItem( resonances=resonances ) else: raise ValueError("Cannot add RestraintItem due to clash with pre-existing item: %s" % restraintItem) # return self._project._data2Obj[apiContribution]
#========================================================================================= # Implementation functions #========================================================================================= @classmethod def _getAllWrappedData(cls, parent: Restraint) -> list: """get wrappedData - all ConstraintContribution children of parent ConstraintList""" return parent._wrappedData.sortedContributions() def _finaliseAction(self, action: str): if not super()._finaliseAction(action): return # pass on any action in the list to the parent object if action in ['change', 'delete', 'create']: self.restraint._finaliseAction(action)
#========================================================================================= # CCPN functions #========================================================================================= #=========================================================================================== # new'Object' and other methods # Call appropriate routines in their respective locations #=========================================================================================== #========================================================================================= # Connections to parents: #========================================================================================= @newObject(RestraintContribution) def _newRestraintContribution(self: Restraint, targetValue: float = None, error: float = None, weight: float = 1.0, upperLimit: float = None, lowerLimit: float = None, additionalUpperLimit: float = None, additionalLowerLimit: float = None, scale: float = 1.0, isDistanceDependent: bool = False, combinationId: int = None, restraintItems: Sequence = () ) -> RestraintContribution: """Create new RestraintContribution within Restraint See the RestraintContribution class for details. :param targetValue: :param error: :param weight: :param upperLimit: :param lowerLimit: :param additionalUpperLimit: :param additionalLowerLimit: :param scale: :param isDistanceDependent: :param combinationId: :param restraintItems: :return: a new RestraintContribution instance. """ func = self._wrappedData.newGenericContribution obj = func(targetValue=targetValue, error=error, weight=weight, upperLimit=upperLimit, lowerLimit=lowerLimit, additionalUpperLimit=additionalUpperLimit, additionalLowerLimit=additionalLowerLimit, scale=scale, isDistanceDependent=isDistanceDependent, combinationId=combinationId) result = self._project._data2Obj.get(obj) if result is None: raise RuntimeError('Unable to generate new RestraintContribution item') result.restraintItems = restraintItems return result #EJB 20181206: moved to Restraint # Restraint.newRestraintContribution = _newRestraintContribution # del _newRestraintContribution # Notifiers: # Change RestraintContribution when Api RestraintItems are created or deleted for clazz in NmrConstraint.ConstraintItem._metaclass.getNonAbstractSubtypes(): className = clazz.qualifiedName() Project._apiNotifiers.extend( (('_notifyRelatedApiObject', {'pathToObject': 'contribution', 'action': 'change'}, className, 'delete'), ('_notifyRelatedApiObject', {'pathToObject': 'contribution', 'action': 'change'}, className, 'create'), ) ) def _fixedResonance2AtomId(fixedResonance: NmrConstraint.FixedResonance) -> str: """Utility function - get AtomId from FixedResonance """ tags = ('chainCode', 'sequenceCode', 'residueType', 'name') return Pid.createId(*(getattr(fixedResonance, tag) for tag in tags)) #EJB 20181122: moved to _finaliseAction # # Change constraint when ConstraintContribution is creted, deleted, or changed # RestraintContribution._setupCoreNotifier('create', AbstractWrapperObject._finaliseRelatedObject, # {'pathToObject': 'restraint', 'action': 'change'}) # RestraintContribution._setupCoreNotifier('delete', AbstractWrapperObject._finaliseRelatedObject, # {'pathToObject': 'restraint', 'action': 'change'}) # RestraintContribution._setupCoreNotifier('change', AbstractWrapperObject._finaliseRelatedObject, # {'pathToObject': 'restraint', 'action': 'change'})