transformnd 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ """
2
+ .. include:: ../../README.md
3
+ """
4
+
5
+ from .base import Transform, TransformSequence, TransformWrapper
6
+ from .util import SpaceRef, TransformSignature, check_ndim
7
+ from . import transforms
8
+ from . import adapters
9
+ from .graph import TransformGraph
10
+ from importlib.metadata import version as _version
11
+
12
+ __version__ = _version("transformnd")
13
+
14
+ __all__ = [
15
+ "Transform",
16
+ "TransformGraph",
17
+ "TransformSequence",
18
+ "TransformWrapper",
19
+ "TransformSignature",
20
+ "SpaceRef",
21
+ "check_ndim",
22
+ "transforms",
23
+ "adapters",
24
+ ]
@@ -0,0 +1,34 @@
1
+ """Adapters for transforming objects which are not well-behaved numpy arrays.
2
+
3
+ Adapter instances are callables which take the transform to be applied,
4
+ the object to apply it to, and optionally some other arguments.
5
+ The adapter knows how to get coordinates out of the object,
6
+ and then create a new object with those transformed coordinates.
7
+
8
+ Classes which compose over transformable objects can be adapted with the
9
+ `AttrAdapter` class.
10
+ See the `SimpleAdapter` or `FnAdapter` for wrapping simple adapting functions.
11
+ Implement your own adapter by inheriting from `BaseAdapter`.
12
+
13
+ See `.pandas.DataFrameAdapter` for an example of creating an adapter
14
+ for an external type.
15
+
16
+ """
17
+
18
+ from .base import (
19
+ AttrAdapter,
20
+ BaseAdapter,
21
+ FnAdapter,
22
+ NullAdapter,
23
+ ReshapeAdapter,
24
+ SimpleAdapter,
25
+ )
26
+
27
+ __all__ = [
28
+ "BaseAdapter",
29
+ "SimpleAdapter",
30
+ "NullAdapter",
31
+ "FnAdapter",
32
+ "AttrAdapter",
33
+ "ReshapeAdapter",
34
+ ]
@@ -0,0 +1,167 @@
1
+ """Simple adapter cases."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from copy import deepcopy
5
+ from functools import partial
6
+ from collections.abc import Callable
7
+ from typing import TypeVar, Any
8
+
9
+ from array_api_compat import array_namespace
10
+
11
+ from ..base import Transform, ArrayT
12
+
13
+ ObjectT = TypeVar("ObjectT")
14
+
15
+
16
+ class BaseAdapter[ObjectT, ArrayT](ABC):
17
+ @abstractmethod
18
+ def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
19
+ """Apply the given transformation to a non-array object.
20
+
21
+ Parameters
22
+ ----------
23
+ transform : Transform
24
+ obj : T
25
+
26
+ Returns
27
+ -------
28
+ T
29
+ """
30
+ pass
31
+
32
+ def partial(self, *args, **kwargs) -> Callable[..., ObjectT]:
33
+ """Create a partial function with frozen arguments.
34
+
35
+ Useful for applying the same transform to many objects,
36
+ or many transforms to the same object,
37
+ or for adapters with additional arguments,
38
+ using the same config repeatedly.
39
+
40
+ Returns
41
+ -------
42
+ Callable
43
+ """
44
+ return partial(self.apply, *args, **kwargs)
45
+
46
+
47
+ class NullAdapter(BaseAdapter[ArrayT, ArrayT]):
48
+ """Adapter which simply applies the transform."""
49
+
50
+ def apply(self, transform: Transform[ArrayT], obj: ArrayT) -> ArrayT:
51
+ return transform.apply(obj)
52
+
53
+
54
+ class FnAdapter(BaseAdapter[ObjectT, ArrayT]):
55
+ def __init__(self, fn: Callable[[Transform[ArrayT], ObjectT], ObjectT]):
56
+ """Adapter which simply wraps a function, for typing purposes.
57
+
58
+ Parameters
59
+ ----------
60
+ fn : Callable[[Transform, T], T]
61
+ Function which takes the object,
62
+ and applies the transformation to it.
63
+ """
64
+ self.fn = fn
65
+
66
+ def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
67
+ return self.fn(transform, obj)
68
+
69
+
70
+ class AttrAdapter(BaseAdapter[ObjectT, ArrayT]):
71
+ def __init__(self, **kwargs: BaseAdapter[Any, ArrayT] | None) -> None:
72
+ """Adapter which transforms an object by applying transforms to its attributes.
73
+
74
+ Parameters
75
+ ----------
76
+ adapters : Dict[str, Optional[BaseAdapter]]
77
+ Keys are attribute names, values are adapters with which
78
+ to apply the transform to those attributes.
79
+ `None` is shorthand for `NullAdapter()`;
80
+ i.e. the attribute is an array and can be transformed
81
+ without being adapted.
82
+ """
83
+ self.adapters = {
84
+ k: NullAdapter[ArrayT]() if v is None else v for k, v in kwargs.items()
85
+ }
86
+
87
+ def apply(
88
+ self, transform: Transform[ArrayT], obj: ObjectT, in_place: bool = False
89
+ ) -> ObjectT:
90
+ """Apply the given transformation to the object, via its attributes.
91
+
92
+ Parameters
93
+ ----------
94
+ transform : Transform
95
+ obj : T
96
+ in_place : bool, optional
97
+ Whether to mutate the given object in place,
98
+ by default False (i.e. make a deep copy of it).
99
+
100
+ Returns
101
+ -------
102
+ T
103
+ """
104
+ if not in_place:
105
+ obj = deepcopy(obj)
106
+
107
+ for k, v in self.adapters.items():
108
+ member = getattr(obj, k)
109
+ try:
110
+ transformed = v.apply(transform, member, in_place=True) # type: ignore
111
+ except TypeError as e:
112
+ if "got an unexpected keyword argument 'in_place'" in str(e):
113
+ transformed = v.apply(transform, member)
114
+ else:
115
+ raise e
116
+ setattr(obj, k, transformed)
117
+
118
+ return obj
119
+
120
+
121
+ class SimpleAdapter(BaseAdapter[ObjectT, ArrayT], ABC):
122
+ """
123
+ Helper class for cases with simple conversion methods.
124
+ """
125
+
126
+ @abstractmethod
127
+ def _to_array(self, obj: ObjectT) -> ArrayT:
128
+ """Convert the object into an array of coordinates."""
129
+ pass
130
+
131
+ @abstractmethod
132
+ def _from_array(self, coords: ArrayT) -> ObjectT:
133
+ """Convert an array of coordinates into the correct type."""
134
+ pass
135
+
136
+ def apply(self, transform: Transform[ArrayT], obj: ObjectT) -> ObjectT:
137
+ coords: ArrayT = self._to_array(obj)
138
+ transformed = transform.apply(coords)
139
+ return self._from_array(transformed)
140
+
141
+
142
+ class ReshapeAdapter(BaseAdapter[ArrayT, ArrayT]):
143
+ """Adapter which reshapes a numpy.ndarray"""
144
+
145
+ def __init__(self, dim_axis: int = -1) -> None:
146
+ """Adapt numpy arrays which are not of the correct shape.
147
+
148
+ Parameters
149
+ ----------
150
+ dim_axis : int, optional
151
+ Which axis contains the coordinates' dimensions,
152
+ by default -1 (last)
153
+ """
154
+ self.dim_axis: int = dim_axis
155
+
156
+ def apply(self, transform: Transform[ArrayT], obj: ArrayT) -> ArrayT:
157
+ xp = array_namespace(obj)
158
+ dim_axis = self.dim_axis
159
+ if self.dim_axis < 0:
160
+ dim_axis += xp.ndim(obj)
161
+
162
+ moved = xp.moveaxis(obj, dim_axis, -1)
163
+ m_shape = moved.shape
164
+
165
+ flattened = xp.reshape(moved, (-1, m_shape[-1]))
166
+ transformed = transform.apply(flattened)
167
+ return xp.moveaxis(xp.reshape(transformed, m_shape), -1, dim_axis)
@@ -0,0 +1,16 @@
1
+ from array_api_compat import array_namespace
2
+ from transformnd.base import Transform
3
+ from .base import BaseAdapter
4
+ from ..extents.bounding_box import BoundingBox
5
+ from ..base import ArrayT
6
+
7
+
8
+ class BoundingBoxAdapter(BaseAdapter[BoundingBox[ArrayT], ArrayT]):
9
+ def apply(
10
+ self, transform: Transform[ArrayT], obj: BoundingBox[ArrayT]
11
+ ) -> BoundingBox[ArrayT]:
12
+ xp = array_namespace(obj.mins)
13
+ stacked = xp.stack([obj.mins, obj.maxes])
14
+ transformed = transform.apply(stacked)
15
+ mins, maxes = xp.unstack(transformed)
16
+ return BoundingBox(mins, maxes)
@@ -0,0 +1,46 @@
1
+ """Adapt pandas DataFrames for transformation."""
2
+
3
+ from collections.abc import Hashable
4
+
5
+ import pandas as pd
6
+ import numpy as np
7
+
8
+ from ..base import Transform
9
+ from .base import BaseAdapter
10
+
11
+
12
+ class PandasAdapter(BaseAdapter[pd.DataFrame, np.ndarray]):
13
+ def __init__(self, columns: list[Hashable]):
14
+ """Adapt transformation for coordinates stored in a pandas DataFrame.
15
+
16
+ Parameters
17
+ ----------
18
+ columns : list of keys
19
+ Keys for columns containing coordinates, e.g. `["x", "y", "z"]`
20
+ """
21
+ self.columns = columns
22
+
23
+ def apply(
24
+ self, transform: Transform, df: pd.DataFrame, in_place: bool = False
25
+ ) -> pd.DataFrame:
26
+ """Transform the dataframe, optionally in-place.
27
+
28
+ Parameters
29
+ ----------
30
+ transform : Transform
31
+ df : pd.DataFrame
32
+
33
+ in_place : bool, optional
34
+ Whether to mutate the dataframe in place,
35
+ by default False (i.e. make a copy of it).
36
+
37
+ Returns
38
+ -------
39
+ pandas.DataFrame
40
+ """
41
+ coords = df[self.columns].to_numpy()
42
+ transformed = transform.apply(coords)
43
+ if not in_place:
44
+ df = df.copy()
45
+ df[self.columns] = transformed
46
+ return df
@@ -0,0 +1,44 @@
1
+ """Adapt polars DataFrames for transformation."""
2
+
3
+ import polars as pl
4
+ import numpy as np
5
+
6
+ from ..base import Transform
7
+ from .base import BaseAdapter
8
+
9
+
10
+ class PolarsAdapter(BaseAdapter[pl.DataFrame, np.ndarray]):
11
+ def __init__(self, columns: list[str]):
12
+ """Adapt transformation for coordinates stored in a polars DataFrame.
13
+
14
+ Parameters
15
+ ----------
16
+ columns : list of keys
17
+ Keys for columns containing coordinates, e.g. `["x", "y", "z"]`
18
+ """
19
+ self.columns = columns
20
+
21
+ def apply(
22
+ self, transform: Transform, df: pl.DataFrame, in_place: bool = False
23
+ ) -> pl.DataFrame:
24
+ """Transform the dataframe, optionally in-place.
25
+
26
+ Parameters
27
+ ----------
28
+ transform : Transform
29
+ df : pl.DataFrame
30
+
31
+ in_place : bool, optional
32
+ Whether to mutate the dataframe in place,
33
+ by default False (i.e. make a copy of it).
34
+
35
+ Returns
36
+ -------
37
+ pandas.DataFrame
38
+ """
39
+ coords = df[self.columns].to_numpy()
40
+ transformed = transform.apply(coords)
41
+ if not in_place:
42
+ df = df.clone()
43
+ df[self.columns] = transformed
44
+ return df
@@ -0,0 +1,124 @@
1
+ import numpy as np
2
+ from typing import Callable
3
+ from shapely.geometry import (
4
+ LinearRing,
5
+ LineString,
6
+ MultiLineString,
7
+ MultiPoint,
8
+ MultiPolygon,
9
+ Point,
10
+ Polygon,
11
+ GeometryCollection,
12
+ )
13
+ from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
14
+ from shapely.coords import CoordinateSequence
15
+
16
+ from ..base import Transform, ArrayT
17
+ from .base import BaseAdapter
18
+
19
+
20
+ def as_numpy(coords: CoordinateSequence) -> np.ndarray:
21
+ return np.asarray(coords)
22
+
23
+
24
+ class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
25
+ """Transform shapely geometries.
26
+
27
+ As well as the generic `apply()`,
28
+ there are `apply_*()` methods for transforming different geometry subclasses.
29
+
30
+ N.B. some transforms may create invalid topologies
31
+ (incorrect winding, self-intersections etc.)
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ array_fn: Callable[[CoordinateSequence], ArrayT] = as_numpy, # type:ignore
37
+ ) -> None:
38
+ self.array_fn = array_fn
39
+
40
+ def apply_point(self, transform: Transform, point: Point) -> Point:
41
+ return Point(*transform.apply(self.array_fn(point.coords))[0])
42
+
43
+ def apply_linestring(
44
+ self, transform: Transform, linestring: LineString
45
+ ) -> LineString:
46
+ return LineString(transform.apply(self.array_fn(linestring.coords)))
47
+
48
+ def apply_linear_ring(
49
+ self, transform: Transform, linear_ring: LinearRing
50
+ ) -> LinearRing:
51
+ return LinearRing(transform.apply(self.array_fn(linear_ring.coords)))
52
+
53
+ def apply_polygon(self, transform: Transform, polygon: Polygon) -> Polygon:
54
+ return Polygon(
55
+ self.apply_linear_ring(transform, polygon.exterior),
56
+ [self.apply_linear_ring(transform, i) for i in polygon.interiors],
57
+ )
58
+
59
+ def apply_multipoint(self, transform: Transform, obj: MultiPoint) -> MultiPoint:
60
+ return MultiPoint([self.apply_point(transform, o) for o in obj.geoms])
61
+
62
+ def apply_multilinestring(
63
+ self, transform: Transform, obj: MultiLineString
64
+ ) -> MultiLineString:
65
+ return MultiLineString([self.apply_linestring(transform, o) for o in obj.geoms])
66
+
67
+ def apply_multipolygon(
68
+ self, transform: Transform, obj: MultiPolygon
69
+ ) -> MultiPolygon:
70
+ return MultiPolygon([self.apply_polygon(transform, o) for o in obj.geoms])
71
+
72
+ def apply_multipart(
73
+ self, transform: Transform, obj: BaseMultipartGeometry
74
+ ) -> BaseMultipartGeometry:
75
+ """Apply the transform to any shapely multipart geometry."""
76
+ if isinstance(obj, MultiPoint):
77
+ return self.apply_multipoint(transform, obj)
78
+ elif isinstance(obj, MultiLineString):
79
+ return self.apply_multilinestring(transform, obj)
80
+ elif isinstance(obj, MultiPolygon):
81
+ return self.apply_multipolygon(transform, obj)
82
+ elif isinstance(obj, GeometryCollection):
83
+ return self.apply_collection(transform, obj)
84
+ else:
85
+ raise ValueError(f"Unknown multipart geometry type {type(obj)}")
86
+
87
+ def apply_collection(
88
+ self, transform: Transform, obj: GeometryCollection
89
+ ) -> GeometryCollection:
90
+ return GeometryCollection([self.apply(transform, o) for o in obj.geoms])
91
+
92
+ def apply(
93
+ self,
94
+ transform: Transform,
95
+ obj: BaseGeometry,
96
+ ) -> BaseGeometry:
97
+ """Transform the shapely geometry.
98
+
99
+ See the other `apply_*` methods if you already know what type of geometry you're working with;
100
+ this may be a bit faster.
101
+
102
+ Parameters
103
+ ----------
104
+ transform : Transform
105
+ obj : BaseGeometry
106
+ Some shapely geometry in 2 or 3D
107
+
108
+ Returns
109
+ -------
110
+ BaseGeometry
111
+ An object of the same type as the input.
112
+ """
113
+ if isinstance(obj, BaseMultipartGeometry):
114
+ return self.apply_multipart(transform, obj)
115
+ elif isinstance(obj, Point):
116
+ return self.apply_point(transform, obj)
117
+ elif isinstance(obj, LineString):
118
+ return self.apply_linestring(transform, obj)
119
+ elif isinstance(obj, LinearRing):
120
+ return self.apply_linear_ring(transform, obj)
121
+ elif isinstance(obj, Polygon):
122
+ return self.apply_polygon(transform, obj)
123
+ else:
124
+ raise ValueError(f"Unknown geometry type {type(obj)}")