├── .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 | 
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 | 
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 | 
149 |
150 | This results in the following upper ocean evolution:
151 |
152 | 
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 | 
165 |
166 | 
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
--------------------------------------------------------------------------------