MaldiAMRKit 0.16.0__tar.gz → 0.17.0__tar.gz

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.
Files changed (103) hide show
  1. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0/MaldiAMRKit.egg-info}/PKG-INFO +1 -1
  2. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/SOURCES.txt +4 -1
  3. {maldiamrkit-0.16.0/MaldiAMRKit.egg-info → maldiamrkit-0.17.0}/PKG-INFO +1 -1
  4. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/__init__.py +3 -3
  5. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/__init__.py +2 -0
  6. maldiamrkit-0.17.0/maldiamrkit/alignment/peak_align.py +123 -0
  7. maldiamrkit-0.17.0/maldiamrkit/detection/__init__.py +12 -0
  8. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/detection/peak_detector.py +271 -1
  9. maldiamrkit-0.17.0/maldiamrkit/detection/peaklist.py +469 -0
  10. maldiamrkit-0.17.0/tests/test_peaklist.py +334 -0
  11. maldiamrkit-0.16.0/maldiamrkit/detection/__init__.py +0 -5
  12. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/LICENSE +0 -0
  13. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/dependency_links.txt +0 -0
  14. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/entry_points.txt +0 -0
  15. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/requires.txt +0 -0
  16. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/top_level.txt +0 -0
  17. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/README.md +0 -0
  18. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/_compat.py +0 -0
  19. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/raw_warping.py +0 -0
  20. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/strategies.py +0 -0
  21. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/warping.py +0 -0
  22. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/cli.py +0 -0
  23. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/__init__.py +0 -0
  24. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/README.md +0 -0
  25. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.0.yaml +0 -0
  26. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.1.yaml +0 -0
  27. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.2.yaml +0 -0
  28. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.3.yaml +0 -0
  29. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v10.0.yaml +0 -0
  30. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v11.0.yaml +0 -0
  31. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v12.0.yaml +0 -0
  32. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v13.1.yaml +0 -0
  33. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v14.0.yaml +0 -0
  34. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v15.0.yaml +0 -0
  35. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v16.0.yaml +0 -0
  36. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v2.0.yaml +0 -0
  37. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v4.0.yaml +0 -0
  38. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v5.0.yaml +0 -0
  39. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v6.0.yaml +0 -0
  40. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v7.1.yaml +0 -0
  41. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v8.0.yaml +0 -0
  42. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v8.1.yaml +0 -0
  43. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v9.0.yaml +0 -0
  44. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/example.yaml +0 -0
  45. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/builder.py +0 -0
  46. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/dataset_layouts.py +0 -0
  47. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/duplicates.py +0 -0
  48. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/input_layouts.py +0 -0
  49. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/loader.py +0 -0
  50. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/site_info.py +0 -0
  51. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/dataset.py +0 -0
  52. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/__init__.py +0 -0
  53. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/analysis.py +0 -0
  54. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/plots.py +0 -0
  55. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/stats.py +0 -0
  56. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/__init__.py +0 -0
  57. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/monitor.py +0 -0
  58. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/pca_drift.py +0 -0
  59. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/peak_drift.py +0 -0
  60. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/plots.py +0 -0
  61. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/reference.py +0 -0
  62. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/__init__.py +0 -0
  63. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/label_encoder.py +0 -0
  64. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/metrics.py +0 -0
  65. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/mic_regression.py +0 -0
  66. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/splitting.py +0 -0
  67. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/filters.py +0 -0
  68. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/__init__.py +0 -0
  69. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/mic.py +0 -0
  70. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/readers.py +0 -0
  71. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/__init__.py +0 -0
  72. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/binning.py +0 -0
  73. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/merging.py +0 -0
  74. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/pipeline.py +0 -0
  75. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/preprocessing_pipeline.py +0 -0
  76. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/quality.py +0 -0
  77. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/transformers.py +0 -0
  78. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/py.typed +0 -0
  79. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/__init__.py +0 -0
  80. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/clustering.py +0 -0
  81. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/metrics.py +0 -0
  82. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/pairwise.py +0 -0
  83. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/plots.py +0 -0
  84. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/spectrum.py +0 -0
  85. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/__init__.py +0 -0
  86. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/breakpoint.py +0 -0
  87. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/label_encoder.py +0 -0
  88. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/mic_encoder.py +0 -0
  89. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/__init__.py +0 -0
  90. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/_common.py +0 -0
  91. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/alignment_plots.py +0 -0
  92. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/exploratory_plots.py +0 -0
  93. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/peak_plots.py +0 -0
  94. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/spectrum_plots.py +0 -0
  95. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/pyproject.toml +0 -0
  96. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-batch.txt +0 -0
  97. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-dev.txt +0 -0
  98. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-docs.txt +0 -0
  99. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements.txt +0 -0
  100. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/setup.cfg +0 -0
  101. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/tests/test_compat.py +0 -0
  102. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/tests/test_numerical_edge_cases.py +0 -0
  103. {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/tests/test_peak_detector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MaldiAMRKit
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: A comprehensive toolkit for MALDI-TOF mass spectrometry data preprocessing for antimicrobial resistance (AMR) prediction purposes
5
5
  Author-email: Ettore Rocchi <ettore.rocchi3@unibo.it>
6
6
  License-Expression: MIT
@@ -19,6 +19,7 @@ maldiamrkit/filters.py
19
19
  maldiamrkit/py.typed
20
20
  maldiamrkit/spectrum.py
21
21
  maldiamrkit/alignment/__init__.py
22
+ maldiamrkit/alignment/peak_align.py
22
23
  maldiamrkit/alignment/raw_warping.py
23
24
  maldiamrkit/alignment/strategies.py
24
25
  maldiamrkit/alignment/warping.py
@@ -52,6 +53,7 @@ maldiamrkit/data/breakpoints/eucast/eucast_v8.1.yaml
52
53
  maldiamrkit/data/breakpoints/eucast/eucast_v9.0.yaml
53
54
  maldiamrkit/detection/__init__.py
54
55
  maldiamrkit/detection/peak_detector.py
56
+ maldiamrkit/detection/peaklist.py
55
57
  maldiamrkit/differential/__init__.py
56
58
  maldiamrkit/differential/analysis.py
57
59
  maldiamrkit/differential/plots.py
@@ -94,4 +96,5 @@ maldiamrkit/visualization/peak_plots.py
94
96
  maldiamrkit/visualization/spectrum_plots.py
95
97
  tests/test_compat.py
96
98
  tests/test_numerical_edge_cases.py
97
- tests/test_peak_detector.py
99
+ tests/test_peak_detector.py
100
+ tests/test_peaklist.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MaldiAMRKit
3
- Version: 0.16.0
3
+ Version: 0.17.0
4
4
  Summary: A comprehensive toolkit for MALDI-TOF mass spectrometry data preprocessing for antimicrobial resistance (AMR) prediction purposes
5
5
  Author-email: Ettore Rocchi <ettore.rocchi3@unibo.it>
6
6
  License-Expression: MIT
@@ -5,8 +5,8 @@ Subpackage guide
5
5
  - ``maldiamrkit.data`` -- DatasetBuilder, DatasetLoader, InputLayout, DatasetLayout
6
6
  - ``maldiamrkit.filters`` -- SpeciesFilter, DrugFilter, QualityFilter, MetadataFilter
7
7
  - ``maldiamrkit.preprocessing`` -- PreprocessingPipeline, transformers, binning, quality, merging
8
- - ``maldiamrkit.alignment`` -- Warping, RawWarping, create_raw_input, AlignmentStrategy, AlignmentMethod
9
- - ``maldiamrkit.detection`` -- MaldiPeakDetector
8
+ - ``maldiamrkit.alignment`` -- Warping, RawWarping, create_raw_input, align_peaks, AlignmentStrategy, AlignmentMethod
9
+ - ``maldiamrkit.detection`` -- MaldiPeakDetector, PeakSet, PeakList, create_peakset_input
10
10
  - ``maldiamrkit.evaluation`` -- AMR metrics (binary + mic_regression_report), splitting (LabelEncoder moved to susceptibility)
11
11
  - ``maldiamrkit.susceptibility`` -- MICEncoder, BreakpointTable, BreakpointResult, LabelEncoder
12
12
  - ``maldiamrkit.similarity`` -- spectral_distance, pairwise_distances, clustering, plots
@@ -27,7 +27,7 @@ Examples
27
27
  from .dataset import MaldiSet
28
28
  from .spectrum import MaldiSpectrum
29
29
 
30
- __version__ = "0.16.0"
30
+ __version__ = "0.17.0"
31
31
  __author__ = "Ettore Rocchi"
32
32
 
33
33
  __all__ = [
@@ -1,5 +1,6 @@
1
1
  """Spectral alignment and warping transformers."""
2
2
 
3
+ from .peak_align import align_peaks
3
4
  from .raw_warping import RawWarping, create_raw_input
4
5
  from .strategies import AlignmentMethod, AlignmentStrategy
5
6
  from .warping import Warping
@@ -9,5 +10,6 @@ __all__ = [
9
10
  "AlignmentStrategy",
10
11
  "RawWarping",
11
12
  "Warping",
13
+ "align_peaks",
12
14
  "create_raw_input",
13
15
  ]
@@ -0,0 +1,123 @@
1
+ """Fit-free peak-m/z alignment for peak sets.
2
+
3
+ :func:`align_peaks` warps the m/z positions of a :class:`~maldiamrkit.detection.PeakSet`
4
+ onto a reference peak list using the shared alignment strategies. It is
5
+ **fit-free**: the reference peaks are supplied by the caller, which is expected
6
+ to estimate them on the training split only (in the consumer's fold). The
7
+ intensities are carried through unchanged.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+ from ..detection.peaklist import PeakSet
15
+ from .strategies import ALIGNMENT_REGISTRY, AlignmentMethod, AlignmentStrategy
16
+
17
+
18
+ def _build_strategy(
19
+ method: AlignmentMethod,
20
+ max_shift_da: float,
21
+ n_segments: int,
22
+ smooth_sigma: float,
23
+ lowess_frac: float,
24
+ lowess_it: int,
25
+ ) -> AlignmentStrategy:
26
+ """Instantiate the requested alignment strategy with its parameters."""
27
+ cls = ALIGNMENT_REGISTRY[method]
28
+ if method in (AlignmentMethod.shift, AlignmentMethod.linear):
29
+ return cls(max_shift=max_shift_da)
30
+ if method == AlignmentMethod.piecewise:
31
+ return cls(
32
+ n_segments=n_segments, smooth_sigma=smooth_sigma, max_shift=max_shift_da
33
+ )
34
+ if method == AlignmentMethod.quadratic:
35
+ return cls(max_shift=max_shift_da, degree=2)
36
+ if method == AlignmentMethod.cubic:
37
+ return cls(max_shift=max_shift_da, degree=3)
38
+ if method == AlignmentMethod.lowess:
39
+ return cls(max_shift=max_shift_da, frac=lowess_frac, it=lowess_it)
40
+ return cls()
41
+
42
+
43
+ def align_peaks(
44
+ peaks: PeakSet,
45
+ ref_peaks_mz: np.ndarray,
46
+ *,
47
+ method: str | AlignmentMethod = "shift",
48
+ max_shift_da: float = 50.0,
49
+ n_segments: int = 5,
50
+ smooth_sigma: float = 2.0,
51
+ lowess_frac: float = 0.3,
52
+ lowess_it: int = 3,
53
+ ) -> PeakSet:
54
+ """Align a peak set's m/z positions to a reference peak list.
55
+
56
+ Fit-free: ``ref_peaks_mz`` is supplied by the caller. Builds the requested
57
+ warping transform from the matched (sample, reference) peak pairs and applies
58
+ it to the peak m/z; intensities are unchanged.
59
+
60
+ Parameters
61
+ ----------
62
+ peaks : PeakSet
63
+ Peak set to align.
64
+ ref_peaks_mz : array-like
65
+ Reference peak m/z positions to align to.
66
+ method : {"shift", "linear", "piecewise", "quadratic", "cubic", "lowess"}, default="shift"
67
+ Warping strategy. ``"dtw"`` is unsupported for peak sets because it
68
+ resamples onto a dense grid and would destroy the set structure.
69
+ max_shift_da : float, default=50.0
70
+ Maximum allowed shift in Daltons (used by the shift fallback and the
71
+ rigid/linear strategies).
72
+ n_segments : int, default=5
73
+ Number of segments for ``method="piecewise"``.
74
+ smooth_sigma : float, default=2.0
75
+ Gaussian smoothing (Da) for piecewise transitions.
76
+ lowess_frac : float, default=0.3
77
+ LOWESS bandwidth for ``method="lowess"``.
78
+ lowess_it : int, default=3
79
+ LOWESS robustness iterations for ``method="lowess"``.
80
+
81
+ Returns
82
+ -------
83
+ PeakSet
84
+ A new peak set with warped m/z and unchanged intensities.
85
+
86
+ Raises
87
+ ------
88
+ ValueError
89
+ If ``method="dtw"``.
90
+
91
+ Notes
92
+ -----
93
+ ``"shift"``, ``"linear"``, ``"quadratic"``, ``"cubic"`` and ``"lowess"``
94
+ operate directly on the matched peak m/z and are the well-behaved choices
95
+ for sparse peak sets. ``"piecewise"`` derives its Gaussian smoothing width
96
+ from the median spacing of the input points; on a sparse peak set that
97
+ spacing is large, so the smoothing is effectively inactive and the warp
98
+ reduces to an unsmoothed piecewise-linear transform.
99
+ """
100
+ method = AlignmentMethod(method)
101
+ if method == AlignmentMethod.dtw:
102
+ raise ValueError(
103
+ "method='dtw' is not supported for peak sets (it resamples onto a "
104
+ "dense grid). Use 'shift', 'linear', 'piecewise', 'quadratic', "
105
+ "'cubic', or 'lowess'."
106
+ )
107
+
108
+ ref_peaks_mz = np.asarray(ref_peaks_mz, dtype=float).ravel()
109
+ if peaks.n_peaks == 0 or ref_peaks_mz.size == 0:
110
+ return PeakSet(peaks.mz.copy(), peaks.intensity.copy())
111
+
112
+ strategy = _build_strategy(
113
+ method, max_shift_da, n_segments, smooth_sigma, lowess_frac, lowess_it
114
+ )
115
+ new_mz, _ = strategy.align_raw(
116
+ peaks.mz,
117
+ peaks.intensity,
118
+ peaks.mz,
119
+ ref_peaks_mz,
120
+ ref_peaks_mz,
121
+ np.ones_like(ref_peaks_mz),
122
+ )
123
+ return PeakSet(np.asarray(new_mz, dtype=float), peaks.intensity.copy())
@@ -0,0 +1,12 @@
1
+ """Peak detection algorithms and transformers."""
2
+
3
+ from .peak_detector import MaldiPeakDetector, PeakMethod
4
+ from .peaklist import PeakList, PeakSet, create_peakset_input
5
+
6
+ __all__ = [
7
+ "MaldiPeakDetector",
8
+ "PeakMethod",
9
+ "PeakList",
10
+ "PeakSet",
11
+ "create_peakset_input",
12
+ ]
@@ -3,14 +3,25 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
6
8
 
7
9
  import gudhi
8
10
  import numpy as np
9
11
  import pandas as pd
10
12
  from joblib import Parallel, delayed
11
- from scipy.signal import find_peaks
13
+ from scipy.signal import find_peaks, peak_prominences
12
14
  from sklearn.base import BaseEstimator, TransformerMixin
13
15
 
16
+ from .peaklist import (
17
+ RANK_BY,
18
+ PeakList,
19
+ PeakSet,
20
+ _array_hash,
21
+ _config_hash,
22
+ _PeakCache,
23
+ )
24
+
14
25
 
15
26
  class PeakMethod(str, Enum):
16
27
  """Supported peak detection methods.
@@ -350,3 +361,262 @@ class MaldiPeakDetector(BaseEstimator, TransformerMixin):
350
361
  )
351
362
 
352
363
  return pd.DataFrame(stats, index=X.index)
364
+
365
+ def _detect_peaks_ph_scored(self, row: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
366
+ """Detect persistent-homology peaks and their persistence values.
367
+
368
+ Like :meth:`_detect_peaks_ph` but also returns the persistence
369
+ (death - birth) of each kept peak, index-ascending.
370
+
371
+ Parameters
372
+ ----------
373
+ row : np.ndarray
374
+ 1D spectrum intensity array.
375
+
376
+ Returns
377
+ -------
378
+ indices : np.ndarray
379
+ Peak indices, ascending.
380
+ persistences : np.ndarray
381
+ Persistence of each peak, aligned with ``indices``.
382
+ """
383
+ row = np.asarray(row, dtype=float)
384
+ if np.allclose(row, row[0]):
385
+ return np.array([], dtype=int), np.array([], dtype=float)
386
+
387
+ signal = -row
388
+ signal = signal - signal.min()
389
+
390
+ cc = gudhi.CubicalComplex(top_dimensional_cells=signal[np.newaxis, :])
391
+ cc.persistence()
392
+ regular_pairs, essential_pairs = cc.cofaces_of_persistence_pairs()
393
+
394
+ regular = regular_pairs[0] if len(regular_pairs) else np.empty((0, 2), int)
395
+ essential = essential_pairs[0] if len(essential_pairs) else np.empty(0, int)
396
+ signal_max = float(np.max(signal))
397
+
398
+ idx_to_pers: dict[int, float] = {}
399
+ if regular.size:
400
+ births = signal[regular[:, 0]]
401
+ deaths = signal[regular[:, 1]]
402
+ persistences = deaths - births
403
+ keep = persistences >= self.persistence_threshold
404
+ for i, pers in zip(
405
+ regular[keep, 0].tolist(), persistences[keep].tolist(), strict=True
406
+ ):
407
+ idx_to_pers[int(i)] = max(idx_to_pers.get(int(i), 0.0), float(pers))
408
+ if essential.size:
409
+ essential_births = signal[essential]
410
+ persistences = signal_max - essential_births
411
+ keep = persistences >= self.persistence_threshold
412
+ for i, pers in zip(
413
+ essential[keep].tolist(), persistences[keep].tolist(), strict=True
414
+ ):
415
+ idx_to_pers[int(i)] = max(idx_to_pers.get(int(i), 0.0), float(pers))
416
+
417
+ if not idx_to_pers:
418
+ return np.array([], dtype=int), np.array([], dtype=float)
419
+ indices = np.array(sorted(idx_to_pers), dtype=int)
420
+ pers = np.array([idx_to_pers[int(i)] for i in indices], dtype=float)
421
+ return indices, pers
422
+
423
+ def _detect_scored(
424
+ self, row: np.ndarray, rank_by: str
425
+ ) -> tuple[np.ndarray, np.ndarray]:
426
+ """Return ``(peak_indices, ranking_scores)`` for one spectrum row.
427
+
428
+ Peak indices are ascending. ``rank_by="persistence"`` requires
429
+ ``method="ph"``; ``"intensity"`` and ``"prominence"`` work for either
430
+ detection method.
431
+ """
432
+ row = np.asarray(row, dtype=float)
433
+ if self.method == "local":
434
+ indices = self._detect_peaks_local(row)
435
+ persistences: np.ndarray | None = None
436
+ elif self.method == "ph":
437
+ indices, persistences = self._detect_peaks_ph_scored(row)
438
+ else:
439
+ raise ValueError(f"Unknown method: {self.method}")
440
+
441
+ indices = np.asarray(indices, dtype=int)
442
+ if indices.size == 0:
443
+ return indices, np.empty(0, dtype=float)
444
+
445
+ if rank_by == "intensity":
446
+ scores = row[indices]
447
+ elif rank_by == "prominence":
448
+ scores = peak_prominences(row, indices)[0]
449
+ elif rank_by == "persistence":
450
+ if persistences is None:
451
+ raise ValueError("rank_by='persistence' requires method='ph'.")
452
+ scores = persistences
453
+ else:
454
+ raise ValueError(f"Unknown rank_by={rank_by!r}; expected one of {RANK_BY}.")
455
+ return indices, np.asarray(scores, dtype=float)
456
+
457
+ def detect_peakset(
458
+ self,
459
+ mz: np.ndarray,
460
+ intensity: np.ndarray,
461
+ top_k: int | None = 200,
462
+ rank_by: str = "intensity",
463
+ ) -> PeakSet:
464
+ """Detect peaks in one spectrum and return a :class:`PeakSet`.
465
+
466
+ Stateless and per-spectrum: the result depends only on this spectrum,
467
+ so it is identical whether computed over a whole dataset or inside a
468
+ single fold.
469
+
470
+ Parameters
471
+ ----------
472
+ mz : array-like
473
+ m/z axis of the spectrum, shape ``(n_points,)``.
474
+ intensity : array-like
475
+ Intensity values aligned with ``mz``.
476
+ top_k : int or None, default=200
477
+ Keep at most this many peaks, ranked by ``rank_by``. ``None`` keeps
478
+ all detected peaks.
479
+ rank_by : {"intensity", "persistence", "prominence"}, default="intensity"
480
+ Ranking used to select the ``top_k`` peaks.
481
+
482
+ Returns
483
+ -------
484
+ PeakSet
485
+ The detected peaks (always carrying their true intensities,
486
+ regardless of the detector's ``binary`` setting).
487
+ """
488
+ mz = np.asarray(mz, dtype=float).ravel()
489
+ intensity = np.asarray(intensity, dtype=float).ravel()
490
+ if mz.shape != intensity.shape:
491
+ raise ValueError(
492
+ "mz and intensity must have the same shape; "
493
+ f"got {mz.shape} and {intensity.shape}."
494
+ )
495
+ indices, scores = self._detect_scored(intensity, rank_by)
496
+ if indices.size == 0:
497
+ return PeakSet(np.empty(0), np.empty(0))
498
+ if top_k is not None and indices.size > top_k:
499
+ keep = np.argsort(scores, kind="stable")[::-1][: int(top_k)]
500
+ indices = indices[keep]
501
+ scores = scores[keep]
502
+ peak_score = None if rank_by == "intensity" else scores
503
+ return PeakSet(mz[indices], intensity[indices], peak_score)
504
+
505
+ def _resolve_mz_index(
506
+ self, X: Any, mz: np.ndarray | None
507
+ ) -> tuple[np.ndarray, pd.Index]:
508
+ """Recover the m/z axis (from ``mz`` or numeric columns) and the index."""
509
+ if isinstance(X, pd.DataFrame):
510
+ index = X.index
511
+ columns: pd.Index | None = X.columns
512
+ else:
513
+ index = pd.RangeIndex(np.asarray(X).shape[0])
514
+ columns = None
515
+
516
+ if mz is not None:
517
+ return np.asarray(mz, dtype=float).ravel(), index
518
+ if columns is not None:
519
+ try:
520
+ return columns.to_numpy(dtype=float), index
521
+ except (ValueError, TypeError):
522
+ raise ValueError(
523
+ "Could not recover an m/z axis from the DataFrame columns "
524
+ f"({list(columns[:3])}...). Pass mz= explicitly (e.g. the bin "
525
+ "centres from maldiamrkit.preprocessing.get_bin_metadata)."
526
+ ) from None
527
+ raise ValueError(
528
+ "X has no column labels to recover m/z from; pass mz= explicitly."
529
+ )
530
+
531
+ def transform_peaklist(
532
+ self,
533
+ X: Any,
534
+ top_k: int | None = 200,
535
+ rank_by: str = "intensity",
536
+ mz: np.ndarray | None = None,
537
+ cache_dir: str | Path | None = None,
538
+ ) -> PeakList:
539
+ """Extract a compact :class:`PeakList` from binned spectra.
540
+
541
+ Per-spectrum and stateless. The m/z axis is taken from ``mz`` if given,
542
+ otherwise recovered from the (numeric) DataFrame column labels.
543
+
544
+ Parameters
545
+ ----------
546
+ X : pd.DataFrame, pd.Series, or ndarray
547
+ Binned spectra of shape ``(n_samples, n_bins)``.
548
+ top_k : int or None, default=200
549
+ Maximum number of peaks kept per spectrum.
550
+ rank_by : {"intensity", "persistence", "prominence"}, default="intensity"
551
+ Ranking used to select the ``top_k`` peaks.
552
+ mz : array-like, optional
553
+ Explicit m/z axis of length ``n_bins``. Overrides column labels.
554
+ cache_dir : str, optional
555
+ If given, cache the resulting :class:`PeakList` keyed by a content +
556
+ config hash and reuse it on subsequent calls.
557
+
558
+ Returns
559
+ -------
560
+ PeakList
561
+ One :class:`PeakSet` per spectrum.
562
+ """
563
+ if rank_by not in RANK_BY:
564
+ raise ValueError(f"Unknown rank_by={rank_by!r}; expected one of {RANK_BY}.")
565
+ if isinstance(X, pd.Series):
566
+ X = X.to_frame().T
567
+
568
+ mz_axis, index = self._resolve_mz_index(X, mz)
569
+ values = X.to_numpy() if hasattr(X, "to_numpy") else np.asarray(X)
570
+ values = np.asarray(values, dtype=float)
571
+ if values.ndim == 1:
572
+ values = values.reshape(1, -1)
573
+ if values.shape[1] != mz_axis.shape[0]:
574
+ raise ValueError(
575
+ f"m/z axis length {mz_axis.shape[0]} does not match the "
576
+ f"{values.shape[1]} feature columns of X."
577
+ )
578
+
579
+ config = {
580
+ "source": "transform_peaklist",
581
+ "top_k": None if top_k is None else int(top_k),
582
+ "rank_by": rank_by,
583
+ "detector": self.get_params(),
584
+ "detector_kwargs": dict(self.kwargs),
585
+ "mz_axis": _array_hash(mz_axis),
586
+ }
587
+ config_hash = _config_hash(config)
588
+ content_hash = _array_hash(values)
589
+
590
+ cache = _PeakCache(cache_dir)
591
+ cached = cache.get(content_hash, config_hash)
592
+ if cached is not None:
593
+ return cached
594
+
595
+ rows = [values[i] for i in range(values.shape[0])]
596
+ if self.n_jobs == 1:
597
+ peaks = [
598
+ self.detect_peakset(mz_axis, row, top_k=top_k, rank_by=rank_by)
599
+ for row in rows
600
+ ]
601
+ else:
602
+ peaks = Parallel(n_jobs=self.n_jobs, prefer="processes")(
603
+ delayed(self.detect_peakset)(mz_axis, row, top_k, rank_by)
604
+ for row in rows
605
+ )
606
+
607
+ peaklist = PeakList(
608
+ peaks,
609
+ index=index,
610
+ meta={
611
+ "method": self.method.value,
612
+ "top_k": None if top_k is None else int(top_k),
613
+ "rank_by": rank_by,
614
+ "mz_range": [float(mz_axis.min()), float(mz_axis.max())],
615
+ "warped": False,
616
+ "content_hash": content_hash,
617
+ "config_hash": config_hash,
618
+ "source": "transform_peaklist",
619
+ },
620
+ )
621
+ cache.put(content_hash, config_hash, peaklist)
622
+ return peaklist