Exemple #1
0
    def _ad_convert_type(self, value, options=None):
        options = {} if options is None else options
        riesz_representation = options.get("riesz_representation", "l2")

        if riesz_representation == "l2":
            return create_overloaded_object(
                compat.function_from_vector(self.function_space(),
                                            value,
                                            cls=backend.Function))
        elif riesz_representation == "L2":
            ret = compat.create_function(self.function_space())
            u = backend.TrialFunction(self.function_space())
            v = backend.TestFunction(self.function_space())
            M = backend.assemble(backend.inner(u, v) * backend.dx)
            compat.linalg_solve(M, ret.vector(), value)
            return ret
        elif riesz_representation == "H1":
            ret = compat.create_function(self.function_space())
            u = backend.TrialFunction(self.function_space())
            v = backend.TestFunction(self.function_space())
            M = backend.assemble(
                backend.inner(u, v) * backend.dx +
                backend.inner(backend.grad(u), backend.grad(v)) * backend.dx)
            compat.linalg_solve(M, ret.vector(), value)
            return ret
        elif callable(riesz_representation):
            return riesz_representation(value)
        else:
            raise NotImplementedError("Unknown Riesz representation %s" %
                                      riesz_representation)
Exemple #2
0
    def evaluate_hessian_component(self,
                                   inputs,
                                   hessian_inputs,
                                   adj_inputs,
                                   block_variable,
                                   idx,
                                   relevant_dependencies,
                                   prepared=None):
        form = prepared
        hessian_input = hessian_inputs[0]
        adj_input = adj_inputs[0]

        c1 = block_variable.output
        c1_rep = block_variable.saved_output

        if isinstance(c1, backend.Function):
            dc = backend.TestFunction(c1.function_space())
        elif isinstance(c1, compat.ExpressionType):
            mesh = form.ufl_domain().ufl_cargo()
            W = c1._ad_function_space(mesh)
            dc = backend.TestFunction(W)
        elif isinstance(c1, backend.Constant):
            mesh = compat.extract_mesh_from_form(form)
            dc = backend.TestFunction(c1._ad_function_space(mesh))
        elif isinstance(c1, compat.MeshType):
            pass
        else:
            return None

        if isinstance(c1, compat.MeshType):
            X = backend.SpatialCoordinate(c1)
            dform = backend.derivative(form, X)
        else:
            dform = backend.derivative(form, c1_rep, dc)
        hessian_outputs = hessian_input * compat.assemble_adjoint_value(dform)

        for other_idx, bv in relevant_dependencies:
            c2_rep = bv.saved_output
            tlm_input = bv.tlm_value

            if tlm_input is None:
                continue

            if isinstance(c2_rep, compat.MeshType):
                X = backend.SpatialCoordinate(c2_rep)
                ddform = backend.derivative(dform, X, tlm_input)
            else:
                ddform = backend.derivative(dform, c2_rep, tlm_input)
            hessian_outputs += adj_input * compat.assemble_adjoint_value(
                ddform)

        if isinstance(c1, compat.ExpressionType):
            return [(hessian_outputs, W)]
        else:
            return hessian_outputs
Exemple #3
0
def compute_gst(ic, final, nsv, ic_norm="mass", final_norm="mass", which=1):
    '''This function computes the generalised stability analysis of a simulation.
    Generalised stability theory computes the perturbations to a field (such as an
    initial condition, forcing term, etc.) that /grow the most/ over the finite
    time window of the simulation. For more details, see the mathematical documentation
    on `the website <http://dolfin-adjoint.org>`_.

    - :py:data:`ic` -- the input of the propagator
    - :py:data:`final` -- the output of the propagator
    - :py:data:`nsv` -- the number of optimal perturbations to compute
    - :py:data:`ic_norm` -- a symmetric positive-definite bilinear form that defines the norm on the input space
    - :py:data:`final_norm` -- a symmetric positive-definite bilinear form that defines the norm on the output space
    - :py:data:`which` -- which singular vectors to compute. Use e.g. slepc4py.SLEPc.EPS.Which.LARGEST_REAL

    You can supply :py:data:`"mass"` for :py:data:`ic_norm` and :py:data:`final_norm` to use the (default) mass matrices associated
    with these spaces.

    For example:

    .. code-block:: python

      gst = compute_gst("State", "State", nsv=10)
      for i in range(gst.ncv): # number of converged vectors
        (sigma, u, v) = gst.get_gst(i, return_vectors=True)
    '''

    ic_var = adjglobals.adj_variables[ic]
    ic_var.c_object.timestep = 0
    ic_var.c_object.iteration = 0
    final_var = adjglobals.adj_variables[final]

    if final_norm == "mass":
        final_value = adjglobals.adjointer.get_variable_value(final_var).data
        final_fnsp = final_value.function_space()
        u = backend.TrialFunction(final_fnsp)
        v = backend.TestFunction(final_fnsp)
        final_mass = backend.inner(u, v) * backend.dx
        final_norm = adjlinalg.Matrix(final_mass)
    elif final_norm is not None:
        final_norm = adjlinalg.Matrix(final_norm)

    if ic_norm == "mass":
        ic_value = adjglobals.adjointer.get_variable_value(ic_var).data
        ic_fnsp = ic_value.function_space()
        u = backend.TrialFunction(ic_fnsp)
        v = backend.TestFunction(ic_fnsp)
        ic_mass = backend.inner(u, v) * backend.dx
        ic_norm = adjlinalg.Matrix(ic_mass)
    elif ic_norm is not None:
        ic_norm = adjlinalg.Matrix(ic_norm)

    return adjglobals.adjointer.compute_gst(ic_var, ic_norm, final_var,
                                            final_norm, nsv, which)
