Skip to content
Snippets Groups Projects
Forked from Jan Habscheid / fxdgm
124 commits behind the upstream repository.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
EqN.py 11.87 KiB
'''
Jan Habscheid
Jan.Habscheid@rwth-aachen.de

This script implements the fenics solver for the generic system of equations for N species
'''

import numpy as np
from mpi4py import MPI
from dolfinx import mesh, fem, log
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from ufl import TestFunctions, split, dot, grad, dx, inner, ln, Mesh
from basix.ufl import element, mixed_element
import matplotlib.pyplot as plt

# Define mesh
def create_refined_mesh(refinement_style:str, number_cells:int) -> Mesh:
    '''
    Creates a one-dimensional mesh with a refined region at the left boundary

    Parameters
    ----------
    refinement_style : str
        How the mesh should be refined. Options are 'log', 'hard_log', 'hard_hard_log'
    number_cells : int
        Number of cells in the mesh

    Returns
    -------
    Mesh
        One-dimensional mesh, ready for use in FEniCSx
    '''
    if refinement_style == 'log':
        coordinates_np = (np.logspace(0, 1, number_cells+1) - 1) / 9
    elif refinement_style == 'hard_log':
        coordinates_np1 = (np.logspace(0,1,int(number_cells*0.9)+1,endpoint=False)-1)/9 * 0.1
        coordinates_np2 = 0.1 + (np.logspace(0,1,int(number_cells*0.1)+1)-1)/9 * 0.9
        coordinates_np = np.concatenate((coordinates_np1, coordinates_np2), axis=0)
    elif refinement_style == 'hard_hard_log':
        coordinates_np1 = (np.logspace(0,1,int(number_cells*0.9)+1,endpoint=False)-1)/9 * 0.004
        coordinates_np2 = 0.004 + (np.logspace(0,1,int(number_cells*0.1)+1)-1)/9 * 0.996
        coordinates_np = np.concatenate((coordinates_np1, coordinates_np2), axis=0)
    num_vertices = len(coordinates_np)
    num_cells = num_vertices - 1
    cells_np = np.column_stack((np.arange(num_cells), np.arange(1, num_cells+1)))
    gdim = 1
    shape = 'interval' # 'interval', 'triangle', 'quadrilateral', 'tetrahedron', 'hexahedron'
    degree = 1
    domain = Mesh(element("Lagrange", shape, 1, shape=(1,)))
    coordinates_np_ = []
    [coordinates_np_.append([coord]) for coord in coordinates_np]
    msh = mesh.create_mesh(MPI.COMM_WORLD, cells_np, coordinates_np_, domain)
    return msh

