Skip to content

Commit 83646ec

Browse files
authored
feat: integrate LFQ and TMT workflows (#27)
* feat: LFQ/TMT Integration * fix: resolve merge conflicts and add tool_instance-name support to run_topp
1 parent 46bf208 commit 83646ec

11 files changed

Lines changed: 2306 additions & 915 deletions

app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
st.Page(Path("content", "results_pca.py"), title="PCA", icon="📊"),
2828
st.Page(Path("content", "results_heatmap.py"), title="Heatmap", icon="🔥"),
2929
st.Page(Path("content", "results_library.py"), title="Spectral Library", icon="📚"),
30-
st.Page(Path("content", "results_proteomicslfq.py"), title="Proteomics LFQ", icon="🧪"),
30+
st.Page(Path("content", "results_pathway_analysis.py"), title="Pathway Analysis", icon="📉"),
3131
],
3232
}
3333

content/results_abundance.py

Lines changed: 90 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pathlib import Path
55
from src.common.common import page_setup
66
from src.common.results_helpers import get_workflow_dir, get_abundance_data
7+
from src.workflow.ParameterManager import ParameterManager
78

89
params = page_setup()
910
st.title("Abundance Quantification")
@@ -21,6 +22,10 @@
2122

2223
workflow_dir = get_workflow_dir(st.session_state["workspace"])
2324
quant_dir = workflow_dir / "results" / "quant_results"
25+
parameter_manager = ParameterManager(workflow_dir, "TOPP Workflow")
26+
27+
workflow_params = parameter_manager.get_parameters_from_json()
28+
analysis_mode = workflow_params.get("analysis-mode", "LFQ")
2429

2530
if not quant_dir.exists():
2631
st.info("No quantification results available yet. Please run the workflow first.")
@@ -35,6 +40,60 @@
3540

3641
csv_file = csv_files[0]
3742

43+
def render_protein_table(pivot_df, group_map, is_lfq=True):
44+
"""Common function to render the protein-level abundance table"""
45+
st.markdown("### Protein-Level Abundance Table")
46+
st.info(
47+
"This protein-level table is generated by grouping all PSMs that map to the "
48+
"same protein and aggregating their intensities across samples.\n\n"
49+
"Additionally, log2 fold change and p-values are calculated between sample groups."
50+
)
51+
52+
# Display group comparison info
53+
groups = sorted(set(group_map.values()))
54+
if len(groups) >= 2:
55+
group1, group2 = sorted(groups)[:2]
56+
st.info(f"Statistical comparison: **{group2} vs {group1}**")
57+
58+
if is_lfq:
59+
# Handle LFQ mode columns (Raw Intensity)
60+
id_col = "ProteinName"
61+
exclude_cols = [id_col, "log2FC", "p-value", "PeptideSequence"]
62+
sample_cols = [c for c in pivot_df.columns if c not in exclude_cols]
63+
64+
pivot_df["Intensity"] = pivot_df[sample_cols].apply(list, axis=1)
65+
display_cols = [id_col, "log2FC", "p-value", "Intensity"] + sample_cols + ["PeptideSequence"]
66+
help_text = "Raw sample intensities"
67+
y_min = None
68+
else:
69+
# Handle non-LFQ mode columns (Log2-transformed Intensity)
70+
id_col = "protein"
71+
exclude_cols = [id_col, "log2FC", "p-value", "p-adj", "n_proteins", "n_peptides", "protein_score"]
72+
sample_cols = [c for c in pivot_df.columns if c not in exclude_cols and "ratio" not in c.lower()]
73+
74+
pivot_df["Intensity"] = pivot_df[sample_cols].apply(
75+
lambda row: [np.log2(v + 1) for v in row], axis=1
76+
)
77+
display_cols = [id_col, "log2FC", "p-value", "Intensity"] + sample_cols
78+
help_text = "Sample intensities (log2 scale)"
79+
y_min = 0
80+
81+
# Filter to available columns, then sort and display
82+
available_cols = [c for c in display_cols if c in pivot_df.columns]
83+
84+
st.dataframe(
85+
pivot_df[available_cols].sort_values("p-value"),
86+
column_config={
87+
"Intensity": st.column_config.BarChartColumn(
88+
"Intensity",
89+
help=help_text,
90+
width="small",
91+
y_min=y_min,
92+
),
93+
},
94+
use_container_width=True,
95+
)
96+
3897
protein_tab, psm_tab = st.tabs(["Protein Table", "PSM-level Quantification Table"])
3998

