def _newton_solve(z, E, scale, tolerance=1e-6, armijo=1e-4, max_iterations=50): F = derivative(E, z) H = derivative(F, z) Q = z.function_space() bc = firedrake.DirichletBC(Q, 0, 'on_boundary') p = firedrake.Function(Q) for iteration in range(max_iterations): firedrake.solve(H == -F, p, bc, solver_parameters={'ksp_type': 'preonly', 'pc_type': 'lu'}) dE_dp = assemble(action(F, p)) α = 1.0 E0 = assemble(E) Ez = assemble(replace(E, {z: z + firedrake.Constant(α) * p})) while (Ez > E0 + armijo * α * dE_dp) or np.isnan(Ez): α /= 2 Ez = assemble(replace(E, {z: z + firedrake.Constant(α) * p})) z.assign(z + firedrake.Constant(α) * p) if abs(dE_dp) < tolerance * assemble(scale): return z raise ValueError("Newton solver failed to converge after {0} iterations" .format(max_iterations))
def _setup(self, **kwargs): for name, field in kwargs.items(): if name in self._fields.keys(): self._fields[name].assign(field) else: if isinstance(field, firedrake.Constant): self._fields[name] = firedrake.Constant(field) elif isinstance(field, firedrake.Function): self._fields[name] = field.copy(deepcopy=True) else: raise TypeError( "Input %s field has type %s, must be Constant or Function!" % (name, type(field)) ) # Create symbolic representations of the flux and sources of damage dt = firedrake.Constant(1.0) flux = self.model.flux(**self.fields) # Create the finite element mass matrix D = self.fields["damage"] Q = D.function_space() φ, ψ = firedrake.TrialFunction(Q), firedrake.TestFunction(Q) M = φ * ψ * dx L1 = -dt * flux D1 = firedrake.Function(Q) D2 = firedrake.Function(Q) L2 = firedrake.replace(L1, {D: D1}) L3 = firedrake.replace(L1, {D: D2}) dD = firedrake.Function(Q) parameters = { "solver_parameters": { "ksp_type": "preonly", "pc_type": "bjacobi", "sub_pc_type": "ilu", } } problem1 = LinearVariationalProblem(M, L1, dD) problem2 = LinearVariationalProblem(M, L2, dD) problem3 = LinearVariationalProblem(M, L3, dD) solver1 = LinearVariationalSolver(problem1, **parameters) solver2 = LinearVariationalSolver(problem2, **parameters) solver3 = LinearVariationalSolver(problem3, **parameters) self._solvers = [solver1, solver2, solver3] self._stages = [D1, D2] self._damage_change = dD self._timestep = dt
def step(self): r"""Perform a backtracking line search for the next value of the solution and compute the search direction for the next step""" E = self.problem.E u = self.problem.u v = self.v t = self.t t.assign(1.) E_0 = self.problem.assemble(E) slope = self.problem.assemble(self.dE_dv) if slope > 0: raise firedrake.ConvergenceError( 'Minimization solver has invalid search direction. This is ' 'likely due to a negative thickness or friction coefficient or' 'otherwise physically invalid input data.') E_t = firedrake.replace(E, {u: u + t * v}) armijo = self.armijo contraction = self.contraction while self.problem.assemble(E_t) > E_0 + armijo * float(t) * slope: t.assign(t * contraction) u.assign(u + t * v) self.search_direction_solver.solve() self.iteration += 1
def reconstruct(self, field=None, V=None, subu=None, u=None, row_field=None, col_field=None, action_x=None, use_split=False): subu = subu or self.u row_field = row_field or field col_field = col_field or field # define W and form if field is None: # Returns self W = self._function_space form = self.f else: assert V is not None, "`V` can not be `None` when `field` is not `None`" W = self.as_subspace(field, V, use_split) if W is None: return rank = len(self.f.arguments()) splitter = ExtractSubBlock() if rank == 1: form = splitter.split(self.f, argument_indices=(row_field, )) elif rank == 2: form = splitter.split(self.f, argument_indices=(row_field, col_field)) if u is not None: form = replace(form, {self.u: u}) if action_x is not None: assert len(form.arguments()) == 2, "rank of self.f must be 2 when using action_x parameter" form = ufl_expr.action(form, action_x) ebc = EquationBCSplit(form, subu, self.sub_domain, method=self.method, V=W) for bc in self.bcs: if isinstance(bc, DirichletBC): ebc.add(bc.reconstruct(V=W, g=bc.function_arg, sub_domain=bc.sub_domain, method=bc.method, use_split=use_split)) elif isinstance(bc, EquationBCSplit): bc_temp = bc.reconstruct(field=field, V=V, subu=subu, u=u, row_field=row_field, col_field=col_field, action_x=action_x, use_split=use_split) # Due to the "if index", bc_temp can be None if bc_temp is not None: ebc.add(bc_temp) return ebc
def reconstruct(self, field=None, V=None, subu=None, u=None, row_field=None, col_field=None, action_x=None, use_split=False): subu = subu or self.u row_field = row_field or field col_field = col_field or field # define W and form if field is None: # Returns self W = self._function_space form = self.f else: assert V is not None, "`V` can not be `None` when `field` is not `None`" W = self.as_subspace(field, V, use_split) if W is None: return rank = len(self.f.arguments()) splitter = ExtractSubBlock() if rank == 1: form = splitter.split(self.f, argument_indices=(row_field, )) elif rank == 2: form = splitter.split(self.f, argument_indices=(row_field, col_field)) if u is not None: form = replace(form, {self.u: u}) if action_x is not None: assert len(form.arguments()) == 2, "rank of self.f must be 2 when using action_x parameter" form = ufl_expr.action(form, action_x) ebc = EquationBCSplit(form, subu, self.sub_domain, method=self.method, V=W) for bc in self.bcs: if isinstance(bc, DirichletBC): ebc.add(bc.reconstruct(V=W, g=bc.function_arg, sub_domain=bc.sub_domain, method=bc.method, use_split=use_split)) elif isinstance(bc, EquationBCSplit): bc_temp = bc.reconstruct(field=field, V=V, subu=subu, u=u, row_field=row_field, col_field=col_field, action_x=action_x, use_split=use_split) # Due to the "if index", bc_temp can be None if bc_temp is not None: ebc.add(bc_temp) return ebc
def initialize(self, init_solution): self.solution_old.assign(init_solution) z_theta = (1-self.theta)*self.solution_old + self.theta*self.solution self._fields = [] for fields in self.fields: cfields = fields.copy() for field_name, field_expr in fields.items(): if isinstance(field_expr, float): continue cfields[field_name] = firedrake.replace(field_expr, {self.solution: z_theta}) self._fields.append(cfields) F = 0 for test, u, u_old, eq, mass_term, fields, bcs in zip(self.test, firedrake.split(self.solution), firedrake.split(self.solution_old), self.equations, self.mass_terms, self._fields, self.bcs): if mass_term: F += eq.mass_term(test, u-u_old) u_theta = (1-self.theta)*u_old + self.theta*u F -= self.dt_const * eq.residual(test, u_theta, u_theta, fields, bcs=bcs) self.problem = firedrake.NonlinearVariationalProblem(F, self.solution, bcs=self.strong_bcs) self.solver = firedrake.NonlinearVariationalSolver(self.problem, solver_parameters=self.solver_parameters, options_prefix=self.name) self._initialized = True
def _setup(self, **kwargs): for name, field in kwargs.items(): if name in self.fields.keys(): self.fields[name].assign(field) else: self.fields[name] = utilities.copy(field) # Create symbolic representations of the flux and sources of damage dt = firedrake.Constant(1.) flux = self.model.flux(**self.fields) # Create the finite element mass matrix D = self.fields.get('damage', self.fields.get('D')) Q = D.function_space() φ, ψ = firedrake.TrialFunction(Q), firedrake.TestFunction(Q) M = φ * ψ * dx L1 = -dt * flux D1 = firedrake.Function(Q) D2 = firedrake.Function(Q) L2 = firedrake.replace(L1, {D: D1}) L3 = firedrake.replace(L1, {D: D2}) dD = firedrake.Function(Q) parameters = { 'solver_parameters': { 'ksp_type': 'preonly', 'pc_type': 'bjacobi', 'sub_pc_type': 'ilu' } } problem1 = LinearVariationalProblem(M, L1, dD) problem2 = LinearVariationalProblem(M, L2, dD) problem3 = LinearVariationalProblem(M, L3, dD) solver1 = LinearVariationalSolver(problem1, **parameters) solver2 = LinearVariationalSolver(problem2, **parameters) solver3 = LinearVariationalSolver(problem3, **parameters) self._solvers = [solver1, solver2, solver3] self._stages = [D1, D2] self._damage_change = dD self._timestep = dt
def derivative_form(self, w): if args.discretisation == "pkp0": fd.warning(fd.RED % "Using residual without grad-div") F = solver.F_nograddiv else: F = solver.F F = fd.replace(F, {F.arguments()[0]: solver.z_adj}) L = F + self.value_form() X = fd.SpatialCoordinate(solver.z.ufl_domain()) return fd.derivative(L, X, w)
def eval_ddJdw(self): u = self.u v = self.v J = self.J F = self.F X = self.X w = self.w V = self.V L = self.L bil_form = self.bil_form params = self.params s = w y_s = Function(V) # follow p 65 of Hinze, Pinnau, Ulbrich, Ulbrich # Step 1: solve( assemble(derivative(F, u)), y_s, assemble(derivative(-F, X, s)), solver_parameters=params, bcs=self.bc, ) # Step 2: Lyy_y_s = assemble(derivative(derivative(L, u), u, y_s)) Lyu_s = assemble(derivative(derivative(L, u), X, s)) h1 = Lyy_y_s h1 += Lyu_s Luy_y_s = assemble(derivative(derivative(L, X), u, y_s)) Luu_s = assemble(derivative(derivative(L, X), X, s)) h2 = Luy_y_s h2 += Luu_s h3_temp = Function(V) # Step 3: solve(assemble(bil_form), h3_temp, h1, bcs=self.bc, solver_parameters=params) F_h3_temp = replace(F, {v: h3_temp}) h3 = assemble(derivative(-F_h3_temp, X)) res = h2 res += h3 return res.vector().inner(w.vector())
def _update_func(self, name, val): """ Utility function to update a function in the main attributes. Args: name (str): The name of the function to update. val (Function): The new value for the function. Raises: AttributeError: If name is not in self. """ if not hasattr(self, name): raise AttributeError('Cannot update {}'.format(name)) attrs = vars(self).copy() old_val = getattr(self, name) for attr_name, attr_val in attrs.items(): if isinstance(attr_val, (Form, Integral, Expr)): updated_val = replace(attr_val, {old_val: val}) setattr(self, attr_name, updated_val)
def eval_dJdw(self): u = self.u v = self.v J = self.J F = self.F X = self.X w = self.w V = self.V params = self.params solve(self.F == 0, u, bcs=self.bc, solver_parameters=params) bil_form = adjoint(derivative(F, u)) rhs = -derivative(J, u) u_adj = Function(V) solve(assemble(bil_form), u_adj, assemble(rhs), bcs=self.bc, solver_parameters=params) L = J + replace(self.F, {v: u_adj}) self.L = L self.bil_form = bil_form return assemble(derivative(L, X, w))
def split(self, fields): from firedrake import replace, as_vector, split from firedrake import NonlinearVariationalProblem as NLVP fields = tuple(tuple(f) for f in fields) splits = self._splits.get(tuple(fields)) if splits is not None: return splits splits = [] problem = self._problem splitter = ExtractSubBlock() for field in fields: F = splitter.split(problem.F, argument_indices=(field, )) J = splitter.split(problem.J, argument_indices=(field, field)) us = problem.u.split() V = F.arguments()[0].function_space() # Exposition: # We are going to make a new solution Function on the sub # mixed space defined by the relevant fields. # But the form may refer to the rest of the solution # anyway. # So we pull it apart and will make a new function on the # subspace that shares data. pieces = [us[i].dat for i in field] if len(pieces) == 1: val, = pieces subu = function.Function(V, val=val) subsplit = (subu, ) else: val = op2.MixedDat(pieces) subu = function.Function(V, val=val) # Split it apart to shove in the form. subsplit = split(subu) # Permutation from field indexing to indexing of pieces field_renumbering = dict([f, i] for i, f in enumerate(field)) vec = [] for i, u in enumerate(us): if i in field: # If this is a field we're keeping, get it from # the new function. Otherwise just point to the # old data. u = subsplit[field_renumbering[i]] if u.ufl_shape == (): vec.append(u) else: for idx in numpy.ndindex(u.ufl_shape): vec.append(u[idx]) # So now we have a new representation for the solution # vector in the old problem. For the fields we're going # to solve for, it points to a new Function (which wraps # the original pieces). For the rest, it points to the # pieces from the original Function. # IOW, we've reinterpreted our original mixed solution # function as being made up of some spaces we're still # solving for, and some spaces that have just become # coefficients in the new form. u = as_vector(vec) F = replace(F, {problem.u: u}) J = replace(J, {problem.u: u}) if problem.Jp is not None: Jp = splitter.split(problem.Jp, argument_indices=(field, field)) Jp = replace(Jp, {problem.u: u}) else: Jp = None bcs = [] for bc in problem.bcs: Vbc = bc.function_space() if Vbc.parent is not None and isinstance(Vbc.parent.ufl_element(), VectorElement): index = Vbc.parent.index else: index = Vbc.index cmpt = Vbc.component # TODO: need to test this logic if index in field: if len(field) == 1: W = V else: W = V.sub(field_renumbering[index]) if cmpt is not None: W = W.sub(cmpt) bcs.append(type(bc)(W, bc.function_arg, bc.sub_domain, method=bc.method)) new_problem = NLVP(F, subu, bcs=bcs, J=J, Jp=Jp, form_compiler_parameters=problem.form_compiler_parameters) new_problem._constant_jacobian = problem._constant_jacobian splits.append(type(self)(new_problem, mat_type=self.mat_type, pmat_type=self.pmat_type, appctx=self.appctx)) return self._splits.setdefault(tuple(fields), splits)
def split(self, fields): from firedrake import replace, as_vector, split from firedrake_ts.ts_solver import DAEProblem from firedrake.bcs import DirichletBC, EquationBC fields = tuple(tuple(f) for f in fields) splits = self._splits.get(tuple(fields)) if splits is not None: return splits splits = [] problem = self._problem splitter = ExtractSubBlock() for field in fields: F = splitter.split(problem.F, argument_indices=(field, )) J = splitter.split(problem.J, argument_indices=(field, field)) us = problem.u.split() V = F.arguments()[0].function_space() # Exposition: # We are going to make a new solution Function on the sub # mixed space defined by the relevant fields. # But the form may refer to the rest of the solution # anyway. # So we pull it apart and will make a new function on the # subspace that shares data. pieces = [us[i].dat for i in field] if len(pieces) == 1: (val, ) = pieces subu = function.Function(V, val=val) subsplit = (subu, ) else: val = op2.MixedDat(pieces) subu = function.Function(V, val=val) # Split it apart to shove in the form. subsplit = split(subu) # Permutation from field indexing to indexing of pieces field_renumbering = dict([f, i] for i, f in enumerate(field)) vec = [] for i, u in enumerate(us): if i in field: # If this is a field we're keeping, get it from # the new function. Otherwise just point to the # old data. u = subsplit[field_renumbering[i]] if u.ufl_shape == (): vec.append(u) else: for idx in numpy.ndindex(u.ufl_shape): vec.append(u[idx]) # So now we have a new representation for the solution # vector in the old problem. For the fields we're going # to solve for, it points to a new Function (which wraps # the original pieces). For the rest, it points to the # pieces from the original Function. # IOW, we've reinterpreted our original mixed solution # function as being made up of some spaces we're still # solving for, and some spaces that have just become # coefficients in the new form. u = as_vector(vec) F = replace(F, {problem.u: u}) J = replace(J, {problem.u: u}) if problem.Jp is not None: Jp = splitter.split(problem.Jp, argument_indices=(field, field)) Jp = replace(Jp, {problem.u: u}) else: Jp = None bcs = [] for bc in problem.bcs: if isinstance(bc, DirichletBC): bc_temp = bc.reconstruct( field=field, V=V, g=bc.function_arg, sub_domain=bc.sub_domain, method=bc.method, ) elif isinstance(bc, EquationBC): bc_temp = bc.reconstruct(field, V, subu, u) if bc_temp is not None: bcs.append(bc_temp) new_problem = DAEProblem( F, subu, problem.udot, problem.tspan, bcs=bcs, J=J, Jp=Jp, form_compiler_parameters=problem.form_compiler_parameters, ) new_problem._constant_jacobian = problem._constant_jacobian splits.append( type(self)( new_problem, mat_type=self.mat_type, pmat_type=self.pmat_type, appctx=self.appctx, transfer_manager=self.transfer_manager, )) return self._splits.setdefault(tuple(fields), splits)
def f(t): p_t.assign(p + firedrake.Constant(t) * q) u_t.assign(self._forward_solve(p_t)) return self._assemble(replace(self._J, {u: u_t, p: p_t}))
def newton_search(E, u, bc, tolerance, scale, max_iterations=50, armijo=1e-4, contraction_factor=0.5, form_compiler_parameters={}, solver_parameters={'ksp_type': 'preonly', 'pc_type': 'lu'}): r"""Find the minimizer of a convex functional Parameters ---------- E : firedrake.Form The functional to be minimized u0 : firedrake.Function Initial guess for the minimizer tolerance : float Stopping criterion for the optimization procedure scale : firedrake.Form A positive scale functional by which to measure the objective max_iterations : int, optional Optimization procedure will stop at this many iterations regardless of convergence armijo : float, optional The constant in the Armijo condition (see Nocedal and Wright) contraction_factor : float, optional The amount by which to backtrack in the line search if the Armijo condition is not satisfied form_compiler_parameters : dict, optional Extra options to pass to the firedrake form compiler solver_parameters : dict, optional Extra options to pass to the linear solver Returns ------- firedrake.Function The approximate minimizer of `E` to within tolerance """ F = firedrake.derivative(E, u) H = firedrake.derivative(F, u) v = firedrake.Function(u.function_space()) dE_dv = firedrake.action(F, v) def assemble(*args, **kwargs): return firedrake.assemble( *args, **kwargs, form_compiler_parameters=form_compiler_parameters) problem = firedrake.LinearVariationalProblem(H, -F, v, bc, form_compiler_parameters=form_compiler_parameters, constant_jacobian=False) solver = firedrake.LinearVariationalSolver(problem, solver_parameters=solver_parameters) n = 0 while True: # Compute a search direction solver.solve() # Compute the directional derivative, check if we're done slope = assemble(dE_dv) assert slope < 0 if (abs(slope) < assemble(scale) * tolerance) or (n >= max_iterations): return u # Backtracking search E0 = assemble(E) α = firedrake.Constant(1) Eα = firedrake.replace(E, {u: u + α * v}) while assemble(Eα) > E0 + armijo * α.values()[0] * slope: α.assign(α * contraction_factor) u.assign(u + α * v) n += 1
def f(t): s.assign(t) u_s.assign(self._forward_solve(p_s)) return self._assemble(replace(self._J, {u: u_s, p: p_s}))
def solve(self, dt, D0, u, A, D_inflow=None, **kwargs): r"""Propogate the damage forward by one timestep This function uses a Runge-Kutta scheme to upwind damage (limiting damage diffusion) while sourcing and sinking damage assocaited with crevasse opening/crevasse healing Parameters ---------- dt : float Timestep D0 : firedrake.Function initial damage feild should be discontinuous u : firedrake.Function Ice velocity A : firedrake.Function fluidity parameter D_inflow : firedrake.Function Damage of the upstream ice that advects into the domain Returns ------- D : firedrake.Function Ice damage at `t + dt` """ D_inflow = D_inflow if D_inflow is not None else D0 Q = D0.function_space() dD, φ = firedrake.TrialFunction(Q), firedrake.TestFunction(Q) d = φ * dD * dx D = D0.copy(deepcopy=True) n = firedrake.FacetNormal(Q.mesh()) un = 0.5 * (inner(u, n) + abs(inner(u, n))) L1 = dt * (D * div(φ * u) * dx - φ * max_value(inner(u, n), 0) * D * ds - φ * min_value(inner(u, n), 0) * D_inflow * ds - (φ('+') - φ('-')) * (un('+') * D('+') - un('-') * D('-')) * dS) D1 = firedrake.Function(Q) D2 = firedrake.Function(Q) L2 = firedrake.replace(L1, {D: D1}) L3 = firedrake.replace(L1, {D: D2}) dq = firedrake.Function(Q) # Three-stage strong structure-preserving Runge Kutta (SSPRK3) method params = { 'ksp_type': 'preonly', 'pc_type': 'bjacobi', 'sub_pc_type': 'ilu' } prob1 = firedrake.LinearVariationalProblem(d, L1, dq) solv1 = firedrake.LinearVariationalSolver(prob1, solver_parameters=params) prob2 = firedrake.LinearVariationalProblem(d, L2, dq) solv2 = firedrake.LinearVariationalSolver(prob2, solver_parameters=params) prob3 = firedrake.LinearVariationalProblem(d, L3, dq) solv3 = firedrake.LinearVariationalSolver(prob3, solver_parameters=params) solv1.solve() D1.assign(D + dq) solv2.solve() D2.assign(0.75 * D + 0.25 * (D1 + dq)) solv3.solve() D.assign((1.0 / 3.0) * D + (2.0 / 3.0) * (D2 + dq)) # Increase/decrease damage depending on stress and strain rates ε = sym(grad(u)) ε_1 = eigenvalues(ε)[0] σ = M(ε, A) σ_e = sqrt(inner(σ, σ) - det(σ)) ε_h = firedrake.Constant(self.healing_strain_rate) σ_d = firedrake.Constant(self.damage_stress) γ_h = firedrake.Constant(self.healing_rate) γ_d = firedrake.Constant(self.damage_rate) healing = γ_h * min_value(ε_1 - ε_h, 0) fracture = γ_d * conditional(σ_e - σ_d > 0, ε_1, 0.) * (1 - D) # Clamp damage field to [0, 1] D.project(min_value(max_value(D + dt * (healing + fracture), 0), 1)) return D