def solve_System_Neq(phi_left:float, phi_right:float, p_right:float, z_alpha:list, y_R:list, K:float|str, Lambda2:float, a2:float, number_cells:int, solvation:float = 0, PoissonBoltzmann:bool=False, relax_param:float=None, x0:float=0, x1:float=1, refinement_style:str='uniform', return_type:str='Vector', rtol:float=1e-8, max_iter:float=500):
    '''
    Solve the dimensionless system of equations presented in: Numerical Treatment of a Thermodynamically Consistent Electrolyte Model, B.Sc. Thesis Habscheid 2024

    System of equations:
        λ²Δ φ =−L²n^F
        a²∇p=−n^F∇ φ
        div(J_α)=0  α∈ {1,...,N−1}

    with φ the electric potential, p the pressure, n^F the total free charge density, J_α the diffusion fluxes of species α, λ² a dimensionless parameter, L²=1, a² a dimensionless parameter, N the number of species, and α the species index.

    ! If the Newton solver diverges, you may try to reduce the relaxation parameter.

    Parameters
    ----------
    phi_left : float
        Value of φ at the left boundary
    phi_right : float
        Value of φ at the right boundary
    p_right : float
        Value of p at the right boundary
    z_alpha : list
        Charge numbers for species α = 1,...,N-1
    y_R : list
        Atomic fractions at right boundary for species α = 1,...,N-1
    K : float | str
        Dimensioness bulk modulus of the electrolyte. If 'incompressible', the system is solved for an incompressible electrolyte
    Lambda2 : float
        Dimensionless parameter
    a2 : float
        Dimensionless parameter
    number_cells : int
        Number of cells in the mesh
    solvation : float, optional
        solvation number, not implemented yet, by default 0
    PoissonBoltzmann : bool, optional
        Solve classical Nernst-Planck model with the use of the Poisson-Boltzmann formulation if True, else solve the presented model by Dreyer, Guhlke, Müller, Not implemented yet, by default False
    relax_param : float, optional
        Relaxation parameter for the Newton solver
        xₙ₊₁ = γ xₙ f(xₙ)/f'(xₙ) with γ the relaxation parameter
        , by default None -> Determined automatically
    x0 : float, optional
        Left boundary of the domain, by default 0
    x1 : float, optional
        Right boundary of the domain, by default 1
    refinement_style : str, optional
        Specify for refinement towards zero
        Options are 'uniform', 'log', 'hard_log', 'hard_hard_log' by default 'uniform'
    return_type : str, optional
        'Vector' or 'Scalar' (not implemented yet, should be implemented in a later version), 'Scalar' returns dolfinx.fem type and 'Vector' numpy arrays of the solution, by default 'Vector'
    rtol : float, optional
        Relative tolerance for Newton solver, by default 1e-8
    max_iter : float, optional
        Maximum number of Newton iterations, by default 500

    Returns
    -------
    y_A, y_C, phi, p, msh
        Returns atomic fractions for species A and C, electric potential, pressure, and the mesh
        If return_type is 'Vector', the solution is returned as numpy arrays
        Only return_type 'Vector' is implemented yet
    '''
    if solvation != 0: 
        raise NotImplementedError('Solvation number not implemented yet')
    if PoissonBoltzmann:
        raise NotImplementedError('Poisson-Boltzmann not implemented yet')
    # Define boundaries of the domain
    x0 = 0
    x1 = 1

    # Define boundaries
    def Left(x):
        return np.isclose(x[0], x0)

    def Right(x):
        return np.isclose(x[0], x1)
    
    # Create mesh
    if refinement_style == 'uniform':
        msh = mesh.create_unit_interval(MPI.COMM_WORLD, number_cells, dtype=np.float64)
    else:
        msh = create_refined_mesh(refinement_style, number_cells)

    # Define Finite Elements
    CG1_elem = element('Lagrange', msh.basix_cell(), 1)

    # Define Mixed Function Space
    Elem_list = [CG1_elem, CG1_elem]
    [Elem_list.append(CG1_elem) for _ in range(len(z_alpha))]
    W_elem = mixed_element(Elem_list)
    W = fem.functionspace(msh, W_elem)

    # Define Trial- and Testfunctions
    u = fem.Function(W)
    my_TrialFunctions = split(u)
    my_TestFunctions = TestFunctions(W)

    phi, p = my_TrialFunctions[0], my_TrialFunctions[1]
    v_1, v_2 = my_TestFunctions[0], my_TestFunctions[1]
    y_alpha = my_TrialFunctions[2:]
    v_alpha = my_TestFunctions[2:]

    # Collapse function space for bcs
    W_ = []
    [W_.append(W.sub(i).collapse()[0]) for i in range(len(z_alpha)+2)]

    # Define boundary conditions values
    def phi_left_(x):
        return np.full_like(x[0], phi_left)
    def phi_right_(x):
        return np.full_like(x[0], phi_right)
    def p_right_(x):
        return np.full_like(x[0], p_right)
    
    # Interpolate bcs functions
    phi_left_bcs = fem.Function(W_[0])
    phi_left_bcs.interpolate(phi_left_)
    phi_right_bcs = fem.Function(W_[0])
    phi_right_bcs.interpolate(phi_right_)
    p_right_bcs = fem.Function(W_[1])
    p_right_bcs.interpolate(p_right_)

    # Identify dofs for boundary conditions
    facet_left_dofs = fem.locate_dofs_geometrical((W.sub(0), W.sub(0).collapse()[0]), Left)
    facet_right_dofs = fem.locate_dofs_geometrical((W.sub(0), W.sub(0).collapse()[0]), Right)
    bc_left_phi = fem.dirichletbc(phi_left_bcs, facet_left_dofs, W.sub(0))
    bc_right_phi = fem.dirichletbc(phi_right_bcs, facet_right_dofs, W.sub(0))

    facet_right_dofs = fem.locate_dofs_geometrical((W.sub(1), W.sub(1).collapse()[0]), Right)
    bc_right_p = fem.dirichletbc(p_right_bcs, facet_right_dofs, W.sub(1))

    # Combine boundary conditions for electric potential and pressure into list
    bcs = [bc_left_phi, bc_right_phi, bc_right_p]

    # Repeat the same for the boundary conditoins for the atomic fractions
    for i in range(len(z_alpha)):
        y_right_bcs = fem.Function(W_[i+2])
        def y_right_(x):
            return np.full_like(x[0], y_R[i])
        y_right_bcs.interpolate(y_right_)
        facet_right_dofs = fem.locate_dofs_geometrical((W.sub(i+2), W.sub(i+2).collapse()[0]), Right)
        bc_right_y = fem.dirichletbc(y_right_bcs, facet_right_dofs, W.sub(i+2))
        bcs.append(bc_right_y)
        
    # Define variational problem
    if K == 'incompressible':
        # total free charge density
        def nF(y_alpha):
            nF = 0
            for i in range(len(z_alpha)):
                nF += z_alpha[i] * y_alpha[i]
            return nF
        
        # Diffusion fluxes for species A and C
        def J_alpha(y_alpha, alpha, phi, p):
            mu_alpha = ln(y_alpha[alpha])
            mu_S = ln(1 - sum(y_alpha))
            return grad(mu_alpha - mu_S + z_alpha[alpha] * phi)
        
        # Variational Form
        A = (
            inner(grad(phi), grad(v_1)) * dx
            - 1 / Lambda2 * nF(y_alpha) * v_1 * dx
        ) + (
            inner(grad(p), grad(v_2)) * dx
            + 1 / a2 * nF(y_alpha) * dot(grad(phi), grad(v_2)) * dx
        )
        for alpha in range(len(z_alpha)):
            A += (
                inner(J_alpha(y_alpha, alpha, phi, p), grad(v_alpha[alpha])) * dx
            )
        if PoissonBoltzmann:
            raise ValueError('Poisson-Boltzmann not implemented for incompressible systems')
    else: 
        raise ValueError('Only incompressible systems are implemented')
    F = A

    # Initialize initial guess for u
    with u.vector.localForm() as u_loc:
        u_loc.set(0)

    # Initialize initial guess for u
    for alpha in range(len(z_alpha)):
        y_alpha_init = fem.Function(W_[alpha+2])
        y_alpha_init.interpolate(lambda x: np.full_like(x[0], y_R[alpha]))
        u.sub(alpha+2).interpolate(y_alpha_init)

    # Define Nonlinear Problem
    problem = NonlinearProblem(F, u, bcs=bcs)

    # Define Newton Solver
    solver = NewtonSolver(MPI.COMM_WORLD, problem)
    solver.convergence_criterion = "incremental"
    solver.rtol = rtol
    if relax_param != None:
        solver.relaxation_parameter = relax_param
    else:
        if phi_right == phi_left:
            solver.relaxation_parameter = 1.0
        else:
            solver.relaxation_parameter = 1/(np.abs(phi_right-phi_left)**(5/4))
    solver.max_it = max_iter
    solver.report = True

    
    log.set_log_level(log.LogLevel.INFO)
    n, converged = solver.solve(u)
    assert (converged)
    print(f"Number of (interations: {n:d}")

    # Return the solution    
    if return_type=='Vector':
        x = np.array(msh.geometry.x[:,0])
        phi = np.array(u.sub(0).collapse().x.array)
        p = np.array(u.sub(1).collapse().x.array)
        y = []
        [y.append(u.sub(i+2).collapse().x.array) for i in range(len(z_alpha))]
        y = np.array(y)    
        return y, phi, p, x
    
    
