This document describes the scripts used to compute the steric and ocean heat content anomaly estimates from the HOMaGE project. The HOMaGE project provides gridded estimates of steric sea-level anomalies and Ocean Heat Content (OHC) anomalies. The HOMaGE dataset (DOI:10.5067/HMSSO-4TJ01) can be found on PO.DAAC.
This repository provides the codes and the underlying theory on how to compute steric sea level and ocean heat content anomalies from ocean subsurface temperature and salinity anomalies, as well as a tutorial on how to execute these scripts, and an example how to plot the output data.
If you notice bugs or want to add features, please don't hesitate to fill in a bug report or file a pull request.
Authors: Thomas Frederikse, Felix Landerer, Emmy Killett (NASA Jet Propulsion Laboratory)
Changes in the temperature and salinity of sea water affect the water density, which causes sea-level changes. Furthermore, a change in ocean temperatures also means a change in the amount of heat stored in the oceans: the ocean heat content (OHC). Steric sea-level and OHC changes play an important role in many fundamental and applied problems in climate science, including sea-level and coastal hazards studies, physical oceanography, and in determining the Earth Energy Imbalance and the Equilibrium Climate Sensitivity.
With this software package, we provide fast and easy-to-use Julia scripts to compute steric sea level and OHC from global gridded estimates of temperature and salinity. These estimates can for example come from observations from the Argo network, but the scripts can in principle also process fields from ocean and Earth system models. The scripts compute the following quantities:
- Thermosteric sea level (Sea-level changes due to water temperature changes)
- Halosteric sea level (Sea-level changes due to salinity changes)
- Steric sea level (Sea-level changes due to both water temperature and salinity changes)
- Ocean heat content
All these quantities are computed on a grid for analyzing local changes and are averaged to provide the global mean values.
Steric sea-level changes can be caused by both changes in temperature and salinity. In a column of water with a constant mass, a change in the temperature
where
Ocean heat content (OHC) anomalies
The data that comes from measurements and models is not continuous and is discretized in space and time. We assume that all input
Steric sea levels and ocean heat content are quantities that are relative to a specific baseline. As a baseline, we use a climatology of settings["years_clim"]
. Hence, there is no seasonal cycle in the climatology, and seasonal anomalies are retained in the end result.
We use the Thermodynamic Equations of Seawater 2010 TEOS-10 Gibbs Sea Water (GSW) equations to compute the density as a function of
To obtain the estimates of steric sea-level and ocean heat content anomalies from gridded sub-surface
-
Download a set of gridded estimates of temperature and salinity. We refer to this data as the 'source data`. The repository contains a few routines to download these estimates from some often-used sources, such as IAP, EN4, Ishii et al. (2017) and Scripps.
-
Read the grid properties, such as the volume of each grid cell and a sea-land mask.
-
Read the source data and convert them to
$\Theta$ and$S_A$ using TEOS-10 GSW, and write them to a standard format field. -
Compute the climatology of
$\Theta$ and$S_A$ by averaging all fields over the years specified bysettings["years_clim"]
. From this climatology, compute the climatology of the density$\rho$ . Save this climatology. -
For each time step, compute the steric and OHC anomalies relative to the pre-computed climatology.
-
Write the results into a final file.
The scripts are all written in the Julia language. Thanks to Julia, the computations are quite fast: it will typically take only a few minutes to convert the source files, compute the climatology, and the steric and OHC changes on a normal desktop or laptop.
To run the scripts, a working Julia installation is needed. Julia can be run from the command line, but to make life much easier, just use VSCode and install the Julia language plug-in. This will allow you to run code snippets from the source files directly at the Julia REPL.
The software has been written for Julia 1.7 and requires a few dependencies, which can all be installed from Julia's internal package manager. To do so, press ]
at the REPL and type
add GMT NCDatasets GibbsSeaWater ZipFile FTPClient
This will install and pre-compile all necessary prerequisites.
Then, download all the scripts from this repository, or directly clone the repository to your local machine.
The main loop is located in compute_steric_main.jl
. To run the program, type at the command line:
julia compute_steric_main.jl
Some parts of the calculations can be sped up by parallelization. We use both threaded and distributed loops. To parallelize the code, add the -p
(for multiple processes) and -t
(for multiple threads), together with the number of processor cores available to the command line. So, if for example you want to use 4 cores, use:
julia -p4 -t4 compute_steric_main.jl
Now the code will apply steps 1-6 from the above procedure. However, the program will likely crash this time, because we need to set it up properly before it runs. To do so, we have to update the settings, which are stored in compute_steric_main.jl
.
The first step, is that we have to tell Julia where to find the scripts that need to be imported. One line 13 in compute_steric_main.jl
there is a line:
dir_code = homedir()*"/Projects/HOMAGE/"
Modify this line, such that dir_code
point to the directory where this source code is located.
Then, we go to the function def_settings
. This function needs to be set up correctly in order to run. As you can see, the settings are stored in a Dictionary settings
. We have the following settings that we need to define:
Settings command | Description |
---|---|
Model and years | |
settings["model_name"] |
This setting describes which product to analyse. Currently accepts the following products: "BOA" (BOA-Argo, Li et al. 2017), "EN4_l09" (EN4 with Levitus et al. 2009 XBT/MBT corrections), "EN4_g10" (EN4 with Gouretski et al. 2010 XBT/MBT corrections) , "EN4_c13" (EN4 with Cowley et al. 2013 XBT/MBT corrections), "EN4_c14" (EN4 with Cheng et al. 2014 XBT/MBT corrections), "IAP" (IAP, Cheng et al., 2016, 2020), "I17" (Ishii et al. 2017), and "SIO" (Scripps, Roemmich and Gilson, 2009). |
settings["years"] |
Sets the years that must be downloaded and processed. Uses the [2018:2021...] notation, which runs everything from 2018 until and including 2021. |
settings["years_clim"] |
Sets the years used for computing the climatology. Uses the [2018:2021...] notation, which computes the climatology using all monthly data files from 2018 until and including 2021. |
settings["tvec"] |
An array with the years and months of each time step. Will be set automagically after settings["years"]
|
settings["tvec_clim"] |
An array with the years and months of each time step for which the climatology is computed. Will be set automagically after settings["years"]
|
Source and destination directories | |
settings["dir_analysis"] |
Sets the directory to which the source files must be downloaded. This directory must be created before running the script. |
settings["dir_analysis_fmt"] |
Sets the directory where the converted monthly |
settings["fn_savefile"] |
Sets the location and file name of the file with the final resulting steric and OHC will be stored. The directory where the file will be stored msut be created before running the script. |
Tasks to perform | |
settings["download_files"] = true |
Set to true to download the raw source data or false to skip this step |
settings["convert_files"] = true |
Set to true to convert the raw source data to monthly NetCDF files or false to skip this step |
settings["compute_climatology"] = true |
Set to true to compute the climatology or false to skip this step |
settings["compute_steric_ohc"] = true |
Set to true to compute steric and OHC from T and S or false to skip this step |
Be mindful to follow the Julia syntax, which can differ from Python and matlab:
- Strings use double
"
and not single'
quotation marks- Strings are concatenated with a
*
and not a+
.- The
homedir()
command is used to obtain the location of the home directory.- The booleans
true
andfalse
must not be capitalized.
An example of the settings could read:
settings = Dict()
settings["model_name"] = "SIO"
settings["years"] = [2004:2021...]
settings["years_clim"] = [2018:2020...]
settings["tvec"] = zeros(Int32,12*length(settings["years"]),2)
settings["tvec"] = years2tvec(settings["years"])
settings["tvec_clim"] = years2tvec(settings["years_clim"])
settings["dir_analysis"] = homedir() * "/Data/Steric/" * settings["model_name"] * "/analysis/"
settings["dir_analysis_fmt"] = homedir() * "/Data/Steric/" * settings["model_name"] * "/analysis_fmt/"
settings["fn_savefile"] = homedir() * "/Data/Steric/" * settings["model_name"] * "/postprocessed/"*settings["model_name"] * "_steric_ohc_" * string(settings["years"][1]) * "_"*string(settings["years"][end]) * ".nc"
settings["download_files"] = false
settings["convert_files"] = true
settings["compute_climatology"] = true
settings["compute_steric_ohc"] = true
In this case, we will run the code for SIO
, which refers to the Scripps product. We will compute everything for 2004-2021. The baseline will be the climatology over 2018 to 2020. We will not download the source data, but we will convert the source data to the standard format and convert everything to
When this is all set correctly, it's time to run the code:
julia compute_steric_main.jl
When everything runs well, an output NetCDF file should appear with the name of settings["fn_savefile"]
. There are many software packages that can read and visualise NetCDF files. An easy-to use and fast viewer is ncview, which can be easily installed from Homebrew or the Debian/Ubuntu repository. For most programming and scripting languages, packages to read NetCDF files are available.
The output file contains the following variables
Variable | Description | Unit |
---|---|---|
time |
Analysis time steps | Days since 1900-01-01 |
lon |
Longitude | Degrees |
lat |
Latitude | Degrees |
slm |
Sea-land mask | 1 = ocean, 0 = land |
ohc_2d |
Gridded ocean heat content anomalies | Joules per grid cell |
thermosteric_2d |
Gridded thermosteric sea-level anomalies | m |
halosteric_2d |
Gridded halosteric sea-level anomalies | m |
totalsteric_2d |
Gridded steric sea-level anomalies | m |
ohc_ts |
Global ocean heat content anomalies | Joules |
thermosteric_ts |
Global-mean thermosteric sea-level anomalies | m |
halosteric_ts |
Global-mean halosteric sea-level anomalies | m |
totalsteric_ts |
Global-mean steric sea-level anomalies | m |
To correct for the salinity bias in some Argo profiles (Hakuba et al. 2021), global-mean halosteric sea level has been removed from gridded steric sea level (totalsteric_2d
). Therefore, the bias is still in all other variables that include salinity (halosteric_2d
, halosteric_ts
, totalsteric_ts
). The thermosteric and OHC estimates are not affected by the salinity bias.
We can read and visualize the output data in Julia. As an example, let's plot the time series of global ocean heat content and a map of the trend in steric expansion from the Scripps dataset. We will be using the GMT.jl package to make the plots.
The below commands can be combined into a script that can be run by Julia, or can be directly typed in Julia's REPL. The repository contains the script example_analysis.jl
with all the below commands.
First, import some dependencies:
using GMT
using NCDatasets
using LinearAlgebra
using Statistics
using Dates
The next step is reading the output file. We first open a file handle fh
, and then we read the entries in the file and store them as variables.
fh = Dataset("SIO_steric_ohc_2004_2021.nc","r")
t = nomissing(fh["time"][:]) # Time
ζ = nomissing(fh["ohc_ts"][:]) # Global-mean ocean heat content anomalies
ϕ = nomissing(fh["lon"][:]) # Longitude
θ = nomissing(fh["lat"][:]) # Latitude
η = nomissing(fh["totalsteric_2d"][:]) # Gridded steric anomalies
close(fh)
Here, we assume that the output file SIO_steric_ohc_2004_2021.nc
is located in the active directory of Julia.
After reading the data, we plot global ocean heat content
plot(t, ζ/1e23, lw=1.5, lc="#4c78a8", ylabel="Global ocean heat content (ZJ)", show=true, limits=(t[1],t[end],-1.2,1.4),figname="ohc_timeseries.png",figsize=(5,4),conf=(FONT_ANNOT=6,FONT_LABEL=6,PS_LINE_JOIN=round))
To plot a map with the trends in steric sea level, we first have to compute the linear trend from the time series at each grid cell. We will use ordinary least squares for that. Remember that the mean seasonal cycle is retained in our solution, and thus we should co-estimate the seasonal cycle to avoid that we bias our trend. We approximate the seasonal cycle here by an annual and semi-annual cycle.
First, we need a numeric time vector:
tval = @. year(t) + month(t)/12 - 1/24
Then create the design matrix:
design_matrix = ones(length(tval),6)
@. design_matrix[:,2] = tval - $mean(tval)
@. design_matrix[:,3] = sin(2π*tval)
@. design_matrix[:,4] = cos(2π*tval)
@. design_matrix[:,5] = sin(4π*tval)
@. design_matrix[:,6] = cos(4π*tval)
To speed up the trend calculation, we use a QR decomposition:
qrd = qr(design_matrix)
rinv = inv(qrd.R) * qrd.Q'
Now create an empty array to store the trends:
steric_trend = zeros(length(ϕ),length(θ))
And loop over all grid cells to compute the actual trend:
for i in CartesianIndices(steric_trend)
steric_trend[i] = (rinv * η[i,:])[2]
end
Then, plot the actual trend (multiplied by 1000, so we have the trend in units
cpt = makecpt(color=:vik, range=(-5,5,0.25))
grid = mat2grid(steric_trend'.*1000,x=convert.(Float64,ϕ),y=convert.(Float64,θ))
grdimage(grid,projection=:robinson,color=cpt,region=(-180, 180, -90, 90),figsize=(6.5,4),conf=(FONT_ANNOT_PRIMARY=6,FONT_LABEL=6))
coast!(land=:darkgrey)
colorbar!(pos=(anchor=:BC,length=(4,0.2), horizontal=true, offset=(0,0.5),triangles=true), color=cpt, frame=(annot=2, xlabel="Steric sea level (mm yr@+-1@+)"), show=true, nolines=true,figname="trend_map.png",conf=(FONT_ANNOT_PRIMARY=6,FONT_LABEL=6))
The data from Ishii et al. (2017) is provided in the GRIB2 format, and we were not yet able to get the Julia GRIB package to correctly read the data. Therefore, the Ishii et al. (2017) data can be downloaded and converted using the Python script dl_cv_I17.py
. This script downloads and converts the source data into monthly NetCDF files that can be analysed with the Julia scripts. Therefore, to process this data, we need Python (tested with version 3.10) and the packages netcdf4
, numpy
, gsw
, and pygrib
. These packages can be installed with for example pip
or conda
. After installing, check the settings in the def_settings
function in dl_cv_I17.py
. Like the Julia scripts, set settings['dir_analysis']
and settings['dir_analysis_fmt']
to the directories where you want the data to be stored. settings['years']
controls the years that will be processed, and settings['nprocs']
the number of parallel processes.
When this is all set, just run from the terminal:
python dl_cv_I17.py
And everything should work. Then, we can finish the analysis in Julia, by setting settings["dir_analysis_fmt"]
to the same directory as in the Python script, setting settings["model_name"]
to I17
, and setting settings["download_files"]
and settings["convert_files"]
to false
. Then we can run the script and produce the results for Ishii et al. (2017).
The scripts can be expanded to apply them to other datasets, such as other observational products or output from for example CMIP6 models. This does require some Julia coding. A great introduction to Julia is this lecture series from Chris Rackauckas (MIT).
First, we need to download the source data. You can either write a download script to do so, or manually download the data and disable the download_files
flag in the settings.
Next, we have to manually set up the conversion from the source data into the standard format based on struct
of the type grid_struct
, defined in mod_steric_generic.jl
:
struct grid_struct
# Structure to hold all grid variables
ϕ :: Array{Float32,1} # Longitude (°)
θ :: Array{Float32,1} # Latitude (°)
z :: Array{Float32,3} # Depth (m)
p :: Array{Float32,3} # Pressure (Pa)
Δz :: Array{Float32,3} # Height of each grid cell (m)
Δp :: Array{Float32,3} # Pressure difference over each grid cell (Pa)
A :: Array{Float32,2} # Area of each grid cell (m²)
V :: Array{Float32,3} # Volume of each grid cell (m³)
slm :: Array{Bool,3} # Sea-land mask
end
Write a function that reads or computes all these quantities (check the dimensions of eacht variable and convert to the right data type!) and store them in a structure using:
grid=grid_struct(ϕ,θ,z,p,Δz,Δp,A,V,slm);
save_grid_info(grid,settings)
Then we need to write a function that reads the source data and converts it to the standard format. It strongly depends on the source data how this functions should look like, but what we need is for each time step, have a three-dimensional field of SA
) and CT
), compute the density
The source data is often expressed in potential temperature (pt
) and practical salinity (ps
). To convert these fields into
@. SA = grid.slm * gsw_sa_from_sp(ps,grid.p,$reshape(grid.ϕ,(:,1,1)),$reshape(grid.θ,(1,:,1)));
@. CT = grid.slm * gsw_ct_from_pt(SA,pt);
Note that we use Julia's dot notation and add singleton dimensions to the latitude and longitude vectors. With this notation, we apply the conversion to each grid cell.
Then the density
ρ = @. grid.slm * convert(Float32,gsw_rho(SA,CT,grid.p));
Here, we multiply all fields with grid.slm
, our sea-land mask. That ensures all fields that are not in the oceans are set to zero. This avoids the propagation of NaN
values.
The converted fields can be saved in the right format by calling:
data_monthly = data_struct(SA,CT,ρ)
save_analysis_fmt(settings["dir_analysis_fmt"],settings["model_name"],yr,mnth,grid,data_monthly)
Here, mnth
and year
must be integers that specify the specific month and year. The codes are written to produce monthly-mean estimates, hence this format. When the grid and the converted grids have been processed, the calculation of the climatology and the steric and OHC anomalies is straightforward. The scripts can read the standard files and will do all the calculations.
The code does not work as of now when running the native Mac M1 version of Julia. This is caused by GibbsSeaWater, whose backend library has not yet been compiled for the new processor architecture.
To use the multi-processing routines from the dl_cv_I17.py
package on MacOSX Big Sur and newer, we need to add the following line to .bash_profile
:
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
The code currently does not support computing steric anomalies for years for which the data is not complete, for example if the last few months in the current year are not yet available. However, computing results in such situations can be done by manually removing the non-existent months from settings["tvec"]
and settings["tvec_clim]
.