thyra 2.2.0__tar.gz → 2.2.2__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.2.0 → thyra-2.2.2}/PKG-INFO +1 -1
  2. {thyra-2.2.0 → thyra-2.2.2}/pyproject.toml +1 -1
  3. {thyra-2.2.0 → thyra-2.2.2}/thyra/__init__.py +1 -1
  4. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/imzml/imzml_reader.py +18 -1
  5. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/data_characteristics.py +14 -1
  6. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/instrument_detectors.py +102 -11
  7. {thyra-2.2.0 → thyra-2.2.2}/LICENSE +0 -0
  8. {thyra-2.2.0 → thyra-2.2.2}/README.md +0 -0
  9. {thyra-2.2.0 → thyra-2.2.2}/thyra/__main__.py +0 -0
  10. {thyra-2.2.0 → thyra-2.2.2}/thyra/alignment/__init__.py +0 -0
  11. {thyra-2.2.0 → thyra-2.2.2}/thyra/alignment/affine.py +0 -0
  12. {thyra-2.2.0 → thyra-2.2.2}/thyra/alignment/teaching_points.py +0 -0
  13. {thyra-2.2.0 → thyra-2.2.2}/thyra/config.py +0 -0
  14. {thyra-2.2.0 → thyra-2.2.2}/thyra/convert.py +0 -0
  15. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/__init__.py +0 -0
  16. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/__init__.py +0 -0
  17. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/_chunking.py +0 -0
  18. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
  19. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/converter.py +0 -0
  20. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/spatialdata_2d_converter.py +0 -0
  21. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/spatialdata_3d_converter.py +0 -0
  22. {thyra-2.2.0 → thyra-2.2.2}/thyra/converters/spatialdata/streaming_converter.py +0 -0
  23. {thyra-2.2.0 → thyra-2.2.2}/thyra/core/__init__.py +0 -0
  24. {thyra-2.2.0 → thyra-2.2.2}/thyra/core/base_converter.py +0 -0
  25. {thyra-2.2.0 → thyra-2.2.2}/thyra/core/base_extractor.py +0 -0
  26. {thyra-2.2.0 → thyra-2.2.2}/thyra/core/base_reader.py +0 -0
  27. {thyra-2.2.0 → thyra-2.2.2}/thyra/core/registry.py +0 -0
  28. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/__init__.py +0 -0
  29. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/extractors/__init__.py +0 -0
  30. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/extractors/bruker_extractor.py +0 -0
  31. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/extractors/imzml_extractor.py +0 -0
  32. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/extractors/waters_extractor.py +0 -0
  33. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/ontology/__init__.py +0 -0
  34. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/ontology/_ims.py +0 -0
  35. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/ontology/_ms.py +0 -0
  36. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/ontology/_uo.py +0 -0
  37. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/ontology/cache.py +0 -0
  38. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/types.py +0 -0
  39. {thyra-2.2.0 → thyra-2.2.2}/thyra/metadata/validator.py +0 -0
  40. {thyra-2.2.0 → thyra-2.2.2}/thyra/preview.py +0 -0
  41. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/__init__.py +0 -0
  42. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/__init__.py +0 -0
  43. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/base_bruker_reader.py +0 -0
  44. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/folder_structure.py +0 -0
  45. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/mis_parser.py +0 -0
  46. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
  47. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
  48. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/__init__.py +0 -0
  49. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
  50. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
  51. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
  52. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
  53. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
  54. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
  55. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
  56. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
  57. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
  58. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
  59. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
  60. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
  61. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
  62. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
  63. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/imzml/__init__.py +0 -0
  64. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/__init__.py +0 -0
  65. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/imaging_grid.py +0 -0
  66. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/lib/MLReader.dll +0 -0
  67. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
  68. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/lib/libMLReader.so +0 -0
  69. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
  70. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/masslynx_lib.py +0 -0
  71. {thyra-2.2.0 → thyra-2.2.2}/thyra/readers/waters/waters_reader.py +0 -0
  72. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/__init__.py +0 -0
  73. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/common_axis.py +0 -0
  74. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/constants.py +0 -0
  75. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/decision_tree.py +0 -0
  76. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/gaps.py +0 -0
  77. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/__init__.py +0 -0
  78. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/base_generator.py +0 -0
  79. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
  80. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/linear_generator.py +0 -0
  81. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
  82. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
  83. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
  84. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/strategies/__init__.py +0 -0
  85. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/strategies/base.py +0 -0
  86. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
  87. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/strategies/tic_preserving.py +0 -0
  88. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/tic.py +0 -0
  89. {thyra-2.2.0 → thyra-2.2.2}/thyra/resampling/types.py +0 -0
  90. {thyra-2.2.0 → thyra-2.2.2}/thyra/tools/__init__.py +0 -0
  91. {thyra-2.2.0 → thyra-2.2.2}/thyra/tools/check_ontology.py +0 -0
  92. {thyra-2.2.0 → thyra-2.2.2}/thyra/tools/make_example_data.py +0 -0
  93. {thyra-2.2.0 → thyra-2.2.2}/thyra/utils/__init__.py +0 -0
  94. {thyra-2.2.0 → thyra-2.2.2}/thyra/utils/bruker_exceptions.py +0 -0
  95. {thyra-2.2.0 → thyra-2.2.2}/thyra/utils/logging_config.py +0 -0
  96. {thyra-2.2.0 → thyra-2.2.2}/thyra/utils/windows_paths.py +0 -0
  97. {thyra-2.2.0 → thyra-2.2.2}/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.2.0
