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.
- pyearthmesh/__init__.py +32 -0
- pyearthmesh/classes/__init__.py +19 -0
- pyearthmesh/classes/dggrid.py +627 -0
- pyearthmesh/classes/edge.py +398 -0
- pyearthmesh/classes/hexagon.py +572 -0
- pyearthmesh/classes/latlon.py +557 -0
- pyearthmesh/classes/link.py +562 -0
- pyearthmesh/classes/meshcell.py +648 -0
- pyearthmesh/classes/mpas.py +652 -0
- pyearthmesh/classes/square.py +533 -0
- pyearthmesh/classes/tin.py +721 -0
- pyearthmesh/classes/vertex.py +240 -0
- pyearthmesh/meshes/__init__.py +3 -0
- pyearthmesh/meshes/structured/__init__.py +3 -0
- pyearthmesh/meshes/structured/cubicsphere/__init__.py +3 -0
- pyearthmesh/meshes/structured/cubicsphere/create_cubicsphere_mesh.py +287 -0
- pyearthmesh/meshes/structured/dggs/__init__.py +3 -0
- pyearthmesh/meshes/structured/dggs/dggrid/__init__.py +3 -0
- pyearthmesh/meshes/structured/dggs/dggrid/create_dggrid_mesh.py +268 -0
- pyearthmesh/meshes/structured/dggs/healpix/__init__.py +3 -0
- pyearthmesh/meshes/structured/dggs/healpix/create_rhealpix_mesh.py +219 -0
- pyearthmesh/meshes/structured/dggs/isea/__init__.py +3 -0
- pyearthmesh/meshes/structured/dggs/isea/create_isea_mesh.py +221 -0
- pyearthmesh/meshes/structured/hexaon/__init__.py +1 -0
- pyearthmesh/meshes/structured/hexaon/create_hexagon_mesh.py +524 -0
- pyearthmesh/meshes/structured/latlon/__init__.py +1 -0
- pyearthmesh/meshes/structured/latlon/create_latlon_mesh.py +475 -0
- pyearthmesh/meshes/structured/square/__init__.py +0 -0
- pyearthmesh/meshes/structured/square/create_square_mesh.py +270 -0
- pyearthmesh/meshes/structured/triangular/__init__.py +3 -0
- pyearthmesh/meshes/structured/triangular/create_triangular_mesh.py +197 -0
- pyearthmesh/meshes/unstructured/__init__.py +3 -0
- pyearthmesh/meshes/unstructured/mpas/__init__.py +3 -0
- pyearthmesh/meshes/unstructured/mpas/create_mpas_mesh.py +784 -0
- pyearthmesh/meshes/unstructured/tin/__init__.py +3 -0
- pyearthmesh/meshes/unstructured/tin/create_tin_mesh.py +280 -0
- pyearthmesh/utility/__init__.py +1 -0
- pyearthmesh/utility/convert_attributes.py +57 -0
- pyearthmesh/utility/convert_coordinates.py +121 -0
- pyearthmesh/utility/extract_land_meshcells.py +202 -0
- pyearthmesh/utility/find_minimal_enclosing_polygon.py +28 -0
- pyearthmesh/utility/jigsaw/__init__.py +3 -0
- pyearthmesh/utility/jigsaw/inpoly2.py +221 -0
- pyearthmesh/utility/jigsaw/loadgeo.py +189 -0
- pyearthmesh/utility/jigsaw/loadgeo2.py +142 -0
- pyearthmesh/utility/jigsaw/run_jigsaw.py +1071 -0
- pyearthmesh/utility/jigsaw/saveesm.py +299 -0
- pyearthmesh/utility/jigsaw/savetin.py +82 -0
- pyearthmesh/utility/jigsaw/utility.py +155 -0
- pyearthmesh/utility/mesh_utility.py +703 -0
- pyearthmesh/utility/mpas_tools/__init__.py +3 -0
- pyearthmesh/utility/mpas_tools/compose.py +73 -0
- pyearthmesh/utility/mpas_tools/mpasmsh.py +299 -0
- pyearthmesh/utility/mpas_tools/spacing.py +263 -0
- pyearthmesh/utility/potentiometric/__init__.py +3 -0
- pyearthmesh/utility/potentiometric/calculate_potentiometric.py +49 -0
- pyearthmesh/utility/split_by_length.py +127 -0
- pyearthmesh-0.1.0.dist-info/METADATA +155 -0
- pyearthmesh-0.1.0.dist-info/RECORD +62 -0
- pyearthmesh-0.1.0.dist-info/WHEEL +5 -0
- pyearthmesh-0.1.0.dist-info/licenses/LICENSE +28 -0
- pyearthmesh-0.1.0.dist-info/top_level.txt +1 -0
pyearthmesh/__init__.py
ADDED
|
@@ -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
|