Exemple #4
0
    def __init__(self, form, control):

        if not isinstance(control.control, backend.Function):
            raise NotImplementedError("Only implemented for Function controls")

        args = ufl.algorithms.extract_arguments(form)
        if len(args) != 0:
            raise ValueError("Must be a rank-zero form, i.e. a functional")

        u = control.control
        self.V = u.function_space()
        # We want to make a copy of the control purely for use
        # in the constraint, so that our writing it isn't
        # bothering anyone else
        self.u = backend_types.Function(self.V)
        self.form = ufl.replace(form, {u: self.u})

        self.trial = backend.TrialFunction(self.V)
        self.dform = backend.derivative(self.form, self.u, self.trial)
        if len(
                ufl.algorithms.extract_arguments(
                    ufl.algorithms.expand_derivatives(self.dform))) == 0:
            raise ValueError("Form must depend on control")

        self.test = backend.TestFunction(self.V)
        self.hess = ufl.algorithms.expand_derivatives(
            backend.derivative(self.dform, self.u, self.test))
        if len(ufl.algorithms.extract_arguments(self.hess)) == 0:
            self.zero_hess = True
        else:
            self.zero_hess = False
Exemple #5
0
def project_test(func):
    if isinstance(func, backend.Function):
        V = func.function_space()
        u = backend.TrialFunction(V)
        v = backend.TestFunction(V)
        M = backend.assemble(backend.inner(u, v) * backend.dx)
        proj = backend.Function(V)
        backend.solve(M, proj.vector(), func.vector())
        return proj
    else:
        return func
Exemple #6
0
    def __init__(self, v, V, output, bcs=[], *args, **kwargs):
        mesh = kwargs.pop("mesh", None)
        if mesh is None:
            mesh = V.mesh()
        dx = backend.dx(mesh)
        w = backend.TestFunction(V)
        Pv = backend.TrialFunction(V)
        a = backend.inner(w, Pv) * dx
        L = backend.inner(w, v) * dx

        super(ProjectBlock, self).__init__(a == L, output, bcs, *args,
                                           **kwargs)
Exemple #7
0
    def evaluate_adj_component(self,
                               inputs,
                               adj_inputs,
                               block_variable,
                               idx,
                               prepared=None):
        form = prepared
        adj_input = adj_inputs[0]
        c = block_variable.output
        c_rep = block_variable.saved_output

        if isinstance(c, compat.ExpressionType):
            # Create a FunctionSpace from self.form and Expression.
            # And then make a TestFunction from this space.
            mesh = self.form.ufl_domain().ufl_cargo()
            V = c._ad_function_space(mesh)
            dc = backend.TestFunction(V)

            dform = backend.derivative(form, c_rep, dc)
            output = compat.assemble_adjoint_value(dform)
            return [[adj_input * output, V]]
        elif isinstance(c, compat.MeshType):
            X = backend.SpatialCoordinate(c_rep)
            dform = backend.derivative(form, X)
            output = compat.assemble_adjoint_value(dform)
            return adj_input * output

        if isinstance(c, backend.Function):
            dc = backend.TestFunction(c.function_space())
        elif isinstance(c, backend.Constant):
            mesh = compat.extract_mesh_from_form(self.form)
            dc = backend.TestFunction(c._ad_function_space(mesh))

        dform = backend.derivative(form, c_rep, dc)
        output = compat.assemble_adjoint_value(dform)
        return adj_input * output
    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)
