Skip to content
Open
20 changes: 17 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import os
# Polars' default (CPU-core-count-sized) native thread pool access-violation
# crashes the whole process on some high-core-count Windows machines when
# invoked from Streamlit's script-runner thread. Must be set before polars
# is imported anywhere (including transitively via openms_insight).
os.environ.setdefault("POLARS_MAX_THREADS", "1")
Comment on lines +1 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app.py ==\n'
nl -ba app.py | sed -n '1,120p'

printf '\n== search POLARS_MAX_THREADS / polars imports ==\n'
rg -n --hidden -S 'POLARS_MAX_THREADS|import polars|from polars|thread_pool_size|thread pool' .

printf '\n== files mentioning openms_insight ==\n'
rg -n --hidden -S 'openms_insight' .

Repository: OpenMS/quantms-web

Length of output: 210


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== app.py ==\n'
sed -n '1,120p' app.py | cat -n

printf '\n== search POLARS_MAX_THREADS / polars imports ==\n'
grep -RIn --exclude-dir=.git --exclude-dir=.venv --exclude-dir=node_modules \
  -e 'POLARS_MAX_THREADS' -e 'import polars' -e 'from polars' -e 'thread_pool_size' -e 'thread pool' .

printf '\n== files mentioning openms_insight ==\n'
grep -RIn --exclude-dir=.git --exclude-dir=.venv --exclude-dir=node_modules \
  -e 'openms_insight' .

Repository: OpenMS/quantms-web

Length of output: 6090


Scope POLARS_MAX_THREADS to Windows.
app.py:1-6 sets this override for every platform, so Linux/macOS deployments lose Polars’ default parallelism too.

Proposed fix
 import os
+import sys
 
