Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return DataArray from NdInfo #8

Merged
merged 2 commits into from
Oct 8, 2024
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# GitHub syntax highlighting
pixi.lock linguist-language=YAML linguist-generated=true
40 changes: 20 additions & 20 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: test

on:
push:
branches: [main, master]
branches: [main]
pull_request:
branches: [main, master]
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
Expand All @@ -16,27 +16,27 @@ env:

jobs:
run:
name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}
name: ${{ matrix.pixi-environment }} - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']

pixi-environment:
- py310-base
- py310-xarray
- py311-base
- py311-xarray
- py312-base
- py312-xarray
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Hatch
run: pip install --upgrade hatch

- name: Run static analysis
run: hatch fmt --check

- name: Run tests
run: hatch run cov
- uses: actions/checkout@v4

- name: Set up Pixi
uses: prefix-dev/[email protected]
with:
cache: true
environments: ${{ matrix.pixi-environment }}

- name: Run tests
run: pixi run --environment ${{ matrix.pixi-environment }} test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
dist
__pycache__
.coverage*
# pixi environments
.pixi
*.egg-info
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![PyPI - Version](https://img.shields.io/pypi/v/metamorph-mda-parser.svg)](https://pypi.org/project/metamorph-mda-parser)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/metamorph-mda-parser.svg)](https://pypi.org/project/metamorph-mda-parser)
[![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)

-----

Expand Down
7,804 changes: 7,804 additions & 0 deletions pixi.lock

Large diffs are not rendered by default.

71 changes: 50 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ dependencies = [
"pydantic",
]

[project.optional-dependencies]
xarray = [
"dask",
"numpy<2",
"tifffile",
"xarray",
"zarr",
]

[project.urls]
Documentation = "https://github.com/fmi-faim/metamorph-mda-parser#readme"
Issues = "https://github.com/fmi-faim/metamorph-mda-parser/issues"
Expand All @@ -41,27 +50,6 @@ dependencies = [
"coverage[toml]>=6.5",
"pytest",
]
[tool.hatch.envs.default.scripts]
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
"- coverage combine",
"coverage report",
]
cov = [
"test-cov",
"cov-report",
]

[[tool.hatch.envs.all.matrix]]
python = ["3.10", "3.11", "3.12"]

[tool.hatch.envs.types]
dependencies = [
"mypy>=1.0.0",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:src/metamorph_mda_parser tests}"

[tool.coverage.run]
source_pkgs = ["metamorph_mda_parser", "tests"]
Expand Down Expand Up @@ -93,3 +81,44 @@ runtime-evaluated-base-classes = ["pydantic.BaseModel"]
addopts = [
"--import-mode=importlib",
]

[tool.pixi.project]
channels = ["conda-forge"]
platforms = ["win-64", "linux-64", "osx-arm64"]

[tool.pixi.pypi-dependencies]
metamorph-mda-parser = { path = ".", editable = true }

[tool.pixi.environments]
default = { solve-group = "py312" }
xarray = { features = ["xarray"], solve-group = "py312" }
jupyter = { features = ["jupyter", "xarray"], solve-group = "py312" }
py310-base = { features = ["py310", "test"], solve-group = "py310" }
py310-xarray = { features = ["py310", "test", "xarray"], solve-group = "py310" }
py311-base = { features = ["py311", "test"], solve-group = "py311" }
py311-xarray = { features = ["py311", "test", "xarray"], solve-group = "py311" }
py312-base = { features = ["py312", "test"], solve-group = "py312" }
py312-xarray = { features = ["py312", "test", "xarray"], solve-group = "py312" }

[tool.pixi.tasks]

[tool.pixi.feature.jupyter.dependencies]
jupyter = "*"

[tool.pixi.feature.jupyter.tasks]
jupyter = "jupyter lab"

[tool.pixi.feature.test.dependencies]
pytest = "*"

[tool.pixi.feature.test.tasks]
test = "pytest"

[tool.pixi.feature.py310.dependencies]
python = "==3.10"

[tool.pixi.feature.py311.dependencies]
python = "==3.11"

[tool.pixi.feature.py312.dependencies]
python = "==3.12"
15 changes: 15 additions & 0 deletions src/metamorph_mda_parser/nd.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,18 @@ def get_files(self) -> pd.DataFrame:
"time",
],
)