Exemple #9
0
    def derivative_action(self, dependencies, values, variable,
                          contraction_vector, hermitian):
        if not hermitian:
            fn = Function_split(contraction_vector)[self.idx]
            action = backend.inner(self.test, fn)
        else:
            bigtest = backend.TestFunction(self.function.function_space())
            outfn = backend.Function(self.function.function_space())

            # DOLFIN is a bit annoying when it comes to splits. Actually, it is very annoying.
            # You can't do anything like
            # outfn[idx].vector()[:] = values_I_want_to_assign_to_outfn[idx]
            # or
            # fn = outfn.split()[idx]; fn.vector()[:] = values_I_want_to_assign_to_outfn[idx]
            # for whatever reason
            assert False, "No idea how to assign to a subfunction yet .. "
            assignment.dolfin_assign(outfn, contraction_vector.data)

            action = backend.inner(bigtest, outfn) * backend.dx

        return adjlinalg.Vector(action)
Exemple #10
0
def perturbed_replay(parameter,
                     perturbation,
                     perturbation_scale,
                     observation,
                     perturbation_norm="mass",
                     observation_norm="mass",
                     callback=None,
                     forget=False):
    r"""Perturb the forward run and compute

    .. math::

      \frac{
      \left|\left| \delta \mathrm{observation} \right|\right|
      }{
      \left|\left| \delta \mathrm{input} \right| \right|
      }

    as a function of time.

    :py:data:`parameter` -- an FunctionControl to say what variable should be
                            perturbed (e.g. FunctionControl('InitialConcentration'))
    :py:data:`perturbation` -- a Function to give the perturbation direction (from a GST analysis, for example)
    :py:data:`perturbation_norm` -- a bilinear Form which induces a norm on the space of perturbation inputs
    :py:data:`perturbation_scale` -- how big the norm of the initial perturbation should be
    :py:data:`observation` -- the variable to observe (e.g. 'Concentration')
    :py:data:`observation_norm` -- a bilinear Form which induces a norm on the space of perturbation outputs
    :py:data:`callback` -- a function f(var, perturbed, unperturbed) that the user can supply (e.g. to dump out variables during the perturbed replay)
    """

    if not backend.parameters["adjoint"]["record_all"]:
        info_red(
            "Warning: your replay test will be much more effective with backend.parameters['adjoint']['record_all'] = True."
        )

    assert isinstance(parameter, controls.FunctionControl)

    if perturbation_norm == "mass":
        p_fnsp = perturbation.function_space()
        u = backend.TrialFunction(p_fnsp)
        v = backend.TestFunction(p_fnsp)
        p_mass = backend.inner(u, v) * backend.dx
        perturbation_norm = p_mass

    if not isinstance(perturbation_norm, backend.GenericMatrix):
        perturbation_norm = backend.assemble(perturbation_norm)
    if not isinstance(observation_norm,
                      backend.GenericMatrix) and observation_norm != "mass":
        observation_norm = backend.assemble(observation_norm)

    def compute_norm(perturbation, norm):
        # Need to compute <x, Ax> and then take its sqrt
        # where x is perturbation, A is norm
        try:
            vec = perturbation.vector()
        except:
            vec = perturbation

        Ax = vec.copy()
        norm.mult(vec, Ax)
        xAx = vec.inner(Ax)
        return math.sqrt(xAx)

    growths = []

    for i in range(adjglobals.adjointer.equation_count):
        (fwd_var, output) = adjglobals.adjointer.get_forward_solution(i)

        if fwd_var == parameter.var:  # we've hit the initial condition we want to perturb
            current_norm = compute_norm(perturbation, perturbation_norm)
            output.data.vector()[:] += (perturbation_scale /
                                        current_norm) * perturbation.vector()

        unperturbed = adjglobals.adjointer.get_variable_value(fwd_var).data

        if fwd_var.name == observation:  # we've hit something we want to observe
            # Fetch the unperturbed result from the record

            if observation_norm == "mass":  # we can't do this earlier, because we don't have the observation function space yet
                o_fnsp = output.data.function_space()
                u = backend.TrialFunction(o_fnsp)
                v = backend.TestFunction(o_fnsp)
                o_mass = backend.inner(u, v) * backend.dx
                observation_norm = backend.assemble(o_mass)

            diff = output.data.vector() - unperturbed.vector()
            growths.append(
                compute_norm(diff, observation_norm) /
                perturbation_scale)  # <--- the action line

        if callback is not None:
            callback(fwd_var, output.data, unperturbed)

        storage = libadjoint.MemoryStorage(output)
        storage.set_compare(tol=None)
        storage.set_overwrite(True)
        out = adjglobals.adjointer.record_variable(fwd_var, storage)

        if forget:
            adjglobals.adjointer.forget_forward_equation(i)

    # can happen if we initialised a nonlinear solve with a constant zero guess
    if growths[0] == 0.0:
        return growths[1:]
    else:
        return growths
