├── .gitignore ├── PWP.py ├── PWP_helper.py ├── README.md ├── docs └── HYCOM_pwp_doc.pdf ├── example_plots ├── initial_final_TS_profiles_demo2_1e6diff.png ├── initial_final_TS_profiles_demo2_1e6diff_empOFF.png ├── initial_final_TS_profiles_demo2_1e6diff_windsOFF.png ├── surface_forcing_demo2_1e6diff.png ├── surface_forcing_demo2_1e6diff_empOFF.png └── surface_forcing_demo2_1e6diff_windsOFF.png ├── input_data ├── SO_met_100day.nc ├── SO_met_30day.nc ├── SO_profile1.nc ├── beaufort_met.nc └── beaufort_profile.nc ├── matlab_files ├── PWP_Byron.m ├── met.mat ├── output.mat └── prof.mat ├── output └── pwp_output.nc └── plots ├── initial_final_TS_profiles_demo1_1e6diff.png ├── initial_final_TS_profiles_demo1_nodiff.png ├── initial_final_TS_profiles_demo2_1e6diff.png ├── surface_forcing_demo1.png └── surface_forcing_demo2.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | *.p 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | ehthumbs.db 38 | Thumbs.db 39 | pwp_output_diff.nc 40 | output 41 | plots 42 | pwp_soccom.py 43 | personal_modules.py 44 | pwp_argo.py 45 | *.bak 46 | 47 | ##directories 48 | plots/* 49 | old_files/* 50 | -------------------------------------------------------------------------------- /PWP.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a Python implementation of the Price Weller Pinkel (PWP) ocean mixed layer model. 3 | This code is based on the MATLAB implementation of the PWP model originally 4 | written by Peter Lazarevich and Scott Stoermer (http://www.po.gso.uri.edu/rafos/research/pwp/) 5 | (U. Rhode Island) and later modified by Byron Kilbourne (University of Washington) 6 | and Sarah Dewey (University of Washington). 7 | 8 | The run() function provides an outline of how the code works. 9 | 10 | Earle Wilson 11 | School of Oceanography 12 | University of Washington 13 | April 18, 2016 14 | """ 15 | 16 | import numpy as np 17 | import seawater as sw 18 | import matplotlib.pyplot as plt 19 | import xarray as xr 20 | import pickle 21 | import timeit 22 | import os 23 | from datetime import datetime 24 | import PWP_helper as phf 25 | import imp 26 | 27 | 28 | imp.reload(phf) 29 | 30 | #from IPython.core.debugger import Tracer 31 | #debug_here = set_trace 32 | 33 | def run(met_data, prof_data, param_kwds=None, overwrite=True, diagnostics=False, suffix='', save_plots=False): 34 | 35 | #TODO: move this to the helper file 36 | """ 37 | This is the main controller function for the model. The flow of the algorithm 38 | is as follows: 39 | 40 | 1) Set model parameters (see set_params function in PWP_helper.py). 41 | 2) Read in forcing and initial profile data. 42 | 3) Prepare forcing and profile data for model run (see prep_data in PWP_helper.py). 43 | 3.1) Interpolate forcing data to prescribed time increments. 44 | 3.2) Interpolate profile data to prescribed depth increments. 45 | 3.3) Initialize model output variables. 46 | 4) Iterate the PWP model specified time interval: 47 | 4.1) apply heat and salt fluxes 48 | 4.2) rotate, adjust to wind, rotate 49 | 4.3) apply bulk Richardson number mixing 50 | 4.4) apply gradient Richardson number mixing 51 | 4.5) apply drag associated with internal wave dissipation 52 | 4.5) apply diapycnal diffusion 53 | 5) Save results to output file 54 | 55 | Input: 56 | met_data - path to netCDF file containing forcing/meterological data. This file must be in the 57 | input_data/ directory. 58 | 59 | The data fields should include 'time', 'sw', 'lw', 'qlat', 'qsens', 'tx', 60 | 'ty', and 'precip'. These fields should store 1-D time series of the same 61 | length. 62 | 63 | The model expects positive heat flux values to represent ocean warming. The time 64 | data field should contain a 1-D array representing fraction of day. For example, 65 | for 6 hourly data, met_data['time'] should contain a number series that increases 66 | in steps of 0.25, such as np.array([1.0, 1.25, 1.75, 2.0, 2.25...]). 67 | 68 | See https://github.com/earlew/pwp_python#input-data for more info about the 69 | expect intput data. 70 | 71 | prof_data - path to netCDF file containing initial profile data. This must be in input_data/ directory. 72 | The fields of this dataset should include: 73 | ['z', 't', 's', 'lat']. These represent 1-D vertical profiles of temperature, 74 | salinity and density. 'lat' is expected to be a length=1 array-like object. e.g. 75 | prof_data['lat'] = [25.0]. 76 | 77 | See https://github.com/earlew/pwp_python#input-data for more info about the 78 | expect intput data. 79 | 80 | overwrite - controls the naming of output file. If True, the same filename is used for 81 | every model run. If False, a unique time_stamp is generated and appended 82 | to the file name. Default is True. 83 | 84 | diagnostics - if True, the code will generate live plots of mixed layer properties at 85 | each time step. 86 | 87 | suffix - string to add to the end of filenames. e.g. suffix = 'nodiff' leads to 'pwp_out_nodiff.nc. 88 | default is an empty string ''. 89 | 90 | save_plots -this gets passed on to the makeSomePlots() function in the PWP_helper. If True, the code 91 | saves the generated plots. Default is False. 92 | 93 | param_kwds -dict containing keyword arguments for set_params function. See PWP_helper.set_params() 94 | for more details. If None, default parameters are used. Default is None. 95 | 96 | Output: 97 | 98 | forcing, pwp_out = PWP.run() 99 | 100 | forcing: a dictionary containing the interpolated surface forcing. 101 | pwp_out: a dictionary containing the solutions generated by the model. 102 | 103 | This script also saves the following to file: 104 | 105 | 'pwp_output.nc'- a netCDF containing the output generated by the model. 106 | 'pwp_output.p' - a pickle file containing the output generated by the model. 107 | 'forcing.p' - a pickle file containing the (interpolated) forcing used for the model run. 108 | If overwrite is set to False, a timestamp will be added to these file names. 109 | 110 | ------------------------------------------------------------------------------ 111 | There are two ways to run the model: 112 | 1. You can run the model by typing "python PWP.py" from the bash command line. This 113 | will initiate this function with the set defaults. Typing "%run PWP" from the ipython 114 | command line will do the same thing. 115 | 116 | 2. You can also import the module then call the run() function specifically. For example, 117 | >> import PWP 118 | >> forcing, pwp_out = PWP.run() 119 | Alternatively, if you want to change the defaults... 120 | >> forcing, pwp_out = PWP.run(met_data='new_forcing.nc', overwrite=False, diagnostics=False) 121 | 122 | This is a more interactive approach as it provides direct access to all of the model's 123 | subfunctions. 124 | 125 | """ 126 | 127 | #close all figures 128 | plt.close('all') 129 | 130 | #start timer 131 | t0 = timeit.default_timer() 132 | 133 | ## Get surface forcing and profile data 134 | # These are x-ray datasets, but you can treat them as dicts. 135 | # Do met_dset.keys() to explore the data fields 136 | met_dset = xr.open_dataset('input_data/%s' %met_data) 137 | prof_dset = xr.open_dataset('input_data/%s' %prof_data) 138 | 139 | ## get model parameters and constants (read docs for set_params function) 140 | lat = prof_dset['lat'] #needed to compute internal wave dissipation 141 | if param_kwds is None: 142 | params = phf.set_params(lat=lat) 143 | else: 144 | param_kwds['lat'] = lat 145 | params = phf.set_params(**param_kwds) 146 | 147 | ## prep forcing and initial profile data for model run (see prep_data function for more details) 148 | forcing, pwp_out, params = phf.prep_data(met_dset, prof_dset, params) 149 | 150 | ## run the model 151 | pwp_out = pwpgo(forcing, params, pwp_out, diagnostics) 152 | 153 | 154 | ## write output to disk 155 | if overwrite: 156 | time_stamp = '' 157 | else: 158 | #use unique time stamp 159 | time_stamp = datetime.now().strftime("_%Y%m%d_%H%M") 160 | 161 | if len(suffix)>0 and suffix[0] != '_': 162 | suffix = '_%s' %suffix 163 | 164 | # save output as netCDF file 165 | pwp_out_ds = xr.Dataset({'temp': (['z', 'time'], pwp_out['temp']), 'sal': (['z', 'time'], pwp_out['sal']), 166 | 'uvel': (['z', 'time'], pwp_out['uvel']), 'vvel': (['z', 'time'], pwp_out['vvel']), 167 | 'dens': (['z', 'time'], pwp_out['dens']), 'mld': (['time'], pwp_out['mld'])}, 168 | coords={'z': pwp_out['z'], 'time': pwp_out['time']}) 169 | 170 | pwp_out_ds.to_netcdf("output/pwp_output%s%s.nc" %(suffix, time_stamp)) 171 | 172 | # also output and forcing as pickle file 173 | pickle.dump(forcing, open( "output/forcing%s%s.p" %(suffix, time_stamp), "wb" )) 174 | pickle.dump(pwp_out, open( "output/pwp_out%s%s.p" %(suffix, time_stamp), "wb" )) 175 | 176 | #check timer 177 | tnow = timeit.default_timer() 178 | t_elapsed = (tnow - t0) 179 | print("Time elapsed: %i minutes and %i seconds" %(np.floor(t_elapsed/60), t_elapsed%60)) 180 | 181 | ## do analysis of the results 182 | phf.makeSomePlots(forcing, pwp_out, suffix=suffix, save_plots=save_plots) 183 | 184 | return forcing, pwp_out 185 | 186 | def pwpgo(forcing, params, pwp_out, diagnostics): 187 | 188 | """ 189 | This is the main driver of the PWP module. 190 | """ 191 | 192 | #unpack some of the variables 193 | #This is not necessary, but I don't want to update all the variable names just yet. 194 | q_in = forcing['q_in'] 195 | q_out = forcing['q_out'] 196 | emp = forcing['emp'] 197 | taux = forcing['tx'] 198 | tauy = forcing['ty'] 199 | absrb = forcing['absrb'] 200 | 201 | z = pwp_out['z'] 202 | dz = pwp_out['dz'] 203 | dt = pwp_out['dt'] 204 | zlen = len(z) 205 | tlen = len(pwp_out['time']) 206 | 207 | rb = params['rb'] 208 | rg = params['rg'] 209 | f = params['f'] 210 | cpw = params['cpw'] 211 | g = params['g'] 212 | ucon = params['ucon'] 213 | 214 | printDragWarning = True 215 | 216 | print("Number of time steps: %s" %tlen) 217 | 218 | for n in range(1,tlen): 219 | percent_comp = 100*n/float(tlen) 220 | print('Loop iter. %s (%.1f %%)' %(n, percent_comp)) 221 | 222 | #select for previous profile data 223 | temp = pwp_out['temp'][:, n-1] 224 | sal = pwp_out['sal'][:, n-1] 225 | dens = pwp_out['dens'][:, n-1] 226 | uvel = pwp_out['uvel'][:, n-1] 227 | vvel = pwp_out['vvel'][:, n-1] 228 | 229 | ### Absorb solar radiation and FWF in surf layer ### 230 | 231 | #save initial T,S (may not be necessary) 232 | temp_old = pwp_out['temp'][0, n-1] 233 | sal_old = pwp_out['sal'][0, n-1] 234 | 235 | #update layer 1 temp and sal 236 | temp[0] = temp[0] + (q_in[n-1]*absrb[0]-q_out[n-1])*dt/(dz*dens[0]*cpw) 237 | #sal[0] = sal[0]/(1-emp[n-1]*dt/dz) 238 | sal[0] = sal[0] + sal[0]*emp[n-1]*dt/dz 239 | 240 | # debug_here() 241 | 242 | #check if temp is less than freezing point 243 | T_fz = sw.fp(sal_old, 1) #why use sal_old? Need to recheck 244 | if temp[0] < T_fz: 245 | temp[0] = T_fz 246 | 247 | ### Absorb rad. at depth ### 248 | temp[1:] = temp[1:] + q_in[n-1]*absrb[1:]*dt/(dz*dens[1:]*cpw) 249 | 250 | ### compute new density ### 251 | dens = sw.dens0(sal, temp) 252 | 253 | ### relieve static instability ### 254 | temp, sal, dens, uvel, vvel = remove_si(temp, sal, dens, uvel, vvel) 255 | 256 | ### Compute MLD ### 257 | #find ml index 258 | ml_thresh = params['mld_thresh'] 259 | mld_idx = np.flatnonzero(dens-dens[0]>ml_thresh)[0] #finds the first index that exceed ML threshold 260 | 261 | #check to ensure that ML is defined 262 | assert mld_idx.size != 0, "Error: Mixed layer depth is undefined." 263 | 264 | #get surf MLD 265 | mld = z[mld_idx] 266 | 267 | ### Rotate u,v do wind input, rotate again, apply mixing ### 268 | ang = -f*dt/2 269 | uvel, vvel = rot(uvel, vvel, ang) 270 | du = (taux[n-1]/(mld*dens[0]))*dt 271 | dv = (tauy[n-1]/(mld*dens[0]))*dt 272 | uvel[:mld_idx] = uvel[:mld_idx]+du 273 | vvel[:mld_idx] = vvel[:mld_idx]+dv 274 | 275 | 276 | ### Apply drag to current ### 277 | #Original comment: this is a horrible parameterization of inertial-internal wave dispersion 278 | if params['drag_ON']: 279 | if ucon > 1e-10: 280 | uvel = uvel*(1-dt*ucon) 281 | vvel = vvel*(1-dt*ucon) 282 | else: 283 | if printDragWarning: 284 | print("Warning: Parameterization for inertial-internal wave dispersion is turned off.") 285 | printDragWarning = False 286 | 287 | uvel, vvel = rot(uvel, vvel, ang) 288 | 289 | ### Apply Bulk Richardson number instability form of mixing (as in PWP) ### 290 | if rb > 1e-5: 291 | temp, sal, dens, uvel, vvel = bulk_mix(temp, sal, dens, uvel, vvel, g, rb, zlen, z, mld_idx) 292 | 293 | ### Do the gradient Richardson number instability form of mixing ### 294 | if rg > 0: 295 | temp, sal, dens, uvel, vvel = grad_mix(temp, sal, dens, uvel, vvel, dz, g, rg, zlen) 296 | 297 | 298 | ### Apply diffusion ### 299 | if params['rkz'] > 0: 300 | temp = diffus(params['dstab'], zlen, temp) 301 | sal = diffus(params['dstab'], zlen, sal) 302 | dens = sw.dens0(sal, temp) 303 | uvel = diffus(params['dstab'], zlen, uvel) 304 | vvel = diffus(params['dstab'], zlen, vvel) 305 | 306 | ### update output profile data ### 307 | pwp_out['temp'][:, n] = temp 308 | pwp_out['sal'][:, n] = sal 309 | pwp_out['dens'][:, n] = dens 310 | pwp_out['uvel'][:, n] = uvel 311 | pwp_out['vvel'][:, n] = vvel 312 | pwp_out['mld'][n] = mld 313 | 314 | #do diagnostics 315 | if diagnostics==1: 316 | phf.livePlots(pwp_out, n) 317 | 318 | return pwp_out 319 | 320 | 321 | def absorb(beta1, beta2, zlen, dz): 322 | 323 | # Compute solar radiation absorption profile. This 324 | # subroutine assumes two wavelengths, and a double 325 | # exponential depth dependence for absorption. 326 | # 327 | # Subscript 1 is for red, non-penetrating light, and 328 | # 2 is for blue, penetrating light. rs1 is the fraction 329 | # assumed to be red. 330 | 331 | rs1 = 0.6 332 | rs2 = 1.0-rs1 333 | z1 = np.arange(0,zlen)*dz 334 | z2 = z1 + dz 335 | z1b1 = z1/beta1 336 | z2b1 = z2/beta1 337 | z1b2 = z1/beta2 338 | z2b2 = z2/beta2 339 | absrb = rs1*(np.exp(-z1b1)-np.exp(-z2b1))+rs2*(np.exp(-z1b2)-np.exp(-z2b2)) 340 | 341 | return absrb 342 | 343 | def remove_si(t, s, d, u, v): 344 | 345 | # Find and relieve static instability that may occur in the 346 | # density array 'd'. This simulates free convection. 347 | # ml_index is the index of the depth of the surface mixed layer after adjustment, 348 | 349 | stat_unstable = True 350 | 351 | while stat_unstable: 352 | 353 | d_diff = np.diff(d) 354 | if np.any(d_diff<0): 355 | stat_unstable=True 356 | first_inst_idx = np.flatnonzero(d_diff<0)[0] 357 | d0 = d 358 | (t, s, d, u, v) = mix5(t, s, d, u, v, first_inst_idx+1) 359 | 360 | #plot density 361 | # plt.figure(num=86) 362 | # plt.clf() #probably redundant 363 | # plt.plot(d0-1000, range(len(d0)), 'b-', label='pre-mix') 364 | # plt.plot(d-1000, range(len(d0)), 'r-', label='post-mix') 365 | # plt.gca().invert_yaxis() 366 | # plt.xlabel('Density-1000 (kg/m3)') 367 | # plt.grid(True) 368 | # plt.pause(0.05) 369 | # plt.show() 370 | 371 | #debug_here() 372 | else: 373 | stat_unstable = False 374 | 375 | return t, s, d, u, v 376 | 377 | 378 | def mix5(t, s, d, u, v, j): 379 | 380 | #This subroutine mixes the arrays t, s, u, v down to level j. 381 | j = j+1 #so that the j-th layer is included in the mixing 382 | t[:j] = np.mean(t[:j]) 383 | s[:j] = np.mean(s[:j]) 384 | d[:j] = sw.dens0(s[:j], t[:j]) 385 | u[:j] = np.mean(u[:j]) 386 | v[:j] = np.mean(v[:j]) 387 | 388 | return t, s, d, u, v 389 | 390 | def rot(u, v, ang): 391 | 392 | #This subroutine rotates the vector (u,v) through an angle, ang 393 | r = (u+1j*v)*np.exp(1j*ang) 394 | u = r.real 395 | v = r.imag 396 | 397 | return u, v 398 | 399 | def bulk_mix(t, s, d, u, v, g, rb, nz, z, mld_idx): 400 | #sub-routine to do bulk richardson mixing 401 | 402 | rvc = rb #critical rich number?? 403 | 404 | for j in range(mld_idx, nz): 405 | h = z[j] 406 | #it looks like density and velocity are mixed from the surface down to the ML depth 407 | dd = (d[j]-d[0])/d[0] 408 | dv = (u[j]-u[0])**2+(v[j]-v[0])**2 409 | if dv == 0: 410 | rv = np.inf 411 | else: 412 | rv = g*h*dd/dv 413 | 414 | if rv > rvc: 415 | break 416 | else: 417 | t, s, d, u, v = mix5(t, s, d, u, v, j) 418 | 419 | return t, s, d, u, v 420 | 421 | def grad_mix(t, s, d, u, v, dz, g, rg, nz): 422 | 423 | #copied from source script: 424 | # % This function performs the gradeint Richardson Number relaxation 425 | # % by mixing adjacent cells just enough to bring them to a new 426 | # % Richardson Number. 427 | # % Compute the gradeint Richardson Number, taking care to avoid dividing by 428 | # % zero in the mixed layer. The numerical values of the minimum allowable 429 | # % density and velocity differences are entirely arbitrary, and should not 430 | # % effect the calculations (except that on some occasions they evidently have!) 431 | #print "entered grad mix" 432 | 433 | rc = rg #critical rich. number 434 | j1 = 0 435 | j2 = nz-1 436 | j_range = np.arange(j1,j2) 437 | i = 0 #loop count 438 | #debug_here() 439 | 440 | while 1: 441 | #TODO: find a better way to do implement this loop 442 | 443 | r = np.zeros(len(j_range)) 444 | 445 | for j in j_range: 446 | 447 | dd = (d[j+1]-d[j])/d[j] 448 | dv = (u[j+1]-u[j])**2+(v[j+1]-v[j])**2 449 | if dv==0 or dv<1e-10: 450 | r[j] = np.inf 451 | else: 452 | #compute grad. rich. number 453 | r[j] = g*dz*dd/dv 454 | 455 | #debug_here() 456 | #find the smallest value of r in the profile 457 | r_min = np.min(r) 458 | j_min_idx = np.argmin(r) 459 | 460 | #Check to see whether the smallest r is critical or not. 461 | if r_min > rc: 462 | break 463 | 464 | #Mix the cells j_min_idx and j_min_idx+1 that had the smallest Richardson Number 465 | t, s, d, u, v = stir(t, s, d, u, v, rc, r_min, j_min_idx) 466 | 467 | #recompute the rich number over the part of the profile that has changed 468 | j1 = j_min_idx-2 469 | if j1 < 1: 470 | j1 = 0 471 | 472 | j2 = j_min_idx+2 473 | if j2 > nz-1: 474 | j2 = nz-1 475 | 476 | i+=1 477 | 478 | return t, s, d, u, v 479 | 480 | def stir(t, s, d, u, v, rc, r, j): 481 | 482 | #copied from source script: 483 | 484 | # % This subroutine mixes cells j and j+1 just enough so that 485 | # % the Richardson number after the mixing is brought up to 486 | # % the value rnew. In order to have this mixing process 487 | # % converge, rnew must exceed the critical value of the 488 | # % richardson number where mixing is presumed to start. If 489 | # % r critical = rc = 0.25 (the nominal value), and r = 0.20, then 490 | # % rnew = 0.3 would be reasonable. If r were smaller, then a 491 | # % larger value of rnew - rc is used to hasten convergence. 492 | # 493 | # % This subroutine was modified by JFP in Sep 93 to allow for an 494 | # % aribtrary rc and to achieve faster convergence. 495 | 496 | #TODO: This needs better commenting 497 | rcon = 0.02+(rc-r)/2 498 | rnew = rc+rcon/5. 499 | f = 1-r/rnew 500 | 501 | #mix temp 502 | dt = (t[j+1]-t[j])*f/2. 503 | t[j+1] = t[j+1]-dt 504 | t[j] = t[j]+dt 505 | 506 | #mix sal 507 | ds = (s[j+1]-s[j])*f/2. 508 | s[j+1] = s[j+1]-ds 509 | s[j] = s[j]+ds 510 | 511 | #recompute density 512 | #d[j:j+1] = sw.dens0(s[j:j+1], t[j:j+1]) 513 | #have to be careful here. x[j:j+1] in python is not the same as x[[j,j+1]]. We want the latter 514 | d[[j,j+1]] = sw.dens0(s[[j,j+1]], t[[j,j+1]]) 515 | 516 | du = (u[j+1]-u[j])*f/2 517 | u[j+1] = u[j+1]-du 518 | u[j] = u[j]+du 519 | 520 | dv = (v[j+1]-v[j])*f/2 521 | v[j+1] = v[j+1]-dv 522 | v[j] = v[j]+dv 523 | 524 | return t, s, d, u, v 525 | 526 | def diffus(dstab,nz,a): 527 | 528 | "finite difference implementation of diffusion equation" 529 | 530 | #matlab code: 531 | #a(2:nz-1) = a(2:nz-1) + dstab*(a(1:nz-2) - 2*a(2:nz-1) + a(3:nz)); 532 | 533 | a[1:nz-1] = a[1:nz-1] + dstab*(a[0:nz-2] - 2*a[1:nz-1] + a[2:nz]) 534 | return a 535 | 536 | if __name__ == "__main__": 537 | 538 | print("Running default test case using data from Beaufort gyre...") 539 | 540 | forcing_fname = 'beaufort_met.nc' 541 | prof_fname = 'beaufort_profile.nc' 542 | print("Running Test Case 1 with data from Beaufort gyre...") 543 | forcing, pwp_out = run(met_data=forcing_fname, prof_data=prof_fname, suffix='demo1_nodiff', save_plots=True, diagnostics=False) 544 | print("----------------------------------------------------------------------------") 545 | print("NOTE:\nSee run_demo1() and run_demo2() in PWP_helper.py for more examples.") 546 | print("Additional details can be found here: https://github.com/earlew/pwp_python_00.") 547 | print("----------------------------------------------------------------------------") 548 | -------------------------------------------------------------------------------- /PWP_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the helper functions to assist with the running and analysis of the 3 | PWP model. 4 | """ 5 | 6 | import numpy as np 7 | import seawater as sw 8 | import matplotlib.pyplot as plt 9 | import PWP 10 | from datetime import datetime 11 | import warnings 12 | 13 | #warnings.filterwarnings("error") 14 | #warnings.simplefilter('error', RuntimeWarning) 15 | 16 | #from IPython.core.debugger import set_trace 17 | #debug_here = set_trace 18 | 19 | def run_demo1(): 20 | """ 21 | Example script of how to run the PWP model. 22 | This run uses summertime data from the Beaufort gyre 23 | """ 24 | 25 | #ds = xr.Dataset({'t': (['z'], prof_dset['t'].values), 's': (['z'], prof_dset['s'].values), 'd': (['z'], prof_dset['d'].values), 'z': (['z'], prof_dset['z'].values), 'lat': 74.0}) 26 | 27 | forcing_fname = 'beaufort_met.nc' 28 | prof_fname = 'beaufort_profile.nc' 29 | print("Running Test Case 1 with data from Beaufort gyre...") 30 | forcing, pwp_out = PWP.run(met_data=forcing_fname, prof_data=prof_fname, suffix='demo1_nodiff', save_plots=True, diagnostics=False) 31 | 32 | #debug_here() 33 | def run_demo2(winds_ON=True, emp_ON=True, heat_ON=True, drag_ON=True): 34 | 35 | """ 36 | Example script of how to run the PWP model. 37 | This run uses summertime data from the Atlantic sector of the Southern Ocean 38 | """ 39 | 40 | forcing_fname = 'SO_met_30day.nc' 41 | prof_fname = 'SO_profile1.nc' 42 | print("Running Test Case 2 with data from Southern Ocean...") 43 | p={} 44 | p['rkz']=1e-6 45 | p['dz'] = 2.0 46 | p['max_depth'] = 500.0 47 | p['rg'] = 0.25 # to turn off gradient richardson number mixing set to 0. (code runs much faster) 48 | p['winds_ON'] = winds_ON 49 | p['emp_ON'] = emp_ON 50 | p['heat_ON'] = heat_ON 51 | p['drag_ON'] = drag_ON 52 | 53 | if emp_ON: 54 | emp_flag='' 55 | else: 56 | emp_flag='_empOFF' 57 | 58 | if winds_ON: 59 | winds_flag='' 60 | else: 61 | winds_flag='_windsOFF' 62 | 63 | if heat_ON: 64 | heat_flag='' 65 | else: 66 | heat_flag='_heatingOFF' 67 | 68 | if drag_ON: 69 | drag_flag='' 70 | else: 71 | drag_flag='_dragOFF' 72 | 73 | suffix = 'demo2_1e6diff%s%s%s%s' %(winds_flag, emp_flag, heat_flag, drag_flag) 74 | forcing, pwp_out = PWP.run(met_data=forcing_fname, prof_data=prof_fname, suffix=suffix, save_plots=True, param_kwds=p) 75 | 76 | 77 | def set_params(lat, dt=3., dz=1., max_depth=100., mld_thresh=1e-4, dt_save=1., rb=0.65, rg=0.25, rkz=0., beta1=0.6, beta2=20.0, heat_ON=True, winds_ON=True, emp_ON=True, drag_ON=True): 78 | 79 | """ 80 | This function sets the main paramaters/constants used in the model. 81 | These values are packaged into a dictionary, which is returned as output. 82 | Definitions are listed below. 83 | 84 | CONTROLS (default values are in [ ]): 85 | lat: latitude of profile 86 | dt: time-step increment. Input value in units of hours, but this is immediately converted to seconds.[3 hours] 87 | dz: depth increment (meters). [1m] 88 | max_depth: Max depth of vertical coordinate (meters). [100] 89 | mld_thresh: Density criterion for MLD (kg/m3). [1e-4] 90 | dt_save: time-step increment for saving to file (multiples of dt). [1] 91 | rb: critical bulk richardson number. [0.65] 92 | rg: critical gradient richardson number. [0.25] 93 | rkz: background vertical diffusion (m**2/s). [0.] 94 | beta1: longwave extinction coefficient (meters). [0.6] 95 | beta2: shortwave extinction coefficient (meters). [20] 96 | winds_ON: True/False flag to turn ON/OFF wind forcing. [True] 97 | emp_ON: True/False flag to turn ON/OFF freshwater forcing. [True] 98 | heat_ON: True/False flag to turn ON/OFF surface heat flux forcing. [True] 99 | drag_ON: True/False flag to turn ON/OFF current drag due to internal-inertial wave breaking. [True] 100 | 101 | OUTPUT is dict with fields containing the above variables plus the following: 102 | dt_d: time increment (dt) in units of days 103 | g: acceleration due to gravity [9.8 m/s^2] 104 | cpw: specific heat of water [4183.3 J/kgC] 105 | f: coriolis term (rad/s). [sw.f(lat)] 106 | ucon: coefficient of inertial-internal wave dissipation (s^-1) [0.1*np.abs(f)] 107 | """ 108 | params = {} 109 | params['dt'] = 3600.0*dt 110 | params['dt_d'] = params['dt']/86400. 111 | params['dz'] = dz 112 | params['dt_save'] = dt_save 113 | params['lat'] = lat 114 | params['rb'] = rb 115 | params['rg'] = rg 116 | params['rkz'] = rkz 117 | params['beta1'] = beta1 118 | params['beta2'] = beta2 119 | params['max_depth'] = max_depth 120 | 121 | params['g'] = 9.81 122 | params['f'] = sw.f(lat) 123 | params['cpw'] = 4183.3 124 | params['ucon'] = (0.1*np.abs(params['f'])) 125 | params['mld_thresh'] = mld_thresh 126 | 127 | params['winds_ON'] = winds_ON 128 | params['emp_ON'] = emp_ON 129 | params['heat_ON'] = heat_ON 130 | params['drag_ON'] = drag_ON 131 | 132 | return params 133 | 134 | 135 | 136 | def prep_data(met_dset, prof_dset, params): 137 | 138 | """ 139 | This function prepares the forcing and profile data for the model run. 140 | 141 | Below, the surface forcing and profile data are interpolated to the user defined time steps 142 | and vertical resolutions, respectively. Secondary quantities are also computed and packaged 143 | into dictionaries. The code also checks that the time and vertical increments meet the 144 | necessary stability requirements. 145 | 146 | Lastly, this function initializes the numpy arrays to collect the model's output. 147 | 148 | INPUT: 149 | met_data: dictionary-like object with forcing data. Fields should include: 150 | ['time', 'sw', 'lw', 'qlat', 'qsens', 'tx', 'ty', 'precip']. These fields should 151 | store 1-D time series of the same length. 152 | 153 | The model expects positive heat flux values to represent ocean warming. The time 154 | data field should contain a 1-D array representing fraction of day. For example, 155 | for 6 hourly data, met_data['time'] should contain a number series that increases 156 | in steps of 0.25, such as np.array([1.0, 1.25, 1.75, 2.0, 2.25...]). 157 | 158 | See https://github.com/earlew/pwp_python#input-data for more info about the 159 | expect intput data. 160 | 161 | TODO: Modify code to accept met_data['time'] as an array of datetime objects 162 | 163 | 164 | prof_data: dictionary-like object with initial profile data. Fields should include: 165 | ['z', 't', 's', 'lat']. These represent 1-D vertical profiles of temperature, 166 | salinity and density. 'lat' is expected to be a length=1 array-like object. e.g. 167 | prof_data['lat'] = [25.0] 168 | 169 | params: dictionary-like object with fields defined by set_params function 170 | 171 | OUTPUT: 172 | 173 | forcing: dictionary with interpolated surface forcing data. 174 | pwp_out: dictionary with initialized variables to collect model output. 175 | """ 176 | 177 | #create new time vector with time step dt_d 178 | #time_vec = np.arange(met_dset['time'][0], met_dset['time'][-1]+params['dt_d'], params['dt_d']) 179 | time_vec = np.arange(met_dset['time'][0], met_dset['time'][-1], params['dt_d']) 180 | tlen = len(time_vec) 181 | 182 | #debug_here() 183 | 184 | #interpolate surface forcing data to new time vector 185 | from scipy.interpolate import interp1d 186 | forcing = {} 187 | for vname in met_dset: 188 | p_intp = interp1d(met_dset['time'], met_dset[vname], axis=0) 189 | forcing[vname] = p_intp(time_vec) 190 | 191 | #interpolate E-P to dt resolution (not sure why this has to be done separately) 192 | evap_intp = interp1d(met_dset['time'], met_dset['qlat'], axis=0, kind='nearest', bounds_error=False) 193 | evap = (0.03456/(86400*1000))*evap_intp(np.floor(time_vec)) #(meters per second?) 194 | emp = np.abs(evap) - np.abs(forcing['precip']) 195 | emp[np.isnan(emp)] = 0. 196 | forcing['emp'] = emp 197 | forcing['evap'] = evap 198 | 199 | if params['emp_ON'] == False: 200 | print("WARNING: E-P is turned OFF.") 201 | forcing['emp'][:] = 0.0 202 | forcing['precip'][:] = 0.0 203 | forcing['evap'][:] = 0.0 204 | 205 | if params['heat_ON'] == False: 206 | print("WARNING: Surface heating is turned OFF.") 207 | forcing['sw'][:] = 0.0 208 | forcing['lw'][:] = 0.0 209 | forcing['qlat'][:] = 0.0 210 | forcing['qsens'][:] = 0.0 211 | 212 | 213 | #define q_in and q_out (positive values should mean ocean warming) 214 | forcing['q_in'] = forcing['sw'] #heat flux into ocean 215 | forcing['q_out'] = -(forcing['lw'] + forcing['qlat'] + forcing['qsens']) 216 | 217 | #add time_vec to forcing 218 | forcing['time'] = time_vec 219 | 220 | if params['winds_ON'] == False: 221 | print("Winds are set to OFF.") 222 | forcing['tx'][:] = 0.0 223 | forcing['ty'][:] = 0.0 224 | 225 | #define depth coordinate, but first check to see if profile max depth 226 | #is greater than user defined max depth 227 | zmax = max(prof_dset.z) 228 | if zmax < params['max_depth']: 229 | depth = zmax 230 | print('Profile input shorter than depth selected, truncating to %sm' %depth) 231 | 232 | 233 | #define new z-coordinates 234 | init_prof = {} 235 | init_prof['z'] = np.arange(0, params['max_depth']+params['dz'], params['dz']) 236 | zlen = len(init_prof['z']) 237 | 238 | #compute absorption and incoming radiation (function defined in PWP_model.py) 239 | absrb = PWP.absorb(params['beta1'], params['beta2'], zlen, params['dz']) #(units unclear) 240 | dstab = params['dt']*params['rkz']/params['dz']**2 #courant number 241 | if dstab > 0.5: 242 | print("WARNING: unstable CFL condition for diffusion! dt*rkz/dz**2 > 0.5.") 243 | print("To fix this, try to reduce the time step or increase the depth increment.") 244 | inpt = eval(input("Proceed with simulation? Enter 'y'or 'n'. ")) 245 | if inpt == 'n': 246 | raise ValueError("Please restart PWP.m with a larger dz and/or smaller dt. Exiting...") 247 | 248 | forcing['absrb'] = absrb 249 | params['dstab'] = dstab 250 | 251 | #check depth resolution of profile data 252 | prof_incr = np.diff(prof_dset['z']).mean() 253 | # if params['dz'] < prof_incr/5.: 254 | # message = "Specified depth increment (%s m), is much smaller than mean profile resolution (%s m)." %(params['dz'], prof_incr) 255 | # warnings.warn(message) 256 | 257 | 258 | #debug_here() 259 | #interpolate profile data to new z-coordinate 260 | from scipy.interpolate import InterpolatedUnivariateSpline 261 | for vname in prof_dset: 262 | if vname == 'lat' or vname=='lon': 263 | continue 264 | else: 265 | #first strip nans 266 | not_nan = np.logical_not(np.isnan(prof_dset[vname])) 267 | indices = np.arange(len(prof_dset[vname])) 268 | #p_intp = interp1d(prof_dset['z'], prof_dset[vname], axis=0, kind='linear', bounds_error=False) 269 | #interp1d doesn't work here because it doesn't extrapolate. Can't have Nans in interpolated profile 270 | p_intp = InterpolatedUnivariateSpline(prof_dset['z'][not_nan], prof_dset[vname][not_nan], k=1) 271 | init_prof[vname] = p_intp(init_prof['z']) 272 | 273 | #get profile variables 274 | temp0 = init_prof['t'] #initial profile temperature 275 | sal0 = init_prof['s'] #intial profile salinity 276 | dens0 = sw.dens0(sal0, temp0) #intial profile density 277 | 278 | #initialize variables for output 279 | pwp_out = {} 280 | pwp_out['time'] = time_vec 281 | pwp_out['dt'] = params['dt'] 282 | pwp_out['dz'] = params['dz'] 283 | pwp_out['lat'] = params['lat'] 284 | pwp_out['z'] = init_prof['z'] 285 | 286 | tlen = int(np.floor(tlen/params['dt_save'])) 287 | arr_sz = (zlen, tlen) 288 | pwp_out['temp'] = np.zeros(arr_sz) 289 | pwp_out['sal'] = np.zeros(arr_sz) 290 | pwp_out['dens'] = np.zeros(arr_sz) 291 | pwp_out['uvel'] = np.zeros(arr_sz) 292 | pwp_out['vvel'] = np.zeros(arr_sz) 293 | pwp_out['mld'] = np.zeros((tlen,)) 294 | 295 | #use temp, sal and dens profile data for the first time step 296 | pwp_out['sal'][:,0] = sal0 297 | pwp_out['temp'][:,0] = temp0 298 | pwp_out['dens'][:,0] = dens0 299 | 300 | return forcing, pwp_out, params 301 | 302 | def livePlots(pwp_out, n): 303 | 304 | """ 305 | function to make live plots of the model output. 306 | """ 307 | 308 | #too lazy to re-write the plotting code, so i'm just going to unpack pwp_out here: 309 | time = pwp_out['time'] 310 | uvel = pwp_out['uvel'] 311 | vvel = pwp_out['vvel'] 312 | temp = pwp_out['temp'] 313 | sal = pwp_out['sal'] 314 | dens = pwp_out['dens'] 315 | z = pwp_out['z'] 316 | 317 | 318 | #plot depth int. KE and momentum 319 | plt.figure(num=1) 320 | 321 | plt.subplot(211) 322 | plt.plot(time[n]-time[0], np.trapz(0.5*dens[:,n]*(uvel[:,n]**2+vvel[:,n]**2)), 'b.') 323 | plt.grid(True) 324 | if n==1: 325 | plt.title('Depth integrated KE') 326 | 327 | plt.subplot(212) 328 | plt.plot(time[n]-time[0], np.trapz(dens[:,n]*np.sqrt(uvel[:,n]**2+vvel[:,n]**2)), 'b.') 329 | plt.grid(True) 330 | plt.pause(0.05) 331 | plt.subplots_adjust(hspace=0.35) 332 | 333 | #debug_here() 334 | if n==1: 335 | plt.title('Depth integrated Mom.') 336 | #plt.get_current_fig_manager().window.wm_geometry("400x600+20+40") 337 | 338 | #plot T,S and U,V 339 | plt.figure(num=2, figsize=(12,6)) 340 | ax1 = plt.subplot2grid((1,4), (0, 0), colspan=2) 341 | ax1.plot(uvel[:,n], z, 'b', label='uvel') 342 | ax1.plot(vvel[:,n], z, 'r', label='vvel') 343 | ax1.invert_yaxis() 344 | ax1.grid(True) 345 | ax1.legend(loc=3) 346 | 347 | ax2 = plt.subplot2grid((1,4), (0, 2), colspan=1) 348 | ax2.plot(temp[:,n], z, 'b') 349 | ax2.grid(True) 350 | ax2.set_xlabel('Temp.') 351 | ax2.invert_yaxis() 352 | xlims = ax2.get_xlim() 353 | xticks = np.round(np.linspace(xlims[0], xlims[1], 4), 1) 354 | ax2.set_xticks(xticks) 355 | 356 | ax3 = plt.subplot2grid((1,4), (0, 3), colspan=1) 357 | ax3.plot(sal[:,n], z, 'b') 358 | ax3.set_xlabel('Salinity') 359 | ax3.grid(True) 360 | ax3.invert_yaxis() 361 | xlims = ax3.get_xlim() 362 | xticks = np.round(np.linspace(xlims[0], xlims[1], 4), 1) 363 | ax3.set_xticks(xticks) 364 | 365 | plt.pause(0.05) 366 | 367 | plt.show() 368 | 369 | def makeSomePlots(forcing, pwp_out, time_vec=None, save_plots=False, suffix=''): 370 | 371 | """ 372 | TODO: add doc file 373 | Function to make plots of the results once the model iterations are complete. 374 | 375 | """ 376 | 377 | if len(suffix)>0 and suffix[0] != '_': 378 | suffix = '_%s' %suffix 379 | 380 | #plot summary of ML evolution 381 | fig, axes = plt.subplots(3,1, sharex=True, figsize=(7.5,9)) 382 | 383 | if time_vec is None: 384 | tvec = pwp_out['time'] 385 | else: 386 | tvec = time_vec 387 | 388 | axes = axes.flatten() 389 | ##plot surface heat flux 390 | axes[0].plot(tvec, forcing['lw'], label='$Q_{lw}$') 391 | axes[0].plot(tvec, forcing['qlat'], label='$Q_{lat}$') 392 | axes[0].plot(tvec, forcing['qsens'], label='$Q_{sens}$') 393 | axes[0].plot(tvec, forcing['sw'], label='$Q_{sw}$') 394 | axes[0].hlines(0, tvec[0], pwp_out['time'][-1], linestyle='-', color='0.3') 395 | axes[0].plot(tvec, forcing['q_in']-forcing['q_out'], ls='-', lw=2, color='k', label='$Q_{net}$') 396 | axes[0].set_ylabel('Heat flux (W/m2)') 397 | axes[0].set_title('Heat flux into ocean') 398 | axes[0].grid(True) 399 | #axes[0].set_ylim(-500,300) 400 | 401 | axes[0].legend(loc=0, ncol=2, fontsize='smaller') 402 | 403 | 404 | ##plot wind stress 405 | axes[1].plot(tvec, forcing['tx'], label=r'$\tau_x$') 406 | axes[1].plot(tvec, forcing['ty'], label=r'$\tau_y$') 407 | axes[1].hlines(0, tvec[0], pwp_out['time'][-1], linestyle='--', color='0.3') 408 | axes[1].set_ylabel('Wind stress (N/m2)') 409 | axes[1].set_title('Wind stress') 410 | axes[1].grid(True) 411 | axes[1].legend(loc=0, fontsize='medium') 412 | 413 | 414 | ## plot freshwater forcing 415 | # emp_mmpd = forcing['emp']*1000*3600*24 #convert to mm per day 416 | # axes[2].plot(tvec, emp_mmpd, label='E-P') 417 | # axes[2].hlines(0, tvec[0], pwp_out['time'][-1], linestyle='--', color='0.3') 418 | # axes[2].set_ylabel('Freshwater forcing (mm/day)') 419 | # axes[2].set_title('Freshwater forcing') 420 | # axes[2].grid(True) 421 | # axes[2].legend(loc=0, fontsize='medium') 422 | # axes[2].set_xlabel('Time (days)') 423 | 424 | emp_mmpd = forcing['emp']*1000*3600*24 #convert to mm per day 425 | evap_mmpd = forcing['evap']*1000*3600*24 #convert to mm per day 426 | precip_mmpd = forcing['precip']*1000*3600*24 #convert to mm per day 427 | axes[2].plot(tvec, precip_mmpd, label='$P$', lw=1, color='b') 428 | axes[2].plot(tvec, evap_mmpd, label='$-E$', lw=1, color='r') 429 | axes[2].plot(tvec, emp_mmpd, label='$|E| - P$', lw=2, color='k') 430 | axes[2].hlines(0, tvec[0], tvec[-1], linestyle='--', color='0.3') 431 | axes[2].set_ylabel('Freshwater forcing (mm/day)') 432 | axes[2].set_title('Freshwater forcing') 433 | axes[2].grid(True) 434 | axes[2].legend(loc=1, fontsize=8, ncol=2) 435 | axes[2].set_xlabel('Time (days)') 436 | 437 | if save_plots: 438 | plt.savefig('plots/surface_forcing%s.png' %suffix, bbox_inches='tight') 439 | 440 | 441 | ##plot temp and sal change over time 442 | fig, axes = plt.subplots(2,1, sharex=True) 443 | vble = ['temp', 'sal'] 444 | units = ['$^{\circ}$C', 'PSU'] 445 | #cmap = custom_div_cmap(numcolors=17) 446 | cmap = plt.cm.rainbow 447 | for i in range(2): 448 | ax = axes[i] 449 | im = ax.contourf(pwp_out['time'], pwp_out['z'], pwp_out[vble[i]], 15, cmap=cmap, extend='both') 450 | ax.set_ylabel('Depth (m)') 451 | ax.set_title('Evolution of ocean %s (%s)' %(vble[i], units[i])) 452 | ax.invert_yaxis() 453 | cb = plt.colorbar(im, ax=ax, format='%.1f') 454 | 455 | ax.set_xlabel('Days') 456 | 457 | 458 | ## plot initial and final T-S profiles 459 | from mpl_toolkits.axes_grid1 import host_subplot 460 | import mpl_toolkits.axisartist as AA 461 | 462 | plt.figure() 463 | host = host_subplot(111, axes_class=AA.Axes) 464 | host.invert_yaxis() 465 | par1 = host.twiny() #par for parasite axis 466 | host.set_ylabel("Depth (m)") 467 | host.set_xlabel("Temperature ($^{\circ}$C)") 468 | par1.set_xlabel("Salinity (PSU)") 469 | 470 | p1, = host.plot(pwp_out['temp'][:,0], pwp_out['z'], '--r', label='$T_i$') 471 | host.plot(pwp_out['temp'][:,-1], pwp_out['z'], '-r', label='$T_f$') 472 | p2, = par1.plot(pwp_out['sal'][:,0], pwp_out['z'], '--b', label='$S_i$') 473 | par1.plot(pwp_out['sal'][:,-1], pwp_out['z'], '-b', label='$S_f$') 474 | host.grid(True) 475 | host.legend(loc=0, ncol=2) 476 | #par1.legend(loc=3) 477 | 478 | host.axis["bottom"].label.set_color(p1.get_color()) 479 | host.axis["bottom"].major_ticklabels.set_color(p1.get_color()) 480 | host.axis["bottom"].major_ticks.set_color(p1.get_color()) 481 | 482 | par1.axis["top"].label.set_color(p2.get_color()) 483 | par1.axis["top"].major_ticklabels.set_color(p2.get_color()) 484 | par1.axis["top"].major_ticks.set_color(p2.get_color()) 485 | 486 | if save_plots: 487 | plt.savefig('plots/initial_final_TS_profiles%s.png' %suffix, bbox_inches='tight') 488 | 489 | 490 | 491 | plt.show() 492 | 493 | 494 | def custom_div_cmap(numcolors=11, name='custom_div_cmap', mincol='blue', midcol='white', maxcol='red'): 495 | 496 | """ Create a custom diverging colormap with three colors 497 | 498 | Default is blue to white to red with 11 colors. Colors can be specified 499 | in any way understandable by matplotlib.colors.ColorConverter.to_rgb() 500 | """ 501 | 502 | from matplotlib.colors import LinearSegmentedColormap 503 | 504 | cmap = LinearSegmentedColormap.from_list(name=name, 505 | colors =[mincol, midcol, maxcol], 506 | N=numcolors) 507 | return cmap -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | This is a Python implementation of the Price Weller Pinkel (PWP) ocean mixed layer model. This code is based on the MATLAB verision of the PWP model, originally written by [Peter Lazarevich and Scott Stoermer](http://www.po.gso.uri.edu/rafos/research/pwp/) (U. Rhode Island) and later modified by Byron Kilbourne (University of Washington) and Sarah Dewey (University of Washington). 4 | 5 | For a detailed description of the theory behind the model, the best source is the original [Price et al. (1986)](http://onlinelibrary.wiley.com/doi/10.1029/JC091iC07p08411/full) paper that introduced the model. A much shorter review of the algorithm is provided in the [HYCOM documentation for the PWP](https://hycom.org/attachments/067_pwp.pdf); a google search may yield better sources. 6 | 7 | The code presented here is functionally similar to its MATLAB equivalent (see *matlab\_files/PWP_Byron.m*), but I have made significant changes to the code organization and flow. One big difference is that this code is split into two files: **PWP.py** and **PWP_helper.py**. 8 | 9 | *PWP.py* contains the core numerical algorithms for the PWP model and is mostly a line-by-line translation of the original MATLAB code. 10 | 11 | *PWP_helper.py* contains helper functions to facilitate model initialization, output analysis and other miscellaneous tasks. Many of these functions were introduced in this implementation. 12 | 13 | DISCLAIMER: 14 | I did this python translation as a personal exercise, so I would recommend thoroughly examining this code before adopting it for your personal use. Feel free to leave a note under the [issues tab](https://github.com/earlew/pwp_python_00/issues) if you encounter any problems. 15 | 16 | 17 | ## Required modules/libraries 18 | To run this code, you'll need Python 3 (might work with later versions of Python 2) and the following libraries: 19 | 20 | + Numpy 21 | + Scipy 22 | + Matplotlib 23 | + [xarray](http://xarray.pydata.org/en/stable/) 24 | + seawater 25 | 26 | The first three modules are available with popular python distributions such as [Anaconda](https://www.continuum.io/downloads) and [Canopy](https://store.enthought.com/downloads/#default). You can get the other two modules via `pip install` from the unix command line: 27 | 28 | ``` 29 | pip install xarray 30 | pip install seawater 31 | ``` 32 | 33 | Once these libraries are installed, you should be able to run the demos that are mentioned below. 34 | 35 | ## How the code works 36 | 37 | As mentioned above, the code is split across two files *PWP.py* and *PWP_helper.py*. *PWP.py* contains all the numerical algorithms while *PWP_helper.py* has a few auxillary functions. The order of operations is as follows: 38 | 39 | 1. Set and derive model parameters. (See *set\_params* function in *PWP\_helper.py*). 40 | 2. Prepare forcing and profile data for model run (see *prep\_data* function in *PWP\_helper.py*). 41 | 3. Iterate the PWP model: 42 | + apply heat and salt fluxes. 43 | + apply wind stress (momentum flux). 44 | + apply drag associated with internal-inertial wave dispersion. 45 | + apply bulk Richardson mixing. 46 | + apply gradient Richardson mixing. 47 | + apply diapycnal diffusion (if ON). 48 | 4. Save results to an output file. 49 | 5. Make simple plots to visualize the results. 50 | 51 | If you wish to obtain a deeper understanding of how this code works, the `PWP.run()` function would be a good place to start. 52 | 53 | ## Input data 54 | 55 | The PWP model requires two input netCDF files: one for the surface forcing and another for the initial CTD profile. The surface forcing file must have the following data fields: 56 | 57 | + **time**: time (days). 58 | + **sw**: net shortwave radiation (W/m2) 59 | + **lw**: net longwave radiation (W/m2) 60 | + **qlat**: latent heat flux (W/m2) 61 | + **qsens**: sensible heat flux (W/m2) 62 | + **tx**: eastward wind stress (N/m2) 63 | + **ty**: northward wind stress (N/m2) 64 | + **precip**: precipitation rate (m/s) 65 | 66 | For fluxes, **positive values should correspond to heat/freshwater gained by the ocean**. Note that the MATLAB version of this code uses a different sign convention. 67 | 68 | Freshwater flux due to evaporation is computed from **qlat**. 69 | 70 | The time data field should contain a 1-D array representing fraction of day. For example, for 6 hourly data, this should be a number series that increases in steps of 0.25, such as np.array([1.0, 1.25, 1.75, 2.0, 2.25...]). 71 | 72 | The initial profile file should have the following data fields: 73 | 74 | + **z**: 1-D array of depth levels (m) 75 | + **t**: 1-D array containing temperature profile (degrees celsius) 76 | + **s**: 1-D array containing salinity profile (PSU) 77 | + **lat**: Array with float representing the latitude of profile. e.g. `prof_data['lat'] = [45] #45N` 78 | 79 | Examples of both input files are provided in the input directory. 80 | 81 | ## Running the code 82 | 83 | For examples of how to run the code, see the `run_demo1()` and `run_demo2()` functions in *PWP_helper.py*. `run_demo2()` is illustrated below. 84 | 85 | ## Default settings 86 | 87 | The main model parameters and their defaults are listed below. See test runs below for examples of how to change these settings: 88 | 89 | + **dt**: time-step increment in units of hours [3 hours] 90 | + **dz**: depth increment (meters). [1m] 91 | + **max_depth**: Max depth of vertical coordinate (meters). [100] 92 | + **mld_thresh**: Density criterion for MLD (kg/m3). [1e-4] 93 | + **dt_save**: time-step increment for saving to file (multiples of dt). [1] 94 | + **rb**: critical bulk richardson number. [0.65] 95 | + **rg**: critical gradient richardson number. [0.25] 96 | + **rkz**: background vertical diffusion (m**2/s). [0.] 97 | + **beta1**: longwave extinction coefficient (meters) [0.6] 98 | + **beta2**: shortwave extinction coefficient (meters). [20] 99 | + **winds_ON**: True/False flag to turn ON/OFF wind forcing. [True] 100 | + **emp_ON**: True/False flag to turn ON/OFF freshwater forcing. [True] 101 | + **heat_ON**: True/False flag to turn ON/OFF surface heat flux forcing. [True] 102 | + **drag_ON**: True/False flag to turn ON/OFF current drag due to internal-inertial wave dispersion. [True] 103 | 104 | 105 | ## Test case 1: Southern Ocean in the summer 106 | This example uses data from *SO\_profile1.nc* and *SO\_met\_30day.nc*, which contain the initial profile and surface forcing data respectively. For the initial profile, we use the first profile collected by Argo float [5904469](http://www.ifremer.fr/co-argoFloats/float?detail=false&ptfCode=5904469). This profile was recorded in the Atlantic sector of the Southern Ocean (specifically, -53.5$^{\circ}$ N and 0.02$^{\circ}$ E) on December 11, 2014. For the surface forcing, we use 30 day time series of 6-hourly surface fluxes from [NCEP reanalysis](http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanalysis.surfaceflux.html) at the above location. 107 | 108 | To run the demo that uses this data, you can do the following from a Python terminal 109 | 110 | ``` 111 | >>> import PWP_helper 112 | >>> PWP_helper.run_demo2() 113 | ``` 114 | 115 | Doing this will execute the model and generate several plots. 116 | 117 | ### Run1: Full simulation 118 | The surface forcing time series are shown below. 119 | 120 | ![Sample Forcing](example_plots/surface_forcing_demo2_1e6diff.png) 121 | 122 | For this model run, we set the vertical diffusivity to 1x10-6 m2/s, and change the max depth and depth increment to 500m and 2m, respectively: 123 | 124 | ``` 125 | forcing_fname = 'SO_met_30day.nc' 126 | prof_fname = 'SO_profile1.nc' 127 | p={} 128 | p['rkz']=1e-6 129 | p['dz'] = 2.0 130 | p['max_depth'] = 500.0 131 | forcing, pwp_out = PWP.run(met_data=forcing_fname, prof_data=prof_fname, save_plots=True, param_kwds=p) 132 | ``` 133 | 134 | The results are displayed below. 135 | 136 | ![Sample Forcing](example_plots/initial_final_TS_profiles_demo2_1e6diff.png) 137 | 138 | ### Run2: Freshwater fluxes off 139 | 140 | In this run, everything is the same as the previous example except that the E-P forcing is turned off. To execute this run, you can do 141 | 142 | ``` 143 | >>> PWP_helper.run_demo2(emp_ON=False) 144 | ``` 145 | 146 | The resulting surface forcing is shown below: 147 | 148 | ![Sample Forcing](example_plots/surface_forcing_demo2_1e6diff_empOFF.png) 149 | 150 | This results in the following upper ocean evolution: 151 | 152 | ![Sample Forcing](example_plots/initial_final_TS_profiles_demo2_1e6diff_empOFF.png) 153 | 154 | As we can see, setting the freshwater flux to zero results in a final mixed layer is slightly deeper, saltier and cooler. 155 | 156 | ### Run3: Wind forcing off 157 | 158 | In this simulation, the wind forcing is turned off. 159 | 160 | ``` 161 | >>> PWP_helper.run_demo2(winds_ON =False) 162 | ``` 163 | 164 | ![Sample Forcing](example_plots/surface_forcing_demo2_1e6diff_windsOFF.png) 165 | 166 | ![Sample Forcing](example_plots/initial_final_TS_profiles_demo2_1e6diff_windsOFF.png) 167 | 168 | Without wind-driven mixing, the final mixed layer depth is much shallower than the previous cases. 169 | 170 | ## Future work 171 | + Create an option to add a passive tracer to the model. 172 | + Incorporate a rudimentary sea-ice model to provide ice induced heat and salt fluxes. 173 | -------------------------------------------------------------------------------- /docs/HYCOM_pwp_doc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/docs/HYCOM_pwp_doc.pdf -------------------------------------------------------------------------------- /example_plots/initial_final_TS_profiles_demo2_1e6diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/initial_final_TS_profiles_demo2_1e6diff.png -------------------------------------------------------------------------------- /example_plots/initial_final_TS_profiles_demo2_1e6diff_empOFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/initial_final_TS_profiles_demo2_1e6diff_empOFF.png -------------------------------------------------------------------------------- /example_plots/initial_final_TS_profiles_demo2_1e6diff_windsOFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/initial_final_TS_profiles_demo2_1e6diff_windsOFF.png -------------------------------------------------------------------------------- /example_plots/surface_forcing_demo2_1e6diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/surface_forcing_demo2_1e6diff.png -------------------------------------------------------------------------------- /example_plots/surface_forcing_demo2_1e6diff_empOFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/surface_forcing_demo2_1e6diff_empOFF.png -------------------------------------------------------------------------------- /example_plots/surface_forcing_demo2_1e6diff_windsOFF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/example_plots/surface_forcing_demo2_1e6diff_windsOFF.png -------------------------------------------------------------------------------- /input_data/SO_met_100day.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/input_data/SO_met_100day.nc -------------------------------------------------------------------------------- /input_data/SO_met_30day.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/input_data/SO_met_30day.nc -------------------------------------------------------------------------------- /input_data/SO_profile1.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/input_data/SO_profile1.nc -------------------------------------------------------------------------------- /input_data/beaufort_met.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/input_data/beaufort_met.nc -------------------------------------------------------------------------------- /input_data/beaufort_profile.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/input_data/beaufort_profile.nc -------------------------------------------------------------------------------- /matlab_files/PWP_Byron.m: -------------------------------------------------------------------------------- 1 | function PWP_Byron_Earle(met_input_file, profile_input_file, pwp_output_file) 2 | %-------------------------------------------------------------------------- 3 | % MATLAB implementation of the Price Weller Pinkel mixed layer model 4 | % 19 May 2011 5 | % Byron Kilbourne 6 | % University of Washington 7 | % School of Oceanography 8 | %-------------------------------------------------------------------------- 9 | % input (.mat format) 10 | % met_input_file -> path and file name for MET forcing 11 | % profile_input_file -> path and file name for intial density profile 12 | % output (.mat format) 13 | % pwp_output -> path and file name for output 14 | %-------------------------------------------------------------------------- 15 | % The 2001 version of PWP was having some tricky bugs related to the timing 16 | % of respnse to the wind stress. I will rewrite the matlab version for 17 | % MATLAB 2001a 18 | % I will: remove obsolete function calls (eg sw_den0) 19 | % change structure to remove global variables 20 | % preallocate array size memory 21 | % Effort has been made to preserve the original format for input and output 22 | % for compatability with the previous version. The variable names, though 23 | % obtuse, have been preserved as well. 24 | %-------------------------------------------------------------------------- 25 | % this code has sections 26 | % 1) set parameters 27 | % 2) read in forcing data 28 | % 3) preallocate variables 29 | % 4) main model loop 30 | % 4.1) heat and salt fluxes 31 | % 4.2) rotate, adjust to wind, rotate 32 | % 4.3) bulk Richardson number mixing 33 | % 4.4) gradient Richardson number mixing 34 | % 5) save results to output file 35 | 36 | %-------------------------------------------------------------------------- 37 | % diagnostic plots 38 | % 0 -> no plots 39 | % 1 -> shows depth integrated KE and momentum and current,T, and S profiles 40 | % 2 -> plot momentum after model run 41 | % 3 -> plot mixed layer depth after model run 42 | diagnostics = 1; 43 | %-------------------------------------------------------------------------- 44 | tic % log runtime 45 | % set parameters 46 | 47 | dt = 3600*3;%900; %time-step increment (seconds) 48 | dz = 1;%5; %depth increment (meters) 49 | % the days parameter has been deprecated 50 | %days = 17; %the number of days to run (max time grid) 51 | depth = 100; %the depth to run (max depth grid) 52 | dt_save = 1; %time-step increment for saving to file (multiples of dt) 53 | lat = 74;%-58.2; %latitude (degrees) 54 | g = 9.8; %gravity (9.8 m/s^2) 55 | cpw = 4183.3; %specific heat of water (4183.3 J/kgC) 56 | rb = 0.65; %critical bulk richardson number (0.65) 57 | rg = 0.25; %critical gradient richardson number (0.25) 58 | rkz = 0; %background vertical diffusion (0) m^2/s 59 | beta1 = 0.6; %longwave extinction coefficient (0.6 m) 60 | beta2 = 20; %shortwave extinction coefficient (20 m) 61 | 62 | f = sw_f(lat); %coriolis term (rad/s) 63 | ucon = (.1*abs(f)); %coefficient of inertial-internal wave dissipation (0) s^-1 64 | %-------------------------------------------------------------------------- 65 | % load forcing data 66 | 67 | load(met_input_file) 68 | load(profile_input_file) 69 | dtd = dt/86400; 70 | % days parameter deprecated 71 | time = met.time(1):dtd:met.time(end); 72 | nmet = length(time); 73 | clear dtd 74 | qi = interp1(met.time,met.sw,time); 75 | qo = interp1(met.time,(met.lw + met.qlat + met.qsens),time); 76 | tx = interp1(met.time,met.tx,time); 77 | ty = interp1(met.time,met.ty,time); 78 | precip = interp1(met.time,met.precip,time); 79 | % make depth grid 80 | zmax = max(profile.z); 81 | if zmax < depth 82 | depth = zmax; 83 | disp(['Profile input shorter than depth selected, truncating to ' num2str(zmax)]) 84 | end 85 | clear zmax 86 | z = 0:dz:depth; 87 | nz = length(z); 88 | % Check the depth-resolution of the profile file 89 | profile_increment = (profile.z(end)-profile.z(1))/(length(profile.z)-1); 90 | if dz < profile_increment/5 91 | yorn = input('Depth increment, dz, is much smaller than profile resolution. Is this okay? (y/n)','s'); 92 | if yorn == 'n' 93 | error(['Please restart PWP.m with a new dz >= ' num2str(profile_increment/5)]) 94 | end 95 | end 96 | t = zeros(nz,nmet); 97 | s = zeros(nz,nmet); 98 | t(:,1) = interp1(profile.z,profile.t,z.','linear','extrap'); 99 | s(:,1) = interp1(profile.z,profile.s,z.','linear','extrap'); 100 | d = sw_dens0(s(:,1),t(:,1)); 101 | % Interpolate evaporation minus precipitaion at dt resolution 102 | evap = (0.03456/(86400*1000))*interp1(met.time,met.qlat,floor(time),'nearest'); 103 | emp = evap - precip; 104 | emp(isnan(emp)) = 0; 105 | %-------------------------------------------------------------------------- 106 | % preallocate space 107 | u = zeros(nz,nmet); % east velocity m/s 108 | v = zeros(nz,nmet); % north velocity m/s 109 | % set initial conditions in PWP 18 Mar 2013 110 | %u(1:13,1) = -5.514e-2;v(1:13,1) = -1.602e-1; 111 | mld = zeros(1,nmet); % mized layer depth m 112 | absrb = absorb(beta1,beta2,nz,dz); % absorbtion of incoming rad. (units?) 113 | dstab = dt*rkz/dz^2; % Courant number 114 | if dstab > 0.5 115 | disp('!unstable CFL condition for diffusion!') 116 | pause 117 | end 118 | % output space 119 | pwp_output.dt = dt; 120 | pwp_output.dz = dz; 121 | pwp_output.lat = lat; 122 | pwp_output.z = z; 123 | pwp_output.time = zeros(nz,floor(nmet/dt_save)); 124 | pwp_output.t = zeros(nz,floor(nmet/dt_save)); 125 | pwp_output.s = zeros(nz,floor(nmet/dt_save)); 126 | pwp_output.d = zeros(nz,floor(nmet/dt_save)); 127 | pwp_output.u = zeros(nz,floor(nmet/dt_save)); 128 | pwp_output.v = zeros(nz,floor(nmet/dt_save)); 129 | pwp_output.mld = zeros(nz,floor(nmet/dt_save)); 130 | 131 | %-------------------------------------------------------------------------- 132 | % model loop 133 | for n = 2:nmet 134 | disp(['loop iter. ' sprintf('%2d',n)]) 135 | % pwpgo function does the "math" for fluxes and vel 136 | [s(:,n), t(:,n), u(:,n), v(:,n), mld(n)] = pwpgo(qi(n-1),qo(n-1),emp(n-1),tx(n-1),ty(n-1), ... 137 | dt,dz,g,cpw,rb,rg,nz,z,t(:,n-1),s(:,n-1),d,u(:,n-1),v(:,n-1),absrb,f,ucon,n); 138 | 139 | % vertical (diapycnal) diffusion 140 | if rkz > 0 141 | diffus(dstab,t); 142 | diffus(dstab,s); 143 | d = sw_dens0(s,t); 144 | diffus(dstab,u); 145 | diffus(dstab,v); 146 | end % diffusion 147 | 148 | % Diagnostic plots 149 | switch diagnostics 150 | case 1 151 | figure(1) 152 | subplot(211) 153 | plot(time(n)-time(1),trapz(z,.5.*d.*(u(:,n).^2+v(:,n).^2)),'b.') 154 | if n == 2 155 | set(gcf,'position',[1 400 700 400]) 156 | hold on 157 | grid on 158 | title('depth int. KE') 159 | end 160 | subplot(212) 161 | plot(time(n)-time(1),trapz(z,d.*sqrt(u(:,n).^2+v(:,n).^2)),'b.') 162 | if n == 2 163 | hold on 164 | grid on 165 | title('depth int. momentum') 166 | end 167 | figure(2) 168 | if n == 2 169 | set(gcf,'position',[700 400 700 400]) 170 | end 171 | 172 | subplot(121) 173 | plot(u(:,n),z,'b',v(:,n),z,'r') 174 | axis ij 175 | grid on 176 | xlabel('u (b) v (r)') 177 | subplot(143) 178 | plot(t(:,n),z,'b') 179 | axis ij 180 | grid on 181 | xlabel('temp') 182 | subplot(144) 183 | plot(s(:,n),z,'b') 184 | axis ij 185 | grid on 186 | xlabel('salinity') 187 | 188 | pause(.2) 189 | case 2 190 | mom = zeros(1,nmet); 191 | if n == nmet 192 | for k = 1:nmet 193 | mom(k) = trapz(z,d.*sqrt(u(:,k).^2+v(:,k).^2)); 194 | mli = find(sqrt(u(:,k).^2+v(:,k).^2) < 10^-3,1,'first'); 195 | mld(k) = z(mli); 196 | end 197 | w = u(1,:)+1i*v(1,:); 198 | tau = tx+1i*ty; 199 | T = tau./mld; 200 | dTdt = gradient(T,dt); 201 | pi_w = d(1).*mld.*real((w./1i*f).*dTdt); 202 | 203 | figure(1) 204 | plot(time-time(1),mom,'b.-') 205 | 206 | end 207 | case 3 208 | if n == nmet 209 | plot(time-time(1),mld,'b.') 210 | axis ij 211 | pause 212 | end 213 | end 214 | 215 | end % model loop 216 | % save ouput 217 | pwp_output.s = s(:,1:dt_save:end); 218 | pwp_output.t = t(:,1:dt_save:end); 219 | pwp_output.u = u(:,1:dt_save:end); 220 | pwp_output.v = v(:,1:dt_save:end); 221 | pwp_output.d = sw_dens0(pwp_output.s,pwp_output.t); 222 | time = repmat(time,nz,1); 223 | pwp_output.time = time(:,1:dt_save:end); 224 | mld(1)=mld(2); 225 | pwp_output.mld = mld(:,1:dt_save:end); 226 | 227 | save(pwp_output_file,'pwp_output') 228 | toc 229 | end % PWP driver routine 230 | %-------------------------------------------------------------------------- 231 | 232 | function [s t u v mld] = pwpgo(qi,qo,emp,tx,ty,dt,dz,g,cpw,rb,rg,nz,z,t,s, ... 233 | d,u,v,absrb,f,ucon,n) 234 | % pwpgo is the part of the model where all the dynamics "happen" 235 | 236 | t_old = t(1); s_old = s(1); 237 | t(1) = t(1) + (qi*absrb(1)-qo)*dt./(dz*d(1)*cpw); 238 | s(1) = s(1)/(1-emp*dt/dz); 239 | 240 | Tf = sw_fp(s_old,1); 241 | if t(1) < Tf; 242 | t(1) = Tf; 243 | end 244 | 245 | % Absorb solar radiation at depth. 246 | if size(dz*d(2:nz)*cpw,2) > 1 247 | t(2:nz) = t(2:nz)+(qi*absrb(2:nz)*dt)./(dz*d(2:nz)*cpw)'; %THIS IS THE ORIG ONE! 248 | else 249 | t(2:nz) = t(2:nz)+(qi*absrb(2:nz)*dt)./(dz*d(2:nz)*cpw); 250 | end 251 | 252 | d = sw_dens0(s,t); 253 | [t s d u v] = remove_si(t,s,d,u,v); %relieve static instability 254 | 255 | % original ml_index criteria 256 | ml_index = find(diff(d)>1E-4,1,'first'); %1E 257 | %ml_index = find(diff(d)>1E-3,1,'first'); 258 | %ml_index = find( (d-d(1)) > 1e-4 ,1,'first'); 259 | 260 | % debug MLD index 261 | %{ 262 | figure(86) 263 | clf 264 | plot(d,-z,'b.-') 265 | hold on 266 | plot(d(ml_index),-z(ml_index),'r*') 267 | pause 268 | %} 269 | if isempty(ml_index) 270 | error_text = 'Mixed layer depth is undefined'; 271 | error(error_text) 272 | end 273 | 274 | % Get the depth of the surfacd mixed-layer. 275 | ml_depth = z(ml_index+1); 276 | mld = ml_depth; % added 2013 03 07 277 | 278 | % rotate u,v do wind input, rotate again, apply mixing 279 | ang = -f*dt/2; % should be moved to main driver 280 | [u v] = rot(u,v,ang); 281 | du = (tx/(ml_depth*d(1)))*dt; 282 | dv = (ty/(ml_depth*d(1)))*dt; 283 | u(1:ml_index) = u(1:ml_index)+du; 284 | v(1:ml_index) = v(1:ml_index)+dv; 285 | 286 | % Apply drag to the current (this is a horrible parameterization of 287 | % inertial-internal wave dispersion). 288 | if ucon > 1E-10 289 | u = u*(1-dt*ucon); 290 | v = v*(1-dt*ucon); 291 | end 292 | [u v] = rot(u,v,ang); 293 | 294 | % Bulk Richardson number instability form of mixing (as in PWP). 295 | if rb > 1E-5 296 | [t s d u v] = bulk_mix(t,s,d,u,v,g,rb,nz,z,ml_index); 297 | end 298 | 299 | % Do the gradient Richardson number instability form of mixing. 300 | if rg > 0 301 | [t,s,~,u,v] = grad_mix(t,s,d,u,v,dz,g,rg,nz); 302 | end 303 | 304 | % Debugging plots 305 | %{ 306 | figure(1) 307 | subplot(211) 308 | plot(1,1) 309 | hold on 310 | plot(u,z,'b') 311 | plot(v,z,'r') 312 | axis ij 313 | grid on 314 | xlim([-.2 .2]) 315 | 316 | subplot(212) 317 | plot(1,1) 318 | hold on 319 | plot(s-30,z,'b') 320 | plot(t,z,'r') 321 | grid on 322 | axis ij 323 | xlim([2 6]) 324 | pause(.05) 325 | clf 326 | %} 327 | end % pwpgo 328 | 329 | %-------------------------------------------------------------------------- 330 | function absrb = absorb(beta1,beta2,nz,dz) 331 | % Compute solar radiation absorption profile. This 332 | % subroutine assumes two wavelengths, and a double 333 | % exponential depth dependence for absorption. 334 | % 335 | % Subscript 1 is for red, non-penetrating light, and 336 | % 2 is for blue, penetrating light. rs1 is the fraction 337 | % assumed to be red. 338 | 339 | rs1 = 0.6; 340 | rs2 = 1.0-rs1; 341 | %absrb = zeros(nz,1); 342 | z1 = (0:nz-1)*dz; 343 | z2 = z1 + dz; 344 | z1b1 = z1/beta1; 345 | z2b1 = z2/beta1; 346 | z1b2 = z1/beta2; 347 | z2b2 = z2/beta2; 348 | absrb = (rs1*(exp(-z1b1)-exp(-z2b1))+rs2*(exp(-z1b2)-exp(-z2b2)))'; 349 | end % absorb 350 | 351 | %-------------------------------------------------------------------------- 352 | 353 | function [t s d u v] = remove_si(t,s,d,u,v) 354 | % Find and relieve static instability that may occur in the 355 | % density array d. This simulates free convection. 356 | % ml_index is the index of the depth of the surface mixed layer after adjustment, 357 | 358 | while 1 359 | ml_index = find(diff(d)<0,1,'first'); 360 | if isempty(ml_index) 361 | break 362 | end 363 | %%{ 364 | figure(86) 365 | clf 366 | plot(d,'b-') 367 | hold on 368 | [t s d u v] = mix5(t,s,d,u,v,ml_index+1); 369 | plot(d,'r-') 370 | %pause 371 | %} 372 | %[t s d u v] = mix5(t,s,d,u,v,ml_index+1); 373 | 374 | end 375 | 376 | end % remove_si 377 | %-------------------------------------------------------------------------- 378 | 379 | function [t s d u v] = mix5(t,s,d,u,v,j) 380 | % This subroutine mixes the arrays t, s, u, v down to level j. 381 | t(1:j) = mean(t(1:j)); 382 | s(1:j) = mean(s(1:j)); 383 | d(1:j) = sw_dens0(s(1:j),t(1:j)); 384 | u(1:j) = mean(u(1:j)); 385 | v(1:j) = mean(v(1:j)); 386 | end % mix5 387 | 388 | %-------------------------------------------------------------------------- 389 | 390 | function [u v] = rot(u,v,ang) 391 | % This subroutine rotates the vector (u,v) through an angle, ang 392 | r = (u+1i*v)*exp(1i*ang); 393 | u = real(r); 394 | v = imag(r); 395 | 396 | end %rot 397 | %-------------------------------------------------------------------------- 398 | 399 | function [t s d u v] = bulk_mix(t,s,d,u,v,g,rb,nz,z,ml_index) 400 | 401 | rvc = rb; 402 | for j = ml_index+1:nz 403 | h = z(j); 404 | dd = (d(j)-d(1))/d(1); 405 | dv = (u(j)-u(1))^2+(v(j)-v(1))^2; 406 | if dv == 0 407 | rv = Inf; 408 | else 409 | rv = g*h*dd/dv; 410 | end 411 | if rv > rvc 412 | break 413 | else 414 | [t s d u v] = mix5(t,s,d,u,v,j); 415 | end 416 | end 417 | 418 | end % bulk_mix 419 | %-------------------------------------------------------------------------- 420 | 421 | function [t s d u v] = grad_mix(t,s,d,u,v,dz,g,rg,nz) 422 | 423 | % This function performs the gradeint Richardson Number relaxation 424 | % by mixing adjacent cells just enough to bring them to a new 425 | % Richardson Number. 426 | 427 | rc = rg; 428 | 429 | % Compute the gradeint Richardson Number, taking care to avoid dividing by 430 | % zero in the mixed layer. The numerical values of the minimum allowable 431 | % density and velocity differences are entirely arbitrary, and should not 432 | % effect the calculations (except that on some occasions they evidnetly have!) 433 | 434 | j1 = 1; 435 | j2 = nz-1; 436 | 437 | while 1 438 | r = zeros(size(j1:j2)); 439 | for j = j1:j2 440 | if j <= 0 441 | keyboard 442 | end 443 | dd = (d(j+1)-d(j))/d(j); 444 | dv = (u(j+1)-u(j))^2+(v(j+1)-v(j))^2; 445 | if dv == 0 446 | r(j) = Inf; 447 | else 448 | r(j) = g*dz*dd/dv; 449 | end 450 | end 451 | 452 | % Find the smallest value of r in profile 453 | [rs js] = min(r); 454 | %js = find(r==rs,1,'first'); 455 | 456 | % Check to see whether the smallest r is critical or not. 457 | if rs > rc 458 | return 459 | end 460 | 461 | % Mix the cells js and js+1 that had the smallest Richardson Number 462 | [t s d u v] = stir(t,s,d,u,v,rc,rs,js); 463 | 464 | % Recompute the Richardson Number over the part of the profile that has changed 465 | j1 = js-2; 466 | if j1 < 1 467 | j1 = 1; 468 | end 469 | j2 = js+2; 470 | if j2 > nz-1 471 | j2 = nz-1; 472 | end 473 | end 474 | 475 | end % grad_mix 476 | 477 | %-------------------------------------------------------------------------- 478 | function [t s d u v] = stir(t,s,d,u,v,rc,r,j) 479 | 480 | % This subroutine mixes cells j and j+1 just enough so that 481 | % the Richardson number after the mixing is brought up to 482 | % the value rnew. In order to have this mixing process 483 | % converge, rnew must exceed the critical value of the 484 | % richardson number where mixing is presumed to start. If 485 | % r critical = rc = 0.25 (the nominal value), and r = 0.20, then 486 | % rnew = 0.3 would be reasonable. If r were smaller, then a 487 | % larger value of rnew - rc is used to hasten convergence. 488 | 489 | % This subroutine was modified by JFP in Sep 93 to allow for an 490 | % aribtrary rc and to achieve faster convergence. 491 | 492 | rcon = 0.02+(rc-r)/2; 493 | rnew = rc+rcon/5; 494 | f = 1-r/rnew; 495 | dt = (t(j+1)-t(j))*f/2; 496 | t(j+1) = t(j+1)-dt; 497 | t(j) = t(j)+dt; 498 | ds = (s(j+1)-s(j))*f/2; 499 | s(j+1) = s(j+1)-ds; 500 | s(j) = s(j)+ds; 501 | d(j:j+1) = sw_dens0(s(j:j+1),t(j:j+1)); 502 | du = (u(j+1)-u(j))*f/2; 503 | u(j+1) = u(j+1)-du; 504 | u(j) = u(j)+du; 505 | dv = (v(j+1)-v(j))*f/2; 506 | v(j+1) = v(j+1)-dv; 507 | v(j) = v(j)+dv; 508 | 509 | end % stir 510 | -------------------------------------------------------------------------------- /matlab_files/met.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/matlab_files/met.mat -------------------------------------------------------------------------------- /matlab_files/output.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/matlab_files/output.mat -------------------------------------------------------------------------------- /matlab_files/prof.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/matlab_files/prof.mat -------------------------------------------------------------------------------- /output/pwp_output.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/output/pwp_output.nc -------------------------------------------------------------------------------- /plots/initial_final_TS_profiles_demo1_1e6diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/plots/initial_final_TS_profiles_demo1_1e6diff.png -------------------------------------------------------------------------------- /plots/initial_final_TS_profiles_demo1_nodiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/plots/initial_final_TS_profiles_demo1_nodiff.png -------------------------------------------------------------------------------- /plots/initial_final_TS_profiles_demo2_1e6diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/plots/initial_final_TS_profiles_demo2_1e6diff.png -------------------------------------------------------------------------------- /plots/surface_forcing_demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/plots/surface_forcing_demo1.png -------------------------------------------------------------------------------- /plots/surface_forcing_demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earlew/pwp_python_00/a36748fbcefbc3b73f0c6f36b0ea422b2e3267bd/plots/surface_forcing_demo2.png --------------------------------------------------------------------------------