Source code for schrodinger.application.msv.gui.color

"""
Contains the color scheme constants for msv
"""

import copy
import enum
import math
import re
import weakref
from collections import defaultdict
from collections import namedtuple

import more_itertools
import numpy as np

from schrodinger import structure
from schrodinger.application.msv.gui import color_ramp
from schrodinger.application.msv.gui.viewconstants import RowType
from schrodinger.models import json
from schrodinger.protein import alignment
from schrodinger.protein import annotation
from schrodinger.protein import predictors
from schrodinger.protein import properties
from schrodinger.protein import residue
from schrodinger.Qt import QtGui

ALN_ANNO_TYPES = annotation.ProteinAlignmentAnnotations.ANNOTATION_TYPES
SEQ_ANNO_TYPES = annotation.ProteinSequenceAnnotations.ANNOTATION_TYPES

# Annotations which will use the Text color schemes (e.g. LightModeTextScheme)
TEXT_ANN_TYPES = (SEQ_ANNO_TYPES.resnum, ALN_ANNO_TYPES.consensus_symbols,
                  ALN_ANNO_TYPES.indices, SEQ_ANNO_TYPES.pfam)

# The color used for selection
PALE_BLUE = QtGui.QColor(96, 176, 220, 150)
RES_SEL_COLOR = QtGui.QColor('#006ca6')
GREY = (160, 160, 160)
NO_EMPH_TEXT_COLOR = QtGui.QColor("grey")
NO_BACKGROUND_ALPHA_CUTOFF = 90  # 35%
NO_BACKGROUND_TEXT_COLOR = QtGui.QColor('#b5b5b5')
NO_BACKGROUND_TEXT_COLOR_LM = QtGui.QColor('#555555')
SELECTED_TEXT_COLOR = QtGui.QColor("white")
HM_CHIMERA_PICK_COLOR = QtGui.QColor(244, 182, 66, 150)
HM_CHIMERA_PICK_COLOR_STRUCTURELESS = QtGui.QColor("#f4753d")
BINDING_SITE_PICK_HEX = "#43ebcd"
BINDING_SITE_PICK_HEX_BORDER = "#66db77"
PROXIMITY_PICK_HEX_BORDER = "#148510"

# Colors used for alignment info model
REF_SEQ_BACKGROUND_COLOR = QtGui.QColor('#202020')
REF_SEQ_FONT_COLOR = QtGui.QColor('#e8d18a')
REF_SEQ_SEL_FONT_COLOR = QtGui.QColor('#ffeaab')
REG_FONT_COLOR = QtGui.QColor('#dedede')
REG_SEL_FONT_COLOR = QtGui.QColor('#fff')
NONREF_SEL_COLOR = QtGui.QColor("#60b0dc")
REF_SEQ_SEL_COLOR = QtGui.QColor("#417c9c")
ANN_SEL_COLOR = QtGui.QColor("#565656")
HIDDEN_SEQ_COLOR = QtGui.QColor("#ff0000")
HOMOLOGY_TARGET_COLOR = QtGui.QColor("#4c7b45")
HOMOLOGY_TEMPLATE_COLOR = QtGui.QColor("#a95aa8")
HOVER_COLOR = QtGui.QColor(255, 255, 255, 64)
HOVER_LIGHTER = 125
# Special hover color is shown for cells with only right-click options
SPECIAL_HOVER_COLOR = QtGui.QColor(209, 191, 134, 84)  #D1BF86
HEADER_BACKGROUND_COLOR = QtGui.QColor('#474747')

# Colors used for alignment info model - Light Mode
REF_SEQ_BACKGROUND_COLOR_LM = QtGui.QColor('#ffffff')
REF_SEQ_FONT_COLOR_LM = QtGui.QColor('#0d2498')
REG_FONT_COLOR_LM = QtGui.QColor('#1c1c1c')
HEADER_BACKGROUND_COLOR_LM = QtGui.QColor('#e5e5e5')
HOVER_COLOR_LM = QtGui.QColor(0, 0, 0, 32)
HOVER_LIGHTER_LM = 80

# Colors use to fade structureless and anchored residues
SEQRES_ONLY_FADE = QtGui.QColor('#66242424')  # 40% Alpha
SEQRES_ONLY_FADE_LM = QtGui.QColor('#73ffffff')  # 45% Alpha

ANCHOR_RES_FADE = QtGui.QColor('#73ffffff')  # 45% Alpha
ANCHOR_RES_FADE_LM = QtGui.QColor('#66242424')  # 40% Alpha

NONMATCHING_FADE = QtGui.QColor(114, 114, 114, 76)


