Summary
Add support for mesh_layout="triangular" in create_all_graph_components(), creating regular triangular meshes using networkx.triangular_lattice_graph. This mirrors the existing rectilinear mesh functions with zero new dependencies.
Motivation
A regular triangular mesh has several advantages over rectilinear:
- 6-connectivity (vs 8 for rectilinear with diagonals) — more isotropic message passing
- Better approximation of circular receptive fields — equilateral triangles tile more uniformly than squares + diagonals
- Natural stepping stone toward icosahedral meshes on a sphere
- Relevant for LAM models — some regional NWP grids use triangular meshes
As discussed in #71, mesh_layout="triangular" means "uniform equilateral triangles in the coordinates of the provided CRS" — a regular planar grid, not Delaunay triangulation of irregular points.
Proposed API
# Flat triangular mesh
graph = create_all_graph_components(
coords=xy,
m2m_connectivity="flat",
m2m_connectivity_kwargs=dict(mesh_node_distance=3.0),
mesh_layout="triangular",
...
)
# Hierarchical triangular mesh
graph = create_all_graph_components(
coords=xy,
m2m_connectivity="hierarchical",
m2m_connectivity_kwargs=dict(mesh_node_distance=3.0, max_num_levels=3),
mesh_layout="triangular",
...
)
Implementation: Mirrored Function Pairs
The triangular layout mirrors the existing rectilinear functions exactly:
| Rectilinear (existing) |
Triangular (new) |
create_single_level_2d_mesh_graph uses networkx.grid_2d_graph (8-connectivity) |
create_single_level_2d_triangular_mesh_graph uses networkx.triangular_lattice_graph (6-connectivity) |
create_flat_singlescale_mesh_graph(xy, mesh_node_distance) |
create_flat_singlescale_triangular_mesh_graph(xy, mesh_node_distance) |
create_flat_multiscale_mesh_graph(xy, mesh_node_distance, ...) |
create_flat_multiscale_triangular_mesh_graph(xy, mesh_node_distance, ...) |
Core function: create_single_level_2d_triangular_mesh_graph
def create_single_level_2d_triangular_mesh_graph(xy, nx, ny):
"""
Create directed graph with triangular lattice topology.
Uses networkx.triangular_lattice_graph which produces offset rows
with equilateral triangle side length 1 and row spacing sqrt(3)/2.
"""
xm, xM = np.amin(xy[:, 0]), np.amax(xy[:, 0])
ym, yM = np.amin(xy[:, 1]), np.amax(xy[:, 1])
dx = (xM - xm) / nx
dy = (yM - ym) / ny
g = networkx.triangular_lattice_graph(ny, nx, with_positions=True)
# Scale and offset positions to cover the domain
for node in g.nodes:
pos = np.array(g.nodes[node]["pos"])
# Scale from unit triangles to domain
g.nodes[node]["pos"] = np.array([
xm + dx/2 + pos[0] * dx,
ym + dy/2 + pos[1] * dy
])
g.nodes[node]["type"] = "mesh"
# Convert to DiGraph with bidirectional edges
dg = networkx.DiGraph(g)
for u, v in g.edges():
d = np.sqrt(np.sum((dg.nodes[u]["pos"] - dg.nodes[v]["pos"]) ** 2))
dg.edges[u, v]["len"] = d
dg.edges[u, v]["vdiff"] = dg.nodes[u]["pos"] - dg.nodes[v]["pos"]
dg.add_edge(v, u)
dg.edges[v, u]["len"] = d
dg.edges[v, u]["vdiff"] = dg.nodes[v]["pos"] - dg.nodes[u]["pos"]
return dg
mesh_node_distance interface
Same as rectilinear. For triangular, mesh_node_distance controls the side length:
nx = int(range_x / mesh_node_distance)
ny = int(range_y / (mesh_node_distance * np.sqrt(3) / 2))
CRS Warning
If graph_crs.is_geographic == True and mesh_layout="triangular", emit a warning:
if graph_crs is not None and graph_crs.is_geographic and mesh_layout == "triangular":
warnings.warn(
"mesh_layout='triangular' produces non-uniform physical spacing in "
"geographic coordinates. Consider mesh_layout='icosahedral' for "
"uniform coverage on a sphere."
)
Depends On
- mesh_layout argument introduction issue
Related
Summary
Add support for mesh_layout="triangular" in
create_all_graph_components(), creating regular triangular meshes usingnetworkx.triangular_lattice_graph. This mirrors the existing rectilinear mesh functions with zero new dependencies.Motivation
A regular triangular mesh has several advantages over rectilinear:
As discussed in #71, mesh_layout="triangular" means "uniform equilateral triangles in the coordinates of the provided CRS" — a regular planar grid, not Delaunay triangulation of irregular points.
Proposed API
Implementation: Mirrored Function Pairs
The triangular layout mirrors the existing rectilinear functions exactly:
create_single_level_2d_mesh_graphusesnetworkx.grid_2d_graph(8-connectivity)create_single_level_2d_triangular_mesh_graphusesnetworkx.triangular_lattice_graph(6-connectivity)create_flat_singlescale_mesh_graph(xy, mesh_node_distance)create_flat_singlescale_triangular_mesh_graph(xy, mesh_node_distance)create_flat_multiscale_mesh_graph(xy, mesh_node_distance, ...)create_flat_multiscale_triangular_mesh_graph(xy, mesh_node_distance, ...)Core function: create_single_level_2d_triangular_mesh_graph
mesh_node_distanceinterfaceSame as rectilinear. For triangular,
mesh_node_distancecontrols the side length:CRS Warning
If
graph_crs.is_geographic == Trueand mesh_layout="triangular", emit a warning:Depends On
Related
neural-lam-dev#13— Reference implementation for triangular graphs