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.
@@ -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
+ )