Source code for festim.h_transport_problem

from fenics import *
import festim


[docs] class HTransportProblem: """Hydrogen Transport Problem. Used internally in festim.Simulation Args: mobile (festim.Mobile): the mobile concentration traps (festim.Traps): the traps T (festim.Temperature): the temperature settings (festim.Settings): the problem settings initial_conditions (list of festim.initial_conditions): the initial conditions of the h transport problem Attributes: expressions (list): contains time-dependent fenics.Expressions J (ufl.Form): the jacobian of the variational problem V (fenics.FunctionSpace): the vector-function space for concentrations u (fenics.Function): the vector holding the concentrations (c_m, ct1, ct2, ...) v (fenics.TestFunction): the test function u_n (fenics.Function): the "previous" function newton_solver (fenics.NewtonSolver): Newton solver for solving the nonlinear problem bcs (list): list of fenics.DirichletBC for H transport """ def __init__(self, mobile, traps, T, settings, initial_conditions) -> None: self.mobile = mobile self.traps = traps self.T = T self.settings = settings self.initial_conditions = initial_conditions self.J = None self.u = None self.v = None self.u_n = None self.newton_solver = None self.boundary_conditions = [] self.bcs = None self.V = None self.V_CG1 = None self.expressions = [] @property def newton_solver(self): return self._newton_solver @newton_solver.setter def newton_solver(self, value): if value is None: self._newton_solver = value elif isinstance(value, NewtonSolver): if self._newton_solver: festim.festim_print( "Settings for the Newton solver will be overwritten" ) self._newton_solver = value else: raise TypeError("accepted type for newton_solver is fenics.NewtonSolver") @property def _all_surf_kinetics(self): return [ bc for bc in self.boundary_conditions if isinstance(bc, festim.SurfaceKinetics) ]
[docs] def initialise(self, mesh, materials, dt=None): """Assigns BCs, create suitable function space, initialise concentration fields, define variational problem Args: mesh (festim.Mesh): the mesh materials (festim.Materials): the materials dt (festim.Stepsize, optional): the stepsize, only needed if self.settings.transient is True. Defaults to None. """ if self.settings.chemical_pot: self.mobile.S = materials.S self.mobile.materials = materials self.mobile.volume_markers = mesh.volume_markers self.mobile.T = self.T self.attribute_flux_boundary_conditions() self.traps.assign_traps_ids() # Define functions self.define_function_space(mesh) self.initialise_concentrations() self.traps.make_traps_materials(materials) self.traps.initialise_extrinsic_traps(self.V_CG1) # Define variational problem H transport # if chemical pot create form to convert theta to concentration if self.settings.chemical_pot: self.mobile.create_form_post_processing(self.V_DG1, materials, mesh.dx) self.define_variational_problem(materials, mesh, dt) self.define_newton_solver() # Boundary conditions festim.festim_print("Defining boundary conditions") self.create_dirichlet_bcs(materials, mesh) if self.settings.transient: self.traps.define_variational_problem_extrinsic_traps(mesh.dx, dt, self.T) self.traps.define_newton_solver_extrinsic_traps()
[docs] def define_function_space(self, mesh): """Creates a suitable function space for H transport problem Args: mesh (festim.Mesh): the mesh """ order_trap = 1 element_solute, order_solute = "CG", 1 # function space for H concentrations nb_traps = len(self.traps) # the number of surfaces where SurfaceKinetics is used nb_adsorbed = sum([len(bc.surfaces) for bc in self._all_surf_kinetics]) if nb_traps == 0 and nb_adsorbed == 0: V = FunctionSpace(mesh.mesh, element_solute, order_solute) else: solute = FiniteElement(element_solute, mesh.mesh.ufl_cell(), order_solute) traps = FiniteElement( self.settings.traps_element_type, mesh.mesh.ufl_cell(), order_trap ) adsorbed = FiniteElement("R", mesh.mesh.ufl_cell(), 0) element = [solute] + [traps] * nb_traps + [adsorbed] * nb_adsorbed V = FunctionSpace(mesh.mesh, MixedElement(element)) self.V = V self.V_CG1 = FunctionSpace(mesh.mesh, "CG", 1) self.V_DG1 = FunctionSpace(mesh.mesh, "DG", 1)
[docs] def initialise_concentrations(self): """Creates the main fenics.Function (holding all the concentrations), eventually split it and assign it to Trap and Mobile. Then initialise self.u_n based on self.initial_conditions Args: materials (festim.Materials): the materials """ # TODO rename u and u_n to c and c_n self.u = Function(self.V, name="c") # Function for concentrations self.v = TestFunction(self.V) # TestFunction for concentrations self.u_n = Function(self.V, name="c_n") if self.V.num_sub_spaces() == 0: self.mobile.solution = self.u self.mobile.previous_solution = self.u_n self.mobile.test_function = self.v else: conc_list = [self.mobile] if self.traps: conc_list += [*self.traps] if len(self._all_surf_kinetics) > 0: conc_list += self._all_surf_kinetics index = 0 for concentration in conc_list: if isinstance(concentration, festim.SurfaceKinetics): # iterate through each surface of each SurfaceKinetics for i in range(len(concentration.surfaces)): concentration.solutions[i] = self.u.sub(index) concentration.previous_solutions[i] = self.u_n.sub(index) concentration.test_functions[i] = list(split(self.v))[index] index += 1 else: concentration.solution = self.u.sub(index) concentration.previous_solution = self.u_n.sub(index) concentration.test_function = list(split(self.v))[index] index += 1 festim.festim_print("Defining initial values") field_to_component = { "solute": 0, "0": 0, 0: 0, } for i, trap in enumerate(self.traps, 1): field_to_component[trap.id] = i field_to_component[str(trap.id)] = i # TODO refactore this, attach the initial conditions to the objects directly for ini in self.initial_conditions: value = ini.value component = field_to_component[ini.field] if self.V.num_sub_spaces() == 0: functionspace = self.V else: functionspace = self.V.sub(component).collapse() if component == 0: self.mobile.initialise( functionspace, value, label=ini.label, time_step=ini.time_step ) else: trap = self.traps.get_trap(component) trap.initialise( functionspace, value, label=ini.label, time_step=ini.time_step ) # assign initial condition for SurfaceKinetics BC # iterate through each surface of each SurfaceKinetics index = len(self.traps) + 1 for bc in self._all_surf_kinetics: for i in range(len(bc.previous_solutions)): functionspace = self.V.sub(index).collapse() comp = interpolate(Constant(bc.initial_condition), functionspace) assign(bc.previous_solutions[i], comp) index += 1 # initial guess needs to be non zero if chemical pot if self.settings.chemical_pot: if self.V.num_sub_spaces() == 0: functionspace = self.V else: functionspace = self.V.sub(0).collapse() initial_guess = project( self.mobile.previous_solution + Constant(DOLFIN_EPS), functionspace ) self.mobile.solution.assign(initial_guess) # this is needed to correctly create the formulation # TODO: write a test for this? if self.V.num_sub_spaces() != 0: index = 0 for concentration in conc_list: if isinstance(concentration, festim.SurfaceKinetics): for i in range(len(concentration.surfaces)): concentration.solutions[i] = list(split(self.u))[index] concentration.previous_solutions[i] = list(split(self.u_n))[ index ] index += 1 else: concentration.solution = list(split(self.u))[index] concentration.previous_solution = list(split(self.u_n))[index] index += 1
[docs] def define_variational_problem(self, materials, mesh, dt=None): """Creates the variational problem for hydrogen transport (form, Dirichlet boundary conditions) Args: materials (festim.Materials): the materials mesh (festim.Mesh): the mesh dt (festim.Stepsize, optional): the stepsize, only needed if self.settings.transient is True. Defaults to None. """ if MPI.comm_world.rank == 0: print("Defining variational problem") expressions = [] F = 0 # diffusion + transient terms self.mobile.create_form( materials, mesh, self.T, dt, traps=self.traps, soret=self.settings.soret ) F += self.mobile.F expressions += self.mobile.sub_expressions # Add traps self.traps.create_forms(self.mobile, materials, self.T, mesh.dx, dt) F += self.traps.F expressions += self.traps.sub_expressions self.F = F self.expressions = expressions
[docs] def define_newton_solver(self): """Creates the Newton solver and sets its parameters""" self.newton_solver = NewtonSolver(MPI.comm_world) self.newton_solver.parameters["error_on_nonconvergence"] = False self.newton_solver.parameters["absolute_tolerance"] = ( self.settings.absolute_tolerance ) self.newton_solver.parameters["relative_tolerance"] = ( self.settings.relative_tolerance ) self.newton_solver.parameters["maximum_iterations"] = ( self.settings.maximum_iterations ) self.newton_solver.parameters["linear_solver"] = self.settings.linear_solver self.newton_solver.parameters["preconditioner"] = self.settings.preconditioner
[docs] def attribute_flux_boundary_conditions(self): """Iterates through self.boundary_conditions, checks if it's a FluxBC and its field is 0, and assign fluxes to self.mobile """ for bc in self.boundary_conditions: if isinstance(bc, festim.FluxBC) and bc.field == 0: self.mobile.boundary_conditions.append(bc)
[docs] def create_dirichlet_bcs(self, materials, mesh): """Creates fenics.DirichletBC objects for the hydrogen transport problem and add them to self.bcs """ self.bcs = [] for bc in self.boundary_conditions: if bc.field != "T" and isinstance(bc, festim.DirichletBC): bc.create_dirichletbc( self.V, self.T.T, mesh.surface_markers, chemical_pot=self.settings.chemical_pot, materials=materials, volume_markers=mesh.volume_markers, ) self.bcs += bc.dirichlet_bc self.expressions += bc.sub_expressions self.expressions.append(bc.expression)
def compute_jacobian(self): du = TrialFunction(self.u.function_space()) self.J = derivative(self.F, self.u, du)
[docs] def update(self, t, dt): """Updates the H transport problem. Args: t (float): the current time (s) dt (festim.Stepsize): the stepsize """ festim.update_expressions(self.expressions, t) converged = False u_ = Function(self.u.function_space()) u_.assign(self.u) while converged is False: self.u.assign(u_) nb_it, converged = self.solve_once() if dt.adaptive_stepsize is not None or dt.milestones is not None: dt.adapt(t, nb_it, converged) # Update previous solutions self.update_previous_solutions() # Solve extrinsic traps formulation self.traps.solve_extrinsic_traps()
[docs] def solve_once(self): """Solves non linear problem Returns: int, bool: number of iterations for reaching convergence, True if converged else False """ if self.J is None: # Define the Jacobian du = TrialFunction(self.u.function_space()) J = derivative(self.F, self.u, du) else: J = self.J problem = festim.Problem(J, self.F, self.bcs) begin("Solving nonlinear variational problem.") # Add message to fenics logs nb_it, converged = self.newton_solver.solve(problem, self.u.vector()) end() return nb_it, converged
def update_previous_solutions(self): self.u_n.assign(self.u) self.traps.update_extrinsic_traps_density() def update_post_processing_solutions(self, exports): if self.u.function_space().num_sub_spaces() == 0: res = [self.u] else: res = list(self.u.split()) for i, trap in enumerate(self.traps, 1): trap.post_processing_solution = res[i] index = len(self.traps) + 1 for bc in self._all_surf_kinetics: for i in range(len(bc.post_processing_solutions)): bc.post_processing_solutions[i] = res[index] index += 1 if self.settings.chemical_pot: self.mobile.post_processing_solution_to_concentration() else: self.mobile.post_processing_solution = res[0]