transformnd 0.7.2__tar.gz → 0.9.0__tar.gz
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.
- {transformnd-0.7.2 → transformnd-0.9.0}/PKG-INFO +8 -3
- {transformnd-0.7.2 → transformnd-0.9.0}/README.md +7 -2
- transformnd-0.9.0/pyproject.toml +125 -0
- transformnd-0.7.2/pyproject.toml → transformnd-0.9.0/pyproject.toml.orig +1 -1
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/__init__.py +5 -5
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/base.py +64 -112
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/graph.py +136 -108
- transformnd-0.9.0/src/transformnd/spaced.py +67 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/__init__.py +2 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/affine.py +14 -56
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/bijection.py +5 -15
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/by_dimension.py +25 -8
- transformnd-0.9.0/src/transformnd/transforms/grid.py +47 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/map_axis.py +8 -10
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/moving_least_squares.py +1 -7
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/project_axis.py +13 -8
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/reflection.py +4 -16
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/simple.py +33 -30
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/thinplate.py +2 -7
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/transforms/vector_field.py +14 -18
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/types.py +9 -11
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/util.py +6 -9
- transformnd-0.7.2/src/transformnd/constants.py +0 -1
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/__init__.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/base.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/bounding_box.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/pandas.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/polars.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/adapters/shapely.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/extents/__init__.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/extents/base.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/extents/bounding_box.py +0 -0
- {transformnd-0.7.2 → transformnd-0.9.0}/src/transformnd/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transformnd
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: ND coordinate transformations
|
|
5
5
|
Author: Chris Barnes
|
|
6
6
|
Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
|
|
@@ -86,6 +86,11 @@ See the [tutorial here](https://github.com/clbarnes/transformnd/blob/main/exampl
|
|
|
86
86
|
It is a [marimo](https://marimo.io) notebook.
|
|
87
87
|
Open it with `uv run --group examples marimo edit examples/tutorial.py`.
|
|
88
88
|
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
`transformnd` is pre-1.0 and the API is subject to change.
|
|
92
|
+
Production users should pin the maximum dependency version at the next minor version, like `uv add --bounds minor`.
|
|
93
|
+
|
|
89
94
|
## Implemented transforms
|
|
90
95
|
|
|
91
96
|
All transforms are accessed under the `transformnd.transforms` subpackage.
|
|
@@ -138,11 +143,11 @@ Methods which MUST be implemented:
|
|
|
138
143
|
|
|
139
144
|
Methods which SHOULD be implemented if applicable:
|
|
140
145
|
|
|
141
|
-
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU)
|
|
146
|
+
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU). The base class implementation returns `self`.
|
|
142
147
|
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
143
148
|
- `to_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
144
149
|
- `invert`: if your transformation can be inverted (default None if not)
|
|
145
|
-
- This automatically implements `__invert__` (the
|
|
150
|
+
- This automatically implements `__invert__` (the `~` operator), which returns `NotImplemented` (probably raising `NotImplementedError`) if `invert` would return `None`.
|
|
146
151
|
|
|
147
152
|
## Contributing
|
|
148
153
|
|
|
@@ -31,6 +31,11 @@ See the [tutorial here](https://github.com/clbarnes/transformnd/blob/main/exampl
|
|
|
31
31
|
It is a [marimo](https://marimo.io) notebook.
|
|
32
32
|
Open it with `uv run --group examples marimo edit examples/tutorial.py`.
|
|
33
33
|
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
`transformnd` is pre-1.0 and the API is subject to change.
|
|
37
|
+
Production users should pin the maximum dependency version at the next minor version, like `uv add --bounds minor`.
|
|
38
|
+
|
|
34
39
|
## Implemented transforms
|
|
35
40
|
|
|
36
41
|
All transforms are accessed under the `transformnd.transforms` subpackage.
|
|
@@ -83,11 +88,11 @@ Methods which MUST be implemented:
|
|
|
83
88
|
|
|
84
89
|
Methods which SHOULD be implemented if applicable:
|
|
85
90
|
|
|
86
|
-
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU)
|
|
91
|
+
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU). The base class implementation returns `self`.
|
|
87
92
|
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
88
93
|
- `to_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
89
94
|
- `invert`: if your transformation can be inverted (default None if not)
|
|
90
|
-
- This automatically implements `__invert__` (the
|
|
95
|
+
- This automatically implements `__invert__` (the `~` operator), which returns `NotImplemented` (probably raising `NotImplementedError`) if `invert` would return `None`.
|
|
91
96
|
|
|
92
97
|
## Contributing
|
|
93
98
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "transformnd"
|
|
3
|
+
version = "0.9.0"
|
|
4
|
+
description = "ND coordinate transformations"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12, <4.0"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"numpy>=2",
|
|
9
|
+
"networkx>=3",
|
|
10
|
+
"array_api_compat>=1.14",
|
|
11
|
+
"typing-extensions>=4.15.0",
|
|
12
|
+
]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
21
|
+
"Natural Language :: English",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Chris Barnes"
|
|
30
|
+
email = "chris.barnes@gerbi-gmb.de"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
documentation = "https://transformnd.readthedocs.io/en/latest/"
|
|
34
|
+
source = "https://github.com/clbarnes/transformnd"
|
|
35
|
+
issues = "https://github.com/clbarnes/transformnd/issues"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
thinplatesplines = ["morphops>=0.1.13"]
|
|
39
|
+
movingleastsquares = ["molesq>=0.4.0"]
|
|
40
|
+
pandas = ["pandas>=3.0.2"]
|
|
41
|
+
shapely = ["shapely>=2.1.2"]
|
|
42
|
+
polars = ["polars>=1.40.1"]
|
|
43
|
+
vectorfield = ["scipy>=1.17.1"]
|
|
44
|
+
vectorfield-dask = [
|
|
45
|
+
"dask>=2026.3.0",
|
|
46
|
+
"dask-image>=2026.5.0",
|
|
47
|
+
]
|
|
48
|
+
transforms = [
|
|
49
|
+
"transformnd[thinplatesplines]",
|
|
50
|
+
"transformnd[movingleastsquares]",
|
|
51
|
+
"transformnd[vectorfield]",
|
|
52
|
+
"transformnd[vectorfield-dask]",
|
|
53
|
+
]
|
|
54
|
+
adapters = [
|
|
55
|
+
"transformnd[pandas]",
|
|
56
|
+
"transformnd[shapely]",
|
|
57
|
+
"transformnd[polars]",
|
|
58
|
+
]
|
|
59
|
+
all = [
|
|
60
|
+
"transformnd[transforms]",
|
|
61
|
+
"transformnd[adapters]",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[dependency-groups]
|
|
65
|
+
test = [
|
|
66
|
+
"pytest",
|
|
67
|
+
"jax",
|
|
68
|
+
"dask>=2026.3.0",
|
|
69
|
+
]
|
|
70
|
+
lint = [
|
|
71
|
+
"ruff",
|
|
72
|
+
"mypy",
|
|
73
|
+
"prek>=0.3.9",
|
|
74
|
+
"types-shapely>=2.1.0.20260408",
|
|
75
|
+
"pydoclint>=0.8.6",
|
|
76
|
+
]
|
|
77
|
+
doc = ["pdoc>=16.0.0"]
|
|
78
|
+
examples = [
|
|
79
|
+
"marimo>=0.9",
|
|
80
|
+
"matplotlib>=3.10.8",
|
|
81
|
+
"pandas>=3.0.2",
|
|
82
|
+
"pooch>=1.9.0",
|
|
83
|
+
"scikit-image>=0.26.0",
|
|
84
|
+
]
|
|
85
|
+
bench = ["pytest-benchmark>=5.2.3"]
|
|
86
|
+
|
|
87
|
+
[[dependency-groups.dev]]
|
|
88
|
+
include-group = "test"
|
|
89
|
+
|
|
90
|
+
[[dependency-groups.dev]]
|
|
91
|
+
include-group = "lint"
|
|
92
|
+
|
|
93
|
+
[[dependency-groups.dev]]
|
|
94
|
+
include-group = "doc"
|
|
95
|
+
|
|
96
|
+
[[dependency-groups.dev]]
|
|
97
|
+
include-group = "examples"
|
|
98
|
+
|
|
99
|
+
[[dependency-groups.dev]]
|
|
100
|
+
include-group = "bench"
|
|
101
|
+
|
|
102
|
+
[build-system]
|
|
103
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
104
|
+
build-backend = "uv_build"
|
|
105
|
+
|
|
106
|
+
[tool.mypy]
|
|
107
|
+
ignore_missing_imports = true
|
|
108
|
+
check_untyped_defs = true
|
|
109
|
+
|
|
110
|
+
[tool.pytest]
|
|
111
|
+
testpaths = [
|
|
112
|
+
"tests",
|
|
113
|
+
"bench",
|
|
114
|
+
]
|
|
115
|
+
addopts = ["--benchmark-skip"]
|
|
116
|
+
|
|
117
|
+
[tool.pydoclint]
|
|
118
|
+
arg-type-hints-in-docstring = false
|
|
119
|
+
allow-init-docstring = true
|
|
120
|
+
|
|
121
|
+
[tool.ruff.lint]
|
|
122
|
+
external = [
|
|
123
|
+
"DOC",
|
|
124
|
+
"D",
|
|
125
|
+
]
|
|
@@ -8,12 +8,12 @@ You can find some examples here:
|
|
|
8
8
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from .base import Transform, TransformSequence,
|
|
12
|
-
from .
|
|
13
|
-
from .types import Spaces, TransformSignature, NDims
|
|
11
|
+
from .base import Transform, TransformSequence, TransformFnWrapper
|
|
12
|
+
from .types import TransformSignature, NDims, SpaceRef
|
|
14
13
|
from . import transforms
|
|
15
14
|
from . import adapters
|
|
16
15
|
from .graph import TransformGraph
|
|
16
|
+
from .spaced import Spaced
|
|
17
17
|
from importlib.metadata import version as _version
|
|
18
18
|
|
|
19
19
|
__version__ = _version("transformnd")
|
|
@@ -22,11 +22,11 @@ __all__ = [
|
|
|
22
22
|
"Transform",
|
|
23
23
|
"TransformGraph",
|
|
24
24
|
"TransformSequence",
|
|
25
|
-
"
|
|
25
|
+
"TransformFnWrapper",
|
|
26
26
|
"TransformSignature",
|
|
27
27
|
"SpaceRef",
|
|
28
28
|
"transforms",
|
|
29
29
|
"adapters",
|
|
30
|
-
"Spaces",
|
|
31
30
|
"NDims",
|
|
31
|
+
"Spaced",
|
|
32
32
|
]
|
|
@@ -11,14 +11,12 @@ from types import ModuleType
|
|
|
11
11
|
from array_api_compat import array_namespace
|
|
12
12
|
|
|
13
13
|
from .util import (
|
|
14
|
-
SpaceRef,
|
|
15
|
-
same_or_none,
|
|
16
|
-
space_str,
|
|
17
14
|
ArrayT,
|
|
15
|
+
join_strs,
|
|
18
16
|
)
|
|
19
17
|
from itertools import pairwise
|
|
20
18
|
|
|
21
|
-
from .types import TransformSignature,
|
|
19
|
+
from .types import TransformSignature, NDims
|
|
22
20
|
|
|
23
21
|
if TYPE_CHECKING:
|
|
24
22
|
from .transforms import Affine
|
|
@@ -30,19 +28,14 @@ class Transform[ArrayT](ABC):
|
|
|
30
28
|
def __init__(
|
|
31
29
|
self,
|
|
32
30
|
ndims: NDims,
|
|
33
|
-
*,
|
|
34
|
-
spaces: Spaces = Spaces(None, None),
|
|
35
31
|
):
|
|
36
32
|
"""
|
|
37
33
|
Parameters
|
|
38
34
|
----------
|
|
39
35
|
ndims
|
|
40
36
|
Source and target dimensionality.
|
|
41
|
-
spaces
|
|
42
|
-
Optional source and target spaces
|
|
43
37
|
"""
|
|
44
38
|
self.ndims: NDims = ndims
|
|
45
|
-
self.spaces: Spaces = spaces
|
|
46
39
|
|
|
47
40
|
def is_identity(self) -> bool:
|
|
48
41
|
"""Whether this is a no-op transformation."""
|
|
@@ -105,18 +98,18 @@ class Transform[ArrayT](ABC):
|
|
|
105
98
|
"""
|
|
106
99
|
pass
|
|
107
100
|
|
|
108
|
-
def invert(self) -> Transform | None:
|
|
101
|
+
def invert(self) -> Transform[ArrayT] | None:
|
|
109
102
|
"""Invert the transformation, returning `None` if not possible."""
|
|
110
103
|
return None
|
|
111
104
|
|
|
112
|
-
def __invert__(self) -> Transform:
|
|
105
|
+
def __invert__(self) -> Transform[ArrayT]:
|
|
113
106
|
"""Invert transformation if possible.
|
|
114
107
|
|
|
115
108
|
Returns `NotImplemented` otherwise (will raise `NotImplementedError`).
|
|
116
109
|
|
|
117
110
|
Returns
|
|
118
111
|
-------
|
|
119
|
-
Transform
|
|
112
|
+
Transform[ArrayT]
|
|
120
113
|
Inverted transformation.
|
|
121
114
|
"""
|
|
122
115
|
t = self.invert()
|
|
@@ -141,10 +134,9 @@ class Transform[ArrayT](ABC):
|
|
|
141
134
|
Returns
|
|
142
135
|
-------
|
|
143
136
|
Self
|
|
144
|
-
A new transform instance with parameters on the target device
|
|
145
|
-
or NotImplemented if the subclass does not support device placement.
|
|
137
|
+
A new transform instance with parameters on the target device
|
|
146
138
|
"""
|
|
147
|
-
return
|
|
139
|
+
return self
|
|
148
140
|
|
|
149
141
|
def __or__(self, other: Transform[ArrayT]) -> TransformSequence[ArrayT]:
|
|
150
142
|
"""Compose transformations into a sequence.
|
|
@@ -166,7 +158,6 @@ class Transform[ArrayT](ABC):
|
|
|
166
158
|
transforms = as_transform_list(self) + as_transform_list(other)
|
|
167
159
|
return TransformSequence[ArrayT](
|
|
168
160
|
transforms,
|
|
169
|
-
spaces=Spaces(self.spaces.source, other.spaces.target),
|
|
170
161
|
)
|
|
171
162
|
|
|
172
163
|
def __ror__(self, other: Transform[ArrayT]) -> TransformSequence[ArrayT]:
|
|
@@ -189,17 +180,13 @@ class Transform[ArrayT](ABC):
|
|
|
189
180
|
transforms = as_transform_list(other) + as_transform_list(self)
|
|
190
181
|
return TransformSequence(
|
|
191
182
|
transforms,
|
|
192
|
-
spaces=Spaces(other.spaces.source, self.spaces.target),
|
|
193
183
|
)
|
|
194
184
|
|
|
195
185
|
def __str__(self) -> str:
|
|
196
|
-
|
|
197
|
-
src = space_str(self.spaces.source)
|
|
198
|
-
tgt = space_str(self.spaces.target)
|
|
199
|
-
return f"{cls_name}[{src}->{tgt}]"
|
|
186
|
+
return f"{type(self).__qualname__}@{hex(id(self))}[{self.ndims}]"
|
|
200
187
|
|
|
201
188
|
|
|
202
|
-
class
|
|
189
|
+
class TransformFnWrapper(Transform[ArrayT]):
|
|
203
190
|
"""Wrapper around an arbitrary function which transforms coordinates."""
|
|
204
191
|
|
|
205
192
|
def __init__(
|
|
@@ -207,8 +194,6 @@ class TransformWrapper(Transform[ArrayT]):
|
|
|
207
194
|
fn: TransformSignature[ArrayT],
|
|
208
195
|
in_ndim: int,
|
|
209
196
|
out_ndim: int,
|
|
210
|
-
*,
|
|
211
|
-
spaces: Spaces = Spaces(None, None),
|
|
212
197
|
):
|
|
213
198
|
"""Wrapper around an arbitrary function.
|
|
214
199
|
|
|
@@ -223,46 +208,28 @@ class TransformWrapper(Transform[ArrayT]):
|
|
|
223
208
|
Dimensionality of the input coordinates.
|
|
224
209
|
out_ndim
|
|
225
210
|
Dimensionality of the output coordinates.
|
|
226
|
-
spaces
|
|
227
|
-
Optional source and target spaces
|
|
228
211
|
"""
|
|
229
|
-
super().__init__(NDims(in_ndim, out_ndim)
|
|
212
|
+
super().__init__(NDims(in_ndim, out_ndim))
|
|
230
213
|
self.fn = fn
|
|
231
214
|
|
|
232
215
|
def apply(self, coords: ArrayT) -> ArrayT:
|
|
233
216
|
self._validate_coords(coords)
|
|
234
217
|
return self.fn(coords)
|
|
235
218
|
|
|
219
|
+
def __str__(self) -> str:
|
|
220
|
+
return f"{super().__str__()}({self.fn})"
|
|
236
221
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
target_space: SpaceRef | None = None,
|
|
241
|
-
) -> Transform[ArrayT]:
|
|
242
|
-
src_tgt = (t.spaces.source, t.spaces.target)
|
|
243
|
-
src = same_or_none(src_tgt[0], source_space, default=None)
|
|
244
|
-
tgt = same_or_none(src_tgt[1], target_space, default=None)
|
|
245
|
-
if (src, tgt) != src_tgt:
|
|
246
|
-
t = copy(t)
|
|
247
|
-
t.spaces = Spaces(src, tgt)
|
|
248
|
-
return t
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def infer_spaces(
|
|
252
|
-
transforms: Sequence[Transform[ArrayT]], source_space=None, target_space=None
|
|
253
|
-
) -> list[Transform[ArrayT]]:
|
|
254
|
-
prev_tgts = [source_space]
|
|
255
|
-
next_srcs = []
|
|
256
|
-
for t1, t2 in pairwise(transforms):
|
|
257
|
-
prev_tgts.append(t1.spaces.target)
|
|
258
|
-
next_srcs.append(t2.spaces.source)
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_flat(cls, fn: TransformSignature[ArrayT]) -> Self:
|
|
224
|
+
"""Create a 1D transform from a function which would take and return a 1D array."""
|
|
259
225
|
|
|
260
|
-
|
|
226
|
+
def fn2(arr: ArrayT) -> ArrayT:
|
|
227
|
+
xp = array_namespace(arr)
|
|
228
|
+
flat = xp.reshape(arr, (-1,))
|
|
229
|
+
transformed = fn(flat)
|
|
230
|
+
return xp.expand_dims(transformed, 1)
|
|
261
231
|
|
|
262
|
-
|
|
263
|
-
for t, next_src, prev_tgt in zip(transforms, next_srcs, prev_tgts):
|
|
264
|
-
out.append(_with_spaces(t, prev_tgt, next_src))
|
|
265
|
-
return out
|
|
232
|
+
return cls(fn2, 1, 1)
|
|
266
233
|
|
|
267
234
|
|
|
268
235
|
def as_transform_list(t: Transform[ArrayT]) -> list[Transform[ArrayT]]:
|
|
@@ -273,36 +240,39 @@ def as_transform_list(t: Transform[ArrayT]) -> list[Transform[ArrayT]]:
|
|
|
273
240
|
|
|
274
241
|
|
|
275
242
|
class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
276
|
-
"""Chain transforms, applying one after another.
|
|
243
|
+
"""Chain transforms, applying one after another.
|
|
244
|
+
|
|
245
|
+
The `TransformSequence()` constructor takes a sequence of transforms which must not be empty.
|
|
246
|
+
Empty sequences can be handled with `TransformSequence.empty(ndim: int)`.
|
|
247
|
+
"""
|
|
277
248
|
|
|
278
249
|
def __init__(
|
|
279
250
|
self,
|
|
280
251
|
transforms: Sequence[Transform[ArrayT]],
|
|
281
|
-
*,
|
|
282
|
-
spaces: Spaces = Spaces(None, None),
|
|
283
252
|
) -> None:
|
|
284
253
|
"""Combine transforms by chaining them.
|
|
285
254
|
|
|
286
|
-
|
|
287
|
-
|
|
255
|
+
Empty sequences raise an error;
|
|
256
|
+
use the `TransformSequence.empty(ndim)` constructor instead.
|
|
257
|
+
|
|
258
|
+
Also checks for consistent dimensionality.
|
|
288
259
|
|
|
289
260
|
Parameters
|
|
290
261
|
----------
|
|
291
262
|
transforms :
|
|
292
263
|
Items which are a TransformSequences
|
|
293
264
|
will each still be treated as a single transform.
|
|
294
|
-
spaces :
|
|
295
|
-
Optional source and target spaces.
|
|
296
|
-
Can also be inferred from the first and last transforms.
|
|
297
265
|
|
|
298
266
|
Raises
|
|
299
267
|
------
|
|
300
268
|
ValueError
|
|
301
|
-
If spaces are incompatible.
|
|
269
|
+
If spaces are incompatible, or no spaces are given.
|
|
302
270
|
"""
|
|
303
|
-
ts =
|
|
271
|
+
ts = list(transforms)
|
|
304
272
|
if not ts:
|
|
305
|
-
raise ValueError(
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"Empty transform sequence; use TransformSequence.empty(ndim)"
|
|
275
|
+
)
|
|
306
276
|
|
|
307
277
|
for idx, (t1, t2) in enumerate(pairwise(ts)):
|
|
308
278
|
if t1.ndims.target != t2.ndims.source:
|
|
@@ -312,16 +282,20 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
312
282
|
f"and the next source is {t2.ndims.source}D"
|
|
313
283
|
)
|
|
314
284
|
|
|
315
|
-
spaces = Spaces(ts[0].spaces.source, ts[-1].spaces.target)
|
|
316
285
|
ndims = NDims(ts[0].ndims.source, ts[-1].ndims.target)
|
|
317
286
|
|
|
318
|
-
super().__init__(
|
|
319
|
-
ndims,
|
|
320
|
-
spaces=spaces,
|
|
321
|
-
)
|
|
287
|
+
super().__init__(ndims)
|
|
322
288
|
|
|
323
289
|
self.transforms: list[Transform[ArrayT]] = ts
|
|
324
290
|
|
|
291
|
+
@classmethod
|
|
292
|
+
def empty(cls, ndim: int) -> Self:
|
|
293
|
+
from .transforms import Identity
|
|
294
|
+
|
|
295
|
+
out = cls([Identity(ndim)])
|
|
296
|
+
out.transforms.pop()
|
|
297
|
+
return out
|
|
298
|
+
|
|
325
299
|
def __iter__(self) -> Iterator[Transform[ArrayT]]:
|
|
326
300
|
"""Iterate through component transforms.
|
|
327
301
|
|
|
@@ -341,13 +315,15 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
341
315
|
return len(self.transforms)
|
|
342
316
|
|
|
343
317
|
def invert(self) -> Transform[ArrayT] | None:
|
|
318
|
+
if self.is_empty():
|
|
319
|
+
return type(self).empty(self.ndims.source)
|
|
320
|
+
|
|
344
321
|
try:
|
|
345
322
|
transforms = [~t for t in reversed(self.transforms)]
|
|
346
323
|
except NotImplementedError:
|
|
347
324
|
return None
|
|
348
325
|
return type(self)(
|
|
349
326
|
transforms,
|
|
350
|
-
spaces=self.spaces.invert(),
|
|
351
327
|
)
|
|
352
328
|
|
|
353
329
|
def apply(self, coords: ArrayT) -> ArrayT:
|
|
@@ -357,45 +333,13 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
357
333
|
|
|
358
334
|
def to_device(self, xp: ModuleType, device: str | None = None) -> Self:
|
|
359
335
|
result = copy(self)
|
|
360
|
-
|
|
336
|
+
if not self.is_empty():
|
|
337
|
+
result.transforms = [t.to_device(xp, device) for t in self.transforms]
|
|
361
338
|
return result
|
|
362
339
|
|
|
363
|
-
def list_spaces(self, skip_none: bool = False) -> list[SpaceRef]:
|
|
364
|
-
"""List spaces in this transform.
|
|
365
|
-
|
|
366
|
-
Parameters
|
|
367
|
-
----------
|
|
368
|
-
skip_none
|
|
369
|
-
Whether to skip undefined spaces, default False.
|
|
370
|
-
|
|
371
|
-
Returns
|
|
372
|
-
-------
|
|
373
|
-
list[SpaceRef]
|
|
374
|
-
The list of spaces.
|
|
375
|
-
"""
|
|
376
|
-
spaces = [self.spaces.source] + [t.spaces.target for t in self.transforms]
|
|
377
|
-
if skip_none:
|
|
378
|
-
spaces = [s for s in spaces if s is not None]
|
|
379
|
-
return spaces
|
|
380
|
-
|
|
381
|
-
def split(self) -> Iterator[Transform[ArrayT]]:
|
|
382
|
-
"""Split the sequence where an intermediate space is known."""
|
|
383
|
-
this_seq = []
|
|
384
|
-
|
|
385
|
-
for t in self.transforms:
|
|
386
|
-
if t.spaces.source is not None and t.spaces.target is not None:
|
|
387
|
-
yield t
|
|
388
|
-
continue
|
|
389
|
-
|
|
390
|
-
this_seq.append(t)
|
|
391
|
-
if t.spaces.target is not None:
|
|
392
|
-
yield type(self)(this_seq)
|
|
393
|
-
this_seq = []
|
|
394
|
-
|
|
395
340
|
def __str__(self) -> str:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return f"{cls_name}[{spaces_str}]"
|
|
341
|
+
spaces_str = join_strs(self.transforms, "|")
|
|
342
|
+
return f"{super().__str__()}({spaces_str})"
|
|
399
343
|
|
|
400
344
|
def __getitem__(self, idx: slice | int):
|
|
401
345
|
if isinstance(idx, int):
|
|
@@ -405,10 +349,16 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
405
349
|
def is_identity(self) -> bool:
|
|
406
350
|
return all(t.is_identity() for t in self)
|
|
407
351
|
|
|
352
|
+
def is_empty(self) -> bool:
|
|
353
|
+
return not self.transforms
|
|
354
|
+
|
|
408
355
|
def flatten(self, drop_inverse: bool = True) -> Self:
|
|
409
356
|
"""Flatten nested sequences."""
|
|
410
357
|
from .transforms.bijection import Bijection
|
|
411
358
|
|
|
359
|
+
if self.is_empty():
|
|
360
|
+
return copy(self)
|
|
361
|
+
|
|
412
362
|
out: list[Transform[ArrayT]] = []
|
|
413
363
|
|
|
414
364
|
for t in self.transforms:
|
|
@@ -418,7 +368,7 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
418
368
|
out.extend(t.flatten())
|
|
419
369
|
else:
|
|
420
370
|
out.append(t)
|
|
421
|
-
return TransformSequence(out
|
|
371
|
+
return TransformSequence(out) # type:ignore
|
|
422
372
|
|
|
423
373
|
def simplify(self, drop_inverse: bool = True):
|
|
424
374
|
"""Reduce the number of transformations in this sequence if possible.
|
|
@@ -433,7 +383,8 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
433
383
|
Does not check whether transforms invert each other,
|
|
434
384
|
e.g. `Translation(1) | Translation(-1)`.
|
|
435
385
|
"""
|
|
436
|
-
|
|
386
|
+
if self.is_empty():
|
|
387
|
+
return copy(self)
|
|
437
388
|
|
|
438
389
|
out: list[Transform[ArrayT]] = []
|
|
439
390
|
affine = None
|
|
@@ -459,11 +410,13 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
459
410
|
add_to_output(affine, out)
|
|
460
411
|
|
|
461
412
|
if not out:
|
|
462
|
-
|
|
413
|
+
return type(self).empty(self.ndims.source)
|
|
463
414
|
|
|
464
|
-
return type(self)(out
|
|
415
|
+
return type(self)(out)
|
|
465
416
|
|
|
466
417
|
def to_affine(self) -> Affine[ArrayT] | None:
|
|
418
|
+
if self.is_empty():
|
|
419
|
+
return Affine.identity(self.ndims.source) # type:ignore
|
|
467
420
|
simple = self.simplify(True)
|
|
468
421
|
if len(simple) != 1:
|
|
469
422
|
return None
|
|
@@ -475,6 +428,5 @@ def add_to_output(transform: Transform, lst: list[Transform]) -> bool:
|
|
|
475
428
|
return False
|
|
476
429
|
|
|
477
430
|
transform = copy(transform)
|
|
478
|
-
transform.spaces = Spaces(None, None)
|
|
479
431
|
lst.append(transform)
|
|
480
432
|
return True
|