Skip to content

Commit 525c293

Browse files
committed
Adding the heatmap script
1 parent d8344b2 commit 525c293

5 files changed

Lines changed: 341 additions & 4 deletions

File tree

MHCXGraph/app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import logging
22
import os
3-
import sys
43
import webbrowser
54
from itertools import combinations
65
from pathlib import Path
76

87
from MHCXGraph.cli.cli_parser import parse_args
98
from MHCXGraph.core.residue_tracking import ResidueTracker
109
from MHCXGraph.core.tracking import init_tracker
10+
from MHCXGraph.scripts.create_heatmaps import create_heatmap
1111
from MHCXGraph.scripts.renumber_MHCI_imgt import load_mhci_templates, process_structure_file_mhci
1212
from MHCXGraph.scripts.renumber_MHCII_imgt import load_mhcii_templates, process_structure_file_mhcii
1313
from MHCXGraph.utils.logging_utils import setup_logging
@@ -286,7 +286,9 @@ def main():
286286

287287
elif args.command == "renumber":
288288
renumber(args)
289-
289+
290+
elif args.command == "heatmap":
291+
create_heatmap(args)
290292

291293
if __name__ == "__main__":
292294
main()

MHCXGraph/cli/cli_parser.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import argparse
2-
2+
import os
33

44
from .. import __version__
55

@@ -69,6 +69,11 @@ def parse_args():
6969
help="Path to the JSON manifest with complete set of parameters and settings.",
7070
)
7171

