Source code for phasefieldx.Element.Phase_Field_Fracture.solver.solver

'''
Solver: Phase-field fracture/fatigue
====================================

This module provides a solver for phase-field fracture and fatigue simulations
using the Finite Element Method (FEM).

'''

# Libraries ############################################################
########################################################################
import os
import time
import dolfinx
import ufl
from mpi4py import MPI
import numpy as np

from phasefieldx.files import prepare_simulation, append_results_to_file
from phasefieldx.solvers.newton import NewtonSolver
from phasefieldx.Logger.library_versions import set_logger, log_system_info, log_library_versions, log_end_analysis, log_model_information


from phasefieldx.Materials.elastic_isotropic import epsilon
from phasefieldx.Reactions import calculate_reaction_forces


from phasefieldx.Element.Phase_Field_Fracture.g_degradation_functions import dg, g
from phasefieldx.Element.Phase_Field_Fracture.split_energy_stress_tangent_functions import (psi_a, psi_b, sigma_a,
                                                                                            sigma_b)
from phasefieldx.Element.Phase_Field_Fracture.fatigue_degradation_functions import fatigue_degradation

from phasefieldx.Element.Phase_Field.geometric_crack import geometric_crack_function_derivative, geometric_crack_coefficient
from phasefieldx.Element.Phase_Field.energy import calculate_crack_surface_energy
from phasefieldx.Element.Phase_Field_Fracture.energy import compute_total_energies

from phasefieldx.Math.projection import project
from phasefieldx.errors_functions import eval_error_L2, eval_error_L2_normalized
from phasefieldx.files import prepare_simulation
from phasefieldx.solvers.newton import NewtonSolver


