def __init__(self, config, scale=1.0, forward_model=sw_model.sw_solve,
                 plot=False, save_functional_values=False):
        ''' If plot is True, the functional values will be automatically saved in a plot.
            scale is ignored if automatic_scaling is active. '''
        # Hide the configuration since changes would break the memoize algorithm.
        self.__config__ = config
        self.scale = scale
        self.automatic_scaling_factor = None
        self.plot = plot
        self.save_functional_values = save_functional_values
        # Caching variables that store which controls the last forward run was performed
        self.last_m = None
        self.last_state = None
        self.in_euclidian_space = False
        if self.__config__.params["dump_period"] > 0:
            self.turbine_file = File(config.params['base_path'] + os.path.sep + "turbines.pvd", "compressed")

            if config.params['output_turbine_power']:
                self.power_file = File(config.params['base_path'] + os.path.sep + "power.pvd", "compressed")

        class Variable:
            name = ""

        class Parameter:
            var = Variable()

            def data(self):
                m = []
                if config.params["turbine_parametrisation"] == "smeared":
                    if len(config.params["turbine_friction"]) == 0:
                        # If the user has not set the turbine friction it is initialised here
                        m = numpy.zeros(config.turbine_function_space.dim())
                    else:
                        m = config.params["turbine_friction"]
                else:
                    if 'turbine_friction' in config.params["controls"]:
                        m += list(config.params['turbine_friction'])
                    if 'turbine_pos' in config.params["controls"]:
                        m += numpy.reshape(config.params['turbine_pos'], -1).tolist()
                return numpy.array(m)

        self.parameter = [Parameter()]

        if plot:
            self.plotter = AnimatedPlot(xlabel="Iteration", ylabel="Functional value")

        def compute_functional(m, return_final_state=False, annotate=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional of interest. '''

            self.last_m = m

            self.update_turbine_cache(m)
            tf = config.turbine_cache.cache["turbine_field"]
            #info_green("Turbine integral: %f ", assemble(tf*dx))
            #info_green("The correct integral should be: %f ",  25.2771) # computed with wolfram alpha using:
            # int 0.17353373* (exp(-1.0/(1-(x/10)**2)) * exp(-1.0/(1-(y/10)**2)) * exp(2)) dx dy, x=-10..10, y=-10..10
            #info_red("relative error: %f", (assemble(tf*dx)-25.2771)/25.2771)

            return compute_functional_from_tf(tf, return_final_state, annotate=annotate)

        def compute_functional_from_tf(tf, return_final_state, annotate=True):
            ''' Takes in the turbine friction field and computes the resulting functional of interest. '''
            adj_reset()
            parameters["adjoint"]["record_all"] = True

            # Get initial conditions
            if config.params["implicit_turbine_thrust_parametrisation"]:
                state = Function(config.function_space_2enriched, name="Current_state")
            elif config.params["turbine_thrust_parametrisation"]:
                state = Function(config.function_space_enriched, name="Current_state")
            else:
                state = Function(config.function_space, name="Current_state")

            if config.params["steady_state"] and config.params["include_time_term"] and self.last_state is not None:
                # Speed up the nonlinear solves by starting the Newton solve with the most recent state solution
                state.assign(self.last_state, annotate=False)
            else:
                ic = config.params['initial_condition']
                state.assign(ic, annotate=False)

            # Solve the shallow water system
            functional = config.functional(config)
            j = forward_model(config, state, functional=functional, turbine_field=tf, annotate=annotate)
            self.last_state = state

            if return_final_state:
                return j, state
            else:
                return j

        def compute_gradient(m, forget=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional gradient. '''
            # If the last forward run was performed with the same parameters, then all recorded values by dolfin-adjoint are still valid for this adjoint run
            # and we do not have to rerun the forward model.
            if numpy.any(m != self.last_m):
                compute_functional(m, annotate=True)

            state = self.last_state
            functional = config.functional(config)

            # Produce power plot
            if config.params['output_turbine_power']:
                if config.params['turbine_thrust_parametrisation'] or config.params["implicit_turbine_thrust_parametrisation"] or "dynamic_turbine_friction" in config.params["controls"]:
                    info_red("Turbine power VTU's is not yet implemented with thrust based turbines parameterisations and dynamic turbine friction control.")
                else:
                    turbines = self.__config__.turbine_cache.cache["turbine_field"]
                    self.power_file << project(functional.power(state, turbines), config.turbine_function_space, annotate=False)

            # The functional depends on the turbine friction function which we do not have on scope here.
            # But dolfin-adjoint only cares about the name, so we can just create a dummy function with the desired name.
            dummy_tf = Function(FunctionSpace(state.function_space().mesh(), "R", 0), name="turbine_friction")

            if config.params['steady_state'] or config.params["functional_final_time_only"]:
                J = Functional(functional.Jt(state, dummy_tf) * dt[FINISH_TIME])

            elif config.params['functional_quadrature_degree'] == 0:
                # Pseudo-redo the time loop to collect the necessary timestep information
                t = config.params["start_time"]
                timesteps = [t]
                while (t < config.params["finish_time"]):
                    t += config.params["dt"]
                    timesteps.append(t)

                if not config.params["include_time_term"]:
                    # Remove the initial condition. I think this is a bug in dolfin-adjoint, since really I expected pop(0) here - but the Taylor tests pass only with pop(1)!
                    timesteps.pop(1)

                # Construct the functional
                J = Functional(sum(functional.Jt(state, dummy_tf) * dt[t] for t in timesteps))

            else:
                if not config.params["include_time_term"]:
                    raise NotImplementedError, "Multi-steady state simulations only work with 'functional_quadrature_degree=0' or 'functional_final_time_only=True'" 
                J = Functional(functional.Jt(state, dummy_tf) * dt)

            if 'dynamic_turbine_friction' in config.params["controls"]:
                parameters = [InitialConditionParameter("turbine_friction_cache_t_%i" % i) for i in range(len(config.params["turbine_friction"]))]

            else:
                parameters = InitialConditionParameter("turbine_friction_cache")

            djdtf = dolfin_adjoint.compute_gradient(J, parameters, forget=forget)
            dolfin.parameters["adjoint"]["stop_annotating"] = False

            # Decide if we need to apply the chain rule to get the gradient of interest
            if config.params['turbine_parametrisation'] == 'smeared':
                # We are looking for the gradient with respect to the friction
                dj = dolfin_adjoint.optimization.get_global(djdtf)

            else:
                # Let J be the functional, m the parameter and u the solution of the PDE equation F(u) = 0.
                # Then we have
                # dJ/dm = (\partial J)/(\partial u) * (d u) / d m + \partial J / \partial m
                #               = adj_state * \partial F / \partial u + \partial J / \partial m
                # In this particular case m = turbine_friction, J = \sum_t(ft)
                dj = []

                if 'turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for tfd in config.turbine_cache.cache["turbine_derivative_friction"]:
                        config.turbine_cache.update(config)
                        dj.append(djdtf.vector().inner(tfd.vector()))

                elif 'dynamic_turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for djdtf_arr, t in zip(djdtf, config.turbine_cache.cache["turbine_derivative_friction"]):
                        for tfd in t:
                            config.turbine_cache.update(config)
                            dj.append(djdtf_arr.vector().inner(tfd.vector()))

                if 'turbine_pos' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine position
                    for d in config.turbine_cache.cache["turbine_derivative_pos"]:
                        for var in ('turbine_pos_x', 'turbine_pos_y'):
                            config.turbine_cache.update(config)
                            tfd = d[var]
                            dj.append(djdtf.vector().inner(tfd.vector()))

                dj = numpy.array(dj)

            return dj

        def compute_hessian_action(m, m_dot):
            if numpy.any(m != self.last_m):
                self.run_adjoint_model_mem(m, forget=False)

            state = self.last_state

            functional = config.functional(config)
            if config.params['steady_state'] or config.params["functional_final_time_only"]:
                J = Functional(functional.Jt(state) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state) * dt)

            H = drivers.hessian(J, InitialConditionParameter("friction"), warn=False)
            m_dot = project(Constant(1), config.turbine_function_space)
            return H(m_dot)

        # For smeared turbine parametrisations we only want to store the 
        # hash of the control values into the pickle datastructure
        hash_keys = (config.params["turbine_parametrisation"] == "smeared")

        self.compute_functional_mem = memoize.MemoizeMutable(compute_functional, hash_keys)
        self.compute_gradient_mem = memoize.MemoizeMutable(compute_gradient, hash_keys)
        self.compute_hessian_action_mem = memoize.MemoizeMutable(compute_hessian_action, hash_keys)
