pysnapping 0.2.1__tar.gz → 0.2.3__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 (33) hide show
  1. {pysnapping-0.2.1 → pysnapping-0.2.3}/.gitignore +1 -0
  2. {pysnapping-0.2.1 → pysnapping-0.2.3}/.gitlab-ci.yml +1 -1
  3. {pysnapping-0.2.1 → pysnapping-0.2.3}/.pre-commit-config.yaml +11 -14
  4. {pysnapping-0.2.1 → pysnapping-0.2.3}/PKG-INFO +10 -4
  5. {pysnapping-0.2.1 → pysnapping-0.2.3}/README.md +6 -1
  6. {pysnapping-0.2.1 → pysnapping-0.2.3}/dev-requirements.txt +11 -15
  7. {pysnapping-0.2.1 → pysnapping-0.2.3}/pyproject.toml +3 -5
  8. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/linear_referencing.py +12 -15
  9. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/snap.py +20 -8
  10. pysnapping-0.2.3/pysnapping/stats.py +132 -0
  11. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_snap.py +39 -19
  12. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/PKG-INFO +10 -4
  13. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/SOURCES.txt +3 -0
  14. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/requires.txt +1 -1
  15. pysnapping-0.2.3/pysnapping.egg-info/scm_file_list.json +27 -0
  16. pysnapping-0.2.3/pysnapping.egg-info/scm_version.json +8 -0
  17. pysnapping-0.2.3/requirements.txt +12 -0
  18. pysnapping-0.2.1/requirements.txt +0 -16
  19. {pysnapping-0.2.1 → pysnapping-0.2.3}/.editorconfig +0 -0
  20. {pysnapping-0.2.1 → pysnapping-0.2.3}/LICENSE +0 -0
  21. {pysnapping-0.2.1 → pysnapping-0.2.3}/MANIFEST.in +0 -0
  22. {pysnapping-0.2.1 → pysnapping-0.2.3}/dev-requirements.in +0 -0
  23. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/__init__.py +0 -0
  24. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/py.typed +0 -0
  25. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/__init__.py +0 -0
  26. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/complex_trip.geojson +0 -0
  27. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/conftest.py +0 -0
  28. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_linear_referencing.py +0 -0
  29. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_util.py +0 -0
  30. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/util.py +0 -0
  31. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/dependency_links.txt +0 -0
  32. {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/top_level.txt +0 -0
  33. {pysnapping-0.2.1 → pysnapping-0.2.3}/setup.cfg +0 -0
@@ -1,5 +1,6 @@
1
1
  # Configuration
2
2
  /*.ini
3
+ uv.lock
3
4
 
4
5
  # Byte-compiled / optimized / DLL files
5
6
  __pycache__/
@@ -1,4 +1,4 @@
1
- image: registry.geops.de/build/bullseye-python:latest
1
+ image: registry.geops.com/geops/gitlab-ci-dockerfiles/bullseye-python:latest
2
2
 
3
3
  variables:
4
4
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
@@ -1,7 +1,7 @@
1
1
  fail_fast: false
2
2
  repos:
3
3
  - repo: https://github.com/pre-commit/pre-commit-hooks
4
- rev: v4.5.0
4
+ rev: v5.0.0
5
5
  hooks:
6
6
  - id: no-commit-to-branch
7
7
  - id: check-merge-conflict
@@ -13,11 +13,20 @@ repos:
13
13
  - id: check-case-conflict
14
14
  - id: check-docstring-first
15
15
  - repo: https://github.com/astral-sh/ruff-pre-commit
16
- rev: v0.1.14
16
+ rev: v0.9.9
17
17
  hooks:
18
18
  - id: ruff
19
19
  args: [ --fix ]
20
20
  - id: ruff-format
21
+ - repo: https://github.com/astral-sh/uv-pre-commit
22
+ rev: 0.6.3
23
+ hooks:
24
+ - id: pip-compile
25
+ args: [pyproject.toml, -o, requirements.txt, -p, "3.9"]
26
+ files: ^(pyproject.toml|requirements.txt)$
27
+ - id: pip-compile
28
+ args: [dev-requirements.in, -o, dev-requirements.txt, -p, "3.9"]
29
+ files: ^(dev\-)?requirements.(txt|in)$
21
30
  - repo: local
22
31
  hooks:
23
32
  - id: mypy
@@ -27,15 +36,3 @@ repos:
27
36
  pass_filenames: false
28
37
  always_run: true
29
38
  args: ["-p", "pysnapping"]
30
- - id: pip-compile
31
- name: pip-compile
32
- entry: pip-compile pyproject.toml
33
- language: python
34
- pass_filenames: false
35
- files: 'pyproject\.toml|requirements\.txt'
36
- - id: pip-compile-dev-requirements
37
- name: pip-compile-dev-requirements
38
- entry: pip-compile dev-requirements.in
39
- language: python
40
- pass_filenames: false
41
- files: '(dev-)?requirements\.(in|txt)'
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pysnapping
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Snap points to a line string keeping a given order or spacing intact
5
5
  Author-email: Alexander Held | geOps <alexander.held@geops.com>
6
6
  Maintainer-email: Alexander Held | geOps <alexander.held@geops.com>
@@ -40,8 +40,9 @@ Requires-Python: >=3.9
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE
42
42
  Requires-Dist: shapely
43
- Requires-Dist: numpy
43
+ Requires-Dist: numpy~=2.0
44
44
  Requires-Dist: pyproj>=3.2.0
45
+ Dynamic: license-file
45
46
 
46
47
  # PySnapping
47
48
 
@@ -85,9 +86,14 @@ The `pysnapping.util` module contains common helper functions used in other part
85
86
 
86
87
  ### Snapping
87
88
 
88
- The `pysanpping.snap` module is the main entry point for users of the `pysnapping` library and
89
+ The `pysnapping.snap` module is the main entry point for users of the `pysnapping` library and
89
90
  provides the classes needed to use the library.
90
91
 
92
+ ### Statistics
93
+
94
+ The `pysnapping.stats` module provides tools to aggregate statistics about snapping
95
+ failures and the methods used in case of success.
96
+
91
97
  ## Usage
92
98
 
93
99
  The typical usage pattern is to create a `pysnapping.snap.DubiousTrajectory` instance which represents
@@ -40,9 +40,14 @@ The `pysnapping.util` module contains common helper functions used in other part
40
40
 
41
41
  ### Snapping
42
42
 
43
- The `pysanpping.snap` module is the main entry point for users of the `pysnapping` library and
43
+ The `pysnapping.snap` module is the main entry point for users of the `pysnapping` library and
44
44
  provides the classes needed to use the library.
45
45
 
46
+ ### Statistics
47
+
48
+ The `pysnapping.stats` module provides tools to aggregate statistics about snapping
49
+ failures and the methods used in case of success.
50
+
46
51
  ## Usage
47
52
 
48
53
  The typical usage pattern is to create a `pysnapping.snap.DubiousTrajectory` instance which represents
@@ -1,19 +1,13 @@
1
- #
2
- # This file is autogenerated by pip-compile with Python 3.9
3
- # by the following command:
4
- #
5
- # pip-compile dev-requirements.in
6
- #
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile dev-requirements.in -o dev-requirements.txt -p 3.9
7
3
  build==1.0.3
8
4
  # via pip-tools
9
5
  cfgv==3.4.0
10
6
  # via pre-commit
11
7
  click==8.1.7
12
8
  # via pip-tools
13
- coverage[toml]==7.4.1
14
- # via
15
- # coverage
16
- # pytest-cov
9
+ coverage==7.4.1
10
+ # via pytest-cov
17
11
  distlib==0.3.8
18
12
  # via virtualenv
19
13
  exceptiongroup==1.2.0
@@ -32,7 +26,7 @@ mypy-extensions==1.0.0
32
26
  # via mypy
33
27
  nodeenv==1.8.0
34
28
  # via pre-commit
35
- numpy==1.26.3
29
+ numpy==2.0.2
36
30
  # via
37
31
  # -c requirements.txt
38
32
  # -r dev-requirements.in
@@ -40,6 +34,8 @@ packaging==23.2
40
34
  # via
41
35
  # build
42
36
  # pytest
37
+ pip==25.0.1
38
+ # via pip-tools
43
39
  pip-tools==7.3.0
44
40
  # via -r dev-requirements.in
45
41
  platformdirs==4.1.0
@@ -58,6 +54,10 @@ pytest-cov==4.1.0
58
54
  # via -r dev-requirements.in
59
55
  pyyaml==6.0.1
60
56
  # via pre-commit
57
+ setuptools==75.8.2
58
+ # via
59
+ # nodeenv
60
+ # pip-tools
61
61
  tomli==2.0.1
62
62
  # via
63
63
  # build
@@ -74,7 +74,3 @@ wheel==0.42.0
74
74
  # via pip-tools
75
75
  zipp==3.17.0
76
76
  # via importlib-metadata
77
-
78
- # The following packages are considered to be unsafe in a requirements file:
79
- # pip
80
- # setuptools
@@ -4,7 +4,7 @@ description = "Snap points to a line string keeping a given order or spacing int
4
4
  readme = "README.md"
5
5
  dependencies = [
6
6
  "shapely",
7
- "numpy",
7
+ "numpy~=2.0", # for vectorized operations
8
8
  "pyproj>=3.2.0", # for defining custom CRS
9
9
  ]
10
10
  requires-python = ">=3.9"
@@ -47,7 +47,7 @@ build-backend = "setuptools.build_meta"
47
47
  [tool.setuptools.packages.find]
48
48
  include = ["pysnapping", "pysnapping.*"]
49
49
 
50
- [tool.ruff]
50
+ [tool.ruff.lint]
51
51
  select = [
52
52
  "F", # pyflakes
53
53
  "E", # pycodestyle
@@ -60,6 +60,7 @@ select = [
60
60
  "PIE", # unnecessary statements
61
61
  "SLOT", # str, tuple, namedtuple subclasses need __slots__
62
62
  "FIX003", # line contains XXX
63
+ "NPY201", # numpy 2.0 deprecations
63
64
  ]
64
65
  ignore = [
65
66
  "E501", # line too long
@@ -71,9 +72,6 @@ max-complexity = 12
71
72
 
72
73
  [tool.mypy]
73
74
  ignore_missing_imports = true
74
- plugins = [
75
- "numpy.typing.mypy_plugin",
76
- ]
77
75
  install_types = true
78
76
  non_interactive = true
79
77
 
@@ -14,9 +14,6 @@ from math import floor
14
14
 
15
15
  import numpy as np
16
16
 
17
- # faster than np.clip
18
- from numpy.core.umath import clip # type: ignore
19
-
20
17
  from . import ExtrapolationError
21
18
 
22
19
 
@@ -389,7 +386,7 @@ class LineFractions:
389
386
  else:
390
387
  high = 1.0
391
388
 
392
- fractions = clip(fractions, low, high)
389
+ fractions = np.clip(fractions, low, high)
393
390
 
394
391
  projected_points = fractions * segment_dirs
395
392
  projected_points += segment_starts
@@ -527,9 +524,9 @@ class LineFractions:
527
524
  i,
528
525
  segment_indices[keep],
529
526
  # clip to segments
530
- clip(min_fractions[keep], 0, 1),
531
- clip(closest_fractions[keep], 0, 1),
532
- clip(max_fractions[keep], 0, 1),
527
+ np.clip(min_fractions[keep], 0, 1),
528
+ np.clip(closest_fractions[keep], 0, 1),
529
+ np.clip(max_fractions[keep], 0, 1),
533
530
  )
534
531
  ball_slices.append(slfs)
535
532
  return ball_slices
@@ -622,15 +619,15 @@ class SlicedLineFractions:
622
619
  right_fractions = np.linspace(
623
620
  self.closest_fractions[i], self.max_fractions[i], n_right_samples[i]
624
621
  )
625
- locations.from_vertices[
626
- index_offset : index_offset + n_samples[i]
627
- ] = self.segment_indices[i]
622
+ locations.from_vertices[index_offset : index_offset + n_samples[i]] = (
623
+ self.segment_indices[i]
624
+ )
628
625
  locations.to_vertices[index_offset : index_offset + n_samples[i]] = (
629
626
  self.segment_indices[i] + 1
630
627
  )
631
- locations.fractions[
632
- index_offset : index_offset + n_left_samples[i]
633
- ] = left_fractions
628
+ locations.fractions[index_offset : index_offset + n_left_samples[i]] = (
629
+ left_fractions
630
+ )
634
631
  # 1 sample overlap thus - 1
635
632
  locations.fractions[
636
633
  index_offset + n_left_samples[i] - 1 : index_offset + n_samples[i]
@@ -726,14 +723,14 @@ def substrings(
726
723
  frac_start_ceil = np.ceil(frac_start)
727
724
  first = frac_start_ceil.astype(int)
728
725
  first[frac_start == frac_start_ceil] += 1
729
- clip(first, starts.from_vertices, starts.to_vertices + 1, out=first)
726
+ np.clip(first, starts.from_vertices, starts.to_vertices + 1, out=first)
730
727
 
731
728
  # the last indices that are not convered by end interpolation/extrapolation
732
729
  frac_end = ends.get_fractional_indices()
733
730
  frac_end_floor = np.floor(frac_end)
734
731
  last = frac_end_floor.astype(int)
735
732
  last[frac_end == frac_end_floor] -= 1
736
- clip(last, ends.from_vertices - 1, ends.to_vertices, out=last)
733
+ np.clip(last, ends.from_vertices - 1, ends.to_vertices, out=last)
737
734
 
738
735
  data_length = len(data)
739
736
  start_data = interpolate(data, starts)
@@ -140,21 +140,26 @@ class SnappingParams(typing.NamedTuple):
140
140
 
141
141
  values_arr = np.asarray(values, dtype=float)
142
142
 
143
- min_spacing = self.min_spacing
143
+ nan_min_spacing = self.min_spacing
144
144
  if consider_sampling_accuracy:
145
- min_spacing += 2.01 * self.sampling_step
145
+ # extra spacing for numerical solution for untrusted points
146
+ nan_min_spacing += 2.01 * self.sampling_step
146
147
 
147
148
  # padding to treat NaNs at the boundary, d_min, d_max and min_spacing in an
148
149
  # elegant way
149
150
  padded_values = np.empty(len(values_arr) + 2)
150
- padded_values[0] = d_min - min_spacing
151
+ padded_values[0] = d_min
151
152
  padded_values[1:-1] = values_arr
152
- padded_values[-1] = d_max + min_spacing
153
+ padded_values[-1] = d_max
153
154
  del values_arr # prevent accidental usage
154
155
 
155
156
  non_nan_indices = np.nonzero(np.logical_not(np.isnan(padded_values)))[0]
156
- min_spacing_arr = np.diff(non_nan_indices).astype(float)
157
- min_spacing_arr *= min_spacing
157
+ index_diffs = np.diff(non_nan_indices)
158
+ index_diffs[0] -= 1
159
+ index_diffs[-1] -= 1
160
+ min_spacing_arr = index_diffs.astype(float) * np.where(
161
+ index_diffs > 1, nan_min_spacing, self.min_spacing
162
+ )
158
163
 
159
164
  # bool to ensure builtin bool (and not numpy bool)
160
165
  return bool(np.all(np.diff(padded_values[non_nan_indices]) >= min_spacing_arr))
@@ -578,10 +583,17 @@ class TrajectoryTrip(XYZDMixin):
578
583
  )
579
584
  ppoints_list[i_global] = ppoints
580
585
  else:
581
- logger.debug("spacing of distances is not OK, untrusting all distances")
586
+ logger.debug(
587
+ "spacing of distances %s is not OK (d_min=%s, d_max=%s, "
588
+ "min_spacing=%s), untrusting all distances",
589
+ self.dists,
590
+ self.trajectory.d_min,
591
+ self.trajectory.d_max,
592
+ params.min_spacing,
593
+ )
582
594
  # if the spacing was not ok, it can still be bad after untrusting all points
583
595
  # since the total length can be too short for the points
584
- untrusted_dists = np.full_like(self.dists, np.NaN)
596
+ untrusted_dists = np.full_like(self.dists, np.nan)
585
597
  if not traj_spacing_ok(untrusted_dists):
586
598
  raise SnappingError(
587
599
  f"trajectory of length {self.trajectory.length:g} is too short "
@@ -0,0 +1,132 @@
1
+ from dataclasses import dataclass, field
2
+ from enum import Enum
3
+ import typing
4
+ from collections import Counter
5
+ import logging
6
+
7
+ from pysnapping import SnappingError, SnappingMethod
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from pysnapping.snap import SnappedTripPoints, TrajectoryTrip
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class AggregatedSnappingMethod(Enum):
17
+ failed = "failed"
18
+ all_trusted = "all_trusted"
19
+ all_routed = "all_routed"
20
+ all_fallback = "all_fallback"
21
+ mixed = "mixed"
22
+ total = "total"
23
+
24
+
25
+ @dataclass
26
+ class SnappingStats:
27
+ method_counters: list[Counter[SnappingMethod]] = field(default_factory=list)
28
+ aggregated_counter: Counter[AggregatedSnappingMethod] = field(
29
+ default_factory=Counter
30
+ )
31
+
32
+ def reset(self) -> None:
33
+ self.method_counters.clear()
34
+ self.aggregated_counter.clear()
35
+
36
+ def process_result(
37
+ self, result: "SnappedTripPoints"
38
+ ) -> tuple[Counter[SnappingMethod], AggregatedSnappingMethod]:
39
+ counter = Counter(result.methods)
40
+ self.method_counters.append(counter)
41
+ agg_method = self.aggregate_methods(counter)
42
+ self.aggregated_counter[agg_method] += 1
43
+ self.aggregated_counter[AggregatedSnappingMethod.total] += 1
44
+ return counter, agg_method
45
+
46
+ def aggregate_methods(
47
+ self, counter: Counter[SnappingMethod]
48
+ ) -> AggregatedSnappingMethod:
49
+ n_total = sum(v for v in counter.values())
50
+ if counter[SnappingMethod.trusted] == n_total:
51
+ return AggregatedSnappingMethod.all_trusted
52
+ elif counter[SnappingMethod.routed] == n_total:
53
+ return AggregatedSnappingMethod.all_routed
54
+ elif counter[SnappingMethod.fallback] == n_total:
55
+ return AggregatedSnappingMethod.all_fallback
56
+ else:
57
+ return AggregatedSnappingMethod.mixed
58
+
59
+ def process_failure(self) -> None:
60
+ agg_method = AggregatedSnappingMethod.failed
61
+ self.aggregated_counter[agg_method] += 1
62
+ self.aggregated_counter[AggregatedSnappingMethod.total] += 1
63
+ logger.debug("snapping failed")
64
+
65
+ @typing.overload
66
+ def snap_trip_points(
67
+ self,
68
+ ttrip: "TrajectoryTrip",
69
+ *args,
70
+ log_failed: bool = False,
71
+ raise_failed: typing.Literal[True] = ...,
72
+ **kwargs,
73
+ ) -> "SnappedTripPoints": ...
74
+
75
+ @typing.overload
76
+ def snap_trip_points(
77
+ self,
78
+ ttrip: "TrajectoryTrip",
79
+ *args,
80
+ log_failed: bool = False,
81
+ raise_failed: typing.Literal[False],
82
+ **kwargs,
83
+ ) -> typing.Optional["SnappedTripPoints"]: ...
84
+
85
+ def snap_trip_points(
86
+ self,
87
+ ttrip: "TrajectoryTrip",
88
+ *args,
89
+ log_failed: bool = False,
90
+ raise_failed: bool = True,
91
+ **kwargs,
92
+ ) -> typing.Optional["SnappedTripPoints"]:
93
+ """Wrapper for `TrajectoryTrip.snap_trip_points` that updates the statistics.
94
+
95
+ Snapping statistics and failures are aggregated and logged at level DEBUG. Apart
96
+ from that, by default, snapping errors are raised and not logged. By setting
97
+ `raise_failed` and/or `log_failed` you can choose whether to raise and/or log
98
+ failures at level ERROR (with traceback).
99
+
100
+ If you need more custom logging, you should either raise, catch and log or not
101
+ use this wrapper and use `process_result` and `process_failure` instead for even
102
+ more control.
103
+ """
104
+ try:
105
+ result = ttrip.snap_trip_points(*args, **kwargs)
106
+ except SnappingError as error:
107
+ self.process_failure()
108
+ (logger.exception if log_failed else logger.debug)(
109
+ "snapping %r failed: %s",
110
+ ttrip,
111
+ error,
112
+ )
113
+ if raise_failed:
114
+ raise
115
+ else:
116
+ return None
117
+ else:
118
+ counter, agg_method = self.process_result(result)
119
+ logger.debug(
120
+ "snapping %r succeeded with methods %s aggregated to %s",
121
+ ttrip,
122
+ {k.value: v for k, v in counter.items()},
123
+ agg_method.value,
124
+ )
125
+ return result
126
+
127
+ def log_aggregated(self, prefix: str = "") -> None:
128
+ logger.info(
129
+ "%s%s",
130
+ prefix,
131
+ {k.value: v for k, v in self.aggregated_counter.items()},
132
+ )
@@ -15,6 +15,7 @@ from pysnapping.snap import (
15
15
  SnappingParams,
16
16
  )
17
17
  from pysnapping.linear_referencing import locate, interpolate, resample
18
+ from pysnapping.stats import SnappingStats
18
19
  from pysnapping.util import get_trafo, transform_coords
19
20
  from pysnapping import EPSG4326, EPSG4978
20
21
 
@@ -52,27 +53,40 @@ def segment_lengths_to_dists(segment_lengths, start=0):
52
53
 
53
54
 
54
55
  @pytest.mark.parametrize(
55
- "values,d_min,d_max,min_spacing,ok",
56
+ "values,d_min,d_max,min_spacing,consider_sampling_accuracy,ok",
56
57
  [
57
- ([], 0, 0, 10, True),
58
- ([np.NaN], 0, 0, 10, True),
59
- ([0], 0, 0, 10, True),
60
- ([1], 0, 1, 10, True),
61
- ([1], 0, 1 - 1e-6, 10, False),
62
- ([np.NaN, 1, np.NaN], 0, 2, 1 - 1e-6, True),
63
- ([np.NaN, 1, np.NaN], 0, 1.9, 1, False),
64
- ([-1, np.NaN, np.NaN, 0], -10, 10, 1, False),
65
- ([-1, np.NaN, np.NaN, 0], -10, 10, 0.5, False),
66
- ([-1, np.NaN, np.NaN, 0], -10, 10, 1 / 3 - 1e-6, True),
67
- ([np.NaN, np.NaN, np.NaN], -100, 0, 50 - 1e-6, True),
68
- ([np.NaN, np.NaN, np.NaN], -100, 0, 50 + 1e-6, False),
58
+ ([], 0, 0, 10, False, True),
59
+ ([np.nan], 0, 0, 10, False, True),
60
+ ([0], 0, 0, 10, False, True),
61
+ ([1], 0, 1, 10, False, True),
62
+ ([1], 0, 1 - 1e-6, 10, False, False),
63
+ ([np.nan, 1, np.nan], 0, 2, 1 - 1e-6, False, True),
64
+ ([np.nan, 1, np.nan], 0, 1.9, 1, False, False),
65
+ ([-1, np.nan, np.nan, 0], -10, 10, 1, False, False),
66
+ ([-1, np.nan, np.nan, 0], -10, 10, 0.5, False, False),
67
+ ([-1, np.nan, np.nan, 0], -10, 10, 1 / 3 - 1e-6, False, True),
68
+ ([np.nan, np.nan, np.nan], -100, 0, 50 - 1e-6, False, True),
69
+ ([np.nan, np.nan, np.nan], -100, 0, 50 + 1e-6, False, False),
70
+ ([0, 0, 1, 1, 1.001, 1.001, 10, 10], 0, 10, 0, False, True),
71
+ ([0, 0, 1, 1, 1.001, 1.001, 10, 10], 0, 10, 0, True, True),
72
+ ([0, 0, 1, np.nan, 1, 1.001, 1.001, 10, 10], 0, 10, 0, False, True),
73
+ ([0, 0, 1, np.nan, 1, 1.001, 1.001, 10, 10], 0, 10, 0, True, False),
74
+ ([-1e9, 1e9], -1e9, 1e9, 0, False, True),
75
+ ([-1e9, 1e9], -1e9, 1e9, 0, True, True),
76
+ ([-1e9, 1e9], -1e9, 1e9, 2.01 * 5.0, False, True),
77
+ ([-1e9, 1e9], -1e9, 1e9, 2.01 * 5.0, True, True),
69
78
  ],
70
79
  )
71
80
  def test_spacing_ok(
72
- values: "ArrayLike", d_min: float, d_max: float, min_spacing: float, ok: bool
81
+ values: "ArrayLike",
82
+ d_min: float,
83
+ d_max: float,
84
+ min_spacing: float,
85
+ consider_sampling_accuracy: bool,
86
+ ok: bool,
73
87
  ) -> None:
74
88
  params = SnappingParams(min_spacing=min_spacing)
75
- assert params.spacing_ok(values, d_min, d_max) == ok
89
+ assert params.spacing_ok(values, d_min, d_max, consider_sampling_accuracy) == ok
76
90
 
77
91
 
78
92
  # test around the entire earth with different orientations
@@ -276,6 +290,9 @@ def test_short_trajectory_and_random_garbage(seed):
276
290
  atol_snap=float("inf"),
277
291
  )
278
292
 
293
+ # just to get code coverage of stats module
294
+ stats = SnappingStats()
295
+
279
296
  rng = np.random.default_rng(seed=seed)
280
297
 
281
298
  if seed == 0:
@@ -312,7 +329,9 @@ def test_short_trajectory_and_random_garbage(seed):
312
329
 
313
330
  dtrip = DubiousTrajectoryTrip(dtraj, trip_xyzd)
314
331
  trip = dtrip.to_trajectory_trip()
315
- snapped = trip.snap_trip_points(params)
332
+ snapped = stats.snap_trip_points(trip, params)
333
+ stats.log_aggregated(prefix=f"seed {seed}: ")
334
+ stats.reset()
316
335
  min_spacing = min(params.min_spacing, trip.trajectory.length / (n_points - 1))
317
336
  for split_segment in snapped.get_inter_point_ls_lon_lat_in_travel_direction():
318
337
  assert (
@@ -350,11 +369,12 @@ def test_complex_trip():
350
369
 
351
370
 
352
371
  # 25 is default min_spacing and 5 is default sampling_step.
353
- # The cutoff is at 25 + 2.01 * 5 thus 35 should be too close but 36 should be good.
372
+ # The cutoff is at 25 thus 24.9 should be too close but 25.1 should be good.
354
373
  @pytest.mark.parametrize(
355
- "spacing,method", ((35, SnappingMethod.routed), (36, SnappingMethod.trusted))
374
+ "spacing,method", ((24.9, SnappingMethod.routed), (25.1, SnappingMethod.trusted))
356
375
  )
357
376
  def test_untrust_bad_spacing(spacing: float, method: SnappingMethod) -> None:
377
+ params = SnappingParams(atol_trusted=1e3)
358
378
  length = 50
359
379
  dtraj = make_dubious_traj(
360
380
  [length],
@@ -370,7 +390,7 @@ def test_untrust_bad_spacing(spacing: float, method: SnappingMethod) -> None:
370
390
  xyzd=trip_xyzd,
371
391
  )
372
392
  trip = dtrip.to_trajectory_trip()
373
- snapped = trip.snap_trip_points()
393
+ snapped = trip.snap_trip_points(params)
374
394
  assert not snapped.reverse_order
375
395
  assert np.all(snapped.methods == method)
376
396
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pysnapping
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Snap points to a line string keeping a given order or spacing intact
5
5
  Author-email: Alexander Held | geOps <alexander.held@geops.com>
6
6
  Maintainer-email: Alexander Held | geOps <alexander.held@geops.com>
@@ -40,8 +40,9 @@ Requires-Python: >=3.9
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE
42
42
  Requires-Dist: shapely
43
- Requires-Dist: numpy
43
+ Requires-Dist: numpy~=2.0
44
44
  Requires-Dist: pyproj>=3.2.0
45
+ Dynamic: license-file
45
46
 
46
47
  # PySnapping
47
48
 
@@ -85,9 +86,14 @@ The `pysnapping.util` module contains common helper functions used in other part
85
86
 
86
87
  ### Snapping
87
88
 
88
- The `pysanpping.snap` module is the main entry point for users of the `pysnapping` library and
89
+ The `pysnapping.snap` module is the main entry point for users of the `pysnapping` library and
89
90
  provides the classes needed to use the library.
90
91
 
92
+ ### Statistics
93
+
94
+ The `pysnapping.stats` module provides tools to aggregate statistics about snapping
95
+ failures and the methods used in case of success.
96
+
91
97
  ## Usage
92
98
 
93
99
  The typical usage pattern is to create a `pysnapping.snap.DubiousTrajectory` instance which represents
@@ -13,11 +13,14 @@ pysnapping/__init__.py
13
13
  pysnapping/linear_referencing.py
14
14
  pysnapping/py.typed
15
15
  pysnapping/snap.py
16
+ pysnapping/stats.py
16
17
  pysnapping/util.py
17
18
  pysnapping.egg-info/PKG-INFO
18
19
  pysnapping.egg-info/SOURCES.txt
19
20
  pysnapping.egg-info/dependency_links.txt
20
21
  pysnapping.egg-info/requires.txt
22
+ pysnapping.egg-info/scm_file_list.json
23
+ pysnapping.egg-info/scm_version.json
21
24
  pysnapping.egg-info/top_level.txt
22
25
  pysnapping/test/__init__.py
23
26
  pysnapping/test/complex_trip.geojson
@@ -1,3 +1,3 @@
1
1
  shapely
2
- numpy
2
+ numpy~=2.0
3
3
  pyproj>=3.2.0
@@ -0,0 +1,27 @@
1
+ {
2
+ "files": [
3
+ "pyproject.toml",
4
+ "LICENSE",
5
+ "MANIFEST.in",
6
+ ".pre-commit-config.yaml",
7
+ "dev-requirements.in",
8
+ "README.md",
9
+ ".gitignore",
10
+ "requirements.txt",
11
+ ".gitlab-ci.yml",
12
+ ".editorconfig",
13
+ "dev-requirements.txt",
14
+ "pysnapping/__init__.py",
15
+ "pysnapping/linear_referencing.py",
16
+ "pysnapping/util.py",
17
+ "pysnapping/snap.py",
18
+ "pysnapping/py.typed",
19
+ "pysnapping/stats.py",
20
+ "pysnapping/test/__init__.py",
21
+ "pysnapping/test/test_snap.py",
22
+ "pysnapping/test/test_util.py",
23
+ "pysnapping/test/test_linear_referencing.py",
24
+ "pysnapping/test/conftest.py",
25
+ "pysnapping/test/complex_trip.geojson"
26
+ ]
27
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.3",
3
+ "distance": 0,
4
+ "node": "g3978a8a6d40caae6c9ffd928cc5b8e56a36e187b",
5
+ "dirty": false,
6
+ "branch": "master",
7
+ "node_date": "2026-06-29"
8
+ }
@@ -0,0 +1,12 @@
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml -o requirements.txt -p 3.9
3
+ certifi==2025.1.31
4
+ # via pyproj
5
+ numpy==2.0.2
6
+ # via
7
+ # pysnapping (pyproject.toml)
8
+ # shapely
9
+ pyproj==3.6.1
10
+ # via pysnapping (pyproject.toml)
11
+ shapely==2.0.7
12
+ # via pysnapping (pyproject.toml)
@@ -1,16 +0,0 @@
1
- #
2
- # This file is autogenerated by pip-compile with Python 3.9
3
- # by the following command:
4
- #
5
- # pip-compile pyproject.toml
6
- #
7
- certifi==2023.11.17
8
- # via pyproj
9
- numpy==1.26.3
10
- # via
11
- # pysnapping (pyproject.toml)
12
- # shapely
13
- pyproj==3.6.1
14
- # via pysnapping (pyproject.toml)
15
- shapely==2.0.2
16
- # via pysnapping (pyproject.toml)
File without changes
File without changes
File without changes
File without changes