gphys 1.5.6 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,7 +48,12 @@ module NumRu
48
48
 
49
49
  pcv_val = pcv.val
50
50
  v_val = v.val # should be NArray or NArrayMiss
51
- v_val = v_val.to_na if v_val.is_a?(NArrayMiss)
51
+ if v_val.is_a?(NArrayMiss)
52
+ misval = 9.9692099683868690e+36
53
+ v_val = v_val.to_na(misval)
54
+ else
55
+ misval = nil
56
+ end
52
57
  if pcv_val[0] > pcv_val[-1]
53
58
  # reverse the p coordinate to the increasing order
54
59
  pcv_val = pcv_val[-1..0]
@@ -67,7 +72,7 @@ module NumRu
67
72
  vs_val = vs_val.to_na if vs_val.is_a?(NArrayMiss)
68
73
 
69
74
  v_val, p_over_g, nzbound = GPhys.c_cap_by_boundary(v_val, zdim,
70
- pcv_over_g, true, ps_over_g, vs_val)
75
+ pcv_over_g, true, ps_over_g, vs_val, misval)
71
76
 
72
77
  elsif zdim = SigmaCoord.find_sigma_d(v) # substitution, not comparison
73
78
  # has a sigma coordnate
@@ -85,6 +90,12 @@ module NumRu
85
90
  ps = ps.convert_units(pascal) if ps.units != pascal
86
91
  sig_val = sig.val
87
92
  v_val = v.val # should be NArray, not NArrayMiss (coz sigma)
93
+ if v_val.is_a?(NArrayMiss)
94
+ v_val = v_val.to_na
95
+ mask = v_val.get_mask
96
+ else
97
+ mask = nil
98
+ end
88
99
  p_over_g = SigmaCoord.sig_ps2p(ps.val/grav, sig_val, zdim)
89
100
  else
90
101
  raise ArgumentError, "v does not have a p or sigma coordinate."
@@ -102,7 +113,7 @@ module NumRu
102
113
 
103
114
  pc_over_g = pc_val / grav
104
115
 
