thyra 2.0.4__tar.gz → 2.2.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 (97) hide show
  1. {thyra-2.0.4 → thyra-2.2.0}/PKG-INFO +1 -1
  2. {thyra-2.0.4 → thyra-2.2.0}/pyproject.toml +1 -1
  3. {thyra-2.0.4 → thyra-2.2.0}/thyra/__init__.py +1 -1
  4. {thyra-2.0.4 → thyra-2.2.0}/thyra/__main__.py +17 -0
  5. {thyra-2.0.4 → thyra-2.2.0}/thyra/convert.py +7 -4
  6. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/base_spatialdata_converter.py +23 -0
  7. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/imzml/imzml_reader.py +19 -3
  8. thyra-2.2.0/thyra/resampling/gaps.py +76 -0
  9. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/strategies/tic_preserving.py +23 -1
  10. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/types.py +8 -0
  11. {thyra-2.0.4 → thyra-2.2.0}/LICENSE +0 -0
  12. {thyra-2.0.4 → thyra-2.2.0}/README.md +0 -0
  13. {thyra-2.0.4 → thyra-2.2.0}/thyra/alignment/__init__.py +0 -0
  14. {thyra-2.0.4 → thyra-2.2.0}/thyra/alignment/affine.py +0 -0
  15. {thyra-2.0.4 → thyra-2.2.0}/thyra/alignment/teaching_points.py +0 -0
  16. {thyra-2.0.4 → thyra-2.2.0}/thyra/config.py +0 -0
  17. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/__init__.py +0 -0
  18. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/__init__.py +0 -0
  19. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/_chunking.py +0 -0
  20. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/converter.py +0 -0
  21. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/spatialdata_2d_converter.py +0 -0
  22. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/spatialdata_3d_converter.py +0 -0
  23. {thyra-2.0.4 → thyra-2.2.0}/thyra/converters/spatialdata/streaming_converter.py +0 -0
  24. {thyra-2.0.4 → thyra-2.2.0}/thyra/core/__init__.py +0 -0
  25. {thyra-2.0.4 → thyra-2.2.0}/thyra/core/base_converter.py +0 -0
  26. {thyra-2.0.4 → thyra-2.2.0}/thyra/core/base_extractor.py +0 -0
  27. {thyra-2.0.4 → thyra-2.2.0}/thyra/core/base_reader.py +0 -0
  28. {thyra-2.0.4 → thyra-2.2.0}/thyra/core/registry.py +0 -0
  29. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/__init__.py +0 -0
  30. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/extractors/__init__.py +0 -0
  31. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/extractors/bruker_extractor.py +0 -0
  32. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/extractors/imzml_extractor.py +0 -0
  33. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/extractors/waters_extractor.py +0 -0
  34. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/ontology/__init__.py +0 -0
  35. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/ontology/_ims.py +0 -0
  36. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/ontology/_ms.py +0 -0
  37. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/ontology/_uo.py +0 -0
  38. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/ontology/cache.py +0 -0
  39. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/types.py +0 -0
  40. {thyra-2.0.4 → thyra-2.2.0}/thyra/metadata/validator.py +0 -0
  41. {thyra-2.0.4 → thyra-2.2.0}/thyra/preview.py +0 -0
  42. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/__init__.py +0 -0
  43. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/__init__.py +0 -0
  44. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/base_bruker_reader.py +0 -0
  45. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/folder_structure.py +0 -0
  46. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/mis_parser.py +0 -0
  47. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
  48. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
  49. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/__init__.py +0 -0
  50. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
  51. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
  52. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
  53. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
  54. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
  55. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
  56. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
  57. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
  58. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
  59. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
  60. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
  61. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
  62. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
  63. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
  64. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/imzml/__init__.py +0 -0
  65. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/__init__.py +0 -0
  66. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/imaging_grid.py +0 -0
  67. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/lib/MLReader.dll +0 -0
  68. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
  69. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/lib/libMLReader.so +0 -0
  70. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
  71. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/masslynx_lib.py +0 -0
  72. {thyra-2.0.4 → thyra-2.2.0}/thyra/readers/waters/waters_reader.py +0 -0
  73. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/__init__.py +0 -0
  74. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/common_axis.py +0 -0
  75. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/constants.py +0 -0
  76. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/data_characteristics.py +0 -0
  77. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/decision_tree.py +0 -0
  78. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/instrument_detectors.py +0 -0
  79. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/__init__.py +0 -0
  80. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/base_generator.py +0 -0
  81. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
  82. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/linear_generator.py +0 -0
  83. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
  84. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
  85. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
  86. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/strategies/__init__.py +0 -0
  87. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/strategies/base.py +0 -0
  88. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
  89. {thyra-2.0.4 → thyra-2.2.0}/thyra/resampling/tic.py +0 -0
  90. {thyra-2.0.4 → thyra-2.2.0}/thyra/tools/__init__.py +0 -0
  91. {thyra-2.0.4 → thyra-2.2.0}/thyra/tools/check_ontology.py +0 -0
  92. {thyra-2.0.4 → thyra-2.2.0}/thyra/tools/make_example_data.py +0 -0
  93. {thyra-2.0.4 → thyra-2.2.0}/thyra/utils/__init__.py +0 -0
  94. {thyra-2.0.4 → thyra-2.2.0}/thyra/utils/bruker_exceptions.py +0 -0
  95. {thyra-2.0.4 → thyra-2.2.0}/thyra/utils/logging_config.py +0 -0
  96. {thyra-2.0.4 → thyra-2.2.0}/thyra/utils/windows_paths.py +0 -0
  97. {thyra-2.0.4 → thyra-2.2.0}/thyra/utils/zarr_atomic_write.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thyra