def get_data_array(self, channels=None, positions=None, timepoints=None):
from metamorph_mda_parser.xarray import HAS_XARRAY
if HAS_XARRAY:
from metamorph_mda_parser.xarray import dataarray_from_dataframe
else:
raise ValueError("Dependencies for data array creation not found.")
files = self.get_files()
if channels:
files = files[files["channel"].isin(channels)]
if positions:
files = files[files["position"].isin(positions)]
if timepoints:
files = files[files["time"].isin(timepoints)]
return dataarray_from_dataframe(files, self.wave_do_z)
51 changes: 51 additions & 0 deletions src/metamorph_mda_parser/xarray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
try:
import dask.array as da
import xarray as xr
from tifffile.tifffile import imread
from pandas import DataFrame
HAS_XARRAY = True
except ImportError:
HAS_XARRAY = False


def dataarray_from_dataframe(df: "DataFrame", channels_3d: list[bool]):
required_columns = ["path", "channel", "position", "time"]
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
msg = f"Missing columns: {missing_columns}"
raise ValueError(msg)
if df["channel"].max() > len(channels_3d):
msg = f"No dimension information available for certain channels."
raise ValueError(msg)
data_arrays = [_load_file(row, channels_3d) for _, row in df.iterrows()]
return xr.combine_by_coords(data_arrays)["intensity"]


def _load_file(row, channels_3d: list[bool]):
path = row['path']
position = row['position']
time = row['time']
channel = row['channel']

chunks = (-1,) * (3 if channels_3d[channel] else 2)
with imread(path, aszarr=True) as store:
data = da.from_zarr(store, chunks=chunks)

# Determine if the array is 2D or 3D
if data.ndim == 2:
data = data[None, ...] # Add a dummy Z dimension

data_array = xr.DataArray(
data[None, None, None, ...], # Add singleton dimensions for position, time, and channel
dims=['position', 'time', 'channel', 'z', 'y', 'x'],
coords={
'position': [int(position)],
'time': [int(time)],
'channel': [int(channel)],
'z': range(data.shape[0]),
'y': range(data.shape[1]),
'x': range(data.shape[2])
}
)

return xr.Dataset({'intensity': data_array})
21 changes: 21 additions & 0 deletions tests/resources/data/sample_3ch_2pos_mixed-z.nd
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"NDInfoFile", Version 1.0
"Description", File recreated from images.
"StartTime1", 20241001 12:00:00.000
"DoTimelapse", FALSE
"DoStage", TRUE
"NStagePositions", 2
"Stage1", "Position1"
"Stage2", "Position2"
"DoWave", TRUE
"NWavelengths", 3
"WaveName1", "Conf640"
"WaveDoZ1", FALSE
"WaveName2", "Conf561"
"WaveDoZ2", TRUE
"WaveName3", "Conf488"
"WaveDoZ3", TRUE
"DoZSeries", TRUE
"NZSteps", 42
"ZStepSize", 3
"WaveInFileName", TRUE
"EndFile"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
25 changes: 25 additions & 0 deletions tests/test_xarray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest
from pathlib import Path

from metamorph_mda_parser.nd import NdInfo

pytest.importorskip("xarray")

@pytest.fixture
def sample_3ch_2pos_mixed_z():
return Path("tests/resources/data/sample_3ch_2pos_mixed-z.nd")

def test_data_array_from_nd(sample_3ch_2pos_mixed_z):
ndinfo = NdInfo.from_path(sample_3ch_2pos_mixed_z)
dataarray2d = ndinfo.get_data_array(channels=[0])
dataarray3d = ndinfo.get_data_array(channels=[1, 2])

assert dataarray2d.dims == ("position", "time", "channel", "z", "y", "x")
assert dataarray2d[0, 0, 0, 0, :, :].data.compute().tolist() == [[0,0], [0,128], [0,0]]
assert dataarray2d[1, 0, 0, 0, :, :].data.compute().tolist() == [[0,1], [0,128], [0,0]]

assert dataarray3d.dims == ("position", "time", "channel", "z", "y", "x")
assert dataarray3d[0, 0, 0, 0, :, :].data.compute().tolist() == [[1,0], [0,128], [0,0]]
assert dataarray3d[0, 0, 1, 0, :, :].data.compute().tolist() == [[2,0], [0,128], [0,0]]
assert dataarray3d[1, 0, 0, 0, :, :].data.compute().tolist() == [[1,1], [0,128], [0,0]]
assert dataarray3d[1, 0, 1, 0, :, :].data.compute().tolist() == [[2,1], [0,128], [0,0]]