diff --git a/generate_all_hyteg_forms.py b/generate_all_hyteg_forms.py index e59ef72ae8e198132ab6fd19282af32bd3a1ea7b..d96ca5defea2c8af6a16ac3616d77dc6eb526d6f 100644 --- a/generate_all_hyteg_forms.py +++ b/generate_all_hyteg_forms.py @@ -29,10 +29,14 @@ from hog.blending import GeometryMap, IdentityMap, ExternalMap, AnnulusMap from hog.element_geometry import ( TriangleElement, TetrahedronElement, - EmbeddedTriangle, ElementGeometry, ) -from hog.function_space import FunctionSpace, LagrangianFunctionSpace, N1E1Space +from hog.function_space import ( + LagrangianFunctionSpace, + N1E1Space, + TrialSpace, + TestSpace, +) from hog.forms import ( mass, diffusion, @@ -42,6 +46,7 @@ from hog.forms import ( pspg, linear_form, divergence, + gradient, full_stokes, divdiv, supg_diffusion, @@ -463,12 +468,12 @@ form_infos = [ quad_schemes={3: 3}, blending=ExternalMap(), ), - FormInfo("manifold_mass", trial_degree=1, test_degree=1, quad_schemes={3: 1}), + FormInfo("manifold_mass", trial_degree=1, test_degree=1, quad_schemes={2: 1}), FormInfo( "manifold_mass", trial_degree=1, test_degree=1, - quad_schemes={3: 3}, + quad_schemes={2: 3}, blending=ExternalMap(), ), ] @@ -609,7 +614,7 @@ for blending in [IdentityMap(), ExternalMap()]: "manifold_vector_mass", trial_degree=2, test_degree=2, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=3, is_implemented=is_implemented_for_vector_to_vector, @@ -622,7 +627,7 @@ for blending in [IdentityMap(), ExternalMap()]: "manifold_normal_penalty", trial_degree=2, test_degree=2, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=3, is_implemented=is_implemented_for_vector_to_vector, @@ -635,7 +640,7 @@ for blending in [IdentityMap(), ExternalMap()]: "manifold_epsilon", trial_degree=2, test_degree=2, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=3, is_implemented=is_implemented_for_vector_to_vector, @@ -648,7 +653,7 @@ for blending in [IdentityMap(), ExternalMap()]: "manifold_epsilon", trial_degree=2, test_degree=2, - quad_schemes={3: 6}, + quad_schemes={2: 6}, row_dim=3, col_dim=3, is_implemented=is_implemented_for_vector_to_vector, @@ -661,7 +666,7 @@ for blending in [IdentityMap(), ExternalMap()]: "vector_laplace_beltrami", trial_degree=2, test_degree=2, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=3, is_implemented=is_implemented_for_vector_to_vector, @@ -677,7 +682,7 @@ for trial_deg, test_deg, transpose in [(1, 2, True), (2, 1, False)]: "manifold_div", trial_degree=trial_deg, test_degree=test_deg, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=1, col_dim=3, is_implemented=is_implemented_for_vector_to_scalar, @@ -690,7 +695,7 @@ for trial_deg, test_deg, transpose in [(1, 2, True), (2, 1, False)]: "manifold_divt", trial_degree=trial_deg, test_degree=test_deg, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=1, is_implemented=is_implemented_for_scalar_to_vector, @@ -711,7 +716,7 @@ for trial_deg, test_deg, transpose in [ "manifold_vector_div", trial_degree=trial_deg, test_degree=test_deg, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=1, col_dim=3, is_implemented=is_implemented_for_vector_to_scalar, @@ -724,7 +729,7 @@ for trial_deg, test_deg, transpose in [ "manifold_vector_divt", trial_degree=trial_deg, test_degree=test_deg, - quad_schemes={3: 3}, + quad_schemes={2: 3}, row_dim=3, col_dim=1, is_implemented=is_implemented_for_scalar_to_vector, @@ -737,8 +742,8 @@ def form_func( name: str, row: int, col: int, - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, @@ -814,13 +819,12 @@ def form_func( elif name.startswith("divt"): if row not in [0, 1, 2] or col != 0: raise HOGException("Invalid call to divt form.") - return divergence( + return gradient( trial, test, geometry, symbolizer, component_index=row, - transpose=True, blending=blending, ).integrate(quad, symbolizer) elif name.startswith("divdiv"): @@ -836,7 +840,6 @@ def form_func( geometry, symbolizer, component_index=col, - transpose=False, blending=blending, ).integrate(quad, symbolizer) elif name.startswith("supg_d"): @@ -1056,7 +1059,7 @@ def main(): geometries = [TetrahedronElement()] elif args.geometry == "embedded_triangle": logger.info(f"- selected geometry: embedded triangle") - geometries = [EmbeddedTriangle()] + geometries = [TriangleElement(space_dimension=3)] else: logger.info(f"- selected geometries: triangle, tetrahedron") geometries = [TriangleElement(), TetrahedronElement()] @@ -1071,17 +1074,19 @@ def main(): for form_info in filtered_form_infos: logger.info(f"{form_info}") - trial: FunctionSpace + trial: TrialSpace if form_info.trial_family == "N1E1": - trial = N1E1Space(symbolizer) + trial = TrialSpace(N1E1Space(symbolizer)) else: - trial = LagrangianFunctionSpace(form_info.trial_degree, symbolizer) + trial = TrialSpace( + LagrangianFunctionSpace(form_info.trial_degree, symbolizer) + ) - test: FunctionSpace + test: TestSpace if form_info.test_family == "N1E1": - test = N1E1Space(symbolizer) + test = TestSpace(N1E1Space(symbolizer)) else: - test = LagrangianFunctionSpace(form_info.test_degree, symbolizer) + test = TestSpace(LagrangianFunctionSpace(form_info.test_degree, symbolizer)) form_classes = [] diff --git a/generate_all_operators.py b/generate_all_operators.py index a3a4bf0a71117e8ea8d7c01d0d3afb1f5351f74d..6ce4a12dcab8ddddd60fb2e05107591d8415a84a 100644 --- a/generate_all_operators.py +++ b/generate_all_operators.py @@ -29,13 +29,19 @@ import sympy as sp from sympy.core.cache import clear_cache from tabulate import tabulate -from hog.blending import GeometryMap, IdentityMap, AnnulusMap, IcosahedralShellMap +from hog.blending import GeometryMap, IdentityMap, AnnulusMap, IcosahedralShellMap, ParametricMap from hog.cse import CseImplementation -from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement +from hog.element_geometry import ( + ElementGeometry, + TriangleElement, + TetrahedronElement, + LineElement, +) from hog.exception import HOGException from hog.forms import ( diffusion, divergence, + gradient, div_k_grad, shear_heating, epsilon, @@ -43,20 +49,30 @@ from hog.forms import ( nonlinear_diffusion, nonlinear_diffusion_newton_galerkin, supg_diffusion, + advection, + supg_advection, + grad_rho_by_rho_dot_u, +) +from hog.forms_boundary import ( + mass_boundary, + freeslip_gradient_weak_boundary, + freeslip_divergence_weak_boundary, + freeslip_momentum_weak_boundary, ) from hog.forms_vectorial import curl_curl, curl_curl_plus_mass, mass_n1e1 from hog.function_space import ( - FunctionSpace, LagrangianFunctionSpace, N1E1Space, TensorialVectorFunctionSpace, + TrialSpace, + TestSpace, ) from hog.logger import get_logger, TimedLogger from hog.operator_generation.kernel_types import ( - Apply, - Assemble, - AssembleDiagonal, - KernelType, + ApplyWrapper, + AssembleWrapper, + AssembleDiagonalWrapper, + KernelWrapperType, ) from hog.operator_generation.loop_strategies import ( LoopStrategy, @@ -81,11 +97,14 @@ from generate_all_hyteg_forms import valid_base_dir from hog.operator_generation.types import parse_argument_type, HOGType, HOGPrecision import quadpy from hog.quadrature.quadrature import select_quadrule +from hog.integrand import Form ALL_GEOMETRY_MAPS = [ IdentityMap(), AnnulusMap(), IcosahedralShellMap(), + ParametricMap(1), + ParametricMap(2), ] @@ -354,7 +373,7 @@ def main(): args.quad_degree if args.quad_rule is None else args.quad_rule, ) } - + enabled_geometries: Set[TriangleElement | TetrahedronElement] = set() if 2 in args.dimensions: enabled_geometries.add(TriangleElement()) @@ -439,6 +458,7 @@ def main(): loop, blending, quad, + quad, type_descriptor=type_descriptor, ) @@ -475,20 +495,37 @@ def all_opts_both_cses() -> List[Tuple[Set[Opts], LoopStrategy, str]]: class OperatorInfo: mapping: str name: str - trial_space: FunctionSpace - test_space: FunctionSpace - form: Callable[ - [ - FunctionSpace, - FunctionSpace, - ElementGeometry, - Symbolizer, - GeometryMap, - ], - sp.Matrix, - ] + trial_space: TrialSpace + test_space: TestSpace + form: ( + Callable[ + [ + TrialSpace, + TestSpace, + ElementGeometry, + Symbolizer, + GeometryMap, + ], + Form, + ] + | None + ) type_descriptor: HOGType - kernel_types: List[KernelType] = None # type: ignore[assignment] # will definitely be initialized in __post_init__ + form_boundary: ( + Callable[ + [ + TrialSpace, + TestSpace, + ElementGeometry, + ElementGeometry, + Symbolizer, + GeometryMap, + ], + Form, + ] + | None + ) = None + kernel_types: List[KernelWrapperType] = None # type: ignore[assignment] # will definitely be initialized in __post_init__ geometries: Sequence[ElementGeometry] = field( default_factory=lambda: [TriangleElement(), TetrahedronElement()] ) @@ -507,9 +544,9 @@ class OperatorInfo: if self.kernel_types is None: dims = [g.dimensions for g in self.geometries] self.kernel_types = [ - Apply( - self.test_space, + ApplyWrapper( self.trial_space, + self.test_space, type_descriptor=self.type_descriptor, dims=dims, ) @@ -518,9 +555,9 @@ class OperatorInfo: all_opts = set().union(*[o for (o, _, _) in self.opts]) if not ({Opts.VECTORIZE, Opts.VECTORIZE512}.intersection(all_opts)): self.kernel_types.append( - Assemble( - self.test_space, + AssembleWrapper( self.trial_space, + self.test_space, type_descriptor=self.type_descriptor, dims=dims, ) @@ -528,8 +565,10 @@ class OperatorInfo: if self.test_space == self.trial_space: self.kernel_types.append( - AssembleDiagonal( - self.test_space, type_descriptor=self.type_descriptor, dims=dims + AssembleDiagonalWrapper( + self.trial_space, + type_descriptor=self.type_descriptor, + dims=dims, ) ) @@ -554,44 +593,60 @@ def all_operators( # fmt: off # TODO switch to manual specification of opts for now/developement, later use default factory - ops.append(OperatorInfo(mapping="N1E1", name="CurlCurl", trial_space=N1E1, test_space=N1E1, form=curl_curl, + ops.append(OperatorInfo("N1E1", "CurlCurl", TrialSpace(N1E1), TestSpace(N1E1), form=curl_curl, type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="N1E1", name="Mass", trial_space=N1E1, test_space=N1E1, form=mass_n1e1, + ops.append(OperatorInfo("N1E1", "Mass", TrialSpace(N1E1), TestSpace(N1E1), form=mass_n1e1, type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="N1E1", name="CurlCurlPlusMass", trial_space=N1E1, test_space=N1E1, + ops.append(OperatorInfo("N1E1", "CurlCurlPlusMass", TrialSpace(N1E1), TestSpace(N1E1), form=partial(curl_curl_plus_mass, alpha_fem_space=P1, beta_fem_space=P1), type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P1", name="Diffusion", trial_space=P1, test_space=P1, form=diffusion, + ops.append(OperatorInfo("P1", "Diffusion", TrialSpace(P1), TestSpace(P1), form=diffusion, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P1", name="DivKGrad", trial_space=P1, test_space=P1, + ops.append(OperatorInfo("P1", "DivKGrad", TrialSpace(P1), TestSpace(P1), form=partial(div_k_grad, coefficient_function_space=P1), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P2", name="Diffusion", trial_space=P2, test_space=P2, form=diffusion, + ops.append(OperatorInfo("P2", "Diffusion", TrialSpace(P2), TestSpace(P2), form=diffusion, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P2", name="DivKGrad", trial_space=P2, test_space=P2, + ops.append(OperatorInfo("P2", "DivKGrad", TrialSpace(P2), TestSpace(P2), form=partial(div_k_grad, coefficient_function_space=P2), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P2", name="ShearHeating", trial_space=P2, test_space=P2, + ops.append(OperatorInfo("P2", "ShearHeating", TrialSpace(P2), TestSpace(P2), form=partial(shear_heating, viscosity_function_space=P2, velocity_function_space=P2), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P1", name="NonlinearDiffusion", trial_space=P1, test_space=P1, + ops.append(OperatorInfo("P1", "NonlinearDiffusion", TrialSpace(P1), TestSpace(P1), form=partial(nonlinear_diffusion, coefficient_function_space=P1), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P1", name="NonlinearDiffusionNewtonGalerkin", trial_space=P1, - test_space=P1, form=partial(nonlinear_diffusion_newton_galerkin, - coefficient_function_space=P1, onlyNewtonGalerkinPartOfForm=False), + ops.append(OperatorInfo("P1", "NonlinearDiffusionNewtonGalerkin", TrialSpace(P1), + TestSpace(P1), form=partial(nonlinear_diffusion_newton_galerkin, + coefficient_function_space=P1, only_newton_galerkin_part_of_form=False), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P1Vector", name="Diffusion", trial_space=P1Vector, test_space=P1Vector, + ops.append(OperatorInfo("P1Vector", "Diffusion", TrialSpace(P1Vector), TestSpace(P1Vector), form=diffusion, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping="P2", name="SUPGDiffusion", trial_space=P2, test_space=P2, + ops.append(OperatorInfo("P2", "SUPGDiffusion", TrialSpace(P2), TestSpace(P2), form=partial(supg_diffusion, velocity_function_space=P2, diffusivityXdelta_function_space=P2), type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) + ops.append(OperatorInfo("P2", "AdvectionConstCp", TrialSpace(P2), TestSpace(P2), + form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2, constant_cp = True), + type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) + + ops.append(OperatorInfo("P2", "AdvectionVarCp", TrialSpace(P2), TestSpace(P2), + form=partial(advection, velocity_function_space=P2, coefficient_function_space=P2), + type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) + + ops.append(OperatorInfo("P2", "SUPGAdvection", TrialSpace(P2), TestSpace(P2), + form=partial(supg_advection, velocity_function_space=P2, coefficient_function_space=P2), + type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) + + ops.append(OperatorInfo("P2", "BoundaryMass", TrialSpace(P2), TestSpace(P2), form=None, + form_boundary=mass_boundary, type_descriptor=type_descriptor, geometries=list(geometries), + opts=opts, blending=blending)) + # fmt: on p2vec_epsilon = partial( @@ -601,10 +656,10 @@ def all_operators( ) ops.append( OperatorInfo( - mapping=f"P2Vector", - name=f"Epsilon", - trial_space=P2Vector, - test_space=P2Vector, + f"P2Vector", + f"Epsilon", + TrialSpace(P2Vector), + TestSpace(P2Vector), form=p2vec_epsilon, type_descriptor=type_descriptor, geometries=list(geometries), @@ -613,18 +668,87 @@ def all_operators( ) ) + p2vec_freeslip_momentum_weak_boundary = partial( + freeslip_momentum_weak_boundary, function_space_mu=P2 + ) + + ops.append( + OperatorInfo( + f"P2Vector", + f"EpsilonFreeslip", + TrialSpace(P2Vector), + TestSpace(P2Vector), + form=p2vec_epsilon, + form_boundary=p2vec_freeslip_momentum_weak_boundary, + type_descriptor=type_descriptor, + geometries=list(geometries), + opts=opts, + blending=blending, + ) + ) + + ops.append( + OperatorInfo( + f"P2VectorToP1", + f"DivergenceFreeslip", + TrialSpace(P2Vector), + TestSpace(P1), + form=divergence, + form_boundary=freeslip_divergence_weak_boundary, + type_descriptor=type_descriptor, + geometries=list(geometries), + opts=opts, + blending=blending, + ) + ) + + ops.append( + OperatorInfo( + f"P1ToP2Vector", + f"GradientFreeslip", + TrialSpace(P1), + TestSpace(P2Vector), + form=gradient, + form_boundary=freeslip_gradient_weak_boundary, + type_descriptor=type_descriptor, + geometries=list(geometries), + opts=opts, + blending=blending, + ) + ) + + p2vec_grad_rho = partial( + grad_rho_by_rho_dot_u, + density_function_space=P2, + ) + ops.append( + OperatorInfo( + f"P2VectorToP1", + f"GradRhoByRhoDotU", + TrialSpace(P1), + TestSpace(P2Vector), + form=p2vec_grad_rho, + type_descriptor=type_descriptor, + geometries=list(geometries), + opts=opts, + blending=blending, + ) + ) + for c in [0, 1, 2]: # fmt: off if c == 2: div_geometries = three_d else: div_geometries = list(geometries) - ops.append(OperatorInfo(mapping=f"P2ToP1", name=f"Div_{c}", trial_space=P1, test_space=P2, - form=partial(divergence, transpose=False, component_index=c), + ops.append(OperatorInfo(f"P2ToP1", f"Div_{c}", + TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), TestSpace(P1), + form=partial(divergence, component_index=c), type_descriptor=type_descriptor, opts=opts, geometries=div_geometries, blending=blending)) - ops.append(OperatorInfo(mapping=f"P1ToP2", name=f"DivT_{c}", trial_space=P2, test_space=P1, - form=partial(divergence, transpose=True, component_index=c), + ops.append(OperatorInfo(f"P1ToP2", f"DivT_{c}", TrialSpace(P1), + TestSpace(TensorialVectorFunctionSpace(P2, single_component=c)), + form=partial(gradient, component_index=c), type_descriptor=type_descriptor, opts=opts, geometries=div_geometries, blending=blending)) # fmt: on @@ -648,11 +772,16 @@ def all_operators( ) # fmt: off ops.append( - OperatorInfo(mapping=f"P2", name=f"Epsilon_{r}_{c}", trial_space=P2, test_space=P2, form=p2_epsilon, - type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, blending=blending)) - ops.append(OperatorInfo(mapping=f"P2", name=f"FullStokes_{r}_{c}", trial_space=P2, test_space=P2, - form=p2_full_stokes, type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, - blending=blending)) + OperatorInfo(f"P2", f"Epsilon_{r}_{c}", + TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), + TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon, + type_descriptor=type_descriptor, geometries=list(geometries), opts=opts, + blending=blending)) + ops.append(OperatorInfo(f"P2", f"FullStokes_{r}_{c}", + TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), + TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), + form=p2_full_stokes, type_descriptor=type_descriptor, geometries=list(geometries), + opts=opts, blending=blending)) # fmt: on for c, r in [(0, 2), (1, 2), (2, 2), (2, 1), (2, 0)]: p2_epsilon = partial( @@ -672,11 +801,13 @@ def all_operators( ) # fmt: off ops.append( - OperatorInfo(mapping=f"P2", name=f"Epsilon_{r}_{c}", trial_space=P2, test_space=P2, form=p2_epsilon, + OperatorInfo(f"P2", f"Epsilon_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), + TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_epsilon, type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) ops.append( - OperatorInfo(mapping=f"P2", name=f"FullStokes_{r}_{c}", trial_space=P2, test_space=P2, form=p2_full_stokes, - type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) + OperatorInfo(f"P2", f"FullStokes_{r}_{c}", TrialSpace(TensorialVectorFunctionSpace(P2, single_component=c)), + TestSpace(TensorialVectorFunctionSpace(P2, single_component=r)), form=p2_full_stokes, + type_descriptor=type_descriptor, geometries=three_d, opts=opts, blending=blending)) # fmt: on # Removing all operators without viable element types (e.g.: some ops only support 2D, but a blending map maybe only @@ -695,6 +826,7 @@ def generate_elementwise_op( loop_strategy: LoopStrategy, blending: GeometryMap, quad_info: dict[ElementGeometry, int | str], + quad_info_boundary: dict[ElementGeometry, int | str], type_descriptor: HOGType, ) -> None: """Generates a single operator and writes it to the HyTeG directory.""" @@ -702,8 +834,7 @@ def generate_elementwise_op( operator = HyTeGElementwiseOperator( name, symbolizer, - opts=optimizations, - kernel_types=op_info.kernel_types, + kernel_wrapper_types=op_info.kernel_types, type_descriptor=type_descriptor, ) @@ -712,32 +843,65 @@ def generate_elementwise_op( if geometry not in blending.supported_geometries(): continue - quad = Quadrature( - select_quadrule(quad_info[geometry], geometry), - geometry, - ) + if op_info.form is not None: + quad = Quadrature( + select_quadrule(quad_info[geometry], geometry), + geometry, + ) - form = op_info.form( - op_info.test_space, - op_info.trial_space, - geometry, - symbolizer, - blending=blending, # type: ignore[call-arg] # kw-args are not supported by Callable - ) + form = op_info.form( + op_info.trial_space, + op_info.test_space, + geometry, + symbolizer, + blending=blending, # type: ignore[call-arg] # kw-args are not supported by Callable + ) - operator.set_element_matrix( - dim=geometry.dimensions, - geometry=geometry, - integration_domain=MacroIntegrationDomain.VOLUME, - quad=quad, - blending=blending, - form=form, - ) + operator.add_volume_integral( + name="".join(name.split()), + volume_geometry=geometry, + quad=quad, + blending=blending, + form=form, + loop_strategy=loop_strategy, + optimizations=optimizations, + ) + + if op_info.form_boundary is not None: + boundary_geometry: ElementGeometry + if geometry == TriangleElement(): + boundary_geometry = LineElement(space_dimension=2) + elif geometry == TetrahedronElement(): + boundary_geometry = TriangleElement(space_dimension=3) + else: + raise HOGException("Invalid volume geometry for boundary integral.") + + quad = Quadrature( + select_quadrule(quad_info_boundary[geometry], boundary_geometry), + boundary_geometry, + ) + + form_boundary = op_info.form_boundary( + op_info.trial_space, + op_info.test_space, + geometry, + boundary_geometry, + symbolizer, + blending=blending, # type: ignore[call-arg] # kw-args are not supported by Callable + ) + + operator.add_boundary_integral( + name="".join(name.split()), + volume_geometry=geometry, + quad=quad, + blending=blending, + form=form_boundary, + optimizations=set(), + ) dir_path = os.path.join(args.output, op_info.name.split("_")[0]) operator.generate_class_code( dir_path, - loop_strategy=loop_strategy, clang_format_binary=args.clang_format_binary, ) diff --git a/hog/blending.py b/hog/blending.py index 7a8f884e0924f6882fdf65593e416c38fe0c1bf8..63478a11fe9b88e99f8bb5be6d44564b0aed03fd 100644 --- a/hog/blending.py +++ b/hog/blending.py @@ -49,7 +49,7 @@ class GeometryMap: def jacobian(self, x: sp.Matrix) -> sp.Matrix: """Evaluates the Jacobian of the geometry map at the passed point.""" raise HOGException("jacobian() not implemented for this map.") - + def hessian(self, x: sp.Matrix) -> List[sp.Matrix]: """Evaluates the hessian of the geometry map at the passed point.""" raise HOGException("hessian() not implemented for this map.") @@ -100,6 +100,48 @@ class ExternalMap(GeometryMap): return [LineElement(), TriangleElement(), TetrahedronElement()] +class ParametricMap(GeometryMap): + """ + This is a special map that indicates that you want a parametric mapping. + + It uses HyTeG's MicroMesh implementation and introduces a vector finite element coefficient that represents the + coordinates at each node. Locally, the Jacobians are computed through the gradients of the shape functions. + + Depending on your choice of polynomial degree, you can use this blending map to construct sub-, super-, and + isoparametric mappings. + + The affine Jacobians in all integrands will automatically be set to the identity if you use this map. + The blending Jacobians will be set to the transpose of the gradient of the shape functions. + """ + + def __init__(self, degree: int): + + if degree not in [1, 2]: + raise HOGException( + "Only first and second order parametric maps are supported." + ) + + self.degree = degree + + def supported_geometries(self) -> List[ElementGeometry]: + return [TriangleElement(), TetrahedronElement()] + + def parameter_coupling_code(self) -> str: + return "" + + def coupling_includes(self) -> List[str]: + return [ + "hyteg/p1functionspace/P1VectorFunction.hpp", + "hyteg/p2functionspace/P2VectorFunction.hpp", + ] + + def __eq__(self, other: Any) -> bool: + return type(self) is type(other) and self.degree == other.degree + + def __str__(self): + return self.__class__.__name__ + f"P{self.degree}" + + class AnnulusMap(GeometryMap): """Projects HyTeG's approximate annulus to the actual curved geometry. @@ -148,7 +190,7 @@ class AnnulusMap(GeometryMap): """Evaluates the Jacobian of the geometry map at the passed point.""" if sp.shape(x) != (2, 1): - raise HOGException("Invalid input shape for AnnulusMap.") + raise HOGException(f"Invalid input shape {sp.shape(x)} for AnnulusMap.") radRefVertex = self.radRefVertex radRayVertex = self.radRayVertex diff --git a/hog/code_generation.py b/hog/code_generation.py index e20e3197b26b988b002ed119d87ff6e4c9a0fd09..afa5aaea38a5a0a1a11b1ec72239c4cb747b9234 100644 --- a/hog/code_generation.py +++ b/hog/code_generation.py @@ -160,9 +160,9 @@ def replace_multi_assignments( # Actually filling the dict. for ma in multi_assignments: replacement_symbol = replacement_symbols[ma.output_arg()] - multi_assignments_replacement_symbols[ - ma.unique_identifier - ] = replacement_symbol + multi_assignments_replacement_symbols[ma.unique_identifier] = ( + replacement_symbol + ) if multi_assignments_replacement_symbols: with TimedLogger( @@ -238,9 +238,15 @@ def jacobi_matrix_assignments( assignments = [] - jac_aff_symbol = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_aff_inv_symbol = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_aff_det_symbol = symbolizer.abs_det_jac_ref_to_affine() + # If the reference space dimension (geometry.dimension) is not equal to the space dimension + # the Jacobian of the affine map is not square. Processing its inverse and determinant is + # thus not meaningful. + is_affine_jac_square = geometry.dimensions == geometry.space_dimension + + jac_aff_symbol = symbolizer.jac_ref_to_affine(geometry) + if is_affine_jac_square: + jac_aff_inv_symbol = symbolizer.jac_ref_to_affine_inv(geometry) + jac_aff_det_symbol = symbolizer.abs_det_jac_ref_to_affine() free_symbols = element_matrix.free_symbols | { free_symbol @@ -250,27 +256,28 @@ def jacobi_matrix_assignments( } # Steps 1 and 2. - jac_affine_inv_in_expr = set(jac_aff_inv_symbol).intersection(free_symbols) - abs_det_jac_affine_in_expr = jac_aff_det_symbol in free_symbols - - # Just an early exit. Not strictly required, but might accelerate this process in some cases. - if jac_affine_inv_in_expr: - jac_aff_inv_expr = jac_aff_symbol.inv() - for s_ij, e_ij in zip(jac_aff_inv_symbol, jac_aff_inv_expr): - if s_ij in jac_affine_inv_in_expr: - assignments.append(SympyAssignment(s_ij, e_ij)) + if is_affine_jac_square: + jac_affine_inv_in_expr = set(jac_aff_inv_symbol).intersection(free_symbols) + abs_det_jac_affine_in_expr = jac_aff_det_symbol in free_symbols + + if jac_affine_inv_in_expr: + jac_aff_inv_expr = jac_aff_symbol.inv() + for s_ij, e_ij in zip(jac_aff_inv_symbol, jac_aff_inv_expr): + if s_ij in jac_affine_inv_in_expr: + assignments.append(SympyAssignment(s_ij, e_ij)) - if abs_det_jac_affine_in_expr: - assignments.append( - SympyAssignment(jac_aff_det_symbol, sp.Abs(jac_aff_symbol.det())) - ) + if abs_det_jac_affine_in_expr: + assignments.append( + SympyAssignment(jac_aff_det_symbol, sp.Abs(jac_aff_symbol.det())) + ) - # Collecting all expressions to parse for step 3. - free_symbols |= {free_symbol for a in assignments for free_symbol in a.rhs.atoms()} + # Collecting all expressions to parse for step 3. + free_symbols |= { + free_symbol for a in assignments for free_symbol in a.rhs.atoms() + } jac_affine_in_expr = set(jac_aff_symbol).intersection(free_symbols) - # Just an early exit. Not strictly required, but might accelerate this process in some cases. if jac_affine_in_expr: jac_aff_expr = jac_ref_to_affine(geometry, symbolizer, affine_points) for s_ij, e_ij in zip(jac_aff_symbol, jac_aff_expr): @@ -335,7 +342,6 @@ def blending_jacobi_matrix_assignments( jac_blending_in_expr = set(jac_blend_symbol).intersection(free_symbols) - # Just an early exit. Not strictly required, but might accelerate this process in some cases. if jac_blending_in_expr: if isinstance(blending, ExternalMap): HOGException("Not implemented or cannot be?") @@ -344,7 +350,7 @@ def blending_jacobi_matrix_assignments( # Collecting all expressions to parse for step 3. spat_coord_subs = {} for idx, symbol in enumerate( - symbolizer.ref_coords_as_list(geometry.dimensions) + symbolizer.ref_coords_as_list(quad_info.geometry.dimensions) ): spat_coord_subs[symbol] = point[idx] jac_blend_expr_sub = jac_blend_expr.subs(spat_coord_subs) diff --git a/hog/dof_symbol.py b/hog/dof_symbol.py index 97d33d48dda3c73d440b1662d41a88894c42af67..383b30789ba13734824741be8d393c4ae5e83462 100644 --- a/hog/dof_symbol.py +++ b/hog/dof_symbol.py @@ -14,6 +14,7 @@ # 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 copy import deepcopy import sympy as sp from hog.function_space import FunctionSpace from hog.element_geometry import ElementGeometry @@ -46,3 +47,11 @@ class DoFSymbol(sp.Symbol): obj.dof_id = dof_id obj.function_id = function_id return obj + + def __deepcopy__(self, memo): + return DoFSymbol( + deepcopy(self.name), + deepcopy(self.function_space), + deepcopy(self.dof_id), + deepcopy(self.function_id), + ) diff --git a/hog/element_geometry.py b/hog/element_geometry.py index b69c3e95945c5cdf59ee4e05e46f7d807b9067cd..9fe91fc1b7a7e0f886003a20beec765e4f0862c7 100644 --- a/hog/element_geometry.py +++ b/hog/element_geometry.py @@ -14,69 +14,68 @@ # 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 hog.exception import HOGException + class ElementGeometry: - def __init__(self, dimensions: int, num_vertices: int): + def __init__(self, dimensions: int, num_vertices: int, space_dimension: int): + + if space_dimension < dimensions: + raise HOGException( + "The space dimension should be larger or equal to the dimension of the geometry." + ) + self.dimensions = dimensions self.num_vertices = num_vertices + self.space_dimension = space_dimension def __str__(self): - return f"ElementGeometry(dim: {self.dimensions}, vertices: {self.num_vertices})" + return f"ElementGeometry(dim: {self.dimensions}, vertices: {self.num_vertices}), space dim: {self.space_dimension}" def __repr__(self): return str(self) def __hash__(self): - return hash((self.dimensions, self.num_vertices)) + return hash((self.dimensions, self.num_vertices, self.space_dimension)) def __eq__(self, other): if isinstance(other, ElementGeometry): return ( self.dimensions == other.dimensions and self.num_vertices == other.num_vertices + and self.space_dimension == other.space_dimension ) return False class LineElement(ElementGeometry): - def __init__(self): - super().__init__(1, 2) + def __init__(self, space_dimension: int = 1): + super().__init__(1, 2, space_dimension=space_dimension) def __str__(self): - return f"line, dim: 1, vertices: 2" + return f"line, dim: 1, vertices: 2, spacedim: {self.space_dimension}" def __repr__(self): return str(self) class TriangleElement(ElementGeometry): - def __init__(self): - super().__init__(2, 3) - - def __str__(self): - return f"triangle, dim: 2, vertices: 3" - - def __repr__(self): - return str(self) - - -class EmbeddedTriangle(ElementGeometry): - def __init__(self): - super().__init__(3, 3) + def __init__(self, space_dimension: int = 2): + super().__init__(2, 3, space_dimension=space_dimension) def __str__(self): - return f"embedded triangle, dim: 3, vertices: 3" + return f"triangle, dim: 2, vertices: 3, spacedim: {self.space_dimension}" def __repr__(self): return str(self) class TetrahedronElement(ElementGeometry): - def __init__(self): - super().__init__(3, 4) + def __init__(self, space_dimension: int = 3): + super().__init__(3, 4, space_dimension=space_dimension) def __str__(self): - return f"tetrahedron, dim: 3, vertices: 4" + return f"tetrahedron, dim: 3, vertices: 4, spacedim: {self.space_dimension}" def __repr__(self): return str(self) diff --git a/hog/fem_helpers.py b/hog/fem_helpers.py index a6a525fbb850ea8cdc317ec794c428d6c40c8bbe..56e0fae22664f0dc375c988dc26ce7305d6bd337 100644 --- a/hog/fem_helpers.py +++ b/hog/fem_helpers.py @@ -22,28 +22,24 @@ from hog.blending import ( GeometryMap, ExternalMap, IdentityMap, - AnnulusMap, - IcosahedralShellMap, + ParametricMap, ) from hog.element_geometry import ( ElementGeometry, TriangleElement, - EmbeddedTriangle, TetrahedronElement, LineElement, ) from hog.exception import HOGException -from hog.function_space import FunctionSpace +from hog.function_space import FunctionSpace, TrialSpace, TestSpace from hog.math_helpers import inv, det from hog.multi_assignment import MultiAssignment from hog.symbolizer import Symbolizer from hog.external_functions import ( BlendingFTriangle, - BlendingFEmbeddedTriangle, BlendingFTetrahedron, BlendingDFTetrahedron, BlendingDFTriangle, - BlendingDFInvDFTriangle, BlendingDFEmbeddedTriangle, ScalarVariableCoefficient2D, ScalarVariableCoefficient3D, @@ -53,7 +49,7 @@ from hog.dof_symbol import DoFSymbol def create_empty_element_matrix( - trial: FunctionSpace, test: FunctionSpace, geometry: ElementGeometry + trial: TrialSpace, test: TestSpace, geometry: ElementGeometry ) -> sp.Matrix: """ Returns a sympy matrix of the required size corresponding to the trial and test spaces, initialized with zeros. @@ -76,7 +72,7 @@ class ElementMatrixData: def element_matrix_iterator( - trial: FunctionSpace, test: FunctionSpace, geometry: ElementGeometry + trial: TrialSpace, test: TestSpace, geometry: ElementGeometry ) -> Iterator[ElementMatrixData]: """Call this to create a generator to conveniently fill the element matrix.""" for row, (psi, grad_psi, hessian_psi) in enumerate( @@ -118,12 +114,10 @@ def trafo_ref_to_affine( vector symbols, useful for example if the trafo of two or more different elements is required """ ref_symbols_vector = symbolizer.ref_coords_as_vector(geometry.dimensions) - if isinstance(geometry, EmbeddedTriangle): - ref_symbols_vector = symbolizer.ref_coords_as_vector(geometry.dimensions - 1) if affine_points is None: affine_points = symbolizer.affine_vertices_as_vectors( - geometry.dimensions, geometry.num_vertices + geometry.space_dimension, geometry.num_vertices ) else: if len(affine_points) != geometry.num_vertices: @@ -135,12 +129,12 @@ def trafo_ref_to_affine( affine_points[p][d] - affine_points[0][d] for p in range(1, geometry.num_vertices) ] - for d in range(geometry.dimensions) + for d in range(geometry.space_dimension) ] ) trafo = trafo * ref_symbols_vector trafo = trafo + sp.Matrix( - [[affine_points[0][d]] for d in range(geometry.dimensions)] + [[affine_points[0][d]] for d in range(geometry.space_dimension)] ) return trafo @@ -199,8 +193,6 @@ def jac_ref_to_affine( vector symbols, useful for example if the trafo of two or more different elements is required """ ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - if isinstance(geometry, EmbeddedTriangle): - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions - 1) trafo = trafo_ref_to_affine(geometry, symbolizer, affine_points=affine_points) return trafo.jacobian(ref_symbols_list) @@ -214,14 +206,17 @@ def trafo_ref_to_physical( if geometry not in blending.supported_geometries(): raise HOGException("Geometry not supported by blending map.") + if isinstance(blending, ParametricMap): + raise HOGException("Evaluation not implemented for parametric maps.") + t = trafo_ref_to_affine(geometry, symbolizer) if isinstance(blending, ExternalMap): blending_class: type[MultiAssignment] if isinstance(geometry, TriangleElement): blending_class = BlendingFTriangle - elif isinstance(geometry, EmbeddedTriangle): - blending_class = BlendingDFEmbeddedTriangle + if geometry.space_dimension == 3: + blending_class = BlendingDFEmbeddedTriangle elif isinstance(geometry, TetrahedronElement): blending_class = BlendingFTetrahedron else: @@ -246,15 +241,15 @@ def jac_affine_to_physical( blending_class: type[MultiAssignment] if isinstance(geometry, TriangleElement): blending_class = BlendingDFTriangle - elif isinstance(geometry, EmbeddedTriangle): - blending_class = BlendingDFEmbeddedTriangle + if geometry.space_dimension == 3: + blending_class = BlendingDFEmbeddedTriangle elif isinstance(geometry, TetrahedronElement): blending_class = BlendingDFTetrahedron else: raise HOGException("Blending not implemented for the passed element geometry.") t = trafo_ref_to_affine(geometry, symbolizer) - jac = sp.zeros(geometry.dimensions, geometry.dimensions) + jac = sp.zeros(geometry.space_dimension, geometry.space_dimension) rows, cols = jac.shape for row in range(rows): for col in range(cols): @@ -305,32 +300,61 @@ def jac_blending_inv_eval_symbols( return inv(jac_blending) -def hessian_ref_to_affine( - geometry: ElementGeometry, hessian_ref: sp.Matrix, Jinv: sp.Matrix +def hessian_shape_affine_ref_pullback( + hessian_shape_ref: sp.MatrixBase, jac_affine_inv: sp.Matrix ) -> sp.Matrix: - hessian_affine = Jinv.T * hessian_ref * Jinv + """ + Small helper function that applies the chain rule for the transformation theorem when going from affine to + reference space. + + See also https://scicomp.stackexchange.com/q/36780 + + :param hessian_shape_ref: the hessian of the shape function + :param jac_affine_inv: the inverse of the Jacobian of the mapping from reference to affine space + :return: the transformed integrand for integration over the reference element + """ + hessian_affine = jac_affine_inv.T * hessian_shape_ref * jac_affine_inv return hessian_affine -def hessian_affine_to_blending( +def hessian_shape_blending_ref_pullback( geometry: ElementGeometry, - hessian_affine: sp.Matrix, - hessian_blending_map: List[sp.Matrix], - Jinv: sp.Matrix, - shape_grad_affine: sp.Matrix, + grad_shape_ref: sp.MatrixBase, + hessian_shape_ref: sp.MatrixBase, + jac_affine_inv: sp.Matrix, + hessian_blending: List[sp.Matrix], + jac_blending_inv: sp.Matrix, ) -> sp.Matrix: """ - This stack answer was for nonlinear FE mapping (Q2 elements) but just using the same derivation for our blending nonlinear mapping - https://scicomp.stackexchange.com/q/36780 + Small helper function that applies the chain rule for the transformation theorem when going from physical to + reference space. + + See also https://scicomp.stackexchange.com/q/36780 + + :param geometry: the element geometry + :param grad_shape_ref: the gradient of the shape function + :param hessian_shape_ref: the hessian of the shape function + :param jac_affine_inv: the inverse of the Jacobian of the mapping from reference to affine space + :param hessian_blending: the Hessian of the blending map + :param jac_blending_inv: the inverse of the Jacobian of the blending map + :return: the transformed integrand for integration over the reference element """ + shape_grad_affine = jac_affine_inv.T * grad_shape_ref + + hessian_affine = hessian_shape_affine_ref_pullback( + hessian_shape_ref, jac_affine_inv + ) + jacinvjac_blending = [] # jacinvjac_blending = sp.MutableDenseNDimArray(hessian_blending_map) * 0.0 for i in range(geometry.dimensions): - jacinvjac_blending.append(-Jinv * hessian_blending_map[i] * Jinv) + jacinvjac_blending.append( + -jac_blending_inv.T * hessian_blending[i] * jac_blending_inv.T + ) - hessian_blending = Jinv * hessian_affine * Jinv.T + result = jac_blending_inv.T * hessian_affine * jac_blending_inv d = geometry.dimensions aux_matrix = sp.zeros(d, d) @@ -338,9 +362,9 @@ def hessian_affine_to_blending( for i in range(geometry.dimensions): aux_matrix[:, i] = jacinvjac_blending[i] * shape_grad_affine - hessian_blending += Jinv * aux_matrix + result += jac_blending_inv.T * aux_matrix - return hessian_blending + return result def scalar_space_dependent_coefficient( @@ -444,10 +468,13 @@ def fem_function_on_element( domain == "reference" ), "Tabulating the basis evaluation not implemented for affine domain." + rows = geometry.dimensions if function_space.is_vectorial else 1 + if domain == "reference": # On the reference domain, the reference coordinates symbols can be used directly, so no substitution # has to be performed for the shape functions. - s = sp.zeros(1, 1) + + s = sp.zeros(rows, 1) for dof, phi in zip( dofs, ( @@ -463,7 +490,7 @@ def fem_function_on_element( # On the affine / computational domain, the evaluation point is first mapped to reference space and then # the reference space coordinate symbols are substituted with the transformed point. eval_point_on_ref = trafo_affine_point_to_ref(geometry, symbolizer=symbolizer) - s = sp.zeros(1, 1) + s = sp.zeros(rows, 1) for dof, phi in zip( dofs, function_space.shape( @@ -499,7 +526,7 @@ def fem_function_gradient_on_element( dof_map: Optional[List[int]] = None, basis_eval: Union[str, List[sp.Expr]] = "default", dof_symbols: Optional[List[DoFSymbol]] = None, -) -> sp.Matrix: +) -> Tuple[sp.Matrix, List[DoFSymbol]]: """Returns an expression that is the gradient of the element-local polynomial, either in affine or reference coordinates. The expression is build using DoFSymbol instances so that the DoFs can be resolved later. @@ -529,7 +556,8 @@ def fem_function_gradient_on_element( if domain == "reference": # On the reference domain, the reference coordinates symbols can be used directly, so no substitution # has to be performed for the shape functions. - s = sp.zeros(geometry.dimensions, 1) + cols = geometry.dimensions if function_space.is_vectorial else 1 + s = sp.zeros(geometry.dimensions, cols) for dof, grad_phi in zip( dofs, ( diff --git a/hog/forms.py b/hog/forms.py index a33f75bd161cc741d6f866e31db5c5cf784422cc..774752bb4c99066c8a46b8157c2bb01c7bc11209 100644 --- a/hog/forms.py +++ b/hog/forms.py @@ -14,20 +14,18 @@ # 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 dataclasses import dataclass import logging import sympy as sp -from typing import List, Optional, Tuple +from typing import Optional, Callable, Any -from hog.ast import Operations, count_operations -from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement +from hog.element_geometry import ElementGeometry from hog.exception import HOGException from hog.fem_helpers import ( trafo_ref_to_affine, jac_ref_to_affine, jac_affine_to_physical, - hessian_ref_to_affine, - hessian_affine_to_blending, + hessian_shape_affine_ref_pullback, + hessian_shape_blending_ref_pullback, create_empty_element_matrix, element_matrix_iterator, scalar_space_dependent_coefficient, @@ -35,38 +33,18 @@ from hog.fem_helpers import ( fem_function_on_element, fem_function_gradient_on_element, ) -from hog.function_space import FunctionSpace, EnrichedGalerkinFunctionSpace, N1E1Space -from hog.math_helpers import dot, grad, inv, abs, det, double_contraction, e_vec +from hog.function_space import FunctionSpace, N1E1Space, TrialSpace, TestSpace +from hog.math_helpers import dot, inv, abs, det, double_contraction from hog.quadrature import Quadrature, Tabulation from hog.symbolizer import Symbolizer -from hog.logger import TimedLogger, get_logger +from hog.logger import TimedLogger from hog.blending import GeometryMap, ExternalMap, IdentityMap - - -@dataclass -class Form: - integrand: sp.MatrixBase - tabulation: Tabulation - symmetric: bool - docstring: str = "" - - def integrate(self, quad: Quadrature, symbolizer: Symbolizer) -> sp.Matrix: - """Integrates the form using the passed quadrature directly, i.e. without tabulations or loops.""" - mat = self.tabulation.inline_tables(self.integrand) - - for row in range(mat.rows): - for col in range(mat.cols): - if self.symmetric and row > col: - mat[row, col] = mat[col, row] - else: - mat[row, col] = quad.integrate(mat[row, col], symbolizer) - - return mat +from hog.integrand import process_integrand, Form def diffusion( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -88,77 +66,32 @@ Weak formulation ∫ ∇u · ∇v """ - if trial != test: - raise HOGException( - "Trial space must be equal to test space to assemble diffusion matrix." - ) - - with TimedLogger("assembling diffusion matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - else: - # affine_coords = trafo_ref_to_affine(geometry, symbolizer) - # jac_blending = blending.jacobian(affine_coords) - jac_blending = symbolizer.jac_affine_to_blending(geometry.dimensions) - jac_blending_inv = symbolizer.jac_affine_to_blending_inv( - geometry.dimensions - ) - jac_blending_det = symbolizer.abs_det_jac_affine_to_blending() - - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - - for data in it: - grad_phi = data.trial_shape_grad - grad_psi = data.test_shape_grad - if blending != IdentityMap(): - jac_affine_inv_T_grad_phi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * grad_phi, - ) - jac_affine_inv_T_grad_psi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * grad_psi, - ) - form = ( - double_contraction( - jac_blending_inv.T - * sp.Matrix(jac_affine_inv_T_grad_phi_symbols), - jac_blending_inv.T - * sp.Matrix(jac_affine_inv_T_grad_psi_symbols), - ) - * jac_affine_det - * jac_blending_det - ) - else: - jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det_symbol = ( - tabulation.register_factor( - "jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det", - double_contraction( - jac_affine_inv.T * grad_phi, - jac_affine_inv.T * grad_psi, - ) - * jac_affine_det, - ) - )[0] - form = jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det_symbol + from hog.recipes.integrands.volume.diffusion import integrand + from hog.recipes.integrands.volume.diffusion_affine import ( + integrand as integrand_affine, + ) - mat[data.row, data.col] = form + # mypy type checking supposedly cannot figure out ternaries. + # https://stackoverflow.com/a/70832391 + integr: Callable[..., Any] = integrand + if blending == IdentityMap(): + integr = integrand_affine - return Form(mat, tabulation, symmetric=True, docstring=docstring) + return process_integrand( + integr, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + ) def mass( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -175,53 +108,24 @@ Weak formulation ∫ uv """ - if trial != test: - raise HOGException( - "Trial space must be equal to test space to assemble mass matrix." - ) - - with TimedLogger("assembling mass matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - # TODO tabulate when blending is implemented in the FOG, - # without blending move_constants is enough - - with TimedLogger( - f"integrating {mat.shape[0] * mat.shape[1]} expressions", - level=logging.DEBUG, - ): - for data in it: - phi = data.trial_shape - psi = data.test_shape - phi_psi_det_jac_aff = tabulation.register_factor( - "phi_psi_det_jac_aff", sp.Matrix([phi * psi * jac_affine_det]) - )[0] - if blending != IdentityMap(): - form = phi_psi_det_jac_aff * jac_blending_det - mat[data.row, data.col] = form - else: - form = phi_psi_det_jac_aff - mat[data.row, data.col] = form + from hog.recipes.integrands.volume.mass import integrand - return Form(mat, tabulation, symmetric=True, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + ) def div_k_grad( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -241,92 +145,33 @@ Weak formulation ∫ k ∇u · ∇v """ - if trial != test: - raise HOGException( - "Trial space must be equal to test space to assemble diffusion matrix." - ) - - with TimedLogger("assembling div-k-grad matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - - if coefficient_function_space: - phi_eval_symbols = tabulation.register_phi_evals( - coefficient_function_space.shape(geometry) - ) - - k, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="k", - basis_eval=phi_eval_symbols, - ) - else: - k = scalar_space_dependent_coefficient( - "k", geometry, symbolizer, blending=blending - ) - - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - - for data in it: - if blending != IdentityMap(): - # the following factors of the weak form can always be tabulated - jac_affine_inv_T_grad_phi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * data.trial_shape_grad, - ) - jac_affine_inv_T_grad_psi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * data.test_shape_grad, - ) - form = ( - k - * dot( - jac_blending_inv.T - * sp.Matrix(jac_affine_inv_T_grad_phi_symbols), - jac_blending_inv.T - * sp.Matrix(jac_affine_inv_T_grad_psi_symbols), - ) - * jac_affine_det - * jac_blending_det - ) - else: - jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det_symbol = ( - tabulation.register_factor( - "jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det", - dot( - jac_affine_inv.T * data.test_shape_grad, - jac_affine_inv.T * data.trial_shape_grad, - ) - * jac_affine_det, - ) - )[0] - form = k * jac_affine_inv_grad_phi_jac_affine_inv_grad_psi_det_symbol + from hog.recipes.integrands.volume.div_k_grad import integrand + from hog.recipes.integrands.volume.div_k_grad_affine import ( + integrand as integrand_affine, + ) - mat[data.row, data.col] = form + # mypy type checking supposedly cannot figure out ternaries. + # https://stackoverflow.com/a/70832391 + integr: Callable[..., Any] = integrand + if blending == IdentityMap(): + integr = integrand_affine - return Form(mat, tabulation, symmetric=True, docstring=docstring) + return process_integrand( + integr, + trial, + test, + geometry, + symbolizer, + blending=blending, + fe_coefficients={"k": coefficient_function_space}, + is_symmetric=trial == test, + docstring=docstring, + ) def nonlinear_diffusion( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, coefficient_function_space: FunctionSpace, @@ -357,63 +202,46 @@ Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended "Trial space must be equal to test space to assemble non-linear diffusion matrix." ) - with TimedLogger("assembling non-linear diffusion matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - - phi_eval_symbols = tabulation.register_phi_evals(trial.shape(geometry)) - - if coefficient_function_space: - u, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="u", - basis_eval=phi_eval_symbols, + def integrand( + *, + jac_a_inv, + jac_a_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, + ): + a = sp.Rational(1, 8) + k["u"] * k["u"] + return a * tabulate( + double_contraction( + jac_a_inv.T * grad_u, + jac_a_inv.T * grad_v, ) - else: - raise HOGException("Not implemented.") - - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - - for data in it: - grad_phi = jac_affine_inv.T * data.trial_shape_grad - grad_psi = jac_affine_inv.T * data.test_shape_grad - - a = sp.Matrix([sp.Rational(1, 8)]) + u * u - - dot_grad_phi_grad_psi_symbol = tabulation.register_factor( - "dot_grad_phi_grad_psi", dot(grad_phi, grad_psi) * jac_affine_det - )[0] - - mat[data.row, data.col] = a * dot_grad_phi_grad_psi_symbol + * jac_a_abs_det + ) - return Form(mat, tabulation, symmetric=True, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + fe_coefficients={"u": coefficient_function_space}, + ) def nonlinear_diffusion_newton_galerkin( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, coefficient_function_space: FunctionSpace, blending: GeometryMap = IdentityMap(), - onlyNewtonGalerkinPartOfForm: Optional[bool] = True, + only_newton_galerkin_part_of_form: Optional[bool] = True, ) -> Form: docstring = f""" @@ -423,11 +251,11 @@ Weak formulation u: trial function (space: {trial}) v: test function (space: {test}) - c: FE coefficient function (space: {coefficient_function_space}) + k: FE coefficient function (space: {coefficient_function_space}) - ∫ a(c) ∇u · ∇v + ∫ a'(c) u ∇c · ∇v + ∫ a(k) ∇u · ∇v + ∫ a'(k) u ∇k · ∇v -Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended for :math:`c = u`. +Note: :math:`a(k) = 1/8 + k^2` is currently hard-coded and the form is intended for :math:`k = u`. """ if trial != test: raise HOGException( @@ -439,86 +267,53 @@ Note: :math:`a(c) = 1/8 + u^2` is currently hard-coded and the form is intended "The nonlinear_diffusion_newton_galerkin form does currently not support blending." ) - with TimedLogger( - "assembling nonlinear_diffusion_newton_galerkin matrix", level=logging.DEBUG + def integrand( + *, + jac_a_inv, + jac_a_abs_det, + u, + grad_u, + grad_v, + k, + grad_k, + tabulate, + **_, ): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) + a = sp.Rational(1, 8) + k["k"] * k["k"] + a_prime = 2 * k["k"] - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - - phi_eval_symbols = tabulation.register_phi_evals(trial.shape(geometry)) - - u, dof_symbols = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="u", - basis_eval=phi_eval_symbols, + diffusion_term = a * tabulate( + dot(jac_a_inv.T * grad_u, jac_a_inv.T * grad_v) * jac_a_abs_det ) - grad_u, _ = fem_function_gradient_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="grad_u", - dof_symbols=dof_symbols, + newton_galerkin_term = ( + a_prime + * u + * dot(jac_a_inv.T * grad_k["k"], tabulate(jac_a_inv.T * grad_v)) + * tabulate(jac_a_abs_det) ) - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - - for data in it: - phi = data.trial_shape - grad_psi = jac_affine_inv.T * data.test_shape_grad - - a = sp.Matrix([sp.Rational(1, 8)]) + u * u - - aPrime = 2 * u - - grad_psi_symbol = tabulation.register_factor("grad_psi", grad_psi) - - if onlyNewtonGalerkinPartOfForm: - mat[data.row, data.col] = ( - aPrime - * phi - * dot(jac_affine_inv.T * grad_u, grad_psi_symbol) - * jac_affine_det - ) - else: - grad_phi = jac_affine_inv.T * data.trial_shape_grad - - dot_grad_phi_grad_psi_symbol = tabulation.register_factor( - "dot_grad_phi_grad_psi", dot(grad_phi, grad_psi) * jac_affine_det - )[0] - - mat[data.row, data.col] = ( - a * dot_grad_phi_grad_psi_symbol - + aPrime - * phi - * dot(jac_affine_inv.T * grad_u, grad_psi_symbol) - * jac_affine_det - ) + if only_newton_galerkin_part_of_form: + return newton_galerkin_term + else: + return diffusion_term + newton_galerkin_term - return Form(mat, tabulation, symmetric=False, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=False, + docstring=docstring, + fe_coefficients={"k": coefficient_function_space}, + ) def epsilon( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -527,22 +322,9 @@ def epsilon( variable_viscosity: bool = True, coefficient_function_space: Optional[FunctionSpace] = None, ) -> Form: - - if trial.is_vectorial ^ test.is_vectorial: - raise HOGException( - "Either both (trial and test) spaces or none should be vectorial." - ) - - vectorial_spaces = trial.is_vectorial - docstring_components = ( - "" - if vectorial_spaces - else f"\nComponent trial: {component_trial}\nComponent test: {component_test}" - ) - docstring = f""" "Epsilon" operator. -{docstring_components} + Geometry map: {blending} Weak formulation @@ -559,170 +341,34 @@ where """ if not variable_viscosity: raise HOGException("Constant viscosity currently not supported.") - # TODO fix issue with undeclared p_affines - - if ( - not vectorial_spaces - and geometry.dimensions < 3 - and (component_trial > 1 or component_test > 1) - ): - return create_empty_element_matrix(trial, test, geometry) - - with TimedLogger("assembling epsilon matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - jac_blending_det = abs(det(jac_blending)) - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - - mu: sp.Expr = 1 - if variable_viscosity: - if coefficient_function_space: - phi_eval_symbols = tabulation.register_phi_evals( - coefficient_function_space.shape(geometry) - ) - - mu, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="mu", - basis_eval=phi_eval_symbols, - ) - else: - mu = scalar_space_dependent_coefficient( - "mu", geometry, symbolizer, blending=blending - ) + from hog.recipes.integrands.volume.epsilon import integrand + from hog.recipes.integrands.volume.epsilon_affine import ( + integrand as integrand_affine, + ) - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) + # mypy type checking supposedly cannot figure out ternaries. + # https://stackoverflow.com/a/70832391 + integr: Callable[..., Any] = integrand + if blending == IdentityMap(): + integr = integrand_affine - for data in it: - phi = data.trial_shape - psi = data.test_shape - grad_phi_vec = data.trial_shape_grad - grad_psi_vec = data.test_shape_grad - - # EG gradient transformation TODO move to function space - if isinstance(trial, EnrichedGalerkinFunctionSpace): - # for EDG, the shape function is already vectorial and does not have to be multiplied by e_vec - grad_phi_vec = jac_affine * grad_phi_vec - elif not vectorial_spaces: - grad_phi_vec = ( - (e_vec(geometry.dimensions, component_trial) * phi) - .jacobian(ref_symbols_list) - .T - ) - # same for test space - if isinstance(test, EnrichedGalerkinFunctionSpace): - # for EDG, the shape function is already vectorial and does not have to be multiplied by e_vec - grad_psi_vec = jac_affine * grad_psi_vec - elif not vectorial_spaces: - grad_psi_vec = ( - (e_vec(geometry.dimensions, component_test) * psi) - .jacobian(ref_symbols_list) - .T - ) - - # setup of the form expression with tabulation - if blending != IdentityMap(): - # chain rule, premultiply with transposed inverse jacobians of the affine trafo - # the results are tensors of order 2 - # + tabulate affine transformed gradients (can only do this due to incoming, micro-element dependent blending jacobian) - jac_affine_inv_T_grad_phi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * grad_phi_vec, - ) - ) - jac_affine_inv_T_grad_psi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * grad_psi_vec, - ) - ) - - # transform gradients according to blending map - jac_blending_T_jac_affine_inv_T_grad_phi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_phi_symbols - ) - jac_blending_T_jac_affine_inv_T_grad_psi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_psi_symbols - ) - - # extract the symmetric part - sym_grad_phi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_phi - + jac_blending_T_jac_affine_inv_T_grad_phi.T - ) - sym_grad_psi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_psi - + jac_blending_T_jac_affine_inv_T_grad_psi.T - ) - - # double contract everything + determinants - form = ( - double_contraction(2 * mu[0, 0] * sym_grad_phi, sym_grad_psi) - * jac_affine_det - * jac_blending_det - ) - - else: - # chain rule, premultiply with transposed inverse jacobians of affine trafo - # the results are tensors of order 2 - jac_affine_inv_T_grad_phi = jac_affine_inv.T * grad_phi_vec - jac_affine_inv_T_grad_psi = jac_affine_inv.T * grad_psi_vec - - # now let's extract the symmetric part - sym_grad_phi = 0.5 * ( - jac_affine_inv_T_grad_phi + jac_affine_inv_T_grad_phi.T - ) - sym_grad_psi = 0.5 * ( - jac_affine_inv_T_grad_psi + jac_affine_inv_T_grad_psi.T - ) - - # double contract everything + determinants, tabulate the whole contraction - # TODO maybe shorten naming, although its nice to have everything in the name - contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi_det_symbol = ( - tabulation.register_factor( - "contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi_det_symbol", - double_contraction(2 * sym_grad_phi, sym_grad_psi) - * jac_affine_det, - ) - )[ - 0 - ] - form = ( - mu - * contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi_det_symbol - ) - - mat[data.row, data.col] = form - - return Form( - mat, - tabulation, - symmetric=vectorial_spaces or (component_trial == component_test), + return process_integrand( + integr, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, docstring=docstring, + fe_coefficients={"mu": coefficient_function_space}, ) def k_mass( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -742,66 +388,23 @@ Weak formulation ∫ k uv """ - if trial != test: - TimedLogger( - "Trial and test space can be different, but please make sure this is intensional!", - level=logging.INFO, - ).log() - - with TimedLogger("assembling k-mass matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - - mat = create_empty_element_matrix(trial, test, geometry) - - it = element_matrix_iterator(trial, test, geometry) - - if coefficient_function_space: - k, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="k", - ) - else: - k = scalar_space_dependent_coefficient( - "k", geometry, symbolizer, blending=blending - ) - - with TimedLogger( - f"integrating {mat.shape[0] * mat.shape[1]} expressions", - level=logging.DEBUG, - ): - for data in it: - phi = data.trial_shape - psi = data.test_shape - - phi_psi_jac_affine_det = tabulation.register_factor( - "phi_psi_jac_affine_det", - sp.Matrix([phi * psi * jac_affine_det]), - )[0] - if blending == IdentityMap(): - form = k * phi_psi_jac_affine_det - else: - form = k * phi_psi_jac_affine_det * jac_blending_det - mat[data.row, data.col] = form + from hog.recipes.integrands.volume.k_mass import integrand - return Form(mat, tabulation, symmetric=trial == test, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + ) def pspg( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, @@ -843,63 +446,23 @@ or for details. """ - if quad.is_exact() and isinstance(blending, ExternalMap): - raise HOGException( - "Exact integration is not supported for externally defined blending functions." - ) + from hog.recipes.integrands.volume.pspg import integrand - with TimedLogger("assembling diffusion matrix", level=logging.DEBUG): - jac_affine = jac_ref_to_affine(geometry, symbolizer) - with TimedLogger("inverting affine Jacobian", level=logging.DEBUG): - jac_affine_inv = inv(jac_affine) - jac_affine_det = abs(det(jac_affine)) - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - - if geometry.dimensions == 2: - volume = jac_blending_det * 0.5 * jac_affine_det - tau = -volume * 0.2 - else: - volume = jac_blending_det * jac_affine_det / 6.0 - tau = -pow(volume, 2.0 / 3.0) / 12.0 - - mat = create_empty_element_matrix(trial, test, geometry) - - it = element_matrix_iterator(trial, test, geometry) - # TODO tabulate - - with TimedLogger( - f"integrating {mat.shape[0] * mat.shape[1]} expressions", - level=logging.DEBUG, - ): - for data in it: - grad_phi = data.trial_shape_grad - grad_psi = data.test_shape_grad - form = ( - dot( - jac_blending_inv.T * jac_affine_inv.T * grad_phi, - jac_blending_inv.T * jac_affine_inv.T * grad_psi, - ) - * jac_affine_det - * tau - * jac_blending_det - ) - mat[data.row, data.col] = quad.integrate(form, symbolizer) - - return Form(mat, Tabulation(symbolizer), symmetric=True, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + ) def linear_form( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, @@ -970,30 +533,14 @@ def linear_form( def divergence( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), component_index: int = 0, - transpose: bool = False, ) -> Form: - if transpose: - docstring = f""" -Gradient. - -Component: {component_index} -Geometry map: {blending} - -Weak formulation - - u: trial function (scalar space: {trial}) - v: test function (vectorial space: {test}) - - ∫ - ( ∇ · v ) u -""" - else: - docstring = f""" + docstring = f""" Divergence. Component: {component_index} @@ -1007,74 +554,59 @@ Weak formulation ∫ - ( ∇ · u ) v """ - with TimedLogger( - f"assembling divergence {'transpose' if transpose else ''} matrix", - level=logging.DEBUG, - ): - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - jac_blending_det = abs(det(jac_blending)) - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - - mat = create_empty_element_matrix(trial, test, geometry) - - it = element_matrix_iterator(trial, test, geometry) - # TODO tabulate + from hog.recipes.integrands.volume.divergence import integrand - # guard in 2D against the z derivative, which is not defined here: - component_index = min(component_index, geometry.dimensions - 1) - - for data in it: - if transpose: - phi = data.trial_shape - grad_phi = data.test_shape_grad - else: - phi = data.test_shape - grad_phi = data.trial_shape_grad - form = ( - -(jac_blending_inv.T * jac_affine_inv.T * grad_phi)[component_index] - * phi - * jac_affine_det - * jac_blending_det - ) - - mat[data.row, data.col] = form - - return Form(mat, Tabulation(symbolizer), symmetric=False, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=False, + docstring=docstring, + ) def gradient( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), component_index: int = 0, ) -> Form: - """See divergence form. Just calls that with the transpose argument set to True.""" - return divergence( + docstring = f""" + Gradient. + + Component: {component_index} + Geometry map: {blending} + + Weak formulation + + u: trial function (scalar space: {trial}) + v: test function (vectorial space: {test}) + + ∫ - ( ∇ · v ) u + """ + + from hog.recipes.integrands.volume.gradient import integrand + + return process_integrand( + integrand, trial, test, geometry, symbolizer, blending=blending, - component_index=component_index, - transpose=True, + is_symmetric=False, + docstring=docstring, ) def full_stokes( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -1113,192 +645,24 @@ where ε(w) := (1/2) (∇w + (∇w)ᵀ) """ - if variable_viscosity == False: - raise HOGException("Constant viscosity currently not supported.") - # TODO fix issue with undeclared p_affines - - if geometry.dimensions < 3 and (component_trial > 1 or component_test > 1): - return create_empty_element_matrix(trial, test, geometry) - with TimedLogger("assembling full stokes matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) + from hog.recipes.integrands.volume.full_stokes import integrand - jac_blending_inv = inv(jac_blending) - jac_blending_det = abs(det(jac_blending)) - - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - - mu: sp.Expr = 1 - if variable_viscosity: - if coefficient_function_space: - phi_eval_symbols = tabulation.register_phi_evals( - coefficient_function_space.shape(geometry) - ) - - mu, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="mu", - basis_eval=phi_eval_symbols, - ) - else: - mu = scalar_space_dependent_coefficient( - "mu", geometry, symbolizer, blending=blending - ) - - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) - - for data in it: - phi = data.trial_shape - psi = data.test_shape - grad_phi_vec = data.trial_shape_grad - grad_psi_vec = data.test_shape_grad - - # gradient of e_i * phi, where i is the trial space component - # results in a order 2 tensor, - # equal to the transposed Jacobian of e_i * phi - grad_phi_vec = ( - (e_vec(geometry.dimensions, component_trial) * phi) - .jacobian(ref_symbols_list) - .T - ) - - # same for test space - grad_psi_vec = ( - (e_vec(geometry.dimensions, component_test) * psi) - .jacobian(ref_symbols_list) - .T - ) - - # setup of the form expression with tabulation - if blending != IdentityMap(): - # chain rule, premultiply with transposed inverse jacobians of the affine trafo - # the results are tensors of order 2 - # + tabulate affine transformed gradients (can only do this due to incoming, micro-element dependent blending jacobian) - jac_affine_inv_T_grad_phi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * grad_phi_vec, - ) - ) - jac_affine_inv_T_grad_psi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * grad_psi_vec, - ) - ) - - # transform gradients according to blending map - jac_blending_T_jac_affine_inv_T_grad_phi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_phi_symbols - ) - jac_blending_T_jac_affine_inv_T_grad_psi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_psi_symbols - ) - - # extract the symmetric part - sym_grad_phi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_phi - + jac_blending_T_jac_affine_inv_T_grad_phi.T - ) - sym_grad_psi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_psi - + jac_blending_T_jac_affine_inv_T_grad_psi.T - ) - - # form divdiv part - # ( div(e_i*phi), div(e_j*psi) )_Omega results in - # - # ( \partial phi / \partial x_i ) * ( \partial psi / \partial x_j ) - # - # for which we have to take the distortions by the two mappings into account - divdiv = sp.Matrix( - [ - jac_blending_T_jac_affine_inv_T_grad_phi[ - component_trial, component_trial - ] - * jac_blending_T_jac_affine_inv_T_grad_psi[ - component_test, component_test - ] - ] - ) - - # double contract sym grads + divdiv + determinants - form = ( - mu - * ( - double_contraction(2 * sym_grad_phi, sym_grad_psi) - - sp.Rational(2, 3) * divdiv - ) - * jac_affine_det - * jac_blending_det - ) - - else: - # chain rule, premultiply with transposed inverse jacobians of affine trafo - # the results are tensors of order 2 - jac_affine_inv_T_grad_phi = jac_affine_inv.T * grad_phi_vec - jac_affine_inv_T_grad_psi = jac_affine_inv.T * grad_psi_vec - - # now let's extract the symmetric part - sym_grad_phi = 0.5 * ( - jac_affine_inv_T_grad_phi + jac_affine_inv_T_grad_phi.T - ) - sym_grad_psi = 0.5 * ( - jac_affine_inv_T_grad_psi + jac_affine_inv_T_grad_psi.T - ) - - divdiv = sp.Matrix( - [ - jac_affine_inv_T_grad_phi[component_trial, component_trial] - * jac_affine_inv_T_grad_psi[component_test, component_test] - ] - ) - - # double contract sym grads + divdiv + determinants + tabulate the whole expression - # TODO maybe shorten naming, although its nice to have everything in the name - contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi__min_2third_divdiv_det_symbol = ( - tabulation.register_factor( - "contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi_det_plus_min2third_divdiv", - ( - double_contraction(2 * sym_grad_phi, sym_grad_psi) - - sp.Rational(2, 3) * divdiv - ) - * jac_affine_det, - ) - )[ - 0 - ] - form = ( - mu - * contract_2_jac_affine_inv_sym_grad_phi_jac_affine_inv_sym_grad_psi__min_2third_divdiv_det_symbol - ) - - mat[data.row, data.col] = form - - return Form( - mat, - tabulation, - symmetric=component_trial == component_test, + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, docstring=docstring, + fe_coefficients={"mu": coefficient_function_space}, ) def shear_heating( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -1322,8 +686,8 @@ Listing 2 The strong representation of the operator is given by: - 𝜏(u) : grad(u) - 2 {{[ μ (grad(u)+grad(u)ᵀ) / 2 ] - 1/dim [ μ div(u) ]I}} : grad(u) + 𝜏(w) : grad(w) + 2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w) Note that the factor 1/dim means that for 2D this is the pseudo-3D form of the operator. @@ -1337,207 +701,75 @@ Weak formulation T: trial function (scalar space: {trial}) s: test function (scalar space: {test}) μ: coefficient (scalar space: {viscosity_function_space}) - u: velocity (vectorial space: {velocity_function_space}) + w: velocity (vectorial space: {velocity_function_space}) - ∫ {{ 2 {{[ μ (grad(u)+grad(u)ᵀ) / 2 ] - 1/3 [ μ div(u) ]I}} : grad(u) }} T_h s_h + ∫ {{ 2 {{[ μ (grad(w)+grad(w)ᵀ) / 2 ] - 1/dim [ μ div(w) ]I}} : grad(w) }} T_h s_h The resulting matrix must be multiplied with a vector of ones to be used as the shear heating term in the RHS """ - if variable_viscosity == False: - raise HOGException("Constant viscosity currently not supported.") - # TODO fix issue with undeclared p_affines - - if geometry.dimensions < 3 and (component_trial > 1 or component_test > 1): - return create_empty_element_matrix(trial, test, geometry) - with TimedLogger("assembling shear heating matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - jac_blending = symbolizer.jac_affine_to_blending(geometry.dimensions) - jac_blending_inv = symbolizer.jac_affine_to_blending_inv( - geometry.dimensions - ) - jac_blending_det = symbolizer.abs_det_jac_affine_to_blending() - # affine_coords = trafo_ref_to_affine(geometry, symbolizer) - # jac_blending = blending.jacobian(affine_coords) - - # jac_blending_inv = inv(jac_blending) - # jac_blending_det = abs(det(jac_blending)) - - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - - mu: sp.Expr = 1 - if viscosity_function_space: - phi_eval_symbols = tabulation.register_phi_evals( - viscosity_function_space.shape(geometry) - ) - - mu, _ = fem_function_on_element( - viscosity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="mu", - basis_eval=phi_eval_symbols, - ) - else: - raise HOGException("scalar_space_dependent_coefficient currently not supported in opgen.") - # mu = scalar_space_dependent_coefficient( - # "mu", geometry, symbolizer, blending=blending - # ) - - if velocity_function_space: - phi_eval_symbols_u = tabulation.register_phi_evals( - velocity_function_space.shape(geometry) - ) - ux, dof_symbols_ux = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="ux", - basis_eval=phi_eval_symbols_u, - ) - - grad_ux, _ = fem_function_gradient_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="grad_ux", - dof_symbols=dof_symbols_ux, - ) - - uy, dof_symbols_uy = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="uy", - basis_eval=phi_eval_symbols_u, - ) - - grad_uy, _ = fem_function_gradient_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="grad_uy", - dof_symbols=dof_symbols_uy, - ) - - - # if geometry.dimensions > 2: - uz, dof_symbols_uz = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="uz", - basis_eval=phi_eval_symbols_u, - ) - - grad_uz, _ = fem_function_gradient_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="grad_uz", - dof_symbols=dof_symbols_uz, - ) - - else: - raise HOGException("velocity function needed as an external function") - - if blending != IdentityMap(): - grad_ux = jac_blending_inv.T * jac_affine_inv.T * grad_ux - grad_uy = jac_blending_inv.T * jac_affine_inv.T * grad_uy - grad_uz = jac_blending_inv.T * jac_affine_inv.T * grad_uz - else: - grad_ux = jac_affine_inv.T * grad_ux - grad_uy = jac_affine_inv.T * grad_uy - grad_uz = jac_affine_inv.T * grad_uz + def integrand( + *, + jac_a_inv, + jac_b_inv, + jac_a_abs_det, + jac_b_abs_det, + u, + v, + k, + grad_k, + volume_geometry, + tabulate, + **_, + ): + """First function: mu, other functions: ux, uy, uz.""" - grad_u = grad_ux.row_join(grad_uy) + mu = k["mu"] - dim = geometry.dimensions - if dim == 2: - u = sp.Matrix([[ux], [uy]]) - elif dim == 3: - u = sp.Matrix([[ux], [uy], [uz]]) - grad_u = grad_u.row_join(grad_uz) + # grad_k[0] is grad_mu_ref + grad_wx = jac_b_inv.T * jac_a_inv.T * grad_k["wx"] + grad_wy = jac_b_inv.T * jac_a_inv.T * grad_k["wy"] + grad_wz = jac_b_inv.T * jac_a_inv.T * grad_k["wz"] - _sym_grad_u = (grad_u + grad_u.T) / 2 + grad_w = grad_wx.row_join(grad_wy) + dim = volume_geometry.dimensions + if dim == 3: + grad_w = grad_w.row_join(grad_wz) - # Compute div(u) + sym_grad_w = 0.5 * (grad_w + grad_w.T) - divdiv = grad_u.trace() * sp.eye(dim) + divdiv = grad_w.trace() * sp.eye(dim) - tau = 2 * (_sym_grad_u - sp.Rational(1, dim) * divdiv) + tau = 2 * (sym_grad_w - sp.Rational(1, dim) * divdiv) - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) + return ( + mu + * (double_contraction(tau, grad_w)[0]) + * jac_b_abs_det + * tabulate(jac_a_abs_det * u * v) + ) - for data in it: - phi = data.trial_shape - psi = data.test_shape - - if blending != IdentityMap(): - affine_factor = ( - tabulation.register_factor( - "affine_factor_symbol", - sp.Matrix([phi * psi * jac_affine_det]), - ) - )[ - 0 - ] - form = ( - mu[0] - * ( - double_contraction(tau, grad_u)[0] - ) - * jac_blending_det - * affine_factor - ) - else: - shear_heating_det_symbol = ( - tabulation.register_factor( - "shear_heating_det_symbol", - ( - double_contraction(tau, grad_u) - ) - * phi - * psi - * jac_affine_det, - ) - )[ - 0 - ] - form = ( - mu[0] * shear_heating_det_symbol - ) - - mat[data.row, data.col] = form - - return Form( - mat, - tabulation, - symmetric=component_trial == component_test, + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + fe_coefficients={ + "mu": viscosity_function_space, + "wx": velocity_function_space, + "wy": velocity_function_space, + "wz": velocity_function_space, + }, + is_symmetric=trial == test, docstring=docstring, ) - def divdiv( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, component_trial: int, component_test: int, geometry: ElementGeometry, @@ -1566,83 +798,114 @@ Weak formulation ∫ ( ∇ · u ) · ( ∇ · v ) """ - if geometry.dimensions < 3 and (component_trial > 1 or component_test > 1): - return create_empty_element_matrix(trial, test, geometry) + from hog.recipes.integrands.volume.divdiv import integrand + + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring=docstring, + ) + - jac_affine = jac_ref_to_affine(geometry, symbolizer) - jac_affine_inv = inv(jac_affine) - jac_affine_det = abs(det(jac_affine)) +def advection( + trial: TrialSpace, + test: TestSpace, + geometry: ElementGeometry, + symbolizer: Symbolizer, + velocity_function_space: FunctionSpace, + coefficient_function_space: FunctionSpace, + constant_cp: bool = False, + blending: GeometryMap = IdentityMap(), +) -> Form: + docstring = f""" +advection operator which needs to be used in combination with SUPG - if isinstance(blending, ExternalMap): - jac_blending = jac_affine_to_physical(geometry, symbolizer) - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) +Geometry map: {blending} - jac_blending_inv = inv(jac_blending) - jac_blending_det = abs(det(jac_blending)) +Weak formulation - mat = create_empty_element_matrix(trial, test, geometry) + T: trial function (scalar space: {trial}) + s: test function (scalar space: {test}) + u: velocity function (vectorial space: {velocity_function_space}) - it = element_matrix_iterator(trial, test, geometry) + ∫ cp ( u · ∇T ) s +""" - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - # TODO tabulate + from hog.recipes.integrands.volume.advection import integrand - with TimedLogger( - f"integrating {mat.shape[0] * mat.shape[1]} expressions", - level=logging.DEBUG, - ): - for data in it: - phi = data.trial_shape - psi = data.test_shape - - # gradient of e_i * phi, where i is the trial space component - # results in a order 2 tensor, - # equal to the transposed Jacobian of e_i * phi - grad_phi_vec = ( - (e_vec(geometry.dimensions, component_trial) * phi) - .jacobian(ref_symbols_list) - .T - ) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=False, + docstring=docstring, + fe_coefficients={ + "ux": velocity_function_space, + "uy": velocity_function_space, + "uz": velocity_function_space, + } if constant_cp else { + "ux": velocity_function_space, + "uy": velocity_function_space, + "uz": velocity_function_space, + "cp": coefficient_function_space, + }, + ) - # same for test space - grad_psi_vec = ( - (e_vec(geometry.dimensions, component_test) * psi) - .jacobian(ref_symbols_list) - .T - ) - # chain rule, premultiply with transposed inverse jacobians of blending and affine trafos - # the results are tensors of order 2 - grad_phi_vec_chain = jac_blending_inv.T * jac_affine_inv.T * grad_phi_vec - grad_psi_vec_chain = jac_blending_inv.T * jac_affine_inv.T * grad_psi_vec - - # ( div(e_i*phi), div(e_j*psi) )_Omega results in - # - # ( \partial phi / \partial x_i ) * ( \partial psi / \partial x_j ) - # - # for which we have to take the distortians by the two mappings into account - divdiv = ( - grad_phi_vec_chain[component_trial, component_trial] - * grad_psi_vec_chain[component_test, component_test] - ) +def supg_advection( + trial: TrialSpace, + test: TestSpace, + geometry: ElementGeometry, + symbolizer: Symbolizer, + velocity_function_space: FunctionSpace, + coefficient_function_space: FunctionSpace, + blending: GeometryMap = IdentityMap(), +) -> Form: + docstring = f""" +advection operator which needs to be used in combination with SUPG - form = divdiv * jac_affine_det * jac_blending_det +Geometry map: {blending} + +Weak formulation - mat[data.row, data.col] = quad.integrate(form, symbolizer) + T: trial function (scalar space: {trial}) + s: test function (scalar space: {test}) + u: velocity function (vectorial space: {velocity_function_space}) - return Form( - mat, - Tabulation(symbolizer), - symmetric=component_trial == component_test, + ∫ cp ( u · ∇T ) 𝛿(u · ∇s) +""" + + from hog.recipes.integrands.volume.supg_advection import integrand + + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=False, docstring=docstring, + fe_coefficients={ + "ux": velocity_function_space, + "uy": velocity_function_space, + "uz": velocity_function_space, + "cp_times_delta": coefficient_function_space, + }, ) def supg_diffusion( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, velocity_function_space: FunctionSpace, @@ -1658,12 +921,12 @@ Weak formulation T: trial function (space: {trial}) s: test function (space: {test}) - u: velocity function (space: {velocity_function_space}) + w: velocity function (space: {velocity_function_space}) k𝛿: FE function representing k·𝛿 (space: {diffusivityXdelta_function_space}) For OpGen, - ∫ k(ΔT) · 𝛿(u · ∇s) + ∫ k(ΔT) · 𝛿(w · ∇s) ------------------- @@ -1672,363 +935,202 @@ Weak formulation ∫ (ΔT) s """ - if trial != test: - raise HOGException( - "Trial space must be equal to test space to assemble SUPG diffusion matrix." - ) - - with TimedLogger("assembling second derivative matrix", level=logging.DEBUG): - tabulation = Tabulation(symbolizer) - - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) - jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() - - if isinstance(blending, ExternalMap): - HOGException("ExternalMap is not supported") - else: - affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = symbolizer.jac_affine_to_blending(geometry.dimensions) - jac_blending_inv = symbolizer.jac_affine_to_blending_inv( - geometry.dimensions - ) - jac_blending_det = symbolizer.abs_det_jac_affine_to_blending() - if not isinstance(blending, IdentityMap): - # hessian_blending_map = blending.hessian(affine_coords) - hessian_blending_map = symbolizer.hessian_blending_map(geometry.dimensions) + def integrand( + *, + jac_a_inv, + jac_b_inv, + hessian_b, + jac_a_abs_det, + jac_b_abs_det, + grad_u, + grad_v, + hessian_u, + k, + volume_geometry, + tabulate, + **_, + ): + """First function: k𝛿, other functions: ux, uy, uz.""" - # jac_blending_det = abs(det(jac_blending)) - # with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - # jac_blending_inv = inv(jac_blending) + k_times_delta = k["diffusivity_times_delta"] + wx = k["wx"] + wy = k["wy"] + wz = k["wz"] - mat = create_empty_element_matrix(trial, test, geometry) - it = element_matrix_iterator(trial, test, geometry) + dim = volume_geometry.dimensions + if dim == 2: + w = sp.Matrix([[wx], [wy]]) + elif dim == 3: + w = sp.Matrix([[wx], [wy], [wz]]) - if velocity_function_space != None and diffusivityXdelta_function_space != None: - u_eval_symbols = tabulation.register_phi_evals( - velocity_function_space.shape(geometry) - ) + if isinstance(blending, IdentityMap): + hessian_affine = hessian_shape_affine_ref_pullback(hessian_u, jac_a_inv) - ux, _ = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="ux", - basis_eval=u_eval_symbols, - ) + laplacian = sum([hessian_affine[i, i] for i in range(geometry.dimensions)]) - uy, _ = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="uy", - basis_eval=u_eval_symbols, - ) - - if isinstance(geometry, TetrahedronElement): - uz, _ = fem_function_on_element( - velocity_function_space, - geometry, - symbolizer, - domain="reference", - function_id="uz", - basis_eval=u_eval_symbols, - ) - u = sp.Matrix([[ux], [uy], [uz]]) - else: - u = sp.Matrix([[ux], [uy]]) - - kdelta_eval_symbols = tabulation.register_phi_evals( - diffusivityXdelta_function_space.shape(geometry) + form = ( + k_times_delta + * tabulate(laplacian) + * dot(w, tabulate(jac_a_inv.T * grad_v)) + * jac_a_abs_det ) - kdelta, _ = fem_function_on_element( - diffusivityXdelta_function_space, + else: + hessian_blending = hessian_shape_blending_ref_pullback( geometry, - symbolizer, - domain="reference", - function_id="kdelta", - basis_eval=kdelta_eval_symbols, + grad_u, + hessian_u, + jac_a_inv, + hessian_b, + jac_b_inv, ) - for data in it: - psi = data.test_shape - grad_phi = data.trial_shape_grad - grad_psi = data.test_shape_grad - hessian_phi = data.trial_shape_hessian - hessian_affine = hessian_ref_to_affine( - geometry, hessian_phi, jac_affine_inv + laplacian = sum( + [hessian_blending[i, i] for i in range(geometry.dimensions)] ) - hessian_affine_symbols = tabulation.register_factor( - "hessian_affine", - hessian_affine, - ) - - jac_affine_inv_T_grad_phi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * grad_phi, + form = ( + laplacian + * dot(w, jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)) + * k_times_delta + * jac_a_abs_det + * jac_b_abs_det ) - jac_affine_inv_T_grad_psi_symbols = tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * grad_psi, - ) + return form - # jac_blending_inv_T_jac_affine_inv_T_grad_psi_symbols = tabulation.register_factor( - # "jac_affine_inv_T_grad_psi", - # jac_blending_inv.T * jac_affine_inv_T_grad_psi_symbols, - # ) - - if isinstance(blending, IdentityMap): - laplacian = sum( - [hessian_affine_symbols[i, i] for i in range(geometry.dimensions)] - ) - form = ( - laplacian - * dot(u, jac_affine_inv_T_grad_psi_symbols) - * kdelta - * jac_affine_det - ) - else: - hessian_blending = hessian_affine_to_blending( - geometry, - hessian_affine, - hessian_blending_map, - jac_blending_inv.T, - jac_affine_inv_T_grad_phi_symbols, - ) - - laplacian = sum( - [hessian_blending[i, i] for i in range(geometry.dimensions)] - ) - - form = ( - laplacian - * dot(u, jac_blending_inv.T * jac_affine_inv.T * grad_psi) - * kdelta - * jac_affine_det - * jac_blending_det - ) - # HOGException("Only for testing with Blending map") - - mat[data.row, data.col] = form - - return Form(mat, tabulation, symmetric=False, docstring=docstring) + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + fe_coefficients={ + "diffusivity_times_delta": diffusivityXdelta_function_space, + "wx": velocity_function_space, + "wy": velocity_function_space, + "wz": velocity_function_space, + }, + ) -def zero_form( - trial: FunctionSpace, test: FunctionSpace, geometry: ElementGeometry -) -> sp.Matrix: - rows = test.num_dofs(geometry) - cols = trial.num_dofs(geometry) if trial is not None else 1 - return sp.zeros(rows, cols) - - -def linear_elasticity( - trial: FunctionSpace, - test: FunctionSpace, - geometry: ElementGeometry, - symbolizer: Symbolizer, - blending: GeometryMap = IdentityMap(), - component_trial: int = 0, - component_test: int = 0, - variable_viscosity: bool = True, - coefficient_function_space: Optional[FunctionSpace] = None, +def grad_rho_by_rho_dot_u( + trial: TrialSpace, + test: TestSpace, + geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), + density_function_space: Optional[FunctionSpace] = None, ) -> Form: - if trial.is_vectorial ^ test.is_vectorial: - raise HOGException( - "Either both (trial and test) spaces or none should be vectorial." - ) - - vectorial_spaces = trial.is_vectorial - docstring_components = ( - "" - if vectorial_spaces - else f"\nComponent trial: {component_trial}\nComponent test: {component_test}" - ) - docstring = f""" -"Linear elasticity" operator. -{docstring_components} -Geometry map: {blending} +RHS operator for the frozen velocity approach. + +Geometry map: {blending} Weak formulation u: trial function (vectorial space: {trial}) - v: test function (vectorial space: {test}) - λ: coefficient (scalar space: {coefficient_function_space}) - μ: coefficient (scalar space: {coefficient_function_space}) - - ∫ σ(u) : ε(v) - -where + v: test function (space: {test}) + rho: coefficient (space: {density_function_space}) - σ(w) := λ(∇.w)I + μ(∇w + (∇w)ᵀ) - ε(w) := (1/2) (∇w + (∇w)ᵀ) + ∫ ((∇ρ / ρ) · u) v """ - if not variable_viscosity: - raise HOGException("Constant viscosity currently not supported.") - # TODO fix issue with undeclared p_affines - if ( - not vectorial_spaces - and geometry.dimensions < 3 - and (component_trial > 1 or component_test > 1) + with TimedLogger( + "assembling grad_rho_by_rho_dot_u-mass matrix", level=logging.DEBUG ): - return create_empty_element_matrix(trial, test, geometry) - - with TimedLogger("assembling epsilon matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() + jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry) if isinstance(blending, ExternalMap): jac_blending = jac_affine_to_physical(geometry, symbolizer) else: affine_coords = trafo_ref_to_affine(geometry, symbolizer) - jac_blending = blending.jacobian(affine_coords) - - with TimedLogger("inverting blending Jacobian", level=logging.DEBUG): - jac_blending_inv = inv(jac_blending) - jac_blending_det = abs(det(jac_blending)) + # jac_blending = blending.jacobian(affine_coords) + jac_blending_inv = symbolizer.jac_affine_to_blending_inv( + geometry.dimensions + ) + jac_blending_det = symbolizer.abs_det_jac_affine_to_blending() - ref_symbols_list = symbolizer.ref_coords_as_list(geometry.dimensions) - - mu: sp.Expr = 1 - lamb: sp.Expr = 1 - if variable_viscosity: - if coefficient_function_space: - phi_eval_symbols = tabulation.register_phi_evals( - coefficient_function_space.shape(geometry) - ) - - mu, _ = fem_function_on_element( - coefficient_function_space, - geometry, - symbolizer, - domain="reference", - function_id="mu", - basis_eval=phi_eval_symbols, - ) - else: - mu = scalar_space_dependent_coefficient( - "mu", geometry, symbolizer, blending=blending - ) - lamb = scalar_space_dependent_coefficient( - "lambda", geometry, symbolizer, blending=blending - ) + # jac_blending_det = abs(det(jac_blending)) mat = create_empty_element_matrix(trial, test, geometry) + it = element_matrix_iterator(trial, test, geometry) - for data in it: - phi = data.trial_shape - psi = data.test_shape - grad_phi_vec = data.trial_shape_grad - grad_psi_vec = data.test_shape_grad - - # EG gradient transformation TODO move to function space - if isinstance(trial, EnrichedGalerkinFunctionSpace): - # for EDG, the shape function is already vectorial and does not have to be multiplied by e_vec - grad_phi_vec = jac_affine * grad_phi_vec - elif not vectorial_spaces: - grad_phi_vec = ( - (e_vec(geometry.dimensions, component_trial) * phi) - .jacobian(ref_symbols_list) - .T - ) - # same for test space - if isinstance(test, EnrichedGalerkinFunctionSpace): - # for EDG, the shape function is already vectorial and does not have to be multiplied by e_vec - grad_psi_vec = jac_affine * grad_psi_vec - elif not vectorial_spaces: - grad_psi_vec = ( - (e_vec(geometry.dimensions, component_test) * psi) - .jacobian(ref_symbols_list) - .T - ) - - # setup of the form expression with tabulation - if blending != IdentityMap(): - raise HOGException("Not implemented") - # chain rule, premultiply with transposed inverse jacobians of the affine trafo - # the results are tensors of order 2 - # + tabulate affine transformed gradients (can only do this due to incoming, micro-element dependent blending jacobian) - jac_affine_inv_T_grad_phi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_phi", - jac_affine_inv.T * grad_phi_vec, - ) - ) - jac_affine_inv_T_grad_psi_symbols = sp.Matrix( - tabulation.register_factor( - "jac_affine_inv_T_grad_psi", - jac_affine_inv.T * grad_psi_vec, + if density_function_space: + rho, dof_symbols = fem_function_on_element( + density_function_space, + geometry, + symbolizer, + domain="reference", + function_id="rho", + ) + + grad_rho, _ = fem_function_gradient_on_element( + density_function_space, + geometry, + symbolizer, + domain="reference", + function_id="grad_rho", + dof_symbols=dof_symbols, + ) + + with TimedLogger( + f"integrating {mat.shape[0] * mat.shape[1]} expressions", + level=logging.DEBUG, + ): + for data in it: + phi = data.trial_shape + psi = data.test_shape + # print("phi = ", psi.shape) + # phi_psi_jac_affine_det = tabulation.register_factor( + # "phi_psi_jac_affine_det", + # sp.Matrix([phi * psi * jac_affine_det]), + # )[0] + if blending == IdentityMap(): + form = ( + dot(((jac_affine_inv.T * grad_rho) / rho[0]), phi) + * psi + * jac_affine_det ) - ) - - # transform gradients according to blending map - jac_blending_T_jac_affine_inv_T_grad_phi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_phi_symbols - ) - jac_blending_T_jac_affine_inv_T_grad_psi = ( - jac_blending_inv.T * jac_affine_inv_T_grad_psi_symbols - ) - - # extract the symmetric part - sym_grad_phi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_phi - + jac_blending_T_jac_affine_inv_T_grad_phi.T - ) - sym_grad_psi = 0.5 * ( - jac_blending_T_jac_affine_inv_T_grad_psi - + jac_blending_T_jac_affine_inv_T_grad_psi.T - ) - - # double contract everything + determinants - form = ( - double_contraction(2 * mu[0, 0] * sym_grad_phi, sym_grad_psi) + else: + form = ( + dot( + ( + (jac_blending_inv.T * jac_affine_inv.T * grad_rho) + / rho[0] + ), + phi, + ) + * psi * jac_affine_det * jac_blending_det - ) - - else: - # chain rule, premultiply with transposed inverse jacobians of affine trafo - # the results are tensors of order 2 - jac_affine_inv_T_grad_phi = jac_affine_inv.T * grad_phi_vec - jac_affine_inv_T_grad_psi = jac_affine_inv.T * grad_psi_vec - - # now let's compute the σ(φ) and ε(ψ) in reference space - sigma = lamb * jac_affine_inv_T_grad_phi.trace() * sp.eye(jac_affine_inv_T_grad_phi.shape[0]) + mu * ( - jac_affine_inv_T_grad_phi + jac_affine_inv_T_grad_phi.T - ) - eps = 0.5 * ( - jac_affine_inv_T_grad_psi + jac_affine_inv_T_grad_psi.T - ) - - # double contract everything + determinants, tabulate the whole contraction - # TODO maybe shorten naming, although its nice to have everything in the name - contract_2_sigma_eps_ref = ( - tabulation.register_factor( - "contract_2_sigma_eps_ref", - double_contraction(sigma, eps) - * jac_affine_det, ) - )[ - 0 - ] - form = (contract_2_sigma_eps_ref) + mat[data.row, data.col] = form - mat[data.row, data.col] = form + return Form(mat, tabulation, symmetric=trial == test, docstring=docstring) - return Form( - mat, - tabulation, - symmetric=vectorial_spaces or (component_trial == component_test), - docstring=docstring, + +def zero_form( + trial: TrialSpace, + test: TestSpace, + geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), +) -> Form: + from hog.recipes.integrands.volume.zero import integrand + + return process_integrand( + integrand, + trial, + test, + geometry, + symbolizer, + blending=blending, + is_symmetric=trial == test, + docstring="", ) diff --git a/hog/forms_boundary.py b/hog/forms_boundary.py new file mode 100644 index 0000000000000000000000000000000000000000..5fde334d33540ce2165cf674eded1374d8f8c48f --- /dev/null +++ b/hog/forms_boundary.py @@ -0,0 +1,220 @@ +# 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 typing import Optional +from hog.element_geometry import ElementGeometry +from hog.function_space import FunctionSpace, TrialSpace, TestSpace +from hog.symbolizer import Symbolizer +from hog.blending import GeometryMap, IdentityMap +from hog.integrand import process_integrand, Form + + +def mass_boundary( + trial: TrialSpace, + test: TestSpace, + volume_geometry: ElementGeometry, + boundary_geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), +) -> Form: + docstring = f""" +Mass operator. + +Geometry map: {blending} + +Weak formulation + + u: trial function (space: {trial}) + v: test function (space: {test}) + + ∫ uv ds +""" + + from hog.recipes.integrands.boundary.mass import integrand as integrand + + return process_integrand( + integrand, + trial, + test, + volume_geometry, + symbolizer, + blending=blending, + boundary_geometry=boundary_geometry, + is_symmetric=trial == test, + docstring=docstring, + ) + + +def freeslip_momentum_weak_boundary( + trial: TrialSpace, + test: TestSpace, + volume_geometry: ElementGeometry, + boundary_geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), + function_space_mu: Optional[FunctionSpace] = None, +) -> Form: + docstring = f""" +Weak (Nitsche) free-slip boundary operator for an analytical outward normal. + +The normal is specified via an affine mapping of the coordinates: + + n(x) = A x + b + +where x = (x_1, x_2, x_3) are the physical coordinates of the position on the boundary. + +n is normalized automatically. + +This enables for instance to specify some simple cases like + + normals away from the origin: + A = I, b = 0 + + normals towards the origin: + A = -I, b = 0 + + normals in one coordinate direction (e.g. in x-direction) + A = 0, b = (1, 0, 0)ᵀ + +Weak formulation + + From + Davies et al. + Towards automatic finite-element methods for geodynamics via Firedrake + in Geosci. Model Dev. (2022) + DOI: 10.5194/gmd-15-5127-2022 + + + u: trial function (space: {trial}) + v: test function (space: {test}) + n: outward normal + + − ∫_Γ 𝑣 ⋅ 𝑛 𝑛 ⋅ (𝜇 [∇𝑢 + (∇𝑢)ᵀ]) ⋅ 𝑛 𝑑𝑠 + − ∫_Γ 𝑛 ⋅ (𝜇 [∇𝑣 + (∇𝑣)ᵀ]) ⋅ 𝑛 𝑢 ⋅ 𝑛 𝑑𝑠 + + ∫_Γ C_n 𝜇 𝑣 ⋅ 𝑛 𝑢 ⋅ 𝑛 𝑑𝑠 + + +Geometry map: {blending} + +""" + + from hog.recipes.integrands.boundary.freeslip_nitsche_momentum import ( + integrand as integrand, + ) + + return process_integrand( + integrand, + trial, + test, + volume_geometry, + symbolizer, + blending=blending, + boundary_geometry=boundary_geometry, + is_symmetric=trial == test, + fe_coefficients={"mu": function_space_mu}, + docstring=docstring, + ) + + +def freeslip_divergence_weak_boundary( + trial: TrialSpace, + test: TestSpace, + volume_geometry: ElementGeometry, + boundary_geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), +) -> Form: + docstring = f""" +Weak (Nitsche) free-slip boundary operator for an analytical outward normal. + +From + Davies et al. + Towards automatic finite-element methods for geodynamics via Firedrake + in Geosci. Model Dev. (2022) + DOI: 10.5194/gmd-15-5127-2022 + +Weak formulation + + u: trial function (space: {trial}) + v: test function (space: {test}) + n: outward normal + + − ∫_Γ 𝑣 𝑛 ⋅ 𝑢 𝑑𝑠 + +Geometry map: {blending} + +""" + + from hog.recipes.integrands.boundary.freeslip_nitsche_divergence import ( + integrand as integrand, + ) + + return process_integrand( + integrand, + trial, + test, + volume_geometry, + symbolizer, + blending=blending, + boundary_geometry=boundary_geometry, + docstring=docstring, + ) + + +def freeslip_gradient_weak_boundary( + trial: TrialSpace, + test: TestSpace, + volume_geometry: ElementGeometry, + boundary_geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), +) -> Form: + docstring = f""" +Weak (Nitsche) free-slip boundary operator for an analytical outward normal. + +From + Davies et al. + Towards automatic finite-element methods for geodynamics via Firedrake + in Geosci. Model Dev. (2022) + DOI: 10.5194/gmd-15-5127-2022 + +Weak formulation + + u: trial function (space: {trial}) + v: test function (space: {test}) + n: outward normal + + − ∫_Γ 𝑛 ⋅ 𝑣 𝑢 𝑑𝑠 + +Geometry map: {blending} + +""" + + from hog.recipes.integrands.boundary.freeslip_nitsche_gradient import ( + integrand as integrand, + ) + + return process_integrand( + integrand, + trial, + test, + volume_geometry, + symbolizer, + blending=blending, + boundary_geometry=boundary_geometry, + docstring=docstring, + ) diff --git a/hog/forms_facets.py b/hog/forms_facets.py index 5229ceb41fc76d370e0c9093f49322c3bb473ddb..daff842ae003bd86ca62323cbeb3d6c10665fab6 100644 --- a/hog/forms_facets.py +++ b/hog/forms_facets.py @@ -33,7 +33,7 @@ from hog.external_functions import ( ScalarVariableCoefficient2D, ScalarVariableCoefficient3D, ) -from hog.function_space import FunctionSpace +from hog.function_space import FunctionSpace, TrialSpace, TestSpace from hog.math_helpers import ( dot, inv, @@ -51,7 +51,9 @@ sigma_0 = 3 def _affine_element_vertices( volume_element_geometry: ElementGeometry, symbolizer: Symbolizer -) -> Tuple[List[sp.Matrix], List[sp.Matrix], List[sp.Matrix], sp.Matrix, sp.Matrix, sp.Matrix]: +) -> Tuple[ + List[sp.Matrix], List[sp.Matrix], List[sp.Matrix], sp.Matrix, sp.Matrix, sp.Matrix +]: """Helper function that returns the symbols of the affine points of two neighboring elements. Returns a tuple of @@ -145,8 +147,7 @@ def trafo_ref_interface_to_ref_element( affine_points_E = affine_points_E2 affine_point_E_opposite = affine_point_E2_opposite else: - raise HOGException( - "Invalid element type (should be 'inner' or 'outer')") + raise HOGException("Invalid element type (should be 'inner' or 'outer')") # First we compute the transformation from the interface reference space to the affine interface. trafo_ref_interface_to_affine_interface_E = trafo_ref_to_affine( @@ -168,8 +169,8 @@ def trafo_ref_interface_to_ref_element( def stokes_p0_stabilization( interface_type: str, - test_element: FunctionSpace, - trial_element: FunctionSpace, + test_element: TestSpace, + trial_element: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, @@ -247,7 +248,6 @@ def stokes_p0_stabilization( level=logging.DEBUG, ): for data in it: - # TODO: fix this by introducing extra symbols for the shape functions phi = data.trial_shape psi = data.test_shape @@ -274,28 +274,26 @@ def stokes_p0_stabilization( gamma = 0.1 if interface_type == "inner": - form = ((gamma * volume_interface) * - phi * psi) * volume_interface + form = ((gamma * volume_interface) * phi * psi) * volume_interface elif interface_type == "outer": - form = (-(gamma * volume_interface) * - phi * psi) * volume_interface + form = (-(gamma * volume_interface) * phi * psi) * volume_interface - mat[data.row, data.col] = facet_quad.integrate(form, symbolizer)[0].subs( - reference_symbols[volume_element_geometry.dimensions - 1], 0 - ) + mat[data.row, data.col] = facet_quad.integrate(form, symbolizer)[ + 0 + ].subs(reference_symbols[volume_element_geometry.dimensions - 1], 0) return mat def diffusion_sip_facet( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, - blending: GeometryMap = IdentityMap() + blending: GeometryMap = IdentityMap(), ) -> sp.Matrix: r""" Interface integrals for the symmetric interior penalty formulation for the (constant-coeff.) Laplacian. @@ -427,15 +425,15 @@ def diffusion_sip_facet( level=logging.DEBUG, ): for data in it: - # TODO: fix this by introducing extra symbols for the shape functions phi = data.trial_shape psi = data.test_shape grad_phi = data.trial_shape_grad grad_psi = data.test_shape_grad - shape_symbols = ["xi_shape_0", "xi_shape_1", - "xi_shape_2"][:volume_element_geometry.dimensions] + shape_symbols = ["xi_shape_0", "xi_shape_1", "xi_shape_2"][ + : volume_element_geometry.dimensions + ] phi = phi.subs(zip(reference_symbols, shape_symbols)) psi = psi.subs(zip(reference_symbols, shape_symbols)) grad_phi = grad_phi.subs(zip(reference_symbols, shape_symbols)) @@ -468,37 +466,35 @@ def diffusion_sip_facet( if interface_type == "inner": form = ( -0.5 - * dot(grad_psi*jac_affine_inv_E1, outward_normal)[0, 0] + * dot(grad_psi * jac_affine_inv_E1, outward_normal)[0, 0] * phi - 0.5 - * dot(grad_phi*jac_affine_inv_E1, outward_normal)[0, 0] + * dot(grad_phi * jac_affine_inv_E1, outward_normal)[0, 0] * psi - + (sigma_0 / volume_interface ** beta_0) * phi * psi + + (sigma_0 / volume_interface**beta_0) * phi * psi ) * volume_interface elif interface_type == "outer": form = ( 0.5 - * dot(grad_psi*jac_affine_inv_E1, outward_normal)[0, 0] + * dot(grad_psi * jac_affine_inv_E1, outward_normal)[0, 0] * phi - 0.5 - * dot(grad_phi*jac_affine_inv_E2, outward_normal)[0, 0] + * dot(grad_phi * jac_affine_inv_E2, outward_normal)[0, 0] * psi - - (sigma_0 / volume_interface ** beta_0) * phi * psi + - (sigma_0 / volume_interface**beta_0) * phi * psi ) * volume_interface elif interface_type == "dirichlet": form = ( - -dot(grad_psi*jac_affine_inv_E1, - outward_normal)[0, 0] * phi - - dot(grad_phi*jac_affine_inv_E1, outward_normal)[0, 0] - * psi - + (4 * sigma_0 / volume_interface ** beta_0) * phi * psi + -dot(grad_psi * jac_affine_inv_E1, outward_normal)[0, 0] * phi + - dot(grad_phi * jac_affine_inv_E1, outward_normal)[0, 0] * psi + + (4 * sigma_0 / volume_interface**beta_0) * phi * psi ) * volume_interface - mat[data.row, data.col] = facet_quad.integrate(form, symbolizer)[0].subs( - reference_symbols[volume_element_geometry.dimensions - 1], 0 - ) + mat[data.row, data.col] = facet_quad.integrate(form, symbolizer)[ + 0 + ].subs(reference_symbols[volume_element_geometry.dimensions - 1], 0) return mat @@ -612,19 +608,16 @@ def diffusion_sip_rhs_dirichlet( coeff_class = ScalarVariableCoefficient2D elif isinstance(volume_element_geometry, TetrahedronElement): coeff_class = ScalarVariableCoefficient3D - g = coeff_class(sp.Symbol("g"), 0, * - trafo_ref_interface_to_affine_interface) + g = coeff_class(sp.Symbol("g"), 0, *trafo_ref_interface_to_affine_interface) with TimedLogger( f"integrating {mat.shape[0] * mat.shape[1]} expressions", level=logging.DEBUG, ): for i in range(function_space.num_dofs(volume_element_geometry)): - # TODO: fix this by introducing extra symbols for the shape functions phi = function_space.shape(volume_element_geometry)[i] - grad_phi = function_space.grad_shape( - volume_element_geometry)[i] + grad_phi = function_space.grad_shape(volume_element_geometry)[i] shape_symbols = ["xi_shape_0", "xi_shape_1"] phi = phi.subs(zip(reference_symbols, shape_symbols)) @@ -643,9 +636,8 @@ def diffusion_sip_rhs_dirichlet( form = ( 1 * ( - -dot(jac_affine_inv_E1.T * - grad_phi, outward_normal)[0, 0] - + (4 * sigma_0 / volume_interface ** beta_0) * phi + -dot(jac_affine_inv_E1.T * grad_phi, outward_normal)[0, 0] + + (4 * sigma_0 / volume_interface**beta_0) * phi ) * g * volume_interface diff --git a/hog/forms_facets_vectorial.py b/hog/forms_facets_vectorial.py index db52401c2a664e5ba286249367daefc207c258ad..5779447ffaf479d6c38d67cdb8d95dc8b5c4220b 100644 --- a/hog/forms_facets_vectorial.py +++ b/hog/forms_facets_vectorial.py @@ -31,7 +31,7 @@ from hog.external_functions import ( ScalarVariableCoefficient2D, ScalarVariableCoefficient3D, ) -from hog.function_space import FunctionSpace +from hog.function_space import FunctionSpace, TrialSpace, TestSpace from hog.math_helpers import ( dot, inv, @@ -52,8 +52,8 @@ from hog.function_space import EnrichedGalerkinFunctionSpace def diffusion_sip_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, @@ -227,8 +227,8 @@ def diffusion_sip_facet_vectorial( def diffusion_iip_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, @@ -382,8 +382,8 @@ def diffusion_iip_facet_vectorial( def divergence_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, transpose: bool, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, @@ -540,8 +540,8 @@ def symm_grad(grad, jac): def epsilon_sip_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, @@ -760,8 +760,8 @@ def epsilon_sip_facet_vectorial( def epsilon_nip_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, @@ -1092,8 +1092,8 @@ def epsilon_sip_rhs_dirichlet_vectorial( def diffusion_nip_facet_vectorial( interface_type: str, - test_element_1: FunctionSpace, - trial_element_2: FunctionSpace, + test_element_1: TestSpace, + trial_element_2: TrialSpace, volume_element_geometry: ElementGeometry, facet_quad: Quadrature, symbolizer: Symbolizer, diff --git a/hog/forms_vectorial.py b/hog/forms_vectorial.py index a5619145cbe36452bd936a8525be1cf4e5da61b4..af5d447112e90f8a9bab054f6a7813824eb01b6b 100644 --- a/hog/forms_vectorial.py +++ b/hog/forms_vectorial.py @@ -30,8 +30,14 @@ from hog.fem_helpers import ( element_matrix_iterator, scalar_space_dependent_coefficient, ) -from hog.forms import Form -from hog.function_space import FunctionSpace, EnrichedGalerkinFunctionSpace, N1E1Space +from hog.integrand import Form +from hog.function_space import ( + FunctionSpace, + EnrichedGalerkinFunctionSpace, + N1E1Space, + TrialSpace, + TestSpace, +) from hog.math_helpers import inv, abs, det, double_contraction, dot, curl from hog.quadrature import Quadrature, Tabulation from hog.symbolizer import Symbolizer @@ -40,8 +46,8 @@ from hog.sympy_extensions import fast_subs def diffusion_vectorial( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, @@ -99,8 +105,8 @@ def diffusion_vectorial( def mass_vectorial( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, @@ -149,8 +155,8 @@ def mass_vectorial( def mass_n1e1( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -159,8 +165,8 @@ def mass_n1e1( raise HOGException("mass_n1e1 form is only implemented for N1E1.") with TimedLogger("assembling mass matrix", level=logging.DEBUG): - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) + jac_affine = symbolizer.jac_ref_to_affine(geometry) + jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry) jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() if isinstance(blending, ExternalMap): @@ -195,8 +201,8 @@ def mass_n1e1( def divergence_vectorial( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, transpose: bool, geometry: ElementGeometry, quad: Quadrature, @@ -264,8 +270,8 @@ def divergence_vectorial( def curl_curl( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -287,7 +293,7 @@ def curl_curl( raise HOGException("curl-curl form is only implemented for N1E1.") with TimedLogger("assembling curl-curl matrix", level=logging.DEBUG): - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) + jac_affine = symbolizer.jac_ref_to_affine(geometry) jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() if isinstance(blending, ExternalMap): @@ -320,8 +326,8 @@ def curl_curl( def curl_curl_plus_mass( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -353,8 +359,8 @@ Strong formulation with TimedLogger("assembling curl_curl_plus_mass matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions) - jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions) + jac_affine = symbolizer.jac_ref_to_affine(geometry) + jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry) jac_affine_det = symbolizer.abs_det_jac_ref_to_affine() if isinstance(blending, ExternalMap): @@ -460,8 +466,8 @@ Strong formulation curl_curl_symbol = tabulation.register_factor( "curl_curl_det", curl_curl - )[0] - mass_symbol = tabulation.register_factor("mass_det", mass)[0] + ) + mass_symbol = tabulation.register_factor("mass_det", mass) form = alpha * curl_curl_symbol + beta * mass_symbol @@ -471,8 +477,8 @@ Strong formulation def linear_form_vectorial( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, quad: Quadrature, symbolizer: Symbolizer, diff --git a/hog/function_space.py b/hog/function_space.py index 8ee718ed5551fb85631fc5a49066fe16fa974889..af4088369061277242670f6742b05fd230a9cdc1 100644 --- a/hog/function_space.py +++ b/hog/function_space.py @@ -14,14 +14,14 @@ # 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 abc import ABC, abstractmethod from pyclbr import Function -from typing import Any, List, Optional, Protocol +from typing import Any, List, NewType, Optional, Union import sympy as sp from hog.element_geometry import ( ElementGeometry, TriangleElement, - EmbeddedTriangle, TetrahedronElement, ) from hog.exception import HOGException @@ -29,34 +29,40 @@ from hog.math_helpers import grad, hessian from hog.symbolizer import Symbolizer -class FunctionSpace(Protocol): +class FunctionSpace(ABC): """Representation of a finite element function space.""" @property + @abstractmethod def family(self) -> str: """The common name of this FEM space.""" ... @property + @abstractmethod def is_vectorial(self) -> bool: """Whether shape functions are scalar or vector valued.""" ... @property + @abstractmethod def is_continuous(self) -> bool: """Whether functions in this space are continuous across elements.""" ... @property + @abstractmethod def degree(self) -> int: """The polynomial degree of the shape functions.""" ... @property + @abstractmethod def symbolizer(self) -> Symbolizer: """The symbolizer used to construct this object.""" ... + @abstractmethod def shape( self, geometry: ElementGeometry, @@ -89,8 +95,6 @@ class FunctionSpace(Protocol): """ if domain in ["ref", "reference"]: symbols = self.symbolizer.ref_coords_as_list(geometry.dimensions) - if isinstance(geometry, EmbeddedTriangle): - symbols = self.symbolizer.ref_coords_as_list(geometry.dimensions - 1) basis_functions_gradients = [ grad(f, symbols) for f in self.shape(geometry, domain=domain, dof_map=dof_map) @@ -127,6 +131,14 @@ class FunctionSpace(Protocol): """The number of DoFs per element.""" return len(self.shape(geometry)) + @abstractmethod + def __eq__(self, other: Any) -> bool: + ... + + +TrialSpace = NewType("TrialSpace", FunctionSpace) +TestSpace = NewType("TestSpace", FunctionSpace) + class LagrangianFunctionSpace(FunctionSpace): """Representation of a finite element function spaces. @@ -201,6 +213,7 @@ class LagrangianFunctionSpace(FunctionSpace): elif ( isinstance(geometry, TriangleElement) + and geometry.dimensions == geometry.space_dimension and self.family in ["Lagrange"] and self._degree == 2 ): @@ -216,14 +229,16 @@ class LagrangianFunctionSpace(FunctionSpace): ] elif ( - isinstance(geometry, EmbeddedTriangle) + isinstance(geometry, TriangleElement) + and geometry.dimensions == geometry.space_dimension - 1 and self.family in ["Lagrange"] and self._degree == 0 ): basis_functions = [sp.sympify(1)] elif ( - isinstance(geometry, EmbeddedTriangle) + isinstance(geometry, TriangleElement) + and geometry.dimensions == geometry.space_dimension - 1 and self.family in ["Lagrange"] and self._degree == 1 ): @@ -234,7 +249,8 @@ class LagrangianFunctionSpace(FunctionSpace): ] elif ( - isinstance(geometry, EmbeddedTriangle) + isinstance(geometry, TriangleElement) + and geometry.dimensions == geometry.space_dimension - 1 and self.family in ["Lagrange"] and self._degree == 2 ): @@ -359,15 +375,27 @@ class TensorialVectorFunctionSpace(FunctionSpace): (0, 0, N_1), ... (0, 0, N_n). + + This class also enables specifying only a single component to get the shape functions, e.g. only for component 1 + (starting to count components at 0) + + (0, N_1, 0), + ... + (0, N_n, 0), + """ - def __init__(self, function_space: FunctionSpace): + def __init__( + self, function_space: FunctionSpace, single_component: Union[None, int] = None + ): """ Initializes a tensorial vector function space from a scalar function space. :param function_space: the (scalar) component function space + :param single_component: set to the component that shall be non-zero - None if all components shall be present """ self._component_function_space = function_space + self._single_component = single_component @property def is_vectorial(self) -> bool: @@ -393,6 +421,10 @@ class TensorialVectorFunctionSpace(FunctionSpace): def component_function_space(self) -> FunctionSpace: return self._component_function_space + @property + def single_component(self) -> Union[int, None]: + return self._single_component + def _to_vector( self, phi: sp.MatrixBase, component: int, dimensions: int ) -> sp.MatrixBase: @@ -408,26 +440,43 @@ class TensorialVectorFunctionSpace(FunctionSpace): domain: str = "reference", dof_map: Optional[List[int]] = None, ) -> List[sp.MatrixBase]: - dim = geometry.dimensions shape_functions = self._component_function_space.shape( geometry, domain, dof_map ) - return [ - self._to_vector(phi, c, dim) for c in range(dim) for phi in shape_functions - ] + if self._single_component is None: + return [ + self._to_vector(phi, c, dim) + for c in range(dim) + for phi in shape_functions + ] + else: + return [ + self._to_vector(phi, self._single_component, dim) + for phi in shape_functions + ] def __eq__(self, other: Any) -> bool: if type(self) != type(other): return False if not hasattr(other, "_component_function_space"): return False - return self._component_function_space == other._component_function_space + if not hasattr(other, "single_component"): + return False + return ( + self.component_function_space == other.component_function_space + and self.single_component == other.single_component + ) def __str__(self): - return f"TensorialVectorSpace({self._component_function_space})" + component = ( + "" + if self.single_component is None + else f", component {self.single_component}" + ) + return f"TensorialVectorSpace({self._component_function_space}{component})" def __repr__(self): return str(self) @@ -498,6 +547,9 @@ class EnrichedGalerkinFunctionSpace(FunctionSpace): """Returns the number of DoFs per element.""" return len(self.shape(geometry)) + def __eq__(self, other: Any) -> bool: + return type(self) == type(other) + def __str__(self): return f"EnrichedDG" diff --git a/hog/hyteg_form_template.py b/hog/hyteg_form_template.py index 6a2bccd5779658e35d9edfbbe050f5caab0939ae..f0d43087871aab1c97978286f5a0a4776f6eafbb 100644 --- a/hog/hyteg_form_template.py +++ b/hog/hyteg_form_template.py @@ -21,7 +21,7 @@ from hog.ast import Assignment, CodeBlock from hog.exception import HOGException from hog.quadrature import Quadrature from hog.symbolizer import Symbolizer -from hog.function_space import (FunctionSpace, N1E1Space) +from hog.function_space import N1E1Space, TrialSpace, TestSpace from hog.element_geometry import ElementGeometry from hog.code_generation import code_block_from_element_matrix from hog.multi_assignment import Member @@ -91,8 +91,9 @@ class HyTeGIntegrator: ) return info - def _setup_methods(self) -> Tuple[str, str, List[str], List[str], List[str], List[str], List[Member]]: - + def _setup_methods( + self, + ) -> Tuple[str, str, List[str], List[str], List[str], List[str], List[Member]]: rows, cols = self.element_matrix.shape # read from input array of computational vertices @@ -110,9 +111,7 @@ class HyTeGIntegrator: integrate_impl = {} for integrate_matrix_element in self.integrate_matrix_elements: - if integrate_matrix_element[0] == "all": - method_name = "integrateAll" element_matrix_sliced = self.element_matrix cpp_override = True @@ -121,15 +120,13 @@ class HyTeGIntegrator: output_assignments = [] for row in range(rows): for col in range(cols): - lhs = self.symbolizer.output_element_matrix_access( - row, col) + lhs = self.symbolizer.output_element_matrix_access(row, col) rhs = self.symbolizer.element_matrix_entry(row, col) output_assignments.append( Assignment(lhs, rhs, is_declaration=False) ) elif integrate_matrix_element[0] == "row": - integrate_row = integrate_matrix_element[1] method_name = f"integrateRow{integrate_row}" @@ -144,8 +141,7 @@ class HyTeGIntegrator: output_assignments = [] for col in range(cols): lhs = self.symbolizer.output_element_matrix_access(0, col) - rhs = self.symbolizer.element_matrix_entry( - integrate_row, col) + rhs = self.symbolizer.element_matrix_entry(integrate_row, col) output_assignments.append( Assignment(lhs, rhs, is_declaration=False) ) @@ -164,8 +160,7 @@ class HyTeGIntegrator: if self.not_implemented: code_block_code = "" else: - code_block_code = "\n ".join( - code_block.to_code().splitlines()) + code_block_code = "\n ".join(code_block.to_code().splitlines()) hyteg_matrix_type = f"Matrix< real_t, {output_rows}, {output_cols} >" @@ -183,8 +178,7 @@ class HyTeGIntegrator: return f" void {prefix}{method_name}( const std::array< Point3D, {self.geometry.num_vertices} >& {'' if without_argnames else 'coords'}, {hyteg_matrix_type}& {'' if without_argnames else 'elMat'} ) const{override_str}" integrate_decl[integrate_matrix_element] = ( - " " + - "\n ".join(self._docstring(code_block).splitlines()) + "\n" + " " + "\n ".join(self._docstring(code_block).splitlines()) + "\n" ) integrate_decl[ integrate_matrix_element @@ -213,8 +207,7 @@ class HyTeGIntegrator: fd_code = " " + "\n ".join(fd.declaration().splitlines()) helper_methods_decl.append(fd_code) fd_code = " " + "\n ".join( - fd.implementation( - name_prefix=self.class_name + "::").splitlines() + fd.implementation(name_prefix=self.class_name + "::").splitlines() ) helper_methods_impl.append(fd_code) @@ -230,12 +223,11 @@ class HyTeGIntegrator: class HyTeGFormClass: - def __init__( self, name: str, - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, integrators: List[HyTeGIntegrator], description: str = "", ): @@ -266,22 +258,20 @@ class HyTeGFormClass: for f in self.integrators: members += f.members - members = sorted(set(members), key = lambda m: m.name_constructor) + members = sorted(set(members), key=lambda m: m.name_constructor) if not members: return "" default_constructor = f'{self.name}() {{ WALBERLA_ABORT("Not implemented."); }}' - ctr_prms = ", ".join( - [f"{m.dtype} {m.name_constructor}" for m in members]) + ctr_prms = ", ".join([f"{m.dtype} {m.name_constructor}" for m in members]) init_list = "\n , ".join( [f"{m.name_member}({m.name_constructor})" for m in members] ) - member_decl = "\n ".join( - [f"{m.dtype} {m.name_member};" for m in members]) + member_decl = "\n ".join([f"{m.dtype} {m.name_member};" for m in members]) constructor_string = f""" public: @@ -298,7 +288,6 @@ class HyTeGFormClass: return constructor_string def to_code(self, header: bool = True) -> str: - file_string = [] if isinstance(self.trial, N1E1Space): @@ -346,7 +335,6 @@ class HyTeGFormClass: class HyTeGForm: - NAMESPACE_OPEN = "namespace hyteg {\nnamespace forms {" NAMESPACE_CLOSE = "} // namespace forms\n} // namespace hyteg" @@ -354,8 +342,8 @@ class HyTeGForm: def __init__( self, name: str, - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, formClasses: List[HyTeGFormClass], description: str = "", ): @@ -366,7 +354,6 @@ class HyTeGForm: self.description = description def to_code(self, header: bool = True) -> str: - file_string = [] if isinstance(self.trial, N1E1Space): @@ -374,7 +361,9 @@ class HyTeGForm: elif self.trial.degree == self.test.degree: super_class = f"form_hyteg_base/P{self.trial.degree}FormHyTeG" else: - super_class = f"form_hyteg_base/P{self.trial.degree}ToP{self.test.degree}FormHyTeG" + super_class = ( + f"form_hyteg_base/P{self.trial.degree}ToP{self.test.degree}FormHyTeG" + ) if header: includes = "\n".join( diff --git a/hog/integrand.py b/hog/integrand.py new file mode 100644 index 0000000000000000000000000000000000000000..81586f34781bb8bda245bfa4b72aad9d83650b05 --- /dev/null +++ b/hog/integrand.py @@ -0,0 +1,500 @@ +# 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/>. + +""" +As of writing this documentation, the HOG supports the generation of operators from bilinear forms with the structure + + ∫ F dx + ∫ G ds + +where F and G are integrands that typically contain trial and test functions, their derivatives, and/or other terms. + +In the HOG, the integrands are expressed with respect to the reference element! +That means, the integral transformations from the physical space to the reference space have to be done manually. +Specifically, the HOG takes care of approximating integrals of the form + + ∫_T K d(x_T) + +where T is a reference element and x_T are the reference coordinates. That means that various pull-back mappings, +the transformation theorem (https://en.wikipedia.org/wiki/Integration_by_substitution#Substitution_for_multiple_variables), +etc. have to be written out manually. There are several reasons for that, e.g.: + + 1. It is not clear if/how we can decide what the additional terms look like (for instance, they look different for + volume, boundary, and manifold integrals - even when the same function spaces are used). + Although it is certainly possible to automate that, it requires a careful design. FEniCS, Firedrake and co have + been doing that successfully, however it would require a lot of effort to implement here. + + 2. Tabulation (i.e., the precomputation of certain inter-element-invariant terms) is very hard to automate + efficiently. However, one central goal of the HOG is aggressive optimization, and we do not want to remove that + granularity. + + 3. Developer time-constraints :) + +This module contains various functions and data structures to formulate the integrands, i.e. what is K above. +The actual integration and code generation is handled somewhere else. +""" + +from typing import Callable, List, Union, Tuple, Any, Dict +from dataclasses import dataclass, asdict, fields, field + +import sympy as sp + +from hog.exception import HOGException +from hog.function_space import ( + FunctionSpace, + TrialSpace, + TestSpace, + TensorialVectorFunctionSpace, + LagrangianFunctionSpace, +) +from hog.element_geometry import ElementGeometry +from hog.quadrature import Quadrature, Tabulation +from hog.symbolizer import Symbolizer +from hog.blending import GeometryMap, IdentityMap, ExternalMap, ParametricMap +from hog.fem_helpers import ( + create_empty_element_matrix, + element_matrix_iterator, + fem_function_on_element, + fem_function_gradient_on_element, + scalar_space_dependent_coefficient, + jac_affine_to_physical, + trafo_ref_to_physical, +) +from hog.math_helpers import inv, det + + +@dataclass +class Form: + """ + Wrapper class around the local system matrix that carries some additional information such as whether the bilinear + form is symmetric and a docstring. + """ + + integrand: sp.MatrixBase + tabulation: Tabulation + symmetric: bool + free_symbols: List[sp.Symbol] = field(default_factory=lambda: list()) + docstring: str = "" + + def integrate(self, quad: Quadrature, symbolizer: Symbolizer) -> sp.Matrix: + """Integrates the form using the passed quadrature directly, i.e. without tabulations or loops.""" + mat = self.tabulation.inline_tables(self.integrand) + + for row in range(mat.rows): + for col in range(mat.cols): + if self.symmetric and row > col: + mat[row, col] = mat[col, row] + else: + mat[row, col] = quad.integrate(mat[row, col], symbolizer) + + return mat + + +@dataclass +class IntegrandSymbols: + """ + The members of this class are the terms that are supported by the HOG and therefore available to the user for + the formulation of integrands of integrals over reference elements. + + Always make sure to check whether a term that is required in an integrand is available here first before + constructing it otherwise. + + See :func:`~process_integrand` for more details. + """ + + @staticmethod + def fields(): + """ + Convenience method to return a list of all symbols/functions that can be used for the integrand construction. + """ + return [f.name for f in fields(IntegrandSymbols) if not f.name.startswith("_")] + + # Jacobian from reference to affine space. + jac_a: sp.Matrix | None = None + # Its inverse. + jac_a_inv: sp.Matrix | None = None + # The absolute of its determinant. + jac_a_abs_det: sp.Symbol | None = None + + # Jacobian from affine to physical space. + jac_b: sp.Matrix | None = None + # Its inverse. + jac_b_inv: sp.Matrix | None = None + # The absolute of its determinant. + jac_b_abs_det: sp.Symbol | None = None + + # Hessian of the mapping from affine to physical space. + hessian_b: sp.Matrix | None = None + + # The trial shape function (reference space!). + u: sp.Expr | None = None + # The gradient of the trial shape function (reference space!). + grad_u: sp.Matrix | None = None + # The Hessian of the trial shape function (reference space!). + hessian_u: sp.Matrix | None = None + + # The test shape function (reference space!). + v: sp.Expr | None = None + # The gradient of the test shape function (reference space!). + grad_v: sp.Matrix | None = None + # The Hessian of the test shape function (reference space!). + hessian_v: sp.Matrix | None = None + + # The physical coordinates. + x: sp.Matrix | None = None + + # A dict of finite element functions that can be used as function parameters. + # The keys are specified by the strings that are passed to process_integrand. + k: Dict[str, sp.Symbol] | None = None + # A list of the gradients of the parameter finite element functions. + # The keys are specified by the strings that are passed to process_integrand. + grad_k: Dict[str, sp.Matrix] | None = None + + # The geometry of the volume element. + volume_geometry: ElementGeometry | None = None + + # The geometry of the boundary element. + boundary_geometry: ElementGeometry | None = None + + # If a boundary geometry is available, this is populated with the Jacobian of the affine mapping from the reference + # space of the boundary element to the computational (affine) space. + # The reference space has the dimensions of the boundary element. + # The affine space has the space dimension (aka the dimension of the space it is embedded in) of the boundary + # element. + jac_a_boundary: sp.Matrix | None = None + + # A callback to generate free symbols that can be chosen by the user later. + # + # To get (and register new symbols) simply pass a list of symbol names to this function. + # It returns sympy symbols that can be safely used in the integrand: + # + # n_x, n_y, n_z = scalars(["n_x", "n_y", "n_z"]) + # + # or for a single symbol + # + # a = scalars("a") + # + # or use the sympy-like space syntax + # + # a, b = scalars("a b") + # + # Simply using sp.Symbol will not work since the symbols must be registered in the generator. + scalars: Callable[[str | List[str]], sp.Symbol | List[sp.Symbol]] | None = None + + # Same as scalars, but returns the symbols arranged as matrices. + # + # For a matrix with three rows and two columns: + # + # A = matrix("A", 3, 2) + # + matrix: Callable[[str, int, int], sp.Matrix] | None = None + + # A callback to tabulate (aka precompute) terms that are identical on all elements of the same type. + # + # Simply enclose such a factor with this function, e.g., replace + # + # some_term = jac_a_inv.T * grad_u + # + # with + # + # some_term = tabulate(jac_a_inv.T * grad_u) + # + # Use at your own risk, you may get wrong code if used on terms that are not element-invariant! + # + # For debugging, you can also give the table an optional name: + # + # some_term = tabulate(jac_a_inv.T * grad_u, factor_name="jac_grad_u") + # + tabulate: Callable[[Union[sp.Expr, sp.Matrix], str], sp.Matrix] | None = None + + +def process_integrand( + integrand: Callable[..., Any], + trial: TrialSpace, + test: TestSpace, + volume_geometry: ElementGeometry, + symbolizer: Symbolizer, + blending: GeometryMap = IdentityMap(), + boundary_geometry: ElementGeometry | None = None, + fe_coefficients: Dict[str, Union[FunctionSpace, None]] | None = None, + is_symmetric: bool = False, + docstring: str = "", +) -> Form: + """ + Constructs an element matrix (:class:`~Form` object) from an integrand. + + Note that this function does not specify the loop structure of the kernel. + Make sure to pass the result into the correct methods later on (specifically, take care that boundary integrals are + actually executed on the boundary, volume integrals over all elements). + + Integrands are passed in as a callable (aka function). For instance: + + .. code-block:: python + + # The arguments of the function must begin with an asterisk (*), followed by + # keyword arguments, followed by the unused keyword arguments (**_). All + # keyword arguments must be members of the IntegrandSymbols class. + # + # The function must return the integrand. You can use functions from the + # module hog.math_helpers module. + # + # Many integrands are already implemented under hog/recipes/integrands/. + + def my_diffusion_integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + tabulate, + **_, + ): + return ( + double_contraction( + jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), + jac_b_inv.T * tabulate(jac_a_inv.T * grad_v), + ) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) + + The callable (here `my_diffusion_integrand`, not `my_diffusion_integrand()`) is then passed to this function: + + .. code-block:: python + + form = process_integrand( my_diffusion_integrand, trial, test, ... ) + + + :param integrand: an integrand callable + :param trial: the finite-element trial function space + :param test: the finite-element test function space + :param volume_geometry: the geometry of the volume element + :param symbolizer: a Symbolizer instance + :param blending: an optional blending map e.g., for curved geometries + :param boundary_geometry: the geometry to integrate over for boundary integrals - passed through to the callable via + the IntegrandSymbols object + :param fe_coefficients: a dictionary of type (str, FunctionSpace) that are names and spaces for scalar + 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 docstring: documentation of the integrand/bilinear form - will end up in the docstring of the generated code + """ + + if fe_coefficients is None: + fe_coefficients = {} + + s = IntegrandSymbols() + + #################### + # Element geometry # + #################### + + s.volume_geometry = volume_geometry + + if boundary_geometry is not None: + if boundary_geometry.dimensions != boundary_geometry.space_dimension - 1: + raise HOGException( + "Since you are integrating over a boundary, the boundary element's space dimension should be larger " + "than its dimension." + ) + + if boundary_geometry.space_dimension != volume_geometry.space_dimension: + raise HOGException("All geometries must be embedded in the same space.") + + s.boundary_geometry = boundary_geometry + + ############## + # Tabulation # + ############## + + tabulation = Tabulation(symbolizer) + + def _tabulate( + factor: Union[sp.Expr, sp.Matrix], factor_name: str = "tabulated_and_untitled" + ) -> sp.Matrix: + if isinstance(factor, sp.Expr): + factor = sp.Matrix([factor]) + + return tabulation.register_factor(factor_name, factor) + + s.tabulate = _tabulate + + ################################ + # Scalar and matrix parameters # + ################################ + + free_symbols = set() + + def _scalars(symbol_names: str | List[str]) -> sp.Symbol | List[sp.Symbol]: + nonlocal free_symbols + symbs = sp.symbols(symbol_names) + if isinstance(symbs, list): + free_symbols |= set(symbs) + elif isinstance(symbs, sp.Symbol): + free_symbols.add(symbs) + else: + raise HOGException( + f"I did not expect sp.symbols() to return whatever this is: {type(symbs)}" + ) + return symbs + + def _matrix(base_name: str, rows: int, cols: int) -> sp.Matrix: + symbs = _scalars( + [f"{base_name}_{row}_{col}" for row in range(rows) for col in range(cols)] + ) + return sp.Matrix(symbs).reshape(rows, cols) + + s.scalars = _scalars + s.matrix = _matrix + + ################### + # FE coefficients # + ################### + + fe_coefficients_modified = {k: v for k, v in fe_coefficients.items()} + + special_name_of_micromesh_coeff = "micromesh" + if isinstance(blending, ParametricMap): + # We add a vector coefficient for the parametric mapping here. + if special_name_of_micromesh_coeff in fe_coefficients: + raise HOGException( + 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) + ) + ) + + s.k = dict() + s.grad_k = dict() + for name, coefficient_function_space in fe_coefficients_modified.items(): + if coefficient_function_space is None: + k = scalar_space_dependent_coefficient( + name, volume_geometry, symbolizer, blending=blending + ) + grad_k = None + else: + k, dof_symbols = fem_function_on_element( + coefficient_function_space, + volume_geometry, + symbolizer, + domain="reference", + function_id=name, + basis_eval=tabulation.register_phi_evals( + coefficient_function_space.shape(volume_geometry) + ), + ) + + if isinstance(k, sp.Matrix) and k.shape == (1, 1): + k = k[0, 0] + + grad_k, _ = fem_function_gradient_on_element( + coefficient_function_space, + volume_geometry, + symbolizer, + domain="reference", + function_id=f"grad_{name}", + dof_symbols=dof_symbols, + ) + s.k[name] = k + s.grad_k[name] = grad_k + + ############################## + # Jacobians and determinants # + ############################## + + s.jac_a = symbolizer.jac_ref_to_affine(volume_geometry) + s.jac_a_inv = symbolizer.jac_ref_to_affine_inv(volume_geometry) + s.jac_a_abs_det = symbolizer.abs_det_jac_ref_to_affine() + + if boundary_geometry is not None: + s.jac_a_boundary = symbolizer.jac_ref_to_affine(boundary_geometry) + + if isinstance(blending, IdentityMap): + s.jac_b = sp.eye(volume_geometry.space_dimension) + s.jac_b_inv = sp.eye(volume_geometry.space_dimension) + s.jac_b_abs_det = 1 + elif isinstance(blending, ExternalMap): + s.jac_b = jac_affine_to_physical(volume_geometry, symbolizer) + s.jac_b_inv = inv(s.jac_b) + s.jac_b_abs_det = abs(det(s.jac_b)) + elif isinstance(blending, ParametricMap): + s.jac_a = sp.eye(volume_geometry.space_dimension) + s.jac_a_inv = sp.eye(volume_geometry.space_dimension) + s.jac_a_abs_det = 1 + + if boundary_geometry is not None: + raise HOGException( + "Boundary integrals not tested with parametric mappings yet. " + "We have to handle/set the affine Jacobian at the boundary appropriately.\n" + "Dev note to future me: since we assume the boundary element to have the last ref coord zero, " + "I suppose we can set this thing to:\n" + " ⎡ 1 ⎤ \n" + " ⎣ 0 ⎦ \n" + "in 2D and to\n" + " ⎡ 1 0 ⎤ \n" + " | 0 1 | \n" + " ⎣ 0 0 ⎦ \n" + "in 3D." + ) + + s.jac_b = s.grad_k[special_name_of_micromesh_coeff].T + s.jac_b_inv = inv(s.jac_b) + s.jac_b_abs_det = abs(det(s.jac_b)) + + s.x = s.k[special_name_of_micromesh_coeff] + + else: + s.jac_b = symbolizer.jac_affine_to_blending(volume_geometry.space_dimension) + s.jac_b_inv = symbolizer.jac_affine_to_blending_inv( + volume_geometry.space_dimension + ) + s.jac_b_abs_det = symbolizer.abs_det_jac_affine_to_blending() + s.hessian_b = symbolizer.hessian_blending_map(volume_geometry.dimensions) + + if not isinstance(blending, ParametricMap): + s.x = trafo_ref_to_physical(volume_geometry, symbolizer, blending) + + ####################################### + # Assembling the local element matrix # + ####################################### + + mat = create_empty_element_matrix(trial, test, volume_geometry) + it = element_matrix_iterator(trial, test, volume_geometry) + + for data in it: + s.u = data.trial_shape + s.grad_u = data.trial_shape_grad + s.hessian_u = data.trial_shape_hessian + + s.v = data.test_shape + s.grad_v = data.test_shape_grad + s.hessian_v = data.test_shape_hessian + + mat[data.row, data.col] = integrand(**asdict(s)) + + free_symbols_sorted = sorted(list(free_symbols), key=lambda x: str(x)) + + return Form( + mat, + tabulation, + symmetric=is_symmetric, + free_symbols=free_symbols_sorted, + docstring=docstring, + ) diff --git a/hog/manifold_forms.py b/hog/manifold_forms.py index f53fcfed70272186bfd12aa2cc7afb418bc7bb9c..1bd4e414c69320a80a1f7b4c82d0e7d5f44b544f 100644 --- a/hog/manifold_forms.py +++ b/hog/manifold_forms.py @@ -17,8 +17,7 @@ import logging import sympy as sp -from hog.element_geometry import ElementGeometry -from hog.element_geometry import EmbeddedTriangle +from hog.element_geometry import ElementGeometry, TriangleElement from hog.exception import HOGException from hog.fem_helpers import ( trafo_ref_to_affine, @@ -28,20 +27,20 @@ from hog.fem_helpers import ( create_empty_element_matrix, element_matrix_iterator, ) -from hog.function_space import FunctionSpace +from hog.function_space import TrialSpace, TestSpace from hog.math_helpers import dot, inv, abs, det, double_contraction, e_vec from hog.quadrature import Tabulation from hog.symbolizer import Symbolizer from hog.logger import TimedLogger from hog.blending import GeometryMap, ExternalMap, IdentityMap -from hog.forms import Form +from hog.integrand import Form from hog.manifold_helpers import face_projection, embedded_normal def laplace_beltrami( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -65,8 +64,10 @@ Weak formulation "Trial space must be equal to test space to assemble laplace beltrami matrix." ) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Laplace Beltrami only works for embedded triangles") + if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) @@ -114,8 +115,8 @@ Weak formulation def manifold_mass( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -139,8 +140,10 @@ Weak formulation "Trial space must be equal to test space to assemble laplace beltrami matrix." ) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) with TimedLogger("assembling laplace beltrami matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) @@ -183,8 +186,8 @@ Weak formulation def manifold_vector_mass( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -210,8 +213,12 @@ Weak formulation with TimedLogger("assembling manifold vector mass matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) projection = face_projection(geometry, symbolizer, blending=blending) @@ -254,8 +261,8 @@ Weak formulation def manifold_normal_penalty( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -281,8 +288,12 @@ Weak formulation with TimedLogger("assembling manifold normal penalty matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) normal = embedded_normal(geometry, symbolizer, blending) @@ -325,8 +336,8 @@ Weak formulation def manifold_divergence( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -351,8 +362,12 @@ Weak formulation with TimedLogger("assembling manifold div matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) jac_affine = jac_ref_to_affine(geometry, symbolizer) @@ -394,8 +409,8 @@ Weak formulation def manifold_vector_divergence( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -425,8 +440,12 @@ Weak formulation f"WARNING: Manifold vector divergence does NOT compute derivative of matrix P yet. Generated form might not work as intended." ) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) projection_mat = face_projection(geometry, symbolizer, blending=blending) @@ -481,8 +500,8 @@ Weak formulation def manifold_epsilon( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -509,8 +528,12 @@ Weak formulation with TimedLogger("assembling manifold epsilon matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) projection_mat = face_projection(geometry, symbolizer, blending=blending) @@ -560,7 +583,7 @@ Weak formulation "epsilon_epsilon_prod", double_contraction(phi_epsilon, psi_epsilon) * fundamental_form_det**0.5, - )[0] + ) mat[data.row, data.col] = form @@ -573,8 +596,8 @@ Weak formulation def vector_laplace_beltrami( - trial: FunctionSpace, - test: FunctionSpace, + trial: TrialSpace, + test: TestSpace, geometry: ElementGeometry, symbolizer: Symbolizer, blending: GeometryMap = IdentityMap(), @@ -601,8 +624,12 @@ Weak formulation with TimedLogger("assembling vector laplace beltrami matrix", level=logging.DEBUG): tabulation = Tabulation(symbolizer) - if not isinstance(geometry, EmbeddedTriangle): - raise HOGException("Manifold forms only work for embedded triangles.") + if not ( + isinstance(geometry, TriangleElement) and geometry.space_dimension == 3 + ): + raise HOGException( + "Laplace Beltrami only works for triangles embedded in 3D space." + ) projection_mat = face_projection(geometry, symbolizer, blending=blending) @@ -649,7 +676,7 @@ Weak formulation "phi_psi_prod", double_contraction(phi_projected_grad, psi_projected_grad) * fundamental_form_det**0.5, - )[0] + ) mat[data.row, data.col] = form diff --git a/hog/manifold_helpers.py b/hog/manifold_helpers.py index 4d2bf663ab74c27ceb6f1ed96d9b5914e4a3a855..125e36a957d1b08d2bdf17696da538354ca99084 100644 --- a/hog/manifold_helpers.py +++ b/hog/manifold_helpers.py @@ -19,7 +19,7 @@ import sympy as sp from hog.exception import HOGException -from hog.element_geometry import ElementGeometry, EmbeddedTriangle +from hog.element_geometry import ElementGeometry, TriangleElement from hog.symbolizer import Symbolizer from hog.blending import GeometryMap, IdentityMap from hog.fem_helpers import jac_affine_to_physical @@ -33,13 +33,13 @@ def embedded_normal( ) -> sp.Matrix: """Returns an unoriented unit normal vector for an embedded triangle.""" - if not isinstance(geometry, EmbeddedTriangle): + if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3): raise HOGException( - "Embedded normal vectors are only defined for embedded triangles." + "Embedded normal vectors are only defined for triangles embedded in 3D space." ) vert_points = symbolizer.affine_vertices_as_vectors( - geometry.dimensions, geometry.num_vertices + geometry.space_dimension, geometry.num_vertices ) span0 = vert_points[1] - vert_points[0] span1 = vert_points[2] - vert_points[0] @@ -67,9 +67,9 @@ def face_projection( ) -> sp.Matrix: """Returns a projection matrix for an embedded triangle.""" - if not isinstance(geometry, EmbeddedTriangle): + if not (isinstance(geometry, TriangleElement) and geometry.space_dimension == 3): raise HOGException( - "Projection matrices are only defined for embedded triangles." + "Projection matrices are only defined for triangles embedded in 3D space." ) normal = embedded_normal(geometry, symbolizer, blending=blending) diff --git a/hog/math_helpers.py b/hog/math_helpers.py index a0fdeaefecb7dca8f1c64e9c5e5416ee633c07ce..fdddc355d2390f0cc12a3b8f80ba6ec68d23169f 100644 --- a/hog/math_helpers.py +++ b/hog/math_helpers.py @@ -118,11 +118,9 @@ def e_vec(dim: int, idx: int) -> sp.Matrix: return e -def inv(mat: sp.Matrix) -> sp.Matrix: +def inv(mat: sp.MatrixBase) -> sp.Matrix: """Optimized implementation of matrix inverse for 2x2, and 3x3 matrices. Use this instead of sympy's mat**-1.""" - if isinstance(mat, sp.Expr): - return 1 / mat - elif isinstance(mat, sp.Matrix): + if isinstance(mat, sp.MatrixBase): rows, cols = mat.shape if rows != cols: raise HOGException("Matrix is not square - cannot be inverted.") @@ -155,6 +153,8 @@ def inv(mat: sp.Matrix) -> sp.Matrix: return invmat else: return mat**-1 + elif isinstance(mat, sp.Expr): + return 1 / mat def normal(plane: List[sp.Matrix], d: sp.Matrix) -> sp.Matrix: diff --git a/hog/operator_generation/function_space_impls.py b/hog/operator_generation/function_space_impls.py index 130539f6040a59defd1ed11b668adcffa775ed5f..7b3a51fe8857c1a2c7b476ee7a9a143cbfda8e4b 100644 --- a/hog/operator_generation/function_space_impls.py +++ b/hog/operator_generation/function_space_impls.py @@ -103,9 +103,15 @@ class FunctionSpaceImpl(ABC): elif isinstance(func_space, TensorialVectorFunctionSpace): if isinstance(func_space.component_function_space, LagrangianFunctionSpace): if func_space.component_function_space.degree == 1: - impl_class = P1VectorFunctionSpaceImpl + if func_space.single_component is None: + impl_class = P1VectorFunctionSpaceImpl + else: + impl_class = P1FunctionSpaceImpl elif func_space.component_function_space.degree == 2: - impl_class = P2VectorFunctionSpaceImpl + if func_space.single_component is None: + impl_class = P2VectorFunctionSpaceImpl + else: + impl_class = P2FunctionSpaceImpl else: raise HOGException( "TensorialVectorFunctionSpaces not supported for the chosen components." @@ -166,8 +172,15 @@ class FunctionSpaceImpl(ABC): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: - """Returns a list of local dof values on the current element.""" + """ + Returns a list of local dof values on the current element. + + The element_vertex_ordering is a list that specifies the ordering of the reference vertices. + The ordering in which the DoFs are returned depends on this list. The "default" ordering is + [0, 1, ..., num_vertices - 1]. + """ ... @abstractmethod @@ -186,6 +199,7 @@ class FunctionSpaceImpl(ABC): geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: """Returns HyTeG code that computes the basis/DoF transformation and a symbolic expression of the result. @@ -214,6 +228,10 @@ class FunctionSpaceImpl(ABC): assembling operators into matrices, these transformations must be "baked into" the matrix because vectors are assembled locally and our communication routine is not performed during the operator application. + + The element_vertex_ordering is a list that specifies the ordering of the reference vertices. + The ordering in which the DoFs are returned depends on this list. The "default" ordering is + [0, 1, ..., num_vertices - 1]. """ ... @@ -296,10 +314,14 @@ class P1FunctionSpaceImpl(FunctionSpaceImpl): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: vertex_dof_indices = micro_element_to_vertex_indices( geometry, element_type, element_index ) + + vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering] + vertex_array_indices = [ dof_idx.array_index(geometry, indexing_info) for dof_idx in vertex_dof_indices @@ -320,6 +342,7 @@ class P1FunctionSpaceImpl(FunctionSpaceImpl): geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: return ( CustomCodeNode("", [], []), @@ -422,10 +445,14 @@ class P2FunctionSpaceImpl(FunctionSpaceImpl): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: vertex_dof_indices = micro_element_to_vertex_indices( geometry, element_type, element_index ) + + vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering] + edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices) vrtx_array_idcs = [ @@ -454,6 +481,7 @@ class P2FunctionSpaceImpl(FunctionSpaceImpl): geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: return ( CustomCodeNode("", [], []), @@ -557,6 +585,7 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: """ Returns the element-local DoFs (field accesses) in a list (i.e., linearized). @@ -606,6 +635,9 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl): vertex_dof_indices = micro_element_to_vertex_indices( geometry, element_type, element_index ) + + vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering] + vertex_array_indices = [ dof_idx.array_index(geometry, indexing_info) for dof_idx in vertex_dof_indices @@ -628,6 +660,7 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl): geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: return ( CustomCodeNode("", [], []), @@ -756,6 +789,7 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: """ Returns the element-local DoFs (field accesses) in a list (i.e., linearized). @@ -772,6 +806,8 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl): geometry, element_type, element_index ) + vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering] + edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices) vertex_array_indices = [ @@ -801,14 +837,17 @@ class P2VectorFunctionSpaceImpl(FunctionSpaceImpl): return f"P2VectorFunction< {self.type_descriptor.pystencils_type} >" def includes(self) -> Set[str]: - return {f"hyteg/p2functionspace/P2VectorFunction.hpp", - f"hyteg/p2functionspace/P2Function.hpp"} + return { + f"hyteg/p2functionspace/P2VectorFunction.hpp", + f"hyteg/p2functionspace/P2Function.hpp", + } def dof_transformation( self, geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: return ( CustomCodeNode("", [], []), @@ -872,10 +911,14 @@ class N1E1FunctionSpaceImpl(FunctionSpaceImpl): element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], indexing_info: IndexingInfo, + element_vertex_ordering: List[int], ) -> List[Field.Access]: vertex_dof_indices = micro_element_to_vertex_indices( geometry, element_type, element_index ) + + vertex_dof_indices = [vertex_dof_indices[i] for i in element_vertex_ordering] + edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices) edge_array_indices = [ dof_idx.array_index(geometry, indexing_info) for dof_idx in edge_dof_indices @@ -897,7 +940,14 @@ class N1E1FunctionSpaceImpl(FunctionSpaceImpl): geometry: ElementGeometry, element_index: Tuple[int, int, int], element_type: Union[FaceType, CellType], + element_vertex_ordering: List[int], ) -> Tuple[CustomCodeNode, sp.MatrixBase]: + + if element_vertex_ordering != [0, 1, 2, 3]: + raise HOGException( + "Element vertex re-ordering not supported for Nédélec elements (yet)." + ) + Macro = {2: "Face", 3: "Cell"}[geometry.dimensions] macro = {2: "face", 3: "cell"}[geometry.dimensions] name = "basisTransformation" diff --git a/hog/operator_generation/indexing.py b/hog/operator_generation/indexing.py index dd445364aec0157b14a58ede2d6f61af509fd749..e33704ecc6112ec192671029c69e88ed92e66095 100644 --- a/hog/operator_generation/indexing.py +++ b/hog/operator_generation/indexing.py @@ -22,7 +22,6 @@ import sympy as sp from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement from hog.exception import HOGException -from hog.function_space import FunctionSpace, N1E1Space from hog.symbolizer import Symbolizer from pystencils.integer_functions import int_div @@ -35,7 +34,6 @@ import sympy as sp from hog.element_geometry import ElementGeometry, TriangleElement, TetrahedronElement from hog.exception import HOGException -from hog.function_space import FunctionSpace from hog.symbolizer import Symbolizer from pystencils.integer_functions import int_div @@ -173,7 +171,6 @@ def num_microcells_per_cell(level: int) -> int: def linear_macro_cell_size(width: int) -> int: - if USE_SYMPY_INT_DIV: return sympy_int_div((width + 2) * (width + 1) * width, 6) else: @@ -334,7 +331,7 @@ class IndexingInfo: ) -def all_element_types(dimensions: int) -> Union[List[FaceType], List[CellType]]: +def all_element_types(dimensions: int) -> List[Union[FaceType, CellType]]: if dimensions == 2: return [FaceType.GRAY, FaceType.BLUE] if dimensions == 3: diff --git a/hog/operator_generation/kernel_types.py b/hog/operator_generation/kernel_types.py index 778077cd48eff87879dc4bccebc868293eefa71d..f0ba9f13523100cb3c2862dd1bcce32faea0644f 100644 --- a/hog/operator_generation/kernel_types.py +++ b/hog/operator_generation/kernel_types.py @@ -37,21 +37,59 @@ from hog.cpp_printing import ( ) from hog.element_geometry import ElementGeometry from hog.exception import HOGException -from hog.function_space import FunctionSpace +from hog.function_space import FunctionSpace, TrialSpace, TestSpace from hog.operator_generation.function_space_impls import FunctionSpaceImpl from hog.operator_generation.indexing import FaceType, CellType from hog.operator_generation.pystencils_extensions import create_generic_fields from hog.operator_generation.types import HOGPrecision, HOGType, hyteg_type +from hog.operator_generation.loop_strategies import LoopStrategy, SAWTOOTH class KernelType(ABC): - name: str - src_fields: List[FunctionSpaceImpl] - dst_fields: List[FunctionSpaceImpl] - _template: Template - - """Type to specify the type of kernel. - E.g. a GEMv kernel defines multiple source fields and scalar parameters.""" + """ + A HyTeG operator may implement multiple types of methods that perform some kind of operation on a function that + is inferred from the bilinear form. For instance, a (matrix-free) matrix-vector multiplication or the assembly + of its diagonal. + + Certain operations such as setting boundary data to zero or communication have to be executed before the actual + kernel can be executed. + + E.g.: + + ``` + void apply() { + communication() // "pre-processing" + kernel() // actual kernel + post_communication() // "post-processing" + } + ``` + + For some applications, it might be necessary or at least comfortable to execute multiple kernels inside such a + method. For instance, if boundary conditions are applied: + + ``` + // form is sum of volume and boundary integral: + // ∫ ... dΩ + ∫ ... dS + void assemble() { + communication() // "pre-processing" + kernel() // volume kernel (∫ ... dΩ) + kernel_boundary() // boundary kernel (∫ ... dS | additive execution) + post_communication() // "post-processing" + } + ``` + + This class (KernelType) describes the "action" of the kernel (matvec, assembly, ...). + Another class (KernelWrapperType) then describes what kind of pre- and post-processing is required. + + ``` + void assemble() { // from KernelTypeWrapper + communication() // from KernelTypeWrapper + kernel() // from KernelType + IntegrationInfo 1 + kernel_boundary() // from KernelType + IntegrationInfo 2 + post_communication() // from KernelTypeWrapper + } + ``` + """ @abstractmethod def kernel_operation( @@ -76,6 +114,7 @@ class KernelType(ABC): element_type: Union[FaceType, CellType], src_vecs_accesses: List[List[Field.Access]], dst_vecs_accesses: List[List[Field.Access]], + element_vertex_ordering: List[int], ) -> List[ps.astnodes.Node]: """ Operations to be executed after the access resolution and common subexpression elimination on the symbols @@ -90,6 +129,225 @@ class KernelType(ABC): """ ... + +class Apply(KernelType): + def __init__(self): + self.result_prefix = "elMatVec_" + + def kernel_operation( + self, + src_vecs: List[sp.MatrixBase], + dst_vecs: List[sp.MatrixBase], + mat: sp.MatrixBase, + rows: int, + ) -> List[SympyAssignment]: + kernel_ops = mat * src_vecs[0] + + tmp_symbols = sp.numbered_symbols(self.result_prefix) + + kernel_op_assignments = [ + SympyAssignment(tmp, kernel_op) + for tmp, kernel_op in zip(tmp_symbols, kernel_ops) + ] + + return kernel_op_assignments + + def kernel_post_operation( + self, + geometry: ElementGeometry, + element_index: Tuple[int, int, int], + element_type: Union[FaceType, CellType], + src_vecs_accesses: List[List[Field.Access]], + dst_vecs_accesses: List[List[Field.Access]], + element_vertex_ordering: List[int], + ) -> List[ps.astnodes.Node]: + tmp_symbols = sp.numbered_symbols(self.result_prefix) + + # Add and store result to destination. + store_dst_vecs = [ + SympyAssignment(a, a + s) for a, s in zip(dst_vecs_accesses[0], tmp_symbols) + ] + return store_dst_vecs + + +class AssembleDiagonal(KernelType): + def __init__(self): + self.result_prefix = "elMatDiag_" + + def kernel_operation( + self, + src_vecs: List[sp.MatrixBase], + dst_vecs: List[sp.MatrixBase], + mat: sp.MatrixBase, + rows: int, + ) -> List[SympyAssignment]: + kernel_ops = mat.diagonal() + + tmp_symbols = sp.numbered_symbols(self.result_prefix) + + kernel_op_assignments = [ + SympyAssignment(tmp, kernel_op) + for tmp, kernel_op in zip(tmp_symbols, kernel_ops) + ] + + return kernel_op_assignments + + def kernel_post_operation( + self, + geometry: ElementGeometry, + element_index: Tuple[int, int, int], + element_type: Union[FaceType, CellType], + src_vecs_accesses: List[List[Field.Access]], + dst_vecs_accesses: List[List[Field.Access]], + element_vertex_ordering: List[int], + ) -> List[ps.astnodes.Node]: + tmp_symbols = sp.numbered_symbols(self.result_prefix) + + # Add and store result to destination. + store_dst_vecs = [ + SympyAssignment(a, a + s) for a, s in zip(dst_vecs_accesses[0], tmp_symbols) + ] + return store_dst_vecs + + +class Assemble(KernelType): + def __init__( + self, + src: FunctionSpaceImpl, + dst: FunctionSpaceImpl, + ): + self.result_prefix = "elMat_" + + self.src = src + self.dst = dst + + def kernel_operation( + self, + src_vecs: List[sp.MatrixBase], + dst_vecs: List[sp.MatrixBase], + mat: sp.MatrixBase, + rows: int, + ) -> List[SympyAssignment]: + return [ + SympyAssignment(sp.Symbol(f"{self.result_prefix}{r}_{c}"), mat[r, c]) + for r in range(mat.shape[0]) + for c in range(mat.shape[1]) + ] + + def kernel_post_operation( + self, + geometry: ElementGeometry, + element_index: Tuple[int, int, int], + element_type: Union[FaceType, CellType], + src_vecs_accesses: List[List[Field.Access]], + dst_vecs_accesses: List[List[Field.Access]], + element_vertex_ordering: List[int], + ) -> List[ps.astnodes.Node]: + src, dst = dst_vecs_accesses + el_mat = sp.Matrix( + [ + [sp.Symbol(f"{self.result_prefix}{r}_{c}") for c in range(len(src))] + for r in range(len(dst)) + ] + ) + + # apply basis/dof transformations + transform_src_code, transform_src_mat = self.src.dof_transformation( + geometry, element_index, element_type, element_vertex_ordering + ) + transform_dst_code, transform_dst_mat = self.dst.dof_transformation( + geometry, element_index, element_type, element_vertex_ordering + ) + transformed_el_mat = transform_dst_mat.T * el_mat * transform_src_mat + + nr = len(dst) + nc = len(src) + mat_size = nr * nc + + rowIdx, colIdx = create_generic_fields(["rowIdx", "colIdx"], dtype=np.uint64) + # NOTE: The type is 'hyteg_type().pystencils_type', i.e. `real_t` + # (not `self._type_descriptor.pystencils_type`) + # because this function is implemented manually in HyTeG with + # this signature. Passing `np.float64` is not ideal (if `real_t != + # double`) but it makes sure that casts are inserted if necessary + # (though some might be superfluous). + mat = create_generic_fields( + ["mat"], dtype=hyteg_type(HOGPrecision.REAL_T).pystencils_type + )[0] + rowIdxSymb = FieldPointerSymbol(rowIdx.name, rowIdx.dtype, False) + colIdxSymb = FieldPointerSymbol(colIdx.name, colIdx.dtype, False) + matSymb = FieldPointerSymbol(mat.name, mat.dtype, False) + + body: List[ps.astnodes.Node] = [ + CustomCodeNode( + f"std::vector< uint_t > {rowIdxSymb.name}( {nr} );\n" + f"std::vector< uint_t > {colIdxSymb.name}( {nc} );\n" + f"std::vector< {mat.dtype} > {matSymb.name}( {mat_size} );", + [], + [rowIdxSymb, colIdxSymb, matSymb], + ), + ps.astnodes.EmptyLine(), + ] + body += [ + SympyAssignment( + rowIdx.absolute_access((k,), (0,)), CastFunc(dst_access, np.uint64) + ) + for k, dst_access in enumerate(dst) + ] + body += [ + SympyAssignment( + colIdx.absolute_access((k,), (0,)), CastFunc(src_access, np.uint64) + ) + for k, src_access in enumerate(src) + ] + + body += [ + ps.astnodes.EmptyLine(), + ps.astnodes.SourceCodeComment("Apply basis transformation"), + *sorted({transform_src_code, transform_dst_code}, key=lambda n: n._code), + ps.astnodes.EmptyLine(), + ] + + body += [ + SympyAssignment( + mat.absolute_access((r * nc + c,), (0,)), + CastFunc(transformed_el_mat[r, c], mat.dtype), + ) + for r in range(nr) + for c in range(nc) + ] + + body += [ + ps.astnodes.EmptyLine(), + CustomCodeNode( + f"mat->addValues( {rowIdxSymb.name}, {colIdxSymb.name}, {matSymb.name} );", + [ + TypedSymbol("mat", "std::shared_ptr< SparseMatrixProxy >"), + rowIdxSymb, + colIdxSymb, + matSymb, + ], + [], + ), + ] + + return body + + +class KernelWrapperType(ABC): + name: str + src_fields: List[FunctionSpaceImpl] + dst_fields: List[FunctionSpaceImpl] + _template: Template + """ + See documentation of class KernelType. + """ + + @property + @abstractmethod + def kernel_type(self) -> KernelType: + ... + @abstractmethod def includes(self) -> Set[str]: ... @@ -110,11 +368,11 @@ class KernelType(ABC): self._template = Template(self._template.safe_substitute(subs)) -class Apply(KernelType): +class ApplyWrapper(KernelWrapperType): def __init__( self, - src_space: FunctionSpace, - dst_space: FunctionSpace, + src_space: TrialSpace, + dst_space: TestSpace, type_descriptor: HOGType, dims: List[int] = [2, 3], ): @@ -224,39 +482,9 @@ class Apply(KernelType): f'this->stopTiming( "{self.name}" );' ) - def kernel_operation( - self, - src_vecs: List[sp.MatrixBase], - dst_vecs: List[sp.MatrixBase], - mat: sp.MatrixBase, - rows: int, - ) -> List[SympyAssignment]: - kernel_ops = mat * src_vecs[0] - - tmp_symbols = sp.numbered_symbols(self.result_prefix) - - kernel_op_assignments = [ - SympyAssignment(tmp, kernel_op) - for tmp, kernel_op in zip(tmp_symbols, kernel_ops) - ] - - return kernel_op_assignments - - def kernel_post_operation( - self, - geometry: ElementGeometry, - element_index: Tuple[int, int, int], - element_type: Union[FaceType, CellType], - src_vecs_accesses: List[List[Field.Access]], - dst_vecs_accesses: List[List[Field.Access]], - ) -> List[ps.astnodes.Node]: - tmp_symbols = sp.numbered_symbols(self.result_prefix) - - # Add and store result to destination. - store_dst_vecs = [ - SympyAssignment(a, a + s) for a, s in zip(dst_vecs_accesses[0], tmp_symbols) - ] - return store_dst_vecs + @property + def kernel_type(self) -> KernelType: + return Apply() def includes(self) -> Set[str]: return ( @@ -302,7 +530,7 @@ class Apply(KernelType): return [] -class GEMV(KernelType): +class GEMVWrapper(KernelWrapperType): def __init__( self, type_descriptor: HOGType, @@ -325,7 +553,7 @@ class GEMV(KernelType): raise HOGException("Not implemented") -class AssembleDiagonal(KernelType): +class AssembleDiagonalWrapper(KernelWrapperType): def __init__( self, fe_space: FunctionSpace, @@ -340,7 +568,6 @@ class AssembleDiagonal(KernelType): self.src_fields = [] self.dst_fields = [self.dst] self.dims = dims - self.result_prefix = "elMatDiag_" def macro_loop(dim: int) -> str: Macro = {2: "Face", 3: "Cell"}[dim] @@ -387,7 +614,7 @@ class AssembleDiagonal(KernelType): f"\n" f"for ( uint_t level = minLevel_; level <= maxLevel_; level++ )\n" f"{{\n" - f" {self.dst.name}->interpolate( 0, level );\n" + f" {self.dst.name}->setToZero( level );\n" f"\n" f" if ( storage_->hasGlobalCells() )\n" f" {{\n" @@ -413,39 +640,9 @@ class AssembleDiagonal(KernelType): f'this->stopTiming( "{self.name}" );' ) - def kernel_operation( - self, - src_vecs: List[sp.MatrixBase], - dst_vecs: List[sp.MatrixBase], - mat: sp.MatrixBase, - rows: int, - ) -> List[SympyAssignment]: - kernel_ops = mat.diagonal() - - tmp_symbols = sp.numbered_symbols(self.result_prefix) - - kernel_op_assignments = [ - SympyAssignment(tmp, kernel_op) - for tmp, kernel_op in zip(tmp_symbols, kernel_ops) - ] - - return kernel_op_assignments - - def kernel_post_operation( - self, - geometry: ElementGeometry, - element_index: Tuple[int, int, int], - element_type: Union[FaceType, CellType], - src_vecs_accesses: List[List[Field.Access]], - dst_vecs_accesses: List[List[Field.Access]], - ) -> List[ps.astnodes.Node]: - tmp_symbols = sp.numbered_symbols(self.result_prefix) - - # Add and store result to destination. - store_dst_vecs = [ - SympyAssignment(a, a + s) for a, s in zip(dst_vecs_accesses[0], tmp_symbols) - ] - return store_dst_vecs + @property + def kernel_type(self) -> KernelType: + return AssembleDiagonal() def includes(self) -> Set[str]: return {"hyteg/solvers/Smoothables.hpp"} | self.dst.includes() @@ -483,11 +680,11 @@ class AssembleDiagonal(KernelType): ] -class Assemble(KernelType): +class AssembleWrapper(KernelWrapperType): def __init__( self, - src_space: FunctionSpace, - dst_space: FunctionSpace, + src_space: TrialSpace, + dst_space: TestSpace, type_descriptor: HOGType, dims: List[int] = [2, 3], ): @@ -508,7 +705,6 @@ class Assemble(KernelType): self.type_descriptor = type_descriptor self.dims = dims - self.result_prefix = "elMat_" def macro_loop(dim: int) -> str: Macro = {2: "Face", 3: "Cell"}[dim] @@ -563,116 +759,9 @@ class Assemble(KernelType): f'this->stopTiming( "{self.name}" );' ) - def kernel_operation( - self, - src_vecs: List[sp.MatrixBase], - dst_vecs: List[sp.MatrixBase], - mat: sp.MatrixBase, - rows: int, - ) -> List[SympyAssignment]: - return [ - SympyAssignment(sp.Symbol(f"{self.result_prefix}{r}_{c}"), mat[r, c]) - for r in range(mat.shape[0]) - for c in range(mat.shape[1]) - ] - - def kernel_post_operation( - self, - geometry: ElementGeometry, - element_index: Tuple[int, int, int], - element_type: Union[FaceType, CellType], - src_vecs_accesses: List[List[Field.Access]], - dst_vecs_accesses: List[List[Field.Access]], - ) -> List[ps.astnodes.Node]: - src, dst = dst_vecs_accesses - el_mat = sp.Matrix( - [ - [sp.Symbol(f"{self.result_prefix}{r}_{c}") for c in range(len(src))] - for r in range(len(dst)) - ] - ) - - # apply basis/dof transformations - transform_src_code, transform_src_mat = self.src.dof_transformation( - geometry, element_index, element_type - ) - transform_dst_code, transform_dst_mat = self.dst.dof_transformation( - geometry, element_index, element_type - ) - transformed_el_mat = transform_dst_mat.T * el_mat * transform_src_mat - - nr = len(dst) - nc = len(src) - mat_size = nr * nc - - rowIdx, colIdx = create_generic_fields(["rowIdx", "colIdx"], dtype=np.uint64) - # NOTE: The type is 'hyteg_type().pystencils_type', i.e. `real_t` - # (not `self._type_descriptor.pystencils_type`) - # because this function is implemented manually in HyTeG with - # this signature. Passing `np.float64` is not ideal (if `real_t != - # double`) but it makes sure that casts are inserted if necessary - # (though some might be superfluous). - mat = create_generic_fields( - ["mat"], dtype=hyteg_type(HOGPrecision.REAL_T).pystencils_type - )[0] - rowIdxSymb = FieldPointerSymbol(rowIdx.name, rowIdx.dtype, False) - colIdxSymb = FieldPointerSymbol(colIdx.name, colIdx.dtype, False) - matSymb = FieldPointerSymbol(mat.name, mat.dtype, False) - - body: List[ps.astnodes.Node] = [ - CustomCodeNode( - f"std::vector< uint_t > {rowIdxSymb.name}( {nr} );\n" - f"std::vector< uint_t > {colIdxSymb.name}( {nc} );\n" - f"std::vector< {mat.dtype} > {matSymb.name}( {mat_size} );", - [], - [rowIdxSymb, colIdxSymb, matSymb], - ), - ps.astnodes.EmptyLine(), - ] - body += [ - SympyAssignment( - rowIdx.absolute_access((k,), (0,)), CastFunc(dst_access, np.uint64) - ) - for k, dst_access in enumerate(dst) - ] - body += [ - SympyAssignment( - colIdx.absolute_access((k,), (0,)), CastFunc(src_access, np.uint64) - ) - for k, src_access in enumerate(src) - ] - - body += [ - ps.astnodes.EmptyLine(), - ps.astnodes.SourceCodeComment("Apply basis transformation"), - *sorted({transform_src_code, transform_dst_code}, key=lambda n: n._code), - ps.astnodes.EmptyLine(), - ] - - body += [ - SympyAssignment( - mat.absolute_access((r * nc + c,), (0,)), - CastFunc(transformed_el_mat[r, c], mat.dtype), - ) - for r in range(nr) - for c in range(nc) - ] - - body += [ - ps.astnodes.EmptyLine(), - CustomCodeNode( - f"mat->addValues( {rowIdxSymb.name}, {colIdxSymb.name}, {matSymb.name} );", - [ - TypedSymbol("mat", "std::shared_ptr< SparseMatrixProxy >"), - rowIdxSymb, - colIdxSymb, - matSymb, - ], - [], - ), - ] - - return body + @property + def kernel_type(self) -> KernelType: + return Assemble(self.src, self.dst) def includes(self) -> Set[str]: return ( diff --git a/hog/operator_generation/loop_strategies.py b/hog/operator_generation/loop_strategies.py index 9eb28a48cd4277af001d5de9052753a80c3ddcf8..0c370e42b7901a15b0fea556bf75365f608078cf 100644 --- a/hog/operator_generation/loop_strategies.py +++ b/hog/operator_generation/loop_strategies.py @@ -16,7 +16,7 @@ from abc import ABC, abstractmethod import re -from typing import Type, Union +from typing import Type, Union, Dict from pystencils import TypedSymbol from pystencils.astnodes import ( @@ -24,6 +24,7 @@ from pystencils.astnodes import ( Conditional, ResolvedFieldAccess, SourceCodeComment, + LoopOverCoordinate, ) from pystencils.sympyextensions import fast_subs from pystencils.typing import FieldPointerSymbol @@ -33,6 +34,7 @@ import sympy as sp from hog.exception import HOGException from hog.operator_generation.pystencils_extensions import ( loop_over_simplex, + loop_over_simplex_facet, get_innermost_loop, create_field_access, create_micro_element_loops, @@ -96,12 +98,14 @@ class LoopStrategy(ABC): class CUBES(LoopStrategy): - """For the "cubes" loop strategy, we want to update all micro-elements in the current "cube", i.e. loop over all - # element types for the current micro-element index before incrementing. - # This way we only have one loop, but need to insert conditionals that take care of those element types that - # are not existing at the loop boundary. - # The hope is that this strategy induces better cache locality, and that the conditionals can be automatically - # transformed by the loop cutting features of pystencils.""" + """ + For the "cubes" loop strategy, we want to update all micro-elements in the current "cube", i.e. loop over all + element types for the current micro-element index before incrementing. + This way we only have one loop, but need to insert conditionals that take care of those element types that + are not existing at the loop boundary. + The hope is that this strategy induces better cache locality, and that the conditionals can be automatically + transformed by the loop cutting features of pystencils. + """ def __init__(self): super(CUBES, self).__init__() @@ -310,3 +314,120 @@ class FUSEDROWS(LoopStrategy): def __str__(self): return "FUSEDROWS" + + +class BOUNDARY(LoopStrategy): + """ + Special loop strategy that only loops over elements of a specified boundary. + + Concretely, this means that it iterates over all elements with facets (edges in 2D, faces in 3D) that fully overlap + with the specified boundary (one of 3 macro-edges in 2D, one of 4 macro-faces in 3D). + + To loop over multiple boundaries, just construct multiple loops. + + The loop is constructed using conditionals, see the CUBES loop strategy. + """ + + def __init__(self, facet_id: int): + """ + Constructs and initializes the BOUNDARY loop strategy. + + :param facet_id: in [0, 2] for 2D, in [0, 3] for 3D + """ + super(BOUNDARY, self).__init__() + self.facet_id = facet_id + self.element_loops: Dict[Union[FaceType, CellType], LoopOverCoordinate] = dict() + + def create_loop(self, dim, element_index, micro_edges_per_macro_edge): + + if dim == 2: + if self.facet_id not in [0, 1, 2]: + raise HOGException("Invalid facet ID for BOUNDARY loop strategy in 2D.") + + self.element_loops = { + FaceType.GRAY: loop_over_simplex_facet( + dim, micro_edges_per_macro_edge, self.facet_id + ), + } + + elif dim == 3: + if self.facet_id not in [0, 1, 2, 3]: + raise HOGException("Invalid facet ID for BOUNDARY loop strategy in 3D.") + + second_cell_type = { + 0: CellType.BLUE_UP, + 1: CellType.GREEN_UP, + 2: CellType.BLUE_DOWN, + 3: CellType.GREEN_DOWN, + } + + self.element_loops = { + CellType.WHITE_UP: loop_over_simplex_facet( + dim, micro_edges_per_macro_edge, self.facet_id + ), + second_cell_type[self.facet_id]: loop_over_simplex_facet( + dim, micro_edges_per_macro_edge - 1, self.facet_id + ), + } + + return Block( + [ + Block([SourceCodeComment(str(element_type)), loop]) + for element_type, loop in self.element_loops.items() + ] + ) + + def add_body_to_loop(self, loop, body, element_type): + """Register a given loop body to innermost loop of the outer loop corresponding to element_type""" + if element_type not in self.element_loops: + return + innermost_loop = get_innermost_loop(self.element_loops[element_type]) + body = Block(body) + innermost_loop[0].body = body + body.parent = innermost_loop[0] + + def get_inner_bodies(self, loop_body): + return [ + inner_loop.body + for inner_loop in get_innermost_loop(loop_body, return_all_inner=True) + ] + + def add_preloop_for_loop(self, loops, preloop_stmts, element_type): + """add given list of statements directly in front of the loop corresponding to element_type.""" + + if element_type not in self.element_loops: + return loops + + preloop_stmts_lhs_subs = { + stmt.lhs: get_element_replacement(stmt.lhs, element_type) + for stmt in preloop_stmts + } + + if not isinstance(loops, Block): + loops = Block(loops) + + blocks = loops.take_child_nodes() + new_blocks = [] + for block in blocks: + idx_to_slice_at = -1 + for idx, stmt in enumerate(block.args): + if stmt == self.element_loops[element_type]: + idx_to_slice_at = idx + break + + if idx_to_slice_at == -1: + new_blocks.append(block) + else: + new_stmts = ( + block.args[0:idx_to_slice_at] + + preloop_stmts + + block.args[idx_to_slice_at:] + ) + new_block = Block(new_stmts) + new_block.fast_subs(preloop_stmts_lhs_subs) + new_blocks.append(new_block) + + return new_blocks + + def __str__(self): + return "BOUNDARY" diff --git a/hog/operator_generation/operators.py b/hog/operator_generation/operators.py index 34b28661fe21af58c4e2168cab54a6680308f009..130ea291fc8aa4f9606c7ae61af81ad26cd4b2bb 100644 --- a/hog/operator_generation/operators.py +++ b/hog/operator_generation/operators.py @@ -14,10 +14,10 @@ # 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 dataclasses import dataclass +from dataclasses import dataclass, field from enum import auto, Enum import logging -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple, Union import os from textwrap import indent @@ -70,21 +70,33 @@ from pystencils.typing.transformations import add_types from pystencils.backends.cbackend import CustomCodeNode from pystencils.typing.typed_sympy import FieldPointerSymbol -from hog.forms import Form +from hog.integrand import Form from hog.ast import Operations, count_operations from hog.blending import GeometryMap import hog.code_generation import hog.cse from hog.dof_symbol import DoFSymbol -from hog.element_geometry import ElementGeometry +from hog.element_geometry import ( + ElementGeometry, + TriangleElement, + TetrahedronElement, +) from hog.exception import HOGException from hog.operator_generation.indexing import ( all_element_types, element_vertex_coordinates, IndexingInfo, + FaceType, + CellType, ) +from hog.operator_generation.kernel_types import KernelWrapperType from hog.operator_generation.kernel_types import Assemble, KernelType -from hog.operator_generation.loop_strategies import LoopStrategy, SAWTOOTH +from hog.operator_generation.loop_strategies import ( + LoopStrategy, + SAWTOOTH, + BOUNDARY, + CUBES, +) from hog.operator_generation.optimizer import Optimizer, Opts from hog.quadrature import QuadLoop, Quadrature from hog.symbolizer import Symbolizer @@ -94,16 +106,53 @@ from hog.operator_generation.types import HOGType class MacroIntegrationDomain(Enum): """Enum type to specify where to integrate.""" - # Integration over the volume element + # Integration over the volume element. VOLUME = "Volume" + # Integration over the boundary of the domain (for forms like ∫ ... d(∂Ω)). + # + # Note: Having one flag for all boundaries of one "type" is not very flexible. + # At some point there should be additional logic that uses HyTeG's BoundaryUIDs. + # To avoid ambiguity, the operator should (at least if there are boundary integrals) have a + # hyteg::BoundaryCondition constructor parameter, and for each boundary integral one hyteg::BoundaryUID + # parameter. Then the BC is tested by comparing those for each facet. + # + # Like so (application pseudocode in HyTeG): + # + # // generating an operator with one volume and one free-slip boundary integral + # // ∫ F dx + ∫ G ds + # // where ds corresponds to integrating over parts of the boundary that are marked with a specific + # // BoundaryUID + # + # // see HyTeG's documentation and/or BC tutorial + # BoundaryCondition someBC( ... ); + # + # // setting up the BCs + # BoundaryUID freeslipBC = someBC.createFreeslipBC( ... ); + # + # // the generated operator + # MyFancyFreeSlipOperator op( storage, + # ..., + # someBC, // this is what will be tested against - no ambiguity because src + # // and dst func might have different BCs! + # freeslipBC // this is the BCUID that will be used for the boundary integral + # // if there are more boundary integrals there are more UIDs in the + # // constructor (this BCUID is linked to 'ds' above) + # ); + # + + DOMAIN_BOUNDARY = "Domain boundary" + @dataclass class IntegrationInfo: + """Data associated with one integral term and the corresponding loop pattern.""" + geometry: ElementGeometry # geometry of the element, e.g. tetrahedron integration_domain: ( MacroIntegrationDomain # entity of geometry to integrate over, e.g. facet ) + quad: Quadrature # quadrature over integration domain of geometry, e.g. triangle blending: GeometryMap @@ -111,22 +160,33 @@ class IntegrationInfo: quad_loop: Optional[QuadLoop] mat: sp.MatrixBase + loop_strategy: LoopStrategy + + optimizations: Set[Opts] + + free_symbols: List[sp.Symbol] + + name: str = "unknown_integral" docstring: str = "" + boundary_uid_name: str = "" + integrand_name: str = "unknown_integrand" def _str_(self): - return f"Integration Info: {self.geometry}, {self.integration_domain}, mat shape {self.mat.shape}, quad degree {self.quad.degree}, blending {self.blending}" + return f"Integration Info: {self.name}, {self.geometry}, {self.integration_domain}, mat shape {self.mat.shape}, quad degree {self.quad.degree}, blending {self.blending}" def _repr_(self): return str(self) @dataclass -class Kernel: - kernel_type: KernelType - kernel_function: KernelFunction - platform_dependent_funcs: Dict[str, KernelFunction] - operation_count: str - integration_info: IntegrationInfo +class OperatorMethod: + """Collection of kernels and metadata required for each method of an operator.""" + + kernel_wrapper_type: KernelWrapperType + kernel_functions: List[KernelFunction] + platform_dependent_funcs: List[Dict[str, KernelFunction]] + operation_counts: List[str] + integration_infos: List[IntegrationInfo] class CppClassFiles(Enum): @@ -137,6 +197,66 @@ class CppClassFiles(Enum): HEADER_IMPL_AND_VARIANTS = auto() +def micro_vertex_permutation_for_facet( + volume_geometry: ElementGeometry, + element_type: Union[FaceType, CellType], + facet_id: int, +) -> List[int]: + """ + Provides a re-ordering of the micro-vertices such that the facet of the micro-element that coincides with the + macro-facet (which is given by the facet_id parameter) is spanned by the first three returned vertex positions. + + The reordering can then for instance be executed by + + ```python + element_vertex_order = shuffle_order_for_element_micro_vertices( ... ) + + el_vertex_coordinates = [ + el_vertex_coordinates[i] for i in element_vertex_order + ] + ``` + + """ + + if volume_geometry == TriangleElement(): + + if element_type == FaceType.BLUE: + return [0, 1, 2] + + shuffle_order_gray = { + 0: [0, 1, 2], + 1: [0, 2, 1], + 2: [1, 2, 0], + } + + return shuffle_order_gray[facet_id] + + elif volume_geometry == TetrahedronElement(): + + if element_type == CellType.WHITE_DOWN: + return [0, 1, 2, 3] + + # All element types but WHITE_UP only overlap with a single macro-facet. + # It's a different element type for each facet. WHITE_DOWN is never at the boundary. + shuffle_order: Dict[Union[FaceType, CellType], Dict[int, List[int]]] = { + CellType.WHITE_UP: { + 0: [0, 1, 2, 3], + 1: [0, 1, 3, 2], + 2: [0, 2, 3, 1], + 3: [1, 2, 3, 0], + }, + CellType.BLUE_UP: {0: [0, 1, 2, 3]}, + CellType.GREEN_UP: {1: [0, 2, 3, 1]}, + CellType.BLUE_DOWN: {2: [0, 1, 3, 2]}, + CellType.GREEN_DOWN: {3: [1, 2, 3, 0]}, + } + + return shuffle_order[element_type][facet_id] + + else: + raise HOGException("Not implemented.") + + class HyTeGElementwiseOperator: """ This class handles the code generation of HyTeG-type 'elementwise' operators. @@ -150,6 +270,8 @@ class HyTeGElementwiseOperator: "hyteg/edgedofspace/EdgeDoFMacroCell.hpp", "hyteg/primitivestorage/PrimitiveStorage.hpp", "hyteg/LikwidWrapper.hpp", + "hyteg/boundary/BoundaryConditions.hpp", + "hyteg/types/types.hpp", } VAR_NAME_MICRO_EDGES_PER_MACRO_EDGE = "micro_edges_per_macro_edge" @@ -163,19 +285,15 @@ class HyTeGElementwiseOperator: self, name: str, symbolizer: Symbolizer, - kernel_types: List[KernelType], - opts: Set[Opts], + kernel_wrapper_types: List[KernelWrapperType], type_descriptor: HOGType, ): self.name = name self.symbolizer = symbolizer - self.kernel_types = kernel_types # type of kernel: e.g. GEMV + self.kernel_wrapper_types = kernel_wrapper_types # type of kernel: e.g. GEMV - # Holds one element matrix and quad scheme for each ElementGeometry. - self.element_matrices: Dict[int, IntegrationInfo] = {} - - self._optimizer = Optimizer(opts) - self._optimizer_no_vec = Optimizer(opts - {Opts.VECTORIZE, Opts.VECTORIZE512}) + # Each IntegrationInfo object represents one integral of the weak formulation. + self.integration_infos: Dict[int, List[IntegrationInfo]] = {} # set the precision in which the operations are to be performed self._type_descriptor = type_descriptor @@ -183,56 +301,79 @@ class HyTeGElementwiseOperator: # coefficients self.coeffs: Dict[str, FunctionSpaceImpl] = {} # implementations for each kernel, generated at a later stage - self.kernels: List[Kernel] = [] + self.operator_methods: List[OperatorMethod] = [] - def set_element_matrix( + def _add_integral( self, - dim: int, - geometry: ElementGeometry, + name: str, + volume_geometry: ElementGeometry, integration_domain: MacroIntegrationDomain, quad: Quadrature, blending: GeometryMap, form: Form, + loop_strategy: LoopStrategy, + boundary_uid_name: str, + optimizations: Set[Opts], + integrand_name: str | None = None, ) -> None: """ - Use this method to add element matrices to the operator. + Use this method to add integrals to the operator if you know what you are doing. There are helper methods for + adding integrals that are a little simpler to use. - :param dim: the dimensionality of the domain, i.e. the volume - it may be that this does not match the geometry - of the element, e.g., when facet integrals are required, note that only one routine per dim is - created - :param geometry: geometry that shall be integrated over + :param name: some name for this integral (no spaces please) + :param volume_geometry: the volume element (even for boundary integrals pass the element with dim == space_dim) :param integration_domain: where to integrate - see MacroIntegrationDomain - :param mat: the local element matrix - :param quad: the employed quadrature scheme - should match what has been used to integrate the weak form + :param quad: the employed quadrature scheme :param blending: the same geometry map that has been passed to the form + :param form: the integrand + :param loop_strategy: loop pattern over the refined macro-volume - must somehow be compatible with the + integration domain + :param boundary_uid_name: string that defines the name of the boundary UID if this is a boundary integral + the parameter is ignored for volume integrals + :param optimizations: optimizations that shall be applied to this integral + :param integrand_name: while each integral is a separate kernel with a name, for some types of integrals (e.g., + boundary integrals) more than one kernel with the same integrand is added - for some + features (e.g., symbol naming) it is convenient to be able to identify all those + integrals by a string - this (optional) integrand_name does not have to be unique (other + than the name parameter which has to be unique) but can be the same for all integrals + with different domains but the same integrand """ - if dim not in [2, 3]: - raise HOGException("Only supporting 2D and 3D. Dim should be in [2, 3]") - - if integration_domain != MacroIntegrationDomain.VOLUME: - raise HOGException("Only volume integrals supported as of now.") - if dim != geometry.dimensions: - raise HOGException("Only volume integrals supported as of now.") - - if dim in self.element_matrices: + if "".join(name.split()) != name: raise HOGException( - "You are trying to overwrite an already specified integration routine by calling this method twice with " - "the same dim argument." + "Please give the integral an identifier without white space." ) + if integrand_name is None: + integrand_name = name + + if volume_geometry.space_dimension in self.integration_infos: + if name in [ + ii.name + for ii in self.integration_infos[volume_geometry.space_dimension] + ]: + raise HOGException(f"Integral with name {name} already added!") + + if volume_geometry.space_dimension not in [2, 3]: + raise HOGException("Only supporting 2D and 3D. Dim should be in [2, 3]") + + if integration_domain == MacroIntegrationDomain.VOLUME and not ( + isinstance(loop_strategy, SAWTOOTH) or isinstance(loop_strategy, CUBES) + ): + raise HOGException("Invalid loop strategy for volume integrals.") + tables = [] quad_loop = None mat = form.integrand - if self._optimizer[Opts.TABULATE]: + if Opts.TABULATE in optimizations: mat = form.tabulation.resolve_table_accesses(mat, self._type_descriptor) with TimedLogger(f"constructing tables", level=logging.DEBUG): tables = form.tabulation.construct_tables(quad, self._type_descriptor) else: mat = form.tabulation.inline_tables(mat) - if self._optimizer[Opts.QUADLOOPS]: + if Opts.QUADLOOPS in optimizations: quad_loop = QuadLoop( self.symbolizer, quad, @@ -256,25 +397,143 @@ class HyTeGElementwiseOperator: mat[row, col], self.symbolizer, blending ) - self.element_matrices[dim] = IntegrationInfo( - geometry=geometry, - integration_domain=integration_domain, - quad=quad, - blending=blending, - tables=tables, - quad_loop=quad_loop, - mat=mat, - docstring=form.docstring, + if volume_geometry.space_dimension not in self.integration_infos: + self.integration_infos[volume_geometry.space_dimension] = [] + + self.integration_infos[volume_geometry.space_dimension].append( + IntegrationInfo( + name=name, + geometry=volume_geometry, + integration_domain=integration_domain, + quad=quad, + blending=blending, + tables=tables, + quad_loop=quad_loop, + mat=mat, + docstring=form.docstring, + loop_strategy=loop_strategy, + boundary_uid_name=boundary_uid_name, + optimizations=optimizations, + free_symbols=form.free_symbols, + integrand_name=integrand_name, + ) + ) + + def add_volume_integral( + self, + name: str, + volume_geometry: ElementGeometry, + quad: Quadrature, + blending: GeometryMap, + form: Form, + loop_strategy: LoopStrategy, + optimizations: Union[None, Set[Opts]] = None, + ) -> None: + """ + Adds a volume integral to the operator. Wrapper around _add_integral() for volume integrals. + + :param name: some name for this integral (no spaces please) + :param volume_geometry: the volume element + :param quad: the employed quadrature scheme + :param blending: the same geometry map that has been passed to the form + :param form: the integrand + :param loop_strategy: loop pattern over the refined macro-volume - must somehow be compatible with the + integration domain + :param optimizations: optimization applied to this integral + """ + if optimizations is None: + optimizations = set() + + if volume_geometry.dimensions != quad.geometry.dimensions: + raise HOGException( + "The quadrature geometry does not match the volume geometry." + ) + + self._add_integral( + name, + volume_geometry, + MacroIntegrationDomain.VOLUME, + quad, + blending, + form, + loop_strategy, + "", + optimizations, + ) + + def add_boundary_integral( + self, + name: str, + volume_geometry: ElementGeometry, + quad: Quadrature, + blending: GeometryMap, + form: Form, + optimizations: Union[None, Set[Opts]] = None, + ) -> None: + """ + Adds a boundary integral to the operator. Wrapper around _add_integral() for boundary integrals. + + :param name: some name for this integral (no spaces please) + :param volume_geometry: the volume element (not the geometry of the boundary, also no embedded elements, just + the volume element geometry) + :param quad: the employed quadrature scheme - this must use the embedded geometry (e.g., for boundary integrals + in 2D, this should use a LineElement(space_dimension=2)) + :param blending: the same map that has been passed to the form + :param form: the integrand + :param optimizations: optimization applied to this integral + """ + + if optimizations is None: + optimizations = set() + + allowed_boundary_optimizations = {Opts.MOVECONSTANTS} + if optimizations - allowed_boundary_optimizations != set(): + raise HOGException( + f"Only allowed (aka tested and working) optimizations for boundary integrals are " + f"{allowed_boundary_optimizations}." + ) + + if volume_geometry not in [TriangleElement(), TetrahedronElement()]: + raise HOGException( + "Boundary integrals only implemented for triangle and tetrahedral elements." + ) + + if volume_geometry.dimensions - 1 != quad.geometry.dimensions: + raise HOGException( + "The quadrature geometry does not match the boundary geometry." + ) + + # Since we will only integrate over the reference facet that lies on the x-line (2D) or xy-plane (3D) we need to + # set the last reference coordinate to zero since it will otherwise appear as a free, uninitialized variable. + # + # This has to be repeated later before the quadrature is applied in case we are working with symbols. + # + form.integrand = form.integrand.subs( + self.symbolizer.ref_coords_as_list(volume_geometry.dimensions)[-1], 0 ) + for facet_id in range(volume_geometry.num_vertices): + self._add_integral( + name + f"_facet_id_{facet_id}", + volume_geometry, + MacroIntegrationDomain.DOMAIN_BOUNDARY, + quad, + blending, + form, + BOUNDARY(facet_id=facet_id), + name + "_boundary_uid", + optimizations, + integrand_name=name, + ) + def coefficients(self) -> List[FunctionSpaceImpl]: """Returns all coefficients sorted by name. During generation coefficents are detected in the element matrix and stored in the `coeffs` field. Being a Python dictionary, iterating over it yields the coefficients in an arbitrary order. Whenever generating - code for all coefficents it is a good idea to access them in a well - defined order. Most importantly, the order of constructor arguments must + code for all coefficents it is a good idea to access them in a well-defined + order. Most importantly, the order of constructor arguments must be deterministic. """ return sorted(self.coeffs.values(), key=lambda c: c.name) @@ -282,44 +541,45 @@ class HyTeGElementwiseOperator: def generate_class_code( self, dir_path: str, - loop_strategy: LoopStrategy = SAWTOOTH(), class_files: CppClassFiles = CppClassFiles.HEADER_AND_IMPL, clang_format_binary: Optional[str] = None, ) -> None: """ Invokes the code generation process, writing the full operator C++ code to file. - :param dir_path: directory where to write the files - the file names are built automatically - :param loop_strategy: iteration pattern - :param header_only: if True, the entire class (incl. implementation) is written into a single file - :clang_format_binary: path and/or name of binary for clang-format, defaults to None, which turns - off formatting + :param dir_path: directory where to write the files - the file names are built automatically + :param class_files: determines whether header and or impl files are generated + :param clang_format_binary: path and/or name of binary for clang-format, defaults to None, which turns + off formatting """ - # Asking the optimizer if optimizations are valid. - self._optimizer.check_opts_validity(loop_strategy) - # Generate each kernel type (apply, gemv, ...). - self.generate_kernels(loop_strategy) + with TimedLogger( + f"Generating kernels for operator {self.name}", level=logging.INFO + ): + + # Generate each kernel type (apply, gemv, ...). + self.generate_kernels() # Setting up the final C++ class. operator_cpp_class = CppClass( name=self.name, base_classes=sorted( - {base for kt in self.kernel_types for base in kt.base_classes()} + {base for kt in self.kernel_wrapper_types for base in kt.base_classes()} ), ) # Adding form docstring to C++ class form_docstrings = set() - for d, io in self.element_matrices.items(): - form_docstrings.add(io.docstring) + for d, ios in self.integration_infos.items(): + for io in ios: + form_docstrings.add(io.docstring) for form_docstring in form_docstrings: form_docstring_with_slashes = "/// ".join(form_docstring.splitlines(True)) operator_cpp_class.add(CppComment(form_docstring_with_slashes, where="all")) - for kernel_type in self.kernel_types: + for kernel_wrapper_type in self.kernel_wrapper_types: # Setting up communication. - kernel_type.substitute( + kernel_wrapper_type.substitute( { "comm_fe_functions_2D": "\n".join( coeff.pre_communication(2) for coeff in self.coefficients() @@ -331,48 +591,137 @@ class HyTeGElementwiseOperator: ) # Wrapper methods ("hand-crafted") - for kernel_wrapper_cpp_method in kernel_type.wrapper_methods(): + for kernel_wrapper_cpp_method in kernel_wrapper_type.wrapper_methods(): operator_cpp_class.add(kernel_wrapper_cpp_method) # Member variables - for member in kernel_type.member_variables(): + for member in kernel_wrapper_type.member_variables(): operator_cpp_class.add(member) # Add all kernels to the class. - for kernel in self.kernels: - kernel_op_count = "\n".join( - [ - f"Kernel type: {kernel.kernel_type.name}", - f"- quadrature rule: {kernel.integration_info.quad}", - f"- operations per element:", - kernel.operation_count, - ] - ) + for operator_method in self.operator_methods: - if class_files == CppClassFiles.HEADER_IMPL_AND_VARIANTS: - operator_cpp_class.add( - CppMethodWithVariants( - { - platform: CppMethod.from_kernel_function( - kernel, - is_const=True, - visibility="private", - docstring=indent(kernel_op_count, "/// "), - ) - for platform, kernel in kernel.platform_dependent_funcs.items() - } - ) + num_integrals = len(operator_method.integration_infos) + + if num_integrals != len( + operator_method.kernel_functions + ) or num_integrals != len(operator_method.operation_counts): + raise HOGException( + "There should be as many IntegrationInfo (aka integrals) as KernelFunctions (aka kernels)." ) - else: - operator_cpp_class.add( - CppMethod.from_kernel_function( - kernel.kernel_function, - is_const=True, - visibility="private", - docstring=indent(kernel_op_count, "/// "), - ) + + for ( + integration_info, + sub_kernel, + operation_count, + platform_dependent_funcs, + ) in zip( + operator_method.integration_infos, + operator_method.kernel_functions, + operator_method.operation_counts, + operator_method.platform_dependent_funcs, + ): + kernel_docstring = "\n".join( + [ + f"\nIntegral: {integration_info.name}", + f"- volume element: {integration_info.geometry}", + f"- kernel type: {operator_method.kernel_wrapper_type.name}", + f"- loop strategy: {integration_info.loop_strategy}", + f"- quadrature rule: {integration_info.quad}", + f"- blending map: {integration_info.blending}", + f"- operations per element:", + operation_count, + ] ) + if class_files == CppClassFiles.HEADER_IMPL_AND_VARIANTS: + operator_cpp_class.add( + CppMethodWithVariants( + { + platform: CppMethod.from_kernel_function( + plat_dep_kernel, + is_const=True, + visibility="private", + docstring=indent( + kernel_docstring, + "/// ", + ), + ) + for platform, plat_dep_kernel in platform_dependent_funcs.items() + } + ) + ) + else: + operator_cpp_class.add( + CppMethod.from_kernel_function( + sub_kernel, + is_const=True, + visibility="private", + docstring=indent( + kernel_docstring, + "/// ", + ), + ) + ) + + # Free symbols that shall be settable through the ctor. + # Those are only free symbols that have been explicitly defined by the user. Other undefined sympy symbols are + # not (and are not supposed to be) handled here. + # + # We append the name of the integrand (!= name of the kernel) to the free symbols we found in the + # integrand to make sure that two different integrands (e.g., a boundary and a volume integrand) + # that use the same symbol name do not clash. + # + # However, if more than one kernel is added for the same integrand by the HOG (e.g. for boundary + # integrals, a separate kernel per side of the simplex is added) this name will (and should) clash + # to make sure all kernels use the same symbols. + # + free_symbol_vars_set = set() + for integration_infos in self.integration_infos.values(): + for integration_info in integration_infos: + for fs in integration_info.free_symbols: + free_symbol_vars_set.add( + f"{str(fs)}_{integration_info.integrand_name}" + ) + + free_symbol_vars = [ + CppVariable( + name=fs, + type=str(self._type_descriptor.pystencils_type), + ) + for fs in free_symbol_vars_set + ] + + free_symbol_vars = sorted(free_symbol_vars, key=lambda x: x.name) + + free_symbol_vars_members = [ + CppVariable(name=fsv.name + "_", type=fsv.type) for fsv in free_symbol_vars + ] + + # Let's now check whether we need ctor arguments and member variables for boundary integrals. + boundary_condition_vars = [] + for integration_infos in self.integration_infos.values(): + if not all( + ii.integration_domain == MacroIntegrationDomain.VOLUME + for ii in integration_infos + ): + bc_var = CppVariable(name="boundaryCondition", type="BoundaryCondition") + if bc_var not in boundary_condition_vars: + boundary_condition_vars.append(bc_var) + + for ii in integration_infos: + if ii.integration_domain == MacroIntegrationDomain.DOMAIN_BOUNDARY: + bcuid_var = CppVariable( + name=ii.boundary_uid_name, type="BoundaryUID" + ) + if bcuid_var not in boundary_condition_vars: + boundary_condition_vars.append(bcuid_var) + + boundary_condition_vars_members = [ + CppVariable(name=bcv.name + "_", type=bcv.type) + for bcv in boundary_condition_vars + ] + # Finally we know what fields we need and can build the constructors, member variables, and includes. # Constructors ... @@ -396,9 +745,21 @@ class HyTeGElementwiseOperator: is_reference=True, ) for coeff in self.coefficients() - ], + ] + + free_symbol_vars + + boundary_condition_vars, initializer_list=["Operator( storage, minLevel, maxLevel )"] - + [f"{coeff.name}( _{coeff.name} )" for coeff in self.coefficients()], + + [f"{coeff.name}( _{coeff.name} )" for coeff in self.coefficients()] + + [ + f"{fsv[0].name}( {fsv[1].name} )" + for fsv in zip(free_symbol_vars_members, free_symbol_vars) + ] + + [ + f"{bcv[0].name}( {bcv[1].name} )" + for bcv in zip( + boundary_condition_vars_members, boundary_condition_vars + ) + ], ) ) @@ -414,7 +775,14 @@ class HyTeGElementwiseOperator: ) ) - os.makedirs(dir_path, exist_ok=True) # Create path if it doesn't exist + for fsv in free_symbol_vars_members: + operator_cpp_class.add(CppMemberVariable(fsv, visibility="private")) + + for bcv in boundary_condition_vars_members: + operator_cpp_class.add(CppMemberVariable(bcv, visibility="private")) + + # Create path if it doesn't exist + os.makedirs(dir_path, exist_ok=True) output_path_header = os.path.join(dir_path, f"{self.name}.hpp") output_path_impl = os.path.join(dir_path, f"{self.name}.cpp") @@ -423,10 +791,25 @@ class HyTeGElementwiseOperator: func_space_includes = set().union( *[coeff.includes() for coeff in self.coefficients()] ) - kernel_includes = set().union(*[kt.includes() for kt in self.kernel_types]) + kernel_includes = set().union( + *[kt.includes() for kt in self.kernel_wrapper_types] + ) blending_includes = set() - for dim, integration_info in self.element_matrices.items(): - for inc in integration_info.blending.coupling_includes(): + for dim, integration_infos in self.integration_infos.items(): + + if not all( + [ + integration_infos[0].blending.coupling_includes() + == io.blending.coupling_includes() + for io in integration_infos + ] + ): + raise HOGException( + "Seems that there are different blending functions in one bilinear form (likely in two different " + "integrals). This is not supported yet. :(" + ) + + for inc in integration_infos[0].blending.coupling_includes(): blending_includes.add(inc) all_includes = ( self.INCLUDES | func_space_includes | kernel_includes | blending_includes @@ -556,7 +939,10 @@ class HyTeGElementwiseOperator: # This list is only filled if we want to vectorize. loop_counter_custom_code_nodes = [] - if not (self._optimizer[Opts.VECTORIZE] or self._optimizer[Opts.VECTORIZE512]): + if ( + Opts.VECTORIZE not in integration_info.optimizations + and Opts.VECTORIZE512 not in integration_info.optimizations + ): # The Jacobians are loop-counter dependent, and we do not care about vectorization. # So we just use the indices. pystencils will handle casting them to float. el_matrix_element_index = element_index.copy() @@ -620,7 +1006,9 @@ class HyTeGElementwiseOperator: ] # Let's fill the array. - float_ctr_array_size = 8 if self._optimizer[Opts.VECTORIZE512] else 4 + float_ctr_array_size = ( + 8 if Opts.VECTORIZE512 in integration_info.optimizations else 4 + ) custom_code = "" custom_code += f"const int64_t phantom_ctr_0 = ctr_0;\n" @@ -665,8 +1053,9 @@ class HyTeGElementwiseOperator: self, dim: int, integration_info: IntegrationInfo, - loop_strategy: LoopStrategy, kernel_type: KernelType, + src_fields: List[FunctionSpaceImpl], + dst_fields: List[FunctionSpaceImpl], ) -> Tuple[ps.astnodes.Block, str]: """ This method generates an AST that represents the passed kernel type. @@ -675,7 +1064,6 @@ class HyTeGElementwiseOperator: :param integration_info: IntegrationInfo object holding the symbolic element matrix, quadrature rule, element geometry, etc. - :param loop_strategy: defines the iteration pattern :param kernel_type: specifies the kernel to execute - this could be e.g., a matrix-vector multiplication :returns: tuple (pre_loop_stmts, loop, operations_table) @@ -687,6 +1075,9 @@ class HyTeGElementwiseOperator: rows, cols = mat.shape + optimizer = Optimizer(integration_info.optimizations) + optimizer.check_opts_validity() + kernel_config = CreateKernelConfig( default_number_float=self._type_descriptor.pystencils_type, data_type=self._type_descriptor.pystencils_type, @@ -708,14 +1099,14 @@ class HyTeGElementwiseOperator: self.symbolizer.dof_symbols_as_vector( src_field.fe_space.num_dofs(geometry), src_field.name ) - for src_field in kernel_type.src_fields + for src_field in src_fields ] dst_vecs_symbols = [ self.symbolizer.dof_symbols_as_vector( dst_field.fe_space.num_dofs(geometry), dst_field.name ) - for dst_field in kernel_type.dst_fields + for dst_field in dst_fields ] # Do the kernel operation. @@ -725,7 +1116,7 @@ class HyTeGElementwiseOperator: # Common subexpression elimination. with TimedLogger("cse on kernel operation", logging.DEBUG): - cse_impl = self._optimizer.cse_impl() + cse_impl = optimizer.cse_impl() kernel_op_assignments = hog.cse.cse( kernel_op_assignments, cse_impl, @@ -741,7 +1132,7 @@ class HyTeGElementwiseOperator: with TimedLogger("constructing quadrature loops"): quad_loop = integration_info.quad_loop.construct_quad_loop( - accessed_mat_entries, self._optimizer.cse_impl() + accessed_mat_entries, optimizer.cse_impl() ) else: quad_loop = [] @@ -785,14 +1176,59 @@ class HyTeGElementwiseOperator: element_index = [ LoopOverCoordinate.get_loop_counter_symbol(i) for i in range(dim) ] - loop = loop_strategy.create_loop( + loop = integration_info.loop_strategy.create_loop( dim, element_index, indexing_info.micro_edges_per_macro_edge ) # element coordinates, jacobi matrix, tabulations, etc. preloop_stmts = {} - for element_type in all_element_types(geometry.dimensions): + # Deciding on which element types we want to iterate over. + # We skip certain element types for macro-volume boundary integrals. + element_types: List[Union[FaceType, CellType]] = all_element_types( + geometry.dimensions + ) + if isinstance(integration_info.loop_strategy, BOUNDARY): + element_types = list(integration_info.loop_strategy.element_loops.keys()) + + for element_type in element_types: + + # Re-ordering micro-element vertices for the handling of domain boundary integrals. + # + # Boundary integrals are handled by looping over all (volume-)elements that have a facet at one of the + # macro-element boundaries. There are three such boundaries in 2D and four in 3D. For each case, a separate + # kernel has to be generated. The logic that decides which of these kernels are called on which + # macro-volumes is handled by the HyTeG operator that is generated around these kernels. + # + # Instead of integrating over the micro-volume, we need to integrate over one of the micro-element facets. + # For that, the micro-element vertices are re-ordered such that the transformation from the affine element + # to the reference element results in the integration (boundary-)domain being mapped to the reference + # domain. + # + # E.g., in 2D, if the integration over the xy-boundary of the macro-volume (== macro-face) shall be + # generated this is signalled here by loop_strategy.facet_id == 2. + # In 2D all boundaries are only touched by the GRAY micro-elements (this is a little more complicated in 3D + # where at each macro-volume boundary, two types of elements overlap). + # Thus, we + # a) Shuffle the affine element vertices such that the xy-boundary of the GRAY elements is mapped to the + # (0, 1) line, which is the reference line for integration. + # b) Only iterate over the GRAY elements (handled later below). + + # Default order. + element_vertex_order = list(range(geometry.num_vertices)) + + # Shuffling vertices if a boundary integral is requested. + if ( + integration_info.integration_domain + == MacroIntegrationDomain.DOMAIN_BOUNDARY + and isinstance(integration_info.loop_strategy, BOUNDARY) + ): + element_vertex_order = micro_vertex_permutation_for_facet( + volume_geometry=geometry, + element_type=element_type, + facet_id=integration_info.loop_strategy.facet_id, + ) + # Create array accesses to the source and destination vector(s) for # the kernel. src_vecs_accesses = [ @@ -801,8 +1237,9 @@ class HyTeGElementwiseOperator: element_index, # type: ignore[arg-type] # list of sympy expressions also works element_type, indexing_info, + element_vertex_order, ) - for src_field in kernel_type.src_fields + for src_field in src_fields ] dst_vecs_accesses = [ dst_field.local_dofs( @@ -810,8 +1247,9 @@ class HyTeGElementwiseOperator: element_index, # type: ignore[arg-type] # list of sympy expressions also works element_type, indexing_info, + element_vertex_order, ) - for dst_field in kernel_type.dst_fields + for dst_field in dst_fields ] # Load source DoFs. @@ -827,6 +1265,7 @@ class HyTeGElementwiseOperator: element_type, src_vecs_accesses, dst_vecs_accesses, + element_vertex_order, ) # Load DoFs of coefficients. Those appear whenever a form is @@ -837,6 +1276,7 @@ class HyTeGElementwiseOperator: for ass in kernel_op_assignments + quad_loop for a in ass.atoms(DoFSymbol) } + dof_symbols = sorted(dof_symbols_set, key=lambda ds: ds.name) coeffs = dict( ( @@ -856,7 +1296,11 @@ class HyTeGElementwiseOperator: SympyAssignment( sp.Symbol(dof_symbol.name), coeffs[dof_symbol.function_id].local_dofs( - geometry, element_index, element_type, indexing_info + geometry, + element_index, + element_type, + indexing_info, + element_vertex_order, )[dof_symbol.dof_id], ) ) @@ -884,6 +1328,13 @@ class HyTeGElementwiseOperator: self.symbolizer, ) + # Re-ordering the element vertex coordinates + # (for all computations but affine transformation Jacobians - those are re-ordered later). + # See comment on boundary integrals above. + el_vertex_coordinates = [ + el_vertex_coordinates[i] for i in element_vertex_order + ] + coords_assignments = [ SympyAssignment( element_vertex_coordinates_symbols[vertex][component], @@ -893,7 +1344,7 @@ class HyTeGElementwiseOperator: for component in range(geometry.dimensions) ] - if integration_info.blending.is_affine() or self._optimizer[Opts.QUADLOOPS]: + if integration_info.blending.is_affine() or optimizer[Opts.QUADLOOPS]: blending_assignments = [] else: blending_assignments = ( @@ -908,20 +1359,18 @@ class HyTeGElementwiseOperator: ) ) - blending_assignments += ( - hog.code_generation.hessian_matrix_assignments( - mat, - integration_info.tables + quad_loop, - geometry, - self.symbolizer, - affine_points=element_vertex_coordinates_symbols, - blending=integration_info.blending, - quad_info=integration_info.quad, - ) + blending_assignments += hog.code_generation.hessian_matrix_assignments( + mat, + integration_info.tables + quad_loop, + geometry, + self.symbolizer, + affine_points=element_vertex_coordinates_symbols, + blending=integration_info.blending, + quad_info=integration_info.quad, ) with TimedLogger("cse on blending operation", logging.DEBUG): - cse_impl = self._optimizer.cse_impl() + cse_impl = optimizer.cse_impl() blending_assignments = hog.cse.cse( blending_assignments, cse_impl, @@ -939,7 +1388,25 @@ class HyTeGElementwiseOperator: + kernel_op_post_assignments ) - if not self._optimizer[Opts.QUADLOOPS]: + if ( + integration_info.integration_domain + == MacroIntegrationDomain.DOMAIN_BOUNDARY + and isinstance(integration_info.loop_strategy, BOUNDARY) + ): + with TimedLogger( + "boundary integrals: setting unused reference coordinate to 0", + logging.DEBUG, + ): + for node in body: + node.subs( + { + self.symbolizer.ref_coords_as_list(geometry.dimensions)[ + -1 + ]: 0 + } + ) + + if not optimizer[Opts.QUADLOOPS]: # Only now we replace the quadrature points and weights - if there are any. # We also setup sympy assignments in body with TimedLogger( @@ -972,7 +1439,7 @@ class HyTeGElementwiseOperator: body = add_types(body, kernel_config) # add the created loop body of statements to the loop according to the loop strategy - loop_strategy.add_body_to_loop(loop, body, element_type) + integration_info.loop_strategy.add_body_to_loop(loop, body, element_type) # This actually replaces the abstract field access instances with # array accesses that contain the index computations. @@ -993,6 +1460,12 @@ class HyTeGElementwiseOperator: self.symbolizer, ) + # Re-ordering the element vertex coordinates for the Jacobians. + # See comment on boundary integrals above. + el_vertex_coordinates = [ + el_vertex_coordinates[i] for i in element_vertex_order + ] + coords_assignments = [ SympyAssignment( coord_symbols_for_jac_affine[vertex][component], @@ -1005,7 +1478,7 @@ class HyTeGElementwiseOperator: elem_dependent_stmts = ( hog.cse.cse( coords_assignments + jacobi_assignments, - self._optimizer.cse_impl(), + optimizer.cse_impl(), "tmp_coords_jac", return_type=SympyAssignment, ) @@ -1020,7 +1493,9 @@ class HyTeGElementwiseOperator: "renaming loop bodies and preloop stmts for element types", logging.DEBUG ): for element_type, preloop in preloop_stmts.items(): - loop = loop_strategy.add_preloop_for_loop(loop, preloop, element_type) + loop = integration_info.loop_strategy.add_preloop_for_loop( + loop, preloop, element_type + ) # Add quadrature points and weights array declarations, but only those # which are actually needed. @@ -1033,7 +1508,7 @@ class HyTeGElementwiseOperator: return (Block(body), ops.to_table()) - def generate_kernels(self, loop_strategy: LoopStrategy) -> None: + def generate_kernels(self) -> None: """ TODO: Split this up in a meaningful way. Currently does a lot of stuff and modifies the kernel_types. @@ -1051,58 +1526,83 @@ class HyTeGElementwiseOperator: This information is gathered here, and written to the kernel type object, """ - for kernel_type in self.kernel_types: - for dim, integration_info in self.element_matrices.items(): - geometry = integration_info.geometry + for kernel_wrapper_type in self.kernel_wrapper_types: + for dim, integration_infos in self.integration_infos.items(): + + kernel_functions = [] + kernel_op_counts = [] + platform_dep_kernels = [] + macro_type = {2: "face", 3: "cell"} - # generate AST of kernel loop - with TimedLogger( - f"Generating kernel: {kernel_type.name} in {dim}D", logging.INFO - ): - ( - function_body, - kernel_op_count, - ) = self._generate_kernel( - dim, integration_info, loop_strategy, kernel_type + geometry = integration_infos[0].geometry + if not all([geometry == io.geometry for io in integration_infos]): + raise HOGException( + "All element geometries should be the same. Regardless of whether you integrate over their " + "boundary or volume. Dev note: this information seems to be redundant and we should only have " + "it in one place." ) - kernel_function = KernelFunction( - function_body, - Target.CPU, - Backend.C, - make_python_function, - ghost_layers=None, - function_name=f"{kernel_type.name}_macro_{geometry.dimensions}D", - assignments=None, - ) + for integration_info in integration_infos: + + # generate AST of kernel loop + with TimedLogger( + f"Generating kernel {integration_info.name} ({kernel_wrapper_type.name}, {dim}D)", + logging.INFO, + ): + + ( + function_body, + kernel_op_count, + ) = self._generate_kernel( + dim, + integration_info, + kernel_wrapper_type.kernel_type, + kernel_wrapper_type.src_fields, + kernel_wrapper_type.dst_fields, + ) - # optimizer applies optimizations - with TimedLogger( - f"Optimizing kernel: {kernel_type.name} in {dim}D", logging.INFO - ): - optimizer = ( - self._optimizer - if not isinstance(kernel_type, Assemble) - else self._optimizer_no_vec - ) - platform_dep_kernels = optimizer.apply_to_kernel( - kernel_function, dim, loop_strategy - ) + kernel_function = KernelFunction( + function_body, + Target.CPU, + Backend.C, + make_python_function, + ghost_layers=None, + function_name=f"{kernel_wrapper_type.name}_{integration_info.name}_macro_{geometry.dimensions}D", + assignments=None, + ) + + kernel_functions.append(kernel_function) + kernel_op_counts.append(kernel_op_count) - kernel_parameters = kernel_function.get_parameters() + # optimizer applies optimizations + with TimedLogger( + f"Optimizing kernel: {kernel_function.function_name} in {dim}D", + logging.INFO, + ): + optimizer = Optimizer(integration_info.optimizations) + optimizer.check_opts_validity() - # setup kernel string and op count table + if isinstance(kernel_wrapper_type.kernel_type, Assemble): + optimizer = optimizer.copy_without_vectorization() + + platform_dep_kernels.append( + optimizer.apply_to_kernel( + kernel_function, dim, integration_info.loop_strategy + ) + ) + + # Setup kernel wrapper string and op count table # - # in the following, we insert the sub strings of the final kernel string: + # In the following, we insert the sub strings of the final kernel string: # coefficients (retrieving pointers), setup of scalar parameters, kernel function call # This is done as follows: # - the kernel type has the skeleton string in which the sub string must be substituted - # - the function space impl knows from which function type a src/dst/coefficient field is and can return the - # corresponding sub string + # - the function space impl knows from which function type a src/dst/coefficient field is and can return + # the corresponding sub string # Retrieve coefficient pointers - kernel_type.substitute( + kernel_wrapper_type.substitute( { f"pointer_retrieval_{dim}D": "\n".join( coeff.pointer_retrieval(dim) @@ -1126,32 +1626,107 @@ class HyTeGElementwiseOperator: ] ) - scalar_parameter_setup += ( - integration_info.blending.parameter_coupling_code() - ) + blending_parameter_coupling_code = integration_infos[ + 0 + ].blending.parameter_coupling_code() + if not all( + [ + blending_parameter_coupling_code + == io.blending.parameter_coupling_code() + for io in integration_infos + ] + ): + raise HOGException( + "It seems you specified different blending maps for one bilinear form. " + "This may be desired, but it is certainly not supported :)." + ) + + scalar_parameter_setup += blending_parameter_coupling_code - kernel_type.substitute( + kernel_wrapper_type.substitute( {f"scalar_parameter_setup_{dim}D": scalar_parameter_setup} ) - # Kernel function call. - kernel_function_call_parameter_string = ",\n".join( - [str(prm) for prm in kernel_parameters] - ) - kernel_type.substitute( + # Kernel function call(s). + kernel_function_call_strings = [] + for kernel_function, integration_info in zip( + kernel_functions, integration_infos + ): + + pre_call_code = "" + post_call_code = "" + + if ( + integration_info.integration_domain + == MacroIntegrationDomain.DOMAIN_BOUNDARY + ): + + if not isinstance(integration_info.loop_strategy, BOUNDARY): + raise HOGException( + "The loop strategy should be BOUNDARY for boundary integrals." + ) + + facet_type = "Edge" if dim == 2 else "Face" + + neighbor_facet = ( + f"getStorage()->get{facet_type}( " + f"{macro_type[dim]}.getLowerDimNeighbors()" + f"[{integration_info.loop_strategy.facet_id}] )" + ) + + pre_call_code = ( + f"if ( boundaryCondition_.getBoundaryUIDFromMeshFlag( " + f"{neighbor_facet}->getMeshBoundaryFlag() ) == {integration_info.boundary_uid_name}_ ) {{" + ) + post_call_code = "}" + + kernel_parameters = kernel_function.get_parameters() + + # We append the name of the integrand (!= name of the kernel) to the free symbols we found in the + # integrand to make sure that two different integrands (e.g., a boundary and a volume integrand) + # that use the same symbol name do not clash. + # + # However, if more than one kernel is added for the same integrand by the HOG (e.g. for boundary + # integrals, a separate kernel per side of the simplex is added) this name will (and should) clash + # to make sure all kernels use the same symbols. + + kernel_parameters_updated = [] + for prm in kernel_parameters: + if str(prm) in [ + str(fs) for fs in integration_info.free_symbols + ]: + kernel_parameters_updated.append( + f"{str(prm)}_{integration_info.integrand_name}_" + ) + else: + kernel_parameters_updated.append(str(prm)) + + kernel_function_call_parameter_string = ",\n".join( + kernel_parameters_updated + ) + + kernel_function_call_strings.append( + f""" + {pre_call_code}\n + {kernel_function.function_name}(\n + {kernel_function_call_parameter_string});\n + {post_call_code}\n""" + ) + + kernel_wrapper_type.substitute( { - f"kernel_function_call_{dim}D": f""" - {kernel_function.function_name}(\n - {kernel_function_call_parameter_string});""" + f"kernel_function_call_{dim}D": "\n".join( + kernel_function_call_strings + ) } ) # Collect all information - kernel = Kernel( - kernel_type, - kernel_function, + kernel = OperatorMethod( + kernel_wrapper_type, + kernel_functions, platform_dep_kernels, - kernel_op_count, - integration_info, + kernel_op_counts, + integration_infos, ) - self.kernels.append(kernel) + self.operator_methods.append(kernel) diff --git a/hog/operator_generation/optimizer.py b/hog/operator_generation/optimizer.py index a64fc0ae499c63f805a051e30e5ab599c444960d..e9922a24ea34fd3a628c5e37dd6f9a81c8345a93 100644 --- a/hog/operator_generation/optimizer.py +++ b/hog/operator_generation/optimizer.py @@ -18,7 +18,7 @@ from copy import deepcopy import enum import logging import sympy as sp -from typing import Dict, Iterable, List, Set +from typing import Dict, Iterable, List, Set, Any from pystencils import TypedSymbol from pystencils.astnodes import ( @@ -108,8 +108,13 @@ class Optimizer: def __getitem__(self, opt): return opt in self._opts - def check_opts_validity(self, loop_strategy: LoopStrategy) -> None: - """Checks if the desired optimizations are valid for the given loop strategy.""" + def copy_without_vectorization( + self, + ) -> Any: # Optimizer and Self are not accepted :/ + return Optimizer(self._opts - {Opts.VECTORIZE, Opts.VECTORIZE512}) + + def check_opts_validity(self) -> None: + """Checks if the desired optimizations are valid.""" if Opts.VECTORIZE512 in self._opts and not Opts.VECTORIZE in self._opts: raise HOGException("Optimization VECTORIZE512 requires VECTORIZE.") @@ -191,6 +196,7 @@ class Optimizer: with TimedLogger("simplifying conditionals", logging.DEBUG): simplify_conditionals(loop, loop_counter_simplification=True) + if self[Opts.MOVECONSTANTS]: with TimedLogger("moving constants out of loop", logging.DEBUG): # This has to be done twice because sometimes constants are not moved completely to the surrounding block but diff --git a/hog/operator_generation/pystencils_extensions.py b/hog/operator_generation/pystencils_extensions.py index 9aa6dbe4a1d71fb89bd9a1d692cf49c39a90a8b2..8ee01d446f1d28cd2e8460d914d62bdb1f8b4512 100644 --- a/hog/operator_generation/pystencils_extensions.py +++ b/hog/operator_generation/pystencils_extensions.py @@ -18,7 +18,7 @@ from typing import Dict, List, Tuple, Union import sympy as sp from pystencils import FieldType, Field -from pystencils.astnodes import Block, LoopOverCoordinate, Node +from pystencils.astnodes import Block, LoopOverCoordinate, Node, get_next_parent_of_type import pystencils as ps from pystencils.astnodes import ( Block, @@ -88,6 +88,60 @@ def loop_over_simplex( return loops[dim - 1] +def loop_over_simplex_facet(dim: int, width: int, facet_id: int) -> LoopOverCoordinate: + """ + Loops over one boundary facet of a simplex (e.g., one edge in 2D, one face in 3D). + + The facet is specified by an integer. It is required that + + 0 ≤ facet_id ≤ dim + + Let [x_0, x_1, ..., x_(dim-1)] be the coordinate of one element that is looped over. + + For facet_id < dim, we have that + + x_(dim - 1 - facet_id) = 0 + + and the remaining boundary is selected with facet_id == dim. + + So in 2D for example, we get + + facet_id = 0: (x_0, 0 ) + facet_id = 1: (0, x_1) + facet_id = 2: the xy- (or "diagonal") boundary + """ + loop = loop_over_simplex(dim, width) + innermost_loops = get_innermost_loop(loop) + if len(innermost_loops) != 1: + raise HOGException("There should be only one innermost loop.") + innermost_loop: LoopOverCoordinate = innermost_loops[0] + + if facet_id not in range(0, dim + 1): + raise HOGException(f"Bad facet_id ({facet_id}) for dim {dim}.") + + if facet_id == dim: + # For the "diagonal" loop we can just iterate as usual but skip all but the last element of the innermost loop. + # I hope. + innermost_loop.start = innermost_loop.stop - 1 + return loop + + # For the other facet_ids we need to find the corresponding loop and set that counter to 0. + # I am doing this here by just traversing the loops from the inside out, assuming that no loop cutting etc. + # occurred. + loop_with_counter_to_be_set_to_zero = innermost_loop + for d in range(dim - 1 - facet_id): + loop_with_counter_to_be_set_to_zero = get_next_parent_of_type( + loop_with_counter_to_be_set_to_zero, LoopOverCoordinate + ) + if loop_with_counter_to_be_set_to_zero is None: + raise HOGException("There was no parent loop. This should not happen. I think.") + + loop_with_counter_to_be_set_to_zero.start = 0 + loop_with_counter_to_be_set_to_zero.stop = 1 + + return loop + + def create_micro_element_loops( dim: int, micro_edges_per_macro_edge: int ) -> Dict[Union[FaceType, CellType], LoopOverCoordinate]: diff --git a/hog/quadrature/quad_loop.py b/hog/quadrature/quad_loop.py index 5c8444997041774c9b7971e9ad2f5d5cd5118af4..2cf1821e45e56c7533162b515832f2144ca4e546 100644 --- a/hog/quadrature/quad_loop.py +++ b/hog/quadrature/quad_loop.py @@ -37,7 +37,7 @@ from hog.fem_helpers import ( jac_blending_inv_eval_symbols, ) from hog.element_geometry import ElementGeometry -from hog.blending import GeometryMap, IdentityMap +from hog.blending import GeometryMap, IdentityMap, ParametricMap class QuadLoop: @@ -104,7 +104,9 @@ class QuadLoop: for dim, symbol in enumerate(ref_symbols) } - if not self.blending.is_affine(): + if not self.blending.is_affine() and not isinstance( + self.blending, ParametricMap + ): jac = jac_blending_evaluate( self.symbolizer, self.quadrature.geometry, self.blending ) @@ -117,7 +119,9 @@ class QuadLoop: hess = hess_blending_evaluate( self.symbolizer, self.quadrature.geometry, self.blending ) - hess_evaluated = [fast_subs(hess[idx], coord_subs_dict) for idx in range(len(hess))] + hess_evaluated = [ + fast_subs(hess[idx], coord_subs_dict) for idx in range(len(hess)) + ] quadrature_assignments += self.blending_hessian_quad_loop_assignments( self.quadrature.geometry, self.symbolizer, hess_evaluated ) @@ -229,7 +233,7 @@ class QuadLoop: ] return quadrature_assignments - + def blending_hessian_quad_loop_assignments( self, geometry: ElementGeometry, @@ -242,7 +246,11 @@ class QuadLoop: dim = geometry.dimensions quadrature_assignments += [ - ast.SympyAssignment(hess_symbols[k][i, j], hessian_blending_evaluated[k][i, j], is_const=False) + ast.SympyAssignment( + hess_symbols[k][i, j], + hessian_blending_evaluated[k][i, j], + is_const=False, + ) for i in range(dim) for j in range(dim) for k in range(dim) diff --git a/hog/quadrature/quadrature.py b/hog/quadrature/quadrature.py index b51dd0bfce6014ac3e10c0ce05c89694295d5c20..dc0d39b4e7d49fe816ae05f331bac1a8554915fb 100644 --- a/hog/quadrature/quadrature.py +++ b/hog/quadrature/quadrature.py @@ -26,7 +26,6 @@ import sympy as sp from hog.element_geometry import ( ElementGeometry, TriangleElement, - EmbeddedTriangle, TetrahedronElement, LineElement, ) @@ -51,7 +50,6 @@ def select_quadrule( """Checks for availability of a specified quadrature rule and chooses a rule with minimal points if only a degree is given.""" - # TODO for now, leave out line elements as we have no use for them without DG logger = get_logger() # quadrule given by name, just check if it exists and return it @@ -96,24 +94,43 @@ def select_quadrule( warnings.simplefilter("ignore") all_schemes = [] - if isinstance(geometry, TriangleElement) or isinstance( - geometry, EmbeddedTriangle - ): + if isinstance(geometry, LineElement): + # Since line integrals can be approximated with arbitrary order, registering schemes is not + # meaningful. I doubt that we will need quadrature with order > 20, so we simply limit it here. + # If we require higher orders, simply bump this number. + line_quad_degree_limit = 20 + # Also, for now let's restrict ourselves to Gauss-Legendre polynomials. + schemes = { + f"gauss_legendre_{d}": quadpy.c1.gauss_legendre(d) + for d in range(1, line_quad_degree_limit + 1) + } + elif isinstance(geometry, TriangleElement): schemes = quadpy.t2.schemes elif isinstance(geometry, TetrahedronElement): schemes = quadpy.t3.schemes for key, s in schemes.items(): try: - scheme = s() + if isinstance(geometry, LineElement): + # For some reason the quadpy implementation is a little different for line segments. + scheme = s + else: + scheme = s() + except TypeError as e: pass all_schemes.append(scheme) def select_degree(x: TnScheme) -> int: - if x.degree >= degree: - return x.points.shape[1] + if isinstance(geometry, LineElement): + if x.degree >= degree: + return x.degree + else: + return 10**10000 # just a large number else: - return 10**10000 # just a large number + if x.degree >= degree: + return x.points.shape[1] + else: + return 10**10000 # just a large number return min(all_schemes, key=lambda x: select_degree(x)) @@ -121,9 +138,11 @@ def select_quadrule( else: raise HOGException(f"Unexpected {scheme_info}") - logger.info( - f"Integrating over {geometry} with rule: {scheme.name} (degree: {scheme.degree}, #points: {scheme.points.shape[1]})." - ) + if isinstance(geometry, LineElement): + num_points = len(scheme.points) + else: + num_points = scheme.points.shape[1] + return scheme @@ -226,8 +245,6 @@ class Quadrature: f"Cannot apply quadrature rule to matrix of shape {f.shape}: {f}." ) ref_symbols = symbolizer.ref_coords_as_list(self._geometry.dimensions) - if isinstance(self._geometry, EmbeddedTriangle): - ref_symbols = symbolizer.ref_coords_as_list(self._geometry.dimensions - 1) if self._scheme_name == "exact": mat_entry = integrate_exact_over_reference(f, self._geometry, symbolizer) @@ -245,10 +262,12 @@ class Quadrature: for i, (point, weight) in enumerate(zip(inline_points, inline_weights)): spat_coord_subs = {} for idx, symbol in enumerate(ref_symbols): + if not isinstance(point, sp.Matrix): + point = sp.Matrix([point]) spat_coord_subs[symbol] = point[idx] if not blending.is_affine(): for symbol in symbolizer.quadpoint_dependent_free_symbols( - self._geometry.dimensions + self._geometry.space_dimension ): spat_coord_subs[symbol] = sp.Symbol(symbol.name + f"_q_{i}") f_sub = fast_subs(f, spat_coord_subs) @@ -291,9 +310,6 @@ class Quadrature: elif isinstance(geometry, LineElement): vertices = np.asarray([[0.0], [1.0]]) degree = scheme.degree - elif isinstance(geometry, EmbeddedTriangle): - vertices = np.asarray([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]) - degree = scheme.degree else: raise HOGException("Invalid geometry for quadrature.") diff --git a/hog/quadrature/tabulation.py b/hog/quadrature/tabulation.py index 23fbf2ac1bfa0be157d11324077c1298049db4b9..cae9b41b1ce76abede9fe73c9aa8e0cd1a1e7ed9 100644 --- a/hog/quadrature/tabulation.py +++ b/hog/quadrature/tabulation.py @@ -15,7 +15,7 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. import sympy as sp -from typing import Dict, List, Iterable, Tuple +from typing import Dict, List, Tuple, Union import pystencils.astnodes as ast from pystencils.typing import BasicType @@ -55,12 +55,20 @@ class Tabulation: self.symbolizer = symbolizer self.tables: Dict[str, Table] = {} - def register_factor(self, factor_name: str, factor: sp.Matrix) -> sp.Matrix: + def register_factor( + self, factor_name: str, factor: sp.Matrix | int | float + ) -> sp.Matrix: """Register a factor of the weak form that can be tabulated. Returns symbols replacing the expression for the factor. The symbols are returned in the same form as the factor was given. E.g. in case of a blended full Stokes operator we might encounter J_F^-1 grad phi being a matrix.""" + if not isinstance(factor, sp.MatrixBase): + factor = sp.Matrix([factor]) + + if all(f.is_constant() for f in factor): + return factor + replacement_symbols = sp.zeros(factor.rows, factor.cols) for r in range(factor.rows): for c in range(factor.cols): @@ -138,7 +146,9 @@ class Tabulation: subs_dict |= {symbol: expr for expr, (_, symbol) in table.entries.items()} return fast_subs(mat, subs_dict) - def register_phi_evals(self, phis: List[sp.Expr]) -> List[sp.Expr]: + def register_phi_evals( + self, phis: Union[List[sp.Expr], List[sp.MatrixBase]] + ) -> List[sp.Expr]: """Convenience function to register factors for the evaluation of basis functions.""" phi_eval_symbols = [] for idx, phi in enumerate(phis): diff --git a/hog/recipes/__init__.py b/hog/recipes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..66608a37410f3c7a2e12498fa816fda9799ddac7 --- /dev/null +++ b/hog/recipes/__init__.py @@ -0,0 +1,15 @@ +# 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/>. \ No newline at end of file diff --git a/hog/recipes/common.py b/hog/recipes/common.py new file mode 100644 index 0000000000000000000000000000000000000000..6ebdc77bc5e8225e04e56070db173d6a4a0042e4 --- /dev/null +++ b/hog/recipes/common.py @@ -0,0 +1,22 @@ +# 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/>. + + +# Collects common imports for forming integrand and operator recipes. +import sympy as sp +from hog.math_helpers import dot, abs, det, inv, double_contraction + +__all__ = ["sp", "dot", "abs", "det", "inv", "double_contraction"] diff --git a/hog/recipes/integrands/__init__.py b/hog/recipes/integrands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4f026e513de97c126ce27a765a931d2f730e1c39 --- /dev/null +++ b/hog/recipes/integrands/__init__.py @@ -0,0 +1,15 @@ +# 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/>. diff --git a/hog/recipes/integrands/boundary/__init__.py b/hog/recipes/integrands/boundary/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4f026e513de97c126ce27a765a931d2f730e1c39 --- /dev/null +++ b/hog/recipes/integrands/boundary/__init__.py @@ -0,0 +1,15 @@ +# 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/>. diff --git a/hog/recipes/integrands/boundary/freeslip_nitsche_divergence.py b/hog/recipes/integrands/boundary/freeslip_nitsche_divergence.py new file mode 100644 index 0000000000000000000000000000000000000000..d85a4c96ad95c2c4abadc8f02f561f623123265f --- /dev/null +++ b/hog/recipes/integrands/boundary/freeslip_nitsche_divergence.py @@ -0,0 +1,31 @@ +# 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 hog.recipes.common import * + + +def integrand(*, u, v, x, jac_a_boundary, jac_b, matrix, **_): + + space_dim = len(x) + + A = matrix("A", space_dim, space_dim) + b = matrix("b", space_dim, 1) + + n = A * x + b + n = n / n.norm() + + ds = abs(det(jac_a_boundary.T * jac_b.T * jac_b * jac_a_boundary)) ** 0.5 + return -v * dot(n, u) * ds diff --git a/hog/recipes/integrands/boundary/freeslip_nitsche_gradient.py b/hog/recipes/integrands/boundary/freeslip_nitsche_gradient.py new file mode 100644 index 0000000000000000000000000000000000000000..569e2381a43721e11074ba4c32d1f444993c6ca4 --- /dev/null +++ b/hog/recipes/integrands/boundary/freeslip_nitsche_gradient.py @@ -0,0 +1,31 @@ +# 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 hog.recipes.common import * + + +def integrand(*, u, v, x, jac_a_boundary, jac_b, matrix, **_): + + space_dim = len(x) + + A = matrix("A", space_dim, space_dim) + b = matrix("b", space_dim, 1) + + n = A * x + b + n = n / n.norm() + + ds = abs(det(jac_a_boundary.T * jac_b.T * jac_b * jac_a_boundary)) ** 0.5 + return -dot(n, v) * u * ds diff --git a/hog/recipes/integrands/boundary/freeslip_nitsche_momentum.py b/hog/recipes/integrands/boundary/freeslip_nitsche_momentum.py new file mode 100644 index 0000000000000000000000000000000000000000..57eeb01cd488407262bccb9e7bed1a54825dbaf1 --- /dev/null +++ b/hog/recipes/integrands/boundary/freeslip_nitsche_momentum.py @@ -0,0 +1,59 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + u, + v, + grad_u, + grad_v, + jac_a_inv, + jac_a_boundary, + jac_b, + jac_b_inv, + x, + k, + scalars, + matrix, + **_, +): + space_dim = len(x) + + c_penalty = scalars("c_penalty") + A = matrix("A", space_dim, space_dim) + b = matrix("b", space_dim, 1) + + n = A * x + b + n = n / n.norm() + + mu = k["mu"] + + grad_u_chain = jac_b_inv.T * jac_a_inv.T * grad_u + grad_v_chain = jac_b_inv.T * jac_a_inv.T * grad_v + + sym_grad_u = mu * (grad_u_chain + grad_u_chain.T) + sym_grad_v = mu * (grad_v_chain + grad_v_chain.T) + + term_consistency = -dot(v, n) * dot(dot(n, sym_grad_u).T, n) + term_symmetry = -dot(dot(n, sym_grad_v).T, n) * dot(u, n) + term_penalty = c_penalty * mu * dot(v, n) * dot(u, n) + + ds = abs(det(jac_a_boundary.T * jac_b.T * jac_b * jac_a_boundary)) ** 0.5 + + return (term_consistency + term_symmetry + term_penalty) * ds diff --git a/hog/recipes/integrands/boundary/mass.py b/hog/recipes/integrands/boundary/mass.py new file mode 100644 index 0000000000000000000000000000000000000000..321c61caa5f8a87ca6a701ab5c4abb79cb17fb2e --- /dev/null +++ b/hog/recipes/integrands/boundary/mass.py @@ -0,0 +1,22 @@ +# 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 hog.recipes.common import * + + +def integrand(*, u, v, jac_a_boundary, jac_b, tabulate, **_): + G = abs(det(jac_a_boundary.T * jac_b.T * jac_b * jac_a_boundary)) + return tabulate(u * v) * G**0.5 diff --git a/hog/recipes/integrands/volume/__init__.py b/hog/recipes/integrands/volume/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4f026e513de97c126ce27a765a931d2f730e1c39 --- /dev/null +++ b/hog/recipes/integrands/volume/__init__.py @@ -0,0 +1,15 @@ +# 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/>. diff --git a/hog/recipes/integrands/volume/advection.py b/hog/recipes/integrands/volume/advection.py new file mode 100644 index 0000000000000000000000000000000000000000..5bdaeacf294a68dd6795471a625b78235811375e --- /dev/null +++ b/hog/recipes/integrands/volume/advection.py @@ -0,0 +1,51 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + v, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + k, + scalars, + volume_geometry, + tabulate, + **_, +): + if volume_geometry.dimensions > 2: + u = sp.Matrix([[k["ux"]], [k["uy"]], [k["uz"]]]) + else: + u = sp.Matrix([[k["ux"]], [k["uy"]]]) + + if "cp" in k.keys(): + coeff = k["cp"] + else: + coeff = scalars("cp") + + return ( + coeff + * dot(jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), u) + * v + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/diffusion.py b/hog/recipes/integrands/volume/diffusion.py new file mode 100644 index 0000000000000000000000000000000000000000..f5347ea93d7bb63d80ac48ab02cbd57631797cd4 --- /dev/null +++ b/hog/recipes/integrands/volume/diffusion.py @@ -0,0 +1,38 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + tabulate, + **_, +): + return ( + double_contraction( + jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), + jac_b_inv.T * tabulate(jac_a_inv.T * grad_v), + ) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/diffusion_affine.py b/hog/recipes/integrands/volume/diffusion_affine.py new file mode 100644 index 0000000000000000000000000000000000000000..0fe7c5c0738edfed9e177112fc7543de327a219b --- /dev/null +++ b/hog/recipes/integrands/volume/diffusion_affine.py @@ -0,0 +1,35 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + grad_u, + grad_v, + tabulate, + **_, +): + return tabulate( + double_contraction( + jac_a_inv.T * grad_u, + jac_a_inv.T * grad_v, + ) + * jac_a_abs_det + ) diff --git a/hog/recipes/integrands/volume/div_k_grad.py b/hog/recipes/integrands/volume/div_k_grad.py new file mode 100644 index 0000000000000000000000000000000000000000..f0ca46a64ad6d83a2c66fed6500b43ccf8fb8cfb --- /dev/null +++ b/hog/recipes/integrands/volume/div_k_grad.py @@ -0,0 +1,39 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, +): + return k["k"] * ( + double_contraction( + jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), + jac_b_inv.T * tabulate(jac_a_inv.T * grad_v), + ) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/div_k_grad_affine.py b/hog/recipes/integrands/volume/div_k_grad_affine.py new file mode 100644 index 0000000000000000000000000000000000000000..2a78278e5a1f19747b1b80ccf354417c52c204da --- /dev/null +++ b/hog/recipes/integrands/volume/div_k_grad_affine.py @@ -0,0 +1,36 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, +): + return k["k"] * tabulate( + double_contraction( + jac_a_inv.T * grad_u, + jac_a_inv.T * grad_v, + ) + * jac_a_abs_det + ) diff --git a/hog/recipes/integrands/volume/divdiv.py b/hog/recipes/integrands/volume/divdiv.py new file mode 100644 index 0000000000000000000000000000000000000000..80315d110b8eb18e74902c1f20009b27c6ca4f72 --- /dev/null +++ b/hog/recipes/integrands/volume/divdiv.py @@ -0,0 +1,33 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + tabulate, + **_, +): + div_u = (jac_b_inv.T * tabulate(jac_a_inv.T * grad_u)).trace() + div_v = (jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)).trace() + return div_u * div_v * tabulate(jac_a_abs_det) * jac_b_abs_det diff --git a/hog/recipes/integrands/volume/divergence.py b/hog/recipes/integrands/volume/divergence.py new file mode 100644 index 0000000000000000000000000000000000000000..75ae7517998e3c5e4a53920530817760ef70238c --- /dev/null +++ b/hog/recipes/integrands/volume/divergence.py @@ -0,0 +1,35 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + v, + tabulate, + **_, +): + return ( + -(jac_b_inv.T * tabulate(jac_a_inv.T * grad_u)).trace() + * tabulate(v * jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/epsilon.py b/hog/recipes/integrands/volume/epsilon.py new file mode 100644 index 0000000000000000000000000000000000000000..ab7888e56fa474658e72208089d050b8d4fe02b5 --- /dev/null +++ b/hog/recipes/integrands/volume/epsilon.py @@ -0,0 +1,46 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, +): + + grad_u_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_u) + grad_v_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_v) + + def symm_grad(w): + return 0.5 * (w + w.T) + + symm_grad_u = symm_grad(grad_u_chain) + symm_grad_v = symm_grad(grad_v_chain) + + return ( + double_contraction(2 * k["mu"] * symm_grad_u, symm_grad_v) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/epsilon_affine.py b/hog/recipes/integrands/volume/epsilon_affine.py new file mode 100644 index 0000000000000000000000000000000000000000..049f334a027fa363c38be1f18f59da494cb6c12c --- /dev/null +++ b/hog/recipes/integrands/volume/epsilon_affine.py @@ -0,0 +1,42 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, +): + + grad_u_chain = jac_a_inv.T * grad_u + grad_v_chain = jac_a_inv.T * grad_v + + def symm_grad(w): + return 0.5 * (w + w.T) + + symm_grad_u = symm_grad(grad_u_chain) + symm_grad_v = symm_grad(grad_v_chain) + + return k["mu"] * tabulate( + double_contraction(2 * symm_grad_u, symm_grad_v) * jac_a_abs_det + ) diff --git a/hog/recipes/integrands/volume/full_stokes.py b/hog/recipes/integrands/volume/full_stokes.py new file mode 100644 index 0000000000000000000000000000000000000000..d8e40a1d0e94c278278de8d0f340c28d0ca34c51 --- /dev/null +++ b/hog/recipes/integrands/volume/full_stokes.py @@ -0,0 +1,52 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + k, + tabulate, + **_, +): + + grad_u_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_u) + grad_v_chain = jac_b_inv.T * tabulate(jac_a_inv.T * grad_v) + + def symm_grad(w): + return 0.5 * (w + w.T) + + symm_grad_u = symm_grad(grad_u_chain) + symm_grad_v = symm_grad(grad_v_chain) + + div_u = (jac_b_inv.T * tabulate(jac_a_inv.T * grad_u)).trace() + div_v = (jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)).trace() + + return k["mu"] * ( + ( + double_contraction(2 * symm_grad_u, symm_grad_v) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) + - (2.0 / 3.0) * div_u * div_v * tabulate(jac_a_abs_det) * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/gradient.py b/hog/recipes/integrands/volume/gradient.py new file mode 100644 index 0000000000000000000000000000000000000000..982ba2329ecdc6b5aee3491e6e8a5c1703f19d5f --- /dev/null +++ b/hog/recipes/integrands/volume/gradient.py @@ -0,0 +1,35 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + u, + grad_v, + tabulate, + **_, +): + return ( + -(jac_b_inv.T * tabulate(jac_a_inv.T * grad_v)).trace() + * tabulate(u * jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/k_mass.py b/hog/recipes/integrands/volume/k_mass.py new file mode 100644 index 0000000000000000000000000000000000000000..bd2f997473aadeb9181c3d9c23205cc24f528daa --- /dev/null +++ b/hog/recipes/integrands/volume/k_mass.py @@ -0,0 +1,21 @@ +# 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 hog.recipes.common import * + + +def integrand(*, u, v, jac_a_abs_det, jac_b_abs_det, k, tabulate, **_): + return k["k"] * tabulate(u * v * jac_a_abs_det) * jac_b_abs_det diff --git a/hog/recipes/integrands/volume/mass.py b/hog/recipes/integrands/volume/mass.py new file mode 100644 index 0000000000000000000000000000000000000000..b3368d12d62c477a662e06afc93a2fcb74058ce5 --- /dev/null +++ b/hog/recipes/integrands/volume/mass.py @@ -0,0 +1,21 @@ +# 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 hog.recipes.common import * + + +def integrand(*, u, v, jac_a_abs_det, jac_b_abs_det, tabulate, **_): + return tabulate(u * v * jac_a_abs_det) * jac_b_abs_det diff --git a/hog/recipes/integrands/volume/pspg.py b/hog/recipes/integrands/volume/pspg.py new file mode 100644 index 0000000000000000000000000000000000000000..c73d12300e83e237d209db38790860058a28051d --- /dev/null +++ b/hog/recipes/integrands/volume/pspg.py @@ -0,0 +1,46 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + volume_geometry, + tabulate, + **_, +): + if volume_geometry.dimensions == 2: + volume = jac_b_abs_det * 0.5 * jac_a_abs_det + tau = -volume * 0.2 + else: + volume = jac_b_abs_det * jac_a_abs_det / 6.0 + tau = -pow(volume, 2.0 / 3.0) / 12.0 + + return tau * ( + double_contraction( + jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), + jac_b_inv.T * tabulate(jac_a_inv.T * grad_v), + ) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/supg_advection.py b/hog/recipes/integrands/volume/supg_advection.py new file mode 100644 index 0000000000000000000000000000000000000000..560bf45b97216dafd68e49679f6e9fdffd3955ee --- /dev/null +++ b/hog/recipes/integrands/volume/supg_advection.py @@ -0,0 +1,45 @@ +# 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 hog.recipes.common import * + + +def integrand( + *, + v, + jac_a_inv, + jac_a_abs_det, + jac_b_inv, + jac_b_abs_det, + grad_u, + grad_v, + k, + volume_geometry, + tabulate, + **_, +): + if volume_geometry.dimensions > 2: + u = sp.Matrix([[k["ux"]], [k["uy"]], [k["uz"]]]) + else: + u = sp.Matrix([[k["ux"]], [k["uy"]]]) + + return ( + k["cp_times_delta"] + * dot(jac_b_inv.T * tabulate(jac_a_inv.T * grad_u), u) + * dot(jac_b_inv.T * tabulate(jac_a_inv.T * grad_v), u) + * tabulate(jac_a_abs_det) + * jac_b_abs_det + ) diff --git a/hog/recipes/integrands/volume/zero.py b/hog/recipes/integrands/volume/zero.py new file mode 100644 index 0000000000000000000000000000000000000000..3b12a48e99260a659d0c18a164aac573f46bd0e7 --- /dev/null +++ b/hog/recipes/integrands/volume/zero.py @@ -0,0 +1,21 @@ +# 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 hog.recipes.common import * + + +def integrand(**_): + return 0 diff --git a/hog/symbolizer.py b/hog/symbolizer.py index 721e82000c6707a5fe5ff7069c891fad25b427b2..48d3ad607d1c2a0b92059d19561b97ce45e9c706 100644 --- a/hog/symbolizer.py +++ b/hog/symbolizer.py @@ -18,6 +18,7 @@ from typing import List import sympy as sp import string from hog.exception import HOGException +from hog.element_geometry import ElementGeometry class Symbolizer: @@ -58,6 +59,8 @@ class Symbolizer: :param symbol_element_matrix: lambda for the names of the element matrix entries :param tmp_prefix: string that is used as a prefix for temporary variables """ + self._running_integer = 0 + self._symbol_ref_coords = symbol_ref_coords self._symbol_affine_vertices = symbol_affine_vertices self._input_affine_vertices_name = input_affine_vertices_name @@ -82,6 +85,15 @@ class Symbolizer: self._symbol_abs_det_jac_blending = symbol_abs_det_jac_blending self._symbol_hessian_blending = symbol_hessian_blending + def get_next_running_integer(self): + """ + Simply returns a new integer for every call for each Symbolizer instance. + Just handy if you need unique integers for any reason. + """ + next_integer = self._running_integer + self._running_integer += 1 + return next_integer + def ref_coords_as_list(self, dimensions: int) -> List[sp.Symbol]: """Returns a list of symbols that correspond to the coordinates on the reference element.""" return sp.symbols([self._symbol_ref_coords(i) for i in range(dimensions)]) @@ -167,25 +179,27 @@ class Symbolizer: def tmp_prefix(self) -> str: return self._tmp_prefix - def jac_ref_to_affine(self, dimensions: int) -> sp.Matrix: + def jac_ref_to_affine(self, geometry: ElementGeometry) -> sp.Matrix: return sp.Matrix( [ [ sp.Symbol(f"{self._symbol_jac_affine}_{i}_{j}") - for j in range(dimensions) + for j in range(geometry.dimensions) ] - for i in range(dimensions) + for i in range(geometry.space_dimension) ] ) - def jac_ref_to_affine_inv(self, dimensions: int) -> sp.Matrix: + def jac_ref_to_affine_inv(self, geometry: ElementGeometry) -> sp.Matrix: + if geometry.dimensions != geometry.space_dimension: + raise HOGException("Cannot invert Jacobian for embedded elements.") return sp.Matrix( [ [ sp.Symbol(f"{self._symbol_jac_affine_inv}_{i}_{j}") - for j in range(dimensions) + for j in range(geometry.dimensions) ] - for i in range(dimensions) + for i in range(geometry.dimensions) ] ) @@ -216,8 +230,9 @@ class Symbolizer: def abs_det_jac_affine_to_blending(self, q_pt: str = "") -> sp.Symbol: return sp.Symbol(f"{self._symbol_abs_det_jac_blending}{q_pt}") - + def hessian_blending_map(self, dimensions: int, q_pt: str = "") -> List[sp.Matrix]: + """Returns the Hessian for each component f_1, f_2, ... of the blending map f = (f_1, f_2, ...).""" return [ sp.Matrix( [ @@ -227,7 +242,8 @@ class Symbolizer: ] for i in range(dimensions) ] - ) for k in range(dimensions) + ) + for k in range(dimensions) ] def blending_parameter_symbols(self, num_symbols: int) -> List[sp.Symbol]: diff --git a/hog/sympy_extensions.py b/hog/sympy_extensions.py index 3f92ee99ba1dcd4802d435817a208d850cd1043e..5d6467975da0349fafb80d8d84f46857f3e12924 100644 --- a/hog/sympy_extensions.py +++ b/hog/sympy_extensions.py @@ -22,7 +22,9 @@ T = TypeVar("T") def fast_subs( - expression: T, substitutions: Dict[sp.Expr, sp.Expr], skip: Optional[Callable[[sp.Expr], bool]] = None + expression: T, + substitutions: Dict[sp.Expr, sp.Expr], + skip: Optional[Callable[[sp.Expr], bool]] = None, ) -> T: """Similar to sympy subs function. diff --git a/hog_tests/operator_generation/test_boundary_loop.py b/hog_tests/operator_generation/test_boundary_loop.py new file mode 100644 index 0000000000000000000000000000000000000000..41e733f0fd66defc00ce5329ade21a36036be2a3 --- /dev/null +++ b/hog_tests/operator_generation/test_boundary_loop.py @@ -0,0 +1,108 @@ +# 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/>. +import logging + +from sympy.core.cache import clear_cache + +from hog.blending import AnnulusMap, GeometryMap, IcosahedralShellMap +from hog.element_geometry import ( + ElementGeometry, + LineElement, + TriangleElement, + TetrahedronElement, +) +from hog.function_space import LagrangianFunctionSpace, TrialSpace, TestSpace +from hog.operator_generation.operators import ( + HyTeGElementwiseOperator, +) +from hog.symbolizer import Symbolizer +from hog.quadrature import Quadrature, select_quadrule +from hog.operator_generation.kernel_types import ApplyWrapper, KernelWrapperType +from hog.operator_generation.types import hyteg_type +from hog.forms_boundary import mass_boundary +from hog.logger import TimedLogger + + +def test_boundary_loop(): + # TimedLogger.set_log_level(logging.DEBUG) + + dims = [2, 3] + + clear_cache() + + symbolizer = Symbolizer() + + name = f"P2MassBoundary" + + trial = TrialSpace(LagrangianFunctionSpace(2, symbolizer)) + test = TestSpace(LagrangianFunctionSpace(2, symbolizer)) + + type_descriptor = hyteg_type() + + kernel_types: list[KernelWrapperType] = [ + ApplyWrapper( + trial, + test, + type_descriptor=type_descriptor, + dims=dims, + ) + ] + + operator = HyTeGElementwiseOperator( + name, + symbolizer=symbolizer, + kernel_wrapper_types=kernel_types, + type_descriptor=type_descriptor, + ) + + for dim in dims: + if dim == 2: + volume_geometry: ElementGeometry = TriangleElement() + boundary_geometry: ElementGeometry = LineElement(space_dimension=2) + blending: GeometryMap = AnnulusMap() + + else: + volume_geometry = TetrahedronElement() + boundary_geometry = TriangleElement(space_dimension=3) + blending = IcosahedralShellMap() + + quad = Quadrature(select_quadrule(5, boundary_geometry), boundary_geometry) + + form = mass_boundary( + trial, + test, + volume_geometry, + boundary_geometry, + symbolizer, + blending=blending, + ) + + operator.add_boundary_integral( + name=f"boundary_mass", + volume_geometry=volume_geometry, + quad=quad, + blending=blending, + form=form, + ) + + operator.generate_class_code( + ".", + clang_format_binary="clang-format", + ) + + +if __name__ == "__main__": + test_boundary_loop() diff --git a/hog_tests/operator_generation/test_indexing.py b/hog_tests/operator_generation/test_indexing.py index 852463bbc6b120f8ff4cd080ed83afa366f7df7b..1387e7506ac20bfa5e2d006894835e44f992e7dc 100644 --- a/hog_tests/operator_generation/test_indexing.py +++ b/hog_tests/operator_generation/test_indexing.py @@ -263,13 +263,13 @@ def test_micro_volume_to_volume_indices(): geometry: ElementGeometry, level: int, indexing_info: IndexingInfo, - n_dofs_per_primitive, + n_dofs_per_primitive: int, primitive_type: Union[FaceType, CellType], primitive_index: Tuple[int, int, int], target_array_index: int, intra_primitive_index: int = 0, memory_layout: VolumeDoFMemoryLayout = VolumeDoFMemoryLayout.AoS, - ): + ) -> None: indexing_info.level = level dof_indices = indexing.micro_element_to_volume_indices( primitive_type, primitive_index, n_dofs_per_primitive, memory_layout diff --git a/hog_tests/operator_generation/test_opgen_smoke.py b/hog_tests/operator_generation/test_opgen_smoke.py new file mode 100644 index 0000000000000000000000000000000000000000..1b99af15aef87f108e3ddbf5a78d265363d846ed --- /dev/null +++ b/hog_tests/operator_generation/test_opgen_smoke.py @@ -0,0 +1,144 @@ +# 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 sympy.core.cache import clear_cache + +from hog.operator_generation.loop_strategies import CUBES +from hog.operator_generation.optimizer import Opts +from hog.element_geometry import ( + ElementGeometry, + LineElement, + TriangleElement, + TetrahedronElement, +) +from hog.function_space import LagrangianFunctionSpace, TrialSpace, TestSpace +from hog.operator_generation.operators import HyTeGElementwiseOperator +from hog.symbolizer import Symbolizer +from hog.quadrature import Quadrature, select_quadrule +from hog.forms import div_k_grad +from hog.forms_boundary import mass_boundary +from hog.operator_generation.kernel_types import ApplyWrapper, AssembleWrapper +from hog.operator_generation.types import hyteg_type +from hog.blending import AnnulusMap, GeometryMap, IcosahedralShellMap + + +def test_opgen_smoke(): + """ + Just a simple smoke test to check that an operator can be generated. + + If something is really broken, this will make the CI fail early. + + We are generating a matvec method here for + + ∫ k ∇u · ∇v dx + ∫ uv ds + + with either integral being evaluated in their own kernel. + + That may not be reasonable but tests some features. + """ + + clear_cache() + + symbolizer = Symbolizer() + + name = f"P2DivKGradBlendingPlusBoundaryMass" + + dims = [2] + + trial = TrialSpace(LagrangianFunctionSpace(2, symbolizer)) + test = TestSpace(LagrangianFunctionSpace(2, symbolizer)) + coeff = LagrangianFunctionSpace(2, symbolizer) + + type_descriptor = hyteg_type() + + kernel_types = [ + ApplyWrapper( + trial, + test, + type_descriptor=type_descriptor, + dims=dims, + ), + AssembleWrapper( + trial, + test, + type_descriptor=type_descriptor, + dims=dims, + ), + ] + + operator = HyTeGElementwiseOperator( + name, + symbolizer=symbolizer, + kernel_wrapper_types=kernel_types, + type_descriptor=type_descriptor, + ) + + opts_volume = {Opts.MOVECONSTANTS, Opts.VECTORIZE, Opts.TABULATE, Opts.QUADLOOPS} + opts_boundary = {Opts.MOVECONSTANTS} + + for d in dims: + if d == 2: + volume_geometry: ElementGeometry = TriangleElement() + boundary_geometry: ElementGeometry = LineElement(space_dimension=2) + blending_map: GeometryMap = AnnulusMap() + else: + volume_geometry = TetrahedronElement() + boundary_geometry = TriangleElement(space_dimension=3) + blending_map = IcosahedralShellMap() + + quad_volume = Quadrature(select_quadrule(2, volume_geometry), volume_geometry) + quad_boundary = Quadrature( + select_quadrule(5, boundary_geometry), boundary_geometry + ) + + divkgrad = div_k_grad( + trial, test, volume_geometry, symbolizer, blending_map, coeff + ) + mass_b = mass_boundary( + trial, + test, + volume_geometry, + boundary_geometry, + symbolizer, + blending_map, + ) + + operator.add_volume_integral( + name="div_k_grad", + volume_geometry=volume_geometry, + quad=quad_volume, + blending=blending_map, + form=divkgrad, + loop_strategy=CUBES(), + optimizations=opts_volume, + ) + + operator.add_boundary_integral( + name="mass_boundary", + volume_geometry=volume_geometry, + quad=quad_boundary, + blending=blending_map, + form=mass_b, + optimizations=opts_boundary, + ) + + operator.generate_class_code( + ".", + ) + + +if __name__ == "__main__": + test_opgen_smoke() diff --git a/hog_tests/test_diffusion.py b/hog_tests/test_diffusion.py index b92b534b9e5d837171a7a29307f139b8336da54c..dc8d32103251597148eacdd7253ae4b7167b6820 100644 --- a/hog_tests/test_diffusion.py +++ b/hog_tests/test_diffusion.py @@ -20,7 +20,7 @@ import logging from hog.blending import IdentityMap, ExternalMap from hog.element_geometry import TriangleElement, TetrahedronElement from hog.forms import diffusion -from hog.function_space import LagrangianFunctionSpace +from hog.function_space import LagrangianFunctionSpace, TrialSpace, TestSpace from hog.hyteg_form_template import HyTeGForm, HyTeGFormClass, HyTeGIntegrator from hog.quadrature import Quadrature, select_quadrule from hog.symbolizer import Symbolizer @@ -40,7 +40,7 @@ def test_diffusion_p1_affine(): symbolizer = Symbolizer() geometries = [TriangleElement(), TetrahedronElement()] - schemes = {TriangleElement() : 2, TetrahedronElement() : 2 } + schemes = {TriangleElement(): 2, TetrahedronElement(): 2} blending = IdentityMap() class_name = f"P1DiffusionAffine" @@ -48,8 +48,8 @@ def test_diffusion_p1_affine(): form_codes = [] for geometry in geometries: - trial = LagrangianFunctionSpace(1, symbolizer) - test = LagrangianFunctionSpace(1, symbolizer) + trial = TrialSpace(LagrangianFunctionSpace(1, symbolizer)) + test = TestSpace(LagrangianFunctionSpace(1, symbolizer)) quad = Quadrature(select_quadrule(schemes[geometry], geometry), geometry) mat = diffusion( @@ -90,8 +90,8 @@ def test_diffusion_p2_blending_2D(): form_codes = [] - trial = LagrangianFunctionSpace(2, symbolizer) - test = LagrangianFunctionSpace(2, symbolizer) + trial = TrialSpace(LagrangianFunctionSpace(2, symbolizer)) + test = TestSpace(LagrangianFunctionSpace(2, symbolizer)) schemes = {TriangleElement(): 4, TetrahedronElement(): 4} quad = Quadrature(select_quadrule(schemes[geometry], geometry), geometry) diff --git a/hog_tests/test_function_spaces.py b/hog_tests/test_function_spaces.py index 1ffa6043372a485eff98f838e83e5f33a72dc844..00bdc1e946c70792b4b2b24f39bcf2f1e9e45aa0 100644 --- a/hog_tests/test_function_spaces.py +++ b/hog_tests/test_function_spaces.py @@ -16,7 +16,11 @@ import sympy as sp from hog.element_geometry import TriangleElement -from hog.function_space import LagrangianFunctionSpace, TensorialVectorFunctionSpace +from hog.function_space import ( + FunctionSpace, + LagrangianFunctionSpace, + TensorialVectorFunctionSpace, +) from hog.symbolizer import Symbolizer from hog.exception import HOGException @@ -27,7 +31,7 @@ def test_function_spaces(): print() - f = LagrangianFunctionSpace(1, symbolizer) + f: FunctionSpace = LagrangianFunctionSpace(1, symbolizer) f_shape = f.shape(geometry) f_grad_shape = f.grad_shape(geometry) print(f) diff --git a/hog_tests/test_pspg.py b/hog_tests/test_pspg.py index 08ffcc55131435bc5dd279d7f265ac538b3088f4..671532ec4fa5ca0c5ff70fbfdab3119475ddbf40 100644 --- a/hog_tests/test_pspg.py +++ b/hog_tests/test_pspg.py @@ -20,7 +20,7 @@ import logging from hog.blending import IdentityMap from hog.element_geometry import TriangleElement, TetrahedronElement from hog.forms import pspg -from hog.function_space import LagrangianFunctionSpace +from hog.function_space import LagrangianFunctionSpace, TrialSpace, TestSpace from hog.hyteg_form_template import HyTeGForm, HyTeGFormClass, HyTeGIntegrator from hog.quadrature import Quadrature from hog.symbolizer import Symbolizer @@ -44,8 +44,8 @@ def test_pspg_p1_affine(): form_codes = [] for geometry in geometries: - trial = LagrangianFunctionSpace(1, symbolizer) - test = LagrangianFunctionSpace(1, symbolizer) + trial = TrialSpace(LagrangianFunctionSpace(1, symbolizer)) + test = TestSpace(LagrangianFunctionSpace(1, symbolizer)) quad = Quadrature("exact", geometry) form = pspg( @@ -58,7 +58,12 @@ def test_pspg_p1_affine(): ) form_codes.append( HyTeGIntegrator( - class_name, form.integrand, geometry, quad, symbolizer, integrate_rows=[0] + class_name, + form.integrand, + geometry, + quad, + symbolizer, + integrate_rows=[0], ) ) diff --git a/hog_tests/test_quadrature.py b/hog_tests/test_quadrature.py index 7574265ef85442ce17ceddc8207f4c127bac3de4..42ff33870d5973ba1feabf12f64609df0122f9d3 100644 --- a/hog_tests/test_quadrature.py +++ b/hog_tests/test_quadrature.py @@ -14,7 +14,12 @@ # 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 hog.element_geometry import LineElement, TriangleElement, TetrahedronElement +from hog.element_geometry import ( + ElementGeometry, + LineElement, + TriangleElement, + TetrahedronElement, +) from hog.quadrature import Quadrature, select_quadrule from hog.exception import HOGException @@ -22,10 +27,13 @@ from hog.exception import HOGException def test_smoke(): """Just a brief test to see if the quadrature class does _something_.""" - geometries = [TriangleElement(), TetrahedronElement()] # TODO fix quad for lines + geometries = [TriangleElement(), TetrahedronElement()] # TODO fix quad for lines for geometry in geometries: - schemes = {TriangleElement(): "exact", TetrahedronElement(): "exact"} + schemes: dict[ElementGeometry, str | int] = { + TriangleElement(): "exact", + TetrahedronElement(): "exact", + } quad = Quadrature(select_quadrule(schemes[geometry], geometry), geometry) print("points", quad.points()) @@ -42,4 +50,5 @@ def test_smoke(): print("points", quad.points()) print("weights", quad.weights()) -test_smoke() \ No newline at end of file + +test_smoke() diff --git a/hyteg_integration_tests/src/BoundaryMassTest.cpp b/hyteg_integration_tests/src/BoundaryMassTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ce26752d15ac998d592e5ab302b0241e65989117 --- /dev/null +++ b/hyteg_integration_tests/src/BoundaryMassTest.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024 Marcus Mohr, Nils Kohl. + * + * This file is part of HyTeG + * (see https://i10git.cs.fau.de/hyteg/hyteg). + * + * 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 <http://www.gnu.org/licenses/>. + */ +#include <core/Environment.h> +#include <core/math/Constants.h> +#include <core/timing/Timer.h> + +#include "hyteg/boundary/BoundaryConditions.hpp" +#include "hyteg/dataexport/VTKOutput/VTKOutput.hpp" +#include "hyteg/geometry/AnnulusMap.hpp" +#include "hyteg/geometry/IcosahedralShellMap.hpp" +#include "hyteg/mesh/MeshInfo.hpp" +#include "hyteg/primitivestorage/PrimitiveStorage.hpp" +#include "hyteg/primitivestorage/SetupPrimitiveStorage.hpp" +#include "hyteg/primitivestorage/Visualization.hpp" + +#include "BoundaryMass/TestOpBoundaryMass.hpp" + +using walberla::real_t; +using walberla::uint_c; +using walberla::uint_t; +using walberla::math::pi; + +namespace hyteg { + +real_t sphericalSurface( uint_t dim, real_t radius ) +{ + return dim == 2 ? 2 * radius * pi : 4 * radius * radius * pi; +} + +real_t runTestSphericalAnyDim( uint_t dim, uint_t level, bool inside, real_t innerRad, real_t outerRad ) +{ + bool beVerbose = true; + + std::shared_ptr< SetupPrimitiveStorage > setupStorage; + + uint_t markerInnerBoundary = 11; + uint_t markerOuterBoundary = 22; + + if ( dim == 2 ) + { + uint_t nLayers = 2; + + MeshInfo meshInfo = MeshInfo::meshAnnulus( innerRad, outerRad, MeshInfo::CROSS, 8, nLayers ); + + setupStorage = + std::make_shared< SetupPrimitiveStorage >( meshInfo, uint_c( walberla::mpi::MPIManager::instance()->numProcesses() ) ); + AnnulusMap::setMap( *setupStorage ); + } + else + { + uint_t nRad = 2; + uint_t nTan = 2; + + MeshInfo meshInfo = MeshInfo::meshSphericalShell( nTan, nRad, innerRad, outerRad ); + + setupStorage = + std::make_shared< SetupPrimitiveStorage >( meshInfo, uint_c( walberla::mpi::MPIManager::instance()->numProcesses() ) ); + IcosahedralShellMap::setMap( *setupStorage ); + } + + real_t tol = 1e-6; + real_t boundaryRad = 0.0; + // flag the inner and outer boundary by assigning different values + auto onBoundary = [&boundaryRad, tol]( const Point3D& x ) { + real_t radius = std::sqrt( x[0] * x[0] + x[1] * x[1] + x[2] * x[2] ); + return std::abs( boundaryRad - radius ) < tol; + }; + + boundaryRad = outerRad; + setupStorage->setMeshBoundaryFlagsByVertexLocation( markerOuterBoundary, onBoundary, true ); + + boundaryRad = innerRad; + setupStorage->setMeshBoundaryFlagsByVertexLocation( markerInnerBoundary, onBoundary, true ); + + auto storage = std::make_shared< PrimitiveStorage >( *setupStorage ); + + writeDomainPartitioningVTK( storage, ".", "domain" ); + + // ----------------------- + // Function Manipulation + // ----------------------- + + P2Function< real_t > one( "one", storage, level, level ); + P2Function< real_t > result( "result", storage, level, level ); + + // generate bc object and set different conditions on inside and outside + BoundaryCondition bcs; + BoundaryUID massBoundaryUID = inside ? bcs.createDirichletBC( "massBoundary", markerInnerBoundary ) : + bcs.createDirichletBC( "massBoundary", markerOuterBoundary ); + + one.setBoundaryCondition( bcs ); + result.setBoundaryCondition( bcs ); + + one.interpolate( 1.0, level, All ); + + operatorgeneration::TestOpBoundaryMass boundaryMassOp( storage, level, level, bcs, massBoundaryUID ); + + boundaryMassOp.apply( one, result, level, All ); + + communication::syncFunctionBetweenPrimitives( result, level ); + + auto sumFreeslipBoundary = result.sumGlobal( level, All ); + + if ( beVerbose ) + { + std::string fPath = "."; + std::string fName = "BoundaryIntegralTest"; + VTKOutput vtkOutput( fPath, fName, storage ); + vtkOutput.add( one ); + vtkOutput.add( result ); + vtkOutput.write( level ); + } + + return sumFreeslipBoundary; +} + +} // namespace hyteg + +int main( int argc, char* argv[] ) +{ + walberla::Environment walberlaEnv( argc, argv ); + walberla::MPIManager::instance()->useWorldComm(); + + real_t innerRad = 1.1; + real_t outerRad = 2.1; + + for ( uint_t dim : { 2u } ) + { + for ( bool inside : { true, false } ) + { + WALBERLA_LOG_INFO_ON_ROOT( "" << ( dim == 2 ? "Annulus " : "IcoShell " ) << ( inside ? "inside " : "outside " ) + << "test" ) + real_t lastError = 0; + real_t rate = 1; + + uint_t maxLevel = dim == 2 ? 7 : 5; + + for ( uint_t level = 2; level < maxLevel; level++ ) + { + auto surfaceComputed = hyteg::runTestSphericalAnyDim( dim, level, inside, innerRad, outerRad ); + + auto error = std::abs( surfaceComputed - hyteg::sphericalSurface( dim, inside ? innerRad : outerRad ) ); + + if ( level > 2 ) + { + rate = error / lastError; + if ( error > 1e-13 ) + { + WALBERLA_CHECK_LESS( rate, 0.016 ); + } + else + { + WALBERLA_CHECK_LESS( error, 1e-13 ); + } + + } + + WALBERLA_LOG_INFO_ON_ROOT( "level: " << level << " | surface computed: " << surfaceComputed << " | error: " << error + << " | rate: " << rate ); + + lastError = error; + } + } + } + + return 0; +} diff --git a/hyteg_integration_tests/src/CMakeLists.txt b/hyteg_integration_tests/src/CMakeLists.txt index d1c23b1d28ffd1483fb72bd7b46dcdb6527e8820..a72249fecdc2161cec678af292bfc30a83456527 100644 --- a/hyteg_integration_tests/src/CMakeLists.txt +++ b/hyteg_integration_tests/src/CMakeLists.txt @@ -73,3 +73,5 @@ add_operator_test(FILE DiffusionP1IcosahedralShell.cpp FORM Diffusion A add_operator_test(FILE FullStokes.cpp DEF TEST_DIAG FORM=forms::p2_full_stokesvar_0_0_blending_q3 FORM FullStokes_0_0 ABBR 00b3VIQT GEN_ARGS -s P2 -b IcosahedralShellMap -o MOVECONSTANTS QUADLOOPS TABULATE VECTORIZE --quad-rule yu_3 yu_3) add_operator_test(FILE FullStokes.cpp DEF FORM=forms::p2_full_stokesvar_2_1_blending_q3 FORM FullStokes_2_1 ABBR 21b3VIQT GEN_ARGS -s P2 -b IcosahedralShellMap -o MOVECONSTANTS QUADLOOPS TABULATE VECTORIZE --quad-rule yu_3 yu_3) + +add_operator_test(FILE BoundaryMassTest.cpp DEF FORM BoundaryMass ABBR BM GEN_ARGS -s P2 -b AnnulusMap --quad-degree 5 5 --dimensions 2) diff --git a/hyteg_integration_tests/src/CurlCurl.cpp b/hyteg_integration_tests/src/CurlCurl.cpp index e692845c15d109213ab6f03cb57fe802256d8800..deb836c5b39f898f555c270cd666ac06a70d0efc 100644 --- a/hyteg_integration_tests/src/CurlCurl.cpp +++ b/hyteg_integration_tests/src/CurlCurl.cpp @@ -34,7 +34,7 @@ int main( int argc, char* argv[] ) const uint_t level = 2; const StorageSetup storageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); real_t thresholdOverMachineEpsApply = 225; real_t thresholdOverMachineEpsInvDiag = 9.0e6; diff --git a/hyteg_integration_tests/src/CurlCurlPlusMass.cpp b/hyteg_integration_tests/src/CurlCurlPlusMass.cpp index 8451a988e9a80886a8f100c9b5661a1c6faba438..f991650c9fcefc84845c43cbb0556d3a57c40ba8 100644 --- a/hyteg_integration_tests/src/CurlCurlPlusMass.cpp +++ b/hyteg_integration_tests/src/CurlCurlPlusMass.cpp @@ -61,7 +61,7 @@ int main( int argc, char* argv[] ) const uint_t level = 2; const StorageSetup storageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); real_t thresholdOverMachineEpsApply = 225; real_t thresholdOverMachineEpsInvDiag = 9.0e6; diff --git a/hyteg_integration_tests/src/DiffusionP1.cpp b/hyteg_integration_tests/src/DiffusionP1.cpp index 6341dfeb6033cfb4b233ef7a34c136708ee19bfb..ba0c748d9b021c079dd744a502ad2f652c66d96c 100644 --- a/hyteg_integration_tests/src/DiffusionP1.cpp +++ b/hyteg_integration_tests/src/DiffusionP1.cpp @@ -40,12 +40,12 @@ int main( int argc, char* argv[] ) if ( d == 2 ) { storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); } else { storageSetup = StorageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); } testOperators< P1Function< real_t >, P1ElementwiseLaplaceOperator, operatorgeneration::TestOpDiffusion >( diff --git a/hyteg_integration_tests/src/DiffusionP1Vector.cpp b/hyteg_integration_tests/src/DiffusionP1Vector.cpp index 2cbd2c9b4a2cdf919a5d71fb3d863bfc6ad73fa5..9af9d18722d197c894615a49773c744b157098e5 100644 --- a/hyteg_integration_tests/src/DiffusionP1Vector.cpp +++ b/hyteg_integration_tests/src/DiffusionP1Vector.cpp @@ -19,10 +19,10 @@ #include "core/DataTypes.h" #include "hyteg/elementwiseoperators/P1ElementwiseOperator.hpp" -#include "mixed_operator/VectorLaplaceOperator.hpp" #include "Diffusion/TestOpDiffusion.hpp" #include "OperatorGenerationTest.hpp" +#include "mixed_operator/VectorLaplaceOperator.hpp" using namespace hyteg; using walberla::real_t; @@ -40,12 +40,12 @@ int main( int argc, char* argv[] ) if ( d == 2 ) { storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); } else { storageSetup = StorageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); } testOperators< P1VectorFunction< real_t >, P1ElementwiseVectorLaplaceOperator, operatorgeneration::TestOpDiffusion >( diff --git a/hyteg_integration_tests/src/DiffusionP2.cpp b/hyteg_integration_tests/src/DiffusionP2.cpp index c6614e4c87c4d99002205932a40c0ac0c2937343..5fc3aa20db5f9f53e8af12740c10b041107efc43 100644 --- a/hyteg_integration_tests/src/DiffusionP2.cpp +++ b/hyteg_integration_tests/src/DiffusionP2.cpp @@ -40,12 +40,12 @@ int main( int argc, char* argv[] ) if ( d == 2 ) { storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); } else { storageSetup = StorageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); } testOperators< P2Function< real_t >, P2ElementwiseLaplaceOperator, operatorgeneration::TestOpDiffusion >( diff --git a/hyteg_integration_tests/src/Div.cpp b/hyteg_integration_tests/src/Div.cpp index bdaec8a9ee1bee38e184e68bc1d73ec23dfe1c4c..01b767d9bfb9950ec4bcf5757ace30b552c4a086 100644 --- a/hyteg_integration_tests/src/Div.cpp +++ b/hyteg_integration_tests/src/Div.cpp @@ -33,7 +33,7 @@ int main( int argc, char* argv[] ) const uint_t level = 3; StorageSetup storageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); real_t thresholdOverMachineEpsApply = 225; diff --git a/hyteg_integration_tests/src/DivKGrad.cpp b/hyteg_integration_tests/src/DivKGrad.cpp index 4fc2cb6a63cd53807b1719e10b933a8f945754c5..2717a0c28588754b37827ea6b710171b26bfaf09 100644 --- a/hyteg_integration_tests/src/DivKGrad.cpp +++ b/hyteg_integration_tests/src/DivKGrad.cpp @@ -66,12 +66,12 @@ int main( int argc, char* argv[] ) if ( d == 2 ) { storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); } else { storageSetup = StorageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); } compareApply< P2ElementwiseAffineDivKGradOperator, operatorgeneration::TestOpDivKGrad >( diff --git a/hyteg_integration_tests/src/DivT.cpp b/hyteg_integration_tests/src/DivT.cpp index da5fc13144c4b619d924d4066a3e77c6a590b72e..67c70b5112c644f17760c0bc184e034599a69fef 100644 --- a/hyteg_integration_tests/src/DivT.cpp +++ b/hyteg_integration_tests/src/DivT.cpp @@ -33,7 +33,7 @@ int main( int argc, char* argv[] ) const uint_t level = 3; StorageSetup storageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); real_t thresholdOverMachineEpsApply = 225; diff --git a/hyteg_integration_tests/src/Epsilon.cpp b/hyteg_integration_tests/src/Epsilon.cpp index 0fcb6f2e5cdde18346361b572214fb45bae53e6f..d79d9e051b8aa4f0a27dd881b00057a0e384fe58 100644 --- a/hyteg_integration_tests/src/Epsilon.cpp +++ b/hyteg_integration_tests/src/Epsilon.cpp @@ -78,12 +78,12 @@ int main( int argc, char* argv[] ) if ( d == 2 ) { storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); } else { storageSetup = StorageSetup( - "cube_6el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/3D/cube_6el.msh" ), GeometryMap::Type::IDENTITY ); + "cube_6el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "3D/cube_6el.msh" ) ), GeometryMap::Type::IDENTITY ); } compareApply< P2ElementwiseOperator< FORM >, operatorgeneration::TestOpEpsilon >( diff --git a/hyteg_integration_tests/src/EpsilonVector.cpp b/hyteg_integration_tests/src/EpsilonVector.cpp index 5ad41ed18fc4457b859be85c4a774d256ebf8cfa..3d7ba6acb1f3b8ba3aca2acb9374955a47e33a7d 100644 --- a/hyteg_integration_tests/src/EpsilonVector.cpp +++ b/hyteg_integration_tests/src/EpsilonVector.cpp @@ -21,12 +21,12 @@ #include "core/DataTypes.h" #include "hyteg/elementwiseoperators/P2ElementwiseOperator.hpp" -#include "constant_stencil_operator/P2ConstantEpsilonOperator.hpp" #include "hyteg/forms/form_hyteg_generated/p2/p2_epsilonvar_affine_q4.hpp" #include "hyteg/p2functionspace/P2Function.hpp" #include "Epsilon/TestOpEpsilon.hpp" #include "OperatorGenerationTest.hpp" +#include "constant_stencil_operator/P2ConstantEpsilonOperator.hpp" using namespace hyteg; using walberla::real_t; @@ -67,7 +67,7 @@ int main( int argc, char* argv[] ) StorageSetup storageSetup; storageSetup = StorageSetup( - "quad_4el", MeshInfo::fromGmshFile( "../hyteg/data/meshes/quad_4el.msh" ), GeometryMap::Type::IDENTITY ); + "quad_4el", MeshInfo::fromGmshFile( prependHyTeGMeshDir( "2D/quad_4el.msh" ) ), GeometryMap::Type::IDENTITY ); compareApply< P2ElementwiseAffineEpsilonOperator, operatorgeneration::TestOpEpsilon >( makeRefOp, diff --git a/hyteg_integration_tests/src/OperatorGenerationTest.hpp b/hyteg_integration_tests/src/OperatorGenerationTest.hpp index 3eaec8c9f591fcc9b39a7dd99863f61a4cae1024..8719f6d5ef62fbed630bedb3fc89c39deb71f1d7 100644 --- a/hyteg_integration_tests/src/OperatorGenerationTest.hpp +++ b/hyteg_integration_tests/src/OperatorGenerationTest.hpp @@ -367,10 +367,12 @@ void compareInvDiag( OperatorFactory< RefOpType > refOpFactory, // apply reference and test operators RefOpType opRef = refOpFactory( storage, level, level ); opRef.computeInverseDiagonalOperatorValues(); + opRef.computeInverseDiagonalOperatorValues(); std::shared_ptr< RefDiagType > diagRef = opRef.getInverseDiagonalValues(); TestOpType opTest = testOpFactory( storage, level, level ); opTest.computeInverseDiagonalOperatorValues(); + opTest.computeInverseDiagonalOperatorValues(); std::shared_ptr< TestDiagType > diagTest = opTest.getInverseDiagonalValues(); // compare diff --git a/pyproject.toml b/pyproject.toml index da0695c357b91b75c35e34ca3bb6878fe21f9a32..5da6620d565c734fb43fada2f9cd98360cc09905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "hog" version = "0.1" dependencies = [ "islpy", + "ndim @ https://github.com/sigma-py/ndim/archive/refs/tags/v0.1.6.tar.gz", "numpy==1.24.3", "quadpy-gpl==0.16.10", "poly-cse-py", @@ -13,7 +14,15 @@ dependencies = [ ] [tool.setuptools] -packages = ["hog", "hog.operator_generation", "hog.quadrature"] +packages = [ + "hog", + "hog.operator_generation", + "hog.quadrature", + "hog.recipes", + "hog.recipes.integrands", + "hog.recipes.integrands.boundary", + "hog.recipes.integrands.volume" +] [tool.mypy] pretty = true diff --git a/requirements.txt b/requirements.txt index 75d777c50a55a287d4f9e9a432552bb95f68e07b..2caff95ea34485b863246abbef5cc33a8748e1be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ --extra-index-url https://test.pypi.org/simple/ islpy +ndim @ https://github.com/sigma-py/ndim/archive/refs/tags/v0.1.6.tar.gz numpy==1.24.3 poly-cse-py pystencils @ git+https://i10git.cs.fau.de/hyteg/pystencils.git@4a790e1c48f32c07fc4058de9b20734bcea9cca0