Skip to content

Commit 812bf1e

Browse files
committed
clearer fcns for getting speed from MDT + fcn for clipping coriolis param
1 parent 0a92684 commit 812bf1e

File tree

3 files changed

+207
-53
lines changed

3 files changed

+207
-53
lines changed

src/current_denoising/generation/ioutils.py

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,42 @@ def _coriolis_parameter(latitudes: np.ndarray) -> np.ndarray:
7777
return 2 * omega * np.sin(latitudes * torad)
7878

7979

80-
def current_speed_from_mdt(mdt: np.ndarray) -> np.ndarray:
80+
def clipped_coriolis_param(latitudes: np.ndarray, clip_at: float) -> np.ndarray:
8181
"""
82-
Convert geodetic MDT to currents.
82+
Calculate the coriolis parameter at each latitude, clipping it to the value at `clip_at` degrees
83+
84+
Sets values at exactly 0 to + (could equally choose it to be negative, but we don't)
85+
"""
86+
orig = _coriolis_parameter(latitudes)
87+
to_clip = np.abs(latitudes) <= clip_at
88+
89+
clipped = orig.copy()
90+
clipped[to_clip] = np.min(np.abs(orig[~to_clip]))
91+
# If the original coriolis parameter was negative, make sure we keep it negative
92+
clipped[to_clip & (orig < 0)] *= -1
93+
94+
return clipped
95+
96+
97+
def _dlat_dlong(shape: tuple[int, int]) -> tuple[float, np.ndarray]:
98+
"""
99+
Get the grid spacing in metres, given the shape of the grid.
100+
"""
101+
torad = np.pi / 180.0
102+
R = 6_371_229.0
103+
104+
lats, longs = util.lat_long_grid(shape)
105+
dlat = np.abs(lats[1] - lats[0]) * torad * R
106+
dlong = (
107+
np.abs(longs[1] - longs[0]) * torad * R * np.cos(torad * lats)[:, np.newaxis]
108+
)
109+
110+
return dlat, dlong
111+
112+
113+
def current_speed_from_mdt(mdt: np.ndarray, clip_at: float = 2.5) -> np.ndarray:
114+
"""
115+
Convert geodetic MDT to currents, clipping the coriolis parameter to avoid infinities at the equator.
83116
84117
By assuming geostrophic balance, we can take the gradient of the MDT to get the steady-state
85118
currents.
@@ -91,32 +124,27 @@ def current_speed_from_mdt(mdt: np.ndarray) -> np.ndarray:
91124
:returns: the current speed in m/s
92125
93126
"""
94-
g = 9.80665
95-
torad = np.pi / 180.0
96-
R = 6_371_229.0
127+
128+
lats, _ = util.lat_long_grid(mdt.shape)
97129

98130
# Find the grid spacing (in m)
99-
lats, longs = util.lat_long_grid(mdt.shape)
100-
dlat = np.abs(lats[1] - lats[0]) * torad * R
101-
dlong = (
102-
np.abs(longs[1] - longs[0]) * torad * R * np.cos(torad * lats)[:, np.newaxis]
103-
)
131+
dlat, dlong = _dlat_dlong(mdt.shape)
104132

105133
# Find the coriolis parameter at each latitude
106-
f = _coriolis_parameter(lats)
134+
f = clipped_coriolis_param(lats, clip_at)
107135

108136
# Velocities are gradients * coriolis param for geostrophic balance
109137
dmdt_dlat = np.gradient(mdt, axis=0) / dlat
110138
dmdt_dlon = np.gradient(mdt, axis=1) / dlong
111139

112140
# u should be negative, but it doesnt matter for speed
113-
u = g / f[:, np.newaxis] * dmdt_dlat
114-
v = g / f[:, np.newaxis] * dmdt_dlon
141+
u = GRAVITY / f[:, np.newaxis] * dmdt_dlat
142+
v = GRAVITY / f[:, np.newaxis] * dmdt_dlon
115143

116144
return np.sqrt(u**2 + v**2)
117145

118146

