transformnd 0.9.1__tar.gz → 0.10.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.
Files changed (32) hide show
  1. {transformnd-0.9.1 → transformnd-0.10.1}/PKG-INFO +3 -6
  2. {transformnd-0.9.1 → transformnd-0.10.1}/README.md +0 -4
  3. {transformnd-0.9.1 → transformnd-0.10.1}/pyproject.toml +28 -20
  4. {transformnd-0.9.1 → transformnd-0.10.1}/pyproject.toml.orig +28 -9
  5. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/__init__.py +10 -10
  6. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/base.py +4 -7
  7. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/bounding_box.py +4 -2
  8. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/polars.py +1 -0
  9. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/shapely.py +2 -2
  10. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/base.py +4 -6
  11. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/extents/bounding_box.py +5 -3
  12. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/graph.py +10 -9
  13. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/spaced.py +4 -4
  14. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/affine.py +11 -10
  15. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/bijection.py +1 -1
  16. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/by_dimension.py +27 -3
  17. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/grid.py +3 -1
  18. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/map_axis.py +4 -3
  19. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/moving_least_squares.py +3 -2
  20. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/project_axis.py +3 -0
  21. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/vector_field.py +2 -2
  22. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/types.py +4 -3
  23. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/util.py +5 -5
  24. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/__init__.py +4 -4
  25. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/adapters/pandas.py +0 -0
  26. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/extents/__init__.py +0 -0
  27. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/extents/base.py +0 -0
  28. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/py.typed +0 -0
  29. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/__init__.py +13 -13
  30. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/reflection.py +0 -0
  31. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/simple.py +4 -4
  32. {transformnd-0.9.1 → transformnd-0.10.1}/src/transformnd/transforms/thinplate.py +1 -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transformnd
3
- Version: 0.9.1
3
+ Version: 0.10.1
4
4
  Summary: ND coordinate transformations
5
5
  Author: Chris Barnes
6
6
  Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
19
20
  Requires-Dist: numpy>=2
20
21
  Requires-Dist: networkx>=3
21
22
  Requires-Dist: array-api-compat>=1.14
@@ -28,7 +29,7 @@ Requires-Dist: transformnd[adapters] ; extra == 'all'
28
29
  Requires-Dist: molesq>=0.4.0 ; extra == 'movingleastsquares'
29
30
  Requires-Dist: pandas>=3.0.2 ; extra == 'pandas'
30
31
  Requires-Dist: polars>=1.40.1 ; extra == 'polars'
31
- Requires-Dist: shapely>=2.1.2 ; extra == 'shapely'
32
+ Requires-Dist: shapely>=2.2 ; extra == 'shapely'
32
33
  Requires-Dist: morphops>=0.1.13 ; extra == 'thinplatesplines'
33
34
  Requires-Dist: transformnd[thinplatesplines] ; extra == 'transforms'
34
35
  Requires-Dist: transformnd[movingleastsquares] ; extra == 'transforms'
@@ -82,10 +83,6 @@ are of valid type and dimensions.
82
83
  Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
83
84
  and implements these adapters for a few common types.
84
85
 
