transformnd 0.5.0__tar.gz → 0.7.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.
Files changed (31) hide show
  1. {transformnd-0.5.0 → transformnd-0.7.0}/PKG-INFO +6 -7
  2. {transformnd-0.5.0 → transformnd-0.7.0}/README.md +5 -6
  3. {transformnd-0.5.0 → transformnd-0.7.0}/pyproject.toml +1 -1
  4. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/__init__.py +6 -1
  5. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/pandas.py +7 -4
  6. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/polars.py +7 -4
  7. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/shapely.py +10 -6
  8. transformnd-0.7.0/src/transformnd/transforms/__init__.py +31 -0
  9. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/affine.py +2 -2
  10. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/by_dimension.py +22 -1
  11. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/moving_least_squares.py +5 -2
  12. transformnd-0.7.0/src/transformnd/transforms/project_axis.py +173 -0
  13. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/thinplate.py +9 -4
  14. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/vector_field.py +8 -2
  15. transformnd-0.5.0/src/transformnd/transforms/__init__.py +0 -19
  16. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/__init__.py +0 -0
  17. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/base.py +0 -0
  18. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/bounding_box.py +0 -0
  19. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/base.py +0 -0
  20. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/constants.py +0 -0
  21. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/__init__.py +0 -0
  22. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/base.py +0 -0
  23. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/bounding_box.py +0 -0
  24. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/graph.py +0 -0
  25. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/py.typed +0 -0
  26. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/bijection.py +0 -0
  27. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/map_axis.py +0 -0
  28. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/reflection.py +0 -0
  29. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/simple.py +0 -0
  30. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/types.py +0 -0
  31. {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transformnd
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: ND coordinate transformations
5
5
  Author: Chris Barnes
6
6
  Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
@@ -99,10 +99,10 @@ All transforms are accessed under the `transformnd.transforms` subpackage.
99
99
  | `MapAxis` | | Rearrange axes of the input coordinates |
100
100
  | `Affine` | | Multiply augmented coordinates by an affine transformation matrix. Can represent all of the above transformations. Can be composed with matrix multiplication `aff2 @ aff1`. |
101
101
  | `ByDimension` | | Apply different transformations to subsets of the input coordinates' dimensions |
102
- | `moving_least_squares.MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
103
- | `thin_plate_splines.ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
104
- | `vector_field.Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
105
- | `vector_field.Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
102
+ | `MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
103
+ | `ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
104
+ | `Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
105
+ | `Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
106
106
 
107
107
  Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
108
108
  A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
@@ -110,13 +110,12 @@ A graph of transforms between defined spaces can be traversed using the `Transfo
110
110
  ## Implemented adapters
111
111
 
112
112
  - Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
113
- - `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
114
113
  - `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
115
114
  - Takes a subset of columns as a coordinate array
116
115
  - `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
117
116
  - Similar to the pandas adapter
118
117
  - Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
119
- - Geometries from `shapely` (`transformnd.adapters.shapely.GeometryAdapter`)
118
+ - Geometries from `shapely` (`transformnd.adapters.shapely.ShapelyAdapter`)
120
119
  - Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
121
120
 
122
121
  ## Additional transforms and adapters
@@ -44,10 +44,10 @@ All transforms are accessed under the `transformnd.transforms` subpackage.
44
44
  | `MapAxis` | | Rearrange axes of the input coordinates |
45
45
  | `Affine` | | Multiply augmented coordinates by an affine transformation matrix. Can represent all of the above transformations. Can be composed with matrix multiplication `aff2 @ aff1`. |
46
46
  | `ByDimension` | | Apply different transformations to subsets of the input coordinates' dimensions |
47
- | `moving_least_squares.MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
48
- | `thin_plate_splines.ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
49
- | `vector_field.Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
50
- | `vector_field.Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
47
+ | `MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
48
+ | `ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
49
+ | `Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
50
+ | `Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
51
51
 
52
52
  Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
53
53
  A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
@@ -55,13 +55,12 @@ A graph of transforms between defined spaces can be traversed using the `Transfo
55
55
  ## Implemented adapters
56
56
 
57
57
  - Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
58
- - `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
59
58
  - `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
60
59
  - Takes a subset of columns as a coordinate array
61
60
  - `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
62
61
  - Similar to the pandas adapter
63
62
  - Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
64
- - Geometries from `shapely` (`transformnd.adapters.shapely.GeometryAdapter`)
63
+ - Geometries from `shapely` (`transformnd.adapters.shapely.ShapelyAdapter`)
65
64
  - Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
66
65
 
67
66
  ## Additional transforms and adapters
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transformnd"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "ND coordinate transformations"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Chris Barnes", email = "chris.barnes@gerbi-gmb.de" }]
@@ -12,7 +12,6 @@ Implement your own adapter by inheriting from `BaseAdapter`.
12
12
 
