def gauss_newton_energy_norm(self, q): r"""Compute the energy norm of a field w.r.t. the Gauss-Newton operator The energy norm of a field :math:`q` w.r.t. the Gauss-Newton operator :math:`H` can be computed using one fewer linear solve than if we were to calculate the action of :math:`H\cdot q` on :math:`q`. This saves computation when using the conjugate gradient method to solve for the search direction. """ u, p = self.state, self.parameter dE = derivative(self._E, u) dR = derivative(self._R, p) dF_du, dF_dp = self._dF_du, derivative(self._F, p) v = firedrake.Function(u.function_space()) firedrake.solve(dF_du == action(dF_dp, q), v, self._bc, solver_parameters=self._solver_params, form_compiler_parameters=self._fc_params) return self._assemble( firedrake.energy_norm(derivative(dE, u), v) + firedrake.energy_norm(derivative(dR, p), q))
def update_search_direction(self): r"""Solve the Gauss-Newton system for the new search direction using the preconditioned conjugate gradient method""" p, q, dJ = self.parameter, self.search_direction, self.gradient dR = derivative(self.regularization, self.parameter) Q = q.function_space() M = firedrake.TrialFunction(Q) * firedrake.TestFunction(Q) * dx + \ derivative(dR, p) # Compute the preconditioned residual z = firedrake.Function(Q) firedrake.solve(M == -dJ, z, solver_parameters=self._solver_params, form_compiler_parameters=self._fc_params) # This variable is a search direction for a search direction, which # is definitely not confusing at all. s = z.copy(deepcopy=True) q *= 0.0 old_cost = np.inf while True: z_mnorm = self._assemble(firedrake.energy_norm(M, z)) s_hnorm = self.gauss_newton_energy_norm(s) α = z_mnorm / s_hnorm δz = firedrake.Function(Q) g = self.gauss_newton_mult(s) firedrake.solve(M == g, δz, solver_parameters=self._solver_params, form_compiler_parameters=self._fc_params) q += α * s z -= α * δz β = self._assemble(firedrake.energy_norm(M, z)) / z_mnorm s *= β s += z energy_norm = self.gauss_newton_energy_norm(q) cost = 0.5 * energy_norm + self._assemble(action(dJ, q)) if (abs(old_cost - cost) / (0.5 * energy_norm) < self._search_tolerance): return old_cost = cost
def step(self): r"""Take one step of the conjugate gradient iteration""" q = self.solution s = self.search_direction z = self.residual δz = self._delta_residual α = self.residual_energy / self.search_direction_energy Gs = self.operator_product dJ = self._rhs delta_energy = α * (self._assemble(action(Gs, q)) + 0.5 * α * self.search_direction_energy) self._energy += delta_energy self._objective += delta_energy + α * self._assemble(action(dJ, s)) q.assign(q + Constant(α) * s) z.assign(z - Constant(α) * δz) M = self.preconditioner residual_energy = self._assemble(firedrake.energy_norm(M, z)) β = residual_energy / self.residual_energy s.assign(Constant(β) * s + z) self.update_state() self._residual_energy = residual_energy Gs = self.operator_product self._search_direction_energy = self._assemble(action(Gs, s)) self._iteration += 1
def reinit(self): r"""Restart the solution to 0""" self._iteration = 0 M = self._preconditioner z = self.residual s = self.search_direction Gs = self.operator_product Q = z.function_space() self.solution.assign(firedrake.Function(Q)) self._residual_solver.solve() s.assign(z) self._residual_energy = self._assemble(firedrake.energy_norm(M, z)) self.update_state() self._search_direction_energy = self._assemble(action(Gs, s)) self._energy = 0. self._objective = 0.