pyearthmesh 0.1.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.
Files changed (62) hide show
  1. pyearthmesh/__init__.py +32 -0
  2. pyearthmesh/classes/__init__.py +19 -0
  3. pyearthmesh/classes/dggrid.py +627 -0
  4. pyearthmesh/classes/edge.py +398 -0
  5. pyearthmesh/classes/hexagon.py +572 -0
  6. pyearthmesh/classes/latlon.py +557 -0
  7. pyearthmesh/classes/link.py +562 -0
  8. pyearthmesh/classes/meshcell.py +648 -0
  9. pyearthmesh/classes/mpas.py +652 -0
  10. pyearthmesh/classes/square.py +533 -0
  11. pyearthmesh/classes/tin.py +721 -0
  12. pyearthmesh/classes/vertex.py +240 -0
  13. pyearthmesh/meshes/__init__.py +3 -0
  14. pyearthmesh/meshes/structured/__init__.py +3 -0
  15. pyearthmesh/meshes/structured/cubicsphere/__init__.py +3 -0
  16. pyearthmesh/meshes/structured/cubicsphere/create_cubicsphere_mesh.py +287 -0
  17. pyearthmesh/meshes/structured/dggs/__init__.py +3 -0
  18. pyearthmesh/meshes/structured/dggs/dggrid/__init__.py +3 -0
  19. pyearthmesh/meshes/structured/dggs/dggrid/create_dggrid_mesh.py +268 -0
  20. pyearthmesh/meshes/structured/dggs/healpix/__init__.py +3 -0
  21. pyearthmesh/meshes/structured/dggs/healpix/create_rhealpix_mesh.py +219 -0
  22. pyearthmesh/meshes/structured/dggs/isea/__init__.py +3 -0
  23. pyearthmesh/meshes/structured/dggs/isea/create_isea_mesh.py +221 -0
  24. pyearthmesh/meshes/structured/hexaon/__init__.py +1 -0
  25. pyearthmesh/meshes/structured/hexaon/create_hexagon_mesh.py +524 -0
  26. pyearthmesh/meshes/structured/latlon/__init__.py +1 -0
  27. pyearthmesh/meshes/structured/latlon/create_latlon_mesh.py +475 -0
  28. pyearthmesh/meshes/structured/square/__init__.py +0 -0
  29. pyearthmesh/meshes/structured/square/create_square_mesh.py +270 -0
  30. pyearthmesh/meshes/structured/triangular/__init__.py +3 -0
  31. pyearthmesh/meshes/structured/triangular/create_triangular_mesh.py +197 -0
  32. pyearthmesh/meshes/unstructured/__init__.py +3 -0
  33. pyearthmesh/meshes/unstructured/mpas/__init__.py +3 -0
  34. pyearthmesh/meshes/unstructured/mpas/create_mpas_mesh.py +784 -0
  35. pyearthmesh/meshes/unstructured/tin/__init__.py +3 -0
  36. pyearthmesh/meshes/unstructured/tin/create_tin_mesh.py +280 -0
  37. pyearthmesh/utility/__init__.py +1 -0
  38. pyearthmesh/utility/convert_attributes.py +57 -0
  39. pyearthmesh/utility/convert_coordinates.py +121 -0
  40. pyearthmesh/utility/extract_land_meshcells.py +202 -0
  41. pyearthmesh/utility/find_minimal_enclosing_polygon.py +28 -0
  42. pyearthmesh/utility/jigsaw/__init__.py +3 -0
  43. pyearthmesh/utility/jigsaw/inpoly2.py +221 -0
  44. pyearthmesh/utility/jigsaw/loadgeo.py +189 -0
  45. pyearthmesh/utility/jigsaw/loadgeo2.py +142 -0
  46. pyearthmesh/utility/jigsaw/run_jigsaw.py +1071 -0
  47. pyearthmesh/utility/jigsaw/saveesm.py +299 -0
  48. pyearthmesh/utility/jigsaw/savetin.py +82 -0
  49. pyearthmesh/utility/jigsaw/utility.py +155 -0
  50. pyearthmesh/utility/mesh_utility.py +703 -0
  51. pyearthmesh/utility/mpas_tools/__init__.py +3 -0
  52. pyearthmesh/utility/mpas_tools/compose.py +73 -0
  53. pyearthmesh/utility/mpas_tools/mpasmsh.py +299 -0
  54. pyearthmesh/utility/mpas_tools/spacing.py +263 -0
  55. pyearthmesh/utility/potentiometric/__init__.py +3 -0
  56. pyearthmesh/utility/potentiometric/calculate_potentiometric.py +49 -0
  57. pyearthmesh/utility/split_by_length.py +127 -0
  58. pyearthmesh-0.1.0.dist-info/METADATA +155 -0
  59. pyearthmesh-0.1.0.dist-info/RECORD +62 -0
  60. pyearthmesh-0.1.0.dist-info/WHEEL +5 -0
  61. pyearthmesh-0.1.0.dist-info/licenses/LICENSE +28 -0
  62. pyearthmesh-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,32 @@
