Getting LFRic output on a rectiliniear lat-lon grid#

Note: you need to install python-stratify and esmpy. The best way to do it is to use conda/mamba:

conda install -c conda-forge python-stratify esmpy
[1]:
from functools import partial
from pathlib import Path

import iris
from aeolus.io import create_dummy_cube
from aeolus.lfric import (
    add_equally_spaced_height_coord,
    fix_time_coord,
    load_lfric_raw,
    simple_regrid_lfric,
)
from aeolus.model import lfric

0. Using XIOS#

To re-run LFRic with output on a lat-lon grid, follow this guide.

If the output is already on the native cubed sphere mesh, follow the steps below.

1. Using aeolus functions in a script or notebook#

1.1. Load the raw data#

Set the input file name as a Path object.

[2]:
sample_file = (
    Path.cwd().parent
    / "tests"
    / "data"
    / "test_data"
    / "netcdf"
    / "lfric"
    / "1"
    / "run_gungho_model_shallow-hot-jupiter_C24_MG_dt-2400p0_intel_64-bit_fast-debug"
    / "lfric_diag.nc"
)

Optionally, define what coordinates should be deleted.

[3]:
drop_coord = ["forecast_reference_time"]

Define callback functions to add the vertical height coordinate and clean up the time dimension metadata.

[4]:
_add_levs = partial(add_equally_spaced_height_coord, model_top_height=3.29689e6)
_fix_time = partial(fix_time_coord, downgrade_to_scalar=True)


def _combi_callback(cube, field, filename):
    return _fix_time(
        _add_levs(cube, field, filename), field, filename, downgrade_to_scalar=True
    )

Load the data using a wrapper function from aeolus.

[5]:
cubes_raw = load_lfric_raw(sample_file, callback=_combi_callback, drop_coord=drop_coord)
print(cubes_raw)
0: Axial Angular Momentum on W3 points / (kg m2 s-1) (half_levels: 32; -- : 3456)
1: Horisontal kinetic energy on W3 points / (J) (half_levels: 32; -- : 3456)
2: Vertical kinetic energy on W3 points / (J) (half_levels: 32; -- : 3456)
3: air_density / (kg m-3)              (half_levels: 32; -- : 3456)
4: air_potential_temperature / (K)     (full_levels: 33; -- : 3456)
5: air_pressure / (1)                  (full_levels: 33; -- : 3456)
6: atmosphere_mass_of_air_per_unit_area / (kg m-2) (-- : 3456)
7: eastward_wind / (ms-1)              (half_levels: 32; -- : 3456)
8: northward_wind / (ms-1)             (half_levels: 32; -- : 3456)
9: physics W wind on W3 points / (ms-1) (half_levels: 32; -- : 3456)
10: potential_temperature_increment_from_slow_physics / (K) (full_levels: 33; -- : 3456)
11: pressure_at_cell_interfaces / (Pa)  (full_levels: 33; -- : 3456)

1.2. Regrid all cubes horizontally and vertically#

Create an empty cube with a lat-lon grid as a target for regridding. For example, 96 longitudes is equivalent to the resolution of the C24 mesh at the equator. We use pm180=True so that longitudes range from -180 to 180 instead of 0 to 360.

[6]:
tgt_cube = create_dummy_cube(nlat=48, nlon=96, pm180=True)

Select a cube within the input dataset to serve as a target for the interpolation along the height dimension, i.e. in the vertical. This way, all cubes will be on the same grid in 3 dimensions. If the vertical interpolation is not needed, set interp_vertically=False.

Now, regrid the dataset and print out the resulting cube list.

[7]:
cubes_regr = simple_regrid_lfric(
    cubes_raw, tgt_cube=tgt_cube, ref_cube_constr="air_potential_temperature"
)
print(cubes_regr)
0: Axial Angular Momentum on W3 points / (kg m2 s-1) (level_height: 33; latitude: 48; longitude: 96)
1: Horisontal kinetic energy on W3 points / (J) (level_height: 33; latitude: 48; longitude: 96)
2: Vertical kinetic energy on W3 points / (J) (level_height: 33; latitude: 48; longitude: 96)
3: air_density / (kg m-3)              (level_height: 33; latitude: 48; longitude: 96)
4: air_potential_temperature / (K)     (level_height: 33; latitude: 48; longitude: 96)
5: air_pressure / (1)                  (level_height: 33; latitude: 48; longitude: 96)
6: atmosphere_mass_of_air_per_unit_area / (kg m-2) (latitude: 48; longitude: 96)
7: eastward_wind / (ms-1)              (level_height: 33; latitude: 48; longitude: 96)
8: northward_wind / (ms-1)             (level_height: 33; latitude: 48; longitude: 96)
9: physics W wind on W3 points / (ms-1) (level_height: 33; latitude: 48; longitude: 96)
10: potential_temperature_increment_from_slow_physics / (K) (level_height: 33; latitude: 48; longitude: 96)
11: pressure_at_cell_interfaces / (Pa)  (level_height: 33; latitude: 48; longitude: 96)

