gri-convolve 0.2.0__py3-none-any.whl
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.
- gri_convolve/__init__.py +5 -0
- gri_convolve/altitude/__init__.py +5 -0
- gri_convolve/altitude/nearest.py +28 -0
- gri_convolve/convolve/__init__.py +7 -0
- gri_convolve/convolve/atwa_convolve.py +186 -0
- gri_convolve/convolve/cluster_convolve.py +260 -0
- gri_convolve/convolve/convolve.py +88 -0
- gri_convolve/convolve/smart_convolve.py +72 -0
- gri_convolve/py.typed +0 -0
- gri_convolve-0.2.0.dist-info/METADATA +175 -0
- gri_convolve-0.2.0.dist-info/RECORD +13 -0
- gri_convolve-0.2.0.dist-info/WHEEL +4 -0
- gri_convolve-0.2.0.dist-info/licenses/LICENSE +21 -0
gri_convolve/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Simple nearest distance altitude setter for smart convolve."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from gri_pos import LLA
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def nearest(inputs: Sequence[tuple[LLA, list[LLA]]]) -> list[float]:
|
|
14
|
+
"""Take a list of (LLA, list[LLA]) and output the nearest altitude in list.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
inputs(Sequence[tuple[LLA, list[LLA]]]): The LLA point to find the nearest
|
|
18
|
+
altitude to, and the list of LLA points to search for the nearest point.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
list of altitudes in the same order as points
|
|
22
|
+
"""
|
|
23
|
+
alts: list[float] = []
|
|
24
|
+
for lla, points in inputs:
|
|
25
|
+
dist = [lla.dist_surface_simple_m(p) for p in points]
|
|
26
|
+
idx = np.argmin(dist)
|
|
27
|
+
alts.append(points[idx].alt_m)
|
|
28
|
+
return alts
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# ruff: noqa: T201
|
|
2
|
+
"""Test methods to compare various convolution methods. Not for production use."""
|
|
3
|
+
|
|
4
|
+
# NOTE: Test functions ... this should not be used. Use convolve instead.
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from gri_ell import Ell
|
|
10
|
+
from gri_pos import Pos
|
|
11
|
+
from gri_utils import conversion
|
|
12
|
+
from scipy.linalg import block_diag
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Generator, Sequence
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def atwa_convolve(ells: Sequence[Ell] | Generator[Ell]) -> Ell:
|
|
19
|
+
"""Compare various convolve types.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
ells(Sequence[Ell]): A set of ell objects. Uses xyz and info.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Ell
|
|
26
|
+
"""
|
|
27
|
+
np.set_printoptions(suppress=True, precision=5)
|
|
28
|
+
ell = _position(ells)
|
|
29
|
+
print("\nATWAI_ENU")
|
|
30
|
+
# print(np.array(ell.cov.semi_axis_vec))
|
|
31
|
+
print(ell.cov)
|
|
32
|
+
print(ell.ellipse)
|
|
33
|
+
|
|
34
|
+
ell2 = _std_cov(ell, ells)
|
|
35
|
+
print("\nSTD COV")
|
|
36
|
+
# print(np.array(ell2.cov.semi_axis_vec))
|
|
37
|
+
print(ell2.cov)
|
|
38
|
+
print(ell2.ellipse)
|
|
39
|
+
|
|
40
|
+
ell3 = _bart_cov(ell, ells)
|
|
41
|
+
print("\nBART COV")
|
|
42
|
+
# print(np.array(ell2.cov.semi_axis_vec))
|
|
43
|
+
print(ell3.cov)
|
|
44
|
+
print(ell3.ellipse)
|
|
45
|
+
|
|
46
|
+
ell4 = _bart_cov2(ell, ells)
|
|
47
|
+
print("\nBART COV 2")
|
|
48
|
+
# print(np.array(ell2.cov.semi_axis_vec))
|
|
49
|
+
print(ell4.cov)
|
|
50
|
+
print(ell4.ellipse)
|
|
51
|
+
|
|
52
|
+
return ell3
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _position(ells: Sequence[Ell] | Generator[Ell]) -> Ell:
|
|
56
|
+
"""Get the position of the convolved point with direct covariance."""
|
|
57
|
+
# 3Nx3 identity stack
|
|
58
|
+
a = np.vstack([np.identity(3) for _ in ells])
|
|
59
|
+
# 3Nx3N diagonalized information matrices
|
|
60
|
+
w = block_diag(*(ell.info for ell in ells))
|
|
61
|
+
# 3Nx1 xyz positions
|
|
62
|
+
y = np.concatenate([e.xyz for e in ells])
|
|
63
|
+
atw = a.T @ w # 3x3N
|
|
64
|
+
atwa = atw @ a # 3x3
|
|
65
|
+
atwy = atw @ y # 3x1
|
|
66
|
+
atwai = np.linalg.inv(atwa) # 3x3
|
|
67
|
+
xyz = atwai @ atwy # 3x1
|
|
68
|
+
atwai = conversion.xyz_to_enu_cov(xyz, atwai)
|
|
69
|
+
return Ell.COV(Pos.XYZ(xyz), atwai)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _std_cov(pos: Pos, ellipsoids: Sequence[Ell] | Generator[Ell]) -> Ell:
|
|
73
|
+
"""Get the covariance the standard way."""
|
|
74
|
+
norm = 0
|
|
75
|
+
s = np.zeros((3, 3), dtype=float)
|
|
76
|
+
s_model = np.zeros((3, 3), dtype=float)
|
|
77
|
+
n = 0
|
|
78
|
+
for e in ellipsoids:
|
|
79
|
+
n += 1
|
|
80
|
+
w = e.info # xyz, 1/m^2, 1 sigma
|
|
81
|
+
s_model += w
|
|
82
|
+
del_xyz = pos.delta_xyz(e)
|
|
83
|
+
norm += float(del_xyz @ w @ del_xyz)
|
|
84
|
+
s += np.outer(del_xyz, del_xyz)
|
|
85
|
+
s_model = np.linalg.inv(s_model)
|
|
86
|
+
x_2 = norm
|
|
87
|
+
s_mod_inflated = (x_2 + 1) / n * s_model
|
|
88
|
+
s_sample = s / n**2
|
|
89
|
+
cov = s_mod_inflated + s_sample
|
|
90
|
+
cov = conversion.xyz_to_enu_cov(pos.xyz, cov)
|
|
91
|
+
return Ell.COV(pos, cov)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _bart_cov(pos: Pos, ellipsoids: Sequence[Ell] | Generator[Ell]) -> Ell:
|
|
95
|
+
"""Get the sample_smi variation of covariance."""
|
|
96
|
+
norm = 0
|
|
97
|
+
s = np.zeros((3, 3), dtype=float)
|
|
98
|
+
s_model = np.zeros((3, 3), dtype=float)
|
|
99
|
+
n = 0
|
|
100
|
+
for e in ellipsoids:
|
|
101
|
+
n += 1
|
|
102
|
+
w = e.info # xyz, 1/m^2, 1 sigma
|
|
103
|
+
s_model += w
|
|
104
|
+
del_xyz = pos.delta_xyz(e)
|
|
105
|
+
norm += float(del_xyz @ w @ del_xyz)
|
|
106
|
+
# Bart's correction is to s
|
|
107
|
+
theta = e.ellipse.ori_rad
|
|
108
|
+
del_n1 = (pos.lla.lat_deg - e.lla.lat_deg) * 60 * 1.852 * 1000
|
|
109
|
+
avg_lat = (pos.lla.lat_deg + e.lla.lat_deg) / 2
|
|
110
|
+
del_e1 = (
|
|
111
|
+
(pos.lla.lon_deg - e.lla.lon_deg)
|
|
112
|
+
* 60
|
|
113
|
+
* 1.852
|
|
114
|
+
* np.cos(np.radians(avg_lat))
|
|
115
|
+
* 1000
|
|
116
|
+
)
|
|
117
|
+
del_smi = del_e1 * np.cos(theta) - del_n1 * np.sin(theta)
|
|
118
|
+
del_e = del_smi * np.cos(theta)
|
|
119
|
+
del_n = -del_smi * np.sin(theta)
|
|
120
|
+
enu_vec = np.array((del_e, del_n, pos.lla.alt_m - e.lla.alt_m))
|
|
121
|
+
s += np.outer(enu_vec, enu_vec)
|
|
122
|
+
# End bart's correction
|
|
123
|
+
s_model = np.linalg.inv(s_model)
|
|
124
|
+
x_2 = norm
|
|
125
|
+
s_mod_inflated = (x_2 + 1) / n * s_model
|
|
126
|
+
s_mod_inflated = conversion.xyz_to_enu_cov(pos.xyz, s_mod_inflated)
|
|
127
|
+
s_sample = s / n**2
|
|
128
|
+
cov = s_mod_inflated + s_sample
|
|
129
|
+
return Ell.COV(pos, cov)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _bart_cov2(pos: Pos, ellipsoids: Sequence[Ell] | Generator[Ell]) -> Ell:
|
|
133
|
+
"""Original implementations."""
|
|
134
|
+
# s_model_inflated section
|
|
135
|
+
n = len(list(ellipsoids))
|
|
136
|
+
# chi squared
|
|
137
|
+
chi_squared = 0
|
|
138
|
+
for e in ellipsoids:
|
|
139
|
+
w = e.info # xyz, 1/m^2, 95%
|
|
140
|
+
del_xyz = pos.delta_xyz(e)
|
|
141
|
+
chi_squared += float(del_xyz @ w @ del_xyz)
|
|
142
|
+
|
|
143
|
+
# inflation factor
|
|
144
|
+
inflation_factor = (chi_squared + 1) / n
|
|
145
|
+
|
|
146
|
+
# s model
|
|
147
|
+
s_model = np.zeros((3, 3))
|
|
148
|
+
for e in ellipsoids:
|
|
149
|
+
s_model += e.info
|
|
150
|
+
s_model = np.linalg.inv(s_model)
|
|
151
|
+
|
|
152
|
+
# inflated model
|
|
153
|
+
s_model_inflated = inflation_factor * s_model
|
|
154
|
+
|
|
155
|
+
# rotate from ecef to enu
|
|
156
|
+
s_model_inflated = conversion.xyz_to_enu_cov(pos.xyz, s_model_inflated)
|
|
157
|
+
|
|
158
|
+
# s_sample_minor
|
|
159
|
+
delta_enus = []
|
|
160
|
+
for e in ellipsoids:
|
|
161
|
+
orient_rad = e.ellipse.ori_rad
|
|
162
|
+
|
|
163
|
+
delta_north = 1.852 * 60 * (pos.lla.lat_deg - e.lla.lat_deg) * 1000
|
|
164
|
+
delta_east = (
|
|
165
|
+
1.852
|
|
166
|
+
* 60
|
|
167
|
+
* (pos.lla.lon_deg - e.lla.lon_deg)
|
|
168
|
+
* np.cos(np.radians((pos.lla.lat_deg + e.lla.lat_deg) / 2))
|
|
169
|
+
* 1000
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
delta_smi = delta_east * np.cos(orient_rad) - delta_north * np.sin(orient_rad)
|
|
173
|
+
|
|
174
|
+
delta_e = delta_smi * np.cos(orient_rad)
|
|
175
|
+
delta_n = -delta_smi * np.sin(orient_rad)
|
|
176
|
+
delta_up = pos.lla.alt_m - e.lla.alt_m
|
|
177
|
+
|
|
178
|
+
delta_enus.append(np.array([[delta_e], [delta_n], [delta_up]]))
|
|
179
|
+
|
|
180
|
+
s_sample_minor = 0
|
|
181
|
+
for enu in delta_enus:
|
|
182
|
+
s_sample_minor += enu @ enu.T
|
|
183
|
+
s_sample_minor /= n**2
|
|
184
|
+
|
|
185
|
+
cov = s_model_inflated + s_sample_minor
|
|
186
|
+
return Ell.COV(pos, cov)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Find multiple convolved points with outlier detection."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from gri_ell import Ell
|
|
7
|
+
from gri_pos import Pos
|
|
8
|
+
|
|
9
|
+
from .smart_convolve import smart_convolve
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable, Generator, Sequence
|
|
13
|
+
|
|
14
|
+
from gri_pos import LLA
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cluster_convolve( # noqa: PLR0913
|
|
18
|
+
ellipsoids: Sequence[Ell] | Generator[Ell],
|
|
19
|
+
*,
|
|
20
|
+
max_norm: float = 2,
|
|
21
|
+
min_pts: int = 3,
|
|
22
|
+
max_pts: int | None = None,
|
|
23
|
+
min_sma_m: float = 0,
|
|
24
|
+
max_ori_spread: bool = True,
|
|
25
|
+
alt_post_process: Callable[[Sequence[tuple[LLA, list[LLA]]]], Sequence[float]]
|
|
26
|
+
| None = None,
|
|
27
|
+
alt_post_process_fudge_m: float = 0,
|
|
28
|
+
# Can give a function that does the spline in utils, can take nearest, nearest+int,
|
|
29
|
+
# spline, spline+int # spline would need scipy in utils ... do we want that? Probs.
|
|
30
|
+
# alt can be handled by:
|
|
31
|
+
# dted: https://pypi.org/project/dted/
|
|
32
|
+
# elevation: https://pypi.org/project/elevation/
|
|
33
|
+
# google: https://developers.google.com/maps/documentation/javascript/elevation
|
|
34
|
+
# https://airsci.com/google-maps-elevation-api-python-example/
|
|
35
|
+
) -> tuple[list[Ell], list[list[int]], list[int]]:
|
|
36
|
+
"""Find the convolved point location from all the points.
|
|
37
|
+
|
|
38
|
+
Returns a list of Ell convolved locations, the list of Ells per position that were
|
|
39
|
+
used by that location (such that return[1][1] is the list of Ells in return[1]), and
|
|
40
|
+
a list of remaining / discarded Ells.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
ellipsoids(Sequence[Ell]|Generator[Ell]): A set of ell objects. Uses xyz and
|
|
44
|
+
info.
|
|
45
|
+
max_norm(float, optional): Maximum mahalanobis distance from convolved point to
|
|
46
|
+
allow. If an input it outside that range (input ell to point distance), add
|
|
47
|
+
it to the discard list and return it. Defaults to 2.0
|
|
48
|
+
min_pts(int, optional): Minimum allowed points in a convolved answer. Defaults
|
|
49
|
+
to 3
|
|
50
|
+
max_pts(int, optional): Maximum allowed points in a convolved answer. If there
|
|
51
|
+
are leftover points (eg: 5 points passed, 3 max and 3 min, 2 left over that
|
|
52
|
+
cannot be used), they will be added to discard. This takes more processing.
|
|
53
|
+
min_sma_m(float, optional): Do not allow final convolved sma to go below min
|
|
54
|
+
sma unless min_pts is reached. This will split up convolutions. If there are
|
|
55
|
+
leftover points that cannot be used, they will be added to discard. This
|
|
56
|
+
is iterative and takes a lot more processing.
|
|
57
|
+
max_ori_spread(bool, optional): If set to true, for any clusters that need to be
|
|
58
|
+
split up for min_sma or max_pts, first sort the data by orientation, then
|
|
59
|
+
split into quarters and shuffle so that maximal orientation differences are
|
|
60
|
+
seen by the convolver.
|
|
61
|
+
alt_post_process(Callable[[Sequence[tuple[LLA,Sequence[LLA]]]], Sequence[float]], optional):
|
|
62
|
+
A function that takes a list of (LLA,list[LLA]) where LLA is an np.ndarray
|
|
63
|
+
vector that is latitude degrees, longitude degrees, altitude meters. The
|
|
64
|
+
first element is the convolved location and the second element is the list
|
|
65
|
+
of positions (Ells) as LLAs that went into that location. Sequences are
|
|
66
|
+
required to enable batch processing by the function.
|
|
67
|
+
|
|
68
|
+
It should then output a list of altitudes in the same order as the input
|
|
69
|
+
list.
|
|
70
|
+
|
|
71
|
+
This package provides altitude.nearest, which replaces the Ell altitude with
|
|
72
|
+
the nearest Ell altitude (often a decent estimate).
|
|
73
|
+
alt_post_process_fudge_m(float, optional): Adds a set amount of meters to the
|
|
74
|
+
altitude after any other alt_post_process function.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
(list[Ell], list[list[used Ells indexes]], list[discarded Ells indexes])
|
|
78
|
+
""" # noqa: E501
|
|
79
|
+
# To keep track of the index, make it a list of tuple[idx,ell]
|
|
80
|
+
ells = [(idx, ell) for idx, ell in enumerate(ellipsoids)]
|
|
81
|
+
if max_ori_spread:
|
|
82
|
+
# sort by ori, split into quarters and shuffle
|
|
83
|
+
ells = _max_spread_ori(ells)
|
|
84
|
+
|
|
85
|
+
locations, loc_ells, discards = _first_pass(ells, max_norm, min_pts)
|
|
86
|
+
|
|
87
|
+
# Now we have the collections that should go together (roughly), let's post process
|
|
88
|
+
reprocess: list[int] = []
|
|
89
|
+
for idx, (loc, loc_ell) in enumerate(zip(locations, loc_ells, strict=False)):
|
|
90
|
+
if (
|
|
91
|
+
max_pts is not None and len(loc_ell) > max_pts
|
|
92
|
+
) or loc.ellipse.sma_95 < min_sma_m:
|
|
93
|
+
reprocess.append(idx)
|
|
94
|
+
for idx in reprocess:
|
|
95
|
+
new_locs, new_ells, new_discards = _reprocess(
|
|
96
|
+
[ells[i] for i in loc_ells[idx]],
|
|
97
|
+
max_norm,
|
|
98
|
+
min_pts,
|
|
99
|
+
max_pts,
|
|
100
|
+
min_sma_m,
|
|
101
|
+
max_ori_spread,
|
|
102
|
+
)
|
|
103
|
+
locations.extend(new_locs)
|
|
104
|
+
loc_ells.extend(new_ells)
|
|
105
|
+
discards.extend(new_discards)
|
|
106
|
+
for idx in reprocess[::-1]:
|
|
107
|
+
del locations[idx]
|
|
108
|
+
del loc_ells[idx]
|
|
109
|
+
|
|
110
|
+
# Adjust the altitude with a post process function if supplied and add any
|
|
111
|
+
# fudge altitude distances
|
|
112
|
+
if alt_post_process is not None or alt_post_process_fudge_m != 0:
|
|
113
|
+
locations = _fix_alts(
|
|
114
|
+
ells,
|
|
115
|
+
locations,
|
|
116
|
+
loc_ells,
|
|
117
|
+
alt_post_process,
|
|
118
|
+
alt_post_process_fudge_m,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Sort the index lists
|
|
122
|
+
discards.sort()
|
|
123
|
+
loc_ells = [sorted(idxs) for idxs in loc_ells]
|
|
124
|
+
# Now sort by min ell sma (smallest to largest)
|
|
125
|
+
if len(locations) > 0:
|
|
126
|
+
tups = sorted(
|
|
127
|
+
zip(locations, loc_ells, strict=True),
|
|
128
|
+
key=lambda x: x[0].ellipse.sma_95,
|
|
129
|
+
)
|
|
130
|
+
locations, loc_ells = zip(*tups, strict=True)
|
|
131
|
+
return list(locations), list(loc_ells), discards
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _max_spread_ori(ells: list[tuple[int, Ell]]) -> list[tuple[int, Ell]]:
|
|
135
|
+
"""Spread the data into quarters by ori."""
|
|
136
|
+
ells = sorted(ells, key=lambda x: x[1].ellipse.ori_deg)
|
|
137
|
+
a = np.array([e[0] for e in ells], dtype=int)
|
|
138
|
+
add = np.array([-1] * int(np.ceil(len(a) / 4) * 4 - len(a)), dtype=int)
|
|
139
|
+
a = np.append(a, add)
|
|
140
|
+
a = a.reshape((4, -1))
|
|
141
|
+
a = a.T.flatten()
|
|
142
|
+
a = a[a >= 0]
|
|
143
|
+
return [ells[idx] for idx in a]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _reprocess( # noqa: PLR0913
|
|
147
|
+
loc_ells: list[tuple[int, Ell]],
|
|
148
|
+
max_norm: float = 2,
|
|
149
|
+
min_pts: int = 3,
|
|
150
|
+
max_pts: int | None = None,
|
|
151
|
+
min_sma_m: float = 0,
|
|
152
|
+
max_ori_spread: bool = False, # noqa: FBT001, FBT002
|
|
153
|
+
) -> tuple[list[Ell], list[list[int]], list[int]]:
|
|
154
|
+
"""Find any indexes (loc,loc_ells) that we need to reprocess for max_pts or min_sma.
|
|
155
|
+
|
|
156
|
+
Reprocess them in place. Remove from lists, add to tail end with new answers.
|
|
157
|
+
"""
|
|
158
|
+
if max_ori_spread:
|
|
159
|
+
# respread
|
|
160
|
+
loc_ells = _max_spread_ori(loc_ells)
|
|
161
|
+
|
|
162
|
+
if max_pts is None:
|
|
163
|
+
max_pts = len(loc_ells)
|
|
164
|
+
|
|
165
|
+
start = 0
|
|
166
|
+
stop = max_pts
|
|
167
|
+
ells = [ell for _, ell in loc_ells]
|
|
168
|
+
new_locs: list[Ell] = []
|
|
169
|
+
new_loc_ells: list[list[int]] = []
|
|
170
|
+
new_discarded: list[int] = []
|
|
171
|
+
while stop - start >= min_pts:
|
|
172
|
+
ans = smart_convolve(
|
|
173
|
+
ells[start:stop],
|
|
174
|
+
max_norm=max_norm,
|
|
175
|
+
min_pts=min_pts,
|
|
176
|
+
)
|
|
177
|
+
if ans is None:
|
|
178
|
+
# odd case where we can't find an answer within max_pts but could with
|
|
179
|
+
# the whole number of ells. Skip this batch
|
|
180
|
+
start = stop
|
|
181
|
+
stop = min(start + max_pts + 1, len(ells))
|
|
182
|
+
new_discarded.extend(list(range(start, stop)))
|
|
183
|
+
continue
|
|
184
|
+
ell, used, discard = ans
|
|
185
|
+
if min_sma_m > 0 and ell.ellipse.sma_95 < min_sma_m and stop - start > min_pts:
|
|
186
|
+
stop -= 1
|
|
187
|
+
continue
|
|
188
|
+
# discards are unlikely in this case, just skip handling them
|
|
189
|
+
new_discarded.extend([loc_ells[d + start][0] for d in discard])
|
|
190
|
+
new_locs.append(ell)
|
|
191
|
+
new_loc_ells.append([loc_ells[u + start][0] for u in used])
|
|
192
|
+
start = stop
|
|
193
|
+
stop = min(start + max_pts, len(ells))
|
|
194
|
+
# Add any unused at the tail
|
|
195
|
+
new_discarded.extend(range(start, stop))
|
|
196
|
+
return new_locs, new_loc_ells, new_discarded
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _fix_alts(
|
|
200
|
+
ells: list[tuple[int, Ell]],
|
|
201
|
+
locations: list[Ell],
|
|
202
|
+
loc_ells: list[list[int]],
|
|
203
|
+
alt_post_process: Callable[[Sequence[tuple[LLA, list[LLA]]]], Sequence[float]]
|
|
204
|
+
| None = None,
|
|
205
|
+
alt_post_process_fudge_m: float = 0,
|
|
206
|
+
) -> list[Ell]:
|
|
207
|
+
"""Use the provided function to adjust altitudes, and add any fudge factor."""
|
|
208
|
+
# Get base alts
|
|
209
|
+
if alt_post_process is not None:
|
|
210
|
+
# Convert to LLAs
|
|
211
|
+
pos = [e.lla for e in locations]
|
|
212
|
+
pos_lists = [[ells[idx][1].lla for idx in idx_list] for idx_list in loc_ells]
|
|
213
|
+
# Get alts
|
|
214
|
+
alts = alt_post_process(list(zip(pos, pos_lists, strict=True)))
|
|
215
|
+
else:
|
|
216
|
+
# Get existing alts
|
|
217
|
+
alts = [e.lla.alt_m for e in locations]
|
|
218
|
+
# Replace alts
|
|
219
|
+
new_ell: list[Ell] = []
|
|
220
|
+
for alt, ell in zip(alts, locations, strict=True):
|
|
221
|
+
new_ell.append(
|
|
222
|
+
Ell.COV(
|
|
223
|
+
Pos.LLA(
|
|
224
|
+
ell.lla.lat_deg,
|
|
225
|
+
ell.lla.lon_deg,
|
|
226
|
+
alt + alt_post_process_fudge_m,
|
|
227
|
+
),
|
|
228
|
+
ell.cov,
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
return new_ell
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _first_pass(
|
|
235
|
+
ells: list[tuple[int, Ell]],
|
|
236
|
+
max_norm: float = 2,
|
|
237
|
+
min_pts: int = 3,
|
|
238
|
+
) -> tuple[list[Ell], list[list[int]], list[int]]:
|
|
239
|
+
"""First smart convolve pass.
|
|
240
|
+
|
|
241
|
+
Get all convolved locations to cluster data regardless of max size or min sma.
|
|
242
|
+
"""
|
|
243
|
+
locations: list[Ell] = []
|
|
244
|
+
loc_ells: list[list[int]] = []
|
|
245
|
+
# To keep track of the index, make it a list of tuple[idx,ell]
|
|
246
|
+
# First pass
|
|
247
|
+
while ans := smart_convolve(
|
|
248
|
+
[ell for _, ell in ells],
|
|
249
|
+
max_norm=max_norm,
|
|
250
|
+
min_pts=min_pts,
|
|
251
|
+
):
|
|
252
|
+
ell, used, discard = ans
|
|
253
|
+
# Add location
|
|
254
|
+
locations.append(ell)
|
|
255
|
+
# Add the original indexes to the location ell
|
|
256
|
+
loc_ells.append([e[0] for idx, e in enumerate(ells) if idx in used])
|
|
257
|
+
# redo ells list to be the remainint tuples of unused ells
|
|
258
|
+
ells = [e for idx, e in enumerate(ells) if idx in discard]
|
|
259
|
+
|
|
260
|
+
return locations, loc_ells, [e[0] for e in ells]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Convolution of all points.
|
|
2
|
+
|
|
3
|
+
Find the center of the points via convolution and find the error region via different
|
|
4
|
+
inflation methods.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from gri_ell import Ell
|
|
11
|
+
from gri_pos import Pos
|
|
12
|
+
from gri_utils import conversion
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convolve(
|
|
19
|
+
ellipsoids: Sequence[Ell],
|
|
20
|
+
*,
|
|
21
|
+
inflation: Literal["none", "std", "bart"] = "bart",
|
|
22
|
+
) -> Ell:
|
|
23
|
+
"""Find the convolved point location from all the points.
|
|
24
|
+
|
|
25
|
+
Does not throw away any points. Choose an inflation technique to estimate the error
|
|
26
|
+
region.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
ellipsoids: A set of ell objects. Uses xyz and info.
|
|
30
|
+
inflation: "none" performs strict covariance calculations by combining
|
|
31
|
+
information matrices. "std" inflates covariance based on the sample
|
|
32
|
+
distribution, "bart" inflates covariances based on SMI reduction rules.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Ell
|
|
36
|
+
"""
|
|
37
|
+
# Find the position and basic uninflated covariance
|
|
38
|
+
s_model = np.add.reduce([e.info for e in ellipsoids]) # xyz, 1/m^2, 1-sigma
|
|
39
|
+
s_var = np.add.reduce([e.info @ e.xyz for e in ellipsoids])
|
|
40
|
+
cov = np.linalg.inv(s_model)
|
|
41
|
+
pos = Pos.XYZ(cov @ s_var)
|
|
42
|
+
ell = Ell.COV(pos, cov)
|
|
43
|
+
if inflation == "none":
|
|
44
|
+
return ell
|
|
45
|
+
|
|
46
|
+
# Begin normalized and smi-normalized inflation of covariance
|
|
47
|
+
s_model = cov
|
|
48
|
+
n = len(ellipsoids)
|
|
49
|
+
x_2 = 0
|
|
50
|
+
s = np.zeros((3, 3), dtype=float)
|
|
51
|
+
for e in ellipsoids:
|
|
52
|
+
del_xyz = pos.delta_xyz(e)
|
|
53
|
+
x_2 += float(del_xyz @ e.info @ del_xyz)
|
|
54
|
+
if inflation == "std":
|
|
55
|
+
s += np.outer(del_xyz, del_xyz)
|
|
56
|
+
elif inflation == "bart":
|
|
57
|
+
theta = e.ellipse.ori_rad
|
|
58
|
+
|
|
59
|
+
delta_north = 1.852 * 60 * 1000 * (pos.lla.lat_deg - e.lla.lat_deg)
|
|
60
|
+
delta_east = (
|
|
61
|
+
1.852
|
|
62
|
+
* 60
|
|
63
|
+
* 1000
|
|
64
|
+
* (pos.lla.lon_deg - e.lla.lon_deg)
|
|
65
|
+
* np.cos(np.radians((pos.lla.lat_deg + e.lla.lat_deg) / 2))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
delta_smi = delta_east * np.cos(theta) - delta_north * np.sin(theta)
|
|
69
|
+
|
|
70
|
+
del_e = delta_smi * np.cos(theta)
|
|
71
|
+
del_n = -delta_smi * np.sin(theta)
|
|
72
|
+
del_u = pos.lla.alt_m - e.lla.alt_m
|
|
73
|
+
|
|
74
|
+
enu_vec = np.array((del_e, del_n, del_u))
|
|
75
|
+
|
|
76
|
+
s += np.outer(enu_vec, enu_vec)
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("Bad inflation setting")
|
|
79
|
+
s_sample = s / n**2
|
|
80
|
+
s_mod_inflated = (x_2 + 1) / n * s_model
|
|
81
|
+
# s_mod_inflated is in xyz
|
|
82
|
+
# std s_sample is in xyz, bart s_sample is in enu
|
|
83
|
+
if inflation == "std":
|
|
84
|
+
cov = conversion.xyz_to_enu_cov(pos.xyz, s_mod_inflated + s_sample)
|
|
85
|
+
else:
|
|
86
|
+
s_mod_inflated = conversion.xyz_to_enu_cov(pos.xyz, s_mod_inflated)
|
|
87
|
+
cov = s_mod_inflated + s_sample
|
|
88
|
+
return Ell.COV(pos, cov)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Find convolved point with outlier detection."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from gri_utils import constants
|
|
7
|
+
|
|
8
|
+
from .convolve import convolve
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Generator, Sequence
|
|
12
|
+
|
|
13
|
+
from gri_ell import Ell
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def smart_convolve(
|
|
17
|
+
ellipsoids: Sequence[Ell] | Generator[Ell],
|
|
18
|
+
*,
|
|
19
|
+
max_norm: float = 2,
|
|
20
|
+
min_pts: int = 3,
|
|
21
|
+
) -> None | tuple[Ell, list[int], list[int]]:
|
|
22
|
+
"""Find the convolved point location from all the points.
|
|
23
|
+
|
|
24
|
+
Returns an Ell (or None), the list of Ells (by index) used to make that
|
|
25
|
+
ellipsoid, and the remaining Ells (by index) that were excluded.
|
|
26
|
+
|
|
27
|
+
Useful to tell all the points that should go together (by norm) and remove the
|
|
28
|
+
points that do not belong.
|
|
29
|
+
|
|
30
|
+
Note that outlier convolve can (incorrectly) return no answer when an answer does
|
|
31
|
+
exist if the data is not pre-clustered decently. EG: a valid cluster of five points
|
|
32
|
+
and a invalid group of twenty points further away that does not convolve will
|
|
33
|
+
exclude the five points first by norm, then exclude each other. Roughly pre-cluster
|
|
34
|
+
your data before using this function.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
ellipsoids(Sequence[Ell]|Generator[Ell]): A set of ell objects. Uses xyz and
|
|
38
|
+
info.
|
|
39
|
+
max_norm(float, optional): Maximum ellipse norm distance from convolved point to
|
|
40
|
+
allow. If an input it outside that range (input ell to point distance), add
|
|
41
|
+
it to the discard list and return it. Defaults to 2.0
|
|
42
|
+
min_pts(int, optional): Minimum allowed points in a convolved answer. Defaults
|
|
43
|
+
to 3
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
tuple[Ell, list[used Ells idx], list[discarded Ells idx]] or None
|
|
47
|
+
"""
|
|
48
|
+
# To keep track of the index, make it a list of tuple[idx,ell]
|
|
49
|
+
ells = [(idx, ell) for idx, ell in enumerate(ellipsoids)]
|
|
50
|
+
# Verify length is minimal
|
|
51
|
+
if len(ells) < min_pts:
|
|
52
|
+
return None
|
|
53
|
+
unused: list[int] = []
|
|
54
|
+
# loop as long as we have enough ells until none are discarded or all are discarded.
|
|
55
|
+
while len(ells) >= min_pts:
|
|
56
|
+
ell, discard_idx = _test_norm([ell for _, ell in ells], max_norm)
|
|
57
|
+
if discard_idx is None:
|
|
58
|
+
# Answer found!
|
|
59
|
+
return ell, [idx for idx, _ in ells], unused
|
|
60
|
+
# Remove index from ells and add to unused
|
|
61
|
+
unused.append(ells[discard_idx][0])
|
|
62
|
+
del ells[discard_idx]
|
|
63
|
+
# Not enough left and no answer found
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _test_norm(ells: list[Ell], max_norm: float) -> tuple[Ell, int | None]:
|
|
68
|
+
"""Get a discarded index at max norm distance or None if all are contained."""
|
|
69
|
+
ell = convolve(ells, inflation="none")
|
|
70
|
+
norms = [e.dist_mahalanobis(ell) / constants.SIG_TO_95_3D**0.5 for e in ells]
|
|
71
|
+
idx = int(np.argmax(norms))
|
|
72
|
+
return ell, idx if norms[idx] > max_norm else None
|
gri_convolve/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gri-convolve
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Project-URL: repository, https://gitlab.com/geosol-foss/python/gri-convolve
|
|
5
|
+
Project-URL: homepage, https://geosolresearch.com
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Requires-Dist: gri-ell
|
|
10
|
+
Requires-Dist: gri-pos
|
|
11
|
+
Requires-Dist: gri-utils
|
|
12
|
+
Requires-Dist: numpy>=2.3.3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
[](https://geosolresearch.com)
|
|
16
|
+
|
|
17
|
+
# Convolve (Ellipsoid Fusion)
|
|
18
|
+
|
|
19
|
+
Ellipsoid convolution functions for combining geolocation estimates with outlier detection and multi-cluster support.
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
gri-convolve provides three functions of increasing sophistication for fusing collections of `Ell` (ellipsoid) objects into combined position estimates:
|
|
24
|
+
|
|
25
|
+
- **`convolve`** -- combine all input ellipsoids into a single fused result with no outlier rejection
|
|
26
|
+
- **`smart_convolve`** -- iteratively remove outliers by Mahalanobis distance before fusing
|
|
27
|
+
- **`cluster_convolve`** -- find multiple clusters within a dataset and fuse each independently
|
|
28
|
+
|
|
29
|
+
Each function operates on `Ell` objects from gri-ell, which pair a 3D position with a statistical covariance (or information matrix). The output is one or more fused `Ell` objects representing the combined position estimate and its uncertainty.
|
|
30
|
+
|
|
31
|
+
Requires Python 3.14+.
|
|
32
|
+
|
|
33
|
+
## Mathematical Background
|
|
34
|
+
|
|
35
|
+
**Information matrix fusion.** Given N ellipsoids, each with position `x_k` and information matrix `I_k` (the inverse of the covariance matrix, in XYZ coordinates, 1/m^2, 1-sigma), the fused position and information matrix are:
|
|
36
|
+
|
|
37
|
+
S = sum(I_k) (combined information matrix)
|
|
38
|
+
x = S^{-1} sum(I_k x_k) (fused position)
|
|
39
|
+
|
|
40
|
+
This is the maximum-likelihood estimator under Gaussian assumptions.
|
|
41
|
+
|
|
42
|
+
**Inflation methods.** The raw fusion above underestimates uncertainty when inputs are inconsistent. Three modes control how the output covariance is inflated:
|
|
43
|
+
|
|
44
|
+
- `"none"` -- strict information matrix combination (no inflation)
|
|
45
|
+
- `"std"` -- inflate by the sample scatter of input positions in XYZ
|
|
46
|
+
- `"bart"` -- inflate along the semi-major axis direction in ENU (recommended default)
|
|
47
|
+
|
|
48
|
+
**Outlier detection.** `smart_convolve` computes the Mahalanobis distance from each input to the fused point:
|
|
49
|
+
|
|
50
|
+
d_M = sqrt((x - mu)^T I (x - mu))
|
|
51
|
+
|
|
52
|
+
where `mu` is the fused position and `I` is its information matrix. Distances are normalized to 95% confidence scale. Inputs exceeding `max_norm` are iteratively removed, worst first.
|
|
53
|
+
|
|
54
|
+
Reference: Mahalanobis, P.C. (1936). "On the generalized distance in statistics."
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install gri-convolve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://gitlab.com/geosol-foss/python/gri-convolve.git
|
|
66
|
+
cd gri-convolve
|
|
67
|
+
. .init_venv.sh
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from gri_convolve import convolve, smart_convolve, cluster_convolve
|
|
74
|
+
from gri_ell import Ell
|
|
75
|
+
from gri_pos import Pos
|
|
76
|
+
import numpy as np
|
|
77
|
+
|
|
78
|
+
# Create some ellipsoids at nearby positions
|
|
79
|
+
e1 = Ell.from_2d(Pos.LLA(40.0, -105.0, 1600), 100, 50, 45)
|
|
80
|
+
e2 = Ell.from_2d(Pos.LLA(40.001, -105.001, 1610), 120, 60, 30)
|
|
81
|
+
e3 = Ell.from_2d(Pos.LLA(40.0005, -104.999, 1605), 90, 45, 50)
|
|
82
|
+
|
|
83
|
+
# Simple fusion
|
|
84
|
+
fused = convolve([e1, e2, e3])
|
|
85
|
+
print(fused.lla) # Fused position
|
|
86
|
+
print(fused.ellipse.sma_95) # Fused semi-major axis (95%, meters)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## `convolve()`
|
|
90
|
+
|
|
91
|
+
Fuses all input ellipsoids into a single result. No outlier detection.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
fused = convolve(ellipsoids, inflation="bart")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
|
|
99
|
+
- `ellipsoids` -- sequence of `Ell` objects
|
|
100
|
+
- `inflation` -- `"none"`, `"std"`, or `"bart"` (default: `"bart"`)
|
|
101
|
+
|
|
102
|
+
**Returns:** A single fused `Ell`.
|
|
103
|
+
|
|
104
|
+
## `smart_convolve()`
|
|
105
|
+
|
|
106
|
+
Fuses with iterative outlier rejection. Computes the fused point, finds the input with the largest normalized Mahalanobis distance, and removes it if it exceeds `max_norm`. Repeats until all remaining inputs are within tolerance or fewer than `min_pts` remain.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
result = smart_convolve(ellipsoids, max_norm=2.0, min_pts=3)
|
|
110
|
+
if result is not None:
|
|
111
|
+
fused_ell, used_indices, discarded_indices = result
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Parameters:**
|
|
115
|
+
|
|
116
|
+
- `ellipsoids` -- sequence of `Ell` objects
|
|
117
|
+
- `max_norm` -- maximum allowed normalized distance (default: 2.0)
|
|
118
|
+
- `min_pts` -- minimum inputs required for a valid result (default: 3)
|
|
119
|
+
|
|
120
|
+
**Returns:** `(Ell, list[int], list[int])` or `None` if no valid cluster is found.
|
|
121
|
+
|
|
122
|
+
Pre-cluster your data before calling `smart_convolve`. Without pre-clustering, a large group of scattered noise points can cause valid clusters to be discarded first.
|
|
123
|
+
|
|
124
|
+
## `cluster_convolve()`
|
|
125
|
+
|
|
126
|
+
Finds multiple clusters within a dataset by iteratively applying `smart_convolve`. After finding the largest valid cluster, the discarded points are passed back in to find additional clusters.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
locations, used_per_location, discarded = cluster_convolve(
|
|
130
|
+
ellipsoids,
|
|
131
|
+
max_norm=2.0,
|
|
132
|
+
min_pts=3,
|
|
133
|
+
max_pts=10,
|
|
134
|
+
min_sma_m=50.0,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for loc, indices in zip(locations, used_per_location):
|
|
138
|
+
print(f"Cluster at {loc.lla} using {len(indices)} inputs")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Parameters:**
|
|
142
|
+
|
|
143
|
+
- `ellipsoids` -- sequence of `Ell` objects
|
|
144
|
+
- `max_norm` -- maximum normalized Mahalanobis distance (default: 2.0)
|
|
145
|
+
- `min_pts` -- minimum inputs per cluster (default: 3)
|
|
146
|
+
- `max_pts` -- maximum inputs per cluster; splits larger groups (default: None)
|
|
147
|
+
- `min_sma_m` -- minimum semi-major axis for output ellipsoids in meters (default: 0)
|
|
148
|
+
- `max_ori_spread` -- sort by orientation before splitting for diversity (default: True)
|
|
149
|
+
- `alt_post_process` -- callback for altitude correction (e.g., snap to terrain)
|
|
150
|
+
|
|
151
|
+
**Returns:** `(list[Ell], list[list[int]], list[int])`
|
|
152
|
+
|
|
153
|
+
## Units and Conventions
|
|
154
|
+
|
|
155
|
+
- Positions are in ECEF XYZ (meters) internally
|
|
156
|
+
- Information matrices are in XYZ, 1/m^2, 1-sigma
|
|
157
|
+
- Covariance matrices are in ENU, m^2, 1-sigma
|
|
158
|
+
- Output ellipse parameters (SMA, SMI, orientation) are at 95% confidence
|
|
159
|
+
- Mahalanobis distances are normalized to 95% scale for `max_norm` comparisons
|
|
160
|
+
|
|
161
|
+
## Dependencies
|
|
162
|
+
|
|
163
|
+
- **gri-ell**: Ellipsoid objects with position and covariance
|
|
164
|
+
- **gri-pos**: Position objects (XYZ, LLA coordinates)
|
|
165
|
+
- **gri-utils**: Coordinate conversions and constants
|
|
166
|
+
- **numpy**: Array operations
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## Other Projects
|
|
170
|
+
|
|
171
|
+
Current list of other [GRI FOSS Projects](.docs_other_projects.md) we are building and maintaining.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
gri_convolve/__init__.py,sha256=UAP0TyrpUN1IvpcIikEv_pgp8nPs6QQhsEBz_KVQ5-I,175
|
|
2
|
+
gri_convolve/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
gri_convolve/altitude/__init__.py,sha256=la-somGa2CQ2jv0D13fMFuQVIeWrBA-TdQVebCxpejY,107
|
|
4
|
+
gri_convolve/altitude/nearest.py,sha256=JX1kU0rS0kGAw-euofynnyDsDjTPIi-ji67qOrtd6Co,820
|
|
5
|
+
gri_convolve/convolve/__init__.py,sha256=zDdlA3mhvg0CeylrN3hec1f-RhQ5Ugr7ao5lkA76hGo,242
|
|
6
|
+
gri_convolve/convolve/atwa_convolve.py,sha256=wm_VpJkWL05yPt3Jl3vTIR--TL55OjinQ19m7PEcA_Q,5563
|
|
7
|
+
gri_convolve/convolve/cluster_convolve.py,sha256=kfHgLYCrN00ze52FeWz6QGPTqLt947n_HssgLjVPbcE,10142
|
|
8
|
+
gri_convolve/convolve/convolve.py,sha256=homB2MrfGabMslD6qbP93miK9NMU4eaKisyLbeRC7MQ,2851
|
|
9
|
+
gri_convolve/convolve/smart_convolve.py,sha256=k5MAgXL6aLoFisl2oIR5E7qaHQ6w-uFt6NwxdYUDO4w,2809
|
|
10
|
+
gri_convolve-0.2.0.dist-info/METADATA,sha256=D3KxLJyZsUM_CxM4dDtOLl4KrPi6Yofzp6jN3JFQhKA,6382
|
|
11
|
+
gri_convolve-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
gri_convolve-0.2.0.dist-info/licenses/LICENSE,sha256=Noh51pACFQty7ATtDK2D_6HNrc_UIcsyCFYJY0I9zEM,1077
|
|
13
|
+
gri_convolve-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 GeoSol Research Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|