[docs] def solve(Data, msh, final_time, V_u, V_Φ, bc_list_u=[], bc_list_phi=[], update_boundary_conditions=None, f_list_u=None, T_list_u=None, update_loading=None, ds_bound=None, dt=1.0, path=None, bcs_list_u_names=None, min_stagger_iter=2, max_stagger_iter=10000, stagger_error_tol=1e-8): """ Solve the phase-field fracture and fatigue problem. Parameters ---------- Data : object An object containing the simulation parameters and settings. msh : dolfinx.mesh The computational mesh for the simulation. final_time : float The final simulation time. V_u : dolfinx.FunctionSpace The function space for the displacement field. V_Φ : dolfinx.FunctionSpace The function space for the phase-field. bc_list_u : list of dolfinx.DirichletBC, optional List of Dirichlet boundary conditions for the displacement field. bc_list_phi : list of dolfinx.DirichletBC, optional List of Dirichlet boundary conditions for the phase-field. update_boundary_conditions : callable, optional A function to update boundary conditions dynamically. f_list_u : list, optional List of body forces. T_list_u : list of tuple, optional List of tuples containing traction forces and corresponding measures. update_loading : callable, optional A function to update loading conditions dynamically. ds_bound : numpy.ndarray, optional Array of boundary measures for calculating reaction forces. dt : float, optional Time step size. path : str, optional Directory path for saving simulation results. Defaults to current working directory. min_stagger_iter: int Minimum stagger iterations for numerical simulations. max_stagger_iter: int Maximum stagger iterations for numerical simulations. stagger_error_tol: float Tolerance for stagger error in simulations. Returns ------- None """ case = 'AT2' # Get MPI communicator info comm = msh.comm rank = comm.Get_rank() if path is None: path = os.getcwd() # Common - Only rank 0 handles file operations ###################################################################### result_folder_name = Data.results_folder_name if rank == 0: prepare_simulation(path, result_folder_name) logger = set_logger(result_folder_name) log_system_info(logger) # log system information log_library_versions(logger) # log Library versions Data.save_log_info(logger) # log Simulation input data Data.save_parameters_to_csv(os.path.join(result_folder_name, "parameters.input")) log_model_information(msh, logger) logger.info("========== Stagger settings ===========") logger.info(f" minimum stagger iterations: {min_stagger_iter}") logger.info(f" maximum stagger iterations: {max_stagger_iter}") logger.info(f" stagger error tolerance: {stagger_error_tol}") else: logger = None # Synchronize all processes comm.Barrier() # Dolfinx cpp logger - all processes dolfinx.log.set_log_level(dolfinx.log.LogLevel.INFO) if rank == 0: dolfinx.cpp.log.set_output_file( os.path.join(result_folder_name, "dolfinx.log")) if bcs_list_u_names is None: bcs_list_u_names = [f"bc_u_{i}" for i in range(len(bc_list_u))] # Formulation ########################################################## ######################################################################## # Displacement ------------------------ u_new = dolfinx.fem.Function(V_u, name="u") u_old = dolfinx.fem.Function(V_u, name="u_old") δu = ufl.TestFunction(V_u) # Phase-field ------------------------- Φ_new = dolfinx.fem.Function(V_Φ, name="phi") Φ_old = dolfinx.fem.Function(V_Φ, name="phi_old") δΦ = ufl.TestFunction(V_Φ) # History------------------------------ H = dolfinx.fem.Function(V_Φ, name="H") V_c = dolfinx.fem.Function(V_Φ, name="V_c") V_n = dolfinx.fem.Function(V_Φ, name="V_n") # Fatigue------------------------------ if Data.fatigue: Fatigue = dolfinx.fem.Function(V_Φ, name="fatigue") alpha_c = dolfinx.fem.Function(V_Φ, name="alpha_c") alpha_n = dolfinx.fem.Function(V_Φ, name="alpha_n") alpha_cum_bar_c = dolfinx.fem.Function(V_Φ, name="alpha_cum_bar_c") alpha_cum_bar_n = dolfinx.fem.Function(V_Φ, name="alpha_cum_bar_n") delta_alpha = dolfinx.fem.Function(V_Φ, name="delta_alpha") # Displacement ----------------------------------------------------------- F_u = ufl.inner((g(Φ_new, Data.degradation_function) + Data.k) * sigma_a(u_new, Data) + sigma_b(u_new, Data), epsilon(δu)) * ufl.dx #F_u_form = dolfinx.fem.form(F_u) F_u_form = dolfinx.fem.form(ufl.inner((g(Φ_new, Data.degradation_function) + Data.k) * sigma_a(u_new, Data) + sigma_b(u_new, Data), epsilon(δu)) * ufl.dx) # External forces -------------------------------------------------------- if T_list_u is not None: L = ufl.inner(T_list_u[0][0], δu) * T_list_u[0][1] for i in range(1, len(T_list_u)): L += ufl.inner(T_list_u[i][0], δu) * T_list_u[i][1] F_u -= L J_u = ufl.derivative(F_u, u_new) J_u_form = dolfinx.fem.form(J_u) petsc_options_u = { "ksp_type": "preonly", "pc_type": "lu", "pc_factor_mat_solver_type": "mumps", "snes_linesearch_type": "none", "snes_max_it": 50000, "snes_rtol": 1e-8, "snes_atol": 1e-9, } problem_u = dolfinx.fem.petsc.NonlinearProblem( F_u, u_new, bcs=bc_list_u, J=J_u, petsc_options=petsc_options_u, petsc_options_prefix="elasticity", ) snes_u = problem_u.solver if rank == 0 and logger: logger.info(" SNES Settings u:") for key, value in petsc_options_u.items(): logger.info(f" {key}: {value}") # Phase-field ------------------------------------------------------------ F_phi = dg(Φ_new, Data.degradation_function) * H * δΦ * ufl.dx c0 = geometric_crack_coefficient(case) if Data.fatigue: F_phi += Fatigue * Data.Gc * (1 / Data.l * ufl.inner(Φ_new, δΦ) + Data.l * ufl.inner(ufl.grad(Φ_new), ufl.grad(δΦ))) * ufl.dx else: F_phi += Data.Gc*(1.0/(c0*Data.l)*geometric_crack_function_derivative(Φ_new, case)*δΦ + Data.l * 2/c0*ufl.inner(ufl.grad(Φ_new), ufl.grad(δΦ)))*ufl.dx J_phi = ufl.derivative(F_phi, Φ_new) problem_phi = dolfinx.fem.petsc.NewtonSolverNonlinearProblem( F_phi, Φ_new, bcs=bc_list_phi, J=J_phi) petsc_options_phi = { "ksp_type": "preonly", "pc_type": "lu", "pc_factor_mat_solver_type": "mumps", "snes_linesearch_type": "none", "snes_max_it": 50000, "snes_rtol": 1e-8, "snes_atol": 1e-9, } problem_phi = dolfinx.fem.petsc.NonlinearProblem( F_phi, Φ_new, bcs=bc_list_phi, J=J_phi, petsc_options=petsc_options_phi, petsc_options_prefix="phase_field", ) snes_phi = problem_phi.solver if rank == 0 and logger: logger.info(" SNES Settings phi:") for key, value in petsc_options_phi.items(): logger.info(f" {key}: {value}") # Solve ################################################################ ######################################################################## start = time.perf_counter() if rank == 0 and logger: logger.info(f" start time: {start}") # Paraview files - DOLFINx handles parallel I/O automatically if Data.save_solution_xdmf: paraview_solution_folder_name_xdmf = os.path.join( result_folder_name, "paraview-solutions_xdmf") xdmf_phi = dolfinx.io.XDMFFile(msh.comm, os.path.join( paraview_solution_folder_name_xdmf, "phi.xdmf"), "w") xdmf_phi.write_mesh(msh) xdmf_u = dolfinx.io.XDMFFile(msh.comm, os.path.join( paraview_solution_folder_name_xdmf, "u.xdmf"), "w") # Create directory only on rank 0 if rank == 0: os.makedirs(paraview_solution_folder_name_xdmf, exist_ok=True) comm.Barrier() # Wait for directory creation xdmf_phi = dolfinx.io.XDMFFile(msh.comm, os.path.join( paraview_solution_folder_name_xdmf, "phi.xdmf"), "w") xdmf_phi.write_mesh(msh) xdmf_u = dolfinx.io.XDMFFile(msh.comm, os.path.join( paraview_solution_folder_name_xdmf, "u.xdmf"), "w") if Data.save_solution_vtu: paraview_solution_folder_name_vtu = os.path.join( result_folder_name, "paraview-solutions_vtu") # Create directory only on rank 0 if rank == 0: os.makedirs(paraview_solution_folder_name_vtu, exist_ok=True) comm.Barrier() # Wait for directory creation vtk_sol = dolfinx.io.VTKFile(msh.comm, os.path.join( paraview_solution_folder_name_vtu, "phasefieldx.pvd"), "w") if rank == 0 and logger: logger.info(f" S t a r t i n g A n a l y s i s ") logger.info(f" ---------------------------------- ") logger.info(f" ---------------------------------- ") t = 0 step = 0 while t < final_time: if rank == 0 and logger: logger.info( f"\n\nSolution at (pseudo) time = {t}, dt = {dt}, Step = {step} ") logger.info( f"===========================================================================") if update_boundary_conditions is not None: bc_ux, bc_uy, bc_uz = update_boundary_conditions(bc_list_u, t) else: bc_ux, bc_uy, bc_uz = 0, 0, 0 if update_loading is not None: T_ux, T_uy, T_uz = update_loading(T_list_u, t) # if Data.fatigue: # delta_alpha = np.abs(alpha_c.x.array - alpha_n.x.array) * np.heaviside((alpha_c.x.array - alpha_n.x.array) / dt, 1) # alpha_cum_bar_c.x.array[:] = alpha_cum_bar_n.x.array + delta_alpha # Fatigue.x.array[:] = fatigue_degradation(alpha_cum_bar_c.x.array, Data) error_L2_phi = 1 error_L2_u = 1 stagger_iter = 0 while (error_L2_phi > stagger_error_tol or error_L2_u > stagger_error_tol or stagger_iter < min_stagger_iter) and (stagger_iter < max_stagger_iter): stagger_iter += 1 if rank == 0 and logger: logger.info(f" Stagger Iteration: {stagger_iter}") logger.info(f" ---------------------- ") # Displacement -------------------------------------------- if rank == 0 and logger: logger.info(f">>> Solving phase for dofs: u ") problem_u.solve() converged = problem_u.solver.getConvergedReason() u_iterations = problem_u.solver.getIterationNumber() residuals = snes_u.getConvergenceHistory() residual_norm_u = snes_u.getFunctionNorm() error_L2_u = eval_error_L2_normalized(u_new, u_old, msh) if rank == 0 and logger: if converged <= 0: logger.error(f"Solver did not converge, got {converged}.") raise RuntimeError(f"Solver did not converge, got {converged}.") else: logger.info(f" Newton iterations: {u_iterations}") logger.info(f" Residual norm u: {residual_norm_u}") logger.info(f" Converged reason {converged}.") logger.info(f" Residual history u: {residuals}") logger.info(f" L2 error in u direction: {error_L2_u}") u_old.x.array[:] = u_new.x.array # Irreversibility .... project(psi_a(u_new, Data), V_c) if Data.irreversibility == "miehe": H.x.array[:] = np.maximum(V_c.x.array, V_n.x.array) else: H.x.array[:] = V_c.x.array if Data.fatigue: project(g(Φ_old, Data.degradation_function) * V_c, alpha_c) accelerated = False if accelerated: N = 100 delta_alpha = N * alpha_c.x.array else: delta_alpha = np.abs(alpha_c.x.array - alpha_n.x.array) * \ np.heaviside( (alpha_c.x.array - alpha_n.x.array) / dt, 1) alpha_cum_bar_c.x.array[:] = alpha_cum_bar_n.x.array + delta_alpha Fatigue.x.array[:] = fatigue_degradation( alpha_cum_bar_c.x.array, Data) # Phase-field --------------------------------------------- if rank == 0 and logger: logger.info(f">>> Solving phase for dofs: Φ ") problem_phi.solve() converged = problem_phi.solver.getConvergedReason() phi_iterations = problem_phi.solver.getIterationNumber() residuals = snes_phi.getConvergenceHistory() residual_norm_phi = snes_phi.getFunctionNorm() error_L2_phi = eval_error_L2_normalized(Φ_new, Φ_old, msh) if rank == 0 and logger: if converged <= 0: logger.error(f"Solver did not converge, got {converged}.") raise RuntimeError(f"Solver did not converge, got {converged}.") else: logger.info(f" Newton iterations: {phi_iterations}") logger.info(f" Residual norm Φ: {residual_norm_phi}") logger.info(f" Converged reason {converged}.") logger.info(f" Residual history Φ: {residuals}") logger.info(f" L2 error in Φ direction: {error_L2_phi}") Φ_old.x.array[:] = Φ_new.x.array # Irreversibility .... if Data.irreversibility == "miehe": V_n.x.array[:] = np.maximum(V_c.x.array, V_n.x.array) if Data.fatigue: # project(g(Φ_new, Data.degradation_function) * V_c, alpha_c) # delta_alpha = np.abs(alpha_c.x.array - alpha_n.x.array) * np.heaviside((alpha_c.x.array - alpha_n.x.array) / dt, 1) # alpha_cum_bar_c.x.array[:] = alpha_cum_bar_c.x.array + delta_alpha # + delta_alpha alpha_cum_bar_n.x.array[:] = alpha_cum_bar_c.x.array alpha_n.x.array[:] = alpha_c.x.array # Save results ###################################################################### if rank == 0 and logger: logger.info(f"\n\n Saving results: ") # conv --------------------------------------------------------------- append_results_to_file(os.path.join(result_folder_name, "phasefieldx.conv"), '#step\tstagger\titerPhi\titerU', step, stagger_iter, phi_iterations, u_iterations) # Degree of freedom -------------------------------------------------- append_results_to_file(os.path.join(result_folder_name, "top.dof"), '#step\tUx\tUy\tUz\tphi', step, bc_ux, bc_uy, bc_uz, 0.0) # Reaction ----------------------------------------------------------- if comm.Get_size() == 1: # Only available for single process for i in range(0, len(bc_list_u)): R = calculate_reaction_forces(J_u_form, F_u_form, [bc_list_u[i]], u_new, V_u, msh.topology.dim) append_results_to_file(os.path.join( result_folder_name, bcs_list_u_names[i] + ".reaction"), '#step\tRx\tRy\tRz', step, R[0], R[1], R[2]) # Energy ------------------------------------------------------------- E, PSI_a, PSI_b = compute_total_energies(u_new, Φ_new, Data, comm, dx=ufl.dx) gamma, gamma_phi, gamma_gradphi = calculate_crack_surface_energy(Φ_new, Data.l, comm, case, ufl.dx) if Data.fatigue: W_phi = comm.allreduce(dolfinx.fem.assemble_scalar(dolfinx.fem.form( Fatigue * Data.Gc * 1 / (2 * Data.l) * ufl.inner(Φ_new, Φ_new) * ufl.dx)),op=MPI.SUM) W_gradphi = comm.allreduce(dolfinx.fem.assemble_scalar(dolfinx.fem.form( Fatigue * Data.Gc * Data.l / 2 * ufl.inner(ufl.grad(Φ_new), ufl.grad(Φ_new)) * ufl.dx)),op=MPI.SUM) alpha_acum = comm.allreduce(dolfinx.fem.assemble_scalar( dolfinx.fem.form(alpha_cum_bar_c * ufl.dx)),op=MPI.SUM) else: W_phi = Data.Gc * gamma_phi W_gradphi = Data.Gc * gamma_gradphi alpha_acum = 0.0 W = W_phi + W_gradphi EplusW = E + W # Only rank 0 writes energy results if rank == 0: header = '#step\tEplusW\tE\tW\tW_phi\tW_gradphi\tgamma\tgamma_phi\tgamma_gradphi\tPSI_a\tPSI_b\talpha_acum' append_results_to_file(os.path.join(result_folder_name, "total.energy"), header, step, EplusW, E, W, W_phi, W_gradphi, gamma, gamma_phi, gamma_gradphi, PSI_a, PSI_b, alpha_acum) # Paraview ----------------------------------------------------------- if Data.save_solution_xdmf: xdmf_phi.write_function(Φ_new, step) xdmf_u.write_function(u_new, step) if Data.save_solution_vtu: vtk_sol.write_function([Φ_new, u_new], step) t += dt step += 1 if Data.save_solution_xdmf: xdmf_phi.close() xdmf_u.close() if Data.save_solution_vtu: vtk_sol.close() if rank == 0: end = time.perf_counter() if logger: log_end_analysis(logger, end - start)