3
+ Version: 2.2.2
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.2.0"
7
+ version = "2.2.2"
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.2.0"
39
+ __version__ = "2.2.2"
40
40
 
41
41
  # Import key components - avoid wildcard imports
42
42
  try:
@@ -415,9 +415,26 @@ class ImzMLReader(BaseMSIReader):
415
415
  # Initialize the parser
416
416
  logger.info(f"Initializing ImzML parser for {imzml_path}")
417
417
  try:
418
+ # ElementTree, NOT lxml. pyimzml prunes each <spectrum> out of the
419
+ # tree (`slist.remove(elem)`) while iterparse is still streaming the
420
+ # document, which invalidates libxml2's text-node coalescing
421
+ # accelerator: ctxt->nodelen/nodemem go on describing a text node
422
+ # inside the subtree that was just removed, so later character data
423
+ # is appended at a stale offset and the buffer is doubled on every
424
+ # miss until xmlRealloc fails. lxml surfaces libxml2's
425
+ # XML_ERR_NO_MEMORY as a *syntax* error, so it reads as a corrupt
426
+ # file when nothing is wrong with it:
427
+ # XMLSyntaxError: xmlSAX2Characters, line 212575, column 1
428
+ # It only bites when the text between </spectrum> and <spectrum> is
429
+ # exactly "\r\n" -- CRLF with no indentation, how IONTOF SurfaceLab
430
+ # writes imzML. Indented or LF-only files coalesce differently and
431
+ # survive, which is why most files never hit it. ElementTree builds
432
+ # the tree in Python, so there is no C parser state to invalidate;
433
+ # it is also pyimzml's own default, parses byte-identically, and is
434
+ # faster here -- 67s vs 118s on a 2.1 GB imzML at the same peak RSS.
418
435
  self.parser = ImzMLParser(
419
436
  filename=str(imzml_path),
420
- parse_lib="lxml",
437
+ parse_lib="ElementTree",
421
438
  ibd_file=self.ibd_file,
422
439
  )
423
440
  except Exception as e:
@@ -66,6 +66,14 @@ class DataCharacteristics:
66
66
 
67
67
  Profile data typically has >5000 points per spectrum,
68
68
  indicating continuous signal rather than centroid peaks.