3
- Version: 2.0.4
3
+ Version: 2.2.0
4
4
  Summary: A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "thyra"
7
- version = "2.0.4"
7
+ version = "2.2.0"
8
8
  description = "A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics"
9
9
  authors = ["Theodoros Visvikis <t.visvikis@maastrichtuniversity.nl>"]
10
10
  maintainers = ["Theodoros Visvikis <t.visvikis@maastrichtuniversity.nl>"]
@@ -36,7 +36,7 @@ warnings.filterwarnings(
36
36
  category=FutureWarning,
37
37
  )
38
38
 
39
- __version__ = "2.0.4"
39
+ __version__ = "2.2.0"
40
40
 
41
41
  # Import key components - avoid wildcard imports
42
42
  try:
@@ -243,6 +243,7 @@ def _build_resampling_config(
243
243
  resample_max_mz: Optional[float],
244
244
  resample_width_at_mz: Optional[float],
245
245
  resample_reference_mz: float,
246
+ resample_gap_tolerance: Optional[float] = None,
246
247
  ) -> dict:
247
248
  """Build resampling configuration dictionary."""
248
249
  return {
@@ -253,6 +254,7 @@ def _build_resampling_config(
253
254
  "max_mz": resample_max_mz,
254
255
  "width_at_mz": resample_width_at_mz,
255
256
  "reference_mz": resample_reference_mz,
257
+ "gap_tolerance_da": resample_gap_tolerance,
256
258
  }
257
259
 
258
260
 
@@ -536,6 +538,16 @@ class GroupedCommand(click.Command):
536
538
  default=1000.0,
537
539
  help="Reference m/z for width specification (default: 1000.0)",
538
540
  )
541
+ @click.option(
542
+ "--resample-gap-tolerance",
543
+ type=float,
544
+ default=None,
545
+ help=(
546
+ "For tic_preserving only: discard target bins farther than this many "
547
+ "Da from any measured m/z, instead of interpolating across the gap "
548
+ "(default: no limit)"
549
+ ),
550
+ )
539
551
  # -- Bruker-specific --
