"""
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 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]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]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
@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 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]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()