69
+
70
+ This says how densely the spectra are sampled. It says nothing about
71
+ which instrument produced them, so it **must not be used to pick a
72
+ resampling strategy or an axis type**: those encode assumptions about
73
+ the acquisition's physics. ``RapiflexDetector`` used to match on this
74
+ alone, which handed MALDI-TOF treatment to any dense profile data --
75
+ TOF-SIMS, Orbitrap, anything -- with no check that the data was
76
+ MALDI-TOF at all.
69
77
  """
70
78
  avg = self.avg_peaks_per_spectrum
71
79
  return (
@@ -76,7 +84,12 @@ class DataCharacteristics:
76
84
 
77
85
  @property
78
86
  def is_maldi_tof(self) -> bool:
79
- """Check if this is MALDI-TOF data."""
87
+ """Check if this is MALDI-TOF data.
88
+
89
+ Carries the same caveat as :attr:`is_high_density_profile`, which its
90
+ last clause consults: density plus a vendor name is not a modality.
91
+ No detector calls this.
92
+ """
80
93
  return (
81
94
  self.is_rapiflex_format
82
95
  or self.instrument_type == "MALDI-TOF"
@@ -52,6 +52,30 @@ class InstrumentDetector(ABC):
52
52
  """Get the recommended mass axis type for this instrument."""
53
53
  pass
54
54
 
55
+ @property
56
+ def source_grid_law(self) -> Optional[AxisType]:
57
+ """Spacing law of the *source* m/z grid, when it is known.
58
+
59
+ ``tic_preserving`` interpolates onto the target axis and then applies
60
+ one global scaling factor. That composite operator is exact only when
61
+ the source grid follows the same spacing law as the target axis; off
62
+ the diagonal it distorts the ratio between two peaks by exactly
63
+ ``(m_hi / m_lo) ** (p_target - p_source)``, with ``p`` = 0, 0.5, 1,
64
+ 1.5, 2 for constant / linear_tof / reflector_tof / orbitrap / fticr.
65
+
66
+ SCiLS Lab gates its own TIC-preserving resampling on the same
67
+ condition -- "If all axis types are identical, a TIC preserving
68
+ resampling is applied, otherwise a linear interpolation is performed"
69
+ (SCiLS Lab 2026b User Guide, p.80).
70
+
71
+ ``None`` -- the default -- means Thyra does not know the source law.
72
+ That is the honest answer for every detector that matches on
73
+ something other than a vendor format whose grid Thyra itself lays
74
+ out, and :class:`InstrumentDetectorChain` refuses ``TIC_PRESERVING``
75
+ for such a detector.
76
+ """
77
+ return None
78
+
55
79
 
56
80
  class CentroidImzMLDetector(InstrumentDetector):
57
81
  """Detector for centroid ImzML data.
@@ -91,23 +115,33 @@ class RapiflexDetector(InstrumentDetector):
91
115
  return "Rapiflex MALDI-TOF"
92
116
 
93
117
  def matches(self, characteristics: DataCharacteristics) -> bool:
94
- """Check if data matches Rapiflex, Bruker MALDI-TOF, or high-density profile."""
118
+ """Check if data came from Bruker flexImaging/Rapiflex.
119
+
120
+ Both branches below are written by one producer, the Rapiflex
121
+ metadata extractor, so a match here means the spectra arrive on the
122
+ uniform-in-m/z grid ``RapiflexReader`` builds.
123
+
124
+ Peak density is deliberately *not* consulted. It used to be: any
125
+ profile data averaging more than
126
+ ``Thresholds.PROFILE_PEAK_DENSITY`` points per spectrum was handed
127
+ MALDI-TOF treatment regardless of what instrument produced it. That
128
+ made the detector modality-blind -- a dense TOF-SIMS or Orbitrap
129
+ profile acquisition would have been resampled by MALDI-TOF logic
130
+ onto a MALDI-shaped axis, silently. SCiLS Lab does not guess
131
+ modality either; its importer takes ``--project TIMSTOF|TOF|FT`` as
132
+ an argument (2026b User Guide, p.81). Unknown-provenance profile
133
+ data now falls through to :class:`DefaultDetector`, which bins
134
+ counts rather than interpolating and so is safe for any modality.
135
+ """
95
136
  # Direct Rapiflex format detection
96
137
  if characteristics.is_rapiflex_format:
97
138
  return True
98
139
 
99
140
  # Bruker MALDI-TOF detection
100
- if (
141
+ return (
101
142
  characteristics.instrument_type == "MALDI-TOF"
102
143
  and characteristics.manufacturer == "Bruker"
103
- ):
104
- return True
105
-
106
- # Profile data with high peak density (likely MALDI-TOF)
107
- if characteristics.is_high_density_profile:
108
- return True
109
-
110
- return False
144
+ )
111
145
 
