BERATools 0.4.2__tar.gz → 0.5.0__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.
- {beratools-0.4.2 → beratools-0.5.0}/PKG-INFO +9 -15
- {beratools-0.4.2 → beratools-0.5.0}/README.md +2 -3
- beratools-0.5.0/beratools/core/algo_astar.py +397 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_canopy_footprint_absolute.py +114 -5
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_centerline.py +304 -37
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_check_seed_line_validate.py +220 -2
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_common.py +41 -23
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_cost.py +48 -0
- beratools-0.5.0/beratools/core/algo_geometry.py +83 -0
- beratools-0.5.0/beratools/core/alt_spatial_common.py +108 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/constants.py +23 -1
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/line_footprint_functions.py +1 -1
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/tool_geo_simplify.py +36 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/polygon_centerline/_src.py +131 -3
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/beratools.json +279 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/bt_gui_main.py +12 -8
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/canopy_footprint_absolute.py +22 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/centerline.py +67 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/check_seed_line.py +17 -0
- {beratools-0.4.2 → beratools-0.5.0}/pyproject.toml +10 -14
- {beratools-0.4.2 → beratools-0.5.0}/.gitignore +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/LICENSE +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/cli/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/cli/entry.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_check_seed_line.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_dijkstra.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_line_grouping.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_merge_lines.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_seed_line_correction.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_split_with_lines.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_vertex_optimization.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/algo_vertex_preclean.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/canopy_threshold_relative.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/logger.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/core/tool_base.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/geo_simplify/geo-simplify-linux-x64 +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/geo_simplify/geo-simplify-win-x64.exe +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/polygon_centerline/LICENSE.md +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/polygon_centerline/UPSTREAM.md +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/polygon_centerline/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/external/polygon_centerline/exceptions.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/BERALogo.ico +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/BERALogo.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/BERA_GUI_Main.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/BERA_Splash.gif +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/BERA_WizardImage.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/closed.gif +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/closed.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/gui.json +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/open.gif +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/open.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/tool.gif +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/assets/tool.png +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/bt_data.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/geometry_types.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/main.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/gui/tool_widgets.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/__init__.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/common.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/ground_footprint.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/line_footprint_relative.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/tool_template.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/tools/vertex_optimization.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/utility/env_checks.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/utility/spatial_common.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/utility/tool_args.py +0 -0
- {beratools-0.4.2 → beratools-0.5.0}/beratools/utility/unit_conversion.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BERATools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: An advanced forest line feature analysis platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/appliedgrg/beratools
|
|
6
6
|
Author-email: AppliedGRG <appliedgrg@gmail.com>, Richard Zeng <richardqzeng@gmail.com>
|
|
@@ -11,19 +11,18 @@ Classifier: Development Status :: 2 - Pre-Alpha
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
-
Requires-Python:
|
|
20
|
-
Requires-Dist: gdal; platform_system != 'Windows'
|
|
17
|
+
Requires-Python: <3.15,>=3.12
|
|
18
|
+
Requires-Dist: gdal<4,>=3.12; platform_system != 'Windows'
|
|
21
19
|
Requires-Dist: geopandas
|
|
22
20
|
Requires-Dist: networkit
|
|
21
|
+
Requires-Dist: numpy<2.4,>=1.26
|
|
23
22
|
Requires-Dist: pandarallel
|
|
24
|
-
Requires-Dist: pyogrio>=0.
|
|
23
|
+
Requires-Dist: pyogrio>=0.12.0
|
|
25
24
|
Requires-Dist: pyqt5
|
|
26
|
-
Requires-Dist: rasterio
|
|
25
|
+
Requires-Dist: rasterio>=1.5.0
|
|
27
26
|
Requires-Dist: scikit-image>=0.24.0
|
|
28
27
|
Requires-Dist: tabulate
|
|
29
28
|
Requires-Dist: tqdm
|
|
@@ -40,15 +39,11 @@ Requires-Dist: ruff; extra == 'dev'
|
|
|
40
39
|
Requires-Dist: tox; extra == 'dev'
|
|
41
40
|
Requires-Dist: twine; extra == 'dev'
|
|
42
41
|
Provides-Extra: doc
|
|
43
|
-
Requires-Dist: click==8.2.1; extra == 'doc'
|
|
44
|
-
Requires-Dist: mkdocs; extra == 'doc'
|
|
45
|
-
Requires-Dist: mkdocs-git-revision-date-localized-plugin; extra == 'doc'
|
|
46
|
-
Requires-Dist: mkdocs-material; extra == 'doc'
|
|
47
42
|
Requires-Dist: mkdocstrings; extra == 'doc'
|
|
48
43
|
Requires-Dist: mkdocstrings-python; extra == 'doc'
|
|
49
44
|
Requires-Dist: pymdown-extensions; extra == 'doc'
|
|
45
|
+
Requires-Dist: zensical==0.0.53; extra == 'doc'
|
|
50
46
|
Provides-Extra: test
|
|
51
|
-
Requires-Dist: gdal==3.8.4; extra == 'test'
|
|
52
47
|
Requires-Dist: pytest; extra == 'test'
|
|
53
48
|
Requires-Dist: pytest-qt; extra == 'test'
|
|
54
49
|
Requires-Dist: pywinauto; (platform_system == 'Windows') and extra == 'test'
|
|
@@ -60,12 +55,11 @@ BERA Tools is successor of [Forest Line Mapper](https://github.com/appliedgrg/fl
|
|
|
60
55
|
|
|
61
56
|
<div align="center">
|
|
62
57
|
|
|
63
|
-
[](https://codecov.io/gh/appliedgrg/beratools)
|
|
58
|
+
[](https://github.com/appliedgrg/beratools/actions/workflows/python-integration-tests.yml)
|
|
65
59
|
[](https://appliedgrg.github.io/beratools/)
|
|
66
60
|
[](https://anaconda.org/AppliedGRG/beratools)
|
|
67
61
|
[](https://pypi.org/project/BERATools/)
|
|
68
|
-
[](https://www.python.org/downloads/)
|
|
69
63
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
70
64
|
|
|
71
65
|
</div>
|
|
@@ -4,12 +4,11 @@ BERA Tools is successor of [Forest Line Mapper](https://github.com/appliedgrg/fl
|
|
|
4
4
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
|
|
7
|
-
[](https://codecov.io/gh/appliedgrg/beratools)
|
|
7
|
+
[](https://github.com/appliedgrg/beratools/actions/workflows/python-integration-tests.yml)
|
|
9
8
|
[](https://appliedgrg.github.io/beratools/)
|
|
10
9
|
[](https://anaconda.org/AppliedGRG/beratools)
|
|
11
10
|
[](https://pypi.org/project/BERATools/)
|
|
12
|
-
[](https://www.python.org/downloads/)
|
|
13
12
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
14
13
|
|
|
15
14
|
</div>
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Reusable A* path and corridor algorithms for centerline workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import heapq
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import count
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import rasterio
|
|
13
|
+
from rasterio.features import geometry_mask
|
|
14
|
+
from scipy import ndimage
|
|
15
|
+
from shapely.geometry import LineString
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SQRT2 = math.sqrt(2.0)
|
|
19
|
+
ASTAR_LINE_BIAS = 0.001
|
|
20
|
+
DEFAULT_CORRIDOR_LINE_BIAS_WEIGHT = 0.1
|
|
21
|
+
DEFAULT_CORRIDOR_DISTANCE_PENALTY_WEIGHT = 0.2
|
|
22
|
+
|
|
23
|
+
NEIGHBORS: tuple[tuple[int, int], ...] = (
|
|
24
|
+
(1, 0),
|
|
25
|
+
(0, -1),
|
|
26
|
+
(-1, 0),
|
|
27
|
+
(0, 1),
|
|
28
|
+
(1, -1),
|
|
29
|
+
(1, 1),
|
|
30
|
+
(-1, -1),
|
|
31
|
+
(-1, 1),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AStarAccumulation:
|
|
37
|
+
path: list[tuple[int, int]]
|
|
38
|
+
best_cost: float
|
|
39
|
+
g_scores: np.ndarray
|
|
40
|
+
closed: np.ndarray
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_least_cost_path_astar_closest_line(cost_arr, meta: dict, input_line: LineString) -> LineString | None:
|
|
44
|
+
"""Find an 8-neighbor A* LCP, tie-broken toward the seed-line direction."""
|
|
45
|
+
|
|
46
|
+
if input_line is None or input_line.is_empty or len(input_line.coords) < 2:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
costs, walkable = _prepare_lcp_costs(cost_arr, meta.get("nodata"))
|
|
50
|
+
rows, cols = costs.shape
|
|
51
|
+
transformer = rasterio.transform.AffineTransformer(meta["transform"])
|
|
52
|
+
start_xy = input_line.coords[0]
|
|
53
|
+
end_xy = input_line.coords[-1]
|
|
54
|
+
start = _clamp_row_col(transformer.rowcol(start_xy[0], start_xy[1]), rows, cols)
|
|
55
|
+
end = _clamp_row_col(transformer.rowcol(end_xy[0], end_xy[1]), rows, cols)
|
|
56
|
+
if not walkable[start] or not walkable[end]:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
min_cost = float(costs[walkable].min()) if np.any(walkable) else 0.0
|
|
60
|
+
tie_scores = np.full((rows, cols), math.inf, dtype="float64")
|
|
61
|
+
g_scores = np.full((rows, cols), math.inf, dtype="float64")
|
|
62
|
+
came_from: dict[tuple[int, int], tuple[int, int] | None] = {start: None}
|
|
63
|
+
closed: set[tuple[int, int]] = set()
|
|
64
|
+
sequence = count()
|
|
65
|
+
heap: list[tuple] = []
|
|
66
|
+
|
|
67
|
+
g_scores[start] = 0.0
|
|
68
|
+
tie_scores[start] = 0.0
|
|
69
|
+
heapq.heappush(heap, _lcp_queue_entry(start, end, start, 0.0, 0.0, min_cost, next(sequence)))
|
|
70
|
+
|
|
71
|
+
while heap:
|
|
72
|
+
*_, current = heapq.heappop(heap)
|
|
73
|
+
if current in closed:
|
|
74
|
+
continue
|
|
75
|
+
if current == end:
|
|
76
|
+
return _path_to_linestring(_reconstruct_path(came_from, end), transformer, start_xy, end_xy)
|
|
77
|
+
closed.add(current)
|
|
78
|
+
|
|
79
|
+
for neighbor in _neighbors(current, rows, cols, walkable):
|
|
80
|
+
if neighbor in closed:
|
|
81
|
+
continue
|
|
82
|
+
new_g = g_scores[current] + _lcp_edge_cost(costs, current, neighbor)
|
|
83
|
+
new_tie = _line_bias_score(neighbor, start, end)
|
|
84
|
+
current_g = g_scores[neighbor]
|
|
85
|
+
current_tie = tie_scores[neighbor]
|
|
86
|
+
improved = new_g < current_g - 1e-6
|
|
87
|
+
tied_better = abs(new_g - current_g) <= 1e-6 and new_tie < current_tie
|
|
88
|
+
if not improved and not tied_better:
|
|
89
|
+
continue
|
|
90
|
+
g_scores[neighbor] = new_g
|
|
91
|
+
tie_scores[neighbor] = new_tie
|
|
92
|
+
came_from[neighbor] = current
|
|
93
|
+
heapq.heappush(
|
|
94
|
+
heap,
|
|
95
|
+
_lcp_queue_entry(neighbor, end, start, new_g, new_tie, min_cost, next(sequence)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def astar_accumulation_corridor_raster(
|
|
102
|
+
cost_arr,
|
|
103
|
+
meta: dict,
|
|
104
|
+
lc_path: LineString,
|
|
105
|
+
*,
|
|
106
|
+
corridor_threshold: float,
|
|
107
|
+
line_bias_weight: float = DEFAULT_CORRIDOR_LINE_BIAS_WEIGHT,
|
|
108
|
+
distance_penalty_weight: float = DEFAULT_CORRIDOR_DISTANCE_PENALTY_WEIGHT,
|
|
109
|
+
) -> tuple[np.ma.MaskedArray, dict[str, object]]:
|
|
110
|
+
"""Build an A* accumulation corridor raster using BERA's 0-inside/1-outside convention."""
|
|
111
|
+
|
|
112
|
+
if lc_path is None or lc_path.is_empty or len(lc_path.coords) < 2:
|
|
113
|
+
raise RuntimeError("A* corridor requires a valid least-cost path")
|
|
114
|
+
|
|
115
|
+
cost = _prepare_corridor_cost_surface(cost_arr, meta)
|
|
116
|
+
rows, cols = cost.shape
|
|
117
|
+
transform = meta["transform"]
|
|
118
|
+
transformer = rasterio.transform.AffineTransformer(transform)
|
|
119
|
+
start_xy = lc_path.coords[0]
|
|
120
|
+
end_xy = lc_path.coords[-1]
|
|
121
|
+
source = _clamp_row_col(transformer.rowcol(*start_xy), rows, cols)
|
|
122
|
+
destination = _clamp_row_col(transformer.rowcol(*end_xy), rows, cols)
|
|
123
|
+
sampling = _raster_sampling(transform)
|
|
124
|
+
|
|
125
|
+
forward = _astar_mcp_geometric_accumulation(
|
|
126
|
+
cost,
|
|
127
|
+
source,
|
|
128
|
+
destination,
|
|
129
|
+
sampling=sampling,
|
|
130
|
+
line_bias_weight=line_bias_weight,
|
|
131
|
+
)
|
|
132
|
+
reverse = _astar_mcp_geometric_accumulation(
|
|
133
|
+
cost,
|
|
134
|
+
destination,
|
|
135
|
+
source,
|
|
136
|
+
sampling=sampling,
|
|
137
|
+
line_bias_weight=line_bias_weight,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
valid = forward.closed & reverse.closed
|
|
141
|
+
score_data = forward.g_scores + reverse.g_scores - forward.best_cost
|
|
142
|
+
if distance_penalty_weight > 0.0:
|
|
143
|
+
astar_path = _path_to_linestring(forward.path, transformer, start_xy, end_xy)
|
|
144
|
+
distance_to_path = _distance_raster_to_line(astar_path, meta, cost.shape)
|
|
145
|
+
score_data = score_data + distance_to_path * distance_penalty_weight
|
|
146
|
+
|
|
147
|
+
score = np.ma.masked_invalid(score_data)
|
|
148
|
+
score = np.ma.array(score, mask=np.ma.getmaskarray(score) | ~valid)
|
|
149
|
+
inside = (~np.ma.getmaskarray(score)) & np.asarray(score.filled(np.inf) < corridor_threshold, dtype=bool)
|
|
150
|
+
corridor = np.ma.where(inside, 0.0, 1.0)
|
|
151
|
+
corridor = np.ma.array(corridor, mask=np.ma.getmaskarray(score))
|
|
152
|
+
|
|
153
|
+
details = {
|
|
154
|
+
"corridor_threshold": float(corridor_threshold),
|
|
155
|
+
"astar_line_bias_weight": float(line_bias_weight),
|
|
156
|
+
"astar_distance_penalty_weight": float(distance_penalty_weight),
|
|
157
|
+
"astar_best_cost": float(forward.best_cost),
|
|
158
|
+
"inside_cells": int(np.count_nonzero(inside)),
|
|
159
|
+
"inside_area": float(np.count_nonzero(inside) * _cell_area(transform)),
|
|
160
|
+
"forward_closed_cells": int(np.count_nonzero(forward.closed)),
|
|
161
|
+
"reverse_closed_cells": int(np.count_nonzero(reverse.closed)),
|
|
162
|
+
"both_closed_cells": int(np.count_nonzero(valid)),
|
|
163
|
+
"astar_path_vertices": int(len(forward.path)),
|
|
164
|
+
}
|
|
165
|
+
return corridor, details
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _prepare_lcp_costs(cost_arr, nodata) -> tuple[np.ndarray, np.ndarray]:
|
|
169
|
+
array = np.ma.asarray(cost_arr)
|
|
170
|
+
if array.ndim > 2:
|
|
171
|
+
array = np.ma.squeeze(array, axis=0)
|
|
172
|
+
costs = np.asarray(array.filled(np.nan), dtype="float64")
|
|
173
|
+
walkable = np.isfinite(costs) & (costs > 0.0)
|
|
174
|
+
if nodata is not None:
|
|
175
|
+
walkable &= costs != float(nodata)
|
|
176
|
+
return costs, walkable
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _prepare_corridor_cost_surface(cost_arr, meta: dict) -> np.ndarray:
|
|
180
|
+
arr = np.ma.asarray(cost_arr)
|
|
181
|
+
if arr.ndim > 2:
|
|
182
|
+
arr = np.ma.squeeze(arr, axis=0)
|
|
183
|
+
cost = np.asarray(arr.filled(np.inf), dtype="float64")
|
|
184
|
+
nodata = meta.get("nodata")
|
|
185
|
+
if nodata is not None:
|
|
186
|
+
cost[cost == float(nodata)] = np.inf
|
|
187
|
+
cost[~np.isfinite(cost)] = np.inf
|
|
188
|
+
cost[cost <= 0.0] = np.inf
|
|
189
|
+
return cost
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _astar_mcp_geometric_accumulation(
|
|
193
|
+
cost: np.ndarray,
|
|
194
|
+
source: tuple[int, int],
|
|
195
|
+
destination: tuple[int, int],
|
|
196
|
+
*,
|
|
197
|
+
sampling: tuple[float, float],
|
|
198
|
+
line_bias_weight: float,
|
|
199
|
+
) -> AStarAccumulation:
|
|
200
|
+
if not np.isfinite(cost[source]) or not np.isfinite(cost[destination]):
|
|
201
|
+
raise RuntimeError("A* source or destination is not traversable")
|
|
202
|
+
|
|
203
|
+
rows, cols = cost.shape
|
|
204
|
+
walkable = np.isfinite(cost) & (cost > 0.0)
|
|
205
|
+
min_cost = float(cost[walkable].min()) if np.any(walkable) else 0.0
|
|
206
|
+
g_scores = np.full(cost.shape, math.inf, dtype="float64")
|
|
207
|
+
tie_scores = np.full(cost.shape, math.inf, dtype="float64")
|
|
208
|
+
came_from: dict[tuple[int, int], tuple[int, int] | None] = {source: None}
|
|
209
|
+
closed_nodes: set[tuple[int, int]] = set()
|
|
210
|
+
closed = np.zeros(cost.shape, dtype=bool)
|
|
211
|
+
sequence = count()
|
|
212
|
+
heap: list[tuple[float, float, float, int, tuple[int, int]]] = []
|
|
213
|
+
|
|
214
|
+
g_scores[source] = 0.0
|
|
215
|
+
tie_scores[source] = 0.0
|
|
216
|
+
destination_path = None
|
|
217
|
+
destination_cost = None
|
|
218
|
+
heapq.heappush(
|
|
219
|
+
heap,
|
|
220
|
+
_corridor_queue_entry(
|
|
221
|
+
source,
|
|
222
|
+
destination,
|
|
223
|
+
source,
|
|
224
|
+
0.0,
|
|
225
|
+
0.0,
|
|
226
|
+
min_cost,
|
|
227
|
+
sampling,
|
|
228
|
+
line_bias_weight,
|
|
229
|
+
next(sequence),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
while heap:
|
|
234
|
+
*_, current = heapq.heappop(heap)
|
|
235
|
+
if current in closed_nodes:
|
|
236
|
+
continue
|
|
237
|
+
if current == destination and destination_path is None:
|
|
238
|
+
destination_path = _reconstruct_path(came_from, destination)
|
|
239
|
+
destination_cost = float(g_scores[destination])
|
|
240
|
+
closed_nodes.add(current)
|
|
241
|
+
closed[current] = True
|
|
242
|
+
|
|
243
|
+
for neighbor in _neighbors(current, rows, cols, walkable):
|
|
244
|
+
if neighbor in closed_nodes:
|
|
245
|
+
continue
|
|
246
|
+
new_g = g_scores[current] + _geometric_edge_cost(cost, current, neighbor, sampling)
|
|
247
|
+
new_tie = _line_bias_score(neighbor, source, destination)
|
|
248
|
+
improved = new_g < g_scores[neighbor] - 1e-9
|
|
249
|
+
tied_better = abs(new_g - g_scores[neighbor]) <= 1e-9 and new_tie < tie_scores[neighbor]
|
|
250
|
+
if not improved and not tied_better:
|
|
251
|
+
continue
|
|
252
|
+
g_scores[neighbor] = new_g
|
|
253
|
+
tie_scores[neighbor] = new_tie
|
|
254
|
+
came_from[neighbor] = current
|
|
255
|
+
heapq.heappush(
|
|
256
|
+
heap,
|
|
257
|
+
_corridor_queue_entry(
|
|
258
|
+
neighbor,
|
|
259
|
+
destination,
|
|
260
|
+
source,
|
|
261
|
+
new_g,
|
|
262
|
+
new_tie,
|
|
263
|
+
min_cost,
|
|
264
|
+
sampling,
|
|
265
|
+
line_bias_weight,
|
|
266
|
+
next(sequence),
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if destination_path is None or destination_cost is None:
|
|
271
|
+
raise RuntimeError("A* path did not reach destination")
|
|
272
|
+
|
|
273
|
+
return AStarAccumulation(
|
|
274
|
+
path=destination_path,
|
|
275
|
+
best_cost=destination_cost,
|
|
276
|
+
g_scores=g_scores,
|
|
277
|
+
closed=closed,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _neighbors(node: tuple[int, int], rows: int, cols: int, walkable: np.ndarray) -> Iterable[tuple[int, int]]:
|
|
282
|
+
row, col = node
|
|
283
|
+
for d_row, d_col in NEIGHBORS:
|
|
284
|
+
neighbor = row + d_row, col + d_col
|
|
285
|
+
if 0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols and walkable[neighbor]:
|
|
286
|
+
yield neighbor
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _lcp_edge_cost(costs: np.ndarray, current: tuple[int, int], neighbor: tuple[int, int]) -> float:
|
|
290
|
+
base = (float(costs[current]) + float(costs[neighbor])) / 2.0
|
|
291
|
+
return base * (SQRT2 if current[0] != neighbor[0] and current[1] != neighbor[1] else 1.0)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _geometric_edge_cost(
|
|
295
|
+
cost: np.ndarray,
|
|
296
|
+
current: tuple[int, int],
|
|
297
|
+
neighbor: tuple[int, int],
|
|
298
|
+
sampling: tuple[float, float],
|
|
299
|
+
) -> float:
|
|
300
|
+
d_row = neighbor[0] - current[0]
|
|
301
|
+
d_col = neighbor[1] - current[1]
|
|
302
|
+
offset_length = math.hypot(d_row * sampling[0], d_col * sampling[1])
|
|
303
|
+
return offset_length * 0.5 * (float(cost[current]) + float(cost[neighbor]))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _lcp_queue_entry(
|
|
307
|
+
node: tuple[int, int],
|
|
308
|
+
end: tuple[int, int],
|
|
309
|
+
start: tuple[int, int],
|
|
310
|
+
g_score: float,
|
|
311
|
+
tie_score: float,
|
|
312
|
+
min_cost: float,
|
|
313
|
+
sequence: int,
|
|
314
|
+
) -> tuple:
|
|
315
|
+
heuristic = _octile(node, end) * min_cost
|
|
316
|
+
priority = g_score + heuristic + tie_score * ASTAR_LINE_BIAS
|
|
317
|
+
return priority, g_score + heuristic, tie_score, heuristic, sequence, node
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _corridor_queue_entry(
|
|
321
|
+
node: tuple[int, int],
|
|
322
|
+
destination: tuple[int, int],
|
|
323
|
+
source: tuple[int, int],
|
|
324
|
+
g_score: float,
|
|
325
|
+
tie_score: float,
|
|
326
|
+
min_cost: float,
|
|
327
|
+
sampling: tuple[float, float],
|
|
328
|
+
line_bias_weight: float,
|
|
329
|
+
sequence: int,
|
|
330
|
+
) -> tuple[float, float, float, int, tuple[int, int]]:
|
|
331
|
+
heuristic = _euclidean_grid_distance(node, destination, sampling) * min_cost
|
|
332
|
+
priority = g_score + heuristic + tie_score * line_bias_weight
|
|
333
|
+
return priority, heuristic, _line_bias_score(node, source, destination), sequence, node
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _octile(a: tuple[int, int], b: tuple[int, int]) -> float:
|
|
337
|
+
d_row = abs(a[0] - b[0])
|
|
338
|
+
d_col = abs(a[1] - b[1])
|
|
339
|
+
return d_row + d_col + (SQRT2 - 2.0) * min(d_row, d_col)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _euclidean_grid_distance(a: tuple[int, int], b: tuple[int, int], sampling: tuple[float, float]) -> float:
|
|
343
|
+
return math.hypot((a[0] - b[0]) * sampling[0], (a[1] - b[1]) * sampling[1])
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _line_bias_score(node: tuple[int, int], start: tuple[int, int], end: tuple[int, int]) -> float:
|
|
347
|
+
dx1 = node[1] - end[1]
|
|
348
|
+
dy1 = node[0] - end[0]
|
|
349
|
+
dx2 = start[1] - end[1]
|
|
350
|
+
dy2 = start[0] - end[0]
|
|
351
|
+
return abs(dx1 * dy2 - dx2 * dy1) / max(1.0, math.hypot(dx2, dy2))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _reconstruct_path(
|
|
355
|
+
came_from: dict[tuple[int, int], tuple[int, int] | None], end: tuple[int, int]
|
|
356
|
+
) -> list[tuple[int, int]]:
|
|
357
|
+
path = [end]
|
|
358
|
+
current = end
|
|
359
|
+
while came_from[current] is not None:
|
|
360
|
+
current = came_from[current]
|
|
361
|
+
path.append(current)
|
|
362
|
+
path.reverse()
|
|
363
|
+
return path
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _path_to_linestring(path: list[tuple[int, int]], transformer, start_xy, end_xy) -> LineString | None:
|
|
367
|
+
if len(path) < 2:
|
|
368
|
+
return None
|
|
369
|
+
points = [transformer.xy(row, col) for row, col in path]
|
|
370
|
+
points[0] = (start_xy[0], start_xy[1])
|
|
371
|
+
points[-1] = (end_xy[0], end_xy[1])
|
|
372
|
+
return LineString(points)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _distance_raster_to_line(line: LineString, meta: dict, shape: tuple[int, int]) -> np.ndarray:
|
|
376
|
+
transform = meta["transform"]
|
|
377
|
+
cell_size = max(abs(float(transform.a)), abs(float(transform.e)))
|
|
378
|
+
path_mask = geometry_mask(
|
|
379
|
+
[line.buffer(cell_size * 0.5)],
|
|
380
|
+
out_shape=shape,
|
|
381
|
+
transform=transform,
|
|
382
|
+
invert=True,
|
|
383
|
+
all_touched=True,
|
|
384
|
+
)
|
|
385
|
+
return ndimage.distance_transform_edt(~path_mask, sampling=_raster_sampling(transform))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _raster_sampling(transform) -> tuple[float, float]:
|
|
389
|
+
return (abs(float(transform.e)), abs(float(transform.a)))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _cell_area(transform) -> float:
|
|
393
|
+
return abs(float(transform.a) * float(transform.e))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _clamp_row_col(row_col: tuple[int, int], rows: int, cols: int) -> tuple[int, int]:
|
|
397
|
+
return max(0, min(int(row_col[0]), rows - 1)), max(0, min(int(row_col[1]), cols - 1))
|