1.3. Make a few simple plots#

[8]:
import matplotlib.colors as mcol
import matplotlib.pyplot as plt

plt.rcParams["mathtext.default"] = "regular"

1.3.1. Horizontal slice of potential temperature#

Extract a cube of potential temperature from the regridded dataset and select a height level closest to 123,456 m.

[9]:
cube = cubes_regr.extract_cube("air_potential_temperature")
if not cube.coord("level_height").has_bounds():
    cube.coord("level_height").guess_bounds()
cube_slice = cube.extract(iris.Constraint(level_height=123_456))
print(cube_slice)
air_potential_temperature / (K)     (latitude: 48; longitude: 96)
    Dimension coordinates:
        latitude                             x              -
        longitude                            -              x
    Scalar coordinates:
        forecast_period             102816000.0 seconds
        level_height                103027.8125 m, bound=(51513.90625, 154541.71875) m
        time                        2019-04-05 15:00:00, bound=(2019-04-05 15:00:00, 2019-04-05 15:00:00)
    Cell methods:
        0                           time: point (interval: 2400 s)
    Attributes:
        Conventions                 'UGRID-1.0'
        description                 'LFRic file format v0.2.0'
        interval_operation          '2400 s'
        interval_write              '1.728e+06 s'
        name                        'lfric_diag'
        online_operation            'instant'
        title                       'Created by xios'

For convenience, extract the latitude and longitude arrays.

[10]:
lats = cube_slice.coord("latitude").points
lons = cube_slice.coord("longitude").points

Assemble the figure.

[11]:
fig, ax = plt.subplots(constrained_layout=True)
im = ax.pcolormesh(lons, lats, cube_slice.data)
fig.colorbar(im)
ax.set_title(
    f"{cube_slice.name()} / {cube_slice.units}"
    f"\n@ {cube_slice.coord('level_height').points[0]} {cube_slice.coord('level_height').units}",
    loc="right",
)
ax.set_ylim(-90, 90)
ax.set_xlim(-180, 180)
ax.set_ylabel("Latitude / deg")
ax.set_xlabel("Longitude / deg");
../_images/examples_05_Postprocessing_LFRic_29_0.png

1.3.2. Horizontal wind vectors#

[12]:
from aeolus.coord import isel

Extract eastward and northward components of the wind from the regridded dataset.

[13]:
cube_u = cubes_regr.extract_cube("eastward_wind")
cube_v = cubes_regr.extract_cube("northward_wind")

Select a random slice w.r.t. the vertical axis.

[14]:
cube_u_slice = isel(cube_u, "level_height", 11)  # equivalent to `cube_u[11, ...]`
cube_v_slice = isel(cube_v, "level_height", 11)  # equivalent to `cube_u[11, ...]`
[15]:
fig, ax = plt.subplots(constrained_layout=True)
ax.streamplot(
    lons,
    lats,
    cube_u_slice.data,
    cube_v_slice.data,
)
ax.set_title(
    f"Horizontal flow @ {cube_u_slice.coord('level_height').points[0]} {cube_u_slice.coord('level_height').units}",
    loc="right",
)
ax.set_ylim(-90, 90)
ax.set_xlim(-180, 180)
ax.set_ylabel("Latitude / deg")
ax.set_xlabel("Longitude / deg");
../_images/examples_05_Postprocessing_LFRic_36_0.png

1.3.3. Zonal mean eastward wind#

[16]:
from aeolus.calc import zonal_mean
[17]:
u_zm = zonal_mean(cubes_regr.extract_cube("eastward_wind"))
[18]:
fig, ax = plt.subplots(constrained_layout=True)