-if os.environ.setdefault("POLARS_MAX_THREADS", "1")
+if sys.platform == "win32":
+    os.environ.setdefault("POLARS_MAX_THREADS", "1")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import os
# Polars' default (CPU-core-count-sized) native thread pool access-violation
# crashes the whole process on some high-core-count Windows machines when
# invoked from Streamlit's script-runner thread. Must be set before polars
# is imported anywhere (including transitively via openms_insight).
os.environ.setdefault("POLARS_MAX_THREADS", "1")
import os
import sys
# Polars' default (CPU-core-count-sized) native thread pool access-violation
# crashes the whole process on some high-core-count Windows machines when
# invoked from Streamlit's script-runner thread. Must be set before polars
# is imported anywhere (including transitively via openms_insight).
if sys.platform == "win32":
os.environ.setdefault("POLARS_MAX_THREADS", "1")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.py` around lines 1 - 6, Scope the POLARS_MAX_THREADS override in the
app.py startup initialization to Windows only, applying
os.environ.setdefault("POLARS_MAX_THREADS", "1") when the runtime platform is
Windows. Preserve the ordering so this condition executes before any direct or
transitive Polars import, while leaving non-Windows platforms’ default thread
settings unchanged.


import streamlit as st
from pathlib import Path
import json
Expand All @@ -23,12 +30,19 @@
st.Page(Path("content", "results_rescoring.py"), title="Rescoring", icon="📈"),
st.Page(Path("content", "results_filtered.py"), title="Filtered PSMs", icon="🎯"),
st.Page(Path("content", "results_abundance.py"), title="Abundance", icon="📋"),

],
"Differential Protein Analysis": [
st.Page(Path("content", "filtering.py"), title="Filtering", icon="🧹"),
st.Page(Path("content", "imputation.py"), title="Imputation", icon="🩹"),
st.Page(Path("content", "normalization.py"), title="Normalization", icon="⚖️"),
st.Page(Path("content", "statistical.py"), title="Statistical", icon="🔢"),
st.Page(Path("content", "results_volcano.py"), title="Volcano", icon="🌋"),
st.Page(Path("content", "results_pca.py"), title="PCA", icon="📊"),
st.Page(Path("content", "results_heatmap.py"), title="Heatmap", icon="🔥"),
st.Page(Path("content", "results_library.py"), title="Spectral Library", icon="📚"),
st.Page(Path("content", "results_pathway_analysis.py"), title="Pathway Analysis", icon="📉"),
],
st.Page(Path("content", "results_heatmap_clustered.py"), title="Clustered Heatmap", icon="🧬"),
st.Page(Path("content", "enrichment.py"), title="Pathway Analysis", icon="📉"),
]
}

pg = st.navigation(pages)
Expand Down
141 changes: 141 additions & 0 deletions content/enrichment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Pathway Analysis Page."""

from pathlib import Path
import pandas as pd
import polars as pl
import streamlit as st
from src.common.common import page_setup
from src.common.results_helpers import get_abundance_data, get_id_column
# Import GO Enrichment modules from openms_insight engine
from openms_insight.analysis.enrichment import calculate_go_enrichment

params = page_setup()
st.title("GO Enrichment Analysis")

st.markdown(
"""
Identify overrepresented biological themes (BP, CC, MF) within your differentially expressed protein features using MyGene.info and Fisher's Exact Test.
"""
)

if "workspace" not in st.session_state:
st.warning("Please initialize your workspace first.")
st.stop()

# --- STEP 1: Upstream Statistics Checkpoint ---
if (
"statistics_df" in st.session_state
and st.session_state["statistics_df"] is not None
):
final_statistics_report = st.session_state["statistics_df"]
st.info(
"🔄 **Upstream Pipeline Detected**: Using analyzed matrices from the **Statistical Inference** step."
)
else:
st.warning(
"⚠️ **Missing Prerequisites**: Statistical inference data not detected. Please run hypothesis testing first."
)
st.page_link(
"content/statistical.py", label="Go to Statistical Inference", icon="🔬"
)
st.stop()

# --- STEP 2: Preprocessing Mapping Key Configuration ---
# Identify target identifier columns dynamically
abundance_result = get_abundance_data(st.session_state["workspace"])
id_col = get_id_column(st.session_state["workspace"], abundance_result[0]) if abundance_result else "ProteinName"
if id_col not in final_statistics_report.columns:
st.error(f"❌ Structural Error: Column '{id_col}' is missing from the active matrix context.")
st.stop()

# --- SECTION 1: Parameter Setup & Dynamic Cutoff Labels ---
st.subheader("Configure Enrichment Thresholds")

# Check if target p-value should be adjusted or raw based on previous selections (Fallback safely to 'p-adj')
target_p_col = "p-adj" if "p-adj" in final_statistics_report.columns else "p-value"
p_label = (
"Adjusted P-value (p-adj) Cutoff"
if target_p_col == "p-adj"
else "Raw P-value (p-value) Cutoff"
)

ui_go_col1, ui_go_col2 = st.columns(2)

with ui_go_col1:
p_cutoff = st.number_input(
f"🔬 {p_label}",
min_value=0.0001,
max_value=1.0,
value=0.05,
step=0.01,
format="%.4f",
help="Proteins with significance metrics below this value are mapped to the foreground cohort.",
)

with ui_go_col2:
fc_cutoff = st.number_input(
"📈 Absolute Difference Cutoff (|log2FC|)",
min_value=0.0,
max_value=10.0,
value=1.0,
step=0.1,
format="%.2f",
help="Proteins with absolute log2 fold change greater than or equal to this threshold will be selected.",
)

# --- SECTION 2: Execution and Interactive View Charts ---
st.markdown("<br>", unsafe_allow_html=True)
if st.button("🚀 Run GO Enrichment Analysis", type="primary", key="run_go_analysis"):

with st.spinner("Querying MyGene.info API & executing hyper-geometric calculation loops..."):
# Convert internal pandas DataFrame to openms_insight Polars DataFrame expectation
stats_pl = pl.from_pandas(final_statistics_report)

status, output = calculate_go_enrichment(
final_report=stats_pl,
id_col=id_col,
target_p_col=target_p_col,
p_cutoff=p_cutoff,
fc_cutoff=fc_cutoff,
)

# Route response structures based on analysis output status code
if status == "empty_data":
st.error("❌ No valid statistical rows found containing standard columns to run GO alignment.")

elif status == "insufficient_proteins":
st.warning(
f"⚠️ Not enough significant proteins found to construct target datasets. "
f"(Criteria: {target_p_col} < {p_cutoff:.4f}, |log2FC| ≥ {fc_cutoff:.2f})."
)
st.info(f"💡 Found significant proteins count: **{output}**. Try relaxing your p-value or log2FC filters.")

elif status == "success":
st.success("⭕ GO Enrichment Analysis completed successfully!")

# Display operational matrix scale
st.markdown(
f"📊 **Analysis Profile Scope**: Mapped **{output['fg_count']}** significant foreground profiles out of **{output['bg_count']}** reference background items."
)

# Build multi-tab interface layer for ontology subcategories
tabs = st.tabs([
"🧬 Biological Process (BP)",
"🔬 Cellular Component (CC)",
"🧪 Molecular Function (MF)"
])
categories_data = output["categories"]

for idx, go_type in enumerate(["BP", "CC", "MF"]):
with tabs[idx]:
fig = categories_data[go_type]["fig"]
df_go = categories_data[go_type]["df"]

if fig is not None and df_go is not None:
# Render plotly bar figures generated straight from backend engine
st.plotly_chart(fig, use_container_width=True)

st.subheader(f"📊 {go_type} Results Dataframe")
st.dataframe(df_go, use_container_width=True)
else:
st.info(f"No statistically overrepresented terms identified for Category: **{go_type}**")
173 changes: 173 additions & 0 deletions content/filtering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Filtering Page."""

from pathlib import Path
import pandas as pd
import polars as pl
import streamlit as st
from src.common.common import page_setup
from src.common.results_helpers import get_abundance_data, get_id_column, get_sample_group_map

# Import filtering functions from openms_insight package
from openms_insight.analysis.filter import (
filter_low_abundance,
filter_low_repeatability,
filter_low_variance,
)
Comment on lines +11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== repo files =='
git ls-files 'content/*.py' 'src/common/common.py' 'pyproject.toml' 'requirements*.txt' 'setup.py' 'setup.cfg' 'environment*.yml' | sed 's#^`#-` #'