4099
try:
@@ -44,58 +103,44 @@
44103
st.info("No data found in this file.")
45104
st.stop()
46105

47-
with protein_tab:
48-
st.markdown("### Protein-Level Abundance Table")
106+
result = get_abundance_data(st.session_state["workspace"])
49107

50-
st.info(
51-
"This protein-level table is generated by grouping all PSMs that map to the "
52-
"same protein and aggregating their intensities across samples.\n\n"
53-
"Additionally, log2 fold change and p-values are calculated between sample groups."
54-
)
108+
if analysis_mode == "LFQ":
109+
protein_tab, psm_tab = st.tabs(["Protein Table", "PSM-level Quantification Table"])
55110

56-
result = get_abundance_data(st.session_state["workspace"])
57-
if result is None:
58-
st.warning("Could not compute abundance data. Please ensure sample groups are defined in the Configure page.")
59-
st.page_link("content/workflow_configure.py", label="Go to Configure", icon="⚙️")
60-
st.stop()
111+
with protein_tab:
112+
if result is None:
113+
st.warning("Could not compute abundance data. Please ensure sample groups are defined in the Configure page.")
114+
# st.page_link("content/workflow_configure.py", label="Go to Configure", icon="⚙️")
115+
st.stop()
116+
117+
pivot_df, expr_df, group_map = result
118+
render_protein_table(pivot_df, group_map, is_lfq=True)
61119

62-
pivot_df, expr_df, group_map = result
120+
with psm_tab:
121+
st.markdown("### PSM-level Quantification Table")
122+
st.info(
123+
"This table shows the PSM-level quantification data, including protein IDs, "
124+
"peptide sequences, charge states, and intensities across samples. "
125+
"Each row represents one peptide-spectrum match detected from the MS/MS analysis."
126+
)
127+
st.dataframe(df, use_container_width=True)
63128

64-
# Display group comparison info
65-
groups = sorted(set(group_map.values()))
66-
if len(groups) >= 2:
67-
group1, group2 = sorted(groups)[:2]
68-
st.info(f"Statistical comparison: **{group2} vs {group1}**")
129+
else:
130+
pre_processing_tab, protein_tab = st.tabs(["Pre-processing", "Protein Table"])
69131

70-
# Get sample columns (between stats and PeptideSequence)
71-
sample_cols = [c for c in pivot_df.columns if c not in ["ProteinName", "log2FC", "p-value", "PeptideSequence"]]
72-
73-
pivot_df["Intensity"] = pivot_df[sample_cols].apply(list, axis=1)
132+
if result is None:
133+
st.info("💡 Please complete the configuration in the 'Configure' page to see results.")
134+
st.stop()
135+
136+
pivot_df, expr_df, group_map = result
74137

75-
# Reorder columns: place Intensity after p-value
76-
display_cols = ["ProteinName", "log2FC", "p-value", "Intensity"] + sample_cols + ["PeptideSequence"]
77-
display_df = pivot_df[display_cols]
78-
79-
st.dataframe(
80-
display_df.sort_values("p-value"),
81-
column_config={
82-
"Intensity": st.column_config.BarChartColumn(
83-
"Intensity",
84-
help="Raw sample intensities",
85-
width="small",
86-
),
87-
},
88-
use_container_width=True,
89-
)
138+
with pre_processing_tab:
139+
st.write("### Final Results (Group row removed, Stats added)")
140+
st.dataframe(pivot_df.head(10))
90141