13
13
  See `.pandas.DataFrameAdapter` for an example of creating an adapter
14
14
  for an external type.
15
-
16
15
  """
17
16
 
18
17
  from .base import (
@@ -23,6 +22,9 @@ from .base import (
23
22
  ReshapeAdapter,
24
23
  SimpleAdapter,
25
24
  )
25
+ from .pandas import PandasAdapter
26
+ from .polars import PolarsAdapter
27
+ from .shapely import ShapelyAdapter
26
28
 
27
29
  __all__ = [
28
30
  "BaseAdapter",
@@ -31,4 +33,7 @@ __all__ = [
31
33
  "FnAdapter",
32
34
  "AttrAdapter",
33
35
  "ReshapeAdapter",
36
+ "PandasAdapter",
37
+ "PolarsAdapter",
38
+ "ShapelyAdapter",
34
39
  ]
@@ -1,15 +1,18 @@
1
1
  """Adapt pandas DataFrames for transformation."""
2
2
 
3
3
  from collections.abc import Hashable
4
+ from typing import TYPE_CHECKING
4
5
 
5
- import pandas as pd
6
6
  import numpy as np
7
7
 
8
8
  from ..base import Transform
9
9
  from .base import BaseAdapter
10
10
 
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
11
13
 
12
- class PandasAdapter(BaseAdapter[pd.DataFrame, np.ndarray]):
14
+
15
+ class PandasAdapter(BaseAdapter["pd.DataFrame", np.ndarray]):
13
16
  def __init__(self, columns: list[Hashable]):
14
17
  """Adapt transformation for coordinates stored in a pandas DataFrame.
15
18
 
@@ -21,8 +24,8 @@ class PandasAdapter(BaseAdapter[pd.DataFrame, np.ndarray]):
21
24
  self.columns = columns
22
25
 
23
26
  def apply(
24
- self, transform: Transform, df: pd.DataFrame, in_place: bool = False
25
- ) -> pd.DataFrame:
27
+ self, transform: Transform, df: "pd.DataFrame", in_place: bool = False
28
+ ) -> "pd.DataFrame":
26
29
  """Transform the dataframe, optionally in-place.
27
30
 
28
31
  Parameters
@@ -1,13 +1,16 @@
1
1
  """Adapt polars DataFrames for transformation."""
2
2
 
3
- import polars as pl
3
+ from typing import TYPE_CHECKING
4
4
  import numpy as np
5
5
 
6
6
  from ..base import Transform
7
7
  from .base import BaseAdapter
8
8
 
9
+ if TYPE_CHECKING:
10
+ import polars as pl
9
11
 
10
- class PolarsAdapter(BaseAdapter[pl.DataFrame, np.ndarray]):
12
+
13
+ class PolarsAdapter(BaseAdapter["pl.DataFrame", np.ndarray]):
11
14
  def __init__(self, columns: list[str]):
12
15
  """Adapt transformation for coordinates stored in a polars DataFrame.
13
16
 
@@ -19,8 +22,8 @@ class PolarsAdapter(BaseAdapter[pl.DataFrame, np.ndarray]):
19
22
  self.columns = columns
20
23
 
