def test_gradient_array(J, dJdx, x, seed = 0.01, perturbation_direction = None): '''Checks the correctness of the derivative dJ. x must be an array that specifies at which point in the parameter space the gradient is to be checked, and dJdx must be an array containing the gradient. The function J(x) must return the functional value. This function returns the order of convergence of the Taylor series remainder, which should be 2 if the gradient is correct.''' # 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("Running Taylor remainder convergence analysis to check the gradient ... ") # First run the problem unperturbed j_direct = J(x) # Randomise the perturbation direction: if perturbation_direction is None: perturbation_direction = x.copy() compatibility.randomise(perturbation_direction) # Run the forward problem for various perturbed initial conditions functional_values = [] perturbations = [] perturbation_sizes = [seed/(2**i) for i in range(5)] for perturbation_size in perturbation_sizes: perturbation = perturbation_direction.copy() * perturbation_size perturbations.append(perturbation) perturbed_x = x.copy() + perturbation functional_values.append(J(perturbed_x)) # First-order Taylor remainders (not using adjoint) no_gradient = [abs(perturbed_j - j_direct) for perturbed_j in functional_values] info("Absolute functional evaluation differences: %s" % str(no_gradient)) info("Convergence orders for Taylor remainder without adjoint information (should all be 1): %s" % str(convergence_order(no_gradient))) with_gradient = [] for i in range(len(perturbations)): remainder = abs(functional_values[i] - j_direct - numpy.dot(perturbations[i], dJdx)) with_gradient.append(remainder) if min(with_gradient + no_gradient) < 1e-16: info("Warning: The Taylor remainders are close to machine precision (< %s). Try increasing the seed value in case the Taylor remainder test fails." % min(with_gradient + no_gradient)) info("Absolute functional evaluation differences with adjoint: %s" % str(with_gradient)) info("Convergence orders for Taylor remainder with adjoint information (should all be 2): %s" % str(convergence_order(with_gradient))) return min(convergence_order(with_gradient))
def test_initial_condition_adjoint(J, ic, final_adjoint, 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 adjoint associated with the initial condition (usually the last adjoint equation solved). This function returns the order of convergence of the Taylor series remainder, which should be 2 if the adjoint 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 adjoint 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 = [] perturbation_sizes = [seed/(2**i) for i in range(5)] for perturbation_size in perturbation_sizes: 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 adjoint information: " + str(no_gradient)) info("Convergence orders for Taylor remainder without adjoint information (should all be 1): " + str(convergence_order(no_gradient))) adjoint_vector = final_adjoint.vector() with_gradient = [] gradient_fd = [] for i in range(len(perturbations)): gradient_fd.append((functional_values[i] - f_direct)/perturbation_sizes[i]) remainder = abs(functional_values[i] - f_direct - adjoint_vector.inner(perturbations[i].vector())) with_gradient.append(remainder) info("Taylor remainder with adjoint information: " + str(with_gradient)) info("Convergence orders for Taylor remainder with adjoint information (should all be 2): " + str(convergence_order(with_gradient))) info("Gradients (finite differencing): " + str(gradient_fd)) info("Gradient (adjoint): " + str(adjoint_vector.inner(perturbation_direction.vector()))) return min(convergence_order(with_gradient))
def taylor_test(J, m, Jm, dJdm, HJm=None, seed=None, perturbation_direction=None, value=None): '''J must be a function that takes in a parameter value m and returns the value of the functional: func = J(m) Jm is the value of the function J at the parameter m. dJdm is the gradient of J evaluated at m, to be tested for correctness. This function returns the order of convergence of the Taylor series remainder, which should be 2 if the adjoint is working correctly. If HJm is not None, the Taylor test will also attempt to verify the correctness of the Hessian. HJm should be a callable which takes in a direction and returns the Hessian of the functional in that direction (i.e., takes in a vector and returns a vector). In that case, an additional Taylor remainder is computed, which should converge at order 3 if the Hessian is correct.''' info_blue("Running Taylor remainder convergence test ... ") import controls if isinstance(m, list): m = ListControl(m) if isinstance(m, controls.ListControl): if perturbation_direction is None: perturbation_direction = [None] * len(m.controls) if value is None: value = [None] * len(m.controls) return min(taylor_test(J, m[i], Jm, dJdm[i], HJm, seed, perturbation_direction[i], value[i]) for i in range(len(m.controls))) def get_const(val): if isinstance(val, str): return float(constant.constant_values[val]) else: return float(val) 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 # First, compute perturbation sizes. seed_default = 0.01 if seed is None: if isinstance(m, controls.ConstantControl): seed = get_const(m.a) / 5.0 if seed == 0.0: seed = 0.1 elif isinstance(m, controls.FunctionControl): ic = get_value(m, value) if len(ic.vector()) == 1: # our control is in R seed = float(ic) / 5.0 else: seed = seed_default else: seed = seed_default perturbation_sizes = [seed/(2.0**i) for i in range(5)] # Next, compute the perturbation direction. if perturbation_direction is None: if isinstance(m, controls.ConstantControl): perturbation_direction = 1 elif isinstance(m, controls.ConstantControls): perturbation_direction = numpy.array([get_const(x)/5.0 for x in m.v]) elif isinstance(m, controls.FunctionControl): ic = get_value(m, value) perturbation_direction = backend.Function(ic.function_space()) compatibility.randomise(perturbation_direction) else: raise libadjoint.exceptions.LibadjointErrorNotImplemented("Don't know how to compute a perturbation direction") else: if isinstance(m, controls.FunctionControl): ic = get_value(m, value) # So now compute the perturbations: if not isinstance(perturbation_direction, backend.Function): perturbations = [x*perturbation_direction for x in perturbation_sizes] else: perturbations = [] for x in perturbation_sizes: perturbation = backend.Function(perturbation_direction) vec = perturbation.vector() vec *= x perturbations.append(perturbation) # And now the perturbed inputs: if isinstance(m, controls.ConstantControl): pinputs = [backend.Constant(get_const(m.a) + x) for x in perturbations] elif isinstance(m, controls.ConstantControls): a = numpy.array([get_const(x) for x in m.v]) def make_const(arr): return [backend.Constant(x) for x in arr] pinputs = [make_const(a + x) for x in perturbations] elif isinstance(m, controls.FunctionControl): pinputs = [] for x in perturbations: pinput = backend.Function(x) vec = pinput.vector() vec += ic.vector() pinputs.append(pinput) # Issue 34: We must evaluate HJm before we evaluate the tape at the # perturbed controls below. if HJm is not None: HJm_values = [] for perturbation in perturbations: HJmp = HJm(perturbation) HJm_values.append(HJmp) # At last: the common bit! functional_values = [] for pinput in pinputs: Jp = J(pinput) functional_values.append(Jp) # First-order Taylor remainders (not using adjoint) no_gradient = [abs(perturbed_J - Jm) for perturbed_J in functional_values] info("Taylor remainder without gradient information: " + str(no_gradient)) info("Convergence orders for Taylor remainder without gradient information (should all be 1): " + str(convergence_order(no_gradient))) with_gradient = [] for i in range(len(perturbations)): if isinstance(m, controls.ConstantControl) or isinstance(m, controls.ConstantControls): remainder = taylor_remainder_with_gradient(m, Jm, dJdm, functional_values[i], perturbations[i]) else: remainder = taylor_remainder_with_gradient(m, Jm, dJdm, functional_values[i], perturbations[i], ic=ic) with_gradient.append(remainder) if min(with_gradient + no_gradient) < 1e-16: warning("Warning: The Taylor remainders are close to machine precision (< %s). Try increasing the seed value in case the Taylor remainder test fails." % min(with_gradient + no_gradient)) info("Taylor remainder with gradient information: " + str(with_gradient)) info("Convergence orders for Taylor remainder with gradient information (should all be 2): " + str(convergence_order(with_gradient))) if HJm is not None: with_hessian = [] if isinstance(m, controls.ConstantControl): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - float(dJdm)*perturbations[i] - 0.5*perturbations[i]*HJm_values[i]) with_hessian.append(remainder) elif isinstance(m, controls.ConstantControls): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - numpy.dot(dJdm, perturbations[i]) - 0.5*numpy.dot(perturbations[i], HJm_values[i])) with_hessian.append(remainder) elif isinstance(m, controls.FunctionControl): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - dJdm.vector().inner(perturbations[i].vector()) - 0.5*perturbations[i].vector().inner(HJm_values[i].vector())) with_hessian.append(remainder) info("Taylor remainder with Hessian information: " + str(with_hessian)) info("Convergence orders for Taylor remainder with Hessian information (should all be 3): " + str(convergence_order(with_hessian))) return min(convergence_order(with_hessian)) else: return min(convergence_order(with_gradient))
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))
def _taylor_test_single_control(J, m, Jm, dJdm, HJm, seed, perturbation_direction, value): import function # Check inputs if not isinstance(m, libadjoint.Parameter): raise ValueError, "m must be a valid control instance." def get_const(val): if isinstance(val, str): return float(constant.constant_values[val]) else: return float(val) 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 # First, compute perturbation sizes. seed_default = 0.01 if seed is None: if isinstance(m, controls.ConstantControl): seed = get_const(m.a) / 5.0 if seed == 0.0: seed = 0.1 elif isinstance(m, controls.FunctionControl): ic = get_value(m, value) if len(ic.vector()) == 1: # our control is in R seed = float(ic) / 5.0 else: seed = seed_default else: seed = seed_default perturbation_sizes = [seed/(2.0**i) for i in range(5)] # Next, compute the perturbation direction. if perturbation_direction is None: if isinstance(m, controls.ConstantControl): perturbation_direction = 1 elif isinstance(m, controls.ConstantControls): perturbation_direction = numpy.array([get_const(x)/5.0 for x in m.v]) elif isinstance(m, controls.FunctionControl): ic = get_value(m, value) # Check for MultiMeshFunction_space if isinstance(ic.function_space(), compatibility.multi_mesh_function_space_type): perturbation_direction = backend.MultiMeshFunction(ic.function_space()) else: perturbation_direction = function.Function(ic.function_space()) compatibility.randomise(perturbation_direction) else: raise libadjoint.exceptions.LibadjointErrorNotImplemented("Don't know how to compute a perturbation direction") else: if isinstance(m, controls.FunctionControl): ic = get_value(m, value) elif isinstance(m, controls.ConstantControl): perturbation_direction = float(perturbation_direction) # So now compute the perturbations: if not isinstance(perturbation_direction, (backend.Function, backend.MultiMeshFunction)): perturbations = [x*perturbation_direction for x in perturbation_sizes] else: perturbations = [] for x in perturbation_sizes: perturbation = perturbation_direction.copy(deepcopy=True) vec = perturbation.vector() vec *= x perturbations.append(perturbation) # And now the perturbed inputs: if isinstance(m, controls.ConstantControl): pinputs = [backend.Constant(get_const(m.a) + x) for x in perturbations] elif isinstance(m, controls.ConstantControls): a = numpy.array([get_const(x) for x in m.v]) def make_const(arr): return [backend.Constant(x) for x in arr] pinputs = [make_const(a + x) for x in perturbations] elif isinstance(m, controls.FunctionControl): pinputs = [] for x in perturbations: pinput = x.copy(deepcopy=True) vec = pinput.vector() vec += ic.vector() pinputs.append(pinput) # Issue 34: We must evaluate HJm before we evaluate the tape at the # perturbed controls below. if HJm is not None: HJm_values = [] for perturbation in perturbations: HJmp = HJm(perturbation) HJm_values.append(HJmp) # At last: the common bit! functional_values = [] for pinput in pinputs: Jp = J(pinput) functional_values.append(Jp) # First-order Taylor remainders (not using adjoint) no_gradient = [abs(perturbed_J - Jm) for perturbed_J in functional_values] info("Taylor remainder without gradient information: " + str(no_gradient)) info("Convergence orders for Taylor remainder without gradient information (should all be 1): " + str(convergence_order(no_gradient))) with_gradient = [] for i in range(len(perturbations)): if isinstance(m, controls.ConstantControl) or isinstance(m, controls.ConstantControls): remainder = taylor_remainder_with_gradient(m, Jm, dJdm, functional_values[i], perturbations[i]) else: remainder = taylor_remainder_with_gradient(m, Jm, dJdm, functional_values[i], perturbations[i], ic=ic) with_gradient.append(remainder) if min(with_gradient + no_gradient) < 1e-16: warning("Warning: The Taylor remainders are close to machine precision (< %s). Try increasing the seed value in case the Taylor remainder test fails." % min(with_gradient + no_gradient)) info("Taylor remainder with gradient information: " + str(with_gradient)) info("Convergence orders for Taylor remainder with gradient information (should all be 2): " + str(convergence_order(with_gradient))) if HJm is not None: with_hessian = [] if isinstance(m, controls.ConstantControl): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - float(dJdm)*perturbations[i] - 0.5*perturbations[i]*HJm_values[i]) with_hessian.append(remainder) elif isinstance(m, controls.ConstantControls): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - numpy.dot(dJdm, perturbations[i]) - 0.5*numpy.dot(perturbations[i], HJm_values[i])) with_hessian.append(remainder) elif isinstance(m, controls.FunctionControl): for i in range(len(perturbations)): remainder = abs(functional_values[i] - Jm - dJdm.vector().inner(perturbations[i].vector()) - 0.5*perturbations[i].vector().inner(HJm_values[i].vector())) with_hessian.append(remainder) info("Taylor remainder with Hessian information: " + str(with_hessian)) info("Convergence orders for Taylor remainder with Hessian information (should all be 3): " + str(convergence_order(with_hessian))) return min(convergence_order(with_hessian)) else: return min(convergence_order(with_gradient))