Dear FiPy users --
This is a simple example of how and why fipy may fail to solve a
steady advection diffusion problem, and how solving the transient
problem can reduce the error. I also found something that was a
surprise -- the "initial" condition of a steady problem can affect
the solution for some solvers.
At the end are two interesting questions for those who want to
understand what FiPy is actually doing.... I admit to being a bit
lost
The equation I am solving is
\Del\dot (D\del psi + u*psi) =0
Where the diffusion D is 1, and u is a vector (1,0) -- so there is
only a flow of speed -1 in the x direction. I solve this equation
on a 10 by 10 grid. The boundary conditions are no normal gradient
on the y=0 and y=10 boundary:
psi_y =0 at y=0 and y=10
For the x boundary, I impose a value of x=1 on the inflow boundary at
x=10
(this is a little tricky -- the way the equation is written, u is
the negative of velocity).
psi=1 at x=10
and a no-normal-gradient condition at x=0.
psi_x=0 at x=0
since all of the domain and boundary is symmetrical with respect to
y, we can assume psi_y=0 is zero everywhere. This reduces the equation to
psi_xx + psi_x =0
The general solution to this equation is
psi=C1+C2*exp(-x)
Where C1 and C2 are constants. For these boundary conditions, C1=1
and C2=0, so psi=1 everywhere.
Now run the code SquareGrid_HomemadeDelaunay and look at figure(3)
-- this is the plot of psi versus x, and you can see that it does
not match the true solution of psi=1 everywhere! Instead, it
appears to be decaying exponential. The blue line is actually just
(1+exp(-x)). What is going on? It appears that small errors in the
boundary condition at x=0 are allowing C2 to not be exactly 0, and
this error is this exponential mode. The error is the artificial
exiting of a correct mode of the interior equation, albeit one that
should not be excited by these BC's.
Interestingly, the size of this error depends on the value the psi
is initially set to. If the line
psi=fipy.CellVariable(name='psi',mesh=mesh,value=0.0)
is changed so psi is initially 1, the error goes away entirely; if
it is set to some other value, you get different errors. I do not
know if this is a bug, or just numerical error in a well programmed
solver.
Now if you run SquareGrid_HomemadeDelaunay_transient which implements
psi_t = \Del\dot (D\del psi + u*psi)
you can see that the error in the numerical solution is advected
out of the x=0 boundary, and the solution approaches the true
solution of psi=1 rapidly.
The interesting question is if the formulation of the boundary
condition at x=0 could be altered to less excite the spurious mode?
Also, why does the "initial" condition have any effect on the
steady equation?
Cheers,
Jamie
from pylab import *
from numpy import *
import fipy
from collections import defaultdict
import copy as Copy
def ScipyDelaunay2mesh(points,simplices,debug=False):
'''ScipyDelaunay2mesh(points,simplices):
Creates a FiPy 2Dmesh object from the output of a
scipy.spatial.Delaunay triangulation,
Also returns a list of border faces for the application of
boundary conditions. The list of border conditions is computed
within this routine; it will handle the case where
triangles/simplices have been removed from the output of the
Delaunay() triangulation.
Parameters
----------
points : array
an (N,2) array of the location of N points that made the
triangulation. If Delaunay is called with
tri=scipy.spatial.Delaunay(), points is tri.points
simplices : array
an (M,3) array of the location of M triangles returned by
Delaunay. If Delaunay is called with
tri=scipy.spatial.Delaunay(), simplices is tri.simplices
debug=False : Boolean
if this is set to true, the mesh will be drawn in blue,
boundaries will be drawn in red, boundary point numbers will be
written at vertices, and numbers for triangles will be drawn in
the triangle. This output is useful for finding where triangles
have been removed incorrectly.
Returns
-------
mesh: a FiPy mesh2D object
'''
#Create the arrays that fipy.mesh.mesh2D needs. vertexCoords is
#of shape (2,N), where N is the number of points, and contains the
#coordinates of the vertices. It is equivalent to points in
#content, but reshaped.
print 'Entering ScipyDelaunay2Mesh; Making vertexCoords'
vertexCoords=points.T
#faceVertexID is of shape (2,M) where M is the number of faces/edges
#(faces is the fipy terminology, edges is the scipy.spatial.Delaunay
#terminology). faceVertexID contains the points (as indices into
#vertexCoords) which make up each face. This data can be extracted
#from simplices from Delaunay, which contains the faces of each
#triangle. HOWEVER, simplices contains many repeated faces, since each
#triangle will border on others. Only one copy of each face should be
#inserted into faceVertexID.
print ' Making faceVertexIDs'
faceIn={} #this is a dictionary of all faces that have already been
#inserted into the faceVertexID, with the key a tuple of
#indices, sorted
faceVertexIDs_list=[] #structure to build
for ntri in xrange(simplices.shape[0]):
for nface in xrange(3):
if nface==0:
face=simplices[ntri,[0,1]]
elif nface==1:
face=simplices[ntri,[1,2]]
else:
face=simplices[ntri,[2,0]]
faceKey=tuple(sort(face))
if not (faceKey in faceIn):
faceIn[faceKey]=True
faceVertexIDs_list.append(face)
faceVertexIDs=array(faceVertexIDs_list).T
#now create cellFaceIDs of shape (3,P) where P is the number of cells
#in the domain. It contains the faces (as indices into faceVertexIDs)
#that make up each cell.
#first create dictionary with a key made up of a sorted tuple of vertex
#indices which maps from a face to its location in faceVertexIDs
print ' Making cellFaceIDs'
faceMap={}
for nface in xrange(faceVertexIDs.shape[1]):
faceKey=tuple(sort(faceVertexIDs[:,nface]))
faceMap[faceKey]=nface
#now loop over simplices, find each edge/face, and map from points to
#faces
cellFaceIDs=simplices.T*0
for ntri in xrange(simplices.shape[0]):
for nface in xrange(3):
if nface==0:
face=simplices[ntri,[0,1]]
elif nface==1:
face=simplices[ntri,[1,2]]
else:
face=simplices[ntri,[2,0]]
faceKey=tuple(sort(face))
cellFaceIDs[nface,ntri]=faceMap[faceKey]
#ok, now instantiate a mesh2D object with this data
print ' Making mesh'
mesh=fipy.meshes.mesh2D.Mesh2D(vertexCoords,faceVertexIDs,cellFaceIDs)
return mesh
def Matrix2Delaunay(xmat,ymat,mask,addOrthog=False):
'''Matrix2Delaunay(xmat,ymat,mask,addOrthog=False):
Creates the elements of a Delaunay triangulation, simplices and
points, from two matrices, containing the x and y positions of the
points, and a mask array which. Only include triangles connecting
points where mask is true.
Parameters
----------
xmat: x location of points, 2D matrix
ymat: y location of points, 2D matrix
mask: boolean array, 2D matrix
mask.shape must be the same as xmat.shape and ymat.shape
mask MUST be so that triangles are able to be formed!
addOrthog=False: if true add points to result which improve
orthogonality of resulting triangles.
Returns
-------
points, the location of vertices as returned by Delaunay
simplices, the points which make up triangles, as returned by Delaunay
pntMat: a matrix of xmat.shape which contains the indices of the
vertices that correspond to points in the matrix. Note -- does
not return points that where added to improve orthogonality,
since they are not in xmat,ymat.
'''
assert xmat.shape==mask.shape, 'xmat, ymat and mask must be same shape'
assert ymat.shape==mask.shape, 'xmat, ymat and mask must be same shape'
assert mask.dtype==type(True), 'mask must be a boolean array'
#make lists of x and y points that are to be included in mesh, and
#pntMat, a matrix that maps from a location in the patrix to the
#index of the points.
xvec=xmat[mask]
yvec=ymat[mask]
pntMat=xmat.astype(type(1))
pntMat[:,:]=-1 #-1 means point not included in mesh
pntMat[mask]=arange(sum(mask))
#Create some of the arrays that Delaunay would create. points is
#of shape (N,2), where N is the number of points, and contains the
#coordinates of the vertices.
print 'Entering Matrix2Delaunay; Making points'
points=zeros([len(xvec),2])
points[:,0]=xvec
points[:,1]=yvec
#now create simplices, an (M,3) array of integers for the M
#triangles that will be created.
print ' making simplices'
if not(addOrthog):
#If the nodes are arranged
#
# 4--3
# | |
# 1--2
#
#create triangles 1->2->4 and 2->3->4
simplices_list=[]
for nx in range(xmat.shape[0]-1):
for ny in range(xmat.shape[1]-1):
#if possible, create 1->2->4
if mask[nx,ny] & mask[nx+1,ny] & mask[nx,ny+1]:
simplices_list.append([pntMat[nx,ny],pntMat[nx+1,ny],pntMat[nx,ny+1]])
#if possible, create 2->3->4
if mask[nx+1,ny] & mask[nx+1,ny+1] & mask[nx,ny+1]:
simplices_list.append([pntMat[nx+1,ny],pntMat[nx+1,ny+1],pntMat[nx,ny+1]])
else:
#If the nodes are arranged
#
# 4--3
# | |
# 1--2
#
#if (1->2) or (2->3) or (3->4) or (4->1) exist, create a new point np
#
# 4----3
# | |
# | np |
# | |
# 1----2
#
#create triangles 1->2->np, 2->3->np, 3->4->np and 4->1->np
#create list from points, same shape as points, to append new points to
points_list=[list(np) for np in list(points)]
simplices_list=[]
for nx in range(xmat.shape[0]-1):
for ny in range(xmat.shape[1]-1):
#only make points if the following sets are not masked
#(1,2,3) or (2,3,4) or (3,4,1) to avoid jaggies on the
#boundary
if ((mask[nx,ny] & mask[nx+1,ny] & mask[nx+1,ny+1]) |
(mask[nx+1,ny] & mask[nx+1,ny+1] & mask[nx,ny+1]) |
(mask[nx+1,ny+1] & mask[nx,ny+1] & mask[nx,ny])):
xnp=0.25*(xmat[nx,ny]+xmat[nx+1,ny]+xmat[nx+1,ny+1]+xmat[nx,ny+1])
ynp=0.25*(ymat[nx,ny]+ymat[nx+1,ny]+ymat[nx+1,ny+1]+ymat[nx,ny+1])
points_list.append([xnp,ynp])
n_newpoint=len(points_list)-1
if (mask[nx,ny] & mask[nx+1,ny]): # add triangle 1->2->np
simplices_list.append([pntMat[nx,ny],pntMat[nx+1,ny],n_newpoint])
if (mask[nx+1,ny] & mask[nx+1,ny+1]): # add triangle 2->3->n
simplices_list.append([pntMat[nx+1,ny],pntMat[nx+1,ny+1],n_newpoint])
if (mask[nx+1,ny+1] & mask[nx,ny+1]): # add triangle 3->4->np
simplices_list.append([pntMat[nx+1,ny+1],pntMat[nx,ny+1],n_newpoint])
if (mask[nx,ny+1] & mask[nx,ny]): # add triangle 4->1->np
simplices_list.append([pntMat[nx,ny+1],pntMat[nx,ny],n_newpoint])
#convert points_list back to an array
points=array(points_list)
#convert list to array
simplices=array(simplices_list)
#what happens if we reverse order of triangles
if False:
for ns in range(simplices.shape[0]):
j0=simplices[ns,0]
j2=simplices[ns,2]
simplices[ns,0]=j2
simplices[ns,2]=j0
return points,simplices,pntMat
if __name__ == "__main__":
#test code by making and drawing simple grid
#make points in square domain of size 1
points=[]
xvec=linspace(0.0,10.0,50)
yvec=linspace(0.0,10.0,50)
xmat,ymat=meshgrid(xvec,yvec)
#make Mask
mask=xmat<Inf
#now make triangulation, and convert to mesh
points,simplices,matPnts=Matrix2Delaunay(xmat,ymat,mask,addOrthog=True)
mesh=ScipyDelaunay2mesh(points,simplices)
#setup fipy solution for Del^2 Psi = 1 with BC of 0 everywhere
#first, make grid variable
psi=fipy.CellVariable(name='psi',mesh=mesh,value=0.0)
figure(1)
clf()
#now plot mesh
triplot(mesh.vertexCoords[0,:],mesh.vertexCoords[1,:],triangles=simplices)
title('simple square mesh')
axis('square')
draw()
show()
# These modules takes the output of scipy.spatial.Delaunay's
# triangulation of data points, and returns both the FiPy mesh for the
# points, and a list of the boundary edges so that boundary conditions
# can be set for the points.
#
# This code handles the case where triangles/simplices have been
# removed from output of the Delaunay triangulation after the
# triangulation. This triangle removal can be useful to create
# interior holes in the domain, or to handle concave boundaries of the
# domain.
#
# The documentation for the routines can be found in there docstrings,
# and examples of there use can be found in __main__
from numpy import *
import fipy
from collections import defaultdict
import copy as Copy
from JMP_Make_Grids import *
if __name__ == "__main__":
from pylab import *
from scipy.spatial import Delaunay
print ' '
print 'Test case -- square domain with Del^2 Psi=0 and various BC'
#set up figure
figure(1)
clf()
#make points in square domain of size 1
points=[]
xvec=linspace(0.0,10.0,50)
yvec=linspace(0.0,10.0,50)
xmat,ymat=meshgrid(xvec,yvec)
#make Mask
mask=xmat<Inf
#if true, make mask irregular
makeBight=False
if makeBight:
if False: #notch
bight=logical_and(xmat<2.0,ymat>5)
mask[bight]=False
else:
#diagnonal cut,slope<1 to make jaggies
bight=(ymat>(1.5*xmat+5))
#bight=(ymat>(0.75*xmat+5))
mask[bight]=False
#now make triangulation, and convert to mesh
points,simplices,matPnts=Matrix2Delaunay(xmat,ymat,mask,addOrthog=True)
mesh=ScipyDelaunay2mesh(points,simplices)
#setup fipy solution for Del^2 Psi = 1 with BC of 0 everywhere
#first, make grid variable
psi=fipy.CellVariable(name='psi',mesh=mesh,value=0.0)
#apply boundary of Del\Psi \dot \nhat=1 on x=0 boundary
# Del\Psi \dot \nhat=0 on y=0,1 boundary
#and Psi=1 on x=1
xFace,yFace=mesh.faceCenters
faceNorm=mesh.faceNormals
figure(1)
clf()
plot(xFace[array(mesh.exteriorFaces)],yFace[array(mesh.exteriorFaces)],'co',markersize=10,mec='none')
#keep track of which boundaries have been set
boundariesNotSet=array(mesh.exteriorFaces)
#y=0 boundary
yeq0bound=yFace==0.0
psi.faceGrad.constrain(0.0*faceNorm,where=yeq0bound)
plot(xFace[yeq0bound],yFace[yeq0bound],'rs')
boundariesNotSet[yeq0bound]=False
#y=10.0 boundary
yeq10bound=yFace==10.0
psi.faceGrad.constrain(0.0*faceNorm,where=yeq10bound)
plot(xFace[yeq10bound],yFace[yeq10bound],'rs')
boundariesNotSet[yeq10bound]=False
#x=0 boundary
xeq0bound=xFace==0.0
#psi.faceGrad.constrain(-1.0*faceNorm,where=xeq0bound)
psi.faceGrad.constrain(0.0*faceNorm,where=xeq0bound)
#psi.constrain(1.0,where=xeq0bound)
plot(xFace[xeq0bound],yFace[xeq0bound],'rs')
boundariesNotSet[xeq0bound]=False
#x=10 boundary
xeq10bound=xFace==10.0
psi.constrain(1.0,where=xeq10bound)
plot(xFace[xeq10bound],yFace[xeq10bound],'rs')
boundariesNotSet[xeq10bound]=False
#set remaining boundaries to zero normal gradient; this should just be bight
psi.faceGrad.constrain(0.0*faceNorm,where=boundariesNotSet)
plot(xFace[boundariesNotSet],yFace[boundariesNotSet],'ys')
#make equatiion and solve
DiffCoeff=1.0e+0
ConvecCoeff=(1.0,0.0) #flow towards x=0
#ConvecCoeff=(-1.0,0.0) #flow towards x=1
#first solve steady, then use that as intial to transient
solver=fipy.DefaultAsymmetricSolver
#start with solution to steady solution
eq=(fipy.DiffusionTerm(var=psi,coeff=DiffCoeff)+fipy.PowerLawConvectionTerm(var=psi,coeff=ConvecCoeff))
eq.solve(var=psi,solver=solver())
#now continue with transient solution. If the solution to the
#steady solution were perfect, nothing would change...
eq=(fipy.TransientTerm(var=psi)==
fipy.DiffusionTerm(var=psi,coeff=DiffCoeff)+fipy.PowerLawConvectionTerm(var=psi,coeff=ConvecCoeff))
#now loop in time and solve
dx=xvec[2]-xvec[1]
print 'for u=1 and diff=1 and dx=%f, u cfl=%f and diff cfl=%f',dx/1.0,dx**2/1.0
dt=min(dx/1.0,dx**2/1.0)*25.5
print ' and dt = ',dt
figure(2)
show()
nt=0
# while nt*dt<200.0:
while nt<10:
#and plot
if remainder(nt,1)==0:
clf()
subplot(2,1,1)
tripcolor(psi.mesh.vertexCoords[0,:],psi.mesh.vertexCoords[1,:],
simplices,psi.numericValue)
colorbar()
title('solution, min=%f, max=%f at t=%3.2f'%(amin(psi.numericValue),amax(psi.numericValue),nt*dt))
axis('square')
subplot(2,1,2)
plot(array(psi.mesh.x),psi.numericValue,'r.')
draw()
pause(0.001)
print ' doing time',dt*nt,'step nt=',nt,'STD=',std(psi.numericValue)
nt+=1
eq.solve(var=psi,solver=solver(),dt=dt)
#now plot mesh
figure(1)
triplot(mesh.vertexCoords[0,:],mesh.vertexCoords[1,:],triangles=simplices)
title('simple square mesh')
axis('square')
if True:
#plot gradients at cell location
dPdx=array(psi.grad[0,:])
dPdy=array(psi.grad[1,:])
x=array(psi.mesh.x)
y=array(psi.mesh.y)
quiver(x,y,dPdx,dPdy,scale=20.0)
axis('square')
draw()
show()
#now plot solution
figure(2)
clf()
tripcolor(psi.mesh.vertexCoords[0,:],psi.mesh.vertexCoords[1,:],
simplices,psi.numericValue)
colorbar()
title('solution, min=%f, max=%f'%(amin(psi.numericValue),amax(psi.numericValue)))
if False:
#plot gradients at cell location
dPdx=array(psi.grad[0,:])
dPdy=array(psi.grad[1,:])
x=array(psi.mesh.x)
y=array(psi.mesh.y)
quiver(x,y,dPdx,dPdy,scale=20.0)
axis('square')
figure(3)
clf()
plot(array(psi.mesh.x),psi.numericValue,'r.')
x=array(psi.mesh.x)
plot(x,exp(-x)+1.0,'b.',alpha=0.1)
xlabel('x')
ylabel('psi')
draw()
show()
# These modules takes the output of scipy.spatial.Delaunay's
# triangulation of data points, and returns both the FiPy mesh for the
# points, and a list of the boundary edges so that boundary conditions
# can be set for the points.
#
# This code handles the case where triangles/simplices have been
# removed from output of the Delaunay triangulation after the
# triangulation. This triangle removal can be useful to create
# interior holes in the domain, or to handle concave boundaries of the
# domain.
#
# The documentation for the routines can be found in there docstrings,
# and examples of there use can be found in __main__
from numpy import *
import fipy
from collections import defaultdict
import copy as Copy
from JMP_Make_Grids import *
if __name__ == "__main__":
from pylab import *
from scipy.spatial import Delaunay
print ' '
print 'Test case -- square domain with Del^2 Psi=0 and various BC'
#set up figure
figure(1)
clf()
#make points in square domain of size 1
points=[]
xvec=linspace(0.0,10.0,50)
yvec=linspace(0.0,10.0,50)
xmat,ymat=meshgrid(xvec,yvec)
#make Mask
mask=xmat<Inf
#if true, make mask irregular
makeBight=False
if makeBight:
if True: #notch
bight=logical_and(xmat<2.0,ymat>5)
mask[bight]=False
else:
#diagnonal cut,slope<1 to make jaggies
bight=(ymat>(1.5*xmat+5))
#bight=(ymat>(0.75*xmat+5))
mask[bight]=False
#now make triangulation, and convert to mesh
points,simplices,matPnts=Matrix2Delaunay(xmat,ymat,mask,addOrthog=True)
mesh=ScipyDelaunay2mesh(points,simplices)
#setup fipy solution for Del^2 Psi = 1 with BC of 0 everywhere
#first, make grid variable
psi=fipy.CellVariable(name='psi',mesh=mesh,value=0.0)
#apply boundary of Del\Psi \dot \nhat=1 on x=0 boundary
# Del\Psi \dot \nhat=0 on y=0,1 boundary
#and Psi=1 on x=1
xFace,yFace=mesh.faceCenters
faceNorm=mesh.faceNormals
figure(1)
clf()
plot(xFace[array(mesh.exteriorFaces)],yFace[array(mesh.exteriorFaces)],'co',markersize=10,mec='none')
#keep track of which boundaries have been set
boundariesNotSet=array(mesh.exteriorFaces)
#y=0 boundary
yeq0bound=yFace==0.0
psi.faceGrad.constrain(0.0*faceNorm,where=yeq0bound)
plot(xFace[yeq0bound],yFace[yeq0bound],'rs')
boundariesNotSet[yeq0bound]=False
#y=10.0 boundary
yeq10bound=yFace==10.0
psi.faceGrad.constrain(0.0*faceNorm,where=yeq10bound)
plot(xFace[yeq10bound],yFace[yeq10bound],'rs')
boundariesNotSet[yeq10bound]=False
#x=0 boundary
xeq0bound=xFace==0.0
#psi.faceGrad.constrain(-1.0*faceNorm,where=xeq0bound)
psi.faceGrad.constrain(0.0*faceNorm,where=xeq0bound)
#psi.constrain(1.0,where=xeq0bound)
plot(xFace[xeq0bound],yFace[xeq0bound],'rs')
boundariesNotSet[xeq0bound]=False
#x=10 boundary
xeq10bound=xFace==10.0
psi.constrain(1.0,where=xeq10bound)
plot(xFace[xeq10bound],yFace[xeq10bound],'rs')
boundariesNotSet[xeq10bound]=False
#set remaining boundaries to zero normal gradient; this should just be bight
psi.faceGrad.constrain(0.0*faceNorm,where=boundariesNotSet)
plot(xFace[boundariesNotSet],yFace[boundariesNotSet],'ys')
#make equatiion and solve
DiffCoeff=1.0e+0
ConvecCoeff=(1.0,0.0) #flow towards x=0
#ConvecCoeff=(-1.0,0.0) #flow towards x=1
eq=(fipy.DiffusionTerm(var=psi,coeff=DiffCoeff)+fipy.ExponentialConvectionTerm(var=psi,coeff=ConvecCoeff))
#eq=(fipy.DiffusionTerm(var=psi,coeff=DiffCoeff)+fipy.PowerLawConvectionTerm(var=psi,coeff=ConvecCoeff))
#eq=(fipy.DiffusionTerm(var=psi,coeff=DiffCoeff)+fipy.CentralDifferenceConvectionTerm(var=psi,coeff=ConvecCoeff))
#eq=(fipy.DiffusionTerm(var=psi,coeff=DiffCoeff))
eq.solve(var=psi,solver=fipy.LinearLUSolver())
#now plot mesh
triplot(mesh.vertexCoords[0,:],mesh.vertexCoords[1,:],triangles=simplices)
title('simple square mesh')
axis('square')
if True:
#plot gradients at cell location
dPdx=array(psi.grad[0,:])
dPdy=array(psi.grad[1,:])
x=array(psi.mesh.x)
y=array(psi.mesh.y)
quiver(x,y,dPdx,dPdy,scale=20.0)
axis('square')
draw()
show()
#now plot solution
figure(2)
clf()
tripcolor(psi.mesh.vertexCoords[0,:],psi.mesh.vertexCoords[1,:],
simplices,psi.numericValue)
colorbar()
title('solution, min=%f, max=%f'%(amin(psi.numericValue),amax(psi.numericValue)))
if False:
#plot gradients at cell location
dPdx=array(psi.grad[0,:])
dPdy=array(psi.grad[1,:])
x=array(psi.mesh.x)
y=array(psi.mesh.y)
quiver(x,y,dPdx,dPdy,scale=20.0)
axis('square')
figure(3)
clf()
plot(array(psi.mesh.x),psi.numericValue,'r.',label='Numerical Solution')
x=array(psi.mesh.x)
plot(x,exp(-x)+1.0,'b.',alpha=1.,label='1+exp(-x)')
legend()
xlabel('x')
ylabel('psi')
draw()
show()
_______________________________________________
fipy mailing list
[email protected]
http://www.ctcms.nist.gov/fipy
[ NIST internal ONLY: https://email.nist.gov/mailman/listinfo/fipy ]