p0 = ax.contourf(
    u_zm.coord("latitude").points,
    u_zm.coord("level_height").points,
    u_zm.data,
    cmap="RdBu_r",
    norm=mcol.CenteredNorm(),
)
cb0 = fig.colorbar(p0, ax=ax)
cb0.ax.set_ylabel("Wind Speed / $m$ $s^{-1}$")

ax.set_ylabel("Level Height / m")
ax.set_xlabel("Latitude / deg")

ax.set_title("Zonal mean eastward wind")
[18]:
Text(0.5, 1.0, 'Zonal mean eastward wind')
../_images/examples_05_Postprocessing_LFRic_40_1.png

1.3.4. Interpolate the data to isobaric (constant pressure levels)#

It is often more informative to visualise the vertical structure of the atmosphere with respect to pressure. Let us interpolate our data to isobaric levels and plot the zonal mean cross-section with pressure as the y-axis.

[19]:
import numpy as np
from aeolus.coord import interp_cube_from_height_to_pressure_levels
from iris.experimental import stratify

Create a callable to pass to the interpolation routine. Use linear extrapolation.

[20]:
INTERPOLATOR = partial(
    stratify.stratify.interpolate,
    interpolation=stratify.stratify.INTERPOLATE_LINEAR,
    extrapolation=stratify.stratify.EXTRAPOLATE_LINEAR,
)

Temporarily rename the pressure variable in the lfric registry.

If your output contains another pressure variable, change it accordingly.

[21]:
lfric.pres = "pressure_at_cell_interfaces"  # optional

Interpolate the eastward wind component to the levels of constant pressure.

[22]:
(pres_lev := np.arange(1e5, 0, -1e4))
[22]:
array([100000.,  90000.,  80000.,  70000.,  60000.,  50000.,  40000.,
        30000.,  20000.,  10000.])
[23]:
u_p = interp_cube_from_height_to_pressure_levels(
    cubes_regr.extract_cube("eastward_wind"),
    cubes_regr.extract_cube("pressure_at_cell_interfaces"),
    levels=pres_lev,
    model=lfric,
    interpolator=INTERPOLATOR,
)

Apply zonal averaging.

[24]:
u_p_zm = zonal_mean(u_p)

Create the same figure but with pressure as the y-axis (in the logarithmic scale).

[25]:
fig, ax = plt.subplots(constrained_layout=True)

p0 = ax.contourf(
    u_p_zm.coord("latitude").points,
    u_p_zm.coord(lfric.pres).points,
    u_p_zm.data,
    cmap="RdBu_r",
    norm=mcol.CenteredNorm(),
)
cb0 = fig.colorbar(p0, ax=ax)
cb0.ax.set_ylabel("Wind Speed / $m$ $s^{-1}$")

ax.set_ylim(1e5, 1e4)
ax.set_yscale("log")

ax.set_ylabel("Pressure / Pa")
ax.set_xlabel("Latitude / deg")

ax.set_title("Zonal mean eastward wind");
../_images/examples_05_Postprocessing_LFRic_54_0.png

2. Using the “all inclusive” function in aeolus#

The regridding steps 1.1-1.2 above can be done using the process_lfric function. Note that it expects the input directory structure as it is currently in LFRic’s rose-stem test suite.

[26]:
import tempfile