540
552
  @click.option(
541
553
  "--use-recalibrated/--no-recalibrated",
@@ -583,6 +595,7 @@ def main(
583
595
  resample_max_mz: Optional[float],
584
596
  resample_width_at_mz: Optional[float],
585
597
  resample_reference_mz: float,
598
+ resample_gap_tolerance: Optional[float],
586
599
  mass_axis_type: str,
587
600
  sparse_format: str,
588
601
  include_optical: bool,
@@ -604,6 +617,9 @@ def main(
604
617
  resample_width_at_mz,
605
618
  resample_reference_mz,
606
619
  )
620
+ _validate_positive_float(
621
+ resample_gap_tolerance, "resample_gap_tolerance", "Gap tolerance"
622
+ )
607
623
  _validate_positive_float(
608
624
  intensity_threshold, "intensity_threshold", "Intensity threshold"
609
625
  )
@@ -641,6 +657,7 @@ def main(
641
657
  resample_max_mz,
642
658
  resample_width_at_mz,
643
659
  resample_reference_mz,
660
+ resample_gap_tolerance,
644
661
  )
645
662
  if resample
646
663
  else None
@@ -283,10 +283,13 @@ def convert_msi(
283
283
  use active/recalibrated calibration (default True).
284
284
  - max_mass_axis_length: int - For processed-mode imzML
285
285
  converted with --no-resample, give up once the raw
286
- mass axis exceeds this many unique m/z values.
287
- Default: None (unlimited). Useful when the peak
288
- lists share no m/z values, where the raw axis grows
289
- to roughly one column per peak in the whole dataset.
286
+ mass axis exceeds this many unique m/z values. This
287
+ matters when the peak lists share no m/z values,
288
+ where the raw axis grows to roughly one column per
289
+ peak in the whole dataset. Default: 10,000,000,
290
+ which is the limit SCiLS Lab places on the same
291
+ quantity (2026b User Guide, p.76). Pass None for
292
+ no limit.
290
293
  sparse_format: Sparse matrix format ('csc' or 'csr')
291
294
  include_optical: Include optical images (default: True)
292
295
  apply_optical_alignment: If True (default) and the MSI source
@@ -17,6 +17,7 @@ from ...core.base_converter import BaseMSIConverter, PixelSizeSource
17
17
  from ...core.base_reader import BaseMSIReader
18
18
  from ...metadata.types import ComprehensiveMetadata, EssentialMetadata
19
19
  from ...resampling import ResamplingDecisionTree, ResamplingMethod
20
+ from ...resampling.gaps import zero_across_gaps
20
21
  from ...resampling.tic import preserved_tic, rescale_to_preserved_tic
21
22
  from ...resampling.types import ResamplingConfig
22
23
  from ...utils.zarr_atomic_write import install_windows_atomic_write_retry
@@ -137,6 +138,7 @@ def _normalize_resampling_config(
137
138
  ),
138
139
  min_mz=config.get("min_mz"),
139
140
  max_mz=config.get("max_mz"),
141
+ gap_tolerance_da=config.get("gap_tolerance_da"),
140
142
  )
141
143
 
142
144
 
@@ -378,6 +380,12 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
378
380
  self._max_mz = config.max_mz
379
381
  self._width_at_mz = config.mass_width_da
380
382
  self._reference_mz = config.reference_mz
383
+ self._gap_tolerance_da = config.gap_tolerance_da
384
+ if self._gap_tolerance_da is not None:
385
+ logger.info(
386
+ f"Interpolation gap tolerance: {self._gap_tolerance_da} Da "
387
+ "(target bins farther than this from any source m/z are zeroed)"
388
+ )
381
389
 
382
390
  def _get_cached_metadata_for_resampling(self) -> Dict[str, Any]:
383
391
  """Get cached metadata for resampling decision tree to avoid multiple reader calls."""
@@ -1116,6 +1124,11 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1116
1124
  When the axis spans the spectrum, which is the default, that share
1117
1125
  is the whole input TIC.
1118
1126
 
1127
+ When ``ResamplingConfig.gap_tolerance_da`` is set, bins farther than
1128
+ that from any source m/z are zeroed before the rescale, so
1129
+ interpolation cannot claim regions nothing was measured in. See
1130
+ ``thyra.resampling.gaps``.
1131
+
1119
1132
  Args:
1120
1133
  mzs: Original m/z values from the spectrum.
1121
1134
  intensities: Corresponding intensity values.
@@ -1163,6 +1176,16 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1163
1176
  right=0,
1164
1177
  )
1165
1178
 
1179
+ # Discard bins no source point vouches for, before the rescale so the
1180
+ # intensity returns to the bins that were measured rather than being
1181
+ # deleted. No-op when no tolerance was configured.
1182
+ zero_across_gaps(
1183
+ resampled,
1184
+ axis,
1185
+ mzs_sorted,
1186
+ getattr(self, "_gap_tolerance_da", None),
1187
+ )
1188
+
1166
1189
  # Rescale to the required TIC. This is the step that makes the
1167
1190
  # method live up to its name.
1168
1191
  return rescale_to_preserved_tic(resampled, axis, mzs_sorted, intensities_sorted)
@@ -27,6 +27,16 @@ _MASS_AXIS_BATCH_VALUES = 1 << 20
27
27
  # Bounds that function's temporaries to O(block) rather than O(batch).
28
28
  _MASS_AXIS_PROBE_BLOCK = 1 << 20
29
29
 
30
+ # Default cap on the number of unique m/z values the processed-mode raw mass
31
+ # axis may reach, in ELEMENTS. Taken from SCiLS Lab, which applies the same
32
+ # limit to the same quantity: "Data sets in SCiLS Lab are limited to a maximum
33
+ # of 10 million bins on the common mass axis" (SCiLS Lab 2026b User Guide,
34
+ # p.76). The cap previously defaulted to None because no defensible number was
35
+ # available; this is one, from the tool Thyra's resampling was designed
36
+ # against. Override with ``reader_options={"max_mass_axis_length": N}``, or
37
+ # pass None for the old unlimited behaviour.
38
+ DEFAULT_MAX_MASS_AXIS_LENGTH = 10_000_000
39
+
30
40
 
31
41
  def _dedupe_sorted(a: NDArray[Any]) -> NDArray[Any]:
32
42
  """Deduplicate an ALREADY-SORTED array, as ``np.unique`` would.
@@ -337,14 +347,20 @@ class ImzMLReader(BaseMSIReader):
337
347
  cache_coordinates: Whether to cache coordinates upfront
338
348
  **kwargs: Additional arguments. ``max_mass_axis_length`` caps the
339
349
  number of unique m/z values the processed-mode raw axis may
340
- reach before the build gives up; None (the default) is
341
- unlimited. See ``_extract_continuous_mass_axis``.
350
+ reach before the build gives up. Defaults to
351
+ ``DEFAULT_MAX_MASS_AXIS_LENGTH`` (10 million, SCiLS Lab's own
352
+ limit); pass ``None`` explicitly for unlimited. See
353
+ ``_extract_continuous_mass_axis``.
342
354
  """
343
355
  super().__init__(data_path, **kwargs)
344
356
  self.filepath: Optional[Union[str, Path]] = data_path
345
357
  self.batch_size: int = batch_size
346
358
  self.cache_coordinates: bool = cache_coordinates
347
- self.max_mass_axis_length: Optional[int] = kwargs.get("max_mass_axis_length")
359
+ # Absent means "use the default"; an explicit None means "unlimited",
360
+ # so this cannot be a bare ``.get(...)`` with a None fallback.
361
+ self.max_mass_axis_length: Optional[int] = kwargs.get(
362
+ "max_mass_axis_length", DEFAULT_MAX_MASS_AXIS_LENGTH
363
+ )
348
364
  self.parser: Optional[ImzMLParser] = None
349
365
  self.ibd_file: Optional[Any] = None
350
366
  self.imzml_path: Optional[Path] = None
@@ -0,0 +1,76 @@
1
+ """Refusing to interpolate across regions that were never measured.
2
+
3
+ ``np.interp`` has no notion of a gap. Handed a spectrum whose m/z values are
4
+ sparse or thresholded, it draws a straight line from the last point before the
5
+ gap to the first point after it, and every target bin in between comes back
6
+ with a fabricated intensity. Nothing was measured there.
7
+
8
+ On real data this is not a rounding-scale effect. Forcing ``tic_preserving``
9
+ onto ``bellini.imzML`` -- 2,222 points per spectrum, median source spacing
10
+ 0.0072 Da, largest gap 1,269 Da -- puts 80.6% of the output total ion current
11
+ into m/z regions more than 0.5 Da from any measured point.
12
+
13
+ The fix is the parameter Cardinal (``tolerance``) and matter
14
+ (``approx1(..., tol=)``) both expose: how far a target m/z may sit from the
15
+ nearest source m/z before its interpolated value is discarded rather than
16
+ trusted.
17
+
18
+ Order matters
19
+ -------------
20
+
21
+ Masking happens **before** the TIC rescale, not after. The intensity was
22
+ measured; it belongs somewhere. Interpolation had spread it across a gap where
23
+ nothing was recorded, so returning it to the bins that do have measurements
24
+ behind them keeps ``ResamplingMethod.TIC_PRESERVING``'s promise intact. Masking
25
+ after the rescale would instead delete that intensity and quietly break the
26
+ method's defining property.
27
+ """
28
+
29
+ from typing import Any, Optional
30
+
31
+ import numpy as np
32
+ import numpy.typing as npt
33
+
34
+
35
+ def zero_across_gaps(
36
+ resampled: npt.NDArray[Any],
37
+ axis: npt.NDArray[np.floating[Any]],
38
+ mzs: npt.NDArray[np.floating[Any]],
39
+ tolerance: Optional[float],
40
+ ) -> npt.NDArray[Any]:
41
+ """Zero the bins of ``resampled`` that no source point vouches for.
42
+
43
+ Modifies ``resampled`` in place and returns it.
44
+
45
+ A bin survives when some source m/z lies within ``tolerance`` of it. On a
46
+ source grid of uniform step ``s`` the farthest any target point can be
47
+ from a source point is ``s / 2``, so any tolerance of at least half the
48
+ source spacing leaves such a spectrum untouched -- which is what makes it
49
+ safe to switch on for continuous profile data. Half the *widest* source
50
+ gap is the rule to size it by; exactly half is a floating-point knife
51
+ edge, so give it a hair of margin.
52
+
53
+ Args:
54
+ resampled: Interpolated intensities on ``axis``, modified in place.
55
+ axis: The target mass axis, ascending.
56
+ mzs: Source m/z values, ascending.
57
+ tolerance: Maximum distance in Da from a target bin to the nearest
58
+ source m/z. ``None`` disables the check and returns ``resampled``
59
+ untouched, as does a non-positive tolerance.
60
+
61
+ Returns:
62
+ ``resampled``, with unsupported bins set to zero.
63
+ """
64
+ if tolerance is None or tolerance <= 0.0 or mzs.size == 0:
65
+ return resampled
66
+
67
+ # Distance from each target bin to the nearest source m/z. searchsorted
68
+ # gives the insertion point; the nearest source point is on one side or
69
+ # the other of it.
70
+ right = np.searchsorted(mzs, axis)
71
+ lo = np.clip(right - 1, 0, mzs.size - 1)
72
+ hi = np.clip(right, 0, mzs.size - 1)
73
+ nearest = np.minimum(np.abs(axis - mzs[lo]), np.abs(axis - mzs[hi]))
74
+
75
+ resampled[nearest > tolerance] = 0.0
76
+ return resampled
@@ -13,11 +13,12 @@ cropped intensity rather than redistributing it over the bins that remain.
13
13
  When the axis spans the spectrum, the whole TIC is preserved exactly.
14
14
  """
15
15
 
16
- from typing import Any
16
+ from typing import Any, Optional
17
17
 
18
18
  import numpy as np
19
19
  import numpy.typing as npt
20
20
 
21
+ from ..gaps import zero_across_gaps
21
22
  from ..tic import preserved_tic, rescale_to_preserved_tic
22
23
  from .base import ResamplingStrategy, Spectrum
23
24
 
@@ -25,6 +26,18 @@ from .base import ResamplingStrategy, Spectrum
25
26
  class TICPreservingStrategy(ResamplingStrategy):
26
27
  """TIC-preserving linear interpolation strategy for profile data."""
27
28
 
29
+ def __init__(self, gap_tolerance_da: Optional[float] = None) -> None:
30
+ """Initialize the strategy.
31
+
32
+ Args:
33
+ gap_tolerance_da: How far, in Daltons, a target bin may sit from
34
+ the nearest source m/z before its interpolated value is
35
+ discarded. ``None`` (the default) keeps ``np.interp``'s own
36
+ behaviour of drawing straight lines across unmeasured
37
+ regions. See :mod:`thyra.resampling.gaps`.
38
+ """
39
+ self.gap_tolerance_da = gap_tolerance_da
40
+
28
41
  def resample(
29
42
  self, spectrum: Spectrum, target_axis: npt.NDArray[np.floating[Any]]
30
43
  ) -> Spectrum:
@@ -109,6 +122,15 @@ class TICPreservingStrategy(ResamplingStrategy):
109
122
  # Ensure no negative values (can happen with extrapolation)
110
123
  interpolated_intensity = np.maximum(interpolated_intensity, 0.0)
111
124
 
125
+ # Discard bins no source point vouches for, before the rescale so the
126
+ # intensity returns to the measured bins rather than being deleted.
127
+ zero_across_gaps(
128
+ interpolated_intensity,
129
+ np.asarray(target_axis, dtype=np.float64),
130
+ mzs_sorted,
131
+ self.gap_tolerance_da,
132
+ )
133
+
112
134
  # Scale onto the TIC the target axis is entitled to carry. Shared
113
135
  # with the converters' hot path so the two cannot drift apart again.
114
136
  rescale_to_preserved_tic(
@@ -109,6 +109,13 @@ class ResamplingConfig:
109
109
  default.
110
110
  min_mz: Override the lower bound of the mass range.
111
111
  max_mz: Override the upper bound of the mass range.
112
+ gap_tolerance_da: How far, in Daltons, a target bin may sit from the
113
+ nearest source m/z before ``tic_preserving`` discards its
114
+ interpolated value instead of trusting it. ``None`` -- the
115
+ default -- means no check, which is how ``np.interp`` behaves:
116
+ straight lines are drawn across regions where nothing was
117
+ measured. Only affects ``tic_preserving``; ``nearest_neighbor``
118
+ never invents a bin. See :mod:`thyra.resampling.gaps`.
112
119
  """
113
120
 
114
121
  method: Optional[ResamplingMethod] = None
@@ -118,3 +125,4 @@ class ResamplingConfig:
118
125
  reference_mz: float = DEFAULT_REFERENCE_MZ
119
126
  min_mz: Optional[float] = None
120
127
  max_mz: Optional[float] = None
128
+ gap_tolerance_da: Optional[float] = None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes