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.
- {pysnapping-0.2.1 → pysnapping-0.2.3}/.gitignore +1 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/.gitlab-ci.yml +1 -1
- {pysnapping-0.2.1 → pysnapping-0.2.3}/.pre-commit-config.yaml +11 -14
- {pysnapping-0.2.1 → pysnapping-0.2.3}/PKG-INFO +10 -4
- {pysnapping-0.2.1 → pysnapping-0.2.3}/README.md +6 -1
- {pysnapping-0.2.1 → pysnapping-0.2.3}/dev-requirements.txt +11 -15
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pyproject.toml +3 -5
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/linear_referencing.py +12 -15
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/snap.py +20 -8
- pysnapping-0.2.3/pysnapping/stats.py +132 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_snap.py +39 -19
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/PKG-INFO +10 -4
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/SOURCES.txt +3 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/requires.txt +1 -1
- pysnapping-0.2.3/pysnapping.egg-info/scm_file_list.json +27 -0
- pysnapping-0.2.3/pysnapping.egg-info/scm_version.json +8 -0
- pysnapping-0.2.3/requirements.txt +12 -0
- pysnapping-0.2.1/requirements.txt +0 -16
- {pysnapping-0.2.1 → pysnapping-0.2.3}/.editorconfig +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/LICENSE +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/MANIFEST.in +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/dev-requirements.in +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/__init__.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/py.typed +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/__init__.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/complex_trip.geojson +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/conftest.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_linear_referencing.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/test/test_util.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping/util.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/dependency_links.txt +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/pysnapping.egg-info/top_level.txt +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.3}/setup.cfg +0 -0
|
@@ -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:
|
|
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.
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pysnapping
|
|
3
|
-
Version: 0.2.
|
|
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 `
|
|
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 `
|
|
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
|
-
#
|
|
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
|
|
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==
|
|
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
|
-
|
|
627
|
-
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
143
|
+
nan_min_spacing = self.min_spacing
|
|
144
144
|
if consider_sampling_accuracy:
|
|
145
|
-
|
|
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
|
|
151
|
+
padded_values[0] = d_min
|
|
151
152
|
padded_values[1:-1] = values_arr
|
|
152
|
-
padded_values[-1] = d_max
|
|
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
|
-
|
|
157
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
59
|
-
([0], 0, 0, 10, True),
|
|
60
|
-
([1], 0, 1, 10, True),
|
|
61
|
-
([1], 0, 1 - 1e-6, 10, False),
|
|
62
|
-
([np.
|
|
63
|
-
([np.
|
|
64
|
-
([-1, np.
|
|
65
|
-
([-1, np.
|
|
66
|
-
([-1, np.
|
|
67
|
-
([np.
|
|
68
|
-
([np.
|
|
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",
|
|
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 =
|
|
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
|
|
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", ((
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pysnapping
|
|
3
|
-
Version: 0.2.
|
|
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 `
|
|
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
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|