from aeolus.postprocess import process_lfric
[27]:
inpdir = Path("..") / "tests" / "data" / "test_data" / "netcdf" / "lfric"
outdir = Path(tempfile.mkdtemp())
label = "shj"
planet = ""  # do not add planet constants to the output file
c_num = "C24"
ref_cube = "air_potential_temperature"
level_height = "uniform"
top_height = 3.29689e6
time_prof = "inst"
file_chunk_size = 1
nlat = 48
nlon = 96
[28]:
fname_out = process_lfric(
    inpdir,
    outdir,
    label,
    planet,
    c_num,
    ref_cube,
    level_height,
    top_height,
    time_prof,
    file_chunk_size,
    nlat,
    nlon,
)
2024-11-07 14:40:03.089 | INFO     | aeolus.postprocess:process_lfric:128 - fnames(1) = ../tests/data/test_data/netcdf/lfric/1/run_gungho_model_shallow-hot-jupiter_C24_MG_dt-2400p0_intel_64-bit_fast-debug/lfric_diag.nc
2024-11-07 14:40:04.208 | SUCCESS  | aeolus.postprocess:process_lfric:174 - Saved to /tmp/tmpkv1oabui/shj_c24_inst__001-001_regr.nc
2024-11-07 14:40:04.208 | INFO     | aeolus.postprocess:process_lfric:175 - Execution time: 1.1s
[29]:
cubes_regr = iris.load(outdir / "shj_c24_inst__001-001_regr.nc")
print(cubes_regr)
0: potential_temperature_increment_from_slow_physics / (K) (level_height: 33; latitude: 48; longitude: 96)
1: Vertical kinetic energy on W3 points / (J) (level_height: 33; latitude: 48; longitude: 96)
2: atmosphere_mass_of_air_per_unit_area / (kg m-2) (latitude: 48; longitude: 96)
3: Axial Angular Momentum on W3 points / (kg m2 s-1) (level_height: 33; latitude: 48; longitude: 96)
4: pressure_at_cell_interfaces / (Pa)  (level_height: 33; latitude: 48; longitude: 96)
5: Horisontal kinetic energy on W3 points / (J) (level_height: 33; latitude: 48; longitude: 96)
6: physics W wind on W3 points / (m s-1) (level_height: 33; latitude: 48; longitude: 96)
7: air_density / (kg m-3)              (level_height: 33; latitude: 48; longitude: 96)
8: air_potential_temperature / (K)     (level_height: 33; latitude: 48; longitude: 96)
9: air_pressure / (1)                  (level_height: 33; latitude: 48; longitude: 96)
10: eastward_wind / (m s-1)             (level_height: 33; latitude: 48; longitude: 96)
11: northward_wind / (m s-1)            (level_height: 33; latitude: 48; longitude: 96)

3. Using aeolus in the command-line interface#

The process_lfric function can also be invoked from the command line as following.

[30]:
!aeolus pp --inpdir ../tests/data/test_data/netcdf/lfric --outdir ./tmp -m lfric -l shj -p "" --top_height 3.29689e6 -c C24 --ref_cube air_potential_temperature --file_chunk_size 1
Processing lfric output...
2024-11-07 14:40:05.568 | INFO     | aeolus.postprocess:process_lfric:128 - fnames(1) = ../tests/data/test_data/netcdf/lfric/1/run_gungho_model_shallow-hot-jupiter_C24_MG_dt-2400p0_intel_64-bit_fast-debug/lfric_diag.nc
2024-11-07 14:40:07.097 | SUCCESS  | aeolus.postprocess:process_lfric:174 - Saved to tmp/shj_c24_inst__001-001_regr.nc
2024-11-07 14:40:07.097 | INFO     | aeolus.postprocess:process_lfric:175 - Execution time: 1.5s
Saved to tmp/shj_c24_inst__001-001_regr.nc
[31]:
cubes_regr = iris.load(Path("tmp") / "shj_c24_inst__001-001_regr.nc")
print(cubes_regr)
0: potential_temperature_increment_from_slow_physics / (K) (level_height: 33; latitude: 90; longitude: 144)
1: Vertical kinetic energy on W3 points / (J) (level_height: 33; latitude: 90; longitude: 144)
2: atmosphere_mass_of_air_per_unit_area / (kg m-2) (latitude: 90; longitude: 144)
3: Axial Angular Momentum on W3 points / (kg m2 s-1) (level_height: 33; latitude: 90; longitude: 144)
4: pressure_at_cell_interfaces / (Pa)  (level_height: 33; latitude: 90; longitude: 144)
5: Horisontal kinetic energy on W3 points / (J) (level_height: 33; latitude: 90; longitude: 144)
6: physics W wind on W3 points / (m s-1) (level_height: 33; latitude: 90; longitude: 144)
7: air_density / (kg m-3)              (level_height: 33; latitude: 90; longitude: 144)
8: air_potential_temperature / (K)     (level_height: 33; latitude: 90; longitude: 144)
9: air_pressure / (1)                  (level_height: 33; latitude: 90; longitude: 144)
10: eastward_wind / (m s-1)             (level_height: 33; latitude: 90; longitude: 144)
11: northward_wind / (m s-1)            (level_height: 33; latitude: 90; longitude: 144)
[32]:
import shutil

shutil.rmtree("tmp")