transformnd 0.4.2__tar.gz → 0.5.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 (29) hide show
  1. {transformnd-0.4.2 → transformnd-0.5.0}/PKG-INFO +1 -1
  2. {transformnd-0.4.2 → transformnd-0.5.0}/pyproject.toml +1 -1
  3. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/base.py +32 -4
  4. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/graph.py +69 -148
  5. {transformnd-0.4.2 → transformnd-0.5.0}/README.md +0 -0
  6. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/__init__.py +0 -0
  7. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/__init__.py +0 -0
  8. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/base.py +0 -0
  9. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/bounding_box.py +0 -0
  10. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/pandas.py +0 -0
  11. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/polars.py +0 -0
  12. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/adapters/shapely.py +0 -0
  13. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/constants.py +0 -0
  14. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/extents/__init__.py +0 -0
  15. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/extents/base.py +0 -0
  16. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/extents/bounding_box.py +0 -0
  17. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/py.typed +0 -0
  18. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/__init__.py +0 -0
  19. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/affine.py +0 -0
  20. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/bijection.py +0 -0
  21. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/by_dimension.py +0 -0
  22. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/map_axis.py +0 -0
  23. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/moving_least_squares.py +0 -0
  24. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/reflection.py +0 -0
  25. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/simple.py +0 -0
  26. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/thinplate.py +0 -0
  27. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/transforms/vector_field.py +0 -0
  28. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/types.py +0 -0
  29. {transformnd-0.4.2 → transformnd-0.5.0}/src/transformnd/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: transformnd
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: ND coordinate transformations
5
5
  Author: Chris Barnes
6
6
  Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "transformnd"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "ND coordinate transformations"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Chris Barnes", email = "chris.barnes@gerbi-gmb.de" }]
@@ -370,6 +370,20 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
370
370
  spaces = [s for s in spaces if s is not None]
371
371
  return spaces
372
372
 
373
+ def split(self) -> Iterator[Transform[ArrayT]]:
374
+ """Split the sequence where an intermediate space is known."""
375
+ this_seq = []
376
+
377
+ for t in self.transforms:
378
+ if t.spaces.source is not None and t.spaces.target is not None:
379
+ yield t
380
+ continue
381
+
382
+ this_seq.append(t)
383
+ if t.spaces.target is not None:
384
+ yield type(self)(this_seq)
385
+ this_seq = []
386
+
373
387
  def __str__(self) -> str:
374
388
  cls_name = type(self).__name__
375
389
  spaces_str = "->".join(space_str(s) for s in self.list_spaces())
@@ -383,6 +397,21 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
383
397
  def is_identity(self) -> bool:
384
398
  return all(t.is_identity() for t in self)
385
399
 
400
+ def flatten(self, drop_inverse: bool = True) -> Self:
401
+ """Flatten nested sequences."""
402
+ from .transforms.bijection import Bijection
403
+
404
+ out: list[Transform[ArrayT]] = []
405
+
406
+ for t in self.transforms:
407
+ if drop_inverse and isinstance(t, Bijection):
408
+ t = t.forward
409
+ if isinstance(t, TransformSequence):
410
+ out.extend(t.flatten())
411
+ else:
412
+ out.append(t)
413
+ return TransformSequence(out, spaces=self.spaces) # type:ignore
414
+
386
415
  def simplify(self, drop_inverse: bool = True):
387
416
  """Reduce the number of transformations in this sequence if possible.
388
417
 
@@ -396,14 +425,13 @@ class TransformSequence(Transform[ArrayT], Sequence[Transform[ArrayT]]):
396
425
  Does not check whether transforms invert each other,
397
426
  e.g. `Translation(1) | Translation(-1)`.
398
427
  """
399
- from .transforms.bijection import Bijection
400
428
  from .transforms import Identity
401
429
 
402
430
  out: list[Transform[ArrayT]] = []
403
431
  affine = None
404
- for t in self.transforms:
405
- if drop_inverse and isinstance(t, Bijection):
406
- t = t.forward
432
+ for t in self.flatten(drop_inverse):
433
+ if t.is_identity():
434
+ continue
407
435
 
408
436
  new_affine = t.to_affine()
409
437
 
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
  from functools import lru_cache
5
- from collections.abc import Iterable, Iterator
5
+ from collections.abc import Callable, Iterator
6
6
  import logging
7
- from itertools import chain, pairwise
7
+ from itertools import pairwise
8
8
  from types import ModuleType
9
+ from typing import Any
9
10
 
10
11
  import networkx as nx
11
12
 
@@ -16,6 +17,9 @@ from .types import Spaces
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
20
+ TRANSFORM_KEY = "_transform"
21
+ WeightFn = Callable[[SpaceRef, SpaceRef, dict[str, Any]], int]
22
+
19
23
 
