transformnd 0.1.0__tar.gz → 0.2.1__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.1.0 → transformnd-0.2.1}/PKG-INFO +26 -13
- {transformnd-0.1.0 → transformnd-0.2.1}/README.md +11 -12
- {transformnd-0.1.0 → transformnd-0.2.1}/pyproject.toml +19 -1
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/__init__.py +4 -2
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/base.py +58 -72
- transformnd-0.2.1/src/transformnd/constants.py +1 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/extents/base.py +2 -1
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/extents/bounding_box.py +9 -3
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/graph.py +87 -28
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/affine.py +68 -76
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/bijection.py +9 -13
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/by_dimension.py +16 -18
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/map_axis.py +9 -13
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/moving_least_squares.py +14 -9
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/reflection.py +13 -5
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/simple.py +36 -59
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/thinplate.py +8 -11
- transformnd-0.2.1/src/transformnd/types.py +46 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/util.py +12 -101
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/__init__.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/base.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/bounding_box.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/pandas.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/polars.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/adapters/shapely.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/extents/__init__.py +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/py.typed +0 -0
- {transformnd-0.1.0 → transformnd-0.2.1}/src/transformnd/transforms/__init__.py +0 -0
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transformnd
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: ND coordinate transformations
|
|
5
5
|
Author: Chris Barnes
|
|
6
6
|
Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
|
|
7
7
|
License-Expression: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
8
19
|
Requires-Dist: numpy>=2
|
|
9
20
|
Requires-Dist: networkx>=3
|
|
10
21
|
Requires-Dist: array-api-compat>=1.14
|
|
@@ -15,6 +26,9 @@ Requires-Dist: polars>=1.40.1 ; extra == 'polars'
|
|
|
15
26
|
Requires-Dist: shapely>=2.1.2 ; extra == 'shapely'
|
|
16
27
|
Requires-Dist: morphops>=0.1.13 ; extra == 'thinplatesplines'
|
|
17
28
|
Requires-Python: >=3.12, <4.0
|
|
29
|
+
Project-URL: documentation, https://transformnd.readthedocs.io/en/latest/
|
|
30
|
+
Project-URL: source, https://github.com/clbarnes/transformnd
|
|
31
|
+
Project-URL: issues, https://github.com/clbarnes/transformnd/issues
|
|
18
32
|
Provides-Extra: movingleastsquares
|
|
19
33
|
Provides-Extra: pandas
|
|
20
34
|
Provides-Extra: polars
|
|
@@ -24,10 +38,10 @@ Description-Content-Type: text/markdown
|
|
|
24
38
|
|
|
25
39
|
# transformnd
|
|
26
40
|
|
|
27
|
-
[](https://pypi.org/project/transformnd/)
|
|
42
|
+
[](https://github.com/clbarnes/transformnd/actions)
|
|
43
|
+
[](https://transformnd.readthedocs.io)
|
|
44
|
+

|
|
31
45
|
|
|
32
46
|
A library providing an API for coordinate transformations,
|
|
33
47
|
as well as some common transforms.
|
|
@@ -45,7 +59,7 @@ co-developed with [xform](https://github.com/schlegelp/xform/) as a red team pro
|
|
|
45
59
|
`Transform` subclasses which are restricted to certain dimensionalities
|
|
46
60
|
can specify this in their `ndim` class variable.
|
|
47
61
|
Instances of `Transform` subclasses can further restrict their `ndim`.
|
|
48
|
-
Use `self._validate_coords(coords)` in `
|
|
62
|
+
Use `self._validate_coords(coords)` in the `apply` method to ensure the coordinates
|
|
49
63
|
are of valid type and dimensions.
|
|
50
64
|
|
|
51
65
|
Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
|
|
@@ -91,27 +105,25 @@ Contributions of additional transforms and adapters are welcome!
|
|
|
91
105
|
Even if they're only thin wrappers around an external library,
|
|
92
106
|
the downstream ecosystem benefits from a consistent API.
|
|
93
107
|
|
|
94
|
-
Such external transformation libraries should be specified as "extras",
|
|
108
|
+
Such external transformation libraries should be specified as "extras" (`pyproject.toml:project.optional-dependencies`),
|
|
95
109
|
and be contained in a submodule so that they are not immediately imported
|
|
96
110
|
with `transformnd`.
|
|
97
|
-
Dependencies for new adapters do not need to be included in `transformnd`'s dependencies,
|
|
98
|
-
but should be specified in the `requirements.txt` for tests.
|
|
99
111
|
|
|
100
112
|
Alternatively, consider adopting `transformnd`'s base classes in your own library,
|
|
101
113
|
and have your transformation instantly compatible for downstream users.
|
|
102
114
|
|
|
103
115
|
Methods which MUST be implemented:
|
|
104
116
|
|
|
105
|
-
- `__init__`: should validate parameters and
|
|
117
|
+
- `__init__`: should validate parameters and must call the `super()` constructor
|
|
106
118
|
- `apply`: should call `_validate_coords` method early to check that the given coordinates are the correct shape
|
|
107
119
|
|
|
108
120
|
Methods which SHOULD be implemented if applicable:
|
|
109
121
|
|
|
110
122
|
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU)
|
|
111
123
|
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
112
|
-
- `
|
|
124
|
+
- `to_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
113
125
|
- `invert`: if your transformation can be inverted (default None if not)
|
|
114
|
-
- This automatically implements `__invert__` (the `~my_transform` operator), which
|
|
126
|
+
- This automatically implements `__invert__` (the `~my_transform` operator), which returns `NotImplemented` (probably raising `NotImplementedError`) if `invert` would return `None`.
|
|
115
127
|
|
|
116
128
|
## Contributing
|
|
117
129
|
|
|
@@ -121,7 +133,8 @@ Methods which SHOULD be implemented if applicable:
|
|
|
121
133
|
- `prek install-hooks && prek run --all-files` to get started.
|
|
122
134
|
- Use [`just`](https://github.com/casey/just) for common development tasks (format, lint, test, generate docs, run benchmarks).
|
|
123
135
|
- `just` to list commands.
|
|
124
|
-
- Docs are generated with `pdoc` (use `just doc`) and hosted on ReadTheDocs
|
|
136
|
+
- Docs are generated with [`pdoc`](https://pdoc.dev/) (use `just doc`) and hosted on ReadTheDocs
|
|
137
|
+
- `just bump` bumps the version, commits, and tags (but does not push); depends on [`schpet/changelog`](https://github.com/schpet/changelog)
|
|
125
138
|
|
|
126
139
|
## Thanks
|
|
127
140
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# transformnd
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/transformnd/)
|
|
4
|
+
[](https://github.com/clbarnes/transformnd/actions)
|
|
5
|
+
[](https://transformnd.readthedocs.io)
|
|
6
|
+

|
|
7
7
|
|
|
8
8
|
A library providing an API for coordinate transformations,
|
|
9
9
|
as well as some common transforms.
|
|
@@ -21,7 +21,7 @@ co-developed with [xform](https://github.com/schlegelp/xform/) as a red team pro
|
|
|
21
21
|
`Transform` subclasses which are restricted to certain dimensionalities
|
|
22
22
|
can specify this in their `ndim` class variable.
|
|
23
23
|
Instances of `Transform` subclasses can further restrict their `ndim`.
|
|
24
|
-
Use `self._validate_coords(coords)` in `
|
|
24
|
+
Use `self._validate_coords(coords)` in the `apply` method to ensure the coordinates
|
|
25
25
|
are of valid type and dimensions.
|
|
26
26
|
|
|
27
27
|
Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
|
|
@@ -67,27 +67,25 @@ Contributions of additional transforms and adapters are welcome!
|
|
|
67
67
|
Even if they're only thin wrappers around an external library,
|
|
68
68
|
the downstream ecosystem benefits from a consistent API.
|
|
69
69
|
|
|
70
|
-
Such external transformation libraries should be specified as "extras",
|
|
70
|
+
Such external transformation libraries should be specified as "extras" (`pyproject.toml:project.optional-dependencies`),
|
|
71
71
|
and be contained in a submodule so that they are not immediately imported
|
|
72
72
|
with `transformnd`.
|
|
73
|
-
Dependencies for new adapters do not need to be included in `transformnd`'s dependencies,
|
|
74
|
-
but should be specified in the `requirements.txt` for tests.
|
|
75
73
|
|
|
76
74
|
Alternatively, consider adopting `transformnd`'s base classes in your own library,
|
|
77
75
|
and have your transformation instantly compatible for downstream users.
|
|
78
76
|
|
|
79
77
|
Methods which MUST be implemented:
|
|
80
78
|
|
|
81
|
-
- `__init__`: should validate parameters and
|
|
79
|
+
- `__init__`: should validate parameters and must call the `super()` constructor
|
|
82
80
|
- `apply`: should call `_validate_coords` method early to check that the given coordinates are the correct shape
|
|
83
81
|
|
|
84
82
|
Methods which SHOULD be implemented if applicable:
|
|
85
83
|
|
|
86
84
|
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU)
|
|
87
85
|
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
88
|
-
- `
|
|
86
|
+
- `to_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
89
87
|
- `invert`: if your transformation can be inverted (default None if not)
|
|
90
|
-
- This automatically implements `__invert__` (the `~my_transform` operator), which
|
|
88
|
+
- This automatically implements `__invert__` (the `~my_transform` operator), which returns `NotImplemented` (probably raising `NotImplementedError`) if `invert` would return `None`.
|
|
91
89
|
|
|
92
90
|
## Contributing
|
|
93
91
|
|
|
@@ -97,7 +95,8 @@ Methods which SHOULD be implemented if applicable:
|
|
|
97
95
|
- `prek install-hooks && prek run --all-files` to get started.
|
|
98
96
|
- Use [`just`](https://github.com/casey/just) for common development tasks (format, lint, test, generate docs, run benchmarks).
|
|
99
97
|
- `just` to list commands.
|
|
100
|
-
- Docs are generated with `pdoc` (use `just doc`) and hosted on ReadTheDocs
|
|
98
|
+
- Docs are generated with [`pdoc`](https://pdoc.dev/) (use `just doc`) and hosted on ReadTheDocs
|
|
99
|
+
- `just bump` bumps the version, commits, and tags (but does not push); depends on [`schpet/changelog`](https://github.com/schpet/changelog)
|
|
101
100
|
|
|
102
101
|
## Thanks
|
|
103
102
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "transformnd"
|
|
3
|
-
version = "0.1
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "ND coordinate transformations"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Chris Barnes", email = "chris.barnes@gerbi-gmb.de" }]
|
|
@@ -12,6 +12,24 @@ dependencies = [
|
|
|
12
12
|
"typing-extensions>=4.15.0",
|
|
13
13
|
]
|
|
14
14
|
license = "MIT"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
22
|
+
"Natural Language :: English",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
documentation = "https://transformnd.readthedocs.io/en/latest/"
|
|
31
|
+
source = "https://github.com/clbarnes/transformnd"
|
|
32
|
+
issues = "https://github.com/clbarnes/transformnd/issues"
|
|
15
33
|
|
|
16
34
|
[project.optional-dependencies]
|
|
17
35
|
thinplatesplines = [
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .base import Transform, TransformSequence, TransformWrapper
|
|
6
|
-
from .util import SpaceRef
|
|
6
|
+
from .util import SpaceRef
|
|
7
|
+
from .types import Spaces, TransformSignature, NDims
|
|
7
8
|
from . import transforms
|
|
8
9
|
from . import adapters
|
|
9
10
|
from .graph import TransformGraph
|
|
@@ -18,7 +19,8 @@ __all__ = [
|
|
|
18
19
|
"TransformWrapper",
|
|
19
20
|
"TransformSignature",
|
|
20
21
|
"SpaceRef",
|
|
21
|
-
"check_ndim",
|
|
22
22
|
"transforms",
|
|
23
23
|
"adapters",
|
|
24
|
+
"Spaces",
|
|
25
|
+
"NDims",
|
|
24
26
|
]
|
|
@@ -11,17 +11,13 @@ from array_api_compat import array_namespace
|
|
|
11
11
|
|
|
12
12
|
from .util import (
|
|
13
13
|
SpaceRef,
|
|
14
|
-
TransformSignature,
|
|
15
|
-
check_ndim,
|
|
16
|
-
dim_intersection,
|
|
17
|
-
invert_spaces,
|
|
18
14
|
same_or_none,
|
|
19
15
|
space_str,
|
|
20
|
-
to_single_ndim,
|
|
21
|
-
window,
|
|
22
|
-
SpaceTuple,
|
|
23
16
|
ArrayT,
|
|
24
17
|
)
|
|
18
|
+
from itertools import pairwise
|
|
19
|
+
|
|
20
|
+
from .types import TransformSignature, Spaces, NDims
|
|
25
21
|
|
|
26
22
|
if TYPE_CHECKING:
|
|
27
23
|
from .transforms import Affine
|
|
@@ -30,41 +26,28 @@ if TYPE_CHECKING:
|
|
|
30
26
|
class Transform[ArrayT](ABC):
|
|
31
27
|
"""Base class for transforms."""
|
|
32
28
|
|
|
33
|
-
ndim: set[int] | None = None
|
|
34
|
-
|
|
35
29
|
def __init__(
|
|
36
30
|
self,
|
|
31
|
+
ndims: NDims,
|
|
37
32
|
*,
|
|
38
|
-
spaces:
|
|
33
|
+
spaces: Spaces = Spaces(None, None),
|
|
39
34
|
):
|
|
40
35
|
"""
|
|
41
36
|
Parameters
|
|
42
37
|
----------
|
|
43
|
-
spaces
|
|
38
|
+
spaces
|
|
44
39
|
Optional source and target spaces
|
|
45
40
|
"""
|
|
46
|
-
self.
|
|
47
|
-
|
|
48
|
-
@property
|
|
49
|
-
def source_space(self):
|
|
50
|
-
return self.spaces[0]
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def target_space(self):
|
|
54
|
-
return self.spaces[1]
|
|
41
|
+
self.ndims: NDims = ndims
|
|
42
|
+
self.spaces: Spaces = spaces
|
|
55
43
|
|
|
56
44
|
def is_identity(self) -> bool:
|
|
57
45
|
"""Whether this is a no-op transformation."""
|
|
58
46
|
return False
|
|
59
47
|
|
|
60
|
-
def to_affine(self
|
|
48
|
+
def to_affine(self) -> Affine[ArrayT] | None:
|
|
61
49
|
"""Convert the transform into affine, if conversion is possible.
|
|
62
50
|
|
|
63
|
-
Parameters
|
|
64
|
-
----------
|
|
65
|
-
dim: int, optional
|
|
66
|
-
Total number of dimensions; If None, dim is set equal to self.ndim.
|
|
67
|
-
|
|
68
51
|
Returns
|
|
69
52
|
-------
|
|
70
53
|
Transform | None:
|
|
@@ -74,9 +57,9 @@ class Transform[ArrayT](ABC):
|
|
|
74
57
|
return None
|
|
75
58
|
|
|
76
59
|
def _validate_coords(self, coords: ArrayT) -> ArrayT:
|
|
77
|
-
"""Check that
|
|
60
|
+
"""Check that input coordinates are of the correct shape.
|
|
78
61
|
|
|
79
|
-
Also ensure that coords is a
|
|
62
|
+
Also ensure that coords is a 2Darray.
|
|
80
63
|
|
|
81
64
|
Parameters
|
|
82
65
|
----------
|
|
@@ -91,7 +74,11 @@ class Transform[ArrayT](ABC):
|
|
|
91
74
|
xp = array_namespace(coords)
|
|
92
75
|
if xp.ndim(coords) != 2:
|
|
93
76
|
raise ValueError("Coords must be a 2D array")
|
|
94
|
-
|
|
77
|
+
dim = xp.shape(coords)[1]
|
|
78
|
+
if xp.shape(coords)[1] != self.ndims.source:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Coords must have dimensionality {self.ndims.source}, got {dim}"
|
|
81
|
+
)
|
|
95
82
|
return coords
|
|
96
83
|
|
|
97
84
|
@abstractmethod
|
|
@@ -112,8 +99,6 @@ class Transform[ArrayT](ABC):
|
|
|
112
99
|
|
|
113
100
|
def invert(self) -> Transform | None:
|
|
114
101
|
"""Invert the transformation, returning `None` if not possible."""
|
|
115
|
-
if self.is_identity():
|
|
116
|
-
return copy(self)
|
|
117
102
|
return None
|
|
118
103
|
|
|
119
104
|
def __invert__(self) -> Transform:
|
|
@@ -168,10 +153,10 @@ class Transform[ArrayT](ABC):
|
|
|
168
153
|
"""
|
|
169
154
|
if not isinstance(other, Transform):
|
|
170
155
|
return NotImplemented
|
|
171
|
-
transforms =
|
|
156
|
+
transforms = as_transform_list(self) + as_transform_list(other)
|
|
172
157
|
return TransformSequence[ArrayT](
|
|
173
158
|
transforms,
|
|
174
|
-
spaces=(self.
|
|
159
|
+
spaces=Spaces(self.spaces.source, other.spaces.target),
|
|
175
160
|
)
|
|
176
161
|
|
|
177
162
|
def __ror__(self, other: Transform[ArrayT]) -> TransformSequence[ArrayT]:
|
|
@@ -189,16 +174,16 @@ class Transform[ArrayT](ABC):
|
|
|
189
174
|
"""
|
|
190
175
|
if not isinstance(other, Transform):
|
|
191
176
|
return NotImplemented
|
|
192
|
-
transforms =
|
|
177
|
+
transforms = as_transform_list(other) + as_transform_list(self)
|
|
193
178
|
return TransformSequence(
|
|
194
179
|
transforms,
|
|
195
|
-
spaces=(other.
|
|
180
|
+
spaces=Spaces(other.spaces.source, self.spaces.target),
|
|
196
181
|
)
|
|
197
182
|
|
|
198
183
|
def __str__(self) -> str:
|
|
199
184
|
cls_name = type(self).__name__
|
|
200
|
-
src = space_str(self.
|
|
201
|
-
tgt = space_str(self.
|
|
185
|
+
src = space_str(self.spaces.source)
|
|
186
|
+
tgt = space_str(self.spaces.target)
|
|
202
187
|
return f"{cls_name}[{src}->{tgt}]"
|
|
203
188
|
|
|
204
189
|
|
|
@@ -208,9 +193,10 @@ class TransformWrapper(Transform[ArrayT]):
|
|
|
208
193
|
def __init__(
|
|
209
194
|
self,
|
|
210
195
|
fn: TransformSignature[ArrayT],
|
|
211
|
-
|
|
196
|
+
in_ndim: int,
|
|
197
|
+
out_ndim: int,
|
|
212
198
|
*,
|
|
213
|
-
spaces:
|
|
199
|
+
spaces: Spaces = Spaces(None, None),
|
|
214
200
|
):
|
|
215
201
|
"""Wrapper around an arbitrary function.
|
|
216
202
|
|
|
@@ -224,13 +210,8 @@ class TransformWrapper(Transform[ArrayT]):
|
|
|
224
210
|
spaces : tuple[SpaceRef, SpaceRef]
|
|
225
211
|
Optional source and target spaces
|
|
226
212
|
"""
|
|
227
|
-
super().__init__(spaces=spaces)
|
|
213
|
+
super().__init__(NDims(in_ndim, out_ndim), spaces=spaces)
|
|
228
214
|
self.fn = fn
|
|
229
|
-
if ndim is not None:
|
|
230
|
-
if isinstance(ndim, int):
|
|
231
|
-
self.ndim = {ndim}
|
|
232
|
-
else:
|
|
233
|
-
self.ndim = set(ndim)
|
|
234
215
|
|
|
235
216
|
def apply(self, coords: ArrayT) -> ArrayT:
|
|
236
217
|
self._validate_coords(coords)
|
|
@@ -242,12 +223,12 @@ def _with_spaces(
|
|
|
242
223
|
source_space: SpaceRef | None = None,
|
|
243
224
|
target_space: SpaceRef | None = None,
|
|
244
225
|
) -> Transform[ArrayT]:
|
|
245
|
-
src_tgt = (t.
|
|
226
|
+
src_tgt = (t.spaces.source, t.spaces.target)
|
|
246
227
|
src = same_or_none(src_tgt[0], source_space, default=None)
|
|
247
228
|
tgt = same_or_none(src_tgt[1], target_space, default=None)
|
|
248
229
|
if (src, tgt) != src_tgt:
|
|
249
230
|
t = copy(t)
|
|
250
|
-
t.spaces = (src, tgt)
|
|
231
|
+
t.spaces = Spaces(src, tgt)
|
|
251
232
|
return t
|
|
252
233
|
|
|
253
234
|
|
|
@@ -256,9 +237,9 @@ def infer_spaces(
|
|
|
256
237
|
) -> list[Transform[ArrayT]]:
|
|
257
238
|
prev_tgts = [source_space]
|
|
258
239
|
next_srcs = []
|
|
259
|
-
for t1, t2 in
|
|
260
|
-
prev_tgts.append(t1.
|
|
261
|
-
next_srcs.append(t2.
|
|
240
|
+
for t1, t2 in pairwise(transforms):
|
|
241
|
+
prev_tgts.append(t1.spaces.target)
|
|
242
|
+
next_srcs.append(t2.spaces.source)
|
|
262
243
|
|
|
263
244
|
next_srcs.append(target_space)
|
|
264
245
|
|
|
@@ -268,7 +249,7 @@ def infer_spaces(
|
|
|
268
249
|
return out
|
|
269
250
|
|
|
270
251
|
|
|
271
|
-
def
|
|
252
|
+
def as_transform_list(t: Transform[ArrayT]) -> list[Transform[ArrayT]]:
|
|
272
253
|
if isinstance(t, TransformSequence):
|
|
273
254
|
return t.transforms.copy()
|
|
274
255
|
else:
|
|
@@ -282,7 +263,7 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
282
263
|
self,
|
|
283
264
|
transforms: Sequence[Transform[ArrayT]],
|
|
284
265
|
*,
|
|
285
|
-
spaces:
|
|
266
|
+
spaces: Spaces = Spaces(None, None),
|
|
286
267
|
) -> None:
|
|
287
268
|
"""Combine transforms by chaining them.
|
|
288
269
|
|
|
@@ -291,10 +272,10 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
291
272
|
|
|
292
273
|
Parameters
|
|
293
274
|
----------
|
|
294
|
-
transforms :
|
|
275
|
+
transforms :
|
|
295
276
|
Items which are a TransformSequences
|
|
296
277
|
will each still be treated as a single transform.
|
|
297
|
-
spaces :
|
|
278
|
+
spaces :
|
|
298
279
|
Optional source and target spaces.
|
|
299
280
|
Can also be inferred from the first and last transforms.
|
|
300
281
|
|
|
@@ -304,23 +285,19 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
304
285
|
If spaces are incompatible.
|
|
305
286
|
"""
|
|
306
287
|
ts = infer_spaces(transforms, *spaces)
|
|
288
|
+
if not ts:
|
|
289
|
+
raise ValueError("Empty transform sequence")
|
|
290
|
+
|
|
291
|
+
spaces = Spaces(ts[0].spaces.source, ts[-1].spaces.target)
|
|
292
|
+
ndims = NDims(ts[0].ndims.source, ts[-1].ndims.target)
|
|
307
293
|
|
|
308
294
|
super().__init__(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
ts[-1].target_space,
|
|
312
|
-
),
|
|
295
|
+
ndims,
|
|
296
|
+
spaces=spaces,
|
|
313
297
|
)
|
|
314
298
|
|
|
315
299
|
self.transforms: list[Transform[ArrayT]] = ts
|
|
316
300
|
|
|
317
|
-
self.ndim = None
|
|
318
|
-
for t in self.transforms:
|
|
319
|
-
self.ndim = dim_intersection(self.ndim, t.ndim)
|
|
320
|
-
|
|
321
|
-
if self.ndim is not None and len(self.ndim) == 0:
|
|
322
|
-
raise ValueError("Transforms have incompatible dimensionalities")
|
|
323
|
-
|
|
324
301
|
def __iter__(self) -> Iterator[Transform[ArrayT]]:
|
|
325
302
|
"""Iterate through component transforms.
|
|
326
303
|
|
|
@@ -346,7 +323,7 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
346
323
|
return None
|
|
347
324
|
return type(self)(
|
|
348
325
|
transforms,
|
|
349
|
-
spaces=
|
|
326
|
+
spaces=self.spaces.invert(),
|
|
350
327
|
)
|
|
351
328
|
|
|
352
329
|
def apply(self, coords: ArrayT) -> ArrayT:
|
|
@@ -371,7 +348,7 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
371
348
|
-------
|
|
372
349
|
List[SpaceRef]
|
|
373
350
|
"""
|
|
374
|
-
spaces = [self.
|
|
351
|
+
spaces = [self.spaces.source] + [t.spaces.target for t in self.transforms]
|
|
375
352
|
if skip_none:
|
|
376
353
|
spaces = [s for s in spaces if s is not None]
|
|
377
354
|
return spaces
|
|
@@ -389,7 +366,7 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
389
366
|
def is_identity(self) -> bool:
|
|
390
367
|
return all(t.is_identity() for t in self)
|
|
391
368
|
|
|
392
|
-
def simplify(self,
|
|
369
|
+
def simplify(self, drop_inverse: bool = True):
|
|
393
370
|
"""Reduce the number of transformations in this sequence if possible.
|
|
394
371
|
|
|
395
372
|
- Compose consecutive transformations which can be expressed as affines
|
|
@@ -403,15 +380,15 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
403
380
|
e.g. `Translation(1) | Translation(-1)`.
|
|
404
381
|
"""
|
|
405
382
|
from .transforms.bijection import Bijection
|
|
383
|
+
from .transforms import Identity
|
|
406
384
|
|
|
407
|
-
ndim = to_single_ndim(ndim, self.ndim)
|
|
408
385
|
out: list[Transform[ArrayT]] = []
|
|
409
386
|
affine = None
|
|
410
387
|
for t in self.transforms:
|
|
411
388
|
if drop_inverse and isinstance(t, Bijection):
|
|
412
389
|
t = t.forward
|
|
413
390
|
|
|
414
|
-
new_affine = t.to_affine(
|
|
391
|
+
new_affine = t.to_affine()
|
|
415
392
|
|
|
416
393
|
if new_affine is None:
|
|
417
394
|
if affine is not None:
|
|
@@ -428,7 +405,16 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
|
|
|
428
405
|
if affine is not None:
|
|
429
406
|
add_to_output(affine, out)
|
|
430
407
|
|
|
431
|
-
|
|
408
|
+
if not out:
|
|
409
|
+
out.append(Identity(self.ndims.source))
|
|
410
|
+
|
|
411
|
+
return type(self)(out, spaces=self.spaces)
|
|
412
|
+
|
|
413
|
+
def to_affine(self) -> Affine[ArrayT] | None:
|
|
414
|
+
simple = self.simplify(True)
|
|
415
|
+
if len(simple) != 1:
|
|
416
|
+
return None
|
|
417
|
+
return simple[0].to_affine()
|
|
432
418
|
|
|
433
419
|
|
|
434
420
|
def add_to_output(transform: Transform, lst: list[Transform]) -> bool:
|
|
@@ -436,6 +422,6 @@ def add_to_output(transform: Transform, lst: list[Transform]) -> bool:
|
|
|
436
422
|
return False
|
|
437
423
|
|
|
438
424
|
transform = copy(transform)
|
|
439
|
-
transform.spaces = (None, None)
|
|
425
|
+
transform.spaces = Spaces(None, None)
|
|
440
426
|
lst.append(transform)
|
|
441
427
|
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UNSPECIFIED_SPACE_NAME = "???"
|
|
@@ -4,7 +4,8 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
class Extents[ArrayT](ABC):
|
|
5
5
|
"""Base class for determining whether coordinates are "inside" a space."""
|
|
6
6
|
|
|
7
|
-
ndim:
|
|
7
|
+
def __init__(self, ndim: int) -> None:
|
|
8
|
+
self.ndim = ndim
|
|
8
9
|
|
|
9
10
|
@abstractmethod
|
|
10
11
|
def contains(self, coords: ArrayT) -> ArrayT: ...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from functools import lru_cache
|
|
2
2
|
from array_api_compat import array_namespace
|
|
3
|
-
from ..util import ArrayT
|
|
3
|
+
from ..util import ArrayT
|
|
4
4
|
from .base import Extents
|
|
5
5
|
from array_api_compat import device as xp_device
|
|
6
6
|
|
|
@@ -13,9 +13,9 @@ class BoundingBox(Extents[ArrayT]):
|
|
|
13
13
|
if len(xp.shape(mins)) != 1:
|
|
14
14
|
raise ValueError("mins and maxes must be 1D")
|
|
15
15
|
|
|
16
|
-
self.ndim = {xp.shape(mins)[0]}
|
|
17
16
|
self.mins = mins
|
|
18
17
|
self.maxes = maxes
|
|
18
|
+
super().__init__(xp.shape(mins)[0])
|
|
19
19
|
|
|
20
20
|
@lru_cache()
|
|
21
21
|
def extents_cast(self, namespace, device) -> tuple[ArrayT, ArrayT]:
|
|
@@ -25,7 +25,13 @@ class BoundingBox(Extents[ArrayT]):
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
def _validate_coords(self, coords: ArrayT) -> ArrayT:
|
|
28
|
-
|
|
28
|
+
xp = array_namespace(coords)
|
|
29
|
+
if xp.ndim(coords) != 2:
|
|
30
|
+
raise ValueError("Coords must be a 2D array")
|
|
31
|
+
dim = xp.shape(coords)[1]
|
|
32
|
+
if xp.shape(coords)[1] != self.ndim:
|
|
33
|
+
raise ValueError(f"Coords must have dimensionality {self.ndim}, got {dim}")
|
|
34
|
+
return coords
|
|
29
35
|
|
|
30
36
|
def contains(self, coords: ArrayT) -> ArrayT:
|
|
31
37
|
coords = self._validate_coords(coords)
|