pysnapping 0.2.0__tar.gz → 0.2.2__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.0 → pysnapping-0.2.2}/.gitlab-ci.yml +1 -1
- {pysnapping-0.2.0 → pysnapping-0.2.2}/.pre-commit-config.yaml +11 -14
- {pysnapping-0.2.0 → pysnapping-0.2.2}/PKG-INFO +9 -4
- {pysnapping-0.2.0 → pysnapping-0.2.2}/README.md +6 -1
- {pysnapping-0.2.0 → pysnapping-0.2.2}/dev-requirements.txt +11 -15
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pyproject.toml +2 -2
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/__init__.py +27 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/linear_referencing.py +6 -6
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/snap.py +28 -13
- pysnapping-0.2.2/pysnapping/stats.py +132 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/test_snap.py +39 -19
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping.egg-info/PKG-INFO +9 -4
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping.egg-info/SOURCES.txt +1 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping.egg-info/requires.txt +1 -1
- pysnapping-0.2.2/requirements.txt +12 -0
- pysnapping-0.2.0/requirements.txt +0 -16
- {pysnapping-0.2.0 → pysnapping-0.2.2}/.editorconfig +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/.gitignore +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/LICENSE +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/MANIFEST.in +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/dev-requirements.in +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/py.typed +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/__init__.py +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/complex_trip.geojson +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/conftest.py +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/test_linear_referencing.py +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/test/test_util.py +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping/util.py +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping.egg-info/dependency_links.txt +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/pysnapping.egg-info/top_level.txt +0 -0
- {pysnapping-0.2.0 → pysnapping-0.2.2}/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.2
|
|
2
2
|
Name: pysnapping
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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,7 +40,7 @@ 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
|
|
44
44
|
Requires-Dist: pyproj>=3.2.0
|
|
45
45
|
|
|
46
46
|
# PySnapping
|
|
@@ -85,9 +85,14 @@ The `pysnapping.util` module contains common helper functions used in other part
|
|
|
85
85
|
|
|
86
86
|
### Snapping
|
|
87
87
|
|
|
88
|
-
The `
|
|
88
|
+
The `pysnapping.snap` module is the main entry point for users of the `pysnapping` library and
|
|
89
89
|
provides the classes needed to use the library.
|
|
90
90
|
|
|
91
|
+
### Statistics
|
|
92
|
+
|
|
93
|
+
The `pysnapping.stats` module provides tools to aggregate statistics about snapping
|
|
94
|
+
failures and the methods used in case of success.
|
|
95
|
+
|
|
91
96
|
## Usage
|
|
92
97
|
|
|
93
98
|
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==1.26.
|
|
29
|
+
numpy==1.26.4
|
|
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",
|
|
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
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import typing
|
|
1
2
|
from enum import Enum
|
|
2
3
|
|
|
3
4
|
import pyproj
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
if typing.TYPE_CHECKING:
|
|
8
|
+
from numpy.typing import ArrayLike
|
|
4
9
|
|
|
5
10
|
|
|
6
11
|
EPSG4326 = pyproj.CRS.from_epsg(4326)
|
|
@@ -25,6 +30,28 @@ class SnappingError(ValueError):
|
|
|
25
30
|
pass
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
class BadShortestDistances(SnappingError):
|
|
34
|
+
bad_indices: np.ndarray
|
|
35
|
+
bad_distances: np.ndarray
|
|
36
|
+
max_allowed_distance: float
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
bad_indices: "ArrayLike",
|
|
41
|
+
distances: "ArrayLike",
|
|
42
|
+
max_allowed_distance: float,
|
|
43
|
+
) -> None:
|
|
44
|
+
self.bad_indices = np.array(bad_indices, dtype=int)
|
|
45
|
+
self.bad_distances = np.asarray(distances, dtype=float)[self.bad_indices]
|
|
46
|
+
self.max_allowed_distance = max_allowed_distance
|
|
47
|
+
message = (
|
|
48
|
+
f"The shortest distances {self.bad_distances} between points at indices "
|
|
49
|
+
f"{self.bad_indices} and the trajectory are greater than "
|
|
50
|
+
f"{max_allowed_distance:g} meters."
|
|
51
|
+
)
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
|
|
54
|
+
|
|
28
55
|
class ExtrapolationError(ValueError):
|
|
29
56
|
pass
|
|
30
57
|
|
|
@@ -622,15 +622,15 @@ class SlicedLineFractions:
|
|
|
622
622
|
right_fractions = np.linspace(
|
|
623
623
|
self.closest_fractions[i], self.max_fractions[i], n_right_samples[i]
|
|
624
624
|
)
|
|
625
|
-
locations.from_vertices[
|
|
626
|
-
|
|
627
|
-
|
|
625
|
+
locations.from_vertices[index_offset : index_offset + n_samples[i]] = (
|
|
626
|
+
self.segment_indices[i]
|
|
627
|
+
)
|
|
628
628
|
locations.to_vertices[index_offset : index_offset + n_samples[i]] = (
|
|
629
629
|
self.segment_indices[i] + 1
|
|
630
630
|
)
|
|
631
|
-
locations.fractions[
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
locations.fractions[index_offset : index_offset + n_left_samples[i]] = (
|
|
632
|
+
left_fractions
|
|
633
|
+
)
|
|
634
634
|
# 1 sample overlap thus - 1
|
|
635
635
|
locations.fractions[
|
|
636
636
|
index_offset + n_left_samples[i] - 1 : index_offset + n_samples[i]
|
|
@@ -27,6 +27,7 @@ from .util import (
|
|
|
27
27
|
)
|
|
28
28
|
from . import (
|
|
29
29
|
SnappingError,
|
|
30
|
+
BadShortestDistances,
|
|
30
31
|
ExtrapolationError,
|
|
31
32
|
EPSG4326,
|
|
32
33
|
EPSG4978,
|
|
@@ -139,21 +140,26 @@ class SnappingParams(typing.NamedTuple):
|
|
|
139
140
|
|
|
140
141
|
values_arr = np.asarray(values, dtype=float)
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
nan_min_spacing = self.min_spacing
|
|
143
144
|
if consider_sampling_accuracy:
|
|
144
|
-
|
|
145
|
+
# extra spacing for numerical solution for untrusted points
|
|
146
|
+
nan_min_spacing += 2.01 * self.sampling_step
|
|
145
147
|
|
|
146
148
|
# padding to treat NaNs at the boundary, d_min, d_max and min_spacing in an
|
|
147
149
|
# elegant way
|
|
148
150
|
padded_values = np.empty(len(values_arr) + 2)
|
|
149
|
-
padded_values[0] = d_min
|
|
151
|
+
padded_values[0] = d_min
|
|
150
152
|
padded_values[1:-1] = values_arr
|
|
151
|
-
padded_values[-1] = d_max
|
|
153
|
+
padded_values[-1] = d_max
|
|
152
154
|
del values_arr # prevent accidental usage
|
|
153
155
|
|
|
154
156
|
non_nan_indices = np.nonzero(np.logical_not(np.isnan(padded_values)))[0]
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
)
|
|
157
163
|
|
|
158
164
|
# bool to ensure builtin bool (and not numpy bool)
|
|
159
165
|
return bool(np.all(np.diff(padded_values[non_nan_indices]) >= min_spacing_arr))
|
|
@@ -443,12 +449,14 @@ class TrajectoryTrip(XYZDMixin):
|
|
|
443
449
|
See `SnappingParams` for an explanation how the snapping is done and how it can
|
|
444
450
|
be controlled.
|
|
445
451
|
"""
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
+
bad_indices = np.nonzero(
|
|
453
|
+
self.projected_points.cartesian_distances > params.max_shortest_distance
|
|
454
|
+
)[0]
|
|
455
|
+
if bad_indices.size:
|
|
456
|
+
raise BadShortestDistances(
|
|
457
|
+
bad_indices,
|
|
458
|
+
self.projected_points.cartesian_distances,
|
|
459
|
+
params.max_shortest_distance,
|
|
452
460
|
)
|
|
453
461
|
fallback = False
|
|
454
462
|
reverse_order_allowed = params.reverse_order_allowed and bool(
|
|
@@ -575,7 +583,14 @@ class TrajectoryTrip(XYZDMixin):
|
|
|
575
583
|
)
|
|
576
584
|
ppoints_list[i_global] = ppoints
|
|
577
585
|
else:
|
|
578
|
-
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
|
+
)
|
|
579
594
|
# if the spacing was not ok, it can still be bad after untrusting all points
|
|
580
595
|
# since the total length can be too short for the points
|
|
581
596
|
untrusted_dists = np.full_like(self.dists, np.NaN)
|
|
@@ -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",
|
|
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.2
|
|
2
2
|
Name: pysnapping
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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,7 +40,7 @@ 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
|
|
44
44
|
Requires-Dist: pyproj>=3.2.0
|
|
45
45
|
|
|
46
46
|
# PySnapping
|
|
@@ -85,9 +85,14 @@ The `pysnapping.util` module contains common helper functions used in other part
|
|
|
85
85
|
|
|
86
86
|
### Snapping
|
|
87
87
|
|
|
88
|
-
The `
|
|
88
|
+
The `pysnapping.snap` module is the main entry point for users of the `pysnapping` library and
|
|
89
89
|
provides the classes needed to use the library.
|
|
90
90
|
|
|
91
|
+
### Statistics
|
|
92
|
+
|
|
93
|
+
The `pysnapping.stats` module provides tools to aggregate statistics about snapping
|
|
94
|
+
failures and the methods used in case of success.
|
|
95
|
+
|
|
91
96
|
## Usage
|
|
92
97
|
|
|
93
98
|
The typical usage pattern is to create a `pysnapping.snap.DubiousTrajectory` instance which represents
|
|
@@ -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==1.26.4
|
|
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
|