1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Isotope-pattern scoring: Bhattacharyya coefficient against a theoretical
//! distribution from an [`IsotopeModel`].
//!
//! The composition itself lives in [`super::model`]; this module is the scoring
//! layer over it, including the sulfur-aware maximum used for peptides. All
//! arithmetic is handed off to the element cache in [`super::elements`], which
//! makes varying `n_S` alone cheap and exact.
use ;
use IsotopeModel;
/// Resolve user-declared sulfur `offsets` against a molecule's expected count.
///
/// Each offset is applied to `ceil(expected n_S)`, negatives saturate at 0, and
/// the result is deduplicated (order-preserving) so a config like
/// `[-2, -1, 0, 1]` on a small peptide collapses to `{0, 1, 2}` rather than
/// scoring `n_S = 0` three times. Counts above `MAX_S` are clamped by the
/// element cache.
///
/// Empty `offsets` means "no sulfur awareness" and yields a single template at
/// the plain half-up rounded count — see [`bhattacharyya_score_best_sulfur`].
/// Sulfur-aware Bhattacharyya scoring.
///
/// Scores `obs` against one template per declared sulfur offset (see
/// [`resolve_sulfur_counts`]) and returns the best BC with its winning `n_S`.
/// For peptides this corrects the systematic bias on Cys/Met-rich sequences,
/// where ³⁴S (4.25 %, +2 Da) elevates M+2 well above the prediction.
///
/// Offsets are relative to the **ceiling** of the expected count, so on the
/// peptide model the default `[-1, 0, 1]` gives `{0, 1, 2}` up to 2665 Da,
/// `{1, 2, 3}` to 5330, `{2, 3, 4}` to 7995. Declare `[-1, 0, 1, 2]` (or include
/// a `0`-reaching offset) to keep the no-sulfur template on large peptides —
/// ~32 % of 3 kDa tryptic peptides have no sulfur at all.
///
/// An empty offset list disables sulfur awareness: a single plain template at
/// the half-up rounded count. **A model with no sulfur** (RNA, DNA) does the
/// same whatever the offsets say — see [`IsotopeModel::has_sulfur`].
///
/// Note this is a **max over templates**, so widening the offset list can only
/// raise scores — including for decoys and mis-assembled chains. Judge a change
/// to the list on discrimination (recall / target-decoy separation), never on
/// the isotope-score distribution alone.
/// Compute Bhattacharyya coefficient between observed and theoretical distributions.
///
/// BC = Σ sqrt(p_i * q_i) over all `k = min(obs.len(), 10)` positions, then
/// multiplied by `(1 - missed_penalty)` where `missed_penalty` is the
/// theoretical mass beyond position k.
///
/// No active-position mask. Positions where the template predicts ~nothing
/// still appear in both `obs_sum` and `theo_sum`. Effect: an over-extended
/// chain that grafted a noise hill at M+8 (where theory is ~0) dilutes
/// `obs_sum`, shrinking p_i at the real positions; the noise position itself
/// contributes ~0 to BC because q_i ≈ 0. Net: BC drops, as it should.
///
/// Returns BC in [0, 1].