diff --git a/.gitignore b/.gitignore index 10b91ea..16e0a54 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ test_dump/ fastvpinns.egg-info/ +#ignore output files +examples/**/output/ + # pytest *.vtk *.msh diff --git a/fastvpinns/data/datahandler2d.py b/fastvpinns/data/datahandler2d.py index 5b5ac1d..976ccf4 100644 --- a/fastvpinns/data/datahandler2d.py +++ b/fastvpinns/data/datahandler2d.py @@ -11,42 +11,50 @@ class DataHandler2D: """ - This class is to handle data for 2D problems, convert them into tensors using custom tf functions - Responsible for all type conversions and data handling - Note: All inputs to these functions are generally numpy arrays with dtype np.float64 - So we can either maintain the same dtype or convert them to tf.float32 ( for faster computation ) - - Attributes: - - fespace (FESpace2D): The FESpace2D object - - domain (Domain2D): The Domain2D object - - shape_val_mat_list (list): List of shape function values for each cell - - grad_x_mat_list (list): List of shape function derivatives with respect to x for each cell - - grad_y_mat_list (list): List of shape function derivatives with respect to y for each cell - - x_pde_list (list): List of actual coordinates of the quadrature points for each cell - - forcing_function_list (list): List of forcing function values for each cell - - dtype (tf.DType): The tensorflow dtype to be used for all the tensors - - Methods: - - get_pde_input(): Returns the input for the PDE training data - - get_dirichlet_input(): Returns the input for the Dirichlet boundary data - - get_test_points(num_test_points): Returns the test points - - get_bilinear_params_dict_as_tensors(function): Accepts a function from example file and converts all the values into tensors of the given dtype - - get_sensor_data(exact_sol, num_sensor_points, mesh_type, file_name=None): Returns the sensor data - - get_inverse_params(inverse_params_dict_function): Accepts a function from example file and converts all the values into tensors of the given dtype - + This class is to handle data for 2D problems, convert them into tensors using custom tf functions. + It is responsible for all type conversions and data handling. + + .. note:: All inputs to these functions are generally numpy arrays with dtype np.float64. + So we can either maintain the same dtype or convert them to tf.float32 ( for faster computation ). + + :param fespace: The FESpace2D object. + :type fespace: FESpace2D + :param domain: The Domain2D object. + :type domain: Domain2D + :param shape_val_mat_list: List of shape function values for each cell. + :type shape_val_mat_list: list + :param grad_x_mat_list: List of shape function derivatives with respect to x for each cell. + :type grad_x_mat_list: list + :param grad_y_mat_list: List of shape function derivatives with respect to y for each cell. + :type grad_y_mat_list: list + :param x_pde_list: List of actual coordinates of the quadrature points for each cell. + :type x_pde_list: list + :param forcing_function_list: List of forcing function values for each cell. + :type forcing_function_list: list + :param dtype: The tensorflow dtype to be used for all the tensors. + :type dtype: tf.DType """ def __init__(self, fespace, domain, dtype): """ Constructor for the DataHandler2D class - Parameters: - - fespace (FESpace2D): The FESpace2D object - - domain (Domain2D): The Domain2D object - - dtype (tf.DType): The tensorflow dtype to be used for all the tensors - - Returns: - None + :param fespace: The FESpace2D object. + :type fespace: FESpace2D + :param domain: The Domain2D object. + :type domain: Domain2D + :param shape_val_mat_list: List of shape function values for each cell. + :type shape_val_mat_list: list + :param grad_x_mat_list: List of shape function derivatives with respect to x for each cell. + :type grad_x_mat_list: list + :param grad_y_mat_list: List of shape function derivatives with respect to y for each cell. + :type grad_y_mat_list: list + :param x_pde_list: List of actual coordinates of the quadrature points for each cell. + :type x_pde_list: list + :param forcing_function_list: List of forcing function values for each cell. + :type forcing_function_list: list + :param dtype: The tensorflow dtype to be used for all the tensors. + :type dtype: tf.DType """ self.fespace = fespace @@ -97,26 +105,13 @@ def __init__(self, fespace, domain, dtype): # test points self.test_points = None - def get_pde_input(self): - """ - This function will return the input for the PDE training data - - Returns: - - input_pde (tf.Tensor): The input for the PDE training data - """ - return self.fespace.get_pde_training_data() - def get_dirichlet_input(self): """ This function will return the input for the Dirichlet boundary data - Args: - None - - Returns: - - input_dirichlet (tf.Tensor): The input for the Dirichlet boundary data - - actual_dirichlet (tf.Tensor): The actual Dirichlet boundary data - + :return: + - input_dirichlet (tf.Tensor): The input for the Dirichlet boundary data + - actual_dirichlet (tf.Tensor): The actual Dirichlet boundary data """ input_dirichlet, actual_dirichlet = self.fespace.generate_dirichlet_boundary_data() @@ -127,18 +122,14 @@ def get_dirichlet_input(self): return input_dirichlet, actual_dirichlet - def get_test_points(self, num_test_points): + def get_test_points(self): """ - This function will return the test points for the given domain + Get the test points for the given domain. - Args: - - num_test_points (int): The number of test points to be generated - - Returns: - - test_points (tf.Tensor): The test points for the given domain + :return: The test points for the given domain. + :rtype: tf.Tensor """ - - self.test_points = self.domain.generate_test_points(num_test_points) + self.test_points = self.domain.get_test_points() self.test_points = tf.constant(self.test_points, dtype=self.dtype) return self.test_points @@ -151,8 +142,12 @@ def get_bilinear_params_dict_as_tensors(self, function): Returns: - bilinear_params_dict (dict): The bilinear parameters dictionary with all the values converted to tensors - """ + :param function: The function from the example file which returns the bilinear parameters dictionary + :type function: function + :return: The bilinear parameters dictionary with all the values converted to tensors + :rtype: dict + """ # get the dictionary of bilinear parameters bilinear_params_dict = function() @@ -167,15 +162,16 @@ def get_sensor_data(self, exact_sol, num_sensor_points, mesh_type, file_name=Non """ Accepts a function from example file and converts all the values into tensors of the given dtype - Parameters: - - exact_sol (function): The function from the example file which returns the exact solution - - num_sensor_points (int): The number of sensor points to be generated - - mesh_type (str): The type of mesh to be used for sensor data generation - - file_name (str): The name of the file to be used for external mesh generation - - Returns: - - points (tf.Tensor): The sensor points - - sensor_values (tf.Tensor): The sensor values + :param exact_sol: The function from the example file which returns the exact solution + :type exact_sol: function + :param num_sensor_points: The number of sensor points to be generated + :type num_sensor_points: int + :param mesh_type: The type of mesh to be used for sensor data generation + :type mesh_type: str + :param file_name: The name of the file to be used for external mesh generation, defaults to None + :type file_name: str, optional + :return: The sensor points and sensor values as tensors + :rtype: tuple[tf.Tensor, tf.Tensor] """ print(f"mesh_type = {mesh_type}") if mesh_type == "internal": @@ -200,11 +196,10 @@ def get_inverse_params(self, inverse_params_dict_function): """ Accepts a function from example file and converts all the values into tensors of the given dtype - Parameters: - - inverse_params_dict_function (function): The function from the example file which returns the inverse parameters dictionary - - Returns: - - inverse_params_dict (dict): The inverse parameters dictionary with all the values converted to tensors + :param inverse_params_dict_function: The function from the example file which returns the inverse parameters dictionary + :type inverse_params_dict_function: function + :return: The inverse parameters dictionary with all the values converted to tensors + :rtype: dict """ # loop over all keys and convert the values to tensors diff --git a/tests/unit/data/test_data_functions.py b/tests/unit/data/test_data_functions.py new file mode 100644 index 0000000..a7883f4 --- /dev/null +++ b/tests/unit/data/test_data_functions.py @@ -0,0 +1,290 @@ +# Author : Thivin Anandh. D +# Added test cases for validating Datahandler routines +# The test cases are parametrized for different quadrature types and transformations. + +import numpy as np +import pytest +from pathlib import Path +import tensorflow as tf + +from fastvpinns.Geometry.geometry_2d import Geometry_2D +from fastvpinns.FE_2D.fespace2d import Fespace2D +from fastvpinns.data.datahandler2d import DataHandler2D +from fastvpinns.model.model import DenseModel +from fastvpinns.physics.cd2d import pde_loss_cd2d +from fastvpinns.utils.compute_utils import compute_errors_combined + + +@pytest.fixture +def cd2d_test_data_internal(): + """ + Generate test data for the cd2d equation. + """ + omega = 4.0 * np.pi + left_boundary = lambda x, y: np.tanh(np.pi * x) * np.cos(2 * np.pi * y) + right_boundary = lambda x, y: np.tanh(np.pi * x) * np.cos(2 * np.pi * y) + bottom_boundary = lambda x, y: np.tanh(np.pi * x) * np.cos(2 * np.pi * y) + top_boundary = lambda x, y: np.tanh(np.pi * x) * np.cos(2 * np.pi * y) + boundary_functions = { + 1000: bottom_boundary, + 1001: right_boundary, + 1002: top_boundary, + 1003: left_boundary, + } + + boundary_conditions = { + 1000: "dirichlet", + 1001: "dirichlet", + 1002: "dirichlet", + 1003: "dirichlet", + } + + bilinear_params = lambda: {"eps": 1.0, "b_x": 0.2, "b_y": -0.1, "c": 0.0} + + def rhs(x, y): + result = 0.2 * np.sin(2 * np.pi * y) * np.sinh(2 * np.pi * x) + result += 4.0 * np.pi * np.cos(2 * np.pi * y) * np.sinh(2 * np.pi * x) + result += 4.0 * np.pi * np.cos(2 * np.pi * y) * np.tanh(np.pi * x) + result += 0.4 * np.cos(2 * np.pi * y) + result = (np.pi * result) / (np.cosh(2 * np.pi * x) + 1) + return result + + forcing_function = rhs + + exact_solution = lambda x, y: np.tanh(np.pi * x) * np.cos(2 * np.pi * y) + + return ( + boundary_functions, + boundary_conditions, + bilinear_params, + forcing_function, + exact_solution, + ) + + +def cd2d_learning_rate_static_data(): + """ + Generate the learning rate dictionary for the cd2d equation. + """ + initial_learning_rate = 0.001 + use_lr_scheduler = False + decay_steps = 1000 + decay_rate = 0.99 + staircase = False + + learning_rate_dict = {} + learning_rate_dict["initial_learning_rate"] = initial_learning_rate + learning_rate_dict["use_lr_scheduler"] = use_lr_scheduler + learning_rate_dict["decay_steps"] = decay_steps + learning_rate_dict["decay_rate"] = decay_rate + learning_rate_dict["staircase"] = staircase + + return learning_rate_dict + + +# This is a module-scoped fixture that stores the results of the setup +@pytest.fixture(scope="module") +def setup_results(): + return {} + + +@pytest.mark.parametrize("quad_order", [3, 5, 7]) +@pytest.mark.parametrize("fe_order", [2, 4, 6]) +@pytest.mark.parametrize("cell_dimensions", [[4, 4], [2, 4], [6, 5]]) +@pytest.mark.parametrize("precision", ["float32", "float64"]) +def test_setup( + cd2d_test_data_internal, quad_order, fe_order, cell_dimensions, precision, setup_results +): + # obtain the test data + bound_function_dict, bound_condition_dict, bilinear_params, rhs, exact_solution = ( + cd2d_test_data_internal + ) + n_cells_x = cell_dimensions[0] + n_cells_y = cell_dimensions[1] + n_cells = n_cells_x * n_cells_y + output_folder = "tests/test_dump" + + if precision == "float32": + precision = tf.float32 + elif precision == "float64": + precision = tf.float64 + + # use pathlib to create the directory + Path(output_folder).mkdir(parents=True, exist_ok=True) + + # generate a internal mesh + domain = Geometry_2D("quadrilateral", "internal", 89, 89, output_folder) + cells, boundary_points = domain.generate_quad_mesh_internal( + x_limits=[0, 1], + y_limits=[0, 1], + n_cells_x=n_cells_x, + n_cells_y=n_cells_y, + num_boundary_points=500, + ) + + # create the fespace + fespace = Fespace2D( + mesh=domain.mesh, + cells=cells, + boundary_points=boundary_points, + cell_type=domain.mesh_type, + fe_order=fe_order, + fe_type="jacobi", + quad_order=quad_order, + quad_type="gauss-jacobi", + fe_transformation_type="bilinear", + bound_function_dict=bound_function_dict, + bound_condition_dict=bound_condition_dict, + forcing_function=rhs, + output_path=output_folder, + generate_mesh_plot=False, + ) + # create the data handler + datahandler = DataHandler2D(fespace, domain, dtype=precision) + + # obtain all variables of Datahandler + x_pde_list = datahandler.x_pde_list + train_dirichlet_input, train_dirichlet_output = datahandler.get_dirichlet_input() + shape_val_mat_list = datahandler.shape_val_mat_list + grad_x_mat_list = datahandler.grad_x_mat_list + grad_y_mat_list = datahandler.grad_y_mat_list + forcing_function_list = datahandler.forcing_function_list + + test_points = datahandler.get_test_points() + + setup_results[(quad_order, fe_order, tuple(cell_dimensions), precision)] = ( + x_pde_list, + train_dirichlet_input, + train_dirichlet_output, + shape_val_mat_list, + grad_x_mat_list, + grad_y_mat_list, + forcing_function_list, + test_points, + ) + + +def test_x_pde_list(setup_results): + """ + Test function for checking the properties of x_pde_list. + + :param setup_results: A dictionary containing setup results. + """ + for key, value in setup_results.items(): + x_pde_list = value[0] + + quad_order = key[0] + fe_order = key[1] + cell_dimensions = key[2] + n_cell = cell_dimensions[0] * cell_dimensions[1] + precision = key[3] + + # check if the x_pde_list is a tensor + assert isinstance(x_pde_list, tf.Tensor) + # check precision + assert x_pde_list.dtype == precision + # check shape + assert x_pde_list.shape == (n_cell * quad_order**2, 2) + + +def test_dirichlet_inputs(setup_results): + """ + Test function for checking the properties of dirichlet inputs. + + :param setup_results: A dictionary containing setup results. + """ + for key, value in setup_results.items(): + train_dirichlet_input = value[1] + train_dirichlet_output = value[2] + + quad_order = key[0] + fe_order = key[1] + cell_dimensions = key[2] + n_cell = cell_dimensions[0] * cell_dimensions[1] + precision = key[3] + + # check if the x_pde_list is a tensor + assert isinstance(train_dirichlet_input, tf.Tensor) + assert isinstance(train_dirichlet_output, tf.Tensor) + # check precision + assert train_dirichlet_input.dtype == precision + assert train_dirichlet_output.dtype == precision + + # check first dimensions of input and output + assert train_dirichlet_input.shape[0] == train_dirichlet_output.shape[0] + + +def test_shape_tensors(setup_results): + """ + Test function for checking the properties of Shape function and gradient matrices. + + :param setup_results: A dictionary containing setup results. + """ + for key, value in setup_results.items(): + shape_val_mat_list = value[3] + grad_x_mat_list = value[4] + grad_y_mat_list = value[5] + + quad_order = key[0] + fe_order = key[1] + cell_dimensions = key[2] + n_cell = cell_dimensions[0] * cell_dimensions[1] + precision = key[3] + + # check if the x_pde_list is a tensor + assert isinstance(shape_val_mat_list, tf.Tensor) + assert isinstance(grad_x_mat_list, tf.Tensor) + assert isinstance(grad_y_mat_list, tf.Tensor) + # check precision + assert shape_val_mat_list.dtype == precision + assert grad_x_mat_list.dtype == precision + assert grad_y_mat_list.dtype == precision + # check shape + assert shape_val_mat_list.shape == (n_cell, fe_order**2, quad_order**2) + assert grad_x_mat_list.shape == (n_cell, fe_order**2, quad_order**2) + assert grad_y_mat_list.shape == (n_cell, fe_order**2, quad_order**2) + + +def test_forcing_function_list(setup_results): + """ + Test function for checking the properties of forcing function list. + + :param setup_results: A dictionary containing setup results. + """ + for key, value in setup_results.items(): + forcing_function_list = value[6] + + quad_order = key[0] + fe_order = key[1] + cell_dimensions = key[2] + n_cell = cell_dimensions[0] * cell_dimensions[1] + precision = key[3] + + # check if the x_pde_list is a tensor + assert isinstance(forcing_function_list, tf.Tensor) + # check precision + assert forcing_function_list.dtype == precision + # check shape + assert forcing_function_list.shape == (fe_order**2, n_cell) + + +def test_num_test_points(setup_results): + """ + Test function for checking the number of test points. + + :param setup_results: A dictionary containing setup results. + """ + for key, value in setup_results.items(): + test_points = value[7] + quad_order = key[0] + fe_order = key[1] + cell_dimensions = key[2] + n_cell = cell_dimensions[0] * cell_dimensions[1] + precision = key[3] + + # check if the x_pde_list is a tensor + assert isinstance(test_points, tf.Tensor) + # check precision + assert test_points.dtype == precision + # check shape + assert test_points.shape == (89 * 89, 2)