icon-grid-generator 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.
- grid_generator/__init__.py +19 -0
- grid_generator/_geometry.py +50 -0
- grid_generator/_io.py +21 -0
- grid_generator/_limited_area.py +383 -0
- grid_generator/_metrics.py +32 -0
- grid_generator/_ordering.py +64 -0
- grid_generator/_refinement.py +30 -0
- grid_generator/_topology.py +52 -0
- grid_generator/_torus.py +248 -0
- grid_generator/_types.py +54 -0
- grid_generator/_validation.py +50 -0
- grid_generator/grid_generator.py +1917 -0
- icon_grid_generator-0.1.0.dist-info/METADATA +110 -0
- icon_grid_generator-0.1.0.dist-info/RECORD +16 -0
- icon_grid_generator-0.1.0.dist-info/WHEEL +4 -0
- icon_grid_generator-0.1.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Pure Python ICON RxxByy geodesic grid generation."""
|
|
2
|
+
|
|
3
|
+
from .grid_generator import (
|
|
4
|
+
IconGrid,
|
|
5
|
+
IconGridOptions,
|
|
6
|
+
GlobalGridSpec,
|
|
7
|
+
LimitedAreaGridSpec,
|
|
8
|
+
TorusGridSpec,
|
|
9
|
+
generate_grid,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"IconGrid",
|
|
14
|
+
"IconGridOptions",
|
|
15
|
+
"GlobalGridSpec",
|
|
16
|
+
"LimitedAreaGridSpec",
|
|
17
|
+
"TorusGridSpec",
|
|
18
|
+
"generate_grid",
|
|
19
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Spherical icosahedral geometry construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ._types import GeometryData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SphericalIcosahedralGeometry:
|
|
13
|
+
"""Build global triangular RxxByy geometry on a sphere."""
|
|
14
|
+
|
|
15
|
+
def build(self, spec: Any, options: Any) -> GeometryData:
|
|
16
|
+
from . import grid_generator as gg
|
|
17
|
+
|
|
18
|
+
base_vertices, faces = gg._icosahedron()
|
|
19
|
+
vertices = base_vertices
|
|
20
|
+
cells = np.asarray(
|
|
21
|
+
[gg._orient_cell(tuple(face), vertices) for face in faces],
|
|
22
|
+
dtype=np.int32,
|
|
23
|
+
)
|
|
24
|
+
if spec.root > 1:
|
|
25
|
+
vertices, cells = gg._refine_triangles(vertices, cells, spec.root)
|
|
26
|
+
for _ in range(spec.bisections):
|
|
27
|
+
vertices, cells = gg._refine_triangles(vertices, cells, 2)
|
|
28
|
+
|
|
29
|
+
vertices = gg._rotate_points(
|
|
30
|
+
vertices,
|
|
31
|
+
options.rotation_axis,
|
|
32
|
+
options.rotation_angle_degrees,
|
|
33
|
+
)
|
|
34
|
+
vertices = vertices * options.radius
|
|
35
|
+
gg._check_expected_counts(spec, vertices, cells)
|
|
36
|
+
|
|
37
|
+
vertex_lon, vertex_lat = gg._lon_lat(vertices)
|
|
38
|
+
cell_center_xyz = gg._cell_centers(vertices, cells, options.radius)
|
|
39
|
+
lon, lat = gg._lon_lat(cell_center_xyz)
|
|
40
|
+
return GeometryData(
|
|
41
|
+
vertices=vertices,
|
|
42
|
+
cells=cells,
|
|
43
|
+
lon=lon,
|
|
44
|
+
lat=lat,
|
|
45
|
+
vertex_lon=vertex_lon,
|
|
46
|
+
vertex_lat=vertex_lat,
|
|
47
|
+
cell_center_xyz=cell_center_xyz,
|
|
48
|
+
cell_vertex_lon=vertex_lon[cells],
|
|
49
|
+
cell_vertex_lat=vertex_lat[cells],
|
|
50
|
+
)
|
grid_generator/_io.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ICON NetCDF writing boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IconNetcdfWriter:
|
|
10
|
+
"""Write complete ICON grid objects to NetCDF."""
|
|
11
|
+
|
|
12
|
+
def write(
|
|
13
|
+
self,
|
|
14
|
+
grid: Any,
|
|
15
|
+
path: str | Path,
|
|
16
|
+
*,
|
|
17
|
+
sphere_radius: float | None = None,
|
|
18
|
+
) -> Path:
|
|
19
|
+
from . import grid_generator as gg
|
|
20
|
+
|
|
21
|
+
return gg._write_icon_grid(grid, path, sphere_radius=sphere_radius)
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Limited-area grids extracted from generated global grids."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ._types import GeometryData, MetricsData, RefinementData, TopologyData
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LimitedAreaExtractor:
|
|
14
|
+
"""Extract a compact limited-area grid from a generated global parent."""
|
|
15
|
+
|
|
16
|
+
def build(self, spec: Any, options: Any) -> tuple[GeometryData, TopologyData, MetricsData, RefinementData]:
|
|
17
|
+
from . import grid_generator as gg
|
|
18
|
+
|
|
19
|
+
parent = gg.generate_grid(spec.parent_grid_name, options=options)
|
|
20
|
+
selected = _selected_cells(parent, spec)
|
|
21
|
+
selected = _expand_cells(parent, selected, spec.boundary_depth)
|
|
22
|
+
if not selected:
|
|
23
|
+
raise ValueError("limited-area selection does not contain any cells")
|
|
24
|
+
ordered_parent_cells = _order_cells_by_boundary(parent, selected)
|
|
25
|
+
geometry = _compact_geometry(parent, ordered_parent_cells)
|
|
26
|
+
topology = _open_topology(parent, geometry, ordered_parent_cells, options)
|
|
27
|
+
metrics = _limited_metrics(parent, geometry, topology, options.sphere_radius)
|
|
28
|
+
refinement = _limited_refinement(parent, geometry, topology, ordered_parent_cells)
|
|
29
|
+
return geometry, topology, metrics, refinement
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _selected_cells(parent: Any, spec: Any) -> set[int]:
|
|
33
|
+
lon_min = spec.lon_min
|
|
34
|
+
lon_max = spec.lon_max
|
|
35
|
+
if lon_min <= lon_max:
|
|
36
|
+
lon_mask = (parent.lon >= lon_min) & (parent.lon <= lon_max)
|
|
37
|
+
else:
|
|
38
|
+
lon_mask = (parent.lon >= lon_min) | (parent.lon <= lon_max)
|
|
39
|
+
lat_mask = (parent.lat >= spec.lat_min) & (parent.lat <= spec.lat_max)
|
|
40
|
+
return set(np.nonzero(lon_mask & lat_mask)[0].astype(int))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _expand_cells(parent: Any, selected: set[int], depth: int) -> set[int]:
|
|
44
|
+
expanded = set(selected)
|
|
45
|
+
frontier = set(selected)
|
|
46
|
+
for _ in range(depth):
|
|
47
|
+
next_frontier: set[int] = set()
|
|
48
|
+
for cell in frontier:
|
|
49
|
+
next_frontier.update(int(neighbor) for neighbor in parent.icon_connectivity["c2c"][cell])
|
|
50
|
+
next_frontier -= expanded
|
|
51
|
+
expanded.update(next_frontier)
|
|
52
|
+
frontier = next_frontier
|
|
53
|
+
return expanded
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _order_cells_by_boundary(parent: Any, selected: set[int]) -> np.ndarray:
|
|
57
|
+
boundary = {
|
|
58
|
+
cell
|
|
59
|
+
for cell in selected
|
|
60
|
+
if any(int(neighbor) not in selected for neighbor in parent.icon_connectivity["c2c"][cell])
|
|
61
|
+
}
|
|
62
|
+
levels = {cell: 0 for cell in boundary}
|
|
63
|
+
queue = deque(boundary)
|
|
64
|
+
while queue:
|
|
65
|
+
cell = queue.popleft()
|
|
66
|
+
for neighbor in parent.icon_connectivity["c2c"][cell]:
|
|
67
|
+
neighbor = int(neighbor)
|
|
68
|
+
if neighbor in selected and neighbor not in levels:
|
|
69
|
+
levels[neighbor] = levels[cell] + 1
|
|
70
|
+
queue.append(neighbor)
|
|
71
|
+
return np.asarray(
|
|
72
|
+
sorted(selected, key=lambda cell: (levels.get(cell, 10**9), cell)),
|
|
73
|
+
dtype=np.int32,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _compact_geometry(parent: Any, parent_cells: np.ndarray) -> GeometryData:
|
|
78
|
+
source_vertices = np.asarray(
|
|
79
|
+
sorted({int(vertex) for cell in parent.cells[parent_cells] for vertex in cell}),
|
|
80
|
+
dtype=np.int32,
|
|
81
|
+
)
|
|
82
|
+
vertex_map = {int(source): local for local, source in enumerate(source_vertices)}
|
|
83
|
+
cells = np.asarray(
|
|
84
|
+
[[vertex_map[int(vertex)] for vertex in parent.cells[cell]] for cell in parent_cells],
|
|
85
|
+
dtype=np.int32,
|
|
86
|
+
)
|
|
87
|
+
return GeometryData(
|
|
88
|
+
vertices=parent.vertices[source_vertices],
|
|
89
|
+
cells=cells,
|
|
90
|
+
lon=parent.lon[parent_cells],
|
|
91
|
+
lat=parent.lat[parent_cells],
|
|
92
|
+
vertex_lon=parent.vertex_lon[source_vertices],
|
|
93
|
+
vertex_lat=parent.vertex_lat[source_vertices],
|
|
94
|
+
cell_center_xyz=parent.cell_center_xyz[parent_cells],
|
|
95
|
+
cell_vertex_lon=parent.cell_vertex_lon[parent_cells],
|
|
96
|
+
cell_vertex_lat=parent.cell_vertex_lat[parent_cells],
|
|
97
|
+
source_cell_index=parent_cells,
|
|
98
|
+
source_vertex_index=source_vertices,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _open_topology(
|
|
103
|
+
parent: Any,
|
|
104
|
+
geometry: GeometryData,
|
|
105
|
+
parent_cells: np.ndarray,
|
|
106
|
+
options: Any,
|
|
107
|
+
) -> TopologyData:
|
|
108
|
+
from . import grid_generator as gg
|
|
109
|
+
|
|
110
|
+
edge_ids: dict[tuple[int, int], int] = {}
|
|
111
|
+
edges: list[tuple[int, int]] = []
|
|
112
|
+
edge_cells: list[list[int]] = []
|
|
113
|
+
source_edge_ids: list[int] = []
|
|
114
|
+
parent_edge_lookup = {
|
|
115
|
+
tuple(sorted((int(v0), int(v1)))): edge_index
|
|
116
|
+
for edge_index, (v0, v1) in enumerate(parent.edges)
|
|
117
|
+
}
|
|
118
|
+
source_vertex_to_local = {
|
|
119
|
+
int(source): local for local, source in enumerate(geometry.source_vertex_index)
|
|
120
|
+
}
|
|
121
|
+
cell_edges = np.empty((geometry.cells.shape[0], 3), dtype=np.int32)
|
|
122
|
+
for local_cell, cell in enumerate(geometry.cells):
|
|
123
|
+
for local_edge, pair in enumerate(((cell[0], cell[1]), (cell[1], cell[2]), (cell[2], cell[0]))):
|
|
124
|
+
key = tuple(sorted((int(pair[0]), int(pair[1]))))
|
|
125
|
+
edge_id = edge_ids.get(key)
|
|
126
|
+
if edge_id is None:
|
|
127
|
+
edge_id = len(edges)
|
|
128
|
+
edge_ids[key] = edge_id
|
|
129
|
+
edges.append(key)
|
|
130
|
+
edge_cells.append([local_cell])
|
|
131
|
+
source_pair = tuple(
|
|
132
|
+
sorted(
|
|
133
|
+
int(geometry.source_vertex_index[vertex])
|
|
134
|
+
for vertex in key
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
source_edge_ids.append(parent_edge_lookup[source_pair])
|
|
138
|
+
else:
|
|
139
|
+
edge_cells[edge_id].append(local_cell)
|
|
140
|
+
cell_edges[local_cell, local_edge] = edge_id
|
|
141
|
+
|
|
142
|
+
edge_cell_array = np.full((len(edges), 2), -1, dtype=np.int32)
|
|
143
|
+
for edge_id, adjacent in enumerate(edge_cells):
|
|
144
|
+
edge_cell_array[edge_id, : len(adjacent)] = adjacent
|
|
145
|
+
|
|
146
|
+
edges_array = np.asarray(edges, dtype=np.int32)
|
|
147
|
+
edge_center_xyz = parent.edge_center_xyz[source_edge_ids]
|
|
148
|
+
edge_lon = parent.edge_lon[source_edge_ids]
|
|
149
|
+
edge_lat = parent.edge_lat[source_edge_ids]
|
|
150
|
+
icon_connectivity = _open_icon_connectivity(
|
|
151
|
+
geometry.vertices,
|
|
152
|
+
geometry.cells,
|
|
153
|
+
geometry.cell_center_xyz,
|
|
154
|
+
edges_array,
|
|
155
|
+
cell_edges,
|
|
156
|
+
edge_cell_array,
|
|
157
|
+
)
|
|
158
|
+
del gg, options, parent_cells, source_vertex_to_local
|
|
159
|
+
return TopologyData(
|
|
160
|
+
edges=edges_array,
|
|
161
|
+
cell_edges=cell_edges,
|
|
162
|
+
edge_cells=edge_cell_array,
|
|
163
|
+
edge_center_xyz=edge_center_xyz,
|
|
164
|
+
edge_lon=edge_lon,
|
|
165
|
+
edge_lat=edge_lat,
|
|
166
|
+
icon_connectivity=icon_connectivity,
|
|
167
|
+
connectivity=_open_public_connectivity(geometry.cells, edges_array, edge_cell_array, icon_connectivity),
|
|
168
|
+
neighbor_tables=_open_neighbor_tables(geometry.cells, edges_array, edge_cell_array, icon_connectivity),
|
|
169
|
+
source_edge_index=np.asarray(source_edge_ids, dtype=np.int32),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _open_icon_connectivity(
|
|
174
|
+
vertices: np.ndarray,
|
|
175
|
+
cells: np.ndarray,
|
|
176
|
+
cell_center_xyz: np.ndarray,
|
|
177
|
+
edges: np.ndarray,
|
|
178
|
+
cell_edges: np.ndarray,
|
|
179
|
+
edge_cells: np.ndarray,
|
|
180
|
+
) -> dict[str, np.ndarray]:
|
|
181
|
+
from . import grid_generator as gg
|
|
182
|
+
|
|
183
|
+
n_vertices = vertices.shape[0]
|
|
184
|
+
c2e = np.asarray(cell_edges, dtype=np.int32)
|
|
185
|
+
c2c = np.full_like(c2e, -1)
|
|
186
|
+
orientation = np.empty_like(c2e)
|
|
187
|
+
for cell_index in range(cells.shape[0]):
|
|
188
|
+
for local_index, edge_index in enumerate(c2e[cell_index]):
|
|
189
|
+
adjacent = edge_cells[edge_index]
|
|
190
|
+
if adjacent[1] < 0:
|
|
191
|
+
c2c[cell_index, local_index] = -1
|
|
192
|
+
else:
|
|
193
|
+
c2c[cell_index, local_index] = (
|
|
194
|
+
adjacent[1] if adjacent[0] == cell_index else adjacent[0]
|
|
195
|
+
)
|
|
196
|
+
orientation[cell_index, local_index] = 1 if adjacent[0] == cell_index else -1
|
|
197
|
+
|
|
198
|
+
incident_cells: list[list[int]] = [[] for _ in range(n_vertices)]
|
|
199
|
+
incident_edges: list[list[int]] = [[] for _ in range(n_vertices)]
|
|
200
|
+
incident_vertices: list[list[int]] = [[] for _ in range(n_vertices)]
|
|
201
|
+
for cell_index, cell in enumerate(cells):
|
|
202
|
+
for vertex in cell:
|
|
203
|
+
incident_cells[int(vertex)].append(cell_index + 1)
|
|
204
|
+
for edge_index, (v0, v1) in enumerate(edges):
|
|
205
|
+
incident_edges[int(v0)].append(edge_index + 1)
|
|
206
|
+
incident_edges[int(v1)].append(edge_index + 1)
|
|
207
|
+
incident_vertices[int(v0)].append(int(v1) + 1)
|
|
208
|
+
incident_vertices[int(v1)].append(int(v0) + 1)
|
|
209
|
+
|
|
210
|
+
v2c = np.zeros((n_vertices, 6), dtype=np.int32)
|
|
211
|
+
v2e = np.zeros((n_vertices, 6), dtype=np.int32)
|
|
212
|
+
v2v = np.zeros((n_vertices, 6), dtype=np.int32)
|
|
213
|
+
edge_orientation = np.zeros((n_vertices, 6), dtype=np.int32)
|
|
214
|
+
edge_lookup = {edge_id + 1: tuple(edge) for edge_id, edge in enumerate(edges)}
|
|
215
|
+
edge_centers = gg._edge_centers(vertices, edges, 1.0)
|
|
216
|
+
unit_centers = gg._normalize_rows(cell_center_xyz)
|
|
217
|
+
for vertex in range(n_vertices):
|
|
218
|
+
ordered_vertices = gg._sort_around_vertex(vertices, vertex, incident_vertices[vertex])
|
|
219
|
+
ordered_edges = gg._sort_around_vertex(vertices, vertex, incident_edges[vertex], points=edge_centers)
|
|
220
|
+
ordered_cells = gg._sort_around_vertex(vertices, vertex, incident_cells[vertex], points=unit_centers)
|
|
221
|
+
v2v[vertex, : len(ordered_vertices)] = ordered_vertices
|
|
222
|
+
v2e[vertex, : len(ordered_edges)] = ordered_edges
|
|
223
|
+
v2c[vertex, : len(ordered_cells)] = ordered_cells
|
|
224
|
+
for pos, edge_id in enumerate(ordered_edges):
|
|
225
|
+
edge = edge_lookup[edge_id]
|
|
226
|
+
edge_orientation[vertex, pos] = 1 if edge[0] == vertex else -1
|
|
227
|
+
return {
|
|
228
|
+
"c2e": c2e,
|
|
229
|
+
"c2c": c2c,
|
|
230
|
+
"v2c": v2c,
|
|
231
|
+
"v2e": v2e,
|
|
232
|
+
"v2v": v2v,
|
|
233
|
+
"orientation_of_normal": orientation,
|
|
234
|
+
"edge_orientation": edge_orientation,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _open_public_connectivity(
|
|
239
|
+
cells: np.ndarray,
|
|
240
|
+
edges: np.ndarray,
|
|
241
|
+
edge_cells: np.ndarray,
|
|
242
|
+
icon_connectivity: dict[str, np.ndarray],
|
|
243
|
+
) -> dict[str, np.ndarray]:
|
|
244
|
+
from . import grid_generator as gg
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
"edge_of_cell": icon_connectivity["c2e"],
|
|
248
|
+
"vertex_of_cell": cells,
|
|
249
|
+
"neighbor_cell_index": icon_connectivity["c2c"],
|
|
250
|
+
"adjacent_cell_of_edge": edge_cells,
|
|
251
|
+
"edge_vertices": edges,
|
|
252
|
+
"cells_of_vertex": gg._zero_based_with_skip(icon_connectivity["v2c"]),
|
|
253
|
+
"edges_of_vertex": gg._zero_based_with_skip(icon_connectivity["v2e"]),
|
|
254
|
+
"vertices_of_vertex": gg._zero_based_with_skip(icon_connectivity["v2v"]),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _open_neighbor_tables(
|
|
259
|
+
cells: np.ndarray,
|
|
260
|
+
edges: np.ndarray,
|
|
261
|
+
edge_cells: np.ndarray,
|
|
262
|
+
icon_connectivity: dict[str, np.ndarray],
|
|
263
|
+
) -> dict[str, np.ndarray]:
|
|
264
|
+
from . import grid_generator as gg
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"c2e2c": icon_connectivity["c2c"],
|
|
268
|
+
"c2e": icon_connectivity["c2e"],
|
|
269
|
+
"e2c": np.asarray(edge_cells, dtype=np.int32),
|
|
270
|
+
"v2e": gg._zero_based_with_skip(icon_connectivity["v2e"]),
|
|
271
|
+
"v2c": gg._zero_based_with_skip(icon_connectivity["v2c"]),
|
|
272
|
+
"c2v": np.asarray(cells, dtype=np.int32),
|
|
273
|
+
"v2e2v": gg._zero_based_with_skip(icon_connectivity["v2v"]),
|
|
274
|
+
"e2v": np.asarray(edges, dtype=np.int32),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _limited_metrics(parent: Any, geometry: GeometryData, topology: TopologyData, sphere_radius: float) -> MetricsData:
|
|
279
|
+
from . import grid_generator as gg
|
|
280
|
+
|
|
281
|
+
source_edges = topology.source_edge_index
|
|
282
|
+
edge_lengths = parent.geometry["edge_length"][source_edges]
|
|
283
|
+
dual_edge_lengths = parent.geometry["dual_edge_length"][source_edges].copy()
|
|
284
|
+
edge_cell_distance = np.empty((topology.edges.shape[0], 2), dtype=np.float64)
|
|
285
|
+
for edge_index, adjacent in enumerate(topology.edge_cells):
|
|
286
|
+
for side in range(2):
|
|
287
|
+
if adjacent[side] >= 0:
|
|
288
|
+
center = geometry.cell_center_xyz[adjacent[side]][np.newaxis, :]
|
|
289
|
+
edge_center = topology.edge_center_xyz[edge_index][np.newaxis, :]
|
|
290
|
+
edge_cell_distance[edge_index, side] = gg._edge_cell_distances(
|
|
291
|
+
center,
|
|
292
|
+
np.array([[0, 0]], dtype=np.int32),
|
|
293
|
+
edge_center,
|
|
294
|
+
sphere_radius,
|
|
295
|
+
)[0, 0]
|
|
296
|
+
else:
|
|
297
|
+
edge_cell_distance[edge_index, side] = edge_cell_distance[edge_index, 0]
|
|
298
|
+
dual_edge_lengths[edge_index] = 2.0 * edge_cell_distance[edge_index, 0]
|
|
299
|
+
edge_system_orientation = np.ones(topology.edges.shape[0], dtype=np.int32)
|
|
300
|
+
normals = gg._edge_normal_fields(
|
|
301
|
+
geometry.vertices,
|
|
302
|
+
topology.edges,
|
|
303
|
+
topology.edge_center_xyz,
|
|
304
|
+
edge_system_orientation,
|
|
305
|
+
)
|
|
306
|
+
cell_areas = parent.geometry["cell_area"][geometry.source_cell_index]
|
|
307
|
+
return MetricsData(
|
|
308
|
+
fields={
|
|
309
|
+
"cell_area": cell_areas,
|
|
310
|
+
"dual_area": gg._dual_areas(geometry.vertices.shape[0], geometry.cells, cell_areas),
|
|
311
|
+
"edge_length": edge_lengths,
|
|
312
|
+
"dual_edge_length": dual_edge_lengths,
|
|
313
|
+
"edge_cell_distance": edge_cell_distance,
|
|
314
|
+
"edge_vert_distance": np.column_stack((edge_lengths * 0.5, edge_lengths * 0.5)),
|
|
315
|
+
"orientation_of_normal": topology.icon_connectivity["orientation_of_normal"],
|
|
316
|
+
"edge_system_orientation": edge_system_orientation,
|
|
317
|
+
"edge_orientation": topology.icon_connectivity["edge_orientation"],
|
|
318
|
+
"edgequad_area": 0.5 * edge_lengths * dual_edge_lengths,
|
|
319
|
+
**normals,
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _limited_refinement(
|
|
325
|
+
parent: Any,
|
|
326
|
+
geometry: GeometryData,
|
|
327
|
+
topology: TopologyData,
|
|
328
|
+
parent_cells: np.ndarray,
|
|
329
|
+
) -> RefinementData:
|
|
330
|
+
from . import grid_generator as gg
|
|
331
|
+
|
|
332
|
+
n_cells = geometry.cells.shape[0]
|
|
333
|
+
n_edges = topology.edges.shape[0]
|
|
334
|
+
n_vertices = geometry.vertices.shape[0]
|
|
335
|
+
boundary_distance = _boundary_distance(topology)
|
|
336
|
+
refin_c_ctrl = np.asarray(boundary_distance + 1, dtype=np.int32)
|
|
337
|
+
edge_ctrl = np.zeros(n_edges, dtype=np.int32)
|
|
338
|
+
for edge_index, adjacent in enumerate(topology.edge_cells):
|
|
339
|
+
active = adjacent[adjacent >= 0]
|
|
340
|
+
edge_ctrl[edge_index] = int(np.max(refin_c_ctrl[active]))
|
|
341
|
+
vertex_ctrl = np.zeros(n_vertices, dtype=np.int32)
|
|
342
|
+
for cell_index, cell in enumerate(geometry.cells):
|
|
343
|
+
vertex_ctrl[cell] = np.maximum(vertex_ctrl[cell], refin_c_ctrl[cell_index])
|
|
344
|
+
return RefinementData(
|
|
345
|
+
fields={
|
|
346
|
+
"refin_c_ctrl": refin_c_ctrl,
|
|
347
|
+
"refin_e_ctrl": edge_ctrl,
|
|
348
|
+
"refin_v_ctrl": vertex_ctrl,
|
|
349
|
+
"start_idx_c": gg._start_index_fixed("cell_grf", n_cells),
|
|
350
|
+
"end_idx_c": gg._end_index_fixed("cell_grf", n_cells),
|
|
351
|
+
"start_idx_e": gg._start_index_fixed("edge_grf", n_edges),
|
|
352
|
+
"end_idx_e": gg._end_index_fixed("edge_grf", n_edges),
|
|
353
|
+
"start_idx_v": gg._start_index_fixed("vert_grf", n_vertices),
|
|
354
|
+
"end_idx_v": gg._end_index_fixed("vert_grf", n_vertices),
|
|
355
|
+
"parent_cell_index": parent_cells.astype(np.int32) + 1,
|
|
356
|
+
"parent_cell_type": np.zeros(n_cells, dtype=np.int32),
|
|
357
|
+
"edge_parent_type": np.zeros(n_edges, dtype=np.int32),
|
|
358
|
+
"parent_edge_index": topology.source_edge_index.astype(np.int32) + 1,
|
|
359
|
+
"parent_vertex_index": geometry.source_vertex_index.astype(np.int32) + 1,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _boundary_distance(topology: TopologyData) -> np.ndarray:
|
|
365
|
+
boundary_cells = {
|
|
366
|
+
int(cell)
|
|
367
|
+
for adjacent in topology.edge_cells
|
|
368
|
+
if adjacent[1] < 0
|
|
369
|
+
for cell in adjacent
|
|
370
|
+
if cell >= 0
|
|
371
|
+
}
|
|
372
|
+
distances = np.full(topology.cell_edges.shape[0], -1, dtype=np.int32)
|
|
373
|
+
queue = deque(boundary_cells)
|
|
374
|
+
for cell in boundary_cells:
|
|
375
|
+
distances[cell] = 0
|
|
376
|
+
while queue:
|
|
377
|
+
cell = queue.popleft()
|
|
378
|
+
for neighbor in topology.icon_connectivity["c2c"][cell]:
|
|
379
|
+
neighbor = int(neighbor)
|
|
380
|
+
if neighbor >= 0 and distances[neighbor] < 0:
|
|
381
|
+
distances[neighbor] = distances[cell] + 1
|
|
382
|
+
queue.append(neighbor)
|
|
383
|
+
return distances
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Metric and orientation field computation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._types import GeometryData, MetricsData, TopologyData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SphericalMetricsBuilder:
|
|
11
|
+
"""Compute ICON metric fields for a spherical triangular grid."""
|
|
12
|
+
|
|
13
|
+
def build(
|
|
14
|
+
self,
|
|
15
|
+
options: Any,
|
|
16
|
+
geometry: GeometryData,
|
|
17
|
+
topology: TopologyData,
|
|
18
|
+
) -> MetricsData:
|
|
19
|
+
from . import grid_generator as gg
|
|
20
|
+
|
|
21
|
+
return MetricsData(
|
|
22
|
+
fields=gg._geometry_fields(
|
|
23
|
+
geometry.vertices,
|
|
24
|
+
geometry.cells,
|
|
25
|
+
geometry.cell_center_xyz,
|
|
26
|
+
topology.edges,
|
|
27
|
+
topology.edge_cells,
|
|
28
|
+
topology.edge_center_xyz,
|
|
29
|
+
topology.icon_connectivity,
|
|
30
|
+
options.sphere_radius,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Ordering helpers matching ICON grid-generator conventions where possible."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ._types import GeometryData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CHILD_ORDER = {
|
|
13
|
+
200: 0,
|
|
14
|
+
201: 1,
|
|
15
|
+
202: 2,
|
|
16
|
+
203: 3,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FortranOrderingBuilder:
|
|
21
|
+
"""Apply deterministic child ordering used by ICON's Fortran grid generator."""
|
|
22
|
+
|
|
23
|
+
def order_spherical_bisection(self, spec: Any, options: Any, geometry: GeometryData) -> GeometryData:
|
|
24
|
+
if getattr(spec, "bisections", 0) == 0:
|
|
25
|
+
return geometry
|
|
26
|
+
|
|
27
|
+
from . import grid_generator as gg
|
|
28
|
+
|
|
29
|
+
parent = gg.generate_grid(
|
|
30
|
+
f"R{spec.root:02d}B{spec.bisections - 1:02d}",
|
|
31
|
+
options=options,
|
|
32
|
+
)
|
|
33
|
+
parent_vertex_index = gg._parent_vertex_indices(geometry.vertices, parent)
|
|
34
|
+
parent_cell_index, parent_cell_type = gg._parent_cell_fields(
|
|
35
|
+
geometry.cells,
|
|
36
|
+
parent_vertex_index,
|
|
37
|
+
parent,
|
|
38
|
+
)
|
|
39
|
+
child_order = np.asarray(
|
|
40
|
+
[CHILD_ORDER[int(child_type)] for child_type in parent_cell_type],
|
|
41
|
+
dtype=np.int32,
|
|
42
|
+
)
|
|
43
|
+
permutation = np.lexsort((child_order, parent_cell_index))
|
|
44
|
+
return _permute_cells(geometry, permutation)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _permute_cells(geometry: GeometryData, permutation: np.ndarray) -> GeometryData:
|
|
48
|
+
return GeometryData(
|
|
49
|
+
vertices=geometry.vertices,
|
|
50
|
+
cells=geometry.cells[permutation],
|
|
51
|
+
lon=geometry.lon[permutation],
|
|
52
|
+
lat=geometry.lat[permutation],
|
|
53
|
+
vertex_lon=geometry.vertex_lon,
|
|
54
|
+
vertex_lat=geometry.vertex_lat,
|
|
55
|
+
cell_center_xyz=geometry.cell_center_xyz[permutation],
|
|
56
|
+
cell_vertex_lon=geometry.cell_vertex_lon[permutation],
|
|
57
|
+
cell_vertex_lat=geometry.cell_vertex_lat[permutation],
|
|
58
|
+
source_cell_index=(
|
|
59
|
+
None
|
|
60
|
+
if geometry.source_cell_index is None
|
|
61
|
+
else geometry.source_cell_index[permutation]
|
|
62
|
+
),
|
|
63
|
+
source_vertex_index=geometry.source_vertex_index,
|
|
64
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Refinement-control and parent-provenance field computation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._types import GeometryData, RefinementData, TopologyData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GlobalRefinementBuilder:
|
|
11
|
+
"""Compute parent-provenance fields for global bisection refinement."""
|
|
12
|
+
|
|
13
|
+
def build(
|
|
14
|
+
self,
|
|
15
|
+
spec: Any,
|
|
16
|
+
options: Any,
|
|
17
|
+
geometry: GeometryData,
|
|
18
|
+
topology: TopologyData,
|
|
19
|
+
) -> RefinementData:
|
|
20
|
+
from . import grid_generator as gg
|
|
21
|
+
|
|
22
|
+
return RefinementData(
|
|
23
|
+
fields=gg._refinement_fields(
|
|
24
|
+
spec,
|
|
25
|
+
options,
|
|
26
|
+
geometry.vertices,
|
|
27
|
+
geometry.cells,
|
|
28
|
+
topology.edges,
|
|
29
|
+
)
|
|
30
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Topology construction for closed triangular spherical grids."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ._types import GeometryData, TopologyData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GlobalTopologyBuilder:
|
|
11
|
+
"""Build global edge and adjacency tables from triangular cells."""
|
|
12
|
+
|
|
13
|
+
def build(self, spec: Any, options: Any, geometry: GeometryData) -> TopologyData:
|
|
14
|
+
from . import grid_generator as gg
|
|
15
|
+
|
|
16
|
+
edges, cell_edges, edge_cells = gg._build_edges(geometry.cells)
|
|
17
|
+
if edges.shape[0] != spec.expected_edges:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
f"generated {edges.shape[0]} edges, expected {spec.expected_edges}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
edge_center_xyz = gg._edge_centers(geometry.vertices, edges, options.radius)
|
|
23
|
+
edge_lon, edge_lat = gg._lon_lat(edge_center_xyz)
|
|
24
|
+
icon_connectivity = gg._icon_connectivity(
|
|
25
|
+
geometry.vertices,
|
|
26
|
+
geometry.cells,
|
|
27
|
+
geometry.cell_center_xyz,
|
|
28
|
+
edges,
|
|
29
|
+
cell_edges,
|
|
30
|
+
edge_cells,
|
|
31
|
+
)
|
|
32
|
+
return TopologyData(
|
|
33
|
+
edges=edges,
|
|
34
|
+
cell_edges=cell_edges,
|
|
35
|
+
edge_cells=edge_cells,
|
|
36
|
+
edge_center_xyz=edge_center_xyz,
|
|
37
|
+
edge_lon=edge_lon,
|
|
38
|
+
edge_lat=edge_lat,
|
|
39
|
+
icon_connectivity=icon_connectivity,
|
|
40
|
+
connectivity=gg._public_connectivity(
|
|
41
|
+
geometry.cells,
|
|
42
|
+
edges,
|
|
43
|
+
edge_cells,
|
|
44
|
+
icon_connectivity,
|
|
45
|
+
),
|
|
46
|
+
neighbor_tables=gg._neighbor_tables(
|
|
47
|
+
geometry.cells,
|
|
48
|
+
edges,
|
|
49
|
+
edge_cells,
|
|
50
|
+
icon_connectivity,
|
|
51
|
+
),
|
|
52
|
+
)
|