105
- rho_v_cum = GPhys.c_cum_integ_irreg(v_val, p_over_g, zdim, nzbound,
116
+ rho_v_cum = GPhys.c_cum_integ_irreg(v_val, mask, p_over_g, zdim, nzbound,
106
117
  pc_over_g, nil)
107
118
 
108
119
  #< zonal mean & latitudinal factor >
@@ -295,6 +306,124 @@ module NumRu
295
306
  mstrm
296
307
  end
297
308
 
309
+ # Integrate v with p to ps (where v==vs if vs is given)
310
+ #
311
+ # Normally, p and ps are pressure, but they are actually arbitrary.
312
+ # The assumption here is that the ps is the upper cap of p, and
313
+ # the integration with p is from the smallest p up to ps.
314
+ # fact is a factor. E.g., 1/gravity to get mass-weighted integration
315
+ # through dp/g = -\rho dz
316
+ #
317
+ # ARGUMENT
318
+ # * v [GPhys] a multi-dimensional GPhys
319
+ # * pdim [Integer or String] The dimension of p
320
+ # * ps [GPhys] the capping value of p (~surface pressure);
321
+ # rank must be one smaller than v's (no-missing data allowed)
322
+ # * vs [nil or GPhys] v at ps (shape must be ps's); if nil, v is
323
+ # interpolated. (If vs==nil, no extrapolation is made when ps>p.max)
324
+ # * fact [nil or UNumeric or..] factor to be multiplied afterwords
325
+ # (e.g., 1/Met.g)
326
+ # * name [nil or String] name to be set
327
+ # * long_name [nil or String] long_name to be set
328
+ #
329
+ # RETURN VALUE
330
+ # * a GPhys
331
+ def integrate_w_p_to_ps(v, pdim, ps, vs: nil, fact: nil,
332
+ name: nil, long_name: nil)
333
+ pdim = v.dim_index(pdim)
334
+ p = v.coord(pdim)
335
+
336
+ if ps
337
+ if p.units !~ ps.units
338
+ raise("units mis-match #{p.units} vs #{ps.units}")
339
+ end
340
+ if p.units != ps.units
341
+ p = p.convert_units(ps.units)
342
+ end
343
+ pv = p.val
344
+ psv = ps.val
345
+ if psv.is_a?(NArrayMiss)
346
+ raise("data missing exists in ps") if psv.count_invalid != 0
347
+ psv = psv.to_na
348
+ end
349
+ else
350
+ pv = p.val
351
+ psv = NArray.float(1).fill!(pv.max)
352
+ end
353
+
354
+ vv = v.val
355
+ if vv.is_a?(NArrayMiss)
356
+ mask = vv.get_mask
357
+ misval = 9.9692099683868690e+36 # near 15 * 2^119 (as nc fill val)
358
+ vv = vv.to_na(misval)
359
+ else
360
+ mask = nil
361
+ misval = nil
362
+ end
363
+
364
+ nzbound = nil
365
+
366
+ if ps
367
+ if pv[0] > pv[-1]
368
+ # reverse the p coordinate to the increasing order
369
+ pv = pv[-1..0]
370
+ vv = vv[ *([true]*pdim + [-1..0,false]) ]
371
+ end
372
+ if vs
373
+ vsv = vs.val
374
+ if vsv.is_a?(NArrayMiss)
375
+ raise("data missing exists in vs") if vsv.count_invalid != 0
376
+ vsv = vsv.to_na
377
+ end
378
+ else
379
+ vsv = nil
380
+ end
381
+ vv, pv, nzbound = GPhys.c_cap_by_boundary(vv, pdim,
382
+ pv, true, psv, vsv, misval)
383
+ mask_e = NArray.byte(*vv.shape).fill!(1) # pdim has get longer by one
384
+ mask_e[ *( [true]*pdim + [0..-2,false]) ] = mask
385
+ mask = mask_e
386
+ end
387
+
388
+ psv = psv.newdim!(pdim) if ps
389
+ for iidx in [0,20]
390
+ sel = [iidx,50,true,0]
391
+ vi = GPhys.c_cell_integ_irreg(vv, mask, pv, pdim, nzbound, psv, nil)
392
+ end
393
+ osh = v.shape.clone
394
+ osh.delete_at(pdim)
395
+ vi.reshape!(*osh)
396
+
397
+ data = VArray.new(vi, v.data, v.name)
398
+ data.units = v.units * p.units
399
+ data = data * fact if fact
400
+ data.name = name if name
401
+ data.long_name = long_name if long_name
402
+
403
+ grid = v.grid.delete_axes(pdim)
404
+ grid.set_lost_axes( Array.new ) # reset
405
+ GPhys.new(grid, data)
406
+ end
407
+
298
408
  end
299
409
  end
300
410
  end
411
+
412
+ ######################################
413
+
414
+ if $0 == __FILE__
415
+ require "numru/ggraph"
416
+ include NumRu
417
+
418
+ v = GPhys::IO.open("../../../testdata/T.jan.nc","T")
419
+ v = v.copy.replace_val( NArray.float(*v.shape).fill!(1.0) )
420
+ ps = GPhys::IO.open("../../../testdata/pres.jan.nc","pres")
421
+ ps = ps.convert_units("Pa")
422
+ pdim = GAnalysis::Met.find_prs_d(v)
423
+ vp = GAnalysis::MetZ.integrate_w_p_to_ps(v, pdim, ps)
424
+ p "ps", ps.val
425
+ p "result 1", vp.val+1000
426
+ vs = ps * 0.0
427
+ vp = GAnalysis::MetZ.integrate_w_p_to_ps(v, pdim, ps, vs: vs)
428
+ p "result 2", vp.val+1000
429
+ end
@@ -1,5 +1,5 @@
1
1
  require "narray"
2
- require "numru/ganalysis/ganalysis_ext"
2
+ require "numru/ganalysis_ext"
3
3
 
4
4
  class NArray
5
5
  def cum_sum(dim)
@@ -1,5 +1,5 @@
1
1
  require "numru/gphys"
2
- require "numru/ganalysis/ganalysis_ext"
2
+ require "numru/ganalysis_ext"
3
3
 
4
4
  module NumRu
5
5
  module GAnalysis
@@ -87,4 +87,23 @@ if $0 == __FILE__
87
87
  eps = 1e-6
88
88
  ome = 1.95
89
89
  GAnalysis::PDE.SOR_2D_2ndorder(z, a, b, c, d, e, f, dx, dy, ome, eps: eps)
90
+
91
+ # -- make gphys object
92
+ nax = NArray.sfloat(nx).indgen!
93
+ hax = {'long_name'=>'x-coordinate', 'units'=>'grid number'}
94
+ vax = VArray.new(nax, hax, 'x')
95
+ axx = Axis.new.set_pos(vax)
96
+
97
+ nay = NArray.sfloat(ny).indgen!
98
+ hay = {'long_name'=>'y-coordinate', 'units'=>'grid number'}
99
+ vay = VArray.new(nay, hay, 'y')
100
+ axy = Axis.new.set_pos(vay)
101
+
102
+ haz = {'long_name'=>'', 'units'=>''}
103
+ vaz = VArray.new(z, haz, 'data')
104
+ gpz = GPhys.new(Grid.new(axx, axy), vaz)
105
+
106
+ GGraph.startup('iws'=>1)
107
+ GGraph.tone_and_contour gpz,true,'color_bar'=>true
108
+ GGraph.close
90
109
  end
@@ -95,6 +95,82 @@ module NumRu
95
95
  rot
96
96
  end
97
97
 
98
+ # spherical divergence for entire rectagular regional data
99
+ def div_s_box(u,v, only_u: false, only_v: false,
100
+ xs0: false, xe0: false, ys0: false, ye0: false)
101
+ lam, phi, xd, yd = get_lambda_phi(u)
102
+ lon = u.coord(xd)
103
+ lat = u.coord(yd)
104
+ cos_phi = phi.cos
105
+ nx = lam.length
106
+ ny = phi.length
107
+ if nx>2
108
+ ixends = {0..-1=>(nx-1)}
109
+ else
110
+ ixends = true
111
+ end
112
+ if ny>2
113
+ iyends = {0..-1=>(ny-1)}
114
+ else
115
+ iyends = true
116
+ end
117
+ ub = u[*([true]*xd + [ixends, false])].copy # ends along lon
118
+ vb = v[*([true]*yd + [iyends, false])].copy # ends along lat
119
+ ub.axis(xd).set_pos(lam[ixends].data)
120
+ ub.axis(yd).set_pos(phi.data)
121
+ vb.axis(xd).set_pos(lam.data)
122
+ vb.axis(yd).set_pos(phi[iyends].data)
123
+ ubi = ub.integrate(yd) # trapezoidal integration by default
124
+ vbi = vb.integrate(xd) * cos_phi[iyends]
125
+ dlam = lam[-1].val - lam[0].val
126
+ sin_phib = phi[iyends].sin.val
127
+ dsin_phi = sin_phib[-1] - sin_phib[0]
128
+ if xd<yd
129
+ xdi = xd
130
+ ydi = yd-1
131
+ else
132
+ xdi = xd-1
133
+ ydi = yd
134
+ end
135
+ scalar=false
136
+ ubi.axis(xdi).set_pos(lon[ixends]) # to lon before the cut is recoreded
137
+ vbi.axis(ydi).set_pos(lat[iyends]) # to lat before the cut is recoreded
138
+ if ubi.rank>1
139
+ xe = ubi[*([true]*xdi+[-1,false])]
140
+ xs = ubi[*([true]*xdi+[0,false])]
141
+ ye = vbi[*([true]*ydi+[-1,false])]
142
+ ys = vbi[*([true]*ydi+[0,false])]
143
+ else
144
+ # to defer the result becomes a scalar
145
+ scalar=true
146
+ xe = ubi[*([true]*xdi+[-1..-1,false])]
147
+ xs = ubi[*([true]*xdi+[0..0,false])]
148
+ ye = vbi[*([true]*ydi+[-1..-1,false])]
149
+ ys = vbi[*([true]*ydi+[0..0,false])]
150
+ end
151
+ xe *= 0.0 if xe0
152
+ xs *= 0.0 if xs0
153
+ ye *= 0.0 if ye0
154
+ ys *= 0.0 if ys0
155
+ dub = xe - xs
156
+ dvb = ye - ys
157
+ if only_u
158
+ div = dub/(@@R*dlam*dsin_phi.abs)
159
+ elsif only_v
160
+ div = dvb/(@@R*dlam.abs*dsin_phi)
161
+ else
162
+ div = dub/(@@R*dlam*dsin_phi.abs) +
163
+ dvb/(@@R*dlam.abs*dsin_phi)
164
+ # (dlam*dsin_phi).abs is the area for the unit radius
165
+ end
166
+ if scalar
167
+ div = div[0]
168
+ end
169
+ div.long_name = "div(#{u.name},#{v.name})"
170
+ div.name = "div"
171
+ div
172
+ end
173
+
98
174
  def vor_s(u,v)
99
175
  vor = rot_s(u,v)
100
176
  vor.long_name = "Relative Vorticity"
@@ -131,7 +207,7 @@ module NumRu
131
207
  end
132
208
 
133
209
  def grad_sy(s)
134
- lam, phi, lond, latd = get_lambda_phi(s)
210
+ phi, latd = get_phi(s)
135
211
  cos_phi = phi.cos
136
212
  ys = s.cderiv(latd,latbc(phi),phi) / @@R
137
213
  ys.long_name = "ygrad(#{s.name})"
@@ -196,6 +272,22 @@ module NumRu
196
272
  [lam, phi, lond, latd]
197
273
  end
198
274
 
275
+ def get_lambda(gp, err_raise=true)
276
+ lond = find_lon_dim(gp, err_raise)
277
+ lam = lond && gp.axis(lond).to_gphys.convert_units("rad") # lon in rad
278
+ lam.units = "" if lond # treat as non-dim
279
+ @@lam = lam
280
+ [lam, lond]
281
+ end
282
+
283
+ def get_phi(gp, err_raise=true)
284
+ latd = find_lat_dim(gp, err_raise)
285
+ phi = latd && gp.axis(latd).to_gphys.convert_units("rad") # lat in rad
286
+ phi.units = "" if latd # treat as non-dim
287
+ @@phi = phi
288
+ [phi, latd]
289
+ end
290
+
199
291
  # Find longitude and latitude coordinates.
200
292
  #
201
293
  # ARGUMENTS
@@ -254,6 +346,49 @@ module NumRu
254
346
  end
255
347
  [lond,latd]
256
348
  end
349
+
350
+ def find_lon_dim(gp, err_raise=false)
351
+ lond = nil
352
+ (0...gp.rank).each do |dim|
353
+ crd = gp.coord(dim)
354
+ if /^degrees?_east$/i =~ crd.get_att("units")
355
+ lond = dim
356
+ break
357
+ elsif ( ( /longitude/i =~ crd.long_name ||
358
+ /^lon/i =~ crd.name ||
359
+ (nm=crd.get_att("standard_name") && /longitude/i =~ nm ) &&
360
+ (crd.units =~ Units["degrees_east"]) ) )
361
+ lond = dim
362
+ break
363
+ end
364
+ end
365
+ if err_raise
366
+ raise("Longitude dimension was not found") if !lond
367
+ end
368
+ lond
369
+ end
370
+
371
+ def find_lat_dim(gp, err_raise=false)
372
+ latd = nil
373
+ (0...gp.rank).each do |dim|
374
+ crd = gp.coord(dim)
375
+ if /^degrees?_north$/i =~ crd.get_att("units")
376
+ latd = dim
377
+ break
378
+ elsif ( ( /latitude/i =~ crd.long_name ||
379
+ /^lat/i =~ crd.name ||
380
+ (nm=crd.get_att("standard_name") && /latitude/i =~ nm ) &&
381
+ (crd.units =~ Units["degrees_north"]) ) )
382
+ latd = dim
383
+ break
384
+ end
385
+ end
386
+ if err_raise
387
+ raise("Latitude dimension was not found") if !latd
388
+ end
389
+ latd
390
+ end
391
+
257
392
  end
258
393
 
259
394
  end
@@ -213,7 +213,217 @@ module NumRu
213
213
  divh
214
214
  end
215
215
 
216
+ # Wave activity flux by Takaya and Nakamura (2001) (TN2001)
217
+ #
218
+ # This method works by mix-in in QG (Cartesian) or QG_sphere (spherical).
219
+ # (so the call it either as QG::waf_tn2001(gph, gphbs, ..) or
220
+ # QG_sphere::waf_tn2001(gph, gphbs, ..) ).
221
+ #
222
+ # DEFINITIONS
223
+ #
224
+ # * if called through QG: a modified version of Eq (32) of TN2001
225
+ # * if called through QG_sphere: a modified version of Eq (38) of TN2001
226
+ # The modification of Eq (32) and (38) is as follows:
227
+ # * The multiplication by p is omitted. This is because
228
+ # it is more convenient to do it afterwords if needed
229
+ # (for example, to integrate with p would require not to have
230
+ # done this multiplication).
231
+ # * The C_u M term is not included as in convention.
232
+ # (but the multiplication by cos(phi) is done in QG_sphere).
233
+ # Note that this term vanishes for stationary waves, because C_u=0.
234
+ # * The normal vector (n1, n2) in TN2001 can be set flexibly.
235
+ # This vector is called (nx, ny) in this method, since n2 is used
236
+ # for the buoyancy frequency. In eqs. (32) and (38), (nx, ny)
237
+ # is prescribed as (nx, ny) = (U,V)/|U,V| where U is ubs and V is vbs.
238
+ # However, this relation is not necessarily valid, since (nx, ny)
239
+ # is originally attributed to the QGPV gradient of the basic flow.
240
+ # Therefore, in this method, (nx, ny) can be set arbitrary.
241
+ # Also, one can set (nx, ny) = (1, 0) or (0, 1) by setting the
242
+ # optional argument zonal or merid to true.
243
+ #
244
+ # ARGUMENTS
245
+ # * gph: geopotential height
246
+ # * gphbs: geopotential height (basic state). Basically, it needs to have
247
+ # the same spacial grid as gph, but it can be a meridional section
248
+ # (such as zonal mean), if the normal vector is specified by
249
+ # (ubs, vbs) or (nx, ny) or zonal or merid.
250
+ # * nx, ny: normalized vector to designate the direction of wave
251
+ # propagation. By default, set to the direction of (ubs, vbs),
252
+ # or set by zonal/merid (see below).
253
+ # * ubs, vbs: Basic state zonal and meridional winds to compute
254
+ # the normal vector (nx,ny) as (ubs,vbs)/sqrt(ubs**2 + vbs**2).
255
+ # Neglected if nx and ny are specified. By default ubs and vbs
256
+ # are derived geostrophically from gphbs
257
+ # * zonal: derive the flux in the zonal direction assuming (nx,ny)=(1,0)
258
+ # * merid: derive the flux in the meridional dir assuming (nx,ny)=(0,1)
259
+ # * n2: Brunt-Vaisala (buoyancy) frequency squared. By default, derived
260
+ # from gphbs
261
+ # * derive_M0: derive M (the wave-activity pseudomomentum of TN20001)
262
+ # when the wave is stationary (cp=0). You need to give ubs and vbs
263
+ # (or have them derived from gphbs by not using nx, ny, zonal or merid)
264
+ # to make this option available.
265
+ #
266
+ # RETURN VALUE
267
+ # * [wx, wy, wz, ret_hash] where [wx, wy, wz] are the WAF vector
268
+ # and ret_hash provides extra derived quantities such as the
269
+ # perturbation stream function (:pp), pseudomomentum M0 (:M0), etc.
270
+ #
271
+ # REFERENCE
272
+ # * Takaya and Namaura (2001): A formulation of a phase-independent
273
+ # wave-activity flux for stationary and migratory quasigeostrophic
274
+ # eddies on a zonally varying basic flow. J. Atmos. Sci, 58, 608-627.
275
+ #
276
+ def waf_tn2001(gph, gphbs, nx: nil, ny: nil, ubs: nil, vbs: nil,
277
+ zonal: false, merid: false, n2: nil,
278
+ derive_M0: false)
279
+
280
+ #< prepration >
281
+ if self==QG
282
+ f = f0
283
+ wgt = nil
284
+ elsif self==QG_sphere
285
+ f = f_mask0(gph)
286
+ wgt = cos_phi(gph)
287
+ else
288
+ raise("unsupported module to mix-in #{self}")
289
+ end
290
+ ret_hash = Hash.new
291
+
292
+ #< perturbation stream function >
293
+ gf = Met::g / f
294
+ pp = (gph - gphbs)*gf # psi' (perturbation stream function)
295
+ if /gpm/i =~ (us=pp.units.to_s)
296
+ pp.units = us.sub(/gpm/i,'m')
297
+ end
298
+ ret_hash[:pp] = pp
299
+
300
+ #< derive nx, ny if not given >
301
+ ul = psibs = nil
302
+ if zonal
303
+ nx = 1.0
304
+ ny = 0.0
305
+ elsif merid
306
+ nx = 0.0
307
+ ny = 1.0
308
+ elsif nx.nil? && ny.nil?
309
+ if !ubs || !vbs
310
+ psibs, gpref = gph2psi_gpref(gphbs)
311
+ if /gpm/i =~ (us=psibs.units.to_s)
312
+ psibs.units = us.sub(/gpm/i,'m')
313
+ end
314
+ ubs, vbs = psi2ug_vg(psibs)
315
+ ret_hash[:ubs] = ubs
316
+ ret_hash[:vbs] = vbs
317
+ ret_hash[:gpref] = gpref
318
+ end
319
+ ul = (ubs**2+vbs**2).sqrt
320
+ nx = ubs/ul
321
+ ny = vbs/ul
322
+ end
323
+
324
+ #< derive aspect ratio >
325
+ unless n2
326
+ n2 = LogP::gph2n2(gphbs)
327
+ ret_hash[:n2] = n2
328
+ end
329
+ eps = f**2/n2
330
+
331
+ #< cross terms --> WAF >
332
+ px,py = grad_h(pp)
333
+ pxx,pxy = grad_h(px)
334
+ pyy = grad_hy(py) unless ny==0.0
335
+ pz = LogP::pcdata_dz(pp)
336
+ pxz = LogP::pcdata_dz(px) unless nx==0.0
337
+ pyz = LogP::pcdata_dz(py) unless ny==0.0
338
+ ret_hash[:px] = px
339
+ ret_hash[:py] = py
340
+
341
+ wx = nil
342
+ unless nx==0.0
343
+ wx = px*px - pp*pxx
344
+ wy = px*py - pp*pxy
345
+ wz = ( px*pz - pp*pxz ) * eps
346
+ nx2 = nx/2.0
347
+ wx = wx * nx2
348
+ wy = wy * nx2
349
+ wz = wz * nx2
350
+ end
351
+
352
+ unless ny==0.0
353
+ wx_ = px*py - pp*pxy
354
+ wy_ = py*py - pp*pyy
355
+ wz_ = (py*pz - pp*pyz) * eps
356
+ ny2 = ny/2.0
357
+ wx_ = wx_ * ny2
358
+ wy_ = wy_ * ny2
359
+ wz_ = wz_ * ny2
360
+ if wx
361
+ wx += wx_
362
+ wy += wy_
363
+ wz += wz_
364
+ else
365
+ wx = wx_
366
+ wy = wy_
367
+ wz = wz_
368
+ end
369
+ end
216
370
 
371
+ if wgt
372
+ wx = wx * wgt
373
+ wy = wy * wgt
374
+ wz = wz * wgt
375
+ end
376
+ wx.name = "Wx"
377
+ wy.name = "Wy"
378
+ wz.name = "Wz"
379
+ wx.long_name = "WAFx"
380
+ wy.long_name = "WAFy"
381
+ wz.long_name = "WAFz"
382
+
383
+ #< derive M0 if required >
384
+ if derive_M0
385
+ raise("ubs and vbs are needed to derive M0") if !ubs || !vbs
386
+ ul = (ubs**2+vbs**2).sqrt unless ul
387
+ unless psibs
388
+ psibs, gpref = gph2psi_gpref(gphbs)
389
+ if /gpm/i =~ (us=psibs.units.to_s)
390
+ psibs.units = us.sub(/gpm/i,'m')
391
+ end
392
+ ret_hash[:gpref] = gpref
393
+ end
394
+ n2p = Planet::ave_s(n2)
395
+ if n2p.rank >= 2
396
+ pdim = Met.find_prs_d(n2p)
397
+ ds = Array.new
398
+ (0...n2p.rank).each do |d|
399
+ ds.push(d) unless d == pdim
400
+ end
401
+ n2p = n2p.mean(*ds)
402
+ end
403
+ qp, = psi2q(pp,n2p)
404
+ qbs, = psi2q(psibs,n2p)
405
+ qbx, qby = grad_h(qbs)
406
+ e2 = px**2 + py**2 + pz**2 * eps # 2*e
407
+ qbgrad = (qbx**2+qby**2).sqrt.running_mean(0,3).running_mean(1,3)
408
+ a = qp**2 / qbgrad / 4.0 # A of TN2001
409
+ e0 = e2 / ul / 4.0 # E of TN2001 when cp=0
410
+ m0 = a + e0
411
+ a.name = "A"
412
+ a.long_name = "A"
413
+ e0.name = "E0"
414
+ e0.long_name = "stationary E"
415
+ m0.name = "M0"
416
+ m0.long_name = "stationary TN2001 pseudomomentum"
417
+ ret_hash[:qp] = qp
418
+ ret_hash[:qbs] = qbs
419
+ ret_hash[:A] = a
420
+ ret_hash[:E0] = e0
421
+ ret_hash[:M0] = m0
422
+ end
423
+
424
+ #< return >
425
+ [wx, wy, wz, ret_hash]
426
+ end
217
427
  end
218
428
 
219
429
  ######################################################
@@ -409,6 +619,12 @@ module NumRu
409
619
  def grad_h(gphys)
410
620
  @@bp.grad_h(gphys)
411
621
  end
622
+ def grad_hy(gphys)
623
+ @@bp.grad_y(gphys)
624
+ end
625
+ def grad_hx(gphys)
626
+ @@bp.grad_x(gphys)
627
+ end
412
628
 
413
629
  # horizontal divergence (Cartesian)
414
630
  def div_h(gphys_u, gphys_v)
@@ -442,9 +658,8 @@ module NumRu
442
658
 
443
659
  # geopotential height -> geostrophic winds
444
660
  def gph2ug_vg(gph)
445
- gpx, gpy = Planet::grad_s(gph * Met::g)
446
- f = f_mask0(gph)
447
- [ -gpy/f, gpx/f]
661
+ psi, gpref = gph2psi_gpref(gph)
662
+ psi2ug_vg(psi)
448
663
  end
449
664
 
450
665
  # geopotential height -> QG stream function and the reference geopotential
@@ -524,6 +739,12 @@ module NumRu
524
739
  def grad_h(gphys)
525
740
  Planet::grad_s(gphys)
526
741
  end
742
+ def grad_hx(gphys)
743
+ Planet::grad_sx(gphys)
744
+ end
745
+ def grad_hy(gphys)
746
+ Planet::grad_sy(gphys)
747
+ end
527
748
 
528
749
  # horizontal divergence (spherical)
529
750
  def div_h(fx, fy)