########################################################################################################################
# Copyright 2021 the authors (see AUTHORS file for full list). #
# #
# This file is part of OpenCMP. #
# #
# OpenCMP is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public #
# License as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any #
# later version. #
# #
# OpenCMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied #
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
# details. #
# #
# You should have received a copy of the GNU Lesser General Public License along with OpenCMP. If not, see #
# <https://www.gnu.org/licenses/>. #
########################################################################################################################
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple
from ngsolve import CoefficientFunction, exp, GridFunction, Parameter
from ..config_functions import ControllerFunctions
from ..models import Model
[docs]class Controller(ABC):
"""
Base class which other controllers will subclass
"""
def __init__(self, t_params: List[Parameter], model: Model, config_rel_path: str, import_dir: str) -> None:
"""
Initializer for the controller classes.
Args:
t_params: Parameter representing the current time
model: The model that the control is being applied to
config_rel_path: The filename, and relative path, for the config file for this controller
import_dir: The path to the main run directory containing the file from which to import any Python functions.
"""
# The time of the simulation
self.t_params = t_params
# Save the model
self.model = model
# Config parameter loader
self.config_func = ControllerFunctions(config_rel_path, import_dir, self.model.mesh, self.t_params)
# Time step between each control action
# TODO: How to also non-dimensionalize this?
self.dt_control = self.config_func.config.get_item(['PARAMETERS', 'dt_control'], float)
# Time constant for the dynamics of the control action
self.tau: float = self.config_func.config.get_item(['PARAMETERS', 't_ramp'], float)
# Check that tau <= dt_control. Alert the user if it's not
# TODO: Is this warning still needed?
if self.tau > self.dt_control:
print('WARNING: tau is higher than dt_control, this was likely not intentional')
# The time at which the next control action will take place
self.t_next_action = self.t_params[0].Get()
# Load info for manipulated variables
# [[type_1, var_1, loc_1],[...],...]
self.vars_manipulate = self.config_func.get_manipulated_variables()
# Load info for control variables info
# [[name_1, pos_1, val_1, index_1],[...],...]
self.vars_control = self.config_func.get_control_variables()
[docs] def _apply_dynamics_equation(self, next_control_action: float, prev_control_action: float, t_param: Parameter)\
-> CoefficientFunction:
"""
Function add dynamics to the transition between two control actions.
Args:
next_control_action: float representing the new control action
prev_control_action: float representing the previous control action
t_param: Parameter representing time
Return:
~: Coefficient function
"""
# TODO: add more documentation, espcially with the t_param - t_param.Get()
# TODO: rename delta_... to make the equation more readable
# Define helper variable to make equation easier to read
delta_control_action = prev_control_action - next_control_action
# Shift exponential so that t=0 is defined as the present
t_zeroed = t_param - t_param.Get()
return CoefficientFunction(next_control_action + delta_control_action * exp(- t_zeroed / self.tau))
[docs] def _evaluate_control_variables(self, soln: GridFunction) -> List[Tuple[float]]:
"""
Function to measure the values of the control variables.
Args:
soln: GridFunction containing the results of the most recent time solve
Return:
~: A list containing the values of the control variables.
Both scale and vector values will return as a tuple.
"""
measurements: List[Tuple[float]] = []
for i in range(len(self.vars_control)):
var_name = self.vars_control[i][0]
coords = self.vars_control[i][1]
# Create the point at which to measure the value at
if len(coords) == 1:
point = self.model.mesh(coords[0])
elif len(coords) == 2:
point = self.model.mesh(coords[0], coords[1])
elif len(coords) == 3:
point = self.model.mesh(coords[0], coords[1], coords[2])
else:
raise ValueError("Only 1-3D meshes are supported")
# Get the index into the gfu corresponding to the variable we want to measure
var_index = self.model.model_components[var_name]
# Evaluate the control variable at the point we want
if var_index is None:
measurement = soln(point)
else:
measurement = soln.components[var_index](point)
# Convert float values to a tuple of length one so that it's consistent with vector values
if type(measurement) is float:
measurement = tuple([measurement])
# Append solution
measurements.append(measurement)
return measurements
[docs] def _update_time_of_next_action(self) -> None:
"""
Function to update the time at which the next control action occurs.
This function must be called when
"""
self.t_next_action += self.dt_control
[docs] @abstractmethod
def calculate_control_action(self, soln: GridFunction, rk_scheme: bool)\
-> Dict[str, Dict[str, Dict[str, List[Optional[CoefficientFunction]]]]]:
"""
Function which calculates what the new value for the manipulated variable should be.
Args:
soln: GridFunction containing the results of the most recent time solve
rk_scheme: bool indicating if the time scheme being used is an RK scheme, and thus requiring the
recalculation of control actions for all values in t_params
Return:
bc_dict_patch: Dictionary containing new values for the BCs representing the manipulated variables
"""