From d3651ddcd8342c03a8c15399b5762e681750f988 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Fri, 2 Aug 2024 07:56:19 +0200 Subject: [PATCH] Non Matching Multipatch (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## List of changes - Add non matching conforming projection operators w/ homogeneous BCs for V0 and V1 - Update multipatch examples to newer Psydac version - Add `__init__.py` to tests - Add non matching examples from previous branch - Add more tests - Change in naming convention: primal Hodge operator corresponds to mass matrix --------- Co-authored-by: Yaman Güçlü Co-authored-by: Martin Campos Pinto Co-authored-by: jowezarek Co-authored-by: e-moral-sanchez Co-authored-by: Elena Moral Sánchez <88042165+e-moral-sanchez@users.noreply.github.com> Co-authored-by: kvrigor --- .gitignore | 1 + docs/source/modules/feec.multipatch.rst | 4 + psydac/api/discretization.py | 4 +- psydac/api/fem.py | 4 +- psydac/api/postprocessing.py | 7 +- psydac/api/settings.py | 2 +- psydac/api/tests/build_domain.py | 45 +- .../examples/h1_source_pbms_conga_2d.py | 260 ++-- .../examples/hcurl_eigen_pbms_conga_2d.py | 406 +++-- .../examples/hcurl_eigen_pbms_dg_2d.py | 309 ++++ .../examples/hcurl_eigen_testcases.py | 294 ++++ .../examples/hcurl_source_pbms_conga_2d.py | 370 ++--- .../examples/hcurl_source_testcase.py | 144 ++ .../examples/mixed_source_pbms_conga_2d.py | 220 +-- .../multipatch/examples/ppc_test_cases.py | 610 ++++---- .../multipatch/examples/timedomain_maxwell.py | 1278 ++++++++++++++++ .../examples/timedomain_maxwell_testcase.py | 275 ++++ .../multipatch/multipatch_domain_utilities.py | 1303 ++++++++++------- .../feec/multipatch/non_matching_operators.py | 1186 +++++++++++++++ psydac/feec/multipatch/operators.py | 659 +++++---- psydac/feec/multipatch/plotting_utilities.py | 532 +++++-- psydac/feec/multipatch/tests/__init__.py | 0 .../test_feec_conf_projectors_cart_2d.py | 309 ++++ .../tests/test_feec_maxwell_multipatch_2d.py | 188 ++- .../tests/test_feec_poisson_multipatch_2d.py | 55 +- psydac/feec/multipatch/utilities.py | 120 +- psydac/feec/multipatch/utils_conga_2d.py | 280 ++++ pyproject.toml | 5 +- 28 files changed, 7108 insertions(+), 1762 deletions(-) create mode 100644 psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py create mode 100644 psydac/feec/multipatch/examples/hcurl_eigen_testcases.py create mode 100644 psydac/feec/multipatch/examples/hcurl_source_testcase.py create mode 100644 psydac/feec/multipatch/examples/timedomain_maxwell.py create mode 100644 psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py create mode 100644 psydac/feec/multipatch/non_matching_operators.py create mode 100644 psydac/feec/multipatch/tests/__init__.py create mode 100644 psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py diff --git a/.gitignore b/.gitignore index b65496d78..71dfeaf04 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.lock __psydac__/ __*pyccel__/ +docs/source/modules/STUBDIR/* build *build* diff --git a/docs/source/modules/feec.multipatch.rst b/docs/source/modules/feec.multipatch.rst index 03daf1d63..64bf49a12 100644 --- a/docs/source/modules/feec.multipatch.rst +++ b/docs/source/modules/feec.multipatch.rst @@ -9,5 +9,9 @@ feec.multipatch multipatch.api multipatch.fem_linear_operators + multipatch.multipatch_domain_utilities + multipatch.non_matching_operators multipatch.operators + multipatch.plotting_utilities multipatch.utilities + multipatch.utils_conga_2d diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 9cddd4a23..597baa731 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -102,7 +102,7 @@ def get_max_degree_of_one_space(Vh): Vh : FemSpace The finite element space under investigation. - Results + Returns ------- list[int] The maximum polynomial degre of Vh with respect to each coordinate. @@ -133,7 +133,7 @@ def get_max_degree(*spaces): *spaces : tuple[FemSpace] The finite element spaces under investigation. - Results + Returns ------- list[int] The maximum polynomial degree across all spaces, with respect to each diff --git a/psydac/api/fem.py b/psydac/api/fem.py index a62d69948..9a5eba0a4 100644 --- a/psydac/api/fem.py +++ b/psydac/api/fem.py @@ -767,7 +767,9 @@ def allocate_matrices(self, backend=None): elif use_prolongation: Ps = [knot_insertion_projection_operator(trs, trs.get_refined_space(ncells)) for trs in trial_fem_space.spaces] P = BlockLinearOperator(trial_fem_space.vector_space, trial_fem_space.get_refined_space(ncells).vector_space) - for ni,Pi in enumerate(Ps):P[ni,ni] = Pi + for ni,Pi in enumerate(Ps): + P[ni,ni] = Pi + mat = ComposedLinearOperator(trial_space, test_space, mat, P) self._matrix[i,j] = mat diff --git a/psydac/api/postprocessing.py b/psydac/api/postprocessing.py index 2ac42fa64..52e8fd0ae 100644 --- a/psydac/api/postprocessing.py +++ b/psydac/api/postprocessing.py @@ -953,12 +953,9 @@ def _reconstruct_spaces(self): for subdomain_names, space_dict in subdomains_to_spaces.items(): if space_dict == {}: continue - ncells_dict = {interior_name: interior_names_to_ncells[interior_name] for interior_name in subdomain_names} - # No need for a a dict until PR about non-conforming meshes is merged - # Check for conformity - ncells = list(ncells_dict.values())[0] - assert all(ncells_patch == ncells for ncells_patch in ncells_dict.values()) + ncells = {interior_name: interior_names_to_ncells[interior_name] for interior_name in subdomain_names} + subdomain = domain.get_subdomain(subdomain_names) space_name_0 = list(space_dict.keys())[0] periodic = space_dict[space_name_0][2].get('periodic', None) diff --git a/psydac/api/settings.py b/psydac/api/settings.py index 74a48c645..6d0988451 100644 --- a/psydac/api/settings.py +++ b/psydac/api/settings.py @@ -67,4 +67,4 @@ 'pyccel-intel' : PSYDAC_BACKEND_IPYCCEL, 'pyccel-pgi' : PSYDAC_BACKEND_PGPYCCEL, 'pyccel-nvidia': PSYDAC_BACKEND_NVPYCCEL, -} +} \ No newline at end of file diff --git a/psydac/api/tests/build_domain.py b/psydac/api/tests/build_domain.py index c34645dfc..a6ca8ec33 100644 --- a/psydac/api/tests/build_domain.py +++ b/psydac/api/tests/build_domain.py @@ -1,10 +1,16 @@ # coding: utf-8 +# todo: this file has a lot of redundant code with psydac/feec/multipatch/multipatch_domain_utilities.py +# it should probably be removed in the future + import numpy as np from sympde.topology import Square, Domain from sympde.topology import IdentityMapping, PolarMapping, AffineMapping, Mapping +# remove after sympde PR #155 is merged and call Domain.join instead +from psydac.feec.multipatch.multipatch_domain_utilities import sympde_Domain_join + #============================================================================== # small extension to SymPDE: class TransposedPolarMapping(Mapping): @@ -20,6 +26,7 @@ class TransposedPolarMapping(Mapping): _ldim = 2 _pdim = 2 +# todo: remove this def create_domain(patches, interfaces, name): connectivity = [] patches_interiors = [D.interior for D in patches] @@ -54,6 +61,8 @@ def flip_axis(name='no_name', c1=0., c2=0.): ) #============================================================================== + +# todo: use build_multipatch_domain instead def build_pretzel(domain_name='pretzel', r_min=None, r_max=None): """ design pretzel-like domain @@ -189,23 +198,29 @@ def build_pretzel(domain_name='pretzel', r_min=None, r_max=None): domain_14, ]) - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1), 1], - [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=1), 1], - [domain_6.get_boundary(axis=1, ext=-1), domain_2.get_boundary(axis=1, ext=-1), 1], - [domain_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1), 1], - [domain_7.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1), 1], - [domain_3.get_boundary(axis=1, ext=+1), domain_9.get_boundary(axis=1, ext=-1), 1], - [domain_9.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1), 1], - [domain_4.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=1), 1], - [domain_12.get_boundary(axis=1, ext=-1), domain_1.get_boundary(axis=1, ext=-1), 1], - [domain_6.get_boundary(axis=0, ext=-1), domain_13.get_boundary(axis=0, ext=1), 1], - [domain_7.get_boundary(axis=0, ext=-1), domain_13.get_boundary(axis=0, ext=-1), 1], - [domain_5.get_boundary(axis=0, ext=-1), domain_14.get_boundary(axis=0, ext=-1), 1], - [domain_12.get_boundary(axis=0, ext=-1), domain_14.get_boundary(axis=0, ext=+1),1], + axis_0 = 0 + axis_1 = 1 + ext_0 = -1 + ext_1 = +1 + + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_6, axis_1, ext_1), 1], + [(domain_6, axis_1, ext_0), (domain_2, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_7, axis_1, ext_0), 1], + [(domain_7, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_9, axis_1, ext_0), 1], + [(domain_9, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_12, axis_1, ext_1), 1], + [(domain_12, axis_1, ext_0), (domain_1, axis_1, ext_0), 1], + [(domain_6, axis_0, ext_0), (domain_13, axis_0, ext_1), 1], + [(domain_7, axis_0, ext_0), (domain_13, axis_0, ext_0), 1], + [(domain_5, axis_0, ext_0), (domain_14, axis_0, ext_0), 1], + [(domain_12, axis_0, ext_0), (domain_14, axis_0, ext_1), 1], ] + # domain = Domain.join(patches, connectivity, name=domain_name) + domain = sympde_Domain_join(patches, connectivity, name=domain_name) - domain = create_domain(patches, interfaces, domain_name) return domain diff --git a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py index 31e3ddf5d..c0e401e8f 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -1,4 +1,17 @@ -# coding: utf-8 +""" + solver for the problem: find u in H^1, such that + + A u = f on \\Omega + u = u_bc on \\partial \\Omega + + where the operator + + A u := eta * u - mu * div grad u + + is discretized as Ah: V0h -> V0h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h +""" from mpi4py import MPI @@ -9,42 +22,47 @@ from sympy import lambdify from scipy.sparse.linalg import spsolve +from sympde.calculus import dot from sympde.expr.expr import LinearForm from sympde.expr.expr import integral, Norm -from sympde.topology import Derham +from sympde.topology import Derham from sympde.topology import element_of - -from psydac.api.settings import PSYDAC_BACKENDS +from psydac.api.settings import PSYDAC_BACKENDS from psydac.feec.multipatch.api import discretize -from psydac.feec.pull_push import pull_2d_h1 +from psydac.feec.pull_push import pull_2d_h1 +from psydac.feec.multipatch.utils_conga_2d import P0_phys -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.plotting_utilities import plot_field from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution -from psydac.feec.multipatch.utilities import time_count +from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_h1 +from psydac.feec.multipatch.utilities import time_count +from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection +from psydac.api.postprocessing import OutputManager, PostProcessManager from psydac.linalg.utilities import array_to_psydac -from psydac.fem.basic import FemField +from psydac.fem.basic import FemField + +from psydac.api.postprocessing import OutputManager, PostProcessManager + def solve_h1_source_pbm( - nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='P_L2', source_type='manu_poisson', - eta=-10., mu=1., gamma_h=10., - plot_source=False, plot_dir=None, hide_plots=True + nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_type='manu_poisson_elliptic', + eta=-10., mu=1., gamma_h=10., plot_dir=None, ): """ solver for the problem: find u in H^1, such that - A u = f on \Omega - u = u_bc on \partial \Omega + A u = f on \\Omega + u = u_bc on \\partial \\Omega where the operator A u := eta * u - mu * div grad u - is discretized as Ah: V0h -> V0h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \Omega, + is discretized as Ah: V0h -> V0h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, V0h --grad-> V1h -—curl-> V2h @@ -60,35 +78,41 @@ def solve_h1_source_pbm( :param nc: nb of cells per dimension, in each patch :param deg: coordinate degree in each patch + :param domain_name: name of the domain + :param backend_language: backend language for the operators + :param source_type: must be implemented in get_source_and_solution_h1 + :param eta: coefficient of the elliptic operator + :param mu: coefficient of the elliptic operator :param gamma_h: jump penalization parameter - :param source_proj: approximation operator for the source, possible values are 'P_geom' or 'P_L2' - :param source_type: must be implemented in get_source_and_solution() + :param plot_dir: directory for the plots (if None, no plots are generated) """ - ncells = [nc, nc] - degree = [deg,deg] - - # if backend_language is None: - # backend_language='python' - # print('[note: using '+backend_language+ ' backends in discretize functions]') + degree = [deg, deg] print('---------------------------------------------------------------------------------------------------------') print('Starting solve_h1_source_pbm function with: ') - print(' ncells = {}'.format(ncells)) + print(' ncells = {}'.format(nc)) print(' degree = {}'.format(degree)) print(' domain_name = {}'.format(domain_name)) - print(' source_proj = {}'.format(source_proj)) print(' backend_language = {}'.format(backend_language)) print('---------------------------------------------------------------------------------------------------------') print('building the multipatch domain...') domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) for P in domain.interior]) + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) mappings_list = list(mappings.values()) + + if isinstance(nc, int): + ncells = [nc, nc] + else: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + domain_h = discretize(domain, ncells=ncells) print('building the symbolic and discrete deRham sequences...') - derham = Derham(domain, ["H1", "Hcurl", "L2"]) + derham = Derham(domain, ["H1", "Hcurl", "L2"]) derham_h = discretize(derham, domain_h, degree=degree) # multi-patch (broken) spaces @@ -103,11 +127,10 @@ def solve_h1_source_pbm( # broken (patch-wise) differential operators bD0, bD1 = derham_h.broken_derivatives_as_operators bD0_m = bD0.to_sparse_matrix() - # bD1_m = bD1.to_sparse_matrix() print('building the discrete operators:') print('commuting projection operators...') - nquads = [4*(d + 1) for d in degree] + nquads = [4 * (d + 1) for d in degree] P0, P1, P2 = derham_h.projectors(nquads=nquads) I0 = IdLinearOperator(V0h) @@ -118,31 +141,21 @@ def solve_h1_source_pbm( H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language) H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language) - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = mass matrix of V0 - H0_m = H0.to_sparse_matrix() # = inverse mass matrix of V0 - dH1_m = H1.get_dual_Hodge_sparse_matrix() # = mass matrix of V1 - # H1_m = H1.to_sparse_matrix() # = inverse mass matrix of V1 + H0_m = H0.to_sparse_matrix() # = mass matrix of V0 + dH0_m = H0.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V0 + H1_m = H1.to_sparse_matrix() # = mass matrix of V1 print('conforming projection operators...') - # conforming Projections (should take into account the boundary conditions of the continuous deRham sequence) - cP0 = derham_h.conforming_projection(space='V0', hom_bc=True, backend_language=backend_language) - cP0_m = cP0.to_sparse_matrix() - # cP1 = derham_h.conforming_projection(space='V1', hom_bc=True, backend_language=backend_language) - # cP1_m = cP1.to_sparse_matrix() - - if not os.path.exists(plot_dir): - os.makedirs(plot_dir) + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) def lift_u_bc(u_bc): if u_bc is not None: print('lifting the boundary condition in V0h... [warning: Not Tested Yet!]') - # note: for simplicity we apply the full P1 on u_bc, but we only need to set the boundary dofs - u_bc = lambdify(domain.coordinates, u_bc) - u_bc_log = [pull_2d_h1(u_bc, m) for m in mappings_list] - # it's a bit weird to apply P1 on the list of (pulled back) logical fields -- why not just apply it on u_bc ? - uh_bc = P0(u_bc_log) - ubc_c = uh_bc.coeffs.toarray() - # removing internal dofs (otherwise ubc_c may already be a very good approximation of uh_c ...) + d_ubc_c = derham_h.get_dual_dofs(space='V0', f=u_bc, backend_language=backend_language, return_format='numpy_array') + ubc_c = dH0_m.dot(d_ubc_c) + ubc_c = ubc_c - cP0_m.dot(ubc_c) else: ubc_c = None @@ -150,49 +163,26 @@ def lift_u_bc(u_bc): # Conga (projection-based) stiffness matrices: # div grad: - pre_DG_m = - bD0_m.transpose() @ dH1_m @ bD0_m + pre_DG_m = - bD0_m.transpose() @ H1_m @ bD0_m # jump penalization: jump_penal_m = I0_m - cP0_m - JP0_m = jump_penal_m.transpose() * dH0_m * jump_penal_m + JP0_m = jump_penal_m.transpose() @ H0_m @ jump_penal_m - pre_A_m = cP0_m.transpose() @ ( eta * dH0_m - mu * pre_DG_m ) # useful for the boundary condition (if present) + # useful for the boundary condition (if present) + pre_A_m = cP0_m.transpose() @ (eta * H0_m - mu * pre_DG_m) A_m = pre_A_m @ cP0_m + gamma_h * JP0_m print('getting the source and ref solution...') - # (not all the returned functions are useful here) - N_diag = 200 - method = 'conga' - f_scal, f_vect, u_bc, ph_ref, uh_ref, p_ex, u_ex, phi, grad_phi = get_source_and_solution( + f_scal, u_bc, u_ex = get_source_and_solution_h1( source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name, - refsol_params=[N_diag, method, source_proj], ) # compute approximate source f_h - b_c = f_c = None - if source_proj == 'P_geom': - print('projecting the source with commuting projection P0...') - f = lambdify(domain.coordinates, f_scal) - f_log = [pull_2d_h1(f, m) for m in mappings_list] - f_h = P0(f_log) - f_c = f_h.coeffs.toarray() - b_c = dH0_m.dot(f_c) - - elif source_proj == 'P_L2': - print('projecting the source with L2 projection...') - v = element_of(V0h.symbolic_space, name='v') - expr = f_scal * v - l = LinearForm(v, integral(domain, expr)) - lh = discretize(l, domain_h, V0h, backend=PSYDAC_BACKENDS[backend_language]) - b = lh.assemble() - b_c = b.toarray() - if plot_source: - f_c = H0_m.dot(b_c) - else: - raise ValueError(source_proj) - - if plot_source: - plot_field(numpy_coeffs=f_c, Vh=V0h, space_kind='h1', domain=domain, title='f_h with P = '+source_proj, filename=plot_dir+'/fh_'+source_proj+'.png', hide_plot=hide_plots) + b_c = derham_h.get_dual_dofs(space='V0', f=f_scal, backend_language=backend_language, return_format='numpy_array') + # source in primal sequence for plotting + f_c = dH0_m.dot(b_c) + b_c = cP0_m.transpose() @ b_c ubc_c = lift_u_bc(u_bc) @@ -215,60 +205,90 @@ def lift_u_bc(u_bc): uh_c += ubc_c print('getting and plotting the FEM solution from numpy coefs array...') - title = r'solution $\phi_h$ (amplitude)' - params_str = 'eta={}_mu={}_gamma_h={}'.format(eta, mu, gamma_h) - plot_field(numpy_coeffs=uh_c, Vh=V0h, space_kind='h1', domain=domain, title=title, filename=plot_dir+params_str+'_phi_h.png', hide_plot=hide_plots) - if u_ex: - u = element_of(V0h.symbolic_space, name='u') - l2norm = Norm(u - u_ex, domain, kind='l2') - l2norm_h = discretize(l2norm, domain_h, V0h) - uh_c = array_to_psydac(uh_c, V0h.vector_space) - l2_error = l2norm_h.assemble(u=FemField(V0h, coeffs=uh_c)) - return l2_error + u_ex_c = derham_h.get_dual_dofs(space='V0', f=u_ex, backend_language=backend_language, return_format='numpy_array') + u_ex_c = dH0_m.dot(u_ex_c) + + if plot_dir is not None: + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V0h=V0h) + OM.set_static() + + stencil_coeffs = array_to_psydac(uh_c, V0h.vector_space) + vh = FemField(V0h, coeffs=stencil_coeffs) + OM.export_fields(vh=vh) + + stencil_coeffs = array_to_psydac(f_c, V0h.vector_space) + fh = FemField(V0h, coeffs=stencil_coeffs) + OM.export_fields(fh=fh) + + if u_ex: + stencil_coeffs = array_to_psydac(u_ex_c, V0h.vector_space) + uh_ex = FemField(V0h, coeffs=stencil_coeffs) + OM.export_fields(uh_ex=uh_ex) + + OM.export_space_info() + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + + PM.export_to_vtk( + plot_dir + "/u_h", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + + PM.export_to_vtk( + plot_dir + "/f_h", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='fh') + + if u_ex: + PM.export_to_vtk( + plot_dir + "/uh_ex", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='uh_ex') + + PM.close() -if __name__ == '__main__': + if u_ex: + err = uh_c - u_ex_c + rel_err = np.sqrt(np.dot(err, H0_m.dot(err)))/np.sqrt(np.dot(u_ex_c,H0_m.dot(u_ex_c))) + + return rel_err - t_stamp_full = time_count() - quick_run = True - # quick_run = False +if __name__ == '__main__': - omega = np.sqrt(170) # source - roundoff = 1e4 - eta = int(-omega**2 * roundoff)/roundoff - # print(eta) - # source_type = 'elliptic_J' - source_type = 'manu_poisson' + omega = np.sqrt(170) # source + eta = -omega**2 - # if quick_run: - # domain_name = 'curved_L_shape' - # nc = 4 - # deg = 2 - # else: - # nc = 8 - # deg = 4 + source_type = 'manu_poisson_elliptic' domain_name = 'pretzel_f' - # domain_name = 'curved_L_shape' + nc = 10 deg = 2 - # nc = 2 - # deg = 2 - run_dir = '{}_{}_nc={}_deg={}/'.format(domain_name, source_type, nc, deg) solve_h1_source_pbm( nc=nc, deg=deg, eta=eta, - mu=1, #1, + mu=1, # 1, domain_name=domain_name, source_type=source_type, backend_language='pyccel-gcc', - plot_source=True, - plot_dir='./plots/h1_tests_source_february/'+run_dir, - hide_plots=True, - ) - - time_count(t_stamp_full, msg='full program') + plot_dir='./plots/h1_source_pbms_conga_2d/' + run_dir, + ) \ No newline at end of file diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py index 759efce0e..588775e8b 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -1,57 +1,83 @@ +""" + Solve the eigenvalue problem for the curl-curl operator in 2D with a FEEC discretization +""" +import os from mpi4py import MPI -import os import numpy as np import matplotlib.pyplot as plt from collections import OrderedDict +from sympde.topology import Derham -from scipy.sparse.linalg import spilu, lgmres -from scipy.sparse.linalg import LinearOperator, eigsh, minres -from scipy.linalg import norm - -from sympde.topology import Derham - -from psydac.feec.multipatch.api import discretize -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.api import discretize +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.feec.multipatch.operators import HodgeOperator from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.plotting_utilities import plot_field -from psydac.feec.multipatch.utilities import time_count +from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file -def hcurl_solve_eigen_pbm(nc=4, deg=4, domain_name='pretzel_f', backend_language='python', mu=1, nu=1, gamma_h=10, - sigma=None, nb_eigs=4, nb_eigs_plot=4, - plot_dir=None, hide_plots=True, m_load_dir="",): - """ - solver for the eigenvalue problem: find lambda in R and u in H0(curl), such that - - A u = lambda * u on \Omega - - with an operator +from sympde.topology import Square +from sympde.topology import IdentityMapping, PolarMapping +from psydac.fem.vector import ProductFemSpace - A u := mu * curl curl u - nu * grad div u - - discretized as Ah: V1h -> V1h with a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \Omega, +from scipy.sparse.linalg import spilu, lgmres +from scipy.sparse.linalg import LinearOperator, eigsh, minres +from scipy.sparse import csr_matrix +from scipy.linalg import norm - V0h --grad-> V1h -—curl-> V2h +from psydac.linalg.utilities import array_to_psydac +from psydac.fem.basic import FemField - Examples: +from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain +from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection - - curl-curl eigenvalue problem with - mu = 1 - nu = 0 +from psydac.api.postprocessing import OutputManager, PostProcessManager - - Hodge-Laplacian eigenvalue problem with - mu = 1 - nu = 1 - :param nc: nb of cells per dimension, in each patch - :param deg: coordinate degree in each patch - :param gamma_h: jump penalization parameter +def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), domain=([0, np.pi], [0, np.pi]), domain_name='refined_square', backend_language='pyccel-gcc', mu=1, nu=0, gamma_h=0, + generalized_pbm=False, sigma=5, nb_eigs_solve=8, nb_eigs_plot=5, skip_eigs_threshold=1e-7, + plot_dir=None, m_load_dir=None,): """ + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization + + Parameters + ---------- + ncells : array + Number of cells in each direction + degree : tuple + Degree of the basis functions + domain : list + Interval in x- and y-direction + domain_name : str + Name of the domain + backend_language : str + Language used for the backend + mu : float + Coefficient in the curl-curl operator + nu : float + Coefficient in the curl-curl operator + gamma_h : float + Coefficient in the curl-curl operator + generalized_pbm : bool + If True, solve the generalized eigenvalue problem + sigma : float + Calculate eigenvalues close to sigma + nb_eigs_solve : int + Number of eigenvalues to solve + nb_eigs_plot : int + Number of eigenvalues to plot + skip_eigs_threshold : float + Threshold for the eigenvalues to skip + plot_dir : str + Directory for the plots + m_load_dir : str + Directory to save and load the matrices + """ + + diags = {} - ncells = [nc, nc] - degree = [deg,deg] if sigma is None: raise ValueError('please specify a value for sigma') @@ -62,16 +88,47 @@ def hcurl_solve_eigen_pbm(nc=4, deg=4, domain_name='pretzel_f', backend_language print(' domain_name = {}'.format(domain_name)) print(' backend_language = {}'.format(backend_language)) print('---------------------------------------------------------------------------------------------------------') - + t_stamp = time_count() print('building symbolic and discrete domain...') - domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) for P in domain.interior]) + + int_x, int_y = domain + if isinstance(ncells, int): + domain = build_multipatch_domain(domain_name=domain_name) + + elif domain_name == 'refined_square' or domain_name == 'square_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='identity') + + elif domain_name == 'curved_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='polar') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(ncells, int): + ncells = [ncells, ncells] + elif ncells.ndim == 1: + ncells = {patch.name: [ncells[i], ncells[i]] + for (i, patch) in enumerate(domain.interior)} + elif ncells.ndim == 2: + ncells = {patch.name: [ncells[int(patch.name[2])][int(patch.name[4])], + ncells[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) mappings_list = list(mappings.values()) - domain_h = discretize(domain, ncells=ncells) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + domain_h = discretize(domain, ncells=ncells) # Vh space print('building symbolic and discrete derham sequences...') - derham = Derham(domain, ["H1", "Hcurl", "L2"]) - derham_h = discretize(derham, domain_h, degree=degree, backend=PSYDAC_BACKENDS[backend_language]) + t_stamp = time_count() + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + t_stamp = time_count(t_stamp) + print(' .. discrete derham sequence...') + derham_h = discretize(derham, domain_h, degree=degree) V0h = derham_h.V0 V1h = derham_h.V1 @@ -79,109 +136,221 @@ def hcurl_solve_eigen_pbm(nc=4, deg=4, domain_name='pretzel_f', backend_language print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) + diags['ndofs_V0'] = V0h.nbasis + diags['ndofs_V1'] = V1h.nbasis + diags['ndofs_V2'] = V2h.nbasis + t_stamp = time_count(t_stamp) print('building the discrete operators:') print('commuting projection operators...') - nquads = [4*(d + 1) for d in degree] - P0, P1, P2 = derham_h.projectors(nquads=nquads) I1 = IdLinearOperator(V1h) I1_m = I1.to_sparse_matrix() + t_stamp = time_count(t_stamp) print('Hodge operators...') # multi-patch (broken) linear operators / matrices - H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=0) - H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=1) - H2 = HodgeOperator(V2h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=2) - - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = mass matrix of V0 - H0_m = H0.to_sparse_matrix() # = inverse mass matrix of V0 - dH1_m = H1.get_dual_Hodge_sparse_matrix() # = mass matrix of V1 - H1_m = H1.to_sparse_matrix() # = inverse mass matrix of V1 - dH2_m = H2.get_dual_Hodge_sparse_matrix() # = mass matrix of V2 - + H0 = HodgeOperator( + V0h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=0) + H1 = HodgeOperator( + V1h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=1) + H2 = HodgeOperator( + V2h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=2) + + H0_m = H0.to_sparse_matrix() # = mass matrix of V0 + dH0_m = H0.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V0 + H1_m = H1.to_sparse_matrix() # = mass matrix of V1 + dH1_m = H1.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V1 + H2_m = H2.to_sparse_matrix() # = mass matrix of V2 + dH2_m = H2.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V2 + + t_stamp = time_count(t_stamp) print('conforming projection operators...') - # conforming Projections (should take into account the boundary conditions of the continuous deRham sequence) - cP0 = derham_h.conforming_projection(space='V0', hom_bc=True, backend_language=backend_language, load_dir=m_load_dir) - cP1 = derham_h.conforming_projection(space='V1', hom_bc=True, backend_language=backend_language, load_dir=m_load_dir) - cP0_m = cP0.to_sparse_matrix() - cP1_m = cP1.to_sparse_matrix() + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) + cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) + t_stamp = time_count(t_stamp) print('broken differential operators...') bD0, bD1 = derham_h.broken_derivatives_as_operators bD0_m = bD0.to_sparse_matrix() bD1_m = bD1.to_sparse_matrix() + t_stamp = time_count(t_stamp) + print('converting some matrices to csr format...') + + H1_m = H1_m.tocsr() + dH1_m = dH1_m.tocsr() + H2_m = H2_m.tocsr() + bD1_m = bD1_m.tocsr() + if not os.path.exists(plot_dir): os.makedirs(plot_dir) - # Conga (projection-based) stiffness matrices - # curl curl: - print('curl-curl stiffness matrix...') - pre_CC_m = bD1_m.transpose() @ dH2_m @ bD1_m - CC_m = cP1_m.transpose() @ pre_CC_m @ cP1_m # Conga stiffness matrix + print('computing the full operator matrix...') + A_m = np.zeros_like(H1_m) - # grad div: - print('grad-div stiffness matrix...') - pre_GD_m = - dH1_m @ bD0_m @ cP0_m @ H0_m @ cP0_m.transpose() @ bD0_m.transpose() @ dH1_m - GD_m = cP1_m.transpose() @ pre_GD_m @ cP1_m # Conga stiffness matrix + # Conga (projection-based) stiffness matrices + if mu != 0: + # curl curl: + t_stamp = time_count(t_stamp) + print('mu = {}'.format(mu)) + print('curl-curl stiffness matrix...') + + pre_CC_m = bD1_m.transpose() @ H2_m @ bD1_m + CC_m = cP1_m.transpose() @ pre_CC_m @ cP1_m # Conga stiffness matrix + A_m += mu * CC_m + + if nu != 0: + pre_GD_m = - H1_m @ bD0_m @ cP0_m @ dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m + GD_m = cP1_m.transpose() @ pre_GD_m @ cP1_m # Conga stiffness matrix + A_m -= nu * GD_m + + # jump stabilization in V1h: + if gamma_h != 0 or generalized_pbm: + t_stamp = time_count(t_stamp) + print('jump stabilization matrix...') + jump_stab_m = I1_m - cP1_m + JS_m = jump_stab_m.transpose() @ H1_m @ jump_stab_m + A_m += gamma_h * JS_m + + if generalized_pbm: + print('adding jump stabilization to RHS of generalized eigenproblem...') + B_m = cP1_m.transpose() @ H1_m @ cP1_m + JS_m + else: + B_m = H1_m + + t_stamp = time_count(t_stamp) + print('solving matrix eigenproblem...') + all_eigenvalues, all_eigenvectors_transp = get_eigenvalues( + nb_eigs_solve, sigma, A_m, B_m) + # Eigenvalue processing + t_stamp = time_count(t_stamp) + print('sorting out eigenvalues...') + zero_eigenvalues = [] + if skip_eigs_threshold is not None: + eigenvalues = [] + eigenvectors = [] + for val, vect in zip(all_eigenvalues, all_eigenvectors_transp.T): + if abs(val) < skip_eigs_threshold: + zero_eigenvalues.append(val) + # we skip the eigenvector + else: + eigenvalues.append(val) + eigenvectors.append(vect) + else: + eigenvalues = all_eigenvalues + eigenvectors = all_eigenvectors_transp.T - # jump penalization in V1h: - jump_penal_m = I1_m - cP1_m - JP_m = jump_penal_m.transpose() * dH1_m * jump_penal_m + for k, val in enumerate(eigenvalues): + diags['eigenvalue_{}'.format(k)] = val # eigenvalues[k] - print('computing the full operator matrix...') - print('mu = {}'.format(mu)) - print('nu = {}'.format(nu)) - A_m = mu * CC_m - nu * GD_m + gamma_h * JP_m + for k, val in enumerate(zero_eigenvalues): + diags['skipped eigenvalue_{}'.format(k)] = val - eigenvalues, eigenvectors = get_eigenvalues(nb_eigs, sigma, A_m, dH1_m) + t_stamp = time_count(t_stamp) + print('plotting the eigenmodes...') - # plot first eigenvalues + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V1h=V1h) + OM.export_space_info() + nb_eigs = len(eigenvalues) for i in range(min(nb_eigs_plot, nb_eigs)): print('looking at emode i = {}... '.format(i)) - lambda_i = eigenvalues[i] - emode_i = np.real(eigenvectors[:,i]) - norm_emode_i = np.dot(emode_i,dH1_m.dot(emode_i)) - print('norm of computed eigenmode: ', norm_emode_i) - eh_c = emode_i/norm_emode_i # numpy coeffs of the normalized eigenmode - plot_field(numpy_coeffs=eh_c, Vh=V1h, space_kind='hcurl', domain=domain, title='mode e_{}, lambda_{}={}'.format(i,i,lambda_i), - filename=plot_dir+'e_{}.png'.format(i), hide_plot=hide_plots) + lambda_i = eigenvalues[i] + emode_i = np.real(eigenvectors[i]) + norm_emode_i = np.dot(emode_i, H1_m.dot(emode_i)) + eh_c = emode_i / norm_emode_i - return eigenvalues, eigenvectors + stencil_coeffs = array_to_psydac(cP1_m @ eh_c, V1h.vector_space) + vh = FemField(V1h, coeffs=stencil_coeffs) + OM.add_snapshot(i, i) + OM.export_fields(vh=vh) + + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/eigenvalues", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.close() + + t_stamp = time_count(t_stamp) + + return diags, eigenvalues def get_eigenvalues(nb_eigs, sigma, A_m, M_m): + """ + Compute the eigenvalues of the matrix A close to sigma and right-hand-side M + + Parameters + ---------- + nb_eigs : int + Number of eigenvalues to compute + sigma : float + Value close to which the eigenvalues are computed + A_m : sparse matrix + Matrix A + M_m : sparse matrix + Matrix M + """ + print('----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ') - print('computing {0} eigenvalues (and eigenvectors) close to sigma={1} with scipy.sparse.eigsh...'.format(nb_eigs, sigma) ) + print( + 'computing {0} eigenvalues (and eigenvectors) close to sigma={1} with scipy.sparse.eigsh...'.format( + nb_eigs, + sigma)) mode = 'normal' which = 'LM' # from eigsh docstring: # ncv = number of Lanczos vectors generated ncv must be greater than k and smaller than n; # it is recommended that ncv > 2*k. Default: min(n, max(2*k + 1, 20)) - ncv = 4*nb_eigs + ncv = 4 * nb_eigs print('A_m.shape = ', A_m.shape) try_lgmres = True - max_shape_splu = 17000 + max_shape_splu = 24000 # OK for nc=20, deg=6 on pretzel_f if A_m.shape[0] < max_shape_splu: print('(via sparse LU decomposition)') OPinv = None tol_eigsh = 0 else: - OP_m = A_m - sigma*M_m + OP_m = A_m - sigma * M_m tol_eigsh = 1e-7 if try_lgmres: - print('(via SPILU-preconditioned LGMRES iterative solver for A_m - sigma*M1_m)') + print( + '(via SPILU-preconditioned LGMRES iterative solver for A_m - sigma*M1_m)') OP_spilu = spilu(OP_m, fill_factor=15, drop_tol=5e-5) - preconditioner = LinearOperator(OP_m.shape, lambda x: OP_spilu.solve(x) ) + preconditioner = LinearOperator( + OP_m.shape, lambda x: OP_spilu.solve(x)) tol = tol_eigsh OPinv = LinearOperator( matvec=lambda v: lgmres(OP_m, v, x0=None, tol=tol, atol=tol, M=preconditioner, - callback=lambda x: print('cg -- residual = ', norm(OP_m.dot(x)-v)) - )[0], + callback=lambda x: print( + 'cg -- residual = ', norm(OP_m.dot(x) - v)) + )[0], shape=M_m.shape, dtype=M_m.dtype ) @@ -192,45 +361,16 @@ def get_eigenvalues(nb_eigs, sigma, A_m, M_m): # > here, minres: MINimum RESidual iteration to solve Ax=b # suggested in https://github.com/scipy/scipy/issues/4170 print('(with minres iterative solver for A_m - sigma*M1_m)') - OPinv = LinearOperator(matvec=lambda v: minres(OP_m, v, tol=1e-10)[0], shape=M_m.shape, dtype=M_m.dtype) + OPinv = LinearOperator( + matvec=lambda v: minres( + OP_m, + v, + tol=1e-10)[0], + shape=M_m.shape, + dtype=M_m.dtype) - eigenvalues, eigenvectors = eigsh(A_m, k=nb_eigs, M=M_m, sigma=sigma, mode=mode, which=which, ncv=ncv, tol=tol_eigsh, OPinv=OPinv) + eigenvalues, eigenvectors = eigsh( + A_m, k=nb_eigs, M=M_m, sigma=sigma, mode=mode, which=which, ncv=ncv, tol=tol_eigsh, OPinv=OPinv) print("done: eigenvalues found: " + repr(eigenvalues)) return eigenvalues, eigenvectors - -if __name__ == '__main__': - - t_stamp_full = time_count() - - quick_run = True - # quick_run = False - - if quick_run: - domain_name = 'curved_L_shape' - nc = 4 - deg = 2 - else: - nc = 8 - deg = 4 - - domain_name = 'pretzel_f' - # domain_name = 'curved_L_shape' - nc = 10 - deg = 2 - - m_load_dir = 'matrices_{}_nc={}_deg={}/'.format(domain_name, nc, deg) - run_dir = 'eigenpbm_{}_nc={}_deg={}/'.format(domain_name, nc, deg) - hcurl_solve_eigen_pbm( - nc=nc, deg=deg, - nu=0, - mu=1, #1, - sigma=1, - domain_name=domain_name, - backend_language='pyccel-gcc', - plot_dir='./plots/tests_source_february/'+run_dir, - hide_plots=True, - m_load_dir=m_load_dir, - ) - - time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py new file mode 100644 index 000000000..50ec032fa --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py @@ -0,0 +1,309 @@ +""" + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization, following + A. Buffa and I. Perugia, “Discontinuous Galerkin Approximation of the Maxwell Eigenproblem” + SIAM Journal on Numerical Analysis 44 (2006) +""" + +import os +from mpi4py import MPI +from collections import OrderedDict + +import numpy as np +import matplotlib.pyplot +from scipy.sparse.linalg import spsolve, inv +from scipy.sparse.linalg import LinearOperator, eigsh, minres + +from sympde.calculus import grad, dot, curl, cross +from sympde.calculus import minus, plus +from sympde.topology import VectorFunctionSpace +from sympde.topology import elements_of +from sympde.topology import NormalVector +from sympde.topology import Square +from sympde.topology import IdentityMapping, PolarMapping +from sympde.expr.expr import LinearForm, BilinearForm +from sympde.expr.expr import integral +from sympde.expr.expr import Norm +from sympde.expr.equation import find, EssentialBC + +from psydac.linalg.utilities import array_to_psydac +from psydac.api.tests.build_domain import build_pretzel +from psydac.fem.basic import FemField +from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL +from psydac.feec.pull_push import pull_2d_hcurl + +from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain +from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain +from psydac.api.postprocessing import OutputManager, PostProcessManager + + +def hcurl_solve_eigen_pbm_dg(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), domain=([0, np.pi], [0, np.pi]), domain_name='refined_square', backend_language='pyccel-gcc', mu=1, nu=0, + sigma=5, nb_eigs_solve=8, nb_eigs_plot=5, skip_eigs_threshold=1e-7, + plot_dir=None,): + """ + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization + + Parameters + ---------- + ncells : array + Number of cells in each direction + degree : tuple + Degree of the basis functions + domain : list + Interval in x- and y-direction + domain_name : str + Name of the domain + backend_language : str + Language used for the backend + mu : float + Coefficient in the curl-curl operator + nu : float + Coefficient in the curl-curl operator + sigma : float + Calculate eigenvalues close to sigma + nb_eigs_solve : int + Number of eigenvalues to solve + nb_eigs_plot : int + Number of eigenvalues to plot + skip_eigs_threshold : float + Threshold for the eigenvalues to skip + plot_dir : str + Directory for the plots + """ + + diags = {} + + if sigma is None: + raise ValueError('please specify a value for sigma') + + print('---------------------------------------------------------------------------------------------------------') + print('Starting hcurl_solve_eigen_pbm function with: ') + print(' ncells = {}'.format(ncells)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' backend_language = {}'.format(backend_language)) + print('---------------------------------------------------------------------------------------------------------') + t_stamp = time_count() + print('building symbolic and discrete domain...') + + int_x, int_y = domain + if isinstance(ncells, int): + domain = build_multipatch_domain(domain_name=domain_name) + + elif domain_name == 'refined_square' or domain_name == 'square_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='identity') + + elif domain_name == 'curved_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='polar') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(ncells, int): + ncells = [ncells, ncells] + elif ncells.ndim == 1: + ncells = {patch.name: [ncells[i], ncells[i]] + for (i, patch) in enumerate(domain.interior)} + elif ncells.ndim == 2: + ncells = {patch.name: [ncells[int(patch.name[2])][int(patch.name[4])], + ncells[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) + mappings_list = list(mappings.values()) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + + V = VectorFunctionSpace('V', domain, kind='hcurl') + + u, v, F = elements_of(V, names='u, v, F') + nn = NormalVector('nn') + + I = domain.interfaces + boundary = domain.boundary + + kappa = 10 + k = 1 + + def jump(w): return plus(w) - minus(w) + def avr(w): return 0.5 * plus(w) + 0.5 * minus(w) + + expr1_I = cross(nn, jump(v)) * curl(avr(u))\ + + k * cross(nn, jump(u)) * curl(avr(v))\ + + kappa * cross(nn, jump(u)) * cross(nn, jump(v)) + + expr1 = curl(u) * curl(v) + expr1_b = -cross(nn, v) * curl(u) - k * cross(nn, u) * \ + curl(v) + kappa * cross(nn, u) * cross(nn, v) + # curl curl u = - omega**2 u + + expr2 = dot(u, v) + # expr2_I = kappa*cross(nn, jump(u))*cross(nn, jump(v)) + # expr2_b = -k*cross(nn, u)*curl(v) + kappa * cross(nn, u) * cross(nn, v) + + # Bilinear form a: V x V --> R + a = BilinearForm((u, v), integral(domain, expr1) + + integral(I, expr1_I) + integral(boundary, expr1_b)) + + # Linear form l: V --> R + # + integral(I, expr2_I) + integral(boundary, expr2_b)) + b = BilinearForm((u, v), integral(domain, expr2)) + + # +++++++++++++++++++++++++++++++ + # 2. Discretization + # +++++++++++++++++++++++++++++++ + + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + + ah = discretize(a, domain_h, [Vh, Vh]) + Ah_m = ah.assemble().tosparse() + + bh = discretize(b, domain_h, [Vh, Vh]) + Bh_m = bh.assemble().tosparse() + + all_eigenvalues_2, all_eigenvectors_transp_2 = get_eigenvalues( + nb_eigs_solve, sigma, Ah_m, Bh_m) + + # Eigenvalue processing + t_stamp = time_count(t_stamp) + print('sorting out eigenvalues...') + zero_eigenvalues = [] + if skip_eigs_threshold is not None: + eigenvalues = [] + eigenvectors = [] + for val, vect in zip(all_eigenvalues_2, all_eigenvectors_transp_2.T): + if abs(val) < skip_eigs_threshold: + zero_eigenvalues.append(val) + # we skip the eigenvector + else: + eigenvalues.append(val) + eigenvectors.append(vect) + else: + eigenvalues = all_eigenvalues_2 + eigenvectors = all_eigenvectors_transp_2.T + diags['DG'] = True + for k, val in enumerate(eigenvalues): + diags['eigenvalue2_{}'.format(k)] = val # eigenvalues[k] + + for k, val in enumerate(zero_eigenvalues): + diags['skipped eigenvalue2_{}'.format(k)] = val + + t_stamp = time_count(t_stamp) + print('plotting the eigenmodes...') + + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(Vh=Vh) + OM.export_space_info() + + nb_eigs = len(eigenvalues) + for i in range(min(nb_eigs_plot, nb_eigs)): + + print('looking at emode i = {}... '.format(i)) + lambda_i = eigenvalues[i] + emode_i = np.real(eigenvectors[i]) + norm_emode_i = np.dot(emode_i, Bh_m.dot(emode_i)) + eh_c = emode_i / norm_emode_i + + stencil_coeffs = array_to_psydac(eh_c, Vh.vector_space) + vh = FemField(Vh, coeffs=stencil_coeffs) + OM.add_snapshot(i, i) + OM.export_fields(vh=vh) + + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/eigenvalues", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.close() + + t_stamp = time_count(t_stamp) + + return diags, eigenvalues + + +def get_eigenvalues(nb_eigs, sigma, A_m, M_m): + """ + Compute the eigenvalues of the matrix A close to sigma and right-hand-side M + + Parameters + ---------- + nb_eigs : int + Number of eigenvalues to compute + sigma : float + Value close to which the eigenvalues are computed + A_m : sparse matrix + Matrix A + M_m : sparse matrix + Matrix M + """ + + print('----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ') + print( + 'computing {0} eigenvalues (and eigenvectors) close to sigma={1} with scipy.sparse.eigsh...'.format( + nb_eigs, + sigma)) + mode = 'normal' + which = 'LM' + # from eigsh docstring: + # ncv = number of Lanczos vectors generated ncv must be greater than k and smaller than n; + # it is recommended that ncv > 2*k. Default: min(n, max(2*k + 1, 20)) + ncv = 4 * nb_eigs + print('A_m.shape = ', A_m.shape) + try_lgmres = True + max_shape_splu = 24000 # OK for nc=20, deg=6 on pretzel_f + if A_m.shape[0] < max_shape_splu: + print('(via sparse LU decomposition)') + OPinv = None + tol_eigsh = 0 + else: + + OP_m = A_m - sigma * M_m + tol_eigsh = 1e-7 + if try_lgmres: + print( + '(via SPILU-preconditioned LGMRES iterative solver for A_m - sigma*M1_m)') + OP_spilu = spilu(OP_m, fill_factor=15, drop_tol=5e-5) + preconditioner = LinearOperator( + OP_m.shape, lambda x: OP_spilu.solve(x)) + tol = tol_eigsh + OPinv = LinearOperator( + matvec=lambda v: lgmres(OP_m, v, x0=None, tol=tol, atol=tol, M=preconditioner, + callback=lambda x: print( + 'cg -- residual = ', norm(OP_m.dot(x) - v)) + )[0], + shape=M_m.shape, + dtype=M_m.dtype + ) + + else: + # from https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.eigsh.html: + # the user can supply the matrix or operator OPinv, which gives x = OPinv @ b = [A - sigma * M]^-1 @ b. + # > here, minres: MINimum RESidual iteration to solve Ax=b + # suggested in https://github.com/scipy/scipy/issues/4170 + print('(with minres iterative solver for A_m - sigma*M1_m)') + OPinv = LinearOperator( + matvec=lambda v: minres( + OP_m, + v, + tol=1e-10)[0], + shape=M_m.shape, + dtype=M_m.dtype) + + eigenvalues, eigenvectors = eigsh( + A_m, k=nb_eigs, M=M_m, sigma=sigma, mode=mode, which=which, ncv=ncv, tol=tol_eigsh, OPinv=OPinv) + + print("done: eigenvalues found: " + repr(eigenvalues)) + return eigenvalues, eigenvectors diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py new file mode 100644 index 000000000..4f311a7eb --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py @@ -0,0 +1,294 @@ +""" + Runner script for solving the eigenvalue problem for the H(curl) operator for different discretizations. +""" + +import os +import numpy as np + +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_conga_2d import hcurl_solve_eigen_pbm +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_dg_2d import hcurl_solve_eigen_pbm_dg +from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file +from psydac.api.postprocessing import OutputManager, PostProcessManager + +t_stamp_full = time_count() + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# +# test-case and numerical parameters: +method = 'feec' +# method = 'dg' + +operator = 'curl-curl' +degree = [3, 3] # shared across all patches + +# pretzel_f (18 patches) +# domain_name = 'pretzel_f' +# ncells = np.array([8, 8, 16, 16, 8, 4, 4, 4, 4, 4, 2, 2, 4, 16, 16, 8, 2, 2, 2]) +# ncells = np.array([4 for _ in range(18)]) + +# domain onlyneeded for square like domains +# domain = [[0, np.pi], [0, np.pi]] # interval in x- and y-direction + +# refined square domain +# domain_name = 'refined_square' +# the shape of ncells gives the shape of the domain, +# while the entries describe the isometric number of cells in each patch +# 2x2 = 4 patches +# ncells = np.array([[8, 4], +# [4, 4]]) +# 3x3= 9 patches +# ncells = np.array([[4, 2, 4], +# [2, 4, 2], +# [4, 2, 4]]) + +# L-shaped domain +# domain_name = 'square_L_shape' +# domain=[[-1, 1],[-1, 1]] # interval in x- and y-direction + +# The None indicates the patches to leave out +# 2x2 = 4 patches +# ncells = np.array([[None, 2], +# [2, 2]]) +# 4x4 = 16 patches +# ncells = np.array([[None, None, 4, 2], +# [None, None, 8, 4], +# [4, 8, 8, 4], +# [2, 4, 4, 2]]) +# 8x8 = 64 patches +# ncells = np.array([[None, None, None, None, 2, 2, 2,1 2], +# [None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 4, 4, 2, 2], +# [2, 2, 2, 4, 8, 4, 2, 2], +# [2, 2, 2, 4, 4, 4, 2, 2], +# [2, 2, 2, 2, 2, 2, 2, 2], +# [2, 2, 2, 2, 2, 2, 2, 2]]) + +# Curved L-shape domain +domain_name = 'curved_L_shape' +domain = [[1, 3], [0, np.pi / 4]] # interval in x- and y-direction + + +ncells = np.array([[None, 5], + [5, 10]]) +# ncells = 5 + +# ncells = np.array([[None, None, 2, 2], +# [None, None, 4, 2], +# [ 2, 4, 8, 4], +# [ 2, 2, 4, 4]]) + +# ncells = np.array([[None, None, None, 2, 2, 2], +# [None, None, None, 4, 4, 2], +# [None, None, None, 8, 4, 2], +# [2, 4, 8, 8, 4, 2], +# [2, 4, 4, 4, 4, 2], +# [2, 2, 2, 2, 2, 2]]) + +# ncells = np.array([[None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 4, 4, 4, 2], +# [None, None, None, None, 8, 8, 4, 2], +# [None, None, None, None, 16, 8, 4, 2], +# [2, 4, 8, 16, 16, 8, 4, 2], +# [2, 4, 8, 8, 8, 8, 4, 2], +# [2, 4, 4, 4, 4, 4, 4, 2], +# [2, 2, 2, 2, 2, 2, 2, 2]]) + +# all kinds of different square refinements and constructions are possible, eg +# doubly connected domains +# ncells = np.array([[4, 2, 2, 4], +# [2, None, None, 2], +# [2, None, None, 2], +# [4, 2, 2, 4]]) + +gamma_h = 0 +# solves generalized eigenvalue problem with: B(v,w) = + +# <(I-P)v,(I-P)w> in rhs +generalized_pbm = True + +if operator == 'curl-curl': + nu = 0 + mu = 1 +else: + raise ValueError(operator) + +case_dir = 'eigenpbm_' + operator + '_' + method +ref_case_dir = case_dir + +ref_sigmas = None +sigma = None +nb_eigs_solve = None +nb_eigs_plot = None +skip_eigs_threshold = None +diags = None +eigenvalues = None + +if domain_name == 'refined_square': + assert domain == [[0, np.pi], [0, np.pi]] + ref_sigmas = [ + 1, 1, + 2, + 4, 4, + 5, 5, + 8, + 9, 9, + ] + sigma = 5 + nb_eigs_solve = 10 + nb_eigs_plot = 10 + skip_eigs_threshold = 1e-7 + +elif domain_name == 'square_L_shape': + assert domain == [[-1, 1], [-1, 1]] + ref_sigmas = [ + 1.47562182408, + 3.53403136678, + 9.86960440109, + 9.86960440109, + 11.3894793979, + ] + sigma = 6 + nb_eigs_solve = 5 + nb_eigs_plot = 5 + skip_eigs_threshold = 1e-7 + +elif domain_name == 'curved_L_shape': + # ref eigenvalues from Monique Dauge benchmark page + assert domain == [[1, 3], [0, np.pi / 4]] + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + +elif domain_name in ['pretzel_f']: + if operator == 'curl-curl': + # ref sigmas computed with nc=20 and deg=6 and gamma = 0 (and + # generalized ev-pbm) + ref_sigmas = [ + 0.1795339843, + 0.1992261261, + 0.6992717244, + 0.8709410438, + 1.1945106937, + 1.2546992683, + ] + + sigma = .8 + nb_eigs_solve = 10 + nb_eigs_plot = 5 + skip_eigs_threshold = 1e-7 + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +common_diag_filename = './' + case_dir + '_diags.txt' + + +params = { + 'domain_name': domain_name, + 'domain': domain, + 'operator': operator, + 'mu': mu, + 'nu': nu, + 'ncells': ncells, + 'degree': degree, + 'gamma_h': gamma_h, + 'generalized_pbm': generalized_pbm, + 'nb_eigs_solve': nb_eigs_solve, + 'skip_eigs_threshold': skip_eigs_threshold +} + +print(params) + +# backend_language = 'numba' +backend_language = 'pyccel-gcc' + +dims = 1 if isinstance(ncells, int) else ncells.shape +sz = 1 if isinstance(ncells, int) else ncells[ncells != None].sum() + +# get_run_dir(domain_name, nc, deg) +run_dir = domain_name + str(dims) + 'patches_' + 'size_{}'.format(sz) +plot_dir = get_plot_dir(case_dir, run_dir) +diag_filename = plot_dir + '/' + diag_fn() +common_diag_filename = './' + case_dir + '_diags.txt' + +# to save and load matrices +# m_load_dir = get_mat_dir(domain_name, nc, deg) +m_load_dir = None + +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') +print(' Calling hcurl_solve_eigen_pbm() with params = {}'.format(params)) +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# calling eigenpbm solver for: +# +# find lambda in R and u in H0(curl), such that +# A u = lambda * u on \Omega +# with +# +# A u := mu * curl curl u - nu * grad div u +# +# note: +# - we look for nb_eigs_solve eigenvalues close to sigma (skip zero eigenvalues if skip_zero_eigs==True) +# - we plot nb_eigs_plot eigenvectors +if method == 'feec': + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=gamma_h, + generalized_pbm=generalized_pbm, + nu=nu, + mu=mu, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language=backend_language, + plot_dir=plot_dir, + m_load_dir=m_load_dir, + ) + +elif method == 'dg': + diags, eigenvalues = hcurl_solve_eigen_pbm_dg( + ncells=ncells, degree=degree, + nu=nu, + mu=mu, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language=backend_language, + plot_dir=plot_dir, + ) + +if ref_sigmas is not None: + errors = [] + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + diags['error_{}'.format(k)] = abs(eigenvalues[k] - ref_sigmas[k]) +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=diag_filename, + params=params) +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=common_diag_filename, + params=params) + +# PM = PostProcessManager(geometry_file=, ) +time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py index 5b769a098..bbff3b839 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -1,8 +1,20 @@ -# coding: utf-8 +""" + solver for the problem: find u in H(curl), such that -from mpi4py import MPI + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega + + where the operator + + A u := eta * u + mu * curl curl u - nu * grad div u + + is discretized as Ah: V1h -> V1h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h +""" import os +from mpi4py import MPI import numpy as np from collections import OrderedDict @@ -10,42 +22,46 @@ from scipy.sparse.linalg import spsolve -from sympde.calculus import dot -from sympde.topology import element_of +from sympde.calculus import dot +from sympde.topology import element_of from sympde.expr.expr import LinearForm from sympde.expr.expr import integral, Norm -from sympde.topology import Derham +from sympde.topology import Derham -from psydac.api.settings import PSYDAC_BACKENDS +from psydac.api.settings import PSYDAC_BACKENDS from psydac.feec.pull_push import pull_2d_hcurl -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.plotting_utilities import plot_field from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution -from psydac.feec.multipatch.utilities import time_count -from psydac.linalg.utilities import array_to_psydac -from psydac.fem.basic import FemField +from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_hcurl +from psydac.feec.multipatch.utils_conga_2d import DiagGrid, P0_phys, P1_phys, P2_phys, get_Vh_diags_for +from psydac.feec.multipatch.utilities import time_count +from psydac.linalg.utilities import array_to_psydac +from psydac.fem.basic import FemField +from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection +from psydac.api.postprocessing import OutputManager, PostProcessManager + def solve_hcurl_source_pbm( nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='P_geom', source_type='manu_J', eta=-10., mu=1., nu=1., gamma_h=10., - plot_source=False, plot_dir=None, hide_plots=True, + project_sol=False, plot_dir=None, m_load_dir=None, ): """ solver for the problem: find u in H(curl), such that - A u = f on \Omega - n x u = n x u_bc on \partial \Omega + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega where the operator A u := eta * u + mu * curl curl u - nu * grad div u - is discretized as Ah: V1h -> V1h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \Omega, + is discretized as Ah: V1h -> V1h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, V0h --grad-> V1h -—curl-> V2h @@ -64,108 +80,132 @@ def solve_hcurl_source_pbm( :param nc: nb of cells per dimension, in each patch :param deg: coordinate degree in each patch :param gamma_h: jump penalization parameter - :param source_proj: approximation operator for the source, possible values are 'P_geom' or 'P_L2' + :param source_proj: approximation operator (in V1h) for the source, possible values are + - 'tilde_Pi': dual commuting projection, an L2 projection filtered by the adjoint conforming projection) :param source_type: must be implemented in get_source_and_solution() :param m_load_dir: directory for matrix storage """ + diags = {} - ncells = [nc, nc] - degree = [deg,deg] + degree = [deg, deg] - # if backend_language is None: - # backend_language='python' - # print('[note: using '+backend_language+ ' backends in discretize functions]') if m_load_dir is not None: if not os.path.exists(m_load_dir): os.makedirs(m_load_dir) print('---------------------------------------------------------------------------------------------------------') print('Starting solve_hcurl_source_pbm function with: ') - print(' ncells = {}'.format(ncells)) + print(' ncells = {}'.format(nc)) print(' degree = {}'.format(degree)) print(' domain_name = {}'.format(domain_name)) print(' source_proj = {}'.format(source_proj)) print(' backend_language = {}'.format(backend_language)) print('---------------------------------------------------------------------------------------------------------') + print() + print(' -- building discrete spaces and operators --') + t_stamp = time_count() - print('building symbolic domain sequence...') + print(' .. multi-patch domain...') domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) for P in domain.interior]) + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) mappings_list = list(mappings.values()) + if isinstance(nc, int): + ncells = [nc, nc] + else: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + + # for diagnosttics + diag_grid = DiagGrid(mappings=mappings, N_diag=100) + t_stamp = time_count(t_stamp) - print('building derham sequence...') - derham = Derham(domain, ["H1", "Hcurl", "L2"]) + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) t_stamp = time_count(t_stamp) - print('building discrete domain...') + print(' .. discrete domain...') domain_h = discretize(domain, ncells=ncells) t_stamp = time_count(t_stamp) - print('building discrete derham sequence...') + print(' .. discrete derham sequence...') derham_h = discretize(derham, domain_h, degree=degree) t_stamp = time_count(t_stamp) - print('building commuting projection operators...') - nquads = [4*(d + 1) for d in degree] + print(' .. commuting projection operators...') + nquads = [4 * (d + 1) for d in degree] P0, P1, P2 = derham_h.projectors(nquads=nquads) - # multi-patch (broken) spaces t_stamp = time_count(t_stamp) - print('calling the multi-patch spaces...') + print(' .. multi-patch spaces...') V0h = derham_h.V0 V1h = derham_h.V1 V2h = derham_h.V2 print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) + diags['ndofs_V0'] = V0h.nbasis + diags['ndofs_V1'] = V1h.nbasis + diags['ndofs_V2'] = V2h.nbasis t_stamp = time_count(t_stamp) - print('building the Id operator and matrix...') + print(' .. Id operator and matrix...') I1 = IdLinearOperator(V1h) I1_m = I1.to_sparse_matrix() t_stamp = time_count(t_stamp) - print('instanciating the Hodge operators...') + print(' .. Hodge operators...') # multi-patch (broken) linear operators / matrices # other option: define as Hodge Operators: - H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=0) - H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=1) - H2 = HodgeOperator(V2h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=2) + H0 = HodgeOperator( + V0h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=0) + H1 = HodgeOperator( + V1h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=1) + H2 = HodgeOperator( + V2h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=2) t_stamp = time_count(t_stamp) - print('building the dual Hodge matrix dH0_m = M0_m ...') - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = mass matrix of V0 - + print(' .. Hodge matrix H0_m = M0_m ...') + H0_m = H0.to_sparse_matrix() t_stamp = time_count(t_stamp) - print('building the primal Hodge matrix H0_m = inv_M0_m ...') - H0_m = H0.to_sparse_matrix() # = inverse mass matrix of V0 + print(' .. dual Hodge matrix dH0_m = inv_M0_m ...') + dH0_m = H0.get_dual_Hodge_sparse_matrix() t_stamp = time_count(t_stamp) - print('building the dual Hodge matrix dH1_m = M1_m ...') - dH1_m = H1.get_dual_Hodge_sparse_matrix() # = mass matrix of V1 - + print(' .. Hodge matrix H1_m = M1_m ...') + H1_m = H1.to_sparse_matrix() t_stamp = time_count(t_stamp) - print('building the primal Hodge matrix H1_m = inv_M1_m ...') - H1_m = H1.to_sparse_matrix() # = inverse mass matrix of V1 - - # print("dH1_m @ H1_m == I1_m: {}".format(np.allclose((dH1_m @ H1_m).todense(), I1_m.todense())) ) # CHECK: OK + print(' .. dual Hodge matrix dH1_m = inv_M1_m ...') + dH1_m = H1.get_dual_Hodge_sparse_matrix() t_stamp = time_count(t_stamp) - print('building the dual Hodge matrix dH2_m = M2_m ...') - dH2_m = H2.get_dual_Hodge_sparse_matrix() # = mass matrix of V2 + print(' .. Hodge matrix H2_m = M2_m ...') + H2_m = H2.to_sparse_matrix() + dH2_m = H2.get_dual_Hodge_sparse_matrix() t_stamp = time_count(t_stamp) - print('building the conforming Projection operators and matrices...') - # conforming Projections (should take into account the boundary conditions of the continuous deRham sequence) - cP0 = derham_h.conforming_projection(space='V0', hom_bc=True, backend_language=backend_language, load_dir=m_load_dir) - cP1 = derham_h.conforming_projection(space='V1', hom_bc=True, backend_language=backend_language, load_dir=m_load_dir) - cP0_m = cP0.to_sparse_matrix() - cP1_m = cP1.to_sparse_matrix() + print(' .. conforming Projection operators...') + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) + cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) t_stamp = time_count(t_stamp) - print('building the broken differential operators and matrices...') + print(' .. broken differential operators...') # broken (patch-wise) differential operators bD0, bD1 = derham_h.broken_derivatives_as_operators bD0_m = bD0.to_sparse_matrix() @@ -177,14 +217,12 @@ def solve_hcurl_source_pbm( def lift_u_bc(u_bc): if u_bc is not None: print('lifting the boundary condition in V1h...') - # note: for simplicity we apply the full P1 on u_bc, but we only need to set the boundary dofs - u_bc_x = lambdify(domain.coordinates, u_bc[0]) - u_bc_y = lambdify(domain.coordinates, u_bc[1]) - u_bc_log = [pull_2d_hcurl([u_bc_x, u_bc_y], m) for m in mappings_list] - # it's a bit weird to apply P1 on the list of (pulled back) logical fields -- why not just apply it on u_bc ? - uh_bc = P1(u_bc_log) + # note: for simplicity we apply the full P1 on u_bc, but we only + # need to set the boundary dofs + uh_bc = P1_phys(u_bc, P1, domain, mappings_list) ubc_c = uh_bc.coeffs.toarray() - # removing internal dofs (otherwise ubc_c may already be a very good approximation of uh_c ...) + # removing internal dofs (otherwise ubc_c may already be a very + # good approximation of uh_c ...) ubc_c = ubc_c - cP1_m.dot(ubc_c) else: ubc_c = None @@ -193,158 +231,130 @@ def lift_u_bc(u_bc): # Conga (projection-based) stiffness matrices # curl curl: t_stamp = time_count(t_stamp) - print('computing the curl-curl stiffness matrix...') - print(bD1_m.shape, dH2_m.shape ) - pre_CC_m = bD1_m.transpose() @ dH2_m @ bD1_m + print(' .. curl-curl stiffness matrix...') + print(bD1_m.shape, H2_m.shape) + pre_CC_m = bD1_m.transpose() @ H2_m @ bD1_m # CC_m = cP1_m.transpose() @ pre_CC_m @ cP1_m # Conga stiffness matrix # grad div: t_stamp = time_count(t_stamp) - print('computing the grad-div stiffness matrix...') - pre_GD_m = - dH1_m @ bD0_m @ cP0_m @ H0_m @ cP0_m.transpose() @ bD0_m.transpose() @ dH1_m + print(' .. grad-div stiffness matrix...') + pre_GD_m = - H1_m @ bD0_m @ cP0_m @ dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m # GD_m = cP1_m.transpose() @ pre_GD_m @ cP1_m # Conga stiffness matrix - # jump penalization: + # jump stabilization: t_stamp = time_count(t_stamp) - print('computing the jump penalization matrix...') + print(' .. jump stabilization matrix...') jump_penal_m = I1_m - cP1_m - JP_m = jump_penal_m.transpose() * dH1_m * jump_penal_m + JP_m = jump_penal_m.transpose() @ H1_m @ jump_penal_m t_stamp = time_count(t_stamp) - print('computing the full operator matrix...') + print(' .. full operator matrix...') print('eta = {}'.format(eta)) print('mu = {}'.format(mu)) print('nu = {}'.format(nu)) - pre_A_m = cP1_m.transpose() @ ( eta * dH1_m + mu * pre_CC_m - nu * pre_GD_m ) # useful for the boundary condition (if present) + print('STABILIZATION: gamma_h = {}'.format(gamma_h)) + # useful for the boundary condition (if present) + pre_A_m = cP1_m.transpose() @ (eta * H1_m + mu * pre_CC_m - nu * pre_GD_m) A_m = pre_A_m @ cP1_m + gamma_h * JP_m - # get exact source, bc's, ref solution... - # (not all the returned functions are useful here) t_stamp = time_count(t_stamp) - print('getting the source and ref solution...') - N_diag = 200 - method = 'conga' - f_scal, f_vect, u_bc, ph_ref, uh_ref, p_ex, u_ex, phi, grad_phi = get_source_and_solution( - source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name, - refsol_params=[N_diag, method, source_proj], - ) + print() + print(' -- getting source --') + + f_vect, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl( + source_type=source_type, eta=eta, mu=mu, domain=domain, domain_name=domain_name,) # compute approximate source f_h t_stamp = time_count(t_stamp) - b_c = f_c = None - if source_proj == 'P_geom': - # f_h = P1-geometric (commuting) projection of f_vect - print('projecting the source with commuting projection...') - f_x = lambdify(domain.coordinates, f_vect[0]) - f_y = lambdify(domain.coordinates, f_vect[1]) - f_log = [pull_2d_hcurl([f_x, f_y], m) for m in mappings_list] - f_h = P1(f_log) - f_c = f_h.coeffs.toarray() - b_c = dH1_m.dot(f_c) - - elif source_proj == 'P_L2': - # f_h = L2 projection of f_vect - print('projecting the source with L2 projection...') - v = element_of(V1h.symbolic_space, name='v') - expr = dot(f_vect,v) - l = LinearForm(v, integral(domain, expr)) - lh = discretize(l, domain_h, V1h, backend=PSYDAC_BACKENDS[backend_language]) - b = lh.assemble() - b_c = b.toarray() - if plot_source: - f_c = H1_m.dot(b_c) - else: - raise ValueError(source_proj) - if plot_source: - plot_field(numpy_coeffs=f_c, Vh=V1h, space_kind='hcurl', domain=domain, title='f_h with P = '+source_proj, filename=plot_dir+'/fh_'+source_proj+'.png', hide_plot=hide_plots) + # f_h = L2 projection of f_vect, with filtering if tilde_Pi + print(' .. projecting the source with ' + + source_proj +' projection...') + + tilde_f_c = derham_h.get_dual_dofs( + space='V1', + f=f_vect, + backend_language=backend_language, + return_format='numpy_array') + if source_proj == 'tilde_Pi': + print(' .. filtering the discrete source with P0.T ...') + tilde_f_c = cP1_m.transpose() @ tilde_f_c - ubc_c = lift_u_bc(u_bc) + ubc_c = lift_u_bc(u_bc) if ubc_c is not None: # modified source for the homogeneous pbm t_stamp = time_count(t_stamp) - print('modifying the source with lifted bc solution...') - b_c = b_c - pre_A_m.dot(ubc_c) + print(' .. modifying the source with lifted bc solution...') + tilde_f_c = tilde_f_c - pre_A_m.dot(ubc_c) # direct solve with scipy spsolve t_stamp = time_count(t_stamp) - print('solving source problem with scipy.spsolve...') - uh_c = spsolve(A_m, b_c) + print() + print(' -- solving source problem with scipy.spsolve...') + uh_c = spsolve(A_m, tilde_f_c) # project the homogeneous solution on the conforming problem space - t_stamp = time_count(t_stamp) - print('projecting the homogeneous solution on the conforming problem space...') - uh_c = cP1_m.dot(uh_c) + if project_sol: + t_stamp = time_count(t_stamp) + print(' .. projecting the homogeneous solution on the conforming problem space...') + uh_c = cP1_m.dot(uh_c) + else: + print(' .. NOT projecting the homogeneous solution on the conforming problem space') if ubc_c is not None: # adding the lifted boundary condition t_stamp = time_count(t_stamp) - print('adding the lifted boundary condition...') + print(' .. adding the lifted boundary condition...') uh_c += ubc_c + uh = FemField(V1h, coeffs=array_to_psydac(uh_c, V1h.vector_space)) + #need cp1 here? + f_c = dH1_m.dot(tilde_f_c) + jh = FemField(V1h, coeffs=array_to_psydac(f_c, V1h.vector_space)) + t_stamp = time_count(t_stamp) - print('getting and plotting the FEM solution from numpy coefs array...') - title = r'solution $u_h$ (amplitude) for $\eta = $'+repr(eta) - params_str = 'eta={}_mu={}_nu={}_gamma_h={}'.format(eta, mu, nu, gamma_h) + print(' -- plots and diagnostics --') if plot_dir: - plot_field(numpy_coeffs=uh_c, Vh=V1h, space_kind='hcurl', domain=domain, title=title, filename=plot_dir+params_str+'_uh.png', hide_plot=hide_plots) + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V1h=V1h) + OM.set_static() + OM.export_fields(vh=uh) + OM.export_fields(jh=jh) + OM.export_space_info() + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + + '/spaces.yml', + fields_file=plot_dir + + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/sol", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.export_to_vtk( + plot_dir + "/source", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='jh') + + PM.close() time_count(t_stamp) if u_ex: - u = element_of(V1h.symbolic_space, name='u') - l2norm = Norm(Matrix([u[0] - u_ex[0],u[1] - u_ex[1]]), domain, kind='l2') - l2norm_h = discretize(l2norm, domain_h, V1h) - uh_c = array_to_psydac(uh_c, V1h.vector_space) - l2_error = l2norm_h.assemble(u=FemField(V1h, coeffs=uh_c)) - return l2_error - -if __name__ == '__main__': - - t_stamp_full = time_count() - - quick_run = True - # quick_run = False - - omega = np.sqrt(170) # source - roundoff = 1e4 - eta = int(-omega**2 * roundoff)/roundoff - - source_type = 'manu_maxwell' - # source_type = 'manu_J' - - if quick_run: - domain_name = 'curved_L_shape' - nc = 4 - deg = 2 - else: - nc = 8 - deg = 4 - - domain_name = 'pretzel_f' - # domain_name = 'curved_L_shape' - nc = 20 - deg = 2 - - # nc = 2 - # deg = 2 - - run_dir = '{}_{}_nc={}_deg={}/'.format(domain_name, source_type, nc, deg) - m_load_dir = 'matrices_{}_nc={}_deg={}/'.format(domain_name, nc, deg) - solve_hcurl_source_pbm( - nc=nc, deg=deg, - eta=eta, - nu=0, - mu=1, #1, - domain_name=domain_name, - source_type=source_type, - backend_language='pyccel-gcc', - plot_source=True, - plot_dir='./plots/tests_source_feb_13/'+run_dir, - hide_plots=True, - m_load_dir=m_load_dir - ) - - time_count(t_stamp_full, msg='full program') + u_ex_c = P1_phys(u_ex, P1, domain, mappings_list).coeffs.toarray() + err = u_ex_c - uh_c + l2_error = np.sqrt(np.dot(err, H1_m.dot(err)))/np.sqrt(np.dot(u_ex_c,H1_m.dot(u_ex_c))) + print(l2_error) + #return l2_error + diags['err'] = l2_error + + return diags diff --git a/psydac/feec/multipatch/examples/hcurl_source_testcase.py b/psydac/feec/multipatch/examples/hcurl_source_testcase.py new file mode 100644 index 000000000..35aa79dd6 --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_source_testcase.py @@ -0,0 +1,144 @@ +""" + Runner script for solving the H(curl) source problem. +""" + +import os +import numpy as np +from psydac.feec.multipatch.examples.hcurl_source_pbms_conga_2d import solve_hcurl_source_pbm + +from psydac.feec.multipatch.utilities import time_count, FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file + +t_stamp_full = time_count() + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# +# main test-cases used for the ppc paper: + +# test_case = 'maxwell_hom_eta=50' # used in paper +#test_case = 'maxwell_hom_eta=170' # used in paper +test_case = 'maxwell_inhom' # used in paper + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +# numerical parameters: +domain_name = 'pretzel_f' +# domain_name = 'curved_L_shape' + +# currently only 'tilde_Pi' is implemented +source_proj = 'tilde_Pi' + +# nc_s = [np.array([16 for _ in range(18)])] + +# corners in pretzel [2, 2, 2*,2*, 2, 1, 1, 1, 1, 1, 0, 0, 1, 2*, 2*, 2, 0, 0 ] +nc_s = [np.array([16, 16, 16, 16, 16, 8, 8, 8, 8, + 8, 8, 8, 8, 16, 16, 16, 8, 8])] +# nc_s = [10] +# refine handles only +# nc_s = [np.array([16, 16, 16, 16, 16, 8, 8, 8, 8, 4, 2, 2, 4, 16, 16, 16, 2, 2])] + +# refine source +# nc_s = [np.array([32, 8, 8, 32, 32, 32, 32, 8, 8, 8, 8, 8, 8, 32, 8, 8, 8, 8])] + +deg_s = [3] + +if test_case == 'maxwell_hom_eta=50': + homogeneous = True + source_type = 'elliptic_J' + omega = np.sqrt(50) # source time pulsation + +elif test_case == 'maxwell_hom_eta=170': + homogeneous = True + source_type = 'elliptic_J' + omega = np.sqrt(170) # source time pulsation + +elif test_case == 'maxwell_inhom': + homogeneous = False + source_type = 'manu_maxwell_inhom' + omega = np.pi + +else: + raise ValueError(test_case) + +case_dir = test_case + +eta = int(-omega**2 * roundoff) / roundoff + +project_sol = True # True # (use conf proj of solution for visualization) +gamma_h = 10 + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +common_diag_filename = './' + case_dir + '_diags.txt' + +for nc in nc_s: + for deg in deg_s: + + params = { + 'domain_name': domain_name, + 'nc': nc, + 'deg': deg, + 'homogeneous': homogeneous, + 'source_type': source_type, + 'source_proj': source_proj, + 'project_sol': project_sol, + 'omega': omega, + 'gamma_h': gamma_h, + } + # backend_language = 'numba' + backend_language = 'pyccel-gcc' + + run_dir = get_run_dir(domain_name, nc, deg, source_type=source_type) + plot_dir = get_plot_dir(case_dir, run_dir) + diag_filename = plot_dir + '/' + \ + diag_fn(source_type=source_type, source_proj=source_proj) + + # to save and load matrices + m_load_dir = get_mat_dir(domain_name, nc, deg) + # to save the FEM sol + + + print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + print(' Calling solve_hcurl_source_pbm() with params = {}'.format(params)) + print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + # calling solver for: + # + # find u in H(curl), s.t. + # A u = f on \Omega + # n x u = n x u_bc on \partial \Omega + # with + # A u := eta * u + mu * curl curl u - nu * grad div u + + diags = solve_hcurl_source_pbm( + nc=nc, deg=deg, + eta=eta, + nu=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + source_proj=source_proj, + backend_language=backend_language, + project_sol=project_sol, + gamma_h=gamma_h, + plot_dir=plot_dir, + m_load_dir=m_load_dir, + ) + + # + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + + write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=diag_filename, + params=params) + write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=common_diag_filename, + params=params) + +time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/examples/mixed_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/mixed_source_pbms_conga_2d.py index e6aa96118..ef7abdce2 100644 --- a/psydac/feec/multipatch/examples/mixed_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/mixed_source_pbms_conga_2d.py @@ -19,14 +19,17 @@ from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_l2 -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.plotting_utilities import plot_field -from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_sol_for_magnetostatic_pbm +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain +from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_sol_for_magnetostatic_pbm from psydac.feec.multipatch.examples.hcurl_eigen_pbms_conga_2d import get_eigenvalues -from psydac.feec.multipatch.utilities import time_count +from psydac.feec.multipatch.utilities import time_count + +from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection + def solve_magnetostatic_pbm( nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='P_L2_wcurl_J', @@ -45,8 +48,8 @@ def solve_magnetostatic_pbm( written in the form of a mixed problem: find p in H1, u in H(curl), such that - G^* u = f_scal on \Omega - G p + A u = f_vect on \Omega + G^* u = f_scal on \\Omega + G p + A u = f_vect on \\Omega with operators @@ -66,7 +69,7 @@ def solve_magnetostatic_pbm( Gh: V0h -> V1h and Ah: V1h -> V1h - in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \Omega, + in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, V0h --grad-> V1h -—curl-> V2h @@ -74,7 +77,7 @@ def solve_magnetostatic_pbm( Harmonic constraint: if dim_harmonic_space > 0, a constraint is added, of the form - u in H^\perp + u in H^\\perp where H = ker(L) is the kernel of the Hodge-Laplace operator L = curl curl u - grad div @@ -92,7 +95,7 @@ def solve_magnetostatic_pbm( """ ncells = [nc, nc] - degree = [deg,deg] + degree = [deg, deg] # if backend_language is None: # backend_language='python' @@ -111,13 +114,14 @@ def solve_magnetostatic_pbm( print('building symbolic and discrete domain...') domain = build_multipatch_domain(domain_name=domain_name) - mappings = OrderedDict([(P.logical_domain, P.mapping) for P in domain.interior]) + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) mappings_list = list(mappings.values()) domain_h = discretize(domain, ncells=ncells) print('building symbolic and discrete derham sequences...') - derham = Derham(domain, ["H1", "Hcurl", "L2"]) - derham_h = discretize(derham, domain_h, degree=degree, backend=PSYDAC_BACKENDS[backend_language]) + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + derham_h = discretize(derham, domain_h, degree=degree) V0h = derham_h.V0 V1h = derham_h.V1 @@ -128,24 +132,28 @@ def solve_magnetostatic_pbm( print('building the discrete operators:') print('commuting projection operators...') - nquads = [4*(d + 1) for d in degree] + nquads = [4 * (d + 1) for d in degree] P0, P1, P2 = derham_h.projectors(nquads=nquads) - # these physical projection operators should probably be in the interface... + # these physical projection operators should probably be in the + # interface... def P0_phys(f_phys): f = lambdify(domain.coordinates, f_phys) - f_log = [pull_2d_h1(f, m) for m in mappings_list] + f_log = [pull_2d_h1(f, m.get_callable_mapping()) + for m in mappings_list] return P0(f_log) def P1_phys(f_phys): f_x = lambdify(domain.coordinates, f_phys[0]) f_y = lambdify(domain.coordinates, f_phys[1]) - f_log = [pull_2d_hcurl([f_x, f_y], m) for m in mappings_list] + f_log = [pull_2d_hcurl([f_x, f_y], m.get_callable_mapping()) + for m in mappings_list] return P1(f_log) def P2_phys(f_phys): f = lambdify(domain.coordinates, f_phys) - f_log = [pull_2d_l2(f, m) for m in mappings_list] + f_log = [pull_2d_l2(f, m.get_callable_mapping()) + for m in mappings_list] return P2(f_log) I0_m = IdLinearOperator(V0h).to_sparse_matrix() @@ -153,29 +161,43 @@ def P2_phys(f_phys): print('Hodge operators...') # multi-patch (broken) linear operators / matrices - H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=0) - H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=1) - H2 = HodgeOperator(V2h, domain_h, backend_language=backend_language, load_dir=m_load_dir, load_space_index=2) - - dH0_m = H0.get_dual_Hodge_sparse_matrix() # = mass matrix of V0 - H0_m = H0.to_sparse_matrix() # = inverse mass matrix of V0 - dH1_m = H1.get_dual_Hodge_sparse_matrix() # = mass matrix of V1 - H1_m = H1.to_sparse_matrix() # = inverse mass matrix of V1 - dH2_m = H2.get_dual_Hodge_sparse_matrix() # = mass matrix of V2 - H2_m = H2.to_sparse_matrix() # = inverse mass matrix of V2 - - M0_m = dH0_m - M1_m = dH1_m # usual notation - - hom_bc = (bc_type == 'pseudo-vacuum') # /!\ here u = B is in H(curl), not E /!\ + H0 = HodgeOperator( + V0h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=0) + H1 = HodgeOperator( + V1h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=1) + H2 = HodgeOperator( + V2h, + domain_h, + backend_language=backend_language, + load_dir=m_load_dir, + load_space_index=2) + + H0_m = H0.to_sparse_matrix() # = mass matrix of V0 + dH0_m = H0.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V0 + H1_m = H1.to_sparse_matrix() # = mass matrix of V1 + dH1_m = H1.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V1 + H2_m = H2.to_sparse_matrix() # = mass matrix of V2 + dH2_m = H2.get_dual_Hodge_sparse_matrix() # = inverse mass matrix of V2 + + M0_m = H0_m + M1_m = H1_m # usual notation + + hom_bc = (bc_type == 'pseudo-vacuum') # /!\ here u = B is in H(curl), not E /!\ print('with hom_bc = {}'.format(hom_bc)) print('conforming projection operators...') - # conforming Projections (should take into account the boundary conditions of the continuous deRham sequence) - cP0 = derham_h.conforming_projection(space='V0', hom_bc=hom_bc, backend_language=backend_language, load_dir=m_load_dir) - cP1 = derham_h.conforming_projection(space='V1', hom_bc=hom_bc, backend_language=backend_language, load_dir=m_load_dir) - cP0_m = cP0.to_sparse_matrix() - cP1_m = cP1.to_sparse_matrix() + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + cP0_m = construct_h1_conforming_projection(V0h, hom_bc=True) + cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) print('broken differential operators...') bD0, bD1 = derham_h.broken_derivatives_as_operators @@ -188,18 +210,18 @@ def P2_phys(f_phys): # Conga (projection-based) operator matrices print('grad matrix...') G_m = bD0_m @ cP0_m - tG_m = dH1_m @ G_m # grad: V0h -> tV1h + tG_m = H1_m @ G_m # grad: V0h -> tV1h print('curl-curl stiffness matrix...') C_m = bD1_m @ cP1_m - CC_m = C_m.transpose() @ dH2_m @ C_m + CC_m = C_m.transpose() @ H2_m @ C_m # jump penalization and stabilization operators: JP0_m = I0_m - cP0_m - S0_m = JP0_m.transpose() @ dH0_m @ JP0_m + S0_m = JP0_m.transpose() @ H0_m @ JP0_m JP1_m = I1_m - cP1_m - S1_m = JP1_m.transpose() @ dH1_m @ JP1_m + S1_m = JP1_m.transpose() @ H1_m @ JP1_m if not hom_bc: # very small regularization to avoid constant p=1 in the kernel @@ -213,65 +235,78 @@ def P2_phys(f_phys): print('computing the harmonic fields...') gamma_Lh = 10 # penalization value should not change the kernel - GD_m = - tG_m @ H0_m @ G_m.transpose() @ dH1_m # todo: check with paper + GD_m = - tG_m @ dH0_m @ G_m.transpose() @ H1_m # todo: check with paper L_m = CC_m - GD_m + gamma_Lh * S1_m - eigenvalues, eigenvectors = get_eigenvalues(dim_harmonic_space+1, 1e-6, L_m, dH1_m) + eigenvalues, eigenvectors = get_eigenvalues( + dim_harmonic_space + 1, 1e-6, L_m, H1_m) for i in range(dim_harmonic_space): - lambda_i = eigenvalues[i] - print(".. storing eigenmode #{}, with eigenvalue = {}".format(i, lambda_i)) + lambda_i = eigenvalues[i] + print( + ".. storing eigenmode #{}, with eigenvalue = {}".format( + i, lambda_i)) # check: if abs(lambda_i) > 1e-8: print(" ****** WARNING! this eigenvalue should be 0! ****** ") - hf_cs.append(eigenvectors[:,i]) + hf_cs.append(eigenvectors[:, i]) # matrix of the coefs of the harmonic fields (Lambda^H_i) in the basis (Lambda_i), in the form: - # hf_m = (c^H_{i,j})_{i < dim_harmonic_space, j < dim_V1} such that Lambda^H_i = sum_j c^H_{i,j} Lambda^1_j + # hf_m = (c^H_{i,j})_{i < dim_harmonic_space, j < dim_V1} such that + # Lambda^H_i = sum_j c^H_{i,j} Lambda^1_j hf_m = bmat(hf_cs).transpose() MH_m = M1_m @ hf_m # check: - lambda_i = eigenvalues[dim_harmonic_space] # should be the first positive eigenvalue of L_h + # should be the first positive eigenvalue of L_h + lambda_i = eigenvalues[dim_harmonic_space] if abs(lambda_i) < 1e-4: print(" ****** Warning -- something is probably wrong: ") - print(" ****** eigenmode #{} should have positive eigenvalue: {}".format(dim_harmonic_space, lambda_i)) + print( + " ****** eigenmode #{} should have positive eigenvalue: {}".format( + dim_harmonic_space, lambda_i)) print('computing the full operator matrix with harmonic constraint...') - A_m = bmat([[ reg_S0_m, tG_m.transpose(), None ], - [ tG_m, CC_m + gamma1_h * S1_m, MH_m ], - [ None, MH_m.transpose(), None ]]) + A_m = bmat([[reg_S0_m, tG_m.transpose(), None], + [tG_m, CC_m + gamma1_h * S1_m, MH_m], + [None, MH_m.transpose(), None]]) else: print('computing the full operator matrix without harmonic constraint...') - A_m = bmat([[ reg_S0_m, tG_m.transpose() ], - [ tG_m, CC_m + gamma1_h * S1_m ]]) + A_m = bmat([[reg_S0_m, tG_m.transpose()], + [tG_m, CC_m + gamma1_h * S1_m]]) # get exact source, bc's, ref solution... # (not all the returned functions are useful here) print('getting the source and ref solution...') N_diag = 200 method = 'conga' - f_scal, f_vect, j_scal, uh_ref = get_source_and_sol_for_magnetostatic_pbm(source_type=source_type, domain=domain, domain_name=domain_name) + f_scal, f_vect, j_scal, uh_ref = get_source_and_sol_for_magnetostatic_pbm( + source_type=source_type, domain=domain, domain_name=domain_name) # compute approximate source: # ff_h = (f0_h, f1_h) = (P0_h f_scal, P1_h f_vect) with projection operators specified by source_proj # and dual-basis coefficients in column array bb_c = (b0_c, b1_c) - # note: f1_h may also be defined through the special option 'P_L2_wcurl_J' for magnetostatic problems + # note: f1_h may also be defined through the special option 'P_L2_wcurl_J' + # for magnetostatic problems f0_c = f1_c = j2_c = None assert source_proj in ['P_geom', 'P_L2', 'P_L2_wcurl_J'] if f_scal is None: tilde_f0_c = np.zeros(V0h.nbasis) else: - print('approximating the V0 source with '+source_proj) + print('approximating the V0 source with ' + source_proj) if source_proj == 'P_geom': f0_h = P0_phys(f_scal) f0_c = f0_h.coeffs.toarray() - tilde_f0_c = dH0_m.dot(f0_c) + tilde_f0_c = H0_m.dot(f0_c) else: # L2 proj - tilde_f0_c = derham_h.get_dual_dofs(space='V0', f=f_scal, backend_language=backend_language, return_format='numpy_array') + tilde_f0_c = derham_h.get_dual_dofs( + space='V0', + f=f_scal, + backend_language=backend_language, + return_format='numpy_array') if source_proj == 'P_L2_wcurl_J': if j_scal is None: @@ -279,34 +314,42 @@ def P2_phys(f_phys): tilde_f1_c = np.zeros(V1h.nbasis) else: print('approximating the V1 source as a weak curl of j_scal') - tilde_j2_c = derham_h.get_dual_dofs(space='V2', f=j_scal, backend_language=backend_language, return_format='numpy_array') + tilde_j2_c = derham_h.get_dual_dofs( + space='V2', + f=j_scal, + backend_language=backend_language, + return_format='numpy_array') tilde_f1_c = C_m.transpose().dot(tilde_j2_c) elif f_vect is None: - tilde_f1_c = np.zeros(V1h.nbasis) + tilde_f1_c = np.zeros(V1h.nbasis) else: - print('approximating the V1 source with '+source_proj) + print('approximating the V1 source with ' + source_proj) if source_proj == 'P_geom': f1_h = P1_phys(f_vect) f1_c = f1_h.coeffs.toarray() - tilde_f1_c = dH1_m.dot(f1_c) + tilde_f1_c = H1_m.dot(f1_c) else: assert source_proj == 'P_L2' - tilde_f1_c = derham_h.get_dual_dofs(space='V1', f=f_vect, backend_language=backend_language, return_format='numpy_array') + tilde_f1_c = derham_h.get_dual_dofs( + space='V1', + f=f_vect, + backend_language=backend_language, + return_format='numpy_array') if plot_source: if f0_c is None: - f0_c = H0_m.dot(tilde_f0_c) - plot_field(numpy_coeffs=f0_c, Vh=V0h, space_kind='h1', domain=domain, title='f0_h with P = '+source_proj, - filename=plot_dir+'f0h_'+source_proj+'.png', hide_plot=hide_plots) + f0_c = dH0_m.dot(tilde_f0_c) + plot_field(numpy_coeffs=f0_c, Vh=V0h, space_kind='h1', domain=domain, title='f0_h with P = ' + source_proj, + filename=plot_dir + 'f0h_' + source_proj + '.png', hide_plot=hide_plots) if f1_c is None: - f1_c = H1_m.dot(tilde_f1_c) - plot_field(numpy_coeffs=f1_c, Vh=V1h, space_kind='hcurl', domain=domain, title='f1_h with P = '+source_proj, - filename=plot_dir+'f1h_'+source_proj+'.png', hide_plot=hide_plots) + f1_c = dH1_m.dot(tilde_f1_c) + plot_field(numpy_coeffs=f1_c, Vh=V1h, space_kind='hcurl', domain=domain, title='f1_h with P = ' + source_proj, + filename=plot_dir + 'f1h_' + source_proj + '.png', hide_plot=hide_plots) if source_proj == 'P_L2_wcurl_J': if j2_c is None: - j2_c = H2_m.dot(tilde_j2_c) + j2_c = dH2_m.dot(tilde_j2_c) plot_field(numpy_coeffs=j2_c, Vh=V2h, space_kind='l2', domain=domain, title='P_L2 jh in V2h', - filename=plot_dir+'j2h.png', hide_plot=hide_plots) + filename=plot_dir + 'j2h.png', hide_plot=hide_plots) print("building block RHS") if dim_harmonic_space > 0: @@ -320,15 +363,18 @@ def P2_phys(f_phys): sol_c = spsolve(A_m.asformat('csr'), b_c) # ------------------------------------------------------------ ph_c = sol_c[:V0h.nbasis] - uh_c = sol_c[V0h.nbasis:V0h.nbasis+V1h.nbasis] + uh_c = sol_c[V0h.nbasis:V0h.nbasis + V1h.nbasis] hh_c = np.zeros(V1h.nbasis) if dim_harmonic_space > 0: # compute the harmonic part (h) of the solution - hh_hbcoefs = sol_c[V0h.nbasis+V1h.nbasis:] # coefs of the harmonic part, in the basis of the harmonic fields + # coefs of the harmonic part, in the basis of the harmonic fields + hh_hbcoefs = sol_c[V0h.nbasis + V1h.nbasis:] assert len(hh_hbcoefs) == dim_harmonic_space for i in range(dim_harmonic_space): - hi_c = hf_cs[i] # coefs the of the i-th harmonic field, in the B/M spline basis of V1h - hh_c += hh_hbcoefs[i]*hi_c + # coefs the of the i-th harmonic field, in the B/M spline basis of + # V1h + hi_c = hf_cs[i] + hh_c += hh_hbcoefs[i] * hi_c if project_solution: print('projecting the homogeneous solution on the conforming problem space...') @@ -344,19 +390,20 @@ def P2_phys(f_phys): params_str = 'gamma0_h={}_gamma1_h={}'.format(gamma0_h, gamma1_h) title = r'solution {} (amplitude)'.format(p_name) plot_field(numpy_coeffs=ph_c, Vh=V0h, space_kind='h1', - domain=domain, title=title, filename=plot_dir+params_str+'_ph.png', hide_plot=hide_plots) + domain=domain, title=title, filename=plot_dir + params_str + '_ph.png', hide_plot=hide_plots) title = r'solution $h_h$ (amplitude)' plot_field(numpy_coeffs=hh_c, Vh=V1h, space_kind='hcurl', - domain=domain, title=title, filename=plot_dir+params_str+'_hh.png', hide_plot=hide_plots) + domain=domain, title=title, filename=plot_dir + params_str + '_hh.png', hide_plot=hide_plots) title = r'solution {} (amplitude)'.format(u_name) plot_field(numpy_coeffs=uh_c, Vh=V1h, space_kind='hcurl', - domain=domain, title=title, filename=plot_dir+params_str+'_uh.png', hide_plot=hide_plots) + domain=domain, title=title, filename=plot_dir + params_str + '_uh.png', hide_plot=hide_plots) title = r'solution {} (vector field)'.format(u_name) plot_field(numpy_coeffs=uh_c, Vh=V1h, space_kind='hcurl', - domain=domain, title=title, filename=plot_dir+params_str+'_uh_vf.png', hide_plot=hide_plots) + domain=domain, title=title, filename=plot_dir + params_str + '_uh_vf.png', hide_plot=hide_plots) title = r'solution {} (components)'.format(u_name) plot_field(numpy_coeffs=uh_c, Vh=V1h, space_kind='hcurl', - domain=domain, title=title, filename=plot_dir+params_str+'_uh_xy.png', hide_plot=hide_plots) + domain=domain, title=title, filename=plot_dir + params_str + '_uh_xy.png', hide_plot=hide_plots) + if __name__ == '__main__': @@ -382,7 +429,8 @@ def P2_phys(f_phys): # nc = 2 # deg = 2 - run_dir = '{}_{}_bc={}_nc={}_deg={}/'.format(domain_name, source_type, bc_type, nc, deg) + run_dir = '{}_{}_bc={}_nc={}_deg={}/'.format( + domain_name, source_type, bc_type, nc, deg) m_load_dir = 'matrices_{}_nc={}_deg={}/'.format(domain_name, nc, deg) solve_magnetostatic_pbm( nc=nc, deg=deg, @@ -393,7 +441,7 @@ def P2_phys(f_phys): backend_language='pyccel-gcc', dim_harmonic_space=dim_harmonic_space, plot_source=True, - plot_dir='./plots/magnetostatic_runs/'+run_dir, + plot_dir='./plots/magnetostatic_runs/' + run_dir, hide_plots=True, m_load_dir=m_load_dir ) diff --git a/psydac/feec/multipatch/examples/ppc_test_cases.py b/psydac/feec/multipatch/examples/ppc_test_cases.py index 2e7c60039..122a68dca 100644 --- a/psydac/feec/multipatch/examples/ppc_test_cases.py +++ b/psydac/feec/multipatch/examples/ppc_test_cases.py @@ -1,34 +1,236 @@ # coding: utf-8 +from sympy.functions.special.error_functions import erf from mpi4py import MPI import os import numpy as np -from sympy import pi, cos, sin, Tuple, exp +from sympy import pi, cos, sin, Tuple, exp, atan, atan2 from sympde.topology import Derham -from psydac.fem.basic import FemField -from psydac.feec.multipatch.api import discretize -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.plotting_utilities import plot_field -from psydac.feec.multipatch.plotting_utilities import get_plotting_grid, my_small_plot, my_small_streamplot +from psydac.fem.basic import FemField +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.plotting_utilities import get_plotting_grid, my_small_plot, my_small_streamplot from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -from psydac.feec.multipatch.utilities import sol_ref_fn, error_fn, get_method_name, get_fem_name, get_load_dir - comm = MPI.COMM_WORLD -# todo [MCP, 12/02/2022]: add an 'equation' argument to be able to return 'exact solution' +# todo [MCP, 12/02/2022]: add an 'equation' argument to be able to return +# 'exact solution' + +def get_phi_pulse(x_0, y_0, domain=None): + x, y = domain.coordinates + ds2_0 = (0.02)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + + return phi_0 + + +def get_div_free_pulse(x_0, y_0, domain=None): + x, y = domain.coordinates + ds2_0 = (0.02)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 + dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + f_x = dy_phi_0 + f_y = - dx_phi_0 + f_vect = Tuple(f_x, f_y) + + return f_vect + + +def get_curl_free_pulse(x_0, y_0, domain=None, pp=False): + # return -grad phi_0 + x, y = domain.coordinates + if pp: + # psi=phi + ds2_0 = (0.02)**2 + else: + ds2_0 = (0.1)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 + dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + f_x = -dx_phi_0 + f_y = -dy_phi_0 + f_vect = Tuple(f_x, f_y) + + return f_vect + + +def get_Delta_phi_pulse(x_0, y_0, domain=None, pp=False): + # return -Delta phi_0, with same phi_0 as in get_curl_free_pulse() + x, y = domain.coordinates + if pp: + # psi=phi + ds2_0 = (0.02)**2 + else: + ds2_0 = (0.1)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dxx_sig_0 = 2 + dyy_sig_0 = 2 + dxx_phi_0 = ((dx_sig_0 * sigma_0 / ds2_0)**2 - + ((dx_sig_0)**2 + dxx_sig_0 * sigma_0) / ds2_0) * phi_0 + dyy_phi_0 = ((dy_sig_0 * sigma_0 / ds2_0)**2 - + ((dy_sig_0)**2 + dyy_sig_0 * sigma_0) / ds2_0) * phi_0 + f = - dxx_phi_0 - dyy_phi_0 + + return f + + +def get_Gaussian_beam_old(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = (10, 0) + nk = np.sqrt(k[0]**2 + k[1]**2) + + v = (k[0] / nk, k[1] / nk) + + sigma = 0.05 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k[1] * x + k[0] * y) * ef + B = (-v[1] * x + v[0] * y) / (sigma**2) * E + + return Tuple(v[0] * E, v[1] * E), B + + +def get_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + + x = x - x_0 + y = y - y_0 + + sigma = 0.1 + + xy = x**2 + y**2 + ef = 1 / (sigma**2) * exp(- xy / (2 * sigma**2)) + + # E = curl exp + E = Tuple(y * ef, -x * ef) + + # B = curl E + B = (xy / (sigma**2) - 2) * ef + + return E, B + + +def get_diag_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = (np.pi, np.pi) + nk = np.sqrt(k[0]**2 + k[1]**2) + + v = (k[0] / nk, k[1] / nk) + + sigma = 0.25 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k[1] * x + k[0] * y) * ef + B = (-v[1] * x + v[0] * y) / (sigma**2) * E + + return Tuple(v[0] * E, v[1] * E), B + + +def get_easy_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = pi + sigma = 0.5 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k * y) * ef + B = -y / (sigma**2) * E + + return Tuple(E, 0), B + + +def get_Gaussian_beam2(x_0, y_0, domain=None): + """ + Gaussian beam + Beam inciding from the left, centered and normal to wall: + x: axial normalized distance to the beam's focus + y: radial normalized distance to the center axis of the beam + """ + x, y = domain.coordinates + + x0 = x_0 + y0 = y_0 + theta = pi / 2 + w0 = 1 + + t = [(x - x0) * cos(theta) - (y - y0) * sin(theta), + (x - x0) * sin(theta) + (y - y0) * cos(theta)] + + EW0 = 1.0 # amplitude at the waist + k0 = 2 * pi # free-space wavenumber + + x_ray = pi * w0 ** 2 # Rayleigh range + + w = w0 * (1 + t[0]**2 / x_ray**2)**0.5 # width + curv = t[0] / (t[0]**2 + x_ray**2) # curvature + + # corresponds to atan(x / x_ray), which is the Gouy phase + gouy_psi = -0.5 * atan2(t[0] / x_ray, 1.) + + EW_mod = EW0 * (w0 / w)**0.5 * exp(-(t[1] ** 2) / (w ** 2)) # Amplitude + phase = k0 * t[0] + 0.5 * k0 * curv * t[1] ** 2 + gouy_psi # Phase + + EW_r = EW_mod * cos(phase) # Real part + EW_i = EW_mod * sin(phase) # Imaginary part + + B = 0 # t[1]/(w**2) * EW_r + + return Tuple(0, EW_r), B + def get_source_and_sol_for_magnetostatic_pbm( source_type=None, domain=None, domain_name=None, refsol_params=None ): - x,y = domain.coordinates + """ + provide source, and exact solutions when available, for: + + Find u=B in H(curl) such that + + div B = 0 + curl B = j + + written as a mixed problem, see solve_magnetostatic_pbm() + """ + u_ex = None # exact solution + x, y = domain.coordinates if source_type == 'dipole_J': # we compute two possible source terms: # . a dipole current j_scal = phi_0 - phi_1 (two blobs) @@ -36,329 +238,187 @@ def get_source_and_sol_for_magnetostatic_pbm( x_0 = 1.0 y_0 = 1.0 ds2_0 = (0.02)**2 - sigma_0 = (x-x_0)**2 + (y-y_0)**2 - phi_0 = exp(-sigma_0**2/(2*ds2_0)) - dx_sig_0 = 2*(x-x_0) - dy_sig_0 = 2*(y-y_0) + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 x_1 = 2.0 y_1 = 2.0 ds2_1 = (0.02)**2 - sigma_1 = (x-x_1)**2 + (y-y_1)**2 - phi_1 = exp(-sigma_1**2/(2*ds2_1)) - dx_sig_1 = 2*(x-x_1) - dy_sig_1 = 2*(y-y_1) + sigma_1 = (x - x_1)**2 + (y - y_1)**2 + phi_1 = exp(-sigma_1**2 / (2 * ds2_1)) + dx_sig_1 = 2 * (x - x_1) + dy_sig_1 = 2 * (y - y_1) dx_phi_1 = - dx_sig_1 * sigma_1 / ds2_1 * phi_1 dy_phi_1 = - dy_sig_1 * sigma_1 / ds2_1 * phi_1 - f_scal = None # + f_scal = None j_scal = phi_0 - phi_1 - f_x = dy_phi_0 - dy_phi_1 - f_y = - dx_phi_0 + dx_phi_1 + f_x = dy_phi_0 - dy_phi_1 + f_y = - dx_phi_0 + dx_phi_1 f_vect = Tuple(f_x, f_y) else: raise ValueError(source_type) - # ref solution in V1h: - uh_ref = get_sol_ref_V1h(source_type, domain, domain_name, refsol_params) - - return f_scal, f_vect, j_scal, uh_ref + return f_scal, f_vect, j_scal, u_ex -def get_source_and_solution(source_type=None, eta=0, mu=0, nu=0, - domain=None, domain_name=None, - refsol_params=None): - """ - compute source and reference solution (exact, or reference values) when possible, depending on the source_type +def get_source_and_solution_hcurl( + source_type=None, eta=0, mu=0, nu=0, + domain=None, domain_name=None): """ + provide source, and exact solutions when available, for: + + Find u in H(curl) such that - # ref solution (values on diag grid) - ph_ref = None - uh_ref = None + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega + + with + + A u := eta * u + mu * curl curl u - nu * grad div u + + see solve_hcurl_source_pbm() + """ # exact solutions (if available) u_ex = None - p_ex = None + curl_u_ex = None + div_u_ex = None - # bc solution: describe the bc on boundary. Inside domain, values should not matter. Homogeneous bc will be used if None + # bc solution: describe the bc on boundary. Inside domain, values should + # not matter. Homogeneous bc will be used if None u_bc = None - # only hom bc on p (for now...) # source terms f_vect = None - f_scal = None # auxiliary term (for more diagnostics) grad_phi = None phi = None - x,y = domain.coordinates - - if source_type == 'manu_J': - # todo: remove if not used ? - # use a manufactured solution, with ad-hoc (homogeneous or inhomogeneous) bc - if domain_name in ['square_2', 'square_6', 'square_8', 'square_9']: - t = 1 - else: - t = pi + x, y = domain.coordinates - u_ex = Tuple(sin(t*y), sin(t*x)*cos(t*y)) - f_vect = Tuple( - sin(t*y) * (eta + t**2 *(mu - cos(t*x)*(mu-nu))), - sin(t*x) * cos(t*y) * (eta + t**2 *(mu+nu) ) - ) - - # boundary condition: (here we only need to coincide with u_ex on the boundary !) - if domain_name in ['square_2', 'square_6', 'square_9']: - u_bc = None - else: - u_bc = u_ex - - elif source_type == 'manutor_poisson': - # todo: remove if not used ? - # same as manu_poisson, with arbitrary value for tor - x0 = 1.5 - y0 = 1.5 - s = (x-x0) - (y-y0) - t = (x-x0) + (y-y0) - a = (1/1.9)**2 - b = (1/1.2)**2 - sigma2 = 0.0121 - tor = 2 - tau = a*s**2 + b*t**2 - 1 - phi = exp(-tau**tor/(2*sigma2)) - dx_tau = 2*( a*s + b*t) - dy_tau = 2*(-a*s + b*t) - dxx_tau = 2*(a + b) - dyy_tau = 2*(a + b) - f_scal = -((tor*tau**(tor-1)*dx_tau/(2*sigma2))**2 - (tau**(tor-1)*dxx_tau + (tor-1)*tau**(tor-2)*dx_tau**2)*tor/(2*sigma2) - +(tor*tau**(tor-1)*dy_tau/(2*sigma2))**2 - (tau**(tor-1)*dyy_tau + (tor-1)*tau**(tor-2)*dy_tau**2)*tor/(2*sigma2))*phi - p_ex = phi - - elif source_type == 'manu_maxwell': + if source_type == 'manu_maxwell_inhom': # used for Maxwell equation with manufactured solution - alpha = eta - u_ex = Tuple(sin(pi*y), sin(pi*x)*cos(pi*y)) - f_vect = Tuple(alpha*sin(pi*y) - pi**2*sin(pi*y)*cos(pi*x) + pi**2*sin(pi*y), - alpha*sin(pi*x)*cos(pi*y) + pi**2*sin(pi*x)*cos(pi*y)) + f_vect = Tuple(eta * sin(pi * y) - pi**2 * sin(pi * y) * cos(pi * x) + pi**2 * sin(pi * y), + eta * sin(pi * x) * cos(pi * y) + pi**2 * sin(pi * x) * cos(pi * y)) + if nu == 0: + u_ex = Tuple(sin(pi * y), sin(pi * x) * cos(pi * y)) + curl_u_ex = pi * (cos(pi * x) * cos(pi * y) - cos(pi * y)) + div_u_ex = -pi * sin(pi * x) * sin(pi * y) + else: + raise NotImplementedError u_bc = u_ex - elif source_type in ['manu_poisson', 'elliptic_J']: - # 'manu_poisson': used for Poisson pbm with manufactured solution - # 'elliptic_J': used for Maxwell pbm (no manufactured solution) -- (was 'ellnew_J' in previous version) + elif source_type == 'elliptic_J': + # no manufactured solution for Maxwell pbm x0 = 1.5 y0 = 1.5 - s = (x-x0) - (y-y0) - t = (x-x0) + (y-y0) - a = (1/1.9)**2 - b = (1/1.2)**2 + s = (x - x0) - (y - y0) + t = (x - x0) + (y - y0) + a = (1 / 1.9)**2 + b = (1 / 1.2)**2 sigma2 = 0.0121 - tau = a*s**2 + b*t**2 - 1 - phi = exp(-tau**2/(2*sigma2)) - dx_tau = 2*( a*s + b*t) - dy_tau = 2*(-a*s + b*t) - dxx_tau = 2*(a + b) - dyy_tau = 2*(a + b) - - dx_phi = (-tau*dx_tau/sigma2)*phi - dy_phi = (-tau*dy_tau/sigma2)*phi - grad_phi = Tuple(dx_phi, dy_phi) + tau = a * s**2 + b * t**2 - 1 + phi = exp(-tau**2 / (2 * sigma2)) + dx_tau = 2 * (a * s + b * t) + dy_tau = 2 * (-a * s + b * t) + f_x = dy_tau * phi + f_y = - dx_tau * phi + f_vect = Tuple(f_x, f_y) - f_scal = -( (tau*dx_tau/sigma2)**2 - (tau*dxx_tau + dx_tau**2)/sigma2 - +(tau*dy_tau/sigma2)**2 - (tau*dyy_tau + dy_tau**2)/sigma2 )*phi + else: + raise ValueError(source_type) - # exact solution of -p'' = f with hom. bc's on pretzel domain - p_ex = phi + # u_ex = Tuple(0, 1) # DEBUG + return f_vect, u_bc, u_ex, curl_u_ex, div_u_ex # , phi, grad_phi - if not domain_name in ['pretzel', 'pretzel_f']: - print("WARNING (87656547) -- I'm not sure we have an exact solution -- check the bc's on the domain "+domain_name) - # raise NotImplementedError(domain_name) - f_x = dy_tau * phi - f_y = - dx_tau * phi - f_vect = Tuple(f_x, f_y) +def get_source_and_solution_h1(source_type=None, eta=0, mu=0, + domain=None, domain_name=None): + """ + provide source, and exact solutions when available, for: - elif source_type == 'manu_poisson_2': - f_scal = -4 - p_ex = x**2+y**2 - phi = p_ex - u_bc = p_ex - u_ex = p_ex - elif source_type == 'curl_dipole_J': - # used for the magnetostatic problem - - # was 'dicurl_J' in previous version - - # here, f is the curl of a dipole current j = phi_0 - phi_1 (two blobs) that correspond to a scalar current density - # - # the solution u of the curl-curl problem with free-divergence constraint - # curl curl u = curl j - # - # then corresponds to a magnetic density, - # see Beirão da Veiga, Brezzi, Dassi, Marini and Russo, Virtual Element approx of 2D magnetostatic pbms, CMAME 327 (2017) + Find u in H^1, such that - x_0 = 1.0 - y_0 = 1.0 - ds2_0 = (0.02)**2 - sigma_0 = (x-x_0)**2 + (y-y_0)**2 - phi_0 = exp(-sigma_0**2/(2*ds2_0)) - dx_sig_0 = 2*(x-x_0) - dy_sig_0 = 2*(y-y_0) - dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 - dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + A u = f on \\Omega + u = u_bc on \\partial \\Omega - x_1 = 2.0 - y_1 = 2.0 - ds2_1 = (0.02)**2 - sigma_1 = (x-x_1)**2 + (y-y_1)**2 - phi_1 = exp(-sigma_1**2/(2*ds2_1)) - dx_sig_1 = 2*(x-x_1) - dy_sig_1 = 2*(y-y_1) - dx_phi_1 = - dx_sig_1 * sigma_1 / ds2_1 * phi_1 - dy_phi_1 = - dy_sig_1 * sigma_1 / ds2_1 * phi_1 + with - f_x = dy_phi_0 - dy_phi_1 - f_y = - dx_phi_0 + dx_phi_1 - f_scal = 0 # phi_0 - phi_1 - f_vect = Tuple(f_x, f_y) + A u := eta * u - mu * div grad u - elif source_type == 'old_ellip_J': - - # divergence-free f field along an ellipse curve - if domain_name in ['pretzel', 'pretzel_f']: - dr = 0.2 - r0 = 1 - x0 = 1.5 - y0 = 1.5 - # s0 = x0-y0 - # t0 = x0+y0 - s = (x-x0) - (y-y0) - t = (x-x0) + (y-y0) - aa = (1/1.7)**2 - bb = (1/1.1)**2 - dsigpsi2 = 0.01 - sigma = aa*s**2 + bb*t**2 - 1 - psi = exp(-sigma**2/(2*dsigpsi2)) - dx_sig = 2*( aa*s + bb*t) - dy_sig = 2*(-aa*s + bb*t) - f_x = dy_sig * psi - f_y = - dx_sig * psi - - dsigphi2 = 0.01 # this one gives approx 1e-10 at boundary for phi - # dsigphi2 = 0.005 # if needed: smaller support for phi, to have a smaller value at boundary - phi = exp(-sigma**2/(2*dsigphi2)) - dx_phi = phi*(-dx_sig*sigma/dsigphi2) - dy_phi = phi*(-dy_sig*sigma/dsigphi2) - - grad_phi = Tuple(dx_phi, dy_phi) - f_vect = Tuple(f_x, f_y) + see solve_h1_source_pbm() + """ - else: - raise NotImplementedError + # exact solutions (if available) + u_ex = None - elif source_type in ['ring_J', 'sring_J']: - # used for the magnetostatic problem - # 'rotating' (divergence-free) f field: - - if domain_name in ['square_2', 'square_6', 'square_8', 'square_9']: - r0 = np.pi/4 - dr = 0.1 - x0 = np.pi/2 - y0 = np.pi/2 - omega = 43/2 - # alpha = -omega**2 # not a square eigenvalue - f_factor = 100 - - elif domain_name in ['curved_L_shape']: - r0 = np.pi/4 - dr = 0.1 - x0 = np.pi/2 - y0 = np.pi/2 - omega = 43/2 - # alpha = -omega**2 # not a square eigenvalue - f_factor = 100 + # bc solution: describe the bc on boundary. Inside domain, values should + # not matter. Homogeneous bc will be used if None + u_bc = None - else: - # for pretzel - - # omega = 8 # ? - # alpha = -omega**2 - - source_option = 2 - - if source_option==1: - # big circle: - r0 = 2.4 - dr = 0.05 - x0 = 0 - y0 = 0.5 - f_factor = 10 - - elif source_option==2: - # small circle in corner: - if source_type == 'ring_J': - dr = 0.2 - else: - # smaller ring - dr = 0.1 - assert source_type == 'sring_J' - r0 = 1 - x0 = 1.5 - y0 = 1.5 - f_factor = 10 - - else: - raise NotImplementedError - - # note: some other currents give sympde error, see below [1] - phi = f_factor * exp( - .5*(( (x-x0)**2 + (y-y0)**2 - r0**2 )/dr)**2 ) - - f_x = - (y-y0) * phi - f_y = (x-x0) * phi + # source terms + f_scal = None - f_vect = Tuple(f_x, f_y) + # auxiliary term (for more diagnostics) + grad_phi = None + phi = None - else: - raise ValueError(source_type) + x, y = domain.coordinates - if u_ex is None: - uh_ref = get_sol_ref_V1h(source_type, domain, domain_name, refsol_params) + if source_type in ['manu_poisson_elliptic']: + x0 = 1.5 + y0 = 1.5 + s = (x - x0) - (y - y0) + t = (x - x0) + (y - y0) + a = (1 / 1.9)**2 + b = (1 / 1.2)**2 + sigma2 = 0.0121 + tau = a * s**2 + b * t**2 - 1 + phi = exp(-tau**2 / (2 * sigma2)) + dx_tau = 2 * (a * s + b * t) + dy_tau = 2 * (-a * s + b * t) + dxx_tau = 2 * (a + b) + dyy_tau = 2 * (a + b) + + dx_phi = (-tau * dx_tau / sigma2) * phi + dy_phi = (-tau * dy_tau / sigma2) * phi + grad_phi = Tuple(dx_phi, dy_phi) - return f_scal, f_vect, u_bc, ph_ref, uh_ref, p_ex, u_ex, phi, grad_phi + f_scal = -((tau * dx_tau / sigma2)**2 - (tau * dxx_tau + dx_tau**2) / sigma2 + + (tau * dy_tau / sigma2)**2 - (tau * dyy_tau + dy_tau**2) / sigma2) * phi + # exact solution of -p'' = f with hom. bc's on pretzel domain + if mu == 1 and eta == 0: + u_ex = phi + else: + print('WARNING (54375385643): exact solution not available in this case!') -def get_sol_ref_V1h( source_type=None, domain=None, domain_name=None, refsol_params=None ): - """ - get a reference solution as a V1h FemField - """ - uh_ref = None - if refsol_params is not None: - N_diag, method_ref, source_proj_ref = refsol_params - u_ref_filename = ( get_load_dir(method=method_ref, domain_name=domain_name,nc=None,deg=None,data='solutions') - + sol_ref_fn(source_type, N_diag, source_proj=source_proj_ref) ) - print("no exact solution for this test-case, looking for ref solution values in file {}...".format(u_ref_filename)) - if os.path.isfile(u_ref_filename): - print("-- file found") - with open(u_ref_filename, 'rb') as file: - ncells_degree = np.load(file) - ncells = [int(i) for i in ncells_degree['ncells_degree'][0]] - degree = [int(i) for i in ncells_degree['ncells_degree'][1]] - - derham = Derham(domain, ["H1", "Hcurl", "L2"]) - domain_h = discretize(domain, ncells=ncells, comm=comm) - V1h = discretize(derham.V1, domain_h, degree=degree, basis='M') - uh_ref = FemField(V1h) - for i,Vi in enumerate(V1h.spaces): - for j,Vij in enumerate(Vi.spaces): - filename = u_ref_filename+'_%d_%d'%(i,j) - uij = Vij.import_fields(filename, 'phi') - uh_ref.fields[i].fields[j].coeffs._data = uij[0].coeffs._data + if not domain_name in ['pretzel', 'pretzel_f']: + # we may have non-hom bc's + u_bc = u_ex + elif source_type == 'manu_poisson_2': + f_scal = -4 + if mu == 1 and eta == 0: + u_ex = x**2 + y**2 else: - print("-- no file, skipping it") + raise NotImplementedError + u_bc = u_ex + + elif source_type == 'manu_poisson_sincos': + u_ex = sin(pi * x) * cos(pi * y) + f_scal = (eta + 2 * mu * pi**2) * u_ex + u_bc = u_ex + + else: + raise ValueError(source_type) - return uh_ref + return f_scal, u_bc, u_ex diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py new file mode 100644 index 000000000..9b1ccb437 --- /dev/null +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -0,0 +1,1278 @@ +""" + solver for the TD Maxwell problem: find E(t) in H(curl), B in L2, such that + + dt E - curl B = -J on \\Omega + dt B + curl E = 0 on \\Omega + n x E = n x E_bc on \\partial \\Omega + + with Ampere discretized weakly and Faraday discretized strongly, in a broken-FEEC approach on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + (Eh) (Bh) +""" + +from pytest import param +from mpi4py import MPI + +import os +import numpy as np +import scipy as sp +from collections import OrderedDict +import matplotlib.pyplot as plt + +from sympy import lambdify, Matrix + +from scipy.sparse.linalg import spsolve +from scipy import special + +from sympde.calculus import dot +from sympde.topology import element_of +from sympde.expr.expr import LinearForm +from sympde.expr.expr import integral, Norm +from sympde.topology import Derham + +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.feec.pull_push import pull_2d_hcurl +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.feec.multipatch.operators import HodgeOperator, get_K0_and_K0_inv, get_K1_and_K1_inv +# , write_field_to_diag_grid, +from psydac.feec.multipatch.plotting_utilities import plot_field +from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain +# , get_praxial_Gaussian_beam_E, get_easy_Gaussian_beam_E, get_easy_Gaussian_beam_B,get_easy_Gaussian_beam_E_2, get_easy_Gaussian_beam_B_2 +from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_hcurl, get_div_free_pulse, get_curl_free_pulse, get_Delta_phi_pulse, get_Gaussian_beam +from psydac.feec.multipatch.utils_conga_2d import DiagGrid, P0_phys, P1_phys, P2_phys, get_Vh_diags_for +from psydac.feec.multipatch.utilities import time_count # , export_sol, import_sol +from psydac.linalg.utilities import array_to_psydac +from psydac.fem.basic import FemField +from psydac.feec.multipatch.non_matching_operators import construct_hcurl_conforming_projection, construct_h1_conforming_projection +from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain + +from psydac.api.postprocessing import OutputManager, PostProcessManager + + +def solve_td_maxwell_pbm(*, + nc=4, + deg=4, + final_time=20, + cfl_max=0.8, + dt_max=None, + domain_name='pretzel_f', + backend='pyccel-gcc', + source_type='zero', + source_omega=None, + source_proj='P_geom', + conf_proj='BSP', + gamma_h=10., + project_sol=False, + filter_source=True, + quad_param=1, + E0_type='zero', + E0_proj='P_L2', + hide_plots=True, + plot_dir=None, + plot_time_ranges=None, + plot_source=False, + plot_divE=False, + diag_dt=None, + # diag_dtau = None, + cb_min_sol=None, + cb_max_sol=None, + m_load_dir=None, + th_sol_filename="", + source_is_harmonic=False, + domain_lims=None + ): + """ + solver for the TD Maxwell problem: find E(t) in H(curl), B in L2, such that + + dt E - curl B = -J on \\Omega + dt B + curl E = 0 on \\Omega + n x E = n x E_bc on \\partial \\Omega + + with Ampere discretized weakly and Faraday discretized strongly, in a broken-FEEC approach on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + (Eh) (Bh) + + Parameters + ---------- + nc : int + Number of cells (same along each direction) in every patch. + + deg : int + Polynomial degree (same along each direction) in every patch, for the + spline space V0 in H1. + + final_time : float + Final simulation time. Given that the speed of light is set to c=1, + this can be easily chosen based on the wave transit time in the domain. + + cfl_max : float + Maximum Courant parameter in the simulation domain, used to determine + the time step size. + + dt_max : float + Maximum time step size, which has to be met together with cfl_max. This + additional constraint is useful to resolve a time-dependent source. + + domain_name : str + Name of the multipatch geometry used in the simulation, to be chosen + among those available in the function `build_multipatch_domain`. + + backend : str + Name of the backend used for acceleration of the computational kernels, + to be chosen among the available keys of the PSYDAC_BACKENDS dict. + + source_type : str {'zero' | 'pulse' | 'cf_pulse' | 'Il_pulse'} + Name that identifies the space-time profile of the current source, to be + chosen among those available in the function get_source_and_solution(). + Available options: + - 'zero' : no current source + - 'pulse' : div-free current source, time-harmonic + - 'cf_pulse': curl-free current source, time-harmonic + - 'Il_pulse': Issautier-like pulse, with both a div-free and a + curl-free component, not time-harmonic. + + source_omega : float + Pulsation of the time-harmonic component (if any) of a time-dependent + current source. + + source_proj : str {'P_geom' | 'P_L2'} + Name of the approximation operator for the current source: 'P_geom' is + a geometric projector (based on inter/histopolation) which yields the + primal degrees of freedom; 'P_L2' is an L2 projector which yields the + dual degrees of freedom. Change of basis from primal to dual (and vice + versa) is obtained through multiplication with the proper Hodge matrix. + + conf_proj : str {'BSP' | 'GSP'} + Kind of conforming projection operator. Choose 'BSP' for an operator + based on the spline coefficients, which has maximum data locality. + Choose 'GSP' for an operator based on the geometric degrees of freedom, + which requires a change of basis (from B-spline to geometric, and then + vice versa) on the patch interfaces. + + gamma_h : float + Jump penalization parameter. + + project_sol : bool + Whether the solution fields should be projected onto the corresponding + conforming spaces before plotting them. + + filter_source : bool + If True, the current source will be filtered with the conforming + projector operator (or its dual, depending on which basis is used). + + quad_param : int + Multiplicative factor for the number of quadrature points; set + `quad_param` > 1 if you suspect that the quadrature is not accurate. + + E0_type : str {'zero', 'th_sol', 'pulse'} + Initial conditions for the electric field. Choose 'zero' for E0=0, + 'th_sol' for a field obtained from the time-harmonic Maxwell solver + (must provide a time-harmonic current source and set `source_omega`), + and 'pulse' for a non-zero field localized in a small region. + + E0_proj : str {'P_geom' | 'P_L2'} + Name of the approximation operator for the initial electric field E0 + (see source_proj for details). Only relevant if E0 is not zero. + + hide_plots : bool + If True, no windows are opened to show the figures interactively. + + plot_dir : str + Path to the directory where the figures will be saved. + + plot_time_ranges : list + List of lists, of the form `[[start, end], dtp]`, where `[start, end]` + is a time interval and `dtp` is the time between two successive plots. + + plot_source : bool + If True, plot the discrete field that approximates the current source. + + plot_divE : bool + If True, compute and plot the (weak) divergence of the electric field. + + diag_dt : float + Time elapsed between two successive calculations of scalar diagnostic + quantities. + + cb_min_sol : float + Minimum value to be used in colorbars when visualizing the solution. + + cb_max_sol : float + Maximum value to be used in colorbars when visualizing the solution. + + m_load_dir : str + Path to directory for matrix storage. + + th_sol_filename : str + Path to file with time-harmonic solution (to be used in conjuction with + `source_is_harmonic = True` and `E0_type = 'th_sol'`). + + """ + diags = {} + + # ncells = [nc, nc] + degree = [deg, deg] + + if source_omega is not None: + period_time = 2 * np.pi / source_omega + Nt_pp = period_time // dt_max + + if plot_time_ranges is None: + plot_time_ranges = [ + [[0, final_time], final_time] + ] + + if diag_dt is None: + diag_dt = 0.1 + + # if backend is None: + # if domain_name in ['pretzel', 'pretzel_f'] and nc > 8: + # backend = 'numba' + # else: + # backend = 'python' + # print('[note: using '+backend_language+ ' backends in discretize functions]') + if m_load_dir is not None: + if not os.path.exists(m_load_dir): + os.makedirs(m_load_dir) + + print('---------------------------------------------------------------------------------------------------------') + print('Starting solve_td_maxwell_pbm function with: ') + print(' ncells = {}'.format(nc)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' E0_type = {}'.format(E0_type)) + print(' E0_proj = {}'.format(E0_proj)) + print(' source_type = {}'.format(source_type)) + print(' source_proj = {}'.format(source_proj)) + print(' backend = {}'.format(backend)) + # TODO: print other parameters + print('---------------------------------------------------------------------------------------------------------') + + debug = False + + print() + print(' -- building discrete spaces and operators --') + + t_stamp = time_count() + print(' .. multi-patch domain...') + if domain_name == 'refined_square' or domain_name == 'square_L_shape': + int_x, int_y = domain_lims + domain = build_cartesian_multipatch_domain(nc, int_x, int_y, mapping='identity') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(nc, int): + ncells = [nc, nc] + elif ncells.ndim == 1: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + elif ncells.ndim == 2: + ncells = {patch.name: [nc[int(patch.name[2])][int(patch.name[4])], + nc[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) + mappings_list = list(mappings.values()) + + # for diagnosttics + diag_grid = DiagGrid(mappings=mappings, N_diag=100) + + t_stamp = time_count(t_stamp) + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + domain_h = discretize(domain, ncells=ncells) + + t_stamp = time_count(t_stamp) + print(' .. discrete derham sequence...') + + derham_h = discretize(derham, domain_h, degree=degree) + + t_stamp = time_count(t_stamp) + print(' .. commuting projection operators...') + nquads = [4 * (d + 1) for d in degree] + P0, P1, P2 = derham_h.projectors(nquads=nquads) + + t_stamp = time_count(t_stamp) + print(' .. multi-patch spaces...') + V0h = derham_h.V0 + V1h = derham_h.V1 + V2h = derham_h.V2 + print('dim(V0h) = {}'.format(V0h.nbasis)) + print('dim(V1h) = {}'.format(V1h.nbasis)) + print('dim(V2h) = {}'.format(V2h.nbasis)) + diags['ndofs_V0'] = V0h.nbasis + diags['ndofs_V1'] = V1h.nbasis + diags['ndofs_V2'] = V2h.nbasis + + t_stamp = time_count(t_stamp) + print(' .. Id operator and matrix...') + I1 = IdLinearOperator(V1h) + I1_m = I1.to_sparse_matrix() + + t_stamp = time_count(t_stamp) + print(' .. Hodge operators...') + # multi-patch (broken) linear operators / matrices + # other option: define as Hodge Operators: + H0 = HodgeOperator( + V0h, + domain_h, + backend_language=backend, + load_dir=m_load_dir, + load_space_index=0) + H1 = HodgeOperator( + V1h, + domain_h, + backend_language=backend, + load_dir=m_load_dir, + load_space_index=1) + H2 = HodgeOperator( + V2h, + domain_h, + backend_language=backend, + load_dir=m_load_dir, + load_space_index=2) + + t_stamp = time_count(t_stamp) + print(' .. Hodge matrix H0_m = M0_m ...') + H0_m = H0.to_sparse_matrix() + t_stamp = time_count(t_stamp) + print(' .. dual Hodge matrix dH0_m = inv_M0_m ...') + dH0_m = H0.get_dual_Hodge_sparse_matrix() + + t_stamp = time_count(t_stamp) + print(' .. Hodge matrix H1_m = M1_m ...') + H1_m = H1.to_sparse_matrix() + t_stamp = time_count(t_stamp) + print(' .. dual Hodge matrix dH1_m = inv_M1_m ...') + dH1_m = H1.get_dual_Hodge_sparse_matrix() + + t_stamp = time_count(t_stamp) + print(' .. Hodge matrix dH2_m = M2_m ...') + H2_m = H2.to_sparse_matrix() + print(' .. dual Hodge matrix dH2_m = inv_M2_m ...') + dH2_m = H2.get_dual_Hodge_sparse_matrix() + + t_stamp = time_count(t_stamp) + print(' .. conforming Projection operators...') + cP0_m = construct_h1_conforming_projection(V0h, hom_bc=False) + cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=False) + + if conf_proj == 'GSP': + print(' [* GSP-conga: using Geometric Spline conf Projections ]') + K0, K0_inv = get_K0_and_K0_inv(V0h, uniform_patches=False) + cP0_m = K0_inv @ cP0_m @ K0 + K1, K1_inv = get_K1_and_K1_inv(V1h, uniform_patches=False) + cP1_m = K1_inv @ cP1_m @ K1 + elif conf_proj == 'BSP': + print(' [* BSP-conga: using B-Spline conf Projections ]') + else: + raise ValueError(conf_proj) + + t_stamp = time_count(t_stamp) + print(' .. broken differential operators...') + # broken (patch-wise) differential operators + bD0, bD1 = derham_h.broken_derivatives_as_operators + bD0_m = bD0.to_sparse_matrix() + bD1_m = bD1.to_sparse_matrix() + + if plot_dir is not None and not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + # Conga (projection-based) matrices + t_stamp = time_count(t_stamp) + dH1_m = dH1_m.tocsr() + H2_m = H2_m.tocsr() + cP1_m = cP1_m.tocsr() + bD1_m = bD1_m.tocsr() + + print(' .. matrix of the primal curl (in primal bases)...') + C_m = bD1_m @ cP1_m + print(' .. matrix of the dual curl (also in primal bases)...') + + from sympde.calculus import grad, dot, curl, cross + from sympde.topology import NormalVector + from sympde.expr.expr import BilinearForm + from sympde.topology import elements_of + + u, v = elements_of(derham.V1, names='u, v') + nn = NormalVector('nn') + boundary = domain.boundary + expr_b = cross(nn, u) * cross(nn, v) + + a = BilinearForm((u, v), integral(boundary, expr_b)) + ah = discretize(a, domain_h, [V1h, V1h], backend=PSYDAC_BACKENDS[backend],) + A_eps = ah.assemble().tosparse() + + dC_m = dH1_m @ C_m.transpose() @ H2_m + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Compute stable time step size based on max CFL and max dt + dt = compute_stable_dt(C_m=C_m, dC_m=dC_m, cfl_max=cfl_max, dt_max=dt_max) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Absorbing dC_m + CH2 = C_m.transpose() @ H2_m + H1A = H1_m + dt * A_eps + dC_m = sp.sparse.linalg.spsolve(H1A, CH2) + + dCH1_m = sp.sparse.linalg.spsolve(H1A, H1_m) + + print(' .. matrix of the dual div (still in primal bases)...') + div_m = dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m + + # jump stabilization (may not be needed) + t_stamp = time_count(t_stamp) + print(' .. jump stabilization matrix...') + jump_penal_m = I1_m - cP1_m + JP_m = jump_penal_m.transpose() * H1_m * jump_penal_m + + # t_stamp = time_count(t_stamp) + # print(' .. full operator matrix...') + # print('STABILIZATION: gamma_h = {}'.format(gamma_h)) + # pre_A_m = cP1_m.transpose() @ ( eta * H1_m + mu * pre_CC_m - nu * pre_GD_m ) # useful for the boundary condition (if present) + # A_m = pre_A_m @ cP1_m + gamma_h * JP_m + + print(" Reduce time step to match the simulation final time:") + Nt = int(np.ceil(final_time / dt)) + dt = final_time / Nt + print(f" . Time step size : dt = {dt}") + print(f" . Nb of time steps: Nt = {Nt}") + + # ... + def is_plotting_time(nt, *, dt=dt, Nt=Nt, + plot_time_ranges=plot_time_ranges): + if nt in [0, Nt]: + return True + for [start, end], dt_plots in plot_time_ranges: + # number of time steps between two successive plots + ds = max(dt_plots // dt, 1) + if (start <= nt * dt <= end) and (nt % ds == 0): + return True + return False + # ... + + # Number of time step between two successive calculations of the scalar + # diagnostics + diag_nt = max(int(diag_dt // dt), 1) + + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print( + ' total nb of time steps: Nt = {}, final time: T = {:5.4f}'.format( + Nt, + final_time)) + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' plotting times: the solution will be plotted for...') + for nt in range(Nt + 1): + if is_plotting_time(nt): + print(' * nt = {}, t = {:5.4f}'.format(nt, dt * nt)) + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # source + + t_stamp = time_count(t_stamp) + print() + print(' -- getting source --') + f0_c = None + f0_harmonic_c = None + if source_type == 'zero': + + f0 = None + f0_harmonic = None + + elif source_type == 'pulse': + + f0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + + elif source_type == 'cf_pulse': + + f0 = get_curl_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + + elif source_type == 'Il_pulse': # Issautier-like pulse + # source will be + # J = curl A + cos(om*t) * grad phi + # so that + # dt rho = - div J = - cos(om*t) Delta phi + # for instance, with rho(t=0) = 0 this gives + # rho = - sin(om*t)/om * Delta phi + # and Gauss' law reads + # div E = rho = - sin(om*t)/om * Delta phi + f0 = get_div_free_pulse( + x_0=1.0, y_0=1.0, domain=domain) # this is curl A + f0_harmonic = get_curl_free_pulse( + x_0=1.0, y_0=1.0, domain=domain) # this is grad phi + assert not source_is_harmonic + + rho0 = get_Delta_phi_pulse( + x_0=1.0, y_0=1.0, domain=domain) # this is Delta phi + tilde_rho0_c = derham_h.get_dual_dofs( + space='V0', + f=rho0, + backend_language=backend, + return_format='numpy_array') + tilde_rho0_c = cP0_m.transpose() @ tilde_rho0_c + rho0_c = dH0_m.dot(tilde_rho0_c) + else: + + f0, u_bc, u_ex, curl_u_ex, div_u_ex = get_source_and_solution_hcurl( + source_type=source_type, domain=domain, domain_name=domain_name, + ) + assert u_bc is None # only homogeneous BC's for now + + # f0_c = np.zeros(V1h.nbasis) + + if source_omega is not None: + f0_harmonic = f0 + f0 = None + if E0_type == 'th_sol': + # use source enveloppe for smooth transition from 0 to 1 + def source_enveloppe(tau): + return (special.erf((tau / 25) - 2) - special.erf(-2)) / 2 + else: + def source_enveloppe(tau): + return 1 + + t_stamp = time_count(t_stamp) + tilde_f0_c = f0_c = None + tilde_f0_harmonic_c = f0_harmonic_c = None + if source_proj == 'P_geom': + print(' .. projecting the source with commuting projection...') + if f0 is not None: + f0_h = P1_phys(f0, P1, domain, mappings_list) + f0_c = f0_h.coeffs.toarray() + tilde_f0_c = H1_m.dot(f0_c) + if f0_harmonic is not None: + f0_harmonic_h = P1_phys(f0_harmonic, P1, domain, mappings_list) + f0_harmonic_c = f0_harmonic_h.coeffs.toarray() + tilde_f0_harmonic_c = H1_m.dot(f0_harmonic_c) + + elif source_proj == 'P_L2': + # helper: save/load coefs + if f0 is not None: + if source_type == 'Il_pulse': + source_name = 'Il_pulse_f0' + else: + source_name = source_type + sdd_filename = m_load_dir + '/' + source_name + \ + '_dual_dofs_qp{}.npy'.format(quad_param) + if os.path.exists(sdd_filename): + print( + ' .. loading source dual dofs from file {}'.format(sdd_filename)) + tilde_f0_c = np.load(sdd_filename) + else: + print(' .. projecting the source f0 with L2 projection...') + tilde_f0_c = derham_h.get_dual_dofs( + space='V1', f=f0, backend_language=backend, return_format='numpy_array') + print(' .. saving source dual dofs to file {}'.format(sdd_filename)) + np.save(sdd_filename, tilde_f0_c) + if f0_harmonic is not None: + if source_type == 'Il_pulse': + source_name = 'Il_pulse_f0_harmonic' + else: + source_name = source_type + sdd_filename = m_load_dir + '/' + source_name + \ + '_dual_dofs_qp{}.npy'.format(quad_param) + if os.path.exists(sdd_filename): + print( + ' .. loading source dual dofs from file {}'.format(sdd_filename)) + tilde_f0_harmonic_c = np.load(sdd_filename) + else: + print(' .. projecting the source f0_harmonic with L2 projection...') + tilde_f0_harmonic_c = derham_h.get_dual_dofs( + space='V1', f=f0_harmonic, backend_language=backend, return_format='numpy_array') + print(' .. saving source dual dofs to file {}'.format(sdd_filename)) + np.save(sdd_filename, tilde_f0_harmonic_c) + + else: + raise ValueError(source_proj) + + t_stamp = time_count(t_stamp) + if filter_source: + print(' .. filtering the source...') + if tilde_f0_c is not None: + tilde_f0_c = cP1_m.transpose() @ tilde_f0_c + if tilde_f0_harmonic_c is not None: + tilde_f0_harmonic_c = cP1_m.transpose() @ tilde_f0_harmonic_c + + if tilde_f0_c is not None: + f0_c = dH1_m.dot(tilde_f0_c) + + if debug: + title = 'f0 part of source' + params_str = 'omega={}_gamma_h={}_Pf={}'.format( + source_omega, gamma_h, source_proj) + plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_f0.pdf', + plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_f0_vf.pdf', + plot_type='vector_field', cb_min=None, cb_max=None, hide_plot=hide_plots) + divf0_c = div_m @ f0_c + title = 'div f0' + plot_field(numpy_coeffs=divf0_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_divf0.pdf', + plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + + if tilde_f0_harmonic_c is not None: + f0_harmonic_c = dH1_m.dot(tilde_f0_harmonic_c) + + if debug: + title = 'f0_harmonic part of source' + params_str = 'omega={}_gamma_h={}_Pf={}'.format( + source_omega, gamma_h, source_proj) + plot_field(numpy_coeffs=f0_harmonic_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_f0_harmonic.pdf', + plot_type='components', cb_min=None, cb_max=None, hide_plot=hide_plots) + plot_field(numpy_coeffs=f0_harmonic_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_f0_harmonic_vf.pdf', + plot_type='vector_field', cb_min=None, cb_max=None, hide_plot=hide_plots) + divf0_c = div_m @ f0_harmonic_c + title = 'div f0_harmonic' + plot_field(numpy_coeffs=divf0_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + '_divf0_harmonic.pdf', + plot_type='components', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + + # else: + # raise NotImplementedError + + if f0_c is None: + f0_c = np.zeros(V1h.nbasis) + + # if plot_source and plot_dir: + # plot_field(numpy_coeffs=f0_c, Vh=V1h, space_kind='hcurl', domain=domain, title='f0_h with P = '+source_proj, filename=plot_dir+'/f0h_'+source_proj+'.png', hide_plot=hide_plots) + # plot_field(numpy_coeffs=f0_c, Vh=V1h, plot_type='vector_field', space_kind='hcurl', domain=domain, title='f0_h with P = '+source_proj, filename=plot_dir+'/f0h_'+source_proj+'_vf.png', hide_plot=hide_plots) + + t_stamp = time_count(t_stamp) + + def plot_J_source_nPlusHalf(f_c, nt): + print(' .. plotting the source...') + title = r'source $J^{n+1/2}_h$ (amplitude)' + \ + ' for $\\omega = {}$, $n = {}$'.format(source_omega, nt) + params_str = 'omega={}_gamma_h={}_Pf={}'.format( + source_omega, gamma_h, source_proj) + plot_field(numpy_coeffs=f_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + + '_Jh_nt={}.pdf'.format(nt), + plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + title = r'source $J^{n+1/2}_h$' + \ + ' for $\\omega = {}$, $n = {}$'.format(source_omega, nt) + plot_field(numpy_coeffs=f_c, Vh=V1h, space_kind='hcurl', domain=domain, title=title, + filename=plot_dir + '/' + params_str + + '_Jh_vf_nt={}.pdf'.format(nt), + plot_type='vector_field', vf_skip=1, hide_plot=hide_plots) + + def plot_E_field(E_c, nt, project_sol=False, plot_divE=False): + + # only E for now + if plot_dir: + + plot_omega_normalized_sol = (source_omega is not None) + # project the homogeneous solution on the conforming problem space + if project_sol: + # t_stamp = time_count(t_stamp) + print( + ' .. projecting the homogeneous solution on the conforming problem space...') + Ep_c = cP1_m.dot(E_c) + else: + Ep_c = E_c + print( + ' .. NOT projecting the homogeneous solution on the conforming problem space') + if plot_omega_normalized_sol: + print(' .. plotting the E/omega field...') + u_c = (1 / source_omega) * Ep_c + title = r'$u_h = E_h/\omega$ (amplitude) for $\omega = {:5.4f}$, $t = {:5.4f}$'.format( + source_omega, dt * nt) + params_str = 'omega={:5.4f}_gamma_h={}_Pf={}_Nt_pp={}'.format( + source_omega, gamma_h, source_proj, Nt_pp) + else: + print(' .. plotting the E field...') + if E0_type == 'pulse': + title = r'$t = {:5.4f}$'.format(dt * nt) + else: + title = r'$E_h$ (amplitude) at $t = {:5.4f}$'.format( + dt * nt) + u_c = Ep_c + params_str = f'gamma_h={gamma_h}_dt={dt}' + + plot_field(numpy_coeffs=u_c, Vh=V1h, space_kind='hcurl', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + + '_Eh_nt={}.pdf'.format(nt), + plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + + if plot_divE: + params_str = f'gamma_h={gamma_h}_dt={dt}' + if source_type == 'Il_pulse': + plot_type = 'components' + rho_c = rho0_c * \ + np.sin(source_omega * dt * nt) / source_omega + rho_norm2 = np.dot(rho_c, H0_m.dot(rho_c)) + title = r'$\rho_h$ at $t = {:5.4f}, norm = {}$'.format( + dt * nt, np.sqrt(rho_norm2)) + plot_field(numpy_coeffs=rho_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + + '_rho_nt={}.pdf'.format(nt), + plot_type=plot_type, cb_min=None, cb_max=None, hide_plot=hide_plots) + else: + plot_type = 'amplitude' + + divE_c = div_m @ Ep_c + divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) + if project_sol: + title = r'div $P^1_h E_h$ at $t = {:5.4f}, norm = {}$'.format( + dt * nt, np.sqrt(divE_norm2)) + else: + title = r'div $E_h$ at $t = {:5.4f}, norm = {}$'.format( + dt * nt, np.sqrt(divE_norm2)) + plot_field(numpy_coeffs=divE_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + + '_divEh_nt={}.pdf'.format(nt), + plot_type=plot_type, cb_min=None, cb_max=None, hide_plot=hide_plots) + + else: + print(' -- WARNING: unknown plot_dir !!') + + def plot_B_field(B_c, nt): + + if plot_dir: + + print(' .. plotting B field...') + params_str = f'gamma_h={gamma_h}_dt={dt}' + + title = r'$B_h$ (amplitude) for $t = {:5.4f}$'.format(dt * nt) + plot_field(numpy_coeffs=B_c, Vh=V2h, space_kind='l2', domain=domain, surface_plot=False, title=title, + filename=plot_dir + '/' + params_str + + '_Bh_nt={}.pdf'.format(nt), + plot_type='amplitude', cb_min=cb_min_sol, cb_max=cb_max_sol, hide_plot=hide_plots) + + else: + print(' -- WARNING: unknown plot_dir !!') + + def plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start, nt_end, + GaussErr_norm2_diag=None, GaussErrP_norm2_diag=None, + PE_norm2_diag=None, I_PE_norm2_diag=None, J_norm2_diag=None, skip_titles=True): + + nt_start = max(nt_start, 0) + nt_end = min(nt_end, Nt) + + td = time_diag[nt_start:nt_end + 1] + t_label = r'$t$' + + # norm || E || + fig, ax = plt.subplots() + ax.plot(td, + np.sqrt(E_norm2_diag[nt_start:nt_end + 1]), + '-', + ms=7, + mfc='None', + mec='k') # , label='||E||', zorder=10) + if skip_titles: + title = '' + else: + title = r'$||E_h(t)||$ vs ' + t_label + ax.set_xlabel(t_label, fontsize=16) + ax.set_title(title, fontsize=18) + fig.tight_layout() + diag_fn = plot_dir + \ + f'/diag_E_norm_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start}, {dt*nt_end}].pdf' + print(f"saving plot for '{title}' in figure '{diag_fn}") + fig.savefig(diag_fn) + + # energy + fig, ax = plt.subplots() + E_energ = .5 * E_norm2_diag[nt_start:nt_end + 1] + B_energ = .5 * B_norm2_diag[nt_start:nt_end + 1] + ax.plot(td, E_energ, '-', ms=7, mfc='None', c='k', + label=r'$\frac{1}{2}||E||^2$') # , zorder=10) + ax.plot(td, B_energ, '-', ms=7, mfc='None', c='g', + label=r'$\frac{1}{2}||B||^2$') # , zorder=10) + ax.plot(td, E_energ + B_energ, '-', ms=7, mfc='None', c='b', + label=r'$\frac{1}{2}(||E||^2+||B||^2)$') # , zorder=10) + ax.legend(loc='best') + if skip_titles: + title = '' + else: + title = r'energy vs ' + t_label + if E0_type == 'pulse': + ax.set_ylim([0, 5]) + ax.set_xlabel(t_label, fontsize=16) + ax.set_title(title, fontsize=18) + fig.tight_layout() + diag_fn = plot_dir + \ + f'/diag_energy_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start},{dt*nt_end}].pdf' + print(f"saving plot for '{title}' in figure '{diag_fn}") + fig.savefig(diag_fn) + + # One curve per plot from now on. + # Collect information in a list where each item is of the form [tag, + # data, title] + time_diagnostics = [] + + if project_sol: + time_diagnostics += [['divPE', divE_norm2_diag, + r'$||div_h P^1_h E_h(t)||$ vs ' + t_label]] + else: + time_diagnostics += [['divE', divE_norm2_diag, + r'$||div_h E_h(t)||$ vs ' + t_label]] + + time_diagnostics += [ + ['I_PE', I_PE_norm2_diag, r'$||(I-P^1)E_h(t)||$ vs ' + t_label], + ['PE', PE_norm2_diag, r'$||(I-P^1)E_h(t)||$ vs ' + t_label], + ['GaussErr', GaussErr_norm2_diag, + r'$||(\rho_h - div_h E_h)(t)||$ vs ' + t_label], + ['GaussErrP', GaussErrP_norm2_diag, + r'$||(\rho_h - div_h E_h)(t)||$ vs ' + t_label], + ['J_norm', J_norm2_diag, r'$||J_h(t)||$ vs ' + t_label], + ] + + for tag, data, title in time_diagnostics: + if data is None: + continue + fig, ax = plt.subplots() + ax.plot(td, + np.sqrt(I_PE_norm2_diag[nt_start:nt_end + 1]), + '-', + ms=7, + mfc='None', + mec='k') # , label='||E||', zorder=10) + diag_fn = plot_dir + \ + f'/diag_{tag}_gamma={gamma_h}_dt={dt}_trange=[{dt*nt_start},{dt*nt_end}].pdf' + ax.set_xlabel(t_label, fontsize=16) + if not skip_titles: + ax.set_title(title, fontsize=18) + fig.tight_layout() + print(f"saving plot for '{title}' in figure '{diag_fn}") + fig.savefig(diag_fn) + + # diags arrays + E_norm2_diag = np.zeros(Nt + 1) + B_norm2_diag = np.zeros(Nt + 1) + divE_norm2_diag = np.zeros(Nt + 1) + time_diag = np.zeros(Nt + 1) + PE_norm2_diag = np.zeros(Nt + 1) + I_PE_norm2_diag = np.zeros(Nt + 1) + J_norm2_diag = np.zeros(Nt + 1) + if source_type == 'Il_pulse': + GaussErr_norm2_diag = np.zeros(Nt + 1) + GaussErrP_norm2_diag = np.zeros(Nt + 1) + else: + GaussErr_norm2_diag = None + GaussErrP_norm2_diag = None + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # initial solution + + print(' .. initial solution ..') + + # initial B sol + B_c = np.zeros(V2h.nbasis) + + # initial E sol + if E0_type == 'th_sol': + + if os.path.exists(th_sol_filename): + print( + ' .. loading time-harmonic solution from file {}'.format(th_sol_filename)) + E_c = source_omega * np.load(th_sol_filename) + assert len(E_c) == V1h.nbasis + else: + print( + ' .. Error: time-harmonic solution file given {}, but not found'.format(th_sol_filename)) + raise ValueError(th_sol_filename) + + elif E0_type == 'zero': + E_c = np.zeros(V1h.nbasis) + + elif E0_type == 'pulse': + + E0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + + if E0_proj == 'P_geom': + print(' .. projecting E0 with commuting projection...') + E0_h = P1_phys(E0, P1, domain, mappings_list) + E_c = E0_h.coeffs.toarray() + + elif E0_proj == 'P_L2': + # helper: save/load coefs + E0dd_filename = m_load_dir + \ + '/E0_pulse_dual_dofs_qp{}.npy'.format(quad_param) + if os.path.exists(E0dd_filename): + print(' .. loading E0 dual dofs from file {}'.format(E0dd_filename)) + tilde_E0_c = np.load(E0dd_filename) + else: + print(' .. projecting E0 with L2 projection...') + tilde_E0_c = derham_h.get_dual_dofs( + space='V1', f=E0, backend_language=backend, return_format='numpy_array') + print(' .. saving E0 dual dofs to file {}'.format(E0dd_filename)) + np.save(E0dd_filename, tilde_E0_c) + E_c = dH1_m.dot(tilde_E0_c) + + elif E0_type == 'pulse_2': + # E0 = get_praxial_Gaussian_beam_E(x_0=3.14, y_0=3.14, domain=domain) + + # E0 = get_easy_Gaussian_beam_E_2(x_0=0.05, y_0=0.05, domain=domain) + # B0 = get_easy_Gaussian_beam_B_2(x_0=0.05, y_0=0.05, domain=domain) + + E0, B0 = get_Gaussian_beam(y_0=3.14, x_0=3.14, domain=domain) + # B0 = get_easy_Gaussian_beam_B(x_0=3.14, y_0=0.05, domain=domain) + + if E0_proj == 'P_geom': + print(' .. projecting E0 with commuting projection...') + + E0_h = P1_phys(E0, P1, domain, mappings_list) + E_c = E0_h.coeffs.toarray() + + # B_c = np.real( - 1j * C_m @ E_c) + # E_c = np.real(E_c) + B0_h = P2_phys(B0, P2, domain, mappings_list) + B_c = B0_h.coeffs.toarray() + + elif E0_proj == 'P_L2': + # helper: save/load coefs + E0dd_filename = m_load_dir + \ + '/E0_pulse_dual_dofs_qp{}.npy'.format(quad_param) + if False: # os.path.exists(E0dd_filename): + print(' .. loading E0 dual dofs from file {}'.format(E0dd_filename)) + tilde_E0_c = np.load(E0dd_filename) + else: + print(' .. projecting E0 with L2 projection...') + + tilde_E0_c = derham_h.get_dual_dofs( + space='V1', f=E0, backend_language=backend, return_format='numpy_array') + print(' .. saving E0 dual dofs to file {}'.format(E0dd_filename)) + # np.save(E0dd_filename, tilde_E0_c) + + E_c = dH1_m.dot(tilde_E0_c) + dH2_m = H2.get_dual_sparse_matrix() + tilde_B0_c = derham_h.get_dual_dofs( + space='V2', f=B0, backend_language=backend, return_format='numpy_array') + B_c = dH2_m.dot(tilde_B0_c) + + # B_c = np.real( - C_m @ E_c) + # E_c = np.real(E_c) + else: + raise ValueError(E0_type) + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # time loop + + def compute_diags(E_c, B_c, J_c, nt): + time_diag[nt] = (nt) * dt + PE_c = cP1_m.dot(E_c) + I_PE_c = E_c - PE_c + E_norm2_diag[nt] = np.dot(E_c, H1_m.dot(E_c)) + PE_norm2_diag[nt] = np.dot(PE_c, H1_m.dot(PE_c)) + I_PE_norm2_diag[nt] = np.dot(I_PE_c, H1_m.dot(I_PE_c)) + J_norm2_diag[nt] = np.dot(J_c, H1_m.dot(J_c)) + B_norm2_diag[nt] = np.dot(B_c, H2_m.dot(B_c)) + divE_c = div_m @ E_c + divE_norm2_diag[nt] = np.dot(divE_c, H0_m.dot(divE_c)) + if source_type == 'Il_pulse': + rho_c = rho0_c * np.sin(source_omega * nt * dt) / omega + GaussErr = rho_c - divE_c + GaussErrP = rho_c - div_m @ PE_c + GaussErr_norm2_diag[nt] = np.dot(GaussErr, H0_m.dot(GaussErr)) + GaussErrP_norm2_diag[nt] = np.dot(GaussErrP, H0_m.dot(GaussErrP)) + + if plot_dir: + OM1 = OutputManager(plot_dir + '/spaces1.yml', plot_dir + '/fields1.h5') + OM1.add_spaces(V1h=V1h) + OM1.export_space_info() + + OM2 = OutputManager(plot_dir + '/spaces2.yml', plot_dir + '/fields2.h5') + OM2.add_spaces(V2h=V2h) + OM2.export_space_info() + + stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.vector_space) + Eh = FemField(V1h, coeffs=stencil_coeffs_E) + OM1.add_snapshot(t=0, ts=0) + OM1.export_fields(Eh=Eh) + + stencil_coeffs_B = array_to_psydac(B_c, V2h.vector_space) + Bh = FemField(V2h, coeffs=stencil_coeffs_B) + OM2.add_snapshot(t=0, ts=0) + OM2.export_fields(Bh=Bh) + + # PM = PostProcessManager(domain=domain, space_file=plot_dir+'/spaces1.yml', fields_file=plot_dir+'/fields1.h5' ) + # PM.export_to_vtk(plot_dir+"/Eh",grid=None, npts_per_cell=[6]*2, snapshots='all', fields='vh' ) + + # OM1.close() + # PM.close() + + # plot_E_field(E_c, nt=0, project_sol=project_sol, plot_divE=plot_divE) + # plot_B_field(B_c, nt=0) + + f_c = np.copy(f0_c) + for nt in range(Nt): + print(' .. nt+1 = {}/{}'.format(nt + 1, Nt)) + + # 1/2 faraday: Bn -> Bn+1/2 + B_c[:] -= (dt / 2) * C_m @ E_c + + # ampere: En -> En+1 + if f0_harmonic_c is not None: + f_harmonic_c = f0_harmonic_c * (np.sin(source_omega * (nt + 1) * dt) - np.sin( + source_omega * (nt) * dt)) / (dt * source_omega) # * source_enveloppe(omega*(nt+1/2)*dt) + f_c[:] = f0_c + f_harmonic_c + + if nt == 0: + if plot_dir: + plot_J_source_nPlusHalf(f_c, nt=0) + compute_diags(E_c, B_c, f_c, nt=0) + + E_c[:] = dCH1_m @ E_c + dt * (dC_m @ B_c - f_c) + + # if abs(gamma_h) > 1e-10: + # E_c[:] -= dt * gamma_h * JP_m @ E_c + + # 1/2 faraday: Bn+1/2 -> Bn+1 + B_c[:] -= (dt / 2) * C_m @ E_c + + # diags: + compute_diags(E_c, B_c, f_c, nt=nt + 1) + + # PE_c = cP1_m.dot(E_c) + # I_PE_c = E_c-PE_c + # E_norm2_diag[nt+1] = np.dot(E_c,H1_m.dot(E_c)) + # PE_norm2_diag[nt+1] = np.dot(PE_c,H1_m.dot(PE_c)) + # I_PE_norm2_diag[nt+1] = np.dot(I_PE_c,H1_m.dot(I_PE_c)) + # B_norm2_diag[nt+1] = np.dot(B_c,H2_m.dot(B_c)) + # time_diag[nt+1] = (nt+1)*dt + + # diags: div + # if project_sol: + # Ep_c = PE_c # = cP1_m.dot(E_c) + # else: + # Ep_c = E_c + # divE_c = div_m @ Ep_c + # divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) + # # print('in diag[{}]: divE_norm = {}'.format(nt+1, np.sqrt(divE_norm2))) + # divE_norm2_diag[nt+1] = divE_norm2 + + # if source_type == 'Il_pulse': + # rho_c = rho0_c * np.sin(omega*dt*(nt+1))/omega + # GaussErr = rho_c - div_m @ E_c + # GaussErrP = rho_c - div_m @ (cP1_m.dot(E_c)) + # GaussErr_norm2_diag[nt+1] = np.dot(GaussErr, H0_m.dot(GaussErr)) + # GaussErrP_norm2_diag[nt+1] = np.dot(GaussErrP, H0_m.dot(GaussErrP)) + + if debug: + divCB_c = div_m @ dC_m @ B_c + divCB_norm2 = np.dot(divCB_c, H0_m.dot(divCB_c)) + print('-- [{}]: dt*|| div CB || = {}'.format(nt + + 1, dt * np.sqrt(divCB_norm2))) + + divf_c = div_m @ f_c + divf_norm2 = np.dot(divf_c, H0_m.dot(divf_c)) + print('-- [{}]: dt*|| div f || = {}'.format(nt + + 1, dt * np.sqrt(divf_norm2))) + + divE_c = div_m @ E_c + divE_norm2 = np.dot(divE_c, H0_m.dot(divE_c)) + print('-- [{}]: || div E || = {}'.format(nt + 1, np.sqrt(divE_norm2))) + + if is_plotting_time(nt + 1) and plot_dir: + print("Plot Stuff") + # plot_E_field(E_c, nt=nt+1, project_sol=True, plot_divE=False) + # plot_B_field(B_c, nt=nt+1) + # plot_J_source_nPlusHalf(f_c, nt=nt) + + stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.vector_space) + Eh = FemField(V1h, coeffs=stencil_coeffs_E) + OM1.add_snapshot(t=nt * dt, ts=nt) + OM1.export_fields(Eh=Eh) + + stencil_coeffs_B = array_to_psydac(B_c, V2h.vector_space) + Bh = FemField(V2h, coeffs=stencil_coeffs_B) + OM2.add_snapshot(t=nt * dt, ts=nt) + OM2.export_fields(Bh=Bh) + + # if (nt+1) % diag_nt == 0: + # plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start=(nt+1)-diag_nt, nt_end=(nt+1), + # PE_norm2_diag=PE_norm2_diag, I_PE_norm2_diag=I_PE_norm2_diag, J_norm2_diag=J_norm2_diag, + # GaussErr_norm2_diag=GaussErr_norm2_diag, + # GaussErrP_norm2_diag=GaussErrP_norm2_diag) + if plot_dir: + OM1.close() + + print("Do some PP") + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + + '/spaces1.yml', + fields_file=plot_dir + + '/fields1.h5') + PM.export_to_vtk( + plot_dir + "/Eh", + grid=None, + npts_per_cell=2, + snapshots='all', + fields='Eh') + PM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + + '/spaces2.yml', + fields_file=plot_dir + + '/fields2.h5') + PM.export_to_vtk( + plot_dir + "/Bh", + grid=None, + npts_per_cell=2, + snapshots='all', + fields='Bh') + PM.close() + + # plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_start=0, nt_end=Nt, + # PE_norm2_diag=PE_norm2_diag, I_PE_norm2_diag=I_PE_norm2_diag, J_norm2_diag=J_norm2_diag, + # GaussErr_norm2_diag=GaussErr_norm2_diag, + # GaussErrP_norm2_diag=GaussErrP_norm2_diag) + + # Eh = FemField(V1h, coeffs=array_to_stencil(E_c, V1h.vector_space)) + # t_stamp = time_count(t_stamp) + + # if sol_filename: + # raise NotImplementedError + # print(' .. saving final solution coeffs to file {}'.format(sol_filename)) + # np.save(sol_filename, E_c) + + # time_count(t_stamp) + + # print() + # print(' -- plots and diagnostics --') + + # # diagnostics: errors + # err_diags = diag_grid.get_diags_for(v=uh, space='V1') + # for key, value in err_diags.items(): + # diags[key] = value + + # if u_ex is not None: + # check_diags = get_Vh_diags_for(v=uh, v_ref=uh_ref, M_m=H1_m, msg='error between Ph(u_ex) and u_h') + # diags['norm_Pu_ex'] = check_diags['sol_ref_norm'] + # diags['rel_l2_error_in_Vh'] = check_diags['rel_l2_error'] + + # if curl_u_ex is not None: + # print(' .. diag on curl_u:') + # curl_uh_c = bD1_m @ cP1_m @ uh_c + # title = r'curl $u_h$ (amplitude) for $\eta = $'+repr(eta) + # params_str = 'eta={}_mu={}_nu={}_gamma_h={}_Pf={}'.format(eta, mu, nu, gamma_h, source_proj) + # plot_field(numpy_coeffs=curl_uh_c, Vh=V2h, space_kind='l2', domain=domain, surface_plot=False, title=title, filename=plot_dir+'/'+params_str+'_curl_uh.png', + # plot_type='amplitude', cb_min=None, cb_max=None, hide_plot=hide_plots) + + # curl_uh = FemField(V2h, coeffs=array_to_stencil(curl_uh_c, V2h.vector_space)) + # curl_diags = diag_grid.get_diags_for(v=curl_uh, space='V2') + # diags['curl_error (to be checked)'] = curl_diags['rel_l2_error'] + + # title = r'div_h $u_h$ (amplitude) for $\eta = $'+repr(eta) + # params_str = 'eta={}_mu={}_nu={}_gamma_h={}_Pf={}'.format(eta, mu, nu, gamma_h, source_proj) + # plot_field(numpy_coeffs=div_uh_c, Vh=V0h, space_kind='h1', domain=domain, surface_plot=False, title=title, filename=plot_dir+'/'+params_str+'_div_uh.png', + # plot_type='amplitude', cb_min=None, cb_max=None, hide_plot=hide_plots) + + # div_uh = FemField(V0h, coeffs=array_to_stencil(div_uh_c, V0h.vector_space)) + # div_diags = diag_grid.get_diags_for(v=div_uh, space='V0') + # diags['div_error (to be checked)'] = div_diags['rel_l2_error'] + + return diags + + +# def compute_stable_dt(cfl_max, dt_max, C_m, dC_m, V1_dim): +def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): + """ + Compute a stable time step size based on the maximum CFL parameter in the + domain. To this end we estimate the operator norm of + + `dC_m @ C_m: V1h -> V1h`, + + find the largest stable time step compatible with Strang splitting, and + rescale it by the provided `cfl_max`. Setting `cfl_max = 1` would run the + scheme exactly at its stability limit, which is not safe because of the + unavoidable round-off errors. Hence we require `0 < cfl_max < 1`. + + Optionally the user can provide a maximum time step size in order to + properly resolve some time scales of interest (e.g. a time-dependent + current source). + + Parameters + ---------- + C_m : scipy.sparse.spmatrix + Matrix of the Curl operator. + + dC_m : scipy.sparse.spmatrix + Matrix of the dual Curl operator. + + cfl_max : float + Maximum Courant parameter in the domain, intended as a stability + parameter (=1 at the stability limit). Must be `0 < cfl_max < 1`. + + dt_max : float, optional + If not None, restrict the computed dt by this value in order to + properly resolve time scales of interest. Must be > 0. + + Returns + ------- + dt : float + Largest stable dt which satisfies the provided constraints. + + """ + + print(" .. compute_stable_dt by estimating the operator norm of ") + print(" .. dC_m @ C_m: V1h -> V1h ") + print(" .. with dim(V1h) = {} ...".format(C_m.shape[1])) + + if not (0 < cfl_max < 1): + print(' ****** ****** ****** ****** ****** ****** ') + print(' WARNING !!! cfl = {} '.format(cfl)) + print(' ****** ****** ****** ****** ****** ****** ') + + def vect_norm_2(vv): + return np.sqrt(np.dot(vv, vv)) + + t_stamp = time_count() + vv = np.random.random(C_m.shape[1]) + norm_vv = vect_norm_2(vv) + max_ncfl = 500 + ncfl = 0 + spectral_rho = 1 + conv = False + CC_m = dC_m @ C_m + + while not (conv or ncfl > max_ncfl): + + vv[:] = (1. / norm_vv) * vv + ncfl += 1 + vv[:] = CC_m.dot(vv) + + norm_vv = vect_norm_2(vv) + old_spectral_rho = spectral_rho + spectral_rho = vect_norm_2(vv) # approximation + conv = abs((spectral_rho - old_spectral_rho) / spectral_rho) < 0.001 + print(" ... spectral radius iteration: spectral_rho( dC_m @ C_m ) ~= {}".format(spectral_rho)) + t_stamp = time_count(t_stamp) + + norm_op = np.sqrt(spectral_rho) + c_dt_max = 2. / norm_op + + light_c = 1 + dt = cfl_max * c_dt_max / light_c + + if dt_max is not None: + dt = min(dt, dt_max) + + print(" Time step dt computed for Maxwell solver:") + print( + f" Based on cfl_max = {cfl_max} and dt_max = {dt_max}, we set dt = {dt}") + print( + f" -- note that c*Dt = {light_c*dt} and c_dt_max = {c_dt_max}, thus c * dt / c_dt_max = {light_c*dt/c_dt_max}") + print( + f" -- and spectral_radius((c*dt)**2* dC_m @ C_m ) = {(light_c * dt * norm_op)**2} (should be < 4).") + + return dt diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py new file mode 100644 index 000000000..19c1e13d6 --- /dev/null +++ b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py @@ -0,0 +1,275 @@ +""" + Runner script for solving the time-domain Maxwell problem. +""" + +import numpy as np + +from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm +from psydac.feec.multipatch.utilities import time_count, FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file + +t_stamp_full = time_count() + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# +# main test-cases and parameters used for the ppc paper: + +test_case = 'E0_pulse_no_source' # used in paper +# test_case = 'Issautier_like_source' # used in paper +# test_case = 'transient_to_harmonic' # actually, not used in paper + +# J_proj_case = 'P_geom' +J_proj_case = 'P_L2' +# J_proj_case = 'tilde Pi_1' + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +# Parameters to be changed in the batch run +deg = 3 + +# Common simulation parameters +# domain_name = 'square_6' +# ncells = [4,4,4,4,4,4] +# domain_name = 'pretzel_f' + +# non-conf domains +domain = [[0, 2 * np.pi], [0, 2 * np.pi]] # interval in x- and y-direction +domain_name = 'refined_square' +# use isotropic meshes (probably with a square domain) +# 4x8= 64 patches +# care for the transpose +ncells = np.array([[16, 16], + [16, 16]]) + +# ncells = np.array([[8,8,16,8], +# [8,8,16,8], +# [8,8,16,8], +# [8,8,16,8]]) +# ncells = np.array([[8,8,8,8], +# [8,8,8,8], +# [8,8,8,8], +# [8,8,8,8]]) +# ncells = np.array([[8,8,16,8,8,8], +# [8,8,16,8,8,8], +# [8,8,16,8,8,8], +# [8,8,16,8,8,8]]) + +# ncells = np.array([[4, 4, 4], +# [4, 8, 4], +# [8, 16, 8], +# [4, 8, 4], +# [4, 4, 4]]) +# ncells = np.array([[4, 4, 4, 4], +# [4, 8, 8, 4], +# [8, 16, 16, 8], +# [4, 8, 8, 4], +# [4, 4, 4, 4]]).transpose() +# ncells = np.array([[4, 4, 4, 4], +# [4, 4, 4, 4], +# [4, 8, 8, 4], +# [8, 16, 16, 8], +# [8, 16, 16, 8], +# [4, 8, 8, 4], +# [4, 4, 4, 4], +# [4, 4, 4, 4]]) + + +cfl_max = 0.8 +# 'P_geom' # projection used for initial E0 (B0 = 0 in all cases) +E0_proj = 'P_geom' +backend = 'pyccel-gcc' +project_sol = True # whether cP1 E_h is plotted instead of E_h +# multiplicative parameter for quadrature order in (bi)linear forms +# discretizaion +quad_param = 4 +gamma_h = 0 # jump dissipation parameter (not used in paper) +# 'BSP' # type of conforming projection operators (averaging B-spline or Geometric-splines coefficients) +conf_proj = 'GSP' +hide_plots = True +plot_divE = True +# time interval between scalar diagnostics (if None, compute every time step) +diag_dt = None + +# Parameters that depend on test case +if test_case == 'E0_pulse_no_source': + + E0_type = 'pulse_2' # non-zero initial conditions + source_type = 'zero' # no current source + source_omega = None + final_time = 9.02 # wave transit time in domain is > 4 + dt_max = None + plot_source = False + + plot_a_lot = True + if plot_a_lot: + plot_time_ranges = [[[0, final_time], 0.1]] + else: + plot_time_ranges = [ + [[0, 2], 0.1], + [[final_time - 1, final_time], 0.1], + ] + + cb_min_sol = 0 + cb_max_sol = 5 + +# TODO: check +elif test_case == 'Issautier_like_source': + + E0_type = 'zero' # zero initial conditions + source_type = 'Il_pulse' + source_omega = None + final_time = 20 + plot_source = True + dt_max = None + if deg_s == [3] and final_time == 20: + + plot_time_ranges = [ + [[1.9, 2], 0.1], + [[4.9, 5], 0.1], + [[9.9, 10], 0.1], + [[19.9, 20], 0.1], + ] + + # plot_time_ranges = [ + # ] + # if nc_s == [8]: + # Nt_pp = 10 + + cb_min_sol = 0 # None + cb_max_sol = 0.3 # None + +# TODO: check +elif test_case == 'transient_to_harmonic': + + E0_type = 'th_sol' + source_type = 'elliptic_J' + source_omega = np.sqrt(50) # source time pulsation + plot_source = True + + source_period = 2 * np.pi / source_omega + nb_t_periods = 100 + Nt_pp = 20 + + dt_max = source_period / Nt_pp + final_time = nb_t_periods * source_period + + plot_time_ranges = [ + [[(nb_t_periods - 2) * source_period, final_time], dt_max] + ] + + cb_min_sol = 0 + cb_max_sol = 1 + +else: + raise ValueError(test_case) + + +# projection used for the source J +if J_proj_case == 'P_geom': + source_proj = 'P_geom' + filter_source = False + +elif J_proj_case == 'P_L2': + source_proj = 'P_L2' + filter_source = False + +elif J_proj_case == 'tilde Pi_1': + source_proj = 'P_L2' + filter_source = True + +else: + raise ValueError(J_proj_case) + +case_dir = 'nov14_' + test_case + '_J_proj=' + \ + J_proj_case + '_qp{}'.format(quad_param) +if filter_source: + case_dir += '_Jfilter' +else: + case_dir += '_Jnofilter' +if not project_sol: + case_dir += '_E_noproj' + +if source_omega is not None: + case_dir += f'_omega={source_omega}' + +case_dir += f'_tend={final_time}' + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +common_diag_filename = './' + case_dir + '_diags.txt' + + +run_dir = get_run_dir( + domain_name, + sum(ncells), + deg, + source_type=source_type, + conf_proj=conf_proj) +plot_dir = get_plot_dir(case_dir, run_dir) +diag_filename = plot_dir + '/' + \ + diag_fn(source_type=source_type, source_proj=source_proj) + +# to save and load matrices +m_load_dir = get_mat_dir(domain_name, sum(ncells), deg, quad_param=quad_param) + +if E0_type == 'th_sol': + # initial E0 will be loaded from time-harmonic FEM solution + th_case_dir = 'maxwell_hom_eta=50' + th_sol_dir = get_sol_dir(th_case_dir, domain_name, sum(ncells), deg) + th_sol_filename = th_sol_dir + '/' + \ + FEM_sol_fn(source_type=source_type, source_proj=source_proj) +else: + # no initial solution to load + th_sol_filename = '' + +params = { + 'nc': ncells, + 'deg': deg, + 'final_time': final_time, + 'cfl_max': cfl_max, + 'dt_max': dt_max, + 'domain_name': domain_name, + 'backend': backend, + 'source_type': source_type, + 'source_omega': source_omega, + 'source_proj': source_proj, + 'conf_proj': conf_proj, + 'gamma_h': gamma_h, + 'project_sol': project_sol, + 'filter_source': filter_source, + 'quad_param': quad_param, + 'E0_type': E0_type, + 'E0_proj': E0_proj, + 'hide_plots': hide_plots, + 'plot_dir': plot_dir, + 'plot_time_ranges': plot_time_ranges, + 'plot_source': plot_source, + 'plot_divE': plot_divE, + 'diag_dt': diag_dt, + 'cb_min_sol': cb_min_sol, + 'cb_max_sol': cb_max_sol, + 'm_load_dir': m_load_dir, + 'th_sol_filename': th_sol_filename, + 'domain_lims': domain +} + +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') +print(' Calling solve_td_maxwell_pbm() with params = {}'.format(params)) +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + +diags = solve_td_maxwell_pbm(**params) + +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=diag_filename, + params=params) +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=common_diag_filename, + params=params) + +time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/multipatch_domain_utilities.py b/psydac/feec/multipatch/multipatch_domain_utilities.py index 09a3ff06f..8ded13651 100644 --- a/psydac/feec/multipatch/multipatch_domain_utilities.py +++ b/psydac/feec/multipatch/multipatch_domain_utilities.py @@ -5,13 +5,20 @@ import numpy as np from sympde.topology import Square, Domain -from sympde.topology import IdentityMapping, PolarMapping, AffineMapping, Mapping #TransposedPolarMapping +# TransposedPolarMapping +from sympde.topology import IdentityMapping, PolarMapping, AffineMapping, Mapping -__all__ = ('TransposedPolarMapping', 'create_domain', 'get_2D_rotation_mapping', 'flip_axis', - 'build_multipatch_domain', 'get_ref_eigenvalues') +__all__ = ( + 'TransposedPolarMapping', + 'get_2D_rotation_mapping', + 'flip_axis', + 'build_multipatch_domain', + 'build_cartesian_multipatch_domain') -#============================================================================== +# ============================================================================== # small extension to SymPDE: + + class TransposedPolarMapping(Mapping): """ Represents a Transposed (x1 <> x2) Polar 2D Mapping object (Annulus). @@ -20,60 +27,38 @@ class TransposedPolarMapping(Mapping): _expressions = {'x': 'c1 + (rmin*(1-x2)+rmax*x2)*cos(x1)', 'y': 'c2 + (rmin*(1-x2)+rmax*x2)*sin(x1)'} - _ldim = 2 - _pdim = 2 + _ldim = 2 + _pdim = 2 + +def sympde_Domain_join(patches, connectivity, name): + """ + temporary fix while sympde PR #155 is not merged + """ + connectivity_by_indices = [] + for I in connectivity: + connectivity_by_indices.append( + [(patches.index(I[0][0]), I[0][1], I[0][2]), + (patches.index(I[1][0]), I[1][1], I[1][2]), + I[2]]) + return Domain.join(patches, connectivity_by_indices, name) -def create_domain(patches, interfaces, name): - connectivity = [] - patches_interiors = [D.interior for D in patches] - for I in interfaces: - connectivity.append(((patches_interiors.index(I[0].domain),I[0].axis, I[0].ext), (patches_interiors.index(I[1].domain), I[1].axis, I[1].ext), I[2])) - return Domain.join(patches, connectivity, name) - -# def get_annulus_fourpatches(r_min, r_max): -# -# dom_log_1 = Square('dom1',bounds1=(r_min, r_max), bounds2=(0, np.pi/2)) -# dom_log_2 = Square('dom2',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi)) -# dom_log_3 = Square('dom3',bounds1=(r_min, r_max), bounds2=(np.pi, np.pi*3/2)) -# dom_log_4 = Square('dom4',bounds1=(r_min, r_max), bounds2=(np.pi*3/2, np.pi*2)) -# -# mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) -# mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) -# mapping_3 = PolarMapping('M3',2, c1= 0., c2= 0., rmin = 0., rmax=1.) -# mapping_4 = PolarMapping('M4',2, c1= 0., c2= 0., rmin = 0., rmax=1.) -# -# domain_1 = mapping_1(dom_log_1) -# domain_2 = mapping_2(dom_log_2) -# domain_3 = mapping_3(dom_log_3) -# domain_4 = mapping_4(dom_log_4) -# -# interfaces = [ -# [domain_1.get_boundary(axis=1, ext=1), domain_2.get_boundary(axis=1, ext=-1), 1], -# [domain_2.get_boundary(axis=1, ext=1), domain_3.get_boundary(axis=1, ext=-1), 1], -# [domain_3.get_boundary(axis=1, ext=1), domain_4.get_boundary(axis=1, ext=-1), 1], -# [domain_4.get_boundary(axis=1, ext=1), domain_1.get_boundary(axis=1, ext=-1), 1] -# ] -# patches = [domain_1, domain_2, domain_3, domain_4] -# domain = create_domain(patches, interfaces, name='domain') -# -# return domain - - -def get_2D_rotation_mapping(name='no_name', c1=0., c2=0., alpha=np.pi/2): +def get_2D_rotation_mapping(name='no_name', c1=0., c2=0., alpha=None): # AffineMapping: # _expressions = {'x': 'c1 + a11*x1 + a12*x2 + a13*x3', # 'y': 'c2 + a21*x1 + a22*x2 + a23*x3', - # 'z': 'c3 + a31*x1 + a32*x2 + a33*x3'} - + # 'z': 'c3 + a31*x1 + a32*x2 + a33*x3'} + if alpha is None: + alpha = np.pi/2 return AffineMapping( name, 2, c1=c1, c2=c2, a11=np.cos(alpha), a12=-np.sin(alpha), a21=np.sin(alpha), a22=np.cos(alpha), ) + def flip_axis(name='no_name', c1=0., c2=0.): # AffineMapping: @@ -87,6 +72,7 @@ def flip_axis(name='no_name', c1=0., c2=0.): a21=1, a22=0, ) + def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): """ Create a 2D multipatch domain among the many available. @@ -105,23 +91,61 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): The symbolic multipatch domain """ + connectivity = None + + # for readability + axis_0 = 0 + axis_1 = 1 + ext_0 = -1 + ext_1 = +1 + + # create the patches if domain_name == 'square_2': # reference square [0,pi]x[0,pi] with 2 patches # mp structure: # 2 # 1 - OmegaLog1 = Square('OmegaLog1',bounds1=(0., np.pi), bounds2=(0., np.pi/2)) - mapping_1 = IdentityMapping('M1',2) - domain_1 = mapping_1(OmegaLog1) - - OmegaLog2 = Square('OmegaLog2',bounds1=(0., np.pi), bounds2=(np.pi/2, np.pi)) - mapping_2 = IdentityMapping('M2',2) - domain_2 = mapping_2(OmegaLog2) + OmegaLog1 = Square( + 'OmegaLog1', bounds1=( + 0., np.pi), bounds2=( + 0., np.pi / 2)) + mapping_1 = IdentityMapping('M1', 2) + domain_1 = mapping_1(OmegaLog1) + + OmegaLog2 = Square( + 'OmegaLog2', bounds1=( + 0., np.pi), bounds2=( + np.pi / 2, np.pi)) + mapping_2 = IdentityMapping('M2', 2) + domain_2 = mapping_2(OmegaLog2) patches = [domain_1, domain_2] - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_2.get_boundary(axis=1, ext=-1),1] + connectivity = [[(domain_1, axis_1, ext_1), (domain_2, axis_1, ext_0), 1]] + elif domain_name == 'square_4': + # C D + # A B + + A = Square('A', bounds1=(0, np.pi / 2), bounds2=(0, np.pi / 2)) + B = Square('B', bounds1=(np.pi / 2, np.pi), bounds2=(0, np.pi / 2)) + C = Square('C', bounds1=(0, np.pi / 2), bounds2=(np.pi / 2, np.pi)) + D = Square('D', bounds1=(np.pi / 2, np.pi), bounds2=(np.pi / 2, np.pi)) + M1 = IdentityMapping('M1', dim=2) + M2 = IdentityMapping('M2', dim=2) + M3 = IdentityMapping('M3', dim=2) + M4 = IdentityMapping('M4', dim=2) + A = M1(A) + B = M2(B) + C = M3(C) + D = M4(D) + + patches = [A, B, C, D] + + connectivity = [ + [(A, axis_0, ext_1), (B, axis_0, ext_0), 1], + [(A, axis_1, ext_1), (C, axis_1, ext_0), 1], + [(C, axis_0, ext_1), (D, axis_0, ext_0), 1], + [(B, axis_1, ext_1), (D, axis_1, ext_0), 1], ] elif domain_name == 'square_6': @@ -130,81 +154,186 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): # 5 6 # 3 4 # 1 2 - OmegaLog1 = Square('OmegaLog1',bounds1=(0., np.pi/2), bounds2=(0., np.pi/3)) - mapping_1 = IdentityMapping('M1',2) - domain_1 = mapping_1(OmegaLog1) - - OmegaLog2 = Square('OmegaLog2',bounds1=(np.pi/2, np.pi), bounds2=(0., np.pi/3)) - mapping_2 = IdentityMapping('M2',2) - domain_2 = mapping_2(OmegaLog2) - - OmegaLog3 = Square('OmegaLog3',bounds1=(0., np.pi/2), bounds2=(np.pi/3, np.pi*2/3)) - mapping_3 = IdentityMapping('M3',2) - domain_3 = mapping_3(OmegaLog3) - - OmegaLog4 = Square('OmegaLog4',bounds1=(np.pi/2, np.pi), bounds2=(np.pi/3, np.pi*2/3)) - mapping_4 = IdentityMapping('M4',2) - domain_4 = mapping_4(OmegaLog4) - - OmegaLog5 = Square('OmegaLog5',bounds1=(0., np.pi/2), bounds2=(np.pi*2/3, np.pi)) - mapping_5 = IdentityMapping('M5',2) - domain_5 = mapping_5(OmegaLog5) - - OmegaLog6 = Square('OmegaLog6',bounds1=(np.pi/2, np.pi), bounds2=(np.pi*2/3, np.pi)) - mapping_6 = IdentityMapping('M6',2) - domain_6 = mapping_6(OmegaLog6) + OmegaLog1 = Square( + 'OmegaLog1', + bounds1=( + 0., + np.pi / 2), + bounds2=( + 0., + np.pi / 3)) + mapping_1 = IdentityMapping('M1', 2) + domain_1 = mapping_1(OmegaLog1) + + OmegaLog2 = Square( + 'OmegaLog2', + bounds1=( + np.pi / 2, + np.pi), + bounds2=( + 0., + np.pi / 3)) + mapping_2 = IdentityMapping('M2', 2) + domain_2 = mapping_2(OmegaLog2) + + OmegaLog3 = Square( + 'OmegaLog3', + bounds1=( + 0., + np.pi / 2), + bounds2=( + np.pi / 3, + np.pi * 2 / 3)) + mapping_3 = IdentityMapping('M3', 2) + domain_3 = mapping_3(OmegaLog3) + + OmegaLog4 = Square( + 'OmegaLog4', + bounds1=( + np.pi / 2, + np.pi), + bounds2=( + np.pi / 3, + np.pi * 2 / 3)) + mapping_4 = IdentityMapping('M4', 2) + domain_4 = mapping_4(OmegaLog4) + + OmegaLog5 = Square( + 'OmegaLog5', + bounds1=( + 0., + np.pi / 2), + bounds2=( + np.pi * 2 / 3, + np.pi)) + mapping_5 = IdentityMapping('M5', 2) + domain_5 = mapping_5(OmegaLog5) + + OmegaLog6 = Square( + 'OmegaLog6', + bounds1=( + np.pi / 2, + np.pi), + bounds2=( + np.pi * 2 / 3, + np.pi)) + mapping_6 = IdentityMapping('M6', 2) + domain_6 = mapping_6(OmegaLog6) patches = [domain_1, domain_2, domain_3, domain_4, domain_5, domain_6] - interfaces = [ - [domain_1.get_boundary(axis=0, ext=+1), domain_2.get_boundary(axis=0, ext=-1),1], - [domain_3.get_boundary(axis=0, ext=+1), domain_4.get_boundary(axis=0, ext=-1),1], - [domain_5.get_boundary(axis=0, ext=+1), domain_6.get_boundary(axis=0, ext=-1),1], - [domain_1.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1),1], - [domain_2.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1),1], - [domain_4.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=-1),1], + connectivity = [ + [(domain_1, axis_0, ext_1), (domain_2, axis_0, ext_0), 1], + [(domain_3, axis_0, ext_1), (domain_4, axis_0, ext_0), 1], + [(domain_5, axis_0, ext_1), (domain_6, axis_0, ext_0), 1], + [(domain_1, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_6, axis_1, ext_0), 1], ] elif domain_name in ['square_8', 'square_9']: # square with third-length patches, with or without a hole: - OmegaLog1 = Square('OmegaLog1',bounds1=(0., np.pi/3), bounds2=(0., np.pi/3)) - mapping_1 = IdentityMapping('M1',2) - domain_1 = mapping_1(OmegaLog1) - - OmegaLog2 = Square('OmegaLog2',bounds1=(np.pi/3, np.pi*2/3), bounds2=(0., np.pi/3)) - mapping_2 = IdentityMapping('M2',2) - domain_2 = mapping_2(OmegaLog2) - - OmegaLog3 = Square('OmegaLog3',bounds1=(np.pi*2/3, np.pi), bounds2=(0., np.pi/3)) - mapping_3 = IdentityMapping('M3',2) - domain_3 = mapping_3(OmegaLog3) - - OmegaLog4 = Square('OmegaLog4',bounds1=(0., np.pi/3), bounds2=(np.pi/3, np.pi*2/3)) - mapping_4 = IdentityMapping('M4',2) - domain_4 = mapping_4(OmegaLog4) - - OmegaLog5 = Square('OmegaLog5',bounds1=(np.pi*2/3, np.pi), bounds2=(np.pi/3, np.pi*2/3)) - mapping_5 = IdentityMapping('M5',2) - domain_5 = mapping_5(OmegaLog5) - - OmegaLog6 = Square('OmegaLog6',bounds1=(0., np.pi/3), bounds2=(np.pi*2/3, np.pi)) - mapping_6 = IdentityMapping('M6',2) - domain_6 = mapping_6(OmegaLog6) - - OmegaLog7 = Square('OmegaLog7',bounds1=(np.pi/3, np.pi*2/3), bounds2=(np.pi*2/3, np.pi)) - mapping_7 = IdentityMapping('M7',2) - domain_7 = mapping_7(OmegaLog7) - - OmegaLog8 = Square('OmegaLog8',bounds1=(np.pi*2/3, np.pi), bounds2=(np.pi*2/3, np.pi)) - mapping_8 = IdentityMapping('M8',2) - domain_8 = mapping_8(OmegaLog8) + OmegaLog1 = Square( + 'OmegaLog1', + bounds1=( + 0., + np.pi / 3), + bounds2=( + 0., + np.pi / 3)) + mapping_1 = IdentityMapping('M1', 2) + domain_1 = mapping_1(OmegaLog1) + + OmegaLog2 = Square( + 'OmegaLog2', + bounds1=( + np.pi / 3, + np.pi * 2 / 3), + bounds2=( + 0., + np.pi / 3)) + mapping_2 = IdentityMapping('M2', 2) + domain_2 = mapping_2(OmegaLog2) + + OmegaLog3 = Square( + 'OmegaLog3', + bounds1=( + np.pi * 2 / 3, + np.pi), + bounds2=( + 0., + np.pi / 3)) + mapping_3 = IdentityMapping('M3', 2) + domain_3 = mapping_3(OmegaLog3) + + OmegaLog4 = Square( + 'OmegaLog4', + bounds1=( + 0., + np.pi / 3), + bounds2=( + np.pi / 3, + np.pi * 2 / 3)) + mapping_4 = IdentityMapping('M4', 2) + domain_4 = mapping_4(OmegaLog4) + + OmegaLog5 = Square( + 'OmegaLog5', + bounds1=( + np.pi * 2 / 3, + np.pi), + bounds2=( + np.pi / 3, + np.pi * 2 / 3)) + mapping_5 = IdentityMapping('M5', 2) + domain_5 = mapping_5(OmegaLog5) + + OmegaLog6 = Square( + 'OmegaLog6', + bounds1=( + 0., + np.pi / 3), + bounds2=( + np.pi * 2 / 3, + np.pi)) + mapping_6 = IdentityMapping('M6', 2) + domain_6 = mapping_6(OmegaLog6) + + OmegaLog7 = Square( + 'OmegaLog7', + bounds1=( + np.pi / 3, + np.pi * 2 / 3), + bounds2=( + np.pi * 2 / 3, + np.pi)) + mapping_7 = IdentityMapping('M7', 2) + domain_7 = mapping_7(OmegaLog7) + + OmegaLog8 = Square( + 'OmegaLog8', + bounds1=( + np.pi * 2 / 3, + np.pi), + bounds2=( + np.pi * 2 / 3, + np.pi)) + mapping_8 = IdentityMapping('M8', 2) + domain_8 = mapping_8(OmegaLog8) # center domain - OmegaLog9 = Square('OmegaLog9',bounds1=(np.pi/3, np.pi*2/3), bounds2=(np.pi/3, np.pi*2/3)) - mapping_9 = IdentityMapping('M9',2) - domain_9 = mapping_9(OmegaLog9) + OmegaLog9 = Square( + 'OmegaLog9', + bounds1=( + np.pi / 3, + np.pi * 2 / 3), + bounds2=( + np.pi / 3, + np.pi * 2 / 3)) + mapping_9 = IdentityMapping('M9', 2) + domain_9 = mapping_9(OmegaLog9) if domain_name == 'square_8': # square domain with a hole: @@ -212,18 +341,25 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): # 4 * 5 # 1 2 3 - - patches = [domain_1, domain_2, domain_3, domain_4, domain_5, domain_6, domain_7, domain_8] - - interfaces = [ - [domain_1.get_boundary(axis=0, ext=+1), domain_2.get_boundary(axis=0, ext=-1),1], - [domain_2.get_boundary(axis=0, ext=+1), domain_3.get_boundary(axis=0, ext=-1),1], - [domain_6.get_boundary(axis=0, ext=+1), domain_7.get_boundary(axis=0, ext=-1),1], - [domain_7.get_boundary(axis=0, ext=+1), domain_8.get_boundary(axis=0, ext=-1),1], - [domain_1.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1),1], - [domain_4.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1),1], - [domain_5.get_boundary(axis=1, ext=+1), domain_8.get_boundary(axis=1, ext=-1),1], + patches = [ + domain_1, + domain_2, + domain_3, + domain_4, + domain_5, + domain_6, + domain_7, + domain_8] + + connectivity = [ + [(domain_1, axis_0, ext_1), (domain_2, axis_0, ext_0), 1], + [(domain_2, axis_0, ext_1), (domain_3, axis_0, ext_0), 1], + [(domain_6, axis_0, ext_1), (domain_7, axis_0, ext_0), 1], + [(domain_7, axis_0, ext_1), (domain_8, axis_0, ext_0), 1], + [(domain_1, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_6, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_8, axis_1, ext_0), 1], ] elif domain_name == 'square_9': @@ -232,22 +368,30 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): # 4 9 5 # 1 2 3 - - patches = [domain_1, domain_2, domain_3, domain_4, domain_5, domain_6, domain_7, domain_8, domain_9] - - interfaces = [ - [domain_1.get_boundary(axis=0, ext=+1), domain_2.get_boundary(axis=0, ext=-1),1], - [domain_2.get_boundary(axis=0, ext=+1), domain_3.get_boundary(axis=0, ext=-1),1], - [domain_4.get_boundary(axis=0, ext=+1), domain_9.get_boundary(axis=0, ext=-1),1], - [domain_9.get_boundary(axis=0, ext=+1), domain_5.get_boundary(axis=0, ext=-1),1], - [domain_6.get_boundary(axis=0, ext=+1), domain_7.get_boundary(axis=0, ext=-1),1], - [domain_7.get_boundary(axis=0, ext=+1), domain_8.get_boundary(axis=0, ext=-1),1], - [domain_1.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1),1], - [domain_4.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=-1),1], - [domain_2.get_boundary(axis=1, ext=+1), domain_9.get_boundary(axis=1, ext=-1),1], - [domain_9.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1),1], - [domain_5.get_boundary(axis=1, ext=+1), domain_8.get_boundary(axis=1, ext=-1),1], + patches = [ + domain_1, + domain_2, + domain_3, + domain_4, + domain_5, + domain_6, + domain_7, + domain_8, + domain_9] + + connectivity = [ + [(domain_1, axis_0, ext_1), (domain_2, axis_0, ext_0), 1], + [(domain_2, axis_0, ext_1), (domain_3, axis_0, ext_0), 1], + [(domain_4, axis_0, ext_1), (domain_9, axis_0, ext_0), 1], + [(domain_9, axis_0, ext_1), (domain_5, axis_0, ext_0), 1], + [(domain_6, axis_0, ext_1), (domain_7, axis_0, ext_0), 1], + [(domain_7, axis_0, ext_1), (domain_8, axis_0, ext_0), 1], + [(domain_1, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_6, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_9, axis_1, ext_0), 1], + [(domain_9, axis_1, ext_1), (domain_7, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_8, axis_1, ext_0), 1], ] else: @@ -255,99 +399,143 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): elif domain_name in ['pretzel', 'pretzel_f', 'pretzel_annulus', 'pretzel_debug']: # pretzel-shaped domain with quarter-annuli and quadrangles -- setting parameters - # note: 'pretzel_f' is a bit finer than 'pretzel', to have a roughly uniform resolution (patches of approx same size) + # note: 'pretzel_f' is a bit finer than 'pretzel', to have a roughly + # uniform resolution (patches of approx same size) if r_min is None: - r_min=1 # smaller radius of quarter-annuli + r_min = 1 # smaller radius of quarter-annuli if r_max is None: - r_max=2 # larger radius of quarter-annuli + r_max = 2 # larger radius of quarter-annuli assert 0 < r_min assert r_min < r_max dr = r_max - r_min h = dr # offset from axes of quarter-annuli - hr = dr/2 - cr = h +(r_max+r_min)/2 - - dom_log_1 = Square('dom1',bounds1=(r_min, r_max), bounds2=(0, np.pi/2)) - mapping_1 = PolarMapping('M1',2, c1= h, c2= h, rmin = 0., rmax=1.) - domain_1 = mapping_1(dom_log_1) - - dom_log_1_1 = Square('dom1_1',bounds1=(r_min, r_max), bounds2=(0, np.pi/4)) - mapping_1_1 = PolarMapping('M1_1',2, c1= h, c2= h, rmin = 0., rmax=1.) - domain_1_1 = mapping_1_1(dom_log_1_1) - - dom_log_1_2 = Square('dom1_2',bounds1=(r_min, r_max), bounds2=(np.pi/4, np.pi/2)) - mapping_1_2 = PolarMapping('M1_2',2, c1= h, c2= h, rmin = 0., rmax=1.) - domain_1_2 = mapping_1_2(dom_log_1_2) - - dom_log_2 = Square('dom2',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi)) - mapping_2 = PolarMapping('M2',2, c1= -h, c2= h, rmin = 0., rmax=1.) - domain_2 = mapping_2(dom_log_2) - - dom_log_2_1 = Square('dom2_1',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi*3/4)) - mapping_2_1 = PolarMapping('M2_1',2, c1= -h, c2= h, rmin = 0., rmax=1.) - domain_2_1 = mapping_2_1(dom_log_2_1) - - dom_log_2_2 = Square('dom2_2',bounds1=(r_min, r_max), bounds2=(np.pi*3/4, np.pi)) - mapping_2_2 = PolarMapping('M2_2',2, c1= -h, c2= h, rmin = 0., rmax=1.) - domain_2_2 = mapping_2_2(dom_log_2_2) + hr = dr / 2 + cr = h + (r_max + r_min) / 2 + + dom_log_1 = Square( + 'dom1', bounds1=( + r_min, r_max), bounds2=( + 0, np.pi / 2)) + mapping_1 = PolarMapping('M1', 2, c1=h, c2=h, rmin=0., rmax=1.) + domain_1 = mapping_1(dom_log_1) + + dom_log_1_1 = Square( + 'dom1_1', bounds1=( + r_min, r_max), bounds2=( + 0, np.pi / 4)) + mapping_1_1 = PolarMapping('M1_1', 2, c1=h, c2=h, rmin=0., rmax=1.) + domain_1_1 = mapping_1_1(dom_log_1_1) + + dom_log_1_2 = Square( + 'dom1_2', bounds1=( + r_min, r_max), bounds2=( + np.pi / 4, np.pi / 2)) + mapping_1_2 = PolarMapping('M1_2', 2, c1=h, c2=h, rmin=0., rmax=1.) + domain_1_2 = mapping_1_2(dom_log_1_2) + + dom_log_2 = Square( + 'dom2', bounds1=( + r_min, r_max), bounds2=( + np.pi / 2, np.pi)) + mapping_2 = PolarMapping('M2', 2, c1=-h, c2=h, rmin=0., rmax=1.) + domain_2 = mapping_2(dom_log_2) + + dom_log_2_1 = Square( + 'dom2_1', bounds1=( + r_min, r_max), bounds2=( + np.pi / 2, np.pi * 3 / 4)) + mapping_2_1 = PolarMapping('M2_1', 2, c1=-h, c2=h, rmin=0., rmax=1.) + domain_2_1 = mapping_2_1(dom_log_2_1) + + dom_log_2_2 = Square( + 'dom2_2', bounds1=( + r_min, r_max), bounds2=( + np.pi * 3 / 4, np.pi)) + mapping_2_2 = PolarMapping('M2_2', 2, c1=-h, c2=h, rmin=0., rmax=1.) + domain_2_2 = mapping_2_2(dom_log_2_2) # for debug: - dom_log_10 = Square('dom10',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi)) - mapping_10 = PolarMapping('M10',2, c1= h, c2= h, rmin = 0., rmax=1.) - domain_10 = mapping_10(dom_log_10) - - dom_log_3 = Square('dom3',bounds1=(r_min, r_max), bounds2=(np.pi, np.pi*3/2)) - mapping_3 = PolarMapping('M3',2, c1= -h, c2= 0, rmin = 0., rmax=1.) - domain_3 = mapping_3(dom_log_3) - - dom_log_3_1 = Square('dom3_1',bounds1=(r_min, r_max), bounds2=(np.pi, np.pi*5/4)) - mapping_3_1 = PolarMapping('M3_1',2, c1= -h, c2= 0, rmin = 0., rmax=1.) - domain_3_1 = mapping_3_1(dom_log_3_1) - - dom_log_3_2 = Square('dom3_2',bounds1=(r_min, r_max), bounds2=(np.pi*5/4, np.pi*3/2)) - mapping_3_2 = PolarMapping('M3_2',2, c1= -h, c2= 0, rmin = 0., rmax=1.) - domain_3_2 = mapping_3_2(dom_log_3_2) - - dom_log_4 = Square('dom4',bounds1=(r_min, r_max), bounds2=(np.pi*3/2, np.pi*2)) - mapping_4 = PolarMapping('M4',2, c1= h, c2= 0, rmin = 0., rmax=1.) - domain_4 = mapping_4(dom_log_4) - - dom_log_4_1 = Square('dom4_1',bounds1=(r_min, r_max), bounds2=(np.pi*3/2, np.pi*7/4)) - mapping_4_1 = PolarMapping('M4_1',2, c1= h, c2= 0, rmin = 0., rmax=1.) - domain_4_1 = mapping_4_1(dom_log_4_1) - - dom_log_4_2 = Square('dom4_2',bounds1=(r_min, r_max), bounds2=(np.pi*7/4, np.pi*2)) - mapping_4_2 = PolarMapping('M4_2',2, c1= h, c2= 0, rmin = 0., rmax=1.) - domain_4_2 = mapping_4_2(dom_log_4_2) - - dom_log_5 = Square('dom5',bounds1=(-hr,hr) , bounds2=(-h/2, h/2)) - mapping_5 = get_2D_rotation_mapping('M5', c1=h/2, c2=cr , alpha=np.pi/2) - domain_5 = mapping_5(dom_log_5) - - dom_log_6 = Square('dom6',bounds1=(-hr,hr) , bounds2=(-h/2, h/2)) - mapping_6 = flip_axis('M6', c1=-h/2, c2=cr) - domain_6 = mapping_6(dom_log_6) - - dom_log_7 = Square('dom7',bounds1=(-hr, hr), bounds2=(-h/2, h/2)) - mapping_7 = get_2D_rotation_mapping('M7', c1=-cr, c2=h/2 , alpha=np.pi) - domain_7 = mapping_7(dom_log_7) + dom_log_10 = Square( + 'dom10', bounds1=( + r_min, r_max), bounds2=( + np.pi / 2, np.pi)) + mapping_10 = PolarMapping('M10', 2, c1=h, c2=h, rmin=0., rmax=1.) + domain_10 = mapping_10(dom_log_10) + + dom_log_3 = Square( + 'dom3', bounds1=( + r_min, r_max), bounds2=( + np.pi, np.pi * 3 / 2)) + mapping_3 = PolarMapping('M3', 2, c1=-h, c2=0, rmin=0., rmax=1.) + domain_3 = mapping_3(dom_log_3) + + dom_log_3_1 = Square( + 'dom3_1', bounds1=( + r_min, r_max), bounds2=( + np.pi, np.pi * 5 / 4)) + mapping_3_1 = PolarMapping('M3_1', 2, c1=-h, c2=0, rmin=0., rmax=1.) + domain_3_1 = mapping_3_1(dom_log_3_1) + + dom_log_3_2 = Square( + 'dom3_2', bounds1=( + r_min, r_max), bounds2=( + np.pi * 5 / 4, np.pi * 3 / 2)) + mapping_3_2 = PolarMapping('M3_2', 2, c1=-h, c2=0, rmin=0., rmax=1.) + domain_3_2 = mapping_3_2(dom_log_3_2) + + dom_log_4 = Square( + 'dom4', bounds1=( + r_min, r_max), bounds2=( + np.pi * 3 / 2, np.pi * 2)) + mapping_4 = PolarMapping('M4', 2, c1=h, c2=0, rmin=0., rmax=1.) + domain_4 = mapping_4(dom_log_4) + + dom_log_4_1 = Square( + 'dom4_1', bounds1=( + r_min, r_max), bounds2=( + np.pi * 3 / 2, np.pi * 7 / 4)) + mapping_4_1 = PolarMapping('M4_1', 2, c1=h, c2=0, rmin=0., rmax=1.) + domain_4_1 = mapping_4_1(dom_log_4_1) + + dom_log_4_2 = Square( + 'dom4_2', bounds1=( + r_min, r_max), bounds2=( + np.pi * 7 / 4, np.pi * 2)) + mapping_4_2 = PolarMapping('M4_2', 2, c1=h, c2=0, rmin=0., rmax=1.) + domain_4_2 = mapping_4_2(dom_log_4_2) + + dom_log_5 = Square('dom5', bounds1=(-hr, hr), bounds2=(-h / 2, h / 2)) + mapping_5 = get_2D_rotation_mapping( + 'M5', c1=h / 2, c2=cr, alpha=np.pi / 2) + domain_5 = mapping_5(dom_log_5) + + dom_log_6 = Square('dom6', bounds1=(-hr, hr), bounds2=(-h / 2, h / 2)) + mapping_6 = flip_axis('M6', c1=-h / 2, c2=cr) + domain_6 = mapping_6(dom_log_6) + + dom_log_7 = Square('dom7', bounds1=(-hr, hr), bounds2=(-h / 2, h / 2)) + mapping_7 = get_2D_rotation_mapping( + 'M7', c1=-cr, c2=h / 2, alpha=np.pi) + domain_7 = mapping_7(dom_log_7) # dom_log_8 = Square('dom8',bounds1=(-hr, hr), bounds2=(-h/2, h/2)) # mapping_8 = get_2D_rotation_mapping('M8', c1=-cr, c2=-h/2 , alpha=np.pi) # domain_8 = mapping_8(dom_log_8) - dom_log_9 = Square('dom9',bounds1=(-hr,hr) , bounds2=(-h, h)) - mapping_9 = get_2D_rotation_mapping('M9', c1=0, c2=h-cr , alpha=np.pi*3/2) - domain_9 = mapping_9(dom_log_9) - - dom_log_9_1 = Square('dom9_1',bounds1=(-hr,hr) , bounds2=(-h, 0)) - mapping_9_1 = get_2D_rotation_mapping('M9_1', c1=0, c2=h-cr , alpha=np.pi*3/2) - domain_9_1 = mapping_9_1(dom_log_9_1) + dom_log_9 = Square('dom9', bounds1=(-hr, hr), bounds2=(-h, h)) + mapping_9 = get_2D_rotation_mapping( + 'M9', c1=0, c2=h - cr, alpha=np.pi * 3 / 2) + domain_9 = mapping_9(dom_log_9) - dom_log_9_2 = Square('dom9_2',bounds1=(-hr,hr) , bounds2=(0, h)) - mapping_9_2 = get_2D_rotation_mapping('M9_2', c1=0, c2=h-cr , alpha=np.pi*3/2) - domain_9_2 = mapping_9_2(dom_log_9_2) + dom_log_9_1 = Square('dom9_1', bounds1=(-hr, hr), bounds2=(-h, 0)) + mapping_9_1 = get_2D_rotation_mapping( + 'M9_1', c1=0, c2=h - cr, alpha=np.pi * 3 / 2) + domain_9_1 = mapping_9_1(dom_log_9_1) + dom_log_9_2 = Square('dom9_2', bounds1=(-hr, hr), bounds2=(0, h)) + mapping_9_2 = get_2D_rotation_mapping( + 'M9_2', c1=0, c2=h - cr, alpha=np.pi * 3 / 2) + domain_9_2 = mapping_9_2(dom_log_9_2) # dom_log_10 = Square('dom10',bounds1=(-hr,hr) , bounds2=(-h/2, h/2)) # mapping_10 = get_2D_rotation_mapping('M10', c1=h/2, c2=h-cr , alpha=np.pi*3/2) @@ -357,34 +545,91 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): # mapping_11 = get_2D_rotation_mapping('M11', c1=cr, c2=-h/2 , alpha=0) # domain_11 = mapping_11(dom_log_11) - dom_log_12 = Square('dom12',bounds1=(-hr, hr), bounds2=(-h/2, h/2)) -# mapping_12 = get_2D_rotation_mapping('M12', c1=cr, c2=h/2 , alpha=0) - mapping_12 = AffineMapping('M12', 2, c1=cr, c2=h/2, a11=1, a22=-1, a21=0, a12=0) - domain_12 = mapping_12(dom_log_12) - - dom_log_13 = Square('dom13',bounds1=(np.pi*3/2, np.pi*2), bounds2=(r_min, r_max)) - mapping_13 = TransposedPolarMapping('M13',2, c1= -r_min-h, c2= r_min+h, rmin = 0., rmax=1.) - domain_13 = mapping_13(dom_log_13) - - dom_log_13_1 = Square('dom13_1',bounds1=(np.pi*3/2, np.pi*7/4), bounds2=(r_min, r_max)) - mapping_13_1 = TransposedPolarMapping('M13_1',2, c1= -r_min-h, c2= r_min+h, rmin = 0., rmax=1.) - domain_13_1 = mapping_13_1(dom_log_13_1) - - dom_log_13_2 = Square('dom13_2',bounds1=(np.pi*7/4, np.pi*2), bounds2=(r_min, r_max)) - mapping_13_2 = TransposedPolarMapping('M13_2',2, c1= -r_min-h, c2= r_min+h, rmin = 0., rmax=1.) - domain_13_2 = mapping_13_2(dom_log_13_2) - - dom_log_14 = Square('dom14',bounds1=(np.pi, np.pi*3/2), bounds2=(r_min, r_max)) - mapping_14 = TransposedPolarMapping('M14',2, c1= r_min+h, c2= r_min+h, rmin = 0., rmax=1.) - domain_14 = mapping_14(dom_log_14) - - dom_log_14_1 = Square('dom14_1',bounds1=(np.pi, np.pi*5/4), bounds2=(r_min, r_max)) # STOP ICI: check domain - mapping_14_1 = TransposedPolarMapping('M14_1',2, c1= r_min+h, c2= r_min+h, rmin = 0., rmax=1.) - domain_14_1 = mapping_14_1(dom_log_14_1) - - dom_log_14_2 = Square('dom14_2',bounds1=(np.pi*5/4, np.pi*3/2), bounds2=(r_min, r_max)) - mapping_14_2 = TransposedPolarMapping('M14_2',2, c1= r_min+h, c2= r_min+h, rmin = 0., rmax=1.) - domain_14_2 = mapping_14_2(dom_log_14_2) + dom_log_12 = Square('dom12', bounds1=(-hr, hr), + bounds2=(-h / 2, h / 2)) + #mapping_12 = get_2D_rotation_mapping('M12', c1=cr, c2=h/2 , alpha=0) + mapping_12 = AffineMapping( + 'M12', + 2, + c1=cr, + c2=h / 2, + a11=1, + a22=-1, + a21=0, + a12=0) + domain_12 = mapping_12(dom_log_12) + + dom_log_13 = Square( + 'dom13', + bounds1=( + np.pi * 3 / 2, + np.pi * 2), + bounds2=( + r_min, + r_max)) + mapping_13 = TransposedPolarMapping( + 'M13', 2, c1=-r_min - h, c2=r_min + h, rmin=0., rmax=1.) + domain_13 = mapping_13(dom_log_13) + + dom_log_13_1 = Square( + 'dom13_1', + bounds1=( + np.pi * 3 / 2, + np.pi * 7 / 4), + bounds2=( + r_min, + r_max)) + mapping_13_1 = TransposedPolarMapping( + 'M13_1', 2, c1=-r_min - h, c2=r_min + h, rmin=0., rmax=1.) + domain_13_1 = mapping_13_1(dom_log_13_1) + + dom_log_13_2 = Square( + 'dom13_2', + bounds1=( + np.pi * 7 / 4, + np.pi * 2), + bounds2=( + r_min, + r_max)) + mapping_13_2 = TransposedPolarMapping( + 'M13_2', 2, c1=-r_min - h, c2=r_min + h, rmin=0., rmax=1.) + domain_13_2 = mapping_13_2(dom_log_13_2) + + dom_log_14 = Square( + 'dom14', + bounds1=( + np.pi, + np.pi * 3 / 2), + bounds2=( + r_min, + r_max)) + mapping_14 = TransposedPolarMapping( + 'M14', 2, c1=r_min + h, c2=r_min + h, rmin=0., rmax=1.) + domain_14 = mapping_14(dom_log_14) + + dom_log_14_1 = Square( + 'dom14_1', + bounds1=( + np.pi, + np.pi * 5 / 4), + bounds2=( + r_min, + r_max)) # STOP ICI: check domain + mapping_14_1 = TransposedPolarMapping( + 'M14_1', 2, c1=r_min + h, c2=r_min + h, rmin=0., rmax=1.) + domain_14_1 = mapping_14_1(dom_log_14_1) + + dom_log_14_2 = Square( + 'dom14_2', + bounds1=( + np.pi * 5 / 4, + np.pi * 3 / 2), + bounds2=( + r_min, + r_max)) + mapping_14_2 = TransposedPolarMapping( + 'M14_2', 2, c1=r_min + h, c2=r_min + h, rmin=0., rmax=1.) + domain_14_2 = mapping_14_2(dom_log_14_2) # dom_log_15 = Square('dom15', bounds1=(-r_min-h, r_min+h), bounds2=(0, h)) # mapping_15 = IdentityMapping('M15', 2) @@ -392,201 +637,271 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): if domain_name == 'pretzel': patches = ([ - domain_1, - domain_2, - domain_3, - domain_4, - domain_5, - domain_6, - domain_7, - domain_9, - domain_12, - domain_13, - domain_14, - ]) - - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1), 1], - [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=1), 1], - [domain_6.get_boundary(axis=1, ext=-1), domain_2.get_boundary(axis=1, ext=-1), 1], - [domain_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1), 1], - [domain_7.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1), 1], - [domain_3.get_boundary(axis=1, ext=+1), domain_9.get_boundary(axis=1, ext=-1), 1], - [domain_9.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1), 1], - [domain_4.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=1), 1], - [domain_12.get_boundary(axis=1, ext=-1), domain_1.get_boundary(axis=1, ext=-1), 1], - [domain_6.get_boundary(axis=0, ext=-1), domain_13.get_boundary(axis=0, ext=1), 1], - [domain_7.get_boundary(axis=0, ext=-1), domain_13.get_boundary(axis=0, ext=-1), 1], - [domain_5.get_boundary(axis=0, ext=-1), domain_14.get_boundary(axis=0, ext=-1), 1], - [domain_12.get_boundary(axis=0, ext=-1), domain_14.get_boundary(axis=0, ext=+1),1], - ] + domain_1, + domain_2, + domain_3, + domain_4, + domain_5, + domain_6, + domain_7, + domain_9, + domain_12, + domain_13, + domain_14, + ]) + + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_6, axis_1, ext_1), 1], + [(domain_6, axis_1, ext_0), (domain_2, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_7, axis_1, ext_0), 1], + [(domain_7, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_9, axis_1, ext_0), 1], + [(domain_9, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_12, axis_1, ext_1), 1], + [(domain_12, axis_1, ext_0), (domain_1, axis_1, ext_0), 1], + [(domain_6, axis_0, ext_0), (domain_13, axis_0, ext_1), 1], + [(domain_7, axis_0, ext_0), (domain_13, axis_0, ext_0), 1], + [(domain_5, axis_0, ext_0), (domain_14, axis_0, ext_0), 1], + [(domain_12, axis_0, ext_0), (domain_14, axis_0, ext_1), 1], + ] elif domain_name == 'pretzel_f': patches = ([ - domain_1_1, - domain_1_2, - domain_2_1, - domain_2_2, - domain_3_1, - domain_3_2, - domain_4_1, - domain_4_2, - domain_5, - domain_6, - domain_7, - domain_9_1, - domain_9_2, - domain_12, - domain_13_1, - domain_13_2, - domain_14_1, - domain_14_2, - ]) - - interfaces = [ - [domain_1_1.get_boundary(axis=1, ext=+1), domain_1_2.get_boundary(axis=1, ext=-1), 1], - [domain_1_2.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1), 1], - [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=1), 1], - [domain_6.get_boundary(axis=1, ext=-1), domain_2_1.get_boundary(axis=1, ext=-1), 1], - [domain_2_1.get_boundary(axis=1, ext=+1), domain_2_2.get_boundary(axis=1, ext=-1), 1], - [domain_2_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1), 1], - [domain_7.get_boundary(axis=1, ext=+1), domain_3_1.get_boundary(axis=1, ext=-1), 1], - [domain_3_1.get_boundary(axis=1, ext=+1), domain_3_2.get_boundary(axis=1, ext=-1), 1], - [domain_3_2.get_boundary(axis=1, ext=+1), domain_9_1.get_boundary(axis=1, ext=-1), 1], - [domain_9_1.get_boundary(axis=1, ext=+1), domain_9_2.get_boundary(axis=1, ext=-1), 1], - [domain_9_2.get_boundary(axis=1, ext=+1), domain_4_1.get_boundary(axis=1, ext=-1), 1], - [domain_4_1.get_boundary(axis=1, ext=+1), domain_4_2.get_boundary(axis=1, ext=-1), 1], - [domain_4_2.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=1), 1], - [domain_12.get_boundary(axis=1, ext=-1), domain_1_1.get_boundary(axis=1, ext=-1), 1], - [domain_6.get_boundary(axis=0, ext=-1), domain_13_2.get_boundary(axis=0, ext=1), 1], - [domain_13_2.get_boundary(axis=0, ext=-1), domain_13_1.get_boundary(axis=0, ext=1), 1], - [domain_7.get_boundary(axis=0, ext=-1), domain_13_1.get_boundary(axis=0, ext=-1), 1], - [domain_5.get_boundary(axis=0, ext=-1), domain_14_1.get_boundary(axis=0, ext=-1), 1], - [domain_14_1.get_boundary(axis=0, ext=+1), domain_14_2.get_boundary(axis=0, ext=-1), 1], - [domain_12.get_boundary(axis=0, ext=-1), domain_14_2.get_boundary(axis=0, ext=+1),1], - ] - - # reste: 13 et 14 + domain_1_1, + domain_1_2, + domain_2_1, + domain_2_2, + domain_3_1, + domain_3_2, + domain_4_1, + domain_4_2, + domain_5, + domain_6, + domain_7, + domain_9_1, + domain_9_2, + domain_12, + domain_13_1, + domain_13_2, + domain_14_1, + domain_14_2, + ]) + + # interfaces = [ + # [domain_1_1.get_boundary(axis=1, ext=+1), domain_1_2.get_boundary(axis=1, ext=-1), 1], + # [domain_1_2.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1), 1], + # [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=1), 1], + # [domain_6.get_boundary(axis=1, ext=-1), domain_2_1.get_boundary(axis=1, ext=-1), 1], + # [domain_2_1.get_boundary(axis=1, ext=+1), domain_2_2.get_boundary(axis=1, ext=-1), 1], + # [domain_2_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1), 1], + # [domain_7.get_boundary(axis=1, ext=+1), domain_3_1.get_boundary(axis=1, ext=-1), 1], + # [domain_3_1.get_boundary(axis=1, ext=+1), domain_3_2.get_boundary(axis=1, ext=-1), 1], + # [domain_3_2.get_boundary(axis=1, ext=+1), domain_9_1.get_boundary(axis=1, ext=-1), 1], + # [domain_9_1.get_boundary(axis=1, ext=+1), domain_9_2.get_boundary(axis=1, ext=-1), 1], + # [domain_9_2.get_boundary(axis=1, ext=+1), domain_4_1.get_boundary(axis=1, ext=-1), 1], + # [domain_4_1.get_boundary(axis=1, ext=+1), domain_4_2.get_boundary(axis=1, ext=-1), 1], + # [domain_4_2.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=1), 1], + # [domain_12.get_boundary(axis=1, ext=-1), domain_1_1.get_boundary(axis=1, ext=-1), 1], + # [domain_6.get_boundary(axis=0, ext=-1), domain_13_2.get_boundary(axis=0, ext=1), 1], + # [domain_13_2.get_boundary(axis=0, ext=-1), domain_13_1.get_boundary(axis=0, ext=1), 1], + # [domain_7.get_boundary(axis=0, ext=-1), domain_13_1.get_boundary(axis=0, ext=-1), 1], + # [domain_5.get_boundary(axis=0, ext=-1), domain_14_1.get_boundary(axis=0, ext=-1), 1], + # [domain_14_1.get_boundary(axis=0, ext=+1), domain_14_2.get_boundary(axis=0, ext=-1), 1], + # [domain_12.get_boundary(axis=0, ext=-1), domain_14_2.get_boundary(axis=0, ext=+1), 1], + # ] + + # interfaces = [ + # [domain_1_1.get_boundary(axis=1, ext=+1), domain_1_2.get_boundary(axis=1, ext=-1), 1], + # [domain_1_2.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1), 1], + # [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=1), 1], + # [domain_6.get_boundary(axis=1, ext=-1), domain_2_1.get_boundary(axis=1, ext=-1), 1], + # [domain_2_1.get_boundary(axis=1, ext=+1), domain_2_2.get_boundary(axis=1, ext=-1), 1], + # [domain_2_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1), 1], + # [domain_7.get_boundary(axis=1, ext=+1), domain_3_1.get_boundary(axis=1, ext=-1), 1], + # [domain_3_1.get_boundary(axis=1, ext=+1), domain_3_2.get_boundary(axis=1, ext=-1), 1], + # [domain_3_2.get_boundary(axis=1, ext=+1), domain_9_1.get_boundary(axis=1, ext=-1), 1], + # [domain_9_1.get_boundary(axis=1, ext=+1), domain_9_2.get_boundary(axis=1, ext=-1), 1], + # [domain_9_2.get_boundary(axis=1, ext=+1), domain_4_1.get_boundary(axis=1, ext=-1), 1], + # [domain_4_1.get_boundary(axis=1, ext=+1), domain_4_2.get_boundary(axis=1, ext=-1), 1], + # [domain_4_2.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=1), 1], + # [domain_12.get_boundary(axis=1, ext=-1), domain_1_1.get_boundary(axis=1, ext=-1), 1], + # [domain_6, axis_0, ext=-1), domain_13_2, axis_0, ext=1), 1], + # [domain_13_2, axis_0, ext=-1), domain_13_1, axis_0, ext=1), 1], + # [domain_7, axis_0, ext=-1), domain_13_1, axis_0, ext=-1), 1], + # [domain_5, axis_0, ext=-1), domain_14_1, axis_0, ext=-1), 1], + # [domain_14_1, axis_0, ext=+1), domain_14_2, axis_0, ext=-1), 1], + # [domain_12, axis_0, ext=-1), domain_14_2, axis_0, ext=+1), 1], + # ] + + connectivity = [ + [(domain_1_1, axis_1, ext_1), (domain_1_2, axis_1, ext_0), 1], + [(domain_1_2, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_6, axis_1, ext_1), 1], + [(domain_6, axis_1, ext_0), (domain_2_1, axis_1, ext_0), 1], + [(domain_2_1, axis_1, ext_1), (domain_2_2, axis_1, ext_0), 1], + [(domain_2_2, axis_1, ext_1), (domain_7, axis_1, ext_0), 1], + [(domain_7, axis_1, ext_1), (domain_3_1, axis_1, ext_0), 1], + [(domain_3_1, axis_1, ext_1), (domain_3_2, axis_1, ext_0), 1], + [(domain_3_2, axis_1, ext_1), (domain_9_1, axis_1, ext_0), 1], + [(domain_9_1, axis_1, ext_1), (domain_9_2, axis_1, ext_0), 1], + [(domain_9_2, axis_1, ext_1), (domain_4_1, axis_1, ext_0), 1], + [(domain_4_1, axis_1, ext_1), (domain_4_2, axis_1, ext_0), 1], + [(domain_4_2, axis_1, ext_1), (domain_12, axis_1, ext_1), 1], + [(domain_12, axis_1, ext_0), (domain_1_1, axis_1, ext_0), 1], + [(domain_6, axis_0, ext_0), (domain_13_2, axis_0, ext_1), 1], + [(domain_13_2, axis_0, ext_0), (domain_13_1, axis_0, ext_1), 1], + [(domain_7, axis_0, ext_0), (domain_13_1, axis_0, ext_0), 1], + [(domain_5, axis_0, ext_0), (domain_14_1, axis_0, ext_0), 1], + [(domain_14_1, axis_0, ext_1), (domain_14_2, axis_0, ext_0), 1], + [(domain_12, axis_0, ext_0), (domain_14_2, axis_0, ext_1), 1], + ] + + # reste: 13 et 14 elif domain_name == 'pretzel_annulus': # only the annulus part of the pretzel (not the inner arcs) patches = ([ - domain_1, - domain_5, - domain_6, - domain_2, - domain_7, - domain_3, - domain_9, - domain_4, - domain_12, - ]) - - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_5.get_boundary(axis=1, ext=-1),1], - [domain_5.get_boundary(axis=1, ext=+1), domain_6.get_boundary(axis=1, ext=-1),1], - [domain_6.get_boundary(axis=1, ext=+1), domain_2.get_boundary(axis=1, ext=-1),1], - [domain_2.get_boundary(axis=1, ext=+1), domain_7.get_boundary(axis=1, ext=-1),1], - [domain_7.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_9.get_boundary(axis=1, ext=-1),1], - [domain_9.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1),1], - [domain_4.get_boundary(axis=1, ext=+1), domain_12.get_boundary(axis=1, ext=-1),1], - [domain_12.get_boundary(axis=1, ext=+1), domain_1.get_boundary(axis=1, ext=-1),1], - ] + domain_1, + domain_5, + domain_6, + domain_2, + domain_7, + domain_3, + domain_9, + domain_4, + domain_12, + ]) + + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_5, axis_1, ext_0), 1], + [(domain_5, axis_1, ext_1), (domain_6, axis_1, ext_0), 1], + [(domain_6, axis_1, ext_1), (domain_2, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_7, axis_1, ext_0), 1], + [(domain_7, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_9, axis_1, ext_0), 1], + [(domain_9, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_12, axis_1, ext_0), 1], + [(domain_12, axis_1, ext_1), (domain_1, axis_1, ext_0), 1], + ] elif domain_name == 'pretzel_debug': patches = ([ - domain_1, - domain_10, - ]) + domain_1, + domain_10, + ]) - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_10.get_boundary(axis=1, ext=-1),1], - ] + connectivity = [[(domain_1, axis_1, ext_1), (domain_10, axis_1, ext_0), 1], ] else: raise NotImplementedError - elif domain_name == 'curved_L_shape': # Curved L-shape benchmark domain of Monique Dauge, see 2DomD in https://perso.univ-rennes1.fr/monique.dauge/core/index.html # here with 3 patches - dom_log_1 = Square('dom1',bounds1=(2, 3), bounds2=(0., np.pi/8)) - mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_1 = mapping_1(dom_log_1) - - dom_log_2 = Square('dom2',bounds1=(2, 3), bounds2=(np.pi/8, np.pi/4)) - mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_2 = mapping_2(dom_log_2) - - dom_log_3 = Square('dom3',bounds1=(1, 2), bounds2=(np.pi/8, np.pi/4)) - mapping_3 = PolarMapping('M3',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_3 = mapping_3(dom_log_3) + dom_log_1 = Square('dom1', bounds1=(2, 3), bounds2=(0., np.pi / 8)) + mapping_1 = PolarMapping('M1', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_1 = mapping_1(dom_log_1) + + dom_log_2 = Square( + 'dom2', bounds1=( + 2, 3), bounds2=( + np.pi / 8, np.pi / 4)) + mapping_2 = PolarMapping('M2', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_2 = mapping_2(dom_log_2) + + dom_log_3 = Square( + 'dom3', bounds1=( + 1, 2), bounds2=( + np.pi / 8, np.pi / 4)) + mapping_3 = PolarMapping('M3', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_3 = mapping_3(dom_log_3) patches = ([ - domain_1, - domain_2, - domain_3, - ]) - - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_2.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=0, ext=+1), domain_2.get_boundary(axis=0, ext=-1),1], + domain_1, + domain_2, + domain_3, + ]) + + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_2, axis_1, ext_0), 1], + [(domain_3, axis_0, ext_1), (domain_2, axis_0, ext_0), 1], ] elif domain_name in ['annulus_3', 'annulus_4']: # regular annulus if r_min is None: - r_min=0.5 # smaller radius + r_min = 0.5 # smaller radius if r_max is None: - r_max=1. # larger radius + r_max = 1. # larger radius if domain_name == 'annulus_3': - OmegaLog1 = Square('OmegaLog1',bounds1=(r_min, r_max), bounds2=(0., np.pi/2)) - mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_1 = mapping_1(OmegaLog1) - - OmegaLog2 = Square('OmegaLog2',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi)) - mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_2 = mapping_2(OmegaLog2) - - OmegaLog3 = Square('OmegaLog3',bounds1=(r_min, r_max), bounds2=(np.pi, 2*np.pi)) - mapping_3 = PolarMapping('M3',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_3 = mapping_3(OmegaLog3) + OmegaLog1 = Square( + 'OmegaLog1', bounds1=( + r_min, r_max), bounds2=( + 0., np.pi / 2)) + mapping_1 = PolarMapping('M1', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_1 = mapping_1(OmegaLog1) + + OmegaLog2 = Square( + 'OmegaLog2', bounds1=( + r_min, r_max), bounds2=( + np.pi / 2, np.pi)) + mapping_2 = PolarMapping('M2', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_2 = mapping_2(OmegaLog2) + + OmegaLog3 = Square( + 'OmegaLog3', bounds1=( + r_min, r_max), bounds2=( + np.pi, 2 * np.pi)) + mapping_3 = PolarMapping('M3', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_3 = mapping_3(OmegaLog3) patches = [domain_1, domain_2, domain_3] - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_2.get_boundary(axis=1, ext=-1),1], - [domain_2.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_1.get_boundary(axis=1, ext=-1),1], + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_2, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_1, axis_1, ext_0), 1], ] elif domain_name == 'annulus_4': - OmegaLog1 = Square('OmegaLog1',bounds1=(r_min, r_max), bounds2=(0., np.pi/2)) - mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_1 = mapping_1(OmegaLog1) - - OmegaLog2 = Square('OmegaLog2',bounds1=(r_min, r_max), bounds2=(np.pi/2, np.pi)) - mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_2 = mapping_2(OmegaLog2) - - OmegaLog3 = Square('OmegaLog3',bounds1=(r_min, r_max), bounds2=(np.pi, np.pi*3/2)) - mapping_3 = PolarMapping('M3',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_3 = mapping_3(OmegaLog3) - - OmegaLog4 = Square('OmegaLog4',bounds1=(r_min, r_max), bounds2=(np.pi*3/2, np.pi*2)) - mapping_4 = PolarMapping('M4',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - domain_4 = mapping_4(OmegaLog4) + OmegaLog1 = Square( + 'OmegaLog1', bounds1=( + r_min, r_max), bounds2=( + 0., np.pi / 2)) + mapping_1 = PolarMapping('M1', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_1 = mapping_1(OmegaLog1) + + OmegaLog2 = Square( + 'OmegaLog2', bounds1=( + r_min, r_max), bounds2=( + np.pi / 2, np.pi)) + mapping_2 = PolarMapping('M2', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_2 = mapping_2(OmegaLog2) + + OmegaLog3 = Square( + 'OmegaLog3', bounds1=( + r_min, r_max), bounds2=( + np.pi, np.pi * 3 / 2)) + mapping_3 = PolarMapping('M3', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_3 = mapping_3(OmegaLog3) + + OmegaLog4 = Square( + 'OmegaLog4', bounds1=( + r_min, r_max), bounds2=( + np.pi * 3 / 2, np.pi * 2)) + mapping_4 = PolarMapping('M4', 2, c1=0., c2=0., rmin=0., rmax=1.) + domain_4 = mapping_4(OmegaLog4) patches = [domain_1, domain_2, domain_3, domain_4] - interfaces = [ - [domain_1.get_boundary(axis=1, ext=+1), domain_2.get_boundary(axis=1, ext=-1),1], - [domain_2.get_boundary(axis=1, ext=+1), domain_3.get_boundary(axis=1, ext=-1),1], - [domain_3.get_boundary(axis=1, ext=+1), domain_4.get_boundary(axis=1, ext=-1),1], - [domain_4.get_boundary(axis=1, ext=+1), domain_1.get_boundary(axis=1, ext=-1),1], + connectivity = [ + [(domain_1, axis_1, ext_1), (domain_2, axis_1, ext_0), 1], + [(domain_2, axis_1, ext_1), (domain_3, axis_1, ext_0), 1], + [(domain_3, axis_1, ext_1), (domain_4, axis_1, ext_0), 1], + [(domain_4, axis_1, ext_1), (domain_1, axis_1, ext_0), 1], ] else: raise NotImplementedError @@ -594,91 +909,101 @@ def build_multipatch_domain(domain_name='square_2', r_min=None, r_max=None): else: raise NotImplementedError - domain = create_domain(patches, interfaces, name='domain') - - # print("int: ", domain.interior) - # print("bound: ", domain.boundary) - # print("len(bound): ", len(domain.boundary)) - # print("interfaces: ", domain.interfaces) - + # domain = Domain.join(patches, connectivity, name='domain') + domain = sympde_Domain_join(patches, connectivity, name='domain') + return domain -def get_ref_eigenvalues(domain_name, operator): - # return ref_eigenvalues for the given operator and domain - # and 'sigma' value, around which discrete eigenvalues will be searched by eigenvalue solver such as eigsh - # (Note: eigsh may yield a singular error if sigma is an exact discrete eigenvalue) - - assert operator in ['curl_curl', 'hodge_laplacian'] - ref_sigmas = [] +def build_cartesian_multipatch_domain(ncells, log_interval_x, log_interval_y, mapping='identity'): + """ + Create a 2D multipatch Cartesian domain with the prescribed pattern of patches and with possible mappings. - if domain_name in ['square_2','square_6']: - # todo - if operator == 'curl_curl': - ref_sigmas = [ - 1, - 2, - 2, - ] - raise NotImplementedError - else: - ref_sigmas = [ - 1, - 2, - 2, - ] - raise NotImplementedError - elif domain_name in ['annulus_3','annulus_4']: - if operator == 'curl_curl': - ref_sigmas = [ - 1, - 2, - 2, - ] - raise NotImplementedError - else: - ref_sigmas = [ - 1, - 2, - 2, - ] - raise NotImplementedError + Parameters + ---------- + ncells: + (Incomplete) Cartesian grid of patches, where some patches may be empty. + The pattern of the multipatch domain is defined by the non-None entries of the matrix ncells. + (Different numerical values will give rise to the same multipatch decompostion) + + ncells can then be used (afterwards) for discretizing the domain with ncells[i,j] being the number of cells + (assumed isotropic in each patch) in the patch (i,j), and None for empty patches (removed from domain). + + Example: + + >>> ncells = np.array([[1, None, 5], + >>> [2, 3, 4]]) + + corresponds to a domain with 5 patches as follows: + + >>> |X| |X| + >>> ------- + >>> |X|X|X| + + log_interval_x: + The interval in the x direction in the logical domain. + log_interval_y: + The interval in the y direction in the logical domain. + mapping: + The type of mapping to use. Can be 'identity' or 'polar'. - elif domain_name == 'curved_L_shape': - if operator == 'curl_curl': - # sigma = 10 - ref_sigmas = [ - 0.181857115231E+01, - 0.349057623279E+01, - 0.100656015004E+02, - 0.101118862307E+02, - 0.124355372484E+02, - ] - elif operator == 'hodge_laplacian': - raise NotImplementedError - else: - raise NotImplementedError + Returns + ------- + domain : + The symbolic multipatch domain + """ + ax, bx = log_interval_x + ay, by = log_interval_y + nb_patchx, nb_patchy = np.shape(ncells) + + # equidistant logical patches + # ensure the following lists have the same shape as ncells + list_log_patches = [[Square('Log_' + str(j) + '_' + str(i), + bounds1=(ax + j / nb_patchx * (bx - ax), ax + (j + 1) / nb_patchx * (bx - ax)), + bounds2=(by - (i + 1) / nb_patchy * (by - ay), by - i / nb_patchy * (by - ay))) + for i in range(nb_patchy)] for j in range(nb_patchx)] + + # mappings + if mapping == 'identity': + list_mapping = [[IdentityMapping('M_' + str(j) + '_' + str(i), 2) + for i in range(nb_patchy)] for j in range(nb_patchx)] + elif mapping == 'polar': + list_mapping = [[PolarMapping('M_' + str(j) + '_' + str(i), 2, c1=0., c2=0., rmin=0., rmax=1.) + for i in range(nb_patchy)] for j in range(nb_patchx)] + + # list of physical patches + list_patches = [[list_mapping[j][i](list_log_patches[j][i]) + for i in range(nb_patchy)] for j in range(nb_patchx)] + + # flatten for the join function + patches = [] + for i in range(nb_patchx): + for j in range(nb_patchy): + if ncells[i, j] is not None: + patches.append(list_patches[j][i]) + + axis_0 = 0 + axis_1 = 1 + ext_0 = -1 + ext_1 = +1 + connectivity = [] - elif domain_name == 'pretzel': - if operator == 'curl_curl': - raise NotImplementedError - elif operator == 'hodge_laplacian': - ref_sigmas = [ - 0, - 0, - 0, - 0.1795447761871659, - 0.19922705025897117, - 0.699286528403241, - 0.8709410737744409, - 1.1945444491250097, - ] - else: - raise NotImplementedError - else: - raise NotImplementedError + # interfaces in y + for i in range(nb_patchx): + connectivity.extend([ + [(list_patches[j ][i], axis_0, ext_1), + (list_patches[j+1][i], axis_0, ext_0), 1] + for j in range(nb_patchy -1) if ncells[i][j] is not None and ncells[i][j+1] is not None]) - sigma = ref_sigmas[len(ref_sigmas)//2] + # interfaces in x + for j in range(nb_patchy): + connectivity.extend([ + [(list_patches[j][i ], axis_1, ext_0), + (list_patches[j][i+1], axis_1, ext_1), 1] + for i in range(nb_patchx -1) if ncells[i][j] is not None and ncells[i+1][j] is not None]) - return sigma, ref_sigmas + # domain = Domain.join(patches, connectivity, name='domain') + domain = sympde_Domain_join(patches, connectivity, name='domain') + return domain + \ No newline at end of file diff --git a/psydac/feec/multipatch/non_matching_operators.py b/psydac/feec/multipatch/non_matching_operators.py new file mode 100644 index 000000000..461509646 --- /dev/null +++ b/psydac/feec/multipatch/non_matching_operators.py @@ -0,0 +1,1186 @@ +""" +This module provides utilities for constructing the conforming projections +for a H1-Hcurl-L2 broken FEEC de Rham sequence. +""" + +import os + +import numpy as np + +from scipy.sparse import eye as sparse_eye +from scipy.sparse import csr_matrix + +from sympde.topology import Boundary, Interface + +from psydac.fem.splines import SplineSpace +from psydac.utilities.quadratures import gauss_legendre +from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid + + +def get_patch_index_from_face(domain, face): + """ + Return the patch index of subdomain/boundary + + Parameters + ---------- + domain : + The Symbolic domain + + face : + A patch or a boundary of a patch + + Returns + ------- + i : + The index of a subdomain/boundary in the multipatch domain + """ + + if domain.mapping: + domain = domain.logical_domain + if face.mapping: + face = face.logical_domain + + domains = domain.interior.args + if isinstance(face, Interface): + raise NotImplementedError( + "This face is an interface, it has several indices -- I am a machine, I cannot choose. Help.") + elif isinstance(face, Boundary): + i = domains.index(face.domain) + else: + i = domains.index(face) + return i + + +class Local2GlobalIndexMap: + def __init__(self, ndim, n_patches, n_components): + self._shapes = [None] * n_patches + self._ndofs = [None] * n_patches + self._ndim = ndim + self._n_patches = n_patches + self._n_components = n_components + + def set_patch_shapes(self, patch_index, *shapes): + assert len(shapes) == self._n_components + assert all(len(s) == self._ndim for s in shapes) + self._shapes[patch_index] = shapes + self._ndofs[patch_index] = sum(np.prod(s) for s in shapes) + + def get_index(self, k, d, cartesian_index): + """ Return a global scalar index. + + Parameters + ---------- + k : int + The patch index. + + d : int + The component of a scalar field in the system of equations. + + cartesian_index: tuple[int] + Multi index [i1, i2, i3 ...] + + Returns + ------- + I : int + The global scalar index. + """ + sizes = [np.prod(s) for s in self._shapes[k][:d]] + Ipc = np.ravel_multi_index( + cartesian_index, dims=self._shapes[k][d], order='C') + Ip = sum(sizes) + Ipc + I = sum(self._ndofs[:k]) + Ip + return I + + +def knots_to_insert(coarse_grid, fine_grid, tol=1e-14): + """knot insertion for refinement of a 1d spline space.""" + intersection = coarse_grid[( + np.abs(fine_grid[:, None] - coarse_grid) < tol).any(0)] + assert abs(intersection - coarse_grid).max() < tol + T = fine_grid[~(np.abs(coarse_grid[:, None] - fine_grid) < tol).any(0)] + return T + + +def get_corners(domain, boundary_only): + """ + Given the domain, extract the vertices on their respective domains with local coordinates. + + Parameters + ---------- + domain: + The discrete domain of the projector + + boundary_only : + Only return vertices that lie on a boundary + + """ + cos = domain.corners + patches = domain.interior.args + bd = domain.boundary + + # corner_data[corner] = (patch_ind => local coordinates) + corner_data = dict() + + if boundary_only: + for co in cos: + + corner_data[co] = dict() + c = False + for cb in co.corners: + axis = set() + # check if corner boundary is part of the domain boundary + for cbbd in cb.args: + if bd.has(cbbd): + c = True + + p_ind = patches.index(cb.domain) + c_coord = cb.coordinates + corner_data[co][p_ind] = c_coord + + if not c: + corner_data.pop(co) + + else: + for co in cos: + corner_data[co] = dict() + + for cb in co.corners: + p_ind = patches.index(cb.domain) + c_coord = cb.coordinates + corner_data[co][p_ind] = c_coord + + return corner_data + + +def construct_extension_operator_1D(domain, codomain): + """ + Compute the matrix of the extension operator on the interface. + + Parameters + ---------- + domain : 1d spline space on the interface (coarse grid) + codomain : 1d spline space on the interface (fine grid) + """ + + from psydac.core.bsplines import hrefinement_matrix + ops = [] + + assert domain.ncells <= codomain.ncells + + Ts = knots_to_insert(domain.breaks, codomain.breaks) + P = hrefinement_matrix(Ts, domain.degree, domain.knots) + + if domain.basis == 'M': + assert codomain.basis == 'M' + P = np.diag( + 1 / codomain._scaling_array) @ P @ np.diag(domain._scaling_array) + + return csr_matrix(P) + + +def construct_restriction_operator_1D( + coarse_space_1d, fine_space_1d, E, p_moments=-1): + """ + Compute the matrix of the (moment preserving) restriction operator on the interface. + + Parameters + ---------- + coarse_space_1d : 1d spline space on the interface (coarse grid) + fine_space_1d : 1d spline space on the interface (fine grid) + E : Extension matrix + p_moments : Amount of moments to be preserved + """ + n_c = coarse_space_1d.nbasis + n_f = fine_space_1d.nbasis + + R = np.zeros((n_c, n_f)) + + if coarse_space_1d.basis == 'B': + + T = np.zeros((n_f, n_f)) + for i in range(1, n_f - 1): + for j in range(n_f): + T[i, j] = int(i == j) - E[i, 0] * int(0 == j) - \ + E[i, -1] * int(n_f - 1 == j) + + cf_mass_mat = calculate_mixed_mass_matrix(coarse_space_1d, fine_space_1d)[ + 1:-1, 1:-1].transpose() + c_mass_mat = calculate_mass_matrix(coarse_space_1d)[1:-1, 1:-1] + + if p_moments > 0: + + if not p_moments % 2 == 0: + p_moments += 1 + c_poly_mat = calculate_poly_basis_integral( + coarse_space_1d, p_moments=p_moments - 1)[:, 1:-1] + f_poly_mat = calculate_poly_basis_integral( + fine_space_1d, p_moments=p_moments - 1)[:, 1:-1] + + c_mass_mat[0:p_moments // 2, :] = c_poly_mat[0:p_moments // 2, :] + c_mass_mat[-p_moments // 2:, :] = c_poly_mat[-p_moments // 2:, :] + + cf_mass_mat[0:p_moments // 2, :] = f_poly_mat[0:p_moments // 2, :] + cf_mass_mat[-p_moments // 2:, :] = f_poly_mat[-p_moments // 2:, :] + + R0 = np.linalg.solve(c_mass_mat, cf_mass_mat) + R[1:-1, 1:-1] = R0 + R = R @ T + + R[0, 0] += 1 + R[-1, -1] += 1 + else: + + cf_mass_mat = calculate_mixed_mass_matrix( + coarse_space_1d, fine_space_1d).transpose() + c_mass_mat = calculate_mass_matrix(coarse_space_1d) + + if p_moments > 0: + + if not p_moments % 2 == 0: + p_moments += 1 + c_poly_mat = calculate_poly_basis_integral( + coarse_space_1d, p_moments=p_moments - 1) + f_poly_mat = calculate_poly_basis_integral( + fine_space_1d, p_moments=p_moments - 1) + + c_mass_mat[0:p_moments // 2, :] = c_poly_mat[0:p_moments // 2, :] + c_mass_mat[-p_moments // 2:, :] = c_poly_mat[-p_moments // 2:, :] + + cf_mass_mat[0:p_moments // 2, :] = f_poly_mat[0:p_moments // 2, :] + cf_mass_mat[-p_moments // 2:, :] = f_poly_mat[-p_moments // 2:, :] + + R = np.linalg.solve(c_mass_mat, cf_mass_mat) + + return R + + +def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): + """ + Calculate the extension and restriction matrices for refining along an interface. + + Parameters + ---------- + + coarse_space_1d : SplineSpace + Spline space of the coarse space. + + fine_space_1d : SplineSpace + Spline space of the fine space. + + p_moments : {int} + Amount of moments to be preserved. + + Returns + ------- + E_1D : numpy array + Extension matrix. + + R_1D : numpy array + Restriction matrix. + + ER_1D : numpy array + Extension-restriction matrix. + """ + matching_interfaces = (coarse_space_1d.ncells == fine_space_1d.ncells) + assert (coarse_space_1d.degree == fine_space_1d.degree) + assert (coarse_space_1d.basis == fine_space_1d.basis) + spl_type = coarse_space_1d.basis + + if not matching_interfaces: + grid = np.linspace( + fine_space_1d.breaks[0], fine_space_1d.breaks[-1], coarse_space_1d.ncells + 1) + coarse_space_1d_k_plus = SplineSpace( + degree=fine_space_1d.degree, + grid=grid, + basis=fine_space_1d.basis) + + E_1D = construct_extension_operator_1D( + domain=coarse_space_1d_k_plus, codomain=fine_space_1d) + + R_1D = construct_restriction_operator_1D( + coarse_space_1d_k_plus, fine_space_1d, E_1D, p_moments) + + ER_1D = E_1D @ R_1D + + else: + ER_1D = R_1D = E_1D = sparse_eye( + fine_space_1d.nbasis, format="lil") + + # TODO remove later + assert ( + np.allclose( + np.linalg.norm( + R_1D @ E_1D - + np.eye( + coarse_space_1d.nbasis)), + 0, + 1e-12, + 1e-12)) + return E_1D, R_1D, ER_1D + + +# Didn't find this utility in the code base. +def calculate_mass_matrix(space_1d): + """ + Calculate the mass-matrix of a 1d spline-space. + + Parameters + ---------- + + space_1d : SplineSpace + Spline space of the fine space. + + Returns + ------- + + Mass_mat : numpy array + Mass matrix. + """ + Nel = space_1d.ncells + deg = space_1d.degree + knots = space_1d.knots + spl_type = space_1d.basis + + u, w = gauss_legendre(deg + 1) + + nquad = len(w) + quad_x, quad_w = quadrature_grid(space_1d.breaks, u, w) + + basis = basis_ders_on_quad_grid(knots, deg, quad_x, 0, spl_type) + spans = elements_spans(knots, deg) + + Mass_mat = np.zeros((space_1d.nbasis, space_1d.nbasis)) + + for ie1 in range(Nel): # loop on cells + for il1 in range(deg + 1): # loops on basis function in each cell + for il2 in range(deg + 1): # loops on basis function in each cell + val = 0. + + for q1 in range(nquad): # loops on quadrature points + v0 = basis[ie1, il1, 0, q1] + w0 = basis[ie1, il2, 0, q1] + val += quad_w[ie1, q1] * v0 * w0 + + locind1 = il1 + spans[ie1] - deg + locind2 = il2 + spans[ie1] - deg + Mass_mat[locind1, locind2] += val + + return Mass_mat + + +# Didn't find this utility in the code base. +def calculate_mixed_mass_matrix(domain_space, codomain_space): + """ + Calculate the mixed mass-matrix of two 1d spline-spaces on the same domain. + + Parameters + ---------- + + domain_space : SplineSpace + Spline space of the domain space. + + codomain_space : SplineSpace + Spline space of the codomain space. + + Returns + ------- + + Mass_mat : numpy array + Mass matrix. + """ + if domain_space.nbasis > codomain_space.nbasis: + coarse_space = codomain_space + fine_space = domain_space + else: + coarse_space = domain_space + fine_space = codomain_space + + deg = coarse_space.degree + knots = coarse_space.knots + spl_type = coarse_space.basis + breaks = coarse_space.breaks + + fdeg = fine_space.degree + fknots = fine_space.knots + fbreaks = fine_space.breaks + fspl_type = fine_space.basis + fNel = fine_space.ncells + + assert spl_type == fspl_type + assert deg == fdeg + assert ((knots[0] == fknots[0]) and (knots[-1] == fknots[-1])) + + u, w = gauss_legendre(deg + 1) + + nquad = len(w) + quad_x, quad_w = quadrature_grid(fbreaks, u, w) + + fine_basis = basis_ders_on_quad_grid(fknots, fdeg, quad_x, 0, spl_type) + coarse_basis = [ + basis_ders_on_irregular_grid( + knots, deg, q, cell_index( + breaks, q), 0, spl_type) for q in quad_x] + + fine_spans = elements_spans(fknots, deg) + coarse_spans = [find_spans(knots, deg, q[0])[0] for q in quad_x] + + Mass_mat = np.zeros((fine_space.nbasis, coarse_space.nbasis)) + + for ie1 in range(fNel): # loop on cells + for il1 in range(deg + 1): # loops on basis function in each cell + for il2 in range(deg + 1): # loops on basis function in each cell + val = 0. + + for q1 in range(nquad): # loops on quadrature points + v0 = fine_basis[ie1, il1, 0, q1] + w0 = coarse_basis[ie1][q1, il2, 0] + val += quad_w[ie1, q1] * v0 * w0 + + locind1 = il1 + fine_spans[ie1] - deg + locind2 = il2 + coarse_spans[ie1] - deg + Mass_mat[locind1, locind2] += val + + return Mass_mat + + +def calculate_poly_basis_integral(space_1d, p_moments=-1): + """ + Calculate the "mixed mass-matrix" of a 1d spline-space with polynomials. + + Parameters + ---------- + + space_1d : SplineSpace + Spline space of the fine space. + + p_moments : Int + Amount of moments to be preserved. + + Returns + ------- + + Mass_mat : numpy array + Mass matrix. + """ + + Nel = space_1d.ncells + deg = space_1d.degree + knots = space_1d.knots + spl_type = space_1d.basis + breaks = space_1d.breaks + enddom = breaks[-1] + begdom = breaks[0] + denom = enddom - begdom + + order = max(p_moments + 1, deg + 1) + u, w = gauss_legendre(order) + + nquad = len(w) + quad_x, quad_w = quadrature_grid(space_1d.breaks, u, w) + + coarse_basis = basis_ders_on_quad_grid(knots, deg, quad_x, 0, spl_type) + spans = elements_spans(knots, deg) + + Mass_mat = np.zeros((p_moments + 1, space_1d.nbasis)) + + for ie1 in range(Nel): # loop on cells + for pol in range( + p_moments + 1): # loops on basis function in each cell + for il2 in range(deg + 1): # loops on basis function in each cell + val = 0. + + for q1 in range(nquad): # loops on quadrature points + v0 = coarse_basis[ie1, il2, 0, q1] + x = quad_x[ie1, q1] + # val += quad_w[ie1, q1] * v0 * ((enddom-x)/denom)**pol + val += quad_w[ie1, q1] * v0 * \ + ((enddom - x) / denom)**(p_moments - pol) * (x / denom)**pol + locind2 = il2 + spans[ie1] - deg + Mass_mat[pol, locind2] += val + + return Mass_mat + + +def get_1d_moment_correction(space_1d, p_moments=-1): + """ + Calculate the coefficients for the one-dimensional moment correction. + + Parameters + ---------- + patch_space : SplineSpace + 1d spline space. + + p_moments : int + Number of moments to be preserved. + + Returns + ------- + gamma : array + Moment correction coefficients without the conformity factor. + """ + + if p_moments < 0: + return None + + if space_1d.ncells <= p_moments + 1: + print("Careful, the correction term is currently not independent of the mesh.") + + if p_moments >= 0: + # to preserve moments of degree p we need 1+p conforming basis functions in the patch (the "interior" ones) + # and for the given regularity constraint, there are + # local_shape[conf_axis]-2*(1+reg) such conforming functions + p_max = space_1d.nbasis - 3 + if p_max < p_moments: + print( + " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + print(" ** WARNING -- WARNING -- WARNING ") + print( + f" ** conf. projection imposing C0 smoothness on scalar space along this axis :") + print( + f" ** there are not enough dofs in a patch to preserve moments of degree {p_moments} !") + print(f" ** Only able to preserve up to degree --> {p_max} <-- ") + print( + " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + p_moments = p_max + + Mass_mat = calculate_poly_basis_integral(space_1d, p_moments) + gamma = np.linalg.solve(Mass_mat[:, 1:p_moments + 2], Mass_mat[:, 0]) + + return gamma + + +def construct_h1_conforming_projection( + Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a scalar space for a given regularity (0 continuous, -1 discontinuous). + + Parameters + ---------- + Vh : TensorFemSpace + Finite Element Space coming from the discrete de Rham sequence. + + reg_orders : (int) + Regularity in each space direction -1 or 0. + + p_moments : (int) + Number of moments to be preserved. + + hom_bc : (bool) + Homogeneous boundary conditions. + + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ + + dim_tot = Vh.nbasis + + # fully discontinuous space + if reg_orders < 0: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # assume same moments everywhere + gamma = get_1d_moment_correction( + Vh.spaces[0].spaces[0], p_moments=p_moments) + + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 1 + n_patches = len(domain) + + l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) + for k in range(n_patches): + Vk = Vh.spaces[k] + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [S.nbasis for S in Vk.spaces] + l2g.set_patch_shapes(k, shapes) + + # P vertex + # vertex correction matrix + Proj_vertex = sparse_eye(dim_tot, format="lil") + + corner_indices = set() + corners = get_corners(domain, False) + + def get_vertex_index_from_patch(patch, coords): + # coords = co[patch] + nbasis0 = Vh.spaces[patch].spaces[coords[0]].nbasis - 1 + nbasis1 = Vh.spaces[patch].spaces[coords[1]].nbasis - 1 + + # patch local index + multi_index = [None] * ndim + multi_index[0] = 0 if coords[0] == 0 else nbasis0 + multi_index[1] = 0 if coords[1] == 0 else nbasis1 + + # global index + return l2g.get_index(patch, 0, multi_index) + + def vertex_moment_indices(axis, coords, patch, p_moments): + if coords[axis] == 0: + return range(1, p_moments + 2) + else: + return range(Vh.spaces[patch].spaces[coords[axis]].nbasis - 1 - 1, + Vh.spaces[patch].spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) + + # loop over all vertices + for (bd, co) in corners.items(): + + # len(co)=#v is the number of adjacent patches at a vertex + corr = len(co) + + for patch1 in co: + # local vertex coordinates in patch1 + coords1 = co[patch1] + # global index + ig = get_vertex_index_from_patch(patch1, coords1) + + corner_indices.add(ig) + + for patch2 in co: + + # local vertex coordinates in patch2 + coords2 = co[patch2] + # global index + jg = get_vertex_index_from_patch(patch2, coords2) + + # conformity constraint + Proj_vertex[jg, ig] = 1 / corr + + if patch1 == patch2: + continue + + if p_moments == -1: + continue + + # moment corrections from patch1 to patch2 + axis = 0 + d = 1 + multi_index_p = [None] * ndim + + d_moment_index = vertex_moment_indices( + d, coords2, patch2, p_moments) + axis_moment_index = vertex_moment_indices( + axis, coords2, patch2, p_moments) + + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] + + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] + + pg = l2g.get_index(patch2, 0, multi_index_p) + Proj_vertex[pg, ig] += - 1 / \ + corr * gamma[p] * gamma[pd] + + if p_moments == -1: + continue + + # moment corrections from patch1 to patch1 + axis = 0 + d = 1 + multi_index_p = [None] * ndim + + d_moment_index = vertex_moment_indices( + d, coords1, patch1, p_moments) + axis_moment_index = vertex_moment_indices( + axis, coords1, patch1, p_moments) + + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] + + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] + + pg = l2g.get_index(patch1, 0, multi_index_p) + Proj_vertex[pg, ig] += (1 - 1 / corr) * \ + gamma[p] * gamma[pd] + + # boundary conditions + corners = get_corners(domain, True) + if hom_bc: + for (bd, co) in corners.items(): + + for patch1 in co: + + # local vertex coordinates in patch2 + coords1 = co[patch1] + + # global index + ig = get_vertex_index_from_patch(patch1, coords1) + + for patch2 in co: + + # local vertex coordinates in patch2 + coords2 = co[patch2] + # global index + jg = get_vertex_index_from_patch(patch2, coords2) + + # conformity constraint + Proj_vertex[jg, ig] = 0 + + if patch1 == patch2: + continue + + if p_moments == -1: + continue + + # moment corrections from patch1 to patch2 + axis = 0 + d = 1 + multi_index_p = [None] * ndim + + d_moment_index = vertex_moment_indices( + d, coords2, patch2, p_moments) + axis_moment_index = vertex_moment_indices( + axis, coords2, patch2, p_moments) + + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] + + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] + + pg = l2g.get_index(patch2, 0, multi_index_p) + Proj_vertex[pg, ig] = 0 + + if p_moments == -1: + continue + + # moment corrections from patch1 to patch1 + axis = 0 + d = 1 + multi_index_p = [None] * ndim + + d_moment_index = vertex_moment_indices( + d, coords1, patch1, p_moments) + axis_moment_index = vertex_moment_indices( + axis, coords1, patch1, p_moments) + + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] + + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] + + pg = l2g.get_index(patch1, 0, multi_index_p) + Proj_vertex[pg, ig] = gamma[p] * gamma[pd] + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + Interfaces = domain.interfaces + if isinstance(Interfaces, Interface): + Interfaces = (Interfaces, ) + + def get_edge_index(j, axis, ext, space, k): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == - 1 else space.spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(k, 0, multi_index) + + def edge_moment_index(p, i, axis, ext, space, k): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == - \ + 1 else space.spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(k, 0, multi_index) + + def get_mu_plus(j, fine_space): + mu_plus = np.zeros(fine_space.nbasis) + for p in range(p_moments + 1): + if j == 0: + mu_plus[p + 1] = gamma[p] + else: + mu_plus[j - (p + 1)] = gamma[p] + return mu_plus + + def get_mu_minus(j, coarse_space, fine_space, R): + mu_plus = np.zeros(fine_space.nbasis) + mu_minus = np.zeros(coarse_space.nbasis) + + if j == 0: + mu_minus[0] = 1 + for p in range(p_moments + 1): + mu_plus[p + 1] = gamma[p] + else: + mu_minus[-1] = 1 + for p in range(p_moments + 1): + mu_plus[-1 - (p + 1)] = gamma[p] + + for m in range(coarse_space.nbasis): + for l in range(fine_space.nbasis): + mu_minus[m] += R[m, l] * mu_plus[l] + + if j == 0: + mu_minus[m] -= R[m, 0] + else: + mu_minus[m] -= R[m, -1] + + return mu_minus + + # loop over all interfaces + for I in Interfaces: + axis = I.axis + direction = I.ornt + # for now assume the interfaces are along the same direction + assert direction == 1 + k_minus = get_patch_index_from_face(domain, I.minus) + k_plus = get_patch_index_from_face(domain, I.plus) + + I_minus_ncells = Vh.spaces[k_minus].ncells + I_plus_ncells = Vh.spaces[k_plus].ncells + + # logical directions normal to interface + if I_minus_ncells <= I_plus_ncells: + k_fine, k_coarse = k_plus, k_minus + fine_axis, coarse_axis = I.plus.axis, I.minus.axis + fine_ext, coarse_ext = I.plus.ext, I.minus.ext + + else: + k_fine, k_coarse = k_minus, k_plus + fine_axis, coarse_axis = I.minus.axis, I.plus.axis + fine_ext, coarse_ext = I.minus.ext, I.plus.ext + + # logical directions along the interface + d_fine = 1 - fine_axis + d_coarse = 1 - coarse_axis + + space_fine = Vh.spaces[k_fine] + space_coarse = Vh.spaces[k_coarse] + + coarse_space_1d = space_coarse.spaces[d_coarse] + fine_space_1d = space_fine.spaces[d_fine] + E_1D, R_1D, ER_1D = get_extension_restriction( + coarse_space_1d, fine_space_1d, p_moments=p_moments) + + # Projecting coarse basis functions + for j in range(coarse_space_1d.nbasis): + jg = get_edge_index( + j, + coarse_axis, + coarse_ext, + space_coarse, + k_coarse) + + if (not corner_indices.issuperset({jg})): + + Proj_edge[jg, jg] = 1 / 2 + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, j, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[pg, jg] += 1 / 2 * gamma[p] + + for i in range(fine_space_1d.nbasis): + ig = get_edge_index( + i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * E_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[pg, jg] += -1 / 2 * gamma[p] * E_1D[i, j] + else: + mu_minus = get_mu_minus( + j, coarse_space_1d, fine_space_1d, R_1D) + + for p in range(p_moments + 1): + for m in range(coarse_space_1d.nbasis): + pg = edge_moment_index( + p, m, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[pg, jg] += 1 / 2 * gamma[p] * mu_minus[m] + + for i in range(1, fine_space_1d.nbasis - 1): + ig = get_edge_index( + i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * E_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + for m in range(coarse_space_1d.nbasis): + Proj_edge[pg, jg] += -1 / 2 * \ + gamma[p] * E_1D[i, m] * mu_minus[m] + + # Projecting fine basis functions + for j in range(fine_space_1d.nbasis): + jg = get_edge_index(j, fine_axis, fine_ext, space_fine, k_fine) + + if (not corner_indices.issuperset({jg})): + for i in range(fine_space_1d.nbasis): + ig = get_edge_index( + i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * ER_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[pg, jg] += 1 / 2 * gamma[p] * ER_1D[i, j] + + for i in range(coarse_space_1d.nbasis): + ig = get_edge_index( + i, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[ig, jg] = 1 / 2 * R_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[pg, jg] += - 1 / 2 * gamma[p] * R_1D[i, j] + else: + mu_plus = get_mu_plus(j, fine_space_1d) + + for i in range(1, fine_space_1d.nbasis - 1): + ig = get_edge_index( + i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * ER_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + + for m in range(fine_space_1d.nbasis): + Proj_edge[pg, jg] += 1 / 2 * \ + gamma[p] * ER_1D[i, m] * mu_plus[m] + + for i in range(1, coarse_space_1d.nbasis - 1): + ig = get_edge_index( + i, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[ig, jg] = 1 / 2 * R_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, coarse_axis, coarse_ext, space_coarse, k_coarse) + + for m in range(fine_space_1d.nbasis): + Proj_edge[pg, jg] += - 1 / 2 * \ + gamma[p] * R_1D[i, m] * mu_plus[m] + + # boundary condition + if hom_bc: + for bn in domain.boundary: + k = get_patch_index_from_face(domain, bn) + space_k = Vh.spaces[k] + axis = bn.axis + + d = 1 - axis + ext = bn.ext + space_k_1d = space_k.spaces[d] + + for i in range(0, space_k_1d.nbasis): + ig = get_edge_index(i, axis, ext, space_k, k) + Proj_edge[ig, ig] = 0 + + if (i != 0 and i != space_k_1d.nbasis - 1): + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext, space_k, k) + Proj_edge[pg, ig] = gamma[p] + else: + if corner_indices.issuperset({ig}): + mu_minus = get_mu_minus( + j, space_k_1d, space_k_1d, np.eye( + space_k_1d.nbasis)) + + for p in range(p_moments + 1): + for m in range(space_k_1d.nbasis): + pg = edge_moment_index( + p, m, axis, ext, space_k, k) + Proj_edge[pg, ig] = gamma[p] * mu_minus[m] + else: + multi_index = [None] * ndim + + for p in range(p_moments + 1): + multi_index[axis] = p + 1 if ext == - \ + 1 else space_k.spaces[axis].nbasis - 1 - p - 1 + for pd in range(p_moments + 1): + multi_index[1 - axis] = pd + \ + 1 if i == 0 else space_k.spaces[1 - + axis].nbasis - 1 - pd - 1 + pg = l2g.get_index(k, 0, multi_index) + Proj_edge[pg, ig] = gamma[p] * gamma[pd] + + return Proj_edge @ Proj_vertex + + +def construct_hcurl_conforming_projection( + Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a vector Hcurl space for a given regularity (0 continuous, -1 discontinuous). + + Parameters + ---------- + Vh : TensorFemSpace + Finite Element Space coming from the discrete de Rham sequence. + + reg_orders : (int) + Regularity in each space direction -1 or 0. + + p_moments : (int) + Number of polynomial moments to be preserved. + + hom_bc : (bool) + Tangential homogeneous boundary conditions. + + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ + + dim_tot = Vh.nbasis + + # fully discontinuous space + if reg_orders < 0: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + gamma = [get_1d_moment_correction( + Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 2 + n_patches = len(domain) + + l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) + for k in range(n_patches): + Vk = Vh.spaces[k] + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [[S.nbasis for S in T.spaces] for T in Vk.spaces] + l2g.set_patch_shapes(k, *shapes) + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + Interfaces = domain.interfaces + if isinstance(Interfaces, Interface): + Interfaces = (Interfaces, ) + + def get_edge_index(j, axis, ext, space, k): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == - \ + 1 else space.spaces[1 - axis].spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(k, 1 - axis, multi_index) + + def edge_moment_index(p, i, axis, ext, space, k): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == - \ + 1 else space.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(k, 1 - axis, multi_index) + + # loop over all interfaces + for I in Interfaces: + direction = I.ornt + # for now assume the interfaces are along the same direction + assert direction == 1 + k_minus = get_patch_index_from_face(domain, I.minus) + k_plus = get_patch_index_from_face(domain, I.plus) + + # logical directions normal to interface + minus_axis, plus_axis = I.minus.axis, I.plus.axis + # logical directions along the interface + d_minus, d_plus = 1 - minus_axis, 1 - plus_axis + I_minus_ncells = Vh.spaces[k_minus].spaces[d_minus].ncells[d_minus] + I_plus_ncells = Vh.spaces[k_plus].spaces[d_plus].ncells[d_plus] + + # logical directions normal to interface + if I_minus_ncells <= I_plus_ncells: + k_fine, k_coarse = k_plus, k_minus + fine_axis, coarse_axis = I.plus.axis, I.minus.axis + fine_ext, coarse_ext = I.plus.ext, I.minus.ext + + else: + k_fine, k_coarse = k_minus, k_plus + fine_axis, coarse_axis = I.minus.axis, I.plus.axis + fine_ext, coarse_ext = I.minus.ext, I.plus.ext + + # logical directions along the interface + d_fine = 1 - fine_axis + d_coarse = 1 - coarse_axis + + space_fine = Vh.spaces[k_fine] + space_coarse = Vh.spaces[k_coarse] + + coarse_space_1d = space_coarse.spaces[d_coarse].spaces[d_coarse] + fine_space_1d = space_fine.spaces[d_fine].spaces[d_fine] + E_1D, R_1D, ER_1D = get_extension_restriction( + coarse_space_1d, fine_space_1d, p_moments=p_moments) + + # Projecting coarse basis functions + for j in range(coarse_space_1d.nbasis): + jg = get_edge_index( + j, + coarse_axis, + coarse_ext, + space_coarse, + k_coarse) + + Proj_edge[jg, jg] = 1 / 2 + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, j, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[pg, jg] += 1 / 2 * gamma[d_coarse][p] + + for i in range(fine_space_1d.nbasis): + ig = get_edge_index(i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * E_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[pg, jg] += -1 / 2 * gamma[d_fine][p] * E_1D[i, j] + + # Projecting fine basis functions + for j in range(fine_space_1d.nbasis): + jg = get_edge_index(j, fine_axis, fine_ext, space_fine, k_fine) + + for i in range(fine_space_1d.nbasis): + ig = get_edge_index(i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[ig, jg] = 1 / 2 * ER_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, fine_axis, fine_ext, space_fine, k_fine) + Proj_edge[pg, jg] += 1 / 2 * gamma[d_fine][p] * ER_1D[i, j] + + for i in range(coarse_space_1d.nbasis): + ig = get_edge_index( + i, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[ig, jg] = 1 / 2 * R_1D[i, j] + + for p in range(p_moments + 1): + pg = edge_moment_index( + p, i, coarse_axis, coarse_ext, space_coarse, k_coarse) + Proj_edge[pg, jg] += - 1 / 2 * \ + gamma[d_coarse][p] * R_1D[i, j] + + # boundary condition + for bn in domain.boundary: + k = get_patch_index_from_face(domain, bn) + space_k = Vh.spaces[k] + axis = bn.axis + + if not hom_bc: + continue + + d = 1 - axis + ext = bn.ext + space_k_1d = space_k.spaces[d].spaces[d] + + for i in range(0, space_k_1d.nbasis): + ig = get_edge_index(i, axis, ext, space_k, k) + Proj_edge[ig, ig] = 0 + + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext, space_k, k) + Proj_edge[pg, ig] = gamma[d][p] + + return Proj_edge diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py index 34d4d8c62..87368a1eb 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -2,6 +2,7 @@ # Conga operators on piecewise (broken) de Rham sequences +from sympy import Tuple from mpi4py import MPI import os import numpy as np @@ -10,30 +11,31 @@ from scipy.sparse import kron, block_diag from scipy.sparse.linalg import inv -from sympde.topology import Boundary, Interface, Union -from sympde.topology import element_of, elements_of -from sympde.topology.space import ScalarFunction -from sympde.calculus import grad, dot, inner, rot, div -from sympde.calculus import laplace, bracket, convect -from sympde.calculus import jump, avg, Dn, minus, plus +from sympde.topology import Boundary, Interface, Union +from sympde.topology import element_of, elements_of +from sympde.topology.space import ScalarFunction +from sympde.calculus import grad, dot, inner, rot, div +from sympde.calculus import laplace, bracket, convect +from sympde.calculus import jump, avg, Dn, minus, plus from sympde.expr.expr import LinearForm, BilinearForm from sympde.expr.expr import integral -from psydac.core.bsplines import collocation_matrix, histopolation_matrix +from psydac.core.bsplines import collocation_matrix, histopolation_matrix -from psydac.api.discretization import discretize -from psydac.api.essential_bc import apply_essential_bc_stencil -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator -from psydac.linalg.stencil import StencilVector, StencilMatrix, StencilInterfaceMatrix -from psydac.linalg.solvers import inverse -from psydac.fem.basic import FemField +from psydac.api.discretization import discretize +from psydac.api.essential_bc import apply_essential_bc_stencil +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator +from psydac.linalg.stencil import StencilVector, StencilMatrix, StencilInterfaceMatrix +from psydac.linalg.solvers import inverse +from psydac.fem.basic import FemField -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 -from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D +from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 +from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator + def get_patch_index_from_face(domain, face): """ Return the patch index of subdomain/boundary @@ -58,13 +60,15 @@ def get_patch_index_from_face(domain, face): domains = domain.interior.args if isinstance(face, Interface): - raise NotImplementedError("This face is an interface, it has several indices -- I am a machine, I cannot choose. Help.") + raise NotImplementedError( + "This face is an interface, it has several indices -- I am a machine, I cannot choose. Help.") elif isinstance(face, Boundary): i = domains.index(face.domain) else: i = domains.index(face) return i + def get_interface_from_corners(corner1, corner2, domain): """ Return the interface between two corners from two different patches that correspond to a single (physical) vertex. @@ -103,14 +107,16 @@ def get_interface_from_corners(corner1, corner2, domain): new_interface = [] for i in interface: - if i.minus in bd1+bd2: - if i.plus in bd2+bd1: + if i.minus in bd1 + bd2: + if i.plus in bd2 + bd1: new_interface.append(i) if len(new_interface) == 1: return new_interface[0] - if len(new_interface)>1: - raise ValueError('found more than one interface for the corners {} and {}'.format(corner1, corner2)) + if len(new_interface) > 1: + raise ValueError( + 'found more than one interface for the corners {} and {}'.format( + corner1, corner2)) return None @@ -144,51 +150,57 @@ def get_row_col_index(corner1, corner2, interface, axis, V1, V2): The StencilInterfaceMatrix index of the corner, it has the form (i1, i2, k1, k2) in 2D, where (i1, i2) identifies the row and (k1, k2) the diagonal. """ - start = V1.vector_space.starts - end = V1.vector_space.ends - degree = V2.degree + start = V1.vector_space.starts + end = V1.vector_space.ends + degree = V2.degree start_end = (start, end) - row = [None]*len(start) - col = [0]*len(start) + row = [None] * len(start) + col = [0] * len(start) assert corner1.boundaries[0].axis == corner2.boundaries[0].axis for bd in corner1.boundaries: - row[bd.axis] = start_end[(bd.ext+1)//2][bd.axis] + row[bd.axis] = start_end[(bd.ext + 1) // 2][bd.axis] if interface is None and corner1.domain != corner2.domain: - bd = [i for i in corner1.boundaries if i.axis==axis][0] - if bd.ext == 1:row[bd.axis] = degree[bd.axis] + bd = [i for i in corner1.boundaries if i.axis == axis][0] + if bd.ext == 1: + row[bd.axis] = degree[bd.axis] if interface is None: - return row+col + return row + col axis = interface.axis if interface.minus.domain == corner1.domain: - if interface.minus.ext == -1:row[axis] = 0 - else:row[axis] = degree[axis] + if interface.minus.ext == -1: + row[axis] = 0 + else: + row[axis] = degree[axis] else: - if interface.plus.ext == -1:row[axis] = 0 - else:row[axis] = degree[axis] + if interface.plus.ext == -1: + row[axis] = 0 + else: + row[axis] = degree[axis] if interface.minus.ext == interface.plus.ext: pass elif interface.minus.domain == corner1.domain: if interface.minus.ext == -1: - col[axis] = degree[axis] + col[axis] = degree[axis] else: - col[axis] = -degree[axis] + col[axis] = -degree[axis] else: if interface.plus.ext == -1: - col[axis] = degree[axis] + col[axis] = degree[axis] else: - col[axis] = -degree[axis] + col[axis] = -degree[axis] + + return row + col - return row+col -#=============================================================================== +# =============================================================================== def allocate_interface_matrix(corners, test_space, trial_space): """ Allocate the interface matrix for a vertex shared by two patches @@ -213,37 +225,52 @@ def allocate_interface_matrix(corners, test_space, trial_space): flips = [] k = 0 - while k primal basis - self._matrix: matrix of the primal Hodge = this is the INVERSE mass matrix ! - self.dual_Hodge_matrix: this is the mass matrix + self._matrix: matrix of the primal Hodge = this is the mass matrix ! + self.dual_Hodge_matrix: this is the INVERSE mass matrix Parameters ---------- @@ -850,6 +934,9 @@ class HodgeOperator( FemLinearOperator ): domain_h: The discrete domain of the projector + metric : + the metric of the de Rham complex + backend_language: The backend used to accelerate the code @@ -863,69 +950,71 @@ class HodgeOperator( FemLinearOperator ): ----- Either we use a storage, or these matrices are only computed on demand # todo: we compute the sparse matrix when to_sparse_matrix is called -- but never the stencil matrix (should be fixed...) - + We only support the identity metric, this implies that the dual Hodge is the inverse of the primal one. + # todo: allow for non-identity metrics """ - def __init__( self, Vh, domain_h, backend_language='python', load_dir=None, load_space_index=''): + def __init__( + self, + Vh, + domain_h, + metric='identity', + backend_language='python', + load_dir=None, + load_space_index=''): FemLinearOperator.__init__(self, fem_domain=Vh) self._domain_h = domain_h self._backend_language = backend_language - self._dual_Hodge_matrix = None self._dual_Hodge_sparse_matrix = None + assert metric == 'identity' + self._metric = metric + if load_dir and isinstance(load_dir, str): if not os.path.exists(load_dir): os.makedirs(load_dir) assert str(load_space_index) in ['0', '1', '2', '3'] - primal_Hodge_storage_fn = load_dir+'/H{}_m.npz'.format(load_space_index) - dual_Hodge_storage_fn = load_dir+'/dH{}_m.npz'.format(load_space_index) + primal_Hodge_storage_fn = load_dir + \ + '/H{}_m.npz'.format(load_space_index) + dual_Hodge_storage_fn = load_dir + \ + '/dH{}_m.npz'.format(load_space_index) primal_Hodge_is_stored = os.path.exists(primal_Hodge_storage_fn) dual_Hodge_is_stored = os.path.exists(dual_Hodge_storage_fn) - if primal_Hodge_is_stored: - assert dual_Hodge_is_stored - print(" ... loading dual Hodge sparse matrix from "+dual_Hodge_storage_fn) - self._dual_Hodge_sparse_matrix = load_npz(dual_Hodge_storage_fn) - print("[HodgeOperator] loading primal Hodge sparse matrix from "+primal_Hodge_storage_fn) + if dual_Hodge_is_stored: + assert primal_Hodge_is_stored + print( + " ... loading dual Hodge sparse matrix from " + + dual_Hodge_storage_fn) + self._dual_Hodge_sparse_matrix = load_npz( + dual_Hodge_storage_fn) + print( + "[HodgeOperator] loading primal Hodge sparse matrix from " + + primal_Hodge_storage_fn) self._sparse_matrix = load_npz(primal_Hodge_storage_fn) else: - assert not dual_Hodge_is_stored - print("[HodgeOperator] assembling both sparse matrices for storage...") - self.assemble_dual_Hodge_matrix() - print("[HodgeOperator] storing primal Hodge sparse matrix in "+dual_Hodge_storage_fn) - save_npz(dual_Hodge_storage_fn, self._dual_Hodge_sparse_matrix) + assert not primal_Hodge_is_stored + print( + "[HodgeOperator] assembling both sparse matrices for storage...") self.assemble_primal_Hodge_matrix() - print("[HodgeOperator] storing primal Hodge sparse matrix in "+primal_Hodge_storage_fn) + print( + "[HodgeOperator] storing primal Hodge sparse matrix in " + + primal_Hodge_storage_fn) save_npz(primal_Hodge_storage_fn, self._sparse_matrix) + self.assemble_dual_Hodge_matrix() + print( + "[HodgeOperator] storing dual Hodge sparse matrix in " + + dual_Hodge_storage_fn) + save_npz(dual_Hodge_storage_fn, self._dual_Hodge_sparse_matrix) else: # matrices are not stored, we will probably compute them later pass - def assemble_primal_Hodge_matrix(self): - - if self._sparse_matrix is None: - if not self._dual_Hodge_matrix: - self.assemble_dual_Hodge_matrix() - - M = self._dual_Hodge_matrix # mass matrix of the (primal) basis - nrows = M.n_block_rows - ncols = M.n_block_cols - - inv_M_blocks = [] - for i in range(nrows): - Mii = M[i,i].tosparse() - inv_Mii = inv(Mii.tocsc()) - inv_Mii.eliminate_zeros() - inv_M_blocks.append(inv_Mii) - - inv_M = block_diag(inv_M_blocks) - self._sparse_matrix = inv_M - - def to_sparse_matrix( self ): + def to_sparse_matrix(self): """ - the Hodge matrix is the patch-wise inverse of the multi-patch mass matrix - it is not stored by default but computed on demand, by local (patch-wise) inversion of the mass matrix + the Hodge matrix is the patch-wise multi-patch mass matrix + it is not stored by default but assembled on demand """ if (self._sparse_matrix is not None) or (self._matrix is not None): @@ -935,12 +1024,13 @@ def to_sparse_matrix( self ): return self._sparse_matrix - def assemble_dual_Hodge_matrix( self ): + def assemble_primal_Hodge_matrix(self): """ - the dual Hodge matrix is the patch-wise multi-patch mass matrix + the Hodge matrix is the patch-wise multi-patch mass matrix it is not stored by default but assembled on demand """ - if self._dual_Hodge_matrix is None: + + if self._matrix is None: Vh = self.fem_domain assert Vh == self.fem_codomain @@ -948,25 +1038,52 @@ def assemble_dual_Hodge_matrix( self ): domain = V.domain # domain_h = V0h.domain: would be nice... u, v = elements_of(V, names='u, v') - # print(type(u)) - # exit() + if isinstance(u, ScalarFunction): - expr = u*v + expr = u * v else: - expr = dot(u,v) - a = BilinearForm((u,v), integral(domain, expr)) - ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) + expr = dot(u, v) + + a = BilinearForm((u, v), integral(domain, expr)) + ah = discretize(a, self._domain_h, [ + Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) - self._dual_Hodge_matrix = ah.assemble() # Mass matrix in stencil format - self._dual_Hodge_sparse_matrix = self._dual_Hodge_matrix.tosparse() + self._matrix = ah.assemble() # Mass matrix in stencil format + self._sparse_matrix = self._matrix.tosparse() - def get_dual_Hodge_sparse_matrix( self ): + def get_dual_Hodge_sparse_matrix(self): if self._dual_Hodge_sparse_matrix is None: self.assemble_dual_Hodge_matrix() return self._dual_Hodge_sparse_matrix -#============================================================================== + def assemble_dual_Hodge_matrix(self): + """ + the dual Hodge matrix is the patch-wise inverse of the multi-patch mass matrix + it is not stored by default but computed on demand, by local (patch-wise) inversion of the mass matrix + """ + + if self._dual_Hodge_sparse_matrix is None: + if not self._matrix: + self.assemble_primal_Hodge_matrix() + + M = self._matrix # mass matrix of the (primal) basis + nrows = M.n_block_rows + ncols = M.n_block_cols + + inv_M_blocks = [] + for i in range(nrows): + Mii = M[i, i].tosparse() + inv_Mii = inv(Mii.tocsc()) + inv_Mii.eliminate_zeros() + inv_M_blocks.append(inv_Mii) + + inv_M = block_diag(inv_M_blocks) + self._dual_Hodge_sparse_matrix = inv_M + +# ============================================================================== + + class BrokenGradient_2D(FemLinearOperator): def __init__(self, V0h, V1h): @@ -975,31 +1092,33 @@ def __init__(self, V0h, V1h): D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] - self._matrix = BlockLinearOperator(self.domain, self.codomain, \ - blocks={(i, i): D0i._matrix for i, D0i in enumerate(D0s)}) + self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ + (i, i): D0i._matrix for i, D0i in enumerate(D0s)}) def transpose(self, conjugate=False): # todo (MCP): define as the dual differential operator return BrokenTransposedGradient_2D(self.fem_domain, self.fem_codomain) -#============================================================================== -class BrokenTransposedGradient_2D( FemLinearOperator ): +# ============================================================================== + + +class BrokenTransposedGradient_2D(FemLinearOperator): - def __init__( self, V0h, V1h): + def __init__(self, V0h, V1h): FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V0h) D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] - self._matrix = BlockLinearOperator(self.domain, self.codomain, \ - blocks={(i, i): D0i._matrix.T for i, D0i in enumerate(D0s)}) + self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ + (i, i): D0i._matrix.T for i, D0i in enumerate(D0s)}) def transpose(self, conjugate=False): # todo (MCP): discard return BrokenGradient_2D(self.fem_codomain, self.fem_domain) -#============================================================================== +# ============================================================================== class BrokenScalarCurl_2D(FemLinearOperator): def __init__(self, V1h, V2h): @@ -1007,34 +1126,34 @@ def __init__(self, V1h, V2h): D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] - self._matrix = BlockLinearOperator(self.domain, self.codomain, \ - blocks={(i, i): D1i._matrix for i, D1i in enumerate(D1s)}) + self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ + (i, i): D1i._matrix for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): - return BrokenTransposedScalarCurl_2D(V1h=self.fem_domain, V2h=self.fem_codomain) + return BrokenTransposedScalarCurl_2D( + V1h=self.fem_domain, V2h=self.fem_codomain) -#============================================================================== -class BrokenTransposedScalarCurl_2D( FemLinearOperator ): +# ============================================================================== +class BrokenTransposedScalarCurl_2D(FemLinearOperator): - def __init__( self, V1h, V2h): + def __init__(self, V1h, V2h): FemLinearOperator.__init__(self, fem_domain=V2h, fem_codomain=V1h) D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] - self._matrix = BlockLinearOperator(self.domain, self.codomain, \ - blocks={(i, i): D1i._matrix.T for i, D1i in enumerate(D1s)}) + self._matrix = BlockLinearOperator(self.domain, self.codomain, blocks={ + (i, i): D1i._matrix.T for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) - -#============================================================================== -from sympy import Tuple +# ============================================================================== # def multipatch_Moments_Hcurl(f, V1h, domain_h): + def ortho_proj_Hcurl(EE, V1h, domain_h, M1, backend_language='python'): """ return orthogonal projection of E on V1h, given M1 the mass matrix @@ -1042,23 +1161,30 @@ def ortho_proj_Hcurl(EE, V1h, domain_h, M1, backend_language='python'): assert isinstance(EE, Tuple) V1 = V1h.symbolic_space v = element_of(V1, name='v') - l = LinearForm(v, integral(V1.domain, dot(v,EE))) - lh = discretize(l, domain_h, V1h, backend=PSYDAC_BACKENDS[backend_language]) + l = LinearForm(v, integral(V1.domain, dot(v, EE))) + lh = discretize( + l, + domain_h, + V1h, + backend=PSYDAC_BACKENDS[backend_language]) b = lh.assemble() M1_inv = inverse(M1.mat(), 'pcg', pc='jacobi', tol=1e-10) sol_coeffs = M1_inv @ b return FemField(V1h, coeffs=sol_coeffs) -#============================================================================== +# ============================================================================== + + class Multipatch_Projector_H1: """ to apply the H1 projection (2D) on every patch """ + def __init__(self, V0h): self._P0s = [Projector_H1(V) for V in V0h.spaces] - self._V0h = V0h # multipatch Fem Space + self._V0h = V0h # multipatch Fem Space def __call__(self, funs_log): """ @@ -1066,21 +1192,24 @@ def __call__(self, funs_log): """ u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] - u0_coeffs = BlockVector(self._V0h.vector_space, \ - blocks = [u0j.coeffs for u0j in u0s]) + u0_coeffs = BlockVector(self._V0h.vector_space, + blocks=[u0j.coeffs for u0j in u0s]) + + return FemField(self._V0h, coeffs=u0_coeffs) + +# ============================================================================== - return FemField(self._V0h, coeffs = u0_coeffs) -#============================================================================== class Multipatch_Projector_Hcurl: """ to apply the Hcurl projection (2D) on every patch """ + def __init__(self, V1h, nquads=None): self._P1s = [Projector_Hcurl(V, nquads=nquads) for V in V1h.spaces] - self._V1h = V1h # multipatch Fem Space + self._V1h = V1h # multipatch Fem Space def __call__(self, funs_log): """ @@ -1088,21 +1217,24 @@ def __call__(self, funs_log): """ E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] - E1_coeffs = BlockVector(self._V1h.vector_space, \ - blocks = [E1j.coeffs for E1j in E1s]) + E1_coeffs = BlockVector(self._V1h.vector_space, + blocks=[E1j.coeffs for E1j in E1s]) + + return FemField(self._V1h, coeffs=E1_coeffs) + +# ============================================================================== - return FemField(self._V1h, coeffs = E1_coeffs) -#============================================================================== class Multipatch_Projector_L2: """ to apply the L2 projection (2D) on every patch """ + def __init__(self, V2h, nquads=None): self._P2s = [Projector_L2(V, nquads=nquads) for V in V2h.spaces] - self._V2h = V2h # multipatch Fem Space + self._V2h = V2h # multipatch Fem Space def __call__(self, funs_log): """ @@ -1110,8 +1242,7 @@ def __call__(self, funs_log): """ B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] - B2_coeffs = BlockVector(self._V2h.vector_space, \ - blocks = [B2j.coeffs for B2j in B2s]) - - return FemField(self._V2h, coeffs = B2_coeffs) + B2_coeffs = BlockVector(self._V2h.vector_space, + blocks=[B2j.coeffs for B2j in B2s]) + return FemField(self._V2h, coeffs=B2_coeffs) diff --git a/psydac/feec/multipatch/plotting_utilities.py b/psydac/feec/multipatch/plotting_utilities.py index 0fedc2354..af522c6f1 100644 --- a/psydac/feec/multipatch/plotting_utilities.py +++ b/psydac/feec/multipatch/plotting_utilities.py @@ -1,30 +1,42 @@ # coding: utf-8 from mpi4py import MPI -from sympy import lambdify +from sympy import lambdify import numpy as np +import matplotlib import matplotlib.pyplot as plt -from matplotlib import cm, colors -from mpl_toolkits import mplot3d +from matplotlib import cm, colors from collections import OrderedDict from psydac.linalg.utilities import array_to_psydac -from psydac.fem.basic import FemField -from psydac.fem.vector import ProductFemSpace, VectorFemSpace -from psydac.utilities.utils import refine_array_1d -from psydac.feec.pull_push import push_2d_h1, push_2d_hcurl, push_2d_hdiv, push_2d_l2 +from psydac.fem.basic import FemField +from psydac.utilities.utils import refine_array_1d +from psydac.feec.pull_push import push_2d_h1, push_2d_hcurl, push_2d_hdiv, push_2d_l2 + +__all__ = ( + 'is_vector_valued', + 'get_grid_vals', + 'get_grid_quad_weights', + 'get_plotting_grid', + 'get_diag_grid', + 'get_patch_knots_gridlines', + 'plot_field', + 'my_small_plot', + 'my_small_streamplot') + +# ============================================================================== -__all__ = ('is_vector_valued', 'get_grid_vals', 'get_grid_quad_weights', 'get_plotting_grid', - 'get_diag_grid', 'get_patch_knots_gridlines', 'plot_field', 'my_small_plot', 'my_small_streamplot') -#============================================================================== def is_vector_valued(u): # small utility function, only tested for FemFields in multi-patch spaces of the 2D grad-curl sequence - # todo: a proper interface returning the number of components of a general FemField would be nice + # todo: a proper interface returning the number of components of a general + # FemField would be nice return u.fields[0].space.is_product -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + + def get_grid_vals(u, etas, mappings_list, space_kind='hcurl'): """ get the physical field values, given the logical field and the logical grid @@ -33,24 +45,26 @@ def get_grid_vals(u, etas, mappings_list, space_kind='hcurl'): :param space_kind: specifies the push-forward for the physical values """ n_patches = len(mappings_list) - vector_valued = is_vector_valued(u) if isinstance(u, FemField) else isinstance(u[0],(list, tuple)) + vector_valued = is_vector_valued(u) if isinstance( + u, FemField) else isinstance(u[0], (list, tuple)) if vector_valued: # WARNING: here we assume 2D ! - u_vals_components = [n_patches*[None], n_patches*[None]] + u_vals_components = [n_patches * [None], n_patches * [None]] else: - u_vals_components = [n_patches*[None]] + u_vals_components = [n_patches * [None]] for k in range(n_patches): eta_1, eta_2 = np.meshgrid(etas[k][0], etas[k][1], indexing='ij') for vals in u_vals_components: vals[k] = np.empty_like(eta_1) uk_field_1 = None - if isinstance(u,FemField): + if isinstance(u, FemField): if vector_valued: uk_field_0 = u[k].fields[0] uk_field_1 = u[k].fields[1] else: - uk_field_0 = u.fields[k] # it would be nice to just write u[k].fields[0] here... + # it would be nice to just write u[k].fields[0] here... + uk_field_0 = u.fields[k] else: # then u should be callable if vector_valued: @@ -60,124 +74,176 @@ def get_grid_vals(u, etas, mappings_list, space_kind='hcurl'): uk_field_0 = u[k] # computing the pushed-fwd values on the grid - if space_kind == 'h1': + if space_kind == 'h1' or space_kind == 'V0': assert not vector_valued # todo (MCP): add 2d_hcurl_vector - push_field = lambda eta1, eta2: push_2d_h1(uk_field_0, eta1, eta2) - elif space_kind == 'hcurl': + + def push_field( + eta1, eta2): return push_2d_h1( + uk_field_0, eta1, eta2) + elif space_kind == 'hcurl' or space_kind == 'V1': # todo (MCP): specify 2d_hcurl_scalar in push functions - push_field = lambda eta1, eta2: push_2d_hcurl(uk_field_0, uk_field_1, eta1, eta2, mappings_list[k]) - elif space_kind == 'hdiv': - push_field = lambda eta1, eta2: push_2d_hdiv(uk_field_0, uk_field_1, eta1, eta2, mappings_list[k]) + def push_field( + eta1, + eta2): return push_2d_hcurl( + uk_field_0, + uk_field_1, + eta1, + eta2, + mappings_list[k].get_callable_mapping()) + elif space_kind == 'hdiv' or space_kind == 'V2': + def push_field( + eta1, + eta2): return push_2d_hdiv( + uk_field_0, + uk_field_1, + eta1, + eta2, + mappings_list[k].get_callable_mapping()) elif space_kind == 'l2': assert not vector_valued - push_field = lambda eta1, eta2: push_2d_l2(uk_field_0, eta1, eta2, mappings_list[k]) + + def push_field( + eta1, + eta2): return push_2d_l2( + uk_field_0, + eta1, + eta2, + mappings_list[k].get_callable_mapping()) else: - raise ValueError('unknown value for space_kind = {}'.format(space_kind)) + raise ValueError( + 'unknown value for space_kind = {}'.format(space_kind)) for i, x1i in enumerate(eta_1[:, 0]): for j, x2j in enumerate(eta_2[0, :]): if vector_valued: - u_vals_components[0][k][i, j], u_vals_components[1][k][i, j] = push_field(x1i, x2j) + u_vals_components[0][k][i, j], u_vals_components[1][k][i, j] = push_field( + x1i, x2j) else: u_vals_components[0][k][i, j] = push_field(x1i, x2j) # always return a list, even for scalar-valued functions ? if not vector_valued: - return np.array(u_vals_components[0]) + return u_vals_components[0] else: - return [np.array(a) for a in u_vals_components] + return u_vals_components + +# ------------------------------------------------------------------------------ + -#------------------------------------------------------------------------------ -def get_grid_quad_weights(etas, patch_logvols, mappings_list): #_obj): +def get_grid_quad_weights(etas, patch_logvols, mappings_list): # _obj): # get approximate weights for a physical quadrature, namely # |J_F(xi1, xi2)| * log_weight with uniform log_weight = h1*h2 for (xi1, xi2) in the logical grid, - # in the same format as the fields value in get_grid_vals_scalar and get_grid_vals_vector + # in the same format as the fields value in get_grid_vals_scalar and + # get_grid_vals_vector n_patches = len(mappings_list) - quad_weights = n_patches*[None] + quad_weights = n_patches * [None] for k in range(n_patches): eta_1, eta_2 = np.meshgrid(etas[k][0], etas[k][1], indexing='ij') quad_weights[k] = np.empty_like(eta_1) - one_field = lambda xi1, xi2: 1 + def one_field(xi1, xi2): return 1 N0 = eta_1.shape[0] N1 = eta_1.shape[1] - log_weight = patch_logvols[k]/(N0*N1) + log_weight = patch_logvols[k] / (N0 * N1) + Fk = mappings_list[k].get_callable_mapping() for i, x1i in enumerate(eta_1[:, 0]): for j, x2j in enumerate(eta_2[0, :]): - quad_weights[k][i, j] = push_2d_l2(one_field, x1i, x2j, mapping=mappings_list[k]) * log_weight + det_Fk_ij = Fk.metric_det(x1i, x2j)**0.5 + quad_weights[k][i, j] = det_Fk_ij * log_weight return quad_weights -#------------------------------------------------------------------------------ -def get_plotting_grid(mappings, N, centered_nodes=False, return_patch_logvols=False): +# ------------------------------------------------------------------------------ + + +def get_plotting_grid( + mappings, + N, + centered_nodes=False, + return_patch_logvols=False): # if centered_nodes == False, returns a regular grid with (N+1)x(N+1) nodes, starting and ending at patch boundaries # (useful for plotting the full patches) # if centered_nodes == True, returns the grid consisting of the NxN centers of the latter # (useful for quadratures and to avoid evaluating at patch boundaries) - # if return_patch_logvols == True, return the logival volume (area) of the patches + # if return_patch_logvols == True, return the logival volume (area) of the + # patches nb_patches = len(mappings) grid_min_coords = [np.array(D.min_coords) for D in mappings] grid_max_coords = [np.array(D.max_coords) for D in mappings] if return_patch_logvols: - patch_logvols = [(D.max_coords[1]-D.min_coords[1])*(D.max_coords[0]-D.min_coords[0]) for D in mappings] + patch_logvols = [(D.max_coords[1] - D.min_coords[1]) * + (D.max_coords[0] - D.min_coords[0]) for D in mappings] else: patch_logvols = None if centered_nodes: for k in range(nb_patches): for dim in range(2): - h_grid = (grid_max_coords[k][dim] - grid_min_coords[k][dim])/N - grid_max_coords[k][dim] -= h_grid/2 - grid_min_coords[k][dim] += h_grid/2 - N_cells = N-1 + h_grid = (grid_max_coords[k][dim] - + grid_min_coords[k][dim]) / N + grid_max_coords[k][dim] -= h_grid / 2 + grid_min_coords[k][dim] += h_grid / 2 + N_cells = N - 1 else: N_cells = N # etas = [[refine_array_1d( bounds, N ) for bounds in zip(D.min_coords, D.max_coords)] for D in mappings] - etas = [[refine_array_1d( bounds, N_cells ) for bounds in zip(grid_min_coords[k], grid_max_coords[k])] for k in range(nb_patches)] - mappings_lambda = [lambdify(M.logical_coordinates, M.expressions) for d,M in mappings.items()] + etas = [[refine_array_1d(bounds, N_cells) for bounds in zip( + grid_min_coords[k], grid_max_coords[k])] for k in range(nb_patches)] + mappings_lambda = [ + lambdify( + M.logical_coordinates, + M.expressions) for d, + M in mappings.items()] - pcoords = [np.array( [[f(e1,e2) for e2 in eta[1]] for e1 in eta[0]] ) for f,eta in zip(mappings_lambda, etas)] + pcoords = [np.array([[f(e1, e2) for e2 in eta[1]] for e1 in eta[0]]) + for f, eta in zip(mappings_lambda, etas)] - xx = [pcoords[k][:,:,0] for k in range(nb_patches)] - yy = [pcoords[k][:,:,1] for k in range(nb_patches)] + xx = [pcoords[k][:, :, 0] for k in range(nb_patches)] + yy = [pcoords[k][:, :, 1] for k in range(nb_patches)] if return_patch_logvols: return etas, xx, yy, patch_logvols else: return etas, xx, yy -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + + def get_diag_grid(mappings, N): nb_patches = len(mappings) - etas = [[refine_array_1d( bounds, N ) for bounds in zip(D.min_coords, D.max_coords)] for D in mappings] - mappings_lambda = [lambdify(M.logical_coordinates, M.expressions) for d,M in mappings.items()] + etas = [[refine_array_1d(bounds, N) for bounds in zip( + D.min_coords, D.max_coords)] for D in mappings] + mappings_lambda = [ + lambdify( + M.logical_coordinates, + M.expressions) for d, + M in mappings.items()] - pcoords = [np.array( [[f(e1,e2) for e2 in eta[1]] for e1 in eta[0]] ) for f,eta in zip(mappings_lambda, etas)] + pcoords = [np.array([[f(e1, e2) for e2 in eta[1]] for e1 in eta[0]]) + for f, eta in zip(mappings_lambda, etas)] # pcoords = np.concatenate(pcoords, axis=1) # xx = pcoords[:,:,0] # yy = pcoords[:,:,1] - xx = [pcoords[k][:,:,0] for k in range(nb_patches)] - yy = [pcoords[k][:,:,1] for k in range(nb_patches)] + xx = [pcoords[k][:, :, 0] for k in range(nb_patches)] + yy = [pcoords[k][:, :, 1] for k in range(nb_patches)] return etas, xx, yy -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + + def get_patch_knots_gridlines(Vh, N, mappings, plotted_patch=-1): # get gridlines for one patch grid - F = [M.get_callable_mapping() for d,M in mappings.items()] + F = [M.get_callable_mapping() for d, M in mappings.items()] if plotted_patch in range(len(mappings)): - space = Vh.spaces[plotted_patch] - if isinstance(space, (VectorFemSpace, ProductFemSpace)): - space = space.spaces[0] - - grid_x1 = space.breaks[0] - grid_x2 = space.breaks[1] + grid_x1 = Vh.spaces[plotted_patch].spaces[0].breaks[0] + grid_x2 = Vh.spaces[plotted_patch].spaces[0].breaks[1] x1 = refine_array_1d(grid_x1, N) x2 = refine_array_1d(grid_x2, N) @@ -185,7 +251,7 @@ def get_patch_knots_gridlines(Vh, N, mappings, plotted_patch=-1): x1, x2 = np.meshgrid(x1, x2, indexing='ij') x, y = F[plotted_patch](x1, x2) - gridlines_x1 = (x[:, ::N], y[:, ::N] ) + gridlines_x1 = (x[:, ::N], y[:, ::N]) gridlines_x2 = (x[::N, :].T, y[::N, :].T) # gridlines = (gridlines_x1, gridlines_x2) else: @@ -194,17 +260,48 @@ def get_patch_knots_gridlines(Vh, N, mappings, plotted_patch=-1): return gridlines_x1, gridlines_x2 -#------------------------------------------------------------------------------ -def plot_field(fem_field=None, stencil_coeffs=None, numpy_coeffs=None, Vh=None, domain=None, space_kind=None, title=None, filename='dummy_plot.png', subtitles=None, hide_plot=True): +# ------------------------------------------------------------------------------ + + +def plot_field( + fem_field=None, + stencil_coeffs=None, + numpy_coeffs=None, + Vh=None, + domain=None, + surface_plot=False, + cb_min=None, + cb_max=None, + plot_type='amplitude', + cmap='hsv', + space_kind=None, + title=None, + filename='dummy_plot.png', + subtitles=None, + N_vis=20, + vf_skip=2, + hide_plot=True): """ plot a discrete field (given as a FemField or by its coeffs in numpy or stencil format) on the given domain - :param Vh: Fem space needed if v is given by its coeffs - :param space_kind: type of the push-forward defining the physical Fem Space - :param subtitles: in case one would like to have several subplots # todo: then v should be given as a list of fields... + Parameters + ---------- + numpy_coeffs : (np.ndarray) + Coefficients of the field to plot + + Vh : TensorFemSpace + Fem space needed if v is given by its coeffs + + space_kind : (str) + type of the push-forward defining the physical Fem Space + + N_vis : (int) + nb of visualization points per patch (per dimension) """ - if not space_kind in ['h1', 'hcurl', 'l2']: - raise ValueError('invalid value for space_kind = {}'.format(space_kind)) + + if space_kind not in ['h1', 'hcurl', 'l2']: + raise ValueError( + 'invalid value for space_kind = {}'.format(space_kind)) vh = fem_field if vh is None: @@ -213,34 +310,104 @@ def plot_field(fem_field=None, stencil_coeffs=None, numpy_coeffs=None, Vh=None, stencil_coeffs = array_to_psydac(numpy_coeffs, Vh.vector_space) vh = FemField(Vh, coeffs=stencil_coeffs) - mappings = OrderedDict([(P.logical_domain, P.mapping) for P in domain.interior]) + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) mappings_list = list(mappings.values()) - etas, xx, yy = get_plotting_grid(mappings, N=20) - grid_vals = lambda v: get_grid_vals(v, etas, mappings_list, space_kind=space_kind) + etas, xx, yy = get_plotting_grid(mappings, N=N_vis) + + def grid_vals(v): return get_grid_vals( + v, etas, mappings_list, space_kind=space_kind) vh_vals = grid_vals(vh) - if is_vector_valued(vh): - # then vh_vals[d] contains the values of the d-component of vh (as a patch-indexed list) - vh_abs_vals = [np.sqrt(abs(v[0])**2 + abs(v[1])**2) for v in zip(vh_vals[0],vh_vals[1])] + if plot_type == 'vector_field' and not is_vector_valued(vh): + print( + "WARNING [plot_field]: vector_field plot is not possible with a scalar field, plotting the amplitude instead") + plot_type = 'amplitude' + + if plot_type == 'vector_field': + if is_vector_valued(vh): + my_small_streamplot( + title=title, + vals_x=vh_vals[0], + vals_y=vh_vals[1], + skip=vf_skip, + xx=xx, + yy=yy, + amp_factor=2, + save_fig=filename, + hide_plot=hide_plot, + dpi=200, + ) + else: - # then vh_vals just contains the values of vh (as a patch-indexed list) - vh_abs_vals = np.abs(vh_vals) - - my_small_plot( - title=title, - vals=[vh_abs_vals], - titles=subtitles, - xx=xx, - yy=yy, - surface_plot=False, - save_fig=filename, - save_vals = True, - hide_plot=hide_plot, - cmap='hsv', - dpi = 400, - ) + # computing plot_vals_list: may have several elements for several plots + if plot_type == 'amplitude': + + if is_vector_valued(vh): + # then vh_vals[d] contains the values of the d-component of vh + # (as a patch-indexed list) + plot_vals = [np.sqrt(abs(v[0])**2 + abs(v[1])**2) + for v in zip(vh_vals[0], vh_vals[1])] + else: + # then vh_vals just contains the values of vh (as a + # patch-indexed list) + plot_vals = np.abs(vh_vals) + plot_vals_list = [plot_vals] + + elif plot_type == 'components': + if is_vector_valued(vh): + # then vh_vals[d] contains the values of the d-component of vh + # (as a patch-indexed list) + plot_vals_list = vh_vals + if subtitles is None: + subtitles = ['x-component', 'y-component'] + else: + # then vh_vals just contains the values of vh (as a + # patch-indexed list) + plot_vals_list = [vh_vals] + else: + raise ValueError(plot_type) + + my_small_plot( + title=title, + vals=plot_vals_list, + titles=subtitles, + xx=xx, + yy=yy, + surface_plot=surface_plot, + cb_min=cb_min, + cb_max=cb_max, + save_fig=filename, + save_vals=False, + hide_plot=hide_plot, + cmap=cmap, + dpi=300, + ) + + # if is_vector_valued(vh): + # # then vh_vals[d] contains the values of the d-component of vh (as a patch-indexed list) + # vh_abs_vals = [np.sqrt(abs(v[0])**2 + abs(v[1])**2) for v in zip(vh_vals[0],vh_vals[1])] + # else: + # # then vh_vals just contains the values of vh (as a patch-indexed list) + # vh_abs_vals = np.abs(vh_vals) + + # my_small_plot( + # title=title, + # vals=[vh_abs_vals], + # titles=subtitles, + # xx=xx, + # yy=yy, + # surface_plot=False, + # save_fig=filename, + # save_vals=False, + # hide_plot=hide_plot, + # cmap='hsv', + # dpi = 400, + # ) + +# ------------------------------------------------------------------------------ + -#------------------------------------------------------------------------------ def my_small_plot( title, vals, titles=None, xx=None, yy=None, @@ -248,102 +415,164 @@ def my_small_plot( gridlines_x2=None, surface_plot=False, cmap='viridis', + cb_min=None, + cb_max=None, save_fig=None, - save_vals = False, + save_vals=False, hide_plot=False, dpi='figure', show_xylabel=True, ): + """ + plot a list of scalar fields on a list of patches + + Parameters + ---------- + title : (str) + title of the plot + + vals : (list) + list of scalar fields to plot + + titles : (list) + list of titles for each plot + + xx : (list) + list of x-coordinates of the grid points + + yy : (list) + list of y-coordinates of the grid points + """ # titles is discarded if only one plot # cmap = 'jet' is nice too, but not so uniform. 'plasma' or 'magma' are uniform also. # cmap = 'hsv' is good for singular fields, for its rapid color change assert xx and yy n_plots = len(vals) if n_plots > 1: - assert n_plots == len(titles) + if titles is None or n_plots != len(titles): + titles = n_plots * [title] else: if titles: - print('Warning [my_small_plot]: will discard argument titles for a single plot') + print( + 'Warning [my_small_plot]: will discard argument titles for a single plot') + titles = [title] n_patches = len(xx) assert n_patches == len(yy) if save_vals: + # saving as vals.npz np.savez('vals', xx=xx, yy=yy, vals=vals) - - fig = plt.figure(figsize=(2.6+4.8*n_plots, 4.8)) + + fig = plt.figure(figsize=(2.6 + 4.8 * n_plots, 4.8)) fig.suptitle(title, fontsize=14) for i in range(n_plots): - vmin = np.min(vals[i]) - vmax = np.max(vals[i]) + if cb_min is None: + vmin = np.min(vals[i]) + else: + vmin = cb_min + if cb_max is None: + vmax = np.max(vals[i]) + else: + vmax = cb_max cnorm = colors.Normalize(vmin=vmin, vmax=vmax) assert n_patches == len(vals[i]) - ax = fig.add_subplot(1, n_plots, i+1) - for k in range(n_patches): - ax.contourf(xx[k], yy[k], vals[i][k], 50, norm=cnorm, cmap=cmap) #, extend='both') - cbar = fig.colorbar(cm.ScalarMappable(norm=cnorm, cmap=cmap), ax=ax, pad=0.05) - - if gridlines_x1 is not None and gridlines_x2 is not None: - if isinstance(gridlines_x1[0], (list,tuple)): - for x1,x2 in zip(gridlines_x1,gridlines_x2): - if x1 is None or x2 is None:continue - kwargs = {'lw': 0.5} - ax.plot(*x1, color='k', **kwargs) - ax.plot(*x2, color='k', **kwargs) - else: - ax.plot(*gridlines_x1, color='k') - ax.plot(*gridlines_x2, color='k') + ax = fig.add_subplot(1, n_plots, i + 1) + for k in range(n_patches): + ax.contourf( + xx[k], + yy[k], + vals[i][k], + 50, + norm=cnorm, + cmap=cmap, + zorder=- + 10) # , extend='both') + ax.set_rasterization_zorder(0) + cbar = fig.colorbar( + cm.ScalarMappable( + norm=cnorm, + cmap=cmap), + ax=ax, + pad=0.05) + if gridlines_x1 is not None: + ax.plot(*gridlines_x1, color='k') + ax.plot(*gridlines_x2, color='k') if show_xylabel: - ax.set_xlabel( r'$x$', rotation='horizontal' ) - ax.set_ylabel( r'$y$', rotation='horizontal' ) + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') if n_plots > 1: - ax.set_title ( titles[i] ) + ax.set_title(titles[i]) + ax.set_aspect('equal') if save_fig: - print('saving contour plot in file '+save_fig) - plt.savefig(save_fig, bbox_inches='tight',dpi=dpi) + print('saving contour plot in file ' + save_fig) + plt.savefig(save_fig, bbox_inches='tight', dpi=dpi) if not hide_plot: plt.show() if surface_plot: - fig = plt.figure(figsize=(2.6+4.8*n_plots, 4.8)) - fig.suptitle(title+' -- surface', fontsize=14) + fig = plt.figure(figsize=(2.6 + 4.8 * n_plots, 4.8)) + fig.suptitle(title + ' -- surface', fontsize=14) for i in range(n_plots): - vmin = np.min(vals[i]) - vmax = np.max(vals[i]) + if cb_min is None: + vmin = np.min(vals[i]) + else: + vmin = cb_min + if cb_max is None: + vmax = np.max(vals[i]) + else: + vmax = cb_max cnorm = colors.Normalize(vmin=vmin, vmax=vmax) assert n_patches == len(vals[i]) - ax = fig.add_subplot(1, n_plots, i+1, projection='3d') + ax = fig.add_subplot(1, n_plots, i + 1, projection='3d') for k in range(n_patches): - ax.plot_surface(xx[k], yy[k], vals[i][k], norm=cnorm, rstride=10, cstride=10, cmap=cmap, - linewidth=0, antialiased=False) - cbar = fig.colorbar(cm.ScalarMappable(norm=cnorm, cmap=cmap), ax=ax, pad=0.05) + ax.plot_surface( + xx[k], + yy[k], + vals[i][k], + norm=cnorm, + rstride=10, + cstride=10, + cmap=cmap, + linewidth=0, + antialiased=False) + cbar = fig.colorbar( + cm.ScalarMappable( + norm=cnorm, + cmap=cmap), + ax=ax, + pad=0.05) if show_xylabel: - ax.set_xlabel( r'$x$', rotation='horizontal' ) - ax.set_ylabel( r'$y$', rotation='horizontal' ) - ax.set_title ( titles[i] ) + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') + ax.set_title(titles[i]) if save_fig: ext = save_fig[-4:] if ext[0] != '.': - print('WARNING: extension unclear for file_name '+save_fig) - save_fig_surf = save_fig[:-4]+'_surf'+ext - print('saving surface plot in file '+save_fig_surf) + print('WARNING: extension unclear for file_name ' + save_fig) + save_fig_surf = save_fig[:-4] + '_surf' + ext + print('saving surface plot in file ' + save_fig_surf) plt.savefig(save_fig_surf, bbox_inches='tight', dpi=dpi) - else: + + if not hide_plot: plt.show() -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + + def my_small_streamplot( title, vals_x, vals_y, xx, yy, skip=2, amp_factor=1, save_fig=None, hide_plot=False, + show_xylabel=True, dpi='figure', ): """ @@ -352,24 +581,39 @@ def my_small_streamplot( n_patches = len(xx) assert n_patches == len(yy) - fig = plt.figure(figsize=(2.6+4.8, 4.8)) + # fig = plt.figure(figsize=(2.6+4.8, 4.8)) + + fig, ax = plt.subplots(1, 1, figsize=(2.6 + 4.8, 4.8)) + fig.suptitle(title, fontsize=14) delta = 0.25 # x = y = np.arange(-3.0, 3.01, delta) # X, Y = np.meshgrid(x, y) max_val = max(np.max(vals_x), np.max(vals_y)) - #print('max_val = {}'.format(max_val)) - vf_amp = amp_factor/max_val + # print('max_val = {}'.format(max_val)) + vf_amp = amp_factor / max_val for k in range(n_patches): - plt.quiver(xx[k][::skip, ::skip], yy[k][::skip, ::skip], vals_x[k][::skip, ::skip], vals_y[k][::skip, ::skip], - scale=1/(vf_amp*0.05), width=0.002) # width=) units='width', pivot='mid', + ax.quiver(xx[k][::skip, + ::skip], + yy[k][::skip, + ::skip], + vals_x[k][::skip, + ::skip], + vals_y[k][::skip, + ::skip], + scale=1 / (vf_amp * 0.05), + width=0.002) # width=) units='width', pivot='mid', + + if show_xylabel: + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') + + ax.set_aspect('equal') if save_fig: - print('saving vector field (stream) plot in file '+save_fig) + print('saving vector field (stream) plot in file ' + save_fig) plt.savefig(save_fig, bbox_inches='tight', dpi=dpi) if not hide_plot: plt.show() - - diff --git a/psydac/feec/multipatch/tests/__init__.py b/psydac/feec/multipatch/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py new file mode 100644 index 000000000..1aeef51ca --- /dev/null +++ b/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py @@ -0,0 +1,309 @@ +import pytest +from collections import OrderedDict + +import numpy as np +from sympy import Tuple +from scipy.sparse.linalg import norm as sp_norm + +from sympde.topology.domain import Domain +from sympde.topology import Derham, Square, IdentityMapping + +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.operators import HodgeOperator +from psydac.feec.multipatch.non_matching_operators import construct_h1_conforming_projection, construct_hcurl_conforming_projection +from psydac.feec.multipatch.utils_conga_2d import P_phys_l2, P_phys_hdiv, P_phys_hcurl, P_phys_h1 + + +def get_polynomial_function(degree, hom_bc_axes, domain): + """Return a polynomial function of given degree and homogeneus boundary conditions on the domain.""" + + x, y = domain.coordinates + if hom_bc_axes[0]: + assert degree[0] > 1 + g0_x = x * (x - 1) * (x - 1.554)**(degree[0] - 2) + else: + # if degree[0] > 1: + # g0_x = (x-0.543)**2 * (x-1.554)**(degree[0]-2) + # else: + g0_x = (x - 0.25)**degree[0] + + if hom_bc_axes[1]: + assert degree[1] > 1 + g0_y = y * (y - 1) * (y - 0.324)**(degree[1] - 2) + else: + # if degree[1] > 1: + # g0_y = (y-1.675)**2 * (y-0.324)**(degree[1]-2) + + # else: + g0_y = (y - 0.75)**degree[1] + + return g0_x * g0_y + + +# ============================================================================== +@pytest.mark.parametrize('V1_type', ["Hcurl"]) +@pytest.mark.parametrize('degree', [[3, 3]]) +@pytest.mark.parametrize('nc', [5]) +@pytest.mark.parametrize('reg', [0]) +@pytest.mark.parametrize('hom_bc', [False, True]) +@pytest.mark.parametrize('domain_name', ["4patch_nc", "2patch_nc"]) +@pytest.mark.parametrize("nonconforming, full_mom_pres", + [(True, True), (False, True)]) +def test_conf_projectors_2d( + V1_type, + degree, + nc, + reg, + hom_bc, + full_mom_pres, + domain_name, + nonconforming +): + + if domain_name == '2patch_nc': + + A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) + B = Square('B', bounds1=(0.5, 1.), bounds2=(0, 1)) + M1 = IdentityMapping('M1', dim=2) + M2 = IdentityMapping('M2', dim=2) + A = M1(A) + B = M2(B) + + domain = Domain.join(patches=[A, B], + connectivity=[((0, 0, 1), (1, 0, -1), 1)], + name='domain') + + elif domain_name == '4patch_nc': + + A = Square('A', bounds1=(0, 0.5), bounds2=(0, 0.5)) + B = Square('B', bounds1=(0.5, 1.), bounds2=(0, 0.5)) + C = Square('C', bounds1=(0, 0.5), bounds2=(0.5, 1)) + D = Square('D', bounds1=(0.5, 1.), bounds2=(0.5, 1)) + M1 = IdentityMapping('M1', dim=2) + M2 = IdentityMapping('M2', dim=2) + M3 = IdentityMapping('M3', dim=2) + M4 = IdentityMapping('M4', dim=2) + A = M1(A) + B = M2(B) + C = M3(C) + D = M4(D) + + domain = Domain.join(patches=[A, B, C, D], + connectivity=[((0, 0, 1), (1, 0, -1), 1), + ((2, 0, 1), (3, 0, -1), 1), + ((0, 1, 1), (2, 1, -1), 1), + ((1, 1, 1), (3, 1, -1), 1)], + name='domain') + + if nonconforming: + if len(domain) == 2: + ncells_h = { + 'M1(A)': [nc, nc], + 'M2(B)': [2 * nc, 2 * nc], + } + elif len(domain) == 4: + ncells_h = { + 'M1(A)': [nc, nc], + 'M2(B)': [2 * nc, 2 * nc], + 'M3(C)': [2 * nc, 2 * nc], + 'M4(D)': [4 * nc, 4 * nc], + } + + else: + ncells_h = {} + for k, D in enumerate(domain.interior): + ncells_h[D.name] = [nc, nc] + + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + domain_h = discretize(domain, ncells=ncells_h) # Vh space + derham_h = discretize(derham, domain_h, degree=degree) + V0h = derham_h.V0 + V1h = derham_h.V1 + V2h = derham_h.V2 + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) + mappings_list = [m.get_callable_mapping() for m in mappings.values()] + p_derham = Derham(domain, ["H1", V1_type, "L2"]) + + nquads = [(d + 1) for d in degree] + p_derham_h = discretize(p_derham, domain_h, degree=degree) + p_V0h = p_derham_h.V0 + p_V1h = p_derham_h.V1 + p_V2h = p_derham_h.V2 + + # full moment preservation only possible if enough interior functions in a + # patch (<=> enough cells) + if full_mom_pres and (nc >= degree[0] + 1): + mom_pres = degree[0] + else: + mom_pres = -1 + # NOTE: if mom_pres but not full_mom_pres we could test reduced order + # moment preservation... + + # geometric projections (operators) + p_geomP0, p_geomP1, p_geomP2 = p_derham_h.projectors(nquads=nquads) + + # conforming projections (scipy matrices) + cP0 = construct_h1_conforming_projection(V0h, reg, mom_pres, hom_bc) + cP1 = construct_hcurl_conforming_projection(V1h, reg, mom_pres, hom_bc) + cP2 = construct_h1_conforming_projection(V2h, reg - 1, mom_pres, hom_bc) + + HOp0 = HodgeOperator(p_V0h, domain_h) + M0 = HOp0.to_sparse_matrix() # mass matrix + M0_inv = HOp0.get_dual_Hodge_sparse_matrix() # inverse mass matrix + + HOp1 = HodgeOperator(p_V1h, domain_h) + M1 = HOp1.to_sparse_matrix() # mass matrix + M1_inv = HOp1.get_dual_Hodge_sparse_matrix() # inverse mass matrix + + HOp2 = HodgeOperator(p_V2h, domain_h) + M2 = HOp2.to_sparse_matrix() # mass matrix + M2_inv = HOp2.get_dual_Hodge_sparse_matrix() # inverse mass matrix + + bD0, bD1 = p_derham_h.broken_derivatives_as_operators + + bD0 = bD0.to_sparse_matrix() # broken grad + bD1 = bD1.to_sparse_matrix() # broken curl or div + D0 = bD0 @ cP0 # Conga grad + D1 = bD1 @ cP1 # Conga curl or div + + assert np.allclose(sp_norm(cP0 - cP0 @ cP0), 0, 1e-12, + 1e-12) # cP0 is a projection + assert np.allclose(sp_norm(cP1 - cP1 @ cP1), 0, 1e-12, + 1e-12) # cP1 is a projection + assert np.allclose(sp_norm(cP2 - cP2 @ cP2), 0, 1e-12, + 1e-12) # cP2 is a projection + + # D0 maps in the conforming V1 space (where cP1 coincides with Id) + assert np.allclose(sp_norm(D0 - cP1 @ D0), 0, 1e-12, 1e-12) + # D1 maps in the conforming V2 space (where cP2 coincides with Id) + assert np.allclose(sp_norm(D1 - cP2 @ D1), 0, 1e-12, 1e-12) + + # comparing projections of polynomials which should be exact + + # tests on cP0: + g0 = get_polynomial_function( + degree=degree, hom_bc_axes=[ + hom_bc, hom_bc], domain=domain) + g0h = P_phys_h1(g0, p_geomP0, domain, mappings_list) + g0_c = g0h.coeffs.toarray() + + tilde_g0_c = p_derham_h.get_dual_dofs( + space='V0', f=g0, return_format='numpy_array') + g0_L2_c = M0_inv @ tilde_g0_c + + # (P0_geom - P0_L2) polynomial = 0 + assert np.allclose(g0_c, g0_L2_c, 1e-12, 1e-12) + # (P0_geom - confP0 @ P0_L2) polynomial= 0 + assert np.allclose(g0_c, cP0 @ g0_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # testing that polynomial moments are preserved: + # the following projection should be exact for polynomials of proper degree (no bc) + # conf_P0* : L2 -> V0 defined by := + # for all phi in V0 + g0 = get_polynomial_function(degree=degree, hom_bc_axes=[ + False, False], domain=domain) + g0h = P_phys_h1(g0, p_geomP0, domain, mappings_list) + g0_c = g0h.coeffs.toarray() + + tilde_g0_c = p_derham_h.get_dual_dofs( + space='V0', f=g0, return_format='numpy_array') + g0_star_c = M0_inv @ cP0.transpose() @ tilde_g0_c + # (P10_geom - P0_star) polynomial = 0 + assert np.allclose(g0_c, g0_star_c, 1e-12, 1e-12) + + # tests on cP1: + + G1 = Tuple( + get_polynomial_function( + degree=[ + degree[0] - 1, + degree[1]], + hom_bc_axes=[ + False, + hom_bc], + domain=domain), + get_polynomial_function( + degree=[ + degree[0], + degree[1] - 1], + hom_bc_axes=[ + hom_bc, + False], + domain=domain) + ) + + if V1_type == "Hcurl": + G1h = P_phys_hcurl(G1, p_geomP1, domain, mappings_list) + elif V1_type == "Hdiv": + G1h = P_phys_hdiv(G1, p_geomP1, domain, mappings_list) + + G1_c = G1h.coeffs.toarray() + tilde_G1_c = p_derham_h.get_dual_dofs( + space='V1', f=G1, return_format='numpy_array') + G1_L2_c = M1_inv @ tilde_G1_c + + assert np.allclose(G1_c, G1_L2_c, 1e-12, 1e-12) + # (P1_geom - confP1 @ P1_L2) polynomial= 0 + assert np.allclose(G1_c, cP1 @ G1_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # as above + G1 = Tuple( + get_polynomial_function( + degree=[ + degree[0] - 1, + degree[1]], + hom_bc_axes=[ + False, + False], + domain=domain), + get_polynomial_function( + degree=[ + degree[0], + degree[1] - 1], + hom_bc_axes=[ + False, + False], + domain=domain) + ) + + G1h = P_phys_hcurl(G1, p_geomP1, domain, mappings_list) + G1_c = G1h.coeffs.toarray() + + tilde_G1_c = p_derham_h.get_dual_dofs( + space='V1', f=G1, return_format='numpy_array') + G1_star_c = M1_inv @ cP1.transpose() @ tilde_G1_c + # (P1_geom - P1_star) polynomial = 0 + assert np.allclose(G1_c, G1_star_c, 1e-12, 1e-12) + + # tests on cP2 (non trivial for reg = 1): + g2 = get_polynomial_function( + degree=[ + degree[0] - 1, + degree[1] - 1], + hom_bc_axes=[ + False, + False], + domain=domain) + g2h = P_phys_l2(g2, p_geomP2, domain, mappings_list) + g2_c = g2h.coeffs.toarray() + + tilde_g2_c = p_derham_h.get_dual_dofs( + space='V2', f=g2, return_format='numpy_array') + g2_L2_c = M2_inv @ tilde_g2_c + + # (P2_geom - P2_L2) polynomial = 0 + assert np.allclose(g2_c, g2_L2_c, 1e-12, 1e-12) + # (P2_geom - confP2 @ P2_L2) polynomial = 0 + assert np.allclose(g2_c, cP2 @ g2_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # as above, here with same degree and bc as + # tilde_g2_c = p_derham_h.get_dual_dofs(space='V2', f=g2, return_format='numpy_array', nquads=nquads) + g2_star_c = M2_inv @ cP2.transpose() @ tilde_g2_c + # (P2_geom - P2_star) polynomial = 0 + assert np.allclose(g2_c, g2_star_c, 1e-12, 1e-12) diff --git a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py index 51746c218..bb1b09004 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -3,36 +3,202 @@ import numpy as np from psydac.feec.multipatch.examples.hcurl_source_pbms_conga_2d import solve_hcurl_source_pbm +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_conga_2d import hcurl_solve_eigen_pbm +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_dg_2d import hcurl_solve_eigen_pbm_dg +from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm + def test_time_harmonic_maxwell_pretzel_f(): - nc,deg = 10,2 - source_type = 'manu_maxwell' + nc = 4 + deg = 2 + + source_type = 'manu_maxwell_inhom' domain_name = 'pretzel_f' + source_proj = 'tilde_Pi' - omega = np.sqrt(170) # source - roundoff = 1e4 - eta = int(-omega**2 * roundoff)/roundoff + omega = np.pi + eta = -omega**2 # source - l2_error = solve_hcurl_source_pbm( + diags = solve_hcurl_source_pbm( nc=nc, deg=deg, eta=eta, nu=0, - mu=1, #1, + mu=1, domain_name=domain_name, source_type=source_type, + source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(l2_error - 0.06246693595198972)<1e-10 + assert abs(diags["err"] - 0.007201508128407582) < 1e-10 -#============================================================================== -# CLEAN UP SYMPY NAMESPACE -#============================================================================== +def test_time_harmonic_maxwell_pretzel_f_nc(): + deg = 2 + nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, + 4, 4, 4, 4, 8, 8, 8, 4, 4]) + + source_type = 'manu_maxwell_inhom' + domain_name = 'pretzel_f' + source_proj = 'tilde_Pi' + + omega = np.pi + eta = -omega**2 # source + + diags = solve_hcurl_source_pbm( + nc=nc, deg=deg, + eta=eta, + nu=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + source_proj=source_proj, + backend_language='pyccel-gcc') + + assert abs(diags["err"] - 0.004849165663310541) < 1e-10 + + +def test_maxwell_eigen_curved_L_shape(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = 4 + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=0, + generalized_pbm=True, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.01291539899483907) < 1e-10 + +def test_maxwell_eigen_curved_L_shape_nc(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = np.array([[None, 4], + [4, 8]]) + + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=0, + generalized_pbm=True, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell_nc', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.010504876643873904) < 1e-10 + + +def test_maxwell_eigen_curved_L_shape_dg(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = np.array([[None, 4], + [4, 8]]) + + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm_dg( + ncells=ncells, degree=degree, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell_dg', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.035139029534570064) < 1e-10 + + +def test_maxwell_timedomain(): + solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') + +# ============================================================================== +# CLEAN UP SYMPY NAMESPACE +# ============================================================================== def teardown_module(): from sympy.core import cache cache.clear_cache() + def teardown_function(): from sympy.core import cache cache.clear_cache() diff --git a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py index 8eea88ec0..7a0d94cbb 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -1,32 +1,55 @@ +import numpy as np + from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm + def test_poisson_pretzel_f(): source_type = 'manu_poisson_2' domain_name = 'pretzel_f' - nc = 10 + nc = 4 deg = 2 - run_dir = '{}_{}_nc={}_deg={}/'.format(domain_name, source_type, nc, deg) + l2_error = solve_h1_source_pbm( - nc=nc, deg=deg, - eta=0, - mu=1, - domain_name=domain_name, - source_type=source_type, - backend_language='pyccel-gcc', - plot_source=False, - plot_dir='./plots/h1_tests_source_february/'+run_dir) - - assert abs(l2_error-8.054935880021907e-05)<1e-10 - -#============================================================================== -# CLEAN UP SYMPY NAMESPACE -#============================================================================== + nc=nc, deg=deg, + eta=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + backend_language='pyccel-gcc', + plot_dir=None) + + assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 + +def test_poisson_pretzel_f_nc(): + + source_type = 'manu_poisson_2' + domain_name = 'pretzel_f' + nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, + 4, 4, 4, 4, 8, 8, 8, 4, 4]) + deg = 2 + + l2_error = solve_h1_source_pbm( + nc=nc, deg=deg, + eta=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + backend_language='pyccel-gcc', + plot_dir=None) + + assert abs(l2_error - 6.051557012306659e-06) < 1e-10 + + +# ============================================================================== +# CLEAN UP SYMPY NAMESPACE +# ============================================================================== def teardown_module(): from sympy.core import cache cache.clear_cache() + def teardown_function(): from sympy.core import cache cache.clear_cache() diff --git a/psydac/feec/multipatch/utilities.py b/psydac/feec/multipatch/utilities.py index ac37be3a5..2b856f37c 100644 --- a/psydac/feec/multipatch/utilities.py +++ b/psydac/feec/multipatch/utilities.py @@ -1,75 +1,157 @@ # coding: utf-8 import time + + def time_count(t_stamp=None, msg=None): new_t_stamp = time.time() if msg is None: msg = '' else: - msg = '['+msg+']' + msg = '[' + msg + ']' if t_stamp: - print('time elapsed '+msg+': '+repr(new_t_stamp - t_stamp)) + print('time elapsed ' + msg + ': ' + repr(new_t_stamp - t_stamp)) elif len(msg) > 0: - print('time stamp set for '+msg) + print('time stamp set for ' + msg) return new_t_stamp # --------------------------------------------------------------------------------------------------------------- # small/temporary utility for saving/loading sparse matrices, plots... # (should be cleaned !) + def source_name(source_type=None, source_proj=None): """ Get the source term name""" assert source_type and source_proj - return source_type+'_'+source_proj + return source_type + '_' + source_proj + def sol_ref_fn(source_type, N_diag, source_proj=None): """ Get the reference solution filename based on the source term type""" - fn = 'u_ref_'+source_name(source_type, source_proj)+'_N'+repr(N_diag)+'.npz' + fn = 'u_ref_' + source_name(source_type, + source_proj) + '_N' + repr(N_diag) + '.npz' return fn -def error_fn(source_type=None, method=None, conf_proj=None, k=None, domain_name=None,deg=None): + +def error_fn( + source_type=None, + method=None, + conf_proj=None, + k=None, + domain_name=None, + deg=None): """ Get the error filename based on the method used to solve the multpatch problem""" - return 'errors/error_'+domain_name+'_'+source_type+'_'+'_deg'+repr(deg)+'_'+get_method_name(method, k, conf_proj=conf_proj)+'.txt' + return 'errors/error_' + domain_name + '_' + source_type + '_' + '_deg' + \ + repr(deg) + '_' + get_method_name(method, k, conf_proj=conf_proj) + '.txt' + def get_method_name(method=None, k=None, conf_proj=None, penal_regime=None): """ Get method name used to solve the multpatch problem""" if method == 'nitsche': method_name = method - if k==1: + if k == 1: method_name += '_SIP' - elif k==-1: + elif k == -1: method_name += '_NIP' - elif k==0: + elif k == 0: method_name += '_IIP' else: assert k is None elif method == 'conga': method_name = method if conf_proj is not None: - method_name += '_'+conf_proj + method_name += '_' + conf_proj else: raise ValueError(method) if penal_regime is not None: - method_name += '_pr'+repr(penal_regime) + method_name += '_pr' + repr(penal_regime) return method_name -def get_fem_name(method=None, k=None, DG_full=False, conf_proj=None, domain_name=None,nc=None,deg=None,hom_seq=True): + +def get_fem_name( + method=None, + k=None, + DG_full=False, + conf_proj=None, + domain_name=None, + nc=None, + deg=None, + hom_seq=True): """ Get Fem name used to solve the multipatch problem""" assert domain_name - fn = domain_name+(('_nc'+repr(nc)) if nc else '') +(('_deg'+repr(deg)) if deg else '') + fn = domain_name + (('_nc' + repr(nc)) if nc else '') + \ + (('_deg' + repr(deg)) if deg else '') if DG_full: fn += '_fDG' if method is not None: - fn += '_'+get_method_name(method, k, conf_proj) + fn += '_' + get_method_name(method, k, conf_proj) if not hom_seq: fn += '_inhom' return fn -def get_load_dir(method=None, DG_full=False, domain_name=None,nc=None,deg=None,data='matrices'): + +def FEM_sol_fn(source_type=None, source_proj=None): + """ Get the filename for FEM solution coeffs in numpy array format """ + fn = 'sol_' + source_name(source_type, source_proj) + '.npy' + return fn + + +def get_load_dir( + method=None, + DG_full=False, + domain_name=None, + nc=None, + deg=None, + data='matrices'): """ get load directory name based on the fem name""" - assert data in ['matrices','solutions','rhs'] + assert data in ['matrices', 'solutions', 'rhs'] if method is None: assert data == 'rhs' - fem_name = get_fem_name(domain_name=domain_name,method=method, nc=nc,deg=deg, DG_full=DG_full) - return './saved_'+data+'/'+fem_name+'/' + fem_name = get_fem_name( + domain_name=domain_name, + method=method, + nc=nc, + deg=deg, + DG_full=DG_full) + return './saved_' + data + '/' + fem_name + '/' + + +def get_run_dir(domain_name, nc, deg, source_type=None, conf_proj=None): + """ Get the run directory name""" + rdir = domain_name + if source_type: + rdir += '_' + source_type + if conf_proj: + rdir += '_P=' + conf_proj + rdir += '_nc={}_deg={}'.format(nc, deg) + return rdir + + +def get_plot_dir(case_dir, run_dir): + """ Get the plot directory name""" + return './plots/' + case_dir + '/' + run_dir + + +def get_mat_dir(domain_name, nc, deg, quad_param=None): + """ Get the directory name where matrices are stored""" + mat_dir = './saved_matrices/matrices_{}_nc={}_deg={}'.format( + domain_name, nc, deg) + if quad_param is not None: + mat_dir += '_qp={}'.format(quad_param) + return mat_dir + + +def get_sol_dir(case_dir, domain_name, nc, deg): + """ Get the directory name where solutions are stored""" + return './saved_solutions/' + case_dir + \ + '/solutions_{}_nc={}_deg={}'.format(domain_name, nc, deg) + + +def diag_fn(source_type=None, source_proj=None): + """ Get the diagnostics filename""" + if source_type is not None: + fn = 'diag_' + source_name(source_type, source_proj) + '.txt' + else: + fn = 'diag.txt' + return fn diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index e69de29bb..d86defda3 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -0,0 +1,280 @@ +import os +import datetime + +import numpy as np + +from sympy import lambdify +from sympde.topology import Derham + +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_l2 + +from psydac.feec.multipatch.api import discretize +from psydac.feec.multipatch.utilities import time_count # , export_sol, import_sol +from psydac.linalg.utilities import array_to_psydac +from psydac.fem.basic import FemField +from psydac.feec.multipatch.plotting_utilities import get_plotting_grid, get_grid_quad_weights, get_grid_vals + + +# commuting projections on the physical domain (should probably be in the +# interface) +def P0_phys(f_phys, P0, domain, mappings_list): + f = lambdify(domain.coordinates, f_phys) + f_log = [pull_2d_h1(f, m.get_callable_mapping()) for m in mappings_list] + return P0(f_log) + + +def P1_phys(f_phys, P1, domain, mappings_list): + f_x = lambdify(domain.coordinates, f_phys[0]) + f_y = lambdify(domain.coordinates, f_phys[1]) + f_log = [pull_2d_hcurl([f_x, f_y], m.get_callable_mapping()) + for m in mappings_list] + return P1(f_log) + + +def P2_phys(f_phys, P2, domain, mappings_list): + f = lambdify(domain.coordinates, f_phys) + f_log = [pull_2d_l2(f, m.get_callable_mapping()) for m in mappings_list] + return P2(f_log) + +# commuting projections on the physical domain (should probably be in the +# interface) + + +def P_phys_h1(f_phys, P0, domain, mappings_list): + f = lambdify(domain.coordinates, f_phys) + if len(mappings_list) == 1: + m = mappings_list[0] + f_log = pull_2d_h1(f, m) + else: + f_log = [pull_2d_h1(f, m) for m in mappings_list] + return P0(f_log) + + +def P_phys_hcurl(f_phys, P1, domain, mappings_list): + f_x = lambdify(domain.coordinates, f_phys[0]) + f_y = lambdify(domain.coordinates, f_phys[1]) + f_log = [pull_2d_hcurl([f_x, f_y], m) for m in mappings_list] + return P1(f_log) + + +def P_phys_hdiv(f_phys, P1, domain, mappings_list): + f_x = lambdify(domain.coordinates, f_phys[0]) + f_y = lambdify(domain.coordinates, f_phys[1]) + f_log = [pull_2d_hdiv([f_x, f_y], m) for m in mappings_list] + return P1(f_log) + + +def P_phys_l2(f_phys, P2, domain, mappings_list): + f = lambdify(domain.coordinates, f_phys) + f_log = [pull_2d_l2(f, m) for m in mappings_list] + return P2(f_log) + + +def get_kind(space='V*'): + # temp helper + if space == 'V0': + kind = 'h1' + elif space == 'V1': + kind = 'hcurl' + elif space == 'V2': + kind = 'l2' + else: + raise ValueError(space) + return kind + + +# =============================================================================== +class DiagGrid(): + """ + Class storing: + - a diagnostic cell-centered grid + - writing / quadrature utilities + - a ref solution + + to compare solutions from different FEM spaces on same domain + """ + + def __init__(self, mappings=None, N_diag=None): + + mappings_list = list(mappings.values()) + etas, xx, yy, patch_logvols = get_plotting_grid( + mappings, N=N_diag, centered_nodes=True, return_patch_logvols=True) + quad_weights = get_grid_quad_weights( + etas, patch_logvols, mappings_list) + + self.etas = etas + self.xx = xx + self.yy = yy + self.patch_logvols = patch_logvols + self.quad_weights = quad_weights + self.mappings_list = mappings_list + + self.sol_ref = {} # Fem fields + self.sol_vals = {} # values on diag grid + self.sol_ref_vals = {} # values on diag grid + + def grid_vals_h1(self, v): + return get_grid_vals(v, self.etas, self.mappings_list, space_kind='h1') + + def grid_vals_hcurl(self, v): + return get_grid_vals( + v, + self.etas, + self.mappings_list, + space_kind='hcurl') + + def create_ref_fem_spaces(self, domain=None, ref_nc=None, ref_deg=None): + print('[DiagGrid] Discretizing the ref FEM space...') + degree = [ref_deg, ref_deg] + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + ref_nc = {patch.name: [ref_nc, ref_nc] for patch in domain.interior} + + domain_h = discretize(domain, ncells=ref_nc) + # , backend=PSYDAC_BACKENDS[backend_language]) + derham_h = discretize(derham, domain_h, degree=degree) + self.V0h = derham_h.V0 + self.V1h = derham_h.V1 + + def import_ref_sol_from_coeffs(self, sol_ref_filename=None, space='V*'): + print('[DiagGrid] loading coeffs of ref_sol from {}...'.format( + sol_ref_filename)) + if space == 'V0': + Vh = self.V0h + elif space == 'V1': + Vh = self.V1h + else: + raise ValueError(space) + try: + coeffs = np.load(sol_ref_filename) + except OSError: + print("-- WARNING: file not found, setting sol_ref = 0") + coeffs = np.zeros(Vh.nbasis) + if space in self.sol_ref: + print( + 'WARNING !! sol_ref[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + self.sol_ref[space] = FemField( + Vh, coeffs=array_to_psydac( + coeffs, Vh.vector_space)) + + def write_sol_values(self, v, space='V*'): + """ + v: FEM field + """ + if space in self.sol_vals: + print( + 'WARNING !! sol_vals[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + self.sol_vals[space] = get_grid_vals( + v, self.etas, self.mappings_list, space_kind=get_kind(space)) + + def write_sol_ref_values(self, v=None, space='V*'): + """ + if no FemField v is provided, then use the self.sol_ref (must have been imported) + """ + if space in self.sol_vals: + print( + 'WARNING !! sol_ref_vals[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + if v is None: + # then sol_ref must have been imported + v = self.sol_ref[space] + self.sol_ref_vals[space] = get_grid_vals( + v, self.etas, self.mappings_list, space_kind=get_kind(space)) + + def compute_l2_error(self, space='V*'): + if space in ['V0', 'V2']: + u = self.sol_ref_vals[space] + uh = self.sol_vals[space] + abs_u = [np.abs(p) for p in u] + abs_uh = [np.abs(p) for p in uh] + errors = [np.abs(p - q) for p, q in zip(u, uh)] + elif space == 'V1': + u_x, u_y = self.sol_ref_vals[space] + uh_x, uh_y = self.sol_vals[space] + abs_u = [np.sqrt((u1)**2 + (u2)**2) for u1, u2 in zip(u_x, u_y)] + abs_uh = [np.sqrt((u1)**2 + (u2)**2) for u1, u2 in zip(uh_x, uh_y)] + errors = [np.sqrt((u1 - v1)**2 + (u2 - v2)**2) + for u1, v1, u2, v2 in zip(u_x, uh_x, u_y, uh_y)] + else: + raise ValueError(space) + + l2_norm_uh = ( + np.sum([J_F * v**2 for v, J_F in zip(abs_uh, self.quad_weights)]))**0.5 + l2_norm_u = ( + np.sum([J_F * v**2 for v, J_F in zip(abs_u, self.quad_weights)]))**0.5 + l2_error = ( + np.sum([J_F * v**2 for v, J_F in zip(errors, self.quad_weights)]))**0.5 + + return l2_norm_uh, l2_norm_u, l2_error + + def get_diags_for(self, v, space='V*', print_diags=True): + self.write_sol_values(v, space) + sol_norm, sol_ref_norm, l2_error = self.compute_l2_error(space) + rel_l2_error = l2_error / (max(sol_norm, sol_ref_norm)) + diags = { + 'sol_norm': sol_norm, + 'sol_ref_norm': sol_ref_norm, + 'rel_l2_error': rel_l2_error, + } + if print_diags: + print(' .. l2 norms (computed via quadratures on diag_grid): ') + print(diags) + + return diags + + +def get_Vh_diags_for( + v=None, + v_ref=None, + M_m=None, + print_diags=True, + msg='error between ?? and ?? in Vh'): + """ + v, v_ref: FemField + M_m: mass matrix in scipy format + """ + uh_c = v.coeffs.toarray() + uh_ref_c = v_ref.coeffs.toarray() + err_c = uh_c - uh_ref_c + l2_error = np.dot(err_c, M_m.dot(err_c))**0.5 + sol_norm = np.dot(uh_c, M_m.dot(uh_c))**0.5 + sol_ref_norm = np.dot(uh_ref_c, M_m.dot(uh_ref_c))**0.5 + rel_l2_error = l2_error / (max(sol_norm, sol_ref_norm)) + diags = { + 'sol_norm': sol_norm, + 'sol_ref_norm': sol_ref_norm, + 'rel_l2_error': rel_l2_error, + } + if print_diags: + print(' .. l2 norms ({}): '.format(msg)) + print(diags) + + return diags + + +def write_diags_to_file(diags, script_filename, diag_filename, params=None): + """ write diagnostics to file """ + print(' -- writing diags to file {} --'.format(diag_filename)) + if not os.path.exists(diag_filename): + open(diag_filename, 'w') + + with open(diag_filename, 'a') as a_writer: + a_writer.write('\n') + a_writer.write( + ' ---------- ---------- ---------- ---------- ---------- ---------- \n') + a_writer.write(' run script: \n {}\n'.format(script_filename)) + a_writer.write( + ' executed on: \n {}\n\n'.format( + datetime.datetime.now())) + a_writer.write(' params: \n') + for key, value in params.items(): + a_writer.write(' {}: {} \n'.format(key, value)) + a_writer.write('\n') + a_writer.write(' diags: \n') + for key, value in diags.items(): + a_writer.write(' {}: {} \n'.format(key, value)) + a_writer.write( + ' ---------- ---------- ---------- ---------- ---------- ---------- \n') + a_writer.write('\n') diff --git a/pyproject.toml b/pyproject.toml index 5a3781725..d59494d00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,10 @@ dependencies = [ 'tblib', # IGAKIT - not on PyPI - 'igakit @ https://github.com/dalcinl/igakit/archive/refs/heads/master.zip' + + # !! WARNING !! Path to igakit below is from fork pyccel/igakit. This was done to + # quickly fix the numpy 2.0 issue. See https://github.com/dalcinl/igakit/pull/4 + 'igakit @ https://github.com/pyccel/igakit/archive/refs/heads/bugfix-numpy2.0.zip' ] [project.urls]