def getMLvec(ntrunc, m="pn"): if ntrunc == 0: M = [0] L = [0] elif m == "pn": m, l = getspecindx(ntrunc) M = list(m) + list(-m[ntrunc + 1 :]) L = list(l) + list(l[ntrunc + 1 :]) elif m == "p": M, L = getspecindx(ntrunc) return zip(M, L)
def getMLvec(ntrunc,m='pn'): if ntrunc==0: M = [0] L = [0] elif m=='pn': m,l = getspecindx(ntrunc) M = list(m) + list(-m[ntrunc+1:]) L = list(l) + list(l[ntrunc+1:]) elif m=='p': M,L = getspecindx(ntrunc) return zip(M,L)
def sph_filter(field, l_cut, gridtype='gaussian'): """Remove all waves above a specific spherical wavenumber l_cut""" nlat, nlon = len(field.lat), len(field.lon) grid = spharm.Spharmt(nlon, nlat, gridtype=gridtype) other_dims = [d for d in field.dims if d not in ('lat', 'lon')] # need the field in N-S, E-W form, (lat, lon, other coords) vfield = (field.sel(lat=sorted(cbbt.lat, reverse=True), lon=sorted(cbbt.lon)).transpose( 'lat', 'lon', *other_dims)) ntrunc = nlat - 1 m, n = spharm.getspecindx(ntrunc) l = m + n # transform to spherical modes, eliminate high wavenumbers and transform back coeffs = grid.grdtospec(vfield.values, ntrunc) mask = l > l_cut coeffs[mask] = 0 values = grid.spectogrd(coeffs) # copy the original field and comply to the same coordinate ordering nfield = vfield.copy() nfield.values = values return nfield.transpose(*field.dims)
def spht(field, ntrunc=None, gridtype='gaussian'): """Transform a field on lat-lon grid to spherical harmonics. Currently only works for: - fields defined on the lat-lon points, not latb-lonb. - triangular truncation. Returns an xarray.DataArray """ nlat, nlon = len(field.lat), len(field.lon) grid = spharm.Spharmt(nlon, nlat, gridtype=gridtype) if ntrunc is None: ntrunc = nlat - 1 other_dims = [d for d in field.dims if d not in ('lat', 'lon')] # need the field in N-S, E-W form, (lat, lon, other coords) vfield = (field.sel(lat=sorted(cbbt.lat, reverse=True), lon=sorted(cbbt.lon)).transpose( 'lat', 'lon', *other_dims)) # calculate the spectral coefficients, given the truncation level coeffs = grid.grdtospec(vfield.values, ntrunc) m, n = spharm.getspecindx(ntrunc) # put the coefficients into a grid, half of which will be empty # due to the triangular trunctation cc = np.zeros((ntrunc + 1, ntrunc + 1) + tuple(coeffs.shape[1:]), dtype=np.complex128) cc[m, n] = coeffs coords = [('m', np.arange(0, ntrunc + 1)), ('n', np.arange(0, ntrunc + 1))] for d in other_dims: coords.append((d, field.coords[d])) return xr.DataArray(data=cc, coords=coords, name=field.name)
def __init__(self, rsphere=EARTH_RADIUS, omega=EARTH_OMEGA, resolution=2.5, ntrunc=None, legfunc="stored"): """Grid constructor. - The radius and angular velocity of the planet are given by `rsphere` in m and `omega` in 1/s, respectively (the default values correspond to those of the Earth). - The grid resolution is uniform and specified with the `resolution` parameter in degrees. - Uses spherical harmonics for some operations utilizing the `spharm`/`pyspharm` package. By default (`ntrunc=None`, `legfunc="stored"`), the spherical harmonics are truncated after (number of latitudes - 1) functions and precomputed. Consult the documentation of `spharm.Spharmt` for more information on these parameters. """ # Planet parameters (radius, angular velocity) self.rsphere = rsphere self.omega = omega # Setup longitude and latitude grid. Make sure longitude 0° is not # repeated and that both latitudes 90° and -90° exist. Latitudes start # at the North pole, which is the convention used by pyspharm. self.nlon = int(360. / resolution) self.nlat = int(180. / resolution) + 1 if self.nlat % 2 == 0: raise ValueError( "Number of latitudes must be odd but is {}".format(self.nlat)) self.lons = np.linspace(0., 360., self.nlon, endpoint=False) self.lats = np.linspace(90., -90., self.nlat, endpoint=True) self.lon, self.lat = np.meshgrid(self.lons, self.lats) # Grid spacing in degrees self.dlon = resolution self.dlat = -resolution # Spherical coordinate grid (for use with trigonometric functions) self.lams = np.deg2rad(self.lons) self.phis = np.deg2rad(self.lats) self.lam, self.phi = np.meshgrid(self.lams, self.phis) # Grid spacing in radians self.dlam = np.deg2rad(self.dlon) self.dphi = np.deg2rad(self.dlat) # Precompute Coriolis field self.fcor = self.coriolis(self.lat) # Spherical harmonic transform object self._spharm = spharm.Spharmt(self.nlon, self.nlat, rsphere=rsphere, gridtype="regular", legfunc=legfunc) self._ntrunc = (self.nlat - 1) if ntrunc is None else ntrunc _, self.specindxn = spharm.getspecindx(self._ntrunc) # Eigenvalues of the horizontal Laplacian for each spherical harmonic. # Force use of complex64 datatype (= 2 * float32) because spharm will # cast to float32 components anyway but the multiplication with the # python scalars results in float64 values. self.laplacian_eigenvalues = (self.specindxn * (1. + self.specindxn) / rsphere / rsphere).astype( np.complex64, casting="same_kind")
def __init__(self,sp,dt,ntrunc,ptop=0.,p0=1.e5,grav=9.80616,omega=7.292e-5,cp=1004,\ rgas=287.,efold=3600.,ndiss=8,tdrag=1.e30,tdiab=1.e30,\ umax=40,jetexp=2,delth=20,moistfact=1.0): # set model parameters self.p0 = p0 # mean surface pressure self.ptop = ptop # model top pressure self.rgas = rgas # gas constant for dry air self.grav = grav # gravity self.omega = omega # rotation rate self.cp = cp # specific heat of dry air at constant pressure self.delth = delth # static stability # factor to reduce static stability in rising air # (crude moist physics assuming air is saturated) # moistfact = 1 is dry model self.moistfact = moistfact dp = 0.5*(ptop-p0) exnf1 = cp*((p0+0.5*dp)/p0)**(rgas/cp) exnf2 = cp*((p0+1.5*dp)/p0)**(rgas/cp) self.delta_exnf = exnf2-exnf1 # diff in exner function between 2 levs. # efolding time scale for hyperdiffusion at shortest wavenumber self.efold = efold self.ndiss = ndiss # order of hyperdiffusion (2 for laplacian) self.sp = sp # Spharmt instance self.ntrunc = ntrunc # triangular truncation wavenumber self.dt = dt # time step (secs) self.tdiab = tdiab # lower layer drag timescale self.tdrag = tdrag # interface relaxation timescale # create lat/lon arrays delta = 2.*np.pi/sp.nlon if sp.gridtype == 'regular': lats1d = 0.5*np.pi-delta*np.arange(sp.nlat) wts = np.cos(lats1d) else: lats1d,wts = gaussian_lats_wts(sp.nlat) lats1d = lats1d*np.pi/180. # convert to radians. lons1d = np.arange(-np.pi,np.pi,delta) lons,lats = np.meshgrid(lons1d,lats1d) self.lons = lons; self.lats = lats # weights for computing global means. self.globalmeanwts = np.ones((sp.nlat,sp.nlon))*wts[:,np.newaxis] self.globalmeanwts = self.globalmeanwts/self.globalmeanwts.sum() self.f = 2.*omega*np.sin(lats)[:,:,np.newaxis] # coriolis # create laplacian operator and its inverse. indxm, indxn = getspecindx(ntrunc) indxn = indxn.astype(np.float32) totwavenum = indxn*(indxn+1.0) self.lap = -totwavenum/sp.rsphere**2 self.ilap = np.zeros(self.lap.shape, np.float32) self.ilap[1:] = 1./self.lap[1:] # hyperdiffusion operator self.hyperdiff = -(1./efold)*(totwavenum/totwavenum[-1])**(ndiss/2) # set equilibrium layer thicknes profile. self._interface_profile(umax,jetexp)
def __init__(self,shape,lengths): self.shape=shape self.lengths=lengths self.spharm=spharm.Spharmt(self.shape[1],self.shape[0],legfunc='computed') #Define the inverse laplacian in spectral space: setattr(self,'_s_Inv_Laplacian',self.spharm.grdtospec(np.zeros(self.shape))) self.legendre_order=spharm.getspecindx(self.shape[0]-1)[1] self._s_Inv_Laplacian[1:]=(self.legendre_order[1:]*(self.legendre_order[1:]+1)) self._s_Inv_Laplacian[0]=1.0 self._s_Inv_Laplacian/=self.lengths.rsphere**2 #To shift input to have first index at Greenwich meridian self.shift_index=np.argmin(np.abs(self.lengths.lon)) #For putting on the C-grid: #self.spharm_regrid=spharm.Spharmt(self.shape[1]*2,(self.shape[0]-1)*2+1,legfunc='computed') return
def __init__(self,sp,dt,ntrunc,theta1=300,theta2=330,grav=9.80616,omega=7.292e-5,cp=1004,\ zmid=5.e3,ztop=15.e3,efold=3600.,ndiss=8,tdrag=1.e30,tdiab=1.e30,umax=30,jetexp=4): # setup model parameters self.theta1 = theta1 # lower layer pot. temp. self.theta2 = theta2 # upper layer pot. temp. self.delth = theta2-theta1 # difference in potential temp between layers self.grav = grav # gravity self.omega = omega # rotation rate self.cp = cp # Specific Heat of Dry Air at Constant Pressure, self.zmid = zmid # resting depth of lower layer (m) self.ztop = ztop # resting depth of both layers (m) # efolding time scale for hyperdiffusion at shortest wavenumber self.efold = efold self.ndiss = ndiss # order of hyperdiffusion (2 for laplacian) self.sp = sp # Spharmt instance self.ntrunc = ntrunc # triangular truncation wavenumber self.dt = dt # time step (secs) self.tdiab = tdiab # lower layer drag timescale self.tdrag = tdrag # interface relaxation timescale # create lat/lon arrays delta = 2.*np.pi/sp.nlon if sp.gridtype == 'regular': lats1d = 0.5*np.pi-delta*np.arange(sp.nlat) else: lats1d,wts = gaussian_lats_wts(sp.nlat) lats1d = lats1d*np.pi/180. lons1d = np.arange(-np.pi,np.pi,delta) lons,lats = np.meshgrid(lons1d,lats1d) self.lons = lons self.lats = lats self.f = 2.*omega*np.sin(lats)[:,:,np.newaxis] # coriolis # create laplacian operator and its inverse. indxm, indxn = getspecindx(ntrunc) indxn = indxn.astype(np.float32)[:,np.newaxis] totwavenum = indxn*(indxn+1.0) self.lap = -totwavenum/sp.rsphere**2 self.ilap = np.zeros(self.lap.shape, np.float32) self.ilap[1:,:] = 1./self.lap[1:,:] # hyperdiffusion operator self.hyperdiff = -(1./efold)*(totwavenum/totwavenum[-1])**(ndiss/2) # initialize orography to zero. self.orog = np.zeros((sp.nlat,sp.nlon),np.float32) # set equilibrium layer thicknes profile. self._interface_profile(umax,jetexp)
times = nc['time'][:].tolist() levels = nc['plev'][:].tolist() ntime = times.index(fhour) nlev = levels.index(level) lons = nc['longitude'][:] lats = nc['latitude'][:] nlons = len(lons) nlats = len(lats) re = 6.3712e6 ntrunc = nlats - 1 spec = Spharmt(nlons, nlats, rsphere=re, gridtype='regular', legfunc='computed') indxm, indxn = getspecindx(ntrunc) degree = indxn.astype(np.float) if int(nc['time'][ntime]) != fhour: raise ValueError('incorrect forecast time') fcst_data1 = nc[varnc][ntime, nlev, ...] nc.close() if fhour > 9: fcstfile = '%s/%s/fv3longcontrol2_historyp_%s_latlon.nc' % (datapath2, date, date) else: fcstfile = '%s/%s/fv3control2_historyp_%s_latlon.nc' % (datapath2, date, date) nc = Dataset(fcstfile) if int(nc['time'][ntime]) != fhour: raise ValueError('incorrect forecast time') fcst_data2 = nc[varnc][ntime, nlev, ...]
import numpy as np # set up orthographic map projection. map = Basemap(projection='ortho',lat_0=30,lon_0=-60,resolution='l') # draw coastlines, country boundaries, fill continents. map.drawcoastlines() # draw the edge of the map projection region (the projection limb) map.drawmapboundary() # draw lat/lon grid lines every 30 degrees. map.drawmeridians(np.arange(0,360,30)) map.drawparallels(np.arange(-90,90,30)) min = int(raw_input('input degree (m) of legendre function to plot:')) nin = int(raw_input('input order (n) of legendre function to plot:')) nlons = 720; nlats = 361 x = Spharmt(nlons,nlats,legfunc='computed') ntrunc = nlats-1 indxm, indxn = getspecindx(ntrunc) nm = -1 i = 0 for m,n in zip(indxm,indxn): if m == min and n == nin: nm = i exit else: i = i + 1 if nm < 0: raise ValueError('invalid m,n - must fit within triangular truncation at wavenumber '+repr(ntrunc)) coeffs = np.zeros((ntrunc+1)*(ntrunc+2)/2,np.complex) coeffs[nm] = 1. spharmonic = x.spectogrd(coeffs) delta = 360./nlons lats = 90.-delta*np.arange(nlats)
def wavenumbers(self): """ Wavenumbers corresponding to the spectral fields. """ return getspecindx(self.truncation)
def performConvolution(self, out_times=None, ntrunc=None, topo=None, verbose=False, eliter=5, nrem=1, massconerr=1e-2): """Convolve an ice load and an earth response model in fft space. Calculate the uplift associated with stored earth and ice model. Parameters ---------- out_times : an array of times at which to caluclate the convolution. (default is to use previously stored values). ntrunc : int The truncation number of the spherical harmonic expansion. Default is from the earth model, must be <= ice model's and < grid.nlat topo : array Topography on which to compute. If None (default), assumes a flat topography. Must be the same shapt as ice. verbose : boolean Display progress on computation. Depends on progressbar module. Default is False. eliter : int The maximum number of iterations allowed to compute initial elastic response to redistributed load at each stage. If 0, instantaneous elastic response is not computed. Default 5. nrem : int Number of removal stages between the provided ice stages (intermediate steps are interpolated linearly). Default 1. Results ------- observerDict : GiaSimOutput A dictionary whose keys are fields of interest, such as uplift ('upl'), geoid ('geo'), and solid surface topography ('sstopo'), computed on the input grid at out_times. """ DENICE = 931. #934. # kg/m^3 DENWAT = 1000. #999. # kg/m^3 DENSEA = 1000. #1029. # kg/m^3 GSURF = 9.815 # m/s^2 #PAperM = DENSEA*GSURF NREM = nrem # number of intermediate steps earth = self.earth ice = self.ice grid = self.grid if topo is None and self.topo is not None: topo = self.topo if topo is not None: assert topo.shape == ice.shape, 'Topo and Ice must have the same shape' # Resolution ntrunc = ntrunc or min(earth.nmax, ice.nlat-1) assert ntrunc <= ice.nlat-1, 'ntrunc > ice.nlat-1' ms, ns = spharm.getspecindx(ice.nlat-1) # npad is the indices in the larger (padded) array of spherical # harmonics that correspond to the smaller (response) array. npad = (ns <= ntrunc) # Store out_times if out_times is None: out_times = self.out_times else: out_times = out_times self.out_times = out_times assert out_times is not None, 'out_times is not set' # Calculate times of intermediate removal stages. diffs = np.diff(ice.times) addRemovalTimes = [] for i in range(1, NREM+1): addRemovalTimes.append(ice.times[:-1]+i*diffs/NREM) addRemovalTimes = np.array(addRemovalTimes).flatten() remTimes = np.union1d(ice.times, addRemovalTimes)[::-1] calcTimes = np.union1d(remTimes, out_times)[::-1] # Initialize output observer observerDict = initialize_output(self, out_times, calcTimes, ice.nlat-1, ntrunc, ns, ice.shape) for o in observerDict: o.loadStageUpdate(ice.times[0], sstopo=topo) esl = 0 # Equivalent sea level assumed to start at 0. elRespArray = earth.getResp(0.) ssResp = np.zeros_like(ns) ssResp[npad] = observerDict['SS'].isolateRespArray(elRespArray) # Convolve each ice stage to the each output time. # Primary loop: over ice load changes. for icea, ta, iceb, tb in ice.pairIter(): ################### LOAD STAGE CALCULATION ################### # Determine the water load redistribution for ice, uplift, and # geoid changes between ta and tb, if topo is not None: # Get index for starting time. nta = observerDict['SS'].locateByTime(ta) # Collect the solid-surface topography at beginning of step. Ta = observerDict['sstopo'].array[nta] # Redistribute the ocean by change in ocean floor / surface. ssa, ssb = observerDict['SS'].array[[nta, nta+1]] dSS = self.harmTrans.spectogrd(ssb-ssa) dhwBarU = sealevelChangeByUplift(dSS, Ta+DENICE/DENSEA*icea, grid) dhwU = oceanUpliftLoad(dhwBarU, Ta+DENICE/DENSEA*icea, dSS) # Update the solid-surface topography with uplift / geoid. Tb = Ta + dSS - dhwBarU esl += dhwBarU dLoad = dhwU.copy() dwLoad = dhwU.copy() # Save the water load # Redistribute ice, consistent with current floating ice. dILoad, dhwBarI = floatingIceRedistribute(icea, iceb, Tb, grid, DENICE/DENSEA) # Combine loads from ocean changes and ice volume changes. dLoad += dILoad esl += dhwBarI dwLoad += volumeChangeLoad(dhwBarI, Tb+DENICE/DENSEA*iceb) Tb -= dhwBarI # Calculate instantaneous (elastic and gravity) responses to # the load shift and redistribute ocean accordingly. # Note: WE DO NOT CURRENTLY RECHECK FOR FLOATING ICE LOADS. if eliter: # Get elastic and geoid response to the water load. # Find the elastic uplift in response to stage's load # redistribution. dSSel = self.harmTrans.spectogrd((ssResp)*\ self.harmTrans.grdtospec(dLoad)) dhwBarUel = sealevelChangeByUplift(dSSel, Tb+DENICE/DENSEA*iceb, grid) dhwUel = oceanUpliftLoad(dhwBarUel, Tb+DENICE/DENSEA*iceb, dSSel) Tb = Tb + dSSel - dhwBarUel esl += dhwBarUel dLoad = dLoad + dhwUel dwLoad += dhwUel # Iterate elastic responses until they are sufficiently small. for i in range(eliter): # Need to save elastic uplift and geoid at each iteration # to compare to previous steps for convergence. dSSelp = self.harmTrans.spectogrd((ssResp)*\ self.harmTrans.grdtospec(dhwUel)) dhwBarUel = sealevelChangeByUplift(dSSelp, Tb+DENICE/DENSEA*iceb, grid) dhwUel = oceanUpliftLoad(dhwBarUel, Tb+DENICE/DENSEA*iceb, dSSelp) # Correct topography Tb = Tb + dSSelp - dhwBarUel esl += dhwBarUel dLoad = dLoad + dhwUel dwLoad += dhwUel # Truncation error from further iteration err = np.mean(np.abs(dSSelp))/np.mean(np.abs(dSSel)) if err <= massconerr: break else: dSSel = dSSel + dSSelp continue observerDict['SS'].array[nta+1] += self.harmTrans.grdtospec(dSSel) for o in observerDict: # Topography and load for time tb are updated and saved. o.loadStageUpdate(tb, dLoad=dLoad, topo=Tb+iceb*(Tb + DENICE/DENSEA*iceb>=0), esl=esl, dwLoad=dwLoad, sstopo=Tb) else: dLoad = (iceb-icea)*DENICE/DENSEA Tb = None for o in observerDict: # Topography and load for time tb are updated and saved. o.loadStageUpdate(tb, dLoad=dLoad) # Transform load change into spherical harmonics. loadChangeSpec = self.harmTrans.grdtospec(dLoad)/NREM # Check for mass conservation. massConCheck = np.abs(loadChangeSpec[0])/np.abs(loadChangeSpec.max()) if verbose and massConCheck >= massconerr: print("Load at {0} doesn't conserve mass: {1}.".format(ta, massConCheck)) # N.B. the n=0 load should be zero in cases of glacial isostasy, as # mass is conserved during redistribution. ################# RESPONSE STAGE CALCULATION ################# # Secondary loop: over output times. for inter_time in np.linspace(tb, ta, NREM, endpoint=False)[::-1]: # Perform the time convolution for each output time for t_out in calcTimes[calcTimes < inter_time]: respArray = earth.getResp(inter_time-t_out) for o in observerDict: o.respStageUpdate(t_out, respArray, DENSEA*loadChangeSpec) # Don't keep the intermediate uplift stages for water redistribution #observerDict.removeObserver('eslUpl', 'eslGeo') return observerDict