Skip to content

Commit fcf2e7e

Browse files
authored
Merge pull request #212 from singjc/split/scoring-large-osw
feat: improve scoring workflows for large OSW datasets
2 parents d678739 + 7cab202 commit fcf2e7e

41 files changed

Lines changed: 1421 additions & 540 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyprophet/_config.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ class RunnerConfig:
114114
ipf_max_peakgroup_pep (float): Max PEP for peak group consideration in IPF.
115115
ipf_max_transition_isotope_overlap (float): Max isotope overlap for transition selection in IPF.
116116
ipf_min_transition_sn (float): Min log S/N for transition selection in IPF.
117+
transition_training_require_unique_mapping (bool): Whether to restrict transition semi-supervised target training peaks to uniquely mapped transitions.
118+
transition_training_require_phospho_loss (bool): Whether to restrict transition semi-supervised target training peaks to phospho-loss transitions.
117119
118120
glyco (bool): Whether glycopeptide-specific scoring is enabled.
119121
density_estimator (str): Score density estimation method ('kde' or 'gmm').
@@ -124,6 +126,8 @@ class RunnerConfig:
124126
threads (int): Number of CPU threads to use; -1 means all CPUs.
125127
test (bool): Whether to enable test mode with deterministic behavior.
126128
color_palette (str): Color palette used in PDF report rendering.
129+
report_mode (str): PDF report scope: 'full', 'main', or 'none'.
130+
apply_weights_run_batch_size (int): Number of runs to score together per streamed OSW apply batch. `0` means auto.
127131
"""
128132

129133
# Scoring / classifier options
@@ -160,6 +164,8 @@ class RunnerConfig:
160164
ipf_max_peakgroup_pep: float = 0.7
161165
ipf_max_transition_isotope_overlap: float = 0.5
162166
ipf_min_transition_sn: float = 0.0
167+
transition_training_require_unique_mapping: bool = False
168+
transition_training_require_phospho_loss: bool = False
163169

164170
# Glyco options
165171
glyco: bool = False
@@ -172,6 +178,8 @@ class RunnerConfig:
172178
threads: int = 1
173179
test: bool = False
174180
color_palette: str = "normal"
181+
report_mode: Literal["full", "main", "none"] = "full"
182+
apply_weights_run_batch_size: int = 0
175183

176184
def __post_init__(self):
177185
# Check for auto main score selection
@@ -215,6 +223,8 @@ def __str__(self):
215223
f" ipf_max_peakgroup_pep={self.ipf_max_peakgroup_pep}",
216224
f" ipf_max_transition_isotope_overlap={self.ipf_max_transition_isotope_overlap}",
217225
f" ipf_min_transition_sn={self.ipf_min_transition_sn}",
226+
f" transition_training_require_unique_mapping={self.transition_training_require_unique_mapping}",
227+
f" transition_training_require_phospho_loss={self.transition_training_require_phospho_loss}",
218228
]
219229
)
220230

@@ -235,6 +245,8 @@ def __str__(self):
235245
f" threads={self.threads}",
236246
f" test={self.test}",
237247
f" color_palette='{self.color_palette}'",
248+
f" report_mode='{self.report_mode}'",
249+
f" apply_weights_run_batch_size={self.apply_weights_run_batch_size}",
238250
")",
239251
]
240252
)
@@ -247,7 +259,11 @@ def __repr__(self):
247259
f"ss_main_score='{self.ss_main_score}', xeval_fraction={self.xeval_fraction}, "
248260
f"xeval_num_iter={self.xeval_num_iter}, ss_initial_fdr={self.ss_initial_fdr}, "
249261
f"ss_iteration_fdr={self.ss_iteration_fdr}, ss_num_iter={self.ss_num_iter}, "
250-
f"group_id='{self.group_id}', glyco={self.glyco}, threads={self.threads})"
262+
f"group_id='{self.group_id}', glyco={self.glyco}, threads={self.threads}, "
263+
f"transition_training_require_unique_mapping={self.transition_training_require_unique_mapping}, "
264+
f"transition_training_require_phospho_loss={self.transition_training_require_phospho_loss}, "
265+
f"report_mode='{self.report_mode}', "
266+
f"apply_weights_run_batch_size={self.apply_weights_run_batch_size})"
251267
)
252268

253269

@@ -267,6 +283,7 @@ class RunnerIOConfig(BaseIOConfig):
267283
"""
268284