112
146
  def get_resampling_method(self) -> ResamplingMethod:
113
147
  """Return TIC-preserving for profile MALDI-TOF data."""
@@ -117,6 +151,19 @@ class RapiflexDetector(InstrumentDetector):
117
151
  """Return constant axis matching SCiLS Lab convention."""
118
152
  return AxisType.CONSTANT
119
153
 
154
+ @property
155
+ def source_grid_law(self) -> Optional[AxisType]:
156
+ """Report the constant law of the flexImaging source grid.
157
+
158
+ ``RapiflexReader.get_common_mass_axis`` lays every spectrum out with
159
+ ``np.linspace(mass_start, mass_end, n_points)``, so the source
160
+ spacing is constant in m/z -- the same law as the ``constant`` target
161
+ axis :meth:`get_axis_type` asks for. Source law equal to target law
162
+ is what makes ``tic_preserving`` exact here, and it is the reason
163
+ this is the only route on which the chain permits it.
164
+ """
165
+ return AxisType.CONSTANT
166
+
120
167
 
121
168
  class TimsTOFDetector(InstrumentDetector):
122
169
  """Detector for Bruker timsTOF data.
@@ -273,6 +320,9 @@ class InstrumentDetectorChain:
273
320
  ) -> ResamplingMethod:
274
321
  """Get resampling method for the detected instrument.
275
322
 
323
+ ``TIC_PRESERVING`` additionally has to clear the matching-axis-law
324
+ gate; see :meth:`_gate_tic_preserving`.
325
+
276
326
  Args:
277
327
  characteristics: Data characteristics to match
278
328
 
@@ -280,10 +330,51 @@ class InstrumentDetectorChain:
280
330
  Recommended resampling method for the instrument
281
331
  """
282
332
  detector = self.detect(characteristics)
283
- method = detector.get_resampling_method()
333
+ method = self._gate_tic_preserving(detector)
284
334
  logger.info(f"Selected resampling method: {method.name}")
285
335
  return method
286
336
 
337
+ @staticmethod
338
+ def _gate_tic_preserving(detector: InstrumentDetector) -> ResamplingMethod:
339
+ """Allow ``TIC_PRESERVING`` only when source and target laws agree.
340
+
341
+ SCiLS Lab applies TIC-preserving resampling to profile data only
342
+ when all the mass axes being combined are of the same type, and
343
+ linear interpolation otherwise (2026b User Guide, p.80). That is
344
+ also precisely the condition under which Thyra's operator --
345
+ interpolate, then rescale by one global factor -- is exact. See
346
+ :attr:`InstrumentDetector.source_grid_law` for the error off the
347
+ diagonal.
348
+
349
+ A detector that has not declared its source grid law does not clear
350
+ the gate. Auto-selection therefore cannot reach the interpolating
351
+ path on data whose acquisition Thyra has not actually identified.
352
+
353
+ Args:
354
+ detector: The detector that matched.
355
+
356
+ Returns:
357
+ The detector's method, or ``NEAREST_NEIGHBOR`` in its place.
358
+ """
359
+ method = detector.get_resampling_method()
360
+ if method is not ResamplingMethod.TIC_PRESERVING:
361
+ return method
362
+
363
+ axis_type = detector.get_axis_type()
364
+ source_law = detector.source_grid_law
365
+ if source_law is axis_type:
366
+ return method
367
+
368
+ logger.info(
369
+ "%s asks for TIC_PRESERVING onto a %s axis, but the source grid "
370
+ "law is %s. TIC-preserving resampling is exact only when the two "
371
+ "match, so NEAREST_NEIGHBOR is used instead.",
372
+ detector.name,
373
+ axis_type.name,
374
+ "unknown" if source_law is None else source_law.name,
375
+ )
376
+ return ResamplingMethod.NEAREST_NEIGHBOR
377
+
287
378
  def get_axis_type(self, characteristics: DataCharacteristics) -> AxisType:
288
379
  """Get axis type for the detected instrument.
289
380
 
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