20
24
  def split_sequence(seq: TransformSequence[ArrayT]) -> Iterator[Transform[ArrayT]]:
21
25
  """Split a TransformSequence into Transforms with spaces defined.
@@ -46,6 +50,15 @@ def split_sequence(seq: TransformSequence[ArrayT]) -> Iterator[Transform[ArrayT]
46
50
  this_seq = []
47
51
 
48
52
 
53
+ def normalise_edge_weight_fn(w: str | WeightFn | None) -> WeightFn:
54
+ if w is None:
55
+ return lambda _s, _t, _d: 1
56
+ elif isinstance(w, str):
57
+ return lambda _s, _t, d: d.get(w, 1)
58
+ else:
59
+ return w
60
+
61
+
49
62
  class TransformGraph[ArrayT]:
50
63
  """Transform between any number of arbitrary spaces/ coordinate systems.
51
64
 
@@ -57,18 +70,14 @@ class TransformGraph[ArrayT]:
57
70
 
58
71
  def __init__(
59
72
  self,
60
- transforms: Iterable[Transform[ArrayT]] | None = None,
61
- and_inverse: bool = True,
62
73
  ):
63
74
  """Create an transform graph, optionally with some starting transforms.
64
75
 
65
76
  See the `TransformGraph.add_transforms` documentation for restrictions on the
66
77
  given transforms.
67
78
  """
68
- self.graph = nx.DiGraph()
79
+ self.graph = nx.MultiDiGraph()
69
80
  self.space_ndims: dict[SpaceRef, int] = dict()
70
- if transforms is not None:
71
- self.add_transforms(transforms, and_inverse)
72
81
 
73
82
  def _update_spaces(
74
83
  self,
@@ -99,48 +108,22 @@ class TransformGraph[ArrayT]:
99
108
  transform: Transform[ArrayT],
100
109
  source: SpaceRef | None,
101
110
  target: SpaceRef | None,
102
- and_inverse: bool,
111
+ edge_data: dict[str, Any] | None,
103
112
  ) -> list[tuple[SpaceRef, SpaceRef]]:
104
113
  """Clearing the get_sequence cache and splitting sequences and bijections should be handled outside this method."""
105
114
  out = []
106
115
 
107
116
  src, tgt = self._update_spaces(transform, source, target)
108
117
 
109
- if self.graph.has_edge(src, tgt):
110
- logger.warning(f"Replacing existing edge between {src} and {tgt}")
118
+ if edge_data is None:
119
+ edge_data = dict()
111
120
 
112
- self.graph.add_edge(src, tgt, transform=transform)
113
- out.append((src, tgt))
114
- if and_inverse:
115
- out.extend(self._add_inverse(transform, src, tgt))
116
- return out
117
-
118
- def _add_inverse(
119
- self,
120
- transform: Transform[ArrayT],
121
- source: SpaceRef | None,
122
- target: SpaceRef | None,
123
- ) -> list[tuple[SpaceRef, SpaceRef]]:
124
- src, tgt = self._update_spaces(transform, source, target)
125
- out = []
121
+ if TRANSFORM_KEY in edge_data:
122
+ raise ValueError(f"Must not use the key '{TRANSFORM_KEY}' in edge_data")
126
123
 
127
- if self.graph.has_edge(tgt, src):
128
- logger.debug(
129
- "Implicit reverse edge not added to graph as explicit edge already exists for %s->%s",
130
- tgt,
131
- src,
132
- )
133
- elif t := transform.invert():
134
- if isinstance(t, Bijection):
135
- t = t.forward
136
- self.graph.add_edge(tgt, src, transform=t)
137
- out.append((tgt, src))
138
- else:
139
- logger.debug(
140
- "Reverse edge not added to graph for non-invertible %s->%s transform",
141
- src,
142
- tgt,
143
- )
124
+ d = {TRANSFORM_KEY: transform, **edge_data}
125
+ self.graph.add_edge(src, tgt, **d)
126
+ out.append((src, tgt))
144
127
  return out
145
128
 
146
129
  def add_transform(
@@ -148,20 +131,21 @@ class TransformGraph[ArrayT]:
148
131
  transform: Transform[ArrayT],
149
132
  source: SpaceRef | None = None,
150
133
  target: SpaceRef | None = None,
151
- and_inverse: bool = True,
134
+ *,
135
+ edge_data: dict[str, Any] | None = None,
152
136
  ) -> list[tuple[SpaceRef, SpaceRef]]:
153
- """Add a transform to the graph, optionally with its inverse.
154
-
155
- If the given transform is a `TransformSequence`,
156
- it will be split down into subsequences where intermediate spaces are known.
137
+ """Add a transform to the graph.
157
138
 
158
139
  If the given transform is a `Bijection`,
159
- its forward component will be added as an independent edges;
160
- if `and_inverse=True`, the same will be done with the inverse component.
140
+ only the forward component will be added as an independent edges.
141
+
142
+ This method will NOT overwrite intermediate edges.
161
143
 
162
- This method will overwrite existing edges.
163
- Implicit inverses calculated from the given transform will not overwrite existing explicit edges,
164
- except in the case of the `Bijection`.
144
+ N.B. Previously this method implicitly added inverse edges where possible.
145
+ Now these edges must be added explicitly by calling `add_transform(~transform)`.
146
+ Additionally, previously `TransformSequence`s would be split out into multiple edges
147
+ if any intermediate spaces were defined;
148
+ now these edges must be added explicitly with the `TransformSequence.split` method.
165
149
 
166
150
  Parameters
167
151
  ----------
@@ -171,8 +155,10 @@ class TransformGraph[ArrayT]:
171
155
  May be omitted if `transform` has its source space defined.
172
156
  target
173
157
  May be omitted if `transform` has its target space defined.
174
- and_inverse
175
- Try to add the reverse edge by inverting the transform if possible; default True
158
+ edge_data
159
+ Dict of string keys to arbitrary values to associate with an edge.
160
+ Used during path-finding.
161
+ Must not have the `"_transform"` key.
176
162
 
177
163
  Returns
178
164
  -------
@@ -180,111 +166,32 @@ class TransformGraph[ArrayT]:
180
166
  List of `(src, tgt)` edges added to the graph.
181
167
  """
182
168
  out: list[tuple[SpaceRef, SpaceRef]] = []
183
- if isinstance(transform, TransformSequence):
184
- # TODO: weighting of split-out sequences could be problematic
185
- ts = split_sequence(transform)
186
- out.extend(
187
- chain.from_iterable(
188
- self.add_transform(t, None, None, and_inverse) for t in ts
189
- )
190
- )
191
-
192
- elif isinstance(transform, Bijection):
169
+ if isinstance(transform, Bijection):
193
170
  out.extend(
194
171
  self.add_transform(
195
172
  transform.forward,
196
173
  source,
197
174
  target,
198
- False,
175
+ edge_data=edge_data,
199
176
  )
200
177
  )
201
- if and_inverse:
202
- out.extend(
203
- self.add_transform(
204
- transform.inverse,
205
- target,
206
- source,
207
- False,
208
- )
209
- )
210
178
 
211
179
  else:
212
- out.extend(self._add_transform(transform, source, target, and_inverse))
180
+ out.extend(self._add_transform(transform, source, target, edge_data))
213
181
 
214
182
  if out:
215
183
  self.get_sequence.cache_clear()
216
184
 
217
185
  return out
218
186
 
219
- def add_transforms(
220
- self,
221
- transforms: Iterable[Transform[ArrayT]],
222
- and_inverse: bool = True,
223
- ) -> list[tuple[SpaceRef, SpaceRef]]:
224
- """Bulk-add transformations to the graph.
225
-
226
- Every given transform must have a source and target space defined;
227
- these spaces are the nodes of the graph.
228
-
229
- This method is preferred over `TransformGraph.add_transform`
230
- when some reverse edges are explicitly defined
231
- and you don't want them to be overridden by implicit reverse edges
232
- when `and_inverse=True`.
233
-
234
- `Bijection` s and `TransformSequence` s will be split out as documented in
235
- `TransformGraph.add_transform`.
236
-
237
- Note that a single `TransformSequence` is itself an `Iterable[Transform]`
238
- and so could be used as the `transforms` argument.
239
- However, a `TransformSequence` does not require that all of its members
240
- have explicit source and target spaces,
241
- where the `transforms` argument here does,
242
- so not all `TransformSequence` s can be used directly as the argument
243
- (wrap them in a list instead or use `TransformGraph.add_transform`).
244
-
245
- Parameters
246
- ----------
247
- transforms
248
- Transforms which must have a source and target space defined.
249
- and_inverse
250
- Invert the transformations and add them too.
251
-
252
- Returns
253
- -------
254
- list[tuple[SpaceRef, SpaceRef]]
255
- List of `(src, tgt)` edges added to the graph.
256
- """
257
- if isinstance(transforms, TransformSequence):
258
- logger.warning(
259
- "add_transforms() argument is a TransformSequence, "
260
- "which allows undefined intermediate spaces, "
261
- "in which case this method will fail. "
262
- "Prefer the add_transform() argument for single logical transforms, "
263
- "or wrap the given argument in a collection (e.g. a list)."
264
- )
265
-
266
- forwards = []
267
- for t in transforms:
268
- forwards.extend(self.add_transform(t, and_inverse=False))
269
-
270
- if not and_inverse:
271
- return forwards
272
-
273
- out = list(forwards)
274
-
275
- # add inverses in second stage to prevent implicit reverse transforms blocking explicit
276
- for src, tgt in forwards:
277
- t = self.graph.edges[src, tgt]["transform"]
278
- out.extend(self._add_inverse(t, src, tgt))
279
-
280
- return out
281
-
282
187
  @lru_cache()
283
188
  def get_sequence(
284
189
  self,
285
190
  source_space: SpaceRef,
286
191
  target_space: SpaceRef,
287
192
  full: bool = False,
193
+ *,
194
+ weight: None | str | WeightFn = None,
288
195
  ) -> TransformSequence[ArrayT]:
289
196
  """Get the shortest TransformSequence for transforming between two spaces.
290
197
 
@@ -297,25 +204,32 @@ class TransformGraph[ArrayT]:
297
204
  full
298
205
  By default, simplifies consecutive affines and drops bijections' inverse form.
299
206
  If `full` is True, keeps each transformation as-is.
207
+ weight
208
+ str key in the `edge_data` dict given when an edge was added,
209
+ or a function to determine a weight from the args `src_space, tgt_space, edge_data`,
210
+ or None (all weights are 1).
300
211
 
301
212
  Returns
302
213
  -------
303
214
  TransformSequence[ArrayT]
304
215
  The shortest transform sequence between the spaces.
305
216
  """
306
- path = nx.shortest_path(self.graph, source_space, target_space)
307
- if len(path) <= 1:
308
- transforms = []
309
- else:
310
- transforms = [
311
- self.graph.edges[src, tgt]["transform"] for src, tgt in pairwise(path)
312
- ]
217
+ path = nx.shortest_path(self.graph, source_space, target_space, weight) # type:ignore
218
+ transforms = []
219
+ wfn = normalise_edge_weight_fn(weight)
220
+
221
+ for src, tgt in pairwise(path):
222
+ edges = self.graph[src][tgt]
223
+ transforms.append(
224
+ min(edges.values(), key=lambda d: wfn(src, tgt, d))[TRANSFORM_KEY]
225
+ )
226
+
313
227
  seq = TransformSequence(
314
228
  transforms,
315
229
  spaces=Spaces(source_space, target_space),
316
230
  )
317
231
  if not full:
318
- seq = seq.simplify(drop_inverse=False)
232
+ seq = seq.simplify(drop_inverse=True)
319
233
  return seq
320
234
 
321
235
  def transform(
@@ -323,6 +237,8 @@ class TransformGraph[ArrayT]:
323
237
  source_space: SpaceRef,
324
238
  target_space: SpaceRef,
325
239
  coords: ArrayT,
240
+ *,
241
+ weight: None | str | WeightFn = None,
326
242
  ) -> ArrayT:
327
243
  """Transform coordinates from one space to another,
328
244
  possibly via intermediates.
@@ -335,13 +251,18 @@ class TransformGraph[ArrayT]:
335
251
  The target coordinate space.
336
252
  coords
337
253
  The coordinates to transform.
254
+ weight
255
+ str key in the `edge_data` dict given when an edge was added,
256
+ or a function to determine a weight from the args `src_space, tgt_space, edge_data`,
257
+ or None (all weights are 1).
258
+
338
259
 
339
260
  Returns
340
261
  -------
341
262
  ArrayT
342
263
  The transformed coordinates.
343
264
  """
344
- t = self.get_sequence(source_space, target_space)
265
+ t = self.get_sequence(source_space, target_space, weight=weight)
345
266
  return t.apply(coords)
346
267
 
347
268
  def __iter__(self) -> Iterator[Transform[ArrayT]]:
@@ -364,13 +285,13 @@ class TransformGraph[ArrayT]:
364
285
  >>> new_tgraph = TransformGraph([extra_transform, *old_tgraph])
365
286
 
366
287
  """
367
- for _, _, t in self.graph.edges.data("transform"):
288
+ for _, _, t in self.graph.edges.data(TRANSFORM_KEY):
368
289
  yield t
369
290
 
370
291
  def to_device(
371
292
  self, xp: ModuleType, device: str | None = None
372
293
  ) -> TransformGraph[ArrayT]:
373
294
  result: TransformGraph[ArrayT] = TransformGraph()
374
- for src, tgt, t in self.graph.edges.data("transform"):
295
+ for src, tgt, t in self.graph.edges.data(TRANSFORM_KEY):
375
296
  result.graph.add_edge(src, tgt, transform=t.to_device(xp, device))
376
297
  return result
File without changes