269285
runner: RunnerConfig
286+
run_id_filter: Optional[Union[int, List[int], tuple]] = None
270287
extra_writes: dict = field(init=False)
271288

272289
def __post_init__(self):
@@ -294,6 +311,7 @@ def to_kwargs(self) -> Dict[str, Any]:
294311
"subsample_ratio": self.subsample_ratio,
295312
"level": self.level,
296313
"prefix": self.prefix,
314+
"run_id_filter": self.run_id_filter,
297315
**vars(self.runner),
298316
}
299317

@@ -331,6 +349,8 @@ def from_cli_args(
331349
ipf_max_peakgroup_pep,
332350
ipf_max_transition_isotope_overlap,
333351
ipf_min_transition_sn,
352+
transition_training_require_unique_mapping,
353+
transition_training_require_phospho_loss,
334354
add_alignment_features,
335355
glyco,
336356
density_estimator,
@@ -340,6 +360,8 @@ def from_cli_args(
340360
test,
341361
color_palette,
342362
main_score_selection_report,
363+
report_mode,
364+
apply_weights_run_batch_size,
343365
):
344366
"""
345367
Creates a configuration object from command-line arguments.
@@ -399,6 +421,8 @@ def from_cli_args(
399421
ipf_max_peakgroup_pep=ipf_max_peakgroup_pep,
400422
ipf_max_transition_isotope_overlap=ipf_max_transition_isotope_overlap,
401423
ipf_min_transition_sn=ipf_min_transition_sn,
424+
transition_training_require_unique_mapping=transition_training_require_unique_mapping,
425+
transition_training_require_phospho_loss=transition_training_require_phospho_loss,
402426
add_alignment_features=add_alignment_features,
403427
glyco=glyco,
404428
density_estimator=density_estimator,
@@ -407,6 +431,8 @@ def from_cli_args(
407431
threads=threads,
408432
test=test,
409433
color_palette=color_palette,
434+
report_mode=report_mode,
435+
apply_weights_run_batch_size=apply_weights_run_batch_size,
410436
)
411437

412438
return cls(

pyprophet/cli/score.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# Defer import of runner to avoid premature sklearn import before OMP_NUM_THREADS is set
1717
# from ..scoring.runner import PyProphetLearner, PyProphetWeightApplier
1818

19+
LARGE_RUN_MAIN_REPORT_THRESHOLD = 50
20+
1921

2022
# PyProphet semi-supervised learning and scoring
2123
@click.command(name="score", cls=AdvancedHelpCommand)
@@ -176,6 +178,18 @@
176178
help="Minimum log signal-to-noise level to consider transitions in IPF. Set -1 to disable this filter.",
177179
hidden=True,
178180
)
181+
@click.option(
182+
"--transition_training_require_unique_mapping/--no-transition_training_require_unique_mapping",
183+
default=False,
184+
show_default=True,
185+
help="Experimental: when learning transition scores, restrict target training peaks to uniquely mapped transitions.",
186+
)
187+
@click.option(
188+
"--transition_training_require_phospho_loss/--no-transition_training_require_phospho_loss",
189+
default=False,
190+
show_default=True,
191+
help="Experimental: when learning transition scores, restrict target training peaks to phospho-loss transitions.",
192+
)
179193
# Glyco/GproDIA Options
180194
@click.option(
181195
"--glyco/--no-glyco",
@@ -224,6 +238,20 @@
224238
help="Generate a report for main score selection process.",
225239
hidden=True,
226240
)
241+
@click.option(
242+
"--report_mode",
243+
default="auto",
244+
show_default=True,
245+
type=click.Choice(["auto", "full", "main", "none"]),
246+
help="PDF report scope: 'full' writes all report pages, 'main' writes only the core score diagnostics, and 'none' disables report generation.",
247+
)
248+
@click.option(
249+
"--apply_weights_run_batch_size",
250+
default=0,
251+
show_default=True,
252+
type=int,
253+
help="When streamed OSW weight application is used, score this many runs per batch. Use 0 for automatic batching and 1 to force one run at a time.",
254+
)
227255
# Processing
228256
@click.option(
229257
"--threads",
@@ -283,12 +311,16 @@ def score(
283311
ipf_max_peakgroup_pep,
284312
ipf_max_transition_isotope_overlap,
285313
ipf_min_transition_sn,
314+
transition_training_require_unique_mapping,
315+
transition_training_require_phospho_loss,
286316
glyco,
287317
density_estimator,
288318
grid_size,
289319
tric_chromprob,
290320
color_palette,
291321
main_score_selection_report,
322+
report_mode,
323+
apply_weights_run_batch_size,
292324
threads,
293325
test,
294326
profile, # NOQA: F841 unused variable, but used in decorator
@@ -357,6 +389,8 @@ def score(
357389
ipf_max_peakgroup_pep,
358390
ipf_max_transition_isotope_overlap,
359391
ipf_min_transition_sn,
392+
transition_training_require_unique_mapping,
393+
transition_training_require_phospho_loss,
360394
add_alignment_features,
361395
glyco,
362396
density_estimator,
@@ -366,6 +400,8 @@ def score(
366400
test,
367401
color_palette,
368402
main_score_selection_report,
403+
report_mode,
404+
apply_weights_run_batch_size,
369405
)
370406

371407
write_logfile(
@@ -374,10 +410,13 @@ def score(
374410
ctx.obj["LOG_HEADER"],
375411
)
376412

413+
num_runs = None
414+
if subsample_ratio == 1.0 or report_mode == "auto":
415+
num_runs = get_num_runs(infile, config.file_type)
416+
377417
# Auto-subsample based on number of runs if applicable
378418
if subsample_ratio == 1.0:
379419
# Check if we should auto-subsample
380-
num_runs = get_num_runs(infile, config.file_type)
381420
if num_runs > 20:
382421
config.subsample_ratio = 1.0 / num_runs
383422
logger.info(
@@ -393,6 +432,19 @@ def score(
393432
"Using full dataset for semi-supervised learning."
394433
)
395434

435+
if report_mode == "auto":
436+
if num_runs and num_runs > LARGE_RUN_MAIN_REPORT_THRESHOLD:
437+
config.runner.report_mode = "main"
438+
logger.info(
439+
f"Large experiment detected ({num_runs} runs). "
440+
"Switching report_mode to 'main' to skip expensive identification/quantification report pages. "
441+
"Use --report_mode full to force the complete report."
442+
)
443+
else:
444+
config.runner.report_mode = "full"
445+
else:
446+
config.runner.report_mode = report_mode
447+
396448
# Validate file type and subsample ratio. OSW, parquet, parquet_split, and parquet_split_multi all support subsampling
397449
if (
398450
config.file_type not in ["osw", "parquet", "parquet_split", "parquet_split_multi"]

pyprophet/io/_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,14 @@ def save_weights(self, weights):
351351
f"Classifier {self.classifier} not supported for saving weights."
352352
)
353353

354+
def save_scorer(self, scorer):
355+
"""
356+
Persist a scorer object when the backend supports it.
357+
358+
The default implementation is a no-op.
359+
"""
360+
return None
361+
354362
def _prepare_score_dataframe(
355363
self, df: pd.DataFrame, level: str, prefix: str
356364
) -> pd.DataFrame:
@@ -446,6 +454,11 @@ def _write_pdf_report(self, result, pi0):
446454
Write a PDF report if the scoring results contain final statistics.
447455
"""
448456

457+
report_mode = getattr(self.config.runner, "report_mode", "full")
458+
if report_mode == "none":
459+
logger.info("Skipping PDF report generation (report_mode=none).")
460+
return
461+
449462
if result.final_statistics is None:
450463
return
451464

@@ -503,6 +516,7 @@ def _write_pdf_report(self, result, pi0):
503516
self.config.runner.color_palette,
504517
self.level,
505518
df=df,
519+
report_mode=report_mode,
506520
)
507521
logger.success(f"{pdf_path} written.")
508522