printf '\n%s\n' '== relevant imports/usages =='
for f in content/filtering.py content/imputation.py content/normalization.py; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "openms_insight\.analysis|page_setup\(|`@st`\.fragment|executor\.run_topp|streamlit_plotly_events|plotly" "$f" || true
  fi
done

printf '\n%s\n' '== package availability probe =='
python3 - <<'PY'
import importlib.util
mods = [
    "openms_insight",
    "openms_insight.analysis",
    "openms_insight.analysis.filter",
    "openms_insight.analysis.imputation",
    "openms_insight.analysis.normalization",
]
for m in mods:
    spec = importlib.util.find_spec(m)
    print(f"{m}: {'FOUND' if spec else 'MISSING'}")
PY

printf '\n%s\n' '== tree around openms_insight if present in repo =='
fd -t f 'openms_insight*' . || true

Repository: OpenMS/quantms-web

Length of output: 1421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== requirements.txt =='
cat -n requirements.txt

printf '\n%s\n' '== openms_insight references in repo =='
rg -n "openms_insight" .

printf '\n%s\n' '== top of affected files =='
for f in content/filtering.py content/imputation.py content/normalization.py; do
  echo "--- $f"
  sed -n '1,40p' "$f"
done

Repository: OpenMS/quantms-web

Length of output: 10510


🌐 Web query:

openms-insight 0.1.13 openms_insight.analysis package

💡 Result:

