pysnapping 0.2.1__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.1 → pysnapping-0.2.2}/.gitlab-ci.yml +1 -1
- {pysnapping-0.2.1 → pysnapping-0.2.2}/.pre-commit-config.yaml +11 -14
- {pysnapping-0.2.1 → pysnapping-0.2.2}/PKG-INFO +9 -4
- {pysnapping-0.2.1 → pysnapping-0.2.2}/README.md +6 -1
- {pysnapping-0.2.1 → pysnapping-0.2.2}/dev-requirements.txt +11 -15
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pyproject.toml +2 -2
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/linear_referencing.py +6 -6
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/snap.py +19 -7
- pysnapping-0.2.2/pysnapping/stats.py +132 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/test_snap.py +39 -19
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping.egg-info/PKG-INFO +9 -4
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping.egg-info/SOURCES.txt +1 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping.egg-info/requires.txt +1 -1
- pysnapping-0.2.2/requirements.txt +12 -0
- pysnapping-0.2.1/requirements.txt +0 -16
- {pysnapping-0.2.1 → pysnapping-0.2.2}/.editorconfig +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/.gitignore +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/LICENSE +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/MANIFEST.in +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/dev-requirements.in +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/__init__.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/py.typed +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/__init__.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/complex_trip.geojson +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/conftest.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/test_linear_referencing.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/test/test_util.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping/util.py +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping.egg-info/dependency_links.txt +0 -0
- {pysnapping-0.2.1 → pysnapping-0.2.2}/pysnapping.egg-info/top_level.txt +0 -0
- {pysnapping-0.2.1 → 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
|
|
@@ -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]
|
|
@@ -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,7 +583,14 @@ 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
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
|
|
File without changes
|