Skip to content

Commit d909a1c

Browse files
joeloskarssonsadamovleifdenby
authored
Allow non-regularly gridded and lat-lon coordinates (#32)
* Start looking into where xy has to be changed * Change shapes in docstrings * Make flat mesh graphs work with new coordinate layout * Merge PR #26 into branch * Fix coordinate handling in multirange graph creation * Rename grid_refinement_factor to mesh_node_distance * Fix existing tests to work with new coordinate format * Add test for irregularlygridded coordinates * Remove unneeded eps in mesh level calculation * Change documentation to use new format and arguments for coordinates * Fix bug in coordinate order for flat graphs * Start working on allowing latlon coordinates * Introduce coords and projection * Fix linting * Fix tests with coords keyword argument * Implement lat-lon transformation through projection * Add documentation page about graphs constructed using lat-lons * Adjust coords keyword arg in docs * Add test for lat-lon coordinates * Fix linting of docs * Merge main into branch * Fix typos and clarifications as suggested from code review Co-authored-by: sadamov <45732287+sadamov@users.noreply.github.com> * Change euclidean coordinates to Cartesian coordinates * Update src/weather_model_graphs/create/mesh/kinds/hierarchical.py Co-authored-by: Leif Denby <leif@denby.eu> * Clarify comments and variable names around mesh level computation * Add check for number of nodes in test with irregular coords * Update docs line on square meshes * Reference lat-lon notebook in coordinate section * Change projection spec to use pyproj crs:s * Adjust test to crs arguments * Fix linting * Update docs to crs change * Add cartopy dependency to visualization group * Update src/weather_model_graphs/create/base.py Co-authored-by: Leif Denby <leif@denby.eu> * Fix documentation * Add changelog entry * Update CHANGELOG.md Co-authored-by: Leif Denby <leif@denby.eu> * Trim whitespace --------- Co-authored-by: sadamov <45732287+sadamov@users.noreply.github.com> Co-authored-by: Leif Denby <leif@denby.eu>
1 parent a6d43e3 commit d909a1c

File tree

17 files changed

+529
-206
lines changed

17 files changed

+529
-206
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased](https://github.com/mllam/weather-model-graphs/compare/v0.2.0...HEAD)
99

10+
### Added
11+
12+
- Add coords_crs and graph_crs arguments to allow for using lat-lons coordinates
13+
or other CRSs as input. These are then converted to the specific CRS used when
14+
constructing the graph.
15+
[\#32](https://github.com/mllam/weather-model-graphs/pull/32), @joeloskarsson
16+
17+
### Changed
18+
19+
- Change coordinate input to array of shape [N_grid_points, 2] (was previously
20+
[2, Ny, Nx]), to allow for non-regularly gridded coordinates
21+
[\#32](https://github.com/mllam/weather-model-graphs/pull/32), @joeloskarsson
1022

1123
## [v0.2.0](https://github.com/mllam/weather-model-graphs/releases/tag/v0.2.0)
1224

docs/_toc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ chapters:
77
- file: background
88
- file: design
99
- file: creating_the_graph
10+
- file: lat_lons

docs/background.ipynb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@
3232
"\n",
3333
"# create some fake cartesian coordinates\n",
3434
"def _create_fake_xy(N=10):\n",
35-
" x = np.linspace(0.0, 1.0, N)\n",
36-
" y = np.linspace(0.0, 1.0, N)\n",
37-
" xy = np.stack(np.meshgrid(x, y), axis=0)\n",
35+
" x = np.linspace(0.0, N, N)\n",
36+
" y = np.linspace(0.0, N, N)\n",
37+
" xy_mesh = np.meshgrid(x, y)\n",
38+
" xy = np.stack([mg_coord.flatten() for mg_coord in xy_mesh], axis=1) # Shaped (N, 2)\n",
3839
" return xy\n",
3940
"\n",
4041
"\n",
41-
"xy_grid = _create_fake_xy(N=10)\n",
42+
"xy = _create_fake_xy(N=10)\n",
4243
"\n",
43-
"graph = wmg.create.archetype.create_keisler_graph(xy_grid=xy_grid)\n",
44+
"graph = wmg.create.archetype.create_keisler_graph(coords=xy)\n",
4445
"\n",
4546
"# remove all edges from the graph\n",
4647
"graph.remove_edges_from(list(graph.edges))\n",
@@ -85,9 +86,7 @@
8586
"source": [
8687
"import matplotlib.pyplot as plt\n",
8788
"\n",
88-
"graph = wmg.create.archetype.create_keisler_graph(\n",
89-
" xy_grid=xy_grid, grid_refinement_factor=2\n",
90-
")\n",
89+
"graph = wmg.create.archetype.create_keisler_graph(coords=xy, mesh_node_distance=2)\n",
9190
"graph_components = wmg.split_graph_by_edge_attribute(graph=graph, attr=\"component\")\n",
9291
"\n",
9392
"fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(16, 5))\n",

docs/creating_the_graph.ipynb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"source": [
2727
"# The grid nodes\n",
2828
"\n",
29-
"To get started we will create a set of fake grid nodes, which represent the geographical locations (lat/lon or x/y cartesian) where we have values for the physical fields. Here we'll just use cartesian coordinates for simplicity.\n"
29+
"To get started we will create a set of fake grid nodes, which represent the geographical locations where we have values for the physical fields. We will here work with cartesian x/y coordinates. See [this page](./lat_lons.ipynb) for how to use lat/lon coordinates in weather-model-graphs."
3030
]
3131
},
3232
{
@@ -47,10 +47,10 @@
4747
"outputs": [],
4848
"source": [
4949
"def _create_fake_xy(N=10):\n",
50-
" x = np.linspace(0, 1, N)\n",
51-
" y = np.linspace(0, 1, N)\n",
52-
" xy = np.meshgrid(x, y)\n",
53-
" xy = np.stack(xy, axis=0)\n",
50+
" x = np.linspace(0.0, N, N)\n",
51+
" y = np.linspace(0.0, N, N)\n",
52+
" xy_mesh = np.meshgrid(x, y)\n",
53+
" xy = np.stack([mg_coord.flatten() for mg_coord in xy_mesh], axis=1) # Shaped (N, 2)\n",
5454
" return xy"
5555
]
5656
},
@@ -63,7 +63,7 @@
6363
"xy = _create_fake_xy(32)\n",
6464
"\n",
6565
"fig, ax = plt.subplots()\n",
66-
"ax.scatter(xy[0], xy[1])\n",
66+
"ax.scatter(xy[:, 0], xy[:, 1])\n",
6767
"ax.set_aspect(1)"
6868
]
6969
},
@@ -80,7 +80,7 @@
8080
"cell_type": "markdown",
8181
"metadata": {},
8282
"source": [
83-
"Lets start with a simple mesh which only has nearest neighbour connections. At the moment `weather-model-graphs` creates a square mesh that sits within the spatial domain spanned by the grid nodes. Techniques for adding non-square meshes are in development."
83+
"Lets start with a simple mesh which only has nearest neighbour connections. At the moment `weather-model-graphs` creates a rectangular mesh that sits within the spatial domain spanned by the grid nodes (specifically within the axis-aligned bounding box of the grid nodes). Techniques for adding non-square meshes are in development."
8484
]
8585
},
8686
{
@@ -130,7 +130,7 @@
130130
"metadata": {},
131131
"outputs": [],
132132
"source": [
133-
"graph = wmg.create.archetype.create_keisler_graph(xy_grid=xy)\n",
133+
"graph = wmg.create.archetype.create_keisler_graph(coords=xy)\n",
134134
"graph"
135135
]
136136
},
@@ -200,7 +200,7 @@
200200
"metadata": {},
201201
"outputs": [],
202202
"source": [
203-
"graph = wmg.create.archetype.create_graphcast_graph(xy_grid=xy)"
203+
"graph = wmg.create.archetype.create_graphcast_graph(coords=xy)"
204204
]
205205
},
206206
{
@@ -275,7 +275,7 @@
275275
"metadata": {},
276276
"outputs": [],
277277
"source": [
278-
"graph = wmg.create.archetype.create_oskarsson_hierarchical_graph(xy_grid=xy)\n",
278+
"graph = wmg.create.archetype.create_oskarsson_hierarchical_graph(coords=xy)\n",
279279
"graph"
280280
]
281281
},
@@ -410,9 +410,9 @@
410410
"source": [
411411
"graph = wmg.create.create_all_graph_components(\n",
412412
" m2m_connectivity=\"flat_multiscale\",\n",
413-
" xy=xy,\n",
413+
" coords=xy,\n",
414414
" m2m_connectivity_kwargs=dict(\n",
415-
" grid_refinement_factor=2, level_refinement_factor=3, max_num_levels=None\n",
415+
" mesh_node_distance=2, level_refinement_factor=3, max_num_levels=None\n",
416416
" ),\n",
417417
" g2m_connectivity=\"nearest_neighbour\",\n",
418418
" m2g_connectivity=\"nearest_neighbour\",\n",
@@ -458,7 +458,7 @@
458458
"name": "python",
459459
"nbconvert_exporter": "python",
460460
"pygments_lexer": "ipython3",
461-
"version": "3.10.14"
461+
"version": "3.10.15"
462462
}
463463
},
464464
"nbformat": 4,