Exemple #11
0
def project_dolfin(v,
                   V=None,
                   bcs=None,
                   mesh=None,
                   solver_type="lu",
                   preconditioner_type="default",
                   form_compiler_parameters=None,
                   annotate=None,
                   name=None):
    '''The project call performs an equation solve, and so it too must be annotated so that the
    adjoint and tangent linear models may be constructed automatically by libadjoint.

    To disable the annotation of this function, just pass :py:data:`annotate=False`. This is useful in
    cases where the solve is known to be irrelevant or diagnostic for the purposes of the adjoint
    computation (such as projecting fields to other function spaces for the purposes of
    visualisation).'''

    to_annotate = utils.to_annotate(annotate)

    if isinstance(v, backend.Expression) and (annotate is not True):
        to_annotate = False

    if isinstance(v, backend.Constant) and (annotate is not True):
        to_annotate = False

    out = backend.project(v=v,
                          V=V,
                          bcs=bcs,
                          mesh=mesh,
                          solver_type=solver_type,
                          preconditioner_type=preconditioner_type,
                          form_compiler_parameters=form_compiler_parameters)
    out = utils.function_to_da_function(out)

    if name is not None:
        out.adj_name = name
        out.rename(name, "a Function from dolfin-adjoint")

    if to_annotate:
        # reproduce the logic from project. This probably isn't future-safe, but anyway

        if V is None:
            V = backend.fem.projection._extract_function_space(v, mesh)
        if mesh is None:
            mesh = V.mesh()

        # Define variational problem for projection
        w = backend.TestFunction(V)
        Pv = backend.TrialFunction(V)
        a = backend.inner(w, Pv) * backend.dx(domain=mesh)
        L = backend.inner(w, v) * backend.dx(domain=mesh)

        solving.annotate(a == L,
                         out,
                         bcs,
                         solver_parameters={
                             "linear_solver": solver_type,
                             "preconditioner": preconditioner_type,
                             "symmetric": True
                         })

        if backend.parameters["adjoint"]["record_all"]:
            adjglobals.adjointer.record_variable(
                adjglobals.adj_variables[out],
                libadjoint.MemoryStorage(adjlinalg.Vector(out)))

    return out
Exemple #12
0
def annotate_split(bigfn, idx, smallfn, bcs):
    fn_space = smallfn.function_space().collapse()
    test = backend.TestFunction(fn_space)
    trial = backend.TrialFunction(fn_space)
    eq_lhs = backend.inner(test, trial) * backend.dx

    key = "{}split{}{}{}{}".format(eq_lhs, smallfn, bigfn, idx,
                                   random.random()).encode('utf8')
    diag_name = "Split:%s:" % idx + hashlib.md5(key).hexdigest()

    diag_deps = []
    diag_block = libadjoint.Block(
        diag_name,
        dependencies=diag_deps,
        test_hermitian=backend.parameters["adjoint"]["test_hermitian"],
        test_derivative=backend.parameters["adjoint"]["test_derivative"])

    solving.register_initial_conditions(
        [(bigfn, adjglobals.adj_variables[bigfn])], linear=True, var=None)

    var = adjglobals.adj_variables.next(smallfn)
    frozen_expressions_dict = expressions.freeze_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)
        value_coeffs = [v.data for v in values]
        eq_l = eq_lhs

        if hermitian:
            adjoint_bcs = [
                utils.homogenize(bc)
                for bc in bcs if isinstance(bc, backend.DirichletBC)
            ] + [bc for bc in bcs if not isinstance(bc, backend.DirichletBC)]
            if len(adjoint_bcs) == 0: adjoint_bcs = None
            return (adjlinalg.Matrix(backend.adjoint(eq_l), bcs=adjoint_bcs),
                    adjlinalg.Vector(None, fn_space=fn_space))
        else:
            return (adjlinalg.Matrix(eq_l, bcs=bcs),
                    adjlinalg.Vector(None, fn_space=fn_space))

    diag_block.assemble = diag_assembly_cb

    rhs = SplitRHS(test, bigfn, idx)

    eqn = libadjoint.Equation(var, blocks=[diag_block], targets=[var], rhs=rhs)

    cs = adjglobals.adjointer.register_equation(eqn)
    solving.do_checkpoint(cs, var, rhs)

    if backend.parameters["adjoint"]["fussy_replay"]:
        mass = eq_lhs
        smallfn_massed = backend.Function(fn_space)
        backend.solve(mass == backend.action(mass, smallfn), smallfn_massed)
        assert False, "No idea how to assign to a subfunction yet .. "
        #assignment.dolfin_assign(bigfn, smallfn_massed)

    if backend.parameters["adjoint"]["record_all"]:
        smallfn_record = backend.Function(fn_space)
        assignment.dolfin_assign(smallfn_record, smallfn)
        adjglobals.adjointer.record_variable(
            var, libadjoint.MemoryStorage(adjlinalg.Vector(smallfn_record)))
