To clarify and answer my question, and to give an analysis code for those interested in figuring these out, here is some text. (I would love comments from those who know more!)
First a restatement of the question. To test the solvers, create a field PsiTruth, and solve for PsiInvert with \Del^2 PsiInvert = \Del^2 PsiTruth With various boundary conditions. The attached code does this. (To understand the notation in the code, see my prior email). My conclusions are that for maximum accuracy: 1. Compute \Del^2 PsiTruth in the interior on center points with the grad() operator. It is significantly more consistent with the solver numerics than the leastSquaresGrad() operator. This results in smaller error. 2. On the boundaries, use .faceGradAverage() to calculate the gradients of PsiTruth; .faceGrad() is incorrect on the boundaries. 3. At least part of one boundary must be of the form PsiInvert.constrain(), for otherwise the solution is arbitrary to within an additive constant, and that constant can be so large that it compromises the accuracy of the solution due to the finite number of significant digits in floating point numbers. 4. When gradient boundary conditions are specified, accuracy increases linearly with 1/resolution, and is much less than when the boundary values are fixed. I hope this helps someone, and I would like to hear from others about the choice of gradient operators and how to achieve greater accuracy in calculating and specifying gradients on the boundary. In particular, I would love to have a gradient calculation that is exactly consistent with how the gradient is specified in the boundary condition constraint. It would also be nice to have a Laplacian operator that is consistent with the solver in the model -- or do I get that by using the .grad() operator twice? Thanks all, Jamie On Fri, Apr 21, 2017 at 11:28 AM, Pringle, James <james.prin...@unh.edu> wrote: > Dear all -- > > I am using fipy to solve a series of fluid dynamics problems. As part of > this, I need to compute a stream function Psi from the vorticity of flow > omega=V_x-U_y. U and V are calculated from the gradient of the solution to > another set of elliptic equations, and are available on the same mesh as > Psi will be calculated on. > > The equation relating the stream function Psi to the vorticity omega is > > \Del^2 Psi = omega > > > And the boundary conditions on Psi are that the gradient normal to the > boundary is equal to the flow parallel to the boundary. All very standard. > > I calculate velocities U and V from the gradient of another variable (a > pressure like term). My question is this: which of the routines that > calculate the gradient of a fipy variable on the faces is most consistent > with how the normal derivative is specified on the boundary? Which of > .faceGrad() or faceGradAverage() is most consistent with how the gradient > is implemented in Psi.faceGrad.constrain()? or some other gradient > calculation? I ask because .faceGrad() does not seem to calculate the > gradient on the boundary faces... > > Thanks, > Jamie Pringle > > Director Ocean Process Analysis Laboratory in the Insitute for Earth, > Ocean and Space > > Associate Professor of Earth Sciences > > University of New Hampshire > > http://oxbow.sr.unh.edu >
from pylab import * from numpy import * import time import matplotlib.tri as tri import fipy #this code creates and arbitrary streamfunction psi, finds velocity #from it, and then inverts the velocity back to the stream function #using the vorticity equation. The result should be the same as the #original streamfunction, to within an additive constant. #================================================================== #define domain dx=1.0 Lx=125.0 nx=int(round(Lx/dx)); ny=int(round(Lx/dx)) mesh=fipy.Grid2D(dx=dx,dy=dx,nx=nx,ny=ny) assert max(mesh.faceCenters.value[0,:])==Lx,'dx must be multiple of Lx, or the BCs will fail' #define streamfunction as cos(2*pi/wavelength*mesh.x)*cos(2*pi/wavelength*mesh.y) and plot wavelength=80.0 psiTrue=fipy.CellVariable(name='PsiTrue',mesh=mesh, value=cos(2*pi/wavelength*mesh.x)*cos(2*pi/wavelength*mesh.y)) print 'wavelength/dx=',wavelength/dx figure(1) clf() tricontourf(mesh.x,mesh.y,psiTrue.value,30) colorbar() title('True Psi') #================================================================== #calculate U and V from streamfunction with U=-Psi_y and V=Psi_x, #and plot with quiver. if False: #calculate gradient with leastSquaresGrad print 'Calculating gradient in interior with leastSquaresGrad' jnk=psiTrue.leastSquaresGrad() Vcenter=jnk[0,:] Ucenter=-jnk[1,:] #================================================================== #Now calculate the vorticity omega=V_x-U_y Ufipy=fipy.CellVariable(name='U',mesh=mesh,value=Ucenter); Ugrad=Ufipy.leastSquaresGrad() Vfipy=fipy.CellVariable(name='V',mesh=mesh,value=Vcenter); Vgrad=Vfipy.leastSquaresGrad() omega=Vgrad[0,:]-Ugrad[1,:] #V_x-U_y elif True: #calculate gradient with Grad print 'Calculating gradient in interior with Grad' jnk=psiTrue.grad() Vcenter=jnk[0,:] Ucenter=-jnk[1,:] #================================================================== #Now calculate the vorticity omega=V_x-U_y Ufipy=fipy.CellVariable(name='U',mesh=mesh,value=Ucenter); Ugrad=Ufipy.grad() Vfipy=fipy.CellVariable(name='V',mesh=mesh,value=Vcenter); Vgrad=Vfipy.grad() omega=Vgrad[0,:]-Ugrad[1,:] #V_x-U_y else: #analytical solution print 'Calculating gradient in interior analytically' omega=-2*((2*pi/wavelength)**2.0)*cos(2*pi/wavelength*mesh.x)*cos(2*pi/wavelength*mesh.y) if False: #plot velocity nxcull=15; quiver(mesh.x[::nxcull],mesh.y[::nxcull], Ucenter[::nxcull],Vcenter[::nxcull],scale=2.0e0) #================================================================== #Now invert omega for Psi using \Del^2 Psi=omega. #The boundary conditions are from the velocity on the faces # #Psi_x=V on the y=0,Lx boundaries and Psi_y=-U on the x=0,Lx boundary. # #because these boundaries are specified on the faces, we need to #recalculate U and V on the faces if True: print 'Calculating gradient of psi on faces for BC with faceGradAverage' jnk=psiTrue.faceGradAverage(); Vface=jnk[0,:] Uface=-jnk[1,:] elif True: #NOTE WELL -- this is a very bad solution, since it gives a normal derivative of 0 #regardless of psiTrue print 'Calculating gradient of psi on faces for BC with faceGrad -- which is wrong' jnk=psiTrue.faceGrad(); Vface=jnk[0,:] Uface=-jnk[1,:] else: #instead use analytical values for the boundary condition, #again with U=-Psi_y, V=Psi_x print 'Calculating gradient in psi on faces for BC with analytical solutions' xFace,yFace=mesh.faceCenters Vface=-2*pi/wavelength*sin(2*pi*xFace/wavelength)*cos(2*pi*yFace/wavelength) Uface= 2*pi/wavelength*cos(2*pi*xFace/wavelength)*sin(2*pi*yFace/wavelength) #nxcull=15; quiver(xFace[::nxcull],yFace[::nxcull], # Uface[::nxcull],Vface[::nxcull],scale=2.0e0,color='r') #set up differential equation and solve #first, find locations of boundaries xFace,yFace=mesh.faceCenters yeq0bound=yFace==0.0 yeq1bound=yFace==Lx xeq0bound=xFace==0.0 xeq1bound=xFace==Lx #at some location, we need to constrain psi, and not just the normal #gradient of psi, at the boundary. Otherwise it will differ by an #additive constant. Boundary points where the value is constrained are #defined by the vector pntsTofix pntsToFix=copy(xeq1bound) #just fix on the x=Lx boundary #pntsToFix=copy(yeq0bound) #just fix on the y=0 boundary #pntsToFix=xeq1bound | xeq0bound | yeq1bound | yeq0bound #fix all boundaries #remove fixed points from points which are fixed by gradient xeq0bound[pntsToFix]=False; xeq1bound[pntsToFix]=False yeq0bound[pntsToFix]=False; yeq1bound[pntsToFix]=False fixValueHere=isnan(yFace) # all false fixValueHere[pntsToFix]=True #now define variable and set BCs psiInvert=fipy.CellVariable(name='psiInvert',mesh=mesh,value=0.0) faceNorm=mesh.faceNormals psiInvert.faceGrad.constrain(-Uface*faceNorm,where=yeq0bound) psiInvert.faceGrad.constrain(-Uface*faceNorm,where=yeq1bound) psiInvert.faceGrad.constrain(Vface*faceNorm,where=xeq0bound) psiInvert.faceGrad.constrain(Vface*faceNorm,where=xeq1bound) #set value at some location on boundary, so constant offset is constrained psiInvert.constrain(psiTrue.faceValue,fixValueHere) #solve \Del^2 Psi=omega tic=time.time() sourceTermPsi=fipy.CellVariable(name='psiForcing',mesh=mesh,value=-omega) DiffCoefPsi=fipy.CellVariable(mesh=mesh,rank=0,value=1.0) eqPsi=(fipy.DiffusionTerm(var=psiInvert,coeff=DiffCoefPsi)+sourceTermPsi) eqPsi.solve(var=psiInvert,solver=fipy.LinearLUSolver()) #eqPsi.solve(var=psiInvert) print 'Done finding psi in',time.time()-tic print 'max and min psi are %e,%e'%(amin(psiInvert.numericValue),amax(psiInvert.numericValue)) #print 'Psi at fixed point in true %f and inverted %f Psi',valueAtOnePoint,psiInvert.faceValue.value[fixValueHere][0] #when comparing, it is important to remove means, because the solution #is undefined to within a constant meanPsiTrue=mean(psiTrue.value) meanPsiInvert=mean(psiInvert.value) print 'STD of error is %e'%(std(((psiInvert.value-meanPsiInvert)-(psiTrue.value-meanPsiTrue))),) print 'STD of PsiTrue is %f, PsiInvert %f'%(std(psiTrue.value),std(psiInvert.value)) figure(2) clf() tricontourf(mesh.x,mesh.y,(psiInvert.value-meanPsiInvert)-(psiTrue.value-meanPsiTrue),30) colorbar() title('(Inverted Psi)-(True Psi), both with means removed') figure(3) clf() tricontourf(mesh.x,mesh.y,psiInvert.value,30) colorbar() title('Inverted Psi') draw() show()
_______________________________________________ fipy mailing list fipy@nist.gov http://www.ctcms.nist.gov/fipy [ NIST internal ONLY: https://email.nist.gov/mailman/listinfo/fipy ]