gtrack 0.3.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.
- gtrack/__init__.py +137 -0
- gtrack/boundaries.py +396 -0
- gtrack/config.py +202 -0
- gtrack/geometry.py +348 -0
- gtrack/hpc_integration.py +851 -0
- gtrack/initial_conditions.py +255 -0
- gtrack/io_formats.py +477 -0
- gtrack/logging.py +193 -0
- gtrack/mesh.py +101 -0
- gtrack/mor_seeds.py +390 -0
- gtrack/point_rotation.py +836 -0
- gtrack/polygon_filter.py +223 -0
- gtrack/spatial.py +397 -0
- gtrack-0.3.0.dist-info/METADATA +66 -0
- gtrack-0.3.0.dist-info/RECORD +17 -0
- gtrack-0.3.0.dist-info/WHEEL +5 -0
- gtrack-0.3.0.dist-info/top_level.txt +1 -0
gtrack/config.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Configuration classes for gtrack package."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TracerConfig:
|
|
9
|
+
"""
|
|
10
|
+
Configuration parameters for seafloor age tracking.
|
|
11
|
+
|
|
12
|
+
Parameters match GPlately's SeafloorGrid for compatibility.
|
|
13
|
+
|
|
14
|
+
Attributes
|
|
15
|
+
----------
|
|
16
|
+
time_step : float
|
|
17
|
+
Time step size in Myr (default: 1.0)
|
|
18
|
+
earth_radius : float
|
|
19
|
+
Earth's radius in meters (default: 6.3781e6)
|
|
20
|
+
|
|
21
|
+
Collision Detection (C++ backend)
|
|
22
|
+
----------------------------------
|
|
23
|
+
velocity_delta_threshold : float
|
|
24
|
+
Minimum velocity difference to trigger collision check, in km/Myr.
|
|
25
|
+
Converted to cm/yr (divide by 10) for pygplates C++ API.
|
|
26
|
+
Default: 7.0 km/Myr (= 0.7 cm/yr)
|
|
27
|
+
distance_threshold_per_myr : float
|
|
28
|
+
Base distance threshold for collision detection, in km/Myr.
|
|
29
|
+
Default: 10.0 km/Myr
|
|
30
|
+
|
|
31
|
+
Initialization
|
|
32
|
+
--------------
|
|
33
|
+
default_mesh_points : int
|
|
34
|
+
Number of points for the initial sphere mesh.
|
|
35
|
+
Default: 10000
|
|
36
|
+
initial_ocean_mean_spreading_rate : float
|
|
37
|
+
Mean spreading rate in mm/yr for initial age calculation.
|
|
38
|
+
Used to compute age = distance / (rate / 2).
|
|
39
|
+
Default: 75.0 mm/yr (GPlately default)
|
|
40
|
+
|
|
41
|
+
MOR Seed Generation
|
|
42
|
+
-------------------
|
|
43
|
+
ridge_sampling_degrees : float
|
|
44
|
+
Ridge tessellation resolution in degrees (~50 km at equator).
|
|
45
|
+
Default: 0.5 degrees
|
|
46
|
+
spreading_offset_degrees : float
|
|
47
|
+
Angle to rotate new points off ridge, in degrees.
|
|
48
|
+
Default: 0.01 degrees (~1 km)
|
|
49
|
+
|
|
50
|
+
Continental Handling
|
|
51
|
+
--------------------
|
|
52
|
+
continental_cache_size : int
|
|
53
|
+
Number of timesteps to cache for continental polygon queries.
|
|
54
|
+
Default: 10
|
|
55
|
+
|
|
56
|
+
Reinitialization
|
|
57
|
+
----------------
|
|
58
|
+
reinit_k_neighbors : int
|
|
59
|
+
Number of nearest neighbors for interpolation during reinitialization.
|
|
60
|
+
Default: 5
|
|
61
|
+
reinit_max_distance : float
|
|
62
|
+
Maximum distance (meters) for valid interpolation neighbors.
|
|
63
|
+
Default: 500e3 (500 km)
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> # Use default configuration (GPlately-compatible)
|
|
68
|
+
>>> config = TracerConfig()
|
|
69
|
+
>>>
|
|
70
|
+
>>> # Custom configuration with higher resolution
|
|
71
|
+
>>> config = TracerConfig(
|
|
72
|
+
... default_mesh_points=40000, # Higher resolution mesh
|
|
73
|
+
... ridge_sampling_degrees=0.25, # ~25 km resolution
|
|
74
|
+
... time_step=0.5 # 0.5 Myr timesteps
|
|
75
|
+
... )
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Time stepping
|
|
79
|
+
time_step: float = 1.0 # Myr
|
|
80
|
+
earth_radius: float = 6.3781e6 # meters
|
|
81
|
+
|
|
82
|
+
# Collision detection (C++ backend - GPlately compatible)
|
|
83
|
+
# These are passed to pygplates.ReconstructedGeometryTimeSpan.DefaultDeactivatePoints
|
|
84
|
+
velocity_delta_threshold: float = 7.0 # km/Myr (converted to 0.7 cm/yr for API)
|
|
85
|
+
distance_threshold_per_myr: float = 10.0 # km/Myr
|
|
86
|
+
|
|
87
|
+
# Initialization - sphere mesh
|
|
88
|
+
default_mesh_points: int = 10000
|
|
89
|
+
initial_ocean_mean_spreading_rate: float = 75.0 # mm/yr (GPlately default)
|
|
90
|
+
|
|
91
|
+
# MOR seed generation
|
|
92
|
+
ridge_sampling_degrees: float = 0.5 # ~50 km at equator
|
|
93
|
+
spreading_offset_degrees: float = 0.01 # ~1 km offset from ridge
|
|
94
|
+
|
|
95
|
+
# Continental polygon caching
|
|
96
|
+
continental_cache_size: int = 10
|
|
97
|
+
|
|
98
|
+
# Reinitialization parameters
|
|
99
|
+
reinit_k_neighbors: int = 5
|
|
100
|
+
reinit_max_distance: float = 500e3 # meters
|
|
101
|
+
|
|
102
|
+
def __post_init__(self):
|
|
103
|
+
"""Validate configuration parameters."""
|
|
104
|
+
if self.time_step <= 0:
|
|
105
|
+
raise ValueError(f"time_step must be positive, got {self.time_step}")
|
|
106
|
+
if self.earth_radius <= 0:
|
|
107
|
+
raise ValueError(f"earth_radius must be positive, got {self.earth_radius}")
|
|
108
|
+
if self.velocity_delta_threshold < 0:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"velocity_delta_threshold must be non-negative, "
|
|
111
|
+
f"got {self.velocity_delta_threshold}"
|
|
112
|
+
)
|
|
113
|
+
if self.distance_threshold_per_myr < 0:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"distance_threshold_per_myr must be non-negative, "
|
|
116
|
+
f"got {self.distance_threshold_per_myr}"
|
|
117
|
+
)
|
|
118
|
+
if self.default_mesh_points < 1:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"default_mesh_points must be at least 1, "
|
|
121
|
+
f"got {self.default_mesh_points}"
|
|
122
|
+
)
|
|
123
|
+
if self.initial_ocean_mean_spreading_rate <= 0:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"initial_ocean_mean_spreading_rate must be positive, "
|
|
126
|
+
f"got {self.initial_ocean_mean_spreading_rate}"
|
|
127
|
+
)
|
|
128
|
+
if self.ridge_sampling_degrees <= 0:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"ridge_sampling_degrees must be positive, "
|
|
131
|
+
f"got {self.ridge_sampling_degrees}"
|
|
132
|
+
)
|
|
133
|
+
if self.spreading_offset_degrees <= 0:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"spreading_offset_degrees must be positive, "
|
|
136
|
+
f"got {self.spreading_offset_degrees}"
|
|
137
|
+
)
|
|
138
|
+
if self.continental_cache_size < 0:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"continental_cache_size must be non-negative, "
|
|
141
|
+
f"got {self.continental_cache_size}"
|
|
142
|
+
)
|
|
143
|
+
if self.reinit_k_neighbors < 1:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"reinit_k_neighbors must be at least 1, "
|
|
146
|
+
f"got {self.reinit_k_neighbors}"
|
|
147
|
+
)
|
|
148
|
+
if self.reinit_max_distance <= 0:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"reinit_max_distance must be positive, "
|
|
151
|
+
f"got {self.reinit_max_distance}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def velocity_delta_threshold_cm_yr(self) -> float:
|
|
156
|
+
"""
|
|
157
|
+
Velocity threshold in cm/yr for pygplates C++ API.
|
|
158
|
+
|
|
159
|
+
The C++ API expects cm/yr, while we store km/Myr for readability.
|
|
160
|
+
Conversion: 1 km/Myr = 0.1 cm/yr
|
|
161
|
+
"""
|
|
162
|
+
return self.velocity_delta_threshold / 10.0
|
|
163
|
+
|
|
164
|
+
def to_dict(self) -> dict:
|
|
165
|
+
"""
|
|
166
|
+
Convert configuration to dictionary.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
dict
|
|
171
|
+
Configuration as dictionary
|
|
172
|
+
"""
|
|
173
|
+
return {
|
|
174
|
+
'time_step': self.time_step,
|
|
175
|
+
'earth_radius': self.earth_radius,
|
|
176
|
+
'velocity_delta_threshold': self.velocity_delta_threshold,
|
|
177
|
+
'distance_threshold_per_myr': self.distance_threshold_per_myr,
|
|
178
|
+
'default_mesh_points': self.default_mesh_points,
|
|
179
|
+
'initial_ocean_mean_spreading_rate': self.initial_ocean_mean_spreading_rate,
|
|
180
|
+
'ridge_sampling_degrees': self.ridge_sampling_degrees,
|
|
181
|
+
'spreading_offset_degrees': self.spreading_offset_degrees,
|
|
182
|
+
'continental_cache_size': self.continental_cache_size,
|
|
183
|
+
'reinit_k_neighbors': self.reinit_k_neighbors,
|
|
184
|
+
'reinit_max_distance': self.reinit_max_distance,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_dict(cls, config_dict: dict):
|
|
189
|
+
"""
|
|
190
|
+
Create configuration from dictionary.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
config_dict : dict
|
|
195
|
+
Dictionary with configuration parameters
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
TracerConfig
|
|
200
|
+
Configuration object
|
|
201
|
+
"""
|
|
202
|
+
return cls(**config_dict)
|
gtrack/geometry.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometric utility functions for coordinate transformations and operations on a sphere.
|
|
3
|
+
|
|
4
|
+
All functions are vectorized using numpy for performance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Earth's radius in meters
|
|
13
|
+
EARTH_RADIUS = 6.3781e6
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_list(x):
|
|
17
|
+
"""
|
|
18
|
+
Ensure x is a list. Wrap single items (str, Path) in a list.
|
|
19
|
+
|
|
20
|
+
This is useful for APIs that accept either a single file path or a list
|
|
21
|
+
of file paths, allowing users to write either:
|
|
22
|
+
- rotation_files="rotations.rot"
|
|
23
|
+
- rotation_files=["rot1.rot", "rot2.rot"]
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
x : str, Path, list, or None
|
|
28
|
+
Input to convert to a list.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
list
|
|
33
|
+
The input as a list. Single str/Path items are wrapped in a list.
|
|
34
|
+
None returns an empty list.
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
>>> ensure_list("file.rot")
|
|
39
|
+
['file.rot']
|
|
40
|
+
>>> ensure_list(Path("file.rot"))
|
|
41
|
+
[PosixPath('file.rot')]
|
|
42
|
+
>>> ensure_list(["file1.rot", "file2.rot"])
|
|
43
|
+
['file1.rot', 'file2.rot']
|
|
44
|
+
>>> ensure_list(None)
|
|
45
|
+
[]
|
|
46
|
+
"""
|
|
47
|
+
if x is None:
|
|
48
|
+
return []
|
|
49
|
+
if isinstance(x, (str, Path)):
|
|
50
|
+
return [x]
|
|
51
|
+
return list(x)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def LatLon2XYZ(latlon):
|
|
55
|
+
"""
|
|
56
|
+
Convert geographic coordinates (lat, lon) to Cartesian coordinates (x, y, z).
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
latlon : np.ndarray
|
|
61
|
+
Array of shape (N, 2) containing [latitude, longitude] pairs in degrees.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
xyz : np.ndarray
|
|
66
|
+
Array of shape (N, 3) containing [x, y, z] Cartesian coordinates in meters.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> latlons = np.array([[0, 0], [90, 0], [-90, 0]])
|
|
71
|
+
>>> xyz = LatLon2XYZ(latlons)
|
|
72
|
+
>>> xyz.shape
|
|
73
|
+
(3, 3)
|
|
74
|
+
"""
|
|
75
|
+
R = EARTH_RADIUS
|
|
76
|
+
lat = latlon[:, 0]
|
|
77
|
+
lon = latlon[:, 1]
|
|
78
|
+
lon = np.deg2rad(lon)
|
|
79
|
+
lat = np.deg2rad(lat)
|
|
80
|
+
x = np.zeros([len(lon), 3]) # initialize coordinate-array
|
|
81
|
+
x[:, 0] = R * np.cos(lat) * np.cos(lon) # load x-coordinates
|
|
82
|
+
x[:, 1] = R * np.cos(lat) * np.sin(lon) # load y-coordinates
|
|
83
|
+
x[:, 2] = R * np.sin(lat) # load z-coordinates
|
|
84
|
+
return x
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def XYZ2LatLon(xyz):
|
|
88
|
+
"""
|
|
89
|
+
Convert Cartesian coordinates (x, y, z) to geographic coordinates (lat, lon).
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
xyz : np.ndarray
|
|
94
|
+
Array of shape (N, 3) containing [x, y, z] Cartesian coordinates in meters.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
lats : np.ndarray
|
|
99
|
+
Array of latitudes in degrees, shape (N,).
|
|
100
|
+
lons : np.ndarray
|
|
101
|
+
Array of longitudes in degrees, shape (N,).
|
|
102
|
+
|
|
103
|
+
Examples
|
|
104
|
+
--------
|
|
105
|
+
>>> xyz = np.array([[6.3781e6, 0, 0], [0, 0, 6.3781e6]])
|
|
106
|
+
>>> lats, lons = XYZ2LatLon(xyz)
|
|
107
|
+
>>> np.allclose(lats, [0, 90])
|
|
108
|
+
True
|
|
109
|
+
>>> np.allclose(lons, [0, 0])
|
|
110
|
+
True
|
|
111
|
+
"""
|
|
112
|
+
R = EARTH_RADIUS
|
|
113
|
+
x = xyz[:, 0]
|
|
114
|
+
y = xyz[:, 1]
|
|
115
|
+
z = xyz[:, 2]
|
|
116
|
+
lats = np.arcsin(z / R) * 360 / (2 * np.pi)
|
|
117
|
+
lons = np.arctan2(y, x) * 360 / (2 * np.pi)
|
|
118
|
+
return lats, lons
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def RefineGreatCircleArcSegment(p1, p2, N):
|
|
122
|
+
"""
|
|
123
|
+
Refine a great circle arc segment defined by two points into N segments.
|
|
124
|
+
|
|
125
|
+
This function takes two points on a sphere and creates a refined path
|
|
126
|
+
between them by interpolating along the great circle arc.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
p1 : tuple or list
|
|
131
|
+
First point as (latitude, longitude) in degrees.
|
|
132
|
+
p2 : tuple or list
|
|
133
|
+
Second point as (latitude, longitude) in degrees.
|
|
134
|
+
N : int
|
|
135
|
+
Number of segments to create (N+1 points total).
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
lats : np.ndarray
|
|
140
|
+
Array of latitudes along the refined segment, shape (N+1,).
|
|
141
|
+
lons : np.ndarray
|
|
142
|
+
Array of longitudes along the refined segment, shape (N+1,).
|
|
143
|
+
|
|
144
|
+
Notes
|
|
145
|
+
-----
|
|
146
|
+
Two points that define a line going through Earth's center will give
|
|
147
|
+
erroneous results.
|
|
148
|
+
|
|
149
|
+
Examples
|
|
150
|
+
--------
|
|
151
|
+
>>> p1 = (0, 0) # Equator at prime meridian
|
|
152
|
+
>>> p2 = (0, 90) # Equator at 90E
|
|
153
|
+
>>> lats, lons = RefineGreatCircleArcSegment(p1, p2, 2)
|
|
154
|
+
>>> len(lats)
|
|
155
|
+
3
|
|
156
|
+
"""
|
|
157
|
+
R = EARTH_RADIUS
|
|
158
|
+
|
|
159
|
+
LatLon = np.array([p1, p2])
|
|
160
|
+
XYZ = LatLon2XYZ(LatLon)
|
|
161
|
+
p1 = XYZ[0, :]
|
|
162
|
+
p2 = XYZ[1, :]
|
|
163
|
+
|
|
164
|
+
n = np.arange(N + 1) # weights
|
|
165
|
+
p = np.zeros([N + 1, 3]) #
|
|
166
|
+
for i in range(0, N + 1):
|
|
167
|
+
p[i, :] = (float(N) - n[i]) / N * p1 + (n[i] / float(N)) * p2
|
|
168
|
+
x_mag = np.sqrt(p[:, 0] ** 2 + p[:, 1] ** 2 + p[:, 2] ** 2) # compute vector magnitude
|
|
169
|
+
x_mag = np.transpose(np.array([x_mag, x_mag, x_mag]))
|
|
170
|
+
p = (p / x_mag) * R # project new position back to Earth surface
|
|
171
|
+
lats, lons = XYZ2LatLon(p)
|
|
172
|
+
return lats, lons
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def Segments2Points(segments, res):
|
|
176
|
+
"""
|
|
177
|
+
Convert great circle arc segments to a set of points with specified resolution.
|
|
178
|
+
|
|
179
|
+
Takes line segments defined by endpoint pairs and refines them into a set of
|
|
180
|
+
points spaced at approximately the specified resolution.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
segments : np.ndarray
|
|
185
|
+
Array of shape (N, 4) where each row is [lat1, lon1, lat2, lon2] defining
|
|
186
|
+
a segment's endpoints in degrees.
|
|
187
|
+
res : float
|
|
188
|
+
Target resolution for point spacing in meters.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
lats : np.ndarray
|
|
193
|
+
Array of latitudes for all refined points in degrees.
|
|
194
|
+
lons : np.ndarray
|
|
195
|
+
Array of longitudes for all refined points in degrees.
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
>>> segments = np.array([[0, 0, 0, 10], [0, 10, 0, 20]])
|
|
200
|
+
>>> lats, lons = Segments2Points(segments, 100e3) # 100 km resolution
|
|
201
|
+
>>> len(lats) > 2 # Should have more points than segments
|
|
202
|
+
True
|
|
203
|
+
"""
|
|
204
|
+
num_segments = len(segments[:, 0])
|
|
205
|
+
lats, lons = np.array([]), np.array([])
|
|
206
|
+
|
|
207
|
+
R = EARTH_RADIUS
|
|
208
|
+
# Fixed: Process all segments starting from index 0 (was bug in original starting at 1)
|
|
209
|
+
for i in range(0, num_segments):
|
|
210
|
+
p1, p2 = segments[i, :2], segments[i, 2:]
|
|
211
|
+
lat1, lon1, lat2, lon2 = np.deg2rad(p1[0]), np.deg2rad(p1[1]), np.deg2rad(p2[0]), np.deg2rad(p2[1])
|
|
212
|
+
dlon = abs(lon2 - lon1)
|
|
213
|
+
dist = R * np.arccos(np.sin(lat1) * np.sin(lat2) + np.cos(lat1) * np.cos(lat2) * np.cos(dlon))
|
|
214
|
+
|
|
215
|
+
if dist > 0:
|
|
216
|
+
N = int(round(dist / res))
|
|
217
|
+
else:
|
|
218
|
+
N = 0
|
|
219
|
+
|
|
220
|
+
if N > 0:
|
|
221
|
+
lats_, lons_ = RefineGreatCircleArcSegment(p1, p2, N)
|
|
222
|
+
lats = np.append(lats, lats_)
|
|
223
|
+
lons = np.append(lons, lons_)
|
|
224
|
+
|
|
225
|
+
return lats, lons
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def normalize_to_sphere(xyz, radius=None):
|
|
229
|
+
"""
|
|
230
|
+
Normalize XYZ coordinates to lie on a sphere of given radius.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
xyz : np.ndarray
|
|
235
|
+
Array of shape (N, 3) containing Cartesian coordinates.
|
|
236
|
+
radius : float, optional
|
|
237
|
+
Radius of the sphere in meters. If None, uses Earth's radius.
|
|
238
|
+
|
|
239
|
+
Returns
|
|
240
|
+
-------
|
|
241
|
+
xyz_normalized : np.ndarray
|
|
242
|
+
Array of shape (N, 3) with points projected onto the sphere.
|
|
243
|
+
|
|
244
|
+
Examples
|
|
245
|
+
--------
|
|
246
|
+
>>> xyz = np.array([[1, 0, 0], [0, 1, 0]])
|
|
247
|
+
>>> normalized = normalize_to_sphere(xyz, radius=1.0)
|
|
248
|
+
>>> np.allclose(np.linalg.norm(normalized, axis=1), 1.0)
|
|
249
|
+
True
|
|
250
|
+
"""
|
|
251
|
+
if radius is None:
|
|
252
|
+
radius = EARTH_RADIUS
|
|
253
|
+
|
|
254
|
+
magnitudes = np.linalg.norm(xyz, axis=1, keepdims=True)
|
|
255
|
+
return (xyz / magnitudes) * radius
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def inverse_distance_weighted_interpolation(values, distances, epsilon=1e-10):
|
|
259
|
+
"""
|
|
260
|
+
Compute inverse distance weighted (IDW) interpolation.
|
|
261
|
+
|
|
262
|
+
For each query point, computes a weighted average of neighbor values
|
|
263
|
+
where weights are inversely proportional to distance.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
values : np.ndarray
|
|
268
|
+
Array of shape (N, K) containing values from K neighbors for N points.
|
|
269
|
+
distances : np.ndarray
|
|
270
|
+
Array of shape (N, K) containing distances to K neighbors for N points.
|
|
271
|
+
epsilon : float, default=1e-10
|
|
272
|
+
Small value to detect zero distances. Points with distance < epsilon
|
|
273
|
+
are treated as exact matches and their value is used directly.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
result : np.ndarray
|
|
278
|
+
Array of shape (N,) containing interpolated values.
|
|
279
|
+
|
|
280
|
+
Notes
|
|
281
|
+
-----
|
|
282
|
+
For points where any neighbor has distance < epsilon (essentially zero),
|
|
283
|
+
the value from that neighbor is used directly to avoid division by zero.
|
|
284
|
+
|
|
285
|
+
The IDW formula is: result = sum(v_i / d_i) / sum(1 / d_i)
|
|
286
|
+
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
289
|
+
>>> values = np.array([[10, 20], [30, 40]])
|
|
290
|
+
>>> distances = np.array([[1.0, 2.0], [1.0, 1.0]])
|
|
291
|
+
>>> result = inverse_distance_weighted_interpolation(values, distances)
|
|
292
|
+
>>> # First point: (10/1 + 20/2) / (1/1 + 1/2) = 20/1.5 = 13.33
|
|
293
|
+
>>> # Second point: (30/1 + 40/1) / (1/1 + 1/1) = 70/2 = 35
|
|
294
|
+
"""
|
|
295
|
+
values = np.atleast_2d(values)
|
|
296
|
+
distances = np.atleast_2d(distances)
|
|
297
|
+
|
|
298
|
+
n_points = values.shape[0]
|
|
299
|
+
result = np.zeros(n_points)
|
|
300
|
+
|
|
301
|
+
# Handle each point
|
|
302
|
+
for i in range(n_points):
|
|
303
|
+
dists = distances[i]
|
|
304
|
+
vals = values[i]
|
|
305
|
+
|
|
306
|
+
# Check for zero distance (exact match)
|
|
307
|
+
zero_mask = dists < epsilon
|
|
308
|
+
if np.any(zero_mask):
|
|
309
|
+
# Use the value from the first zero-distance neighbor
|
|
310
|
+
zero_idx = np.argmax(zero_mask)
|
|
311
|
+
result[i] = vals[zero_idx]
|
|
312
|
+
else:
|
|
313
|
+
# Standard IDW: w_i = 1/d_i
|
|
314
|
+
weights = 1.0 / dists
|
|
315
|
+
result[i] = np.sum(vals * weights) / np.sum(weights)
|
|
316
|
+
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def compute_mesh_spacing_km(n_points, earth_radius_km=6378.1):
|
|
321
|
+
"""
|
|
322
|
+
Compute approximate mesh spacing for a given number of points on a sphere.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
n_points : int
|
|
327
|
+
Number of points on the sphere.
|
|
328
|
+
earth_radius_km : float, default=6378.1
|
|
329
|
+
Earth's radius in kilometers.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
spacing_km : float
|
|
334
|
+
Approximate average spacing between mesh points in kilometers.
|
|
335
|
+
|
|
336
|
+
Notes
|
|
337
|
+
-----
|
|
338
|
+
Average area per point = 4 * pi * R^2 / N
|
|
339
|
+
Approximate spacing = sqrt(area per point)
|
|
340
|
+
|
|
341
|
+
Examples
|
|
342
|
+
--------
|
|
343
|
+
>>> spacing = compute_mesh_spacing_km(10000)
|
|
344
|
+
>>> 200 < spacing < 300 # ~10k points gives ~220 km spacing
|
|
345
|
+
True
|
|
346
|
+
"""
|
|
347
|
+
avg_area = 4 * np.pi * earth_radius_km**2 / n_points
|
|
348
|
+
return np.sqrt(avg_area)
|