Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .commitizen/cz_ngff_zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def _detect_package(self, config):
tag_format = config.settings.get("tag_format", "")
if "py-v" in tag_format:
return "py"
elif "mcp-v" in tag_format:
if "mcp-v" in tag_format:
return "mcp"
elif "ts-v" in tag_format:
if "ts-v" in tag_format:
return "ts"
return None

Expand Down Expand Up @@ -81,9 +81,9 @@ def _should_include_commit(self, scope: str | None, files: list[str]) -> bool:
# Handle package-specific scope filtering
if self.package == "py":
return self._should_include_for_py(scope, files)
elif self.package == "mcp":
if self.package == "mcp":
return self._should_include_for_mcp(scope, files)
elif self.package == "ts":
if self.package == "ts":
return self._should_include_for_ts(scope, files)

# If package unknown, include by default
Expand Down
17 changes: 11 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ default_install_hook_types:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: "v4.6.0"
rev: "v6.0.0"
hooks:
- id: check-added-large-files
# Example notebooks are stored with their outputs, figures included.
Expand Down Expand Up @@ -41,27 +41,32 @@ repos:
exclude: '^\.github/(agents/.*\.agent\.md|workflows/(.*\.lock\.yml|agentics-maintenance\.yml))$'

- repo: https://github.com/codespell-project/codespell
rev: "v2.2.5"
rev: "v2.4.3"
hooks:
- id: codespell
# Executed example notebooks embed base64 figures.
exclude: ^(ts/deno\.lock|py/examples/.*\.ipynb)$

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: "v0.9.0.5"
rev: "v0.11.0.1-1"
hooks:
- id: shellcheck

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.2
rev: v0.16.5
hooks:
- id: ruff
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
exclude: py/test/conftest.py
- id: ruff-format
# ruff-format also formats Python blocks in Markdown as of
# ruff-pre-commit v0.16. AGENTS.md documents formatting mistakes with
# deliberately misformatted "WRONG" examples, so formatting it would
# erase the distinction it is trying to draw.
exclude: ^AGENTS\.md$

- repo: https://github.com/commitizen-tools/commitizen
rev: "v4.13.0"
rev: "v4.18.0"
hooks:
- id: commitizen
stages: [commit-msg]
1 change: 1 addition & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ store wraps instead:
```python
# Before
from zarr.storage import LocalStore

multiscales = from_ngff_zarr(LocalStore("image.ome.zarr"))

# After
Expand Down
98 changes: 57 additions & 41 deletions docs/hcs.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,7 @@ The expected workflow for writing HCS data is to:
The plate metadata structure is the same for both v0.4 and v0.5. The version is specified when creating the `Plate` object:

```python
from ngff_zarr import (
Plate, PlateColumn, PlateRow, PlateWell, PlateAcquisition
)
from ngff_zarr import Plate, PlateColumn, PlateRow, PlateWell, PlateAcquisition

# Define plate structure
columns = [PlateColumn(name="1"), PlateColumn(name="2"), PlateColumn(name="3")]
Expand Down Expand Up @@ -308,6 +306,7 @@ with HCSPlateWriter("my_screen.ozx", plate_metadata) as writer:
```python
from concurrent.futures import ThreadPoolExecutor


def write_well(writer, well_data):
writer.write_well_image(
multiscales=well_data.image,
Expand All @@ -316,6 +315,7 @@ def write_well(writer, well_data):
field_index=well_data.field,
)


with HCSPlateWriter("plate.ozx", plate_metadata) as writer:
with ThreadPoolExecutor(max_workers=4) as executor:
executor.map(lambda wd: write_well(writer, wd), well_data_list)
Expand Down Expand Up @@ -361,8 +361,8 @@ from ngff_zarr import write_store_to_zip
# Convert an existing plate to .ozx
write_store_to_zip(
"existing_plate.ome.zarr", # Source plate directory
"plate.ozx", # Output .ozx file
version="0.5" # Required for .ozx
"plate.ozx", # Output .ozx file
version="0.5", # Required for .ozx
)
```

Expand Down Expand Up @@ -449,33 +449,33 @@ ome_zarr_version = "0.4"

# Create plate layout for a 96-well plate (subset)
columns = [PlateColumn(name=str(i)) for i in range(1, 13)] # 12 columns
rows = [PlateRow(name=chr(65 + i)) for i in range(8)] # 8 rows (A-H)
rows = [PlateRow(name=chr(65 + i)) for i in range(8)] # 8 rows (A-H)