[docs]class AbstractRowColorSchemeMeta(type): """ A metaclass for `AbstractRowColorScheme`. Populates `_KEY_ENUM` for `AbstractRowColorScheme` subclasses that define `COLOR_BY_KEY`. """ def __new__(metacls, cls, bases, classdict): # If a class defines a non-empty COLOR_BY_KEY but doesn't explicitly # define KEY_ENUM, use introspection to determine KEY_ENUM if ("COLOR_BY_KEY" in classdict and "_KEY_ENUM" not in classdict and classdict["COLOR_BY_KEY"]): key_type = type(next(iter(classdict["COLOR_BY_KEY"].keys()))) if issubclass(key_type, enum.Enum): classdict["_KEY_ENUM"] = key_type return super().__new__(metacls, cls, bases, classdict)
[docs]class AbstractRowColorScheme(json.JsonableClassMixin, metaclass=AbstractRowColorSchemeMeta): """ A color scheme for a row in MSV. A color scheme subclass instance encapsulates all the color information needed to draw a particular row type, as well as (optionally) the meaning of the colors. Concrete subclasses should provide `ANN_TYPE`, and may optionally provide `NAME`, `COLOR_BY_KEY`, `KEY_FUNC`, `DESCRIPTION`, `TEXT_COLOR`, and `TEXT_COLOR_GAP`. For color schemes that apply to `RowType.Sequence` rows, a unique `NAME` is required. :cvar NAME: The name of the color scheme. For sequence row color schemes, this is the name that will appear in the drop down menu. :vartype NAME: str :cvar ANN_TYPE: The row type or types that this color scheme should be applied to. Only one class in this file may apply to a given annotation type, but multiple classes may apply to `RowType.Sequnce`. :vartype ANN_TYPE: RowType or annotation._AnnotationEnum or list(RowType) or list(annotation._AnnotationEnum) :cvar COLOR_BY_KEY: A dictionary mapping color keys to rgb tuples. May be None if the color scheme doesn't provide any colors (other than text colors). :vartype COLOR_BY_KEY: dict(object, tuple(int, int, int)) or None :car _KEY_ENUM: If the keys of `COLOR_BY_KEY` are enums, gives the enum class. Will be `None` otherwise. Note that this value is automatically populated by `AbstractRowColorSchemeMeta`. Also note that if any key of `COLOR_BY_KEY` is an enum, then all keys must be enums from the same class. This is required to be able to restore a color scheme from JSON, and is enforced via test_color_scheme.py::TestJson::TestSingleEnumType. :vartype _KEY_ENUM: Type[enum.Enum] or None :cvar KEY_NAMES: A dictionary mapping color keys to human-readable key names. May be None if the key values are already human-readable strings or enums with human-readable names. :vartype KEY_NAMES: dict(object, str) or None :cvar KEY_FUNC: A function that takes input of a `residue.Residue` object and outputs the appropriate key for the `COLOR_BY_KEY` dictionary. This defaults to `str`, which leads to keys of one-letter residue abbreviations for `COLOR_BY_KEY`. :vartype KEY_FUNC: function :cvar DESCRIPTION: A textual description of the color scheme and the meaning of each color. Used for the row tooltip. :vartype DESCRIPTION: str :cvar TEXT_COLOR: The color to use for text in residue (i.e. non-gap) cells. :vartype TEXT_COLOR: QtGui.QColor :cvar TEXT_COLOR_GAP: The color to use for text in gap cells. :vartype TEXT_COLOR_GAP: QtGui.QColor :cvar TEXT_COLOR_TERM_GAP: The color to use for text in terminal gap cells. :vartype TEXT_COLOR_TERM_GAP: QtGui.QColor :cvar CHAIN_DIVIDER_COLOR: The color to use for the chain divider indicator when "Split chain view" is disabled and the first residue of the chain is not selected. :vartype CHAIN_DIVIDER_COLOR: QtGui.QColor :cvar SEL_CHAIN_DIVIDER_COLOR: The color to use for the chain divider indicator when "Split chain view" is disabled and the first residue of the chain is selected. :vartype SEL_CHAIN_DIVIDER_COLOR: QtGui.QColor """ NAME = "" ANN_TYPE = None COLOR_BY_KEY = None _KEY_ENUM = None KEY_NAMES = None KEY_FUNC = str DESCRIPTION = "" TEXT_COLOR = QtGui.QColor("black") TEXT_COLOR_GAP = QtGui.QColor("gray") TEXT_COLOR_TERM_GAP = QtGui.QColor("#505050") CHAIN_DIVIDER_COLOR = QtGui.QColor("#cc0000") SEL_CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffff33") DEFAULT_COLOR = (222, 222, 222)
[docs] def __init__(self, *, custom=False): """ :param custom: Whether this color scheme instance will contain user-defined colors (set via the Define Custom Color Scheme dialog). :type custom: bool """ self.custom = custom if self.COLOR_BY_KEY is None: self._color_by_key = {} else: self._color_by_key = self.COLOR_BY_KEY.copy() self._brush_by_key = { key: QtGui.QBrush(QtGui.QColor(*rgb)) for key, rgb in self._color_by_key.items() } self._default_color = tuple(self.DEFAULT_COLOR) self._default_brush = QtGui.QBrush(QtGui.QColor(*self._default_color)) self._custom_res_color_map = weakref.WeakKeyDictionary() self._camelcase_re = re.compile(r"([a-z])([A-Z])") # If KEY_FUNC is a lambda, then it will be wrapped into a method during # class instantiation and the wrapped method will be passed a self # argument. Since our lambdas don't expect a self argument, we undo # this wrapping. self.KEY_FUNC = self.__class__.KEY_FUNC
def __deepcopy__(self, memo): new_scheme = self._instantiateCopy() self._copyColorsToScheme(new_scheme, memo) return new_scheme @property def display_name(self): return self.NAME def _instantiateCopy(self): """ Instantiate and return a copy of this class. Further modifications to this copy will be made in `__deepcopy__`. :rtype: AbstractRowColorScheme """ return self.__class__(custom=self.custom) def _copyColorsToScheme(self, new_scheme, memo): """ Copy colors, brushes, and custom colors from this scheme to a newly created copy of this scheme. :param new_scheme: The scheme to copy colors to. :type new_scheme: AbstractRowColorScheme :param memo: The deepcopy memo :type memo: dict """ new_scheme._color_by_key = copy.deepcopy(self._color_by_key, memo) # deepcopy can't handle QBrushes, so we manually copy _brush_by_key new_scheme._brush_by_key = { key: QtGui.QBrush(brush) for key, brush in self._brush_by_key.items() } new_scheme._custom_res_color_map = copy.deepcopy( self._custom_res_color_map, memo) def __eq__(self, other): return (type(self) is type(other) and self.custom == other.custom and self._default_color == other._default_color and self._color_by_key == other._color_by_key and self._custom_res_color_map == other._custom_res_color_map)
[docs] def toJsonImplementation(self): """ Convert this class to a JSON-able object. See `json.JsonableClassMixin` for additional documentation. .. note:: We don't store custom residue colors (set via `updateCustomResColors`) in the JSON file. These colors are only used for matching the coloring of the Maestro workspace in WorkspaceScheme, so they can easily be regenerated after the JSON file is read in. """ json_dict = {"name": self.NAME, "custom": self.custom} self._updateJsonDict(json_dict) return json_dict
def _updateJsonDict(self, json_dict): """ Add data to the dictionary that will be stored for conversion to JSON. :param json_dict: The dictionary to add data to. :type json_dict: dict """ if self.custom: self._storeColorByKey(json_dict) def _storeColorByKey(self, json_dict): """ Store custom colors from `_color_by_key` in the JSON dictionary. :param json_dict: The dictionary to add data to. :type json_dict: dict """ # dictionaries in JSON must have string keys, so we store color_by_key # as a list of lists instead color_by_key = [] for key, rgb in self._color_by_key.items(): # JSON doesn't support tuples rgb = list(rgb) if self._KEY_ENUM is not None: key = key.name color_by_key.append([key, rgb]) json_dict["color_by_key"] = color_by_key
[docs] @json.adapter(version=50003) def adapter50003(cls, json_dict): """ MSV-3197 """ if cls is AbstractRowColorScheme: if json_dict['name'] == 'Exposure Propensity': json_dict['name'] = 'Exposure Tendency' elif json_dict['name'] == 'Steric Propensity': json_dict['name'] = 'Steric Group' return json_dict
[docs] @classmethod def fromJsonImplementation(cls, json_dict): """ Create a new class instance from a JSON dictionary. See `json.JsonableClassMixin` for additional documentation. :param json_dict: The data that was read in the JSON file. :type json_dict: dict .. note:: We don't store custom residue colors (set via `updateCustomResColors`) in the JSON file. These colors are only used for matching the coloring of the Maestro workspace in WorkspaceScheme, so they can easily be regenerated after the JSON file is read in. """ if cls is AbstractRowColorScheme: name = json_dict.pop("name") return SEQ_SCHEMES_BY_NAME[name].fromJsonImplementation(json_dict) else: scheme = cls._instantiateFromJson(json_dict) if scheme.custom: scheme._restoreColorByKey(json_dict) return scheme
@classmethod def _instantiateFromJson(cls, json_dict): """ Instantiate a class instance from a JSON dictionary. Further modifications to this instance will be made in `fromJsonImplementation`. :rtype: AbstractRowColorScheme """ return cls(custom=json_dict["custom"]) def _restoreColorByKey(self, json_dict): """ Restore custom colors from the JSON dictionary. :param json_dict: The data that was read in the JSON file. :type json_dict: dict """ for key, rgb in json_dict["color_by_key"]: if self._KEY_ENUM: key = self._KEY_ENUM[key] self.setColor(key, *rgb)
[docs] def getColorByKey(self, key): """ Get a color tuple for the specified key. :param key: The key to fetch the color for :type key: object :return: A tuple representing the requested color. :rtype: tuple(int, int, int) """ if key in self._color_by_key: return self._color_by_key[key] else: return self._default_color
[docs] def getBrushByKey(self, key): """ Get a brush for the specified key. Note that the returned brush must not be modified, as that will affect the brush stored in this class. If modifications are necessary, make a copy of the brush first. (The brush is not copied before being returned for performance reasons.) :param key: The key to fetch the color for :type key: object :return: The requested brush. :rtype: QtGui.QBrush """ if key in self._brush_by_key: return self._brush_by_key[key] else: return self._default_brush
[docs] def getBrushByRes(self, res): """ Get the brush for the specified residue. If None or a gap is passed in, None will be returned. Note that this method does not check custom residue colors set via `updateCustomResColors`. Only the colors specified via `COLOR_BY_KEY` or `setColor` will be returned. Use `getColorByRes` is custom residue colors are required. Also note that the returned brush must not be modified, as that will affect the brush stored in this class. If modifications are necessary, make a copy of the brush first. (The brush is not copied before being returned for performance reasons.) :param key: The residue to fetch the color for. :type key: residue.Residue or None :return: The requested brush. :rtype: QtGui.QBrush or None """ # Note: res can be None if beyond the end of a sequence if res is None or res.is_gap: return None key = self.KEY_FUNC(res) # we intentionally don't call getBrushByKey here for performance reasons if key in self._brush_by_key: return self._brush_by_key[key] else: return self._default_brush
[docs] def setColor(self, key, r, g, b): """ Set a new color for the specified key. :param key: The key value to set the color for. :type key: object :param r: The red value of the color. :type r: int :param g: The green value of the color. :type g: int :param b: The blue value of the color. :type b: int """ if key not in self._color_by_key: # Make sure that this is a valid key for this class raise RuntimeError("Invalid key") self._color_by_key[key] = (r, g, b) self._brush_by_key[key] = QtGui.QBrush(QtGui.QColor(r, g, b))
[docs] def getCustomResColorMap(self): """ Get the map of custom residue colors :return: Custom residue color map :rtype: dict(residue.Residue, tuple(int, int, int)) """ return copy.deepcopy(self._custom_res_color_map)
[docs] def updateCustomResColors(self, color_map): """ Apply a custom color to a list of residues using a map of residues keyed to the color tuples to be applied, or None to remove custom colors. :param color_map: Map of residues and the colors to apply :type color_map: dict(residue.Residue, tuple(int, int, int)) .. note:: Residue highlighting (i.e. the "Paint selected" swatches in the "Color Sequences" pop up) are implemented using `schrodinger.application.msv.gui.gui_alignment._ProteinAlignment.setResidueHighlight`. Color scheme custom res colors are currently only used for workspace colors. """ for res, color in color_map.items(): if color is None and res in self._custom_res_color_map: del self._custom_res_color_map[res] else: self._custom_res_color_map[res] = color
def _resNeedsToolTip(self, res) -> bool: """ :return: Whether the given residue should have a tooltip """ return (res is not None and res.is_res and res not in self._custom_res_color_map)
[docs] def getKeys(self): """ Return a list of all possible key values. :rtype: list[object] """ return list(self._color_by_key.keys())
[docs] def prettyKeyName(self, key): """ Get a name for the specified key suitable for presenting to the user. :param key: The key to fetch the name for. :type key: object :return: The key name :rtype: str """ if self.KEY_NAMES and key in self.KEY_NAMES: return self.KEY_NAMES[key] elif isinstance(key, enum.Enum): key_text = key.name.replace("_", " ") key_text = self._camelcase_re.sub(r"\1 \2", key_text) if len(key_text) > 2: # enforce proper capitalization, but don't switch "NA" to # "Na" key_text = key_text.title() return key_text else: return key
[docs] def getSchemeSummary(self): """ Return a dictionary where each key is an RGB tuple and the value is a text description of what that color represents. """ descriptions_by_color = defaultdict(list) for key, color_value in self._color_by_key.items(): desc = self.prettyKeyName(key) descriptions_by_color[color_value].append(desc) return {k: ", ".join(v) for k, v in descriptions_by_color.items()}
[docs] def removeKey(self, key): """ Removes a key color pair from the color dict (also removes the corresponding brush from the brush dict) :param key: The key to remove from the color scheme :type key: object :return: the corresponding color if the key is in the color scheme :rtype: tuple(int, int, int) """ self._brush_by_key.pop(key) return self._color_by_key.pop(key)
[docs] @json.adapter(version=50003) def adapter_50003(self, json_dict): """ For ResiduePropertyScheme we switch from using property names to access structure properties on atoms to SequencePropertys to access structure properties *or* descriptors """ if json_dict["name"] != ResiduePropertyScheme.NAME: return json_dict old_prop_name = json_dict.pop("prop_name") seq_prop = properties.SequenceProperty( property_name=old_prop_name, property_source=properties.PropertySource.Residue, property_type=properties.PropertyType.StructureProperty, ) json_dict["seq_prop"] = seq_prop.toJson() return json_dict
[docs]class ResidueRowColorScheme(AbstractRowColorScheme): """ Color schemes that require the residue at a given position to determine the appropriate color. """
[docs] def getColorByResAndAln(self, res, aln): """ Get a color tuple for the specified residue. If None or a gap is passed in, None will be returned. :param res: The residue to fetch the brush for :type res: residue.Residue or None :param aln: Alignment the residue belongs to, used for determining the color :type aln: schrodinger.protein.alignment.ProteinAlignment :rtype: tuple(int, int, int) :return: A tuple representing (r, g, b) values This method exists on the ResidueRowColorScheme to make it compatible with an AlignmentRowColorScheme. """ return self.getColorByRes(res)
[docs] def getBrushByResAndAln(self, res, aln): """ Get a brush for the specified residue. If None or a gap is passed in, None will be returned. :param res: The residue to fetch the brush for :type res: residue.Residue or None :param aln: Alignment the residue belongs to, used for determining the color :type aln: schrodinger.protein.alignment.ProteinAlignment :return: The requested brush. :rtype: QtGui.QBrush or None This method exists on the ResidueRowColorScheme to make it compatible with an AlignmentRowColorScheme. """ return self.getBrushByRes(res)
[docs] def getColorByRes(self, res): """ Get a color tuple for the specified residue. If None or a gap is passed in, None will be returned. :param key: The residue to fetch the color for. :type key: residue.Residue or None :return: A tuple representing the requested color. :rtype: tuple(int, int, int) or None """ # Note: res can be None if beyond the end of a sequence if res is None or res.is_gap: return None if res in self._custom_res_color_map: return self._custom_res_color_map[res] key = self.KEY_FUNC(res) # we intentionally don't call getColorByKey here for performance reasons if key in self._color_by_key: return self._color_by_key[key] else: return self._default_color
[docs] def getColorKeyToolTipByRes(self, res): """ Get a tool tip describing the color scheme for the specified residue. If None or a gap is passed in, an empty str will be returned. If the residue is set with a custom color, then an empty str will be returned. :param res: The residue to fetch the color key tool tip for. :type res: residue.Residue or None :return: A tool tip describing the color key. :rtype: str """ if not self._resNeedsToolTip(res): return '' key = self.KEY_FUNC(res) return self.prettyKeyName(key)
[docs]class AlignmentRowColorScheme(AbstractRowColorScheme): """ A color scheme that uses both alignment and residue to determine the appropriate color to be used. """
[docs] def getBrushByResAndAln(self, res, aln): """ Given a residue and an alignment, returns a QBrush for the background color :param res: The residue to fetch the brush for :type res: residue.Residue or None :param aln: Alignment the residue belongs to, used for determining the color :type aln: schrodinger.protein.alignment.ProteinAlignment :return: The requested brush. :rtype: QtGui.QBrush or None """ color = self.getColorByResAndAln(res, aln) return QtGui.QBrush(QtGui.QColor(*color))
[docs] def getColorByResAndAln(self, res, aln): """ Given a residue and an alignment, returns a color :param res: Residue to get the color for :type res: residue.Residue :param aln: Alignment the residue belongs to, used for determining the color :type aln: schrodinger.protein.alignment.ProteinAlignment :rtype: tuple(int, int, int) :return: A tuple representing (r, g, b) values """ if res is None: return None if res in self._custom_res_color_map: return self._custom_res_color_map[res] key = self.KEY_FUNC(res, aln) return self._color_by_key.get(key, self._default_color)
[docs] def getColorKeyToolTipByResAndAln(self, res, aln): """ Get a tool tip describing the color scheme for the specified residue. If None or a gap is passed in, an empty str will be returned. If the residue is set with a custom color, then an empty str will be returned. :param res: The residue to fetch the color key tool tip for. :type res: residue.Residue or None :param aln: Alignment the residue belongs to, used for determining the color :type aln: schrodinger.protein.alignment.ProteinAlignment :return: A tool tip describing the color key. :rtype: str """ if not self._resNeedsToolTip(res): return '' key = self.KEY_FUNC(res, aln) return self.prettyKeyName(key)
[docs]class AbstractColorRampScheme(ResidueRowColorScheme): """ A base class for schemes that use a color ramp. """
[docs] def __init__(self, ramp, *, custom=False): """ :param ramp: The color ramp to use. :type ramp: color_ramp.NamedColorRamp :param custom: Whether this color scheme instance contains a user-defined color ramp (set via the Define Custom Color Scheme dialog). :type custom: bool """ super().__init__(custom=custom) self._ramp = ramp
def __eq__(self, other): return super().__eq__(other) and self._ramp == other._ramp def _updateJsonDict(self, json_dict): # See parent class for method documentation super()._updateJsonDict(json_dict) if self.custom: json_dict["ramp"] = self.ramp.name @classmethod def _instantiateFromJson(cls, json_dict): # See parent class for method documentation ramp = cls._getRampFromJson(json_dict) return cls(ramp, custom=json_dict["custom"]) @classmethod def _getRampFromJson(cls, json_dict): """ Retrieve the color ramp stored in the JSON dictionary. :param json_dict: The data that was read in the JSON file. :type json_dict: dict :return: The color ramp, or None if the default color ramp should be used. :rtype: color_ramp.NamedColorRamp or None """ if json_dict["custom"]: ramp_name = json_dict["ramp"] return color_ramp.RAMP_BY_NAME[ramp_name] else: return None
[docs] def removeKey(self, key): """ Override removeKey so that it raises an Exception. """ msg = "removeKey should not be called on a ramp color scheme." raise RuntimeError(msg)
@property def ramp(self): return self._ramp
[docs] def getSchemeSummary(self): ramp = self.ramp desc = {} values = ramp._values for val in values: color_value = tuple(ramp.getRGB(val)) desc[color_value] = f"{val:.1f}" return desc
[docs]class AbstractColorRampOnlyScheme(AbstractColorRampScheme): """ A base class for schemes that use a color ramp and only allow customization of the color ramp. :cvar DEFAULT_RAMP: The color ramp to use if `None` is passed to `__init__`. This ramp must define colors for input values from 0 to 100. This value must be defined in concrete subclasses. :vartype DEFAULT_RAMP: color_ramp.NamedColorRamp """ DEFAULT_RAMP = None
[docs] def __init__(self, ramp=None, *, custom=False): """ :param ramp: The color ramp to use. This ramp must define colors for input values from 0 to 100. If None, `DEFAULT_RAMP` will be used. :type ramp: color_ramp.NamedColorRamp or None :param custom: Whether this color scheme instance contains a user-defined color ramp (set via the Define Custom Color Scheme dialog). :type custom: bool """ if ramp is None: ramp = self.DEFAULT_RAMP super().__init__(ramp, custom=custom)
def _instantiateCopy(self): # See parent class for method documentation return self.__class__(self._ramp, custom=self.custom) def _storeColorByKey(self, json_dict): # See parent class for method documentation # This class only allows for customization of the color ramp, not # per-key colors, so there's nothing to store here. pass def _restoreColorByKey(self, json_dict): # See parent class for method documentation # This class only allows for customization of the color ramp, not # per-key colors, so there's nothing to restore here. pass
[docs]class NucleicAcidColorScheme(ResidueRowColorScheme): """ A color scheme for nucleic acids """ COLOR_BY_KEY = { 'A': (131, 115, 255), #8373ff 'C': (255, 89, 133), #ff5985 'G': (34, 224, 151), #22e097 'T': (216, 227, 47), #d8e32f 'U': (229, 186, 76), #e5ba4c }
[docs]class SingleColorScheme(ResidueRowColorScheme): """ A color scheme that uses a single color for the entire row. Custom residue colors are not supported. Note that `KEY_FUNC` is not used in this class and should not be defined. """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if len(self._color_by_key) > 1: raise ValueError( "%s only supports a single color, but %i colors given." % (self.__class__.__name__, len(self._color_by_key))) if self._color_by_key: self._key_name, self._bg_color = list(self._color_by_key.items())[0] self._bg_brush = self._brush_by_key[self._key_name] else: # No colors were given self._key_name = "" self._bg_color = self._default_color self._bg_brush = self._default_brush
[docs] def getColorByKey(self, key=None): # See parent class for method documentation return self._bg_color
[docs] def getColorByRes(self, res=None): # See parent class for method documentation return self._bg_color
[docs] def getBrushByKey(self, key=None): # See parent class for method documentation return self._bg_brush
[docs] def getBrushByRes(self, res=None): # See parent class for method documentation return self._bg_brush
[docs] def setColor(self, key, r, g, b): # See parent class for method documentation if key != self._key_name: raise ValueError("Trying to set color for key %s, but this scheme " "only supports key %s" % (key, self._key_name)) super().setColor(key, r, g, b) self._bg_color = self._color_by_key[key] self._bg_brush = self._brush_by_key[key]
[docs] def getCustomResColorMap(self): raise RuntimeError("%s does't support custom residues colors." % self.__class__.__name__)
[docs] def updateCustomResColors(self, color_map): raise RuntimeError("%s does't support custom residues colors." % self.__class__.__name__)
[docs]class DarkModeTextScheme(SingleColorScheme): """ A color scheme for rows that always use the default background color and white text. """ ANN_TYPE = TEXT_ANN_TYPES TEXT_COLOR = QtGui.QColor("white") TEXT_COLOR_GAP = QtGui.QColor("white")
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._bg_brush = QtGui.QBrush(QtGui.QColor("#474747")) # Dark Grey
[docs]class LightModeTextScheme(SingleColorScheme): """ A color scheme for rows that use a light background with dark text """ TEXT_COLOR = QtGui.QColor("black") TEXT_COLOR_GAP = QtGui.QColor("black")
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._bg_brush = QtGui.QBrush(QtGui.QColor("#dfdfdf")) # Light Grey
[docs]class SimilarityScheme(AlignmentRowColorScheme): """ A scheme that handles coloring residues according to their similarity to the current reference seq residue at their position in the alignment. """ NAME = "Residue Similarity" ANN_TYPE = RowType.Sequence KEY_FUNC = lambda res, aln: aln.getResidueSimilarity(res) COLOR_BY_KEY = { alignment.ResidueSimilarity.Identical: (255, 64, 64), alignment.ResidueSimilarity.Similar: (255, 128, 0), alignment.ResidueSimilarity.Dissimilar: (255, 255, 255), alignment.ResidueSimilarity.NA: (255, 255, 255), }
[docs]class PositionScheme(AbstractColorRampOnlyScheme): """ A scheme that handles coloring residues according to the length of the sequence setLength must be called to update the color scheme whenever the sequence changes """ NAME = "Residue Position" ANN_TYPE = RowType.Sequence KEY_FUNC = lambda res: res.gapless_idx_in_seq CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffffff") DEFAULT_RAMP = color_ramp.RED_GREEN_VIOLET
[docs] def setLength(self, sequence_length): """ Sets the position color scheme based on the length of the sequence :param sequence_length: The total length of the sequence. :type: int """ if sequence_length == 0: sequence_length = 1 steps = np.linspace(0, 100, sequence_length) self._color_by_key = { i: tuple(self._ramp.getRGB(step)) for i, step in enumerate(steps) } self._brush_by_key = { key: QtGui.QBrush(QtGui.QColor(*rgb)) for key, rgb in self._color_by_key.items() }
[docs] def getLength(self): """ Return the current length that is set for this color scheme. :return: The scheme's current length :rtype: int """ return len(self._brush_by_key)
[docs] def toJsonImplementation(self): # See parent class for method documentation json_dict = super().toJsonImplementation() json_dict["length"] = self.getLength() return json_dict
[docs] @classmethod def fromJsonImplementation(cls, json_dict): # See parent class for method documentation scheme = super().fromJsonImplementation(json_dict) scheme.setLength(json_dict["length"]) return scheme
[docs]class ResidueTypeScheme(ResidueRowColorScheme): NAME = "Residue Type" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { "A": (143, 251, 143), "C": (251, 251, 146), "D": (251, 147, 194), "E": (251, 147, 194), "F": (249, 170, 147), "G": (143, 251, 143), "H": (149, 186, 251), "I": (143, 251, 143), "K": (149, 186, 251), "L": (143, 251, 143), "M": (143, 251, 143), "N": (151, 251, 252), "P": (181, 181, 181), "Q": (251, 222, 146), "R": (149, 186, 251), "S": (151, 251, 252), "T": (151, 251, 252), "V": (143, 251, 143), "W": (249, 170, 147), "Y": (249, 170, 147) }
[docs]class TaylorScheme(ResidueRowColorScheme): NAME = "Taylor Scheme" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { "D": (255, 0, 0), "S": (255, 51, 0), "T": (255, 102, 0), "G": (255, 153, 0), "P": (255, 204, 0), "C": (255, 255, 0), "A": (204, 255, 0), "V": (153, 255, 0), "I": (102, 255, 0), "L": (51, 255, 0), "M": (0, 255, 0), "F": (0, 255, 102), "Y": (0, 255, 204), "W": (0, 204, 255), "H": (0, 102, 255), "R": (0, 0, 255), "K": (102, 0, 255), "N": (204, 0, 255), "Q": (255, 0, 204), "E": (255, 0, 102), } # Order as shown in Taylor 1997 CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffffff")
[docs]class ClustalXScheme(AlignmentRowColorScheme): KEY_FUNC = lambda res, aln: ClustalXScheme.getCategoryByThreshold(res, aln) NAME = 'Clustal X Scheme' ANN_TYPE = RowType.Sequence DEFAULT_COLOR = (222, 222, 222) _BLUE = (143, 178, 227) _CYAN = (77, 170, 179) _GREEN = (116, 191, 78) _MAGENTA = (191, 88, 191) _ORANGE = (198, 121, 153) _RED = (191, 86, 86) _YELLOW = (166, 157, 66) _PINK = (226, 134, 131) _HYDROPHOBIC_GROUP = "WLVIMAFCHP" COLOR_BY_KEY = { "A": _BLUE, "CH": _BLUE, # Cysteine is sometimes blue . . . "C": _PINK, # . . . and sometimes pink "D": _MAGENTA, "E": _MAGENTA, "F": _BLUE, "G": _ORANGE, "H": _CYAN, "I": _BLUE, "K": _RED, "L": _BLUE, "M": _BLUE, "N": _GREEN, "P": _YELLOW, "Q": _GREEN, "R": _RED, "S": _GREEN, "T": _GREEN, "V": _BLUE, "W": _BLUE, "Y": _CYAN, } # yapf: disable THRESHOLDS = { "A": ((_HYDROPHOBIC_GROUP, 0.6),), "I": ((_HYDROPHOBIC_GROUP, 0.6),), "L": ((_HYDROPHOBIC_GROUP, 0.6),), "M": ((_HYDROPHOBIC_GROUP, 0.6),), "F": ((_HYDROPHOBIC_GROUP, 0.6),), "W": ((_HYDROPHOBIC_GROUP, 0.6),), "V": ((_HYDROPHOBIC_GROUP, 0.6),), "K": (("KR", 0.6), ("K", 0.8), ("R", 0.8), ("Q", 0.8)), "R": (("KR", 0.6), ("K", 0.8), ("R", 0.8), ("Q", 0.8)), "E": (("KR", 0.6), ("QE", 0.5), ("E", 0.85), ("Q", 0.85), ("D", 0.85)), "D": (("KR", 0.6), ("K", 0.85), ("R", 0.85), ("Q", 0.85), ("ED", 0.5)), "N": (("N", 0.5), ("Y", 0.85)), "Q": (("KR", 0.6), ("QE", 0.5), ("QEKR", 0.85)), "S": ((_HYDROPHOBIC_GROUP, 0.6), ("TS", 0.5), ("S", 0.85), ("T", 0.85)), "T": ((_HYDROPHOBIC_GROUP, 0.6), ("TS", 0.5), ("S", 0.85), ("T", 0.85)), "H": ((_HYDROPHOBIC_GROUP, 0.6), ("W", 0.85), ("Y", 0.85), ("A", 0.85), ("C", 0.85), ("P", 0.85), ("Q", 0.85), ("F", 0.85), ("H", 0.85), ("I", 0.85), ("L", 0.85), ("M", 0.85), ("V", 0.85)), "Y": ((_HYDROPHOBIC_GROUP, 0.6), ("W", 0.85), ("Y", 0.85), ("A", 0.85), ("C", 0.85), ("P", 0.85), ("Q", 0.85), ("F", 0.85), ("H", 0.85), ("I", 0.85), ("L", 0.85), ("M", 0.85), ("V", 0.85)), } #yapf: enable
[docs] @classmethod def getCategoryByThreshold(cls, res, aln): """ Return the resname for the residue only if it meets certain conditions. The returned resname is matched with a color in the COLOR_BY_KEY dict. If None is returned, the DEFAULT_COLOR will be used. See http://www.jalview.org/help/html/colourSchemes/clustal.html for rules. """ res, _ = cls._getCategoryByThreshold(res, aln) return res
[docs] def getColorKeyToolTipByResAndAln(self, res, aln): # See parent class for documentation if not self._resNeedsToolTip(res): return '' resname, rule = self._getCategoryByThreshold(res, aln) if not resname: return 'Unconserved' group, percentage = rule percentage = percentage * 100 return f">{percentage}% {group}"
@classmethod def _getCategoryByThreshold(cls, res, aln): """ Helper function that returns both the resname and the rule only if it meets certain conditions. See getCategoryByThreshold for details. """ unconserved = (None, None) if res is None or res.is_gap: return unconserved resname = str(res).upper() if resname == "X": return unconserved if resname in ("G", "P"): return resname, (resname, 0.0) column = [ str(res) for res in aln.getColumn(res.idx_in_seq, omit_gaps=True) ] len_column = len(column) def above_threshold(residue_group, percentage): """ Return whether the percentage of the residues in the column that belong to the residue_group rises above percentage """ num_matching = sum( 1 for resname in column if resname in residue_group) return (num_matching / len_column) > percentage if resname == "C": # Cysteine has more complicated rules than the other residues cystein_test = ("C", 0.85) cystein_hydrophobic_test = (cls._HYDROPHOBIC_GROUP, 0.6) if above_threshold(*cystein_test): return "C", cystein_test elif above_threshold(*cystein_hydrophobic_test): return "CH", cystein_hydrophobic_test else: return unconserved tests = cls.THRESHOLDS[resname] # If the residue meets any of the tests, then return the resname and # the test for test in tests: if above_threshold(*test): return resname, test return unconserved # None of the criteria are met
[docs] def getSchemeSummary(self): return { self._BLUE: "Hydrophobic", self._RED: "Positive charge", self._MAGENTA: "Negative charge", self._GREEN: "Polar", self._PINK: "Cysteine", self._ORANGE: "Glycine", self._YELLOW: "Proline", self._CYAN: "Aromatic", self.DEFAULT_COLOR: "Unconserved", }
[docs]class AlignmentSetScheme(AlignmentRowColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.alignment_set COLOR_BY_KEY = dict( enumerate(( (141, 211, 199), (255, 255, 179), (190, 186, 218), (251, 128, 114), (128, 177, 211), (253, 180, 98), (179, 222, 105), (252, 205, 229), )))
[docs] @classmethod def KEY_FUNC(cls, res, aln): seq = res.sequence aln_set = aln.alnSetForSeq(seq) if aln_set is None: return set_index = list(aln.alnSets()).index(aln_set) color_index = set_index % len(cls.COLOR_BY_KEY) return color_index
[docs]class HelixPropensityScheme(ResidueRowColorScheme): NAME = "Helix Propensity" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.helix_propensity) COLOR_BY_KEY = { residue.HELIX_PROPENSITY.Likely: (249, 146, 146), residue.HELIX_PROPENSITY.Weak: (194, 147, 251), residue.HELIX_PROPENSITY.Ambivalent: (181, 181, 181), residue.HELIX_PROPENSITY.HelixBreaking: (149, 186, 251), residue.HELIX_PROPENSITY.NoPropensity: (222, 222, 222), } KEY_FUNC = lambda res: res.helix_propensity DESCRIPTION = ("Helix Propensity Color Code:\n" + "\n".join([ "red - helix-forming", "magenta - weak helix-forming", "grey - ambivalent", "blue - helix-breaking", "white - no propensity" ]))
[docs]class StrandPropensityScheme(ResidueRowColorScheme): NAME = "Strand Propensity" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.beta_strand_propensity) COLOR_BY_KEY = { residue.BETA_STRAND_PROPENSITY.StrandForming: (249, 146, 146), residue.BETA_STRAND_PROPENSITY.Ambivalent: (181, 181, 181), residue.BETA_STRAND_PROPENSITY.StrandBreaking: (149, 186, 251), residue.BETA_STRAND_PROPENSITY.NoPropensity: (222, 222, 222), } KEY_FUNC = lambda res: res.beta_strand_propensity DESCRIPTION = ("Beta Strand Color Code:\n" + "\n".join([ "blue - strand-forming", "grey - ambivalent", "red - strand-breaking" ]))
[docs]class TurnPropensityScheme(ResidueRowColorScheme): NAME = "Turn Propensity" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.turn_propensity) COLOR_BY_KEY = { residue.TURN_PROPENSITY.TurnForming: (151, 251, 252), residue.TURN_PROPENSITY.Ambivalent: (181, 181, 181), residue.TURN_PROPENSITY.TurnBreaking: (250, 147, 251), residue.TURN_PROPENSITY.NoPropensity: (222, 222, 222), } KEY_FUNC = lambda res: res.turn_propensity DESCRIPTION = ("Turn Propensity Color Code:\n" + "\n".join([ "cyan - turn-forming", "grey - ambivalent", "magenta - turn-breaking" ]))
[docs]class HelixTerminationTendencyScheme(ResidueRowColorScheme): NAME = "Helix Termination" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.helix_termination_tendency) COLOR_BY_KEY = { residue.HELIX_TERMINATION_TENDENCY.NoTendency: (222, 222, 222), residue.HELIX_TERMINATION_TENDENCY.HelixStarting: (143, 251, 143), residue.HELIX_TERMINATION_TENDENCY.Ambivalent: (181, 181, 181), residue.HELIX_TERMINATION_TENDENCY.HelixEnding: (249, 146, 146) } KEY_FUNC = lambda res: res.helix_termination_tendency DESCRIPTION = ("Helix Termination Tendency:\n" + "\n".join( ["green - helix-starting", "grey - ambivalent", "red - helix-ending"]))
[docs]class HydrophobicityKDScheme(ResidueRowColorScheme): NAME = "Hydrophobicity (Kyte-Doolittle)" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { "I": (255, 0, 0), "V": (255, 16, 16), "L": (255, 39, 39), "F": (255, 96, 96), "C": (255, 113, 113), "M": (255, 147, 147), "A": (255, 153, 153), "G": (232, 232, 255), "T": (215, 215, 255), "S": (209, 209, 255), "W": (204, 204, 255), "Y": (181, 181, 255), "P": (164, 164, 255), "H": (73, 73, 255), "D": (56, 56, 255), "E": (56, 56, 255), "N": (56, 56, 255), "Q": (56, 56, 255), "K": (34, 34, 255), "R": (0, 0, 255), } # Ordered from most red to most blue CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffff33")
[docs]class HydrophobicityHWScheme(ResidueRowColorScheme): NAME = "Hydrophobicity (Hopp-Woods)" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { "D": (255, 30, 30), "E": (255, 30, 30), "K": (255, 30, 30), "R": (255, 30, 30), "S": (255, 232, 232), "N": (255, 240, 240), "Q": (255, 240, 240), "G": (255, 255, 255), "P": (255, 255, 255), "T": (225, 225, 255), "A": (217, 217, 255), "H": (217, 217, 255), "C": (179, 179, 255), "M": (157, 157, 255), "V": (142, 142, 255), "I": (120, 120, 255), "L": (120, 120, 255), "Y": (82, 82, 255), "F": (67, 67, 255), "W": (0, 0, 255), } # Ordered from most red to most blue CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffff33")
[docs]class SecondaryStructureScheme(ResidueRowColorScheme): NAME = "Secondary Structure" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.secondary_structure) COLOR_BY_KEY = { structure.SS_HELIX: (247, 150, 131), structure.SS_STRAND: (136, 216, 236), structure.SS_TURN: (222, 222, 222), structure.SS_LOOP: (222, 222, 222), structure.SS_NONE: (222, 222, 222), } KEY_FUNC = lambda res: res.secondary_structure KEY_NAMES = { structure.SS_NONE: "None", structure.SS_LOOP: "Loop", structure.SS_HELIX: "Helix", structure.SS_STRAND: "Strand", structure.SS_TURN: "Turn" }
[docs]class BFactorScheme(AbstractColorRampOnlyScheme): NAME = "B-Factor" ANN_TYPE = RowType.Sequence DEFAULT_RAMP = color_ramp.GREEN_RED
[docs] def getColorByRes(self, res): if res is None or res.is_gap: return None if res.b_factor is None: return self._default_color seq = res.sequence min_bf = seq.annotations.min_b_factor max_bf = seq.annotations.max_b_factor res_bf = res.b_factor if math.isclose(min_bf, max_bf): min_bf = 0.0 max_bf = max(100.0, res_bf) scaled_bf = (res_bf - min_bf) / (max_bf - min_bf) * 100 return self._ramp.getRGB(scaled_bf)
[docs] def getBrushByRes(self, res): # See parent class for method documentation. Note that, unlike other # color schemes, this class does not cache brushes and instead generates # a new brush every time this method is called. If this method is used # for anything performance sensitive, this behavior should be changed. rgb = self.getColorByRes(res) return QtGui.QBrush(QtGui.QColor(*rgb))
[docs] def getColorKeyToolTipByRes(self, res): # @overrides: ResidueRowColorScheme if not self._resNeedsToolTip(res): return '' return res.b_factor
[docs] def getColorByKey(self, key): raise RuntimeError( "BFactorScheme only supports fetching colors using a residue")
[docs] def getBrushByKey(self, key): raise RuntimeError( "BFactorScheme only supports fetching colors using a residue")
[docs]class ExposureTendencyScheme(ResidueRowColorScheme): NAME = "Exposure Tendency" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.exposure_tendency) COLOR_BY_KEY = { residue.SOLVENT_EXPOSURE_TENDENCY.Surface: (149, 186, 251), residue.SOLVENT_EXPOSURE_TENDENCY.Ambivalent: (181, 181, 181), residue.SOLVENT_EXPOSURE_TENDENCY.Buried: (251, 222, 146), residue.SOLVENT_EXPOSURE_TENDENCY.NoTendency: (222, 222, 222), } KEY_FUNC = lambda res: res.exposure_tendency DESCRIPTION = ( "Solvent Exposure Tendency:\n" + "\n".join(["blue - surface", "grey - ambivalent", "orange - buried"]))
[docs]class StericGroupScheme(ResidueRowColorScheme): NAME = "Steric Group" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.steric_group) COLOR_BY_KEY = { residue.STERIC_GROUP.Small: (249, 146, 146), residue.STERIC_GROUP.Ambivalent: (250, 147, 251), residue.STERIC_GROUP.Polar: (151, 251, 252), residue.STERIC_GROUP.Aromatic: (149, 186, 251), residue.STERIC_GROUP.NoSteric: (222, 222, 222), } KEY_FUNC = lambda res: res.steric_group DESCRIPTION = ("Steric Group Color Code:\n" + "\n".join([ "red - small, non-interfering", "magenta - ambivalent", "cyan - sticky polar", "blue - aromatic" ]))
[docs]class SideChainChemistryScheme(ResidueRowColorScheme): NAME = "Side Chain Chemistry" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.side_chain_chem) COLOR_BY_KEY = { residue.SIDE_CHAIN_CHEM.AcidicHydrophilic: (251, 147, 194), residue.SIDE_CHAIN_CHEM.BasicHydrophilic: (149, 186, 251), residue.SIDE_CHAIN_CHEM.NeutralHydrophobicAliphatic: (143, 251, 143), residue.SIDE_CHAIN_CHEM.NeutralHydrophobicAromatic: (249, 170, 147), residue.SIDE_CHAIN_CHEM.NeutralHydrophilic: (151, 251, 252), residue.SIDE_CHAIN_CHEM.PrimaryThiol: (251, 251, 146), residue.SIDE_CHAIN_CHEM.IminoAcid: (181, 181, 181), residue.SIDE_CHAIN_CHEM.NoSideChainChem: (222, 222, 222), } KEY_FUNC = lambda res: res.side_chain_chem DESCRIPTION = ("Side Chain Chemistry:\n" + "\n".join([ "red - acidic, hydrophilic", "blue - basic, hydrophilic", "green - neutral, hydrophobic, aliphatic", "orange - neutral, hydrophobic, aromatic", "cyan - neutral, hydrophilic", "yellow - primary thiol", "dark grey - imino acid" ]))
[docs]class ResidueChargeScheme(ResidueRowColorScheme): NAME = "Residue Charge" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { residue.RESIDUE_CHARGE.Positive: (0, 0, 255), residue.RESIDUE_CHARGE.Neutral: GREY, residue.RESIDUE_CHARGE.Negative: (255, 46, 46), } KEY_FUNC = lambda res: res.charge DESCRIPTION = ( "Residue Charge Color Code:\n" + "\n".join(["red - negative", "blue - positive", "grey - neutral"])) CHAIN_DIVIDER_COLOR = QtGui.QColor("#ffff33")
[docs]class WorkspaceScheme(ResidueRowColorScheme): NAME = "Workspace Colors" ANN_TYPE = RowType.Sequence
[docs] def getSchemeSummary(self): return {(0, 0, 0): "Workspace Colors"}
[docs]class ChainNameScheme(ResidueRowColorScheme): NAME = "Chain Name" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = { 'A': (46, 255, 46), 'B': (46, 255, 255), 'C': (255, 107, 255), 'D': (255, 255, 46), 'E': (255, 144, 107), 'F': (230, 230, 230), 'G': (107, 107, 255), 'H': (255, 150, 46), 'I': (107, 255, 107), 'J': (0, 140, 140), 'K': (255, 46, 150), 'L': (255, 218, 107), 'M': (104, 0, 140), 'N': (191, 191, 191), 'O': (0, 102, 204), 'P': (204, 204, 0), 'Q': (181, 255, 107), 'R': (0, 204, 204), 'S': (140, 69, 0), 'T': (255, 181, 107), 'U': (255, 107, 107), 'V': (255, 107, 181), 'W': (107, 255, 255), 'X': (255, 255, 107), 'Y': (0, 204, 102), 'Z': (0, 69, 188) } KEY_FUNC = lambda res: res.sequence.chain DESCRIPTION = "Chain Name"
[docs]class ColorRangeKey(namedtuple("ColorRangeKey", ("min_value", "max_value"))): """ A key for use in `ResiduePropertyScheme`. Represents a range of property values. """ def __str__(self): return f"{self.min_value:.8g} - {self.max_value:.8g}"
[docs]class AbstractResiduePropertyScheme(AbstractColorRampScheme): """ Abstract class for residue property color schemes. Derived classes must define `_ATTRS` as a tuple of private attributes that represent additional init args between seq_prop and ramp. These attributes will be automatically considered when comparing, copying, and jsonifying the class. For example, a derived class with an additional argument `foo` should look like this:: class FooScheme(AbstractResiduePropertyScheme): _ATTRS = ('_foo',) def __init__(self, seq_prop, foo, ramp): super().__init__(seq_prop, ramp) self._foo = foo """ _ATTRS = NotImplemented
[docs] def __init__(self, seq_prop, ramp): # These schemes are always custom super().__init__(ramp, custom=True) self._seq_prop = seq_prop
def __eq__(self, other): if not super().__eq__(other): return False attrs_match = all( getattr(self, name) == getattr(other, name) for name in self._ATTRS) return all((self._seq_prop == other._seq_prop, attrs_match)) def _updateJsonDict(self, json_dict): # See parent class for method documentation super()._updateJsonDict(json_dict) json_dict["seq_prop"] = self._seq_prop.toJson() for name in self._ATTRS: val = getattr(self, name) json_dict[name[1:]] = val # ramp is stored in json_dict in the super-class method @classmethod def _instantiateFromJson(cls, json_dict): # See parent class for method documentation ramp = cls._getRampFromJson(json_dict) seq_prop = properties.SequenceProperty.fromJson(json_dict["seq_prop"]) args = [json_dict[name[1:]] for name in cls._ATTRS] return cls(seq_prop, *args, ramp) def _instantiateCopy(self): # See docstring of base class method args = [getattr(self, name) for name in self._ATTRS] return self.__class__(self._seq_prop, *args, self._ramp)
[docs]class ResiduePropertyScheme(AbstractResiduePropertyScheme): NAME = "Residue Property" ANN_TYPE = RowType.Sequence _ATTRS = ('_min_value', '_max_value', '_num_bins')
[docs] def __init__(self, seq_prop, min_value, max_value, num_bins, ramp): """ :param seq_prop: The property name to color by. Property values will be taken from the alpha-carbon of the residue. The value should be the property's data name, not user name (i.e. "i_m_whatever_property", not "whatever property") and property values must be numerical, not strings. :type seq_prop: protein.properties.SequenceProperty :param min_value: The minimum value to color. :type min_value: float :param max_value: The maximum value to color. :type max_value: float :param num_bins: The number of different colors to use. :type num_bins: int :param ramp: The color ramp to use for the initial colors. This ramp must have a minimum value of 0 and a maximum value of 100. :type ramp: schrodinger.structutils.color.ColorRamp """ super().__init__(seq_prop, ramp) self._min_value = min_value self._max_value = max_value self._num_bins = num_bins self._generateKeysAndColors(min_value, max_value, num_bins, ramp) self.KEY_FUNC = self.keyFunc
def _storeColorByKey(self, json_dict): # See parent class for method documentation # Don't store the keys so that we don't need to make ColorRangeKey # JSON-able and don't need to worry about floating point precision after # conversion to a string color_by_key = [list(self._color_by_key[key]) for key in self._keys] json_dict["color_by_key"] = color_by_key def _restoreColorByKey(self, json_dict): # See parent class for method documentation for key, rgb in zip(self._keys, json_dict["color_by_key"]): self.setColor(key, *rgb) @property def seq_prop(self): return self._seq_prop @property def min_value(self): return self._min_value @property def max_value(self): return self._max_value @property def num_bins(self): return self._num_bins @property def prop_name(self): return self.seq_prop.property_name @property def display_name(self): return self.seq_prop.display_name def _generateKeysAndColors(self, min_value, max_value, num_bins, ramp): """ Generate the `ColorRampKey` objects to use for this instance and the initial colors. See `__init__` for argument documentation. """ edges, step_size = np.linspace(min_value, max_value, num_bins + 1, retstep=True) if step_size == 0.0: # The min and max are the same, so only create one bin num_bins = 1 edges = [min_value, max_value] keys = [ColorRangeKey(*pair) for pair in more_itertools.pairwise(edges)] rgbs = [ramp.getRGB(step) for step in np.linspace(0, 100, num_bins)] # Make sure the rgb values are immutable rgbs = list(map(tuple, rgbs)) brushs = [QtGui.QBrush(QtGui.QColor(*rgb)) for rgb in rgbs] self._step_size = step_size self._keys = keys self._color_by_key = dict(zip(keys, rgbs)) self._brush_by_key = dict(zip(keys, brushs))
[docs] def keyFunc(self, res): """ Convert a residue to the appropriate `ColorRangeKey` based on the property value. Will return `None` if the residue doesn't have a value for the specified property or if the property value is outside the range of values this instance has colors for. :param res: The residue or gap to convert. :type res: residue.AbstractSequenceElement :return: The appropriate key. :rtype: ColorRangeKey or None """ if res.is_gap or res.seqres_only: return None prop_val = res.getProperty(self._seq_prop) if (prop_val is None or prop_val < self._min_value or prop_val > self._max_value): return None if self._step_size > 0: bin_num = (prop_val - self._min_value) // self._step_size bin_num = int(bin_num) # account for floating point imprecision if bin_num < 0: bin_num = 0 elif bin_num >= self._num_bins: bin_num = self._num_bins - 1 else: bin_num = 0 return self._keys[bin_num]
[docs] def getKeys(self): # See parent class for documentation return self._keys
[docs] def prettyKeyName(self, key): # See parent class for documentation return str(key)
[docs] def getSchemeSummary(self): """ @overrides: AbstractColorRampScheme.getSchemeSummary Return a dictionary where key is an RGB tuple and the value is a range of property values that the color represents :return: RGB tuple to property value range map. :rtype: dict{tuple(RGB color):str(range of property value)} """ return {self.getColorByKey(key): str(key) for key in self.getKeys()}
[docs] def getColorKeyToolTipByRes(self, res): """ Get a tool tip describing the color scheme for the specified residue. Displays the actual residue value and wraps the custom color range with a color descriptor. :param res: The residue to fetch the color key tool tip for. :type res: residue.Residue or None :return: A tool tip describing the color key. :rtype: str """ color_range = super().getColorKeyToolTipByRes(res) prop_val = res.getProperty(self._seq_prop) return f'{prop_val} (color: {color_range})'
[docs]class CategoricalResiduePropertyScheme(AbstractResiduePropertyScheme): NAME = "Categorical Residue Property" ANN_TYPE = RowType.Sequence _ATTRS = ('_categories',)
[docs] def __init__(self, seq_prop, categories, ramp): """ :param seq_prop: The property name to color by. Property values will be taken from the alpha-carbon of the residue. The property values must be either numeric (between -1 to 10, both inclusive) or string. :type seq_prop: protein.properties.SequenceProperty :param categories: The categories in which the property value will fall :type categories: list[str] or list[int] :param ramp: The color ramp to use for the initial colors. This ramp must have a minimum value of 0 and a maximum value of 100. :type ramp: schrodinger.structutils.color.ColorRamp """ super().__init__(seq_prop, ramp) self._categories = categories self._generateKeysAndColors(categories, ramp) self.KEY_FUNC = self.keyFunc
def _generateKeysAndColors(self, categories, ramp): """ Generate the color keys and colors to use for this instance. """ num_bins = len(categories) # Think of 100 as a total percentage here, each category will map to a # share out of this overall 100 % rgbs = [ramp.getRGB(step) for step in np.linspace(0, 100, num_bins)] # make sure the rgb values are immutable rgbs = list(map(tuple, rgbs)) brushes = [QtGui.QBrush(QtGui.QColor(*rgb)) for rgb in rgbs] self._color_by_key = dict(zip(categories, rgbs)) self._brush_by_key = dict(zip(categories, brushes))
[docs] def keyFunc(self, res): """ Convert a residue to the appropriate key (property value). Return `None` if the residue doesn't have a value for the specified property or its a gap filling residue. :param res: residue or gap to convert. :type res: protein.residue.Residue :return: key for the residue. :rtype: str or None """ return None if res.is_gap else res.getProperty(self._seq_prop)
[docs] def getSchemeSummary(self): """ @overrides: AbstractColorRampScheme.getSchemeSummary Return a dictionary where key is an RGB tuple and the value is a category :return: RGB tuple to category map :rtype: dict(tuple, str) """ return {rgb: str(label) for label, rgb in self._color_by_key.items()}
# This class doesn't appear in the color drop-down, but it's applied if you # uncheck the "Color Sequences" option
[docs]class NoColorScheme(SingleColorScheme): NAME = "No Color" ANN_TYPE = RowType.Sequence COLOR_BY_KEY = {None: GREY}
[docs] def getSchemeSummary(self): return {}
[docs]class BindingSiteScheme(ResidueRowColorScheme): NAME = "Binding Site" ANN_TYPE = SEQ_ANNO_TYPES.binding_sites COLOR_BY_KEY = { annotation.BINDING_SITE.CloseContact: (255, 0, 0), annotation.BINDING_SITE.FarContact: (255, 136, 0), annotation.BINDING_SITE.NoContact: (0, 0, 0, 0) }
[docs]class AntibodyCDRScheme(ResidueRowColorScheme): NAME = "Antibody CDR" ANN_TYPE = SEQ_ANNO_TYPES.antibody_cdr COLOR_BY_KEY = { annotation.AntibodyCDRLabel.NotCDR: (36, 36, 36), annotation.AntibodyCDRLabel.L1: (255, 0, 0), annotation.AntibodyCDRLabel.L2: (255, 0, 0), annotation.AntibodyCDRLabel.L3: (255, 0, 0), annotation.AntibodyCDRLabel.H1: (255, 0, 0), annotation.AntibodyCDRLabel.H2: (255, 0, 0), annotation.AntibodyCDRLabel.H3: (255, 0, 0) }
[docs]class DomainScheme(ResidueRowColorScheme): NAME = "Domains" ANN_TYPE = SEQ_ANNO_TYPES.domains COLOR_BY_KEY = { annotation.Domains.Domain: (249, 146, 146), annotation.Domains.NoDomain: (64, 64, 64) }
[docs]class HydrophobicityBarScheme(SingleColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.window_hydrophobicity COLOR_BY_KEY = {'hydrophobicity': (240, 160, 240)}
[docs]class IsoelectricBarScheme(SingleColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.window_isoelectric_point COLOR_BY_KEY = {'isoelectric': (160, 240, 192)}
[docs]class BFactorBarScheme(SingleColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.b_factor COLOR_BY_KEY = {'b_factor': (134, 162, 246)}
[docs]class MeanHydrophobicityBarScheme(SingleColorScheme): ANN_TYPE = ALN_ANNO_TYPES.mean_hydrophobicity COLOR_BY_KEY = {'mean_hydrophobicity': (240, 160, 240)}
[docs]class MeanIsoelectricBarScheme(SingleColorScheme): ANN_TYPE = ALN_ANNO_TYPES.mean_isoelectric_point COLOR_BY_KEY = {'mean_isoelectric': (160, 240, 192)}
[docs]class ConsensusFreqBarScheme(SingleColorScheme): ANN_TYPE = ALN_ANNO_TYPES.consensus_freq COLOR_BY_KEY = {'consensus_freq': (160, 240, 192)}
[docs]class DisulfideScheme(SingleColorScheme): ANN_TYPE = [ SEQ_ANNO_TYPES.disulfide_bonds, SEQ_ANNO_TYPES.pred_disulfide_bonds ] COLOR_BY_KEY = {'disulfide_bonds': (255, 255, 255)}
[docs]class ConstraintScheme(SingleColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.pairwise_constraints COLOR_BY_KEY = {'pairwise_constraints': (70, 166, 232)}
[docs]class ProximityConstraintScheme(SingleColorScheme): ANN_TYPE = SEQ_ANNO_TYPES.proximity_constraints COLOR_BY_KEY = {'proximity_constraints': (20, 133, 16)}
[docs]class KinaseFeatureScheme(ResidueRowColorScheme): NAME = "Kinase Features" ANN_TYPE = SEQ_ANNO_TYPES.kinase_features COLOR_BY_KEY = { annotation.KinaseFeatureLabel.GLYCINE_RICH_LOOP: (155, 112, 255), annotation.KinaseFeatureLabel.ALPHA_C: (110, 250, 110), annotation.KinaseFeatureLabel.GATE_KEEPER: (248, 110, 250), annotation.KinaseFeatureLabel.HINGE: (250, 143, 110), annotation.KinaseFeatureLabel.LINKER: (250, 250, 110), annotation.KinaseFeatureLabel.HRD: (110, 250, 252), annotation.KinaseFeatureLabel.CATALYTIC_LOOP: (217, 95, 110), annotation.KinaseFeatureLabel.DFG: (110, 161, 250), annotation.KinaseFeatureLabel.ACTIVATION_LOOP: (250, 210, 110), annotation.KinaseFeatureLabel.NO_FEATURE: (181, 181, 181) }
[docs]class KinaseConservationScheme(ResidueRowColorScheme): NAME = "Kinase Conservation" ANN_TYPE = SEQ_ANNO_TYPES.kinase_conservation KEY_FUNC = lambda res: res.kinase_conservation DEFAULT_COLOR = (0, 0, 0, 0) COLOR_BY_KEY = { annotation.KinaseConservation.VeryLow: (226, 53, 30), #e2351e annotation.KinaseConservation.Low: (235, 190, 53), #ebbe35 annotation.KinaseConservation.Medium: (124, 163, 207), #7ca3cf annotation.KinaseConservation.High: (34, 223, 170), #22dfaa annotation.KinaseConservation.VeryHigh: (11, 196, 28) #0bc41c }
[docs]class PredictedAnnotationMixin: """ Should be mixed in with subclasses of AbstractRowColorScheme. """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._default_brush = None
[docs]class PredictedSecondaryStructureScheme(PredictedAnnotationMixin, SecondaryStructureScheme): ANN_TYPE = SEQ_ANNO_TYPES.pred_secondary_structure KEY_FUNC = lambda res: res.pred_secondary_structure
[docs]class PredictedSolventAccessibilityScheme(PredictedAnnotationMixin, ResidueRowColorScheme): NAME = "Predicted Solvent Accessibility" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.pred_accessibility) COLOR_BY_KEY = { predictors.SolventAccessibility.BURIED: (235, 235, 45), predictors.SolventAccessibility.EXPOSED: (46, 64, 198), } KEY_FUNC = lambda res: res.pred_accessibility
[docs]class PredictedDisorderedRegionsScheme(PredictedAnnotationMixin, ResidueRowColorScheme): NAME = "Predicted Disordered Regions" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.pred_disordered) COLOR_BY_KEY = { predictors.Disordered.HIGHSCORE: (255, 0, 0), predictors.Disordered.MEDIUMSCORE: (255, 127, 0), predictors.Disordered.LOWSCORE: (85, 85, 85), } KEY_FUNC = lambda res: res.pred_disordered
[docs]class PredictedDomainArrangementScheme(PredictedAnnotationMixin, ResidueRowColorScheme): NAME = "Predicted Domain Arrangement" ANN_TYPE = (RowType.Sequence, SEQ_ANNO_TYPES.pred_domain_arr) COLOR_BY_KEY = { predictors.DomainArrangement.Interdomain: (255, 0, 0), predictors.DomainArrangement.DomainForming: (181, 181, 181) } KEY_FUNC = lambda res: res.pred_domain_arr
# The default color scheme for sequence rows (and consensus sequence rows, since # those always use the same scheme) _DEFAULT_SEQ_SCHEME = SideChainChemistryScheme def _collect_schemes(): """ Parse the classes in this file to generate SEQ_SCHEMES_BY_NAME and DEFAULT_ROW_COLOR_SCHEMES. :raises ImportError: If any sequence row color scheme doesn't have a name or if more than one color scheme applies to the same annotation row. (ImportError is used since this function is only run during module import.) """ seq_schemes = {} default_schemes = {} for scheme in globals().values(): if (not isinstance(scheme, type) or not issubclass(scheme, AbstractRowColorScheme) or scheme.ANN_TYPE is None): continue if isinstance(scheme.ANN_TYPE, (list, tuple, set)): ann_types = scheme.ANN_TYPE else: ann_types = (scheme.ANN_TYPE,) if RowType.Sequence in ann_types: if not scheme.NAME: raise ImportError("Unnamed sequence row scheme: %s" % scheme) elif (scheme.NAME in seq_schemes and seq_schemes[scheme.NAME] is not scheme): raise ImportError( "Multiple sequence row schemes found with name %s: %s, %s" % (scheme.NAME, seq_schemes[scheme.NAME].__name__, scheme.__name__)) seq_schemes[scheme.NAME] = scheme for cur_ann in ann_types: if cur_ann == RowType.Sequence: continue if (cur_ann in default_schemes and default_schemes[cur_ann] is not scheme): raise ImportError( "Multiple schemes found for annotation %s: %s, %s" % (cur_ann, default_schemes[cur_ann].__name__, scheme.__name__)) default_schemes[cur_ann] = scheme default_schemes[RowType.Sequence] = _DEFAULT_SEQ_SCHEME default_schemes[ALN_ANNO_TYPES.consensus_seq] = _DEFAULT_SEQ_SCHEME return seq_schemes, default_schemes # SEQ_SCHEMES_BY_NAME is a {scheme name: SchemeClass} dictionary for schemes that # apply to sequence rows. # DEFAULT_ROW_COLORS is a {row type: SchemeClass} dictionary for all row types. SEQ_SCHEMES_BY_NAME, DEFAULT_ROW_COLOR_SCHEMES = _collect_schemes()