thyra 2.0.3__tar.gz → 2.1.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.
- {thyra-2.0.3 → thyra-2.1.0}/PKG-INFO +1 -1
- {thyra-2.0.3 → thyra-2.1.0}/pyproject.toml +1 -1
- {thyra-2.0.3 → thyra-2.1.0}/thyra/__init__.py +1 -1
- {thyra-2.0.3 → thyra-2.1.0}/thyra/__main__.py +17 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/convert.py +7 -4
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/base_spatialdata_converter.py +30 -2
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/imzml/imzml_reader.py +19 -3
- thyra-2.1.0/thyra/resampling/gaps.py +76 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/strategies/nearest_neighbor.py +21 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/strategies/tic_preserving.py +23 -1
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/types.py +20 -3
- {thyra-2.0.3 → thyra-2.1.0}/LICENSE +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/README.md +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/alignment/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/alignment/affine.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/alignment/teaching_points.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/config.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/spatialdata_2d_converter.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/spatialdata_3d_converter.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/core/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/core/base_converter.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/core/base_extractor.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/core/base_reader.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/core/registry.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/extractors/imzml_extractor.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/types.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/metadata/validator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/preview.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/common_axis.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/constants.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/decision_tree.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/strategies/base.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/resampling/tic.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/tools/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/tools/check_ontology.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/tools/make_example_data.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/utils/__init__.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/utils/logging_config.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/utils/windows_paths.py +0 -0
- {thyra-2.0.3 → thyra-2.1.0}/thyra/utils/zarr_atomic_write.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "2.0
|
|
7
|
+
version = "2.1.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>"]
|
|
@@ -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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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."""
|
|
@@ -1082,8 +1090,13 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
1082
1090
|
# bincount without minlength is fastest
|
|
1083
1091
|
accumulated = np.bincount(indices_clipped, weights=intensities)
|
|
1084
1092
|
|
|
1085
|
-
# Extract non-zero bins for sparse representation
|
|
1086
|
-
|
|
1093
|
+
# Extract non-zero bins for sparse representation. The test is
|
|
1094
|
+
# ``!= 0`` rather than ``> 0`` because a bin whose accumulated value is
|
|
1095
|
+
# negative is still a measurement: dropping it silently raises the
|
|
1096
|
+
# stored TIC above the input's. Baseline-subtracted data can carry
|
|
1097
|
+
# negative intensities, though every export seen so far filters them
|
|
1098
|
+
# out upstream.
|
|
1099
|
+
nonzero_mask = accumulated != 0
|
|
1087
1100
|
nonzero_indices = np.where(nonzero_mask)[0].astype(np.int_)
|
|
1088
1101
|
nonzero_values = accumulated[nonzero_mask]
|
|
1089
1102
|
|
|
@@ -1111,6 +1124,11 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
1111
1124
|
When the axis spans the spectrum, which is the default, that share
|
|
1112
1125
|
is the whole input TIC.
|
|
1113
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
|
+
|
|
1114
1132
|
Args:
|
|
1115
1133
|
mzs: Original m/z values from the spectrum.
|
|
1116
1134
|
intensities: Corresponding intensity values.
|
|
@@ -1158,6 +1176,16 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
|
|
|
1158
1176
|
right=0,
|
|
1159
1177
|
)
|
|
1160
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
|
+
|
|
1161
1189
|
# Rescale to the required TIC. This is the step that makes the
|
|
1162
1190
|
# method live up to its name.
|
|
1163
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
|
|
341
|
-
|
|
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
|
-
|
|
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
|
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
This strategy is optimal for centroid data (e.g., from timsTOF
|
|
4
4
|
instruments) where each peak represents a discrete mass value.
|
|
5
|
+
|
|
6
|
+
Not to be confused with the converters' ``_nearest_neighbor_resample``
|
|
7
|
+
(``thyra/converters/spatialdata/base_spatialdata_converter.py``), which is
|
|
8
|
+
what ``ResamplingMethod.NEAREST_NEIGHBOR`` actually selects during a
|
|
9
|
+
conversion. The two share a name and do opposite things with intensity:
|
|
10
|
+
|
|
11
|
+
- This class INTERPOLATES. Each target point takes the intensity of the
|
|
12
|
+
nearest source point, so one source peak is replicated across every
|
|
13
|
+
target point closer to it than to any other. Summed intensity therefore
|
|
14
|
+
scales with how densely the target axis is laid out, and is not
|
|
15
|
+
preserved. That is the defining behaviour of nearest-neighbour
|
|
16
|
+
interpolation, not a defect -- but it means this class is the wrong tool
|
|
17
|
+
if you care about total ion current.
|
|
18
|
+
- The converter's version BINS. Each source peak is accumulated into
|
|
19
|
+
exactly one target bin, so summed intensity is preserved exactly.
|
|
20
|
+
|
|
21
|
+
If you want the conversion pipeline's behaviour, use the converter.
|
|
5
22
|
"""
|
|
6
23
|
|
|
7
24
|
from typing import Any
|
|
@@ -24,6 +41,10 @@ class NearestNeighborStrategy(ResamplingStrategy):
|
|
|
24
41
|
and assigns its intensity. This preserves the discrete nature
|
|
25
42
|
of centroid data.
|
|
26
43
|
|
|
44
|
+
Does NOT preserve total ion current -- a source peak is repeated
|
|
45
|
+
at every target point nearest to it, so the sum grows with the
|
|
46
|
+
target axis density. See the module docstring.
|
|
47
|
+
|
|
27
48
|
Parameters
|
|
28
49
|
----------
|
|
29
50
|
spectrum : Spectrum
|
|
@@ -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(
|
|
@@ -70,7 +70,13 @@ class MassAxis:
|
|
|
70
70
|
return np.diff(self.mz_values)
|
|
71
71
|
|
|
72
72
|
def resolution_at(self, mz: float) -> float:
|
|
73
|
-
"""
|
|
73
|
+
"""Return ``mz`` divided by the local bin width.
|
|
74
|
+
|
|
75
|
+
This is a property of the AXIS, not of the data on it. A finely
|
|
76
|
+
spaced axis reports a large number here whether or not the spectra
|
|
77
|
+
stored on it resolve anything at that scale, so it must not be
|
|
78
|
+
reported to users as the acquisition's resolving power.
|
|
79
|
+
"""
|
|
74
80
|
idx = int(np.searchsorted(self.mz_values, mz))
|
|
75
81
|
if idx > 0 and idx < len(self.mz_values):
|
|
76
82
|
delta_mz = float(self.mz_values[idx] - self.mz_values[idx - 1])
|
|
@@ -91,8 +97,11 @@ class ResamplingConfig:
|
|
|
91
97
|
axis_type: Mass axis spacing model. ``None`` auto-detects from
|
|
92
98
|
the instrument metadata.
|
|
93
99
|
target_bins: Number of bins in the resampled axis. ``None``
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
derives the count from ``mass_width_da`` at ``reference_mz``
|
|
101
|
+
across the mass range. Note that this is a fixed target
|
|
102
|
+
width, not a measurement: the source spectra's own point
|
|
103
|
+
spacing is never consulted, so the resulting axis can be
|
|
104
|
+
finer or coarser than the data it will hold.
|
|
96
105
|
mass_width_da: Bin width in Daltons at ``reference_mz``.
|
|
97
106
|
Alternative to ``target_bins`` -- specify one or the other.
|
|
98
107
|
reference_mz: Reference m/z for ``mass_width_da``. Default
|
|
@@ -100,6 +109,13 @@ class ResamplingConfig:
|
|
|
100
109
|
default.
|
|
101
110
|
min_mz: Override the lower bound of the mass range.
|
|
102
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`.
|
|
103
119
|
"""
|
|
104
120
|
|
|
105
121
|
method: Optional[ResamplingMethod] = None
|
|
@@ -109,3 +125,4 @@ class ResamplingConfig:
|
|
|
109
125
|
reference_mz: float = DEFAULT_REFERENCE_MZ
|
|
110
126
|
min_mz: Optional[float] = None
|
|
111
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|