Exemple #2
0
    def __init__(self,
                 config,
                 scale=1.0,
                 forward_model=sw_model.sw_solve,
                 plot=False):
        ''' If plot is True, the functional values will be automatically saved in a plot.
            scale is ignored if automatic_scaling is active. '''
        # Hide the configuration since changes would break the memoize algorithm.
        self.__config__ = config
        self.scale = scale
        self.automatic_scaling_factor = None
        self.plot = plot
        # Caching variables that store which controls the last forward run was performed
        self.last_m = None
        self.last_state = None
        if self.__config__.params["dump_period"] > 0:
            self.turbine_file = File("turbines.pvd", "compressed")

            if config.params['output_turbine_power']:
                self.power_file = File("power.pvd", "compressed")

        class Parameter:
            def data(self):
                m = []
                if config.params["turbine_parametrisation"] == "smooth":
                    m = numpy.zeros(config.turbine_function_space.dim())
                else:
                    if 'turbine_friction' in config.params["controls"]:
                        m += list(config.params['turbine_friction'])
                    if 'turbine_pos' in config.params["controls"]:
                        m += numpy.reshape(config.params['turbine_pos'],
                                           -1).tolist()
                return numpy.array(m)

        self.parameter = [Parameter()]

        if plot:
            self.plotter = AnimatedPlot(xlabel="Iteration",
                                        ylabel="Functional value")

        def compute_functional(m, return_final_state=False):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional of interest. '''

            self.last_m = m

            self.update_turbine_cache(m)
            tf = config.turbine_cache.cache["turbine_field"]
            #info_green("Turbine integral: %f ", assemble(tf*dx))
            #info_green("The correct integral should be: %f ",  25.2771) # computed with wolfram alpha using:
            # int 0.17353373* (exp(-1.0/(1-(x/10)**2)) * exp(-1.0/(1-(y/10)**2)) * exp(2)) dx dy, x=-10..10, y=-10..10
            #info_red("relative error: %f", (assemble(tf*dx)-25.2771)/25.2771)

            return compute_functional_from_tf(tf, return_final_state)

        def compute_functional_from_tf(tf, return_final_state):
            ''' Takes in the turbine friction field and computes the resulting functional of interest. '''
            adj_reset()
            parameters["adjoint"]["record_all"] = True

            # Get initial conditions
            if config.params["implicit_turbine_thrust_parametrisation"]:
                state = Function(config.function_space_2enriched,
                                 name="Current_state")
            elif config.params["turbine_thrust_parametrisation"]:
                state = Function(config.function_space_enriched,
                                 name="Current_state")
            else:
                state = Function(config.function_space, name="Current_state")

            if config.params["steady_state"] and self.last_state != None:
                # Speed up the nonlinear solves by starting the Newton solve with the most recent state solution
                state.assign(self.last_state, annotate=False)
            else:
                ic = config.params['initial_condition']
                state.assign(ic, annotate=False)

            # Solve the shallow water system
            functional = config.functional(config)
            j = forward_model(config,
                              state,
                              functional=functional,
                              turbine_field=tf)
            self.last_state = state

            if return_final_state:
                return j, state
            else:
                return j

        def compute_gradient(m, forget=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional gradient. '''
            myt = Timer("full compute_gradient")
            # If the last forward run was performed with the same parameters, then all recorded values by dolfin-adjoint are still valid for this adjoint run
            # and we do not have to rerun the forward model.
            if numpy.any(m != self.last_m):
                compute_functional(m)

            state = self.last_state
            functional = config.functional(config)

            # Produce power plot
            if config.params['output_turbine_power']:
                if config.params['turbine_thrust_parametrisation'] or config.params[
                        "implicit_turbine_thrust_parametrisation"] or "dynamic_turbine_friction" in config.params[
                            "controls"]:
                    info_red(
                        "Turbine power VTU's is not yet implemented with thrust based turbines parameterisations and dynamic turbine friction control."
                    )
                else:
                    turbines = self.__config__.turbine_cache.cache[
                        "turbine_field"]
                    self.power_file << project(functional.expr(
                        state, turbines),
                                               config.turbine_function_space,
                                               annotate=False)

            # The functional depends on the turbine friction function which we do not have on scope here.
            # But dolfin-adjoint only cares about the name, so we can just create a dummy function with the desired name.
            dummy_tf = Function(FunctionSpace(state.function_space().mesh(),
                                              "R", 0),
                                name="turbine_friction")

            if config.params['steady_state'] or config.params[
                    "functional_final_time_only"]:
                J = Functional(
                    functional.Jt(state, dummy_tf) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state, dummy_tf) * dt)

            if 'dynamic_turbine_friction' in config.params["controls"]:
                parameters = [
                    InitialConditionParameter("turbine_friction_cache_t_%i" %
                                              i)
                    for i in range(len(config.params["turbine_friction"]))
                ]

            else:
                parameters = InitialConditionParameter(
                    "turbine_friction_cache")

            djdtf = dolfin_adjoint.compute_gradient(J,
                                                    parameters,
                                                    forget=forget)
            dolfin.parameters["adjoint"]["stop_annotating"] = False

            # Decide if we need to apply the chain rule to get the gradient of interest
            if config.params['turbine_parametrisation'] == 'smooth':
                # We are looking for the gradient with respect to the friction
                dj = dolfin_adjoint.optimization.get_global(djdtf)

            else:
                # Let J be the functional, m the parameter and u the solution of the PDE equation F(u) = 0.
                # Then we have
                # dJ/dm = (\partial J)/(\partial u) * (d u) / d m + \partial J / \partial m
                #               = adj_state * \partial F / \partial u + \partial J / \partial m
                # In this particular case m = turbine_friction, J = \sum_t(ft)
                dj = []

                if 'turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for tfd in config.turbine_cache.cache[
                            "turbine_derivative_friction"]:
                        config.turbine_cache.update(config)
                        dj.append(djdtf.vector().inner(tfd.vector()))

                elif 'dynamic_turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for djdtf_arr, t in zip(
                            djdtf, config.turbine_cache.
                            cache["turbine_derivative_friction"]):
                        for tfd in t:
                            config.turbine_cache.update(config)
                            dj.append(djdtf_arr.vector().inner(tfd.vector()))

                if 'turbine_pos' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine position
                    for d in config.turbine_cache.cache[
                            "turbine_derivative_pos"]:
                        for var in ('turbine_pos_x', 'turbine_pos_y'):
                            config.turbine_cache.update(config)
                            tfd = d[var]
                            dj.append(djdtf.vector().inner(tfd.vector()))

                dj = numpy.array(dj)

            return dj

        def compute_hessian_action(m, m_dot):
            if numpy.any(m != self.last_m):
                self.run_adjoint_model_mem(m, forget=False)

            state = self.last_state

            functional = config.functional(config)
            if config.params['steady_state'] or config.params[
                    "functional_final_time_only"]:
                J = Functional(functional.Jt(state) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state) * dt)

            H = drivers.hessian(J,
                                InitialConditionParameter("friction"),
                                warn=False)
            m_dot = project(Constant(1), config.turbine_function_space)
            return H(m_dot)

        self.compute_functional_mem = memoize.MemoizeMutable(
            compute_functional)
        self.compute_gradient_mem = memoize.MemoizeMutable(compute_gradient)
        self.compute_hessian_action_mem = memoize.MemoizeMutable(
            compute_hessian_action)
