voxeldata 0.0.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.
- voxeldata/__init__.py +82 -0
- voxeldata/_coordinates.py +201 -0
- voxeldata/_dims.py +21 -0
- voxeldata/accessors.py +606 -0
- voxeldata/affine.py +470 -0
- voxeldata/create.py +573 -0
- voxeldata/geometry.py +1947 -0
- voxeldata/py.typed +0 -0
- voxeldata/validation.py +559 -0
- voxeldata-0.0.1.dist-info/METADATA +143 -0
- voxeldata-0.0.1.dist-info/RECORD +13 -0
- voxeldata-0.0.1.dist-info/WHEEL +4 -0
- voxeldata-0.0.1.dist-info/licenses/LICENSE +27 -0
voxeldata/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""VoxelData: spatially referenced voxel arrays built on xarray.
|
|
2
|
+
|
|
3
|
+
VoxelData defines a small, modality-independent data model for arrays gridded in
|
|
4
|
+
space. A VoxelData array is an `xarray.DataArray` with native voxel dimensions
|
|
5
|
+
`(k, j, i)` (plus any extra leading dimensions) and a single
|
|
6
|
+
[VoxelToWorldIndex][voxeldata.geometry.VoxelToWorldIndex] that lazily derives world
|
|
7
|
+
coordinates `(z, y, x)` from a voxel-to-world affine.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from voxeldata._dims import CORE_DIMS, GRID_DIM, TIME_DIM, VOXEL_DIMS, WORLD_DIMS
|
|
11
|
+
from voxeldata.accessors import VoxelDataAccessor
|
|
12
|
+
from voxeldata.affine import (
|
|
13
|
+
apply_affine,
|
|
14
|
+
get_bounding_box,
|
|
15
|
+
get_relative_affine,
|
|
16
|
+
reindex_voxels,
|
|
17
|
+
reindex_voxels_like,
|
|
18
|
+
)
|
|
19
|
+
from voxeldata.create import create_voxeldata
|
|
20
|
+
from voxeldata.geometry import (
|
|
21
|
+
VoxelToWorldIndex,
|
|
22
|
+
VoxelToWorldTransform,
|
|
23
|
+
attach_voxel_to_world_index,
|
|
24
|
+
convert_length_units,
|
|
25
|
+
get_affine_axis_scalings,
|
|
26
|
+
get_affine_direction_matrix,
|
|
27
|
+
get_voxel_to_world_affine,
|
|
28
|
+
get_voxel_to_world_direction_matrix,
|
|
29
|
+
get_voxel_to_world_index,
|
|
30
|
+
get_voxel_to_world_index_origin,
|
|
31
|
+
get_voxel_to_world_index_spacing,
|
|
32
|
+
get_voxel_to_world_spatial_dims,
|
|
33
|
+
get_voxel_to_world_units,
|
|
34
|
+
has_axis_aligned_voxel_to_world_index,
|
|
35
|
+
has_voxel_to_world_index,
|
|
36
|
+
require_scalar_grid_affine,
|
|
37
|
+
restore_voxel_to_world_index,
|
|
38
|
+
)
|
|
39
|
+
from voxeldata.validation import (
|
|
40
|
+
canonicalize_voxeldata,
|
|
41
|
+
ensure_voxeldata,
|
|
42
|
+
require_dataarray,
|
|
43
|
+
require_positive_finite,
|
|
44
|
+
validate_voxeldata,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"CORE_DIMS",
|
|
49
|
+
"GRID_DIM",
|
|
50
|
+
"TIME_DIM",
|
|
51
|
+
"VOXEL_DIMS",
|
|
52
|
+
"WORLD_DIMS",
|
|
53
|
+
"VoxelDataAccessor",
|
|
54
|
+
"VoxelToWorldIndex",
|
|
55
|
+
"VoxelToWorldTransform",
|
|
56
|
+
"apply_affine",
|
|
57
|
+
"attach_voxel_to_world_index",
|
|
58
|
+
"canonicalize_voxeldata",
|
|
59
|
+
"convert_length_units",
|
|
60
|
+
"create_voxeldata",
|
|
61
|
+
"ensure_voxeldata",
|
|
62
|
+
"get_affine_axis_scalings",
|
|
63
|
+
"get_affine_direction_matrix",
|
|
64
|
+
"get_bounding_box",
|
|
65
|
+
"get_relative_affine",
|
|
66
|
+
"get_voxel_to_world_affine",
|
|
67
|
+
"get_voxel_to_world_direction_matrix",
|
|
68
|
+
"get_voxel_to_world_index",
|
|
69
|
+
"get_voxel_to_world_index_origin",
|
|
70
|
+
"get_voxel_to_world_index_spacing",
|
|
71
|
+
"get_voxel_to_world_spatial_dims",
|
|
72
|
+
"get_voxel_to_world_units",
|
|
73
|
+
"has_axis_aligned_voxel_to_world_index",
|
|
74
|
+
"has_voxel_to_world_index",
|
|
75
|
+
"reindex_voxels",
|
|
76
|
+
"reindex_voxels_like",
|
|
77
|
+
"require_dataarray",
|
|
78
|
+
"require_positive_finite",
|
|
79
|
+
"require_scalar_grid_affine",
|
|
80
|
+
"restore_voxel_to_world_index",
|
|
81
|
+
"validate_voxeldata",
|
|
82
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Coordinate spacing helpers shared across voxeldata modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
import xarray as xr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_representative_step(
|
|
13
|
+
values: npt.NDArray[np.floating], uniformity_tolerance: float = 1e-2
|
|
14
|
+
) -> tuple[float | None, bool]:
|
|
15
|
+
"""Return a representative step size for a 1D coordinate array.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
values : numpy.ndarray
|
|
20
|
+
One-dimensional coordinate values.
|
|
21
|
+
uniformity_tolerance : float, default: 1e-2
|
|
22
|
+
Maximum allowed per-interval relative deviation from the median consecutive
|
|
23
|
+
difference. Each interval must satisfy
|
|
24
|
+
`|interval - median| <= uniformity_tolerance * |median|` for the coordinate
|
|
25
|
+
to be considered uniform.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
float or None
|
|
30
|
+
Exact step size when sampling is uniform, the median consecutive difference
|
|
31
|
+
when sampling is non-uniform, or `None` when fewer than two values are
|
|
32
|
+
provided.
|
|
33
|
+
bool
|
|
34
|
+
Whether the returned step is a median approximation derived from non-uniform
|
|
35
|
+
spacing.
|
|
36
|
+
"""
|
|
37
|
+
if len(values) < 2:
|
|
38
|
+
return None, False
|
|
39
|
+
|
|
40
|
+
diffs = np.diff(values)
|
|
41
|
+
median = float(np.median(diffs))
|
|
42
|
+
if np.isclose(median, 0.0):
|
|
43
|
+
is_uniform = bool(np.isclose(np.max(diffs), np.min(diffs)))
|
|
44
|
+
else:
|
|
45
|
+
is_uniform = bool(
|
|
46
|
+
np.allclose(diffs, median, rtol=uniformity_tolerance, atol=0.0)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if is_uniform:
|
|
50
|
+
return median, False
|
|
51
|
+
return median, True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CoordinateSpacingInfo:
|
|
55
|
+
"""Result of per-dimension spacing analysis.
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
value : float or None
|
|
60
|
+
Exact spacing when the coordinate is uniform. `None` otherwise, including for
|
|
61
|
+
single-point coordinates.
|
|
62
|
+
median : float or None
|
|
63
|
+
Median consecutive difference for numeric coordinates with two or more points.
|
|
64
|
+
Available for both uniform and non-uniform coordinates; `None` for missing,
|
|
65
|
+
non-numeric, or single-point coordinates.
|
|
66
|
+
warn_msg : str or None
|
|
67
|
+
Warning message to emit, or `None` if no warning is needed.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
__slots__ = ("median", "value", "warn_msg")
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
value: float | None,
|
|
75
|
+
median: float | None,
|
|
76
|
+
warn_msg: str | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.value = value
|
|
79
|
+
self.median = median
|
|
80
|
+
self.warn_msg = warn_msg
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_coordinate_spacing_info(
|
|
84
|
+
dim: str,
|
|
85
|
+
data: xr.DataArray,
|
|
86
|
+
uniformity_tolerance: float,
|
|
87
|
+
) -> CoordinateSpacingInfo:
|
|
88
|
+
"""Compute coordinate spacing information for a single dimension.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
dim : str
|
|
93
|
+
Dimension name.
|
|
94
|
+
data : xarray.DataArray
|
|
95
|
+
DataArray whose coordinate to inspect.
|
|
96
|
+
uniformity_tolerance : float
|
|
97
|
+
Maximum allowed per-interval relative deviation from the median consecutive
|
|
98
|
+
difference.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
CoordinateSpacingInfo
|
|
103
|
+
Spacing result for the dimension.
|
|
104
|
+
"""
|
|
105
|
+
if dim not in data.coords:
|
|
106
|
+
return CoordinateSpacingInfo(
|
|
107
|
+
value=None,
|
|
108
|
+
median=None,
|
|
109
|
+
warn_msg=f"Dimension '{dim}' has no coordinate; spacing is undefined.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
coord = data.coords[dim]
|
|
113
|
+
if not np.issubdtype(coord.dtype, np.integer) and not np.issubdtype(
|
|
114
|
+
coord.dtype, np.floating
|
|
115
|
+
):
|
|
116
|
+
return CoordinateSpacingInfo(value=None, median=None, warn_msg=None)
|
|
117
|
+
|
|
118
|
+
# A coordinate named after its dim is not necessarily 1D (e.g. a grid-dependent
|
|
119
|
+
# "time" coordinate can be (time, grid)-shaped). Compute the step along `dim`
|
|
120
|
+
# independently for every combination of the other dims' values.
|
|
121
|
+
other_dims = [d for d in coord.dims if d != dim]
|
|
122
|
+
dim_axis = coord.dims.index(dim)
|
|
123
|
+
|
|
124
|
+
if coord.shape[dim_axis] < 2:
|
|
125
|
+
return CoordinateSpacingInfo(
|
|
126
|
+
value=None,
|
|
127
|
+
median=None,
|
|
128
|
+
warn_msg=(
|
|
129
|
+
f"Dimension '{dim}' has a single coordinate point; spacing is "
|
|
130
|
+
"undefined."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
values = np.moveaxis(coord.values, dim_axis, 0).reshape(coord.shape[dim_axis], -1)
|
|
135
|
+
|
|
136
|
+
steps: list[float] = []
|
|
137
|
+
for column in range(values.shape[1]):
|
|
138
|
+
step, is_approximate = get_representative_step(
|
|
139
|
+
values[:, column], uniformity_tolerance=uniformity_tolerance
|
|
140
|
+
)
|
|
141
|
+
assert step is not None # values.shape[0] >= 2, checked above.
|
|
142
|
+
if is_approximate:
|
|
143
|
+
return CoordinateSpacingInfo(
|
|
144
|
+
value=None,
|
|
145
|
+
median=float(np.median(steps + [step])),
|
|
146
|
+
warn_msg=(
|
|
147
|
+
f"Coordinate '{dim}' has non-uniform sampling; spacing is "
|
|
148
|
+
"undefined."
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
steps.append(step)
|
|
152
|
+
|
|
153
|
+
if other_dims and not np.allclose(
|
|
154
|
+
steps, steps[0], rtol=uniformity_tolerance, atol=0.0
|
|
155
|
+
):
|
|
156
|
+
return CoordinateSpacingInfo(
|
|
157
|
+
value=None,
|
|
158
|
+
median=float(np.median(steps)),
|
|
159
|
+
warn_msg=(
|
|
160
|
+
f"Coordinate '{dim}' spacing along '{dim}' differs across "
|
|
161
|
+
f"{other_dims!r}; spacing is undefined."
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return CoordinateSpacingInfo(value=steps[0], median=steps[0], warn_msg=None)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_coordinate_origins(data: xr.DataArray) -> dict[str, float]:
|
|
169
|
+
"""Return the first coordinate value for each dimension.
|
|
170
|
+
|
|
171
|
+
For each dimension, returns the first coordinate value. If a coordinate is
|
|
172
|
+
missing, falls back to `0.0` with a warning. If a coordinate is non-numeric
|
|
173
|
+
(e.g. string-based), falls back to `0.0` without a warning.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
data : xarray.DataArray
|
|
178
|
+
DataArray whose coordinate origins to compute.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
dict[str, float]
|
|
183
|
+
Origin per dimension in DataArray dimension order.
|
|
184
|
+
"""
|
|
185
|
+
result: dict[str, float] = {}
|
|
186
|
+
for dim in (str(d) for d in data.dims):
|
|
187
|
+
if dim not in data.coords:
|
|
188
|
+
warnings.warn(
|
|
189
|
+
f"Dimension '{dim}' has no coordinate; origin defaults to 0.0.",
|
|
190
|
+
stacklevel=2,
|
|
191
|
+
)
|
|
192
|
+
result[dim] = 0.0
|
|
193
|
+
else:
|
|
194
|
+
coord = data.coords[dim]
|
|
195
|
+
if not np.issubdtype(coord.dtype, np.integer) and not np.issubdtype(
|
|
196
|
+
coord.dtype, np.floating
|
|
197
|
+
):
|
|
198
|
+
result[dim] = 0.0
|
|
199
|
+
else:
|
|
200
|
+
result[dim] = float(coord.values.flat[0])
|
|
201
|
+
return result
|
voxeldata/_dims.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""VoxelData dimension name conventions.
|
|
2
|
+
|
|
3
|
+
VoxelData arrays have trailing spatial dimensions and arbitrary leading nonspatial
|
|
4
|
+
dimensions. This module names the spatial and special nonspatial dimensions so that
|
|
5
|
+
IO, processing, validation, and UI code can share their conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
WORLD_DIMS: tuple[str, ...] = ("z", "y", "x")
|
|
9
|
+
"""World spatial dimension names in VoxelData order."""
|
|
10
|
+
|
|
11
|
+
VOXEL_DIMS: tuple[str, ...] = ("k", "j", "i")
|
|
12
|
+
"""Native voxel-space dimension names in VoxelData order."""
|
|
13
|
+
|
|
14
|
+
GRID_DIM: str = "grid"
|
|
15
|
+
"""Dimension selecting one voxel-to-world mapping per spatial sampling grid."""
|
|
16
|
+
|
|
17
|
+
TIME_DIM: str = "time"
|
|
18
|
+
"""Temporal sample dimension name."""
|
|
19
|
+
|
|
20
|
+
CORE_DIMS: tuple[str, ...] = (GRID_DIM, TIME_DIM, *VOXEL_DIMS)
|
|
21
|
+
"""Core dimension names in canonical order, not a required nonspatial order."""
|