2

Let me start by saying that I have found similar problems to mine on the NARKIVE FiPy mailing list archive but since the equations won't load, they are not very useful. For example Convection-diffusion problem on a 1D cylindrical grid, or on another mailing list archive Re: FiPy Heat Transfer Solution. In the second linked mail Daniel says:

There are two ways to solve on a cylindrical domain in FiPy. You can either use the standard diffusion equation in Cartesian coordinates (2nd equation below) and with a mesh that is actually cylindrical in shape or you can use the diffusion equation formulated on a cylindrical coordinate system (1st equation below) and use a standard 2D / 1D grid mesh.

And the equations are not there. In this case it is actually fine because I understand the first solution and I want to use that.

I want to solve the following equation on a 1D cylindrical grid (sorry I don't have 10 reputation yet so I cannot post the nice rendered equations):

with boundary conditions:

where rho_core is the left side of the mesh, and rho_edge is the right side of the mesh. rho is the normalized radius, J is the Jacobian:

R is the real radius in meters, so the dimension of the Jacobian is distance. The initial conditions doesn't really matter, but in my code example I will use a numerical Dirac-delta at R=0.8.

I have a working example without(!) the Jacobian, but it's quite long, and it doesn't use FiPy's Viewers so I'll link a gist: https://gist.github.com/leferi99/142b90bb686cdf5116ef5aee425a4736

The main part in question is the following:

    import fipy as fp  ## finite volume PDE solver
    from fipy.tools import numerix  ## requirement for FiPy, in practice same as numpy
    import copy  ## we need the deepcopy() function because some FiPy objects are mutable
    import numpy as np
    import math

    ## numeric implementation of Dirac delta function
    def delta_func(x, epsilon, coeff):
        return ((x < epsilon) & (x > -epsilon)) * \
            (coeff * (1 + numerix.cos(numerix.pi * x / epsilon)) / (2 * epsilon))

    rho_from = 0.7  ## normalized inner radius
    rho_to = 1.  ## normalized outer radius
    nr = 1000  ## number of mesh cells
    dr = (rho_to - rho_from) / nr  ## normalized distance between the centers of the mesh cells
    duration = 0.001  ## length of examined time evolution in seconds
    nt = 1000  ## number of timesteps
    dt = duration / nt  ## length of one timestep
    
    ## 3D array for storing the density with the correspondant normalized radius values
    ## the density values corresponding to the n-th timestep will be in the n-th line 
    solution = np.zeros((nt,nr,2))
    ## loading the normalized radial coordinates into the array
    for j in range(nr):
        solution[:,j,0] = (j * dr) + (dr / 2) + rho_from
    
    mesh = fp.CylindricalGrid1D(dx=dr, nx=nr)  ## 1D mesh based on the normalized radial coordinates 
    mesh = mesh + (0.7,)  ## translation of the mesh to rho=0.7
    n = fp.CellVariable(mesh=mesh)  ## fipy.CellVariable for the density solution in each timestep
    
    diracLoc = 0.8  ## location of the middle of the Dirac delta
    diracCoeff = 1.  ## Dirac delta coefficient ("height")
    diracPercentage = 2  ## width of Dirac delta (full width from 0 to 0) in percentage of full examined radius
    diracWidth = int((nr / 100) * diracPercentage)
    
    ## diffusion coefficient
    diffCoeff = fp.CellVariable(mesh=mesh, value=100.)
    ## convection coefficient - must be a vector
    convCoeff = fp.CellVariable(mesh=mesh, value=(1000.,))
    
    ## applying initial condition - uniform density distribution
    n.setValue(1)

    ## boundary conditions
    gradLeft = (0.,)  ## density gradient (at the "left side of the radius") - must be a vector
    valueRight = 0.  ## density value (at the "right end of the radius")
    n.faceGrad.constrain(gradLeft, where=mesh.facesLeft)  ## applying Neumann boundary condition
    n.constrain(valueRight, mesh.facesRight)  ## applying Dirichlet boundary condition
    convCoeff.setValue(0, where=mesh.x<(R_from + dr))  ## convection coefficient 0 at the inner edge
    
    ## the PDE
    eq = (fp.TransientTerm() == fp.DiffusionTerm(coeff=diffCoeff)
          - fp.ConvectionTerm(coeff=convCoeff))
    
    ## Solving the PDE and storing the data
    for i in range(nt):
        eq.solve(var=n, dt=dt)
        solution[i,0:nr,1]=copy.deepcopy(n.value)

My code can solve the following equation with the same boundary conditions as indicated above:

To keep it simple I use spatially independent coefficients with the only exeption on the inner edge, where the convection coefficient is 0, and the diffusion coefficient is almost 0. In the linked code I am using a uniform distribution initial condition.

My first question is why do I get the exact same results when using fipy.Grid1D and fipy.CylindricalGrid1D? I should get different results, right? How should I rewrite my code for it to be able to differentiate between the simple 1D Grid and the 1D Cylindrical Grid?

My actual problem is not with this exact code, I just wanted to simplify my problem, but as indicated in the comments this code doesn't produce the same results with the different Grids. So I will just post a GitHub link to a Jupyter Notebook, which may stop working in the future. The Jupyter Notebook If you want to run it, the first code cell should be run first and after that only the very last cell is relevant. Ignore the reference images. The line plots show the diffusion and convection coefficients. When I ran the last cell with Grid1D or CylindricalGrid1D I got the same results (I compared the plots very precisely)

Sorry but I just cannot rename all my variables, so I hope that based on my comment, and the changed code above (I changed the comments in the code too) you can understand what I'm trying to do.

My other question is regarding the Jacobian. How can I implement it? I've looked at the only example in the documentation which uses a Jacobian, but that Jacobian is a matrix and also it uses the scipy.optimize.fsolve() function.

  • The results are similar between a `Grid1D` and a `CylindricalGrid1D`, particularly in the early steps, but they are not the same. They are quite different as the problem evolves. – jeguyer Mar 31 '21 at 18:36
  • I don't understand what you're doing with the Jacobian. If R is the radius, then what is rho? – jeguyer Mar 31 '21 at 18:36
  • I'm sorry, it's kinda messy. So rho in the equation is R in the code and in reality it is a normalized radius. I would like to add a Jacobian which is the partial derivative of the real radius with respect to the normalized radius. J=d(real_r)/d(rho) Regarding the other comment I will edit the question later and I will possibly change parts of the code as well because when I had spatially dependent coefficients, and uniform initial condition, I could not see the difference between the different types of Grids. (I just didn't want to use that code here because I thought it's overly complicated. – Ferenc Lengyel Mar 31 '21 at 21:38
  • Also I will try to resolve the naming problems as well. – Ferenc Lengyel Mar 31 '21 at 21:42
  • FiPy doesn't like things outside the divergence, but you should be able to multiply the equation by J and put it in the coefficient of the `TransientTerm`. – jeguyer Mar 31 '21 at 23:04
  • I ran the code again, and I probably messed up yesterday, because now I can see the difference betwenn the solutions using different Grids. – Ferenc Lengyel Apr 01 '21 at 11:04
  • As for the Jacobian you mean like this: `eq = (fp.TransientTerm(J) == J * fp.DiffusionTerm(coeff=diffCoeff) - J * fp.ConvectionTerm(coeff=convCoeff))` ? How should I set up J? I'm guessing simply `J = 1.8` (in my case) isn't going to do anything. Should I set up a new CellVariable which contains the real radius on the same mesh as my density function and then take the derivative of that with respect to `mesh.x`? – Ferenc Lengyel Apr 01 '21 at 11:26
  • You already have a factor of `1/J` outside the divergence on the right-hand side. The point of multiplying by `J` is to get rid of this, because FiPy doesn't want things outside the divergence, so `eq = (fp.TransientTerm(J) == fp.DiffusionTerm(coeff=diffCoeff) - fp.ConvectionTerm(coef=convCoeff)`. – jeguyer Apr 01 '21 at 14:52
  • 1
    Since I don't understand the purpose of solving on the unphysical mesh in the first place, it's hard to advise. You can certainly set up a `CellVariable` holding the "real" radius and then `J = real_radius.grad.dot([[1]])` (`.grad` returns a vector, even in 1D, but the coefficient must be scalar, so `dot` to get the x component). – jeguyer Apr 01 '21 at 14:54
  • Thank you. I managed to implement the Jacobian and I can clearly see the difference between the Grids and between using the Jacobian and not using it now. – Ferenc Lengyel Apr 01 '21 at 21:08

1 Answers1

1

[cobbling an answer from the discussion in the comments]

The results are similar between a Grid1D and a CylindricalGrid1D, particularly in the early steps, but they are not the same. They are quite different as the problem evolves. comparison at 10 steps comparison at 100 steps comparison at 1000 steps

FiPy doesn't like things outside the divergence, but you should be able to multiply the equation by J and put it in the coefficient of the TransientTerm, e.g.,

or

eq = fp.TransientTerm(J) == fp.DiffusionTerm(coeff=J * diffCoeff) - fp.ConvectionTerm(coef=J * convCoeff)

For the Jacobian, you could create a CellVariable for the real radius in terms of the normalized radius, and then take its gradient:

real_radius = fp.CellVariable(mesh=mesh, value=...)
J = real_radius.grad.dot([[1]])

.grad returns a vector, even in 1D, but the coefficient must be scalar, so take the dot product to get the x component.

jeguyer
  • 2,379
  • 1
  • 11
  • 15