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.
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0/MaldiAMRKit.egg-info}/PKG-INFO +1 -1
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/SOURCES.txt +4 -1
- {maldiamrkit-0.16.0/MaldiAMRKit.egg-info → maldiamrkit-0.17.0}/PKG-INFO +1 -1
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/__init__.py +3 -3
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/__init__.py +2 -0
- maldiamrkit-0.17.0/maldiamrkit/alignment/peak_align.py +123 -0
- maldiamrkit-0.17.0/maldiamrkit/detection/__init__.py +12 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/detection/peak_detector.py +271 -1
- maldiamrkit-0.17.0/maldiamrkit/detection/peaklist.py +469 -0
- maldiamrkit-0.17.0/tests/test_peaklist.py +334 -0
- maldiamrkit-0.16.0/maldiamrkit/detection/__init__.py +0 -5
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/LICENSE +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/dependency_links.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/entry_points.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/requires.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/MaldiAMRKit.egg-info/top_level.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/README.md +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/_compat.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/raw_warping.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/strategies.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/alignment/warping.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/cli.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/README.md +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.1.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.2.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v1.3.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v10.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v11.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v12.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v13.1.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v14.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v15.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v16.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v2.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v4.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v5.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v6.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v7.1.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v8.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v8.1.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/eucast/eucast_v9.0.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/breakpoints/example.yaml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/builder.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/dataset_layouts.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/duplicates.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/input_layouts.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/loader.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/data/site_info.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/dataset.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/analysis.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/differential/stats.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/monitor.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/pca_drift.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/peak_drift.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/drift/reference.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/label_encoder.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/metrics.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/mic_regression.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/evaluation/splitting.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/filters.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/mic.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/io/readers.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/binning.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/merging.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/pipeline.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/preprocessing_pipeline.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/quality.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/preprocessing/transformers.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/py.typed +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/clustering.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/metrics.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/pairwise.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/similarity/plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/spectrum.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/breakpoint.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/label_encoder.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/susceptibility/mic_encoder.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/__init__.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/_common.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/alignment_plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/exploratory_plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/peak_plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/maldiamrkit/visualization/spectrum_plots.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/pyproject.toml +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-batch.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-dev.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements-docs.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/requirements.txt +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/setup.cfg +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/tests/test_compat.py +0 -0
- {maldiamrkit-0.16.0 → maldiamrkit-0.17.0}/tests/test_numerical_edge_cases.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|