Skip to content

Commit ac3f37e

Browse files
committed
Merge branch 'develop' of github.com:RBVI/ChimeraX into develop
2 parents d72b8f3 + 5e0f9f2 commit ac3f37e

File tree

8 files changed

+377
-17
lines changed

8 files changed

+377
-17
lines changed

src/bundles/mutation_scores/bundle_info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<ChimeraXClassifier>Command :: mutationscores structure :: Molecular structure :: Associate a structure with a set of mutations scores</ChimeraXClassifier>
4848
<ChimeraXClassifier>Command :: mutationscores close :: Molecular structure :: Close a set of mutations scores</ChimeraXClassifier>
4949
<ChimeraXClassifier>Command :: mutationscores color :: Molecular structure :: Color residues using previous colorings based on mutations scores</ChimeraXClassifier>
50+
<ChimeraXClassifier>Command :: mutationscores heatmap :: Molecular structure :: Show a heat map of mutation scors</ChimeraXClassifier>
5051
</Classifiers>
5152

5253
</BundleInfo>

src/bundles/mutation_scores/src/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def register_command(command_name, logger):
4545
ms_umap.register_command(logger)
4646
from . import ms_color_history
4747
ms_color_history.register_command(logger)
48+
from . import ms_heatmap
49+
ms_heatmap.register_command(logger)
4850