21
24
  def apply(
22
- self, transform: Transform, df: pl.DataFrame, in_place: bool = False
23
- ) -> pl.DataFrame:
25
+ self, transform: Transform, df: "pl.DataFrame", in_place: bool = False
26
+ ) -> "pl.DataFrame":
24
27
  """Transform the dataframe, optionally in-place.
25
28
 
26
29
  Parameters
@@ -1,21 +1,24 @@
1
1
  import logging
2
+ from typing import TYPE_CHECKING
2
3
 
3
4
  import numpy as np
4
- import shapely
5
- from shapely.geometry.base import BaseGeometry
6
- from shapely.coords import CoordinateSequence
7
5
 
8
6
  from ..base import Transform, ArrayT
9
7
  from .base import BaseAdapter
10
8
 
9
+ if TYPE_CHECKING:
10
+ from shapely.geometry.base import BaseGeometry
11
+ from shapely.coords import CoordinateSequence
12
+
13
+
11
14
  logger = logging.getLogger(__name__)
12
15
 
13
16
 
14
- def as_numpy(coords: CoordinateSequence) -> np.ndarray:
17
+ def as_numpy(coords: "CoordinateSequence") -> np.ndarray:
15
18
  return np.asarray(coords)
16
19
 
17
20
 
18
- class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
21
+ class ShapelyAdapter(BaseAdapter["BaseGeometry", ArrayT]):
19
22
  """Transform shapely geometries.
20
23
 
21
24
  As well as the generic `apply()`,
@@ -27,7 +30,7 @@ class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
27
30
  N.B. shapely geometries' coordinates are in `XY(Z)` order
28
31
  """
29
32
 
30
- def apply[T: BaseGeometry](
33
+ def apply[T: "BaseGeometry"](
31
34
  self,
32
35
  transform: Transform,
33
36
  obj: T,
@@ -51,6 +54,7 @@ class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
51
54
  T
52
55
  An object of the same type as the input.
53
56
  """
57
+ import shapely
54
58
 
55
59
  def fn(coords: np.ndarray) -> np.ndarray:
56
60
  c = coords.copy()
@@ -0,0 +1,31 @@
1
+ """Implementations of some common transforms."""
2
+
3
+ from .affine import Affine
4
+ from .reflection import Reflect
5
+ from .simple import Identity, Scale, Translate
6
+ from .map_axis import MapAxis
7
+ from .bijection import Bijection
8
+ from .project_axis import ProjectAxis, Insert, Remove
9
+ from .by_dimension import ByDimension, SubTransform
10
+ from .vector_field import Coordinates, Displacements
11
+ from .moving_least_squares import MovingLeastSquares
12
+ from .thinplate import ThinPlateSplines
13
+
14
+ __all__ = [
15
+ "Affine",
16
+ "Identity",
17
+ "ProjectAxis",
18
+ "Insert",
19
+ "Remove",
20
+ "Reflect",
21
+ "Scale",
22
+ "Translate",
23
+ "MapAxis",
24
+ "Bijection",
25
+ "ByDimension",
26
+ "SubTransform",
27
+ "Coordinates",
28
+ "Displacements",
29
+ "MovingLeastSquares",
30
+ "ThinPlateSplines",
31
+ ]
@@ -42,7 +42,7 @@ class Affine(Transform[ArrayT]):
42
42
  ----------
43
43
  matrix
44
44
  Affine transformation matrix,
45
- i.e. a 2D array-like with shape `(Di + 1, Do + 1)`,
45
+ i.e. a 2D array-like with shape `(Do + 1, Di + 1)`,
46
46
  where the bottom row is all 0s except in the rightmost column, which is 1.
47
47
  spaces
48
48
  Optional source and target spaces
@@ -64,7 +64,7 @@ class Affine(Transform[ArrayT]):
64
64
  f"Transformation matrix is not affine (expected bottom row {expected}, got {bottom_row})."
65
65
  )
66
66
 
67
- super().__init__(NDims(m.shape[0] - 1, m.shape[1] - 1), spaces=spaces)
67
+ super().__init__(NDims(m.shape[1] - 1, m.shape[0] - 1), spaces=spaces)
68
68
 
69
69
  self.matrix: np.ndarray = m
70
70
 
@@ -8,7 +8,10 @@ from ..types import NDims, Spaces
8
8
 
9
9
 
10
10
  class SubTransform[ArrayT]:
11
- """Transformation to apply to subsets of the input dimensions and which output dimensions they calculate."""
11
+ """Component of the `ByDimension` transformation.
12
+
13
+ Transformation to apply to subsets of the input dimensions and which output dimensions they calculate.
14
+ """
12
15
 