1
+ """
2
+ PyEarthMesh: A lightweight mesh generation and manipulation package for Earth science applications.
3
+
4
+ This package provides tools for creating and manipulating various types of meshes:
5
+ - Structured meshes (square, latlon, hexagon, triangular, DGGRID, Healpix)
6
+ - Unstructured meshes (TIN, MPAS)
7
+
8
+ Part of the PyEarthSuite ecosystem.
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+ __author__ = "Chang Liao"
13
+ __email__ = "changliao.climate@gmail.com"
14
+
15
+ # Import main classes
16
+ from pyearthmesh.classes.vertex import Vertex
17
+ from pyearthmesh.classes.edge import Edge
18
+ from pyearthmesh.classes.meshcell import MeshCell
19
+ from pyearthmesh.classes.hexagon import Hexagon
20
+ from pyearthmesh.classes.square import Square
21
+ from pyearthmesh.classes.latlon import LatLon
22
+ from pyearthmesh.classes.link import Link
23
+
24
+ __all__ = [
25
+ "Vertex",
26
+ "Edge",
27
+ "MeshCell",
28
+ "Hexagon",
29
+ "Square",
30
+ "LatLon",
31
+ "Link",
32
+ ]
@@ -0,0 +1,19 @@
1
+ """PyEarthMesh classes module."""
2
+
3
+ from pyearthmesh.classes.vertex import Vertex
4
+ from pyearthmesh.classes.edge import Edge
5
+ from pyearthmesh.classes.meshcell import MeshCell
6
+ from pyearthmesh.classes.hexagon import Hexagon
7
+ from pyearthmesh.classes.square import Square
8
+ from pyearthmesh.classes.latlon import LatLon
9
+ from pyearthmesh.classes.link import Link
10
+
11
+ __all__ = [
12
+ "Vertex",
13
+ "Edge",
14
+ "MeshCell",
15
+ "Hexagon",
16
+ "Square",
17
+ "LatLon",
18
+ "Link",
19
+ ]
@@ -0,0 +1,627 @@
1
+ """
2
+ PyDGGRID class for representing Discrete Global Grid mesh cells in flowline networks.
3
+
4
+ This module defines the pydggrid class which extends pymeshcell to provide
5
+ DGGRID-specific mesh cell functionality for flowline network modeling.
6
+ DGGRID (Discrete Global Grid) systems use hierarchical grids with various
7
+ cell shapes including hexagons, triangles, and diamonds.
8
+ """
9
+
10
+ import json
11
+ from json import JSONEncoder
12
+ from typing import List, Optional, Any, Tuple, Dict
13
+ import numpy as np
14
+
15
+ from pyearth.gis.geometry.calculate_polygon_area import calculate_polygon_area
16
+
17
+ from pyearthmesh.classes.vertex import pyvertex
18
+ from pyearthmesh.classes.edge import pyedge
19
+ from pyearthmesh.classes.meshcell import pymeshcell
20
+
21
+
22
+ class DggridClassEncoder(JSONEncoder):
23
+ """
24
+ Custom JSON encoder for pydggrid objects.
25
+
26
+ Handles numpy data types, pyvertex objects, and other complex types,
27
+ converting them to native Python types for JSON serialization.
28
+ """
29
+
30
+ def default(self, obj):
31
+ if isinstance(obj, np.integer):
32
+ return int(obj)
33
+ if isinstance(obj, (np.float32, np.float64)):
34
+ return float(obj)
35
+ if isinstance(obj, np.ndarray):
36
+ return obj.tolist()
37
+ if isinstance(obj, list):
38
+ return obj
39
+ if pyvertex and isinstance(obj, pyvertex):
40
+ return json.loads(obj.tojson())
41
+ if pyedge and isinstance(obj, pyedge):
42
+ return getattr(obj, "lEdgeID", str(obj))
43
+ if isinstance(obj, pydggrid):
44
+ return obj.lCellID
45
+ return JSONEncoder.default(self, obj)
46
+
47
+
48
+ class pydggrid(pymeshcell):
49
+ """
50
+ Discrete Global Grid (DGGRID) mesh cell class for flowline network representation.
51
+
52
+ Extends the pymeshcell class to provide DGGRID-specific functionality for
53
+ mesh cells in flowline topology and stream network analysis. A DGGRID cell
54
+ represents a polygonal area in a hierarchical discrete global grid system.
55
+
56
+ DGGRID systems use multi-resolution grids that can have various cell shapes
57
+ including hexagons, triangles, diamonds, and other polygons depending on
58
+ the grid configuration and hierarchical level. This flexibility makes DGGRID
59
+ suitable for global modeling at multiple scales.
60
+
61
+ Attributes:
62
+ All attributes from pymeshcell plus DGGRID-specific properties.
63
+ The DGGRID cell can have variable numbers of edges and vertices
64
+ depending on the grid system configuration.
65
+ dggrid_level (int): Hierarchical level in the DGGRID system
66
+ dggrid_id (str): Unique identifier within the DGGRID system
67
+ dggrid_resolution (float): Resolution class of the DGGRID cell
68
+
69
+ Args:
70
+ dLon (float): The longitude of the DGGRID cell center in degrees
71
+ dLat (float): The latitude of the DGGRID cell center in degrees
72
+ aEdge (List): A list of edges that define the DGGRID cell boundary
73
+ aVertex (List): A list of vertices that define the DGGRID cell boundary
74
+
75
+ Raises:
76
+ ValueError: If the cell doesn't have at least 3 edges and vertices
77
+ TypeError: If input parameters are not of the expected types
78
+
79
+ Example:
80
+ >>> center_lon, center_lat = -77.0, 38.0
81
+ >>> edges = [edge1, edge2, edge3, edge4, edge5, edge6] # List of edges
82
+ >>> vertices = [v1, v2, v3, v4, v5, v6] # List of vertices
83
+ >>> dggrid_cell = pydggrid(center_lon, center_lat, edges, vertices)
84
+ >>> dggrid_cell.set_cell_id(100)
85
+ >>> dggrid_cell.set_dggrid_properties(level=5, grid_id="N1234", resolution=1000.0)
86
+ >>> print(dggrid_cell)
87
+ pydggrid(ID=100, Center=(-77.0, 38.0), Level=5, Edges=6, Area=1234.56m²)
88
+ """
89
+
90
+ def __init__(self, dLon: float, dLat: float, aEdge: List, aVertex: List) -> None:
91
+ """
92
+ Initialize a DGGRID mesh cell object.
93
+
94
+ Args:
95
+ dLon (float): The longitude of the DGGRID cell center in degrees
96
+ dLat (float): The latitude of the DGGRID cell center in degrees
97
+ aEdge (List): A list of edges that define the DGGRID cell boundary
98
+ aVertex (List): A list of vertices that define the DGGRID cell boundary
99
+
100
+ Raises:
101
+ ValueError: If the cell doesn't have at least 3 edges and vertices
102
+ TypeError: If input parameters are not of the expected types
103
+ """
104
+ # Input validation
105
+ if not isinstance(dLon, (int, float, np.number)):
106
+ raise TypeError(f"dLon must be a number, got {type(dLon)}")
107
+ if not isinstance(dLat, (int, float, np.number)):
108
+ raise TypeError(f"dLat must be a number, got {type(dLat)}")
109
+ if not isinstance(aEdge, list):
110
+ raise TypeError(f"aEdge must be a list, got {type(aEdge)}")
111
+ if not isinstance(aVertex, list):
112
+ raise TypeError(f"aVertex must be a list, got {type(aVertex)}")
113
+
114
+ # Validate coordinate ranges
115
+ if not (-180 <= dLon <= 180):
116
+ raise ValueError(f"Longitude must be between -180 and 180, got {dLon}")
117
+ if not (-90 <= dLat <= 90):
118
+ raise ValueError(f"Latitude must be between -90 and 90, got {dLat}")
119
+
120
+ # Validate DGGRID geometry requirements (minimum 3 edges for polygon)
121
+ nEdge = len(aEdge)
122
+ nVertex = len(aVertex)
123
+
124
+ if nEdge < 3:
125
+ raise ValueError(f"DGGRID cell must have at least 3 edges, got {nEdge}")
126
+ if nVertex < 3:
127
+ raise ValueError(
128
+ f"DGGRID cell must have at least 3 vertices, got {nVertex}"
129
+ )
130
+
131
+ # Validate edges and vertices are not None
132
+ for i, edge in enumerate(aEdge):
133
+ if edge is None:
134
+ raise ValueError(f"Edge at index {i} cannot be None")
135
+
136
+ for i, vertex in enumerate(aVertex):
137
+ if vertex is None:
138
+ raise ValueError(f"Vertex at index {i} cannot be None")
139
+
140
+ # Initialize parent class
141
+ super().__init__(dLon, dLat, aEdge, aVertex)
142
+
143
+ # DGGRID-specific initialization
144
+ self.nEdge = nEdge
145
+ self.nVertex = nVertex
146
+
147
+ # DGGRID-specific attributes
148
+ self.dggrid_level: int = -1
149
+ self.dggrid_id: str = ""
150
+ self.dggrid_resolution: float = -1.0
151
+ self.dggrid_shape_type: str = "unknown"
152
+
153
+ # Create center vertex if pyvertex is available
154
+ if pyvertex:
155
+ pVertex_params = {
156
+ "dLongitude_degree": self.dLongitude_center_degree,
157
+ "dLatitude_degree": self.dLatitude_center_degree,
158
+ }
159
+ self.pVertex_center = pyvertex(pVertex_params)
160
+ else:
161
+ self.pVertex_center = None
162
+
163
+ # Calculate initial properties
164
+ self.calculate_cell_area()
165
+ if self.dArea > 0:
166
+ self.calculate_edge_length()
167
+
168
+ # Determine shape type based on number of edges
169
+ self._determine_shape_type()
170
+
171
+ def __repr__(self) -> str:
172
+ """
173
+ Return a detailed string representation of the DGGRID cell.
174
+
175
+ Returns:
176
+ str: Detailed representation including cell ID, center coordinates, level, and area
177
+ """
178
+ return (
179
+ f"pydggrid(ID={self.lCellID}, "
180
+ f"Center=({self.dLongitude_center_degree:.6f}, {self.dLatitude_center_degree:.6f}), "
181
+ f"Level={self.dggrid_level}, Edges={self.nEdge}, Shape={self.dggrid_shape_type}, "
182
+ f"Area={self.dArea:.2f}m², Resolution={self.dLength:.2f}m, "
183
+ f"Flowlines={self.nFlowline}, Neighbors={self.nNeighbor})"
184
+ )
185
+
186
+ def __str__(self) -> str:
187
+ """
188
+ Return a concise string representation of the DGGRID cell.
189
+
190
+ Returns:
191
+ str: Concise representation with ID, center coordinates, level, and area
192
+ """
193
+ return (
194
+ f"pydggrid(ID={self.lCellID}, "
195
+ f"Center=({self.dLongitude_center_degree:.6f}, {self.dLatitude_center_degree:.6f}), "
196
+ f"Level={self.dggrid_level}, Shape={self.dggrid_shape_type}, Area={self.dArea:.2f}m²)"
197
+ )
198
+
199
+ def _determine_shape_type(self) -> None:
200
+ """
201
+ Determine the shape type based on the number of edges.
202
+
203
+ Sets the dggrid_shape_type attribute based on edge count.
204
+ """
205
+ shape_map = {
206
+ 3: "triangle",
207
+ 4: "quadrilateral",
208
+ 5: "pentagon",
209
+ 6: "hexagon",
210
+ 7: "heptagon",
211
+ 8: "octagon",
212
+ }
213
+ self.dggrid_shape_type = shape_map.get(
214
+ self.nEdge, f"{self.nEdge}-sided polygon"
215
+ )
216
+
217
+ def set_dggrid_properties(
218
+ self, level: int = -1, grid_id: str = "", resolution: float = -1.0
219
+ ) -> None:
220
+ """
221
+ Set DGGRID-specific properties for the cell.
222
+
223
+ Args:
224
+ level (int): Hierarchical level in the DGGRID system
225
+ grid_id (str): Unique identifier within the DGGRID system
226
+ resolution (float): Resolution class of the DGGRID cell
227
+
228
+ Raises:
229
+ TypeError: If parameters are not of the expected types
230
+ ValueError: If level or resolution are negative when provided
231
+ """
232
+ if not isinstance(level, (int, np.integer)):
233
+ raise TypeError(f"Level must be an integer, got {type(level)}")
234
+ if not isinstance(grid_id, str):
235
+ raise TypeError(f"Grid ID must be a string, got {type(grid_id)}")
236
+ if not isinstance(resolution, (int, float, np.number)):
237
+ raise TypeError(f"Resolution must be a number, got {type(resolution)}")
238
+
239
+ if level < -1:
240
+ raise ValueError(f"Level must be -1 (unset) or positive, got {level}")
241
+ if resolution < -1.0:
242
+ raise ValueError(
243
+ f"Resolution must be -1.0 (unset) or positive, got {resolution}"
244
+ )
245
+
246
+ self.dggrid_level = int(level)
247
+ self.dggrid_id = str(grid_id)
248
+ self.dggrid_resolution = float(resolution)
249
+
250
+ def calculate_cell_bound(self) -> Tuple[float, float, float, float]:
251
+ """
252
+ Calculate the bounding box of the DGGRID cell.
253
+
254
+ Computes the minimum and maximum longitude and latitude values
255
+ from all vertices to create a bounding rectangle for spatial indexing.
256
+
257
+ Returns:
258
+ Tuple[float, float, float, float]: Bounding box as (min_lon, min_lat, max_lon, max_lat)
259
+ """
260
+ if not self.aVertex or len(self.aVertex) == 0:
261
+ raise ValueError("Cannot calculate bounds: no vertices defined")
262
+
263
+ dLat_min = 90.0
264
+ dLat_max = -90.0
265
+ dLon_min = 180.0
266
+ dLon_max = -180.0
267
+
268
+ for vertex in self.aVertex:
269
+ if hasattr(vertex, "dLongitude_degree") and hasattr(
270
+ vertex, "dLatitude_degree"
271
+ ):
272
+ dLon_max = max(dLon_max, vertex.dLongitude_degree)
273
+ dLon_min = min(dLon_min, vertex.dLongitude_degree)
274
+ dLat_max = max(dLat_max, vertex.dLatitude_degree)
275
+ dLat_min = min(dLat_min, vertex.dLatitude_degree)
276
+ else:
277
+ raise ValueError(
278
+ "Vertex must have dLongitude_degree and dLatitude_degree attributes"
279
+ )
280
+
281
+ self.pBound = (dLon_min, dLat_min, dLon_max, dLat_max)
282
+ return self.pBound
283
+
284
+ def has_this_edge(self, pEdge_in) -> int:
285
+ """
286
+ Check whether the DGGRID cell contains a specific edge.
287
+
288
+ Args:
289
+ pEdge_in: The edge to be checked
290
+
291
+ Returns:
292
+ int: 1 if found, 0 if not found
293
+
294
+ Raises:
295
+ ValueError: If pEdge_in is None
296
+ """
297
+ if pEdge_in is None:
298
+ raise ValueError("Edge to check cannot be None")
299
+
300
+ iFlag_found = 0
301
+ for pEdge in self.aEdge:
302
+ if hasattr(pEdge, "is_overlap") and pEdge.is_overlap(pEdge_in):
303
+ iFlag_found = 1
304
+ break
305
+
306
+ return iFlag_found
307
+
308
+ def which_edge_cross_this_vertex(self, pVertex_in) -> Tuple[int, Optional[Any]]:
309
+ """
310
+ Find which edge overlaps with a vertex.
311
+
312
+ Args:
313
+ pVertex_in: The vertex to be checked
314
+
315
+ Returns:
316
+ Tuple[int, Optional[Any]]: (1 if found with edge object, 0 if not found with None)
317
+
318
+ Raises:
319
+ ValueError: If pVertex_in is None
320
+ """
321
+ if pVertex_in is None:
322
+ raise ValueError("Vertex to check cannot be None")
323
+
324
+ iFlag_found = 0
325
+ pEdge_out = None
326
+
327
+ for pEdge in self.aEdge:
328
+ if hasattr(pEdge, "check_vertex_on_edge"):
329
+ iFlag, dummy, diff = pEdge.check_vertex_on_edge(pVertex_in)
330
+ if iFlag == 1:
331
+ iFlag_found = 1
332
+ pEdge_out = pEdge
333
+ break
334
+
335
+ return iFlag_found, pEdge_out
336
+
337
+ def calculate_cell_area(self) -> float:
338
+ """
339
+ Calculate the area of the DGGRID cell.
340
+
341
+ Uses the polygon area calculation based on the vertex coordinates.
342
+ This method works for any polygon shape with 3 or more vertices.
343
+
344
+ Returns:
345
+ float: The area in square meters
346
+
347
+ Raises:
348
+ ValueError: If vertices are not properly defined
349
+ """
350
+ if not self.aVertex or len(self.aVertex) == 0:
351
+ raise ValueError("Cannot calculate area: no vertices defined")
352
+
353
+ lons = []
354
+ lats = []
355
+
356
+ for vertex in self.aVertex:
357
+ if hasattr(vertex, "dLongitude_degree") and hasattr(
358
+ vertex, "dLatitude_degree"
359
+ ):
360
+ lons.append(vertex.dLongitude_degree)
361
+ lats.append(vertex.dLatitude_degree)
362
+ else:
363
+ raise ValueError(
364
+ "Vertex must have dLongitude_degree and dLatitude_degree attributes"
365
+ )
366
+
367
+ if len(lons) < 3:
368
+ raise ValueError(
369
+ f"DGGRID cell must have at least 3 vertices, got {len(lons)}"
370
+ )
371
+
372
+ self.dArea = calculate_polygon_area(lons, lats)
373
+ return self.dArea
374
+
375
+ def calculate_edge_length(self) -> float:
376
+ """
377
+ Calculate the effective edge length of the DGGRID cell.
378
+
379
+ The calculation method depends on the shape type:
380
+ - For hexagonal cells: Uses hexagon-specific formula
381
+ - For other shapes: Uses square root of area as characteristic length
382
+
383
+ Returns:
384
+ float: The effective edge length in meters
385
+
386
+ Raises:
387
+ ValueError: If area is negative or not calculated
388
+ """
389
+ if self.dArea < 0:
390
+ raise ValueError("Cannot calculate edge length: area is negative")
391
+ if self.dArea == 0:
392
+ # Calculate area first if not done
393
+ self.calculate_cell_area()
394
+
395
+ if self.dggrid_shape_type == "hexagon":
396
+ # Formula for regular hexagon: A = (3√3/2) * s²
397
+ # Solving for s: s = √(2A / (3√3))
398
+ dLength_edge = np.sqrt(2.0 * self.dArea / (3.0 * np.sqrt(3.0)))
399
+ else:
400
+ # For other shapes, use square root of area as characteristic length
401
+ dLength_edge = np.sqrt(self.dArea)
402
+
403
+ self.dLength = dLength_edge
404
+ return dLength_edge
405
+
406
+ def share_edge(self, other: "pydggrid") -> int:
407
+ """
408
+ Check whether this DGGRID cell shares an edge with another DGGRID cell.
409
+
410
+ Two DGGRID cells share an edge if any of their boundary edges overlap.
411
+ This is essential for determining adjacency in hierarchical grids.
412
+
413
+ Args:
414
+ other (pydggrid): The other DGGRID cell to check
415
+
416
+ Returns:
417
+ int: 1 if cells share an edge, 0 if not
418
+
419
+ Raises:
420
+ TypeError: If other is not a pydggrid instance
421
+ ValueError: If other is None
422
+ """
423
+ if other is None:
424
+ raise ValueError("Other cell cannot be None")
425
+ if not isinstance(other, pydggrid):
426
+ raise TypeError(
427
+ f"Other cell must be a pydggrid instance, got {type(other)}"
428
+ )
429
+
430
+ iFlag_share = 0
431
+ for pEdge in self.aEdge:
432
+ for pEdge2 in other.aEdge:
433
+ if hasattr(pEdge, "is_overlap") and hasattr(pEdge2, "is_overlap"):
434
+ if pEdge.is_overlap(pEdge2) == 1:
435
+ iFlag_share = 1
436
+ break
437
+ if iFlag_share == 1:
438
+ break
439
+
440
+ return iFlag_share
441
+
442
+ def get_dggrid_properties(self) -> Dict[str, Any]:
443
+ """
444
+ Get properties specific to the DGGRID cell.
445
+
446
+ Returns:
447
+ dict: Dictionary containing DGGRID-specific properties including:
448
+ - level: Hierarchical level
449
+ - grid_id: DGGRID identifier
450
+ - resolution: Cell resolution
451
+ - shape_type: Polygon shape description
452
+ - edge_count: Number of edges/vertices
453
+ - area: Cell area
454
+ - characteristic_length: Effective cell size
455
+ """
456
+ properties = {
457
+ "level": self.dggrid_level,
458
+ "grid_id": self.dggrid_id,
459
+ "resolution": self.dggrid_resolution,
460
+ "shape_type": self.dggrid_shape_type,
461
+ "edge_count": self.nEdge,
462
+ "vertex_count": self.nVertex,
463
+ "area": self.dArea,
464
+ "characteristic_length": self.dLength,
465
+ }
466
+ return properties
467
+
468
+ def get_hierarchical_neighbors(self, level_offset: int = 0) -> List[str]:
469
+ """
470
+ Get potential neighbor cell IDs at a different hierarchical level.
471
+
472
+ Args:
473
+ level_offset (int): Offset from current level (0 = same level, +1 = finer, -1 = coarser)
474
+
475
+ Returns:
476
+ List[str]: List of potential neighbor grid IDs (implementation dependent)
477
+
478
+ Note:
479
+ This is a placeholder method. Actual implementation would depend on
480
+ the specific DGGRID system being used (ISEA, H3, etc.)
481
+ """
482
+ # This would need to be implemented based on the specific DGGRID system
483
+ # For now, return empty list as placeholder
484
+ target_level = self.dggrid_level + level_offset
485
+ if target_level < 0:
486
+ return []
487
+
488
+ # Placeholder implementation
489
+ return []
490
+
491
+ def is_at_pole(self, tolerance: float = 0.01) -> bool:
492
+ """
493
+ Check if the DGGRID cell is near a pole.
494
+
495
+ Args:
496
+ tolerance (float): Tolerance in degrees for pole proximity
497
+
498
+ Returns:
499
+ bool: True if cell is within tolerance of north or south pole
500
+ """
501
+ return (
502
+ abs(self.dLatitude_center_degree - 90.0) < tolerance
503
+ or abs(self.dLatitude_center_degree + 90.0) < tolerance
504
+ )
505
+
506
+ def get_corner_coordinates(self) -> List[Tuple[float, float]]:
507
+ """
508
+ Get the coordinates of all corners of the DGGRID cell.
509
+
510
+ Returns:
511
+ List[Tuple[float, float]]: List of (longitude, latitude) tuples for each corner
512
+ """
513
+ corners = []
514
+ for vertex in self.aVertex:
515
+ if hasattr(vertex, "dLongitude_degree") and hasattr(
516
+ vertex, "dLatitude_degree"
517
+ ):
518
+ corners.append((vertex.dLongitude_degree, vertex.dLatitude_degree))
519
+ else:
520
+ raise ValueError(
521
+ "Vertex must have dLongitude_degree and dLatitude_degree attributes"
522
+ )
523
+
524
+ return corners
525
+
526
+ def is_valid(self) -> bool:
527
+ """
528
+ Check if the DGGRID cell has valid attributes.
529
+
530
+ A DGGRID cell is considered valid if:
531
+ - It has valid center coordinates
532
+ - It has at least 3 boundary vertices and edges
533
+ - It has a positive area
534
+ - It has a valid cell ID
535
+ - All vertices and edges are properly defined
536
+ - DGGRID-specific properties are consistent
537
+
538
+ Returns:
539
+ bool: True if cell has valid DGGRID attributes
540
+ """
541
+ # Check parent class validity
542
+ if not super().is_valid():
543
+ return False
544
+
545
+ # Check DGGRID-specific requirements
546
+ has_min_edges = len(self.aEdge) >= 3
547
+ has_min_vertices = len(self.aVertex) >= 3
548
+ has_positive_length = self.dLength > 0
549
+ has_valid_level = self.dggrid_level >= -1 # -1 means unset
550
+
551
+ # Check that all vertices have required attributes
552
+ vertices_valid = True
553
+ for vertex in self.aVertex:
554
+ if not (
555
+ hasattr(vertex, "dLongitude_degree")
556
+ and hasattr(vertex, "dLatitude_degree")
557
+ ):
558
+ vertices_valid = False
559
+ break
560
+
561
+ return (
562
+ has_min_edges
563
+ and has_min_vertices
564
+ and has_positive_length
565
+ and has_valid_level
566
+ and vertices_valid
567
+ )
568
+
569
+ def copy(self) -> "pydggrid":
570
+ """
571
+ Create a deep copy of the DGGRID cell.
572
+
573
+ Returns:
574
+ pydggrid: A new DGGRID cell object with the same attributes
575
+ """
576
+ # Create new DGGRID cell with same basic parameters
577
+ new_dggrid = pydggrid(
578
+ self.dLongitude_center_degree,
579
+ self.dLatitude_center_degree,
580
+ self.aEdge.copy(),
581
+ self.aVertex.copy(),
582
+ )
583
+
584
+ # Copy all attributes from parent and this class
585
+ for attr_name, attr_value in self.__dict__.items():
586
+ if hasattr(new_dggrid, attr_name):
587
+ if isinstance(attr_value, list) and attr_value is not None:
588
+ setattr(new_dggrid, attr_name, attr_value.copy())
589
+ else:
590
+ setattr(new_dggrid, attr_name, attr_value)
591
+
592
+ return new_dggrid
593
+
594
+ def tojson(self) -> str:
595
+ """
596
+ Convert DGGRID cell object to a JSON string.
597
+
598
+ Serializes all cell attributes including neighbor information and
599
+ geometric properties. Uses the custom DggridClassEncoder to handle
600
+ numpy data types and complex objects.
601
+
602
+ Returns:
603
+ str: JSON string representation of the DGGRID cell
604
+
605
+ Example:
606
+ >>> dggrid_cell = pydggrid(-77.0, 38.0, edges, vertices)
607
+ >>> json_str = dggrid_cell.tojson()
608
+ """
609
+ aSkip = [
610
+ "aEdge",
611
+ "aFlowline",
612
+ "dLongitude_radian",
613
+ "dLatitude_radian",
614
+ "wkt",
615
+ "pPoint_start",
616
+ "pPoint_end",
617
+ "pBound",
618
+ ]
619
+
620
+ obj = self.__dict__.copy()
621
+ for sKey in aSkip:
622
+ obj.pop(sKey, None)
623
+
624
+ sJson = json.dumps(
625
+ obj, sort_keys=True, indent=4, ensure_ascii=True, cls=DggridClassEncoder
626
+ )
627
+ return sJson