From f56baaba3cc169ed30c0f44788c52f91be2a3c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Sousa=20Almeida?= Date: Wed, 31 Jul 2024 16:49:10 -0300 Subject: [PATCH 1/9] special functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index b6a38f3..0300666 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -52,6 +52,7 @@ def __init__( device: str = "cpu", engine: str = "torch", auxiliary_expressions: list = None, + special_expressions: list = None, ) -> None: if engine == "torch": super(SymbolicOperator, self).__init__() @@ -129,6 +130,7 @@ def __init__( self.f_expressions = list() self.g_expressions = dict() + self.h_expressions = list() self.feed_vars = None @@ -164,6 +166,7 @@ def __init__( self.f_expressions.append(f_expr) + # auxiliary expressions (usually boundary conditions) if self.auxiliary_expressions is not None: for key, expr in self.auxiliary_expressions.items(): if not callable(expr): @@ -173,6 +176,16 @@ def __init__( self.g_expressions[key] = g_expr + # special expressions (usually employed for certain kinds of loss functions) + if self.special_expressions is not None: + for key, expr in self.special_expressions.items(): + if not callable(expr): + h_expr = sympy.lambdify(self.all_vars, expr, subs) + else: + h_expr = expr + + self.h_expressions.append(h_expr) + # Method for executing the expressions evaluation if self.processing == "serial": self.process_expression = self._process_expression_serial From 62055436437d34cd773107bdb2d9b59f2a38da6f Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Wed, 31 Jul 2024 21:52:52 -0300 Subject: [PATCH 2/9] Parsing special expressions Signed-off-by: Joao Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 49 +++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index 0300666..c310316 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -185,13 +185,16 @@ def __init__( h_expr = expr self.h_expressions.append(h_expr) + + self.process_special_expression = self._factory_process_expression_serial(expressions=self.h_expressions) # Method for executing the expressions evaluation if self.processing == "serial": - self.process_expression = self._process_expression_serial + self.process_expression = self._factory_process_expression_serial(expressions=self.f_expressions) else: raise Exception(f"Processing case {self.processing} not supported.") + def _construct_protected_functions(self): """This function creates a dictionary of protected functions from the engine object attribute. @@ -376,33 +379,39 @@ def _forward_dict(self, input_data: dict = None) -> torch.Tensor: """ return self.function.forward(**input_data) + + def _factory_process_expression_serial(self, expressions:list=None): + def _process_expression_serial(feed_vars: dict = None) -> List[torch.Tensor]: + """Process the expression list serially using the given feed variables. - def _process_expression_serial(self, feed_vars: dict = None) -> List[torch.Tensor]: - """Process the expression list serially using the given feed variables. + Args: + feed_vars (dict, optional): The feed variables. (Default value = None) - Args: - feed_vars (dict, optional): The feed variables. (Default value = None) + Returns: + List[torch.Tensor]: A list of tensors after evaluating the expressions serially. - Returns: - List[torch.Tensor]: A list of tensors after evaluating the expressions serially. + """ + return [f(**feed_vars).to(self.device) for f in expressions] - """ - return [f(**feed_vars).to(self.device) for f in self.f_expressions] + return _process_expression_serial - def _process_expression_individual( - self, index: int = None, feed_vars: dict = None - ) -> torch.Tensor: - """Evaluates a single expression specified by index from the f_expressions list with given feed variables. + def _factory_process_expression_individual(self, expressions:list=None): + def _process_expression_individual( + index: int = None, feed_vars: dict = None + ) -> torch.Tensor: + """Evaluates a single expression specified by index from the f_expressions list with given feed variables. - Args: - index (int, optional): Index of the expression to be evaluated, by default None - feed_vars (dict, optional): Dictionary of feed variables, by default None + Args: + index (int, optional): Index of the expression to be evaluated, by default None + feed_vars (dict, optional): Dictionary of feed variables, by default None - Returns: - torch.Tensor: Result of evaluating the specified expression with given feed variables + Returns: + torch.Tensor: Result of evaluating the specified expression with given feed variables - """ - return self.f_expressions[index](**feed_vars).to(self.device) + """ + return self.expressions[index](**feed_vars).to(self.device) + + return _process_expression_individual def __call__( self, inputs_data: Union[np.ndarray, dict] = None From bee254cd0c9f557f949cbd67eec950615929e638 Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Wed, 31 Jul 2024 21:54:38 -0300 Subject: [PATCH 3/9] Reformatting Signed-off-by: Joao Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 34 +++++++++++++++++-------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index c310316..a4bf0f7 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -73,7 +73,16 @@ def __init__( self.processing = processing self.periodic_bc_protected_key = "periodic" - self.protected_funcs = ["cos", "sin", "sqrt", "exp", "tanh", "cosh", "sech", "sinh"] + self.protected_funcs = [ + "cos", + "sin", + "sqrt", + "exp", + "tanh", + "cosh", + "sech", + "sinh", + ] self.protected_operators = ["L", "Div", "Identity", "Kronecker"] self.protected_funcs_subs = self._construct_protected_functions() @@ -185,16 +194,19 @@ def __init__( h_expr = expr self.h_expressions.append(h_expr) - - self.process_special_expression = self._factory_process_expression_serial(expressions=self.h_expressions) + + self.process_special_expression = self._factory_process_expression_serial( + expressions=self.h_expressions + ) # Method for executing the expressions evaluation if self.processing == "serial": - self.process_expression = self._factory_process_expression_serial(expressions=self.f_expressions) + self.process_expression = self._factory_process_expression_serial( + expressions=self.f_expressions + ) else: raise Exception(f"Processing case {self.processing} not supported.") - def _construct_protected_functions(self): """This function creates a dictionary of protected functions from the engine object attribute. @@ -379,8 +391,8 @@ def _forward_dict(self, input_data: dict = None) -> torch.Tensor: """ return self.function.forward(**input_data) - - def _factory_process_expression_serial(self, expressions:list=None): + + def _factory_process_expression_serial(self, expressions: list = None): def _process_expression_serial(feed_vars: dict = None) -> List[torch.Tensor]: """Process the expression list serially using the given feed variables. @@ -395,7 +407,7 @@ def _process_expression_serial(feed_vars: dict = None) -> List[torch.Tensor]: return _process_expression_serial - def _factory_process_expression_individual(self, expressions:list=None): + def _factory_process_expression_individual(self, expressions: list = None): def _process_expression_individual( index: int = None, feed_vars: dict = None ) -> torch.Tensor: @@ -653,20 +665,20 @@ def sech(self, x): cosh = getattr(self.engine, "cosh") - return 1/cosh(x) + return 1 / cosh(x) def csch(self, x): sinh = getattr(self.engine, "sinh") - return 1/sinh(x) + return 1 / sinh(x) def coth(self, x): cosh = getattr(self.engine, "cosh") sinh = getattr(self.engine, "sinh") - return cosh(x)/sinh(x) + return cosh(x) / sinh(x) def diff(feature: torch.Tensor, param: torch.Tensor) -> torch.Tensor: From fd6d31f4f6b752551a445d92cf3b81c65d3239c3 Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Thu, 1 Aug 2024 07:17:05 -0300 Subject: [PATCH 4/9] Extending the supported operators to include Grad Signed-off-by: Joao Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 6 ++--- simulai/tokens.py | 32 ++++++++++++++++++++++++ tests/residuals/test_symbolicoperator.py | 31 +++++++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index a4bf0f7..1c3661b 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -83,7 +83,7 @@ def __init__( "sech", "sinh", ] - self.protected_operators = ["L", "Div", "Identity", "Kronecker"] + self.protected_operators = ["L", "Div", "Grad", "Identity", "Kronecker"] self.protected_funcs_subs = self._construct_protected_functions() self.protected_operators_subs = self._construct_implict_operators() @@ -186,8 +186,8 @@ def __init__( self.g_expressions[key] = g_expr # special expressions (usually employed for certain kinds of loss functions) - if self.special_expressions is not None: - for key, expr in self.special_expressions.items(): + if special_expressions is not None: + for expr in self.special_expressions: if not callable(expr): h_expr = sympy.lambdify(self.all_vars, expr, subs) else: diff --git a/simulai/tokens.py b/simulai/tokens.py index 3641ced..ca6e1cf 100644 --- a/simulai/tokens.py +++ b/simulai/tokens.py @@ -93,6 +93,38 @@ def Div(u: sympy.Symbol, vars: tuple) -> callable: return l +def Grad(u: sympy.Symbol, vars: tuple) -> callable: + """ + Generate a callable object to compute the gradient operator. + + The gradient operator is a first-order differential operator that measures the + magnitude and direction of a flow of a vector field from its source and + convergence to a point. + + Parameters + ---------- + u : sympy.Symbol + The vector field to compute the divergence of. + vars : tuple + A tuple of variables to compute the divergence with respect to. + + Returns + ------- + callable + A callable object that computes the divergence of a vector field with respect + to the given variables. + + Examples + -------- + >>> x, y, z = sympy.symbols('x y z') + >>> u = sympy.Matrix([x**2, y**2, z**2]) + >>> Grad(u, (x, y, z)) + 2*x + 2*y + 2*z + """ + g = [D(u, var) for var in vars] + + return g + def Gp( g0: Union[torch.tensor, float], r: Union[torch.tensor, float], n: int diff --git a/tests/residuals/test_symbolicoperator.py b/tests/residuals/test_symbolicoperator.py index 4a410d7..ce3a0d0 100644 --- a/tests/residuals/test_symbolicoperator.py +++ b/tests/residuals/test_symbolicoperator.py @@ -226,6 +226,37 @@ def test_symbolic_operator_diff_operators(self): assert all([isinstance(item, torch.Tensor) for item in residual(data)]) + def test_symbolic_operator_grad_operator(self): + + f = "Grad(u, (x, y))" + + input_labels = ["x", "y"] + output_labels = ["u"] + + L_x = 1 + L_y = 1 + N_x = 100 + N_y = 100 + dx = L_x / N_x + dy = L_y / N_y + + grid = np.mgrid[0:L_x:dx, 0:L_y:dy] + + data = np.hstack([grid[1].flatten()[:, None], grid[0].flatten()[:, None]]) + + net = model(n_inputs=len(input_labels), n_outputs=len(output_labels)) + + residual = SymbolicOperator( + expressions=[f], + input_vars=input_labels, + constants={"alpha": 5}, + output_vars=output_labels, + function=net, + engine="torch", + ) + + assert all([isinstance(item, torch.Tensor) for item in residual(data)]) + def test_symbolic_operator_1d_pde(self): # Allen-Cahn equation f_0 = "D(u, t) - mu*D(D(u, x), x) + alpha*(u**3) + beta*u" From 22d65e8f79673b5ff675ccf3088aa449d1752318 Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Thu, 1 Aug 2024 07:40:10 -0300 Subject: [PATCH 5/9] Supporting lists of expressions (representing vector operators, as gradient) during compilation Signed-off-by: Joao Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index 1c3661b..0647336 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -207,6 +207,16 @@ def __init__( else: raise Exception(f"Processing case {self.processing} not supported.") + def _subs_expr(self, expr=None, constants=None): + + if isinstance(expr, list): + for j, e in enumerate(expr): + expr[j] = e.subs(constants) + else: + expr = expr.subs(constants) + + return expr + def _construct_protected_functions(self): """This function creates a dictionary of protected functions from the engine object attribute. @@ -331,9 +341,10 @@ def _parse_expression(self, expr=Union[sympy.Expr, str]) -> sympy.Expr: ) if self.constants is not None: - expr_ = expr_.subs(self.constants) + expr_ = self._subs_expr(expr=expr_, constants=self.constants) if self.trainable_parameters is not None: - expr_ = expr_.subs(self.trainable_parameters) + expr_ = self._subs_expr(expr=expr_, constants=self.trainable_parameters) + except ValueError: if self.constants is not None: _expr = expr From 53ee3d5093a78970b9d22b78887d48f33e46124c Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Thu, 1 Aug 2024 21:49:20 -0300 Subject: [PATCH 6/9] Minor adjusts Signed-off-by: Joao Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index 0647336..d7b3543 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -113,6 +113,8 @@ def __init__( else: self.auxiliary_expressions = auxiliary_expressions + self.special_expressions = special_expressions + self.input_vars = [self._parse_variable(var=var) for var in input_vars] self.output_vars = [self._parse_variable(var=var) for var in output_vars] From 107863108bdc58058646a8a3822a1c0ed6da4b56 Mon Sep 17 00:00:00 2001 From: Joao Lucas de Sousa Almeida Date: Fri, 2 Aug 2024 15:31:35 -0300 Subject: [PATCH 7/9] Minor changes in the tests Signed-off-by: Joao Lucas de Sousa Almeida --- tests/residuals/test_symbolicoperator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/residuals/test_symbolicoperator.py b/tests/residuals/test_symbolicoperator.py index ce3a0d0..eb5bad4 100644 --- a/tests/residuals/test_symbolicoperator.py +++ b/tests/residuals/test_symbolicoperator.py @@ -228,7 +228,8 @@ def test_symbolic_operator_diff_operators(self): def test_symbolic_operator_grad_operator(self): - f = "Grad(u, (x, y))" + f = "D(u, t) - alpha*D(u, (x, y))" + s = "Grad(D(u, t) - alpha*D(u, (x, y)), (x, y))" input_labels = ["x", "y"] output_labels = ["u"] @@ -248,14 +249,18 @@ def test_symbolic_operator_grad_operator(self): residual = SymbolicOperator( expressions=[f], + special_expressions=[s], input_vars=input_labels, constants={"alpha": 5}, output_vars=output_labels, function=net, engine="torch", ) + u = net(input_data=data) - assert all([isinstance(item, torch.Tensor) for item in residual(data)]) + feed_vars = {'x': data[:, 0], 'y': data[:,1], 'u': u} + + print(residual.process_special_expression(feed_vars)) def test_symbolic_operator_1d_pde(self): # Allen-Cahn equation From 5841823673d581dbc7337f1381a150f6ab82fa6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Sousa=20Almeida?= Date: Fri, 2 Aug 2024 17:27:33 -0300 Subject: [PATCH 8/9] Prepare dataset to be used together with symbolic expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Lucas de Sousa Almeida --- simulai/residuals/_pytorch_residuals.py | 36 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/simulai/residuals/_pytorch_residuals.py b/simulai/residuals/_pytorch_residuals.py index d7b3543..7aa1a78 100644 --- a/simulai/residuals/_pytorch_residuals.py +++ b/simulai/residuals/_pytorch_residuals.py @@ -139,9 +139,9 @@ def __init__( self.output = None - self.f_expressions = list() - self.g_expressions = dict() - self.h_expressions = list() + self.f_expressions = list() # Main expressions, as PDEs and ODEs + self.g_expressions = dict() # Auxiliary expressions, as boundary conditions + self.h_expressions = list() # Others auxiliary expressions, as those used to evaluate special loss functions self.feed_vars = None @@ -165,9 +165,11 @@ def __init__( else: gradient_function = gradient + # Diff symbol is related to automatic differentiation subs = {self.diff_symbol.name: gradient_function} subs.update(self.external_functions) subs.update(self.protected_funcs_subs) + subs.update(self.protected_operators_subs) for expr in self.expressions: if not callable(expr): @@ -438,9 +440,7 @@ def _process_expression_individual( return _process_expression_individual - def __call__( - self, inputs_data: Union[np.ndarray, dict] = None - ) -> List[torch.Tensor]: + def _create_input_for_eval(self, inputs_data: Union[np.ndarray, dict]=None) -> List[torch.Tensor]: """Evaluate the symbolic expression. This function takes either a numpy array or a dictionary of numpy arrays as input. @@ -457,6 +457,7 @@ def __call__( does: not match with the inputs_key attribute """ + constructor = MakeTensor( input_names=self.input_names, output_names=self.output_names ) @@ -490,6 +491,29 @@ def __call__( for inputs_list" ) + return outputs, inputs + + def __call__( + self, inputs_data: Union[np.ndarray, dict] = None + ) -> List[torch.Tensor]: + """Evaluate the symbolic expression. + + This function takes either a numpy array or a dictionary of numpy arrays as input. + + Args: + inputs_data (Union[np.ndarray, dict], optional): Union (Default value = None) + + Returns: + List[torch.Tensor]: List[torch.Tensor]: A list of tensors containing the evaluated expressions. + + Raises: + + Raises: + does: not match with the inputs_key attribute + + """ + outputs, inputs = self._create_input_for_eval(inputs_data=inputs_data) + feed_vars = {**outputs, **inputs} # It returns a list of tensors containing the expressions From c7eef6534ba754e1d360dcb82e1aec26663c094c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Sousa=20Almeida?= Date: Fri, 2 Aug 2024 17:28:34 -0300 Subject: [PATCH 9/9] Final test for special expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Lucas de Sousa Almeida --- tests/residuals/test_symbolicoperator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/residuals/test_symbolicoperator.py b/tests/residuals/test_symbolicoperator.py index eb5bad4..48d6d42 100644 --- a/tests/residuals/test_symbolicoperator.py +++ b/tests/residuals/test_symbolicoperator.py @@ -228,8 +228,9 @@ def test_symbolic_operator_diff_operators(self): def test_symbolic_operator_grad_operator(self): - f = "D(u, t) - alpha*D(u, (x, y))" - s = "Grad(D(u, t) - alpha*D(u, (x, y)), (x, y))" + f = "D(u, x) - D(u, y)" + s_1 = "D(D(u, x) - D(u, y), x)" + s_2 = "D(D(u, x) - D(u, y), y)" input_labels = ["x", "y"] output_labels = ["u"] @@ -249,18 +250,17 @@ def test_symbolic_operator_grad_operator(self): residual = SymbolicOperator( expressions=[f], - special_expressions=[s], + special_expressions=[s_1, s_2], input_vars=input_labels, - constants={"alpha": 5}, output_vars=output_labels, function=net, engine="torch", ) u = net(input_data=data) + outputs, inputs = residual._create_input_for_eval(inputs_data=data) + feed_vars = {**outputs, **inputs} - feed_vars = {'x': data[:, 0], 'y': data[:,1], 'u': u} - - print(residual.process_special_expression(feed_vars)) + all(isinstance(item, torch.Tensor) for item in residual.process_special_expression(feed_vars)) def test_symbolic_operator_1d_pde(self): # Allen-Cahn equation