diff --git a/.gitmodules b/.gitmodules index 770389c..8d7475c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ url = git@github.com:SBFRF/testbedutils.git [submodule "prepdata"] path = prepdata - url = git@github.com:SBFRF/prepdata.git + url = https://github.com/l-szczyrba/prepdata.git diff --git a/.idea/cmtb.iml b/.idea/cmtb.iml index 34ddd3f..e07acc1 100644 --- a/.idea/cmtb.iml +++ b/.idea/cmtb.iml @@ -8,7 +8,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 789e113..5f45815 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,7 @@ - + - - + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml index a847ac6..c46ee1f 100644 --- a/.idea/other.xml +++ b/.idea/other.xml @@ -5,4 +5,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index bf2fc5e..35921b0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ The test bed is established at the USACE CHL Field REsearch Facility in Duck, No - conda install conda install -c conda-forge cartopy - * netCDF4, pyproj, utm, wavespectra, progressbar, opencv-python + * netCDF4, pyproj, utm, wavespectra, progressbar, opencv-python, xarray - there's likely more, please add as you find!! :-[] diff --git a/notebooks/cshore/cshore_example.ipynb b/notebooks/cshore/cshore_example.ipynb index bfd4899..3388bb1 100644 --- a/notebooks/cshore/cshore_example.ipynb +++ b/notebooks/cshore/cshore_example.ipynb @@ -8,6 +8,7 @@ "source": [ "import sys\n", "sys.path.insert(0, '/Users/rdchlth9/Codes/cmtb_refactor/')\n", + "\n", "from getdatatestbed import getDataFRF\n", "from prepdata.prepDataLib import PrepDataTools as preptools\n", "from prepdata import inputOutput\n", @@ -501,7 +502,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" } }, "nbformat": 4, diff --git a/notebooks/cshore/cshore_test_multiple.ipynb b/notebooks/cshore/cshore_test_multiple.ipynb index 9f25300..923d4b7 100644 --- a/notebooks/cshore/cshore_test_multiple.ipynb +++ b/notebooks/cshore/cshore_test_multiple.ipynb @@ -307,7 +307,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" } }, "nbformat": 4, diff --git a/notebooks/swash/swash_example.ipynb b/notebooks/swash/swash_example.ipynb new file mode 100644 index 0000000..587710b --- /dev/null +++ b/notebooks/swash/swash_example.ipynb @@ -0,0 +1,839 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cd1aa9ea", + "metadata": {}, + "source": [ + "# Coastal Model Test Bed (CMTB) SWASH 1D Example " + ] + }, + { + "cell_type": "markdown", + "id": "93d32e7d", + "metadata": {}, + "source": [ + "#### This notebook walks through the CMTB SWASH 1D Workflow\n", + " CMTB sets this up as a \"skinny\" 2D simulation that is effectively 1D\n", + " Notebook users will have to download CMTB from github and install the required python packages\n", + " Users select simulation settings in the User Input Cell and can walk through the rest of the code\n", + " Once CMTB writes the input files, users can run SWASH separately in a command line interface \n", + " (commands listed at end of notebook) OR, if users have a SWASH mpiexec compiled (aka SWASH compiled in parallel mode), \n", + " users may run SWASH via CMTB" + ] + }, + { + "cell_type": "markdown", + "id": "b1a304a5", + "metadata": {}, + "source": [ + "## Load packages and libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7797d0ff", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '/Users/lszcz/Documents/CMTB/cmtb/')\n", + "\n", + "# CMTB \n", + "from prepdata.prepDataLib import PrepDataTools as preptools\n", + "from getdatatestbed import getDataFRF\n", + "from getdatatestbed.getDataFRF import getDataTestBed\n", + "from prepdata import writeRunRead as wrr\n", + "from prepdata import inputOutput\n", + "from testbedutils import waveLib as sbwave\n", + "from subprocess import check_output\n", + "from plotting.operationalPlots import generate_CrossShoreTimeseries\n", + "from plotting import operationalPlots as oP\n", + "from testbedutils import sblib as sb\n", + "\n", + "# Standard utilities\n", + "import datetime as dt\n", + "import netCDF4 as nc\n", + "import numpy as np\n", + "import os, glob, makenc\n", + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import netCDF4 as nc" + ] + }, + { + "cell_type": "markdown", + "id": "7d8ae1d2", + "metadata": {}, + "source": [ + "## User Input Cell: Declare input settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f39ac02", + "metadata": {}, + "outputs": [], + "source": [ + "# Define various directories:\n", + "path_prefix = '/Users/lszcz/Documents/CMTB/cmtb' # core cmtb path\n", + "workingDir = '/Users/lszcz/Documents/CMTB/cmtb/data' \n", + "exe = '/mirror/swash/swash.exe'\n", + "# choose location and name of netcdf file with results:\n", + "npath= '/Users/lszcz/Documents/CMTB/cmtb/data/swash1D_test_mpi/ncresults/results.nc' \n", + "# local path to swash_global.yml:\n", + "fieldYaml = '/Users/lszcz/Documents/CMTB/cmtb/yaml_files/WaveModels/swash/base/swash_global.yml' \n", + "# local path to swash_var.yml:\n", + "varYaml = '/Users/lszcz/Documents/CMTB/cmtb/yaml_files/waveModels/swash/swash_var.yml' \n", + "\n", + "# Define simulation\n", + "testName = 'swash1D_test_mpi'\n", + "versionPrefix='base'\n", + "model = 'swash'\n", + "\n", + "# Select simulated period\n", + "date_str = '2019-10-11' \n", + "startTime = dt.datetime(2019,10,11,15,0,0) # Start time, Year, month, day, hour\n", + "endTime = dt.datetime(2019,10,11,16,0,0) # Stop time, 1 hour after start time, Year, month, day, hour\n", + "simulationDuration = 1 # length of time for simulated event (should be 1 hour)\n", + "spinup = 900 # initial spinup time before model output (in seconds)\n", + "ncores = 18 # number of cores to use\n", + "\n", + "# Select bathymetric setting\n", + "bathy_loc = 'survey'\n", + "profile_num = 945 # FRF profile number\n", + "survey_date = dt.datetime(2019, 10, 15, 23) # Select survey date (always include 23 aka 11 pm to search for any profile collected that day)\n", + "xmin = 25 # Onshore extent in FRF coordinates\n", + "xmax = 915 # Offshore extent in FRF coordinates\n", + "dx = 1 # Resolution in x\n", + "dy = 1 # Resolution in y \n", + "fric_fac = 0.015\n", + "\n", + "# Select sensor for boundary conditions\n", + "sensor = '8m-array' # FRF Sensor used to pull wave conditions" + ] + }, + { + "cell_type": "markdown", + "id": "8e1ed2dd", + "metadata": {}, + "source": [ + "***\n", + "***\n", + "***" + ] + }, + { + "cell_type": "markdown", + "id": "094a9a32", + "metadata": {}, + "source": [ + "## Auto-format additional variables" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f784ad3", + "metadata": {}, + "outputs": [], + "source": [ + "runDuration = (endTime - startTime).total_seconds()\n", + "ymin = profile_num - dy # Extending selected transect for \"Psuedo 1D\" simulation, allowing for a skinny alongshore interpolated grid\n", + "ymax = profile_num + dy\n", + "\n", + "waveTimeList = preptools.timeLists(startTime,endTime,30*60) # dt in hours\n", + "wlTimeList = preptools.timeLists(startTime+dt.timedelta(minutes=30), # Finds 1 water level halfway through the target event\n", + " endTime,30*60) " + ] + }, + { + "cell_type": "markdown", + "id": "9486819c", + "metadata": {}, + "source": [ + "## Gather raw data" + ] + }, + { + "cell_type": "markdown", + "id": "6777efc8", + "metadata": {}, + "source": [ + "Waves and water levels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85167670", + "metadata": {}, + "outputs": [], + "source": [ + "go = getDataFRF.getObs(startTime,endTime) # initialize go command with simulated period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "956cd954", + "metadata": {}, + "outputs": [], + "source": [ + "wave_spec = go.getWaveData(sensor,spec=True) # grab raw spectra\n", + "wl = go.getWL() # grab raw water levels" + ] + }, + { + "cell_type": "markdown", + "id": "92e9e24e", + "metadata": {}, + "source": [ + "Bathymetry" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3d9696f", + "metadata": {}, + "outputs": [], + "source": [ + "gdTB = getDataTestBed(startTime, endTime) # initialize gdTB command with simulated period" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0c6e8a0", + "metadata": {}, + "outputs": [], + "source": [ + "bathy_data = gdTB.getBathyIntegratedTransect(method=1, # Gathers relevant data (beyond designated bounds) to pass into prepbathy for interpolation\n", + " ForcedSurveyDate=survey_date, \n", + " ybounds=[ymin, ymax]) " + ] + }, + { + "cell_type": "markdown", + "id": "1a832afc", + "metadata": {}, + "source": [ + "Check out raw data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41bf8025", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(wave_spec['wavefreqbin'], wave_spec['fspec'])\n", + "plt.ylabel('E [$m^2/Hz$]')\n", + "plt.xlabel('Frequency [$Hz$]')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15307d8d", + "metadata": {}, + "outputs": [], + "source": [ + "rmesh, thetamesh = np.meshgrid(wave_spec['wavefreqbin'],wave_spec['wavedirbin'])\n", + "thetamesh = thetamesh*np.pi/180\n", + "fig, ax = plt.subplots(subplot_kw=dict(projection='polar'))\n", + "E_2D_plot = wave_spec['dWED'].T\n", + "ax.contourf(np.flip(thetamesh), rmesh,E_2D_plot,10)\n", + "ax.set_theta_zero_location(\"N\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "724859f6", + "metadata": {}, + "outputs": [], + "source": [ + "plt.plot(wl['time'], wl['WL'])\n", + "plt.ylabel('$\\eta$ [$m$]')\n", + "plt.xlabel('time [$s$]')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "1443e3f1", + "metadata": {}, + "source": [ + "## Format gathered raw data for SWASH input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fd88a23", + "metadata": {}, + "outputs": [], + "source": [ + "prepdata = preptools() # initialize prepdata command" + ] + }, + { + "cell_type": "markdown", + "id": "c5eb10ff", + "metadata": {}, + "source": [ + "Process wave packet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b7ce294", + "metadata": {}, + "outputs": [], + "source": [ + "good_freqs = wave_spec['wavefreqbin'][wave_spec['fspec'].mask == False] \n", + " # Above needed otherwise will interpolated frequencies beyond sensor range, skewing energy levels down to -999\n", + "wavePacket = prepdata.prep_spec_phaseResolved(rawspec=wave_spec, \n", + " version_prefix=versionPrefix, \n", + " grid='1D',\n", + " spinUpTime=spinup, \n", + " runDuration=runDuration,\n", + " freqRange=[min(good_freqs), max(good_freqs)]) # Filter down to \"good\" frequencies" + ] + }, + { + "cell_type": "markdown", + "id": "3675869c", + "metadata": {}, + "source": [ + "Process water level packet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70288ebf", + "metadata": {}, + "outputs": [], + "source": [ + "wlPacket = prepdata.prep_WL(wl,wlTimeList)\n", + " # Should return 1 record for SWASH input" + ] + }, + { + "cell_type": "markdown", + "id": "bf8d31bf", + "metadata": {}, + "source": [ + "Process bathy packet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a326907", + "metadata": {}, + "outputs": [], + "source": [ + "bathyPacket = prepdata.prep_SwashBathy(x0=xmin, \n", + " y0=profile_num-1, \n", + " bathy=bathy_data, \n", + " xBounds=[xmin, xmax+1], \n", + " yBounds = [ymin,ymax+1], \n", + " dx=dx, dy=dy)\n", + " # if 1D, ybounds should be numbers surrounding target profile number (established in user input cell)\n", + " # Xbounds and Ybounds plus 1 because python is 0-indexed" + ] + }, + { + "cell_type": "markdown", + "id": "2555ca09", + "metadata": {}, + "source": [ + "Check out processed data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "835175e1", + "metadata": {}, + "outputs": [], + "source": [ + "h = bathyPacket['elevation']\n", + "h = h[2,:] # Identify target profile\n", + "plt.figure()\n", + "plt.plot(bathyPacket['xFRF'], h)\n", + "plt.ylabel('h [$m$]')\n", + "plt.xlabel('x [$m$]')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae534253", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(wavePacket['freqbins'], wavePacket['fspec'])\n", + "plt.ylabel('E [$m^2/Hz$]')\n", + "plt.xlabel('Frequency [$Hz$]')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce937502", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(wl['time'],wl['WL'])\n", + "plt.plot(wlPacket['time'],wlPacket['avgWL'],marker=\"o\", markersize=20)\n", + "plt.ylabel('Water Level (m)')\n", + "plt.xlabel('Date')\n", + "plt.legend(('Raw WL','Input WL'))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "811a4393", + "metadata": {}, + "source": [ + "## Setup the SWASH model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5514b05d", + "metadata": {}, + "outputs": [], + "source": [ + "swio = wrr.swashIO(WL=wlPacket['avgWL'], \n", + " equilbTime=wavePacket['spinUp'], \n", + " Hs=wavePacket['Hs'], \n", + " Tp=1/wavePacket['peakf'],\n", + " Dm=wavePacket['waveDm'],\n", + " workingDirectory=workingDir, \n", + " testName=testName, fileNameBase=date_str,\n", + " version_prefix=versionPrefix, \n", + " runTime=simulationDuration, \n", + " startTime=startTime, \n", + " endTime=endTime,\n", + " runFlag=True, \n", + " generateFlag=False,\n", + " readFlag=True)\n", + " # Above initializes swio command" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dfd3213", + "metadata": {}, + "outputs": [], + "source": [ + "swio._replaceDefaultParams() # fill out swio command with default model settings\n", + "\n", + "if not os.path.isdir(swio.workingDirectory): # make directory where input files stored\n", + " os.makedirs(swio.workingDirectory)\n", + "os.chdir(swio.workingDirectory)\n", + "\n", + "swio._check_input() # final checks before writing" + ] + }, + { + "cell_type": "markdown", + "id": "7b1c7c4d", + "metadata": {}, + "source": [ + "Write the input files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34516143", + "metadata": {}, + "outputs": [], + "source": [ + "swio.write_spec1D(wavePacket['freqbins'], wavePacket['fspec']) # writes the SWASH forcing.BND file\n", + "swio.write_bot(bathyPacket['elevation']) # writes the SWASH bathy.BOT file\n", + "swio.write_sws(gridDict=bathyPacket, WLpacket=wlPacket) # writes the SWASH INPUT.sws file" + ] + }, + { + "cell_type": "markdown", + "id": "001b0e42", + "metadata": {}, + "source": [ + "## -----------------------------------------------------------------------------------------\n", + "# RUNING SWASH BELOW, TWO OPTIONS:\n", + "## (1) Run SWASH separately \n", + " Switch to command line, cd to the test directory with input files, and run 'swash INPUT'" + ] + }, + { + "cell_type": "markdown", + "id": "61fd75b6", + "metadata": {}, + "source": [ + "## (2) User runs SWASH mpiexec via this notebook in the following cells: \n", + " Can be applied if user has SWASH compiled in parallel mode on their machine and can call mpiexec:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a26d524", + "metadata": {}, + "outputs": [], + "source": [ + "swio._generateRunStrings(exe) # default run string generated by cmtb: 'mpiexec -n 18 /USER/PATHTO.EXE INPUT'\n", + "# overwriting below to account for windows running ubuntu shell on windows machine\n", + "swio.runString1 = 'wsl mpiexec -n 18 /mirror/swash/swash.exe INPUT' " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1e0302d", + "metadata": {}, + "outputs": [], + "source": [ + "swio._preprocessCheck()\n", + "init_t = dt.datetime.now()\n", + "print('Running {} Simulation starting at {}'.format(swio.modelName, init_t))\n", + "\n", + "_ = check_output(swio.runString1, shell=False) # RUN COMMAND" + ] + }, + { + "cell_type": "markdown", + "id": "31c1306c", + "metadata": {}, + "source": [ + "## -----------------------------------------------------------------------------------------\n", + "## Load in results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d45b92d", + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(swio.workingDirectory) # switch to where .mat results stored (working directory)\n", + "[dataDict, metaDict] = swio.loadSwash_Mat(testName+'.mat') # load in and format results\n", + " # (produces a benign warning)" + ] + }, + { + "cell_type": "markdown", + "id": "3c15b839", + "metadata": {}, + "source": [ + "Calculate some wave statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57f4c95f", + "metadata": {}, + "outputs": [], + "source": [ + "eta = dataDict['eta'].squeeze()\n", + "\n", + "fspec, freqs = sbwave.timeSeriesAnalysis1D(dataDict['time'].squeeze(), dataDict['eta'].squeeze(), bandAvg=6)\n", + "Stats = sbwave.stats1D(fspec=fspec, frqbins=freqs, lowFreq=None, highFreq=None)\n", + " # (produces a benign warning)\n", + "HsTS = 4 * np.std(dataDict['eta'].squeeze(), axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efc5076f", + "metadata": {}, + "outputs": [], + "source": [ + "# We use 0.08 m for runup threshold\n", + "r_depth = 0.08 # 4.0 * np.nanmax(np.abs(h[runupInd][1:] - h[runupInd][:-1]))\n", + "\n", + "# Pre-allocate runup variable\n", + "runup = np.zeros(eta.shape[0])\n", + "x_runup = np.zeros_like(runup)\n", + "\n", + "for aa in range(runup.shape[0]):\n", + " # Water depth\n", + " wdepth = eta[aa, :] + dataDict['elevation']\n", + " # Find the runup contour (search from left to right)\n", + " wdepth_ind = np.argmin(abs(wdepth - r_depth)) # changed from Chuan's original code\n", + " # Store the water surface elevation in matrix\n", + " runup[aa] = eta[aa, wdepth_ind] # unrealistic values for large r_depth\n", + " # runup[aa]= -h[wdepth_ind]\n", + " # Store runup position\n", + " x_runup[aa] = dataDict['xFRF'][wdepth_ind]\n", + "maxRunup = np.amax(runup)" + ] + }, + { + "cell_type": "markdown", + "id": "318a36c1", + "metadata": {}, + "source": [ + "## Check out processed results" + ] + }, + { + "cell_type": "markdown", + "id": "b4fa1db9", + "metadata": {}, + "source": [ + "Plot eta" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b82ad6a3", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(dataDict['time'],dataDict['eta'][:,0,400])\n", + "plt.ylabel('$\\eta$ (m)')\n", + "plt.xlabel('Date')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0d0da1bf", + "metadata": {}, + "source": [ + "Plot Hs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57084005", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(dataDict['xFRF'],HsTS)\n", + "plt.ylabel('$H_s$ (m)')\n", + "plt.xlabel('x (m)')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "dd08cf52", + "metadata": {}, + "source": [ + "Plot setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20736e05", + "metadata": {}, + "outputs": [], + "source": [ + "if not os.path.exists(os.path.join(path_prefix, 'figures')):\n", + " os.makedirs(os.path.join(path_prefix, 'figures')) # make a directory 'figures' for the simulation plots\n", + "figurepath = workingDir+'/'+testName+'/'+'figures'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d94caad4", + "metadata": {}, + "outputs": [], + "source": [ + "generate_CrossShoreTimeseries(figurepath+'/setup', np.mean(dataDict['eta'].squeeze(), axis=0), dataDict['elevation'], dataDict['xFRF'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b411852", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from IPython.display import Image\n", + "Image(figurepath+'/setup'+'.png')" + ] + }, + { + "cell_type": "markdown", + "id": "9c406888", + "metadata": {}, + "source": [ + "Plot timeseries of eta" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c0a9477", + "metadata": {}, + "outputs": [], + "source": [ + "figureBaseFname = 'CMTB_waveModels_{}_{}_'.format(model, versionPrefix) # names the images generated" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f27a622", + "metadata": {}, + "outputs": [], + "source": [ + "# choose the subsample of timesteps to include in the gif\n", + "nSubSample = 2\n", + "# Choose frames per second of the generated gif ( fps = nSubSample*dt -->> default option)\n", + "fps = 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7f2bbcb", + "metadata": {}, + "outputs": [], + "source": [ + "for tidx in np.arange(0, len(dataDict['time']), nSubSample).astype(int):\n", + " ofPlotName = os.path.join(figurepath, figureBaseFname + 'TS_' + dataDict['time'][tidx].strftime('%Y%m%dT%H%M%S%fZ') +'.png')\n", + " oP.generate_CrossShoreTimeseries(ofPlotName, dataDict['eta'][tidx].squeeze(), -dataDict['elevation'], dataDict['xFRF'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fdc54d7", + "metadata": {}, + "outputs": [], + "source": [ + "imgList = sorted(glob.glob(os.path.join(figurepath, '*_TS_*.png')))\n", + "dt = np.median(np.diff(dataDict['time'])).microseconds / 1000000\n", + "sb.makeMovie(os.path.join(figurepath, figureBaseFname + 'TS_{}.avi'.format(date_str)), imgList, fps=fps)\n", + "tarOutFile = os.path.join(figurepath, figureBaseFname + 'TS.tar.gz')\n", + "sb.myTarMaker(tarOutFile, imgList)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b367943e", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from IPython.display import Video\n", + "Video(figurepath+'/'+figureBaseFname + 'TS_{}.mp4'.format(date_str), embed=True)" + ] + }, + { + "cell_type": "markdown", + "id": "eb68069f", + "metadata": {}, + "source": [ + "## Save Results to netcdf" + ] + }, + { + "cell_type": "markdown", + "id": "7980c432", + "metadata": {}, + "source": [ + "Format a data dictionary for proper netcdf storage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7e9f82b", + "metadata": {}, + "outputs": [], + "source": [ + "deet = np.median(np.diff(dataDict['time'])).microseconds / 1000000\n", + "tsTime = np.arange(0, len(dataDict['time'])*deet, deet)\n", + "spatial = {'time': nc.date2num(startTime, units='seconds since 1970-01-01 00:00:00'),\n", + " 'station_name': '{} Field Data'.format(model),\n", + " 'tsTime': tsTime,\n", + " 'waveHsIG': np.reshape(Stats['Hm0'], (1, len(dataDict['xFRF']))),\n", + " 'eta': np.swapaxes(dataDict['eta'], 0, 1),\n", + " 'totalWaterLevel': maxRunup,\n", + " 'totalWaterLevelTS': np.reshape(runup, (1, len(runup))),\n", + " 'velocityU': np.swapaxes(dataDict['velocityU'], 0, 1),\n", + " 'velocityV': np.swapaxes(dataDict['velocityV'], 0, 1),\n", + " 'waveHs': np.reshape(Stats['Hm0'], (1, len(dataDict['xFRF']))), # or from HsTS??\n", + " 'xFRF': dataDict['xFRF'],\n", + " 'yFRF': dataDict['yFRF'][0],\n", + " 'runTime': np.expand_dims(swio.simulationWallTime, axis=0),\n", + " 'DX': np.median(np.diff(dataDict['xFRF'])).astype(int),\n", + " 'DY': 1, # must be adjusted for 2D simulations\n", + " 'NI': len(dataDict['xFRF']),\n", + " 'NJ': dataDict['velocityU'].shape[1],} # should automatically adjust for 2D simulations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3030a6b7", + "metadata": {}, + "outputs": [], + "source": [ + "flagfname = os.path.join(npath, 'Flags{}.out.txt'.format(date_str)) # the name of flag file\n", + "makenc.makenc_phaseresolved(data_lib=spatial, globalyaml_fname=fieldYaml, ofname=npath, flagfname=flagfname,\n", + " var_yaml_fname=varYaml)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/swash/swash_example_highlevel.ipynb b/notebooks/swash/swash_example_highlevel.ipynb new file mode 100644 index 0000000..5095c0e --- /dev/null +++ b/notebooks/swash/swash_example_highlevel.ipynb @@ -0,0 +1,350 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "02a13233", + "metadata": {}, + "source": [ + "# High Level Coastal Model Test Bed (CMTB) SWASH 1D Example " + ] + }, + { + "cell_type": "markdown", + "id": "a974db70", + "metadata": {}, + "source": [ + "#### This notebook demonstrates CMTB integrated capabilities\n", + " CMTB sets this up as a \"skinny\" 2D simulation that is effectively 1D\n", + " Notebook users will have to download CMTB from github and install the required python packages\n", + " Users select simulation settings in the User Input Cell and can walk through the rest of the code\n", + " Once CMTB writes the input files, users can run SWASH separately in a command line interface \n", + " (commands listed at end of notebook) OR, if users have a SWASH mpiexec compiled (aka SWASH compiled in parallel mode), \n", + " users may run SWASH via CMTB" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e84ee98", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.insert(0, '/Users/lszcz/Documents/CMTB/cmtb/')\n", + "\n", + "from getdatatestbed import getDataFRF\n", + "from getdatatestbed.getDataFRF import getDataTestBed\n", + "from frontback import frontBackNEW\n", + "from prepdata import writeRunRead as wrr\n", + "from testbedutils import waveLib as sbwave\n", + "from plotting.operationalPlots import generate_CrossShoreTimeseries\n", + "\n", + "import datetime as dt\n", + "import numpy as np\n", + "import os, glob, makenc\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "f1f4c282", + "metadata": {}, + "source": [ + "## User Input Cell: Declare input settings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acfa5edf", + "metadata": {}, + "outputs": [], + "source": [ + "workingDir = '/Users/lszcz/Documents/CMTB/cmtb/data'\n", + "exe = '/mirror/swash/swash.exe'\n", + "testName = 'swash1D_test_mpi'\n", + "versionPrefix='base'\n", + "\n", + "# Select simulated period\n", + "startTime = dt.datetime(2019,10,11,15,0,0)\n", + "date_str = '2019-10-11'\n", + "endTime = dt.datetime(2019,10,11,16,0,0)\n", + "simulationDuration = 1\n", + "spinup = 900 # initial spinup time before model output (in seconds)\n", + "ncores = 18 # number of cores to use\n", + "\n", + "# Select bathymetric setting\n", + "bathy_loc = 'survey'\n", + "profile_num = 945\n", + "xmin = 25\n", + "xmax = 915\n", + "dx = 1\n", + "dy = 1\n", + "fric_fac = 0.015\n", + "\n", + "# Select sensor for boundary conditions\n", + "sensor = '8m-array'\n" + ] + }, + { + "cell_type": "markdown", + "id": "fbee3c73", + "metadata": {}, + "source": [ + "## Auto-format additional variables " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39cf1b97", + "metadata": {}, + "outputs": [], + "source": [ + "runDuration = (endTime - startTime).total_seconds()\n", + "ymin = profile_num - dy\n", + "ymax = profile_num + dy" + ] + }, + { + "cell_type": "markdown", + "id": "7bc228f1", + "metadata": {}, + "source": [ + "## Create input dictionary " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe5df7f5", + "metadata": {}, + "outputs": [], + "source": [ + "Inputs = {}\n", + "Inputs['THREDDS'] = 'CHL'\n", + "Inputs['bathyLoc'] = 'integrated_bathy'\n", + "Inputs['startTime'] = dt.datetime.strftime(startTime,'%Y-%m-%dT%H:%M:%SZ')\n", + "Inputs['endTime'] = dt.datetime.strftime(endTime,'%Y-%m-%dT%H:%M:%SZ')\n", + "Inputs['modelSettings'] = {}\n", + "Inputs['modelSettings']['version_prefix'] = 'base'\n", + "Inputs['modelSettings']['yBounds'] = [ymin, ymax+1]\n", + "Inputs['modelSettings']['grid'] = '1D'\n", + "Inputs['modelSettings']['modelName'] = 'swash'\n", + "Inputs['profileNumber'] = profile_num\n", + "Inputs['simulationDuration'] = simulationDuration\n", + "Inputs['modelExecutable'] = '/Users/lszcz/Documents/CMTB/cmtb/data/swash.exe'\n", + "Inputs['workingDirectory'] = '/data'\n", + "Inputs['netCDFdir'] = '/data'\n", + "Inputs['logfileLoc'] = 'data'\n", + "Inputs['plotFlag'] = False\n", + "Inputs['generateFlag'] = True\n", + "Inputs['runFlag'] = True\n", + "Inputs['analyzeFlag'] = False" + ] + }, + { + "cell_type": "markdown", + "id": "5837c251", + "metadata": {}, + "source": [ + "## Gather data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d841704", + "metadata": {}, + "outputs": [], + "source": [ + "go = getDataFRF.getObs(startTime,endTime) # initialize go command\n", + "wave_spec = go.getWaveData(sensor,spec=True) # grab raw spectra\n", + "wl = go.getWL() # grab raw water levels\n", + "gdTB = getDataTestBed(startTime, endTime) # initialize gdTB command\n", + "rawwind = go.getWind(gaugenumber=0)" + ] + }, + { + "cell_type": "markdown", + "id": "baea545c", + "metadata": {}, + "source": [ + "## Initialize swio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "620a73b2", + "metadata": {}, + "outputs": [], + "source": [ + "swio = wrr.swashIO(workingDirectory=workingDir, testName=testName,\n", + " versionPrefix=versionPrefix, startTime=startTime,\n", + " simulatedRunTime=simulationDuration,\n", + " endTime=endTime, runFlag=Inputs['runFlag'],\n", + " generateFlag=Inputs['generateFlag'], readFlag=Inputs['analyzeFlag'],\n", + " newModelParams=Inputs['modelSettings'])" + ] + }, + { + "cell_type": "markdown", + "id": "7d569842", + "metadata": {}, + "source": [ + "## Get AND prep bathy, prep wave and waterlevel packets formatted for SWASH input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb005d9a", + "metadata": {}, + "outputs": [], + "source": [ + "wavePacket, windPacket, wlPacket, bathyPacket, swio = frontBackNEW.swashSimSetup(Inputs['startTime'],\n", + " inputDict=Inputs,\n", + " allWL=wl,\n", + " allWind=rawwind,\n", + " allWave=wave_spec,\n", + " wrr=swio)" + ] + }, + { + "cell_type": "markdown", + "id": "c456be67", + "metadata": {}, + "source": [ + "## Write SWASH files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e028a830", + "metadata": {}, + "outputs": [], + "source": [ + "swio.writeAllFiles(wavePacket=wavePacket,wlPacket=wlPacket,\n", + " bathyPacket=bathyPacket)" + ] + }, + { + "cell_type": "markdown", + "id": "5e9d0fbe", + "metadata": {}, + "source": [ + "## -----------------------------------------------------------------------------------------\n", + "## Run SWASH" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48b7ce99", + "metadata": {}, + "outputs": [], + "source": [ + "# overwriting below to account for windows running ubuntu shell on windows machine\n", + "swio.runString1 = 'wsl mpiexec -n 18 /mirror/swash/swash.exe INPUT' " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a6691ad", + "metadata": {}, + "outputs": [], + "source": [ + "swio.runSimulation(exe)" + ] + }, + { + "cell_type": "markdown", + "id": "54424000", + "metadata": {}, + "source": [ + "## -----------------------------------------------------------------------------------------\n", + "## Load in results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e89e40b0", + "metadata": {}, + "outputs": [], + "source": [ + "os.chdir(swio.workingDirectory) # add path to results .mat file to directory\n", + "[dataDict, metaDict] = swio.loadSwash_Mat(testName+'.mat') # load in formatted results\n", + " # produces a benign warning" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4c1febe", + "metadata": {}, + "outputs": [], + "source": [ + "fspec, freqs = sbwave.timeSeriesAnalysis1D(dataDict['time'].squeeze(), dataDict['eta'].squeeze(), bandAvg=6)\n", + "Stats = sbwave.stats1D(fspec=fspec, frqbins=freqs, lowFreq=None, highFreq=None)\n", + "HsTS = 4 * np.std(dataDict['eta'].squeeze(), axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22309738", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.plot(dataDict['xFRF'],HsTS)\n", + "plt.ylabel('$H_s$ (m)')\n", + "plt.xlabel('x (m)')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2a38612", + "metadata": {}, + "outputs": [], + "source": [ + "generate_CrossShoreTimeseries(workingDir+'/'+testName+'/img', np.mean(dataDict['eta'].squeeze(), axis=0), dataDict['elevation'], dataDict['xFRF'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ab6b88d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/ww3/ww3_example.ipynb b/notebooks/ww3/ww3_example.ipynb index 83d2e54..67fe75f 100644 --- a/notebooks/ww3/ww3_example.ipynb +++ b/notebooks/ww3/ww3_example.ipynb @@ -2,12 +2,12 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import sys\n", - "sys.path.insert(0, '/Users/rdchlth9/Codes/cmtb_refactor/')\n", + "sys.path.insert(0, '/Users/lszcz/Documents/CMTB/cmtb/')\n", "from getdatatestbed import getDataFRF\n", "from prepdata.prepDataLib import PrepDataTools as preptools\n", "from prepdata import inputOutput\n", @@ -18,15 +18,15 @@ "import netCDF4 as nc4\n", "import os\n", "import f90nml\n", - "from plotting import operationalPlots\n", + "# from plotting import operationalPlots\n", "from testbedutils import fileHandling\n", - "import py2netCDF as p2nc\n", + "# import py2netCDF as p2nc\n", "%matplotlib inline" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -56,23 +56,40 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://chldata.erdc.dren.mil/thredds/dodsC/frf/oceanography/waterlevel/eopNoaaTide/2016/FRF-ocean_waterlevel_eopNoaaTide_201608.nc\n" + ] + } + ], "source": [ "rawWL = go.getWL()" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/lszcz/Documents/CMTB/cmtb\\getdatatestbed\\getDataFRF.py:463: UserWarning: WARNING: getWaveSpec is depreciated, update to use getWaveData, spec=True\n", + " warnings.warn(\"WARNING: getWaveSpec is depreciated, update to use getWaveData, spec=True\")\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - " Removing Duplicates from FRF 8m Array\n" + "https://chldata.erdc.dren.mil/thredds/dodsC/frf/oceanography/waves/waverider-26m/2016/FRF-ocean_waves_waverider-26m_201608.nc\n", + " Removing Duplicates from FRF 26m Datawell Waverider Buoy\n" ] } ], @@ -82,9 +99,17 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://chldata.erdc.dren.mil/thredds/dodsC/frf/meteorology/wind/derived/2016/FRF-met_wind_derived_201608.nc\n" + ] + } + ], "source": [ "rawwind = go.getWind()" ] @@ -98,16 +123,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "outtime = np.arange(d1,d2,dt.timedelta(minutes=30))" + "\n" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -116,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -127,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ @@ -144,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -161,22 +186,22 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -197,14 +222,20 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - " -- 48 wind records with 0 interpolated\n" + "ename": "AssertionError", + "evalue": "time data types must be the same (for comparison)", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mAssertionError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mwind\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mprepdata\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mprep_wind\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mrawwind\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mwindTimeList\u001b[0m\u001b[1;33m,\u001b[0m\u001b[0mmodel\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m'ww3'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32m/Users/lszcz/Documents/CMTB/cmtb\\prepdata\\prepDataLib.py\u001b[0m in \u001b[0;36mprep_wind\u001b[1;34m(self, rawwind, timerecord, **kwargs)\u001b[0m\n\u001b[0;32m 1175\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 1176\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mmodel\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m'ww3'\u001b[0m\u001b[1;33m:\u001b[0m \u001b[1;31m# operates in global coordinate system instead of local coordinate system\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m-> 1177\u001b[1;33m [_, avgspeed, avgDir, flagOut] = self.polartimeavg(timerecord, windTime, windDir,\n\u001b[0m\u001b[0;32m 1178\u001b[0m windSpeed, radin=False)\n\u001b[0;32m 1179\u001b[0m \u001b[0mstdirec\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0manglesLib\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mangle_correct\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mavgDir\u001b[0m\u001b[1;33m+\u001b[0m\u001b[1;36m180\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mrad\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mFalse\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# correct angles to 0-360\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;32m/Users/lszcz/Documents/CMTB/cmtb\\prepdata\\prepDataLib.py\u001b[0m in \u001b[0;36mpolartimeavg\u001b[1;34m(self, trtm, trD, dirD, spdD, radin, interp)\u001b[0m\n\u001b[0;32m 93\u001b[0m \"\"\"\n\u001b[0;32m 94\u001b[0m \u001b[1;32mfrom\u001b[0m \u001b[0mscipy\u001b[0m \u001b[1;32mimport\u001b[0m \u001b[0minterpolate\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 95\u001b[1;33m \u001b[1;32massert\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrD\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mfloat\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[0misinstance\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrtm\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mfloat\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'time data types must be the same (for comparison)'\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 96\u001b[0m \u001b[0mflagLoc\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mzeros\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrtm\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# alloccating for x avgDir\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 97\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msize\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrtm\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;36m1\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mtype\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrtm\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mlist\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mtype\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mtrtm\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mnp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", + "\u001b[1;31mAssertionError\u001b[0m: time data types must be the same (for comparison)" ] } ], @@ -914,9 +945,9 @@ ], "metadata": { "kernelspec": { - "display_name": "cmtb", + "display_name": "Python 3", "language": "python", - "name": "cmtb" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -928,7 +959,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.8" } }, "nbformat": 4, diff --git a/plotting/operationalPlots.py b/plotting/operationalPlots.py index a001b53..83b6dfb 100644 --- a/plotting/operationalPlots.py +++ b/plotting/operationalPlots.py @@ -4,7 +4,7 @@ from matplotlib import image, tri import matplotlib.dates as mdates import matplotlib.image as image -import os, math, glob +import os, math from scipy.interpolate import interpn, RectBivariateSpline from getdatatestbed.getDataFRF import getObs from testbedutils import sblib as sb @@ -12,8 +12,6 @@ import cartopy.crs as ccrs import os, pandas from testbedutils.sblib import statsBryant -import netCDF4 as nc -from matplotlib import colors def unstructuredSpatialPlot(outFname, fieldNc, variable='waveHs', **kwargs): """Plots unstructured data from open netCDF file @@ -85,31 +83,6 @@ def unstructuredSpatialPlot(outFname, fieldNc, variable='waveHs', **kwargs): plt.tight_layout() plt.savefig(outFname) -def makeCrossShoreTimeSeriesPlotAndMovie(ncfile, **kwargs): - figsize = kwargs.get('figsize', (8,4)) - pathBase = kwargs.get('plottingDirectory', '.') - nSubSample = kwargs.get('nSubSample', 5) # data are output at high rate, how often do we want to plot - version_prefix = kwargs.get('versionPrefix', 'base') - xBounds = kwargs.get('xBounds', None) - fnameBase = kwargs.get('fnameBase', f'CMTB_phaseResolved_TimeSeries_{version_prefix}') - time = ncfile['tsTime'][:] - eta = ncfile['eta'][:].squeeze() - bathy = ncfile['elevation'][:].squeeze() - xFRF = ncfile['xFRF'][:] - imgList = [] - for tidx in np.arange(0, len(time), nSubSample).astype(int): - ofPlotName = os.path.join(pathBase, fnameBase + f'_{time[tidx]:08}.png') - generate_CrossShoreTimeseries(ofPlotName, eta[tidx], bathy, xFRF, yBounds=xBounds, figsize=figsize) - imgList.append(ofPlotName) - # now make gif of waves moving across shore - dt = np.median(np.diff(time)) - outfname = os.path.join(pathBase, fnameBase + '.gif') - sb.makegif(imgList, outfname, dt=dt/nSubSample) - sb.myTarMaker(os.path.join(pathBase, fnameBase + '.tar.gz'), imgList) - if xBounds is not None: - imgList = sorted(glob.glob(os.path.join(pathBase, fnameBase+'*.png'))) - sb.makegif(imgList, imgList[0].split('.png')[0]+'.gif', dt=dt/nSubSample) - sb.myTarMaker(os.path.join(pathBase, fnameBase + '.tar.gz'), imgList) # these are all the ones that were formerly in plotFunctions.py def plotTripleSpectra(fnameOut, time, Hs, raw, rot, interp, full=False): @@ -533,36 +506,36 @@ def plotWaveProfile(x, waveHs, bathyToPlot, fname): plt.close() -# these are all the ones that were formerly in CSHORE_plotLib -def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): - """ - This script basically just compares two time series, under - the assmption that one is from the model and one a set of observations +def adjust_plot_ticks(p_dict): + """ This scripts adjusts the axis scale factor of the time series plot in obs_v_mod_ts. - :param file_path: this is the full file-path (string) to the location where the plot will be saved - :param p_dict: has 6 keys to it. - (1) a vector of datetimes ('time') - (2) vector of observations ('obs') - (3) vector of model data ('model') - (4) variable name (string) ('var_name') - (5) variable units (string!!) ('units') -> this will be put inside a tex math environment!!!! - (6) plot title (string) ('p_title') - :return: a model vs. observation time-series plot' - the dictionary of the statistics calculated + Args: - """ - # this function plots observed data vs. model data for time-series data and computes stats for it. + p_dict: has 6 keys to it. + 'time': a vector of datetimes - assert len(p_dict['time']) == len(p_dict['obs']) == len(p_dict['model']), "Your time, model, and observation arrays are not all the same length!" - assert sum([isinstance(p_dict['time'][ss], DT.datetime) for ss in range(0, len(p_dict['time']))]) == len(p_dict['time']), 'Your times input must be an array of datetimes!' + 'obs': vector of observations + + 'model': vector of model data + + 'var_name' (str): variable name + + 'units' (str): variable units -> this will be put inside a tex math environment!!!! + + 'p_title' (str): plot title""" + + assert len(p_dict['time']) == len(p_dict['obs']) == len( + p_dict['model']), "Your time, model, and observation arrays are not all the same length!" + assert sum([isinstance(p_dict['time'][ss], DT.datetime) for ss in range(0, len(p_dict['time']))]) == len( + p_dict['time']), 'Your times input must be an array of datetimes!' # calculate total duration of data to pick ticks for Xaxis on time series plot totalDuration = p_dict['time'][-1] - p_dict['time'][0] if totalDuration.days > 365: # this is a year + of data # mark 7 day increments with monthly major lables - majorTickLocator = mdates.MonthLocator(interval=3) # every 3 months - minorTickLocator = mdates.AutoDateLocator() # DayLocator(7) + majorTickLocator = mdates.MonthLocator(interval=3) # every 3 months + minorTickLocator = mdates.AutoDateLocator() # DayLocator(7) xfmt = mdates.DateFormatter('%Y-%m') - elif totalDuration.days > 30: # thie is months of data that is not a year + elif totalDuration.days > 30: # thie is months of data that is not a year # mark 12 hour with daily major labels majorTickLocator = mdates.DayLocator(1) minorTickLocator = mdates.HourLocator(12) @@ -574,30 +547,36 @@ def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): xfmt = mdates.DateFormatter('%Y-%m-%d %H:%M') else: # mark hourly with 6 hour labels major intervals - tickInterval = 12 # hours? + tickInterval = 6 # hours? majorTickLocator = mdates.HourLocator(interval=tickInterval) minorTickLocator = mdates.HourLocator(1) - xfmt = mdates.DateFormatter('%m/%d\n%H:%M') - # DLY notes 12/17/2018 - I think this tick selection section still needs work, - # it works fine in some cases but terrible in others + xfmt = mdates.DateFormatter('%Y-%m-%d %H:%M') - #################################################################################################################### - # Begin Plot - #################################################################################################################### - fig = plt.figure(figsize=(10, 10)) - if 'p_title' in p_dict.keys(): - fig.suptitle(p_dict['p_title'], fontsize=18, fontweight='bold', verticalalignment='top') + return xfmt, minorTickLocator, majorTickLocator - # time series - ax1 = plt.subplot2grid((2, 2), (0, 0), colspan=2) + +def determine_axis_scale_factor(p_dict): + """ This scripts determines the axis scale factor of the time series plot in obs_v_mod_ts. + + Args: + + p_dict: has 6 keys to it. + 'time': a vector of datetimes + + 'obs': vector of observations + + 'model': vector of model data + + 'var_name' (str): variable name + + 'units' (str): variable units -> this will be put inside a tex math environment!!!! + + 'p_title' (str): plot title""" + + # determine axis scale factor min_val = np.nanmin([np.nanmin(p_dict['obs']), np.nanmin(p_dict['model'])]) max_val = np.nanmax([np.nanmax(p_dict['obs']), np.nanmax(p_dict['model'])]) - if min_val < 0 and max_val > 0: - ax1.plot(p_dict['time'], np.zeros(len(p_dict['time'])), 'k--') - ax1.plot(p_dict['time'], p_dict['obs'], 'r.', label='Observed') - ax1.plot(p_dict['time'], p_dict['model'], 'b.', label='Model') - ax1.set_ylabel('%s [$%s$]' % (p_dict['var_name'], p_dict['units']), fontsize=16) - # determine axis scale factor + if min_val >= 0: sf1 = 0.9 else: @@ -606,24 +585,145 @@ def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): sf2 = 1.1 else: sf2 = 0.9 + + return min_val, max_val, sf1, sf2 + + +def plot_string_message(p_dict, stats_dict): + """ This script creates the statistics string used for the one:one plot in obs_v_mod_ts + + Args: + + p_dict: has 6 keys to it. + 'time': a vector of datetimes + + 'obs': vector of observations + + 'model': vector of model data + + 'var_name' (str): variable name + + 'units' (str): variable units -> this will be put inside a tex math environment!!!! + + 'p_title' (str): plot title""" + + + header_str = '%s Comparison \nModel to Observations:' % (p_dict['var_name']) + m_mean_str = '\n Model Mean $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['m_mean']), p_dict['units']) + o_mean_str = '\n Observation Mean $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['o_mean']), p_dict['units']) + bias_str = '\n Bias $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['bias']), p_dict['units']) + RMSE_str = '\n RMSE $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['RMSE']), p_dict['units']) + SI_str = '\n Similarity Index $=%s$' % ("{0:.2f}".format(stats_dict['scatterIndex'])) + sym_slp_str = '\n Symmetric Slope $=%s$' % ("{0:.2f}".format(stats_dict['symSlope'])) + corr_coef_str = '\n Correlation Coefficient $=%s$' % ("{0:.2f}".format(stats_dict['corr'])) + RMSE_Norm_str = '\n %%RMSE $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['RMSEnorm']), p_dict['units']) + num_String = '\n Number of samples $= %s$' % len(stats_dict['residuals']) + plot_str = m_mean_str + o_mean_str + bias_str + RMSE_str + SI_str + sym_slp_str + corr_coef_str + RMSE_Norm_str + num_String + + return plot_str, header_str + +def statistics_dictionary_p_dict(p_dict): + """ This script calls statsBryant function and creates stats_dict (used in obs_v_mod_ts, + obs_v_mod_bathy, and obs_v_mod_bathy_TN) + + Args: + + p_dict: has 6 keys to it. + 'time': a vector of datetimes + + 'obs': vector of observations + + 'model': vector of model data + + 'var_name' (str): variable name + + 'units' (str): variable units -> this will be put inside a tex math environment!!!! + + 'p_title' (str): plot title""" + + stats_dict = {} + if isinstance(p_dict['obs'], np.ma.masked_array) and ~p_dict['obs'].mask.any(): + p_dict['obs'] = np.array(p_dict['obs']) + stats_dict = statsBryant(p_dict['obs'], p_dict['model']) + stats_dict['m_mean'] = np.nanmean(p_dict['model']) + stats_dict['o_mean'] = np.nanmean(p_dict['obs']) + del stats_dict['meta'] # remove meta key from stats_dict + + return stats_dict + + +def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): + """This script basically just compares two time series, under + the assmption that one is from the model and one a set of observations + + Args: + file_path: this is the full file-path (string) to the location where the plot will be saved + p_dict: has 6 keys to it. + 'time': a vector of datetimes + + 'obs': vector of observations + + 'model': vector of model data + + 'var_name' (str): variable name + + 'units' (str): variable units -> this will be put inside a tex math environment!!!! + + 'p_title' (str): plot title + + ofname: output file name + logo_path: path to a small logo to put at the bottom of the figure (Default value = 'ArchiveFolder/CHL_logo.png') + + Returns: + a model vs. observation time-series plot' + the dictionary of the statistics calculated + + """ + # this function plots observed data vs. model data for time-series data and computes stats for it. + + #################################################################################################################### + # Begin Plot + #################################################################################################################### + fig = plt.figure(figsize=(10, 10)) + fig.suptitle(p_dict['p_title'], fontsize=18, fontweight='bold', verticalalignment='top') + + # time series + ax1 = plt.subplot2grid((2, 2), (0, 0), colspan=2) + + min_val, max_val, sf1, sf2 = determine_axis_scale_factor(p_dict) + xfmt, minorTickLocator, majorTickLocator = adjust_plot_ticks(p_dict) + + if min_val < 0 and max_val > 0: + base_date = min(p_dict['time']) - DT.timedelta( + seconds=0.5 * (p_dict['time'][1] - p_dict['time'][0]).total_seconds()) + base_times = np.array( + [base_date + DT.timedelta(seconds=n * (p_dict['time'][1] - p_dict['time'][0]).total_seconds()) for n in + range(0, len(p_dict['time']) + 1)]) + ax1.plot(base_times, np.zeros(len(base_times)), 'k--') + + plt.grid() + ax1.scatter(p_dict['time'], p_dict['obs'], s=75, c='r', marker='o', label='Observed') + ax1.scatter(p_dict['time'], p_dict['model'], s=75, c='b', marker='o', label='Model') + ax1.set_ylabel('%s [$%s$]' % (p_dict['var_name'], p_dict['units']), fontsize=16) + ax1.set_ylim([sf1 * min_val, sf2 * max_val]) - ax1.set_xlim([min(p_dict['time']) - DT.timedelta(seconds=0.5 * (p_dict['time'][1] - p_dict['time'][0]).total_seconds()), - max(p_dict['time']) + DT.timedelta(seconds=0.5 * (p_dict['time'][1] - p_dict['time'][0]).total_seconds())]) + ax1.set_xlim( + [min(p_dict['time']) - DT.timedelta(seconds=0.5 * (p_dict['time'][1] - p_dict['time'][0]).total_seconds()), + max(p_dict['time']) + DT.timedelta(seconds=0.5 * (p_dict['time'][1] - p_dict['time'][0]).total_seconds())]) # this is what you change for time-series x-axis ticks!!!!! - # # ax1.xaxis.set_major_locator(majorTickLocator) # ax1.xaxis.set_minor_locator(minorTickLocator) # ax1.xaxis.set_major_formatter(xfmt) + for tick in ax1.xaxis.get_major_ticks(): tick.label.set_fontsize(14) for tick in ax1.yaxis.get_major_ticks(): tick.label.set_fontsize(14) - ax1.minorticks_off() ax1.tick_params(labelsize=14) - plt.legend(bbox_to_anchor=(0., 1.02, 1., 0.102), loc=3, ncol=3, borderaxespad=0., fontsize=14) - fig.autofmt_xdate() + plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=3, borderaxespad=0., fontsize=14) + # Now working on the 1-1 comparison subplot one_one = np.linspace(min_val - 0.05 * (max_val - min_val), max_val + 0.05 * (max_val - min_val), 100) ax2 = plt.subplot2grid((2, 2), (1, 0), colspan=1) @@ -631,7 +731,7 @@ def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): if min_val < 0 and max_val > 0: ax2.plot(one_one, np.zeros(len(one_one)), 'k--') ax2.plot(np.zeros(len(one_one)), one_one, 'k--') - ax2.plot(p_dict['obs'], p_dict['model'], 'r*') + ax2.scatter(p_dict['obs'], p_dict['model'], s=75, c='r', marker='*') ax2.set_xlabel('Observed %s [$%s$]' % (p_dict['var_name'], p_dict['units']), fontsize=16) ax2.set_ylabel('Model %s [$%s$]' % (p_dict['var_name'], p_dict['units']), fontsize=16) ax2.set_xlim([min_val - 0.025 * (max_val - min_val), max_val + 0.025 * (max_val - min_val)]) @@ -643,45 +743,36 @@ def obs_V_mod_TS(ofname, p_dict, logo_path='ArchiveFolder/CHL_logo.png'): ax2.tick_params(labelsize=14) plt.legend(loc=0, ncol=1, borderaxespad=0.5, fontsize=14) - # stats and stats text - stats_dict = statsBryant(p_dict['obs'], p_dict['model']) - stats_dict['m_mean'] = np.nanmean(p_dict['model']) - stats_dict['o_mean'] = np.nanmean(p_dict['obs']) - - header_str = '%s Comparison \nModel to Observations:' % (p_dict['var_name']) - m_mean_str = '\n Model Mean $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['m_mean']), p_dict['units']) - o_mean_str = '\n Observation Mean $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['o_mean']), p_dict['units']) - bias_str = '\n Bias $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['bias']), p_dict['units']) - RMSE_str = '\n RMSE $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['RMSE']), p_dict['units']) - SI_str = '\n Similarity Index $=%s$' % ("{0:.2f}".format(stats_dict['scatterIndex'])) - sym_slp_str = '\n Symmetric Slope $=%s$' % ("{0:.2f}".format(stats_dict['symSlope'])) - corr_coef_str = '\n Correlation Coefficient $=%s$' % ("{0:.2f}".format(stats_dict['corr'])) - RMSE_Norm_str = '\n %%RMSE $=%s$ $(%s)$' % ("{0:.2f}".format(stats_dict['RMSEnorm']), p_dict['units']) + stats_dict = statistics_dictionary_p_dict(p_dict) + plot_str, header_str = plot_string_message(p_dict, stats_dict) - num_String = '\n Number of samples $= %s$' %len(stats_dict['residuals']) - plot_str = m_mean_str + o_mean_str + bias_str + RMSE_str + RMSE_Norm_str + SI_str + sym_slp_str + corr_coef_str + num_String ax3 = plt.subplot2grid((2, 2), (1, 1), colspan=1) + ax3.axis('off') ax4 = ax3.twinx() ax3.axis('off') - ax4.axis('off') + try: - CHL_logo = image.imread(logo_path) + ax4.axis('off') + CHL_logo = image.imread(os.path.join(logo_path)) ax4 = fig.add_axes([0.78, 0.02, 0.20, 0.20], anchor='SE', zorder=-1) ax4.imshow(CHL_logo) ax4.axis('off') except: print('Plot generated sans CHL Logo!') + ax3.axis('off') ax3.text(0.01, 0.99, header_str, verticalalignment='top', horizontalalignment='left', color='black', fontsize=18, fontweight='bold') ax3.text(0.01, 0.90, plot_str, verticalalignment='top', horizontalalignment='left', color='black', fontsize=16) fig.subplots_adjust(wspace=0.4, hspace=0.1) - # fig.tight_layout(pad=1, h_pad=2.5, w_pad=1, rect=[0.0, 0.0, 1.0, 0.925]) - fig.savefig(ofname, dpi=300) + fig.tight_layout(pad=1, h_pad=2.5, w_pad=1, rect=[0.0, 0.0, 1.0, 0.925]) + fig.savefig(ofname, dpi=80) plt.close() + return stats_dict + def bc_plot(ofname, p_dict): """ This is the script to plot some information about the boundary conditions that were put into the CSHORE infile.. @@ -995,7 +1086,8 @@ def obs_V_mod_bathy(ofname, p_dict, obs_dict, logo_path='ArchiveFolder/CHL_logo. plt.legend(loc=0, ncol=1, borderaxespad=0.5, fontsize=14) # stats and stats text - stats_dict = sb.statsBryant(models=p_dict['model'], observations=p_dict['obs']) + #stats_dict = sb.statsBryant(models=p_dict['model'], observations=p_dict['obs']) + stats_dict = statistics_dictionary_p_dict(p_dict) # volume change, shallow index_XXm = np.min(np.argwhere(p_dict[ @@ -2212,7 +2304,9 @@ def generate_CrossShoreTimeseries(ofname, dataIn, bottomIn, xIn, **kwargs): a plot """ - yBounds = kwargs.get('yBounds', None) + xmin = kwargs.get('xmin', np.min(xIn)) + xmax = kwargs.get('xmin', np.max(xIn)) + figsize = kwargs.get('figsize', (8,4)) beachColor = 'wheat' skyColor = 'aquamarine' @@ -2224,23 +2318,18 @@ def generate_CrossShoreTimeseries(ofname, dataIn, bottomIn, xIn, **kwargs): plt.figure(figsize=figsize) ax1 = plt.subplot(111) ax1.set_facecolor(skyColor) - ax1.plot(xIn, dataIn, color=waterColor) # plot water line + ax1.plot(xIn, dataIn) # plot water line ax1.plot(xIn, bottomIn, color=beachColor) # plot beach ax1.fill_betweenx(bottomIn, xIn, color=beachColor) # fill in beach ax1.fill_between(xIn, bottomIn, dataIn, color=waterColor) # fill in water - ax1.set_xlim([np.min(xIn), np.max(xIn)]) + ax1.set_xlim([xmin, xmax]) ax1.set_ylim([np.min(bottomIn), np.max(bottomIn) + 0.5]) - ax1.set_xlabel('x') - ax1.set_ylabel('elevation') + plt.savefig(ofname) - - if yBounds is not None: - ax1.set_xlim(yBounds) - plt.savefig(f"{ofname.split('.png')[0]}_yMin{yBounds[0]}_yMax{yBounds[-1]}.png") plt.close() -def plotCrossShoreSummaryTS(ofname, ncfile=None, **kwargs): +def plotCrossShoreSummaryTS(ofname, xFRF, bathy, totalStatisticDict, SeaSwellStats, IGstats, setup, WL, **kwargs): """ plots a 4 panel plot summary of cross-shore performance of model that can resolve IG Args: @@ -2261,25 +2350,9 @@ def plotCrossShoreSummaryTS(ofname, ncfile=None, **kwargs): """ obs = kwargs.get('obs', None) - + HsTS = kwargs.get('HsTs', None) fs = kwargs.get('fontSize', 12) var = kwargs.get('plotVar', 'Hm0') - if ncfile is None: - xFRF = kwargs.get('xFRF', None) - totalStatisticDict = kwargs.get('totalStatisticDict', None) - SeaSwellStats = kwargs.get('Hs_ss', None) - IGstats = kwargs.get('IGstats', None) - setup = kwargs.get('setup', None) - WL = kwargs.get('WL', None) - else: - xFRF = ncfile['xFRF'][:].squeeze() - bathy = ncfile['elevation'][:].squeeze() - Hs_total = ncfile['waveHs'][:].squeeze() - Hs_ss= kwargs.get('Hs_ss', np.ma.empty_like(xFRF)) - Hs_IG = ncfile['waveHsIG'][:].squeeze() - setup = np.nanmean(ncfile['eta'][:].squeeze(), axis=0) - WL = kwargs.get('WL', None) - beachColor = 'wheat' waterColor = 'aquamarine' setupColor = 'green' @@ -2291,23 +2364,24 @@ def plotCrossShoreSummaryTS(ofname, ncfile=None, **kwargs): ########### make Figure ######################## plt.figure(figsize=figsize); ax1 = plt.subplot(int(size + '1')) - - ax1.plot(xFRF, Hs_total, label='$Hs_{Total}$') - ax1.plot(xFRF, Hs_ss, label='$Hs_{seaSwell}$') - ax1.plot(xFRF, Hs_IG, label='$Hs_{IG}$') + if HsTS is not None: + ax1.plot(xFRF, HsTS, label='$Hs_{Ts}$') + ax1.plot(xFRF, totalStatisticDict[var], label='$Hs_{Total}$') + ax1.plot(xFRF, SeaSwellStats[var], label='$Hs_{seaSwell}$') + ax1.plot(xFRF, IGstats[var], label='$Hs_{IG}$') ax1.legend(loc='upper left', fontsize=fs) ax1.set_ylabel('Wave Height $[m]$', fontsize=fs) ax2 = plt.subplot(int(size + '2')) - ax2.plot(xFRF, Hs_IG, label='$Hs_{IG}$') + ax2.plot(xFRF, IGstats[var], label='$Hs_{IG}$') ax2.set_ylabel('IG wave Height', fontsize=fs) ax3 = plt.subplot(int(size + '3')) ax3.plot(xFRF, setup) - ax3.set_ylabel('$setup$', fontsize=fs) + ax3.set_ylabel('$\eta$', fontsize=fs) ax4 = plt.subplot(int(size + '4')) - ax4.plot(xFRF, bathy, '-', lw=7, color=beachColor) + ax4.plot(xFRF, -bathy,'-', lw=7, color=beachColor) ax4.plot(xFRF, np.tile(WL, (xFRF.shape[0])), color=waterColor, label='Water Level') ax4.plot(xFRF, setup, color=setupColor, label='TWL') ax4.set_ylabel('Z NAVD88 - [m]') @@ -2328,14 +2402,9 @@ def crossShoreSurfaceTS2D(ofname, eta, xFRF, time): Returns: """ - eta = eta.squeeze() + eta= eta.squeeze() plt.figure() - surf = plt.pcolormesh(xFRF, time, eta, cmap='RdBu', norm=colors.TwoSlopeNorm(vcenter=0)) - cbar = plt.colorbar(mappable=surf) - cbar.set_label('Water Elevation') - plt.xlabel('Cross-shore Position [m]') - plt.ylabel('time [s]') - plt.tight_layout() + plt.pcolormesh(xFRF, time, eta, cmap='RdBu', shading='auto') plt.savefig(ofname) plt.close() @@ -2354,15 +2423,12 @@ def crossShoreSpectrograph(ofname, xFRF, freqs, fspec, **kwargs): a plot """ - ylims = kwargs.get('ylims', (freqs[0], 0.4)) - plt.figure() + ylims = kwargs.get('ylims', (0, 0.4)) + plt.figure(); plt.pcolormesh(xFRF, freqs, fspec.T) - a = plt.colorbar() - plt.yscale('log') - a.set_label('Energy Density $[m^2/s]$') - plt.ylabel('Frequency', fontsize=12) - plt.xlabel('Cross-shore location', fontsize=12) + plt.colorbar() + plt.ylabel('frequency', fontsize=12) + plt.xlabel('cross-shore location', fontsize=12) plt.ylim(ylims) - plt.tight_layout() plt.savefig(ofname); plt.close(); diff --git a/testbedutils b/testbedutils index b12bbba..6de568d 160000 --- a/testbedutils +++ b/testbedutils @@ -1 +1 @@ -Subproject commit b12bbba4803d0a9cb3935c7f138ed3226c3390d7 +Subproject commit 6de568dd6e881aa4e864f533133fe77ad66940cc