Beispiel #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)
Beispiel #2
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)
Beispiel #3
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
Beispiel #4
0
    def prepare_evaluate_adj(self, inputs, adj_inputs, relevant_dependencies):
        fwd_block_variable = self.get_outputs()[0]
        u = fwd_block_variable.output

        dJdu = adj_inputs[0]

        F_form = self._create_F_form()

        dFdu = backend.derivative(F_form, fwd_block_variable.saved_output,
                                  backend.TrialFunction(u.function_space()))
        dFdu_form = backend.adjoint(dFdu)
        dJdu = dJdu.copy()

        compute_bdy = self._should_compute_boundary_adjoint(
            relevant_dependencies)
        adj_sol, adj_sol_bdy = self._assemble_and_solve_adj_eq(
            dFdu_form, dJdu, compute_bdy)
        self.adj_sol = adj_sol
        if self.adj_cb is not None:
            self.adj_cb(adj_sol)
        if self.adj_bdy_cb is not None and compute_bdy:
            self.adj_bdy_cb(adj_sol_bdy)

        r = {}
        r["form"] = F_form
        r["adj_sol"] = adj_sol
        r["adj_sol_bdy"] = adj_sol_bdy
        return r
Beispiel #5
0
    def derivative_action(self, dependencies, values, variable,
                          contraction_vector, hermitian):

        if contraction_vector.data is None:
            return adjlinalg.Vector(None)

        if isinstance(self.form, ufl.form.Form):
            # Find the dolfin Function corresponding to variable.
            dolfin_variable = values[dependencies.index(variable)].data

            dolfin_dependencies = [
                dep for dep in _extract_function_coeffs(self.form)
            ]

            dolfin_values = [val.data for val in values]

            current_form = backend.replace(
                self.form, dict(zip(dolfin_dependencies, dolfin_values)))
            trial = backend.TrialFunction(dolfin_variable.function_space())

            d_rhs = backend.derivative(current_form, dolfin_variable, trial)

            if hermitian:
                action = backend.action(backend.adjoint(d_rhs),
                                        contraction_vector.data)
            else:
                action = backend.action(d_rhs, contraction_vector.data)

            return adjlinalg.Vector(action)
        else:
            # RHS is a adjlinalg.Vector. Its derivative is therefore zero.
            return adjlinalg.Vector(None)
Beispiel #6
0
    def evaluate_adj_component(self,
                               inputs,
                               adj_inputs,
                               block_variable,
                               idx,
                               prepared=None):
        if not self.linear and self.func == block_variable.output:
            # We are not able to calculate derivatives wrt initial guess.
            return None
        F_form = prepared["form"]
        adj_sol = prepared["adj_sol"]
        adj_sol_bdy = prepared["adj_sol_bdy"]
        c = block_variable.output
        c_rep = block_variable.saved_output

        if isinstance(c, backend.Function):
            trial_function = backend.TrialFunction(c.function_space())
        elif isinstance(c, backend.Constant):
            mesh = compat.extract_mesh_from_form(F_form)
            trial_function = backend.TrialFunction(c._ad_function_space(mesh))
        elif isinstance(c, compat.ExpressionType):
            mesh = F_form.ufl_domain().ufl_cargo()
            c_fs = c._ad_function_space(mesh)
            trial_function = backend.TrialFunction(c_fs)
        elif isinstance(c, backend.DirichletBC):
            tmp_bc = compat.create_bc(c,
                                      value=extract_subfunction(
                                          adj_sol_bdy, c.function_space()))
            return [tmp_bc]
        elif isinstance(c, compat.MeshType):
            # Using CoordianteDerivative requires us to do action before
            # differentiating, might change in the future.
            F_form_tmp = backend.action(F_form, adj_sol)
            X = backend.SpatialCoordinate(c_rep)
            dFdm = backend.derivative(-F_form_tmp, X)
            dFdm = compat.assemble_adjoint_value(dFdm, **self.assemble_kwargs)
            return dFdm

        dFdm = -backend.derivative(F_form, c_rep, trial_function)
        dFdm = backend.adjoint(dFdm)
        dFdm = dFdm * adj_sol
        dFdm = compat.assemble_adjoint_value(dFdm, **self.assemble_kwargs)
        if isinstance(c, compat.ExpressionType):
            return [[dFdm, c_fs]]
        else:
            return dFdm
Beispiel #7
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
Beispiel #8
0
    def prepare_evaluate_tlm(self, inputs, tlm_inputs, relevant_outputs):
        fwd_block_variable = self.get_outputs()[0]
        u = fwd_block_variable.output

        F_form = self._create_F_form()

        # Obtain dFdu.
        dFdu = backend.derivative(F_form, fwd_block_variable.saved_output,
                                  backend.TrialFunction(u.function_space()))

        return {"form": F_form, "dFdu": dFdu}
Beispiel #9
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)
Beispiel #10
0
    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)
Beispiel #11
0
    def second_derivative_action(self, dependencies, values, inner_variable,
                                 inner_contraction_vector, outer_variable,
                                 hermitian, action_vector):

        if isinstance(self.form, ufl.form.Form):
            # Find the dolfin Function corresponding to variable.
            dolfin_inner_variable = values[dependencies.index(
                inner_variable)].data
            dolfin_outer_variable = values[dependencies.index(
                outer_variable)].data

            dolfin_dependencies = [
                dep for dep in _extract_function_coeffs(self.form)
            ]

            dolfin_values = [val.data for val in values]

            current_form = backend.replace(
                self.form, dict(zip(dolfin_dependencies, dolfin_values)))
            trial = backend.TrialFunction(
                dolfin_outer_variable.function_space())

            d_rhs = backend.derivative(current_form, dolfin_inner_variable,
                                       inner_contraction_vector.data)
            d_rhs = ufl.algorithms.expand_derivatives(d_rhs)
            if len(d_rhs.integrals()) == 0:
                return None

            d_rhs = backend.derivative(d_rhs, dolfin_outer_variable, trial)
            d_rhs = ufl.algorithms.expand_derivatives(d_rhs)

            if len(d_rhs.integrals()) == 0:
                return None

            if hermitian:
                action = backend.action(backend.adjoint(d_rhs),
                                        action_vector.data)
            else:
                action = backend.action(d_rhs, action_vector.data)

            return adjlinalg.Vector(action)
        else:
            # RHS is a adjlinalg.Vector. Its derivative is therefore zero.
            raise libadjoint.exceptions.LibadjointErrorNotImplemented(
                "No derivative method for constant RHS.")
Beispiel #12
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
Beispiel #13
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
Beispiel #14
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)))