4951
@staticmethod
5052
def run_provider(session, name, mgr):
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# vim: set expandtab shiftwidth=4 softtabstop=4:
2+
3+
# === UCSF ChimeraX Copyright ===
4+
# Copyright 2022 Regents of the University of California. All rights reserved.
5+
# The ChimeraX application is provided pursuant to the ChimeraX license
6+
# agreement, which covers academic and commercial uses. For more details, see
7+
# <https://www.rbvi.ucsf.edu/chimerax/docs/licensing.html>
8+
#
9+
# This particular file is part of the ChimeraX library. You can also
10+
# redistribute and/or modify it under the terms of the GNU Lesser General
11+
# Public License version 2.1 as published by the Free Software Foundation.
12+
# For more details, see
13+
# <https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html>
14+
#
15+
# THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
16+
# EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ADDITIONAL LIABILITY
18+
# LIMITATIONS ARE DESCRIBED IN THE GNU LESSER GENERAL PUBLIC LICENSE
19+
# VERSION 2.1
20+
#
21+
# This notice must be embedded in or attached to all copies, including partial
22+
# copies, of the software or any revisions or derivations thereof.
23+
# === UCSF ChimeraX Copyright ===
24+
25+
# -----------------------------------------------------------------------------
26+
#
27+
from chimerax.core.tools import ToolInstance
28+
class MutationScoresHeatmap(ToolInstance):
29+
30+
help = 'https://www.rbvi.ucsf.edu/chimerax/data/mutation-scores-oct2024/mutation_scores.html'
31+
32+
def __init__(self, session, tool_name = 'Mutation Scores Heatmap'):
33+
34+
ToolInstance.__init__(self, session, tool_name)
35+
36+
from chimerax.ui import MainToolWindow
37+
tw = MainToolWindow(self)
38+
tw.fill_context_menu = self._fill_context_menu
39+
self.tool_window = tw
40+
parent = tw.ui_area
41+
42+
from chimerax.ui.widgets import vertical_layout
43+
layout = vertical_layout(parent, margins = (5,0,0,0))
44+
45+
self._score_view = gv = ScoreView(parent, self._report_cell_info)
46+
from Qt.QtWidgets import QSizePolicy
47+
from Qt.QtCore import Qt
48+
# gv.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
49+
# gv.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
50+
# gv.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
51+
layout.addWidget(gv, stretch=1)
52+
53+
from Qt.QtWidgets import QGraphicsScene
54+
self._scene = gs = QGraphicsScene(gv)
55+
gs.setSceneRect(0, 0, 500, 500)
56+
gv.setScene(gs)
57+
58+
from Qt.QtWidgets import QLabel
59+
self._info_label = info = QLabel(parent)
60+
layout.addWidget(info)
61+
62+
self._set_heatmap_image()
63+
64+
tw.manage(placement=None) # Start floating
65+
66+
# ---------------------------------------------------------------------------
67+
#
68+
def closed(self):
69+
return self.tool_window.tool_instance is None
70+
71+
# ---------------------------------------------------------------------------
72+
#
73+
def _fill_context_menu(self, menu, x, y):
74+
menu.addAction('Select residue', self._select_residue)
75+
menu.addAction('Save image', self._save_image)
76+
77+
# ---------------------------------------------------------------------------
78+
#
79+
def _set_heatmap_image(self):
80+
score_matrix = self._score_matrix()
81+
blue,white,red = (0,0,1,1), (1,1,1,1), (1,0,0,1)
82+
from chimerax.core.colors import Colormap
83+
colormap = Colormap((-2.0,-1.0,1.0,2.0), (blue, white, white, red))
84+
self._score_view._make_image(score_matrix, colormap)
85+
86+
# ---------------------------------------------------------------------------
87+
#
88+
_amino_acids = 'HRKDEFWYNQILCSTVMAGP'
89+
def _score_matrix(self):
90+
from .ms_data import mutation_all_scores
91+
msets = mutation_all_scores(self.session)
92+
self._mutation_set = mset = msets[0] # TODO: Allow choosing mutation set
93+
scores = None
94+
score_names = mset.score_names()
95+
# TODO: Allow choosing score names
96+
score_names = [score_name for score_name in score_names if score_name.endswith('_effect')]
97+
self._score_names = score_names
98+
self._num_scores = score_count = len(score_names)
99+
aa_to_index = {aa:i for i, aa in enumerate(self._amino_acids)}
100+
self._res_aa = res_aa = {}
101+
for snum, score_name in enumerate(score_names):
102+
score_values = mset.score_values(score_name)
103+
if scores is None:
104+
# TODO: This may not give maximum res number
105+
self._num_residues = rmax = max(score_values.residue_numbers())
106+
from numpy import zeros, float32
107+
self._scores = scores = zeros((rmax, 20, score_count), float32)
108+
sscores = scores[:,:,snum]
109+
for res_num, from_aa, to_aa, value in score_values.all_values():
110+
res_aa[res_num] = from_aa
111+
aa_index = aa_to_index[to_aa]
112+
sscores[res_num-1, aa_index] = value
113+
mean, sdev = score_values.synonymous_mean_and_sdev()
114+
sscores -= mean
115+
sscores /= sdev
116+
117+
scores_2d = scores.reshape((rmax, 20*score_count)).transpose()
118+
return scores_2d
119+
120+
# ---------------------------------------------------------------------------
121+
#
122+
def _report_cell_info(self, column_index, row_index):
123+
num_cols = self._num_residues
124+
num_rows = 20 * self._num_scores
125+
if column_index < 0 or row_index < 0 or column_index >= num_cols or row_index >= num_rows or column_index+1 not in self._res_aa:
126+
msg = ''
127+
else:
128+
res_num = column_index + 1
129+
from_aa = self._res_aa[res_num]
130+
score_num = row_index % self._num_scores
131+
score_name = self._score_names[score_num]
132+
aa_index = row_index // self._num_scores
133+
to_aa = self._amino_acids[aa_index]
134+
score_value = self._scores[res_num-1, aa_index, score_num]
135+
msg = f'{from_aa}{res_num}{to_aa} {score_name} {"%.2f"%score_value}'
136+
self._info_label.setText(msg)
137+
138+
# ---------------------------------------------------------------------------
139+
#
140+
def _save_image(self, default_suffix = '_heatmap.png'):
141+
from os.path import dirname, join
142+
filename = self._mutation_set.name + default_suffix
143+
dir = dirname(self._mutation_set.path)
144+
suggested_path = join(dir, filename)
145+
from Qt.QtWidgets import QFileDialog
146+
parent = self.tool_window.ui_area
147+
path, ftype = QFileDialog.getSaveFileName(parent,
148+
'Mutation Heatmap Image',
149+
suggested_path)
150+
if path:
151+
self._score_view.save_image(path)
152+
153+
# ---------------------------------------------------------------------------
154+
#
155+
def _show_help(self):
156+
from chimerax.core.commands import run
157+
run(self.session, 'help %s' % self.help)
158+
159+
# ---------------------------------------------------------------------------
160+
#
161+
from Qt.QtWidgets import QGraphicsView
162+
class ScoreView(QGraphicsView):
163+
def __init__(self, parent, report_cell_info_cb=None):
164+
QGraphicsView.__init__(self, parent)
165+
self._report_cell_info_callback = report_cell_info_cb
166+
self._pixmap_item = None
167+
168+
# Report cell info as mouse hovers over plot.
169+
self.setMouseTracking(True)
170+
171+
# Zoom in
172+
self.scale(2,2)
173+
174+
def sizeHint(self):
175+
from Qt.QtCore import QSize
176+
return QSize(500,500)
177+
178+
def mouseMoveEvent(self, event):
179+
if self._report_cell_info_callback:
180+
x,y = self._scene_position(event)
181+
self._report_cell_info_callback(int(x),int(y))
182+
183+
def _scene_position(self, event):
184+
p = self.mapToScene(event.pos())
185+
return p.x(), p.y()
186+
187+
def _make_image(self, matrix, colormap):
188+
scene = self.scene()
189+
pi = self._pixmap_item
190+
if pi is not None:
191+
scene.removeItem(pi)
192+
193+
rgb = matrix_to_rgb(matrix, colormap)
194+
pixmap = rgb_to_pixmap(rgb)
195+
self._pixmap_item = scene.addPixmap(pixmap)
196+
scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())
197+
198+
def save_image(self, path):
199+
pixmap = self.grab()
200+
pixmap.save(path)
201+
202+
# -----------------------------------------------------------------------------
203+
#
204+
def matrix_to_rgb(matrix, colormap):
205+
rgb_flat = colormap.interpolated_rgba8(matrix.ravel())[:,:3]
206+
n,m = matrix.shape
207+
rgb = rgb_flat.reshape((n,m,3)).copy()
208+
return rgb
209+
210+
# -----------------------------------------------------------------------------
211+
#
212+
def rgb_to_pixmap(rgb):
213+
# Save image to a PNG file
214+
from Qt.QtGui import QImage, QPixmap
215+
h, w = rgb.shape[:2]
216+
im = QImage(rgb.data, w, h, 3*w, QImage.Format_RGB888)
217+
pixmap = QPixmap.fromImage(im)
218+
return pixmap
219+
220+
def mutation_heatmap(session):
221+
hm = MutationScoresHeatmap(session)
222+
return hm
223+
224+
def register_command(logger):
225+
from chimerax.core.commands import CmdDesc, register, StringArg, BoolArg
226+
desc = CmdDesc(
227+
synopsis = 'Show a heatmap of mutation scores.'
228+
)
229+
register('mutationscores heatmap', desc, mutation_heatmap, logger=logger)

