Skip to content

Commit 30b1faa

Browse files
committed
added update to documentation for tide checker
1 parent e243f7d commit 30b1faa

File tree

6 files changed

+101
-34
lines changed

6 files changed

+101
-34
lines changed
91.3 KB
Loading

docs/source/tides.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,60 @@ not been tested with the global tide models.
4040
Other options include "ln_tide" a boolean that when set to true will generate tidal boundaries. "sn_tide_model" is a string that defines the model to use, currently only
4141
"fes" or "tpxo" are supported. "ln_trans" is a boolean that when set to true will interpolate transport rather than velocities.
4242

43+
Harmonic Output Checker
44+
-----------------------
45+
46+
There is an harmonic output checker that can be utilised to check the output of PyNEMO with a reference tide model. So far
47+
the only supported reference model is FES but TPXO will be added in the future. Any tidal output from PyNEMO can be checked
48+
(e.g. FES and TPXO). While using the same model used as input to check output doesn't improve accuracy, it does confirm that the
49+
output is within acceptable/expected limits of the nearest model reference point.
50+
51+
There are differences as PyNEMO interpolates the harmonics and the tidal checker does not, so there can be some difference
52+
in the values particularly close to coastlines.
53+
54+
The checker can be enabled by editing the following in the relevent bdy file::
55+
56+
ln_tide_checker = .true. ! run tide checker on PyNEMO tide output
57+
sn_ref_model = 'fes' ! which model to check output against (FES only)
58+
59+
The boolean determines if to run the checker or not, this takes place after creating the interpolated harmonics
60+
and writing them to disk. The string denotes which tide model to use as reference, so far only FES is supported.
61+
The string denoting model is not strictly needed, by default fes is used.
62+
63+
The checker will output information regarding the checking to the NRCT log, and also write an spreadsheet to the output folder containing any
64+
exceedance values, the closest reference model value and their locations. Amplitude and phase are checked independently, so both have latitude and longitude
65+
associated with them. It is also useful to know the amplitude of a exceeded phase to see how much impact it will have so this
66+
is also written to the spreadsheet. An example output is shown below, as can be seen the majority of the amplitudes, both
67+
the two amplitudes exceedances and the ones associated with the phase exceedances are low (~0.01), so can most likely be ignored.
68+
There a few phase exceedances that have higher amplitudes (~0.2) which would potentially require further investigation. A common
69+
reason for such an exceedance is due to coastlines and the relevant point being further away from an FES data point.
70+
71+
Tide Checker Example Output for M2 U currents
72+
---------------------------------------------
73+
74+
.. figure:: _static/comparision_fes.png
75+
:align: center
76+
77+
The actual thresholds for both amplitude and phase are based on the amplitude of the output or reference, this is due to
78+
different tolerances based on the amplitude. e.g. high amplitudes should have lower percentage differences to the FES reference,
79+
than lower ones simply due to the absolute amount of the ampltiude itself, e.g. a 0.1 m difference for a 1.0 m amplitude is
80+
acceptable but not for a 0.01 m amplitude. The smaller amplitudes contribute less to the overall tide height so larger percentage
81+
differences are acceptable. The same also applies to phases, where large amplitude phases have little room for differences but at
82+
lower amplitudes this is less critical so a higher threshold is tolerated.
83+
84+
The following power functions are used to determine what threshold to apply based on the reference model amplitude.
85+
86+
Amplitude Threshold
87+
-------------------
88+
89+
.. important:: Percentage Exceedance = 26.933 * Reference Amplitude ^ -0.396'
90+
91+
Phases Threshold
92+
----------------
93+
94+
.. important:: Phase Exceedance = 5.052 * PyNEMO Amplitude ^ -0.60
95+
96+
4397
Future work
4498
-----------
4599

