Part 2: Thermal Wind¶
Andrew Delman, updated 2023-02-05.
Objectives¶
To use ECCO state estimate output to explore thermal wind and its use in understanding the ocean circulation.
By the end of the tutorial, you will be able to:
Plot transects along lines of latitude in the ECCO native grid
Rotate fields in model axes to true zonal/meridional orientation
Relate the density structure to the direction of oceanic flow (thermal wind balance)
Estimate oceanic velocity using thermal wind and assumption of a level of no motion
Introduction¶
In the previous tutorial we established that geostrophic balance explains most of the oceanic circulation–at least at the spatial and temporal scales of ECCOv4r4 output. Now we consider thermal wind, a property of geostrophic flow that has been very useful to atmospheric and ocean scientists alike. Thermal wind balance is derived directly from geostrophic balance (in horizontal directions)
and hydrostatic balance (in the vertical direction)
Therefore by differentiating geostrophic balance along the axis and assuming hydrostatic conditions, we get thermal wind balance:
(cf. Vallis ch. 2.8, Kundu and Cohen ch. 14.5, Gill ch. 7.7)
The thermal wind name originates from the application of these equations to atmospheric dynamics, where the velocities are the wind and density is mostly a function of temperature. But the balance is just as important to oceanic dynamics. In both the atmosphere and ocean, thermal wind enabled early measurements of temperature (and salinity in the ocean) to be used to construct velocity fields, where velocities are often difficult to measure directly. And even in present times when velocities can be measured directly, the thermal wind relation connects changes in heat and salt content to changes in the ocean circulation.
Viewing and Plotting Density¶
Transect on a single tile¶
Let’s use the density and velocity output datasets from ECCOv4r4 for January 2000, as used in the previous tutorial. This time, we’ll focus on tile=10
, which is rotated counterclockwise so that the index increases with increasing longitude (not latitude), and the index increases with decreasing latitude. Tiles 7-12 (the 8th through 13th tiles, remember Python indexing starts at 0) are rotated in this way, as illustrated in the following tile plot
Now we are going to load the density dataset and plot density on longitude-depth axes following the line in the grid nearest to N, i.e., i = 73
.
[1]:
# first import needed packages
import numpy as np
import xarray as xr
import xmitgcm
import xgcm
import glob
from os.path import expanduser,join
import sys
user_home_dir = expanduser('~')
sys.path.append(join(user_home_dir,'ECCOv4-py')) # only needed if ecco_v4_py files are stored under this directory
import matplotlib.pyplot as plt
import ecco_v4_py as ecco
from ecco_po_tutorials import * # import from ecco_po_tutorials.py module downloaded in the last tutorial
[2]:
# now open density and grid parameter files
# ShortNames
vel_monthly_shortname = "ECCO_L4_OCEAN_VEL_LLC0090GRID_MONTHLY_V4R4"
denspress_monthly_shortname = "ECCO_L4_DENS_STRAT_PRESS_LLC0090GRID_MONTHLY_V4R4"
grid_params_shortname = "ECCO_L4_GEOMETRY_LLC0090GRID_V4R4"
# density file
download_root_dir = join(user_home_dir,'Downloads','ECCO_V4r4_PODAAC')
download_dir = join(download_root_dir,denspress_monthly_shortname)
curr_denspress_file = list(glob.glob(join(download_dir,'*2000-01*.nc')))
ds_dens = xr.open_dataset(curr_denspress_file[0])
ds_dens
[2]:
<xarray.Dataset> Dimensions: (i: 90, i_g: 90, j: 90, j_g: 90, k: 50, k_u: 50, k_l: 50, k_p1: 51, tile: 13, time: 1, nv: 2, nb: 4) Coordinates: (12/22) * i (i) int32 0 1 2 3 4 5 6 7 8 9 ... 80 81 82 83 84 85 86 87 88 89 * i_g (i_g) int32 0 1 2 3 4 5 6 7 8 9 ... 80 81 82 83 84 85 86 87 88 89 * j (j) int32 0 1 2 3 4 5 6 7 8 9 ... 80 81 82 83 84 85 86 87 88 89 * j_g (j_g) int32 0 1 2 3 4 5 6 7 8 9 ... 80 81 82 83 84 85 86 87 88 89 * k (k) int32 0 1 2 3 4 5 6 7 8 9 ... 40 41 42 43 44 45 46 47 48 49 * k_u (k_u) int32 0 1 2 3 4 5 6 7 8 9 ... 40 41 42 43 44 45 46 47 48 49 ... ... Zu (k_u) float32 -10.0 -20.0 -30.0 ... -5.678e+03 -6.134e+03 Zl (k_l) float32 0.0 -10.0 -20.0 ... -5.244e+03 -5.678e+03 time_bnds (time, nv) datetime64[ns] 2000-01-01 2000-02-01 XC_bnds (tile, j, i, nb) float32 ... YC_bnds (tile, j, i, nb) float32 ... Z_bnds (k, nv) float32 0.0 -10.0 -10.0 ... -5.678e+03 -6.134e+03 Dimensions without coordinates: nv, nb Data variables: RHOAnoma (time, k, tile, j, i) float32 ... DRHODR (time, k_l, tile, j, i) float32 ... PHIHYD (time, k, tile, j, i) float32 ... PHIHYDcR (time, k, tile, j, i) float32 ... Attributes: (12/62) acknowledgement: This research was carried out by the Jet... author: Ian Fenty and Ou Wang cdm_data_type: Grid comment: Fields provided on the curvilinear lat-l... Conventions: CF-1.8, ACDD-1.3 coordinates_comment: Note: the global 'coordinates' attribute... ... ... time_coverage_duration: P1M time_coverage_end: 2000-02-01T00:00:00 time_coverage_resolution: P1M time_coverage_start: 2000-01-01T00:00:00 title: ECCO Ocean Density, Stratification, and ... uuid: 166a1992-4182-11eb-82f9-0cc47a3f43f9
- i: 90
- i_g: 90
- j: 90
- j_g: 90
- k: 50
- k_u: 50
- k_l: 50
- k_p1: 51
- tile: 13
- time: 1
- nv: 2
- nb: 4
- i(i)int320 1 2 3 4 5 6 ... 84 85 86 87 88 89
- axis :
- X
- long_name :
- grid index in x for variables at tracer and 'v' locations
- swap_dim :
- XC
- comment :
- In the Arakawa C-grid system, tracer (e.g., THETA) and 'v' variables (e.g., VVEL) have the same x coordinate on the model grid.
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89])
- i_g(i_g)int320 1 2 3 4 5 6 ... 84 85 86 87 88 89
- axis :
- X
- long_name :
- grid index in x for variables at 'u' and 'g' locations
- c_grid_axis_shift :
- -0.5
- swap_dim :
- XG
- comment :
- In the Arakawa C-grid system, 'u' (e.g., UVEL) and 'g' variables (e.g., XG) have the same x coordinate on the model grid.
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89])
- j(j)int320 1 2 3 4 5 6 ... 84 85 86 87 88 89
- axis :
- Y
- long_name :
- grid index in y for variables at tracer and 'u' locations
- swap_dim :
- YC
- comment :
- In the Arakawa C-grid system, tracer (e.g., THETA) and 'u' variables (e.g., UVEL) have the same y coordinate on the model grid.
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89])
- j_g(j_g)int320 1 2 3 4 5 6 ... 84 85 86 87 88 89
- axis :
- Y
- long_name :
- grid index in y for variables at 'v' and 'g' locations
- c_grid_axis_shift :
- -0.5
- swap_dim :
- YG
- comment :
- In the Arakawa C-grid system, 'v' (e.g., VVEL) and 'g' variables (e.g., XG) have the same y coordinate.
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89])
- k(k)int320 1 2 3 4 5 6 ... 44 45 46 47 48 49
- axis :
- Z
- long_name :
- grid index in z for tracer variables
- swap_dim :
- Z
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49])
- k_u(k_u)int320 1 2 3 4 5 6 ... 44 45 46 47 48 49
- axis :
- Z
- c_grid_axis_shift :
- 0.5
- swap_dim :
- Zu
- coverage_content_type :
- coordinate
- long_name :
- grid index in z corresponding to the bottom face of tracer grid cells ('w' locations)
- comment :
- First index corresponds to the bottom surface of the uppermost tracer grid cell. The use of 'u' in the variable name follows the MITgcm convention for ocean variables in which the upper (u) face of a tracer grid cell on the logical grid corresponds to the bottom face of the grid cell on the physical grid.
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49])
- k_l(k_l)int320 1 2 3 4 5 6 ... 44 45 46 47 48 49
- axis :
- Z
- c_grid_axis_shift :
- -0.5
- swap_dim :
- Zl
- coverage_content_type :
- coordinate
- long_name :
- grid index in z corresponding to the top face of tracer grid cells ('w' locations)
- comment :
- First index corresponds to the top surface of the uppermost tracer grid cell. The use of 'l' in the variable name follows the MITgcm convention for ocean variables in which the lower (l) face of a tracer grid cell on the logical grid corresponds to the top face of the grid cell on the physical grid.
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49])
- k_p1(k_p1)int320 1 2 3 4 5 6 ... 45 46 47 48 49 50
- axis :
- Z
- long_name :
- grid index in z for variables at 'w' locations
- c_grid_axis_shift :
- [-0.5 0.5]
- swap_dim :
- Zp1
- comment :
- Includes top of uppermost model tracer cell (k_p1=0) and bottom of lowermost tracer cell (k_p1=51).
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50])
- tile(tile)int320 1 2 3 4 5 6 7 8 9 10 11 12
- long_name :
- lat-lon-cap tile index
- comment :
- The ECCO V4 horizontal model grid is divided into 13 tiles of 90x90 cells for convenience.
- coverage_content_type :
- coordinate
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
- time(time)datetime64[ns]2000-01-16T12:00:00
- long_name :
- center time of averaging period
- axis :
- T
- bounds :
- time_bnds
- coverage_content_type :
- coordinate
- standard_name :
- time
array(['2000-01-16T12:00:00.000000000'], dtype='datetime64[ns]')
- XC(tile, j, i)float32...
- long_name :
- longitude of tracer grid cell center
- units :
- degrees_east
- coordinate :
- YC XC
- bounds :
- XC_bnds
- comment :
- nonuniform grid spacing
- coverage_content_type :
- coordinate
- standard_name :
- longitude
[105300 values with dtype=float32]
- YC(tile, j, i)float32...
- long_name :
- latitude of tracer grid cell center
- units :
- degrees_north
- coordinate :
- YC XC
- bounds :
- YC_bnds
- comment :
- nonuniform grid spacing
- coverage_content_type :
- coordinate
- standard_name :
- latitude
[105300 values with dtype=float32]
- XG(tile, j_g, i_g)float32...
- long_name :
- longitude of 'southwest' corner of tracer grid cell
- units :
- degrees_east
- coordinate :
- YG XG
- comment :
- Nonuniform grid spacing. Note: 'southwest' does not correspond to geographic orientation but is used for convenience to describe the computational grid. See MITgcm dcoumentation for details.
- coverage_content_type :
- coordinate
- standard_name :
- longitude
[105300 values with dtype=float32]
- YG(tile, j_g, i_g)float32...
- long_name :
- latitude of 'southwest' corner of tracer grid cell
- units :
- degrees_north
- coordinate :
- YG XG
- comment :
- Nonuniform grid spacing. Note: 'southwest' does not correspond to geographic orientation but is used for convenience to describe the computational grid. See MITgcm dcoumentation for details.
- coverage_content_type :
- coordinate
- standard_name :
- latitude
[105300 values with dtype=float32]
- Z(k)float32...
- long_name :
- depth of tracer grid cell center
- units :
- m
- positive :
- up
- bounds :
- Z_bnds
- comment :
- Non-uniform vertical spacing.
- coverage_content_type :
- coordinate
- standard_name :
- depth
array([-5.000000e+00, -1.500000e+01, -2.500000e+01, -3.500000e+01, -4.500000e+01, -5.500000e+01, -6.500000e+01, -7.500500e+01, -8.502500e+01, -9.509500e+01, -1.053100e+02, -1.158700e+02, -1.271500e+02, -1.397400e+02, -1.544700e+02, -1.724000e+02, -1.947350e+02, -2.227100e+02, -2.574700e+02, -2.999300e+02, -3.506800e+02, -4.099300e+02, -4.774700e+02, -5.527100e+02, -6.347350e+02, -7.224000e+02, -8.144700e+02, -9.097400e+02, -1.007155e+03, -1.105905e+03, -1.205535e+03, -1.306205e+03, -1.409150e+03, -1.517095e+03, -1.634175e+03, -1.765135e+03, -1.914150e+03, -2.084035e+03, -2.276225e+03, -2.491250e+03, -2.729250e+03, -2.990250e+03, -3.274250e+03, -3.581250e+03, -3.911250e+03, -4.264250e+03, -4.640250e+03, -5.039250e+03, -5.461250e+03, -5.906250e+03], dtype=float32)
- Zp1(k_p1)float32...
- long_name :
- depth of tracer grid cell interface
- units :
- m
- positive :
- up
- comment :
- Contains one element more than the number of vertical layers. First element is 0m, the depth of the upper interface of the surface grid cell. Last element is the depth of the lower interface of the deepest grid cell.
- coverage_content_type :
- coordinate
- standard_name :
- depth
array([ 0. , -10. , -20. , -30. , -40. , -50. , -60. , -70. , -80.01, -90.04, -100.15, -110.47, -121.27, -133.03, -146.45, -162.49, -182.31, -207.16, -238.26, -276.68, -323.18, -378.18, -441.68, -513.26, -592.16, -677.31, -767.49, -861.45, -958.03, -1056.28, -1155.53, -1255.54, -1356.87, -1461.43, -1572.76, -1695.59, -1834.68, -1993.62, -2174.45, -2378. , -2604.5 , -2854. , -3126.5 , -3422. , -3740.5 , -4082. , -4446.5 , -4834. , -5244.5 , -5678. , -6134.5 ], dtype=float32)
- Zu(k_u)float32...
- units :
- m
- positive :
- up
- coverage_content_type :
- coordinate
- standard_name :
- depth
- long_name :
- depth of the bottom face of tracer grid cells
- comment :
- First element is -10m, the depth of the bottom face of the first tracer grid cell. Last element is the depth of the bottom face of the deepest grid cell. The use of 'u' in the variable name follows the MITgcm convention for ocean variables in which the upper (u) face of a tracer grid cell on the logical grid corresponds to the bottom face of the grid cell on the physical grid. In other words, the logical vertical grid of MITgcm ocean variables is inverted relative to the physical vertical grid.
array([ -10. , -20. , -30. , -40. , -50. , -60. , -70. , -80.01, -90.04, -100.15, -110.47, -121.27, -133.03, -146.45, -162.49, -182.31, -207.16, -238.26, -276.68, -323.18, -378.18, -441.68, -513.26, -592.16, -677.31, -767.49, -861.45, -958.03, -1056.28, -1155.53, -1255.54, -1356.87, -1461.43, -1572.76, -1695.59, -1834.68, -1993.62, -2174.45, -2378. , -2604.5 , -2854. , -3126.5 , -3422. , -3740.5 , -4082. , -4446.5 , -4834. , -5244.5 , -5678. , -6134.5 ], dtype=float32)
- Zl(k_l)float32...
- units :
- m
- positive :
- up
- coverage_content_type :
- coordinate
- standard_name :
- depth
- long_name :
- depth of the top face of tracer grid cells
- comment :
- First element is 0m, the depth of the top face of the first tracer grid cell (ocean surface). Last element is the depth of the top face of the deepest grid cell. The use of 'l' in the variable name follows the MITgcm convention for ocean variables in which the lower (l) face of a tracer grid cell on the logical grid corresponds to the top face of the grid cell on the physical grid. In other words, the logical vertical grid of MITgcm ocean variables is inverted relative to the physical vertical grid.
array([ 0. , -10. , -20. , -30. , -40. , -50. , -60. , -70. , -80.01, -90.04, -100.15, -110.47, -121.27, -133.03, -146.45, -162.49, -182.31, -207.16, -238.26, -276.68, -323.18, -378.18, -441.68, -513.26, -592.16, -677.31, -767.49, -861.45, -958.03, -1056.28, -1155.53, -1255.54, -1356.87, -1461.43, -1572.76, -1695.59, -1834.68, -1993.62, -2174.45, -2378. , -2604.5 , -2854. , -3126.5 , -3422. , -3740.5 , -4082. , -4446.5 , -4834. , -5244.5 , -5678. ], dtype=float32)
- time_bnds(time, nv)datetime64[ns]...
- comment :
- Start and end times of averaging period.
- coverage_content_type :
- coordinate
- long_name :
- time bounds of averaging period
array([['2000-01-01T00:00:00.000000000', '2000-02-01T00:00:00.000000000']], dtype='datetime64[ns]')
- XC_bnds(tile, j, i, nb)float32...
- comment :
- Bounds array follows CF conventions. XC_bnds[i,j,0] = 'southwest' corner (j-1, i-1), XC_bnds[i,j,1] = 'southeast' corner (j-1, i+1), XC_bnds[i,j,2] = 'northeast' corner (j+1, i+1), XC_bnds[i,j,3] = 'northwest' corner (j+1, i-1). Note: 'southwest', 'southeast', northwest', and 'northeast' do not correspond to geographic orientation but are used for convenience to describe the computational grid. See MITgcm dcoumentation for details.
- coverage_content_type :
- coordinate
- long_name :
- longitudes of tracer grid cell corners
[421200 values with dtype=float32]
- YC_bnds(tile, j, i, nb)float32...
- comment :
- Bounds array follows CF conventions. YC_bnds[i,j,0] = 'southwest' corner (j-1, i-1), YC_bnds[i,j,1] = 'southeast' corner (j-1, i+1), YC_bnds[i,j,2] = 'northeast' corner (j+1, i+1), YC_bnds[i,j,3] = 'northwest' corner (j+1, i-1). Note: 'southwest', 'southeast', northwest', and 'northeast' do not correspond to geographic orientation but are used for convenience to describe the computational grid. See MITgcm dcoumentation for details.
- coverage_content_type :
- coordinate
- long_name :
- latitudes of tracer grid cell corners
[421200 values with dtype=float32]
- Z_bnds(k, nv)float32...
- comment :
- One pair of depths for each vertical level.
- coverage_content_type :
- coordinate
- long_name :
- depths of tracer grid cell upper and lower interfaces
array([[ 0. , -10. ], [ -10. , -20. ], [ -20. , -30. ], [ -30. , -40. ], [ -40. , -50. ], [ -50. , -60. ], [ -60. , -70. ], [ -70. , -80.01 ], [ -80.01 , -90.04 ], [ -90.04 , -100.15 ], [ -100.15 , -110.47 ], [ -110.47 , -121.270004], [ -121.270004, -133.03 ], [ -133.03 , -146.45 ], [ -146.45 , -162.48999 ], [ -162.48999 , -182.31 ], [ -182.31 , -207.16 ], [ -207.16 , -238.26001 ], [ -238.26001 , -276.68 ], [ -276.68 , -323.18 ], [ -323.18 , -378.18 ], [ -378.18 , -441.68 ], [ -441.68 , -513.26 ], [ -513.26 , -592.16003 ], [ -592.16003 , -677.31006 ], [ -677.31006 , -767.49005 ], [ -767.49005 , -861.4501 ], [ -861.4501 , -958.0301 ], [ -958.0301 , -1056.28 ], [-1056.28 , -1155.53 ], [-1155.53 , -1255.54 ], [-1255.54 , -1356.87 ], [-1356.87 , -1461.4299 ], [-1461.4299 , -1572.7599 ], [-1572.7599 , -1695.5898 ], [-1695.5898 , -1834.6798 ], [-1834.6798 , -1993.6199 ], [-1993.6199 , -2174.45 ], [-2174.45 , -2378. ], [-2378. , -2604.5 ], [-2604.5 , -2854. ], [-2854. , -3126.5 ], [-3126.5 , -3422. ], [-3422. , -3740.5 ], [-3740.5 , -4082. ], [-4082. , -4446.5 ], [-4446.5 , -4834. ], [-4834. , -5244.5 ], [-5244.5 , -5678. ], [-5678. , -6134.5 ]], dtype=float32)
- RHOAnoma(time, k, tile, j, i)float32...
- long_name :
- In-situ seawater density anomaly
- units :
- kg m-3
- coverage_content_type :
- modelResult
- valid_min :
- -18.81316375732422
- valid_max :
- 25.540061950683594
- comment :
- In-situ seawater density anomaly relative to the reference density, rhoConst. rhoConst = 1029 kg m-3
[5265000 values with dtype=float32]
- DRHODR(time, k_l, tile, j, i)float32...
- long_name :
- Density stratification
- units :
- kg m-3 m-1
- coverage_content_type :
- modelResult
- comment :
- Density stratification: d(sigma) d z-1. Note: density computations are done with in-situ density. The vertical derivatives of in-situ density and locally-referenced potential density are identical The equation of state is a modified UNESCO formula by Jackett and McDougall (1995), which uses the model variable potential temperature as input assuming a horizontally and temporally constant pressure of $p_0=-g\rho_{0} z$.
- valid_min :
- -0.7393171787261963
- valid_max :
- 3.208351699868217e-05
[5265000 values with dtype=float32]
- PHIHYD(time, k, tile, j, i)float32...
- long_name :
- Ocean hydrostatic pressure anomaly
- units :
- m2 s-2
- coverage_content_type :
- modelResult
- comment :
- PHIHYD = p(k) / rhoConst - g z*(k,t), where p = hydrostatic ocean pressure at depth level k, rhoConst = reference density (1029 kg m-3), g is acceleration due to gravity (9.81 m s-2), and z*(k,t) is model depth at level k and time t. Units: p:[kg m-1 s-2], rhoConst:[kg m-3], g:[m s-2], H(t):[m]. Note: includes atmospheric pressure loading. Quantity referred to in some contexts as hydrostatic pressure anomaly. PHIBOT accounts for the model's time-varying grid cell thickness (z* coordinate system). See PHIHYDcR for hydrostatic pressure potential anomaly calculated using time-invariant grid cell thicknesses. PHIHYD is NOT corrected for global mean steric sea level changes related to density changes in the Boussinesq volume-conserving model (Greatbatch correction, see sterGloH).
- valid_min :
- 79.53374481201172
- valid_max :
- 783.2001953125
[5265000 values with dtype=float32]
- PHIHYDcR(time, k, tile, j, i)float32...
- long_name :
- Ocean hydrostatic pressure anomaly at constant depths
- units :
- m2 s-2
- coverage_content_type :
- modelResult
- comment :
- PHIHYD = p(k) / rhoConst - g z(k,t), where p = hydrostatic ocean pressure at depth level k, rhoConst = reference density (1029 kg m-3), g is acceleration due to gravity (9.81 m s-2), and z(k,t) is fixed model depth at level k. Units: p:[kg m-1 s-2], rhoConst:[kg m-3], g:[m s-2], H(t):[m]. Note: includes atmospheric pressure loading. Quantity referred to in some contexts as hydrostatic pressure potential anomaly. PHIHYDcR is calculated with respect to the model's initial, time-invariant grid cell thicknesses. See PHIHYD for hydrostatic pressure anomaly calculated using model's time-variable grid cell thicknesses (z* coordinate system). PHIHYDcR is is NOT corrected for global mean steric sea level changes related to density changes in the Boussinesq volume-conserving model (Greatbatch correction, see sterGloH).
- valid_min :
- 78.17041778564453
- valid_max :
- 783.6217041015625
[5265000 values with dtype=float32]
- acknowledgement :
- This research was carried out by the Jet Propulsion Laboratory, managed by the California Institute of Technology under a contract with the National Aeronautics and Space Administration.
- author :
- Ian Fenty and Ou Wang
- cdm_data_type :
- Grid
- comment :
- Fields provided on the curvilinear lat-lon-cap 90 (llc90) native grid used in the ECCO model.
- Conventions :
- CF-1.8, ACDD-1.3
- coordinates_comment :
- Note: the global 'coordinates' attribute describes auxillary coordinates.
- creator_email :
- ecco-group@mit.edu
- creator_institution :
- NASA Jet Propulsion Laboratory (JPL)
- creator_name :
- ECCO Consortium
- creator_type :
- group
- creator_url :
- https://ecco-group.org
- date_created :
- 2020-12-18T14:40:54
- date_issued :
- 2020-12-18T14:40:54
- date_metadata_modified :
- 2021-03-15T21:55:10
- date_modified :
- 2021-03-15T21:55:10
- geospatial_bounds_crs :
- EPSG:4326
- geospatial_lat_max :
- 90.0
- geospatial_lat_min :
- -90.0
- geospatial_lat_resolution :
- variable
- geospatial_lat_units :
- degrees_north
- geospatial_lon_max :
- 180.0
- geospatial_lon_min :
- -180.0
- geospatial_lon_resolution :
- variable
- geospatial_lon_units :
- degrees_east
- geospatial_vertical_max :
- 0.0
- geospatial_vertical_min :
- -6134.5
- geospatial_vertical_positive :
- up
- geospatial_vertical_resolution :
- variable
- geospatial_vertical_units :
- meter
- history :
- Inaugural release of an ECCO Central Estimate solution to PO.DAAC
- id :
- 10.5067/ECL5M-ODE44
- institution :
- NASA Jet Propulsion Laboratory (JPL)
- instrument_vocabulary :
- GCMD instrument keywords
- keywords :
- EARTH SCIENCE > OCEANS > OCEAN PRESSURE > WATER PRESSURE, EARTH SCIENCE SERVICES > MODELS > EARTH SCIENCE REANALYSES/ASSIMILATION MODELS, EARTH SCIENCE > OCEANS > OCEAN PRESSURE, EARTH SCIENCE > OCEANS > SALINITY/DENSITY > DENSITY, EARTH SCIENCE > OCEANS > SALINITY/DENSITY
- keywords_vocabulary :
- NASA Global Change Master Directory (GCMD) Science Keywords
- license :
- Public Domain
- metadata_link :
- https://cmr.earthdata.nasa.gov/search/collections.umm_json?ShortName=ECCO_L4_DENS_STRAT_PRESS_LLC0090GRID_MONTHLY_V4R4
- naming_authority :
- gov.nasa.jpl
- platform :
- ERS-1/2, TOPEX/Poseidon, Geosat Follow-On (GFO), ENVISAT, Jason-1, Jason-2, CryoSat-2, SARAL/AltiKa, Jason-3, AVHRR, Aquarius, SSM/I, SSMIS, GRACE, DTU17MDT, Argo, WOCE, GO-SHIP, MEOP, Ice Tethered Profilers (ITP)
- platform_vocabulary :
- GCMD platform keywords
- processing_level :
- L4
- product_name :
- OCEAN_DENS_STRAT_PRESS_mon_mean_2000-01_ECCO_V4r4_native_llc0090.nc
- product_time_coverage_end :
- 2018-01-01T00:00:00
- product_time_coverage_start :
- 1992-01-01T12:00:00
- product_version :
- Version 4, Release 4
- program :
- NASA Physical Oceanography, Cryosphere, Modeling, Analysis, and Prediction (MAP)
- project :
- Estimating the Circulation and Climate of the Ocean (ECCO)
- publisher_email :
- podaac@podaac.jpl.nasa.gov
- publisher_institution :
- PO.DAAC
- publisher_name :
- Physical Oceanography Distributed Active Archive Center (PO.DAAC)
- publisher_type :
- institution
- publisher_url :
- https://podaac.jpl.nasa.gov
- references :
- ECCO Consortium, Fukumori, I., Wang, O., Fenty, I., Forget, G., Heimbach, P., & Ponte, R. M. 2020. Synopsis of the ECCO Central Production Global Ocean and Sea-Ice State Estimate (Version 4 Release 4). doi:10.5281/zenodo.3765928
- source :
- The ECCO V4r4 state estimate was produced by fitting a free-running solution of the MITgcm (checkpoint 66g) to satellite and in situ observational data in a least squares sense using the adjoint method
- standard_name_vocabulary :
- NetCDF Climate and Forecast (CF) Metadata Convention
- summary :
- This dataset provides monthly-averaged ocean density, stratification, and hydrostatic pressure on the lat-lon-cap 90 (llc90) native model grid from the ECCO Version 4 Release 4 (V4r4) ocean and sea-ice state estimate. Estimating the Circulation and Climate of the Ocean (ECCO) state estimates are dynamically and kinematically-consistent reconstructions of the three-dimensional, time-evolving ocean, sea-ice, and surface atmospheric states. ECCO V4r4 is a free-running solution of a global, nominally 1-degree configuration of the MIT general circulation model (MITgcm) that has been fit to observations in a least-squares sense. Observational data constraints used in V4r4 include sea surface height (SSH) from satellite altimeters [ERS-1/2, TOPEX/Poseidon, GFO, ENVISAT, Jason-1,2,3, CryoSat-2, and SARAL/AltiKa]; sea surface temperature (SST) from satellite radiometers [AVHRR], sea surface salinity (SSS) from the Aquarius satellite radiometer/scatterometer, ocean bottom pressure (OBP) from the GRACE satellite gravimeter; sea-ice concentration from satellite radiometers [SSM/I and SSMIS], and in-situ ocean temperature and salinity measured with conductivity-temperature-depth (CTD) sensors and expendable bathythermographs (XBTs) from several programs [e.g., WOCE, GO-SHIP, Argo, and others] and platforms [e.g., research vessels, gliders, moorings, ice-tethered profilers, and instrumented pinnipeds]. V4r4 covers the period 1992-01-01T12:00:00 to 2018-01-01T00:00:00.
- time_coverage_duration :
- P1M
- time_coverage_end :
- 2000-02-01T00:00:00
- time_coverage_resolution :
- P1M
- time_coverage_start :
- 2000-01-01T00:00:00
- title :
- ECCO Ocean Density, Stratification, and Hydrostatic Pressure - Monthly Mean llc90 Grid (Version 4 Release 4)
- uuid :
- 166a1992-4182-11eb-82f9-0cc47a3f43f9
[3]:
# load grid parameters file
grid_params_file = "GRID_GEOMETRY_ECCO_V4r4_native_llc0090.nc"
grid_params_file_path = join(download_root_dir,grid_params_shortname,grid_params_file)
ds_grid = xr.open_dataset(grid_params_file_path)
# find index closest to target latitude
lat_target = 26.
target_i = np.argmin(np.abs(ds_grid.YC[10,70,:].values - lat_target))
# the choice of j = 70 above is random, any j index from 0 to 89 will yield the same target_i
target_i
[3]:
73
[4]:
# # define two new functions (also in ecco_po_tutorials module, but shown here for clarity)
# function to create 2 subplots with split depth axes
def depth_two_subplots(horiz_coords,depth_coords,data,k_split,cmap,mask=None,fig=None,axs=None):
"""
Make 2 subplots with depth on y-axis, for shallow and deeper depths, given parameters:
horiz_coords: horizontal coordinate, xarray DataArray
depth_coords: depth_coordinate, xarray DataArray
data: 2-D xarray DataArray
k_split: k coordinate to split the plot at, integer
cmap: string specifying colormap to use
mask: 2-D boolean (land) mask, optional
fig: figure object, optional, default is new figure is created
axs: axes with two subplots oriented vertically, default is they are created
"""
if (fig is None) and (axs is None):
fig,axs = plt.subplots(2,1,figsize=(10,10)) # subplots for different depths
elif (fig is None) or (axs is None):
print("Warning: Only one of fig or axs has been supplied, not both")
curr_ax = axs[0]
curr_plot_0 = curr_ax.pcolormesh(horiz_coords,depth_coords[:k_split],\
data.isel(k=np.arange(k_split)),cmap=cmap)
if mask is not None:
curr_mask = mask.isel(k=np.arange(k_split))
plot_mask(horiz_coords,depth_coords[:k_split],curr_mask,ax=curr_ax,color=(0,0,0))
curr_ax.set_ylim(curr_ax.get_ylim()[::-1])
curr_ax.set_ylabel('Depth [m]')
clim_0 = curr_plot_0.get_clim()
curr_ax = axs[1]
curr_plot_1 = curr_ax.pcolormesh(horiz_coords,depth_coords[k_split:],\
data.isel(k=np.arange(25,len(depth_coords))),cmap=cmap)
if mask is not None:
curr_mask = mask.isel(k=np.arange(25,len(depth_coords)))
plot_mask(horiz_coords,depth_coords[k_split:],curr_mask,ax=curr_ax,color=(0,0,0))
curr_ax.set_ylim(curr_ax.get_ylim()[::-1])
curr_ax.set_ylabel('Depth [m]')
clim_1 = curr_plot_1.get_clim()
# create shared colorbar for 2 subplots
new_clim = [np.fmin(clim_0[0],clim_1[0]),np.fmax(clim_0[1],clim_1[1])]
curr_plot_0.set_clim(new_clim)
curr_plot_1.set_clim(new_clim)
fig.colorbar(curr_plot_1,ax=axs[:])
return fig,axs
# function to scale colormap, coordinating among multiple plots
def cmap_zerocent_scale_multiplots(plot_objs,scale_factor):
"""
Center colormap at zero and scale relative to existing |maximum| value across multiple plots,
given plot objects (as list) and scale_factor, a number of type float.
Returns new colormap limits as new_clim.
"""
clim_plots = np.empty((len(plot_objs),2))
for count,curr_obj in enumerate(plot_objs):
clim_plots[count,:] = curr_obj.get_clim()
new_clim = (scale_factor*np.nanmax(np.abs(clim_plots)))*np.array([-1,1])
for curr_obj in plot_objs:
curr_obj.set_clim(new_clim)
return new_clim
[5]:
# plot density data along target latitude
# retrieve density field
densanom = ds_dens.RHOAnoma
rhoConst = 1029.
dens = rhoConst + densanom
# load land mask (a boolean array) from grid parameters
land_mask = ~ds_grid.maskC
# plot density along latitude line
tile_plot = 10
i_plot = target_i
# create dictionaries to pass as indices to xarray isel
isel_dict = dict({'tile':tile_plot,'i':i_plot})
isel_dict_ig = dict({'tile':tile_plot,'i_g':i_plot})
plt.rcParams["font.size"] = 12 # set default font size for plots in this tutorial
# make figure
curr_mask = land_mask.isel(isel_dict).squeeze()
fig,axs = depth_two_subplots(ds_grid.XG.isel(isel_dict_ig),-ds_grid.Zl,\
dens.isel(isel_dict).squeeze(),\
k_split=25,cmap='seismic_r',mask=curr_mask)
# '_r' at the end of a colormap name reverses the colormap
axs[0].set_title('Density [kg m$^{-3}$] along 26 N latitude')
axs[1].set_xlabel('Longitude')
plt.show()
Density anomalies at depth¶
Notice that in the plots above the density is primarily a function of depth (i.e. pressure). However, thermal wind balance involves horizontal (not vertical) density gradients, and so the density structure that drives the ocean circulation is largely obscured in these plots. One way to look at the horizontal variation of density is to remove a mean density at each depth level; here’s what this looks like if we compute the means using only grid cells along this transect:
[6]:
tile_plot = 10
i_plot = target_i
isel_dict = dict({'tile':tile_plot,'i':i_plot})
isel_dict_ig = dict({'tile':tile_plot,'i_g':i_plot})
# compute horizontal density means at each depth level
curr_mask = (~np.isnan((ds_grid.rA*dens)))
dens_mean_atdepth = ((curr_mask*ds_grid.rA*dens).isel(isel_dict).sum("j"))\
/((curr_mask*ds_grid.rA).isel(isel_dict).sum("j"))
# anomalies from density means at depth
dens_anom_atdepth = dens.isel(isel_dict) - dens_mean_atdepth
# make figure
curr_mask = land_mask.isel(isel_dict).squeeze()
fig,axs = depth_two_subplots(ds_grid.XG.isel(isel_dict_ig),-ds_grid.Zl,\
dens_anom_atdepth.squeeze(),\
k_split=25,cmap='seismic_r',mask=curr_mask)
axs[0].set_title('Density minus transect horiz mean [kg m$^{-3}$],\n along 26 N latitude')
axs[1].set_xlabel('Longitude')
plot_objs = [axs[0].get_children()[0],axs[1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.8)
plt.show()
and if we instead remove the global mean at each depth level:
[7]:
# compute horizontal density means at each depth level
curr_mask = (~np.isnan((ds_grid.rA*dens)))
dens_mean_atdepth = (((curr_mask*ds_grid.rA*dens).sum("tile")).sum("j")).sum("i")\
/((((curr_mask*ds_grid.rA).sum("tile")).sum("j")).sum("i"))
# anomalies from density means at depth
dens_anom_atdepth = dens.isel(isel_dict) - dens_mean_atdepth
# make figure
curr_mask = land_mask.isel(isel_dict).squeeze()
fig,axs = depth_two_subplots(ds_grid.XG.isel(isel_dict_ig),-ds_grid.Zl,\
dens_anom_atdepth.squeeze(),\
k_split=25,cmap='seismic_r',mask=curr_mask)
axs[0].set_title('Density minus global horiz mean [kg m$^{-3}$],\n along 26 N latitude')
axs[1].set_xlabel('Longitude')
plot_objs = [axs[0].get_children()[0],axs[1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.8)
plt.show()
Plotting along a line of latitude (multiple tiles)¶
All the plots above consider a transect in a single tile, but ocean basins generally span multiple tiles of the llc90 grid architecture. In the code below, we define functions to map transects following a line of latitude, across multiple tiles if need be. Then we apply the function to plot density minus horizontal means along the Atlantic 26 N transect.
[8]:
# # define some functions (these are also in the ecco_po_tutorials.py module)
# define function to take modulus 360, output in a given range
def mod_360_range(n,low_bound):
"Compute n mod 360; output is in the range [n,n+360)"
out = ((n - low_bound) % 360) + low_bound
return out
# define function that searches for and outputs grid indices along line of latitude
def llc_grid_idx_along_lat(lat_transect,lon_bnds):
"""
Finds grid indices along a given line of latitude.
Input parameters:
- lat_transect: line of latitude to plot along, float
- lon_bnds: 2 elements specifying western and eastern longitude bounds of transect, list or numpy array
The difference between the bounds must be >0 and <=360
Outputs:
- idx_along_lat: grid indices along line of latitude, dict containing 'tile','j','i' as keys
- XC_along_lat: longitude of grid cell centers, numpy array
- XG_along_lat: longitude of western grid cell edges, numpy array
"""
# identify indices of grid cells along latitude transect
mask_along_lat = np.logical_and((ds_grid.YC_bnds > lat_transect).sum("nb") > 0,\
(ds_grid.YC_bnds > lat_transect).sum("nb") < 4)
mask_in_lon_bnds = np.logical_and((mod_360_range(ds_grid.XC,lon_bnds[1]) > mod_360_range(lon_bnds[0],lon_bnds[1])),\
(mod_360_range(ds_grid.XC,lon_bnds[0]) < mod_360_range(lon_bnds[1],lon_bnds[0] + 1.e-5)))
mask_along_lat = np.logical_and(mask_along_lat,mask_in_lon_bnds)
idx_along_lat = mask_along_lat.values.nonzero()
# create longitude arrays along transect
XC_along_lat = np.zeros((len(idx_along_lat[0]),))
XG_along_lat = np.zeros((len(idx_along_lat[0]),))
# for loop through indices along transect, using zip to iterate through three spatial indices
for count,(tile_idx,j_idx,i_idx) in enumerate(zip(idx_along_lat[0],idx_along_lat[1],idx_along_lat[2])):
curr_XC = mod_360_range(ds_grid.XC[tile_idx,j_idx,i_idx],lon_bnds[0])
XC_along_lat[count] = curr_XC
XG_along_lat[count] = np.min(mod_360_range(ds_grid.XC_bnds[tile_idx,j_idx,i_idx,:],curr_XC - 180))
# sort grid cells in order of increasing longitude
idx_sorted = np.argsort(XC_along_lat)
XC_along_lat = XC_along_lat[idx_sorted]
XG_along_lat = XG_along_lat[idx_sorted]
tile_idx_sorted = idx_along_lat[0][idx_sorted]
j_idx_sorted = idx_along_lat[1][idx_sorted]
i_idx_sorted = idx_along_lat[2][idx_sorted]
idx_along_lat = {'tile':tile_idx_sorted,'j':j_idx_sorted,'i':i_idx_sorted}
return idx_along_lat,XC_along_lat,XG_along_lat
# define function to retrieve data along latitude transect, given grid indices
def data_along_lat(data_in,idx_along_lat,XC_along_lat,XG_along_lat):
"""
Creates xarray DataArray along latitude transect, given grid indices
Input parameters:
- data_in: data to plot along latitude, xarray DataArray or numpy array
must have 4 or 5 dimensions, 'time','k','tile','j','i', 'time' is optional
- XC_along_lat: longitudes of grid cell centers, numpy array
- XG_along_lat: longitudes of western grid cell edges, numpy array
Outputs:
- data_xrarray: data along latitude transect, xarray DataArray
"""
# retrieve grid indices along transect
tile_idx_along = idx_along_lat['tile']
j_idx_along = idx_along_lat['j']
i_idx_along = idx_along_lat['i']
# construct DataArray along transect
data_along_lat = np.empty(data_in.shape[:-3] + (len(idx_along_lat['i']),))
data_along_lat.fill(np.nan)
for count,(tile_idx,j_idx,i_idx) in enumerate(zip(tile_idx_along,j_idx_along,i_idx_along)):
if len(data_in.shape) == 4:
data_along_lat[:,count] = data_in[:,tile_idx,j_idx,i_idx]
elif len(data_in.shape) == 5:
data_along_lat[:,:,count] = data_in[:,:,tile_idx,j_idx,i_idx]
if len(data_in.shape) == 4: # no time dimension
data_xrarray = xr.DataArray(
data=data_along_lat,
dims=["k","lon"],
coords=dict(
Z=(["k"],ds_grid.Z.data),
lon=(["lon"],XC_along_lat),
lonW=(["lon"],XG_along_lat),
lat=lat_transect,
),
)
elif len(data_in.shape) == 5: # include time dimension
try:
time_coord = data_in.time.data
except:
time_coord = np.nan
data_xrarray = xr.DataArray(
data=data_along_lat,
dims=["time","k","lon"],
coords=dict(
time=(["time"],time_coord),
Z=(["k"],ds_grid.Z.data),
lon=(["lon"],XC_along_lat),
lonW=(["lon"],XG_along_lat),
lat=lat_transect,
),
)
return data_xrarray
# define function to extract data along line of latitude
def lon_depth_along_lat(lat_transect,lon_bnds,data_in):
"""
Function to extract data along line of latitude.
Input parameters:
- lat_transect: line of latitude to plot along, float
- lon_bnds: 2 elements specifying western and eastern longitude bounds of transect, list or numpy array
- data_in: data, numpy array or xarray DataArray
Outputs:
- XC_along_lat: longitude of grid cell centers, numpy array
- XG_along_lat: longitude of western grid cell edges, numpy array
- data_xrarray: data along latitude transect, xarray DataArray
"""
# identify indices of grid cells along latitude transect
idx_along_lat,XC_along_lat,XG_along_lat = llc_grid_idx_along_lat(lat_transect,lon_bnds)
# construct DataArray along transect
data_xrarray = data_along_lat(data_in,idx_along_lat,XC_along_lat,XG_along_lat)
return XC_along_lat,XG_along_lat,data_xrarray
[9]:
# create data array along line of latitude
lat_transect = 26.
Atl_W_bound = -82.
Atl_E_bound = -14.
lon_bnds = [Atl_W_bound,Atl_E_bound]
XC_transect,XG_transect,mask_transect = lon_depth_along_lat(lat_transect,lon_bnds,land_mask)
XC_transect,XG_transect,dens_transect = lon_depth_along_lat(lat_transect,lon_bnds,dens)
# anomalies from density means at depth
dens_anom_transect = dens_transect - np.expand_dims(dens_mean_atdepth,axis=-1)
# make figure
fig,axs = depth_two_subplots(XG_transect,-ds_grid.Zl,\
dens_anom_transect.squeeze(),\
k_split=25,cmap='seismic_r',mask=mask_transect)
axs[0].set_title('Density minus global horiz mean [kg m$^{-3}$],\n along 26 N latitude')
axs[1].set_xlabel('Longitude')
plot_objs = [axs[0].get_children()[0],axs[1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.8)
plt.show()
Computing thermal wind balance¶
Thermal wind balance across Atlantic (26 N)¶
Now let’s compute horizontal density gradients (times ) and compare with vertical velocity gradients—i.e., thermal wind balance. This calculation is complicated by the fact that density, and all start out at different positions on each grid cell, and that we take differences in both horizontal and vertical directions. To enable this to be done so that both sides of the equations end up on the same points in the grid, the calculations are done in the following order:
Density at center of grid cells (i,j,k) –horiz difference–>
Horizontal density gradients at edge of grid cells (i_g,j,k) or (i,j_g,k) –horiz interpolate–>
Interpolated horizontal density gradients (i,j,k)
, at edge of grid cells (i_g,j,k) or (i,j_g,k) –horiz interpolate–>
Centered , (i,j,k) –vert difference–>
Vertical , gradients (i,j,k_l) –vert interpolate–>
Interpolated , gradients (i,j,k)
Then we rotate velocities and gradients from model axes to geographical (zonal and meridional) axes. Finally, we apply the lon_depth_along_lat function used above to look at the thermal wind balance across the Atlantic basin.
[10]:
# get xgcm Grid object for ECCO LLC90 grid
xgcm_grid = ecco.get_llc_grid(ds_grid)
[11]:
g = 9.81
# compute f from latitude of grid cell centers
lat = ds_grid.YC
Omega = (2*np.pi)/86164
lat_rad = (np.pi/180)*lat # convert latitude from degrees to radians
# expand dimensions so that f broadcasts correctly across DataArrays
f = np.expand_dims(2*Omega*np.sin(lat_rad),axis=(0,1))
# compute derivatives of density in x and y
d_rho_dx = (xgcm_grid.diff(dens,axis="X",boundary='extend'))/ds_grid.dxC
d_rho_dy = (xgcm_grid.diff(dens,axis="Y",boundary='extend'))/ds_grid.dyC
# interpolate (vector) gradient values to center of grid cells
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore") # use this to ignore future warnings caused by interp_2d_vector function
rho_grads_interp = xgcm_grid.interp_2d_vector({"X":d_rho_dx,"Y":d_rho_dy},boundary='extend')
d_rho_dx = rho_grads_interp['X']
d_rho_dy = rho_grads_interp['Y']
d_rho_dx.name = 'drho_dx'
d_rho_dy.name = 'drho_dy'
# rotate from model to geographical (lon-lat) axes
CS = np.expand_dims(ds_grid.CS,axis=(0,1))
SN = np.expand_dims(ds_grid.SN,axis=(0,1))
d_rho_zonal = (CS*d_rho_dx) - (SN*d_rho_dy)
d_rho_merid = (SN*d_rho_dx) + (CS*d_rho_dy)
# right-hand side of thermal wind balance equations
therm_wind_RHS_1 = -(g/(f*dens))*d_rho_zonal
therm_wind_RHS_2 = (g/(f*dens))*d_rho_merid
[12]:
# load velocity dataset
download_dir = join(download_root_dir,vel_monthly_shortname)
curr_vel_file = list(glob.glob(download_dir + '/*2000-01*.nc'))
ds_vel = xr.open_dataset(curr_vel_file[0])
# interpolate velocities to center of grid cells
ds_vel.UVEL.values[np.isnan(ds_vel.UVEL.values)] = 0
ds_vel.VVEL.values[np.isnan(ds_vel.VVEL.values)] = 0
with warnings.catch_warnings():
warnings.simplefilter("ignore")
vel_interp = xgcm_grid.interp_2d_vector({'X':ds_vel.UVEL,'Y':ds_vel.VVEL},boundary='extend')
u = vel_interp['X']
v = vel_interp['Y']
# vertical derivatives of velocity
# (negative sign after = because z increases upward, while k indices decrease upward)
du_dz_gridedge = -(u.diff("k").values)/np.expand_dims(ds_grid.drC[1:-1].values,axis=(0,2,3,4))
dv_dz_gridedge = -(v.diff("k").values)/np.expand_dims(ds_grid.drC[1:-1].values,axis=(0,2,3,4))
# vertical interpolation of du/dz, dv/dz
du_dz = np.empty(u.shape)
du_dz.fill(np.nan) # fill empty numpy array with NaNs
dv_dz = np.empty(v.shape)
dv_dz.fill(np.nan)
du_dz[:,1:-1,:,:,:] = du_dz_gridedge[:,1:,:,:,:] - (np.diff(du_dz_gridedge,axis=1)/2)
dv_dz[:,1:-1,:,:,:] = dv_dz_gridedge[:,1:,:,:,:] - (np.diff(dv_dz_gridedge,axis=1)/2)
# create xarray DataArrays
du_dz_array = xr.DataArray(
data=du_dz,
dims=["time","k","tile","j","i"],
coords=u.coords,
)
dv_dz_array = xr.DataArray(
data=dv_dz,
dims=["time","k","tile","j","i"],
coords=v.coords,
)
# rotate from model to geographical (lon-lat) axes
du_dz_zonal = (CS*du_dz_array) - (SN*dv_dz_array)
dv_dz_merid = (SN*du_dz_array) + (CS*dv_dz_array)
# left-hand side of thermal wind balance equations
therm_wind_LHS_1 = dv_dz_merid
therm_wind_LHS_2 = du_dz_zonal
[13]:
# generate DataArrays along line of latitude
lat_transect = 26.
Atl_W_bound = -82.
Atl_E_bound = -14.
lon_bnds = [Atl_W_bound,Atl_E_bound]
# use llc_grid_idx_along_lat and data_along_lat functions separately (runs faster)
idx_along_lat,XC_transect,XG_transect = llc_grid_idx_along_lat(lat_transect,lon_bnds)
mask_transect = data_along_lat(land_mask,idx_along_lat,XC_transect,XG_transect)
tw_LHS_1_transect = data_along_lat(therm_wind_LHS_1,idx_along_lat,XC_transect,XG_transect)
tw_LHS_2_transect = data_along_lat(therm_wind_LHS_2,idx_along_lat,XC_transect,XG_transect)
tw_RHS_1_transect = data_along_lat(therm_wind_RHS_1,idx_along_lat,XC_transect,XG_transect)
tw_RHS_2_transect = data_along_lat(therm_wind_RHS_2,idx_along_lat,XC_transect,XG_transect)
[14]:
# make figure
plt.rcParams["font.size"] = 12 # change font size
fig,axs = plt.subplots(4,2,figsize=(10,15))
depth_two_subplots(XG_transect,-ds_grid.Zl,\
tw_LHS_1_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[:2,0])
axs[0,0].set_title('dv/dz [s$^{-1}$] along 26 N latitude,\n in lon-lat axes')
depth_two_subplots(XG_transect,-ds_grid.Zl,\
tw_RHS_1_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[:2,1])
axs[0,1].set_title('$-(g/f\\rho)(d\\rho/dx)$ [s$^{-1}$] along 26 N latitude,\n in lon-lat axes')
plot_objs = [axs[0,0].get_children()[0],axs[1,0].get_children()[0],\
axs[0,1].get_children()[0],axs[1,1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.2)
depth_two_subplots(XG_transect,-ds_grid.Zl,\
tw_LHS_2_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[2:,0])
axs[2,0].set_title('du/dz [s$^{-1}$] along 26 N latitude')
axs[3,0].set_xlabel('Longitude')
depth_two_subplots(XG_transect,-ds_grid.Zl,\
tw_RHS_2_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[2:,1])
axs[2,1].set_title('$(g/f\\rho)(d\\rho/dy)$ [s$^{-1}$] along 26 N latitude')
axs[3,1].set_xlabel('Longitude')
plot_objs = [axs[2,0].get_children()[0],axs[3,0].get_children()[0],\
axs[2,1].get_children()[0],axs[3,1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.2)
# remove colorbars from left column plots (to declutter figure)
fig.get_children()[-4].remove()
fig.get_children()[-2].remove()
plt.show()
Note that the plots in the left and right columns are very similar at depths below ~100 meters, and very different in the top 100 meters. Why do you think this is?
Velocity reconstruction using thermal wind¶
In early studies of the atmosphere and ocean, measuring in-situ velocities accurately was nearly impossible, but properties such as temperature, humidity, and salinity (that are closely related to density) could be more readily observed. Even in present-day observing systems such as atmospheric radiosondes and oceanic Argo floats, observing the density structure can tell us a lot about the movement of the fluid, because of the application of thermal wind balance.
Given the thermal wind balance equations introduced earlier, integrating vertically between two depth levels and gives us
Since velocities in the deep ocean tend to be much smaller than those near the surface, a common technique used in oceanography has been to assume a level of no motion, i.e., let . Then upper ocean geostrophic velocities can be inferred from vertical profiles of density, spaced at regular intervals. Here we can assess the accuracy of this technique using ECCO v4r4 output.
Assessment¶
Thermal wind-based velocity reconstruction¶
Let’s integrate geostrophic velocity from a depth level where we assume the velocity is zero (even though we know from ECCO output that the velocity will not be exactly zero). In this example we’ll take the level of no motion to be 3000 meters depth. Then we consider the result of this velocity reconstruction along the Atlantic 26 N transect we just considered.
[15]:
# set level of no motion
z_0 = -3000.
# integrate (sum) RHS of thermal wind balance upward
dist_z0_upper = ds_grid.Zl - z_0
dist_z0_upper[ds_grid.Zl < z_0] = 0
delta_upper = (-dist_z0_upper.diff("k_l")).values
delta_upper = np.concatenate((delta_upper,np.array([0,])))
delta_upper = np.expand_dims(delta_upper,axis=(0,2,3,4))
v_g_reconstr_upward = (((delta_upper*therm_wind_RHS_1)\
.sel(k=slice(None,None,-1)))\
.cumsum(dim="k",skipna=True))\
.sel(k=slice(None,None,-1)) # slice object "flips" array along k dimension
u_g_reconstr_upward = (((delta_upper*therm_wind_RHS_2)\
.sel(k=slice(None,None,-1)))\
.cumsum(dim="k",skipna=True))\
.sel(k=slice(None,None,-1)) # slice object "flips" array along k dimension
# interpolate DataArrays to center of tracer cells
v_g_reconstr_upward_interp = v_g_reconstr_upward
v_g_reconstr_upward_interp.isel(k=np.arange(0,len(ds_grid.k) - 1)).values = \
v_g_reconstr_upward.isel(k=np.arange(0,len(ds_grid.k) - 1))\
+ ((v_g_reconstr_upward.diff("k").values)/2)
u_g_reconstr_upward_interp = u_g_reconstr_upward
u_g_reconstr_upward_interp.isel(k=np.arange(0,len(ds_grid.k) - 1)).values = \
u_g_reconstr_upward.isel(k=np.arange(0,len(ds_grid.k) - 1))\
+ ((u_g_reconstr_upward.diff("k").values)/2)
# integrate (sum) RHS of thermal wind balance downward
dist_z0_lower = ds_grid.Zu - z_0
dist_z0_lower[ds_grid.Zu > z_0] = 0
delta_lower = (-dist_z0_lower.diff("k_u")).values
delta_lower = np.concatenate((np.array([0,]),delta_lower))
delta_lower = np.expand_dims(delta_lower,axis=(0,2,3,4))
v_g_reconstr_downward = (delta_lower*therm_wind_RHS_1)\
.cumsum(dim="k",skipna=True)
u_g_reconstr_downward = (delta_lower*therm_wind_RHS_2)\
.cumsum(dim="k",skipna=True)
v_g_reconstr_downward_interp = v_g_reconstr_downward.isel(k=np.arange(0,len(ds_grid.k) - 1))\
+ ((v_g_reconstr_downward.diff("k").values)/2)
v_g_reconstr_downward_interp = v_g_reconstr_downward_interp.pad(k=(1,0),constant_value=0)
u_g_reconstr_downward_interp = u_g_reconstr_downward.isel(k=np.arange(0,len(ds_grid.k) - 1))\
+ ((u_g_reconstr_downward.diff("k").values)/2)
u_g_reconstr_downward_interp = u_g_reconstr_downward_interp.pad(k=(1,0),constant_value=0)
# combine results from upward and downward integration
v_g_reconstr = v_g_reconstr_upward_interp
v_g_reconstr.values[:,v_g_reconstr.Z.values < z_0,:,:,:] = \
v_g_reconstr_downward_interp.values[:,v_g_reconstr.Z.values < z_0,:,:,:]
u_g_reconstr = u_g_reconstr_upward_interp
u_g_reconstr.values[:,u_g_reconstr.Z.values < z_0,:,:,:] = \
u_g_reconstr_downward_interp.values[:,u_g_reconstr.Z.values < z_0,:,:,:]
# rotate (actual) velocities from model to geographical axes
u_zonal = (CS*u) - (SN*v)
v_merid = (SN*u) + (CS*v)
[16]:
# generate DataArrays along line of latitude
lat_transect = 26.
Atl_W_bound = -82.
Atl_E_bound = -14.
lon_bnds = [Atl_W_bound,Atl_E_bound]
# # only need this line uncommented if new transect is specified above
# idx_along_lat,XC_transect,XG_transect = llc_grid_idx_along_lat(lat_transect,lon_bnds)
mask_transect = data_along_lat(land_mask,idx_along_lat,XC_transect,XG_transect)
u_zonal_transect = data_along_lat(u_zonal,idx_along_lat,XC_transect,XG_transect)
v_merid_transect = data_along_lat(v_merid,idx_along_lat,XC_transect,XG_transect)
u_g_reconstr_transect = data_along_lat(u_g_reconstr,idx_along_lat,XC_transect,XG_transect)
v_g_reconstr_transect = data_along_lat(v_g_reconstr,idx_along_lat,XC_transect,XG_transect)
# make figure
plt.rcParams["font.size"] = 12 # change font size
fig,axs = plt.subplots(4,2,figsize=(10,15))
depth_two_subplots(XG_transect,-ds_grid.Zl,\
v_merid_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[:2,0])
axs[0,0].set_title('Meridional v [m s$^{-1}$]\n along 26 N latitude')
depth_two_subplots(XG_transect,-ds_grid.Zl,\
v_g_reconstr_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[:2,1])
axs[0,1].set_title('v reconstruction [m s$^{-1}$]\n from z_0 = ' + str(int(-z_0)) + ' meters depth')
plot_objs = [axs[0,0].get_children()[0],axs[1,0].get_children()[0],\
axs[0,1].get_children()[0],axs[1,1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.5)
depth_two_subplots(XG_transect,-ds_grid.Zl,\
u_zonal_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[2:,0])
axs[2,0].set_title('Zonal u [m s$^{-1}$]')
axs[3,0].set_xlabel('Longitude')
depth_two_subplots(XG_transect,-ds_grid.Zl,\
u_g_reconstr_transect.squeeze(),\
k_split=25,cmap='seismic',mask=mask_transect,\
fig=fig,axs=axs[2:,1])
axs[2,1].set_title('u reconstruction [m s$^{-1}$]')
axs[3,1].set_xlabel('Longitude')
plot_objs = [axs[2,0].get_children()[0],axs[3,0].get_children()[0],\
axs[2,1].get_children()[0],axs[3,1].get_children()[0]]
cmap_zerocent_scale_multiplots(plot_objs,0.5)
# remove colorbars from left column plots (to declutter figure)
fig.get_children()[-4].remove()
fig.get_children()[-2].remove()
plt.show()
Latitude and depth dependence¶
Now as we did with geostrophic balance in the previous tutorial, we can compute the normalized difference over the global domain, sorted into bins by latitude and depth. Note: the ``mean_weighted_binned`` function may take a few minutes to run over the global domain.
[17]:
u_diff = u_zonal - u_g_reconstr
v_diff = v_merid - v_g_reconstr
vel_diff_complex = u_diff + (1j*v_diff) # in Python, imaginary number i is indicated by 1j
vel_complex = u_zonal + (1j*v_merid)
# normalize magnitude of difference vector by magnitude of actual velocity
vel_diff_abs = np.abs(vel_diff_complex)
vel_abs = np.abs(vel_complex)
vel_diff_norm = vel_diff_abs/vel_abs
# bins of latitude
lat_bin_spacing = 2
lat_bin_bounds = np.c_[-90:90:lat_bin_spacing] + np.array([[0,lat_bin_spacing]])
lat_bin_centers = np.mean(lat_bin_bounds,axis=-1)
# bins of depth
depth_bin_bounds = -ds_grid.Z_bnds.values
# apply "small velocity" mask as in geostrophic balance tutorial
mask_threshold = 0.005 # mask out velocities <0.5 cm s-1
mask_smallvel = (vel_abs < mask_threshold)
curr_weighting = ((~mask_smallvel)*(ds_grid.maskC)*(ds_grid.rA)).values
# create depth array that will broadcast correctly across horizontal dimensions
depth_expand_dims = np.expand_dims(-ds_grid.Z,axis=(-3,-2,-1))
# compute normalized differences
diff_norm_lat = mean_weighted_binned(vel_diff_norm.values,curr_weighting,\
ds_grid.YC.values,lat_bin_bounds)
diff_norm_depth = mean_weighted_binned(vel_diff_norm.values,curr_weighting,\
depth_expand_dims,depth_bin_bounds)
# apply additional masks for depth (100-1000 m) and latitude (>5 deg from equator) ranges
depth_mask = np.logical_and(depth_expand_dims > 100,\
depth_expand_dims < 1000)
lat_mask = np.expand_dims((np.abs(ds_grid.YC) > 5),axis=0)
# compute norm diff with additional masks
curr_weighting = (depth_mask*(~mask_smallvel)*(ds_grid.maskC)*(ds_grid.rA)).values
diff_norm_lat_moremask = mean_weighted_binned(vel_diff_norm.values,curr_weighting,\
ds_grid.YC.values,lat_bin_bounds)
curr_weighting = (lat_mask*(~mask_smallvel)*(ds_grid.maskC)*(ds_grid.rA)).values
diff_norm_depth_moremask = mean_weighted_binned(vel_diff_norm.values,curr_weighting,\
depth_expand_dims,depth_bin_bounds)
[18]:
# load norm diff from previous geostrophic balance tutorial
# (if you don't have this file run that tutorial,
# or else comment out the lines containing geostr_file below)
geostr_file = np.load('diff_norm_geostr_bal.npz')
# plot normalized RMS difference, alongside values for geostrophic balance
fig,axs = plt.subplots(2,2,figsize=(15,14))
curr_ax = axs[0,0]
curr_ax.plot(lat_bin_centers,geostr_file['diff_norm_lat'],color='blue',label='Geostrophy')
curr_ax.plot(lat_bin_centers,diff_norm_lat,color='red',label='Thermal wind')
curr_ax.set_ylim(0,1)
curr_ax.axhline(y=0.1,color='black',lw=2)
curr_ax.axvline(x=0,color='black',lw=1)
curr_ax.set_xlabel('Latitude')
curr_ax.set_ylabel('Normalized diff')
curr_ax.set_title('Normalized diff variation with latitude\n z_0 = ' + str(int(-z_0)) + ' meters depth')
curr_ax.legend()
curr_ax = axs[0,1]
curr_ax.plot(geostr_file['diff_norm_depth'],-ds_grid.Z,color='blue',label='Geostrophy')
curr_ax.plot(diff_norm_depth,-ds_grid.Z,color='red',label='Thermal wind')
# Note: in older versions of Matplotlib
# may need to use linthreshy instead of linthresh
curr_ax.set_yscale('symlog',linthresh=100)
curr_ax.invert_yaxis()
curr_ax.axvline(x=0.1,color='black',lw=2)
curr_ax.axhline(y=-z_0,color='black',lw=1)
curr_ax.text(0.02,-0.92*z_0,'z_0')
curr_ax.set_yticks([0,50,100,500,1000,5000])
curr_ax.set_yticklabels(['0','50','100','500','1000','5000'])
curr_ax.set_xlim(0,1)
curr_ax.set_xlabel('Normalized diff')
curr_ax.set_ylabel('Depth [m]')
curr_ax.set_title('Normalized diff variation with depth\n z_0 = ' + str(int(-z_0)) + ' meters depth')
curr_ax.legend()
curr_ax = axs[1,0]
curr_ax.plot(lat_bin_centers,geostr_file['diff_norm_lat_moremask'],color='blue',label='Geostrophy')
curr_ax.plot(lat_bin_centers,diff_norm_lat_moremask,color='red',label='Thermal wind')
curr_ax.set_ylim(0,1)
curr_ax.axhline(y=0.1,color='black',lw=2)
curr_ax.axvline(x=0,color='black',lw=1)
curr_ax.set_xlabel('Latitude')
curr_ax.set_ylabel('Normalized diff')
curr_ax.set_title('In 100-1000 m depth range only')
curr_ax.legend()
curr_ax = axs[1,1]
curr_ax.plot(geostr_file['diff_norm_depth_moremask'],-ds_grid.Z,color='blue',label='Geostrophy')
curr_ax.plot(diff_norm_depth_moremask,-ds_grid.Z,color='red',label='Thermal wind')
# Note: in older versions of Matplotlib
# may need to use linthreshy instead of linthresh
curr_ax.set_yscale('symlog',linthresh=100)
curr_ax.invert_yaxis()
curr_ax.axvline(x=0.1,color='black',lw=2)
curr_ax.axhline(y=-z_0,color='black',lw=1)
curr_ax.text(0.02,-0.92*z_0,'z_0')
curr_ax.set_yticks([0,50,100,500,1000,5000])
curr_ax.set_yticklabels(['0','50','100','500','1000','5000'])
curr_ax.set_xlim(0,1)
curr_ax.set_xlabel('Normalized diff')
curr_ax.set_ylabel('Depth [m]')
curr_ax.set_title('>5 degrees from equator only')
curr_ax.legend()
plt.show()
Comparing the geostrophy and thermal wind normalized differences provides some insight into the challenges of reconstructing velocity fields using thermal wind. Geostrophic balance is a more accurate predictor of velocity than thermal wind, since thermal wind assumes:
geostrophic balance
hydrostatic balance (not a factor here since hydrostatic balance is actually assumed in the model code itself)
a level of no motion (a potentially more problematic assumption, especially close to ).
The plots above also illustrate why satellite observations of sea surface height are so valuable to oceanographers. Prior to reliable measurements of sea surface height, ocean velocity fields were typically reconstructed using density profiles and thermal wind. When sea surface height data are combined with density profiles, we can compute subsurface pressure gradients, and then obtain velocity estimates using only geostrophy, which is much more accurate than having to rely on thermal wind.
Exercises¶
Try out different values of (i.e., level of no motion) to use in the velocity reconstructions. How does this affect the normalized differences in the plots above? Which values permit the most accurate reconstructions (lowest normalized difference) at 100 meters depth? (For bonus points, write some code to determine the optimal value that minimizes normalized difference at 100 m depth, rather than using trial and error.)
Adapt the
lon_depth_along_lat
function (and the two functions it calls:llc_grid_idx_along_lat
anddata_along_lat
) to plot transects following lines of longitude instead of latitude. Use this to look at the application of thermal wind to reconstruct velocities through Drake Passage (~64 deg W, 55-65 deg S).
References¶
Gill, A.E. (1982). Atmosphere-Ocean Dynamics. Academic Press, Elsevier.
Kundu, P.K. and Cohen, I.M. (2008). Fluid Mechanics (4th ed.). Elsevier.
Vallis, G.K. (2006). Atmospheric and Oceanic Fluid Dynamics. Cambridge University Press.