Skip to content

Commit 72729f1

Browse files
authored
Merge pull request #533 from bigbio/dev
Adding Quant plots to FragPipe based on ion.tsv
2 parents 121572e + 0ba4589 commit 72729f1

File tree

8 files changed

+2681
-19
lines changed

8 files changed

+2681
-19
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ jobs:
274274
- name: Test FragPipe file
275275
run: |
276276
wget -nv -P ./fragpipe https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/psm.tsv
277+
wget -nv -P ./fragpipe https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/ion.tsv
277278
multiqc --fragpipe-plugin ./fragpipe -o ./results_fragpipe
278279
- uses: actions/upload-artifact@v4
279280
if: always()

docs/config.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,17 @@
167167
{
168168
"accession": "PXD066146",
169169
"urls": [
170-
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/psm.tsv"
170+
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/psm.tsv",
171+
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/ion.tsv"
171172
],
172173
"path": "docs/PXD066146",
173174
"file_type": ["fragpipe", ""]
174175
},
175176
{
176177
"accession": "PXD066146_disable_hoverinfo",
177178
"urls": [
178-
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/psm.tsv"
179+
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/psm.tsv",
180+
"https://ftp.pride.ebi.ac.uk/pride/data/archive/2025/08/PXD066146/ion.tsv"
179181
],
180182
"path": "docs/PXD066146_disable_hoverinfo",
181183
"file_type": ["fragpipe", "disable_hoverinfo"]

pmultiqc/modules/fragpipe/fragpipe.py

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import re
44

55
from pmultiqc.modules.base import BasePMultiqcModule
6-
from pmultiqc.modules.fragpipe import fragpipe_io
6+
from pmultiqc.modules.fragpipe.fragpipe_io import (
7+
get_fragpipe_files,
8+
psm_reader,
9+
ion_reader,
10+
get_ion_intensity_data,
11+
)
712
from pmultiqc.modules.common.stats import (
813
cal_delta_mass_dict,
914
nanmedian,
@@ -37,6 +42,7 @@
3742
)
3843
from pmultiqc.modules.common.plots.general import (
3944
plot_html_check,
45+
plot_data_check,
4046
stat_pep_intensity,
4147
search_engine_score_bins,
4248
draw_search_engine_scores,
@@ -47,7 +53,7 @@
4753
from pmultiqc.modules.common.histogram import Histogram
4854

4955
from multiqc import config
50-
from multiqc.plots import bargraph
56+
from multiqc.plots import bargraph, box
5157

5258
from pmultiqc.modules.common.logging import get_logger
5359

@@ -76,14 +82,22 @@ def __init__(self, find_log_files_func, sub_sections, heatmap_colors):
7682
self.mods = []
7783
self.hm_data = []
7884

85+
# Ion-level intensity data from ion.tsv
86+
self.ion_intensity_data = None
87+
self.ion_sample_cols = []
88+
7989

8090
def get_data(self):
8191

8292
log.info("Starting data recognition and processing...")
8393

84-
self.fragpipe_files = fragpipe_io.get_fragpipe_files(self.find_log_files)
94+
self.fragpipe_files = get_fragpipe_files(self.find_log_files)
95+
96+
if self.fragpipe_files is None:
97+
log.warning("No FragPipe files found.")
98+
return False
8599

86-
if self.fragpipe_files["psm"]:
100+
if self.fragpipe_files.get("psm"):
87101
(
88102
self.delta_masses,
89103
self.charge_states,
@@ -102,6 +116,12 @@ def get_data(self):
102116
log.warning("Required input not found: psm.tsv")
103117
return False
104118

119+
# Parse ion.tsv for ion-level intensity data
120+
if self.fragpipe_files.get("ion"):
121+
self.ion_intensity_data, self.ion_sample_cols = self.parse_ion(
122+
fragpipe_files=self.fragpipe_files
123+
)
124+
105125
return True
106126

107127
def draw_plots(self):
@@ -211,6 +231,13 @@ def draw_plots(self):
211231
retentions=self.retentions
212232
)
213233

234+
# Ion-level intensity plots from ion.tsv
235+
if self.ion_intensity_data:
236+
self.draw_ion_intensity_distribution(
237+
sub_section=self.sub_sections["quantification"],
238+
intensity_data=self.ion_intensity_data
239+
)
240+
214241
section_group_dict = {
215242
"summary_sub_section": self.sub_sections["summary"],
216243
"identification_sub_section": self.sub_sections["identification"],
@@ -243,7 +270,7 @@ def parse_psm(fragpipe_files):
243270

244271
for psm in fragpipe_files.get("psm", []):
245272

246-
psm_df = fragpipe_io.psm_reader(psm)
273+
psm_df = psm_reader(psm)
247274

248275
if psm_df is None or psm_df.empty:
249276
log.warning(f"Skipping unreadable/empty FragPipe PSM file: {psm}")
@@ -703,6 +730,130 @@ def draw_ids_over_rt(sub_section, retentions: list):
703730
report_type="fragpipe"
704731
)
705732

733+
@staticmethod
734+
def parse_ion(fragpipe_files):
735+
"""
736+
Parse ion.tsv files for ion-level intensity data.
737+
738+
Parameters
739+
----------
740+
fragpipe_files : dict
741+
Dictionary of FragPipe file paths.
742+
743+
Returns
744+
-------
745+
tuple
746+
(ion_intensity_data, sample_cols) where:
747+
- ion_intensity_data: Dictionary with intensity distribution and CV data
748+
- sample_cols: List of sample intensity column names
749+
"""
750+
ion_files = fragpipe_files.get("ion", [])
751+
752+
if not ion_files:
753+
log.info("No ion.tsv files found.")
754+
return None, []
755+
756+
all_intensity_data = {
757+
'intensity_distribution': {},
758+
}
759+
all_sample_cols = []
760+
761+
for ion_file in ion_files:
762+
try:
763+
ion_df, sample_cols = ion_reader(ion_file)
764+
765+
if ion_df is None or ion_df.empty:
766+
log.warning(f"Skipping unreadable/empty ion.tsv file: {ion_file}")
767+
continue
768+
769+
log.info(f"Loaded ion.tsv with {len(ion_df)} rows and {len(sample_cols)} samples")
770+
771+
intensity_data = get_ion_intensity_data(ion_df, sample_cols)
772+
773+
if intensity_data:
774+
# Merge intensity distributions
775+
for sample, values in intensity_data.get('intensity_distribution', {}).items():
776+
if sample in all_intensity_data['intensity_distribution']:
777+
all_intensity_data['intensity_distribution'][sample].extend(values)
778+
else:
779+
all_intensity_data['intensity_distribution'][sample] = values
780+
781+
all_sample_cols.extend([c for c in sample_cols if c not in all_sample_cols])
782+
783+
except Exception as e:
784+
log.warning(f"Error parsing ion.tsv file {ion_file}: {e}")
785+
continue
786+
787+
if not all_intensity_data['intensity_distribution']:
788+
log.info("No valid intensity data found in ion.tsv files.")
789+
return None, []
790+
791+
log.info(f"Ion intensity data parsed for {len(all_sample_cols)} samples")
792+
793+
return all_intensity_data, all_sample_cols
794+
795+
@staticmethod
796+
def draw_ion_intensity_distribution(sub_section, intensity_data: dict):
797+
"""
798+
Draw ion-level intensity distribution box plot.
799+
800+
Parameters
801+
----------
802+
sub_section : dict
803+
Section to add the plot to.
804+
intensity_data : dict
805+
Dictionary containing 'intensity_distribution' data.
806+
"""
807+
distribution = intensity_data.get('intensity_distribution', {})
808+
809+
if not distribution:
810+
log.info("No ion intensity distribution data available.")
811+
return
812+
813+
log.info(f"Drawing ion intensity distribution for {len(distribution)} samples")
814+
815+
draw_config = {
816+
"id": "ion_intensity_distribution_box",
817+
"cpswitch": False,
818+
"cpswitch_c_active": False,
819+
"title": "Ion Intensity Distribution",
820+
"tt_decimals": 2,
821+
"xlab": "log2(Intensity)",
822+
"sort_samples": False,
823+
"save_data_file": False,
824+
}
825+
826+
box_html = box.plot(list_of_data_by_sample=distribution, pconfig=draw_config)
827+
828+
box_html = plot_data_check(
829+
plot_data=distribution,
830+
plot_html=box_html,
831+
log_text="pmultiqc.modules.fragpipe.fragpipe",
832+
function_name="draw_ion_intensity_distribution"
833+
)
834+
box_html = plot_html_check(box_html)
835+
836+
add_sub_section(
837+
sub_section=sub_section,
838+
plot=box_html,
839+
order=6,
840+
description="Ion-level intensity distribution per sample from ion.tsv.",
841+
helptext="""
842+
[FragPipe: ion.tsv] This plot shows the log2-transformed ion intensity
843+
distribution for each sample/channel. The ion.tsv file contains precursor-level
844+
quantification data from IonQuant.
845+
846+
For TMT experiments, each box represents a TMT channel.
847+
For label-free experiments, each box represents a sample/run.
848+
849+
A higher median intensity and narrower distribution typically indicate
850+
better quantification quality. Large differences between samples may
851+
indicate normalization issues or batch effects.
852+
""",
853+
)
854+
855+
log.info("Ion intensity distribution plot generated.")
856+
706857

707858
def _calculate_statistics(pipeline_stats: list):
708859

0 commit comments

Comments
 (0)