pyprophet/io/export/osw.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -781,9 +781,9 @@ def _add_transition_data(self, data, con, cfg):
781781
if check_sqlite_table(con, "SCORE_TRANSITION"):
782782
transition_query = f"""
783783
SELECT FEATURE_TRANSITION.FEATURE_ID AS id,
784-
GROUP_CONCAT(AREA_INTENSITY,';') AS aggr_Peak_Area,
785-
GROUP_CONCAT(APEX_INTENSITY,';') AS aggr_Peak_Apex,
786-
GROUP_CONCAT(TRANSITION.ID || "_" || TRANSITION.TYPE || TRANSITION.ORDINAL || "_" || TRANSITION.CHARGE,';') AS aggr_Fragment_Annotation
784+
GROUP_CONCAT(AREA_INTENSITY,';' ORDER BY TRANSITION.ID) AS aggr_Peak_Area,
785+
GROUP_CONCAT(APEX_INTENSITY,';' ORDER BY TRANSITION.ID) AS aggr_Peak_Apex,
786+
GROUP_CONCAT(TRANSITION.ID || "_" || TRANSITION.TYPE || TRANSITION.ORDINAL || "_" || TRANSITION.CHARGE,';' ORDER BY TRANSITION.ID) AS aggr_Fragment_Annotation
787787
FROM FEATURE_TRANSITION
788788
INNER JOIN TRANSITION ON FEATURE_TRANSITION.TRANSITION_ID = TRANSITION.ID
789789
INNER JOIN SCORE_TRANSITION ON FEATURE_TRANSITION.TRANSITION_ID = SCORE_TRANSITION.TRANSITION_ID AND FEATURE_TRANSITION.FEATURE_ID = SCORE_TRANSITION.FEATURE_ID
@@ -793,9 +793,9 @@ def _add_transition_data(self, data, con, cfg):
793793
else:
794794
transition_query = """
795795
SELECT FEATURE_ID AS id,
796-
GROUP_CONCAT(AREA_INTENSITY,';') AS aggr_Peak_Area,
797-
GROUP_CONCAT(APEX_INTENSITY,';') AS aggr_Peak_Apex,
798-
GROUP_CONCAT(TRANSITION.ID || "_" || TRANSITION.TYPE || TRANSITION.ORDINAL || "_" || TRANSITION.CHARGE,';') AS aggr_Fragment_Annotation
796+
GROUP_CONCAT(AREA_INTENSITY,';' ORDER BY TRANSITION.ID) AS aggr_Peak_Area,
797+
GROUP_CONCAT(APEX_INTENSITY,';' ORDER BY TRANSITION.ID) AS aggr_Peak_Apex,
798+
GROUP_CONCAT(TRANSITION.ID || "_" || TRANSITION.TYPE || TRANSITION.ORDINAL || "_" || TRANSITION.CHARGE,';' ORDER BY TRANSITION.ID) AS aggr_Fragment_Annotation
799799
FROM FEATURE_TRANSITION
800800
INNER JOIN TRANSITION ON FEATURE_TRANSITION.TRANSITION_ID = TRANSITION.ID
801801
GROUP BY FEATURE_ID

0 commit comments

Comments
 (0)