diff --git a/docs/examples/3d_reconstruction_example.md b/docs/examples/3d_reconstruction_example.md new file mode 100644 index 00000000..5e02baf6 --- /dev/null +++ b/docs/examples/3d_reconstruction_example.md @@ -0,0 +1,203 @@ +# 3D Reconstruction Example: Assigning Z-Coordinates to Multiple Slices + +This example demonstrates how to use Spateo's `assign_z_coordinates` function to build 3D models from multiple 2D spatial transcriptomics slices. + +## Quick Start Example + +```python +import spateo as st +import scanpy as sc + +# Load your aligned 2D slices +# Assuming you have already aligned them using st.tl.morpho_align +slices = [ + sc.read_h5ad("aligned_slice_0.h5ad"), + sc.read_h5ad("aligned_slice_1.h5ad"), + sc.read_h5ad("aligned_slice_2.h5ad"), + sc.read_h5ad("aligned_slice_3.h5ad"), +] + +# Method 1: Default uniform spacing (spacing = 1.0) +st.tl.assign_z_coordinates(slices, spatial_key="align_spatial") + +# Method 2: Use known tissue thickness (e.g., 20 µm sections) +st.tl.assign_z_coordinates( + slices, + spatial_key="align_spatial", + tissue_thickness=20.0 +) + +# Method 3: Custom uniform spacing +st.tl.assign_z_coordinates( + slices, + spatial_key="align_spatial", + z_spacing=15.0 +) + +# Method 4: Variable spacing (e.g., if section 2 is missing) +st.tl.assign_z_coordinates( + slices, + spatial_key="align_spatial", + z_spacing=[20.0, 40.0, 20.0] # Double spacing where section is missing +) + +# Visualize the 3D reconstruction +st.pl.three_d_multi_plot.multi_models( + *slices, + spatial_key="align_spatial", + group_key="cell_type", + mode="single" +) +``` + +## Complete Workflow Example + +```python +import spateo as st +import scanpy as sc +import numpy as np + +# Step 1: Load your raw 2D slices +slice_files = [ + "slice_0.h5ad", + "slice_1.h5ad", + "slice_2.h5ad", + "slice_3.h5ad" +] + +slices = [] +for i, file_path in enumerate(slice_files): + adata = sc.read_h5ad(file_path) + adata.obs['slice_id'] = i + slices.append(adata) + +# Step 2: Perform pairwise alignment +# Align each slice to the previous one +aligned_slices = [slices[0]] # First slice is the reference + +for i in range(1, len(slices)): + st.tl.morpho_align( + sliceA=slices[i], + sliceB=aligned_slices[-1], + spatial_key='spatial', + key_added='align_spatial', + # Additional alignment parameters + n_sampling=2000, + ) + aligned_slices.append(slices[i]) + +# Step 3: Assign z-coordinates based on tissue thickness +# If you know your sections are 15 µm thick +st.tl.assign_z_coordinates( + aligned_slices, + spatial_key='align_spatial', + tissue_thickness=15.0, + z_offset=0.0 +) + +# Step 4: Optionally perform global refinement to reduce cumulative errors +st.tl.morpho_align_ref( + models=aligned_slices, + spatial_key='align_spatial', + # Global refinement parameters +) + +# Step 5: Visualize the 3D model +st.pl.three_d_multi_plot.multi_models( + *aligned_slices, + spatial_key='align_spatial', + group_key='cell_type', + mode='single', + show_model=True +) +``` + +## Working with Different Coordinate Systems + +If your spatial coordinates are in pixels and you want to convert to physical units: + +```python +# Your imaging parameters +pixel_size_um = 0.5 # Each pixel represents 0.5 µm +section_thickness_um = 20.0 # Tissue sections are 20 µm thick + +# Calculate spacing in pixel units +section_thickness_pixels = section_thickness_um / pixel_size_um # 40 pixels + +# Assign z-coordinates in pixel units +st.tl.assign_z_coordinates( + slices, + spatial_key='align_spatial', + tissue_thickness=section_thickness_pixels, + z_offset=0.0 +) +``` + +## Handling Missing Sections + +When you have missing tissue sections, use variable spacing: + +```python +# You have 5 slices, but section 2 is missing +# Normal spacing is 20 µm, so missing section = 40 µm gap + +st.tl.assign_z_coordinates( + slices, + spatial_key='align_spatial', + z_spacing=[20.0, 40.0, 20.0, 20.0] # Larger gap where section is missing +) +``` + +## Best Practices + +### 1. **Match Units** +Always ensure z-spacing uses the same units as your xy coordinates: +```python +# If xy coordinates are in µm, use µm for z-spacing +# If xy coordinates are in pixels, convert appropriately +``` + +### 2. **Validate Visually** +After assigning z-coordinates, always visualize to check: +```python +# If slices appear too compressed or too separated, adjust z_spacing +st.pl.three_d_multi_plot.multi_models(*slices, spatial_key='align_spatial') +``` + +### 3. **Use Inplace Wisely** +```python +# Keep original data unchanged +slices_3d = st.tl.assign_z_coordinates(slices, inplace=False) + +# Modify in place to save memory +st.tl.assign_z_coordinates(slices, inplace=True) +``` + +### 4. **Check Biological Structures** +Verify that known continuous structures (e.g., blood vessels, tissue layers) appear continuous across slices. If not, adjust your z-spacing or check alignment quality. + +## Troubleshooting + +**Issue: Slices appear too close together** +```python +# Increase z_spacing +st.tl.assign_z_coordinates(slices, z_spacing=50.0) # Larger spacing +``` + +**Issue: Slices appear too far apart** +```python +# Decrease z_spacing +st.tl.assign_z_coordinates(slices, z_spacing=5.0) # Smaller spacing +``` + +**Issue: Known structures don't align** +- Check your 2D alignment quality first +- Consider using global refinement with `st.tl.morpho_align_ref` +- Verify that you're using the correct spatial_key + +## See Also + +- [3D Reconstruction Technical Documentation](../technicals/3d_reconstruction.md) +- API Documentation: `st.tl.assign_z_coordinates` +- API Documentation: `st.tl.morpho_align` +- API Documentation: `st.pl.three_d_multi_plot` diff --git a/docs/examples/README.md b/docs/examples/README.md new file mode 100644 index 00000000..f3ca116a --- /dev/null +++ b/docs/examples/README.md @@ -0,0 +1,18 @@ +# Spateo Examples + +This directory contains practical examples and code snippets demonstrating how to use Spateo for various spatial transcriptomics analysis tasks. + +## Available Examples + +### [3D Reconstruction Example](3d_reconstruction_example.md) + +Learn how to build 3D models from multiple 2D spatial transcriptomics slices, including: +- Assigning z-coordinates to tissue slices +- Different spacing strategies (uniform, tissue thickness-based, variable) +- Complete alignment workflow +- Handling missing sections +- Best practices and troubleshooting + +## Contributing Examples + +If you have useful examples or workflows you'd like to share, please consider contributing them to this directory via a pull request. diff --git a/docs/technicals/3d_reconstruction.md b/docs/technicals/3d_reconstruction.md new file mode 100644 index 00000000..8655a1f9 --- /dev/null +++ b/docs/technicals/3d_reconstruction.md @@ -0,0 +1,339 @@ +# 3D Reconstruction from Multiple Tissue Slices + +This guide provides comprehensive information on how to build 3D models from multiple 2D spatial transcriptomics slices using Spateo. + +## Overview + +Spateo provides robust tools for reconstructing 3D structures from sequential 2D tissue sections. The workflow involves: + +1. **Slice Alignment**: Align 2D slices in the xy-plane using Spateo's alignment functions +2. **Z-Coordinate Assignment**: Calculate and assign appropriate z-axis coordinates for each slice +3. **3D Model Construction**: Combine aligned slices into a coherent 3D model +4. **Visualization and Analysis**: Explore the 3D structure using Spateo's visualization tools + +## Z-Axis Coordinate Calculation + +### Understanding Z-Spacing + +When reconstructing 3D models from 2D slices, one of the most critical decisions is determining the spacing between slices along the z-axis. Spateo provides the `assign_z_coordinates` function to handle this systematically. + +### Basic Usage + +```python +import spateo as st + +# Load your aligned 2D slices +slices = [slice1, slice2, slice3, slice4] # List of AnnData objects + +# Assign z-coordinates with default spacing (1.0 unit between slices) +st.tl.assign_z_coordinates(slices, spatial_key="align_spatial") +``` + +### Z-Spacing Strategies + +Spateo supports three main strategies for z-coordinate assignment: + +#### 1. Uniform Spacing (Default) + +Use this when slices are evenly spaced or when relative positioning is more important than absolute measurements: + +```python +# Default uniform spacing of 1.0 +st.tl.assign_z_coordinates(slices, spatial_key="align_spatial") + +# Custom uniform spacing +st.tl.assign_z_coordinates(slices, spatial_key="align_spatial", z_spacing=10.0) +``` + +**Best for:** +- Exploratory analysis where relative positions matter most +- When exact tissue thickness is unknown +- Consistent section thickness across all slices + +#### 2. Tissue Thickness-Based Spacing + +Use this when you know the physical thickness of your tissue sections: + +```python +# If your tissue sections are 15 µm thick +st.tl.assign_z_coordinates( + slices, + spatial_key="align_spatial", + tissue_thickness=15.0 +) +``` + +**Best for:** +- When physical measurements are available +- Quantitative spatial analysis requiring accurate distances +- Comparing structures across different specimens + +#### 3. Custom Variable Spacing + +Use this when different slices have different spacing (e.g., non-uniform sectioning): + +```python +# For 4 slices, provide 3 spacing values +# Spacing between: slice0-1: 10, slice1-2: 12, slice2-3: 10 +st.tl.assign_z_coordinates( + slices, + spatial_key="align_spatial", + z_spacing=[10.0, 12.0, 10.0] +) +``` + +**Best for:** +- Non-uniform section thickness +- Missing sections (use larger spacing to represent gaps) +- Variable sampling intervals + +## Complete 3D Reconstruction Workflow + +### Step 1: Prepare 2D Slices + +```python +import spateo as st +import scanpy as sc + +# Load your spatial transcriptomics data +slices = [] +for i, file_path in enumerate(slice_files): + adata = sc.read_h5ad(file_path) + # Add slice identifier + adata.obs['slice_id'] = i + slices.append(adata) +``` + +### Step 2: Align Slices in 2D + +```python +# Perform pairwise alignment +aligned_slices = [] +for i in range(len(slices) - 1): + # Align slice i+1 to slice i + st.tl.morpho_align( + sliceA=slices[i], + sliceB=slices[i+1], + spatial_key='spatial', + key_added='align_spatial', + # Additional alignment parameters + ) + aligned_slices.append(slices[i]) + +aligned_slices.append(slices[-1]) # Add the last slice +``` + +### Step 3: Assign Z-Coordinates + +```python +# Choose your z-spacing strategy +# Option A: Using known tissue thickness +st.tl.assign_z_coordinates( + aligned_slices, + spatial_key='align_spatial', + tissue_thickness=15.0, # 15 µm sections + z_offset=0.0 +) + +# Option B: Using custom spacing for each gap +st.tl.assign_z_coordinates( + aligned_slices, + spatial_key='align_spatial', + z_spacing=[10.0, 10.0, 15.0], # Variable spacing + z_offset=0.0 +) +``` + +### Step 4: Visualize in 3D + +```python +# Visualize all slices together in 3D +st.pl.three_d_multi_plot.multi_models( + *aligned_slices, + spatial_key='align_spatial', + mode='single', # or 'overlap' or 'both' + show_model=True +) +``` + +## Best Practices and Recommendations + +### Determining Appropriate Z-Spacing + +1. **Physical Measurements First**: If you know the tissue thickness from your experimental protocol, use that value. + +2. **Relative to XY Scale**: Consider the scale of your xy coordinates. If your spatial coordinates are in pixels and each pixel represents 1 µm, ensure your z_spacing uses the same units. + +3. **Visual Inspection**: After initial reconstruction, visually inspect the 3D model: + ```python + # If slices appear too compressed, increase z_spacing + # If slices appear too separated, decrease z_spacing + ``` + +4. **Biological Validation**: Check if known structures appear continuous across slices. Discontinuities may indicate: + - Incorrect z-spacing + - Missing sections that need larger gaps + - Need for non-uniform spacing + +### Common Pitfalls + +1. **Unit Mismatch**: Ensure z_spacing uses the same units as your xy coordinates + ```python + # If xy coordinates are in µm, z_spacing should also be in µm + # If xy coordinates are in pixels, convert appropriately + ``` + +2. **Missing Sections**: Account for missing tissue sections by using larger z-spacing + ```python + # If section 2 is missing between sections 1 and 3 + z_spacing = [10.0, 20.0, 10.0] # Double spacing for the gap + ``` + +3. **Coordinate Systems**: Ensure all slices use the same spatial coordinate system after alignment + +## Advanced Topics + +### Combining with Global Alignment Refinement + +For more accurate 3D reconstruction with many slices: + +```python +# First, perform pairwise alignment and assign z-coordinates +# Then, use global refinement to minimize cumulative errors +st.tl.morpho_align_ref( + models=aligned_slices, + spatial_key='align_spatial', + # Global refinement parameters +) +``` + +### Adjusting Z-Coordinates Post-Reconstruction + +If you need to modify z-coordinates after initial assignment: + +```python +# Re-run with different parameters +st.tl.assign_z_coordinates( + aligned_slices, + spatial_key='align_spatial', + z_spacing=20.0, # New spacing + inplace=True +) +``` + +### Working with Different Coordinate Keys + +You can maintain both 2D and 3D coordinates: + +```python +# Keep original 2D coordinates in 'spatial' +# Create 3D coordinates in 'spatial_3d' + +# First copy 2D coordinates +for adata in slices: + adata.obsm['spatial_3d'] = adata.obsm['align_spatial'].copy() + +# Then assign z-coordinates to the 3D version +st.tl.assign_z_coordinates( + slices, + spatial_key='spatial_3d', + tissue_thickness=15.0 +) +``` + +## Example Workflows + +### Example 1: Standard Uniform Sections + +```python +# 10 tissue sections, each 20 µm thick +slices = load_aligned_slices() # Your data loading function + +st.tl.assign_z_coordinates( + slices, + spatial_key='align_spatial', + tissue_thickness=20.0, + z_offset=0.0 +) + +# Visualize +st.pl.three_d_multi_plot.multi_models( + *slices, + spatial_key='align_spatial', + group_key='cell_type' +) +``` + +### Example 2: Non-Uniform Sections with Gaps + +```python +# 5 sections with known spacing: 15, 15, 30 (missing section), 15 µm +slices = load_aligned_slices() + +st.tl.assign_z_coordinates( + slices, + spatial_key='align_spatial', + z_spacing=[15.0, 15.0, 30.0, 15.0], # Note: n-1 spacing values for n slices + z_offset=0.0 +) +``` + +### Example 3: Pixel-Based Coordinates + +```python +# If spatial coordinates are in pixels, and each pixel = 0.5 µm +# Tissue sections are 20 µm thick +slices = load_aligned_slices() + +pixel_size = 0.5 # µm per pixel +section_thickness_um = 20.0 # µm +section_thickness_pixels = section_thickness_um / pixel_size # 40 pixels + +st.tl.assign_z_coordinates( + slices, + spatial_key='align_spatial', + tissue_thickness=section_thickness_pixels, + z_offset=0.0 +) +``` + +## API Reference + +### assign_z_coordinates + +```python +st.tl.assign_z_coordinates( + adatas, + spatial_key='spatial', + z_spacing=None, + tissue_thickness=None, + z_offset=0.0, + inplace=True +) +``` + +**Parameters:** + +- `adatas`: AnnData or List[AnnData] - Single or list of AnnData objects with 2D spatial coordinates +- `spatial_key`: str - Key in adata.obsm where spatial coordinates are stored +- `z_spacing`: float, List[float], or None - Spacing between slices +- `tissue_thickness`: float or None - Physical tissue thickness (overrides z_spacing if provided) +- `z_offset`: float - Starting z-coordinate for the first slice +- `inplace`: bool - Whether to modify input objects in place + +**Returns:** + +- None if inplace=True, otherwise modified AnnData object(s) + +## See Also + +- [Spatial Transcriptomics Alignment](spatial_transcriptomics_alignment.md) - Details on 2D slice alignment +- API Documentation: `st.tl.assign_z_coordinates` +- API Documentation: `st.tl.morpho_align` +- API Documentation: `st.pl.three_d_multi_plot` + +## References + +For more information about the 3D reconstruction methods in Spateo, please refer to: + +Qiu, X., Zhu, D.Y., Lu, Y. et al. Spatiotemporal modeling of molecular holograms. *Cell* (2024). https://doi.org/10.1016/j.cell.2024.10.022 diff --git a/docs/technicals/index.md b/docs/technicals/index.md index dc38e0bc..1b8529db 100755 --- a/docs/technicals/index.md +++ b/docs/technicals/index.md @@ -8,4 +8,6 @@ implementations in a more technical point of view. cell_segmentation digitization +spatial_transcriptomics_alignment +3d_reconstruction ``` diff --git a/spateo/align.py b/spateo/align.py index 7e1a9026..335b4011 100644 --- a/spateo/align.py +++ b/spateo/align.py @@ -1,4 +1,5 @@ from .alignment import ( + assign_z_coordinates, BA_transform, BA_transform_and_assignment, Mesh_correction, diff --git a/spateo/alignment/__init__.py b/spateo/alignment/__init__.py index 35e6c866..c5e75767 100644 --- a/spateo/alignment/__init__.py +++ b/spateo/alignment/__init__.py @@ -15,6 +15,7 @@ from .paste_alignment import paste_align, paste_align_ref from .transform import BA_transform, BA_transform_and_assignment, paste_transform from .utils import ( + assign_z_coordinates, downsampling, generate_label_transfer_prior, get_labels_based_on_coords, diff --git a/spateo/alignment/utils.py b/spateo/alignment/utils.py index 49e71cd8..9e43019d 100644 --- a/spateo/alignment/utils.py +++ b/spateo/alignment/utils.py @@ -512,6 +512,152 @@ def split_slice( # return adata_tps, lambda x: tps.transform(x) +def assign_z_coordinates( + adatas: Union[List[AnnData], AnnData], + spatial_key: str = "spatial", + z_spacing: Optional[Union[float, List[float]]] = None, + tissue_thickness: Optional[float] = None, + z_offset: float = 0.0, + inplace: bool = True, +) -> Union[List[AnnData], AnnData, None]: + """ + Assign z-coordinates to a list of 2D spatial slices for 3D reconstruction. + + This function facilitates 3D reconstruction from multiple 2D spatial transcriptomics slices by + assigning appropriate z-axis coordinates. It supports multiple spacing strategies including + uniform spacing, custom spacing per slice, or tissue thickness-based spacing. + + Args: + adatas: A single AnnData object or a list of AnnData objects representing sequential tissue slices. + Each should have 2D spatial coordinates in `obsm[spatial_key]`. + spatial_key: The key in `adata.obsm` where spatial coordinates are stored. Default is ``'spatial'``. + z_spacing: Spacing between consecutive slices along the z-axis. Can be: + - A single float value for uniform spacing between all slices + - A list of float values specifying the spacing after each slice (length should be n_slices - 1) + - None (default): automatically calculated as 1.0 for uniform spacing + tissue_thickness: Optional tissue thickness parameter. If provided, this value is used as uniform + z_spacing (overrides z_spacing parameter). This is useful when you know the + physical thickness of your tissue sections. + z_offset: Starting z-coordinate for the first slice. Default is 0.0. + inplace: If True, modifies the input AnnData objects in place. If False, returns modified copies. + Default is True. + + Returns: + If inplace is True, returns None. If inplace is False, returns the modified AnnData object(s) + (single AnnData or list of AnnData objects with 3D coordinates). + + Examples: + >>> # Example 1: Uniform spacing with default spacing of 1.0 + >>> slices_3d = st.tl.assign_z_coordinates(slice_list, spatial_key="spatial") + + >>> # Example 2: Uniform spacing with custom spacing + >>> slices_3d = st.tl.assign_z_coordinates( + ... slice_list, + ... spatial_key="align_spatial", + ... z_spacing=10.0, + ... inplace=False + ... ) + + >>> # Example 3: Using tissue thickness + >>> st.tl.assign_z_coordinates( + ... slice_list, + ... tissue_thickness=15.0, # 15 µm tissue sections + ... z_offset=0.0 + ... ) + + >>> # Example 4: Custom spacing between each slice + >>> # For 4 slices, provide 3 spacing values + >>> st.tl.assign_z_coordinates( + ... slice_list, + ... z_spacing=[10.0, 12.0, 10.0], # Variable spacing + ... inplace=True + ... ) + + Notes: + - Input slices should have 2D spatial coordinates (shape: N x 2) + - The function will add a third column (z-coordinate) to create 3D coordinates (shape: N x 3) + - If input already has 3D coordinates, the existing z-values will be overwritten + - For sequential slices, z-coordinates increase along the slice order + - The z-coordinate assignment follows the pattern: + * Slice 0: z = z_offset + * Slice i: z = z_offset + sum(z_spacing[0:i]) + + See Also: + - :func:`spateo.alignment.morpho_align`: For aligning multiple slices + - :func:`spateo.plotting.static.three_d_plot.multi_models`: For 3D visualization + """ + # Handle single AnnData input + is_single = not isinstance(adatas, list) + adata_list = [adatas] if is_single else adatas + + if len(adata_list) == 0: + raise ValueError("Empty list of AnnData objects provided.") + + # Determine z_spacing strategy + if tissue_thickness is not None: + # Use tissue thickness as uniform spacing + z_spacing_values = [tissue_thickness] * (len(adata_list) - 1) + elif z_spacing is None: + # Default uniform spacing of 1.0 + z_spacing_values = [1.0] * (len(adata_list) - 1) + elif isinstance(z_spacing, (int, float)): + # Single value: uniform spacing + z_spacing_values = [float(z_spacing)] * (len(adata_list) - 1) + elif isinstance(z_spacing, (list, tuple, np.ndarray)): + # List of values: custom spacing + z_spacing_values = list(z_spacing) + if len(z_spacing_values) != len(adata_list) - 1: + raise ValueError( + f"Length of z_spacing list ({len(z_spacing_values)}) must equal number of slices - 1 ({len(adata_list) - 1})" + ) + else: + raise ValueError( + "z_spacing must be None, a numeric value, or a list/array of numeric values." + ) + + # Calculate cumulative z-positions for each slice + z_positions = [z_offset] + for spacing in z_spacing_values: + z_positions.append(z_positions[-1] + spacing) + + # Process each slice + result_list = [] + for i, adata in enumerate(adata_list): + # Work on copy if not inplace + adata_proc = adata if inplace else adata.copy() + + # Get current spatial coordinates + if spatial_key not in adata_proc.obsm: + raise ValueError(f"Spatial key '{spatial_key}' not found in adata.obsm") + + coords = adata_proc.obsm[spatial_key] + + # Check dimensionality + if coords.shape[1] < 2: + raise ValueError( + f"Spatial coordinates must have at least 2 dimensions, got {coords.shape[1]}" + ) + + # Create z-coordinate column with the assigned z-position + n_cells = coords.shape[0] + z_coord = np.full((n_cells, 1), z_positions[i]) + + # Assign 3D coordinates + if coords.shape[1] == 2: + # Add z-coordinate as third dimension + adata_proc.obsm[spatial_key] = np.c_[coords, z_coord] + else: + # Replace existing z-coordinate (third column) + adata_proc.obsm[spatial_key][:, 2:3] = z_coord + + result_list.append(adata_proc) + + if inplace: + return None + else: + return result_list[0] if is_single else result_list + + def tps_deformation( adata, spatial_key, diff --git a/tests/alignment/test_utils.py b/tests/alignment/test_utils.py index 501662d0..57b7486a 100644 --- a/tests/alignment/test_utils.py +++ b/tests/alignment/test_utils.py @@ -6,6 +6,7 @@ from anndata import AnnData from spateo.alignment.methods.utils import check_rep_layer +from spateo.alignment.utils import assign_z_coordinates class TestCheckRepLayer(unittest.TestCase): @@ -86,5 +87,168 @@ def test_invalid_rep_field(self): check_rep_layer(self.samples, rep_layer=["layer1"], rep_field=["invalid"]) +class TestAssignZCoordinates(unittest.TestCase): + def setUp(self): + """Set up test fixtures with 2D spatial data""" + np.random.seed(42) + self.n_cells = 100 + self.n_genes = 50 + + # Create test slices with 2D spatial coordinates + self.slices_2d = [] + for i in range(4): + spatial_coords = np.random.rand(self.n_cells, 2) * 100 + adata = AnnData( + X=np.random.randn(self.n_cells, self.n_genes), + obsm={"spatial": spatial_coords} + ) + adata.obs["slice_id"] = i + self.slices_2d.append(adata) + + def test_default_spacing(self): + """Test default uniform spacing of 1.0""" + result = assign_z_coordinates(self.slices_2d, spatial_key="spatial", inplace=False) + + # Check that all slices now have 3D coordinates + for i, adata in enumerate(result): + self.assertEqual(adata.obsm["spatial"].shape[1], 3) + # Check z-coordinate is correct + expected_z = float(i) + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z) + + def test_custom_uniform_spacing(self): + """Test custom uniform spacing""" + spacing = 10.0 + result = assign_z_coordinates( + self.slices_2d, + spatial_key="spatial", + z_spacing=spacing, + inplace=False + ) + + # Check z-coordinates + for i, adata in enumerate(result): + expected_z = i * spacing + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z) + + def test_tissue_thickness_spacing(self): + """Test tissue thickness-based spacing""" + thickness = 15.0 + result = assign_z_coordinates( + self.slices_2d, + spatial_key="spatial", + tissue_thickness=thickness, + inplace=False + ) + + # Check z-coordinates + for i, adata in enumerate(result): + expected_z = i * thickness + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z) + + def test_variable_spacing(self): + """Test variable spacing between slices""" + spacings = [10.0, 15.0, 12.0] # For 4 slices, need 3 spacing values + result = assign_z_coordinates( + self.slices_2d, + spatial_key="spatial", + z_spacing=spacings, + inplace=False + ) + + # Check z-coordinates + expected_z_values = [0.0, 10.0, 25.0, 37.0] # Cumulative sum + for i, adata in enumerate(result): + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z_values[i]) + + def test_z_offset(self): + """Test z-offset parameter""" + offset = 100.0 + spacing = 5.0 + result = assign_z_coordinates( + self.slices_2d, + spatial_key="spatial", + z_spacing=spacing, + z_offset=offset, + inplace=False + ) + + # Check z-coordinates start from offset + for i, adata in enumerate(result): + expected_z = offset + (i * spacing) + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z) + + def test_inplace_modification(self): + """Test that inplace=True modifies original objects""" + slices_copy = [adata.copy() for adata in self.slices_2d] + result = assign_z_coordinates(slices_copy, spatial_key="spatial", inplace=True) + + # Should return None when inplace=True + self.assertIsNone(result) + + # Original slices should be modified + for i, adata in enumerate(slices_copy): + self.assertEqual(adata.obsm["spatial"].shape[1], 3) + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], float(i)) + + def test_single_slice(self): + """Test with single AnnData object""" + single_slice = self.slices_2d[0].copy() + result = assign_z_coordinates(single_slice, spatial_key="spatial", inplace=False) + + # Should return single AnnData + self.assertIsInstance(result, AnnData) + self.assertEqual(result.obsm["spatial"].shape[1], 3) + np.testing.assert_allclose(result.obsm["spatial"][:, 2], 0.0) + + def test_invalid_spacing_length(self): + """Test error when spacing list length is incorrect""" + invalid_spacings = [10.0, 15.0] # Wrong length for 4 slices + with self.assertRaises(ValueError): + assign_z_coordinates( + self.slices_2d, + spatial_key="spatial", + z_spacing=invalid_spacings, + inplace=False + ) + + def test_missing_spatial_key(self): + """Test error when spatial_key doesn't exist""" + with self.assertRaises(ValueError): + assign_z_coordinates( + self.slices_2d, + spatial_key="nonexistent_key", + inplace=False + ) + + def test_preserves_xy_coordinates(self): + """Test that original XY coordinates are preserved""" + original_xy = [adata.obsm["spatial"].copy() for adata in self.slices_2d] + result = assign_z_coordinates(self.slices_2d, spatial_key="spatial", inplace=False) + + # Check XY coordinates are unchanged + for i, adata in enumerate(result): + np.testing.assert_allclose(adata.obsm["spatial"][:, :2], original_xy[i]) + + def test_overwrite_existing_z(self): + """Test that existing z-coordinates are overwritten""" + # Create slices with existing 3D coordinates + slices_3d = [] + for i in range(3): + spatial_coords = np.random.rand(self.n_cells, 3) * 100 + adata = AnnData( + X=np.random.randn(self.n_cells, self.n_genes), + obsm={"spatial": spatial_coords} + ) + slices_3d.append(adata) + + result = assign_z_coordinates(slices_3d, spatial_key="spatial", z_spacing=20.0, inplace=False) + + # Check that z-coordinates were overwritten + for i, adata in enumerate(result): + expected_z = i * 20.0 + np.testing.assert_allclose(adata.obsm["spatial"][:, 2], expected_z) + + if __name__ == "__main__": unittest.main()