transformnd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transformnd-0.1.0/PKG-INFO +133 -0
- transformnd-0.1.0/README.md +109 -0
- transformnd-0.1.0/pyproject.toml +64 -0
- transformnd-0.1.0/src/transformnd/__init__.py +24 -0
- transformnd-0.1.0/src/transformnd/adapters/__init__.py +34 -0
- transformnd-0.1.0/src/transformnd/adapters/base.py +167 -0
- transformnd-0.1.0/src/transformnd/adapters/bounding_box.py +16 -0
- transformnd-0.1.0/src/transformnd/adapters/pandas.py +46 -0
- transformnd-0.1.0/src/transformnd/adapters/polars.py +44 -0
- transformnd-0.1.0/src/transformnd/adapters/shapely.py +124 -0
- transformnd-0.1.0/src/transformnd/base.py +441 -0
- transformnd-0.1.0/src/transformnd/extents/__init__.py +0 -0
- transformnd-0.1.0/src/transformnd/extents/base.py +10 -0
- transformnd-0.1.0/src/transformnd/extents/bounding_box.py +36 -0
- transformnd-0.1.0/src/transformnd/graph.py +192 -0
- transformnd-0.1.0/src/transformnd/py.typed +0 -0
- transformnd-0.1.0/src/transformnd/transforms/__init__.py +19 -0
- transformnd-0.1.0/src/transformnd/transforms/affine.py +512 -0
- transformnd-0.1.0/src/transformnd/transforms/bijection.py +68 -0
- transformnd-0.1.0/src/transformnd/transforms/by_dimension.py +133 -0
- transformnd-0.1.0/src/transformnd/transforms/map_axis.py +67 -0
- transformnd-0.1.0/src/transformnd/transforms/moving_least_squares.py +66 -0
- transformnd-0.1.0/src/transformnd/transforms/reflection.py +207 -0
- transformnd-0.1.0/src/transformnd/transforms/simple.py +172 -0
- transformnd-0.1.0/src/transformnd/transforms/thinplate.py +83 -0
- transformnd-0.1.0/src/transformnd/util.py +251 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transformnd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ND coordinate transformations
|
|
5
|
+
Author: Chris Barnes
|
|
6
|
+
Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: numpy>=2
|
|
9
|
+
Requires-Dist: networkx>=3
|
|
10
|
+
Requires-Dist: array-api-compat>=1.14
|
|
11
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
12
|
+
Requires-Dist: molesq>=0.4.0 ; extra == 'movingleastsquares'
|
|
13
|
+
Requires-Dist: pandas>=3.0.2 ; extra == 'pandas'
|
|
14
|
+
Requires-Dist: polars>=1.40.1 ; extra == 'polars'
|
|
15
|
+
Requires-Dist: shapely>=2.1.2 ; extra == 'shapely'
|
|
16
|
+
Requires-Dist: morphops>=0.1.13 ; extra == 'thinplatesplines'
|
|
17
|
+
Requires-Python: >=3.12, <4.0
|
|
18
|
+
Provides-Extra: movingleastsquares
|
|
19
|
+
Provides-Extra: pandas
|
|
20
|
+
Provides-Extra: polars
|
|
21
|
+
Provides-Extra: shapely
|
|
22
|
+
Provides-Extra: thinplatesplines
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# transformnd
|
|
26
|
+
|
|
27
|
+
[](https://github.com/psf/black)
|
|
28
|
+
[](https://github.com/clbarnes/transformnd/blob/main/LICENSE)
|
|
29
|
+
[](https://github.com/clbarnes/transformnd/actions/workflows/ci.yaml)
|
|
30
|
+
[](https://clbarnes.github.io/transformnd/)
|
|
31
|
+
|
|
32
|
+
A library providing an API for coordinate transformations,
|
|
33
|
+
as well as some common transforms.
|
|
34
|
+
The goal is to allow downstream applications which require such transformations
|
|
35
|
+
(e.g. image registration) to be generic over anything inheriting from `transformnd.Transform`.
|
|
36
|
+
|
|
37
|
+
The base classes and utilities are very lightweight with few dependencies, for use as an API; additional transforms and features use extras.
|
|
38
|
+
|
|
39
|
+
Heavily inspired by/ cribbed directly from
|
|
40
|
+
[Philipp Schlegel's work in navis](https://github.com/schlegelp/navis/tree/master/navis/transforms);
|
|
41
|
+
co-developed with [xform](https://github.com/schlegelp/xform/) as a red team prototype.
|
|
42
|
+
|
|
43
|
+
`N` coordinates in `D` dimensions are given as a numpy array of shape `(N, D)`.
|
|
44
|
+
|
|
45
|
+
`Transform` subclasses which are restricted to certain dimensionalities
|
|
46
|
+
can specify this in their `ndim` class variable.
|
|
47
|
+
Instances of `Transform` subclasses can further restrict their `ndim`.
|
|
48
|
+
Use `self._validate_coords(coords)` in `__call__` to ensure the coordinates
|
|
49
|
+
are of valid type and dimensions.
|
|
50
|
+
|
|
51
|
+
Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
|
|
52
|
+
and implements these adapters for a few common types.
|
|
53
|
+
|
|
54
|
+
See the [tutorial here](https://github.com/clbarnes/transformnd/blob/main/examples/tutorial.py).
|
|
55
|
+
It is a [marimo](https://marimo.io) notebook.
|
|
56
|
+
Open it with `uv run --group tutorial marimo edit examples/tutorial.py`.
|
|
57
|
+
|
|
58
|
+
## Implemented transforms
|
|
59
|
+
|
|
60
|
+
- Identity (`transformnd.transforms.Identity`)
|
|
61
|
+
- Translation (`transformnd.transforms.Translate`)
|
|
62
|
+
- Scale (`transformnd.transforms.Scale`)
|
|
63
|
+
- Reflection (`transformnd.transforms.Reflect`)
|
|
64
|
+
- Affine (`transformnd.transforms.Affine`)
|
|
65
|
+
- Can be composed efficiently with `@` operator; the right hand operand is effectively applied first
|
|
66
|
+
- MapAxis (`transformnd.transforms.MapAxis`): permute coordinate axes
|
|
67
|
+
- ByDimension (`transformnd.transforms.ByDimension`): apply transformations to subsets of coordinate axes
|
|
68
|
+
- Moving Least Squares, affine (`transformnd.transforms.moving_least_squares.MovingLeastSquares`)
|
|
69
|
+
- uses `movingleastsquares` extra
|
|
70
|
+
- Thin Plate Splines (`transformnd.transforms.thinplate.ThinPlateSplines`)
|
|
71
|
+
- uses `thinplatesplines` extra
|
|
72
|
+
|
|
73
|
+
Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
|
|
74
|
+
A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
|
|
75
|
+
|
|
76
|
+
## Implemented adapters
|
|
77
|
+
|
|
78
|
+
- Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
|
|
79
|
+
- `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
|
|
80
|
+
- `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
|
|
81
|
+
- Takes a subset of columns as a coordinate array
|
|
82
|
+
- `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
|
|
83
|
+
- Similar to the pandas adapter
|
|
84
|
+
- Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
|
|
85
|
+
- Geometries from `shapely` (`transformnd.adapters.shapely.GeometryAdapter`)
|
|
86
|
+
- Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
|
|
87
|
+
|
|
88
|
+
## Additional transforms and adapters
|
|
89
|
+
|
|
90
|
+
Contributions of additional transforms and adapters are welcome!
|
|
91
|
+
Even if they're only thin wrappers around an external library,
|
|
92
|
+
the downstream ecosystem benefits from a consistent API.
|
|
93
|
+
|
|
94
|
+
Such external transformation libraries should be specified as "extras",
|
|
95
|
+
and be contained in a submodule so that they are not immediately imported
|
|
96
|
+
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
|
+
|
|
100
|
+
Alternatively, consider adopting `transformnd`'s base classes in your own library,
|
|
101
|
+
and have your transformation instantly compatible for downstream users.
|
|
102
|
+
|
|
103
|
+
Methods which MUST be implemented:
|
|
104
|
+
|
|
105
|
+
- `__init__`: should validate parameters and set `self.ndim` if the parameters constrain the dimensionality
|
|
106
|
+
- `apply`: should call `_validate_coords` method early to check that the given coordinates are the correct shape
|
|
107
|
+
|
|
108
|
+
Methods which SHOULD be implemented if applicable:
|
|
109
|
+
|
|
110
|
+
- `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
|
+
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
112
|
+
- `into_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
113
|
+
- `invert`: if your transformation can be inverted (default None if not)
|
|
114
|
+
- This automatically implements `__invert__` (the `~my_transform` operator), which raises NotImplemented if `invert` would return `None`.
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
|
|
118
|
+
- Use [`uv`](https://docs.astral.sh/uv/) for environment and dependency management.
|
|
119
|
+
- `uv sync` to set up the environment.
|
|
120
|
+
- Use [`prek`](https://prek.j178.dev/) for running pre-commit hooks.
|
|
121
|
+
- `prek install-hooks && prek run --all-files` to get started.
|
|
122
|
+
- Use [`just`](https://github.com/casey/just) for common development tasks (format, lint, test, generate docs, run benchmarks).
|
|
123
|
+
- `just` to list commands.
|
|
124
|
+
- Docs are generated with `pdoc` (use `just doc`) and hosted on ReadTheDocs
|
|
125
|
+
|
|
126
|
+
## Thanks
|
|
127
|
+
|
|
128
|
+
Thanks to contributors
|
|
129
|
+
|
|
130
|
+
- [Francesca Drummer](https://github.com/FrancescaDr)
|
|
131
|
+
- [Lorenzo Cerrone](https://github.com/lorenzocerrone)
|
|
132
|
+
- [Maks Hess](https://github.com/MaksHess)
|
|
133
|
+
- [Silvia Maria Macrì](https://github.com/SilviaMariaMacri)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# transformnd
|
|
2
|
+
|
|
3
|
+
[](https://github.com/psf/black)
|
|
4
|
+
[](https://github.com/clbarnes/transformnd/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/clbarnes/transformnd/actions/workflows/ci.yaml)
|
|
6
|
+
[](https://clbarnes.github.io/transformnd/)
|
|
7
|
+
|
|
8
|
+
A library providing an API for coordinate transformations,
|
|
9
|
+
as well as some common transforms.
|
|
10
|
+
The goal is to allow downstream applications which require such transformations
|
|
11
|
+
(e.g. image registration) to be generic over anything inheriting from `transformnd.Transform`.
|
|
12
|
+
|
|
13
|
+
The base classes and utilities are very lightweight with few dependencies, for use as an API; additional transforms and features use extras.
|
|
14
|
+
|
|
15
|
+
Heavily inspired by/ cribbed directly from
|
|
16
|
+
[Philipp Schlegel's work in navis](https://github.com/schlegelp/navis/tree/master/navis/transforms);
|
|
17
|
+
co-developed with [xform](https://github.com/schlegelp/xform/) as a red team prototype.
|
|
18
|
+
|
|
19
|
+
`N` coordinates in `D` dimensions are given as a numpy array of shape `(N, D)`.
|
|
20
|
+
|
|
21
|
+
`Transform` subclasses which are restricted to certain dimensionalities
|
|
22
|
+
can specify this in their `ndim` class variable.
|
|
23
|
+
Instances of `Transform` subclasses can further restrict their `ndim`.
|
|
24
|
+
Use `self._validate_coords(coords)` in `__call__` to ensure the coordinates
|
|
25
|
+
are of valid type and dimensions.
|
|
26
|
+
|
|
27
|
+
Additionally, `transformnd` provides an interface for transforming types other than NxD numpy arrays,
|
|
28
|
+
and implements these adapters for a few common types.
|
|
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 tutorial marimo edit examples/tutorial.py`.
|
|
33
|
+
|
|
34
|
+
## Implemented transforms
|
|
35
|
+
|
|
36
|
+
- Identity (`transformnd.transforms.Identity`)
|
|
37
|
+
- Translation (`transformnd.transforms.Translate`)
|
|
38
|
+
- Scale (`transformnd.transforms.Scale`)
|
|
39
|
+
- Reflection (`transformnd.transforms.Reflect`)
|
|
40
|
+
- Affine (`transformnd.transforms.Affine`)
|
|
41
|
+
- Can be composed efficiently with `@` operator; the right hand operand is effectively applied first
|
|
42
|
+
- MapAxis (`transformnd.transforms.MapAxis`): permute coordinate axes
|
|
43
|
+
- ByDimension (`transformnd.transforms.ByDimension`): apply transformations to subsets of coordinate axes
|
|
44
|
+
- Moving Least Squares, affine (`transformnd.transforms.moving_least_squares.MovingLeastSquares`)
|
|
45
|
+
- uses `movingleastsquares` extra
|
|
46
|
+
- Thin Plate Splines (`transformnd.transforms.thinplate.ThinPlateSplines`)
|
|
47
|
+
- uses `thinplatesplines` extra
|
|
48
|
+
|
|
49
|
+
Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
|
|
50
|
+
A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
|
|
51
|
+
|
|
52
|
+
## Implemented adapters
|
|
53
|
+
|
|
54
|
+
- Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
|
|
55
|
+
- `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
|
|
56
|
+
- `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
|
|
57
|
+
- Takes a subset of columns as a coordinate array
|
|
58
|
+
- `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
|
|
59
|
+
- Similar to the pandas adapter
|
|
60
|
+
- Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
|
|
61
|
+
- Geometries from `shapely` (`transformnd.adapters.shapely.GeometryAdapter`)
|
|
62
|
+
- Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
|
|
63
|
+
|
|
64
|
+
## Additional transforms and adapters
|
|
65
|
+
|
|
66
|
+
Contributions of additional transforms and adapters are welcome!
|
|
67
|
+
Even if they're only thin wrappers around an external library,
|
|
68
|
+
the downstream ecosystem benefits from a consistent API.
|
|
69
|
+
|
|
70
|
+
Such external transformation libraries should be specified as "extras",
|
|
71
|
+
and be contained in a submodule so that they are not immediately imported
|
|
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
|
+
|
|
76
|
+
Alternatively, consider adopting `transformnd`'s base classes in your own library,
|
|
77
|
+
and have your transformation instantly compatible for downstream users.
|
|
78
|
+
|
|
79
|
+
Methods which MUST be implemented:
|
|
80
|
+
|
|
81
|
+
- `__init__`: should validate parameters and set `self.ndim` if the parameters constrain the dimensionality
|
|
82
|
+
- `apply`: should call `_validate_coords` method early to check that the given coordinates are the correct shape
|
|
83
|
+
|
|
84
|
+
Methods which SHOULD be implemented if applicable:
|
|
85
|
+
|
|
86
|
+
- `to_device`: if any of the transformation's parameters need to be placed on a specific device (e.g. affine matrices on the GPU)
|
|
87
|
+
- `is_identity`: if you can cheaply check whether your transformation is an identity transformation. The base class implementation returns `False`.
|
|
88
|
+
- `into_affine`: if your transformation can be represented as an affine matrix. The base class implementation returns `None`.
|
|
89
|
+
- `invert`: if your transformation can be inverted (default None if not)
|
|
90
|
+
- This automatically implements `__invert__` (the `~my_transform` operator), which raises NotImplemented if `invert` would return `None`.
|
|
91
|
+
|
|
92
|
+
## Contributing
|
|
93
|
+
|
|
94
|
+
- Use [`uv`](https://docs.astral.sh/uv/) for environment and dependency management.
|
|
95
|
+
- `uv sync` to set up the environment.
|
|
96
|
+
- Use [`prek`](https://prek.j178.dev/) for running pre-commit hooks.
|
|
97
|
+
- `prek install-hooks && prek run --all-files` to get started.
|
|
98
|
+
- Use [`just`](https://github.com/casey/just) for common development tasks (format, lint, test, generate docs, run benchmarks).
|
|
99
|
+
- `just` to list commands.
|
|
100
|
+
- Docs are generated with `pdoc` (use `just doc`) and hosted on ReadTheDocs
|
|
101
|
+
|
|
102
|
+
## Thanks
|
|
103
|
+
|
|
104
|
+
Thanks to contributors
|
|
105
|
+
|
|
106
|
+
- [Francesca Drummer](https://github.com/FrancescaDr)
|
|
107
|
+
- [Lorenzo Cerrone](https://github.com/lorenzocerrone)
|
|
108
|
+
- [Maks Hess](https://github.com/MaksHess)
|
|
109
|
+
- [Silvia Maria Macrì](https://github.com/SilviaMariaMacri)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "transformnd"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ND coordinate transformations"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Chris Barnes", email = "chris.barnes@gerbi-gmb.de" }]
|
|
7
|
+
requires-python = ">=3.12, <4.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"numpy>=2",
|
|
10
|
+
"networkx>=3",
|
|
11
|
+
"array_api_compat>=1.14",
|
|
12
|
+
"typing-extensions>=4.15.0",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
thinplatesplines = [
|
|
18
|
+
"morphops>=0.1.13",
|
|
19
|
+
]
|
|
20
|
+
movingleastsquares = ["molesq>=0.4.0"]
|
|
21
|
+
pandas = ["pandas>=3.0.2"]
|
|
22
|
+
shapely = ["shapely>=2.1.2"]
|
|
23
|
+
polars = [
|
|
24
|
+
"polars>=1.40.1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
{include-group = "test"},
|
|
30
|
+
{include-group = "lint"},
|
|
31
|
+
{include-group = "doc"},
|
|
32
|
+
{include-group = "tutorial"},
|
|
33
|
+
{include-group = "bench"},
|
|
34
|
+
]
|
|
35
|
+
test = ["pytest", "jax"]
|
|
36
|
+
lint = [
|
|
37
|
+
"ruff",
|
|
38
|
+
"mypy",
|
|
39
|
+
"prek>=0.3.9",
|
|
40
|
+
"types-shapely>=2.1.0.20260408",
|
|
41
|
+
]
|
|
42
|
+
doc = [
|
|
43
|
+
"pdoc>=16.0.0",
|
|
44
|
+
]
|
|
45
|
+
tutorial = [
|
|
46
|
+
"marimo>=0.9",
|
|
47
|
+
"matplotlib>=3.10.8",
|
|
48
|
+
"pandas>=3.0.2",
|
|
49
|
+
]
|
|
50
|
+
bench = [
|
|
51
|
+
"pytest-benchmark>=5.2.3",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
56
|
+
build-backend = "uv_build"
|
|
57
|
+
|
|
58
|
+
[tool.mypy]
|
|
59
|
+
ignore_missing_imports = true
|
|
60
|
+
check_untyped_defs = true
|
|
61
|
+
|
|
62
|
+
[tool.pytest]
|
|
63
|
+
testpaths = ["tests", "bench"]
|
|
64
|
+
addopts = ["--benchmark-skip"]
|
|
@@ -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
|