inputs/namelist_cmems.bdy

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@
107107
sn_tide_model = 'fes' ! Name of tidal model (fes|tpxo)
108108
clname(1) = 'M2' ! constituent name
109109
clname(2) = 'S2'
110-
clname(3) = 'N2'
111-
clname(4) = 'O1'
112-
clname(5) = 'K1'
110+
!clname(3) = 'N2'
111+
!clname(4) = 'O1'
112+
!clname(5) = 'K1'
113113
!clname(6) = 'K2'
114114
!clname(7) = 'L2'
115115
!clname(8) = 'NU2'
@@ -123,7 +123,6 @@
123123
ln_trans = .false. ! interpolate transport rather than velocities
124124
ln_tide_checker = .true. ! run tide checker on PyNEMO tide output
125125
sn_ref_model = 'fes' ! which model to check output against (FES only)
126-
nn_amp_thres = 0.10 ! amplitude thresold to compare against (m)
127126
!------------------------------------------------------------------------------
128127
! Time information
129128
!------------------------------------------------------------------------------

pynemo/profile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ def process_bdy(setup_filepath=0, mask_gui=False):
443443

444444
if settings['tide_checker'] == True:
445445
logger.info('tide checker starting now.....')
446-
tt_test = tt.main(setup_filepath,settings['amp_thres'],settings['ref_model'])
446+
tt_test = tt.main(setup_filepath,settings['ref_model'])
447447
if tt_test == 0:
448448
logger.info('tide checker ran successfully, check spreadsheet in output folder')
449449
if tt_test !=0:
@@ -457,7 +457,7 @@ def process_bdy(setup_filepath=0, mask_gui=False):
457457

458458
if settings['tide_checker'] == True:
459459
logger.info('tide checker starting now.....')
460-
tt_test = tt.main(setup_filepath,settings['amp_thres'],settings['ref_model'])
460+
tt_test = tt.main(setup_filepath,settings['ref_model'])
461461
if tt_test == 0:
462462
logger.info('tide checker ran successfully, check spreadsheet in output folder')
463463
if tt_test !=0:

pynemo/tests/nemo_tide_test.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@
3232
a separate sheet in the spreadsheet. The name of the spreadsheet contains meta data showing thresholds and reference
3333
model used. Units for threshold are meters and degrees.
3434
35+
Update: fill values for FES are commonly returned at coastlines, this is due to the nearest FES cell being land but PyNEMO
36+
will have interpolated data from the water. In instance the code checks the cells aroud the fill value and averages both
37+
amplitude and phase (using HsinG,HcosG) to act as a reference.
38+
39+
Phase threshold is not longer required as it is applied using an function that references amplitude, the idea is that the
40+
threshold is low for high amplitudes, e.g. 5 degrees for 1.0m and high for low amplitudes 80 degrees for 0.01 m.
41+
42+
Amplitudes at phase exceedance locataions are also returned to allow assessment of the impact, e.g. low amplitude low impact
43+
3544
"""
3645
from netCDF4 import Dataset
3746
import numpy as np
@@ -47,7 +56,7 @@
4756

4857
# TODO: add TPXO read and subset functionality currently only uses FES as "truth"
4958

50-
def main(bdy_file='inputs/namelist_cmems.bdy',amplitude_threshold = 0.1,model='fes',model_res=1/16):
59+
def main(bdy_file='inputs/namelist_cmems.bdy',model='fes'):
5160
logger.info('============================================')
5261
logger.info('Start Tide Test Logging: ' + time.asctime())
5362
logger.info('============================================')
@@ -60,7 +69,7 @@ def main(bdy_file='inputs/namelist_cmems.bdy',amplitude_threshold = 0.1,model='f
6069
if model == 'fes':
6170
logger.info('using FES as reference.......')
6271
# open writer object to write pandas dataframes to spreadsheet
63-
writer = pd.ExcelWriter(settings['dst_dir'] + 'exceed_values_amp_thres-'+str(amplitude_threshold)+'_reference_model-'+str(model)+'.xlsx', engine='xlsxwriter')
72+
writer = pd.ExcelWriter(settings['dst_dir'] + 'comparision_with_'+str(model)+'.xlsx', engine='xlsxwriter')
6473
for key in constituents:
6574
for j in range(len(grids)):
6675
out_fname = settings['dst_dir']+settings['fn']+'_bdytide_'+constituents[key].strip("',/\n")+'_grd_'+grids[j]+'.nc'
@@ -74,7 +83,7 @@ def main(bdy_file='inputs/namelist_cmems.bdy',amplitude_threshold = 0.1,model='f
7483
# subset FES to match PyNEMO list of lat lons
7584
subset_fes = subset_reference(pynemo_out, fes)
7685
# compare the two lists (or dicts really)
77-
error_log = compare_tides(pynemo_out, subset_fes, amplitude_threshold, model_res)
86+
error_log = compare_tides(pynemo_out, subset_fes)
7887
# return differences above threshold as a Pandas Dataframe and name using HC and Grid
7988
error_log.name = constituents[key].strip("',/\n") + grids[j]
8089
# if the dataframe is empty (no exceedances) then discard dataframe and log the good news
@@ -150,6 +159,7 @@ def read_fes(fes_fname,grid):
150159
# subset FES dict from read_FES, this uses find_nearest to find nearest FES point using PyNEMO dict from extract_PyNEMO
151160
# It also converts FES amplitude from cm to m.
152161
def subset_reference(pynemo_out, reference):
162+
model_res = np.abs(reference['lon'][0]-reference['lon'][1])
153163
idx_lat = np.zeros(np.shape(pynemo_out['lat']))
154164
for i in range(np.shape(pynemo_out['lat'])[1]):
155165
idx_lat[0, i] = find_nearest(reference['lat'], pynemo_out['lat'][0, i])
@@ -167,7 +177,7 @@ def subset_reference(pynemo_out, reference):
167177
for i in range(np.shape(amp_sub)[1]):
168178
# if a fill value in FES subset is found
169179
if amp_sub[0, i] == 184467436613926912.0000:
170-
logger.warning('found fill value in FES subset, taking nanmean from surrounding points')
180+
logger.warning('found fill value in FES subset, taking nanmean from surrounding amplitude points')
171181
# if there are fill values surrounding subset fill value change these to NaN
172182
if reference['amp'][idx_lat[0,i]+1, idx_lon[0,i]]== 184467436613926912.0000:
173183
reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]] = np.nan
@@ -187,7 +197,7 @@ def subset_reference(pynemo_out, reference):
187197
reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]-1] = np.nan
188198
# nan mean surrounding points to replace fill value subset point
189199
amp_sub[0,i] = np.nanmean([reference['amp'][idx_lat[0,i]+1, idx_lon[0,i]], \
190-
reference['amp'][idx_lat[0,i], idx_lon[0,i]+1], \
200+
reference['amp'][idx_lat[0,i], idx_lon[0,i]+1], \
191201
reference['amp'][idx_lat[0,i]-1, idx_lon[0,i]], \
192202
reference['amp'][idx_lat[0,i], idx_lon[0,i]-1], \
193203
reference['amp'][idx_lat[0,i]+1, idx_lon[0,i]]+1, \
@@ -197,8 +207,10 @@ def subset_reference(pynemo_out, reference):
197207
])
198208
phase_sub = reference['phase'][idx_lat, idx_lon]
199209
for i in range(np.shape(phase_sub)[1]):
210+
# if a fill value in FES subset is found
200211
if phase_sub[0, i] == 18446744073709551616.0000:
201-
logger.warning('found fill value in FES subset, taking nanmean value from surrounding points')
212+
logger.warning('found fill value in FES subset, taking nanmean from surrounding phase points')
213+
# if there are fill values surrounding subset fill value change these to NaN
202214
if reference['phase'][idx_lat[0, i] + 1, idx_lon[0, i]] == 18446744073709551616.0000:
203215
reference['phase'][idx_lat[0, i] + 1, idx_lon[0, i]] = np.nan
204216
if reference['phase'][idx_lat[0, i], idx_lon[0, i] + 1] == 18446744073709551616.0000:
@@ -215,7 +227,7 @@ def subset_reference(pynemo_out, reference):
215227
reference['phase'][idx_lat[0, i] - 1, idx_lon[0, i] + 1] = np.nan
216228
if reference['phase'][idx_lat[0, i] + 1, idx_lon[0, i] - 1] == 18446744073709551616.0000:
217229
reference['phase'][idx_lat[0, i] + 1, idx_lon[0, i] - 1] = np.nan
218-
230+
# calculate HcosG and then average
219231
HcosG = np.nanmean([reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]]*np.cos(
220232
reference['phase'][idx_lat[0, i]+1, idx_lon[0, i]]*np.pi/180),
221233
reference['amp'][idx_lat[0, i], idx_lon[0, i]+1] * np.cos(
@@ -233,7 +245,7 @@ def subset_reference(pynemo_out, reference):
233245
reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]-1] * np.cos(
234246
reference['phase'][idx_lat[0, i]+1, idx_lon[0, i]-1] * np.pi / 180),
235247
])
236-
248+
# calculate HsinG and then average
237249
HsinG = np.nanmean([reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]]*np.sin(
238250
reference['phase'][idx_lat[0, i]+1, idx_lon[0, i]]*np.pi/180),
239251
reference['amp'][idx_lat[0, i], idx_lon[0, i]+1] * np.sin(
@@ -251,33 +263,39 @@ def subset_reference(pynemo_out, reference):
251263
reference['amp'][idx_lat[0, i]+1, idx_lon[0, i]-1] * np.sin(
252264
reference['phase'][idx_lat[0, i]+1, idx_lon[0, i]-1] * np.pi / 180),
253265
])
254-
266+
# convert back to phase
255267
phase_sub[0,i] = np.arctan2(HsinG,HcosG)
256268

257269
lat_sub = reference['lat'][idx_lat]
258270
lon_sub = reference['lon'][idx_lon]
259-
subset = {'lat':lat_sub,'lon':lon_sub,'amp':amp_sub,'phase':phase_sub}
271+
subset = {'lat':lat_sub,'lon':lon_sub,'amp':amp_sub,'phase':phase_sub,'model_res':model_res}
260272
return subset
261273

262274
# takes pynemo extract dict, subset fes dict, and the thresholds and model res passed to main function.
263275
# returns a Pandas Dataframe with any PyNEMO values that exceed the nearest FES point by defined threshold
264276
# It also checks lats and lons are within the model reference resolution
265277
# i.e. ensure closest model reference point is used.
266-
def compare_tides(pynemo_out,subset,amp_thres,model_res):
278+
def compare_tides(pynemo_out,subset):
267279
# compare lat and lons
268280
diff_lat = np.abs(pynemo_out['lat']-subset['lat'])
269281
diff_lon = np.abs(pynemo_out['lon'] - subset['lon'])
270-
exceed_lat = diff_lat > model_res
271-
exceed_lon = diff_lon > model_res
282+
exceed_lat = diff_lat > subset['model_res']
283+
exceed_lon = diff_lon > subset['model_res']
272284
exceed_sum = np.sum(exceed_lat+exceed_lon)
273285
if exceed_sum > 0:
274286
raise Exception('Dont Panic: Lat and/or Lon further away from model point than model resolution')
275287
# surpress warnings as NaNs from averaging surrounding pixels can cause issues
276288
with warnings.catch_warnings():
277289
warnings.simplefilter("ignore", category=RuntimeWarning)
278290
# compare amp
279-
abs_amp = np.abs(pynemo_out['amp']-subset['amp'])
280-
abs_amp_thres = abs_amp > amp_thres
291+
abs_amp_diff = np.abs(pynemo_out['amp']-subset['amp'])
292+
# calculate threshold in percentage terms
293+
logger.info('percentage amplitude exceedance calculated using the following.....')
294+
amp_percentage_exceed = 26.933 * subset['amp'] ** -0.396
295+
logger.info('Percentage Exceedance = 26.933 * Reference Amplitude ^ -0.396')
296+
# work out difference based on percentage and reference amplitude
297+
percent_diff = (abs_amp_diff / pynemo_out['amp']) * 100
298+
abs_amp_thres = percent_diff > amp_percentage_exceed
281299
err_amp = pynemo_out['amp'][abs_amp_thres].tolist()
282300
err_amp_lats = pynemo_out['lat'][abs_amp_thres].tolist()
283301
err_amp_lons = pynemo_out['lon'][abs_amp_thres].tolist()
@@ -297,7 +315,7 @@ def compare_tides(pynemo_out,subset,amp_thres,model_res):
297315
abs_ph[abs_ph < 0.0 ] = abs_ph[abs_ph < 0.0] *-1
298316
# calculate phase threshold based on amplitude and power relationship
299317
# as amplitude decreases the phase exceedance allowed increases.
300-
logger.info('phase exccedance calculates using the following.....')
318+
logger.info('phase exceedance calculated using the following.....')
301319
phase_thres = 5.052 * pynemo_out['amp'] ** -0.60
302320
logger.info('Exceedance = 5.052 * Amplitude ^ -0.60')
303321
abs_ph_thres = abs_ph > phase_thres

pynemo/tide/fes_extract_HC.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def __init__(self, settings, lat, lon, grid_type):
4848
self.mask_dataset = {}
4949

5050
# extract lon and lat z data
51-
lon_z = np.array(Dataset(settings['tide_fes']+constituents[0]+'_Z.nc').variables['lon'])
52-
lat_z = np.array(Dataset(settings['tide_fes']+constituents[0]+'_Z.nc').variables['lat'])
51+
lon_z = np.array(Dataset(settings['tide_fes']+constituents[i]+'_Z.nc').variables['lon'])
52+
lat_z = np.array(Dataset(settings['tide_fes']+constituents[i]+'_Z.nc').variables['lat'])
5353
lon_resolution = lon_z[1] - lon_z[0]
5454
data_in_km = 0 # added to maintain the reference to matlab tmd code
5555

@@ -63,8 +63,8 @@ def __init__(self, settings, lat, lon, grid_type):
6363
#read and convert the height_dataset file to complex and store in dicts
6464
hRe = []
6565
hIm = []
66-
lat_z = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_Z.nc').variables['lat'][:])
67-
lon_z = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_Z.nc').variables['lon'][:])
66+
lat_z = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_Z.nc').variables['lat'][:])
67+
lon_z = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_Z.nc').variables['lon'][:])
6868
for ncon in range(len(constituents)):
6969
amp = np.ma.MaskedArray.filled(np.flipud(np.rot90(Dataset(settings['tide_fes']+str(constituents[ncon])+'_Z.nc').variables['amplitude'][:])))
7070
# set fill values to zero
@@ -90,14 +90,12 @@ def __init__(self, settings, lat, lon, grid_type):
9090

9191
URe = []
9292
UIm = []
93-
lat_u = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_U.nc').variables['lat'][:])
94-
lon_u = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_U.nc').variables['lon'][:])
93+
lat_u = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_U.nc').variables['lat'][:])
94+
lon_u = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_U.nc').variables['lon'][:])
9595
for ncon in range(len(constituents)):
9696
amp = np.ma.MaskedArray.filled(np.flipud(np.rot90(Dataset(settings['tide_fes']+constituents[ncon]+'_U.nc').variables['Ua'][:])))
9797
# set fill values to zero
9898
amp[amp == 18446744073709551616.00000] = 0
99-
# convert amp units to m/s
100-
amp = amp/100.00
10199
phase = np.ma.MaskedArray.filled(np.flipud(np.rot90(Dataset(settings['tide_fes']+constituents[ncon]+'_U.nc').variables['Ug'][:])))
102100
phase[phase == 18446744073709551616.00000] = 0
103101
URe.append(amp*np.cos(phase*(np.pi/180)))
@@ -114,14 +112,12 @@ def __init__(self, settings, lat, lon, grid_type):
114112

115113
VRe = []
116114
VIm = []
117-
lat_v = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_V.nc').variables['lat'][:])
118-
lon_v = np.array(Dataset(settings['tide_fes'] + constituents[0] + '_V.nc').variables['lon'][:])
115+
lat_v = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_V.nc').variables['lat'][:])
116+
lon_v = np.array(Dataset(settings['tide_fes'] + constituents[i] + '_V.nc').variables['lon'][:])
119117
for ncon in range(len(constituents)):
120118
amp = np.ma.MaskedArray.filled(np.flipud(np.rot90(Dataset(settings['tide_fes']+constituents[ncon]+'_V.nc').variables['Va'][:])))
121119
# set fill value to zero
122120
amp[amp == 18446744073709551616.00000] = 0
123-
# convert amp units to m/s
124-
amp = amp/100.00
125121
phase = np.ma.MaskedArray.filled(np.flipud(np.rot90(Dataset(settings['tide_fes']+constituents[ncon]+'_V.nc').variables['Vg'][:])))
126122
phase[phase == 18446744073709551616.00000] = 0
127123
VRe.append(amp*np.cos(phase*(np.pi/180)))

0 commit comments

Comments
 (0)