85
- See the [tutorial here](https://github.com/clbarnes/transformnd/blob/main/examples/tutorial.py).
86
- It is a [marimo](https://marimo.io) notebook.
87
- Open it with `uv run --group examples marimo edit examples/tutorial.py`.
88
-
89
86
  ## Usage
90
87
 
91
88
  `transformnd` is pre-1.0 and the API is subject to change.
@@ -27,10 +27,6 @@ are of valid type and dimensions.
27
27
  Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
28
28
  and implements these adapters for a few common types.
29
29
 
30
- See the [tutorial here](https://github.com/clbarnes/transformnd/blob/main/examples/tutorial.py).
31
- It is a [marimo](https://marimo.io) notebook.
32
- Open it with `uv run --group examples marimo edit examples/tutorial.py`.
33
-
34
30
  ## Usage
35
31
 
36
32
  `transformnd` is pre-1.0 and the API is subject to change.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transformnd"
3
- version = "0.9.1"
3
+ version = "0.10.1"
4
4
  description = "ND coordinate transformations"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12, <4.0"
@@ -23,6 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Programming Language :: Python :: 3.13",
25
25
  "Programming Language :: Python :: 3.14",
26
+ "Programming Language :: Python :: 3.15",
26
27
  ]
27
28
 
28
29
  [[project.authors]]
@@ -38,7 +39,7 @@ issues = "https://github.com/clbarnes/transformnd/issues"
38
39
  thinplatesplines = ["morphops>=0.1.13"]
39
40
  movingleastsquares = ["molesq>=0.4.0"]
40
41
  pandas = ["pandas>=3.0.2"]
41
- shapely = ["shapely>=2.1.2"]
42
+ shapely = ["shapely>=2.2"]
42
43
  polars = ["polars>=1.40.1"]
43
44
  vectorfield = ["scipy>=1.17.1"]
44
45
  vectorfield-dask = [
@@ -62,9 +63,18 @@ all = [
62
63
  ]
63
64
 
64
65
  [dependency-groups]
66
+ dev = [
67
+ { include-group = "test" },
68
+ { include-group = "lint" },
69
+ { include-group = "doc" },
70
+ { include-group = "examples" },
71
+ { include-group = "bench" },
72
+ "prek>=0.3.11",
73
+ "rust-just>=1.58.0",
74
+ ]
65
75
  test = [
66
76
  "pytest",
67
- "jax",
77
+ "jax>=0.12",
68
78
  "dask>=2026.3.0",
69
79
  ]
70
80
  lint = [
@@ -74,31 +84,22 @@ lint = [
74
84
  "types-shapely>=2.1.0.20260408",
75
85
  "pydoclint>=0.8.6",
76
86
  ]
77
- doc = ["pdoc>=16.0.0"]
78
87
  examples = [
79
- "marimo>=0.9",
80
88
  "matplotlib>=3.10.8",
81
89
  "pandas>=3.0.2",
82
90
  "pooch>=1.9.0",
83
91
  "scikit-image>=0.26.0",
92
+ "jupytext>=1.19.6",
93
+ "ipykernel>=7.4.0",
94
+ "scipy",
95
+ ]
96
+ doc = [
97
+ "pdoc>=16.0.0",
98
+ "nbconvert>=7.17.1",
99
+ { include-group = "examples" },
84
100
  ]
85
101
  bench = ["pytest-benchmark>=5.2.3"]
86
102
 
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
103
  [build-system]
103
104
  requires = ["uv-build>=0.12.10,<0.13.0"]
104
105
  build-backend = "uv_build"
@@ -123,3 +124,10 @@ external = [
123
124
  "DOC",
124
125
  "D",
125
126
  ]
127
+ ignore = [
128
+ "C408",
129
+ "UP046",
130
+ ]
131
+
132
+ [tool.ruff.lint.extend-per-file-ignores]
133
+ "examples/*.py" = ["E402"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transformnd"
3
- version = "0.9.1"
3
+ version = "0.10.1"
4
4
  description = "ND coordinate transformations"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Chris Barnes", email = "chris.barnes@gerbi-gmb.de" }]
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.12",
25
25
  "Programming Language :: Python :: 3.13",
26
26
  "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
27
28
  ]
28
29
 
29
30
  [project.urls]
@@ -37,7 +38,7 @@ thinplatesplines = [
37
38
  ]
38
39
  movingleastsquares = ["molesq>=0.4.0"]
39
40
  pandas = ["pandas>=3.0.2"]
40
- shapely = ["shapely>=2.1.2"]
41
+ shapely = ["shapely>=2.2"]
41
42
  polars = [
42
43
  "polars>=1.40.1",
43
44
  ]
@@ -64,10 +65,12 @@ dev = [
64
65
  {include-group = "doc"},
65
66
  {include-group = "examples"},
66
67
  {include-group = "bench"},
68
+ "prek>=0.3.11",
69
+ "rust-just>=1.58.0",
67
70
  ]
68
71
  test = [
69
72
  "pytest",
70
- "jax",
73
+ "jax>=0.12",
71
74
  "dask>=2026.3.0",
72
75
  ]
73
76
  lint = [
@@ -77,15 +80,19 @@ lint = [
77
80
  "types-shapely>=2.1.0.20260408",
78
81
  "pydoclint>=0.8.6",
79
82
  ]
80
- doc = [
81
- "pdoc>=16.0.0",
82
- ]
83
83
  examples = [
84
- "marimo>=0.9",
85
84
  "matplotlib>=3.10.8",
86
85
  "pandas>=3.0.2",
87
86
  "pooch>=1.9.0",
88
87
  "scikit-image>=0.26.0",
88
+ "jupytext>=1.19.6",
89
+ "ipykernel>=7.4.0",
90
+ "scipy",
91
+ ]
92
+ doc = [
93
+ "pdoc>=16.0.0",
94
+ "nbconvert>=7.17.1",
95
+ {include-group = "examples"},
89
96
  ]
90
97
  bench = [
91
98
  "pytest-benchmark>=5.2.3",
@@ -107,8 +114,20 @@ addopts = ["--benchmark-skip"]
107
114
  arg-type-hints-in-docstring = false
108
115
  allow-init-docstring = true
109
116
 
110
- [tool.ruff]
111
- lint.external = [
117
+ [tool.ruff.lint]
118
+ external = [
112
119
  "DOC", # pydoclint
113
120
  "D", # pydocstyle
114
121
  ]
122
+ ignore = [
123
+ # Would ban dict() in favour of {};
124
+ # I prefer dict() to distinguish from set literals.
125
+ "C408",
126
+ # Prefers type parameters instead of inheriting from Generic[typevar].
127
+ # Currently, we need to use typevars because we python 3.12 doesn't support default type parameters.
128
+ # Should be revisited when python 3.12 is deprecated.
129
+ "UP046",
130
+ ]
131
+
132
+ [tool.ruff.lint.extend-per-file-ignores]
133
+ "examples/*.py" = ["E402"]
@@ -8,25 +8,25 @@ You can find some examples here:
8
8
 
9
9
  """
10
10
 
11
- from .base import Transform, TransformSequence, TransformFnWrapper
12
- from .types import TransformSignature, NDims, SpaceRef
13
- from . import transforms
14
- from . import adapters
11
+ from importlib.metadata import version as _version
12
+
13
+ from . import adapters, transforms
14
+ from .base import Transform, TransformFnWrapper, TransformSequence
15
15
  from .graph import TransformGraph
16
16
  from .spaced import Spaced
17
- from importlib.metadata import version as _version
17
+ from .types import NDims, SpaceRef, TransformSignature
18
18
 
19
19
  __version__ = _version("transformnd")
20
20
 
21
21
  __all__ = [
22
+ "NDims",
23
+ "SpaceRef",
24
+ "Spaced",
22
25
  "Transform",
26
+ "TransformFnWrapper",
23
27
  "TransformGraph",
24
28
  "TransformSequence",
25
- "TransformFnWrapper",
26
29
  "TransformSignature",
27
- "SpaceRef",
28
- "transforms",
29
30
  "adapters",
30
- "NDims",
31
- "Spaced",
31
+ "transforms",
32
32
  ]
@@ -1,14 +1,14 @@
1
1
  """Simple adapter cases."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
4
5
  from copy import deepcopy
5
6
  from functools import partial
6
- from collections.abc import Callable
7
- from typing import TypeVar, Any
7
+ from typing import Any, TypeVar
8
8
 
9
9
  from array_api_compat import array_namespace
10
10
 
11
- from ..base import Transform, ArrayT
11
+ from ..base import ArrayT, Transform
12
12
 
13
13
  ObjectT = TypeVar("ObjectT")
14
14
 
@@ -32,7 +32,6 @@ class BaseAdapter[ObjectT, ArrayT](ABC):
32
32
  ObjectT
33
33
  The transformed object.
34
34
  """
35
- pass
36
35
 
37
36
  def partial(self, *args: Any, **kwargs: Any) -> Callable[..., ObjectT]:
38
37
  """Create a partial function with frozen arguments.
@@ -135,7 +134,7 @@ class AttrAdapter(BaseAdapter[ObjectT, ArrayT]):
135
134
  if "got an unexpected keyword argument 'in_place'" in str(e):
136
135
  transformed = v.apply(transform, member)
137
136
  else:
138
- raise e
137
+ raise
139
138
  setattr(obj, k, transformed)
140
139
 
141
140
  return obj
@@ -149,12 +148,10 @@ class SimpleAdapter(BaseAdapter[ObjectT, ArrayT], ABC):
149
148
  @abstractmethod
150
149
  def _to_array(self, obj: ObjectT) -> ArrayT:
151
150
  """Convert the object into an array of coordinates."""
152
- pass
153
151
 
154
152
  @abstractmethod
155
153
  def _from_array(self, coords: ArrayT) -> ObjectT:
156
154
  """Convert an array of coordinates into the correct type."""
157
- pass
158
155
 
159
156
  def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
160
157
  coords: ArrayT = self._to_array(obj)
@@ -1,8 +1,10 @@
1
1
  from array_api_compat import array_namespace
2
+
2
3
  from transformnd.base import Transform
3
- from .base import BaseAdapter
4
- from ..extents.bounding_box import BoundingBox
4
+
5
5
  from ..base import ArrayT
6
+ from ..extents.bounding_box import BoundingBox
7
+ from .base import BaseAdapter
6
8
 
7
9
 
8
10
  class BoundingBoxAdapter(BaseAdapter[BoundingBox[ArrayT], ArrayT]):
@@ -1,6 +1,7 @@
1
1
  """Adapt polars DataFrames for transformation."""
2
2
 
3
3
  from typing import TYPE_CHECKING
4
+
4
5
  import numpy as np
5
6
 
6
7
  from ..base import Transform
@@ -3,12 +3,12 @@ from typing import TYPE_CHECKING
3
3
 
4
4
  import numpy as np
5
5
 
6
- from ..base import Transform, ArrayT
6
+ from ..base import ArrayT, Transform
7
7
  from .base import BaseAdapter
8
8
 
9
9
  if TYPE_CHECKING:
10
- from shapely.geometry.base import BaseGeometry
11
10
  from shapely.coords import CoordinateSequence
11
+ from shapely.geometry.base import BaseGeometry
12
12
 
13
13
 
14
14
  logger = logging.getLogger(__name__)
@@ -5,18 +5,17 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import Iterator, Sequence
7
7
  from copy import copy
8
- from typing import Self, TYPE_CHECKING
8
+ from itertools import pairwise
9
9
  from types import ModuleType
10
+ from typing import TYPE_CHECKING, Self
10
11
 
11
12
  from array_api_compat import array_namespace
12
13
 
14
+ from .types import NDims, TransformSignature
13
15
  from .util import (
14
16
  ArrayT,
15
17
  join_strs,
16
18
  )
17
- from itertools import pairwise
18
-
19
- from .types import TransformSignature, NDims
20
19
 
21
20
  if TYPE_CHECKING:
22
21
  from .transforms import Affine
@@ -96,7 +95,6 @@ class Transform[ArrayT](ABC):
96
95
  ArrayT
97
96
  Transformed coordinates in the same shape.
98
97
  """
99
- pass
100
98
 
101
99
  def invert(self) -> Transform[ArrayT] | None:
102
100
  """Invert the transformation, returning `None` if not possible."""
@@ -117,7 +115,7 @@ class Transform[ArrayT](ABC):
117
115
  return NotImplemented
118
116
  return t
119
117
 
120
- def to_device(self, xp: ModuleType, device: str | None = None) -> Self: # noqa: ARG002
118
+ def to_device(self, xp: ModuleType, device: str | None = None) -> Self:
121
119
  """Return a copy of this transform with array parameters placed on the given device.
122
120
 
123
121
  Useful for pre-allocating parameters on GPU before a tight apply() loop,
@@ -1,8 +1,10 @@
1
1
  from functools import lru_cache
2
+
2
3
  from array_api_compat import array_namespace
4
+ from array_api_compat import device as xp_device
5
+
3
6
  from ..util import ArrayT
4
7
  from .base import Extents
5
- from array_api_compat import device as xp_device
6
8
 
7
9
 
8
10
  class BoundingBox(Extents[ArrayT]):
@@ -15,10 +17,10 @@ class BoundingBox(Extents[ArrayT]):
15
17
 
16
18
  self.mins = mins
17
19
  self.maxes = maxes
20
+ self.extents_cast = lru_cache()(self._extents_cast)
18
21
  super().__init__(xp.shape(mins)[0])
19
22
 
20
- @lru_cache()
21
- def extents_cast(self, namespace, device) -> tuple[ArrayT, ArrayT]:
23
+ def _extents_cast(self, namespace, device) -> tuple[ArrayT, ArrayT]:
22
24
  return (
23
25
  namespace.asarray(self.mins, device=device),
24
26
  namespace.asarray(self.maxes, device=device),
@@ -1,24 +1,25 @@
1
1
  """Bridging transforms between known spaces."""
2
2
 
3
3
  from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping
4
7
  from copy import copy, deepcopy
5
8
  from functools import lru_cache
6
- from collections.abc import Callable, Iterable, Iterator, Mapping
7
- import logging
8
9
  from itertools import pairwise
9
10
  from types import ModuleType
10
- from typing import Any, Generic, Hashable, Self
11
- from typing_extensions import TypeVar
12
- from .spaced import Spaced
11
+ from typing import Any, Generic, Self
13
12
 
14
13
  import networkx as nx
14
+ from typing_extensions import TypeVar
15
15
 
16
16
  from transformnd.transforms.simple import Identity
17
17
 
18
- from .transforms.bijection import Bijection
19
18
  from .base import Transform, TransformSequence
20
- from .util import ArrayT
19
+ from .spaced import Spaced
20
+ from .transforms.bijection import Bijection
21
21
  from .types import SpaceRef
22
+ from .util import ArrayT
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
@@ -56,6 +57,7 @@ class TransformGraph(Generic[ArrayT, SpaceRef]):
56
57
  given transforms.
57
58
  """
58
59
  self.graph = nx.MultiDiGraph()
60
+ self.get_sequence = lru_cache()(self._get_sequence)
59
61
 
60
62
  def ndim(self, space: SpaceRef) -> int | None:
61
63
  """Get the dimensionality of the given space, or None if missing."""
@@ -196,8 +198,7 @@ class TransformGraph(Generic[ArrayT, SpaceRef]):
196
198
 
197
199
  return out
198
200
 
199
- @lru_cache()
200
- def get_sequence(
201
+ def _get_sequence(
201
202
  self,
202
203
  source_space: SpaceRef,
203
204
  target_space: SpaceRef,
@@ -1,8 +1,8 @@
1
1
  from collections.abc import Iterable, Sequence
2
- from typing import Self, Generic
2
+ from typing import Generic, Self
3
+
3
4
  from .base import Transform
4
- from .types import ArrayT, SrcTgt
5
- from .types import SpaceRef
5
+ from .types import ArrayT, SpaceRef, SrcTgt
6
6
 
7
7
 
8
8
  class Spaced(Generic[ArrayT, SpaceRef]):
@@ -38,7 +38,7 @@ class Spaced(Generic[ArrayT, SpaceRef]):
38
38
  t = self.transform.invert()
39
39
  if t is None:
40
40
  return None
41
- return type(self)(t, self.spaces.source, self.spaces.target)
41
+ return type(self)(t, self.spaces.target, self.spaces.source)
42
42
 
43
43
  def __invert__(self):
44
44
  out = self.invert()
@@ -5,18 +5,19 @@ Rigid transformations implemented as affine multiplications.
5
5
  from __future__ import annotations
6
6
 
7
7
  import math
8
- from typing import Container, Union, Self
8
+ from collections.abc import Container
9
+ from copy import copy
9
10
  from types import ModuleType
11
+ from typing import Self
10
12
 
11
13
  import numpy as np
14
+ from array_api_compat import array_namespace
15
+ from array_api_compat import device as xp_device
12
16
  from numpy.typing import ArrayLike
13
17
 
14
- from copy import copy
15
-
16
- from array_api_compat import array_namespace, device as xp_device
17
- from ..base import Transform, ArrayT
18
- from ..util import as_floats, is_square
18
+ from ..base import ArrayT, Transform
19
19
  from ..types import NDims
20
+ from ..util import as_floats, is_square
20
21
 
21
22
 
22
23
  class Affine(Transform[ArrayT]):
@@ -148,7 +149,7 @@ class Affine(Transform[ArrayT]):
148
149
  self.matrix @ rhs.matrix,
149
150
  )
150
151
 
151
- def to_device(self, xp: ModuleType, device: str | None = None) -> "Affine[ArrayT]":
152
+ def to_device(self, xp: ModuleType, device: str | None = None) -> Affine[ArrayT]:
152
153
  """Return a copy with the matrix placed on the given device/backend.
153
154
 
154
155
  Use this before a tight apply() loop to avoid per-call host-to-device
@@ -291,7 +292,7 @@ class Affine(Transform[ArrayT]):
291
292
  @classmethod
292
293
  def reflection(
293
294
  cls,
294
- axis: Union[int, Container[int]],
295
+ axis: int | Container[int],
295
296
  ndim: int,
296
297
  ) -> Affine[ArrayT]:
297
298
  """Create an affine reflection.
@@ -346,7 +347,7 @@ class Affine(Transform[ArrayT]):
346
347
  @classmethod
347
348
  def rotation3(
348
349
  cls,
349
- rotation: Union[float, tuple[float, float, float]],
350
+ rotation: float | tuple[float, float, float],
350
351
  degrees: bool = True,
351
352
  clockwise: bool = False,
352
353
  order: tuple[int, int, int] = (0, 1, 2),
@@ -402,7 +403,7 @@ class Affine(Transform[ArrayT]):
402
403
  @classmethod
403
404
  def shearing(
404
405
  cls,
405
- factor: Union[float, np.ndarray],
406
+ factor: float | np.ndarray,
406
407
  ndim: int | None = None,
407
408
  ) -> Affine[ArrayT]:
408
409
  """Create an affine shear.
@@ -4,7 +4,7 @@ from array_api_compat import array_namespace
4
4
 
5
5
  from transformnd.transforms.affine import Affine
6
6
 
7
- from ..base import Transform, ArrayT
7
+ from ..base import ArrayT, Transform
8
8
 
9
9
 
10
10
  class Bijection(Transform[ArrayT]):
@@ -1,12 +1,14 @@
1
1
  from collections.abc import Iterable
2
2
  from typing import Self
3
- from array_api_compat import array_namespace
4
3
 
5
- from .simple import Identity
4
+ import numpy as np
5
+ from array_api_compat import array_namespace
6
6
 
7
7
  from ..base import Transform
8
- from ..util import ArrayT, join_strs
9
8
  from ..types import NDims
9
+ from ..util import ArrayT, join_strs
10
+ from .affine import Affine
11
+ from .simple import Identity
10
12
 
11
13
 
12
14
  class SubTransform[ArrayT]:
@@ -164,6 +166,28 @@ class ByDimension(Transform[ArrayT]):
164
166
  subtransforms=inverted_transforms,
165
167
  )
166
168
 
169
+ def to_affine(self) -> Affine[ArrayT] | None:
170
+ """Combine the subtransforms' affines, if all subtransforms can be converted.
171
+
172
+ Each subtransform's linear map and translation are placed in the rows of its output axes,
173
+ and the columns of its input axes.
174
+
175
+ Returns
176
+ -------
177
+ Affine[ArrayT] | None
178
+ The affine transformation, if all subtransforms can be converted to affine.
179
+ None otherwise.
180
+ """
181
+ m = np.zeros((self.ndims.target + 1, self.ndims.source + 1))
182
+ m[-1, -1] = 1
183
+ for t in self.subtransforms:
184
+ sub_affine = t.transform.to_affine()
185
+ if sub_affine is None:
186
+ return None
187
+ m[np.ix_(t.output_axes, t.input_axes)] = sub_affine.matrix[:-1, :-1]
188
+ m[t.output_axes, -1] = sub_affine.matrix[:-1, -1]
189
+ return Affine(m)
190
+
167
191
  def is_identity(self) -> bool:
168
192
  for t in self.subtransforms:
169
193
  if t.input_axes != t.output_axes or not t.transform.is_identity():
@@ -1,8 +1,10 @@
1
- from typing import Any, Protocol, Generic
1
+ from typing import Any, Generic, Protocol
2
2
 
3
3
  from array_api_compat import array_namespace
4
+
4
5
  from transformnd.types import NDims
5
6
  from transformnd.util import join_strs
7
+
6
8
  from ..base import Transform
7
9
  from ..types import ArrayT
8
10
 
@@ -1,11 +1,12 @@
1
1
  from typing import Self
2
- from array_api_compat import array_namespace
2
+
3
3
  import numpy as np
4
+ from array_api_compat import array_namespace
4
5
 
5
6
  from ..base import Transform
6
- from ..util import ArrayT, join_strs
7
- from ..types import NDims
8
7
  from ..transforms.affine import Affine
8
+ from ..types import NDims
9
+ from ..util import ArrayT, join_strs
9
10
 
10
11
 
11
12
  class MapAxis(Transform[ArrayT]):
@@ -4,10 +4,11 @@ Implementation of Moving Least Squares transformation.
4
4
  Requires the `movingleastsquares` extra.
5
5
  """
6
6
 
7
- from array_api_compat import array_namespace
8
- import numpy as np
9
7
  from typing import Self
10
8
 
9
+ import numpy as np
10
+ from array_api_compat import array_namespace
11
+
11
12
  from ..base import Transform
12
13
  from ..types import NDims
13
14
  from ..util import as_floats
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import Self
3
4
 
4
5
  import numpy as np
5
6
  from array_api_compat import array_namespace
7
+
6
8
  from transformnd.transforms import Affine
7
9
  from transformnd.types import NDims
10
+
8
11
  from ..base import Transform
9
12
  from ..types import ArrayT
10
13
 
@@ -6,9 +6,9 @@ from typing import Self
6
6
  import numpy as np
7
7
  from array_api_compat import array_namespace, is_dask_array
8
8
 
9
+ from ..base import ArrayT, Transform
9
10
  from ..types import NDims
10
- from ..base import Transform, ArrayT
11
- from ..util import join_strs, set_scipy_array_api, as_floats
11
+ from ..util import as_floats, join_strs, set_scipy_array_api
12
12
 
13
13
  __all__ = ["Coordinates", "Displacements"]
14
14
 
@@ -1,7 +1,8 @@
1
- from collections.abc import Sequence
2
- from typing import Callable, Generic, Hashable, NamedTuple, Self
3
- from typing_extensions import TypeVar
1
+ from collections.abc import Callable, Hashable, Sequence
2
+ from typing import Generic, NamedTuple, Self
3
+
4
4
  import numpy as np
5
+ from typing_extensions import TypeVar
5
6
 
6
7
  ArrayT = TypeVar("ArrayT", default=np.ndarray)
7
8
 
@@ -1,17 +1,17 @@
1
1
  """Utilities used elsewhere in the package."""
2
2
 
3
- from collections.abc import Iterable
3
+ import logging
4
4
  import os
5
- from types import ModuleType
6
5
  import warnings
7
- import logging
6
+ from collections.abc import Iterable
7
+ from types import ModuleType
8
8
 
9
+ import numpy as np
9
10
  from array_api_compat import (
10
11
  array_namespace,
11
12
  is_dask_namespace,
12
13
  is_jax_namespace,
13
14
  )
14
- import numpy as np
15
15
 
16
16
  from .types import ArrayT
17
17
 
@@ -69,7 +69,7 @@ def chain_or[T](*args: T | None, default: _NoDefault | T = NO_DEFAULT) -> T:
69
69
  if arg is not None:
70
70
  return arg
71
71
  if isinstance(default, _NoDefault):
72
- raise ValueError("No non-None arguments")
72
+ raise ValueError("No non-None arguments") # noqa: TRY004
73
73
  return default
74
74
 
75
75
 
@@ -27,13 +27,13 @@ from .polars import PolarsAdapter
27
27
  from .shapely import ShapelyAdapter
28
28
 
29
29
  __all__ = [
30
+ "AttrAdapter",
30
31
  "BaseAdapter",
31
- "SimpleAdapter",
32
- "NullAdapter",
33
32
  "FnAdapter",
34
- "AttrAdapter",
35
- "ReshapeAdapter",
33
+ "NullAdapter",
36
34
  "PandasAdapter",
37
35
  "PolarsAdapter",
36
+ "ReshapeAdapter",
38
37
  "ShapelyAdapter",
38
+ "SimpleAdapter",
39
39
  ]
@@ -1,31 +1,31 @@
1
1
  """Implementations of some common transforms."""
2
2
 
3
3
  from .affine import Affine
4
- from .grid import GridInterpolation
5
- from .reflection import Reflect
6
- from .simple import Identity, Scale, Translate
7
- from .map_axis import MapAxis
8
4
  from .bijection import Bijection
9
- from .project_axis import ProjectAxis
10
5
  from .by_dimension import ByDimension, SubTransform
11
- from .vector_field import Coordinates, Displacements
6
+ from .grid import GridInterpolation
7
+ from .map_axis import MapAxis
12
8
  from .moving_least_squares import MovingLeastSquares
9
+ from .project_axis import ProjectAxis
10
+ from .reflection import Reflect
11
+ from .simple import Identity, Scale, Translate
13
12
  from .thinplate import ThinPlateSplines
13
+ from .vector_field import Coordinates, Displacements
14
14
 
15
15
  __all__ = [
16
16
  "Affine",
17
+ "Bijection",
18
+ "ByDimension",
19
+ "Coordinates",
20
+ "Displacements",
17
21
  "GridInterpolation",
18
22
  "Identity",
23
+ "MapAxis",
24
+ "MovingLeastSquares",
19
25
  "ProjectAxis",
20
26
  "Reflect",
21
27
  "Scale",
22
- "Translate",
23
- "MapAxis",
24
- "Bijection",
25
- "ByDimension",
26
28
  "SubTransform",
27
- "Coordinates",
28
- "Displacements",
29
- "MovingLeastSquares",
30
29
  "ThinPlateSplines",
30
+ "Translate",
31
31
  ]
@@ -3,17 +3,17 @@ Simple transformations like rigid translation and scaling.
3
3
  """
4
4
 
5
5
  from copy import copy
6
- from typing import Self
7
6
  from types import ModuleType
8
-
9
- from numpy.typing import ArrayLike
7
+ from typing import Self
10
8
 
11
9
  from array_api_compat import array_namespace
12
10
  from array_api_compat import device as xp_device
11
+ from numpy.typing import ArrayLike
12
+
13
13
  from ..base import Transform
14
+ from ..transforms.affine import Affine
14
15
  from ..types import NDims
15
16
  from ..util import ArrayT, as_floats, join_strs
16
- from ..transforms.affine import Affine
17
17
 
18
18
 
19
19
  class Identity(Transform[ArrayT]):
@@ -9,8 +9,8 @@ import logging
9
9
  import numpy as np
10
10
 
11
11
  from ..base import Transform
12
- from ..util import as_floats
13
12
  from ..types import NDims
13
+ from ..util import as_floats
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16