diff --git a/pyproject.toml b/pyproject.toml index ae89133..24276c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,8 @@ testing = [ "coverage>=7.4.3", "hypothesis>=6.99.11", "mypy>=1.9.0", - "pytype>=2023.12.18" + "pytype>=2023.12.18", + "logot" ] dev = [ "bokeh>=3.3.2", diff --git a/src/depiction/spectrum/peak_picking/basic_interpolated_peak_picker.py b/src/depiction/spectrum/peak_picking/basic_interpolated_peak_picker.py index c961ef5..f904325 100644 --- a/src/depiction/spectrum/peak_picking/basic_interpolated_peak_picker.py +++ b/src/depiction/spectrum/peak_picking/basic_interpolated_peak_picker.py @@ -67,7 +67,7 @@ def _interpolate_max_mz_and_intensity( # Fit a cubic spline spline = scipy.interpolate.CubicSpline(interp_mz, interp_int) roots = spline.derivative().roots() - if len(roots) == 0: + if np.isnan(roots).any(): loguru.logger.warning( f"Error: {len(roots)} roots found for local maximum at index {local_max_index}; " f"{interp_mz=}, {interp_int=}, {roots=}" @@ -89,3 +89,7 @@ def _find_local_maxima_indices(self, mz_arr: NDArray[float], int_arr: NDArray[fl ), ) return local_maxima_indices + + +# TODO the interpolation could be much faster, if it were implemented in numba for our specific case of 3 points, +# since in general the scipy library will do everything much moe general than is actually required. diff --git a/tests/unit/spectrum/peak_picking/test_basic_interpolated_peak_picker.py b/tests/unit/spectrum/peak_picking/test_basic_interpolated_peak_picker.py index 96f880f..509cad2 100644 --- a/tests/unit/spectrum/peak_picking/test_basic_interpolated_peak_picker.py +++ b/tests/unit/spectrum/peak_picking/test_basic_interpolated_peak_picker.py @@ -3,6 +3,8 @@ from unittest.mock import patch, MagicMock import numpy as np +from logot import Logot, logged +from logot.loguru import LoguruCapturer from depiction.spectrum.peak_picking import BasicPeakPicker, BasicInterpolatedPeakPicker @@ -23,11 +25,46 @@ def basic_interpolated_peak_picker(self) -> BasicInterpolatedPeakPicker: peak_filtering=self.mock_peak_filtering, ) - def test_interpolate_max_mz_and_intensity_when_success(self) -> None: - pass + def test_interpolate_max_mz_and_intensity_when_success_and_exact(self) -> None: + """In this very simple case, interpolation should return the same value as the intensity on both sides is + symmetric.""" + local_max_index = 2 + mz_arr = np.array([1., 2, 3, 4, 5]) + int_arr = np.array([0., 0, 10, 0, 0]) + + interpolated_mz, interpolated_int = self.basic_interpolated_peak_picker._interpolate_max_mz_and_intensity( + local_max_index, mz_arr, int_arr + ) + + self.assertAlmostEqual(3, interpolated_mz) + self.assertAlmostEqual(10, interpolated_int) + + def test_interpolate_max_mz_and_intensity_when_success_and_not_exact(self) -> None: + local_max_index = 2 + mz_arr = np.array([1., 2, 3, 4, 5]) + int_arr = np.array([0., 0, 10, 5, 0]) + + interpolated_mz, interpolated_int = self.basic_interpolated_peak_picker._interpolate_max_mz_and_intensity( + local_max_index, mz_arr, int_arr + ) + + self.assertAlmostEqual(3.16667, interpolated_mz, places=5) + self.assertAlmostEqual(10.20833, interpolated_int, places=5) def test_interpolate_max_mz_and_intensity_when_failure(self) -> None: - pass + local_max_index = 2 + mz_arr = np.array([1., 2, 3, 4, 5]) + int_arr = np.array([0., 10, 10, 10, 0]) + + with Logot().capturing(capturer=LoguruCapturer) as logot: + interpolated_mz, interpolated_int = self.basic_interpolated_peak_picker._interpolate_max_mz_and_intensity( + local_max_index, mz_arr, int_arr + ) + self.assertIsNone(interpolated_mz) + self.assertIsNone(interpolated_int) + logot.assert_logged(logged.warning("Error: %d roots found for local maximum at index 2; %s")) + + # TODO test interpolate when there are 2 roots because of numerical issues, 3 is apparently also possible @patch.object(BasicPeakPicker, "get_min_distance_indices") def test_find_local_maxima_indices(self, mock_get_min_distance_indices) -> None: