impy-array 2.4.6__tar.gz → 2.4.8__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 (71) hide show
  1. {impy_array-2.4.6 → impy_array-2.4.8}/PKG-INFO +3 -2
  2. {impy_array-2.4.6 → impy_array-2.4.8}/README.md +1 -0
  3. {impy_array-2.4.6 → impy_array-2.4.8}/impy/__init__.py +1 -1
  4. {impy_array-2.4.6 → impy_array-2.4.8}/impy/_const.py +10 -10
  5. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_corr.py +3 -1
  6. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_skimage.py +14 -3
  7. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/imgarray.py +26 -15
  8. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/labeledarray.py +72 -15
  9. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/lazy.py +256 -46
  10. {impy_array-2.4.6 → impy_array-2.4.8}/impy/axes/_axis.py +40 -40
  11. {impy_array-2.4.6 → impy_array-2.4.8}/impy/core.py +13 -21
  12. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_mrc.py +16 -9
  13. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_tiff.py +15 -7
  14. {impy_array-2.4.6 → impy_array-2.4.8}/impy/lazy/core.py +3 -10
  15. {impy_array-2.4.6 → impy_array-2.4.8}/impy/random.py +4 -4
  16. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/gauss.py +4 -1
  17. {impy_array-2.4.6 → impy_array-2.4.8}/pyproject.toml +1 -1
  18. {impy_array-2.4.6 → impy_array-2.4.8}/.gitignore +0 -0
  19. {impy_array-2.4.6 → impy_array-2.4.8}/LICENSE +0 -0
  20. {impy_array-2.4.6 → impy_array-2.4.8}/impy/__main__.py +0 -0
  21. {impy_array-2.4.6 → impy_array-2.4.8}/impy/_types.py +0 -0
  22. {impy_array-2.4.6 → impy_array-2.4.8}/impy/array_api.py +0 -0
  23. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/__init__.py +0 -0
  24. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_imshow.py +0 -0
  25. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/__init__.py +0 -0
  26. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_deconv.py +0 -0
  27. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_docs.py +0 -0
  28. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_filters.py +0 -0
  29. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_glcm.py +0 -0
  30. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_linalg.py +0 -0
  31. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_misc.py +0 -0
  32. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_plot.py +0 -0
  33. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_process_numba.py +0 -0
  34. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_structures.py +0 -0
  35. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/_utils/_transform.py +0 -0
  36. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/axesmixin.py +0 -0
  37. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/bases/__init__.py +0 -0
  38. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/bases/metaarray.py +0 -0
  39. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/bases/overload.py +0 -0
  40. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/big.py +0 -0
  41. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/label.py +0 -0
  42. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/phasearray.py +0 -0
  43. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/specials.py +0 -0
  44. {impy_array-2.4.6 → impy_array-2.4.8}/impy/arrays/tiled.py +0 -0
  45. {impy_array-2.4.6 → impy_array-2.4.8}/impy/axes/__init__.py +0 -0
  46. {impy_array-2.4.6 → impy_array-2.4.8}/impy/axes/_axes.py +0 -0
  47. {impy_array-2.4.6 → impy_array-2.4.8}/impy/axes/_axes_tuple.py +0 -0
  48. {impy_array-2.4.6 → impy_array-2.4.8}/impy/axes/_slicer.py +0 -0
  49. {impy_array-2.4.6 → impy_array-2.4.8}/impy/binder.py +0 -0
  50. {impy_array-2.4.6 → impy_array-2.4.8}/impy/collections.py +0 -0
  51. {impy_array-2.4.6 → impy_array-2.4.8}/impy/correlation.py +0 -0
  52. {impy_array-2.4.6 → impy_array-2.4.8}/impy/frame/__init__.py +0 -0
  53. {impy_array-2.4.6 → impy_array-2.4.8}/impy/frame/frames.py +0 -0
  54. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/__init__.py +0 -0
  55. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_nd2.py +0 -0
  56. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_npy.py +0 -0
  57. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_registry.py +0 -0
  58. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_utils.py +0 -0
  59. {impy_array-2.4.6 → impy_array-2.4.8}/impy/io/_zarr.py +0 -0
  60. {impy_array-2.4.6 → impy_array-2.4.8}/impy/lazy/__init__.py +0 -0
  61. {impy_array-2.4.6 → impy_array-2.4.8}/impy/lazy/random.py +0 -0
  62. {impy_array-2.4.6 → impy_array-2.4.8}/impy/roi.py +0 -0
  63. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/__init__.py +0 -0
  64. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/axesop.py +0 -0
  65. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/deco.py +0 -0
  66. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/misc.py +0 -0
  67. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/slicer.py +0 -0
  68. {impy_array-2.4.6 → impy_array-2.4.8}/impy/utils/utilcls.py +0 -0
  69. {impy_array-2.4.6 → impy_array-2.4.8}/impy/viewer/__init__.py +0 -0
  70. {impy_array-2.4.6 → impy_array-2.4.8}/impy/viewer/utils.py +0 -0
  71. {impy_array-2.4.6 → impy_array-2.4.8}/impy/viewer/viewer.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: impy-array
3
- Version: 2.4.6
3
+ Version: 2.4.8
4
4
  Summary: Speed up coding/extending image analysis in Python.
5
5
  Project-URL: Download, https://github.com/hanjinliu/impy
6
6
  Author-email: Hanjin Liu <liuhanjin-sc@g.ecc.u-tokyo.ac.jp>
@@ -131,6 +131,7 @@ img_prj.imsave("image_max.tif") # Save in the same place. Don't spend tim
131
131
  `impy` automatically chooses the proper reader/writer according to the extension.
132
132
 
133
133
  - Tiff file (".tif", ".tiff")
134
+ - LSM file (".lsm")
134
135
  - MRC file (".mrc", ".rec", ".st", ".map", ".map.gz")
135
136
  - Zarr file (".zarr")
136
137
  - ND2 file (".nd2")
@@ -58,6 +58,7 @@ img_prj.imsave("image_max.tif") # Save in the same place. Don't spend tim
58
58
  `impy` automatically chooses the proper reader/writer according to the extension.
59
59
 
60
60
  - Tiff file (".tif", ".tiff")
61
+ - LSM file (".lsm")
61
62
  - MRC file (".mrc", ".rec", ".st", ".map", ".map.gz")
62
63
  - Zarr file (".zarr")
63
64
  - ND2 file (".nd2")
@@ -1,4 +1,4 @@
1
- __version__ = "2.4.6"
1
+ __version__ = "2.4.8"
2
2
  __author__ = "Hanjin Liu"
3
3
  __email__ = "liuhanjin-sc@g.ecc.u-tokyo.ac.jp"
4
4
 
@@ -8,13 +8,13 @@ MAX_GB_LIMIT = memory.total / 2 * 1e-9
8
8
 
9
9
  class GlobalConstant(MutableMapping[str, Any]):
10
10
  _const: dict[str, Any]
11
-
11
+
12
12
  def __init__(self, **kwargs):
13
13
  object.__setattr__(self, "_const", dict(**kwargs))
14
-
14
+
15
15
  def __len__(self) -> int:
16
16
  return len(self._const)
17
-
17
+
18
18
  def __iter__(self):
19
19
  raise StopIteration
20
20
 
@@ -47,15 +47,15 @@ class GlobalConstant(MutableMapping[str, Any]):
47
47
  dask.config.set(scheduler=v)
48
48
  else:
49
49
  raise RuntimeError("Cannot set new keys.")
50
-
50
+
51
51
  self._const[k] = v
52
-
52
+
53
53
  __getattr__ = __getitem__
54
54
  __setattr__ = __setitem__
55
55
 
56
56
  def __delitem__(self, v):
57
57
  raise RuntimeError("Cannot delete any items.")
58
-
58
+
59
59
  def __repr__(self):
60
60
  return (
61
61
  f"""
@@ -65,12 +65,12 @@ class GlobalConstant(MutableMapping[str, Any]):
65
65
  SCHEDULER : {self['SCHEDULER']}
66
66
  """
67
67
  )
68
-
68
+
69
69
  def asdict(self) -> dict[str, Any]:
70
70
  return self._const.copy()
71
71
 
72
72
  Const = GlobalConstant(
73
- MAX_GB = MAX_GB_LIMIT/2,
73
+ MAX_GB = MAX_GB_LIMIT * 0.8,
74
74
  ID_AXIS = "N",
75
75
  RESOURCE = "numpy",
76
76
  SCHEDULER = "threads",
@@ -80,12 +80,12 @@ class SetConst:
80
80
  n_ongoing = 0
81
81
  _locked_keys: set[str] = set()
82
82
  _old_dict: dict[str, Any] = dict()
83
-
83
+
84
84
  def __init__(self, dict_: dict[str, Any] | None =None, **kwargs):
85
85
  dict_ = dict_ or {}
86
86
  dict_.update(kwargs)
87
87
  self._kwargs = dict_
88
-
88
+
89
89
  def __enter__(self):
90
90
  self.__class__.n_ongoing += 1
91
91
  if self.__class__.n_ongoing == 1:
@@ -34,7 +34,7 @@ def subpixel_pcc(
34
34
  f0: np.ndarray,
35
35
  f1: np.ndarray,
36
36
  upsample_factor: int,
37
- max_shifts: tuple[float, ...] | None = None,
37
+ max_shifts: float | tuple[float, ...] | None = None,
38
38
  ):
39
39
  power, product = _pcc_and_power_spec(f0, f1, max_shifts)
40
40
  maxima = xp.unravel_index(xp.argmax(power), power.shape)
@@ -60,6 +60,8 @@ def subpixel_pcc(
60
60
  power = abs2(cross_correlation)
61
61
 
62
62
  if max_shifts is not None:
63
+ if np.isscalar(max_shifts):
64
+ max_shifts = (max_shifts,) * f0.ndim
63
65
  max_shifts = xp.asarray(max_shifts)
64
66
  _upsampled_left_shifts = (shifts + max_shifts) * upsample_factor
65
67
  _upsampled_right_shifts = (max_shifts - shifts) * upsample_factor
@@ -6,8 +6,19 @@ from functools import reduce, lru_cache
6
6
 
7
7
  # same as the function in skimage.filters._fft_based (available in scikit-image >= 0.19)
8
8
  @lru_cache(maxsize=4)
9
- def _get_ND_butterworth_filter(shape: tuple[int, ...], cutoff: float, order: int,
10
- high_pass: bool, real: bool):
9
+ def _get_ND_butterworth_filter(
10
+ shape: tuple[int, ...],
11
+ cutoff: float,
12
+ order: int,
13
+ high_pass: bool,
14
+ real: bool,
15
+ ):
16
+ if cutoff == 0:
17
+ if high_pass:
18
+ wfilt = np.ones(shape, dtype=np.float32)
19
+ else:
20
+ wfilt = np.zeros(shape, dtype=np.float32)
21
+ return wfilt
11
22
  ranges = []
12
23
  for d, fc in zip(shape, cutoff):
13
24
  axis = np.arange(-(d - 1) // 2, (d - 1) // 2 + 1, dtype=np.float32) / (d*fc)
@@ -19,4 +30,4 @@ def _get_ND_butterworth_filter(shape: tuple[int, ...], cutoff: float, order: int
19
30
  wfilt = 1 / (1 + q2**order)
20
31
  if high_pass:
21
32
  wfilt = 1 - wfilt
22
- return wfilt
33
+ return wfilt
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
  import warnings
3
+ from functools import partial
4
+ from itertools import product
5
+ from typing import TYPE_CHECKING, Literal, Sequence, Iterable, Callable, overload, Any
6
+
3
7
  import numpy as np
4
8
  from numpy.typing import DTypeLike
5
- from functools import partial
6
9
  from scipy import ndimage as ndi
7
- from typing import TYPE_CHECKING, Literal, Sequence, Iterable, Callable, overload, Any
8
10
 
9
11
  from impy.arrays.labeledarray import LabeledArray
10
12
  from impy.arrays.label import Label
@@ -247,12 +249,11 @@ class ImgArray(LabeledArray):
247
249
  cval = cval(self.value)
248
250
 
249
251
  prefilter = prefilter or order > 1
250
- mtx = _transform.compose_affine_matrix(translation=translation)
251
252
 
252
253
  return self._apply_dask(
253
- _transform.warp,
254
+ _transform.shift,
254
255
  c_axes=complement_axes(dims, self.axes),
255
- kwargs=dict(matrix=mtx, order=order, mode=mode, cval=cval,
256
+ kwargs=dict(shift=translation, order=order, mode=mode, cval=cval,
256
257
  prefilter=prefilter)
257
258
  )
258
259
 
@@ -3986,9 +3987,9 @@ class ImgArray(LabeledArray):
3986
3987
  along: AxisLike | None = None,
3987
3988
  show_drift: bool = False,
3988
3989
  upsample_factor: int = 10,
3990
+ max_shift: nDFloat | None = None,
3989
3991
  ) -> MarkerFrame:
3990
- """
3991
- Calculate yx-directional drift using the method equivalent to
3992
+ """Calculate yx-directional drift using the method equivalent to
3992
3993
  ``skimage.registration.phase_cross_correlation``.
3993
3994
 
3994
3995
  Parameters
@@ -3999,6 +4000,8 @@ class ImgArray(LabeledArray):
3999
4000
  If True, plot the result.
4000
4001
  upsample_factor : int, default is 10
4001
4002
  Up-sampling factor when calculating phase cross correlation.
4003
+ max_shift : tuple of float, optional
4004
+ Maximum shift in spatial directions.
4002
4005
 
4003
4006
  Returns
4004
4007
  -------
@@ -4013,7 +4016,9 @@ class ImgArray(LabeledArray):
4013
4016
  raise ValueError("`along` must be single character.")
4014
4017
  if not isinstance(upsample_factor, int):
4015
4018
  raise TypeError(f"upsample-factor must be integer but got {type(upsample_factor)}")
4016
-
4019
+ if max_shift is not None:
4020
+ if np.isscalar(max_shift):
4021
+ max_shift = np.full(self.ndim - 1, max_shift)
4017
4022
  result = np.zeros((self.sizeof(along), self.ndim-1), dtype=np.float32)
4018
4023
  c_axes = complement_axes(along, self.axes)
4019
4024
  last_img = None
@@ -4022,7 +4027,12 @@ class ImgArray(LabeledArray):
4022
4027
  img = xp.asarray(img)
4023
4028
  if last_img is not None:
4024
4029
  result[i] = xp.asnumpy(
4025
- _corr.subpixel_pcc(last_img, img, upsample_factor=upsample_factor)[0]
4030
+ _corr.subpixel_pcc(
4031
+ last_img,
4032
+ img,
4033
+ max_shifts=max_shift,
4034
+ upsample_factor=upsample_factor,
4035
+ )[0]
4026
4036
  )
4027
4037
  last_img = img
4028
4038
  else:
@@ -4047,15 +4057,17 @@ class ImgArray(LabeledArray):
4047
4057
  *,
4048
4058
  zero_ave: bool = True,
4049
4059
  along: AxisLike | None = None,
4060
+ max_shift: nDFloat | None = None,
4050
4061
  order: int = 1,
4051
4062
  mode: str = "constant",
4052
4063
  cval: float = 0,
4053
4064
  dims: Dims = 2,
4054
4065
  update: bool = False,
4055
4066
  ) -> ImgArray:
4056
- """
4057
- Drift correction using iterative Affine translation. If translation vectors ``shift``
4058
- is not given, then it will be determined using ``track_drift`` method of ImgArray.
4067
+ """Drift correction using iterative Affine translation.
4068
+
4069
+ If translation vectors ``shift`` is not given, then it will be determined using
4070
+ ``track_drift`` method of ImgArray.
4059
4071
 
4060
4072
  Parameters
4061
4073
  ----------
@@ -4082,7 +4094,7 @@ class ImgArray(LabeledArray):
4082
4094
  Examples
4083
4095
  --------
4084
4096
  Drift correction of multichannel image using the first channel as the reference.
4085
- >>> img.drift_correction(ref=img["c=0"])
4097
+ >>> img.drift_correction(ref=img["c=0"])
4086
4098
  """
4087
4099
 
4088
4100
  if along is None:
@@ -4097,7 +4109,6 @@ class ImgArray(LabeledArray):
4097
4109
  elif not isinstance(ref, ImgArray):
4098
4110
  ref = self[ref]
4099
4111
  if ref.axes != [along] + dims:
4100
- from itertools import product
4101
4112
  _c_axes = complement_axes([along] + dims, str(ref.axes))
4102
4113
  fmt = slicer.get_formatter(_c_axes)
4103
4114
  out = np.empty_like(self)
@@ -4109,7 +4120,7 @@ class ImgArray(LabeledArray):
4109
4120
  )
4110
4121
  return out
4111
4122
 
4112
- shift = ref.track_drift(along=along).values
4123
+ shift = ref.track_drift(along=along, max_shift=max_shift).values
4113
4124
 
4114
4125
  else:
4115
4126
  shift = np.asarray(shift, dtype=np.float32)
@@ -3,7 +3,7 @@ import numpy as np
3
3
  from pathlib import Path
4
4
  import itertools
5
5
  from functools import partial
6
- from typing import TYPE_CHECKING, Any, Sequence, MutableMapping, Protocol
6
+ from typing import TYPE_CHECKING, Any, Literal, Sequence, MutableMapping, Protocol
7
7
  from warnings import warn
8
8
  from scipy import ndimage as ndi
9
9
 
@@ -23,7 +23,7 @@ from impy.array_api import xp
23
23
 
24
24
  if TYPE_CHECKING:
25
25
  from typing_extensions import Self
26
- from numpy.typing import ArrayLike, DTypeLike
26
+ from numpy.typing import ArrayLike, DTypeLike, NDArray
27
27
  from impy.frame import PathFrame
28
28
  from impy.roi import RoiList
29
29
 
@@ -401,10 +401,18 @@ class LabeledArray(MetaArray):
401
401
  out._set_info(self)
402
402
  return out
403
403
 
404
- def as_float(self) -> Self:
405
- if self.dtype == np.float32:
404
+ def as_float(self, *, depth: int = 32) -> Self:
405
+ if depth == 16:
406
+ dtype = np.float16
407
+ elif depth == 32:
408
+ dtype = np.float32
409
+ elif depth == 64:
410
+ dtype = np.float64
411
+ else:
412
+ raise ValueError(f"depth must be 16, 32, or 64, but got {depth}")
413
+ if self.dtype == dtype:
406
414
  return self
407
- out = self.value.astype(np.float32).view(self.__class__)
415
+ out = self.value.astype(dtype).view(self.__class__)
408
416
  out._set_info(self)
409
417
  return out
410
418
 
@@ -417,8 +425,12 @@ class LabeledArray(MetaArray):
417
425
  return self.as_uint16()
418
426
  elif dtype == "uint8":
419
427
  return self.as_uint8()
428
+ elif dtype == "float16":
429
+ return self.as_float(depth=16)
420
430
  elif dtype == "float32":
421
- return self.as_float()
431
+ return self.as_float(depth=32)
432
+ elif dtype == "float64":
433
+ return self.as_float(depth=64)
422
434
  elif dtype == "bool":
423
435
  return self.astype("bool")
424
436
  elif dtype == "float64":
@@ -576,6 +588,7 @@ class LabeledArray(MetaArray):
576
588
  *,
577
589
  order: int = 3,
578
590
  prefilter: bool | None = None,
591
+ how: Literal["pack", "equal"] = "pack",
579
592
  ) -> PropArray:
580
593
  """
581
594
  Measure line profile (kymograph) iteratively for every slice of image. This
@@ -590,7 +603,12 @@ class LabeledArray(MetaArray):
590
603
  `a = [[y0, x0], [y1, x1], ..., [yn, xn]]`
591
604
  b : array-like, optional
592
605
  Destination coordinate. If specified, `a` must be the source coordinate.
593
- {order}
606
+ {order}{prefilter}
607
+ how : "pack" or "equal", default "pack"
608
+ How to sample points on the line. "pack" means that the points are sampled
609
+ at 1-px interval from the start point. "equal" means that the points are
610
+ sampled equally at the interval larger but neareset to 1-px so that the
611
+ points covers the entire line.
594
612
 
595
613
  Returns
596
614
  -------
@@ -616,7 +634,12 @@ class LabeledArray(MetaArray):
616
634
  a = np.asarray(a, dtype=np.float32)
617
635
  _, ndim = a.shape
618
636
  seg = SegmentedLine(a)
619
- coords = seg.sample_points().T
637
+ if how == "pack":
638
+ coords = np.stack(seg.arange(), axis=0)
639
+ elif how == "equal":
640
+ coords = np.stack(seg.linspace(int(seg.length())), axis=0)
641
+ else:
642
+ raise ValueError(f"No option matches how={how}")
620
643
 
621
644
  if ndim == self.ndim:
622
645
  dims = self.axes
@@ -632,7 +655,11 @@ class LabeledArray(MetaArray):
632
655
  axes=c_axes+[new_axis], propname="reslice")
633
656
 
634
657
  out.set_scale(self)
635
- out.set_scale({new_axis: self.scale[dims[-1]] * seg.interv})
658
+ axis = self.axes[dims[-1]]
659
+ if how == "equal":
660
+ out.set_scale({new_axis: axis.scale * seg.interv}, unit=axis.unit)
661
+ else:
662
+ out.set_scale({new_axis: axis.scale}, unit=axis.unit)
636
663
  return out
637
664
 
638
665
  @_docs.write_docs
@@ -1576,31 +1603,61 @@ class SegmentedLine:
1576
1603
  dist = np.sqrt(np.sum(vec**2, axis=1))
1577
1604
  dist_sum = np.sum(dist)
1578
1605
  npoints = int(dist_sum)
1579
- interv = dist_sum / npoints
1606
+ self.interv = dist_sum / npoints
1580
1607
 
1581
- self.length = dist_sum
1582
1608
  self.vec = vec
1583
1609
  self.dist = dist
1584
1610
  self.nodes = nodes
1585
- self.interv = interv
1586
1611
 
1587
1612
  def sample_points(self) -> np.ndarray:
1588
1613
  res = 0
1589
1614
  out = [self.nodes[0:1]]
1590
1615
  npoints = 1
1616
+ interv = self.length() / int(self.length())
1591
1617
  for v, d, p in zip(self.vec, self.dist, self.nodes[:-1]):
1592
1618
  res0 = res
1593
- d_int, res = divmod(d + res, self.interv)
1594
- idx = self.interv*(np.arange(d_int) + 1) - res0
1619
+ d_int, res = divmod(d + res, interv)
1620
+ idx = interv * (np.arange(d_int) + 1) - res0
1595
1621
  xs = idx[:, np.newaxis] * v[np.newaxis]/d + p
1596
1622
  out.append(xs)
1597
1623
  npoints += xs.shape[0]
1598
1624
 
1599
- if npoints <= int(self.length):
1625
+ if npoints <= int(self.length()):
1600
1626
  out.append(self.nodes[-1:])
1601
1627
 
1602
1628
  return np.concatenate(out, axis=0)
1603
1629
 
1630
+ def length(self) -> np.float64:
1631
+ return np.sum(self.lengths())
1632
+
1633
+ def lengths(self) -> NDArray[np.float64]:
1634
+ return np.sqrt(np.sum(np.diff(self.nodes, axis=0) ** 2, axis=1))
1635
+
1636
+ def linspace(self, num: int) -> tuple[NDArray[np.float64], ...]:
1637
+ """Return a tuple of x and y coordinates of np.linspace along the line."""
1638
+ tnots = np.cumsum(
1639
+ np.concatenate([[0], self.lengths()], dtype=np.float64)
1640
+ )
1641
+ teval = np.linspace(0, tnots[-1], num)
1642
+ return tuple(
1643
+ np.interp(teval, tnots, self.nodes[:, i])
1644
+ for i in range(self.nodes.shape[1])
1645
+ )
1646
+
1647
+ def arange(
1648
+ self, step: float = 1.0
1649
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
1650
+ tnots = np.cumsum(
1651
+ np.concatenate([[0], self.lengths()], dtype=np.float64)
1652
+ )
1653
+ length = tnots[-1]
1654
+ num, rem = divmod(length, step)
1655
+ teval = np.linspace(0, length - rem, int(num + 1))
1656
+ return tuple(
1657
+ np.interp(teval, tnots, self.nodes[:, i])
1658
+ for i in range(self.nodes.shape[1])
1659
+ )
1660
+
1604
1661
  def _count_list_depth(x) -> int:
1605
1662
  n = 0
1606
1663
  out = x