|
| 1 | +from matplotlib import pyplot |
| 2 | +import numpy as np |
| 3 | +from tqdm import trange, tqdm |
| 4 | + |
| 5 | +from petsc4py import PETSc |
| 6 | +from mpi4py import MPI |
| 7 | + |
| 8 | +import ufl |
| 9 | +from ufl import dot, grad, dx, exp, inner, sqrt, erf, cos, pi |
| 10 | +from dolfinx import fem, mesh, io, default_scalar_type, log |
| 11 | +from dolfinx.fem.petsc import NonlinearProblem |
| 12 | +from dolfinx.nls.petsc import NewtonSolver |
| 13 | +from basix.ufl import element |
| 14 | + |
| 15 | +import sys |
| 16 | +from runpy import run_path |
| 17 | + |
| 18 | +# Inject the variables from the parameter file to the global namespace |
| 19 | +try: |
| 20 | + param_file = sys.argv[1] |
| 21 | + params = run_path(param_file) |
| 22 | +except: |
| 23 | + error("Please provide the parameter file (.py) in the working directory!") |
| 24 | + |
| 25 | +for k, v in params.items(): |
| 26 | + if not k.startswith('_'): |
| 27 | + globals()[k] = v |
| 28 | + |
| 29 | + |
| 30 | +# Output |
| 31 | +solution_file = f'{label}_solution.npy' # Full solution |
| 32 | +output_file = f'{label}_output.bp' # VTX mesh for visualization |
| 33 | + |
| 34 | +log.set_output_file(f'{label}.log') |
| 35 | +log.set_log_level(log.LogLevel.INFO) |
| 36 | + |
| 37 | +# Helper functions |
| 38 | + |
| 39 | +def get_variable_value(v): |
| 40 | + """ Get the nodal values for an expression v """ |
| 41 | + q = fem.Function(V) |
| 42 | + q.interpolate(fem.Expression(v, V.element.interpolation_points())) |
| 43 | + return q.x.array |
| 44 | + |
| 45 | +def ramp_up(val=1, t1=0.1): |
| 46 | + """ Linear ramp from zero beginning for t = 0, useful to assist convergence in the beginning """ |
| 47 | + return val*ufl.min_value(1, t/t1) |
| 48 | + |
| 49 | +# Generate 3D mesh |
| 50 | +domain = mesh.create_box(MPI.COMM_WORLD, [[xmin, ymin, zmin], [xmax, ymax, zmax]], [nx, ny, nz], |
| 51 | + mesh.CellType.hexahedron) |
| 52 | + |
| 53 | +x = ufl.SpatialCoordinate(domain) |
| 54 | + |
| 55 | +# Current time and time step size |
| 56 | +t = fem.Constant(domain, default_scalar_type(0)) |
| 57 | +Δt = fem.Constant(domain, default_scalar_type(Δt0)) |
| 58 | + |
| 59 | +# Create function space |
| 60 | +P1 = element("Lagrange", domain.basix_cell(), 1) |
| 61 | +V = fem.functionspace(domain, P1) |
| 62 | + |
| 63 | +# Unknown solution (t=n+1) |
| 64 | +u = fem.Function(V, name='u') |
| 65 | + |
| 66 | +# Previous time step solution |
| 67 | +u_n = fem.Function(V, name='u_n') |
| 68 | + |
| 69 | +# Saturation curve |
| 70 | +def S_curve(r): |
| 71 | + σ = rstd*r_μ |
| 72 | + return 1/2*(erf((r-r_μ)/(sqrt(2)*σ)) - erf((r_cutoff-r_μ)/(sqrt(2)*σ))) |
| 73 | + |
| 74 | +# Capillary radius |
| 75 | +θ = θdeg*pi/180 |
| 76 | +r_c = -2*γ*cos(θ)/u |
| 77 | + |
| 78 | +# Saturation |
| 79 | +#S = ufl.conditional(u < 0, S_curve(r_c), 1.0) |
| 80 | +S = S_curve(r_c) |
| 81 | + |
| 82 | +# Previous saturation |
| 83 | +S_n = ufl.replace(S, {u: u_n}) |
| 84 | + |
| 85 | +# Flux |
| 86 | +k = ufl.as_tensor([[k_long, 0, 0], [0, k_trans, 0], [0, 0, k_trans]]) |
| 87 | +k_s = 1 + ramp_up(-1+S) |
| 88 | +J = -k_s*k/μ*grad(u) |
| 89 | + |
| 90 | +############### |
| 91 | +# Weak form |
| 92 | +############### |
| 93 | + |
| 94 | +δu = ufl.TestFunction(V) |
| 95 | + |
| 96 | +F = δu * ϕ*(S - S_n) * dx - Δt*dot(grad(δu), J) * dx |
| 97 | + |
| 98 | +# Direchlet boundary conditions |
| 99 | + |
| 100 | +fdim = domain.topology.dim - 1 |
| 101 | +domain.topology.create_connectivity(fdim, fdim+1) |
| 102 | +boundary_facets = mesh.exterior_facet_indices(domain.topology) |
| 103 | +boundary_dofs = fem.locate_dofs_topological(V, fdim, boundary_facets) |
| 104 | +bc = fem.dirichletbc(default_scalar_type(u_ext), boundary_dofs, V) |
| 105 | +bcs = [bc] |
| 106 | + |
| 107 | +# Initial pressure |
| 108 | +u0 = -2*γ*cos(θ)/r_cutoff |
| 109 | + |
| 110 | +if __name__ == '__main__': |
| 111 | + # Initial moisture content |
| 112 | + u.interpolate(lambda x: np.full(x[0].shape, u0)) |
| 113 | + u.x.array[boundary_dofs] = u_ext |
| 114 | + |
| 115 | + # Initialize the Newton solver |
| 116 | + problem = NonlinearProblem(F, u, bcs) |
| 117 | + solver = NewtonSolver(MPI.COMM_WORLD, problem) |
| 118 | + |
| 119 | + # Solution is rather sensitive to tolerance, |
| 120 | + # numerical accuracy seems to be close to 1e-11 |
| 121 | + solver.rtol = 0 |
| 122 | + solver.atol = 1e-11 |
| 123 | + solver.report = True |
| 124 | + solver.error_on_nonconvergence = False |
| 125 | + solver.max_it = 10 |
| 126 | + solver.relaxation_parameter = 0.8 |
| 127 | + |
| 128 | + # Use mumps LU solver |
| 129 | + ksp = solver.krylov_solver |
| 130 | + ksp.setType("preonly") |
| 131 | + ksp.getPC().setType("lu") |
| 132 | + ksp.getPC().setFactorSolverType("mumps") |
| 133 | + |
| 134 | + # Solutions at each time step |
| 135 | + sol = [u.x.array.copy()] |
| 136 | + |
| 137 | + # Time values |
| 138 | + times = [t.value.item()] |
| 139 | + |
| 140 | + if domain.comm.rank == 0: |
| 141 | + pbar = tqdm(total=t_end, unit_scale=True) |
| 142 | + |
| 143 | + S_func = fem.Function(V, name='S') |
| 144 | + |
| 145 | + with io.VTXWriter(domain.comm, output_file, [u, S_func]) as vtxfile: |
| 146 | + while True: |
| 147 | + u_n.x.array[:] = u.x.array |
| 148 | + |
| 149 | + # Adaptive timestepping |
| 150 | + while True: |
| 151 | + n, converged = solver.solve(u) |
| 152 | + if converged: |
| 153 | + solver.relaxation_parameter = 0.8 |
| 154 | + if n <= 3: |
| 155 | + Δt.value = min(Δt.value * 1.2, Δt_max) |
| 156 | + log.log(log.LogLevel.INFO, f'Timestep = {Δt.value}') |
| 157 | + elif n >= 7: |
| 158 | + Δt.value = Δt.value * 0.5 |
| 159 | + log.log(log.LogLevel.INFO, f'Timestep = {Δt.value}') |
| 160 | + break |
| 161 | + else: |
| 162 | + solver.relaxation_parameter = 0.4 |
| 163 | + Δt.value = Δt.value * 0.5 |
| 164 | + if Δt.value < Δt_min: |
| 165 | + raise Error("Minimum timestep size reached!") |
| 166 | + u.x.array[:] = u_n.x.array |
| 167 | + log.log(log.LogLevel.INFO, f'Timestep = {Δt.value}') |
| 168 | + |
| 169 | + t.value += Δt.value |
| 170 | + |
| 171 | + # Record values |
| 172 | + sol.append(u.x.array.copy()) |
| 173 | + times.append(t.value.item()) |
| 174 | + |
| 175 | + if domain.comm.rank == 0: |
| 176 | + pbar.update(Δt.value) |
| 177 | + |
| 178 | + # Save output |
| 179 | + S_func.interpolate(fem.Expression(S, V.element.interpolation_points())) |
| 180 | + vtxfile.write(t.value.item()) |
| 181 | + |
| 182 | + # Termination criterion |
| 183 | + if t.value > t_end: |
| 184 | + break |
| 185 | + |
| 186 | + S_threshold = 0.99 |
| 187 | + S_vals = get_variable_value(S) |
| 188 | + finished = np.all(S_vals > S_threshold) |
| 189 | + statuses = domain.comm.gather(finished, root=0) |
| 190 | + can_stop = None |
| 191 | + if domain.comm.rank == 0: |
| 192 | + can_stop = all(statuses) |
| 193 | + if domain.comm.bcast(can_stop, root=0): |
| 194 | + break |
| 195 | + |
| 196 | + # Numpy array output for single parallel runs |
| 197 | + if domain.comm.size == 1: |
| 198 | + sol = np.array(sol) |
| 199 | + np.save(solution_file, sol) |
| 200 | + |
| 201 | + |
| 202 | +# The residual and Jacobian for diagnostics |
| 203 | +#residual = fem.form(F) |
| 204 | +#RD = fem.petsc.create_vector(residual) |
| 205 | +#fem.petsc.assemble_vector(RD, residual) |
| 206 | +#jacobian = fem.form(ufl.derivative(F, u)) |
| 207 | + |
| 208 | +#fem.petsc.apply_lifting(RD, [jacobian], [bcs]) |
| 209 | + |
| 210 | +#A = fem.petsc.create_matrix(jacobian) |
| 211 | +#fem.petsc.assemble_matrix(A, jacobian) |
| 212 | +#A.assemble() |
| 213 | +#print(domain.comm.rank, A.getSize()) |
| 214 | +#m = A.convert('dense').getDenseArray() |
0 commit comments