13
16
  def __init__(
14
17
  self,
@@ -16,6 +19,24 @@ class SubTransform[ArrayT]:
16
19
  input_axes: list[int],
17
20
  output_axes: list[int] | None = None,
18
21
  ):
22
+ """
23
+ Parameters
24
+ ----------
25
+ transform
26
+ Transformation to apply to the subset of axes.
27
+ input_axes
28
+ Which axes to apply the transformation to, in order.
29
+ The length must match the input dimensionality of `transform`.
30
+ output_axes
31
+ Which axes to apply the transformation to, in order.
32
+ The length must match the input dimensionality of `transform`.
33
+ If None, re-use the input axes.
34
+
35
+ Raises
36
+ ------
37
+ ValueError
38
+ `transform`'s dimensionality does not match the input/output axes.
39
+ """
19
40
 
20
41
  self.input_axes = input_axes
21
42
  if output_axes is None:
@@ -7,7 +7,6 @@ Requires the `movingleastsquares` extra.
7
7
  from array_api_compat import array_namespace
8
8
  import numpy as np
9
9
  from typing import Self
10
- from molesq.transform import Transformer as _Transformer
11
10
 
12
11
  from ..base import Transform
13
12
  from ..types import NDims, Spaces
@@ -18,6 +17,8 @@ class MovingLeastSquares(Transform[np.ndarray]):
18
17
  """Moving least squares transformation.
19
18
 
20
19
  Deform based on a matched pairs of source and target control points; see <https://dl.acm.org/doi/10.1145/1141911.1141920>
20
+
21
+ REQUIRES: `movingleastsquares` extra.
21
22
  """
22
23
 