docs/lat_lons.ipynb

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "65f79439-0b75-483f-858b-932422f7599d",
6+
"metadata": {},
7+
"source": [
8+
"# Working with lat-lon coordinates\n",
9+
"\n",
10+
"In the previous sections we have considered grid point positions `coords` given as Cartesian coordinates. However, it is common that we have coordinates given as latitudes and longitudes. This notebook describes how we can constuct graphs directly using lat-lon coordinates. This is achieved by specifying the Coordinate Reference System (CRS) of `coords` and the CRS that the graph construction should be carried out in. `coords` will then be projected to this new CRS before any calculations are carried out."
11+
]
12+
},
13+
{
14+
"cell_type": "markdown",
15+
"id": "8cbcd3bd-a80c-41e4-a54b-bcfb5dfbb75c",
16+
"metadata": {},
17+
"source": [
18+
"## A motivating example"
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"id": "4c710a27-7e03-4f4f-a4dd-5d1c53a1e4eb",
24+
"metadata": {},
25+
"source": [
26+
"Let's start by defining some example lat-lons to use in our example. When using lat-lons the first column of `coords` should contain longitudes and the second column latitudes.\n",
27+
"\n",
28+
"In the example below we create lat-lons laid out around the geographic North Pole. These example points are equidistantly spaced, but this does not have to be the case."
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": null,
34+
"id": "9d577c2f-6dca-4b1b-8bdc-1359e6573cda",
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"import numpy as np\n",
39+
"import matplotlib.pyplot as plt\n",
40+
"import cartopy.crs as ccrs\n",
41+
"\n",
42+
"longitudes = np.linspace(-180, 180, 40)\n",
43+
"latitudes = np.linspace(65, 85, 5) # Very close to north pole\n",
44+
"\n",
45+
"meshgridded_lat_lons = np.meshgrid(longitudes, latitudes)\n",
46+
"coords = np.stack([mg_coord.flatten() for mg_coord in meshgridded_lat_lons], axis=1)\n",
47+
"\n",
48+
"fig, ax = plt.subplots(figsize=(15, 9), subplot_kw={\"projection\": ccrs.PlateCarree()})\n",
49+
"ax.scatter(coords[:, 0], coords[:, 1], marker=\".\")\n",
50+
"ax.coastlines()\n",
51+
"ax.set_extent((-180, 180, -90, 90))"
52+
]
53+
},
54+
{
55+
"cell_type": "markdown",
56+
"id": "4d4f5f35-01bf-4eeb-be52-d7deabbf2039",
57+
"metadata": {},
58+
"source": [
59+
"We first consider what happens if we directly feed these lat-lons as `coords`, treating them as if they were Cartesian coordinates. In this notebook we will only create flat \"Keisler-like\" graphs, but everything works analogously for the other graph types."
60+
]
61+
},
62+
{
63+
"cell_type": "code",
64+
"execution_count": null,
65+
"id": "8e04abfd-f3e2-4f77-908c-6e2374431e9e",
66+
"metadata": {},
67+
"outputs": [],
68+
"source": [
69+
"import weather_model_graphs as wmg\n",
70+
"\n",
71+
"graph = wmg.create.archetype.create_keisler_graph(coords, mesh_node_distance=10)\n",
72+
"fig, ax = plt.subplots(figsize=(15, 9), subplot_kw={\"projection\": ccrs.PlateCarree()})\n",
73+
"wmg.visualise.nx_draw_with_pos_and_attr(\n",
74+
" graph, ax=ax, node_size=30, edge_color_attr=\"component\", node_color_attr=\"type\"\n",
75+
")\n",
76+
"ax.coastlines()\n",
77+
"ax.set_extent((-180, 180, -90, 90))"
78+
]
79+
},
80+
{
81+
"cell_type": "markdown",
82+
"id": "65b39a86-d64f-46a8-a732-0e608a3fb07f",
83+
"metadata": {},
84+
"source": [
85+
"This creates a useable mesh graph, but we can note a few problems with it:\n",
86+
"\n",
87+
"* There are no connections between nodes around longitude -180/180, i.e. the periodicity of longitude is not considered.\n",
88+
"* All nodes at the top of the plot, close to the pole, are actually very close spatially. Yet there are no connections between them.\n",
89+
"\n",
90+
"These are issues both in the connection between the grid nodes and the mesh, and in the connections between mesh nodes. This points to the fact that we should probably use a different projection when building our graph. "
91+
]
92+
},
93+
{
94+
"cell_type": "markdown",
95+
"id": "103a3784-6ad4-4188-ba02-189f9cf71b2f",
96+
"metadata": {},
97+
"source": [
98+
"## Constructing a graph within a projection\n",
99+
"For our example above, let's instead try to construct the graph based on first projecting our lat-lon coordinates to another CRS with 2-dimensional cartesian coordinates. This can be done by giving the `coords_crs` and `graph_crs` arguments to the graph creation functions. Theses arguments should both be instances of `pyproj.crs.CRS` ([pyproj docs.](https://pyproj4.github.io/pyproj/stable/api/crs/crs.html#pyproj.crs.CRS)). Nicely, they can be `cartopy.crs.CRS`, which provides easy ways to specify such CRSs. For more advanced use cases a `pyproj.crs.CRS` can be specified directly. See [the cartopy documentation](https://scitools.org.uk/cartopy/docs/latest/reference/projections.html) for a list of readily available CRSs to use for projecting the coordinates. \n",
100+
"\n",
101+
"We will here try the same thing as above, but using a Azimuthal equidistant projection centered at the pole. The CRS of our lat-lon coordinates will be `cartopy.crs.PlateCarree` and we want to project this to `cartopy.crs.AzimuthalEquidistant`:"
102+
]
103+
},
104+
{
105+
"cell_type": "code",
106+
"execution_count": null,
107+
"id": "fffd518b-f323-4e24-8544-60edeaaa8bb2",
108+
"metadata": {},
109+
"outputs": [],
110+
"source": [
111+
"# Define our projection\n",
112+
"coords_crs = ccrs.PlateCarree()\n",
113+
"graph_crs = ccrs.AzimuthalEquidistant(central_latitude=90)\n",
114+
"\n",
115+
"fig, ax = plt.subplots(figsize=(15, 9), subplot_kw={\"projection\": graph_crs})\n",
116+
"ax.scatter(coords[:, 0], coords[:, 1], marker=\".\", transform=ccrs.PlateCarree())\n",
117+
"_ = ax.coastlines()"
118+
]
119+
},
120+
{
121+
"cell_type": "markdown",
122+
"id": "afa6b933-121b-41c4-a341-9e548b92ad87",
123+
"metadata": {},
124+
"source": [
125+
"Note that distances within projections tend to have very large magnitudes, so the distance between mesh nodes should be specified accordingly."
126+
]
127+
},
128+
{
129+
"cell_type": "code",
130+
"execution_count": null,
131+
"id": "1ddd7ec3-0d66-4feb-a0d3-293f3194dddd",
132+
"metadata": {},
133+
"outputs": [],
134+
"source": [
135+
"mesh_distance = (\n",
136+
" 10**6\n",
137+
") # Large euclidean distance in projection coordinates between mesh nodes\n",
138+
"graph = wmg.create.archetype.create_keisler_graph(\n",
139+
" coords, mesh_node_distance=mesh_distance, coords_crs=coords_crs, graph_crs=graph_crs\n",
140+
") # Note that we here specify the projection argument\n",
141+
"fig, ax = plt.subplots(figsize=(15, 9), subplot_kw={\"projection\": graph_crs})\n",
142+
"wmg.visualise.nx_draw_with_pos_and_attr(\n",
143+
" graph, ax=ax, node_size=30, edge_color_attr=\"component\", node_color_attr=\"type\"\n",
144+
")\n",
145+
"_ = ax.coastlines()"
146+
]
147+
},
148+
{
149+
"cell_type": "markdown",
150+
"id": "f5acf83a-df33-4925-bb32-c106e27e51a4",
151+
"metadata": {},
152+
"source": [
153+
"Now this looks like a more reasonable graph layout, that better respects the spatial relations between the grid points. There are still things that could be tweaked further (e.g. the large number of grid nodes connected to the center mesh node), but this ends our example of defining graphs using lat-lon coordinates.\n",
154+
"\n",
155+
"It can be noted that this projection between different CRSs provides more general functionality than just handling lat-lon coordinates. It is entirely possible to transform from any `coords_crs` to any `graph_crs` using these arguments."
156+
]
157+
}
158+
],
159+
"metadata": {
160+
"kernelspec": {
161+
"display_name": "Python 3 (ipykernel)",
162+
"language": "python",
163+
"name": "python3"
164+
},
165+
"language_info": {
166+
"codemirror_mode": {
167+
"name": "ipython",
168+
"version": 3
169+
},
170+
"file_extension": ".py",
171+
"mimetype": "text/x-python",
172+
"name": "python",
173+
"nbconvert_exporter": "python",
174+
"pygments_lexer": "ipython3",
175+
"version": "3.10.15"
176+
}
177+
},
178+
"nbformat": 4,
179+
"nbformat_minor": 5
180+
}

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies = [
1010
"loguru>=0.7.2",
1111
"networkx>=3.3",
1212
"scipy>=1.13.0",
13+
"pyproj>=3.7.0",
1314
]
1415
requires-python = ">=3.10"
1516
readme = "README.md"
@@ -22,6 +23,7 @@ pytorch = [
2223
visualisation = [
2324
"matplotlib>=3.8.4",
2425
"ipykernel>=6.29.4",
26+
"cartopy>=0.24.1",
2527
]
2628
docs = [
2729
"jupyter-book>=1.0.0",

0 commit comments

Comments
 (0)