OpenMS-Insight is a Python package designed to provide interactive visualization components for mass spectrometry data within Streamlit applications, leveraging Vue.js for the frontend [1][2]. While the package structure includes core visualization classes like Table, LinePlot, Heatmap, and VolcanoPlot, there is no public-facing module explicitly named openms_insight.analysis in the 0.1.13 version or subsequent releases [1][3]. The package's primary functionality is focused on data visualization and component-based interactivity, such as cross-component selection linking, memory-efficient preprocessing, and disk-based caching [1][4]. Developers extending the package add new components by implementing specific abstract methods—such as _preprocess, _prepare_vue_data, and _get_cache_config—within a component class, rather than using a dedicated analysis package [5]. If you are looking for mass spectrometry analysis tools, you should refer to the core pyOpenMS library, which contains extensive modules for tasks such as chromatographic analysis, peak picking, and signal processing [6]. OpenMS-Insight is specifically intended for the visualization layer of a Streamlit-based data science workflow, not as a backend for raw data analysis algorithms [2][4].

Citations:


Pin a dependency or rewrite these imports

  • content/filtering.py
  • content/imputation.py
  • content/normalization.py

openms-insight>=0.1.13 does not expose openms_insight.analysis.*, so these pages fail on import unless the dependency is switched to a fork/revision that provides that namespace or the code is moved to the current API.

