diff --git a/doc/config.rst b/doc/config.rst index 869c4815..57e1c5ec 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -192,9 +192,14 @@ Onset Hobo - ``names``: option for user specified column names (only recommended when code will not read names using automated/default method) Lowell TCM Hobo ----------- +--------------- - All the _min, _max, _bad_ens, etc. options available to the EXO. - ``skipfooter``: number of lines to skip in the CSV file at the end of the file - ``ncols``: number of columns of data to read, starting at first - ``names``: option for user specified column names (only recommended when code will not read names using automated/default method) + +Vector +------ +- ``pressure_sensor_height`` and ``velocity_sample_volume_height`` to specify the elevations of these two sensors. +- ``puv``: set to ``true`` to compute PUV wave statistics. **(EXPERIMENTAL)** diff --git a/doc/vec.rst b/doc/vec.rst index 46b72c3f..1cd8c2a0 100644 --- a/doc/vec.rst +++ b/doc/vec.rst @@ -29,3 +29,17 @@ runveccdf2nc.py .. argparse:: :ref: stglib.core.cmd.veccdf2nc_parser :prog: runveccdf2nc.py + +Compute wave statistics +======================= + +Use stglib's built-in wave-statistics toolbox to compute a wave-statistics file. + +**Experimental** PUV support is also present. Testing welcome! + +runvecnc2waves.py +----------------- + +.. argparse:: + :ref: stglib.core.cmd.vecnc2waves_parser + :prog: runvecnc2waves.py diff --git a/doc/wvs.rst b/doc/wvs.rst index 489e607f..88dfd594 100644 --- a/doc/wvs.rst +++ b/doc/wvs.rst @@ -31,12 +31,12 @@ runwvscdf2nc.py :ref: stglib.core.cmd.wvscdf2nc_parser :prog: runwvscdf2nc.py -Compute waves statistics -======================== +Compute wave statistics +======================= Using DIWASP: This is run separately in Matlab using the DIWASP toolbox (docs TODO). -Use stglib's built-in waves statistics toolbox to compute a wave statistics file. +Use stglib's built-in wave-statistics toolbox to compute a wave-statistics file. runwvsnc2waves.py ----------------- diff --git a/setup.py b/setup.py index 09d560fa..738e70cf 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ "runeofecdf2nc.py=stglib.core.runcmd:runeofecdf2nc", "runvecdat2cdf.py=stglib.core.runcmd:runvecdat2cdf", "runveccdf2nc.py=stglib.core.runcmd:runveccdf2nc", + "runvecnc2waves.py=stglib.core.runcmd:runvecnc2waves", "runsigmat2cdf.py=stglib.core.runcmd:runsigmat2cdf", "runsigraw2cdf.py=stglib.core.runcmd:runsigraw2cdf", "runsigcdf2nc.py=stglib.core.runcmd:runsigcdf2nc", diff --git a/stglib/aqd/aqdutils.py b/stglib/aqd/aqdutils.py index 34d3f3ff..9ef46a4d 100644 --- a/stglib/aqd/aqdutils.py +++ b/stglib/aqd/aqdutils.py @@ -1091,30 +1091,33 @@ def add_attributes(var, dsattrs): if "w_1204" in ds: ds["w_1204"].attrs["units"] = "m s-1" - if "vel1_1277" in ds: - ds["vel1_1277"].attrs.update( - { - "units": "m s-1", - "long_name": "Beam 1 Velocity", - "epic_code": 1277, - } - ) - if "vel2_1278" in ds: - ds["vel2_1278"].attrs.update( - { - "units": "m s-1", - "long_name": "Beam 2 Velocity", - "epic_code": 1278, - } - ) - if "vel3_1279" in ds: - ds["vel3_1279"].attrs.update( - { - "units": "m s-1", - "long_name": "Beam 3 Velocity", - "epic_code": 1279, - } - ) + if "vel1_1277" in ds: + ds["vel1_1277"].attrs.update( + { + "units": "m s-1", + "long_name": "Beam 1 Velocity", + "epic_code": 1277, + "standard_name": "radial_sea_water_velocity_away_from_instrument", + } + ) + if "vel2_1278" in ds: + ds["vel2_1278"].attrs.update( + { + "units": "m s-1", + "long_name": "Beam 2 Velocity", + "epic_code": 1278, + "standard_name": "radial_sea_water_velocity_away_from_instrument", + } + ) + if "vel3_1279" in ds: + ds["vel3_1279"].attrs.update( + { + "units": "m s-1", + "long_name": "Beam 3 Velocity", + "epic_code": 1279, + "standard_name": "radial_sea_water_velocity_away_from_instrument", + } + ) if "AGC1_1221" in ds: ds["AGC1_1221"].attrs.update( diff --git a/stglib/core/cmd.py b/stglib/core/cmd.py index 67e13947..f71227d0 100644 --- a/stglib/core/cmd.py +++ b/stglib/core/cmd.py @@ -325,6 +325,14 @@ def veccdf2nc_parser(): return parser +def vecnc2waves_parser(): + description = "Generate Vector waves statistics file" + parser = argparse.ArgumentParser(description=description) + ncarg(parser) + + return parser + + def sigmat2cdf_parser(): description = "Convert Signature files exported in Matlab format to raw .cdf format. Run this script from the directory containing Signature files." parser = argparse.ArgumentParser(description=description) diff --git a/stglib/core/runcmd.py b/stglib/core/runcmd.py index 2be4e710..6d3ee433 100644 --- a/stglib/core/runcmd.py +++ b/stglib/core/runcmd.py @@ -256,6 +256,12 @@ def runvecdat2cdf(): stglib.vec.dat2cdf.dat_to_cdf(metadata) +def runvecnc2waves(): + args = stglib.cmd.vecnc2waves_parser().parse_args() + + stglib.vec.nc2waves.nc_to_waves(args.ncname) + + def runwvscdf2nc(): args = stglib.cmd.wvscdf2nc_parser().parse_args() diff --git a/stglib/core/waves.py b/stglib/core/waves.py index 5c424186..6f128c16 100644 --- a/stglib/core/waves.py +++ b/stglib/core/waves.py @@ -5,8 +5,7 @@ def make_waves_ds(ds, noise=0.75): - - print("Computing waves statistics") + print("Computing wave statistics") if "P_1ac" in ds: presvar = "P_1ac" else: @@ -14,7 +13,10 @@ def make_waves_ds(ds, noise=0.75): f, Pxx = pressure_spectra(ds[presvar].squeeze(), fs=1 / ds.attrs["sample_interval"]) - z = ds.attrs["initial_instrument_height"] + if "pressure_sensor_height" in ds.attrs: + z = ds.attrs["pressure_sensor_height"] + else: + z = ds.attrs["initial_instrument_height"] h = ds[presvar].squeeze().mean(dim="sample") + z k = np.asarray([qkfs(2 * np.pi * f, x) for x in h.values]) diff --git a/stglib/tests/data/.gitattributes b/stglib/tests/data/.gitattributes index 47f53eb6..d714f433 100644 --- a/stglib/tests/data/.gitattributes +++ b/stglib/tests/data/.gitattributes @@ -86,3 +86,10 @@ L0221705_v2_rs.csv filter=lfs diff=lfs merge=lfs -text Bang_--_MBW.csv filter=lfs diff=lfs merge=lfs -text Barometer_w_RainGauge.csv filter=lfs diff=lfs merge=lfs -text 1711426_Bang--MBW_0_CR.txt filter=lfs diff=lfs merge=lfs -text +NBMCSB02.hdr filter=lfs diff=lfs merge=lfs -text +NBMCSB02.pck filter=lfs diff=lfs merge=lfs -text +NBMCSB02.sen filter=lfs diff=lfs merge=lfs -text +NBMCSB02.ssl filter=lfs diff=lfs merge=lfs -text +NBMCSB02.vhd filter=lfs diff=lfs merge=lfs -text +NBMCSB02.VEC filter=lfs diff=lfs merge=lfs -text +NBMCSB02.dat filter=lfs diff=lfs merge=lfs -text diff --git a/stglib/tests/data/NBMCSB02.VEC b/stglib/tests/data/NBMCSB02.VEC new file mode 100755 index 00000000..878f736d --- /dev/null +++ b/stglib/tests/data/NBMCSB02.VEC @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d795f6ef9b7362eebba1789ee2068063059dc6defb497e55b89c0acaa5da568 +size 342548674 diff --git a/stglib/tests/data/NBMCSB02.dat b/stglib/tests/data/NBMCSB02.dat new file mode 100755 index 00000000..facdff37 --- /dev/null +++ b/stglib/tests/data/NBMCSB02.dat @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2e46b38952b275b485467b0be3aaa8c4080a1eb43f1b235fcc6984d00e47b98 +size 1446936360 diff --git a/stglib/tests/data/NBMCSB02.hdr b/stglib/tests/data/NBMCSB02.hdr new file mode 100755 index 00000000..575bf75a --- /dev/null +++ b/stglib/tests/data/NBMCSB02.hdr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb45479019f9ae64324cfc109e31d2239857f4cbcbecbd8b61dd973cf2dfd165 +size 9282 diff --git a/stglib/tests/data/NBMCSB02.pck b/stglib/tests/data/NBMCSB02.pck new file mode 100755 index 00000000..136050ab --- /dev/null +++ b/stglib/tests/data/NBMCSB02.pck @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b6891a591bf5b573312e04d655807593c8abf737db3cb475db44cd77a3a0a6b +size 108428176 diff --git a/stglib/tests/data/NBMCSB02.sen b/stglib/tests/data/NBMCSB02.sen new file mode 100755 index 00000000..2f669e9d --- /dev/null +++ b/stglib/tests/data/NBMCSB02.sen @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0770d574b04867288459a803e3756a0ea9911f372fb873f6dcadbe032c8b466a +size 130120150 diff --git a/stglib/tests/data/NBMCSB02.ssl b/stglib/tests/data/NBMCSB02.ssl new file mode 100755 index 00000000..3964917d --- /dev/null +++ b/stglib/tests/data/NBMCSB02.ssl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:189588d7bc228a3a603322084e50de601aef8f1bd86562963d163e524393881e +size 299120 diff --git a/stglib/tests/data/NBMCSB02.vhd b/stglib/tests/data/NBMCSB02.vhd new file mode 100755 index 00000000..2d7d9910 --- /dev/null +++ b/stglib/tests/data/NBMCSB02.vhd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c58180ae2d90370bfd9de6178c258fd0bba33319787b9581c087a5751521f6e +size 869850 diff --git a/stglib/tests/data/config_NBM22CSB.yaml b/stglib/tests/data/config_NBM22CSB.yaml new file mode 100755 index 00000000..0e6b0dc8 --- /dev/null +++ b/stglib/tests/data/config_NBM22CSB.yaml @@ -0,0 +1,7 @@ +Conventions: CF-1.8 +basefile: NBMCSB02 +filename: NBMCSBaq01 +initial_instrument_height: 0.5 # DJN guessed value +orientation: UP # for Vector instruments, UP is when the head is pointing down +AnalogInput1: "true" +AnalogInput1_warmup_samples: 1 diff --git a/stglib/tests/data/gatts_NBM22CSB.txt b/stglib/tests/data/gatts_NBM22CSB.txt new file mode 100755 index 00000000..8204e2ce --- /dev/null +++ b/stglib/tests/data/gatts_NBM22CSB.txt @@ -0,0 +1,33 @@ +MOORING; NBM22CSB +PROGRAM; Sediment Transport in Coastal Environments +PROJECT; Temporal variability in sediment delivery to a North and a Central San Francisco Bay salt marsh +EXPERIMENT; NBM22 +Region; Northern and Central San Francisco Bay +CruiseID; 2022-619-FA +SciPi; Jessica Lacy, Samantha McGill +Year; 2022 +Site; CSB +platform_type; Tripod +latitude; 37.92865 +longitude; -122.48343 +WATER_DEPTH; 2.2 +WATER_DEPTH_source; Ships echosounder +WATER_DEPTH_note; Water depth at time of deployment +NAVD88_elevation_ref; 999 +NAVD88_elevation_source; RTK survey +elevation_datum; NAVD88 +Deployment_date; 14-JUN-2022 19:13 +Recovery_date; 10-AUG-2022 18:19 +OFAFunding; San Francisco Bay Regional Monitoring Program, San Francisco Estuary Institute +CollaboratingAgency; USGS WERC +DESCRIPTION; Coffee Table +magnetic_variation; 13.24 +DATA_TYPE; TIME +DATA_SUBTYPE; MOORED +DATA_ORIGIN; USGS/PCMSC +COORD_SYSTEM; GEOGRAPHIC +Conventions; PMEL/EPIC +POS_CONST; 0 +DEPTH_CONST; 0 +history; +DATA_CMNT; none \ No newline at end of file diff --git a/stglib/tests/test_scripts.py b/stglib/tests/test_scripts.py index 54c09e9d..8e1deae1 100644 --- a/stglib/tests/test_scripts.py +++ b/stglib/tests/test_scripts.py @@ -36,6 +36,18 @@ def exo_nc(nc_file, atmpres=None): assert "Done writing netCDF file" in result.stdout.decode("utf8") +def test_exo(): + exo_raw("glob_attbel5C.txt", "config_bel5C.yaml") + exo_nc("bel53Cexo-raw.cdf") + exo_raw("glob_att1119a.txt", "1119Aexo_config.yaml") + exo_nc("1119Aexo-raw.cdf") + exo_raw("glob_att1151b.txt", "1151Bexo_config.yaml") + exo_nc("1151Bexo-raw.cdf") + # test for atmospheric correction + exo_raw("glob_attbel3C.txt", "config_bel3C.yaml") + exo_nc("BEL19B3C03exo-raw.cdf", "atmpres_BEL19B3C03exo.cdf") + + def aqd_raw(glob_att, config_yaml): result = subprocess.run( [scripts / "runaqdhdr2cdf.py", glob_att, config_yaml], @@ -76,18 +88,6 @@ def aqdhr_nc(nc_file): assert "Done writing netCDF file" in result.stdout.decode("utf8") -def test_exo(): - exo_raw("glob_attbel5C.txt", "config_bel5C.yaml") - exo_nc("bel53Cexo-raw.cdf") - exo_raw("glob_att1119a.txt", "1119Aexo_config.yaml") - exo_nc("1119Aexo-raw.cdf") - exo_raw("glob_att1151b.txt", "1151Bexo_config.yaml") - exo_nc("1151Bexo-raw.cdf") - # test for atmospheric correction - exo_raw("glob_attbel3C.txt", "config_bel3C.yaml") - exo_nc("BEL19B3C03exo-raw.cdf", "atmpres_BEL19B3C03exo.cdf") - - def test_aqd(): aqd_raw("glob_att1118a_b.txt", "aqd1118A_config.yaml") aqd_nc("1118ABaqd-raw.cdf") @@ -123,6 +123,43 @@ def test_aqdturnaround(): aqdturnaround("BEL503") +def vec_raw(glob_att, config_yaml): + result = subprocess.run( + [scripts / "runvecdat2cdf.py", glob_att, config_yaml], + capture_output=True, + cwd=cwd, + ) + assert "Finished writing data" in result.stdout.decode("utf8") + + +def vec_nc(nc_file, atmpres=None): + if atmpres is not None: + runlist = [scripts / "runveccdf2nc.py", nc_file, "--atmpres", atmpres] + else: + runlist = [scripts / "runveccdf2nc.py", nc_file] + result = subprocess.run( + runlist, + capture_output=True, + cwd=cwd, + ) + assert "Done writing netCDF file" in result.stdout.decode("utf8") + + +def vec_wvs(nc_file): + result = subprocess.run( + [scripts / "runvecnc2waves.py", nc_file], + capture_output=True, + cwd=cwd, + ) + assert "Done writing netCDF file" in result.stdout.decode("utf8") + + +def test_vec(): + vec_raw("gatts_NBM22CSB.txt", "config_NBM22CSB.yaml") + vec_nc("NBMCSBaq01-raw.cdf") + vec_wvs("NBMCSBaq01-a.nc") + + def wxt_raw(glob_att, config_yaml): result = subprocess.run( [scripts / "runwxtcsv2cdf.py", glob_att, config_yaml], diff --git a/stglib/vec/__init__.py b/stglib/vec/__init__.py index 70baf63d..95bcb13a 100644 --- a/stglib/vec/__init__.py +++ b/stglib/vec/__init__.py @@ -1 +1 @@ -from . import dat2cdf, cdf2nc +from . import cdf2nc, dat2cdf, nc2waves diff --git a/stglib/vec/dat2cdf.py b/stglib/vec/dat2cdf.py index 9347173f..4ac9861d 100644 --- a/stglib/vec/dat2cdf.py +++ b/stglib/vec/dat2cdf.py @@ -40,6 +40,7 @@ def dat_to_cdf(metadata): r = np.shape(dssen.Heading)[0] senburstlen = int(ds.attrs["VECSamplesPerBurst"] / ds.attrs["VECSamplingRate"] + 1) + ds.attrs["sample_interval"] = 1 / ds.attrs["VECSamplingRate"] mod = r % senburstlen if mod: diff --git a/stglib/vec/nc2waves.py b/stglib/vec/nc2waves.py new file mode 100644 index 00000000..244ad2df --- /dev/null +++ b/stglib/vec/nc2waves.py @@ -0,0 +1,192 @@ +import numpy as np +import xarray as xr +from tqdm import tqdm + +from ..core import utils, waves + + +def nc_to_waves(nc_filename): + """ + Process burst data to wave statistics + """ + + ds = xr.load_dataset(nc_filename) + + spec = waves.make_waves_ds(ds) + + for k in ["wp_peak", "wh_4061", "wp_4060", "pspec"]: + ds[k] = spec[k] + + dopuv = False + if "puv" in ds.attrs: + if ds.attrs["puv"].lower() == "true": + dopuv = True + + if dopuv: + ds = ds_puv(ds) + + # keep burst mean P_1 and P_1ac for reference + for k in ["P_1", "P_1ac"]: + if k in ds: + ds[k] = ds[k].mean(dim="sample", keep_attrs=True) + + for k in [ + "burst", + "sample", + "u_1205", + "v_1206", + "w_1204", + "vel1_1277", + "vel2_1278", + "vel3_1279", + "AGC1_1221", + "AGC2_1222", + "AGC3_1223", + "SNR1", + "SNR2", + "SNR3", + "cor1_1285", + "cor2_1286", + "cor3_1287", + "AnalogInput1", + "AnalogInput2", + "Hdg_1215", + "Ptch_1216", + "Roll_1217", + "Bat_106", + "Tx_1211", + "SV_80", + ]: + if k in ds: + ds = ds.drop(k) + + ds = utils.trim_max_wp(ds) + + ds = utils.trim_min_wh(ds) + + ds = utils.trim_max_wh(ds) + + ds = utils.trim_wp_ratio(ds) + + if dopuv: + ds = waves.puv_qaqc(ds) + + # Add attrs + ds = utils.ds_add_wave_attrs(ds) + + # assign min/max (need to do this after trimming): + ds = utils.add_min_max(ds) + + nc_filename = ds.attrs["filename"] + "s-a.nc" + + ds.to_netcdf(nc_filename, unlimited_dims=["time"]) + utils.check_compliance(nc_filename, conventions=ds.attrs["Conventions"]) + print("Done writing netCDF file", nc_filename) + + return ds + + +def ds_puv(ds): + print("Running puv_quick") + + N, M = np.shape(ds["u_1205"].squeeze()) + + if "puv_first_frequency_cutoff" in ds.attrs: + first_frequency_cutoff = ds.attrs["puv_first_frequency_cutoff"] + else: + first_frequency_cutoff = 1 / 10 + + if "puv_last_frequency_cutoff" in ds.attrs: + last_frequency_cutoff = ds.attrs["puv_last_frequency_cutoff"] + else: + last_frequency_cutoff = 1 / 2.5 + + desc = { + "Hrmsp": "Hrms (=Hmo) from pressure", + "Hrmsu": "Hrms from u,v", + "ubr": f"Representative orbital velocity amplitude in freq. band ( {first_frequency_cutoff} <= f <= {last_frequency_cutoff} ) (m/s)", + "omegar": "Representative orbital velocity (radian frequency)", + "Tr": "Representative orbital velocity period (s)", + "Tpp": "Peak period from pressure (s)", + "Tpu": "Peak period from velocity (s)", + "phir": "Representative orbital velocity direction (angles from x-axis, positive ccw)", + "azr": "Representative orb. velocity direction (deg; geographic azimuth; ambiguous =/- 180 degrees)", + "ublo": f"ubr in freq. band (f <= {first_frequency_cutoff}) (m/s)", + "ubhi": f"ubr in freq. band (f >= {last_frequency_cutoff}) (m/s)", + "ubig": f"ubr in infra-gravity freq. band ({first_frequency_cutoff} f <= 1/20) (m/s)", + "Hrmsp_tail": "Hrms (=Hmo) from pressure with tail applied", + "Hrmsu_tail": "Hrms from u,v with tail applied", + "phir_tail": "Representative orbital velocity direction (angles from x-axis, positive ccw) with tail applied", + "azr_tail": "Representative orb. velocity direction (deg; geographic azimuth; ambiguous =/- 180 degrees) with tail applied", + } + standard_name = { + "Tpp": "sea_surface_wave_period_at_variance_spectral_density_maximum", + "Tpu": "sea_surface_wave_period_at_variance_spectral_density_maximum", + "phir": "sea_surface_wave_from_direction", + "azr": "sea_surface_wave_from_direction", + "phir_tail": "sea_surface_wave_from_direction", + "azr_tail": "sea_surface_wave_from_direction", + } + + puvs = {k: np.full_like(ds["time"].values, np.nan, dtype=float) for k in desc} + + if "P_1ac" in ds: + pvar = "P_1ac" + else: + pvar = "P_1" + + for n in tqdm(range(N)): + puv = waves.puv_quick( + ds[pvar][n, :].values, + ds["u_1205"][n, :], + ds["v_1206"][n, :], + ds[pvar][n, :].mean().values + ds.attrs["pressure_sensor_height"], + ds.attrs["pressure_sensor_height"], + ds.attrs["velocity_sample_volume_height"], + 1 / ds.attrs["sample_interval"], + first_frequency_cutoff=first_frequency_cutoff, + last_frequency_cutoff=last_frequency_cutoff, + ) + + for k in puvs: + puvs[k][n] = puv[k] + + for k in puvs: + ds["puv_" + k] = xr.DataArray(puvs[k], dims="time") + if k in desc: + ds["puv_" + k].attrs["description"] = desc[k] + if k in standard_name: + ds["puv_" + k].attrs["standard_name"] = standard_name[k] + + ds["puv_Hsp"] = np.sqrt(2) * ds["puv_Hrmsp"] + ds["puv_Hsp"].attrs["description"] = "Hs computed via sqrt(2) * Hrmsp" + ds["puv_Hsp"].attrs["standard_name"] = "sea_surface_wave_significant_height" + ds["puv_Hsp"].attrs["units"] = "m" + + ds["puv_Hsu"] = np.sqrt(2) * ds["puv_Hrmsu"] + ds["puv_Hsu"].attrs["description"] = "Hs computed via sqrt(2) * Hrmsu" + ds["puv_Hsu"].attrs["standard_name"] = "sea_surface_wave_significant_height" + ds["puv_Hsu"].attrs["units"] = "m" + + ds["puv_Hsp_tail"] = np.sqrt(2) * ds["puv_Hrmsp_tail"] + ds["puv_Hsp_tail"].attrs[ + "description" + ] = "Hs computed via sqrt(2) * Hrmsp with tail applied" + ds["puv_Hsp_tail"].attrs["standard_name"] = "sea_surface_wave_significant_height" + ds["puv_Hsp_tail"].attrs["units"] = "m" + + ds["puv_Hsu_tail"] = np.sqrt(2) * ds["puv_Hrmsu_tail"] + ds["puv_Hsu_tail"].attrs[ + "description" + ] = "Hs computed via sqrt(2) * Hrmsu with tail applied" + ds["puv_Hsu_tail"].attrs["standard_name"] = "sea_surface_wave_significant_height" + ds["puv_Hsu_tail"].attrs["units"] = "m" + + ds["puv_Tpp"].attrs["units"] = "s" + + ds["puv_Tpu"].attrs["units"] = "s" + + ds["puv_azr"].attrs["units"] = "degrees" + ds["puv_azr_tail"].attrs["units"] = "degrees" + + return ds