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/mesh.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ Sphere mesh generation using Fibonacci spiral distribution.
3
+
4
+ This module provides functions to create approximately uniform point
5
+ distributions on a sphere using the Fibonacci spiral algorithm.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import Tuple
10
+
11
+
12
+ def create_sphere_mesh_xyz(n_points: int, radius: float = 1.0) -> np.ndarray:
13
+ """
14
+ Create approximately uniform points on a sphere using Fibonacci spiral.
15
+
16
+ The Fibonacci spiral algorithm distributes points approximately evenly
17
+ over the surface of a sphere using the golden angle. This avoids the
18
+ pole clustering problem of regular lat/lon grids.
19
+
20
+ Parameters
21
+ ----------
22
+ n_points : int
23
+ Number of points to generate on the sphere's surface.
24
+ radius : float, default=1.0
25
+ Radius of the sphere (1.0 for unit sphere, or Earth radius in meters).
26
+
27
+ Returns
28
+ -------
29
+ xyz : np.ndarray
30
+ XYZ coordinates, shape (n_points, 3).
31
+
32
+ Examples
33
+ --------
34
+ >>> xyz = create_sphere_mesh_xyz(10000)
35
+ >>> xyz.shape
36
+ (10000, 3)
37
+ >>> xyz = create_sphere_mesh_xyz(40000, radius=6.3781e6) # Earth radius
38
+ >>> xyz.shape
39
+ (40000, 3)
40
+ """
41
+ if n_points < 1:
42
+ raise ValueError(f"n_points must be >= 1, got {n_points}")
43
+
44
+ # Golden angle in radians
45
+ phi = np.pi * (3.0 - np.sqrt(5.0))
46
+
47
+ # Generate indices
48
+ indices = np.arange(n_points)
49
+
50
+ # Y coordinates: evenly spaced from 1 to -1
51
+ y = 1.0 - (indices / (n_points - 1)) * 2.0 if n_points > 1 else np.array([0.0])
52
+
53
+ # Radius at each y (distance from y-axis)
54
+ r = np.sqrt(1.0 - y * y)
55
+
56
+ # Angle for each point (golden angle spiral)
57
+ theta = phi * indices
58
+
59
+ # Cartesian coordinates on unit sphere
60
+ x = np.cos(theta) * r
61
+ z = np.sin(theta) * r
62
+
63
+ # Stack and scale by radius
64
+ xyz = np.column_stack([x, y, z]) * radius
65
+
66
+ return xyz
67
+
68
+
69
+ def create_sphere_mesh_latlon(n_points: int) -> Tuple[np.ndarray, np.ndarray]:
70
+ """
71
+ Create approximately uniform points on a sphere returning lat/lon coordinates.
72
+
73
+ Parameters
74
+ ----------
75
+ n_points : int
76
+ Number of points to generate.
77
+
78
+ Returns
79
+ -------
80
+ lats : np.ndarray
81
+ Latitudes in degrees, shape (n_points,). Range: -90 to 90.
82
+ lons : np.ndarray
83
+ Longitudes in degrees, shape (n_points,). Range: -180 to 180.
84
+
85
+ Examples
86
+ --------
87
+ >>> lats, lons = create_sphere_mesh_latlon(10000)
88
+ >>> len(lats)
89
+ 10000
90
+ >>> lats.min() >= -90 and lats.max() <= 90
91
+ True
92
+ """
93
+ xyz = create_sphere_mesh_xyz(n_points, radius=1.0)
94
+
95
+ # Convert XYZ to lat/lon
96
+ x, y, z = xyz[:, 0], xyz[:, 1], xyz[:, 2]
97
+
98
+ lats = np.degrees(np.arcsin(np.clip(y, -1.0, 1.0)))
99
+ lons = np.degrees(np.arctan2(z, x))
100
+
101
+ return lats, lons
gtrack/mor_seeds.py ADDED
@@ -0,0 +1,390 @@
1
+ """
2
+ MOR seed point generation using stage rotation (GPlately approach).
3
+
4
+ This module provides functions to generate new seafloor points at
5
+ mid-ocean ridges using the stage rotation between spreading plates.
6
+ """
7
+
8
+ import numpy as np
9
+ from typing import List, Optional, Tuple
10
+
11
+ import pygplates
12
+
13
+
14
+ def get_stage_rotation_for_reconstructed_geometry(
15
+ spreading_feature,
16
+ rotation_model: pygplates.RotationModel,
17
+ spreading_time: float,
18
+ return_left_right_plates: bool = False,
19
+ ):
20
+ """
21
+ Find the stage rotation of a spreading feature in its reconstructed geometry frame.
22
+
23
+ The returned stage rotation can be used to find the spreading direction
24
+ (stage pole) relative to the reconstructed mid-ocean ridge geometry.
25
+
26
+ Parameters
27
+ ----------
28
+ spreading_feature : pygplates.Feature
29
+ A MidOceanRidge feature with left/right plate IDs.
30
+ rotation_model : pygplates.RotationModel
31
+ The rotation model.
32
+ spreading_time : float
33
+ Time at which spreading is happening (Ma).
34
+ return_left_right_plates : bool, default=False
35
+ If True, return (stage_rotation, left_plate_id, right_plate_id).
36
+
37
+ Returns
38
+ -------
39
+ pygplates.FiniteRotation or tuple or None
40
+ The stage rotation in the frame of the reconstructed geometry.
41
+ If return_left_right_plates is True, returns a 3-tuple.
42
+ Returns None if the feature doesn't have valid plate IDs.
43
+
44
+ Notes
45
+ -----
46
+ This function matches GPlately's implementation in
47
+ `ptt.separate_ridge_transform_segments.get_stage_rotation_for_reconstructed_geometry`.
48
+
49
+ The stage rotation is computed as:
50
+ R(0->t, A->Left) * R(t+1->t, Left->Right) * R(0->t, A->Left)^-1
51
+
52
+ This transforms the raw stage rotation into the reference frame of the
53
+ reconstructed geometry so that the stage pole can be directly compared
54
+ to the ridge geometry.
55
+ """
56
+ # Check for left/right plate IDs (required for MidOceanRidge features)
57
+ left_plate_id = spreading_feature.get_left_plate(None)
58
+ right_plate_id = spreading_feature.get_right_plate(None)
59
+
60
+ if left_plate_id is None or right_plate_id is None:
61
+ return None
62
+
63
+ # Get stage rotation from right plate to left plate over 1 Myr
64
+ # This gives the relative motion between the two plates
65
+ stage_rotation = rotation_model.get_rotation(
66
+ spreading_time, right_plate_id, spreading_time + 1, left_plate_id
67
+ )
68
+
69
+ if stage_rotation.represents_identity_rotation():
70
+ return None
71
+
72
+ # Transform stage rotation to the reference frame of the reconstructed geometry
73
+ # This allows the stage pole to be directly compared to the ridge geometry
74
+ from_stage_pole_reference_frame = rotation_model.get_rotation(
75
+ spreading_time, left_plate_id
76
+ )
77
+ to_stage_pole_reference_frame = from_stage_pole_reference_frame.get_inverse()
78
+
79
+ stage_rotation = (
80
+ from_stage_pole_reference_frame
81
+ * stage_rotation
82
+ * to_stage_pole_reference_frame
83
+ )
84
+
85
+ if return_left_right_plates:
86
+ return stage_rotation, left_plate_id, right_plate_id
87
+
88
+ return stage_rotation
89
+
90
+
91
+ def generate_mor_seeds(
92
+ time: float,
93
+ topology_features,
94
+ rotation_model: pygplates.RotationModel,
95
+ ridge_sampling_degrees: float = 0.5,
96
+ spreading_offset_degrees: float = 0.01,
97
+ ) -> Tuple[np.ndarray, np.ndarray]:
98
+ """
99
+ Generate MOR seed points using stage rotation approach.
100
+
101
+ For each mid-ocean ridge segment, creates pairs of points offset
102
+ perpendicular to the ridge using the stage rotation pole. This
103
+ ensures new crust is created on both sides of the spreading center.
104
+
105
+ Parameters
106
+ ----------
107
+ time : float
108
+ Current geological time (Ma).
109
+ topology_features : pygplates.FeatureCollection or list
110
+ Topology feature files or collection.
111
+ rotation_model : pygplates.RotationModel
112
+ The rotation model.
113
+ ridge_sampling_degrees : float, default=0.5
114
+ Resolution for tessellating ridges in degrees (~50 km at equator).
115
+ spreading_offset_degrees : float, default=0.01
116
+ Angle in degrees to rotate points off the ridge (~1 km).
117
+ GPlately uses 0.01 degrees.
118
+
119
+ Returns
120
+ -------
121
+ lats : np.ndarray
122
+ Latitudes of seed points in degrees.
123
+ lons : np.ndarray
124
+ Longitudes of seed points in degrees.
125
+
126
+ Notes
127
+ -----
128
+ The algorithm:
129
+ 1. Resolve topologies at the current time
130
+ 2. For each MidOceanRidge boundary section:
131
+ - Get the stage rotation (relative motion between plates)
132
+ - Extract the stage pole (spreading axis)
133
+ - Tessellate the ridge at the sampling resolution
134
+ - For each ridge point (excluding endpoints):
135
+ - Rotate +spreading_offset_degrees around stage pole
136
+ - Rotate -spreading_offset_degrees around stage pole
137
+ - Add both rotated points (creating symmetric spreading)
138
+ 3. All seed points have age=0 (just formed at ridge)
139
+
140
+ This matches GPlately's `_generate_mid_ocean_ridge_points` function.
141
+
142
+ Examples
143
+ --------
144
+ >>> lats, lons = generate_mor_seeds(
145
+ ... time=100.0,
146
+ ... topology_features=topology_features,
147
+ ... rotation_model=rotation_model
148
+ ... )
149
+ >>> ages = np.zeros(len(lats)) # New crust has age 0
150
+ """
151
+ # Resolve topologies at the current time
152
+ resolved_topologies = []
153
+ shared_boundary_sections = []
154
+ pygplates.resolve_topologies(
155
+ topology_features,
156
+ rotation_model,
157
+ resolved_topologies,
158
+ time,
159
+ shared_boundary_sections,
160
+ )
161
+
162
+ all_lats = []
163
+ all_lons = []
164
+
165
+ # Process each boundary section
166
+ for shared_boundary_section in shared_boundary_sections:
167
+ # Only process MidOceanRidge features
168
+ if (
169
+ shared_boundary_section.get_feature().get_feature_type()
170
+ != pygplates.FeatureType.create_gpml("MidOceanRidge")
171
+ ):
172
+ continue
173
+
174
+ spreading_feature = shared_boundary_section.get_feature()
175
+
176
+ # Get stage rotation for this spreading feature
177
+ stage_rotation = get_stage_rotation_for_reconstructed_geometry(
178
+ spreading_feature, rotation_model, time
179
+ )
180
+
181
+ if stage_rotation is None:
182
+ # Skip if we can't get a valid stage rotation
183
+ continue
184
+
185
+ # Get the stage pole (spreading axis)
186
+ stage_pole, _ = stage_rotation.get_euler_pole_and_angle()
187
+
188
+ # Create rotations to offset points from ridge
189
+ # One rotates "left", the other "right" relative to spreading direction
190
+ rotate_one_way = pygplates.FiniteRotation(
191
+ stage_pole, np.radians(spreading_offset_degrees)
192
+ )
193
+ rotate_opposite_way = rotate_one_way.get_inverse()
194
+
195
+ # Process each sub-segment of the ridge
196
+ for shared_sub_segment in shared_boundary_section.get_shared_sub_segments():
197
+ # Tessellate the ridge segment
198
+ mor_points = pygplates.MultiPointOnSphere(
199
+ shared_sub_segment.get_resolved_geometry().to_tessellated(
200
+ np.radians(ridge_sampling_degrees)
201
+ )
202
+ )
203
+
204
+ # Get the points (skip first and last to avoid ridge endpoints)
205
+ points = mor_points.get_points()
206
+ if len(points) <= 2:
207
+ continue
208
+
209
+ # Get interior points (skip endpoints)
210
+ interior_points = points[1:-1]
211
+
212
+ # Batch rotate all interior points (single C++ call per rotation)
213
+ interior_mp = pygplates.MultiPointOnSphere(interior_points)
214
+ rotated_left = rotate_one_way * interior_mp
215
+ rotated_right = rotate_opposite_way * interior_mp
216
+
217
+ # Extract lat/lon from rotated points
218
+ for p_left, p_right in zip(rotated_left.get_points(), rotated_right.get_points()):
219
+ lat_left, lon_left = p_left.to_lat_lon()
220
+ lat_right, lon_right = p_right.to_lat_lon()
221
+ all_lats.extend([lat_left, lat_right])
222
+ all_lons.extend([lon_left, lon_right])
223
+
224
+ return np.array(all_lats), np.array(all_lons)
225
+
226
+
227
+ def generate_mor_seeds_with_plate_ids(
228
+ time: float,
229
+ topology_features,
230
+ rotation_model: pygplates.RotationModel,
231
+ ridge_sampling_degrees: float = 0.5,
232
+ spreading_offset_degrees: float = 0.01,
233
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
234
+ """
235
+ Generate MOR seed points with explicit plate ID assignments.
236
+
237
+ Like generate_mor_seeds, but also returns the plate ID for each point
238
+ based on which side of the ridge it's on (left or right plate).
239
+
240
+ Parameters
241
+ ----------
242
+ time : float
243
+ Current geological time (Ma).
244
+ topology_features : pygplates.FeatureCollection or list
245
+ Topology feature files or collection.
246
+ rotation_model : pygplates.RotationModel
247
+ The rotation model.
248
+ ridge_sampling_degrees : float, default=0.5
249
+ Resolution for tessellating ridges in degrees.
250
+ spreading_offset_degrees : float, default=0.01
251
+ Angle in degrees to rotate points off the ridge.
252
+
253
+ Returns
254
+ -------
255
+ lats : np.ndarray
256
+ Latitudes of seed points in degrees.
257
+ lons : np.ndarray
258
+ Longitudes of seed points in degrees.
259
+ plate_ids : np.ndarray
260
+ Plate IDs for each point (left plate or right plate).
261
+
262
+ Notes
263
+ -----
264
+ The plate IDs are assigned based on which side of the ridge each
265
+ point is on. Points rotated in the "left" direction get the left
266
+ plate ID, and points rotated in the "right" direction get the right
267
+ plate ID.
268
+
269
+ This is useful for the first time step when we need to explicitly
270
+ assign plate IDs rather than querying topology.
271
+ """
272
+ # Resolve topologies at the current time
273
+ resolved_topologies = []
274
+ shared_boundary_sections = []
275
+ pygplates.resolve_topologies(
276
+ topology_features,
277
+ rotation_model,
278
+ resolved_topologies,
279
+ time,
280
+ shared_boundary_sections,
281
+ )
282
+
283
+ all_lats = []
284
+ all_lons = []
285
+ all_plate_ids = []
286
+
287
+ # Process each boundary section
288
+ for shared_boundary_section in shared_boundary_sections:
289
+ # Only process MidOceanRidge features
290
+ if (
291
+ shared_boundary_section.get_feature().get_feature_type()
292
+ != pygplates.FeatureType.create_gpml("MidOceanRidge")
293
+ ):
294
+ continue
295
+
296
+ spreading_feature = shared_boundary_section.get_feature()
297
+
298
+ # Get stage rotation with plate IDs
299
+ result = get_stage_rotation_for_reconstructed_geometry(
300
+ spreading_feature, rotation_model, time, return_left_right_plates=True
301
+ )
302
+
303
+ if result is None:
304
+ continue
305
+
306
+ stage_rotation, left_plate_id, right_plate_id = result
307
+
308
+ # Get the stage pole
309
+ stage_pole, _ = stage_rotation.get_euler_pole_and_angle()
310
+
311
+ # Create rotations to offset points
312
+ rotate_one_way = pygplates.FiniteRotation(
313
+ stage_pole, np.radians(spreading_offset_degrees)
314
+ )
315
+ rotate_opposite_way = rotate_one_way.get_inverse()
316
+
317
+ # Process each sub-segment
318
+ for shared_sub_segment in shared_boundary_section.get_shared_sub_segments():
319
+ mor_points = pygplates.MultiPointOnSphere(
320
+ shared_sub_segment.get_resolved_geometry().to_tessellated(
321
+ np.radians(ridge_sampling_degrees)
322
+ )
323
+ )
324
+
325
+ points = mor_points.get_points()
326
+ if len(points) <= 2:
327
+ continue
328
+
329
+ for point in points[1:-1]:
330
+ # Create points on each side
331
+ point_left = rotate_one_way * point
332
+ point_right = rotate_opposite_way * point
333
+
334
+ lat_left, lon_left = point_left.to_lat_lon()
335
+ lat_right, lon_right = point_right.to_lat_lon()
336
+
337
+ # Add with respective plate IDs
338
+ all_lats.append(lat_left)
339
+ all_lons.append(lon_left)
340
+ all_plate_ids.append(left_plate_id)
341
+
342
+ all_lats.append(lat_right)
343
+ all_lons.append(lon_right)
344
+ all_plate_ids.append(right_plate_id)
345
+
346
+ return np.array(all_lats), np.array(all_lons), np.array(all_plate_ids)
347
+
348
+
349
+ def get_ridge_geometries(
350
+ time: float,
351
+ topology_features,
352
+ rotation_model: pygplates.RotationModel,
353
+ ) -> List[pygplates.PolylineOnSphere]:
354
+ """
355
+ Extract mid-ocean ridge geometries at a given time.
356
+
357
+ Parameters
358
+ ----------
359
+ time : float
360
+ Geological time (Ma).
361
+ topology_features : pygplates.FeatureCollection or list
362
+ Topology features.
363
+ rotation_model : pygplates.RotationModel
364
+ Rotation model.
365
+
366
+ Returns
367
+ -------
368
+ list of pygplates.PolylineOnSphere
369
+ Ridge geometries at the specified time.
370
+ """
371
+ resolved_topologies = []
372
+ shared_boundary_sections = []
373
+ pygplates.resolve_topologies(
374
+ topology_features,
375
+ rotation_model,
376
+ resolved_topologies,
377
+ time,
378
+ shared_boundary_sections,
379
+ )
380
+
381
+ ridge_geometries = []
382
+ for shared_boundary_section in shared_boundary_sections:
383
+ if (
384
+ shared_boundary_section.get_feature().get_feature_type()
385
+ == pygplates.FeatureType.create_gpml("MidOceanRidge")
386
+ ):
387
+ for shared_sub_segment in shared_boundary_section.get_shared_sub_segments():
388
+ ridge_geometries.append(shared_sub_segment.get_resolved_geometry())
389
+
390
+ return ridge_geometries