From 296a8f07a8a67a0d7de38995d1f359e05da9e8f0 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Mon, 20 Oct 2025 21:14:58 +0100 Subject: [PATCH 01/13] Add 2024 summer placement students to contributors list --- docs/source/contributing.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index dd7394eb7c..b98e3fce76 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -116,13 +116,15 @@ ideas, code, and documentation to the cf library: * Bryan Lawrence * Charles Roberts * David Hassell -* Evert Rol +* Evert Rol +* George Pulickan * Javier Dehesa * Jonathan Gregory * Klaus Zimmermann * Kristian Sebastián * Mark Rhodes-Smith * Matt Brown +* Natalia Hunt * Michael Decker * Oliver Kotla * Sadie Bartholomew From 9328936f04628183b8f6eda1a06006c50e5a54e9 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 21 Oct 2025 00:01:38 +0100 Subject: [PATCH 02/13] Add all original recipe Python scripts since lost from repo Used command to get from v3.17.0 branch: $ git checkout v3.17.0 -- recipes --- docs/source/recipes/plot_01_recipe.py | 77 ++++++++ docs/source/recipes/plot_02_recipe.py | 96 +++++++++ docs/source/recipes/plot_03_recipe.py | 35 ++++ docs/source/recipes/plot_04_recipe.py | 40 ++++ docs/source/recipes/plot_05_recipe.py | 64 ++++++ docs/source/recipes/plot_06_recipe.py | 65 +++++++ docs/source/recipes/plot_07_recipe.py | 65 +++++++ docs/source/recipes/plot_08_recipe.py | 143 ++++++++++++++ docs/source/recipes/plot_09_recipe.py | 82 ++++++++ docs/source/recipes/plot_10_recipe.py | 96 +++++++++ docs/source/recipes/plot_11_recipe.py | 92 +++++++++ docs/source/recipes/plot_12_recipe.py | 117 +++++++++++ docs/source/recipes/plot_13_recipe.py | 268 ++++++++++++++++++++++++++ docs/source/recipes/plot_14_recipe.py | 181 +++++++++++++++++ docs/source/recipes/plot_15_recipe.py | 163 ++++++++++++++++ docs/source/recipes/plot_16_recipe.py | 57 ++++++ docs/source/recipes/plot_17_recipe.py | 109 +++++++++++ docs/source/recipes/plot_18_recipe.py | 143 ++++++++++++++ docs/source/recipes/plot_19_recipe.py | 105 ++++++++++ docs/source/recipes/plot_20_recipe.py | 97 ++++++++++ 20 files changed, 2095 insertions(+) create mode 100644 docs/source/recipes/plot_01_recipe.py create mode 100644 docs/source/recipes/plot_02_recipe.py create mode 100644 docs/source/recipes/plot_03_recipe.py create mode 100644 docs/source/recipes/plot_04_recipe.py create mode 100644 docs/source/recipes/plot_05_recipe.py create mode 100644 docs/source/recipes/plot_06_recipe.py create mode 100644 docs/source/recipes/plot_07_recipe.py create mode 100644 docs/source/recipes/plot_08_recipe.py create mode 100644 docs/source/recipes/plot_09_recipe.py create mode 100644 docs/source/recipes/plot_10_recipe.py create mode 100644 docs/source/recipes/plot_11_recipe.py create mode 100644 docs/source/recipes/plot_12_recipe.py create mode 100644 docs/source/recipes/plot_13_recipe.py create mode 100644 docs/source/recipes/plot_14_recipe.py create mode 100644 docs/source/recipes/plot_15_recipe.py create mode 100644 docs/source/recipes/plot_16_recipe.py create mode 100644 docs/source/recipes/plot_17_recipe.py create mode 100644 docs/source/recipes/plot_18_recipe.py create mode 100644 docs/source/recipes/plot_19_recipe.py create mode 100644 docs/source/recipes/plot_20_recipe.py diff --git a/docs/source/recipes/plot_01_recipe.py b/docs/source/recipes/plot_01_recipe.py new file mode 100644 index 0000000000..93b9dde417 --- /dev/null +++ b/docs/source/recipes/plot_01_recipe.py @@ -0,0 +1,77 @@ +""" +Calculating global mean temperature timeseries +============================================== + +In this recipe we will calculate and plot monthly and annual global mean temperature timeseries. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: + +f = cf.read("~/recipes/cru_ts4.06.1901.2021.tmp.dat.nc") +print(f) + +# %% +# 3. Select near surface temperature by index and look at its contents: + +temp = f[1] +print(temp) + +# %% +# 4. Select latitude and longitude dimensions by identities, with two different techniques: + +lon = temp.coordinate("long_name=longitude") +lat = temp.coordinate("Y") + +# %% +# 5. Print the description of near surface temperature using the dump method to show properties of all constructs: + +temp.dump() + +# %% +# 6. Latitude and longitude dimension coordinate cell bounds are absent, which are created and set: + +a = lat.create_bounds() +lat.set_bounds(a) +lat.dump() + +# %% + +b = lon.create_bounds() +lon.set_bounds(b) +lon.dump() + +# %% + +print(b.array) + +# %% +# 7. Time dimension coordinate cell bounds are similarly created and set for cell sizes of one calendar month: + +time = temp.coordinate("long_name=time") +c = time.create_bounds(cellsize=cf.M()) +time.set_bounds(c) +time.dump() + +# %% +# 8. Calculate and plot the area weighted mean surface temperature for each time: + +global_avg = temp.collapse("area: mean", weights=True) +cfp.lineplot(global_avg, color="red", title="Global mean surface temperature") + +# %% +# 9. Calculate and plot the annual global mean surface temperature: + +annual_global_avg = global_avg.collapse("T: mean", group=cf.Y()) +cfp.lineplot( + annual_global_avg, + color="red", + title="Annual global mean surface temperature", +) diff --git a/docs/source/recipes/plot_02_recipe.py b/docs/source/recipes/plot_02_recipe.py new file mode 100644 index 0000000000..902c796788 --- /dev/null +++ b/docs/source/recipes/plot_02_recipe.py @@ -0,0 +1,96 @@ +""" +Calculating and plotting the global average temperature anomalies +================================================================= + +In this recipe we will calculate and plot the global average temperature anomalies. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: + +f = cf.read("~/recipes/cru_ts4.06.1901.2021.tmp.dat.nc") +print(f) + +# %% +# 3. Select near surface temperature by index and look at its contents: + +temp = f[1] +print(temp) + +# %% +# 4. Select latitude and longitude dimensions by identities, with two different techniques: + +lon = temp.coordinate("long_name=longitude") +lat = temp.coordinate("Y") + +# %% +# 5. Print the description of near surface temperature to show properties of all constructs: + +temp.dump() + +# %% +# 6. Latitude and longitude dimension coordinate cell bounds are absent, which are created and set: + +a = lat.create_bounds() +lat.set_bounds(a) +lat.dump() + +# %% + +b = lon.create_bounds() +lon.set_bounds(b) +lon.dump() + +# %% + +print(b.array) + +# %% +# 7. Time dimension coordinate cell bounds are similarly created and set for cell sizes of one calendar month: + +time = temp.coordinate("long_name=time") +c = time.create_bounds(cellsize=cf.M()) +time.set_bounds(c) +time.dump() + +# %% +# 8. Calculate the area weighted mean surface temperature for each time using the collapse method: + +global_avg = temp.collapse("area: mean", weights=True) + +# %% +# 9. Calculate the annual global mean surface temperature: + +annual_global_avg = global_avg.collapse("T: mean", group=cf.Y()) + +# %% +# 10. The temperature values are averaged for the climatological period of 1961-1990 by defining a subspace within these years using `cf.wi` query instance over subspace and doing a statistical collapse with the collapse method: + +annual_global_avg_61_90 = annual_global_avg.subspace( + T=cf.year(cf.wi(1961, 1990)) +) +print(annual_global_avg_61_90) + +# %% + +temp_clim = annual_global_avg_61_90.collapse("T: mean") +print(temp_clim) + +# %% +# 11. The temperature anomaly is then calculated by subtracting these climatological temperature values from the annual global average temperatures and plotted: + +temp_anomaly = annual_global_avg - temp_clim +cfp.lineplot( + temp_anomaly, + color="red", + title="Global Average Temperature Anomaly (1901-2021)", + ylabel="1961-1990 climatology difference ", + yunits="degree Celcius", +) diff --git a/docs/source/recipes/plot_03_recipe.py b/docs/source/recipes/plot_03_recipe.py new file mode 100644 index 0000000000..25633b1c54 --- /dev/null +++ b/docs/source/recipes/plot_03_recipe.py @@ -0,0 +1,35 @@ +""" +Plotting global mean temperatures spatially +=========================================== + +In this recipe, we will plot the global mean temperature spatially. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: + +f = cf.read("~/recipes/cru_ts4.06.1901.2021.tmp.dat.nc") +print(f) + +# %% +# 3. Select near surface temperature by index and look at its contents: + +temp = f[1] +print(temp) + +# %% +# 4. Average the monthly mean surface temperature values by the time axis using the collapse method: + +global_avg = temp.collapse("mean", axes="long_name=time") + +# %% +# 5. Plot the global mean surface temperatures: + +cfp.con(global_avg, lines=False, title="Global mean surface temperature") diff --git a/docs/source/recipes/plot_04_recipe.py b/docs/source/recipes/plot_04_recipe.py new file mode 100644 index 0000000000..a50296bb9e --- /dev/null +++ b/docs/source/recipes/plot_04_recipe.py @@ -0,0 +1,40 @@ +""" +Comparing two datasets with different resolutions using regridding +================================================================== + +In this recipe, we will regrid two different datasets with different resolutions. An example use case could be one where the observational dataset with a higher resolution needs to be regridded to that of the model dataset so that they can be compared with each other. +""" + +# %% +# 1. Import cf-python: + +import cf + +# %% +# 2. Read the field constructs: + +obs = cf.read("~/recipes/cru_ts4.06.1901.2021.tmp.dat.nc", dask_chunks=None) +print(obs) + +# %% + +model = cf.read( + "~/recipes/tas_Amon_HadGEM3-GC3-1_hist-1p0_r3i1p1f2_gn_185001-201412.nc" +) +print(model) + +# %% +# 3. Select observation and model temperature fields by identity and index respectively, and look at their contents: + +obs_temp = obs.select_field("long_name=near-surface temperature") +print(obs_temp) + +# %% + +model_temp = model[0] +print(model_temp) + +# %% +# 4. Regrid observational data to that of the model data and create a new low resolution observational data using bilinear interpolation: +obs_temp_regrid = obs_temp.regrids(model_temp, method="linear") +print(obs_temp_regrid) diff --git a/docs/source/recipes/plot_05_recipe.py b/docs/source/recipes/plot_05_recipe.py new file mode 100644 index 0000000000..e40f1c23ad --- /dev/null +++ b/docs/source/recipes/plot_05_recipe.py @@ -0,0 +1,64 @@ +""" +Plotting wind vectors overlaid on precipitation data +==================================================== + +In this recipe we will plot wind vectors, derived from northward and eastward wind components, over precipitation data. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: + +f1 = cf.read("~/recipes/northward.nc") +print(f1) + +# %% + +f2 = cf.read("~/recipes/eastward.nc") +print(f2) + +# %% + +f3 = cf.read("~/recipes/monthly_precipitation.nc") +print(f3) + +# %% +# 3. Select wind vectors and precipitation data by index and look at their contents: +v = f1[0] +print(v) + +# %% + +u = f2[0] +print(u) + +# %% + +pre = f3[0] +print(pre) + +# %% +# 4. Plot the wind vectors on top of precipitation data for June 1995 by creating a subspace with a date-time object and using `cfplot.con `_. Here `cfplot.gopen `_ is used to define the parts of the plot area, which is closed by `cfplot.gclose `_; `cfplot.cscale `_ is used to choose one of the colour maps amongst many available; `cfplot.levs `_ is used to set the contour levels for precipitation data; and `cfplot.vect `_ is used to plot the wind vectors for June 1995: +june_95 = cf.year(1995) & cf.month(6) +cfp.gopen() +cfp.cscale("precip4_11lev") +cfp.levs(step=100) +cfp.con( + pre.subspace(T=june_95), + lines=False, + title="June 1995 monthly global precipitation", +) +cfp.vect( + u=u.subspace(T=june_95), + v=v.subspace(T=june_95), + key_length=10, + scale=35, + stride=5, +) +cfp.gclose() diff --git a/docs/source/recipes/plot_06_recipe.py b/docs/source/recipes/plot_06_recipe.py new file mode 100644 index 0000000000..91ca4ab100 --- /dev/null +++ b/docs/source/recipes/plot_06_recipe.py @@ -0,0 +1,65 @@ +""" +Converting from rotated latitude-longitude to regular latitude-longitude +======================================================================== + +In this recipe, we will be regridding from a rotated latitude-longitude source domain to a regular latitude-longitude destination domain. +""" + +# %% +# 1. Import cf-python, cf-plot and numpy: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs using read function: + +f = cf.read("~/recipes/au952a.pd20510414.pp") +print(f) + +# %% +# 3. Select the field by index and print its description to show properties of all constructs: + +gust = f[0] +gust.dump() + +# %% +# 4. Access the time coordinate of the gust field and retrieve the datetime values of the time coordinate: + +print(gust.coordinate("time").datetime_array) + +# %% +# 5. Create a new instance of the `cf.dt` class with a specified year, month, day, hour, minute, second and microsecond. Then store the result in the variable ``test``: +test = cf.dt(2051, 4, 14, 1, 30, 0, 0) +print(test) + +# %% +# 6. Plot the wind gust by creating a subspace for the specified variable ``test`` using `cfplot.con `_. Here `cfplot.mapset `_ is used to set the mapping parameters like setting the map resolution to 50m: +cfp.mapset(resolution="50m") +cfp.con(gust.subspace(T=test), lines=False) + +# %% +# 7. To see the rotated pole data on the native grid, the above steps are repeated and projection is set to rotated in `cfplot.mapset `_: +cfp.mapset(resolution="50m", proj="rotated") +cfp.con(gust.subspace(T=test), lines=False) + +# %% +# 8. Create dimension coordinates for the destination grid with the latitude and +# longitude values for Europe. `cf.Domain.create_regular +# `_ +# method is used to +# create a regular grid with longitudes and latitudes. Spherical regridding is +# then performed on the gust variable by passing the target domain as argument. +# The method also takes an argument ``'linear'`` which specifies the type of +# regridding method to use. The description of the ``regridded_data`` is finally +# printed to show properties of all its constructs: + +target_domain = cf.Domain.create_regular((-25, 45, 10), (32, 72, 10)) +regridded_data = gust.regrids(target_domain, "linear") +regridded_data.dump() + +# %% +# 9. Step 6 is similarly repeated for the ``regridded_data`` to plot the wind gust on a regular latitude-longitude domain: +cfp.mapset(resolution="50m") +cfp.con(regridded_data.subspace(T=test), lines=False) diff --git a/docs/source/recipes/plot_07_recipe.py b/docs/source/recipes/plot_07_recipe.py new file mode 100644 index 0000000000..02908e83ee --- /dev/null +++ b/docs/source/recipes/plot_07_recipe.py @@ -0,0 +1,65 @@ +""" +Plotting members of a model ensemble +==================================== + +In this recipe, we will plot the members of a model ensemble. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs using read function and store it in the variable ``f``. The * in the filename is a wildcard character which means the function reads all files in the directory that match the specified pattern. [0:5] selects the first five elements of the resulting list: + +f = cf.read("~/recipes/realization/PRMSL.1941_mem*.nc")[0:5] +print(f) + +# %% +# 3. The description of one of the fields from the list shows ``'realization'`` as a property by which the members of the model ensemble are labelled: + +f[1].dump() + +# %% +# 4. An ensemble of the members is then created by aggregating the data in ``f`` along a new ``'realization'`` axis using the cf.aggregate function, and storing the result in the variable ``ensemble``. ``'relaxed_identities=True'`` allows for missing coordinate identities to be inferred. [0] selects the first element of the resulting list. ``id%realization`` now shows as an auxiliary coordinate for the ensemble: + +ensemble = cf.aggregate( + f, dimension=("realization",), relaxed_identities=True +)[0] +print(ensemble) + + +# %% +# 5. To see the constructs for the ensemble, print the *constructs* attribute: + +print(ensemble.constructs) + +# %% +# 6. Loop over the realizations in the ensemble using the *range* function and the *domain_axis* to determine the size of the realization dimension. For each realization, extract a subspace of the ensemble using the *subspace* method and the ``'id%realization'`` keyword argument along a specific latitude and longitude and plot the realizations from the 4D field using `cfplot.lineplot `_. +# A moving average of the ensemble along the time axis, with a window size of 90 (i.e. an approximately 3-month moving average) is calculated using the *moving_window* method. The ``mode='nearest'`` parameter is used to specify how to pad the data outside of the time range. The *squeeze* method removes any dimensions of size 1 from the field to produce a 2D field: + +cfp.gopen() + +for realization in range(1, ensemble.domain_axis("id%realization").size + 1): + cfp.lineplot( + ensemble.subspace( + **{"id%realization": realization}, latitude=[0], longitude=[0] + ).squeeze(), + label=f"Member {realization}", + linewidth=1.0, + ) + +cfp.lineplot( + ensemble.moving_window( + method="mean", window_size=90, axis="T", mode="nearest" + )[0, :, 0, 0].squeeze(), + label="Ensemble mean", + linewidth=2.0, + color="black", + title="Model Ensemble Pressure", +) + +cfp.gclose() diff --git a/docs/source/recipes/plot_08_recipe.py b/docs/source/recipes/plot_08_recipe.py new file mode 100644 index 0000000000..63427f62a7 --- /dev/null +++ b/docs/source/recipes/plot_08_recipe.py @@ -0,0 +1,143 @@ +""" +Plotting statistically significant temperature trends with stippling +==================================================================== + +In this recipe, we will analyse and plot temperature trends from the HadCRUT.5.0.1.0 dataset for two different time periods. The plotted maps also include stippling, which is used to highlight areas where the temperature trends are statistically significant. +""" + +# %% +# 1. Import cf-python, cf-plot, numpy and scipy.stats: + +import cfplot as cfp +import cf + +import numpy as np +import scipy.stats as stats + + +# %% +# 2. Three functions are defined: + +# %% +# * ``linear_trend(data, time_axis)``: This function calculates the linear regression slope and p-value for the input data along the time axis. It takes two arguments: ``'data'``, which represents the temperature anomalies or any other data you want to analyse, and ``'time_axis'``, which represents the corresponding time points for the data. The function uses the `stats.linregress `_ method from the `scipy.stats `_ library to calculate the slope and p-value of the linear regression. It returns these two values as a tuple: + + +def linear_trend(data, time_axis): + slope, _, _, p_value, _ = stats.linregress(time_axis, data) + return slope, p_value + + +# %% +# * ``create_trend_stipple_obj(temp_data, input_data)``: This function creates a new object with the input data provided and *collapses* the time dimension by taking the mean. It takes two arguments: ``'temp_data'``, which represents the temperature data object, and ``'input_data'``, which is the data to be set in the new object. The function creates a copy of the ``'temp_data'`` object by selecting the first element using index and *squeezes* it to remove the size 1 axis. It then sets the input data with the ``'Y'`` (latitude) and ``'X'`` (longitude) axes, and then *collapses* the time dimension using the ``"T: mean"`` operation: + + +def create_trend_stipple_obj(temp_data, input_data): + trend_stipple_obj = temp_data[0].squeeze() + trend_stipple_obj.set_data(input_data, axes=["Y", "X"]) + return trend_stipple_obj + + +# %% +# * ``process_subsets(subset_mask)``: This function processes the subsets of data by applying the ``linear_trend`` function along a specified axis. It takes one argument, ``'subset_mask'``, which is a boolean mask representing the time points to be considered in the analysis. The function first extracts the masked subset of data and then applies the ``linear_trend`` function along the time axis (axis 0) using the `numpy.ma.apply_along_axis `_ function. The result is an array containing the slope and p-value for each grid point in the dataset: + + +def process_subsets(subset_mask): + subset_data = masked_data[subset_mask, :, :] + return np.ma.apply_along_axis( + linear_trend, 0, subset_data, time_axis[subset_mask] + ) + + +# %% +# 3. Read the field constructs: + +temperature_data = cf.read( + "~/recipes/HadCRUT.5.0.1.0.analysis.anomalies.ensemble_mean.nc" +)[0] +print(temperature_data) + +# %% +# 4. Calculate the annual mean temperature anomalies. The ``'weights=True'`` argument is used take the varying lengths of months into account which ensures that the calculated mean is more accurate. A masked array is created for the annual mean temperature anomalies, masking any invalid values: + +annual_temperature = temperature_data.collapse( + "T: mean", weights=True, group=cf.Y() +) +time_axis = annual_temperature.coordinate("T").year.array +masked_data = np.ma.masked_invalid(annual_temperature.array) + +# %% +# 5. Define two time periods for analysis: 1850-2020 and 1980-2020, along with a significance level (alpha) of 0.05: + +time_periods = [(1850, 2020, "sub_1850_2020"), (1980, 2020, "sub_1980_2020")] +alpha = 0.05 +results = {} + +# %% +# 6. Loop through the time periods, processing the subsets, calculating trend p-values, and creating stipple objects. For each time period, the script calculates the trends and p-values using the ``process_subsets`` function. If the p-value is less than the significance level (alpha = 0.05), a stippling mask is created. The script then creates a new object for the trend and stippling mask using the ``create_trend_stipple_obj`` function: + +for start, end, prefix in time_periods: + subset_mask = (time_axis >= start) & (time_axis <= end) + subset_trend_pvalue = process_subsets(subset_mask) + results[prefix + "_trend_pvalue"] = subset_trend_pvalue + results[prefix + "_stipple"] = subset_trend_pvalue[1] < alpha + results[prefix + "_trend"] = create_trend_stipple_obj( + temperature_data, subset_trend_pvalue[0] + ) + results[prefix + "_stipple_obj"] = create_trend_stipple_obj( + temperature_data, results[prefix + "_stipple"] + ) + +# %% +# 7. Create two plots - one for the 1850-2020 time period and another for the 1980-2020 time period using `cfplot.con `_. +# The results are multiplied by 10 so that each plot displays the temperature trend in K/decade with stippling to indicate areas where the trend is statistically significant (p-value < 0.05). +# Here `cfplot.gopen `_ is used to define the parts of the plot area with two rows and one column, and setting the bottom margin to 0.2. +# It is closed by `cfplot.gclose `_; +# `cfplot.gpos `_ is used to set the plotting position of both the plots; +# `cfplot.mapset `_ is used to set the map projection to Robinson; +# `cfplot.cscale `_ is used to choose one of the colour maps amongst many available; +# `cfplot.levs `_ is used to set the contour levels; +# and `cfplot.stipple `_ is used to add stippling to show statistically significant areas: + +cfp.gopen(rows=2, columns=1, bottom=0.2) + +cfp.gpos(1) +cfp.mapset(proj="robin") +cfp.cscale("temp_19lev") +cfp.levs(min=-1, max=1, step=0.1) +cfp.con( + results["sub_1850_2020_trend"] * 10, + lines=False, + colorbar=None, + title="Temperature Trend 1850-2020", +) +cfp.stipple( + results["sub_1850_2020_stipple_obj"], + min=1, + max=1, + size=5, + color="k", + marker=".", +) + +cfp.gpos(2) +cfp.mapset(proj="robin") +cfp.cscale("temp_19lev") +cfp.levs(min=-1, max=1, step=0.1) +cfp.con( + results["sub_1980_2020_trend"] * 10, + lines=False, + title="Temperature Trend 1980-2020", + colorbar_position=[0.1, 0.1, 0.8, 0.02], + colorbar_orientation="horizontal", + colorbar_title="K/decade", +) +cfp.stipple( + results["sub_1980_2020_stipple_obj"], + min=1, + max=1, + size=5, + color="k", + marker=".", +) + +cfp.gclose() diff --git a/docs/source/recipes/plot_09_recipe.py b/docs/source/recipes/plot_09_recipe.py new file mode 100644 index 0000000000..42fe49cbd9 --- /dev/null +++ b/docs/source/recipes/plot_09_recipe.py @@ -0,0 +1,82 @@ +""" +Plotting a joint histogram +========================== + +In this recipe, we will be creating a joint histogram of PM2.5 mass +concentration and 2-metre temperature. +""" + +# %% +# 1. Import cf-python, numpy and matplotlib.pyplot: + +import matplotlib.pyplot as plt +import numpy as np + +import cf + +# %% +# 2. Read the field constructs using read function: +f = cf.read("~/recipes/levtype_sfc.nc") +print(f) + +# %% +# 3. Select the PM2.5 mass concentration and 2-metre temperature fields by +# index and print the description to show properties of all constructs: +pm25_field = f[2] +pm25_field.dump() + +# %% + +temp_field = f[3] +temp_field.dump() + +# %% +# 4. Convert the units to degree celsius for the temperature field: +temp_field.units = "degC" +temp_field.get_property("units") + +# %% +# 5. Digitize the PM2.5 mass concentration and 2-metre temperature fields. +# This step counts the number of values in each of the 10 equally sized bins +# spanning the range of the values. The ``'return_bins=True'`` argument makes +# sure that the calculated bins are also returned: +pm25_indices, pm25_bins = pm25_field.digitize(10, return_bins=True) +temp_indices, temp_bins = temp_field.digitize(10, return_bins=True) + +# %% +# 6. Create a joint histogram of the digitized fields: +joint_histogram = cf.histogram(pm25_indices, temp_indices) + +# %% +# 7. Get histogram data from the ``joint_histogram``: +histogram_data = joint_histogram.array + +# %% +# 8. Calculate bin centres for PM2.5 and temperature bins: +pm25_bin_centers = [(a + b) / 2 for a, b in pm25_bins] +temp_bin_centers = [(a + b) / 2 for a, b in temp_bins] + + +# %% +# 9. Create grids for PM2.5 and temperature bins using `numpy.meshgrid +# `_: +temp_grid, pm25_grid = np.meshgrid(temp_bin_centers, pm25_bin_centers) + +# %% +# 10. Plot the joint histogram using `matplotlib.pyplot.pcolormesh +# `_. Use `cf.Field.unique +# `_ to get +# the unique data array values and show the bin boundaries as ticks on the +# plot. ``'style='plain'`` in `matplotlib.axes.Axes.ticklabel_format +# `_ +# disables the scientific notation on the y-axis: +plt.pcolormesh(temp_grid, pm25_grid, histogram_data, cmap="viridis") +plt.xlabel("2-metre Temperature (degC)") +plt.ylabel("PM2.5 Mass Concentration (kg m**-3)") +plt.xticks(temp_bins.unique().array, rotation=45) +plt.yticks(pm25_bins.unique().array) +plt.colorbar(label="Frequency") +plt.title("Joint Histogram of PM2.5 and 2-metre Temperature", y=1.05) +plt.ticklabel_format(style="plain") +plt.show() diff --git a/docs/source/recipes/plot_10_recipe.py b/docs/source/recipes/plot_10_recipe.py new file mode 100644 index 0000000000..8c1a3f65dd --- /dev/null +++ b/docs/source/recipes/plot_10_recipe.py @@ -0,0 +1,96 @@ +""" +Calculating and plotting the relative vorticity +=============================================== + +Vorticity, the microscopic measure of rotation in a fluid, is a vector field +defined as the curl of velocity +`(James R. Holton, Gregory J. Hakim, An Introduction to Dynamic Meteorology, +2013, Elsevier : Academic Press p95-125) +`_. +In this recipe, we will be calculating and plotting the relative vorticity +from the wind components. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: +f = cf.read("~/recipes/ERA5_monthly_averaged_pressure_levels.nc") +print(f) + +# %% +# 3. Select wind components and look at their contents: +u = f.select_field("eastward_wind") +print(u) + +# %% + +v = f.select_field("northward_wind") +print(v) + +# %% +# 4. Create a date-time object for the required time period: +jan_2023 = cf.year(2023) & cf.month(1) + +# %% +# 5. The relative vorticity is calculated using `cf.curl_xy +# `_ and +# plotted using `cfplot.con `_. +# The ``with cf.relaxed_identities(True)`` context manager statement prevents +# the curl operation broadcasting across the two ``expver`` dimensions because +# it can't be certain that they are the same as they lack the standardised +# metadata. Setting +# ``cf.relaxed_identities(True)`` allows the ``long_name`` to be treated +# as standardised metadata. Since the horizontal coordinates are latitude and +# longitude, the +# `cf.curl_xy `_ +# function automatically accounts for the Earth's spherical geometry when +# calculating the spatial derivatives in the horizontal directions, and for this +# it requires the Earth's radius. In this case the radius is not stored in the +# wind fields, so must be provided by setting ``radius="earth"`` keyword +# parameter. While plotting, the relative vorticity is subspaced for January +# 2023 and one of the `experiment versions` using the dictionary unpacking +# operator (``**``) as there is an equal to sign in the identifier +# (``"long_name=expver"``): + +with cf.relaxed_identities(True): + rv = cf.curl_xy(u, v, radius="earth") + +cfp.con( + rv.subspace(T=jan_2023, **{"long_name=expver": 1}), + lines=False, + title="Relative Vorticity", +) + +# %% +# 6. Although the X axis is cyclic, it is not recognised as such, owing to the +# fact that the longitude coordinate bounds are missing. This results in +# discontinuities in the calculated vorticity field on the plot at the +# wrap-around location of 0 degrees east. The cyclicity could either be set on +# the field itself or just in the curl command by setting ``'x_wrap=True'`` +# while calculating the relative vorticity. Setting ``rv.units = "s-1"``, +# ensures that the units of the relative vorticity field are consistent with +# the calculation and the physical interpretation of the quantity: + +print(v.coordinate("X").has_bounds()) + +# %% + +with cf.relaxed_identities(True): + rv = cf.curl_xy(u, v, x_wrap=True, radius="earth") + +rv.units = "s-1" +print(rv) + +# %% + +cfp.con( + rv.subspace(T=jan_2023, **{"long_name=expver": 1}), + lines=False, + title="Relative Vorticity", +) diff --git a/docs/source/recipes/plot_11_recipe.py b/docs/source/recipes/plot_11_recipe.py new file mode 100644 index 0000000000..b147e51809 --- /dev/null +++ b/docs/source/recipes/plot_11_recipe.py @@ -0,0 +1,92 @@ +""" +Plotting the Warming Stripes +============================ + +In this recipe, we will plot the `Warming Stripes (Climate Stripes) +`_ created by +Professor Ed Hawkins at NCAS, University of Reading. Here we will use the +ensemble mean of the +`HadCRUT.5.0.1.0 analysis gridded data +`_ for +the same. + +""" + +# %% +# 1. Import cf-python and matplotlib.pyplot: + +import matplotlib.pyplot as plt + +import cf + +# %% +# 2. Read the field constructs: +temperature_data = cf.read( + "~/recipes/HadCRUT.5.0.1.0.analysis.anomalies.ensemble_mean.nc" +)[0] +print(temperature_data) + +# %% +# 3. Calculate the annual mean temperature anomalies. The ``'weights=True'`` +# argument is used to take the varying lengths of months into account which +# ensures that the calculated mean is more accurate: +annual_temperature = temperature_data.collapse( + "T: mean", weights=True, group=cf.Y() +) + +# %% +# 4. Select the data from 1850 to 2022: +period = annual_temperature.subspace(T=cf.year(cf.wi(1850, 2022))) + +# %% +# 5. Calculate the global average temperature for each year: +global_temperature = period.collapse("X: Y: mean") + +# %% +# 6. Get the global average temperature and squeeze it to remove the size 1 axis: +global_avg_temp = global_temperature.array.squeeze() + +# %% +# 7. Create a normalisation function that maps the interval from the minimum to +# the maximum temperature to the interval [0, 1] for colouring: +norm_global = plt.Normalize(global_avg_temp.min(), global_avg_temp.max()) + +# %% +# 8. Set the colormap instance: +cmap = plt.get_cmap("RdBu_r") + +# %% +# 9. Create the figure and the axes for the global plot. Loop over the selected +# years, plot a colored vertical stripe for each and remove the axes: +fig_global, ax_global = plt.subplots(figsize=(10, 2)) + +for i in range(global_avg_temp.shape[0]): + ax_global.axvspan( + xmin=i - 0.5, xmax=i + 0.5, color=cmap(norm_global(global_avg_temp[i])) + ) + +ax_global.axis("off") + +plt.show() + +# %% +# 10. For the regional warming stripes, steps 5 to 9 are repeated for the +# specific region. Here, we define the bounding box for UK by subspacing over +# a domain spanning 49.9 to 59.4 degrees north and -10.5 to 1.8 degrees east: +uk_temperature = period.subspace(X=cf.wi(-10.5, 1.8), Y=cf.wi(49.9, 59.4)) +uk_avg_temperature = uk_temperature.collapse("X: Y: mean") +uk_avg_temp = uk_avg_temperature.array.squeeze() +norm_uk = plt.Normalize(uk_avg_temp.min(), uk_avg_temp.max()) + +# %% + +fig_uk, ax_uk = plt.subplots(figsize=(10, 2)) + +for i in range(uk_avg_temp.shape[0]): + ax_uk.axvspan( + xmin=i - 0.5, xmax=i + 0.5, color=cmap(norm_uk(uk_avg_temp[i])) + ) + +ax_uk.axis("off") + +plt.show() diff --git a/docs/source/recipes/plot_12_recipe.py b/docs/source/recipes/plot_12_recipe.py new file mode 100644 index 0000000000..b09db0b29f --- /dev/null +++ b/docs/source/recipes/plot_12_recipe.py @@ -0,0 +1,117 @@ +""" +Using mask to plot Aerosol Optical Depth +======================================== + +In this recipe, we will make use of a +`masked array +`_ +to plot the `high-quality` retrieval of Aerosol Optical Depth (AOD) from all other +retrievals. + +""" + +# %% +# 1. Import cf-python, cf-plot and matplotlib.pyplot: + +import matplotlib.pyplot as plt +import cfplot as cfp + +import cf + +# %% +# 2. Read the field constructs: +fl = cf.read( + "~/recipes/JRR-AOD_v3r0_npp_s202012310752331_e202012310753573_c202100000000000.nc" +) +print(fl) + +# %% +# 3. Select AOD from the field list by identity and look at the contents: +aod = fl.select_field("long_name=AOT at 0.55 micron for both ocean and land") +print(aod) + +# %% +# 4. Select AOD retrieval quality by index and look at the quality flags: +quality = fl[13] +print(quality) + +# %% +# 5. Select latitude and longitude dimensions by identities, with two different +# techniques: +lon = aod.coordinate("long_name=Longitude") +lat = aod.coordinate("Y") + +# %% +# 6. Plot the AOD for all the retrievals using +# `cfplot.con `_. Here the argument +# ``'ptype'`` specifies the type of plot to use (latitude-longitude here) and +# the argument ``'lines=False'`` does not draw contour lines: +cfp.con(f=aod.array, x=lon.array, y=lat.array, ptype=1, lines=False) + +# %% +# 7. Create a mask for AOD based on the quality of the retrieval. The +# ``'__ne__'`` method is an implementation of the ``!=`` operator. It is used to +# create a mask where all the `high-quality` AOD points (with the flag 0) are +# marked as ``False``, and all the other data points (medium quality, low +# quality, or no retrieval) are marked as ``True``: +mask = quality.array.__ne__(0) + +# %% +# 8. Apply the mask to the AOD dataset. The ``'where'`` function takes the +# mask as an input and replaces all the values in the AOD dataset that +# correspond to ``True`` in the mask with a masked value using `cf.masked +# `_. +# In this case, all AOD values that are not of `high-quality` (since they were +# marked as ``True`` in the mask) are masked. This means that the ``high`` +# variable contains only the AOD data that was retrieved with `high-quality`: +high = aod.where(mask, cf.masked) + +# %% +# 9. Now plot both the AOD from `high-quality` retrieval and all other retrievals +# using `cfplot.con `_. Here: +# +# - `cfplot.gopen `_ is used to +# define the parts of the plot area, specifying that the figure should have +# 1 row and 2 columns, which is closed by +# `cfplot.gclose `_; +# - `plt.suptitle `_ +# is used to add a title for the whole figure; +# - the subplots for plotting are selected using +# `cfplot.gpos `_ after which +# `cfplot.mapset `_ is used to +# set the map limits and resolution for the subplots; +# - and as cf-plot stores the plot in a plot object with the name +# ``cfp.plotvars.plot``, country borders are added using normal +# `Cartopy operations `_ +# on the ``cfp.plotvars.mymap`` object: +import cartopy.feature as cfeature + +cfp.gopen(rows=1, columns=2, bottom=0.2) +plt.suptitle("AOD for both ocean and land", fontsize=20) +cfp.gpos(1) +cfp.mapset(resolution="50m", lonmin=68, lonmax=98, latmin=7, latmax=36) +cfp.con( + f=aod.array, + x=lon.array, + y=lat.array, + ptype=1, + lines=False, + title="All retrievals", + colorbar=None, +) +cfp.plotvars.mymap.add_feature(cfeature.BORDERS) +cfp.gpos(2) +cfp.mapset(resolution="50m", lonmin=68, lonmax=98, latmin=7, latmax=36) +cfp.con( + f=high.array, + x=lon.array, + y=lat.array, + ptype=1, + lines=False, + title="High quality retrieval", + colorbar_position=[0.1, 0.20, 0.8, 0.02], + colorbar_orientation="horizontal", + colorbar_title="AOD at 0.55 $\mu$", +) +cfp.plotvars.mymap.add_feature(cfeature.BORDERS) +cfp.gclose() diff --git a/docs/source/recipes/plot_13_recipe.py b/docs/source/recipes/plot_13_recipe.py new file mode 100644 index 0000000000..bf0398713e --- /dev/null +++ b/docs/source/recipes/plot_13_recipe.py @@ -0,0 +1,268 @@ +""" +Calculate and plot the Niño 3.4 Index +===================================== + +In this recipe, we will calculate and plot the sea surface temperature (SST) +anomaly in the Niño 3.4 region. According to `NCAR Climate Data Guide +`_, +the Niño 3.4 anomalies may be thought of as representing the average equatorial +SSTs across the Pacific from about the dateline to the South American coast. +The Niño 3.4 index typically uses a 5-month running mean, and El Niño or La +Niña events are defined when the Niño 3.4 SSTs exceed +/- 0.4 degrees Celsius for a +period of six months or more. + +""" + +# %% +# 1. Import cf-python and cf-plot, as well as some other libraries for use +# in next steps. + +import cartopy.crs as ccrs +import matplotlib.patches as mpatches + +import cfplot as cfp + +import cf + + +# %% +# 2. Read and select the SST by index and look at its contents: +sst = cf.read("~/recipes/ERA5_monthly_averaged_SST.nc")[0] +print(sst) + +# %% +# 3. Set the units from Kelvin to degrees Celsius: +sst.Units = cf.Units("degreesC") + +# %% +# 4. SST is subspaced for the Niño 3.4 region (5N-5S, 170W-120W) and as the +# dataset is using longitudes in 0-360 degrees East format, they are subtracted +# from 360 to convert them: +region = sst.subspace(X=cf.wi(360 - 170, 360 - 120), Y=cf.wi(-5, 5)) + +# %% +# 5. Plot the various Niño regions using cf-plot. Here: +# +# - `cfplot.gopen `_ is used to +# define the parts of the plot area, which is closed by +# `cfplot.gclose `_; +# - `cfplot.mapset `_ is used to +# set the map limits and projection; +# - `cfplot.setvars `_ is used to +# set various attributes of the plot, like setting the land colour to grey; +# - `cfplot.cscale `_ is used to +# choose one of the colour maps amongst many available; +# - `cfplot.con `_ plots contour data +# from the ``region`` subspace at a specific time with no contour lines and a +# title; +# - next, four Niño regions and labels are defined using +# `Matplotlib's Rectangle `_ +# and +# `Text `_ +# function with cf-plot plot object (``cfp.plotvars.plot``): + +cfp.gopen() +cfp.mapset(proj="cyl", lonmin=0, lonmax=360, latmin=-90, latmax=90) +cfp.setvars(land_color="grey") +cfp.cscale(scale="scale1") +cfp.con( + region.subspace(T=cf.dt(2022, 12, 1, 0, 0, 0, 0)), + lines=False, + title="Niño Index Regions", +) + +# Niño 3.4 region(5N-5S, 170W-120W): +rectangle = mpatches.Rectangle( + (-170, -5), + 50, + 10, + fill=False, + linewidth=1, + edgecolor="black", + transform=ccrs.PlateCarree(), +) +cfp.plotvars.mymap.add_patch(rectangle) +cfp.plotvars.mymap.text( + -145, + 7, + "3.4", + horizontalalignment="center", + fontsize=14, + weight="bold", + transform=ccrs.PlateCarree(), +) + +# Niño 1+2 region (0-10S, 90W-80W): +rectangle = mpatches.Rectangle( + (-90, 0), + 10, + 10, + hatch="**", + fill=False, + linewidth=1, + edgecolor="black", + alpha=0.3, + transform=ccrs.PlateCarree(), +) +cfp.plotvars.mymap.add_patch(rectangle) +cfp.plotvars.mymap.text( + -85, + 3, + "1+2", + horizontalalignment="center", + fontsize=8, + weight="bold", + transform=ccrs.PlateCarree(), +) + +# Niño 3 region (5N-5S, 150W-90W): +rectangle = mpatches.Rectangle( + (-150, -5), + 60, + 10, + hatch="xxx", + fill=False, + linewidth=1, + edgecolor="black", + alpha=0.3, + transform=ccrs.PlateCarree(), +) +cfp.plotvars.mymap.add_patch(rectangle) +cfp.plotvars.mymap.text( + -120, + -3, + "3", + horizontalalignment="center", + fontsize=14, + weight="bold", + transform=ccrs.PlateCarree(), +) + +# Niño 4 region (5N-5S, 160E-150W): +rectangle = mpatches.Rectangle( + (-200, -5), + 50, + 10, + hatch="oo", + fill=False, + linewidth=1, + edgecolor="black", + alpha=0.3, + transform=ccrs.PlateCarree(), +) +cfp.plotvars.mymap.add_patch(rectangle) +cfp.plotvars.mymap.text( + -175, + -3, + "4", + horizontalalignment="center", + fontsize=14, + weight="bold", + transform=ccrs.PlateCarree(), +) +cfp.gclose() + +# %% +# 6. Calculate the Niño 3.4 index and standardise it to create an anomaly index. +# The `collapse `_ +# method is used to calculate the mean over the longitude (X) and latitude (Y) +# dimensions: +nino34_index = region.collapse("X: Y: mean") + +# %% +# 7. The result, ``nino34_index``, represents the average SST in the defined +# Niño 3.4 region for each time step. In the variable ``base_period``, +# ``nino34_index`` is subset to only include data from the years 1961 to 1990. +# This period is often used as a reference period for calculating anomalies. +# The variables ``climatology`` and ``std_dev`` include the mean and the +# standard deviation over the time (T) dimension of the ``base_period`` data +# respectively: +base_period = nino34_index.subspace(T=cf.year(cf.wi(1961, 1990))) +climatology = base_period.collapse("T: mean") +std_dev = base_period.collapse("T: sd") + +# %% +# 8. The line for variable ``nino34_anomaly`` calculates the standardised +# anomaly for each time step in the ``nino34_index`` data. It subtracts the +# ``climatology`` from the ``nino34_index`` and then divides by the ``std_dev``. +# The resulting ``nino34_anomaly`` data represents how much the SST in the Niño +# 3.4 region deviates from the 1961-1990 average, in units of standard +# deviations. This is a common way to quantify climate anomalies like El Niño +# and La Niña events: +nino34_anomaly = (nino34_index - climatology) / std_dev + +# %% +# 9. A moving average of the ``nino34_anomaly`` along the time axis, with a +# window size of 5 (i.e. an approximately 5-month moving average) is calculated +# using the +# `moving_window `_ +# method. The ``mode='nearest'`` parameter is used to specify how to pad the +# data outside of the time range. The resulting ``nino34_rolling`` variable +# represents a smoothed version of the ``nino34_anomaly`` data. It removes +# short-term fluctuations and highlights longer-term trends or cycles: +nino34_rolling = nino34_anomaly.moving_window( + method="mean", window_size=5, axis="T", mode="nearest" +) + +# %% +# 10. Define El Niño and La Niña events by creating Boolean masks to identify +# El Niño and La Niña events. Now plot SST anomalies in the Niño 3.4 region over +# time using cf-plot. Here: +# +# - `cfplot.gset `_ sets the limits +# of the x-axis (years from 1940 to 2022) and y-axis (anomalies from -3 +# degrees C to 3 degrees C) for the plot; +# - `cfplot.gopen `_ is used to +# define the parts of the plot area, which is closed by +# `cfplot.gclose `_; +# - `cfplot.lineplot `_ plots +# the rolling Niño 3.4 index over time; +# - a zero line and also horizontal dashed lines are drawn for El Niño and +# La Niña thresholds using +# `Matplotlib's axhline `_ +# with cf-plot plot object (``cfp.plotvars.plot``); +# - `fill_between `_ +# from Matplotlib is used with cf-plot plot object (``cfp.plotvars.plot``) +# to fill the area between the Niño 3.4 index and the El Niño/La Niña +# thresholds; +# - similarly, +# `cfplot.plotvars.plot.legend `_ +# is used to add a legend in the end: +elnino = nino34_rolling >= 0.4 +lanina = nino34_rolling <= -0.4 + +cfp.gset(xmin="1940-1-1", xmax="2022-12-31", ymin=-3, ymax=3) + +cfp.gopen(figsize=(10, 6)) +cfp.lineplot( + nino34_rolling, + color="black", + title="SST Anomaly in Niño 3.4 Region (5N-5S, 120-170W)", + ylabel="Temperature anomaly ($\degree C$)", + xlabel="Year", +) +cfp.plotvars.plot.axhline( + 0.4, color="red", linestyle="--", label="El Niño Threshold" +) +cfp.plotvars.plot.axhline( + -0.4, color="blue", linestyle="--", label="La Niña Threshold" +) +cfp.plotvars.plot.axhline(0, color="black", linestyle="-", linewidth=1) +cfp.plotvars.plot.fill_between( + nino34_rolling.coordinate("T").array, + 0.4, + nino34_rolling.array.squeeze(), + where=elnino.squeeze(), + color="red", + alpha=0.3, +) +cfp.plotvars.plot.fill_between( + nino34_rolling.coordinate("T").array, + -0.4, + nino34_rolling.array.squeeze(), + where=lanina.squeeze(), + color="blue", + alpha=0.3, +) +cfp.plotvars.plot.legend(frameon=False, loc="lower center", ncol=2) +cfp.gclose() diff --git a/docs/source/recipes/plot_14_recipe.py b/docs/source/recipes/plot_14_recipe.py new file mode 100644 index 0000000000..957a3f38d7 --- /dev/null +++ b/docs/source/recipes/plot_14_recipe.py @@ -0,0 +1,181 @@ +""" +Overlay Geopotential height contours over Temperature anomalies +=============================================================== + +In this recipe, we will overlay Geopotential height contours over Temperature +anomalies to help analyse meteorological conditions during July 2018, +specifically focusing on the significant concurrent extreme events that occurred +during the 2018 boreal spring/summer season in the Northern Hemisphere. + +""" + +# %% +# 1. Import cf-python and cf-plot: +import cfplot as cfp + +import cf + +# %% +# 2. Read and select the 200 hPa geopotential by index and look at its contents: +gp = cf.read("~/recipes/ERA5_monthly_averaged_z200.nc")[0] +print(gp) + +# %% +# 3. Convert the geopotential data to geopotential height by dividing it by the +# acceleration due to gravity (approximated as 9.81 :math:`m \cdot {s}^{-2}`): +gph = gp / 9.81 + +# %% +# 4. Subset the geopotential height to extract data specifically for July 2018, +# a significant month due to heat extremes and heavy rainfall: +gph_july = gph.subspace(T=cf.month(7) & cf.year(2018)).squeeze() + +# %% +# 5. Plot contour lines of this geopotential height for July 2018. Here: +# +# - `cfplot.gopen `_ is used to +# define the parts of the plot area, which is closed by +# `cfplot.gclose `_; +# - `cfplot.mapset `_ is used to +# set the map projection to North Polar Stereographic; +# - `cfplot.setvars `_ is used to +# set various attributes of the plot, like setting the thickness of the lines +# that represent continents; +# - `cfplot.con `_ plots the contour +# lines representing the 200 hPa geopotential height values without filling +# between the contour lines (``fill=False``) and no colour bar +# (``colorbar=False``); +# - `cfplot.levs `_ is used to +# specify two contour levels, 12000 and 12300 m, corresponding to the +# approximate polar-front jet and subtropical jet respectively; +# - `cfplot.con `_ is again used to +# plot the contour lines for polar-front jet and subtropical jet with a +# thicker line width; +# - `cfp.plotvars.mymap.stock_img() `_ +# then finally visualises the Earth's surface in cf-plot's +# ``cfp.plotvars.mymap`` plot object: +cfp.gopen() +cfp.mapset(proj="npstere") +cfp.setvars(continent_thickness=0.5) + +cfp.con( + f=gph_july, + fill=False, + lines=True, + line_labels=False, + colors="black", + linewidths=1, + colorbar=False, +) + +cfp.levs(manual=[12000, 12300]) +cfp.con( + f=gph_july, + fill=False, + lines=True, + colors="black", + linewidths=3.0, + colorbar=False, +) + +cfp.plotvars.mymap.stock_img() +cfp.gclose() + +# %% +# 6. Read and select the 2-metre temperature by index and look at its contents: +t2m = cf.read("~/recipes/ERA5_monthly_averaged_t2m.nc")[0] +print(t2m) + +# %% +# 7. Set the units from Kelvin to degrees Celsius: +t2m.Units = cf.Units("degreesC") + +# %% +# 8. Extract a subset for July across the years for ``t2m``: +t2m_july = t2m.subspace(T=cf.month(7)) + +# %% +# 9. The 2-meter temperature climatology is then calculated for the month of +# July over the period from 1981 to 2010, which provides a baseline against +# which anomalies in later years are compared: +t2m_july_climatology = t2m_july.subspace( + T=cf.year(cf.wi(1981, 2010)) +).collapse("T: mean") + +# %% +# 10. Calculate the temperature anomaly for the month of July in the year 2018 +# relative to the climatological baseline (``t2m_july_climatology``). This +# indicates how much the temperatures for that month in that year deviated from +# the long-term average for July across the 1981-2010 period: +t2m_july_anomaly_2018 = ( + t2m_july.subspace(T=cf.year(2018)).squeeze() - t2m_july_climatology +) + +# %% +# 11. +# The July 2018 season experienced extreme heat in many parts of the Northern +# Hemisphere. This period's extreme events were related to unusual +# meteorological conditions, particularly abnormalities in the jet stream. To +# provide an insight into the atmospheric conditions, the temperature anomalies +# and the geopotential height contours are plotted using cf-plot. Here: +# +# - `cfplot.gopen `_ is used to +# define the parts of the plot area, which is closed by +# `cfplot.gclose `_; +# - `cfplot.mapset `_ is used to +# set the map projection to Robinson; +# - `cfplot.setvars `_ is used to +# set various attributes of the plot, like setting the thickness of the lines +# that represent continents and master title properties; +# - `cfplot.levs `_ is used to +# specify the contour levels for temperature anomalies, starting from -2 to 2 +# with an interval of 0.5; +# - `cfplot.cscale `_ is used to +# choose one of the colour maps amongst many available; +# - `cfplot.con `_ plots contour fill +# of temperature anomalies without contour lines (``lines=False``); +# - `cfplot.levs() `_ is used to +# reset contour levels to default after which the steps to plot the contour +# lines representing the 200 hPa geopotential height values, the approximate +# polar-front jet and subtropical jet from Step 5 are repeated: +cfp.gopen() +cfp.mapset(proj="robin") +cfp.setvars( + continent_thickness=0.5, + master_title="July 2018", + master_title_fontsize=22, + master_title_location=[0.53, 0.83], +) + +cfp.levs(min=-2, max=2, step=0.5) +cfp.cscale("temp_19lev") +cfp.con( + f=t2m_july_anomaly_2018, + lines=False, + colorbar_title="Temperature anomaly relative to 1981-2010 ($\degree C$)", + colorbar_fontsize=13, + colorbar_thick=0.04, +) + +cfp.levs() +cfp.con( + f=gph_july, + fill=False, + lines=True, + line_labels=False, + colors="black", + linewidths=1, + colorbar=False, +) + +cfp.levs(manual=[12000, 12300]) +cfp.con( + f=gph_july, + fill=False, + lines=True, + colors="black", + linewidths=3.0, + colorbar=False, +) + +cfp.gclose() diff --git a/docs/source/recipes/plot_15_recipe.py b/docs/source/recipes/plot_15_recipe.py new file mode 100644 index 0000000000..d20f622826 --- /dev/null +++ b/docs/source/recipes/plot_15_recipe.py @@ -0,0 +1,163 @@ +""" +Resampling Land Use Flags to a Coarser Grid +=========================================== + +In this recipe, we will compare the land use distribution in different countries +using a land use data file and visualize the data as a histogram. This will help +to understand the proportion of different land use categories in each country. + +The land use data is initially available at a high spatial resolution of +approximately 100 m, with several flags defined with numbers representing the +type of land use. Regridding the data to a coarser resolution of approximately +25 km would incorrectly represent the flags on the new grids. + +To avoid this, we will resample the data to the coarser resolution by +aggregating the data within predefined spatial regions or bins. This approach +will give a dataset where each 25 km grid cell contains a histogram of land use +flags, as determined by the original 100 m resolution data. It retains the +original spatial extent of the data while reducing its spatial complexity. +Regridding, on the other hand, involves interpolating the data onto a new grid, +which can introduce artefacts and distortions in the data. + +""" + +import cartopy.io.shapereader as shpreader +import matplotlib.pyplot as plt +import numpy as np + +# %% +# 1. Import the required libraries. We will use Cartopy's ``shapereader`` to +# work with shapefiles that define country boundaries: +import cf + +# %% +# 2. Read and select land use data by index and see properties of +# all constructs: +f = cf.read("~/recipes/output.tif.nc")[0] +f.dump() + + +# %% +# 3. Define a function to extract data for a specific country: +# +# - The ``extract_data`` function is defined to extract land use data for a +# specific country, specified by the ``country_name`` parameter. +# - It uses the `Natural Earth `_ +# shapefile to get the bounding coordinates of the selected country. +# - The `shpreader.natural_earth `_ +# function is called to access the Natural +# Earth shapefile of country boundaries with a resolution of 10 m. +# - The `shpreader.Reader `_ +# function reads the shapefile, and the selected country's record is retrieved +# by filtering the records based on the ``NAME_LONG`` attribute. +# - The bounding coordinates are extracted using the ``bounds`` attribute of the +# selected country record. +# - The land use data file is then read and subset using these bounding +# coordinates with the help of the ``subspace`` function. The subset data is +# stored in the ``f`` variable. + + +def extract_data(country_name): + shpfilename = shpreader.natural_earth( + resolution="10m", category="cultural", name="admin_0_countries" + ) + reader = shpreader.Reader(shpfilename) + country = [ + country + for country in reader.records() + if country.attributes["NAME_LONG"] == country_name + ][0] + lon_min, lat_min, lon_max, lat_max = country.bounds + + f = cf.read("~/recipes/output.tif.nc")[0] + f = f.subspace(X=cf.wi(lon_min, lon_max), Y=cf.wi(lat_min, lat_max)) + + return f + + +# %% +# 4. Define a function to plot a histogram of land use distribution for a +# specific country: +# +# - The `digitize `_ +# function of the ``cf.Field`` object is called to convert the land use data +# into indices of bins. It takes an array of bins (defined by +# the `np.linspace `_ function) +# and the ``return_bins=True`` parameter, which returns the actual bin values +# along with the digitized data. +# - The `np.linspace `_ +# function is used to create an array of evenly spaced bin edges from 0 to 50, +# with 51 total values. This creates bins of width 1. +# - The ``digitized`` variable contains the bin indices for each data point, +# while the bins variable contains the actual bin values. +# - The `cf.histogram `_ +# function is called on the digitized data to create a histogram. This +# function returns a field object with the histogram data. +# - The `squeeze `_ +# function applied to the histogram ``array`` extracts the histogram data as a NumPy +# array and removes any single dimensions. +# - The ``total_valid_sub_cells`` variable calculates the total number of valid +# subcells (non-missing data points) by summing the histogram data. +# - The last element of the bin_counts array is removed with slicing +# (``bin_counts[:-1]``) to match the length of the ``bin_indices`` array. +# - The ``percentages`` variable calculates the percentage of each bin by +# dividing the ``bin_counts`` by the ``total_valid_sub_cells`` and multiplying +# by 100. +# - The ``bin_indices`` variable calculates the centre of each bin by averaging +# the bin edges. This is done by adding the ``bins.array[:-1, 0]`` and +# ``bins.array[1:, 0]`` arrays and dividing by 2. +# - The ``ax.bar`` function is called to plot the histogram as a bar chart on +# the provided axis. The x-axis values are given by the ``bin_indices`` array, +# and the y-axis values are given by the ``percentages`` array. +# - The title, x-axis label, y-axis label, and axis limits are set based on the +# input parameters. + + +def plot_histogram(field, ax, label, ylim, xlim): + digitized, bins = field.digitize(np.linspace(0, 50, 51), return_bins=True) + + h = cf.histogram(digitized) + bin_counts = h.array.squeeze() + + total_valid_sub_cells = bin_counts.sum() + + bin_counts = bin_counts[:-1] + + percentages = bin_counts / total_valid_sub_cells * 100 + + bin_indices = (bins.array[:-1, 0] + bins.array[1:, 0]) / 2 + + ax.bar(bin_indices, percentages, label=label) + ax.set_title(label) + ax.set_xlabel("Land Use Flag") + ax.set_ylabel("Percentage") + ax.set_ylim(ylim) + ax.set_xlim(xlim) + + +# %% +# 5. Define the countries of interest: +countries = ["Ireland", "Belgium", "Switzerland"] + +# %% +# 6. Set up the figure and axes for plotting the histograms: +# +# - The ``plt.subplots`` function is called to set up a figure with three +# subplots, with a figure size of 8 inches by 10 inches. +# - A loop iterates over each country in the countries list and for each +# country, the ``extract_data`` function is called to extract its land use +# data. +# - The ``plot_histogram`` function is then called to plot the histogram of land +# use distribution on the corresponding subplot. +# - The ``plt.tight_layout`` function is called to ensure that the subplots are +# properly spaced within the figure and finally, the ``plt.show`` function +# displays the figure with the histograms. +fig, axs = plt.subplots(3, 1, figsize=(8, 10)) + +for i, country in enumerate(countries): + ax = axs[i] + data = extract_data(country) + plot_histogram(data, ax, label=country, ylim=(0, 50), xlim=(0, 50)) + +plt.tight_layout() +plt.show() diff --git a/docs/source/recipes/plot_16_recipe.py b/docs/source/recipes/plot_16_recipe.py new file mode 100644 index 0000000000..00a7f2e1d0 --- /dev/null +++ b/docs/source/recipes/plot_16_recipe.py @@ -0,0 +1,57 @@ +""" +Plotting contour subplots with different projections +==================================================== + +In this recipe, we will plot the same data using different projections +as subplots to illustrate visually some available possibilities. + +""" + +# %% +# 1. Import cf-python and cf-plot: + +import cfplot as cfp + +import cf + +# %% +# 2. Read the field in: +f = cf.read("~/recipes/ggap.nc")[0] + +# %% +# 3. List the projection types to use. Here we are using +# Cylindrical/Default, North Pole Stereographic, South Pole Stereographic, +# Mollweide, Mercator and Robinson. However there are several other choices +# possible, see: +# https://ncas-cms.github.io/cf-plot/build/user_guide.html#appendixc. Our +# chosen list is: +projtypes = ["cyl", "npstere", "spstere", "moll", "merc", "robin"] + +# %% +# 4. Create the file with subplots. If changing the number of subplots, +# ensure the number of rows * number of columns = the number of projections. +# Here we are doing 6 projections so 2 x 3 is fine. Then loop through the +# list of projection types and plot each as a sub-plot: +cfp.gopen(rows=2, columns=3, bottom=0.2) +for i, proj in enumerate(projtypes): + # gpos has 1 added to the index because it takes 1 as its first value + cfp.gpos(i + 1) + cfp.mapset(proj=proj) + + # For the final plot only, add a colour bar to cover all the sub-plots + if i == len(projtypes) - 1: + cfp.con( + f.subspace(pressure=850), + lines=False, + title=proj, + colorbar_position=[0.1, 0.1, 0.8, 0.02], + colorbar_orientation="horizontal", + ) + else: + cfp.con( + f.subspace(pressure=850), + lines=False, + title=proj, + colorbar=False, + ) +cfp.gclose() diff --git a/docs/source/recipes/plot_17_recipe.py b/docs/source/recipes/plot_17_recipe.py new file mode 100644 index 0000000000..c94769e2ba --- /dev/null +++ b/docs/source/recipes/plot_17_recipe.py @@ -0,0 +1,109 @@ +""" +Plotting contour subplots with different colour maps/scales +=========================================================== + +In this recipe, we will plot the same data with different colour maps from +three categories in separate subplots to illustrate the importance of +choosing a suitable one for given data. To avoid unintended bias and +misrepresentation, or lack of accessibility, a careful choice must be made. +""" + +# %% +# 1. Import cf-python and cf-plot: + +import matplotlib.pyplot as plt +import cfplot as cfp + +import cf + +# %% +# 2. Read the field in: +f = cf.read("~/recipes/ggap.nc")[0] + +# %% +# 3. Choose a set of predefined colour scales to view. These can be chosen +# from the selection at: +# https://ncas-cms.github.io/cf-plot/build/colour_scales.html or you +# can define your own, see: +# https://ncas-cms.github.io/cf-plot/build/colour_scales.html#user-defined-colour-scales. +# Here we take three colour scales each from three different general +# categories, to showcase some differences in representation. +# Note colour scale levels can be adjusted using 'cscale' and keywords such as: +# cfp.cscale(, ncols=16, below=2, above=14) + +# %% +# a. Perceptually uniform colour scales, with no zero value, see: +# https://ncas-cms.github.io/cf-plot/build/colour_scales.html#perceptually-uniform-colour-scales. +colour_scales_pu = ["viridis", "magma", "plasma"] + +# %% +# b. NCAR Command Language colour scales enhanced to help with colour +# blindness, see: +# https://ncas-cms.github.io/cf-plot/build/colour_scales.html#ncar-command-language-enhanced-to-help-with-colour-blindness. +# These colour maps are better for accessibility. +colour_scales_ncl = ["posneg_1", "GreenMagenta16", "StepSeq25"] + +# %% +# c. Orography/bathymetry colour scales, see: +# https://ncas-cms.github.io/cf-plot/build/colour_scales.html#orography-bathymetry-colour-scales. +# These are used to show the shape/contour of land masses, but bear in mind the +# data we show here is air temperature so doesn't represent this and +# therefore it is not a good choice in this case: +colour_scales_ob = ["wiki_1_0_2", "wiki_2_0", "wiki_2_0_reduced"] + + +# %% +# 4. We plot each category of colourmap in a given columns of the subplot, +# but given the 'gpos' function positions subplots from left to right, row by +# row from the top, we need to interleave the values in a list. We can use +# zip to do this: +colour_scales_columns = [ + cscale + for category in zip(colour_scales_pu, colour_scales_ncl, colour_scales_ob) + for cscale in category +] + + +# %% +# 5. Create the figure and give it an overall title. Ensure the +# number of rows * number of columns = number of colour scales. +# Then we loop through all the different colour maps defined and plot +# as subplots, with each category in the same column, labelling each column +# with the colour scale category: +cfp.gopen(rows=3, columns=3, bottom=0.1, top=0.85) +plt.suptitle( + ( + "Air temperature (K) at 850 mbar pressure shown in different " + "categories of colour scale" + ), + fontsize=18, +) +for i, colour_scale in enumerate(colour_scales_columns): + cfp.gpos(i + 1) + cfp.cscale(colour_scale, ncols=15) + + # For the topmost plots, label the column with the colour scale category + # using the 'title' argument, otherwise don't add a title. + # Ensure the order the titles are written in corresponds to the + # order unzipped in step 4, so the columns match up correctly. + if i == 0: + set_title = "Perceptually uniform\ncolour maps" + elif i == 1: + set_title = "NCL colour maps enhanced to \nhelp with colour blindness" + elif i == 2: + set_title = "Orography/bathymetry\ncolour maps" + else: + set_title = "" + + cfp.con( + f.subspace(pressure=850), + title=set_title, + lines=False, + axes=False, + colorbar_drawedges=False, + colorbar_title=f"Shown in '{colour_scale}'", + colorbar_fraction=0.04, + colorbar_thick=0.02, + colorbar_fontsize=11, + ) +cfp.gclose() diff --git a/docs/source/recipes/plot_18_recipe.py b/docs/source/recipes/plot_18_recipe.py new file mode 100644 index 0000000000..f0eae36e35 --- /dev/null +++ b/docs/source/recipes/plot_18_recipe.py @@ -0,0 +1,143 @@ +""" +Calculating the Pearson correlation coefficient between datasets +================================================================ + +In this recipe, we will take two datasets, one for an independent variable +(in this example elevation) and one for a dependent variable (snow +cover over a particular day), regrid them to the same resolution then +calculate the correlation coefficient, to get a measure of the relationship +between them. + +""" + +# %% +# 1. Import cf-python, cf-plot and other required packages: +import matplotlib.pyplot as plt +import scipy.stats.mstats as mstats +import cfplot as cfp + +import cf + + +# %% +# 2. Read the data in and unpack the Fields from FieldLists using indexing. +# In our example We are investigating the influence of the land height on +# the snow cover extent, so snow cover is the dependent variable. The snow +# cover data is the +# 'Snow Cover Extent 2017-present (raster 500 m), Europe, daily – version 1' +# sourced from the Copernicus Land Monitoring Service which is described at: +# https://land.copernicus.eu/en/products/snow/snow-cover-extent-europe-v1-0-500m +# and the elevation data is the 'NOAA NGDC GLOBE topo: elevation data' dataset +# which can be sourced from the IRI Data Library, or details found, at: +# http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NGDC/.GLOBE/.topo/index.html. +orog = cf.read("~/recipes/1km_elevation.nc")[0] +snow = cf.read("~/recipes/snowcover")[0] + +# %% +# 3. Choose the day of pre-aggregated snow cover to investigate. We will +# take the first datetime element corresponding to the first day from the +# datasets, 1st January 2024, but by changing the indexing you can explore +# other days by changing the index. We also get the string corresponding to +# the date, to reference later: +snow_day = snow[0] +snow_day_dt = snow_day.coordinate("time")[0].data +snow_day_daystring = f"{snow_day_dt.datetime_as_string[0].split(' ')[0]}" + +# %% +# 4. Choose the region to consider to compare the relationship across, +# which must be defined across both datasets, though not necessarily on the +# same grid since we regrid to the same grid next and subspace to the same +# area for both datasets ready for comparison in the next steps. By changing +# the latitude and longitude points in the tuple below, you can change the +# area that is used: +region_in_mid_uk = ((-3.0, -1.0), (52.0, 55.0)) +sub_orog = orog.subspace( + longitude=cf.wi(*region_in_mid_uk[0]), latitude=cf.wi(*region_in_mid_uk[1]) +) +sub_snow = snow_day.subspace( + longitude=cf.wi(*region_in_mid_uk[0]), latitude=cf.wi(*region_in_mid_uk[1]) +) + +# %% +# 5. Ensure data quality, since the standard name here corresponds to a +# unitless fraction, but the values are in the tens, so we need to +# normalise these to all lie between 0 and 1 and change the units +# appropriately: +sub_snow = (sub_snow - sub_snow.minimum()) / (sub_snow.range()) +sub_snow.override_units("1", inplace=True) + +# %% +# 6. Regrid the data so that they lie on the same grid and therefore each +# array structure has values with corresponding geospatial points that +# can be statistically compared. Here the elevation field is regridded to the +# snow field since the snow is higher-resolution, but the other way round is +# possible by switching the field order: +regridded_orog = sub_orog.regrids(sub_snow, method="linear") + +# %% +# 7. Squeeze the snow data to remove the size 1 axes so we have arrays of +# the same dimensions for each of the two fields to compare: +sub_snow = sub_snow.squeeze() + +# %% +# 8. Finally, perform the statistical calculation by using the SciPy method +# to find the Pearson correlation coefficient for the two arrays now they are +# in comparable form. Note we need to use 'scipy.stats.mstats' and not +# 'scipy.stats' for the 'pearsonr' method, to account for masked +# data in the array(s) properly: +coefficient = mstats.pearsonr(regridded_orog.array, sub_snow.array) +print(f"The Pearson correlation coefficient is: {coefficient}") + +# %% +# 9. Make a final plot showing the two arrays side-by-side and quoting the +# determined Pearson correlation coefficient to illustrate the relationship +# and its strength visually. We use 'gpos' to position the plots in two +# columns and apply some specific axes ticks and labels for clarity. +cfp.gopen( + rows=1, + columns=2, + top=0.85, + user_position=True, +) + +# Joint configuration of the plots, including adding an overall title +plt.suptitle( + ( + "Snow cover compared to elevation for the same area of the UK " + f"aggregated across\n day {snow_day_daystring} with correlation " + "coefficient (on the same grid) of " + f"{coefficient.statistic:.4g} (4 s.f.)" + ), + fontsize=17, +) +cfp.mapset(resolution="10m") +cfp.setvars(ocean_color="white", lake_color="white") +label_info = { + "xticklabels": ("3W", "2W", "1W"), + "yticklabels": ("52N", "53N", "54N", "55N"), + "xticks": (-3, -2, -1), + "yticks": (52, 53, 54, 55), +} + +# Plot the two contour plots as columns +cfp.gpos(1) +cfp.cscale("wiki_2_0_reduced", ncols=11) +cfp.con( + regridded_orog, + lines=False, + title="Elevation (from 1km-resolution orography)", + colorbar_drawedges=False, + **label_info, +) +cfp.gpos(2) +# Don't add extentions on the colourbar since it can only be 0 to 1 inclusive +cfp.levs(min=0, max=1, step=0.1, extend="neither") +cfp.cscale("precip_11lev", ncols=11, reverse=1) +cfp.con( + sub_snow, + lines=False, + title="Snow cover extent (from satellite imagery)", + colorbar_drawedges=False, + **label_info, +) +cfp.gclose() diff --git a/docs/source/recipes/plot_19_recipe.py b/docs/source/recipes/plot_19_recipe.py new file mode 100644 index 0000000000..dcc0926fbd --- /dev/null +++ b/docs/source/recipes/plot_19_recipe.py @@ -0,0 +1,105 @@ +""" +Plotting per-season trends in global sea surface temperature extrema +==================================================================== + +In this recipe we find the area-based extrema of global sea surface +temperature per month and, because it is very difficult to +interpret for trends when in a monthly form, we calculate and plot +on top of this the mean across each season for both the minima and the +maxima. +""" + +# %% +# 1. Import cf-python, cf-plot and other required packages: +import matplotlib.pyplot as plt +import cfplot as cfp + +import cf + +# %% +# 2. Read the dataset in extract the SST Field from the FieldList: +f = cf.read("~/recipes/ERA5_monthly_averaged_SST.nc") +sst = f[0] # this gives the sea surface temperature (SST) + +# %% +# 3. Collapse the SST data by area extrema (extrema over spatial dimensions): +am_max = sst.collapse("area: maximum") # equivalent to "X Y: maximum" +am_min = sst.collapse("area: minimum") # equivalent to "X Y: minimum" + +# %% +# 4. Reduce all timeseries down to just 1980+ since there are some data +# quality issues before 1970 and also this window is about perfect size +# for viewing the trends without the line plot becoming too cluttered: +am_max = am_max.subspace(T=cf.ge(cf.dt("1980-01-01"))) +am_min = am_min.subspace(T=cf.ge(cf.dt("1980-01-01"))) + +# %% +# 5. Create a mapping which provides the queries we need to collapse on +# the four seasons, along with our description of them, as a value, with +# the key of the string encoding the colour we want to plot these +# trend lines in. This structure will be iterated over to make our plot: +colours_seasons_mapping = { + "red": (cf.mam(), "Mean across MAM: March, April and May"), + "blue": (cf.jja(), "Mean across JJA: June, July and August"), + "green": (cf.son(), "Mean across SON: September, October and November"), + "purple": (cf.djf(), "Mean across DJF: December, January and February"), +} + +# %% +# 6. Create and open the plot file. Put maxima subplot at top since these +# values are higher, given increasing x axis. +# Note we set limits manually with 'gset' only to +# allow space so the legend doesn't overlap the data, which isn't +# possible purely from positioning it anywhere within the default plot. +# Otherwise cf-plot handles this for us. To plot the per-season means +# of the maxima, we loop through the season query mapping and do a +# "T: mean" collapse setting the season as the grouping: +cfp.gopen( + rows=2, columns=1, bottom=0.1, top=0.85, +) +cfp.gpos(1) +cfp.gset(xmin="1980-01-01", xmax="2022-12-01", ymin=304, ymax=312) +for colour, season_query in colours_seasons_mapping.items(): + query_on_season, season_description = season_query + am_max_collapse = am_max.collapse("T: mean", group=query_on_season) + cfp.lineplot( + am_max_collapse, + color=colour, + markeredgecolor=colour, + marker="o", + label=season_description, + title="Maxima per month or season", + ) +cfp.lineplot( + am_max, + color="grey", + xlabel="", + label="All months", +) +# Create and add minima subplot below the maxima one. Just like for the +# maxima case, we plot per-season means by looping through the season query +# mapping and doing a "T: mean" collapse setting the season as the grouping +cfp.gpos(2) +cfp.gset(xmin="1980-01-01", xmax="2022-12-01", ymin=269, ymax=272) +for colour, season_query in colours_seasons_mapping.items(): + query_on_season, season_description = season_query + am_min_collapse = am_min.collapse("T: mean", group=query_on_season) + cfp.lineplot( + am_min_collapse, + color=colour, + markeredgecolor=colour, + marker="o", + xlabel="", + title="Minima per month or season", + ) +cfp.lineplot( + am_min, + color="grey", +) +# Add an overall title to the plot and close the file to save it +plt.suptitle( + "Global mean sea surface temperature (SST) monthly\nminima and maxima " + "showing seasonal means of these extrema", + fontsize=18, +) +cfp.gclose() diff --git a/docs/source/recipes/plot_20_recipe.py b/docs/source/recipes/plot_20_recipe.py new file mode 100644 index 0000000000..95e02b01d7 --- /dev/null +++ b/docs/source/recipes/plot_20_recipe.py @@ -0,0 +1,97 @@ +""" +Calculating and plotting the divergence of sea currents +======================================================= + +In this recipe, we will calculate the divergence of depth-averaged +currents in the Irish Sea, then plot the divergence as a contour +fill plot underneath the vectors themselves in the form of a vector plot. +""" + +# %% +# 1. Import cf-python and cf-plot: +import cfplot as cfp + +import cf + +# %% +# 2. Read the fields in. This dataset consists of depth-averaged eastward and +# northward current components plus the sea surface height above sea level and +# is a gridded dataset, with grid resolution of 1.85 km, covering the entire +# Irish Sea area. It was found via the CEDA Archive at the location of: +# https://catalogue.ceda.ac.uk/uuid/1b89e025eedd49e8976ee0721ec6e9b5, with +# DOI of https://dx.doi.org/10.5285/031e7ca1-9710-280d-e063-6c86abc014a0: +f = cf.read("~/recipes/POLCOMS_WAM_ZUV_01_16012006.nc") + +# %% +# 3. Get the separate vector components, which are stored as separate fields. +# The first, 'u', corresponds to the eastward component and the second, 'v', +# the northward component: +u = f[0] +v = f[1] + +# %% +# 4. Squeeze the fields to remove the size 1 axes in each case: +u = u.squeeze() +v = v.squeeze() + +# %% +# 5. Consider the currents at a set point in time. To do this we +# select one of the 720 datetime sample points in the fields to +# investigate, in this case by subspacing to pick out a particular +# datetime value we saw within the time coordinate data of the field (but +# you could also use indexing or filtering to select a specific value). +# Once we subspace to one datetime, we squeeze out the size 1 time axis +# in each case: +chosen_time = "2006-01-15 23:30:00" # 720 choices to pick from, try this one! +u_1 = u.subspace(T=cf.dt(chosen_time)) +v_1 = v.subspace(T=cf.dt(chosen_time)) +u_1 = u_1.squeeze() +v_1 = v_1.squeeze() + +# %% +# 6. +# When inspecting the u and v fields using cf inspection methods such as +# from print(u_1.data) and u_1.data.dump(), for example, we can see that there are +# lots of -9999 values in their data array, apparently used as a +# fill/placeholder value, including to indicate undefined data over the land. +# In order for these to not skew the data and dominate the plot, we need +# to mask values matching this, so that only meaningful values remain. +u_2 = u_1.where(cf.lt(-9000), cf.masked) +v_2 = v_1.where(cf.lt(-9000), cf.masked) + +# %% +# 7. Calculate the divergence using the 'div_xy' function operating on the +# vector eastward and northward components as the first and second argument +# respectively. We need to calculate this for the latitude-longitude plane +# of the Earth, defined in spherical polar coordinates, so we must specify +# the Earth's radius for the appropriate calculation: +div = cf.div_xy(u_2, v_2, radius="earth") + +# %% +# 8. First we configure the overall plot by +# making the map higher resolution, to show the coastlines of the UK and +# Ireland in greater detail, and changing the colourmap to better reflect +# the data which can be positive or negative, i.e. has 0 as the 'middle' +# value of significance, so should use a diverging colour map. +cfp.mapset(resolution="10m") +cfp.cscale("ncl_default", ncols=21) + +# %% +# 9. Now generate the final plot. Plot the current vectors, noting we had +# to play around with the 'stride' and 'scale' parameter values to adjust +# the vector spacing and size so that the vector field is best represented +# and visible without over-cluttering the plot. Finally we plot the +# divergence as a contour plot without any lines showing. This compound +# plot is saved on one canvas using 'gopen' and 'gclose' to wrap the two +# plotting calls: +cfp.gopen() +cfp.vect(u=u_2, v=v_2, stride=6, scale=3, key_length=1) +cfp.con( + div, + lines=False, + title=( + f"Depth-averaged Irish Sea currents at {chosen_time} with " + "their divergence" + ), +) +cfp.gclose() From c62c49453749c3650601c275744f2f7276a61004 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 21 Oct 2025 16:19:50 +0100 Subject: [PATCH 03/13] Fix a few broken intersphinx mappings by using updated links --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 945eec8118..4d43520233 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -148,7 +148,7 @@ def _get_date(): "sphinx": ("https://www.sphinx-doc.org/en/master/", None), "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable", None), - "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), + "scipy": ("https://docs.scipy.org/doc/scipy", None), # 'netCDF4': ("https://unidata.github.io/netcdf4-python", None), "cftime": ("https://unidata.github.io/cftime", None), "cfunits": ("https://ncas-cms.github.io/cfunits", None), From 7d306d8c5e7ce5da5d862fc457badd28b548043e Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 21 Oct 2025 18:45:04 +0100 Subject: [PATCH 04/13] Update sphinx-gallery settings in Sphinx configuration --- docs/source/conf.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4d43520233..0fd99f4d31 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -145,7 +145,7 @@ def _get_date(): intersphinx_cache_limit = 5 # days to keep the cached inventories intersphinx_mapping = { - "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master", None), "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), @@ -153,12 +153,12 @@ def _get_date(): "cftime": ("https://unidata.github.io/cftime", None), "cfunits": ("https://ncas-cms.github.io/cfunits", None), "cfdm": ("https://ncas-cms.github.io/cfdm", None), - "cfplot": ("https://ncas-cms.github.io/cf-plot/build/", None), + "cfplot": ("https://ncas-cms.github.io/cf-plot", None), "dask": ("https://docs.dask.org/en/latest", None), - "matplotlib": ("https://matplotlib.org/stable/", None), + "matplotlib": ("https://matplotlib.org/stable", None), # REVIEW: h5: new intersphinx mapping "h5netcdf": ("https://h5netcdf.org", None), - "zarr": ("https://zarr.readthedocs.io/en/stable/", None), + "zarr": ("https://zarr.readthedocs.io/en/stable", None), } # This extension is meant to help with the common pattern of having @@ -387,16 +387,18 @@ def _get_date(): "examples_dirs": "recipes", # path to recipe files "gallery_dirs": "recipes", # path to save gallery generated output "run_stale_examples": False, - "reference_url": {"cf": None}, + # Below setting can be buggy: see: + # https://github.com/sphinx-gallery/sphinx-gallery/issues/967 + #"reference_url": {"cf": None}, "backreferences_dir": "gen_modules/backreferences", "doc_module": ("cf",), "inspect_global_variables": True, "within_subsection_order": FileNameSortKey, - "default_thumb_file": "_static/logo.svg", + "default_thumb_file": "_static/cf-recipe-placeholder-squarecrop.png", "image_scrapers": ( "matplotlib", ), # Ensures Matplotlib images are captured - "plot_gallery": "True", # Enables plot rendering + "plot_gallery": True, # Enables plot rendering "reset_modules": ("matplotlib",), # Helps with memory management "capture_repr": (), } From cdb2e3875f6d1286f128f0592f926bfdc6d20b52 Mon Sep 17 00:00:00 2001 From: Oliver Kotla <56128805+ThatDesert@users.noreply.github.com> Date: Tue, 21 Oct 2025 18:47:22 +0100 Subject: [PATCH 05/13] Add new recipe 21 by Oliver Kotla --- docs/source/recipes/plot_21_recipe.py | 237 ++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/source/recipes/plot_21_recipe.py diff --git a/docs/source/recipes/plot_21_recipe.py b/docs/source/recipes/plot_21_recipe.py new file mode 100644 index 0000000000..ee75cb176e --- /dev/null +++ b/docs/source/recipes/plot_21_recipe.py @@ -0,0 +1,237 @@ +""" +Applying functions and mathematical operations to data +====================================================== + +In this recipe we will explore various methods to apply a mathematical operation or a function to a set of data in a field. For the purposes of the example, we will look at various ways of calculating the sine of each element in a data array. + +There are various options to do this, the recommended option is to use `cf native functions `_, as they preserve units and metadata associated with fields. Sometimes, however, the function you need is not implemented in cf, so there are alternative methods. +""" + +#%% [markdown] +# +# .. figure:: ../../sample-gallery-1/cf-flowchart.png +# :scale: 50 % +# :alt: flowchart showing process of location a function in cf, then in Dask, then in NumPy, and finally vectorising it with NumPy. +# +# It is recommended to use the highest possible implementation of a given function as shown by the chart. +# + +#%% +# 1. Import cf-python: + +import cf + +#%% +# 2. Read the template field constructs from the example: + +f = cf.example_field(1) +print(f) + +#%% [markdown] +# +# 1: Native cf +# ------------ +# +# As mentioned, cf supports a handful of `field operations `_ that automatically update the domain and metadata alongside the data array. +# +# Additionally, where a function or operation has a specific domain, cf will mask any erroneous elements that were not processed properly. +# + +#%% +# 1. Create an instance of the template field to work with: + +field1 = f.copy() + +#%% +# 2. Calculate the sine of the elements in the data array: + +new_field = field1.sin() + +print(new_field.data) + +#%% +# Alternatively, we can update the original field in place using the ``inplace`` parameter: + +field1.sin(inplace=True) + +print(field1.data) + +# cf will automatically update the units of our field depending on the operation. +# Here, since the sine is a dimensionless value, we get the units "1". + +print(f.units) # Original +print(field1.units) # After operation + +#%% [markdown] +# +# 2: Dask +# ------- +# +# When it comes to computing mathematical operations on a data array, +# cf utilises two different libraries under the hood: Dask and NumPy. +# +# In the event that cf does not natively support an operation, the next +# port of call is Dask (or specifically, the``dask.array``module). +# +# Dask implements `a number of functions `_, either as pass-throughs for +# NumPy functions (see below) or as its own implementations. +# +# To preserve the metadata associated with the origin field, we will +# have to create a duplicate of it and rewrite the data array using the +# ``f.Field.set_data()`` method. However, care must be taken to also +# update metadata such as units or coordinates when applying a function +# from outside of cf. +# + +#%% +# 1. Import the necessary Dask module: + +import dask as da + +#%% +# 2. Create an instance of the template field to work with: + +field2 = f.copy() + +#%% +# 3. Load the data from the field as a Dask array: + +data = field2.data + +dask_array = data.to_dask_array() + +#%% +# 4. Create a new field, calculate the sine of the elements, +# and write the array to the new field: + +new_field = field2.copy() + +calculated_array = da.array.sin(dask_array) + +new_field.set_data(calculated_array) + +print(new_field.data) + +#%% +# 5. Manually update the units: + +new_field.override_units('1', inplace=True) + +print(new_field.units) + +#%% +# To instead update the original field in place, as before: + +calculated_array = da.array.sin(dask_array) + +field2.set_data(calculated_array) + +field2.override_units('1', inplace=True) + +print(field2.data) +print(field2.units) + +#%% [markdown] +# +# 3: NumPy Universal Functions +# ---------------------------- +# +# Applying an operation with Dask and NumPy is a similar process, +# and some Dask functions are effectively aliases for equivalent NumPy +# functions. NumPy has so-called `universal functions `_ that improve +# performance when working on large arrays compared to just iterating +# through each element and running a function on it. +# +# As above, take care to manually update any metadata for the new field. +# + +#%% +# 1. Import NumPy: + +import numpy as np + +#%% +# 2. Create an instance of the template field to work with: + +field3 = f.copy() + +#%% +# 3. Create a new field, compute the sine of the elements, +# and write the array to the new field: + +new_field = field3.copy() + +calculated_array = np.sin(field3) + +new_field.set_data(calculated_array) + +print(new_field.data) + +#%% +# 4. Manually update the units: + +new_field.override_units('1', inplace=True) + +print(new_field.units) + +#%% [markdown] +# +# 4: NumPy Vectorization +# ---------------------- +# +# In the event that the operation you need is not supported in cf, Dask, +# or NumPy, then any standard Python function can be vectorized using +# NumPy. In essence, this simply allows the function to take an array as +# input, and return the updated array as output. There is no improvement +# in performance to simply iterating through each element in the data +# array and applying the function. +# + +#%% +# 1. Import our third-party function; here, from the ``math`` module: + +import math + +#%% +# 2. Create an instance of the template field to work with: + +field4 = f.copy() + +#%% +# 3. Vectorize the function with NumPy: + +vectorized_function = np.vectorize(math.sin) + +#%% +# 4. Create a new field, calculate the sine of the elements, +# and write the array to the new field: + +new_field = field4.copy() + +calculated_array = vectorized_function(field4) + +new_field.set_data(calculated_array) + +print(new_field.data) + +#%% +# 5. Manually update the units: + +new_field.override_units('1', inplace=True) + +print(new_field.units) + +#%% [markdown] +# +# Performance +# ----------- +# +# NumPy and Dask tend to work the quickest thanks to their universal +# functions. NumPy vectorization works much slower as functions cannot +# be optimised in this fashion. +# +# Operations in cf, whilst running NumPy and Dask under the hood, still +# come with all the performance overheads necessary to accurately adapt +# metadata between fields to ensure that resultant fields are still +# compliant with conventions. +# \ No newline at end of file From 6fa0e96a1764d5aea695af46770e1e150e3c75f4 Mon Sep 17 00:00:00 2001 From: Oliver Kotla <56128805+ThatDesert@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:21:24 +0100 Subject: [PATCH 06/13] Add new recipe 22 by Oliver Kotla --- docs/source/recipes/plot_22_recipe.py | 118 ++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/source/recipes/plot_22_recipe.py diff --git a/docs/source/recipes/plot_22_recipe.py b/docs/source/recipes/plot_22_recipe.py new file mode 100644 index 0000000000..0ac6f8cec1 --- /dev/null +++ b/docs/source/recipes/plot_22_recipe.py @@ -0,0 +1,118 @@ +""" +Plotting a wind rose as a scatter plot +====================================== + +Given a file containing northerly and easterly wind components, we can +calculate the magnitude and bearing of the resultant wind at each point +in the region and plot them using a scatter plot on a polar grid to +create a wind rose representing wind vectors in the given area. +""" +# %% +# 1. Import cf-python, Dask.array, NumPy, and Matplotlib: + +import cf +import dask.array as da +import numpy as np +import matplotlib.pyplot as plt + +# %% +# 2. Read the field constructs and load the wind speed component fields: + +f = cf.read('data1.nc') +print(f) + +U = f[2].squeeze() # Easterly wind speed component +V = f[3].squeeze() # Northerly wind speed component + + +# %% +# 3. Set a bounding region for the data and discard readings outside of it: + +tl = (41, 72) # (long, lat) of top left of bounding box. +br = (65, 46) # (long, lat) of bottom right of bounding box. + +U_region = U.subspace(X=cf.wi(tl[0], br[0]), Y=cf.wi(br[1], tl[1])) +V_region = V.subspace(X=cf.wi(tl[0], br[0]), Y=cf.wi(br[1], tl[1])) + +# %% +# 4. Select measurements for a specific pressure using the subspace method, +# then use squeeze to remove the size 1 axis: + +U_sub = U_region.subspace(pressure=500.0) +V_sub = V_region.subspace(pressure=500.0) + +U_sub.squeeze(inplace=True) +V_sub.squeeze(inplace=True) + +# %% +# 5. Calculate the magnitude of each resultant vector using Dask's hypot +# function: + +magnitudes = da.hypot(U_sub.data, V_sub.data) + +# %% +# 6. Calculate the angle of the resultant vector (relative to an Easterly ray) +# using Dask's arctan2 function, then convert to a clockwise bearing: + +azimuths = da.arctan2(V_sub.data, U_sub.data) + +bearings = ((np.pi/2) - azimuths) % (np.pi*2) + +# %% +# 7. Flatten the two dimensions of each array for plotting with Matplotlib: + +bearings_flattened = da.ravel(bearings) + +magnitudes_flattened = da.ravel(magnitudes) + +# %% +# 8. Draw the scatter plot using Matplotlib: + +plt.figure(figsize=(5, 6)) +# sphinx_gallery_start_ignore +fig = plt.gcf() +# sphinx_gallery_end_ignore +ax = plt.subplot(polar=True) +ax.set_theta_zero_location("N") # Place 0 degrees at the top. +ax.set_theta_direction(-1) # Arrange bearings clockwise around the plot. +# sphinx_gallery_start_ignore +plt.close() +# sphinx_gallery_end_ignore + +# %% +# 9. Draw a scatter plot on the polar plot, using the wind direction bearing as +# the angle and the magnitude of the resultant wind speed as the distance +# from the pole. +ax.scatter(bearings_flattened.compute(), magnitudes_flattened.compute(), s=1.2) +# sphinx_gallery_start_ignore +plt.close() +# sphinx_gallery_end_ignore + +# %% +# 10. Label the axes and add a title. + +# sphinx_gallery_start_ignore +plt.figure(fig) +# sphinx_gallery_end_ignore +plt.title(f'Wind Rose Scatter Plot\nLat: {br[1]}°-{tl[1]}°, Long: {tl[0]}°-{br[0]}°') + +ax.set_xlabel("Bearing [°]") + +ax.set_ylabel("Speed [m/s]", rotation=45, labelpad=30, size=8) + +ax.yaxis.set_label_coords(0.45, 0.45) + +ax.yaxis.set_tick_params(which='both', labelrotation=45, labelsize=8) + +ax.set_rlabel_position(45) +# sphinx_gallery_start_ignore +plt.close() +# sphinx_gallery_end_ignore + +# %% +# 11. Display the plot. + +# sphinx_gallery_start_ignore +plt.figure(fig) +# sphinx_gallery_end_ignore +plt.show() \ No newline at end of file From 1b58b6c8e66238c39670afbcb6718857307c471e Mon Sep 17 00:00:00 2001 From: Oliver Kotla <56128805+ThatDesert@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:22:13 +0100 Subject: [PATCH 07/13] Add new recipe 23 by Oliver Kotla --- docs/source/recipes/plot_23_recipe.py | 166 ++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/source/recipes/plot_23_recipe.py diff --git a/docs/source/recipes/plot_23_recipe.py b/docs/source/recipes/plot_23_recipe.py new file mode 100644 index 0000000000..706c4b2d91 --- /dev/null +++ b/docs/source/recipes/plot_23_recipe.py @@ -0,0 +1,166 @@ +""" +Combining cf and Matplotlib plots in one figure +=============================================== + +Presently, cf-plot has very few exposed interfaces to its Matplotlib and +Cartopy backend. This makes it difficult to combine plots from the three +in one figure, but not impossible. + +A combined cf and Matplotlib plot can be achieved by amending the figure +stored at ``cfp.plotvars.master_plot``, and then redrawing it with the +new subplots. +""" + +#%% +# 1. Import cf-python, cf-plot, Matplotlib, NumPy, and Dask.array: + +# sphinx_gallery_start_ignore +# sphinx_gallery_thumbnail_number = 2 +# sphinx_gallery_end_ignore + +import matplotlib.pyplot as plt +import cfplot as cfp +import cf + +import numpy as np +import dask.array as da + +#%% +# 2. Read example data field constructs, and set region for our plots: + +f = cf.read(f"data1.nc") + +u = f.select_by_identity("eastward_wind")[0] +v = f.select_by_identity("northward_wind")[0] +t = f.select_by_identity("air_temperature")[0] + +# Subspace to get values for a specified pressure, here 500 mbar +u = u.subspace(pressure=500) +v = v.subspace(pressure=500) +t = t.subspace(pressure=500) + +lonmin, lonmax, latmin, latmax = 10, 120, -30, 30 + +#%% [markdown] +# +# Outlining the figure with cf-plot +# --------------------------------- +# + +#%% +# 1. Set desired dimensions for our final figure: + +rows, cols = 2, 2 + +#%% +# 2. Create a figure of set dimensions with ``cfp.gopen()``, then set the +# position of the cf plot: + + +cfp.gopen(rows, cols) + +pos = 2 # Second position in the figure + +cfp.gpos(pos) +# sphinx_gallery_start_ignore +plt.close() +# sphinx_gallery_end_ignore + +# %% +# 3. Create a simple vector plot: + +cfp.mapset(lonmin=lonmin, lonmax=lonmax, latmin=latmin, latmax=latmax) +cfp.vect(u=u, v=v, key_length=10, scale=120, stride=4) + +#%% [markdown] +# +# Creating our Matplotlib plots +# ----------------------------- +# + +#%% +# 1. Access the newly-created figure: + +fig = cfp.plotvars.master_plot + +#%% +# 2. Reduce fields down to our test data for a wind rose scatter plot: + +# Limit to specific geographic region +u_region = u.subspace(X=cf.wi(lonmin, lonmax), Y=cf.wi(latmin, latmax)) +v_region = v.subspace(X=cf.wi(lonmin, lonmax), Y=cf.wi(latmin, latmax)) +t_region = t.subspace(X=cf.wi(lonmin, lonmax), Y=cf.wi(latmin, latmax)) + +# Remove size 1 axes +u_squeeze = u_region.squeeze() +v_squeeze = v_region.squeeze() +t_squeeze = t_region.squeeze() + +# Flatten to one dimension for plot +u_f = da.ravel(u_squeeze.data) +v_f = da.ravel(v_squeeze.data) +t_f = da.ravel(t_squeeze.data) + +#%% +# 3. Perform calculations to create appropriate plot data: + +mag_f = da.hypot(u_f, v_f) # Wind speed magnitude + +azimuths_f = da.arctan2(v_f, u_f) +rad_f = ((np.pi/2) - azimuths_f) % (np.pi*2) # Wind speed bearing + +# Normalise temperature data into a range appropriate for setting point sizes (1-10pt). +temp_scaled = 1 + (t_f - t_f.min()) / (t_f.max() - t_f.min()) * (10 - 1) + +#%% +# 4. Add Matplotlib subplot to our existing cf figure: + +pos = 1 # First position in the figure + +ax = fig.add_subplot(rows, cols, pos, polar=True) +ax.set_theta_zero_location("N") +ax.set_theta_direction(-1) + +ax.scatter(rad_f.compute(), mag_f.compute(), s=temp_scaled.compute(), c=temp_scaled.compute(), alpha=0.5) + +ax.set_xlabel("Bearing [°]") +ax.set_ylabel("Speed [m/s]", rotation=45, labelpad=30, size=8) +ax.yaxis.set_label_coords(0.45, 0.45) +ax.yaxis.set_tick_params(which='both', labelrotation=45, labelsize=8) +ax.set_rlabel_position(45) + +#%% +# 5. Create and add a third plot, for example: + +x = np.linspace(0, 10, 100) +y = np.sin(x) + +pos = 3 # Third position in the figure + +ax1 = fig.add_subplot(rows, cols, pos) + +ax1.plot(x, y, label='sin(x)') +ax1.legend() + +#%% [markdown] +# +# Drawing the new figure +# ---------------------- +# + +#%% +# 1. Draw final figure: + +fig = plt.figure(fig) +fig.tight_layout() +fig.show() + +#%% [markdown] +# +# Summary +# ------- +# +# In summary, to use other plotting libraries with cf-plot, you must first +# create your figure with cf-plot with placeholders for your other plots, +# then add subplots by accessing the ``cfp.plotvars.master_plot`` object, +# and finally redraw the figure containing the new plots. \ No newline at end of file From 07d5916c7ae129973ec6513643467127f49fd611 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Tue, 21 Oct 2025 22:28:13 +0100 Subject: [PATCH 08/13] Update recipes listing to cover three new summer 2025 recipes --- docs/source/recipes/recipe_list.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/recipes/recipe_list.txt b/docs/source/recipes/recipe_list.txt index 3dad79a79c..0a8930811a 100644 --- a/docs/source/recipes/recipe_list.txt +++ b/docs/source/recipes/recipe_list.txt @@ -37,4 +37,10 @@ plot_18_recipe.html#sphx-glr-recipes-plot-18-recipe-py plot_19_recipe.html#sphx-glr-recipes-plot-19-recipe-py
plot_20_recipe.html#sphx-glr-recipes-plot-20-recipe-py -
\ No newline at end of file +
+plot_21_recipe.html#sphx-glr-recipes-plot-21-recipe-py +
+plot_22_recipe.html#sphx-glr-recipes-plot-22-recipe-py +
+plot_23_recipe.html#sphx-glr-recipes-plot-23-recipe-py +
From 873cd0dbe46e6fdaeab3c3b3d83a5cd52542b2a7 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 22 Oct 2025 01:18:57 +0100 Subject: [PATCH 09/13] Prevent possible build issue due to None in linkcode_resolve regex --- docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0fd99f4d31..239aa988fa 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -475,7 +475,6 @@ def _get_date(): import cf -link_release = re.search("(\d+\.\d+\.\d+)", release).groups()[0] def linkcode_resolve(domain, info): @@ -485,7 +484,6 @@ def linkcode_resolve(domain, info): # # >> rm -fr build/.doctrees build/*/.doctrees build/*/*/.doctrees # ================================================================= - online_source_code = True if domain != "py": @@ -549,6 +547,8 @@ def linkcode_resolve(domain, info): cfdm_version, fn, linespec ) else: + link_release = re.search("(\d+\.\d+\.\d+)", release).groups()[0] + # Point to on-line cf # code. E.g. https://github.com/NCAS-CMS/cf-python/blob/v3.0.1/cf/data/data.py#L4292 url = "https://github.com/NCAS-CMS/cf-python/blob/v{0}/cf/{1}{2}".format( From cb9268435a0ff326351676379c3f37d28002a840 Mon Sep 17 00:00:00 2001 From: Oliver Kotla <56128805+ThatDesert@users.noreply.github.com> Date: Wed, 22 Oct 2025 01:21:33 +0100 Subject: [PATCH 10/13] Add schematic for recipe 21, created by Oliver Kotla --- .../source/images/data-operations-flowchart.png | Bin 0 -> 71059 bytes docs/source/recipes/plot_21_recipe.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/source/images/data-operations-flowchart.png diff --git a/docs/source/images/data-operations-flowchart.png b/docs/source/images/data-operations-flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..9699fe886625c51322b62d135f5a1e028b648e9e GIT binary patch literal 71059 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�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 A^#A|> literal 0 HcmV?d00001 diff --git a/docs/source/recipes/plot_21_recipe.py b/docs/source/recipes/plot_21_recipe.py index ee75cb176e..49701f30f9 100644 --- a/docs/source/recipes/plot_21_recipe.py +++ b/docs/source/recipes/plot_21_recipe.py @@ -9,7 +9,7 @@ #%% [markdown] # -# .. figure:: ../../sample-gallery-1/cf-flowchart.png +# .. figure:: ../images/data-operations-flowchart.png # :scale: 50 % # :alt: flowchart showing process of location a function in cf, then in Dask, then in NumPy, and finally vectorising it with NumPy. # @@ -234,4 +234,4 @@ # come with all the performance overheads necessary to accurately adapt # metadata between fields to ensure that resultant fields are still # compliant with conventions. -# \ No newline at end of file +# From 18f2ac254a4c05bbb06455a130d027b64ac36013 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 22 Oct 2025 12:29:07 +0100 Subject: [PATCH 11/13] Update data paths in new recipes to point to usual directory --- docs/source/recipes/plot_22_recipe.py | 4 ++-- docs/source/recipes/plot_23_recipe.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/recipes/plot_22_recipe.py b/docs/source/recipes/plot_22_recipe.py index 0ac6f8cec1..2514ef285e 100644 --- a/docs/source/recipes/plot_22_recipe.py +++ b/docs/source/recipes/plot_22_recipe.py @@ -18,7 +18,7 @@ # %% # 2. Read the field constructs and load the wind speed component fields: -f = cf.read('data1.nc') +f = cf.read('~/recipes/data1.nc') print(f) U = f[2].squeeze() # Easterly wind speed component @@ -115,4 +115,4 @@ # sphinx_gallery_start_ignore plt.figure(fig) # sphinx_gallery_end_ignore -plt.show() \ No newline at end of file +plt.show() diff --git a/docs/source/recipes/plot_23_recipe.py b/docs/source/recipes/plot_23_recipe.py index 706c4b2d91..38e20c62b7 100644 --- a/docs/source/recipes/plot_23_recipe.py +++ b/docs/source/recipes/plot_23_recipe.py @@ -28,7 +28,7 @@ #%% # 2. Read example data field constructs, and set region for our plots: -f = cf.read(f"data1.nc") +f = cf.read(f"~/recipes/data1.nc") u = f.select_by_identity("eastward_wind")[0] v = f.select_by_identity("northward_wind")[0] @@ -163,4 +163,4 @@ # In summary, to use other plotting libraries with cf-plot, you must first # create your figure with cf-plot with placeholders for your other plots, # then add subplots by accessing the ``cfp.plotvars.master_plot`` object, -# and finally redraw the figure containing the new plots. \ No newline at end of file +# and finally redraw the figure containing the new plots. From 2275af7b6154cb7389c51dc4b941c8ecfa5166a4 Mon Sep 17 00:00:00 2001 From: "Sadie L. Bartholomew" Date: Wed, 22 Oct 2025 13:21:19 +0100 Subject: [PATCH 12/13] Lint all new recipes with black tool --- docs/source/recipes/plot_21_recipe.py | 64 +++++++++++++-------------- docs/source/recipes/plot_22_recipe.py | 23 +++++----- docs/source/recipes/plot_23_recipe.py | 50 ++++++++++++--------- 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/docs/source/recipes/plot_21_recipe.py b/docs/source/recipes/plot_21_recipe.py index 49701f30f9..0f18f3aea7 100644 --- a/docs/source/recipes/plot_21_recipe.py +++ b/docs/source/recipes/plot_21_recipe.py @@ -7,7 +7,7 @@ There are various options to do this, the recommended option is to use `cf native functions `_, as they preserve units and metadata associated with fields. Sometimes, however, the function you need is not implemented in cf, so there are alternative methods. """ -#%% [markdown] +# %% [markdown] # # .. figure:: ../images/data-operations-flowchart.png # :scale: 50 % @@ -16,18 +16,18 @@ # It is recommended to use the highest possible implementation of a given function as shown by the chart. # -#%% +# %% # 1. Import cf-python: import cf -#%% +# %% # 2. Read the template field constructs from the example: f = cf.example_field(1) print(f) -#%% [markdown] +# %% [markdown] # # 1: Native cf # ------------ @@ -37,19 +37,19 @@ # Additionally, where a function or operation has a specific domain, cf will mask any erroneous elements that were not processed properly. # -#%% +# %% # 1. Create an instance of the template field to work with: field1 = f.copy() -#%% +# %% # 2. Calculate the sine of the elements in the data array: new_field = field1.sin() print(new_field.data) -#%% +# %% # Alternatively, we can update the original field in place using the ``inplace`` parameter: field1.sin(inplace=True) @@ -59,10 +59,10 @@ # cf will automatically update the units of our field depending on the operation. # Here, since the sine is a dimensionless value, we get the units "1". -print(f.units) # Original -print(field1.units) # After operation +print(f.units) # Original +print(field1.units) # After operation -#%% [markdown] +# %% [markdown] # # 2: Dask # ------- @@ -83,24 +83,24 @@ # from outside of cf. # -#%% +# %% # 1. Import the necessary Dask module: import dask as da -#%% +# %% # 2. Create an instance of the template field to work with: field2 = f.copy() -#%% +# %% # 3. Load the data from the field as a Dask array: data = field2.data dask_array = data.to_dask_array() -#%% +# %% # 4. Create a new field, calculate the sine of the elements, # and write the array to the new field: @@ -112,26 +112,26 @@ print(new_field.data) -#%% +# %% # 5. Manually update the units: -new_field.override_units('1', inplace=True) +new_field.override_units("1", inplace=True) print(new_field.units) -#%% +# %% # To instead update the original field in place, as before: calculated_array = da.array.sin(dask_array) field2.set_data(calculated_array) -field2.override_units('1', inplace=True) +field2.override_units("1", inplace=True) print(field2.data) print(field2.units) -#%% [markdown] +# %% [markdown] # # 3: NumPy Universal Functions # ---------------------------- @@ -145,17 +145,17 @@ # As above, take care to manually update any metadata for the new field. # -#%% +# %% # 1. Import NumPy: import numpy as np -#%% +# %% # 2. Create an instance of the template field to work with: field3 = f.copy() -#%% +# %% # 3. Create a new field, compute the sine of the elements, # and write the array to the new field: @@ -167,14 +167,14 @@ print(new_field.data) -#%% +# %% # 4. Manually update the units: -new_field.override_units('1', inplace=True) +new_field.override_units("1", inplace=True) print(new_field.units) -#%% [markdown] +# %% [markdown] # # 4: NumPy Vectorization # ---------------------- @@ -187,22 +187,22 @@ # array and applying the function. # -#%% +# %% # 1. Import our third-party function; here, from the ``math`` module: import math -#%% +# %% # 2. Create an instance of the template field to work with: field4 = f.copy() -#%% +# %% # 3. Vectorize the function with NumPy: vectorized_function = np.vectorize(math.sin) -#%% +# %% # 4. Create a new field, calculate the sine of the elements, # and write the array to the new field: @@ -214,14 +214,14 @@ print(new_field.data) -#%% +# %% # 5. Manually update the units: -new_field.override_units('1', inplace=True) +new_field.override_units("1", inplace=True) print(new_field.units) -#%% [markdown] +# %% [markdown] # # Performance # ----------- diff --git a/docs/source/recipes/plot_22_recipe.py b/docs/source/recipes/plot_22_recipe.py index 2514ef285e..377313c899 100644 --- a/docs/source/recipes/plot_22_recipe.py +++ b/docs/source/recipes/plot_22_recipe.py @@ -7,6 +7,7 @@ in the region and plot them using a scatter plot on a polar grid to create a wind rose representing wind vectors in the given area. """ + # %% # 1. Import cf-python, Dask.array, NumPy, and Matplotlib: @@ -18,18 +19,18 @@ # %% # 2. Read the field constructs and load the wind speed component fields: -f = cf.read('~/recipes/data1.nc') +f = cf.read("~/recipes/data1.nc") print(f) -U = f[2].squeeze() # Easterly wind speed component -V = f[3].squeeze() # Northerly wind speed component +U = f[2].squeeze() # Easterly wind speed component +V = f[3].squeeze() # Northerly wind speed component # %% # 3. Set a bounding region for the data and discard readings outside of it: -tl = (41, 72) # (long, lat) of top left of bounding box. -br = (65, 46) # (long, lat) of bottom right of bounding box. +tl = (41, 72) # (long, lat) of top left of bounding box. +br = (65, 46) # (long, lat) of bottom right of bounding box. U_region = U.subspace(X=cf.wi(tl[0], br[0]), Y=cf.wi(br[1], tl[1])) V_region = V.subspace(X=cf.wi(tl[0], br[0]), Y=cf.wi(br[1], tl[1])) @@ -56,7 +57,7 @@ azimuths = da.arctan2(V_sub.data, U_sub.data) -bearings = ((np.pi/2) - azimuths) % (np.pi*2) +bearings = ((np.pi / 2) - azimuths) % (np.pi * 2) # %% # 7. Flatten the two dimensions of each array for plotting with Matplotlib: @@ -73,8 +74,8 @@ fig = plt.gcf() # sphinx_gallery_end_ignore ax = plt.subplot(polar=True) -ax.set_theta_zero_location("N") # Place 0 degrees at the top. -ax.set_theta_direction(-1) # Arrange bearings clockwise around the plot. +ax.set_theta_zero_location("N") # Place 0 degrees at the top. +ax.set_theta_direction(-1) # Arrange bearings clockwise around the plot. # sphinx_gallery_start_ignore plt.close() # sphinx_gallery_end_ignore @@ -94,7 +95,9 @@ # sphinx_gallery_start_ignore plt.figure(fig) # sphinx_gallery_end_ignore -plt.title(f'Wind Rose Scatter Plot\nLat: {br[1]}°-{tl[1]}°, Long: {tl[0]}°-{br[0]}°') +plt.title( + f"Wind Rose Scatter Plot\nLat: {br[1]}°-{tl[1]}°, Long: {tl[0]}°-{br[0]}°" +) ax.set_xlabel("Bearing [°]") @@ -102,7 +105,7 @@ ax.yaxis.set_label_coords(0.45, 0.45) -ax.yaxis.set_tick_params(which='both', labelrotation=45, labelsize=8) +ax.yaxis.set_tick_params(which="both", labelrotation=45, labelsize=8) ax.set_rlabel_position(45) # sphinx_gallery_start_ignore diff --git a/docs/source/recipes/plot_23_recipe.py b/docs/source/recipes/plot_23_recipe.py index 38e20c62b7..2499b0d875 100644 --- a/docs/source/recipes/plot_23_recipe.py +++ b/docs/source/recipes/plot_23_recipe.py @@ -11,7 +11,7 @@ new subplots. """ -#%% +# %% # 1. Import cf-python, cf-plot, Matplotlib, NumPy, and Dask.array: # sphinx_gallery_start_ignore @@ -25,7 +25,7 @@ import numpy as np import dask.array as da -#%% +# %% # 2. Read example data field constructs, and set region for our plots: f = cf.read(f"~/recipes/data1.nc") @@ -41,25 +41,25 @@ lonmin, lonmax, latmin, latmax = 10, 120, -30, 30 -#%% [markdown] +# %% [markdown] # # Outlining the figure with cf-plot # --------------------------------- # -#%% +# %% # 1. Set desired dimensions for our final figure: rows, cols = 2, 2 -#%% +# %% # 2. Create a figure of set dimensions with ``cfp.gopen()``, then set the # position of the cf plot: cfp.gopen(rows, cols) -pos = 2 # Second position in the figure +pos = 2 # Second position in the figure cfp.gpos(pos) # sphinx_gallery_start_ignore @@ -72,18 +72,18 @@ cfp.mapset(lonmin=lonmin, lonmax=lonmax, latmin=latmin, latmax=latmax) cfp.vect(u=u, v=v, key_length=10, scale=120, stride=4) -#%% [markdown] +# %% [markdown] # # Creating our Matplotlib plots # ----------------------------- # -#%% +# %% # 1. Access the newly-created figure: fig = cfp.plotvars.master_plot -#%% +# %% # 2. Reduce fields down to our test data for a wind rose scatter plot: # Limit to specific geographic region @@ -101,61 +101,67 @@ v_f = da.ravel(v_squeeze.data) t_f = da.ravel(t_squeeze.data) -#%% +# %% # 3. Perform calculations to create appropriate plot data: -mag_f = da.hypot(u_f, v_f) # Wind speed magnitude +mag_f = da.hypot(u_f, v_f) # Wind speed magnitude azimuths_f = da.arctan2(v_f, u_f) -rad_f = ((np.pi/2) - azimuths_f) % (np.pi*2) # Wind speed bearing +rad_f = ((np.pi / 2) - azimuths_f) % (np.pi * 2) # Wind speed bearing # Normalise temperature data into a range appropriate for setting point sizes (1-10pt). temp_scaled = 1 + (t_f - t_f.min()) / (t_f.max() - t_f.min()) * (10 - 1) -#%% +# %% # 4. Add Matplotlib subplot to our existing cf figure: -pos = 1 # First position in the figure +pos = 1 # First position in the figure ax = fig.add_subplot(rows, cols, pos, polar=True) ax.set_theta_zero_location("N") ax.set_theta_direction(-1) -ax.scatter(rad_f.compute(), mag_f.compute(), s=temp_scaled.compute(), c=temp_scaled.compute(), alpha=0.5) +ax.scatter( + rad_f.compute(), + mag_f.compute(), + s=temp_scaled.compute(), + c=temp_scaled.compute(), + alpha=0.5, +) ax.set_xlabel("Bearing [°]") ax.set_ylabel("Speed [m/s]", rotation=45, labelpad=30, size=8) ax.yaxis.set_label_coords(0.45, 0.45) -ax.yaxis.set_tick_params(which='both', labelrotation=45, labelsize=8) +ax.yaxis.set_tick_params(which="both", labelrotation=45, labelsize=8) ax.set_rlabel_position(45) -#%% +# %% # 5. Create and add a third plot, for example: x = np.linspace(0, 10, 100) y = np.sin(x) -pos = 3 # Third position in the figure +pos = 3 # Third position in the figure ax1 = fig.add_subplot(rows, cols, pos) -ax1.plot(x, y, label='sin(x)') +ax1.plot(x, y, label="sin(x)") ax1.legend() -#%% [markdown] +# %% [markdown] # # Drawing the new figure # ---------------------- # -#%% +# %% # 1. Draw final figure: fig = plt.figure(fig) fig.tight_layout() fig.show() -#%% [markdown] +# %% [markdown] # # Summary # ------- From 3908f20e3a73b68afaa8d5c3be96a9b95f38ddf7 Mon Sep 17 00:00:00 2001 From: Oliver Kotla <56128805+ThatDesert@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:33:45 +0100 Subject: [PATCH 13/13] Add image by Oliver Kotla for custom gallery default thumbnail --- .../cf-recipe-placeholder-squarecrop.png | Bin 0 -> 40274 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/source/_static/cf-recipe-placeholder-squarecrop.png diff --git a/docs/source/_static/cf-recipe-placeholder-squarecrop.png b/docs/source/_static/cf-recipe-placeholder-squarecrop.png new file mode 100644 index 0000000000000000000000000000000000000000..94c89b826fea26bac7e8f617e4639791ef5a6970 GIT binary patch literal 40274 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 X_Cs6mp5&`_00000NkvXXu0mjfO66PR literal 0 HcmV?d00001