MaldiDeepKit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ """Composable per-batch augmentations for MALDI-TOF binned spectra.
2
+
3
+ All transforms operate on a ``torch.Tensor`` of shape ``(batch, n_bins)``.
4
+ Each augmentation step is gated by a parameter and is a no-op at its
5
+ default value. Applied in order:
6
+
7
+ 1. Additive Gaussian noise (``noise_std``).
8
+ 2. Per-sample intensity jitter (``intensity_jitter``).
9
+ 3. Random peak dropout (``peak_dropout_rate``).
10
+ 4. Per-sample m/z shift (``mz_shift_max_bins``).
11
+ 5. Spline-based m/z warp (``mz_warp_max_bins`` + ``mz_warp_n_knots``).
12
+ 6. Gaussian blur (``blur_sigma``).
13
+
14
+ Only invoked on training batches. All m/z-axis parameters are specified
15
+ in *bins*, not Daltons.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+
22
+ import numpy as np
23
+ import torch
24
+ from scipy.interpolate import CubicSpline
25
+
26
+
27
+ class SpectrumAugment:
28
+ """Composable per-batch spectrum augmentation.
29
+
30
+ Parameters
31
+ ----------
32
+ noise_std : float, default=0.0
33
+ Standard deviation of additive Gaussian noise.
34
+ intensity_jitter : float, default=0.0
35
+ Half-range of the per-sample multiplicative jitter: every
36
+ sample is scaled by ``1 + U(-jitter, jitter)``. Must be in
37
+ ``[0, 1)``.
38
+ peak_dropout_rate : float, default=0.0
39
+ Per-bin Bernoulli zero-out probability. Must be in ``[0, 1)``.
40
+ mz_shift_max_bins : int, default=0
41
+ Per-sample global m/z shift, drawn uniformly in
42
+ ``[-mz_shift_max_bins, +mz_shift_max_bins]`` and applied with
43
+ :func:`torch.roll`. Units are bins. Must be non-negative.
44
+ mz_warp_max_bins : int, default=0
45
+ Peak amplitude (in bins) of a smooth cubic-spline warp of the
46
+ m/z axis. ``0`` disables the warp. Runs on CPU. A warning is
47
+ emitted when the amplitude exceeds 5 % of ``n_bins``, since
48
+ beyond that the boundary clipping starts to dominate the
49
+ augmentation distribution. Must be non-negative.
50
+ mz_warp_n_knots : int, default=10
51
+ Number of interior spline control points for the m/z warp.
52
+ Only used when ``mz_warp_max_bins > 0``. Must be non-negative.
53
+ blur_sigma : float, default=0.0
54
+ Standard deviation (in bins) of a 1-D Gaussian blur along the
55
+ m/z axis. Zero disables the blur.
56
+ random_state : int, optional
57
+ If provided, the transform is seeded for deterministic batches.
58
+ When ``None`` (default), PyTorch's global RNG is used.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ noise_std: float = 0.0,
64
+ intensity_jitter: float = 0.0,
65
+ peak_dropout_rate: float = 0.0,
66
+ mz_shift_max_bins: int = 0,
67
+ mz_warp_max_bins: int = 0,
68
+ mz_warp_n_knots: int = 10,
69
+ blur_sigma: float = 0.0,
70
+ random_state: int | None = None,
71
+ ) -> None:
72
+ if noise_std < 0:
73
+ raise ValueError(f"noise_std must be >= 0; got {noise_std!r}.")
74
+ if not 0.0 <= intensity_jitter < 1.0:
75
+ raise ValueError(
76
+ f"intensity_jitter must be in [0, 1); got {intensity_jitter!r}."
77
+ )
78
+ if not 0.0 <= peak_dropout_rate < 1.0:
79
+ raise ValueError(
80
+ f"peak_dropout_rate must be in [0, 1); got {peak_dropout_rate!r}."
81
+ )
82
+ if mz_shift_max_bins < 0:
83
+ raise ValueError(
84
+ f"mz_shift_max_bins must be >= 0; got {mz_shift_max_bins!r}."
85
+ )
86
+ if mz_warp_max_bins < 0:
87
+ raise ValueError(
88
+ f"mz_warp_max_bins must be >= 0; got {mz_warp_max_bins!r}."
89
+ )
90
+ if mz_warp_n_knots < 0:
91
+ raise ValueError(f"mz_warp_n_knots must be >= 0; got {mz_warp_n_knots!r}.")
92
+ if blur_sigma < 0:
93
+ raise ValueError(f"blur_sigma must be >= 0; got {blur_sigma!r}.")
94
+ self.noise_std = float(noise_std)
95
+ self.intensity_jitter = float(intensity_jitter)
96
+ self.peak_dropout_rate = float(peak_dropout_rate)
97
+ self.mz_shift_max_bins = int(mz_shift_max_bins)
98
+ self.mz_warp_max_bins = int(mz_warp_max_bins)
99
+ self.mz_warp_n_knots = int(mz_warp_n_knots)
100
+ self.blur_sigma = float(blur_sigma)
101
+ self.random_state = random_state
102
+ self._generator: torch.Generator | None = None
103
+ self._np_rng: np.random.Generator | None = None
104
+ self._blur_kernel: torch.Tensor | None = None
105
+
106
+ def _generator_for(self, device: torch.device) -> torch.Generator | None:
107
+ if self.random_state is None:
108
+ return None
109
+ if self._generator is None or self._generator.device != device:
110
+ self._generator = torch.Generator(device=device)
111
+ self._generator.manual_seed(int(self.random_state))
112
+ return self._generator
113
+
114
+ def _numpy_generator(self) -> np.random.Generator:
115
+ if self._np_rng is None:
116
+ self._np_rng = np.random.default_rng(self.random_state)
117
+ return self._np_rng
118
+
119
+ def _is_identity(self) -> bool:
120
+ return (
121
+ self.noise_std == 0.0
122
+ and self.intensity_jitter == 0.0
123
+ and self.peak_dropout_rate == 0.0
124
+ and self.mz_shift_max_bins == 0
125
+ and self.mz_warp_max_bins == 0
126
+ and self.blur_sigma == 0.0
127
+ )
128
+
129
+ def _apply_mz_shift(
130
+ self, X: torch.Tensor, gen: torch.Generator | None
131
+ ) -> torch.Tensor:
132
+ k = self.mz_shift_max_bins
133
+ shifts = torch.randint(
134
+ low=-k,
135
+ high=k + 1,
136
+ size=(X.shape[0],),
137
+ generator=gen,
138
+ device=X.device,
139
+ )
140
+ out = torch.empty_like(X)
141
+ for i in range(X.shape[0]):
142
+ out[i] = torch.roll(X[i], shifts=int(shifts[i].item()), dims=0)
143
+ return out
144
+
145
+ def _apply_spline_warp(self, X: torch.Tensor) -> torch.Tensor:
146
+ rng = self._numpy_generator()
147
+ device = X.device
148
+ x_np = X.detach().cpu().numpy().copy()
149
+ n_samples, n_bins = x_np.shape
150
+ if self.mz_warp_max_bins > 0.05 * n_bins:
151
+ import warnings as _warnings
152
+
153
+ _warnings.warn(
154
+ f"mz_warp_max_bins={self.mz_warp_max_bins} exceeds 5% of "
155
+ f"n_bins={n_bins}; warped indices outside the support are "
156
+ "clipped before interpolation, which produces asymmetric "
157
+ "edge flattening. Consider reducing mz_warp_max_bins.",
158
+ stacklevel=2,
159
+ )
160
+ original_indices = np.arange(n_bins, dtype=np.float64)
161
+ n_knots = self.mz_warp_n_knots
162
+ knot_positions = np.linspace(0, n_bins - 1, n_knots + 2)
163
+ for i in range(n_samples):
164
+ knot_shifts = np.zeros(n_knots + 2)
165
+ if n_knots > 0:
166
+ knot_shifts[1:-1] = rng.uniform(
167
+ -self.mz_warp_max_bins,
168
+ self.mz_warp_max_bins,
169
+ size=n_knots,
170
+ )
171
+ spline = CubicSpline(knot_positions, knot_shifts, bc_type="clamped")
172
+ smooth_shifts = spline(original_indices)
173
+ warped = np.clip(original_indices + smooth_shifts, 0, n_bins - 1)
174
+ x_np[i] = np.interp(original_indices, warped, x_np[i])
175
+ return torch.from_numpy(x_np).to(device=device, dtype=X.dtype)
176
+
177
+ def _build_blur_kernel(
178
+ self, device: torch.device, dtype: torch.dtype
179
+ ) -> torch.Tensor:
180
+ if (
181
+ self._blur_kernel is not None
182
+ and self._blur_kernel.device == device
183
+ and self._blur_kernel.dtype == dtype
184
+ ):
185
+ return self._blur_kernel
186
+ radius = int(math.ceil(3.0 * self.blur_sigma))
187
+ xs = torch.arange(-radius, radius + 1, device=device, dtype=dtype)
188
+ kernel = torch.exp(-0.5 * (xs / self.blur_sigma) ** 2)
189
+ kernel = kernel / kernel.sum()
190
+ self._blur_kernel = kernel.view(1, 1, -1)
191
+ return self._blur_kernel
192
+
193
+ def _apply_blur(self, X: torch.Tensor) -> torch.Tensor:
194
+ kernel = self._build_blur_kernel(X.device, X.dtype)
195
+ padding = kernel.shape[-1] // 2
196
+ x3 = X.unsqueeze(1)
197
+ out = torch.nn.functional.conv1d(x3, kernel, padding=padding)
198
+ return out.squeeze(1)
199
+
200
+ def __call__(self, X: torch.Tensor) -> torch.Tensor:
201
+ """Apply the enabled augmentations to ``X``.
202
+
203
+ Returns the input unchanged if no augmentation is enabled.
204
+ """
205
+ if self._is_identity():
206
+ return X
207
+ gen = self._generator_for(X.device)
208
+ out = X
209
+
210
+ if self.noise_std > 0.0:
211
+ noise = (
212
+ torch.randn(out.shape, generator=gen, device=out.device)
213
+ * self.noise_std
214
+ )
215
+ out = out + noise
216
+
217
+ if self.intensity_jitter > 0.0:
218
+ jitter = (
219
+ torch.rand((out.shape[0], 1), generator=gen, device=out.device) * 2.0
220
+ - 1.0
221
+ ) * self.intensity_jitter
222
+ out = out * (1.0 + jitter)
223
+
224
+ if self.peak_dropout_rate > 0.0:
225
+ keep = 1.0 - self.peak_dropout_rate
226
+ mask = torch.empty(out.shape, device=out.device).bernoulli_(
227
+ keep, generator=gen
228
+ )
229
+ out = out * mask
230
+
231
+ if self.mz_shift_max_bins > 0:
232
+ out = self._apply_mz_shift(out, gen)
233
+
234
+ if self.mz_warp_max_bins > 0:
235
+ out = self._apply_spline_warp(out)
236
+
237
+ if self.blur_sigma > 0.0:
238
+ out = self._apply_blur(out)
239
+
240
+ return out
241
+
242
+ def __repr__(self) -> str:
243
+ return (
244
+ f"SpectrumAugment(noise_std={self.noise_std}, "
245
+ f"intensity_jitter={self.intensity_jitter}, "
246
+ f"peak_dropout_rate={self.peak_dropout_rate}, "
247
+ f"mz_shift_max_bins={self.mz_shift_max_bins}, "
248
+ f"mz_warp_max_bins={self.mz_warp_max_bins}, "
249
+ f"mz_warp_n_knots={self.mz_warp_n_knots}, "
250
+ f"blur_sigma={self.blur_sigma})"
251
+ )
@@ -0,0 +1,15 @@
1
+ """Shared primitives for MaldiDeepKit classifiers.
2
+
3
+ Exposes :class:`BaseSpectralClassifier`, the abstract base for all six
4
+ model families in the package, together with the :class:`SpectralDataset`
5
+ / :func:`make_loaders` data utilities.
6
+ """
7
+
8
+ from .classifier import BaseSpectralClassifier
9
+ from .data import SpectralDataset, make_loaders
10
+
11
+ __all__ = [
12
+ "BaseSpectralClassifier",
13
+ "SpectralDataset",
14
+ "make_loaders",
15
+ ]