class ReducedFunctionalNumPy(dolfin_adjoint.ReducedFunctionalNumPy):

    def __init__(self, config, scale=1.0, forward_model=sw_model.sw_solve,
                 plot=False, save_functional_values=False):
        ''' If plot is True, the functional values will be automatically saved in a plot.
            scale is ignored if automatic_scaling is active. '''
        # Hide the configuration since changes would break the memoize algorithm.
        self.__config__ = config
        self.scale = scale
        self.automatic_scaling_factor = None
        self.plot = plot
        self.save_functional_values = save_functional_values
        # Caching variables that store which controls the last forward run was performed
        self.last_m = None
        self.last_state = None
        self.in_euclidian_space = False
        if self.__config__.params["dump_period"] > 0:
            self.turbine_file = File(config.params['base_path'] + os.path.sep + "turbines.pvd", "compressed")

            if config.params['output_turbine_power']:
                self.power_file = File(config.params['base_path'] + os.path.sep + "power.pvd", "compressed")

        class Variable:
            name = ""

        class Parameter:
            var = Variable()

            def data(self):
                m = []
                if config.params["turbine_parametrisation"] == "smeared":
                    if len(config.params["turbine_friction"]) == 0:
                        # If the user has not set the turbine friction it is initialised here
                        m = numpy.zeros(config.turbine_function_space.dim())
                    else:
                        m = config.params["turbine_friction"]
                else:
                    if 'turbine_friction' in config.params["controls"]:
                        m += list(config.params['turbine_friction'])
                    if 'turbine_pos' in config.params["controls"]:
                        m += numpy.reshape(config.params['turbine_pos'], -1).tolist()
                return numpy.array(m)

        self.parameter = [Parameter()]

        if plot:
            self.plotter = AnimatedPlot(xlabel="Iteration", ylabel="Functional value")

        def compute_functional(m, return_final_state=False, annotate=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional of interest. '''

            self.last_m = m

            self.update_turbine_cache(m)
            tf = config.turbine_cache.cache["turbine_field"]
            #info_green("Turbine integral: %f ", assemble(tf*dx))
            #info_green("The correct integral should be: %f ",  25.2771) # computed with wolfram alpha using:
            # int 0.17353373* (exp(-1.0/(1-(x/10)**2)) * exp(-1.0/(1-(y/10)**2)) * exp(2)) dx dy, x=-10..10, y=-10..10
            #info_red("relative error: %f", (assemble(tf*dx)-25.2771)/25.2771)

            return compute_functional_from_tf(tf, return_final_state, annotate=annotate)

        def compute_functional_from_tf(tf, return_final_state, annotate=True):
            ''' Takes in the turbine friction field and computes the resulting functional of interest. '''
            adj_reset()
            parameters["adjoint"]["record_all"] = True

            # Get initial conditions
            if config.params["implicit_turbine_thrust_parametrisation"]:
                state = Function(config.function_space_2enriched, name="Current_state")
            elif config.params["turbine_thrust_parametrisation"]:
                state = Function(config.function_space_enriched, name="Current_state")
            else:
                state = Function(config.function_space, name="Current_state")

            if config.params["steady_state"] and config.params["include_time_term"] and self.last_state is not None:
                # Speed up the nonlinear solves by starting the Newton solve with the most recent state solution
                state.assign(self.last_state, annotate=False)
            else:
                ic = config.params['initial_condition']
                state.assign(ic, annotate=False)

            # Solve the shallow water system
            functional = config.functional(config)
            j = forward_model(config, state, functional=functional, turbine_field=tf, annotate=annotate)
            self.last_state = state

            if return_final_state:
                return j, state
            else:
                return j

        def compute_gradient(m, forget=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional gradient. '''
            # If the last forward run was performed with the same parameters, then all recorded values by dolfin-adjoint are still valid for this adjoint run
            # and we do not have to rerun the forward model.
            if numpy.any(m != self.last_m):
                compute_functional(m, annotate=True)

            state = self.last_state
            functional = config.functional(config)

            # Produce power plot
            if config.params['output_turbine_power']:
                if config.params['turbine_thrust_parametrisation'] or config.params["implicit_turbine_thrust_parametrisation"] or "dynamic_turbine_friction" in config.params["controls"]:
                    info_red("Turbine power VTU's is not yet implemented with thrust based turbines parameterisations and dynamic turbine friction control.")
                else:
                    turbines = self.__config__.turbine_cache.cache["turbine_field"]
                    self.power_file << project(functional.power(state, turbines), config.turbine_function_space, annotate=False)

            # The functional depends on the turbine friction function which we do not have on scope here.
            # But dolfin-adjoint only cares about the name, so we can just create a dummy function with the desired name.
            dummy_tf = Function(FunctionSpace(state.function_space().mesh(), "R", 0), name="turbine_friction")

            if config.params['steady_state'] or config.params["functional_final_time_only"]:
                J = Functional(functional.Jt(state, dummy_tf) * dt[FINISH_TIME])

            elif config.params['functional_quadrature_degree'] == 0:
                # Pseudo-redo the time loop to collect the necessary timestep information
                t = config.params["start_time"]
                timesteps = [t]
                while (t < config.params["finish_time"]):
                    t += config.params["dt"]
                    timesteps.append(t)

                if not config.params["include_time_term"]:
                    # Remove the initial condition. I think this is a bug in dolfin-adjoint, since really I expected pop(0) here - but the Taylor tests pass only with pop(1)!
                    timesteps.pop(1)

                # Construct the functional
                J = Functional(sum(functional.Jt(state, dummy_tf) * dt[t] for t in timesteps))

            else:
                if not config.params["include_time_term"]:
                    raise NotImplementedError, "Multi-steady state simulations only work with 'functional_quadrature_degree=0' or 'functional_final_time_only=True'" 
                J = Functional(functional.Jt(state, dummy_tf) * dt)

            if 'dynamic_turbine_friction' in config.params["controls"]:
                parameters = [InitialConditionParameter("turbine_friction_cache_t_%i" % i) for i in range(len(config.params["turbine_friction"]))]

            else:
                parameters = InitialConditionParameter("turbine_friction_cache")

            djdtf = dolfin_adjoint.compute_gradient(J, parameters, forget=forget)
            dolfin.parameters["adjoint"]["stop_annotating"] = False

            # Decide if we need to apply the chain rule to get the gradient of interest
            if config.params['turbine_parametrisation'] == 'smeared':
                # We are looking for the gradient with respect to the friction
                dj = dolfin_adjoint.optimization.get_global(djdtf)

            else:
                # Let J be the functional, m the parameter and u the solution of the PDE equation F(u) = 0.
                # Then we have
                # dJ/dm = (\partial J)/(\partial u) * (d u) / d m + \partial J / \partial m
                #               = adj_state * \partial F / \partial u + \partial J / \partial m
                # In this particular case m = turbine_friction, J = \sum_t(ft)
                dj = []

                if 'turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for tfd in config.turbine_cache.cache["turbine_derivative_friction"]:
                        config.turbine_cache.update(config)
                        dj.append(djdtf.vector().inner(tfd.vector()))

                elif 'dynamic_turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for djdtf_arr, t in zip(djdtf, config.turbine_cache.cache["turbine_derivative_friction"]):
                        for tfd in t:
                            config.turbine_cache.update(config)
                            dj.append(djdtf_arr.vector().inner(tfd.vector()))

                if 'turbine_pos' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine position
                    for d in config.turbine_cache.cache["turbine_derivative_pos"]:
                        for var in ('turbine_pos_x', 'turbine_pos_y'):
                            config.turbine_cache.update(config)
                            tfd = d[var]
                            dj.append(djdtf.vector().inner(tfd.vector()))

                dj = numpy.array(dj)

            return dj

        def compute_hessian_action(m, m_dot):
            if numpy.any(m != self.last_m):
                self.run_adjoint_model_mem(m, forget=False)

            state = self.last_state

            functional = config.functional(config)
            if config.params['steady_state'] or config.params["functional_final_time_only"]:
                J = Functional(functional.Jt(state) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state) * dt)

            H = drivers.hessian(J, InitialConditionParameter("friction"), warn=False)
            m_dot = project(Constant(1), config.turbine_function_space)
            return H(m_dot)

        # For smeared turbine parametrisations we only want to store the 
        # hash of the control values into the pickle datastructure
        hash_keys = (config.params["turbine_parametrisation"] == "smeared")

        self.compute_functional_mem = memoize.MemoizeMutable(compute_functional, hash_keys)
        self.compute_gradient_mem = memoize.MemoizeMutable(compute_gradient, hash_keys)
        self.compute_hessian_action_mem = memoize.MemoizeMutable(compute_hessian_action, hash_keys)

    def update_turbine_cache(self, m):
        ''' Reconstructs the parameters from the flattened parameter array m and updates the configuration. '''

        if self.__config__.params["turbine_parametrisation"] == "smeared":
            self.__config__.params["turbine_friction"] = m

        else:
            shift = 0
            if 'turbine_friction' in self.__config__.params['controls']:
                shift = len(self.__config__.params["turbine_friction"])
                self.__config__.params["turbine_friction"] = m[:shift]

            elif 'dynamic_turbine_friction' in self.__config__.params['controls']:
                shift = len(numpy.reshape(self.__config__.params["turbine_friction"], -1))
                nb_turbines = len(self.__config__.params["turbine_pos"])
                self.__config__.params["turbine_friction"] = numpy.reshape(m[:shift], (-1, nb_turbines)).tolist()

            if 'turbine_pos' in self.__config__.params['controls']:
                mp = m[shift:]
                self.__config__.params["turbine_pos"] = numpy.reshape(mp, (-1, 2)).tolist()

        # Set up the turbine field
        self.__config__.turbine_cache.update(self.__config__)

    def save_checkpoint(self, base_filename):
        ''' Checkpoint the reduceduced functional from which can be used to restart the turbine optimisation. '''
        base_path = os.path.join(self.__config__.params["base_path"], base_filename)
        self.compute_functional_mem.save_checkpoint(base_path + "_fwd.dat")
        self.compute_gradient_mem.save_checkpoint(base_path + "_adj.dat")

    def load_checkpoint(self, base_filename='checkpoint'):
        ''' Checkpoint the reduceduced functional from which can be used to restart the turbine optimisation. '''
        base_path = os.path.join(self.__config__.params["base_path"], base_filename)
        self.compute_functional_mem.load_checkpoint(base_path + "_fwd.dat")
        self.compute_gradient_mem.load_checkpoint(base_path + "_adj.dat")

    def j(self, m, annotate=True):
        ''' This memoised function returns the functional value for the parameter choice m. '''
        info_green('Start evaluation of j')
        timer = dolfin.Timer("j evaluation")
        j = self.compute_functional_mem(m, annotate=annotate)
        timer.stop()

        if self.__config__.params["save_checkpoints"]:
            self.save_checkpoint("checkpoint")

        info_blue('Runtime: ' + str(timer.value()) + " s")
        info_green('j = ' + str(j))
        self.last_j = j

        if self.__config__.params['automatic_scaling']:
            if not self.automatic_scaling_factor:
                # Computing dj will set the automatic scaling factor.
                info_blue("Computing derivative to determine the automatic scaling factor")
                self.dj(m, forget=False, optimisation_iteration=False)
            return j * self.scale * self.automatic_scaling_factor
        else:
            return j * self.scale

    def dj(self, m, forget, optimisation_iteration=True):
        ''' This memoised function returns the gradient of the functional for the parameter choice m. '''
        info_green('Start evaluation of dj')
        timer = dolfin.Timer("dj evaluation")
        dj = self.compute_gradient_mem(m, forget)

        # We assume that the gradient is computed at and only at the beginning of each new optimisation iteration.
        # Hence, this is the right moment to store the turbine friction field and to increment the optimisation iteration
        # counter.
        if optimisation_iteration:
            self.__config__.optimisation_iteration += 1
            if self.__config__.params["dump_period"] > 0:
                # A cache hit skips the turbine cache update, so we need
                # trigger it manually.
                if self.compute_gradient_mem.has_cache(m, forget):
                    self.update_turbine_cache(m)
                if "dynamic_turbine_friction" in self.__config__.params["controls"]:
                    info_red("Turbine VTU output not yet implemented for dynamic turbine control")
                else:
                    self.turbine_file << self.__config__.turbine_cache.cache["turbine_field"]
                    # Compute the total amount of friction due to turbines
                    if self.__config__.params["turbine_parametrisation"] == "smeared":
                        print "Total amount of friction: ", assemble(self.__config__.turbine_cache.cache["turbine_field"] * dx)

        if self.save_functional_values and MPI.process_number() == 0:
            with open("functional_values.txt", "a") as functional_values:
                functional_values.write(str(self.last_j) + "\n")

        if self.plot:
            self.plotter.addPoint(self.last_j)
            self.plotter.savefig("functional_plot.png")

        if self.__config__.params["save_checkpoints"]:
            self.save_checkpoint("checkpoint")

        # Compute the scaling factor if never done before
        if self.__config__.params['automatic_scaling'] and not self.automatic_scaling_factor:
            if not 'turbine_pos' in self.__config__.params['controls']:
                raise NotImplementedError("Automatic scaling only works if the turbine positions are control parameters")

            if len(self.__config__.params['controls']) > 1:
                assert(len(dj) % 3 == 0)
                # Exclude the first third from the automatic scaling as it contains the friction coefficients
                djl2 = max(abs(dj[len(dj) / 3:]))
            else:
                djl2 = max(abs(dj))

            if djl2 == 0:
                raise ValueError("Automatic scaling failed: The gradient at the parameter point is zero")
            else:
                self.automatic_scaling_factor = abs(self.__config__.params['automatic_scaling_multiplier'] * max(self.__config__.params['turbine_x'], self.__config__.params['turbine_y']) / djl2 / self.scale)
                info_blue("The automatic scaling factor was set to " + str(self.automatic_scaling_factor * self.scale) + ".")

        info_blue('Runtime: ' + str(timer.stop()) + " s")
        info_green('|dj| = ' + str(numpy.linalg.norm(dj)))

        if self.__config__.params['automatic_scaling']:
            return dj * self.scale * self.automatic_scaling_factor
        else:
            return dj * self.scale

    def dj_with_check(self, m, seed=0.1, tol=1.8, forget=True):
        ''' This function checks the correctness and returns the gradient of the functional for the parameter choice m. '''

        info_red("Checking derivative at m = " + str(m))
        p = numpy.random.rand(len(m))
        minconv = helpers.test_gradient_array(self.j, self.dj, m, seed=seed, perturbation_direction=p)
        if minconv < tol:
            info_red("The gradient taylor remainder test failed.")
            sys.exit(1)
        else:
            info_green("The gradient taylor remainder test passed.")

        return self.dj(m, forget)

    def initial_control(self):
        ''' This function returns the control variable array that derives from the initial configuration. '''
        config = self.__config__
        res = []
        if config.params["turbine_parametrisation"] == "smeared":
            res = numpy.zeros(config.turbine_function_space.dim())

        else:
            if 'turbine_friction' in config.params["controls"] or 'dynamic_turbine_friction' in config.params["controls"]:
                res += numpy.reshape(config.params['turbine_friction'], -1).tolist()

            if 'turbine_pos' in config.params["controls"]:
                res += numpy.reshape(config.params['turbine_pos'], -1).tolist()

        return numpy.array(res)

    def __call__(self, m):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        return self.j(m)

    def derivative(self, m_array, taylor_test=False, seed=0.001, forget=True, **kwargs):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        if taylor_test:
            return self.dj_with_check(m_array, seed, forget)
        else:
            return self.dj(m_array, forget)

    def hessian(self, m_array, m_dot_array):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        raise NotImplementedError('The Hessian computation is not yet implemented')

    def obj_to_array(self, obj):
        return dolfin_adjoint.optimization.get_global(obj)

    def set_parameters(self, m_array):
        m = [p.data() for p in self.parameter]
        dolfin_adjoint.optimization.set_local(m, m_array)
