1+ """Peak-row generation helpers for assignment-noise scenarios."""
2+
13from __future__ import annotations
24
35from typing import Any , Mapping
911
1012
1113def _skew (values : np .ndarray ) -> float :
14+ """Return sample skewness with a stable value for near-constant profiles."""
15+
1216 arr = np .asarray (values , dtype = float )
1317 sd = arr .std ()
1418 if sd < 1e-8 :
@@ -17,6 +21,8 @@ def _skew(values: np.ndarray) -> float:
1721
1822
1923def _beta_score (rng : np .random .Generator , true : bool , fp_prob : float ) -> float :
24+ """Sample an MS2-like support score for true and false candidate evidence."""
25+
2026 if true :
2127 return float (rng .beta (9.0 , 2.2 ))
2228 if rng .random () < fp_prob :
@@ -49,6 +55,8 @@ def _peak_row(
4955 ms2_true_score : bool = False ,
5056 metadata : Mapping [str , Any ] | None = None ,
5157) -> dict [str , Any ]:
58+ """Create one normalized picked-peak row with feature and truth metadata."""
59+
5260 row : dict [str , Any ] = {
5361 "mz" : float (mz ),
5462 "rt" : float (rt ),
@@ -93,6 +101,8 @@ def _peak_row(
93101 available_prob = 0.70 if ms2_true_score else min (0.85 , 0.25 + 0.55 * fp_prob )
94102 available = bool (rng .random () < available_prob )
95103 if not available :
104+ # Keep weak residual scores so the feature exists, but make it clear
105+ # that no useful spectrum should support this row.
96106 for candidate_index in range (n_candidates ):
97107 row [f"ms2_score_cand_{ candidate_index } " ] *= float (rng .uniform (0.0 , 0.25 ))
98108 ms2_scores = [
@@ -107,6 +117,8 @@ def _peak_row(
107117
108118
109119def _finalize_peak_table (peaks : list [dict [str , Any ]], rng : np .random .Generator ) -> pd .DataFrame :
120+ """Compute local context features and assign stable peak identifiers."""
121+
110122 df = pd .DataFrame (peaks )
111123 if df .empty :
112124 return df
@@ -151,6 +163,8 @@ def _add_diffuse_background(
151163 profile : AssignmentNoiseProfile ,
152164 n : int ,
153165) -> None :
166+ """Append unstructured clutter and blank-contaminant background peaks."""
167+
154168 mz_min = float (candidates ["neutral_mass" ].min () - 25.0 )
155169 mz_max = float (candidates ["neutral_mass" ].max () + 45.0 )
156170 rt_center = float (candidates ["pred_rt" ].mean ())
@@ -193,6 +207,8 @@ def _add_candidate_ions(
193207 roles : tuple [str , ...],
194208 latent_profiles : dict [int , np .ndarray ],
195209) -> None :
210+ """Append true candidate-role peaks plus split/merged artifacts."""
211+
196212 for _ , candidate in candidates .iterrows ():
197213 candidate_index = int (candidate ["candidate_index" ])
198214 if int (candidate ["present" ]) <= 0 :
@@ -272,6 +288,8 @@ def _add_candidate_ions(
272288 emitted_any = True
273289
274290 if rng .random () < profile .p_split :
291+ # Split/shoulder artifacts resemble the parent ion in m/z/RT and
292+ # abundance profile but remain background labels.
275293 split_profile = peak_profile + rng .normal (0.0 , 0.15 , size = profile .n_bio_samples )
276294 peaks .append (
277295 _peak_row (
@@ -300,6 +318,9 @@ def _add_candidate_ions(
300318 )
301319
302320 if rng .random () < profile .p_merge :
321+ # Merged peaks intentionally have good-looking evidence but are
322+ # labelled background because the picked feature is not the clean
323+ # theoretical ion.
303324 merged_profile = peak_profile + rng .normal (0.0 , 0.18 , size = profile .n_bio_samples )
304325 peaks .append (
305326 _peak_row (
@@ -335,6 +356,8 @@ def _add_structured_interferents(
335356 ions : pd .DataFrame ,
336357 profile : AssignmentNoiseProfile ,
337358) -> None :
359+ """Append wrong-source peaks near theoretical ions and candidate RTs."""
360+
338361 for _ , ion in ions .iterrows ():
339362 if rng .random () >= profile .p_interferent :
340363 continue
@@ -408,6 +431,8 @@ def _add_coeluting_isobars(
408431 ions : pd .DataFrame ,
409432 profile : AssignmentNoiseProfile ,
410433) -> None :
434+ """Append high-quality background peaks that coelute with theoretical ions."""
435+
411436 for _ , ion in ions .iterrows ():
412437 if rng .random () >= profile .p_coeluting_isobar :
413438 continue
@@ -460,6 +485,8 @@ def _add_matched_decoy_clusters(
460485 profile : AssignmentNoiseProfile ,
461486 latent_profiles : dict [int , np .ndarray ],
462487) -> None :
488+ """Append plausible isotope/adduct clusters for absent or wrong candidates."""
489+
463490 candidate_indices = list (candidates ["candidate_index" ].astype (int ))
464491 absent = list (
465492 candidates .loc [candidates ["present" ].astype (int ) == 0 , "candidate_index" ].astype (int )
@@ -479,6 +506,8 @@ def _add_matched_decoy_clusters(
479506 cand_ions = ions [ions ["candidate_index" ] == candidate_index ].sort_values ("role_index" )
480507 decoy_log_a = float (candidate ["expected_log_intensity" ] + rng .normal (- 0.05 , 0.55 ))
481508 if latent_profiles and rng .random () < 0.65 :
509+ # Reusing a true latent profile makes a decoy cluster look like a
510+ # coherent biological signal instead of independent random clutter.
482511 base_profile = latent_profiles [int (rng .choice (list (latent_profiles .keys ())))].copy ()
483512 base_profile = base_profile - base_profile .mean () + decoy_log_a
484513 else :
@@ -541,6 +570,8 @@ def _add_matched_decoy_clusters(
541570 },
542571 )
543572 if rng .random () < profile .p_decoy_false_ms2 :
573+ # Some decoy clusters get candidate-specific false MS2 support,
574+ # matching the failure mode the classifier must reject.
544575 row [f"ms2_score_cand_{ candidate_index } " ] = float (
545576 max (row .get (f"ms2_score_cand_{ candidate_index } " , 0.0 ), rng .beta (5.0 , 2.0 ))
546577 )
@@ -590,6 +621,8 @@ def _trim_peaks(
590621 rng : np .random .Generator ,
591622 max_peaks : int ,
592623) -> list [dict [str , Any ]]:
624+ """Limit peak count while preserving true ions and hard negative artifacts."""
625+
593626 if len (peaks ) <= max_peaks :
594627 return peaks
595628 true_rows = [p for p in peaks if int (p ["true_label" ]) > 0 ]
@@ -605,6 +638,8 @@ def _trim_peaks(
605638 easy_rows = [
606639 p for p in peaks if int (p ["true_label" ]) == 0 and p ["source_type" ] not in hard_types
607640 ]
641+ # True ions are always retained. Hard negatives are retained before easy
642+ # diffuse clutter so stress profiles remain adversarial after trimming.
608643 remaining = max (0 , max_peaks - len (true_rows ))
609644 if len (hard_rows ) > remaining :
610645 idx = rng .choice (len (hard_rows ), size = remaining , replace = False )
0 commit comments