91-
with psm_tab:
92-
st.markdown("### PSM-level Quantification Table")
93-
st.info(
94-
"This table shows the PSM-level quantification data, including protein IDs, "
95-
"peptide sequences, charge states, and intensities across samples. "
96-
"Each row represents one peptide-spectrum match detected from the MS/MS analysis."
97-
)
98-
st.dataframe(df, use_container_width=True)
142+
with protein_tab:
143+
render_protein_table(pivot_df, group_map, is_lfq=False)
99144

100145
except Exception as e:
101146
st.error(f"Failed to load {csv_file.name}: {e}")

content/results_heatmap.py

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from scipy.cluster.hierarchy import linkage, leaves_list
66
from scipy.spatial.distance import pdist
77
from src.common.common import page_setup
8-
from src.common.results_helpers import get_abundance_data
8+
from src.common.results_helpers import get_abundance_data, get_workflow_dir
9+
from src.workflow.ParameterManager import ParameterManager
910

1011
params = page_setup()
1112
st.title("Heatmap")
@@ -29,48 +30,103 @@
2930

3031
pivot_df, expr_df, group_map = result
3132

32-
top_n = st.slider("Number of proteins", 20, 200, 50, key="heatmap_top_n")
33+
workflow_dir = get_workflow_dir(st.session_state["workspace"])
34+
parameter_manager = ParameterManager(workflow_dir, "TOPP Workflow")
3335

34-
var_series = expr_df.var(axis=1)
35-
top_proteins = var_series.sort_values(ascending=False).head(top_n).index
36-
heatmap_df = expr_df.loc[top_proteins]
37-
heatmap_z = heatmap_df.sub(heatmap_df.mean(axis=1), axis=0).div(heatmap_df.std(axis=1), axis=0)
38-
heatmap_z = heatmap_z.replace([np.inf, -np.inf], np.nan).dropna()
36+
workflow_params = parameter_manager.get_parameters_from_json()
37+
analysis_mode = workflow_params.get("analysis-mode", "LFQ")
3938

40-
if not heatmap_z.empty:
41-
row_linkage = linkage(pdist(heatmap_z.values), method="average")
42-
row_order = leaves_list(row_linkage)
39+
st.write("Workflow Analysis Mode:", analysis_mode)
4340

44-
col_linkage = linkage(pdist(heatmap_z.T.values), method="average")
45-
col_order = leaves_list(col_linkage)
41+
if analysis_mode == "LFQ":
42+
top_n = st.slider("Number of proteins", 20, 200, 50, key="heatmap_top_n")
4643

47-
heatmap_clustered = heatmap_z.iloc[row_order, col_order]
44+
var_series = expr_df.var(axis=1)
45+
top_proteins = var_series.sort_values(ascending=False).head(top_n).index
46+
heatmap_df = expr_df.loc[top_proteins]
47+
heatmap_z = heatmap_df.sub(heatmap_df.mean(axis=1), axis=0).div(heatmap_df.std(axis=1), axis=0)
48+
heatmap_z = heatmap_z.replace([np.inf, -np.inf], np.nan).dropna()
4849

49-
fig_heatmap = px.imshow(
50-
heatmap_clustered,
51-
labels=dict(x="Sample", y="Protein", color="Z-score"),
52-
aspect="auto",
53-
color_continuous_scale=[[0.0, "#3b6fb6"], [0.5, "white"], [1.0, "#b40426"]],
54-
zmin=-3, zmax=3
55-
)
50+
if not heatmap_z.empty:
51+
row_linkage = linkage(pdist(heatmap_z.values), method="average")
52+
row_order = leaves_list(row_linkage)
5653

57-
fig_heatmap.update_layout(
58-
height=700,
59-
xaxis={'side': 'bottom'},
60-
yaxis={'side': 'left'}
61-
)
54+
col_linkage = linkage(pdist(heatmap_z.T.values), method="average")
55+
col_order = leaves_list(col_linkage)
6256

63-
fig_heatmap.update_xaxes(tickfont=dict(size=10))
64-
fig_heatmap.update_yaxes(tickfont=dict(size=8))
57+
heatmap_clustered = heatmap_z.iloc[row_order, col_order]
6558