Exemple #4
0
class ReducedFunctional:
    def update_turbine_cache(self, m):
        ''' Reconstructs the parameters from the flattened parameter array m and updates the configuration. '''

        if self.__config__.params["turbine_parametrisation"] == "smooth":
            self.__config__.params["turbine_friction"] = m

        else:
            shift = 0
            if 'turbine_friction' in self.__config__.params['controls']:
                shift = len(self.__config__.params["turbine_friction"])
                self.__config__.params["turbine_friction"] = m[:shift]

            elif 'dynamic_turbine_friction' in self.__config__.params[
                    'controls']:
                shift = len(
                    numpy.reshape(self.__config__.params["turbine_friction"],
                                  -1))
                nb_turbines = len(self.__config__.params["turbine_pos"])
                self.__config__.params["turbine_friction"] = numpy.reshape(
                    m[:shift], (-1, nb_turbines)).tolist()

            if 'turbine_pos' in self.__config__.params['controls']:
                mp = m[shift:]
                self.__config__.params["turbine_pos"] = numpy.reshape(
                    mp, (-1, 2)).tolist()

        # Set up the turbine field
        self.__config__.turbine_cache.update(self.__config__)

    def __init__(self,
                 config,
                 scale=1.0,
                 forward_model=sw_model.sw_solve,
                 plot=False):
        ''' If plot is True, the functional values will be automatically saved in a plot.
            scale is ignored if automatic_scaling is active. '''
        # Hide the configuration since changes would break the memoize algorithm.
        self.__config__ = config
        self.scale = scale
        self.automatic_scaling_factor = None
        self.plot = plot
        # Caching variables that store which controls the last forward run was performed
        self.last_m = None
        self.last_state = None
        if self.__config__.params["dump_period"] > 0:
            self.turbine_file = File("turbines.pvd", "compressed")

            if config.params['output_turbine_power']:
                self.power_file = File("power.pvd", "compressed")

        class Parameter:
            def data(self):
                m = []
                if config.params["turbine_parametrisation"] == "smooth":
                    m = numpy.zeros(config.turbine_function_space.dim())
                else:
                    if 'turbine_friction' in config.params["controls"]:
                        m += list(config.params['turbine_friction'])
                    if 'turbine_pos' in config.params["controls"]:
                        m += numpy.reshape(config.params['turbine_pos'],
                                           -1).tolist()
                return numpy.array(m)

        self.parameter = [Parameter()]

        if plot:
            self.plotter = AnimatedPlot(xlabel="Iteration",
                                        ylabel="Functional value")

        def compute_functional(m, return_final_state=False):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional of interest. '''

            self.last_m = m

            self.update_turbine_cache(m)
            tf = config.turbine_cache.cache["turbine_field"]
            #info_green("Turbine integral: %f ", assemble(tf*dx))
            #info_green("The correct integral should be: %f ",  25.2771) # computed with wolfram alpha using:
            # int 0.17353373* (exp(-1.0/(1-(x/10)**2)) * exp(-1.0/(1-(y/10)**2)) * exp(2)) dx dy, x=-10..10, y=-10..10
            #info_red("relative error: %f", (assemble(tf*dx)-25.2771)/25.2771)

            return compute_functional_from_tf(tf, return_final_state)

        def compute_functional_from_tf(tf, return_final_state):
            ''' Takes in the turbine friction field and computes the resulting functional of interest. '''
            adj_reset()
            parameters["adjoint"]["record_all"] = True

            # Get initial conditions
            if config.params["implicit_turbine_thrust_parametrisation"]:
                state = Function(config.function_space_2enriched,
                                 name="Current_state")
            elif config.params["turbine_thrust_parametrisation"]:
                state = Function(config.function_space_enriched,
                                 name="Current_state")
            else:
                state = Function(config.function_space, name="Current_state")

            if config.params["steady_state"] and self.last_state != None:
                # Speed up the nonlinear solves by starting the Newton solve with the most recent state solution
                state.assign(self.last_state, annotate=False)
            else:
                ic = config.params['initial_condition']
                state.assign(ic, annotate=False)

            # Solve the shallow water system
            functional = config.functional(config)
            j = forward_model(config,
                              state,
                              functional=functional,
                              turbine_field=tf)
            self.last_state = state

            if return_final_state:
                return j, state
            else:
                return j

        def compute_gradient(m, forget=True):
            ''' Takes in the turbine positions/frictions values and computes the resulting functional gradient. '''
            myt = Timer("full compute_gradient")
            # If the last forward run was performed with the same parameters, then all recorded values by dolfin-adjoint are still valid for this adjoint run
            # and we do not have to rerun the forward model.
            if numpy.any(m != self.last_m):
                compute_functional(m)

            state = self.last_state
            functional = config.functional(config)

            # Produce power plot
            if config.params['output_turbine_power']:
                if config.params['turbine_thrust_parametrisation'] or config.params[
                        "implicit_turbine_thrust_parametrisation"] or "dynamic_turbine_friction" in config.params[
                            "controls"]:
                    info_red(
                        "Turbine power VTU's is not yet implemented with thrust based turbines parameterisations and dynamic turbine friction control."
                    )
                else:
                    turbines = self.__config__.turbine_cache.cache[
                        "turbine_field"]
                    self.power_file << project(functional.expr(
                        state, turbines),
                                               config.turbine_function_space,
                                               annotate=False)

            # The functional depends on the turbine friction function which we do not have on scope here.
            # But dolfin-adjoint only cares about the name, so we can just create a dummy function with the desired name.
            dummy_tf = Function(FunctionSpace(state.function_space().mesh(),
                                              "R", 0),
                                name="turbine_friction")

            if config.params['steady_state'] or config.params[
                    "functional_final_time_only"]:
                J = Functional(
                    functional.Jt(state, dummy_tf) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state, dummy_tf) * dt)

            if 'dynamic_turbine_friction' in config.params["controls"]:
                parameters = [
                    InitialConditionParameter("turbine_friction_cache_t_%i" %
                                              i)
                    for i in range(len(config.params["turbine_friction"]))
                ]

            else:
                parameters = InitialConditionParameter(
                    "turbine_friction_cache")

            djdtf = dolfin_adjoint.compute_gradient(J,
                                                    parameters,
                                                    forget=forget)
            dolfin.parameters["adjoint"]["stop_annotating"] = False

            # Decide if we need to apply the chain rule to get the gradient of interest
            if config.params['turbine_parametrisation'] == 'smooth':
                # We are looking for the gradient with respect to the friction
                dj = dolfin_adjoint.optimization.get_global(djdtf)

            else:
                # Let J be the functional, m the parameter and u the solution of the PDE equation F(u) = 0.
                # Then we have
                # dJ/dm = (\partial J)/(\partial u) * (d u) / d m + \partial J / \partial m
                #               = adj_state * \partial F / \partial u + \partial J / \partial m
                # In this particular case m = turbine_friction, J = \sum_t(ft)
                dj = []

                if 'turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for tfd in config.turbine_cache.cache[
                            "turbine_derivative_friction"]:
                        config.turbine_cache.update(config)
                        dj.append(djdtf.vector().inner(tfd.vector()))

                elif 'dynamic_turbine_friction' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine friction
                    for djdtf_arr, t in zip(
                            djdtf, config.turbine_cache.
                            cache["turbine_derivative_friction"]):
                        for tfd in t:
                            config.turbine_cache.update(config)
                            dj.append(djdtf_arr.vector().inner(tfd.vector()))

                if 'turbine_pos' in config.params["controls"]:
                    # Compute the derivatives with respect to the turbine position
                    for d in config.turbine_cache.cache[
                            "turbine_derivative_pos"]:
                        for var in ('turbine_pos_x', 'turbine_pos_y'):
                            config.turbine_cache.update(config)
                            tfd = d[var]
                            dj.append(djdtf.vector().inner(tfd.vector()))

                dj = numpy.array(dj)

            return dj

        def compute_hessian_action(m, m_dot):
            if numpy.any(m != self.last_m):
                self.run_adjoint_model_mem(m, forget=False)

            state = self.last_state

            functional = config.functional(config)
            if config.params['steady_state'] or config.params[
                    "functional_final_time_only"]:
                J = Functional(functional.Jt(state) * dt[FINISH_TIME])
            else:
                J = Functional(functional.Jt(state) * dt)

            H = drivers.hessian(J,
                                InitialConditionParameter("friction"),
                                warn=False)
            m_dot = project(Constant(1), config.turbine_function_space)
            return H(m_dot)

        self.compute_functional_mem = memoize.MemoizeMutable(
            compute_functional)
        self.compute_gradient_mem = memoize.MemoizeMutable(compute_gradient)
        self.compute_hessian_action_mem = memoize.MemoizeMutable(
            compute_hessian_action)

    def save_checkpoint(self, base_filename):
        ''' Checkpoint the reduceduced functional from which can be used to restart the turbine optimisation. '''
        self.compute_functional_mem.save_checkpoint(base_filename + "_fwd.dat")
        self.compute_gradient_mem.save_checkpoint(base_filename + "_adj.dat")

    def load_checkpoint(self, base_filename='checkpoint'):
        ''' Checkpoint the reduceduced functional from which can be used to restart the turbine optimisation. '''
        self.compute_functional_mem.load_checkpoint(base_filename + "_fwd.dat")
        self.compute_gradient_mem.load_checkpoint(base_filename + "_adj.dat")

    def j(self, m):
        ''' This memoised function returns the functional value for the parameter choice m. '''
        info_green('Start evaluation of j')
        timer = dolfin.Timer("j evaluation")
        j = self.compute_functional_mem(m)
        timer.stop()

        if self.__config__.params["save_checkpoints"]:
            self.save_checkpoint("checkpoint")

        if self.plot:
            self.plotter.addPoint(j)
            self.plotter.savefig("functional_plot.png")
        info_blue('Runtime: ' + str(timer.value()) + " s")
        info_green('j = ' + str(j))

        if self.__config__.params['automatic_scaling']:
            if not self.automatic_scaling_factor:
                # Computing dj will set the automatic scaling factor.
                info_blue(
                    "Computing derivative to determine the automatic scaling factor"
                )
                dj = self.dj(m, forget=False)
            return j * self.scale * self.automatic_scaling_factor
        else:
            return j * self.scale

    def dj(self, m, forget):
        ''' This memoised function returns the gradient of the functional for the parameter choice m. '''
        info_green('Start evaluation of dj')
        timer = dolfin.Timer("dj evaluation")
        dj = self.compute_gradient_mem(m, forget)

        # We assume that at the gradient is computed if and only if at the beginning of each new optimisation iteration.
        # Hence, let this is the right moment to store the turbine friction field.
        if self.__config__.params["dump_period"] > 0:
            # A cache hit skips the turbine cache update, so we need
            # trigger it manually.
            if self.compute_gradient_mem.has_cache(m, forget):
                self.update_turbine_cache(m)
            if "dynamic_turbine_friction" in self.__config__.params[
                    "controls"]:
                info_red(
                    "Turbine VTU output not yet implemented for dynamic turbine control"
                )
            else:
                self.turbine_file << self.__config__.turbine_cache.cache[
                    "turbine_field"]

        if self.__config__.params["save_checkpoints"]:
            self.save_checkpoint("checkpoint")

        # Compute the scaling factor if never done before
        if self.__config__.params[
                'automatic_scaling'] and not self.automatic_scaling_factor:
            if not 'turbine_pos' in self.__config__.params['controls']:
                raise NotImplementedError, "Automatic scaling only works if the turbine positions are control parameters"

            if len(self.__config__.params['controls']) > 1:
                assert (len(dj) % 3 == 0)
                # Exclude the first third from the automatic scaling as it contains the friction coefficients
                djl2 = max(abs(dj[len(dj) / 3:]))
            else:
                djl2 = max(abs(dj))

            if djl2 == 0:
                raise ValueError, "Automatic scaling failed: The gradient at the parameter point is zero"
            else:
                self.automatic_scaling_factor = abs(
                    self.__config__.params['automatic_scaling_multiplier'] *
                    max(self.__config__.params['turbine_x'],
                        self.__config__.params['turbine_y']) / djl2 /
                    self.scale)
                info_blue("The automatic scaling factor was set to " +
                          str(self.automatic_scaling_factor * self.scale) +
                          ".")

        info_blue('Runtime: ' + str(timer.stop()) + " s")
        info_green('|dj| = ' + str(numpy.linalg.norm(dj)))

        if self.__config__.params['automatic_scaling']:
            return dj * self.scale * self.automatic_scaling_factor
        else:
            return dj * self.scale

    def dj_with_check(self, m, seed=0.1, tol=1.8, forget=True):
        ''' This function checks the correctness and returns the gradient of the functional for the parameter choice m. '''

        info_red("Checking derivative at m = " + str(m))
        p = numpy.random.rand(len(m))
        minconv = helpers.test_gradient_array(self.j,
                                              self.dj,
                                              m,
                                              seed=seed,
                                              perturbation_direction=p)
        if minconv < tol:
            info_red("The gradient taylor remainder test failed.")
            sys.exit(1)
        else:
            info_green("The gradient taylor remainder test passed.")

        return self.dj(m, forget)

    def initial_control(self):
        ''' This function returns the control variable array that derives from the initial configuration. '''
        config = self.__config__
        res = []
        if config.params["turbine_parametrisation"] == "smooth":
            res = numpy.zeros(config.turbine_function_space.dim())

        else:
            if 'turbine_friction' in config.params[
                    "controls"] or 'dynamic_turbine_friction' in config.params[
                        "controls"]:
                res += numpy.reshape(config.params['turbine_friction'],
                                     -1).tolist()

            if 'turbine_pos' in config.params["controls"]:
                res += numpy.reshape(config.params['turbine_pos'], -1).tolist()

        return numpy.array(res)

    def eval_array(self, m):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        return self.j(m)

    def derivative_array(self,
                         m_array,
                         taylor_test=False,
                         seed=0.001,
                         forget=True):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        if taylor_test:
            return self.dj_with_check(m_array, seed, forget)
        else:
            return self.dj(m_array, forget)

    def hessian_array(self, m_array, m_dot_array):
        ''' Interface function for dolfin_adjoint.ReducedFunctional '''
        raise NotImplementedError, 'The Hessian computation is not yet implemented'