thyra 3.2.0__tar.gz → 3.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-3.2.0 → thyra-3.2.2}/PKG-INFO +1 -1
  2. {thyra-3.2.0 → thyra-3.2.2}/pyproject.toml +1 -1
  3. {thyra-3.2.0 → thyra-3.2.2}/thyra/__init__.py +1 -1
  4. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/base_spatialdata_converter.py +53 -52
  5. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/spatialdata_2d_converter.py +1 -3
  6. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/spatialdata_3d_converter.py +1 -3
  7. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/streaming_converter.py +18 -10
  8. {thyra-3.2.0 → thyra-3.2.2}/LICENSE +0 -0
  9. {thyra-3.2.0 → thyra-3.2.2}/README.md +0 -0
  10. {thyra-3.2.0 → thyra-3.2.2}/thyra/__main__.py +0 -0
  11. {thyra-3.2.0 → thyra-3.2.2}/thyra/alignment/__init__.py +0 -0
  12. {thyra-3.2.0 → thyra-3.2.2}/thyra/alignment/affine.py +0 -0
  13. {thyra-3.2.0 → thyra-3.2.2}/thyra/alignment/teaching_points.py +0 -0
  14. {thyra-3.2.0 → thyra-3.2.2}/thyra/config.py +0 -0
  15. {thyra-3.2.0 → thyra-3.2.2}/thyra/convert.py +0 -0
  16. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/__init__.py +0 -0
  17. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/__init__.py +0 -0
  18. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/_chunking.py +0 -0
  19. {thyra-3.2.0 → thyra-3.2.2}/thyra/converters/spatialdata/converter.py +0 -0
  20. {thyra-3.2.0 → thyra-3.2.2}/thyra/core/__init__.py +0 -0
  21. {thyra-3.2.0 → thyra-3.2.2}/thyra/core/base_converter.py +0 -0
  22. {thyra-3.2.0 → thyra-3.2.2}/thyra/core/base_extractor.py +0 -0
  23. {thyra-3.2.0 → thyra-3.2.2}/thyra/core/base_reader.py +0 -0
  24. {thyra-3.2.0 → thyra-3.2.2}/thyra/core/registry.py +0 -0
  25. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/__init__.py +0 -0
  26. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/extractors/__init__.py +0 -0
  27. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/extractors/bruker_extractor.py +0 -0
  28. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/extractors/imzml_extractor.py +0 -0
  29. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/extractors/waters_extractor.py +0 -0
  30. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/ontology/__init__.py +0 -0
  31. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/ontology/_ims.py +0 -0
  32. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/ontology/_ms.py +0 -0
  33. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/ontology/_uo.py +0 -0
  34. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/ontology/cache.py +0 -0
  35. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/types.py +0 -0
  36. {thyra-3.2.0 → thyra-3.2.2}/thyra/metadata/validator.py +0 -0
  37. {thyra-3.2.0 → thyra-3.2.2}/thyra/preview.py +0 -0
  38. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/__init__.py +0 -0
  39. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/__init__.py +0 -0
  40. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/base_bruker_reader.py +0 -0
  41. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/folder_structure.py +0 -0
  42. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/mis_parser.py +0 -0
  43. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
  44. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
  45. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/__init__.py +0 -0
  46. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
  47. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
  48. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
  49. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
  50. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
  51. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
  52. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
  53. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
  54. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
  55. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/utils/__init__.py +0 -0
  56. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/utils/batch_processor.py +0 -0
  57. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/utils/coordinate_cache.py +0 -0
  58. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/utils/mass_axis_builder.py +0 -0
  59. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/bruker/timstof/utils/memory_manager.py +0 -0
  60. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/imzml/__init__.py +0 -0
  61. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/imzml/imzml_reader.py +0 -0
  62. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/__init__.py +0 -0
  63. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/imaging_grid.py +0 -0
  64. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/lib/MLReader.dll +0 -0
  65. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
  66. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/lib/libMLReader.so +0 -0
  67. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
  68. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/masslynx_lib.py +0 -0
  69. {thyra-3.2.0 → thyra-3.2.2}/thyra/readers/waters/waters_reader.py +0 -0
  70. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/__init__.py +0 -0
  71. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/common_axis.py +0 -0
  72. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/constants.py +0 -0
  73. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/data_characteristics.py +0 -0
  74. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/decision_tree.py +0 -0
  75. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/gaps.py +0 -0
  76. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/instrument_detectors.py +0 -0
  77. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/__init__.py +0 -0
  78. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/base_generator.py +0 -0
  79. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
  80. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/linear_generator.py +0 -0
  81. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
  82. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
  83. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
  84. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/strategies/__init__.py +0 -0
  85. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/strategies/base.py +0 -0
  86. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
  87. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/strategies/tic_preserving.py +0 -0
  88. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/tic.py +0 -0
  89. {thyra-3.2.0 → thyra-3.2.2}/thyra/resampling/types.py +0 -0
  90. {thyra-3.2.0 → thyra-3.2.2}/thyra/tools/__init__.py +0 -0
  91. {thyra-3.2.0 → thyra-3.2.2}/thyra/tools/check_ontology.py +0 -0
  92. {thyra-3.2.0 → thyra-3.2.2}/thyra/tools/make_example_data.py +0 -0
  93. {thyra-3.2.0 → thyra-3.2.2}/thyra/utils/__init__.py +0 -0
  94. {thyra-3.2.0 → thyra-3.2.2}/thyra/utils/bruker_exceptions.py +0 -0
  95. {thyra-3.2.0 → thyra-3.2.2}/thyra/utils/logging_config.py +0 -0
  96. {thyra-3.2.0 → thyra-3.2.2}/thyra/utils/windows_paths.py +0 -0
  97. {thyra-3.2.0 → thyra-3.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: 3.2.0
3
+ Version: 3.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 = "3.2.0"
7
+ version = "3.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__ = "3.2.0"
39
+ __version__ = "3.2.2"
40
40
 
41
41
  # Import key components - avoid wildcard imports
42
42
  try:
@@ -151,7 +151,7 @@ try:
151
151
  import xarray as xr
152
152
  import zarr
153
153
  from anndata import AnnData
154
- from shapely.geometry import Polygon, box
154
+ from shapely.geometry import box
155
155
  from spatialdata import SpatialData
156
156
  from spatialdata.models import Image2DModel, ShapesModel, TableModel
157
157
  from spatialdata.transformations import Affine, Identity, Scale, Sequence
@@ -1509,48 +1509,63 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1509
1509
 
1510
1510
  coo_arrays["current_idx"] = end_idx
1511
1511
 
1512
- def _create_pixel_shapes(
1513
- self, adata: AnnData, is_3d: bool = False
1514
- ) -> "ShapesModel":
1512
+ def _create_pixel_shapes(self, adata: AnnData) -> "ShapesModel":
1515
1513
  """Create geometric shapes for pixels with proper transformations.
1516
1514
 
1517
1515
  When optical alignment is available (FlexImaging with Area definitions),
1518
1516
  shapes are created in optical image pixel coordinates for proper overlay.
1519
1517
  Otherwise, shapes use physical (micrometer) coordinates.
1520
1518
 
1521
- For a multi-slice volume the footprints are ``POLYGON Z``: a flat
1522
- square at the micrometre depth of the slice it was acquired on,
1523
- taken from ``obs["spatial_z"]``. They are footprints, not voxels --
1524
- shapely has no solid, so a pixel is its square at one depth rather
1525
- than a box spanning the slice thickness.
1526
-
1527
- Two alternatives were measured and rejected:
1528
-
1519
+ **Footprints are two-dimensional, including on a multi-slice
1520
+ volume.** A slice's depth is carried by the TIC image's ``Scale``
1521
+ and by ``obs["spatial_z"]``; it is deliberately not also put on
1522
+ the polygon geometry.
1523
+
1524
+ Three options were measured. All three are imperfect, so what
1525
+ follows is the reasoning rather than a claim that this one is
1526
+ free:
1527
+
1528
+ * **``POLYGON Z``** -- geometrically the most honest, and what
1529
+ 7317792 shipped in v3.2.0. It breaks ``spatialdata``'s spatial
1530
+ queries: measured on a 5x3x2 volume, a bounding box enclosing
1531
+ the whole dataset with 1000um of margin returned 26 of 30
1532
+ footprints and 26 of 30 table rows, with no exception and no
1533
+ warning. ``shapely.force_2d`` on the same geometry restored
1534
+ 30 of 30. A z-restricted query returned the same rows whether
1535
+ z was inside or far outside the data, so z is ignored by the
1536
+ query path rather than honoured.
1529
1537
  * **Flat 2D shapes carrying the depth in a transformation**
1530
- (one element per slice, each with a ``Translation`` in z). This
1531
- is what spatialdata's 2D shapes model invites, and it does not
1532
- work: the element parses without complaint, coexists with a 3D
1533
- image and round-trips, but the transform **silently drops z**
1534
- and every slice comes back at the same depth. It would put a
1535
- depth in the metadata that never reaches the geometry.
1536
- * **Leaving them flat** and documenting it. Honest, but then
1537
- ``docs/coordinate-systems.md``'s promise that every element
1538
- agrees at ``"global"`` stays false in z.
1539
-
1540
- The cost of the choice: ``ShapesModel.parse``/``validate`` and
1541
- every downstream ``transform`` emit a ``UserWarning`` that 2
1542
- dimensions are expected. It is left unsuppressed deliberately --
1543
- it is upstream telling consumers this is outside the model, and
1544
- hiding it would be Thyra deciding that on their behalf. Only the
1545
- micrometre branch gains z; under optical alignment the frame is
1546
- optical pixels, which have no depth calibration to place a slice
1547
- in.
1538
+ (one element per slice, each with a ``Translation`` in z).
1539
+ The element parses, coexists with a 3D image and round-trips,
1540
+ but the transform **silently drops z** and every slice comes
1541
+ back at the same depth -- a depth in the metadata that never
1542
+ reaches the geometry. ``test_3d_pixel_shapes_z.py`` pins this,
1543
+ because it is the change someone will otherwise propose.
1544
+ * **Flat, with the depth on the image and in ``obs`` only** --
1545
+ what this does.
1546
+
1547
+ The deciding argument is that ``spatialdata`` itself asks for
1548
+ this. ``ShapesModel.validate`` warns that a 3-dimensional
1549
+ geometry column "could led to unexpected behaviors" and names
1550
+ ``force_2d()`` as the remedy; the query result above is that
1551
+ behaviour. 3D shapes are not on the upstream roadmap
1552
+ (scverse/spatialdata#109 has been idle since June 2023 and
1553
+ covers images, labels and transformations only), and the live
1554
+ 2.5D discussion (#961) scopes itself to points, images and
1555
+ labels. Serial-section MSI is 2.5D in that taxonomy.
1556
+
1557
+ What this costs: ``docs/coordinate-systems.md``'s promise that
1558
+ every element agrees at ``"global"`` is exact in x and y and
1559
+ silent in z for the shapes element. That is a documented gap
1560
+ rather than a wrong answer, which a truncated query is not. See
1561
+ ``docs/output-format.md`` for the consumer-facing statement.
1562
+
1563
+ Reinstating z means restoring the ``is_3d`` parameter at all
1564
+ three call sites as well; it was removed with the geometry so it
1565
+ could not sit unread, which is how the original defect arose.
1548
1566
 
1549
1567
  Args:
1550
1568
  adata: AnnData object containing coordinates
1551
- is_3d: Whether this is the 3D volume route. Combined with
1552
- :attr:`_is_volume` (which also requires more than one
1553
- plane), decides whether footprints carry z.
1554
1569
 
1555
1570
  Returns:
1556
1571
  SpatialData shapes model
@@ -1628,29 +1643,15 @@ class BaseSpatialDataConverter(BaseMSIConverter, ABC):
1628
1643
  y_coords: NDArray[np.float64] = adata.obs["spatial_y"].values
1629
1644
  half_pixel_um = self.pixel_size_um / 2
1630
1645
 
1631
- # A volume's footprints carry the depth of the slice they were
1632
- # acquired on, so they meet the TIC volume at "global" instead
1633
- # of stacking flat at z=0. spatial_z is already the micrometre
1634
- # depth (z index * z_spacing_um), so it needs no scaling here
1635
- # -- the same column the table stores, which is what keeps the
1636
- # two elements agreeing.
1637
- z_coords: Optional[NDArray[np.float64]] = None
1638
- if is_3d and self._is_volume:
1639
- z_coords = adata.obs["spatial_z"].values
1640
-
1646
+ # Footprints are flat, on every route including volumes. A
1647
+ # slice's depth lives on the TIC image's Scale and in
1648
+ # obs["spatial_z"]; see the docstring for why it is not also
1649
+ # put on the geometry.
1641
1650
  for i in range(len(adata)):
1642
1651
  x, y = x_coords[i], y_coords[i]
1643
1652
  x0, y0 = x - half_pixel_um, y - half_pixel_um
1644
1653
  x1, y1 = x + half_pixel_um, y + half_pixel_um
1645
-
1646
- if z_coords is None:
1647
- pixel_box = box(x0, y0, x1, y1)
1648
- else:
1649
- z = float(z_coords[i])
1650
- pixel_box = Polygon(
1651
- [(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)]
1652
- )
1653
- geometries.append(pixel_box)
1654
+ geometries.append(box(x0, y0, x1, y1))
1654
1655
 
1655
1656
  # Create GeoDataFrame with appropriate index
1656
1657
  if valid_indices is not None:
@@ -328,9 +328,7 @@ class SpatialData2DConverter(BaseSpatialDataConverter):
328
328
 
329
329
  # Add to tables and create shapes
330
330
  data_structures["tables"][slice_id] = table
331
- data_structures["shapes"][region_key] = self._create_pixel_shapes(
332
- adata, is_3d=False
333
- )
331
+ data_structures["shapes"][region_key] = self._create_pixel_shapes(adata)
334
332
 
335
333
  # Create TIC image for this slice
336
334
  tic_values = slice_data["tic_values"]
@@ -246,9 +246,7 @@ class SpatialData3DConverter(BaseSpatialDataConverter):
246
246
 
247
247
  # Add to tables and create shapes
248
248
  data_structures["tables"][self.dataset_id] = table
249
- data_structures["shapes"][region_key] = self._create_pixel_shapes(
250
- adata, is_3d=True
251
- )
249
+ data_structures["shapes"][region_key] = self._create_pixel_shapes(adata)
252
250
 
253
251
  # Create TIC image
254
252
  self._create_tic_image(data_structures)
@@ -1004,9 +1004,7 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
1004
1004
 
1005
1005
  # Add to tables and create shapes
1006
1006
  data_structures["tables"][slice_id] = table
1007
- data_structures["shapes"][region_key] = self._create_pixel_shapes(
1008
- adata, is_3d=False
1009
- )
1007
+ data_structures["shapes"][region_key] = self._create_pixel_shapes(adata)
1010
1008
 
1011
1009
  # Create TIC image for this slice
1012
1010
  tic_values = slice_data["tic_values"]
@@ -1278,18 +1276,28 @@ class StreamingSpatialDataConverter(BaseSpatialDataConverter):
1278
1276
 
1279
1277
  nnz = len(mz_indices)
1280
1278
  if nnz > 0:
1281
- # Count entries per column (vectorized)
1282
- np.add.at(col_counts, mz_indices, 1)
1283
- total_nnz += nnz
1284
-
1285
- # Accumulate for average spectrum (vectorized)
1279
+ # Accumulate for average spectrum (vectorized). Left
1280
+ # outside the bounds check to match the COO pre-scan,
1281
+ # which also averages over every spectrum read rather
1282
+ # than every spectrum stored.
1286
1283
  np.add.at(total_intensity, mz_indices, resampled_ints)
1287
1284
 
1288
- # TIC and occupancy. Both are indexed by grid position,
1289
- # so both need the bounds check: a reader yielding a
1285
+ # Column counts, TIC and occupancy all describe a
1286
+ # spectrum that is going to get a row, so all three sit
1287
+ # behind the bounds check: a reader yielding a
1290
1288
  # coordinate outside the declared dimensions would
1291
1289
  # otherwise wrap round and land on an unrelated pixel.
1290
+ #
1291
+ # col_counts and total_nnz size the CSC arrays, and the
1292
+ # scatter pass skips exactly the spectra this rejects
1293
+ # (row_of_grid gives them no row). Counting them here
1294
+ # reserved slots nothing ever wrote: the memmap is
1295
+ # zero-filled, so they surfaced as explicit zeros at
1296
+ # row 0, out of order within their column, which is a
1297
+ # matrix scipy reports as non-canonical.
1292
1298
  if 0 <= y < n_y and 0 <= x < n_x:
1299
+ np.add.at(col_counts, mz_indices, 1)
1300
+ total_nnz += nnz
1293
1301
  tic_values[y, x] = resampled_ints.sum()
1294
1302
  occupancy[y * n_x + x] = True
1295
1303
  else:
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