119-
def read_clean_currents(
147+
def read_clean_mdt(
120148
path: pathlib.Path,
121149
metadata_path: pathlib.Path,
122150
*,
@@ -125,29 +153,7 @@ def read_clean_currents(
125153
name: str = "r1i1p1f1_gn",
126154
) -> np.ndarray:
127155
"""
128-
Read clean current data from a .dat file.
129-
130-
Read a .dat file containing clean current data,
131-
given the model/name/year, returning a 720x1440 numpy array giving the current
132-
in m/s.
133-
Sets land grid points to np.nan.
134-
Since the clean current data is stored in a large file containing multiple years and models, we need
135-
to choose the correct one.
136-
137-
Notes on the name convention from the CMIP6 documentation can be found in docs/current_denoising/generation/ioutils.md,
138-
or in the original at https://docs.google.com/document/d/1h0r8RZr_f3-8egBMMh7aqLwy3snpD6_MrDz1q8n5XUk.
139-
140-
:param path: location of the .dat file; clean current data is located in
141-
data/projects/SING/richard_stuff/Table2/clean_currents/ on the RDSF
142-
:param metadata_path: location of the metadata .csv file describing the contents of the .dat file
143-
:param year: start of the 5-year period for which to extract data
144-
:param model: the climate model to use
145-
:param name: the model variant to use. Name follows the convention {realisation/initialisation/physics/forcing}_grid
146-
147-
:returns: a numpy array holding current speeds
148-
:raises ValueError: if the requested year/model/name is not found in the metadata
149-
:raises IOError: if the file is malformed, or has a different length to expected from the metadata
150-
156+
Read clean MDT data from a .dat file.
151157
"""
152158
metadata = _read_clean_current_metadata(metadata_path)
153159

@@ -202,9 +208,47 @@ def read_clean_currents(
202208
retval[retval == -1.9e19] = np.nan
203209

204210
# Make it look right
205-
retval = np.flipud(retval.reshape((720, 1440)))
211+
return np.flipud(retval.reshape((720, 1440)))
212+
213+
214+
def read_clean_currents(
215+
path: pathlib.Path,
216+
metadata_path: pathlib.Path,
217+
*,
218+
year: int,
219+
model: str = "ACCESS-CM2",
220+
name: str = "r1i1p1f1_gn",
221+
clip_at: float = 2.5,
222+
) -> np.ndarray:
223+
"""
224+
Read clean current data from a .dat file, clipping the speed to the provided value
225+
226+
Read a .dat file containing clean current data,
227+
given the model/name/year, returning a 720x1440 numpy array giving the current
228+
in m/s.
229+
Sets land grid points to np.nan.
230+
Since the clean current data is stored in a large file containing multiple years and models, we need
231+
to choose the correct one.
232+
233+
Notes on the name convention from the CMIP6 documentation can be found in docs/current_denoising/generation/ioutils.md,
234+
or in the original at https://docs.google.com/document/d/1h0r8RZr_f3-8egBMMh7aqLwy3snpD6_MrDz1q8n5XUk.
235+
236+
:param path: location of the .dat file; clean current data is located in
237+
data/projects/SING/richard_stuff/Table2/clean_currents/ on the RDSF
238+
:param metadata_path: location of the metadata .csv file describing the contents of the .dat file
239+
:param year: start of the 5-year period for which to extract data
240+
:param model: the climate model to use
241+
:param name: the model variant to use. Name follows the convention {realisation/initialisation/physics/forcing}_grid
242+
:param clip_at: latitude (in degrees) at which to clip the coriolis parameter
243+
244+
:returns: a numpy array holding current speeds
245+
:raises ValueError: if the requested year/model/name is not found in the metadata
246+
:raises IOError: if the file is malformed, or has a different length to expected from the metadata
247+
248+
"""
249+
mdt = read_clean_mdt(path, metadata_path, year=year, model=model, name=name)
206250

207-
return current_speed_from_mdt(retval)
251+
return current_speed_from_mdt(mdt, clip_at=clip_at)
208252

209253

210254
def _included_indices(

src/notebooks/plot_changing_currents.ipynb

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@
2525
"mdata_path = data_path.with_name(data_path.stem + \"_meta.txt\")\n",
2626
"\n",
2727
"assert mdata_path.exists()\n",
28-
"assert data_path.exists()"
28+
"assert data_path.exists()\n",
29+
"\n",
30+
"model = \"NorESM1-M\"\n",
31+
"name = \"r2i1p1\"\n",
32+
"year = 1861"
33+
]
34+
},
35+
{
36+
"cell_type": "markdown",
37+
"id": "82a3bb07",
38+
"metadata": {},
39+
"source": [
40+
"Animate how the currents change by year...\n",
41+
"----"
2942
]
3043
},
3144
{
@@ -37,10 +50,6 @@
3750
"source": [
3851
"from current_denoising.generation import ioutils\n",
3952
"\n",
40-
"model = \"NorESM1-M\"\n",
41-
"name = \"r2i1p1\"\n",
42-
"year = 1861\n",
43-
"\n",
4453
"data = ioutils.read_clean_currents(\n",
4554
" data_path, mdata_path, year=year, model=model, name=name\n",
4655
")"
@@ -58,15 +67,6 @@
5867
"maps.imshow(data)"
5968
]
6069
},
61-
{
62-
"cell_type": "markdown",
63-
"id": "82a3bb07",
64-
"metadata": {},
65-
"source": [
66-
"Animate how the currents change by year...\n",
67-
"----"
68-
]
69-
},
7070
{
7171
"cell_type": "code",
7272
"execution_count": null,
@@ -123,6 +123,95 @@
123123
" extra_args=[\"-vcodec\", \"libx264\", \"-pix_fmt\", \"yuv420p\", \"-preset\", \"fast\"],\n",
124124
")"
125125
]
126+
},
127+
{
128+
"cell_type": "markdown",
129+
"id": "c51355fa",
130+
"metadata": {},
131+
"source": [
132+
"How to get rid of the infinity\n",
133+
"====\n",
134+
"There's an infinity at the equator, caused by the coriolis term blowing up.\n",
135+
"\n",
136+
"The \"normal\" way to get rid of this would be to just set the coriolis parameter at latitude=0 to the value of its neighbouring grid points"
137+
]
138+
},
139+
{
140+
"cell_type": "code",
141+
"execution_count": null,
142+
"id": "f9f7f45f",
143+
"metadata": {},
144+
"outputs": [],
145+
"source": [
146+
"%load_ext autoreload\n",
147+
"%autoreload 2"
148+
]
149+
},
150+
{
151+
"cell_type": "code",
152+
"execution_count": null,
153+
"id": "dc7f72d2",
154+
"metadata": {},
155+
"outputs": [],
156+
"source": [
157+
"from current_denoising.generation import ioutils\n",
158+
"\n",
159+
"mdt = ioutils.read_clean_mdt(data_path, mdata_path, year=year, model=model, name=name)"
160+
]
161+
},
162+
{
163+
"cell_type": "code",
164+
"execution_count": null,
165+
"id": "03458517",
166+
"metadata": {},
167+
"outputs": [],
168+
"source": [
169+
"import matplotlib.pyplot as plt\n",
170+
"\n",
171+
"plt.imshow(mdt, cmap=\"viridis\")\n",
172+
"plt.colorbar(label=\"Mean Dynamic Topography (m)\")"
173+
]
174+
},
175+
{
176+
"cell_type": "code",
177+
"execution_count": null,
178+
"id": "ed6ba2f8",
179+
"metadata": {},
180+
"outputs": [],
181+
"source": [
182+
"import numpy as np\n",
183+
"from current_denoising.utils import util\n",
184+
"\n",
185+
"lats, _ = util.lat_long_grid(mdt.shape)\n",
186+
"coriolis = ioutils._coriolis_parameter(lats)\n",
187+
"clipped = ioutils.clipped_coriolis_param(lats, 2.5)\n",
188+
"\n",
189+
"fig, axis = plt.subplots(figsize=(8, 4))\n",
190+
"\n",
191+
"axis.plot(lats, 1 / coriolis, \"--\", label=\"Raw\")\n",
192+
"axis.plot(lats, 1 / clipped, label=\"clipped\")\n",
193+
"axis.legend()\n",
194+
"\n",
195+
"axis.set_ylabel(\"1/Coriolis parameter (s)\")\n",
196+
"axis.set_xlabel(\"Latitude (degrees)\")\n",
197+
"\n",
198+
"fig.suptitle(\"Clipped vs raw coriolis parameter\")"
199+
]
200+
},
201+
{
202+
"cell_type": "code",
203+
"execution_count": null,
204+
"id": "0a01b238",
205+
"metadata": {},
206+
"outputs": [],
207+
"source": [
208+
"from current_denoising.plotting import maps\n",
209+
"\n",
210+
"for clip in [0.1, 2.5, 12.5]:\n",
211+
" maps.imshow(ioutils.current_speed_from_mdt(mdt, clip_at=clip)).suptitle(\n",
212+
" f\"Clipped at {clip} $\\degree$\"\n",
213+
" )"
214+
]
126215
}
127216
],
128217
"metadata": {

src/tests/ut/test_io.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,24 @@ def test_exclude_latitude_too_small(tall_image):
188188
"""
189189
with pytest.raises(ioutils.IOError):
190190
ioutils._included_indices(tall_image.shape[0], 4, 15.0)
191+
192+
193+
def test_clipped_coriolis():
194+
"""
195+
Check that we correctly clip the coriolis parameter
196+
"""
197+
latitudes = np.arange(-90, 100, 10)
198+
199+
raw_coriolis = ioutils._coriolis_parameter(latitudes)
200+
expected_coriolis = raw_coriolis.copy()
201+
expected_coriolis[8:9] = raw_coriolis[7]
202+
expected_coriolis[9:11] = -raw_coriolis[7]
203+
204+
assert np.any(raw_coriolis != expected_coriolis)
205+
206+
assert np.array_equal(
207+
ioutils.clipped_coriolis_param(latitudes, clip_at=10), expected_coriolis
208+
)
209+
assert np.array_equal(
210+
ioutils.clipped_coriolis_param(latitudes, clip_at=15), expected_coriolis
211+
)

0 commit comments

Comments
 (0)