Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f21b246
Add torus support to geometry
msimberg Nov 19, 2025
5b14541
Apply suggestion from @msimberg
msimberg Nov 19, 2025
bc7e2bc
Add torus support for interpolation fields
msimberg Nov 20, 2025
65bdfc3
Update metrics fields tests for torus grids
msimberg Nov 21, 2025
521968d
Merge branch 'main' into better-torus-support-metrics
jcanton Dec 1, 2025
4c73db2
Merge remote-tracking branch 'origin/better-torus-support-geometry' i…
jcanton Dec 1, 2025
afaf759
merge back Icosahedron and Torus Geometry classes
jcanton Dec 1, 2025
1d67822
put these back
jcanton Dec 2, 2025
6f98c70
clean up and add TODOs from github
jcanton Dec 2, 2025
cedf32a
clean GeometryType: either TORUS or ICOSAHEDRON, not None anymore
jcanton Dec 2, 2025
f4173f4
add links to updstream MRs
jcanton Dec 2, 2025
e3eee2a
geometry_type() can return None
jcanton Dec 2, 2025
b36d5fa
update grid_manager test for torus indexes
jcanton Dec 2, 2025
21e1219
if damping_height is above model_top, reset it
jcanton Dec 2, 2025
e0ced8f
GeometryType can never be None (the type checker would make sure of t…
jcanton Dec 2, 2025
0469778
now that GeometryType cannot be None, assert_never(s) can be removed
jcanton Dec 2, 2025
47e95ce
actually better without the Nones and TORUS(r=0,b=0)
jcanton Dec 3, 2025
3bf84dd
fix tests for GridSubdivision != None
jcanton Dec 3, 2025
76678a4
remove two more assert_none
jcanton Dec 3, 2025
b2e86c5
add type?
jcanton Dec 3, 2025
8bd2207
I think this comment is useful
jcanton Dec 8, 2025
4535bad
remove assert_never
jcanton Dec 8, 2025
682db97
renaming
jcanton Dec 8, 2025
ee51e8a
small cleanup
jcanton Dec 9, 2025
65eef62
remove TODOs: all others work with _torus, these too ok
jcanton Dec 9, 2025
7da8935
remove one other case
jcanton Dec 9, 2025
f3b4b03
ruff formatting
jcanton Dec 9, 2025
ac2ed72
revert these changes, I cannot get rid of the Nones
jcanton Dec 9, 2025
d6974b7
more reverting to Nones
jcanton Dec 9, 2025
2b1d139
pre-commit
jcanton Dec 9, 2025
86c2190
add gauss3d to diffusion tests
jcanton Dec 9, 2025
fd46530
Merge branch 'main' into better-torus-support-metrics
jcanton Dec 9, 2025
9a640ab
ooops conflicts left
jcanton Dec 9, 2025
fd03b20
more merge fixes
jcanton Dec 9, 2025
b113457
pre-commit fixes
jcanton Dec 9, 2025
67988f8
more pre-commit
jcanton Dec 9, 2025
3d44585
no exchange here
jcanton Dec 9, 2025
d4b5ed9
torus was missing from here
jcanton Dec 9, 2025
e030b4d
fix torus metrics config and remove sketchy defaults
jcanton Dec 9, 2025
db33932
rename for consistency
jcanton Dec 9, 2025
f124ade
ruff
jcanton Dec 9, 2025
0ebe72c
small atol increase for mac
jcanton Dec 10, 2025
47b6940
unused import
jcanton Dec 11, 2025
60ebdf9
remove actually unused vn_only after discussion with @egparedes
jcanton Dec 11, 2025
ed78a24
apply @yiluchen1066 review
jcanton Dec 23, 2025
0218c56
pre-commit
jcanton Dec 23, 2025
ded15dc
Merge branch 'main' into better-torus-support-metrics
jcanton Dec 23, 2025
20c2b12
apply @msimberg review
jcanton Dec 23, 2025
a5f4c8d
remove unused input and hopefully fix bug showing up with cupy
jcanton Dec 24, 2025
48b5806
put this back for consistency with icosahedron
jcanton Dec 24, 2025
188d235
fix wrong docstring (thanks @OngChia) and make returns consistent
jcanton Dec 29, 2025
d490191
apply @ongchia review
jcanton Dec 29, 2025
0dbbe5a
possible second bugfix for array_ns.full
jcanton Dec 29, 2025
c0420f0
pre-commit
jcanton Dec 29, 2025
9642322
one more commit
jcanton Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 236 additions & 75 deletions model/common/src/icon4py/model/common/grid/geometry.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@
EDGE_LENGTH: Final[str] = "edge_length"
DUAL_EDGE_LENGTH: Final[str] = "length_of_dual_edge"

EDGE_TANGENT_X: Final[str] = "x_component_of_edge_tangential_unit_vector"
EDGE_TANGENT_Y: Final[str] = "y_component_of_edge_tangential_unit_vector"
EDGE_TANGENT_Z: Final[str] = "z_component_of_edge_tangential_unit_vector"
EDGE_TANGENT_VERTEX_U: Final[str] = "eastward_component_of_edge_tangent_on_vertex"
EDGE_TANGENT_VERTEX_V: Final[str] = "northward_component_of_edge_tangent_on_vertex"
EDGE_TANGENT_CELL_U: Final[str] = "eastward_component_of_edge_tangent_on_cell"
Expand All @@ -63,6 +60,9 @@
EDGE_NORMAL_Z: Final[str] = "z_component_of_edge_normal_unit_vector"
EDGE_NORMAL_U: Final[str] = "eastward_component_of_edge_normal"
EDGE_NORMAL_V: Final[str] = "northward_component_of_edge_normal"
EDGE_TANGENT_X: Final[str] = "x_component_of_edge_tangential_unit_vector"
EDGE_TANGENT_Y: Final[str] = "y_component_of_edge_tangential_unit_vector"
EDGE_TANGENT_Z: Final[str] = "z_component_of_edge_tangential_unit_vector"
EDGE_DUAL_U: Final[str] = "eastward_component_of_edge_tangent"
EDGE_DUAL_V: Final[str] = "northward_component_of_edge_tangent"
EDGE_NORMAL_VERTEX_U: Final[str] = "eastward_component_of_edge_normal_on_vertex"
Expand Down
281 changes: 264 additions & 17 deletions model/common/src/icon4py/model/common/grid/geometry_stencils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from types import ModuleType

import gt4py.next.typing as gtx_typing
import numpy as np
from gt4py import next as gtx
from gt4py.next import sin, where
Expand All @@ -17,12 +18,14 @@
from icon4py.model.common.math.helpers import (
arc_length_on_edges,
cross_product_on_edges,
diff_on_edges_torus,
distance_on_edges_torus,
geographical_to_cartesian_on_edges,
geographical_to_cartesian_on_vertices,
normalize_cartesian_vector_on_edges,
zonal_and_meridional_components_on_edges,
)
from icon4py.model.common.utils import data_allocation as alloc
from icon4py.model.common.utils import data_allocation as data_alloc


@gtx.field_operator(grid_type=gtx.GridType.UNSTRUCTURED)
Expand Down Expand Up @@ -56,6 +59,51 @@ def cartesian_coordinates_of_edge_tangent(
return normalize_cartesian_vector_on_edges(x, y, z)


@gtx.field_operator(grid_type=gtx.GridType.UNSTRUCTURED)
def cartesian_coordinates_of_edge_tangent_torus(
vertex_x: fa.VertexField[ta.wpfloat],
vertex_y: fa.VertexField[ta.wpfloat],
edge_orientation: fa.EdgeField[ta.wpfloat],
domain_length: ta.wpfloat,
domain_height: ta.wpfloat,
) -> tuple[
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
]:
"""
Compute normalized cartesian vector tangential to an edge on a torus grid.

This is done by taking the difference between the two vertices adjacent to the edge
t = d(v1, v2)

Args:
vertex_x: x coordinates of vertices
vertex_y: y coordinates of vertices
edge_orientation: encoding of the edge orientation: (-1, +1) depending on whether the
edge is directed from first to second neighbor of vice versa.
Returns:
x: x coordinate of normalized tangent vector
y: y coordinate of normalized tangent vector
z: z coordinate of normalized tangent vector
"""
xdiff, ydiff = diff_on_edges_torus(
vertex_x(E2V[0]),
vertex_x(E2V[1]),
vertex_y(E2V[0]),
vertex_y(E2V[1]),
domain_length,
domain_height,
)
x = edge_orientation * xdiff
y = edge_orientation * ydiff
z = 0.0 * x
# TODO(msimberg): This should use something like numpy.zeros_like if and
# when that becomes available in gt4py.

return normalize_cartesian_vector_on_edges(x, y, z)


@gtx.field_operator
def cartesian_coordinates_of_edge_normal(
edge_lat: fa.EdgeField[ta.wpfloat],
Expand Down Expand Up @@ -93,6 +141,32 @@ def cartesian_coordinates_of_edge_normal(
return normalize_cartesian_vector_on_edges(x, y, z)


@gtx.field_operator
def cartesian_coordinates_of_edge_normal_torus(
edge_tangent_x: fa.EdgeField[ta.wpfloat],
edge_tangent_y: fa.EdgeField[ta.wpfloat],
) -> tuple[
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
]:
"""
Compute the normal to the edge tangent vector on a torus grid.

Args:
edge_tangent_x: x coordinate of the tangent
edge_tangent_y: y coordinate of the tangent
Returns:
edge_normal_x: x coordinate of the normal
edge_normal_y: y coordinate of the normal
edge_normal_z: y coordinate of the normal
"""
z = 0.0 * edge_tangent_x
# TODO(msimberg): This should use something like numpy.zeros_like if and
# when that becomes available in gt4py.
return normalize_cartesian_vector_on_edges(-edge_tangent_y, edge_tangent_x, z)


@gtx.field_operator
def cartesian_coordinates_edge_tangent_and_normal(
vertex_lat: fa.VertexField[ta.wpfloat],
Expand Down Expand Up @@ -123,6 +197,59 @@ def cartesian_coordinates_edge_tangent_and_normal(
return tangent_x, tangent_y, tangent_z, normal_x, normal_y, normal_z


@gtx.field_operator
def cartesian_coordinates_edge_tangent_and_normal_torus(
vertex_x: fa.VertexField[ta.wpfloat],
vertex_y: fa.VertexField[ta.wpfloat],
edge_x: fa.EdgeField[ta.wpfloat],
edge_y: fa.EdgeField[ta.wpfloat],
edge_orientation: fa.EdgeField[ta.wpfloat],
domain_length: ta.wpfloat,
domain_height: ta.wpfloat,
) -> tuple[
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
fa.EdgeField[ta.wpfloat],
]:
"""Compute normalized cartesian vectors of edge tangent and edge normal."""
tangent_x, tangent_y, tangent_z = cartesian_coordinates_of_edge_tangent_torus(
vertex_x,
vertex_y,
edge_orientation,
domain_length,
domain_height,
)
tangent_u = tangent_x
tangent_v = tangent_y

normal_x, normal_y, normal_z = cartesian_coordinates_of_edge_normal_torus(
tangent_x,
tangent_y,
)
normal_u = normal_x
normal_v = normal_y

return (
tangent_x,
tangent_y,
tangent_z,
tangent_u,
tangent_v,
normal_x,
normal_y,
normal_z,
normal_u,
normal_v,
)
Comment on lines +239 to +250
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here why we cannot just return the components on x and y. I think u and v are just the copy of them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could but it does not make a difference in terms of memory usage to do it here (and doing it in other places makes it less readable IMO)



@gtx.program(grid_type=gtx.GridType.UNSTRUCTURED)
def compute_cartesian_coordinates_of_edge_tangent_and_normal(
vertex_lat: fa.VertexField[ta.wpfloat],
Expand Down Expand Up @@ -150,6 +277,52 @@ def compute_cartesian_coordinates_of_edge_tangent_and_normal(
)


@gtx.program(grid_type=gtx.GridType.UNSTRUCTURED)
def compute_cartesian_coordinates_of_edge_tangent_and_normal_torus(
vertex_x: fa.VertexField[ta.wpfloat],
vertex_y: fa.VertexField[ta.wpfloat],
edge_x: fa.EdgeField[ta.wpfloat],
edge_y: fa.EdgeField[ta.wpfloat],
edge_orientation: fa.EdgeField[ta.wpfloat],
tangent_x: fa.EdgeField[ta.wpfloat],
tangent_y: fa.EdgeField[ta.wpfloat],
tangent_z: fa.EdgeField[ta.wpfloat],
tangent_u: fa.EdgeField[ta.wpfloat],
tangent_v: fa.EdgeField[ta.wpfloat],
normal_x: fa.EdgeField[ta.wpfloat],
normal_y: fa.EdgeField[ta.wpfloat],
normal_z: fa.EdgeField[ta.wpfloat],
normal_u: fa.EdgeField[ta.wpfloat],
normal_v: fa.EdgeField[ta.wpfloat],
domain_length: ta.wpfloat,
domain_height: ta.wpfloat,
horizontal_start: gtx.int32,
horizontal_end: gtx.int32,
):
cartesian_coordinates_edge_tangent_and_normal_torus(
vertex_x,
vertex_y,
edge_x,
edge_y,
edge_orientation,
domain_length,
domain_height,
out=(
tangent_x,
tangent_y,
tangent_z,
tangent_u,
tangent_v,
normal_x,
normal_y,
normal_z,
normal_u,
normal_v,
),
domain={dims.EdgeDim: (horizontal_start, horizontal_end)},
)


@gtx.field_operator(grid_type=gtx.GridType.UNSTRUCTURED)
def zonal_and_meridional_component_of_edge_field_at_vertex(
vertex_lat: fa.VertexField[ta.wpfloat],
Expand Down Expand Up @@ -377,14 +550,14 @@ def arc_distance_of_far_edges_in_diamond(

Neighboring edges of an edge span up a diamond with 4 edges (E2C2E) and 4 vertices (E2C2V). Here we compute the
arc length between the two vertices in this diamond that are not directly connected to the edge:
between d(v1, v3)
v1-------v4
between d(v2, v3)
v2-------v1
| /|
| / |
| e |
| / |
|/ |
v2 ------v3
v0 ------v3



Expand All @@ -398,16 +571,47 @@ def arc_distance_of_far_edges_in_diamond(

"""
x, y, z = geographical_to_cartesian_on_vertices(vertex_lat, vertex_lon)
x2 = x(E2C2V[2])
x3 = x(E2C2V[3])
y2 = y(E2C2V[2])
y3 = y(E2C2V[3])
z2 = z(E2C2V[2])
z3 = z(E2C2V[3])
# (xi, yi, zi) are normalized by construction
return arc_length_on_edges(
x(E2C2V[2]),
x(E2C2V[3]),
y(E2C2V[2]),
y(E2C2V[3]),
z(E2C2V[2]),
z(E2C2V[3]),
radius,
)

far_vertex_vertex_length = arc_length_on_edges(x2, x3, y2, y3, z2, z3, radius)
return far_vertex_vertex_length

@gtx.field_operator
def distance_of_far_edges_in_diamond_torus(
vertex_x: fa.VertexField[ta.wpfloat],
vertex_y: fa.VertexField[ta.wpfloat],
domain_length: ta.wpfloat,
domain_height: ta.wpfloat,
) -> fa.EdgeField[ta.wpfloat]:
"""
Compute the distance between the "far" vertices of an edge on a torus grid.

See arc_distance_of_far_edges_in_diamond for details.

Args:
vertex_x: x coordinate of vertices
vertex_y: y coordinate of vertices
domain_length: length of the domain
domain_height: height of the domain

Returns:
distance between the "far" vertices in the diamond.

"""
return distance_on_edges_torus(
vertex_x(E2C2V[2]),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suddenly, I am thinking if is it better to define far indices in icon.py as a enum class or global constants or something else instead of putting a hardcoded number here, such as
E2C2V_NEIGHBOR_INDEX1 = 0
E2C2V_NEIGHBOR_INDEX2 = 1
E2C2V_FAR_INDEX1 = 2
E2C2V_FAR_INDEX2 = 3
(I do not remember whether gt4py allows that or not, and I put this comment because it seems that the figure in the docstring of arc_distance_of_far_edges_in_diamond seems to be inconsistent with the actual indices, leading to confusion. If we have a universal definition, then it will be clearer)

Copy link
Copy Markdown
Contributor Author

@jcanton jcanton Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the docstsring, nice catch!
I tried some ways (constants, enum class) to move the indexes to some named variable but gt4py doesn't like it, and I cannot find any other field_operator that does this so I am guessing this is not supported, right @havogt ?

vertex_x(E2C2V[3]),
vertex_y(E2C2V[2]),
vertex_y(E2C2V[3]),
domain_length,
domain_height,
)


@gtx.field_operator
Expand Down Expand Up @@ -502,6 +706,26 @@ def compute_arc_distance_of_far_edges_in_diamond(
)


@gtx.program(grid_type=gtx.GridType.UNSTRUCTURED)
def compute_distance_of_far_edges_in_diamond_torus(
vertex_x: fa.VertexField[ta.wpfloat],
vertex_y: fa.VertexField[ta.wpfloat],
domain_length: ta.wpfloat,
domain_height: ta.wpfloat,
far_vertex_distance: fa.EdgeField[ta.wpfloat],
horizontal_start: gtx.int32,
horizontal_end: gtx.int32,
):
distance_of_far_edges_in_diamond_torus(
vertex_x,
vertex_y,
domain_length,
domain_height,
out=far_vertex_distance,
domain={dims.EdgeDim: (horizontal_start, horizontal_end)},
)


@gtx.field_operator
def edge_area(
owner_mask: fa.EdgeField[bool],
Expand Down Expand Up @@ -557,6 +781,29 @@ def coriolis_parameter_on_edges(
return 2.0 * angular_velocity * sin(edge_center_lat)


def coriolis_parameter_on_edges_torus(
coriolis_coefficient: float,
num_edges: int,
backend: gtx_typing.Backend,
) -> fa.EdgeField[ta.wpfloat]:
"""
Create a coriolis parameter field on edges for a torus grid.
Args:
coriolis_coefficient: coriolis coefficient

Returns:
coriolis parameter
"""
xp = data_alloc.import_array_ns(backend)
coriolis_parameter = gtx.as_field(
(dims.EdgeDim,),
coriolis_coefficient * xp.ones(num_edges),
dtype=ta.wpfloat,
allocator=backend,
)
return coriolis_parameter


@gtx.program(grid_type=gtx.GridType.UNSTRUCTURED)
def compute_coriolis_parameter_on_edges(
edge_center_lat: fa.EdgeField[ta.wpfloat],
Expand All @@ -574,11 +821,11 @@ def compute_coriolis_parameter_on_edges(


def compute_primal_cart_normal(
primal_cart_normal_x: alloc.NDArray,
primal_cart_normal_y: alloc.NDArray,
primal_cart_normal_z: alloc.NDArray,
primal_cart_normal_x: data_alloc.NDArray,
primal_cart_normal_y: data_alloc.NDArray,
primal_cart_normal_z: data_alloc.NDArray,
array_ns: ModuleType = np,
) -> alloc.NDArray:
) -> data_alloc.NDArray:
primal_cart_normal = array_ns.transpose(
array_ns.stack(
(
Expand Down
Loading
Loading