📍 Affects 3 files
  • content/filtering.py#L11-L15 (this comment)
  • content/imputation.py#L10-L11
  • content/normalization.py#L9-L14
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/filtering.py` around lines 11 - 15, Resolve the incompatible
openms_insight imports by either pinning the dependency to a fork or revision
that provides the analysis namespace, or migrating the imports to the current
supported API. Apply the chosen fix at content/filtering.py lines 11-15,
content/imputation.py lines 10-11, and content/normalization.py lines 9-14 so
all three pages import successfully.


STAT_COLUMNS = ["log2FC", "p-value", "p-adj", "stat"]


def strip_stat_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Keep preprocessing tables intensity-only before statistical analysis."""
return df.drop(columns=[c for c in STAT_COLUMNS if c in df.columns], errors="ignore")

params = page_setup()
st.title("Data Filtering")

st.markdown(
"""
Filter out low-quality proteins from your dataset based on abundance, repeatability, or variance thresholds.
"""
)

if "workspace" not in st.session_state:
st.warning("Please initialize your workspace first.")
st.stop()

result = get_abundance_data(st.session_state["workspace"])
if result is None:
st.info(
"Abundance data not available. Please run the workflow and configure sample groups first."
)
st.page_link(
"content/results_abundance.py", label="Go to Abundance", icon="📋"
)
st.stop()

pivot_df, expr_df, group_map = result
pivot_df = strip_stat_columns(pivot_df)
id_col = get_id_column(st.session_state["workspace"], pivot_df)
sample_group_map = get_sample_group_map(st.session_state["workspace"], pivot_df, group_map)

# 1. Identify actual sample columns dynamically
sample_cols = [
c
for c in pivot_df.columns
if c not in [id_col, "PeptideSequence", "log2FC", "p-value", "p-adj"]
]

# --- SECTION 1: Original Data View ---
st.subheader("Original Abundance Table")
st.markdown(
f"Currently displaying **{pivot_df.shape[0]}** proteins and **{len(sample_cols)}** samples before filtering."
)
st.dataframe(pivot_df, use_container_width=True)

st.markdown("---")

# --- SECTION 2: Filter Configuration ---
st.subheader("Configure Filter Engine")

# Prepare Polars Metadata DataFrame required by openms_insight functions
metadata_rows = [{"sample_id": s, "group": sample_group_map[s]} for s in sample_cols if s in sample_group_map]
metadata_pl = pl.DataFrame(
metadata_rows, schema={"sample_id": pl.String, "group": pl.String}
)

# User selection for filtering strategy
filter_method = st.selectbox(
"Select Filtering Method",
options=["Low Abundance", "Low Repeatability", "Low Variance"],
index=0,
help="Choose the statistical criteria to prune unreliable protein entries.",
)

# Render threshold sliders dynamically based on the selected filter method
if filter_method == "Low Abundance":
st.markdown(
"**Low Abundance Filter**: Keeps rows where at least one group's median is above the selected percentile threshold."
)
threshold = st.slider(
"Threshold Percentile (%)",
min_value=0.0,
max_value=100.0,
value=10.0,
step=5.0,
)

elif filter_method == "Low Repeatability":
st.markdown(
"**Low Repeatability Filter**: Keeps rows where at least one group has a missing value ratio within the allowed maximum."
)
threshold = st.slider(
"Max Missing Ratio",
min_value=0.0,
max_value=100.0,
value=50.0,
step=5.0,
help="Allowed missing value (zero or null) ratio per group.",
)

elif filter_method == "Low Variance":
st.markdown(
"**Low Variance Filter**: Keeps rows where at least one group's variance is above the selected percentile threshold."
)
threshold = st.slider(
"Threshold Percentile (%)",
min_value=0.0,
max_value=100.0,
value=10.0,
step=5.0,
)

# --- SECTION 3: Filter Execution and Collected Results View ---
if st.button("Apply Filter", type="primary"):
# Convert the original Pandas DataFrame into a Polars LazyFrame graph
quant_lazy = pl.from_pandas(pivot_df).lazy()

# Route execution to the chosen openms_insight engine function
if filter_method == "Low Abundance":
filtered_lazy = filter_low_abundance(
quantification_data=quant_lazy,
metadata=metadata_pl,
group_column="group",
threshold_percentile=threshold,
)
elif filter_method == "Low Repeatability":
# Convert percent slider input to ratio expected by the function (e.g., 50.0% -> 0.5)
filtered_lazy = filter_low_repeatability(
quantification_data=quant_lazy,
metadata=metadata_pl,
group_column="group",
max_missing_ratio=threshold / 100.0,
)
elif filter_method == "Low Variance":
filtered_lazy = filter_low_variance(
quantification_data=quant_lazy,
metadata=metadata_pl,
group_column="group",
threshold_percentile=threshold,
)

# Collect the evaluated lazy graph and convert back to Pandas for visualization
filtered_df = strip_stat_columns(filtered_lazy.collect().to_pandas())
st.session_state["filtered_df"] = filtered_df
Comment on lines +153 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Invalidate or version downstream pipeline artifacts.

The session keys have no workspace/input fingerprint, and recomputing an upstream stage leaves downstream results intact. Users can therefore run statistics against outputs from an older filter, workspace, or abundance dataset.

  • content/filtering.py#L153-L154: clear imputation, normalization, and statistics after replacing filtered_df.
  • content/imputation.py#L50-L52: only reuse filtering output when its workspace/input fingerprint matches.
  • content/imputation.py#L136-L139: clear normalization and statistics after replacing imputed_df.
  • content/normalization.py#L54-L62: validate provenance before selecting cached upstream outputs.
  • content/normalization.py#L227-L230: clear statistics after replacing normalized_df.
📍 Affects 3 files
  • content/filtering.py#L153-L154 (this comment)
  • content/imputation.py#L50-L52
  • content/imputation.py#L136-L139
  • content/normalization.py#L54-L62
  • content/normalization.py#L227-L230
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/filtering.py` around lines 153 - 154, Invalidate downstream artifacts
when replacing filtered_df: in content/filtering.py lines 153-154, clear
imputation, normalization, and statistics session results. In
content/imputation.py lines 50-52, reuse filtered output only when its
workspace/input fingerprint matches; at lines 136-139, clear normalization and
statistics after replacing imputed_df. In content/normalization.py lines 54-62,
validate provenance before using cached upstream outputs; at lines 227-230,
clear statistics after replacing normalized_df.


# Layout response metrics and the filtered matrix
st.success(f"Successfully applied **{filter_method}** filter!")

# Display dataset scale compression stats
col1, col2, col3 = st.columns(3)
col1.metric("Original Proteins", pivot_df.shape[0])
col2.metric("Filtered Proteins", filtered_df.shape[0])
col3.metric(
"Removed Proteins", pivot_df.shape[0] - filtered_df.shape[0], delta=None
)

st.subheader("Filtered Abundance Table")
if filtered_df.empty:
st.warning(
"The filtered table is empty. Try relaxing the threshold constraints."
)
else:
st.dataframe(filtered_df, use_container_width=True)
Loading