src/bundles/phenix_ui/bundle_info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<BundleInfo name="ChimeraX-PhenixUI" version="1.5.1"
1+
<BundleInfo name="ChimeraX-PhenixUI" version="1.5.4"
22
package="chimerax.phenix_ui"
33
minSessionVersion="1" maxSessionVersion="1">
44

src/bundles/phenix_ui/src/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def start_tool(session, tool_name):
5151
from .tool import LaunchLigandFitTool
5252
return LaunchLigandFitTool(session, tool_name)
5353
if tool_name == 'AlphaFold2 Barbed Wire':
54-
from .tool import LaunchAlphaFoldAnalysisTool
55-
return LaunchAlphaFoldAnalysisTool(session, tool_name)
54+
from .tool import LaunchBarbedWireAnalysisTool
55+
return LaunchBarbedWireAnalysisTool(session, tool_name)
5656

5757
bundle_api = _PhenixBundle()

src/bundles/phenix_ui/src/barbed_wire.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,18 @@
3030
from chimerax.atomic import AtomicStructure, Atom, colors, Residue
3131
from time import time
3232

33+
semantic_category_order = [ "Uncategorized", "Unphysical", "Barbed wire", "Pseudostructure",
34+
"Near-predictive", "Unpacked high pLDDT", "Predictive" ] # order from Elaine
35+
3336
def default_uncategorized_color(session):
3437
return "saddle brown"
3538

39+
def filter_out_gapped(s):
40+
pbg = s.pseudobond_group(s.PBG_MISSING_STRUCTURE, create_type=None)
41+
if not pbg:
42+
return True
43+
return not pbg.pseudobonds
44+
3645
class BarbedWireJob(Job):
3746

3847
SESSION_SAVE = False
@@ -94,7 +103,7 @@ def next_check(self):
94103
def running(self):
95104
return self._running
96105