23
24
  def __init__(
@@ -39,9 +40,11 @@ class MovingLeastSquares(Transform[np.ndarray]):
39
40
  spaces
40
41
  Optional source and target spaces
41
42
  """
43
+ from molesq.transform import Transformer
44
+
42
45
  s = as_floats(source_control_points)
43
46
  t = as_floats(target_control_points)
44
- self._transformer = _Transformer(s, t)
47
+ self._transformer = Transformer(s, t)
45
48
  super().__init__(
46
49
  NDims(
47
50
  s.shape[1],
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from copy import copy
4
+ from typing import Self, Sequence
5
+
6
+ import numpy as np
7
+ from array_api_compat import array_namespace
8
+ from transformnd.transforms import Affine
9
+ from transformnd.types import NDims, Spaces
10
+ from dataclasses import dataclass
11
+ from ..base import Transform
12
+ from ..types import ArrayT
13
+
14
+
15
+ @dataclass(frozen=True, eq=True)
16
+ class BaseOperation(ABC):
17
+ idx: int
18
+ """Which axis to apply the operation to."""
19
+
20
+ def __post_init__(self):
21
+ if self.idx < 0:
22
+ raise ValueError("insert/remove idx must be positive")
23
+
24
+ @abstractmethod
25
+ def check(self, ndim: int) -> int: ...
26
+
27
+ @abstractmethod
28
+ def invert(self) -> BaseOperation: ...
29
+
30
+
31
+ @dataclass(frozen=True, eq=True)
32
+ class Insert(BaseOperation):
33
+ """Component of the `ProjectAxis` transform which inserts a new axis."""
34
+
35
+ def check(self, ndim: int) -> int:
36
+ if self.idx > ndim or self.idx <= -ndim:
37
+ raise ValueError(
38
+ f"Index {self.idx} is out of range for dimensionality {ndim}"
39
+ )
40
+ return ndim + 1
41
+
42
+ def invert(self) -> Remove:
43
+ return Remove(self.idx)
44
+
45
+
46
+ @dataclass(frozen=True, eq=True)
47
+ class Remove(BaseOperation):
48
+ """Component of the `ProjectAxis` transform which removes an existing axis."""
49
+
50
+ def check(self, ndim: int) -> int:
51
+ if self.idx >= ndim or self.idx <= -ndim:
52
+ raise ValueError(
53
+ f"Index {self.idx} is out of range for dimensionality {ndim}"
54
+ )
55
+ return ndim - 1
56
+
57
+ def invert(self) -> Insert:
58
+ if self.idx == -1:
59
+ raise ValueError("Removal of the -1th axis is not invertible")
60
+ return Insert(self.idx)
61
+
62
+
63
+ Operation = Insert | Remove
64
+ """Insert or remove an axis."""
65
+
66
+
67
+ class ProjectAxis(Transform):
68
+ """Transform for adding and removing axes.
69
+
70
+ WARNING: inverting this transformation may be lossy.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ operations: Sequence[Operation],
76
+ source_ndim: int | None = None,
77
+ target_ndim: int | None = None,
78
+ *,
79
+ spaces: Spaces = Spaces(None, None),
80
+ ):
81
+ """Create a transform for adding and dropping axes.
82
+
83
+ At least one of source_ndim and target_ndim must be given.
84
+
85
+ Parameters
86
+ ----------
87
+ operations
88
+ Sequence of operations to apply.
89
+ source_ndim
90
+ If omitted, can be inferred from `target_ndim`.
91
+ target_ndim
92
+ If omitted, can be inferred from `source_ndim`.
93
+ spaces
94
+ Identifiers for source and target spaces, by default Spaces(None, None)
95
+
96
+ Raises
97
+ ------
98
+ ValueError
99
+ Operations are inconsistent with given dimensionality,
100
+ or insufficient dimensionality information was given.
101
+ """
102
+ self.operations = []
103
+ self._has_inserts = False
104
+
105
+ if source_ndim is not None:
106
+ nd = source_ndim
107
+ for op in operations:
108
+ nd = op.check(nd)
109
+ if target_ndim is None:
110
+ target_ndim = nd
111
+ elif target_ndim != nd:
112
+ raise ValueError("Operations do not match expected target ndim")
113
+
114
+ elif target_ndim is not None:
115
+ nd = target_ndim
116
+ for op in reversed(operations):
117
+ nd = op.invert().check(nd)
118
+ if source_ndim is None:
119
+ source_ndim = nd
120
+ elif source_ndim != nd:
121
+ raise ValueError("Operations do not match expected source ndim")
122
+
123
+ else:
124
+ raise ValueError("At least one of source_ndim or target_ndim must be given")
125
+
126
+ idxs: list[int | None] = list(range(source_ndim))
127
+ for op in operations:
128
+ if isinstance(op, Insert):
129
+ self._has_inserts = True
130
+ idxs.insert(op.idx, None)
131
+ elif isinstance(op, Remove):
132
+ idxs.pop(op.idx)
133
+ self.operations.append(op)
134
+ self._idxs = idxs
135
+
136
+ super().__init__(NDims(source_ndim, target_ndim), spaces=spaces)
137
+
138
+ def apply(self, coords: ArrayT) -> ArrayT:
139
+ coords = self._validate_coords(coords)
140
+ if self._has_inserts:
141
+ xp = array_namespace(coords)
142
+ out = xp.zeros_like(coords, shape=(xp.shape(coords)[0], self.ndims.target))
143
+ for idx, orig_idx in enumerate(self._idxs):
144
+ if orig_idx is not None:
145
+ out[:, idx] = coords[:, orig_idx] # type:ignore
146
+
147
+ else:
148
+ out = coords[:, self._idxs] # type:ignore
149
+ return out
150
+
151
+ def is_identity(self) -> bool:
152
+ orig: list[int | None] = list(range(self.ndims.source))
153
+ dims = copy(orig)
154
+ for op in self.operations:
155
+ if isinstance(op, Insert):
156
+ dims.insert(op.idx, None)
157
+ elif isinstance(op, Remove):
158
+ dims.pop(op.idx)
159
+
160
+ return dims == orig
161
+
162
+ def to_affine(self) -> Affine | None:
163
+ m = np.eye(self.ndims.source)
164
+ out_m = self.apply(m)
165
+ return Affine.from_linear_map(out_m.T)
166
+
167
+ def invert(self) -> Self | None:
168
+ return type(self)(
169
+ [op.invert() for op in reversed(self.operations)],
170
+ source_ndim=self.ndims.target,
171
+ target_ndim=self.ndims.source,
172
+ spaces=self.spaces.invert(),
173
+ )
@@ -6,7 +6,6 @@ Requires the `thinplatesplines` extra.
6
6
 