72+
parser_heatmap = subparsers.add_parser("heatmap")
73+
parser_heatmap.add_argument("-i", '--input-dir', required=True, nargs='?', default=os.getcwd())
74+
parser_heatmap.add_argument("-o", "--output-dir", required=True, help="Output directory")
75+
parser_heatmap.add_argument("-n", "--name", required=True)
76+
7277
parser_renumber = subparsers.add_parser(
7378
"renumber",
7479
help="Renumber MHC structures using IMGT mapping."
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import argparse
2+
import glob
3+
import json
4+
import os
5+
6+
import matplotlib.pyplot as plt
7+
import numpy as np
8+
import pandas as pd
9+
import seaborn as sns
10+
from pathlib import Path
11+
from scipy.cluster.hierarchy import linkage
12+
from scipy.spatial.distance import squareform
13+
14+
15+
def extract_unique_aminoacids(file_path):
16+
"""Extrai resíduos únicos por componente e consolida o global por proteína."""
17+
try:
18+
with open(file_path, 'r', encoding='utf-8') as f:
19+
data = json.load(f)
20+
21+
if not isinstance(data, dict):
22+
return None
23+
24+
results = {"components": {}}
25+
global_unique_0 = set()
26+
global_unique_1 = set()
27+
individual_rows = []
28+
29+
# Ordenar componentes numericamente para consistência
30+
#sorted_comps = sorted([k for k in data.keys() if str(k) != "0"], key=int)
31+
sorted_comps = sorted(
32+
[k for k in data.keys() if k.isdigit() and k != "0"],
33+
key=int
34+
)
35+
for comp_id in sorted_comps:
36+
comp_data = data[comp_id]
37+
try:
38+
comp_value = comp_data.get("comp")
39+
if comp_value is None or comp_value <= 0:
40+
continue
41+
except (KeyError, TypeError):
42+
continue
43+
44+
comp_unique_0 = set()
45+
comp_unique_1 = set()
46+
frames = comp_data.get("frames", {})
47+
48+
for frame_id, frame_data in frames.items():
49+
if str(frame_id) == "0":
50+
continue
51+
nodes = frame_data.get("nodes", [])
52+
for pair in nodes:
53+
if isinstance(pair, list) and len(pair) == 2:
54+
res_0 = pair[0]
55+
res_1 = pair[1]
56+
57+
# Adiciona aos sets do componente
58+
comp_unique_0.add(res_0)
59+
comp_unique_1.add(res_1)
60+
61+
# Adiciona aos sets globais da proteína
62+
global_unique_0.add(res_0)
63+
global_unique_1.add(res_1)
64+
65+
# Salva para a matriz global
66+
results["components"][str(comp_id)] = {
67+
"prot_0_count": len(comp_unique_0),
68+
"prot_1_count": len(comp_unique_1)
69+
}
70+
71+
# Prepara dados para o CSV individual (Unique por Componente/Proteína)
72+
for aa in sorted(list(comp_unique_0)):
73+
individual_rows.append({"Component": comp_id, "Protein": "0", "Aminoacid": aa})
74+
for aa in sorted(list(comp_unique_1)):
75+
individual_rows.append({"Component": comp_id, "Protein": "1", "Aminoacid": aa})
76+
77+
results["global_prot_0_count"] = len(global_unique_0)
78+
results["global_prot_1_count"] = len(global_unique_1)
79+
results["individual_csv_data"] = individual_rows
80+
81+
return results
82+
83+
except Exception as e:
84+
print(f"Erro ao processar {file_path}: {e}")
85+
return None
86+
87+
def extract_original_graph_info(file_path):
88+
try:
89+
with open(file_path, 'r', encoding='utf-8') as f:
90+
data = json.load(f)
91+
original_graphs = data.get("original_graphs", {})
92+
node_counts = {}
93+
protein_names = {}
94+
for graph_id, graph_data in original_graphs.items():
95+
nodes = graph_data.get("nodes", [])
96+
node_counts[int(graph_id)] = len(nodes)
97+
protein_names[f"Protein_Name_{graph_id}"] = graph_data.get("name", f"Graph_{graph_id}")
98+
return node_counts, protein_names
99+
except:
100+
return {}, {}
101+
102+
def process_directories(directory_path, output_path):
103+
# Ensure the output directory exists
104+
if not os.path.exists(output_path):
105+
os.makedirs(output_path)
106+
107+
search_pattern = os.path.join(directory_path, "**/graph_*.json")
108+
root_pattern = os.path.join(directory_path, "graph_*.json")
109+
110+
# glob.glob returns absolute or relative paths based on the input pattern
111+
json_files = list(set(glob.glob(search_pattern, recursive=True) + glob.glob(root_pattern)))
112+
113+
summary_data = []
114+
all_comp_cols = set()
115+
116+
for path in json_files:
117+
file_name = os.path.basename(path)
118+
data_extracted = extract_unique_aminoacids(path)
119+
orig_counts, prot_names = extract_original_graph_info(path)
120+
121+
if data_extracted is not None:
122+
row = {"File": file_name}
123+
row.update(prot_names)
124+
125+
# Dados originais
126+
sum_orig = sum(orig_counts.values())
127+
for g_id, count in orig_counts.items():
128+
row[f"Original_Graph_{g_id}"] = count
129+
130+
# Unique Globais por Proteína
131+
u0 = data_extracted["global_prot_0_count"]
132+
u1 = data_extracted["global_prot_1_count"]
133+
row["Unique_Prot_0"] = u0
134+
row["Unique_Prot_1"] = u1
135+
136+
# Colunas por Componente
137+
for comp_id, counts in data_extracted["components"].items():
138+
c0_name, c1_name = f"Comp_{comp_id}_Prot_0", f"Comp_{comp_id}_Prot_1"
139+
row[c0_name] = counts["prot_0_count"]
140+
row[c1_name] = counts["prot_1_count"]
141+
all_comp_cols.update([c0_name, c1_name])
142+
143+
# Totais e Ratio
144+
row["total_prot_comp"] = u0 + u1
145+
row["ratio_total_prot_comp"] = round((u0 + u1) / sum_orig, 4) if sum_orig > 0 else 0
146+
147+
summary_data.append(row)
148+
149+
# --- GERAÇÃO DO ARQUIVO INDIVIDUAL ---
150+
if data_extracted["individual_csv_data"]:
151+
df_ind = pd.DataFrame(data_extracted["individual_csv_data"])
152+
csv_name = f"unique_nodes_{file_name.replace('.json', '.csv')}"
153+
# Save to output_path
154+
individual_csv_path = os.path.join(output_path, csv_name)
155+
df_ind.to_csv(individual_csv_path, index=False)
156+
print(f"Individual file generated: {individual_csv_path}")
157+
158+
if summary_data:
159+
df = pd.DataFrame(summary_data)
160+
prot_names = sorted([c for c in df.columns if "Protein_Name_" in c])
161+
orig_graphs = sorted([c for c in df.columns if "Original_Graph_" in c])
162+
uniques = ["Unique_Prot_0", "Unique_Prot_1"]
163+
164+
# Safe sorting for component columns
165+
comps = sorted(list(all_comp_cols), key=lambda x: (int(x.split('_')[1]), x.split('_')[3]))
166+
167+
final_order = ["File"] + prot_names + orig_graphs + uniques + comps + ["total_prot_comp", "ratio_total_prot_comp"]
168+
df = df.reindex(columns=final_order).fillna(0)
169+
170+
for col in orig_graphs + uniques + comps + ["total_prot_comp"]:
171+
df[col] = df[col].astype(int)
172+
173+
# Save global matrix to output_path
174+
global_matrix_path = os.path.join(output_path, "component_count_matrix.csv")
175+
df.to_csv(global_matrix_path, index=False)
176+
print(f"\nGlobal Matrix saved: {global_matrix_path}")
177+
return df
178+
179+
return None
180+
181+
def create_distance_matrix(csv_path):
182+
"""
183+
Create symmetric distance matrix from protein pairs CSV.
184+
185+
Args:
186+
csv_path: Path to CSV with Protein_Name_0, Protein_Name_1, Ratio_TotalComp_MinOriginal
187+
188+
Returns:
189+
DataFrame with protein distance matrix
190+
"""
191+
df = pd.read_csv(csv_path)
192+
193+
# Get all unique proteins
194+
proteins = sorted(set(df['Protein_Name_0']).union(set(df['Protein_Name_1'])))
195+
196+
# Create empty matrix
197+
n = len(proteins)
198+
matrix = pd.DataFrame(np.zeros((n, n)), index=proteins, columns=proteins)
199+
200+
matrix_values = matrix.values.copy()
201+
202+
# Set diagonal to 0
203+
np.fill_diagonal(matrix_values, 0.0)
204+
205+
# Fill matrix with distances from CSV
206+
for _, row in df.iterrows():
207+
p1, p2, dist = row['Protein_Name_0'], row['Protein_Name_1'],row['ratio_total_prot_comp']
208+
matrix.loc[p1, p2] = 1- dist
209+
matrix.loc[p2, p1] = 1- dist # Symmetric
210+
211+
return matrix
212+
213+
# ==============================
214+
# Construir matriz de componentes
215+
# ==============================
216+
def build_component_matrix(comp_df):
217+
proteins = sorted(set(
218+
comp_df['Protein_Name_0'].tolist() +
219+
comp_df['Protein_Name_1'].tolist()
220+
))
221+
222+
n = len(proteins)
223+
comp_matrix = np.zeros((n, n))
224+
225+
protein_to_idx = {protein: i for i, protein in enumerate(proteins)}
226+
227+
# Dicionário para Original_Graph
228+
protein_to_original = {}
229+
230+
for _, row in comp_df.iterrows():
231+
p0 = row['Protein_Name_0']
232+
p1 = row['Protein_Name_1']
233+
value = row['total_prot_comp']
234+
235+
i = protein_to_idx[p0]
236+
j = protein_to_idx[p1]
237+
238+
comp_matrix[i, j] = value
239+
comp_matrix[j, i] = value
240+
241+
# Guardar Original_Graph
242+
protein_to_original[p0] = row['Original_Graph_0']
243+
protein_to_original[p1] = row['Original_Graph_1']
244+
245+
# Preencher diagonal com Original_Graph
246+
for protein in proteins:
247+
idx = protein_to_idx[protein]
248+
comp_matrix[idx, idx] = protein_to_original.get(protein, 0)*2
249+
250+
comp_df_full = pd.DataFrame(
251+
comp_matrix,
252+
index=proteins,
253+
columns=proteins
254+
)
255+
256+
return comp_df_full
257+
258+
259+
def create_heatmap(args):
260+
args.input_dir, args.output_dir = Path(args.input_dir), Path(args.output_dir)
261+
print(f"Processing directories inside {args.input_dir}")
262+
process_directories(args.input_dir, args.output_dir)
263+
print(f"Creating distance matrices given: {args.output_dir / 'distance_matrix.csv'}")
264+
matrix = create_distance_matrix(args.output_dir / "component_count_matrix.csv")
265+
matrix.to_csv(args.output_dir / "distance_matrix.csv")
266+
dist_df = matrix
267+
# dist_df = pd.read_csv(args.output_dir / "distance_matrix.csv", index_col=0)
268+
dist_matrix = dist_df.values
269+
labels = dist_df.index.tolist()
270+
271+
condensed_dist = squareform(dist_matrix)
272+
linkage_matrix = linkage(condensed_dist, method="average")
273+
274+
# ==============================
275+
# 2. MATRIZ Total_Comp_Sum
276+
# ==============================
277+
comp_df = pd.read_csv(args.output_dir / "component_count_matrix.csv")
278+
comp_df_full = build_component_matrix(comp_df)
279+
280+
# ==============================
281+
# 3. CLUSTERMAP
282+
# ==============================
283+
g = sns.clustermap(
284+
dist_df,
285+
cmap='viridis',
286+
row_linkage=linkage_matrix,
287+
col_linkage=linkage_matrix,
288+
figsize=(10, 8),
289+
dendrogram_ratio=0.15,
290+
cbar_kws={'label': 'Distance'}
291+
)
292+
293+
row_order = g.dendrogram_row.reordered_ind
294+
ordered_labels = [labels[i] for i in row_order]
295+
296+
# Reordenar matriz de componentes
297+
comp_ordered = comp_df_full.reindex(
298+
index=ordered_labels,
299+
columns=ordered_labels
300+
)
301+
302+
g.ax_heatmap.set_xticklabels(ordered_labels, rotation=90)
303+
g.ax_heatmap.set_yticklabels(ordered_labels, rotation=0)
304+
305+
# ==============================
306+
# 6. Inserir valores nas células
307+
# ==============================
308+
ax = g.ax_heatmap
309+
310+
for i in range(len(ordered_labels)):
311+
for j in range(len(ordered_labels)):
312+
value = comp_ordered.iloc[i, j]
313+
ax.text(
314+
j + 0.5,
315+
i + 0.5,
316+
f"{int(value)}",
317+
ha='center',
318+
va='center',
319+
fontsize=7,
320+
color='black'
321+
)
322+
323+
plt.tight_layout()
324+
plt.savefig(args.output_dir / args.name, dpi=300, bbox_inches='tight')
325+
plt.show()

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ pip install -e MHCXGraph
3232
## License
3333

3434
The software is licensed under the terms of the GNU Affero General Public License 3 (AGPL3) and is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
35+
36+
37+
## TODO
38+
39+
Adicionar um botão para travar a rotação de todas as proteínas

examples/minimal/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"settings": {
33
"run_name": "minimal",
4-
"run_mode": "all",
4+
"run_mode": "pair",
55
"max_chunks": 5,
66
"output_path": "results/minimal",
77
"debug_logs": false,

0 commit comments

Comments
 (0)