Exemple #13
0
    def evaluate_hessian_component(self,
                                   inputs,
                                   hessian_inputs,
                                   adj_inputs,
                                   block_variable,
                                   idx,
                                   relevant_dependencies,
                                   prepared=None):
        c = block_variable.output
        if c == self.func and not self.linear:
            return None

        adj_sol2 = prepared["adj_sol2"]
        adj_sol2_bdy = prepared["adj_sol2_bdy"]
        F_form = prepared["form"]
        adj_sol = prepared["adj_sol"]
        fwd_block_variable = self.get_outputs()[0]
        tlm_output = fwd_block_variable.tlm_value

        c_rep = block_variable.saved_output

        # If m = DirichletBC then d^2F(u,m)/dm^2 = 0 and d^2F(u,m)/dudm = 0,
        # so we only have the term dF(u,m)/dm * adj_sol2
        if isinstance(c, backend.DirichletBC):
            tmp_bc = compat.create_bc(c,
                                      value=extract_subfunction(
                                          adj_sol2_bdy, c.function_space()))
            return [tmp_bc]

        if isinstance(c_rep, backend.Constant):
            mesh = compat.extract_mesh_from_form(F_form)
            W = c._ad_function_space(mesh)
        elif isinstance(c, compat.ExpressionType):
            mesh = F_form.ufl_domain().ufl_cargo()
            W = c._ad_function_space(mesh)
        elif isinstance(c, compat.MeshType):
            X = backend.SpatialCoordinate(c)
            element = X.ufl_domain().ufl_coordinate_element()
            W = backend.FunctionSpace(c, element)
        else:
            W = c.function_space()

        dc = backend.TestFunction(W)
        form_adj = backend.action(F_form, adj_sol)
        form_adj2 = backend.action(F_form, adj_sol2)
        if isinstance(c, compat.MeshType):
            dFdm_adj = backend.derivative(form_adj, X, dc)
            dFdm_adj2 = backend.derivative(form_adj2, X, dc)
        else:
            dFdm_adj = backend.derivative(form_adj, c_rep, dc)
            dFdm_adj2 = backend.derivative(form_adj2, c_rep, dc)

        # TODO: Old comment claims this might break on split. Confirm if true or not.
        d2Fdudm = ufl.algorithms.expand_derivatives(
            backend.derivative(dFdm_adj, fwd_block_variable.saved_output,
                               tlm_output))

        hessian_output = 0

        # We need to add terms from every other dependency
        # i.e. the terms d^2F/dm_1dm_2
        for _, bv in relevant_dependencies:
            c2 = bv.output
            c2_rep = bv.saved_output

            if isinstance(c2, backend.DirichletBC):
                continue

            tlm_input = bv.tlm_value
            if tlm_input is None:
                continue

            if c2 == self.func and not self.linear:
                continue

            # TODO: If tlm_input is a Sum, this crashes in some instances?
            if isinstance(c2_rep, compat.MeshType):
                X = backend.SpatialCoordinate(c2_rep)
                d2Fdm2 = ufl.algorithms.expand_derivatives(
                    backend.derivative(dFdm_adj, X, tlm_input))
            else:
                d2Fdm2 = ufl.algorithms.expand_derivatives(
                    backend.derivative(dFdm_adj, c2_rep, tlm_input))
            if d2Fdm2.empty():
                continue

            hessian_output -= compat.assemble_adjoint_value(d2Fdm2)

        if not d2Fdudm.empty():
            # FIXME: This can be empty in the multimesh case, ask sebastian
            hessian_output -= compat.assemble_adjoint_value(d2Fdudm)
        hessian_output -= compat.assemble_adjoint_value(dFdm_adj2)

        if isinstance(c, compat.ExpressionType):
            return [(hessian_output, W)]
        else:
            return hessian_output