7
7
  import logging
8
8
 
9
- import morphops as mops
10
9
  import numpy as np
11
10
 
12
11
  from ..base import Transform
@@ -20,6 +19,8 @@ class ThinPlateSplines(Transform[np.ndarray]):
20
19
  """Thin plate splines transforms.
21
20
 
22
21
  Deform based on matched pairs of control points.
22
+
23
+ REQUIRES: `thinplatesplines` extra.
23
24
  """
24
25
 
25
26
  def __init__(
@@ -48,6 +49,8 @@ class ThinPlateSplines(Transform[np.ndarray]):
48
49
  ValueError
49
50
  Invalid control points.
50
51
  """
52
+ import morphops
53
+
51
54
  self.source_control_points = as_floats(source_control_points)
52
55
  self.target_control_points = as_floats(target_control_points)
53
56
 
@@ -59,7 +62,7 @@ class ThinPlateSplines(Transform[np.ndarray]):
59
62
 
60
63
  ndim = self.source_control_points.shape[1]
61
64
 
62
- self.W, self.A = mops.tps_coefs(
65
+ self.W, self.A = morphops.tps_coefs(
63
66
  self.source_control_points,
64
67
  self.target_control_points,
65
68
  )
@@ -73,8 +76,10 @@ class ThinPlateSplines(Transform[np.ndarray]):
73
76
  )
74
77
 
75
78
  def apply(self, coords: np.ndarray) -> np.ndarray:
79
+ import morphops
80
+
76
81
  coords = self._validate_coords(coords)
77
- U = mops.K_matrix(coords, self.source_control_points)
78
- P = mops.P_matrix(coords)
82
+ U = morphops.K_matrix(coords, self.source_control_points)
83
+ P = morphops.P_matrix(coords)
79
84
  # The warped pts are the affine part + the non-uniform part
80
85
  return P @ self.A + U @ self.W
@@ -10,8 +10,6 @@ from ..types import NDims, Spaces
10
10
  from ..base import Transform, ArrayT
11
11
  from ..util import set_scipy_array_api, as_floats
12
12
 
13
- set_scipy_array_api()
14
-
15
13
  __all__ = ["Coordinates", "Displacements"]
16
14
 
17
15
 
@@ -95,6 +93,8 @@ class BaseVectorField(Transform[ArrayT], ABC):
95
93
 
96
94
  set_scipy_array_api()
97
95
  xp = array_namespace(index_coords_t)
96
+
97
+ # make columnar output array so that each dimension can be written contiguously
98
98
  out = xp.zeros_like(
99
99
  self.vector_field, shape=(self.ndims.target, xp.shape(index_coords_t)[1])
100
100
  )
@@ -133,6 +133,9 @@ class Coordinates(BaseVectorField[ArrayT]):
133
133
  the output coordinate is `vector_field[a, b, c, :].
134
134
 
135
135
  Input coordinates outside the vector field return NaN.
136
+
137
+ REQUIRES: `vectorfield` extra for in-memory,
138
+ or `vectorfield-dask` extra for lazy chunked vector fields.
136
139
  """
137
140
 
138
141
  def __init__(
@@ -182,6 +185,9 @@ class Displacements(BaseVectorField[ArrayT]):
182
185
  the output coordinate is `(a, b, c) + vector_field[a, b, c, :].
183
186
 
184
187
  Input coordinates outside the vector field return NaN.
188
+
189
+ REQUIRES: `vectorfield` extra for in-memory,
190
+ or `vectorfield-dask` extra for lazy chunked vector fields.
185
191
  """
186
192
 
187
193
  def __init__(
@@ -1,19 +0,0 @@
1
- """Implementations of some common transforms."""
2
-
3
- from .affine import Affine
4
- from .reflection import Reflect
5
- from .simple import Identity, Scale, Translate
6
- from .map_axis import MapAxis
7
- from .bijection import Bijection
8
- from .by_dimension import ByDimension
9
-
10
- __all__ = [
11
- "Affine",
12
- "Identity",
13
- "Reflect",
14
- "Scale",
15
- "Translate",
16
- "MapAxis",
17
- "Bijection",
18
- "ByDimension",
19
- ]