from pseudo import * import matplotlib.pyplot as plt import ops flowDict = {'a':0.5/1.72, 'b':0.1/1.72, 'Re':800./1.72} N = 151 linOps = ops.linearize(N=N,flowClass="bl",Y=15.) OSSunweighted = linOps.OSS(**flowDict) OSS = linOps._weightMat(OSSunweighted) # Don't need to solve the eigenvalue problem for this. # All I need to do is check if the eigenvalues from S&H hold. cSH = np.array([0.391929-0.043498j, 0.481315-0.139048j, 0.281945-0.264561j, 0.641946-0.290094j , 0.518690-0.353818j, 0.815075-0.378080j, 0.729010-0.420048j, 0.18924427-0.109716j, 0.33172068-0.190194j, 0.449629-0.253847j, 0.555771-0.3070491j, 0.56631838-0.351969j, 0.751205-0.389606j, 0.845408-0.420542j]) if True: evals, evecs = linOps.eig(linOps.OSS(**flowDict), weighted=True) omega = -1.j*evals c = omega/flowDict['a'] plt.scatter(cSH.real, cSH.imag,marker="x") plt.scatter(c.real, c.imag,marker="+") plt.xlabel('$c_r$',fontsize=15) plt.ylabel('$c_i$',fontsize=15) plt.xlim([0.15,0.9]) plt.ylim([-0.5,0.]) plt.title('Low-speed eigenvalues for a boundary-layer, \n with a=0.5,b=0.1,Re=800 using N=%d'%N,fontsize=15) plt.legend(('Schmid & Henningson', 'Current work')) fig = plt.gcf() fig.set_size_inches(6,6)
from pseudo import * import matplotlib.pyplot as plt import ops flowDict = {'a':0.5, 'b':1., 'Re':2000.} N = 81 y = chebdif(N,1)[0] U = 1.- y**2; dU = -2.*y; d2U=-2.*np.ones(U.size) linOps = ops.linearize(N=N,flowClass="channel", U=U, dU=dU, d2U=d2U,Re=flowDict['Re'] ) #linOps = ops.linearize(N=N,flowClass="channel") evals, evecs = linOps.eig(linOps.OSS(**flowDict), weighted=True) #evals, evecs = linOps.eig(OSS, b=b, weighted=True) print(evals.shape, evecs.shape, y.shape) omega = 1.j*evals c = omega/flowDict['a'] cSH = [0.3723-0.0374j, 0.4994-0.0992j , 0.8877-0.1095j, 0.8880-0.1096j, 0.7953-0.1933j , 0.7265-0.2610j , 0.6478-0.2697j, 0.7047-0.2987j , 0.4332-0.3066j,\ 0.9776-0.0236j , 0.9329-0.0683j , 0.8882-0.1131j, 0.8435-0.1578j , 0.3123-0.1699j, 0.3123-0.1699j , 0.7987-0.2025j , 0.7536-0.2470j, 0.5347-0.2714j, 0.5347-0.2715j] plt.scatter(np.real(cSH),np.imag(cSH),marker="x") #plt.hold(True) plt.scatter(np.real(c[0:]),np.imag(c[0:]),marker="+") plt.xlabel('cr') plt.ylabel('ci') plt.title('Least-stable eigenvalues for a channel flow, \n with a=0.5,b=1.0,Re=2000 using N=151') plt.legend(('Schmid & Henningson', 'Current work'),loc='lower left') plt.xlim([0.,1.5]) plt.ylim([-1.,0.2])
def H2norm(aArr, bArr, Re=2000., N=41, turb=False, eddy=False, epsArr=np.array([1. / 2000.]), y0Arr=np.array([-0.9]), fsAmp=np.identity(3), tau=None): """ Compute H2 norms for Fourier modes Inputs: aArr : Range of streamwise wavenumbers bArr : Range of spanwise wavenumbers Re (= 2000): Reynolds number N (= 41 ) : Number of collocation nodes (internal) turb (=False): If True, use turbulent base flow, otherwise laminar eddy (=False): If True, use eddy viscosity in the dynamics matrix tau (=None): If set to a positive integer, uses exponential discounting, by multiplying the energy at each time with e^{-t/tau}, so that later times (scaled by tau) contribute lesser to the norm Outputs: H2normArr of shape (aArr.size,bArr.size) """ linInst = ops.linearize(N=N, Re=Re, turb=turb, flowClass='channel') weightDict = pseudo.weightMats(N) H2normArr = np.zeros((aArr.size, bArr.size, y0Arr.size, fsAmp.shape[0])) I2 = np.identity(2 * N, dtype=np.complex) if (tau is not None) and (tau > 0.): print( "Using exponential discounting for finite time horizon, over tau=", tau) diffA = -(1. / tau) * I2 else: diffA = 0. * I2 fsArr = np.zeros((y0Arr.size, fsAmp.shape[0], 3 * N)) y = linInst.y N = linInst.N D1 = linInst.D1 W2 = weightDict['W2'] for i2 in range(y0Arr.size): try: eps = epsArr[i2] except: eps = epsArr[-1] y0 = y0Arr[i2] fsArr[i2] = _fs(fsAmp=fsAmp, eps=eps, y0=y0, N=N, Re=Re, turb=turb)['fs'] def _FsFun(fsVec, a, b, DeltaInv): # fsVec should be shape (3,N) fsVec = fsVec.reshape((3, N)) Fs = np.zeros((2 * N), dtype=np.complex) Fsadj = Fs.copy() Fs[:N] += -1.j * a * DeltaInv @ (D1 @ fsVec[0]) Fs[N:] += 1.j * b * fsVec[0] Fs[:N] += -(a**2 + b**2) * DeltaInv @ fsVec[1] Fs[N:] += 0. Fs[:N] += -1.j * b * DeltaInv @ (D1 @ fsVec[2]) Fs[N:] += -1.j * a * fsVec[2] Fsadj += 1. / (a**2 + b**2) * W2 @ np.concatenate( (-1.j * a * D1 @ fsVec[0], -1.j * b * fsVec[0])) Fsadj += W2 @ np.concatenate((fsVec[1], np.zeros(N, dtype=np.complex))) Fsadj += 1. / (a**2 + b**2) * W2 @ np.concatenate( (-1.j * b * D1 @ fsVec[2], 1.j * a * fsVec[2])) return Fs, Fsadj #print("Running for aArr=",aArr) for i0 in range(aArr.size): a = aArr[i0] print("a=", a) for i1 in range(bArr.size): b = bArr[i1] systemDict = linInst.makeSystem(a=a, b=b, eddy=eddy, adjoint=True) A = systemDict['A'] Aadj = systemDict['Aadj'] C = systemDict['C'] B = systemDict['B'] Cadj = B.copy() DeltaInv = systemDict['DeltaInv'] # In case finite time horizon is to be used A = A + diffA Aadj = Aadj + diffA Y = solve_sylvester(Aadj, A, -Cadj @ C) for i2 in range(y0Arr.size): y0 = y0Arr[i2] try: eps = epsArr[i2] except: epsArr[-1] for i3 in range(fsAmp.shape[0]): fsVec = fsArr[i2, i3].flatten() Fs, Fsadj = _FsFun(fsVec, a, b, DeltaInv) H2normArr[i0, i1, i2, i3] = np.real(Fsadj @ Y @ Fs) return np.sqrt(2. * H2normArr)
bArr = b0 * np.arange(nb + 1) fsAmp = np.zeros((9, 3)) fsAmp[0] = np.array([1., 0., 0.]) fsAmp[1] = np.array([0., 1., 0.]) fsAmp[2] = np.array([0., 0., 1.]) fsAmp[3] = np.array([5., 1., 0.]) fsAmp[4] = np.array([-5., 1., 0.]) fsAmp[5] = np.array([1., 1., 0.]) fsAmp[6] = np.array([-1., 1., 0.]) fsAmp[7] = np.array([1., 1., 1.]) fsAmp[8] = np.array([-1., 1., 1.]) eArr = np.zeros( (y0Arr.size, aArr.size, bArr.size, fsAmp.shape[0], 3, tArr.size)) linInst = ops.linearize(N=N, Re=Re, turb=True) for k in range(y0Arr.size): y0 = y0Arr[k] print("y0, y0plus: %.4g, %.4g" % (y0, Re * (1 + y0))) impulseArgs = {'N': N, 'Re': Re, 'turb': True, 'y0': y0, 'eps': eps} for i0 in range(aArr.size): a = aArr[i0] if i0 % 5 == 0: print("a=", a) for i1 in range(bArr.size): b = bArr[i1] if (a == 0.) and (b == 0.): continue caseDict = impres.timeMap(a,b,tArr,linInst=linInst,\ fsAmp=fsAmp,eddy=eddy,\ coeffs=False, impulseArgs=impulseArgs.copy()) energyArr = caseDict.pop('energyArr')
def timeMap(a, b, tArr, fsAmp=None, coeffs=False, linInst=None, modeDict=None, eddy=False, impulseArgs=None, printOut=False): """ Return energy and Fourier coeffs (if requested) for a single Fourier mode for different combinations of forcing at different times We're using the same shape for the impulse in y for each of x,y,z, but changing the relative amplitudes of fx, fy, fz. Inputs: Compulsory args: a, b, tArr: wavenumbers and time keyword args: fsAmp (=None): Relative amplitudes of fx, fy, fz, of shape (m,3) If not supplied, use [[1,0,0]] The total energy in [fx,fy,fz] will be normalized coeffs (=False): If True, return Fourier coeffs as well linInst (=None): Instance of class linearize(). modeDict (=None): If linInst is not supplied, use modeDict to build linInst keys: 'N', 'Re', 'turb' eddy (=False): If True, use eddy viscosity impulseArgs (=None):Build impulse function (wall-normal using this) keys: 'fs' If 'fs' is not supplied, use 'y0', 'eps', and N,Re,turb,eddy from linInst to build fs using _fs() Outputs: outDict: dict with keys 'energyArr', 'coeffArr' (if input kwargs 'coeffs' is True) 'fs', 'y0', 'eps', 'a', 'b', 'tArr', 'N', 'Re', 'turb', 'eddy','fsAmp' If 'fs' was supplied as input to function, set y0 and eps to None """ #===================================================================== # Figure out if tArr is uniformly/linearly spaced or not #=========================== # If tArr is uniformly spaced, then e^{A.n.dt} = e^{A.dt} @ e^{A.(n-1).dt} can be used # If this isn't the case, but tArr is still integral multiples of some dt, then # at least e^{A.n.dt} = (e^{A.dt})^n can be used # If neither is true, just do e^{A.t} = expm(A*t) tArr = np.array([tArr]).flatten() tArr.sort() t0 = np.min(tArr[np.nonzero(tArr)]) assert (tArr >= 0.).all() if _areSame(tArr, t0 * np.arange(1, tArr.size + 1)): # If tArr goes t0*{1,2,3,...} uniformTime = True linearTime = True zeroTime = False tGCD = t0 elif _areSame(tArr, t0 * np.arange(0, tArr.size)): # If tArr goes t0*{0,1,2,...} uniformTime = True linearTime = True zeroTime = True tGCD = t0 else: tGCD = _arrGCD(tArr[np.nonzero(tArr)]) # If I have to raise e^{A.dt} to very large powers, it's not worth it if tGCD // t0 <= 5: uniformTime = False linearTime = True # so, if tGCD is significantly smaller than t0, treat tArr to not be linear and use expm() all the time else: uniformTime = False linearTime = False # These aren't used until the final loop where expFactor is calculated #===================================================================== # Decide if forcings are handled directly or a 3d basis is used #========================= if fsAmp is None: fsAmp = np.array([1, 1, 1]).reshape((1, 3)) else: fsAmp = np.array(fsAmp).reshape((fsAmp.size // 3, 3)) # Because of linearity, I don't need to compute for different fs arrangements # Its sufficient to compute for fs =[fs0;0;0], [0;fs0;0], [0;0;fs0], # and then superpose the velocity fields # Computing energy from superposed velocity fields is trivial if fsAmp.shape[0] > 3: useBasis = True fsAmp0 = np.array([1., 0., 0., 0., 1., 0., 0., 0., 1.]).reshape((3, 3)) else: useBasis = False fsAmp0 = fsAmp #==================================================================== # System matrices #======================= if linInst is None: # Ensure modeDict has entries for N and Re. turb defaults to False assert set(('N', 'Re')) <= set(modeDict) modeDict['turb'] = modeDict.get('turb', False) linInst = ops.linearize(flowClass='channel', **modeDict) N = linInst.N Re = linInst.Re # See if flow is turbulent turb = (linInst.flowState == 'turb') # Construct system matrices for the dynamical system: # d_t psi = A psi + B @ fs delta(t) # [u, v, w]= C psi , where psi = [v, omega_y] # or # u = C_u psi; v = C_v psi; w = C_w psi # So that u(t) = C_u @ (e^{A(t/n)})^n @ B @ fs, and similarly for v, w systemDict = linInst.makeSystem(a=a, b=b, adjoint=False, eddy=eddy) A = systemDict['A'] C = systemDict['C'] B = systemDict['B'] W = linInst.weightDict['W1'] w = linInst.w #================================================================= # Forcing vectors #============== # Function to build Fs for each fsAmp[k] if impulseArgs is None: impulseArgs = {'eddy': eddy, 'turb': turb, 'N': N, 'Re': Re} else: impulseArgs.update({'eddy': eddy, 'turb': turb, 'N': N, 'Re': Re}) if 'fsAmp' in impulseArgs.keys(): impulseArgs.pop('fsAmp') #pdb.set_trace() fsDict = _fs(fsAmp=fsAmp0, **impulseArgs) fs = fsDict['fs'] fs = fs.T # _fs returns fs so that fs[k] refers to a particular forcing, i.e. shape mx 3N # We want fs to a matrix of shape 3Nxm, so that B@fs is shape 2Nxm, and final u(t) is shape 3Nxm Fs = B @ fs if useBasis: # To ensure I don't mess up the weighting factors, # I want forcing to have magnitude unity, i.e. ||fs|| = 1 # Let's denote such forcing with fs~ # fs~ = fs/||fs|| = [Ax*fs0,Ay*fs0,Az*fs0]/||fs||, where ||fs|| = ||fs0|| sqrt{Ax^2+Ay^2+Az^2} = ||fs0|| |A| # Since fx~ = [fs0~;0;0] , fy~ = [0;fs0~;0] and fz~ = [0;0;fs0~], I can write fs~ as # fs~ = Ax/|A| fx~ + Ay/|A| fy~ + Az/|A| fz~ # Ignore the abuse of notation for the amplitude A here.. fsAmpNorm = np.sqrt(np.sum(fsAmp**2, axis=1)).reshape( (fsAmp.shape[0], 1)) scaleFactors = fsAmp / fsAmpNorm # scaleFactors[k] is [Ax/|A|, Ay/|A|, Az/|A|] scaleFactors = scaleFactors.T # Make it of shape 3 x m, to allow # [ a1x; a2x; a3x; a4x;...]_3xm # us_3Nxm = [ux; uy; uz]_3Nx3 [ a1y; a2y; a3y; a4y;...]_3xm # [ a1z; a2z; a3z; a4z;...]_3xm # where a1x = Ax/|A| for the first forcing, a2x for the second forcing, and so on.. #================================================================== # last bits of preparation #================== # Build energyArr by looping over fs.shape[0] and tArr energyArr = np.zeros((tArr.size, 3, fsAmp.shape[0])) expFactor = np.identity(2 * N, dtype=np.complex) fs = fs.reshape((fs.size // (3 * N), 3 * N)).T # So that fs is a matrix of shape 3N x m, allowing matrix multiplication u(t;fs) = C e^{At} B fs_{3N,m} if coeffs: coeffArr = np.zeros((tArr.size, 3 * N, fsAmp.shape[0]), dtype=np.complex) w1 = w.reshape( (1, N)) # w1 is ClenCurt weight matrix, reshaped to a row vector def uvwEnergies(arr): # Use 2d arr, of shape 3Nx m returnArr = np.zeros((3, arr.shape[1])) returnArr[0:1] = w1 @ (np.abs(arr[:N])**2) returnArr[1:2] = w1 @ (np.abs(arr[N:2 * N])**2) returnArr[2:3] = w1 @ (np.abs(arr[2 * N:])**2) return returnArr #============================================================== # And here we go... #====================== if linearTime: expFactor0 = expm(A * t0) for i0 in range(tArr.size): if tArr[i0] == 0.: expFactor = np.identity(2 * N, dtype=np.complex) else: if uniformTime: expFactor = expFactor @ expFactor0 elif linearTime: expFactor = matrix_power(expFactor0, int(round(tArr[i0] / tGCD))) else: expFactor = expm(A * tArr[i0]) tmpArr = C @ expFactor @ Fs # This is a matrix of shape 3N x m # Each column of tmpArr corresponds to field due to a forcing, fsAmp0[m] if not useBasis: # If I'm not computing for too many kinds of forcing, I don't have to split into Fx, Fy, and Fz and then superpose # For such cases, just calculate individually energyArr[i0] = uvwEnergies(tmpArr) if coeffs: coeffArr[i0] = tmpArr.copy() else: assert tmpArr.shape[ 1] == 3, " Need 3d basis for forcing along x,y,z" # When using too many forcings, first compute velocity fields individually for Fx, Fy, Fz # Now, compute velocity fields for each forcing given by fsAmp # Need to be careful about how I weight each of these fields velVec = tmpArr @ scaleFactors energyArr[i0] = uvwEnergies(velVec) if coeffs: coeffArr[i0] = velVec.copy() #=================================================================== # Prepare dict to return... #=========================== if impulseArgs is None: impulseArgs = {} impulseArgs.update(fsDict) impulseArgs.update({'fsAmp':fsAmp, 'N':N, 'Re':linInst.Re, 'a':a, 'b':b, 'tArr':tArr,\ 'turb':(linInst.flowState=='turb'), 'eddy':eddy }) outDict = {'energyArr': energyArr} outDict.update(impulseArgs) if coeffs: outDict.update({'coeffArr': coeffArr}) return outDict