xenium-splitter 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,6 @@
1
+ """xenium-splitter package."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __all__ = ["__version__"]
6
+ __version__ = version("xenium-splitter")
xenium_splitter/cli.py ADDED
@@ -0,0 +1,237 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ import shutil
6
+ import tempfile
7
+
8
+ import typer
9
+
10
+ from xenium_splitter.estimator import estimate_ram
11
+ from xenium_splitter.models import SplitConfig
12
+ from xenium_splitter.show_regions import create_regions_preview
13
+ from xenium_splitter.splitter import run_split
14
+
15
+ app = typer.Typer(help="Split Xenium outputs by LASSO regions.")
16
+
17
+
18
+ def _configure_app_logging(verbose: bool) -> None:
19
+ """Configure logging for xenium_splitter only.
20
+
21
+ Avoid configuring the root logger so dependency loggers like zarr do not
22
+ emit through this CLI unless the caller configures them separately.
23
+ """
24
+ package_logger = logging.getLogger("xenium_splitter")
25
+ package_logger.handlers.clear()
26
+ package_logger.setLevel(logging.DEBUG if verbose else logging.WARNING)
27
+ package_logger.propagate = False
28
+
29
+ handler = logging.StreamHandler()
30
+ handler.setLevel(logging.DEBUG if verbose else logging.WARNING)
31
+ handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
32
+ package_logger.addHandler(handler)
33
+
34
+
35
+ @app.command("clean-temp")
36
+ def clean_temp_command() -> None:
37
+ """Delete xenium-splitter temporary working files."""
38
+ temp_root = Path(tempfile.gettempdir()) / "xenium_splitter"
39
+ if not temp_root.exists():
40
+ typer.echo(f"No temp directory found at: {temp_root}")
41
+ return
42
+
43
+ shutil.rmtree(temp_root)
44
+ typer.echo(f"Deleted temp directory: {temp_root}")
45
+
46
+
47
+ @app.command("show-regions")
48
+ def show_regions_command(
49
+ input_dir: Path = typer.Option(..., exists=True, file_okay=False, dir_okay=True),
50
+ lasso_file: Path = typer.Option(..., exists=True, file_okay=True, dir_okay=False),
51
+ output_image: Path = typer.Option(..., file_okay=True, dir_okay=False),
52
+ he_image: Path | None = typer.Option(None, exists=True, file_okay=True, dir_okay=False),
53
+ max_dimension_px: int = typer.Option(
54
+ 2000,
55
+ min=1,
56
+ max=2000,
57
+ help="Maximum width or height of the generated preview image.",
58
+ ),
59
+ verbose: bool = typer.Option(
60
+ False,
61
+ "-v",
62
+ "--verbose",
63
+ help="Enable debug logging to see detailed processing info.",
64
+ ),
65
+ ) -> None:
66
+ """Create a low-resolution preview of LASSO regions over H&E or morphology.
67
+
68
+ H&E is preferred; morphology is used as a fallback when no H&E-like image
69
+ can be found in the input directory.
70
+ """
71
+ _configure_app_logging(verbose)
72
+ result = create_regions_preview(
73
+ input_dir=input_dir,
74
+ lasso_file=lasso_file,
75
+ output_image=output_image,
76
+ he_image=he_image,
77
+ max_dimension_px=max_dimension_px,
78
+ )
79
+ typer.echo("show_regions complete.")
80
+ typer.echo(f"Source image type: {result['source_kind']}")
81
+ typer.echo(f"Source image: {result['source_path']}")
82
+ typer.echo(f"Regions drawn: {result['region_count']}")
83
+ typer.echo(f"Output image: {result['output_path']}")
84
+
85
+
86
+ @app.command("estimate-ram")
87
+ def estimate_ram_command(
88
+ input_dir: Path = typer.Option(..., exists=True, file_okay=False, dir_okay=True),
89
+ lasso_file: Path = typer.Option(..., exists=True, file_okay=True, dir_okay=False),
90
+ output_dir: Path = typer.Option(..., file_okay=False, dir_okay=True),
91
+ he_image: Path | None = typer.Option(None, exists=True, file_okay=True, dir_okay=False),
92
+ convert_svs_to_ome: bool = typer.Option(
93
+ False, help="If --he-image is .svs, also emit OME-TIFF outputs.",
94
+ ),
95
+ squash_layers: bool = typer.Option(
96
+ True, "--squash-layers/--no-squash-layers",
97
+ help="Flatten multi-layer image stacks when possible.",
98
+ ),
99
+ include_glob: list[str] = typer.Option(
100
+ None, help="Optional glob patterns (repeatable) to limit files from input-dir.",
101
+ ),
102
+ skip_images: bool = typer.Option(
103
+ False, "--skip-images/--process-images", help="Skip image extraction.",
104
+ ),
105
+ overlays: bool = typer.Option(
106
+ False, "--overlays/--no-overlays", help="Write morphology grid overlays.",
107
+ ),
108
+ recalculate_diffexp: bool = typer.Option(
109
+ True, "--recalculate-diffexp/--skip-diffexp-recalc",
110
+ help="Recompute analysis/diffexp outputs.",
111
+ ),
112
+ write_cell_feature_matrix_zarr: bool = typer.Option(
113
+ True, "--write-cell-feature-matrix-zarr/--skip-cell-feature-matrix-zarr",
114
+ help="Write cell_feature_matrix.zarr.zip outputs.",
115
+ ),
116
+ copy_transcripts: bool = typer.Option(
117
+ False, "--copy-transcripts", help="Copy transcript files without filtering or rebasing.",
118
+ ),
119
+ images_only: bool = typer.Option(
120
+ False, "--images-only", help="Process only images; conflicts with --skip-images.",
121
+ ),
122
+ verbose: bool = typer.Option(
123
+ False, "-v", "--verbose", help="Enable debug logging.",
124
+ ),
125
+ ) -> None:
126
+ """Estimate peak RAM without splitting files or creating output directories."""
127
+ _configure_app_logging(verbose)
128
+ if images_only and skip_images:
129
+ raise typer.BadParameter(
130
+ "--images-only and --skip-images are mutually exclusive: "
131
+ "--images-only processes only images while --skip-images skips all images."
132
+ )
133
+ try:
134
+ peak_ram_gb = estimate_ram(input_dir)
135
+ except (FileNotFoundError, OSError) as exc:
136
+ raise typer.BadParameter(str(exc), param_hint="--input-dir") from exc
137
+
138
+ typer.echo(f"Estimated peak RAM: {peak_ram_gb:.2f} GB")
139
+ typer.echo("Archive sizes use decimal MB (1 MB = 1,000,000 bytes).")
140
+ typer.echo("Only archive sizes affect this equation; split options are not modeled.")
141
+ typer.echo("No files were written.")
142
+ typer.echo("This is a rough peak estimate, not a guaranteed RAM limit.")
143
+
144
+
145
+ @app.command("split")
146
+ def split_command(
147
+ input_dir: Path = typer.Option(..., exists=True, file_okay=False, dir_okay=True),
148
+ lasso_file: Path = typer.Option(..., exists=True, file_okay=True, dir_okay=False),
149
+ output_dir: Path = typer.Option(..., file_okay=False, dir_okay=True),
150
+ he_image: Path | None = typer.Option(None, exists=True, file_okay=True, dir_okay=False),
151
+ convert_svs_to_ome: bool = typer.Option(
152
+ False,
153
+ help="If --he-image is .svs, also emit OME-TIFF outputs.",
154
+ ),
155
+ squash_layers: bool = typer.Option(
156
+ True,
157
+ "--squash-layers/--no-squash-layers",
158
+ help="Flatten multi-layer image stacks when possible.",
159
+ ),
160
+ include_glob: list[str] = typer.Option(
161
+ None,
162
+ help="Optional glob patterns (repeatable) to limit files from input-dir.",
163
+ ),
164
+ skip_images: bool = typer.Option(
165
+ False,
166
+ "--skip-images/--process-images",
167
+ help="Skip image extraction to reduce memory usage.",
168
+ ),
169
+ overlays: bool = typer.Option(
170
+ False,
171
+ "--overlays/--no-overlays",
172
+ help="Write region grid overlay images when a morphology_mip.ome.tif output is available.",
173
+ ),
174
+ recalculate_diffexp: bool = typer.Option(
175
+ True,
176
+ "--recalculate-diffexp/--skip-diffexp-recalc",
177
+ help="Recompute analysis/diffexp files from region-filtered matrix and clustering outputs.",
178
+ ),
179
+ write_cell_feature_matrix_zarr: bool = typer.Option(
180
+ True,
181
+ "--write-cell-feature-matrix-zarr/--skip-cell-feature-matrix-zarr",
182
+ help="Write cell_feature_matrix.zarr.zip outputs (disable to benchmark runtime impact).",
183
+ ),
184
+ copy_transcripts: bool = typer.Option(
185
+ False,
186
+ "--copy-transcripts",
187
+ help="Copy transcript files verbatim to each region output with no filtering or rebasing.",
188
+ ),
189
+ images_only: bool = typer.Option(
190
+ False,
191
+ "--images-only",
192
+ help=(
193
+ "Process only image files (morphology, H&E); skip all tabular data, zarr, CFM, and "
194
+ "diffexp stages. Useful for image-only re-runs after a data-only pass, or when RAM "
195
+ "must be reserved entirely for image processing. Mutually exclusive with --skip-images."
196
+ ),
197
+ ),
198
+ verbose: bool = typer.Option(
199
+ False,
200
+ "-v",
201
+ "--verbose",
202
+ help="Enable debug logging to see detailed processing info.",
203
+ ),
204
+ ) -> None:
205
+ """Split Xenium files and optional H&E image into per-region outputs."""
206
+ _configure_app_logging(verbose)
207
+ if images_only and skip_images:
208
+ raise typer.BadParameter(
209
+ "--images-only and --skip-images are mutually exclusive: "
210
+ "--images-only processes only images while --skip-images skips all images."
211
+ )
212
+ config = SplitConfig(
213
+ input_dir=input_dir,
214
+ lasso_file=lasso_file,
215
+ output_dir=output_dir,
216
+ he_image=he_image,
217
+ convert_svs_to_ome=convert_svs_to_ome,
218
+ squash_layers=squash_layers,
219
+ include_globs=include_glob or [],
220
+ skip_images=skip_images,
221
+ overlays=overlays,
222
+ recalculate_diffexp=recalculate_diffexp,
223
+ write_cell_feature_matrix_zarr=write_cell_feature_matrix_zarr,
224
+ copy_transcripts=copy_transcripts,
225
+ images_only=images_only,
226
+ )
227
+ metrics, metadata_path = run_split(config)
228
+ typer.echo("Split complete.")
229
+ typer.echo(f"Regions: {metrics.region_count}")
230
+ typer.echo(f"Files processed: {metrics.files_processed}")
231
+ typer.echo(f"Files skipped: {metrics.files_skipped}")
232
+ typer.echo(f"Files failed: {metrics.files_failed}")
233
+ typer.echo(f"Metadata: {metadata_path}")
234
+
235
+
236
+ if __name__ == "__main__":
237
+ app()
@@ -0,0 +1,20 @@
1
+ """Estimate peak RAM from the encoded Xenium Zarr archive sizes."""
2
+ from pathlib import Path
3
+
4
+
5
+ def estimate_ram(input_dir: Path) -> float:
6
+ """Return estimated peak RAM in GB using archive sizes in decimal MB.
7
+
8
+ Peak RAM = 2.5 + 0.010 * transcript MB + 0.007 * cell MB.
9
+ Both archives must exist directly inside the Xenium output directory.
10
+ No archive contents are read or extracted.
11
+ """
12
+ transcript_path = input_dir / "transcripts.zarr.zip"
13
+ cell_path = input_dir / "cells.zarr.zip"
14
+ for archive_path in (transcript_path, cell_path):
15
+ if not archive_path.is_file():
16
+ raise FileNotFoundError(f"Required Zarr archive not found: {archive_path}")
17
+
18
+ transcript_mb = transcript_path.stat().st_size / 1_000_000
19
+ cell_mb = cell_path.stat().st_size / 1_000_000
20
+ return 2.5 + 0.010 * transcript_mb + 0.007 * cell_mb