if __name__ == '__main__':
    # Define the parameters
    phi_left = 8.0
    phi_right = 0.0
    p_right = 0.0
    y_R = [3/6, 1/6, 1/6]
    z_alpha = [-1.0, 1.0, 2.0]
    K = 'incompressible'
    Lambda2 = 8.553e-6
    a2 = 7.5412e-4
    number_cells = 1024
    relax_param = .05
    rtol = 1e-4
    max_iter = 2_500
    refinement_style = 'hard_log'
    return_type = 'Vector'
    
    # Solve the system
    y, phi, p, x = solve_System_Neq(phi_left, phi_right, p_right, z_alpha, y_R, K, Lambda2, a2, number_cells, relax_param=relax_param, refinement_style=refinement_style, return_type=return_type, max_iter=max_iter, rtol=rtol)
    
    # Plot the solution
    plt.figure()
    plt.plot(x, phi)
    plt.xlim(0,0.05)
    plt.grid()
    plt.xlabel('x [-]')
    plt.ylabel('$\\varphi$ [-]')
    plt.show()
    
    plt.figure()
    plt.xlim(0,0.05)
    plt.plot(x, p)
    plt.grid()
    plt.xlabel('x [-]')
    plt.ylabel('$p$ [-]')
    plt.show()

    plt.figure()
    for i in range(len(z_alpha)):
        plt.plot(x, y[i], label=f'$y_{i}$')
    plt.xlim(0,0.05)
    plt.legend()
    plt.grid()
    plt.xlabel('x [-]')
    plt.ylabel('$y_i$ [-]')
    plt.show()