wells = []
for row_idx, row in enumerate(rows):
for col_idx, col in enumerate(columns):
wells.append(PlateWell(
path=f"{row.name}/{col.name}",
rowIndex=row_idx,
columnIndex=col_idx
))
wells.append(
PlateWell(
path=f"{row.name}/{col.name}", rowIndex=row_idx, columnIndex=col_idx
)
)

plate_metadata = Plate(
name="Compound Screen - Plate 1",
columns=columns,
rows=rows,
wells=wells,
field_count=4, # 4 fields per well
version=ome_zarr_version # Use consistent version
version=ome_zarr_version, # Use consistent version
)


# Function to create synthetic field data
def create_field_image(treatment_effect=1.0):
# Create synthetic microscopy data: T, C, Z, Y, X
base_intensity = 100
data = np.random.poisson(
base_intensity * treatment_effect,
size=(1, 2, 10, 512, 512)
base_intensity * treatment_effect, size=(1, 2, 10, 512, 512)
).astype(np.uint16)

ngff_image = NgffImage(
Expand All @@ -488,6 +488,7 @@ def create_field_image(treatment_effect=1.0):

return to_multiscales(ngff_image, scale_factors=[2, 4])


# Simulate acquisition workflow
plate_store = "drug_screen_plate1.ome.zarr"

Expand All @@ -498,7 +499,6 @@ to_hcs_zarr(hcs_plate, plate_store)
# Iterate through wells as they are imaged
for row in ["A", "B", "C"]: # First 3 rows for demo
for col in ["1", "2", "3", "4"]: # First 4 columns for demo

# Simulate different treatment effects
if row == "A":
effect = 0.5 # Inhibitor
Expand Down Expand Up @@ -529,7 +529,9 @@ print(f"Screening complete! Plate saved to: {plate_store}")
# Verify the written data
final_plate = nz.from_hcs_zarr(plate_store)
print(f"Plate: {final_plate.name}")
print(f"Wells written: {len([w for w in final_plate.wells if final_plate.get_well(final_plate.rows[w.rowIndex].name, final_plate.columns[w.columnIndex].name) is not None])}")
print(
f"Wells written: {len([w for w in final_plate.wells if final_plate.get_well(final_plate.rows[w.rowIndex].name, final_plate.columns[w.columnIndex].name) is not None])}"
)
```

### Complete Example: v0.5 Drug Screening Workflow
Expand All @@ -546,16 +548,16 @@ ome_zarr_version = "0.5"

# Create plate layout for v0.5
columns = [PlateColumn(name=str(i)) for i in range(1, 4)] # 3 columns
rows = [PlateRow(name=chr(65 + i)) for i in range(2)] # 2 rows (A-B)
rows = [PlateRow(name=chr(65 + i)) for i in range(2)] # 2 rows (A-B)

wells = []
for row_idx, row in enumerate(rows):
for col_idx, col in enumerate(columns):
wells.append(PlateWell(
path=f"{row.name}/{col.name}",
rowIndex=row_idx,
columnIndex=col_idx
))
wells.append(
PlateWell(
path=f"{row.name}/{col.name}", rowIndex=row_idx, columnIndex=col_idx
)
)

plate_metadata = Plate(
name="v0.5 Screening Plate",
Expand All @@ -566,12 +568,12 @@ plate_metadata = Plate(
version=ome_zarr_version, # Use consistent version
)


# Function to create synthetic field data
def create_field_image(treatment_effect=1.0):
base_intensity = 100
data = np.random.poisson(
base_intensity * treatment_effect,
size=(1, 2, 10, 512, 512)
base_intensity * treatment_effect, size=(1, 2, 10, 512, 512)
).astype(np.uint16)

ngff_image = NgffImage(
Expand All @@ -584,6 +586,7 @@ def create_field_image(treatment_effect=1.0):

return to_multiscales(ngff_image, scale_factors=[2, 4])


# Create plate structure
plate_store = "screening_v05.ome.zarr"
hcs_plate = HCSPlate(store=plate_store, plate_metadata=plate_metadata)
Expand Down Expand Up @@ -682,17 +685,21 @@ wells = [
]

plate_metadata = Plate(
columns=columns, rows=rows, wells=wells,
name="Example Plate v0.4", field_count=1,
version=ome_zarr_version
columns=columns,
rows=rows,
wells=wells,
name="Example Plate v0.4",
field_count=1,
version=ome_zarr_version,
)

# Create synthetic image data
data = np.random.randint(0, 255, size=(1, 1, 10, 256, 256), dtype=np.uint8)
ngff_image = nz.NgffImage(
data=data, dims=["t", "c", "z", "y", "x"],
data=data,
dims=["t", "c", "z", "y", "x"],
scale={"t": 1.0, "c": 1.0, "z": 0.5, "y": 0.325, "x": 0.325},
translation={"t": 0.0, "c": 0.0, "z": 0.0, "y": 0.0, "x": 0.0}
translation={"t": 0.0, "c": 0.0, "z": 0.0, "y": 0.0, "x": 0.0},
)
multiscales = nz.to_multiscales(ngff_image)

Expand Down Expand Up @@ -744,17 +751,21 @@ wells = [
]

plate_metadata = Plate(
columns=columns, rows=rows, wells=wells,
name="Example Plate v0.5", field_count=1,
version=ome_zarr_version
columns=columns,
rows=rows,
wells=wells,
name="Example Plate v0.5",
field_count=1,
version=ome_zarr_version,
)

# Create synthetic image data
data = np.random.randint(0, 255, size=(1, 1, 10, 256, 256), dtype=np.uint8)
ngff_image = nz.NgffImage(
data=data, dims=["t", "c", "z", "y", "x"],
data=data,
dims=["t", "c", "z", "y", "x"],
scale={"t": 1.0, "c": 1.0, "z": 0.5, "y": 0.325, "x": 0.325},
translation={"t": 0.0, "c": 0.0, "z": 0.0, "y": 0.0, "x": 0.0}
translation={"t": 0.0, "c": 0.0, "z": 0.0, "y": 0.0, "x": 0.0},
)
multiscales = nz.to_multiscales(ngff_image)

Expand Down Expand Up @@ -789,8 +800,13 @@ These approaches allow you to write individual well images as they are acquired

```python
from ngff_zarr.v04.zarr_metadata import (
Plate, PlateColumn, PlateRow, PlateWell,
Well, WellImage, PlateAcquisition
Plate,
PlateColumn,
PlateRow,
PlateWell,
Well,
WellImage,
PlateAcquisition,
)

# Define plate structure
Expand All @@ -811,7 +827,7 @@ plate_metadata = Plate(
columns=columns,
rows=rows,
wells=wells,
field_count=2 # Number of fields per well
field_count=2, # Number of fields per well
)
```

Expand Down Expand Up @@ -961,14 +977,14 @@ memory usage:
import ngff_zarr as nz

# Configure cache sizes globally
nz.config.hcs_well_cache_size = 100 # Max wells to cache per plate
nz.config.hcs_image_cache_size = 50 # Max images to cache per well
nz.config.hcs_well_cache_size = 100 # Max wells to cache per plate
nz.config.hcs_image_cache_size = 50 # Max images to cache per well

# Or configure per operation
plate = nz.from_hcs_zarr(
"plate.zarr",
well_cache_size=50, # Custom well cache size
image_cache_size=25 # Custom image cache size
well_cache_size=50, # Custom well cache size
image_cache_size=25, # Custom image cache size
)
```

Expand Down
3 changes: 2 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ With, for example, the [Pyodide REPL] or [JupyterLite]

```python
import micropip
await micropip.install('ngff-zarr')

await micropip.install("ngff-zarr")
```

:::
Expand Down
5 changes: 1 addition & 4 deletions docs/lif.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,7 @@ with LifFile("tile_scan.lif") as lif:
# Check if image has mosaic dimension
if has_mosaic_dimension(lif_image):
# Convert to HCS plate structure
plate_metadata, well_images = lif_to_hcs_plate(
lif_image,
plate_name="TileScan"
)
plate_metadata, well_images = lif_to_hcs_plate(lif_image, plate_name="TileScan")

# Write as HCS plate
with HCSPlateWriter("tile_scan.ome.zarr", plate_metadata) as writer:
Expand Down
2 changes: 1 addition & 1 deletion docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ agent:
The `ngff-zarr-mcp` Model Context Protocol (MCP) server converts datasets to the
OME-Zarr scientific imaging data format, optimizes the compression codec,
ensures a limited number of files are generated, and creates a Python script for
re-use.
reuse.

<script src="https://asciinema.org/a/726628.js" id="asciicast-726628" async="true"></script>

Expand Down
Loading
Loading