66-
st.plotly_chart(fig_heatmap, use_container_width=True)
59+
fig_heatmap = px.imshow(
60+
heatmap_clustered,
61+
labels=dict(x="Sample", y="Protein", color="Z-score"),
62+
aspect="auto",
63+
color_continuous_scale=[[0.0, "#3b6fb6"], [0.5, "white"], [1.0, "#b40426"]],
64+
zmin=-3, zmax=3
65+
)
66+
67+
fig_heatmap.update_layout(
68+
height=700,
69+
xaxis={'side': 'bottom'},
70+
yaxis={'side': 'left'}
71+
)
72+
73+
fig_heatmap.update_xaxes(tickfont=dict(size=10))
74+
fig_heatmap.update_yaxes(tickfont=dict(size=8))
75+
76+
st.plotly_chart(fig_heatmap, use_container_width=True)
77+
else:
78+
st.warning("Insufficient data to generate the heatmap.")
79+
80+
st.markdown("---")
81+
st.markdown("**Other visualizations:**")
82+
col1, col2 = st.columns(2)
83+
with col1:
84+
st.page_link("content/results_volcano.py", label="Volcano Plot", icon="🌋")
85+
with col2:
86+
st.page_link("content/results_pca.py", label="PCA", icon="📊")
6787
else:
68-
st.warning("Insufficient data to generate the heatmap.")
69-
70-
st.markdown("---")
71-
st.markdown("**Other visualizations:**")
72-
col1, col2 = st.columns(2)
73-
with col1:
74-
st.page_link("content/results_volcano.py", label="Volcano Plot", icon="🌋")
75-
with col2:
76-
st.page_link("content/results_pca.py", label="PCA", icon="📊")
88+
top_n = st.slider("Number of proteins", 20, 200, 50, key="heatmap_top_n")
89+
90+
var_series = expr_df.var(axis=1)
91+
top_proteins = var_series.sort_values(ascending=False).head(top_n).index
92+
heatmap_df = expr_df.loc[top_proteins]
93+
heatmap_z = heatmap_df.sub(heatmap_df.mean(axis=1), axis=0).div(heatmap_df.std(axis=1), axis=0)
94+
heatmap_z = heatmap_z.replace([np.inf, -np.inf], np.nan).dropna()
95+
96+
if not heatmap_z.empty:
97+
row_linkage = linkage(pdist(heatmap_z.values), method="average")
98+
row_order = leaves_list(row_linkage)
99+
100+
col_linkage = linkage(pdist(heatmap_z.T.values), method="average")
101+
col_order = leaves_list(col_linkage)
102+
103+
heatmap_clustered = heatmap_z.iloc[row_order, col_order]
104+
105+
fig_heatmap = px.imshow(
106+
heatmap_clustered,
107+
labels=dict(x="Sample", y="Protein", color="Z-score"),
108+
aspect="auto",
109+
color_continuous_scale=[[0.0, "#3b6fb6"], [0.5, "white"], [1.0, "#b40426"]],
110+
zmin=-3, zmax=3
111+
)
112+
113+
fig_heatmap.update_layout(
114+
height=700,
115+
xaxis={'side': 'bottom'},
116+
yaxis={'side': 'left'}
117+
)
118+
119+
fig_heatmap.update_xaxes(tickfont=dict(size=10))
120+
fig_heatmap.update_yaxes(tickfont=dict(size=8))
121+
122+
st.plotly_chart(fig_heatmap, width="stretch")
123+
else:
124+
st.warning("Insufficient data to generate the heatmap.")
125+
126+
st.markdown("---")
127+
st.markdown("**Other visualizations:**")
128+
col1, col2 = st.columns(2)
129+
with col1:
130+
st.page_link("content/results_volcano.py", label="Volcano Plot", icon="🌋")
131+
with col2:
132+
st.page_link("content/results_pca.py", label="PCA", icon="📊")

0 commit comments

Comments
 (0)