97-
def phenix_barbed_wire(session, structures, *, block=None, phenix_location=None, key=True,
106+
def phenix_barbed_wire(session, structures, *, block=None, phenix_location=None, key=True, show_tool=True,
98107
uncategorized_color=None, verbose=False, option_arg=[], position_arg=[]):
99108

100109
# Find the phenix.barbed_wire_analysis executable
@@ -111,6 +120,11 @@ def phenix_barbed_wire(session, structures, *, block=None, phenix_location=None,
111120
if not structures:
112121
raise UserError("No structures currently open")
113122

123+
structures = [s for s in structures if filter_out_gapped(s)]
124+
if not structures:
125+
raise UserError("Structures specified do not seem to be AlphaFold structures"
126+
" (parts of chains are missing)")
127+
114128
if uncategorized_color is None:
115129
from chimerax.core.colors import Color
116130
uncategorized_color = Color(default_uncategorized_color(session))
@@ -131,10 +145,11 @@ def phenix_barbed_wire(session, structures, *, block=None, phenix_location=None,
131145
# keep a reference to 'tdir' in the callback so that the temporary directory isn't removed before
132146
# the program runs
133147
callback = lambda json, *args, session=session, model=s, show_key=key, ucolor=uncategorized_color, \
134-
d_ref=tdir: _process_results(session, json, model, show_key, ucolor)
148+
show_tool=show_tool, d_ref=tdir: _process_results(
149+
session, json, model, show_key, ucolor, show_tool)
135150
BarbedWireJob(session, exe_path, option_arg, position_arg, temp_dir, verbose, callback, block)
136151

137-
def _process_results(session, json, structure, show_key, uncategorized_color):
152+
def _process_results(session, json, structure, show_key, uncategorized_color, show_tool):
138153
session.logger.status("Barbed wire analysis job finished")
139154
if structure.deleted:
140155
raise UserError("AlphaFold structure was deleted during analysis")
@@ -165,17 +180,21 @@ def _process_results(session, json, structure, show_key, uncategorized_color):
165180
continue
166181
res.barbed_wire_category = cat
167182
try:
168-
res.ribbon_color = cat_colors[cat]
183+
cat_color = cat_colors[cat]
184+
res.ribbon_color = cat_color
185+
res.atoms.colors = cat_color
169186
except KeyError:
170187
raise RuntimeError("Unexpected structure category in barbed wire output: %s" % repr(cat))
171188

172189
if show_key:
173190
from chimerax.core.commands import run, StringArg
174-
semantic_cat_order = [ "Uncategorized", "Unphysical", "Barbed wire", "Pseudostructure",
175-
"Near-predictive", "Unpacked high pLDDT", "Predictive" ] # order from Elaine
176191
run(session, "key %s pos 0.925,0.025 size 0.05,0.2 colorTreatment distinct labelSide left"
177192
" fontSize 16" % ' '.join([StringArg.unparse("%s:%s" % (color_names[cat], cat))
178-
for cat in semantic_cat_order]), log=False)
193+
for cat in semantic_category_order]), log=False)
194+
195+
if session.ui.is_gui and show_tool:
196+
from .tool import BarbedWireResultsViewer
197+
BarbedWireResultsViewer(session, structure, cat_colors)
179198

180199
#NOTE: We don't use a REST server; reference code retained in douse.py
181200

@@ -230,6 +249,7 @@ def register_command(logger):
230249
('block', BoolArg),
231250
('key', BoolArg),
232251
('phenix_location', OpenFolderNameArg),
252+
('show_tool', BoolArg),
233253
('uncategorized_color', ColorArg),
234254
('verbose', BoolArg),
235255
('option_arg', RepeatOf(StringArg)),

src/bundles/phenix_ui/src/emplace_local.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def _run_fit_subprocess(session, exe_path, optional_args, map1_file_name, map2_f
298298
"d_min=%g" % resolution,
299299
"model_file=%s" % StringArg.unparse(model_file_name),
300300
"sphere_center=(%g,%g,%g)" % tuple(search_center.scene_coordinates()),
301-
"--json",
301+
"--json", "--json-filename", "emplace_local_result.json"
302302
] + prefitted_arg + positional_args
303303
tsafe=session.ui.thread_safe
304304
logger = session.logger

0 commit comments

Comments
 (0)