diff --git a/hog/forms.py b/hog/forms.py index 8ae046393a7a8ad2b0330b12c960bb6eccfa7c1b..4a9439ad920f1796126b7f3085311ebe056adf36 100644 --- a/hog/forms.py +++ b/hog/forms.py @@ -33,7 +33,13 @@ from hog.fem_helpers import ( fem_function_on_element, fem_function_gradient_on_element, ) -from hog.function_space import FunctionSpace, N1E1Space, TrialSpace, TestSpace +from hog.function_space import ( + FunctionSpace, + N1E1Space, + TrialSpace, + TestSpace, + LagrangianFunctionSpace, +) from hog.math_helpers import dot, inv, abs, det, double_contraction from hog.quadrature import Quadrature, Tabulation from hog.symbolizer import Symbolizer @@ -321,6 +327,7 @@ def epsilon( component_test: int = 0, variable_viscosity: bool = True, coefficient_function_space: Optional[FunctionSpace] = None, + rotation_wrapper: bool = False, ) -> Form: docstring = f""" "Epsilon" operator. @@ -339,9 +346,11 @@ where ε(w) := (1/2) (∇w + (∇w)ᵀ) """ + if not variable_viscosity: raise HOGException("Constant viscosity currently not supported.") + from hog.recipes.integrands.volume.rotation import RotationType from hog.recipes.integrands.volume.epsilon import integrand from hog.recipes.integrands.volume.epsilon_affine import ( integrand as integrand_affine, @@ -363,6 +372,9 @@ where is_symmetric=trial == test, docstring=docstring, fe_coefficients={"mu": coefficient_function_space}, + rot_type=RotationType.PRE_AND_POST_MULTIPLY + if rotation_wrapper + else RotationType.NO_ROTATION, ) @@ -540,6 +552,7 @@ def divergence( symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), component_index: int = 0, + rotation_wrapper: bool = False, ) -> Form: docstring = f""" Divergence. @@ -556,6 +569,7 @@ Weak formulation """ from hog.recipes.integrands.volume.divergence import integrand + from hog.recipes.integrands.volume.rotation import RotationType return process_integrand( integrand, @@ -566,6 +580,9 @@ Weak formulation blending=blending, is_symmetric=False, docstring=docstring, + rot_type=RotationType.POST_MULTIPLY + if rotation_wrapper + else RotationType.NO_ROTATION, ) @@ -576,6 +593,7 @@ def gradient( symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), component_index: int = 0, + rotation_wrapper: bool = False, ) -> Form: docstring = f""" Gradient. @@ -592,6 +610,7 @@ def gradient( """ from hog.recipes.integrands.volume.gradient import integrand + from hog.recipes.integrands.volume.rotation import RotationType return process_integrand( integrand, @@ -602,6 +621,9 @@ def gradient( blending=blending, is_symmetric=False, docstring=docstring, + rot_type=RotationType.PRE_MULTIPLY + if rotation_wrapper + else RotationType.NO_ROTATION, ) @@ -767,6 +789,7 @@ The resulting matrix must be multiplied with a vector of ones to be used as the docstring=docstring, ) + def divdiv( trial: TrialSpace, test: TestSpace, @@ -844,11 +867,10 @@ Weak formulation blending=blending, is_symmetric=trial == test, docstring=docstring, - fe_coefficients={ - "k": coefficient_function_space - } + fe_coefficients={"k": coefficient_function_space}, ) + def advection( trial: TrialSpace, test: TestSpace, @@ -888,7 +910,9 @@ Weak formulation "ux": velocity_function_space, "uy": velocity_function_space, "uz": velocity_function_space, - } if constant_cp else { + } + if constant_cp + else { "ux": velocity_function_space, "uy": velocity_function_space, "uz": velocity_function_space, diff --git a/hog/integrand.py b/hog/integrand.py index 81586f34781bb8bda245bfa4b72aad9d83650b05..07313705041256cc2c0ffb8f2f35d62ee448dbbc 100644 --- a/hog/integrand.py +++ b/hog/integrand.py @@ -73,6 +73,7 @@ from hog.fem_helpers import ( trafo_ref_to_physical, ) from hog.math_helpers import inv, det +from hog.recipes.integrands.volume.rotation import RotationType @dataclass @@ -230,6 +231,7 @@ def process_integrand( boundary_geometry: ElementGeometry | None = None, fe_coefficients: Dict[str, Union[FunctionSpace, None]] | None = None, is_symmetric: bool = False, + rot_type: RotationType = RotationType.NO_ROTATION, docstring: str = "", ) -> Form: """ @@ -291,6 +293,8 @@ def process_integrand( finite-element function coefficients, they will be available to the callable as `k` supply None as the FunctionSpace for a std::function-type coeff (only works for old forms) :param is_symmetric: whether the bilinear form is symmetric - this is exploited by the generator + :param rot_type: whether the operator has to be wrapped with rotation matrix and the type of rotation that needs + to be applied, only applicable for Vectorial spaces :param docstring: documentation of the integrand/bilinear form - will end up in the docstring of the generated code """ @@ -375,10 +379,10 @@ def process_integrand( f"You cannot use the name {special_name_of_micromesh_coeff} for your FE coefficient." f"It is reserved." ) - fe_coefficients_modified[special_name_of_micromesh_coeff] = ( - TensorialVectorFunctionSpace( - LagrangianFunctionSpace(blending.degree, symbolizer) - ) + fe_coefficients_modified[ + special_name_of_micromesh_coeff + ] = TensorialVectorFunctionSpace( + LagrangianFunctionSpace(blending.degree, symbolizer) ) s.k = dict() @@ -491,6 +495,108 @@ def process_integrand( free_symbols_sorted = sorted(list(free_symbols), key=lambda x: str(x)) + if not rot_type == RotationType.NO_ROTATION: + if rot_type == RotationType.PRE_AND_POST_MULTIPLY: + if not trial == test: + HOGException( + "Trial and Test spaces must be the same for RotationType.PRE_AND_POST_MULTIPLY" + ) + + if not trial.is_vectorial: + raise HOGException( + "Rotation wrapper can only work with vectorial spaces" + ) + + rot_space = TensorialVectorFunctionSpace( + LagrangianFunctionSpace(trial.degree, symbolizer) + ) + + elif rot_type == RotationType.PRE_MULTIPLY: + if not test.is_vectorial: + raise HOGException( + "Rotation wrapper can only work with vectorial spaces" + ) + + rot_space = TensorialVectorFunctionSpace( + LagrangianFunctionSpace(test.degree, symbolizer) + ) + + elif rot_type == RotationType.POST_MULTIPLY: + if not trial.is_vectorial: + raise HOGException( + "Rotation wrapper can only work with vectorial spaces" + ) + + rot_space = TensorialVectorFunctionSpace( + LagrangianFunctionSpace(trial.degree, symbolizer) + ) + + from hog.recipes.integrands.volume.rotation import rotation_matrix + + normal_fspace = rot_space.component_function_space + + normals = ( + ["nx_rotation", "ny_rotation"] + if volume_geometry.dimensions == 2 + else ["nx_rotation", "ny_rotation", "nz_rotation"] + ) + + n_dof_symbols = [] + + for normal in normals: + if normal in fe_coefficients: + raise HOGException( + f"You cannot use the name {normal} for your FE coefficient." + f"It is reserved." + ) + if normal_fspace is None: + raise HOGException("Invalid normal function space") + else: + _, nc_dof_symbols = fem_function_on_element( + normal_fspace, + volume_geometry, + symbolizer, + domain="reference", + function_id=normal, + ) + + n_dof_symbols.append(nc_dof_symbols) + + rotmat = rotation_matrix( + rot_space.num_dofs(volume_geometry), + int(rot_space.num_dofs(volume_geometry) / volume_geometry.dimensions), + n_dof_symbols, + volume_geometry, + ) + + rot_doc_string = "" + + if rot_type == RotationType.PRE_AND_POST_MULTIPLY: + mat = rotmat * mat * rotmat.T + rot_doc_string = "RKRᵀ uᵣ" + elif rot_type == RotationType.PRE_MULTIPLY: + mat = rotmat * mat + rot_doc_string = "RK uᵣ" + elif rot_type == RotationType.POST_MULTIPLY: + mat = mat * rotmat.T + rot_doc_string = "KRᵀ uᵣ" + + docstring += f""" + +And the assembled FE matrix (K) is wrapped with a Rotation matrix (R) locally as below, + + {rot_doc_string} + +where + R : Rotation matrix calculated with the normal vector (n̂) at the DoF + uᵣ: FE function but the components rotated at the boundaries according to the normal FE function passed + + n̂ : normals (vectorial space: {normal_fspace}) + * The passed normal vector must be normalized + * The radial component of the rotated vector will be pointing in the given normal direction + * If the normals are zero at a DoF, the rotation matrix is identity matrix + """ + return Form( mat, tabulation, diff --git a/hog/recipes/integrands/volume/rotation.py b/hog/recipes/integrands/volume/rotation.py new file mode 100644 index 0000000000000000000000000000000000000000..f648988540bbfb867752a41513ebb4b58b98ece2 --- /dev/null +++ b/hog/recipes/integrands/volume/rotation.py @@ -0,0 +1,130 @@ +# HyTeG Operator Generator +# Copyright (C) 2024 HyTeG Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +from enum import Enum +from typing import List +from hog.recipes.common import * +from hog.exception import HOGException +from hog.element_geometry import ElementGeometry + + +class RotationType(Enum): + """ + This decides how the operator must be wrapped + + The global stiffness matrix (K) assembled from an operator would result in, + + NO_ROTATION - K + PRE_MULTIPLY - R K + POST_MULTIPLY - K R^T + PRE_AND_POST_MULTIPLY - R K R^T + """ + + NO_ROTATION = 0 + PRE_MULTIPLY = 1 + POST_MULTIPLY = 2 + PRE_AND_POST_MULTIPLY = 3 + + +def rotation_matrix( + mat_size: int, + mat_comp_size: int, + n_dof_symbols: List[List[sp.Symbol]], + geometry: ElementGeometry, +) -> sp.Matrix: + """ + The Rotation matrix is used to wrap the operators, especially to apply freeslip boundary conditions. + The matrix itself is calculated from the normal vectors according to the method from [Engelmann 1982] + + :param mat_size: Number of local DoF + :param mat_comp_size: Number of local DoF of each vector component + :param n_dof_symbols: Normal vector symbols packed as a list 2D/3D for each DoF + :param geometry: the geometry of the volume element + """ + + rotmat = sp.zeros(mat_size, mat_size) + + for row in range(mat_comp_size): + for col in range(mat_comp_size): + if row != col: + continue + + nx_ = n_dof_symbols[0][row] + ny_ = n_dof_symbols[1][row] + + if geometry.dimensions == 2: + n_ = sp.Matrix([[nx_], [ny_]]) + nnorm = sp.sqrt(nx_ * nx_ + ny_ * ny_) + rotmat_ = sp.Matrix([[-ny_, nx_], [nx_, ny_]]) + elif geometry.dimensions == 3: + nz_ = n_dof_symbols[2][row] + n_ = sp.Matrix([[nx_], [ny_], [nz_]]) + nnorm = sp.sqrt(nx_ * nx_ + ny_ * ny_ + nz_ * nz_) + + ex = sp.Matrix([[1.0], [0.0], [0.0]]) + ey = sp.Matrix([[0.0], [1.0], [0.0]]) + ez = sp.Matrix([[0.0], [0.0], [1.0]]) + + ncross = [n_.cross(ex), n_.cross(ey), n_.cross(ez)] + ncrossnorm = [nc_.norm() for nc_ in ncross] + + t1all = [ + n_.cross(ex).normalized(), + n_.cross(ey).normalized(), + n_.cross(ez).normalized(), + ] + + getrotmat = lambda t1: (t1.row_join(n_.cross(t1)).row_join(n_)).T + + machine_eps = 1e-10 + rotmat_ = sp.eye(geometry.dimensions) + for iRot in range(geometry.dimensions): + for jRot in range(geometry.dimensions): + rotmat_[iRot, jRot] = sp.Piecewise( + ( + getrotmat(t1all[0])[iRot, jRot], + sp.And( + ncrossnorm[0] + machine_eps > ncrossnorm[1], + ncrossnorm[0] + machine_eps > ncrossnorm[2], + ), + ), + ( + getrotmat(t1all[1])[iRot, jRot], + sp.And( + ncrossnorm[1] + machine_eps > ncrossnorm[0], + ncrossnorm[1] + machine_eps > ncrossnorm[2], + ), + ), + (getrotmat(t1all[2])[iRot, jRot], True), + ) + + else: + raise HOGException( + "We have not reached your dimensions, supreme being or little one" + ) + + rotmatid_ = sp.eye(geometry.dimensions) + + ndofs = mat_comp_size + + for iRot in range(geometry.dimensions): + for jRot in range(geometry.dimensions): + rotmat[row + iRot * ndofs, col + jRot * ndofs] = sp.Piecewise( + (rotmat_[iRot, jRot], nnorm > 0.5), + (rotmatid_[iRot, jRot], True), + ) + + return rotmat