From efa13875627bc12c1173a45f7ba473918fc1c1da Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 26 Jun 2025 09:29:22 +0200 Subject: [PATCH 001/102] minor improvements in the calculation of the restriction operator --- .../feec/multipatch/non_matching_operators.py | 172 ++++++++---------- 1 file changed, 73 insertions(+), 99 deletions(-) diff --git a/psydac/feec/multipatch/non_matching_operators.py b/psydac/feec/multipatch/non_matching_operators.py index 4b172e7de..a8003863d 100644 --- a/psydac/feec/multipatch/non_matching_operators.py +++ b/psydac/feec/multipatch/non_matching_operators.py @@ -9,6 +9,7 @@ from scipy.sparse import eye as sparse_eye from scipy.sparse import csr_matrix +from scipy.special import comb from sympde.topology import Boundary, Interface @@ -118,7 +119,6 @@ def get_corners(domain, boundary_only): patches = domain.interior.args bd = domain.boundary - # corner_data[corner] = (patch_ind => local coordinates) corner_data = dict() if boundary_only: @@ -143,7 +143,6 @@ def get_corners(domain, boundary_only): else: for co in cos: corner_data[co] = dict() - for cb in co.corners: p_ind = patches.index(cb.domain) c_coord = cb.coordinates @@ -192,63 +191,48 @@ def construct_restriction_operator_1D( """ 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': + #map V^+ to V^+_0 T = np.zeros((n_f, n_f)) - for i in range(1, n_f - 1): + for i in range(n_f): 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) + 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] + 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)[:, 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 - + # L^2 projection from V^+_0 to V^- + R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) + gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) + n = len(gamma) + + # maps V^- to V^+_0 in a moment preserving way + T2 = np.eye(n_c) + T2[0, 0] = T2[-1, -1] = 0 + T2[1:n+1, 0] += gamma + T2[-(n+1):-1, -1] += gamma[::-1] + + # maps V^+ to V^- in a moment preserving way + R = T2 @ R @ T + + else: + R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) + R = R @ T + + # add the degrees of freedom of T back R[0, 0] += 1 R[-1, -1] += 1 + else: - cf_mass_mat = calculate_mixed_mass_matrix( - coarse_space_1d, fine_space_1d).transpose() + 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:, :] - + # The pure L^2 projection is already moment preserving R = np.linalg.solve(c_mass_mat, cf_mass_mat) return R @@ -287,8 +271,7 @@ def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): 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) + 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, @@ -297,25 +280,18 @@ def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): 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 + assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) + 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 @@ -418,8 +394,7 @@ def calculate_mixed_mass_matrix(domain_space, codomain_space): 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] + 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] @@ -471,7 +446,6 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): enddom = breaks[-1] begdom = breaks[0] denom = enddom - begdom - order = max(p_moments + 1, deg + 1) u, w = gauss_legendre(order) @@ -484,8 +458,7 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): 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 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. @@ -494,7 +467,7 @@ def calculate_poly_basis_integral(space_1d, p_moments=-1): 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 + comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / denom)**pol locind2 = il2 + spans[ie1] - deg Mass_mat[pol, locind2] += val @@ -583,7 +556,7 @@ def construct_h1_conforming_projection( # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction( - Vh.patch_spaces[0].spaces[0], p_moments=p_moments) + Vh.spaces[0].spaces[0], p_moments=p_moments) domain = Vh.symbolic_space.domain ndim = 2 @@ -592,22 +565,22 @@ def construct_h1_conforming_projection( l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) for k in range(n_patches): - Vk = Vh.patch_spaces[k] + 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") + 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.patch_spaces[patch].spaces[coords[0]].nbasis - 1 - nbasis1 = Vh.patch_spaces[patch].spaces[coords[1]].nbasis - 1 + + 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 @@ -621,12 +594,11 @@ def vertex_moment_indices(axis, coords, patch, p_moments): if coords[axis] == 0: return range(1, p_moments + 2) else: - return range(Vh.patch_spaces[patch].spaces[coords[axis]].nbasis - 1 - 1, - Vh.patch_spaces[patch].spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) + 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) @@ -639,9 +611,9 @@ def vertex_moment_indices(axis, coords, patch, p_moments): 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) @@ -701,7 +673,6 @@ def vertex_moment_indices(axis, coords, patch, p_moments): corners = get_corners(domain, True) if hom_bc: for (bd, co) in corners.items(): - for patch1 in co: # local vertex coordinates in patch2 @@ -714,6 +685,7 @@ def vertex_moment_indices(axis, coords, patch, p_moments): # local vertex coordinates in patch2 coords2 = co[patch2] + # global index jg = get_vertex_index_from_patch(patch2, coords2) @@ -830,8 +802,8 @@ def get_mu_minus(j, coarse_space, fine_space, R): k_minus = get_patch_index_from_face(domain, I.minus) k_plus = get_patch_index_from_face(domain, I.plus) - I_minus_ncells = Vh.patch_spaces[k_minus].ncells - I_plus_ncells = Vh.patch_spaces[k_plus].ncells + 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: @@ -848,8 +820,8 @@ def get_mu_minus(j, coarse_space, fine_space, R): d_fine = 1 - fine_axis d_coarse = 1 - coarse_axis - space_fine = Vh.patch_spaces[k_fine] - space_coarse = Vh.patch_spaces[k_coarse] + 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] @@ -962,7 +934,7 @@ def get_mu_minus(j, coarse_space, fine_space, R): if hom_bc: for bn in domain.boundary: k = get_patch_index_from_face(domain, bn) - space_k = Vh.patch_spaces[k] + space_k = Vh.spaces[k] axis = bn.axis d = 1 - axis @@ -979,17 +951,19 @@ def get_mu_minus(j, coarse_space, fine_space, R): 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)) + #if corner_indices.issuperset({ig}): + mu_minus = get_mu_minus( + i, 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: + 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] + + if not corner_indices.issuperset({ig}): + corner_indices.add(ig) multi_index = [None] * ndim for p in range(p_moments + 1): @@ -997,8 +971,7 @@ def get_mu_minus(j, coarse_space, fine_space, R): 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 + 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] @@ -1037,8 +1010,9 @@ def construct_hcurl_conforming_projection( return sparse_eye(dim_tot, format="lil") # moment corrections perpendicular to interfaces + # should be in the V^0 spaces gamma = [get_1d_moment_correction( - Vh.patch_spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] domain = Vh.symbolic_space.domain ndim = 2 @@ -1047,7 +1021,7 @@ def construct_hcurl_conforming_projection( l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) for k in range(n_patches): - Vk = Vh.patch_spaces[k] + 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) @@ -1073,7 +1047,7 @@ def edge_moment_index(p, i, axis, ext, space, k): 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 @@ -1086,8 +1060,8 @@ def edge_moment_index(p, i, axis, ext, space, k): 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.patch_spaces[k_minus].spaces[d_minus].ncells[d_minus] - I_plus_ncells = Vh.patch_spaces[k_plus].spaces[d_plus].ncells[d_plus] + 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: @@ -1104,8 +1078,8 @@ def edge_moment_index(p, i, axis, ext, space, k): d_fine = 1 - fine_axis d_coarse = 1 - coarse_axis - space_fine = Vh.patch_spaces[k_fine] - space_coarse = Vh.patch_spaces[k_coarse] + 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] @@ -1164,7 +1138,7 @@ def edge_moment_index(p, i, axis, ext, space, k): # boundary condition for bn in domain.boundary: k = get_patch_index_from_face(domain, bn) - space_k = Vh.patch_spaces[k] + space_k = Vh.spaces[k] axis = bn.axis if not hom_bc: From e9c8c5bcad200e245f07fcd6731e0f82818c8085 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 26 Jun 2025 10:26:00 +0200 Subject: [PATCH 002/102] move non_matching_operators to operators, remove obsolete code, move things to utils_conga_2d --- .../feec/multipatch/non_matching_operators.py | 1160 ----------- psydac/feec/multipatch/operators.py | 1783 ++++++++++------- psydac/feec/multipatch/utils_conga_2d.py | 142 +- 3 files changed, 1202 insertions(+), 1883 deletions(-) delete mode 100644 psydac/feec/multipatch/non_matching_operators.py diff --git a/psydac/feec/multipatch/non_matching_operators.py b/psydac/feec/multipatch/non_matching_operators.py deleted file mode 100644 index a8003863d..000000000 --- a/psydac/feec/multipatch/non_matching_operators.py +++ /dev/null @@ -1,1160 +0,0 @@ -""" -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 scipy.special import comb - -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 = 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': - - #map V^+ to V^+_0 - T = np.zeros((n_f, n_f)) - for i in range(n_f): - 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).transpose() - c_mass_mat = calculate_mass_matrix(coarse_space_1d) - - if p_moments > 0: - # L^2 projection from V^+_0 to V^- - R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) - gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) - n = len(gamma) - - # maps V^- to V^+_0 in a moment preserving way - T2 = np.eye(n_c) - T2[0, 0] = T2[-1, -1] = 0 - T2[1:n+1, 0] += gamma - T2[-(n+1):-1, -1] += gamma[::-1] - - # maps V^+ to V^- in a moment preserving way - R = T2 @ R @ T - - else: - R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) - R = R @ T - - # add the degrees of freedom of T back - 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) - - # The pure L^2 projection is already moment preserving - 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 - - assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) - - else: - ER_1D = R_1D = E_1D = sparse_eye( - fine_space_1d.nbasis, format="lil") - - 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 * \ - comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / 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): - - 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( - i, 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] - - if not corner_indices.issuperset({ig}): - corner_indices.add(ig) - 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 - # should be in the V^0 spaces - 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 0ee00210c..6fec9752a 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -3,32 +3,30 @@ # Conga operators on piecewise (broken) de Rham sequences from sympy import Tuple -from mpi4py import MPI import os import numpy as np from scipy.sparse import save_npz, load_npz -from scipy.sparse import kron, block_diag +from scipy.sparse import block_diag from scipy.sparse.linalg import inv -from sympde.topology import Boundary, Interface, Union +from sympde.topology import Boundary, Interface 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.calculus import dot +from sympde.calculus import 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 quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid 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.fem.splines import SplineSpace +from psydac.utilities.quadratures import gauss_legendre from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 @@ -36,8 +34,14 @@ from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator +from scipy.sparse import eye as sparse_eye +from scipy.sparse import csr_matrix +from scipy.special import comb + + def get_patch_index_from_face(domain, face): - """ Return the patch index of subdomain/boundary + """ + Return the patch index of subdomain/boundary Parameters ---------- @@ -69,854 +73,1213 @@ def get_patch_index_from_face(domain, 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. +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 ---------- - corner1 : - The first corner of the 2D interface + domain: + The discrete domain of the projector - corner2 : - The second corner of the 2D interface + boundary_only : + Only return vertices that lie on a boundary - domain : - The Symbolic domain + """ + cos = domain.corners + patches = domain.interior.args + bd = domain.boundary - Returns - ------- - interface: - The interface between two vertices + 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) - interface = [] - interfaces = domain.interfaces + 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 - if not isinstance(interfaces, Union): - interfaces = (interfaces,) + return corner_data - for i in interfaces: - if i.plus.domain in [corner1.domain, corner2.domain]: - if i.minus.domain in [corner1.domain, corner2.domain]: - interface.append(i) - bd1 = corner1.boundaries - bd2 = corner2.boundaries +def construct_extension_operator_1D(domain, codomain): + """ + Compute the matrix of the extension operator on the interface. - new_interface = [] + Parameters + ---------- + domain : 1d spline space on the interface (coarse grid) + codomain : 1d spline space on the interface (fine grid) + """ - for i in interface: - if i.minus in bd1 + bd2: - if i.plus in bd2 + bd1: - new_interface.append(i) + from psydac.core.bsplines import hrefinement_matrix + ops = [] - 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)) - return None + assert domain.ncells <= codomain.ncells + Ts = knots_to_insert(domain.breaks, codomain.breaks) + P = hrefinement_matrix(Ts, domain.degree, domain.knots) -def get_row_col_index(corner1, corner2, interface, axis, V1, V2): - """ Return the row and column index of a corner in the StencilInterfaceMatrix - for dofs of H1 type spaces + 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 ---------- - corner1 : - The first corner of the 2D interface + 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': + + #map V^+ to V^+_0 + T = np.zeros((n_f, n_f)) + for i in range(n_f): + 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).transpose() + c_mass_mat = calculate_mass_matrix(coarse_space_1d) + + if p_moments > 0: + # L^2 projection from V^+_0 to V^- + R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) + gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) + n = len(gamma) + + # maps V^- to V^+_0 in a moment preserving way + T2 = np.eye(n_c) + T2[0, 0] = T2[-1, -1] = 0 + T2[1:n+1, 0] += gamma + T2[-(n+1):-1, -1] += gamma[::-1] + + # maps V^+ to V^- in a moment preserving way + R = T2 @ R @ T + + else: + R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) + R = R @ T + + # add the degrees of freedom of T back + 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) + + # The pure L^2 projection is already moment preserving + R = np.linalg.solve(c_mass_mat, cf_mass_mat) - corner2 : - The second corner of the 2D interface + return R - interface : - The interface between the two corners - axis : - Axis of the interface +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. - V1 : - Test Space + fine_space_1d : SplineSpace + Spline space of the fine space. - V2 : - Trial Space + p_moments : {int} + Amount of moments to be preserved. Returns ------- - index: - 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.coeff_space.starts - end = V1.coeff_space.ends - degree = V2.degree - start_end = (start, end) + E_1D : numpy array + Extension matrix. - row = [None] * len(start) - col = [0] * len(start) + R_1D : numpy array + Restriction matrix. - assert corner1.boundaries[0].axis == corner2.boundaries[0].axis + 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 - for bd in corner1.boundaries: - row[bd.axis] = start_end[(bd.ext + 1) // 2][bd.axis] + 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) - 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] + E_1D = construct_extension_operator_1D( + domain=coarse_space_1d_k_plus, codomain=fine_space_1d) - if interface is None: - return row + col + + R_1D = construct_restriction_operator_1D( + coarse_space_1d_k_plus, fine_space_1d, E_1D, p_moments) + + ER_1D = E_1D @ R_1D - axis = interface.axis + assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) - if interface.minus.domain == corner1.domain: - 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] + ER_1D = R_1D = E_1D = sparse_eye( + fine_space_1d.nbasis, format="lil") - if interface.minus.ext == interface.plus.ext: - pass - elif interface.minus.domain == corner1.domain: - if interface.minus.ext == -1: - col[axis] = degree[axis] - else: - col[axis] = -degree[axis] - else: - if interface.plus.ext == -1: - col[axis] = degree[axis] - else: - col[axis] = -degree[axis] + return E_1D, R_1D, ER_1D - return row + col +# Didn't find this utility in the code base. +def calculate_mass_matrix(space_1d): + """ + Calculate the mass-matrix of a 1d spline-space. -# =============================================================================== -def allocate_interface_matrix(corners, test_space, trial_space): - """ Allocate the interface matrix for a vertex shared by two patches + 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 ---------- - corners: - The patch corners corresponding to the common shared vertex - test_space: - The test space + domain_space : SplineSpace + Spline space of the domain space. - trial_space: - The trial space + codomain_space : SplineSpace + Spline space of the codomain space. Returns ------- - mat: - The interface matrix shared by two patches + + Mass_mat : numpy array + Mass matrix. """ - bi, bj = list(zip(*corners)) - permutation = np.arange(bi[0].domain.dim) - - flips = [] - k = 0 - while k < len(bi): - c1 = np.array(bi[k].coordinates) - c2 = np.array(bj[k].coordinates)[permutation] - flips.append( - np.array([-1 if d1 != d2 else 1 for d1, d2 in zip(c1, c2)])) - - if np.sum(abs(flips[0] - flips[-1])) != 0: - prod = [f1 * f2 for f1, f2 in zip(flips[0], flips[-1])] - while -1 in prod: - i1 = prod.index(-1) - if -1 in prod[i1 + 1:]: - i2 = i1 + 1 + prod[i1 + 1:].index(-1) - prod = prod[i2 + 1:] - permutation[i1], permutation[i2] = permutation[i2], permutation[i1] - k = -1 - flips = [] - else: - break - - k += 1 - - assert all(abs(flips[0] - i).sum() == 0 for i in flips) - cs = list(zip(*[i.coordinates for i in bi])) - axis = [all(i[0] == j for j in i) for i in cs].index(True) - ext = 1 if cs[axis][0] == 1 else -1 - s = test_space.get_assembly_grids( - )[axis].spans[-1 if ext == 1 else 0] - test_space.degree[axis] - - mat = StencilInterfaceMatrix( - trial_space.coeff_space, - test_space.coeff_space, - s, - s, - axis, - flip=flips[0], - permutation=list(permutation)) - return mat + if domain_space.nbasis > codomain_space.nbasis: + coarse_space = codomain_space + fine_space = domain_space + else: + coarse_space = domain_space + fine_space = codomain_space -# =============================================================================== -# The following operators are not compatible with the changes in the Stencil format -# and their datatype does not allow for non-matching interfaces, but they might be -# useful for future implementations -# =============================================================================== + 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 -class ConformingProjection_V0(FemLinearOperator): + 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): """ - Conforming projection from global broken V0 space to conforming global V0 space - Defined by averaging of interface dofs + Calculate the "mixed mass-matrix" of a 1d spline-space with polynomials. Parameters ---------- - V0h: - The discrete space - domain_h: - The discrete domain of the projector + space_1d : SplineSpace + Spline space of the fine space. - hom_bc : - Apply homogenous boundary conditions if True + p_moments : Int + Amount of moments to be preserved. - backend_language: - The backend used to accelerate the code + Returns + ------- - storage_fn: - filename to store/load the operator sparse matrix + Mass_mat : numpy array + Mass matrix. """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) - def __init__( - self, - V0h, - domain_h, - hom_bc=False, - backend_language='python', - storage_fn=None): + 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) - FemLinearOperator.__init__(self, fem_domain=V0h) + nquad = len(w) + quad_x, quad_w = quadrature_grid(space_1d.breaks, u, w) - V0 = V0h.symbolic_space - domain = V0.domain - self.symbolic_domain = domain + coarse_basis = basis_ders_on_quad_grid(knots, deg, quad_x, 0, spl_type) + spans = elements_spans(knots, deg) - if storage_fn and os.path.exists(storage_fn): + 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 * \ + comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / 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( - "[ConformingProjection_V0] loading operator sparse matrix from " + - storage_fn) - self._sparse_matrix = load_npz(storage_fn) + " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + 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 - else: - # assemble the operator matrix - u, v = elements_of(V0, names='u, v') - expr = u * v # dot(u,v) + Mass_mat = calculate_poly_basis_integral(space_1d, p_moments) + gamma = np.linalg.solve(Mass_mat[:, 1:p_moments + 2], Mass_mat[:, 0]) - Interfaces = domain.interfaces # note: interfaces does not include the boundary - # this penalization is for an H1-conforming space - expr_I = (plus(u) - minus(u)) * (plus(v) - minus(v)) + return gamma - a = BilinearForm((u, v), integral(domain, expr) + - integral(Interfaces, expr_I)) - # print('[[ forcing python backend for ConformingProjection_V0]] ') - # backend_language = 'python' - ah = discretize( - a, domain_h, [ - V0h, V0h], backend=PSYDAC_BACKENDS[backend_language]) - # self._A = ah.assemble() - self._A = ah.forms[0]._matrix +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). - spaces = self._A.domain.spaces + Parameters + ---------- + Vh : TensorFemSpace + Finite Element Space coming from the discrete de Rham sequence. - if isinstance(Interfaces, Interface): - Interfaces = (Interfaces, ) + reg_orders : (int) + Regularity in each space direction -1 or 0. - for b1 in self._A.blocks: - for A in b1: - if A is None: - continue - A[:, :, :, :] = 0 + p_moments : (int) + Number of moments to be preserved. - indices = [slice(None, None)] * domain.dim + [0] * domain.dim + hom_bc : (bool) + Homogeneous boundary conditions. - for i in range(len(self._A.blocks)): - self._A[i, i][tuple(indices)] = 1 + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ - for I in Interfaces: + dim_tot = Vh.nbasis - axis = I.axis - i_minus = get_patch_index_from_face(domain, I.minus) - i_plus = get_patch_index_from_face(domain, I.plus) + # fully discontinuous space + if reg_orders < 0: + return sparse_eye(dim_tot, format="lil") - sp_minus = spaces[i_minus] - sp_plus = spaces[i_plus] + # moment corrections perpendicular to interfaces + # assume same moments everywhere + gamma = get_1d_moment_correction( + Vh.spaces[0].spaces[0], p_moments=p_moments) - s_minus = sp_minus.starts[axis] - e_minus = sp_minus.ends[axis] + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 1 + n_patches = len(domain) - s_plus = sp_plus.starts[axis] - e_plus = sp_plus.ends[axis] + 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) - d_minus = V0h.spaces[i_minus].degree[axis] - d_plus = V0h.spaces[i_plus].degree[axis] + # P vertex + # vertex correction matrix + Proj_vertex = sparse_eye(dim_tot, format="lil") - indices = [slice(None, None)] * domain.dim + [0] * domain.dim + corner_indices = set() + corners = get_corners(domain, False) - minus_ext = I.minus.ext - plus_ext = I.plus.ext + def get_vertex_index_from_patch(patch, coords): + nbasis0 = Vh.spaces[patch].spaces[coords[0]].nbasis - 1 + nbasis1 = Vh.spaces[patch].spaces[coords[1]].nbasis - 1 - if minus_ext == 1: - indices[axis] = e_minus - else: - indices[axis] = s_minus - self._A[i_minus, i_minus][tuple(indices)] = 1 / 2 + # 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 - if plus_ext == 1: - indices[axis] = e_plus - else: - indices[axis] = s_plus + # global index + return l2g.get_index(patch, 0, multi_index) - self._A[i_plus, i_plus][tuple(indices)] = 1 / 2 + 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) - if plus_ext == minus_ext: - if minus_ext == 1: - indices[axis] = d_minus - else: - indices[axis] = s_minus + # 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) - self._A[i_minus, i_plus][tuple(indices)] = 1 / 2 + for patch1 in co: + # local vertex coordinates in patch1 + coords1 = co[patch1] + # global index + ig = get_vertex_index_from_patch(patch1, coords1) - if plus_ext == 1: - indices[axis] = d_plus - else: - indices[axis] = s_plus + corner_indices.add(ig) - self._A[i_plus, i_minus][tuple(indices)] = 1 / 2 + for patch2 in co: + # local vertex coordinates in patch2 + coords2 = co[patch2] - else: - if minus_ext == 1: - indices[axis] = d_minus - else: - indices[axis] = s_minus - - if plus_ext == 1: - indices[domain.dim + axis] = d_plus - else: - indices[domain.dim + axis] = -d_plus - - self._A[i_minus, i_plus][tuple(indices)] = 1 / 2 - - if plus_ext == 1: - indices[axis] = d_plus - else: - indices[axis] = s_plus - - if minus_ext == 1: - indices[domain.dim + axis] = d_minus - else: - indices[domain.dim + axis] = -d_minus - - self._A[i_plus, i_minus][tuple(indices)] = 1 / 2 - - domain = domain.logical_domain - corner_blocks = {} - for c in domain.corners: - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - if (i, j) in corner_blocks: - corner_blocks[i, j] += [(b1, b2)] - else: - corner_blocks[i, j] = [(b1, b2)] - - for c in domain.corners: - if len(c) == 2: + # global index + jg = get_vertex_index_from_patch(patch2, coords2) + + # conformity constraint + Proj_vertex[jg, ig] = 1 / corr + + if patch1 == patch2: continue - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - interface = get_interface_from_corners(b1, b2, domain) - axis = None - if self._A[i, j] is None: - self._A[i, j] = allocate_interface_matrix( - corner_blocks[i, j], V0h.spaces[i], V0h.spaces[j]) - - if i != j and self._A[i, j]: - axis = self._A[i, j]._dim - index = get_row_col_index( - b1, b2, interface, axis, V0h.spaces[i], V0h.spaces[j]) - self._A[i, j][tuple(index)] = 1 / len(c) - - if hom_bc: - for bn in domain.boundary: - self.set_homogenous_bc(bn) - - self._matrix = self._A - self._sparse_matrix = self._matrix.tosparse() # self._sparse_matrix - if storage_fn: - print( - "[ConformingProjection_V0] storing operator sparse matrix in " + - storage_fn) - save_npz(storage_fn, self._sparse_matrix) + if p_moments == -1: + continue - def set_homogenous_bc(self, boundary, rhs=None): - domain = self.symbolic_domain - Vh = self.fem_domain - if domain.mapping: - domain = domain.logical_domain - if boundary.mapping: - boundary = boundary.logical_domain - - corners = domain.corners - i = get_patch_index_from_face(domain, boundary) - if rhs: - apply_essential_bc_stencil( - rhs[i], axis=boundary.axis, ext=boundary.ext, order=0) - for j in range(len(domain)): - if self._A[i, j] is None: - continue - apply_essential_bc_stencil( - self._A[i, j], axis=boundary.axis, ext=boundary.ext, order=0) + # 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 c in corners: - faces = [f for b in c.corners for f in b.boundaries] - if len(c) == 2: + 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 - if boundary in faces: - for b1 in c.corners: - i = get_patch_index_from_face(domain, b1.domain) - for b2 in c.corners: - j = get_patch_index_from_face(domain, b2.domain) - interface = get_interface_from_corners(b1, b2, domain) - axis = None - if i != j: - axis = self._A[i, j].dim - index = get_row_col_index( - b1, b2, interface, axis, Vh.spaces[i], Vh.spaces[j]) - self._A[i, j][tuple(index)] = 0. - - if i == j and rhs: - rhs[i][tuple(index[:2])] = 0. -# =============================================================================== + # 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) -class ConformingProjection_V1(FemLinearOperator): - """ - Conforming projection from global broken V1 space to conforming V1 global space + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] - proj.dot(v) returns the conforming projection of v, computed by solving linear system + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] - Parameters - ---------- - V1h: - The discrete space + pg = l2g.get_index(patch1, 0, multi_index_p) + Proj_vertex[pg, ig] += (1 - 1 / corr) * \ + gamma[p] * gamma[pd] - domain_h: - The discrete domain of the projector + # boundary conditions + corners = get_corners(domain, True) + if hom_bc: + for (bd, co) in corners.items(): + for patch1 in co: - hom_bc : - Apply homogenous boundary conditions if True + # local vertex coordinates in patch2 + coords1 = co[patch1] - backend_language: - The backend used to accelerate the code + # global index + ig = get_vertex_index_from_patch(patch1, coords1) - storage_fn: - filename to store/load the operator sparse matrix - """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) + for patch2 in co: - def __init__( - self, - V1h, - domain_h, - hom_bc=False, - backend_language='python', - storage_fn=None): + # local vertex coordinates in patch2 + coords2 = co[patch2] - FemLinearOperator.__init__(self, fem_domain=V1h) + # global index + jg = get_vertex_index_from_patch(patch2, coords2) - V1 = V1h.symbolic_space - domain = V1.domain - self.symbolic_domain = domain + # conformity constraint + Proj_vertex[jg, ig] = 0 - if storage_fn and os.path.exists(storage_fn): - print( - "[ConformingProjection_V1] loading operator sparse matrix from " + - storage_fn) - self._sparse_matrix = load_npz(storage_fn) + if patch1 == patch2: + continue - else: - # assemble the operator matrix - u, v = elements_of(V1, names='u, v') - expr = dot(u, v) - # - Interfaces = domain.interfaces # note: interfaces does not include the boundary - # this penalization is for an H1-conforming space - expr_I = dot(plus(u) - minus(u), plus(v) - minus(v)) - - a = BilinearForm((u, v), integral(domain, expr) + - integral(Interfaces, expr_I)) - # print('[[ forcing python backend for ConformingProjection_V1]] ') - # backend_language = 'python' - ah = discretize( - a, domain_h, [ - V1h, V1h], backend=PSYDAC_BACKENDS[backend_language]) - # - # # self._A = ah.assemble() - self._A = ah.forms[0]._matrix - # C1 = V1h.coeff_space - # self._A = BlockLinearOperator(C1, C1) - - for b1 in self._A.blocks: - for b2 in b1: - if b2 is None: + if p_moments == -1: continue - for b3 in b2.blocks: - for A in b3: - if A is None: - continue - A[:, :, :, :] = 0 - spaces = self._A.domain.spaces + # moment corrections from patch1 to patch2 + axis = 0 + d = 1 + multi_index_p = [None] * ndim - if isinstance(Interfaces, Interface): - Interfaces = (Interfaces, ) + d_moment_index = vertex_moment_indices( + d, coords2, patch2, p_moments) + axis_moment_index = vertex_moment_indices( + axis, coords2, patch2, p_moments) - indices = [slice(None, None)] * domain.dim + [0] * domain.dim + for pd in range(0, p_moments + 1): + multi_index_p[d] = d_moment_index[pd] - for i in range(len(self._A.blocks)): - self._A[i, i][0, 0][tuple(indices)] = 1 - self._A[i, i][1, 1][tuple(indices)] = 1 + for p in range(0, p_moments + 1): + multi_index_p[axis] = axis_moment_index[p] - # empty list if no interfaces ? - if Interfaces is not None: + pg = l2g.get_index(patch2, 0, multi_index_p) + Proj_vertex[pg, ig] = 0 - for I in Interfaces: + if p_moments == -1: + continue - i_minus = get_patch_index_from_face(domain, I.minus) - i_plus = get_patch_index_from_face(domain, I.plus) + # 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 - indices = [slice(None, None)] * \ - domain.dim + [0] * domain.dim + def get_mu_minus(j, coarse_space, fine_space, R): + mu_plus = np.zeros(fine_space.nbasis) + mu_minus = np.zeros(coarse_space.nbasis) - sp1 = spaces[i_minus] - sp2 = spaces[i_plus] + 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] - s11 = sp1.spaces[0].starts[I.axis] - e11 = sp1.spaces[0].ends[I.axis] - s12 = sp1.spaces[1].starts[I.axis] - e12 = sp1.spaces[1].ends[I.axis] + for m in range(coarse_space.nbasis): + for l in range(fine_space.nbasis): + mu_minus[m] += R[m, l] * mu_plus[l] - s21 = sp2.spaces[0].starts[I.axis] - e21 = sp2.spaces[0].ends[I.axis] - s22 = sp2.spaces[1].starts[I.axis] - e22 = sp2.spaces[1].ends[I.axis] + if j == 0: + mu_minus[m] -= R[m, 0] + else: + mu_minus[m] -= R[m, -1] - d11 = V1h.spaces[i_minus].spaces[0].degree[I.axis] - d12 = V1h.spaces[i_minus].spaces[1].degree[I.axis] + return mu_minus - d21 = V1h.spaces[i_plus].spaces[0].degree[I.axis] - d22 = V1h.spaces[i_plus].spaces[1].degree[I.axis] + # 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) - s_minus = [s11, s12] - e_minus = [e11, e12] + I_minus_ncells = Vh.spaces[k_minus].ncells + I_plus_ncells = Vh.spaces[k_plus].ncells - s_plus = [s21, s22] - e_plus = [e21, e22] + # 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 - d_minus = [d11, d12] - d_plus = [d21, d22] + 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( + i, 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] + + if not corner_indices.issuperset({ig}): + corner_indices.add(ig) + 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). - minus_ext = I.minus.ext - plus_ext = I.plus.ext + Parameters + ---------- + Vh : TensorFemSpace + Finite Element Space coming from the discrete de Rham sequence. - axis = I.axis - for k in range(domain.dim): - if k == I.axis: - continue + reg_orders : (int) + Regularity in each space direction -1 or 0. - if minus_ext == 1: - indices[axis] = e_minus[k] - else: - indices[axis] = s_minus[k] - self._A[i_minus, i_minus][k, k][tuple(indices)] = 1 / 2 + p_moments : (int) + Number of polynomial moments to be preserved. - if plus_ext == 1: - indices[axis] = e_plus[k] - else: - indices[axis] = s_plus[k] + hom_bc : (bool) + Tangential homogeneous boundary conditions. - self._A[i_plus, i_plus][k, k][tuple(indices)] = 1 / 2 + Returns + ------- + cP : scipy.sparse.csr_array + Conforming projection as a sparse matrix. + """ - if plus_ext == minus_ext: - if minus_ext == 1: - indices[axis] = d_minus[k] - else: - indices[axis] = s_minus[k] + dim_tot = Vh.nbasis + + # fully discontinuous space + if reg_orders < 0: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # should be in the V^0 spaces + 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 - self._A[i_minus, i_plus][k, k][tuple( - indices)] = 1 / 2 * I.direction + 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 - if plus_ext == 1: - indices[axis] = d_plus[k] - else: - indices[axis] = s_plus[k] + # logical directions along the interface + d_fine = 1 - fine_axis + d_coarse = 1 - coarse_axis - self._A[i_plus, i_minus][k, k][tuple( - indices)] = 1 / 2 * I.direction + space_fine = Vh.spaces[k_fine] + space_coarse = Vh.spaces[k_coarse] - else: - if minus_ext == 1: - indices[axis] = d_minus[k] - else: - indices[axis] = s_minus[k] + 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) - if plus_ext == 1: - indices[domain.dim + axis] = d_plus[k] - else: - indices[domain.dim + axis] = -d_plus[k] + # 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) - self._A[i_minus, i_plus][k, k][tuple( - indices)] = 1 / 2 * I.direction + Proj_edge[jg, jg] = 1 / 2 - if plus_ext == 1: - indices[axis] = d_plus[k] - else: - indices[axis] = s_plus[k] + 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] - if minus_ext == 1: - indices[domain.dim + axis] = d_minus[k] - else: - indices[domain.dim + axis] = -d_minus[k] + 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] - self._A[i_plus, i_minus][k, k][tuple( - indices)] = 1 / 2 * I.direction + 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] - if hom_bc: - for bn in domain.boundary: - self.set_homogenous_bc(bn) + # 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) - self._matrix = self._A - self._sparse_matrix = self._matrix.tosparse() + 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] - if storage_fn: - print( - "[ConformingProjection_V1] storing operator sparse matrix in " + - storage_fn) - save_npz(storage_fn, self._sparse_matrix) + 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] - def set_homogenous_bc(self, boundary): - domain = self.symbolic_domain - Vh = self.fem_domain + 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] - i = get_patch_index_from_face(domain, boundary) - axis = boundary.axis - ext = boundary.ext - for j in range(len(domain)): - if self._A[i, j] is None: - continue - apply_essential_bc_stencil( - self._A[i, j][1 - axis, 1 - axis], axis=axis, ext=ext, order=0) + 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 -# =============================================================================== -def get_K0_and_K0_inv(V0h, uniform_patches=False): + 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 + + + +class ConformingProjection_V0(FemLinearOperator): """ - Compute the change of basis matrices K0 and K0^{-1} in V0h. + Conforming projection from global broken V0 space to conforming global V0 space + Defined by averaging of interface dofs - With - K0_ij = sigma^0_i(B_j) = B_jx(n_ix) * B_jy(n_iy) - where sigma_i is the geometric (interpolation) dof - and B_j is the tensor-product B-spline + Parameters + ---------- + V0h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + + storage_fn: + filename to store/load the operator sparse matrix """ - if uniform_patches: - print(' [[WARNING -- hack in get_K0_and_K0_inv: using copies of 1st-patch matrices in every patch ]] ') - - V0 = V0h.symbolic_space # VOh is FemSpace - domain = V0.domain - K0_blocks = [] - K0_inv_blocks = [] - for k, D in enumerate(domain.interior): - if uniform_patches and k > 0: - K0_k = K0_blocks[0].copy() - K0_inv_k = K0_inv_blocks[0].copy() + # todo (MCP, 16.03.2021): + # - avoid discretizing a bilinear form + # - allow case without interfaces (single or multipatch) + + def __init__( + self, + V0h, + p_moments=-1, + hom_bc=False, + storage_fn=None): + + FemLinearOperator.__init__(self, fem_domain=V0h) + + V0 = V0h.symbolic_space + domain = V0.domain + self.symbolic_domain = domain + + if storage_fn and os.path.exists(storage_fn): + print("[ConformingProjection_V0] loading operator sparse matrix from " + storage_fn) + self._sparse_matrix = load_npz(storage_fn) else: - V0_k = V0h.spaces[k] # fem space on patch k: (TensorFemSpace) - K0_k_factors = [None, None] - for d in [0, 1]: - # 1d fem space alond dim d (SplineSpace) - V0_kd = V0_k.spaces[d] - K0_k_factors[d] = collocation_matrix( - knots=V0_kd.knots, - degree=V0_kd.degree, - periodic=V0_kd.periodic, - normalization=V0_kd.basis, - xgrid=V0_kd.greville - ) - K0_k = kron(*K0_k_factors) - K0_k.eliminate_zeros() - K0_inv_k = inv(K0_k.tocsc()) - K0_inv_k.eliminate_zeros() - - K0_blocks.append(K0_k) - K0_inv_blocks.append(K0_inv_k) - K0 = block_diag(K0_blocks) - K0_inv = block_diag(K0_inv_blocks) - return K0, K0_inv + + self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + if storage_fn: + print("[ConformingProjection_V0] storing operator sparse matrix in " + storage_fn) + save_npz(storage_fn, self._sparse_matrix) # =============================================================================== -def get_K1_and_K1_inv(V1h, uniform_patches=False): + + +class ConformingProjection_V1(FemLinearOperator): """ - Compute the change of basis matrices K1 and K1^{-1} in Hcurl space V1h. - - With - K1_ij = sigma^1_i(B_j) = int_{e_ix}(M_jx) * B_jy(n_iy) - if i = horizontal edge [e_ix, n_iy] and j = (M_jx o B_jy) x-oriented MoB spline - or - = B_jx(n_ix) * int_{e_iy}(M_jy) - if i = vertical edge [n_ix, e_iy] and j = (B_jx o M_jy) y-oriented BoM spline - (above, 'o' denotes tensor-product for functions) + Conforming projection from global broken V1 space to conforming V1 global space + + proj.dot(v) returns the conforming projection of v, computed by solving linear system + + Parameters + ---------- + V1h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + + storage_fn: + filename to store/load the operator sparse matrix """ - if uniform_patches: - print(' [[WARNING -- hack in get_K1_and_K1_inv: using copies of 1st-patch matrices in every patch ]] ') - - V1 = V1h.symbolic_space # V1h is FemSpace - domain = V1.domain - K1_blocks = [] - K1_inv_blocks = [] - for k, D in enumerate(domain.interior): - if uniform_patches and k > 0: - K1_k = K1_blocks[0].copy() - K1_inv_k = K1_inv_blocks[0].copy() + # todo (MCP, 16.03.2021): + # - avoid discretizing a bilinear form + # - allow case without interfaces (single or multipatch) + + def __init__( + self, + V1h, + p_moments=-1, + hom_bc=False, + storage_fn=None): + + FemLinearOperator.__init__(self, fem_domain=V1h) + + V1 = V1h.symbolic_space + domain = V1.domain + self.symbolic_domain = domain + + if storage_fn and os.path.exists(storage_fn): + print("[ConformingProjection_V1] loading operator sparse matrix from " + storage_fn) + self._sparse_matrix = load_npz(storage_fn) else: - # fem space on patch k: - V1_k = V1h.spaces[k] - K1_k_blocks = [] - for c in [0, 1]: # dim of component - # fem space for comp. dc (TensorFemSpace) - V1_kc = V1_k.spaces[c] - K1_kc_factors = [None, None] - for d in [0, 1]: # dim of variable - # 1d fem space for comp c alond dim d (SplineSpace) - V1_kcd = V1_kc.spaces[d] - if c == d: - K1_kc_factors[d] = histopolation_matrix( - knots=V1_kcd.knots, - degree=V1_kcd.degree, - periodic=V1_kcd.periodic, - normalization=V1_kcd.basis, - xgrid=V1_kcd.ext_greville - ) - else: - K1_kc_factors[d] = collocation_matrix( - knots=V1_kcd.knots, - degree=V1_kcd.degree, - periodic=V1_kcd.periodic, - normalization=V1_kcd.basis, - xgrid=V1_kcd.greville - ) - K1_kc = kron(*K1_kc_factors) - K1_kc.eliminate_zeros() - K1_k_blocks.append(K1_kc) - K1_k = block_diag(K1_k_blocks) - K1_k.eliminate_zeros() - K1_inv_k = inv(K1_k.tocsc()) - K1_inv_k.eliminate_zeros() - - K1_blocks.append(K1_k) - K1_inv_blocks.append(K1_inv_k) - - K1 = block_diag(K1_blocks) - K1_inv = block_diag(K1_inv_blocks) - return K1, K1_inv - - -# #=============================================================================== -# def get_M_and_M_inv(Vh, subdomains_h, is_scalar, backend_language='python'): -# """ -# compute the mass matrix M and M^{-1} in multipatch space Vh -# DOES NOT WORK -- SHOULD WE HAVE THE POSSIBILITY OF DOING THAT ? -# """ -# from pprint import pprint -# -# V = Vh.symbolic_space # VOh is FemSpace -# domain = V.domain -# M_blocks = [] -# M_inv_blocks = [] -# -# # print('type(domain_h) = ', type(domain_h)) -# # -# # print('type(domain_h._patches) = ', type(domain_h._patches)) -# # print('len(domain_h._patches) = ', len(domain_h._patches)) -# # -# # mappings = domain_h.mappings -# # print('type(mappings) = ', type(mappings)) -# # print('len(mappings) = ', len(mappings)) -# # -# # mappings_list = list(mappings.values()) -# # print('len(mappings_list) = ', len(mappings_list)) -# # -# # print('type(mappings_list[0]) = ', type(mappings_list[0])) -# -# for k, Dh_k in enumerate(subdomains_h): -# -# print('k = ', k) -# print('type(Dh_k) = ', type(Dh_k)) -# # print('Dh = ', Dh) -# D_k = domain.interior[k] -# -# # exit() -# -# # for k, D in enumerate(domain.interior): -# -# V_k = V.spaces[k] -# Vh_k = Vh.spaces[k] -# -# # print(type(domain_h)) -# # -# # pprint(dir(domain_h)) -# # -# # -# # print(len(domain_h._patches)) -# # exit() -# # Dh_k = domain_h.spaces[k] # fem space on patch k: (TensorFemSpace) -# u, v = elements_of(V_k, names='u, v') -# if is_scalar: -# expr = u*v -# else: -# expr = dot(u,v) -# a_k = BilinearForm((u,v), integral(D_k, expr)) -# a_kh = discretize(a_k, Dh_k, [Vh_k, Vh_k], backend=PSYDAC_BACKENDS[backend_language]) # 'pyccel-gcc']) -# -# M_k = a_kh.assemble().toarray() -# M_k.eliminate_zeros() -# M_inv_k = inv(M_k.tocsc()) -# M_inv_k.eliminate_zeros() -# -# M_blocks.append(M_k) -# M_inv_blocks.append(M_inv_k) -# M = block_diag(M_blocks) -# M_inv = block_diag(M_inv_blocks) -# return M, M_inv + + self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + if storage_fn: + print("[ConformingProjection_V1] storing operator sparse matrix in " + storage_fn) + save_npz(storage_fn, self._sparse_matrix) + # =============================================================================== class HodgeOperator(FemLinearOperator): @@ -1149,30 +1512,6 @@ def __init__(self, V1h, V2h): def transpose(self, conjugate=False): return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) - -# ============================================================================== - -# 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 - """ - 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]) - 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) - # ============================================================================== diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index 351511d5e..9acd40c74 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -10,11 +10,15 @@ 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.feec.multipatch.utilities import time_count from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField from psydac.fem.plotting_utilities import get_plotting_grid, get_grid_quad_weights, get_grid_vals +from scipy.sparse import kron, block_diag +from psydac.core.bsplines import collocation_matrix, histopolation_matrix +from psydac.linalg.solvers import inverse + # commuting projections on the physical domain (should probably be in the # interface) @@ -83,6 +87,142 @@ def get_kind(space='V*'): raise ValueError(space) return kind +# =============================================================================== +def get_K0_and_K0_inv(V0h, uniform_patches=False): + """ + Compute the change of basis matrices K0 and K0^{-1} in V0h. + + With + K0_ij = sigma^0_i(B_j) = B_jx(n_ix) * B_jy(n_iy) + where sigma_i is the geometric (interpolation) dof + and B_j is the tensor-product B-spline + """ + if uniform_patches: + print(' [[WARNING -- hack in get_K0_and_K0_inv: using copies of 1st-patch matrices in every patch ]] ') + + V0 = V0h.symbolic_space # VOh is FemSpace + domain = V0.domain + K0_blocks = [] + K0_inv_blocks = [] + for k, D in enumerate(domain.interior): + if uniform_patches and k > 0: + K0_k = K0_blocks[0].copy() + K0_inv_k = K0_inv_blocks[0].copy() + + else: + V0_k = V0h.spaces[k] # fem space on patch k: (TensorFemSpace) + K0_k_factors = [None, None] + for d in [0, 1]: + # 1d fem space alond dim d (SplineSpace) + V0_kd = V0_k.spaces[d] + K0_k_factors[d] = collocation_matrix( + knots=V0_kd.knots, + degree=V0_kd.degree, + periodic=V0_kd.periodic, + normalization=V0_kd.basis, + xgrid=V0_kd.greville + ) + K0_k = kron(*K0_k_factors) + K0_k.eliminate_zeros() + K0_inv_k = inv(K0_k.tocsc()) + K0_inv_k.eliminate_zeros() + + K0_blocks.append(K0_k) + K0_inv_blocks.append(K0_inv_k) + K0 = block_diag(K0_blocks) + K0_inv = block_diag(K0_inv_blocks) + return K0, K0_inv + + +# =============================================================================== +def get_K1_and_K1_inv(V1h, uniform_patches=False): + """ + Compute the change of basis matrices K1 and K1^{-1} in Hcurl space V1h. + + With + K1_ij = sigma^1_i(B_j) = int_{e_ix}(M_jx) * B_jy(n_iy) + if i = horizontal edge [e_ix, n_iy] and j = (M_jx o B_jy) x-oriented MoB spline + or + = B_jx(n_ix) * int_{e_iy}(M_jy) + if i = vertical edge [n_ix, e_iy] and j = (B_jx o M_jy) y-oriented BoM spline + (above, 'o' denotes tensor-product for functions) + """ + if uniform_patches: + print(' [[WARNING -- hack in get_K1_and_K1_inv: using copies of 1st-patch matrices in every patch ]] ') + + V1 = V1h.symbolic_space # V1h is FemSpace + domain = V1.domain + K1_blocks = [] + K1_inv_blocks = [] + for k, D in enumerate(domain.interior): + if uniform_patches and k > 0: + K1_k = K1_blocks[0].copy() + K1_inv_k = K1_inv_blocks[0].copy() + + else: + # fem space on patch k: + V1_k = V1h.spaces[k] + K1_k_blocks = [] + for c in [0, 1]: # dim of component + # fem space for comp. dc (TensorFemSpace) + V1_kc = V1_k.spaces[c] + K1_kc_factors = [None, None] + for d in [0, 1]: # dim of variable + # 1d fem space for comp c alond dim d (SplineSpace) + V1_kcd = V1_kc.spaces[d] + if c == d: + K1_kc_factors[d] = histopolation_matrix( + knots=V1_kcd.knots, + degree=V1_kcd.degree, + periodic=V1_kcd.periodic, + normalization=V1_kcd.basis, + xgrid=V1_kcd.ext_greville + ) + else: + K1_kc_factors[d] = collocation_matrix( + knots=V1_kcd.knots, + degree=V1_kcd.degree, + periodic=V1_kcd.periodic, + normalization=V1_kcd.basis, + xgrid=V1_kcd.greville + ) + K1_kc = kron(*K1_kc_factors) + K1_kc.eliminate_zeros() + K1_k_blocks.append(K1_kc) + K1_k = block_diag(K1_k_blocks) + K1_k.eliminate_zeros() + K1_inv_k = inv(K1_k.tocsc()) + K1_inv_k.eliminate_zeros() + + K1_blocks.append(K1_k) + K1_inv_blocks.append(K1_inv_k) + + K1 = block_diag(K1_blocks) + K1_inv = block_diag(K1_inv_blocks) + return K1, K1_inv + +# =============================================================================== + + +def ortho_proj_Hcurl(EE, V1h, domain_h, M1, backend_language='python'): + """ + return orthogonal projection of E on V1h, given M1 the mass matrix + """ + 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]) + 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 DiagGrid(): From 108e53df9528614489a594248b49abe74d331061 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 26 Jun 2025 10:38:52 +0200 Subject: [PATCH 003/102] move content of feec/multipatch/api to api/feec and api/discretization --- psydac/api/discretization.py | 23 ++- psydac/api/feec.py | 268 +++++++++++++++++++++++++++++- psydac/feec/multipatch/api.py | 301 ---------------------------------- 3 files changed, 289 insertions(+), 303 deletions(-) delete mode 100644 psydac/feec/multipatch/api.py diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 69dc89679..563a8bc3e 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -29,7 +29,7 @@ from psydac.api.fem import DiscreteLinearForm from psydac.api.fem import DiscreteFunctional from psydac.api.fem import DiscreteSumForm -from psydac.api.feec import DiscreteDerham +from psydac.api.feec import DiscreteDerham, DiscreteDerhamMultipatch from psydac.api.glt import DiscreteGltExpr from psydac.api.expr import DiscreteExpr from psydac.api.equation import DiscreteEquation @@ -47,6 +47,7 @@ __all__ = ( 'discretize', 'discretize_derham', + 'discretize_derham_multipatch', 'reduce_space_degrees', 'discretize_space', 'discretize_domain' @@ -194,6 +195,23 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): return DiscreteDerham(mapping, *spaces) +#============================================================================== +def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): + + ldim = derham.shape + mapping = derham.spaces[0].domain.mapping + + bases = ['B'] + ldim * ['M'] + spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ + for V, basis in zip(derham.spaces, bases)] + + return DiscreteDerhamMultipatch( + mapping = mapping, + domain_h = domain_h, + spaces = spaces, + sequence = [V.kind.name for V in derham.spaces] + ) + #============================================================================== def reduce_space_degrees(V, Vh, *, basis='B', sequence='DR'): """ @@ -610,6 +628,9 @@ def discretize(a, *args, **kwargs): elif isinstance(a, Derham): return discretize_derham(a, *args, **kwargs) + elif isinstance(expr, Derham) and expr.V0.is_broken: + return discretize_derham_multipatch(expr, *args, **kwargs) + elif isinstance(a, Domain): return discretize_domain(a, *args, **kwargs) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index cfec097eb..459cc16de 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,4 +1,7 @@ +import os + from sympde.topology.mapping import Mapping +from sympde.topology.space import ScalarFunction from psydac.api.basic import BasicDiscrete from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D @@ -12,7 +15,22 @@ from psydac.fem.basic import FemSpace from psydac.fem.vector import VectorFemSpace -__all__ = ('DiscreteDerham',) +from psydac.feec.multipatch.operators import BrokenGradient_2D +from psydac.feec.multipatch.operators import BrokenScalarCurl_2D +from psydac.feec.multipatch.operators import Multipatch_Projector_H1 +from psydac.feec.multipatch.operators import Multipatch_Projector_Hcurl +from psydac.feec.multipatch.operators import Multipatch_Projector_L2 +from psydac.feec.multipatch.operators import ConformingProjection_V0 +from psydac.feec.multipatch.operators import ConformingProjection_V1 +from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator + +from sympde.expr.expr import LinearForm, integral +from sympde.calculus import dot +from sympde.topology import element_of + +from psydac.api.settings import PSYDAC_BACKENDS + +__all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) #============================================================================== class DiscreteDerham(BasicDiscrete): @@ -264,3 +282,251 @@ def projectors(self, *, kind='global', nquads=None): else : return P0, P1, P2, P3 + +#============================================================================== +class DiscreteDerhamMultipatch(DiscreteDerham): + """ Represents the discrete De Rham sequence for multipatch domains. + It only works when the number of patches>1 + + Parameters + ---------- + mapping: + The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch + + domain_h: + The discrete domain + + spaces: + The discrete spaces that are contained in the De Rham sequence + + sequence: + The space kind of each space in the De Rham sequence + """ + + def __init__(self, *, mapping, domain_h, spaces, sequence=None): + + + dim = len(spaces) - 1 + self._dim = dim + self._mapping = mapping + self._spaces = tuple(spaces) + self._domain_h = domain_h + + if sequence: + if len(sequence) != dim + 1: + raise ValueError('Expected len(sequence) = {}, got {} instead'. + format(dim + 1, len(sequence))) + + if dim == 1: + self._sequence = ('h1', 'l2') + raise NotImplementedError('1D FEEC multipatch non available yet') + + elif dim == 2: + if sequence is None: + raise ValueError('Sequence must be specified in 2D case') + + elif tuple(sequence) == ('h1', 'hcurl', 'l2'): + self._sequence = tuple(sequence) + self._broken_diff_ops = ( + BrokenGradient_2D(self.V0, self.V1), + BrokenScalarCurl_2D(self.V1, self.V2), # None, + ) + + elif tuple(sequence) == ('h1', 'hdiv', 'l2'): + self._sequence = tuple(sequence) + raise NotImplementedError('2D sequence with H-div not available yet') + + else: + raise ValueError('2D sequence not understood') + + elif dim == 3: + self._sequence = ('h1', 'hcurl', 'hdiv', 'l2') + raise NotImplementedError('3D FEEC multipatch non available yet') + + else: + raise ValueError('Dimension {} is not available'.format(dim)) + + #-------------------------------------------------------------------------- + @property + def sequence(self): + return self._sequence + + # ... + @property + def broken_derivatives_as_operators(self): + return self._broken_diff_ops + + # ... + @property + def broken_derivatives_as_matrices(self): + return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) + + #-------------------------------------------------------------------------- + def projectors(self, *, kind='global', nquads=None): + """ + This method returns the patch-wise commuting projectors on the broken multi-patch space + + Parameters + ---------- + kind: + The projectors kind, can be global or local + + nquads: + The number of quadrature points. + + Returns + ------- + P0: + Patch wise H1 projector + + P1: + Patch wise Hcurl projector + + P2: + Patch wise L2 projector + + Notes + ----- + - when applied to smooth functions they return conforming fields + - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids + - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently + """ + if not (kind == 'global'): + raise NotImplementedError('only global projectors are available') + + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + P0 = Multipatch_Projector_H1(self.V0) + + if self.sequence[1] == 'hcurl': + P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) + else: + P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) + raise NotImplementedError('2D sequence with H-div not available yet') + + P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) + return P0, P1, P2 + + elif self.dim == 3: + raise NotImplementedError("3D projectors are not available") + + #-------------------------------------------------------------------------- + def conforming_projection(self, space, hom_bc=False, backend_language="python", load_dir=None): + """ + return the conforming projectors of the broken multi-patch space + + Parameters + ---------- + space : + The space of the projector + + hom_bc: + Apply homogenous boundary conditions if True + + backend_language: + The backend used to accelerate the code + + load_dir: + Filename for storage in sparse matrix format + + Returns + ------- + Cp: + The conforming projector + + """ + if hom_bc is None: + raise ValueError('please provide a value for "hom_bc" argument') + + if isinstance(load_dir, str): + if not os.path.exists(load_dir): + os.makedirs(load_dir) + if space == 'V0': + P_name = 'cP0' + elif space == 'V1': + P_name = 'cP1' + elif space == 'V2': + P_name = 'cP2' + else: + raise ValueError(space) + + if hom_bc: + storage_fn = load_dir + '/{}_hom_m.npz'.format(P_name) + else: + storage_fn = load_dir + '/{}_m.npz'.format(P_name) + else: + storage_fn = None + + cP = None + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + if space == 'V0': + cP = ConformingProjection_V0(self.V0, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + elif space == 'V1': + if self.sequence[1] == 'hcurl': + cP = ConformingProjection_V1(self.V1, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + else: + raise NotImplementedError('2D sequence with H-div not available yet') + + elif space == 'V2': + cP = IdLinearOperator(self.V2) # no storage needed! + else: + raise ValueError('Invalid value for "space" argument: {}'.format(space)) + + elif self.dim == 3: + raise NotImplementedError("3D projectors are not available") + + return cP + + def get_dual_dofs(self, space, f, backend_language="python", return_format='stencil_array'): + """ + return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(V^k)) of a given function f, as a stencil array or numpy array + + Parameters + ---------- + space : + The space of the dual dofs + + f : + The function used for evaluation + + backend_language: + The backend used to accelerate the code + + return_format: + The format of the dofs, can be 'stencil_array' or 'numpy_array' + + Returns + ------- + tilde_f: + The dual dofs + """ + if space == 'V0': + Vh = self.V0 + elif space == 'V1': + Vh = self.V1 + elif space == 'V2': + Vh = self.V2 + else: + raise NotImplementedError("The space of kind {} is not available".format(space)) + + V = Vh.symbolic_space + v = element_of(V, name='v') + + if isinstance(v, ScalarFunction): + expr = f*v + else: + expr = dot(f,v) + + l = LinearForm(v, integral( V.domain, expr)) + lh = discretize(l, self._domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) + tilde_f = lh.assemble() + + if return_format == 'numpy_array': + return tilde_f.toarray() + else: + return tilde_f diff --git a/psydac/feec/multipatch/api.py b/psydac/feec/multipatch/api.py deleted file mode 100644 index 566815421..000000000 --- a/psydac/feec/multipatch/api.py +++ /dev/null @@ -1,301 +0,0 @@ -# coding: utf-8 -import os - -from sympde.topology import Derham -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, integral - -from psydac.api.settings import PSYDAC_BACKENDS - -from psydac.api.discretization import discretize as discretize_single_patch -from psydac.api.discretization import discretize_space -from psydac.api.discretization import DiscreteDerham -from psydac.feec.multipatch.operators import BrokenGradient_2D -from psydac.feec.multipatch.operators import BrokenScalarCurl_2D -from psydac.feec.multipatch.operators import Multipatch_Projector_H1 -from psydac.feec.multipatch.operators import Multipatch_Projector_Hcurl -from psydac.feec.multipatch.operators import Multipatch_Projector_L2 -from psydac.feec.multipatch.operators import ConformingProjection_V0 -from psydac.feec.multipatch.operators import ConformingProjection_V1 -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator - - -__all__ = ('DiscreteDerhamMultipatch', 'discretize', 'discretize_derham_multipatch') - -#============================================================================== -class DiscreteDerhamMultipatch(DiscreteDerham): - """ Represents the discrete De Rham sequence for multipatch domains. - It only works when the number of patches>1 - - Parameters - ---------- - mapping: - The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch - - domain_h: - The discrete domain - - spaces: - The discrete spaces that are contained in the De Rham sequence - - sequence: - The space kind of each space in the De Rham sequence - """ - - def __init__(self, *, mapping, domain_h, spaces, sequence=None): - - - dim = len(spaces) - 1 - self._dim = dim - self._mapping = mapping - self._spaces = tuple(spaces) - self._domain_h = domain_h - - if sequence: - if len(sequence) != dim + 1: - raise ValueError('Expected len(sequence) = {}, got {} instead'. - format(dim + 1, len(sequence))) - - if dim == 1: - self._sequence = ('h1', 'l2') - raise NotImplementedError('1D FEEC multipatch non available yet') - - elif dim == 2: - if sequence is None: - raise ValueError('Sequence must be specified in 2D case') - - elif tuple(sequence) == ('h1', 'hcurl', 'l2'): - self._sequence = tuple(sequence) - self._broken_diff_ops = ( - BrokenGradient_2D(self.V0, self.V1), - BrokenScalarCurl_2D(self.V1, self.V2), # None, - ) - - elif tuple(sequence) == ('h1', 'hdiv', 'l2'): - self._sequence = tuple(sequence) - raise NotImplementedError('2D sequence with H-div not available yet') - - else: - raise ValueError('2D sequence not understood') - - elif dim == 3: - self._sequence = ('h1', 'hcurl', 'hdiv', 'l2') - raise NotImplementedError('3D FEEC multipatch non available yet') - - else: - raise ValueError('Dimension {} is not available'.format(dim)) - - #-------------------------------------------------------------------------- - @property - def sequence(self): - return self._sequence - - # ... - @property - def broken_derivatives_as_operators(self): - return self._broken_diff_ops - - # ... - @property - def broken_derivatives_as_matrices(self): - return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) - - #-------------------------------------------------------------------------- - def projectors(self, *, kind='global', nquads=None): - """ - This method returns the patch-wise commuting projectors on the broken multi-patch space - - Parameters - ---------- - kind: - The projectors kind, can be global or local - - nquads: - The number of quadrature points. - - Returns - ------- - P0: - Patch wise H1 projector - - P1: - Patch wise Hcurl projector - - P2: - Patch wise L2 projector - - Notes - ----- - - when applied to smooth functions they return conforming fields - - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids - - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently - """ - if not (kind == 'global'): - raise NotImplementedError('only global projectors are available') - - if self.dim == 1: - raise NotImplementedError("1D projectors are not available") - - elif self.dim == 2: - P0 = Multipatch_Projector_H1(self.V0) - - if self.sequence[1] == 'hcurl': - P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) - else: - P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) - raise NotImplementedError('2D sequence with H-div not available yet') - - P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) - return P0, P1, P2 - - elif self.dim == 3: - raise NotImplementedError("3D projectors are not available") - - #-------------------------------------------------------------------------- - def conforming_projection(self, space, hom_bc=False, backend_language="python", load_dir=None): - """ - return the conforming projectors of the broken multi-patch space - - Parameters - ---------- - space : - The space of the projector - - hom_bc: - Apply homogenous boundary conditions if True - - backend_language: - The backend used to accelerate the code - - load_dir: - Filename for storage in sparse matrix format - - Returns - ------- - Cp: - The conforming projector - - """ - if hom_bc is None: - raise ValueError('please provide a value for "hom_bc" argument') - - if isinstance(load_dir, str): - if not os.path.exists(load_dir): - os.makedirs(load_dir) - if space == 'V0': - P_name = 'cP0' - elif space == 'V1': - P_name = 'cP1' - elif space == 'V2': - P_name = 'cP2' - else: - raise ValueError(space) - - if hom_bc: - storage_fn = load_dir + '/{}_hom_m.npz'.format(P_name) - else: - storage_fn = load_dir + '/{}_m.npz'.format(P_name) - else: - storage_fn = None - - cP = None - if self.dim == 1: - raise NotImplementedError("1D projectors are not available") - - elif self.dim == 2: - if space == 'V0': - cP = ConformingProjection_V0(self.V0, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) - elif space == 'V1': - if self.sequence[1] == 'hcurl': - cP = ConformingProjection_V1(self.V1, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) - else: - raise NotImplementedError('2D sequence with H-div not available yet') - - elif space == 'V2': - cP = IdLinearOperator(self.V2) # no storage needed! - else: - raise ValueError('Invalid value for "space" argument: {}'.format(space)) - - elif self.dim == 3: - raise NotImplementedError("3D projectors are not available") - - return cP - - def get_dual_dofs(self, space, f, backend_language="python", return_format='stencil_array'): - """ - return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(V^k)) of a given function f, as a stencil array or numpy array - - Parameters - ---------- - space : - The space of the dual dofs - - f : - The function used for evaluation - - backend_language: - The backend used to accelerate the code - - return_format: - The format of the dofs, can be 'stencil_array' or 'numpy_array' - - Returns - ------- - tilde_f: - The dual dofs - """ - if space == 'V0': - Vh = self.V0 - elif space == 'V1': - Vh = self.V1 - elif space == 'V2': - Vh = self.V2 - else: - raise NotImplementedError("The space of kind {} is not available".format(space)) - - V = Vh.symbolic_space - v = element_of(V, name='v') - - if isinstance(v, ScalarFunction): - expr = f*v - else: - expr = dot(f,v) - - l = LinearForm(v, integral( V.domain, expr)) - lh = discretize(l, self._domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) - tilde_f = lh.assemble() - - if return_format == 'numpy_array': - return tilde_f.toarray() - else: - return tilde_f - -#============================================================================== -def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): - - ldim = derham.shape - mapping = derham.spaces[0].domain.mapping - - bases = ['B'] + ldim * ['M'] - spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ - for V, basis in zip(derham.spaces, bases)] - - return DiscreteDerhamMultipatch( - mapping = mapping, - domain_h = domain_h, - spaces = spaces, - sequence = [V.kind.name for V in derham.spaces] - ) - -#============================================================================== -def discretize(expr, *args, **kwargs): - - if isinstance(expr, Derham) and expr.V0.is_broken: - return discretize_derham_multipatch(expr, *args, **kwargs) - - else: - return discretize_single_patch(expr, *args, **kwargs) From 7ee1a5c0dc18eaaf4e9804aa2a25e4738dd548da Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 26 Jun 2025 13:33:37 +0200 Subject: [PATCH 004/102] restructure multipatch/operators. Make sure that DiscreteDerhamMultipatch has all properties of DiscreteDerham. --- psydac/api/discretization.py | 6 +- psydac/api/feec.py | 106 +- psydac/feec/multipatch/derivatives.py | 75 ++ psydac/feec/multipatch/operators.py | 1404 ---------------------- psydac/feec/multipatch/projectors.py | 1344 +++++++++++++++++++++ psydac/feec/multipatch/utils_conga_2d.py | 2 +- psydac/fem/projectors.py | 64 +- 7 files changed, 1517 insertions(+), 1484 deletions(-) create mode 100644 psydac/feec/multipatch/derivatives.py create mode 100644 psydac/feec/multipatch/projectors.py diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 563a8bc3e..b431eda08 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -625,11 +625,11 @@ def discretize(a, *args, **kwargs): elif isinstance(a, BasicFunctionSpace): return discretize_space(a, *args, **kwargs) - elif isinstance(a, Derham): + elif isinstance(a, Derham)and not a.V0.is_broken: return discretize_derham(a, *args, **kwargs) - elif isinstance(expr, Derham) and expr.V0.is_broken: - return discretize_derham_multipatch(expr, *args, **kwargs) + elif isinstance(a, Derham) and a.V0.is_broken: + return discretize_derham_multipatch(a, *args, **kwargs) elif isinstance(a, Domain): return discretize_domain(a, *args, **kwargs) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 459cc16de..85bbc6d16 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,7 +1,6 @@ import os from sympde.topology.mapping import Mapping -from sympde.topology.space import ScalarFunction from psydac.api.basic import BasicDiscrete from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D @@ -15,20 +14,15 @@ from psydac.fem.basic import FemSpace from psydac.fem.vector import VectorFemSpace -from psydac.feec.multipatch.operators import BrokenGradient_2D -from psydac.feec.multipatch.operators import BrokenScalarCurl_2D -from psydac.feec.multipatch.operators import Multipatch_Projector_H1 -from psydac.feec.multipatch.operators import Multipatch_Projector_Hcurl -from psydac.feec.multipatch.operators import Multipatch_Projector_L2 -from psydac.feec.multipatch.operators import ConformingProjection_V0 -from psydac.feec.multipatch.operators import ConformingProjection_V1 +from psydac.feec.multipatch.derivatives import BrokenGradient_2D +from psydac.feec.multipatch.derivatives import BrokenScalarCurl_2D +from psydac.feec.multipatch.projectors import Multipatch_Projector_H1 +from psydac.feec.multipatch.projectors import Multipatch_Projector_Hcurl +from psydac.feec.multipatch.projectors import Multipatch_Projector_L2 +from psydac.feec.multipatch.projectors import ConformingProjection_V0 +from psydac.feec.multipatch.projectors import ConformingProjection_V1 from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator -from sympde.expr.expr import LinearForm, integral -from sympde.calculus import dot -from sympde.topology import element_of - -from psydac.api.settings import PSYDAC_BACKENDS __all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) @@ -305,11 +299,10 @@ class DiscreteDerhamMultipatch(DiscreteDerham): def __init__(self, *, mapping, domain_h, spaces, sequence=None): - - dim = len(spaces) - 1 + dim = len(spaces) - 1 + self._spaces = tuple(spaces) self._dim = dim self._mapping = mapping - self._spaces = tuple(spaces) self._domain_h = domain_h if sequence: @@ -351,14 +344,30 @@ def __init__(self, *, mapping, domain_h, spaces, sequence=None): def sequence(self): return self._sequence - # ... @property - def broken_derivatives_as_operators(self): + def H1vec(self): + raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') + + @property + def spaces(self): + """Spaces of the proper de Rham sequence (excluding Hvec).""" + return self._spaces + + @property + def mapping(self): + """The mapping from the logical space to the physical space.""" + return self._mapping + + @property + def callable_mapping(self): + raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') + + @property + def derivatives(self): return self._broken_diff_ops - # ... @property - def broken_derivatives_as_matrices(self): + def derivatives_as_matrices(self): return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) #-------------------------------------------------------------------------- @@ -412,8 +421,8 @@ def projectors(self, *, kind='global', nquads=None): elif self.dim == 3: raise NotImplementedError("3D projectors are not available") - #-------------------------------------------------------------------------- - def conforming_projection(self, space, hom_bc=False, backend_language="python", load_dir=None): + #-------------------------------------------------------------------------- + def conforming_projection(self, space, p_moments=-1, hom_bc=False, backend_language="python", load_dir=None): """ return the conforming projectors of the broken multi-patch space @@ -465,10 +474,10 @@ def conforming_projection(self, space, hom_bc=False, backend_language="python", elif self.dim == 2: if space == 'V0': - cP = ConformingProjection_V0(self.V0, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + cP = ConformingProjection_V0(self.V0, self._domain_h, p_moments=p_moments, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) elif space == 'V1': if self.sequence[1] == 'hcurl': - cP = ConformingProjection_V1(self.V1, self._domain_h, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + cP = ConformingProjection_V1(self.V1, self._domain_h, p_moments=p_moments, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) else: raise NotImplementedError('2D sequence with H-div not available yet') @@ -481,52 +490,3 @@ def conforming_projection(self, space, hom_bc=False, backend_language="python", raise NotImplementedError("3D projectors are not available") return cP - - def get_dual_dofs(self, space, f, backend_language="python", return_format='stencil_array'): - """ - return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(V^k)) of a given function f, as a stencil array or numpy array - - Parameters - ---------- - space : - The space of the dual dofs - - f : - The function used for evaluation - - backend_language: - The backend used to accelerate the code - - return_format: - The format of the dofs, can be 'stencil_array' or 'numpy_array' - - Returns - ------- - tilde_f: - The dual dofs - """ - if space == 'V0': - Vh = self.V0 - elif space == 'V1': - Vh = self.V1 - elif space == 'V2': - Vh = self.V2 - else: - raise NotImplementedError("The space of kind {} is not available".format(space)) - - V = Vh.symbolic_space - v = element_of(V, name='v') - - if isinstance(v, ScalarFunction): - expr = f*v - else: - expr = dot(f,v) - - l = LinearForm(v, integral( V.domain, expr)) - lh = discretize(l, self._domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) - tilde_f = lh.assemble() - - if return_format == 'numpy_array': - return tilde_f.toarray() - else: - return tilde_f diff --git a/psydac/feec/multipatch/derivatives.py b/psydac/feec/multipatch/derivatives.py new file mode 100644 index 000000000..4670d6057 --- /dev/null +++ b/psydac/feec/multipatch/derivatives.py @@ -0,0 +1,75 @@ +import os +import numpy as np + +from psydac.linalg.block import BlockLinearOperator + +from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D +from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator + + +class BrokenGradient_2D(FemLinearOperator): + + def __init__(self, V0h, V1h): + + FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=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)}) + + 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): + + 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)}) + + 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): + + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=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)}) + + def transpose(self, conjugate=False): + return BrokenTransposedScalarCurl_2D( + V1h=self.fem_domain, V2h=self.fem_codomain) + + +# ============================================================================== +class BrokenTransposedScalarCurl_2D(FemLinearOperator): + + 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)}) + + def transpose(self, conjugate=False): + return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) + +# ============================================================================== diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py index 6fec9752a..6af372362 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -1,8 +1,3 @@ -# coding: utf-8 - -# Conga operators on piecewise (broken) de Rham sequences - -from sympy import Tuple import os import numpy as np @@ -10,1277 +5,19 @@ from scipy.sparse import block_diag from scipy.sparse.linalg import inv -from sympde.topology import Boundary, Interface from sympde.topology import element_of, elements_of from sympde.topology.space import ScalarFunction from sympde.calculus import dot -from sympde.calculus import Dn, minus, plus from sympde.expr.expr import LinearForm, BilinearForm from sympde.expr.expr import integral -from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid from psydac.api.discretization import discretize 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.fem.basic import FemField -from psydac.fem.splines import SplineSpace -from psydac.utilities.quadratures import gauss_legendre - -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 - -from scipy.sparse import eye as sparse_eye -from scipy.sparse import csr_matrix -from scipy.special import comb - - -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 = 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': - - #map V^+ to V^+_0 - T = np.zeros((n_f, n_f)) - for i in range(n_f): - 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).transpose() - c_mass_mat = calculate_mass_matrix(coarse_space_1d) - - if p_moments > 0: - # L^2 projection from V^+_0 to V^- - R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) - gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) - n = len(gamma) - - # maps V^- to V^+_0 in a moment preserving way - T2 = np.eye(n_c) - T2[0, 0] = T2[-1, -1] = 0 - T2[1:n+1, 0] += gamma - T2[-(n+1):-1, -1] += gamma[::-1] - - # maps V^+ to V^- in a moment preserving way - R = T2 @ R @ T - - else: - R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) - R = R @ T - - # add the degrees of freedom of T back - 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) - - # The pure L^2 projection is already moment preserving - 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 - - assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) - - else: - ER_1D = R_1D = E_1D = sparse_eye( - fine_space_1d.nbasis, format="lil") - - 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 * \ - comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / 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): - 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( - i, 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] - - if not corner_indices.issuperset({ig}): - corner_indices.add(ig) - 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 - # should be in the V^0 spaces - 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 - - - -class ConformingProjection_V0(FemLinearOperator): - """ - Conforming projection from global broken V0 space to conforming global V0 space - Defined by averaging of interface dofs - - Parameters - ---------- - V0h: - The discrete space - - p_moments: - Number of polynomial moments to be preserved in the projection. - - hom_bc : - Apply homogenous boundary conditions if True - - storage_fn: - filename to store/load the operator sparse matrix - """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) - - def __init__( - self, - V0h, - p_moments=-1, - hom_bc=False, - storage_fn=None): - - FemLinearOperator.__init__(self, fem_domain=V0h) - - V0 = V0h.symbolic_space - domain = V0.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print("[ConformingProjection_V0] loading operator sparse matrix from " + storage_fn) - self._sparse_matrix = load_npz(storage_fn) - - else: - - self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - - if storage_fn: - print("[ConformingProjection_V0] storing operator sparse matrix in " + storage_fn) - save_npz(storage_fn, self._sparse_matrix) - - -# =============================================================================== - - -class ConformingProjection_V1(FemLinearOperator): - """ - Conforming projection from global broken V1 space to conforming V1 global space - - proj.dot(v) returns the conforming projection of v, computed by solving linear system - - Parameters - ---------- - V1h: - The discrete space - - p_moments: - Number of polynomial moments to be preserved in the projection. - - hom_bc : - Apply homogenous boundary conditions if True - - storage_fn: - filename to store/load the operator sparse matrix - """ - # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form - # - allow case without interfaces (single or multipatch) - - def __init__( - self, - V1h, - p_moments=-1, - hom_bc=False, - storage_fn=None): - - FemLinearOperator.__init__(self, fem_domain=V1h) - - V1 = V1h.symbolic_space - domain = V1.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print("[ConformingProjection_V1] loading operator sparse matrix from " + storage_fn) - self._sparse_matrix = load_npz(storage_fn) - - else: - - self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - - if storage_fn: - print("[ConformingProjection_V1] storing operator sparse matrix in " + storage_fn) - save_npz(storage_fn, self._sparse_matrix) - - # =============================================================================== class HodgeOperator(FemLinearOperator): """ @@ -1444,144 +181,3 @@ def assemble_dual_Hodge_matrix(self): inv_M = block_diag(inv_M_blocks) self._dual_Hodge_sparse_matrix = inv_M -# ============================================================================== - - -class BrokenGradient_2D(FemLinearOperator): - - def __init__(self, V0h, V1h): - - FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=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)}) - - 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): - - 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)}) - - 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): - - FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=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)}) - - def transpose(self, conjugate=False): - return BrokenTransposedScalarCurl_2D( - V1h=self.fem_domain, V2h=self.fem_codomain) - - -# ============================================================================== -class BrokenTransposedScalarCurl_2D(FemLinearOperator): - - 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)}) - - def transpose(self, conjugate=False): - return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) - -# ============================================================================== - - -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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] - - u0_coeffs = BlockVector(self._V0h.coeff_space, - blocks=[u0j.coeffs for u0j in u0s]) - - 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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] - - E1_coeffs = BlockVector(self._V1h.coeff_space, - blocks=[E1j.coeffs for E1j in E1s]) - - 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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] - - B2_coeffs = BlockVector(self._V2h.coeff_space, - blocks=[B2j.coeffs for B2j in B2s]) - - return FemField(self._V2h, coeffs=B2_coeffs) diff --git a/psydac/feec/multipatch/projectors.py b/psydac/feec/multipatch/projectors.py new file mode 100644 index 000000000..ec60b845e --- /dev/null +++ b/psydac/feec/multipatch/projectors.py @@ -0,0 +1,1344 @@ +# coding: utf-8 + +# Conga operators on piecewise (broken) de Rham sequences + +import os +import numpy as np +from sympy import Tuple + +from scipy.sparse import save_npz, load_npz +from scipy.sparse import eye as sparse_eye +from scipy.sparse import csr_matrix +from scipy.special import comb + +from sympde.topology import Boundary, Interface +from sympde.calculus import dot +from sympde.calculus import Dn, minus, plus + + +from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid + +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator +from psydac.fem.basic import FemField +from psydac.fem.splines import SplineSpace +from psydac.utilities.quadratures import gauss_legendre + +from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 +from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator + + +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 = 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': + + #map V^+ to V^+_0 + T = np.zeros((n_f, n_f)) + for i in range(n_f): + 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).transpose() + c_mass_mat = calculate_mass_matrix(coarse_space_1d) + + if p_moments > 0: + # L^2 projection from V^+_0 to V^- + R[:, 1:-1] = np.linalg.solve(c_mass_mat, cf_mass_mat[:, 1:-1]) + gamma = get_1d_moment_correction(coarse_space_1d, p_moments=p_moments) + n = len(gamma) + + # maps V^- to V^+_0 in a moment preserving way + T2 = np.eye(n_c) + T2[0, 0] = T2[-1, -1] = 0 + T2[1:n+1, 0] += gamma + T2[-(n+1):-1, -1] += gamma[::-1] + + # maps V^+ to V^- in a moment preserving way + R = T2 @ R @ T + + else: + R[1:-1, 1:-1] = np.linalg.solve(c_mass_mat[1:-1, 1:-1], cf_mass_mat[1:-1, 1:-1]) + R = R @ T + + # add the degrees of freedom of T back + 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) + + # The pure L^2 projection is already moment preserving + 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 + + assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) + + else: + ER_1D = R_1D = E_1D = sparse_eye( + fine_space_1d.nbasis, format="lil") + + 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 * \ + comb(p_moments, pol) * ((enddom - x) / denom)**(p_moments - pol) * ((x - begdom) / 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): + 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( + i, 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] + + if not corner_indices.issuperset({ig}): + corner_indices.add(ig) + 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 + # should be in the V^0 spaces + 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 + + + +class ConformingProjection_V0(FemLinearOperator): + """ + Conforming projection from global broken V0 space to conforming global V0 space + Defined by averaging of interface dofs + + Parameters + ---------- + V0h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + + storage_fn: + filename to store/load the operator sparse matrix + """ + # todo (MCP, 16.03.2021): + # - avoid discretizing a bilinear form + # - allow case without interfaces (single or multipatch) + + def __init__( + self, + V0h, + p_moments=-1, + hom_bc=False, + storage_fn=None): + + FemLinearOperator.__init__(self, fem_domain=V0h) + + V0 = V0h.symbolic_space + domain = V0.domain + self.symbolic_domain = domain + + if storage_fn and os.path.exists(storage_fn): + print("[ConformingProjection_V0] loading operator sparse matrix from " + storage_fn) + self._sparse_matrix = load_npz(storage_fn) + + else: + + self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + if storage_fn: + print("[ConformingProjection_V0] storing operator sparse matrix in " + storage_fn) + save_npz(storage_fn, self._sparse_matrix) + + +# =============================================================================== + + +class ConformingProjection_V1(FemLinearOperator): + """ + Conforming projection from global broken V1 space to conforming V1 global space + + proj.dot(v) returns the conforming projection of v, computed by solving linear system + + Parameters + ---------- + V1h: + The discrete space + + p_moments: + Number of polynomial moments to be preserved in the projection. + + hom_bc : + Apply homogenous boundary conditions if True + + storage_fn: + filename to store/load the operator sparse matrix + """ + # todo (MCP, 16.03.2021): + # - avoid discretizing a bilinear form + # - allow case without interfaces (single or multipatch) + + def __init__( + self, + V1h, + p_moments=-1, + hom_bc=False, + storage_fn=None): + + FemLinearOperator.__init__(self, fem_domain=V1h) + + V1 = V1h.symbolic_space + domain = V1.domain + self.symbolic_domain = domain + + if storage_fn and os.path.exists(storage_fn): + print("[ConformingProjection_V1] loading operator sparse matrix from " + storage_fn) + self._sparse_matrix = load_npz(storage_fn) + + else: + + self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + if storage_fn: + print("[ConformingProjection_V1] storing operator sparse matrix in " + storage_fn) + save_npz(storage_fn, self._sparse_matrix) + + +# =============================================================================== + +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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] + + u0_coeffs = BlockVector(self._V0h.coeff_space, + blocks=[u0j.coeffs for u0j in u0s]) + + 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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] + + E1_coeffs = BlockVector(self._V1h.coeff_space, + blocks=[E1j.coeffs for E1j in E1s]) + + 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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] + + B2_coeffs = BlockVector(self._V2h.coeff_space, + blocks=[B2j.coeffs for B2j in B2s]) + + return FemField(self._V2h, coeffs=B2_coeffs) diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index 9acd40c74..decbc0705 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -9,7 +9,7 @@ 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.api.discretization import discretize from psydac.feec.multipatch.utilities import time_count from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField diff --git a/psydac/fem/projectors.py b/psydac/fem/projectors.py index d40db6a8c..e13185051 100644 --- a/psydac/fem/projectors.py +++ b/psydac/fem/projectors.py @@ -1,8 +1,17 @@ import numpy as np -from psydac.linalg.kron import KroneckerDenseMatrix -from psydac.core.bsplines import hrefinement_matrix -from psydac.linalg.stencil import StencilVectorSpace +from sympde.topology import element_of +from sympde.topology.space import ScalarFunction +from sympde.topology.mapping import Mapping +from sympde.calculus import dot +from sympde.expr.expr import LinearForm, integral + +from psydac.api.settings import PSYDAC_BACKENDS + +from psydac.linalg.kron import KroneckerDenseMatrix +from psydac.core.bsplines import hrefinement_matrix +from psydac.linalg.stencil import StencilVectorSpace +from psydac.fem.basic import FemSpace __all__ = ('knots_to_insert', 'knot_insertion_projection_operator') @@ -100,3 +109,52 @@ def knot_insertion_projection_operator(domain, codomain): ops.append(np.eye(d.nbasis)) return KroneckerDenseMatrix(domain.coeff_space, codomain.coeff_space, *ops) + + +def get_dual_dofs(Vh, f, domain_h, backend_language="python", return_format='stencil_array'): + """ + return the dual dofs tilde_sigma_i(f) = < Lambda_i, f >_{L2} i = 1, .. dim(Vh)) of a given function f, as a stencil array or numpy array + + Parameters + ---------- + Vh : FemSpace + The discrete space for the dual dofs + + f : + The function used for evaluation + + domain_h : + The discrete domain corresponding to Vh + + backend_language: + The backend used to accelerate the code + + return_format: + The format of the dofs, can be 'stencil_array' or 'numpy_array' + + Returns + ------- + tilde_f: + The dual dofs + """ + + from psydac.api.discretization import discretize + + assert isinstance(Vh, FemSpace) + + V = Vh.symbolic_space + v = element_of(V, name='v') + + if Vh.is_vector_valued: + expr = dot(f,v) + else: + expr = f*v + + l = LinearForm(v, integral( V.domain, expr)) + lh = discretize(l, domain_h, Vh, backend=PSYDAC_BACKENDS[backend_language]) + tilde_f = lh.assemble() + + if return_format == 'numpy_array': + return tilde_f.toarray() + else: + return tilde_f From f0b88879b222de6871ca4ee4721ba052b55d36e4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 26 Jun 2025 16:23:13 +0200 Subject: [PATCH 005/102] add HodgeOperator to DiscreteDeRhamMultipatch --- psydac/api/discretization.py | 2 +- psydac/api/feec.py | 75 +++++++++++++++++++++++++--- psydac/feec/multipatch/operators.py | 8 ++- psydac/feec/multipatch/projectors.py | 3 +- 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index b431eda08..961090501 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -625,7 +625,7 @@ def discretize(a, *args, **kwargs): elif isinstance(a, BasicFunctionSpace): return discretize_space(a, *args, **kwargs) - elif isinstance(a, Derham)and not a.V0.is_broken: + elif isinstance(a, Derham) and not a.V0.is_broken: return discretize_derham(a, *args, **kwargs) elif isinstance(a, Derham) and a.V0.is_broken: diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 85bbc6d16..42c02c0e5 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -14,6 +14,7 @@ from psydac.fem.basic import FemSpace from psydac.fem.vector import VectorFemSpace +from psydac.feec.multipatch.operators import HodgeOperator from psydac.feec.multipatch.derivatives import BrokenGradient_2D from psydac.feec.multipatch.derivatives import BrokenScalarCurl_2D from psydac.feec.multipatch.projectors import Multipatch_Projector_H1 @@ -23,7 +24,6 @@ from psydac.feec.multipatch.projectors import ConformingProjection_V1 from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator - __all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) #============================================================================== @@ -344,6 +344,10 @@ def __init__(self, *, mapping, domain_h, spaces, sequence=None): def sequence(self): return self._sequence + @property + def domain_h(self): + return self._domain_h + @property def H1vec(self): raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') @@ -370,6 +374,10 @@ def derivatives(self): def derivatives_as_matrices(self): return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) + @property + def derivatives_as_sparse_matrices(self): + return tuple(b_diff.tosparse for b_diff in self._broken_diff_ops) + #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): """ @@ -422,7 +430,7 @@ def projectors(self, *, kind='global', nquads=None): raise NotImplementedError("3D projectors are not available") #-------------------------------------------------------------------------- - def conforming_projection(self, space, p_moments=-1, hom_bc=False, backend_language="python", load_dir=None): + def conforming_projection(self, space=None, p_moments=-1, hom_bc=False, load_dir=None): """ return the conforming projectors of the broken multi-patch space @@ -434,9 +442,6 @@ def conforming_projection(self, space, p_moments=-1, hom_bc=False, backend_langu hom_bc: Apply homogenous boundary conditions if True - backend_language: - The backend used to accelerate the code - load_dir: Filename for storage in sparse matrix format @@ -474,15 +479,22 @@ def conforming_projection(self, space, p_moments=-1, hom_bc=False, backend_langu elif self.dim == 2: if space == 'V0': - cP = ConformingProjection_V0(self.V0, self._domain_h, p_moments=p_moments, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + cP = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) elif space == 'V1': if self.sequence[1] == 'hcurl': - cP = ConformingProjection_V1(self.V1, self._domain_h, p_moments=p_moments, hom_bc=hom_bc, backend_language=backend_language, storage_fn=storage_fn) + cP = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) else: raise NotImplementedError('2D sequence with H-div not available yet') elif space == 'V2': cP = IdLinearOperator(self.V2) # no storage needed! + + elif space == None and self.sequence[1] == 'hcurl': + cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) + cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) + cP2 = IdLinearOperator(self.V2) # no storage needed! + + return cP0, cP1, cP2 else: raise ValueError('Invalid value for "space" argument: {}'.format(space)) @@ -490,3 +502,52 @@ def conforming_projection(self, space, p_moments=-1, hom_bc=False, backend_langu raise NotImplementedError("3D projectors are not available") return cP + + #-------------------------------------------------------------------------- + def hodge_operator(self, space=None, backend_language='python', load_dir=None): + """ + return the conforming projectors of the broken multi-patch space + + Parameters + ---------- + space : + The space of the projector + + backend_language: + The backend used to accelerate the code + + load_dir: + Filename for storage in sparse matrix format + + Returns + ------- + H: + The Hodge operator + + """ + + H = None + if self.dim == 1: + raise NotImplementedError("1D Hodges are not available") + + elif self.dim == 2: + if space == 'V0': + H = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) + elif space == 'V1': + H = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + elif space == 'V2': + H = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) + + elif space == None and self.sequence[1] == 'hcurl': + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) + + return H0, H1, H2 + else: + raise ValueError('Invalid value for "space" argument: {}'.format(space)) + + elif self.dim == 3: + raise NotImplementedError("3D Hodgesa are not available") + + return H diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py index 6af372362..6db90e082 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -8,11 +8,9 @@ from sympde.topology import element_of, elements_of from sympde.topology.space import ScalarFunction from sympde.calculus import dot -from sympde.expr.expr import LinearForm, BilinearForm +from sympde.expr.expr import BilinearForm from sympde.expr.expr import integral - -from psydac.api.discretization import discretize from psydac.api.settings import PSYDAC_BACKENDS from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D @@ -129,6 +127,7 @@ def assemble_primal_Hodge_matrix(self): the Hodge matrix is the patch-wise multi-patch mass matrix it is not stored by default but assembled on demand """ + from psydac.api.discretization import discretize if self._matrix is None: Vh = self.fem_domain @@ -145,8 +144,7 @@ def assemble_primal_Hodge_matrix(self): 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]) + ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) self._matrix = ah.assemble() # Mass matrix in stencil format self._sparse_matrix = self._matrix.tosparse() diff --git a/psydac/feec/multipatch/projectors.py b/psydac/feec/multipatch/projectors.py index ec60b845e..00ad2dec4 100644 --- a/psydac/feec/multipatch/projectors.py +++ b/psydac/feec/multipatch/projectors.py @@ -564,8 +564,7 @@ def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=Fa # moment corrections perpendicular to interfaces # assume same moments everywhere - gamma = get_1d_moment_correction( - Vh.spaces[0].spaces[0], p_moments=p_moments) + gamma = get_1d_moment_correction(Vh.spaces[0].spaces[0], p_moments=p_moments) domain = Vh.symbolic_space.domain ndim = 2 From 64e1ae96dc3c53b25085d8328af9424787258667 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 9 Jul 2025 13:16:28 +0200 Subject: [PATCH 006/102] remove DiffOperator class and subclass FemLinearOperator instead --- psydac/feec/derivatives.py | 54 +---- psydac/feec/multipatch/derivatives.py | 22 +- .../feec/multipatch/fem_linear_operators.py | 218 ------------------ psydac/fem/basic.py | 98 +++++++- 4 files changed, 116 insertions(+), 276 deletions(-) delete mode 100644 psydac/feec/multipatch/fem_linear_operators.py diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index f7f28ccbd..90e573670 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -12,10 +12,9 @@ from psydac.fem.basic import FemField, FemSpace from psydac.linalg.basic import LinearOperator from psydac.ddm.cart import DomainDecomposition, CartDecomposition - +from psydac.fem.basic import FemLinearOperator __all__ = ( 'DirectionalDerivativeOperator', - 'DiffOperator', 'Derivative_1D', 'Gradient_2D', 'Gradient_3D', @@ -361,40 +360,7 @@ def copy(self): self._diffdir, negative=self._negative, transposed=self._transposed) #==================================================================================================== -class DiffOperator: - def __init__(self, domain, codomain, matrix): - assert isinstance(domain, FemSpace) - assert isinstance(codomain, FemSpace) - assert isinstance(matrix, LinearOperator) - assert domain.coeff_space is matrix.domain - assert codomain.coeff_space is matrix.codomain - - self._domain = domain - self._codomain = codomain - self._matrix = matrix - - @property - def matrix(self): - return self._matrix - - @property - def domain(self): - return self._domain - - @property - def codomain(self): - return self._codomain - - def __call__(self, u): - assert isinstance(u, FemField) - assert u.space == self.domain - - coeffs = self.matrix.dot(u.coeffs) - - return FemField(self.codomain, coeffs=coeffs) - -#==================================================================================================== -class Derivative_1D(DiffOperator): +class Derivative_1D(FemLinearOperator): """ 1D derivative. @@ -418,7 +384,7 @@ def __init__(self, H1, L2): super().__init__(H1, L2, DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) #==================================================================================================== -class Gradient_2D(DiffOperator): +class Gradient_2D(FemLinearOperator): """ Gradient operator in 2D. @@ -434,7 +400,7 @@ class Gradient_2D(DiffOperator): def __init__(self, H1, Hcurl): assert isinstance( H1, TensorFemSpace); assert H1.ldim == 2 - assert isinstance(Hcurl, VectorFemSpace); assert Hcurl.ldim == 2 + assert isinstance(Hcurl, VectorFemSpace); assert Hcurl.ldim == 2 assert Hcurl.spaces[0].periodic == H1.periodic assert Hcurl.spaces[1].periodic == H1.periodic @@ -458,7 +424,7 @@ def __init__(self, H1, Hcurl): #==================================================================================================== -class Gradient_3D(DiffOperator): +class Gradient_3D(FemLinearOperator): """ Gradient operator in 3D. @@ -500,7 +466,7 @@ def __init__(self, H1, Hcurl): super().__init__(H1, Hcurl, matrix) #==================================================================================================== -class ScalarCurl_2D(DiffOperator): +class ScalarCurl_2D(FemLinearOperator): """ Scalar curl operator in 2D: computes a scalar field from a vector field. @@ -539,7 +505,7 @@ def __init__(self, Hcurl, L2): super().__init__(Hcurl, L2, matrix) #==================================================================================================== -class VectorCurl_2D(DiffOperator): +class VectorCurl_2D(FemLinearOperator): """ Vector curl operator in 2D: computes a vector field from a scalar field. This is sometimes called the 'rot' operator. @@ -579,7 +545,7 @@ def __init__(self, H1, Hdiv): super().__init__(H1, Hdiv, matrix) #==================================================================================================== -class Curl_3D(DiffOperator): +class Curl_3D(FemLinearOperator): """ Curl operator in 3D. @@ -629,7 +595,7 @@ def __init__(self, Hcurl, Hdiv): super().__init__(Hcurl, Hdiv, matrix) #==================================================================================================== -class Divergence_2D(DiffOperator): +class Divergence_2D(FemLinearOperator): """ Divergence operator in 2D. @@ -668,7 +634,7 @@ def __init__(self, Hdiv, L2): super().__init__(Hdiv, L2, matrix) #==================================================================================================== -class Divergence_3D(DiffOperator): +class Divergence_3D(FemLinearOperator): """ Divergence operator in 3D. diff --git a/psydac/feec/multipatch/derivatives.py b/psydac/feec/multipatch/derivatives.py index 4670d6057..74c461027 100644 --- a/psydac/feec/multipatch/derivatives.py +++ b/psydac/feec/multipatch/derivatives.py @@ -1,11 +1,9 @@ import os import numpy as np -from psydac.linalg.block import BlockLinearOperator - +from psydac.linalg.block import BlockLinearOperator from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D -from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator - +from psydac.fem.basic import FemLinearOperator class BrokenGradient_2D(FemLinearOperator): @@ -15,8 +13,8 @@ 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop for i, D0i in enumerate(D0s)}) def transpose(self, conjugate=False): # todo (MCP): define as the dual differential operator @@ -33,8 +31,8 @@ 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.T for i, D0i in enumerate(D0s)}) + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop.T for i, D0i in enumerate(D0s)}) def transpose(self, conjugate=False): # todo (MCP): discard @@ -49,8 +47,8 @@ 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): return BrokenTransposedScalarCurl_2D( @@ -66,8 +64,8 @@ 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.T for i, D1i in enumerate(D1s)}) + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop.T for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) diff --git a/psydac/feec/multipatch/fem_linear_operators.py b/psydac/feec/multipatch/fem_linear_operators.py deleted file mode 100644 index aafb77931..000000000 --- a/psydac/feec/multipatch/fem_linear_operators.py +++ /dev/null @@ -1,218 +0,0 @@ -# coding: utf-8 - -from mpi4py import MPI - -from scipy.sparse import eye as sparse_id - -from psydac.linalg.basic import LinearOperator -from psydac.fem.basic import FemField - -#=============================================================================== -class FemLinearOperator( LinearOperator ): - """ - Linear operators with an additional Fem layer - """ - - def __init__( self, fem_domain=None, fem_codomain=None, matrix=None, sparse_matrix=None): - """ - we may store the matrix of the linear operator with different formats - :param matrix: stencil format - :param sparse_matrix: scipy sparse format - """ - assert fem_domain - self._fem_domain = fem_domain - if fem_codomain: - self._fem_codomain = fem_codomain - else: - self._fem_codomain = fem_domain - self._domain = self._fem_domain.coeff_space - self._codomain = self._fem_codomain.coeff_space - - self._matrix = matrix - self._sparse_matrix = sparse_matrix - - @property - def domain( self ): - return self._domain - - @property - def codomain( self ): - return self._codomain - - @property - def fem_domain( self ): - return self._fem_domain - - @property - def fem_codomain( self ): - return self._fem_codomain - - @property - def matrix( self ): - return self._matrix - - @property - def T(self): - return self.transpose() - - @property - def dtype( self ): - return self.domain.dtype - - def toarray(self): - return self._matrix.toarray() - #raise NotImplementedError('toarray() is not defined for FEMLinearOperators.') - - def tosparse(self): - return self._matrix.tosparse() - #raise NotImplementedError('tosparse() is not defined for FEMLinearOperators.') - - # ... - def transpose(self, conjugate=False): - raise NotImplementedError('Class does not provide a transpose() method') - - # ... - def to_sparse_matrix( self , **kwargs): - if self._sparse_matrix is not None: - return self._sparse_matrix - elif self._matrix is not None: - return self._matrix.tosparse() - else: - raise NotImplementedError('Class does not provide a get_sparse_matrix() method without a matrix') - - # ... - def __call__( self, f ): - if self._matrix is not None: - coeffs = self._matrix.dot(f.coeffs) - return FemField(self.fem_codomain, coeffs=coeffs) - else: - raise NotImplementedError('Class does not provide a __call__ method without a matrix') - - # ... - def dot( self, f_coeffs, out=None ): - # coeffs layer - if self._matrix is not None: - f = FemField(self.fem_domain, coeffs=f_coeffs) - return self(f).coeffs - else: - raise NotImplementedError('Class does not provide a dot method without a matrix') - - # ... - def __mul__(self, c): - return MultLinearOperator(c, self) - - # ... - def __add__(self, C): - assert isinstance(C, FemLinearOperator) - return SumLinearOperator(C, self) - - # ... - def __sub__(self, C): - assert isinstance(C, FemLinearOperator) - return SumLinearOperator(C, -self) - - # ... - def __neg__(self): - return MultLinearOperator(-1, self) - - -#============================================================================== -class ComposedLinearOperator( FemLinearOperator ): - """ - operator L = L_1 .. L_n - with L_i = self._operators[i-1] - (so, the last one is applied first, like in a product) - """ - def __init__( self, operators ): - n = len(operators) - assert all([isinstance(operators[i], FemLinearOperator) for i in range(n)]) - assert all([operators[i].fem_domain == operators[i+1].fem_codomain for i in range(n-1)]) - FemLinearOperator.__init__( - self, fem_domain=operators[-1].fem_domain, fem_codomain=operators[0].fem_codomain - ) - self._operators = operators - self._n = n - - # matrix not defined by matrix product because it could break the Stencil Matrix structure - - def to_sparse_matrix( self, **kwargs): - mat = self._operators[-1].to_sparse_matrix() - for i in range(2, self._n+1): - mat = self._operators[-i].to_sparse_matrix() * mat - return mat - - def __call__( self, f ): - v = self._operators[-1](f) - for i in range(2, self._n+1): - v = self._operators[-i](v) - return v - - def dot( self, f_coeffs, out=None ): - v_coeffs = self._operators[-1].dot(f_coeffs) - for i in range(2, self._n+1): - v_coeffs = self._operators[-i].dot(v_coeffs) - return v_coeffs - - -#============================================================================== -class IdLinearOperator( FemLinearOperator ): - - def __init__( self, V ): - FemLinearOperator.__init__(self, fem_domain=V) - - def to_sparse_matrix( self , **kwargs): - return sparse_id( self.fem_domain.nbasis ) - - def __call__( self, f ): - return f - - def dot( self, f_coeffs, out=None ): - return f_coeffs - -#============================================================================== -class SumLinearOperator( FemLinearOperator ): - - def __init__( self, B, A ): - assert isinstance(A, FemLinearOperator) - assert isinstance(B, FemLinearOperator) - assert B.fem_domain == A.fem_domain - assert B.fem_codomain == A.fem_codomain - FemLinearOperator.__init__( - self, fem_domain=A.fem_domain, fem_codomain=A.fem_codomain - ) - self._A = A - self._B = B - - def to_sparse_matrix( self, **kwargs): - return self._A.to_sparse_matrix() + self._B.to_sparse_matrix() - - def __call__( self, f ): - # fem layer - return self._B(f) + self._A(f) - - def dot( self, f_coeffs, out=None ): - # coeffs layer - return self._B.dot(f_coeffs) + self._A.dot(f_coeffs) - -#============================================================================== -class MultLinearOperator( FemLinearOperator ): - - def __init__( self, c, A ): - assert isinstance(A, FemLinearOperator) - FemLinearOperator.__init__( - self, fem_domain=A.fem_domain, fem_codomain=A.fem_codomain - ) - self._A = A - self._c = c - - def to_sparse_matrix( self, **kwargs): - return self._c * self._A.to_sparse_matrix() - - def __call__( self, f ): - # fem layer - return self._c * self._A(f) - - def dot( self, f_coeffs, out=None ): - # coeffs layer - return self._c * self._A.dot(f_coeffs) - diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 09a1466fb..61b8222c8 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -7,9 +7,9 @@ """ from abc import ABCMeta, abstractmethod -from psydac.linalg.basic import Vector +from psydac.linalg.basic import Vector, LinearOperator -__all__ = ('FemSpace', 'FemField') +__all__ = ('FemSpace', 'FemField', 'FemLinearOperator') #=============================================================================== # ABSTRACT BASE CLASS: FINITE ELEMENT SPACE @@ -380,3 +380,97 @@ def __isub__(self, other): assert self._space is other._space self._coeffs -= other._coeffs return self + +#=============================================================================== +# CONCRETE CLASS: Linear Operator acting on a FEM field +#=============================================================================== +class FemLinearOperator: + """ + Linear operators with an additional Fem layer. + There is also a shorthand access to sparse matrices as they are sometimes used in the FEEC interfaces. + + Parameters + ---------- + fem_domain: FemSpace + The discrete space + + fem_codomain: FemSpace + Number of polynomial moments to be preserved in the projection. + + linop: LinearOperator (optional) + Linear Operator. + + sparse_matrix: sparse matrix (optional) + Sparse matrix representation of the linear operator. + """ + + def __init__(self, fem_domain, fem_codomain, linop=None, sparse_matrix=None): + assert isinstance(fem_domain, FemSpace) + assert isinstance(fem_codomain, FemSpace) + + self._fem_domain = fem_domain + self._fem_codomain = fem_codomain + + self._linop_domain = fem_domain.coeff_space + self._linop_codomain = fem_codomain.coeff_space + + self._linop = linop + self._sparse_matrix = sparse_matrix + + @property + def fem_domain(self): + return self._fem_domain + + @property + def fem_codomain(self): + return self._fem_codomain + + @property + def linop_domain(self): + return self._linop_domain + + @property + def linop_codomain(self): + return self._linop_codomain + + @property + def linop(self): + return self._linop + + @property + def toarray(self): + if self._sparse_matrix is not None: + return self._sparse_matrix.toarray() + else: + return self._linop.toarray() + + @property + def tosparse(self): + if self._sparse_matrix is None: + self._sparse_matrix = self._linop.tosparse() + + return self._sparse_matrix + + + def __call__(self, u): + assert isinstance(u, FemField) + assert u.space == self.fem_domain + + if self._linop is not None: + coeffs = self._linop.dot(u.coeffs) + elif self._sparse_matrix is not None: + coeffs = self._sparse_matrix.dot(u.coeffs) + else: + raise NotImplementedError('Class does not provide a __call__ method without a matrix or linear operator') + + return FemField(self.fem_codomain, coeffs=coeffs) + + def dot( self, f_coeffs, out=None ): + assert isinstance(f_coeffs, Vector) + assert f_coeffs.space is self._linop_domain + + if self._linop is not None or self._sparse_matrix is not None: + f = FemField(self.fem_domain, coeffs=f_coeffs) + return self(f).coeffs + else: + raise NotImplementedError('Class does not provide a dot method without a matrix or linear operator') From 259786132f7e2e9c293b8cf018916ec781889b46 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 9 Jul 2025 13:18:24 +0200 Subject: [PATCH 007/102] fix typo in linalg/basic --- psydac/linalg/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index f9f63406d..062723e35 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -700,7 +700,7 @@ def toarray(self): def tosparse(self): from scipy.sparse import csr_matrix - return self._scalar*csr_matrix(self._operator.toarray()) + return self._scalar*csr_matrix(self._operator.tosparse()) def transpose(self, conjugate=False): return ScaledLinearOperator(domain=self.codomain, codomain=self.domain, c=self._scalar if not conjugate else np.conjugate(self._scalar), A=self._operator.transpose(conjugate=conjugate)) From 555fcb3025b8f34fc5bc811dbfac7553cce07dd7 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 9 Jul 2025 13:19:51 +0200 Subject: [PATCH 008/102] add SparseMatrixLinearOperator --- psydac/linalg/utilities.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index 57a9a6b86..d0d406133 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -3,7 +3,7 @@ import numpy as np from math import sqrt -from psydac.linalg.basic import Vector +from psydac.linalg.basic import Vector, MatrixFreeLinearOperator from psydac.linalg.stencil import StencilVectorSpace, StencilVector from psydac.linalg.block import BlockVector, BlockVectorSpace from psydac.linalg.topetsc import petsc_local_to_psydac, get_npts_per_block @@ -11,7 +11,8 @@ __all__ = ( 'array_to_psydac', 'petsc_to_psydac', - '_sym_ortho' + '_sym_ortho', + 'SparseMatrixLinearOperator' ) #============================================================================== @@ -198,3 +199,28 @@ def _sym_ortho(a, b): s = c * tau r = a / c return c, s, r + +#============================================================================== +class SparseMatrixLinearOperator(MatrixFreeLinearOperator): + """ + Wrap a sparse matrix into a MatrixFreeLinearOperator + """ + + def __init__(self, domain, codomain, sparse_matrix): + self._matrix = sparse_matrix + + def dot_sparse(v): + res = self._matrix @ v.toarray() + Mv = array_to_psydac(res, self.codomain) + return Mv + + super().__init__(domain, codomain, dot_sparse) + + def tosparse(self): + return self._matrix + + def toarray(self): + return self._matrix.toarray() + + def transpose(self, conjugate=False): + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) \ No newline at end of file From 82bbde6675bd619eec51a3eeb517aab28666f33f Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 9 Jul 2025 13:32:51 +0200 Subject: [PATCH 009/102] modify multipatch operators and projectors --- psydac/feec/multipatch/operators.py | 197 +++++++++++++++++---------- psydac/feec/multipatch/projectors.py | 63 ++------- 2 files changed, 137 insertions(+), 123 deletions(-) diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py index 6db90e082..474b2fdbd 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -12,17 +12,20 @@ from sympde.expr.expr import integral from psydac.api.settings import PSYDAC_BACKENDS +#from psydac.api.discretization import discretize -from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D -from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator +# from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D +#from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator +from psydac.linalg.basic import LinearOperator +from psydac.linalg.utilities import SparseMatrixLinearOperator # =============================================================================== -class HodgeOperator(FemLinearOperator): +class HodgeOperator: """ Change of basis operator: dual basis -> primal basis - self._matrix: matrix of the primal Hodge = this is the mass matrix ! - self.dual_Hodge_matrix: this is the INVERSE mass matrix + self._linop: matrix (LinearOperator) of the primal Hodge = this is the mass matrix ! + self.dual_linop: this is the INVERSE mass matrix (LinearOperator) Parameters ---------- @@ -52,19 +55,25 @@ class HodgeOperator(FemLinearOperator): # todo: allow for non-identity metrics """ - def __init__( - self, - Vh, - domain_h, - metric='identity', - 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=''): + + self._fem_domain = Vh + self._fem_codomain = Vh + + # FemLinearOperators + self._primal_Hodge = None + self._dual_Hodge = None + + # LinearOperators + self._linop = None + self._dual_linop = None + + # Sparse matrices + self._sparse_matrix = None + self._dual_sparse_matrix = None - FemLinearOperator.__init__(self, fem_domain=Vh) self._domain_h = domain_h self._backend_language = backend_language - self._dual_Hodge_sparse_matrix = None assert metric == 'identity' self._metric = metric @@ -73,65 +82,41 @@ def __init__( 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 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) + print(" ... loading dual Hodge sparse matrix from " + dual_Hodge_storage_fn) + self._dual_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 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] assembling both sparse matrices for storage...") + self.assemble_matrix() + 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) + self.assemble_dual_sparse_matrix() + print("[HodgeOperator] storing dual Hodge sparse matrix in " + dual_Hodge_storage_fn) + save_npz(dual_Hodge_storage_fn, self._dual_sparse_matrix) else: # matrices are not stored, we will probably compute them later pass - def to_sparse_matrix(self): - """ - 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): - return FemLinearOperator.to_sparse_matrix(self) - - self.assemble_primal_Hodge_matrix() - - return self._sparse_matrix - - def assemble_primal_Hodge_matrix(self): + def assemble_matrix(self): """ the Hodge matrix is the patch-wise multi-patch mass matrix it is not stored by default but assembled on demand """ from psydac.api.discretization import discretize + from psydac.fem.basic import FemLinearOperator - if self._matrix is None: - Vh = self.fem_domain - assert Vh == self.fem_codomain + if self._linop is None: + Vh = self._fem_domain + assert Vh == self._fem_codomain V = Vh.symbolic_space domain = V.domain @@ -146,26 +131,24 @@ def assemble_primal_Hodge_matrix(self): a = BilinearForm((u, v), integral(domain, expr)) ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) - self._matrix = ah.assemble() # Mass matrix in stencil format - self._sparse_matrix = self._matrix.tosparse() + self._linop = ah.assemble() # Mass matrix in stencil format + self._sparse_matrix = self._linop.tosparse() - def get_dual_Hodge_sparse_matrix(self): - if self._dual_Hodge_sparse_matrix is None: - self.assemble_dual_Hodge_matrix() + self._primal_Hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop, sparse_matrix=self._sparse_matrix) - return self._dual_Hodge_sparse_matrix - - def assemble_dual_Hodge_matrix(self): + # which of the two assemblys for the sparse dual matrix is better? + def assemble_dual_sparse_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 + the dual Hodge sparse 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) exact inversion of the mass matrix """ + from psydac.fem.basic import FemLinearOperator + + if self._dual_sparse_matrix is None: + if not self._linop: + self.assemble_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 + M = self._linop # mass matrix of the (primal) basis nrows = M.n_block_rows ncols = M.n_block_cols @@ -177,5 +160,77 @@ def assemble_dual_Hodge_matrix(self): inv_M_blocks.append(inv_Mii) inv_M = block_diag(inv_M_blocks) - self._dual_Hodge_sparse_matrix = inv_M + self._dual_sparse_matrix = inv_M + self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + + + def assemble_dual_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 approximate local (patch-wise) inversion of the mass matrix + """ + from psydac.linalg.solvers import inverse + from psydac.linalg.block import BlockLinearOperator + from psydac.fem.basic import FemLinearOperator + + if self._dual_linop is None: + if not self._linop: + self.assemble_matrix() + + M = self._linop # mass matrix of the (primal) basis + nrows = M.n_block_rows + ncols = M.n_block_cols + + inv_M_blocks = [list(b) for b in M.blocks] + for i in range(nrows): + Mii = M[i, i] + inv_Mii = inverse(M[i,i], solver='gmres') + inv_M_blocks[i][i] = inv_Mii + + self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) + self._dual_sparse_matrix = self._dual_linop.tosparse() + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + + @property + def linop(self): + if self._linop is None: + self.assemble_matrix() + + return self._linop + + @property + def tosparse(self): + if self._sparse_matrix is None: + self.assemble_matrix() + + return self._sparse_matrix + + @property + def dual_linop(self): + if self._dual_linop is None: + self.assemble_dual_sparse_matrix() + + return self._dual_linop + + @property + def dual_tosparse(self): + if self._dual_sparse_matrix is None: + self.assemble_dual_sparse_matrix() + + return self._dual_sparse_matrix + + @property + def Hodge(self): + if self._linop is None: + self.assemble_matrix() + + return self._primal_Hodge + + @property + def dual_Hodge(self): + if self._dual_linop is None: + self.assemble_dual_sparse_matrix() + + return self._dual_Hodge \ No newline at end of file diff --git a/psydac/feec/multipatch/projectors.py b/psydac/feec/multipatch/projectors.py index 00ad2dec4..faa02feea 100644 --- a/psydac/feec/multipatch/projectors.py +++ b/psydac/feec/multipatch/projectors.py @@ -6,26 +6,19 @@ import numpy as np from sympy import Tuple -from scipy.sparse import save_npz, load_npz from scipy.sparse import eye as sparse_eye from scipy.sparse import csr_matrix from scipy.special import comb from sympde.topology import Boundary, Interface -from sympde.calculus import dot -from sympde.calculus import Dn, minus, plus - from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid - -from psydac.api.settings import PSYDAC_BACKENDS -from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator -from psydac.fem.basic import FemField +from psydac.linalg.block import BlockVector +from psydac.fem.basic import FemField, FemLinearOperator from psydac.fem.splines import SplineSpace from psydac.utilities.quadratures import gauss_legendre - +from psydac.linalg.utilities import SparseMatrixLinearOperator from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 -from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator def get_patch_index_from_face(domain, face): @@ -1182,39 +1175,21 @@ class ConformingProjection_V0(FemLinearOperator): hom_bc : Apply homogenous boundary conditions if True - - storage_fn: - filename to store/load the operator sparse matrix """ # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form # - allow case without interfaces (single or multipatch) def __init__( self, V0h, p_moments=-1, - hom_bc=False, - storage_fn=None): - - FemLinearOperator.__init__(self, fem_domain=V0h) - - V0 = V0h.symbolic_space - domain = V0.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print("[ConformingProjection_V0] loading operator sparse matrix from " + storage_fn) - self._sparse_matrix = load_npz(storage_fn) + hom_bc=False): - else: + FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V0h) - self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - - if storage_fn: - print("[ConformingProjection_V0] storing operator sparse matrix in " + storage_fn) - save_npz(storage_fn, self._sparse_matrix) + self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) # =============================================================================== @@ -1235,38 +1210,22 @@ class ConformingProjection_V1(FemLinearOperator): hom_bc : Apply homogenous boundary conditions if True - - storage_fn: - filename to store/load the operator sparse matrix """ # todo (MCP, 16.03.2021): - # - avoid discretizing a bilinear form # - allow case without interfaces (single or multipatch) def __init__( self, V1h, p_moments=-1, - hom_bc=False, - storage_fn=None): + hom_bc=False): - FemLinearOperator.__init__(self, fem_domain=V1h) + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V1h) - V1 = V1h.symbolic_space - domain = V1.domain - self.symbolic_domain = domain - - if storage_fn and os.path.exists(storage_fn): - print("[ConformingProjection_V1] loading operator sparse matrix from " + storage_fn) - self._sparse_matrix = load_npz(storage_fn) - - else: - self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - if storage_fn: - print("[ConformingProjection_V1] storing operator sparse matrix in " + storage_fn) - save_npz(storage_fn, self._sparse_matrix) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) # =============================================================================== From 526d92021ab360962b077ed503b45dc3302155f8 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 9 Jul 2025 13:33:26 +0200 Subject: [PATCH 010/102] add Hodge operators to discrete derham, add option which kind of operators to return. --- psydac/api/feec.py | 201 +++++++++++++++++++++++++++------------------ 1 file changed, 121 insertions(+), 80 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 42c02c0e5..7ae20d8c9 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -11,7 +11,7 @@ from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl, pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec -from psydac.fem.basic import FemSpace +from psydac.fem.basic import FemSpace, FemLinearOperator from psydac.fem.vector import VectorFemSpace from psydac.feec.multipatch.operators import HodgeOperator @@ -22,7 +22,7 @@ from psydac.feec.multipatch.projectors import Multipatch_Projector_L2 from psydac.feec.multipatch.projectors import ConformingProjection_V0 from psydac.feec.multipatch.projectors import ConformingProjection_V1 -from psydac.feec.multipatch.fem_linear_operators import IdLinearOperator +from psydac.linalg.basic import IdentityOperator __all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) @@ -339,6 +339,8 @@ def __init__(self, *, mapping, domain_h, spaces, sequence=None): else: raise ValueError('Dimension {} is not available'.format(dim)) + self._Hodge_operators = () + self._conf_proj = () #-------------------------------------------------------------------------- @property def sequence(self): @@ -366,17 +368,14 @@ def mapping(self): def callable_mapping(self): raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') - @property - def derivatives(self): - return self._broken_diff_ops - - @property - def derivatives_as_matrices(self): - return tuple(b_diff.matrix for b_diff in self._broken_diff_ops) - - @property - def derivatives_as_sparse_matrices(self): - return tuple(b_diff.tosparse for b_diff in self._broken_diff_ops) + #-------------------------------------------------------------------------- + def derivatives(self, kind='femlinop'): + if kind == 'femlinop': + return self._broken_diff_ops + elif kind == 'sparse': + return tuple(b_diff.tosparse for b_diff in self._broken_diff_ops) + elif kind == 'linop': + return tuple(b_diff.linop for b_diff in self._broken_diff_ops) #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): @@ -430,88 +429,65 @@ def projectors(self, *, kind='global', nquads=None): raise NotImplementedError("3D projectors are not available") #-------------------------------------------------------------------------- - def conforming_projection(self, space=None, p_moments=-1, hom_bc=False, load_dir=None): + def conforming_projectors(self, p_moments=-1, hom_bc=False, kind='femlinop'): """ return the conforming projectors of the broken multi-patch space Parameters ---------- - space : - The space of the projector + + p_moments : + The number of moments preserved by the projector. hom_bc: Apply homogenous boundary conditions if True - load_dir: - Filename for storage in sparse matrix format + kind : + The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + - 'femlinop' returns a FemLinearOperator + - 'sparse' returns a sparse matrix + - 'linop' returns a LinearOperator Returns ------- - Cp: + Cp: , or The conforming projector """ if hom_bc is None: raise ValueError('please provide a value for "hom_bc" argument') - if isinstance(load_dir, str): - if not os.path.exists(load_dir): - os.makedirs(load_dir) - if space == 'V0': - P_name = 'cP0' - elif space == 'V1': - P_name = 'cP1' - elif space == 'V2': - P_name = 'cP2' - else: - raise ValueError(space) - - if hom_bc: - storage_fn = load_dir + '/{}_hom_m.npz'.format(P_name) - else: - storage_fn = load_dir + '/{}_m.npz'.format(P_name) - else: - storage_fn = None - - cP = None if self.dim == 1: raise NotImplementedError("1D projectors are not available") elif self.dim == 2: - if space == 'V0': - cP = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) - elif space == 'V1': - if self.sequence[1] == 'hcurl': - cP = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) - else: - raise NotImplementedError('2D sequence with H-div not available yet') - - elif space == 'V2': - cP = IdLinearOperator(self.V2) # no storage needed! - - elif space == None and self.sequence[1] == 'hcurl': - cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) - cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc, storage_fn=storage_fn) - cP2 = IdLinearOperator(self.V2) # no storage needed! - - return cP0, cP1, cP2 + if self.sequence[1] != 'hcurl': + raise NotImplementedError('2D sequence with H-div not available yet') + else: - raise ValueError('Invalid value for "space" argument: {}'.format(space)) + cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc)#, storage_fn=storage_fn) + cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc)#, storage_fn=storage_fn) + cP2 = IdentityOperator(self.V2.coeff_space) # no storage needed! + + self._conf_proj = (cP0, cP1, cP2) elif self.dim == 3: raise NotImplementedError("3D projectors are not available") - return cP + if kind == 'femlinop': + return cP0, cP1, FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=cP2, sparse_matrix=cP2.tosparse) + elif kind == 'sparse': + return cP0.tosparse, cP1.tosparse, cP2.tosparse + elif kind == 'linop': + return cP0.linop, cP1.linop, cP2 #-------------------------------------------------------------------------- - def hodge_operator(self, space=None, backend_language='python', load_dir=None): + def init_Hodge_operator(self, backend_language='python', load_dir=None): """ - return the conforming projectors of the broken multi-patch space + Initialize the Hodge operator for the multipatch de Rham sequence. Parameters ---------- - space : - The space of the projector backend_language: The backend used to accelerate the code @@ -519,35 +495,100 @@ def hodge_operator(self, space=None, backend_language='python', load_dir=None): load_dir: Filename for storage in sparse matrix format - Returns - ------- - H: - The Hodge operator - """ - H = None if self.dim == 1: raise NotImplementedError("1D Hodges are not available") elif self.dim == 2: - if space == 'V0': - H = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) - elif space == 'V1': - H = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) - elif space == 'V2': - H = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) - - elif space == None and self.sequence[1] == 'hcurl': + if not self._Hodge_operators: + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) - return H0, H1, H2 - else: - raise ValueError('Invalid value for "space" argument: {}'.format(space)) + self._Hodge_operators = (H0, H1, H2) elif self.dim == 3: raise NotImplementedError("3D Hodgesa are not available") - return H + #-------------------------------------------------------------------------- + def Hodge_kind(self, H, dual=False, kind='femlinop'): + """ + Helper function to return the Hodge operator in the specified form. + + Parameters + ---------- + H : + + dual : + If True, returns the dual Hodge operator + + kind : + The kind of the operator, can be 'femlinop', 'sparse' or 'linop'. + - 'femlinop' returns a FemLinearOperator + - 'sparse' returns a sparse matrix + - 'linop' returns a LinearOperator + """ + + if not dual: + if kind == 'femlinop': + return H.Hodge + elif kind == 'sparse': + return H.tosparse + elif kind == 'linop': + return H.linop + else: + if kind == 'femlinop': + return H.dual_Hodge + elif kind == 'sparse': + return H.dual_tosparse + elif kind == 'linop': + return H.dual_linop + + #-------------------------------------------------------------------------- + def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): + """ + Returns the Hodge operator for the given space and specified kind. + + Parameters + ---------- + space : str or None + The space for which to return the Hodge operator, can be 'V0', 'V1', 'V2' or None. + If None, returns a tuple with all three Hodge operators. + + dual : bool + If True, returns the dual Hodge operator. + + kind : str + The kind of the operator, can be 'femlinop', 'sparse' or 'linop'. + - 'femlinop' returns a FemLinearOperator + - 'sparse' returns a sparse matrix + - 'linop' returns a LinearOperator + + backend_language : str + The backend used to accelerate the code, default is 'python'. + + load_dir : str or None + Directory to load the Hodge operator from, if None the operator is computed on demand. + + Returns + ------- + Hodge operator for the specified space or a tuple of operators if space is None. + """ + + if not self._Hodge_operators: + self.init_Hodge_operator(backend_language='python', load_dir=None) + + H0, H1, H2 = self._Hodge_operators + + if space == 'V0': + return self.Hodge_kind(H0, dual=dual, kind=kind) + elif space == 'V1': + return self.Hodge_kind(H1, dual=dual, kind=kind) + elif space == 'V2': + return self.Hodge_kind(H2, dual=dual, kind=kind) + elif space is None: + return (self.Hodge_kind(H0, dual=dual, kind=kind), + self.Hodge_kind(H1, dual=dual, kind=kind), + self.Hodge_kind(H2, dual=dual, kind=kind)) \ No newline at end of file From 7a5c4831f2ef65146a584154570b1189afecbd56 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 10 Jul 2025 14:26:16 +0200 Subject: [PATCH 011/102] update conforming projections and Hodge operators for single patch --- psydac/api/discretization.py | 5 +- psydac/api/feec.py | 374 +++++++++++++-------------- psydac/feec/multipatch/operators.py | 67 +++-- psydac/feec/multipatch/projectors.py | 282 +++++++++++++++++++- 4 files changed, 505 insertions(+), 223 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 961090501..88257cdc2 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -193,7 +193,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #We still need to specify the symbolic space because of "_recursive_element_of" not implemented in sympde spaces.append(Xh) - return DiscreteDerham(mapping, *spaces) + return DiscreteDerham(mapping, domain_h, *spaces) #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): @@ -208,8 +208,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): return DiscreteDerhamMultipatch( mapping = mapping, domain_h = domain_h, - spaces = spaces, - sequence = [V.kind.name for V in derham.spaces] + spaces = spaces ) #============================================================================== diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 7ae20d8c9..3508fce2d 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -49,7 +49,7 @@ class DiscreteDerham(BasicDiscrete): - For the multipatch counterpart of this class please see `MultipatchDiscreteDerham` in `psydac.feec.multipatch.api`. """ - def __init__(self, mapping, *spaces): + def __init__(self, mapping, domain_h, *spaces): assert (mapping is None) or isinstance(mapping, Mapping) assert all(isinstance(space, FemSpace) for space in spaces) @@ -64,15 +64,20 @@ def __init__(self, mapping, *spaces): else : dim = len(spaces) - 1 self._spaces = spaces - + + self._domain_h = domain_h + self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) self._dim = dim self._mapping = mapping self._callable_mapping = mapping.get_callable_mapping() if mapping else None if dim == 1: D0 = Derivative_1D(spaces[0], spaces[1]) + spaces[0].diff = spaces[0].grad = D0 + self._derivatives = (D0,) + elif dim == 2: kind = spaces[1].symbolic_space.kind.name @@ -84,6 +89,8 @@ def __init__(self, mapping, *spaces): spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 + self._derivatives = (D0, D1) + elif kind == 'hdiv': D0 = VectorCurl_2D(spaces[0], spaces[1]) @@ -92,6 +99,9 @@ def __init__(self, mapping, *spaces): spaces[0].diff = spaces[0].rot = D0 spaces[1].diff = spaces[1].div = D1 + self._derivatives = (D0, D1) + + elif dim == 3: D0 = Gradient_3D(spaces[0], spaces[1]) @@ -102,14 +112,28 @@ def __init__(self, mapping, *spaces): spaces[1].diff = spaces[1].curl = D1 spaces[2].diff = spaces[2].div = D2 + self._derivatives = (D0, D1, D2) + else: raise ValueError('Dimension {} is not available'.format(dim)) + self._Hodge_operators = () + self._conf_proj = () #-------------------------------------------------------------------------- @property def dim(self): """Dimension of the physical and logical domains, which are assumed to be the same.""" return self._dim + + @property + def domain_h(self): + """Discretized domain.""" + return self._domain_h + + @property + def spaces(self): + """Spaces of the proper de Rham sequence (excluding Hvec).""" + return self._spaces @property def V0(self): @@ -136,6 +160,10 @@ def V3(self): """Fourth space of the de Rham sequence : L2 space in 3d""" return self._spaces[3] + @property + def sequence(self): + return self._sequence + @property def H1vec(self): """Vector-valued H1 space built as the Cartesian product of N copies of V0, @@ -143,11 +171,6 @@ def H1vec(self): assert self.has_vec return self._H1vec - @property - def spaces(self): - """Spaces of the proper de Rham sequence (excluding Hvec).""" - return self._spaces - @property def mapping(self): """The mapping from the logical space to the physical space.""" @@ -158,21 +181,6 @@ def callable_mapping(self): """The mapping as a callable.""" return self._callable_mapping - @property - def derivatives_as_matrices(self): - """Differential operators of the De Rham sequence as LinearOperator objects.""" - return tuple(V.diff.matrix for V in self.spaces[:-1]) - - @property - def derivatives(self): - """Differential operators of the De Rham sequence as `DiffOperator` objects. - - Those are objects with `domain` and `codomain` properties that are `FemSpace`, - they act on `FemField` (they take a `FemField` of their `domain` as input and return - a `FemField` of their `codomain`. - """ - return tuple(V.diff for V in self.spaces[:-1]) - #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): """Projectors mapping callable functions of the physical coordinates to a @@ -276,160 +284,17 @@ def projectors(self, *, kind='global', nquads=None): else : return P0, P1, P2, P3 - -#============================================================================== -class DiscreteDerhamMultipatch(DiscreteDerham): - """ Represents the discrete De Rham sequence for multipatch domains. - It only works when the number of patches>1 - - Parameters - ---------- - mapping: - The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch - - domain_h: - The discrete domain - - spaces: - The discrete spaces that are contained in the De Rham sequence - - sequence: - The space kind of each space in the De Rham sequence - """ - - def __init__(self, *, mapping, domain_h, spaces, sequence=None): - - dim = len(spaces) - 1 - self._spaces = tuple(spaces) - self._dim = dim - self._mapping = mapping - self._domain_h = domain_h - - if sequence: - if len(sequence) != dim + 1: - raise ValueError('Expected len(sequence) = {}, got {} instead'. - format(dim + 1, len(sequence))) - - if dim == 1: - self._sequence = ('h1', 'l2') - raise NotImplementedError('1D FEEC multipatch non available yet') - - elif dim == 2: - if sequence is None: - raise ValueError('Sequence must be specified in 2D case') - - elif tuple(sequence) == ('h1', 'hcurl', 'l2'): - self._sequence = tuple(sequence) - self._broken_diff_ops = ( - BrokenGradient_2D(self.V0, self.V1), - BrokenScalarCurl_2D(self.V1, self.V2), # None, - ) - - elif tuple(sequence) == ('h1', 'hdiv', 'l2'): - self._sequence = tuple(sequence) - raise NotImplementedError('2D sequence with H-div not available yet') - - else: - raise ValueError('2D sequence not understood') - - elif dim == 3: - self._sequence = ('h1', 'hcurl', 'hdiv', 'l2') - raise NotImplementedError('3D FEEC multipatch non available yet') - - else: - raise ValueError('Dimension {} is not available'.format(dim)) - - self._Hodge_operators = () - self._conf_proj = () - #-------------------------------------------------------------------------- - @property - def sequence(self): - return self._sequence - - @property - def domain_h(self): - return self._domain_h - - @property - def H1vec(self): - raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') - - @property - def spaces(self): - """Spaces of the proper de Rham sequence (excluding Hvec).""" - return self._spaces - - @property - def mapping(self): - """The mapping from the logical space to the physical space.""" - return self._mapping - - @property - def callable_mapping(self): - raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') - #-------------------------------------------------------------------------- def derivatives(self, kind='femlinop'): if kind == 'femlinop': - return self._broken_diff_ops + return self._derivatives elif kind == 'sparse': - return tuple(b_diff.tosparse for b_diff in self._broken_diff_ops) + return tuple(b_diff.tosparse for b_diff in self._derivatives) elif kind == 'linop': - return tuple(b_diff.linop for b_diff in self._broken_diff_ops) - - #-------------------------------------------------------------------------- - def projectors(self, *, kind='global', nquads=None): - """ - This method returns the patch-wise commuting projectors on the broken multi-patch space - - Parameters - ---------- - kind: - The projectors kind, can be global or local - - nquads: - The number of quadrature points. - - Returns - ------- - P0: - Patch wise H1 projector - - P1: - Patch wise Hcurl projector - - P2: - Patch wise L2 projector - - Notes - ----- - - when applied to smooth functions they return conforming fields - - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids - - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently - """ - if not (kind == 'global'): - raise NotImplementedError('only global projectors are available') - - if self.dim == 1: - raise NotImplementedError("1D projectors are not available") - - elif self.dim == 2: - P0 = Multipatch_Projector_H1(self.V0) - - if self.sequence[1] == 'hcurl': - P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) - else: - P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) - raise NotImplementedError('2D sequence with H-div not available yet') - - P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) - return P0, P1, P2 - - elif self.dim == 3: - raise NotImplementedError("3D projectors are not available") - + return tuple(b_diff.linop for b_diff in self._derivatives) + #-------------------------------------------------------------------------- - def conforming_projectors(self, p_moments=-1, hom_bc=False, kind='femlinop'): + def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): """ return the conforming projectors of the broken multi-patch space @@ -482,7 +347,7 @@ def conforming_projectors(self, p_moments=-1, hom_bc=False, kind='femlinop'): return cP0.linop, cP1.linop, cP2 #-------------------------------------------------------------------------- - def init_Hodge_operator(self, backend_language='python', load_dir=None): + def _init_Hodge_operators(self, backend_language='python', load_dir=None): """ Initialize the Hodge operator for the multipatch de Rham sequence. @@ -496,24 +361,33 @@ def init_Hodge_operator(self, backend_language='python', load_dir=None): Filename for storage in sparse matrix format """ + if not self._Hodge_operators: - if self.dim == 1: - raise NotImplementedError("1D Hodges are not available") + if self.dim == 1: + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + + self._Hodge_operators = (H0, H1) - elif self.dim == 2: - if not self._Hodge_operators: + elif self.dim == 2: - H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) - H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) - H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) - self._Hodge_operators = (H0, H1, H2) + self._Hodge_operators = (H0, H1, H2) - elif self.dim == 3: - raise NotImplementedError("3D Hodgesa are not available") + elif self.dim == 3: + + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) + H3 = HodgeOperator(self.V3, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=3) + + self._Hodge_operators = (H0, H1, H2, H3) #-------------------------------------------------------------------------- - def Hodge_kind(self, H, dual=False, kind='femlinop'): + def _Hodge_kind(self, H, dual=False, kind='femlinop'): """ Helper function to return the Hodge operator in the specified form. @@ -578,17 +452,139 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu """ if not self._Hodge_operators: - self.init_Hodge_operator(backend_language='python', load_dir=None) + self._init_Hodge_operators(backend_language=backend_language, load_dir=load_dir) H0, H1, H2 = self._Hodge_operators if space == 'V0': - return self.Hodge_kind(H0, dual=dual, kind=kind) + return self._Hodge_kind(self._Hodge_operators[0], dual=dual, kind=kind) + elif space == 'V1': - return self.Hodge_kind(H1, dual=dual, kind=kind) + return self._Hodge_kind(self._Hodge_operators[1], dual=dual, kind=kind) + elif space == 'V2': - return self.Hodge_kind(H2, dual=dual, kind=kind) + return self._Hodge_kind(self._Hodge_operators[2], dual=dual, kind=kind) + + elif space == 'V3': + return self._Hodge_kind(self._Hodge_operators[3], dual=dual, kind=kind) + elif space is None: - return (self.Hodge_kind(H0, dual=dual, kind=kind), - self.Hodge_kind(H1, dual=dual, kind=kind), - self.Hodge_kind(H2, dual=dual, kind=kind)) \ No newline at end of file + return tuple(self._Hodge_kind(H, dual=dual, kind=kind) for H in self._Hodge_operators) + + +#============================================================================== +class DiscreteDerhamMultipatch(DiscreteDerham): + """ Represents the discrete De Rham sequence for multipatch domains. + It only works when the number of patches>1 + + Parameters + ---------- + mapping: + The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch + + domain_h: + The discrete domain + + spaces: + The discrete spaces that are contained in the De Rham sequence + + sequence: + The space kind of each space in the De Rham sequence + """ + + def __init__(self, *, mapping, domain_h, spaces): + + dim = len(spaces) - 1 + self._spaces = tuple(spaces) + self._dim = dim + self._mapping = mapping + self._domain_h = domain_h + self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) + + + if dim == 1: + raise NotImplementedError('1D FEEC multipatch non available yet') + + elif dim == 2: + + if self._sequence[1] == 'hcurl': + + self._derivatives = ( + BrokenGradient_2D(self.V0, self.V1), + BrokenScalarCurl_2D(self.V1, self.V2), # None, + ) + + elif self._sequence[1] == 'hdiv': + raise NotImplementedError('2D sequence with H-div not available yet') + + else: + raise ValueError('2D sequence not understood') + + elif dim == 3: + raise NotImplementedError('3D FEEC multipatch non available yet') + + else: + raise ValueError('Dimension {} is not available'.format(dim)) + + self._Hodge_operators = () + self._conf_proj = () + + #-------------------------------------------------------------------------- + @property + def H1vec(self): + raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') + + @property + def callable_mapping(self): + raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') + + #-------------------------------------------------------------------------- + def projectors(self, *, kind='global', nquads=None): + """ + This method returns the patch-wise commuting projectors on the broken multi-patch space + + Parameters + ---------- + kind: + The projectors kind, can be global or local + + nquads: + The number of quadrature points. + + Returns + ------- + P0: + Patch wise H1 projector + + P1: + Patch wise Hcurl projector + + P2: + Patch wise L2 projector + + Notes + ----- + - when applied to smooth functions they return conforming fields + - default 'global projectors' correspond to geometric interpolation/histopolation operators on Greville grids + - here 'global' is a patch-level notion, as the interpolation-type problems are solved on each patch independently + """ + if not (kind == 'global'): + raise NotImplementedError('only global projectors are available') + + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + P0 = Multipatch_Projector_H1(self.V0) + + if self.sequence[1] == 'hcurl': + P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) + else: + P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) + raise NotImplementedError('2D sequence with H-div not available yet') + + P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) + return P0, P1, P2 + + elif self.dim == 3: + raise NotImplementedError("3D projectors are not available") diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/multipatch/operators.py index 474b2fdbd..5b0859385 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/multipatch/operators.py @@ -136,7 +136,7 @@ def assemble_matrix(self): self._primal_Hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop, sparse_matrix=self._sparse_matrix) - # which of the two assemblys for the sparse dual matrix is better? + # which of the two assemblys for the sparse dual matrix is better? For now use the exact one. def assemble_dual_sparse_matrix(self): """ the dual Hodge sparse matrix is the patch-wise inverse of the multi-patch mass matrix @@ -149,24 +149,35 @@ def assemble_dual_sparse_matrix(self): self.assemble_matrix() M = self._linop # 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) + if self._fem_domain.is_multipatch: + nrows = M.n_block_rows + ncols = M.n_block_cols - inv_M = block_diag(inv_M_blocks) + 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) - self._dual_sparse_matrix = inv_M - self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + inv_M = block_diag(inv_M_blocks) + self._dual_sparse_matrix = inv_M + self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) - def assemble_dual_matrix(self): + else: + M_m = M.tosparse() + inv_M = inv(M_m.tocsc()) + inv_M.eliminate_zeros() + + self._dual_sparse_matrix = inv_M + self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + + + def assemble_dual_matrix(self, solver ='gmres', **kwargs): """ 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 approximate local (patch-wise) inversion of the mass matrix @@ -180,19 +191,27 @@ def assemble_dual_matrix(self): self.assemble_matrix() M = self._linop # mass matrix of the (primal) basis - nrows = M.n_block_rows - ncols = M.n_block_cols - inv_M_blocks = [list(b) for b in M.blocks] - for i in range(nrows): - Mii = M[i, i] - inv_Mii = inverse(M[i,i], solver='gmres') - inv_M_blocks[i][i] = inv_Mii + if self._fem_domain.is_multipatch: - self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) - self._dual_sparse_matrix = self._dual_linop.tosparse() - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + nrows = M.n_block_rows + ncols = M.n_block_cols + inv_M_blocks = [list(b) for b in M.blocks] + for i in range(nrows): + Mii = M[i, i] + inv_Mii = inverse(M[i,i], solver=solver, **kwargs) + inv_M_blocks[i][i] = inv_Mii + + self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) + self._dual_sparse_matrix = None + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + + else: + inv_M = inverse(M, solver=solver, **kwargs) + self._dual_sparse_matrix = None + self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + @property def linop(self): if self._linop is None: diff --git a/psydac/feec/multipatch/projectors.py b/psydac/feec/multipatch/projectors.py index faa02feea..e3b526705 100644 --- a/psydac/feec/multipatch/projectors.py +++ b/psydac/feec/multipatch/projectors.py @@ -525,6 +525,9 @@ def get_1d_moment_correction(space_1d, p_moments=-1): return gamma +#============================================================================== +# Multipatch conforming projectors +#============================================================================== 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). @@ -1158,7 +1161,269 @@ def edge_moment_index(p, i, axis, ext, space, k): return Proj_edge +#============================================================================== +# Singlepatch conforming projectors +#============================================================================== +def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a single patch 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 or not hom_bc: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # should be in the V^0 spaces + + gamma = [get_1d_moment_correction(Vh.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) + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [[S.nbasis for S in T.spaces] for T in Vh.spaces] + l2g.set_patch_shapes(0, *shapes) + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + def get_edge_index(j, axis, ext): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(0, 1 - axis, multi_index) + + def edge_moment_index(p, i, axis, ext): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(0, 1 - axis, multi_index) + + + # boundary condition + for bn in domain.boundary: + + axis = bn.axis + d = 1 - axis + ext = bn.ext + space_1d = Vh.spaces[d].spaces[d] + + for i in range(0, space_1d.nbasis): + ig = get_edge_index(i, axis, ext) + Proj_edge[ig, ig] = 0 + + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext) + Proj_edge[pg, ig] = gamma[d][p] + + return Proj_edge + + + + +def construct_h1_singlepatch_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 or not hom_bc: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # assume same moments everywhere + gamma = get_1d_moment_correction(Vh.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) + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [S.nbasis for S in Vh.spaces] + l2g.set_patch_shapes(0, shapes) + + # P vertex + # vertex correction matrix + Proj_vertex = sparse_eye(dim_tot, format="lil") + + + def get_vertex_index(coords): + nbasis0 = Vh.spaces[coords[0]].nbasis - 1 + nbasis1 = Vh.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(0, 0, multi_index) + + def vertex_moment_indices(axis, coords, p_moments): + if coords[axis] == 0: + return range(1, p_moments + 2) + else: + return range(Vh.spaces[coords[axis]].nbasis - 1 - 1, + Vh.spaces[coords[axis]].nbasis - 1 - p_moments - 2, -1) + + # boundary conditions + + for co in [(0,0), (1,0), (0,1), (1,1)]: + + # global index + ig = get_vertex_index(co) + # conformity constraint + Proj_vertex[ig, 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, co, p_moments) + axis_moment_index = vertex_moment_indices(axis, co, 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(0, 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") + + def get_edge_index(j, axis, ext): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == - 1 else Vh.spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(0, 0, multi_index) + + def edge_moment_index(p, i, axis, ext): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(0, 0, multi_index) + + + 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 + + + # boundary condition + for bn in domain.boundary: + space_k = Vh + 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) + 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) + Proj_edge[pg, ig] = gamma[p] + else: + #if corner_indices.issuperset({ig}): + mu_minus = get_mu_minus( + i, 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) + Proj_edge[pg, ig] = gamma[p] * mu_minus[m] + + + return Proj_edge @ Proj_vertex + + +# =============================================================================== class ConformingProjection_V0(FemLinearOperator): """ @@ -1186,13 +1451,14 @@ def __init__( hom_bc=False): FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V0h) - - self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + + if V0h.is_multipatch: + self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + else: + self._sparse_matrix = construct_h1_singlepatch_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) -# =============================================================================== - class ConformingProjection_V1(FemLinearOperator): """ @@ -1222,9 +1488,11 @@ def __init__( FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V1h) - - self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - + if V1h.is_multipatch: + self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + else: + self._sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) From 312796937bf9f6bfeddfbce36557275f583f5b54 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 10:04:06 +0200 Subject: [PATCH 012/102] add callable mapping to DiscreteDeRhamMultipatch --- psydac/api/discretization.py | 4 +--- psydac/api/feec.py | 25 +++++++++---------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 88257cdc2..f637d66dc 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -180,7 +180,6 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): """ ldim = derham.shape - mapping = domain_h.domain.mapping # NOTE: assuming single-patch domain! bases = ['B'] + ldim * ['M'] spaces = [discretize_space(V, domain_h, basis=basis, **kwargs) for V, basis in zip(derham.spaces, bases)] @@ -193,7 +192,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #We still need to specify the symbolic space because of "_recursive_element_of" not implemented in sympde spaces.append(Xh) - return DiscreteDerham(mapping, domain_h, *spaces) + return DiscreteDerham(domain_h, *spaces) #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): @@ -206,7 +205,6 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): for V, basis in zip(derham.spaces, bases)] return DiscreteDerhamMultipatch( - mapping = mapping, domain_h = domain_h, spaces = spaces ) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 3508fce2d..2fd98de0e 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -32,8 +32,8 @@ class DiscreteDerham(BasicDiscrete): Parameters ---------- - mapping : Mapping or None - Symbolic mapping from the logical space to the physical space, if any. + domain_h : Geometry + The discretized domain, which is a single-patch geometry. *spaces : list of FemSpace The discrete spaces of the de Rham sequence. @@ -49,9 +49,8 @@ class DiscreteDerham(BasicDiscrete): - For the multipatch counterpart of this class please see `MultipatchDiscreteDerham` in `psydac.feec.multipatch.api`. """ - def __init__(self, mapping, domain_h, *spaces): + def __init__(self, domain_h, *spaces): - assert (mapping is None) or isinstance(mapping, Mapping) assert all(isinstance(space, FemSpace) for space in spaces) self.has_vec = isinstance(spaces[-1], VectorFemSpace) @@ -64,12 +63,12 @@ def __init__(self, mapping, domain_h, *spaces): else : dim = len(spaces) - 1 self._spaces = spaces - + self._domain_h = domain_h self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) self._dim = dim - self._mapping = mapping - self._callable_mapping = mapping.get_callable_mapping() if mapping else None + self._mapping = domain_h.domain.mapping + self._callable_mapping = mapping.get_callable_mapping() if self._mapping else None if dim == 1: D0 = Derivative_1D(spaces[0], spaces[1]) @@ -479,9 +478,6 @@ class DiscreteDerhamMultipatch(DiscreteDerham): Parameters ---------- - mapping: - The mapping of the multipatch domain, the multipatch mapping contains the mapping of each patch - domain_h: The discrete domain @@ -492,12 +488,13 @@ class DiscreteDerhamMultipatch(DiscreteDerham): The space kind of each space in the De Rham sequence """ - def __init__(self, *, mapping, domain_h, spaces): + def __init__(self, *, domain_h, spaces): dim = len(spaces) - 1 self._spaces = tuple(spaces) self._dim = dim - self._mapping = mapping + self._mapping = domain_h.domain.mapping + self._callable_mapping = [m.get_callable_mapping() for m in self._mapping.mappings.values()] if self._mapping else None self._domain_h = domain_h self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) @@ -534,10 +531,6 @@ def __init__(self, *, mapping, domain_h, spaces): def H1vec(self): raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') - @property - def callable_mapping(self): - raise NotImplementedError('Not implemented for Multipatch de Rham sequences.') - #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): """ From 0e02d3abdbfaa5b1d9912cd917e4a6e5d9f59b60 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 10:55:28 +0200 Subject: [PATCH 013/102] restructured files in psydac/feec/multipatch --- psydac/api/feec.py | 51 ++++++------ ...projectors.py => conforming_projectors.py} | 74 ------------------ psydac/feec/derivatives.py | 77 ++++++++++++++++++- psydac/feec/global_projectors.py | 75 ++++++++++++++++++ .../{multipatch/operators.py => hodge.py} | 2 +- psydac/feec/multipatch/derivatives.py | 73 ------------------ 6 files changed, 177 insertions(+), 175 deletions(-) rename psydac/feec/{multipatch/projectors.py => conforming_projectors.py} (95%) rename psydac/feec/{multipatch/operators.py => hodge.py} (99%) delete mode 100644 psydac/feec/multipatch/derivatives.py diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 2fd98de0e..d46bddca4 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,28 +1,31 @@ import os -from sympde.topology.mapping import Mapping - -from psydac.api.basic import BasicDiscrete -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_H1vec -from psydac.feec.global_projectors import Projector_Hdiv, Projector_L2 -from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 -from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec -from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl, pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec -from psydac.fem.basic import FemSpace, FemLinearOperator -from psydac.fem.vector import VectorFemSpace - -from psydac.feec.multipatch.operators import HodgeOperator -from psydac.feec.multipatch.derivatives import BrokenGradient_2D -from psydac.feec.multipatch.derivatives import BrokenScalarCurl_2D -from psydac.feec.multipatch.projectors import Multipatch_Projector_H1 -from psydac.feec.multipatch.projectors import Multipatch_Projector_Hcurl -from psydac.feec.multipatch.projectors import Multipatch_Projector_L2 -from psydac.feec.multipatch.projectors import ConformingProjection_V0 -from psydac.feec.multipatch.projectors import ConformingProjection_V1 -from psydac.linalg.basic import IdentityOperator +from psydac.api.basic import BasicDiscrete + +from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D +from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D +from psydac.feec.derivatives import Divergence_2D, Divergence_3D +from psydac.feec.derivatives import BrokenGradient_2D +from psydac.feec.derivatives import BrokenScalarCurl_2D + +from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_H1vec +from psydac.feec.global_projectors import Projector_Hdiv, Projector_L2 +from psydac.feec.global_projectors import Multipatch_Projector_H1 +from psydac.feec.global_projectors import Multipatch_Projector_Hcurl +from psydac.feec.global_projectors import Multipatch_Projector_L2 + +from psydac.feec.conforming_projectors import ConformingProjection_V0 +from psydac.feec.conforming_projectors import ConformingProjection_V1 + +from psydac.feec.hodge import HodgeOperator + +from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 +from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec +from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl, pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec + +from psydac.fem.basic import FemSpace, FemLinearOperator +from psydac.fem.vector import VectorFemSpace +from psydac.linalg.basic import IdentityOperator __all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) @@ -68,7 +71,7 @@ def __init__(self, domain_h, *spaces): self._sequence = tuple(space.symbolic_space.kind.name for space in spaces) self._dim = dim self._mapping = domain_h.domain.mapping - self._callable_mapping = mapping.get_callable_mapping() if self._mapping else None + self._callable_mapping = self._mapping.get_callable_mapping() if self._mapping else None if dim == 1: D0 = Derivative_1D(spaces[0], spaces[1]) diff --git a/psydac/feec/multipatch/projectors.py b/psydac/feec/conforming_projectors.py similarity index 95% rename from psydac/feec/multipatch/projectors.py rename to psydac/feec/conforming_projectors.py index e3b526705..aadca3450 100644 --- a/psydac/feec/multipatch/projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1494,77 +1494,3 @@ def __init__( self._sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) - - -# =============================================================================== - -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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] - - u0_coeffs = BlockVector(self._V0h.coeff_space, - blocks=[u0j.coeffs for u0j in u0s]) - - 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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] - - E1_coeffs = BlockVector(self._V1h.coeff_space, - blocks=[E1j.coeffs for E1j in E1s]) - - 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 - - def __call__(self, funs_log): - """ - project a list of functions given in the logical domain - """ - B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] - - B2_coeffs = BlockVector(self._V2h.coeff_space, - blocks=[B2j.coeffs for B2j in B2s]) - - return FemField(self._V2h, coeffs=B2_coeffs) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index 90e573670..858838da9 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -9,10 +9,10 @@ from psydac.fem.vector import VectorFemSpace from psydac.fem.tensor import TensorFemSpace from psydac.linalg.basic import IdentityOperator -from psydac.fem.basic import FemField, FemSpace +from psydac.fem.basic import FemField, FemSpace, FemLinearOperator from psydac.linalg.basic import LinearOperator from psydac.ddm.cart import DomainDecomposition, CartDecomposition -from psydac.fem.basic import FemLinearOperator + __all__ = ( 'DirectionalDerivativeOperator', 'Derivative_1D', @@ -23,6 +23,10 @@ 'Curl_3D', 'Divergence_2D', 'Divergence_3D', + 'BrokenGradient_2D', + 'BrokenTransposedGradient_2D', + 'BrokenScalarCurl_2D', + 'BrokenTransposedScalarCurl_2D', 'block_tostencil' ) @@ -40,6 +44,8 @@ def block_tostencil(M): blocks[i1][i2] = mat.tostencil() return BlockLinearOperator(M.domain, M.codomain, blocks=blocks) +#==================================================================================================== +# Singlepatch derivative operators #==================================================================================================== class DirectionalDerivativeOperator(LinearOperator): """ @@ -422,7 +428,6 @@ def __init__(self, H1, Hcurl): # Store data in object super().__init__(H1, Hcurl, matrix) - #==================================================================================================== class Gradient_3D(FemLinearOperator): """ @@ -674,3 +679,69 @@ def __init__(self, Hdiv, L2): # Store data in object super().__init__(Hdiv, L2, matrix) + +#==================================================================================================== +# 2D Multipatch derivative operators +#==================================================================================================== +class BrokenGradient_2D(FemLinearOperator): + + def __init__(self, V0h, V1h): + + FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) + + D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop 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): + + 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D0i.linop.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): + + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) + + D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] + + self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop for i, D1i in enumerate(D1s)}) + + def transpose(self, conjugate=False): + return BrokenTransposedScalarCurl_2D( + V1h=self.fem_domain, V2h=self.fem_codomain) + + +# ============================================================================== +class BrokenTransposedScalarCurl_2D(FemLinearOperator): + + 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ + (i, i): D1i.linop.T for i, D1i in enumerate(D1s)}) + + def transpose(self, conjugate=False): + return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_projectors.py index e5a61a0bb..a9d899011 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_projectors.py @@ -18,6 +18,7 @@ from abc import ABCMeta, abstractmethod __all__ = ('GlobalProjector', 'Projector_H1', 'Projector_Hcurl', 'Projector_Hdiv', 'Projector_L2', + 'Multipatch_Projector_H1', 'Multipatch_Projector_Hcurl', 'Multipatch_Projector_L2', 'evaluate_dofs_1d_0form', 'evaluate_dofs_1d_1form', 'evaluate_dofs_2d_0form', 'evaluate_dofs_2d_1form_hcurl', 'evaluate_dofs_2d_1form_hdiv', 'evaluate_dofs_2d_2form', 'evaluate_dofs_3d_0form', 'evaluate_dofs_3d_1form', 'evaluate_dofs_3d_2form', 'evaluate_dofs_3d_3form') @@ -388,6 +389,8 @@ def __call__(self, fun): return FemField(self._space, coeffs=coeffs) +#============================================================================== +# SINGLEPATCH PROJECTORS #============================================================================== class Projector_H1(GlobalProjector): """ @@ -712,6 +715,78 @@ def __call__(self, fun): """ return super().__call__(fun) +#============================================================================== +# MULTIPATCH PROJECTORS +#============================================================================== +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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] + + u0_coeffs = BlockVector(self._V0h.coeff_space, + blocks=[u0j.coeffs for u0j in u0s]) + + 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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] + + E1_coeffs = BlockVector(self._V1h.coeff_space, + blocks=[E1j.coeffs for E1j in E1s]) + + 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 + + def __call__(self, funs_log): + """ + project a list of functions given in the logical domain + """ + B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] + + B2_coeffs = BlockVector(self._V2h.coeff_space, + blocks=[B2j.coeffs for B2j in B2s]) + + return FemField(self._V2h, coeffs=B2_coeffs) + #============================================================================== # 1D DEGREES OF FREEDOM #============================================================================== diff --git a/psydac/feec/multipatch/operators.py b/psydac/feec/hodge.py similarity index 99% rename from psydac/feec/multipatch/operators.py rename to psydac/feec/hodge.py index 5b0859385..1e28922f6 100644 --- a/psydac/feec/multipatch/operators.py +++ b/psydac/feec/hodge.py @@ -16,7 +16,7 @@ # from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D #from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator -from psydac.linalg.basic import LinearOperator +# from psydac.linalg.basic import LinearOperator from psydac.linalg.utilities import SparseMatrixLinearOperator # =============================================================================== diff --git a/psydac/feec/multipatch/derivatives.py b/psydac/feec/multipatch/derivatives.py deleted file mode 100644 index 74c461027..000000000 --- a/psydac/feec/multipatch/derivatives.py +++ /dev/null @@ -1,73 +0,0 @@ -import os -import numpy as np - -from psydac.linalg.block import BlockLinearOperator -from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D -from psydac.fem.basic import FemLinearOperator - -class BrokenGradient_2D(FemLinearOperator): - - def __init__(self, V0h, V1h): - - FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) - - D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] - - self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ - (i, i): D0i.linop 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): - - 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ - (i, i): D0i.linop.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): - - FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) - - D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] - - self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ - (i, i): D1i.linop for i, D1i in enumerate(D1s)}) - - def transpose(self, conjugate=False): - return BrokenTransposedScalarCurl_2D( - V1h=self.fem_domain, V2h=self.fem_codomain) - - -# ============================================================================== -class BrokenTransposedScalarCurl_2D(FemLinearOperator): - - 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._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ - (i, i): D1i.linop.T for i, D1i in enumerate(D1s)}) - - def transpose(self, conjugate=False): - return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) - -# ============================================================================== From 44fec7381e9d22d8bfeedc438e7b2f5f6ae297be Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 13:08:37 +0200 Subject: [PATCH 014/102] make conf proj test run --- psydac/api/feec.py | 2 +- psydac/feec/conforming_projectors.py | 21 ++--- psydac/feec/global_projectors.py | 5 +- psydac/feec/multipatch/utils_conga_2d.py | 44 +++++++-- .../test_feec_conf_projectors_cart_2d.py | 94 +++++++------------ 5 files changed, 85 insertions(+), 81 deletions(-) rename psydac/feec/{multipatch => }/tests/test_feec_conf_projectors_cart_2d.py (73%) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index d46bddca4..43fc99e07 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -344,7 +344,7 @@ def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): if kind == 'femlinop': return cP0, cP1, FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=cP2, sparse_matrix=cP2.tosparse) elif kind == 'sparse': - return cP0.tosparse, cP1.tosparse, cP2.tosparse + return cP0.tosparse, cP1.tosparse, cP2.tosparse() elif kind == 'linop': return cP0.linop, cP1.linop, cP2 diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index aadca3450..416c53f5e 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1,24 +1,19 @@ # coding: utf-8 - # Conga operators on piecewise (broken) de Rham sequences - import os import numpy as np -from sympy import Tuple -from scipy.sparse import eye as sparse_eye -from scipy.sparse import csr_matrix -from scipy.special import comb +from scipy.sparse import eye as sparse_eye +from scipy.sparse import csr_matrix +from scipy.special import comb from sympde.topology import Boundary, Interface -from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid -from psydac.linalg.block import BlockVector -from psydac.fem.basic import FemField, FemLinearOperator -from psydac.fem.splines import SplineSpace -from psydac.utilities.quadratures import gauss_legendre -from psydac.linalg.utilities import SparseMatrixLinearOperator -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_L2 +from psydac.core.bsplines import quadrature_grid, basis_ders_on_quad_grid, find_spans, elements_spans, cell_index, basis_ders_on_irregular_grid +from psydac.fem.basic import FemLinearOperator +from psydac.fem.splines import SplineSpace +from psydac.utilities.quadratures import gauss_legendre +from psydac.linalg.utilities import SparseMatrixLinearOperator def get_patch_index_from_face(domain, face): diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_projectors.py index a9d899011..2a7f1a5e7 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_projectors.py @@ -4,7 +4,7 @@ from psydac.linalg.kron import KroneckerLinearSolver, KroneckerStencilMatrix from psydac.linalg.stencil import StencilMatrix, StencilVectorSpace -from psydac.linalg.block import BlockLinearOperator +from psydac.linalg.block import BlockLinearOperator, BlockVector from psydac.core.bsplines import quadrature_grid from psydac.utilities.quadratures import gauss_legendre from psydac.fem.basic import FemField @@ -651,6 +651,7 @@ def __call__(self, fun): """ return super().__call__(fun) +#============================================================================== class Projector_H1vec(GlobalProjector): """ Projector from H1^3 = H1 x H1 x H1 to a conforming finite element space, i.e. @@ -740,7 +741,6 @@ def __call__(self, funs_log): return FemField(self._V0h, coeffs=u0_coeffs) #============================================================================== - class Multipatch_Projector_Hcurl: """ @@ -764,7 +764,6 @@ def __call__(self, funs_log): return FemField(self._V1h, coeffs=E1_coeffs) #============================================================================== - class Multipatch_Projector_L2: """ diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index decbc0705..575ff680a 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -5,6 +5,7 @@ from sympy import lambdify from sympde.topology import Derham +from sympde.topology.callable_mapping import BasicCallableMapping from psydac.api.settings import PSYDAC_BACKENDS from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_l2 @@ -47,31 +48,62 @@ def P2_phys(f_phys, P2, domain, mappings_list): 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) + + if isinstance(mappings_list, BasicCallableMapping): + f_log = pull_2d_h1(f, mappings_list) + + elif len(mappings_list) == 1: + f_log = pull_2d_h1(f, mappings_list[0]) + 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] + + if isinstance(mappings_list, BasicCallableMapping): + f_log = pull_2d_hcurl([f_x, f_y], mappings_list) + + elif len(mappings_list) == 1: + f_log = pull_2d_hcurl([f_x, f_y], mappings_list[0]) + else: + 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] + + if isinstance(mappings_list, BasicCallableMapping): + f_log = pull_2d_hdiv([f_x, f_y], mappings_list) + + elif len(mappings_list) == 1: + f_log = pull_2d_hdiv([f_x, f_y], mappings_list[0]) + + else: + 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] + + if isinstance(mappings_list, BasicCallableMapping): + f_log = pull_2d_l2(f, mappings_list) + + elif len(mappings_list) == 1: + f_log = pull_2d_l2(f, mappings_list[0]) + + else: + f_log = [pull_2d_l2(f, m) for m in mappings_list] + return P2(f_log) diff --git a/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py similarity index 73% rename from psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py rename to psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 3d0dd85ff..24f0fb0b4 100644 --- a/psydac/feec/multipatch/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -8,11 +8,10 @@ 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.api.discretization import discretize from psydac.feec.multipatch.utils_conga_2d import P_phys_l2, P_phys_hdiv, P_phys_hcurl, P_phys_h1 +from psydac.fem.projectors import get_dual_dofs def get_polynomial_function(degree, hom_bc_axes, domain): """Return a polynomial function of given degree and homogeneus boundary conditions on the domain.""" @@ -46,10 +45,10 @@ def get_polynomial_function(degree, hom_bc_axes, domain): @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('domain_name', ["1patch", "4patch_nc", "2patch_nc"]) @pytest.mark.parametrize("nonconforming, full_mom_pres", [(True, True), (False, True)]) -# NOTE (MCP march 2025): momentum conservation fails for nc = 4 and degree = 3, why? + def test_conf_projectors_2d( V1_type, degree, @@ -60,8 +59,12 @@ def test_conf_projectors_2d( domain_name, nonconforming ): + if domain_name == '1patch': + log_domain = Square('Omega', bounds1=(0, 1), bounds2=(0, 1)) + mapping = IdentityMapping('M1', dim=2) + domain = mapping(log_domain) - if domain_name == '2patch_nc': + elif domain_name == '2patch_nc': A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B', bounds1=(0.5, 1.), bounds2=(0, 1)) @@ -95,8 +98,10 @@ def test_conf_projectors_2d( ((0, 1, 1), (2, 1, -1), 1), ((1, 1, 1), (3, 1, -1), 1)], name='domain') + if domain_name == '1patch': + ncells_h = {domain.name: [nc, nc]} - if nonconforming: + elif nonconforming: if len(domain) == 2: ncells_h = { 'M1(A)': [nc, nc], @@ -115,24 +120,20 @@ def test_conf_projectors_2d( for k, D in enumerate(domain.interior): ncells_h[D.name] = [nc, nc] - derham = Derham(domain, ["H1", "Hcurl", "L2"]) + # 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()] + # derham_h = discretize(derham, domain_h, degree=degree) + # V0h, V1h, V2h = derham_h.spaces + # mappings_list = derham_h.callable_mapping + 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 + p_V0h, p_V1h, p_V2h = p_derham_h.spaces + mappings_list = p_derham_h.callable_mapping #if p_V0h.is_multipatch else [p_derham_h.callable_mapping] + # full moment preservation only possible if enough interior functions in a # patch (<=> enough cells) @@ -148,35 +149,19 @@ def test_conf_projectors_2d( 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 + cP0, cP1, cP2 = p_derham_h.conforming_projectors(kind='sparse', p_moments=mom_pres, hom_bc=hom_bc) - HOp1 = HodgeOperator(p_V1h, domain_h) - M1 = HOp1.to_sparse_matrix() # mass matrix - M1_inv = HOp1.get_dual_Hodge_sparse_matrix() # inverse mass matrix + M0, M1, M2 = p_derham_h.Hodge_operators(kind='sparse')#, backend_language=backend_language, load_dir=m_load_dir) + M0_inv, M1_inv, M2_inv = p_derham_h.Hodge_operators(kind='sparse', dual=True)#, backend_language=backend_language, load_dir=m_load_dir) - 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.derivatives(kind='sparse') - 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 + 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) @@ -186,14 +171,11 @@ def test_conf_projectors_2d( # 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) + 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') + tilde_g0_c = get_dual_dofs(Vh=p_V0h, f=g0, domain_h = domain_h, return_format='numpy_array') g0_L2_c = M0_inv @ tilde_g0_c # (P0_geom - P0_L2) polynomial = 0 @@ -206,13 +188,12 @@ def test_conf_projectors_2d( # 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) + 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') + tilde_g0_c = get_dual_dofs(Vh=p_V0h, f=g0, domain_h = domain_h, 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) @@ -244,8 +225,8 @@ def test_conf_projectors_2d( 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') + tilde_G1_c = get_dual_dofs(Vh=p_V1h, f=G1, domain_h=domain_h, return_format='numpy_array') + G1_L2_c = M1_inv @ tilde_G1_c assert np.allclose(G1_c, G1_L2_c, 1e-12, 1e-12) @@ -276,8 +257,7 @@ def test_conf_projectors_2d( 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') + tilde_G1_c = get_dual_dofs(Vh=p_V1h, f=G1, domain_h=domain_h, 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) @@ -294,8 +274,7 @@ def test_conf_projectors_2d( 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') + tilde_g2_c = get_dual_dofs(Vh=p_V2h, f=g2, domain_h=domain_h, return_format='numpy_array') g2_L2_c = M2_inv @ tilde_g2_c # (P2_geom - P2_L2) polynomial = 0 @@ -305,7 +284,6 @@ def test_conf_projectors_2d( 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) From a67d1d94d0de5718deef39d1a235d69d79baff17 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 14:45:14 +0200 Subject: [PATCH 015/102] fix notation in old tests. disable multipatch tests for now --- psydac/api/tests/test_api_feec_1d.py | 2 +- psydac/api/tests/test_api_feec_2d.py | 2 +- psydac/api/tests/test_api_feec_3d.py | 4 ++-- psydac/api/tests/test_assembly.py | 2 +- .../tests/test_feec_maxwell_multipatch_2d.py | 21 ++++++++++--------- .../tests/test_feec_poisson_multipatch_2d.py | 7 ++++--- psydac/feec/tests/test_axis_projection.py | 6 +++--- .../tests/test_commuting_projections_dual.py | 6 +++--- .../test_feec_conf_projectors_cart_2d.py | 7 +------ 9 files changed, 27 insertions(+), 30 deletions(-) diff --git a/psydac/api/tests/test_api_feec_1d.py b/psydac/api/tests/test_api_feec_1d.py index ad60a2c02..fd7e7f7dd 100644 --- a/psydac/api/tests/test_api_feec_1d.py +++ b/psydac/api/tests/test_api_feec_1d.py @@ -145,7 +145,7 @@ class CollelaMapping1D(Mapping): M1 = a1_h.assemble() # Differential operators - D0, = derham_h.derivatives_as_matrices + D0, = derham_h.derivatives(kind='linop') # Transpose of derivative matrix D0_T = D0.T diff --git a/psydac/api/tests/test_api_feec_2d.py b/psydac/api/tests/test_api_feec_2d.py index 40b607615..c221af521 100644 --- a/psydac/api/tests/test_api_feec_2d.py +++ b/psydac/api/tests/test_api_feec_2d.py @@ -325,7 +325,7 @@ class CollelaMapping2D(Mapping): M2 = a2_h.assemble() # Differential operators (StencilMatrix or BlockLinearOperator objects) - D0, D1 = derham_h.derivatives_as_matrices + D0, D1 = derham_h.derivatives(kind='linop') # Discretize and assemble penalization matrix if not periodic: diff --git a/psydac/api/tests/test_api_feec_3d.py b/psydac/api/tests/test_api_feec_3d.py index d5220b5bc..90012ccab 100644 --- a/psydac/api/tests/test_api_feec_3d.py +++ b/psydac/api/tests/test_api_feec_3d.py @@ -123,7 +123,7 @@ def run_maxwell_3d_scipy(logical_domain, mapping, e_ex, b_ex, ncells, degree, pe M2 = a2_h.assemble().tosparse().tocsr() # Diff operators - GRAD, CURL, DIV = derham_h.derivatives_as_matrices + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') # Porjectors P0, P1, P2, P3 = derham_h.projectors(nquads=[5,5,5]) @@ -218,7 +218,7 @@ def run_maxwell_3d_stencil(logical_domain, mapping, e_ex, b_ex, ncells, degree, M2 = a2_h.assemble() # Diff operators - GRAD, CURL, DIV = derham_h.derivatives_as_matrices + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') # Porjectors P0, P1, P2, P3 = derham_h.projectors(nquads=[5,5,5]) diff --git a/psydac/api/tests/test_assembly.py b/psydac/api/tests/test_assembly.py index a84a41a2b..189b6c102 100644 --- a/psydac/api/tests/test_assembly.py +++ b/psydac/api/tests/test_assembly.py @@ -544,7 +544,7 @@ def test_assembly_no_synchr_args(backend): V1h = derham_h.V1 #differential operator - div, = derham_h.derivatives_as_matrices + div, = derham_h.derivatives(kind='linop') rho = element_of(V1h.symbolic_space, name='rho') g = element_of(V1h.symbolic_space, name='g') 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 bb1b09004..62312a39f 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -1,13 +1,14 @@ # coding: utf-8 import numpy as np +import pytest -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 - +# 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 +@pytest.mark.skip(reason="need to adapt notation") def test_time_harmonic_maxwell_pretzel_f(): nc = 4 deg = 2 @@ -31,7 +32,7 @@ def test_time_harmonic_maxwell_pretzel_f(): assert abs(diags["err"] - 0.007201508128407582) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_time_harmonic_maxwell_pretzel_f_nc(): deg = 2 nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, @@ -56,7 +57,7 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): assert abs(diags["err"] - 0.004849165663310541) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] @@ -99,7 +100,7 @@ def test_maxwell_eigen_curved_L_shape(): assert abs(error - 0.01291539899483907) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape_nc(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] @@ -144,7 +145,7 @@ def test_maxwell_eigen_curved_L_shape_nc(): assert abs(error - 0.010504876643873904) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape_dg(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] @@ -187,7 +188,7 @@ def test_maxwell_eigen_curved_L_shape_dg(): assert abs(error - 0.035139029534570064) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_timedomain(): solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') 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 7a0d94cbb..2a9a4ce1e 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -1,8 +1,9 @@ import numpy as np +import pytest -from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm - +# from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm +@pytest.mark.skip(reason="need to adapt notation") def test_poisson_pretzel_f(): source_type = 'manu_poisson_2' @@ -21,7 +22,7 @@ def test_poisson_pretzel_f(): assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 - +@pytest.mark.skip(reason="need to adapt notation") def test_poisson_pretzel_f_nc(): source_type = 'manu_poisson_2' diff --git a/psydac/feec/tests/test_axis_projection.py b/psydac/feec/tests/test_axis_projection.py index 61e1dfb4f..7a81f0953 100644 --- a/psydac/feec/tests/test_axis_projection.py +++ b/psydac/feec/tests/test_axis_projection.py @@ -1,6 +1,6 @@ -from sympde.topology import Square, Derham, element_of -from sympde.expr.expr import BilinearForm, integral -from psydac.feec.multipatch.api import discretize +from sympde.topology import Square, Derham, element_of +from sympde.expr.expr import BilinearForm, integral +from psydac.api.discretization import discretize from psydac.api.settings import PSYDAC_BACKENDS def test_axis_projection(): diff --git a/psydac/feec/tests/test_commuting_projections_dual.py b/psydac/feec/tests/test_commuting_projections_dual.py index 944915bc4..5bc1a68a7 100644 --- a/psydac/feec/tests/test_commuting_projections_dual.py +++ b/psydac/feec/tests/test_commuting_projections_dual.py @@ -47,7 +47,7 @@ def test_transpose_div_3d(Nel, Nq, p, bc, m): u2 = discretize(f2, domain_h, derham_h.V2, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u3 = discretize(f3, domain_h, derham_h.V3, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - divT_u3 = - div.matrix.T.dot(u3) + divT_u3 = - div.linop.T.dot(u3) error = abs((u2-divT_u3).toarray()).max() assert error < 2e-10 @@ -105,7 +105,7 @@ def test_transpose_curl_3d(Nel, Nq, p, bc, m): u1 = discretize(f1, domain_h, derham_h.V1, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u2 = discretize(f2, domain_h, derham_h.V2, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - curlT_u2 = curl.matrix.T.dot(u2) + curlT_u2 = curl.linop.T.dot(u2) error = abs((u1-curlT_u2).toarray()).max() assert error < 2e-9 @@ -152,7 +152,7 @@ def test_transpose_grad_3d(Nel, Nq, p, bc, m): u0 = discretize(f0, domain_h, derham_h.V0, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() u1 = discretize(f1, domain_h, derham_h.V1, nquads=Nq, backend=PSYDAC_BACKENDS['pyccel-gcc']).assemble() - gradT_u1 = -grad.matrix.T.dot(u1) + gradT_u1 = -grad.linop.T.dot(u1) error = abs((u0-gradT_u1).toarray()).max() assert error < 5e-10 diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 24f0fb0b4..42c630043 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -120,19 +120,14 @@ def test_conf_projectors_2d( 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, V1h, V2h = derham_h.spaces - # mappings_list = derham_h.callable_mapping - 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_V1h, p_V2h = p_derham_h.spaces - mappings_list = p_derham_h.callable_mapping #if p_V0h.is_multipatch else [p_derham_h.callable_mapping] + mappings_list = p_derham_h.callable_mapping # full moment preservation only possible if enough interior functions in a From 20df3abe43791d21023fb67a44a9365c2895d145 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 17:01:02 +0200 Subject: [PATCH 016/102] make the conforming projections more consistent --- psydac/api/feec.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 43fc99e07..def2857c9 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -43,14 +43,8 @@ class DiscreteDerham(BasicDiscrete): Notes ----- - - The basic type Mapping is defined in module sympde.topology.mapping. - A discrete mapping (spline or NURBS) may be attached to it. - - This constructor should not be called directly, but rather from the `discretize_derham` function in `psydac.api.discretization`. - - - For the multipatch counterpart of this class please see - `MultipatchDiscreteDerham` in `psydac.feec.multipatch.api`. """ def __init__(self, domain_h, *spaces): @@ -332,9 +326,11 @@ def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): raise NotImplementedError('2D sequence with H-div not available yet') else: - cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc)#, storage_fn=storage_fn) - cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc)#, storage_fn=storage_fn) - cP2 = IdentityOperator(self.V2.coeff_space) # no storage needed! + cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc) + cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc) + + I2 = IdentityOperator(self.V2.coeff_space) + cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2, sparse_matrix=I2.tosparse()) self._conf_proj = (cP0, cP1, cP2) @@ -342,11 +338,11 @@ def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): raise NotImplementedError("3D projectors are not available") if kind == 'femlinop': - return cP0, cP1, FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=cP2, sparse_matrix=cP2.tosparse) + return cP0, cP1, cP2 elif kind == 'sparse': - return cP0.tosparse, cP1.tosparse, cP2.tosparse() + return cP0.tosparse, cP1.tosparse, cP2.tosparse elif kind == 'linop': - return cP0.linop, cP1.linop, cP2 + return cP0.linop, cP1.linop, cP2.linop #-------------------------------------------------------------------------- def _init_Hodge_operators(self, backend_language='python', load_dir=None): @@ -477,7 +473,7 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu #============================================================================== class DiscreteDerhamMultipatch(DiscreteDerham): """ Represents the discrete De Rham sequence for multipatch domains. - It only works when the number of patches>1 + It only works when the number of patches>1. Parameters ---------- @@ -486,9 +482,6 @@ class DiscreteDerhamMultipatch(DiscreteDerham): spaces: The discrete spaces that are contained in the De Rham sequence - - sequence: - The space kind of each space in the De Rham sequence """ def __init__(self, *, domain_h, spaces): From cf877f5570414ba65c517176d6f29726cc2498ac Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 17:23:01 +0200 Subject: [PATCH 017/102] add doc strings to discretize derham multipatch --- psydac/api/discretization.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index f637d66dc..f1b354660 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -176,7 +176,6 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): See Also -------- discretize_space - """ ldim = derham.shape @@ -196,10 +195,36 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): + """ + Create a discrete multipatch De Rham sequence from a symbolic one. + + This function creates the broken discrete spaces from the symbolic ones, and then + creates a DiscreteDerhamMultipatch object from them. - ldim = derham.shape - mapping = derham.spaces[0].domain.mapping + Parameters + ---------- + derham : sympde.topology.space.Derham + The symbolic Derham sequence. + + domain_h : Geometry + Discrete domain where the spaces will be discretized. + + **kwargs : dict + Optional parameters for the space discretization. + + Returns + ------- + DiscreteDerhamMultipatch + The discrete multipatch De Rham sequence containing the discrete spaces, + differential operators and projectors. + + See Also + -------- + discretize_derham + discretize_space + """ + ldim = derham.shape bases = ['B'] + ldim * ['M'] spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ for V, basis in zip(derham.spaces, bases)] From 87e8f2f826add04e0ad729ca17ee757b41672176 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 14 Jul 2025 17:30:04 +0200 Subject: [PATCH 018/102] fix indentation --- psydac/api/discretization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index f1b354660..1dc60aa74 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -195,7 +195,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): - """ + """ Create a discrete multipatch De Rham sequence from a symbolic one. This function creates the broken discrete spaces from the symbolic ones, and then From b9edf1df3aa3019b65fe8ee0e6e90d5bb2ed7e0b Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Tue, 15 Jul 2025 15:59:21 +0200 Subject: [PATCH 019/102] make the multipatch examples run for the tests --- psydac/feec/conforming_projectors.py | 12 +- psydac/feec/global_projectors.py | 2 +- .../examples/h1_source_pbms_conga_2d.py | 162 ++++++-------- .../examples/hcurl_eigen_pbms_conga_2d.py | 96 ++------ .../examples/hcurl_eigen_pbms_dg_2d.py | 9 +- .../examples/hcurl_source_pbms_conga_2d.py | 208 +++++++----------- .../multipatch/examples/ppc_test_cases.py | 13 +- .../tests/test_feec_maxwell_multipatch_2d.py | 13 +- .../tests/test_feec_poisson_multipatch_2d.py | 6 +- .../test_feec_conf_projectors_cart_2d.py | 2 +- 10 files changed, 183 insertions(+), 340 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 416c53f5e..f9923f141 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -502,16 +502,12 @@ def get_1d_moment_correction(space_1d, p_moments=-1): # local_shape[conf_axis]-2*(1+reg) such conforming functions p_max = space_1d.nbasis - 3 if p_max < p_moments: - print( - " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + 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" ** 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( - " ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") + print(" ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **") p_moments = p_max Mass_mat = calculate_poly_basis_integral(space_1d, p_moments) diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_projectors.py index 2a7f1a5e7..e219c3fbf 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_projectors.py @@ -717,7 +717,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -# MULTIPATCH PROJECTORS +# MULTIPATCH PROJECTORS (2D) #============================================================================== class Multipatch_Projector_H1: """ 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 7b1e33792..656d91143 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -12,38 +12,21 @@ V0h --grad-> V1h -—curl-> V2h """ - -from mpi4py import MPI - import os import numpy as np -from collections import OrderedDict - -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 element_of -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.multipatch.utils_conga_2d import P0_phys +from psydac.api.discretization import discretize +from psydac.linalg.basic import IdentityOperator +from psydac.linalg.solvers import inverse -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.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.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_h1 -from psydac.linalg.utilities import array_to_psydac -from psydac.fem.basic import FemField +from psydac.fem.projectors import get_dual_dofs +from psydac.fem.basic import FemField from psydac.api.postprocessing import OutputManager, PostProcessManager @@ -98,9 +81,6 @@ def solve_h1_source_pbm( 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_list = list(mappings.values()) if isinstance(nc, int): ncells = [nc, nc] @@ -114,102 +94,89 @@ def solve_h1_source_pbm( derham = Derham(domain, ["H1", "Hcurl", "L2"]) derham_h = discretize(derham, domain_h, degree=degree) - # multi-patch (broken) spaces - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 + V0h, V1h, V2h = derham_h.spaces print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) print('broken differential operators...') # broken (patch-wise) differential operators - bD0, bD1 = derham_h.broken_derivatives_as_operators - bD0_m = bD0.to_sparse_matrix() - - 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) - - I0 = IdLinearOperator(V0h) - I0_m = I0.to_sparse_matrix() + bD0, bD1 = derham_h.derivatives(kind='linop') print('Hodge operators...') - # multi-patch (broken) linear operators / matrices - H0 = HodgeOperator(V0h, domain_h, backend_language=backend_language) - H1 = HodgeOperator(V1h, domain_h, backend_language=backend_language) - - 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 + # multi-patch (broken) linear operators + H0 = derham_h.Hodge_operators(space='V0', kind='linop', backend_language=backend_language) + H1 = derham_h.Hodge_operators(space='V1', kind='linop', backend_language=backend_language) + dH0 = derham_h.Hodge_operators(space='V0', kind='linop', dual=True, backend_language=backend_language) 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) - - def lift_u_bc(u_bc): - if u_bc is not None: - print('lifting the boundary condition in V0h... [warning: Not Tested Yet!]') - 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) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) - ubc_c = ubc_c - cP0_m.dot(ubc_c) - else: - ubc_c = None - return ubc_c + print('building the discrete operators:') - # Conga (projection-based) stiffness matrices: - # div grad: - pre_DG_m = - bD0_m.transpose() @ H1_m @ bD0_m + I0 = IdentityOperator(V0h.coeff_space) + + # div grad + DG = - bD0.T @ H1 @ bD0 # jump penalization: - jump_penal_m = I0_m - cP0_m - JP0_m = jump_penal_m.transpose() @ H0_m @ jump_penal_m + JP0 = (I0 - cP0).T @ H0 @ (I0 - cP0) # 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 + pre_A = cP0.T @ (eta * H0 - mu * DG) + + A = pre_A @ cP0 + gamma_h * JP0 + + + 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,) - print('getting the source and ref 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, - ) + df = get_dual_dofs(Vh=V0h, f=f_scal, domain_h=domain_h, backend_language=backend_language) + f = dH0.dot(df) + df = cP0.T @ df - # compute approximate source f_h - 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 + def lift_u_bc(u_bc): + if u_bc is not None: + du_bc = get_dual_dofs(Vh=V0h, f=u_bc, domain_h = domain_h, backend_language=backend_language) + u_bc = dH0.dot(du_bc) + u_bc = u_bc - cP0.dot(u_bc) - ubc_c = lift_u_bc(u_bc) + else: + u_bc = None + + return u_bc - if ubc_c is not None: + ubc = lift_u_bc(u_bc) + + if ubc is not None: # modified source for the homogeneous pbm print('modifying the source with lifted bc solution...') - b_c = b_c - pre_A_m.dot(ubc_c) + df = df - pre_A.dot(ubc) # direct solve with scipy spsolve - print('solving source problem with scipy.spsolve...') - uh_c = spsolve(A_m, b_c) + print('solving source problem with conjugate gradient...') + solver = inverse(A, solver='cg', tol=1e-8) + u = solver.solve(df) # project the homogeneous solution on the conforming problem space print('projecting the homogeneous solution on the conforming problem space...') - uh_c = cP0_m.dot(uh_c) + u = cP0.dot(u) - if ubc_c is not None: + if ubc is not None: # adding the lifted boundary condition print('adding the lifted boundary condition...') - uh_c += ubc_c + u += ubc - print('getting and plotting the FEM solution from numpy coefs array...') if u_ex: - 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) + u_ex = get_dual_dofs(Vh=V0h, f=u_ex, domain_h=domain_h, backend_language=backend_language) + u_ex = dH0.dot(u_ex) + if plot_dir is not None: + print('plotting the FEM solution...') + if not os.path.exists(plot_dir): os.makedirs(plot_dir) @@ -217,17 +184,14 @@ def lift_u_bc(u_bc): OM.add_spaces(V0h=V0h) OM.set_static() - stencil_coeffs = array_to_psydac(uh_c, V0h.coeff_space) - vh = FemField(V0h, coeffs=stencil_coeffs) - OM.export_fields(vh=vh) + uh = FemField(V0h, coeffs=u) + OM.export_fields(uh=uh) - stencil_coeffs = array_to_psydac(f_c, V0h.coeff_space) - fh = FemField(V0h, coeffs=stencil_coeffs) + fh = FemField(V0h, coeffs=f) OM.export_fields(fh=fh) if u_ex: - stencil_coeffs = array_to_psydac(u_ex_c, V0h.coeff_space) - uh_ex = FemField(V0h, coeffs=stencil_coeffs) + uh_ex = FemField(V0h, coeffs=u_ex) OM.export_fields(uh_ex=uh_ex) OM.export_space_info() @@ -243,7 +207,7 @@ def lift_u_bc(u_bc): grid=None, npts_per_cell=[6] * 2, snapshots='all', - fields='vh') + fields='uh') PM.export_to_vtk( plot_dir + "/f_h", @@ -263,9 +227,9 @@ def lift_u_bc(u_bc): PM.close() 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))) - + err = u - u_ex + rel_err = np.sqrt( err.inner(H0.dot(err))) / np.sqrt(u_ex.inner(H0.dot(u_ex))) + return rel_err @@ -273,19 +237,21 @@ def lift_u_bc(u_bc): omega = np.sqrt(170) # source eta = -omega**2 + mu=0 + gamma_h = 10 source_type = 'manu_poisson_elliptic' domain_name = 'pretzel_f' - nc = 10 + nc = 4 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=mu, # 1, domain_name=domain_name, source_type=source_type, backend_language='pyccel-gcc', 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 db54565c7..2009941eb 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -2,34 +2,25 @@ Solve the eigenvalue problem for the curl-curl operator in 2D with a FEEC discretization """ import os -from mpi4py import MPI - import numpy as np -import matplotlib.pyplot as plt -from collections import OrderedDict + from sympde.topology import Derham -from psydac.feec.multipatch.api import discretize +from psydac.api.discretization 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.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.feec.multipatch.utilities import time_count -from sympde.topology import Square -from sympde.topology import IdentityMapping, PolarMapping 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 +from psydac.linalg.basic import IdentityOperator from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField 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 from psydac.api.postprocessing import OutputManager, PostProcessManager @@ -110,10 +101,6 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma 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...') @@ -128,9 +115,7 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma print(' .. discrete derham sequence...') derham_h = discretize(derham, domain_h, degree=degree) - V0h = derham_h.V0 - V1h = derham_h.V1 - V2h = derham_h.V2 + V0h, V1h, V2h = derham_h.spaces print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) @@ -142,64 +127,30 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma print('building the discrete operators:') print('commuting projection operators...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) 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) - - 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 + H0, H1, H2 = derham_h.Hodge_operators(kind='linop', backend_language=backend_language, load_dir=m_load_dir) + dH0, dH1, dH2 = derham_h.Hodge_operators(kind='linop', dual=True, backend_language=backend_language, load_dir=m_load_dir) 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_m = construct_h1_conforming_projection(V0h, hom_bc=True) - cP1_m = construct_hcurl_conforming_projection(V1h, hom_bc=True) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', 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...') + print('broken differential operators...') + bD0, bD1 = derham_h.derivatives(kind='linop') - 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) print('computing the full operator matrix...') - A_m = np.zeros_like(H1_m) # Conga (projection-based) stiffness matrices if mu != 0: @@ -208,33 +159,29 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma 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 + CC = cP1.T @ bD1.T @ H2 @ bD1 @ cP1 # Conga stiffness matrix + A = mu * CC 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 + GD = - cP1.T @ H1 @ bD0 @ cP0 @ dH0 @ cP0.T @ bD0.T @ H1 @ cP1 + A -= nu * GD # 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 + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + A += gamma_h * JS if generalized_pbm: print('adding jump stabilization to RHS of generalized eigenproblem...') - B_m = cP1_m.transpose() @ H1_m @ cP1_m + JS_m + B = cP1.T @ H1 @ cP1 + JS else: - B_m = H1_m + B = H1 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) + all_eigenvalues, all_eigenvectors_transp = get_eigenvalues(nb_eigs_solve, sigma, A.tosparse(), B.tosparse()) # Eigenvalue processing t_stamp = time_count(t_stamp) print('sorting out eigenvalues...') @@ -267,6 +214,9 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma OM.export_space_info() nb_eigs = len(eigenvalues) + H1_m = H1.tosparse() + cP1_m = cP1.tosparse() + for i in range(min(nb_eigs_plot, nb_eigs)): print('looking at emode i = {}... '.format(i)) diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py index 58c6a2bb6..710c2c1b4 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py @@ -3,14 +3,13 @@ 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 @@ -26,14 +25,12 @@ 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.utilities import time_count +from psydac.api.discretization import discretize from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain from psydac.api.postprocessing import OutputManager, PostProcessManager 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 b7442e054..286668771 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -14,40 +14,30 @@ """ import os -from mpi4py import MPI import numpy as np -from collections import OrderedDict -from sympy import lambdify, Matrix - -from scipy.sparse.linalg import spsolve - -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 +from psydac.api.discretization import discretize from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain 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.utils_conga_2d import 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.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 +from psydac.feec.multipatch.utils_conga_2d import P_phys_hcurl + +from psydac.linalg.basic import IdentityOperator +from psydac.fem.projectors import get_dual_dofs +from psydac.linalg.solvers import inverse def solve_hcurl_source_pbm( - nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='P_geom', source_type='manu_J', + nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='tilde_Pi', source_type='manu_J', eta=-10., mu=1., nu=1., gamma_h=10., - project_sol=False, plot_dir=None, + project_sol=True, plot_dir=None, m_load_dir=None, ): """ @@ -107,9 +97,9 @@ def solve_hcurl_source_pbm( t_stamp = time_count() 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_list = list(mappings.values()) + # mappings = OrderedDict([(P.logical_domain, P.mapping) + # for P in domain.interior]) + # mappings_list = list(mappings.values()) if isinstance(nc, int): ncells = [nc, nc] @@ -117,8 +107,6 @@ def solve_hcurl_source_pbm( 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(' .. derham sequence...') @@ -134,14 +122,14 @@ def solve_hcurl_source_pbm( t_stamp = time_count(t_stamp) print(' .. commuting projection operators...') - nquads = [4 * (d + 1) for d in degree] + nquads = [10 * (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 + V0h, V1h, V2h = derham_h.spaces + mappings = derham_h.callable_mapping + print('dim(V0h) = {}'.format(V0h.nbasis)) print('dim(V1h) = {}'.format(V1h.nbasis)) print('dim(V2h) = {}'.format(V2h.nbasis)) @@ -151,101 +139,44 @@ def solve_hcurl_source_pbm( t_stamp = time_count(t_stamp) print(' .. Id operator and matrix...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) 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_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, H1, H2 = derham_h.Hodge_operators(kind='linop', backend_language=backend_language) + dH0, dH1, dH2 = derham_h.Hodge_operators(kind='linop', dual=True, backend_language=backend_language) - 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 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(' .. 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) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) + 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) - - 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 - 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 ...) - ubc_c = ubc_c - cP1_m.dot(ubc_c) - else: - ubc_c = None - return ubc_c + bD0, bD1 = derham_h.derivatives(kind='linop') # Conga (projection-based) stiffness matrices # curl curl: t_stamp = time_count(t_stamp) 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 + pre_CC = bD1.T @ H2 @ bD1 # grad div: t_stamp = time_count(t_stamp) 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 + pre_GD = - H1 @ bD0 @ cP0 @ dH0 @ cP0.T @ bD0.T @ H1 # jump stabilization: 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 + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + t_stamp = time_count(t_stamp) print(' .. full operator matrix...') @@ -254,69 +185,87 @@ def lift_u_bc(u_bc): print('nu = {}'.format(nu)) 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 + pre_A = eta * cP1.T @ H1 + if mu != 0: + pre_A += mu * cP1.T @ pre_CC + if nu != 0: + pre_A -= nu * cP1.T @ pre_GD + + A = pre_A @ cP1 + gamma_h * JS t_stamp = time_count(t_stamp) 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,) + 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) # 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') + print(' .. projecting the source with ' + source_proj +' projection...') + + tilde_f = get_dual_dofs(Vh=V1h, f=f_vect, domain_h=domain_h, backend_language=backend_language) + if source_proj == 'tilde_Pi': - print(' .. filtering the discrete source with P0.T ...') - tilde_f_c = cP1_m.transpose() @ tilde_f_c + print(' .. filtering the discrete source with P1.T ...') + tilde_f = cP1.T @ tilde_f + + def lift_u_bc(u_bc): + if u_bc is not None: + ubc = P_phys_hcurl(u_bc, P1, domain, mappings).coeffs + ubc = ubc - cP1.dot(ubc) + + else: + ubc = None + + return ubc + + ubc = lift_u_bc(u_bc) - ubc_c = lift_u_bc(u_bc) - if ubc_c is not None: + # from psydac.feec.multipatch.utils_conga_2d import P_phys_hcurl + # mappings = derham_h.callable_mapping + # uh_bc = P_phys_hcurl(u_bc, P1, domain, mappings) + # ubc2 = uh_bc.coeffs + # ubc2 = ubc2 - cP1.dot(ubc2) + + if ubc is not None: # modified source for the homogeneous pbm t_stamp = time_count(t_stamp) print(' .. modifying the source with lifted bc solution...') - tilde_f_c = tilde_f_c - pre_A_m.dot(ubc_c) + tilde_f = tilde_f - pre_A.dot(ubc) # direct solve with scipy spsolve t_stamp = time_count(t_stamp) - print() - print(' -- solving source problem with scipy.spsolve...') - uh_c = spsolve(A_m, tilde_f_c) + print('solving source problem with conjugate gradient...') + solver = inverse(A, solver='cg', tol=1e-8) + u = solver.solve(tilde_f) # project the homogeneous solution on the conforming problem space + t_stamp = time_count(t_stamp) 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') + u = cP1.dot(u) - if ubc_c is not None: + if ubc is not None: # adding the lifted boundary condition t_stamp = time_count(t_stamp) print(' .. adding the lifted boundary condition...') - uh_c += ubc_c + u += ubc - uh = FemField(V1h, coeffs=array_to_psydac(uh_c, V1h.coeff_space)) + uh = FemField(V1h, coeffs=u) #need cp1 here? - f_c = dH1_m.dot(tilde_f_c) - jh = FemField(V1h, coeffs=array_to_psydac(f_c, V1h.coeff_space)) + f = dH1.dot(tilde_f) + jh = FemField(V1h, coeffs=f) t_stamp = time_count(t_stamp) print(' -- plots and diagnostics --') if plot_dir: + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') OM.add_spaces(V1h=V1h) OM.set_static() @@ -349,11 +298,12 @@ def lift_u_bc(u_bc): time_count(t_stamp) if u_ex: - 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))) + u_ex_p = P_phys_hcurl(u_ex, P1, domain, mappings).coeffs + + err = u_ex_p - u + print(err.inner(H1.dot(err))) + l2_error = np.sqrt( err.inner(H1.dot(err))) / np.sqrt(u_ex_p.inner(H1.dot(u_ex_p))) print(l2_error) - #return l2_error diags['err'] = l2_error return diags diff --git a/psydac/feec/multipatch/examples/ppc_test_cases.py b/psydac/feec/multipatch/examples/ppc_test_cases.py index 94b772f4d..2fd889c49 100644 --- a/psydac/feec/multipatch/examples/ppc_test_cases.py +++ b/psydac/feec/multipatch/examples/ppc_test_cases.py @@ -1,22 +1,11 @@ # 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, atan, atan2 +from sympy.functions.special.error_functions import erf -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.fem.plotting_utilities import get_plotting_grid, my_small_plot, my_small_streamplot -from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain -comm = MPI.COMM_WORLD # todo [MCP, 12/02/2022]: add an 'equation' argument to be able to return 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 62312a39f..9326291dd 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -3,12 +3,11 @@ import numpy as np import pytest -# 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.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 -@pytest.mark.skip(reason="need to adapt notation") def test_time_harmonic_maxwell_pretzel_f(): nc = 4 deg = 2 @@ -32,7 +31,6 @@ def test_time_harmonic_maxwell_pretzel_f(): assert abs(diags["err"] - 0.007201508128407582) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_time_harmonic_maxwell_pretzel_f_nc(): deg = 2 nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, @@ -55,9 +53,8 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849165663310541) < 1e-10 + assert abs(diags["err"] - 0.004849208049783925) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] @@ -100,7 +97,6 @@ def test_maxwell_eigen_curved_L_shape(): assert abs(error - 0.01291539899483907) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape_nc(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] @@ -145,7 +141,6 @@ def test_maxwell_eigen_curved_L_shape_nc(): assert abs(error - 0.010504876643873904) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_eigen_curved_L_shape_dg(): domain_name = 'curved_L_shape' domain = [[1, 3], [0, np.pi / 4]] 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 2a9a4ce1e..12a1901b7 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -1,9 +1,8 @@ import numpy as np import pytest -# from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm +from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm -@pytest.mark.skip(reason="need to adapt notation") def test_poisson_pretzel_f(): source_type = 'manu_poisson_2' @@ -20,9 +19,10 @@ def test_poisson_pretzel_f(): backend_language='pyccel-gcc', plot_dir=None) + print("l2_error = ", l2_error) + print(l2_error- 1.0585687717792318e-05) assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_poisson_pretzel_f_nc(): source_type = 'manu_poisson_2' diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 42c630043..14448f8f6 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -42,7 +42,7 @@ def get_polynomial_function(degree, hom_bc_axes, domain): # ============================================================================== @pytest.mark.parametrize('V1_type', ["Hcurl"]) @pytest.mark.parametrize('degree', [[3, 3]]) -@pytest.mark.parametrize('nc', [5]) +@pytest.mark.parametrize('nc', [4]) @pytest.mark.parametrize('reg', [0]) @pytest.mark.parametrize('hom_bc', [False, True]) @pytest.mark.parametrize('domain_name', ["1patch", "4patch_nc", "2patch_nc"]) From c643758f4fecbb1c5a9d506139476a1d63ad44d4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Tue, 15 Jul 2025 16:37:46 +0200 Subject: [PATCH 020/102] small fix in tests --- .../feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py | 5 +---- psydac/feec/tests/test_feec_conf_projectors_cart_2d.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) 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 9326291dd..ae9775595 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -53,7 +53,7 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849208049783925) < 1e-10 + assert abs(diags["err"] - 0.004849225522124346) < 1e-7 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' @@ -86,7 +86,6 @@ def test_maxwell_eigen_curved_L_shape(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell', ) error = 0 @@ -130,7 +129,6 @@ def test_maxwell_eigen_curved_L_shape_nc(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_nc', ) error = 0 @@ -172,7 +170,6 @@ def test_maxwell_eigen_curved_L_shape_dg(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_dg', ) error = 0 diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 14448f8f6..42c630043 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -42,7 +42,7 @@ def get_polynomial_function(degree, hom_bc_axes, domain): # ============================================================================== @pytest.mark.parametrize('V1_type', ["Hcurl"]) @pytest.mark.parametrize('degree', [[3, 3]]) -@pytest.mark.parametrize('nc', [4]) +@pytest.mark.parametrize('nc', [5]) @pytest.mark.parametrize('reg', [0]) @pytest.mark.parametrize('hom_bc', [False, True]) @pytest.mark.parametrize('domain_name', ["1patch", "4patch_nc", "2patch_nc"]) From be9726a0bb0e4ea6438df46e1bd9629c0b6c2fe5 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Tue, 15 Jul 2025 17:22:06 +0200 Subject: [PATCH 021/102] removed plot_dir by accident --- .../feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py | 3 +++ 1 file changed, 3 insertions(+) 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 ae9775595..a64c3a190 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -86,6 +86,7 @@ def test_maxwell_eigen_curved_L_shape(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell', ) error = 0 @@ -129,6 +130,7 @@ def test_maxwell_eigen_curved_L_shape_nc(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell_nc', ) error = 0 @@ -170,6 +172,7 @@ def test_maxwell_eigen_curved_L_shape_dg(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', + plot_dir='./plots/eigen_maxell_dg', ) error = 0 From 5631334f63e1e95482f1a9c2d37ae8eeace5cb37 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 12:22:11 +0200 Subject: [PATCH 022/102] improve projectors interface in DiscreteDeRhamMultipatch --- psydac/api/feec.py | 14 ++ .../examples/hcurl_source_pbms_conga_2d.py | 13 +- psydac/feec/multipatch/utils_conga_2d.py | 77 +---------- .../test_feec_conf_projectors_cart_2d.py | 125 +++++++----------- 4 files changed, 69 insertions(+), 160 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index def2857c9..e7433f3c9 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -573,6 +573,20 @@ def projectors(self, *, kind='global', nquads=None): raise NotImplementedError('2D sequence with H-div not available yet') P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) + + if self.mapping: + + P0_m = lambda f : P0([pull_2d_h1(f, m) for m in self.callable_mapping]) + + if self.sequence[1] == 'hcurl': + P1_m = lambda f : P1([pull_2d_hcurl(f, m) for m in self.callable_mapping]) + else: + raise NotImplementedError('2D sequence with H-div not available yet') + + P2_m = lambda f : P2([pull_2d_l2(f, m) for m in self.callable_mapping]) + + return P0_m, P1_m, P2_m + return P0, P1, P2 elif self.dim == 3: 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 286668771..c7f8481cf 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -22,12 +22,11 @@ from psydac.api.discretization import discretize from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain from psydac.feec.multipatch.examples.ppc_test_cases import get_source_and_solution_hcurl -from psydac.feec.multipatch.utils_conga_2d import P0_phys, P1_phys, P2_phys, get_Vh_diags_for +from psydac.feec.multipatch.utils_conga_2d import P1_phys from psydac.feec.multipatch.utilities import time_count # from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField from psydac.api.postprocessing import OutputManager, PostProcessManager -from psydac.feec.multipatch.utils_conga_2d import P_phys_hcurl from psydac.linalg.basic import IdentityOperator from psydac.fem.projectors import get_dual_dofs @@ -213,7 +212,7 @@ def solve_hcurl_source_pbm( def lift_u_bc(u_bc): if u_bc is not None: - ubc = P_phys_hcurl(u_bc, P1, domain, mappings).coeffs + ubc = P1_phys(u_bc, P1, domain).coeffs ubc = ubc - cP1.dot(ubc) else: @@ -224,12 +223,6 @@ def lift_u_bc(u_bc): ubc = lift_u_bc(u_bc) - # from psydac.feec.multipatch.utils_conga_2d import P_phys_hcurl - # mappings = derham_h.callable_mapping - # uh_bc = P_phys_hcurl(u_bc, P1, domain, mappings) - # ubc2 = uh_bc.coeffs - # ubc2 = ubc2 - cP1.dot(ubc2) - if ubc is not None: # modified source for the homogeneous pbm t_stamp = time_count(t_stamp) @@ -298,7 +291,7 @@ def lift_u_bc(u_bc): time_count(t_stamp) if u_ex: - u_ex_p = P_phys_hcurl(u_ex, P1, domain, mappings).coeffs + u_ex_p = P1_phys(u_ex, P1, domain).coeffs err = u_ex_p - u print(err.inner(H1.dot(err))) diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py index 575ff680a..b457f0232 100644 --- a/psydac/feec/multipatch/utils_conga_2d.py +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -23,88 +23,23 @@ # commuting projections on the physical domain (should probably be in the # interface) -def P0_phys(f_phys, P0, domain, mappings_list): +def P0_phys(f_phys, P0, domain): 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 isinstance(mappings_list, BasicCallableMapping): - f_log = pull_2d_h1(f, mappings_list) - - elif len(mappings_list) == 1: - f_log = pull_2d_h1(f, mappings_list[0]) - - else: - f_log = [pull_2d_h1(f, m) for m in mappings_list] - - return P0(f_log) + return P0(f) -def P_phys_hcurl(f_phys, P1, domain, mappings_list): +def P1_phys(f_phys, P1, domain): f_x = lambdify(domain.coordinates, f_phys[0]) f_y = lambdify(domain.coordinates, f_phys[1]) - if isinstance(mappings_list, BasicCallableMapping): - f_log = pull_2d_hcurl([f_x, f_y], mappings_list) + return P1([f_x, f_y]) - elif len(mappings_list) == 1: - f_log = pull_2d_hcurl([f_x, f_y], mappings_list[0]) - else: - 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]) - - if isinstance(mappings_list, BasicCallableMapping): - f_log = pull_2d_hdiv([f_x, f_y], mappings_list) - - elif len(mappings_list) == 1: - f_log = pull_2d_hdiv([f_x, f_y], mappings_list[0]) - else: - 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): +def P2_phys(f_phys, P2, domain): f = lambdify(domain.coordinates, f_phys) - if isinstance(mappings_list, BasicCallableMapping): - f_log = pull_2d_l2(f, mappings_list) - - elif len(mappings_list) == 1: - f_log = pull_2d_l2(f, mappings_list[0]) - - else: - f_log = [pull_2d_l2(f, m) for m in mappings_list] - - return P2(f_log) + return P2(f) def get_kind(space='V*'): diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 42c630043..fca2845e6 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -2,17 +2,17 @@ from collections import OrderedDict import numpy as np -from sympy import Tuple +from sympy import Tuple, lambdify from scipy.sparse.linalg import norm as sp_norm from sympde.topology.domain import Domain from sympde.topology import Derham, Square, IdentityMapping from psydac.api.discretization import discretize -from psydac.feec.multipatch.utils_conga_2d import P_phys_l2, P_phys_hdiv, P_phys_hcurl, P_phys_h1 from psydac.fem.projectors import get_dual_dofs + def get_polynomial_function(degree, hom_bc_axes, domain): """Return a polynomial function of given degree and homogeneus boundary conditions on the domain.""" @@ -36,7 +36,10 @@ def get_polynomial_function(degree, hom_bc_axes, domain): # else: g0_y = (y - 0.75)**degree[1] - return g0_x * g0_y + expr = g0_x * g0_y + callable_function = lambdify(domain.coordinates, expr) + + return expr, callable_function # ============================================================================== @@ -122,12 +125,11 @@ def test_conf_projectors_2d( domain_h = discretize(domain, ncells=ncells_h) # Vh space - p_derham = Derham(domain, ["H1", V1_type, "L2"]) + 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_V1h, p_V2h = p_derham_h.spaces - mappings_list = p_derham_h.callable_mapping + derham_h = discretize(derham, domain_h, degree=degree) + V0h, V1h, V2h = derham_h.spaces # full moment preservation only possible if enough interior functions in a @@ -141,15 +143,15 @@ def test_conf_projectors_2d( # moment preservation... # geometric projections (operators) - p_geomP0, p_geomP1, p_geomP2 = p_derham_h.projectors(nquads=nquads) + geomP0, geomP1, geomP2 = derham_h.projectors(nquads=nquads) # conforming projections (scipy matrices) - cP0, cP1, cP2 = p_derham_h.conforming_projectors(kind='sparse', p_moments=mom_pres, hom_bc=hom_bc) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='sparse', p_moments=mom_pres, hom_bc=hom_bc) - M0, M1, M2 = p_derham_h.Hodge_operators(kind='sparse')#, backend_language=backend_language, load_dir=m_load_dir) - M0_inv, M1_inv, M2_inv = p_derham_h.Hodge_operators(kind='sparse', dual=True)#, backend_language=backend_language, load_dir=m_load_dir) + M0, M1, M2 = derham_h.Hodge_operators(kind='sparse') + M0_inv, M1_inv, M2_inv = derham_h.Hodge_operators(kind='sparse', dual=True) - bD0, bD1 = p_derham_h.derivatives(kind='sparse') + bD0, bD1 = derham_h.derivatives(kind='sparse') D0 = bD0 @ cP0 # Conga grad D1 = bD1 @ cP1 # Conga curl or div @@ -166,11 +168,11 @@ def test_conf_projectors_2d( # 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, g0_fun = get_polynomial_function(degree=degree, hom_bc_axes=[hom_bc, hom_bc], domain=domain) + g0h = geomP0(g0_fun) g0_c = g0h.coeffs.toarray() - tilde_g0_c = get_dual_dofs(Vh=p_V0h, f=g0, domain_h = domain_h, return_format='numpy_array') + tilde_g0_c = get_dual_dofs(Vh=V0h, f=g0, domain_h = domain_h, return_format='numpy_array') g0_L2_c = M0_inv @ tilde_g0_c # (P0_geom - P0_L2) polynomial = 0 @@ -183,44 +185,26 @@ def test_conf_projectors_2d( # 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, g0_fun = get_polynomial_function(degree=degree, hom_bc_axes=[False, False], domain=domain) + g0h = geomP0(g0_fun) g0_c = g0h.coeffs.toarray() - tilde_g0_c = get_dual_dofs(Vh=p_V0h, f=g0, domain_h = domain_h, return_format='numpy_array') + tilde_g0_c = get_dual_dofs(Vh=V0h, f=g0, domain_h = domain_h, 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_x, G1_x_fun = get_polynomial_function(degree=[degree[0] - 1,degree[1]], hom_bc_axes=[False, hom_bc], domain=domain) + G1_y, G1_y_fun = get_polynomial_function(degree=[degree[0], degree[1] - 1], hom_bc_axes=[hom_bc, False], domain=domain) + + G1 = Tuple(G1_x, G1_y) + G1_fun = [G1_x_fun, G1_y_fun] - 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) - + G1h = geomP1(G1_fun) G1_c = G1h.coeffs.toarray() - tilde_G1_c = get_dual_dofs(Vh=p_V1h, f=G1, domain_h=domain_h, return_format='numpy_array') + tilde_G1_c = get_dual_dofs(Vh=V1h, f=G1, domain_h=domain_h, return_format='numpy_array') G1_L2_c = M1_inv @ tilde_G1_c @@ -230,46 +214,29 @@ def test_conf_projectors_2d( 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 = get_dual_dofs(Vh=p_V1h, f=G1, domain_h=domain_h, 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) + G1_x, G1_x_fun = get_polynomial_function(degree=[degree[0] - 1,degree[1]], hom_bc_axes=[False, False], domain=domain) + G1_y, G1_y_fun = get_polynomial_function(degree=[degree[0], degree[1] - 1], hom_bc_axes=[False, False], domain=domain) + + G1 = Tuple(G1_x, G1_y) + G1_fun = [G1_x_fun, G1_y_fun] + + G1h = geomP1(G1_fun) + G1_c = G1h.coeffs.toarray() + + G1h = geomP1(G1_fun) + G1_c = G1h.coeffs.toarray() + + tilde_G1_c = get_dual_dofs(Vh=V1h, f=G1, domain_h=domain_h, 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, g2_fun = get_polynomial_function(degree=[degree[0] - 1, degree[1] - 1], hom_bc_axes=[False, False], domain=domain) + g2h = geomP2(g2_fun) g2_c = g2h.coeffs.toarray() - tilde_g2_c = get_dual_dofs(Vh=p_V2h, f=g2, domain_h=domain_h, return_format='numpy_array') + tilde_g2_c = get_dual_dofs(Vh=V2h, f=g2, domain_h=domain_h, return_format='numpy_array') g2_L2_c = M2_inv @ tilde_g2_c # (P2_geom - P2_L2) polynomial = 0 From 1e45f8519cfe0d5a0289812bd9e017dd8f3d57e8 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 13:55:51 +0200 Subject: [PATCH 023/102] improve docstrings in psydac/api/feec --- psydac/api/feec.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index e7433f3c9..11dcb66cb 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,5 +1,3 @@ -import os - from psydac.api.basic import BasicDiscrete from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D @@ -305,14 +303,14 @@ def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): kind : The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. - - 'femlinop' returns a FemLinearOperator - - 'sparse' returns a sparse matrix - - 'linop' returns a LinearOperator + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'sparse' returns a scipy sparse matrix + - 'linop' returns a psydac LinearOperator Returns ------- - Cp: , or - The conforming projector + cP0, cP1, cP2 : Tuple of , or + The conforming projectors of each space and in desired form. """ if hom_bc is None: @@ -397,10 +395,14 @@ def _Hodge_kind(self, H, dual=False, kind='femlinop'): If True, returns the dual Hodge operator kind : - The kind of the operator, can be 'femlinop', 'sparse' or 'linop'. - - 'femlinop' returns a FemLinearOperator - - 'sparse' returns a sparse matrix - - 'linop' returns a LinearOperator + The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'sparse' returns a scipy sparse matrix + - 'linop' returns a psydac LinearOperator + + Returns + ------- + Hodge operator in the specified form. """ if not dual: @@ -432,11 +434,11 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu dual : bool If True, returns the dual Hodge operator. - kind : str - The kind of the operator, can be 'femlinop', 'sparse' or 'linop'. - - 'femlinop' returns a FemLinearOperator - - 'sparse' returns a sparse matrix - - 'linop' returns a LinearOperator + kind : + The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'sparse' returns a scipy sparse matrix + - 'linop' returns a psydac LinearOperator backend_language : str The backend used to accelerate the code, default is 'python'. @@ -446,7 +448,13 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu Returns ------- - Hodge operator for the specified space or a tuple of operators if space is None. + Either one of the following Hodge operators of the specified kind and space or all of them if space is None. + + H0 : , or + + H1 : , or + + H2 : , or """ if not self._Hodge_operators: @@ -577,7 +585,7 @@ def projectors(self, *, kind='global', nquads=None): if self.mapping: P0_m = lambda f : P0([pull_2d_h1(f, m) for m in self.callable_mapping]) - + if self.sequence[1] == 'hcurl': P1_m = lambda f : P1([pull_2d_hcurl(f, m) for m in self.callable_mapping]) else: From b88340b9e184cb12440da902f7698ad385a83429 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 14:15:39 +0200 Subject: [PATCH 024/102] improve docstrings in FemLinearOperators and psydac/feec/derivatives --- psydac/feec/derivatives.py | 16 ++++++++-------- psydac/fem/basic.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index 858838da9..600db2d75 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -387,7 +387,7 @@ def __init__(self, H1, L2): assert H1.degree[0] == L2.degree[0] + 1 # Store data in object - super().__init__(H1, L2, DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) + super().__init__(fem_domain = H1, fem_codomain = L2, linop = DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) #==================================================================================================== class Gradient_2D(FemLinearOperator): @@ -426,7 +426,7 @@ def __init__(self, H1, Hcurl): matrix = BlockLinearOperator(H1.coeff_space, Hcurl.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hcurl, matrix) + super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== class Gradient_3D(FemLinearOperator): @@ -468,7 +468,7 @@ def __init__(self, H1, Hcurl): matrix = BlockLinearOperator(H1.coeff_space, Hcurl.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hcurl, matrix) + super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== class ScalarCurl_2D(FemLinearOperator): @@ -507,7 +507,7 @@ def __init__(self, Hcurl, L2): matrix = BlockLinearOperator(Hcurl.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hcurl, L2, matrix) + super().__init__(fem_domain = Hcurl, fem_codomain = L2, linop = matrix) #==================================================================================================== class VectorCurl_2D(FemLinearOperator): @@ -547,7 +547,7 @@ def __init__(self, H1, Hdiv): matrix = BlockLinearOperator(H1.coeff_space, Hdiv.coeff_space, blocks=blocks) # Store data in object - super().__init__(H1, Hdiv, matrix) + super().__init__(fem_domain = H1, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== class Curl_3D(FemLinearOperator): @@ -597,7 +597,7 @@ def __init__(self, Hcurl, Hdiv): # ... # Store data in object - super().__init__(Hcurl, Hdiv, matrix) + super().__init__(fem_domain = Hcurl, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== class Divergence_2D(FemLinearOperator): @@ -636,7 +636,7 @@ def __init__(self, Hdiv, L2): matrix = BlockLinearOperator(Hdiv.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hdiv, L2, matrix) + super().__init__(fem_domain = Hdiv, fem_codomain = L2, linop = matrix) #==================================================================================================== class Divergence_3D(FemLinearOperator): @@ -678,7 +678,7 @@ def __init__(self, Hdiv, L2): matrix = BlockLinearOperator(Hdiv.coeff_space, L2.coeff_space, blocks=blocks) # Store data in object - super().__init__(Hdiv, L2, matrix) + super().__init__(fem_domain = Hdiv, fem_codomain = L2, linop = matrix) #==================================================================================================== # 2D Multipatch derivative operators diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 61b8222c8..17192c086 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -391,16 +391,16 @@ class FemLinearOperator: Parameters ---------- - fem_domain: FemSpace + fem_domain : psydac.fem.basic.FemSpace The discrete space - fem_codomain: FemSpace + fem_codomain : psydac.fem.basic.FemSpace Number of polynomial moments to be preserved in the projection. - linop: LinearOperator (optional) + linop : (optional) Linear Operator. - sparse_matrix: sparse matrix (optional) + sparse_matrix : (optional) Sparse matrix representation of the linear operator. """ From 2ee57ee62fdf71388e7a4310675d7f6a2935091f Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 14:55:32 +0200 Subject: [PATCH 025/102] remove backend from maxwell source pbm tests --- .../tests/test_feec_maxwell_multipatch_2d.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 a64c3a190..a3f5cacf5 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -26,8 +26,8 @@ def test_time_harmonic_maxwell_pretzel_f(): mu=1, domain_name=domain_name, source_type=source_type, - source_proj=source_proj, - backend_language='pyccel-gcc') + source_proj=source_proj,) + #backend_language='pyccel-gcc') assert abs(diags["err"] - 0.007201508128407582) < 1e-10 @@ -50,10 +50,10 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): mu=1, domain_name=domain_name, source_type=source_type, - source_proj=source_proj, - backend_language='pyccel-gcc') + source_proj=source_proj,) + #backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849225522124346) < 1e-7 + assert abs(diags["err"] - 0.004849225522124346) < 1e-10 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' From f36a0d3f7ab1e0f8a4d595e9be456c6c0b563bd1 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 15:35:20 +0200 Subject: [PATCH 026/102] pass python backend to feec maxwell tests --- .../multipatch/tests/test_feec_maxwell_multipatch_2d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 a3f5cacf5..4cd1dbf57 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -26,8 +26,8 @@ def test_time_harmonic_maxwell_pretzel_f(): mu=1, domain_name=domain_name, source_type=source_type, - source_proj=source_proj,) - #backend_language='pyccel-gcc') + source_proj=source_proj, + backend_language='python') assert abs(diags["err"] - 0.007201508128407582) < 1e-10 @@ -50,8 +50,8 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): mu=1, domain_name=domain_name, source_type=source_type, - source_proj=source_proj,) - #backend_language='pyccel-gcc') + source_proj=source_proj, + backend_language='python') assert abs(diags["err"] - 0.004849225522124346) < 1e-10 From b4f2ddc23be4b85ba6a6a9d8394c1c34de225de2 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 16:27:57 +0200 Subject: [PATCH 027/102] revert changes in tests --- .../multipatch/tests/test_feec_maxwell_multipatch_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4cd1dbf57..a64c3a190 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -27,7 +27,7 @@ def test_time_harmonic_maxwell_pretzel_f(): domain_name=domain_name, source_type=source_type, source_proj=source_proj, - backend_language='python') + backend_language='pyccel-gcc') assert abs(diags["err"] - 0.007201508128407582) < 1e-10 @@ -51,9 +51,9 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): domain_name=domain_name, source_type=source_type, source_proj=source_proj, - backend_language='python') + backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849225522124346) < 1e-10 + assert abs(diags["err"] - 0.004849225522124346) < 1e-7 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' From 05c25317a4739f6f0cda1b0c7c76a1b0dd2063d3 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 16:28:36 +0200 Subject: [PATCH 028/102] remove commenteed imports --- psydac/feec/hodge.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 1e28922f6..09b3f87ac 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -12,11 +12,7 @@ from sympde.expr.expr import integral from psydac.api.settings import PSYDAC_BACKENDS -#from psydac.api.discretization import discretize -# from psydac.feec.derivatives import Gradient_2D, ScalarCurl_2D -#from psydac.feec.multipatch.fem_linear_operators import FemLinearOperator -# from psydac.linalg.basic import LinearOperator from psydac.linalg.utilities import SparseMatrixLinearOperator # =============================================================================== From dbb6167e47b1da8280a530f8e852a0a7222d82e4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 16 Jul 2025 16:55:12 +0200 Subject: [PATCH 029/102] clean up test file --- .../feec/multipatch/tests/test_feec_poisson_multipatch_2d.py | 3 --- 1 file changed, 3 deletions(-) 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 12a1901b7..fe6aed82f 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -1,5 +1,4 @@ import numpy as np -import pytest from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm @@ -19,8 +18,6 @@ def test_poisson_pretzel_f(): backend_language='pyccel-gcc', plot_dir=None) - print("l2_error = ", l2_error) - print(l2_error- 1.0585687717792318e-05) assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 def test_poisson_pretzel_f_nc(): From 6163f47b220f2eb4fdea7446a9285c95b766b14e Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 23 Jul 2025 16:23:36 +0200 Subject: [PATCH 030/102] rename _Hodge_kind to _get_Hodge_operator --- psydac/api/feec.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 11dcb66cb..3fe29bae9 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -383,7 +383,7 @@ def _init_Hodge_operators(self, backend_language='python', load_dir=None): self._Hodge_operators = (H0, H1, H2, H3) #-------------------------------------------------------------------------- - def _Hodge_kind(self, H, dual=False, kind='femlinop'): + def _get_Hodge_operator(self, H, dual=False, kind='femlinop'): """ Helper function to return the Hodge operator in the specified form. @@ -463,19 +463,19 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu H0, H1, H2 = self._Hodge_operators if space == 'V0': - return self._Hodge_kind(self._Hodge_operators[0], dual=dual, kind=kind) + return self._get_Hodge_operator(self._Hodge_operators[0], dual=dual, kind=kind) elif space == 'V1': - return self._Hodge_kind(self._Hodge_operators[1], dual=dual, kind=kind) + return self._get_Hodge_operator(self._Hodge_operators[1], dual=dual, kind=kind) elif space == 'V2': - return self._Hodge_kind(self._Hodge_operators[2], dual=dual, kind=kind) + return self._get_Hodge_operator(self._Hodge_operators[2], dual=dual, kind=kind) elif space == 'V3': - return self._Hodge_kind(self._Hodge_operators[3], dual=dual, kind=kind) + return self._get_Hodge_operator(self._Hodge_operators[3], dual=dual, kind=kind) elif space is None: - return tuple(self._Hodge_kind(H, dual=dual, kind=kind) for H in self._Hodge_operators) + return tuple(self._get_Hodge_operator(H, dual=dual, kind=kind) for H in self._Hodge_operators) #============================================================================== From d1df68b31cf46253ad306a8a43a402e4d64b207a Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 23 Jul 2025 16:26:47 +0200 Subject: [PATCH 031/102] clarify warning and reduce preservation order --- psydac/feec/conforming_projectors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index f9923f141..56affe909 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -494,7 +494,8 @@ def get_1d_moment_correction(space_1d, p_moments=-1): return None if space_1d.ncells <= p_moments + 1: - print("Careful, the correction term is currently not independent of the mesh.") + print("The correction term is currently not independent of the mesh. The order of moment conservation has been reduced.") + p_moments = space_1d.ncells - 2 if p_moments >= 0: # to preserve moments of degree p we need 1+p conforming basis functions in the patch (the "interior" ones) From 2f8052c905feb3f65536b6d55e4af913c33d335c Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 23 Jul 2025 17:00:38 +0200 Subject: [PATCH 032/102] add mom_pres keyword to CP --- psydac/api/feec.py | 6 +- psydac/feec/conforming_projectors.py | 187 +++++++++++++++------------ 2 files changed, 104 insertions(+), 89 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 3fe29bae9..f71455089 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -288,7 +288,7 @@ def derivatives(self, kind='femlinop'): return tuple(b_diff.linop for b_diff in self._derivatives) #-------------------------------------------------------------------------- - def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): + def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, hom_bc=False): """ return the conforming projectors of the broken multi-patch space @@ -324,8 +324,8 @@ def conforming_projectors(self, kind='femlinop', p_moments=-1, hom_bc=False): raise NotImplementedError('2D sequence with H-div not available yet') else: - cP0 = ConformingProjection_V0(self.V0, p_moments=p_moments, hom_bc=hom_bc) - cP1 = ConformingProjection_V1(self.V1, p_moments=p_moments, hom_bc=hom_bc) + cP0 = ConformingProjection_V0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + cP1 = ConformingProjection_V1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) I2 = IdentityOperator(self.V2.coeff_space) cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2, sparse_matrix=I2.tosparse()) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 56affe909..179ea0e0a 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -553,6 +553,7 @@ def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=Fa # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0].spaces[0], p_moments=p_moments) + p_moments = len(gamma)-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1005,8 +1006,8 @@ def construct_hcurl_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc # moment corrections perpendicular to interfaces # should be in the V^0 spaces - gamma = [get_1d_moment_correction( - Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + gamma = [get_1d_moment_correction(Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + p_moments = min([len(g) for g in gamma])-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1156,90 +1157,6 @@ def edge_moment_index(p, i, axis, ext, space, k): #============================================================================== # Singlepatch conforming projectors #============================================================================== -def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): - """ - Construct the conforming projection for a single patch 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 or not hom_bc: - return sparse_eye(dim_tot, format="lil") - - # moment corrections perpendicular to interfaces - # should be in the V^0 spaces - - gamma = [get_1d_moment_correction(Vh.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) - # T is a TensorFemSpace and S is a 1D SplineSpace - shapes = [[S.nbasis for S in T.spaces] for T in Vh.spaces] - l2g.set_patch_shapes(0, *shapes) - - # P edge - # edge correction matrix - Proj_edge = sparse_eye(dim_tot, format="lil") - - def get_edge_index(j, axis, ext): - multi_index = [None] * ndim - multi_index[axis] = 0 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 - multi_index[1 - axis] = j - return l2g.get_index(0, 1 - axis, multi_index) - - def edge_moment_index(p, i, axis, ext): - multi_index = [None] * ndim - multi_index[1 - axis] = i - multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 - return l2g.get_index(0, 1 - axis, multi_index) - - - # boundary condition - for bn in domain.boundary: - - axis = bn.axis - d = 1 - axis - ext = bn.ext - space_1d = Vh.spaces[d].spaces[d] - - for i in range(0, space_1d.nbasis): - ig = get_edge_index(i, axis, ext) - Proj_edge[ig, ig] = 0 - - for p in range(p_moments + 1): - - pg = edge_moment_index(p, i, axis, ext) - Proj_edge[pg, ig] = gamma[d][p] - - return Proj_edge - - - - def construct_h1_singlepatch_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). @@ -1273,6 +1190,7 @@ def construct_h1_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=- # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0], p_moments=p_moments) + p_moments = len(gamma)-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1415,6 +1333,89 @@ def get_mu_minus(j, coarse_space, fine_space, R): return Proj_edge @ Proj_vertex +def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=False): + """ + Construct the conforming projection for a single patch 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 or not hom_bc: + return sparse_eye(dim_tot, format="lil") + + # moment corrections perpendicular to interfaces + # should be in the V^0 spaces + + gamma = [get_1d_moment_correction(Vh.spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] + p_moments = min([len(g) for g in gamma])-1 + + domain = Vh.symbolic_space.domain + ndim = 2 + n_components = 2 + n_patches = len(domain) + + l2g = Local2GlobalIndexMap(ndim, len(domain), n_components) + # T is a TensorFemSpace and S is a 1D SplineSpace + shapes = [[S.nbasis for S in T.spaces] for T in Vh.spaces] + l2g.set_patch_shapes(0, *shapes) + + # P edge + # edge correction matrix + Proj_edge = sparse_eye(dim_tot, format="lil") + + def get_edge_index(j, axis, ext): + multi_index = [None] * ndim + multi_index[axis] = 0 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 + multi_index[1 - axis] = j + return l2g.get_index(0, 1 - axis, multi_index) + + def edge_moment_index(p, i, axis, ext): + multi_index = [None] * ndim + multi_index[1 - axis] = i + multi_index[axis] = p + 1 if ext == -1 else Vh.spaces[1 - axis].spaces[axis].nbasis - 1 - p - 1 + return l2g.get_index(0, 1 - axis, multi_index) + + + # boundary condition + for bn in domain.boundary: + + axis = bn.axis + d = 1 - axis + ext = bn.ext + space_1d = Vh.spaces[d].spaces[d] + + for i in range(0, space_1d.nbasis): + ig = get_edge_index(i, axis, ext) + Proj_edge[ig, ig] = 0 + + for p in range(p_moments + 1): + + pg = edge_moment_index(p, i, axis, ext) + Proj_edge[pg, ig] = gamma[d][p] + + return Proj_edge + + # =============================================================================== class ConformingProjection_V0(FemLinearOperator): @@ -1439,8 +1440,15 @@ class ConformingProjection_V0(FemLinearOperator): def __init__( self, V0h, + mom_pres=False, p_moments=-1, hom_bc=False): + + if mom_pres: + if V0h.is_multipatch: + p_moments = max(p_moments, max(V0h.degree[0])) + else: + p_moments = max(p_moments, max(V0h.degree)) FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V0h) @@ -1475,9 +1483,16 @@ class ConformingProjection_V1(FemLinearOperator): def __init__( self, V1h, + mom_pres=False, p_moments=-1, hom_bc=False): + if mom_pres: + if V1h.is_multipatch: + p_moments = max(p_moments, max(V1h.spaces[0].degree[0])) + else: + p_moments = max(p_moments, max(V1h.degree[0])) + FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V1h) if V1h.is_multipatch: From b20bc2e05b8794be6cb167e1ae9df7af883789aa Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 23 Jul 2025 17:50:28 +0200 Subject: [PATCH 033/102] fix p_moments if gamma is None --- psydac/feec/conforming_projectors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 179ea0e0a..515346c38 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -553,7 +553,7 @@ def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=Fa # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0].spaces[0], p_moments=p_moments) - p_moments = len(gamma)-1 + p_moments = len(gamma)-1 if gamma else -1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1007,7 +1007,7 @@ def construct_hcurl_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc # moment corrections perpendicular to interfaces # should be in the V^0 spaces gamma = [get_1d_moment_correction(Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] - p_moments = min([len(g) for g in gamma])-1 + p_moments = min([len(g) for g in gamma])-1 if gamma else -1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1190,7 +1190,7 @@ def construct_h1_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=- # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0], p_moments=p_moments) - p_moments = len(gamma)-1 + p_moments = len(gamma)-1 if gamma else -1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1367,7 +1367,7 @@ def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moment # should be in the V^0 spaces gamma = [get_1d_moment_correction(Vh.spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] - p_moments = min([len(g) for g in gamma])-1 + p_moments = min([len(g) for g in gamma])-1 if gamma else -1 domain = Vh.symbolic_space.domain ndim = 2 From 809c31747446143426988c304164b14f06a38611 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 23 Jul 2025 19:17:32 +0200 Subject: [PATCH 034/102] fix p_moments if gamma is None 2 --- psydac/feec/conforming_projectors.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 515346c38..65ef6008a 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -491,7 +491,7 @@ def get_1d_moment_correction(space_1d, p_moments=-1): """ if p_moments < 0: - return None + return [] if space_1d.ncells <= p_moments + 1: print("The correction term is currently not independent of the mesh. The order of moment conservation has been reduced.") @@ -553,7 +553,7 @@ def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=Fa # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0].spaces[0], p_moments=p_moments) - p_moments = len(gamma)-1 if gamma else -1 + p_moments = len(gamma)-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1007,7 +1007,7 @@ def construct_hcurl_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc # moment corrections perpendicular to interfaces # should be in the V^0 spaces gamma = [get_1d_moment_correction(Vh.spaces[0].spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] - p_moments = min([len(g) for g in gamma])-1 if gamma else -1 + p_moments = min([len(g) for g in gamma])-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1190,7 +1190,7 @@ def construct_h1_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=- # moment corrections perpendicular to interfaces # assume same moments everywhere gamma = get_1d_moment_correction(Vh.spaces[0], p_moments=p_moments) - p_moments = len(gamma)-1 if gamma else -1 + p_moments = len(gamma)-1 domain = Vh.symbolic_space.domain ndim = 2 @@ -1367,7 +1367,7 @@ def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moment # should be in the V^0 spaces gamma = [get_1d_moment_correction(Vh.spaces[1 - d].spaces[d], p_moments=p_moments) for d in range(2)] - p_moments = min([len(g) for g in gamma])-1 if gamma else -1 + p_moments = min([len(g) for g in gamma])-1 domain = Vh.symbolic_space.domain ndim = 2 From cf76aa83bcdfd2dc701fa0baa5352b13b70a2b66 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 10:30:30 +0200 Subject: [PATCH 035/102] clarify warning for reduced order of moment preservation --- psydac/feec/conforming_projectors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 65ef6008a..47eaf59f4 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -494,9 +494,9 @@ def get_1d_moment_correction(space_1d, p_moments=-1): return [] if space_1d.ncells <= p_moments + 1: - print("The correction term is currently not independent of the mesh. The order of moment conservation has been reduced.") p_moments = space_1d.ncells - 2 - + print(f"The prescribed degree of preserved moments was too high, given the number of cells in the patch. It has been reduced to degree {p_moments}.") + 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 From 4872e7272fb71c773a7318f38830c5e5a357b0fd Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 10:35:49 +0200 Subject: [PATCH 036/102] change line breaks --- psydac/feec/derivatives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index 600db2d75..5c6d2f104 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -699,7 +699,6 @@ def transpose(self, conjugate=False): return BrokenTransposedGradient_2D(self.fem_domain, self.fem_codomain) # ============================================================================== - class BrokenTransposedGradient_2D(FemLinearOperator): def __init__(self, V0h, V1h): @@ -717,6 +716,7 @@ def transpose(self, conjugate=False): # ============================================================================== class BrokenScalarCurl_2D(FemLinearOperator): + def __init__(self, V1h, V2h): FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) From 306a8539d96b45e8aca056dac93f3f6b3caa4d0d Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 10:59:55 +0200 Subject: [PATCH 037/102] capitalize DiscreteDeRham --- psydac/api/discretization.py | 14 +++++++------- psydac/api/feec.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 1dc60aa74..a11409335 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -29,7 +29,7 @@ from psydac.api.fem import DiscreteLinearForm from psydac.api.fem import DiscreteFunctional from psydac.api.fem import DiscreteSumForm -from psydac.api.feec import DiscreteDerham, DiscreteDerhamMultipatch +from psydac.api.feec import DiscreteDeRham, DiscreteDeRhamMultipatch from psydac.api.glt import DiscreteGltExpr from psydac.api.expr import DiscreteExpr from psydac.api.equation import DiscreteEquation @@ -151,7 +151,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): Create a discrete De Rham sequence from a symbolic one. This function creates the discrete spaces from the symbolic ones, and then - creates a DiscreteDerham object from them. + creates a DiscreteDeRham object from them. Parameters ---------- @@ -169,7 +169,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): Returns ------- - DiscreteDerham + DiscreteDeRham The discrete De Rham sequence containing the discrete spaces, differential operators and projectors. @@ -191,7 +191,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #We still need to specify the symbolic space because of "_recursive_element_of" not implemented in sympde spaces.append(Xh) - return DiscreteDerham(domain_h, *spaces) + return DiscreteDeRham(domain_h, *spaces) #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): @@ -199,7 +199,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): Create a discrete multipatch De Rham sequence from a symbolic one. This function creates the broken discrete spaces from the symbolic ones, and then - creates a DiscreteDerhamMultipatch object from them. + creates a DiscreteDeRhamMultipatch object from them. Parameters ---------- @@ -214,7 +214,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): Returns ------- - DiscreteDerhamMultipatch + DiscreteDeRhamMultipatch The discrete multipatch De Rham sequence containing the discrete spaces, differential operators and projectors. @@ -229,7 +229,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ for V, basis in zip(derham.spaces, bases)] - return DiscreteDerhamMultipatch( + return DiscreteDeRhamMultipatch( domain_h = domain_h, spaces = spaces ) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index f71455089..9acb7383b 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -25,10 +25,10 @@ from psydac.fem.vector import VectorFemSpace from psydac.linalg.basic import IdentityOperator -__all__ = ('DiscreteDerham', 'DiscreteDerhamMultipatch',) +__all__ = ('DiscreteDeRham', 'DiscreteDeRhamMultipatch',) #============================================================================== -class DiscreteDerham(BasicDiscrete): +class DiscreteDeRham(BasicDiscrete): """ A discrete de Rham sequence built over a single-patch geometry. Parameters @@ -479,7 +479,7 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu #============================================================================== -class DiscreteDerhamMultipatch(DiscreteDerham): +class DiscreteDeRhamMultipatch(DiscreteDeRham): """ Represents the discrete De Rham sequence for multipatch domains. It only works when the number of patches>1. From 5f4682b53c135847d056a2816070a0826e5c5598 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 11:16:01 +0200 Subject: [PATCH 038/102] improve csr matrix conversion --- psydac/linalg/basic.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 062723e35..e3304f247 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -696,11 +696,10 @@ def dtype(self): return None def toarray(self): - return self._scalar*self._operator.toarray() + return self._scalar * self._operator.toarray() def tosparse(self): - from scipy.sparse import csr_matrix - return self._scalar*csr_matrix(self._operator.tosparse()) + return self._scalar * self._operator.tosparse().tocsr() def transpose(self, conjugate=False): return ScaledLinearOperator(domain=self.codomain, codomain=self.domain, c=self._scalar if not conjugate else np.conjugate(self._scalar), A=self._operator.transpose(conjugate=conjugate)) From 61cac85207b29a1dc0c597b88ac8855351c7c6f5 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 11:30:17 +0200 Subject: [PATCH 039/102] decapitalize hodges --- psydac/api/feec.py | 38 ++++++------- psydac/feec/hodge.py | 54 +++++++++---------- .../examples/h1_source_pbms_conga_2d.py | 6 +-- .../examples/hcurl_eigen_pbms_conga_2d.py | 4 +- .../examples/hcurl_source_pbms_conga_2d.py | 4 +- 5 files changed, 53 insertions(+), 53 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 9acb7383b..58a331b23 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -111,7 +111,7 @@ def __init__(self, domain_h, *spaces): else: raise ValueError('Dimension {} is not available'.format(dim)) - self._Hodge_operators = () + self._hodge_operators = () self._conf_proj = () #-------------------------------------------------------------------------- @property @@ -343,7 +343,7 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h return cP0.linop, cP1.linop, cP2.linop #-------------------------------------------------------------------------- - def _init_Hodge_operators(self, backend_language='python', load_dir=None): + def _init_hodge_operators(self, backend_language='python', load_dir=None): """ Initialize the Hodge operator for the multipatch de Rham sequence. @@ -357,13 +357,13 @@ def _init_Hodge_operators(self, backend_language='python', load_dir=None): Filename for storage in sparse matrix format """ - if not self._Hodge_operators: + if not self._hodge_operators: if self.dim == 1: H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) - self._Hodge_operators = (H0, H1) + self._hodge_operators = (H0, H1) elif self.dim == 2: @@ -371,7 +371,7 @@ def _init_Hodge_operators(self, backend_language='python', load_dir=None): H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) - self._Hodge_operators = (H0, H1, H2) + self._hodge_operators = (H0, H1, H2) elif self.dim == 3: @@ -380,10 +380,10 @@ def _init_Hodge_operators(self, backend_language='python', load_dir=None): H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) H3 = HodgeOperator(self.V3, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=3) - self._Hodge_operators = (H0, H1, H2, H3) + self._hodge_operators = (H0, H1, H2, H3) #-------------------------------------------------------------------------- - def _get_Hodge_operator(self, H, dual=False, kind='femlinop'): + def _get_hodge_operator(self, H, dual=False, kind='femlinop'): """ Helper function to return the Hodge operator in the specified form. @@ -407,21 +407,21 @@ def _get_Hodge_operator(self, H, dual=False, kind='femlinop'): if not dual: if kind == 'femlinop': - return H.Hodge + return H.hodge elif kind == 'sparse': return H.tosparse elif kind == 'linop': return H.linop else: if kind == 'femlinop': - return H.dual_Hodge + return H.dual_hodge elif kind == 'sparse': return H.dual_tosparse elif kind == 'linop': return H.dual_linop #-------------------------------------------------------------------------- - def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): + def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): """ Returns the Hodge operator for the given space and specified kind. @@ -457,25 +457,25 @@ def Hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu H2 : , or """ - if not self._Hodge_operators: - self._init_Hodge_operators(backend_language=backend_language, load_dir=load_dir) + if not self._hodge_operators: + self._init_hodge_operators(backend_language=backend_language, load_dir=load_dir) - H0, H1, H2 = self._Hodge_operators + H0, H1, H2 = self._hodge_operators if space == 'V0': - return self._get_Hodge_operator(self._Hodge_operators[0], dual=dual, kind=kind) + return self._get_hodge_operator(self._hodge_operators[0], dual=dual, kind=kind) elif space == 'V1': - return self._get_Hodge_operator(self._Hodge_operators[1], dual=dual, kind=kind) + return self._get_hodge_operator(self._hodge_operators[1], dual=dual, kind=kind) elif space == 'V2': - return self._get_Hodge_operator(self._Hodge_operators[2], dual=dual, kind=kind) + return self._get_hodge_operator(self._hodge_operators[2], dual=dual, kind=kind) elif space == 'V3': - return self._get_Hodge_operator(self._Hodge_operators[3], dual=dual, kind=kind) + return self._get_hodge_operator(self._hodge_operators[3], dual=dual, kind=kind) elif space is None: - return tuple(self._get_Hodge_operator(H, dual=dual, kind=kind) for H in self._Hodge_operators) + return tuple(self._get_hodge_operator(H, dual=dual, kind=kind) for H in self._hodge_operators) #============================================================================== @@ -527,7 +527,7 @@ def __init__(self, *, domain_h, spaces): else: raise ValueError('Dimension {} is not available'.format(dim)) - self._Hodge_operators = () + self._hodge_operators = () self._conf_proj = () #-------------------------------------------------------------------------- diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 09b3f87ac..81d829d20 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -57,8 +57,8 @@ def __init__(self, Vh, domain_h, metric='identity', backend_language='python', l self._fem_codomain = Vh # FemLinearOperators - self._primal_Hodge = None - self._dual_Hodge = None + self._primal_hodge = None + self._dual_hodge = None # LinearOperators self._linop = None @@ -78,26 +78,26 @@ def __init__(self, Vh, domain_h, metric='identity', backend_language='python', l 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_is_stored = os.path.exists(primal_Hodge_storage_fn) - dual_Hodge_is_stored = os.path.exists(dual_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_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) + 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 dual_hodge_is_stored: + assert primal_hodge_is_stored + print(" ... loading dual Hodge sparse matrix from " + dual_hodge_storage_fn) + self._dual_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 primal_Hodge_is_stored + assert not primal_hodge_is_stored print("[HodgeOperator] assembling both sparse matrices for storage...") self.assemble_matrix() - print("[HodgeOperator] storing primal Hodge sparse matrix in " + primal_Hodge_storage_fn) - save_npz(primal_Hodge_storage_fn, self._sparse_matrix) + print("[HodgeOperator] storing primal Hodge sparse matrix in " + primal_hodge_storage_fn) + save_npz(primal_hodge_storage_fn, self._sparse_matrix) self.assemble_dual_sparse_matrix() - print("[HodgeOperator] storing dual Hodge sparse matrix in " + dual_Hodge_storage_fn) - save_npz(dual_Hodge_storage_fn, self._dual_sparse_matrix) + print("[HodgeOperator] storing dual Hodge sparse matrix in " + dual_hodge_storage_fn) + save_npz(dual_hodge_storage_fn, self._dual_sparse_matrix) else: # matrices are not stored, we will probably compute them later pass @@ -130,7 +130,7 @@ def assemble_matrix(self): self._linop = ah.assemble() # Mass matrix in stencil format self._sparse_matrix = self._linop.tosparse() - self._primal_Hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop, sparse_matrix=self._sparse_matrix) + self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop, sparse_matrix=self._sparse_matrix) # which of the two assemblys for the sparse dual matrix is better? For now use the exact one. def assemble_dual_sparse_matrix(self): @@ -161,7 +161,7 @@ def assemble_dual_sparse_matrix(self): self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) else: M_m = M.tosparse() @@ -170,7 +170,7 @@ def assemble_dual_sparse_matrix(self): self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) def assemble_dual_matrix(self, solver ='gmres', **kwargs): @@ -201,12 +201,12 @@ def assemble_dual_matrix(self, solver ='gmres', **kwargs): self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) self._dual_sparse_matrix = None - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) else: inv_M = inverse(M, solver=solver, **kwargs) self._dual_sparse_matrix = None - self._dual_Hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) @property def linop(self): @@ -237,15 +237,15 @@ def dual_tosparse(self): return self._dual_sparse_matrix @property - def Hodge(self): + def hodge(self): if self._linop is None: self.assemble_matrix() - return self._primal_Hodge + return self._primal_hodge @property - def dual_Hodge(self): + def dual_hodge(self): if self._dual_linop is None: self.assemble_dual_sparse_matrix() - return self._dual_Hodge \ No newline at end of file + return self._dual_hodge \ No newline at end of file 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 656d91143..2ae28e5d3 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -105,9 +105,9 @@ def solve_h1_source_pbm( print('Hodge operators...') # multi-patch (broken) linear operators - H0 = derham_h.Hodge_operators(space='V0', kind='linop', backend_language=backend_language) - H1 = derham_h.Hodge_operators(space='V1', kind='linop', backend_language=backend_language) - dH0 = derham_h.Hodge_operators(space='V0', kind='linop', dual=True, backend_language=backend_language) + H0 = derham_h.hodge_operators(space='V0', kind='linop', backend_language=backend_language) + H1 = derham_h.hodge_operators(space='V1', kind='linop', backend_language=backend_language) + dH0 = derham_h.hodge_operators(space='V0', kind='linop', dual=True, backend_language=backend_language) print('conforming projection operators...') # conforming Projections (should take into account the boundary conditions 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 2009941eb..06dd5f8a9 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -132,8 +132,8 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma t_stamp = time_count(t_stamp) print('Hodge operators...') # multi-patch (broken) linear operators / matrices - H0, H1, H2 = derham_h.Hodge_operators(kind='linop', backend_language=backend_language, load_dir=m_load_dir) - dH0, dH1, dH2 = derham_h.Hodge_operators(kind='linop', dual=True, backend_language=backend_language, load_dir=m_load_dir) + H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language, load_dir=m_load_dir) + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language, load_dir=m_load_dir) t_stamp = time_count(t_stamp) print('conforming projection operators...') 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 c7f8481cf..46062eaca 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -144,8 +144,8 @@ def solve_hcurl_source_pbm( print(' .. Hodge operators...') # multi-patch (broken) linear operators / matrices # other option: define as Hodge Operators: - H0, H1, H2 = derham_h.Hodge_operators(kind='linop', backend_language=backend_language) - dH0, dH1, dH2 = derham_h.Hodge_operators(kind='linop', dual=True, backend_language=backend_language) + H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language) + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language) t_stamp = time_count(t_stamp) From 168ca92f3727141a7a61f54f478a7a42d6802908 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 11:34:15 +0200 Subject: [PATCH 040/102] add missing new line at end of file --- psydac/feec/hodge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 81d829d20..38623be1f 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -248,4 +248,5 @@ def dual_hodge(self): if self._dual_linop is None: self.assemble_dual_sparse_matrix() - return self._dual_hodge \ No newline at end of file + return self._dual_hodge + \ No newline at end of file From 84a75451a6fb84f650b2d09af70ba656fc40ac7a Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 12:06:38 +0200 Subject: [PATCH 041/102] remove the sparse kind --- psydac/api/feec.py | 25 ++++++------------- .../test_feec_conf_projectors_cart_2d.py | 13 +++++++--- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 58a331b23..9681e5ecf 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -282,8 +282,6 @@ def projectors(self, *, kind='global', nquads=None): def derivatives(self, kind='femlinop'): if kind == 'femlinop': return self._derivatives - elif kind == 'sparse': - return tuple(b_diff.tosparse for b_diff in self._derivatives) elif kind == 'linop': return tuple(b_diff.linop for b_diff in self._derivatives) @@ -302,14 +300,13 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h Apply homogenous boundary conditions if True kind : - The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + The kind of the projector, can be 'femlinop' or 'linop'. - 'femlinop' returns a psydac FemLinearOperator (default) - - 'sparse' returns a scipy sparse matrix - 'linop' returns a psydac LinearOperator Returns ------- - cP0, cP1, cP2 : Tuple of , or + cP0, cP1, cP2 : Tuple of or The conforming projectors of each space and in desired form. """ @@ -337,8 +334,6 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h if kind == 'femlinop': return cP0, cP1, cP2 - elif kind == 'sparse': - return cP0.tosparse, cP1.tosparse, cP2.tosparse elif kind == 'linop': return cP0.linop, cP1.linop, cP2.linop @@ -395,9 +390,8 @@ def _get_hodge_operator(self, H, dual=False, kind='femlinop'): If True, returns the dual Hodge operator kind : - The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + The kind of the projector, can be 'femlinop' or 'linop'. - 'femlinop' returns a psydac FemLinearOperator (default) - - 'sparse' returns a scipy sparse matrix - 'linop' returns a psydac LinearOperator Returns @@ -408,15 +402,11 @@ def _get_hodge_operator(self, H, dual=False, kind='femlinop'): if not dual: if kind == 'femlinop': return H.hodge - elif kind == 'sparse': - return H.tosparse elif kind == 'linop': return H.linop else: if kind == 'femlinop': return H.dual_hodge - elif kind == 'sparse': - return H.dual_tosparse elif kind == 'linop': return H.dual_linop @@ -435,9 +425,8 @@ def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu If True, returns the dual Hodge operator. kind : - The kind of the projector, can be 'femlinop', 'sparse' or 'linop'. + The kind of the projector, can be 'femlinop' or 'linop'. - 'femlinop' returns a psydac FemLinearOperator (default) - - 'sparse' returns a scipy sparse matrix - 'linop' returns a psydac LinearOperator backend_language : str @@ -450,11 +439,11 @@ def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu ------- Either one of the following Hodge operators of the specified kind and space or all of them if space is None. - H0 : , or + H0 : or - H1 : , or + H1 : or - H2 : , or + H2 : or """ if not self._hodge_operators: diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index fca2845e6..0838e8dd3 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -146,12 +146,17 @@ def test_conf_projectors_2d( geomP0, geomP1, geomP2 = derham_h.projectors(nquads=nquads) # conforming projections (scipy matrices) - cP0, cP1, cP2 = derham_h.conforming_projectors(kind='sparse', p_moments=mom_pres, hom_bc=hom_bc) + cP0, cP1, cP2 = derham_h.conforming_projectors(p_moments=mom_pres, hom_bc=hom_bc) + cP0, cP1, cP2 = (m.tosparse for m in (cP0, cP1, cP2)) + + M0, M1, M2 = derham_h.hodge_operators() + M0, M1, M2 = (m.tosparse for m in (M0, M1, M2)) - M0, M1, M2 = derham_h.Hodge_operators(kind='sparse') - M0_inv, M1_inv, M2_inv = derham_h.Hodge_operators(kind='sparse', dual=True) + M0_inv, M1_inv, M2_inv = derham_h.hodge_operators(dual=True) + M0_inv, M1_inv, M2_inv = (m.tosparse for m in (M0_inv, M1_inv, M2_inv)) - bD0, bD1 = derham_h.derivatives(kind='sparse') + bD0, bD1 = derham_h.derivatives() + bD0, bD1 = (m.tosparse for m in (bD0, bD1)) D0 = bD0 @ cP0 # Conga grad D1 = bD1 @ cP1 # Conga curl or div From 819b5fd0b8cb35daf12353a04c3875f125d6dc95 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 12:16:52 +0200 Subject: [PATCH 042/102] add hodge_operator function --- psydac/api/feec.py | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 9681e5ecf..f21d349c7 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -411,7 +411,7 @@ def _get_hodge_operator(self, H, dual=False, kind='femlinop'): return H.dual_linop #-------------------------------------------------------------------------- - def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): + def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): """ Returns the Hodge operator for the given space and specified kind. @@ -437,20 +437,14 @@ def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu Returns ------- - Either one of the following Hodge operators of the specified kind and space or all of them if space is None. + The Hodge operator of the space of the specified kind. - H0 : or - - H1 : or - - H2 : or + H : or """ if not self._hodge_operators: self._init_hodge_operators(backend_language=backend_language, load_dir=load_dir) - H0, H1, H2 = self._hodge_operators - if space == 'V0': return self._get_hodge_operator(self._hodge_operators[0], dual=dual, kind=kind) @@ -463,8 +457,36 @@ def hodge_operators(self, space=None, dual=False, kind='femlinop', backend_langu elif space == 'V3': return self._get_hodge_operator(self._hodge_operators[3], dual=dual, kind=kind) - elif space is None: - return tuple(self._get_hodge_operator(H, dual=dual, kind=kind) for H in self._hodge_operators) + #-------------------------------------------------------------------------- + def hodge_operators(self, dual=False, kind='femlinop', backend_language='python', load_dir=None): + """ + Returns the Hodge operators for the specified kind. + + Parameters + ---------- + dual : bool + If True, returns the dual Hodge operator. + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + backend_language : str + The backend used to accelerate the code, default is 'python'. + + load_dir : str or None + Directory to load the Hodge operator from, if None the operator is computed on demand. + + Returns + ------- + The Hodge operators of all spaces and of the specified kind. + """ + + if not self._hodge_operators: + self._init_hodge_operators(backend_language=backend_language, load_dir=load_dir) + + return tuple(self._get_hodge_operator(H, dual=dual, kind=kind) for H in self._hodge_operators) #============================================================================== From 3bd05c2a29fcfec2ede4cb4796c490da8151e740 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 12:19:49 +0200 Subject: [PATCH 043/102] add NotImplementedError --- psydac/feec/hodge.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 38623be1f..d081a22eb 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -71,7 +71,9 @@ def __init__(self, Vh, domain_h, metric='identity', backend_language='python', l self._domain_h = domain_h self._backend_language = backend_language - assert metric == 'identity' + if not (metric == 'identity'): + raise NotImplementedError('only the identity metric is available') + self._metric = metric if load_dir and isinstance(load_dir, str): From b0d5a4e836bf9c2ca0f14cac4dc2e0240d06e9d9 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 14:04:49 +0200 Subject: [PATCH 044/102] fix typo --- psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 2ae28e5d3..027173a44 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -105,9 +105,9 @@ def solve_h1_source_pbm( print('Hodge operators...') # multi-patch (broken) linear operators - H0 = derham_h.hodge_operators(space='V0', kind='linop', backend_language=backend_language) - H1 = derham_h.hodge_operators(space='V1', kind='linop', backend_language=backend_language) - dH0 = derham_h.hodge_operators(space='V0', kind='linop', dual=True, backend_language=backend_language) + H0 = derham_h.hodge_operator(space='V0', kind='linop', backend_language=backend_language) + H1 = derham_h.hodge_operator(space='V1', kind='linop', backend_language=backend_language) + dH0 = derham_h.hodge_operator(space='V0', kind='linop', dual=True, backend_language=backend_language) print('conforming projection operators...') # conforming Projections (should take into account the boundary conditions From 0ecfbebf99e0b4f8c85314922dd22d3accd11811 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 14:11:18 +0200 Subject: [PATCH 045/102] use a non-sparse lu decomposition to calculate inverse Hodges --- psydac/feec/hodge.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index d081a22eb..474a566b8 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -3,7 +3,7 @@ from scipy.sparse import save_npz, load_npz from scipy.sparse import block_diag -from scipy.sparse.linalg import inv +from scipy.linalg import inv from sympde.topology import element_of, elements_of from sympde.topology.space import ScalarFunction @@ -154,12 +154,11 @@ def assemble_dual_sparse_matrix(self): inv_M_blocks = [] for i in range(nrows): - Mii = M[i, i].tosparse() - inv_Mii = inv(Mii.tocsc()) - inv_Mii.eliminate_zeros() + Mii = M[i, i].toarray() + inv_Mii = inv(Mii) inv_M_blocks.append(inv_Mii) - inv_M = block_diag(inv_M_blocks) + inv_M = block_diag(inv_M_blocks, format='csr') self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) From df0a5e8c6e476dcd862bc20b8d384cd8fb124ce7 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:01:51 +0200 Subject: [PATCH 046/102] use a non-sparse lu decomposition to calculate inverse Hodges - also for single patch --- psydac/feec/hodge.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 474a566b8..037a9e3e4 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -165,9 +165,8 @@ def assemble_dual_sparse_matrix(self): self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) else: - M_m = M.tosparse() - inv_M = inv(M_m.tocsc()) - inv_M.eliminate_zeros() + M_m = M.toarray() + inv_M = inv(M_m) self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) From f1a3abccc77fac948ddab4f361dd79ca9bd95a2f Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:03:47 +0200 Subject: [PATCH 047/102] rename projectors argument --- psydac/feec/global_projectors.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_projectors.py index e219c3fbf..e49c0f80b 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_projectors.py @@ -729,11 +729,11 @@ def __init__(self, V0h): self._P0s = [Projector_H1(V) for V in V0h.spaces] self._V0h = V0h # multipatch Fem Space - def __call__(self, funs_log): + def __call__(self, funs): """ project a list of functions given in the logical domain """ - u0s = [P(fun) for P, fun, in zip(self._P0s, funs_log)] + u0s = [P(fun) for P, fun, in zip(self._P0s, funs)] u0_coeffs = BlockVector(self._V0h.coeff_space, blocks=[u0j.coeffs for u0j in u0s]) @@ -752,11 +752,11 @@ def __init__(self, V1h, nquads=None): self._P1s = [Projector_Hcurl(V, nquads=nquads) for V in V1h.spaces] self._V1h = V1h # multipatch Fem Space - def __call__(self, funs_log): + def __call__(self, funs): """ project a list of functions given in the logical domain """ - E1s = [P(fun) for P, fun, in zip(self._P1s, funs_log)] + E1s = [P(fun) for P, fun, in zip(self._P1s, funs)] E1_coeffs = BlockVector(self._V1h.coeff_space, blocks=[E1j.coeffs for E1j in E1s]) @@ -775,11 +775,11 @@ def __init__(self, V2h, nquads=None): self._P2s = [Projector_L2(V, nquads=nquads) for V in V2h.spaces] self._V2h = V2h # multipatch Fem Space - def __call__(self, funs_log): + def __call__(self, funs): """ project a list of functions given in the logical domain """ - B2s = [P(fun) for P, fun, in zip(self._P2s, funs_log)] + B2s = [P(fun) for P, fun, in zip(self._P2s, funs)] B2_coeffs = BlockVector(self._V2h.coeff_space, blocks=[B2j.coeffs for B2j in B2s]) From 73ca61086c4e4e57029680cf9bfd0b49b881f96f Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:08:56 +0200 Subject: [PATCH 048/102] Update psydac/fem/basic.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/fem/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 17192c086..1a42791e2 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -452,7 +452,7 @@ def tosparse(self): return self._sparse_matrix - def __call__(self, u): + def __call__(self, u, *, out=None): assert isinstance(u, FemField) assert u.space == self.fem_domain From 3ec07d3c0ed6763c5d8da67872e154dc7ff4e9cd Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:09:43 +0200 Subject: [PATCH 049/102] Update psydac/fem/basic.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/fem/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 1a42791e2..57138fc4a 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -465,7 +465,7 @@ def __call__(self, u, *, out=None): return FemField(self.fem_codomain, coeffs=coeffs) - def dot( self, f_coeffs, out=None ): + def dot(self, f_coeffs, *, out=None): assert isinstance(f_coeffs, Vector) assert f_coeffs.space is self._linop_domain From bd8953fc87f846b5b6bfc790ebb10a01c8a0900c Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:10:24 +0200 Subject: [PATCH 050/102] Update psydac/fem/basic.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/fem/basic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 57138fc4a..4b3055774 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -386,9 +386,9 @@ def __isub__(self, other): #=============================================================================== class FemLinearOperator: """ - Linear operators with an additional Fem layer. - There is also a shorthand access to sparse matrices as they are sometimes used in the FEEC interfaces. - + Linear operators with an additional FEM layer. + There is also a shorthand access to sparse matrices as they are sometimes + used in the FEEC interfaces. Parameters ---------- fem_domain : psydac.fem.basic.FemSpace From d0e2baaa332454f7f497dc0ff764d5d32f145879 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:11:20 +0200 Subject: [PATCH 051/102] Update psydac/fem/basic.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/fem/basic.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 4b3055774..98b16edbd 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -404,9 +404,14 @@ class FemLinearOperator: Sparse matrix representation of the linear operator. """ - def __init__(self, fem_domain, fem_codomain, linop=None, sparse_matrix=None): + def __init__(self, fem_domain, fem_codomain, *, linop=None, sparse_matrix=None): assert isinstance(fem_domain, FemSpace) assert isinstance(fem_codomain, FemSpace) + if linop is not None: + assert isinstance(linop, LinearOperator) + if sparse_matrix is not None: + from scipy.sparse import spmatrix + assert isinstance(sparse_matrix, spmatrix) self._fem_domain = fem_domain self._fem_codomain = fem_codomain From f6951d683e4b725651bfd2282e188969040bb9a6 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:12:02 +0200 Subject: [PATCH 052/102] Update psydac/fem/basic.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/fem/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index 98b16edbd..ddbfed91c 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -456,7 +456,7 @@ def tosparse(self): return self._sparse_matrix - + #-------------------------------------------------------------------------- def __call__(self, u, *, out=None): assert isinstance(u, FemField) assert u.space == self.fem_domain From 5cce3c65241a41fb06b7303826decf55d5310491 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:13:33 +0200 Subject: [PATCH 053/102] change docstring --- psydac/fem/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index ddbfed91c..bafa1e796 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -392,10 +392,10 @@ class FemLinearOperator: Parameters ---------- fem_domain : psydac.fem.basic.FemSpace - The discrete space + The discrete space of the domain fem_codomain : psydac.fem.basic.FemSpace - Number of polynomial moments to be preserved in the projection. + The discrete space of the codomain linop : (optional) Linear Operator. From 0460a505f9b674ce1304c879ae0d59c77fd1afc0 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:14:40 +0200 Subject: [PATCH 054/102] add comma and newline --- psydac/linalg/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index d0d406133..0c5c8fab9 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -12,7 +12,7 @@ 'array_to_psydac', 'petsc_to_psydac', '_sym_ortho', - 'SparseMatrixLinearOperator' + 'SparseMatrixLinearOperator', ) #============================================================================== @@ -223,4 +223,4 @@ def toarray(self): return self._matrix.toarray() def transpose(self, conjugate=False): - return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) \ No newline at end of file + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) From 453dcf19fa37d3ea0d340542f494862044c96557 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:16:23 +0200 Subject: [PATCH 055/102] change typo in de Rham --- psydac/api/discretization.py | 8 ++++---- psydac/api/feec.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index a11409335..82904f555 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -148,7 +148,7 @@ def get_max_degree(*spaces): #============================================================================== def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): """ - Create a discrete De Rham sequence from a symbolic one. + Create a discrete de Rham sequence from a symbolic one. This function creates the discrete spaces from the symbolic ones, and then creates a DiscreteDeRham object from them. @@ -170,7 +170,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): Returns ------- DiscreteDeRham - The discrete De Rham sequence containing the discrete spaces, + The discrete de Rham sequence containing the discrete spaces, differential operators and projectors. See Also @@ -196,7 +196,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): #============================================================================== def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): """ - Create a discrete multipatch De Rham sequence from a symbolic one. + Create a discrete multipatch de Rham sequence from a symbolic one. This function creates the broken discrete spaces from the symbolic ones, and then creates a DiscreteDeRhamMultipatch object from them. @@ -215,7 +215,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): Returns ------- DiscreteDeRhamMultipatch - The discrete multipatch De Rham sequence containing the discrete spaces, + The discrete multipatch de Rham sequence containing the discrete spaces, differential operators and projectors. See Also diff --git a/psydac/api/feec.py b/psydac/api/feec.py index f21d349c7..08a711c07 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -178,14 +178,14 @@ def callable_mapping(self): #-------------------------------------------------------------------------- def projectors(self, *, kind='global', nquads=None): """Projectors mapping callable functions of the physical coordinates to a - corresponding `FemField` object in the De Rham sequence. + corresponding `FemField` object in the de Rham sequence. Parameters ---------- kind : str Type of the projection : at the moment, only global is accepted and returns geometric commuting projectors based on interpolation/histopolation - for the De Rham sequence (GlobalProjector objects). + for the de Rham sequence (GlobalProjector objects). nquads : list(int) | tuple(int) Number of quadrature points along each direction, to be used in Gauss @@ -196,7 +196,7 @@ def projectors(self, *, kind='global', nquads=None): P0, ..., Pn : callables Projectors that can be called on any callable function that maps from the physical space to R (scalar case) or R^d (vector case) and - returns a FemField belonging to the i-th space of the De Rham sequence + returns a FemField belonging to the i-th space of the de Rham sequence """ if not (kind == 'global'): @@ -491,7 +491,7 @@ def hodge_operators(self, dual=False, kind='femlinop', backend_language='python' #============================================================================== class DiscreteDeRhamMultipatch(DiscreteDeRham): - """ Represents the discrete De Rham sequence for multipatch domains. + """ Represents the discrete de Rham sequence for multipatch domains. It only works when the number of patches>1. Parameters @@ -500,7 +500,7 @@ class DiscreteDeRhamMultipatch(DiscreteDeRham): The discrete domain spaces: - The discrete spaces that are contained in the De Rham sequence + The discrete spaces that are contained in the de Rham sequence """ def __init__(self, *, domain_h, spaces): From d3989340260609be0a97adf3db70fafce573eaa1 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:18:54 +0200 Subject: [PATCH 056/102] add newline --- psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py | 1 + 1 file changed, 1 insertion(+) 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 fe6aed82f..972eaea63 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -20,6 +20,7 @@ def test_poisson_pretzel_f(): assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 + def test_poisson_pretzel_f_nc(): source_type = 'manu_poisson_2' From 7ed804277436b87182db9e8d7ab1f6a1c554b8bd Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:22:58 +0200 Subject: [PATCH 057/102] remove empty lines --- psydac/feec/multipatch/examples/ppc_test_cases.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/psydac/feec/multipatch/examples/ppc_test_cases.py b/psydac/feec/multipatch/examples/ppc_test_cases.py index 2fd889c49..f0f7d0f8c 100644 --- a/psydac/feec/multipatch/examples/ppc_test_cases.py +++ b/psydac/feec/multipatch/examples/ppc_test_cases.py @@ -4,10 +4,6 @@ from sympy import pi, cos, sin, Tuple, exp, atan, atan2 from sympy.functions.special.error_functions import erf - - - - # todo [MCP, 12/02/2022]: add an 'equation' argument to be able to return # 'exact solution' From 955656e6e930fea32badb244cb261474ecf4da11 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:24:17 +0200 Subject: [PATCH 058/102] Update psydac/feec/conforming_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/conforming_projectors.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 47eaf59f4..b690fdbb0 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1477,9 +1477,6 @@ class ConformingProjection_V1(FemLinearOperator): hom_bc : Apply homogenous boundary conditions if True """ - # todo (MCP, 16.03.2021): - # - allow case without interfaces (single or multipatch) - def __init__( self, V1h, From 900389069096c817ee4b80533d8b2a50513892b9 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:24:32 +0200 Subject: [PATCH 059/102] Update psydac/feec/conforming_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/conforming_projectors.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index b690fdbb0..a277a37a3 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1434,9 +1434,6 @@ class ConformingProjection_V0(FemLinearOperator): hom_bc : Apply homogenous boundary conditions if True """ - # todo (MCP, 16.03.2021): - # - allow case without interfaces (single or multipatch) - def __init__( self, V0h, From ec2426c2db1d6ea5d742f25044a7ed9ee2aeb4a9 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:25:26 +0200 Subject: [PATCH 060/102] Update psydac/feec/conforming_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/conforming_projectors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index a277a37a3..1bb76b057 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -15,6 +15,10 @@ from psydac.utilities.quadratures import gauss_legendre from psydac.linalg.utilities import SparseMatrixLinearOperator +__all__ = ( + 'ConformingProjectionV0', + 'ConformingProjectionV1' +) def get_patch_index_from_face(domain, face): """ From 5c97438cf05297039a942a6481b6e9fc705dfab1 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:30:00 +0200 Subject: [PATCH 061/102] fix doc strings --- psydac/feec/conforming_projectors.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 1bb76b057..619a4d8e2 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -17,7 +17,7 @@ __all__ = ( 'ConformingProjectionV0', - 'ConformingProjectionV1' + 'ConformingProjectionV1', ) def get_patch_index_from_face(domain, face): @@ -1425,7 +1425,8 @@ def edge_moment_index(p, i, axis, ext): class ConformingProjection_V0(FemLinearOperator): """ Conforming projection from global broken V0 space to conforming global V0 space - Defined by averaging of interface dofs + Defined by averaging of interface (including vertex) dofs + and adding moment correction terms Parameters ---------- @@ -1464,8 +1465,8 @@ def __init__( class ConformingProjection_V1(FemLinearOperator): """ Conforming projection from global broken V1 space to conforming V1 global space - - proj.dot(v) returns the conforming projection of v, computed by solving linear system + Defined by averaging of (only) interface dofs + and adding moment correction terms Parameters ---------- From 07fff489ec86ca2d18f6a91a777c391d06c0d02f Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:37:14 +0200 Subject: [PATCH 062/102] change projector class names --- psydac/api/feec.py | 48 +++++++++---------- psydac/api/tests/test_api_2d_fields.py | 4 +- psydac/feec/global_projectors.py | 26 +++++----- .../feec/tests/test_commuting_projections.py | 34 ++++++------- .../tests/test_differentiation_matrices.py | 2 +- psydac/feec/tests/test_global_projectors.py | 6 +-- .../feec/tests/test_projections_parallel.py | 22 ++++----- 7 files changed, 71 insertions(+), 71 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 08a711c07..16313f27e 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -6,11 +6,11 @@ from psydac.feec.derivatives import BrokenGradient_2D from psydac.feec.derivatives import BrokenScalarCurl_2D -from psydac.feec.global_projectors import Projector_H1, Projector_Hcurl, Projector_H1vec -from psydac.feec.global_projectors import Projector_Hdiv, Projector_L2 -from psydac.feec.global_projectors import Multipatch_Projector_H1 -from psydac.feec.global_projectors import Multipatch_Projector_Hcurl -from psydac.feec.global_projectors import Multipatch_Projector_L2 +from psydac.feec.global_projectors import ProjectorH1, ProjectorHcurl, ProjectorH1vec +from psydac.feec.global_projectors import ProjectorHdiv, ProjectorL2 +from psydac.feec.global_projectors import MultipatchProjectorH1 +from psydac.feec.global_projectors import MultipatchProjectorHcurl +from psydac.feec.global_projectors import MultipatchProjectorL2 from psydac.feec.conforming_projectors import ConformingProjection_V0 from psydac.feec.conforming_projectors import ConformingProjection_V1 @@ -214,8 +214,8 @@ def projectors(self, *, kind='global', nquads=None): assert all(nq >= 1 for nq in nquads) if self.dim == 1: - P0 = Projector_H1(self.V0) - P1 = Projector_L2(self.V1, nquads) + P0 = ProjectorH1(self.V0) + P1 = ProjectorL2(self.V1, nquads) if self.mapping: P0_m = lambda f: P0(pull_1d_h1(f, self.callable_mapping)) P1_m = lambda f: P1(pull_1d_l2(f, self.callable_mapping)) @@ -223,19 +223,19 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1 elif self.dim == 2: - P0 = Projector_H1(self.V0) - P2 = Projector_L2(self.V2, nquads) + P0 = ProjectorH1(self.V0) + P2 = ProjectorL2(self.V2, nquads) kind = self.V1.symbolic_space.kind.name if kind == 'hcurl': - P1 = Projector_Hcurl(self.V1, nquads) + P1 = ProjectorHcurl(self.V1, nquads) elif kind == 'hdiv': - P1 = Projector_Hdiv(self.V1, nquads) + P1 = ProjectorHdiv(self.V1, nquads) else: raise TypeError('projector of space type {} is not available'.format(kind)) if self.has_vec : - Pvec = Projector_H1vec(self.H1vec, nquads) + Pvec = ProjectorH1vec(self.H1vec, nquads) if self.mapping: P0_m = lambda f: P0(pull_2d_h1(f, self.callable_mapping)) @@ -256,12 +256,12 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1, P2 elif self.dim == 3: - P0 = Projector_H1 (self.V0) - P1 = Projector_Hcurl(self.V1, nquads) - P2 = Projector_Hdiv (self.V2, nquads) - P3 = Projector_L2 (self.V3, nquads) + P0 = ProjectorH1 (self.V0) + P1 = ProjectorHcurl(self.V1, nquads) + P2 = ProjectorHdiv (self.V2, nquads) + P3 = ProjectorL2 (self.V3, nquads) if self.has_vec : - Pvec = Projector_H1vec(self.H1vec) + Pvec = ProjectorH1vec(self.H1vec) if self.mapping: P0_m = lambda f: P0(pull_3d_h1 (f, self.callable_mapping)) P1_m = lambda f: P1(pull_3d_hcurl(f, self.callable_mapping)) @@ -561,13 +561,13 @@ def projectors(self, *, kind='global', nquads=None): Returns ------- - P0: + P0: Patch wise H1 projector - P1: + P1: Patch wise Hcurl projector - P2: + P2: Patch wise L2 projector Notes @@ -583,15 +583,15 @@ def projectors(self, *, kind='global', nquads=None): raise NotImplementedError("1D projectors are not available") elif self.dim == 2: - P0 = Multipatch_Projector_H1(self.V0) + P0 = MultipatchProjectorH1(self.V0) if self.sequence[1] == 'hcurl': - P1 = Multipatch_Projector_Hcurl(self.V1, nquads=nquads) + P1 = MultipatchProjectorHcurl(self.V1, nquads=nquads) else: - P1 = None # TODO: Multipatch_Projector_Hdiv(self.V1, nquads=nquads) + P1 = None # TODO: Multipatch_ProjectorHdiv(self.V1, nquads=nquads) raise NotImplementedError('2D sequence with H-div not available yet') - P2 = Multipatch_Projector_L2(self.V2, nquads=nquads) + P2 = MultipatchProjectorL2(self.V2, nquads=nquads) if self.mapping: diff --git a/psydac/api/tests/test_api_2d_fields.py b/psydac/api/tests/test_api_2d_fields.py index abd7a1d75..c1f0b893a 100644 --- a/psydac/api/tests/test_api_2d_fields.py +++ b/psydac/api/tests/test_api_2d_fields.py @@ -35,7 +35,7 @@ from psydac.fem.basic import FemField from psydac.api.discretization import discretize from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL -from psydac.feec.global_projectors import Projector_H1 +from psydac.feec.global_projectors import ProjectorH1 # ... get the mesh directory try: @@ -142,7 +142,7 @@ def run_boundary_field_test(domain, boundary, f, ncells): x,y = domain.coordinates f_lambda = lambdify([x,y], f, 'math') - Pi0 = Projector_H1(Vh) + Pi0 = ProjectorH1(Vh) fh = Pi0(f_lambda) fh.coeffs.update_ghost_regions() diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_projectors.py index e49c0f80b..c7deb9a3f 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_projectors.py @@ -17,8 +17,8 @@ from abc import ABCMeta, abstractmethod -__all__ = ('GlobalProjector', 'Projector_H1', 'Projector_Hcurl', 'Projector_Hdiv', 'Projector_L2', - 'Multipatch_Projector_H1', 'Multipatch_Projector_Hcurl', 'Multipatch_Projector_L2', +__all__ = ('GlobalProjector', 'ProjectorH1', 'ProjectorHcurl', 'ProjectorHdiv', 'ProjectorL2', + 'MultipatchProjectorH1', 'MultipatchProjectorHcurl', 'MultipatchProjectorL2', 'evaluate_dofs_1d_0form', 'evaluate_dofs_1d_1form', 'evaluate_dofs_2d_0form', 'evaluate_dofs_2d_1form_hcurl', 'evaluate_dofs_2d_1form_hdiv', 'evaluate_dofs_2d_2form', 'evaluate_dofs_3d_0form', 'evaluate_dofs_3d_1form', 'evaluate_dofs_3d_2form', 'evaluate_dofs_3d_3form') @@ -392,7 +392,7 @@ def __call__(self, fun): #============================================================================== # SINGLEPATCH PROJECTORS #============================================================================== -class Projector_H1(GlobalProjector): +class ProjectorH1(GlobalProjector): """ Projector from H1 to an H1-conforming finite element space (i.e. a finite dimensional subspace of H1) constructed with tensor-product B-splines in 1, @@ -442,7 +442,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_Hcurl(GlobalProjector): +class ProjectorHcurl(GlobalProjector): """ Projector from H(curl) to an H(curl)-conforming finite element space, i.e. a finite dimensional subspace of H(curl), constructed with tensor-product @@ -515,7 +515,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_Hdiv(GlobalProjector): +class ProjectorHdiv(GlobalProjector): """ Projector from H(div) to an H(div)-conforming finite element space, i.e. a finite dimensional subspace of H(div), constructed with tensor-product @@ -592,7 +592,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_L2(GlobalProjector): +class ProjectorL2(GlobalProjector): """ Projector from L2 to an L2-conforming finite element space (i.e. a finite dimensional subspace of L2) constructed with tensor-product M-splines in 1, @@ -652,7 +652,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class Projector_H1vec(GlobalProjector): +class ProjectorH1vec(GlobalProjector): """ Projector from H1^3 = H1 x H1 x H1 to a conforming finite element space, i.e. a finite dimensional subspace of H1^3, constructed with tensor-product @@ -719,14 +719,14 @@ def __call__(self, fun): #============================================================================== # MULTIPATCH PROJECTORS (2D) #============================================================================== -class Multipatch_Projector_H1: +class MultipatchProjectorH1: """ to apply the H1 projection (2D) on every patch """ def __init__(self, V0h): - self._P0s = [Projector_H1(V) for V in V0h.spaces] + self._P0s = [ProjectorH1(V) for V in V0h.spaces] self._V0h = V0h # multipatch Fem Space def __call__(self, funs): @@ -741,7 +741,7 @@ def __call__(self, funs): return FemField(self._V0h, coeffs=u0_coeffs) #============================================================================== -class Multipatch_Projector_Hcurl: +class MultipatchProjectorHcurl: """ to apply the Hcurl projection (2D) on every patch @@ -749,7 +749,7 @@ class Multipatch_Projector_Hcurl: def __init__(self, V1h, nquads=None): - self._P1s = [Projector_Hcurl(V, nquads=nquads) for V in V1h.spaces] + self._P1s = [ProjectorHcurl(V, nquads=nquads) for V in V1h.spaces] self._V1h = V1h # multipatch Fem Space def __call__(self, funs): @@ -764,7 +764,7 @@ def __call__(self, funs): return FemField(self._V1h, coeffs=E1_coeffs) #============================================================================== -class Multipatch_Projector_L2: +class MultipatchProjectorL2: """ to apply the L2 projection (2D) on every patch @@ -772,7 +772,7 @@ class Multipatch_Projector_L2: def __init__(self, V2h, nquads=None): - self._P2s = [Projector_L2(V, nquads=nquads) for V in V2h.spaces] + self._P2s = [ProjectorL2(V, nquads=nquads) for V in V2h.spaces] self._V2h = V2h # multipatch Fem Space def __call__(self, funs): diff --git a/psydac/feec/tests/test_commuting_projections.py b/psydac/feec/tests/test_commuting_projections.py index 38881220f..8f5ae8c66 100644 --- a/psydac/feec/tests/test_commuting_projections.py +++ b/psydac/feec/tests/test_commuting_projections.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from psydac.feec.global_projectors import Projector_H1, Projector_L2, Projector_Hcurl, Projector_Hdiv +from psydac.feec.global_projectors import ProjectorH1, ProjectorL2, ProjectorHcurl, ProjectorHdiv from psydac.fem.tensor import TensorFemSpace, SplineSpace from psydac.fem.vector import VectorFemSpace from psydac.core.bsplines import make_knots @@ -57,13 +57,13 @@ def test_3d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = ProjectorH1(H1) # Build linear operators on stencil arrays grad = Gradient_3D(H1, Hcurl) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) + P1 = ProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -153,8 +153,8 @@ def test_3d_commuting_pro_2(Nel, Nq, p, bc, m): curl = Curl_3D(Hcurl, Hdiv) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) - P2 = Projector_Hdiv(Hdiv, Nq) + P1 = ProjectorHcurl(Hcurl, Nq) + P2 = ProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives @@ -235,8 +235,8 @@ def test_3d_commuting_pro_3(Nel, Nq, p, bc, m): div = Divergence_3D(Hdiv, L2) # create an instance of the projector class - P2 = Projector_Hdiv(Hdiv, Nq) - P3 = Projector_L2(L2, Nq) + P2 = ProjectorHdiv(Hdiv, Nq) + P3 = ProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -307,13 +307,13 @@ def test_2d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = ProjectorH1(H1) # Build linear operators on stencil arrays grad = Gradient_2D(H1, Hcurl) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) + P1 = ProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -380,13 +380,13 @@ def test_2d_commuting_pro_2(Nel, Nq, p, bc, m): Hdiv = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = ProjectorH1(H1) # Linear operator: 2D vector curl curl = VectorCurl_2D(H1, Hdiv) # create an instance of the projector class - P1 = Projector_Hdiv(Hdiv, Nq) + P1 = ProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -464,8 +464,8 @@ def test_2d_commuting_pro_3(Nel, Nq, p, bc, m): div = Divergence_2D(Hdiv, L2) # create an instance of the projector class - P2 = Projector_Hdiv(Hdiv, Nq) - P3 = Projector_L2(L2, Nq) + P2 = ProjectorHdiv(Hdiv, Nq) + P3 = ProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -544,8 +544,8 @@ def test_2d_commuting_pro_4(Nel, Nq, p, bc, m): curl = ScalarCurl_2D(Hcurl, L2) # create an instance of the projector class - P1 = Projector_Hcurl(Hcurl, Nq) - P2 = Projector_L2(L2, Nq) + P1 = ProjectorHcurl(Hcurl, Nq) + P2 = ProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -610,13 +610,13 @@ def test_1d_commuting_pro_1(Nel, Nq, p, bc, m): L2 = H1.reduce_degree(axes=[0], basis='M') # create an instance of the H1 projector class - P0 = Projector_H1(H1) + P0 = ProjectorH1(H1) # Build linear operators on stencil arrays grad = Derivative_1D(H1, L2) # create an instance of the projector class - P1 = Projector_L2(L2, Nq) + P1 = ProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- diff --git a/psydac/feec/tests/test_differentiation_matrices.py b/psydac/feec/tests/test_differentiation_matrices.py index a60cbae36..8a1eb9e88 100644 --- a/psydac/feec/tests/test_differentiation_matrices.py +++ b/psydac/feec/tests/test_differentiation_matrices.py @@ -12,7 +12,7 @@ from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D from psydac.feec.derivatives import Divergence_2D, Divergence_3D -from psydac.feec.global_projectors import Projector_H1 +from psydac.feec.global_projectors import ProjectorH1 from psydac.ddm.cart import DomainDecomposition from mpi4py import MPI diff --git a/psydac/feec/tests/test_global_projectors.py b/psydac/feec/tests/test_global_projectors.py index 1d1f64282..fadf2b058 100644 --- a/psydac/feec/tests/test_global_projectors.py +++ b/psydac/feec/tests/test_global_projectors.py @@ -5,7 +5,7 @@ from psydac.fem.basic import FemField from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace -from psydac.feec.global_projectors import Projector_H1, Projector_L2 +from psydac.feec.global_projectors import ProjectorH1, ProjectorL2 from psydac.ddm.cart import DomainDecomposition from sympde.topology import Square, Cube from psydac.api.discretization import discretize @@ -33,7 +33,7 @@ def test_H1_projector_1d(domain, ncells, degree, periodic, multiplicity): V0 = TensorFemSpace(domain_decomposition, N) # Projector onto H1 space (1D interpolation) - P0 = Projector_H1(V0) + P0 = ProjectorH1(V0) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) @@ -79,7 +79,7 @@ def test_L2_projector_1d(domain, ncells, degree, periodic, nquads, multiplicity) V1 = V0.reduce_degree(axes=[0], basis='M') # Projector onto L2 space (1D histopolation) - P1 = Projector_L2(V1, nquads=[nquads]) + P1 = ProjectorL2(V1, nquads=[nquads]) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) diff --git a/psydac/feec/tests/test_projections_parallel.py b/psydac/feec/tests/test_projections_parallel.py index 034287bd1..6ec757d18 100644 --- a/psydac/feec/tests/test_projections_parallel.py +++ b/psydac/feec/tests/test_projections_parallel.py @@ -9,7 +9,7 @@ from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace -from psydac.feec.global_projectors import Projector_H1, Projector_L2, Projector_Hcurl, Projector_Hdiv +from psydac.feec.global_projectors import ProjectorH1, ProjectorL2, ProjectorHcurl, ProjectorHdiv from psydac.ddm.cart import DomainDecomposition @@ -19,45 +19,45 @@ def run_projection_comparison(domain, ncells, degree, periodic, funcs, reduce): if len(domain) == 1: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = ProjectorH1 else: opV = lambda V0: V0.reduce_degree(axes=[0], basis='M') - opP = Projector_L2 + opP = ProjectorL2 elif len(domain) == 2: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = ProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M')) - opP = Projector_Hcurl + opP = ProjectorHcurl elif reduce == 2: # (note: this would be more instructive, if the index was 1 as well...) opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[0], basis='M')) - opP = Projector_Hdiv + opP = ProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1], basis='M') - opP = Projector_L2 + opP = ProjectorL2 elif len(domain) == 3: if reduce == 0: opV = lambda V0: V0 - opP = Projector_H1 + opP = ProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[2], basis='M')) - opP = Projector_Hcurl + opP = ProjectorHcurl elif reduce == 2: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1,2], basis='M'), V0.reduce_degree(axes=[0,2], basis='M'), V0.reduce_degree(axes=[0,1], basis='M')) - opP = Projector_Hdiv + opP = ProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1,2], basis='M') - opP = Projector_L2 + opP = ProjectorL2 # Choose number of quadrature points nquads = None if reduce == 0 else [d + 1 for d in degree] From d1e9dc129b9bb59fb91e951f419214155cf5b2bd Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:39:10 +0200 Subject: [PATCH 063/102] change conforming projector class names --- psydac/api/feec.py | 8 ++++---- psydac/feec/conforming_projectors.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 16313f27e..34ed4ecbe 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -12,8 +12,8 @@ from psydac.feec.global_projectors import MultipatchProjectorHcurl from psydac.feec.global_projectors import MultipatchProjectorL2 -from psydac.feec.conforming_projectors import ConformingProjection_V0 -from psydac.feec.conforming_projectors import ConformingProjection_V1 +from psydac.feec.conforming_projectors import ConformingProjectionV0 +from psydac.feec.conforming_projectors import ConformingProjectionV1 from psydac.feec.hodge import HodgeOperator @@ -321,8 +321,8 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h raise NotImplementedError('2D sequence with H-div not available yet') else: - cP0 = ConformingProjection_V0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) - cP1 = ConformingProjection_V1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + cP0 = ConformingProjectionV0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + cP1 = ConformingProjectionV1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) I2 = IdentityOperator(self.V2.coeff_space) cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2, sparse_matrix=I2.tosparse()) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 619a4d8e2..fb2fb0904 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1422,7 +1422,7 @@ def edge_moment_index(p, i, axis, ext): # =============================================================================== -class ConformingProjection_V0(FemLinearOperator): +class ConformingProjectionV0(FemLinearOperator): """ Conforming projection from global broken V0 space to conforming global V0 space Defined by averaging of interface (including vertex) dofs @@ -1462,7 +1462,7 @@ def __init__( self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) -class ConformingProjection_V1(FemLinearOperator): +class ConformingProjectionV1(FemLinearOperator): """ Conforming projection from global broken V1 space to conforming V1 global space Defined by averaging of (only) interface dofs From b40bdbdd1b811e99e04f3f03890c6d2434b4b88a Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 17:42:52 +0200 Subject: [PATCH 064/102] change derivative class names --- psydac/api/feec.py | 30 ++++----- psydac/feec/derivatives.py | 66 +++++++++---------- .../feec/tests/test_commuting_projections.py | 22 +++---- .../tests/test_commuting_projections_dual.py | 12 ++-- .../tests/test_differentiation_matrices.py | 56 ++++++++-------- 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 34ed4ecbe..318137815 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,10 +1,10 @@ from psydac.api.basic import BasicDiscrete -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D -from psydac.feec.derivatives import BrokenGradient_2D -from psydac.feec.derivatives import BrokenScalarCurl_2D +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D +from psydac.feec.derivatives import BrokenGradient2D +from psydac.feec.derivatives import BrokenScalarCurl2D from psydac.feec.global_projectors import ProjectorH1, ProjectorHcurl, ProjectorH1vec from psydac.feec.global_projectors import ProjectorHdiv, ProjectorL2 @@ -66,7 +66,7 @@ def __init__(self, domain_h, *spaces): self._callable_mapping = self._mapping.get_callable_mapping() if self._mapping else None if dim == 1: - D0 = Derivative_1D(spaces[0], spaces[1]) + D0 = Derivative1D(spaces[0], spaces[1]) spaces[0].diff = spaces[0].grad = D0 @@ -77,8 +77,8 @@ def __init__(self, domain_h, *spaces): if kind == 'hcurl': - D0 = Gradient_2D(spaces[0], spaces[1]) - D1 = ScalarCurl_2D(spaces[1], spaces[2]) + D0 = Gradient2D(spaces[0], spaces[1]) + D1 = ScalarCurl2D(spaces[1], spaces[2]) spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 @@ -87,8 +87,8 @@ def __init__(self, domain_h, *spaces): elif kind == 'hdiv': - D0 = VectorCurl_2D(spaces[0], spaces[1]) - D1 = Divergence_2D(spaces[1], spaces[2]) + D0 = VectorCurl2D(spaces[0], spaces[1]) + D1 = Divergence2D(spaces[1], spaces[2]) spaces[0].diff = spaces[0].rot = D0 spaces[1].diff = spaces[1].div = D1 @@ -98,9 +98,9 @@ def __init__(self, domain_h, *spaces): elif dim == 3: - D0 = Gradient_3D(spaces[0], spaces[1]) - D1 = Curl_3D(spaces[1], spaces[2]) - D2 = Divergence_3D(spaces[2], spaces[3]) + D0 = Gradient3D(spaces[0], spaces[1]) + D1 = Curl3D(spaces[1], spaces[2]) + D2 = Divergence3D(spaces[2], spaces[3]) spaces[0].diff = spaces[0].grad = D0 spaces[1].diff = spaces[1].curl = D1 @@ -522,8 +522,8 @@ def __init__(self, *, domain_h, spaces): if self._sequence[1] == 'hcurl': self._derivatives = ( - BrokenGradient_2D(self.V0, self.V1), - BrokenScalarCurl_2D(self.V1, self.V2), # None, + BrokenGradient2D(self.V0, self.V1), + BrokenScalarCurl2D(self.V1, self.V2), # None, ) elif self._sequence[1] == 'hdiv': diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index 5c6d2f104..e4ae65d3d 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -15,18 +15,18 @@ __all__ = ( 'DirectionalDerivativeOperator', - 'Derivative_1D', - 'Gradient_2D', - 'Gradient_3D', - 'ScalarCurl_2D', - 'VectorCurl_2D', - 'Curl_3D', - 'Divergence_2D', - 'Divergence_3D', - 'BrokenGradient_2D', - 'BrokenTransposedGradient_2D', - 'BrokenScalarCurl_2D', - 'BrokenTransposedScalarCurl_2D', + 'Derivative1D', + 'Gradient2D', + 'Gradient3D', + 'ScalarCurl2D', + 'VectorCurl2D', + 'Curl3D', + 'Divergence2D', + 'Divergence3D', + 'BrokenGradient2D', + 'BrokenTransposedGradient2D', + 'BrokenScalarCurl2D', + 'BrokenTransposedScalarCurl2D', 'block_tostencil' ) @@ -366,7 +366,7 @@ def copy(self): self._diffdir, negative=self._negative, transposed=self._transposed) #==================================================================================================== -class Derivative_1D(FemLinearOperator): +class Derivative1D(FemLinearOperator): """ 1D derivative. @@ -390,7 +390,7 @@ def __init__(self, H1, L2): super().__init__(fem_domain = H1, fem_codomain = L2, linop = DirectionalDerivativeOperator(H1.coeff_space, L2.coeff_space, 0)) #==================================================================================================== -class Gradient_2D(FemLinearOperator): +class Gradient2D(FemLinearOperator): """ Gradient operator in 2D. @@ -429,7 +429,7 @@ def __init__(self, H1, Hcurl): super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== -class Gradient_3D(FemLinearOperator): +class Gradient3D(FemLinearOperator): """ Gradient operator in 3D. @@ -471,7 +471,7 @@ def __init__(self, H1, Hcurl): super().__init__(fem_domain = H1, fem_codomain = Hcurl, linop = matrix) #==================================================================================================== -class ScalarCurl_2D(FemLinearOperator): +class ScalarCurl2D(FemLinearOperator): """ Scalar curl operator in 2D: computes a scalar field from a vector field. @@ -510,7 +510,7 @@ def __init__(self, Hcurl, L2): super().__init__(fem_domain = Hcurl, fem_codomain = L2, linop = matrix) #==================================================================================================== -class VectorCurl_2D(FemLinearOperator): +class VectorCurl2D(FemLinearOperator): """ Vector curl operator in 2D: computes a vector field from a scalar field. This is sometimes called the 'rot' operator. @@ -550,7 +550,7 @@ def __init__(self, H1, Hdiv): super().__init__(fem_domain = H1, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== -class Curl_3D(FemLinearOperator): +class Curl3D(FemLinearOperator): """ Curl operator in 3D. @@ -600,7 +600,7 @@ def __init__(self, Hcurl, Hdiv): super().__init__(fem_domain = Hcurl, fem_codomain = Hdiv, linop = matrix) #==================================================================================================== -class Divergence_2D(FemLinearOperator): +class Divergence2D(FemLinearOperator): """ Divergence operator in 2D. @@ -639,7 +639,7 @@ def __init__(self, Hdiv, L2): super().__init__(fem_domain = Hdiv, fem_codomain = L2, linop = matrix) #==================================================================================================== -class Divergence_3D(FemLinearOperator): +class Divergence3D(FemLinearOperator): """ Divergence operator in 3D. @@ -683,65 +683,65 @@ def __init__(self, Hdiv, L2): #==================================================================================================== # 2D Multipatch derivative operators #==================================================================================================== -class BrokenGradient_2D(FemLinearOperator): +class BrokenGradient2D(FemLinearOperator): def __init__(self, V0h, V1h): FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) - D0s = [Gradient_2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] + D0s = [Gradient2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ (i, i): D0i.linop 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) + return BrokenTransposedGradient2D(self.fem_domain, self.fem_codomain) # ============================================================================== -class BrokenTransposedGradient_2D(FemLinearOperator): +class BrokenTransposedGradient2D(FemLinearOperator): 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)] + D0s = [Gradient2D(V0, V1) for V0, V1 in zip(V0h.spaces, V1h.spaces)] self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ (i, i): D0i.linop.T for i, D0i in enumerate(D0s)}) def transpose(self, conjugate=False): # todo (MCP): discard - return BrokenGradient_2D(self.fem_codomain, self.fem_domain) + return BrokenGradient2D(self.fem_codomain, self.fem_domain) # ============================================================================== -class BrokenScalarCurl_2D(FemLinearOperator): - +class BrokenScalarCurl2D(FemLinearOperator): + def __init__(self, V1h, V2h): FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) - D1s = [ScalarCurl_2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] + D1s = [ScalarCurl2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ (i, i): D1i.linop for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): - return BrokenTransposedScalarCurl_2D( + return BrokenTransposedScalarCurl2D( V1h=self.fem_domain, V2h=self.fem_codomain) # ============================================================================== -class BrokenTransposedScalarCurl_2D(FemLinearOperator): +class BrokenTransposedScalarCurl2D(FemLinearOperator): 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)] + D1s = [ScalarCurl2D(V1, V2) for V1, V2 in zip(V1h.spaces, V2h.spaces)] self._linop = BlockLinearOperator(self.linop_domain, self.linop_codomain, blocks={ (i, i): D1i.linop.T for i, D1i in enumerate(D1s)}) def transpose(self, conjugate=False): - return BrokenScalarCurl_2D(V1h=self.fem_codomain, V2h=self.fem_domain) + return BrokenScalarCurl2D(V1h=self.fem_codomain, V2h=self.fem_domain) diff --git a/psydac/feec/tests/test_commuting_projections.py b/psydac/feec/tests/test_commuting_projections.py index 8f5ae8c66..a3db1de91 100644 --- a/psydac/feec/tests/test_commuting_projections.py +++ b/psydac/feec/tests/test_commuting_projections.py @@ -7,9 +7,9 @@ from psydac.fem.tensor import TensorFemSpace, SplineSpace from psydac.fem.vector import VectorFemSpace from psydac.core.bsplines import make_knots -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D from psydac.ddm.cart import DomainDecomposition from psydac.linalg.solvers import inverse from psydac.linalg.basic import IdentityOperator @@ -60,7 +60,7 @@ def test_3d_commuting_pro_1(Nel, Nq, p, bc, m): P0 = ProjectorH1(H1) # Build linear operators on stencil arrays - grad = Gradient_3D(H1, Hcurl) + grad = Gradient3D(H1, Hcurl) # create an instance of the projector class P1 = ProjectorHcurl(Hcurl, Nq) @@ -150,7 +150,7 @@ def test_3d_commuting_pro_2(Nel, Nq, p, bc, m): Hdiv = VectorFemSpace(*spaces) # Build linear operators on stencil arrays - curl = Curl_3D(Hcurl, Hdiv) + curl = Curl3D(Hcurl, Hdiv) # create an instance of the projector class P1 = ProjectorHcurl(Hcurl, Nq) @@ -232,7 +232,7 @@ def test_3d_commuting_pro_3(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - div = Divergence_3D(Hdiv, L2) + div = Divergence3D(Hdiv, L2) # create an instance of the projector class P2 = ProjectorHdiv(Hdiv, Nq) @@ -310,7 +310,7 @@ def test_2d_commuting_pro_1(Nel, Nq, p, bc, m): P0 = ProjectorH1(H1) # Build linear operators on stencil arrays - grad = Gradient_2D(H1, Hcurl) + grad = Gradient2D(H1, Hcurl) # create an instance of the projector class P1 = ProjectorHcurl(Hcurl, Nq) @@ -383,7 +383,7 @@ def test_2d_commuting_pro_2(Nel, Nq, p, bc, m): P0 = ProjectorH1(H1) # Linear operator: 2D vector curl - curl = VectorCurl_2D(H1, Hdiv) + curl = VectorCurl2D(H1, Hdiv) # create an instance of the projector class P1 = ProjectorHdiv(Hdiv, Nq) @@ -461,7 +461,7 @@ def test_2d_commuting_pro_3(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - div = Divergence_2D(Hdiv, L2) + div = Divergence2D(Hdiv, L2) # create an instance of the projector class P2 = ProjectorHdiv(Hdiv, Nq) @@ -541,7 +541,7 @@ def test_2d_commuting_pro_4(Nel, Nq, p, bc, m): # create an instance of the H1 projector class # Build linear operators on stencil arrays - curl = ScalarCurl_2D(Hcurl, L2) + curl = ScalarCurl2D(Hcurl, L2) # create an instance of the projector class P1 = ProjectorHcurl(Hcurl, Nq) @@ -613,7 +613,7 @@ def test_1d_commuting_pro_1(Nel, Nq, p, bc, m): P0 = ProjectorH1(H1) # Build linear operators on stencil arrays - grad = Derivative_1D(H1, L2) + grad = Derivative1D(H1, L2) # create an instance of the projector class P1 = ProjectorL2(L2, Nq) diff --git a/psydac/feec/tests/test_commuting_projections_dual.py b/psydac/feec/tests/test_commuting_projections_dual.py index 5bc1a68a7..9057ba1ff 100644 --- a/psydac/feec/tests/test_commuting_projections_dual.py +++ b/psydac/feec/tests/test_commuting_projections_dual.py @@ -1,6 +1,6 @@ -from psydac.feec.derivatives import Gradient_3D -from psydac.feec.derivatives import Curl_3D -from psydac.feec.derivatives import Divergence_3D +from psydac.feec.derivatives import Gradient3D +from psydac.feec.derivatives import Curl3D +from psydac.feec.derivatives import Divergence3D from sympde.expr import LinearForm, integral from sympde.topology import Derham, element_of, Cube from psydac.api.discretization import discretize @@ -39,7 +39,7 @@ def test_transpose_div_3d(Nel, Nq, p, bc, m): v2 = element_of(derham.V2, name='v2') v3 = element_of(derham.V3, name='v3') - div = Divergence_3D(derham_h.V2, derham_h.V3) + div = Divergence3D(derham_h.V2, derham_h.V3) f2 = LinearForm(v2, integral(domain, D1fun1(*domain.coordinates)*v2[0] + D2fun1(*domain.coordinates)*v2[1] + D3fun1(*domain.coordinates)*v2[2])) f3 = LinearForm(v3, integral(domain, fun1(*domain.coordinates) * v3)) @@ -97,7 +97,7 @@ def test_transpose_curl_3d(Nel, Nq, p, bc, m): v1 = element_of(derham.V1, name='v1') v2 = element_of(derham.V2, name='v2') - curl = Curl_3D(derham_h.V1, derham_h.V2) + curl = Curl3D(derham_h.V1, derham_h.V2) f1 = LinearForm(v1, integral(domain, cf1(*domain.coordinates)*v1[0] + cf2(*domain.coordinates)*v1[1] + cf3(*domain.coordinates)*v1[2])) f2 = LinearForm(v2, integral(domain, fun1(*domain.coordinates)*v2[0] + fun2(*domain.coordinates)*v2[1] + fun3(*domain.coordinates)*v2[2])) @@ -144,7 +144,7 @@ def test_transpose_grad_3d(Nel, Nq, p, bc, m): v0 = element_of(derham.V0, name='v0') v1 = element_of(derham.V1, name='v1') - grad = Gradient_3D(derham_h.V0, derham_h.V1) + grad = Gradient3D(derham_h.V0, derham_h.V1) f0 = LinearForm(v0, integral(domain, (D1fun1(*domain.coordinates) + D2fun2(*domain.coordinates) + D3fun3(*domain.coordinates))*v0)) f1 = LinearForm(v1, integral(domain, fun1(*domain.coordinates)*v1[0] + fun2(*domain.coordinates)*v1[1] + fun3(*domain.coordinates)*v1[2])) diff --git a/psydac/feec/tests/test_differentiation_matrices.py b/psydac/feec/tests/test_differentiation_matrices.py index 8a1eb9e88..8a4b66df5 100644 --- a/psydac/feec/tests/test_differentiation_matrices.py +++ b/psydac/feec/tests/test_differentiation_matrices.py @@ -8,9 +8,9 @@ from psydac.fem.vector import VectorFemSpace from psydac.feec.derivatives import DirectionalDerivativeOperator -from psydac.feec.derivatives import Derivative_1D, Gradient_2D, Gradient_3D -from psydac.feec.derivatives import ScalarCurl_2D, VectorCurl_2D, Curl_3D -from psydac.feec.derivatives import Divergence_2D, Divergence_3D +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D from psydac.feec.global_projectors import ProjectorH1 @@ -352,7 +352,7 @@ def test_directional_derivative_operator_3d_par(domain, ncells, degree, periodic @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [1,2]) -def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Derivative1D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -372,7 +372,7 @@ def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): u0 = FemField(V0) # Linear operator: 1D derivative - grad = Derivative_1D(V0, V1) + grad = Derivative1D(V0, V1) # Create random field in V0 s, = V0.coeff_space.starts @@ -401,7 +401,7 @@ def test_Derivative_1D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Gradient2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -421,7 +421,7 @@ def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(DxNy, NxDy) # Linear operator: 2D gradient - grad = Gradient_2D(V0, V1) + grad = Gradient2D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -463,7 +463,7 @@ def test_Gradient_2D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Gradient3D(domain, ncells, degree, periodic, seed, multiplicity): if any([ncells[d] <= degree[d] and periodic[d] for d in range(3)]): return @@ -489,7 +489,7 @@ def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(DxNyNz, NxDyNz, NxNyDz) # Linear operator: 3D gradient - grad = Gradient_3D(V0, V1) + grad = Gradient3D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -530,7 +530,7 @@ def test_Gradient_3D(domain, ncells, degree, periodic, seed, multiplicity): @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_ScalarCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_ScalarCurl2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -555,7 +555,7 @@ def test_ScalarCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): V2 = DxDy # Linear operator: curl - curl = ScalarCurl_2D(V1, V2) + curl = ScalarCurl2D(V1, V2) # ... # Create random field in V1 @@ -605,7 +605,7 @@ def eval_curl(fx, fy, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_VectorCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_VectorCurl2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -625,7 +625,7 @@ def test_VectorCurl_2D(domain, ncells, degree, periodic, seed, multiplicity): V1 = VectorFemSpace(NxDy, DxNy) # Linear operator: 2D vector curl - curl = VectorCurl_2D(V0, V1) + curl = VectorCurl2D(V0, V1) # Create random field in V0 u0 = FemField(V0) @@ -671,7 +671,7 @@ def eval_curl(f, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Curl_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Curl3D(domain, ncells, degree, periodic, seed, multiplicity): if any([ncells[d] <= degree[d] and periodic[d] for d in range(3)]): return @@ -703,7 +703,7 @@ def test_Curl_3D(domain, ncells, degree, periodic, seed, multiplicity): V2 = VectorFemSpace(NxDyDz, DxNyDz, DxDyNz) # Linear operator: curl - curl = Curl_3D(V1, V2) + curl = Curl3D(V1, V2) # ... # Create random field in V1 @@ -763,7 +763,7 @@ def eval_curl(fx, fy, fz, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) -def test_Divergence_2D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Divergence2D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -787,7 +787,7 @@ def test_Divergence_2D(domain, ncells, degree, periodic, seed, multiplicity): V2 = V0.reduce_degree(axes=[0, 1], basis='M') # Linear operator: divergence - div = Divergence_2D(V1, V2) + div = Divergence2D(V1, V2) # ... # Create random field in V1 @@ -839,7 +839,7 @@ def eval_div(fx, fy, *eta): @pytest.mark.parametrize('seed', [1,3]) @pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) -def test_Divergence_3D(domain, ncells, degree, periodic, seed, multiplicity): +def test_Divergence3D(domain, ncells, degree, periodic, seed, multiplicity): # determinize tests np.random.seed(seed) @@ -865,7 +865,7 @@ def test_Divergence_3D(domain, ncells, degree, periodic, seed, multiplicity): V3 = V0.reduce_degree(axes=[0, 1, 2], basis='M') # Linear operator: divergence - div = Divergence_3D(V2, V3) + div = Divergence3D(V2, V3) # ... # Create random field in V2 @@ -915,11 +915,11 @@ def eval_div(fx, fy, fz, *eta): #============================================================================== if __name__ == '__main__': - test_Derivative_1D(domain=[0, 1], ncells=3, degree=3, periodic=False, seed=1, multiplicity=1) - test_Derivative_1D(domain=[0, 1], ncells=12, degree=3, periodic=True, seed=1, multiplicity=1) + test_Derivative1D(domain=[0, 1], ncells=3, degree=3, periodic=False, seed=1, multiplicity=1) + test_Derivative1D(domain=[0, 1], ncells=12, degree=3, periodic=True, seed=1, multiplicity=1) - test_Gradient_2D( + test_Gradient2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -927,7 +927,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Gradient_3D( + test_Gradient3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), @@ -935,7 +935,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_ScalarCurl_2D( + test_ScalarCurl2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -943,7 +943,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_VectorCurl_2D( + test_VectorCurl2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -951,7 +951,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Curl_3D( + test_Curl3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), @@ -959,7 +959,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Divergence_2D( + test_Divergence2D( domain = ([0, 1], [0, 1]), ncells = (10, 15), degree = (3, 2), @@ -967,7 +967,7 @@ def eval_div(fx, fy, fz, *eta): seed = 1 ) - test_Divergence_3D( + test_Divergence3D( domain = ([0, 1], [0, 1], [0, 1]), ncells = (5, 8, 4), degree = (3, 2, 3), From 72c711bfc30149869ebf6ad55adcf77082d87444 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 18 Aug 2025 21:26:39 +0200 Subject: [PATCH 065/102] convert single patch hodge inverse to sparse --- psydac/feec/hodge.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 037a9e3e4..d80afe0cd 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -165,8 +165,10 @@ def assemble_dual_sparse_matrix(self): self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) else: + from scipy.sparse import csr_matrix M_m = M.toarray() inv_M = inv(M_m) + inv_M = csr_matrix(inv_M) self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) From e29dbdacdb2f6cd39d6455aec81d587fa2c305fe Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 3 Sep 2025 14:25:09 +0200 Subject: [PATCH 066/102] update td maxwell example --- .../multipatch/examples/timedomain_maxwell.py | 892 ++++-------------- .../examples/timedomain_maxwell_testcase.py | 153 +-- .../tests/test_feec_maxwell_multipatch_2d.py | 3 +- 3 files changed, 178 insertions(+), 870 deletions(-) diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index 9e772f3e5..ee0ef87f5 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -30,25 +30,23 @@ from sympde.expr.expr import LinearForm from sympde.expr.expr import integral, Norm from sympde.topology import Derham +from psydac.linalg.basic import IdentityOperator 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.api.discretization import discretize + from psydac.fem.plotting_utilities import plot_field_2d as 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.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_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 +from psydac.fem.projectors import get_dual_dofs def solve_td_maxwell_pbm(*, @@ -61,26 +59,13 @@ def solve_td_maxwell_pbm(*, backend='pyccel-gcc', source_type='zero', source_omega=None, - source_proj='P_geom', - conf_proj='BSP', - gamma_h=10., + source_proj='P_L2', project_sol=False, filter_source=True, - quad_param=1, - E0_type='zero', + E0_type='pulse_2', 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 ): """ @@ -145,16 +130,6 @@ def solve_td_maxwell_pbm(*, 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. @@ -163,23 +138,14 @@ def solve_td_maxwell_pbm(*, 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`), + E0_type : str {'zero', 'pulse'} + Initial conditions for the electric field. Choose 'zero' for E0=0 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. @@ -187,33 +153,12 @@ def solve_td_maxwell_pbm(*, 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'`). + domain_lims : list + If the domain_name is 'refined_square' or 'square_L_shape', this + parameter must be set to the list of the two intervals defining the + rectangular domain, i.e. `[[x_min, x_max], [y_min, y_max]]`. """ - diags = {} - - # ncells = [nc, nc] degree = [deg, deg] if source_omega is not None: @@ -225,18 +170,6 @@ def solve_td_maxwell_pbm(*, [[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: ') @@ -248,10 +181,8 @@ def solve_td_maxwell_pbm(*, 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 --') @@ -267,10 +198,10 @@ def solve_td_maxwell_pbm(*, if isinstance(nc, int): ncells = [nc, nc] - elif ncells.ndim == 1: + elif nc.ndim == 1: ncells = {patch.name: [nc[i], nc[i]] for (i, patch) in enumerate(domain.interior)} - elif ncells.ndim == 2: + elif nc.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} @@ -278,8 +209,6 @@ def solve_td_maxwell_pbm(*, 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...') @@ -301,101 +230,37 @@ def solve_td_maxwell_pbm(*, 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 + V0h, V1h, V2h = derham_h.spaces t_stamp = time_count(t_stamp) print(' .. Id operator and matrix...') - I1 = IdLinearOperator(V1h) - I1_m = I1.to_sparse_matrix() + I1 = IdentityOperator(V1h.coeff_space) 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() + H0, H1, H2 = derham_h.hodge_operators(kind='linop') + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True) - 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) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', p_moments = degree[0]+2, hom_bc = False) 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() + bD0, bD1 = derham_h.derivatives(kind='linop') + 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 + C = bD1 @ cP1 print(' .. matrix of the dual curl (also in primal bases)...') + dC = dH1 @ C.T @ H2 + + ### Silvermueller ABC from sympde.calculus import grad, dot, curl, cross from sympde.topology import NormalVector from sympde.expr.expr import BilinearForm @@ -408,37 +273,38 @@ def solve_td_maxwell_pbm(*, a = BilinearForm((u, v), integral(boundary, expr_b)) ah = discretize(a, domain_h, [V1h, V1h], backend=PSYDAC_BACKENDS[backend],) - A_eps = ah.assemble().tosparse() + A_eps = ah.assemble() + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 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) + dt = compute_stable_dt(C_m=C.tosparse(), dC_m=dC.tosparse(), cfl_max=cfl_max, dt_max=dt_max) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Absorbing dC_m - CH2 = C_m.transpose() @ H2_m - H1A = H1_m + dt * A_eps - - H1A_csc = H1A.tocsc() - dC_m = sp.sparse.linalg.spsolve(H1A_csc, CH2.tocsc()) - dCH1_m = sp.sparse.linalg.spsolve(H1A_csc, H1_m.tocsc()) + CH2 = C.T @ H2 + H1A = H1 + dt * A_eps + + # alternative inverse + # from psydac.linalg.solvers import inverse + # H1A_inv = inverse(H1A, solver='cg', tol=1e-8) + ### + M = H1A + from scipy.linalg import inv + from scipy.sparse import csr_matrix + from psydac.linalg.utilities import SparseMatrixLinearOperator + M_inv = inv(M.toarray()) + M_inv = csr_matrix(M_inv) + H1A_inv = SparseMatrixLinearOperator(M.codomain, M.domain, M_inv) + #### + + dC = H1A_inv @ CH2 + dCH1 = H1A_inv @ H1 print(' .. matrix of the dual div (still in primal bases)...') - div_m = dH0_m @ cP0_m.transpose() @ bD0_m.transpose() @ H1_m + D = dH0 @ cP0.T @ bD0.T @ H1 - # 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)) @@ -447,8 +313,7 @@ def solve_td_maxwell_pbm(*, print(f" . Nb of time steps: Nt = {Nt}") # ... - def is_plotting_time(nt, *, dt=dt, Nt=Nt, - plot_time_ranges=plot_time_ranges): + 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: @@ -459,21 +324,11 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, 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(' 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(' ------ ------ ------ ------ ------ ------ ------ ------ ') @@ -483,8 +338,9 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, t_stamp = time_count(t_stamp) print() print(' -- getting source --') - f0_c = None - f0_harmonic_c = None + f0_h = None + f0_harmonic_h = None + if source_type == 'zero': f0 = None @@ -492,11 +348,11 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, elif source_type == 'pulse': - f0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + f0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) elif source_type == 'cf_pulse': - f0 = get_curl_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + f0 = get_curl_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) elif source_type == 'Il_pulse': # Issautier-like pulse # source will be @@ -507,91 +363,60 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, # 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) + f0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is curl A + f0_harmonic = get_curl_free_pulse( x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is grad phi + + rho0 = get_Delta_phi_pulse(x_0=np.pi/2, y_0=np.pi/2, domain=domain) # this is Delta phi + tilde_rho0_h = get_dual_dofs(Vh=V0h, f=rho0, domain_h=domain_h, backend_language=backend) + tilde_rho0_h = cP0.T @ tilde_rho0_h + rho0_h = dH0.dot(tilde_rho0_h) 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, - ) + 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 + + 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 + tilde_f0_h = f0_h = None + tilde_f0_harmonic_h = f0_harmonic_h = 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) + f0_h = P1_phys(f0, P1, domain).coeffs + tilde_f0_h = H1.dot(f0_h) + 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) + f0_harmonic_h = P1_phys(f0_harmonic, P1, domain).coeffs + tilde_f0_harmonic_h = H1.dot(f0_harmonic_h) 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) + + print(' .. projecting the source f0 with L2 projection...') + tilde_f0_h = get_dual_dofs(Vh=V1h, f=f0, domain_h=domain_h, backend_language=backend) + 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) + + print(' .. projecting the source f0_harmonic with L2 projection...') + tilde_f0_harmonic_h = get_dual_dofs(Vh=V1h, f=f0_harmonic, domain_h=domain_h, backend_language=backend) else: raise ValueError(source_proj) @@ -599,259 +424,23 @@ def source_enveloppe(tau): 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) + if tilde_f0_h is not None: + tilde_f0_h = cP1.T @ tilde_f0_h - t_stamp = time_count(t_stamp) + if tilde_f0_harmonic_h is not None: + tilde_f0_harmonic_h = cP1.T @ tilde_f0_harmonic_h - 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], - ] + if tilde_f0_h is not None: + f0_h = dH1.dot(tilde_f0_h) + + if tilde_f0_harmonic_h is not None: + f0_harmonic_h = dH1.dot(tilde_f0_harmonic_h) - 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) + + if f0_h is None: + f0_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) + + t_stamp = time_count(t_stamp) # diags arrays E_norm2_diag = np.zeros(Nt + 1) @@ -874,114 +463,72 @@ def plot_time_diags(time_diag, E_norm2_diag, B_norm2_diag, divE_norm2_diag, nt_s print(' .. initial solution ..') # initial B sol - B_c = np.zeros(V2h.nbasis) + B_h = array_to_psydac(np.zeros(V2h.nbasis), V2h.coeff_space) # 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) + if E0_type == 'zero': + E_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) elif E0_type == 'pulse': - E0 = get_div_free_pulse(x_0=1.0, y_0=1.0, domain=domain) + E0 = get_div_free_pulse(x_0=np.pi/2, y_0=np.pi/2, 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() + E0_h = P1_phys(E0, P1, domain) + E_h = E0_h.coeffs 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) + print(' .. projecting E0 with L2 projection...') + tilde_E0_h = get_dual_dofs(Vh=V1h, f=E0, domain_h=domain_h, backend_language=backend) + E_h = dH1_m.dot(tilde_E0_h) - # 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) + elif E0_type == 'pulse_2': - 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) + E0, B0 = get_Gaussian_beam(y_0=np.pi/2, x_0=np.pi/2, 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() + E0_h = P1_phys(E0, P1, domain) + E_h = E0_h.coeffs - # 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() + B0_h = P2_phys(B0, P2, domain) + B_h = B0_h.coeffs 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...') + + print(' .. projecting E0 with L2 projection...') + tilde_E0_h = get_dual_dofs(Vh=V1h, f=E0, domain_h=domain_h, backend_language=backend) + E_h = dH1.dot(tilde_E0_h) - 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) + tilde_B0_h = get_dual_dofs(Vh=V2h, f=B0, domain_h=domain_h, backend_language=backend) + B_h = dH2.dot(tilde_B0_h) - 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): + def compute_diags(E_h, B_h, J_h, 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)) + PE_h = cP1.dot(E_h) + I_PE_h = E_h - PE_h + E_norm2_diag[nt] = E_h.inner(H1.dot(E_h)) + PE_norm2_diag[nt] = PE_h.inner(H1.dot(PE_h)) + I_PE_norm2_diag[nt] = I_PE_h.inner(H1.dot(I_PE_h)) + J_norm2_diag[nt] = J_h.inner(H1.dot(J_h)) + B_norm2_diag[nt] = B_h.inner(H2.dot(B_h)) + divE_h = D @ E_h + divE_norm2_diag[nt] = divE_h.inner(H0.dot(divE_h)) + if source_type == 'Il_pulse' and source_omega is not None: + rho_h = rho0_h * np.sin(source_omega * nt * dt) / omega + GaussErr = rho_h - divE_h + GaussErrP = rho_h - div_m @ PE_h + GaussErr_norm2_diag[nt] = GaussErr.inner(H0.dot(GaussErr)) + GaussErrP_norm2_diag[nt] = GaussErrP.inner(H0.dot(GaussErrP)) if plot_dir: OM1 = OutputManager(plot_dir + '/spaces1.yml', plot_dir + '/fields1.h5') @@ -992,200 +539,81 @@ def compute_diags(E_c, B_c, J_c, nt): OM2.add_spaces(V2h=V2h) OM2.export_space_info() - stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) - Eh = FemField(V1h, coeffs=stencil_coeffs_E) + #stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) + Eh = FemField(V1h, coeffs=cP1 @ E_h) OM1.add_snapshot(t=0, ts=0) OM1.export_fields(Eh=Eh) - stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) - Bh = FemField(V2h, coeffs=stencil_coeffs_B) + # stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) + Bh = FemField(V2h, coeffs=B_h) 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) + f_h = f0_h.copy() 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 + B_h -= (dt / 2) * C @ E_h # 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 f0_harmonic_h is not None and source_omega is not None: + f_harmonic_h = f0_harmonic_h * (np.sin(source_omega * (nt + 1) * dt) - np.sin(source_omega * (nt) * dt)) / (dt * source_omega) # * source_enveloppe(omega*(nt+1/2)*dt) + f_h = f0_h + f_harmonic_h - # if abs(gamma_h) > 1e-10: - # E_c[:] -= dt * gamma_h * JP_m @ E_c + E_h = dCH1 @ E_h + dt * (dC @ B_h - f_h) # 1/2 faraday: Bn+1/2 -> Bn+1 - B_c[:] -= (dt / 2) * C_m @ E_c + B_h -= (dt / 2) * C @ E_h # 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))) + compute_diags(E_h, B_h, f_h, nt=nt + 1) + + 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) + print("Plot fields") - stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) - Eh = FemField(V1h, coeffs=stencil_coeffs_E) + Eh = FemField(V1h, coeffs=cP1 @ E_h) OM1.add_snapshot(t=nt * dt, ts=nt) OM1.export_fields(Eh=Eh) - stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) - Bh = FemField(V2h, coeffs=stencil_coeffs_B) + Bh = FemField(V2h, coeffs=B_h) 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") + print("Post process fields") PM = PostProcessManager( domain=domain, - space_file=plot_dir + - '/spaces1.yml', - fields_file=plot_dir + - '/fields1.h5') + 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, + npts_per_cell=4, snapshots='all', fields='Eh') PM.close() PM = PostProcessManager( domain=domain, - space_file=plot_dir + - '/spaces2.yml', - fields_file=plot_dir + - '/fields2.h5') + 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, + npts_per_cell=4, 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.coeff_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.coeff_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.coeff_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 diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py index 19c1e13d6..759dd1f52 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py @@ -5,10 +5,7 @@ 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() +from psydac.feec.multipatch.utilities import FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # @@ -16,11 +13,9 @@ 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' # # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- @@ -34,62 +29,21 @@ # domain_name = 'pretzel_f' # non-conf domains -domain = [[0, 2 * np.pi], [0, 2 * np.pi]] # interval in x- and y-direction +domain = [[0, np.pi], [0, 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]]) - +ncells = np.array([[10, 10, 10], + [10, 20, 10], + [10, 10, 10]]) cfl_max = 0.8 + # 'P_geom' # projection used for initial E0 (B0 = 0 in all cases) -E0_proj = 'P_geom' +E0_proj = 'P_L2' 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': @@ -97,9 +51,8 @@ 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 + final_time = 2 # wave transit time in domain is > 4 dt_max = None - plot_source = False plot_a_lot = True if plot_a_lot: @@ -110,9 +63,6 @@ [[final_time - 1, final_time], 0.1], ] - cb_min_sol = 0 - cb_max_sol = 5 - # TODO: check elif test_case == 'Issautier_like_source': @@ -120,9 +70,9 @@ source_type = 'Il_pulse' source_omega = None final_time = 20 - plot_source = True dt_max = None - if deg_s == [3] and final_time == 20: + + if deg == 3 and final_time == 20: plot_time_ranges = [ [[1.9, 2], 0.1], @@ -131,36 +81,6 @@ [[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) @@ -181,8 +101,8 @@ else: raise ValueError(J_proj_case) -case_dir = 'nov14_' + test_case + '_J_proj=' + \ - J_proj_case + '_qp{}'.format(quad_param) +case_dir = 'tdmaxwell_' + test_case + '_J_proj=' + J_proj_case + if filter_source: case_dir += '_Jfilter' else: @@ -198,32 +118,17 @@ # # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -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) + 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, @@ -235,23 +140,12 @@ '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 } @@ -259,17 +153,4 @@ 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') +solve_td_maxwell_pbm(**params) 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 a64c3a190..007221724 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -6,7 +6,7 @@ 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 +from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm def test_time_harmonic_maxwell_pretzel_f(): nc = 4 @@ -183,7 +183,6 @@ def test_maxwell_eigen_curved_L_shape_dg(): assert abs(error - 0.035139029534570064) < 1e-10 -@pytest.mark.skip(reason="need to adapt notation") def test_maxwell_timedomain(): solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') From 43cb462945917df1e91a31bd239204a5b62f7e5b Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 3 Sep 2025 14:32:20 +0200 Subject: [PATCH 067/102] make codacy happy --- psydac/feec/multipatch/examples/timedomain_maxwell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index ee0ef87f5..1966d1be8 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -340,7 +340,8 @@ def is_plotting_time(nt, *, dt=dt, Nt=Nt, plot_time_ranges=plot_time_ranges): print(' -- getting source --') f0_h = None f0_harmonic_h = None - + rho0_h = None + if source_type == 'zero': f0 = None @@ -464,6 +465,7 @@ def source_enveloppe(tau): # initial B sol B_h = array_to_psydac(np.zeros(V2h.nbasis), V2h.coeff_space) + E_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) # initial E sol if E0_type == 'zero': From 93002fd508398a784ca7559d78230eb8dcfd2d7a Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Wed, 3 Sep 2025 14:40:01 +0200 Subject: [PATCH 068/102] remove block_tostencil --- psydac/feec/derivatives.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index e4ae65d3d..07706d601 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -27,23 +27,8 @@ 'BrokenTransposedGradient2D', 'BrokenScalarCurl2D', 'BrokenTransposedScalarCurl2D', - 'block_tostencil' ) -#==================================================================================================== -def block_tostencil(M): - """ - Convert a BlockLinearOperator that contains KroneckerStencilMatrix objects - to a BlockLinearOperator that contains StencilMatrix objects - """ - blocks = [list(b) for b in M.blocks] - for i1,b in enumerate(blocks): - for i2, mat in enumerate(b): - if mat is None: - continue - blocks[i1][i2] = mat.tostencil() - return BlockLinearOperator(M.domain, M.codomain, blocks=blocks) - #==================================================================================================== # Singlepatch derivative operators #==================================================================================================== From 3d136ff92769bf4ee6ec15dc05598949407063bc Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 8 Sep 2025 10:43:55 +0200 Subject: [PATCH 069/102] remove sparse_matrix as property of FemLinearOperator and Hodge --- psydac/api/feec.py | 61 +++++++++++++++++------------------- psydac/feec/hodge.py | 74 +++++--------------------------------------- psydac/fem/basic.py | 28 ++++------------- 3 files changed, 41 insertions(+), 122 deletions(-) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 318137815..6fb25791d 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -310,6 +310,7 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h The conforming projectors of each space and in desired form. """ + if hom_bc is None: raise ValueError('please provide a value for "hom_bc" argument') @@ -321,24 +322,27 @@ def conforming_projectors(self, kind='femlinop', mom_pres=False, p_moments=-1, h raise NotImplementedError('2D sequence with H-div not available yet') else: - cP0 = ConformingProjectionV0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) - cP1 = ConformingProjectionV1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) - I2 = IdentityOperator(self.V2.coeff_space) - cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2, sparse_matrix=I2.tosparse()) + if not self._conf_proj: + + cP0 = ConformingProjectionV0(self.V0, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + cP1 = ConformingProjectionV1(self.V1, mom_pres=mom_pres, p_moments=p_moments, hom_bc=hom_bc) + + I2 = IdentityOperator(self.V2.coeff_space) + cP2 = FemLinearOperator(fem_domain=self.V2, fem_codomain=self.V2, linop=I2) - self._conf_proj = (cP0, cP1, cP2) + self._conf_proj = (cP0, cP1, cP2) + + if kind == 'femlinop': + return self._conf_proj[0], self._conf_proj[1], self._conf_proj[2] + elif kind == 'linop': + return self._conf_proj[0].linop, self._conf_proj[1].linop, self._conf_proj[2].linop elif self.dim == 3: raise NotImplementedError("3D projectors are not available") - if kind == 'femlinop': - return cP0, cP1, cP2 - elif kind == 'linop': - return cP0.linop, cP1.linop, cP2.linop - #-------------------------------------------------------------------------- - def _init_hodge_operators(self, backend_language='python', load_dir=None): + def _init_hodge_operators(self, backend_language='python'): """ Initialize the Hodge operator for the multipatch de Rham sequence. @@ -348,32 +352,29 @@ def _init_hodge_operators(self, backend_language='python', load_dir=None): backend_language: The backend used to accelerate the code - load_dir: - Filename for storage in sparse matrix format - """ if not self._hodge_operators: if self.dim == 1: - H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) - H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) self._hodge_operators = (H0, H1) elif self.dim == 2: - H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) - H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) - H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language) self._hodge_operators = (H0, H1, H2) elif self.dim == 3: - H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=0) - H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=1) - H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=2) - H3 = HodgeOperator(self.V3, self.domain_h, backend_language=backend_language, load_dir=load_dir, load_space_index=3) + H0 = HodgeOperator(self.V0, self.domain_h, backend_language=backend_language) + H1 = HodgeOperator(self.V1, self.domain_h, backend_language=backend_language) + H2 = HodgeOperator(self.V2, self.domain_h, backend_language=backend_language) + H3 = HodgeOperator(self.V3, self.domain_h, backend_language=backend_language) self._hodge_operators = (H0, H1, H2, H3) @@ -411,7 +412,7 @@ def _get_hodge_operator(self, H, dual=False, kind='femlinop'): return H.dual_linop #-------------------------------------------------------------------------- - def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_language='python', load_dir=None): + def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_language='python'): """ Returns the Hodge operator for the given space and specified kind. @@ -432,9 +433,6 @@ def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_langua backend_language : str The backend used to accelerate the code, default is 'python'. - load_dir : str or None - Directory to load the Hodge operator from, if None the operator is computed on demand. - Returns ------- The Hodge operator of the space of the specified kind. @@ -443,7 +441,7 @@ def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_langua """ if not self._hodge_operators: - self._init_hodge_operators(backend_language=backend_language, load_dir=load_dir) + self._init_hodge_operators(backend_language=backend_language) if space == 'V0': return self._get_hodge_operator(self._hodge_operators[0], dual=dual, kind=kind) @@ -458,7 +456,7 @@ def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_langua return self._get_hodge_operator(self._hodge_operators[3], dual=dual, kind=kind) #-------------------------------------------------------------------------- - def hodge_operators(self, dual=False, kind='femlinop', backend_language='python', load_dir=None): + def hodge_operators(self, dual=False, kind='femlinop', backend_language='python'): """ Returns the Hodge operators for the specified kind. @@ -475,16 +473,13 @@ def hodge_operators(self, dual=False, kind='femlinop', backend_language='python' backend_language : str The backend used to accelerate the code, default is 'python'. - load_dir : str or None - Directory to load the Hodge operator from, if None the operator is computed on demand. - Returns ------- The Hodge operators of all spaces and of the specified kind. """ if not self._hodge_operators: - self._init_hodge_operators(backend_language=backend_language, load_dir=load_dir) + self._init_hodge_operators(backend_language=backend_language) return tuple(self._get_hodge_operator(H, dual=dual, kind=kind) for H in self._hodge_operators) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index d80afe0cd..190ae9eb3 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -37,21 +37,13 @@ class HodgeOperator: backend_language: The backend used to accelerate the code - load_dir: - storage files for the primal and dual Hodge sparse matrice - - load_space_index: - the space index in the derham sequence - Notes ----- - 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, metric='identity', backend_language='python', load_dir=None, load_space_index=''): + def __init__(self, Vh, domain_h, metric='identity', backend_language='python'): self._fem_domain = Vh self._fem_codomain = Vh @@ -64,10 +56,6 @@ def __init__(self, Vh, domain_h, metric='identity', backend_language='python', l self._linop = None self._dual_linop = None - # Sparse matrices - self._sparse_matrix = None - self._dual_sparse_matrix = None - self._domain_h = domain_h self._backend_language = backend_language @@ -76,34 +64,6 @@ def __init__(self, Vh, domain_h, metric='identity', backend_language='python', l 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_is_stored = os.path.exists(primal_hodge_storage_fn) - dual_hodge_is_stored = os.path.exists(dual_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_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 primal_hodge_is_stored - print("[HodgeOperator] assembling both sparse matrices for storage...") - self.assemble_matrix() - print("[HodgeOperator] storing primal Hodge sparse matrix in " + primal_hodge_storage_fn) - save_npz(primal_hodge_storage_fn, self._sparse_matrix) - self.assemble_dual_sparse_matrix() - print("[HodgeOperator] storing dual Hodge sparse matrix in " + dual_hodge_storage_fn) - save_npz(dual_hodge_storage_fn, self._dual_sparse_matrix) - else: - # matrices are not stored, we will probably compute them later - pass - def assemble_matrix(self): """ the Hodge matrix is the patch-wise multi-patch mass matrix @@ -130,9 +90,8 @@ def assemble_matrix(self): ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) self._linop = ah.assemble() # Mass matrix in stencil format - self._sparse_matrix = self._linop.tosparse() - self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop, sparse_matrix=self._sparse_matrix) + self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop) # which of the two assemblys for the sparse dual matrix is better? For now use the exact one. def assemble_dual_sparse_matrix(self): @@ -142,7 +101,7 @@ def assemble_dual_sparse_matrix(self): """ from psydac.fem.basic import FemLinearOperator - if self._dual_sparse_matrix is None: + if self._dual_linop is None: if not self._linop: self.assemble_matrix() @@ -160,9 +119,8 @@ def assemble_dual_sparse_matrix(self): inv_M = block_diag(inv_M_blocks, format='csr') - self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) else: from scipy.sparse import csr_matrix @@ -170,9 +128,8 @@ def assemble_dual_sparse_matrix(self): inv_M = inv(M_m) inv_M = csr_matrix(inv_M) - self._dual_sparse_matrix = inv_M self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) def assemble_dual_matrix(self, solver ='gmres', **kwargs): @@ -202,13 +159,11 @@ def assemble_dual_matrix(self, solver ='gmres', **kwargs): inv_M_blocks[i][i] = inv_Mii self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) - self._dual_sparse_matrix = None - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) else: inv_M = inverse(M, solver=solver, **kwargs) - self._dual_sparse_matrix = None - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop, sparse_matrix=self._dual_sparse_matrix) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) @property def linop(self): @@ -216,13 +171,6 @@ def linop(self): self.assemble_matrix() return self._linop - - @property - def tosparse(self): - if self._sparse_matrix is None: - self.assemble_matrix() - - return self._sparse_matrix @property def dual_linop(self): @@ -230,13 +178,6 @@ def dual_linop(self): self.assemble_dual_sparse_matrix() return self._dual_linop - - @property - def dual_tosparse(self): - if self._dual_sparse_matrix is None: - self.assemble_dual_sparse_matrix() - - return self._dual_sparse_matrix @property def hodge(self): @@ -251,4 +192,3 @@ def dual_hodge(self): self.assemble_dual_sparse_matrix() return self._dual_hodge - \ No newline at end of file diff --git a/psydac/fem/basic.py b/psydac/fem/basic.py index bafa1e796..090243205 100644 --- a/psydac/fem/basic.py +++ b/psydac/fem/basic.py @@ -397,21 +397,16 @@ class FemLinearOperator: fem_codomain : psydac.fem.basic.FemSpace The discrete space of the codomain - linop : (optional) + linop : Linear Operator. - sparse_matrix : (optional) - Sparse matrix representation of the linear operator. """ - def __init__(self, fem_domain, fem_codomain, *, linop=None, sparse_matrix=None): + def __init__(self, fem_domain, fem_codomain, *, linop=None): assert isinstance(fem_domain, FemSpace) assert isinstance(fem_codomain, FemSpace) if linop is not None: assert isinstance(linop, LinearOperator) - if sparse_matrix is not None: - from scipy.sparse import spmatrix - assert isinstance(sparse_matrix, spmatrix) self._fem_domain = fem_domain self._fem_codomain = fem_codomain @@ -420,7 +415,6 @@ def __init__(self, fem_domain, fem_codomain, *, linop=None, sparse_matrix=None): self._linop_codomain = fem_codomain.coeff_space self._linop = linop - self._sparse_matrix = sparse_matrix @property def fem_domain(self): @@ -442,19 +436,11 @@ def linop_codomain(self): def linop(self): return self._linop - @property def toarray(self): - if self._sparse_matrix is not None: - return self._sparse_matrix.toarray() - else: return self._linop.toarray() - @property def tosparse(self): - if self._sparse_matrix is None: - self._sparse_matrix = self._linop.tosparse() - - return self._sparse_matrix + return self._linop.tosparse() #-------------------------------------------------------------------------- def __call__(self, u, *, out=None): @@ -463,10 +449,8 @@ def __call__(self, u, *, out=None): if self._linop is not None: coeffs = self._linop.dot(u.coeffs) - elif self._sparse_matrix is not None: - coeffs = self._sparse_matrix.dot(u.coeffs) else: - raise NotImplementedError('Class does not provide a __call__ method without a matrix or linear operator') + raise NotImplementedError('Class does not provide a __call__ method without a linear operator') return FemField(self.fem_codomain, coeffs=coeffs) @@ -474,8 +458,8 @@ def dot(self, f_coeffs, *, out=None): assert isinstance(f_coeffs, Vector) assert f_coeffs.space is self._linop_domain - if self._linop is not None or self._sparse_matrix is not None: + if self._linop is not None: f = FemField(self.fem_domain, coeffs=f_coeffs) return self(f).coeffs else: - raise NotImplementedError('Class does not provide a dot method without a matrix or linear operator') + raise NotImplementedError('Class does not provide a dot method without a linear operator') From aa8c1929a77299761b7a31340bec1d5213252073 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 8 Sep 2025 11:21:32 +0200 Subject: [PATCH 070/102] adapt test --- psydac/feec/tests/test_feec_conf_projectors_cart_2d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 0838e8dd3..80a11e1f6 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -147,16 +147,16 @@ def test_conf_projectors_2d( # conforming projections (scipy matrices) cP0, cP1, cP2 = derham_h.conforming_projectors(p_moments=mom_pres, hom_bc=hom_bc) - cP0, cP1, cP2 = (m.tosparse for m in (cP0, cP1, cP2)) + cP0, cP1, cP2 = (m.tosparse() for m in (cP0, cP1, cP2)) M0, M1, M2 = derham_h.hodge_operators() - M0, M1, M2 = (m.tosparse for m in (M0, M1, M2)) + M0, M1, M2 = (m.tosparse() for m in (M0, M1, M2)) M0_inv, M1_inv, M2_inv = derham_h.hodge_operators(dual=True) - M0_inv, M1_inv, M2_inv = (m.tosparse for m in (M0_inv, M1_inv, M2_inv)) + M0_inv, M1_inv, M2_inv = (m.tosparse() for m in (M0_inv, M1_inv, M2_inv)) bD0, bD1 = derham_h.derivatives() - bD0, bD1 = (m.tosparse for m in (bD0, bD1)) + bD0, bD1 = (m.tosparse() for m in (bD0, bD1)) D0 = bD0 @ cP0 # Conga grad D1 = bD1 @ cP1 # Conga curl or div From 4a5733f38f9ffe3f19b8e0446aa674327aba3ae5 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 8 Sep 2025 12:44:33 +0200 Subject: [PATCH 071/102] remove load_dir --- psydac/feec/conforming_projectors.py | 12 ++++++------ .../multipatch/examples/hcurl_eigen_pbms_conga_2d.py | 8 +++----- .../multipatch/examples/hcurl_eigen_testcases.py | 4 ---- .../examples/hcurl_source_pbms_conga_2d.py | 8 +------- .../multipatch/examples/hcurl_source_testcase.py | 4 ---- 5 files changed, 10 insertions(+), 26 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index fb2fb0904..f4faa4b2c 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1455,11 +1455,11 @@ def __init__( FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V0h) if V0h.is_multipatch: - self._sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) else: - self._sparse_matrix = construct_h1_singlepatch_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + sparse_matrix = construct_h1_singlepatch_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix) class ConformingProjectionV1(FemLinearOperator): @@ -1495,8 +1495,8 @@ def __init__( FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V1h) if V1h.is_multipatch: - self._sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) else: - self._sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, self._sparse_matrix) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix) 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 06dd5f8a9..1c77a7e8f 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -27,7 +27,7 @@ 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,): + plot_dir=None): """ Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization @@ -61,8 +61,6 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma 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 = {} @@ -132,8 +130,8 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma t_stamp = time_count(t_stamp) print('Hodge operators...') # multi-patch (broken) linear operators / matrices - H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language, load_dir=m_load_dir) - dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language, load_dir=m_load_dir) + H0, H1, H2 = derham_h.hodge_operators(kind='linop', backend_language=backend_language) + dH0, dH1, dH2 = derham_h.hodge_operators(kind='linop', dual=True, backend_language=backend_language) t_stamp = time_count(t_stamp) print('conforming projection operators...') diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py index 4f311a7eb..5e887beda 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py @@ -220,9 +220,6 @@ 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)) @@ -254,7 +251,6 @@ domain_name=domain_name, domain=domain, backend_language=backend_language, plot_dir=plot_dir, - m_load_dir=m_load_dir, ) elif method == 'dg': 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 46062eaca..6e132b75e 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -36,9 +36,7 @@ def solve_hcurl_source_pbm( nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_proj='tilde_Pi', source_type='manu_J', eta=-10., mu=1., nu=1., gamma_h=10., - project_sol=True, plot_dir=None, - m_load_dir=None, -): + project_sol=True, plot_dir=None): """ solver for the problem: find u in H(curl), such that @@ -71,15 +69,11 @@ def solve_hcurl_source_pbm( :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 = {} degree = [deg, deg] - 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: ') diff --git a/psydac/feec/multipatch/examples/hcurl_source_testcase.py b/psydac/feec/multipatch/examples/hcurl_source_testcase.py index 35aa79dd6..720fb75b5 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_testcase.py +++ b/psydac/feec/multipatch/examples/hcurl_source_testcase.py @@ -94,9 +94,6 @@ 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') @@ -124,7 +121,6 @@ project_sol=project_sol, gamma_h=gamma_h, plot_dir=plot_dir, - m_load_dir=m_load_dir, ) # From 751df35ace2de1c97fcb36ac7e85dabbc943743e Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 11 Sep 2025 13:41:07 +0200 Subject: [PATCH 072/102] messed up the merge --- psydac/api/discretization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 76e2d4356..97096c7f0 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -30,7 +30,7 @@ from psydac.api.fem import DiscreteBilinearForm from psydac.api.fem import DiscreteLinearForm from psydac.api.fem import DiscreteFunctional -from psydac.api.feec import DiscreteDerham, DiscreteDeRhamMultipatch +from psydac.api.feec import DiscreteDeRham, DiscreteDeRhamMultipatch from psydac.api.glt import DiscreteGltExpr from psydac.api.expr import DiscreteExpr from psydac.api.equation import DiscreteEquation From eec6fb69972e60d2aa324cfc738f822adad07633 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 11 Sep 2025 16:53:15 +0200 Subject: [PATCH 073/102] calculate hodge inverse iteratively, update test tolerances --- psydac/feec/hodge.py | 56 ++------------ .../examples/hcurl_eigen_pbms_conga_2d.py | 76 ++++++++++--------- .../multipatch/examples/timedomain_maxwell.py | 53 ++++++------- .../tests/test_feec_maxwell_multipatch_2d.py | 15 ++-- .../tests/test_feec_poisson_multipatch_2d.py | 4 +- .../test_feec_conf_projectors_cart_2d.py | 6 +- 6 files changed, 79 insertions(+), 131 deletions(-) diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py index 190ae9eb3..26cb702b4 100644 --- a/psydac/feec/hodge.py +++ b/psydac/feec/hodge.py @@ -1,11 +1,7 @@ import os import numpy as np -from scipy.sparse import save_npz, load_npz -from scipy.sparse import block_diag -from scipy.linalg import inv - -from sympde.topology import element_of, elements_of +from sympde.topology import elements_of from sympde.topology.space import ScalarFunction from sympde.calculus import dot from sympde.expr.expr import BilinearForm @@ -13,8 +9,6 @@ from psydac.api.settings import PSYDAC_BACKENDS -from psydac.linalg.utilities import SparseMatrixLinearOperator - # =============================================================================== class HodgeOperator: """ @@ -78,7 +72,6 @@ def assemble_matrix(self): V = Vh.symbolic_space domain = V.domain - # domain_h = V0h.domain: would be nice... u, v = elements_of(V, names='u, v') if isinstance(u, ScalarFunction): @@ -93,46 +86,7 @@ def assemble_matrix(self): self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop) - # which of the two assemblys for the sparse dual matrix is better? For now use the exact one. - def assemble_dual_sparse_matrix(self): - """ - the dual Hodge sparse 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) exact inversion of the mass matrix - """ - from psydac.fem.basic import FemLinearOperator - - if self._dual_linop is None: - if not self._linop: - self.assemble_matrix() - - M = self._linop # mass matrix of the (primal) basis - - if self._fem_domain.is_multipatch: - nrows = M.n_block_rows - ncols = M.n_block_cols - - inv_M_blocks = [] - for i in range(nrows): - Mii = M[i, i].toarray() - inv_Mii = inv(Mii) - inv_M_blocks.append(inv_Mii) - - inv_M = block_diag(inv_M_blocks, format='csr') - - self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) - - else: - from scipy.sparse import csr_matrix - M_m = M.toarray() - inv_M = inv(M_m) - inv_M = csr_matrix(inv_M) - - self._dual_linop = SparseMatrixLinearOperator(M.codomain, M.domain, inv_M) - self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) - - - def assemble_dual_matrix(self, solver ='gmres', **kwargs): + def assemble_dual_matrix(self, solver ='cg', **kwargs): """ 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 approximate local (patch-wise) inversion of the mass matrix @@ -155,7 +109,7 @@ def assemble_dual_matrix(self, solver ='gmres', **kwargs): inv_M_blocks = [list(b) for b in M.blocks] for i in range(nrows): Mii = M[i, i] - inv_Mii = inverse(M[i,i], solver=solver, **kwargs) + inv_Mii = inverse(Mii, solver=solver, **kwargs) inv_M_blocks[i][i] = inv_Mii self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) @@ -175,7 +129,7 @@ def linop(self): @property def dual_linop(self): if self._dual_linop is None: - self.assemble_dual_sparse_matrix() + self.assemble_dual_matrix() return self._dual_linop @@ -189,6 +143,6 @@ def hodge(self): @property def dual_hodge(self): if self._dual_linop is None: - self.assemble_dual_sparse_matrix() + self.assemble_dual_matrix() return self._dual_hodge 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 1c77a7e8f..418118469 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -145,9 +145,6 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma bD0, bD1 = derham_h.derivatives(kind='linop') - if not os.path.exists(plot_dir): - os.makedirs(plot_dir) - print('computing the full operator matrix...') # Conga (projection-based) stiffness matrices @@ -207,40 +204,45 @@ def hcurl_solve_eigen_pbm(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), doma t_stamp = time_count(t_stamp) print('plotting the eigenmodes...') - OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') - OM.add_spaces(V1h=V1h) - OM.export_space_info() - - nb_eigs = len(eigenvalues) - H1_m = H1.tosparse() - cP1_m = cP1.tosparse() - - 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, H1_m.dot(emode_i)) - eh_c = emode_i / norm_emode_i - - stencil_coeffs = array_to_psydac(cP1_m @ eh_c, V1h.coeff_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() + if plot_dir: + + if not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V1h=V1h) + OM.export_space_info() + + nb_eigs = len(eigenvalues) + H1_m = H1.tosparse() + cP1_m = cP1.tosparse() + + 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, H1_m.dot(emode_i)) + eh_c = emode_i / norm_emode_i + + stencil_coeffs = array_to_psydac(cP1_m @ eh_c, V1h.coeff_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) diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index 1966d1be8..714a9cb35 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -41,7 +41,6 @@ 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 -from psydac.linalg.utilities import array_to_psydac from psydac.fem.basic import FemField from psydac.feec.multipatch.multipatch_domain_utilities import build_cartesian_multipatch_domain @@ -279,7 +278,7 @@ def solve_td_maxwell_pbm(*, # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Compute stable time step size based on max CFL and max dt - dt = compute_stable_dt(C_m=C.tosparse(), dC_m=dC.tosparse(), cfl_max=cfl_max, dt_max=dt_max) + dt = compute_stable_dt(C=C, dC=dC, cfl_max=cfl_max, dt_max=dt_max) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Absorbing dC_m @@ -439,7 +438,7 @@ def source_enveloppe(tau): if f0_h is None: - f0_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) + f0_h = V1h.coeff_space.zeros() t_stamp = time_count(t_stamp) @@ -464,12 +463,12 @@ def source_enveloppe(tau): print(' .. initial solution ..') # initial B sol - B_h = array_to_psydac(np.zeros(V2h.nbasis), V2h.coeff_space) - E_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) + B_h = V2h.coeff_space.zeros() + E_h = V1h.coeff_space.zeros() # initial E sol if E0_type == 'zero': - E_h = array_to_psydac(np.zeros(V1h.nbasis), V1h.coeff_space) + E_h = V1h.coeff_space.zeros() elif E0_type == 'pulse': @@ -541,12 +540,10 @@ def compute_diags(E_h, B_h, J_h, nt): OM2.add_spaces(V2h=V2h) OM2.export_space_info() - #stencil_coeffs_E = array_to_psydac(cP1_m @ E_c, V1h.coeff_space) Eh = FemField(V1h, coeffs=cP1 @ E_h) OM1.add_snapshot(t=0, ts=0) OM1.export_fields(Eh=Eh) - # stencil_coeffs_B = array_to_psydac(B_c, V2h.coeff_space) Bh = FemField(V2h, coeffs=B_h) OM2.add_snapshot(t=0, ts=0) OM2.export_fields(Bh=Bh) @@ -616,12 +613,12 @@ def compute_diags(E_h, B_h, J_h, nt): -def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): +def compute_stable_dt(*, C, dC, 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`, + `dC @ C: 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 @@ -634,10 +631,10 @@ def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): Parameters ---------- - C_m : scipy.sparse.spmatrix + C : LinearOperator Matrix of the Curl operator. - dC_m : scipy.sparse.spmatrix + dC : LinearOperator Matrix of the dual Curl operator. cfl_max : float @@ -657,34 +654,35 @@ def compute_stable_dt(*, C_m, dC_m, cfl_max, dt_max=None): 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])) + print(" .. with dim(V1h) = {} ...".format(C.domain.dimension)) 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) + V = C.domain + from psydac.linalg.utilities import array_to_psydac + vv = array_to_psydac(np.random.rand(V.dimension), V) + + norm_vv = np.sqrt(vv.inner(vv)) + max_ncfl = 500 ncfl = 0 spectral_rho = 1 conv = False - CC_m = dC_m @ C_m + CC = dC @ C while not (conv or ncfl > max_ncfl): - vv[:] = (1. / norm_vv) * vv + vv *= (1. / norm_vv) ncfl += 1 - vv[:] = CC_m.dot(vv) + CC.dot(vv, out=vv) - norm_vv = vect_norm_2(vv) + norm_vv = np.sqrt(vv.inner(vv)) old_spectral_rho = spectral_rho - spectral_rho = vect_norm_2(vv) # approximation + spectral_rho = norm_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) @@ -699,11 +697,8 @@ def vect_norm_2(vv): 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).") + 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/tests/test_feec_maxwell_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py index 007221724..5c9580618 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -29,7 +29,7 @@ def test_time_harmonic_maxwell_pretzel_f(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.007201508128407582) < 1e-10 + assert abs(diags["err"] - 0.0072015081402929445) < 1e-10 def test_time_harmonic_maxwell_pretzel_f_nc(): deg = 2 @@ -53,7 +53,7 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849225522124346) < 1e-7 + assert abs(diags["err"] - 0.004849086546305702) < 1e-7 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' @@ -86,7 +86,6 @@ def test_maxwell_eigen_curved_L_shape(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell', ) error = 0 @@ -95,7 +94,7 @@ def test_maxwell_eigen_curved_L_shape(): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - assert abs(error - 0.01291539899483907) < 1e-10 + assert abs(error - 0.012915398994855902) < 1e-10 def test_maxwell_eigen_curved_L_shape_nc(): domain_name = 'curved_L_shape' @@ -130,7 +129,6 @@ def test_maxwell_eigen_curved_L_shape_nc(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_nc', ) error = 0 @@ -139,7 +137,7 @@ def test_maxwell_eigen_curved_L_shape_nc(): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - assert abs(error - 0.010504876643873904) < 1e-10 + assert abs(error - 0.010504876643886937) < 1e-10 def test_maxwell_eigen_curved_L_shape_dg(): domain_name = 'curved_L_shape' @@ -172,7 +170,6 @@ def test_maxwell_eigen_curved_L_shape_dg(): nb_eigs_plot=nb_eigs_plot, domain_name=domain_name, domain=domain, backend_language='pyccel-gcc', - plot_dir='./plots/eigen_maxell_dg', ) error = 0 @@ -180,8 +177,8 @@ def test_maxwell_eigen_curved_L_shape_dg(): for k in range(n_errs): error += (eigenvalues[k] - ref_sigmas[k])**2 error = np.sqrt(error) - - assert abs(error - 0.035139029534570064) < 1e-10 + + assert abs(error - 0.035139029534592255) < 1e-10 def test_maxwell_timedomain(): solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') 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 972eaea63..064efcd0c 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -18,7 +18,7 @@ def test_poisson_pretzel_f(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 1.0585687717792318e-05) < 1e-10 + assert abs(l2_error - 1.1016888403643595e-05) < 1e-10 def test_poisson_pretzel_f_nc(): @@ -38,7 +38,7 @@ def test_poisson_pretzel_f_nc(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 6.051557012306659e-06) < 1e-10 + assert abs(l2_error - 7.079666478120528e-06) < 1e-10 # ============================================================================== diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index 80a11e1f6..01a0dd38e 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -4,6 +4,7 @@ import numpy as np from sympy import Tuple, lambdify from scipy.sparse.linalg import norm as sp_norm +from scipy.sparse.linalg import inv from sympde.topology.domain import Domain from sympde.topology import Derham, Square, IdentityMapping @@ -150,10 +151,9 @@ def test_conf_projectors_2d( cP0, cP1, cP2 = (m.tosparse() for m in (cP0, cP1, cP2)) M0, M1, M2 = derham_h.hodge_operators() - M0, M1, M2 = (m.tosparse() for m in (M0, M1, M2)) + M0, M1, M2 = (m.tosparse().tocsc() for m in (M0, M1, M2)) - M0_inv, M1_inv, M2_inv = derham_h.hodge_operators(dual=True) - M0_inv, M1_inv, M2_inv = (m.tosparse() for m in (M0_inv, M1_inv, M2_inv)) + M0_inv, M1_inv, M2_inv = (inv(m) for m in (M0, M1, M2)) bD0, bD1 = derham_h.derivatives() bD0, bD1 = (m.tosparse() for m in (bD0, bD1)) From 57c8e09cf3b68d54795c1eb4a6d361af4a8c7c2d Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 11 Sep 2025 17:36:40 +0200 Subject: [PATCH 074/102] reiterate tests --- .../examples/hcurl_eigen_pbms_dg_2d.py | 69 ++++++++++--------- .../tests/test_feec_maxwell_multipatch_2d.py | 2 +- .../tests/test_feec_poisson_multipatch_2d.py | 4 +- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py index 710c2c1b4..0235dd72d 100644 --- a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py @@ -191,40 +191,41 @@ def avr(w): return 0.5 * plus(w) + 0.5 * minus(w) 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.coeff_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() + if plot_dir: + 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.coeff_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) 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 5c9580618..3986aabb2 100644 --- a/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -53,7 +53,7 @@ def test_time_harmonic_maxwell_pretzel_f_nc(): source_proj=source_proj, backend_language='pyccel-gcc') - assert abs(diags["err"] - 0.004849086546305702) < 1e-7 + assert abs(diags["err"] - 0.004849225522124346) < 5e-7 def test_maxwell_eigen_curved_L_shape(): domain_name = 'curved_L_shape' 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 064efcd0c..2804fba2d 100644 --- a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -18,7 +18,7 @@ def test_poisson_pretzel_f(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 1.1016888403643595e-05) < 1e-10 + assert abs(l2_error - 1.1016888403643595e-05) < 5e-8 def test_poisson_pretzel_f_nc(): @@ -38,7 +38,7 @@ def test_poisson_pretzel_f_nc(): backend_language='pyccel-gcc', plot_dir=None) - assert abs(l2_error - 7.079666478120528e-06) < 1e-10 + assert abs(l2_error - 7.079666478120528e-06) < 5e-8 # ============================================================================== From 9501ede7f9ad3b102db3739483d1bdff3d31741c Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 14:00:46 +0200 Subject: [PATCH 075/102] update dot of SparseMatrixLinearOperator --- psydac/linalg/tests/test_block.py | 83 ++++++++++++++++++++++++++++++- psydac/linalg/utilities.py | 67 ++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/psydac/linalg/tests/test_block.py b/psydac/linalg/tests/test_block.py index ff3bf1e6c..a1dcdd7ec 100644 --- a/psydac/linalg/tests/test_block.py +++ b/psydac/linalg/tests/test_block.py @@ -9,7 +9,7 @@ from psydac.linalg.stencil import StencilVectorSpace, StencilVector, StencilMatrix from psydac.linalg.block import BlockVectorSpace, BlockVector from psydac.linalg.block import BlockLinearOperator -from psydac.linalg.utilities import array_to_psydac, petsc_to_psydac +from psydac.linalg.utilities import array_to_psydac, petsc_to_psydac, SparseMatrixLinearOperator from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.cart import DomainDecomposition, CartDecomposition @@ -727,6 +727,87 @@ def test_block_linear_operator_serial_dot( dtype, n1, n2, p1, p2, P1, P2 ): @pytest.mark.parametrize( 'dtype', [float, complex] ) @pytest.mark.parametrize( 'n1', [8, 16] ) @pytest.mark.parametrize( 'n2', [8, 12] ) +@pytest.mark.parametrize( 'p1', [1, 3] ) +@pytest.mark.parametrize( 'p2', [1, 2] ) +@pytest.mark.parametrize( 'P1', [True, False] ) +@pytest.mark.parametrize( 'P2', [True] ) + +def test_sparse_matrix_linear_operator_serial_dot( dtype, n1, n2, p1, p2, P1, P2 ): + # set seed for reproducibility + seed(n1*n2*p1*p2) + + D = DomainDecomposition([n1,n2], periods=[P1,P2]) + + # Partition the points + npts = [n1,n2] + global_starts, global_ends = compute_global_starts_ends(D, npts) + + cart = CartDecomposition(D, npts, global_starts, global_ends, pads=[p1,p2], shifts=[1,1]) + + # Create vector spaces, stencil matrices, and stencil vectors + V = StencilVectorSpace( cart, dtype=dtype ) + M1 = StencilMatrix( V, V) + M2 = StencilMatrix( V, V ) + M3 = StencilMatrix( V, V ) + x1 = StencilVector( V ) + x2 = StencilVector( V ) + + # Fill in stencil matrices based on diagonal index + if dtype==complex: + f=lambda k1,k2: 10j*k1+k2 + else: + f=lambda k1,k2: 10*k1+k2 + + for k1 in range(-p1,p1+1): + for k2 in range(-p2,p2+1): + M1[:,:,k1,k2] = f(k1,k2) + M2[:,:,k1,k2] = f(k1,k2)+2. + M3[:,:,k1,k2] = f(k1,k2)+5. + + M1.remove_spurious_entries() + M2.remove_spurious_entries() + M3.remove_spurious_entries() + + # Fill in vector with random values, then update ghost regions + for i1 in range(n1): + for i2 in range(n2): + x1[i1,i2] = 2.0*random() - 1.0 + x2[i1,i2] = 5.0*random() - 1.0 + x1.update_ghost_regions() + x2.update_ghost_regions() + + W = BlockVectorSpace(V, V) + + # Construct a BlockLinearOperator object containing M1, M2, M, using 3 ways + # |M1 M2| + # L = | | + # |M3 0 | + + dict_blocks = {(0,0):M1, (0,1):M2, (1,0):M3} + + L = BlockLinearOperator( W, W, blocks=dict_blocks ) + Lm = SparseMatrixLinearOperator(W, W, L.tosparse().tocsr()) + + # Construct a BlockVector object containing x1 and x2 + # |x1| + # X = | | + # |x2| + + X = BlockVector(W) + X[0] = x1 + X[1] = x2 + + # Compute BlockLinearOperator product + Y = L.dot(X) + + Ym = Lm.dot(X) + + # Check data in 1D array + assert np.allclose( Ym.toarray(), Y.toarray(), rtol=1e-12, atol=1e-12 ) +#=============================================================================== +@pytest.mark.parametrize( 'dtype', [float, complex] ) +@pytest.mark.parametrize( 'n1', [8, 16] ) +@pytest.mark.parametrize( 'n2', [8, 12] ) @pytest.mark.parametrize( 'p1', [1, 2] ) @pytest.mark.parametrize( 'p2', [1, 3] ) @pytest.mark.parametrize( 'P1', [True, False] ) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index 0c5c8fab9..c650af941 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -3,11 +3,13 @@ import numpy as np from math import sqrt -from psydac.linalg.basic import Vector, MatrixFreeLinearOperator +from psydac.linalg.basic import VectorSpace, Vector, MatrixFreeLinearOperator from psydac.linalg.stencil import StencilVectorSpace, StencilVector from psydac.linalg.block import BlockVector, BlockVectorSpace from psydac.linalg.topetsc import petsc_local_to_psydac, get_npts_per_block +from scipy.sparse import csr_matrix + __all__ = ( 'array_to_psydac', 'petsc_to_psydac', @@ -207,14 +209,14 @@ class SparseMatrixLinearOperator(MatrixFreeLinearOperator): """ def __init__(self, domain, codomain, sparse_matrix): - self._matrix = sparse_matrix - def dot_sparse(v): - res = self._matrix @ v.toarray() - Mv = array_to_psydac(res, self.codomain) - return Mv + assert isinstance(domain, VectorSpace) + assert isinstance(codomain, VectorSpace) + assert isinstance(sparse_matrix, csr_matrix) - super().__init__(domain, codomain, dot_sparse) + self._domain = domain + self._codomain = codomain + self._matrix = sparse_matrix def tosparse(self): return self._matrix @@ -224,3 +226,54 @@ def toarray(self): def transpose(self, conjugate=False): return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) + + def dot(self, v, out=None): + + assert v.space is self.domain + + if out is not None: + + assert out.space is self.codomain + + out *= 0.0 + else: + out = self.codomain.zeros() + + if not v.ghost_regions_in_sync: + v.update_ghost_regions() + + if self.domain.parallel: + raise NotImplementedError('Parallel case not implemented yet.') + else: + self._dot_recursive(v, out) + + out.ghost_regions_in_sync = False + + return out + + def _dot_recursive(self, v, out, ind_V=0, ind_W=0): + V = v.space + W = out.space + + if isinstance(v, StencilVector): + index_global_W = tuple(slice(s, e+1) for s, e in zip(W.starts, W.ends)) + index_global_V = tuple(slice(s, e+1) for s, e in zip(V.starts, V.ends)) + + dim_W = W.dimension + dim_V = V.dimension + + out[index_global_W].flat += self._matrix[ind_W:ind_W+dim_W, ind_V:ind_V+dim_V] @ v[index_global_V].flat + + elif isinstance(v, BlockVector): + + offset_i = ind_W + for (i, Wi) in enumerate(W.spaces): + + offset_j = ind_V + for (j, Vj) in enumerate(V.spaces): + + self._dot_recursive(v[j], out[i], ind_V=offset_j, ind_W=offset_i) + + offset_j += Vj.dimension + + offset_i += Wi.dimension From 5be97d6e391e2dc71d5c29c56da361d1af03b06d Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 14:03:40 +0200 Subject: [PATCH 076/102] add empty line --- psydac/linalg/utilities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index c650af941..ad8b96cd0 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -277,3 +277,4 @@ def _dot_recursive(self, v, out, ind_V=0, ind_W=0): offset_j += Vj.dimension offset_i += Wi.dimension + From 3ed4ff9ba8bf00d54ea4bb5fcefb75cc8d2955b5 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 14:11:54 +0200 Subject: [PATCH 077/102] update TD Maxwell example --- psydac/feec/multipatch/examples/timedomain_maxwell.py | 10 +++++----- .../multipatch/examples/timedomain_maxwell_testcase.py | 8 +++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index 714a9cb35..8ad5d1c65 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -281,7 +281,7 @@ def solve_td_maxwell_pbm(*, dt = compute_stable_dt(C=C, dC=dC, cfl_max=cfl_max, dt_max=dt_max) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Absorbing dC_m + # Absorbing dC CH2 = C.T @ H2 H1A = H1 + dt * A_eps @@ -483,7 +483,7 @@ def source_enveloppe(tau): print(' .. projecting E0 with L2 projection...') tilde_E0_h = get_dual_dofs(Vh=V1h, f=E0, domain_h=domain_h, backend_language=backend) - E_h = dH1_m.dot(tilde_E0_h) + E_h = dH1.dot(tilde_E0_h) elif E0_type == 'pulse_2': @@ -527,7 +527,7 @@ def compute_diags(E_h, B_h, J_h, nt): if source_type == 'Il_pulse' and source_omega is not None: rho_h = rho0_h * np.sin(source_omega * nt * dt) / omega GaussErr = rho_h - divE_h - GaussErrP = rho_h - div_m @ PE_h + GaussErrP = rho_h - D @ PE_h GaussErr_norm2_diag[nt] = GaussErr.inner(H0.dot(GaussErr)) GaussErrP_norm2_diag[nt] = GaussErrP.inner(H0.dot(GaussErrP)) @@ -684,7 +684,7 @@ def compute_stable_dt(*, C, dC, cfl_max, dt_max=None): old_spectral_rho = spectral_rho spectral_rho = norm_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)) + print(" ... spectral radius iteration: spectral_rho( dC @ C ) ~= {}".format(spectral_rho)) t_stamp = time_count(t_stamp) norm_op = np.sqrt(spectral_rho) @@ -699,6 +699,6 @@ def compute_stable_dt(*, C, dC, cfl_max, dt_max=None): 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).") + print(f" -- and spectral_radius((c*dt)**2* dC @ C ) = {(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 index 759dd1f52..e17c70b4a 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py @@ -5,15 +5,13 @@ import numpy as np from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm -from psydac.feec.multipatch.utilities import FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utilities import get_run_dir, get_plot_dir # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- # -# 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 = 'E0_pulse_no_source' +# test_case = 'Issautier_like_source' # J_proj_case = 'P_geom' J_proj_case = 'P_L2' From a16a548e2594fcd693027011d0276866c1590a75 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 14:42:33 +0200 Subject: [PATCH 078/102] convert cPl to csr --- psydac/feec/conforming_projectors.py | 4 ++-- psydac/linalg/utilities.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index f4faa4b2c..7f9e3ea98 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -1459,7 +1459,7 @@ def __init__( else: sparse_matrix = construct_h1_singlepatch_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix.tocsr()) class ConformingProjectionV1(FemLinearOperator): @@ -1499,4 +1499,4 @@ def __init__( else: sparse_matrix = construct_hcurl_singlepatch_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) - self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix) + self._linop = SparseMatrixLinearOperator(self.linop_domain, self.linop_codomain, sparse_matrix.tocsr()) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index ad8b96cd0..c650af941 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -277,4 +277,3 @@ def _dot_recursive(self, v, out, ind_V=0, ind_W=0): offset_j += Vj.dimension offset_i += Wi.dimension - From d1a7dd42aed61c3a3feb021a7516fac1f37e1237 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 15:18:23 +0200 Subject: [PATCH 079/102] actually use the MatrixFree interface --- psydac/linalg/utilities.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index c650af941..284540d69 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -214,10 +214,13 @@ def __init__(self, domain, codomain, sparse_matrix): assert isinstance(codomain, VectorSpace) assert isinstance(sparse_matrix, csr_matrix) - self._domain = domain - self._codomain = codomain self._matrix = sparse_matrix + def dot_sparse(v, out=None): + return self._dot_recursive(v, out) + + super().__init__(domain, codomain, dot_sparse) + def tosparse(self): return self._matrix @@ -227,30 +230,6 @@ def toarray(self): def transpose(self, conjugate=False): return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) - def dot(self, v, out=None): - - assert v.space is self.domain - - if out is not None: - - assert out.space is self.codomain - - out *= 0.0 - else: - out = self.codomain.zeros() - - if not v.ghost_regions_in_sync: - v.update_ghost_regions() - - if self.domain.parallel: - raise NotImplementedError('Parallel case not implemented yet.') - else: - self._dot_recursive(v, out) - - out.ghost_regions_in_sync = False - - return out - def _dot_recursive(self, v, out, ind_V=0, ind_W=0): V = v.space W = out.space From ecaa82fe57972a6d8d8070a4b662c9b6b07d7aec Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 15:57:40 +0200 Subject: [PATCH 080/102] fix transpose and keyword argument out --- psydac/linalg/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index 284540d69..b2a2c4f9e 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -216,7 +216,7 @@ def __init__(self, domain, codomain, sparse_matrix): self._matrix = sparse_matrix - def dot_sparse(v, out=None): + def dot_sparse(v, *, out): return self._dot_recursive(v, out) super().__init__(domain, codomain, dot_sparse) @@ -228,7 +228,7 @@ def toarray(self): return self._matrix.toarray() def transpose(self, conjugate=False): - return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T) + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T.tocsr()) def _dot_recursive(self, v, out, ind_V=0, ind_W=0): V = v.space From 124d813828d10f9b770e635db051b9ebac00ee47 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Mon, 15 Sep 2025 18:11:07 +0200 Subject: [PATCH 081/102] fix bug in MatrixFreeLinearOperator --- psydac/linalg/basic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index ff9af07ad..46fce1b78 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -1318,6 +1318,7 @@ def dot(self, v, out=None, **kwargs): if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain + out *= 0 else: out = self.codomain.zeros() From 7eda8c41ce206113192f7329531c0ffc91688aa1 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 18 Sep 2025 13:19:04 +0200 Subject: [PATCH 082/102] make SparseMatrixLinearOperator subclass LinearOperator and move to psydac/linalg/sparse.py --- psydac/feec/conforming_projectors.py | 2 +- .../multipatch/examples/timedomain_maxwell.py | 2 +- psydac/linalg/sparse.py | 96 +++++++++++++++++++ psydac/linalg/tests/test_block.py | 3 +- psydac/linalg/utilities.py | 62 +----------- 5 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 psydac/linalg/sparse.py diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index 7f9e3ea98..d4b9a40dd 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -13,7 +13,7 @@ from psydac.fem.basic import FemLinearOperator from psydac.fem.splines import SplineSpace from psydac.utilities.quadratures import gauss_legendre -from psydac.linalg.utilities import SparseMatrixLinearOperator +from psydac.linalg.sparse import SparseMatrixLinearOperator __all__ = ( 'ConformingProjectionV0', diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py index 8ad5d1c65..f382f0111 100644 --- a/psydac/feec/multipatch/examples/timedomain_maxwell.py +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -292,7 +292,7 @@ def solve_td_maxwell_pbm(*, M = H1A from scipy.linalg import inv from scipy.sparse import csr_matrix - from psydac.linalg.utilities import SparseMatrixLinearOperator + from psydac.linalg.sparse import SparseMatrixLinearOperator M_inv = inv(M.toarray()) M_inv = csr_matrix(M_inv) H1A_inv = SparseMatrixLinearOperator(M.codomain, M.domain, M_inv) diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py new file mode 100644 index 000000000..3c88da686 --- /dev/null +++ b/psydac/linalg/sparse.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +from scipy.sparse import csr_matrix + +from psydac.linalg.basic import LinearOperator +from psydac.linalg.basic import VectorSpace, Vector, LinearOperator +from psydac.linalg.stencil import StencilVector +from psydac.linalg.block import BlockVector + +__all__ = ( + 'SparseMatrixLinearOperator', +) + +class SparseMatrixLinearOperator(LinearOperator): + """ + Wrap a sparse matrix into a LinearOperator. + """ + + def __init__(self, domain, codomain, sparse_matrix): + + assert isinstance(domain, VectorSpace) + assert isinstance(codomain, VectorSpace) + assert isinstance(sparse_matrix, csr_matrix) + + if domain.parallel: + raise NotImplementedError('Parallel SparseMatrixLinearOperator not supported yet.') + + self._domain = domain + self._codomain = codomain + self._matrix = sparse_matrix + + @property + def domain(self): + return self._domain + + @property + def codomain(self): + return self._codomain + + @property + def dtype(self): + return self._matrix.dtype + + def toarray(self): + return self._matrix.toarray() + + def tosparse(self): + return self._matrix + + def transpose(self, conjugate=False): + if conjugate: + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.getH().tocsr()) + else: + return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T.tocsr()) + + def dot(self, v, out=None): + assert isinstance(v, Vector) + assert v.space == self.domain + + if out is not None: + assert isinstance(out, Vector) + assert out.space == self.codomain + out *= 0 + else: + out = self.codomain.zeros() + + self._dot_recursive(v, out=out) + + return out + + def _dot_recursive(self, v, out, ind_V=0, ind_W=0): + V = v.space + W = out.space + + if isinstance(v, StencilVector): + index_global_W = tuple(slice(s, e+1) for s, e in zip(W.starts, W.ends)) + index_global_V = tuple(slice(s, e+1) for s, e in zip(V.starts, V.ends)) + + dim_W = W.dimension + dim_V = V.dimension + + out[index_global_W].flat += self._matrix[ind_W:ind_W+dim_W, ind_V:ind_V+dim_V] @ v[index_global_V].flat + + elif isinstance(v, BlockVector): + + offset_i = ind_W + for (i, Wi) in enumerate(W.spaces): + + offset_j = ind_V + for (j, Vj) in enumerate(V.spaces): + + self._dot_recursive(v[j], out[i], ind_V=offset_j, ind_W=offset_i) + + offset_j += Vj.dimension + + offset_i += Wi.dimension diff --git a/psydac/linalg/tests/test_block.py b/psydac/linalg/tests/test_block.py index a1dcdd7ec..23a53046a 100644 --- a/psydac/linalg/tests/test_block.py +++ b/psydac/linalg/tests/test_block.py @@ -9,7 +9,8 @@ from psydac.linalg.stencil import StencilVectorSpace, StencilVector, StencilMatrix from psydac.linalg.block import BlockVectorSpace, BlockVector from psydac.linalg.block import BlockLinearOperator -from psydac.linalg.utilities import array_to_psydac, petsc_to_psydac, SparseMatrixLinearOperator +from psydac.linalg.utilities import array_to_psydac, petsc_to_psydac +from psydac.linalg.sparse import SparseMatrixLinearOperator from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.cart import DomainDecomposition, CartDecomposition diff --git a/psydac/linalg/utilities.py b/psydac/linalg/utilities.py index b2a2c4f9e..fb13a2e6d 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -3,18 +3,15 @@ import numpy as np from math import sqrt -from psydac.linalg.basic import VectorSpace, Vector, MatrixFreeLinearOperator -from psydac.linalg.stencil import StencilVectorSpace, StencilVector +from psydac.linalg.basic import Vector +from psydac.linalg.stencil import StencilVector, StencilVectorSpace from psydac.linalg.block import BlockVector, BlockVectorSpace from psydac.linalg.topetsc import petsc_local_to_psydac, get_npts_per_block -from scipy.sparse import csr_matrix - __all__ = ( 'array_to_psydac', 'petsc_to_psydac', '_sym_ortho', - 'SparseMatrixLinearOperator', ) #============================================================================== @@ -201,58 +198,3 @@ def _sym_ortho(a, b): s = c * tau r = a / c return c, s, r - -#============================================================================== -class SparseMatrixLinearOperator(MatrixFreeLinearOperator): - """ - Wrap a sparse matrix into a MatrixFreeLinearOperator - """ - - def __init__(self, domain, codomain, sparse_matrix): - - assert isinstance(domain, VectorSpace) - assert isinstance(codomain, VectorSpace) - assert isinstance(sparse_matrix, csr_matrix) - - self._matrix = sparse_matrix - - def dot_sparse(v, *, out): - return self._dot_recursive(v, out) - - super().__init__(domain, codomain, dot_sparse) - - def tosparse(self): - return self._matrix - - def toarray(self): - return self._matrix.toarray() - - def transpose(self, conjugate=False): - return SparseMatrixLinearOperator(self.codomain, self.domain, self._matrix.T.tocsr()) - - def _dot_recursive(self, v, out, ind_V=0, ind_W=0): - V = v.space - W = out.space - - if isinstance(v, StencilVector): - index_global_W = tuple(slice(s, e+1) for s, e in zip(W.starts, W.ends)) - index_global_V = tuple(slice(s, e+1) for s, e in zip(V.starts, V.ends)) - - dim_W = W.dimension - dim_V = V.dimension - - out[index_global_W].flat += self._matrix[ind_W:ind_W+dim_W, ind_V:ind_V+dim_V] @ v[index_global_V].flat - - elif isinstance(v, BlockVector): - - offset_i = ind_W - for (i, Wi) in enumerate(W.spaces): - - offset_j = ind_V - for (j, Vj) in enumerate(V.spaces): - - self._dot_recursive(v[j], out[i], ind_V=offset_j, ind_W=offset_i) - - offset_j += Vj.dimension - - offset_i += Wi.dimension From 5e182ff3657aac142266366d0df376700c8d9a42 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 18 Sep 2025 17:49:09 +0200 Subject: [PATCH 083/102] rename global projectors --- psydac/api/feec.py | 85 +++++++++--------- psydac/api/tests/test_api_2d_fields.py | 10 +-- ...tors.py => global_geometric_projectors.py} | 88 ++++++------------- .../feec/tests/test_commuting_projections.py | 38 ++++---- .../tests/test_differentiation_matrices.py | 2 - psydac/feec/tests/test_global_projectors.py | 16 ++-- .../feec/tests/test_projections_parallel.py | 26 +++--- 7 files changed, 118 insertions(+), 147 deletions(-) rename psydac/feec/{global_projectors.py => global_geometric_projectors.py} (94%) diff --git a/psydac/api/feec.py b/psydac/api/feec.py index 6fb25791d..2bd95ffbd 100644 --- a/psydac/api/feec.py +++ b/psydac/api/feec.py @@ -1,29 +1,32 @@ -from psydac.api.basic import BasicDiscrete +from psydac.api.basic import BasicDiscrete -from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D -from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D -from psydac.feec.derivatives import Divergence2D, Divergence3D -from psydac.feec.derivatives import BrokenGradient2D -from psydac.feec.derivatives import BrokenScalarCurl2D +from psydac.feec.derivatives import Derivative1D, Gradient2D, Gradient3D +from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D +from psydac.feec.derivatives import Divergence2D, Divergence3D +from psydac.feec.derivatives import BrokenGradient2D +from psydac.feec.derivatives import BrokenScalarCurl2D -from psydac.feec.global_projectors import ProjectorH1, ProjectorHcurl, ProjectorH1vec -from psydac.feec.global_projectors import ProjectorHdiv, ProjectorL2 -from psydac.feec.global_projectors import MultipatchProjectorH1 -from psydac.feec.global_projectors import MultipatchProjectorHcurl -from psydac.feec.global_projectors import MultipatchProjectorL2 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1vec +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import MultipatchGeometricProjector -from psydac.feec.conforming_projectors import ConformingProjectionV0 -from psydac.feec.conforming_projectors import ConformingProjectionV1 +from psydac.feec.conforming_projectors import ConformingProjectionV0 +from psydac.feec.conforming_projectors import ConformingProjectionV1 -from psydac.feec.hodge import HodgeOperator +from psydac.feec.hodge import HodgeOperator -from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 -from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl, pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec -from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl, pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec +from psydac.feec.pull_push import pull_1d_h1, pull_1d_l2 +from psydac.feec.pull_push import pull_2d_h1, pull_2d_hcurl +from psydac.feec.pull_push import pull_2d_hdiv, pull_2d_l2, pull_2d_h1vec +from psydac.feec.pull_push import pull_3d_h1, pull_3d_hcurl +from psydac.feec.pull_push import pull_3d_hdiv, pull_3d_l2, pull_3d_h1vec -from psydac.fem.basic import FemSpace, FemLinearOperator -from psydac.fem.vector import VectorFemSpace -from psydac.linalg.basic import IdentityOperator +from psydac.fem.basic import FemSpace, FemLinearOperator +from psydac.fem.vector import VectorFemSpace +from psydac.linalg.basic import IdentityOperator __all__ = ('DiscreteDeRham', 'DiscreteDeRhamMultipatch',) @@ -214,8 +217,8 @@ def projectors(self, *, kind='global', nquads=None): assert all(nq >= 1 for nq in nquads) if self.dim == 1: - P0 = ProjectorH1(self.V0) - P1 = ProjectorL2(self.V1, nquads) + P0 = GlobalGeometricProjectorH1(self.V0) + P1 = GlobalGeometricProjectorL2(self.V1, nquads) if self.mapping: P0_m = lambda f: P0(pull_1d_h1(f, self.callable_mapping)) P1_m = lambda f: P1(pull_1d_l2(f, self.callable_mapping)) @@ -223,19 +226,19 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1 elif self.dim == 2: - P0 = ProjectorH1(self.V0) - P2 = ProjectorL2(self.V2, nquads) + P0 = GlobalGeometricProjectorH1(self.V0) + P2 = GlobalGeometricProjectorL2(self.V2, nquads) kind = self.V1.symbolic_space.kind.name if kind == 'hcurl': - P1 = ProjectorHcurl(self.V1, nquads) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) elif kind == 'hdiv': - P1 = ProjectorHdiv(self.V1, nquads) + P1 = GlobalGeometricProjectorHdiv(self.V1, nquads) else: raise TypeError('projector of space type {} is not available'.format(kind)) if self.has_vec : - Pvec = ProjectorH1vec(self.H1vec, nquads) + Pvec = GlobalGeometricProjectorH1vec(self.H1vec, nquads) if self.mapping: P0_m = lambda f: P0(pull_2d_h1(f, self.callable_mapping)) @@ -256,12 +259,12 @@ def projectors(self, *, kind='global', nquads=None): return P0, P1, P2 elif self.dim == 3: - P0 = ProjectorH1 (self.V0) - P1 = ProjectorHcurl(self.V1, nquads) - P2 = ProjectorHdiv (self.V2, nquads) - P3 = ProjectorL2 (self.V3, nquads) + P0 = GlobalGeometricProjectorH1 (self.V0) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) + P2 = GlobalGeometricProjectorHdiv (self.V2, nquads) + P3 = GlobalGeometricProjectorL2 (self.V3, nquads) if self.has_vec : - Pvec = ProjectorH1vec(self.H1vec) + Pvec = GlobalGeometricProjectorH1vec(self.H1vec) if self.mapping: P0_m = lambda f: P0(pull_3d_h1 (f, self.callable_mapping)) P1_m = lambda f: P1(pull_3d_hcurl(f, self.callable_mapping)) @@ -556,13 +559,13 @@ def projectors(self, *, kind='global', nquads=None): Returns ------- - P0: + P0: Patch wise H1 projector - P1: + P1: Patch wise Hcurl projector - P2: + P2: Patch wise L2 projector Notes @@ -578,18 +581,16 @@ def projectors(self, *, kind='global', nquads=None): raise NotImplementedError("1D projectors are not available") elif self.dim == 2: - P0 = MultipatchProjectorH1(self.V0) + P0 = MultipatchGeometricProjector(self.V0, GlobalGeometricProjectorH1) if self.sequence[1] == 'hcurl': - P1 = MultipatchProjectorHcurl(self.V1, nquads=nquads) + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHcurl, nquads=nquads) else: - P1 = None # TODO: Multipatch_ProjectorHdiv(self.V1, nquads=nquads) - raise NotImplementedError('2D sequence with H-div not available yet') + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHdiv, nquads=nquads) - P2 = MultipatchProjectorL2(self.V2, nquads=nquads) - - if self.mapping: + P2 = MultipatchGeometricProjector(self.V2, GlobalGeometricProjectorL2, nquads=nquads) + if self.mapping: P0_m = lambda f : P0([pull_2d_h1(f, m) for m in self.callable_mapping]) if self.sequence[1] == 'hcurl': diff --git a/psydac/api/tests/test_api_2d_fields.py b/psydac/api/tests/test_api_2d_fields.py index c1f0b893a..d829bcfd5 100644 --- a/psydac/api/tests/test_api_2d_fields.py +++ b/psydac/api/tests/test_api_2d_fields.py @@ -32,10 +32,10 @@ from sympde.expr import Norm from sympde.expr import find, EssentialBC -from psydac.fem.basic import FemField -from psydac.api.discretization import discretize -from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL -from psydac.feec.global_projectors import ProjectorH1 +from psydac.fem.basic import FemField +from psydac.api.discretization import discretize +from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 # ... get the mesh directory try: @@ -142,7 +142,7 @@ def run_boundary_field_test(domain, boundary, f, ncells): x,y = domain.coordinates f_lambda = lambdify([x,y], f, 'math') - Pi0 = ProjectorH1(Vh) + Pi0 = GlobalGeometricProjectorH1(Vh) fh = Pi0(f_lambda) fh.coeffs.update_ghost_regions() diff --git a/psydac/feec/global_projectors.py b/psydac/feec/global_geometric_projectors.py similarity index 94% rename from psydac/feec/global_projectors.py rename to psydac/feec/global_geometric_projectors.py index c7deb9a3f..15cfd58c7 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -17,14 +17,14 @@ from abc import ABCMeta, abstractmethod -__all__ = ('GlobalProjector', 'ProjectorH1', 'ProjectorHcurl', 'ProjectorHdiv', 'ProjectorL2', - 'MultipatchProjectorH1', 'MultipatchProjectorHcurl', 'MultipatchProjectorL2', +__all__ = ('GlobalGeometricProjector', 'GlobalGeometricProjectorH1', 'GlobalGeometricProjectorHcurl', 'GlobalGeometricProjectorHdiv', 'GlobalGeometricProjectorL2', + 'MultipatchGeometricProjector', 'evaluate_dofs_1d_0form', 'evaluate_dofs_1d_1form', 'evaluate_dofs_2d_0form', 'evaluate_dofs_2d_1form_hcurl', 'evaluate_dofs_2d_1form_hdiv', 'evaluate_dofs_2d_2form', 'evaluate_dofs_3d_0form', 'evaluate_dofs_3d_1form', 'evaluate_dofs_3d_2form', 'evaluate_dofs_3d_3form') #============================================================================== -class GlobalProjector(metaclass=ABCMeta): +class GlobalGeometricProjector(metaclass=ABCMeta): """ Projects callable functions to some scalar or vector FEM space. @@ -44,7 +44,7 @@ class GlobalProjector(metaclass=ABCMeta): space : VectorFemSpace | TensorFemSpace Some finite element space, codomain of the projection operator. The exact structure where to use histopolation and where interpolation - has to be given by a subclass of the GlobalProjector class. + has to be given by a subclass of the GlobalGeometricProjector class. As of now, it is implicitly assumed for a VectorFemSpace, that for each direction that all spaces with interpolation are the same, and all spaces with histopolation are the same (i.e. yield the same quadrature/interpolation points etc.); so use with care on an arbitrary VectorFemSpace. @@ -392,7 +392,7 @@ def __call__(self, fun): #============================================================================== # SINGLEPATCH PROJECTORS #============================================================================== -class ProjectorH1(GlobalProjector): +class GlobalGeometricProjectorH1(GlobalGeometricProjector): """ Projector from H1 to an H1-conforming finite element space (i.e. a finite dimensional subspace of H1) constructed with tensor-product B-splines in 1, @@ -442,7 +442,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class ProjectorHcurl(GlobalProjector): +class GlobalGeometricProjectorHcurl(GlobalGeometricProjector): """ Projector from H(curl) to an H(curl)-conforming finite element space, i.e. a finite dimensional subspace of H(curl), constructed with tensor-product @@ -515,7 +515,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class ProjectorHdiv(GlobalProjector): +class GlobalGeometricProjectorHdiv(GlobalGeometricProjector): """ Projector from H(div) to an H(div)-conforming finite element space, i.e. a finite dimensional subspace of H(div), constructed with tensor-product @@ -592,7 +592,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class ProjectorL2(GlobalProjector): +class GlobalGeometricProjectorL2(GlobalGeometricProjector): """ Projector from L2 to an L2-conforming finite element space (i.e. a finite dimensional subspace of L2) constructed with tensor-product M-splines in 1, @@ -652,7 +652,7 @@ def __call__(self, fun): return super().__call__(fun) #============================================================================== -class ProjectorH1vec(GlobalProjector): +class GlobalGeometricProjectorH1vec(GlobalGeometricProjector): """ Projector from H1^3 = H1 x H1 x H1 to a conforming finite element space, i.e. a finite dimensional subspace of H1^3, constructed with tensor-product @@ -719,72 +719,34 @@ def __call__(self, fun): #============================================================================== # MULTIPATCH PROJECTORS (2D) #============================================================================== -class MultipatchProjectorH1: - """ - to apply the H1 projection (2D) on every patch - """ - - def __init__(self, V0h): - - self._P0s = [ProjectorH1(V) for V in V0h.spaces] - self._V0h = V0h # multipatch Fem Space - - def __call__(self, funs): - """ - project a list of functions given in the logical domain - """ - u0s = [P(fun) for P, fun, in zip(self._P0s, funs)] - - u0_coeffs = BlockVector(self._V0h.coeff_space, - blocks=[u0j.coeffs for u0j in u0s]) - - return FemField(self._V0h, coeffs=u0_coeffs) - -#============================================================================== -class MultipatchProjectorHcurl: - - """ - to apply the Hcurl projection (2D) on every patch +class MultipatchGeometricProjector: """ + Global Geometric Projector base class for multipatch domains. - def __init__(self, V1h, nquads=None): - - self._P1s = [ProjectorHcurl(V, nquads=nquads) for V in V1h.spaces] - self._V1h = V1h # multipatch Fem Space - - def __call__(self, funs): - """ - project a list of functions given in the logical domain - """ - E1s = [P(fun) for P, fun, in zip(self._P1s, funs)] - - E1_coeffs = BlockVector(self._V1h.coeff_space, - blocks=[E1j.coeffs for E1j in E1s]) - - return FemField(self._V1h, coeffs=E1_coeffs) - -#============================================================================== -class MultipatchProjectorL2: - - """ - to apply the L2 projection (2D) on every patch + Parameters + ---------- + Vh : MultipatchFemSpace + Multipatch finite element space, codomain of the projection operator + P : GlobalGeometricProjector + Projector to be applied on each patch + nquads : list(int) | tuple(int) + Quadrature points for the GlobalGeometricProjector. """ - def __init__(self, V2h, nquads=None): + def __init__(self, Vh, P, nquads=None): - self._P2s = [ProjectorL2(V, nquads=nquads) for V in V2h.spaces] - self._V2h = V2h # multipatch Fem Space + self._Vh = Vh + self._Ps = [P(V, nquads=nquads) for V in Vh.spaces] def __call__(self, funs): """ project a list of functions given in the logical domain """ - B2s = [P(fun) for P, fun, in zip(self._P2s, funs)] + us = [P(fun) for P, fun, in zip(self._Ps, funs)] - B2_coeffs = BlockVector(self._V2h.coeff_space, - blocks=[B2j.coeffs for B2j in B2s]) + u_c = BlockVector(self._Vh.coeff_space, blocks=[uj.coeffs for uj in us]) - return FemField(self._V2h, coeffs=B2_coeffs) + return FemField(self._Vh, coeffs=u_c) #============================================================================== # 1D DEGREES OF FREEDOM diff --git a/psydac/feec/tests/test_commuting_projections.py b/psydac/feec/tests/test_commuting_projections.py index a3db1de91..b038fbc25 100644 --- a/psydac/feec/tests/test_commuting_projections.py +++ b/psydac/feec/tests/test_commuting_projections.py @@ -3,7 +3,11 @@ import numpy as np import pytest -from psydac.feec.global_projectors import ProjectorH1, ProjectorL2, ProjectorHcurl, ProjectorHdiv +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv + from psydac.fem.tensor import TensorFemSpace, SplineSpace from psydac.fem.vector import VectorFemSpace from psydac.core.bsplines import make_knots @@ -57,13 +61,13 @@ def test_3d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = ProjectorH1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays grad = Gradient3D(H1, Hcurl) # create an instance of the projector class - P1 = ProjectorHcurl(Hcurl, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -153,8 +157,8 @@ def test_3d_commuting_pro_2(Nel, Nq, p, bc, m): curl = Curl3D(Hcurl, Hdiv) # create an instance of the projector class - P1 = ProjectorHcurl(Hcurl, Nq) - P2 = ProjectorHdiv(Hdiv, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives @@ -235,8 +239,8 @@ def test_3d_commuting_pro_3(Nel, Nq, p, bc, m): div = Divergence3D(Hdiv, L2) # create an instance of the projector class - P2 = ProjectorHdiv(Hdiv, Nq) - P3 = ProjectorL2(L2, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -307,13 +311,13 @@ def test_2d_commuting_pro_1(Nel, Nq, p, bc, m): Hcurl = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = ProjectorH1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays grad = Gradient2D(H1, Hcurl) # create an instance of the projector class - P1 = ProjectorHcurl(Hcurl, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -380,13 +384,13 @@ def test_2d_commuting_pro_2(Nel, Nq, p, bc, m): Hdiv = VectorFemSpace(*spaces) # create an instance of the H1 projector class - P0 = ProjectorH1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Linear operator: 2D vector curl curl = VectorCurl2D(H1, Hdiv) # create an instance of the projector class - P1 = ProjectorHdiv(Hdiv, Nq) + P1 = GlobalGeometricProjectorHdiv(Hdiv, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- @@ -464,8 +468,8 @@ def test_2d_commuting_pro_3(Nel, Nq, p, bc, m): div = Divergence2D(Hdiv, L2) # create an instance of the projector class - P2 = ProjectorHdiv(Hdiv, Nq) - P3 = ProjectorL2(L2, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -544,8 +548,8 @@ def test_2d_commuting_pro_4(Nel, Nq, p, bc, m): curl = ScalarCurl2D(Hcurl, L2) # create an instance of the projector class - P1 = ProjectorHcurl(Hcurl, Nq) - P2 = ProjectorL2(L2, Nq) + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives @@ -610,13 +614,13 @@ def test_1d_commuting_pro_1(Nel, Nq, p, bc, m): L2 = H1.reduce_degree(axes=[0], basis='M') # create an instance of the H1 projector class - P0 = ProjectorH1(H1) + P0 = GlobalGeometricProjectorH1(H1) # Build linear operators on stencil arrays grad = Derivative1D(H1, L2) # create an instance of the projector class - P1 = ProjectorL2(L2, Nq) + P1 = GlobalGeometricProjectorL2(L2, Nq) #------------------------------------- # Projections and discrete derivatives #------------------------------------- diff --git a/psydac/feec/tests/test_differentiation_matrices.py b/psydac/feec/tests/test_differentiation_matrices.py index 8a4b66df5..609799dd3 100644 --- a/psydac/feec/tests/test_differentiation_matrices.py +++ b/psydac/feec/tests/test_differentiation_matrices.py @@ -12,8 +12,6 @@ from psydac.feec.derivatives import ScalarCurl2D, VectorCurl2D, Curl3D from psydac.feec.derivatives import Divergence2D, Divergence3D -from psydac.feec.global_projectors import ProjectorH1 - from psydac.ddm.cart import DomainDecomposition from mpi4py import MPI diff --git a/psydac/feec/tests/test_global_projectors.py b/psydac/feec/tests/test_global_projectors.py index fadf2b058..c1389f17f 100644 --- a/psydac/feec/tests/test_global_projectors.py +++ b/psydac/feec/tests/test_global_projectors.py @@ -1,11 +1,13 @@ import numpy as np import pytest -from psydac.core.bsplines import make_knots -from psydac.fem.basic import FemField -from psydac.fem.splines import SplineSpace -from psydac.fem.tensor import TensorFemSpace -from psydac.feec.global_projectors import ProjectorH1, ProjectorL2 +from psydac.core.bsplines import make_knots +from psydac.fem.basic import FemField +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 + from psydac.ddm.cart import DomainDecomposition from sympde.topology import Square, Cube from psydac.api.discretization import discretize @@ -33,7 +35,7 @@ def test_H1_projector_1d(domain, ncells, degree, periodic, multiplicity): V0 = TensorFemSpace(domain_decomposition, N) # Projector onto H1 space (1D interpolation) - P0 = ProjectorH1(V0) + P0 = GlobalGeometricProjectorH1(V0) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) @@ -79,7 +81,7 @@ def test_L2_projector_1d(domain, ncells, degree, periodic, nquads, multiplicity) V1 = V0.reduce_degree(axes=[0], basis='M') # Projector onto L2 space (1D histopolation) - P1 = ProjectorL2(V1, nquads=[nquads]) + P1 = GlobalGeometricProjectorL2(V1, nquads=[nquads]) # Function to project f = lambda xi1 : np.sin( xi1 + 0.5 ) diff --git a/psydac/feec/tests/test_projections_parallel.py b/psydac/feec/tests/test_projections_parallel.py index 6ec757d18..a4cad1cdc 100644 --- a/psydac/feec/tests/test_projections_parallel.py +++ b/psydac/feec/tests/test_projections_parallel.py @@ -9,9 +9,13 @@ from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.vector import VectorFemSpace -from psydac.feec.global_projectors import ProjectorH1, ProjectorL2, ProjectorHcurl, ProjectorHdiv from psydac.ddm.cart import DomainDecomposition +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorH1 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorL2 +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHcurl +from psydac.feec.global_geometric_projectors import GlobalGeometricProjectorHdiv + def run_projection_comparison(domain, ncells, degree, periodic, funcs, reduce): @@ -19,45 +23,45 @@ def run_projection_comparison(domain, ncells, degree, periodic, funcs, reduce): if len(domain) == 1: if reduce == 0: opV = lambda V0: V0 - opP = ProjectorH1 + opP = GlobalGeometricProjectorH1 else: opV = lambda V0: V0.reduce_degree(axes=[0], basis='M') - opP = ProjectorL2 + opP = GlobalGeometricProjectorL2 elif len(domain) == 2: if reduce == 0: opV = lambda V0: V0 - opP = ProjectorH1 + opP = GlobalGeometricProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M')) - opP = ProjectorHcurl + opP = GlobalGeometricProjectorHcurl elif reduce == 2: # (note: this would be more instructive, if the index was 1 as well...) opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[0], basis='M')) - opP = ProjectorHdiv + opP = GlobalGeometricProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1], basis='M') - opP = ProjectorL2 + opP = GlobalGeometricProjectorL2 elif len(domain) == 3: if reduce == 0: opV = lambda V0: V0 - opP = ProjectorH1 + opP = GlobalGeometricProjectorH1 elif reduce == 1: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[0], basis='M'), V0.reduce_degree(axes=[1], basis='M'), V0.reduce_degree(axes=[2], basis='M')) - opP = ProjectorHcurl + opP = GlobalGeometricProjectorHcurl elif reduce == 2: opV = lambda V0: VectorFemSpace(V0.reduce_degree(axes=[1,2], basis='M'), V0.reduce_degree(axes=[0,2], basis='M'), V0.reduce_degree(axes=[0,1], basis='M')) - opP = ProjectorHdiv + opP = GlobalGeometricProjectorHdiv else: opV = lambda V0: V0.reduce_degree(axes=[0,1,2], basis='M') - opP = ProjectorL2 + opP = GlobalGeometricProjectorL2 # Choose number of quadrature points nquads = None if reduce == 0 else [d + 1 for d in degree] From cf17f6dacca64f2684c0330dcec00f76f67018e4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Thu, 18 Sep 2025 17:58:23 +0200 Subject: [PATCH 084/102] add changed files to auto documentation --- docs/source/modules/feec.multipatch.rst | 5 ----- docs/source/modules/feec.rst | 4 +++- docs/source/modules/linalg.rst | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/source/modules/feec.multipatch.rst b/docs/source/modules/feec.multipatch.rst index 64bf49a12..85bf08af1 100644 --- a/docs/source/modules/feec.multipatch.rst +++ b/docs/source/modules/feec.multipatch.rst @@ -7,11 +7,6 @@ feec.multipatch :toctree: STUBDIR :template: autosummary/module.rst - 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/docs/source/modules/feec.rst b/docs/source/modules/feec.rst index 5d424f396..f75dfb8cc 100644 --- a/docs/source/modules/feec.rst +++ b/docs/source/modules/feec.rst @@ -7,8 +7,10 @@ feec :toctree: STUBDIR :template: autosummary/module.rst + feec.conforming_projectors feec.derivatives - feec.global_projectors + feec.global_geometric_projectors + feec.hodge feec.pull_push feec.pushforward diff --git a/docs/source/modules/linalg.rst b/docs/source/modules/linalg.rst index 7ccbbf491..a625296ef 100644 --- a/docs/source/modules/linalg.rst +++ b/docs/source/modules/linalg.rst @@ -14,6 +14,7 @@ linalg linalg.kernels linalg.kron linalg.solvers + linalg.sparse linalg.stencil linalg.topetsc linalg.utilities From 06a1dcb7091c78943b3a69c34022fa710c06b60e Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:43:14 +0200 Subject: [PATCH 085/102] Update psydac/linalg/sparse.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/linalg/sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py index 3c88da686..9a2f26d9c 100644 --- a/psydac/linalg/sparse.py +++ b/psydac/linalg/sparse.py @@ -55,7 +55,7 @@ def transpose(self, conjugate=False): def dot(self, v, out=None): assert isinstance(v, Vector) - assert v.space == self.domain + assert v.space is self.domain if out is not None: assert isinstance(out, Vector) From 749ca76d5f116b74c020d2729f4f524b6fe98ebb Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:43:30 +0200 Subject: [PATCH 086/102] Update psydac/linalg/sparse.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/linalg/sparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py index 9a2f26d9c..db1a918f1 100644 --- a/psydac/linalg/sparse.py +++ b/psydac/linalg/sparse.py @@ -59,7 +59,7 @@ def dot(self, v, out=None): if out is not None: assert isinstance(out, Vector) - assert out.space == self.codomain + assert out.space is self.codomain out *= 0 else: out = self.codomain.zeros() From 4c3855551699a6b2c349c98ec5b72aee5b920980 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:43:55 +0200 Subject: [PATCH 087/102] Update psydac/feec/global_geometric_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/global_geometric_projectors.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/psydac/feec/global_geometric_projectors.py b/psydac/feec/global_geometric_projectors.py index 15cfd58c7..b08bf5622 100644 --- a/psydac/feec/global_geometric_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -725,12 +725,13 @@ class MultipatchGeometricProjector: Parameters ---------- - Vh : MultipatchFemSpace - Multipatch finite element space, codomain of the projection operator - P : GlobalGeometricProjector - Projector to be applied on each patch - nquads : list(int) | tuple(int) - Quadrature points for the GlobalGeometricProjector. + space : MultipatchFemSpace + Multipatch finite element space, codomain of the projection operator. + Projector : type[GlobalGeometricProjector] + Class of the projector to instantiate for each patch. + nquads : Iterable[int] + Number of quadrature points per cell along each direction. + This is a parameter passed to the constructor of Projector. """ def __init__(self, Vh, P, nquads=None): From be334b4a9e953e8db813431f423371c629aacee4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:44:17 +0200 Subject: [PATCH 088/102] Update psydac/feec/conforming_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/conforming_projectors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index d4b9a40dd..d2b601210 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -285,7 +285,6 @@ def get_extension_restriction(coarse_space_1d, fine_space_1d, p_moments=-1): R_1D = construct_restriction_operator_1D( coarse_space_1d_k_plus, fine_space_1d, E_1D, p_moments) - ER_1D = E_1D @ R_1D assert np.allclose(R_1D @ E_1D, np.eye(coarse_space_1d.nbasis), 1e-12, 1e-12) From a1dafc77bff757324d3a2d445d5a629e95ad97b7 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:45:50 +0200 Subject: [PATCH 089/102] Update psydac/feec/global_geometric_projectors.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/global_geometric_projectors.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/psydac/feec/global_geometric_projectors.py b/psydac/feec/global_geometric_projectors.py index b08bf5622..f32e65311 100644 --- a/psydac/feec/global_geometric_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -734,10 +734,13 @@ class MultipatchGeometricProjector: This is a parameter passed to the constructor of Projector. """ - def __init__(self, Vh, P, nquads=None): + def __init__(self, space, Projector, nquads=None): + assert isinstance(space, MultipatchFemSpace) + assert isinstance(Projector, type) + assert issubclass(Projector, GlobalGeometricProjector) - self._Vh = Vh - self._Ps = [P(V, nquads=nquads) for V in Vh.spaces] + self._Vh = Vh = space + self._Ps = [Projector(V, nquads=nquads) for V in Vh.spaces] def __call__(self, funs): """ From 51ce1ca8f086569d016b1e438a3b2aa3c5439b55 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:46:21 +0200 Subject: [PATCH 090/102] Update psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 027173a44..c3a1a7c35 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -139,8 +139,8 @@ def solve_h1_source_pbm( def lift_u_bc(u_bc): if u_bc is not None: du_bc = get_dual_dofs(Vh=V0h, f=u_bc, domain_h = domain_h, backend_language=backend_language) - u_bc = dH0.dot(du_bc) - u_bc = u_bc - cP0.dot(u_bc) + dH0.dot(du_bc, out=u_bc) + u_bc -= cP0.dot(u_bc) else: u_bc = None From 940da2a961aaf08362e025e8bb5c9a029eb30d55 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:47:00 +0200 Subject: [PATCH 091/102] Update psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3a1a7c35..b1329c2c9 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -152,7 +152,7 @@ def lift_u_bc(u_bc): if ubc is not None: # modified source for the homogeneous pbm print('modifying the source with lifted bc solution...') - df = df - pre_A.dot(ubc) + df -= pre_A @ ubc # direct solve with scipy spsolve print('solving source problem with conjugate gradient...') From 468c945ff9478aff40f8693f2c25789698fc1ca4 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:47:26 +0200 Subject: [PATCH 092/102] Update psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b1329c2c9..16b953a64 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -133,7 +133,7 @@ def solve_h1_source_pbm( 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,) df = get_dual_dofs(Vh=V0h, f=f_scal, domain_h=domain_h, backend_language=backend_language) - f = dH0.dot(df) + f = dH0 @ df df = cP0.T @ df def lift_u_bc(u_bc): From 980ebc0e2356692957532a977eeeb7915c5e1fc1 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:48:05 +0200 Subject: [PATCH 093/102] Update psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yaman Güçlü --- psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 16b953a64..af01c01ce 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -228,7 +228,7 @@ def lift_u_bc(u_bc): if u_ex: err = u - u_ex - rel_err = np.sqrt( err.inner(H0.dot(err))) / np.sqrt(u_ex.inner(H0.dot(u_ex))) + rel_err = np.sqrt(H0.dot_inner(err) / H0.dot_inner(u_ex)) return rel_err From 254897935a6a251548483187c2b36590cada6839 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 09:55:53 +0200 Subject: [PATCH 094/102] change signature of discretize_derham_multipatch --- psydac/api/discretization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 97096c7f0..bedec470e 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -194,7 +194,7 @@ def discretize_derham(derham, domain_h, *, get_H1vec_space=False, **kwargs): return DiscreteDeRham(domain_h, *spaces) #============================================================================== -def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): +def discretize_derham_multipatch(derham, domain_h, **kwargs): """ Create a discrete multipatch de Rham sequence from a symbolic one. @@ -226,7 +226,7 @@ def discretize_derham_multipatch(derham, domain_h, *args, **kwargs): ldim = derham.shape bases = ['B'] + ldim * ['M'] - spaces = [discretize_space(V, domain_h, *args, basis=basis, **kwargs) \ + spaces = [discretize_space(V, domain_h, basis=basis, **kwargs) \ for V, basis in zip(derham.spaces, bases)] return DiscreteDeRhamMultipatch( From 9190db6970b1b4c9295ad110fc0c4013f670ba5e Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 10:00:15 +0200 Subject: [PATCH 095/102] update docs in conforming projections --- psydac/feec/conforming_projectors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py index d2b601210..d045a702e 100644 --- a/psydac/feec/conforming_projectors.py +++ b/psydac/feec/conforming_projectors.py @@ -529,7 +529,7 @@ def construct_h1_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc=Fa Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) @@ -983,7 +983,7 @@ def construct_hcurl_conforming_projection(Vh, reg_orders=0, p_moments=-1, hom_bc Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) @@ -1166,7 +1166,7 @@ def construct_h1_singlepatch_conforming_projection(Vh, reg_orders=0, p_moments=- Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) @@ -1342,7 +1342,7 @@ def construct_hcurl_singlepatch_conforming_projection(Vh, reg_orders=0, p_moment Parameters ---------- - Vh : TensorFemSpace + Vh : MultipatchFemSpace Finite Element Space coming from the discrete de Rham sequence. reg_orders : (int) From 8e89187ef53e635eda4f14ac64f8221e02d594e0 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 10:11:23 +0200 Subject: [PATCH 096/102] revert changes in MatrixFreeLinearOperator and fix another typo --- psydac/linalg/basic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index 46fce1b78..ba8ca3663 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -1318,7 +1318,6 @@ def dot(self, v, out=None, **kwargs): if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain - out *= 0 else: out = self.codomain.zeros() @@ -1326,7 +1325,7 @@ def dot(self, v, out=None, **kwargs): self._dot(v, out=out, **kwargs) else: # provided dot product does not take an out argument: we simply copy the result into out - self._dot(v).copy(out=out, **kwargs) + self._dot(v, **kwargs).copy(out=out) return out From 86e0f0859023d0758ea7d8670ab93d3bd8a90611 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 10:28:49 +0200 Subject: [PATCH 097/102] add import for assert --- psydac/feec/global_geometric_projectors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/feec/global_geometric_projectors.py b/psydac/feec/global_geometric_projectors.py index f32e65311..019c2f441 100644 --- a/psydac/feec/global_geometric_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -10,7 +10,7 @@ from psydac.fem.basic import FemField from psydac.fem.tensor import TensorFemSpace -from psydac.fem.vector import VectorFemSpace +from psydac.fem.vector import VectorFemSpace, MultipatchFemSpace from psydac.ddm.cart import DomainDecomposition, CartDecomposition from psydac.utilities.utils import roll_edges From f3465f42c5bbc9ddf29fb1291685c4a8fc532d6c Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 10:31:14 +0200 Subject: [PATCH 098/102] add doc string to broken derivatives --- psydac/feec/derivatives.py | 50 +++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index 07706d601..1fd3de5d2 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -669,7 +669,19 @@ def __init__(self, Hdiv, L2): # 2D Multipatch derivative operators #==================================================================================================== class BrokenGradient2D(FemLinearOperator): + """ + Gradient operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + Parameters + ---------- + V0h : MultipatchFemSpace + Domain of the gradient operator. + + V1h : MultipatchFemSpace + Codomain of the gradient operator. + """ def __init__(self, V0h, V1h): FemLinearOperator.__init__(self, fem_domain=V0h, fem_codomain=V1h) @@ -685,7 +697,19 @@ def transpose(self, conjugate=False): # ============================================================================== class BrokenTransposedGradient2D(FemLinearOperator): - + """ + Transposed gradient operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + + Parameters + ---------- + V0h : MultipatchFemSpace + Codomain of the transposed gradient operator. + + V1h : MultipatchFemSpace + Domain of the transposed gradient operator. + """ def __init__(self, V0h, V1h): FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V0h) @@ -701,7 +725,19 @@ def transpose(self, conjugate=False): # ============================================================================== class BrokenScalarCurl2D(FemLinearOperator): + """ + Scalar curl operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + Parameters + ---------- + V1h : MultipatchFemSpace + Domain of the scalar curl operator. + + V2h : MultipatchFemSpace + Codomain of the scalar curl operator. + """ def __init__(self, V1h, V2h): FemLinearOperator.__init__(self, fem_domain=V1h, fem_codomain=V2h) @@ -718,7 +754,19 @@ def transpose(self, conjugate=False): # ============================================================================== class BrokenTransposedScalarCurl2D(FemLinearOperator): + """ + Transposed scalar curl operator in a 2D multipatch domain, + acting independently on each patch. + In general, the resulting field is therefore discontinuous, or "broken". + Parameters + ---------- + V1h : MultipatchFemSpace + Codomain of the transposed scalar curl operator. + + V2h : MultipatchFemSpace + Domain of the transposed scalar curl operator. + """ def __init__(self, V1h, V2h): FemLinearOperator.__init__(self, fem_domain=V2h, fem_codomain=V1h) From a30b8b21862995bb9980a1893c8790f80e17fe37 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 10:45:11 +0200 Subject: [PATCH 099/102] add docstring to SparseMatrixLinearOperator --- psydac/linalg/sparse.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py index db1a918f1..88367a390 100644 --- a/psydac/linalg/sparse.py +++ b/psydac/linalg/sparse.py @@ -13,7 +13,18 @@ class SparseMatrixLinearOperator(LinearOperator): """ - Wrap a sparse matrix into a LinearOperator. + LinearOperator representation of a sparse matrix. + + Parameters + ---------- + domain : VectorSpace + The domain of the operator. + + codomain : VectorSpace + The codomain of the operator. + + sparse_matrix : scipy.sparse.csr_matrix + The sparse matrix representing the operator. """ def __init__(self, domain, codomain, sparse_matrix): From 695142fb18e9b6a57a982e62a68e775bb2c26f77 Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 11:40:52 +0200 Subject: [PATCH 100/102] revert some changes and fix a missing argument --- .../multipatch/examples/h1_source_pbms_conga_2d.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 af01c01ce..aaebd3140 100644 --- a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -139,13 +139,13 @@ def solve_h1_source_pbm( def lift_u_bc(u_bc): if u_bc is not None: du_bc = get_dual_dofs(Vh=V0h, f=u_bc, domain_h = domain_h, backend_language=backend_language) - dH0.dot(du_bc, out=u_bc) - u_bc -= cP0.dot(u_bc) + ubc = dH0.dot(du_bc) + ubc -= cP0.dot(ubc) else: - u_bc = None + ubc = None - return u_bc + return ubc ubc = lift_u_bc(u_bc) @@ -228,7 +228,7 @@ def lift_u_bc(u_bc): if u_ex: err = u - u_ex - rel_err = np.sqrt(H0.dot_inner(err) / H0.dot_inner(u_ex)) + rel_err = np.sqrt(H0.dot_inner(err, err) / H0.dot_inner(u_ex, u_ex)) return rel_err From 9c5baa9492b785e0cd1326924743652c650598ff Mon Sep 17 00:00:00 2001 From: Frederik Schnack Date: Sat, 20 Sep 2025 11:53:49 +0200 Subject: [PATCH 101/102] use dot_inner --- .../feec/multipatch/examples/hcurl_source_pbms_conga_2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 6e132b75e..e2252a0e2 100644 --- a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -207,7 +207,7 @@ def solve_hcurl_source_pbm( def lift_u_bc(u_bc): if u_bc is not None: ubc = P1_phys(u_bc, P1, domain).coeffs - ubc = ubc - cP1.dot(ubc) + ubc -= cP1.dot(ubc) else: ubc = None @@ -221,7 +221,7 @@ def lift_u_bc(u_bc): # modified source for the homogeneous pbm t_stamp = time_count(t_stamp) print(' .. modifying the source with lifted bc solution...') - tilde_f = tilde_f - pre_A.dot(ubc) + tilde_f -= pre_A.dot(ubc) # direct solve with scipy spsolve t_stamp = time_count(t_stamp) @@ -289,7 +289,7 @@ def lift_u_bc(u_bc): err = u_ex_p - u print(err.inner(H1.dot(err))) - l2_error = np.sqrt( err.inner(H1.dot(err))) / np.sqrt(u_ex_p.inner(H1.dot(u_ex_p))) + l2_error = np.sqrt( H1.dot_inner(err, err) / H1.dot_inner(u_ex_p, u_ex_p)) print(l2_error) diags['err'] = l2_error From 7812d35f8b783e7fb1288c43de5784a40759ff25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 22 Sep 2025 17:44:32 +0200 Subject: [PATCH 102/102] Apply suggestions from code review --- psydac/linalg/sparse.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py index 88367a390..71fa29fed 100644 --- a/psydac/linalg/sparse.py +++ b/psydac/linalg/sparse.py @@ -1,6 +1,7 @@ # coding: utf-8 -from scipy.sparse import csr_matrix +from scipy.sparse import sparray, csr_array, bsr_array +from scipy.sparse import spmatrix, csr_matrix, bsr_matrix from psydac.linalg.basic import LinearOperator from psydac.linalg.basic import VectorSpace, Vector, LinearOperator @@ -23,15 +24,21 @@ class SparseMatrixLinearOperator(LinearOperator): codomain : VectorSpace The codomain of the operator. - sparse_matrix : scipy.sparse.csr_matrix - The sparse matrix representing the operator. + sparse_matrix : scipy.sparse.sparray | scipy.sparse.spmatrix + The sparse SciPy matrix representing the operator. Recommended formats are + CSR and BSR. Any other format will be converted to CSR (csr_array). """ def __init__(self, domain, codomain, sparse_matrix): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) - assert isinstance(sparse_matrix, csr_matrix) + assert isinstance(sparse_matrix, (sparray, spmatrix)) + + if not isinstance(sparse_matrix, + (csr_array, csr_matrix, + bsr_array, bsr_matrix)): + sparse_matrix = sparse_matrix.tocsr() if domain.parallel: raise NotImplementedError('Parallel SparseMatrixLinearOperator not supported yet.')