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.
@@ -0,0 +1,223 @@
1
+ """
2
+ Polygon filtering operations for point clouds.
3
+
4
+ This module provides functionality to filter points based on polygon containment,
5
+ useful for selecting points inside continental polygons, exclusion zones, etc.
6
+ """
7
+
8
+ from typing import List, Union
9
+ import warnings
10
+
11
+ import numpy as np
12
+
13
+
14
+ class PolygonFilter:
15
+ """
16
+ Filter points by polygon containment.
17
+
18
+ Supports filtering by:
19
+ - Continental polygons (keep only continental points)
20
+ - Custom polygons (user-provided)
21
+ - Exclusion zones (remove points inside polygons)
22
+
23
+ Parameters
24
+ ----------
25
+ polygon_files : str or list of str
26
+ Path(s) to polygon files (.gpml, .gpmlz).
27
+ rotation_files : str or list of str
28
+ Paths to rotation model files (.rot).
29
+
30
+ Examples
31
+ --------
32
+ >>> filter = PolygonFilter(
33
+ ... polygon_files='continental_polygons.gpmlz',
34
+ ... rotation_files=['rotations.rot']
35
+ ... )
36
+ >>>
37
+ >>> # Keep only continental points
38
+ >>> continental_cloud = filter.filter_inside(cloud, at_age=0.0)
39
+ >>>
40
+ >>> # Remove continental points (keep oceanic)
41
+ >>> oceanic_cloud = filter.filter_outside(cloud, at_age=0.0)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ polygon_files: Union[str, List[str]],
47
+ rotation_files: Union[str, List[str]]
48
+ ):
49
+ import pygplates
50
+ from .geometry import ensure_list
51
+
52
+ # Handle single file or Path as list
53
+ polygon_files = ensure_list(polygon_files)
54
+ rotation_files = ensure_list(rotation_files)
55
+
56
+ self.rotation_model = pygplates.RotationModel(rotation_files)
57
+
58
+ # Load polygon features
59
+ self.polygon_features = pygplates.FeatureCollection()
60
+ for file in polygon_files:
61
+ features = pygplates.FeatureCollection(file)
62
+ self.polygon_features.add(features)
63
+
64
+ def get_containment_mask(
65
+ self,
66
+ cloud: "PointCloud",
67
+ at_age: float
68
+ ) -> np.ndarray:
69
+ """
70
+ Get boolean mask of points inside polygons.
71
+
72
+ Parameters
73
+ ----------
74
+ cloud : PointCloud
75
+ Points to check.
76
+ at_age : float
77
+ Geological age at which to check containment (Ma).
78
+ Use 0.0 for present-day polygons.
79
+
80
+ Returns
81
+ -------
82
+ np.ndarray
83
+ Boolean mask, shape (N,), True for points inside polygons.
84
+ """
85
+ import pygplates
86
+ from .geometry import XYZ2LatLon
87
+
88
+ # Convert to lat/lon for pygplates
89
+ lats, lons = XYZ2LatLon(cloud.xyz)
90
+
91
+ # Create partitioner at specified age
92
+ partitioner = pygplates.PlatePartitioner(
93
+ self.polygon_features,
94
+ self.rotation_model,
95
+ reconstruction_time=at_age,
96
+ sort_partitioning_plates=(
97
+ pygplates.SortPartitioningPlates
98
+ .by_partition_type_then_plate_area
99
+ )
100
+ )
101
+
102
+ # Check each point
103
+ # Note: pygplates doesn't have batch partition_point, so we loop
104
+ # This is still efficient because PlatePartitioner does internal spatial indexing
105
+ mask = np.zeros(cloud.n_points, dtype=bool)
106
+
107
+ for i in range(cloud.n_points):
108
+ point = pygplates.PointOnSphere(lats[i], lons[i])
109
+ # partition_point returns None if not inside any polygon
110
+ mask[i] = partitioner.partition_point(point) is not None
111
+
112
+ return mask
113
+
114
+ def filter_inside(
115
+ self,
116
+ cloud: "PointCloud",
117
+ at_age: float
118
+ ) -> "PointCloud":
119
+ """
120
+ Keep only points inside polygons.
121
+
122
+ Parameters
123
+ ----------
124
+ cloud : PointCloud
125
+ Points to filter.
126
+ at_age : float
127
+ Geological age at which to check containment (Ma).
128
+
129
+ Returns
130
+ -------
131
+ PointCloud
132
+ Points inside polygons.
133
+
134
+ Examples
135
+ --------
136
+ >>> # Keep only continental points at present day
137
+ >>> continental = filter.filter_inside(cloud, at_age=0.0)
138
+ """
139
+ mask = self.get_containment_mask(cloud, at_age)
140
+ n_inside = np.sum(mask)
141
+ n_total = cloud.n_points
142
+
143
+ if n_inside == 0:
144
+ warnings.warn(
145
+ f"No points found inside polygons at {at_age} Ma. "
146
+ f"Returning empty PointCloud.",
147
+ UserWarning
148
+ )
149
+
150
+ return cloud.subset(mask)
151
+
152
+ def filter_outside(
153
+ self,
154
+ cloud: "PointCloud",
155
+ at_age: float
156
+ ) -> "PointCloud":
157
+ """
158
+ Keep only points outside polygons.
159
+
160
+ Parameters
161
+ ----------
162
+ cloud : PointCloud
163
+ Points to filter.
164
+ at_age : float
165
+ Geological age at which to check containment (Ma).
166
+
167
+ Returns
168
+ -------
169
+ PointCloud
170
+ Points outside polygons.
171
+
172
+ Examples
173
+ --------
174
+ >>> # Remove continental points (keep oceanic) at present day
175
+ >>> oceanic = filter.filter_outside(cloud, at_age=0.0)
176
+ """
177
+ mask = self.get_containment_mask(cloud, at_age)
178
+ n_outside = np.sum(~mask)
179
+
180
+ if n_outside == 0:
181
+ warnings.warn(
182
+ f"All points are inside polygons at {at_age} Ma. "
183
+ f"Returning empty PointCloud.",
184
+ UserWarning
185
+ )
186
+
187
+ return cloud.subset(~mask)
188
+
189
+ def get_statistics(
190
+ self,
191
+ cloud: "PointCloud",
192
+ at_age: float
193
+ ) -> dict:
194
+ """
195
+ Get statistics about polygon containment.
196
+
197
+ Parameters
198
+ ----------
199
+ cloud : PointCloud
200
+ Points to analyze.
201
+ at_age : float
202
+ Geological age at which to check containment (Ma).
203
+
204
+ Returns
205
+ -------
206
+ dict
207
+ Statistics including:
208
+ - total: Total number of points
209
+ - inside: Number of points inside polygons
210
+ - outside: Number of points outside polygons
211
+ - inside_fraction: Fraction of points inside
212
+ """
213
+ mask = self.get_containment_mask(cloud, at_age)
214
+ n_inside = int(np.sum(mask))
215
+ n_total = cloud.n_points
216
+
217
+ return {
218
+ 'total': n_total,
219
+ 'inside': n_inside,
220
+ 'outside': n_total - n_inside,
221
+ 'inside_fraction': n_inside / n_total if n_total > 0 else 0.0,
222
+ 'at_age': at_age
223
+ }
gtrack/spatial.py ADDED
@@ -0,0 +1,397 @@
1
+ """
2
+ Spatial indexing for efficient point-in-polygon queries.
3
+
4
+ This module provides a quad tree spatial index for efficiently testing
5
+ many points against many polygons on a sphere. The algorithm groups
6
+ nearby points and uses bounding polygon tests to quickly determine
7
+ containment for large groups of points at once.
8
+
9
+ The implementation is based on the algorithm from GPlates Plate Tectonic Tools (ptt),
10
+ originally developed at the University of Sydney.
11
+ """
12
+
13
+ import math
14
+ from typing import List, Optional, Sequence, Tuple, Any
15
+
16
+ import pygplates
17
+
18
+
19
+ # Default subdivision depth for the quad tree.
20
+ # A value of 4 works well for point spacings of ~1 degree or less.
21
+ DEFAULT_SUBDIVISION_DEPTH = 4
22
+
23
+
24
+ class PointsSpatialTree:
25
+ """
26
+ Quad tree spatial index for points on a sphere.
27
+
28
+ Groups points into a hierarchical tree structure based on their
29
+ lat/lon coordinates. This enables efficient spatial queries by
30
+ allowing entire groups of points to be included or excluded
31
+ based on their bounding regions.
32
+
33
+ Parameters
34
+ ----------
35
+ points : sequence of pygplates.PointOnSphere
36
+ The points to index.
37
+ subdivision_depth : int, default=4
38
+ Maximum depth of the quad tree. Leaf nodes have lat/lon width
39
+ of 90 / 2^depth degrees. Higher values create finer subdivisions
40
+ but take longer to build.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ points: Sequence[pygplates.PointOnSphere],
46
+ subdivision_depth: int = DEFAULT_SUBDIVISION_DEPTH,
47
+ ):
48
+ if subdivision_depth < 0 or subdivision_depth > 100:
49
+ raise ValueError("subdivision_depth must be in range [0, 100]")
50
+
51
+ # 8 root nodes covering the globe (2 lat bands x 4 lon quadrants)
52
+ self._root_nodes: List[Optional[_SpatialTreeNode]] = [None] * 8
53
+
54
+ # Insert each point into the tree
55
+ for point_index, point in enumerate(points):
56
+ lat, lon = point.to_lat_lon()
57
+
58
+ # Determine which root node this point belongs to
59
+ lat_idx = 1 if lat >= 0 else 0
60
+ if lon < -90:
61
+ lon_idx = 0
62
+ elif lon < 0:
63
+ lon_idx = 1
64
+ elif lon < 90:
65
+ lon_idx = 2
66
+ else:
67
+ lon_idx = 3
68
+
69
+ root_idx = 4 * lat_idx + lon_idx
70
+ is_north = lat_idx == 1
71
+
72
+ # Create root node if needed
73
+ if self._root_nodes[root_idx] is None:
74
+ centre_lon = -180 + 90 * lon_idx + 45
75
+ centre_lat = -90 + 90 * lat_idx + 45
76
+ self._root_nodes[root_idx] = _SpatialTreeNode(
77
+ centre_lon, centre_lat, 45.0, is_north
78
+ )
79
+
80
+ # Traverse down to the appropriate leaf node
81
+ node = self._root_nodes[root_idx]
82
+ node_centre_lon = -180 + 90 * lon_idx + 45
83
+ node_centre_lat = -90 + 90 * lat_idx + 45
84
+ node_half_width = 45.0
85
+
86
+ for _ in range(subdivision_depth):
87
+ child_half_width = node_half_width / 2
88
+
89
+ if lat < node_centre_lat:
90
+ child_lat_idx = 0
91
+ child_centre_lat = node_centre_lat - child_half_width
92
+ else:
93
+ child_lat_idx = 1
94
+ child_centre_lat = node_centre_lat + child_half_width
95
+
96
+ if lon < node_centre_lon:
97
+ child_lon_idx = 0
98
+ child_centre_lon = node_centre_lon - child_half_width
99
+ else:
100
+ child_lon_idx = 1
101
+ child_centre_lon = node_centre_lon + child_half_width
102
+
103
+ # Create child nodes list if needed
104
+ if node._children is None:
105
+ node._children = [None] * 4
106
+
107
+ child_idx = 2 * child_lat_idx + child_lon_idx
108
+ if node._children[child_idx] is None:
109
+ node._children[child_idx] = _SpatialTreeNode(
110
+ child_centre_lon, child_centre_lat, child_half_width, is_north
111
+ )
112
+
113
+ node = node._children[child_idx]
114
+ node_centre_lon = child_centre_lon
115
+ node_centre_lat = child_centre_lat
116
+ node_half_width = child_half_width
117
+
118
+ # Add point index to leaf node
119
+ if node._point_indices is None:
120
+ node._point_indices = []
121
+ node._point_indices.append(point_index)
122
+
123
+ def get_root_nodes(self) -> List["_SpatialTreeNode"]:
124
+ """Get all non-empty root nodes."""
125
+ return [n for n in self._root_nodes if n is not None]
126
+
127
+
128
+ class _SpatialTreeNode:
129
+ """
130
+ A node in the spatial tree representing a lat/lon region.
131
+
132
+ Internal nodes have children; leaf nodes have point indices.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ centre_lon: float,
138
+ centre_lat: float,
139
+ half_width: float,
140
+ is_north: bool,
141
+ ):
142
+ self._centre_lon = centre_lon
143
+ self._centre_lat = centre_lat
144
+ self._half_width = half_width
145
+ self._is_north = is_north
146
+
147
+ self._children: Optional[List[Optional["_SpatialTreeNode"]]] = None
148
+ self._point_indices: Optional[List[int]] = None
149
+ self._bounding_polygon: Optional[pygplates.PolygonOnSphere] = None
150
+
151
+ def is_internal(self) -> bool:
152
+ """True if this is an internal node with children."""
153
+ return self._children is not None
154
+
155
+ def is_leaf(self) -> bool:
156
+ """True if this is a leaf node with point indices."""
157
+ return self._point_indices is not None
158
+
159
+ def get_children(self) -> List["_SpatialTreeNode"]:
160
+ """Get non-empty child nodes."""
161
+ if self._children is None:
162
+ return []
163
+ return [c for c in self._children if c is not None]
164
+
165
+ def get_point_indices(self) -> List[int]:
166
+ """Get point indices in this leaf node."""
167
+ return self._point_indices if self._point_indices else []
168
+
169
+ def get_bounding_polygon(self) -> pygplates.PolygonOnSphere:
170
+ """Get polygon bounding this node's region."""
171
+ if self._bounding_polygon is None:
172
+ self._bounding_polygon = self._create_bounding_polygon()
173
+ return self._bounding_polygon
174
+
175
+ def _create_bounding_polygon(self) -> pygplates.PolygonOnSphere:
176
+ """Create a polygon that bounds this node's lat/lon region."""
177
+ left_lon = self._centre_lon - self._half_width
178
+ right_lon = self._centre_lon + self._half_width
179
+ bottom_lat = self._centre_lat - self._half_width
180
+ top_lat = self._centre_lat + self._half_width
181
+
182
+ points = []
183
+
184
+ if self._is_north:
185
+ # Northern hemisphere - need to handle bottom edge carefully
186
+ left_line = pygplates.PolylineOnSphere([(0, left_lon), (90, left_lon)])
187
+ right_line = pygplates.PolylineOnSphere([(0, right_lon), (90, right_lon)])
188
+
189
+ bottom_mid = pygplates.PointOnSphere(bottom_lat, (left_lon + right_lon) / 2)
190
+
191
+ # Great circle through bottom midpoint oriented toward pole
192
+ axis = pygplates.Vector3D.cross(
193
+ bottom_mid.to_xyz(),
194
+ pygplates.Vector3D.cross(
195
+ pygplates.PointOnSphere.north_pole.to_xyz(),
196
+ bottom_mid.to_xyz(),
197
+ ),
198
+ ).to_normalised()
199
+ rotation = pygplates.FiniteRotation(axis.to_xyz(), math.pi / 2)
200
+
201
+ bottom_line = pygplates.PolylineOnSphere([
202
+ rotation * bottom_mid,
203
+ bottom_mid,
204
+ rotation.get_inverse() * bottom_mid,
205
+ ])
206
+
207
+ _, _, bl = pygplates.GeometryOnSphere.distance(
208
+ bottom_line, left_line, return_closest_positions=True
209
+ )
210
+ _, _, br = pygplates.GeometryOnSphere.distance(
211
+ bottom_line, right_line, return_closest_positions=True
212
+ )
213
+
214
+ points = [bl, br,
215
+ pygplates.PointOnSphere(top_lat, right_lon),
216
+ pygplates.PointOnSphere(top_lat, left_lon)]
217
+ else:
218
+ # Southern hemisphere - need to handle top edge carefully
219
+ left_line = pygplates.PolylineOnSphere([(0, left_lon), (-90, left_lon)])
220
+ right_line = pygplates.PolylineOnSphere([(0, right_lon), (-90, right_lon)])
221
+
222
+ top_mid = pygplates.PointOnSphere(top_lat, (left_lon + right_lon) / 2)
223
+
224
+ axis = pygplates.Vector3D.cross(
225
+ top_mid.to_xyz(),
226
+ pygplates.Vector3D.cross(
227
+ pygplates.PointOnSphere.north_pole.to_xyz(),
228
+ top_mid.to_xyz(),
229
+ ),
230
+ ).to_normalised()
231
+ rotation = pygplates.FiniteRotation(axis.to_xyz(), math.pi / 2)
232
+
233
+ top_line = pygplates.PolylineOnSphere([
234
+ rotation * top_mid,
235
+ top_mid,
236
+ rotation.get_inverse() * top_mid,
237
+ ])
238
+
239
+ _, _, tl = pygplates.GeometryOnSphere.distance(
240
+ top_line, left_line, return_closest_positions=True
241
+ )
242
+ _, _, tr = pygplates.GeometryOnSphere.distance(
243
+ top_line, right_line, return_closest_positions=True
244
+ )
245
+
246
+ points = [tl, tr,
247
+ pygplates.PointOnSphere(bottom_lat, right_lon),
248
+ pygplates.PointOnSphere(bottom_lat, left_lon)]
249
+
250
+ return pygplates.PolygonOnSphere(points)
251
+
252
+
253
+ def find_polygons(
254
+ points: Sequence[pygplates.PointOnSphere],
255
+ polygons: Sequence[pygplates.PolygonOnSphere],
256
+ polygon_proxies: Optional[Sequence[Any]] = None,
257
+ all_polygons: bool = False,
258
+ subdivision_depth: int = DEFAULT_SUBDIVISION_DEPTH,
259
+ ) -> List:
260
+ """
261
+ Find which polygon(s) contain each point.
262
+
263
+ Uses a quad tree spatial index for efficient batch queries.
264
+ For uniformly distributed points, this is ~70x faster than
265
+ naive point-by-point testing.
266
+
267
+ Parameters
268
+ ----------
269
+ points : sequence of pygplates.PointOnSphere
270
+ Points to test.
271
+ polygons : sequence of pygplates.PolygonOnSphere
272
+ Polygons to test against.
273
+ polygon_proxies : sequence, optional
274
+ Objects to return instead of polygons (e.g., features).
275
+ If None, returns the polygons themselves.
276
+ all_polygons : bool, default=False
277
+ If True, find all polygons containing each point (for overlapping polygons).
278
+ If False, find only the first polygon (for non-overlapping polygons).
279
+ subdivision_depth : int, default=4
280
+ Quad tree subdivision depth.
281
+
282
+ Returns
283
+ -------
284
+ list
285
+ For each point: the containing polygon proxy (or None), or
286
+ if all_polygons=True, a list of containing polygon proxies (or None).
287
+ """
288
+ if polygon_proxies is None:
289
+ polygon_proxies = polygons
290
+
291
+ if len(polygons) != len(polygon_proxies):
292
+ raise ValueError("polygons and polygon_proxies must have same length")
293
+
294
+ # Build spatial tree
295
+ tree = PointsSpatialTree(points, subdivision_depth)
296
+
297
+ # Sort polygons by area (largest first) for efficiency
298
+ sorted_pairs = sorted(
299
+ zip(polygons, polygon_proxies),
300
+ key=lambda p: p[0].get_area(),
301
+ reverse=True,
302
+ )
303
+
304
+ # Initialize results
305
+ results: List = [None] * len(points)
306
+
307
+ # Process each root node
308
+ for root in tree.get_root_nodes():
309
+ _process_node(root, points, sorted_pairs, results, all_polygons)
310
+
311
+ return results
312
+
313
+
314
+ def _process_node(
315
+ node: _SpatialTreeNode,
316
+ points: Sequence[pygplates.PointOnSphere],
317
+ polygons_and_proxies: List[Tuple[pygplates.PolygonOnSphere, Any]],
318
+ results: List,
319
+ all_polygons: bool,
320
+ ) -> None:
321
+ """Recursively process a spatial tree node."""
322
+ # Find polygons that overlap this node
323
+ overlapping = []
324
+ node_poly = node.get_bounding_polygon()
325
+
326
+ for polygon, proxy in polygons_and_proxies:
327
+ # Check if node and polygon overlap (distance < threshold means overlap)
328
+ dist = pygplates.GeometryOnSphere.distance(
329
+ node_poly, polygon, 1e-4,
330
+ geometry1_is_solid=True, geometry2_is_solid=True
331
+ )
332
+
333
+ if dist is not None:
334
+ # Check if node is completely inside polygon
335
+ outline_dist = pygplates.GeometryOnSphere.distance(
336
+ node_poly, polygon, 1e-4
337
+ )
338
+
339
+ completely_inside = False
340
+ if outline_dist is None:
341
+ # Outlines don't touch - check if node is inside polygon
342
+ if polygon.is_point_in_polygon(node_poly.get_exterior_ring_points()[0]):
343
+ completely_inside = True
344
+ # Check for interior holes that might be inside node
345
+ for i in range(polygon.get_number_of_interior_rings()):
346
+ hole_pt = polygon.get_interior_ring_points(i)[0]
347
+ if node_poly.is_point_in_polygon(hole_pt):
348
+ completely_inside = False
349
+ break
350
+
351
+ if completely_inside:
352
+ # Fill entire subtree as inside this polygon
353
+ _fill_node(node, proxy, results, all_polygons)
354
+ if not all_polygons:
355
+ return
356
+ else:
357
+ overlapping.append((polygon, proxy))
358
+
359
+ if not overlapping:
360
+ return
361
+
362
+ # Process children or test individual points
363
+ if node.is_internal():
364
+ for child in node.get_children():
365
+ _process_node(child, points, overlapping, results, all_polygons)
366
+ else:
367
+ for idx in node.get_point_indices():
368
+ point = points[idx]
369
+ for polygon, proxy in overlapping:
370
+ if polygon.is_point_in_polygon(point):
371
+ if all_polygons:
372
+ if results[idx] is None:
373
+ results[idx] = []
374
+ results[idx].append(proxy)
375
+ else:
376
+ results[idx] = proxy
377
+ break
378
+
379
+
380
+ def _fill_node(
381
+ node: _SpatialTreeNode,
382
+ proxy: Any,
383
+ results: List,
384
+ all_polygons: bool,
385
+ ) -> None:
386
+ """Mark all points in a node's subtree as inside a polygon."""
387
+ if node.is_internal():
388
+ for child in node.get_children():
389
+ _fill_node(child, proxy, results, all_polygons)
390
+ else:
391
+ for idx in node.get_point_indices():
392
+ if all_polygons:
393
+ if results[idx] is None:
394
+ results[idx] = []
395
+ results[idx].append(proxy)
396
+ else:
397
+ results[idx] = proxy
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtrack
3
+ Version: 0.3.0
4
+ Summary: GPlates-based Tracking of Lithosphere and Kinematics
5
+ Author: S. Ghelichkhani
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sghelichkhani/gtrack
8
+ Project-URL: Documentation, https://gtrack.gadopt.org
9
+ Project-URL: Repository, https://github.com/sghelichkhani/gtrack
10
+ Keywords: plate tectonics,seafloor age,geodynamics,pygplates,lithosphere,kinematics
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: numpy>=1.20
23
+ Requires-Dist: scipy>=1.7
24
+ Requires-Dist: pygplates
25
+ Provides-Extra: visualization
26
+ Requires-Dist: matplotlib>=3.5; extra == "visualization"
27
+ Requires-Dist: cartopy>=0.21; extra == "visualization"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: pytest-cov>=3.0; extra == "dev"
31
+ Provides-Extra: docs
32
+ Requires-Dist: mkdocs>=1.5; extra == "docs"
33
+ Requires-Dist: mkdocs-material>=9.0; extra == "docs"
34
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == "docs"
35
+ Requires-Dist: mkdocs-jupyter>=0.24; extra == "docs"
36
+ Provides-Extra: demos
37
+ Requires-Dist: jupytext>=1.15; extra == "demos"
38
+ Requires-Dist: nbconvert>=7.0; extra == "demos"
39
+ Requires-Dist: ipykernel>=6.0; extra == "demos"
40
+ Requires-Dist: matplotlib>=3.5; extra == "demos"
41
+ Requires-Dist: cartopy>=0.21; extra == "demos"
42
+ Requires-Dist: h5py>=3.0; extra == "demos"
43
+ Provides-Extra: all
44
+ Requires-Dist: gtrack[demos,dev,docs,visualization]; extra == "all"
45
+
46
+ # gtrack
47
+
48
+ **GPlates-based Tracking of Lithosphere and Kinematics**
49
+
50
+ A Python package for computing lithospheric structure through geological time using plate tectonic reconstructions.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install gtrack
56
+ ```
57
+
58
+ ## Documentation
59
+
60
+ For full documentation, examples, and API reference, visit:
61
+
62
+ **[https://gtrack.gadopt.org](https://gtrack.gadopt.org)**
63
+
64
+ ## License
65
+
66
+ MIT License
@@ -0,0 +1,17 @@
1
+ gtrack/__init__.py,sha256=HPCvSZ2YTM0uhaRoMT9LfFwxg4vpMyaQDipd_49YX8I,3618
2
+ gtrack/boundaries.py,sha256=6jF2omFc-YSUXc3_nh3UT6dl-iCCjNT7GWeiID3h6Zs,11581
3
+ gtrack/config.py,sha256=V3qeBvhPFiyeZVEvCnli7eSzljtV3ZnJ2yh02V5v860,7131
4
+ gtrack/geometry.py,sha256=P90FeKFbQTOjT63RI-uc13Wa2YmeVhqWWPKMdYOAKLU,10034
5
+ gtrack/hpc_integration.py,sha256=rcQAINQtCI0VjpJ8FB8JbkLhKuyEdOaJRZoCYm2HKms,29675
6
+ gtrack/initial_conditions.py,sha256=bTgVYBoIWgOq8KFjOUPMwj_P11cSnlkDXi6UrzrXh0U,9185
7
+ gtrack/io_formats.py,sha256=_zXOdn7rqlcD0fORNQmh8hemYKzABCax1X1W-WVGpiE,12769
8
+ gtrack/logging.py,sha256=6N8QivpVq0oahwi0K62zUgQici-rK40V9CEOPDSpwp0,4930
9
+ gtrack/mesh.py,sha256=BCgGWXLnflK_aiD9B6GBY0X_OJur645Sc1lVX6O4Q6Q,2733
10
+ gtrack/mor_seeds.py,sha256=mFF89nKOq7yIEOIE41AnNTJdbQ3Q5OLYGnAu6KbeXg8,13395
11
+ gtrack/point_rotation.py,sha256=FPmOZWKhaySsTZKTWlXIVPEtmcXjX-ufsDM0NPbg9gU,26304
12
+ gtrack/polygon_filter.py,sha256=5wDChgkxF7IU7of2JfkAWVbzxOHSgnuIQsg-eA0Yyoo,6333
13
+ gtrack/spatial.py,sha256=70UXaH1TyaQDR1P_1j2FoCdUTTa8a921h7Wt6kj1hZw,14475
14
+ gtrack-0.3.0.dist-info/METADATA,sha256=S5sBdsfGDbJZBO4ludNyDFO5tQVoIb1TexYwIGuPjGI,2268
15
+ gtrack-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
16
+ gtrack-0.3.0.dist-info/top_level.txt,sha256=x1GottZL9pxdL8Ff3ARhF6tAhM4-CIKgnS9Hl7PZzyQ,7
17
+ gtrack-0.3.0.dist-info/RECORD,,