def hessian(self, m_dot, project=False): """ Evaluates the Hessian action at the most recently evaluated control value in direction m_dot. Args: m_dot: The direction in control space in which to compute the Hessian. Must be of the same type as the Control (e.g. Function, Constant or lists of latter). project (Optional[bool]): If True, the returned value will be the L2 Riesz representer, if False it will be the l2 Riesz representative. The L2 projection requires one additional linear solve. Defaults to False. Returns: The directional second derivative. The returned type is the same as the control type. Note: Hessian evaluations never delete the forward state. """ # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls] + [m_dot]) fnspaces = [ p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls ] if hash in self._cache["hessian_cache"]: info_green("Got a Hessian cache hit.") return cache_load(self._cache["hessian_cache"][hash], fnspaces) else: info_red("Got a Hessian cache miss") # Compute the Hessian action by solving the second order adjoint equations Hm = self.H(m_dot, project=project) # Apply the scaling factor scaled_Hm = utils.scale(Hm, self.scale) # Call callback control_data = [p.data() for p in self.controls] if self.current_func_value is not None: current_func_value = self.scale * self.current_func_value else: current_func_value = None self.hessian_cb(current_func_value, delist(control_data, list_type=self.controls), m_dot, scaled_Hm) # Cache the result if self.cache is not None: self._cache["hessian_cache"][hash] = cache_store( scaled_Hm, self.cache) return scaled_Hm
def get_residual(i): from .adjrhs import adj_get_forward_equation (fwd_var, lhs, rhs) = adj_get_forward_equation(i) if isinstance(lhs, adjlinalg.IdentityMatrix): return None fn_space = ufl.algorithms.extract_arguments(lhs)[0].function_space() x = backend.Function(fn_space) if rhs == 0: form = lhs x = fwd_var.nonlinear_u else: form = backend.action(lhs, x) - rhs try: y = adjglobals.adjointer.get_variable_value(fwd_var).data except libadjoint.exceptions.LibadjointErrorNeedValue: info_red( "Warning: recomputing forward solution; please report this script on launchpad" ) y = adjglobals.adjointer.get_forward_solution(i)[1].data form = backend.replace(form, {x: y}) return form
def assemble_data(self): assert not isinstance(self.data, IdentityMatrix) if backend.__name__ == "firedrake": # Firedrake specifies assembled matrix type as part of the # solver parameters. mat_type = self.solver_parameters.get("mat_type") assemble = lambda x: backend.assemble(self.data, mat_type=mat_type) else: assemble = backend.assemble if not self.cache: if hasattr(self.data.arguments()[0], '_V_multi'): return backend.assemble_multimesh(self.data) else: return backend.assemble(self.data) else: if self.data in caching.assembled_adj_forms: if backend.parameters["adjoint"]["debug_cache"]: backend.info_green("Got an assembly cache hit") return caching.assembled_adj_forms[self.data] else: if backend.parameters["adjoint"]["debug_cache"]: backend.info_red("Got an assembly cache miss") if hasattr(self.data.arguments()[0], '_V_multi'): M = backend.assemble_multimesh(self.data) else: M = backend.assemble(self.data) caching.assembled_adj_forms[self.data] = M return M
def get_residual(i): from adjrhs import adj_get_forward_equation (fwd_var, lhs, rhs) = adj_get_forward_equation(i) if isinstance(lhs, adjlinalg.IdentityMatrix): return None fn_space = ufl.algorithms.extract_arguments(lhs)[0].function_space() x = backend.Function(fn_space) if rhs == 0: form = lhs x = fwd_var.nonlinear_u else: form = backend.action(lhs, x) - rhs try: y = adjglobals.adjointer.get_variable_value(fwd_var).data except libadjoint.exceptions.LibadjointErrorNeedValue: info_red("Warning: recomputing forward solution; please report this script on launchpad") y = adjglobals.adjointer.get_forward_solution(i)[1].data form = backend.replace(form, {x: y}) return form
def derivative_action(self, dependencies, values, variable, contraction_vector, hermitian): # If you want to apply boundary conditions symmetrically in the adjoint # -- and you often do -- # then we need to have a UFL representation of all the terms in the adjoint equation. # However! # Since UFL cannot represent the identity map, # we need to find an f such that when # assemble(inner(f, v)*dx) # we get the contraction_vector.data back. # This involves inverting a mass matrix. if backend.parameters["adjoint"]["symmetric_bcs"] and backend.__version__ <= '1.2.0': backend.info_red("Warning: symmetric BC application requested but unavailable in dolfin <= 1.2.0.") if backend.parameters["adjoint"]["symmetric_bcs"] and backend.__version__ > '1.2.0': V = contraction_vector.data.function_space() v = backend.TestFunction(V) if str(V) not in adjglobals.fsp_lu: u = backend.TrialFunction(V) A = backend.assemble(backend.inner(u, v)*backend.dx) lusolver = backend.LUSolver(A, "mumps") lusolver.parameters["symmetric"] = True lusolver.parameters["reuse_factorization"] = True adjglobals.fsp_lu[str(V)] = lusolver else: lusolver = adjglobals.fsp_lu[str(V)] riesz = backend.Function(V) lusolver.solve(riesz.vector(), contraction_vector.data.vector()) return adjlinalg.Vector(backend.inner(riesz, v)*backend.dx) else: return adjlinalg.Vector(contraction_vector.data)
def solve(self, var, b): timer = backend.Timer("Matrix-free solver") solver = backend.PETScKrylovSolver(*self.solver_parameters) solver.parameters.update(self.parameters) x = backend.Function(self.fn_space) if b.data is None: backend.info_red("Warning: got zero RHS for the solve associated with variable %s" % var) return adjlinalg.Vector(x) if isinstance(b.data, backend.Function): rhs = b.data.vector().copy() else: rhs = backend.assemble(b.data) if var.type in ['ADJ_TLM', 'ADJ_ADJOINT']: self.bcs = [backend.homogenize(bc) for bc in self.bcs if isinstance(bc, backend.cpp.DirichletBC)] + [bc for bc in self.bcs if not isinstance(bc, backend.DirichletBC)] for bc in self.bcs: bc.apply(rhs) if self.operators[1] is not None: # we have a user-supplied preconditioner solver.set_operators(self.data, self.operators[1]) solver.solve(backend.down_cast(x.vector()), backend.down_cast(rhs)) else: solver.solve(self.data, backend.down_cast(x.vector()), backend.down_cast(rhs)) timer.stop() return adjlinalg.Vector(x)
def derivative(self, m_array=None, taylor_test=False, seed=0.001, forget=True, project=False): ''' An implementation of the reduced functional derivative evaluation that accepts the controls as an array of scalars. If no control values are given, the result is derivative at the last forward run. If taylor_test = True, the derivative is automatically verified by the Taylor remainder convergence test. The perturbation direction is random and the perturbation size can be controlled with the seed argument. ''' # In the case that the control values have changed since the last forward run, # we first need to rerun the forward model with the new controls to have the # correct forward solutions m = [p.data() for p in self.controls] if m_array is not None and (m_array != self.get_global(m)).any(): info_red("Rerunning forward model before computing derivative") self(m_array) dJdm = self.__base_derivative__(forget=forget, project=project) if project: dJdm_global = self.get_global(dJdm) else: dJdm_global = get_global(dJdm) # Perform the gradient test if taylor_test: minconv = utils.test_gradient_array(self.__call__, self.scale * dJdm_global, m_array, seed = seed) if minconv < 1.9: raise RuntimeWarning, "A gradient test failed during execution." else: info("Gradient test successful.") self(m_array) return dJdm_global
def __init__(self, J, m, warn=True): self.J = J self.enlisted_controls = enlist(m) self.m = ListControl(self.enlisted_controls) if warn: backend.info_red("Warning: Hessian computation is still experimental and is known to not work for some problems. Please Taylor test thoroughly.")
def get_value(param, value): if value is not None: return value else: try: return param.data() except libadjoint.exceptions.LibadjointErrorNeedValue: info_red("Do you need to pass forget=False to compute_gradient?") raise
def hessian(self, m_dot, project=False): """ Evaluates the Hessian action at the most recently evaluated control value in direction m_dot. Args: m_dot: The direction in control space in which to compute the Hessian. Must be of the same type as the Control (e.g. Function, Constant or lists of latter). project (Optional[bool]): If True, the returned value will be the L2 Riesz representer, if False it will be the l2 Riesz representative. The L2 projection requires one additional linear solve. Defaults to False. Returns: The directional second derivative. The returned type is the same as the control type. Note: Hessian evaluations never delete the forward state. """ # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls] + [m_dot]) fnspaces = [p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls] if hash in self._cache["hessian_cache"]: info_green("Got a Hessian cache hit.") return cache_load(self._cache["hessian_cache"][hash], fnspaces) else: info_red("Got a Hessian cache miss") # Compute the Hessian action by solving the second order adjoint equations Hm = self.H(m_dot, project=project) # Apply the scaling factor scaled_Hm = utils.scale(Hm, self.scale) # Call callback control_data = [p.data() for p in self.controls] if self.current_func_value is not None: current_func_value = self.scale * self.current_func_value else: current_func_value = None self.hessian_cb(current_func_value, delist(control_data, list_type=self.controls), m_dot, scaled_Hm) # Cache the result if self.cache is not None: self._cache["hessian_cache"][hash] = cache_store(scaled_Hm, self.cache) return scaled_Hm
def __init__(self, J, m, warn=True): self.J = J self.enlisted_controls = enlist(m) self.m = ListControl(self.enlisted_controls) if warn: backend.info_red( "Warning: Hessian computation is still experimental and is known to not work for some problems. Please Taylor test thoroughly." )
def basic_solve(self, var, b): if isinstance(self.data, IdentityMatrix): x=b.duplicate() x.axpy(1.0, b) if isinstance(x.data, ufl.Form): x = Vector(backend.Function(x.fn_space, backend.assemble(x.data))) else: if var.type in ['ADJ_TLM', 'ADJ_ADJOINT', 'ADJ_SOA']: dirichlet_bcs = [utils.homogenize(bc) for bc in self.bcs if isinstance(bc, backend.DirichletBC)] other_bcs = [bc for bc in self.bcs if not isinstance(bc, backend.DirichletBC)] bcs = dirichlet_bcs + other_bcs else: bcs = self.bcs test = self.test_function() x = Vector(backend.Function(test.function_space())) #print "b.data is a %s in the solution of %s" % (b.data.__class__, var) if b.data is None and not hasattr(b, 'nonlinear_form'): # This means we didn't get any contribution on the RHS of the adjoint system. This could be that the # simulation ran further ahead than when the functional was evaluated, or it could be that the # functional is set up incorrectly. backend.info_red("Warning: got zero RHS for the solve associated with variable %s" % var) elif isinstance(b.data, backend.Function): assembled_lhs = self.assemble_data() [bc.apply(assembled_lhs) for bc in bcs] assembled_rhs = compatibility.assembled_rhs(b) [bc.apply(assembled_rhs) for bc in bcs] wrap_solve(assembled_lhs, x.data.vector(), assembled_rhs, self.solver_parameters) else: if hasattr(b, 'nonlinear_form'): # was a nonlinear solve x = compatibility.assign_function_to_vector(x, b.nonlinear_u, function_space = test.function_space()) F = backend.replace(b.nonlinear_form, {b.nonlinear_u: x.data}) J = backend.replace(b.nonlinear_J, {b.nonlinear_u: x.data}) try: compatibility.solve(F == 0, x.data, b.nonlinear_bcs, J=J, solver_parameters=self.solver_parameters) except RuntimeError as rte: x.data.vector()[:] = float("nan") else: assembled_lhs = self.assemble_data() [bc.apply(assembled_lhs) for bc in bcs] assembled_rhs = wrap_assemble(b.data, test) [bc.apply(assembled_rhs) for bc in bcs] if backend.__name__ == "dolfin": wrap_solve(assembled_lhs, x.data.vector(), assembled_rhs, self.solver_parameters) else: wrap_solve(assembled_lhs, x.data, assembled_rhs, self.solver_parameters) return x
def __init__(self, value, cell=None, name=None): backend.Constant.__init__(self, value, cell) if name is None: name = hash(self) self.adj_name = name if name in constant_values: backend.info_red("Warning: redefing constant with name %s" % name) constant_values[name] = value constant_objects[name] = self
def __call__(self, adjointer, timestep, dependencies, values): functional_value = self._substitute_form(adjointer, timestep, dependencies, values) if functional_value is not None: args = ufl.algorithms.extract_arguments(functional_value) if len(args) > 0: backend.info_red("The form passed into Functional must be rank-0 (a scalar)! You have passed in a rank-%s form." % len(args)) raise libadjoint.exceptions.LibadjointErrorInvalidInputs return backend.assemble(functional_value) else: return 0.0
def __init__(self, J, m, warn=True): self.J = J self.m = m if warn: backend.info_red("Warning: Hessian computation is still experimental and is known to not work for some problems. Please Taylor test thoroughly.") if not isinstance(m, (FunctionControl, ConstantControl)): error_msg = "Sorry, Hessian computation only works for FunctionControl \ and ConstantControl so far." raise libadjoint.exceptions.LibadjointErrorNotImplemented(error_msg) self.update(m)
def __init__(self, timeform, verbose=False, name=None): if isinstance(timeform, ufl.form.Form): if adjglobals.adjointer.adjointer.ntimesteps > 1: backend.info_red("You are using a steady-state functional (without the *dt term) in a time-dependent simulation.\ndolfin-adjoint will assume that you want to evaluate the functional at the end of time.") timeform = timeform*dt[FINISH_TIME] if not timeform.is_functional(): raise Exception, "Your functional must have rank 0, but is has rank >0" self.timeform = timeform self.verbose = verbose self.name = name
def caching_solve(self, var, b): if isinstance(self.data, IdentityMatrix): output = b.duplicate() output.axpy(1.0, b) if isinstance(output.data, ufl.Form): output = Vector(backend.Function(output.fn_space, backend.assemble(output.data))) elif b.data is None: backend.warning("Warning: got zero RHS for the solve associated with variable %s" % var) output = Vector(backend.Function(self.test_function().function_space())) else: dirichlet_bcs = [utils.homogenize(bc) for bc in self.bcs if isinstance(bc, backend.DirichletBC)] other_bcs = [bc for bc in self.bcs if not isinstance(bc, backend.DirichletBC)] bcs = dirichlet_bcs + other_bcs output = Vector(backend.Function(self.test_function().function_space())) #print "b.data is a %s in the solution of %s" % (b.data.__class__, var) if backend.parameters["adjoint"]["symmetric_bcs"] and backend.__version__ > '1.2.0': assembler = backend.SystemAssembler(self.data, b.data, bcs) assembled_rhs = backend.Vector() assembler.assemble(assembled_rhs) elif isinstance(b.data, ufl.Form): assembled_rhs = wrap_assemble(b.data, self.test_function()) else: if backend.__name__ == "dolfin": assembled_rhs = b.data.vector() else: assembled_rhs = b.data [bc.apply(assembled_rhs) for bc in bcs] if not var in caching.lu_solvers: if backend.parameters["adjoint"]["debug_cache"]: backend.info_red("Got a cache miss for %s" % var) if backend.parameters["adjoint"]["symmetric_bcs"] and backend.__version__ > '1.2.0': assembled_lhs = backend.Matrix() assembler.assemble(assembled_lhs) else: assembled_lhs = self.assemble_data() [bc.apply(assembled_lhs) for bc in bcs] caching.lu_solvers[var] = compatibility.LUSolver(assembled_lhs, "mumps") caching.lu_solvers[var].parameters["reuse_factorization"] = True else: if backend.parameters["adjoint"]["debug_cache"]: backend.info_green("Got a cache hit for %s" % var) caching.lu_solvers[var].solve(output.data.vector(), assembled_rhs) return output
def derivative(self, forget=True, project=False): ''' Evaluates the derivative of the reduced functional for the most recently evaluated control value. ''' # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls]) fnspaces = [p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls] if hash in self._cache["derivative_cache"]: info_green("Got a derivative cache hit.") return cache_load(self._cache["derivative_cache"][hash], fnspaces) # Call callback values = [p.data() for p in self.controls] self.derivative_cb_pre(delist(values, list_type=self.controls)) # Compute the gradient by solving the adjoint equations dfunc_value = drivers.compute_gradient(self.functional, self.controls, forget=forget, project=project) dfunc_value = enlist(dfunc_value) # Reset the checkpointing state in dolfin-adjoint adjointer.reset_revolve() # Apply the scaling factor scaled_dfunc_value = [utils.scale(df, self.scale) for df in list(dfunc_value)] # Call callback # We might have forgotten the control values already, # in which case we can only return Nones values = [] for c in self.controls: try: values.append(p.data()) except libadjoint.exceptions.LibadjointErrorNeedValue: values.append(None) if self.current_func_value is not None: self.derivative_cb_post(self.scale * self.current_func_value, delist(scaled_dfunc_value, list_type=self.controls), delist(values, list_type=self.controls)) # Cache the result if self.cache is not None: info_red("Got a derivative cache miss") self._cache["derivative_cache"][hash] = cache_store(scaled_dfunc_value, self.cache) return scaled_dfunc_value
def assemble_data(self): assert not isinstance(self.data, IdentityMatrix) if not self.cache: return backend.assemble(self.data) else: if self.data in caching.assembled_adj_forms: if backend.parameters["adjoint"]["debug_cache"]: backend.info_green("Got an assembly cache hit") return caching.assembled_adj_forms[self.data] else: if backend.parameters["adjoint"]["debug_cache"]: backend.info_red("Got an assembly cache miss") M = backend.assemble(self.data) caching.assembled_adj_forms[self.data] = M return M
def derivative_action(self, dependencies, values, variable, contraction_vector, hermitian): idx = dependencies.index(variable) # If you want to apply boundary conditions symmetrically in the adjoint # -- and you often do -- # then we need to have a UFL representation of all the terms in the adjoint equation. # However! # Since UFL cannot represent the identity map, # we need to find an f such that when # assemble(inner(f, v)*dx) # we get the contraction_vector.data back. # This involves inverting a mass matrix. if backend.parameters["adjoint"][ "symmetric_bcs"] and backend.__version__ <= '1.2.0': backend.info_red( "Warning: symmetric BC application requested but unavailable in dolfin <= 1.2.0." ) if backend.parameters["adjoint"][ "symmetric_bcs"] and backend.__version__ > '1.2.0': V = contraction_vector.data.function_space() v = backend.TestFunction(V) if str(V) not in adjglobals.fsp_lu: u = backend.TrialFunction(V) A = backend.assemble(backend.inner(u, v) * backend.dx) solver = "mumps" if "mumps" in backend.lu_solver_methods( ).keys() else "default" lusolver = backend.LUSolver(A, solver) lusolver.parameters["symmetric"] = True lusolver.parameters["reuse_factorization"] = True adjglobals.fsp_lu[str(V)] = lusolver else: lusolver = adjglobals.fsp_lu[str(V)] riesz = backend.Function(V) lusolver.solve( riesz.vector(), self.weights[idx] * contraction_vector.data.vector()) out = (backend.inner(riesz, v) * backend.dx) else: out = backend.Function(self.fn_space) out.assign(self.weights[idx] * contraction_vector.data) return adjlinalg.Vector(out)
def __init__(self, data, zero=False, fn_space=None): self.data=data if not (self.data is None or isinstance(self.data, backend.Function) or isinstance(self.data, ufl.Form)): backend.info_red("Got " + str(self.data.__class__) + " as input to the Vector() class. Don't know how to handle that.") raise AssertionError # self.zero is true if we can prove that the vector is zero. if data is None: self.zero=True else: self.zero=zero if fn_space is not None: self.fn_space = fn_space
def __init__(self, timeform, verbose=False, name=None): if isinstance(timeform, ufl.form.Form): if adjglobals.adjointer.adjointer.ntimesteps > 1: backend.info_red( "You are using a steady-state functional (without the *dt term) in a time-dependent simulation.\ndolfin-adjoint will assume that you want to evaluate the functional at the end of time." ) timeform = timeform * dt[FINISH_TIME] if not timeform.is_functional(): raise Exception( "Your functional must have rank 0, but is has rank >0") self.timeform = timeform self.verbose = verbose self.name = name
def derivative(self, adjointer, variable, dependencies, values): functional_value = None for timestep in self._derivative_timesteps(adjointer, variable): functional_value = _add(functional_value, self._substitute_form(adjointer, timestep, dependencies, values)) if functional_value is None: backend.info_red("Your functional is supposed to depend on %s, but does not?" % variable) raise libadjoint.exceptions.LibadjointErrorInvalidInputs d = backend.derivative(functional_value, values[dependencies.index(variable)].data) d = ufl.algorithms.expand_derivatives(d) if len(d.integrals()) == 0: raise SystemExit, "This isn't supposed to happen -- your functional is supposed to depend on %s" % variable return adjlinalg.Vector(d)
def hessian(self, m_dot, project=False): ''' Evaluates the Hessian action in direction m_dot. ''' assert(len(self.controls) == 1) # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls] + [m_dot]) fnspaces = [p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls] if hash in self._cache["hessian_cache"]: info_green("Got a Hessian cache hit.") return cache_load(self._cache["hessian_cache"][hash], fnspaces) else: info_red("Got a Hessian cache miss") # Compute the Hessian action by solving the second order adjoint equations if isinstance(m_dot, list): assert len(m_dot) == 1 Hm = self.H(m_dot[0], project=project) else: Hm = self.H(m_dot, project=project) # Apply the scaling factor scaled_Hm = [utils.scale(Hm, self.scale)] # Call callback control_data = [p.data() for p in self.controls] if self.current_func_value is not None: current_func_value = self.scale * self.current_func_value else: current_func_value = None self.hessian_cb(current_func_value, delist(control_data, list_type=self.controls), m_dot, scaled_Hm[0]) # Cache the result if self.cache is not None: self._cache["hessian_cache"][hash] = cache_store(scaled_Hm, self.cache) return scaled_Hm
def __call__(self, adjointer, timestep, dependencies, values): functional_value = self._substitute_form(adjointer, timestep, dependencies, values) if functional_value is not None: args = ufl.algorithms.extract_arguments(functional_value) if len(args) > 0: backend.info_red( "The form passed into Functional must be rank-0 (a scalar)! You have passed in a rank-%s form." % len(args)) raise libadjoint.exceptions.LibadjointErrorInvalidInputs from .utils import _has_multimesh if _has_multimesh(functional_value): return backend.assemble_multimesh(functional_value) else: return backend.assemble(functional_value) else: return 0.0
def derivative(self, forget=True, project=False): ''' Evaluates the derivative of the reduced functional for the most recently evaluated control value. ''' # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls]) fnspaces = [p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls] if hash in self._cache["derivative_cache"]: info_green("Got a derivative cache hit.") return cache_load(self._cache["derivative_cache"][hash], fnspaces) # Compute the gradient by solving the adjoint equations dfunc_value = drivers.compute_gradient(self.functional, self.controls, forget=forget, project=project) dfunc_value = enlist(dfunc_value) # Reset the checkpointing state in dolfin-adjoint adjointer.reset_revolve() # Apply the scaling factor scaled_dfunc_value = [utils.scale(df, self.scale) for df in list(dfunc_value)] # Call the user-specific callback routine if self.derivative_cb: if self.current_func_value is not None: values = [p.data() for p in self.controls] self.derivative_cb(self.scale * self.current_func_value, delist(scaled_dfunc_value, list_type=self.controls), delist(values, list_type=self.controls)) else: info_red("Gradient evaluated without functional evaluation, not calling derivative callback function") # Cache the result if self.cache is not None: info_red("Got a derivative cache miss") self._cache["derivative_cache"][hash] = cache_store(scaled_dfunc_value, self.cache) return scaled_dfunc_value
def replay_dolfin(forget=False, tol=0.0, stop=False): parameters["adjoint"]["stop_annotating"] = True if not backend.parameters["adjoint"]["record_all"]: info_red("Warning: your replay test will be much more effective with dolfin.parameters['adjoint']['record_all'] = True.") success = True for i in range(adjglobals.adjointer.equation_count): (fwd_var, output) = adjglobals.adjointer.get_forward_solution(i) storage = libadjoint.MemoryStorage(output) storage.set_compare(tol=tol) storage.set_overwrite(True) out = adjglobals.adjointer.record_variable(fwd_var, storage) success = success and out if forget: adjglobals.adjointer.forget_forward_equation(i) if not out and stop: return success return success
def derivative(self, m_array=None, forget=True, project=False): ''' An implementation of the reduced functional derivative evaluation that accepts the controls as an array of scalars. If no control values are given, the result is derivative at the lastest forward run. ''' # In the case that the control values have changed since the last forward run, # we first need to rerun the forward model with the new controls to have the # correct forward solutions m = [p.data() for p in self.controls] if m_array is not None and (m_array != self.get_global(m)).any(): info_red("Rerunning forward model before computing derivative") self(m_array) dJdm = self.__base_derivative__(forget=forget, project=project) if project: dJdm_global = self.get_global(dJdm) else: dJdm_global = get_global(dJdm) return dJdm_global
def solve(self, var, b): timer = backend.Timer("Matrix-free solver") solver = backend.PETScKrylovSolver(*self.solver_parameters) solver.parameters.update(self.parameters) x = backend.Function(self.fn_space) if b.data is None: backend.info_red( "Warning: got zero RHS for the solve associated with variable %s" % var) return adjlinalg.Vector(x) if isinstance(b.data, backend.Function): rhs = b.data.vector().copy() else: rhs = backend.assemble(b.data) if var.type in ['ADJ_TLM', 'ADJ_ADJOINT']: self.bcs = [ utils.homogenize(bc) for bc in self.bcs if isinstance(bc, backend.cpp.DirichletBC) ] + [ bc for bc in self.bcs if not isinstance(bc, backend.DirichletBC) ] for bc in self.bcs: bc.apply(rhs) if self.operators[ 1] is not None: # we have a user-supplied preconditioner solver.set_operators(self.data, self.operators[1]) solver.solve(backend.down_cast(x.vector()), backend.down_cast(rhs)) else: solver.solve(self.data, backend.down_cast(x.vector()), backend.down_cast(rhs)) timer.stop() return adjlinalg.Vector(x)
def replay_dolfin(forget=False, tol=0.0, stop=False): with misc.annotations(False): if not backend.parameters["adjoint"]["record_all"]: info_red( "Warning: your replay test will be much more effective with dolfin.parameters['adjoint']['record_all'] = True." ) success = True for i in range(adjglobals.adjointer.equation_count): (fwd_var, output) = adjglobals.adjointer.get_forward_solution(i) storage = libadjoint.MemoryStorage(output) storage.set_compare(tol=tol) storage.set_overwrite(True) out = adjglobals.adjointer.record_variable(fwd_var, storage) success = success and out if forget: adjglobals.adjointer.forget_forward_equation(i) if not out and stop: break return success
def derivative(self, adjointer, variable, dependencies, values): functional_value = None for timestep in self._derivative_timesteps(adjointer, variable): functional_value = _add( functional_value, self._substitute_form(adjointer, timestep, dependencies, values)) if functional_value is None: backend.info_red( "Your functional is supposed to depend on %s, but does not?" % variable) raise libadjoint.exceptions.LibadjointErrorInvalidInputs d = backend.derivative(functional_value, values[dependencies.index(variable)].data) d = ufl.algorithms.expand_derivatives(d) if len(d.integrals()) == 0: raise SystemExit( "This isn't supposed to happen -- your functional is supposed to depend on %s" % variable) return adjlinalg.Vector(d)
def __call__(self, value): """ Evaluates the reduced functional for the given control value. Args: value: The point in control space where to perform the Taylor test. Must be of the same type as the Control (e.g. Function, Constant or lists of latter). Returns: float: The functional value. """ # Make sure we do not annotate # Reset any cached data in dolfin-adjoint adj_reset_cache() #: The control values at which the reduced functional is to be evaluated. value = enlist(value) # Call callback self.eval_cb_pre(delist(value, list_type=self.controls)) # Update the control values on the tape ListControl(self.controls).update(value) # Check if the result is already cached if self.cache: hash = value_hash(value) if hash in self._cache["functional_cache"]: # Found a cache info_green("Got a functional cache hit") return self._cache["functional_cache"][hash] # Replay the annotation and evaluate the functional func_value = 0. for i in range(adjointer.equation_count): (fwd_var, output) = adjointer.get_forward_solution(i) if isinstance(output.data, Function): output.data.rename(str(fwd_var), "a Function from dolfin-adjoint") # Call callback self.replay_cb(fwd_var, output.data, delist(value, list_type=self.controls)) # Check if we checkpointing is active and if yes # record the exact same checkpoint variables as # in the initial forward run if adjointer.get_checkpoint_strategy() != None: if str(fwd_var) in mem_checkpoints: storage = libadjoint.MemoryStorage(output, cs=True) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if str(fwd_var) in disk_checkpoints: storage = libadjoint.MemoryStorage(output) adjointer.record_variable(fwd_var, storage) storage = libadjoint.DiskStorage(output, cs=True) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if not str(fwd_var) in mem_checkpoints and not str( fwd_var) in disk_checkpoints: storage = libadjoint.MemoryStorage(output) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) # No checkpointing, so we record everything else: storage = libadjoint.MemoryStorage(output) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if i == adjointer.timestep_end_equation(fwd_var.timestep): func_value += adjointer.evaluate_functional( self.functional, fwd_var.timestep) if adjointer.get_checkpoint_strategy() != None: adjointer.forget_forward_equation(i) self.current_func_value = func_value # Call callback self.eval_cb_post(self.scale * func_value, delist(value, list_type=self.controls)) if self.cache: # Add result to cache info_red("Got a functional cache miss") self._cache["functional_cache"][hash] = self.scale * func_value return self.scale * func_value
def solve(self, *args, **kwargs): timer = backend.Timer("Matrix-free solver") annotate = True if "annotate" in kwargs: annotate = kwargs["annotate"] del kwargs["annotate"] if len(args) == 3: A = args[0] x = args[1] b = args[2] elif len(args) == 2: A = self.operators[0] x = args[0] b = args[1] if annotate: if not isinstance(A, AdjointKrylovMatrix): try: A = AdjointKrylovMatrix(A.form) except AttributeError: raise libadjoint.exceptions.LibadjointErrorInvalidInputs("Your A has to either be an AdjointKrylovMatrix or have been assembled after backend_adjoint was imported.") if not hasattr(x, 'function'): raise libadjoint.exceptions.LibadjointErrorInvalidInputs("Your x has to come from code like down_cast(my_function.vector()).") if not hasattr(b, 'form'): raise libadjoint.exceptions.LibadjointErrorInvalidInputs("Your b has to have the .form attribute: was it assembled with from backend_adjoint import *?") if not hasattr(A, 'dependencies'): backend.info_red("A has no .dependencies method; assuming no nonlinear dependencies of the matrix-free operator.") coeffs = [] dependencies = [] else: coeffs = [coeff for coeff in A.dependencies() if hasattr(coeff, 'function_space')] dependencies = [adjglobals.adj_variables[coeff] for coeff in coeffs] if len(dependencies) > 0: assert hasattr(A, "set_dependencies"), "Need a set_dependencies method to replace your values, if you have nonlinear dependencies ... " rhs = adjrhs.RHS(b.form) diag_name = hashlib.md5(str(hash(A)) + str(random.random())).hexdigest() diag_block = libadjoint.Block(diag_name, dependencies=dependencies, test_hermitian=backend.parameters["adjoint"]["test_hermitian"], test_derivative=backend.parameters["adjoint"]["test_derivative"]) solving.register_initial_conditions(zip(rhs.coefficients(),rhs.dependencies()) + zip(coeffs, dependencies), linear=False, var=None) var = adjglobals.adj_variables.next(x.function) frozen_expressions_dict = expressions.freeze_dict() frozen_parameters = self.parameters.to_dict() def diag_assembly_cb(dependencies, values, hermitian, coefficient, context): '''This callback must conform to the libadjoint Python block assembly interface. It returns either the form or its transpose, depending on the value of the logical hermitian.''' assert coefficient == 1 expressions.update_expressions(frozen_expressions_dict) if len(dependencies) > 0: A.set_dependencies(dependencies, [val.data for val in values]) if hermitian: A_transpose = A.hermitian() return (MatrixFree(A_transpose, fn_space=x.function.function_space(), bcs=A_transpose.bcs, solver_parameters=self.solver_parameters, operators=transpose_operators(self.operators), parameters=frozen_parameters), adjlinalg.Vector(None, fn_space=x.function.function_space())) else: return (MatrixFree(A, fn_space=x.function.function_space(), bcs=b.bcs, solver_parameters=self.solver_parameters, operators=self.operators, parameters=frozen_parameters), adjlinalg.Vector(None, fn_space=x.function.function_space())) diag_block.assemble = diag_assembly_cb def diag_action_cb(dependencies, values, hermitian, coefficient, input, context): expressions.update_expressions(frozen_expressions_dict) A.set_dependencies(dependencies, [val.data for val in values]) if hermitian: acting_mat = A.transpose() else: acting_mat = A output_fn = backend.Function(input.data.function_space()) acting_mat.mult(input.data.vector(), output_fn.vector()) vec = output_fn.vector() for i in range(len(vec)): vec[i] = coefficient * vec[i] return adjlinalg.Vector(output_fn) diag_block.action = diag_action_cb if len(dependencies) > 0: def derivative_action(dependencies, values, variable, contraction_vector, hermitian, input, coefficient, context): expressions.update_expressions(frozen_expressions_dict) A.set_dependencies(dependencies, [val.data for val in values]) action = A.derivative_action(values[dependencies.index(variable)].data, contraction_vector.data, hermitian, input.data, coefficient) return adjlinalg.Vector(action) diag_block.derivative_action = derivative_action eqn = libadjoint.Equation(var, blocks=[diag_block], targets=[var], rhs=rhs) cs = adjglobals.adjointer.register_equation(eqn) solving.do_checkpoint(cs, var, rhs) out = backend.PETScKrylovSolver.solve(self, *args) if annotate: if backend.parameters["adjoint"]["record_all"]: adjglobals.adjointer.record_variable(var, libadjoint.MemoryStorage(adjlinalg.Vector(x.function))) timer.stop() return out
def solve(self, *args, **kwargs): timer = backend.Timer("Matrix-free solver") annotate = True if "annotate" in kwargs: annotate = kwargs["annotate"] del kwargs["annotate"] if len(args) == 3: A = args[0] x = args[1] b = args[2] elif len(args) == 2: A = self.operators[0] x = args[0] b = args[1] if annotate: if not isinstance(A, AdjointKrylovMatrix): try: A = AdjointKrylovMatrix(A.form) except AttributeError: raise libadjoint.exceptions.LibadjointErrorInvalidInputs( "Your A has to either be an AdjointKrylovMatrix or have been assembled after backend_adjoint was imported." ) if not hasattr(x, 'function'): raise libadjoint.exceptions.LibadjointErrorInvalidInputs( "Your x has to come from code like down_cast(my_function.vector())." ) if not hasattr(b, 'form'): raise libadjoint.exceptions.LibadjointErrorInvalidInputs( "Your b has to have the .form attribute: was it assembled with from backend_adjoint import *?" ) if not hasattr(A, 'dependencies'): backend.info_red( "A has no .dependencies method; assuming no nonlinear dependencies of the matrix-free operator." ) coeffs = [] dependencies = [] else: coeffs = [ coeff for coeff in A.dependencies() if hasattr(coeff, 'function_space') ] dependencies = [ adjglobals.adj_variables[coeff] for coeff in coeffs ] if len(dependencies) > 0: assert hasattr( A, "set_dependencies" ), "Need a set_dependencies method to replace your values, if you have nonlinear dependencies ... " rhs = adjrhs.RHS(b.form) key = '{}{}'.format(hash(A), random.random()).encode('utf8') diag_name = hashlib.md5(key).hexdigest() diag_block = libadjoint.Block( diag_name, dependencies=dependencies, test_hermitian=backend.parameters["adjoint"]["test_hermitian"], test_derivative=backend.parameters["adjoint"] ["test_derivative"]) solving.register_initial_conditions( zip(rhs.coefficients(), rhs.dependencies()) + zip(coeffs, dependencies), linear=False, var=None) var = adjglobals.adj_variables.next(x.function) frozen_expressions_dict = expressions.freeze_dict() frozen_parameters = self.parameters.to_dict() def diag_assembly_cb(dependencies, values, hermitian, coefficient, context): '''This callback must conform to the libadjoint Python block assembly interface. It returns either the form or its transpose, depending on the value of the logical hermitian.''' assert coefficient == 1 expressions.update_expressions(frozen_expressions_dict) if len(dependencies) > 0: A.set_dependencies(dependencies, [val.data for val in values]) if hermitian: A_transpose = A.hermitian() return (MatrixFree( A_transpose, fn_space=x.function.function_space(), bcs=A_transpose.bcs, solver_parameters=self.solver_parameters, operators=transpose_operators(self.operators), parameters=frozen_parameters), adjlinalg.Vector( None, fn_space=x.function.function_space())) else: return (MatrixFree( A, fn_space=x.function.function_space(), bcs=b.bcs, solver_parameters=self.solver_parameters, operators=self.operators, parameters=frozen_parameters), adjlinalg.Vector( None, fn_space=x.function.function_space())) diag_block.assemble = diag_assembly_cb def diag_action_cb(dependencies, values, hermitian, coefficient, input, context): expressions.update_expressions(frozen_expressions_dict) A.set_dependencies(dependencies, [val.data for val in values]) if hermitian: acting_mat = A.transpose() else: acting_mat = A output_fn = backend.Function(input.data.function_space()) acting_mat.mult(input.data.vector(), output_fn.vector()) vec = output_fn.vector() for i in range(len(vec)): vec[i] = coefficient * vec[i] return adjlinalg.Vector(output_fn) diag_block.action = diag_action_cb if len(dependencies) > 0: def derivative_action(dependencies, values, variable, contraction_vector, hermitian, input, coefficient, context): expressions.update_expressions(frozen_expressions_dict) A.set_dependencies(dependencies, [val.data for val in values]) action = A.derivative_action( values[dependencies.index(variable)].data, contraction_vector.data, hermitian, input.data, coefficient) return adjlinalg.Vector(action) diag_block.derivative_action = derivative_action eqn = libadjoint.Equation(var, blocks=[diag_block], targets=[var], rhs=rhs) cs = adjglobals.adjointer.register_equation(eqn) solving.do_checkpoint(cs, var, rhs) out = backend.PETScKrylovSolver.solve(self, *args) if annotate: if backend.parameters["adjoint"]["record_all"]: adjglobals.adjointer.record_variable( var, libadjoint.MemoryStorage(adjlinalg.Vector(x.function))) timer.stop() return out
def __call__(self, value): """ Evaluates the reduced functional for the given control value. Args: value: The point in control space where to perform the Taylor test. Must be of the same type as the Control (e.g. Function, Constant or lists of latter). Returns: float: The functional value. """ # Make sure we do not annotate # Reset any cached data in dolfin-adjoint adj_reset_cache() #: The control values at which the reduced functional is to be evaluated. value = enlist(value) # Call callback self.eval_cb_pre(delist(value, list_type=self.controls)) # Update the control values on the tape ListControl(self.controls).update(value) # Check if the result is already cached if self.cache: hash = value_hash(value) if hash in self._cache["functional_cache"]: # Found a cache info_green("Got a functional cache hit") return self._cache["functional_cache"][hash] # Replay the annotation and evaluate the functional func_value = 0. for i in range(adjointer.equation_count): (fwd_var, output) = adjointer.get_forward_solution(i) if isinstance(output.data, Function): output.data.rename(str(fwd_var), "a Function from dolfin-adjoint") # Call callback self.replay_cb(fwd_var, output.data, delist(value, list_type=self.controls)) # Check if we checkpointing is active and if yes # record the exact same checkpoint variables as # in the initial forward run if adjointer.get_checkpoint_strategy() != None: if str(fwd_var) in mem_checkpoints: storage = libadjoint.MemoryStorage(output, cs = True) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if str(fwd_var) in disk_checkpoints: storage = libadjoint.MemoryStorage(output) adjointer.record_variable(fwd_var, storage) storage = libadjoint.DiskStorage(output, cs = True) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if not str(fwd_var) in mem_checkpoints and not str(fwd_var) in disk_checkpoints: storage = libadjoint.MemoryStorage(output) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) # No checkpointing, so we record everything else: storage = libadjoint.MemoryStorage(output) storage.set_overwrite(True) adjointer.record_variable(fwd_var, storage) if i == adjointer.timestep_end_equation(fwd_var.timestep): func_value += adjointer.evaluate_functional(self.functional, fwd_var.timestep) if adjointer.get_checkpoint_strategy() != None: adjointer.forget_forward_equation(i) self.current_func_value = func_value # Call callback self.eval_cb_post(self.scale * func_value, delist(value, list_type=self.controls)) if self.cache: # Add result to cache info_red("Got a functional cache miss") self._cache["functional_cache"][hash] = self.scale*func_value return self.scale*func_value
def derivative(self, forget=True, project=False): """ Evaluates the derivative of the reduced functional at the most recently evaluated control value. Args: forget (Optional[bool]): Delete the forward state while solving the adjoint equations. If you want to reevaluate derivative at the same point (or the Hessian) you will need to set this to False or None. Defaults to True. project (Optional[bool]): If True, the returned value will be the L2 Riesz representer, if False it will be the l2 Riesz representative. The L2 projection requires one additional linear solve. Defaults to False. Returns: The functional derivative. The returned type is the same as the control type. """ # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls]) fnspaces = [p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls] if hash in self._cache["derivative_cache"]: info_green("Got a derivative cache hit.") return cache_load(self._cache["derivative_cache"][hash], fnspaces) # Call callback values = [p.data() for p in self.controls] self.derivative_cb_pre(delist(values, list_type=self.controls)) # Compute the gradient by solving the adjoint equations dfunc_value = drivers.compute_gradient(self.functional, self.controls, forget=forget, project=project) dfunc_value = enlist(dfunc_value) # Reset the checkpointing state in dolfin-adjoint adjointer.reset_revolve() # Apply the scaling factor scaled_dfunc_value = [utils.scale(df, self.scale) for df in list(dfunc_value)] # Call callback # We might have forgotten the control values already, # in which case we can only return Nones values = [] for c in self.controls: try: values.append(p.data()) except libadjoint.exceptions.LibadjointErrorNeedValue: values.append(None) if self.current_func_value is not None: self.derivative_cb_post(self.scale * self.current_func_value, delist(scaled_dfunc_value, list_type=self.controls), delist(values, list_type=self.controls)) # Cache the result if self.cache is not None: info_red("Got a derivative cache miss") self._cache["derivative_cache"][hash] = cache_store(scaled_dfunc_value, self.cache) return scaled_dfunc_value
def test_initial_condition_tlm(J, dJ, ic, seed=0.01, perturbation_direction=None): '''forward must be a function that takes in the initial condition (ic) as a backend.Function and returns the functional value by running the forward run: func = J(ic) final_adjoint is the tangent linear variable for the solution on which the functional depends (usually the last TLM equation solved). dJ must be the derivative of the functional with respect to its argument, evaluated and assembled at the unperturbed solution (a backend Vector). This function returns the order of convergence of the Taylor series remainder, which should be 2 if the TLM is working correctly.''' import controls # We will compute the gradient of the functional with respect to the initial condition, # and check its correctness with the Taylor remainder convergence test. info_blue("Running Taylor remainder convergence analysis for the tangent linear model... ") adj_var = adjglobals.adj_variables[ic]; adj_var.timestep = 0 if not adjglobals.adjointer.variable_known(adj_var): info_red(str(adj_var) + " not known.") raise libadjoint.exceptions.LibadjointErrorInvalidInputs("Your initial condition must be the /exact same Function/ as the initial condition used in the forward model.") # First run the problem unperturbed ic_copy = backend.Function(ic) f_direct = J(ic_copy) # Randomise the perturbation direction: if perturbation_direction is None: perturbation_direction = backend.Function(ic.function_space()) compatibility.randomise(perturbation_direction) # Run the forward problem for various perturbed initial conditions functional_values = [] perturbations = [] for perturbation_size in [seed/(2**i) for i in range(5)]: perturbation = backend.Function(perturbation_direction) vec = perturbation.vector() vec *= perturbation_size perturbations.append(perturbation) perturbed_ic = backend.Function(ic) vec = perturbed_ic.vector() vec += perturbation.vector() functional_values.append(J(perturbed_ic)) # First-order Taylor remainders (not using adjoint) no_gradient = [abs(perturbed_f - f_direct) for perturbed_f in functional_values] info("Taylor remainder without tangent linear information: " + str(no_gradient)) info("Convergence orders for Taylor remainder without tangent linear information (should all be 1): " + str(convergence_order(no_gradient))) with_gradient = [] for i in range(len(perturbations)): param = controls.FunctionControl(ic, perturbations[i]) final_tlm = tlm_dolfin(param, forget=False).data remainder = abs(functional_values[i] - f_direct - final_tlm.vector().inner(dJ)) with_gradient.append(remainder) info("Taylor remainder with tangent linear information: " + str(with_gradient)) info("Convergence orders for Taylor remainder with tangent linear information (should all be 2): " + str(convergence_order(with_gradient))) return min(convergence_order(with_gradient))
def derivative(self, forget=True, project=False): """ Evaluates the derivative of the reduced functional at the most recently evaluated control value. Args: forget (Optional[bool]): Delete the forward state while solving the adjoint equations. If you want to reevaluate derivative at the same point (or the Hessian) you will need to set this to False or None. Defaults to True. project (Optional[bool]): If True, the returned value will be the L2 Riesz representer, if False it will be the l2 Riesz representative. The L2 projection requires one additional linear solve. Defaults to False. Returns: The functional derivative. The returned type is the same as the control type. """ # Check if we have the gradient already in the cash. # If so, return the cached value if self.cache is not None: hash = value_hash([x.data() for x in self.controls]) fnspaces = [ p.data().function_space() if isinstance(p.data(), Function) else None for p in self.controls ] if hash in self._cache["derivative_cache"]: info_green("Got a derivative cache hit.") return cache_load(self._cache["derivative_cache"][hash], fnspaces) # Call callback values = [p.data() for p in self.controls] self.derivative_cb_pre(delist(values, list_type=self.controls)) # Compute the gradient by solving the adjoint equations dfunc_value = drivers.compute_gradient(self.functional, self.controls, forget=forget, project=project) dfunc_value = enlist(dfunc_value) # Reset the checkpointing state in dolfin-adjoint adjointer.reset_revolve() # Apply the scaling factor scaled_dfunc_value = [ utils.scale(df, self.scale) for df in list(dfunc_value) ] # Call callback # We might have forgotten the control values already, # in which case we can only return Nones values = [] for p in self.controls: try: values.append(p.data()) except libadjoint.exceptions.LibadjointErrorNeedValue: values.append(None) if self.current_func_value is not None: self.derivative_cb_post( self.scale * self.current_func_value, delist(scaled_dfunc_value, list_type=self.controls), delist(values, list_type=self.controls)) # Cache the result if self.cache is not None: info_red("Got a derivative cache miss") self._cache["derivative_cache"][hash] = cache_store( scaled_dfunc_value, self.cache) return scaled_dfunc_value
def test_initial_condition_tlm(J, dJ, ic, seed=0.01, perturbation_direction=None): '''forward must be a function that takes in the initial condition (ic) as a backend.Function and returns the functional value by running the forward run: func = J(ic) final_adjoint is the tangent linear variable for the solution on which the functional depends (usually the last TLM equation solved). dJ must be the derivative of the functional with respect to its argument, evaluated and assembled at the unperturbed solution (a backend Vector). This function returns the order of convergence of the Taylor series remainder, which should be 2 if the TLM is working correctly.''' # We will compute the gradient of the functional with respect to the initial condition, # and check its correctness with the Taylor remainder convergence test. info_blue("Running Taylor remainder convergence analysis for the tangent linear model... ") import controls adj_var = adjglobals.adj_variables[ic]; adj_var.timestep = 0 if not adjglobals.adjointer.variable_known(adj_var): info_red(str(adj_var) + " not known.") raise libadjoint.exceptions.LibadjointErrorInvalidInputs("Your initial condition must be the /exact same Function/ as the initial condition used in the forward model.") # First run the problem unperturbed ic_copy = backend.Function(ic) f_direct = J(ic_copy) # Randomise the perturbation direction: if perturbation_direction is None: perturbation_direction = backend.Function(ic.function_space()) compatibility.randomise(perturbation_direction) # Run the forward problem for various perturbed initial conditions functional_values = [] perturbations = [] for perturbation_size in [seed/(2**i) for i in range(5)]: perturbation = backend.Function(perturbation_direction) vec = perturbation.vector() vec *= perturbation_size perturbations.append(perturbation) perturbed_ic = backend.Function(ic) vec = perturbed_ic.vector() vec += perturbation.vector() functional_values.append(J(perturbed_ic)) # First-order Taylor remainders (not using adjoint) no_gradient = [abs(perturbed_f - f_direct) for perturbed_f in functional_values] info("Taylor remainder without tangent linear information: " + str(no_gradient)) info("Convergence orders for Taylor remainder without tangent linear information (should all be 1): " + str(convergence_order(no_gradient))) with_gradient = [] for i in range(len(perturbations)): param = controls.FunctionControl(ic, perturbations[i]) final_tlm = tlm_dolfin(param, forget=False).data remainder = abs(functional_values[i] - f_direct - final_tlm.vector().inner(dJ)) with_gradient.append(remainder) info("Taylor remainder with tangent linear information: " + str(with_gradient)) info("Convergence orders for Taylor remainder with tangent linear information (should all be 2): " + str(convergence_order(with_gradient))) return min(convergence_order(with_gradient))