diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 000000000..3ce4bb540 --- /dev/null +++ b/docs/output.md @@ -0,0 +1,167 @@ +# Psydac's outputs +## Structure +Psydac has a class meant to take care of outputing simulation results. This class, named `OuputManager` is located in `psydac/api/postprocessing.py`. +It writes `FemSpace` related information in the Yaml syntax. The file looks like this: +```yaml +ndim: 2 +fields: file.h5 # Name of the fields file +patches: +- name: patch_0 + breakpoints: + - [0.0, 0.25, 0.5, 0.75, 1.0] + - [0.0, 0.25, 0.5, 0.75, 1.0] + scalar_spaces: + - name: Scalar_space + ldim: 2 + kind: l2 + dtype: + rational: false + periodic: [false, false] + degree: [1, 1] + multiplicity: [1, 1] + basis: [B, B] + knots: + - [0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0] + - [0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0] + - &id001 + name: Vector_space[0] + ldim: 2 + kind: undefined + dtype: + rational: false + periodic: [false, false] + degree: [1, 2] + multiplicity: [1, 1] + basis: [B, B] + knots: + - [0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0] + - [0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0] + - &id002 + name: Vector_space[1] + ldim: 2 + kind: undefined + dtype: + rational: false + periodic: [false, false] + degree: [2, 1] + multiplicity: [1, 1] + basis: [B, B] + knots: + - [0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0] + - [0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0] + vector_spaces: + - name: Vector_space + kind: hcurl + components: + - *id001 + - *id002 +- name: patch_1 + breakpoints: + - [1.0, 1.25, 1.5, 1.75, 2.0] + - [0.0, 0.25, 0.5, 0.75, 1.0] + scalar_spaces: + - name: Scalar_space + ldim: 2 + kind: l2 + dtype: + rational: false + periodic: [false, false] + degree: [1, 1] + multiplicity: [1, 1] + basis: [B, B] + knots: + - [1.0, 1.0, 1.25, 1.5, 1.75, 2.0, 2.0] + - [0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0] + +``` +The field coefficients are saved to the `HDF5` format in the following manner : +```bash +file.h5 + attribute: spaces # name of the aforementioned Yaml file + static/ + scalar_space_1/ + field_s1_1 + field_s1_2 + .... + field_s1_n + vector_space_1_[0]/ + attribute: parent_space # 'vector_space_1' + field_v1_1_[0] + attribute: parent_field # 'field_v1_1' + vector_space_1_[1]/ + attribute: parent_space # 'vector_space_1' + field_v1_1_[1] + attribute: parent_field # 'field_v1_1' + ... + snapshot_1/ + attribute: t + attribute: ts + space_1/ + ... + space_n/ + ... + snapshot_n/ +``` +In addition to that, Psydac also features the `PostProcessManager` class to read those files, recreate all the `FemSpace` and `FemField` objects and export them to `VTK`. + +## Usage of class `OutputManager` + +An instance of the `OutputManager` class is created at the beginning of the simulation, by specifying the following: + +1. The name of the YAML file (e.g. `spaces.yaml`) where the information about all FEM spaces will be written, and +2. The name of the HDF5 file (e.g. `fields.h5`) where the coefficients of all FEM fields will be written. + +References to the available FEM spaces are given to the OutputManager object through the `add_spaces(**kwargs)` method, and the corresponding YAML file is created upon calling the method `export_space_info()`. In order to inform the OutputManager object that the next fields to be exported are time-independent, the user should call the `set_static()` method. In the case of time-dependent fields, the user should prepare a time snapshot (which is defined for a specific integer time step `ts` and time value `t`) by calling the method `add_snapshot(t, ts)`. In both cases the fields are exported to the HDF5 file through a call to the method `export_fields(**kwargs)`. Here is a usage example: + +```python +# SymPDE Layer +# Discretization +# V0h and V1h are discretized SymPDE Space +# u0 and u1 are FemFields belonging to either of those spaces +output_m = OutputManager('spaces.yml', 'fields.h5') + +output_m.add_spaces(V0=V0h, V1=V1h) +output_m.export_space_info() # Writes the space information to Yaml + +output_m.set_static() # Tells the object to save in /static/ +output_m.export_fields(u0_static=u0, u1_static=u1) # Actually does the saving + +output_m.add_snapshot(t=0., ts=0) +# The line above tells the object to: +# 1. create the group snapshot_x with attribute t and ts +# 2. save in this snapshot +output_m.export_fields(u0=u0, u1=u1) +``` + +## Usage of class `PostProcessManager` + +Typically the `PostProcessManager` class is used in a separate post-processing script, which is run after the simulation has finished. In essence it evaluates the FEM fields over a uniform grid (applying the appropriate push-forward operations) and exports the values to a VTK file (or a sequence of files in the case of a time series). An instance of the `PostProcessManager` class is created by specifying the following: + +1. The name of the geometry file (in HDF5 format) which defines the geometry or the topological domain from which the geometry is derived. +2. The name of the YAML file that contains the information about the FEM spaces +3. The name of the HDF5 file that contains the coefficients of all the FEM fields + +In order to export the fields to a VTK file, the user needs to call the method `export_to_vtk(base_name, grid, npts_per_cell, snapshots, fields)`, where: +1. `base_name` is the base name for the VTK output files. +2. `grid` is either a user specified evaluation grid or `None`. +3. `npts_per_cell` specifies the refinement in the case of a uniform grid. +4. `snapshots` specifies which time snapshots should be extracted from the HDF5 file (`none` in the case of static fields) +5. `fields` is a tuple of `h5_field_name`. + +Here is a usage example: + +```python +# geometry.h5 is where the domain comes from. See PostProcessManager's docstring for more information +post = PostProcessManager(geometry_file='geometry.h5', space_file='spaces.yml', fields_file='fields.h5') + +# See PostProcessManager.export_to_vtk's and TensorFemSpace.eval_fields' docstrings for more information +post.export_to_vtk('filename_vtk', grid=grid, npts_per_cell=npts_per_cell, snapshots='all', fields = ('u0', 'u1')) +``` + +## Further Examples +Further examples are present in the following files: + +* `examples/poisson_3d_target_torus.py` +* `examples/sample_multipatch_parallel.py` +* `examples/notebooks/Poisson_non_periodic.ipynb` +* `psydac/api/tests/test_postprocessing.py` \ No newline at end of file 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 diff --git a/examples/poisson_2d_mapping.py b/examples/poisson_2d_mapping.py new file mode 100644 index 000000000..79e84b843 --- /dev/null +++ b/examples/poisson_2d_mapping.py @@ -0,0 +1,961 @@ +# coding: utf-8 +from time import time, sleep + +from mpi4py import MPI +import numpy as np +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable + +from sympde.topology.callable_mapping import CallableMapping +from sympde.topology.analytical_mapping import IdentityMapping, PolarMapping +from sympde.topology.analytical_mapping import TargetMapping, CzarnyMapping + +from psydac.ddm.cart import DomainDecomposition +from psydac.linalg.stencil import StencilVector, StencilMatrix +from psydac.linalg.solvers import inverse +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.fem.basic import FemField +from psydac.mapping.discrete import SplineMapping +from psydac.utilities.utils import refine_array_1d +from psydac.cad.geometry import Geometry +from psydac.ddm.cart import DomainDecomposition +from psydac.polar.c1_projections import C1Projector + +#============================================================================== +class Laplacian: + + def __init__(self, mapping): + + assert isinstance(mapping, CallableMapping) + + sym = mapping.symbolic_mapping + + self._eta = sym.logical_coordinates + self._metric = sym.metric_expr + self._metric_det = sym.metric_det_expr + + # ... + def __call__(self, phi): + + from sympy import sqrt, Matrix + + u = self._eta + G = self._metric + sqrt_g = sqrt(self._metric_det) + + # Store column vector of partial derivatives of phi w.r.t. uj + dphi_du = Matrix([phi.diff(uj) for uj in u]) + + # Compute gradient of phi in tangent basis: A = G^(-1) dphi_du + A = G.LUsolve(dphi_du) + + # Compute Laplacian of phi using formula for divergence of vector A + lapl = sum((sqrt_g * Ai).diff(ui) for ui, Ai in zip(u, A)) / sqrt_g + + return lapl + +#============================================================================== +class Poisson2D: + r""" + Exact solution to the 2D Poisson equation with Dirichlet boundary + conditions, to be employed for the method of manufactured solutions. + + :code + $(\partial^2_{xx} + \partial^2_{yy}) \phi(x,y) = -\rho(x,y)$ + + """ + def __init__(self, domain, periodic, mapping, phi, rho, O_point=False): + + self._domain = domain + self._periodic = periodic + self._mapping = mapping + self._phi = phi + self._rho = rho + self._O_point = O_point + + # ... + @staticmethod + def new_square(mx=1, my=1): + r""" + Solve Poisson's equation on the unit square. + + : code + $\phi(x,y) = sin( mx*pi*x ) + sin( my*pi*y )$ + + with $mx$ and $my$ user-defined integer numbers. + + """ + domain = ((0,1), (0,1)) + periodic = (False, False) + mapping = IdentityMapping('F', dim=2).get_callable_mapping() + + from sympy import symbols, sin, cos, pi, lambdify + x,y = symbols('x y') + phi_e = sin(mx * pi * x) * sin(my * pi * y) + rho_e = -phi_e.diff(x, 2) - phi_e.diff(y, 2) + + phi = lambdify([x, y], phi_e) + rho = lambdify([x, y], rho_e) + + return Poisson2D(domain, periodic, mapping, phi, rho) + + # ... + @staticmethod + def new_annulus(rmin=0.5, rmax=1.0): + r""" + Solve Poisson's equation on an annulus centered at (x,y)=(0,0), + with logical coordinates (r,theta): + + - The radial coordinate r belongs to the interval [rmin,rmax]; + - The angular coordinate theta belongs to the interval [0,2*pi). + + : code + $\phi(x,y) = 4(r-rmin)(rmax-r)/(rmax-rmin)^2 \sin(2\pi x) \sin(2\pi y)$. + + """ + domain = ((0, 1), (0, 2*np.pi)) + periodic = (False, True) + mapping = PolarMapping('F', c1=0, c2=0, rmin=rmin, rmax=rmax).get_callable_mapping() + + from sympy import symbols, sin, cos, pi, lambdify + + lapl = Laplacian(mapping) + r, t = mapping.symbolic_mapping.logical_coordinates + x, y = mapping.symbolic_mapping.expressions + + # Manufactured solutions in logical coordinates + parab = (r-rmin) * (rmax-r) * 4 / (rmax-rmin)**2 + phi_e = parab * sin(2*pi*x) * sin(2*pi*y) + rho_e = -lapl(phi_e) + + # For further simplifications, assume that (r,t) are positive and real + R,T = symbols('R T', real=True, positive=True) + phi_e = phi_e.subs({r:R, t:T}).simplify() + rho_e = rho_e.subs({r:R, t:T}).simplify() + + # Callable functions + phi = lambdify([R, T], phi_e) + rho = lambdify([R, T], rho_e) + + return Poisson2D( domain, periodic, mapping, phi, rho, O_point=(rmin==0) ) + + # ... + @staticmethod + def new_circle(): + r""" + Solve Poisson's equation on a unit circle centered at (x,y)=(0,0), + with logical coordinates (r,theta): + + - The radial coordinate r belongs to the interval [0,1]; + - The angular coordinate theta belongs to the interval [0,2*pi). + + : code + $\phi(x,y) = 1-r**2$. + + """ + domain = ((0, 1), (0, 2*np.pi)) + periodic = (False, True) + mapping = PolarMapping('F', c1=0, c2=0, rmin=0, rmax=2*np.pi).get_callable_mapping() + + from sympy import lambdify + + lapl = Laplacian(mapping) + r, t = mapping.symbolic_mapping.logical_coordinates + + # Manufactured solutions in logical coordinates + phi_e = 1-r**2 + rho_e = -lapl(phi_e) + + # Callable functions + phi = lambdify([r, t], phi_e) + rho = lambdify([r, t], rho_e) + + rho = np.vectorize(rho) + + return Poisson2D(domain, periodic, mapping, phi, rho, O_point=True) + + # ... + @staticmethod + def new_target(): + + domain = ((0, 1), (0, 2*np.pi)) + periodic = (False, True) + params = dict(c1=0, c2=0, k=0.3, D=0.2) + mapping = TargetMapping('F', **params).get_callable_mapping() + + from sympy import symbols, sin, cos, pi, lambdify + + lapl = Laplacian(mapping) + s, t = mapping.symbolic_mapping.logical_coordinates + x, y = mapping.symbolic_mapping.expressions + + # Manufactured solution in logical coordinates + k = params['k'] + D = params['D'] + kx = 2*pi/(1-k+D) + ky = 2*pi/(1+k) + phi_e = (1-s**8) * sin(kx*(x-0.5)) * cos(ky*y) + rho_e = -lapl(phi_e) + + # Callable functions + phi = lambdify([s, t], phi_e) + rho = lambdify([s, t], rho_e) + + return Poisson2D( domain, periodic, mapping, phi, rho, O_point=True ) + + # ... + @staticmethod + def new_czarny(): + + domain = ((0, 1), (0, 2*np.pi)) + periodic = (False, True) + params = dict(c1=0, c2=0, eps=0.2, b=1.4) + mapping = CzarnyMapping('F', **params).get_callable_mapping() + + from sympy import symbols, sin, cos, pi, lambdify + + lapl = Laplacian(mapping) + s, t = mapping.symbolic_mapping.logical_coordinates + x, y = mapping.symbolic_mapping.expressions + + # Manufactured solution in logical coordinates + phi_e = (1-s**8) * sin(pi*x) * cos(pi*y) + rho_e = -lapl(phi_e) + + # Callable functions + phi = lambdify([s, t], phi_e) + rho = lambdify([s, t], rho_e) + + return Poisson2D(domain, periodic, mapping, phi, rho, O_point=True) + + # ... + @property + def domain(self): + return self._domain + + @property + def periodic(self): + return self._periodic + + @property + def mapping(self): + return self._mapping + + @property + def phi(self): + return self._phi + + @property + def rho(self): + return self._rho + + @property + def O_point(self): + return self._O_point + +#============================================================================== +def mpi_print(string, *args, comm=None, **kwargs): + if comm is not None: + assert isinstance(comm, MPI.Comm) + if comm.rank == 0: + kwargs['flush'] = True + print(string, *args, **kwargs) + comm.Barrier() + else: + print(string, *args, **kwargs) + +#============================================================================== +def kernel(p1, p2, nq1, nq2, bs1, bs2, w1, w2, jac_mat, mat_m, mat_s): + """ + Kernel for computing the mass/stiffness element matrices. + + Parameters + ---------- + p1 : int + Spline degree along x1 direction. + + p2 : int + Spline degree along x2 direction. + + nq1 : int + Number of quadrature points along x1 (same in each element). + + nq2 : int + Number of quadrature points along x2 (same in each element). + + bs1 : 3D array_like (p1+1, 1+nderiv, nq1) + Values (and derivatives) of non-zero basis functions along x1 + at each quadrature point. + + bs2 : 3D array_like (p2+1, 1+nderiv, nq2) + Values (and derivatives) of non-zero basis functions along x2 + at each quadrature point. + + w1 : 1D array_like (nq1,) + Quadrature weights at each quadrature point. + + w2 : 1D array_like (nq2,) + Quadrature weights at each quadrature point. + + jac_mat : 4D array_like (nq1, nq2, 2, 2) + Jacobian matrix of the mapping F(x1,x2)=(x,y) at each quadrature point. + + mat_m : 4D array_like (p1+1, p2+1, 2*p1+1, 2*p2+1) + Element mass matrix (in/out argument). + + mat_s : 4D array_like (p1+1, p2+1, 2*p1+1, 2*p2+1) + Element stiffness matrix (in/out argument). + + """ + # Reset element matrices + mat_m[:, :, :, :] = 0. + mat_s[:, :, :, :] = 0. + + # Cycle over non-zero test functions in element + for il1 in range(p1+1): + for il2 in range(p2+1): + + # Cycle over non-zero trial functions in element + for jl1 in range(p1+1): + for jl2 in range(p2+1): + + # Reset integrals over element + v_m = 0.0 + v_s = 0.0 + + # Cycle over quadrature points + for q1 in range(nq1): + for q2 in range(nq2): + + # Get test function's value and derivatives + bi_0 = bs1[il1, 0, q1] * bs2[il2, 0, q2] + bi_x1 = bs1[il1, 1, q1] * bs2[il2, 0, q2] + bi_x2 = bs1[il1, 0, q1] * bs2[il2, 1, q2] + + # Get trial function's value and derivatives + bj_0 = bs1[jl1, 0, q1] * bs2[jl2, 0, q2] + bj_x1 = bs1[jl1, 1, q1] * bs2[jl2, 0, q2] + bj_x2 = bs1[jl1, 0, q1] * bs2[jl2, 1, q2] + + # Mapping: + # - from logical coordinates (x1,x2) + # - to Cartesian coordinates (x,y) + [[x_x1, x_x2], + [y_x1, y_x2]] = jac_mat[q1,q2,:,:] + + jac_det = x_x1*y_x2 - x_x2*y_x1 + inv_jac_det = 1./jac_det + + # Convert basis functions' derivatives: + # - from logical coordinates (x1,x2) + # - to Cartesian coordinates (x,y) + bi_x = inv_jac_det * ( y_x2*bi_x1 - y_x1*bi_x2) + bi_y = inv_jac_det * (-x_x2*bi_x1 + x_x1*bi_x2) + + bj_x = inv_jac_det * ( y_x2*bj_x1 - y_x1*bj_x2) + bj_y = inv_jac_det * (-x_x2*bj_x1 + x_x1*bj_x2) + + # Get volume associated to quadrature point + wvol = w1[q1] * w2[q2] * abs(jac_det) + + # Add contribution to integrals + v_m += bi_0 * bj_0 * wvol + v_s += (bi_x * bj_x + bi_y * bj_y) * wvol + + # Update element matrices + mat_m[il1, il2, p1+jl1-il1, p2+jl2-il2] = v_m + mat_s[il1, il2, p1+jl1-il1, p2+jl2-il2] = v_s + +#============================================================================== +def assemble_matrices(V, mapping, kernel, *, nquads): + """ + Assemble mass and stiffness matrices using 2D stencil format. + + Parameters + ---------- + V : TensorFemSpace + Finite element space where the Galerkin method is applied. + + mapping : psydac.mapping.basic.Mapping + Mapping (analytical or discrete) from logical to physical coordinates. + + kernel : callable + Function that performs the assembly process on small element matrices. + + nquads : list or tuple of int + Number of quadrature points in each direction (here two). + + Returns + ------- + mass : StencilMatrix + Mass matrix in 2D stencil format. + + stiffness : StencilMatrix + Stiffness matrix in 2D stencil format. + + """ + # Sizes + [s1, s2] = V.coeff_space.starts + [e1, e2] = V.coeff_space.ends + [p1, p2] = V.coeff_space.pads + + # Quadrature data + quad_grids = V.get_assembly_grids(*nquads) + [ nk1, nk2] = [g.num_elements for g in quad_grids] + [ nq1, nq2] = [g.num_quad_pts for g in quad_grids] + [ spans_1, spans_2] = [g.spans for g in quad_grids] + [ basis_1, basis_2] = [g.basis for g in quad_grids] + [ points_1, points_2] = [g.points for g in quad_grids] + [weights_1, weights_2] = [g.weights for g in quad_grids] + + # Create global matrices + mass = StencilMatrix(V.coeff_space, V.coeff_space) + stiffness = StencilMatrix(V.coeff_space, V.coeff_space) + + # Create element matrices + mat_m = np.zeros((p1+1, p2+1, 2*p1+1, 2*p2+1)) # mass + mat_s = np.zeros((p1+1, p2+1, 2*p1+1, 2*p2+1)) # stiffness + + # Build global matrices: cycle over elements + for k1 in range(nk1): + for k2 in range(nk2): + + # Get spline index, B-splines' values and quadrature weights + is1 = spans_1[k1] + bs1 = basis_1[k1, :, :, :] + w1 = weights_1[k1, :] + + is2 = spans_2[k2] + bs2 = basis_2[k2, :, :, :] + w2 = weights_2[k2, :] + + # Compute Jacobian matrix at all quadrature points + jac_mat = np.empty((nq1, nq2, 2, 2)) + for q1 in range(nq1): + for q2 in range(nq2): + x1 = points_1[k1, q1] + x2 = points_2[k2, q2] + jac_mat[q1, q2, :, :] = mapping.jacobian(x1, x2) + + # Compute element matrices + kernel(p1, p2, nq1, nq2, bs1, bs2, w1, w2, jac_mat, mat_m, mat_s) + + # Update global matrices + mass [is1-p1:is1+1, is2-p2:is2+1, :, :] += mat_m[:, :, :, :] + stiffness[is1-p1:is1+1, is2-p2:is2+1, :, :] += mat_s[:, :, :, :] + + # IMPORTANT: new assembly procedure requires dedicated data exchange + mass .exchange_assembly_data() + stiffness.exchange_assembly_data() + + # Make sure that periodic corners are zero in non-periodic case + mass .remove_spurious_entries() + stiffness.remove_spurious_entries() + + return mass, stiffness + +#============================================================================== +def assemble_rhs(V, mapping, f, *, nquads): + """ + Assemble right-hand-side vector. + + Parameters + ---------- + V : TensorFemSpace + Finite element space where the Galerkin method is applied. + + mapping : psydac.mapping.basic.Mapping + Mapping (analytical or discrete) from logical to physical coordinates. + + f : callable + Right-hand side function rho(x,y) (charge density). + + Returns + ------- + rhs : StencilVector + Vector b of coefficients, in linear system Ax=b. + + """ + # Sizes + [s1, s2] = V.coeff_space.starts + [e1, e2] = V.coeff_space.ends + [p1, p2] = V.coeff_space.pads + + # Quadrature data + quad_grids = V.get_assembly_grids(*nquads) + [ nk1, nk2] = [g.num_elements for g in quad_grids] + [ nq1, nq2] = [g.num_quad_pts for g in quad_grids] + [ spans_1, spans_2] = [g.spans for g in quad_grids] + [ basis_1, basis_2] = [g.basis for g in quad_grids] + [ points_1, points_2] = [g.points for g in quad_grids] + [weights_1, weights_2] = [g.weights for g in quad_grids] + + # Data structure + rhs = StencilVector(V.coeff_space) + + # Build RHS + for k1 in range(nk1): + for k2 in range(nk2): + + # Get spline index, B-splines' values and quadrature weights + is1 = spans_1[k1] + bs1 = basis_1[k1, :, :, :] + w1 = weights_1[k1, :] + x1 = points_1[k1, :] + + is2 = spans_2[k2] + bs2 = basis_2[k2, :, :, :] + w2 = weights_2[k2, :] + x2 = points_2[k2, :] + + # Evaluate function at all quadrature points + f_quad = f(*np.meshgrid(x1, x2, indexing='ij')) + + # Compute Jacobian determinant at all quadrature points + metric_det = np.empty((nq1, nq2)) + for q1 in range(nq1): + for q2 in range(nq2): + metric_det[q1, q2] = mapping.metric_det(x1[q1], x2[q2]) + jac_det = np.sqrt(metric_det) + + for il1 in range(p1+1): + for il2 in range(p2+1): + + v = 0.0 + for q1 in range(nq1): + for q2 in range(nq2): + bi_0 = bs1[il1, 0, q1] * bs2[il2, 0, q2] + wvol = w1[q1] * w2[q2] * jac_det[q1, q2] + v += bi_0 * f_quad[q1, q2] * wvol + + # Global index of test basis + i1 = is1 - p1 + il1 + i2 = is2 - p2 + il2 + + # Update one element of the rhs vector + rhs[i1, i2] += v + + # IMPORTANT: new assembly procedure requires dedicated data exchange + rhs.exchange_assembly_data() + + # IMPORTANT: ghost regions must be up-to-date + rhs.update_ghost_regions() + + return rhs + +#################################################################################### + +def main(*, test_case, ncells, degree, nquads, + use_spline_mapping, c1_correction, distribute_viz): + + timing = {} + timing['assembly' ] = 0.0 + timing['projection' ] = 0.0 + timing['solution' ] = 0.0 + timing['diagnostics'] = 0.0 + timing['export' ] = 0.0 + + # Method of manufactured solution + if test_case == 'square': + model = Poisson2D.new_square(mx=1, my=1) + elif test_case == 'annulus': + model = Poisson2D.new_annulus(rmin=0.1, rmax=1.0) + elif test_case == 'circle': + model = Poisson2D.new_circle() + elif test_case == 'target': + model = Poisson2D.new_target() + elif test_case == 'czarny': + model = Poisson2D.new_czarny() + else: + raise ValueError("Only available test-cases are 'square', 'annulus', " + "'circle', 'target' and 'czarny'") + + # Communicator, size, rank + mpi_comm = MPI.COMM_WORLD + mpi_size = mpi_comm.Get_size() + mpi_rank = mpi_comm.Get_rank() + + # If not explicitly provided, set number quadrature points to default value + if nquads is None: + nquads = [d + 1 for d in degree] + mpi_print(f'NOTE: Setting number of quadrature points to {nquads}', comm=mpi_comm) + + if c1_correction and (not model.O_point): + mpi_print("WARNING: cannot use C1 correction in geometry without polar singularity!\n" + "WARNING: setting 'c1_correction' flag to False...\n", + comm = mpi_comm) + c1_correction = False + + if c1_correction and (not use_spline_mapping): + mpi_print("WARNING: cannot use C1 correction without spline mapping!\n" + "WARNING: setting 'c1_correction' flag to False...\n", + comm = mpi_comm) + c1_correction = False + + # Number of elements and spline degree + ne1, ne2 = ncells + p1 , p2 = degree + + # Is solution periodic? + per1, per2 = model.periodic + + # Create uniform grid + grid_1 = np.linspace(*model.domain[0], num=ne1+1) + grid_2 = np.linspace(*model.domain[1], num=ne2+1) + + # Decompose 2D domain across MPI processes + dd = DomainDecomposition(ncells, model.periodic, comm=mpi_comm) + + # Create 1D finite element spaces + V1 = SplineSpace(p1, grid=grid_1, periodic=per1) + V2 = SplineSpace(p2, grid=grid_2, periodic=per2) + + # Create 2D tensor product finite element space + V = TensorFemSpace(dd, V1, V2) + + s1, s2 = V.coeff_space.starts + e1, e2 = V.coeff_space.ends + + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Print decomposition information to terminal + if mpi_rank == 0: + print('--------------------------------------------------') + print(' CARTESIAN DECOMPOSITION' ) + print('--------------------------------------------------') + int_array_to_str = lambda array: ','.join('{:3d}'.format(i) for i in array) + int_tuples_to_str = lambda tuples: ', '.join( + '[{:d}, {:d}]'.format(a,b) for a,b in tuples) + + cart = V.coeff_space.cart + + block_sizes_i1 = [e1-s1+1 for s1, e1 in zip(cart.global_starts[0], cart.global_ends[0])] + block_sizes_i2 = [e2-s2+1 for s2, e2 in zip(cart.global_starts[1], cart.global_ends[1])] + + block_intervals_i1 = [(s1, e1) for s1, e1 in zip(cart.global_starts[0], cart.global_ends[0])] + block_intervals_i2 = [(s2, e2) for s2, e2 in zip(cart.global_starts[1], cart.global_ends[1])] + + print('> No. of points along eta1 :: {:d}'.format(cart.npts[0])) + print('> No. of points along eta2 :: {:d}'.format(cart.npts[1])) + print('') + print('> No. of blocks along eta1 :: {:d}'.format(cart.nprocs[0])) + print('> No. of blocks along eta2 :: {:d}'.format(cart.nprocs[1])) + print('') + print('> Block sizes along eta1 :: ' + int_array_to_str(block_sizes_i1)) + print('> Block sizes along eta2 :: ' + int_array_to_str(block_sizes_i2)) + print('') + print('> Intervals along eta1 :: ' + int_tuples_to_str(block_intervals_i1)) + print('> Intervals along eta2 :: ' + int_tuples_to_str(block_intervals_i2)) + print('', flush=True) + sleep(0.001) + + mpi_comm.Barrier() + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + # Analytical and spline mappings + map_analytic = model.mapping + + if use_spline_mapping: + map_discrete = SplineMapping.from_mapping(V, map_analytic) + # Write discrete geometry to HDF5 file + t0 = time() + geometry = Geometry.from_discrete_mapping(map_discrete, comm=mpi_comm) + geometry.export('geo.h5') + t1 = time() + timing['export'] += t1-t0 + mapping = map_discrete + else: + mapping = map_analytic + + # Build mass and stiffness matrices, and right-hand side vector + t0 = time() + M, S = assemble_matrices(V, mapping, kernel, nquads=nquads) + b = assemble_rhs(V, mapping, model.rho, nquads=nquads) + t1 = time() + timing['assembly'] = t1-t0 + + # If required by user, create C1 projector and then restrict + # stiffness/mass matrices and right-hand-side vector to C1 space + if c1_correction: + t0 = time() + proj = C1Projector(mapping) + Sp = proj.change_matrix_basis(S) + Mp = proj.change_matrix_basis(M) + bp = proj.change_rhs_basis(b) + t1 = time() + timing['projection'] = t1-t0 + + # Apply homogeneous Dirichlet boundary conditions where appropriate + # NOTE: this does not effect ghost regions + if not V1.periodic: + # left bc at x=0. + if not model.O_point and s1 == 0: + S[s1, :, :, :] = 0. + S[s1, :, 0, 0] = 1. + b[s1, :] = 0. + # right bc at x=1. + if e1 == V1.nbasis-1: + S[e1, :, :, :] = 0. + S[e1, :, 0, 0] = 1. + b[e1, :] = 0. + + if not V2.periodic: + # lower bc at y=0. + if s2 == 0: + S[:, s2, :, :] = 0. + S[:, s2, 0, 0] = 1. + b[:, s2] = 0. + # upper bc at y=1. + if e2 == V2.nbasis-1: + S[:, e2, :, :] = 0. + S[:, e2, 0, 0] = 1. + b[:, e2] = 0. + + if c1_correction and e1 == V1.nbasis-1: + # only bc is at s=1 + last = bp[1].space.npts[0] - 1 + Sp[1,1][last, :, :, :] = 0. + Sp[1,1][last, :, 0, 0] = 1. + bp[1] [last, :] = 0. + + # Solve linear system + t0 = time() + if c1_correction: + Sp_inv = inverse(Sp, 'cg', tol=1e-7, maxiter=1000, verbose=False) + xp = Sp_inv @ bp + info = Sp_inv.get_info() + x = proj.convert_to_tensor_basis(xp) + else: + S_inv = inverse(S, 'cg', tol=1e-7, maxiter=1000, verbose=False) + x = S_inv @ b + info = S_inv.get_info() + t1 = time() + timing['solution'] = t1-t0 + + # Create potential field + phi = FemField(V, coeffs=x) + phi.coeffs.update_ghost_regions() + + # Compute L2 norm of error + t0 = time() + sqrt_g = lambda *x: np.sqrt(mapping.metric_det(*x)) + integrand = lambda *x: (phi(*x) - model.phi(*x))**2 * sqrt_g(*x) + err2 = np.sqrt(V.integral(integrand, nquads=nquads)) + t1 = time() + timing['diagnostics'] = t1-t0 + + # Write solution to HDF5 file + t0 = time() + V.export_fields('fields.h5', phi=phi) + t1 = time() + timing['export'] += t1-t0 + + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Print some information to terminal + for i in range(mpi_size): + if i == mpi_rank: + print('--------------------------------------------------' ) + print(' RANK = {}'.format(mpi_rank)) + print('--------------------------------------------------' ) + print('> Grid :: [{ne1},{ne2}]'.format(ne1=ne1, ne2=ne2)) + print('> Degree :: [{p1},{p2}]' .format(p1=p1, p2=p2)) + print('> CG info :: ', info) + print('> L2 error :: {:.2e}'.format(err2)) + print('' ) + print('> Assembly time :: {:.2e}'.format(timing['assembly'])) + if c1_correction: + print('> Project. time :: {:.2e}'.format( timing['projection'])) + print('> Solution time :: {:.2e}'.format(timing['solution'])) + print('> Evaluat. time :: {:.2e}'.format(timing['diagnostics'])) + print('> Export time :: {:.2e}'.format(timing['export'])) + print('', flush=True) + sleep(0.001) + mpi_comm.Barrier() + + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # VISUALIZATION + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + ########## + N = 10 + ########## + + # Plot domain decomposition (master only) + fig = V.plot_2d_decomposition(mapping, refine=N) + fig.show() + + # Perform other visualization using master or all processes + if not distribute_viz: + + # Non-master processes stop here + if mpi_rank != 0: + return + + # Create new serial FEM space and mapping (if needed) + if use_spline_mapping: + geometry = Geometry(filename='geo.h5', comm=MPI.COMM_SELF) + map_discrete = [*geometry.mappings.values()].pop() + Vnew = map_discrete.space + mapping = map_discrete + else: + dd = DomainDecomposition(ncells, model.periodic, comm=MPI.COMM_SELF) + Vnew = TensorFemSpace(dd, V1, V2) + + # Import solution vector into new serial field + phi, = Vnew.import_fields( 'fields.h5', 'phi' ) + + else: + Vnew = V + + # Compute numerical solution (and error) on refined logical grid + [sk1, sk2], [ek1, ek2] = Vnew.local_domain + + eta1 = refine_array_1d(V1.breaks[sk1:ek1+2], N) + eta2 = refine_array_1d(V2.breaks[sk2:ek2+2], N) + num = np.array([[ phi(e1, e2) for e2 in eta2] for e1 in eta1]) + ex = np.array([[model.phi(e1, e2) for e2 in eta2] for e1 in eta1]) + err = num - ex + + # Compute physical coordinates of logical grid + pcoords = np.array([[model.mapping(e1, e2) for e2 in eta2] for e1 in eta1]) + xx = pcoords[:, :, 0] + yy = pcoords[:, :, 1] + + # Create figure with 3 subplots: + # 1. exact solution on exact domain + # 2. numerical solution on mapped domain (analytical or spline) + # 3. numerical error on mapped domain (analytical or spline) + fig, axes = plt.subplots(1, 3, figsize=(12.8, 4.8)) + + def add_colorbar(im, ax): + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size=0.2, pad=0.2) + cbar = ax.get_figure().colorbar(im, cax=cax) + return cbar + + # Plot exact solution + ax = axes[0] + im = ax.contourf(xx, yy, ex, 40, cmap='jet') + add_colorbar(im, ax) + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') + ax.set_title (r'$\phi_{ex}(x,y)$') + ax.plot(xx[:, ::N] , yy[:, ::N] , 'k') + ax.plot(xx[::N, :].T, yy[::N, :].T, 'k') + ax.set_aspect('equal') + + if use_spline_mapping: + # Recompute physical coordinates of logical grid using spline mapping + pcoords = np.array([[map_discrete(e1, e2) for e2 in eta2] for e1 in eta1]) + xx = pcoords[:, :, 0] + yy = pcoords[:, :, 1] + + # Plot numerical solution + ax = axes[1] + im = ax.contourf(xx, yy, num, 40, cmap='jet') + add_colorbar(im, ax) + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') + ax.set_title (r'$\phi(x,y)$') + ax.plot(xx[:, ::N] , yy[:, ::N] , 'k') + ax.plot(xx[::N, :].T, yy[::N, :].T, 'k') + ax.set_aspect('equal') + + # Plot numerical error + ax = axes[2] + im = ax.contourf(xx, yy, err, 40, cmap='jet') + add_colorbar(im, ax) + ax.set_xlabel(r'$x$', rotation='horizontal') + ax.set_ylabel(r'$y$', rotation='horizontal') + ax.set_title (r'$\phi(x,y) - \phi_{ex}(x,y)$') + ax.plot(xx[:, ::N] , yy[:, ::N] , 'k') + ax.plot(xx[::N, :].T, yy[::N, :].T, 'k') + ax.set_aspect('equal') + + # Show figure + fig.show() + + return locals() + +#============================================================================== +# Parser +#============================================================================== +def parse_input_arguments(): + + import argparse + + parser = argparse.ArgumentParser( + formatter_class = argparse.HelpFormatter, + description = "Solve Poisson's equation on a 2D domain." + ) + + parser.add_argument( '-t', + type = str, + choices =('square', 'annulus', 'circle', 'target', 'czarny'), + default = 'square', + dest = 'test_case', + help = 'Test case (default: square)' + ) + + parser.add_argument( '-n', + type = int, + nargs = 2, + default = [10, 10], + metavar = ('N1','N2'), + dest = 'ncells', + help = 'Number of grid cells (elements) along each dimension (default: [10, 10])' + ) + + parser.add_argument( '-d', + type = int, + nargs = 2, + default = [2, 2], + metavar = ('P1','P2'), + dest = 'degree', + help = 'Spline degree along each dimension (default: [2, 2])' + ) + + parser.add_argument( '-q', + type = int, + nargs = 2, + default = None, + metavar = ('Q1','Q2'), + dest = 'nquads', + help = 'Number of quadrature points along each dimension (default: [P1+1, P2+1])' + ) + + parser.add_argument( '-s', + action = 'store_true', + dest = 'use_spline_mapping', + help = 'Use spline mapping in finite element calculations (default: False)' + ) + + parser.add_argument( '-c', + action = 'store_true', + dest = 'c1_correction', + help = 'Apply C1 correction at polar singularity (O point) (default: False)' + ) + + parser.add_argument( '--distribute_viz', + action = 'store_true', + dest = 'distribute_viz', + help = 'Create separate plots for each subdomain (default: False)' + ) + + return parser.parse_args() + +#============================================================================== +# Script functionality +#============================================================================== +if __name__ == '__main__': + + args = parse_input_arguments() + namespace = main(**vars(args)) + + import __main__ + if hasattr(__main__, '__file__'): + try: + __IPYTHON__ + except NameError: + import matplotlib.pyplot as plt + plt.show() diff --git a/performance/compare_3d_matrix_assembly_speed.py b/performance/compare_3d_matrix_assembly_speed.py new file mode 100644 index 000000000..0bc8678a5 --- /dev/null +++ b/performance/compare_3d_matrix_assembly_speed.py @@ -0,0 +1,643 @@ +import os +import shutil +import time +from pathlib import Path +from datetime import datetime + +from mpi4py import MPI +import numpy as np +#import matplotlib.pyplot as plt +from sympy import sin + +from sympde.calculus import dot, cross, grad, curl +from sympde.expr import BilinearForm, integral +from sympde.topology import element_of, elements_of, Cube, Mapping, ScalarFunctionSpace, Domain, Derham + +from psydac.api.discretization import discretize +from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL +from psydac.cad.geometry import Geometry +from psydac.fem.basic import FemField +from psydac.mapping.discrete import SplineMapping + +datetime_md = datetime.today().strftime('%Y-%m-%d %H:%M:%S') +datetime_file = datetime.today().strftime('%Y-%m-%d_%H:%M:%S') + +""" +Executing this file will add new tables to the matrix_assembly_speed_log.md file in which we keep track of assembly and discretization speed. +Estimated runtime: ~4 min. + +""" + +comm = MPI.COMM_WORLD +backend = PSYDAC_BACKEND_GPYCCEL +mpi_rank = comm.rank + +if mpi_rank == 0: + print('Expected runtime: 4 min.') + print() + +class SquareTorus(Mapping): + + _expressions = {'x': 'x1 * cos(x2)', + 'y': 'x1 * sin(x2)', + 'z': 'x3'} + + _ldim = 3 + _pdim = 3 + +def make_square_torus_geometry_3d(ncells, degree, comm=None): + + if comm is not None: + mpi_rank = comm.Get_rank() + else: + mpi_rank = 0 + + if (ncells[0] == ncells[1]) and (ncells[0] == ncells[2]): + nc = f'{ncells[0]}' + else: + nc = f'{ncells[0]}_{ncells[1]}_{ncells[2]}' + + if (degree[0] == degree[1]) and (degree[0] == degree[2]): + de = f'{degree[0]}' + else: + de = f'{degree[0]}_{degree[1]}_{degree[2]}' + + name = f'st_3d_nc_{nc}_d_{de}' + + r = 0.5 + R = 1. + logical_domain = Cube('C', bounds1=(r, R), bounds2=(0, 2*np.pi), bounds3=(0, 1)) + domain_h = discretize(logical_domain, ncells=ncells, comm=comm) + + V = ScalarFunctionSpace('V', logical_domain) + V_h = discretize(V, domain_h, degree=degree) + + mapping = SquareTorus('S') + map_discrete = SplineMapping.from_mapping(V_h, mapping.get_callable_mapping()) + + geometry = Geometry.from_discrete_mapping(map_discrete, comm=comm) + + if mpi_rank == 0: + if not os.path.isdir('geometry'): + os.makedirs('geometry') + os.makedirs('geometry/files') + else: + if not os.path.isdir('geometry/files'): + os.makedirs('geometry/files') + + geometry.export(f'geometry/files/{name}.h5') + + return f'geometry/files/{name}.h5' + +# ---------- 1 ---------- +#t0_1_glob = time.time() +# +#ncells = [32, 32, 32] +#degree_list = [[2, 2, 2], [3, 3, 3], [4, 4, 4], [5, 5, 5]] +#degree_list2 = [[2, 2, 2], [3, 3, 3], [4, 4, 4]] +#periodic = [False, False, False] +# +##mapping = HalfHollowTorusMapping3D('M', R=2, r=1) +#mapping = SquareTorus('S') +##logical_domain = Cube('C', bounds1=(0,1), bounds2=(0,1), bounds3=(0,1)) +#r = 0.5 +#R = 1. +#logical_domain = Cube('C', bounds1=(r, R), bounds2=(0, 2*np.pi), bounds3=(0, 1)) +# +#domain = mapping(logical_domain) +#derham = Derham(domain) +#plot_domain(domain, draw=True, isolines=True) +# +#ass_time_H1_old = [[] for _ in degree_list] +#ass_time_H1_new = [[] for _ in degree_list] +# +#ass_time_Hcurl_old = [[] for _ in degree_list2] +#ass_time_Hcurl_new = [[] for _ in degree_list2] +# +#for i, degree in enumerate(degree_list): +# +# domain_h = discretize(domain, ncells=ncells, periodic=periodic, comm=comm) +# derham_h = discretize(derham, domain_h, degree=degree) +# +# V0 = derham.V0 +# V0h = derham_h.V0 +# +# u, v = elements_of(V0, names='u, v') +# +# a = BilinearForm((u, v), integral(domain, u*v)) +# +# a_h = discretize(a, domain_h, (V0h, V0h), backend=backend, sum_factorization=False) +# +# t0_ao = time.time() +# M_o = a_h.assemble() +# t1_ao = time.time() +# +# a_h = discretize(a, domain_h, (V0h, V0h), backend=backend) +# +# t0_an = time.time() +# M_n = a_h.assemble() +# t1_an = time.time() +# +# ass_time_H1_old[i].append(t1_ao - t0_ao) +# ass_time_H1_new[i].append(t1_an - t0_an) +# +# if degree != [5, 5, 5]: +# +# V1 = derham.V1 +# V1h = derham_h.V1 +# +# u, v = elements_of(V1, names='u, v') +# +# a = BilinearForm((u, v), integral(domain, dot(u, v))) +# +# a_h = discretize(a, domain_h, (V1h, V1h), backend=backend, sum_factorization=False) +# +# t0_ao = time.time() +# M_o = a_h.assemble() +# t1_ao = time.time() +# +# a_h = discretize(a, domain_h, (V1h, V1h), backend=backend) +# +# t0_an = time.time() +# M_n = a_h.assemble() +# t1_an = time.time() +# +# ass_time_Hcurl_old[i].append(t1_ao - t0_ao) +# ass_time_Hcurl_new[i].append(t1_an - t0_an) +# +#d = [degree[0] for degree in degree_list] +#d2 = [degree[0] for degree in degree_list2] +# +#ass_time_H1_old_d = [ass_time_H1_old[i][0] for i, _ in enumerate(degree_list)] +#ass_time_H1_new_d = [ass_time_H1_new[i][0] for i, _ in enumerate(degree_list)] +# +#ass_time_Hcurl_old_d = [ass_time_Hcurl_old[i][0] for i, _ in enumerate(degree_list2)] +#ass_time_Hcurl_new_d = [ass_time_Hcurl_new[i][0] for i, _ in enumerate(degree_list2)] +# +#if mpi_rank == 0: +# plt.plot(d, ass_time_H1_old_d, '--.', label=f'old Algorithm') +# plt.plot(d, ass_time_H1_new_d, '--.', label=f'new Algorithm') +# plt.title(r'Assembly times for the $H^1(\Omega)$ mass matrix') +# plt.legend() +# plt.yscale('log') +# plt.ylabel('Wallclock Time [s]') +# plt.xlabel('d - [d, d, d] Bspline degrees') +# plt.xticks(d) +# #plt.savefig(f'figures/H1_{datetime_file}.png') +# plt.show() +# plt.clf() +# +# plt.plot(d2, ass_time_Hcurl_old_d, '--.', label=f'old Algorithm') +# plt.plot(d2, ass_time_Hcurl_new_d, '--.', label=f'new Algorithm') +# plt.title(r'Assembly times for the $H(curl;\Omega)$ mass matrix') +# plt.legend() +# plt.yscale('log') +# plt.ylabel('Wallclock Time [s]') +# plt.xlabel('d - [d, d, d] Bspline degrees') +# plt.xticks(d2) +# #plt.savefig(f'figures/Hcurl_{datetime_file}.png') +# plt.show() +# +#t1_1_glob = time.time() +#if mpi_rank == 0: +# print(f'Part 1 out of 3 done after {(t1_1_glob-t0_1_glob)/60:.2g}min') +# ----------------------- + +# ---------- 2 ---------- +t0_2_glob = time.time() + +ncells = [32, 32, 32] +degree = [3, 3, 3] +periodic = [False, False, False] + +r = 0.5 +R = 1. +logical_domain = Cube('C', bounds1=(r, R), bounds2=(0, 2*np.pi), bounds3=(0, 1)) +logical_derham = Derham(logical_domain) + +logical_domain_h = discretize(logical_domain, ncells=ncells, periodic=periodic, comm=comm) +logical_derham_h = discretize(logical_derham, logical_domain_h, degree=degree) + +filename = make_square_torus_geometry_3d(ncells, degree, comm=comm) + +bspline_domain = Domain.from_file(filename) +bspline_derham = Derham(bspline_domain) + +bspline_domain_h = discretize(bspline_domain, filename=filename, comm=comm) +bspline_derham_h = discretize(bspline_derham, bspline_domain_h, degree=bspline_domain.mapping.get_callable_mapping().space.degree) + +mapping = SquareTorus('S') + +analytical_domain = mapping(logical_domain) +analytical_derham = Derham(analytical_domain) + +analytical_domain_h = discretize(analytical_domain, ncells=ncells, periodic=periodic, comm=comm) +analytical_derham_h = discretize(analytical_derham, analytical_domain_h, degree=degree) + +ax, ay, az = analytical_domain.coordinates +agamma = ax*ay*az + sin(ax*ay+az)**2 + +# 2.1 +dom = logical_domain +domh = logical_domain_h +V = logical_derham.V1 +Vh = logical_derham_h.V1 + +F = element_of(V, name='F') +f = Vh.coeff_space.zeros() +f[0]._data = np.ones(f[0]._data.shape) +f[1]._data = np.ones(f[1]._data.shape) +f[2]._data = np.ones(f[2]._data.shape) +f_field = FemField(Vh, f) + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(cross(F, u), cross(F, v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_21 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +old_ass_21 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_21 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +new_ass_21 = round(t1-t0, 3) + +# 2.2 +dom = analytical_domain +domh = analytical_domain_h +V = analytical_derham.V2 +Vh = analytical_derham_h.V2 + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(u, v)*agamma)) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_22 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_22 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_22 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_22 = round(t1-t0, 3) + +# 2.3 +dom = bspline_domain +domh = bspline_domain_h +V = bspline_derham.V1 +Vh = bspline_derham_h.V1 + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(curl(u), curl(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_23 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_23 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_23 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_23 = round(t1-t0, 3) + +t1_2_glob = time.time() +if mpi_rank == 0: + print(f'Part 2 out of 3 done after {(t1_2_glob-t0_2_glob)/60:.2g}min') +# ----------------------- + +# ---------- 3 ---------- +t0_3_glob = time.time() + +ncells = [16, 8, 32] +degree = [2, 4, 3] +periodic = [False, True, False] + +r = 0.5 +R = 1. +logical_domain = Cube('C', bounds1=(r, R), bounds2=(0, 2*np.pi), bounds3=(0, 1)) +logical_derham = Derham(logical_domain) + +logical_domain_h = discretize(logical_domain, ncells=ncells, periodic=periodic, comm=comm) +logical_derham_h = discretize(logical_derham, logical_domain_h, degree=degree) + +filename = make_square_torus_geometry_3d(ncells, degree, comm=comm) + +bspline_domain = Domain.from_file(filename) +bspline_derham = Derham(bspline_domain) + +bspline_domain_h = discretize(bspline_domain, filename=filename, comm=comm) +bspline_derham_h = discretize(bspline_derham, bspline_domain_h, degree=bspline_domain.mapping.get_callable_mapping().space.degree) + +mapping = SquareTorus('S') + +analytical_domain = mapping(logical_domain) +analytical_derham = Derham(analytical_domain) + +analytical_domain_h = discretize(analytical_domain, ncells=ncells, periodic=periodic, comm=comm) +analytical_derham_h = discretize(analytical_derham, analytical_domain_h, degree=degree) + +ax, ay, az = analytical_domain.coordinates +agamma = ax*ay*az + sin(ax*ay+az)**2 + +# 3.1.1 +dom = logical_domain +domh = logical_domain_h +V = logical_derham.V0 +Vh = logical_derham_h.V0 + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_311 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_311 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_311 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_311 = round(t1-t0, 3) + +# 3.1.2 +dom = analytical_domain +domh = analytical_domain_h +V = analytical_derham.V0 +Vh = analytical_derham_h.V0 + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_312 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_312 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_312 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_312 = round(t1-t0, 3) + +# 3.1.3 +dom = bspline_domain +domh = bspline_domain_h +V = bspline_derham.V0 +Vh = bspline_derham_h.V0 + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_313 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_313 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_313 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_313 = round(t1-t0, 3) + +# 3.2 +dom = analytical_domain +domh = analytical_domain_h +V = ScalarFunctionSpace('V', analytical_domain) +Vh = discretize(V, analytical_domain_h, degree=degree) + +u, v = elements_of(V, names='u, v') +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_32 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_32 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_32 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_32 = round(t1-t0, 3) + +# 3.3 +dom = analytical_domain +domh = analytical_domain_h +V = ScalarFunctionSpace('V', analytical_domain) +Vh = discretize(V, analytical_domain_h, degree=degree) +W = analytical_derham.V0 +Wh = analytical_derham_h.V0 + +u = element_of(V, name='u') +v = element_of(W, name='v') +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)))) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_33 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_33 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_33 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_33 = round(t1-t0, 3) + +# ------------------ +dom = analytical_domain +domh = analytical_domain_h +V = analytical_derham.V0 +Vh = analytical_derham_h.V0 + +u, v = elements_of(V, names='u, v') +# ------------------ + +# 3.4 +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)) * agamma)) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_34 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +old_ass_34 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_34 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble() +t1 = time.time() +new_ass_34 = round(t1-t0, 3) + +# 3.5 +F = element_of(V, name='F') +f = Vh.coeff_space.zeros() +f._data = np.ones(f._data.shape) +f_field = FemField(Vh, f) + +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)) * F)) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_35 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +old_ass_35 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_35 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +new_ass_35 = round(t1-t0, 3) + +# 3.6 +mult = [1, 3, 2] +analytical_derham_h = discretize(analytical_derham, analytical_domain_h, degree=degree, multiplicity=mult) +Vh = analytical_derham_h.V0 + +a = BilinearForm((u, v), integral(dom, dot(grad(u), grad(v)) * F)) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend, sum_factorization=False) +t1 = time.time() +old_disc_36 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +old_ass_36 = round(t1-t0, 3) + +t0 = time.time() +ah = discretize(a, domh, (Vh, Vh), backend=backend) +t1 = time.time() +new_disc_36 = round(t1-t0, 3) +t0 = time.time() +M = ah.assemble(F=f_field) +t1 = time.time() +new_ass_36 = round(t1-t0, 3) + +t1_3_glob = time.time() +if mpi_rank == 0: + print(f'Part 3 out of 3 done after {(t1_3_glob-t0_3_glob)/60:.2g}min') +# ----------------------- + +template = '''| Test case | old assembly | new assembly | old discretization | new discretization | +| --- | --- | --- | --- | --- | +| 2.1 | {old_ass_21} | {new_ass_21} | {old_disc_21} | {new_disc_21} | +| 2.2 | {old_ass_22} | {new_ass_22} | {old_disc_22} | {new_disc_22} | +| 2.3 | {old_ass_23} | {new_ass_23} | {old_disc_23} | {new_disc_23} | +| 3.1.1 | {old_ass_311} | {new_ass_311} | {old_disc_311} | {new_disc_311} | +| 3.1.2 | {old_ass_312} | {new_ass_312} | {old_disc_312} | {new_disc_312} | +| 3.1.3 | {old_ass_313} | {new_ass_313} | {old_disc_313} | {new_disc_313} | +| 3.2 | {old_ass_32} | {new_ass_32} | {old_disc_32} | {new_disc_32} | +| 3.3 | {old_ass_33} | {new_ass_33} | {old_disc_33} | {new_disc_33} | +| 3.4 | {old_ass_34} | {new_ass_34} | {old_disc_34} | {new_disc_34} | +| 3.5 | {old_ass_35} | {new_ass_35} | {old_disc_35} | {new_disc_35} | +| 3.6 | {old_ass_36} | {new_ass_36} | {old_disc_36} | {new_disc_36} |''' + +txt = '' +txt += f'{datetime_md}\n' +txt += f'----------\n\n' +#txt += f'![](tests/figures/H1_{datetime_file}.png)\n' +#txt += f'![](tests/figures/Hcurl_{datetime_file}.png)\n\n' +txt += template.format(old_ass_21=old_ass_21, new_ass_21=new_ass_21, old_disc_21=old_disc_21, new_disc_21=new_disc_21, + old_ass_22=old_ass_22, new_ass_22=new_ass_22, old_disc_22=old_disc_22, new_disc_22=new_disc_22, + old_ass_23=old_ass_23, new_ass_23=new_ass_23, old_disc_23=old_disc_23, new_disc_23=new_disc_23, + old_ass_311=old_ass_311, new_ass_311=new_ass_311, old_disc_311=old_disc_311, new_disc_311=new_disc_311, + old_ass_312=old_ass_312, new_ass_312=new_ass_312, old_disc_312=old_disc_312, new_disc_312=new_disc_312, + old_ass_313=old_ass_313, new_ass_313=new_ass_313, old_disc_313=old_disc_313, new_disc_313=new_disc_313, + old_ass_32=old_ass_32, new_ass_32=new_ass_32, old_disc_32=old_disc_32, new_disc_32=new_disc_32, + old_ass_33=old_ass_33, new_ass_33=new_ass_33, old_disc_33=old_disc_33, new_disc_33=new_disc_33, + old_ass_34=old_ass_34, new_ass_34=new_ass_34, old_disc_34=old_disc_34, new_disc_34=new_disc_34, + old_ass_35=old_ass_35, new_ass_35=new_ass_35, old_disc_35=old_disc_35, new_disc_35=new_disc_35, + old_ass_36=old_ass_36, new_ass_36=new_ass_36, old_disc_36=old_disc_36, new_disc_36=new_disc_36) +txt += '\n\n' + +if mpi_rank == 0: + # Write performance table to MarkDown file + with open('matrix_assembly_speed_log.md', 'a') as f: + f.write(txt) + + # Remove temporary folders + base_dir = Path(__file__).parent + dirs_to_remove = [ + "geometry", + "__psydac__", + "__pycache__", + "__epyccel__", + "__gpyccel__" + ] + for d in dirs_to_remove: + path = base_dir / d + if path.exists(): + try: + shutil.rmtree(path) + except Exception as e: + print(f"Failed to remove {path}: {e}") diff --git a/performance/matrix_assembly_speed_log.md b/performance/matrix_assembly_speed_log.md new file mode 100644 index 000000000..996975d26 --- /dev/null +++ b/performance/matrix_assembly_speed_log.md @@ -0,0 +1,86 @@ +New Matrix Assembly for Psydac +------------------------------ + +Here we keep track on the performance of the new assembly algorithm (sum factorization). +We measure both the discretization time of `BilinearForms` as well as the matrix assembly time of a `DiscreteBilinearForm` +and compare it to the old algorithm. Executing `compare_3d_matrix_assembly_speed.py` will add new data to this file. +This allows us to detect whether any future changes have a positive or negative impact. + +(Of course, runtime depends on the machine used to execute this file. Hence, an even decrease in runtime +is no reason to celebrate, and an even increase in runtime no reason to worry.) + +Test cases +---------- + +1. Scaling Analysis \ (**REMOVED**) + We assemble the H1 and Hcurl mass matrices ($\Omega$ is a half hollow torus (analytical mapping), ncells = [32, 32, 32], periodic=[False, False, False]) for varying degrees + and report the assembly times in Figures. + +2. Three specific test cases \ + We report assembly and discretization time for the following test cases \ + 2.1 "Q" + - $(u, v; F)\mapsto\int_{\Omega}(F\times u)\circ(F\times v)$ + - $u, v, F\in H(curl;\Omega)$, $F$ a "free field" + - no mapping ($\Omega=(0,1)^3$) + - ncells = [32, 32, 32], degree = [3, 3, 3], periodic = [False, False, False] + + 2.2 "weighted $Hdiv$ mass matrix" + - $(u, v; \gamma)\mapsto\int_{\Omega}u\circ v\ \gamma$ + - $u, v\in H(div;\Omega)$, $\gamma$ an analytic scalar weight function + - analytical mapping ($\Omega$ a half hollow torus) + - ncells = [32, 32, 32], degree = [3, 3, 3], periodic = [False, False, False] + + 2.3 "curl curl" + - $(u, v)\mapsto\int_{\Omega}(\nabla\times u)\circ(\nabla\times v)$ + - $u, v\in H(curl;\Omega)$ + - Bspline mapping ($\Omega$ a half hollow torus) + - ncells = [32, 32, 32], degree = [3, 3, 3], periodic = [False, False, False] + +3. More variations! + - $(u, v)\mapsto\int_{\Omega}\nabla u\circ\nabla v$ + - ncells = [16, 8, 32], degree = [2, 4, 3], periodic = [False, True, False] + - 3.1.1 + - `u, v = elements_of(derham.V0, names='u, v')` + - no mapping + - 3.1.2 + - `u, v = elements_of(derham.V0, names='u, v')` + - analytical mapping + - 3.1.3 + - `u, v = elements_of(derham.V0, names='u, v')` + - Bspline mapping + - 3.2 + - `u, v = elements_of(ScalarFunctionSpace('Vs', domain), names='u, v')` + - analytical mapping + - 3.3 + - `u = element_of(derham.V0, name='u')` + - `v = element_of(ScalarFunctionSpace('Vs', domain), name='v')` + - analytical mapping + - `u, v = elements_of(derham.V0, names='u, v')` + - analytical mapping + - 3.4 + - additional analytical weight function $\gamma$ + - 3.5 + - additional scalar free field $F\in H^1(\Omega)$ + - 3.6 + - multiplicity vector [1, 3, 2] + +Data +---- + +2025-09-09 17:28:19 (added by Julian O. - ThinkPad T14 on performance mode) +---------- + +| Test case | old assembly | new assembly | old discretization | new discretization | +| --- | --- | --- | --- | --- | +| 2.1 | 21.358 | 1.676 | 16.568 | 16.589 | +| 2.2 | 2.348 | 0.322 | 4.533 | 3.38 | +| 2.3 | 36.858 | 3.171 | 16.749 | 19.413 | +| 3.1.1 | 0.236 | 0.044 | 1.462 | 1.482 | +| 3.1.2 | 0.54 | 0.051 | 1.722 | 1.52 | +| 3.1.3 | 0.802 | 0.169 | 3.579 | 2.414 | +| 3.2 | 0.517 | 0.045 | 1.027 | 1.425 | +| 3.3 | 0.525 | 0.06 | 1.023 | 1.527 | +| 3.4 | 0.524 | 0.044 | 1.085 | 1.547 | +| 3.5 | 0.577 | 0.057 | 1.51 | 1.784 | +| 3.6 | 0.598 | 0.209 | 2.411 | 1.848 | + diff --git a/psydac/api/ast/expr.py b/psydac/api/ast/expr.py new file mode 100644 index 000000000..4805b85d7 --- /dev/null +++ b/psydac/api/ast/expr.py @@ -0,0 +1,603 @@ +from sympy import symbols, Symbol, IndexedBase +from sympy import Mul, Tuple, Range +from sympy import Matrix, ImmutableDenseMatrix +from sympy.core.numbers import ImaginaryUnit + +from psydac.pyccel.ast.core import IndexedVariable +from psydac.pyccel.ast.core import For +from psydac.pyccel.ast.core import Assign +from psydac.pyccel.ast.core import AugAssign +from psydac.pyccel.ast.core import Slice +from psydac.pyccel.ast.core import FunctionDef +from psydac.pyccel.ast.core import FunctionCall +from psydac.pyccel.ast.core import Import +from psydac.pyccel.ast.core import Nil +from psydac.pyccel.ast.core import Len +from psydac.pyccel.ast.core import If, Is, Return +from psydac.pyccel.ast.core import _atomic + +from sympde.core import Constant +from sympde.topology.space import ScalarFunction +from sympde.topology.space import VectorFunction +from sympde.topology.space import IndexedVectorFunction +from sympde.topology.derivatives import _partial_derivatives +from sympde.topology.derivatives import _logical_partial_derivatives +from sympde.topology.derivatives import get_atom_derivatives +from sympde.topology.derivatives import get_index_derivatives +from sympde.topology import LogicalExpr +from sympde.topology import SymbolicExpr +from sympde.calculus.matrices import SymbolicDeterminant + +from .basic import SplBasic +from .utilities import random_string +from .utilities import build_pythran_types_header, variables +from .utilities import build_pyccel_type_annotations +from .utilities import math_atoms_as_str +from .utilities import get_max_partial_derivatives + +from psydac.fem.vector import MultipatchFemSpace +from .nodes import Zeros + +#============================================================================== +def is_scalar_field(expr): + + if isinstance(expr, _partial_derivatives): + return is_scalar_field(expr.args[0]) + + elif isinstance(expr, _logical_partial_derivatives): + return is_scalar_field(expr.args[0]) + + elif isinstance(expr, ScalarFunction): + return True + + return False + +#============================================================================== +def is_vector_field(expr): + + if isinstance(expr, _partial_derivatives): + return is_vector_field(expr.args[0]) + + elif isinstance(expr, _logical_partial_derivatives): + return is_vector_field(expr.args[0]) + + elif isinstance(expr, (VectorFunction, IndexedVectorFunction)): + return True + + return False + +#============================================================================== +def compute_atoms_expr(atom, basis, indices, loc_indices, dim): + + cls = (_partial_derivatives, + ScalarFunction, + VectorFunction, + IndexedVectorFunction) + + if not isinstance(atom, cls): + raise TypeError('atom must be of type {}'.format(str(cls))) + + p_indices = get_index_derivatives(atom) + orders = [0 for i in range(0, dim)] + ind = 0 + a = atom + if isinstance(atom, _partial_derivatives): + a = get_atom_derivatives(atom) + orders[atom.grad_index] = p_indices[atom.coordinate] + + + if isinstance(a, IndexedVectorFunction): + ind = a.indices[0] + args = [] + for i in range(dim): + if isinstance(a, IndexedVectorFunction): + args.append(basis[ind+i*dim][loc_indices[i],orders[i],indices[i]]) + elif isinstance(a, ScalarFunction): + args.append(basis[i][loc_indices[i],orders[i],indices[i]]) + else: + raise NotImplementedError('TODO') + + # + return tuple(args), ind + + +class ExprKernel(SplBasic): + + def __new__(cls, expr, space, name=None, mapping=None, is_rational_mapping=None, backend=None): + + tag = random_string( 8 ) + obj = SplBasic.__new__(cls, tag, name=name, + prefix='kernel', mapping=mapping, + is_rational_mapping=is_rational_mapping) + + obj._expr = expr + obj._space = space + obj._user_functions = [] + obj._backend = backend + + obj._func = obj._initialize() + + return obj + + @property + def expr(self): + return self._expr + + @property + def dim(self): + return self._dim + + @property + def space(self): + return self._space + + @property + def n_rows(self): + return self._n_rows + + @property + def n_cols(self): + return self._n_cols + + @property + def max_nderiv(self): + return self._max_nderiv + + @property + def coordinates(self): + return self._coordinates + + @property + def fields(self): + return self._fields + + @property + def vector_fields(self): + return self._vector_fields + + @property + def fields_coeff(self): + return self._fields_coeff + + @property + def vector_fields_coeff(self): + return self._vector_fields_coeff + + @property + def constants(self): + return self._constants + + @property + def global_mats(self): + return self._global_mats + + @property + def global_mats_types(self): + return self._global_mats_types + + @property + def user_functions(self): + return self._user_functions + + @property + def backend(self): + return self._backend + + def build_arguments(self, data): + + other = data + + if self.constants: + other = other + self.constants + + return self.basic_args + other + + def _initialize(self): + Vh = self.space + expr = self.expr + dim = Vh.ldim + if isinstance(Vh, MultipatchFemSpace): + size = len(Vh.spaces) + else: + size = 1 + + self._dim = dim + # ... discrete values + + + n_elements = Vh.ncells + degrees = Vh.degree + # TODO improve + if isinstance(Vh, MultipatchFemSpace): + degrees = degrees[0] + # ... + + + n_rows = 1 ; n_cols = 1 + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + n_rows = expr.shape[0] + n_cols = expr.shape[1] + + self._n_rows = n_rows + self._n_cols = n_cols + # ... + + # ... + prelude = [] + body = [] + imports = [] + # ... + + # ... + degrees = variables('p1:%s(1:%s)'%(dim+1,size+1), 'int') + n_elements = variables('n1:%s'%(dim+1), 'int') + xis = variables('x1:%s'%(dim+1), 'real') + arr_xis = variables('arr_x1:%s'%(dim+1), dtype='real', rank=1, cls=IndexedVariable) + indices = variables('i1:%s'%(dim+1), 'int') + loc_indices = variables('j1:%s'%(dim+1), 'int') + + lengths = variables('k1:%s'%(dim+1), 'int') + + ranges = [Range(lengths[i]) for i in range(dim)] + # ... + + d_vals = {} + for i in range(0, n_rows): + for j in range(0, n_cols): + is_complex = False + mat = IndexedBase('val_{i}{j}'.format(i=i,j=j)) + d_vals[i, j] = mat + # ... + + xs = [Symbol(x) for x in ['x', 'y', 'z'][:dim]] + for xi, x in zip(xis, xs): + expr = expr.subs(x, xi) + # ... + + # ... + atoms_types = (_partial_derivatives, + _logical_partial_derivatives, + ScalarFunction, + VectorFunction, IndexedVectorFunction, + SymbolicDeterminant, + Symbol) + + atoms = _atomic(expr, cls=atoms_types) + self._constants = _atomic(expr, cls=Constant) + self._coordinates = tuple(xis) + # ... + + atomic_expr_field = [atom for atom in atoms if is_scalar_field(atom)] + atomic_expr_vector_field = [atom for atom in atoms if is_vector_field(atom)] + + self._fields = tuple(expr.atoms(ScalarFunction)) + self._vector_fields = tuple(expr.atoms(VectorFunction)) + # ... + fields_str = tuple(SymbolicExpr(f).name for f in atomic_expr_field) + vector_fields_str = tuple(SymbolicExpr(f).name for f in atomic_expr_vector_field) + + fields = symbols(fields_str) + vector_fields = symbols(vector_fields_str) + + if fields: + fields_coeff = variables(['F_coeff',], + dtype='real', rank=dim, cls=IndexedVariable) + else: + field_coeff = () + + if vector_fields: + vector_fields_coeff = variables(['F_{}_coeff'.format(str(i)) for i in range(size)], + dtype='real', rank=dim, cls=IndexedVariable) + else: + vector_fields_coeff = () + + self._fields_coeff = fields_coeff + self._vector_fields_coeff = vector_fields_coeff + + if fields or vector_fields_str: + basis = variables( 'basis1:%s(1:%s)'%(dim+1,size+1), + dtype = 'real', + rank = 3, + cls = IndexedVariable ) + + spans = variables( 'spans1:%s(1:%s)'%(dim+1,size+1), + dtype = 'int', + rank = 1, + cls = IndexedVariable ) + + # ... TODO add it as a method to basic class + nderiv = 0 + + if isinstance(expr, Matrix): + n_rows, n_cols = expr.shape + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + d_atoms = _atomic(expr[i_row,i_col], cls=atoms_types) + Fs = [get_atom_derivatives(a) for a in d_atoms] + d = get_max_partial_derivatives(expr[i_row,i_col], logical=False, F=Fs) + nderiv = max(nderiv, max(d.values())) + else: + Fs = [get_atom_derivatives(a) for a in atoms] + d = get_max_partial_derivatives(expr, logical=False, F=Fs) + nderiv = max(nderiv, max(d.values())) + + self._max_nderiv = nderiv + + + + # ... + for l,arr_xi in zip(lengths, arr_xis): + prelude += [Assign(l, Len(arr_xi))] + # ... + + # ... + slices = [Slice(None,None)]*dim + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + symbol = d_vals[i_row,i_col] + prelude += [Assign(symbol[slices], 0.)] + # ... + + # ... fields + for i in range(len(fields)): + body.append(Assign(fields[i],0)) + atom = atomic_expr_field[i] + atoms,ind = compute_atoms_expr(atom, basis, indices, loc_indices, dim) + slices = tuple(sp[id]-p+j for sp,p,j,id in zip(spans, degrees, loc_indices, indices)) + args = args + (fields_coeff[0][slices],) + for_body = [AugAssign(fields[i],'+',Mul(*args))] + loc_ranges = [Range(j) for j in degrees] + for j in range(dim): + for_body = [For(loc_indices[dim-1-j], loc_ranges[dim-1-j],for_body)] + + body += for_body + + for i in range(len(vector_fields)): + body.append(Assign(vector_fields[i],0)) + atom = atomic_expr_vector_field[i] + atoms, ind = compute_atoms_expr(atom, basis, indices, loc_indices, dim) + slices = tuple(sp[id]-p+j for sp,p,j,id in zip(spans[ind::size], degrees[ind::size], loc_indices,indices)) + atoms = atoms + (vector_fields_coeff[ind][slices],) + for_body = [AugAssign(vector_fields[i],'+',Mul(*atoms))] + loc_ranges = [Range(j) for j in degrees[ind::size]] + for j in range(dim): + for_body = [For(loc_indices[dim-1-j], loc_ranges[dim-1-j],for_body)] + + body += for_body + + # ... + + # ... + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + val = d_vals[i_row,i_col] + val = val[indices] + + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + rhs = SymbolicExpr(expr[i_row, i_col]) + body += [Assign(val, rhs)] + + else: + rhs = SymbolicExpr(expr) + body += [Assign(val, rhs)] + + for i in range(dim-1, -1, -1): + x = indices[i] + rx = ranges[i] + + xi = xis[i] + arr_xi = arr_xis[i] + body = [Assign(xi, arr_xi[x])] + body + + body = [For(x, rx, body)] + + # ... + body = prelude + body + # ... + + # ... get math functions and constants + math_elements = math_atoms_as_str(expr, 'math') + math_imports = [Import('math', e) for e in math_elements] + + imports += math_imports + # ... + + # ... + self._basic_args = arr_xis + fields_coeff + vector_fields_coeff + degrees + basis + spans + # ... + + # ... + mats = [] + for i in range(0, n_rows): + for j in range(0, n_cols): + mats.append(d_vals[i, j]) + mats = tuple(mats) + self._global_mats = mats + # ... + mats_types = [] + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + for i in range(0, n_rows): + for j in range(0, n_cols): + dtype = 'float' + if expr[i, j].atoms(ImaginaryUnit): + dtype = 'complex' + mats_types.append(dtype) + + else: + dtype = 'float' + if expr.atoms(ImaginaryUnit): + dtype = 'complex' + mats_types.append(dtype) + + mats_types = tuple(mats_types) + self._global_mats_types = mats_types + + self._imports = imports + + # function args + + func_args = self.build_arguments(mats) + decorators = {} + header = None + + if self.backend['name'] == 'pyccel': + func_args = build_pyccel_type_annotations(func_args) + elif self.backend['name'] == 'pythran': + header = build_pythran_types_header(self.name, func_args) + + return FunctionDef(self.name, list(func_args), [], body, + decorators=decorators, header=header) + + +class ExprInterface(SplBasic): + + def __new__(cls, kernel, name=None, mapping=None, is_rational_mapping=None, backend=None): + + if not isinstance(kernel, ExprKernel): + raise TypeError('> Expecting an ExprKernel') + + obj = SplBasic.__new__(cls, kernel.tag, name=name, + prefix='interface', mapping=mapping, + is_rational_mapping=is_rational_mapping) + + obj._kernel = kernel + obj._backend = backend + + # update dependencies + obj._dependencies += [kernel] + + obj._func = obj._initialize() + return obj + + @property + def kernel(self): + return self._kernel + + @property + def backend(self): + return self._backend + + @property + def max_nderiv(self): + return self.kernel.max_nderiv + + @property + def n_rows(self): + return self.kernel.n_rows + + @property + def n_cols(self): + return self.kernel.n_cols + + @property + def global_mats_types(self): + return self.kernel.global_mats_types + + def build_arguments(self, data): + # data must be at the end, since they are optional + return self.basic_args + data + + @property + def in_arguments(self): + return self._in_arguments + + @property + def inout_arguments(self): + return self._inout_arguments + + @property + def user_functions(self): + return self.kernel.user_functions + + def _initialize(self): + + kernel = self.kernel + global_mats = kernel.global_mats + global_mats_types = kernel.global_mats_types + fields = kernel.fields + vector_fields = kernel.vector_fields + dim = kernel.dim + + + # ... declarations + space = Symbol('W') + + arr_xis = symbols('arr_x1:%d'%(dim+1), cls=IndexedBase) + lengths = symbols('k1:%d'%(dim+1)) + # ... + + self._basic_args = (space, ) + kernel.basic_args + # ... + imports = [] + prelude = [] + body = [] + + # ... + imports += [Import('numpy',('zeros',))] + # ... + + # ... + for l,arr_xi in zip(lengths, arr_xis): + prelude += [Assign(l, Len(arr_xi))] + # ... + + # ... + if dim > 1: + lengths = Tuple(*lengths) + lengths = [lengths] + + for M,dtype in zip(global_mats, global_mats_types): + if_cond = Is(M, Nil()) + + _args = list(lengths) + ['{}'.format(dtype)] + if_body = [Assign(M, Zeros(*_args))] + + stmt = If((if_cond, if_body)) + body += [Import('numpy',('zeros',)), stmt] + # ... + + # ... + body = prelude + body + # ... + + # ... + self._inout_arguments = list(global_mats) + self._in_arguments = list(self.kernel.coordinates) + list(self.kernel.constants) + list(fields) + list(vector_fields) + # ... + + # ... call to kernel + # TODO add fields + mat_data = tuple(global_mats) + + args = mat_data + args = kernel.build_arguments(args) + + body += [FunctionCall(kernel.func, args)] + # ... + + # ... results + if len(global_mats) == 1: + M = global_mats[0] + body += [Return(M)] + + else: + body += [Return(global_mats)] + # ... + + # ... arguments + mats = [Assign(M, Nil()) for M in global_mats] + mats = tuple(mats) + + + # TODO improve using in_arguments + if self.kernel.constants: + constants = self.kernel.constants + args = constants + mats + else: + args = mats + + func_args = self.build_arguments(args) + # ... + + self._imports = imports + return FunctionDef(self.name, list(func_args), [], body) diff --git a/psydac/api/ast/fem.py b/psydac/api/ast/fem.py new file mode 100644 index 000000000..adf7ff05f --- /dev/null +++ b/psydac/api/ast/fem.py @@ -0,0 +1,1769 @@ +# -*- coding: UTF-8 -*- + +import numpy as np +from itertools import groupby, product + +from sympy import Basic, S, Function, Integer, Symbol +from sympy import Matrix, ImmutableDenseMatrix, true +from sympy.core.containers import Tuple + +from sympde.expr import LinearForm, BilinearForm, Functional +from sympde.topology.basic import Boundary, Interface +from sympde.topology import H1SpaceType, HcurlSpaceType, HdivSpaceType, L2SpaceType, UndefinedSpaceType, IdentityMapping +from sympde.topology.space import ScalarFunction, VectorFunction, IndexedVectorFunction +from sympde.topology.derivatives import _logical_partial_derivatives, get_atom_logical_derivatives +from sympde.topology.mapping import InterfaceMapping +from sympde.calculus.core import is_zero, PlusInterfaceOperator + +from psydac.pyccel.ast.core import _atomic, Assign, Import, Return, Comment, Continue, Slice + +from .nodes import GlobalTensorQuadratureGrid, PlusGlobalTensorQuadratureGrid, LocalTensorQuadratureGrid, PlusLocalTensorQuadratureGrid +from .nodes import GlobalTensorQuadratureTestBasis, LocalTensorQuadratureTestBasis, GlobalTensorQuadratureTrialBasis, LocalTensorQuadratureTrialBasis +from .nodes import LengthElement, LengthQuadrature +from .nodes import LengthDofTrial, LengthDofTest +from .nodes import Reset, ProductGenerator +from .nodes import BlockStencilMatrixLocalBasis, StencilMatrixLocalBasis, BlockStencilMatrixGlobalBasis, BlockScalarLocalBasis +from .nodes import BlockStencilVectorLocalBasis, StencilVectorLocalBasis, BlockStencilVectorGlobalBasis +from .nodes import GlobalElementBasis, LocalElementBasis +from .nodes import GlobalSpanArray, LocalSpanArray, GlobalThreadSpanArray, CoefficientBasis +from .nodes import MatrixLocalBasis, MatrixGlobalBasis, MatrixRankFromCoords, MatrixCoordsFromRank +from .nodes import GeometryExpressions +from .nodes import Loop, VectorAssign +from .nodes import EvalMapping, EvalField +from .nodes import ComputeKernelExpr +from .nodes import ElementOf, Reduce, Reduction +from .nodes import construct_logical_expressions +from .nodes import Pads, Mask +from .nodes import index_quad, index_element, index_dof_test, index_dof_trial, index_outer_dof_test, index_inner_dof_test +from .nodes import thread_coords, local_index_element, thread_id, neighbour_threads +from .nodes import TensorAssignExpr, TensorInteger, TensorAdd, TensorMul, TensorMax +from .nodes import IntDivNode, AddNode, MulNode, EqNode, IfNode +from .nodes import GlobalThreadStarts, GlobalThreadEnds, GlobalThreadSizes, LocalThreadStarts, LocalThreadEnds +from .nodes import Allocate, Array +from .nodes import AndNode, StrictLessThanNode, WhileLoop, NotNode +from .nodes import GlobalThreadStarts, GlobalThreadEnds, GlobalThreadSizes +from .nodes import Allocate, Array +from .nodes import Block, ParallelBlock +from .utilities import get_max_partial_derivatives + +from psydac.api.ast.utilities import variables +from psydac.api.utilities import flatten +from psydac.linalg.block import BlockVectorSpace +from psydac.fem.vector import VectorFemSpace + +#============================================================================== +def toInteger(a): + if isinstance(a,(int, np.int64)): + return Integer(int(a)) + return a + +#============================================================================== +def convert(dtype): + """ + This function returns the index of a Function Space in a 3D DeRham sequence + + """ + if isinstance(dtype, (H1SpaceType, UndefinedSpaceType)): + return 0 + elif isinstance(dtype, HcurlSpaceType): + return 1 + elif isinstance(dtype, HdivSpaceType): + return 2 + elif isinstance(dtype, L2SpaceType): + return 3 + +#============================================================================== +def compute_diag_len(p, md, mc): + n = ((np.ceil((p+1)/mc)-1)*md).astype('int') + n = n-np.minimum(0, n-p)+p+1 + return n.astype('int') + +#============================================================================== +def regroup(tests): + """ + This function regourps the test/trial functions by their Function Space + + """ + tests = [i.base if isinstance(i, IndexedVectorFunction) else i for i in tests] + new_tests = [] + for i in tests: + if i not in new_tests: + new_tests.append(i) + tests = new_tests + + spaces = [i.space for i in tests] + kinds = [i.kind for i in spaces] + funcs = dict(zip(tests, kinds)) + funcs = sorted(funcs.items(), key=lambda x:convert(x[1])) + grs = [dict(g) for k,g in groupby(funcs,key=lambda x:convert(x[1]))] + grs = [(list(g.values())[0],tuple(g.keys())) for g in grs] + groups = [] + for d,g in grs: + if isinstance(d, (HcurlSpaceType, HdivSpaceType)) and isinstance(g[0], VectorFunction): + dim = g[0].space.ldim + for i in range(dim): + s = [u[i] for u in g] + groups += [(d,tuple(s))] + else: + groups += [(d,g)] + return groups + +#============================================================================== +def expand(args): + """ + This function expands vector functions into indexed functions + + """ + new_args = [] + for i in args: + if isinstance(i, (ScalarFunction, IndexedVectorFunction)): + new_args += [i] + elif isinstance(i, VectorFunction): + new_args += [i[k] for k in range(i.space.ldim)] + else: + raise NotImplementedError("TODO") + return tuple(new_args) + +#============================================================================== +class DefNode(Basic): + """ + DefNode represents a function definition where it contains the arguments and the body + + """ + def __new__(cls, name, arguments, local_variables, body, imports, results, kind, domain_dtype='real'): + obj = Basic.__new__(cls, name, arguments, local_variables, body, imports, results, kind) + obj._domain_dtype=domain_dtype + return obj + + @property + def name(self): + return self._args[0] + + @property + def arguments(self): + return self._args[1] + + @property + def local_variables(self): + return self._args[2] + + @property + def body(self): + return self._args[3] + + @property + def imports(self): + return self._args[4] + + @property + def results(self): + return self._args[5] + + @property + def kind(self): + return self._args[6] + + @property + def domain_dtype(self): + ''' + This property is used when we create the type of a constant for pyccel in build_pyccel_types_decorator. + ''' + return self._domain_dtype + + +#============================================================================== +def expand_hdiv_hcurl(args): + """ + This function expands vector functions of type hdiv and hculr into indexed functions + """ + new_args = [] + for i,a in enumerate(args): + if isinstance(a, ScalarFunction): + new_args += [a] + elif isinstance(a, VectorFunction): + if isinstance(a.space.kind, (HcurlSpaceType, HdivSpaceType)): + new_args += [a[k] for k in range(a.space.ldim)] + else: + new_args += [a] + else: + raise NotImplementedError("TODO") + + return tuple(new_args) + +#============================================================================== +def get_multiplicity(funcs, space): + def recursive_func(space): + if isinstance(space, BlockVectorSpace): + multiplicity = [recursive_func(s) for s in space.spaces] + else: + multiplicity = list(space.shifts) + return multiplicity + + multiplicity = recursive_func(space) + if not isinstance(multiplicity[0], list): + multiplicity = [multiplicity] + + funcs = expand(funcs) + assert len(funcs) == len(multiplicity) + new_multiplicity = [] + for i in range(len(funcs)): + if isinstance(funcs[i], ScalarFunction): + new_multiplicity.append(multiplicity[i]) + elif isinstance(funcs[i].base.space.kind, (HcurlSpaceType, HdivSpaceType)): + new_multiplicity.append(multiplicity[i]) + else: + if i+1==len(funcs) or isinstance(funcs[i+1], ScalarFunction) or funcs[i].base != funcs[i+1].base: + new_multiplicity.append(multiplicity[i]) + return new_multiplicity + +#============================================================================== +def get_degrees(funcs, space): + degrees = list(space.degree) + if not isinstance(degrees[0], (list, tuple)): + degrees = [degrees] + + funcs = expand(funcs) + assert len(funcs) == len(degrees) + new_degrees = [] + for i in range(len(funcs)): + if isinstance(funcs[i], ScalarFunction): + new_degrees.append(degrees[i]) + elif isinstance(funcs[i].base.space.kind, (HcurlSpaceType, HdivSpaceType)): + new_degrees.append(degrees[i]) + else: + if i+1==len(funcs) or isinstance(funcs[i+1], ScalarFunction) or funcs[i].base != funcs[i+1].base: + new_degrees.append(degrees[i]) + return new_degrees + +#============================================================================== +class AST(object): + """ + The AST class transforms a terminal expression returned from SymPDE + into a DefNode object, which it stores into the attribute `expr`. + + A DefNode represents a function definition, and it contains the full + PSYDAC abstract syntax tree for the assembly function of a BilinearForm, + LinearForm, or Functional. + + """ + def __init__(self, expr, terminal_expr, spaces, *, nquads, mapping_space=None, tag=None, mapping=None, is_rational_mapping=None, + num_threads=1, **kwargs): + # ... compute terminal expr + # TODO check that we have one single domain/interface/boundary + + is_bilinear = False + is_linear = False + is_functional = False + tests = () + trials = () + multiplicity_tests = () + multiplicity_trials = () + multiplicity_fields = () + tests_degrees = () + trials_degrees = () + fields_degrees = () + # ... + domain = terminal_expr.target + dim = domain.dim + constants = expr.constants + mask = None + nquads = tuple(nquads) + + # Define mask for different domain + if isinstance(domain, Boundary): + mask = Mask(domain.axis, domain.ext) + + elif isinstance(domain, Interface): + mask = Mask(domain.axis, None) + is_trial = {} + if isinstance(terminal_expr.trial, PlusInterfaceOperator): + is_trial[domain.plus] = True + is_trial[domain.minus] = False + else: + is_trial[domain.plus] = False + is_trial[domain.minus] = True + + kwargs["is_trial"] = is_trial + + # Define variables for different form + if isinstance(expr, LinearForm): + is_linear = True + tests = expr.test_functions + fields = expr.fields + is_broken = spaces.symbolic_space.is_broken + tests_degrees = get_degrees(tests, spaces) + multiplicity_tests = get_multiplicity(tests, spaces.coeff_space) + is_parallel = spaces.coeff_space.parallel + spaces = spaces.symbolic_space + + # Define the type of scalar that the code should manage + dtype = spaces.codomain_type if hasattr(spaces, 'codomain_type') else 'real' + + elif isinstance(expr, BilinearForm): + is_bilinear = True + tests = expr.test_functions + trials = expr.trial_functions + atoms = terminal_expr.expr.atoms(ScalarFunction, VectorFunction) + fields = tuple(i for i in atoms if i not in tests+trials) + is_broken = spaces[1].symbolic_space.is_broken + tests_degrees = get_degrees(tests, spaces[1]) + trials_degrees = get_degrees(trials, spaces[0]) + multiplicity_tests = get_multiplicity(tests, spaces[1].coeff_space) + multiplicity_trials = get_multiplicity(trials, spaces[0].coeff_space) + is_parallel = spaces[1].coeff_space.parallel + spaces = [V.symbolic_space for V in spaces] + + # Define the type of scalar that the code should manage + if hasattr(spaces[0], 'codomain_type'): + # TODO uncomment this line when we have a SesquilinearForm define in SymPDE + #assert isinstance(expr, SesquilinearForm) + dtype = spaces[0].codomain_type + else: + # TODO uncomment this line when we have a SesquilinearForm define in SymPDE + #assert not isinstance(expr, SesquilinearForm) + dtype = 'real' + + elif isinstance(expr, Functional): + is_functional = True + fields = tuple(expr.atoms(ScalarFunction, VectorFunction)) + is_broken = spaces.symbolic_space.is_broken + fields_degrees = get_degrees(fields, spaces) + multiplicity_fields = get_multiplicity(fields, spaces.coeff_space) + is_parallel = spaces.coeff_space.parallel + spaces = spaces.symbolic_space + + # Define the type of scalar that the code should manage + dtype = spaces.codomain_type if hasattr(spaces, 'codomain_type') else 'real' + + else: + raise NotImplementedError('TODO') + + tests = expand_hdiv_hcurl(tests) + trials = expand_hdiv_hcurl(trials) + fields = expand_hdiv_hcurl(fields) + kwargs['nquads'] = nquads + atoms_types = (ScalarFunction, VectorFunction, IndexedVectorFunction) + nderiv = 0 + terminal_expr = terminal_expr.expr + + if isinstance(terminal_expr, (ImmutableDenseMatrix, Matrix)): + n_rows, n_cols = terminal_expr.shape + atomic_expr_field = {f:[] for f in fields} + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + atoms = _atomic(terminal_expr[i_row, i_col], cls=atoms_types+_logical_partial_derivatives) + #-------------------------------------------------------------------- + # TODO [YG, 05.02.2021]: create 'get_test_function' and use it below: +# field_atoms = [a for a in atoms if get_test_function(a) in fields] + field_atoms = [] + #-------------------------------------------------------------------- + for f in field_atoms: + a = _atomic(f, cls=atoms_types) + assert len(a) == 1 + atomic_expr_field[a[0]].append(f) + + Fs = [get_atom_logical_derivatives(a) for a in atoms] + d = get_max_partial_derivatives(terminal_expr[i_row,i_col], logical=True, F=Fs) + nderiv = max(nderiv, max(d.values())) + + else: + atoms = _atomic(terminal_expr, cls=atoms_types+_logical_partial_derivatives) + #-------------------------------------------------------------------- + # TODO [YG, 05.02.2021]: create 'get_test_function' and use it below: +# field_atoms = [a for a in atoms if get_test_function(a) in fields] + field_atoms = [] + #-------------------------------------------------------------------- + atomic_expr_field = {f:[] for f in fields} + for f in field_atoms: + a = _atomic(f, cls=atoms_types) + assert len(a) == 1 + atomic_expr_field[a[0]].append(f) + + Fs = [get_atom_logical_derivatives(a) for a in atoms] + d = get_max_partial_derivatives(terminal_expr, logical=True, F=Fs) + nderiv = max(nderiv, max(d.values())) + + terminal_expr = Matrix([[terminal_expr]]) + + d_tests = {v: {'global': GlobalTensorQuadratureTestBasis(v), + 'local': LocalTensorQuadratureTestBasis(v), + 'span': GlobalSpanArray(v), + 'local_span': LocalSpanArray(v), + 'multiplicity': multiplicity_tests[i], + 'degrees': tests_degrees[i], + 'thread_span': GlobalThreadSpanArray(v)} for i,v in enumerate(tests) } + + d_trials = {u: {'global': GlobalTensorQuadratureTrialBasis(u), + 'local': LocalTensorQuadratureTrialBasis(u), + 'span': GlobalSpanArray(u), + 'local_span': LocalSpanArray(u), + 'multiplicity': multiplicity_trials[i], + 'degrees': trials_degrees[i]} for i,u in enumerate(trials)} + + if isinstance(expr, Functional): + d_fields = {f: {'global': GlobalTensorQuadratureTestBasis(f), + 'local': LocalTensorQuadratureTestBasis(f), + 'span': GlobalSpanArray(f), + 'local_span': LocalSpanArray(f), + 'multiplicity': multiplicity_fields[i], + 'degrees': fields_degrees[i]} for i,f in enumerate(fields)} + + else: + d_fields = {f: {'global': GlobalTensorQuadratureTestBasis (f), + 'local' : LocalTensorQuadratureTestBasis(f), + 'span': GlobalSpanArray(f), + 'local_span': LocalSpanArray(f)} for i,f in enumerate(fields)} + + if mapping_space: + f = (tests+trials+fields)[0] + f = f.base if isinstance(f, IndexedVectorFunction) else f + if isinstance(domain, Interface): + f_m = f.duplicate('mapping_'+f.name) + f_m = expand([f_m])[0] + f_p = f.duplicate('mapping_plus_'+f.name) + f_p = expand([f_p])[0] + f = (f_m, f_p) + + mapping_degrees_m = get_degrees([f_m], mapping_space[0]) + mapping_degrees_p = get_degrees([f_p], mapping_space[1]) + multiplicity_mapping_m = get_multiplicity([f_m], mapping_space[0].coeff_space) + multiplicity_mapping_p = get_multiplicity([f_p], mapping_space[1].coeff_space) + mapping_degrees = (mapping_degrees_m, mapping_degrees_p) + multiplicity_mapping = (multiplicity_mapping_m, multiplicity_mapping_p) + else: + f = f.duplicate('mapping_'+f.name) + f = expand([f])[0] + f = (f,) + mapping_degrees = (get_degrees(f, mapping_space),) + multiplicity_mapping = (get_multiplicity(f, mapping_space.coeff_space),) + + d_mapping = {fi: {'global': GlobalTensorQuadratureTestBasis (fi), + 'local' : LocalTensorQuadratureTestBasis(fi), + 'span': GlobalSpanArray(fi), + 'local_span': LocalSpanArray(fi), + 'multiplicity': multiplicity_mapping_i[0], + 'degrees': mapping_degrees_i[0]} + for fi,mapping_degrees_i,multiplicity_mapping_i in zip(f,mapping_degrees,multiplicity_mapping) } + else: + d_mapping = {} + + if is_broken: + if isinstance(domain, Interface): + if mapping is None: + mapping_minus = IdentityMapping('M_{}'.format(domain.minus.domain.name), dim) + mapping_plus = IdentityMapping('M_{}'.format(domain.plus.domain.name), dim) + else: + mapping_minus = mapping.mappings[domain.minus.domain] + mapping_plus = mapping.mappings[domain.plus.domain] + + mapping = InterfaceMapping(mapping_minus, mapping_plus) + elif isinstance(domain, Boundary) and mapping: + mapping = mapping.mappings[domain.domain] + elif mapping: + mapping = mapping.mappings[domain] + + if mapping is None: + if isinstance(domain, Boundary): + name = domain.domain.name + else: + name = domain.name + mapping = IdentityMapping('M_{}'.format(name), dim) + + invert_quad_loop = False + if mapping_space or (mapping.is_analytical and mapping.jacobian_expr.atoms(Symbol)): + invert_quad_loop = True + + if is_linear: + ast = _create_ast_linear_form(domain, terminal_expr, atomic_expr_field, tests, d_tests, fields, d_fields, constants, + nderiv, dtype, mapping, d_mapping, is_rational_mapping, mapping_space, + mask, tag, num_threads, invert_quad_loop, **kwargs) + + elif is_bilinear: + ast = _create_ast_bilinear_form(domain, terminal_expr, atomic_expr_field, tests, d_tests, trials, d_trials, + fields, d_fields, constants, nderiv, dtype, mapping, + d_mapping, is_rational_mapping, mapping_space, mask, tag, is_parallel, + num_threads, invert_quad_loop, **kwargs) + elif is_functional: + ast = _create_ast_functional_form(domain, terminal_expr, atomic_expr_field, fields, d_fields, constants, nderiv, + dtype, mapping, d_mapping, is_rational_mapping, mapping_space, + mask, tag, num_threads, **kwargs) + else: + raise NotImplementedError('TODO') + # ... + + self._expr = ast + self._nderiv = nderiv + self._domain = domain + self._mapping = mapping + self._num_threads = num_threads + + @property + def expr(self): + return self._expr + + @property + def nderiv(self): + return self._nderiv + + @property + def domain(self): + return self._domain + + @property + def mapping(self): + return self._mapping + + @property + def dim(self): + return self.domain.dim + + @property + def num_threads(self): + return self._num_threads + +#============================================================================== +def _create_ast_bilinear_form(domain, terminal_expr, atomic_expr_field, tests, d_tests, trials, d_trials, fields, d_fields, + constants, nderiv, dtype, mapping, d_mapping, is_rational_mapping, mapping_space, + mask, tag, is_parallel, num_threads, invert_quad_loop, **kwargs): + + """ + This function creates the assembly function of a bilinear form in the real case + or of a sesquilinear form in complex case. + + Parameters + ---------- + + domain : + Sympde Domain object + + terminal_expr : + atomic representation of the bilinear/sesquilinear form + + atomic_expr_field: + dict of atomic expressions of fields + + tests : + list of tests functions + + d_tests : + dictionary that contains the symbolic spans and basis values of each test function + + trials : + list of trial functions + + d_trials: + dictionary that contains the symbolic spans and basis values of each trial function + + fields : + list of fields + + constants : + list of constants + + nderiv : int + the order of the bilinear/sesquilinear form + + dtype : str + type of data 'complex' or 'float' + + mapping : + Sympde Mapping object + + d_mapping : + dictionary that contains the symbolic spans and basis values of the mapping + + is_rational_mapping : + takes the value of True if the mapping is rational + + mask : + the masked direction in case of boundary domain + + tag : + tag to be added to variable names + + is_parallel : + True if the domain is distributed + + num_threads : + Number of threads + + invert_quad_loop : + Invert the quadrature loop if True + + Returns + ------- + node : DefNode + represents the function definition node that computes the assembly + + """ + + # Create flags for parallel case + + dim = domain.dim + backend = kwargs.pop('backend') + is_pyccel = backend['name'] == 'pyccel' if backend else False + add_openmp = is_pyccel and backend['openmp'] and num_threads>1 + + pads = variables(('pad1, pad2, pad3'), dtype='int')[:dim] + g_quad = [GlobalTensorQuadratureGrid(False)] + l_quad = [LocalTensorQuadratureGrid(False)] + + if isinstance(domain, Interface): + g_quad.append(PlusGlobalTensorQuadratureGrid(False)) + l_quad.append(PlusLocalTensorQuadratureGrid(False)) + + rank_from_coords = MatrixRankFromCoords() + coords_from_rank = MatrixCoordsFromRank() + + nquads = kwargs.pop('nquads', None) + thread_span = dict((u,d_tests[u]['thread_span']) for u in tests) + # ........................................................................................... + # Define name of span keys if we used OpenMP or not + if add_openmp: + span = 'local_span' + basis = 'local' + else: + span = 'span' + basis = 'global' + + g_span = dict((u, d_tests[u][span]) for u in tests) + f_span = dict((f, d_fields[f][span]) for f in fields) + + # Collect mapping span + if mapping_space: + m_span = dict((f, d_mapping[f][span]) for f in d_mapping) + else: + m_span = {} + + eval_mappings = [] + m_trials = dict((u,d_trials[u]['multiplicity']) for u in trials) + m_tests = dict((v,d_tests[v]['multiplicity']) for v in tests) + lengths_trials = dict((u,LengthDofTrial(u)) for u in trials) + lengths_tests = dict((v,LengthDofTest(v)) for v in tests) + lengths_fields = dict((f,LengthDofTest(f)) for f in fields) + + # Those dictionaries were defined but never used + # m_trials = dict((u,d_trials[u]['multiplicity']) for u in trials) + # lengths_outer_tests = dict((v,LengthOuterDofTest(v)) for v in tests) + # lengths_inner_tests = dict((v,LengthInnerDofTest(v)) for v in tests) + + # ........................................................................................... + quad_length = LengthQuadrature() + el_length = LengthElement() + global_thread_s = GlobalThreadStarts() + global_thread_e = GlobalThreadEnds() + global_thread_l = GlobalThreadSizes() + local_thread_s = LocalThreadStarts() + local_thread_e = LocalThreadEnds() + lengths = [el_length, quad_length] + + # ........................................................................................... + # Get the Geometry expression from the mapping + if isinstance(domain, Interface): + geos = [GeometryExpressions(mapping.minus, nderiv), GeometryExpressions(mapping.plus, nderiv)] + else: + geos = [GeometryExpressions(mapping, nderiv)] + + # Define the global and local matrices + g_coeffs = {f:[MatrixGlobalBasis(i, i, dtype=dtype) for i in expand([f])] for f in fields} #dtype manage the initialization at 0 + l_mats = BlockStencilMatrixLocalBasis(trials, tests, terminal_expr, dim, tag, dtype=dtype) #dtype manage the reset at 0 + g_mats = BlockStencilMatrixGlobalBasis(trials, tests, pads, m_tests, terminal_expr, l_mats.tag, dtype=dtype) #dtype manage the decorators type in pyccel + # ........................................................................................... + + if nquads is not None: + ind_quad = index_quad.set_range(stop=Tuple(*nquads)) + else: + ind_quad = index_quad.set_range(stop=quad_length) + + # g_starts = Tuple(*[ProductGenerator(global_thread_s.set_index(i), thread_coords.set_index(i)) for i in range(dim)]) + # g_ends = Tuple(*[AddNode(ProductGenerator(global_thread_e.set_index(i), thread_coords.set_index(i)), Integer(1)) for i in range(dim)]) + l_starts = Tuple(*[ProductGenerator(local_thread_s.set_index(i), local_index_element.set_index(i)) for i in range(dim)]) + l_ends = Tuple(*[ProductGenerator(local_thread_e.set_index(i), local_index_element.set_index(i)) for i in range(dim)]) + + #ind_element = index_element.set_range(start=g_starts,stop=g_ends) if add_openmp else index_element.set_range(stop=el_length) + ind_element = index_element.set_range(start=l_starts,stop=l_ends) if add_openmp else index_element.set_range(stop=el_length) + l_ind_element = local_index_element.set_range(stop=TensorInteger(2)) + + # Create mapping loop if the user give a mapping of an interface + if mapping_space and isinstance(domain, Interface): + is_trial = (kwargs["is_trial"][domain.minus], kwargs["is_trial"][domain.plus]) + mappings = (mapping.minus, mapping.plus) + ind_dof_tests = [index_dof_test.set_range(stop=Tuple(*[d+1 for d in d_mapping[f]['degrees']])) for f in d_mapping] + # ........................................................................................... + eval_mappings = [EvalMapping(domain, ind_quad, ind_dof_tests[i], d_mapping[fi][basis], + mappings[i], geos[i], mapping_space[i], nderiv, mask, + is_rational_mapping[i], trial=is_trial[i], quad_loop=(not invert_quad_loop)) for i,fi in enumerate(d_mapping)] + + # Create mapping loop if the user give a mapping of a domain + elif mapping_space: + ind_dof_tests = [index_dof_test.set_range(stop=Tuple(*[d+1 for d in d_mapping[f]['degrees']])) for f in d_mapping] + # ........................................................................................... + eval_mappings = [EvalMapping(domain, ind_quad, ind_dof_tests[i], d_mapping[fi][basis], + mapping, geos[i], mapping_space, nderiv, mask, is_rational_mapping, + quad_loop=(not invert_quad_loop)) for i,fi in enumerate(d_mapping)] + + + # Create Evaluating loop for each field + eval_fields = [] + for f in fields: + f_ex = expand([f]) + coeffs = [CoefficientBasis(i) for i in f_ex] + l_coeffs = [MatrixLocalBasis(i, dtype=dtype) for i in f_ex] #dtype manage the initialization at 0 in the evaluating loop + ind_dof_test = index_dof_test.set_range(stop=lengths_fields[f]+1) + eval_field = EvalField(domain, atomic_expr_field[f], ind_quad, ind_dof_test, d_fields[f][basis], + coeffs, l_coeffs, g_coeffs[f], [f], mapping, nderiv, mask, dtype=dtype, quad_loop=(not invert_quad_loop)) + + eval_fields += [eval_field] + + # Add the Mapping loop into the geometric statements if there is one + g_stmts = [] + if mapping_space: + g_stmts = g_stmts + eval_mappings + + # Add the Evaluating loop into the geometric statements + g_stmts += [*eval_fields] + g_stmts_texpr = [] + + # sort tests and trials by their space type + test_groups = regroup(tests) + trial_groups = regroup(trials) + + # expand every VectorFunction into IndexedVectorFunctions + ex_tests = expand(tests) + ex_trials = expand(trials) + + #=========================================================begin kernel====================================================== + for _, sub_tests in test_groups: + for _, sub_trials in trial_groups: + tests_indices = [ex_tests.index(i) for i in expand(sub_tests)] + trials_indices = [ex_trials.index(i) for i in expand(sub_trials)] + sub_terminal_expr = terminal_expr[tests_indices,trials_indices] + + if is_zero(sub_terminal_expr): + continue + + q_basis_tests = dict((v, d_tests[v][basis]) for v in sub_tests) + q_basis_trials = dict((u, d_trials[u][basis]) for u in sub_trials) + m_tests = dict((v, d_tests[v]['multiplicity']) for v in sub_tests) + m_trials = dict((u, d_trials[u]['multiplicity']) for u in sub_trials) + tests_degree = dict((v, d_tests[v]['degrees']) for v in sub_tests) + trials_degrees = dict((u, d_trials[u]['degrees']) for u in sub_trials) + bs = dict() + es = dict() + for v in sub_tests: + # v_str = str(SymbolicExpr(v)) +# bs[v] = variables(('b_{v}_1, b_{v}_2, b_{v}_3'.format(v=v_str)), dtype='int')[:dim] if is_parallel else [S.Zero]*dim +# es[v] = variables(('e_{v}_1, e_{v}_2, e_{v}_3'.format(v=v_str)), dtype='int')[:dim] if is_parallel else [S.Zero]*dim + bs[v] = [S.Zero]*dim + es[v] = [S.Zero]*dim + +# bs and es contain the starts and the ends of the test function loops. +# This was an optimization when we had the ghost elements and it is not needed after removing them. +# They are not deleted because we can still use them when the communications take more time than the calculations, +# In that case, we can disable the communications and put back the ghost elements. +# Usualy, the communications take more time than the calculations when the degrees are small like 1 or 2, +# and/or the number of processes is really big which makes the elements owned by a process really small and the calculations really fast. +# This optimization can be added after improving the communications. + + if all(a==1 for a in m_tests[sub_tests[0]]+m_trials[sub_trials[0]]): + stmts = [] + for v in sub_tests+sub_trials: + stmts += construct_logical_expressions(v, nderiv) + + l_sub_mats = BlockStencilMatrixLocalBasis(sub_trials, sub_tests, sub_terminal_expr, dim, l_mats.tag, + tests_degree=tests_degree, trials_degree=trials_degrees, + tests_multiplicity=m_tests, trials_multiplicity=m_trials, + dtype=dtype) + l_sub_scalars = BlockScalarLocalBasis(trials = sub_trials, tests=sub_tests, expr=sub_terminal_expr, + tag=l_mats.tag, dtype=dtype) + + if invert_quad_loop: + + # ... loop over trials + length = Tuple(*[d+1 for d in trials_degrees[sub_trials[0]]]) + ind_dof_trial = index_dof_trial.set_range(stop=length) + stmts.append(Reduction(None,ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars))) + trials_loop = Loop((*q_basis_tests.values(), *q_basis_trials.values()), ind_dof_trial, + stmts=[*stmts, VectorAssign(ElementOf(l_sub_mats), ElementOf(l_sub_scalars),'+')]) + + # ... loop over tests + length = Tuple(*[d+1 for d in tests_degree[sub_tests[0]]]) + ends = Tuple(*[d+1-e for d,e in zip(tests_degree[sub_tests[0]], es[sub_tests[0]])]) + starts = Tuple(*bs[sub_tests[0]]) + ind_dof_test = index_dof_test.set_range(start=starts, stop=ends, length=length) + tests_loop = Loop((), ind_dof_test, stmts=[trials_loop]) + else: + # Instructions needed to retrieve the precomputed values of the + # fields (and their derivatives) at a single quadrature point + stmts += flatten([eval_field.inits for eval_field in eval_fields]) + + quadrature_loop = Loop((*l_quad, *q_basis_tests.values(), *q_basis_trials.values(), *geos), ind_quad, stmts=stmts, mask=mask) + reduced_quadrature_loop = Reduce('+', ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars), quadrature_loop) + + # ... loop over trials + length = Tuple(*[d+1 for d in trials_degrees[sub_trials[0]]]) + ind_dof_trial = index_dof_trial.set_range(stop=length) + trials_loop = Loop((), ind_dof_trial, stmts=[Reset(l_sub_scalars),reduced_quadrature_loop, VectorAssign(ElementOf(l_sub_mats), ElementOf(l_sub_scalars))]) + + # ... loop over tests + length = Tuple(*[d+1 for d in tests_degree[sub_tests[0]]]) + ends = Tuple(*[d+1-e for d,e in zip(tests_degree[sub_tests[0]], es[sub_tests[0]])]) + starts = Tuple(*bs[sub_tests[0]]) + ind_dof_test = index_dof_test.set_range(start=starts, stop=ends, length=length) + tests_loop = Loop((), ind_dof_test, stmts=[trials_loop]) + + body = (tests_loop,) + stmts = Block(body) + g_stmts += [stmts] + + # This part of the code was never used and has no impact on the result of this function. +# if is_parallel: +# ln = Tuple(*[d-1 for d in tests_degree[sub_tests[0]]]) +# thr_s = Tuple(*[ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) for i in range(dim)]) if add_openmp else Tuple(*[0]*dim) +# start_expr = TensorMax(TensorMul(TensorAdd(TensorMul(TensorAdd(thr_s,ind_element), +# Tuple(*[-1]*dim)), ln), Tuple(*[S.Zero]*dim)),Tuple(*[S.Zero]*dim)) +# +# start_expr = TensorAssignExpr(Tuple(*bs[sub_tests[0]]), start_expr) +# end_expr = TensorMax(TensorMul(TensorAdd(TensorMul(Tuple(*[-1]*dim), el_length), +# TensorAdd(TensorAdd(thr_s,ind_element), +# Tuple(*tests_degree[sub_tests[0]]))), Tuple(*[S.Zero]*dim)), Tuple(*[S.Zero]*dim)) +# +# end_expr = TensorAssignExpr(Tuple(*es[sub_tests[0]]), end_expr) +# g_stmts_texpr += [start_expr, end_expr] + + else: + l_stmts = [] + mask_inner = [[False, True] for i in range(dim)] + for mask_inner_i in product(*mask_inner): + mask_inner_i = Tuple(*mask_inner_i) + not_mask_inner_i = Tuple(*[not i for i in mask_inner_i]) + stmts = [] + for v in sub_tests+sub_trials: + stmts += construct_logical_expressions(v, nderiv) + + multiplicity = Tuple(*m_tests[sub_tests[0]]) + length = Tuple(*[(d+1) % m if T else (d+1)//m for d, m, T in zip(tests_degree[sub_tests[0]], multiplicity, mask_inner_i)]) + ind_outer_dof_test = index_outer_dof_test.set_range(stop=length) + outer = Tuple(*[d//m for d, m in zip(tests_degree[sub_tests[0]], multiplicity)]) + outer = TensorAdd(TensorMul(ind_outer_dof_test, not_mask_inner_i), TensorMul(outer, mask_inner_i)) + + l_sub_mats = BlockStencilMatrixLocalBasis(sub_trials, sub_tests, sub_terminal_expr, dim, l_mats.tag, outer=outer, + tests_degree=tests_degree, trials_degree=trials_degrees, + tests_multiplicity=m_tests, trials_multiplicity=m_trials, dtype=dtype) + + l_sub_scalars = BlockScalarLocalBasis(trials = sub_trials, tests=sub_tests, expr=sub_terminal_expr, tag=l_mats.tag, dtype=dtype) + + if invert_quad_loop: + + # ... loop over trials + length_t = Tuple(*[d+1 for d in trials_degrees[sub_trials[0]]]) + ind_dof_trial = index_dof_trial.set_range(stop=length_t) + stmts.append(Reduction(None,ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars))) + trials_loop = Loop((*q_basis_tests.values(), *q_basis_trials.values()), ind_dof_trial, + stmts=[*stmts, VectorAssign(ElementOf(l_sub_mats), ElementOf(l_sub_scalars),'+')]) + + rem_length = Tuple(*[(d+1)-(d+1)%m for d,m in zip(tests_degree[sub_tests[0]], multiplicity)]) + ind_inner_dof_test = index_inner_dof_test.set_range(stop=multiplicity) + expr1 = TensorAdd(TensorMul(ind_outer_dof_test, multiplicity),ind_inner_dof_test) + expr2 = TensorAdd(rem_length, ind_outer_dof_test) + expr = TensorAssignExpr(index_dof_test, TensorAdd(TensorMul(expr1,not_mask_inner_i),TensorMul(expr2, mask_inner_i))) + + # ... loop over tests + tests_loop = Loop((expr,), ind_inner_dof_test, stmts=[trials_loop], mask=mask_inner_i) + tests_loop = Loop((), ind_outer_dof_test, stmts=[tests_loop]) + else: + + # Instructions needed to retrieve the precomputed values of the + # fields (and their derivatives) at a single quadrature point + stmts += flatten([eval_field.inits for eval_field in eval_fields]) + + quadrature_loop = Loop((*l_quad, *q_basis_tests.values(), *q_basis_trials.values(), *geos), ind_quad, stmts=stmts, mask=mask) + reduced_quadrature_loop = Reduce('+', ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars), quadrature_loop) + + # ... loop over trials + length_t = Tuple(*[d+1 for d in trials_degrees[sub_trials[0]]]) + ind_dof_trial = index_dof_trial.set_range(stop=length_t) + trials_loop = Loop((), ind_dof_trial, stmts=[Reset(l_sub_scalars), reduced_quadrature_loop, VectorAssign(ElementOf(l_sub_mats), ElementOf(l_sub_scalars))]) + + rem_length = Tuple(*[(d+1)-(d+1)%m for d,m in zip(tests_degree[sub_tests[0]], multiplicity)]) + ind_inner_dof_test = index_inner_dof_test.set_range(stop=multiplicity) + expr1 = TensorAdd(TensorMul(ind_outer_dof_test, multiplicity),ind_inner_dof_test) + expr2 = TensorAdd(rem_length, ind_outer_dof_test) + expr = TensorAssignExpr(index_dof_test, TensorAdd(TensorMul(expr1,not_mask_inner_i),TensorMul(expr2, mask_inner_i))) + + # ... loop over tests + tests_loop = Loop((expr,), ind_inner_dof_test, stmts=[trials_loop], mask=mask_inner_i) + tests_loop = Loop((), ind_outer_dof_test, stmts=[tests_loop]) + + l_stmts += [tests_loop] + + g_stmts += [*l_stmts] + + #=========================================================end kernel========================================================= + # Create the loop over global element code for OpenMP + if add_openmp: +# body = [VectorAssign(Tuple(*[ProductGenerator(thread_span[u].set_index(j), num_threads) for j in range(dim)]), +# Tuple(*[AddNode(2*pads[j],ProductGenerator(g_span[u].set_index(j), AddNode(el_length.set_index(j),Integer(-1)))) for j in range(dim)])) for u in thread_span] + + body = [] + parallel_body = [] + parallel_body += [Assign(thread_id, Function("omp_get_thread_num")())] + parallel_body += [VectorAssign(thread_coords, Tuple(*[ProductGenerator(coords_from_rank, Tuple((thread_id, i))) for i in range(dim)]))] + + for i in range(dim): + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + parallel_body += [Assign(global_thread_l.set_index(i), AddNode(AddNode(thr_e, Integer(1)), MulNode(Integer(-1),thr_s)))] + + for i in range(dim): + lhs = local_thread_s.set_index(i) + #thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + rhs = Array(Tuple(0, IntDivNode(global_thread_l.set_index(i), Integer(2)))) + parallel_body += [Assign(lhs, rhs)] + + for i in range(dim): + lhs = local_thread_e.set_index(i) + # thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + # thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + rhs = Array(Tuple(IntDivNode(global_thread_l.set_index(i), Integer(2)), global_thread_l.set_index(i))) + parallel_body += [Assign(lhs, rhs)] + + get_d = lambda v:d_tests[v]['degrees'] if v in d_tests else d_tests[v.base]['degrees'] + for i in range(dim): + parallel_body += [Allocate(d_tests[v]['local'].set_index(i), + (global_thread_l.set_index(i), + Integer(get_d(v)[i]+1), + Integer(nderiv+1), + Integer(nquads[i]))) for v in d_tests] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = tuple([Slice(None,None)]*4) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))), + Slice(None,None), + Slice(None,Integer(nderiv+1)), + Slice(None,None)) + + parallel_body += [Assign(ProductGenerator(d_tests[v]['local'].set_index(i), Tuple(args1)), + ProductGenerator(d_tests[v]['global'].set_index(i),Tuple(args2))) + for v in d_tests] + + get_d = lambda u:d_trials[u]['degrees'] if u in d_trials else d_trials[u.base]['degrees'] + for i in range(dim): + parallel_body += [Allocate(d_trials[u]['local'].set_index(i), + (global_thread_l.set_index(i), + Integer(get_d(u)[i]+1), + Integer(nderiv+1), + Integer(nquads[i]))) for u in d_trials] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = tuple([Slice(None,None)]*4) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))), + Slice(None,None), + Slice(None,Integer(nderiv+1)), + Slice(None,None)) + + parallel_body += [Assign(ProductGenerator(d_trials[u]['local'].set_index(i), Tuple(args1)), + ProductGenerator(d_trials[u]['global'].set_index(i),Tuple(args2))) + for u in d_trials] + + for i in range(dim): + parallel_body += [Allocate(d_tests[v]['local_span'].set_index(i), + (global_thread_l.set_index(i),)) for v in d_tests] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = (Slice(None,None),) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))),) + + parallel_body += [Assign(ProductGenerator(d_tests[v]['local_span'].set_index(i), Tuple(args1)), + ProductGenerator(d_tests[v]['span'].set_index(i),Tuple(args2))) + for v in d_tests] + + + get_d = lambda f:d_fields[f].get('degrees', [lengths_fields[f].set_index(ii) for ii in range(dim)]) if f in d_fields else\ + d_fields[f.base].get('degrees',[lengths_fields[f].set_index(ii) for ii in range(dim)]) + for i in range(dim): + parallel_body += [Allocate(d_fields[f]['local'].set_index(i), + (global_thread_l.set_index(i), + toInteger(get_d(f)[i]+1), + Integer(nderiv+1), + Integer(nquads[i]))) for f in d_fields] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = tuple([Slice(None,None)]*4) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))), + Slice(None,None), + Slice(None,Integer(nderiv+1)), + Slice(None,None)) + + parallel_body += [Assign(ProductGenerator(d_fields[f]['local'].set_index(i), Tuple(args1)), + ProductGenerator(d_fields[f]['global'].set_index(i),Tuple(args2))) + for f in d_fields] + + for i in range(dim): + parallel_body += [Allocate(d_fields[f]['local_span'].set_index(i), + (global_thread_l.set_index(i),)) for f in d_fields] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = (Slice(None,None),) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))),) + + parallel_body += [Assign(ProductGenerator(d_fields[f]['local_span'].set_index(i), Tuple(args1)), + ProductGenerator(d_fields[f]['span'].set_index(i),Tuple(args2))) + for f in d_fields] + if mapping_space: + get_d = lambda f:d_mapping[f]['degrees'] if f in d_mapping else d_mapping[f.base]['degrees'] + for i in range(dim): + parallel_body += [Allocate(d_mapping[f]['local'].set_index(i), + (global_thread_l.set_index(i), + Integer(get_d(f)[i]+1), + Integer(nderiv+1), + Integer(nquads[i]))) for f in d_mapping] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = tuple([Slice(None,None)]*4) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))), + Slice(None,None), + Slice(None,Integer(nderiv+1)), + Slice(None,None)) + + parallel_body += [Assign(ProductGenerator(d_mapping[f]['local'].set_index(i), Tuple(args1)), + ProductGenerator(d_mapping[f]['global'].set_index(i),Tuple(args2))) + for f in d_mapping] + + for i in range(dim): + parallel_body += [Allocate(d_mapping[f]['local_span'].set_index(i), + (global_thread_l.set_index(i),)) for f in d_mapping] + + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + args1 = (Slice(None,None),) + args2 = (Slice(thr_s, AddNode(thr_e, Integer(1))),) + + parallel_body += [Assign(ProductGenerator(d_mapping[f]['local_span'].set_index(i), Tuple(args1)), + ProductGenerator(d_mapping[f]['span'].set_index(i),Tuple(args2))) + for f in d_mapping] + # for i in range(dim): +# parallel_body += [Assign(neighbour_threads.set_index(i), ProductGenerator(rank_from_coords, +# Tuple(tuple(AddNode(thread_coords.set_index(j), Integer(i==j)) for j in range(dim)))))] + +# for u in thread_span: +# expr1 = [Span(u, index=i) for i in range(dim)] +# expr2 = [ProductGenerator(thread_span[u].set_index(i),AddNode(neighbour_threads.set_index(i),Min(Integer(0), thread_id.length))) for i in range(dim)] +# g_stmts += [WhileLoop(NotNode(AndNode(*[StrictLessThanNode(AddNode(p, e1), e2) for p,e1,e2 in zip(pads, expr1, expr2)])), [Assign(thread_id.length, Min(Integer(100), AddNode(thread_id.length,Integer(1))))])] +# lhs = [ProductGenerator(thread_span[u].set_index(i), Tuple(thread_id)) for i in range(dim)] +# rhs = TensorAdd(Tuple(*expr1), Tuple(*[Integer(0)]*dim)) +# g_stmts_texpr += [TensorAssignExpr(Tuple(*lhs), rhs)] + + if invert_quad_loop: + # ... loop over the quadrature points + loop = Loop((*l_quad,), ind_quad, stmts=g_stmts, mask=mask) + g_stmts = [Reset(l_mats), *[em.inits for em in eval_mappings], *[em.inits for em in eval_fields], loop] + else: + g_stmts = [*[em.inits for em in eval_mappings], *g_stmts] + + #... loop over global elements + global_loop = Loop((*g_quad, *g_span.values(), *m_span.values(), *f_span.values(), *g_stmts_texpr), + ind_element, stmts=g_stmts, mask=mask) + + global_loop_reduction = Reduce('+', l_mats, g_mats, global_loop) + global_loop = Loop((), l_ind_element, stmts=[Comment('#$omp barrier'), global_loop_reduction], mask=mask) + + if mask is not None: + empty_loop = Loop((), l_ind_element, stmts=[Comment('#$omp barrier'), Continue()], mask=mask) + global_loop = IfNode((EqNode(thread_coords.set_index(mask.axis), Integer(0)),[global_loop]),(true, [empty_loop])) + + parallel_body += [global_loop] +# parallel_body += [VectorAssign(Tuple(*[ProductGenerator(thread_span[u].set_index(j), thread_id) for j in range(dim)]), +# Tuple(*[AddNode(2*pads[j],ProductGenerator(g_span[u].set_index(j), AddNode(el_length.set_index(j),Integer(-1)))) for j in range(dim)])) for u in thread_span] + # Create the loop over global element code if we don't use OpenMP + else: + + if invert_quad_loop: + # ... loop over the quadrature points + loop = Loop((*l_quad,), ind_quad, stmts=g_stmts, mask=mask) + g_stmts = [Reset(l_mats), *[em.inits for em in eval_fields], *[em.inits for em in eval_mappings], loop] + else: + g_stmts = [*[em.inits for em in eval_mappings], *g_stmts] + + # ... loop over global elements + global_loop = Loop((*g_quad, *g_span.values(), *m_span.values(), *f_span.values(), *g_stmts_texpr), + ind_element, stmts=g_stmts, mask=mask) + + body = [Reduce('+', l_mats, g_mats, global_loop)] + + # ... + args = {} + args['tests_basis'] = tuple(d_tests[v]['global'] for v in tests) + args['trial_basis'] = tuple(d_trials[u]['global'] for u in trials) + args['spans'] = tuple(d_tests[v]['span'] for v in tests) + args['quads'] = g_quad + args['tests_degrees'] = lengths_tests + args['trials_degrees'] = lengths_trials + args['quads_degree'] = lengths + args['global_pads'] = pads + args['local_pads'] = Pads(tests, trials) + args['mats'] = [g_mats] + + if add_openmp: + args['thread_args'] = (coords_from_rank, rank_from_coords, global_thread_s, global_thread_e, thread_id.length) + + # Collect fields parameters if there is one + if mapping_space: + args['mapping'] = flatten([eval_mapping.coeffs for eval_mapping in eval_mappings]) + args['mapping_degrees'] = [LengthDofTest(f) for f in d_mapping] + args['mapping_basis'] = flatten([d_mapping[f]['global'] for f in d_mapping]) + args['mapping_spans'] = flatten([d_mapping[f]['span'] for f in d_mapping]) + + + # Collect fields parameters if there is one + if fields: + args['f_span'] = tuple(d_fields[f]['span'] for f in fields) + args['f_coeffs'] = flatten(list(g_coeffs.values())) + args['field_basis'] = tuple(d_fields[f]['global'] for f in fields) + args['fields_degrees'] = lengths_fields.values() + args['f_pads'] = [f.pads for f in eval_fields] + fields = tuple(f.base if isinstance(f, IndexedVectorFunction) else f for f in fields) + args['fields'] = tuple(dict.fromkeys(fields)) + + # Collect constants if there is one in the equation + if constants: + args['constants'] = constants + +# args['starts'] = b0s +# args['ends'] = e0s + + # Add the allocation for the OpenMP arguments initialization + allocations = [] + if add_openmp: + allocations = [[Allocate(thread_span[u].set_index(i), (Integer(1+num_threads),)) for i in range(dim)] for u in thread_span] + allocations = [Tuple(*i) for i in allocations] + + body = allocations + body + + # Those dictionaries were defined but never used + # m_trials = dict((u,d_trials[u]['multiplicity']) for u in trials) + # m_tests = dict((v,d_tests[v]['multiplicity']) for v in tests) + # trials_degree = dict((u,d_trials[u]['degrees']) for u in trials) + # tests_degree = dict((v,d_tests[v]['degrees']) for v in tests) + + local_allocations = [] + for j,u in enumerate(ex_trials): + for i,v in enumerate(ex_tests): + if terminal_expr[i,j] == 0: + continue + td = d_tests[v]['degrees'] if v in d_tests else d_tests[v.base]['degrees'] + trd = d_trials[u]['degrees'] if u in d_trials else d_trials[u.base]['degrees'] + tm = d_tests[v]['multiplicity'] if v in d_tests else d_tests[v.base]['multiplicity'] + trm = d_trials[u]['multiplicity'] if u in d_trials else d_trials[u.base]['multiplicity'] + shape = [d+1 for d in td] + pad = np.array([td, trd]).max(axis=0) + diag = compute_diag_len(pad, trm, tm) + shape = tuple(Integer(i) for i in (shape + list(diag))) + mat = Allocate(StencilMatrixLocalBasis(u, v, pads, l_mats.tag, dtype=dtype), shape) + local_allocations.append(mat) + + # Collect arguments for OpenMP if used and add the parallel code + if add_openmp: + shared = (*thread_span.values(), coords_from_rank, rank_from_coords, global_thread_s, global_thread_e, + *args['tests_basis'], *args['trial_basis'], *args['spans'], *args['quads'], g_mats) + if mapping_space: + shared = shared + (*args['mapping'], *args['mapping_basis'], *args['mapping_spans']) + if fields: + shared = shared + (*args['f_span'], *args['f_coeffs'], *args['field_basis']) + + firstprivate = (*args['tests_degrees'].values(), *args['trials_degrees'].values(), *lengths, *pads, thread_id.length) + if mapping_space: + firstprivate = firstprivate + (*args['mapping_degrees'], ) + if fields: + firstprivate = firstprivate + ( *args['fields_degrees'], *args['f_pads']) + if constants: + firstprivate = firstprivate + (*constants,) + + body += [ParallelBlock(default='private', + shared=shared, + firstprivate=firstprivate, + body=local_allocations+parallel_body)] + else: + body = local_allocations + body + + # Add Import OpenMP if used + local_vars = [] + imports = [] + if add_openmp: + imports.append(Import('pyccel.stdlib.internal.openmp',('omp_get_thread_num', ))) + + # Create the tree + node = DefNode(f'assemble_matrix_{tag}', args, local_vars, body, imports, (), 'bilinearform', domain_dtype=dtype) + + return node + +#============================================================================== +def _create_ast_linear_form(domain, terminal_expr, atomic_expr_field, tests, d_tests, fields, d_fields, constants, nderiv, dtype, + mapping, d_mapping, is_rational_mapping, mapping_space, mask, tag, num_threads, invert_quad_loop, **kwargs): + + """ + This function creates the assembly function of a linearform + + Parameters + ---------- + + domain : + Sympde Domain object + + terminal_expr : + atomic representation of the linear form + + atomic_expr_field : + dict of atomic expressions of fields + + tests : + list of tests functions + + d_tests : + dictionary that contains the symbolic spans and basis values of each test function + + fields : + list of fields + + constants : + list of constants + + nderiv : int + the order of the bilinear form + + dtype : str + type of data 'complex' or 'float' + + mapping : + Sympde Mapping object + + d_mapping : + dictionary that contains the symbolic spans and basis values of the mapping + + is_rational_mapping : + takes the value of True if the mapping is rational + + mask : + the masked direction in case of boundary domain + + tag : + tag to be added to variable names + + num_threads : + Number of threads + + invert_quad_loop : + Invert the quadrature loop if True + + Returns + ------- + node : DefNode + represents the function definition node that computes the assembly + + """ + + + # Define flags + backend = kwargs.pop('backend', None) + is_pyccel = backend['name'] == 'pyccel' if backend else False + add_openmp = is_pyccel and backend['openmp'] and num_threads>1 + + dim = domain.dim + pads = variables(('pad1, pad2, pad3'), dtype='int')[:dim] + g_quad = [GlobalTensorQuadratureGrid(False)] + l_quad = [LocalTensorQuadratureGrid(False)] + geo = GeometryExpressions(mapping, nderiv) + g_coeffs = {f:[MatrixGlobalBasis(i, i, dtype) for i in expand([f])] for f in fields} + + rank_from_coords = MatrixRankFromCoords() + coords_from_rank = MatrixCoordsFromRank() + + nquads = kwargs.pop('nquads', None) + thread_span = dict((u,d_tests[u]['thread_span']) for u in tests) + + m_tests = dict((v,d_tests[v]['multiplicity']) for v in tests) + + # Initialize BlockVector locally and globally + l_vecs = BlockStencilVectorLocalBasis(tests, pads, terminal_expr, tag, dtype) + g_vecs = BlockStencilVectorGlobalBasis(tests, pads, m_tests, terminal_expr, l_vecs.tag, dtype) + + g_span = dict((v,d_tests[v]['span']) for v in tests) + f_span = dict((f,d_fields[f]['span']) for f in fields) + + # Collect mapping span when a mapping is given by the user otherwise it returns an empty dictionary + if mapping_space: + m_span = dict((f,d_mapping[f]['span']) for f in d_mapping) + else: + m_span = {} + + eval_mappings = [] + lengths_tests = dict((v,LengthDofTest(v)) for v in tests) + lengths_fields = dict((f,LengthDofTest(f)) for f in fields) + + # ........................................................................................... + quad_length = LengthQuadrature() + el_length = LengthElement() + global_thread_s = GlobalThreadStarts() + global_thread_e = GlobalThreadEnds() + global_thread_l = GlobalThreadSizes() + local_thread_s = LocalThreadStarts() + local_thread_e = LocalThreadEnds() + lengths = [el_length,quad_length] + + # Set index of quadrature + if nquads is not None: + ind_quad = index_quad.set_range(stop=Tuple(*nquads)) + else: + ind_quad = index_quad.set_range(stop=quad_length) + + # g_starts = Tuple(*[ProductGenerator(global_thread_s.set_index(i), thread_coords.set_index(i)) for i in range(dim)]) + # g_ends = Tuple(*[AddNode(ProductGenerator(global_thread_e.set_index(i), thread_coords.set_index(i)), Integer(1)) for i in range(dim)]) + l_starts = Tuple(*[ProductGenerator(local_thread_s.set_index(i), local_index_element.set_index(i)) for i in range(dim)]) + l_ends = Tuple(*[ProductGenerator(local_thread_e.set_index(i), local_index_element.set_index(i)) for i in range(dim)]) + +# ind_element = index_element.set_range(start=g_starts,stop=g_ends) if add_openmp else index_element.set_range(stop=el_length) + ind_element = index_element.set_range(start=l_starts,stop=l_ends) if add_openmp else index_element.set_range(stop=el_length) + l_ind_element = local_index_element.set_range(stop=TensorInteger(2)) + + # Create the loop for the mapping coefficient when a mapping is given by the user + if mapping_space: + ind_dof_test = index_dof_test.set_range(stop=Tuple(*[d+1 for d in list(d_mapping.values())[0]['degrees']])) + # ........................................................................................... + eval_mapping = EvalMapping(domain, ind_quad, ind_dof_test, list(d_mapping.values())[0]['global'], + mapping, geo, mapping_space, nderiv, mask, is_rational_mapping, quad_loop=(not invert_quad_loop)) + + + # Create the loop for the fields coefficient + eval_fields = [] + for f in fields: + f_ex = expand([f]) + coeffs = [CoefficientBasis(i) for i in f_ex] + l_coeffs = [MatrixLocalBasis(i, dtype=dtype) for i in f_ex] + ind_dof_test = index_dof_test.set_range(stop=lengths_fields[f]+1) + + eval_field = EvalField(domain, atomic_expr_field[f], ind_quad, ind_dof_test, d_fields[f]['global'], + coeffs, l_coeffs, g_coeffs[f], [f], mapping, nderiv, mask, dtype=dtype, quad_loop=(not invert_quad_loop)) + eval_fields += [eval_field] + + g_stmts = [] + if mapping_space: + g_stmts.append(eval_mapping) + + g_stmts += [*eval_fields] + + # sort tests by their space type + groups = regroup(tests) + # expand every VectorFunction into IndexedVectorFunctions + ex_tests = expand(tests) + # ... + #=========================================================begin kernel====================================================== + + for _, group in groups: + tests_indices = [ex_tests.index(i) for i in expand(group)] + sub_terminal_expr = terminal_expr[tests_indices, 0] + l_sub_vecs = BlockStencilVectorLocalBasis(group, pads, sub_terminal_expr, l_vecs.tag, dtype=dtype) + l_sub_scalars = BlockScalarLocalBasis(tests=group, expr=sub_terminal_expr, tag=l_vecs.tag, dtype=dtype) + + q_basis = {v: d_tests[v]['global'] for v in group} + if is_zero(sub_terminal_expr): + continue + stmts = [] + for v in group: + stmts += construct_logical_expressions(v, nderiv) + + if invert_quad_loop: + # ... loop over tests + length = lengths_tests[group[0]] + ind_dof_test = index_dof_test.set_range(stop=length+1) + stmts.append(Reduction(None,ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars))) + loop = Loop((*q_basis.values(),), ind_dof_test, stmts=[*stmts, VectorAssign(ElementOf(l_sub_vecs), ElementOf(l_sub_scalars),'+')]) + else: + + # Instructions needed to retrieve the precomputed values of the + # fields (and their derivatives) at a single quadrature point + stmts += flatten([eval_field.inits for eval_field in eval_fields]) + + # ... loop over quadrature points + loop = Loop((*l_quad, *q_basis.values(), geo), ind_quad, stmts=stmts, mask=mask) + loop = Reduce('+', ComputeKernelExpr(sub_terminal_expr, weights=False), ElementOf(l_sub_scalars), loop) + + # ... loop over tests + length = lengths_tests[group[0]] + ind_dof_test = index_dof_test.set_range(stop=length+1) + loop = Loop((), ind_dof_test, stmts=[Reset(l_sub_scalars),loop, VectorAssign(ElementOf(l_sub_vecs), ElementOf(l_sub_scalars))]) + + body = (loop,) + stmts = Block(body) + g_stmts += [stmts] + # ... + + #=========================================================end kernel========================================================= + + # Create the loop over global elements when open_mp is used with pyccel + if add_openmp: + body = [VectorAssign(Tuple(*[ProductGenerator(thread_span[u].set_index(j), num_threads) for j in range(dim)]), + Tuple(*[AddNode(2*pads[j],ProductGenerator(g_span[u].set_index(j), AddNode(el_length.set_index(j),Integer(-1)))) for j in range(dim)])) for u in thread_span] + + parallel_body = [] + parallel_body += [Assign(thread_id, Function("omp_get_thread_num")())] + parallel_body += [VectorAssign(thread_coords, Tuple(*[ProductGenerator(coords_from_rank, Tuple((thread_id, i))) for i in range(dim)]))] + + for i in range(dim): + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + parallel_body += [Assign(global_thread_l.set_index(i), AddNode(AddNode(thr_e, Integer(1)), MulNode(Integer(-1),thr_s)))] + + for i in range(dim): + lhs = local_thread_s.set_index(i) + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + rhs = Array(Tuple(thr_s, AddNode(thr_s, IntDivNode(global_thread_l.set_index(i), Integer(2))))) + parallel_body += [Assign(lhs, rhs)] + + for i in range(dim): + lhs = local_thread_e.set_index(i) + thr_s = ProductGenerator(global_thread_s.set_index(i), Tuple(thread_coords.set_index(i))) + thr_e = ProductGenerator(global_thread_e.set_index(i), Tuple(thread_coords.set_index(i))) + rhs = Array(Tuple(AddNode(thr_s, IntDivNode(global_thread_l.set_index(i), Integer(2))), AddNode(thr_e,Integer(1)))) + parallel_body += [Assign(lhs, rhs)] + +# for i in range(dim): +# parallel_body += [Assign(neighbour_threads.set_index(i), ProductGenerator(rank_from_coords, +# Tuple(tuple(AddNode(thread_coords.set_index(j), Integer(i==j)) for j in range(dim)))))] + +# g_stmts_texpr = [] +# for u in thread_span: +# expr1 = [Span(u, index=i) for i in range(dim)] +# expr2 = [ProductGenerator(thread_span[u].set_index(i),AddNode(neighbour_threads.set_index(i),Min(Integer(0), thread_id.length))) for i in range(dim)] +# g_stmts += [WhileLoop(NotNode(AndNode(*[StrictLessThanNode(AddNode(p, e1), e2) for p,e1,e2 in zip(pads, expr1, expr2)])), [Assign(thread_id.length, Min(Integer(100), AddNode(thread_id.length,Integer(1))))])] +# lhs = [ProductGenerator(thread_span[u].set_index(i), Tuple(thread_id)) for i in range(dim)] +# rhs = TensorAdd(Tuple(*expr1), Tuple(*[Integer(0)]*dim)) +# g_stmts_texpr += [TensorAssignExpr(Tuple(*lhs), rhs)] + + inits = eval_mapping.inits if mapping_space else [] + if invert_quad_loop: + # ... loop over the quadrature points + loop = Loop((*l_quad,), ind_quad, stmts=g_stmts, mask=mask) + g_stmts = flatten([Reset(l_vecs), *[em.inits for em in eval_fields], inits, loop]) + else: + g_stmts = flatten([inits, *g_stmts]) + + # ... loop over global elements + global_elements_loop = Loop((*g_quad, *g_span.values(), *m_span.values(), *f_span.values()), ind_element, stmts=g_stmts, mask=mask) + # ... + + global_elements_loop_reduction = Reduce('+', l_vecs, g_vecs, global_elements_loop) + global_elements_loop = Loop((), l_ind_element, stmts=[Comment('#$omp barrier'), global_elements_loop_reduction], mask=mask) + + # Case where the user impose a mask + if mask is not None: + empty_loop = Loop((), l_ind_element, stmts=[Comment('#$omp barrier'), Continue()], mask=mask) + global_elements_loop = IfNode((EqNode(thread_coords.set_index(mask.axis), Integer(0)), [global_elements_loop]), (true, [empty_loop])) + + parallel_body += [global_elements_loop] +# parallel_body += [VectorAssign(Tuple(*[ProductGenerator(thread_span[u].set_index(j), thread_id) for j in range(dim)]), +# Tuple(*[AddNode(2*pads[j],ProductGenerator(g_span[u].set_index(j), AddNode(el_length.set_index(j),Integer(-1)))) for j in range(dim)])) for u in thread_span] + + # Create the loop over global elements when open_mp is not used with pyccel + else: + inits = eval_mapping.inits if mapping_space else [] + if invert_quad_loop: + # ... loop over the quadrature points + loop = Loop((*l_quad,), ind_quad, stmts=g_stmts, mask=mask) + g_stmts = flatten([Reset(l_vecs), *[em.inits for em in eval_fields], inits, loop]) + + else: + g_stmts = flatten([inits, *g_stmts]) + + # ... loop over global elements + global_element_loop = Loop((*g_quad, *g_span.values(), *m_span.values(), *f_span.values()), ind_element, stmts=g_stmts, mask=mask) + # ... + + body = [Reduce('+', l_vecs, g_vecs, global_element_loop)] + # ... + + args = dict() + args['tests_basis'] = tuple(d_tests[v]['global'] for v in tests) + args['spans'] = g_span.values() + args['quads'] = g_quad + args['tests_degrees'] = lengths_tests + args['quads_degree'] = lengths + args['global_pads'] = pads + args['mats'] = [g_vecs] + + # Collect the mapping data if the user give a mapping + if mapping_space: + args['mapping'] = eval_mapping.coeffs + args['mapping_degrees'] = [LengthDofTest(list(d_mapping.keys())[0])] + args['mapping_basis'] = [list(d_mapping.values())[0]['global']] + args['mapping_spans'] = [list(d_mapping.values())[0]['span']] + + + # Collect the fields data + if fields: + args['f_span'] = f_span.values() + args['f_coeffs'] = flatten(list(g_coeffs.values())) + args['field_basis'] = tuple(d_fields[f]['global'] for f in fields) + args['fields_degrees'] = lengths_fields.values() + args['f_pads'] = [f.pads for f in eval_fields] + fields = tuple(f.base if isinstance(f, IndexedVectorFunction) else f for f in fields) + args['fields'] = tuple(dict.fromkeys(fields)) + + # Collect the constants if the expression have some + if constants: + args['constants'] = constants + + # Collect the thread arguments if we are in a parallel case + if add_openmp: + args['thread_args'] = (coords_from_rank, rank_from_coords, global_thread_s, global_thread_e, thread_id.length) + + # Allocate space for thread arguments if we are in a parallel case + allocations = [] + if add_openmp: + allocations = [[Allocate(thread_span[u].set_index(i), (Integer(1+num_threads),)) for i in range(dim)] for u in thread_span] + allocations = [Tuple(*i) for i in allocations] + + # tests_degree = dict((v,d_tests[v]['degrees']) for v in tests) + + # Allocate space for StencilVectors + local_allocations = [] + for i,v in enumerate(ex_tests): + if terminal_expr[i,0] == 0: + continue + td = d_tests[v]['degrees'] if v in d_tests else d_tests[v.base]['degrees'] + shape = [d+1 for d in td] + shape = tuple(Integer(i) for i in shape) + vec = Allocate(StencilVectorLocalBasis(v, pads, l_vecs.tag, dtype=dtype), shape) + local_allocations.append(vec) + + body = allocations + body + + # Add the Parallel code if it's a parallel case + if add_openmp: + shared = (*thread_span.values(), coords_from_rank, rank_from_coords, global_thread_s, global_thread_e, + *args['tests_basis'], *args['spans'], *args['quads'], g_vecs) + if mapping_space: + shared = shared + (*eval_mapping.coeffs, list(d_mapping.values())[0]['global'], list(d_mapping.values())[0]['span']) + if fields: + shared = shared + (*f_span.values(), *args['f_coeffs'], *args['field_basis']) + + firstprivate = (*args['tests_degrees'].values(), *lengths, *pads, thread_id.length) + + if mapping_space: + firstprivate = firstprivate + (*args['mapping_degrees'], ) + if fields: + firstprivate = firstprivate + ( *args['fields_degrees'], *args['f_pads']) + if constants: + firstprivate = firstprivate + (*constants,) + + body += [ParallelBlock(default='private', + shared=shared, + firstprivate=firstprivate, + body=local_allocations+parallel_body)] + + else: + body = local_allocations + body + + local_vars = [] + imports = [] + # Imports openmp if used + if add_openmp: + imports.append(Import('pyccel.stdlib.internal.openmp',('omp_get_thread_num', ))) + + node = DefNode(f'assemble_vector_{tag}', args, local_vars, body, imports, (), 'linearform', domain_dtype=dtype) + + return node + +#============================================================================== +def _create_ast_functional_form(domain, terminal_expr, atomic_expr, fields, d_fields, constants, nderiv, + dtype, mapping, d_mapping, is_rational_mapping, mapping_space, mask, tag, + num_threads, **kwargs): + """ + This function creates the assembly function of a Functional Form + + Parameters + ---------- + + domain : + Sympde Domain object + + terminal_expr : + atomic representation of the Functional form + + atomic_expr : + atoms used in the terminal_expr + + fields : + list of the fields + + d_fields : + dictionary that contains the symbolic spans and basis values of each field + + constants : + list of constants + + nderiv : int + the order of the bilinear form + + dtype : str + type of data 'complex' or 'float' + + mapping : + Sympde Mapping object + + d_mapping : + dictionary that contains the symbolic spans and basis values of the mapping + + is_rational_mapping : + takes the value of True if the mapping is rational + + space : + sympde symbolic space + + mask : + the masked direction in case of boundary domain + + tag : + tag to be added to variable names + + num_threads : + Number of threads + + Returns + ------- + node : DefNode + represents the function definition node that computes the assembly + + """ + + # Create flags for the code + + dim = domain.dim + backend = kwargs.pop('backend', None) + is_pyccel = backend['name'] == 'pyccel' if backend else False + add_openmp = is_pyccel and backend['openmp'] and num_threads>1 + + # pads = variables(('pad1, pad2, pad3'), dtype='int')[:dim] + g_quad = [GlobalTensorQuadratureGrid()] + l_quad = [LocalTensorQuadratureGrid()] + + #TODO move to EvalField + g_coeffs = {f:[MatrixGlobalBasis(i, i, dtype=dtype) for i in expand([f])] for f in fields} + + geo = GeometryExpressions(mapping, nderiv) + + g_span = dict((v,d_fields[v]['span']) for v in fields) + + # Collect mapping span when a mapping is given by the user otherwise it returns an empty dictionary + if mapping_space: + m_span = dict((f,d_mapping[f]['span']) for f in d_mapping) + else: + m_span = {} + + g_basis = dict((v,d_fields[v]['global']) for v in fields) + + lengths_fields = dict((f,LengthDofTest(f)) for f in fields) + + l_vec = LocalElementBasis() + l_vec.dtype = dtype + g_vec = GlobalElementBasis() + g_vec.dtype = dtype + + # ........................................................................................... + quad_length = LengthQuadrature() + el_length = LengthElement() + lengths = [el_length, quad_length] + + ind_quad = index_quad.set_range(stop=quad_length) + ind_element = index_element.set_range(stop=el_length) + + # Create EvalMapping when a mapping is given by the user + if mapping_space: + ind_dof_test = index_dof_test.set_range(stop=Tuple(*[d+1 for d in list(d_mapping.values())[0]['degrees']])) + # ........................................................................................... + eval_mapping = EvalMapping(domain, ind_quad, ind_dof_test, list(d_mapping.values())[0]['global'], + mapping, geo, mapping_space, nderiv, mask, is_rational_mapping, quad_loop=True) + + eval_fields = [] + + # Create EvalFields for each patch + for f in fields: + f_ex = expand([f]) + coeffs = [CoefficientBasis(i) for i in f_ex] + l_coeffs = [MatrixLocalBasis(i, dtype=dtype) for i in f_ex] + ind_dof_test = index_dof_test.set_range(stop=lengths_fields[f]+1) + eval_field = EvalField(domain, atomic_expr[f], ind_quad, ind_dof_test, d_fields[f]['global'], + coeffs, l_coeffs, g_coeffs[f], [f], mapping, nderiv, mask, dtype=dtype, quad_loop=True) + eval_fields += [eval_field] + + #=========================================================begin kernel====================================================== + # ... loop over tests functions to compute the value (last loop in the dependency) + + test_function_loop = Loop((*l_quad, geo), ind_quad, stmts=flatten([eval_field.inits for eval_field in eval_fields])) + reduced_test_function_loop = Reduce('+', ComputeKernelExpr(terminal_expr), ElementOf(l_vec), test_function_loop) + + # ... loop over tests functions to evaluate the fields (first loop) + kernel_stmts = [] + if mapping_space: + kernel_stmts += [eval_mapping.inits, eval_mapping] + + + kernel_stmts += [*eval_fields, Reset(l_vec), reduced_test_function_loop] + kernel_stmts = Block(kernel_stmts) + + #=========================================================end kernel========================================================= + args = {} + + args['tests_basis'] = g_basis.values() + args['spans'] = g_span.values() + args['quads'] = g_quad + args['tests_degrees'] = lengths_fields + args['quads_degree'] = lengths + args['global_pads'] = [f.pads for f in eval_fields] + args['mats'] = [] + + # Collect mapping data when a mapping is given by the user + if mapping_space: + args['mapping'] = eval_mapping.coeffs + args['mapping_degrees'] = [LengthDofTest(list(d_mapping.keys())[0])] + args['mapping_basis'] = [list(d_mapping.values())[0]['global']] + args['mapping_spans'] = [list(d_mapping.values())[0]['span']] + + args['f_coeffs'] = flatten(list(g_coeffs.values())) + fields = tuple(f.base if isinstance(f, IndexedVectorFunction) else f for f in fields) + args['fields'] = tuple(dict.fromkeys(fields)) + + # Collect constant when there is constant in the expression + if constants: + args['constants'] = constants + + # Case where openmp is used + if add_openmp: + shared = (*args['tests_basis'], *args['spans'], *args['quads'], *args['f_coeffs'], g_vec) + if mapping_space: + shared = shared + (*eval_mapping.coeffs, list(d_mapping.values())[0]['global'], list(d_mapping.values())[0]['span']) + + firstprivate = (*args['tests_degrees'].values(), *lengths, *args['global_pads']) + + if mapping_space: + firstprivate = firstprivate + (*args['mapping_degrees'], ) + if constants: + firstprivate = firstprivate + (*constants,) + + global_element_loop = Loop(iterable=(*g_quad, *g_span.values(), *m_span.values()), index=ind_element, stmts=kernel_stmts, + parallel=True, default='private', shared=shared, firstprivate=firstprivate) + else: + global_element_loop = Loop(iterable=(*g_quad, *g_span.values(), *m_span.values()), index=ind_element, stmts=kernel_stmts) + # ... + + body = (Reset(g_vec), Reduce('+', l_vec, g_vec, global_element_loop), Return(g_vec)) + + local_vars = [] + node = DefNode(f'assemble_scalar_{tag}', args, local_vars, body, (), (g_vec,), 'functionalform', domain_dtype=dtype) + + return node diff --git a/psydac/api/ast/glt.py b/psydac/api/ast/glt.py new file mode 100644 index 000000000..00c80a1c4 --- /dev/null +++ b/psydac/api/ast/glt.py @@ -0,0 +1,826 @@ +from itertools import groupby + +from sympy import symbols, Symbol, IndexedBase +from sympy import Tuple +from sympy import Matrix, ImmutableDenseMatrix +from sympy import simplify, expand +from sympy import Range +from sympy.core.numbers import ImaginaryUnit + +from psydac.pyccel.ast.core import IndexedVariable +from psydac.pyccel.ast.core import For +from psydac.pyccel.ast.core import Assign +from psydac.pyccel.ast.core import Slice +from psydac.pyccel.ast.core import FunctionDef +from psydac.pyccel.ast.core import FunctionCall +from psydac.pyccel.ast.core import Import +from psydac.pyccel.ast.core import DottedName +from psydac.pyccel.ast.core import Nil +from psydac.pyccel.ast.core import Len +from psydac.pyccel.ast.core import If, Is, Return +from psydac.pyccel.ast.core import _atomic + +from psydac.pyccel.ast.numpyext import Zeros + +from sympde.topology.space import ScalarFunction +from sympde.topology.space import VectorFunction +from sympde.topology.space import IndexedVectorFunction +from sympde.topology.derivatives import _partial_derivatives +from sympde.topology.derivatives import _logical_partial_derivatives, get_atom_derivatives +from sympde.topology import LogicalExpr +from sympde.topology import SymbolicExpr +from sympde.calculus.matrices import SymbolicDeterminant +from sympde.topology import IdentityMapping + +from sympde.expr.evaluation import TerminalExpr + +from gelato.expr import gelatize + +from .basic import SplBasic +from .utilities import random_string +from .utilities import build_pythran_types_header, variables +from .utilities import build_pyccel_type_annotations +from .utilities import is_mapping +from .utilities import math_atoms_as_str +from .utilities import get_max_partial_derivatives +from .evaluation import EvalArrayMapping, EvalArrayField + +from psydac.fem.vector import MultipatchFemSpace +from .nodes import Zeros + +#============================================================================== +class GltKernel(SplBasic): + + def __new__(cls, expr, spaces, name=None, domain=None, mapping=None, is_rational_mapping=None, backend=None, **kwargs): + + if isinstance(mapping, IdentityMapping): + mapping = None + + tag = random_string( 8 ) + obj = SplBasic.__new__(cls, tag, name=name, + prefix='kernel', domain=domain,mapping=mapping, + is_rational_mapping=is_rational_mapping) + + obj._expr = expr + obj._spaces = spaces + obj._eval_fields = None + obj._eval_mapping = None + obj._user_functions = [] + obj._backend = backend + + obj._func = obj._initialize(**kwargs) + + return obj + + @property + def expr(self): + return self._expr + + @property + def form(self): + return self.expr.form + + @property + def spaces(self): + return self._spaces + + @property + def n_rows(self): + return self._n_rows + + @property + def n_cols(self): + return self._n_cols + + @property + def max_nderiv(self): + return self._max_nderiv + + @property + def with_coordinates(self): + return self._with_coordinates + + @property + def coordinates(self): + return self._coordinates + + @property + def constants(self): + return tuple(self.expr.constants) + + @property + def fields(self): + return tuple(self.expr.fields) + + @property + def fields_coeffs(self): + return self._fields_coeffs + + @property + def mapping_coeffs(self): + if not self.eval_mapping: + return () + + return self.eval_mapping.mapping_coeffs + + @property + def mapping_values(self): + if not self.eval_mapping: + return () + + return self.eval_mapping.mapping_values + + @property + def eval_fields(self): + return self._eval_fields + + @property + def eval_mapping(self): + return self._eval_mapping + + @property + def global_mats(self): + return self._global_mats + + @property + def global_mats_types(self): + return self._global_mats_types + + @property + def user_functions(self): + return self._user_functions + + @property + def backend(self): + return self._backend + + def build_arguments(self, data): + + other = data + + if self.constants: + other = other + self.constants + + return self.basic_args + other + + def _initialize(self, **kwargs): + form = self.form + dim = self.expr.ldim + domain = self.domain + mapping = self.mapping + + + # ... discrete values + Vh, Wh = self.spaces + + domain = Vh.symbolic_space.domain + + n_elements = Vh.ncells + degrees = Vh.degree + # TODO improve + if isinstance(Vh, MultipatchFemSpace): + degrees = degrees[0] + # ... + + expand_expr = kwargs.pop('expand', False) + # recompute the symbol + expr = gelatize(form, degrees=degrees, n_elements=n_elements, + domain=domain, evaluate=True, human=True, expand=expand_expr) + + + fields = form.fields + fields = sorted(fields, key=lambda x: str(x.name)) + fields = tuple(fields) + + # TODO improve + if mapping is None: + mapping = () + + expr = expand(expr) + expr = expr.evalf() + +# if mapping: +# expr = simplify(expr) + # ... + + # ... + n_rows = 1 ; n_cols = 1 + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + n_rows = expr.shape[0] + n_cols = expr.shape[1] + + self._n_rows = n_rows + self._n_cols = n_cols + # ... + + # ... + prelude = [] + body = [] + imports = [] + # ... + + # ... + degrees = variables('p1:%d'%(dim+1), 'int') + n_elements = variables('n1:%d'%(dim+1), 'int') + tis = variables('t1:%d'%(dim+1), 'real') + arr_tis = variables('arr_t1:%d'%(dim+1), dtype='real', rank=1, cls=IndexedVariable) + xis = variables('x1:%d'%(dim+1), 'real') + arr_xis = variables('arr_x1:%d'%(dim+1), dtype='real', rank=1, cls=IndexedVariable) + indices = variables('i1:%d'%(dim+1), 'int') + lengths = variables('k1:%d'%(dim+1), 'int') + + ranges = [Range(lengths[i]) for i in range(dim)] + # ... + + if fields or mapping: + basis = variables( 'basis1:%s'%(dim+1), + dtype = 'real', + rank = 4, + cls = IndexedVariable ) + + spans = variables( 'spans1:%s'%(dim+1), + dtype = 'int', + rank = 1, + cls = IndexedVariable ) + # ... + self._coordinates = tuple() + if fields or mapping or self.expr.space_variables: + names = ['x1', 'x2', 'x3'][:dim] + self._coordinates = tuple([Symbol(i) for i in names]) + + self._with_coordinates = (len(self._coordinates) > 0) + # ... + + # ... + d_symbols = {} + for i in range(0, n_rows): + for j in range(0, n_cols): + is_complex = False + mat = IndexedBase('symbol_{i}{j}'.format(i=i,j=j)) + d_symbols[i,j] = mat + # ... + + # ... replace tx/ty/tz by t1/t2/t3 + txs = [Symbol(tx) for tx in ['tx', 'ty', 'tz'][:dim]] + for tx, ti in zip(txs, tis): + expr = expr.subs(tx, ti) + + xs = [Symbol(x) for x in ['x', 'y', 'z'][:dim]] + for x, xi in zip(xs, xis): + expr = expr.subs(x, xi) + # ... + + # ... + + atoms_types = (_partial_derivatives, + _logical_partial_derivatives, + ScalarFunction, + VectorFunction, + IndexedVectorFunction, + SymbolicDeterminant) + + atoms = _atomic(expr, cls=atoms_types) + # ... + + # ... +# atomic_expr_mapping = [atom for atom in atoms if is_mapping(atom)] + atomic_expr_field = [atom for atom in atoms if atom.atoms(ScalarFunction)] + atomic_expr_vector_field = [atom for atom in atoms if atom.atoms(VectorFunction)] + # ... + + # ... +# mapping_expressions = [SymbolicExpr(i) for i in atomic_expr_mapping] +# for old, new in zip(atomic_expr_mapping, mapping_expressions): +# expr = expr.subs(old, new) + # ... + + # ... + d_subs = dict(zip(_partial_derivatives, _logical_partial_derivatives)) + atomic_expr_field_logical = tuple(f.subs(d_subs) for f in atomic_expr_field) + fields_str = tuple(sorted(SymbolicExpr(f).name for f in atomic_expr_field)) + fields_logical_str = tuple(sorted(SymbolicExpr(f).name for f in atomic_expr_field_logical)) + field_atoms = tuple(expr.atoms(ScalarFunction)) + # ... + + # ... create EvalArrayField + self._eval_fields = [] + self._map_stmts_fields = {} + if atomic_expr_field: + keyfunc = lambda F: F.space.name + data = sorted(field_atoms, key=keyfunc) + for space_str, group in groupby(data, keyfunc): + g_names = set([f.name for f in group]) + fields_expressions = [] + for e in atomic_expr_field: + fs = e.atoms(ScalarFunction) + f_names = set([f.name for f in fs]) + if f_names & g_names: + fields_expressions += [e] + space = list(fs)[0].space + + eval_field = EvalArrayField(space, fields_expressions, + mapping=mapping,backend=self.backend) + + self._eval_fields.append(eval_field) + for k,v in eval_field.map_stmts.items(): + self._map_stmts_fields[k] = v + # update dependencies + self._dependencies += self.eval_fields + # ... + + # ... TODO add it as a method to basic class + nderiv = 0 + + if isinstance(expr, Matrix): + n_rows, n_cols = expr.shape + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + d_atoms = _atomic(expr[i_row,i_col], cls=atoms_types) + Fs = [get_atom_derivatives(a) for a in d_atoms] + d = get_max_partial_derivatives(expr[i_row,i_col], logical=False, F=Fs) + nderiv = max(nderiv, max(d.values())) + else: + Fs = [get_atom_derivatives(a) for a in atoms] + d = get_max_partial_derivatives(expr, logical=False, F=Fs) + nderiv = max(nderiv, max(d.values())) + + self._max_nderiv = nderiv + # ... + + # ... mapping + mapping = self.mapping + self._eval_mapping = None + if mapping: + + space = form.trial_spaces[0] + + eval_mapping = EvalArrayMapping(space, mapping, + nderiv=nderiv, + is_rational_mapping=self.is_rational_mapping, + backend=self.backend) + self._eval_mapping = eval_mapping + + # update dependencies + self._dependencies += [self.eval_mapping] + # ... + + # ... declarations + fields = symbols(fields_str) + fields_logical = symbols(fields_logical_str) + + fields_coeffs = variables(['coeff_{}'.format(f) for f in field_atoms], + dtype='real', rank=dim, cls=IndexedVariable) + fields_val = variables(['{}_values'.format(f) for f in fields_str], + dtype='real', rank=dim, cls=IndexedVariable) + + fields_tmp_coeffs = variables(['tmp_coeff_{}'.format(f) for f in field_atoms], + dtype='real', rank=dim, cls=IndexedVariable) + +# vector_fields = symbols(vector_fields_str) +# vector_fields_logical = symbols(vector_fields_logical_str) +# +# vector_field_atoms = [f[i] for f in vector_field_atoms for i in range(0, dim)] +# coeffs = ['coeff_{}'.format(SymbolicExpr(f).name) for f in vector_field_atoms] +# vector_fields_coeffs = variables(coeffs, dtype='real', rank=dim, cls=IndexedVariable) +# +# vector_fields_val = variables(['{}_values'.format(f) for f in vector_fields_str], +# dtype='real', rank=dim, cls=IndexedVariable) + # ... + + # ... + if mapping: + mapping_elements = [SymbolicExpr(i) for i in self.eval_mapping.elements] + mapping_coeffs = self.eval_mapping.mapping_coeffs + mapping_values = self.eval_mapping.mapping_values + else: + mapping_elements = () + mapping_coeffs = () + mapping_values = () + # ... + +# # ... +# self._fields_val = fields_val +# self._vector_fields_val = vector_fields_val +# self._fields = fields +# self._fields_logical = fields_logical +# self._fields_coeffs = fields_coeffs +# self._fields_tmp_coeffs = fields_tmp_coeffs +# self._vector_fields = vector_fields +# self._vector_fields_logical = vector_fields_logical +# self._vector_fields_coeffs = vector_fields_coeffs +# self._mapping_coeffs = mapping_coeffs +# # ... + + # ... + for l,arr_ti in zip(lengths, arr_tis): + prelude += [Assign(l, Len(arr_ti))] + # ... + + # ... + slices = [Slice(None,None)]*dim + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + symbol = d_symbols[i_row,i_col] + prelude += [Assign(symbol[slices], 0.)] + # ... + + # ... fields + for i in range(len(fields_val)): + body.append(Assign(fields[i],fields_val[i][indices])) + # ... + + # ... mapping + if mapping: + for value, array in zip(mapping_elements, mapping_values): + body.append(Assign(value, array[indices])) + + jac = TerminalExpr(mapping.det_jacobian, domain.logical_domain) + jac = SymbolicExpr(jac) + + det_jac = SymbolicExpr(SymbolicDeterminant(domain.mapping)) + + body += [Assign(det_jac, jac)] + # ... + + # ... + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + symbol = d_symbols[i_row,i_col] + symbol = symbol[indices] + + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + rhs = SymbolicExpr(expr[i_row,i_col]) + body += [Assign(symbol, rhs)] + + else: + rhs = SymbolicExpr(expr) + body += [Assign(symbol, rhs)] + + for i in range(dim-1,-1,-1): + x = indices[i] + rx = ranges[i] + + ti = tis[i] + arr_ti = arr_tis[i] + body = [Assign(ti, arr_ti[x])] + body + if self.with_coordinates: + xi = xis[i] + arr_xi = arr_xis[i] + body = [Assign(xi, arr_xi[x])] + body + + body = [For(x, rx, body)] + # ... + + # call eval field + for i, eval_field in enumerate(self.eval_fields): + args = (*degrees, *spans, *basis, fields_coeffs[i], fields_val[i]) + args = eval_field.build_arguments(args) + body = [FunctionCall(eval_field.func, args)] + body + +# # call eval vector_field +# for eval_vector_field in self.eval_vector_fields: +# args = degrees + basis + vector_fields_coeffs + vector_fields_val +# args = eval_vector_field.build_arguments(args) +# body = [FunctionCall(eval_vector_field.func, args)] + body + + # call eval mapping + if self.eval_mapping: + args = (degrees + spans + basis + mapping_coeffs + mapping_values) + args = eval_mapping.build_arguments(args) + body = [FunctionCall(eval_mapping.func, args)] + body + + + if fields: + imports += [Import('numpy', 'zeros')] + for F_value in fields_val: + prelude += [Assign(F_value, Zeros(lengths))] + +# if vector_fields_val: +# for F_value in vector_fields_val: +# prelude += [Assign(F_value, Zeros(lengths))] + + if mapping: + imports += [Import('numpy', 'zeros')] + for M_value in mapping_values: + prelude += [Assign(M_value, Zeros(lengths))] + + # ... + body = prelude + body + # ... + + # ... get math functions and constants + math_elements = math_atoms_as_str(expr, 'math') + math_imports = [Import('math', e) for e in math_elements] + + imports += math_imports + # ... + + # ... + self._basic_args = [*arr_tis] + self._basic_args = tuple(self._basic_args) + # ... + + # ... + mats = [] + for i in range(0, n_rows): + for j in range(0, n_cols): + mats.append(d_symbols[i,j]) + mats = tuple(mats) + self._global_mats = mats + # ... + + # ... + mats_types = [] + if isinstance(expr, (Matrix, ImmutableDenseMatrix)): + for i in range(0, n_rows): + for j in range(0, n_cols): + dtype = 'float' + if expr[i,j].atoms(ImaginaryUnit): + dtype = 'complex' + mats_types.append(dtype) + + else: + dtype = 'float' + if expr.atoms(ImaginaryUnit): + dtype = 'complex' + mats_types.append(dtype) + + mats_types = tuple(mats_types) + self._global_mats_types = mats_types + # ... + + self._imports = imports + + # function args + args = () + if self.with_coordinates: + args = arr_xis + + if fields or mapping: + args = args + degrees + spans + basis + + args = args + fields_coeffs + mapping_coeffs + mats + func_args = self.build_arguments(args) + + decorators = {} + header = None + if self.backend['name'] == 'pyccel': + func_args = build_pyccel_type_annotations(func_args) + elif self.backend['name'] == 'pythran': + header = build_pythran_types_header(self.name, func_args) + + return FunctionDef(self.name, list(func_args), [], body, + decorators=decorators,header=header) + + +class GltInterface(SplBasic): + + def __new__(cls, kernel, name=None, domain=None, mapping=None, is_rational_mapping=None, backend=None, **kwargs): + + if not isinstance(kernel, GltKernel): + raise TypeError('> Expecting an GltKernel') + + if isinstance(mapping, IdentityMapping): + mapping = None + + obj = SplBasic.__new__(cls, kernel.tag, name=name, + prefix='interface', domain=domain, mapping=mapping, + is_rational_mapping=is_rational_mapping) + + obj._kernel = kernel + obj._mapping = mapping + obj._backend = backend + + # update dependencies + obj._dependencies += [kernel] + + obj._func = obj._initialize() + return obj + + @property + def kernel(self): + return self._kernel + + @property + def mapping(self): + return self._mapping + + @property + def backend(self): + return self._backend + + @property + def max_nderiv(self): + return self.kernel.max_nderiv + + @property + def n_rows(self): + return self.kernel.n_rows + + @property + def n_cols(self): + return self.kernel.n_cols + + @property + def global_mats_types(self): + return self.kernel.global_mats_types + + def build_arguments(self, data): + # data must be at the end, since they are optional + return self.basic_args + data + + @property + def in_arguments(self): + return self._in_arguments + + @property + def inout_arguments(self): + return self._inout_arguments + + @property + def coordinates(self): + return self.kernel.coordinates + + @property + def user_functions(self): + return self.kernel.user_functions + + @property + def with_coordinates(self): + return self.kernel.with_coordinates + + def _initialize(self): + form = self.kernel.form + kernel = self.kernel + global_mats = kernel.global_mats + global_mats_types = kernel.global_mats_types + fields = form.fields + fields = sorted(fields, key=lambda x: str(x.name)) + fields = tuple(fields) + + dim = form.ldim + + mapping = () + if self.mapping: mapping = Symbol('mapping') + + # ... declarations + test_space = Symbol('W') + trial_space = Symbol('V') + + arr_tis = symbols('arr_t1:%d'%(dim+1), cls=IndexedBase) + arr_xis = symbols('arr_x1:%d'%(dim+1), cls=IndexedBase) + lengths = symbols('k1:%d'%(dim+1)) + degrees = variables( 'p1:%s'%(dim+1), 'int') + + if fields or mapping: + basis_values = Symbol('basis_values') + + basis = variables( 'basis_1:%s'%(dim+1), + dtype = 'real', + rank = 4, + cls = IndexedVariable ) + + spans = variables( 'spans_1:%s'%(dim+1), + dtype = 'int', + rank = 1, + cls = IndexedVariable ) + # ... + + # ... + if mapping or fields: + self._basic_args = kernel.basic_args + (test_space, basis_values,) + + else: + self._basic_args = kernel.basic_args + (test_space, ) + # ... + + # ... + imports = [] + prelude = [] + body = [] + # ... + + # ... + body += [Assign(degrees, DottedName(test_space, 'degree'))] + # ... + + # ... + grid_data = () + if mapping or fields: + body += [Assign(spans, DottedName(basis_values, 'spans'))] + body += [Assign(basis, DottedName(basis_values, 'basis'))] + + grid_data = (*degrees, *spans, *basis) + # ... + + # ... + if mapping: + # we limit the range to dim, since the last element can be the + # weights when using NURBS + for i, coeff in enumerate(kernel.mapping_coeffs[:dim]): + component = IndexedBase(DottedName(mapping, '_fields'))[i] + c_var = DottedName(component, '_coeffs', '_data') + body += [Assign(coeff, c_var)] + + # NURBS case + if self.is_rational_mapping: + coeff = kernel.mapping_coeffs[-1] + + component = DottedName(mapping, '_weights_field') + c_var = DottedName(component, '_coeffs', '_data') + body += [Assign(coeff, c_var)] + # ... + + # ... + imports += [Import('numpy', 'zeros')] + # ... + + # ... + for l,arr_ti in zip(lengths, arr_tis): + prelude += [Assign(l, Len(arr_ti))] + # ... + + # ... + if dim > 1: + lengths = Tuple(*lengths) + lengths = [lengths] + + for M,dtype in zip(global_mats, global_mats_types): + if_cond = Is(M, Nil()) + + _args = list(lengths) + ['{}'.format(dtype)] + if_body = [Assign(M, Zeros(*_args))] + + stmt = If((if_cond, if_body)) + body += [stmt] + # ... + + # ... + body = prelude + body + # ... + + # ... + self._inout_arguments = list(global_mats) + self._in_arguments = list(self.coordinates) + list(self.kernel.constants) + list(fields) + # ... + + # ... call to kernel + # TODO add fields + mat_data = tuple(global_mats) + + field_data = [DottedName(F, '_coeffs', '_data') for F in fields] + field_data = tuple(field_data) + + args = () + if self.with_coordinates: + args = arr_xis + + if fields or mapping: + args = args + degrees + spans + basis + + args = args + field_data + kernel.mapping_coeffs + mat_data + args = kernel.build_arguments(args) + + body += [FunctionCall(kernel.func, args)] + # ... + + # ... results + if len(global_mats) == 1: + M = global_mats[0] + body += [Return(M)] + + else: + body += [Return(global_mats)] + # ... + + # ... arguments + mats = [Assign(M, Nil()) for M in global_mats] + mats = tuple(mats) + + if mapping: + mapping = (mapping,) + + # TODO improve using in_arguments + if self.kernel.constants: + constants = self.kernel.constants + + if self.with_coordinates: + coordinates = arr_xis + args = mapping + constants + coordinates + fields + mats + + else: + args = mapping + constants + fields + mats + + else: + if self.with_coordinates: + coordinates = arr_xis + args = mapping + coordinates + fields + mats + + else: + args = mapping + fields + mats + + func_args = self.build_arguments(args) + # ... + + self._imports = imports + return FunctionDef(self.name, list(func_args), [], body) diff --git a/psydac/api/ast/linalg.py b/psydac/api/ast/linalg.py new file mode 100644 index 000000000..0bdde2aba --- /dev/null +++ b/psydac/api/ast/linalg.py @@ -0,0 +1,488 @@ +# coding: utf-8 + +import sys +import os +import importlib +import inspect + +import numpy as np + +from functools import lru_cache +from mpi4py import MPI + +from sympy import Mul +from sympy import Mod as sy_Mod, Range, Symbol, Max +from sympy import Function, Integer + +from psydac.pyccel.ast.core import Variable, IndexedVariable +from psydac.pyccel.ast.core import For, Comment +from psydac.pyccel.ast.core import String +from psydac.pyccel.ast.core import ValuedArgument +from psydac.pyccel.ast.core import Assign +from psydac.pyccel.ast.core import AugAssign +from psydac.pyccel.ast.core import Product +from psydac.pyccel.ast.core import FunctionDef +from psydac.pyccel.ast.core import Import + +from psydac.pyccel.ast.datatypes import NativeInteger + +from psydac.api.ast.nodes import FloorDiv +from psydac.api.ast.utilities import variables +from psydac.api.ast.utilities import build_pyccel_type_annotations +from psydac.api.utilities import flatten + +from psydac.api.ast.basic import SplBasic +from psydac.api.printing import pycode +from psydac.api.settings import PSYDAC_DEFAULT_FOLDER +from psydac.api.utilities import mkdir_p, touch_init_file, random_string, write_code + +#============================================================================== +def variable_to_sympy(x): + if isinstance(x, Variable) and isinstance(x.dtype, NativeInteger): + x = Symbol(x.name, integer=True) + return x + +#============================================================================== +def compute_diag_len(p, md, mc, return_padding=False): + p, md, mc = np.int64(p), np.int64(md), np.int64(mc) + n = ((np.ceil((p+1)/mc)-1)*md).astype('int') + ep = np.minimum(0, n-p) + n = n-ep + p+1 + if return_padding: + return n.astype('int'), (-ep).astype('int') + else: + return n.astype('int') + +def Mod(a,b): + if b == 1: + return Integer(0) + else: + return sy_Mod(a,b) + +def toInteger(a): + if isinstance(a, np.int64): + return Integer(int(a)) + return a +#============================================================================== + +class LinearOperatorDot(SplBasic): + """ + Generate the Matrix Vector Product function for a BlockLinearOperator,StencilMatrix or StencilInterfaceMatrix. + In case of a BlockLinearOperator we give the number of blocks along the rows and columns specified with the block_shape. + In case of StencilMatrix or StencilInterfaceMatrix the block_shape = (1,1). + + Parameters + ---------- + ndim : int + Number of dimensions. + + block_shape: tuple of ints + The number of blocks along the rows and columns. + + comm: MPI.Comm + MPI intra-communicator. + """ + def __new__(cls, ndim, block_shape, comm=None, **kwargs): + if comm is not None: + assert isinstance(comm, MPI.Comm) + comm_id = comm.py2f() + else: + comm_id = None + return cls.__hashable_new__(ndim, block_shape, comm_id, **kwargs) + + @classmethod + @lru_cache(maxsize=32) + def __hashable_new__(cls, ndim, block_shape, comm_id=None, **kwargs): + + # If integer communicator is provided, convert it to mpi4py object + comm = None if comm_id is None else MPI.COMM_WORLD.f2py(comm_id) + + # Generate random tag, unique for all processes in MPI communicator + tag = random_string(8) + if comm is not None and comm.size>1: + tag = comm.bcast(tag, root=0) + + # Create new instance of this class + obj = SplBasic.__new__(cls, tag, prefix='lo_dot', comm=comm) + + # Initialize instance (code generation happens here) + backend = dict(kwargs.pop('backend')) + code = obj._initialize(ndim, block_shape, backend=backend, **kwargs) + obj._arguments = dict((str(a.name), a) for a in code.arguments) + obj._code = code + obj._folder = obj._initialize_folder() + obj._generate_code(backend=backend) + obj._compile(backend=backend) + + # Return instance + return obj + + @property + def func(self): + return self._func + + @property + def arguments(self): + return self._arguments + + @property + def code(self): + return self._code + + @property + def folder(self): + return self._folder + + def _initialize(self, ndim, block_shape, **kwargs): + + keys = kwargs.pop('keys') + backend = kwargs.pop('backend', None) + nrows = list(kwargs.pop('nrows', [None]*len(keys))) + nrows_extra = list(kwargs.pop('nrows_extra', [None]*len(keys))) + starts = list(kwargs.pop('starts', [None]*len(keys))) + pads = kwargs.pop('pads') + gpads = kwargs.pop('gpads') + cm = kwargs.pop('cm') + dm = kwargs.pop('dm') + interface = kwargs.pop('interface', False) + flip_axis = kwargs.pop('flip_axis',[1]*ndim) + interface_axis = kwargs.pop('interface_axis', None) + d_start = kwargs.pop('d_start', None) + c_start = kwargs.pop('c_start', None) + dtype = kwargs.pop('dtype', float) + + # Adapt the type of data treated in our dot function + if dtype==complex: + dtype_string='complex' + else: + dtype_string='real' + + mats = [variables('mat{}'.format(''.join(str(i) for i in key)),dtype_string, cls=IndexedVariable, rank=2*ndim) for key in keys] + xs = [variables('x{}'.format(i),dtype_string, cls=IndexedVariable, rank=ndim) for i in range(block_shape[1])] + outs = [variables('out{}'.format(i),dtype_string, cls=IndexedVariable, rank=ndim) for i in range(block_shape[0])] + + func_args = (*mats, *xs, *outs) + shared = (*mats, *xs, *outs) + firstprivate = () + openmp = False if backend is None else backend["openmp"] + gbody = [] + + for it in range(2): + diag_keys = True if it==0 else False + for k,key in enumerate(keys): + if diag_keys and key[0] != key[1]:continue + if not diag_keys and key[0] == key[1]:continue + key_str = ''.join(str(i) for i in key) + nrows_k = nrows[k] if nrows[k] else variables('n{}_1:%s'.format(key_str)%(ndim+1), 'int') + nrows_extra_k = nrows_extra[k] if nrows_extra[k] else variables('ne{}_1:%s'.format(key_str)%(ndim+1), 'int') + starts_k = starts[k] if starts[k] else variables('s{}_1:%s'.format(key_str)%(ndim+1), 'int') + nrows_k = tuple(map(toInteger,nrows_k)) + nrows_extra_k = tuple(map(toInteger,nrows_extra_k)) + starts_k = tuple(map(toInteger,starts_k)) + indices1 = variables('i1:%s'%(ndim+1), 'int') + bb = variables('b1:%s'%(ndim+1), 'int') + indices2 = variables('k1:%s'%(ndim+1), 'int') + v = variables('v{}'.format(key_str),dtype_string) + xshape = variables('xn1:%s'%(ndim+1), 'int') + + pads_k = tuple(map(toInteger, pads[k])) + gpads_k = tuple(map(toInteger,gpads[k])) + cm_k = tuple(map(toInteger,cm[k])) + dm_k = tuple(map(toInteger,dm[k])) + d_start_k = toInteger(d_start[k] if d_start else None) + c_start_k = toInteger(c_start[k] if c_start else None) + + nrows[k] = tuple(nrows_k) + nrows_extra[k] = tuple(nrows_extra_k) + starts[k] = tuple(starts_k) + + mat = mats[k] + x = xs[key[1]] + out = outs[key[0]] + + ndiags, _ = list(zip(*[compute_diag_len(p,mj,mi, return_padding=True) for p,mi,mj in zip(pads_k,cm_k,dm_k)])) + + inits = [Assign(b,p*m+p+1-n-Mod(s,m)) for b,p,m,n,s in zip(bb, gpads_k, dm_k, ndiags, starts_k) if not isinstance(p*m+p+1-n-Mod(s,m),(int,np.int64, Integer))] + + if any(f==-1 for f in flip_axis): + inits.append(Assign(xshape, Function('shape')(x))) + + bb = [b if not isinstance(p*m+p+1-n-Mod(s,m),(int,np.int64, Integer)) else p*m+p+1-n-Mod(s,m) for b,p,m,n,s in zip(bb, gpads_k, dm_k, ndiags, starts_k)] + + ranges = [Range(variable_to_sympy(n)) for n in ndiags] + diff = [variable_to_sympy(gp-p) for gp,p in zip(gpads_k, pads_k)] + + # if d_start_k:bb[interface_axis] += d_start_k + + x_indices = [] + for i1,mi,mj,b,s,d,i2,f,xl in zip(indices1,cm_k,dm_k,bb,starts_k,diff,indices2,flip_axis,xshape): + index = b-d+FloorDiv((i1+Mod(s,mj)),mi)*mj + i2 + if f==-1: + index = xl-1-index + x_indices.append(index) + + out_indices = [i+m*j for i,j,m in zip(indices1,gpads_k,cm_k)] + if c_start_k:out_indices[interface_axis] += c_start_k + + v1 = x[tuple(x_indices)] + v2 = mat[tuple(i+m*j for i,j,m in zip(indices1,gpads_k,cm_k))+ tuple(indices2)] + v3 = out[tuple(out_indices)] + + body = [AugAssign(v,'+' ,Mul(v2, v1))] + + # Decompose fused loop over Cartesian product of multiple ranges + # into nested loops, each over a single range + for i,j in zip(indices2[::-1], ranges[::-1]): + body = [For(i,j, body)] + + # Adapt data type of the variable v=0 + if dtype==complex: + body.insert(0,Assign(v, 0.0+0j)) + else: + body.insert(0,Assign(v, 0.0)) + + if diag_keys: + body.append(Assign(v3,v)) + else: + body.append(AugAssign(v3,'+',v)) + + ranges = [Range(variable_to_sympy(i)) for i in nrows_k] + + # Decompose fused loop over Cartesian product of multiple ranges + # into nested loops, each over a single range + for i,j in zip(indices1[::-1], ranges[::-1]): + body = [For(i,j, body)] + + if openmp: + pragma = "#$omp for schedule(static) collapse({}) nowait".format(str(ndim)) + body = [Comment(pragma)] + body + + nrowscopy_k = list(nrows_k).copy() + nrows_k = list(nrows_k) + for dim in range(ndim): + + if nrows_extra_k[dim] == 0:continue + + v1 = [b-d+FloorDiv((i1+(nrows_k[dim] if dim==x else 0)+Mod(s,mj)),mi)*mj + i2 for x,i1,mi,mj,b,s,d,i2 in zip(range(ndim), indices1,cm_k,dm_k,bb,starts_k,diff,indices2)] + v2 = [i+m*j for i,j,m in zip(indices1,gpads_k,cm_k)] + + v2[dim] += nrows_k[dim] + + v3 = v2 + + for i,v1i in enumerate(v1): + if flip_axis[i] == -1: + v1[i] = xshape[i]-1-v1[i] + + v1 = x[tuple(v1)] + v2 = mat[tuple(v2)+ indices2] + + if c_start_k:v3[interface_axis] += c_start_k + v3 = out[tuple(v3)] + + rows = list(nrows_k) + rows[dim] = nrows_extra_k[dim] + + ranges = [variable_to_sympy(n) for n in ndiags] + ranges[dim] -= variable_to_sympy(indices1[dim]) + 1 + ranges = [ind if i>=dim else ind - Max(0, variable_to_sympy(d1)+1-variable_to_sympy(r)) for i,(ind,d1,r) in enumerate(zip(ranges, indices1, nrowscopy_k)) ] + ranges = [Range(i) for i in ranges] + + for_body = [AugAssign(v, '+',Mul(v1,v2))] + + # Decompose fused loop over Cartesian product of multiple ranges + # into nested loops, each over a single range + for i,j in zip(indices2[::-1], ranges[::-1]): + for_body = [For(i,j, for_body)] + + # Adapt data type of the variable v=0 + if dtype == complex: + for_body.insert(0, Assign(v, 0.0 + 0j)) + else: + for_body.insert(0, Assign(v, 0.0)) + + if diag_keys: + for_body.append(Assign(v3,v)) + else: + for_body.append(AugAssign(v3,'+',v)) + + ranges = [Range(variable_to_sympy(i)) for i in rows] + + # Decompose fused loop over Cartesian product of multiple ranges + # into nested loops, each over a single range + for i,j in zip(indices1[::-1], ranges[::-1]): + for_body = [For(i,j, for_body)] + + if openmp: + pragma = "#$omp for schedule(static) collapse({}) nowait".format(str(ndim)) + for_body = [Comment(pragma)] + for_body + + body += for_body + + nrows_k[dim] += nrows_extra_k[dim] + + body = inits + body + gbody += body + + body = gbody + + if isinstance(starts[0][0], Variable): + func_args = func_args + tuple(flatten(starts)) + firstprivate = firstprivate + tuple(flatten(starts)) + + if isinstance(nrows[0][0], Variable): + func_args = func_args + tuple(flatten(nrows)) + firstprivate = firstprivate + tuple(flatten(nrows)) + + if isinstance(nrows_extra[0][0], Variable): + func_args = func_args + tuple(flatten(nrows_extra)) + firstprivate = firstprivate + tuple(flatten(nrows_extra)) + + decorators = {} + header = None + imports = [] + if backend: + if backend['name'] == 'pyccel': + func_args = build_pyccel_type_annotations(func_args) + elif backend['name'] == 'pythran': + header = build_pythran_types_header(name, func_args) + + if openmp: + shared = ','.join(str(a) for a in shared) + firstprivate = "firstprivate({})".format(','.join(str(a) for a in firstprivate)) if firstprivate else "" + pragma1 = "#$omp parallel default(private) shared({}) {}\n".format(shared, firstprivate) + pragma2 = "#$omp end parallel" + body = [Comment(pragma1)] + body + [Comment(pragma2)] + + return FunctionDef(self.name, list(func_args), [], body, imports=imports, decorators=decorators) + + def _initialize_folder(self, folder=None): + # ... + if folder is None: + basedir = os.getcwd() + folder = PSYDAC_DEFAULT_FOLDER['name'] + folder = os.path.join( basedir, folder ) + + # ... add __init__ to all directories to be able to + touch_init_file('__pycache__') + for root, dirs, files in os.walk(folder): + touch_init_file(root) + # ... + + else: + raise NotImplementedError('user output folder not yet available') + + folder = os.path.abspath( folder ) + mkdir_p(folder) + # ... + + return folder + + def _generate_code(self, backend=None): + + modname = 'dependencies_{}'.format(self.tag) + + if self.comm is None or self.comm.rank == 0: + python_code = pycode.pycode(self.code) + write_code(modname + '.py', python_code, folder=self.folder) + + self._modname = modname + + def _compile(self, backend=None): + + # Make sure that code generated by process 0 is available to all others + # Cheapest solution is a broadcast from process 0 + comm = self.comm + if comm is not None and comm.size > 1: + comm.bcast(0, root=0) + + module_name = self._modname + sys.path.append(self.folder) + importlib.invalidate_caches() + package = importlib.import_module(module_name) + sys.path.remove(self.folder) + + if backend and backend['name'] == 'pyccel': + package = self._compile_pyccel(package, backend) + + self._func = getattr(package, self.name) + + def _compile_pyccel(self, mod, backend, verbose=False): + + # ... convert python to fortran using pyccel + compiler_family = backend['compiler_family'] + flags = backend['flags'] + _PYCCEL_FOLDER = backend['folder'] + openmp = backend["openmp"] + + from pyccel import epyccel + + fmod = epyccel(mod, + openmp = openmp, + compiler_family = compiler_family, + flags = flags, + comm = self.comm, + bcast = True, + folder = _PYCCEL_FOLDER, + verbose = verbose) + return fmod + +#============================================================================== +class VectorInner(SplBasic): + + def __new__(cls, ndim, backend=None): + tag = random_string(8) + obj = SplBasic.__new__(cls, tag, prefix='v_dot') + obj._ndim = ndim + obj._backend = backend + obj._func = obj._initialize() + return obj + + @property + def ndim(self): + return self._ndim + + @property + def func(self): + return self._func + + @property + def backend(self): + return self._backend + + def _initialize(self): + + ndim = self.ndim + + indices = variables('i1:%s'%(ndim+1), 'int') + dims = variables('n1:%s'%(ndim+1), 'int') + pads = variables('p1:%s'%(ndim+1), 'int') + out = variables('out','real') + x1,x2 = variables('x1, x2','real', rank=ndim, cls=IndexedVariable) + + body = [] + ranges = [Range(p, n-p) for n, p in zip(dims, pads)] + target = Product(*ranges) + + v1 = x1[indices] + v2 = x2[indices] + + body = [AugAssign(out, '+' , Mul(v1, v2))] + body = [For(indices, target, body)] + body.insert(0, Assign(out, 0.0)) + body.append(Return(out)) + + func_args = (x1, x2) + pads + dims + + self._imports = [Import('itertools', 'product')] + + decorators = {} + header = None + + if self.backend['name'] == 'pyccel': + func_args = build_pyccel_type_annotations(func_args) + elif self.backend['name'] == 'pythran': + header = build_pythran_types_header(self.name, func_args) + + return FunctionDef(self.name, list(func_args), [], body, + decorators=decorators, header=header) diff --git a/psydac/api/ast/nodes.py b/psydac/api/ast/nodes.py new file mode 100644 index 000000000..caa76b554 --- /dev/null +++ b/psydac/api/ast/nodes.py @@ -0,0 +1,2573 @@ +# -*- coding: UTF-8 -*- +import numpy as np + +from itertools import product + +from sympy import Basic, Expr +from sympy import AtomicExpr, S +from sympy import Function +from sympy import Mul, Integer +from sympy.core.singleton import Singleton +from sympy.core.containers import Tuple +from sympy.utilities.iterables import iterable as is_iterable + +from sympde.old_sympy_utilities import with_metaclass + +from sympde.topology import element_of +from sympde.topology import ScalarFunction, VectorFunction +from sympde.topology import VectorFunctionSpace +from sympde.topology import IndexedVectorFunction +from sympde.topology import H1SpaceType, L2SpaceType, UndefinedSpaceType +from sympde.topology import Mapping +from sympde.topology import dx1, dx2, dx3 +from sympde.topology import get_atom_logical_derivatives +from sympde.topology import Interface + +from psydac.pyccel.ast.core import AugAssign, Assign, Slice +from psydac.pyccel.ast.core import _atomic +from psydac.api.utilities import flatten + +#============================================================================== +# TODO move it +import string +import random +def random_string( n ): + chars = string.ascii_lowercase + string.digits + selector = random.SystemRandom() + return ''.join( selector.choice( chars ) for _ in range( n ) ) + +#============================================================================== +def toInteger(a): + if isinstance(a,(int, np.int64)): + return Integer(int(a)) + return a +#============================================================================== +_logical_partial_derivatives = (dx1, dx2, dx3) + +def get_number_derivatives(expr): + """ + returns the number of partial derivatives in expr. + this assumes that expr is of the + form d(a) where a is a single atom. + """ + n = 0 + if isinstance(expr, _logical_partial_derivatives): + assert(len(expr.args) == 1) + + n += 1 + get_number_derivatives(expr.args[0]) + return n + +#============================================================================== +class ZerosLike(Function): + @property + def rhs(self): + return self._args[0] + +class Zeros(Function): + def __new__(cls, shape, dtype='float64'): + return Basic.__new__(cls, shape, dtype) + + @property + def shape(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + +class Array(Function): + def __new__(cls, data, dtype=None): + return Basic.__new__(cls, data, dtype) + + @property + def data(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + +class FloorDiv(Function): + def __new__(cls, arg1, arg2): + if arg2 == 1: + return arg1 + else: + return Basic.__new__(cls, arg1, arg2) + + @property + def arg1(self): + return self._args[0] + + @property + def arg2(self): + return self._args[1] + +class Max(Expr): + pass + +class Min(Expr): + pass + +class Allocate(Basic): + + def __new__(cls, arr, shape): + return Basic.__new__(cls, arr, shape) + + @property + def array(self): + return self._args[0] + + @property + def shape(self): + return self._args[1] +#============================================================================== +class VectorAssign(Basic): + + def __new__(cls, lhs, rhs, op=None): + return Basic.__new__(cls, lhs, rhs, op) + + @property + def lhs(self): + return self._args[0] + + @property + def rhs(self): + return self._args[1] + + @property + def op(self): + return self._args[2] +#============================================================================== +class ArityType(with_metaclass(Singleton, Basic)): + """Base class representing a form type: bilinear/linear/functional""" + +class BilinearArity(ArityType): + pass + +class LinearArity(ArityType): + pass + +class FunctionalArity(ArityType): + pass + +#============================================================================== +class LengthNode(Expr): + """Base class representing one length of an iterator""" + def __new__(cls, target=None, index=None): + obj = Basic.__new__(cls, target, index) + return obj + + @property + def target(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + def set_index(self, index): + obj = type(self)(target=self.target, index=index) + return obj + +class LengthElement(LengthNode): + pass + +class LengthQuadrature(LengthNode): + pass + +class LengthDof(LengthNode): + pass + +class LengthDofTrial(LengthNode): + pass + +class LengthDofTest(LengthNode): + pass + +class LengthOuterDofTest(LengthNode): + pass + +class LengthInnerDofTest(LengthNode): + pass + +class NumThreads(LengthNode): + pass + +class TensorExpression(Expr): + def __new__(cls, *args): + return Expr.__new__(cls, *args) + +class TensorIntDiv(TensorExpression): + pass + +class TensorAdd(TensorExpression): + pass + +class TensorMul(TensorExpression): + pass + +class TensorMax(TensorExpression): + pass + +class TensorInteger(TensorExpression): + pass + +#============================================================================== +class TensorAssignExpr(Basic): + def __new__(cls, lhs, rhs): + assert isinstance(lhs, (Expr, Tuple)) + assert isinstance(rhs, Expr) + return Basic.__new__(cls, lhs, rhs) + + @property + def lhs(self): + return self._args[0] + + @property + def rhs(self): + return self._args[1] + +#============================================================================== +class IndexNode(Expr): + """Base class representing one index of an iterator""" + def __new__(cls, start=0, stop=None, length=None, index=None): + obj = Basic.__new__(cls) + obj._start = start + obj._stop = stop + obj._length = length + obj._index = index + return obj + + @property + def start(self): + return self._start + + @property + def stop(self): + return self._stop + + @property + def length(self): + return self._length + + @property + def index(self): + return self._index + + def set_range(self, start=TensorInteger(0), stop=None, length=None): + if length is None: + length = stop + obj = type(self)(start=start, stop=stop, length=length, index=self.index) + return obj + + def set_index(self, index): + obj = type(self)(start=self.start, stop=self.stop, length=self.length, index=index) + return obj + + def _hashable_content(self): + args = (self.start, self.stop, self.length, self.index) + return tuple([a for a in args if a is not None]) + +class IndexElement(IndexNode): + pass + +class IndexQuadrature(IndexNode): + pass + +class IndexDof(IndexNode): + pass + +class IndexDofTrial(IndexDof): + pass + +class IndexDofTest(IndexDof): + pass + +class IndexOuterDofTest(IndexDof): + pass + +class IndexInnerDofTest(IndexDof): + pass + +class ThreadId(IndexDof): + pass + +class ThreadCoordinates(IndexDof): + pass + +class NeighbourThreadCoordinates(IndexDof): + pass + +class LocalIndexElement(IndexDof): + pass + +class IndexDerivative(IndexNode): + def __new__(cls, length=None): + return Basic.__new__(cls) + + def _hashable_content(self): + return type(self).__mro__ + +index_element = IndexElement() +thread_id = ThreadId(length=NumThreads()) +thread_coords = ThreadCoordinates() +neighbour_threads = NeighbourThreadCoordinates() +index_quad = IndexQuadrature() +index_dof = IndexDof() +index_dof_test = IndexDofTest() +index_dof_trial = IndexDofTrial() +index_deriv = IndexDerivative() +index_outer_dof_test = IndexOuterDofTest() +index_inner_dof_test = IndexInnerDofTest() +local_index_element = LocalIndexElement() + +#============================================================================== +class RankNode(with_metaclass(Singleton, Basic)): + """Base class representing a rank of an iterator""" + pass + +class RankDimension(RankNode): + pass + +rank_dim = RankDimension() + +#============================================================================== +class BaseNode(Basic): + """ + """ + pass + +#============================================================================== +class Element(BaseNode): + """ + """ + pass + +#============================================================================== +class Pattern(Tuple): + """ + """ + pass +#============================================================================== +class Mask(Basic): + def __new__(cls, axis, ext): + return Basic.__new__(cls, axis, ext) + + @property + def axis(self): + return self._args[0] + + @property + def ext(self): + return self._args[1] +#============================================================================== +class EvalField(BaseNode): + """ + This function computes atomic expressions needed + to evaluate EvaluteField/VectorField final expression + + Parameters + ---------- + domain : + Sympde Domain object + + atoms: tuple_like (Expr) + The atomic expression to be evaluated + + q_index: + Indices used for the quadrature loops + + l_index : + Indices used for the basis loops + + q_basis : + The 1d basis function of the tensor-product space + + coeffs : tuple_like (CoefficientBasis) + Coefficient of the basis function + + l_coeffs : tuple_like (MatrixLocalBasis) + Local coefficient of the basis functions + + g_coeffs : tuple_like (MatrixGlobalBasis) + Global coefficient of the basis functions + + tests : tuple_like (Variable) + The field to be evaluated + + mapping : + Sympde Mapping object + + nderiv : int + Maximum number of derivatives + + mask : int,optional + The fixed direction in case of a boundary integral + + """ + + def __new__(cls, domain, atoms, q_index, l_index, q_basis, coeffs, l_coeffs, g_coeffs, tests, mapping, nderiv, mask=None, dtype='real', quad_loop=None): + + stmts_1 = [] + stmts_2 = {} + inits = [] + mats = [] + zero = 0.j if dtype=='complex' else 0. + + old_basis = q_basis.target + + if isinstance(old_basis, IndexedVectorFunction): + space = old_basis.base.space + name = old_basis.base.name + basis = element_of(space, name=name+'_field') + basis = basis[old_basis.indices[0]] + else: + space = old_basis.space + name = old_basis.name + + basis = element_of(space, name=name+'_field') + + for v in tests: + stmts_1 += list(zip(construct_logical_expressions(v, nderiv, lhs=v.subs(old_basis,basis)), + construct_logical_expressions(v.subs(old_basis,basis),nderiv))) + + logical_atoms = [[a[0].expr,a[1].expr] for a in stmts_1] + + stmts_1 = [st[0] for st in stmts_1] + + new_atoms = {} + for a in logical_atoms: + atom = str(get_atom_logical_derivatives(a[0])) + if atom in new_atoms: + new_atoms[atom] += [a] + else: + new_atoms[atom] = [a] + + logical_atoms = new_atoms + for coeff, l_coeff in zip(coeffs,l_coeffs): + for a in logical_atoms[str(coeff.target)]: + node = AtomicNode(a[1]) + if quad_loop: + mat = MatrixQuadrature(a[0], dtype) + val = ProductGenerator(mat, q_index) + else: + val = AtomicNode(a[0]) + rhs = Mul(coeff,node) + stmts_1 += [AugAssign(val, '+', rhs)] + + if quad_loop: + mats += [mat] + inits += [Assign(AtomicNode(a[0]),val)] + else: + inits += [Assign(val, zero)] + + stmts_2[coeff] = Assign(coeff, ProductGenerator(l_coeff, l_index)) + + if quad_loop: + body = Loop( q_basis, q_index, stmts=stmts_1, mask=mask) + stmts_2 = [*stmts_2.values(), body] + body = Loop((), l_index, stmts=stmts_2) + else: + stmts_2 = flatten([*stmts_2.values(), *stmts_1]) + + body = Loop((q_basis), l_index, stmts=stmts_2) + + inits2 = [] + dim = domain.dim + lhs_slices = (Slice(None,None),)*dim + if quad_loop: + inits2 = [Assign(ProductGenerator(mat,Tuple(lhs_slices)), zero) for mat in mats] + + from psydac.api.ast.fem import expand + ex_tests = expand(tests) + + for coeff, l_coeff in zip(g_coeffs, l_coeffs): + basis = coeff.test + index = ex_tests.index(basis) + basis = basis if basis in tests else basis.base + degrees = [LengthDofTest(basis, i) for i in range(dim)] + spans = [Span(basis, i) for i in range(dim)] + pads = [Pads(tests, test_index=index, dim_index=i) for i in range(dim)] + rhs_starts = [AddNode(pads[i],spans[i],MulNode(Integer(-1),degrees[i])) for i in range(dim)] + rhs_ends = [AddNode(pads[i],spans[i],Integer(1)) for i in range(dim)] + rhs_slices = tuple(Slice(toInteger(s), toInteger(e)) for s,e in zip(rhs_starts, rhs_ends)) + stmt = Assign(ProductGenerator(l_coeff,Tuple(lhs_slices)), ProductGenerator(coeff,Tuple(rhs_slices))) + inits2.append(stmt) + + if quad_loop: + body = Block([*inits2, body]) + else: + body = Block([*inits, body]) + inits = Block(inits2) + + obj = Basic.__new__(cls, inits, body, dtype) + obj._pads = Pads(tests) + return obj + + @property + def inits(self): + return self._args[0] + + @property + def body(self): + return self._args[1] + + @property + def dtype(self): + return self._args[2] + + @property + def pads(self): + return self._pads + +class RAT(Basic): + pass +#============================================================================== +class EvalMapping(BaseNode): + """ + This function computes atomic expressions needed + to evaluate EvalMapping final expression. + + Parameters + ---------- + domain : + Sympde Domain object + + quads: + Indices used for the quadrature loops + + indices_basis : + Indices used for the basis loops + + q_basis : + The 1d basis function of the tensor-product space + + mapping : + Sympde Mapping object + + components : + The 1d coefficients of the mapping + + mapping_space : + The vector space of the mapping + + nderiv : + Maximum number of derivatives + + mask : int,optional + The fixed direction in case of a boundary integral + + is_rational: bool,optional + True if the mapping is rational + + """ + def __new__(cls, domain, quads, indices_basis, q_basis, mapping, components, mapping_space, nderiv, mask=None, is_rational=None, trial=None, quad_loop=None): + mapping_atoms = components.arguments + basis = q_basis + target = basis.target + multiplicity = tuple(mapping_space.coeff_space.shifts) if mapping_space else () + pads = tuple(mapping_space.coeff_space.pads) if mapping_space else () + quad_loop = True if quad_loop is None else quad_loop + + if isinstance(target, IndexedVectorFunction): + space = target.base.space + else: + space = target.space + + if mapping.is_plus: + weight = element_of(space, name='weight_plus') + else: + weight = element_of(space, name='weight') + + if isinstance(target, VectorFunction): + target = target[0] + + if isinstance(weight, VectorFunction): + weight = weight[0] + + l_coeffs = [] + g_coeffs = [] + values = set() + + mapping_atoms = sorted(mapping_atoms,key=get_number_derivatives, reverse=True) + components = [get_atom_logical_derivatives(a) for a in mapping_atoms] + test_atoms = [a.subs(comp, target) for a,comp in zip(mapping_atoms, components)] + weights = [a.subs(comp, weight) for a,comp in zip(mapping_atoms, components)] + + stmts = [ComputeLogicalBasis(v,) for v in set(test_atoms)] + declarations = [] + rationalization = [] + for test,mapp,comp in zip(test_atoms, mapping_atoms, components): + test = AtomicNode(test) + if quad_loop: + val = ProductGenerator(MatrixQuadrature(mapp), quads) + else: + val = mapp + + if is_rational: + rhs = Mul(CoefficientBasis(comp),CoefficientBasis(weight),test) + else: + rhs = Mul(CoefficientBasis(comp),test) + stmts += [AugAssign(val, '+', rhs)] + + l_coeff = MatrixLocalBasis(comp) + g_coeff = MatrixGlobalBasis(comp, target) + declarations += [Assign(mapp, val)] + if l_coeff not in l_coeffs: + l_coeffs.append(l_coeff) + g_coeffs.append(g_coeff) + + values.add(val.target if quad_loop else val) + + if is_rational: + l_coeffs.append(MatrixLocalBasis(weight)) + g_coeffs.append(MatrixGlobalBasis(weight, target)) + + for node, w in set(zip(test_atoms, weights)): + node = AtomicNode(node) + if quad_loop: + val = ProductGenerator(MatrixQuadrature(w), quads) + else: + val = w + rhs = Mul(CoefficientBasis(weight),node) + stmts += [AugAssign(val, '+', rhs)] + values.add(val.target if quad_loop else val) + declarations += [Assign(w,val)] + + for test,mapp,w in zip(test_atoms, mapping_atoms, weights): + comp = get_atom_logical_derivatives(mapp) + if quad_loop: + lhs = ProductGenerator(MatrixQuadrature(mapp), quads) + else: + lhs = mapp + rhs = mapp.subs(comp,comp/weight) + rationalization += [Assign(lhs, rhs)] + + rationalization = [*declarations, *rationalization] + + loop = Loop((q_basis,*l_coeffs), indices_basis, stmts=stmts) + + if quad_loop: + stmts = [Loop((), quads, stmts=[loop, *rationalization], mask=mask)] + else: + stmts = [loop, *rationalization] + + dim = domain.dim + test = g_coeffs[0].test + lhs_slices = (Slice(None,None),)*dim + is_trial = trial + spans = [Span(test, i) for i in range(dim)] + degrees = [LengthDofTest(test, i) for i in range(dim)] + inits = [] + for coeff, l_coeff in zip(g_coeffs, l_coeffs): + rhs_starts = [multiplicity[i]*pads[i] + spans[i]-degrees[i] for i in range(dim)] + rhs_ends = [multiplicity[i]*pads[i] + spans[i]+1 for i in range(dim)] + if isinstance(domain, Interface) and is_trial and mapping.is_plus : + axis = domain.plus.axis + rhs_starts[axis] = multiplicity[axis]*pads[axis] + rhs_ends[axis] = multiplicity[axis]*pads[axis] + degrees[axis] + 1 + elif isinstance(domain, Interface) and is_trial and mapping.is_minus : + axis = domain.minus.axis + rhs_starts[axis] = multiplicity[axis]*pads[axis] + rhs_ends[axis] = multiplicity[axis]*pads[axis] + degrees[axis] + 1 + + rhs_slices = tuple(Slice(toInteger(s), toInteger(e)) for s,e in zip(rhs_starts, rhs_ends)) + stmt = Assign(ProductGenerator(l_coeff,Tuple(lhs_slices)), ProductGenerator(coeff,Tuple(rhs_slices))) + inits.append(stmt) + + for val in values: + if quad_loop: + stmts = [Assign(ProductGenerator(val, Tuple(lhs_slices)), 0.)] + stmts + else: + stmts = [Assign(val, 0.)] + stmts + + obj = Basic.__new__(cls, Block(stmts), Block(inits), g_coeffs) + return obj + + @property + def stmts(self): + return self._args[0] + + @property + def inits(self): + return self._args[1] + + @property + def coeffs(self): + return self._args[2] + +#============================================================================== +class IteratorBase(BaseNode): + """ + """ + def __new__(cls, target, dummies=None): + if not dummies is None: + if not isinstance(dummies, (list, tuple, Tuple)): + dummies = [dummies] + dummies = Tuple(*dummies) + + return Basic.__new__(cls, target, dummies) + + @property + def target(self): + return self._args[0] + + @property + def dummies(self): + return self._args[1] + +#============================================================================== +class TensorIterator(IteratorBase): + pass + +#============================================================================== +class ProductIterator(IteratorBase): + pass + +#============================================================================== +# TODO dummies should not be None +class GeneratorBase(BaseNode): + """ + """ + def __new__(cls, target, dummies): + if not isinstance(dummies, (list, tuple, Tuple)): + dummies = [dummies] + dummies = Tuple(*dummies) + + if not isinstance(target, (ArrayNode, MatrixNode, Expr)): + raise TypeError('expecting ArrayNode, MatrixNode or Expr') + + return Basic.__new__(cls, target, dummies) + + @property + def target(self): + return self._args[0] + + @property + def dummies(self): + return self._args[1] + +#============================================================================== +class TensorGenerator(GeneratorBase): + """ + This class represent an array list of array elements with arbitrary number of dimensions. + the length of the list is given by the rank of target. + + Parameters + ---------- + + target : + the array object + + dummies : + multidimensional index + + Examples + -------- + >>> T = TensorGenerator(GlobalTensorQuadrature(), index_quad) + >>> T + TensorGenerator(GlobalTensorQuadrature(), (IndexQuadrature(),)) + >>> ast = parse(T, settings={'dim':2,'nderiv':2,'target':Square()}) + >>> ast[0] + ((IndexedElement(local_x1, i_quad_1), IndexedElement(local_w1, i_quad_1)), + (IndexedElement(local_x2, i_quad_2), IndexedElement(local_w2, i_quad_2))) + """ + +#============================================================================== +class ProductGenerator(GeneratorBase): + """ + This class represent an element of an array with arbitrary number of dimensions. + + Parameters + ---------- + + target : + the array object + + dummies : + multidimensional index + + Examples + -------- + >>> P = ProductGenerator(MatrixRankFromCoords(), thread_coords) + >>> P + ProductGenerator(MatrixRankFromCoords(), (ThreadCoordinates(),)) + >>> ast = parse(P, settings={'dim':2,'nderiv':2,'target':Square()}) + >>> ast + IndexedElement(rank_from_coords, thread_coords_1, thread_coords_2) + """ + +#============================================================================== +class Grid(BaseNode): + """ + """ + pass + +#============================================================================== +class ScalarNode(BaseNode, AtomicExpr): + """ + """ + pass + +#============================================================================== +class ArrayNode(BaseNode, AtomicExpr): + """ + """ + _rank = None + _positions = None + _free_indices = None + + @property + def rank(self): + return self._rank + + @property + def positions(self): + return self._positions + + @property + def free_indices(self): + if self._free_indices is None: + return list(self.positions.keys()) + + else: + return self._free_indices + + def pattern(self): + positions = {} + for a in self.free_indices: + positions[a] = self.positions[a] + + args = [None]*self.rank + for k,v in positions.items(): + args[v] = k + + return Pattern(*args) + +#============================================================================== +class MatrixNode(ArrayNode): + pass + +class BlockLinearOperatorNode(MatrixNode): + pass + +#============================================================================== +class GlobalTensorQuadratureGrid(ArrayNode): + """This class represents the quadrature points and weights in a domain. + """ + _rank = 2 + _positions = {index_element: 0, index_quad: 1} + _free_indices = [index_element] + + def __init__(self, weights=True): + self._weights = weights + + @property + def weights( self ): + return self._weights + +#============================================================================== +class PlusGlobalTensorQuadratureGrid(GlobalTensorQuadratureGrid): + """This class represents the quadrature points and weights in the plus side of an interface. + """ + +#============================================================================== +class LocalTensorQuadratureGrid(ArrayNode): + """ This class represents the element wise quadrature points and weights in a domain. + """ + _rank = 1 + _positions = {index_quad: 0} + + def __init__(self, weights=True): + self._weights = weights + + @property + def weights( self ): + return self._weights + +#============================================================================== +class PlusLocalTensorQuadratureGrid(LocalTensorQuadratureGrid): + """This class represents the element wise quadrature points and weights in the plus side of an interface. + """ + +#============================================================================== +class TensorQuadrature(ScalarNode): + """This class represents the quadrature point and weight in a domain.""" + + def __init__(self, weights=True): + self._weights = weights + + @property + def weights( self ): + return self._weights + +#============================================================================== +class PlusTensorQuadrature(TensorQuadrature): + """This class represents the quadrature point and weight in the plus side of an interface. + """ + +#============================================================================== +class MatrixQuadrature(MatrixNode): + """ + """ + _rank = rank_dim + + def __new__(cls, target, dtype='real'): + # TODO check target + return Basic.__new__(cls, target, dtype) + + @property + def target(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + +#============================================================================== +class MatrixRankFromCoords(MatrixNode): + pass +#============================================================================== +class MatrixCoordsFromRank(MatrixNode): + pass +#============================================================================== +class WeightedVolumeQuadrature(ScalarNode): + """ + """ + pass + +#============================================================================== +class GlobalTensorQuadratureBasis(ArrayNode): + """ + """ + _rank = 4 + _positions = {index_quad: 3, index_deriv: 2, index_dof: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof] + + def __new__(cls, target, index=None): + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + return Basic.__new__(cls, target, index) + + @property + def target(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + @property + def unique_scalar_space(self): + unique_scalar_space = True + if isinstance(self.target, IndexedVectorFunction): + return True + space = self.target.space + if isinstance(space, VectorFunctionSpace): + unique_scalar_space = isinstance(space.kind, (UndefinedSpaceType, H1SpaceType, L2SpaceType)) + return unique_scalar_space + + @property + def is_scalar(self): + return isinstance(self.target, (ScalarFunction, IndexedVectorFunction)) + + def set_index(self, index): + return type(self)(self.target, index) +#============================================================================== +class LocalTensorQuadratureBasis(ArrayNode): + """ + """ + _rank = 4 + _positions = {index_quad: 3, index_deriv: 2, index_dof: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof] + + def __new__(cls, target, index=None): + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + return Basic.__new__(cls, target, index) + + @property + def target(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + @property + def unique_scalar_space(self): + unique_scalar_space = True + if isinstance(self.target, IndexedVectorFunction): + return True + space = self.target.space + if isinstance(space, VectorFunctionSpace): + unique_scalar_space = isinstance(space.kind, (UndefinedSpaceType, H1SpaceType, L2SpaceType)) + return unique_scalar_space + + @property + def is_scalar(self): + return isinstance(self.target, (ScalarFunction, IndexedVectorFunction)) + + def set_index(self, index): + return type(self)(self.target, index) +#============================================================================== +class TensorQuadratureBasis(ArrayNode): + """ + """ + _rank = 2 + _positions = {index_quad: 1, index_deriv: 0} + _free_indices = [index_quad] + + def __new__(cls, target): + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + + return Basic.__new__(cls, target) + + @property + def target(self): + return self._args[0] + + @property + def unique_scalar_space(self): + unique_scalar_space = True + if isinstance(self.target, IndexedVectorFunction): + return True + space = self.target.space + if isinstance(space, VectorFunctionSpace): + unique_scalar_space = isinstance(space.kind, (UndefinedSpaceType, H1SpaceType, L2SpaceType)) + return unique_scalar_space + + @property + def is_scalar(self): + return isinstance(self.target, (ScalarFunction, IndexedVectorFunction)) +#============================================================================== +class CoefficientBasis(ScalarNode): + """ + """ + def __new__(cls, target): + ls = target.atoms(ScalarFunction, VectorFunction, Mapping) + if not len(ls) == 1: + raise TypeError('Expecting a scalar/vector test function or a Mapping') + return Basic.__new__(cls, target) + + @property + def target(self): + return self._args[0] +#============================================================================== +class TensorBasis(CoefficientBasis): + pass + +#============================================================================== +class GlobalTensorQuadratureTestBasis(GlobalTensorQuadratureBasis): + _positions = {index_quad: 3, index_deriv: 2, index_dof_test: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof_test] + +#============================================================================== +class LocalTensorQuadratureTestBasis(LocalTensorQuadratureBasis): + _positions = {index_quad: 3, index_deriv: 2, index_dof_test: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof_test] + +#============================================================================== +class TensorQuadratureTestBasis(TensorQuadratureBasis): + pass + +#============================================================================== +class TensorTestBasis(TensorBasis): + pass + +#============================================================================== +class GlobalTensorQuadratureTrialBasis(GlobalTensorQuadratureBasis): + _positions = {index_quad: 3, index_deriv: 2, index_dof_trial: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof_trial] + +#============================================================================== +class LocalTensorQuadratureTrialBasis(LocalTensorQuadratureBasis): + _positions = {index_quad: 3, index_deriv: 2, index_dof_trial: 1, index_element: 0} + _free_indices = [index_element, index_quad, index_dof_trial] + +#============================================================================== +class TensorQuadratureTrialBasis(TensorQuadratureBasis): + pass + +#============================================================================== +class TensorTrialBasis(TensorBasis): + pass + +class MatrixGlobalBasis(MatrixNode): + """ + used to describe global dof + """ + _rank = rank_dim + + def __new__(cls, target, test, dtype='real'): + # TODO check target + return Basic.__new__(cls, target, test, dtype) + + @property + def target(self): + return self._args[0] + + @property + def test(self): + return self._args[1] + + @property + def dtype(self): + return self._args[2] + + def __getitem__(self, a): + return IndexedMatrixGlobalBasis(self, a) +#============================================================================== +class MatrixLocalBasis(MatrixNode): + """ + used to describe local dof over an element + """ + _rank = rank_dim + + def __new__(cls, target, dtype='real'): + # TODO check target + return Basic.__new__(cls, target, dtype) + + @property + def target(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + + def __getitem__(self, a): + return IndexedMatrixLocalBasis(self, a) + +#============================================================================== +class StencilMatrixLocalBasis(MatrixNode): + """ + used to describe local dof over an element as a stencil matrix + """ + def __new__(cls, u, v, pads, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = 2 * len(pads) + name = (u, v) + tag = tag or random_string(6) + + return Basic.__new__(cls, pads, rank, name, tag, dtype) + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def name(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def dtype(self): + return self._args[4] + +#============================================================================== +class StencilMatrixGlobalBasis(MatrixNode): + """ + used to describe local dof over an element as a stencil matrix + """ + def __new__(cls, u, v, pads, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = 2 * len(pads) + name = (u, v) + tag = tag or random_string(6) + + return Basic.__new__(cls, pads, rank, name, tag, dtype) + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def name(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def dtype(self): + return self._args[4] + +#============================================================================== +class StencilVectorLocalBasis(MatrixNode): + """ + used to describe local dof over an element as a stencil vector + """ + def __new__(cls, v, pads, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = len(pads) + name = v + tag = tag or random_string(6) + + return Basic.__new__(cls, pads, rank, name, tag, dtype) + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def name(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def dtype(self): + return self._args[4] + +#============================================================================== +class StencilVectorGlobalBasis(MatrixNode): + """ + used to describe local dof over an element as a stencil vector + """ + def __new__(cls, v, pads, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = len(pads) + name = v + tag = tag or random_string(6) + + return Basic.__new__(cls, pads, rank, name, tag, dtype) + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def name(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def dtype(self): + return self._args[4] + + +#============================================================================== +class LocalElementBasis(MatrixNode): + tag = random_string( 6 ) + +class GlobalElementBasis(MatrixNode): + tag = random_string( 6 ) + +#============================================================================== +class BlockStencilMatrixLocalBasis(BlockLinearOperatorNode): + """ + used to describe local dof over an element as a block stencil matrix + """ + def __new__(cls, trials, tests, expr, dim, tag=None, outer=None, + tests_degree=None, trials_degree=None, + tests_multiplicity=None, trials_multiplicity=None, dtype='real'): + + pads = Pads(tests, trials, tests_degree, trials_degree, + tests_multiplicity, trials_multiplicity) + + rank = 2 * dim + tag = tag or random_string(6) + + obj = Basic.__new__(cls, pads, rank, trials_multiplicity, tag, expr, dtype) + obj._trials = trials + obj._tests = tests + obj._outer = outer + return obj + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def trials_multiplicity(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def expr(self): + return self._args[4] + + @property + def dtype(self): + return self._args[5] + + @property + def outer(self): + return self._outer + + @property + def unique_scalar_space(self): + types = (H1SpaceType, L2SpaceType, UndefinedSpaceType) + spaces = self.trials.space + cond = False + for cls in types: + cond = cond or all(isinstance(space.kind, cls) for space in spaces) + return cond + +#============================================================================== +class BlockStencilMatrixGlobalBasis(BlockLinearOperatorNode): + """ + used to describe local dof over an element as a block stencil matrix + """ + def __new__(cls, trials, tests, pads, multiplicity, expr, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = 2 * len(pads) + tag = tag or random_string(6) + + obj = Basic.__new__(cls, pads, multiplicity, rank, tag, expr, dtype) + obj._trials = trials + obj._tests = tests + return obj + + @property + def pads(self): + return self._args[0] + + @property + def multiplicity(self): + return self._args[1] + + @property + def rank(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def expr(self): + return self._args[4] + + @property + def dtype(self): + return self._args[5] + + @property + def unique_scalar_space(self): + types = (H1SpaceType, L2SpaceType, UndefinedSpaceType) + spaces = self.trials.space + cond = False + for cls in types: + cond = cond or all(isinstance(space.kind, cls) for space in spaces) + return cond + +#============================================================================== +class BlockStencilVectorLocalBasis(BlockLinearOperatorNode): + """ + used to describe local dof over an element as a block stencil matrix + """ + def __new__(cls,tests, pads, expr, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = len(pads) + tag = tag or random_string(6) + + obj = Basic.__new__(cls, pads, rank, tag, expr, dtype) + obj._tests = tests + return obj + + @property + def pads(self): + return self._args[0] + + @property + def rank(self): + return self._args[1] + + @property + def tag(self): + return self._args[2] + + @property + def expr(self): + return self._args[3] + + @property + def dtype(self): + return self._args[4] + + @property + def unique_scalar_space(self): + types = (H1SpaceType, L2SpaceType, UndefinedSpaceType) + spaces = self._tests.space + cond = False + for cls in types: + cond = cond or all(isinstance(space.kind, cls) for space in spaces) + return cond + +#============================================================================== +class BlockStencilVectorGlobalBasis(BlockLinearOperatorNode): + """ + used to describe local dof over an element as a block stencil matrix + """ + def __new__(cls, tests, pads, multiplicity, expr, tag=None, dtype='real'): + + if not is_iterable(pads): + raise TypeError('Expecting an iterable') + + pads = Tuple(*pads) + rank = len(pads) + tag = tag or random_string(6) + + obj = Basic.__new__(cls, pads, multiplicity, rank, tag, expr, dtype) + obj._tests = tests + return obj + + @property + def pads(self): + return self._args[0] + + @property + def multiplicity(self): + return self._args[1] + + @property + def rank(self): + return self._args[2] + + @property + def tag(self): + return self._args[3] + + @property + def expr(self): + return self._args[4] + + @property + def dtype(self): + return self._args[5] + + @property + def unique_scalar_space(self): + types = (H1SpaceType, L2SpaceType, UndefinedSpaceType) + spaces = self._tests.space + cond = False + for cls in types: + cond = cond or all(isinstance(space.kind, cls) for space in spaces) + return cond + +#============================================================================== +class ScalarLocalBasis(ScalarNode): + """ + This is used to describe scalar dof over an element + """ + def __new__(cls, u=None, v=None, tag=None, dtype='real'): + + tag = tag or random_string(6) + + obj = Basic.__new__(cls, tag, dtype) + obj._test = v + obj._trial = u + return obj + + @property + def tag(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + + @property + def trial(self): + return self._trial + + @property + def test(self): + return self._test + +#============================================================================== +class BlockScalarLocalBasis(ScalarNode): + """ + This is used to describe a block of scalar dofs over an element + """ + def __new__(cls, trials=None, tests=None, expr=None, tag=None, dtype='real'): + + tag = tag or random_string(6) + + obj = Basic.__new__(cls, tag, dtype) + obj._tests = tests + obj._trials = trials + obj._expr = expr + return obj + + @property + def tag(self): + return self._args[0] + + @property + def dtype(self): + return self._args[1] + + @property + def tests(self): + return self._tests + + @property + def trials(self): + return self._trials + + @property + def expr(self): + return self._expr + +#============================================================================== +class SpanArray(ArrayNode): + """ + This represents the global span array + """ + + def __new__(cls, target, index=None): + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + + return Basic.__new__(cls, target, index) + + @property + def target(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + def set_index(self, index): + return type(self)(self.target, index) + +#============================================================================== +class GlobalSpanArray(SpanArray): + """ + This represents the global span array + """ + _rank = 1 + _positions = {index_element: 0} + +#============================================================================== +class LocalSpanArray(SpanArray): + """ + This represents the local span array + """ + _rank = 1 + _positions = {index_element: 0} + +#============================================================================== +class GlobalThreadSpanArray(SpanArray): + """ + This represents the global span array of each thread + """ + _rank = 1 + +#============================================================================== +class GlobalThreadStarts(ArrayNode): + """ + This represents the threads starts over the decomposed domain + """ + _rank = 1 + def __new__(cls, index=None): + # TODO check target + return Basic.__new__(cls, index) + + @property + def index(self): + return self._args[0] + + def set_index(self, index): + return GlobalThreadStarts(index) + +#============================================================================== +class GlobalThreadEnds(ArrayNode): + """ + This represents the threads ends over the decomposed domain + """ + _rank = 1 + def __new__(cls, index=None): + # TODO check target + return Basic.__new__(cls, index) + + @property + def index(self): + return self._args[0] + + def set_index(self, index): + return GlobalThreadEnds(index) + +#============================================================================== +class GlobalThreadSizes(ArrayNode): + """ + This represents the number of elements owned by a thread + """ + _rank = 1 + def __new__(cls, index=None): + # TODO check target + return Basic.__new__(cls, index) + + @property + def index(self): + return self._args[0] + + def set_index(self, index): + return GlobalThreadSizes(index) + +#============================================================================== +class LocalThreadStarts(ArrayNode): + """ + This represents the local threads starts over the decomposed domain + """ + _rank = 1 + def __new__(cls, index=None): + # TODO check target + return Basic.__new__(cls, index) + + @property + def index(self): + return self._args[0] + + def set_index(self, index): + return LocalThreadStarts(index) + +#============================================================================== +class LocalThreadEnds(ArrayNode): + """ + This represents the local threads ends over the decomposed domain + """ + _rank = 1 + def __new__(cls, index=None): + # TODO check target + return Basic.__new__(cls, index) + + @property + def index(self): + return self._args[0] + + def set_index(self, index): + return LocalThreadEnds(index) + +#============================================================================== +class Span(ScalarNode): + """ + This represents the span of a basis in an element + """ + def __new__(cls, target, index=None): + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + + return Basic.__new__(cls, target, index) + + @property + def target(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + def set_index(self, index): + return Span(self.target, index) + +class Pads(ScalarNode): + """ + This represents the global pads + """ + def __new__(cls, tests, trials=None, tests_degree=None, trials_degree=None, + tests_multiplicity=None, trials_multiplicity=None, test_index=None, trial_index=None, dim_index=None): + for target in tests: + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + if trials: + for target in trials: + if not isinstance(target, (ScalarFunction, VectorFunction, IndexedVectorFunction)): + raise TypeError('Expecting a scalar/vector test function') + obj = Basic.__new__(cls, tests, trials) + obj._tests_degree = tests_degree + obj._trials_degree = trials_degree + obj._tests_multiplicity = tests_multiplicity + obj._trials_multiplicity = trials_multiplicity + obj._trial_index = trial_index + obj._test_index = test_index + obj._dim_index = dim_index + return obj + + @property + def tests(self): + return self._args[0] + + @property + def trials(self): + return self._args[1] + + @property + def tests_degree(self): + return self._tests_degree + + @property + def trials_degree(self): + return self._trials_degree + + @property + def tests_multiplicity(self): + return self._tests_multiplicity + + @property + def trials_multiplicity(self): + return self._trials_multiplicity + + @property + def test_index(self): + return self._test_index + + @property + def trial_index(self): + return self._trial_index + + @property + def dim_index(self): + return self._dim_index +#============================================================================== +class Evaluation(BaseNode): + """ + """ + pass + +#============================================================================== +class FieldEvaluation(Evaluation): + """ + """ + pass + +#============================================================================== +class MappingEvaluation(Evaluation): + """ + """ + pass + +#============================================================================== +class ComputeNode(Basic): + """ + """ + def __new__(cls, expr): + return Basic.__new__(cls, expr) + + @property + def expr(self): + return self._args[0] + +#============================================================================== +class ComputePhysical(ComputeNode): + """ + """ + pass + +#============================================================================== +class ComputePhysicalBasis(ComputePhysical): + """ + """ + pass + +#============================================================================== +class ComputeKernelExpr(ComputeNode): + """ + """ + def __new__(cls, expr, weights=True): + return Basic.__new__(cls, expr, weights) + + @property + def expr(self): + return self._args[0] + + @property + def weights(self): + return self._args[1] +#============================================================================== +class ComputeLogical(ComputeNode): + """ + """ + def __new__(cls, expr, weights=True, lhs=None): + return Basic.__new__(cls, expr, weights, lhs) + + @property + def expr(self): + return self._args[0] + + @property + def weights(self): + return self._args[1] + + @property + def lhs(self): + return self._args[2] +#============================================================================== +class ComputeLogicalBasis(ComputeLogical): + """ + """ + def __new__(cls, expr, lhs=None): + return Basic.__new__(cls, expr, lhs) + + @property + def expr(self): + return self._args[0] + + @property + def lhs(self): + return self._args[1] +#============================================================================== +class Reduction(Basic): + """ + """ + def __new__(cls, op, expr, lhs=None): + if not op in ['-', '+', '*', '/', None]: + raise TypeError("Expecting an operation type in : '-', '+', '*', '/'") + return Basic.__new__(cls, op, expr, lhs) + + @property + def op(self): + return self._args[0] + + @property + def expr(self): + return self._args[1] + + @property + def lhs(self): + return self._args[2] + +#============================================================================== +class Reduce(Basic): + """ + """ + def __new__(cls, op, rhs, lhs, loop): + if not op in ['-', '+', '*', '/']: + raise TypeError("Expecting an operation type in : '-', '+', '*', '/'") + if not isinstance(loop, Loop): + raise TypeError('Expecting a Loop') + + return Basic.__new__(cls, op, rhs, lhs, loop) + + @property + def op(self): + return self._args[0] + + @property + def rhs(self): + return self._args[1] + + @property + def lhs(self): + return self._args[2] + + @property + def loop(self): + return self._args[3] + +#============================================================================== +class Reset(Basic): + """ + """ + def __new__(cls, var, expr=None): + return Basic.__new__(cls, var, expr) + + @property + def var(self): + return self._args[0] + + @property + def expr(self): + return self._args[1] + +#============================================================================== +class ElementOf(Basic): + """ + """ + def __new__(cls, target): + return Basic.__new__(cls, target) + + @property + def target(self): + return self._args[0] + +#============================================================================== +class ExprNode(Basic): + """ + """ + pass + +#============================================================================== +class AtomicNode(ExprNode, AtomicExpr): + """ + """ + + @property + def expr(self): + return self._args[0] + +#============================================================================== +class ValueNode(ExprNode): + """ + """ + def __new__(cls, expr): + return Basic.__new__(cls, expr) + + @property + def expr(self): + return self._args[0] + +#============================================================================== +class PhysicalValueNode(ValueNode): + pass + +#============================================================================== +class LogicalValueNode(ValueNode): + pass + +#============================================================================== +class PhysicalBasisValue(PhysicalValueNode): + pass + +#============================================================================== +class LogicalBasisValue(LogicalValueNode): + """ + """ + def __new__(cls, expr): + return Basic.__new__(cls, expr) + + @property + def expr(self): + return self._args[0] +#============================================================================== +class PhysicalGeometryValue(PhysicalValueNode): + pass + +#============================================================================== +class LogicalGeometryValue(LogicalValueNode): + pass + +#============================================================================== +class BasisAtom(AtomicNode): + """ + Used to describe a temporary for the basis coefficient or in the kernel. + """ + def __new__(cls, expr): + types = (IndexedVectorFunction, VectorFunction, ScalarFunction) + + ls = _atomic(expr, cls=types) + if not(len(ls) == 1): + raise ValueError('Expecting an expression with one test function') + + u = ls[0] + + obj = Basic.__new__(cls, expr) + obj._atom = u + return obj + + @property + def expr(self): + return self._args[0] + + @property + def atom(self): + return self._atom + +#============================================================================== +class GeometryAtom(AtomicNode): + """ + """ + def __new__(cls, expr): + ls = list(expr.atoms(Mapping)) + if not(len(ls) == 1): + raise ValueError('Expecting an expression with one mapping') + + # TODO + u = ls[0] + + obj = Basic.__new__(cls, expr) + obj._atom = u + return obj + + @property + def expr(self): + return self._args[0] + + @property + def atom(self): + return self._atom + +#============================================================================== +class GeometryExpr(Basic): + """ + """ + def __new__(cls, expr, dtype='real'): + # TODO assert on expr + atom = GeometryAtom(expr) + expr = MatrixQuadrature(expr, dtype) + + return Basic.__new__(cls, atom, expr) + + @property + def atom(self): + return self._args[0] + + @property + def expr(self): + return self._args[1] + +#============================================================================== +class IfNode(BaseNode): + def __new__(cls, *args): + args = tuple(args) + return Basic.__new__(cls, args) + + @property + def args(self): + return self._args[0] + +#============================================================================== +class WhileLoop(BaseNode): + def __new__(cls, condition, body): + body = tuple(body) + return Basic.__new__(cls, condition, body) + + @property + def condition(self): + return self._args[0] + + @property + def body(self): + return self._args[1] +#============================================================================== +class Loop(BaseNode): + """ + class to describe a dimensionless loop of an iterator over a generator. + + Parameters + ---------- + iterable : + list of iterator object + + index : + represent the dimensionless index used in the for loop + + stmts : + list of body statements + + mask : + the masked dimension where we fix the index in that dimension + + parallel : + specifies whether the loop should be executed in parallel or in serial + + default: + specifies the default behavior of the variables in a parallel region + + shared : + specifies the shared variables in the parallel region + + private: + specifies the private variables in the parallel region + + firstprivate: + specifies the first private variables in the parallel region + + lastprivate: + specifies the last private variables in the parallel region + """ + + def __new__(cls, iterable, index, *, stmts=None, mask=None, + parallel=None, default=None, shared=None, + private=None, firstprivate=None, lastprivate=None, + reduction=None): + # ... + if not is_iterable(iterable): + iterable = [iterable] + # ... + + # ... replace GeometryExpressions by a list of expressions + others = [i for i in iterable if not isinstance(i, GeometryExpressions)] + geos = [i.expressions for i in iterable if isinstance(i, GeometryExpressions)] + + iterable = Tuple(*others, *flatten(geos)) + # ... + + # ... + if not isinstance(index, IndexNode): + raise TypeError('Expecting an index node') + # ... + + # ... TODO - add assert w.r.t index type + # - this should be splitted/moved somewhere + iterator = [] + generator = [] + for a in iterable: + i, g = construct_itergener(a, index) + iterator.append(i) + generator.append(g) + # ... + # ... + iterator = Tuple(*iterator) + generator = Tuple(*generator) + # ... + + # ... + if stmts is None: + stmts = [] + elif not is_iterable(stmts): + stmts = [stmts] + + stmts = Tuple(*stmts) + # ... + + obj = Basic.__new__(cls, iterable, index, stmts, mask) + + obj._iterator = iterator + obj._generator = generator + obj._parallel = parallel + obj._default = default + obj._shared = shared + obj._private = private + obj._firstprivate = firstprivate + obj._lastprivate = lastprivate + obj._reduction = reduction + + return obj + + @property + def iterable(self): + return self._args[0] + + @property + def index(self): + return self._args[1] + + @property + def stmts(self): + return self._args[2] + + @property + def mask(self): + return self._args[3] + + @property + def iterator(self): + return self._iterator + + @property + def generator(self): + return self._generator + + @property + def parallel(self): + return self._parallel + + @property + def default(self): + return self._default + + @property + def shared(self): + return self._shared + + @property + def private(self): + return self._private + + @property + def firstprivate(self): + return self._firstprivate + + @property + def lastprivate(self): + return self._lastprivate + + @property + def reduction(self): + return self._reduction + + def get_geometry_stmts(self, mapping): + + l_quad = list(self.generator.atoms(LocalTensorQuadratureGrid)) + if len(l_quad) == 0: + return Tuple() + + l_quad = l_quad[0] + args = [] + if l_quad.weights: + args = [ComputeLogical(WeightedVolumeQuadrature(l_quad))] + return Tuple(*args) + +#============================================================================== +class TensorIteration(BaseNode): + """ + """ + + def __new__(cls, iterator, generator): + # ... + if not( isinstance(iterator, TensorIterator) ): + raise TypeError('Expecting an TensorIterator') + + if not( isinstance(generator, TensorGenerator) ): + raise TypeError('Expecting a TensorGenerator') + # ... + + return Basic.__new__(cls, iterator, generator) + + @property + def iterator(self): + return self._args[0] + + @property + def generator(self): + return self._args[1] + +#============================================================================== +class ProductIteration(BaseNode): + """ + """ + + def __new__(cls, iterator, generator): + # ... + if not isinstance(iterator, ProductIterator): + raise TypeError('Expecting an ProductIterator') + + if not isinstance(generator, ProductGenerator): + raise TypeError('Expecting a ProductGenerator') + # ... + + return Basic.__new__(cls, iterator, generator) + + @property + def iterator(self): + return self._args[0] + + @property + def generator(self): + return self._args[1] + +#============================================================================== +class SplitArray(BaseNode): + """ + """ + def __new__(cls, target, positions, lengths): + + if not is_iterable(positions): + positions = [positions] + positions = Tuple(*positions) + + if not is_iterable(lengths): + lengths = [lengths] + lengths = Tuple(*lengths) + + return Basic.__new__(cls, target, positions, lengths) + + @property + def target(self): + return self._args[0] + + @property + def positions(self): + return self._args[1] + + @property + def lengths(self): + return self._args[2] + +#============================================================================== +def construct_logical_expressions(u, nderiv, lhs=None): + if isinstance(u, IndexedVectorFunction): + dim = u.base.space.ldim + else: + dim = u.space.ldim + + ops = [dx1, dx2, dx3][:dim] + r = range(nderiv+1) + ranges = [r]*dim + indices = product(*ranges) + + indices = list(indices) + indices = [ijk for ijk in indices if sum(ijk) <= nderiv] + + args = [] + lhs_args = [] + u = [u] if isinstance(u, (ScalarFunction, IndexedVectorFunction)) else [u[i] for i in range(dim)] + + if lhs is not None: + lhs = [lhs]*len(u) if isinstance(lhs, (ScalarFunction, IndexedVectorFunction)) else [lhs[i] for i in range(dim)] + else: + lhs = [lhs]*len(u) + + for ijk in indices: + for atom,lhs_atom in zip(u,lhs): + for n,op in zip(ijk, ops): + for _ in range(1, n+1): + atom = op(atom) + if lhs_atom is not None: + lhs_atom = op(lhs_atom) + args.append(atom) + lhs_args.append(lhs_atom) + + return [ComputeLogicalBasis(i, lhs=a) for i,a in zip(args, lhs_args)] + +#============================================================================== +class GeometryExpressions(Basic): + """ + """ + def __new__(cls, M, nderiv, dtype='real'): + expressions = [] + args = [] + if not M.is_analytical: + + dim = M.ldim + ops = [dx1, dx2, dx3][:dim] + r = range(nderiv+1) + ranges = [r]*dim + indices = product(*ranges) + + indices = list(indices) + indices = [ijk for ijk in indices if sum(ijk) <= nderiv] + + for d in range(dim): + for ijk in indices: + atom = M[d] + for n,op in zip(ijk, ops): + for _ in range(1, n+1): + atom = op(atom) + args.append(atom) + + expressions = [GeometryExpr(i, dtype) for i in args] + + args = Tuple(*args) + expressions = Tuple(*expressions) + return Basic.__new__(cls, args, expressions) + + @property + def arguments(self): + return self._args[0] + + @property + def expressions(self): + return self._args[1] + +#============================================================================== +class Block(Basic): + """ + This class represents a Block of statements + + """ + def __new__(cls, body): + + if not is_iterable(body): + body = [body] + body = Tuple(*body) + + return Basic.__new__(cls, body) + + @property + def body(self): + return self._args[0] + +#============================================================================== +class ParallelBlock(Block): + def __new__(cls, default='private', private=(), shared=(), firstprivate=(), lastprivate=(), body=()): + return Basic.__new__(cls, default, private, shared, firstprivate, lastprivate, body) + + @property + def default(self): + return self._args[0] + + @property + def private(self): + return self._args[1] + + @property + def shared(self): + return self._args[2] + + @property + def firstprivate(self): + return self._args[3] + + @property + def lastprivate(self): + return self._args[4] + + @property + def body(self): + return self._args[5] + +#============================================================================== +def construct_itergener(a, index): + """ + Create the generator and the iterator based on a and the index + """ + # ... create generator + if isinstance(a, PlusGlobalTensorQuadratureGrid): + generator = TensorGenerator(a, index) + element = PlusLocalTensorQuadratureGrid() + + elif isinstance(a, PlusLocalTensorQuadratureGrid): + generator = TensorGenerator(a, index) + element = PlusTensorQuadrature() + + elif isinstance(a, GlobalTensorQuadratureGrid): + generator = TensorGenerator(a, index) + element = LocalTensorQuadratureGrid() + + elif isinstance(a, LocalTensorQuadratureGrid): + generator = TensorGenerator(a, index) + element = TensorQuadrature(a.weights) + + elif isinstance(a, (LocalTensorQuadratureTrialBasis, GlobalTensorQuadratureTrialBasis)): + generator = TensorGenerator(a, index) + element = TensorTrialBasis(a.target) + + elif isinstance(a, (LocalTensorQuadratureTestBasis, GlobalTensorQuadratureTestBasis)): + generator = TensorGenerator(a, index) + element = TensorTestBasis(a.target) + + elif isinstance(a, (GlobalTensorQuadratureBasis, TensorQuadratureBasis)): + generator = TensorGenerator(a, index) + element = TensorBasis(a.target) + + elif isinstance(a, (LocalSpanArray, GlobalSpanArray)): + generator = TensorGenerator(a, index) + element = Span(a.target) + + elif isinstance(a, MatrixLocalBasis): + generator = ProductGenerator(a, index) + element = CoefficientBasis(a.target) + + elif isinstance(a, MatrixGlobalBasis): + generator = ProductGenerator(a, index) + element = MatrixLocalBasis(a.target) + + elif isinstance(a, GeometryExpr): + generator = ProductGenerator(a.expr, index) + element = a.atom + + elif isinstance(a, TensorAssignExpr): + generator = TensorGenerator(a.rhs, index) + element = a.lhs + + else: + raise TypeError('{} not available'.format(type(a))) + # ... + + # ... create iterator + tensor_classes = (LocalTensorQuadratureGrid, + TensorQuadrature, + LocalTensorQuadratureBasis, + TensorQuadratureBasis, + TensorBasis, + Span) + + product_classes = (CoefficientBasis, + GeometryAtom, + MatrixLocalBasis) + + if isinstance(element, tensor_classes): + iterator = TensorIterator(element) + + elif isinstance(element, product_classes): + iterator = ProductIterator(element) + + elif isinstance(element, (Expr, Tuple)): + iterator = TensorIterator(element) + + else: + raise TypeError('{} not available'.format(type(element))) + # ... + + return iterator, generator + +#============================================================================================= +# the Expression class works with fixed dimension expressions instead of vectorized one, +# where in some cases we need to treat each dimesion diffrently + +class Expression(Expr): + """ + The Expression class gives us the possibility to create specific instructions for some dimension, + where the generated code is not in a vectorized form. + For example, the class Loop generates 2 for loops in 2D and 3 in 3D, + the expressions that are generated are the same for 2D and 3D, + because they are written in a way that allows them to be applied in any dimension, + with the fixed dimension expression we can specify the generated code for a specific dimension, + so the generated code in the second dimension of the 2D loop is diffrent from the one in the first dimension of the 2D loop + """ + def __new__(cls, *args): + return Expr.__new__(cls, *args) + +class AddNode(Expression): + pass + +class MulNode(Expression): + pass + +class IntDivNode(Expression): + pass + +class AndNode(Expression): + pass + +class NotNode(Expression): + pass + +class EqNode(Expression): + pass + +class StrictLessThanNode(Expression): + pass diff --git a/psydac/api/ast/parser.py b/psydac/api/ast/parser.py new file mode 100644 index 000000000..f85d82600 --- /dev/null +++ b/psydac/api/ast/parser.py @@ -0,0 +1,2152 @@ +# coding: utf-8 + +import numpy as np + +from sympy import S +from sympy import IndexedBase, Indexed +from sympy import Mul, Matrix +from sympy import Add, And, StrictLessThan, Eq +from sympy import Not +from sympy import Symbol +from sympy import Basic +from sympy import MutableDenseNDimArray as MArray +from sympy.simplify import cse_main +from sympy.core.containers import Tuple + +from sympde.topology import (dx1, dx2, dx3) +from sympde.topology import SymbolicExpr +from sympde.topology import LogicalExpr +from sympde.expr.evaluation import _split_test_function +from sympde.topology import SymbolicWeightedVolume +from sympde.topology import Boundary, NormalVector, Interface +from sympde.topology.basic import BasicDomain +from sympde.topology.mapping import Mapping + +from sympde.topology.derivatives import get_index_logical_derivatives + +from psydac.pyccel.ast.core import Assign, AugAssign, For +from psydac.pyccel.ast.core import Variable, IndexedVariable, IndexedElement +from psydac.pyccel.ast.core import Slice +from psydac.pyccel.ast.core import EmptyNode, Import, While, Return, If +from psydac.pyccel.ast.core import CodeBlock, FunctionDef, Comment +from psydac.pyccel.ast.builtins import Range + +from psydac.api.utilities import flatten +from psydac.api.ast.utilities import variables, math_atoms_as_str, get_name +from psydac.api.ast.utilities import build_pythran_types_header +from psydac.api.ast.utilities import build_pyccel_type_annotations + +from .nodes import AtomicNode +from .nodes import BasisAtom +from .nodes import LogicalBasisValue +from .nodes import TensorQuadrature +from .nodes import LocalTensorQuadratureBasis +from .nodes import LocalTensorQuadratureTestBasis +from .nodes import LocalTensorQuadratureTrialBasis +from .nodes import GlobalTensorQuadratureTestBasis +from .nodes import GlobalTensorQuadratureTrialBasis +from .nodes import GlobalTensorQuadratureBasis +from .nodes import SplitArray +from .nodes import Reduction +from .nodes import LogicalValueNode +from .nodes import TensorIteration +from .nodes import TensorIterator +from .nodes import TensorGenerator +from .nodes import ProductIteration +from .nodes import ProductIterator +from .nodes import ProductGenerator +from .nodes import StencilMatrixLocalBasis +from .nodes import StencilMatrixGlobalBasis, ScalarLocalBasis +from .nodes import BlockStencilMatrixLocalBasis +from .nodes import BlockStencilMatrixGlobalBasis +from .nodes import BlockStencilVectorLocalBasis, BlockScalarLocalBasis +from .nodes import BlockStencilVectorGlobalBasis +from .nodes import StencilVectorLocalBasis +from .nodes import StencilVectorGlobalBasis +from .nodes import GlobalElementBasis +from .nodes import LocalElementBasis +from .nodes import TensorQuadratureTestBasis, TensorQuadratureTrialBasis +from .nodes import Span +from .nodes import Loop +from .nodes import WeightedVolumeQuadrature +from .nodes import LengthDofTest + +from .nodes import index_dof_test, index_dof_trial +from .nodes import index_deriv, Max, Min + +from .nodes import Zeros, ZerosLike, Array +from .fem import expand + +#============================================================================== +# TODO move it +import string +import random + +def random_string( n ): + chars = string.ascii_lowercase + string.digits + selector = random.SystemRandom() + return ''.join( selector.choice( chars ) for _ in range( n ) ) + +class Shape(Basic): + @property + def arg(self): + return self._args[0] + + +def is_scalar_array(var): + indices = var.indices + for ind in indices: + if isinstance(ind, Slice): + return False + return True + + +#============================================================================== +def parse(expr, settings, backend=None): + """ + A function which takes a Psydac AST and transforms it to a Pyccel AST. + + This function takes a Psydac abstract syntax tree (AST) and returns a + Pyccel AST. In turn, this can be translated to Python code through a call + to the function `pycode` from `psydac.pyccel.codegen.printing.pycode`. + + Parameters + ---------- + expr : Any + Psydac AST, of any type supported by the Parser class. + + settings : dict + Dictionary that contains number of dimension, mappings and target if provided + + Returns + ------- + ast : psydac.pyccel.ast.basic.PyccelAstNode | psydac.pyccel.ast.core.FunctionDef + Pyccel abstract syntax tree that can be translated into a Python code. + """ + psy_parser = Parser(settings, backend) + ast = psy_parser.doit(expr) + return ast + +#============================================================================== +class Parser(object): + """ + A Parser which takes a Psydac AST and transforms it to a Pyccel AST. + + This class takes a Psydac AST and transforms it to the AST of an old and + reduced version of Pyccel, which is shipped as `psydac.pyccel`. This + "mini-Pyccel" is then used for printing the Python code. + + The parsing is performed by passing any object of a "supported type" to the + method `Parser.doit`. This in turn calls `Parser._visit` which starts a + recursive tree traversal through specialized `Parser._visit_` + methods. If successful, `Parser.doit` generally returns a `PyccelAstNode` + object (from `psydac.pyccel.ast.basic`). If the input object is a `DefNode` + (representing a function definition) it returns a `FunctionDef` object + (from `psydac.pyccel.ast.core`). The resulting Pyccel AST can be printed to + Python code using the function `pycode` from + `psydac.pyccel.codegen.printing.pycode`. + + By "supported types" we mean any `` type for which a method + `Parser._visit_` is provided. The matching is done by name, and + it also checks any superclasses listed in `.__mro__` in the given + order. + + Parameters + ---------- + settings : dict[str, Any] + A dictionary with required integer arguments `dim` (number of dimensions) + and `nderiv` (maximum number of derivatives), required argument `target` + (symbolic domain of expression, of type `BasicDomain` from + `sympde.topology.basic`), and optional argument `mapping` (domain + `Mapping` from `sympde.topology.mapping`). + + backend : dict[str, Any] + The backend dictionary as defined in `psydac.api.settings`. + """ + def __init__(self, settings, backend=None): + + # Copy settings, hence input dictionary is not modified + settings = settings.copy() + + # ... Pop values from settings and perform sanity checks + dim = settings.pop('dim', None) + if dim is None: + raise ValueError('dim not provided') + else: + assert isinstance(dim, int) + + assert dim > 0 + nderiv = settings.pop('nderiv', None) + if nderiv is None: + raise ValueError('nderiv not provided') + else: + assert isinstance(nderiv, int) + assert nderiv >= 0 + + target = settings.pop('target', None) + if target is None: + raise ValueError('target not provided') + else: + assert isinstance(target, BasicDomain) + + mapping = settings.pop('mapping', None) + if mapping is not None: + assert isinstance(mapping, Mapping) + # ... + + # Store extracted values and other settings + self._dim = dim + self._nderiv = nderiv + self._target = target + self._mapping = mapping + self._settings = settings + + # Store backend dictionary + if backend is not None: + assert isinstance(backend, dict) + assert 'name' in backend.keys() + self.backend = backend + + # TODO improve + self.indices = {} + self.shapes = {} + self.functions = {} + self.variables = {} + self.arguments = {} + self.allocated = {} + self._math_functions = () + + @property + def settings(self): + return self._settings + + @property + def dim(self): + return self._dim + + @property + def nderiv(self): + return self._nderiv + + @property + def mapping(self): + return self._mapping + + @property + def target(self): + return self._target + + def doit(self, expr, **settings): + return self._visit(expr, **settings) + + def insert_variables(self, *args): + args = flatten(args) + for arg in args: + self.variables[str(arg)] = arg + + def get_shape(self, expr): + lhs = expr.lhs + rhs = expr.rhs + + rhs_indices = [] + if isinstance(rhs, (Indexed, IndexedElement)): + rhs_indices = rhs.indices + lhs_indices = lhs.indices + + #TODO fix probleme of indices we should have a unique way of getting indices + lhs_indices = [None if isinstance(i, Slice) and i.start is None else i for i in lhs_indices] + rhs_indices = [None if isinstance(i, Slice) and i.start is None else i for i in rhs_indices] + shape_lhs = None + shape = [] + + if all(i is None for i in lhs_indices): + for i in rhs_indices: + if i is None: + shape.append(None) + elif str(i) in self.indices: + shape.append(self.indices[str(i)]-1) + elif isinstance(i, Slice) and i.start and i.end: + shape.append(i.end-i.start) + if len(shape) == len(rhs_indices): + if any(s is None for s in shape): + shape = tuple(Slice(None,None) if i is None else 0 for i in shape) + rhs = rhs.base + shape_lhs = Shape(rhs[shape]) + else: + shape_lhs = tuple(shape) + + elif all(i is not None for i in lhs_indices): + for i in lhs_indices: + if str(i) in self.indices: + shape.append(self.indices[str(i)]) + if len(shape) == len(lhs_indices): + shape_lhs = tuple(shape) + + return shape_lhs + + def _visit(self, expr, **settings): + classes = type(expr).__mro__ + for cls in classes: + annotation_method = '_visit_' + cls.__name__ + if hasattr(self, annotation_method): + return getattr(self, annotation_method)(expr, **settings) + # Unknown object, we raise an error. + raise NotImplementedError('{}'.format(type(expr))) + + # .................................................... + def _visit_VectorAssign(self, expr, **kwargs): + lhs = self._visit(expr.lhs) + rhs = self._visit(expr.rhs) + if expr.op is None: + return [Assign(l,r) for l,r in zip(lhs, rhs) if l is not None and r is not None and l is not S.Zero] + else: + return [AugAssign(l,expr.op, r) for l,r in zip(lhs, rhs) if l is not None and r is not None and l is not S.Zero] + # .................................................... + def _visit_Assign(self, expr, **kwargs): + + lhs = self._visit(expr.lhs) + rhs = self._visit(expr.rhs) + + # ... extract slices from rhs + slices = [] + if isinstance(rhs, IndexedElement): + slices = [i for i in rhs.indices if isinstance(i, Slice)] + # ... + + # ... update lhs with slices + if len(slices) > 0: + # TODO add assert on type lhs + if isinstance(lhs, (IndexedBase, IndexedVariable)): + lhs = lhs[slices] + + elif isinstance(lhs, Symbol): + lhs = IndexedBase(lhs.name)[slices] + + expr = Assign(lhs, rhs) + # .. + + if isinstance(lhs, (IndexedElement, Indexed)): + name = str(lhs.base) + + shape = self.get_shape(expr) + if shape: + self.shapes[name] = shape + + return expr + + # .................................................... + def _visit_AugAssign(self, expr, **kwargs): + + lhs = self._visit(expr.lhs) + rhs = self._visit(expr.rhs) + op = expr.op + + # ... extract slices from rhs + slices = [] + if isinstance(rhs, IndexedElement): + slices = [i for i in indices if isinstance(i, Slice)] + # ... + + # ... update lhs with slices + if len(slices) > 0: + # TODO add assert on type lhs + if isinstance(lhs, (IndexedBase, IndexedVariable)): + lhs = lhs[slices] + else: + raise NotImplementedError('{}'.format(type(lhs))) + + expr = AugAssign(lhs,op,rhs) + # ... + if isinstance(lhs, (IndexedElement,Indexed)): + name = str(lhs.base) + + shape = self.get_shape(expr) + if shape: + self.shapes[name] = shape + + return expr + + def _visit_Allocate(self, expr, **kwargs): + arr = self._visit(expr.array) + shape = [self._visit(i) for i in expr.shape] + self.allocated[arr.name] = arr + return Assign(arr, Zeros(tuple(shape), arr.dtype)) + + # .................................................... + def _visit_AddNode(self, expr, **kwargs): + return self._visit_Add(expr) + + def _visit_MulNode(self, expr, **kwargs): + return self._visit_Mul(expr) + + # .................................................... + def _visit_IntDivNode(self, expr, **kwargs): + args = [self._visit(a) for a in expr.args] + return args[0]//args[1] + + # .................................................... + def _visit_AndNode(self, expr, **kwargs): + args = [self._visit(a) for a in expr.args] + return And(*args) + + def _visit_NotNode(self, expr, **kwargs): + return Not(self._visit(expr.args[0])) + + def _visit_EqNode(self, expr, **kwargs): + return Eq(self._visit(expr.args[0]), self._visit(expr.args[1])) + + # .................................................... + def _visit_StrictLessThanNode(self, expr, **kwargs): + a = self._visit(expr.args[0]) + b = self._visit(expr.args[1]) + return StrictLessThan(a,b) + + # .................................................... + def _visit_Add(self, expr, **kwargs): + args = [self._visit(i) for i in expr.args] + tuples = [e for e in args if isinstance(e, tuple)] + args = [e for e in args if not e in tuples] + expr = Add(*args) + if tuples: + args = list(tuples[0]) + for e in tuples[1:]: + args = [args[i]+e[i] for i in range(len(args))] + tuples = tuple(Add(expr,e) for e in args) + return tuples + return expr + + # .................................................... + def _visit_Mul(self, expr, **kwargs): + args = [self._visit(i) for i in expr.args] + return Mul(*args) + + # .................................................... + def _visit_Symbol(self, expr, **kwargs): + return expr + + # .................................................... + def _visit_Variable(self, expr, **kwargs): + return expr + + # .................................................... + def _visit_IndexedVariable(self, expr, **kwargs): + return expr + + # .................................................... + def _visit_Tuple(self, expr, **kwargs): + args = [self._visit(i) for i in expr] + return Tuple(*args) + + def _visit_Array(self, expr, **kwargs): + data = self._visit(expr.data) + dtype = expr.dtype + return Array(data, dtype=dtype) + + # .................................................... + def _visit_Block(self, expr, **kwargs): + body = [self._visit(i) for i in expr.body] + body = flatten(body) + if len(body) == 1: + return body[0] + + else: + return CodeBlock(body) + + # .................................................... + def _visit_ParallelBlock(self, expr, **kwargs): + body = [self._visit(i) for i in expr.body] + body = list(flatten(body)) + default = expr.default + shared = [self._visit(i) for i in expr.shared] + private = [self._visit(i) for i in expr.private] + firstprivate = [self._visit(i) for i in expr.firstprivate] + lastprivate = [self._visit(i) for i in expr.lastprivate] + shared = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in shared]) + private = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in private]) + firstprivate = flatten([list(i.values())[0] if isinstance(i, dict)else i for i in firstprivate]) + lastprivate = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in lastprivate]) + txt = '#$ omp parallel default({}) &\n'.format(default) + txt += '#$ shared({}) &\n'.format(','.join(str(i) for i in shared if i)) if shared else '' + txt += '#$ private({}) &\n'.format(','.join(str(i) for i in private if i)) if private else '' + txt += '#$ firstprivate({}) &\n'.format(','.join(str(i) for i in firstprivate if i)) if firstprivate else '' + txt += '#$ lastprivate({})'.format(','.join(str(i) for i in lastprivate if i)) if lastprivate else '' + cmt = [Comment(txt.rstrip().rstrip('&'))] + endcmt = [Comment('#$ omp end parallel')] + return CodeBlock(cmt + body + endcmt) + + # .................................................... + def _visit_DefNode(self, expr, **kwargs): + """ Convert the DefNode object to a FunctionDef and sort its arguments in the following order: + - 1D tests basis functions + - 1D trial basis functions (if present) + - 1D mapping basis functions (if present) + - Span of tests basis functions + - Span of mapping basis functions (if present) + - 1D quadrature points and weights + - Degrees of tests basis functions + - Degrees of trial basis functions (if present) + - Degrees of mapping basis functions (if present) + - Quadrature degrees in each dimension + - Length of ghost regions for global matrices/vectors (if present) + - Coefficient of mapping (if present) + - Global matrices/vectors + - 1D basis function of field space (if present) + - Span of field basis functions (if present) + - Degrees of field basis functions (if present) + - Length of ghost regions for field vectors (if present) + - Coefficient of field (if present) + - Constants (if present) + """ + + args = expr.arguments.copy() + f_args = () + + tests_basis = args.pop('tests_basis') + trial_basis = args.pop('trial_basis',[]) + + g_span = args.pop('spans') + g_quad = args.pop('quads') + + lengths_tests = args.pop('tests_degrees') + lengths_trials = args.pop('trials_degrees', {}) + + lengths = args.pop('quads_degree') + g_pads = args.pop('global_pads') + l_pads = args.pop('local_pads', None) + + mats = args.pop('mats') + + map_coeffs = args.pop('mapping', None) + map_degrees = args.pop('mapping_degrees', None) + map_basis = args.pop('mapping_basis', None) + map_span = args.pop('mapping_spans', None) + thread_args = args.pop('thread_args', None) + + if not map_coeffs: + map_coeffs = [] + map_degrees = [] + map_basis = [] + map_span = [] + + constants = args.pop('constants', None) + f_coeffs = args.pop('f_coeffs' , None) + + starts = args.pop('starts', []) + ends = args.pop('ends' , []) + if f_coeffs: + f_span = args.pop('f_span', []) + f_basis = args.pop('field_basis', []) + f_degrees = args.pop('fields_degrees', []) + f_pads = args.pop('f_pads', []) + f_args = (*f_basis, *f_span, *f_degrees, *f_pads, *f_coeffs) + + args = [*tests_basis, *trial_basis, *map_basis,\ + *g_span, *map_span, *g_quad,\ + *lengths_tests.values(), *lengths_trials.values(),\ + *map_degrees, *lengths, *g_pads, *map_coeffs] + + if mats: + exprs = [mat.expr for mat in mats] + mats = [self._visit(mat) for mat in mats] + mats = [[a for a,e in zip(mat[:],expr[:]) if e] for mat,expr in zip(mats, exprs)] + mats = flatten(mats) + + args = [self._visit(i, **kwargs) for i in args] + args = [tuple(arg.values())[0] if isinstance(arg, dict) else arg for arg in args] + arguments = flatten(args) + mats + + if f_args: + f_args = [self._visit(i, **kwargs) for i in f_args] + f_args = [tuple(arg.values())[0] if isinstance(arg, dict) else arg for arg in f_args] + arguments += flatten(f_args) + + if constants: + arguments += [self._visit(i, **kwargs) for i in constants] + + arguments += starts + ends + + if thread_args: + arguments += flatten([self._visit(i, **kwargs) for i in thread_args]) + + body = flatten(tuple(self._visit(i, **kwargs) for i in expr.body)) + + inits = [] + for k,i in self.shapes.items(): + if not k in self.variables: continue + var = self.variables[k] + if var in arguments or var.name in self.allocated: + continue + if isinstance(i, Shape): + inits.append(Assign(var, ZerosLike(i.arg))) + else: + inits.append(Assign(var, Zeros(i, dtype=var.dtype))) + + inits.append(EmptyNode()) + body = tuple(inits) + body + name = expr.name + + math_library = 'cmath' if expr.domain_dtype=='complex' else 'math' # Function names are the same + math_imports = (*self._math_functions,) + numpy_imports = ('array', 'zeros', 'zeros_like', 'floor') + imports = [Import('numpy', numpy_imports)] + \ + ([Import(math_library, math_imports)] if math_imports else []) + \ + [*expr.imports] + + results = [self._visit(a) for a in expr.results] + + if self.backend['name'] == 'pyccel': + arguments = build_pyccel_type_annotations(arguments) + decorators = {} + elif self.backend['name'] == 'pythran': + header = build_pythran_types_header(name, arguments) # Could this work?? + else: + decorators = {} + + func = FunctionDef(name, arguments, results, body, imports=imports, decorators=decorators) + stmts = func + + self.functions[name] = func + return stmts + + def _visit_EvalField(self, expr, **kwargs): + body = self._visit(expr.body, **kwargs) + return body + + def _visit_EvalMapping(self, expr, **kwargs): + if self._mapping.is_analytical: + return EmptyNode() + stmts = self._visit(expr.stmts) + return stmts + # .................................................... + def _visit_Grid(self, expr, **kwargs): + raise NotImplementedError('TODO') + + # .................................................... + def _visit_Element(self, expr, **kwargs): + raise NotImplementedError('TODO') + + # .................................................... + def _visit_GlobalTensorQuadratureGrid(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + + names = 'global_x1:%s'%(dim+1) + points = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + if expr.weights: + names = 'global_w1:%s'%(dim+1) + weights = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + # gather by axis + targets = tuple(zip(points, weights)) + else: + weights = [] + targets = tuple(zip(points)) + + self.insert_variables(*points, *weights) + + return {0: targets} + + # .................................................... + def _visit_LocalTensorQuadratureGrid(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + names = 'local_x1:%s'%(dim+1) + points = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + if expr.weights: + names = 'local_w1:%s'%(dim+1) + weights = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + # gather by axis + targets = tuple(zip(points, weights)) + else: + weights = [] + targets = tuple(zip(points)) + + self.insert_variables(*points, *weights) + + return {0: targets} + + # .................................................... + def _visit_PlusGlobalTensorQuadratureGrid(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + names = 'global_x1:%s_plus'%(dim+1) + points = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + # gather by axis + self.insert_variables(*points) + + points = tuple(zip(points)) + return dict([(0, points)]) + + # .................................................... + def _visit_PlusLocalTensorQuadratureGrid(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + names = 'local_x1:%s_plus'%(dim+1) + points = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + self.insert_variables(*points) + + points = tuple(zip(points)) + return dict([(0, points)]) + + # .................................................... + def _visit_TensorQuadrature(self, expr, **kwargs): + dim = self.dim + names = 'x1:%s' % (dim+1) + points = variables(names, dtype='real', cls=Variable) + + if expr.weights: + names = 'w1:%s' % (dim+1) + weights = variables(names, dtype='real', cls=Variable) + + # gather by axis + targets = tuple(zip(points, weights)) + else: + weights = [] + targets = tuple(zip(points)) + + self.insert_variables(*points, *weights) + + return {0: targets} + + # .................................................... + def _visit_PlusTensorQuadrature(self, expr, **kwargs): + dim = self.dim + names = 'x1:%s_plus' % (dim+1) + points = variables(names, dtype='real', cls=Variable) + + targets = tuple(zip(points)) + + self.insert_variables(*points) + + return dict([(0, targets)]) + + # .................................................... + def _visit_GlobalThreadSpanArray(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + target = SymbolicExpr(expr.target) + name = 'thread_spans_{}_'.format(target) + targets = variables('{}1:{}'.format(name, dim+1), dtype='int', rank=1, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_GlobalThreadStarts(self, expr, **kwargs): + dim = self.dim + targets = variables('global_thread_starts_1:{}'.format(dim+1), dtype='int', rank=1, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_GlobalThreadEnds(self, expr, **kwargs): + dim = self.dim + targets = variables('global_thread_ends_1:{}'.format(dim+1), dtype='int', rank=1, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_GlobalThreadSizes(self, expr, **kwargs): + dim = self.dim + targets = variables('global_thread_size_1:{}'.format(dim+1), dtype='int') + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_LocalThreadStarts(self, expr, **kwargs): + dim = self.dim + targets = variables('local_thread_starts_1:{}'.format(dim+1), dtype='int', rank=1, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_LocalThreadEnds(self, expr, **kwargs): + dim = self.dim + targets = variables('local_thread_ends_1:{}'.format(dim+1), dtype='int', rank=1, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + return targets + + # .................................................... + def _visit_MatrixQuadrature(self, expr, **kwargs): + rank = self._visit(expr.rank) + dtype = expr.dtype + target = SymbolicExpr(expr.target) + name = 'arr_{}'.format(target.name) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + # .................................................... + def _visit_GlobalTensorQuadratureBasis(self, expr, **kwargs): + # TODO add label + dim = self.dim + rank = expr.rank + unique_scalar_space = expr.unique_scalar_space + is_scalar = expr.is_scalar + target = expr.target + label = str(SymbolicExpr(target)) + if isinstance(expr, GlobalTensorQuadratureTestBasis): + if not unique_scalar_space: + names = 'global_test_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'global_test_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + elif isinstance(expr, GlobalTensorQuadratureTrialBasis): + if not unique_scalar_space: + names = 'global_trial_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'global_trial_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + else: + if not unique_scalar_space: + names = 'global_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'global_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + targets = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + + self.insert_variables(*targets) + + arrays = {} + if unique_scalar_space and not is_scalar: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets)) + elif not unique_scalar_space: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets[i::dim])) + else: + arrays[target] = tuple(zip(targets)) + return arrays + # .................................................... + def _visit_LocalTensorQuadratureBasis(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + unique_scalar_space = expr.unique_scalar_space + is_scalar = expr.is_scalar + target = expr.target + label = str(SymbolicExpr(target)) + if isinstance(expr, LocalTensorQuadratureTestBasis): + if not unique_scalar_space: + names = 'local_test_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'local_test_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + elif isinstance(expr, LocalTensorQuadratureTrialBasis): + if not unique_scalar_space: + names = 'local_trial_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'local_trial_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + else: + if not unique_scalar_space: + names = 'local_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'local_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + targets = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + + self.insert_variables(*targets) + + arrays = {} + if unique_scalar_space and not is_scalar: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets)) + elif not unique_scalar_space: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets[i::dim])) + else: + arrays[target] = tuple(zip(targets)) + return arrays + + # .................................................... + def _visit_TensorQuadratureBasis(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + unique_scalar_space = expr.unique_scalar_space + is_scalar = expr.is_scalar + target = expr.target + label = str(SymbolicExpr(target)) + + if isinstance(expr, TensorQuadratureTestBasis): + if not unique_scalar_space: + names = 'test_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'test_basis_{label}_1:{i}'.format(label=label,i=dim+1) + + elif isinstance(expr, TensorQuadratureTrialBasis): + if not unique_scalar_space: + names = 'trial_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'trial_basis_{label}_1:{i}'.format(label=label,i=dim+1) + else: + if not unique_scalar_space: + names = 'array_basis_{label}(1:{j})_1:{i}'.format(label=label,i=dim+1,j=dim+1) + else: + names = 'array_basis_{label}_1:{i}'.format(label=label,i=dim+1) + # ... + + targets = variables(names, dtype='real', rank=rank, cls=IndexedVariable) + + self.insert_variables(*targets) + arrays = {} + if unique_scalar_space and not is_scalar: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets)) + elif not unique_scalar_space: + for i in range(dim): + arrays[target[i]] = tuple(zip(targets[i::dim])) + else: + arrays[target] = tuple(zip(targets)) + return arrays + + # .................................................... + def _visit_GlobalSpanArray(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + target = expr.target + label = SymbolicExpr(target).name + + names = 'global_span_{}_1:{}'.format(label, str(dim+1)) + targets = variables(names, dtype='int', rank=rank, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + + self.insert_variables(*targets) + if not isinstance(targets[0], (tuple, list, Tuple)): + targets = [targets] + target = {target: tuple(zip(*targets))} + return target + + # .................................................... + def _visit_LocalSpanArray(self, expr, **kwargs): + dim = self.dim + rank = expr.rank + target = expr.target + label = SymbolicExpr(target).name + + names = 'local_span_{}_1:{}'.format(label, str(dim+1)) + targets = variables(names, dtype='int', rank=rank, cls=IndexedVariable) + if expr.index is not None: + return targets[expr.index] + + self.insert_variables(*targets) + if not isinstance(targets[0], (tuple, list, Tuple)): + targets = [targets] + target = {target: tuple(zip(*targets))} + return target + + # .................................................... + def _visit_Span(self, expr, **kwargs): + dim = self.dim + target = expr.target + label = SymbolicExpr(target).name + names = 'span_{}_1:{}'.format(label,str(dim+1)) + targets = variables(names, dtype='int') + + if expr.index is not None: + return targets[expr.index] + + self.insert_variables(*targets) + if not isinstance(targets[0], (tuple, list, Tuple)): + targets = [targets] + + target = {target: tuple(zip(*targets))} + return target + + # .................................................... + def _visit_Pads(self, expr, **kwargs): + dim = self.dim + tests = expand(expr.tests) + tests_degree = expr.tests_degree + trials_degree = expr.trials_degree + m_tests = expr.tests_multiplicity + m_trials = expr.trials_multiplicity + + if expr.trials is not None: + trials = expand(expr.trials) + pads = MArray.zeros(len(tests), len(trials), dim) + for i in range(pads.shape[0]): + for j in range(pads.shape[1]): + label1 = SymbolicExpr(tests[i]).name + label2 = SymbolicExpr(trials[j]).name + names = f'pad_{label2}_{label1}_1:{dim+1}' + targets = variables(names, dtype='int') + pads[i, j, :] = targets + self.insert_variables(*targets) + if expr.test_index is not None and expr.trial_index is not None: + if expr.dim_index is not None: + return pads[expr.test_index, expr.trial_index, expr.dim_index] + return pads[expr.test_index, expr.trial_index] + + else: + pads = MArray.zeros(len(tests), 1, dim) + for i in range(pads.shape[0]): + label1 = SymbolicExpr(tests[i]).name + names = f'pad_{label1}_1:{dim+1}' + targets = variables(names, dtype='int') + pads[i, 0, :] = targets + self.insert_variables(*targets) + + if expr.test_index is not None: + if expr.dim_index is not None: + return pads[expr.test_index, 0, expr.dim_index] + return pads[expr.test_index, 0] + #... + + return pads + + # .................................................... + def _visit_TensorBasis(self, expr, **kwargs): + # TODO label + dim = self.dim + nderiv = self.nderiv + target = expr.target + + ops = [dx1, dx2, dx3][:dim] + atoms = _split_test_function(target) + args = {} + for atom in atoms: + sub_args = [None]*dim + for i in range(dim): + d = ops[i] + a = atoms[atom][i] + ls = [a] + for _ in range(1, nderiv+1): + a = d(a) + ls.append(a) + sub_args[i] = tuple(ls) + args[atom] = tuple(sub_args) + return args + + # .................................................... + def _visit_CoefficientBasis(self, expr, **kwargs): + target = SymbolicExpr(expr.target) + name = 'coeff_{}'.format(target.name) + var = IndexedVariable(name, dtype='real', rank=self.dim) + self.insert_variables(var) + return var + + def _visit_MatrixCoordsFromRank(self, expr, **kwargs): + var = IndexedVariable('coords_from_rank', dtype='int', rank=2) + return var + + def _visit_MatrixRankFromCoords(self, expr, **kwargs): + var = IndexedVariable('rank_from_coords', dtype='int', rank=self.dim) + return var + + # .................................................... + def _visit_MatrixLocalBasis(self, expr, **kwargs): + rank = self._visit(expr.rank) + target = SymbolicExpr(expr.target) + dtype = expr.dtype + name = 'arr_coeffs_{}'.format(target.name) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + # .................................................... + def _visit_MatrixGlobalBasis(self, expr, **kwargs): + rank = self._visit(expr.rank) + target = SymbolicExpr(expr.target) + dtype = expr.dtype + name = 'global_arr_coeffs_{}'.format(target.name) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + # .................................................... + def _visit_Reset(self, expr, **kwargs): + var = expr.var + lhs = self._visit(var, **kwargs) + if hasattr(var, 'dtype'): + dtype = var.dtype + else: + dtype = 'real' + # Define data type of the 0 + zero = 0.0j if dtype == 'complex' else 0.0 + if isinstance(var, (LocalElementBasis, GlobalElementBasis)): + return Assign(lhs, zero) + + elif isinstance(var, BlockScalarLocalBasis): + expr = var.expr + return tuple(Assign(a, zero) for a,b in zip(lhs[:], expr[:]) if b) + + expr = var.expr + if not any(lhs[:]): + return () + rank = [l.rank for l in lhs[:] if l][0] + args = [Slice(None, None)]*rank + return tuple(Assign(a[args], zero) for a,b in zip(lhs[:], expr[:]) if b) + + # .................................................... + def _visit_Reduce(self, expr, **kwargs): + op = expr.op + lhs = expr.lhs + rhs = expr.rhs + loop = expr.loop + parallel = loop.parallel + default = loop.default + shared = loop.shared + private = loop.private + firstprivate = loop.firstprivate + lastprivate = loop.lastprivate + reduction = None + if parallel: + reduction = 'reduction({}:{})'.format(expr.op, self._visit(lhs).name) + + stmts = list(loop.stmts) + [Reduction(op, rhs, lhs)] + loop = Loop(loop.iterable, loop.index, stmts=stmts, mask=loop.mask, parallel=parallel, + default=default, shared=shared, private=private, + firstprivate=firstprivate, lastprivate=lastprivate, reduction=reduction) + return self._visit(loop, **kwargs) + + # .................................................... + def _visit_Reduction(self, expr, **kwargs): + op = expr.op + lhs = expr.lhs + expr = expr.expr + + if isinstance(lhs, (GlobalElementBasis, LocalElementBasis)): + lhs = self._visit(lhs, **kwargs) + rhs = self._visit(expr, **kwargs) + return (AugAssign(lhs, op, rhs),) + + elif isinstance(lhs, BlockStencilMatrixLocalBasis): + lhs = self._visit_BlockStencilMatrixLocalBasis(lhs) + expr = self._visit(expr, op=op, lhs=lhs) + return expr + elif isinstance(lhs, BlockStencilMatrixGlobalBasis): + + dim = self.dim + rank = lhs.rank + pads = lhs.pads + multiplicity = lhs.multiplicity + tests = expand(lhs._tests) + + tests_2 = lhs._tests + lhs = self._visit_BlockStencilMatrixGlobalBasis(lhs) + rhs = self._visit(expr) + + pads = self._visit(pads) + rhs_slices = [Slice(None, None)]*rank + for k1 in range(lhs.shape[0]): + test = tests[k1] + test = test if test in tests_2 else test.base + spans = self._visit_Span(Span(test)) + degrees = self._visit_LengthDofTest(LengthDofTest(test)) + spans = flatten(*spans.values()) + m = multiplicity[test] if test in multiplicity else multiplicity[test.base] + + lhs_starts = [spans[i]+m[i]*pads[i]-degrees[i] for i in range(dim)] + lhs_ends = [spans[i]+m[i]*pads[i]+1 for i in range(dim)] + + if isinstance(self._target, Interface): + axis = self._target.axis + lhs_starts[axis] = m[axis]*pads[axis] + lhs_ends[axis] = m[axis]*pads[axis] + degrees[axis] + 1 + + for k2 in range(lhs.shape[1]): + if expr.expr[k1,k2]: + lhs_slices = [Slice(s, e) for s,e in zip(lhs_starts, lhs_ends)] + lhs_slices += [Slice(None, None)]*dim + lhs[k1,k2] = [lhs[k1,k2][lhs_slices]] + rhs[k1,k2] = [rhs[k1,k2][rhs_slices]] + + if op is None: + return tuple( Assign(a, b) for a,b,e in zip(lhs[:], rhs[:], expr.expr[:]) if e) + else: + return tuple( AugAssign(a, op, b) for a,b,e in zip(lhs[:], rhs[:], expr.expr[:]) if e) + + elif isinstance(lhs, BlockStencilVectorLocalBasis): + lhs = self._visit_BlockStencilVectorLocalBasis(lhs) + expr = self._visit(expr, op=op, lhs=lhs) + return expr + elif isinstance(lhs, BlockStencilVectorGlobalBasis): + dim = self.dim + rank = lhs.rank + pads = lhs.pads + multiplicity = lhs.multiplicity + tests = expand(lhs._tests) + tests_2 = lhs._tests + lhs = self._visit_BlockStencilVectorGlobalBasis(lhs) + rhs = self._visit(expr) + pads = self._visit(pads) + rhs_slices = [Slice(None, None)]*rank + + for k in range(lhs.shape[0]): + if expr.expr[k,0]: + test = tests[k] + m = multiplicity[test] if test in multiplicity else multiplicity[test.base] + test = test if test in tests_2 else test.base + spans = self._visit_Span(Span(test)) + spans = flatten(*spans.values()) + degrees = self._visit_LengthDofTest(LengthDofTest(test)) + lhs_starts = [spans[i]+m[i]*pads[i]-degrees[i] for i in range(dim)] + lhs_ends = [spans[i]+m[i]*pads[i]+1 for i in range(dim)] + lhs_slices = [Slice(s, e) for s,e in zip(lhs_starts, lhs_ends)] + lhs[k,0] = lhs[k,0][lhs_slices] + rhs[k,0] = rhs[k,0][rhs_slices] + + return tuple( AugAssign(a, op, b) for a,b,e in zip(lhs[:], rhs[:], expr.expr[:]) if e) + else: + if not( lhs is None ): + lhs = self._visit(lhs) + + return self._visit(expr, op=op, lhs=lhs) + + # .................................................... + def _visit_ComputeLogical(self, expr, op=None, lhs=None, **kwargs): + expr = expr.expr + if lhs is None: + if not isinstance(expr, (Add, Mul)): + lhs = self._visit_AtomicNode(AtomicNode(expr), **kwargs) + else: + lhs = random_string( 6 ) + lhs = Symbol('tmp_{}'.format(lhs)) + + node = LogicalValueNode(expr) + rhs = self._visit_LogicalValueNode(node, **kwargs) + + if op is None: + stmt = Assign(lhs, rhs) + else: + stmt = AugAssign(lhs, op, rhs) + + return self._visit(stmt, **kwargs) + + # .................................................... + def _visit_ComputeLogicalBasis(self, expr, op=None, lhs=None, **kwargs): + lhs = lhs or expr.lhs + expr = expr.expr + if lhs is None: + atom = BasisAtom(expr) + lhs = self._visit_BasisAtom(atom, **kwargs) + + rhs = self._visit_LogicalBasisValue(LogicalBasisValue(expr), **kwargs) + + if op is None: + stmt = Assign(lhs, rhs) + else: + stmt = AugAssign(lhs, op, rhs) + + return self._visit(stmt, **kwargs) + + # .................................................... + def _visit_ComputeKernelExpr(self, expr, op=None, lhs=None, **kwargs): + """ + Compute Symbolic expression given by the user + + """ + if lhs is None: + if not isinstance(expr, (Add, Mul)): + lhs = self._visit_BasisAtom(BasisAtom(expr), **kwargs) + else: + lhs = random_string( 6 ) + lhs = Symbol('tmp_{}'.format(lhs)) + + exprs = expr.expr + mapping = self.mapping + + if expr.weights: + weight = SymbolicWeightedVolume(mapping) + weight = SymbolicExpr(weight) + else: + weight = 1 + + rhs = [weight*self._visit(expr, **kwargs) for expr in exprs[:]] + lhs = lhs[:] + + # Create a new name for the temporaries used in each patch + name = get_name(lhs) + temps, rhs = cse_main.cse(rhs, symbols=cse_main.numbered_symbols(prefix=f'temp{name}')) + + normal_vec_stmts = [] + normal_vectors = expr.expr.atoms(NormalVector) + target = self._target + dim = self._dim + + if normal_vectors: + axis = target.axis + ext = target.ext if isinstance(target, Boundary) else 1 + + vars_plus = [] + if isinstance(target, Interface): + mapping = mapping.minus + target = target.minus + axis = target.axis + ext = target.ext + elif isinstance(target, Boundary): + ext = target.ext + axis = target.axis + + + for vec in normal_vectors: + + J_inv = LogicalExpr(mapping.jacobian_inv_expr, mapping(target)) + J_inv = SymbolicExpr(J_inv) + values = ext * J_inv[axis, :] + normalization = values.dot(values)**0.5 + values = [v for v in values] + values = [v1/normalization for v1 in values] + normal_vec_stmts += [Assign(SymbolicExpr(vec[i]), values[i]) for i in range(dim)] + + if op is None: + stmts = [Assign(i, j) for i,j in zip(lhs,rhs) if j] + else: + stmts = [AugAssign(i, op, j) for i,j in zip(lhs,rhs) if j] + + temps = tuple(Assign(a,b) for a,b in temps) + stmts = tuple(self._visit(stmt, **kwargs) for stmt in stmts) + stmts = tuple(vars_plus) + tuple(normal_vec_stmts) + temps + stmts + + math_functions = math_atoms_as_str(list(exprs)+normal_vec_stmts, 'math') + math_functions = tuple(m for m in math_functions if m not in self._math_functions) + self._math_functions = math_functions + self._math_functions + return stmts + + # .................................................... + def _visit_BasisAtom(self, expr, **kwargs): + """ + Transform derivatives of the ScalarFunction into the correspondant symbol. + """ + symbol = SymbolicExpr(expr.expr) + self.variables[str(symbol.name)] = symbol + return symbol + + # .................................................... + def _visit_AtomicNode(self, expr, **kwargs): + if isinstance(expr.expr, WeightedVolumeQuadrature): + expr = SymbolicWeightedVolume(self.mapping) + return SymbolicExpr(expr) + + else: + return SymbolicExpr(expr.expr) + + # .................................................... + def _visit_LogicalBasisValue(self, expr, **kwargs): + """ + Split the derivatives of the ScalarFunction along the dimensions, transform it into the correspondant symbol. + """ + # ... + dim = self.dim + coords = ['x1', 'x2', 'x3'][:dim] + + expr = expr.expr + atom = BasisAtom(expr).atom + + # Split the ScalarFunction along each dimension + atoms = _split_test_function(atom) + ops = [dx1, dx2, dx3][:dim] + d_atoms = dict(zip(coords, atoms[atom])) + d_ops = dict(zip(coords, ops)) + d_indices = get_index_logical_derivatives(expr) + + # Create the symbol of the derivative for each splitted ScalarFunction + args = [] + for k, u in d_atoms.items(): + d = d_ops[k] + n = d_indices[k] + for _ in range(n): + u = d(u) + u = SymbolicExpr(u) + args.append(u) + + # ... + # Do the multiplication needed + expr = Mul(*args) + + return expr + + # .................................................... + def _visit_LogicalValueNode(self, expr, **kwargs): + """ + This Node seems to return the multiplication of weight. + """ + + #TODO Should we clear this function and replace it by a _visit_WeigthedVolumeQuadrature + expr = expr.expr + target = self.target + + if isinstance(expr, WeightedVolumeQuadrature): + #TODO improve l_quad should not be used like this + l_quad = self._visit_TensorQuadrature(TensorQuadrature(), **kwargs) + _, weights = list(zip(*list(l_quad.values())[0])) + if isinstance(target, Boundary): + weights = list(weights) + weights.pop(target.axis) + wvol = Mul(*weights) + return wvol + else: + raise TypeError('{} not available'.format(type(expr))) + + # .................................................... + def _visit_PhysicalGeometryValue(self, expr, **kwargs): + target = self._target + expr = LogicalExpr(expr.expr, mapping(target)) + + return SymbolicExpr(expr) + + # .................................................... + def _visit_ElementOf(self, expr, **kwargs): + """ + Create an MutableDenseMatrix containing either a variable for a scalar or an IndexedElement to index an element of the matrix/vector + """ + dim = self.dim + target = expr.target + + + # Case where we need to create an element of the matrix indented + if isinstance(target, BlockStencilMatrixLocalBasis): + # improve we shouldn't use index_dof_test + rows = self._visit(index_dof_test) + outer = self._visit(target.outer) if target.outer else rows + cols = self._visit(index_dof_trial) + pads = target.pads + tests = expand(target._tests) + trials = expand(target._trials) + + targets = self._visit_BlockStencilMatrixLocalBasis(target) + for i in range(targets.shape[0]): + for j in range(targets.shape[1]): + if targets[i,j] is S.Zero: + continue + if trials[j] in pads.trials_multiplicity: + trials_m = pads.trials_multiplicity[trials[j]] + trials_d = pads.trials_degree[trials[j]] + else: + trials_m = pads.trials_multiplicity[trials[j].base] + trials_d = pads.trials_degree[trials[j].base] + + if tests[i] in pads.tests_multiplicity: + tests_m = pads.tests_multiplicity[tests[i]] + tests_d = pads.tests_degree[tests[i]] + else: + tests_m = pads.tests_multiplicity[tests[i].base] + tests_d = pads.tests_degree[tests[i].base] + + pp1 = [max(tests_d[k], trials_d[k]) for k in range(dim)] + pp2 = [int((np.ceil((pp1[k]+1)/tests_m[k])-1)*trials_m[k]) for k in range(dim)] + padding = [p2-min(0,p2-p1) for p1,p2 in zip(pp1, pp2)] + indices = tuple(rows) + tuple(cols[k]+padding[k]-outer[k]*trials_m[k] for k in range(dim)) + targets[i,j] = targets[i,j][indices] + return targets + + # Case where we need to create an element of the vector indented + elif isinstance(target, BlockStencilVectorLocalBasis): + targets = self._visit_BlockStencilVectorLocalBasis(target, **kwargs) + + rows = self._visit(index_dof_test) + indices = list(rows) + for i in range(targets.shape[0]): + for j in range(targets.shape[1]): + if targets[i,j] is S.Zero: + continue + targets[i,j] = targets[i,j][indices] + return targets + + # Case where we need to create a scalar for the kernel loop (l_el_{tag}) + elif isinstance(target, LocalElementBasis): + target = self._visit(target, **kwargs) + return (target,) + + # Case where we need to create a scalar for the kernel loop (c_v_u_{tag})/(c_v_{tag}) + elif isinstance(target, BlockScalarLocalBasis): + targets = self._visit(target) + return targets + + else: + raise NotImplementedError('TODO') + + # ............................................................................. + def _visit_BlockStencilMatrixLocalBasis(self, expr, **kwargs): + pads = self._visit_Pads(expr.pads) + tests = expr._tests + trials = expr._trials + tag = expr.tag + dtype = expr.dtype + tests = expand(tests) + trials = expand(trials) + targets = Matrix.zeros(len(tests), len(trials)) + for i, v in enumerate(tests): + for j, u in enumerate(trials): + if expr.expr[i, j] == 0: + continue + mat = StencilMatrixLocalBasis(u=u, v=v, pads=pads[i, j], tag=tag, dtype=dtype) + mat = self._visit_StencilMatrixLocalBasis(mat, **kwargs) + targets[i, j] = mat + return targets + + def _visit_BlockStencilMatrixGlobalBasis(self, expr, **kwargs): + pads = expr.pads + tests = expr._tests + trials = expr._trials + tag = expr.tag + dtype = expr.dtype + tests = expand(tests) + trials = expand(trials) + targets = Matrix.zeros(len(tests), len(trials)) + for i, v in enumerate(tests): + for j, u in enumerate(trials): + if expr.expr[i, j] == 0: + continue + mat = StencilMatrixGlobalBasis(u=u, v=v, pads=pads, tag=tag, dtype=dtype) + mat = self._visit_StencilMatrixGlobalBasis(mat, **kwargs) + targets[i, j] = mat + return targets + + def _visit_BlockStencilVectorLocalBasis(self, expr, **kwargs): + pads = expr.pads + tests = expr._tests + tag = expr.tag + dtype = expr.dtype + tests = expand(tests) + targets = Matrix.zeros(len(tests), 1) + for i, v in enumerate(tests): + if expr.expr[i, 0] == 0: + continue + mat = StencilVectorLocalBasis(v, pads, tag, dtype) + mat = self._visit_StencilVectorLocalBasis(mat, **kwargs) + targets[i, 0] = mat + return targets + + def _visit_BlockStencilVectorGlobalBasis(self, expr, **kwargs): + pads = expr.pads + tests = expr._tests + tag = expr.tag + dtype = expr.dtype + tests = expand(tests) + targets = Matrix.zeros(len(tests), 1) + for i,v in enumerate(tests): + if expr.expr[i, 0] == 0: + continue + mat = StencilVectorGlobalBasis(v, pads, tag, dtype) + mat = self._visit_StencilVectorGlobalBasis(mat, **kwargs) + targets[i, 0] = mat + return targets + + # ............................................................................. + def _visit_BlockScalarLocalBasis(self, expr, **kwargs): + tag = expr.tag + dtype = expr.dtype + tests = expand(expr._tests) + trials = expand(expr._trials) if expr._trials else (None,) + targets = Matrix.zeros(len(tests), len(trials)) + for i,v in enumerate(tests): + for j,u in enumerate(trials): + if expr.expr[i, j] == 0: + continue + var = ScalarLocalBasis(u, v, tag, dtype) + var = self._visit_ScalarLocalBasis(var, **kwargs) + targets[i, j] = var + return targets + + # ............................................................................. + def _visit_StencilMatrixLocalBasis(self, expr, **kwargs): + rank = expr.rank + tag = expr.tag + dtype = expr.dtype + name = '_'.join(str(SymbolicExpr(e)) for e in expr.name) + + name = 'l_mat_{}_{}'.format(name, tag) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + # .................................................... + def _visit_StencilVectorLocalBasis(self, expr, **kwargs): + rank = expr.rank + tag = expr.tag + dtype = expr.dtype + name = str(SymbolicExpr(expr.name)) + name = 'l_vec_{}_{}'.format(name, tag) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + # .................................................... + def _visit_StencilMatrixGlobalBasis(self, expr, **kwargs): + rank = expr.rank + tag = expr.tag + dtype = expr.dtype + name = '_'.join(str(SymbolicExpr(e)) for e in expr.name) + name = 'g_mat_{}_{}'.format(name, tag) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + # .................................................... + def _visit_StencilVectorGlobalBasis(self, expr, **kwargs): + rank = expr.rank + tag = expr.tag + dtype = expr.dtype + name = str(SymbolicExpr(expr.name)) + name = 'g_vec_{}_{}'.format(name, tag) + var = IndexedVariable(name, dtype=dtype, rank=rank) + self.insert_variables(var) + return var + + def _visit_GlobalElementBasis(self, expr, **kwargs): + tag = expr.tag + dtype = expr.dtype + name = 'g_el_{}'.format(tag) + var = variables(name, dtype=dtype) + self.insert_variables(var) + return var + + def _visit_LocalElementBasis(self, expr, **kwargs): + tag = expr.tag + dtype = expr.dtype + name = 'l_el_{}'.format(tag) + var = variables(name, dtype=dtype) + self.insert_variables(var) + return var + + def _visit_ScalarLocalBasis(self, expr, **kwargs): + tag = expr.tag + dtype = expr.dtype + basis = (expr._test,) + if expr._trial: + basis = (expr._test, expr._trial) + name = '_'.join(str(SymbolicExpr(e)) for e in basis) + name = 'contribution_{}_{}'.format(name, tag) + var = variables(name, dtype=dtype) + self.insert_variables(var) + return var + + # .................................................... + def _visit_Pattern(self, expr, **kwargs): + # this is for multi-indices for the moment + dim = self.dim + args = [] + for a in expr: + if a is None: + args.append([Slice(None, None)]*dim) + + elif isinstance(a, int): + args.append([a]*dim) + + else: + v = self._visit(a) + args.append(v) + args = list(zip(*args)) + return args + + def _visit_Slice(self, expr, **kwargs): + args = [self._visit(a) if a is not None else None for a in expr.args] + return Slice(args[0], args[1]) + + def _visit_TensorIntDiv(self, expr, **kwargs): + args = [self._visit(a, **kwargs) for a in expr.args] + arg1 = args[0] + arg2 = args[1] + newargs = [] + for i,j in zip(arg1, arg2): + newargs.append(i//j) + + return tuple(newargs) + + def _visit_TensorAdd(self, expr, **kwargs): + args = [self._visit(a, **kwargs) for a in expr.args] + arg1 = args[0] + arg2 = args[1] + newargs = [] + for i,j in zip(arg1, arg2): + newargs.append(i+j) + + return tuple(newargs) + + def _visit_TensorMul(self, expr, **kwargs): + args = [self._visit(a, **kwargs) for a in expr.args] + arg1 = args[0] + arg2 = args[1] + newargs = [] + for i,j in zip(arg1, arg2): + newargs.append(i*j) + + return tuple(newargs) + + def _visit_TensorMax(self, expr, **kwargs): + args = [self._visit(a, **kwargs) for a in expr.args] + arg1 = args[0] + arg2 = args[1] + newargs = [] + for i,j in zip(arg1, arg2): + newargs.append(Max(i,j)) + + return tuple(newargs) + + def _visit_TensorInteger(self, expr, **kwargs): + return (expr.args[0],)*self.dim + # .................................................... + + def _visit_Max(self, expr, **kwargs): + args = [self._visit(i) for i in expr.args] + return Max(*args) + + def _visit_Min(self, expr, **kwargs): + args = [self._visit(i) for i in expr.args] + return Min(*args) + + def _visit_Expr(self, expr, **kwargs): + return SymbolicExpr(expr) + + def _visit_Return( self, expr, **kwargs): + return Return(self._visit(expr.expr)) + + def _visit_NumThreads(self, expr, **kwargs): + target = variables('num_threads', dtype='int') + self.insert_variables(target) + return target + + def _visit_BooleanTrue(self, expr, **kwargs): + return int(True) + + def _visit_BooleanFalse(self, expr, **kwargs): + return int(False) + # .................................................... + def _visit_ThreadId(self, expr, **kwargs): + return variables('thread_id', dtype='int') + # ................................................... + def _visit_NeighbourThreadCoordinates(self, expr, **kwargs): + dim = self.dim + target = variables('next_thread_coords_1:%d'%(dim+1), dtype='int') + if expr.index is not None: + return target[expr.index] + self.insert_variables(*target) + return target + # .................................................... + def _visit_ThreadCoordinates(self, expr, **kwargs): + dim = self.dim + target = variables('thread_coords_1:%d'%(dim+1), dtype='int') + if expr.index is not None: + return target[expr.index] + return target + # .................................................... + def _visit_IndexElement(self, expr, **kwargs): + dim = self.dim + target = variables('i_element_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_LocalIndexElement(self, expr, **kwargs): + dim = self.dim + target = variables('local_i_element_1:%d'%(dim+1), dtype='int') + if expr.index is not None: + return target[expr.index] + return target + # .................................................... + def _visit_IndexQuadrature(self, expr, **kwargs): + dim = self.dim + target = variables('i_quad_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexDof(self, expr, **kwargs): + dim = self.dim + target = variables('i_basis_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexDofTrial(self, expr, **kwargs): + dim = self.dim + target = variables('j_basis_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexDofTest(self, expr, **kwargs): + dim = self.dim + target = variables('i_basis_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexOuterDofTest(self, expr, **kwargs): + dim = self.dim + target = variables('outer_i_basis_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexInnerDofTest(self, expr, **kwargs): + dim = self.dim + target = variables('inner_i_basis_1:%d'%(dim+1), dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_IndexDerivative(self, expr, **kwargs): + raise NotImplementedError('TODO') + + # .................................................... + def _visit_LengthElement(self, expr, **kwargs): + dim = self.dim + names = 'n_element_1:%d'%(dim+1) + target = variables(names, dtype='int', cls=Variable) + if expr.index is not None: + return target[expr.index] + self.insert_variables(*target) + return target + # .................................................... + def _visit_LengthQuadrature(self, expr, **kwargs): + dim = self.dim + names = 'k1:%d'%(dim+1) + target = variables(names, dtype='int', cls=Variable) + self.insert_variables(*target) + return target + # .................................................... + def _visit_LengthDof(self, expr, **kwargs): + dim = self.dim + names = 'p1:%d'%(dim+1) + target = variables(names, dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_LengthDofTest(self, expr, **kwargs): + dim = self.dim + target = expr.target + if target: + target = '_' + str(SymbolicExpr(target)) + else: + target = '' + + names = 'test{}_p1:{}'.format(target, dim+1) + target = variables(names, dtype='int') + if expr.index is not None: + return target[expr.index] + self.insert_variables(*target) + return target + # .................................................... + def _visit_LengthOuterDofTest(self, expr, **kwargs): + dim = self.dim + target = expr.target + if target: + target = '_' + str(SymbolicExpr(target)) + else: + target = '' + + names = 'test_outer{}_p1:{}'.format(target, dim+1) + target = variables(names, dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_LengthInnerDofTest(self, expr, **kwargs): + dim = self.dim + target = expr.target + if target: + target = '_' + str(SymbolicExpr(target)) + else: + target = '' + + names = 'test_inner{}_p1:{}'.format(target, dim+1) + target = variables(names, dtype='int') + self.insert_variables(*target) + return target + + # .................................................... + def _visit_LengthDofTrial(self, expr, **kwargs): + dim = self.dim + target = expr.target + if target: + target = '_' + str(SymbolicExpr(target)) + else: + target = '' + + names = 'trial{}_p1:{}'.format(target, dim+1) + target = variables(names, dtype='int') + self.insert_variables(*target) + return target + # .................................................... + def _visit_RankDimension(self, expr, **kwargs): + return self.dim + + # .................................................... + def _visit_TensorIterator(self, expr, **kwargs): + target = self._visit(expr.target) + return target + + # .................................................... + def _visit_ProductIterator(self, expr, **kwargs): + target = self._visit(expr.target) + return target + + # .................................................... + def _visit_TensorGenerator(self, expr, **kwargs): + + targets = self._visit(expr.target) + if expr.dummies is None: + #TODO check if we never pass this condition + return expr.target + + if not hasattr(expr.target, 'pattern'): + return targets + + patterns = expr.target.pattern() + patterns = self._visit_Pattern(patterns) + args = {} + for i,target in targets.items(): + args[i] = [] + for p, xs in zip(patterns, target): + ls = [] + for x in xs: + ls.append(x[p]) + args[i].append(tuple(ls)) + args[i] = tuple(args[i]) + + return args + # .................................................... + def _visit_ProductGenerator(self, expr, **kwargs): + target = self._visit(expr.target) + + # treat dummies and put them in the namespace + dummies = self._visit(expr.dummies) + dummies = dummies[0] # TODO add comment + return target[dummies] + + # .................................................... + def _visit_TensorIteration(self, expr, **kwargs): + """ + Initialize index in loop + Parameters + ---------- + expr + kwargs + + Returns + ------- + inits : list + Initialization instructions + + """ + dim = self.dim + iterator = self._visit(expr.iterator) + generator = self._visit(expr.generator) + + # Case of a simple iterable + if isinstance(iterator, (tuple, Tuple, list)): + return [[Assign(i, g)] for i, g in zip(iterator, generator)] + + # Case of a dictionary + + inits = [()]*dim + + for l_xs, g_xs in zip(iterator.values(), generator.values()): + if isinstance(expr.generator.target, (LocalTensorQuadratureBasis, GlobalTensorQuadratureBasis)): + positions = [expr.generator.target.positions[index_deriv]] + g_xs = [SplitArray(xs[0], positions, [self.nderiv+1]) for xs in g_xs] + g_xs = [tuple(self._visit(xs, **kwargs)) for xs in g_xs] + + for i in range(dim): + ls = [] + for l_x,g_x in zip(l_xs[i], g_xs[i]): + if isinstance(expr.generator.target, (LocalTensorQuadratureBasis, GlobalTensorQuadratureBasis)): + lhs = self._visit_BasisAtom(BasisAtom(l_x)) + else: + lhs = l_x + ls += [self._visit(Assign(lhs, g_x))] + inits[i] += tuple(ls) + inits = [flatten(init) for init in inits] + return inits + + # .................................................... + def _visit_ProductIteration(self, expr, **kwargs): + # TODO for the moment, we do not return indices and lengths + iterator = self._visit(expr.iterator) + generator = self._visit(expr.generator) + + return Assign(iterator, generator) + + def _visit_RAT(self, expr): + return str(expr) + + def _visit_WhileLoop(self, expr, **kwargs): + cond = self._visit(expr.condition) + body = [self._visit(a) for a in expr.body] + return While(cond, body) + + def _visit_IfNode(self, expr, **kwargs): + args = [] + for a in expr.args: + cond = self._visit(a[0]) + body = [self._visit(i) for i in a[1]] + args += [(cond, body)] + return If(*args) + # .................................................... + def _visit_Loop(self, expr, **kwargs): + """ + Create + """ + # we first create iteration statements + # these iterations are splitted between what is tensor or not + + # ... treate tensor iterations + + t_iterator = [i for i in expr.iterator if isinstance(i, TensorIterator)] + t_generator = [i for i in expr.generator if isinstance(i, TensorGenerator)] + t_iterations = [TensorIteration(i, j) + for i,j in zip(t_iterator, t_generator)] + + indices = list(self._visit(expr.index)) + starts, stops, lengths = list(self._visit(expr.index.start)), list(self._visit(expr.index.stop)), list(self._visit(expr.index.length)) + + for i,j in zip(flatten(indices), flatten(lengths)): + self.indices[str(i)] = j + + inits = [()]*self._dim + if t_iterations: + t_iterations = [self._visit_TensorIteration(i) for i in t_iterations] + + # indices and lengths are supposed to be repeated here + # we only take the first occurence + for init in t_iterations: + for i in range(self._dim): + inits[i] += tuple(init[i]) + + # ... + # ... treate product iterations + p_iterator = [i for i in expr.iterator if isinstance(i, ProductIterator)] + p_generator = [i for i in expr.generator if isinstance(i, ProductGenerator)] + p_iterations = [ProductIteration(i,j) + for i,j in zip(p_iterator, p_generator)] + + p_inits = [] + if p_iterations: + p_inits = [self._visit_ProductIteration(i) for i in p_iterations] + # ... + + # ... add weighted volume if local quadrature loop + mapping = self.mapping + geo_stmts = expr.get_geometry_stmts(mapping) + geo_stmts = self._visit(geo_stmts, **kwargs) + # ... + + # ... + # visit loop statements + + stmts = self._visit(expr.stmts, **kwargs) + stmts = flatten(stmts) + + # update with product statements if available + body = list(p_inits) + list(geo_stmts) + list(stmts) + mask = expr.mask + + if isinstance(mask,(tuple,Tuple,list)): + mask_init = [] + for axis,T in enumerate(mask): + if T: + indices[axis] = None + starts [axis] = None + stops [axis] = None + mask_init += list(inits[axis]) + inits[axis] = None + indices = [i for i in indices if i is not None] + starts = [i for i in starts if i is not None] + stops = [i for i in stops if i is not None] + inits = [i for i in inits if i is not None] + + elif mask: + axis = mask.axis + index = indices.pop(axis) + start = starts.pop(axis) + stop = stops.pop(axis) + init = inits.pop(axis) + mask_init = [Assign(index, 0), *init] + + if expr.parallel: + body = list(flatten(inits)) + body + for index, s, e in zip(indices[::-1], starts[::-1], stops[::-1]): + body = [For(index, Range(s, e), body)] + else: + for index, s, e, init in zip(indices[::-1], starts[::-1], stops[::-1], inits[::-1]): + + body = list(init) + body + body = [For(index, Range(s, e), body)] + # ... + # remove the list and return the For Node only + + if mask: + body = [*mask_init, *body] + + if expr.parallel: + default = expr.default + shared = [self._visit(i) for i in expr.shared] if expr.shared else [] + private = [self._visit(i) for i in expr.private] if expr.private else [] + firstprivate = [self._visit(i) for i in expr.firstprivate] if expr.firstprivate else [] + lastprivate = [self._visit(i) for i in expr.lastprivate] if expr.lastprivate else [] + shared = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in shared]) + private = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in private]) + firstprivate = flatten([list(i.values())[0] if isinstance(i, dict)else i for i in firstprivate]) + lastprivate = flatten([list(i.values())[0] if isinstance(i, dict) else i for i in lastprivate]) + txt = '#$ omp parallel default({}) &\n'.format(default) + txt += '#$ shared({}) &\n'.format(','.join(str(i) for i in shared if i)) if shared else '' + txt += '#$ private({}) &\n'.format(','.join(str(i) for i in private if i)) if private else '' + txt += '#$ firstprivate({}) &\n'.format(','.join(str(i) for i in firstprivate if i)) if firstprivate else '' + txt += '#$ lastprivate({})'.format(','.join(str(i) for i in lastprivate if i)) if lastprivate else '' + for_pragmas = '#$ omp for schedule(static) collapse({})'.format(self._dim) + if expr.reduction: + for_pragmas = for_pragmas + expr.reduction + + cmt = [Comment(txt.rstrip().rstrip('&')), Comment(for_pragmas)] + endcmt = [Comment('#$ omp end parallel')] + body = [*cmt, *body, *endcmt] + + if len(body) > 1: + body = CodeBlock(body) + elif len(body) == 1: + body = body[0] + + return body + + # .................................................... + def _visit_SplitArray(self, expr, **kwargs): + target = expr.target + positions = expr.positions + lengths = expr.lengths + base = target.base + + args = [] + for p,n in zip(positions, lengths): + indices = target.indices # sympy is return a tuple of tuples + indices = [i for i in indices] # make a copy + for i in range(n): + indices[p] = i + x = base[tuple(indices)] + args.append(x) + return args + + def _visit_Comment(self, expr, **kwargs): + return expr + + # .................................................... + def _visit_IndexedElement(self, expr, **kwargs): + return expr + + # .................................................... + # TODO to be removed. usefull for testing + def _visit_Pass(self, expr, **kwargs): + return expr + + def _visit_Continue(self, expr, **kwargs): + return expr + + def _visit_EmptyNode(self ,expr, **kwargs): + return expr + + def _visit_NoneType(self, expr, **kwargs): + return expr + + def _visit_float(self, expr, **kwargs): + return expr + + def _visit_complex(self, expr, **kwargs): + return expr + diff --git a/psydac/api/ast/utilities.py b/psydac/api/ast/utilities.py new file mode 100644 index 000000000..61056c6b1 --- /dev/null +++ b/psydac/api/ast/utilities.py @@ -0,0 +1,1144 @@ +import re +import string +import random +from itertools import chain + +from sympy import Symbol, IndexedBase, Indexed, Idx +from sympy import Mul, Pow, Function, Tuple +from sympy import sqrt as sympy_sqrt, Range +from sympy.utilities.iterables import cartes + +from sympde.topology.space import ScalarFunction +from sympde.topology.space import VectorFunction +from sympde.topology.space import IndexedVectorFunction +from sympde.topology.space import element_of +from sympde.topology import Mapping +from sympde.topology import Boundary +from sympde.topology.derivatives import _partial_derivatives +from sympde.topology.derivatives import _logical_partial_derivatives +from sympde.topology.derivatives import get_atom_derivatives +from sympde.topology.derivatives import get_index_derivatives +from sympde.topology.derivatives import get_atom_logical_derivatives +from sympde.topology.derivatives import get_index_logical_derivatives +from sympde.topology.derivatives import get_index_derivatives_atom, get_index_logical_derivatives_atom +from sympde.topology import LogicalExpr +from sympde.topology import SymbolicExpr +from sympde.core import Constant + +from psydac.pyccel.ast.core import Variable, IndexedVariable +from psydac.pyccel.ast.core import For +from psydac.pyccel.ast.core import Assign +from psydac.pyccel.ast.core import AugAssign +from psydac.pyccel.ast.core import Product +from psydac.pyccel.ast.core import _atomic +from psydac.pyccel.ast.core import Comment +from psydac.pyccel.ast.core import String +from psydac.pyccel.ast.core import AnnotatedArgument + +__all__ = ( + 'build_pyccel_type_annotations', + 'build_pythran_types_header', + 'compute_atoms_expr', + 'compute_atoms_expr_field', + 'compute_atoms_expr_mapping', + 'compute_boundary_jacobian', + 'compute_normal_vector', + 'compute_tangent_vector', + 'filter_loops', + 'filter_product', + 'fusion_loops', + 'get_name', + 'is_mapping', + 'logical2physical', + 'math_atoms_as_str', + 'random_string', + 'rationalize_eval_mapping', + 'select_loops', + 'variables', +) + +#============================================================================== +def get_max_partial_derivatives(expr, logical=False, F=None): + """ + Compute the maximum order of partial derivatives for each coordinate in an expression. + + TODO + ---- + Move to SymPDE and combine the `get_index(_logical)_derivatives_atom` functions there. + + Parameters + ---------- + expr : sympy.Expr + The SymPDE expression to analyze for partial derivatives. + + logical : bool, optional + If True, it considers logical coordinates (x1, x2, x3); otherwise, it considers physical coordinates (x, y, z). + + F : sympy.Atom | list[sympy.Atom], optional + If provided, it restricts the analysis to the specified atom(s). Otherwise, + it uses all atoms of default types that are contained in `expr`. The default + types represent elements of function spaces in SymPDE: `ScalarFunction`, + `VectorFunction`, and `IndexedVectorFunction`. + + Returns + ------- + d : dict[str, int] + A dictionary with keys ('x1', 'x2', 'x3') for logical or ('x', 'y', 'z') for physical coordinates and their corresponding maximum order of partial derivatives. + """ + + if logical: + d = {'x1': 0, 'x2': 0, 'x3': 0} + get_index = get_index_logical_derivatives_atom + else: + d = {'x': 0, 'y': 0, 'z': 0} + get_index = get_index_derivatives_atom + + if F is None: + F = (list(expr.atoms(ScalarFunction)) + + list(expr.atoms(VectorFunction)) + + list(expr.atoms(IndexedVectorFunction))) + elif not hasattr(F, '__iter__'): + F = [F] + + indices = chain.from_iterable(get_index(expr, Fi) for Fi in F) + + for dd in indices: + for k, v in dd.items(): + if v > d[k]: + d[k] = v + + return d + +#============================================================================== +def random_string( n ): + chars = string.ascii_lowercase + string.digits + selector = random.SystemRandom() + return ''.join( selector.choice( chars ) for _ in range( n ) ) + + +#============================================================================== +def is_mapping(expr): + + if isinstance(expr, _logical_partial_derivatives): + return is_mapping(expr.args[0]) + + elif isinstance(expr, Indexed) and isinstance(expr.base, Mapping): + return True + + elif isinstance(expr, Mapping): + return True + + return False + +#============================================================================== +def logical2physical(expr): + + partial_der = dict(zip(_logical_partial_derivatives,_partial_derivatives)) + + if isinstance(expr, _logical_partial_derivatives): + argument = logical2physical(expr.args[0]) + new_expr = partial_der[type(expr)](argument) + return new_expr + else: + return expr +#============================================================================== +def _get_name(atom): + atom_name = None + if isinstance( atom, ScalarFunction ): + atom_name = str(atom.name) + + elif isinstance( atom, VectorFunction ): + atom_name = str(atom.name) + + elif isinstance( atom, IndexedVectorFunction ): + atom_name = str(atom.base.name) + + else: + raise TypeError('> Wrong type') + + return atom_name + +#============================================================================== +def compute_atoms_expr(atomic_exprs, indices_quad, indices_test, + indices_trial, basis_trial, + basis_test, cords, test_function, + is_linear, + mapping): + + """ + This function computes atomic expressions needed + to evaluate the Kernel final expression + + Parameters + ---------- + atomic_exprs : + list of atoms + + indices_quad : + list of quadrature indices used in the quadrature loops + + indices_test : + list of test_functions indices used in the for loops of the basis functions + + indices_trial : + list of trial_functions indices used in the for loops of the basis functions + + basis_test : + list of basis functions in each dimesion + + cords : + list of coordinates Symbols + + test_function : + test_function Symbol + + is_linear : + variable to determine if we are in the linear case + + mapping : + Mapping object + + Returns + ------- + inits : + list of assignments of the atomic expression evaluated in the quadrature points + + map_stmts : + list of assigments of atomic expression in case of mapping + + """ + + cls = (_partial_derivatives, + VectorFunction, + ScalarFunction, + IndexedVectorFunction) + + dim = len(indices_test) + + if not isinstance(atomic_exprs, (list, tuple, Tuple)): + raise TypeError('Expecting a list of atoms') + + for atom in atomic_exprs: + if not isinstance(atom, cls): + raise TypeError('atom must be of type {}'.format(str(cls))) + + # If there is a mapping, compute [dx(u), dy(u), dz(u)] as functions + # of [dx1(u), dx2(u), dx3(u)], and store results into intermediate + # variables [u_x, u_y, u_z]. (Same thing is done for higher derivatives.) + # + # Accordingly, we create a new list of atoms where all partial derivatives + # are taken with respect to the logical coordinates. + if mapping: + + new_atoms = set() + map_stmts = [] + get_index = get_index_logical_derivatives + get_atom = get_atom_logical_derivatives + + for atom in atomic_exprs: + + if isinstance(atom, _partial_derivatives): + lhs = SymbolicExpr(atom) + rhs_p = LogicalExpr(mapping, atom) + + # we look for new_atoms that must be added to atomic_exprs + # because we need them in the maps stmts + logical_atoms = _atomic(rhs_p, cls=_logical_partial_derivatives) + for a in logical_atoms: + ls = _atomic(a, Symbol) + assert len(ls) == 1 + if isinstance(ls[0], cls): + new_atoms.add(a) + + rhs = SymbolicExpr(rhs_p) + map_stmts += [Assign(lhs, rhs)] + + else: + new_atoms.add(atom) + + else: + new_atoms = atomic_exprs + map_stmts = [] + get_index = get_index_derivatives + get_atom = get_atom_derivatives + + # Create a list of statements for initialization of the point values, + # for each of the atoms in our (possibly new) list. + inits = [] + for atom in new_atoms: + orders = [*get_index(atom).values()] + a = get_atom(atom) + test = _get_name(a) in [_get_name(f) for f in test_function] + + if test or is_linear: + basis = basis_test + idxs = indices_test + else: + basis = basis_trial + idxs = indices_trial + + args = [b[i, d, q] for b, i, d, q in zip(basis, idxs, orders, indices_quad)] + lhs = SymbolicExpr(atom) + rhs = Mul(*args) + inits += [Assign(lhs, rhs)] + + # Return the initialization statements, and the additional initialization + # of intermediate variables in case of mapping + return inits, map_stmts + +#============================================================================== +def compute_atoms_expr_field(atomic_exprs, indices_quad, + idxs, basis, + test_function, mapping): + + """ + This function computes atomic expressions needed + to evaluate EvaluteField/VectorField final expression + + Parameters + ---------- + atomic_exprs : + list of atoms + + indices_quad : + list of quadrature indices used in the quadrature loops + + idxs : + list of basis functions indices used in the for loops of the basis functions + + + basis : + list of basis functions in each dimesion + + test_function : + test_function Symbol + + mapping : + Mapping object + + Returns + ------- + inits : + list of assignments of the atomic expression evaluated in the quadrature points + + updates : + list of augmented assignments which are updated in each loop iteration + + map_stmts : + list of assignments of atomic expression in case of mapping + + new_atoms: + updated list of atomic expressions (some were introduced in case of a mapping) + """ + + inits = [] + updates = [] + map_stmts = [] + + cls = (_partial_derivatives, + ScalarFunction, + IndexedVectorFunction, + VectorFunction) + + # If there is a mapping, compute [dx(u), dy(u), dz(u)] as functions + # of [dx1(u), dx2(u), dx3(u)], and store results into intermediate + # variables [u_x, u_y, u_z]. (Same thing is done for higher derivatives.) + # + # Accordingly, we create a new list of atoms where all partial derivatives + # are taken with respect to the logical coordinates. + if mapping: + + new_atoms = set() + map_stmts = [] + get_index = get_index_logical_derivatives + get_atom = get_atom_logical_derivatives + + for atom in atomic_exprs: + + if isinstance(atom, _partial_derivatives): + lhs = SymbolicExpr(atom) + rhs_p = LogicalExpr(mapping, atom) + + # we look for new_atoms that must be added to atomic_exprs + # because we need them in the maps stmts + logical_atoms = _atomic(rhs_p, cls=_logical_partial_derivatives) + for a in logical_atoms: + ls = _atomic(a, Symbol) + assert len(ls) == 1 + if isinstance(ls[0], cls): + new_atoms.add(a) + + rhs = SymbolicExpr(rhs_p) + map_stmts += [Assign(lhs, rhs)] + + else: + new_atoms.add(atom) + + else: + new_atoms = atomic_exprs + map_stmts = [] + get_index = get_index_derivatives + get_atom = get_atom_derivatives + + # Make sure that we only pick one between 'dx1(dx2(u))' and 'dx2(dx1(u))' + new_atoms = {SymbolicExpr(a).name : a for a in new_atoms} + new_atoms = tuple(new_atoms.values()) + + # Create a list of statements for initialization of the point values, + # for each of the atoms in our (possibly new) list. + inits = [] + for atom in new_atoms: + + # Extract field, compute name of coefficient variable, and get base + if atom.atoms(ScalarFunction): + field = atom.atoms(ScalarFunction).pop() + field_name = 'coeff_' + SymbolicExpr(field).name + base = field + elif atom.atoms(VectorFunction): + field = atom.atoms(IndexedVectorFunction).pop() + field_name = 'coeff_' + SymbolicExpr(field).name + base = field.base + else: + raise TypeError('atom must be either scalar or vector field') + + # Obtain variable for storing point values of test function + test_fun = SymbolicExpr(atom.subs(base, test_function)) + + # ... + orders = [*get_index(atom).values()] + args = [b[i, d, q] for b, i, d, q in zip(basis, idxs, orders, indices_quad)] + inits += [Assign(test_fun, Mul(*args))] + # ... + + # ... + args = [IndexedBase(field_name)[idxs], test_fun] + val_name = SymbolicExpr(atom).name + '_values' + val = IndexedBase(val_name)[indices_quad] + updates += [AugAssign(val,'+',Mul(*args))] + # ... + + return inits, updates, map_stmts, new_atoms + +#============================================================================== +# TODO: merge into 'compute_atoms_expr_field' +def compute_atoms_expr_mapping(atomic_exprs, indices_quad, + idxs, basis, + test_function): + + """ + This function computes atomic expressions needed + to evaluate EvalMapping final expression + + Parameters + ---------- + + atomic_exprs : + list of atoms + + indices_quad : + list of quadrature indices used in the quadrature loops + + idxs : + list of basis functions indices used in the for loops of the basis functions + + basis : + list of basis functions in each dimesion + + test_function : + test_function Symbol + + Returns + ------- + inits : + list of assignments of the atomic expression evaluated in the quadrature points + + updates : + list of augmented assignments which are updated in each loop iteration + """ + + inits = [] + updates = [] + for atom in atomic_exprs: + + element = get_atom_logical_derivatives(atom) + element_name = 'coeff_' + SymbolicExpr(element).name + + # ... + test_fun = atom.subs(element, test_function) + test_fun = SymbolicExpr(test_fun) + # ... + + # ... + orders = [*get_index_logical_derivatives(atom).values()] + args = [b[i, d, q] for b, i, d, q in zip(basis, idxs, orders, indices_quad)] + inits += [Assign(test_fun, Mul(*args))] + # ... + + # ... + val_name = SymbolicExpr(atom).name + '_values' + val = IndexedBase(val_name)[indices_quad] + expr = IndexedBase(element_name)[idxs] * test_fun + updates += [AugAssign(val, '+', expr)] + # ... + + return inits, updates + +#============================================================================== +def rationalize_eval_mapping(mapping, nderiv, space, indices_quad): + + M = mapping + dim = space.ldim + ops = _logical_partial_derivatives[:dim] + + # ... mapping components and their derivatives + components = [M[i] for i in range(0, dim)] + elements = list(components) + + if nderiv > 0: + elements += [d(M[i]) for d in ops for i in range(0, dim)] + + if nderiv > 1: + elements += [d1(d2(M[i])) for e,d1 in enumerate(ops) + for d2 in ops[:e+1] + for i in range(0, dim)] + + if nderiv > 2: + raise NotImplementedError('TODO') + # ... + + # ... weights and their derivatives + # TODO check if 'w' exist already + weights = element_of(space, name='w') + + weights_elements = [weights] + if nderiv > 0: + weights_elements += [d(weights) for d in ops] + + if nderiv > 1: + weights_elements += [d1(d2(weights)) for e,d1 in enumerate(ops) + for d2 in ops[:e+1]] + + if nderiv > 2: + raise NotImplementedError('TODO') + # ... + + stmts = [] + # declarations + stmts += [Comment('declarations')] + for atom in elements + weights_elements: + atom_name = SymbolicExpr(atom).name + val_name = atom_name + '_values' + val = IndexedBase(val_name)[indices_quad] + + stmt = Assign(atom_name, val) + stmts += [stmt] + + # assignements + stmts += [Comment('rationalize')] + + # 0 order terms + for i in range(dim): + w = SymbolicExpr(weights) + u = SymbolicExpr(M[i]) + + val_name = u.name + '_values' + val = IndexedBase(val_name)[indices_quad] + stmt = Assign(val, u / w ) + + stmts += [stmt] + + # 1 order terms + if nderiv >= 1: + for d in ops: + w = SymbolicExpr( weights ) + dw = SymbolicExpr(d(weights)) + + for i in range(dim): + u = SymbolicExpr( M[i] ) + du = SymbolicExpr(d(M[i])) + + val_name = du.name + '_values' + val = IndexedBase(val_name)[indices_quad] + stmt = Assign(val, du / w - u * dw / w**2 ) + + stmts += [stmt] + + # 2 order terms + if nderiv >= 2: + for e, d1 in enumerate(ops): + for d2 in ops[:e+1]: + w = SymbolicExpr( weights ) + d1w = SymbolicExpr( d1(weights) ) + d2w = SymbolicExpr( d2(weights) ) + d1d2w = SymbolicExpr(d1(d2(weights))) + + for i in range(dim): + u = SymbolicExpr( M[i] ) + d1u = SymbolicExpr( d1(M[i]) ) + d2u = SymbolicExpr( d2(M[i]) ) + d1d2u = SymbolicExpr(d1(d2(M[i]))) + + val_name = d1d2u.name + '_values' + val = IndexedBase(val_name)[indices_quad] + stmt = Assign(val, + d1d2u / w - u * d1d2w / w**2 + - d1w * d2u / w**2 - d2w * d1u / w**2 + + 2 * u * d1w * d2w / w**3) + + stmts += [stmt] + + return stmts + +#============================================================================== +def filter_product(indices, args, boundary): + + mask = [] + ext = [] + if boundary: + + if isinstance(boundary, Boundary): + mask = [boundary.axis] + ext = [boundary.ext] + else: + raise TypeError + + # discrete_boundary gives the perpendicular indices, then we need to + # remove them from directions + + dim = len(indices) + args = [args[i][indices[i]] for i in range(dim) if not(i in mask)] + + return Mul(*args) + +#============================================================================== +# TODO remove it later +def filter_loops(indices, ranges, body, boundary, boundary_basis=False): + + quad_mask = [] + quad_ext = [] + if boundary: + + if isinstance(boundary, Boundary): + quad_mask = [boundary.axis] + quad_ext = [boundary.ext] + else: + raise TypeError + + # discrete_boundary gives the perpendicular indices, then we need to + # remove them from directions + + dim = len(indices) + for i in range(dim-1,-1,-1): + rx = ranges[i] + x = indices[i] + start = rx.start + end = rx.stop + + if i in quad_mask: + i_index = quad_mask.index(i) + ext = quad_ext[i_index] + if ext == -1: + end = start + 1 + + elif ext == 1: + start = end - 1 + else: + raise ValueError('> Wrong value for ext. It should be -1 or 1') + + rx = Range(start, end) + body = [For(x, rx, body)] + + body = fusion_loops(body) + + return body + +#============================================================================== +def select_loops(indices, ranges, body, boundary, boundary_basis=False): + + quad_mask = [] + quad_ext = [] + if boundary: + + if isinstance(boundary, Boundary): + quad_mask = [boundary.axis] + quad_ext = [boundary.ext] + else: + raise TypeError + + # discrete_boundary gives the perpendicular indices, then we need to + # remove them from directions + + dim = len(indices) + dims = [i for i in range(dim-1,-1,-1) if not( i in quad_mask )] + + for i in dims: + rx = ranges[i] + x = indices[i] + start = rx.start + end = rx.stop + + rx = Range(start, end) + body = [For(x, rx, body)] + + body = fusion_loops(body) + return body + +#============================================================================== +def fusion_loops(loops): + ranges = [] + indices = [] + loops_cp = loops + + while len(loops) == 1 and isinstance(loops[0], For): + + loops = loops[0] + target = loops.target + iterable = loops.iterable + + if isinstance(iterable, Product): + ranges += list(iterable.elements) + indices += list(target) + if not isinstance(target,(tuple,list,Tuple)): + raise ValueError('target must be a list or a tuple of indices') + + elif isinstance(iterable, Range): + ranges.append(iterable) + indices.append(target) + else: + raise TypeError('only range an product are supported') + + loops = loops.body + + if len(ranges)>1: + return [For(indices, Product(*ranges), loops)] + else: + return loops_cp + +#============================================================================== +def compute_boundary_jacobian(parent_namespace, boundary, mapping=None): + + # Sanity check on arguments + if not isinstance(boundary, Boundary): + raise TypeError(boundary) + + if mapping is None: + stmts = [] + + else: + # Compute metric determinant g on manifold + J = SymbolicExpr(mapping.jacobian) + Jm = J[:, [i for i in range(J.shape[1]) if i != boundary.axis]] + g = (Jm.T * Jm).det() + + # Create statements for computing sqrt(g) + det_jac_bnd = parent_namespace['det_jac_bnd'] + stmts = [Assign(det_jac_bnd, sympy_sqrt(g))] + + return stmts + +#============================================================================== +def compute_normal_vector(parent_namespace, vector, boundary, mapping=None): + + # Sanity check on arguments + if isinstance(boundary, Boundary): + axis = boundary.axis + ext = boundary.ext + else: + raise TypeError(boundary) + + # If there is no mapping, normal vector has only one non-zero component, + # which is +1 or -1 according to the orientation of the boundary. + if mapping is None: + return [Assign(v, ext if i==axis else 0) for i, v in enumerate(vector)] + + # Given the Jacobian matrix J, we need to extract the (i=axis) row of + # J^(-1) and then normalize it. We recall that J^(-1)[i, j] is equal to + # the cofactor of J[i, j] divided by det(J). For efficiency we only + # compute the cofactors C[i=0:dim] of the (j=axis) column of J, and we + # do not divide them by det(J) because the normal vector will need to + # be normalized anyway. + # + # NOTE: we also change the vector orientation according to 'ext' + J = SymbolicExpr(mapping.jacobian) + values = [ext * J.cofactor(i, j=axis) for i in range(J.shape[0])] + + # Create statements for computing normal vector components + stmts = [Assign(lhs, rhs) for lhs, rhs in zip(vector, values)] + + # Normalize vector + inv_norm_variable = Symbol('inv_norm') + inv_norm_value = 1 / sympy_sqrt(sum(v**2 for v in values)) + stmts += [Assign(inv_norm_variable, inv_norm_value)] + stmts += [AugAssign(v, '*', inv_norm_variable) for v in vector] + + return stmts + +#============================================================================== +def compute_tangent_vector(parent_namespace, vector, boundary, mapping): + raise NotImplementedError('TODO') + +#============================================================================== +_range = re.compile('([0-9]*:[0-9]+|[a-zA-Z]?:[a-zA-Z])') + +def variables(names, dtype, **args): + + def contruct_variable(cls, name, dtype, rank, **args): + if issubclass(cls, Variable): + return Variable(dtype, name, rank=rank, **args) + elif issubclass(cls, IndexedVariable): + return IndexedVariable(name, dtype=dtype, rank=rank, **args) + elif cls==Idx: + assert dtype == "int" + rank = args.pop('rank', 0) + assert rank == 0 + return Idx(name) + else: + raise TypeError('only Variables and IndexedVariables are supported') + + result = [] + cls = args.pop('cls', Variable) + + rank = args.pop('rank', 0) + + if isinstance(names, str): + marker = 0 + literals = [r'\,', r'\:', r'\ '] + for i in range(len(literals)): + lit = literals.pop(0) + if lit in names: + while chr(marker) in names: + marker += 1 + lit_char = chr(marker) + marker += 1 + names = names.replace(lit, lit_char) + literals.append((lit_char, lit[1:])) + def literal(s): + if literals: + for c, l in literals: + s = s.replace(c, l) + return s + + names = names.strip() + as_seq = names.endswith(',') + if as_seq: + names = names[:-1].rstrip() + if not names: + raise ValueError('no symbols given') + + # split on commas + names = [n.strip() for n in names.split(',')] + if not all(n for n in names): + raise ValueError('missing symbol between commas') + # split on spaces + for i in range(len(names) - 1, -1, -1): + names[i: i + 1] = names[i].split() + + seq = args.pop('seq', as_seq) + + for name in names: + if not name: + raise ValueError('missing variable') + + if ':' not in name: + var = contruct_variable(cls, literal(name), dtype, rank, **args) + result.append(var) + continue + + split = _range.split(name) + # remove 1 layer of bounding parentheses around ranges + for i in range(len(split) - 1): + if i and ':' in split[i] and split[i] != ':' and \ + split[i - 1].endswith('(') and \ + split[i + 1].startswith(')'): + split[i - 1] = split[i - 1][:-1] + split[i + 1] = split[i + 1][1:] + for i, s in enumerate(split): + if ':' in s: + if s[-1].endswith(':'): + raise ValueError('missing end range') + a, b = s.split(':') + if b[-1] in string.digits: + a = 0 if not a else int(a) + b = int(b) + split[i] = [str(c) for c in range(a, b)] + else: + a = a or 'a' + split[i] = [string.ascii_letters[c] for c in range( + string.ascii_letters.index(a), + string.ascii_letters.index(b) + 1)] # inclusive + if not split[i]: + break + else: + split[i] = [s] + else: + seq = True + if len(split) == 1: + names = split[0] + else: + names = [''.join(s) for s in cartes(*split)] + if literals: + result.extend([contruct_variable(cls, literal(s), dtype, rank, **args) for s in names]) + else: + result.extend([contruct_variable(cls, s, dtype, rank, **args) for s in names]) + + if not seq and len(result) <= 1: + if not result: + return () + return result[0] + + return tuple(result) + elif isinstance(names,(tuple,list)): + return tuple(variables(i, dtype, cls=cls,rank=rank,**args) for i in names) + else: + raise TypeError('Expecting a string') + +#============================================================================== +def build_pyccel_type_annotations(args, order=None): + + new_args = [] + + for a in args: + if isinstance(a, Variable): + rank = a.rank + dtype = a.dtype.name.lower() + + elif isinstance(a, IndexedVariable): + rank = a.rank + dtype = a.dtype.name.lower() + + elif isinstance(a, Constant): + rank = 0 + if a.is_integer: + dtype = 'int' + elif a.is_real: + dtype = 'float' + elif a.is_complex: + dtype = 'complex' + else: + raise TypeError(f"The Constant {a} don't have any information about the type of the variable.\n" + f"Please create the Constant like this Constant('{a}', real=True), Constant('{a}', complex=True) or Constant('{a}', integer=True).") + + else: + raise TypeError('unexpected type for {}'.format(a)) + + if rank > 0: + shape = ','.join(':' * rank) + dtype = '{dtype}[{shape}]'.format(dtype=dtype, shape=shape) + if order and rank > 1: + dtype = "{dtype}(order={ordering})".format(dtype=dtype, ordering=order) + + dtype = String(dtype) + new_a = AnnotatedArgument(a, dtype) + new_args.append(new_a) + + return new_args + +#============================================================================== +def build_pythran_types_header(name, args, order=None): + """ + builds a types decorator from a list of arguments (of FunctionDef) + """ + types = [] + for a in args: + if isinstance(a, Variable): + dtype = pythran_dtypes[a.dtype.name.lower()] + + elif isinstance(a, IndexedVariable): + dtype = pythran_dtypes[a.dtype.name.lower()] + + else: + raise TypeError('unepected type for {}'.format(a)) + + if a.rank > 0: + shape = ['[]' for i in range(0, a.rank)] + shape = ''.join(i for i in shape) + dtype = '{dtype}{shape}'.format(dtype=dtype, shape=shape) + if order and a.rank > 1: + dtype = "{dtype}".format(dtype=dtype, ordering=order) + + types.append(dtype) + types = ', '.join(_type for _type in types) + header = '#pythran export {name}({types})'.format(name=name, types=types) + return header + +pythran_dtypes = {'real':'float','int':'int'} + +#============================================================================== + +from sympy import preorder_traversal +from sympy import NumberSymbol +from sympy import Pow, S + +_known_functions_math = { + 'acos': 'acos', + 'acosh': 'acosh', + 'asin': 'asin', + 'asinh': 'asinh', + 'atan': 'atan', + 'atan2': 'atan2', + 'atanh': 'atanh', + 'ceiling': 'ceil', + 'cos': 'cos', + 'cosh': 'cosh', + 'erf': 'erf', + 'erfc': 'erfc', + 'exp': 'exp', + 'expm1': 'expm1', + 'factorial': 'factorial', + 'floor': 'floor', + 'gamma': 'gamma', + 'hypot': 'hypot', + 'loggamma': 'lgamma', + 'log': 'log', + 'ln': 'log', + 'log10': 'log10', + 'log1p': 'log1p', + 'log2': 'log2', + 'sin': 'sin', + 'sinh': 'sinh', + 'Sqrt': 'sqrt', + 'tan': 'tan', + 'tanh': 'tanh' + +} # Not used from ``math``: [copysign isclose isfinite isinf isnan ldexp frexp pow modf +# radians trunc fmod fsum gcd degrees fabs] +_known_constants_math = { + 'Exp1': 'e', + 'Pi': 'pi', + 'E': 'e' + # Only in python >= 3.5: + # 'Infinity': 'inf', + # 'NaN': 'nan' +} + +_not_in_mpmath = 'log1p log2'.split() +_in_mpmath = [(k, v) for k, v in _known_functions_math.items() if k not in _not_in_mpmath] +_known_functions_mpmath = dict(_in_mpmath, **{ + 'beta': 'beta', + 'fresnelc': 'fresnelc', + 'fresnels': 'fresnels', + 'sign': 'sign', +}) +_known_constants_mpmath = { + 'Exp1': 'e', + 'Pi': 'pi', + 'GoldenRatio': 'phi', + 'EulerGamma': 'euler', + 'Catalan': 'catalan', + 'NaN': 'nan', + 'Infinity': 'inf', + 'NegativeInfinity': 'ninf' +} + +_not_in_numpy = 'erf erfc factorial gamma loggamma'.split() +_in_numpy = [(k, v) for k, v in _known_functions_math.items() if k not in _not_in_numpy] +_known_functions_numpy = dict(_in_numpy, **{ + 'acos': 'arccos', + 'acosh': 'arccosh', + 'asin': 'arcsin', + 'asinh': 'arcsinh', + 'atan': 'arctan', + 'atan2': 'arctan2', + 'atanh': 'arctanh', + 'exp2': 'exp2', + 'sign': 'sign', +}) +_known_constants_numpy = { + 'Exp1': 'e', + 'Pi': 'pi', + 'EulerGamma': 'euler_gamma', + 'NaN': 'nan', + 'Infinity': 'PINF', + 'NegativeInfinity': 'NINF' +} + + +def math_atoms_as_str(expr, lib='math'): + """ + Given a Sympy expression, find all known mathematical atoms (functions and + constants) that need to be imported from a math library (e.g. Numpy) when + generating Python code. + + Parameters + ---------- + expr : sympy.core.expr.Expr + Symbolic expression for which Python code is to be generated. + + lib : str + Library used to translate symbolic functions/constants into standard + Python ones. Options: ['math', 'mpmath', 'numpy']. Default: 'math'. + + Returns + ------- + imports : set of str + Set of all names (strings) to be imported. + + """ + + # Choose translation dictionaries + if lib == 'math': + known_functions = _known_functions_math + known_constants = _known_constants_math + elif lib == 'mpmath': + known_functions = _known_functions_mpmath + known_constants = _known_constants_mpmath + elif lib == 'numpy': + known_functions = _known_functions_numpy + known_constants = _known_constants_numpy # numpy version missing + else: + raise ValueError("Library {} not supported.".format(mod)) + + # Initialize variables + math_functions = set() + math_constants = set() + sqrt = False + + # Walk expression tree + for i in preorder_traversal(expr): + + # Search for math functions (e.g. cos, sin, exp, ...) + if isinstance(i, Function): + s = str(type(i)) + if s in known_functions: + p = known_functions[s] + math_functions.add(p) + + # Search for math constants (e.g. pi, e, ...) + elif isinstance(i, NumberSymbol): + s = type(i).__name__ + if s in known_constants: + p = known_constants[s] + math_constants.add(p) + + # Search for square roots + elif (not sqrt): + if isinstance(i, Pow) and ((i.exp is S.Half) or (i.exp == -S.Half)): + math_functions.add('sqrt') + sqrt = True + + return set.union(math_functions, math_constants) + +def get_name(lhs): + """ + Given a list of variable return the meaningful part of the name of the + first variable that has a _name attribute. + + Was added to solve issue #327 caused by trying to access the name of a + variable that has not such attribute. + + Parameters + ---------- + lhs : list + list from whom we need to extract a name. + + Returns + ------- + str + meaningful part of the name of the variable or "zero term" if no + variable has a name. + + """ + for term in lhs: + if hasattr(term, '_name'): + return term._name[12:-8] + return "zero_term" diff --git a/psydac/api/basic.py b/psydac/api/basic.py new file mode 100644 index 000000000..608a13246 --- /dev/null +++ b/psydac/api/basic.py @@ -0,0 +1,346 @@ +# coding: utf-8 + +# TODO: - init_fem is called whenever we call discretize. we should check that +# nderiv has not been changed. shall we add nquads too? + +# TODO: avoid using os.system and use subprocess.call + +import sys +import os +import importlib +import numpy as np +from mpi4py import MPI + +from psydac.api.ast.fem import AST +from psydac.api.ast.parser import parse +from psydac.api.printing.pycode import pycode +from psydac.api.settings import PSYDAC_BACKENDS, PSYDAC_DEFAULT_FOLDER +from psydac.api.utilities import mkdir_p, touch_init_file, random_string, write_code + +__all__ = ('BasicCodeGen', 'BasicDiscrete') + +#============================================================================== +# TODO have it as abstract class +class BasicCodeGen: + """ Basic class for any discrete concept that needs code generation. + + Parameters + ---------- + + folder: str + The output folder where we generate the code. + + comm: MPI.Comm + The mpi communicator used in the parallel case. + + root: int + The process that is responsible of generating the code. + + discrete_space: FemSpace | list of FemSpace + The discrete fem spaces. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the bi-linear form. + + nquads: list of tuple + The number of quadrature points used in the assembly method. + + is_rational_mapping : bool + takes the value of True if the mapping is rational. + + mapping: Sympde.topology.Mapping + The symbolic mapping of the bi-linear form domain. + + mapping_space: FemSpace + The discete space of the mapping. + + num_threads: int + Number of threads used in the computing kernels. + + backend: dict + The backend used to accelerate the computing kernels. + The content of the dictionary can be found in psydac/api/settings.py. + + """ + def __init__(self, expr, *, folder=None, comm=None, root=None, discrete_space=None, + kernel_expr=None, nquads=None, is_rational_mapping=None, mapping=None, + mapping_space=None, num_threads=None, backend=None): + + # Get default backend from environment, or use 'python'. + default_backend = PSYDAC_BACKENDS.get(os.environ.get('PSYDAC_BACKEND'))\ + or PSYDAC_BACKENDS['python'] + + backend = backend or default_backend + # ... + if not( comm is None) and comm.size>1: + if root is None: + root = 0 + + assert isinstance( comm, MPI.Comm ) + assert isinstance( root, int ) + + if comm.rank == root: + tag = random_string( 8 ) + ast = self._create_ast( expr=expr, tag=tag, comm=comm, discrete_space=discrete_space, + kernel_expr=kernel_expr, nquads=nquads, is_rational_mapping=is_rational_mapping, + mapping=mapping, mapping_space=mapping_space, num_threads=num_threads, backend=backend ) + + max_nderiv = ast.nderiv + func_name = ast.expr.name + arguments = ast.expr.arguments.copy() + free_args = arguments.pop('fields', ()) + arguments.pop('constants', ()) + free_args = tuple(str(i) for i in free_args) + + else: + tag = None + ast = None + max_nderiv = None + func_name = None + free_args = None + + tag = comm.bcast(tag, root=root ) + func_name = comm.bcast(func_name, root=root) + max_nderiv = comm.bcast(max_nderiv, root=root ) + free_args = comm.bcast(free_args, root=root) + #user_functions = comm.bcast( user_functions, root=root ) + else: + tag = random_string( 8 ) + ast = self._create_ast( expr=expr, tag=tag, discrete_space=discrete_space, + kernel_expr=kernel_expr, nquads=nquads, is_rational_mapping=is_rational_mapping, + mapping=mapping, mapping_space=mapping_space, num_threads=num_threads, backend=backend ) + + max_nderiv = ast.nderiv + func_name = ast.expr.name + arguments = ast.expr.arguments.copy() + free_args = arguments.pop('fields', ()) + arguments.pop('constants', ()) + free_args = tuple(str(i) for i in free_args) + + user_functions = None + self._expr = expr + self._tag = tag + self._ast = ast + self._func_name = func_name + self._free_args = free_args + self._user_functions = user_functions + self._backend = backend + self._folder = self._initialize_folder(folder) + self._comm = comm + self._root = root + self._max_nderiv = max_nderiv + self._code = None + self._func = None + self._dependencies_modname = 'dependencies_{}'.format(self.tag) + self._dependencies_fname = '{}.py'.format(self._dependencies_modname) + # ... + + # ... when using user defined functions, there must be passed as + # arguments of discretize. here we create a dictionary where the key + # is the function name, and the value is a valid implementation. + # if user_functions: + # for f in user_functions: + # if not hasattr(f, '_imp_'): + # # TODO raise appropriate error message + # raise ValueError('can not find {} implementation'.format(f)) + + if ast: + python_code = self._generate_code() + self._save_code(python_code, backend=self.backend['name']) + + if comm is not None and comm.size > 1: + comm.Barrier() + + # compile code + self._compile() + + @property + def expr(self): + return self._expr + + @property + def tag(self): + return self._tag + + @property + def user_functions(self): + return self._user_functions + + @property + def free_args(self): + return self._free_args + + @property + def ast(self): + return self._ast + + @property + def code(self): + return self._interface_code + + @property + def func(self): + return self._func + + @property + def backend(self): + return self._backend + + @property + def comm(self): + return self._comm + + @property + def root(self): + return self._root + + @property + def folder(self): + return self._folder + + @property + def dependencies_fname(self): + return self._dependencies_fname + + @property + def dependencies_modname(self): + return self._dependencies_modname + + def _create_ast(self, **kwargs): + raise NotImplementedError('Must be implemented') + + def _initialize_folder(self, folder=None): + # ... + if folder is None: + basedir = os.getcwd() + folder = PSYDAC_DEFAULT_FOLDER['name'] + folder = os.path.join( basedir, folder ) + + # ... add __init__ to all directories to be able to + touch_init_file('__pycache__') + for root, dirs, files in os.walk(folder): + touch_init_file(root) + # ... + + else: + raise NotImplementedError('user output folder not yet available') + + folder = os.path.abspath( folder ) + mkdir_p(folder) + # ... + + return folder + + def _generate_code(self): + """ + Generate Python code which can be pyccelized. + """ + psydac_ast = self.ast + + parser_settings = { + 'dim' : psydac_ast.dim, + 'nderiv' : psydac_ast.nderiv, + 'mapping': psydac_ast.mapping, + 'target' : psydac_ast.domain + } + + pyccel_ast = parse(psydac_ast.expr, settings=parser_settings, backend=self.backend) + python_code = pycode(pyccel_ast) + + return python_code + + def _save_code(self, code, backend=None): + # ... + write_code(self._dependencies_fname, code, folder = self.folder) + + def _compile_pythran(self, mod): + raise NotImplementedError('Pythran is not available') + + def _compile_pyccel(self, mod, verbose=False): + + # ... convert python to fortran using pyccel + compiler_family = self.backend['compiler_family'] + flags = self.backend['flags'] + openmp = self.backend["openmp"] + _PYCCEL_FOLDER = self.backend['folder'] + + from pyccel import epyccel + fmod = epyccel(mod, + openmp = openmp, + compiler_family = compiler_family, + flags = flags, + comm = self.comm, + bcast = True, + folder = _PYCCEL_FOLDER, + verbose = verbose) + + return fmod + + def _compile(self): + + module_name = self.dependencies_modname + sys.path.append(self.folder) + package = importlib.import_module( module_name ) + sys.path.remove(self.folder) + + if self.backend['name'] == 'pyccel': + package = self._compile_pyccel(package) + elif self.backend['name'] == 'pythran': + package = self._compile_pythran(package) + + self._func = getattr(package, self._func_name) + +#============================================================================== +class BasicDiscrete(BasicCodeGen): + """ mapping is the symbolic mapping here. + kwargs is used to pass user defined functions for the moment. + """ + + def __init__(self, expr, kernel_expr, *, folder=None, comm=None, root=None, discrete_space=None, + nquads=None, is_rational_mapping=None, mapping=None, + mapping_space=None, num_threads=None, backend=None): + + BasicCodeGen.__init__(self, expr, folder=folder, comm=comm, root=root, discrete_space=discrete_space, + kernel_expr=kernel_expr, nquads=nquads, is_rational_mapping=is_rational_mapping, + mapping=mapping, mapping_space=mapping_space, num_threads=num_threads, backend=backend) + # ... + self._kernel_expr = kernel_expr + # ... + + @property + def kernel_expr(self): + return self._kernel_expr + + @property + def target(self): + return self._target + + @property + def mapping(self): + return self._mapping + + @property + def is_rational_mapping(self): + return self._is_rational_mapping + + @property + def max_nderiv(self): + return self._max_nderiv + + def _create_ast(self, **kwargs): + + expr = kwargs.pop('expr') + kernel_expr = kwargs.pop('kernel_expr') + discrete_space = kwargs.pop('discrete_space') + + mapping_space = kwargs.pop('mapping_space', None) + tag = kwargs.pop('tag', None) + nquads = kwargs.pop('nquads', None) + mapping = kwargs.pop('mapping', None) + num_threads = kwargs.pop('num_threads', None) + backend = kwargs.pop('backend', None) + is_rational_mapping = kwargs.pop('is_rational_mapping', None) + + return AST(expr, kernel_expr, discrete_space, mapping_space=mapping_space, + tag=tag, nquads=nquads, mapping=mapping, is_rational_mapping=is_rational_mapping, + backend=backend, num_threads=num_threads) + + diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py new file mode 100644 index 000000000..d98ba1a51 --- /dev/null +++ b/psydac/api/discretization.py @@ -0,0 +1,690 @@ +# coding: utf-8 + +# TODO: - init_fem is called whenever we call discretize. we should check that +# nderiv has not been changed. shall we add nquads too? +import os + +from sympy import Expr as sym_Expr +import numpy as np + +from sympde.expr import BasicForm as sym_BasicForm +from sympde.expr import BilinearForm as sym_BilinearForm +from sympde.expr import LinearForm as sym_LinearForm +from sympde.expr import Functional as sym_Functional +from sympde.expr import Equation as sym_Equation +from sympde.expr import Norm as sym_Norm, SemiNorm as sym_SemiNorm +from sympde.expr import TerminalExpr + +from sympde.topology import BasicFunctionSpace +from sympde.topology import VectorFunctionSpace +from sympde.topology import ProductSpace +from sympde.topology import Domain +from sympde.topology import Derham +from sympde.topology import LogicalExpr +from sympde.topology import H1SpaceType, HcurlSpaceType, HdivSpaceType, L2SpaceType, UndefinedSpaceType + +from gelato.expr import GltExpr as sym_GltExpr + +from psydac.api.fem_bilinear_form import DiscreteBilinearForm as DiscreteBilinearForm_SF +from psydac.api.fem_sum_form import DiscreteSumForm +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.glt import DiscreteGltExpr +from psydac.api.expr import DiscreteExpr +from psydac.api.equation import DiscreteEquation +from psydac.api.utilities import flatten +from psydac.fem.basic import FemSpace +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.fem.partitioning import create_cart, construct_connectivity, construct_interface_spaces, construct_reduced_interface_spaces +from psydac.fem.vector import MultipatchFemSpace, VectorFemSpace +from psydac.cad.geometry import Geometry +from psydac.linalg.stencil import StencilVectorSpace +from psydac.linalg.block import BlockVectorSpace + +__all__ = ( + 'discretize', + 'discretize_derham', + 'discretize_derham_multipatch', + 'reduce_space_degrees', + 'discretize_space', + 'discretize_domain' +) + +#============================================================================== +def change_dtype(V, dtype): + """ + Given a FemSpace V, change its underlying coeff_space (i.e. the space of + its coefficients) so that it matches the required data type. + + Parameters + ---------- + Vh : FemSpace + The FEM space, which is modified in place. + + dtype : float or complex + Datatype of the new coeff_space. + + Returns + ------- + FemSpace + The same FEM space passed as input, which was modified in place. + """ + if not V.coeff_space.dtype == dtype: + if isinstance(V.coeff_space, BlockVectorSpace): + # Recreate the BlockVectorSpace + new_spaces = [] + for v in V.spaces: + change_dtype(v, dtype) + new_spaces.append(v.coeff_space) + V._coeff_space = BlockVectorSpace(*new_spaces, connectivity=V.coeff_space.connectivity) + + # If the coeff_space is a StencilVectorSpace + else: + # Recreate the StencilVectorSpace + interfaces = V.coeff_space.interfaces + V._coeff_space = StencilVectorSpace(V.coeff_space.cart, dtype=dtype) + + # Recreate the interface in the StencilVectorSpace + for (axis, ext), interface_space in interfaces.items(): + V.coeff_space.set_interface(axis, ext, interface_space.cart) + + return V + +#============================================================================== +def get_max_degree_of_one_space(Vh): + """ + Get the maximum polynomial degree of a finite element space, along each + logical (parametric) coordinate. + + Parameters + ---------- + Vh : FemSpace + The finite element space under investigation. + + Returns + ------- + list[int] + The maximum polynomial degre of Vh with respect to each coordinate. + + """ + + if isinstance(Vh, TensorFemSpace): + return Vh.degree + + elif isinstance(Vh, VectorFemSpace): + return [max(p) for p in zip(*Vh.degree)] + + elif isinstance(Vh, MultipatchFemSpace): + degree = [get_max_degree_of_one_space(Vh_i) for Vh_i in Vh.spaces] + return [max(p) for p in zip(*degree)] + + else: + raise TypeError(f'Type({V}) not understood') + + +def get_max_degree(*spaces): + """ + Get the maximum polynomial degree across several finite element spaces, + along each logical (parametric) coordinate. + + Parameters + ---------- + *spaces : tuple[FemSpace] + The finite element spaces under investigation. + + Returns + ------- + list[int] + The maximum polynomial degree across all spaces, with respect to each + coordinate. + + """ + degree = [get_max_degree_of_one_space(Vh) for Vh in spaces] + return [max(p) for p in zip(*degree)] + +#============================================================================== +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. + + Parameters + ---------- + derham : sympde.topology.space.Derham + The symbolic Derham sequence. + + domain_h : Geometry + Discrete domain where the spaces will be discretized. + + get_H1vec_space : bool, default=False + True to also get the "Hvec" space discretizing (H1)^n vector fields. + + **kwargs : dict + Optional parameters for the space discretization. + + Returns + ------- + DiscreteDeRham + The discrete de Rham sequence containing the discrete spaces, + differential operators and projectors. + + See Also + -------- + discretize_space + """ + + ldim = derham.shape + bases = ['B'] + ldim * ['M'] + spaces = [discretize_space(V, domain_h, basis=basis, **kwargs) + for V, basis in zip(derham.spaces, bases)] + + if get_H1vec_space: + X = VectorFunctionSpace('X', domain_h.domain, kind='h1') + V0h = spaces[0] + Xh = VectorFemSpace(*([V0h]*ldim)) + Xh.symbolic_space = X + #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) + +#============================================================================== +def discretize_derham_multipatch(derham, domain_h, **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. + + 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, basis=basis, **kwargs) \ + for V, basis in zip(derham.spaces, bases)] + + return DiscreteDeRhamMultipatch( + domain_h = domain_h, + spaces = spaces + ) + +#============================================================================== +def reduce_space_degrees(V, Vh, *, basis='B', sequence='DR'): + """ + This function takes a tensor FEM space Vh and reduces some degrees in order + to obtain a tensor FEM space Wh that matches the symbolic space V in a + certain sequence of spaces. Where the degree is reduced, Wh employs either + a B-spline or an M-spline basis. + + For example let [p1, p2, p3] indicate the degrees and [r1, r2, r3] indicate + the interior multiplicites in each direction of the space Vh before + reduction. The degrees and multiplicities of the reduced spaces are + specified as follows: + + With the 'DR' sequence in 3D, all multiplicies are [r1, r2, r3] and we have + 'H1' : degree = [p1, p2, p3] + 'Hcurl': degree = [[p1-1, p2, p3], [p1, p2-1, p3], [p1, p2, p3-1]] + 'Hdiv' : degree = [[p1, p2-1, p3-1], [p1-1, p2, p3-1], [p1-1, p2-1, p3]] + 'L2' : degree = [p1-1, p2-1, p3-1] + + With the 'TH' sequence in 2D we have: + 'H1' : degree = [[p1, p2], [p1, p2]], multiplicity = [[r1, r2], [r1, r2]] + 'L2' : degree = [p1-1, p2-1], multiplicity = [r1-1, r2-1] + + With the 'RT' sequence in 2D we have: + 'H1' : degree = [[p1, p2-1], [p1-1, p2]], multiplicity = [[r1,r2], [r1,r2]] + 'L2' : degree = [p1-1, p2-1], multiplicity = [r1, r2] + + With the 'N' sequence in 2D we have: + 'H1' : degree = [[p1, p2], [p1, p2]], multiplicity = [[r1,r2+1], [r1+1,r2]] + 'L2' : degree = [p1-1, p2-1], multiplicity = [r1, r2] + + For more details see: + + [1] : A. Buffa, J. Rivas, G. Sangalli, and R.G. Vazquez. Isogeometric + Discrete Differential Forms in Three Dimensions. SIAM J. Numer. Anal., + 49:818-844, 2011. DOI:10.1137/100786708. (Section 4.1) + + [2] : A. Buffa, C. de Falco, and G. Sangalli. IsoGeometric Analysis: + Stable elements for the 2D Stokes equation. Int. J. Numer. Meth. Fluids, + 65:1407-1422, 2011. DOI:10.1002/fld.2337. (Section 3) + + [3] : A. Bressan, and G. Sangalli. Isogeometric discretizations of the + Stokes problem: stability analysis by the macroelement technique. IMA J. + Numer. Anal., 33(2):629-651, 2013. DOI:10.1093/imanum/drr056. + + Parameters + ---------- + V : FunctionSpace + The symbolic space. + + Vh : TensorFemSpace + The tensor product FEM space. + + basis: str + The basis function of the reduced spaces, it can be either 'B' for + B-spline basis or 'M' for M-spline basis. + + sequence: str + The sequence used to reduce the space. The available choices are: + 'DR': for the de Rham sequence, as described in [1], + 'TH': for Taylor-Hood elements, as described in [2]. + Not implemented yet: + 'N' : for Nedelec elements, as described in [2], + 'RT': for Raviart-Thomas elements, as described in [2]. + + Returns + ------- + Wh : TensorFemSpace, VectorFemSpace + The reduced space. + + """ + multiplicity = Vh.multiplicity + if isinstance(V.kind, HcurlSpaceType): + if sequence == 'DR': + if V.ldim == 2: + spaces = [Vh.reduce_degree(axes=[0], multiplicity=multiplicity[0:1], basis=basis), + Vh.reduce_degree(axes=[1], multiplicity=multiplicity[1:] , basis=basis)] + elif V.ldim == 3: + spaces = [Vh.reduce_degree(axes=[0], multiplicity=multiplicity[0:1], basis=basis), + Vh.reduce_degree(axes=[1], multiplicity=multiplicity[1:2], basis=basis), + Vh.reduce_degree(axes=[2], multiplicity=multiplicity[2:] , basis=basis)] + else: + raise NotImplementedError('TODO') + else: + raise NotImplementedError('The sequence {} is not currently available for the space kind {}'.format(sequence, V.kind)) + Wh = VectorFemSpace(*spaces) + + elif isinstance(V.kind, HdivSpaceType): + if sequence == 'DR': + if V.ldim == 2: + spaces = [Vh.reduce_degree(axes=[1], multiplicity=multiplicity[:1], basis=basis), + Vh.reduce_degree(axes=[0], multiplicity=multiplicity[1:], basis=basis)] + elif V.ldim == 3: + spaces = [Vh.reduce_degree(axes=[1,2], multiplicity=multiplicity[1:], basis=basis), + Vh.reduce_degree(axes=[0,2], multiplicity=[multiplicity[0], multiplicity[2]], basis=basis), + Vh.reduce_degree(axes=[0,1], multiplicity=multiplicity[:2], basis=basis)] + else: + raise NotImplementedError('TODO') + else: + raise NotImplementedError('The sequence {} is not currently available for the space kind {}'.format(sequence, V.kind)) + Wh = VectorFemSpace(*spaces) + + elif isinstance(V.kind, L2SpaceType): + if sequence == 'DR': + if V.ldim == 1: + Wh = Vh.reduce_degree(axes=[0], multiplicity=multiplicity, basis=basis) + elif V.ldim == 2: + Wh = Vh.reduce_degree(axes=[0,1], multiplicity=multiplicity, basis=basis) + elif V.ldim == 3: + Wh = Vh.reduce_degree(axes=[0,1,2], multiplicity=multiplicity, basis=basis) + elif sequence == 'TH': + multiplicity = [max(1,m-1) for m in multiplicity] + if V.ldim == 1: + Wh = Vh.reduce_degree(axes=[0], multiplicity=multiplicity, basis=basis) + elif V.ldim == 2: + Wh = Vh.reduce_degree(axes=[0,1], multiplicity=multiplicity, basis=basis) + elif V.ldim == 3: + Wh = Vh.reduce_degree(axes=[0,1,2], multiplicity=multiplicity, basis=basis) + else: + raise NotImplementedError('The sequence {} is not currently available for the space kind {}'.format(sequence, V.kind)) + + elif isinstance(V.kind, (H1SpaceType, UndefinedSpaceType)): + Wh = Vh # Do not reduce space + + else: + raise NotImplementedError('Cannot create FEM space with kind = {}'.format(V.kind)) + + if isinstance(V, VectorFunctionSpace): + if isinstance(V.kind, (H1SpaceType, L2SpaceType, UndefinedSpaceType)): + Wh = VectorFemSpace(*[Wh]*V.ldim) + + return Wh + +#============================================================================== +# TODO knots +def discretize_space(V, domain_h, *, degree=None, multiplicity=None, knots=None, basis='B', sequence='DR'): + """ + This function creates the discretized space starting from the symbolic space. + + Parameters + ---------- + V : + The symbolic space. + + domain_h : + The discretized domain. + + degree : list | dict + The degree of the h1 space in each direction. + + multiplicity: list | dict + The multiplicity of knots for the h1 space in each direction. + + knots: list | dict + The knots sequence of the h1 space in each direction. + + basis: str + The type of basis function can be 'B' for B-splines or 'M' for M-splines. + + sequence: str + The sequence used to reduce the space. The available choices are: + 'DR': for the de Rham sequence, as described in [1], + 'TH': for Taylor-Hood elements, as described in [2]. + Not implemented yet: + 'N' : for Nedelec elements, as described in [2], + 'RT': for Raviart-Thomas elements, as described in [2]. + + For more details see: + + [1] : A. Buffa, J. Rivas, G. Sangalli, and R.G. Vazquez. Isogeometric + Discrete Differential Forms in Three Dimensions. SIAM J. Numer. Anal., + 49:818-844, 2011. DOI:10.1137/100786708. (Section 4.1) + + [2] : A. Buffa, C. de Falco, and G. Sangalli. IsoGeometric Analysis: + Stable elements for the 2D Stokes equation. Int. J. Numer. Meth. Fluids, + 65:1407-1422, 2011. DOI:10.1002/fld.2337. (Section 3) + + [3] : A. Bressan, and G. Sangalli. Isogeometric discretizations of the + Stokes problem: stability analysis by the macroelement technique. IMA J. + Numer. Anal., 33(2):629-651, 2013. DOI:10.1093/imanum/drr056. + + Returns + ------- + Vh : + The discrete FEM space. + """ + +# we have two cases, the case where we have a geometry file, +# and the case where we have either an analytical mapping or without a mapping. +# We build the dictionary g_spaces for each interior domain, where it conatians the interiors as keys and the spaces as values, +# we then create the compatible spaces if needed with the suitable basis functions. + + comm = domain_h.comm + ldim = V.ldim + is_rational_mapping = False + + assert sequence in ['DR', 'TH', 'N', 'RT'] + if sequence in ['TH', 'N', 'RT']: + assert isinstance(V, ProductSpace) and len(V.spaces) == 2 + + # Define data type of our TensorFemSpace + dtype = float + # TODO remove when codomain_type is implemented in SymPDE + if hasattr(V, 'codomain_type'): + if V.codomain_type == 'complex': + dtype = complex + + g_spaces = {} + domain = domain_h.domain + + if len(domain)==1: + interiors = [domain.interior] + else: + interiors = list(domain.interior.args) + + connectivity = construct_connectivity(domain) + if isinstance(domain_h, Geometry) and all(domain_h.mappings.values()): + # from a discrete geoemtry + if interiors[0].name in domain_h.mappings: + mappings = [domain_h.mappings[inter.name] for inter in interiors] + else: + mappings = [domain_h.mappings[inter.logical_domain.name] for inter in interiors] + + # Get all the FEM spaces from the mapping and convert their coeff_space at the dtype needed + spaces = [change_dtype(m.space, dtype) for m in mappings] + g_spaces = dict(zip(interiors, spaces)) + spaces = [S.spaces for S in spaces] + + if not( comm is None ) and ldim == 1: + raise NotImplementedError('must create a TensorFemSpace in 1d') + + else: + + if isinstance( degree, (list, tuple) ): + degree = {I.name:degree for I in interiors} + else: + assert isinstance(degree, dict) + + if isinstance( multiplicity, (list, tuple) ): + multiplicity = {I.name:multiplicity for I in interiors} + elif multiplicity is None: + multiplicity = {I.name:(1,)*len(degree[I.name]) for I in interiors} + else: + assert isinstance(multiplicity, dict) + + if isinstance(knots, (list, tuple)): + assert len(interiors) == 1 + knots = {interiors[0].name:knots} + + if len(interiors) == 1: + ddms = [domain_h.ddm] + else: + ddms = domain_h.ddm.domains + + spaces = [None]*len(interiors) + for i,interior in enumerate(interiors): + ncells = domain_h.ncells[interior.name] + periodic = domain_h.periodic[interior.name] + degree_i = degree[interior.name] + multiplicity_i = multiplicity[interior.name] + min_coords = interior.min_coords + max_coords = interior.max_coords + + assert len(ncells) == len(periodic) == len(degree_i) == len(multiplicity_i) == len(min_coords) == len(max_coords) + + if knots is None: + # Create uniform grid + grids = [np.linspace(xmin, xmax, num=ne + 1) + for xmin, xmax, ne in zip(min_coords, max_coords, ncells)] + + # Create 1D finite element spaces and precompute quadrature data + spaces[i] = [SplineSpace( p, multiplicity=m, grid=grid , periodic=P) for p,m,grid,P in zip(degree_i, multiplicity_i,grids, periodic)] + else: + # Create 1D finite element spaces and precompute quadrature data + spaces[i] = [SplineSpace( p, knots=T , periodic=P) for p,T, P in zip(degree_i, knots[interior.name], periodic)] + + + carts = create_cart(ddms, spaces) + g_spaces = {inter : TensorFemSpace(ddms[i], *spaces[i], cart=carts[i], dtype=dtype) for i, inter in enumerate(interiors)} + + + for i,j in connectivity: + ((axis_i, ext_i), (axis_j , ext_j)) = connectivity[i, j] + minus = interiors[i] + plus = interiors[j] + max_ncells = [max(ni,nj) for ni,nj in zip(domain_h.ncells[minus.name],domain_h.ncells[plus.name])] + g_spaces[minus].add_refined_space(ncells=max_ncells) + g_spaces[plus].add_refined_space(ncells=max_ncells) + + # ... construct interface spaces + construct_interface_spaces(domain_h.ddm, g_spaces, carts, interiors, connectivity) + + new_g_spaces = {} + for inter in g_spaces: + Vh = g_spaces[inter] + if isinstance(V, ProductSpace): + spaces = [reduce_space_degrees(Vi, Vh, basis=basis, sequence=sequence) for Vi in V.spaces] + spaces = [Vh.spaces if isinstance(Vh, VectorFemSpace) else Vh for Vh in spaces] + spaces = flatten(spaces) + Vh = VectorFemSpace(*spaces) + else: + Vh = reduce_space_degrees(V, Vh, basis=basis, sequence=sequence) + + Vh.symbolic_space = V + for key in Vh._refined_space: + Vh.get_refined_space(key).symbolic_space = V + + new_g_spaces[inter] = Vh + + construct_reduced_interface_spaces(g_spaces, new_g_spaces, interiors, connectivity) + spaces = list(new_g_spaces.values()) + + if connectivity: + assert all((isinstance(Wh, FemSpace) and not Wh.is_multipatch) for Wh in spaces) + Vh = MultipatchFemSpace(*spaces, connectivity=connectivity) + else: + assert all(isinstance(Wh, FemSpace) for Wh in spaces) + if len(spaces) == 1: + Vh = spaces[0] + else: + Vh = VectorFemSpace(*spaces) + + Vh.symbolic_space = V + + return Vh + + +#============================================================================== +def discretize_domain(domain, *, filename=None, ncells=None, periodic=None, comm=None, mpi_dims_mask=None): + + if comm is not None: + # Create a copy of the communicator + comm = comm.Dup() + + if not (filename or ncells): + raise ValueError("Must provide either 'filename' or 'ncells'") + + elif filename and ncells: + raise ValueError("Cannot provide both 'filename' and 'ncells'") + + elif filename: + return Geometry(filename=filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + + elif ncells: + return Geometry.from_topological_domain(domain, ncells, periodic=periodic, comm=comm, mpi_dims_mask=mpi_dims_mask) + +#============================================================================== +def discretize(a, *args, **kwargs): + + if isinstance(a, (sym_BasicForm, sym_GltExpr, sym_Expr)): + domain_h = args[0] + assert isinstance(domain_h, Geometry) + domain = domain_h.domain + dim = domain.dim + + # The current implementation of the sum factorization algorithm does not support openMP parallelization + # It is not properly tested, whether this way of excluding openMP parallelized code from using the sum factorization algorithm works. + backend = kwargs.get('backend')# or None + assembly_backend = kwargs.get('assembly_backend')# or None + assembly_backend = backend or assembly_backend + openmp = False if assembly_backend is None else assembly_backend.get('openmp') + + # Since all keyword arguments are hidden in kwargs, we cannot set the + # defaults in the function signature. Here we set the defaults for + # sum_factorization using dict.setdefault: we default to True for + # bilinear forms in 3D, and to False otherwise. If the user has chosen + # differently, we let the code run and see what happens! + # + # We also default to False if the code is to be parallelized w/ OpenMP. + # TODO [YG 21.07.2025]: Drop this restriction + if isinstance(a, sym_BilinearForm): + default = (dim == 3 and not openmp) + kwargs.setdefault('sum_factorization', default) + + mapping = domain_h.domain.mapping + kwargs['symbolic_mapping'] = mapping + + #... + # In the case of Equation, BilinearForm, LinearForm, or Functional, we + # need the number of quadrature points along each direction. + # + # If not given, we set `nquads[i] = max_p[i] + 1`, where `max_p[i]` is the + # maximum polynomial degree of the spaces along direction i. + # + # If a scalar integer is passed, we use the same number of quadrature + # points in all directions. + if isinstance(a, (sym_BasicForm, sym_Equation)): + nquads = kwargs.get('nquads', None) + if nquads is None: + spaces = args[1] + if not hasattr(spaces, '__iter__'): + spaces = [spaces] + nquads = [p + 1 for p in get_max_degree(*spaces)] + elif not hasattr(nquads, '__iter__'): + assert isinstance(nquads, int) + domain_h = args[0] + nquads = [nquads] * domain_h.ldim + kwargs['nquads'] = nquads + #... + + if isinstance(a, sym_BasicForm): + if isinstance(a, (sym_Norm, sym_SemiNorm)): + kernel_expr = TerminalExpr(a, domain) + if not mapping is None: + kernel_expr = tuple(LogicalExpr(i, domain) for i in kernel_expr) + else: + if not mapping is None: + a = LogicalExpr (a, domain) + domain = domain.logical_domain + + kernel_expr = TerminalExpr(a, domain) + + if len(kernel_expr) > 1: + return DiscreteSumForm(a, kernel_expr, *args, **kwargs) + + # TODO uncomment when the SesquilinearForm subclass of bilinearForm is create in SymPDE + # if isinstance(a, sym_SesquilinearForm): + # return DiscreteSesquilinearForm(a, kernel_expr, *args, **kwargs) + + if isinstance(a, sym_BilinearForm): + if kwargs.pop('sum_factorization'): + return DiscreteBilinearForm_SF(a, kernel_expr, *args, **kwargs) + else: + return DiscreteBilinearForm(a, kernel_expr, *args, **kwargs) + + elif isinstance(a, sym_LinearForm): + return DiscreteLinearForm(a, kernel_expr, *args, **kwargs) + + elif isinstance(a, sym_Functional): + return DiscreteFunctional(a, kernel_expr, *args, **kwargs) + + elif isinstance(a, sym_Equation): + return DiscreteEquation(a, *args, **kwargs) + + elif isinstance(a, BasicFunctionSpace): + return discretize_space(a, *args, **kwargs) + + 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: + return discretize_derham_multipatch(a, *args, **kwargs) + + elif isinstance(a, Domain): + return discretize_domain(a, *args, **kwargs) + + elif isinstance(a, sym_GltExpr): + return DiscreteGltExpr(a, *args, **kwargs) + + elif isinstance(a, sym_Expr): + return DiscreteExpr(a, *args, **kwargs) + + else: + raise NotImplementedError('given {}'.format(type(a))) diff --git a/psydac/api/equation.py b/psydac/api/equation.py new file mode 100644 index 000000000..deba2d081 --- /dev/null +++ b/psydac/api/equation.py @@ -0,0 +1,257 @@ +from collections import namedtuple + +from sympde.topology import ScalarFunction +from sympde.topology import ProductSpace +from sympde.topology import element_of +from sympde.calculus import dot +from sympde.expr import integral +from sympde.expr import Integral +from sympde.expr import find +from sympde.expr import Equation + +from psydac.api.basic import BasicDiscrete +from psydac.api.essential_bc import apply_essential_bc +from psydac.fem.basic import FemField +from psydac.linalg.solvers import inverse + +__all__ = ('l2_boundary_projection', 'DiscreteEquation') + +#============================================================================== +LinearSystem = namedtuple('LinearSystem', ['lhs', 'rhs']) + +#============================================================================== +_default_solver = {'solver':'cg', 'tol':1e-9, 'maxiter':3000, 'verbose':False} + +def l2_boundary_projection(equation): + """ + Create an auxiliary equation (weak formulation) that solves for the + boundary trace of the solution by performing an L2 projection of + inhomogeneous essential boundary conditions. + + Return None if no inhomogeneous essential BCs are imposed on the solution. + + Parameters + ---------- + equation : sympde.expr.Equation + Weak formulation of PDE of interest, which may have essential BCs. + + Returns + ------- + eqn_bc : sympde.expr.Equation + Weak formulation that performs L2 projection of inhomogeneous essential + boundary conditions onto the trial space. None if not needed. + + """ + if not isinstance(equation, Equation): + raise TypeError('> Expecting a symbolic Equation') + + if not equation.bc: + return None + + # Inhomogeneous Dirichlet boundary conditions + idbcs = [i for i in equation.bc if i.rhs != 0] + + if not idbcs: + return None + + # Extract trial functions from model equation + u = equation.trial_functions + + # Create test functions in same space of trial functions + # TODO: check if we should generate random names + V = ProductSpace(*[ui.space for ui in u]) + v = element_of(V, name='v:{}'.format(len(u))) + + # In a system, each essential boundary condition is applied to + # only one component (bc.variable) of the state vector. Hence + # we will select the correct test function using a dictionary. + test_dict = dict(zip(u, v)) + + # Compute product of (u, v) using dot product for vector quantities + product = lambda f, g: (f * g if g.atoms(ScalarFunction) else dot(f, g)) + + # Construct variational formulation that performs L2 projection + # of boundary conditions onto the correct space + factor = lambda bc : bc.lhs.xreplace(test_dict) + lhs_expr = sum(integral(i.boundary, product(i.lhs, factor(i))) for i in idbcs) + rhs_expr = sum(integral(i.boundary, product(i.rhs, factor(i))) for i in idbcs) + eqn_bc = find(u, forall=v, lhs=lhs_expr, rhs=rhs_expr) + + return eqn_bc + +#============================================================================== +class DiscreteEquation(BasicDiscrete): + + def __init__(self, expr, *args, **kwargs): + if not isinstance(expr, Equation): + raise TypeError('> Expecting a symbolic Equation') + + # Warning: circular dependency + from psydac.api.discretization import discretize + + # ... + bc = expr.bc + # ... + + self._expr = expr + # since lhs and rhs are calls, we need to take their expr + + # ... + domain = args[0] + trial_test = args[1] + trial_space = trial_test[0] + test_space = trial_test[1] + # ... + + self._lhs = discretize(expr.lhs, domain, trial_test, **kwargs) + # ... + # sum_factorization (sum factorization algorithm) is currently not supported for LinearForms (the rhs) + if 'sum_factorization' in kwargs: + kwargs.pop('sum_factorization') + self._rhs = discretize(expr.rhs, domain, test_space, **kwargs) + # ... + + # ... + # Create boundary equation (None if not needed) + eqn_bc = l2_boundary_projection(expr) + eqn_bc_h = DiscreteEquation(eqn_bc, domain, [trial_space, trial_space], **kwargs) \ + if eqn_bc else None + # ... + + self._bc = bc + self._linear_system = None + self._domain = domain + self._trial_space = trial_space + self._test_space = test_space + self._boundary_equation = eqn_bc_h + self._solver_parameters = _default_solver.copy() + + @property + def expr(self): + return self._expr + + @property + def lhs(self): + return self._lhs + + @property + def rhs(self): + return self._rhs + + @property + def domain(self): + return self._domain + + @property + def trial_space(self): + return self._trial_space + + @property + def test_space(self): + return self._test_space + + @property + def bc(self): + return self._bc + + @property + def linear_system(self): + return self._linear_system + + @property + def boundary_equation(self): + return self._boundary_equation + + def set_solver(self, solver, **kwargs): + self._solver_parameters.update(solver=solver, **kwargs) + + def get_solver(self): + return self._solver_parameters + + #-------------------------------------------------------------------------- + def assemble(self, **kwargs): + + # Decide if we should assemble + assemble_lhs = not self.linear_system or self.lhs.free_args + assemble_rhs = not self.linear_system or self.rhs.free_args + + # Matrix (left-hand side) + if assemble_lhs: + A = self.lhs.assemble(reset=True, **kwargs) + if self.bc: + apply_essential_bc(A, *self.bc) + else: + A = self.linear_system.lhs + + # Vector (right-hand side) + if assemble_rhs: + b = self.rhs.assemble(reset=True, **kwargs) + if self.bc: + apply_essential_bc(b, *self.bc) + else: + b = self.linear_system.rhs + + # Store linear system + self._linear_system = LinearSystem(A, b) + + #-------------------------------------------------------------------------- + def solve(self, **kwargs): + + self.assemble(**kwargs) + + # Free arguments of current equation + free_args = set(self.lhs.free_args + self.rhs.free_args) + + # Free arguments of boundary equation + if self.boundary_equation: + bc_eq = self.boundary_equation + free_args_bc = set(bc_eq.lhs.free_args + bc_eq.rhs.free_args) + else: + free_args_bc = set() + + #---------------------------------------------------------------------- + # [YG, 18/11/2019] + # + # Impose inhomogeneous Dirichlet boundary conditions through + # L2 projection on the boundary. This requires setting up a + # new variational formulation and solving the resulting linear + # system to obtain a solution that does not live in the space + # of homogeneous solutions. Such a solution is then used as + # initial guess when the model equation is to be solved by an + # iterative method. Our current method of solution does not + # modify the initial guess at the boundary. + + settings = self.get_solver() + if self.boundary_equation: + + # Find inhomogeneous solution (use CG as system is symmetric) + self.boundary_equation.set_solver('cg', info=False) + uh = self.boundary_equation.solve(**kwargs) + + # Use inhomogeneous solution as initial guess to solver + settings['x0'] = uh.coeffs + #---------------------------------------------------------------------- + L = self.linear_system + M = L.lhs + rhs = L.rhs + + solver = settings.get('solver') + solver_settings = settings.copy() + solver_settings.pop('solver') + + if 'info' in settings: + inf = settings.get('info') + solver_settings.pop('info') + else: + inf = False + + M_inv = inverse(M, solver, **solver_settings) + if inf == True: + X = M_inv @ rhs + uh = FemField(self.trial_space, coeffs=X) + info = M_inv.get_info() + return uh, info + else: + X = M_inv @ rhs + uh = FemField(self.trial_space, coeffs=X) + return uh diff --git a/psydac/api/feec.py b/psydac/api/feec.py new file mode 100644 index 000000000..2bd95ffbd --- /dev/null +++ b/psydac/api/feec.py @@ -0,0 +1,608 @@ +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.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.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 +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 + +__all__ = ('DiscreteDeRham', 'DiscreteDeRhamMultipatch',) + +#============================================================================== +class DiscreteDeRham(BasicDiscrete): + """ A discrete de Rham sequence built over a single-patch geometry. + + Parameters + ---------- + domain_h : Geometry + The discretized domain, which is a single-patch geometry. + + *spaces : list of FemSpace + The discrete spaces of the de Rham sequence. + + Notes + ----- + - This constructor should not be called directly, but rather from the + `discretize_derham` function in `psydac.api.discretization`. + """ + def __init__(self, domain_h, *spaces): + + assert all(isinstance(space, FemSpace) for space in spaces) + + self.has_vec = isinstance(spaces[-1], VectorFemSpace) + + if self.has_vec : + dim = len(spaces) - 2 + self._spaces = spaces[:-1] + self._H1vec = spaces[-1] + + 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 = domain_h.domain.mapping + self._callable_mapping = self._mapping.get_callable_mapping() if self._mapping else None + + if dim == 1: + D0 = Derivative1D(spaces[0], spaces[1]) + + spaces[0].diff = spaces[0].grad = D0 + + self._derivatives = (D0,) + + elif dim == 2: + kind = spaces[1].symbolic_space.kind.name + + if kind == 'hcurl': + + 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 + + self._derivatives = (D0, D1) + + elif kind == 'hdiv': + + 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 + + self._derivatives = (D0, D1) + + + elif dim == 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 + 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): + """First space of the de Rham sequence : H1 space""" + return self._spaces[0] + + @property + def V1(self): + """Second space of the de Rham sequence : + - 1d : L2 space + - 2d : either Hdiv or Hcurl space + - 3d : Hcurl space""" + return self._spaces[1] + + @property + def V2(self): + """Third space of the de Rham sequence : + - 2d : L2 space + - 3d : Hdiv space""" + return self._spaces[2] + + @property + 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, + where N is the dimension of the (logical) domain.""" + assert self.has_vec + return self._H1vec + + @property + def mapping(self): + """The mapping from the logical space to the physical space.""" + return self._mapping + + @property + def callable_mapping(self): + """The mapping as a callable.""" + return self._callable_mapping + + #-------------------------------------------------------------------------- + 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. + + 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). + + nquads : list(int) | tuple(int) + Number of quadrature points along each direction, to be used in Gauss + quadrature rule for computing the (approximated) degrees of freedom. + + Returns + ------- + 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 + """ + + if not (kind == 'global'): + raise NotImplementedError('only global projectors are available') + + if nquads is None: + nquads = [p + 1 for p in self.V0.degree] + elif isinstance(nquads, int): + nquads = [nquads] * self.dim + else: + assert hasattr(nquads, '__iter__') + nquads = list(nquads) + + assert all(isinstance(nq, int) for nq in nquads) + assert all(nq >= 1 for nq in nquads) + + if self.dim == 1: + 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)) + return P0_m, P1_m + return P0, P1 + + elif self.dim == 2: + P0 = GlobalGeometricProjectorH1(self.V0) + P2 = GlobalGeometricProjectorL2(self.V2, nquads) + + kind = self.V1.symbolic_space.kind.name + if kind == 'hcurl': + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) + elif kind == 'hdiv': + P1 = GlobalGeometricProjectorHdiv(self.V1, nquads) + else: + raise TypeError('projector of space type {} is not available'.format(kind)) + + if self.has_vec : + Pvec = GlobalGeometricProjectorH1vec(self.H1vec, nquads) + + if self.mapping: + P0_m = lambda f: P0(pull_2d_h1(f, self.callable_mapping)) + P2_m = lambda f: P2(pull_2d_l2(f, self.callable_mapping)) + if kind == 'hcurl': + P1_m = lambda f: P1(pull_2d_hcurl(f, self.callable_mapping)) + elif kind == 'hdiv': + P1_m = lambda f: P1(pull_2d_hdiv(f, self.callable_mapping)) + if self.has_vec : + Pvec_m = lambda f: Pvec(pull_2d_h1vec(f, self.callable_mapping)) + return P0_m, P1_m, P2_m, Pvec_m + else : + return P0_m, P1_m, P2_m + + if self.has_vec : + return P0, P1, P2, Pvec + else : + return P0, P1, P2 + + elif self.dim == 3: + P0 = GlobalGeometricProjectorH1 (self.V0) + P1 = GlobalGeometricProjectorHcurl(self.V1, nquads) + P2 = GlobalGeometricProjectorHdiv (self.V2, nquads) + P3 = GlobalGeometricProjectorL2 (self.V3, nquads) + if self.has_vec : + 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)) + P2_m = lambda f: P2(pull_3d_hdiv (f, self.callable_mapping)) + P3_m = lambda f: P3(pull_3d_l2 (f, self.callable_mapping)) + if self.has_vec : + Pvec_m = lambda f: Pvec(pull_3d_h1vec(f, self.callable_mapping)) + return P0_m, P1_m, P2_m, P3_m, Pvec_m + else : + return P0_m, P1_m, P2_m, P3_m + + if self.has_vec : + return P0, P1, P2, P3, Pvec + else : + return P0, P1, P2, P3 + + #-------------------------------------------------------------------------- + def derivatives(self, kind='femlinop'): + if kind == 'femlinop': + return self._derivatives + elif kind == 'linop': + return tuple(b_diff.linop for b_diff in self._derivatives) + + #-------------------------------------------------------------------------- + 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 + + Parameters + ---------- + + p_moments : + The number of moments preserved by the projector. + + hom_bc: + Apply homogenous boundary conditions if True + + kind : + The kind of the projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + Returns + ------- + cP0, cP1, cP2 : Tuple of or + 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') + + if self.dim == 1: + raise NotImplementedError("1D projectors are not available") + + elif self.dim == 2: + if self.sequence[1] != 'hcurl': + raise NotImplementedError('2D sequence with H-div not available yet') + + else: + + 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) + + 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") + + #-------------------------------------------------------------------------- + def _init_hodge_operators(self, backend_language='python'): + """ + Initialize the Hodge operator for the multipatch de Rham sequence. + + Parameters + ---------- + + backend_language: + The backend used to accelerate the code + + """ + if not self._hodge_operators: + + if self.dim == 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) + 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) + 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) + + #-------------------------------------------------------------------------- + def _get_hodge_operator(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 projector, can be 'femlinop' or 'linop'. + - 'femlinop' returns a psydac FemLinearOperator (default) + - 'linop' returns a psydac LinearOperator + + Returns + ------- + Hodge operator in the specified form. + """ + + if not dual: + if kind == 'femlinop': + return H.hodge + elif kind == 'linop': + return H.linop + else: + if kind == 'femlinop': + return H.dual_hodge + elif kind == 'linop': + return H.dual_linop + + #-------------------------------------------------------------------------- + def hodge_operator(self, space=None, dual=False, kind='femlinop', backend_language='python'): + """ + 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 : + 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'. + + Returns + ------- + The Hodge operator of the space of the specified kind. + + H : or + """ + + if not self._hodge_operators: + self._init_hodge_operators(backend_language=backend_language) + + if space == 'V0': + 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) + + elif space == 'V2': + 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) + + #-------------------------------------------------------------------------- + def hodge_operators(self, dual=False, kind='femlinop', backend_language='python'): + """ + 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'. + + 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) + + return tuple(self._get_hodge_operator(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 + ---------- + domain_h: + The discrete domain + + spaces: + The discrete spaces that are contained in the de Rham sequence + """ + + def __init__(self, *, domain_h, spaces): + + dim = len(spaces) - 1 + self._spaces = tuple(spaces) + self._dim = dim + 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) + + + if dim == 1: + raise NotImplementedError('1D FEEC multipatch non available yet') + + elif dim == 2: + + if self._sequence[1] == 'hcurl': + + self._derivatives = ( + BrokenGradient2D(self.V0, self.V1), + BrokenScalarCurl2D(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.') + + #-------------------------------------------------------------------------- + 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 = MultipatchGeometricProjector(self.V0, GlobalGeometricProjectorH1) + + if self.sequence[1] == 'hcurl': + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHcurl, nquads=nquads) + else: + P1 = MultipatchGeometricProjector(self.V1, GlobalGeometricProjectorHdiv, nquads=nquads) + + 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': + 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: + raise NotImplementedError("3D projectors are not available") diff --git a/psydac/api/fem.py b/psydac/api/fem.py new file mode 100644 index 000000000..e8cfcd4d1 --- /dev/null +++ b/psydac/api/fem.py @@ -0,0 +1,1544 @@ +# coding: utf-8 + +# TODO: - init_fem is called whenever we call discretize. we should check that +# nderiv has not been changed. shall we add nquads too? + +import numpy as np +from sympy import ImmutableDenseMatrix, Matrix + +from sympde.expr import BilinearForm as sym_BilinearForm +from sympde.expr import LinearForm as sym_LinearForm +from sympde.expr import Functional as sym_Functional +from sympde.expr import Norm as sym_Norm +from sympde.expr import SemiNorm as sym_SemiNorm +from sympde.topology import Boundary, Interface +from sympde.calculus.core import PlusInterfaceOperator + +from psydac.linalg.stencil import StencilVector, StencilMatrix, StencilInterfaceMatrix +from psydac.linalg.basic import ComposedLinearOperator +from psydac.linalg.block import BlockVectorSpace, BlockVector, BlockLinearOperator +from psydac.cad.geometry import Geometry +from psydac.mapping.discrete import NurbsMapping +from psydac.fem.vector import VectorFemSpace +from psydac.fem.basic import FemField +from psydac.fem.projectors import knot_insertion_projection_operator +from psydac.core.bsplines import find_span, basis_funs_all_ders +from psydac.ddm.cart import InterfaceCartDecomposition +from psydac.api.basic import BasicDiscrete +from psydac.api.grid import QuadratureGrid, BasisValues +from psydac.api.utilities import flatten, random_string +from psydac.api.fem_common import ( + collect_spaces, + construct_test_space_arguments, + construct_trial_space_arguments, + construct_quad_grids_arguments, + reset_arrays, + do_nothing, + extract_stencil_mats, +) + +__all__ = ( + 'DiscreteBilinearForm', + 'DiscreteFunctional', + 'DiscreteLinearForm', +) + +#============================================================================== +class DiscreteBilinearForm(BasicDiscrete): + """ + Discrete bilinear form ready to be assembled into a matrix. + + This class represents the concept of a discrete bilinear form in Psydac. + Instances of this class generate an appropriate matrix assembly kernel, + allocate the matrix if not provided, and prepare a list of arguments for + the kernel. + + Parameters + ---------- + + expr : sympde.expr.expr.BilinearForm + The symbolic bilinear form. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the bilinear form. + + domain_h : Geometry + The discretized domain. + + spaces : list of FemSpace + The discrete trial and test spaces. + + nquads : list or tuple of int + The number of quadrature points used in the assembly kernel along each + direction. + + matrix : StencilMatrix or BlockLinearOperator, optional + The matrix that we assemble into. If not provided, a new matrix is + created with the appropriate domain and codomain (default: None). + + update_ghost_regions : bool, default=True + Accumulate the contributions of the neighbouring processes. + + backend : dict, optional + The backend used to accelerate the computing kernels. + The backend dictionaries are defined in the file psydac/api/settings.py + + assembly_backend : dict, optional + The backend used to accelerate the assembly kernel. + The backend dictionaries are defined in the file psydac/api/settings.py + + linalg_backend : dict, optional + The backend used to accelerate the computing kernels of the linear operator. + The backend dictionaries are defined in the file psydac/api/settings.py + + symbolic_mapping : Sympde.topology.Mapping, optional + The symbolic mapping which defines the physical domain of the bilinear form. + + See Also + -------- + DiscreteLinearForm + DiscreteFunctional + DiscreteSumForm + + """ + def __init__(self, expr, kernel_expr, domain_h, spaces, *, nquads, + matrix=None, update_ghost_regions=True, backend=None, + linalg_backend=None, assembly_backend=None, + symbolic_mapping=None): + + if not isinstance(expr, sym_BilinearForm): + raise TypeError('> Expecting a symbolic BilinearForm') + + assert isinstance(domain_h, Geometry) + + self._spaces = spaces + + if isinstance(kernel_expr, (tuple, list)): + if len(kernel_expr) == 1: + kernel_expr = kernel_expr[0] + else: + raise ValueError('> Expecting only one kernel') + + self._kernel_expr = kernel_expr + self._target = kernel_expr.target + self._domain = domain_h.domain + self._matrix = matrix + + domain = self.domain + target = self.target + + # ... + if len(domain) > 1: + i, j = self.get_space_indices_from_target(domain, target) + test_space = self.spaces[1].spaces[i] + trial_space = self.spaces[0].spaces[j] + if isinstance(target, Interface): + m,_ = self.get_space_indices_from_target(domain, target.minus) + p,_ = self.get_space_indices_from_target(domain, target.plus) + mapping_m = list(domain_h.mappings.values())[m] + mapping_p = list(domain_h.mappings.values())[p] + mapping = (mapping_m, mapping_p) if mapping_m else None + else: + mapping = list(domain_h.mappings.values())[i] + else: + trial_space = self.spaces[0] + test_space = self.spaces[1] + mapping = list(domain_h.mappings.values())[0] + + self._mapping = mapping + + is_rational_mapping = False + mapping_space = None + if (mapping is not None) and not isinstance(target, Interface): + is_rational_mapping = isinstance(mapping, NurbsMapping) + mapping_space = mapping.space + elif (mapping is not None) and isinstance(target, Interface): + is_rational_mapping = (isinstance(mapping[0], NurbsMapping), isinstance(mapping[1], NurbsMapping)) + mapping_space = (mapping[0].space, mapping[1].space) + + self._is_rational_mapping = is_rational_mapping + # ... + + if isinstance(test_space.coeff_space, BlockVectorSpace): + coeff_space = test_space.coeff_space.spaces[0] + else: + coeff_space = test_space.coeff_space + + self._coeff_space = coeff_space + self._num_threads = 1 + if coeff_space.parallel and coeff_space.cart.num_threads>1: + self._num_threads = coeff_space.cart.num_threads + + self._update_ghost_regions = update_ghost_regions + + # In case of multiple patches, if the communicator is MPI_COMM_NULL, we do not generate the assembly code + # because the patch is not owned by the MPI rank. + if coeff_space.parallel and coeff_space.cart.is_comm_null: + self._free_args = () + self._func = do_nothing + self._args = () + self._element_loop_starts = () + self._element_loop_ends = () + self._global_matrices = () + self._threads_args = () + self._update_ghost_regions = False + return + + # ... + test_ext = None + trial_ext = None + if isinstance(target, Boundary): + axis = target.axis + test_ext = target.ext + trial_ext = target.ext + elif isinstance(target, Interface): + # this part treats the cases of: + # integral(v_minus * u_plus) + # integral(v_plus * u_minus) + # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus) + # are converted to boundary integrals by Sympde + axis = target.axis + test = self.kernel_expr.test + trial = self.kernel_expr.trial + test_target = target.plus if isinstance( test, PlusInterfaceOperator) else target.minus + trial_target = target.plus if isinstance(trial, PlusInterfaceOperator) else target.minus + test_ext = test_target.ext + trial_ext = trial_target.ext + ncells = tuple(max(i, j) for i, j in zip(test_space.ncells, trial_space.ncells)) + if isinstance(trial_space, VectorFemSpace): + spaces = [] + for sp in trial_space.spaces: + if (trial_target.axis, trial_target.ext) in sp.interfaces: + spaces.append(sp.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext]) + + if len(spaces) == len(trial_space.spaces): + sym_space = trial_space.symbolic_space + trial_space = VectorFemSpace(*spaces) + trial_space.symbolic_space = sym_space + + elif (trial_target.axis, trial_target.ext) in trial_space.interfaces: + sym_space = trial_space.symbolic_space + trial_space = trial_space.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext] + trial_space.symbolic_space = sym_space + + test_space = test_space.get_refined_space(ncells) + self._test_ext = test_target.ext + self._trial_ext = trial_target.ext + + #... + discrete_space = (trial_space, test_space) + + # Assuming that all vector spaces (and their Cartesian decomposition, + # if any) are compatible with each other, extract the first available + # vector space from which (starts, ends, npts) will be read: + starts = coeff_space.starts + ends = coeff_space.ends + npts = coeff_space.npts + + # MPI communicator + comm = coeff_space.cart.comm if coeff_space.parallel else None + + # Backends for code generation + assembly_backend = backend or assembly_backend + linalg_backend = backend or linalg_backend + + # BasicDiscrete generates the assembly code and sets the following attributes that are used afterwards: + # self._func, self._free_args, self._max_nderiv and self._backend + BasicDiscrete.__init__(self, expr, kernel_expr, comm=comm, root=0, discrete_space=discrete_space, + nquads=nquads, is_rational_mapping=is_rational_mapping, mapping=symbolic_mapping, + mapping_space=mapping_space, num_threads=self._num_threads, backend=assembly_backend) + + #... Handle the special case where the current MPI process does not need to do anything + if isinstance(target, (Boundary, Interface)): + + # If process does not own the boundary or interface, do not assemble anything + if test_ext == -1: + if starts[axis] != 0: + self._func = do_nothing + + elif test_ext == 1: + if ends[axis] != npts[axis]-1: + self._func = do_nothing + + # In case of target==Interface, we only use the MPI ranks that are on the interface to assemble the BilinearForm + if self._func == do_nothing and isinstance(target, Interface): + self._free_args = () + self._args = () + self._element_loop_starts = () + self._element_loop_ends = () + self._global_matrices = () + self._threads_args = () + return + #... + + #... Build the quadrature grids + if isinstance(target, Boundary): + test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads) + self._grid = (test_grid,) + elif isinstance(target, Interface): + # this part treats the cases of: + # integral(v_minus * u_plus) + # integral(v_plus * u_minus) + # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus) + # are converted to boundary integrals by Sympde + test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads) + self._grid = (test_grid, trial_grid) if test_target == target.minus else (trial_grid, test_grid) + self._test_ext = test_target.ext + self._trial_ext = trial_target.ext + else: + test_grid = QuadratureGrid( test_space, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, nquads=nquads) + self._grid = (test_grid,) + #... + + # Extract the basis function values on the quadrature grids + self._test_basis = BasisValues( + test_space, + nderiv = self.max_nderiv, + nquads = nquads, + trial = False, + grid = test_grid + ) + self._trial_basis = BasisValues( + trial_space, + nderiv = self.max_nderiv, + nquads = nquads, + trial = True , + grid = trial_grid + ) + + # Allocate the output matrix, if needed + self.allocate_matrices(linalg_backend) + + # Determine whether OpenMP instructions were generated + with_openmp = (assembly_backend['name'] == 'pyccel' and assembly_backend['openmp']) if assembly_backend else False + + # Construct the arguments to be passed to the assemble() function, which is stored in self._func + self._args, self._threads_args = self.construct_arguments(with_openmp) + + @property + def domain(self): + return self._domain + + @property + def target(self): + return self._target + + @property + def spaces(self): + return self._spaces + + @property + def grid(self): + return self._grid + + @property + def nquads(self): + return self._grid[0].nquads + + @property + def test_basis(self): + return self._test_basis + + @property + def trial_basis(self): + return self._trial_basis + + @property + def global_matrices(self): + return self._global_matrices + + @property + def args(self): + return self._args + + def assemble(self, *, reset=True, **kwargs): + """ + This method assembles the left hand side Matrix by calling the private method `self._func` with proper arguments. + + In the complex case, this function returns the matrix conjugate. This comes from the fact that the + problem `a(u,v)=b(v)` is discretized as `A @ conj(U) = B` due to the antilinearity of `a` in the first variable. + Thus, to obtain `U`, the assemble function returns `conj(A)`. + + TODO: remove these lines when the dot product is changed for complex. + For now, since the dot product does not compute the conjugate in the complex case. We do not use the conjugate in the assemble function. + It should work if the complex only comes from the `rhs` in the linear form. + """ + + if self._free_args: + basis = [] + spans = [] + degrees = [] + pads = [] + coeffs = [] + consts = [] + + for key in self._free_args: + v = kwargs[key] + + if len(self.domain) > 1 and isinstance(v, FemField) and (v.space.is_multipatch or v.space.is_vector_valued): + assert v.space.is_multipatch ## [MCP 27.03.2025] should hold since len(domain) > 1. If Ok we can simplify above if + i, j = self.get_space_indices_from_target(self.domain, self.target) + assert i == j + v = v[i] + if isinstance(v, FemField): + assert len(self.grid) == 1 + if not v.coeffs.ghost_regions_in_sync: + v.coeffs.update_ghost_regions() + basis_v = BasisValues( + v.space, + nderiv = self.max_nderiv, + nquads = self.nquads, + trial = True, + grid = self.grid[0] + ) + bs, d, s, p, mult = construct_test_space_arguments(basis_v) + basis += bs + spans += s + degrees += [np.int64(a) for a in d] + pads += [np.int64(a) for a in p] + if v.space.is_multipatch or v.space.is_vector_valued: + coeffs += (e._data for e in v.coeffs) + else: + coeffs += (v.coeffs._data, ) + else: + consts += (v, ) + + args = (*self.args, *basis, *spans, *degrees, *pads, *coeffs, *consts) + + else: + args = self._args + + if reset: + reset_arrays(*self.global_matrices) + + self._func(*args, *self._threads_args) + if self._matrix and self._update_ghost_regions: + self._matrix.exchange_assembly_data() + + # TODO : uncomment this line when the conjugate is applied on the dot product in the complex case + #self._matrix.conjugate(out=self._matrix) + + if self._matrix: + self._matrix.ghost_regions_in_sync = False + + return self._matrix + + def get_space_indices_from_target(self, domain, target): + if domain.mapping: + domain = domain.logical_domain + if target.mapping: + target = target.logical_domain + domains = domain.interior.args + if isinstance(target, Interface): + test = self.kernel_expr.test + trial = self.kernel_expr.trial + test_target = target.plus if isinstance( test, PlusInterfaceOperator) else target.minus + trial_target = target.plus if isinstance(trial, PlusInterfaceOperator) else target.minus + i, j = [domains.index(test_target.domain), domains.index(trial_target.domain)] + else: + if isinstance(target, Boundary): + i = domains.index(target.domain) + j = i + else: + i = domains.index(target) + j = i + return i, j + + def construct_arguments(self, with_openmp=False): + """ + Collect the arguments used in the assembly method. + + Parameters + ---------- + with_openmp : bool + If set to True we collect some extra arguments used in the assembly method + + Returns + ------- + + args: tuple + The arguments passed to the assembly method. + + threads_args: tuple + Extra arguments used in the assembly method in case with_openmp=True. + + """ + test_basis, test_degrees, spans, pads, mult = construct_test_space_arguments(self.test_basis) + trial_basis, trial_degrees, pads, mult = construct_trial_space_arguments(self.trial_basis) + n_elements, quads, quad_degrees = construct_quad_grids_arguments(self.grid[0], use_weights=False) + if len(self.grid)>1: + quads = [*quads, *self.grid[1].points] + + pads = self.test_basis.space.coeff_space.pads + + # When self._target is an Interface domain len(self._grid) == 2 + # where grid contains the QuadratureGrid of both sides of the interface + if self.mapping: + + if len(self.grid) == 1: + map_coeffs = [[e._coeffs._data for e in self.mapping._fields]] + spaces = [self.mapping._fields[0].space] + map_degree = [sp.degree for sp in spaces] + map_span = [[q.spans - s for q,s in zip(sp.get_assembly_grids(*self.nquads), sp.coeff_space.starts)] for sp in spaces] + map_basis = [[q.basis for q in sp.get_assembly_grids(*self.nquads)] for sp in spaces] + points = [g.points for g in self.grid] + weights = [self.mapping.weights_field.coeffs._data] if self.is_rational_mapping else [] + + elif len(self.grid) == 2: + target = self.kernel_expr.target + assert isinstance(target, Interface) + mappings = list(self.mapping) + i, j = self.get_space_indices_from_target(self.domain, target) + m, _ = self.get_space_indices_from_target(self.domain, target.minus) + p,_ = self.get_space_indices_from_target(self.domain, target.plus) + + map_coeffs = [[e._coeffs for e in mapping._fields] for mapping in self.mapping] + spaces = [mapping._fields[0].space for mapping in self.mapping] + weights_m = [mappings[0].weights_field.coeffs] if self.is_rational_mapping[0] else [] + weights_p = [mappings[1].weights_field.coeffs] if self.is_rational_mapping[1] else [] + if m == j: + axis = target.minus.axis + ext = target.minus.ext + + spaces[0] = spaces[0].interfaces[axis, ext] + map_coeffs[0] = [coeff._interface_data[axis, ext] for coeff in map_coeffs[0]] + map_coeffs[1] = [coeff._data for coeff in map_coeffs[1]] + if weights_m: + weights_m[0] = weights_m[0]._interface_data[axis, ext] + if weights_p: + weights_p[0] = weights_p[0]._data + elif p == j: + axis = target.plus.axis + ext = target.plus.ext + + spaces[1] = spaces[1].interfaces[axis, ext] + map_coeffs[0] = [coeff._data for coeff in map_coeffs[0]] + map_coeffs[1] = [coeff._interface_data[axis, ext] for coeff in map_coeffs[1]] + if weights_m: + weights_m[0] = weights_m[0]._data + if weights_p: + weights_p[0] = weights_p[0]._interface_data[axis, ext] + + map_degree = [sp.degree for sp in spaces] + map_span = [[q.spans - s for q, s in zip(sp.get_assembly_grids(*self.nquads), sp.coeff_space.starts)] for sp in spaces] + map_basis = [[q.basis for q in sp.get_assembly_grids(*self.nquads)] for sp in spaces] + points = [g.points for g in self.grid] + + nderiv = self.max_nderiv + for i in range(len(self.grid)): + axis = self.grid[i].axis + ext = self.grid[i].ext + if axis is None:continue + space = spaces[i].spaces[axis] + points_i = points[i][axis] + local_span = find_span(space.knots, space.degree, points_i[0, 0]) + boundary_basis = basis_funs_all_ders(space.knots, space.degree, + points_i[0, 0], local_span, nderiv, space.basis) + map_basis[i][axis] = map_basis[i][axis].copy() + map_basis[i][axis][0, :, 0:nderiv+1, 0] = np.transpose(boundary_basis) + if ext == 1: + map_span[i][axis] = map_span[i][axis].copy() + map_span[i][axis][0] = map_span[i][axis][-1] + + map_degree = flatten(map_degree) + map_span = flatten(map_span) + map_basis = flatten(map_basis) + points = flatten(points) + if len(self.grid) == 1: + mapping = [*map_coeffs[0], *weights] + elif len(self.grid)==2: + mapping = [*map_coeffs[0], *weights_m, *map_coeffs[1], *weights_p] + else: + mapping = [] + map_degree = [] + map_span = [] + map_basis = [] + + args = (*test_basis, *trial_basis, *map_basis, *spans, *map_span, *quads, *test_degrees, *trial_degrees, *map_degree, + *n_elements, *quad_degrees, *pads, *mapping, *self._global_matrices) + + with_openmp = with_openmp and self._num_threads>1 + + threads_args = () + if with_openmp: + threads_args = self._coeff_space.cart.get_shared_memory_subdivision(n_elements) + threads_args = (threads_args[0], threads_args[1], *threads_args[2], *threads_args[3], threads_args[4]) + + args = tuple(np.int64(a) if isinstance(a, int) else a for a in args) + threads_args = tuple(np.int64(a) if isinstance(a, int) else a for a in threads_args) + + return args, threads_args + + def allocate_matrices(self, backend=None): + """ + Allocate the global matrices used in the assembly method. + In this method we allocate only the matrices that are computed in the self._target domain, + we also avoid double allocation if we have many DiscreteLinearForm that are defined on the same self._target domain. + + Parameters + ---------- + backend : dict + The backend used to accelerate the computing kernels. + + """ + global_mats = {} + + expr = self.kernel_expr.expr + target = self.kernel_expr.target + test_degree = np.array(self.test_basis.space.degree) + trial_degree = np.array(self.trial_basis.space.degree) + test_space = self.spaces[1].coeff_space + trial_space = self.spaces[0].coeff_space + test_fem_space = self.spaces[1] + trial_fem_space = self.spaces[0] + domain = self.domain + is_broken = len(domain) > 1 + is_conformal = True + + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): + if not isinstance(test_degree[0],(list, tuple, np.ndarray)): + test_degree = [test_degree] + + if not isinstance(trial_degree[0],(list, tuple, np.ndarray)): + trial_degree = [trial_degree] + + pads = np.empty((len(test_degree),len(trial_degree),len(test_degree[0])), dtype=int) + for i in range(len(test_degree)): + for j in range(len(trial_degree)): + td = test_degree[i] + trd = trial_degree[j] + pads[i,j][:] = np.array([td, trd]).max(axis=0) + else: + pads = np.maximum(test_degree, trial_degree) + + if self._matrix is None and (is_broken or isinstance(expr, (ImmutableDenseMatrix, Matrix))): + self._matrix = BlockLinearOperator(trial_space, test_space) + + if is_broken: + i, j = self.get_space_indices_from_target(domain, target) + test_fem_space = self.spaces[1].spaces[i] + trial_fem_space = self.spaces[0].spaces[j] + test_space = test_space.spaces[i] + trial_space = trial_space.spaces[j] + ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells)) + is_conformal = tuple(test_fem_space.ncells) == ncells and tuple(trial_fem_space.ncells) == ncells + if is_broken and not is_conformal and not i==j: + use_restriction = all(trn>=tn for trn,tn in zip(trial_fem_space.ncells, test_fem_space.ncells)) + use_prolongation = not use_restriction + + else: + ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells)) + i=0 + j=0 + #else so initialisation causing bug on line 682 + + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # case of system of equations + + if is_broken: #multi patch + if not self._matrix[i,j]: + mat = BlockLinearOperator(trial_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.get_refined_space(ncells).coeff_space) + if not is_conformal and not i==j: + if use_restriction: + Ps = [knot_insertion_projection_operator(ts.get_refined_space(ncells), ts) for ts in test_fem_space.spaces] + P = BlockLinearOperator(test_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.coeff_space) + for ni,Pi in enumerate(Ps): + P[ni,ni] = Pi + + mat = ComposedLinearOperator(trial_space, test_space, P, mat) + + elif use_prolongation: + Ps = [knot_insertion_projection_operator(trs, trs.get_refined_space(ncells)) for trs in trial_fem_space.spaces] + P = BlockLinearOperator(trial_fem_space.coeff_space, trial_fem_space.get_refined_space(ncells).coeff_space) + for ni,Pi in enumerate(Ps): + P[ni,ni] = Pi + + mat = ComposedLinearOperator(trial_space, test_space, mat, P) + + self._matrix[i,j] = mat + + matrix = self._matrix[i,j] + else: # single patch + matrix = self._matrix + + shape = expr.shape + for k1 in range(shape[0]): + for k2 in range(shape[1]): + if expr[k1,k2].is_zero: + continue + + if isinstance(test_fem_space, VectorFemSpace): + ts_space = test_fem_space.get_refined_space(ncells).coeff_space.spaces[k1] + else: + ts_space = test_fem_space.get_refined_space(ncells).coeff_space + + if isinstance(trial_fem_space, VectorFemSpace): + tr_space = trial_fem_space.get_refined_space(ncells).coeff_space.spaces[k2] + else: + tr_space = trial_fem_space.get_refined_space(ncells).coeff_space + + if is_conformal and matrix[k1, k2]: + global_mats[k1, k2] = matrix[k1, k2] + elif not i == j: # assembling in an interface (type(target) == Interface) + axis = target.axis + ext_d = self._trial_ext + ext_c = self._test_ext + test_n = self. test_basis.space.spaces[k1].spaces[axis].nbasis + test_s = self. test_basis.space.spaces[k1].coeff_space.starts[axis] + trial_n = self.trial_basis.space.spaces[k2].spaces[axis].nbasis + cart = self.trial_basis.space.spaces[k2].coeff_space.cart + trial_s = cart.global_starts[axis][cart._coords[axis]] + + s_d = trial_n - trial_s - trial_degree[k2][axis] - 1 if ext_d == 1 else 0 + s_c = test_n - trial_s - test_degree[k1][axis] - 1 if ext_c == 1 else 0 + + # We only handle the case where direction = 1 + direction = target.ornt + if domain.dim == 2: + assert direction == 1 + elif domain.dim == 3: + assert all(d==1 for d in direction) + + direction = 1 + flip = [direction]*domain.dim + flip[axis] = 1 + if self._func != do_nothing: + global_mats[k1, k2] = StencilInterfaceMatrix(tr_space, ts_space, + s_d, s_c, + axis, axis, + ext_d, ext_c, + pads=tuple(pads[k1, k2]), + flip=flip) + else: + global_mats[k1, k2] = StencilMatrix(tr_space, ts_space, pads = tuple(pads[k1, k2])) + + if is_conformal: + matrix[k1, k2] = global_mats[k1, k2] + elif use_restriction: + matrix.multiplicants[-1][k1, k2] = global_mats[k1, k2] + elif use_prolongation: + matrix.multiplicants[0][k1, k2] = global_mats[k1, k2] + + else: # case of scalar equation + if is_broken: # multi-patch + if self._matrix[i, j]: + global_mats[i, j] = self._matrix[i, j] + + elif not i == j: # assembling in an interface (type(target) == Interface) + axis = target.axis + ext_d = self._trial_ext + ext_c = self._test_ext + test_n = self.test_basis.space.spaces[axis].nbasis + test_s = self.test_basis.space.coeff_space.starts[axis] + trial_n = self.trial_basis.space.spaces[axis].nbasis + cart = self.trial_basis.space.coeff_space.cart + trial_s = cart.global_starts[axis][cart._coords[axis]] + + s_d = trial_n - trial_s - trial_degree[axis] - 1 if ext_d == 1 else 0 + s_c = test_n - trial_s - test_degree[axis] - 1 if ext_c == 1 else 0 + + # We only handle the case where direction = 1 + direction = target.ornt + if domain.dim == 2: + assert direction == 1 + elif domain.dim == 3: + assert all(d==1 for d in direction) + + direction = 1 + flip = [direction]*domain.dim + flip[axis] = 1 + + if self._func != do_nothing: + mat = StencilInterfaceMatrix(trial_fem_space.get_refined_space(ncells).coeff_space, + test_fem_space.get_refined_space(ncells).coeff_space, + s_d, s_c, + axis, axis, + ext_d, ext_c, + flip=flip) + if not is_conformal: + if use_restriction: + P = knot_insertion_projection_operator(test_fem_space.get_refined_space(ncells), test_fem_space) + mat = ComposedLinearOperator(trial_space, test_space, P, mat) + elif use_prolongation: + P = knot_insertion_projection_operator(trial_fem_space, trial_fem_space.get_refined_space(ncells)) + mat = ComposedLinearOperator(trial_space, test_space, mat, P) + + global_mats[i, j] = mat + + # define part of the global matrix as a StencilMatrix + else: + global_mats[i, j] = StencilMatrix(trial_space, test_space, pads=tuple(pads)) + + if (i, j) in global_mats: + self._matrix[i, j] = global_mats[i, j] + + + # in single patch case, we define the matrices needed for the patch + else: + if self._matrix: + global_mats[0, 0] = self._matrix + else: + global_mats[0, 0] = StencilMatrix(trial_space, test_space, pads=tuple(pads)) + + self._matrix = global_mats[0, 0] + + # Set the backend of our matrices if given + if backend is not None and is_broken: + for mat in global_mats.values(): + mat.set_backend(backend) + elif backend is not None: + self._matrix.set_backend(backend) + + self._global_matrices = [M._data for M in extract_stencil_mats(global_mats.values())] + + +# ============================================================================== +class DiscreteSesquilinearForm(DiscreteBilinearForm): + """ Class that represents the concept of a discrete sesqui-linear form with the antilinearity on the first variable. + This class allocates the matrix and generates the matrix assembly method. + + Parameters + ---------- + + expr : sympde.expr.expr.SesquilinearForm + The symbolic sesqui-linear form. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the sesqui-linear form. + + domain_h : Geometry + The discretized domain + + spaces: list of FemSpace + The trial and test discrete spaces. + + nquads : list or tuple of int + The number of quadrature points used in the low-level assembly function + along each direction. + + matrix: Matrix + The matrix that we assemble into it. + If not provided, it will create a new Matrix of the appropriate space. + + update_ghost_regions: bool + Accumulate the contributions of the neighbouring processes. + + backend: dict + The backend used to accelerate the computing kernels. + The backend dictionaries are defined in the file psydac/api/settings.py + + assembly_backend: dict + The backend used to accelerate the assembly method. + The backend dictionaries are defined in the file psydac/api/settings.py + + linalg_backend: dict + The backend used to accelerate the computing kernels of the linear operator. + The backend dictionaries are defined in the file psydac/api/settings.py + + symbolic_mapping: Sympde.topology.Mapping + The symbolic mapping which defines the physical domain of the sesqui-linear form. + + """ + + +#============================================================================== +class DiscreteLinearForm(BasicDiscrete): + """ + Discrete linear form ready to be assembled into a vector. + + This class represents the concept of a discrete linear form in Psydac. + Instances of this class generate an appropriate vector assembly kernel, + allocate the vector if not provided, and prepare a list of arguments for + the kernel. + + Parameters + ---------- + + expr : sympde.expr.expr.LinearForm + The symbolic linear form. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the linear form. + + domain_h : Geometry + The discretized domain. + + space : FemSpace + The discrete test space. + + nquads : list or tuple of int + The number of quadrature points used in the assembly kernel along each + direction. + + vector : StencilVector or BlockVector, optional + The vector that we assemble into. If not provided, a new vector of the + appropriate space is created. + + update_ghost_regions : bool, default=False + Accumulate the contributions of the neighbouring processes. + + backend : dict, optional + The backend used to accelerate the computing kernels. + The backend dictionaries are defined in the file psydac/api/settings.py + + symbolic_mapping : Sympde.topology.Mapping, optional + The symbolic mapping which defines the physical domain of the linear form. + + See Also + -------- + DiscreteBilinearForm + DiscreteFunctional + DiscreteSumForm + + """ + def __init__(self, expr, kernel_expr, domain_h, space, *, nquads, + vector=None, update_ghost_regions=True, backend=None, + symbolic_mapping=None): + + if not isinstance(expr, sym_LinearForm): + raise TypeError('> Expecting a symbolic LinearForm') + + assert isinstance(domain_h, Geometry) + + self._space = space + + if isinstance(kernel_expr, (tuple, list)): + if len(kernel_expr) == 1: + kernel_expr = kernel_expr[0] + else: + raise ValueError('> Expecting only one kernel') + + # ... + self._kernel_expr = kernel_expr + self._target = kernel_expr.target + self._domain = domain_h.domain + self._vector = vector + + domain = self.domain + target = self.target + + if len(domain) > 1: + i = self.get_space_indices_from_target(domain, target) + test_space = self._space.spaces[i] + mapping = list(domain_h.mappings.values())[i] + else: + test_space = self._space + mapping = list(domain_h.mappings.values())[0] + + if isinstance(test_space.coeff_space, BlockVectorSpace): + coeff_space = test_space.coeff_space.spaces[0] + if isinstance(coeff_space, BlockVectorSpace): + coeff_space = coeff_space.spaces[0] + else: + coeff_space = test_space.coeff_space + + self._mapping = mapping + self._coeff_space = coeff_space + self._num_threads = 1 + if coeff_space.parallel and coeff_space.cart.num_threads>1: + self._num_threads = coeff_space.cart._num_threads + + self._update_ghost_regions = update_ghost_regions + + # In case of multiple patches, if the communicator is MPI_COMM_NULL or the cart is an Interface cart, + # we do not generate the assembly code, because the patch is not owned by the MPI rank. + if coeff_space.parallel and (coeff_space.cart.is_comm_null or isinstance(coeff_space.cart, InterfaceCartDecomposition)): + self._free_args = () + self._func = do_nothing + self._args = () + self._threads_args = () + self._global_matrices = () + self._update_ghost_regions = False + return + + if mapping is not None: + is_rational_mapping = isinstance(mapping, NurbsMapping) + mapping_space = mapping.space + else: + is_rational_mapping = False + mapping_space = None + + self._is_rational_mapping = is_rational_mapping + discrete_space = test_space + + # MPI communicator + comm = coeff_space.cart.comm if coeff_space.parallel else None + + # BasicDiscrete generates the assembly code and sets the following attributes that are used afterwards: + # self._func, self._free_args, self._max_nderiv and self._backend + BasicDiscrete.__init__(self, expr, kernel_expr, comm=comm, root=0, discrete_space=discrete_space, + nquads=nquads, is_rational_mapping=is_rational_mapping, mapping=symbolic_mapping, + mapping_space=mapping_space, num_threads=self._num_threads, backend=backend) + + #... Handle the special case where the current MPI process does not need to do anything + if not isinstance(target, Boundary): + ext = None + axis = None + else: + ext = target.ext + axis = target.axis + + # Assuming that all vector spaces (and their Cartesian decomposition, + # if any) are compatible with each other, extract the first available + # vector space from which (starts, ends, pads) will be read: + # If process does not own the boundary or interface, do not assemble anything + if ext == -1: + start = coeff_space.starts[axis] + if start != 0: + self._func = do_nothing + + elif ext == 1: + end = coeff_space.ends[axis] + npts = coeff_space.npts[axis] + if end + 1 != npts: + self._func = do_nothing + #... + + # Build the quadrature grids + test_grid = QuadratureGrid(test_space, axis=axis, ext=ext, nquads=nquads) + self._grid = test_grid + + # Extract the basis function values on the quadrature grid + self._test_basis = BasisValues( + test_space, + nderiv = self.max_nderiv, + nquads = nquads, + grid = test_grid + ) + + # Allocate the output vector, if needed + self.allocate_matrices() + + # Determine whether OpenMP instructions were generated + with_openmp = (backend['name'] == 'pyccel' and backend['openmp']) if backend else False + + # Construct the arguments to be passed to the assemble() function, which is stored in self._func + self._args, self._threads_args = self.construct_arguments(with_openmp=with_openmp) + + @property + def domain(self): + return self._domain + + @property + def target(self): + return self._target + + @property + def space(self): + return self._space + + @property + def grid(self): + return self._grid + + @property + def nquads(self): + return self._grid.nquads + + @property + def test_basis(self): + return self._test_basis + + @property + def global_matrices(self): + return self._global_matrices + + @property + def args(self): + return self._args + + def assemble(self, *, reset=True, **kwargs): + """ + This method assembles the right-hand side Vector by calling the private method `self._func` with proper arguments. + + In the complex case, this function returns the vector conjugate. This comes from the fact that the + problem `a(u,v)=b(v)` is discretize as `A @ conj(U) = B` due to the antilinearity of `a` in the first variable. + Thus, to obtain `U`, the assemble function for the LinearForm return `conj(B)`. + + TODO: remove these lines when the dot product is changed for complex in sympde. + For now, since the dot product does not do the conjugate in the complex case, we do not use the conjugate in the assemble function. + It should work if the complex only comes from the `rhs` in the linear form. + """ + if self._free_args: + basis = [] + spans = [] + degrees = [] + pads = [] + coeffs = [] + consts = [] + for key in self._free_args: + v = kwargs[key] + if len(self.domain) > 1 and isinstance(v, FemField) and (v.space.is_multipatch or v.space.is_vector_valued): + assert v.space.is_multipatch ## [MCP 27.03.2025] should hold since len(domain) > 1. If Ok we can simplify above if + i = self.get_space_indices_from_target(self.domain, self.target) + v = v[i] + if isinstance(v, FemField): + if not v.coeffs.ghost_regions_in_sync: + v.coeffs.update_ghost_regions() + basis_v = BasisValues( + v.space, + nderiv = self.max_nderiv, + nquads = self.nquads, + trial = True, + grid = self.grid + ) + bs, d, s, p, m = construct_test_space_arguments(basis_v) + basis += bs + spans += s + degrees += [np.int64(a) for a in d] + pads += [np.int64(a) for a in p] + if v.space.is_multipatch or v.space.is_vector_valued: + coeffs += (e._data for e in v.coeffs) + else: + coeffs += (v.coeffs._data,) + else: + consts += (v,) + + args = (*self.args, *basis, *spans, *degrees, *pads, *coeffs, *consts) + + else: + args = self._args + + if reset: + reset_arrays(*self.global_matrices) + + self._func(*args, *self._threads_args) + if self._vector and self._update_ghost_regions: + self._vector.exchange_assembly_data() + + # TODO : uncomment this line when the conjugate is applied on the dot product in the complex case + # self._vector.conjugate(out=self._vector) + + if self._vector: + self._vector.ghost_regions_in_sync = False + + return self._vector + + def get_space_indices_from_target(self, domain, target): + if domain.mapping: + domain = domain.logical_domain + if target.mapping: + target = target.logical_domain + + domains = domain.interior.args + + if isinstance(target, Interface): + raise NotImplementedError("Index of an interface is not defined for the LinearForm") + elif isinstance(target, Boundary): + i = domains.index(target.domain) + else: + i = domains.index(target) + return i + + def construct_arguments(self, with_openmp=False): + """ + Collect the arguments used in the assembly method. + + Parameters + ---------- + with_openmp : bool + If set to True we collect some extra arguments used in the assembly method + + Returns + ------- + + args: tuple + The arguments passed to the assembly method. + + threads_args: tuple + Extra arguments used in the assembly method in case with_openmp=True. + + """ + tests_basis, tests_degrees, spans, pads, mult = construct_test_space_arguments(self.test_basis) + n_elements, quads, nquads = construct_quad_grids_arguments(self.grid, use_weights=False) + + global_pads = self.space.coeff_space.pads + + if self.mapping: + mapping = [e._coeffs._data for e in self.mapping._fields] + space = self.mapping._fields[0].space + map_degree = space.degree + map_span = [q.spans - s for q, s in zip(space.get_assembly_grids(*self.nquads), space.coeff_space.starts)] + map_basis = [q.basis for q in space.get_assembly_grids(*self.nquads)] + axis = self.grid.axis + ext = self.grid.ext + points = self.grid.points + if axis is not None: + nderiv = self.max_nderiv + space = space.spaces[axis] + points = points[axis] + local_span = find_span(space.knots, space.degree, points[0, 0]) + boundary_basis = basis_funs_all_ders(space.knots, space.degree, + points[0, 0], local_span, nderiv, space.basis) + map_basis[axis] = map_basis[axis].copy() + map_basis[axis][0, :, 0:nderiv+1, 0] = np.transpose(boundary_basis) + if ext == 1: + map_span[axis] = map_span[axis].copy() + map_span[axis][0] = map_span[axis][-1] + if self.is_rational_mapping: + mapping = [*mapping, self.mapping.weights_field.coeffs._data] + else: + mapping = [] + map_degree = [] + map_span = [] + map_basis = [] + + args = (*tests_basis, *map_basis, *spans, *map_span, *quads, *tests_degrees, *map_degree, *n_elements, *nquads, *global_pads, *mapping, *self._global_matrices) + + with_openmp = with_openmp and self._num_threads>1 + + threads_args = () + if with_openmp: + threads_args = self._coeff_space.cart.get_shared_memory_subdivision(n_elements) + threads_args = (threads_args[0], threads_args[1], *threads_args[2], *threads_args[3], threads_args[4]) + + args = tuple(np.int64(a) if isinstance(a, int) else a for a in args) + threads_args = tuple(np.int64(a) if isinstance(a, int) else a for a in threads_args) + + return args, threads_args + + def allocate_matrices(self): + """ + Allocate the global matrices used in the assembly method. + In this method we allocate only the matrices that are computed in the self._target domain, + we also avoid double allocation if we have many DiscreteLinearForm that are defined on the same self._target domain. + """ + global_mats = {} + + test_space = self.test_basis.space.coeff_space + test_degree = np.array(self.test_basis.space.degree) + + expr = self.kernel_expr.expr + target = self.kernel_expr.target + domain = self.domain + is_broken = len(domain) > 1 + + if self._vector is None and (is_broken or isinstance(expr, (ImmutableDenseMatrix, Matrix))): + self._vector = BlockVector(self.space.coeff_space) + + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # case system of equations + + if is_broken: #multi patch + i = self.get_space_indices_from_target(domain, target) + if not self._vector[i]: + self._vector[i] = BlockVector(test_space) + vector = self._vector[i] + else: # single patch + vector = self._vector + + expr = expr[:] + for i in range(len(expr)): + if expr[i].is_zero: + continue + else: + if vector[i]: + global_mats[i] = vector[i] + else: + global_mats[i] = StencilVector(test_space.spaces[i]) + + vector[i] = global_mats[i] + else: + if is_broken: + i = self.get_space_indices_from_target(domain, target) + if self._vector[i]: + global_mats[i] = self._vector[i] + else: + global_mats[i] = StencilVector(test_space) + + self._vector[i] = global_mats[i] + else: + if self._vector: + global_mats[0] = self._vector + else: + global_mats[0] = StencilVector(test_space) + self._vector = global_mats[0] + + self._global_matrices = [M._data for M in global_mats.values()] + + +#============================================================================== +# NOTE: why do we pass a FemSpace to the constructor? +class DiscreteFunctional(BasicDiscrete): + """ + Discrete functional ready to be assembled into a scalar (real or complex). + + This class represents the concept of a discrete functional in Psydac. + Instances of this class generate an appropriate functional assembly kernel, + and prepare a list of arguments for the kernel. + + Parameters + ---------- + + expr : sympde.expr.expr.Functional + The symbolic functional form. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the functional form. + + domain_h : Geometry + The discretized domain. + + space : FemSpace + The discrete space. + + nquads : list or tuple of int + The number of quadrature points used in the assembly kernel along each + direction. + + update_ghost_regions : bool, default=True + Accumulate the contributions of the neighbouring processes. + + backend : dict + The backend used to accelerate the computing kernels. + The backend dictionaries are defined in the file psydac/api/settings.py + + symbolic_mapping : Sympde.topology.Mapping + The symbolic mapping which defines the physical domain of the functional. + + See Also + -------- + DiscreteBilinearForm + DiscreteLinearForm + DiscreteSumForm + + """ + def __init__(self, expr, kernel_expr, domain_h, space, *, nquads, + backend=None, symbolic_mapping=None): + + if not isinstance(expr, sym_Functional): + raise TypeError('> Expecting a symbolic Functional') + + # ... + assert isinstance(domain_h, Geometry) + + self._space = space + + if isinstance(kernel_expr, (tuple, list)): + if len(kernel_expr) == 1: + kernel_expr = kernel_expr[0] + else: + raise ValueError('> Expecting only one kernel') + + # ... + self._kernel_expr = kernel_expr + self._target = kernel_expr.target + self._symbolic_space = self._space.symbolic_space + self._domain = domain_h.domain + # ... + + domain = self.domain + target = self.target + + if len(domain) > 1: + i = self.get_space_indices_from_target(domain, target) + self._space = self._space.spaces[i] + mapping = list(domain_h.mappings.values())[i] + else: + mapping = list(domain_h.mappings.values())[0] + + if isinstance(self.space.coeff_space, BlockVectorSpace): + coeff_space = self.space.coeff_space.spaces[0] + if isinstance(coeff_space, BlockVectorSpace): + coeff_space = coeff_space.spaces[0] + else: + coeff_space = self.space.coeff_space + + num_threads = 1 + if coeff_space.parallel and coeff_space.cart.num_threads > 1: + num_threads = coeff_space.cart._num_threads + + # In case of multiple patches, if the communicator is MPI_COMM_NULL, we do not generate the assembly code + # because the patch is not owned by the MPI rank. + if coeff_space.parallel and coeff_space.cart.is_comm_null: + self._free_args = () + self._func = do_nothing + self._args = () + self._expr = expr + self._comm = domain_h.comm + return + + if isinstance(target, Boundary): + ext = target.ext + axis = target.axis + else: + ext = None + axis = None + + if mapping is not None: + is_rational_mapping = isinstance( mapping, NurbsMapping ) + mapping_space = mapping.space + else: + is_rational_mapping = False + mapping_space = None + + self._mapping = mapping + self._is_rational_mapping = is_rational_mapping + discrete_space = self._space + + # MPI communicator + comm = coeff_space.cart.comm if coeff_space.parallel else None + + # BasicDiscrete generates the assembly code and sets the following attributes that are used afterwards: + # self._func, self._free_args, self._max_nderiv and self._backend + BasicDiscrete.__init__(self, expr, kernel_expr, comm=comm, root=0, discrete_space=discrete_space, + nquads=nquads, is_rational_mapping=is_rational_mapping, mapping=symbolic_mapping, + mapping_space=mapping_space, num_threads=num_threads, backend=backend) + + # Build the quadrature grid + grid = QuadratureGrid(self.space, axis=axis, ext=ext, nquads=nquads) + self._grid = grid + + # Extract the basis function values on the quadrature grid + self._test_basis = BasisValues( + self.space, + nderiv = self.max_nderiv, + nquads = nquads, + trial = True, + grid = grid + ) + + # Store MPI communicator + # NOTE [YG 18.04.2024]: this is not equal to the variable `comm` when we have multiple patches + self._comm = domain_h.comm + + # Construct the arguments to be passed to the assemble() function, which is stored in self._func + self._args = self.construct_arguments() + + @property + def domain(self): + return self._domain + + @property + def target(self): + return self._target + + @property + def space(self): + return self._space + + @property + def grid(self): + return self._grid + + @property + def nquads(self): + return self._grid.nquads + + @property + def test_basis(self): + return self._test_basis + + def get_space_indices_from_target(self, domain, target): + if domain.mapping: + domain = domain.logical_domain + if target.mapping: + target = target.logical_domain + + domains = domain.interior.args + if isinstance(target, Interface): + raise NotImplementedError("Index of an interface is not defined for the FunctionalForm") + elif isinstance(target, Boundary): + i = domains.index(target.domain) + else: + i = domains.index(target) + return i + + def construct_arguments(self): + """ + Collect the arguments used in the assembly method. + + Returns + ------- + args: tuple + The arguments passed to the assembly method. + """ + + n_elements = [e-s+1 for s,e in zip(self.grid.local_element_start,self.grid.local_element_end)] + + points = self.grid.points + weights = self.grid.weights + tests_basis = self.test_basis.basis + spans = self.test_basis.spans + tests_degrees = self.space.degree + + tests_basis, tests_degrees, spans = collect_spaces(self.space.symbolic_space, tests_basis, tests_degrees, spans) + + global_pads = flatten(self.test_basis.space.pads) + multiplicity = flatten(self.test_basis.space.multiplicity) + global_pads = [p*m for p,m in zip(global_pads, multiplicity)] + + tests_basis = flatten(tests_basis) + tests_degrees = flatten(tests_degrees) + spans = flatten(spans) + quads = flatten(list(zip(points, weights))) + nquads = flatten(self.grid.nquads) + + if self.mapping: + mapping = [e._coeffs._data for e in self.mapping._fields] + space = self.mapping._fields[0].space + map_degree = space.degree + map_span = [q.spans-s for q,s in zip(space.get_assembly_grids(*self.nquads), space.coeff_space.starts)] + map_basis = [q.basis for q in space.get_assembly_grids(*self.nquads)] + + if self.is_rational_mapping: + mapping = [*mapping, self.mapping._weights_field._coeffs._data] + else: + mapping = [] + map_degree = [] + map_span = [] + map_basis = [] + + args = (*tests_basis, *map_basis, *spans, *map_span, *quads, *tests_degrees, *map_degree, *n_elements, *nquads, *global_pads, *mapping) + args = tuple(np.int64(a) if isinstance(a, int) else a for a in args) + + return args + + def assemble(self, **kwargs): + """ + This method assembles the square of the functional expression with the given arguments and then compute + the square root of the absolute value of the result. + + Examples + -------- + >>> n = SemiNorm(1.0j*v, domain, kind='l2') + >>> nh = discretize(n, domain_h, Vh , **kwargs) + >>> fh = FemField(Vh) + >>> fh.coeffs[:] = 1 + >>> n_value = nh.assemble(v=fh) + + In n_value we have the value of np.sqrt(abs(sum((1.0jv)**2))) + """ + args = [*self._args] + for key in self._free_args: + v = kwargs[key] + if isinstance(v, FemField): + if not v.coeffs.ghost_regions_in_sync: + v.coeffs.update_ghost_regions() + if v.space.is_multipatch or v.space.is_vector_valued: + coeffs = v.coeffs + if self._symbolic_space.is_broken: + index = self.get_space_indices_from_target(self._domain, self._target) + coeffs = coeffs[index] + + if isinstance(coeffs, StencilVector): + args += (coeffs._data, ) + else: + args += (e._data for e in coeffs) + else: + args += (v.coeffs._data, ) + else: + args += (v, ) + + v = self._func(*args) + if isinstance(self.expr, (sym_Norm, sym_SemiNorm)): + if not( self.comm is None ): + v = self.comm.allreduce(sendobj=v) + + if self.expr.exponent == 2: + # add abs because of 0 machine + v = np.sqrt(np.abs(v)) + else: + raise NotImplementedError('TODO') + return v diff --git a/psydac/api/fem_bilinear_form.py b/psydac/api/fem_bilinear_form.py new file mode 100644 index 000000000..8af05df83 --- /dev/null +++ b/psydac/api/fem_bilinear_form.py @@ -0,0 +1,2226 @@ +import sys +import os +import importlib + +import numpy as np + +from sympy import ImmutableDenseMatrix, Matrix, Symbol, sympify +from sympy.tensor.indexed import Indexed, IndexedBase +from sympy.simplify import cse_main + +from pyccel import epyccel + +from sympde.topology.basic import Boundary, Interface +from sympde.topology.mapping import Mapping, SymbolicExpr +from sympde.topology.space import ScalarFunction, VectorFunction, IndexedVectorFunction +from sympde.topology.derivatives import get_atom_logical_derivatives +from sympde.topology.derivatives import _logical_partial_derivatives +from sympde.topology.derivatives import get_index_logical_derivatives +from sympde.topology.derivatives import get_max_logical_partial_derivatives # NOTE [YG 31.07.2025]: Maybe use the one in ast.utilities +from sympde.expr.expr import BilinearForm +from sympde.expr.evaluation import KernelExpression, TerminalExpr +from sympde.calculus.core import PlusInterfaceOperator + +from psydac.cad.geometry import Geometry +from psydac.mapping.discrete import SplineMapping, NurbsMapping +from psydac.fem.basic import FemSpace, FemField +from psydac.fem.vector import VectorFemSpace +from psydac.linalg.stencil import StencilMatrix +from psydac.linalg.block import BlockVectorSpace, BlockLinearOperator +from psydac.api.grid import QuadratureGrid, BasisValues +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.api.utilities import flatten, random_string +from psydac.api.fem_common import ( + compute_imports, + compute_max_nderiv, + compute_free_arguments, + construct_test_space_arguments, + construct_trial_space_arguments, + construct_quad_grids_arguments, + reset_arrays, + do_nothing, + extract_stencil_mats, +) + +# TODO [YG 01.08.2025]: Avoid importing anything from psydac.pyccel +from psydac.pyccel.ast.core import _atomic, Assign + +__all__ = ('DiscreteBilinearForm',) + +NoneType = type(None) + +#============================================================================== +class DiscreteBilinearForm: + """ + Discrete bilinear form ready to be assembled into a matrix. + + This class represents the concept of a discrete bilinear form in Psydac. + Instances of this class generate an appropriate matrix assembly kernel, + allocate the matrix if not provided, and prepare a list of arguments for + the kernel. + + An implementation of the sum factorization algorithm is used to assemble + the matrix. + + Parameters + ---------- + + expr : sympde.expr.expr.BilinearForm + The symbolic bilinear form. + + kernel_expr : list or tuple of sympde.expr.evaluation.KernelExpression + The atomic representation of the bilinear form. + + domain_h : psydac.cad.geometry.Geometry + The discretized domain. + + spaces : list of psydac.fem.basic.FemSpace + The discrete trial and test spaces. + + nquads : list or tuple of int + The number of quadrature points used in the assembly kernel along each + direction. + + matrix : psydac.linalg.stencil.StencilMatrix or psydac.linalg.block.BlockLinearOperator, optional + The matrix that we assemble into. If not provided, a new matrix is + created with the appropriate domain and codomain (default: None). + + update_ghost_regions : bool, default=True + Accumulate the contributions of the neighbouring processes. + + backend : dict, optional + The backend used to accelerate the computing kernels. + The backend dictionaries are defined in the file psydac/api/settings.py + + assembly_backend : dict, optional + The backend used to accelerate the assembly kernel. + The backend dictionaries are defined in the file psydac/api/settings.py + + linalg_backend : dict, optional + The backend used to accelerate the computing kernels of the linear operator. + The backend dictionaries are defined in the file psydac/api/settings.py + + symbolic_mapping : sympde.topology.mapping.Mapping, optional + The symbolic mapping which defines the physical domain of the bilinear form. + + See Also + -------- + DiscreteLinearForm + DiscreteFunctional + DiscreteSumForm + + """ + def __init__(self, expr, kernel_expr, domain_h, spaces, *, nquads, + matrix=None, update_ghost_regions=True, backend=None, + linalg_backend=None, assembly_backend=None, + symbolic_mapping=None): + + #... Sanity checks + assert isinstance(expr, BilinearForm) + assert isinstance(domain_h, Geometry) + for space in spaces: + assert isinstance(space, FemSpace) + for nquad in nquads: + assert isinstance(nquad, int) + assert nquad > 0 + assert isinstance(matrix, (NoneType, StencilMatrix, BlockLinearOperator)) + assert isinstance(update_ghost_regions, bool) + assert isinstance( backend, (NoneType, dict)) + assert isinstance( linalg_backend, (NoneType, dict)) + assert isinstance(assembly_backend, (NoneType, dict)) + assert isinstance(symbolic_mapping, (NoneType, Mapping)) + #... + + if isinstance(kernel_expr, (tuple, list)): + if len(kernel_expr) == 1: + kernel_expr = kernel_expr[0] + else: + raise ValueError('> Expecting only one kernel') + assert isinstance(kernel_expr, KernelExpression) + + self._kernel_expr = kernel_expr + self._expr = expr + self._target = kernel_expr.target + self._domain = domain_h.domain + self._spaces = spaces + self._matrix = matrix + + domain = self.domain + target = self.target + + # ... + if len(domain) > 1: + i, j = self.get_space_indices_from_target(domain, target) + test_space = self.spaces[1].spaces[i] + trial_space = self.spaces[0].spaces[j] + if isinstance(target, Interface): + m,_ = self.get_space_indices_from_target(domain, target.minus) + p,_ = self.get_space_indices_from_target(domain, target.plus) + mapping_m = list(domain_h.mappings.values())[m] + mapping_p = list(domain_h.mappings.values())[p] + mapping = (mapping_m, mapping_p) if mapping_m else None + else: + mapping = list(domain_h.mappings.values())[i] + else: + trial_space = self.spaces[0] + test_space = self.spaces[1] + mapping = list(domain_h.mappings.values())[0] + + self._mapping = mapping + + is_rational_mapping = False + mapping_space = None + if (mapping is not None) and not isinstance(target, Interface): + is_rational_mapping = isinstance(mapping, NurbsMapping) + mapping_space = mapping.space + elif (mapping is not None) and isinstance(target, Interface): + is_rational_mapping = (isinstance(mapping[0], NurbsMapping), isinstance(mapping[1], NurbsMapping)) + mapping_space = (mapping[0].space, mapping[1].space) + + self._is_rational_mapping = is_rational_mapping + # ... + + if isinstance(test_space.coeff_space, BlockVectorSpace): + coeff_space = test_space.coeff_space.spaces[0] + else: + coeff_space = test_space.coeff_space + + self._coeff_space = coeff_space + self._num_threads = 1 + if coeff_space.parallel and coeff_space.cart.num_threads > 1: + self._num_threads = coeff_space.cart.num_threads + + self._update_ghost_regions = update_ghost_regions + + # In case of multiple patches, if the communicator is MPI_COMM_NULL, we do not generate the assembly code + # because the patch is not owned by the MPI rank. + if coeff_space.parallel and coeff_space.cart.is_comm_null: + self._free_args = () + self._func = do_nothing + self._args = () + self._threads_args = () + self._global_matrices = () + self._update_ghost_regions = False + return + + # ... + test_ext = None + trial_ext = None + if isinstance(target, Boundary): + axis = target.axis + test_ext = target.ext + trial_ext = target.ext + elif isinstance(target, Interface): + # this part treats the cases of: + # integral(v_minus * u_plus) + # integral(v_plus * u_minus) + # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus) + # are converted to boundary integrals by Sympde + axis = target.axis + test = self.kernel_expr.test + trial = self.kernel_expr.trial + test_target = target.plus if isinstance( test, PlusInterfaceOperator) else target.minus + trial_target = target.plus if isinstance(trial, PlusInterfaceOperator) else target.minus + test_ext = test_target.ext + trial_ext = trial_target.ext + ncells = tuple(max(i, j) for i, j in zip(test_space.ncells, trial_space.ncells)) + if isinstance(trial_space, VectorFemSpace): + spaces = [] + for sp in trial_space.spaces: + if (trial_target.axis, trial_target.ext) in sp.interfaces: + spaces.append(sp.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext]) + + if len(spaces) == len(trial_space.spaces): + sym_space = trial_space.symbolic_space + trial_space = VectorFemSpace(*spaces) + trial_space.symbolic_space = sym_space + + elif (trial_target.axis, trial_target.ext) in trial_space.interfaces: + sym_space = trial_space.symbolic_space + trial_space = trial_space.get_refined_space(ncells).interfaces[trial_target.axis, trial_target.ext] + trial_space.symbolic_space = sym_space + + test_space = test_space.get_refined_space(ncells) + self._test_ext = test_target.ext + self._trial_ext = trial_target.ext + + #... + + # Assuming that all vector spaces (and their Cartesian decomposition, + # if any) are compatible with each other, extract the first available + # vector space from which (starts, ends, npts) will be read: + starts = coeff_space.starts + ends = coeff_space.ends + npts = coeff_space.npts + + # MPI communicator + comm = coeff_space.cart.comm if coeff_space.parallel else None + + # Store the MPI communicator (or None) + self._comm = comm + + #... + # Get default backend from environment, or use 'python'. + default_backend = PSYDAC_BACKENDS.get(os.environ.get('PSYDAC_BACKEND'))\ + or PSYDAC_BACKENDS['python'] + + # Backends for code generation + assembly_backend = backend or assembly_backend + linalg_backend = backend or linalg_backend + + # Store backend dictionary + self._backend = assembly_backend or default_backend + #... + + # TODO: remove + # BasicDiscrete generates the assembly code and sets the following attributes that are used afterwards: + # self._func, self._free_args, self._max_nderiv and self._backend +# BasicDiscrete.__init__(self, expr, kernel_expr, comm=comm, root=0, discrete_space=discrete_space, +# nquads=nquads, is_rational_mapping=is_rational_mapping, mapping=symbolic_mapping, +# mapping_space=mapping_space, num_threads=self._num_threads, backend=assembly_backend) + + + #... Compute the string with all the imports + texpr = kernel_expr + sym_expr = SymbolicExpr(texpr.expr) + imports = compute_imports(sym_expr, spaces=(trial_space, test_space), openmp=False) + indent = 4 + glue = '\n' + ' '* indent + imports_str = glue.join([f"from {m} import {', '.join(vars)}" + for m, vars in imports.items()]) + + # Broadcast the import information (sqrt, sin, pi, ...) to all processes + if (comm is not None) and (comm.size > 1): + imports_str = comm.bcast(imports_str, root=0) + + # Store the imports string as it will be used by make_file() + self._imports_string = imports_str + #... + + # Compute the highest order of derivation in the kernel expression + self._max_nderiv = compute_max_nderiv(kernel_expr) + + # TODO [YG 31.07.2025]: Implement this + self._free_args = compute_free_arguments(expr, kernel_expr) + + #... Handle the special case where the current MPI process does not need to do anything + if isinstance(target, (Boundary, Interface)): + + # If process does not own the boundary or interface, do not assemble anything + if test_ext == -1: + if starts[axis] != 0: + self._func = do_nothing + + elif test_ext == 1: + if ends[axis] != npts[axis]-1: + self._func = do_nothing + + # In case of target==Interface, we only use the MPI ranks that are on the interface to assemble the BilinearForm + if self._func == do_nothing and isinstance(target, Interface): + self._free_args = () + self._args = () + self._global_matrices = () + self._threads_args = () + return + #... + + #... Build the quadrature grids + if isinstance(target, Boundary): + test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads) + self._grid = (test_grid,) + elif isinstance(target, Interface): + # this part treats the cases of: + # integral(v_minus * u_plus) + # integral(v_plus * u_minus) + # the other cases, integral(v_minus * u_minus) and integral(v_plus * u_plus) + # are converted to boundary integrals by Sympde + test_grid = QuadratureGrid( test_space, axis=axis, ext= test_ext, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, axis=axis, ext=trial_ext, nquads=nquads) + self._grid = (test_grid, trial_grid) if test_target == target.minus else (trial_grid, test_grid) + self._test_ext = test_target.ext + self._trial_ext = trial_target.ext + else: + test_grid = QuadratureGrid( test_space, nquads=nquads) + trial_grid = QuadratureGrid(trial_space, nquads=nquads) + self._grid = (test_grid,) + #... + + # Extract the basis function values on the quadrature grids + self._test_basis = BasisValues( + test_space, + nderiv = self.max_nderiv, + nquads = nquads, + trial = False, + grid = test_grid + ) + self._trial_basis = BasisValues( + trial_space, + nderiv = self.max_nderiv, + nquads = nquads, + trial = True , + grid = trial_grid + ) + + # Allocate the output matrix, if needed + self.allocate_matrices(linalg_backend) + + # Determine whether OpenMP instructions were generated + self._with_openmp = (assembly_backend['name'] == 'pyccel' and assembly_backend['openmp']) if assembly_backend else False + + # Construct the arguments to be passed to the assemble() function, which is stored in self._func + # First we generate the assembly file + + # pyccelize process of computing the test_trial arrays + # currently set to False, as a Python 3.9 test fails, and due to the "speed up" not being significant + self._pyccelize_test_trial_computation = False + + # no openmp support yet: with_openmp is not passed + self._args, self._threads_args = self.construct_arguments_generate_assembly_file() + + #-------------------------------------------------------------------------- + @property + def comm(self): + return self._comm + + @property + def expr(self): + return self._expr + + @property + def kernel_expr(self): + return self._kernel_expr + + @property + def domain(self): + return self._domain + + @property + def mapping(self): + return self._mapping + + @property + def is_rational_mapping(self): + return self._is_rational_mapping + + @property + def target(self): + return self._target + + @property + def spaces(self): + return self._spaces + + @property + def test_basis(self): + return self._test_basis + + @property + def trial_basis(self): + return self._trial_basis + + @property + def grid(self): + return self._grid + + @property + def nquads(self): + return self._grid[0].nquads + + @property + def free_args(self): + return self._free_args + + @property + def max_nderiv(self): + # TODO: compute with read_BilinearForm and store + return self._max_nderiv + + @property + def backend(self): + return self._backend + + @property + def args(self): + return self._args + + @property + def global_matrices(self): + return self._global_matrices + + #-------------------------------------------------------------------------- + def allocate_matrices(self, backend=None): + """ + Allocate the global matrices used in the assembly method. + In this method we allocate only the matrices that are computed in the self._target domain, + we also avoid double allocation if we have many DiscreteLinearForm that are defined on the same self._target domain. + + Parameters + ---------- + backend : dict + The backend used to accelerate the computing kernels. + + """ + global_mats = {} + + expr = self.kernel_expr.expr + target = self.kernel_expr.target + test_degree = np.array(self.test_basis.space.degree) + trial_degree = np.array(self.trial_basis.space.degree) + test_space = self.spaces[1].coeff_space + trial_space = self.spaces[0].coeff_space + test_fem_space = self.spaces[1] + trial_fem_space = self.spaces[0] + domain = self.domain + is_broken = len(domain) > 1 + is_conformal = True + + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): + if not isinstance(test_degree[0],(list, tuple, np.ndarray)): + test_degree = [test_degree] + + if not isinstance(trial_degree[0],(list, tuple, np.ndarray)): + trial_degree = [trial_degree] + + pads = np.empty((len(test_degree),len(trial_degree),len(test_degree[0])), dtype=int) + for i in range(len(test_degree)): + for j in range(len(trial_degree)): + td = test_degree[i] + trd = trial_degree[j] + pads[i,j][:] = np.array([td, trd]).max(axis=0) + else: + pads = np.maximum(test_degree, trial_degree) + + if self._matrix is None and (is_broken or isinstance(expr, (ImmutableDenseMatrix, Matrix))): + self._matrix = BlockLinearOperator(trial_space, test_space) + + if is_broken: + i, j = self.get_space_indices_from_target(domain, target) + test_fem_space = self.spaces[1].spaces[i] + trial_fem_space = self.spaces[0].spaces[j] + test_space = test_space.spaces[i] + trial_space = trial_space.spaces[j] + ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells)) + is_conformal = tuple(test_fem_space.ncells) == ncells and tuple(trial_fem_space.ncells) == ncells + if is_broken and not is_conformal and not i==j: + use_restriction = all(trn>=tn for trn,tn in zip(trial_fem_space.ncells, test_fem_space.ncells)) + use_prolongation = not use_restriction + + else: + ncells = tuple(max(i,j) for i,j in zip(test_fem_space.ncells, trial_fem_space.ncells)) + i=0 + j=0 + #else so initialisation causing bug on line 682 + + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # case of system of equations + + if is_broken: #multi patch + if not self._matrix[i,j]: + mat = BlockLinearOperator(trial_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.get_refined_space(ncells).coeff_space) + if not is_conformal and not i==j: + if use_restriction: + Ps = [knot_insertion_projection_operator(ts.get_refined_space(ncells), ts) for ts in test_fem_space.spaces] + P = BlockLinearOperator(test_fem_space.get_refined_space(ncells).coeff_space, test_fem_space.coeff_space) + for ni,Pi in enumerate(Ps): + P[ni,ni] = Pi + + mat = ComposedLinearOperator(trial_space, test_space, P, mat) + + elif use_prolongation: + Ps = [knot_insertion_projection_operator(trs, trs.get_refined_space(ncells)) for trs in trial_fem_space.spaces] + P = BlockLinearOperator(trial_fem_space.coeff_space, trial_fem_space.get_refined_space(ncells).coeff_space) + for ni,Pi in enumerate(Ps): + P[ni,ni] = Pi + + mat = ComposedLinearOperator(trial_space, test_space, mat, P) + + self._matrix[i,j] = mat + + matrix = self._matrix[i,j] + else: # single patch + matrix = self._matrix + + shape = expr.shape + for k1 in range(shape[0]): + for k2 in range(shape[1]): + if expr[k1,k2].is_zero: + continue + + if isinstance(test_fem_space, VectorFemSpace): + ts_space = test_fem_space.get_refined_space(ncells).coeff_space.spaces[k1] + else: + ts_space = test_fem_space.get_refined_space(ncells).coeff_space + + if isinstance(trial_fem_space, VectorFemSpace): + tr_space = trial_fem_space.get_refined_space(ncells).coeff_space.spaces[k2] + else: + tr_space = trial_fem_space.get_refined_space(ncells).coeff_space + + if is_conformal and matrix[k1, k2]: + global_mats[k1, k2] = matrix[k1, k2] + elif not i == j: # assembling in an interface (type(target) == Interface) + axis = target.axis + ext_d = self._trial_ext + ext_c = self._test_ext + test_n = self. test_basis.space.spaces[k1].spaces[axis].nbasis + test_s = self. test_basis.space.spaces[k1].coeff_space.starts[axis] + trial_n = self.trial_basis.space.spaces[k2].spaces[axis].nbasis + cart = self.trial_basis.space.spaces[k2].coeff_space.cart + trial_s = cart.global_starts[axis][cart._coords[axis]] + + s_d = trial_n - trial_s - trial_degree[k2][axis] - 1 if ext_d == 1 else 0 + s_c = test_n - trial_s - test_degree[k1][axis] - 1 if ext_c == 1 else 0 + + # We only handle the case where direction = 1 + direction = target.ornt + if domain.dim == 2: + assert direction == 1 + elif domain.dim == 3: + assert all(d==1 for d in direction) + + direction = 1 + flip = [direction]*domain.dim + flip[axis] = 1 + if self._func != do_nothing: + global_mats[k1, k2] = StencilInterfaceMatrix(tr_space, ts_space, + s_d, s_c, + axis, axis, + ext_d, ext_c, + pads=tuple(pads[k1, k2]), + flip=flip) + else: + global_mats[k1, k2] = StencilMatrix(tr_space, ts_space, pads = tuple(pads[k1, k2])) + + if is_conformal: + matrix[k1, k2] = global_mats[k1, k2] + elif use_restriction: + matrix.multiplicants[-1][k1, k2] = global_mats[k1, k2] + elif use_prolongation: + matrix.multiplicants[0][k1, k2] = global_mats[k1, k2] + + else: # case of scalar equation + if is_broken: # multi-patch + if self._matrix[i, j]: + global_mats[i, j] = self._matrix[i, j] + + elif not i == j: # assembling in an interface (type(target) == Interface) + axis = target.axis + ext_d = self._trial_ext + ext_c = self._test_ext + test_n = self.test_basis.space.spaces[axis].nbasis + test_s = self.test_basis.space.coeff_space.starts[axis] + trial_n = self.trial_basis.space.spaces[axis].nbasis + cart = self.trial_basis.space.coeff_space.cart + trial_s = cart.global_starts[axis][cart._coords[axis]] + + s_d = trial_n - trial_s - trial_degree[axis] - 1 if ext_d == 1 else 0 + s_c = test_n - trial_s - test_degree[axis] - 1 if ext_c == 1 else 0 + + # We only handle the case where direction = 1 + direction = target.ornt + if domain.dim == 2: + assert direction == 1 + elif domain.dim == 3: + assert all(d==1 for d in direction) + + direction = 1 + flip = [direction]*domain.dim + flip[axis] = 1 + + if self._func != do_nothing: + mat = StencilInterfaceMatrix(trial_fem_space.get_refined_space(ncells).coeff_space, + test_fem_space.get_refined_space(ncells).coeff_space, + s_d, s_c, + axis, axis, + ext_d, ext_c, + flip=flip) + if not is_conformal: + if use_restriction: + P = knot_insertion_projection_operator(test_fem_space.get_refined_space(ncells), test_fem_space) + mat = ComposedLinearOperator(trial_space, test_space, P, mat) + elif use_prolongation: + P = knot_insertion_projection_operator(trial_fem_space, trial_fem_space.get_refined_space(ncells)) + mat = ComposedLinearOperator(trial_space, test_space, mat, P) + + global_mats[i, j] = mat + + # define part of the global matrix as a StencilMatrix + else: + global_mats[i, j] = StencilMatrix(trial_space, test_space, pads=tuple(pads)) + + if (i, j) in global_mats: + self._matrix[i, j] = global_mats[i, j] + + + # in single patch case, we define the matrices needed for the patch + else: + if self._matrix: + global_mats[0, 0] = self._matrix + else: + global_mats[0, 0] = StencilMatrix(trial_space, test_space, pads=tuple(pads)) + + self._matrix = global_mats[0, 0] + + # Set the backend of our matrices if given + if backend is not None and is_broken: + for mat in global_mats.values(): + mat.set_backend(backend) + elif backend is not None: + self._matrix.set_backend(backend) + + self._global_matrices = [M._data for M in extract_stencil_mats(global_mats.values())] + + #-------------------------------------------------------------------------- + def assemble(self, *, reset=True, **kwargs): + """ + This method assembles the left hand side Matrix by calling the private method `self._func` with proper arguments. + + In the complex case, this function returns the matrix conjugate. This comes from the fact that the + problem `a(u,v)=b(v)` is discretized as `A @ conj(U) = B` due to the antilinearity of `a` in the first variable. + Thus, to obtain `U`, the assemble function returns `conj(A)`. + + TODO: remove these lines when the dot product is changed for complex. + For now, since the dot product does not compute the conjugate in the complex case. We do not use the conjugate in the assemble function. + It should work if the complex only comes from the `rhs` in the linear form. + """ + + if self._free_args: + basis = [] + spans = [] + degrees = [] + pads = [] + coeffs = [] + consts = [] + + for key in self._free_args: + v = kwargs[key] + + if len(self.domain) > 1 and isinstance(v, FemField) and (v.space.is_multipatch or v.space.is_vector_valued): + assert v.space.is_multipatch ## [MCP 27.03.2025] should hold since len(domain) > 1. If Ok we can simplify above if + i, j = self.get_space_indices_from_target(self.domain, self.target) + assert i == j + v = v[i] + if isinstance(v, FemField): + assert len(self.grid) == 1 + if not v.coeffs.ghost_regions_in_sync: + v.coeffs.update_ghost_regions() + basis_v = BasisValues( + v.space, + nderiv = self.max_nderiv, + nquads = self.nquads, + trial = True, + grid = self.grid[0] + ) + bs, d, s, p, mult = construct_test_space_arguments(basis_v) + basis += bs + spans += s + degrees += [np.int64(a) for a in d] + pads += [np.int64(a) for a in p] + if v.space.is_multipatch or v.space.is_vector_valued: + coeffs += (e._data for e in v.coeffs) + else: + coeffs += (v.coeffs._data, ) + else: + consts += (v, ) + + args = (*self.args, *basis, *spans, *degrees, *pads, *coeffs, *consts) + + else: + args = self._args + + if reset: + reset_arrays(*self.global_matrices) + + self._func(*args, *self._threads_args) + if self._matrix and self._update_ghost_regions: + self._matrix.exchange_assembly_data() + + # TODO : uncomment this line when the conjugate is applied on the dot product in the complex case + #self._matrix.conjugate(out=self._matrix) + + if self._matrix: + self._matrix.ghost_regions_in_sync = False + + return self._matrix + + #-------------------------------------------------------------------------- + @property + def _assembly_template_head(self): + """A template for the 'head' of the assembly function. Only used with the sum factorization algorithm.""" + code = '''def assemble_matrix_{FILE_ID}({MAPPING_PART_1} +{SPAN} {MAPPING_PART_2} + global_x1 : "float64[:,:]", global_x2 : "float64[:,:]", global_x3 : "float64[:,:]", + {MAPPING_PART_3} + n_element_1 : "int64", n_element_2 : "int64", n_element_3 : "int64", + nq1 : "int64", nq2 : "int64", nq3 : "int64", + pad1 : "int64", pad2 : "int64", pad3 : "int64", + {MAPPING_PART_4} +{G_MAT}{NEW_ARGS}{FIELD_ARGS}): + + from numpy import abs as Abs + {imports} +''' + return code + + #-------------------------------------------------------------------------- + @property + def _assembly_template_body_bspline(self): + """A template for the 'body' of the assembly function (when using a spline mapping). Only used with the sum factorization algorithm.""" + code = ''' + arr_coeffs_x = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64') + arr_coeffs_y = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64') + arr_coeffs_z = zeros((1 + test_mapping_p1, 1 + test_mapping_p2, 1 + test_mapping_p3), dtype='float64') + +{F_COEFFS_ZEROS} + +{KEYS} + for k_1 in range(n_element_1): + span_mapping_1 = global_span_mapping_1[k_1] +{LOCAL_SPAN}{F_SPAN_1}{A1} + for q_1 in range(nq1): + for k_2 in range(n_element_2): + span_mapping_2 = global_span_mapping_2[k_2] +{F_SPAN_2} + for q_2 in range(nq2): + for k_3 in range(n_element_3): + span_mapping_3 = global_span_mapping_3[k_3] +{F_SPAN_3}{F_COEFFS} + arr_coeffs_x[:,:,:] = global_arr_coeffs_x[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3] + arr_coeffs_y[:,:,:] = global_arr_coeffs_y[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3] + arr_coeffs_z[:,:,:] = global_arr_coeffs_z[test_mapping_p1 + span_mapping_1 - test_mapping_p1:test_mapping_p1 + 1 + span_mapping_1,test_mapping_p2 + span_mapping_2 - test_mapping_p2:test_mapping_p2 + 1 + span_mapping_2,test_mapping_p3 + span_mapping_3 - test_mapping_p3:test_mapping_p3 + 1 + span_mapping_3] + for q_3 in range(nq3): + x = 0.0 + y = 0.0 + z = 0.0 + + x_x1 = 0.0 + x_x2 = 0.0 + x_x3 = 0.0 + y_x1 = 0.0 + y_x2 = 0.0 + y_x3 = 0.0 + z_x1 = 0.0 + z_x2 = 0.0 + z_x3 = 0.0 +{D2_1} + +{F_INIT} + +{F_ASSIGN_LOOP} + + for i_1 in range(test_mapping_p1+1): + mapping_1 = global_basis_mapping_1[k_1, i_1, 0, q_1] + mapping_1_x1 = global_basis_mapping_1[k_1, i_1, 1, q_1] + {D2_2} + for i_2 in range(test_mapping_p2+1): + mapping_2 = global_basis_mapping_2[k_2, i_2, 0, q_2] + mapping_2_x2 = global_basis_mapping_2[k_2, i_2, 1, q_2] + {D2_3} + for i_3 in range(test_mapping_p3+1): + mapping_3 = global_basis_mapping_3[k_3, i_3, 0, q_3] + mapping_3_x3 = global_basis_mapping_3[k_3, i_3, 1, q_3] + {D2_4} + + coeff_x = arr_coeffs_x[i_1,i_2,i_3] + coeff_y = arr_coeffs_y[i_1,i_2,i_3] + coeff_z = arr_coeffs_z[i_1,i_2,i_3] + + mapping = mapping_1*mapping_2*mapping_3 + mapping_x1 = mapping_1_x1*mapping_2*mapping_3 + mapping_x2 = mapping_1*mapping_2_x2*mapping_3 + mapping_x3 = mapping_1*mapping_2*mapping_3_x3 + +{D2_5} + + x += mapping*coeff_x + y += mapping*coeff_y + z += mapping*coeff_z + + x_x1 += mapping_x1*coeff_x + x_x2 += mapping_x2*coeff_x + x_x3 += mapping_x3*coeff_x + y_x1 += mapping_x1*coeff_y + y_x2 += mapping_x2*coeff_y + y_x3 += mapping_x3*coeff_y + z_x1 += mapping_x1*coeff_z + z_x2 += mapping_x2*coeff_z + z_x3 += mapping_x3*coeff_z + +{D2_6} + +{TEMPS} +{COUPLING_TERMS} +''' + return code + + #-------------------------------------------------------------------------- + @property + def _assembly_template_body_analytic(self): + """A template for the 'body' of the assembly function (when using an analytic or no mapping). Only used with the sum factorization algorithm.""" + code = ''' + local_x1 = zeros_like(global_x1[0,:]) + local_x2 = zeros_like(global_x2[0,:]) + local_x3 = zeros_like(global_x3[0,:]) + +{F_COEFFS_ZEROS} + +{KEYS} + for k_1 in range(n_element_1): + local_x1[:] = global_x1[k_1,:] +{LOCAL_SPAN}{F_SPAN_1}{A1} + for q_1 in range(nq1): + x1 = local_x1[q_1] + for k_2 in range(n_element_2): + local_x2[:] = global_x2[k_2,:] +{F_SPAN_2} + for q_2 in range(nq2): + x2 = local_x2[q_2] + for k_3 in range(n_element_3): + local_x3[:] = global_x3[k_3,:] +{F_SPAN_3}{F_COEFFS} + for q_3 in range(nq3): + x3 = local_x3[q_3] + +{F_INIT} + +{F_ASSIGN_LOOP} + +{TEMPS} +{COUPLING_TERMS} +''' + return code + + #-------------------------------------------------------------------------- + @property + def _assembly_template_loop(self): + """A template for the 'loop' of the assembly function. Only used with the sum factorization algorithm.""" + code = ''' + {A2}[:] = 0.0 + for k_2 in range(n_element_2): + {SPAN_2} = {GLOBAL_SPAN_2}[k_2] + for q_2 in range(nq2): + {A3}[:] = 0.0 + for k_3 in range(n_element_3): + {SPAN_3} = {GLOBAL_SPAN_3}[k_3] + for q_3 in range(nq3): + a4 = {COUPLING_TERMS}[k_2, q_2, k_3, q_3, :] + for i_3 in range({TEST_V_P3} + 1): + for j_3 in range({TRIAL_U_P3} + 1): + for e in range({NEXPR}): + {A3}[e, {SPAN_3} - {TEST_V_P3} + i_3, {MAX_P3} - {I_3} + j_3] += {TEST_TRIAL_3}[k_3, q_3, i_3, j_3, {KEYS_3}[2*e], {KEYS_3}[2*e+1]] * a4[e] + for i_2 in range({TEST_V_P2} + 1): + for j_2 in range({TRIAL_U_P2} + 1): + for e in range({NEXPR}): + {A2}[e, {SPAN_2} - {TEST_V_P2} + i_2, :, {MAX_P2} - {I_2} + j_2, :] += {TEST_TRIAL_2}[k_2, q_2, i_2, j_2, {KEYS_2}[2*e], {KEYS_2}[2*e+1]] * {A3}[e,:,:] + for i_1 in range({TEST_V_P1} + 1): + for j_1 in range({TRIAL_U_P1} + 1): + {A1}[i_1, :, :, {MAX_P1} - {I_1} + j_1, :, :] += {A2_TEMP} +''' + return code + + #-------------------------------------------------------------------------- + def make_file(self, temps, ordered_stmts, field_derivatives, max_logical_derivative, test_mult, trial_mult, test_v_p, trial_u_p, keys_1, keys_2, keys_3, mapping_option): + """ + Part of the sum factorization algorithm implementation. + Generates the correct assembly file. + Used at the end of construct_arguments_generate_assembly_file, before eventually pyccelizing that file. + + Parameters + ---------- + temps : tuple + Tuple of Assign statements defining temporary values. + Arithmetic combinations of these make up the coupling terms. + + ordered_stmts : dict + Dictionary defining the coupling terms. Keys are combinations of + test and trial function components, values are Assign statements + in terms of temporaries appearing in temps. + + field_derivatives : dict + Dictionary containing information on the derivatives of free FemFields. + Keys are components of free FemFields. Values are dictionaries again. + Their keys are names, as appearing in the assembly file, of partial derivatives of the + corresponding FemField component, and their values are dictionaries again. + Example: {F1_0_x3 : {'x1': 0, 'x2': 0, 'x3': 1}, F1_0_x2 : ...} + Meaning: There exists a free FemField named F1. Among other, the partial derivative w.r.t. x3 + of its first component F1_0 appears. + + max_logical_derivative : int + The largest appearing derivative order. + + test_mult : list + List of length 3(scalar test function) or 9(vector test function) including multiplicity information. + + trial_mult : list + List of length 3(scalar trial function) or 9(vector trial function) including multiplicity information. + + test_v_p : dict + Dictionary of length 1(scalar test function) or length 3(vector test function). + Each key corresponds to a component of the funciton (space), and each corresponding value + is a list of Bspline degrees of length 3. Example: Discretizing a de de Rham sequence using + a degree vector [2, 3, 4] means that test_v_p for a test function belonging to H(curl) will be + {0: [1, 3, 4], 1: [2, 2, 4], 2: [2, 3, 3]} + + trial_u_p : dict + Dictionary of length 1(scalar trial function) or length 3(vector trial function). + Each key corresponds to a component of the funciton (space), and each corresponding value + is a list of Bspline degrees of length 3. Example: Discretizing a de de Rham sequence using + a degree vector [2, 3, 4] means that trial_u_p for a trial function belonging to H^1 will be + {0: [2, 3, 4]} + + keys_1 : dict + Dictionary relating subexpressions to x1-derivative combinations. + Keys are combinations of test and trial function components. + Values are lists, each entry corresponding to one appearing partial derivative + combination of these components. + Example: keys_1[(u[0], v[1])][3] = [1,0] means that the fourth ([3]) + sub-expression (partial derivative combination) corresponding to the trial-test-function-component-product + u[0] * v[1] involves a first derivative in x1 direction of the trial function + and no derivative in x1 direction of the test function. + Information on appearing partial derivatives in x2 and x3 direction is stored in keys_2 and keys_3. + + keys_2 : dict + See keys_1. + + keys_3 : dict + See keys_1. + + mapping_option : None | 'Bspline' + None in case of no mapping or an analytical mapping, 'Bspline' in case of a Bspline mapping. + + Returns + ------- + + file_id : str + random string of length 8, corresponding to the assembly file name located in __psydac__/ + + """ + + #------------------------- FILE_ID ------------------------- + comm = self.comm + + # Root process generates a random string to be used as file_id + if comm is None or comm.rank == 0: + file_id = random_string(size=8) + else: + file_id = None + + # Parallel case: root process broadcasts file_id to all processes + if comm is not None and comm.size > 1: + file_id = comm.bcast(file_id, root=0) + + # ----- free FemField related strings ----- + + # used as {FIELD_ARGS} in _assembly_template_head + # adding the right arguments for free FemFields to the assembly function header + basis_args_block = [f'global_test_basis_'+'{field}'+f'_{i+1} : "float64[:,:,:,:]"' for i in range(3)] + basis_args_block = ", ".join(basis_args_block) + "," + basis_args_block = [basis_args_block.format(field=field) for field in field_derivatives] + basis_args = " " + "\n ".join(basis_args_block) + "\n" + span_args_block = [f'global_span_'+'{field}'+f'_{i+1} : "int64[:]"' for i in range(3)] + span_args_block = ", ".join(span_args_block) + "," + span_args_block = [span_args_block.format(field=field) for field in field_derivatives] + span_args = " " + "\n ".join(span_args_block) + "\n" + degree_args_block = [f'test_'+'{field}'+f'_p{i+1} : "int64"' for i in range(3)] + degree_args_block = ", ".join(degree_args_block) + "," + degree_args_block = [degree_args_block.format(field=field) for field in field_derivatives] + degree_args = " " + "\n ".join(degree_args_block) + "\n" + pad_args_block = [f'pad_'+'{field}'+f'_{i+1} : "int64"' for i in range(3)] + pad_args_block = ", ".join(pad_args_block) + "," + pad_args_block = [pad_args_block.format(field=field) for field in field_derivatives] + pad_args = " " + "\n ".join(pad_args_block) + "\n" + coeff_args_block = [f'global_arr_coeffs_{field} : "float64[:,:,:]"' for field in field_derivatives] + coeff_args = " " + ", ".join(coeff_args_block) + FIELD_ARGS = basis_args+span_args+degree_args+pad_args+coeff_args + + # {F_COEFFS_ZEROS} in both _assembly_template_body_bspline & _analytic + F_COEFFS_ZEROS = "\n".join([f" arr_coeffs_{field} = zeros((1 + test_{field}_p1, 1 + test_{field}_p2, 1 + test_{field}_p3), dtype='float64')" for field in field_derivatives]) + + # {F_SPAN_1}, {F_SPAN_2}, {F_SPAN_3} in both _assembly_template_body_bspline & _analytic + F_SPAN_1 = "\n".join([f" span_{field}_1 = global_span_{field}_1[k_1]" for field in field_derivatives]) + "\n" + F_SPAN_2 = "\n".join([f" span_{field}_2 = global_span_{field}_2[k_2]" for field in field_derivatives]) + "\n" + F_SPAN_3 = "\n".join([f" span_{field}_3 = global_span_{field}_3[k_3]" for field in field_derivatives]) + "\n" + + # {F_COEFFS} in both _assembly_template_body_bspline & _analytic + coeff_ranges = ", ".join([f"pad_"+"{field}"+f"_{i+1} + span_"+"{field}"+f"_{i+1} - test_"+"{field}"+f"_p{i+1}:1 + pad_"+"{field}"+f"_{i+1} + span_"+"{field}"+f"_{i+1}" for i in range(3)]) + F_COEFFS = "\n".join([f" arr_coeffs_{field}[:,:,:] = global_arr_coeffs_{field}[{coeff_ranges.format(field=field)}]" for i, field in enumerate(field_derivatives)]) + + # {F_INIT} + F_INIT = "\n".join([f" {derivative} = 0.0" for field in field_derivatives for derivative in field_derivatives[field]]) + + # + # field_init assigns 0 to appearing free FemField derivatives (F_x1 = 0.0 \n F_x2 = 0.0 \n ...) + # In the following, we assemble loops that correctly compute those free FemField derivatives at + # a specific quadrature point (q_1, q_2, q_3). Those values will then be used in the computation + # of the temps or directly in the computation of the coupling terms + # + assign_loop_contents = {'1':{}, '2':{}, '3':{}} + multiplication_info = {} + + for field, derivatives in field_derivatives.items(): + multiplication_info[field] = {} + assign_statements = {'1':[], '2':[], '3':[]} + for derivative, dxs in derivatives.items(): + multiplication_info[field][derivative] = [] + dx1 = dxs['x1'] + dx2 = dxs['x2'] + dx3 = dxs['x3'] + for i, dx in enumerate([dx1, dx2, dx3]): + name = f"{field}_{i+1}" if dx == 0 else f"{field}_{i+1}_{dx*f'x{i+1}'}" + multiplication_info[field][derivative].append(name) + if dx == 0: + assign_statement = f"{name} = global_test_basis_{field}_{i+1}[k_{i+1}, i_{i+1}, 0, q_{i+1}]" + else: + assign_statement = f"{name} = global_test_basis_{field}_{i+1}[k_{i+1}, i_{i+1}, {dx}, q_{i+1}]" + if assign_statement not in assign_statements[f"{i+1}"]: + assign_statements[f"{i+1}"].append(assign_statement) + for i in range(3): + content = ("\n"+(8+i)*" ").join(assign_statements[f"{i+1}"]) + assign_loop_contents[f"{i+1}"][field] = content + tab = 7*" " + assign = [] + for field in field_derivatives: + txt = f"{tab}for i_1 in range(1 + test_{field}_p1):\n" + \ + f"{tab} {assign_loop_contents['1'][field]}\n" + \ + f"{tab} for i_2 in range(1 + test_{field}_p2):\n" + \ + f"{tab} {assign_loop_contents['2'][field]}\n" + \ + f"{tab} for i_3 in range(1 + test_{field}_p3):\n" + \ + f"{tab} {assign_loop_contents['3'][field]}\n" + \ + f"{tab} coeff_{field} = arr_coeffs_{field}[i_1, i_2, i_3]\n" + for derivative in multiplication_info[field]: + factors = " * ".join(multiplication_info[field][derivative]) + txt += f"{tab} {derivative} += {factors} * coeff_{field}\n" + txt += "\n" + assign.append(txt) + + # {F_ASSIGN_LOOP} in both _assembly_template_body_bspline & _analytic + F_ASSIGN_LOOP = "\n".join(assign) + + # ----------------------------------------- + + # ----- load the templates ----- + # + # head for the function header and imports + # body for the computation of coupling terms + # loop (part of function body): one loop per block ( e.g. (u[0], v[1]) ), each loop effectively + # assembles one StencilMatrix per sub expression ( e.g. (dx1(u[0]), dx3(v[1])) ) + code_head = self._assembly_template_head + code_loop = self._assembly_template_loop + if mapping_option == 'Bspline': + code_body = self._assembly_template_body_bspline + else: + code_body = self._assembly_template_body_analytic + # ------------------------------ + + # ---- obtain basic information not explicitely passed in the args ----- + blocks = ordered_stmts.keys() + block_list = list(blocks) + trial_components = [block[0] for block in block_list] + test_components = [block[1] for block in block_list] + nu = len(set(trial_components)) + nv = len(set(test_components)) + d = 3 + assert d == 3 + # ---------------------------------------------------------------------- + + # Prepare strings and string templates depending on whether the trial and test function are vector-valued or not (nu, nv > 1 or == 1) + + # ------------------------- STRINGS HEAD ------------------------- + + global_span_v_str = 'global_span_v_{v_j}_' if nv > 1 else 'global_span_v_' + + if mapping_option == 'Bspline': + MAPPING_PART_1 = 'global_basis_mapping_1 : "float64[:,:,:,:]", global_basis_mapping_2 : "float64[:,:,:,:]", global_basis_mapping_3 : "float64[:,:,:,:]", ' + MAPPING_PART_2 = 'global_span_mapping_1 : "int64[:]", global_span_mapping_2 : "int64[:]", global_span_mapping_3 : "int64[:]", ' + MAPPING_PART_3 = 'test_mapping_p1 : "int64", test_mapping_p2 : "int64", test_mapping_p3 : "int64", ' + MAPPING_PART_4 = 'global_arr_coeffs_x : "float64[:,:,:]", global_arr_coeffs_y : "float64[:,:,:]", global_arr_coeffs_z : "float64[:,:,:]", ' + else: + MAPPING_PART_1 = '' + MAPPING_PART_2 = '' + MAPPING_PART_3 = '' + MAPPING_PART_4 = '' + + if nv > 1: + tt1_str = 'test_trial_1_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_1_u_v_{v_j}' + tt2_str = 'test_trial_2_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_2_u_v_{v_j}' + tt3_str = 'test_trial_3_u_{u_i}_v_{v_j}' if nu > 1 else 'test_trial_3_u_v_{v_j}' + a3_str = 'a3_u_{u_i}_v_{v_j}' if nu > 1 else 'a3_u_v_{v_j}' + a2_str = 'a2_u_{u_i}_v_{v_j}' if nu > 1 else 'a2_u_v_{v_j}' + ct_str = 'coupling_terms_u_{u_i}_v_{v_j}' if nu > 1 else 'coupling_terms_u_v_{v_j}' + g_mat_str = 'g_mat_u_{u_i}_v_{v_j}' if nu > 1 else 'g_mat_u_v_{v_j}' + else: + tt1_str = 'test_trial_1_u_{u_i}_v' if nu > 1 else 'test_trial_1_u_v' + tt2_str = 'test_trial_2_u_{u_i}_v' if nu > 1 else 'test_trial_2_u_v' + tt3_str = 'test_trial_3_u_{u_i}_v' if nu > 1 else 'test_trial_3_u_v' + a3_str = 'a3_u_{u_i}_v' if nu > 1 else 'a3_u_v' + a2_str = 'a2_u_{u_i}_v' if nu > 1 else 'a2_u_v' + ct_str = 'coupling_terms_u_{u_i}_v' if nu > 1 else 'coupling_terms_u_v' + g_mat_str = 'g_mat_u_{u_i}_v' if nu > 1 else 'g_mat_u_v' + + # ---------------------------------------------------------------- + + # ------------------------- STRINGS BODY ------------------------- + + span_v_1_str = 'span_v_{v_j}_1' if nv > 1 else 'span_v_1' + test_v_p1_str = 'test_v_{v_j}_p1' if nv > 1 else 'test_v_p1' + + if nv > 1: + keys_2_str = 'keys_2_u_{u_i}_v_{v_j}' if nu > 1 else 'keys_2_u_v_{v_j}' + keys_3_str = 'keys_3_u_{u_i}_v_{v_j}' if nu > 1 else 'keys_3_u_v_{v_j}' + a1_str = 'a1_u_{u_i}_v_{v_j}' if nu > 1 else 'a1_u_v_{v_j}' + else: + keys_2_str = 'keys_2_u_{u_i}_v' if nu > 1 else 'keys_2_u_v' + keys_3_str = 'keys_3_u_{u_i}_v' if nu > 1 else 'keys_3_u_v' + a1_str = 'a1_u_{u_i}_v' if nu > 1 else 'a1_u_v' + + # ---------------------------------------------------------------- + + # ------------------------- STRINGS LOOP ------------------------- + + span_2_str = 'span_v_{v_j}_2' if nv > 1 else 'span_v_2' + span_3_str = 'span_v_{v_j}_3' if nv > 1 else 'span_v_3' + global_span_2_str = 'global_span_v_{v_j}_2' if nv > 1 else 'global_span_v_2' + global_span_3_str = 'global_span_v_{v_j}_3' if nv > 1 else 'global_span_v_3' + + # ---------------------------------------------------------------- + + #------------------------- MAKE HEAD ------------------------- + SPAN = '' + G_MAT = '' + + TT1 = ' ' + TT2 = ' ' + TT3 = ' ' + A3 = ' ' + A2 = ' ' + CT = ' ' + + for v_j in range(nv): + global_span_v = global_span_v_str.format(v_j=v_j) + SPAN += ' ' + for di in range(d): + SPAN += f'{global_span_v}{di+1} : "int64[:]", ' + SPAN = SPAN[:-1] + '\n' + + for block in blocks: + u_i = block[0].indices[0] if nu > 1 else 0 + v_j = block[1].indices[0] if nv > 1 else 0 + + # reverse order intended + if ((nu > 1) and (nv > 1)): + g_mat = g_mat_str.format(u_i=v_j, v_j=u_i) + else: + g_mat = g_mat_str.format(u_i=u_i, v_j=v_j) + G_MAT += f' {g_mat} : "float64[:,:,:,:,:,:]",\n' + + TT1 += tt1_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", ' + TT2 += tt2_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", ' + TT3 += tt3_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:,:]", ' + A3 += a3_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:]", ' + A2 += a2_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:]", ' + CT += ct_str.format(u_i=u_i, v_j=v_j) + ' : "float64[:,:,:,:,:]", ' + + TT1 += '\n' + TT2 += '\n' + TT3 += '\n' + A3 += '\n' + A2 += '\n' + CT += '\n' + NEW_ARGS = TT1 + TT2 + TT3 + A3 + A2 + CT + IMPORTS = self._imports_string + + head = code_head.format(FILE_ID = file_id, + SPAN = SPAN, + G_MAT = G_MAT, + NEW_ARGS = NEW_ARGS, + MAPPING_PART_1 = MAPPING_PART_1, + MAPPING_PART_2 = MAPPING_PART_2, + MAPPING_PART_3 = MAPPING_PART_3, + MAPPING_PART_4 = MAPPING_PART_4, + FIELD_ARGS = FIELD_ARGS, + imports = IMPORTS) + + #------------------------- MAKE BODY ------------------------- + A1 = '' + KEYS_2 = '' + KEYS_3 = '' + LOCAL_SPAN = '' + TEMPS = '' + COUPLING_TERMS = '' + + for block in blocks: + u_i = block[0].indices[0] if nu > 1 else 0 + v_j = block[1].indices[0] if nv > 1 else 0 + + keys2 = keys_2[block].copy() + keys3 = keys_3[block].copy() + keys2 = ','.join(str(i) for i in keys2.flatten()) + keys3 = ','.join(str(i) for i in keys3.flatten()) + KEYS2 = keys_2_str.format(u_i=u_i, v_j=v_j) + KEYS3 = keys_3_str.format(u_i=u_i, v_j=v_j) + KEYS_2 += f' {KEYS2} = array([{keys2}])\n' + KEYS_3 += f' {KEYS3} = array([{keys3}])\n' + + test_v_p1, test_v_p2, test_v_p3 = test_v_p[v_j] + a1 = a1_str.format(u_i=u_i, v_j=v_j) + g_mat = g_mat_str.format(u_i=u_i, v_j=v_j) + TEST_V_P1 = test_v_p1_str.format(v_j=v_j) + SPAN_V_1 = span_v_1_str.format(v_j=v_j) + + A1_1 = f'{test_mult[0]}*pad1 + {SPAN_V_1} - {test_v_p1} : {test_mult[0]}*pad1 + {SPAN_V_1} + 1' if test_mult[0] > 1 else f'pad1 + {SPAN_V_1} - {test_v_p1} : pad1 + {SPAN_V_1} + 1' + A1_2 = f'{test_mult[1]}*pad2 : {test_mult[1]}*pad2 + n_element_2 + {test_v_p2} + ({test_mult[1]}-1)*(n_element_2-1)' if test_mult[1] > 1 else f'pad2 : pad2 + n_element_2 + {test_v_p2}' + A1_3 = f'{test_mult[2]}*pad3 : {test_mult[2]}*pad3 + n_element_3 + {test_v_p3} + ({test_mult[2]}-1)*(n_element_3-1)' if test_mult[2] > 1 else f'pad3 : pad3 + n_element_3 + {test_v_p3}' + A1 += f' {a1} = {g_mat}[{A1_1}, {A1_2}, {A1_3}, :, :, :]\n' + + for v_j in range(nv): + local_span_v_1 = span_v_1_str.format(v_j=v_j) + global_span_v = global_span_v_str.format(v_j=v_j) + LOCAL_SPAN += f' {local_span_v_1} = {global_span_v}1[k_1]\n' + + for temp in temps: + TEMPS += f' {temp.lhs} = {temp.rhs}\n' + for block in blocks: + for stmt in ordered_stmts[block]: + COUPLING_TERMS += f' {stmt.lhs} = {stmt.rhs}\n' + + KEYS = KEYS_2 + KEYS_3 + + # This part is interesting. Right now, below you find hardcoded rules regarding lines of code + # that need to be included when max_logical_derivative == 2 ( and mapping_option == 'Bspline'). + # E.g., the bilinear form corresponding to a bilaplacian problem ( laplace(laplace(u)) = f ) satisfies this assumption. + # This hardcoded set of rules could be generalized to n-th max derivatives - if needed! + # But for now, higher than second order derivatives on either trial or test function are not supported. + # + # Additional note: Given a Bspline mapping, the code computing the first order derivatives of mapping related terms is always required! + # Even in the case of a trivial bilinear form without derivatives. But only when there are second order partial derivatives involved + # do we need to compute second derivatives of the mapping (chain rule). + if (mapping_option == 'Bspline') and (max_logical_derivative == 2): + D2_1 = '\n' + spaces1 = ' ' + spaces2 = spaces1 + ' ' + for symbol in ('x', 'y', 'z'): + for d1 in range(1, 4): + for d2 in range(1, 4): + if d2 >= d1: + D2_1 += spaces1 + f'{symbol}_x{d1}x{d2} = 0.0\n' + D2_1 += '\n' + D2_2 = 'mapping_1_x1x1 = global_basis_mapping_1[k_1, i_1, 2, q_1]' + D2_3 = 'mapping_2_x2x2 = global_basis_mapping_2[k_2, i_2, 2, q_2]' + D2_4 = 'mapping_3_x3x3 = global_basis_mapping_3[k_3, i_3, 2, q_3]' + D2_5 = spaces2+'mapping_x1x1 = mapping_1_x1x1 * mapping_2 * mapping_3\n'+spaces2 + D2_5 += 'mapping_x1x2 = mapping_1_x1 * mapping_2_x2 * mapping_3\n'+spaces2 + D2_5 += 'mapping_x1x3 = mapping_1_x1 * mapping_2 * mapping_3_x3\n'+spaces2 + D2_5 += 'mapping_x2x2 = mapping_1 * mapping_2_x2x2 * mapping_3\n'+spaces2 + D2_5 += 'mapping_x2x3 = mapping_1 * mapping_2_x2 * mapping_3_x3\n'+spaces2 + D2_5 += 'mapping_x3x3 = mapping_1 * mapping_2 * mapping_3_x3x3\n' + D2_6 = '' + for symbol in ('x', 'y', 'z'): + for d1 in range(1, 4): + for d2 in range(1, 4): + if d2 >= d1: + D2_6 += f'{spaces2}{symbol}_x{d1}x{d2} += mapping_x{d1}x{d2} * coeff_{symbol}\n' + D2_6 += '\n' + else: + D2_1 = '' + D2_2 = '' + D2_3 = '' + D2_4 = '' + D2_5 = '' + D2_6 = '' + + body = code_body.format(LOCAL_SPAN = LOCAL_SPAN, + KEYS = KEYS, + A1 = A1, + TEMPS = TEMPS, + COUPLING_TERMS = COUPLING_TERMS, + F_COEFFS_ZEROS = F_COEFFS_ZEROS, + F_SPAN_1 = F_SPAN_1, + F_SPAN_2 = F_SPAN_2, + F_SPAN_3 = F_SPAN_3, + F_COEFFS = F_COEFFS, + F_INIT = F_INIT, + F_ASSIGN_LOOP = F_ASSIGN_LOOP, + D2_1 = D2_1, + D2_2 = D2_2, + D2_3 = D2_3, + D2_4 = D2_4, + D2_5 = D2_5, + D2_6 = D2_6) + + #------------------------- MAKE LOOP ------------------------- + assembly_code = head + body + loop_str = '' + + for block in blocks: + u_i = block[0].indices[0] if nu > 1 else 0 + v_j = block[1].indices[0] if nv > 1 else 0 + + A1 = a1_str.format(u_i=u_i, v_j=v_j) + A2 = a2_str.format(u_i=u_i, v_j=v_j) + A3 = a3_str.format(u_i=u_i, v_j=v_j) + TEST_TRIAL_2 = tt2_str.format(u_i=u_i, v_j=v_j) + TEST_TRIAL_3 = tt3_str.format(u_i=u_i, v_j=v_j) + SPAN_2 = span_2_str.format(u_i=u_i, v_j=v_j) + SPAN_3 = span_3_str.format(u_i=u_i, v_j=v_j) + GLOBAL_SPAN_2 = global_span_2_str.format(u_i=u_i, v_j=v_j) + GLOBAL_SPAN_3 = global_span_3_str.format(u_i=u_i, v_j=v_j) + KEYS_3 = keys_3_str.format(u_i=u_i, v_j=v_j) + KEYS_2 = keys_2_str.format(u_i=u_i, v_j=v_j) + COUPLING_TERMS = ct_str.format(u_i=u_i, v_j=v_j) + + TEST_V_P1, TEST_V_P2, TEST_V_P3 = test_v_p[v_j] + TRIAL_U_P1, TRIAL_U_P2, TRIAL_U_P3 = trial_u_p[u_i] + MAX_P1 = max(TEST_V_P1, TRIAL_U_P1) + MAX_P2 = max(TEST_V_P2, TRIAL_U_P2) + MAX_P3 = max(TEST_V_P3, TRIAL_U_P3) + NEXPR = len(ordered_stmts[block]) + + keys1 = keys_1[block] + TEST_TRIAL_1 = tt1_str.format(u_i=u_i, v_j=v_j) + A2_TEMP = " + ".join([f"{TEST_TRIAL_1}[k_1, q_1, i_1, j_1, {keys1[e][0]}, {keys1[e][1]}] * {A2}[{e},:,:,:,:]" for e in range(NEXPR)]) + + I_1 = f'int(floor(i_1/{test_mult[0]})*{trial_mult[0]})' if max(test_mult[0], trial_mult[0]) > 1 else 'i_1' + I_2 = f'int(floor(i_2/{test_mult[1]})*{trial_mult[1]})' if max(test_mult[1], trial_mult[1]) > 1 else 'i_2' + I_3 = f'int(floor(i_3/{test_mult[2]})*{trial_mult[2]})' if max(test_mult[2], trial_mult[2]) > 1 else 'i_3' + #MAX_P1 = max(int( ( MAX_P1 + np.floor(MAX_P1 / test_mult[0]) * trial_mult[0] ) / 2 ), MAX_P1) if max(test_mult[0], trial_mult[0]) > 1 else MAX_P1 + #MAX_P2 = max(int( ( MAX_P2 + np.floor(MAX_P2 / test_mult[1]) * trial_mult[1] ) / 2 ), MAX_P2) if max(test_mult[1], trial_mult[1]) > 1 else MAX_P2 + #MAX_P3 = max(int( ( MAX_P3 + np.floor(MAX_P3 / test_mult[2]) * trial_mult[2] ) / 2 ), MAX_P3) if max(test_mult[2], trial_mult[2]) > 1 else MAX_P3 + n_cols_x1 = max( int(MAX_P1 + 1 + np.floor(MAX_P1 / test_mult[0]) * trial_mult[0]), 2*MAX_P1+1 ) + n_cols_x2 = max( int(MAX_P2 + 1 + np.floor(MAX_P2 / test_mult[1]) * trial_mult[1]), 2*MAX_P2+1 ) + n_cols_x3 = max( int(MAX_P3 + 1 + np.floor(MAX_P3 / test_mult[2]) * trial_mult[2]), 2*MAX_P3+1 ) + MAX_P1 = n_cols_x1 - MAX_P1 - 1 + MAX_P2 = n_cols_x2 - MAX_P2 - 1 + MAX_P3 = n_cols_x3 - MAX_P3 - 1 + + loop = code_loop.format(A1 = A1, + A2 = A2, + A3 = A3, + TEST_TRIAL_2 = TEST_TRIAL_2, + TEST_TRIAL_3 = TEST_TRIAL_3, + SPAN_2 = SPAN_2, + SPAN_3 = SPAN_3, + GLOBAL_SPAN_2 = GLOBAL_SPAN_2, + GLOBAL_SPAN_3 = GLOBAL_SPAN_3, + KEYS_2 = KEYS_2, + KEYS_3 = KEYS_3, + COUPLING_TERMS = COUPLING_TERMS, + TEST_V_P1 = TEST_V_P1, + TEST_V_P2 = TEST_V_P2, + TEST_V_P3 = TEST_V_P3, + TRIAL_U_P1 = TRIAL_U_P1, + TRIAL_U_P2 = TRIAL_U_P2, + TRIAL_U_P3 = TRIAL_U_P3, + MAX_P1 = MAX_P1, + MAX_P2 = MAX_P2, + MAX_P3 = MAX_P3, + NEXPR = NEXPR, + A2_TEMP = A2_TEMP, + I_1 = I_1, + I_2 = I_2, + I_3 = I_3) + + loop_str += loop + + assembly_code += loop_str + assembly_code += '\n return\n' + + #------------------------- MAKE FILE ------------------------- + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') + + # Root process writes the assembly code to a file + if comm is None or comm.rank == 0: + filename = f'__psydac__/assemble_{file_id}.py' + f = open(filename, 'w') + f.writelines(assembly_code) + f.close() + + # Parallel case: wait for the file to be closed before proceeding + if comm is not None and comm.size > 1: + _ = comm.bcast(None, root=0) + + return file_id + + #-------------------------------------------------------------------------- + def read_BilinearForm(self): + """ + Part of the sum factorization algorithm implementation. + Used at the beginning of construct_arguments_generate_assembly_file(). + It's output determines both the design of the assembly function, and the arguments passed to it. + + Returns + ------- + + temps : tuple + tuple of Assign objects. Often times usable building blocks of complicated coupling terms. + + ordered_stmts : dict + assigns each block (trial&test component combination) a list of coupling term assignment + + ordered_sub_exprs_keys : dict + relates each coupling term assignment of ordered_stmts a partial derivative combination + + mapping_option : str | None + 'Bspline' if a spline mapping is involved, None if an analytical or no mapping is involved + + field_derivatives : dict + contains information regarding appearing free FemFields and appearing partial derivatives of those + + g_mat_information_false : list + possibly wrong list of non-zero blocks + + g_mat_information_true : list + correct list of non-zero blocks + + max_logical_derivative : int + maximum appearing partial derivative (in any fixed direction) + + """ + + a = self.expr + domain = a.domain + + # Because an analytical mapping only changes the expression, only the case of a Bspline mapping has to be treated + # entirely different + mapping_option = 'Bspline' if isinstance(self._mapping, SplineMapping) else None + + # The following are tuples consisting of test, trial and free FemField functions appearing, e.g. + # u, v, F1, F2 = elements_of(V, names='u, v, F1, F2) + # a = BilinearForm((u, v), integral(domain, dot(u, F1) * dot(v, F2))) + # tests = (v, ), trials = (u, ) fields = (F1, F2) - Note: The order of F1 & F2 is apparently random and changes from time to time! + # tuple entries are either sympde.topology.space.ScalarFunction or sympde.topology.space.VectorFunction objects + tests = a.test_functions + trials = a.trial_functions + fields = a.fields + + # A sympde.expr.evaluation.DomainExpression object + # TODO [YG 31.07.2025]: Why not using self.terminal_expr[0] instead? + texpr = TerminalExpr(a, domain)[0] + + # We extract all appearing components of test, trial and free FemFields, as well as appearing partial derivatives of these. + # e.g. atoms = [F1[1], F2[1], v[0], u[0], F1[2], F2[2], F1[0], v[1], v[2], F2[0], u[1], u[2]] + # for a bilinear form, without derivatives, involving two vector valued Fem fields F1 & F2 and vector valued test & trial functions v and u + atoms_types = (ScalarFunction, VectorFunction, IndexedVectorFunction) + atoms = _atomic(texpr, cls=atoms_types+_logical_partial_derivatives) + + # Preparing to sort all atoms into test_, trial_ and field_atoms + test_atoms = {} + for v in tests: + if isinstance(v, VectorFunction): + for i in range(domain.dim): + test_atoms[v[i]] = [] + else: + test_atoms[v] = [] + + trial_atoms = {} + for u in trials: + if isinstance(u, VectorFunction): + for i in range(domain.dim): + trial_atoms[u[i]] = [] + else: + trial_atoms[u] = [] + + field_atoms = {} + for f in fields: + if isinstance(f, VectorFunction): + for i in range(domain.dim): + field_atoms[f[i]] = [] + else: + field_atoms[f] = [] + + # atoms can consist of scalar functions (u, v), partial derivatives of scalar functions (dx1(u), dx3(v), ...), + # components of vector valued functions (u[0], v[1], ...), partial derivatives of components of vector valued functions + # (dx1(u[0]), dx3(v[1]), ...), and the same thing but for free FemFields. + # With + # get_atom_logical_derivatives(atom) + # we obtain the component without partial derivatives (u -> u ; dx1(u) -> u ; dx2(v[2]) -> v[2] ; ...) + # This way we can gather subexpressions belonging to the same block + for atom in atoms: + a = get_atom_logical_derivatives(atom) + # IF: NOT Indexed Mapping AND NOT VectorFunction + # I guess: <=> IF ScalarFunction + if not ((isinstance(a, Indexed) and isinstance(a.base, Mapping)) or (isinstance(a, IndexedVectorFunction))): + if a in tests: + # tests is a tuple, e.g. (v, ), hence tests[0] = v + test_atoms[tests[0]].append(atom) + elif a in trials: + trial_atoms[trials[0]].append(atom) + elif a in fields: + # while there can only be one trial and one test function, there can be multiple free FemFields. + for f in field_atoms: + if f == a: + field_atoms[f].append(atom) + else: + raise NotImplementedError(f"atoms of type {str(atom)} are not supported") + # IF VectorFunction + elif isinstance(a, IndexedVectorFunction): + # .base returns ... the base of a VectorFunction! E.g., u[2] -> u, v[0] -> v + if a.base in tests: + for vi in test_atoms: + if vi == a: + test_atoms[vi].append(atom) + break + elif a.base in trials: + for ui in trial_atoms: + if ui == a: + trial_atoms[ui].append(atom) + break + elif a.base in fields: + for fi in field_atoms: + if fi == a: + field_atoms[fi].append(atom) + break + else: + raise NotImplementedError(f"atoms of type {str(atom)} are not supported") + + # ----- Julian O. 11.06.25 ----- + # Regarding the code that follows: + # When dealing with a DiscreteBilinearForm depending on two or more free FemFields, + # the order of the dictionary `field_derivatives` must be the same as the order + # of the free FemFields in `self._free_args`. + # For some reason, the order of all appearing "atoms" in a BilinearForm (trial function, test function, free fields, .?.) + # as obtained in the __init__ of AST + # atoms = terminal_expr.expr.atoms(ScalarFunction, VectorFunction) + # is random and changes from code execution to code execution. + # This order of atoms however determines the order of the free FemFields appearing in `self._free_args`. + # In particular, this order only sometimes matches the order of `field_derivatives`, which results in wrong matrices. + # + # Below is the old version of the code that follows: + #field_derivatives = {} + #for key in field_atoms: + # sym_key = SymbolicExpr(key) + # field_derivatives[sym_key] = {} + # for f in field_atoms[key]: + # field_derivatives[sym_key][SymbolicExpr(f)] = get_index_logical_derivatives(f) + # ------------------------------ + + # For the computation of the coupling terms, among other we need to organize information + # related to free FemFields. For now, we have the dictionary field_atoms, whose keys are + # components of appearing fields, and whose values are appearing partial derivatives of these, e.g., + # field_atoms = {'F1[0]':[dx1(F1[0]), ], 'F1[1]':[dx2(F1[1]), ], 'F1[2]':[dx3(F1[2]), ], 'F2':[F2, ]} + # + # We now create the dictionary field_derivatives. + # It's keys are SymbolicExpr of the previous keys (F1[0] -> F1_0, F1[1] -> F1_1, F1[2] -> F1_2, F2 -> F2) + # and its values are again dictionaries, whose keys are symbolic expressions of the appearing partial derivatives, e.g. + # dx1(F1[0]) -> F1_0_x1, dx2(F1[1]) -> F1_1_x2, dx3(F1[2]) -> F1_2_x3, F2 -> F2, + # and whose values are dictionaries that store the respective derivative information. + # Consider for example the BilinearForm (u, v) \mapsto integral(domain, dot(u, grad(Fs)) * dot(v, grad(Fs2)): + # The corresponding field_derivatives dict will be + # {Fs: {Fs_x3: {'x1': 0, 'x2': 0, 'x3': 1}, Fs_x2: {'x1': 0, 'x2': 1, 'x3': 0}, Fs_x1: {'x1': 1, 'x2': 0, 'x3': 0}}, Fs2: {Fs2_x3: {'x1': 0, 'x2': 0, 'x3': 1}, Fs2_x2: {'x1': 0, 'x2': 1, 'x3': 0}, Fs2_x1: {'x1': 1, 'x2': 0, 'x3': 0}}} + + # Amount of free FemFields (NOT counting each component individually) + n_free_fields = len(self._free_args) + field_derivatives = {} + # The keys in field_derivatives will be in the same order as the fields appearing in self._free_args + for n in range(n_free_fields): + # The key might be F1[0], but we want to check whether F1 == self._free_args[0], and ... + for key in field_atoms: + # ... field_name does exactly that + field_name = str(key.base) if hasattr(key, 'base') else str(key) + if field_name == self._free_args[n]: + # SymbolicExpr transforms something like F1[0] into F1_0 (part of the name of a variable in the assembly code later) + sym_key = SymbolicExpr(key) + field_derivatives[sym_key] = {} + for f in field_atoms[key]: + # And similarly f, which might look like dx1(F1[0]), will be transformed to F1_0_x2 + # while get_index_logical_derivatives(dx1(F1[0])) = {'x1': 1, 'x2': 0, 'x3': 0} + field_derivatives[sym_key][SymbolicExpr(f)] = get_index_logical_derivatives(f) + + # This part was proposed by Said at some point + #syme = False + #if syme: + # from symengine import sympify as syme_sympify + # sym_test_atoms = {k:[syme_sympify(SymbolicExpr(ai)) for ai in a] for k,a in test_atoms.items()} + # sym_trial_atoms = {k:[syme_sympify(SymbolicExpr(ai)) for ai in a] for k,a in trial_atoms.items()} + # sym_expr = syme_sympify(SymbolicExpr(texpr.expr)) + #else: + # sym_test_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in test_atoms.items()} + # sym_trial_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in trial_atoms.items()} + # sym_expr = SymbolicExpr(texpr.expr) + + # test_atoms is a dict whose values are components of the test function and whose values + # are arrays with appearing partial derivatives of those components. + # sym_test_atoms has the same structure, but replaces the appearing partial derivatives with + # symbolic expressions of those partial derivatives. E.g., + # test_atoms: {v2[0]: [dx3(v2[0]), dx2(v2[0])], v2[1]: [dx1(v2[1]), dx3(v2[1])], v2[2]: [dx2(v2[2]), dx1(v2[2])]} + # sym_test_atoms: {v2[0]: [v2_0_x3, v2_0_x2], v2[1]: [v2_1_x1, v2_1_x3], v2[2]: [v2_2_x2, v2_2_x1]} + # In the following, we will gather all (coupling) terms of a specific combination of a sym_test_atom with a sym_trial_atom in sym_expr + sym_test_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in test_atoms.items()} + sym_trial_atoms = {k:[SymbolicExpr(ai) for ai in a] for k,a in trial_atoms.items()} + sym_expr = SymbolicExpr(texpr.expr) + + # ----- temps, rhs ----- + + trials_subs = {ui:0 for u in sym_trial_atoms for ui in sym_trial_atoms[u]} + tests_subs = {vi:0 for v in sym_test_atoms for vi in sym_test_atoms[v]} + sub_exprs = {} + + # This is where the real magic happens: The at times extremely long and complicated SymbolicExpr sym_expr + # 0. is brought into a more readable form (sub_exprs) & + # 1. gets split into many small parts (temps), that often times appear in multiple sub_exprs, + # but now only have to be computed once, e.g. (temp_0, -F2_1*F1_1) & + # 2. those temporaries get assigned to coupling terms (rhs), i.e.: + # The coupling term corresponding to the sub-expr dx1(u[0])*dx3(v[1]) might be -temp_7*(temp_22*temp_27 + temp_33*temp_35 + temp_36*temp_37) + for u in sym_trial_atoms: + for v in sym_test_atoms: + if isinstance(u, IndexedVectorFunction) and isinstance(v, IndexedVectorFunction): + sub_expr = sym_expr[v.indices[0], u.indices[0]] + elif isinstance(u, ScalarFunction) and isinstance(v, ScalarFunction): + sub_expr = sym_expr + elif isinstance(u, ScalarFunction) and isinstance(v, IndexedVectorFunction): + sub_expr = sym_expr[v.indices[0]] + elif isinstance(u, IndexedVectorFunction) and isinstance(v, ScalarFunction): + sub_expr = sym_expr[u.indices[0]] + for ui,sui in zip(trial_atoms[u], sym_trial_atoms[u]): + trcp = trials_subs.copy() + trcp[sui] = 1 + newsub_expr = sub_expr.subs(trcp) + for vi,svi in zip(test_atoms[v],sym_test_atoms[v]): + tcp = tests_subs.copy() + tcp[svi] = 1 + expr = newsub_expr.subs(tcp) + if not expr.is_zero: + sub_exprs[ui,vi] = sympify(expr) + + temps, rhs = cse_main.cse(sub_exprs.values(), symbols=cse_main.numbered_symbols(prefix=f'temp_')) + + # ---------------------- + + # Finally, temps and rhs must be brought into a form that can be included in the assembly code, e.g. + # temp_0 = x_x1*y_x2 + # temp_1 = x_x2*z_x1 + # temp_2 = y_x1*z_x2 + # ... + # coupling_terms_u_v[k_2, q_2, k_3, q_3, 0] = temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9) + # coupling_terms_u_v[k_2, q_2, k_3, q_3, 1] = temp_18 + # coupling_terms_u_v[k_2, q_2, k_3, q_3, 2] = temp_22 + # ... + + # See above example: In our implementation of the sum factorization algorithm, we precompute arrays + # for each quadrature point in x1 direction, meaning that those arrays contain values depending on + # elements and quadrature points in x2 and x3 direction (k_2, k_3 & q_2 & q_3) + element_indices = [Symbol('k_{}'.format(i)) for i in range(2,4)] + quadrature_indices = [Symbol('q_{}'.format(i)) for i in range(2,4)] + # indices = (k_2, q_2, k_3, q_3) + indices = tuple(j for i in zip(element_indices, quadrature_indices) for j in i) + + # From the sub_exprs dictionary, we read all the appearing trial and test component combinations (blocks) that + # add a non-zero contribution to the matrix + ordered_stmts = {} + ordered_sub_exprs_keys = {} + for key in sub_exprs.keys(): + u_i, v_j = [get_atom_logical_derivatives(atom) for atom in key] + ordered_stmts[u_i, v_j] = [] + ordered_sub_exprs_keys[u_i, v_j] = [] + blocks = ordered_stmts.keys() + + block_list = list(blocks) + trial_components = [block[0] for block in block_list] + test_components = [block[1] for block in block_list] + nu = len(set(trial_components)) + nv = len(set(test_components)) + + expr = self.kernel_expr.expr + + # We store the maximum partial derivative (for a fixed direction), not including pertial derivatives + # appearing in mapping related terms (i.e., a BilinearForm on a mapped domain will have max_logical_derivative = 0 + # even though derivatives of the (spline) mapping appear in the coupling terms). + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): + shape = expr.shape + logical_max_derivatives = [] + for k1 in range(shape[0]): + for k2 in range(shape[1]): + logical_max_derivatives.append(get_max_logical_partial_derivatives(expr[k1,k2])) + max_logical_derivative = max([max([value for value in dic.values()]) for dic in logical_max_derivatives]) + else: + max_logical_derivative = max([value for value in get_max_logical_partial_derivatives(expr).values()]) + + # See comment underneath this code block for more details. + # There was a test case, in which the amount of generated StencilMatrices (one for each appearing block, + # i.e., one for each trial&test component combination for which a non-zero coupling term exists) + # was larger than the amount true amount of needed StencilMatrices. + # That discrepancy appears when expr[block].is_zero wrongly does not detect that a block is zero, + # whereas the corresponding block does rightfully not appear in block_list! + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): # only relevenat if either trial or test function is vector valued + g_mat_information_false = [] + shape = expr.shape + for k1 in range(shape[0]): + for k2 in range(shape[1]): + if not expr[k1,k2].is_zero: # although it might actually be zero! + if (nu == 1) and (nv > 1): + g_mat_information_false.append((k2,k1)) + else: + g_mat_information_false.append((k1,k2)) + if nu == 1: + g_mat_information_true = [(0, get_atom_logical_derivatives(block[1]).indices[0]) for block in block_list] + elif nv == 1: + g_mat_information_true = [(get_atom_logical_derivatives(block[0]).indices[0], 0) for block in block_list] + else: + g_mat_information_true = [(get_atom_logical_derivatives(block[0]).indices[0], get_atom_logical_derivatives(block[1]).indices[0]) for block in block_list] + else: + g_mat_information_false = [] + g_mat_information_true = [] + + # Julian O. 17.06.25: Back when I added this unreadable comment below I forgot to write a test for this problem. + # Eventually it might be interesting to remove everything related to `g_mat_information_false/true` + # and see where errors occur. + # + #1, 1: expr[1,1] = F0*sqrt(x1**2*(x1*cos(2*pi*x3) + 2)**2*(sin(pi*x2)**2 + cos(pi*x2)**2)**2*(sin(2*pi*x3)**2 + cos(2*pi*x3)**2)**2)*(pi*(x1*cos(2*pi*x3) + 2)* + # (-2*pi*x1*sin(pi*x2)*sin(2*pi*x3)*dx1(v1[1]) - sin(pi*x2)*cos(2*pi*x3)*dx3(v1[1]))*cos(pi*x2)*w2[1] - pi*(x1*cos(2*pi*x3) + 2)*(-2*pi*x1*sin(2*pi*x3)*cos(pi*x2)*dx1(v1[1]) - + # cos(pi*x2)*cos(2*pi*x3)*dx3(v1[1]))*sin(pi*x2)*w2[1])/(2*pi**2*x1**2*(x1*cos(2*pi*x3) + 2)**2*(sin(pi*x2)**2 + cos(pi*x2)**2)**2*(sin(2*pi*x3)**2 + cos(2*pi*x3)**2)**2) + # = 0 - but is not yet detected as 0! Hence a matrix is generated, that later is not required! + # + + # Here we create a template for the names of the coupling terms arrays, + # depending on whether or not trial and test function are scalar or vector valued + if nv > 1: + ct_str = 'coupling_terms_u_{u_i}_v_{v_j}' if nu > 1 else 'coupling_terms_u_v_{v_j}' + else: + ct_str = 'coupling_terms_u_{u_i}_v' if nu > 1 else 'coupling_terms_u_v' + + # Now we format this template based on the appearing blocks (combinations of trial and test function components) + # and transform those formatted strings into IndexedBase objects + lhs = {} + for block in blocks: + u_i = get_atom_logical_derivatives(block[0]).indices[0] if nu > 1 else 0 + v_j = get_atom_logical_derivatives(block[1]).indices[0] if nv > 1 else 0 + ct = ct_str.format(u_i=u_i, v_j=v_j) + lhs[block] = IndexedBase(f'{ct}') + + # lhs[block] will look w.g. like this coupling_terms_u_v (u, v scalar). + # Now, we add to that [k_2, q_2, k_3, q_3, count], where count enumerates the sub expressions belonging to the same block + # sub expressions corresponding to the block (u[0], v[1]) might be: (u[0], v[1]), (dx1(u[0]), v[1]), (dx2(u[0]), v[1]), ... + # and then assign the corresponding rhs, e.g. temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9), to obtain: + # coupling_terms_u_v[k_2, q_2, k_3, q_3, 4] = temp_7*(temp_10**2*temp_9 + temp_11**2*temp_9 + temp_8**2*temp_9) + counts = {block:0 for block in blocks} + for r,key in zip(rhs, sub_exprs.keys()): + u_i, v_j = [get_atom_logical_derivatives(atom) for atom in key] + count = counts[u_i, v_j] + counts[u_i, v_j] += 1 + ordered_stmts[u_i, v_j].append(Assign(lhs[u_i, v_j][(*indices, count)], r)) + ordered_sub_exprs_keys[u_i, v_j].append(key) + # ordered_stmts is a dict whose keys are combinations of trial and test functions components (e.g. u[0], v[1]), + # and whose values are a list of coupling term assignments corresponding to this block, e.g. + # (v1[0], v2[0]): [coupling_terms_u_0_v_0[k_2, q_2, k_3, q_3, 0] := -1, coupling_terms_u_0_v_0[k_2, q_2, k_3, q_3, 1] := 1] + # + # The information regarding which partial derivative combination belongs to which coupling term is stored in ordered_sub_exprs_keys. + # This dict has the same keys, but instead of coupling term assignments as values, list of tuples of partial derivative combinations are stored. + + # temps, which previously consisted of tuples like this one: (temp_0, -F2_1*F1_1), + # will now be a tuple consisting of assignments, e.g. (temp_0 := -F2_1*F1_1, ...) + temps = tuple(Assign(a,b) for a,b in temps) + + return temps, ordered_stmts, ordered_sub_exprs_keys, mapping_option, field_derivatives, g_mat_information_false, g_mat_information_true, max_logical_derivative + + #-------------------------------------------------------------------------- + def construct_arguments_generate_assembly_file(self): + """ + Collect the arguments used in the assembly method, and generate and possibly pyccelize the assembly function. + + Used only when sum factorization is enabled, else the method construct_arguments is called. + + Returns + ------- + args: tuple + The arguments passed to the assembly method. + + threads_args: None + None as openMP parallelization is not supported by this implementation. + + """ + temps, ordered_stmts, ordered_sub_exprs_keys, mapping_option, field_derivatives, g_mat_information_false, g_mat_information_true, max_logical_derivative = self.read_BilinearForm() + + # Each block corresponds to a combination of trial and test function components, and thus indeed to a "block" in the matrix. + # Not all possible combination have to exist, e.g., + # given a function space of vector valued functions V (3d) and a bilinear form a: VxV -> R, a(u, v) = (u, v)_L^2(Omega) + # there will be only 3 blocks on a logical domain (u[0]&v[0], u[1]&v[1], u[2]&v[2]), + # but up to 9 blocks on a mapped domain (e.g. u[0]&v[1], ...) + blocks = ordered_stmts.keys() + block_list = list(blocks) + trial_components = [block[0] for block in block_list] + test_components = [block[1] for block in block_list] + # dim = 1 corresponds to a scalar valued function, dim = 3 to a vector valued function + trial_dim = len(set(trial_components)) + test_dim = len(set(test_components)) + + # A reminder that this implementation only supports bilinear forms on 3d domains. + d = 3 + assert d == 3 + + # Rename - also: establish that throughout "u" corresponds to the trial function, whereas "v" corresponds to the test function + nu = trial_dim # dim of trial function; 1 (scalar) or 3 (vector) + nv = test_dim # dim of test function ; 1 (scalar) or 3 (vector) + + # Obtain the most basic information: function values, degrees, spans, ... + test_basis, test_degrees, spans, pads, test_mult = construct_test_space_arguments(self.test_basis) + trial_basis, trial_degrees, pads, trial_mult = construct_trial_space_arguments(self.trial_basis) + n_elements, quads, quad_degrees = construct_quad_grids_arguments(self.grid[0], use_weights=False) + + #! pads is being overwritten. That is because already somewhere else (__init__ of StencilMatrix via self.allocate_matrices) + # do we assert that domain and codomain (trial and test) pads coincide! + # That is not strictly necessary as Valentin at some point proved in one of his branches, but currently not implemented as + # not required. + + #! the above pads variable is multiplied by the multiplicity vector! For the remaining implementation, we need + # the pads vector un-multiplied, as obtained by : + pads = self.test_basis.space.coeff_space.pads + + # quad_degrees is the amount of quadrature points per element in each direction + # Clearly, this amount must coincide with the amount of basis function values stored per element in test_basis and trial_basis + n_element_1, n_element_2, n_element_3 = n_elements + k1, k2, k3 = quad_degrees + + # We store component wise degree and function values for trial and test function in the dictionaries + # trial_u_p, global_basis_u, test_v_p, global_basis_v + if (nu == 3) and (len(trial_basis) == 3): + # Edge Case: If the trial function space V is a VectorFunctionSpace + # but neither an Hdiv nor an Hcurl space, i.e., + # V = VectorFunctionSpace('V', domain) and not +, kind='hcurl') or +, kind='hdiv') + # then the function values in each of the three directions are identical for each of the three components. + # Hence len(trial_basis) == 3 instead of 9. + # + # global_basis_u is a dict whose values are arrays of function values of one particular trial function component, + # hence for this edge case we simply assign the same array trial_basis to each component + # Same function degree in each direction for each component -> do the same thing with trial_u_p + trial_u_p = {u:trial_degrees for u in range(nu)} + global_basis_u = {u:trial_basis for u in range(nu)} + else: + trial_u_p = {u:trial_degrees[d*u:d*(u+1)] for u in range(nu)} + global_basis_u = {u:trial_basis[d*u:d*(u+1)] for u in range(nu)} + if (nv == 3) and (len(test_basis) == 3): + # See above explanation, which also applies for the spans variable + test_v_p = {v:test_degrees for v in range(nv)} + global_basis_v = {v:test_basis for v in range(nv)} + spans = [*spans, *spans, *spans] + else: + test_v_p = {v:test_degrees[d*v:d*(v+1)] for v in range(nv)} + global_basis_v = {v:test_basis[d*v:d*(v+1)] for v in range(nv)} + + # See other method construct_arguments: + # When self._target is an Interface domain len(self._grid) == 2 + # where grid contains the QuadratureGrid of both sides of the interface + assert len(self.grid) == 1 + if self.mapping: + # We gather mapping related information in the case of a Bspline mapping + # self.mapping == False if either no or an analytical mapping + + map_coeffs = [[e._coeffs._data for e in self.mapping._fields]] + spaces = [self.mapping._fields[0].space] + map_degree = [sp.degree for sp in spaces] + map_span = [[q.spans - s for q,s in zip(sp.get_assembly_grids(*self.nquads), sp.coeff_space.starts)] for sp in spaces] + map_basis = [[q.basis for q in sp.get_assembly_grids(*self.nquads)] for sp in spaces] + points = [g.points for g in self.grid] + weights = [self.mapping.weights_field.coeffs._data] if self.is_rational_mapping else [] + + for i in range(len(self.grid)): + axis = self.grid[i].axis + # See construct_arguments - have not come across an example of when axis was not None! + assert axis is None + + map_degree = flatten(map_degree) + map_span = flatten(map_span) + map_basis = flatten(map_basis) + points = flatten(points) + mapping = [*map_coeffs[0], *weights] + else: + + mapping = [] + map_degree = [] + map_span = [] + map_basis = [] + + #---------- The following part is entirely different from the old construct_arguments method ---------- + + # Each block, say u[0]&v[1], + # consists of possibly many derivative combinations (sub-expressions) of these two components, e.g. + # dx1(u[0])&dx1(v[1]) or dx1(u[0])&dx2(v[1]) (dx1, dx2, dx3 representing respective partial derivatives). + # + # For each block, here still e.g. u[0]&v[1], + # and for each sub-expression, we store corresponding derivative information: + # get_index_logical_derivatives(dx1(u[0])) = {'x1': 1, 'x2': 0, 'x3': 0} + # get_index_logical_derivatives(dx2(v[1])) = {'x1': 0, 'x2': 1, 'x3': 0} + # Each of these 6 dicts has for each block an array of length #sub-expressions (appearing derivative combination) stored + # x2_test_keys[(u[0], v[1])][3] = 2 means, that the fourth sub-expression of block (u[0], v[1]) + # involves a second partial derivative of the test function in x2 direction + x1_trial_keys = {block:[] for block in blocks} + x1_test_keys = {block:[] for block in blocks} + x2_trial_keys = {block:[] for block in blocks} + x2_test_keys = {block:[] for block in blocks} + x3_trial_keys = {block:[] for block in blocks} + x3_test_keys = {block:[] for block in blocks} + + for block in blocks: + # alpha, beta for example being dx1(u[0]), dx2(v[1]) + for alpha, beta in ordered_sub_exprs_keys[block]: + x1_trial_keys[block].append(get_index_logical_derivatives(alpha)['x1']) + x1_test_keys [block].append(get_index_logical_derivatives(beta) ['x1']) + x2_trial_keys[block].append(get_index_logical_derivatives(alpha)['x2']) + x2_test_keys [block].append(get_index_logical_derivatives(beta) ['x2']) + x3_trial_keys[block].append(get_index_logical_derivatives(alpha)['x3']) + x3_test_keys [block].append(get_index_logical_derivatives(beta) ['x3']) + + # See sum factorization paper by Bressan & Takacs: + # coupling_terms, a3 and a2 correspond to A^{>=4}_{x1,x2,x3}, A^{>=3}_{x1,x2} and A^{>=2}_{x1} + # Here, for each block we assign a zero-array of the correct size. + coupling_terms = {} + a3 = {} + a2 = {} + + # For each block, we precompute ~enough~ products of partial derivatives of trial and basis functions in each direction + # These precomputed values will then be read rather than computed in the assembly + test_trial_1s = {} + test_trial_2s = {} + test_trial_3s = {} + + # keys_1/2/3 is a restructuring of the 6 dictionaries created above + keys_1 = {} + keys_2 = {} + keys_3 = {} + + assembly_backend = self.backend + if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel': + + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') + + comm = self.comm + + if comm is not None and comm.size > 1: + if comm.rank == 0: + filename = '__psydac__/test_trial_computation.py' + code = self.test_trial_template + f = open(filename, 'w') + f.writelines(code) + f.close() + else: + filename = '__psydac__/test_trial_computation.py' + code = self.test_trial_template + f = open(filename, 'w') + f.writelines(code) + f.close() + + base_dirpath = os.getcwd() + sys.path.insert(0, base_dirpath) + + package = importlib.import_module(f'__psydac__.test_trial_computation') + kwargs = { + 'language' : 'fortran', + 'compiler_family' : assembly_backend['compiler_family'], + 'flags' : assembly_backend['flags'], + 'openmp' : True if assembly_backend['openmp'] else False, + 'verbose' : False, + 'comm' : self.comm, + } + + test_trial_func = epyccel(package.test_trial_array, **kwargs) + + for block in blocks: + # We translate a block, e.g. (u[0], v[1]) into two integers u_i=0, v_j=1. + # In the case of a scalar function (u, v instead of u[0], u[1], u[2], v[0], v[1], v[2]), store 0. + u_i = block[0].indices[0] if nu > 1 else 0 + v_j = block[1].indices[0] if nv > 1 else 0 + + # keys_2[(u[0], v[1])][3] = (1,2) means that the fourth sub-expression corresponding to the trial-test-function-component-product + # u[0] * v[1] involves a first derivative in x2 direction of the trial function and a second derivative in x2 direction of the test function + keys_1[block] = np.array([(alpha_1, beta_1) for alpha_1, beta_1 in zip(x1_trial_keys[block], x1_test_keys[block])]) + keys_2[block] = np.array([(alpha_2, beta_2) for alpha_2, beta_2 in zip(x2_trial_keys[block], x2_test_keys[block])]) + keys_3[block] = np.array([(alpha_3, beta_3) for alpha_3, beta_3 in zip(x3_trial_keys[block], x3_test_keys[block])]) + + # Those are the function values in each direction of a particular component of the trial/test function + global_basis_u_1, global_basis_u_2, global_basis_u_3 = global_basis_u[u_i] + global_basis_v_1, global_basis_v_2, global_basis_v_3 = global_basis_v[v_j] + + # Those are the Bspline degrees in each direction of a particular component of the trial/test function + trial_u_p1, trial_u_p2, trial_u_p3 = trial_u_p[u_i] + test_v_p1, test_v_p2, test_v_p3 = test_v_p [v_j] + + max_p_2 = max(test_v_p2, trial_u_p2) + max_p_3 = max(test_v_p3, trial_u_p3) + + # That's the amount of subexpressions, i.e., combinations of partial derivatives appearing for a specific combination of + # trial and test function components + n_expr = len(ordered_stmts[block]) + + # To compute enough (possibly too many, but never too few) products of trial and test functions, we read the maximum + # appearing partial derivative (for this specific block, in each direction, for both trial and test function) + max_block_trial_x1_derivative = max(x1_trial_keys[block]) + max_block_trial_x2_derivative = max(x2_trial_keys[block]) + max_block_trial_x3_derivative = max(x3_trial_keys[block]) + max_block_test_x1_derivative = max(x1_test_keys[block]) + max_block_test_x2_derivative = max(x2_test_keys[block]) + max_block_test_x3_derivative = max(x3_test_keys[block]) + + # On each Bspline cell (element / subdomain), there are (test_degree+1)*(trial_degree+1) test & trial function pairs + # of non-zero product. + # Hence, we assign zeros for each element, each quadrature point on the element, each test and trial function combination, + # and each (or even more than required) appearing partial derivative combination of these functions - in each direction + test_trial_1 = np.zeros((n_element_1, k1, test_v_p1 + 1, trial_u_p1 + 1, max_block_trial_x1_derivative+1, max_block_test_x1_derivative+1), dtype='float64') + test_trial_2 = np.zeros((n_element_2, k2, test_v_p2 + 1, trial_u_p2 + 1, max_block_trial_x2_derivative+1, max_block_test_x2_derivative+1), dtype='float64') + test_trial_3 = np.zeros((n_element_3, k3, test_v_p3 + 1, trial_u_p3 + 1, max_block_trial_x3_derivative+1, max_block_test_x3_derivative+1), dtype='float64') + + # And that's how we fill the test_trial arrays + if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel': + for args in zip(n_elements, + quad_degrees, [test_v_p1, test_v_p2, test_v_p3], [trial_u_p1, trial_u_p2, trial_u_p3], + [global_basis_u_1, global_basis_u_2, global_basis_u_3], [global_basis_v_1, global_basis_v_2, global_basis_v_3], + [max_block_trial_x1_derivative, max_block_trial_x2_derivative, max_block_trial_x3_derivative], [max_block_test_x1_derivative, max_block_test_x2_derivative, max_block_test_x3_derivative], + [test_trial_1, test_trial_2, test_trial_3]): + + args = tuple(np.int64(a) if isinstance(a, int) else a for a in args) + + test_trial_func(*args) + else: + for k_1 in range(n_element_1): + for q_1 in range(k1): + for i_1 in range(test_v_p1 + 1): + for j_1 in range(trial_u_p1 + 1): + trial = global_basis_u_1[k_1, j_1, :, q_1] + test = global_basis_v_1[k_1, i_1, :, q_1] + for alpha_1 in range(max_block_trial_x1_derivative+1): + for beta_1 in range(max_block_test_x1_derivative+1): + test_trial_1[k_1, q_1, i_1, j_1, alpha_1, beta_1] = trial[alpha_1] * test[beta_1] + + for k_2 in range(n_element_2): + for q_2 in range(k2): + for i_2 in range(test_v_p2 + 1): + for j_2 in range(trial_u_p2 + 1): + trial = global_basis_u_2[k_2, j_2, :, q_2] + test = global_basis_v_2[k_2, i_2, :, q_2] + for alpha_2 in range(max_block_trial_x2_derivative+1): + for beta_2 in range(max_block_test_x2_derivative+1): + test_trial_2[k_2, q_2, i_2, j_2, alpha_2, beta_2] = trial[alpha_2] * test[beta_2] + + for k_3 in range(n_element_3): + for q_3 in range(k3): + for i_3 in range(test_v_p3 + 1): + for j_3 in range(trial_u_p3 + 1): + trial = global_basis_u_3[k_3, j_3, :, q_3] + test = global_basis_v_3[k_3, i_3, :, q_3] + for alpha_3 in range(max_block_trial_x3_derivative+1): + for beta_3 in range(max_block_test_x3_derivative+1): + test_trial_3[k_3, q_3, i_3, j_3, alpha_3, beta_3] = trial[alpha_3] * test[beta_3] + + test_trial_1s[block] = test_trial_1 + test_trial_2s[block] = test_trial_2 + test_trial_3s[block] = test_trial_3 + + # Instead of having a different a3, a2 & coupling term array for each sub-expression, we choose to have only one + # such array per block. + # a3 will store line integral values for all combinations of test and trial functions in x3 direction, hence the dimension + # (n_element_3 + test_v_p3 + (mult[2]-1)*(n_element_3-1), 2 * max_p_3 + 1) + # a2 will store surface integral values for all combinations of test and trial functions in x2 and x3 direction, hence the dimension ... + # coupling_terms stores point values of the coupling terms at all quadrature points + # but only in x2 and x3 direction, because we only "precompute" this array for a fixed quadrature point in x1 direction + + # a3[block] size explained: #sub expressions ; #test functions depending on x3 ; #complicated expression for the minimum columns needed + # to store local information correctly. 2*degree+1 in the simplest case. + n_funs_x2 = n_element_2 + test_v_p2 + (test_mult[1]-1)*(n_element_2-1) + n_funs_x3 = n_element_3 + test_v_p3 + (test_mult[2]-1)*(n_element_3-1) + n_cols_x2 = max( int(max_p_2 + 1 + np.floor(max_p_2 / test_mult[1]) * trial_mult[1]), 2*max_p_2+1 ) + n_cols_x3 = max( int(max_p_3 + 1 + np.floor(max_p_3 / test_mult[2]) * trial_mult[2]), 2*max_p_3+1 ) + + a3[block] = np.zeros((n_expr, n_funs_x3, n_cols_x3), dtype='float64') + a2[block] = np.zeros((n_expr, n_funs_x2, n_funs_x3, n_cols_x2, n_cols_x3), dtype='float64') + + coupling_terms[block] = np.zeros((n_element_2, k2, n_element_3, k3, n_expr), dtype='float64') + + # We gather the socalled new args - all other args are being obtained in a similar way using the old assembly implementation + new_args = (*list(test_trial_1s.values()), + *list(test_trial_2s.values()), + *list(test_trial_3s.values()), + *list(a3.values()), + *list(a2.values()), + *list(coupling_terms.values())) + + # This part is a bit shady. + # There has been a case, where my code wasn't running, because one instance of deep-(Psydac/Sympde/Sympy)-code + # correctly understood that a possibly complicated expression (corresponding to a block) in fact evaluates to 0, + # and hence no StencilMatrix for that particular block ever needs to be created - but a different part of + # deep-(Psydac/Sympde/Sympy)-code did not get that simplification right (yet?), and decided that the assembly code + # needs a StencilMatrix as input for this particular block. + # See readBilinearForm for additional information. + # This part of the code filters out unnecessary StencilMatrices, such that only the relevant StencilMatrices + # are being passed to the assembly function + expr = self.kernel_expr.expr + if isinstance(expr, (ImmutableDenseMatrix, Matrix)): + matrices = [] + for i, block in enumerate(g_mat_information_false): + if block in g_mat_information_true: + matrices.append(self._global_matrices[i]) + else: + matrices = self._global_matrices + + # We have gathered all args! + args = (*map_basis, *spans, *map_span, *quads, *map_degree, *n_elements, *quad_degrees, *pads, *mapping, *matrices, + *new_args) + + threads_args = () + + args = tuple(np.int64(a) if isinstance(a, int) else a for a in args) + threads_args = tuple(np.int64(a) if isinstance(a, int) else a for a in threads_args) + + #---------- We now generate the assembly file ---------- + + # file_id is a random string that has been used to name the assembly file + file_id = self.make_file(temps, ordered_stmts, field_derivatives, max_logical_derivative, test_mult, trial_mult, test_v_p, trial_u_p, keys_1, keys_2, keys_3, mapping_option) + + # Store the current directory and add it to the variable `sys.path` + # to imitate Python's import behavior + import os + base_dirpath = os.getcwd() + sys.path.insert(0, base_dirpath) + + # Import the generated assembly function + package = importlib.import_module(f'__psydac__.assemble_{file_id}') + + # The assembly function is the one that has been generated in the make_file method + assembly_function_name = f'assemble_matrix_{file_id}' + assembly_function = getattr(package, assembly_function_name) + + # If the backend is pyccel, we compile the new assembly function + assembly_backend = self.backend + if assembly_backend['name'] == 'pyccel': + kwargs = { + 'language' : 'fortran', # hardcoded for now + 'compiler_family' : assembly_backend['compiler_family'], + 'flags' : assembly_backend['flags'], + 'openmp' : True if assembly_backend['openmp'] else False, + 'verbose' : False, + # 'folder': assembly_backend['folder'], + 'comm' : self.comm, + # 'time_execution': verbose, + # 'verbose': verbose + } + new_func = epyccel(assembly_function, **kwargs) + else: + new_func = assembly_function + + # Use the new assembly function (either compiled or not) + self._func = new_func + + return args, threads_args + + #-------------------------------------------------------------------------- + @property + def test_trial_template(self): + code = '''def test_trial_array(n_element : "int64", + quad_degree : "int64", test_degree : "int64", trial_degree : "int64", + trial_basis : "float64[:,:,:,:]", test_basis : "float64[:,:,:,:]", + max_trial_derivative : "int64", max_test_derivative : "int64", + test_trial : "float64[:,:,:,:,:,:]"): + + for k in range(n_element): + for q in range(quad_degree): + for i in range(test_degree + 1): + for j in range(trial_degree + 1): + trial = trial_basis[k, j, :, q] + test = test_basis [k, i, :, q] + for alpha in range(max_trial_derivative + 1): + for beta in range(max_test_derivative + 1): + test_trial[k, q, i, j, alpha, beta] = trial[alpha] * test[beta] + + return +''' + return code diff --git a/psydac/api/fem_common.py b/psydac/api/fem_common.py new file mode 100644 index 000000000..9c6c0bd6a --- /dev/null +++ b/psydac/api/fem_common.py @@ -0,0 +1,286 @@ +from typing import Iterable + +from sympy import Expr, ImmutableDenseMatrix, Matrix + +import numpy as np + +from sympde.expr.basic import BasicForm +from sympde.expr.evaluation import KernelExpression +from sympde.topology.space import ScalarFunction, VectorFunction, IndexedVectorFunction +from sympde.topology.space import ProductSpace, VectorFunctionSpace +from sympde.topology.datatype import H1SpaceType, L2SpaceType, UndefinedSpaceType +from sympde.topology.derivatives import get_atom_logical_derivatives +from sympde.topology.derivatives import _logical_partial_derivatives + +from psydac.fem.basic import FemSpace +from psydac.linalg.stencil import StencilMatrix, StencilInterfaceMatrix +from psydac.linalg.basic import ComposedLinearOperator +from psydac.api.utilities import flatten +from psydac.api.ast.utilities import math_atoms_as_str, get_max_partial_derivatives + +# TODO [YG 01.08.2025]: Avoid importing anything from psydac.pyccel +from psydac.pyccel.ast.core import _atomic + +__all__ = ( + 'compute_max_nderiv', + 'compute_imports', + 'compute_free_arguments', + 'collect_spaces', + 'compute_diag_len', + 'construct_test_space_arguments', + 'construct_trial_space_arguments', + 'construct_quad_grids_arguments', + 'do_nothing', + 'extract_stencil_mats', + 'reset_arrays', +) + +#============================================================================== +def compute_max_nderiv(kernel_expr: KernelExpression) -> int: + """ + Compute the highest derivative order in the given kernel expression. + + Parameters + ---------- + kernel_expr : KernelExpression (from sympde.expr.evaluation) + + Returns + ------- + nderiv : int + The highest order of derivation in `terminal_expr`. + + """ + assert isinstance(kernel_expr, KernelExpression) + + terminal_expr = kernel_expr.expr + if not isinstance(terminal_expr, (ImmutableDenseMatrix, Matrix)): + terminal_expr = ImmutableDenseMatrix([[terminal_expr]]) + n_rows, n_cols = terminal_expr.shape + + atoms_types = (ScalarFunction, VectorFunction, IndexedVectorFunction) + extended_atoms_types = atoms_types + _logical_partial_derivatives + + nderiv = 0 + for i_row in range(n_rows): + for i_col in range(n_cols): + texpr = terminal_expr[i_row, i_col] + atoms = _atomic(texpr, cls=extended_atoms_types) + Fs = [get_atom_logical_derivatives(a) for a in atoms] + d = get_max_partial_derivatives(texpr, logical=True, F=Fs) + nderiv = max(nderiv, max(d.values())) + + return nderiv + +#============================================================================== +def compute_imports(expr: Expr, + spaces: Iterable[FemSpace], + *, + openmp: bool + ) -> dict[str, list[str]]: + """ + Compute all the imports to be added to the generated Python code. + + Parameters + ---------- + expr : sympy.Expr + The integrand expression of a BilinearForm, LinearForm, or + Functional. This is a pure SymPy expression where SymPDE partial + derivatives have been converted to SymPy symbols. See Notes. + + spaces : iterable of psydac.fem.FemSpace + The discrete spaces which define the finite element representation + of a BilinearForm, LinearForm, or Functional. + + openmp : bool + Whether or not OpenMP pragmas and functions are used in the code. + + Returns + ------- + imports : dict[str, list[str]] + A dictionary whose keys are the names of the Python modules to be + imported, and whose values are the names of the corresponding + objects (variables, functions, classes) to be imported from the + modules. + + Notes + ----- + Assume that we start from an object of type BilinearForm, LinearForm, + or Functional. We take the integrand and expand its vector operations + with TerminalExpr(), then pull back from physical to logical + coordinates with LogicalExpr(), and finally convert the symbolic + partial derivatives with SymbolicExpr(). Where: + - TerminalExpr is defined in sympde.expr.evaluation + - LogicalExpr is defined in sympde.topology.mapping + - SymbolicExpr is defined in sympde.topology.mapping + + The resulting expression `expr` can be passed to this function. + """ + assert isinstance(expr, Expr) + assert all(isinstance(V, FemSpace) for V in spaces) + assert isinstance(openmp, bool) + + # Determine the type of scalar quantities to be managed in the code + dtypes = [getattr(V.symbolic_space, 'codomain_type', 'real') for V in spaces] + assert all(t in ['complex', 'real'] for t in dtypes) + dtype = 'complex' if 'complex' in dtypes else 'real' + + # TODO uncomment this line when we have a SesquilinearForm defined in SymPDE + #assert isinstance(expr, SesquilinearForm) + + #... Compute the imports + math_library = 'cmath' if dtype=='complex' else 'math' # Function names are the same + math_imports = math_atoms_as_str(expr, 'math') + numpy_imports = ['array', 'zeros', 'zeros_like', 'floor'] + + imports = {'numpy': numpy_imports} + if math_imports: + imports[math_library] = math_imports + if openmp: + imports['pyccel.stdlib.internal.openmp'] = ['omp_get_thread_num'] + #... + + return imports + +#============================================================================== +def compute_free_arguments(expr: BasicForm, kernel_expr: KernelExpression) -> tuple[str]: + """ + The string representation (i.e. the names) of the free arguments in + the given BilinearForm, LinearForm, or Functional. + + Parameters + ---------- + expr : BilinearForm | LinearForm | Functional + The expression of which we want to compute the free arguments. + + kernel_expr : sympde.expr.evaluation.KernelExpression + The atomic representation of the form, which is obtained after using + LogicalExpr (if there is a mapping) and TerminalExpr on `expr`. + + Returns + ------- + tuple[str] + The string representation (i.e. the names) of the free arguments in + the given BilinearForm, LinearForm, or Functional. + + """ + assert isinstance(expr, BasicForm) + assert isinstance(kernel_expr, KernelExpression) + + free_args_dict = expr.get_free_variables() + free_args_str = tuple(str(a) for a in free_args_dict) + + return free_args_str + +#============================================================================== +def collect_spaces(space, *args): + """ + This function collect the arguments used in the assembly function + + Parameters + ---------- + space: + the symbolic space + + args : + list of discrete space components like basis values, spans, ... + + Returns + ------- + args : + list of discrete space components elements used in the asembly + + """ + + if isinstance(space, ProductSpace): + spaces = space.spaces + indices = [] + i = 0 + for space in spaces: + if isinstance(space, VectorFunctionSpace): + if isinstance(space.kind, (H1SpaceType, L2SpaceType, UndefinedSpaceType)): + indices.append(i) + else: + indices += [i+j for j in range(space.ldim)] + i = i + space.ldim + else: + indices.append(i) + i = i + 1 + args = [[e[i] for i in indices] for e in args] + + elif isinstance(space, VectorFunctionSpace): + if isinstance(space.kind, (H1SpaceType, L2SpaceType, UndefinedSpaceType)): + args = [[e[0]] for e in args] + + return args + +#============================================================================== +def compute_diag_len(p, md, mc): + n = ((np.ceil((p+1)/mc)-1)*md).astype('int') + n = n-np.minimum(0, n-p)+p+1 + return n.astype('int') + +#============================================================================== +def construct_test_space_arguments(basis_values): + space = basis_values.space + test_basis = basis_values.basis + spans = basis_values.spans + test_degrees = space.degree + pads = space.pads + multiplicity = space.multiplicity + + test_basis, test_degrees, spans = collect_spaces(space.symbolic_space, test_basis, test_degrees, spans) + + test_basis = flatten(test_basis) + test_degrees = flatten(test_degrees) + spans = flatten(spans) + pads = flatten(pads) + multiplicity = flatten(multiplicity) + pads = [p*m for p,m in zip(pads, multiplicity)] + return test_basis, test_degrees, spans, pads, multiplicity + +def construct_trial_space_arguments(basis_values): + space = basis_values.space + trial_basis = basis_values.basis + trial_degrees = space.degree + pads = space.pads + multiplicity = space.multiplicity + trial_basis, trial_degrees = collect_spaces(space.symbolic_space, trial_basis, trial_degrees) + + trial_basis = flatten(trial_basis) + trial_degrees = flatten(trial_degrees) + pads = flatten(pads) + multiplicity = flatten(multiplicity) + pads = [p*m for p,m in zip(pads, multiplicity)] + return trial_basis, trial_degrees, pads, multiplicity + +#============================================================================== +def construct_quad_grids_arguments(grid, use_weights=True): + points = grid.points + if use_weights: + weights = grid.weights + quads = flatten(list(zip(points, weights))) + else: + quads = flatten(list(zip(points))) + + nquads = flatten(grid.nquads) + n_elements = grid.n_elements + return n_elements, quads, nquads + +#============================================================================== +def do_nothing(*args): + return 0 + +#============================================================================== +def extract_stencil_mats(mats): + new_mats = [] + for M in mats: + if isinstance(M, (StencilInterfaceMatrix, StencilMatrix)): + new_mats.append(M) + elif isinstance(M, ComposedLinearOperator): + new_mats += [i for i in M.multiplicants if isinstance(i, (StencilInterfaceMatrix, StencilMatrix))] + return new_mats + +#============================================================================== +def reset_arrays(*args): + for a in args: + a[:]= 0.j if a.dtype==complex else 0. diff --git a/psydac/api/fem_sum_form.py b/psydac/api/fem_sum_form.py new file mode 100644 index 000000000..7d387b733 --- /dev/null +++ b/psydac/api/fem_sum_form.py @@ -0,0 +1,123 @@ +import numpy as np + +from sympde.expr.expr import ( + BilinearForm as sym_BilinearForm, + LinearForm as sym_LinearForm, + Functional as sym_Functional +) + +from .basic import BasicDiscrete +from .fem import DiscreteFunctional +from .fem import DiscreteLinearForm +from .fem import DiscreteBilinearForm +from .fem_bilinear_form import DiscreteBilinearForm as DiscreteBilinearForm_SF +from .fem_common import reset_arrays +from .utilities import random_string + +__all__ = ('DiscreteSumForm',) + +#============================================================================== +class DiscreteSumForm(BasicDiscrete): + + def __init__(self, a, kernel_expr, *args, **kwargs): + + # Sum factorization is only implemented for bilinear forms in 3D, in + # which case we use it by default. A 2D implementation should be the + # next step, hence we allow the user to pass `sum_factorization=True` + # even if not supported yet. In the case of linear forms or functionals + # this option is irrelevant for now, so we ignore it. + # + # In every case we remove the `sum_factorization` key from the dict + # in order to avoid errors, because none of the class constructors + # accept this argument. + sum_factorization = kwargs.pop('sum_factorization', a.ldim == 3) + + # TODO Uncomment when the SesquilinearForm exist in SymPDE + #if not isinstance(a, (sym_BilinearForm, sym_SesquilinearForm, sym_LinearForm, sym_Functional)): + # raise TypeError('> Expecting a symbolic BilinearForm, SesquilinearForm, LinearForm, Functional') + if not isinstance(a, (sym_BilinearForm, sym_LinearForm, sym_Functional)): + raise TypeError('> Expecting a symbolic BilinearForm, LinearForm, Functional') + + self._expr = a + backend = kwargs.pop('backend', None) + self._backend = backend + + folder = kwargs.get('folder', None) + self._folder = self._initialize_folder(folder) + + # create a module name if not given + tag = random_string(8) + + # ... + forms = [] + free_args = [] + self._kernel_expr = kernel_expr + operator = None + for e in kernel_expr: + if isinstance(a, sym_LinearForm): + kwargs['update_ghost_regions'] = False + ah = DiscreteLinearForm(a, e, *args, backend=backend, **kwargs) + kwargs['vector'] = ah._vector + operator = ah._vector + + # TODO Uncomment when the SesquilinearForm exist in SymPDE + # elif isinstance(a, sym_SesquilinearForm): + # kwargs['update_ghost_regions'] = False + # ah = DiscreteSesquilinearForm(a, e, *args, assembly_backend=backend, **kwargs) + # kwargs['matrix'] = ah._matrix + # operator = ah._matrix + + elif isinstance(a, sym_BilinearForm): + kwargs['update_ghost_regions'] = False + if sum_factorization: + ah = DiscreteBilinearForm_SF(a, e, *args, assembly_backend=backend, **kwargs) + else: + ah = DiscreteBilinearForm(a, e, *args, assembly_backend=backend, **kwargs) + kwargs['matrix'] = ah._matrix + operator = ah._matrix + + elif isinstance(a, sym_Functional): + ah = DiscreteFunctional(a, e, *args, backend=backend, **kwargs) + + forms.append(ah) + free_args.extend(ah.free_args) + + if isinstance(a, sym_BilinearForm): + is_broken = len(args[0].domain)>1 + if self._backend is not None and is_broken: + for mat in kwargs['matrix']._blocks.values(): + mat.set_backend(backend) + elif self._backend is not None: + kwargs['matrix'].set_backend(backend) + + self._forms = forms + self._operator = operator + self._free_args = tuple(set(free_args)) + self._is_functional = isinstance(a, sym_Functional) + # ... + + @property + def forms(self): + return self._forms + + @property + def free_args(self): + return self._free_args + + @property + def is_functional(self): + return self._is_functional + + def assemble(self, *, reset=True, **kwargs): + if not self.is_functional: + if reset : + reset_arrays(*[i for M in self.forms for i in M.global_matrices]) + + for form in self.forms: + form.assemble(reset=False, **kwargs) + self._operator.exchange_assembly_data() + return self._operator + else: + M = [form.assemble(**kwargs) for form in self.forms] + M = np.sum(M) + return M diff --git a/psydac/api/glt.py b/psydac/api/glt.py new file mode 100644 index 000000000..027b1ff23 --- /dev/null +++ b/psydac/api/glt.py @@ -0,0 +1,672 @@ +# coding: utf-8 + +# TODO for the moment we assume Product of same space + +import numpy as np +from itertools import product +from scipy.linalg import eig as eig_solver + +from gelato.expr import GltExpr as sym_GltExpr + +from psydac.api.ast.glt import GltKernel +from psydac.api.ast.glt import GltInterface +from psydac.api.settings import PSYDAC_BACKEND_PYTHON, PSYDAC_DEFAULT_FOLDER +from psydac.api.grid import CollocationBasisValues + +from psydac.api.utilities import mkdir_p, touch_init_file, random_string, write_code +from psydac.cad.geometry import Geometry +from psydac.mapping.discrete import SplineMapping, NurbsMapping + +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.fem.vector import MultipatchFemSpace + +from sympde.expr.basic import BasicForm +from psydac.api.printing.pycode import pycode + +import inspect +import sys +import os +import importlib +import string +import random +from mpi4py import MPI + +__all__ = ('GltBasicCodeGen', 'DiscreteGltExpr') + +#============================================================================== +class GltBasicCodeGen(object): + """ Basic class for any discrete concept that needs code generation """ + + def __init__(self, expr, **kwargs): + + namespace = kwargs.pop('namespace', globals()) + backend = kwargs.pop('backend', PSYDAC_BACKEND_PYTHON) + folder = kwargs.pop('folder', None) + comm = kwargs.pop('comm', None) + root = kwargs.pop('root', None) + # ... + if not( comm is None): + if root is None: + root = 0 + + assert isinstance( comm, MPI.Comm ) + assert isinstance( root, int ) + + if comm.rank == root: + tag = random_string( 8 ) + ast = self._create_ast( expr, tag, comm=comm, backend=backend, **kwargs ) + interface = ast['interface'] + max_nderiv = interface.max_nderiv + in_arguments = [str(a) for a in interface.in_arguments] + inout_arguments = [str(a) for a in interface.inout_arguments] + user_functions = interface.user_functions + + else: + interface = None + tag = None + max_nderiv = None + in_arguments = None + inout_arguments = None + user_functions = None + + comm.Barrier() + tag = comm.bcast( tag, root=root ) + max_nderiv = comm.bcast( max_nderiv, root=root ) + in_arguments = comm.bcast( in_arguments, root=root ) + inout_arguments = comm.bcast( inout_arguments, root=root ) + user_functions = comm.bcast( user_functions, root=root ) + + else: + tag = random_string( 8 ) + ast = self._create_ast( expr, tag, backend=backend, **kwargs ) + interface = ast['interface'] + max_nderiv = interface.max_nderiv + interface_name = interface.name + in_arguments = [str(a) for a in interface.in_arguments] + inout_arguments = [str(a) for a in interface.inout_arguments] + user_functions = interface.user_functions + # ... + + # ... + self._expr = expr + self._tag = tag + self._interface = interface + self._in_arguments = in_arguments + self._inout_arguments = inout_arguments + self._user_functions = user_functions + self._backend = backend + self._folder = self._initialize_folder(folder) + self._comm = comm + self._root = root + self._max_nderiv = max_nderiv + + self._dependencies = None + self._dependencies_code = None + self._dependencies_fname = None + self._dependencies_modname = None + + interface_name = 'interface_{}'.format(tag) + self._interface_name = interface_name + self._interface_code = None + self._interface_base_import_code = None + self._func = None + # ... + + # ... when using user defined functions, there must be passed as + # arguments of discretize. here we create a dictionary where the key + # is the function name, and the value is a valid implementation. + if user_functions: + for f in user_functions: + if not hasattr(f, '_imp_'): + # TODO raise appropriate error message + raise ValueError('can not find {} implementation'.format(f)) + # ... + + # generate python code as strings for dependencies + if not( interface is None ): + self._dependencies = interface.dependencies + self._dependencies_code = self._generate_code() + + if not( interface is None ): + # save dependencies code + self._save_code() + + if self.backend['name'] == 'pyccel': + self._compile_pyccel(namespace) + elif self.backend['name'] == 'pythran': + self._compile_pythran(namespace) + + # generate code for Python interface + self._generate_interface_code() + + # compile code + self._compile(namespace) + + if not( comm is None): + comm.Barrier() + if comm.rank != root: + if self.backend['name'] == 'pyccel': + _folder = os.path.join(self.folder, self.backend['folder']) + sys.path.append(_folder) + + interface_module_name = interface_name + self._set_func(interface_module_name, interface_name) + + if self.backend['name'] == 'pyccel': + _folder = os.path.join(self.folder, self.backend['folder']) + sys.path.remove(_folder) + + comm.Barrier() + + + @property + def expr(self): + return self._expr + + @property + def tag(self): + return self._tag + + @property + def in_arguments(self): + return self._in_arguments + + @property + def inout_arguments(self): + return self._inout_arguments + + @property + def user_functions(self): + return self._user_functions + + @property + def interface(self): + return self._interface + + @property + def dependencies(self): + return self._dependencies + + @property + def interface_name(self): + return self._interface_name + + @property + def interface_code(self): + return self._interface_code + + @property + def interface_base_import_code(self): + return self._interface_base_import_code + + @property + def dependencies_code(self): + return self._dependencies_code + + @property + def dependencies_fname(self): + return self._dependencies_fname + + @property + def dependencies_modname(self): + return self._dependencies_modname + + @property + def func(self): + return self._func + + @property + def backend(self): + return self._backend + + @property + def comm(self): + return self._comm + + @property + def root(self): + return self._root + + @property + def folder(self): + return self._folder + + def _create_ast(self, **kwargs): + raise NotImplementedError('Must be implemented') + + def _initialize_folder(self, folder=None): + # ... + if folder is None: + basedir = os.getcwd() + folder = PSYDAC_DEFAULT_FOLDER + folder = os.path.join( basedir, folder ) + + # ... add __init__ to all directories to be able to + touch_init_file('__pycache__') + for root, _, _ in os.walk(folder): + touch_init_file(root) + # ... + + else: + raise NotImplementedError('user output folder not yet available') + + folder = os.path.abspath( folder ) + mkdir_p(folder) + # ... + + return folder + + def _generate_code(self): + # ... generate code that can be pyccelized + code = '' + + if self.backend['name'] == 'pyccel': + + code += '\nfrom pyccel.decorators import types' + code += '\nfrom pyccel.decorators import external, external_call' + + imports = '\n'.join(pycode(imp) for dep in self.dependencies for imp in dep.imports ) + + code = '{code}\n{imports}'.format(code=code, imports=imports) + + # ... add user defined functions + if self.user_functions: + for func in self.user_functions: + func_code = get_source_function(func._imp_) + code = '{code}\n{func_code}'.format(code=code, func_code=func_code) + # ... + + for dep in self.dependencies: + code = '{code}\n{dep}'.format(code=code, dep=pycode(dep)) + # ... + return code + + def _save_code(self): + # ... + code = self.dependencies_code + module_name = 'dependencies_{}'.format(self.tag) + + self._dependencies_fname = '{}.py'.format(module_name) + write_code(self.dependencies_fname, code, folder = self.folder) + # ... + + # TODO check this? since we are using relative paths now + self._dependencies_modname = module_name.replace('/', '.') + + def _generate_interface_code(self): + imports = [] + + module_name = self.dependencies_modname + + # ... + if self.backend['name'] == 'pyccel': + imports += [self.interface_base_import_code] + + else: + # ... generate imports from dependencies module + pattern = 'from {module} import {dep}' + + for dep in self.dependencies: + txt = pattern.format(module=module_name, dep=dep.name) + imports.append(txt) + # ... + # ... + + imports = '\n'.join(imports) + + code = pycode(self.interface) + + self._interface_code = '{imports}\n{code}'.format(imports=imports, code=code) + + def _compile_pythran(self, namespace): + + module_name = self.dependencies_modname + + os.chdir(self.folder) + sys.path.append(self.folder) + os.system('pythran {}.py -O3'.format(module_name)) + sys.path.remove(self.folder) + + # ... + def _compile_pyccel(self, namespace, verbose=False): + + module_name = self.dependencies_modname + + # ... + from pyccel import epyccel + + # ... convert python to fortran using pyccel + compiler_family = self.backend['compiler_family'] + flags = self.backend['flags'] + _PYCCEL_FOLDER = self.backend['folder'] + # ... + + # ... + basedir = os.getcwd() + os.chdir(self.folder) + # ... + + # ... + sys.path.append(self.folder) + package = importlib.import_module( module_name ) + f2py_module = epyccel( package, + compiler_family = compiler_family, + flags = flags, + comm = self.comm, + bcast = False, + folder = _PYCCEL_FOLDER ) + sys.path.remove(self.folder) + # ... + + # ... get list of all functions inside the f2py module + functions = [] + for name, obj in inspect.getmembers(f2py_module): + if callable(obj) and not( name.startswith( 'f2py_' ) ): + functions.append(name) + # ... + + # ... + # update module name for dependencies + # needed for interface when importing assembly + name = os.path.join(_PYCCEL_FOLDER, f2py_module.__name__) + name = name.replace('/', '.') + imports = [] + for f in functions: + pattern = 'from {name} import {f}' + stmt = pattern.format( name = name, f = f ) + imports.append(stmt) + imports = '\n'.join(i for i in imports) + + self._interface_base_import_code = imports + # ... + + os.chdir(basedir) + + def _compile(self, namespace): + + # ... TODO move to save + code = self.interface_code + interface_module_name = 'interface_{}'.format(self.tag) + fname = '{}.py'.format(interface_module_name) + fname = write_code(fname, code, folder = self.folder) + # ... + + self._set_func(interface_module_name, self.interface_name) + + def _set_func(self, interface_module_name, interface_name): + # ... + sys.path.append(self.folder) + package = importlib.import_module( interface_module_name ) + sys.path.remove(self.folder) + # ... + + self._func = getattr(package, interface_name) + + def _check_arguments(self, **kwargs): + + # TODO do we need a method from Interface to map the dictionary of arguments + # that are passed for the call (in the same spirit of build_arguments) + # the idea is to be sure of their order, since they can be passed to + # Fortran + + _kwargs = {} + + # ... mandatory arguments + for key in self.in_arguments: + try: + _kwargs[key] = kwargs[key] + except: + raise KeyError('Unconsistent argument with interface') + # ... + + # ... optional (inout) arguments + for key in self.inout_arguments: + try: + _kwargs[key] = kwargs[key] + except: + pass + # ... + + return _kwargs + +#============================================================================== +class DiscreteGltExpr(GltBasicCodeGen): + + def __init__(self, expr, *args, **kwargs): + if not isinstance(expr, sym_GltExpr): + raise TypeError('> Expecting a symbolic Glt expression') + + if not args: + raise ValueError('> fem spaces must be given as a list/tuple') + + assert( len(args) == 2 ) + + # ... + domain_h = args[0] + assert( isinstance(domain_h, Geometry) ) + + mapping = list(domain_h.mappings.values())[0] + self._mapping = mapping + + is_rational_mapping = False + if not( mapping is None ): + is_rational_mapping = isinstance( mapping, NurbsMapping ) + + self._is_rational_mapping = is_rational_mapping + # ... + + # ... + self._spaces = args[1] + # ... + + # ... + kwargs['domain'] = domain_h.domain + kwargs['mapping'] = self.spaces[0].symbolic_mapping + kwargs['is_rational_mapping'] = is_rational_mapping + + GltBasicCodeGen.__init__(self, expr, **kwargs) + # ... + +# print('====================') +# print(self.dependencies_code) +# print('====================') +# print(self.interface_code) +# print('====================') +# import sys; sys.exit(0) + + @property + def mapping(self): + return self._mapping + + @property + def spaces(self): + return self._spaces + + # TODO add comm and treate parallel case + def _create_ast(self, expr, tag, **kwargs): + + domain = kwargs.pop('domain', None) + backend = kwargs.pop('backend', PSYDAC_BACKEND_PYTHON) + is_rational_mapping = kwargs.pop('is_rational_mapping', None) + # ... + kernel = GltKernel( expr, self.spaces, + name = 'kernel_{}'.format(tag), + domain = domain, + is_rational_mapping = is_rational_mapping, + backend = backend, **kwargs ) + + interface = GltInterface( kernel, + name = 'interface_{}'.format(tag), + domain = domain, + is_rational_mapping = is_rational_mapping, + backend = backend , **kwargs) + # ... + + ast = {'kernel': kernel, 'interface': interface} + return ast + + + def _check_arguments(self, **kwargs): + + # TODO do we need a method from Interface to map the dictionary of arguments + # that are passed for the call (in the same spirit of build_arguments) + # the idea is to be sure of their order, since they can be passed to + # Fortran + + _kwargs = {} + + # ... mandatory arguments + sym_args = self.interface.in_arguments + keys = [str(a) for a in sym_args] + for key in keys: + try: + # we use x1 for the call rather than arr_x1, to keep x1 inside + # the loop + if key == 'x1': + _kwargs['arr_x1'] = kwargs[key] + + elif key == 'x2': + _kwargs['arr_x2'] = kwargs[key] + + elif key == 'x3': + _kwargs['arr_x3'] = kwargs[key] + + else: + _kwargs[key] = kwargs[key] + except: + raise KeyError('Unconsistent argument with interface') + # ... + + # ... optional (inout) arguments + sym_args = self.interface.inout_arguments + keys = [str(a) for a in sym_args] + for key in keys: + try: + _kwargs[key] = kwargs[key] + except: + pass + # ... + + return _kwargs + + def evaluate(self, *args, **kwargs): + + kwargs = self._check_arguments(**kwargs) + + Vh = self.spaces[0] + is_block = False + if isinstance(Vh, MultipatchFemSpace): + Vh = Vh.spaces[0] + is_block = True + + if not isinstance(Vh, TensorFemSpace): + raise NotImplementedError('Only TensorFemSpace is available for the moment') + + args = args + (Vh,) + + dim = Vh.ldim + + if self.expr.form.fields or self.mapping: + nderiv = self.interface.max_nderiv + xis = [kwargs['arr_x{}'.format(i)] for i in range(1,dim+1)] + grid = tuple(xis) + # TODO assert that xis are inside the space domain + basis_values = CollocationBasisValues(grid, Vh, nderiv=nderiv) + args = args + (basis_values,) + # ... + + if self.mapping: + args = args + (self.mapping,) + + values = self.func(*args, **kwargs) + + if is_block: + # n_rows = n_cols here + n_rows = self.interface.n_rows + n_cols = self.interface.n_cols + nbasis = [V.nbasis for V in Vh.spaces] + + d = {} + i = 0 + for i_row in range(0, n_rows): + for i_col in range(0, n_cols): + d[i_row, i_col] = values[i] + i += 1 + + eig_mat = np.zeros((n_rows,*nbasis)) + + # ... compute dtype of the matrix + dtype = 'float' + are_complex = [i == 'complex' for i in self.interface.global_mats_types] + if any(are_complex): + dtype = 'complex' + # ... + + mat = np.zeros((n_rows,n_cols), dtype=dtype) + + if dim == 2: + for i1 in range(0, nbasis[0]): + for i2 in range(0, nbasis[1]): + mat[...] = 0. + for i_row in range(0,n_rows): + for i_col in range(0,n_cols): + mat[i_row,i_col] = d[i_row,i_col][i1, i2] + w,v = eig_solver(mat) + wr = w.real + eig_mat[:,i1,i2] = wr[:] + + elif dim == 3: + for i1 in range(0, nbasis[0]): + for i2 in range(0, nbasis[1]): + for i3 in range(0, nbasis[2]): + mat[...] = 0. + for i_row in range(0,n_rows): + for i_col in range(0,n_cols): + mat[i_row,i_col] = d[i_row,i_col][i1, i2, i3] + w,v = eig_solver(mat) + wr = w.real + eig_mat[:,i1,i2,i3] = wr[:] + + else: + raise NotImplementedError('') + + values = eig_mat + + return values + + def __call__(self, *args, **kwargs): + return self.evaluate(*args, **kwargs) + + def eig(self, **kwargs): + """ + Approximates the eigenvalues of the matrix associated to the given + bilinear form. + the current algorithm is based on a uniform sampling of the glt symbol. + """ + Vh = self.spaces[0] + if isinstance(Vh, MultipatchFemSpace): + Vh = Vh.spaces[0] + + if not isinstance(Vh, TensorFemSpace): + raise NotImplementedError('Only TensorFemSpace is available for the moment') + + nbasis = [V.nbasis for V in Vh.spaces] + bounds = [V.domain for V in Vh.spaces] + dim = Vh.ldim + + # ... fourier variables (as arguments) + ts = [np.linspace(-np.pi, np.pi, n) for n in nbasis] + args = tuple(ts) + # ... + + # ... space variables (as key words) + if self.interface.with_coordinates: + xs = [np.linspace(bound[0], bound[1], n) for n, bound in zip(nbasis, bounds)] + for n,x in zip(['x1', 'x2', 'x3'][:dim], xs): + kwargs[n] = x + # ... + + values = self(*args, **kwargs) + + return values diff --git a/psydac/api/tests/test_api_2d_fields.py b/psydac/api/tests/test_api_2d_fields.py new file mode 100644 index 000000000..d829bcfd5 --- /dev/null +++ b/psydac/api/tests/test_api_2d_fields.py @@ -0,0 +1,362 @@ +# -*- coding: UTF-8 -*- +# +# A note on the mappings used in these tests: +# +# - 'identity_2d.h5' is the identity mapping on the unit square [0, 1] X [0, 1] +# +# - 'collela_2d.h5' is a NURBS mapping from the unit square [0, 1]^2 to the +# larger square [-1, 1]^2, with deformations going as sin(pi x) * sin(pi y) +# +# - 'quarter_annulus.h5' is a NURBS transformation from the unit square [0, 1]^2 +# to the quarter annulus in the lower-left quadrant of the Cartesian plane +# (hence both x and y are negative), with r_min = 0.5 and r_max = 1 +# +# Please note that the logical coordinates (x1, x2) correspond to the polar +# coordinates (r, theta), but with reversed order: hence x1=theta and x2=r + +from mpi4py import MPI +from sympy import pi, cos, sin, log, exp, lambdify, symbols +import pytest +import os + +from sympde.calculus import grad, dot +from sympde.calculus import laplace +from sympde.topology import ScalarFunctionSpace +from sympde.topology import element_of +from sympde.topology import NormalVector +from sympde.topology import Domain +from sympde.topology import Union +from sympde.topology import Square +from sympde.expr import linearize +from sympde.expr import BilinearForm, LinearForm, integral +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_geometric_projectors import GlobalGeometricProjectorH1 + +# ... get the mesh directory +try: + mesh_dir = os.environ['PSYDAC_MESH_DIR'] + +except: + base_dir = os.path.dirname(os.path.realpath(__file__)) + base_dir = os.path.join(base_dir, '..', '..', '..') + mesh_dir = os.path.join(base_dir, 'mesh') +# ... +x, y = symbols('x, y', real=True) + +#------------------------------------------------------------------------------ +def run_field_test(filename, f): + + #+++++++++++++++++++++++++++++++ + # 1. Abstract model + #+++++++++++++++++++++++++++++++ + + domain = Domain.from_file(filename) + + V = ScalarFunctionSpace('V', domain) + u = element_of(V, name='u') + v = element_of(V, name='v') + F = element_of(V, name='F') + + # Bilinear form a: V x V --> R + a = BilinearForm((u, v), integral(domain, u * v)) + + # Linear form l: V --> R + l = LinearForm(v, integral(domain, f * v)) + + # Variational model + equation = find(u, forall=v, lhs=a(u, v), rhs=l(v)) + + #+++++++++++++++++++++++++++++++ + # 2. Discretization + #+++++++++++++++++++++++++++++++ + + # Create computational domain from topological domain + domain_h = discretize(domain, filename=filename) + + # Discrete spaces + Vh = discretize(V, domain_h) + + # Discretize equation using Dirichlet bc + equation_h = discretize(equation, domain_h, [Vh, Vh]) + + #+++++++++++++++++++++++++++++++ + # 3. Solution + #+++++++++++++++++++++++++++++++ + + # Solve linear system + # uh is the L2-projection of the analytical field "f" + uh = equation_h.solve() + + #+++++++++++++++++++++++++++++++ + l1 = LinearForm( v, integral(domain, F*v)) + l2 = LinearForm( v, integral(domain, f*v)) + l1_h = discretize(l1, domain_h, Vh) + l2_h = discretize(l2, domain_h, Vh) + + a1 = BilinearForm( (u,v), integral(domain, F*u*v)) + a2 = BilinearForm( (u,v), integral(domain, f*u*v)) + a1_h = discretize(a1, domain_h, [Vh, Vh]) + a2_h = discretize(a2, domain_h, [Vh, Vh]) + + x1 = l1_h.assemble(F=uh) + x2 = l2_h.assemble() + + A1 = a1_h.assemble(F=uh) + A2 = a2_h.assemble() + + error_1 = abs((x1-x2).toarray()).max() + error_2 = abs((A1-A2).toarray()).max() + + return error_1, error_2 + +#------------------------------------------------------------------------------ +def run_boundary_field_test(domain, boundary, f, ncells): + V = ScalarFunctionSpace('V', domain) + u = element_of(V, name='u') + v = element_of(V, name='v') + F = element_of(V, name='F') + + nn = NormalVector('nn') + + # Bilinear form a: V x V --> R + aF = BilinearForm((u, v), integral(boundary, F * u * v)) + af = BilinearForm((u, v), integral(boundary, f * u * v)) + + a_grad_F = BilinearForm((u, v), integral(boundary, dot(grad(F), nn) * u * v)) + a_grad_f = BilinearForm((u, v), integral(boundary, dot(grad(f), nn) * u * v)) + + # Linear form l: V --> R + lF = LinearForm( v, integral(boundary, F*v)) + lf = LinearForm( v, integral(boundary, f*v)) + + l_grad_F = LinearForm( v, integral(boundary, dot(grad(F), nn) * v)) + l_grad_f = LinearForm( v, integral(boundary, dot(grad(f), nn) * v)) + + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=[3,3]) + + x,y = domain.coordinates + f_lambda = lambdify([x,y], f, 'math') + Pi0 = GlobalGeometricProjectorH1(Vh) + fh = Pi0(f_lambda) + fh.coeffs.update_ghost_regions() + + aF_h = discretize(aF, domain_h, [Vh, Vh]) + af_h = discretize(af, domain_h, [Vh, Vh]) + aF_x = aF_h.assemble(F=fh) + af_x = af_h.assemble() + + a_grad_F_h = discretize(a_grad_F, domain_h, [Vh, Vh]) + a_grad_f_h = discretize(a_grad_f, domain_h, [Vh, Vh]) + a_grad_F_x = a_grad_F_h.assemble(F=fh) + a_grad_f_x = a_grad_f_h.assemble() + + lF_h = discretize(lF, domain_h, Vh) + lf_h = discretize(lf, domain_h, Vh) + lF_x = lF_h.assemble(F=fh) + lf_x = lf_h.assemble() + + l_grad_F_h = discretize(l_grad_F, domain_h, Vh) + l_grad_f_h = discretize(l_grad_f, domain_h, Vh) + l_grad_F_x = l_grad_F_h.assemble(F=fh) + l_grad_f_x = l_grad_f_h.assemble() + + error_BilinearForm = (aF_x-af_x).toarray() + rel_error_BilinearForm = abs(error_BilinearForm).max() / abs(af_x.toarray()).max() + + error_BilinearForm_grad = (a_grad_F_x-a_grad_f_x).toarray() + rel_error_BilinearForm_grad = abs(error_BilinearForm_grad).max() / abs(a_grad_f_x.toarray()).max() + + error_LinearForm = (lF_x-lf_x).toarray() + rel_error_LinearForm = abs(error_LinearForm).max() / abs(lf_x.toarray()).max() + + error_LinearForm_grad = (l_grad_F_x-l_grad_f_x).toarray() + rel_error_LinearForm_grad = abs(error_LinearForm_grad).max() / abs(l_grad_f_x.toarray()).max() + + return rel_error_BilinearForm, rel_error_BilinearForm_grad, rel_error_LinearForm, rel_error_LinearForm_grad + +#------------------------------------------------------------------------------ +def run_non_linear_poisson(filename, comm=None): + + # Maximum number of Newton iterations and convergence tolerance + N = 20 + TOL = 1e-14 + + # Define topological domain + Omega = Domain.from_file(filename) + + # Method of manufactured solutions: define exact + # solution phi_e, then compute right-hand side f + x, y = Omega.coordinates + u_e = 2 * log(0.5 * (x**2 + y**2) + 0.5) + + # Define abstract model + V = ScalarFunctionSpace('V', Omega) + v = element_of(V, name='v') + u = element_of(V, name='u') + + f = -2.*exp(-u) + l = LinearForm( v, integral(Omega, dot(grad(v), grad(u)) - f*v )) + + du = element_of(V, name='du') + dir_boundary = Omega.get_boundary(axis=0, ext=1) + bc = EssentialBC(du, 0, dir_boundary) + + # Linearized model (for Newton iteration) + a = linearize(l, u, trials=du) + equation = find(du, forall=v, lhs=a(du, v), rhs=-l(v), bc=bc) + + # Define (abstract) norms + l2norm_err = Norm(u - u_e, Omega, kind='l2') + l2norm_du = Norm(du , Omega, kind='l2') + + # Create computational domain from topological domain + Omega_h = discretize(Omega, filename=filename, comm=comm) + + # Create discrete spline space + Vh = discretize(V, Omega_h) + + # Discretize equation (u is free parameter and must be provided later) + equation_h = discretize(equation, Omega_h, [Vh, Vh], backend=PSYDAC_BACKEND_GPYCCEL) + + # Discretize norms + l2norm_err_h = discretize(l2norm_err, Omega_h, Vh) + l2norm_du_h = discretize(l2norm_du , Omega_h, Vh) + + # First guess: zero solution + u_h = FemField(Vh) + + # Newton iteration + for n in range(N): + + print() + print('==== iteration {} ===='.format(n)) + du_h = equation_h.solve(u=u_h) + + # Compute L2 norm of increment + l2_du = l2norm_du_h.assemble(du=du_h) + print('L2_norm(du) = {}'.format(l2_du)) + + if l2_du <= TOL: + print('CONVERGED') + break + + # update field + u_h += du_h + + # Compute L2 error norm from solution field + l2_error = l2norm_err_h.assemble(u=u_h) + + return l2_error + +############################################################################### +# SERIAL TESTS +############################################################################### + +TOL = 1e-12 + +@pytest.mark.parametrize('n1', [10, 31, 42]) +@pytest.mark.parametrize('axis', [0, 1]) +@pytest.mark.parametrize('ext', [-1, 1]) +def test_boundary_field_identity(n1, axis, ext): + + domain = Square('domain', bounds1=(0., 0.5), bounds2=(0., 1.)) + boundary = domain.get_boundary(axis=axis, ext=ext) + + x,y = domain.coordinates + f = (1+x)**3 + (1+y)**3 + + rel_error_BilinearForm, rel_error_BilinearForm_grad, rel_error_LinearForm, rel_error_LinearForm_grad = run_boundary_field_test(domain, boundary, f, [n1,2*n1]) + + assert rel_error_BilinearForm < TOL + assert rel_error_BilinearForm_grad < TOL + assert rel_error_LinearForm < TOL + assert rel_error_LinearForm_grad < TOL + + +def test_field_identity_1(): + + filename = os.path.join(mesh_dir, 'identity_2d.h5') + f = sin(pi*x)*sin(pi*y) + + error_1, error_2 = run_field_test(filename, f) + + expected_error_1 = 4.77987181085604e-12 + expected_error_2 = 1.196388887893425e-07 + + assert abs(error_1 - expected_error_1) < 1.e-7 + assert abs(error_2 - expected_error_2) < 1.e-7 + +#------------------------------------------------------------------------------ +def test_field_identity_2(): + + filename = os.path.join(mesh_dir, 'identity_2d.h5') + f = x*y*(x-1)*(y-1) + + error_1, error_2 = run_field_test(filename, f) + + expected_error_1 = 5.428295909559039e-11 + expected_error_2 = 2.9890068935570224e-11 + + assert abs(error_1 - expected_error_1) < 1.e-10 + assert abs(error_2 - expected_error_2) < 1.e-10 + +#------------------------------------------------------------------------------ +def test_field_collela(): + + filename = os.path.join(mesh_dir, 'collela_2d.h5') + f = sin(pi*x)*sin(pi*y) + + error_1, error_2 = run_field_test(filename, f) + + expected_error_1 = 1.9180860719170134e-10 + expected_error_2 = 0.00010748308338081464 + + assert abs(error_1 - expected_error_1) < 1.e-7 + assert abs(error_2 - expected_error_2) < 1.e-7 + +#------------------------------------------------------------------------------ +def test_field_quarter_annulus(): + + filename = os.path.join(mesh_dir, 'quarter_annulus.h5') + c = pi / (1. - 0.5**2) + r2 = 1. - x**2 - y**2 + f = x*y*sin(c * r2) + + error_1, error_2 = run_field_test(filename, f) + + expected_error_1 = 1.1146377538410329e-10 + expected_error_2 = 9.18920469410037e-08 + + assert abs(error_1 - expected_error_1) < 1.e-7 + assert abs(error_2 - expected_error_2) < 1.e-7 + +#============================================================================== +def test_nonlinear_poisson_circle(): + + filename = os.path.join(mesh_dir, 'circle.h5') + l2_error = run_non_linear_poisson(filename) + + expected_l2_error = 0.004026218710733066 + + assert abs(l2_error - expected_l2_error) < 1.e-7 + +#============================================================================== +# CLEAN UP SYMPY NAMESPACE +#============================================================================== + +def teardown_module(): + from sympy.core import cache + cache.clear_cache() + +def teardown_function(): + from sympy.core import cache + cache.clear_cache() + +if __name__ == '__main__': + test_field_quarter_annulus() diff --git a/psydac/api/tests/test_api_feec_1d.py b/psydac/api/tests/test_api_feec_1d.py new file mode 100644 index 000000000..fd7e7f7dd --- /dev/null +++ b/psydac/api/tests/test_api_feec_1d.py @@ -0,0 +1,835 @@ +# coding: utf-8 +# Copyright 2020 Yaman Güçlü + +""" + 1D time-dependent Maxwell simulation using FEEC and time splitting with + two operators. These integrate exactly one of the two equations, + respectively, over a given amount of time ∆t: + + 1. Faraday: + + b_new = b - ∆t D0 e + + 2. Amperè-Maxwell: + + e_new = e + ∆t (M0^{-1} D0^T M1) b + + Given a 1D de Rham sequence with coefficient spaces C0 and C1, the vectors + e and e_new belong to C0, while the vectors b and b_new belong to C1. + D0 is the derivative matrix that maps from C0 to C1, while M0 and M1 are + the mass matrices of the two spaces. +""" + +import pytest + +#============================================================================== +# VISUALIZATION +#============================================================================== +def make_plot(ax, t, sol_ex, sol_num, x, xlim, label): + ax.plot(x, sol_ex , '--', label='exact') + ax.plot(x, sol_num, '-' , label='numerical') + ax.legend(loc='upper right') + ax.grid() + ax.set_title('Time t = {:10.3e}'.format(t)) + ax.set_xlabel('x') + ax.set_ylabel(label, rotation='horizontal') + ax.set_xlim(xlim) + +def update_plot(ax, t, sol_ex, sol_num): + ax.set_title('Time t = {:10.3e}'.format(t)) + ax.lines[0].set_ydata(sol_ex ) + ax.lines[1].set_ydata(sol_num) + ax.get_figure().canvas.draw() + +#============================================================================== +# SIMULATION +#============================================================================== +def run_maxwell_1d(*, L, eps, ncells, degree, periodic, Cp, nsteps, tend, + splitting_order, plot_interval, diagnostics_interval, + bc_mode, tol, verbose, mult=1): + + import numpy as np + import matplotlib.pyplot as plt + from mpi4py import MPI + from scipy.integrate import quad + + from sympde.topology import Mapping + from sympde.topology import Line + from sympde.topology import Derham + from sympde.topology import elements_of + from sympde.expr import integral + from sympde.expr import BilinearForm + + from psydac.api.discretization import discretize + from psydac.linalg.solvers import inverse + + from psydac.api.settings import PSYDAC_BACKENDS + from psydac.feec.pull_push import push_1d_l2 + + # For now, use the Python backend, to be able to detect out of bounds errors + backend = PSYDAC_BACKENDS['python'] + + #-------------------------------------------------------------------------- + # Analytical objects: SymPDE + #-------------------------------------------------------------------------- + + # Logical domain: interval (0, 1) + logical_domain = Line('Omega', bounds=(0, 1)) + + #... Mapping and physical domain + class CollelaMapping1D(Mapping): + + _expressions = {'x': 'k * (x1 + eps / (2*pi) * sin(2*pi*x1))'} + _ldim = 1 + _pdim = 1 + + mapping = CollelaMapping1D('M', k=L, eps=eps) + domain = mapping(logical_domain) + #... + + # Exact solution + if periodic: + g = lambda w: np.exp(-(w/0.1)**2) # Gaussian waveform + wr = lambda t, x: (x-t) % L - L/2 # Right-traveling wave, L-periodic + E_ex = lambda t, x: g(wr(t,x)) # Exact solution in periodic domain + B_ex = lambda t, x: g(wr(t,x)) # Exact solution in periodic domain + else: + g = lambda w: np.exp(-(w/0.1)**2) # Gaussian waveform + wr = lambda t, x: (x-t+L/2) % (2*L) - L # Right-traveling wave, (2L)-periodic + wl = lambda t, x: (x+t-L/2) % (2*L) - L # Left-traveling wave, (2L)-periodic + E_ex = lambda t, x: g(wr(t,x)) - g(wl(t,x)) # Exact solution in bounded domain + B_ex = lambda t, x: g(wr(t,x)) + g(wl(t,x)) # Exact solution in bounded domain + + # DeRham sequence + derham = Derham(domain) + + # Trial and test functions + u0, v0 = elements_of(derham.V0, names='u0, v0') + u1, v1 = elements_of(derham.V1, names='u1, v1') + + # Bilinear forms that correspond to mass matrices for spaces V0 and V1 + a0 = BilinearForm((u0, v0), integral(domain, u0 * v0)) + a1 = BilinearForm((u1, v1), integral(domain, u1 * v1)) + + # ... + # If needed, apply homogeneous Dirichlet BCs + if not periodic: + + # Option 1: Apply essential BCs to elements of V0 space + if bc_mode == 'strong': + from sympde.expr import EssentialBC + bcs = [EssentialBC(u0, 0, side) for side in domain.boundary] + + # Option 2: Penalize L2 projection to V0 space + elif bc_mode == 'penalization': + a0_bc = BilinearForm((u0, v0), integral(domain.boundary, 1e30 * u0 * v0)) + + else: + NotImplementedError('bc_mode = {}'.format(bc_mode)) + # ... + + #-------------------------------------------------------------------------- + # Discrete objects: Psydac + #-------------------------------------------------------------------------- + + # Discrete physical domain and discrete DeRham sequence + domain_h = discretize(domain, ncells=[ncells], periodic=[periodic], comm=MPI.COMM_WORLD) + derham_h = discretize(derham, domain_h, degree=[degree], multiplicity=[mult]) + + # Discrete bilinear forms + a0_h = discretize(a0, domain_h, (derham_h.V0, derham_h.V0), nquads=[degree+1], backend=backend) + a1_h = discretize(a1, domain_h, (derham_h.V1, derham_h.V1), nquads=[degree+1], backend=backend) + + # Mass matrices (StencilMatrix objects) + M0 = a0_h.assemble() + M1 = a1_h.assemble() + + # Differential operators + D0, = derham_h.derivatives(kind='linop') + + # Transpose of derivative matrix + D0_T = D0.T + + # Boundary conditions + if not periodic: + + # Option 1: Modify operators to V0h space: mass matrix M0, differentiation matrix D0^T + if bc_mode == 'strong': + from psydac.api.essential_bc import apply_essential_bc + M0_dir = M0.copy() + D0_T_dir = D0_T.tokronstencil().tostencil().copy() + apply_essential_bc( M0_dir, *bcs) + apply_essential_bc(D0_T_dir, *bcs) + + # Make sure that we have ones on the diagonal of the mass matrix, + # in order to use a Jacobi preconditioner + s, = M0.codomain.starts + e, = M0.codomain.ends + n, = M0.codomain.npts + if s == 0: + M0_dir[s, 0] = 1.0 + if e + 1 == n: + M0_dir[e, 0] = 1.0 + + # Option 2: Discretize and assemble penalization matrix + elif bc_mode == 'penalization': + a0_bc_h = discretize(a0_bc, domain_h, (derham_h.V0, derham_h.V0), nquads=[degree+1], backend=backend) + M0_bc = a0_bc_h.assemble() + + # Projectors + P0, P1 = derham_h.projectors(nquads=[degree+2]) + + # Logical and physical grids + F = mapping.get_callable_mapping() + grid_x1 = derham_h.V0.breaks[0] + grid_x = F(grid_x1)[0] + + xmin = grid_x[ 0] + xmax = grid_x[-1] + + #-------------------------------------------------------------------------- + # Time integration setup + #-------------------------------------------------------------------------- + + t = 0 + + # Initial conditions, discrete fields + E = P0(lambda x: E_ex(0, x)) + B = P1(lambda x: B_ex(0, x)) + + # Initial conditions, spline coefficients + e = E.coeffs + b = B.coeffs + + # Time step size + dx_min = min(np.diff(grid_x)) + dt = Cp * dx_min + + # If final time is given, compute number of time steps + if tend is not None: + nsteps = int(np.ceil(tend / dt)) + + #-------------------------------------------------------------------------- + # Scalar diagnostics setup + #-------------------------------------------------------------------------- + + class Diagnostics: + + def __init__(self, E_ex, B_ex, M0, M1): + self._E_ex = E_ex + self._B_ex = B_ex + self._M0 = M0 + self._M1 = M1 + self._tmp0 = None + self._tmp1 = None + + # Energy of exact solution + def exact_energies(self, t): + """ Compute electric & magnetic energies of exact solution. + """ + We = 0.5 * quad(lambda x: self._E_ex(t, x)**2, xmin, xmax)[0] + Wb = 0.5 * quad(lambda x: self._B_ex(t, x)**2, xmin, xmax)[0] + return (We, Wb) + + # Energy of numerical solution + def discrete_energies(self, e, b): + """ Compute electric & magnetic energies of numerical solution. + """ + self._tmp0 = self._M0.dot(e, out=self._tmp0) + self._tmp1 = self._M1.dot(b, out=self._tmp1) + We = 0.5 * self._tmp0.dot(e) + Wb = 0.5 * self._tmp1.dot(b) + return (We, Wb) + + # Scalar diagnostics: + diagnostics_ex = {'time': [], 'electric_energy': [], 'magnetic_energy': []} + diagnostics_num = {'time': [], 'electric_energy': [], 'magnetic_energy': []} + + #-------------------------------------------------------------------------- + # Visualization and diagnostics setup + #-------------------------------------------------------------------------- + + # Very fine grids for evaluation of solution + x1 = np.linspace(grid_x1[0], grid_x1[-1], 101) + x = F(x1)[0] + + # Prepare plots + if plot_interval: + + # Plot physical grid + fig1, ax1 = plt.subplots(2, 1, figsize=(6, 6)) + ax1[0].set_ylim(-1, 1) + ax1[0].plot([xmin, xmax], [0, 0], 'orange') + ax1[0].plot(grid_x, np.zeros_like(grid_x), 'o') + ax1[0].set_title('Mapped grid obtained from uniform logical grid of {} cells'.format(ncells)) + ax1[0].set_xlabel('x', fontsize=14) + ax1[0].yaxis.set_visible(False) + + # Plot derivative of mapping + ax1[1].plot(x1, F.jacobian(x1)[0, 0], '-') + ax1[1].grid() + ax1[1].set_title(r'Derivative of mapping $F$ w.r.t. logical coordinate $x_1$') + ax1[1].set_xlabel(r'$x_1$', size=14) + ax1[1].set_ylabel(r"$F'\left(x_1\right)$", size=14) + + fig1.tight_layout() + fig1.canvas.draw() + fig1.show() + + # ... + # Prepare animations + E_values = [E(xi) for xi in x1] + B_values = push_1d_l2(lambda x1: np.array([B(xi) for xi in x1]), x1, F) + + fig2, ax2 = plt.subplots(1, 2, figsize=(12, 6)) + make_plot(ax2[0], t, E_ex(0, x), E_values, x, [xmin, xmax], label='E') + make_plot(ax2[1], t, B_ex(0, x), B_values, x, [xmin, xmax], label='B') + + ylim = (-0.2, 1.2) if periodic else (-1, 2) + for ax in ax2: + ax.set_ylim(*ylim) + ax.plot(grid_x, np.zeros_like(grid_x), 'ok', mfc='None', ms=5) + + fig2.tight_layout() + fig2.canvas.draw() + fig2.show() + # ... + + input('\nSimulation setup done... press any key to start') + + # Prepare diagnostics + if diagnostics_interval: + + diag = Diagnostics(E_ex, B_ex, M0, M1) + + # Exact energy at t=0 + We_ex, Wb_ex = diag.exact_energies(t) + diagnostics_ex['time'].append(t) + diagnostics_ex['electric_energy'].append(We_ex) + diagnostics_ex['magnetic_energy'].append(Wb_ex) + + # Discrete energy at t=0 + We_num, Wb_num = diag.discrete_energies(e, b) + diagnostics_num['time'].append(t) + diagnostics_num['electric_energy'].append(We_num) + diagnostics_num['magnetic_energy'].append(Wb_num) + + print('\nTotal energy in domain:') + print('t = {:8.4f}, exact = {Wt_ex:.13e}, discrete = {Wt_num:.13e}'.format(t, + Wt_ex = We_ex + Wb_ex, + Wt_num = We_num + Wb_num) + ) + + #-------------------------------------------------------------------------- + # Solution + #-------------------------------------------------------------------------- + + # TODO: add option to convert to scipy sparse format + + # ... Arguments for time stepping + kwargs = {'verbose': verbose, 'tol': tol} + + if periodic: + M0_inv = inverse(M0, 'cg', **kwargs) + step_ampere_1d = dt * ( M0_inv @ D0_T @ M1 ) + + elif bc_mode == 'strong': + M0_dir_inv = inverse(M0_dir, 'cg', **kwargs) + step_ampere_1d = dt * ( M0_dir_inv @ D0_T_dir @ M1 ) + + elif bc_mode == 'penalization': + M0_M0_bc = M0 + M0_bc + M0_M0_bc_inv = inverse(M0_M0_bc, 'pcg', pc = M0_M0_bc.diagonal(inverse=True), **kwargs) + step_ampere_1d = dt * ( M0_M0_bc_inv @ D0_T @ M1 ) + + half_step_faraday_1d = (dt/2) * D0 + + de = derham_h.V0.coeff_space.zeros() + db = derham_h.V1.coeff_space.zeros() + + # Time loop + for i in range(nsteps): + + # TODO: allow for high-order splitting + + # Strang splitting, 2nd order + b -= half_step_faraday_1d.dot(e, out=db) + e += step_ampere_1d.dot(b, out=de) + b -= half_step_faraday_1d.dot(e, out=db) + + #b -= half_step_faraday_1d @ e + #e += step_ampere_1d @ b + #b -= half_step_faraday_1d @ e + + t += dt + + # Animation + if plot_interval and (i % plot_interval == 0 or i == nsteps-1): + + E_values = [E(xi) for xi in x1] + B_values = push_1d_l2(lambda x1: np.array([B(xi) for xi in x1]), x1, F) + + # Update plot + update_plot(ax2[0], t, E_ex(t, x), E_values) + update_plot(ax2[1], t, B_ex(t, x), B_values) + plt.pause(0.01) + + # Scalar diagnostics + if diagnostics_interval and i % diagnostics_interval == 0: + + # Update exact diagnostics + We_ex, Wb_ex = diag.exact_energies(t) + diagnostics_ex['time'].append(t) + diagnostics_ex['electric_energy'].append(We_ex) + diagnostics_ex['magnetic_energy'].append(Wb_ex) + + # Update numerical diagnostics + We_num, Wb_num = diag.discrete_energies(e, b) + diagnostics_num['time'].append(t) + diagnostics_num['electric_energy'].append(We_num) + diagnostics_num['magnetic_energy'].append(Wb_num) + + # Print total energy to terminal + print('t = {:8.4f}, exact = {Wt_ex:.13e}, discrete = {Wt_num:.13e}'.format(t, + Wt_ex = We_ex + Wb_ex, + Wt_num = We_num + Wb_num) + ) + + #-------------------------------------------------------------------------- + # Post-processing + #-------------------------------------------------------------------------- + + if MPI.COMM_WORLD.size == 1: + # (does not work in parallel right now) + # Error at final time + E_values = np.array([E(xi) for xi in x1]) + B_values = push_1d_l2(lambda x1: np.array([B(xi) for xi in x1]), x1, F) + + # for now: no allreduce needed here, since the spline evaluation already does that for us + error_E = max(abs(E_ex(t, x) - E_values)) + error_B = max(abs(B_ex(t, x) - B_values)) + print() + print('Max-norm of error on E(t,x) at final time: {:.2e}'.format(error_E)) + print('Max-norm of error on B(t,x) at final time: {:.2e}'.format(error_B)) + + # compute L2 error as well + F = mapping.get_callable_mapping() + errE = lambda x1: (E(x1) - E_ex(t, *F(x1)))**2 * np.sqrt(F.metric_det(x1)) + errB = lambda x1: (push_1d_l2(B, x1, F) - B_ex(t, *F(x1)))**2 * np.sqrt(F.metric_det(x1)) + error_l2_E = np.sqrt(derham_h.V1.integral(errE, nquads=[degree+1])) + error_l2_B = np.sqrt(derham_h.V0.integral(errB)) + print('L2 norm of error on E(t,x) at final time: {:.2e}'.format(error_l2_E)) + print('L2 norm of error on B(t,x) at final time: {:.2e}'.format(error_l2_B)) + + if diagnostics_interval: + + # Extract exact diagnostics + t_ex = np.asarray(diagnostics_ex['time']) + We_ex = np.asarray(diagnostics_ex['electric_energy']) + Wb_ex = np.asarray(diagnostics_ex['magnetic_energy']) + Wt_ex = We_ex + Wb_ex + + # Extract numerical diagnostics + t_num = np.asarray(diagnostics_num['time']) + We_num = np.asarray(diagnostics_num['electric_energy']) + Wb_num = np.asarray(diagnostics_num['magnetic_energy']) + Wt_num = We_num + Wb_num + + # Energy plots + fig3, (ax31, ax32, ax33) = plt.subplots(3, 1, figsize=(12, 10)) + # + ax31.set_title('Energy of exact solution') + ax31.plot(t_ex, We_ex, label='electric') + ax31.plot(t_ex, Wb_ex, label='magnetic') + ax31.plot(t_ex, Wt_ex, label='total' ) + ax31.legend() + ax31.set_xlabel('t') + ax31.set_ylabel('W', rotation='horizontal') + ax31.grid() + # + ax32.set_title('Energy of numerical solution') + ax32.plot(t_num, We_num, label='electric') + ax32.plot(t_num, Wb_num, label='magnetic') + ax32.plot(t_num, Wt_num, label='total' ) + ax32.legend() + ax32.set_xlabel('t') + ax32.set_ylabel('W', rotation='horizontal') + ax32.grid() + # + ax33.set_title('Relative error in total energy') + ax33.plot(t_ex , (Wt_ex - Wt_ex) / Wt_ex[0], '--', label='exact') + ax33.plot(t_num, (Wt_num - Wt_ex) / Wt_ex[0], '-' , label='numerical') + ax33.legend() + ax33.set_xlabel('t') + ax33.set_ylabel('(W - W_ex) / W_ex(t=0)') + ax33.grid() + # + fig3.tight_layout() + fig3.show() + + # Return whole namespace as dictionary + return locals() + +#============================================================================== +# UNIT TESTS +#============================================================================== + +def test_maxwell_1d_periodic(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 30, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = None, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_E = 4.191954319623381e-04, + error_B = 4.447074070748624e-04) + + assert abs(namespace['error_E'] - ref['error_E']) / ref['error_E'] <= TOL + assert abs(namespace['error_B'] - ref['error_B']) / ref['error_B'] <= TOL + +def test_maxwell_1d_periodic_deg_1(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 30, + degree = 1, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = None, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_E = 0.0072675885645832605, + error_B = 0.06600368653495972) + + assert abs(namespace['error_E'] - ref['error_E']) / ref['error_E'] <= TOL + assert abs(namespace['error_B'] - ref['error_B']) / ref['error_B'] <= TOL + +def test_maxwell_1d_periodic_mult(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 30, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = None, + verbose = False, + mult = 2 + ) + + TOL = 1e-6 + ref = dict(error_E = 4.24689338e-04, + error_B = 4.03195792e-04) + + assert abs(namespace['error_E'] - ref['error_E']) / ref['error_E'] <= TOL + assert abs(namespace['error_B'] - ref['error_B']) / ref['error_B'] <= TOL + + +def test_maxwell_1d_dirichlet_strong(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 20, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = 'strong', + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_E = 1.320471502738063e-03, + error_B = 7.453774187340390e-04) + + assert abs(namespace['error_E'] - ref['error_E']) / ref['error_E'] <= TOL + assert abs(namespace['error_B'] - ref['error_B']) / ref['error_B'] <= TOL + + +def test_maxwell_1d_dirichlet_penalization(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 20, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = 'penalization', + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_E = 1.320290052669426e-03, + error_B = 7.453277842247585e-04) + + assert abs(namespace['error_E'] - ref['error_E']) / ref['error_E'] <= TOL + assert abs(namespace['error_B'] - ref['error_B']) / ref['error_B'] <= TOL + +@pytest.mark.parallel +def test_maxwell_1d_periodic_par(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 30, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = None, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_E = 1.3958706745655869e-04, + error_l2_B = 1.2635727360749016e-04) + + assert abs(namespace['error_l2_E'] - ref['error_l2_E']) / ref['error_l2_E'] <= TOL + assert abs(namespace['error_l2_B'] - ref['error_l2_B']) / ref['error_l2_B'] <= TOL + +@pytest.mark.parallel +def test_maxwell_1d_periodic_par_deg_1(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 30, + degree = 1, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = None, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_E = 0.0024771477815803107, + error_l2_B = 0.016857601931265228) + + assert abs(namespace['error_l2_E'] - ref['error_l2_E']) / ref['error_l2_E'] <= TOL + assert abs(namespace['error_l2_B'] - ref['error_l2_B']) / ref['error_l2_B'] <= TOL + +@pytest.mark.parallel +def test_maxwell_1d_dirichlet_strong_par(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 20, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = 'strong', + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_E = 1.3958706745655869e-04, + error_l2_B = 1.2635727360749016e-04) + + # TODO: after bug is fixed + #assert abs(namespace['error_l2_E'] - ref['error_l2_E']) / ref['error_l2_E'] <= TOL + #assert abs(namespace['error_l2_B'] - ref['error_l2_B']) / ref['error_l2_B'] <= TOL + +@pytest.mark.parallel +def test_maxwell_1d_dirichlet_penalization_par(): + + namespace = run_maxwell_1d( + L = 1.0, + eps = 0.5, + ncells = 20, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + bc_mode = 'penalization', + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_E = 4.7151938048476836e-04, + error_l2_B = 2.5099674095872517e-04) + + assert abs(namespace['error_l2_E'] - ref['error_l2_E']) / ref['error_l2_E'] <= TOL + assert abs(namespace['error_l2_B'] - ref['error_l2_B']) / ref['error_l2_B'] <= TOL + +#============================================================================== +# SCRIPT CAPABILITIES +#============================================================================== +if __name__ == '__main__': + + import argparse + + parser = argparse.ArgumentParser( + formatter_class = argparse.ArgumentDefaultsHelpFormatter, + description = "Solve 1D Maxwell equations with spline FEEC method." + ) + + parser.add_argument('ncells', + type = int, + help = 'Number of cells in domain' + ) + + parser.add_argument('degree', + type = int, + help = 'Polynomial spline degree' + ) + + parser.add_argument( '-P', '--periodic', + action = 'store_true', + help = 'Use periodic boundary conditions' + ) + + parser.add_argument('-o', '--splitting_order', + type = int, + default = 2, + choices = [2, 4, 6], + help = 'Order of accuracy of operator splitting' + ) + + parser.add_argument( '-l', + type = float, + default = 1.0, + dest = 'L', + metavar = 'L', + help = 'Length of domain [0, L]' + ) + + parser.add_argument( '-e', + type = float, + default = 0.25, + dest = 'eps', + metavar = 'EPS', + help = 'Deformation level (0 <= EPS < 1)' + ) + + parser.add_argument( '-c', + type = float, + default = 0.5, + dest = 'Cp', + metavar = 'Cp', + help = 'Courant parameter on uniform grid' + ) + + # ... + time_opts = parser.add_mutually_exclusive_group() + time_opts.add_argument( '-t', + type = int, + default = 1, + dest = 'nsteps', + metavar = 'NSTEPS', + help = 'Number of time-steps to be taken' + ) + time_opts.add_argument( '-T', + type = float, + dest = 'tend', + metavar = 'END_TIME', + help = 'Run simulation until given final time' + ) + # ... + + parser.add_argument( '-p', + type = int, + default = 4, + metavar = 'I', + dest = 'plot_interval', + help = 'No. of time steps between successive plots of solution, if I=0 no plots are made' + ) + + parser.add_argument( '-d', + type = int, + default = 1, + metavar = 'I', + dest = 'diagnostics_interval', + help = 'No. of time steps between successive calculations of scalar diagnostics, if I=0 no diagnostics are computed' + ) + + parser.add_argument( '-v', '--verbose', + action = 'store_true', + help = 'Print convergence information of iterative solver' + ) + + parser.add_argument( '--tol', + type = float, + default = 1e-7, + help = 'Tolerance for iterative solver (L2-norm of residual)' + ) + + parser.add_argument( '--bc_mode', + choices = ['strong', 'penalization'], + default = 'strong', + help = 'Strategy for imposing Dirichlet BCs' + ) + + # Read input arguments + args = parser.parse_args() + + # Run simulation + namespace = run_maxwell_1d(**vars(args)) + + # Keep matplotlib windows open + import matplotlib.pyplot as plt + plt.show() diff --git a/psydac/api/tests/test_api_feec_2d.py b/psydac/api/tests/test_api_feec_2d.py new file mode 100644 index 000000000..c221af521 --- /dev/null +++ b/psydac/api/tests/test_api_feec_2d.py @@ -0,0 +1,1032 @@ +# coding: utf-8 +# Copyright 2021 Yaman Güçlü + +""" + 2D time-dependent Maxwell simulation using FEEC and time splitting with + two operators. These integrate exactly one of the two equations, + respectively, over a given amount of time ∆t: + + 1. Faraday: + + b_new = b - ∆t D1 e + + 2. Amperè-Maxwell: + + e_new = e + ∆t (M1^{-1} D1^T M2) b + + Given a 2D de Rham sequence H1-H(curl)-L2 with coefficient spaces + (C0, C1, C2), the vectors e and e_new belong to C1, while the vectors b + and b_new belong to C2. D1 is the "scalar curl" matrix that maps from C1 + to C2, while M1 and M2 are the mass matrices of the spaces C1 and C2, + respectively. +""" + +import pytest + +#============================================================================== +# ANALYTICAL SOLUTION +#============================================================================== +class CavitySolution: + """ + Time-harmonic solution of Maxwell's equations in a rectangular cavity with + perfectly conducting walls. This is a "transverse electric" solution, with + E = (Ex, Ey) and B = Bz. Domain is [0, a] x [0, b]. + + Parameters + ---------- + a : float + Size of cavity along x direction. + + b : float + Size of cavity along y direction. + + c : float + Speed of light in arbitrary units. + + nx : int + Number of half wavelengths along x direction. + + ny : int + Number of half wavelengths along y direction. + + """ + def __init__(self, *, a, b, c, nx, ny): + + from sympy import symbols + from sympy import lambdify + + sym_params, sym_fields, sym_energy = self.symbolic() + + params = {'a': a, 'b': b, 'c': c, 'nx': nx, 'ny': ny} + repl = [(sym_params[k], params[k]) for k in sym_params.keys()] + args = symbols('t, x, y', real=True) + + # Callable functions + fields = {k: lambdify(args , v.subs(repl), 'numpy') for k, v in sym_fields.items()} + energy = {k: lambdify(args[0], v.subs(repl), 'numpy') for k, v in sym_energy.items()} + + # Store private attributes + self._sym_params = sym_params + self._sym_fields = sym_fields + self._sym_energy = sym_energy + + self._params = params + self._fields = fields + self._energy = energy + + #-------------------------------------------------------------------------- + @staticmethod + def symbolic(): + + from sympy import symbols + from sympy import cos, sin, pi, sqrt + from sympy.integrals import integrate + + t, x, y = symbols('t x y', real=True) + a, b, c = symbols('a b c', positive=True) + nx, ny = symbols('nx ny', positive=True, integer=True) + + kx = pi * nx / a + ky = pi * ny / b + omega = c * sqrt(kx**2 + ky**2) + + # Exact solutions for electric and magnetic field + Ex = cos(kx * x) * sin(ky * y) * cos(omega * t) + Ey = -sin(kx * x) * cos(ky * y) * cos(omega * t) + Bz = cos(kx * x) * cos(ky * y) * sin(omega * t) * (kx + ky) / omega + + # Electric and magnetic energy in domain + We = integrate(integrate((Ex**2 + Ey**2)/ 2, (x, 0, a)), (y, 0, b)).simplify() + Wb = integrate(integrate( Bz**2 / 2, (x, 0, a)), (y, 0, b)).simplify() + + params = {'a': a, 'b': b, 'c': c, 'nx': nx, 'ny': ny} + fields = {'Ex': Ex, 'Ey': Ey, 'Bz': Bz} + energy = {'We': We, 'Wb': Wb} + + return params, fields, energy + + #-------------------------------------------------------------------------- + @property + def params(self): + return self._params + + @property + def fields(self): + return self._fields + + @property + def energy(self): + return self._energy + + @property + def derived_params(self): + from numpy import pi, sqrt + kx = pi * self.params['nx'] / self.params['a'] + ky = pi * self.params['ny'] / self.params['b'] + omega = self.params['c'] * sqrt(kx**2 + ky**2) + return {'kx': kx, 'ky' : ky, 'omega': omega} + + @property + def sym_params(self): + return self._sym_params + + @property + def sym_fields(self): + return self._sym_fields + + @property + def sym_energy(self): + return self._sym_energy + +#============================================================================== +# VISUALIZATION +#============================================================================== + +def add_colorbar(im, ax, **kwargs): + from mpl_toolkits.axes_grid1 import make_axes_locatable + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size=0.2, pad=0.3) + cbar = ax.get_figure().colorbar(im, cax=cax, **kwargs) + return cbar + +def plot_field_and_error(name, x, y, field_h, field_ex, *gridlines): + import matplotlib.pyplot as plt + fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(15, 6)) + im0 = ax0.contourf(x, y, field_h) + im1 = ax1.contourf(x, y, field_ex - field_h) + ax0.set_title(r'${0}_h$'.format(name)) + ax1.set_title(r'${0} - {0}_h$'.format(name)) + for ax in (ax0, ax1): + ax.plot(*gridlines[0], color='k') + ax.plot(*gridlines[1], color='k') + ax.set_xlabel('x', fontsize=14) + ax.set_ylabel('y', fontsize=14, rotation='horizontal') + ax.set_aspect('equal') + add_colorbar(im0, ax0) + add_colorbar(im1, ax1) + fig.suptitle('Time t = {:10.3e}'.format(0)) + fig.tight_layout() + return fig + +def update_plot(fig, t, x, y, field_h, field_ex): + ax0, ax1, cax0, cax1 = fig.axes + ax0.collections.clear(); cax0.clear() + ax1.collections.clear(); cax1.clear() + im0 = ax0.contourf(x, y, field_h) + im1 = ax1.contourf(x, y, field_ex - field_h) + fig.colorbar(im0, cax=cax0) + fig.colorbar(im1, cax=cax1) + fig.suptitle('Time t = {:10.3e}'.format(t)) + fig.canvas.draw() + +#============================================================================== +# SIMULATION +#============================================================================== +def run_maxwell_2d_TE(*, use_spline_mapping, + eps, ncells, degree, periodic, + Cp, nsteps, tend, + splitting_order, plot_interval, diagnostics_interval, tol, verbose, mult=1): + + import os + + import numpy as np + import matplotlib.pyplot as plt + from mpi4py import MPI + from scipy.integrate import dblquad + + from sympde.topology import Domain + from sympde.topology import Square + from sympde.topology import Mapping + from sympde.topology import CallableMapping +# from sympde.topology import CollelaMapping2D + from sympde.topology import Derham + from sympde.topology import elements_of + from sympde.topology import NormalVector + from sympde.calculus import dot, cross + from sympde.expr import integral + from sympde.expr import BilinearForm + + from psydac.api.discretization import discretize + from psydac.api.settings import PSYDAC_BACKENDS + from psydac.feec.pull_push import push_2d_hcurl, push_2d_l2 + from psydac.linalg.solvers import inverse + from psydac.utilities.utils import refine_array_1d + from psydac.mapping.discrete import SplineMapping, NurbsMapping + + backend = PSYDAC_BACKENDS['pyccel-gcc'] + + #-------------------------------------------------------------------------- + # Problem setup + #-------------------------------------------------------------------------- + + # Physical domain is rectangle [0, a] x [0, b] + a = 2.0 + b = 2.0 + + # Speed of light is 1 + c = 1.0 + + # Mode number + (nx, ny) = (2, 2) + + # Exact solution + exact_solution = CavitySolution(a=a, b=b, c=c, nx=nx, ny=ny) + + # Exact fields, as callable functions of (t, x, y) + Ex_ex = exact_solution.fields['Ex'] + Ey_ex = exact_solution.fields['Ey'] + Bz_ex = exact_solution.fields['Bz'] + + #... + + #-------------------------------------------------------------------------- + # Analytical objects: SymPDE + #-------------------------------------------------------------------------- + + if use_spline_mapping: + + try: + mesh_dir = os.environ['PSYDAC_MESH_DIR'] + except KeyError: + base_dir = os.path.dirname(os.path.realpath(__file__)) + mesh_dir = os.path.join(base_dir, '..', '..', '..', 'mesh') + + filename = os.path.join(mesh_dir, 'collela_2d.h5') + domain = Domain.from_file(filename) + mapping = domain.mapping + + else: + # Logical domain is unit square [0, 1] x [0, 1] + logical_domain = Square('Omega') + + # Mapping and physical domain + class CollelaMapping2D(Mapping): + + _ldim = 2 + _pdim = 2 + _expressions = {'x': 'a * (x1 + eps / (2*pi) * sin(2*pi*x1) * sin(2*pi*x2))', + 'y': 'b * (x2 + eps / (2*pi) * sin(2*pi*x1) * sin(2*pi*x2))'} + + # mapping = CollelaMapping2D('M', k1=1, k2=1, eps=eps) + mapping = CollelaMapping2D('M', a=a, b=b, eps=eps) + domain = mapping(logical_domain) + + # DeRham sequence + derham = Derham(domain, sequence=['h1', 'hcurl', 'l2']) + + # Trial and test functions + u1, v1 = elements_of(derham.V1, names='u1, v1') # electric field E = (Ex, Ey) + u2, v2 = elements_of(derham.V2, names='u2, v2') # magnetic field Bz + + # Bilinear forms that correspond to mass matrices for spaces V1 and V2 + a1 = BilinearForm((u1, v1), integral(domain, dot(u1, v1))) + a2 = BilinearForm((u2, v2), integral(domain, u2 * v2)) + + # Penalization to apply homogeneous Dirichlet BCs (will only be used if domain is not periodic) + nn = NormalVector('nn') + a1_bc = BilinearForm((u1, v1), + integral(domain.boundary, 1e30 * cross(u1, nn) * cross(v1, nn))) + + #-------------------------------------------------------------------------- + # Discrete objects: Psydac + #-------------------------------------------------------------------------- + if use_spline_mapping: + domain_h = discretize(domain, filename=filename, comm=MPI.COMM_WORLD) + derham_h = discretize(derham, domain_h, multiplicity = [mult, mult]) + + periodic_list = mapping.get_callable_mapping().space.periodic + degree_list = mapping.get_callable_mapping().space.degree + + # Determine if periodic boundary conditions should be used + if all(periodic_list): + periodic = True + elif not any(periodic_list): + periodic = False + else: + raise ValueError('Cannot handle periodicity along one direction only') + + # Enforce same degree along x1 and x2 + degree = degree_list[0] + if degree != degree_list[1]: + raise ValueError('Cannot handle different degrees in the two directions') + + else: + # Discrete physical domain and discrete DeRham sequence + domain_h = discretize(domain, ncells=[ncells, ncells], periodic=[periodic, periodic], comm=MPI.COMM_WORLD) + derham_h = discretize(derham, domain_h, degree=[degree, degree], multiplicity = [mult, mult]) + + # Discrete bilinear forms + nquads = [degree + 1, degree + 1] + a1_h = discretize(a1, domain_h, (derham_h.V1, derham_h.V1), nquads=nquads, backend=backend) + a2_h = discretize(a2, domain_h, (derham_h.V2, derham_h.V2), nquads=nquads, backend=backend) + + # Mass matrices (StencilMatrix or BlockLinearOperator objects) + M1 = a1_h.assemble() + M2 = a2_h.assemble() + + # Differential operators (StencilMatrix or BlockLinearOperator objects) + D0, D1 = derham_h.derivatives(kind='linop') + + # Discretize and assemble penalization matrix + if not periodic: + a1_bc_h = discretize(a1_bc, domain_h, (derham_h.V1, derham_h.V1), nquads=nquads, backend=backend) + M1_bc = a1_bc_h.assemble() + + # Transpose of derivative matrix + D1_T = D1.T + + # Projectors + P0, P1, P2 = derham_h.projectors(nquads=[degree+2, degree+2]) + + # Logical and physical grids + F = mapping.get_callable_mapping() + grid_x1 = derham_h.V0.breaks[0] + grid_x2 = derham_h.V0.breaks[1] + + # TODO: fix for spline mapping + if isinstance(F, (SplineMapping, NurbsMapping)): + grid_x, grid_y = F.build_mesh([grid_x1, grid_x2]) + elif isinstance(F, CallableMapping): + grid_x, grid_y = F(*np.meshgrid(grid_x1, grid_x2, indexing='ij')) + else: + raise TypeError(F) + + #-------------------------------------------------------------------------- + # Time integration setup + #-------------------------------------------------------------------------- + + t = 0 + + # Initial conditions, discrete fields + E = P1((lambda x, y: Ex_ex(0, x, y), lambda x, y: Ey_ex(0, x, y))) + B = P2(lambda x, y: Bz_ex(0, x, y)) + + # Initial conditions, spline coefficients + e = E.coeffs + b = B.coeffs + + # Time step size + dx_min_1 = np.sqrt(np.diff(grid_x, axis=0)**2 + np.diff(grid_y, axis=0)**2).min() + dx_min_2 = np.sqrt(np.diff(grid_x, axis=1)**2 + np.diff(grid_y, axis=1)**2).min() + + dx_min = min(dx_min_1, dx_min_2) + dt = Cp * dx_min / c + + # If final time is given, compute number of time steps + if tend is not None: + nsteps = int(np.ceil(tend / dt)) + + #-------------------------------------------------------------------------- + # Scalar diagnostics setup + #-------------------------------------------------------------------------- + + class Diagnostics: + + def __init__(self, E_ex, B_ex, M1, M2): + self._E_ex = E_ex + self._B_ex = B_ex + self._M1 = M1 + self._M2 = M2 + self._tmp1 = None + self._tmp2 = None + + # Energy of exact solution + def exact_energies(self, t): + """ Compute electric & magnetic energies of exact solution. + """ + We = self._E_ex(t) + Wb = self._B_ex(t) + return (We, Wb) + + # Energy of numerical solution + def discrete_energies(self, e, b): + """ Compute electric & magnetic energies of numerical solution. + """ + self._tmp1 = self._M1.dot(e, out=self._tmp1) + self._tmp2 = self._M2.dot(b, out=self._tmp2) + We = 0.5 * self._tmp1.dot(e) + Wb = 0.5 * self._tmp2.dot(b) + return (We, Wb) + + # Scalar diagnostics: + diagnostics_ex = {'time': [], 'electric_energy': [], 'magnetic_energy': []} + diagnostics_num = {'time': [], 'electric_energy': [], 'magnetic_energy': []} + + #-------------------------------------------------------------------------- + # Visualization and diagnostics setup + #-------------------------------------------------------------------------- + + # Very fine grids for evaluation of solution + N = 5 + x1_a = refine_array_1d(grid_x1, N) + x2_a = refine_array_1d(grid_x2, N) + + x1, x2 = np.meshgrid(x1_a, x2_a, indexing='ij') + + if use_spline_mapping: + x, y = F.build_mesh([x1_a, x2_a]) + else: + x, y = F(x1, x2) + + gridlines_x1 = (x[:, ::N], y[:, ::N] ) + gridlines_x2 = (x[::N, :].T, y[::N, :].T) + gridlines = (gridlines_x1, gridlines_x2) + + Ex_values = np.empty_like(x1) + Ey_values = np.empty_like(x1) + Bz_values = np.empty_like(x1) + + # Prepare plots + if plot_interval: + + # Plot physical grid and mapping's metric determinant + fig1, ax1 = plt.subplots(1, 1, figsize=(8, 6)) + + if use_spline_mapping: + im = ax1.contourf(x, y, F.jac_det_grid([x1_a, x2_a])) + else: + im = ax1.contourf(x, y, np.sqrt(F.metric_det(x1, x2))) + + add_colorbar(im, ax1, label=r'Metric determinant $\sqrt{g}$ of mapping $F$') + ax1.plot(*gridlines_x1, color='k') + ax1.plot(*gridlines_x2, color='k') + ax1.set_title('Mapped grid of {} x {} cells'.format(ncells, ncells)) + ax1.set_xlabel('x', fontsize=14) + ax1.set_ylabel('y', fontsize=14) + ax1.set_aspect('equal') + fig1.tight_layout() + fig1.show() + + # ... + # Plot initial conditions + # TODO: improve + for i, x1i in enumerate(x1[:, 0]): + for j, x2j in enumerate(x2[0, :]): + + Ex_values[i, j], Ey_values[i, j] = \ + push_2d_hcurl(E.fields[0], E.fields[1], x1i, x2j, F) + + Bz_values[i, j] = push_2d_l2(B, x1i, x2j, F) + + # Electric field, x component + fig2 = plot_field_and_error(r'E^x', x, y, Ex_values, Ex_ex(0, x, y), *gridlines) + fig2.show() + + # Electric field, y component + fig3 = plot_field_and_error(r'E^y', x, y, Ey_values, Ey_ex(0, x, y), *gridlines) + fig3.show() + + # Magnetic field, z component + fig4 = plot_field_and_error(r'B^z', x, y, Bz_values, Bz_ex(0, x, y), *gridlines) + fig4.show() + # ... + + input('\nSimulation setup done... press any key to start') + + # Prepare diagnostics + if diagnostics_interval: + + diag = Diagnostics(exact_solution.energy['We'], exact_solution.energy['Wb'], M1, M2) + + # Exact energy at t=0 + We_ex, Wb_ex = diag.exact_energies(t) + diagnostics_ex['time'].append(t) + diagnostics_ex['electric_energy'].append(We_ex) + diagnostics_ex['magnetic_energy'].append(Wb_ex) + + # Discrete energy at t=0 + We_num, Wb_num = diag.discrete_energies(e, b) + diagnostics_num['time'].append(t) + diagnostics_num['electric_energy'].append(We_num) + diagnostics_num['magnetic_energy'].append(Wb_num) + + print('\nTotal energy in domain:') + print('ts = {:4d}, t = {:8.4f}, exact = {Wt_ex:.13e}, discrete = {Wt_num:.13e}'.format(0, + t, + Wt_ex = We_ex + Wb_ex, + Wt_num = We_num + Wb_num) + ) + else: + print('ts = {:4d}, t = {:8.4f}'.format(0, t)) + + #-------------------------------------------------------------------------- + # Solution + #-------------------------------------------------------------------------- + + # TODO: add option to convert to scipy sparse format + + # ... Arguments for time stepping + kwargs = {'verbose': verbose, 'tol': tol} + + if periodic: + M1_inv = inverse(M1, 'cg', **kwargs) + step_ampere_2d = dt * (M1_inv @ D1_T @ M2) + else: + M1_M1_bc = M1 + M1_bc + M1_M1_bc_inv = inverse(M1_M1_bc, 'pcg', pc = M1_M1_bc.diagonal(inverse=True), **kwargs) + step_ampere_2d = dt * (M1_M1_bc_inv @ D1_T @ M2) + + half_step_faraday_2d = (dt/2) * D1 + #minus_half_step_faraday_2d = (-dt/2) * D1 + + de = derham_h.V1.coeff_space.zeros() + db = derham_h.V2.coeff_space.zeros() + + # Time loop + for ts in range(1, nsteps+1): + # TODO: allow for high-order splitting + + # Strang splitting, 2nd order + b -= half_step_faraday_2d.dot(e, out=db) + e += step_ampere_2d.dot(b, out=de) + b -= half_step_faraday_2d.dot(e, out=db) + + #b -= half_step_faraday_2d @ e + #e += step_ampere_2d @ b + #b -= half_step_faraday_2d @ e + + # potential future PR: use "@" but internally vector.__iadd__() calls .idot() + + #minus_half_step_faraday_2d.idot(e, out = b) + #step_ampere_2d.idot(b, out = e) + #minus_half_step_faraday_2d.idot(e, out = b) + + t += dt + + # Animation + if plot_interval and (ts % plot_interval == 0 or ts == nsteps): + + # ... + # TODO: improve + for i, x1i in enumerate(x1[:, 0]): + for j, x2j in enumerate(x2[0, :]): + + Ex_values[i, j], Ey_values[i, j] = \ + push_2d_hcurl(E.fields[0], E.fields[1], x1i, x2j, F) + + Bz_values[i, j] = push_2d_l2(B, x1i, x2j, F) + # ... + + # Update plot + update_plot(fig2, t, x, y, Ex_values, Ex_ex(t, x, y)) + update_plot(fig3, t, x, y, Ey_values, Ey_ex(t, x, y)) + update_plot(fig4, t, x, y, Bz_values, Bz_ex(t, x, y)) + plt.pause(0.1) + + # Scalar diagnostics + if diagnostics_interval and ts % diagnostics_interval == 0: + + # Update exact diagnostics + We_ex, Wb_ex = diag.exact_energies(t) + diagnostics_ex['time'].append(t) + diagnostics_ex['electric_energy'].append(We_ex) + diagnostics_ex['magnetic_energy'].append(Wb_ex) + + # Update numerical diagnostics + We_num, Wb_num = diag.discrete_energies(e, b) + diagnostics_num['time'].append(t) + diagnostics_num['electric_energy'].append(We_num) + diagnostics_num['magnetic_energy'].append(Wb_num) + + # Print total energy to terminal + print('ts = {:4d}, t = {:8.4f}, exact = {Wt_ex:.13e}, discrete = {Wt_num:.13e}'.format(ts, + t, + Wt_ex = We_ex + Wb_ex, + Wt_num = We_num + Wb_num) + ) + else: + print('ts = {:4d}, t = {:8.4f}'.format(ts, t)) + + #-------------------------------------------------------------------------- + # Post-processing + #-------------------------------------------------------------------------- + if MPI.COMM_WORLD.size == 1: + # (currently not available in parallel) + # ... + # TODO: improve + for i, x1i in enumerate(x1[:, 0]): + for j, x2j in enumerate(x2[0, :]): + + Ex_values[i, j], Ey_values[i, j] = \ + push_2d_hcurl(E.fields[0], E.fields[1], x1i, x2j, F) + + Bz_values[i, j] = push_2d_l2(B, x1i, x2j, F) + # ... + + # Error at final time + error_Ex = abs(Ex_ex(t, x, y) - Ex_values).max() + error_Ey = abs(Ey_ex(t, x, y) - Ey_values).max() + error_Bz = abs(Bz_ex(t, x, y) - Bz_values).max() + print() + print('Max-norm of error on Ex(t,x) at final time: {:.2e}'.format(error_Ex)) + print('Max-norm of error on Ey(t,x) at final time: {:.2e}'.format(error_Ey)) + print('Max-norm of error on Bz(t,x) at final time: {:.2e}'.format(error_Bz)) + + # compute L2 error as well + F = mapping.get_callable_mapping() + errx = lambda x1, x2: (push_2d_hcurl(E.fields[0], E.fields[1], x1, x2, F)[0] - Ex_ex(t, *F(x1, x2)))**2 * np.sqrt(F.metric_det(x1,x2)) + erry = lambda x1, x2: (push_2d_hcurl(E.fields[0], E.fields[1], x1, x2, F)[1] - Ey_ex(t, *F(x1, x2)))**2 * np.sqrt(F.metric_det(x1,x2)) + errz = lambda x1, x2: (push_2d_l2(B, x1, x2, F) - Bz_ex(t, *F(x1, x2)))**2 * np.sqrt(F.metric_det(x1,x2)) + error_l2_Ex = np.sqrt(derham_h.V1.spaces[0].integral(errx, nquads=nquads)) + error_l2_Ey = np.sqrt(derham_h.V1.spaces[1].integral(erry, nquads=nquads)) + error_l2_Bz = np.sqrt(derham_h.V0.integral(errz, nquads=nquads)) + print('L2 norm of error on Ex(t,x,y) at final time: {:.2e}'.format(error_l2_Ex)) + print('L2 norm of error on Ey(t,x,y) at final time: {:.2e}'.format(error_l2_Ey)) + print('L2 norm of error on Bz(t,x,y) at final time: {:.2e}'.format(error_l2_Bz)) + + if diagnostics_interval: + + # Extract exact diagnostics + t_ex = np.asarray(diagnostics_ex['time']) + We_ex = np.asarray(diagnostics_ex['electric_energy']) + Wb_ex = np.asarray(diagnostics_ex['magnetic_energy']) + Wt_ex = We_ex + Wb_ex + + # Extract numerical diagnostics + t_num = np.asarray(diagnostics_num['time']) + We_num = np.asarray(diagnostics_num['electric_energy']) + Wb_num = np.asarray(diagnostics_num['magnetic_energy']) + Wt_num = We_num + Wb_num + + # Energy plots + fig3, (ax31, ax32, ax33) = plt.subplots(3, 1, figsize=(12, 10)) + # + ax31.set_title('Energy of exact solution') + ax31.plot(t_ex, We_ex, label='electric') + ax31.plot(t_ex, Wb_ex, label='magnetic') + ax31.plot(t_ex, Wt_ex, label='total' ) + ax31.legend() + ax31.set_xlabel('t') + ax31.set_ylabel('W', rotation='horizontal') + ax31.grid() + # + ax32.set_title('Energy of numerical solution') + ax32.plot(t_num, We_num, label='electric') + ax32.plot(t_num, Wb_num, label='magnetic') + ax32.plot(t_num, Wt_num, label='total' ) + ax32.legend() + ax32.set_xlabel('t') + ax32.set_ylabel('W', rotation='horizontal') + ax32.grid() + # + ax33.set_title('Relative error in total energy') + ax33.plot(t_ex , (Wt_ex - Wt_ex) / Wt_ex[0], '--', label='exact') + ax33.plot(t_num, (Wt_num - Wt_ex) / Wt_ex[0], '-' , label='numerical') + ax33.legend() + ax33.set_xlabel('t') + ax33.set_ylabel('(W - W_ex) / W_ex(t=0)') + ax33.grid() + # + fig3.tight_layout() + fig3.show() + + # Return whole namespace as dictionary + return locals() + +#============================================================================== +# UNIT TESTS +#============================================================================== + +def test_maxwell_2d_periodic(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 12, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_Ex = 6.870389e-03, + error_Ey = 6.870389e-03, + error_Bz = 4.443822e-03) + + assert abs(namespace['error_Ex'] - ref['error_Ex']) / ref['error_Ex'] <= TOL + assert abs(namespace['error_Ey'] - ref['error_Ey']) / ref['error_Ey'] <= TOL + assert abs(namespace['error_Bz'] - ref['error_Bz']) / ref['error_Bz'] <= TOL + +def test_maxwell_2d_multiplicity(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 10, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False, + mult = 2 + ) + + TOL = 1e-5 + ref = dict(error_l2_Ex = 4.350041934920621e-04, + error_l2_Ey = 4.350041934920621e-04, + error_l2_Bz = 3.76106860e-03) + + assert abs(namespace['error_l2_Ex'] - ref['error_l2_Ex']) / ref['error_l2_Ex'] <= TOL + assert abs(namespace['error_l2_Ey'] - ref['error_l2_Ey']) / ref['error_l2_Ey'] <= TOL + assert abs(namespace['error_l2_Bz'] - ref['error_l2_Bz']) / ref['error_l2_Bz'] <= TOL + +def test_maxwell_2d_periodic_multiplicity(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 30, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False, + mult =2 + ) + + TOL = 1e-6 + ref = dict(error_l2_Ex = 1.78557685e-04, + error_l2_Ey = 1.78557685e-04, + error_l2_Bz = 1.40582413e-04) + + assert abs(namespace['error_l2_Ex'] - ref['error_l2_Ex']) / ref['error_l2_Ex'] <= TOL + assert abs(namespace['error_l2_Ey'] - ref['error_l2_Ey']) / ref['error_l2_Ey'] <= TOL + assert abs(namespace['error_l2_Bz'] - ref['error_l2_Bz']) / ref['error_l2_Bz'] <= TOL + + +def test_maxwell_2d_periodic_multiplicity_equal_deg(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 10, + degree = 2, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False, + mult =2 + ) + + TOL = 1e-6 + ref = dict(error_l2_Ex = 2.50585008e-02, + error_l2_Ey = 2.50585008e-02, + error_l2_Bz = 1.58438290e-02) + + assert abs(namespace['error_l2_Ex'] - ref['error_l2_Ex']) / ref['error_l2_Ex'] <= TOL + assert abs(namespace['error_l2_Ey'] - ref['error_l2_Ey']) / ref['error_l2_Ey'] <= TOL + assert abs(namespace['error_l2_Bz'] - ref['error_l2_Bz']) / ref['error_l2_Bz'] <= TOL + + +def test_maxwell_2d_dirichlet(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 10, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_Ex = 3.597840e-03, + error_Ey = 3.597840e-03, + error_Bz = 4.366314e-03) + + assert abs(namespace['error_Ex'] - ref['error_Ex']) / ref['error_Ex'] <= TOL + assert abs(namespace['error_Ey'] - ref['error_Ey']) / ref['error_Ey'] <= TOL + assert abs(namespace['error_Bz'] - ref['error_Bz']) / ref['error_Bz'] <= TOL + + +def test_maxwell_2d_dirichlet_spline_mapping(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = True, + eps = None, + ncells = None, + degree = None, + periodic = None, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_Ex = 0.11197875072599534, + error_Ey = 0.11197875071916191, + error_Bz = 0.09616100464412525) + + assert abs(namespace['error_Ex'] - ref['error_Ex']) / ref['error_Ex'] <= TOL + assert abs(namespace['error_Ey'] - ref['error_Ey']) / ref['error_Ey'] <= TOL + assert abs(namespace['error_Bz'] - ref['error_Bz']) / ref['error_Bz'] <= TOL + + +@pytest.mark.parallel +def test_maxwell_2d_periodic_par(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 12, + degree = 3, + periodic = True, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_Ex = 4.2115063593622278e-03, + error_l2_Ey = 4.2115065915750306e-03, + error_l2_Bz = 3.6252141126597646e-03) + + assert abs(namespace['error_l2_Ex'] - ref['error_l2_Ex']) / ref['error_l2_Ex'] <= TOL + assert abs(namespace['error_l2_Ey'] - ref['error_l2_Ey']) / ref['error_l2_Ey'] <= TOL + assert abs(namespace['error_l2_Bz'] - ref['error_l2_Bz']) / ref['error_l2_Bz'] <= TOL + +@pytest.mark.parallel +def test_maxwell_2d_dirichlet_par(): + + namespace = run_maxwell_2d_TE( + use_spline_mapping = False, + eps = 0.5, + ncells = 10, + degree = 5, + periodic = False, + Cp = 0.5, + nsteps = 1, + tend = None, + splitting_order = 2, + plot_interval = 0, + diagnostics_interval = 0, + tol = 1e-6, + verbose = False + ) + + TOL = 1e-6 + ref = dict(error_l2_Ex = 1.3223335792411782e-03, + error_l2_Ey = 1.3223335792411910e-03, + error_l2_Bz = 4.0492562719804193e-03) + + assert abs(namespace['error_l2_Ex'] - ref['error_l2_Ex']) / ref['error_l2_Ex'] <= TOL + assert abs(namespace['error_l2_Ey'] - ref['error_l2_Ey']) / ref['error_l2_Ey'] <= TOL + assert abs(namespace['error_l2_Bz'] - ref['error_l2_Bz']) / ref['error_l2_Bz'] <= TOL + +#============================================================================== +# SCRIPT CAPABILITIES +#============================================================================== +if __name__ == '__main__': + + import argparse + + parser = argparse.ArgumentParser( + formatter_class = argparse.ArgumentDefaultsHelpFormatter, + description = "Solve 2D Maxwell's equations in rectangular cavity with spline FEEC method." + ) + + parser.add_argument('-s', '--spline', + action = 'store_true', + dest = 'use_spline_mapping', + help = 'Use spline mapping from geometry file "collela_2d.h5"' + ) + + # ... + disc_group = parser.add_argument_group('Discretization and geometry parameters (ignored for spline mapping)') + disc_group.add_argument('-n', + type = int, + default = 10, + dest = 'ncells', + help = 'Number of cells in domain ' + ) + disc_group.add_argument('-d', + type = int, + default = 3, + dest = 'degree', + help = 'Polynomial spline degree' + ) + disc_group.add_argument( '-P', '--periodic', + action = 'store_true', + help = 'Use periodic boundary conditions' + ) + disc_group.add_argument( '-e', + type = float, + default = 0.25, + dest = 'eps', + metavar = 'EPS', + help = 'Deformation level (0 <= EPS < 1)' + ) + # ... + + # ... + time_group = parser.add_argument_group('Time integration options') + time_group.add_argument('-o', + type = int, + default = 2, + dest = 'splitting_order', + choices = [2, 4, 6], + help = 'Order of accuracy of operator splitting' + ) + time_group.add_argument( '-c', + type = float, + default = 0.5, + dest = 'Cp', + metavar = 'Cp', + help = 'Courant parameter on uniform grid' + ) + time_opts = time_group.add_mutually_exclusive_group() + time_opts.add_argument( '-t', + type = int, + default = 1, + dest = 'nsteps', + metavar = 'NSTEPS', + help = 'Number of time-steps to be taken' + ) + time_opts.add_argument( '-T', + type = float, + dest = 'tend', + metavar = 'END_TIME', + help = 'Run simulation until given final time' + ) + # ... + + # ... + out_group = parser.add_argument_group('Output options') + out_group.add_argument( '-p', + type = int, + default = 4, + metavar = 'I', + dest = 'plot_interval', + help = 'No. of time steps between successive plots of solution, if I=0 no plots are made' + ) + out_group.add_argument( '-D', + type = int, + default = 1, + metavar = 'I', + dest = 'diagnostics_interval', + help = 'No. of time steps between successive calculations of scalar diagnostics, if I=0 no diagnostics are computed' + ) + # ... + + # ... + solver_group = parser.add_argument_group('Iterative solver') + solver_group.add_argument( '--tol', + type = float, + default = 1e-7, + help = 'Tolerance for iterative solver (L2-norm of residual)' + ) + + solver_group.add_argument( '-v', '--verbose', + action = 'store_true', + help = 'Print convergence information of iterative solver' + ) + # ... + + # Read input arguments + args = parser.parse_args() + + # Run simulation + namespace = run_maxwell_2d_TE(**vars(args)) + + # Keep matplotlib windows open + import matplotlib.pyplot as plt + plt.show() diff --git a/psydac/api/tests/test_api_feec_3d.py b/psydac/api/tests/test_api_feec_3d.py new file mode 100644 index 000000000..3a05bdf0b --- /dev/null +++ b/psydac/api/tests/test_api_feec_3d.py @@ -0,0 +1,430 @@ +# -*- coding: UTF-8 -*- + +from mpi4py import MPI +import pytest +import numpy as np + +from sympde.topology import Mapping +from sympde.calculus import grad, dot +from sympde.calculus import laplace +from sympde.topology import ScalarFunctionSpace +from sympde.topology import elements_of +from sympde.topology import NormalVector +from sympde.topology import Cube, Derham +from sympde.topology import Union +from sympde.expr import BilinearForm, LinearForm, integral +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.feec.pull_push import push_3d_hcurl, push_3d_hdiv +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.linalg.utilities import array_to_psydac +from psydac.linalg.solvers import inverse + +#=============================================================================== +def splitting_integrator_scipy(e0, b0, M1, M2, CURL, dt, niter): + + from scipy.sparse.linalg import splu + from scipy.sparse.linalg import LinearOperator as spLinOp + from scipy.sparse.linalg import aslinearoperator as asLinOp + + M1_lu = splu(M1) + M1_inv = spLinOp(shape=M1.shape, dtype=M1.dtype, matvec=M1_lu.solve) + + step_ampere = dt * (M1_inv @ asLinOp(CURL.T @ M2)) + step_faraday = dt * CURL + + e_history = [e0] + b_history = [b0] + + for ts in range(niter): + + b = b_history[ts] + e = e_history[ts] + + b_new = b - step_faraday.dot(e) + e_new = e + step_ampere .dot(b_new) + + b_history.append(b_new) + e_history.append(e_new) + + return e_history, b_history + + +def splitting_integrator_stencil(e0, b0, M1, M2, CURL, dt, niter): + + step_ampere = dt * ( inverse(M1, 'cg', tol=1e-12) @ CURL.T @ M2 ) + step_faraday = dt * CURL + + e_history = [e0] + b_history = [b0] + + de = e0.copy() + db = b0.copy() + + for ts in range(niter): + + b = b_history[ts].copy() + e = e_history[ts].copy() + + b -= step_faraday.dot(e, out=db) + e += step_ampere .dot(b, out=de) + + b_history.append(b) + e_history.append(e) + + return e_history, b_history + + +def evaluation_all_times(fields, x, y, z): + ak_value = np.empty(len(fields), dtype = 'float') + + for i in range(len(fields)): + ak_value[i] = fields[i](x,y,z) + return ak_value + +#================================================================================== +def run_maxwell_3d_scipy(logical_domain, mapping, e_ex, b_ex, ncells, degree, periodic, dt, niter, mult=1): + + #------------------------------------------------------------------------------ + # Symbolic objects: SymPDE + #------------------------------------------------------------------------------ + + domain = mapping(logical_domain) + derham = Derham(domain) + + u0, v0 = elements_of(derham.V0, names='u0, v0') + u1, v1 = elements_of(derham.V1, names='u1, v1') + u2, v2 = elements_of(derham.V2, names='u2, v2') + u3, v3 = elements_of(derham.V3, names='u3, v3') + + a0 = BilinearForm((u0, v0), integral(domain, u0*v0)) + a1 = BilinearForm((u1, v1), integral(domain, dot(u1, v1))) + a2 = BilinearForm((u2, v2), integral(domain, dot(u2, v2))) + a3 = BilinearForm((u3, v3), integral(domain, u3*v3)) + + # Callable mapping + F = mapping.get_callable_mapping() + + #------------------------------------------------------------------------------ + # Discrete objects: Psydac + #------------------------------------------------------------------------------ + + # Select backend for acceleration of the generated assembly code + backend = PSYDAC_BACKENDS['pyccel-gcc'] + + # Select multiplicity of internal knots along each direction + multiplicity = [mult, mult, mult] + + # Create Geometry & DiscreteDerham objects + domain_h = discretize(domain, ncells=ncells, periodic=periodic, comm=MPI.COMM_WORLD) + derham_h = discretize(derham, domain_h, degree=degree, multiplicity=multiplicity) + + # Create DiscreteBilinearForm objects. Assembly code is generated here + a1_h = discretize(a1, domain_h, (derham_h.V1, derham_h.V1), backend=backend) + a2_h = discretize(a2, domain_h, (derham_h.V2, derham_h.V2), backend=backend) + + # Assemble matrices as StencilMatrix objects, then convert them to SciPy's CSC/CSR formats + M1 = a1_h.assemble().tosparse().tocsc() + M2 = a2_h.assemble().tosparse().tocsr() + + # Diff operators + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') + + # Get projectors as objects of type Projector_H1, Projector_Hcurl, Projector_Hdiv, Projector_L2 + P0, P1, P2, P3 = derham_h.projectors(nquads=[5, 5, 5]) + + # Convert the CURL BlockLinearOperator to SciPy's CSR format + CURL = CURL.transform(lambda block: block.tokronstencil().tostencil()).tosparse().tocsr() + + # initial conditions + e0_1 = lambda x, y, z: e_ex[0](0, x, y, z) + e0_2 = lambda x, y, z: e_ex[1](0, x, y, z) + e0_3 = lambda x, y, z: e_ex[2](0, x, y, z) + + e0 = (e0_1, e0_2, e0_3) + + b0_1 = lambda x, y, z : b_ex[0](0, x, y, z) + b0_2 = lambda x, y, z : b_ex[1](0, x, y, z) + b0_3 = lambda x, y, z : b_ex[2](0, x, y, z) + + b0 = (b0_1, b0_2, b0_3) + + # project initial conditions + e0_coeff = P1(e0).coeffs + b0_coeff = P2(b0).coeffs + + # time integrator + e_history, b_history = splitting_integrator_scipy(e0_coeff.toarray(), b0_coeff.toarray(), M1, M2, CURL, dt, niter) + + # study of fields + b_history = [array_to_psydac(bi, derham_h.V2.coeff_space) for bi in b_history] + b_fields = [FemField(derham_h.V2, bi).fields for bi in b_history] + + bx_fields = [bi[0] for bi in b_fields] + by_fields = [bi[1] for bi in b_fields] + bz_fields = [bi[2] for bi in b_fields] + + bx_value_fun = lambda x, y, z: evaluation_all_times(bx_fields, x, y, z) + by_value_fun = lambda x, y, z: evaluation_all_times(by_fields, x, y, z) + bz_value_fun = lambda x, y, z: evaluation_all_times(bz_fields, x, y, z) + + x,y,z = derham_h.V0.breaks + x, y = 0.5, 0.5 + + b_values_0 = [] + for zi in z: + b_value_phys = push_3d_hdiv(bx_value_fun, by_value_fun, bz_value_fun, x, y, zi, F) + b_values_0.append(b_value_phys[0]) + b_values_0 = np.array(b_values_0) + + time_array = np.linspace(0, dt*niter, niter + 1) + tt, zz = np.meshgrid(time_array, z) + + b_ex_values_0 = b_ex[0](tt, x, y, zz) + + error = abs(b_values_0-b_ex_values_0).max() + return error + +#================================================================================== +def run_maxwell_3d_stencil(logical_domain, mapping, e_ex, b_ex, ncells, degree, periodic, dt, niter, mult=1): + + #------------------------------------------------------------------------------ + # Symbolic objects: SymPDE + #------------------------------------------------------------------------------ + + domain = mapping(logical_domain) + derham = Derham(domain) + + u0, v0 = elements_of(derham.V0, names='u0, v0') + u1, v1 = elements_of(derham.V1, names='u1, v1') + u2, v2 = elements_of(derham.V2, names='u2, v2') + u3, v3 = elements_of(derham.V3, names='u3, v3') + + a0 = BilinearForm((u0, v0), integral(domain, u0*v0)) + a1 = BilinearForm((u1, v1), integral(domain, dot(u1, v1))) + a2 = BilinearForm((u2, v2), integral(domain, dot(u2, v2))) + a3 = BilinearForm((u3, v3), integral(domain, u3*v3)) + + # Callable mapping + F = mapping.get_callable_mapping() + + #------------------------------------------------------------------------------ + # Discrete objects: Psydac + #------------------------------------------------------------------------------ + + # Select backend for acceleration of the generated assembly code + backend = PSYDAC_BACKENDS['pyccel-gcc'] + + # Select multiplicity of internal knots along each direction + multiplicity = [mult, mult, mult] + + # Create Geometry & DiscreteDerham objects + domain_h = discretize(domain, ncells=ncells, periodic=periodic, comm=MPI.COMM_WORLD) + derham_h = discretize(derham, domain_h, degree=degree, multiplicity=multiplicity) + + # Create DiscreteBilinearForm objects. Assembly code is generated here + a1_h = discretize(a1, domain_h, (derham_h.V1, derham_h.V1), backend=backend) + a2_h = discretize(a2, domain_h, (derham_h.V2, derham_h.V2), backend=backend) + + # Assemble matrices as StencilMatrix objects + M1 = a1_h.assemble() + M2 = a2_h.assemble() + + # Diff operators + GRAD, CURL, DIV = derham_h.derivatives(kind='linop') + + # Get projectors as objects of type Projector_H1, Projector_Hcurl, Projector_Hdiv, Projector_L2 + P0, P1, P2, P3 = derham_h.projectors(nquads=[5, 5, 5]) + + # initial conditions + e0_1 = lambda x, y, z: e_ex[0](0, x, y, z) + e0_2 = lambda x, y, z: e_ex[1](0, x, y, z) + e0_3 = lambda x, y, z: e_ex[2](0, x, y, z) + + e0 = (e0_1, e0_2, e0_3) + + b0_1 = lambda x, y, z : b_ex[0](0, x, y, z) + b0_2 = lambda x, y, z : b_ex[1](0, x, y, z) + b0_3 = lambda x, y, z : b_ex[2](0, x, y, z) + + b0 = (b0_1, b0_2, b0_3) + + # project initial conditions + e0_coeff = P1(e0).coeffs + b0_coeff = P2(b0).coeffs + + # time integrator + e_history, b_history = splitting_integrator_stencil(e0_coeff, b0_coeff, M1, M2, CURL, dt, niter) + + # study of fields + b_fields = [FemField(derham_h.V2, bi).fields for bi in b_history] + + bx_fields = [bi[0] for bi in b_fields] + by_fields = [bi[1] for bi in b_fields] + bz_fields = [bi[2] for bi in b_fields] + + bx_value_fun = lambda x, y, z: evaluation_all_times(bx_fields, x, y, z) + by_value_fun = lambda x, y, z: evaluation_all_times(by_fields, x, y, z) + bz_value_fun = lambda x, y, z: evaluation_all_times(bz_fields, x, y, z) + + x,y,z = derham_h.V0.breaks + x, y = 0.5, 0.5 + + b_values_0 = [] + for zi in z: + b_value_phys = push_3d_hdiv(bx_value_fun, by_value_fun, bz_value_fun, x, y, zi, F) + b_values_0.append(b_value_phys[0]) + b_values_0 = np.array(b_values_0) + + time_array = np.linspace(0, dt*niter, niter + 1) + tt, zz = np.meshgrid(time_array, z) + + b_ex_values_0 = b_ex[0](tt, x, y, zz) + + error = abs(b_values_0-b_ex_values_0).max() + return error +############################################################################### +# SERIAL TESTS +############################################################################### + +#============================================================================== +# 3D Maxwell's equations with "Collela" map +#============================================================================== +def test_maxwell_3d_1(): + class CollelaMapping3D(Mapping): + + _expressions = {'x': 'k1*(x1 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'y': 'k2*(x2 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'z': 'k3*x3'} + + _ldim = 3 + _pdim = 3 + + M = CollelaMapping3D('M', k1=1, k2=1, k3=1, eps=0.1) + logical_domain = Cube('C', bounds1=(0, 1), bounds2=(0, 1), bounds3=(0, 1)) + + # exact solution + e_ex_0 = lambda t, x, y, z: 0 + e_ex_1 = lambda t, x, y, z: -np.cos(2*np.pi*t-2*np.pi*z) + e_ex_2 = lambda t, x, y, z: 0 + + e_ex = (e_ex_0, e_ex_1, e_ex_2) + + b_ex_0 = lambda t, x, y, z : np.cos(2*np.pi*t-2*np.pi*z) + b_ex_1 = lambda t, x, y, z : 0 + b_ex_2 = lambda t, x, y, z : 0 + + b_ex = (b_ex_0, b_ex_1, b_ex_2) + + #space parameters + ncells = [2**4, 2**3, 2**5] + degree = [2, 2, 2] + periodic = [True, True, True] + + #time parameters + dt = 0.5*1/max(ncells) + niter = 10 + T = dt*niter + + error = run_maxwell_3d_scipy(logical_domain, M, e_ex, b_ex, ncells, degree, periodic, dt, niter) + assert abs(error - 0.04294761712765949) < 1e-9 + +#------------------------------------------------------------------------------ +def test_maxwell_3d_2(): + class CollelaMapping3D(Mapping): + + _expressions = {'x': 'k1*(x1 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'y': 'k2*(x2 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'z': 'k3*x3'} + + _ldim = 3 + _pdim = 3 + + M = CollelaMapping3D('M', k1=1, k2=1, k3=1, eps=0.1) + logical_domain = Cube('C', bounds1=(0, 1), bounds2=(0, 1), bounds3=(0, 1)) + + # exact solution + e_ex_0 = lambda t, x, y, z: 0 + e_ex_1 = lambda t, x, y, z: -np.cos(2*np.pi*t-2*np.pi*z) + e_ex_2 = lambda t, x, y, z: 0 + + e_ex = (e_ex_0, e_ex_1, e_ex_2) + + b_ex_0 = lambda t, x, y, z : np.cos(2*np.pi*t-2*np.pi*z) + b_ex_1 = lambda t, x, y, z : 0 + b_ex_2 = lambda t, x, y, z : 0 + + b_ex = (b_ex_0, b_ex_1, b_ex_2) + + #space parameters + ncells = [7, 7, 7] + degree = [2, 2, 2] + periodic = [True, True, True] + + #time parameters + dt = 0.5*1/max(ncells) + niter = 2 + T = dt*niter + + error = run_maxwell_3d_stencil(logical_domain, M, e_ex, b_ex, ncells, degree, periodic, dt, niter) + assert abs(error - 0.24586986658559362) < 1e-9 + +#------------------------------------------------------------------------------ +def test_maxwell_3d_2_mult(): + class CollelaMapping3D(Mapping): + + _expressions = {'x': 'k1*(x1 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'y': 'k2*(x2 + eps*sin(2.*pi*x1)*sin(2.*pi*x2))', + 'z': 'k3*x3'} + + _ldim = 3 + _pdim = 3 + + M = CollelaMapping3D('M', k1=1, k2=1, k3=1, eps=0.1) + logical_domain = Cube('C', bounds1=(0, 1), bounds2=(0, 1), bounds3=(0, 1)) + + # exact solution + e_ex_0 = lambda t, x, y, z: 0 + e_ex_1 = lambda t, x, y, z: -np.cos(2*np.pi*t-2*np.pi*z) + e_ex_2 = lambda t, x, y, z: 0 + + e_ex = (e_ex_0, e_ex_1, e_ex_2) + + b_ex_0 = lambda t, x, y, z : np.cos(2*np.pi*t-2*np.pi*z) + b_ex_1 = lambda t, x, y, z : 0 + b_ex_2 = lambda t, x, y, z : 0 + + b_ex = (b_ex_0, b_ex_1, b_ex_2) + + #space parameters + ncells = [7, 7, 7] + degree = [2, 2, 2] + periodic = [True, True, True] + + #time parameters + dt = 0.5*1/max(ncells) + niter = 2 + T = dt*niter + + error = run_maxwell_3d_stencil(logical_domain, M, e_ex, b_ex, ncells, degree, periodic, dt, niter, mult=2) + assert abs(error - 0.24749763720543216) < 1e-9 + +#============================================================================== +# CLEAN UP SYMPY NAMESPACE +#============================================================================== + +def teardown_module(): + from sympy.core import cache + cache.clear_cache() + +def teardown_function(): + from sympy.core import cache + cache.clear_cache() + +if __name__ == '__main__' : + test_maxwell_3d_2_mult() + + diff --git a/psydac/api/tests/test_assembly.py b/psydac/api/tests/test_assembly.py new file mode 100644 index 000000000..189b6c102 --- /dev/null +++ b/psydac/api/tests/test_assembly.py @@ -0,0 +1,595 @@ +import pytest +import numpy as np +from mpi4py import MPI +from sympy import pi, sin, cos, tan, atan, atan2, exp, sinh, cosh, tanh, atanh, Tuple, I, sqrt + +from sympde.topology import Line, Square +from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace +from sympde.topology import element_of, Derham +from sympde.core import Constant +from sympde.expr import LinearForm, BilinearForm, Functional, Norm +from sympde.expr import integral +from sympde.calculus import Inner + +from psydac.linalg.solvers import inverse +from psydac.api.discretization import discretize +from psydac.fem.basic import FemField +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.linalg.utilities import array_to_psydac + +#============================================================================== +@pytest.fixture(params=[None, 'pyccel-gcc']) +def backend(request): + return request.param + +@pytest.fixture(params=['real','complex']) +def dtype(request): + return request.param + + + # The assembly method of a BilinearForm applied a conjugate on the theoretical matrices to solve the good equation. + # In theory, we have the system A.conj(u)=conj(b) due to the complex dot product between the tests functions. + # In psydac, we have decided to assemble the matrix conj(A) and b to get the good solution. + +#============================================================================== +def test_field_and_constant(backend, dtype): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + u = element_of(V, name='u') + v = element_of(V, name='v') + f = element_of(V, name='f') + + if dtype == 'complex': + c = Constant(name='c', complex=True) + V.codomain_type = dtype + g = I * c * f**2 + res = 1.j + cst=complex(1.0) + else: + c = Constant(name='c', real=True) + g = c * f**2 + res = 1 + cst=1.0 + + a = BilinearForm((u, v), integral(domain, g * u * v)) + l = LinearForm(v, integral(domain, g * v)) + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + ah = discretize(a, domain_h, [Vh, Vh], **kwargs) + lh = discretize(l, domain_h, Vh , **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + + # Assembly call should not crash if correct arguments are used + A = ah.assemble(c=cst, f=fh) + b = lh.assemble(f=fh, c=cst) + + # Test matrix A + x = fh.coeffs + #TODO change res into np.conj(res) when the conjugate is applied in the dot product in sympde + assert abs(x.inner(A.dot(x)) - res) < 1e-12 + + # Test vector b + assert abs(b.toarray().sum() - res) < 1e-12 + print("PASSED") + +#============================================================================== +def test_field_and_constant_deg_0(backend, dtype): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + u = element_of(V, name='u') + v = element_of(V, name='v') + f = element_of(V, name='f') + + if dtype == 'complex': + c = Constant(name='c', complex=True) + V.codomain_type = dtype + g = I * c * f**2 + res = 1.j + cst=complex(1.0) + else: + c = Constant(name='c', real=True) + g = c * f**2 + res = 1 + cst=1.0 + + a = BilinearForm((u, v), integral(domain, g * u * v)) + l = LinearForm(v, integral(domain, g * v)) + + ncells = (5, 5) + degree = (0, 0) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + ah = discretize(a, domain_h, [Vh, Vh], **kwargs) + lh = discretize(l, domain_h, Vh , **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + + # Assembly call should not crash if correct arguments are used + A = ah.assemble(c=cst, f=fh) + b = lh.assemble(f=fh, c=cst) + + # Test matrix A + x = fh.coeffs + + #TODO change res into np.conj(res) when the conjugate is applied in the dot product in sympde + assert abs(x.inner(A.dot(x)) - res) < 1e-12 + + # Test vector b + assert abs(b.toarray().sum() - res) < 1e-12 + print("PASSED") + +#============================================================================== +def test_bilinearForm_complex(backend): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + V.codomain_type = 'complex' + u = element_of(V, name='u') + v = element_of(V, name='v') + f = element_of(V, name='f') + c = Constant(name='c', complex=True) + + res=(1.+1.j)/2 + # We try to put complex as a sympy object in the expression + g1 = (1.+I)/2 * c * f**2 + + # We try to put complex as a python scalar in the expression + g2 = res * c * f**2 + + # We try to put complex in a Sympde Constant in the expression or in a PSYDAC FemField in the expression + g3 = c * f**2 + + a1 = BilinearForm((u, v), integral(domain, u * v * g1)) + a2 = BilinearForm((u, v), integral(domain, u * v * g2)) + a3 = BilinearForm((u, v), integral(domain, u * v * g3)) + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + a1h = discretize(a1, domain_h, [Vh, Vh], **kwargs) + a2h = discretize(a2, domain_h, [Vh, Vh], **kwargs) + a3h = discretize(a3, domain_h, [Vh, Vh], **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + fh2 = FemField(Vh) + fh2.coeffs[:] = np.sqrt(res) + + # Assembly call should not crash if correct arguments are used + A1 = a1h.assemble(c=complex(1.0), f=fh) + A2 = a2h.assemble(c=complex(1.0), f=fh) + A3 = a3h.assemble(c=res, f=fh) + A4 = a3h.assemble(c=complex(1.0), f=fh2) + + # Test matrix A + x = fh.coeffs + + #TODO change res into np.conj(res) when the conjugate is applied in the dot product in sympde + assert abs(x.inner(A1.dot(x)) - res) < 1e-12 + assert abs(x.inner(A2.dot(x)) - res) < 1e-12 + assert abs(x.inner(A3.dot(x)) - res) < 1e-12 + assert abs(x.inner(A4.dot(x)) - res) < 1e-12 + + print("PASSED") + +#============================================================================== +def test_linearForm_complex(backend): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + V.codomain_type = 'complex' + v = element_of(V, name='v') + f = element_of(V, name='f') + c = Constant(name='c', complex=True) + + res = (1.+1.j)/2 + # We try to put complex as a sympy object in the expression + g1 = (1.+I)/2 * c * f**2 + + # We try to put complex as a python scalar in the expression + g2 = res * c * f**2 + + # We try to put complex in a Sympde Constant in the expression or in a PSYDAC FemField in the expression + g3 = c * f**2 + + l1 = LinearForm(v, integral(domain, g1 * v)) + l2 = LinearForm(v, integral(domain, g2 * v)) + l3 = LinearForm(v, integral(domain, g3 * v)) + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + l1h = discretize(l1, domain_h, Vh, **kwargs) + l2h = discretize(l2, domain_h, Vh, **kwargs) + l3h = discretize(l3, domain_h, Vh, **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + fh2 = FemField(Vh) + fh2.coeffs[:] = np.sqrt(res) + + # Assembly call should not crash if correct arguments are used + b1 = l1h.assemble(c=complex(1.0), f=fh) + b2 = l2h.assemble(c=complex(1.0), f=fh) + b3 = l3h.assemble(c=res, f=fh) + b4 = l3h.assemble(c=complex(1.0), f=fh2) + + + # Test vector b + assert abs(b1.toarray().sum() - res) < 1e-12 + assert abs(b2.toarray().sum() - res) < 1e-12 + assert abs(b3.toarray().sum() - res) < 1e-12 + assert abs(b4.toarray().sum() - res) < 1e-12 + + print("PASSED") + +#============================================================================== +def test_Norm_complex(backend): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + V.codomain_type = 'complex' + v = element_of(V, name='v') + c = Constant(name='c', complex=True) + + res = (1.+1j)/np.sqrt(2) + + # We try to put complex as a sympy object in the expression + g1 = (1.+I)/sqrt(2) + + # We try to put complex as a python scalar in the expression + g2 = res + + # We try to put complex in a Sympde Constant in the expression + g3 = c + + n1 = Norm(v-g1, domain) + n2 = Norm(v-g2, domain) + n3 = Norm(v-g3, domain) + + # We try to put complex in a PSYDAC FemField in the expression + n4 = Norm(v, domain) + + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + n1h = discretize(n1, domain_h, Vh, **kwargs) + n2h = discretize(n2, domain_h, Vh, **kwargs) + n3h = discretize(n3, domain_h, Vh, **kwargs) + n4h = discretize(n4, domain_h, Vh, **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + + fh2 = FemField(Vh) + fh2.coeffs[:] = np.sqrt(res) + + # Assembly call should not crash if correct arguments are used + r1 = n1h.assemble(v=fh) + r2 = n2h.assemble(v=fh) + r3 = n3h.assemble(v=fh, c=res) + r4 = n4h.assemble(v=fh2) + + # Test matrix A + assert abs(r1-0.7653668647301748) < 1e-12 + assert abs(r1 - r2) < 1e-12 + assert abs(r1 - r3) < 1e-12 + assert abs(r4 - 1) < 1e-12 + print("PASSED") + +#============================================================================== +@pytest.mark.parallel +def test_assemble_complex_parallel(backend): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = ScalarFunctionSpace('V', domain) + V.codomain_type = 'complex' + + Vr = ScalarFunctionSpace('Vr', domain) + Vr.codomain_type = 'complex' + + # TODO: remove codomain_type when It is implemented in sympde + u = element_of(V, name='u') + v = element_of(V, name='v') + f = element_of(V, name='f') + + c = Constant(name='c', complex=True) + gr = c * f**2 + gc = I * c * f**2 + cst = 1.0+0.0j + + ac = BilinearForm((u, v), integral(domain, gc * u * v)) + ar = BilinearForm((u, v), integral(domain, gr * u * v)) + lc = LinearForm(v, integral(domain, gc * v)) + lr = LinearForm(v, integral(domain, gr * v)) + nr = Norm(1.0 *v, domain, kind='l2') + nc = Norm(1.0j*v, domain, kind='l2') + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells, comm=MPI.COMM_WORLD) + Vh = discretize(V, domain_h, degree=degree) + Vrh = discretize(Vr, domain_h, degree=degree) + ach = discretize(ac, domain_h, [Vh, Vh], **kwargs) + arh = discretize(ar, domain_h, [Vrh, Vrh], **kwargs) + lch = discretize(lc, domain_h, Vh , **kwargs) + lrh = discretize(lr, domain_h, Vrh , **kwargs) + nch = discretize(nc, domain_h, Vh , **kwargs) + nrh = discretize(nr, domain_h, Vrh , **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + + # Assembly call should not crash if correct arguments are used + Ac = ach.assemble(c=cst, f=fh) + Ar = arh.assemble(c=cst, f=fh) + bc = lch.assemble(f=fh, c=cst) + br = lrh.assemble(f=fh, c=cst) + nc = nch.assemble(v=fh) + nr = nrh.assemble(v=fh) + + # Test matrix Ac and Ar + #TODO change Ar*1j into -Ar*1j when the conjugate is applied in the dot product in sympde + assert np.all(abs((Ac)._data-(Ar)._data*1j))<1e-16 + + # Test vector bc and br + assert np.all(abs((bc)._data-(br)._data*1j)<1e-16) + + # Test Norm nc and nr + assert abs(nc - nr) < 1e-8 +#============================================================================== +def test_multiple_fields(backend, dtype): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Line() + V = ScalarFunctionSpace('V', domain) + + # TODO: remove codomain_type when It is implemented in sympde + V.codomain_type = dtype + u = element_of(V, name='u') + v = element_of(V, name='v') + + f1 = element_of(V, name='f1') + f2 = element_of(V, name='f2') + + if dtype == 'complex': + g = 0.5j * (f1**2 + f2) + res = 1.j + else: + g = 0.5 * (f1**2 + f2) + res = 1 + + a = BilinearForm((u, v), integral(domain, u * v * g)) + l = LinearForm(v, integral(domain, g * v)) + + ncells = (5,) + degree = (3,) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + ah = discretize(a, domain_h, [Vh, Vh], **kwargs) + lh = discretize(l, domain_h, Vh , **kwargs) + + fh = FemField(Vh) + fh.coeffs[:] = 1 + + # Assembly call should not crash if correct arguments are used + A = ah.assemble(f1=fh, f2=fh) + b = lh.assemble(f1=fh, f2=fh) + + x = fh.coeffs + + # Test matrix A + #TODO change res into np.conj(res) when the conjugate is applied in the dot product in sympde + assert abs(x.inner(A.dot(x)) - res) < 1e-12 + + # Test vector b + assert abs(b.toarray().sum() - res) < 1e-12 + + print("PASSED") + +#============================================================================== +def test_math_imports(backend): + + # If 'backend' is specified, accelerate Python code by passing **kwargs + # to discretization of bilinear forms, linear forms and functionals. + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + (x, y) = domain.coordinates + + V = ScalarFunctionSpace('V', domain) + u = element_of(V, name='u') + v = element_of(V, name='v') + + k_a = sin(pi * x) + cos(pi * y) + tan(pi/2 * y/x) + atan(y/x) + atan2(x, y) + k_l = exp(x + y) + sinh(x) + cosh(y) + tanh(x - y) + atanh(x - y) + + a = BilinearForm((u, v), integral(domain, k_a * u * v)) + l = LinearForm(v, integral(domain, k_l * v)) + + ncells = (4, 4) + degree = (2, 2) + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + + # Python code generation works if printer recognizes all Sympy functions + ah = discretize(a, domain_h, [Vh, Vh], **kwargs) + lh = discretize(l, domain_h, Vh , **kwargs) + + # Assembly works if math functions' imports are compatible with calls + A = ah.assemble() + b = lh.assemble() + + # TODO: add meaningful assert statement + print("PASSED") + +#============================================================================== +def test_non_symmetric_BilinearForm(backend): + + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V1 = ScalarFunctionSpace('V1', domain) + V2 = VectorFunctionSpace('V2', domain) + + u = element_of(V2, name='u') + v = element_of(V1, name='v') + + a = BilinearForm((u, v), integral(domain, u[0] * v)) + + ncells = (5, 5) + degree = (3, 3) + domain_h = discretize(domain, ncells=ncells) + Vh1 = discretize(V1, domain_h, degree=degree) + Vh2 = discretize(V2, domain_h, degree=degree) + ah = discretize(a, domain_h, [Vh2, Vh1], **kwargs) + + A = ah.assemble() + + print("PASSED") + +#============================================================================== +def test_non_symmetric_different_space_BilinearForm(backend): + + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + domain = Square() + V = VectorFunctionSpace('V', domain, kind='Hdiv') + X = VectorFunctionSpace('X', domain, kind='h1') + + u = element_of(X, name='u') + w = element_of(V, name='w') + + A = BilinearForm((u, w), integral(domain, Inner(u, w))) + + ncells = [4, 4] + degree = [2, 2] + + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + Xh = discretize(X, domain_h, degree=degree) + + ah = discretize(A, domain_h, (Xh, Vh), **kwargs) + A = ah.assemble() + + print("PASSED") + +#============================================================================== +def test_assembly_no_synchr_args(backend): + + kwargs = {'backend': PSYDAC_BACKENDS[backend]} if backend else {} + + nc = 5 + ncells = (nc,) + degree = (2,) + periodic = (True,) + + domain = Line() + domain_h = discretize(domain, ncells=ncells, periodic=periodic) + + derham = Derham(domain) + derham_h = discretize(derham, domain_h, degree=degree) + + #spaces + V0h = derham_h.V0 + V1h = derham_h.V1 + + #differential operator + div, = derham_h.derivatives(kind='linop') + + rho = element_of(V1h.symbolic_space, name='rho') + g = element_of(V1h.symbolic_space, name='g') + h = element_of(V1h.symbolic_space, name='h') + + #L2 proj rho u -> V1 + expr = g*h*rho + weight_int_prod = BilinearForm((g,h), integral(domain, expr)) + weight_int_prod_h = discretize(weight_int_prod, domain_h, (V1h,V1h), **kwargs) + + expr = g*rho + int_prod = LinearForm(g, integral(domain, expr)) + int_prod_h = discretize(int_prod, domain_h, V1h, **kwargs) + + func = Functional(rho, domain) + func_h = discretize(func, domain_h, V1h, **kwargs) + + uh = array_to_psydac(np.array([i for i in range(nc)]), V0h.coeff_space) + const_1 = array_to_psydac(np.array([1/nc]*nc), V1h.coeff_space) + + rhoh1 = div.dot(uh) + rhof1 = FemField(V1h, rhoh1) + rhoh2 = div.dot(uh) + rhof2 = FemField(V1h, rhoh2) + rhoh3 = div.dot(uh) + rhof3 = FemField(V1h, rhoh3) + weight_mass_matrix = weight_int_prod_h.assemble(rho=rhof1) + inte_bilin = const_1.inner(weight_mass_matrix.dot(const_1)) + + int_prod_rho = int_prod_h.assemble(rho=rhof2) + inte_lin = int_prod_rho.inner(const_1) + + inte_norm = func_h.assemble(rho=rhof3) + + assert( abs(inte_bilin) < 1.e-12) + assert( abs(inte_lin) < 1.e-12) + assert( abs(inte_norm) < 1.e-12) + +#============================================================================== +if __name__ == '__main__': + test_Norm_complex(None) + exit() + test_field_and_constant(None) + test_multiple_fields(None) + test_math_imports(None) + test_non_symmetric_BilinearForm(None) + test_non_symmetric_different_space_BilinearForm(None) + test_assembly_no_synchr_args(None) diff --git a/psydac/api/tests/test_epyccel_flags.py b/psydac/api/tests/test_epyccel_flags.py new file mode 100644 index 000000000..3a85ddcf0 --- /dev/null +++ b/psydac/api/tests/test_epyccel_flags.py @@ -0,0 +1,39 @@ +import pytest + + +@pytest.mark.pyccel +def test_epyccel_flags(): + + from pyccel import epyccel + from psydac.api.settings import PSYDAC_BACKENDS + + # Select the Pyccel backend which uses the GCC compilers + backend = PSYDAC_BACKENDS['pyccel-gcc'] + + # Arguments for `epyccel` + kwargs = {'language' : 'fortran', + 'compiler_family' : backend['compiler_family'], + 'flags' : backend['flags'], + 'openmp' : backend['openmp'], + 'verbose' : True, + } + + # Function to be Pyccel-ized + def f(x : float): + return 3 * x + + # Pyccel magic + # ------------ + # Fortran code is generated and then compiled with the selected compiler family. + # The compiled function is callable from Python through the C Python API. + # The necessary C wrapper functions are also generated by Pyccel. + fast_f = epyccel(f, **kwargs) + + # Check output of pyccelized function + assert fast_f(3.5) == f(3.5) + + +# Interactive usage +if __name__ == '__main__': + test_epyccel_flags() + print('PASSED') diff --git a/psydac/api/tests/test_sum_factorization_assembly_3d.py b/psydac/api/tests/test_sum_factorization_assembly_3d.py new file mode 100644 index 000000000..7aab017a3 --- /dev/null +++ b/psydac/api/tests/test_sum_factorization_assembly_3d.py @@ -0,0 +1,592 @@ +import os +import pytest +import time +import numpy as np + +from sympy import sin, sqrt, pi, Abs, cos, tan + +from sympde.calculus import dot, cross, grad, curl, div, laplace +from sympde.expr import BilinearForm, integral +from sympde.topology import element_of, elements_of, Cube, Mapping, ScalarFunctionSpace, VectorFunctionSpace, Domain, Derham + +from psydac.api.discretization import discretize +from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL +from psydac.linalg.block import BlockVectorSpace +from psydac.fem.basic import FemField + +try: + mesh_dir = os.environ['PSYDAC_MESH_DIR'] + +except: + base_dir = os.path.dirname(os.path.realpath(__file__)) + base_dir = os.path.join(base_dir, '..', '..', '..') + mesh_dir = os.path.join(base_dir, 'mesh') + + +# With PR #448, matrices corresponding to bilinear forms on 3D domains are being assembled using a so called sum factorization algorithm. +# Unless explicitely using the old algorithm, this happens automatically, and hence all old tests passing should indicate that the implementation of the sum factorization algorithm has been successful. +# Nonetheless, there are various difficulties in the implementation, and possibly not all of them are accounted for in the existing tests. + +# This file is designed to test such "difficult" edge cases - for mapped (Bspline & analytical) and parametric domains: + +# Such "difficult" edge cases are: + +# 1. bilinear forms on different spaces +# 2. (FemField / analytical / ...) weight functions +# 3. high derivatives (>=2) +# 4. (multiple) free FemFields +# 5. complicated expressions +# 6. uncommen (numpy) functions that need be imported correctly in the assembly file (here in the weight function) + +# These tests also return old and new discretization and assembly times. + +# Most of the time, being close to the "old matrix" (generated using the old assembly algorithm) will be the requirement to pass a test, +# as the old implementation has not caused problems in a long time and is considered to function properly. + +# Update: Instead of testing all mapping options all of the time, we now rather randomly test one of the three options! +#@pytest.mark.parametrize('mapping', ('None', 'Analytical', 'Bspline')) +def test_assembly(): # mapping): + + rng = np.random.default_rng() # (seed=42) + mapping_options = ['None', 'Analytical', 'Bspline'] + mapping = mapping_options[int(np.floor(rng.random()*3))] + + ncells = [7, 5, 6] + degree = [2, 4, 3] + periodic = [int(np.floor(rng.random()*2))==True for _ in range(3)] + print(f'Random periodicity: {periodic}') + + trial_multiplicity = [1, 3, 2] + test_multiplicity = [2, 2, 3] + + backend = PSYDAC_BACKEND_GPYCCEL + + if mapping == 'None': + + domain = Cube('C', bounds1=(0,1), bounds2=(0,1), bounds3=(0,1)) + derham = Derham(domain) + + domain_h = discretize(domain, ncells=ncells, periodic=periodic) + derham_h = discretize(derham, domain_h, degree=degree, multiplicity=trial_multiplicity) + derham_test_h = discretize(derham, domain_h, degree=degree, multiplicity=test_multiplicity) + + elif mapping == 'Bspline': + + filename = os.path.join(mesh_dir, 'identity_3d.h5') + + domain = Domain.from_file(filename=filename) + derham = Derham(domain) + + domain_h = discretize(domain, filename=filename) + derham_h = discretize(derham, domain_h, degree=domain.mapping.get_callable_mapping().space.degree, multiplicity=trial_multiplicity) + derham_test_h = discretize(derham, domain_h, degree=domain.mapping.get_callable_mapping().space.degree, multiplicity=test_multiplicity) + + elif mapping == 'Analytical': + + class HalfSquareTorusMapping3D(Mapping): + _expressions = {'x': 'x1 * cos(x2)', + 'y': 'x1 * sin(x2)', + 'z': 'x3'} + + _ldim = 3 + _pdim = 3 + + M = HalfSquareTorusMapping3D('M') + logical_domain = Cube('C', bounds1=(0.3,1), bounds2=(0,np.pi), bounds3=(0,1)) + + domain = M(logical_domain) + derham = Derham(domain) + + domain_h = discretize(domain, ncells=ncells, periodic=periodic) + derham_h = discretize(derham, domain_h, degree=degree, multiplicity=trial_multiplicity) + derham_test_h = discretize(derham, domain_h, degree=degree, multiplicity=test_multiplicity) + + x, y, z = domain.coordinates + weight = 1 + sqrt(Abs(x*y**2 + z)) + abs(sin(x-2*y + pi + np.pi)) + + V0 = derham.V0 + V0h = derham_h.V0 + V1 = derham.V1 + V1h = derham_h.V1 + V2 = derham.V2 + V2h = derham_h.V2 + V3 = derham.V3 + V3h = derham_h.V3 + + V0h_test = derham_test_h.V0 + V1h_test = derham_test_h.V1 + V2h_test = derham_test_h.V2 + V3h_test = derham_test_h.V3 + + Vs = ScalarFunctionSpace('Vsh', domain) + Vsh = discretize(Vs, domain_h, degree=degree) + Vvc = VectorFunctionSpace('Vvc', domain, kind='hcurl') + Vvch = discretize(Vvc, domain_h, degree=degree) + Vvd = VectorFunctionSpace('Vvd', domain, kind='hdiv') + Vvdh = discretize(Vvd, domain_h, degree=degree) + + Vsh_test = discretize(Vs, domain_h, degree=degree, multiplicity=test_multiplicity) + Vvch_test = discretize(Vs, domain_h, degree=degree, multiplicity=test_multiplicity) + Vvdh_test = discretize(Vs, domain_h, degree=degree, multiplicity=test_multiplicity) + + u1, u2, F01, F02, F03 = elements_of(V0, names='u1, u2, F01, F02, F03') + v1, v2, F11, F12, F13 = elements_of(V1, names='v1, v2, F11, F12, F13') + w1, w2, F21, F22, F23 = elements_of(V2, names='w1, w2, F21, F22, F23') + f1, f2, F31, F32, F33 = elements_of(V3, names='f1, f2, F31, F32, F33') + + fs1, fs2, Fs1, Fs2, Fs3 = elements_of(Vs, names='fs1, fs2, Fs1, Fs2, Fs3') + fvc1, fvc2, Fvc1, Fvc2, Fvc3 = elements_of(Vvc, names='fvc1, fvc2, Fvc1, Fvc2, Fvc3') + fvd1, fvd2, Fvd1, Fvd2, Fvd3 = elements_of(Vvd, names='fvd1, fvd2, Fvd1, Fvd2, Fvd3') + + trial_spaces = {'V0': {'Vh':V0h, 'funcs':[u1, u2]}, + 'V1': {'Vh':V1h, 'funcs':[v1, v2]}, + 'V2': {'Vh':V2h, 'funcs':[w1, w2]}, + 'V3': {'Vh':V3h, 'funcs':[f1, f2]}, + 'Vs': {'Vh':Vsh, 'funcs':[fs1, fs2]}, + 'Vvc':{'Vh':Vvch, 'funcs':[fvc1, fvc2]}, + 'Vvd':{'Vh':Vvdh, 'funcs':[fvd1, fvd2]}} + + test_spaces = {'V0': {'Vh':V0h_test, 'funcs':[u1, u2]}, + 'V1': {'Vh':V1h_test, 'funcs':[v1, v2]}, + 'V2': {'Vh':V2h_test, 'funcs':[w1, w2]}, + 'V3': {'Vh':V3h_test, 'funcs':[f1, f2]}, + 'Vs': {'Vh':Vsh_test, 'funcs':[fs1, fs2]}, + 'Vvc':{'Vh':Vvch_test, 'funcs':[fvc1, fvc2]}, + 'Vvd':{'Vh':Vvdh_test, 'funcs':[fvd1, fvd2]}} + + F01_coeffs = V0h.coeff_space.zeros() + rng.random(size=F01_coeffs._data.shape, dtype='float64', out=F01_coeffs._data) + F11_coeffs = V1h.coeff_space.zeros() + for block in F11_coeffs.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + F12_coeffs = V1h.coeff_space.zeros() + for block in F12_coeffs.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + F21_coeffs = V2h.coeff_space.zeros() + for block in F21_coeffs.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + Fvc1_coeffs = Vvch.coeff_space.zeros() + for block in Fvc1_coeffs.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + Fs1_coeffs = Vsh.coeff_space.zeros() + rng.random(size=Fs1_coeffs._data.shape, dtype='float64', out=Fs1_coeffs._data) + Fs2_coeffs = Vsh.coeff_space.zeros() + rng.random(size=Fs2_coeffs._data.shape, dtype='float64', out=Fs2_coeffs._data) + Fvd1_coeffs = Vvdh.coeff_space.zeros() + for block in Fvd1_coeffs.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + + F01_field = FemField(V0h, F01_coeffs) + F11_field = FemField(V1h, F11_coeffs) + F12_field = FemField(V1h, F12_coeffs) + F21_field = FemField(V2h, F21_coeffs) + Fvc1_field = FemField(Vvch, Fvc1_coeffs) + Fs1_field = FemField(Vsh, Fs1_coeffs) + Fs2_field = FemField(Vsh, Fs2_coeffs) + Fvd1_field = FemField(Vvdh, Fvd1_coeffs) + + bilinear_forms = { # one and two free FemFields without derivatives (with derivatives in seperate test) + # complicated expressions + 'Q' :{'trial' :'V1', 'test':'V1', + 'expr' :dot(cross(F11, v1), cross(F11, v2)), + 'fields':[F11_field, ]}, + 'equilibrium' :{'trial' :'V1', 'test':'V1', + 'expr' :dot(cross(F11, v1), cross(v2, F12)), + 'fields':[F11_field, F12_field]}, + 'Elena' :{'trial' :'V1', 'test':'V1', + 'expr' :dot(F01*v1, v2), + 'fields':[F01_field, ]}, + # weight function, free FemField, different spaces + 'dot(grad(u),v)':{'trial' :'V0', 'test':'V1', + 'expr' :dot(grad(u1), v2)*F01*weight, + 'fields':[F01_field, ]}, + 'dot(curl(v),w)':{'trial' :'V1', 'test':'V2', + 'expr' :dot(curl(v1), F21)*div(w2)*weight, + 'fields':[F21_field, ]}, + # among other difficulties: multiple scalar FemFields + 'ScalarFields' :{'trial' :'V0', 'test':'V1', + 'expr' :dot(grad(u1), curl(Fvc1))*dot(grad(Fs1), curl(v2))*Fs2*div(Fvd1), + 'fields':[Fvc1_field, Fs1_field, Fs2_field, Fvd1_field]}, + # high derivatives, not FEEC + 'bilaplace' :{'trial' :'Vs', 'test':'Vs', + 'expr' :laplace(fs1)*laplace(fs2)} + } + + # test all BFs + # bilinear_form_strings_to_test = list(bilinear_forms.keys()) + + # or only a subset + bilinear_form_strings_to_test = list(bilinear_forms.keys())[:-1] # exclude expensive bilaplace test + + bilinear_forms_to_test = {} + for name in bilinear_form_strings_to_test: + bilinear_forms_to_test[name] = bilinear_forms[name] + + int_0 = lambda expr: integral(domain, expr) + print() + + for bf_name, bf_data in bilinear_forms_to_test.items(): + + trial_space = trial_spaces[bf_data['trial']] + Vh = trial_space['Vh'] + u = trial_space['funcs'][0] + test_space = test_spaces[bf_data['test']] + Wh = test_space ['Vh'] + v = test_space['funcs'][1] + expr = bf_data['expr'] + if 'fields' in bf_data.keys(): + fields = bf_data['fields'] + + a = BilinearForm((u, v), int_0(expr)) + + t0 = time.time() + ah_old = discretize(a, domain_h, (Vh, Wh), backend=backend, sum_factorization=False) + t1 = time.time() + discretization_time_old = t1 - t0 + + t0 = time.time() + ah = discretize(a, domain_h, (Vh, Wh), backend=backend) + t1 = time.time() + discretization_time = t1 - t0 + + if bf_name == 'Q': + t0_old = time.time() + A_old = ah_old.assemble(F11=fields[0]) + t1_old = time.time() + + t0 = time.time() + A = ah.assemble(F11=fields[0]) + t1 = time.time() + elif bf_name == 'equilibrium': + t0_old = time.time() + A_old = ah_old.assemble(F11=fields[0], F12=fields[1]) + t1_old = time.time() + + t0 = time.time() + A = ah.assemble(F11=fields[0], F12=fields[1]) + t1 = time.time() + elif bf_name in ('Elena', 'dot(grad(u),v)'): + t0_old = time.time() + A_old = ah_old.assemble(F01=fields[0]) + t1_old = time.time() + + t0 = time.time() + A = ah.assemble(F01=fields[0]) + t1 = time.time() + elif bf_name == 'dot(curl(v),w)': + t0_old = time.time() + A_old = ah_old.assemble(F21=fields[0]) + t1_old = time.time() + + t0 = time.time() + A = ah.assemble(F21=fields[0]) + t1 = time.time() + elif bf_name == 'ScalarFields': + t0_old = time.time() + A_old = ah_old.assemble(Fvc1=fields[0], Fs1=fields[1], Fs2=fields[2], Fvd1=fields[3]) + t1_old = time.time() + + t0 = time.time() + A = ah.assemble(Fvc1=fields[0], Fs1=fields[1], Fs2=fields[2], Fvd1=fields[3]) + t1 = time.time() + else: + t0_old = time.time() + A_old = ah_old.assemble() + t1_old = time.time() + + t0 = time.time() + A = ah.assemble() + t1 = time.time() + + assembly_time_old = t1_old - t0_old + assembly_time = t1 - t0 + + # Testing whether two linear operators are identical by comparing their arrays is quite expensive. + # Thus we instead test whether three random domain vectors applied to both + # the old and the new matrix produce the same codomain vector. + + domain_vector1 = Vh.coeff_space.zeros() + domain_vector2 = Vh.coeff_space.zeros() + domain_vector3 = Vh.coeff_space.zeros() + domain_vectors = [domain_vector1, domain_vector2, domain_vector3] + + if isinstance(Vh.coeff_space, BlockVectorSpace): + for domain_vector in domain_vectors: + for block in domain_vector.blocks: + rng.random(size=block._data.shape, dtype='float64', out=block._data) + else: + for domain_vector in domain_vectors: + rng.random(size=domain_vector._data.shape, dtype='float64', out=domain_vector._data) + + err = [] + rel_err = [] + + for domain_vector in domain_vectors: + codomain_vector = A @ domain_vector + codomain_vector_old = A_old @ domain_vector + + norm_old = np.sqrt(codomain_vector_old.inner(codomain_vector_old)) + + diff = codomain_vector - codomain_vector_old + + err.append(np.sqrt(diff.inner(diff))) + rel_err.append(err[-1] / norm_old) + + print(f' >>> Mapping: {mapping}') + print(f' >>> BilinearForm: {bf_name}') + print(f' >>> Discretization in: Old {discretization_time_old:.3g} \t\t || New {discretization_time:.3g} \t\t || Old/New {discretization_time_old/discretization_time:.3g}') + print(f' >>> Assembly in: Old {assembly_time_old:.3g} \t \t || New {assembly_time:.3g} \t\t || Old/New {assembly_time_old/assembly_time:.3g}') + print(f' >>> Error: {max(err):.3g}') + print(f' >>> Rel. Error: {max(rel_err):.3g}') + print() + + assert max(rel_err) < 1e-12 # arbitrary rel. error bound (How to test better?) + + +# fixed by PR #507 +#@pytest.mark.xfail +def test_allocate_matrix_bug(): + """ + This test is related to Issue #504. + + The bilinear form + (V0 x V3) ni (u, f) mapsto int_{Omega} u*f + should be the transpose of the bilinear form + (V3 x V0) ni (f, u) mapsto int_{Omega} u*f + but is not. + """ + + ncells = [15, 16, 17] + degree = [4, 3, 2] + periodic = [False, True, False] + + backend = PSYDAC_BACKEND_GPYCCEL + + domain = Cube('C', bounds1=(0,1), bounds2=(0,1), bounds3=(0,1)) + derham = Derham(domain) + + domain_h = discretize(domain, ncells=ncells, periodic=periodic) + derham_h = discretize(derham, domain_h, degree=degree) + + P0, _, _, P3 = derham_h.projectors() + + V0 = derham.V0 + V0h = derham_h.V0 + V3 = derham.V3 + V3h = derham_h.V3 + + u = element_of(V0, name='u') + f = element_of(V3, name='f') + + fun = lambda x, y, z : 1 + u_coeffs = P0(fun).coeffs + f_coeffs = P3(fun).coeffs + + a0 = BilinearForm((u, f), integral(domain, u*f)) + a1 = BilinearForm((f, u), integral(domain, u*f)) + + a0h = discretize(a0, domain_h, (V0h, V3h), backend=backend, sum_factorization=False) + a1h = discretize(a1, domain_h, (V3h, V0h), backend=backend, sum_factorization=False) + + A0 = a0h.assemble() + A1 = a1h.assemble() + A1T = A1.T + + # Clearly, it should hold A1T = A0, and further ||A0|| = ||A1||. + A0arr = A0.toarray() + A1arr = A1.toarray() + A1Tarr = A1T.toarray() + + diff1 = np.linalg.norm(A0arr - A1Tarr) + diff2 = np.linalg.norm(A0arr) - np.linalg.norm(A1arr) + + print(f' || A0 - A1.T || = {diff1:.3g}') + print(f' ||A0|| - ||A1|| = {diff2:.3g}') + + # Further, the following integral should evaluate to 1. + # This however is only the case for the second integral, + # independent on whether one uses the new or old assembly algorithm. + + print(f' 1 =? {A0.dot_inner(u_coeffs, f_coeffs)}') + print(f' 1 =? {A1.dot_inner(f_coeffs, u_coeffs)}') + + assert diff1 <= 1e-12 # arbitrary error bound + assert diff2 <= 1e-12 # arbitrary error bound + +#@pytest.mark.xfail +def test_free_FemField_derivatives(): + """ + These particular bilinear forms, when using a constant 1-vector coefficient vector for the free FemFields, + causes problems in a different test file of mine. + In particular, the assembled matrices used to have really small norms (~e-13). + That is probably due to the constant 1-vector corresponding to a constant function, + which means that all appearing derivatives of free FemFields are 0. + + When using meaningful free FemFields, these dubious observations disappeared. + + """ + + ncells = [5, 2, 4] + degree = [2, 1, 3] + periodic = [False, False, False] + + backend = PSYDAC_BACKEND_GPYCCEL + + domain = Cube('C', bounds1=(0,1), bounds2=(0,1), bounds3=(0,1)) + derham = Derham(domain) + + domain_h = discretize(domain, ncells=ncells, periodic=periodic) + derham_h = discretize(derham, domain_h, degree=degree) + + P0, P1, P2, P3 = derham_h.projectors() + + V0 = derham.V0 + V0h = derham_h.V0 + V1 = derham.V1 + V1h = derham_h.V1 + V2 = derham.V2 + V2h = derham_h.V2 + + u = element_of (V0, name= 'u') + v, F1 = elements_of(V1, names='v, F1') + w1, w2, F2 = elements_of(V2, names='w1, w2, F2') + + u_func = lambda x, y, z: x + y + z + u_coeffs = P0(u_func).coeffs + + v_1 = lambda x, y, z: 1 + v_2 = lambda x, y, z: 1 + v_3 = lambda x, y, z: 1 + v_func = (v_1, v_2, v_3) + v_coeffs = P1(v_func).coeffs + + w_1 = lambda x, y, z: x + w_2 = lambda x, y, z: y + w_3 = lambda x, y, z: z + w_func = (w_1, w_2, w_3) + w_coeffs = P2(w_func).coeffs + + dubious_observations = False + + if dubious_observations: + F1_coeffs = V1h.coeff_space.zeros() + F2_coeffs = V2h.coeff_space.zeros() + for block in F1_coeffs.blocks: + block._data = np.ones(block._data.shape, dtype='float64') + for block in F2_coeffs.blocks: + block._data = np.ones(block._data.shape, dtype='float64') + F1_FF = FemField(V1h, F1_coeffs) + F2_FF = FemField(V2h, F2_coeffs) + else: + F1_1 = lambda x, y, z: z + F1_2 = lambda x, y, z: x + F1_3 = lambda x, y, z: y + F1_func = (F1_1, F1_2, F1_3) + F1_FF = P1(F1_func) + + F2_FF = FemField(V2h, w_coeffs.copy()) + + # with the above choices (dubious_observation = False): + # a0 reduces to 3*int_{Omega}x+y+z with Omega being the unit square. Expected value: 4.5 + a0 = BilinearForm((u, v), integral(domain, dot(grad(u), curl(F1)) * dot(v, F1))) + # a1 reduces to 9* ----------------------------------------- " -------------------- 13.5 + a1 = BilinearForm((u, w2), integral(domain, dot(grad(u), F2)*div(w2)*div(F2))) + # a2 reduces to 3* ----------------------------------------- " --------------------- 4.5 + a2 = BilinearForm((w1, w2), integral(domain, dot(curl(F1), w1)*div(w2))) + + a0h_old = discretize(a0, domain_h, (V0h, V1h), backend=backend, sum_factorization=False) + a1h_old = discretize(a1, domain_h, (V0h, V2h), backend=backend, sum_factorization=False) + a2h_old = discretize(a2, domain_h, (V2h, V2h), backend=backend, sum_factorization=False) + + a0h = discretize(a0, domain_h, (V0h, V1h), backend=backend) + a1h = discretize(a1, domain_h, (V0h, V2h), backend=backend) + a2h = discretize(a2, domain_h, (V2h, V2h), backend=backend) + + bfs = [(a0h_old, a0h), (a1h_old, a1h), (a2h_old, a2h)] + print() + + for i, (ah_old, ah) in enumerate(bfs): + + if i in (0, 2): + A_old = ah_old.assemble(F1=F1_FF) + A = ah.assemble(F1=F1_FF) + else: + A_old = ah_old.assemble(F2=F2_FF) + A = ah.assemble(F2=F2_FF) + + if i == 0: + value_old = A_old.dot_inner(u_coeffs, v_coeffs) + value = A.dot_inner(u_coeffs, v_coeffs) + elif i == 1: + value_old = A_old.dot_inner(u_coeffs, w_coeffs) + value = A.dot_inner(u_coeffs, w_coeffs) + else: + value_old = A_old.dot_inner(w_coeffs, w_coeffs) + value = A.dot_inner(w_coeffs, w_coeffs) + + A_old_arr = A_old.toarray() + A_arr = A.toarray() + A_old_norm = np.linalg.norm(A_old_arr) + A_norm = np.linalg.norm(A_arr) + + err = np.linalg.norm(A_old_arr - A_arr) + rel_err = err / A_old_norm + + print(f' i = {i}') + print(f' >>> Error: {err:.3g}') + print(f' >>> Rel. Error: {rel_err:.3g}') + print(f' >>> Norms: ||A_old|| = {A_old_norm:.3g} \t\t ||A|| = {A_norm:.3g}') + print() + + # arbitrary tolerance + tol = 1e-12 + if not dubious_observations: + assert abs(value-value_old) < tol + assert rel_err < tol + +def test_assembly_free_FemFields(): + + backend = PSYDAC_BACKEND_GPYCCEL + + domain = Cube(bounds1=(2626,3179), bounds2=(-138, 138), bounds3=(-760.3, 69)) + derham = Derham(domain) + V0 = derham.V0 + V1 = derham.V1 + + u = element_of(V1, name='u') + v = element_of(V1, name='v') + p = element_of(V0, name='p') + + p_call = lambda xi,yi,zi: np.cos(2*np.pi*(xi-2626)/553) + np.tan(2*np.pi*(zi+760.3)/(5*829.3)) + x,y,z = domain.coordinates + p_sym = cos(2*np.pi*(x-2626)/553) + tan(2*np.pi*(z+760.3)/(5*829.3)) + + a_sym = BilinearForm((u, v), integral(domain, p_sym*dot(u, v))) + a_fem = BilinearForm((u, v), integral(domain, p*dot(u, v))) + + ncells = (11, 1, 17) + degree = (3, 1, 4) + domain_h = discretize(domain, ncells=ncells, periodic=(False, True, False)) + derham_h = discretize(derham, domain_h, degree=degree) + V1_h = derham_h.V1 + P0 = derham_h.projectors()[0] + + p_fem = P0(p_call) + + a_sym_h = discretize(a_sym, domain_h, (V1_h, V1_h), backend=backend) + a_fem_h = discretize(a_fem, domain_h, (V1_h, V1_h), backend=backend) + + A_sym = a_sym_h.assemble() + A_fem = a_fem_h.assemble(p=p_fem) + + A_sym_sp = A_sym.tosparse() + A_fem_sp = A_fem.tosparse() + diff = A_sym_sp - A_fem_sp + norm_sym = np.linalg.norm(A_sym_sp.data) + norm_fem = np.linalg.norm(A_fem_sp.data) + error = np.linalg.norm(diff.data) + rel_err = error / norm_sym + + #print(norm_sym, norm_fem, error, rel_err) + + assert rel_err < 1e-4 diff --git a/psydac/api/utilities.py b/psydac/api/utilities.py new file mode 100644 index 000000000..108398e0f --- /dev/null +++ b/psydac/api/utilities.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +import os +import string +import random + +from sympy.core.containers import Tuple +from sympy import Matrix, ImmutableDenseMatrix, MutableDenseNDimArray + +__all__ = ( + 'flatten', + 'mkdir_p', + 'touch_init_file', + 'random_string', + 'write_code' +) +#============================================================================== +def flatten(args): + + types_to_flatten = ( + list, + tuple, + Tuple, + Matrix, + ImmutableDenseMatrix, + MutableDenseNDimArray, + ) + + ls = [] + def rec_flatten(args, ls): + if isinstance(args, types_to_flatten): + for i in tuple(args): + rec_flatten(i, ls) + else: + ls.append(args) + rec_flatten(args, ls) + + if isinstance(args, tuple): + return tuple(ls) + elif isinstance(args, Tuple): + return Tuple(*ls) + else: + return ls + +#============================================================================== +def mkdir_p(folder): + if os.path.isdir(folder): + return + os.makedirs(folder, exist_ok=True) + +#============================================================================== +def touch_init_file(path): + mkdir_p(path) + path = os.path.join(path, '__init__.py') + with open(path, 'a'): + os.utime(path, None) + +#============================================================================== +def random_string(size: int = 8, + chars: int = string.ascii_lowercase + string.digits) -> str: + """ + Create a random string of given length to be used in generated file names. + + Parameters + ---------- + size : int, optional + Length of the string (default: 8). + + chars : str, optional + A string with the avalailable characters for random drawing (default: + ASCII lower case characters + decimal digits) + + Returns + ------- + str + A random string of given length, made of the given characters. + """ + return ''.join(random.choice(chars) for _ in range(size)) + +#============================================================================== +def write_code(filename, code, folder=None): + if not folder: + folder = os.getcwd() + + folder = os.path.abspath(folder) + if not os.path.isdir(folder): + raise ValueError('{} folder does not exist'.format(folder)) + + filename = os.path.basename( filename ) + filename = os.path.join(folder, filename) + + # TODO check if init exists + # add __init__.py for imports + touch_init_file(folder) + + f = open(filename, 'w') + for line in code: + f.write(line) + f.close() + + return filename diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py new file mode 100644 index 000000000..58d3dc3ba --- /dev/null +++ b/psydac/cad/geometry.py @@ -0,0 +1,857 @@ +# coding: utf-8 +# +# a Geometry class contains the list of patches and additional information about +# the topology i.e. connectivity, boundaries +# For the moment, it is used as a container, that can be loaded from a file +# (hdf5) +from itertools import product +from collections import abc +import numpy as np +import string +import random +import h5py +import yaml +import os +import string +import random + + +from mpi4py import MPI + +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.fem.partitioning import create_cart, construct_connectivity, construct_interface_spaces +from psydac.mapping.discrete import SplineMapping, NurbsMapping +from psydac.linalg.block import BlockVectorSpace, BlockVector +from psydac.ddm.cart import DomainDecomposition, MultiPatchDomainDecomposition + + +from sympde.topology import Domain, Interface, Line, Square, Cube, NCubeInterior, Mapping, NCube +from sympde.topology.basic import Union + +#============================================================================== +class Geometry: + """ + Distributed discrete geometry that works for single and multiple patches. + The Geometry object can be created in two ways: + - case 1 : through a geometry file whos name can be given to the constructor + - case 2 : provide the ncells, the periodicity and the mapping objects of each patch. + + Parameters + ---------- + domain : Sympde.topology.Domain + The symbolic domain to be discretized. + + ncells : list | tuple | dict + The number of cells of the discretized topological domain in each direction. + + periodic : list | tuple | dict + The periodicity of the topological domain in each direction. + + mappings : dict + The Mapping of each patch. + + filename: str + The path to the geometry file. + + comm: MPI.Comm + MPI intra-communicator. + + mpi_dims_mask: list of bool + True if the dimension is to be used in the domain decomposition (=default for each dimension). + If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. + + """ + _ldim = None + _pdim = None + _patches = [] + _topology = None + + #-------------------------------------------------------------------------- + # Option [1]: from a (domain, mappings) or a file + #-------------------------------------------------------------------------- + def __init__(self, domain=None, ncells=None, periodic=None, mappings=None, + filename=None, comm=None, mpi_dims_mask=None): + + # ... read the geometry if the filename is given + if filename is not None: + self.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + + elif domain is not None: + assert isinstance(domain, Domain) + assert isinstance(ncells, dict) + assert isinstance(mappings, dict) + if periodic is not None: + assert isinstance(periodic, dict) + + # ... check sanity + interior_names = domain.interior_names + mappings_keys = sorted(list(mappings.keys())) + + assert sorted(interior_names) == mappings_keys + # ... + + if periodic is None: + periodic = {patch: [False]*len(ncells_i) for patch, ncells_i in ncells.items()} + + self._domain = domain + self._ldim = domain.dim + self._pdim = domain.dim # TODO must be given => only dim is defined for a Domain + self._ncells = ncells + self._periodic = periodic + self._mappings = mappings + self._cart = None + self._is_parallel = comm is not None + + if len(domain) == 1: + #name = domain.name + name = interior_names[0] + self._ddm = DomainDecomposition(ncells[name], periodic[name], comm=comm, mpi_dims_mask=mpi_dims_mask) + else: + ncells = [ncells[itr] for itr in interior_names] + periodic = [periodic[itr] for itr in interior_names] + self._ddm = MultiPatchDomainDecomposition(ncells, periodic, comm=comm) + + else: + raise ValueError('Wrong input') + # ... + + self._comm = comm + + #-------------------------------------------------------------------------- + # Option [2]: from a discrete mapping + #-------------------------------------------------------------------------- + @classmethod + def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=None): + """Create a geometry from one discrete mapping. + + Parameters + ---------- + mapping : SplineMapping + The Mapping from the unit square to the physical domain. + + comm : MPI.Comm + MPI intra-communicator. + + mpi_dims_mask: list of bool + True if the dimension is to be used in the domain decomposition (=default for each dimension). + If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. + + name : string + Optional name for the Mapping that will be created. + Needed to avoid conflicts in case several mappings are created + """ + + mapping_name = name if name else 'mapping' + dim = mapping.ldim + M = Mapping(mapping_name, dim = dim) + domain = M(NCube(name = 'Omega', + dim = dim, + min_coords = [0.] * dim, + max_coords = [1.] * dim)) + M.set_callable_mapping(mapping) + mappings = {domain.name: mapping} + ncells = {domain.name: mapping.space.domain_decomposition.ncells} + periodic = {domain.name: mapping.space.domain_decomposition.periods} + + return Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings, comm=comm, mpi_dims_mask=mpi_dims_mask) + + + #-------------------------------------------------------------------------- + # Option [3]: discrete topological line/square/cube + #-------------------------------------------------------------------------- + @classmethod + def from_topological_domain(cls, domain, ncells, *, periodic=None, comm=None, mpi_dims_mask=None): + interior = domain.interior + if not isinstance(interior, Union): + interior = [interior] + + for itr in interior: + if not isinstance(itr, NCubeInterior): + msg = "Topological domain must be an NCube;"\ + " got {} instead.".format(type(itr)) + raise TypeError(msg) + + mappings = {itr.name:None for itr in interior} + + if isinstance(ncells, (list, tuple)): + ncells = {itr.name:ncells for itr in interior} + + if periodic is None: + periodic = [False]*domain.dim + + if isinstance(periodic, (list, tuple)): + periodic = {itr.name:periodic for itr in interior} + + geo = Geometry(domain=domain, mappings=mappings, ncells=ncells, periodic=periodic, comm=comm, mpi_dims_mask=mpi_dims_mask) + + return geo + + #-------------------------------------------------------------------------- + @property + def ldim(self): + return self._ldim + + @property + def pdim(self): + return self._pdim + + @property + def ncells(self): + return self._ncells + + @property + def periodic(self): + return self._periodic + + @property + def comm(self): + return self._comm + + @property + def domain(self): + return self._domain + + @property + def ddm(self): + return self._ddm + + @property + def is_parallel(self): + return self._is_parallel + + @property + def mappings(self): + return self._mappings + + def __len__(self): + return len(self.domain) + + def read(self, filename, comm=None, mpi_dims_mask=None): + # ... check extension of the file + basename, ext = os.path.splitext(filename) + if not(ext == '.h5'): + raise ValueError('> Only h5 files are supported') + # ... + + # read the topological domain + domain = Domain.from_file(filename) + connectivity = construct_connectivity(domain) + + if len(domain)==1: + interiors = [domain.interior] + else: + interiors = list(domain.interior.args) + + if not(comm is None): + kwargs = dict( driver='mpio', comm=comm ) if comm.size > 1 else {} + + else: + kwargs = {} + + h5 = h5py.File( filename, mode='r', **kwargs ) + yml = yaml.load( h5['geometry.yml'][()], Loader=yaml.SafeLoader ) + + ldim = yml['ldim'] + pdim = yml['pdim'] + + n_patches = len( yml['patches'] ) + + # ... + if n_patches == 0: + + h5.close() + raise ValueError( "Input file contains no patches." ) + # ... + + # ... read patchs + mappings = {} + ncells = {} + periodic = {} + spaces = [None]*n_patches + for i_patch in range( n_patches ): + + item = yml['patches'][i_patch] + patch_name = item['name'] + mapping_id = item['mapping_id'] + dtype = item['type'] + patch = h5[mapping_id] + if dtype in ['SplineMapping', 'NurbsMapping']: + + degree = [int (p) for p in patch.attrs['degree' ]] + periodic_i = [bool(b) for b in patch.attrs['periodic']] + knots = [patch['knots_{}'.format(d)][:] for d in range( ldim )] + space_i = [SplineSpace( degree=p, knots=k, periodic=P ) + for p,k,P in zip( degree, knots, periodic_i )] + + spaces[i_patch] = space_i + + ncells [interiors[i_patch].name] = [sp.ncells for sp in space_i] + periodic[interiors[i_patch].name] = periodic_i + + self._cart = None + if n_patches == 1: + self._ddm = DomainDecomposition(ncells[domain.name], periodic[domain.name], comm=comm, mpi_dims_mask=mpi_dims_mask) + ddms = [self._ddm] + else: + ncells_ = [ncells[itr.name] for itr in interiors] + periodic = [periodic[itr.name] for itr in interiors] + self._ddm = MultiPatchDomainDecomposition(ncells_, periodic, comm=comm) + ddms = self._ddm.domains + + carts = create_cart(ddms, spaces) + g_spaces = {inter:TensorFemSpace( ddms[i], *spaces[i], cart=carts[i]) for i,inter in enumerate(interiors)} + + for i,j in connectivity: + ((axis_i, ext_i), (axis_j , ext_j)) = connectivity[i, j] + minus = interiors[i] + plus = interiors[j] + max_ncells = [max(ni,nj) for ni,nj in zip(ncells[minus.name],ncells[plus.name])] + g_spaces[minus].add_refined_space(ncells=max_ncells) + g_spaces[plus].add_refined_space(ncells=max_ncells) + + # ... construct interface spaces + construct_interface_spaces(self._ddm, g_spaces, carts, interiors, connectivity) + + for i_patch in range( n_patches ): + + item = yml['patches'][i_patch] + patch_name = item['name'] + mapping_id = item['mapping_id'] + dtype = item['type'] + patch = h5[mapping_id] + space_i = spaces[i_patch] + if dtype in ['SplineMapping', 'NurbsMapping']: + tensor_space = g_spaces[interiors[i_patch]] + + if dtype == 'SplineMapping': + mapping = SplineMapping.from_control_points( tensor_space, + patch['points'][..., :pdim] ) + + elif dtype == 'NurbsMapping': + mapping = NurbsMapping.from_control_points_weights( tensor_space, + patch['points'][..., :pdim], + patch['weights'] ) + + mapping.set_name( item['name'] ) + mappings[patch_name] = mapping + + if n_patches>1: + coeffs = [[e._coeffs for e in mapping._fields] for mapping in mappings.values()] + spaces = [[coeffs_ij.space for coeffs_ij in coeffs_i] for coeffs_i in coeffs] + spaces = [BlockVectorSpace(*space) for space in spaces] + w_spaces = [sp.spaces[0] for sp in spaces] + space = BlockVectorSpace(*spaces, connectivity=connectivity) + w_space = BlockVectorSpace(*w_spaces, connectivity=connectivity) + v = BlockVector(space) + w = BlockVector(w_space) + mapping_list = list(mappings.values()) + for i in range(n_patches): + for j in range(len(coeffs[i])): + v[i][j] = coeffs[i][j] + + mapping = mapping_list[i] + if isinstance(mapping, NurbsMapping): + w[i] = mapping.weights_field.coeffs + else: + w[i] = v[i][0].space.zeros() + + v.update_ghost_regions() + w.update_ghost_regions() + + else: + mapping = list(mappings.values())[0] + for f in mapping._fields: + f.coeffs.update_ghost_regions() + + if isinstance(mapping, NurbsMapping): + mapping.weights_field.coeffs.update_ghost_regions() + + + # ... close the h5 file + h5.close() + # ... + + # Add spline callable mappings to domain undefined mappings + # NOTE: We assume that interiors and mappings.values() use the same ordering + for patch, F in zip(interiors, mappings.values()): + patch.mapping.set_callable_mapping(F) + + # ... + self._ldim = ldim + self._pdim = pdim + self._mappings = mappings + self._domain = domain + self._comm = comm + self._ncells = ncells + self._periodic = periodic + self._is_parallel = comm is not None + # ... + + def export( self, filename ): + """ + Parameters + ---------- + filename : str + Name of HDF5 output file. + + """ + + # ... + comm = self.comm + # ... + + # Create dictionary with geometry metadata + yml = {} + yml['ldim'] = self.ldim + yml['pdim'] = self.pdim + + # ... information about the patches + if not( self.mappings ): + raise ValueError('No mappings were found') + + patches_info = [] + i_mapping = 0 + for patch_name, mapping in self.mappings.items(): + name = '{}'.format( patch_name ) + mapping_id = 'mapping_{}'.format( i_mapping ) + dtype = '{}'.format( type( mapping ).__name__ ) + + patches_info += [{'name': name, + 'mapping_id': mapping_id, + 'type': dtype}] + + i_mapping += 1 + + yml['patches'] = patches_info + # ... + + + # ... topology + topo_yml = self.domain.todict() + # ... + + # Create HDF5 file (in parallel mode if MPI communicator size > 1) + if not(comm is None) and comm.size > 1: + kwargs = dict( driver='mpio', comm=comm ) + + else: + kwargs = {} + + h5 = h5py.File( filename, mode='w', **kwargs ) + + # ... + # Dump geometry metadata to string in YAML file format + geo = yaml.dump( data = yml, sort_keys=False) + + # Write geometry metadata as fixed-length array of ASCII characters + h5['geometry.yml'] = np.array( geo, dtype='S' ) + # ... + + # ... + # Dump geometry metadata to string in YAML file format + geo = yaml.dump( data = topo_yml, sort_keys=False) + # Write topology metadata as fixed-length array of ASCII characters + h5['topology.yml'] = np.array( geo, dtype='S' ) + # ... + + i_mapping = 0 + for patch_name, mapping in self.mappings.items(): + space = mapping.space + + # Create group for patch 0 + group = h5.create_group( yml['patches'][i_mapping]['mapping_id'] ) + group.attrs['shape' ] = space.coeff_space.npts + group.attrs['degree' ] = space.degree + group.attrs['rational' ] = False # TODO remove + group.attrs['periodic' ] = space.periodic + for d in range( self.ldim ): + group['knots_{}'.format( d )] = space.spaces[d].knots + + # Collective: create dataset for control points + shape = [n for n in space.coeff_space.npts] + [self.pdim] + dtype = space.coeff_space.dtype + dset = group.create_dataset( 'points', shape=shape, dtype=dtype ) + + # Independent: write control points to dataset + starts = space.coeff_space.starts + ends = space.coeff_space.ends + index = [slice(s, e+1) for s, e in zip(starts, ends)] + [slice(None)] + index = tuple( index ) + dset[index] = mapping.control_points[index] + + # case of NURBS + if isinstance(mapping, NurbsMapping): + # Collective: create dataset for weights + shape = [n for n in space.coeff_space.npts] + dtype = space.coeff_space.dtype + dset = group.create_dataset( 'weights', shape=shape, dtype=dtype ) + + # Independent: write weights to dataset + starts = space.coeff_space.starts + ends = space.coeff_space.ends + index = [slice(s, e+1) for s, e in zip(starts, ends)] + index = tuple( index ) + dset[index] = mapping.weights[index] + + i_mapping += 1 + + # Close HDF5 file + h5.close() + +#============================================================================== +def export_nurbs_to_hdf5(filename, nurbs, periodic=None, comm=None ): + + """ + Export a single-patch igakit NURBS object to a Psydac geometry file in HDF5 format + + Parameters + ---------- + + filename : + Name of output geometry file, e.g. 'geo.h5' + + nurbs : + igakit geometry nurbs object + + comm : + mpi communicator + """ + + import os.path + import igakit + assert isinstance(nurbs, igakit.nurbs.NURBS) + + extension = os.path.splitext(filename)[-1] + if not extension == '.h5': + raise ValueError('> Only h5 extension is allowed for filename') + + yml = {} + yml['ldim'] = nurbs.dim + yml['pdim'] = nurbs.dim + + patches_info = [] + i_mapping = 0 + i = 0 + + rational = not abs(nurbs.weights-1).sum()<1e-15 + + patch_name = 'patch_{}'.format(i) + name = '{}'.format( patch_name ) + mapping_id = 'mapping_{}'.format( i_mapping ) + dtype = 'NurbsMapping' if rational else 'SplineMapping' + + patches_info += [{'name': name , 'mapping_id':mapping_id, 'type':dtype}] + + yml['patches'] = patches_info + # ... + + # Create HDF5 file (in parallel mode if MPI communicator size > 1) + if not(comm is None) and comm.size > 1: + kwargs = dict( driver='mpio', comm=comm ) + else: + kwargs = {} + + h5 = h5py.File( filename, mode='w', **kwargs ) + + # ... + # Dump geometry metadata to string in YAML file format + geom = yaml.dump( data = yml, sort_keys=False) + # Write geometry metadata as fixed-length array of ASCII characters + h5['geometry.yml'] = np.array( geom, dtype='S' ) + # ... + + # ... topology + if nurbs.dim == 1: + bounds1 = (float(nurbs.breaks(0)[0]), float(nurbs.breaks(0)[-1])) + domain = Line(patch_name, bounds1=bounds1) + + elif nurbs.dim == 2: + bounds1 = (float(nurbs.breaks(0)[0]), float(nurbs.breaks(0)[-1])) + bounds2 = (float(nurbs.breaks(1)[0]), float(nurbs.breaks(1)[-1])) + domain = Square(patch_name, bounds1=bounds1, bounds2=bounds2) + + elif nurbs.dim == 3: + bounds1 = (float(nurbs.breaks(0)[0]), float(nurbs.breaks(0)[-1])) + bounds2 = (float(nurbs.breaks(1)[0]), float(nurbs.breaks(1)[-1])) + bounds3 = (float(nurbs.breaks(2)[0]), float(nurbs.breaks(2)[-1])) + domain = Cube(patch_name, bounds1=bounds1, bounds2=bounds2, bounds3=bounds3) + + mapping = Mapping(mapping_id, dim=nurbs.dim) + domain = mapping(domain) + topo_yml = domain.todict() + + # Dump geometry metadata to string in YAML file format + geom = yaml.dump( data = topo_yml, sort_keys=False) + # Write topology metadata as fixed-length array of ASCII characters + h5['topology.yml'] = np.array( geom, dtype='S' ) + + group = h5.create_group( yml['patches'][i]['mapping_id'] ) + group.attrs['degree' ] = nurbs.degree + group.attrs['rational' ] = rational + group.attrs['periodic' ] = tuple( False for d in range( nurbs.dim ) ) if periodic is None else periodic + for d in range( nurbs.dim ): + group['knots_{}'.format( d )] = nurbs.knots[d] + + group['points'] = nurbs.points[...,:nurbs.dim] + if rational: + group['weights'] = nurbs.weights + + h5.close() + +#============================================================================== +def refine_nurbs(nrb, ncells=None, degree=None, multiplicity=None, tol=1e-9): + """ + This function refines the nurbs object. + It contructs a new grid based on the new number of cells, and it adds the new break points to the nrb grid, + such that the total number of cells is equal to the new number of cells. + We use knot insertion to construct the new knot sequence , so the geometry is identical to the previous one. + It also elevates the degree of the nrb object based on the new degree. + + Parameters + ---------- + + nrb : + geometry nurbs object + + ncells : + total number of cells in each direction + + degree : + degree in each direction + + multiplicity : + multiplicity of each knot in the knot sequence in each direction + + tol : + Minimum distance between two break points. + + Returns + ------- + nrb : + the refined geometry nurbs object + + """ + + if multiplicity is None: + multiplicity = [1]*nrb.dim + + nrb = nrb.clone() + if ncells is not None: + + for axis in range(0,nrb.dim): + ub = nrb.breaks(axis)[0] + ue = nrb.breaks(axis)[-1] + knots = np.linspace(ub,ue,ncells[axis]+1) + index = nrb.knots[axis].searchsorted(knots) + nrb_knots = nrb.knots[axis][index] + for m,(nrb_k, k) in enumerate(zip(nrb_knots, knots)): + if abs(k-nrb_k)0: + nrb.refine(axis, knots) + + if degree is not None: + for axis in range(0,nrb.dim): + d = degree[axis] - nrb.degree[axis] + if d<0: + raise ValueError('The degree {} must be >= {}'.format(degree, nrb.degree)) + nrb.elevate(axis, times=d) + + for axis in range(nrb.dim): + decimals = abs(np.floor(np.log10(np.abs(tol))).astype(int)) + knots, counts = np.unique(nrb.knots[axis].round(decimals=decimals), return_counts=True) + counts = multiplicity[axis] - counts + counts[counts<0] = 0 + knots = np.repeat(knots, counts) + nrb = nrb.refine(axis, knots) + return nrb + +def refine_knots(knots, ncells, degree, multiplicity=None, tol=1e-9): + """ + This function refines the knot sequence. + It contructs a new grid based on the new number of cells, and it adds the new break points to the nrb grid, + such that the total number of cells is equal to the new number of cells. + We use knot insertion to construct the new knot sequence , so the geometry is identical to the previous one. + It also elevates the degree of the nrb object based on the new degree. + + Parameters + ---------- + + knots : + list of knot sequences in each direction + + ncells : + total number of cells in each direction + + degree : + degree in each direction + + multiplicity : + multiplicity of each knot in the knot sequence in each direction + + tol : + Minimum distance between two break points. + + Returns + ------- + knots : + the refined knot sequences in each direction + """ + from igakit.nurbs import NURBS + dim = len(ncells) + + if multiplicity is None: + multiplicity = [1]*dim + + assert len(knots) == dim + + nrb = NURBS(knots) + for axis in range(dim): + ub = nrb.breaks(axis)[0] + ue = nrb.breaks(axis)[-1] + knots = np.linspace(ub,ue,ncells[axis]+1) + index = nrb.knots[axis].searchsorted(knots) + nrb_knots = nrb.knots[axis][index] + for m,(nrb_k, k) in enumerate(zip(nrb_knots, knots)): + if abs(k-nrb_k)0: + nrb.refine(axis, knots) + + for axis in range(dim): + d = degree[axis] - nrb.degree[axis] + if d<0: + raise ValueError('The degree {} must be >= {}'.format(degree, nrb.degree)) + nrb.elevate(axis, times=d) + + for axis in range(dim): + decimals = abs(np.floor(np.log10(np.abs(tol))).astype(int)) + knots, counts = np.unique(nrb.knots[axis].round(decimals=decimals), return_counts=True) + counts = multiplicity[axis] - counts + counts[counts<0] = 0 + knots = np.repeat(knots, counts) + nrb = nrb.refine(axis, knots) + return nrb.knots +#============================================================================== +def import_geopdes_to_nurbs(filename): + """ + This function reads a geopdes geometry file and convert it to igakit nurbs object + + Parameters + ---------- + + filename : + the filename of the geometry file + + Returns + ------- + nrb : + the geometry nurbs object + + """ + extension = os.path.splitext(filename)[-1] + if not extension == '.txt': + raise ValueError('> Expected .txt extension') + + f = open(filename) + lines = f.readlines() + f.close() + + lines = [line for line in lines if line[0].strip() != "#"] + + data = _read_header(lines[0]) + n_dim = data[0] + r_dim = data[1] + n_patchs = data[2] + + n_lines_per_patch = 3*n_dim + 1 + + list_begin_line = _get_begin_line(lines, n_patchs) + + nrb = _read_patch(lines, 1, n_lines_per_patch, list_begin_line) + + return nrb + +def _read_header(line): + chars = line.split(" ") + data = [] + for c in chars: + try: + data.append(int(c)) + except: + pass + return data + +def _extract_patch_line(lines, i_patch): + text = "PATCH " + str(i_patch) + for i_line,line in enumerate(lines): + r = line.find(text) + if r != -1: + return i_line + return None + +def _get_begin_line(lines, n_patchs): + list_begin_line = [] + for i_patch in range(0, n_patchs): + r = _extract_patch_line(lines, i_patch+1) + if r is not None: + list_begin_line.append(r) + else: + raise ValueError(" could not parse the input file") + return list_begin_line + +def _read_line(line): + chars = line.split(" ") + data = [] + for c in chars: + try: + data.append(int(c)) + except: + try: + data.append(float(c)) + except: + pass + return data + +def _read_patch(lines, i_patch, n_lines_per_patch, list_begin_line): + + from igakit.nurbs import NURBS + + i_begin_line = list_begin_line[i_patch-1] + data_patch = [] + + for i in range(i_begin_line+1, i_begin_line + n_lines_per_patch+1): + data_patch.append(_read_line(lines[i])) + + degree = data_patch[0] + shape = data_patch[1] + + xl = [np.array(i) for i in data_patch[2:2+len(degree)] ] + xp = [np.array(i) for i in data_patch[2+len(degree):2+2*len(degree)] ] + w = np.array(data_patch[2+2*len(degree)]) + + X = [i.reshape(shape, order='F') for i in xp] + W = w.reshape(shape, order='F') + + points = np.zeros((*shape, 3)) + for i in range(len(shape)): + points[..., i] = X[i] + + knots = xl + + nrb = NURBS(knots, control=points, weights=W) + return nrb + diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py new file mode 100644 index 000000000..458fc8f50 --- /dev/null +++ b/psydac/cad/tests/test_geometry.py @@ -0,0 +1,395 @@ +# coding: utf-8 +# +import pytest +import numpy as np +import os + +from sympde.topology import Domain, Line, Square, Cube, Mapping + +from psydac.cad.geometry import Geometry, export_nurbs_to_hdf5, refine_nurbs +from psydac.cad.geometry import import_geopdes_to_nurbs +from psydac.cad.cad import elevate, refine +from psydac.cad.gallery import quart_circle, circle +from psydac.mapping.discrete import SplineMapping, NurbsMapping +from psydac.mapping.discrete_gallery import discrete_mapping +from psydac.fem.splines import SplineSpace +from psydac.fem.tensor import TensorFemSpace +from psydac.utilities.utils import refine_array_1d +from psydac.ddm.cart import DomainDecomposition + +from mpi4py import MPI + +base_dir = os.path.dirname(os.path.realpath(__file__)) +#============================================================================== +def test_geometry_2d_1(): + + ncells = [1,1] + degree = [2,2] + # create an identity mapping + mapping = discrete_mapping('identity', ncells=ncells, degree=degree) + + # create a topological domain + F = Mapping('F', dim=2) + domain = F(Square(name='Omega')) + + # associate the mapping to the topological domain + mappings = {domain.name: mapping} + + # Define ncells as a dict + ncells = {domain.name:ncells} + + # create a geometry from a topological domain and the dict of mappings + geo = Geometry(domain=domain, ncells=ncells, mappings=mappings) + + # export the geometry + geo.export('geo.h5') + + # read it again + geo_0 = Geometry(filename='geo.h5') + + # export it again + geo_0.export('geo_0.h5') + + # create a geometry from a discrete mapping + geo_1 = Geometry.from_discrete_mapping(mapping) + + # export it + geo_1.export('geo_1.h5') + +#============================================================================== +def test_geometry_2d_2(): + + # create a nurbs mapping + degrees, knots, points, weights = quart_circle( rmin=0.5, rmax=1.0, center=None ) + + # Create tensor spline space, distributed + spaces = [SplineSpace( knots=k, degree=p ) for k,p in zip(knots, degrees)] + + ncells = [len(space.breaks)-1 for space in spaces] + domain_decomposition = DomainDecomposition(ncells=ncells, periods=[False]*2, comm=None) + + space = TensorFemSpace( domain_decomposition, *spaces ) + + mapping = NurbsMapping.from_control_points_weights( space, points, weights ) + + mapping = elevate( mapping, axis=0, times=1 ) + mapping = refine( mapping, axis=0, values=[0.3, 0.6, 0.8] ) + + # create a topological domain + F = Mapping('F', dim=2) + domain = F(Square(name='Omega')) + + # associate the mapping to the topological domain + mappings = {domain.name: mapping} + + # Define ncells as a dict + ncells = {domain.name:[len(space.breaks)-1 for space in mapping.space.spaces]} + + periodic = {domain.name:[space.periodic for space in mapping.space.spaces]} + + # create a geometry from a topological domain and the dict of mappings + geo = Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings) + + # export the geometry + geo.export('quart_circle.h5') + + # read it again + geo_0 = Geometry(filename='quart_circle.h5') + + # export it again + geo_0.export('quart_circle_0.h5') + + # create a geometry from a discrete mapping + geo_1 = Geometry.from_discrete_mapping(mapping) + + # export it + geo_1.export('quart_circle_1.h5') + +#============================================================================== +# TODO to be removed +def test_geometry_2d_3(): + + # create a nurbs mapping + degrees, knots, points, weights = quart_circle( rmin=0.5, rmax=1.0, center=None ) + + # Create tensor spline space, distributed + spaces = [SplineSpace( knots=k, degree=p ) for k,p in zip(knots, degrees)] + ncells = [len(space.breaks)-1 for space in spaces] + domain_decomposition = DomainDecomposition(ncells=ncells, periods=[False]*2, comm=None) + + space = TensorFemSpace( domain_decomposition, *spaces ) + + mapping = NurbsMapping.from_control_points_weights( space, points, weights ) + + mapping = elevate( mapping, axis=1, times=1 ) + + n = 8 + t = np.linspace(0, 1, n+1)[1:-1] + + # TODO allow for 1d numpy array + t = list(t) + + for axis in [0, 1]: + mapping = refine( mapping, axis=axis, values=t ) + + # create a geometry from a discrete mapping + geo = Geometry.from_discrete_mapping(mapping) + + # export it + geo.export('quart_circle.h5') + +#============================================================================== +# TODO to be removed +def test_geometry_2d_4(): + + # create a nurbs mapping + radius = np.sqrt(2)/2. + degrees, knots, points, weights = circle( radius=radius, center=None ) + + # Create tensor spline space, distributed + spaces = [SplineSpace( knots=k, degree=p ) for k,p in zip(knots, degrees)] + ncells = [len(space.breaks)-1 for space in spaces] + domain_decomposition = DomainDecomposition(ncells=ncells, periods=[False]*2, comm=None) + + space = TensorFemSpace( domain_decomposition, *spaces ) + + mapping = NurbsMapping.from_control_points_weights( space, points, weights ) + + n = 8 +# n = 32 + t = np.linspace(0, 1, n+1)[1:-1] + + # TODO allow for 1d numpy array + t = list(t) + + for axis in [0, 1]: + mapping = refine( mapping, axis=axis, values=t ) + + # create a geometry from a discrete mapping + geo = Geometry.from_discrete_mapping(mapping) + + # export it + geo.export('circle.h5') + +#============================================================================== +@pytest.mark.parallel +def test_geometry_with_mpi_dims_mask(): + + comm = MPI.COMM_WORLD + rank = comm.rank + size = comm.size + mpi_dims_mask = [False, True, False] # We will verify that this has an effect + ncells = [4, 2*size, 8] # Each process should have two cells along x2 + degree = [2, 2, 2] + + expected_starts = (0, 2 * rank, 0) + expected_ends = (3, 2 * rank + 1, 7) + + # create an identity mapping + mapping = discrete_mapping('identity', ncells=ncells, degree=degree) + + # create a topological domain + F = Mapping('F', dim=3) + domain = F(Cube(name='Omega')) + + # associate the mapping to the topological domain + mappings = {domain.name: mapping} + + # Define d_ncells as a dict + d_ncells = {domain.name: ncells} + + # Create a geometry from a topological domain and the dict of mappings + # Here we allow for any distribution of the domain: mpi_dims_mask is not passed + geo = Geometry(domain=domain, ncells=d_ncells, mappings=mappings, comm=comm) + geo.export('geo_mpi_dims.h5') + + # Read geometry file in parallel, but using mpi_dims_mask + geo_from_file = Geometry(filename='geo_mpi_dims.h5', comm=comm, mpi_dims_mask=mpi_dims_mask) + + # Verify that the domain is distributed as expected + assert geo_from_file.ddm.starts == expected_starts + assert geo_from_file.ddm.ends == expected_ends + + # Safely remove the file + comm.Barrier() + if rank == 0: + os.remove('geo_mpi_dims.h5') + + +# ============================================================================== +@pytest.mark.parallel +def test_from_discrete_mapping(): + + comm = MPI.COMM_WORLD + rank = comm.rank + size = comm.size + mpi_dims_mask = [False, False, True] # We swill verify that this has an effect + ncells = [4, 8, 2 * size] # Each process should have two cells along x3 + degree = [3, 3, 3] + + expected_starts = (0, 0, 2 * rank) + expected_ends = (3, 7, 2 * rank + 1) + + # Create a mapping + mapping = discrete_mapping('identity', ncells=ncells, degree=degree) + + # Create geometry from the mapping using mpi_dims_mask + geo_from_mapping = Geometry.from_discrete_mapping(mapping, comm=comm, mpi_dims_mask=mpi_dims_mask) + + # Verify that the domain is distributed as expected + assert geo_from_mapping.ddm.starts == expected_starts + assert geo_from_mapping.ddm.ends == expected_ends + +# ============================================================================== +@pytest.mark.parallel +def test_from_topological_domain(): + + comm = MPI.COMM_WORLD + rank = comm.rank + size = comm.size + mpi_dims_mask = [False, True, False] # We will verify that this has an effect + ncells = [4, 2 * size, 8] # Each process should have two cells along x2 + + expected_starts = (0, 2 * rank, 0) + expected_ends = (3, 2 * rank + 1, 7) + + # Create a topological domain + F = Mapping('F', dim=3) + domain = F(Cube(name='Omega')) + + # Create geometry from topological domain using mpi_dims_mask + geo_from_domain = Geometry.from_topological_domain(domain, ncells, comm=comm, mpi_dims_mask=mpi_dims_mask) + + # Verify that the domain is distributed as expected + assert geo_from_domain.ddm.starts == expected_starts + assert geo_from_domain.ddm.ends == expected_ends + +#============================================================================== +@pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) +@pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) +def test_export_nurbs_to_hdf5(ncells, degree): + + # create pipe geometry + from igakit.cad import circle, ruled, bilinear, join + C0 = circle(center=(-1,0),angle=(-np.pi/3,0)) + C1 = circle(radius=2,center=(-1,0),angle=(-np.pi/3,0)) + annulus = ruled(C0,C1).transpose() + square = bilinear(np.array([[[0,0],[0,3]],[[1,0],[1,3]]]) ) + pipe = join(annulus, square, axis=1) + + # refine the nurbs object + new_pipe = refine_nurbs(pipe, ncells=ncells, degree=degree) + + filename = "pipe.h5" + export_nurbs_to_hdf5(filename, new_pipe) + + # read the geometry + geo = Geometry(filename=filename) + domain = geo.domain + + min_coords = domain.logical_domain.min_coords + max_coords = domain.logical_domain.max_coords + + assert abs(min_coords[0] - pipe.breaks(0)[0])<1e-15 + assert abs(min_coords[1] - pipe.breaks(1)[0])<1e-15 + + assert abs(max_coords[0] - pipe.breaks(0)[-1])<1e-15 + assert abs(max_coords[1] - pipe.breaks(1)[-1])<1e-15 + + mapping = geo.mappings[domain.logical_domain.name] + + assert isinstance(mapping, NurbsMapping) + + space = mapping.space + knots = space.knots + degree = space.degree + + assert all(np.allclose(pk,k, 1e-15, 1e-15) for pk,k in zip(new_pipe.knots, knots)) + assert degree == list(new_pipe.degree) + + assert np.allclose(new_pipe.weights.flatten(), mapping._weights_field.coeffs.toarray(), 1e-15, 1e-15) + + eta1 = refine_array_1d(new_pipe.breaks(0), 10) + eta2 = refine_array_1d(new_pipe.breaks(1), 10) + + pcoords1 = np.array([[new_pipe(e1,e2) for e2 in eta2] for e1 in eta1]) + pcoords2 = np.array([[mapping(e1,e2) for e2 in eta2] for e1 in eta1]) + + assert np.allclose(pcoords1[..., :domain.dim], pcoords2, 1e-15, 1e-15) + +#============================================================================== +@pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) +@pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) +def test_import_geopdes_to_nurbs(ncells, degree): + + + filename = os.path.join(base_dir, "geo_Lshaped_C1.txt") + L_shaped = import_geopdes_to_nurbs(filename) + + # refine the nurbs object + L_shaped = refine_nurbs(L_shaped, ncells=ncells, degree=degree) + + filename = "L_shaped.h5" + export_nurbs_to_hdf5(filename, L_shaped) + + # read the geometry + geo = Geometry(filename=filename) + domain = geo.domain + + min_coords = domain.logical_domain.min_coords + max_coords = domain.logical_domain.max_coords + + assert abs(min_coords[0] - L_shaped.breaks(0)[0])<1e-15 + assert abs(min_coords[1] - L_shaped.breaks(1)[0])<1e-15 + + assert abs(max_coords[0] - L_shaped.breaks(0)[-1])<1e-15 + assert abs(max_coords[1] - L_shaped.breaks(1)[-1])<1e-15 + + mapping = geo.mappings[domain.logical_domain.name] + + space = mapping.space + knots = space.knots + degree = space.degree + + assert all(np.allclose(pk,k, 1e-15, 1e-15) for pk,k in zip(L_shaped.knots, knots)) + assert degree == list(L_shaped.degree) + + if isinstance(mapping, NurbsMapping): + assert np.allclose(L_shaped.weights.flatten(), mapping._weights_field.coeffs.toarray(), 1e-15, 1e-15) + +#============================================================================== +@pytest.mark.xfail +def test_geometry_1(): + + line = Geometry.as_line(ncells=[10]) + square = Geometry.as_square(ncells=[10, 10]) + cube = Geometry.as_cube(ncells=[10, 10, 10]) + +#============================================================================== +# CLEAN UP SYMPY NAMESPACE +#============================================================================== + +def teardown_module(): + import os + from sympy.core import cache + cache.clear_cache() + + # Remove HDF5 files generated by Geometry.export() + filenames = [ + 'geo.h5', + 'geo_0.h5', + 'geo_1.h5', + 'quart_circle.h5', + 'quart_circle_0.h5', + 'quart_circle_1.h5', + 'circle.h5', + 'pipe.h5', + 'L_shaped.h5', + ] + for fname in filenames: + if os.path.exists(fname): + os.remove(fname) + +def teardown_function(): + from sympy.core import cache + cache.clear_cache() diff --git a/psydac/core/field_evaluation_kernels.py b/psydac/core/field_evaluation_kernels.py index 242f7ff7c..2a341e393 100644 --- a/psydac/core/field_evaluation_kernels.py +++ b/psydac/core/field_evaluation_kernels.py @@ -4287,6 +4287,10 @@ def eval_jacobians_inv_3d_weights(nc1: int, nc2: int, nc3: int, f_p1: int, f_p2 i_cell_3 * k3 + i_quad_3, :, :] = jmat / det +<<<<<<< HEAD +======= + +>>>>>>> upstream/devel def eval_jacobians_inv_2d_weights(nc1: int, nc2: int, f_p1: int, f_p2: int, k1: int, k2: int, global_basis_1: 'float[:,:,:,:]', global_basis_2: 'float[:,:,:,:]', global_spans_1: 'int[:]', global_spans_2: 'int[:]', global_arr_coeff_x: 'T[:,:]', diff --git a/psydac/feec/conforming_projectors.py b/psydac/feec/conforming_projectors.py new file mode 100644 index 000000000..d045a702e --- /dev/null +++ b/psydac/feec/conforming_projectors.py @@ -0,0 +1,1501 @@ +# coding: utf-8 +# Conga operators on piecewise (broken) de Rham sequences +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.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.sparse import SparseMatrixLinearOperator + +__all__ = ( + 'ConformingProjectionV0', + 'ConformingProjectionV1', +) + +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 [] + + if space_1d.ncells <= p_moments + 1: + 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 + # 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 + + +#============================================================================== +# 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). + + Parameters + ---------- + Vh : MultipatchFemSpace + 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) + p_moments = len(gamma)-1 + + 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 : MultipatchFemSpace + 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)] + 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) + 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 + +#============================================================================== +# Singlepatch conforming projectors +#============================================================================== +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 : MultipatchFemSpace + 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) + p_moments = len(gamma)-1 + + 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 + + +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 : MultipatchFemSpace + 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 ConformingProjectionV0(FemLinearOperator): + """ + Conforming projection from global broken V0 space to conforming global V0 space + Defined by averaging of interface (including vertex) dofs + and adding moment correction terms + + 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 + """ + 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) + + if V0h.is_multipatch: + sparse_matrix = construct_h1_conforming_projection(V0h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + 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.tocsr()) + + +class ConformingProjectionV1(FemLinearOperator): + """ + Conforming projection from global broken V1 space to conforming V1 global space + Defined by averaging of (only) interface dofs + and adding moment correction terms + + 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 + """ + 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: + sparse_matrix = construct_hcurl_conforming_projection(V1h, reg_orders=0, p_moments=p_moments, hom_bc=hom_bc) + 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.tocsr()) diff --git a/psydac/feec/derivatives.py b/psydac/feec/derivatives.py index f7f28ccbd..1fd3de5d2 100644 --- a/psydac/feec/derivatives.py +++ b/psydac/feec/derivatives.py @@ -9,38 +9,28 @@ 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 __all__ = ( 'DirectionalDerivativeOperator', - 'DiffOperator', - 'Derivative_1D', - 'Gradient_2D', - 'Gradient_3D', - 'ScalarCurl_2D', - 'VectorCurl_2D', - 'Curl_3D', - 'Divergence_2D', - 'Divergence_3D', - 'block_tostencil' + 'Derivative1D', + 'Gradient2D', + 'Gradient3D', + 'ScalarCurl2D', + 'VectorCurl2D', + 'Curl3D', + 'Divergence2D', + 'Divergence3D', + 'BrokenGradient2D', + 'BrokenTransposedGradient2D', + 'BrokenScalarCurl2D', + 'BrokenTransposedScalarCurl2D', ) #==================================================================================================== -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 #==================================================================================================== class DirectionalDerivativeOperator(LinearOperator): """ @@ -361,40 +351,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 Derivative1D(FemLinearOperator): """ 1D derivative. @@ -415,10 +372,10 @@ 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(DiffOperator): +class Gradient2D(FemLinearOperator): """ Gradient operator in 2D. @@ -434,7 +391,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 @@ -454,11 +411,10 @@ 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(DiffOperator): +class Gradient3D(FemLinearOperator): """ Gradient operator in 3D. @@ -497,10 +453,10 @@ 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(DiffOperator): +class ScalarCurl2D(FemLinearOperator): """ Scalar curl operator in 2D: computes a scalar field from a vector field. @@ -536,10 +492,10 @@ 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(DiffOperator): +class VectorCurl2D(FemLinearOperator): """ Vector curl operator in 2D: computes a vector field from a scalar field. This is sometimes called the 'rot' operator. @@ -576,10 +532,10 @@ 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(DiffOperator): +class Curl3D(FemLinearOperator): """ Curl operator in 3D. @@ -626,10 +582,10 @@ 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(DiffOperator): +class Divergence2D(FemLinearOperator): """ Divergence operator in 2D. @@ -665,10 +621,10 @@ 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(DiffOperator): +class Divergence3D(FemLinearOperator): """ Divergence operator in 3D. @@ -707,4 +663,118 @@ 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 +#==================================================================================================== +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) + + 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 BrokenTransposedGradient2D(self.fem_domain, self.fem_codomain) + +# ============================================================================== +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) + + 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 BrokenGradient2D(self.fem_codomain, self.fem_domain) + +# ============================================================================== +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) + + 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 BrokenTransposedScalarCurl2D( + V1h=self.fem_domain, V2h=self.fem_codomain) + + +# ============================================================================== +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) + + 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 BrokenScalarCurl2D(V1h=self.fem_codomain, V2h=self.fem_domain) 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 9cc1451a9..21685602c 100644 --- a/psydac/feec/global_projectors.py +++ b/psydac/feec/global_geometric_projectors.py @@ -4,27 +4,28 @@ 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 from psydac.feec import dof_kernels 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 abc import ABCMeta, abstractmethod -__all__ = ('GlobalProjector', 'Projector_H1', 'Projector_Hcurl', 'Projector_Hdiv', 'Projector_L2', +__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 +45,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. @@ -407,7 +408,9 @@ def __call__(self, fun, dofs_only = False): return FemField(self._space, coeffs=coeffs) #============================================================================== -class Projector_H1(GlobalProjector): +# SINGLEPATCH PROJECTORS +#============================================================================== +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, @@ -457,7 +460,7 @@ def __call__(self, fun, dofs_only = False): return super().__call__(fun, dofs_only = dofs_only) #============================================================================== -class Projector_Hcurl(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 @@ -530,7 +533,7 @@ def __call__(self, fun, dofs_only = False): return super().__call__(fun, dofs_only = dofs_only) #============================================================================== -class Projector_Hdiv(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 @@ -607,7 +610,7 @@ def __call__(self, fun, dofs_only = False): return super().__call__(fun, dofs_only = dofs_only) #============================================================================== -class Projector_L2(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, @@ -666,7 +669,8 @@ def __call__(self, fun, dofs_only = False): """ return super().__call__(fun, dofs_only = dofs_only) -class Projector_H1vec(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 @@ -730,6 +734,42 @@ def __call__(self, fun, dofs_only = False): """ return super().__call__(fun, dofs_only = dofs_only) +#============================================================================== +# MULTIPATCH PROJECTORS (2D) +#============================================================================== +class MultipatchGeometricProjector: + """ + Global Geometric Projector base class for multipatch domains. + + Parameters + ---------- + 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, space, Projector, nquads=None): + assert isinstance(space, MultipatchFemSpace) + assert isinstance(Projector, type) + assert issubclass(Projector, GlobalGeometricProjector) + + self._Vh = Vh = space + self._Ps = [Projector(V, nquads=nquads) for V in Vh.spaces] + + def __call__(self, funs): + """ + project a list of functions given in the logical domain + """ + us = [P(fun) for P, fun, in zip(self._Ps, funs)] + + u_c = BlockVector(self._Vh.coeff_space, blocks=[uj.coeffs for uj in us]) + + return FemField(self._Vh, coeffs=u_c) + #============================================================================== # 1D DEGREES OF FREEDOM #============================================================================== diff --git a/psydac/feec/hodge.py b/psydac/feec/hodge.py new file mode 100644 index 000000000..26cb702b4 --- /dev/null +++ b/psydac/feec/hodge.py @@ -0,0 +1,148 @@ +import os +import numpy as np + +from sympde.topology import elements_of +from sympde.topology.space import ScalarFunction +from sympde.calculus import dot +from sympde.expr.expr import BilinearForm +from sympde.expr.expr import integral + +from psydac.api.settings import PSYDAC_BACKENDS + +# =============================================================================== +class HodgeOperator: + """ + Change of basis operator: dual basis -> primal basis + + self._linop: matrix (LinearOperator) of the primal Hodge = this is the mass matrix ! + self.dual_linop: this is the INVERSE mass matrix (LinearOperator) + + Parameters + ---------- + Vh: + The discrete space + + domain_h: + The discrete domain of the projector + + metric : + the metric of the de Rham complex + + backend_language: + The backend used to accelerate the code + + Notes + ----- + 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'): + + 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 + + self._domain_h = domain_h + self._backend_language = backend_language + + if not (metric == 'identity'): + raise NotImplementedError('only the identity metric is available') + + self._metric = metric + + 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._linop is None: + Vh = self._fem_domain + assert Vh == self._fem_codomain + + V = Vh.symbolic_space + domain = V.domain + u, v = elements_of(V, names='u, v') + + if isinstance(u, ScalarFunction): + expr = u * v + else: + expr = dot(u, v) + + a = BilinearForm((u, v), integral(domain, expr)) + ah = discretize(a, self._domain_h, [Vh, Vh], backend=PSYDAC_BACKENDS[self._backend_language]) + + self._linop = ah.assemble() # Mass matrix in stencil format + + self._primal_hodge = FemLinearOperator(self._fem_domain, self._fem_codomain, linop=self._linop) + + 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 + """ + 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 + + if self._fem_domain.is_multipatch: + + 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(Mii, solver=solver, **kwargs) + inv_M_blocks[i][i] = inv_Mii + + self._dual_linop = BlockLinearOperator(M.codomain, M.domain, blocks=inv_M_blocks) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) + + else: + inv_M = inverse(M, solver=solver, **kwargs) + self._dual_hodge = FemLinearOperator(self._fem_codomain, self._fem_domain, linop=self._dual_linop) + + @property + def linop(self): + if self._linop is None: + self.assemble_matrix() + + return self._linop + + @property + def dual_linop(self): + if self._dual_linop is None: + self.assemble_dual_matrix() + + return self._dual_linop + + @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_matrix() + + return self._dual_hodge diff --git a/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py new file mode 100644 index 000000000..aaebd3140 --- /dev/null +++ b/psydac/feec/multipatch/examples/h1_source_pbms_conga_2d.py @@ -0,0 +1,259 @@ +""" + solver for the problem: find u in H^1, such that + + A u = f on \\Omega + u = u_bc on \\partial \\Omega + + where the operator + + A u := eta * u - mu * div grad u + + is discretized as Ah: V0h -> V0h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h +""" +import os +import numpy as np + +from sympde.topology import Derham + +from psydac.api.discretization import discretize +from psydac.linalg.basic import IdentityOperator +from psydac.linalg.solvers import inverse + +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.fem.projectors import get_dual_dofs + +from psydac.fem.basic import FemField +from psydac.api.postprocessing import OutputManager, PostProcessManager + + +def solve_h1_source_pbm( + nc=4, deg=4, domain_name='pretzel_f', backend_language=None, source_type='manu_poisson_elliptic', + eta=-10., mu=1., gamma_h=10., plot_dir=None, +): + """ + solver for the problem: find u in H^1, such that + + A u = f on \\Omega + u = u_bc on \\partial \\Omega + + where the operator + + A u := eta * u - mu * div grad u + + is discretized as Ah: V0h -> V0h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + + Examples: + + - Helmholtz equation with + eta = -omega**2 + mu = 1 + + - Poisson equation with Laplace operator L = A, + eta = 0 + mu = 1 + + :param nc: nb of cells per dimension, in each patch + :param deg: coordinate degree in each patch + :param domain_name: name of the domain + :param backend_language: backend language for the operators + :param source_type: must be implemented in get_source_and_solution_h1 + :param eta: coefficient of the elliptic operator + :param mu: coefficient of the elliptic operator + :param gamma_h: jump penalization parameter + :param plot_dir: directory for the plots (if None, no plots are generated) + """ + + degree = [deg, deg] + + print('---------------------------------------------------------------------------------------------------------') + print('Starting solve_h1_source_pbm function with: ') + print(' ncells = {}'.format(nc)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' backend_language = {}'.format(backend_language)) + print('---------------------------------------------------------------------------------------------------------') + + print('building the multipatch domain...') + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(nc, int): + ncells = [nc, nc] + else: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + + domain_h = discretize(domain, ncells=ncells) + + print('building the symbolic and discrete deRham sequences...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + derham_h = discretize(derham, domain_h, degree=degree) + + 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.derivatives(kind='linop') + + print('Hodge operators...') + # multi-patch (broken) linear operators + 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 + # of the continuous deRham sequence) + cP0, cP1, cP2 = derham_h.conforming_projectors(kind='linop', hom_bc = True) + + print('building the discrete operators:') + + I0 = IdentityOperator(V0h.coeff_space) + + # div grad + DG = - bD0.T @ H1 @ bD0 + + # jump penalization: + JP0 = (I0 - cP0).T @ H0 @ (I0 - cP0) + + # useful for the boundary condition (if present) + 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,) + + df = get_dual_dofs(Vh=V0h, f=f_scal, domain_h=domain_h, backend_language=backend_language) + f = dH0 @ df + df = cP0.T @ df + + 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) + ubc = dH0.dot(du_bc) + ubc -= cP0.dot(ubc) + + else: + ubc = None + + return ubc + + 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...') + df -= pre_A @ ubc + + # direct solve with scipy spsolve + 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...') + u = cP0.dot(u) + + if ubc is not None: + # adding the lifted boundary condition + print('adding the lifted boundary condition...') + u += ubc + + + if u_ex: + 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) + + OM = OutputManager(plot_dir + '/spaces.yml', plot_dir + '/fields.h5') + OM.add_spaces(V0h=V0h) + OM.set_static() + + uh = FemField(V0h, coeffs=u) + OM.export_fields(uh=uh) + + fh = FemField(V0h, coeffs=f) + OM.export_fields(fh=fh) + + if u_ex: + uh_ex = FemField(V0h, coeffs=u_ex) + OM.export_fields(uh_ex=uh_ex) + + OM.export_space_info() + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces.yml', + fields_file=plot_dir + '/fields.h5') + + PM.export_to_vtk( + plot_dir + "/u_h", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='uh') + + PM.export_to_vtk( + plot_dir + "/f_h", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='fh') + + if u_ex: + PM.export_to_vtk( + plot_dir + "/uh_ex", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='uh_ex') + + PM.close() + + if u_ex: + err = u - u_ex + rel_err = np.sqrt(H0.dot_inner(err, err) / H0.dot_inner(u_ex, u_ex)) + + return rel_err + + +if __name__ == '__main__': + + omega = np.sqrt(170) # source + eta = -omega**2 + mu=0 + gamma_h = 10 + + source_type = 'manu_poisson_elliptic' + + domain_name = 'pretzel_f' + + 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=mu, # 1, + domain_name=domain_name, + source_type=source_type, + backend_language='pyccel-gcc', + plot_dir='./plots/h1_source_pbms_conga_2d/' + run_dir, + ) \ No newline at end of file diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py new file mode 100644 index 000000000..418118469 --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_conga_2d.py @@ -0,0 +1,324 @@ +""" + Solve the eigenvalue problem for the curl-curl operator in 2D with a FEEC discretization +""" +import os +import numpy as np + +from sympde.topology import Derham + +from psydac.api.discretization import discretize +from psydac.api.settings import PSYDAC_BACKENDS +from psydac.feec.multipatch.multipatch_domain_utilities import build_multipatch_domain +from psydac.feec.multipatch.utilities import time_count + + +from scipy.sparse.linalg import spilu, lgmres +from scipy.sparse.linalg import LinearOperator, eigsh, minres +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.api.postprocessing import OutputManager, PostProcessManager + + +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): + """ + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization + + Parameters + ---------- + ncells : array + Number of cells in each direction + degree : tuple + Degree of the basis functions + domain : list + Interval in x- and y-direction + domain_name : str + Name of the domain + backend_language : str + Language used for the backend + mu : float + Coefficient in the curl-curl operator + nu : float + Coefficient in the curl-curl operator + gamma_h : float + Coefficient in the curl-curl operator + generalized_pbm : bool + If True, solve the generalized eigenvalue problem + sigma : float + Calculate eigenvalues close to sigma + nb_eigs_solve : int + Number of eigenvalues to solve + nb_eigs_plot : int + Number of eigenvalues to plot + skip_eigs_threshold : float + Threshold for the eigenvalues to skip + plot_dir : str + Directory for the plots + """ + + diags = {} + + if sigma is None: + raise ValueError('please specify a value for sigma') + + print('---------------------------------------------------------------------------------------------------------') + print('Starting hcurl_solve_eigen_pbm function with: ') + print(' ncells = {}'.format(ncells)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' backend_language = {}'.format(backend_language)) + print('---------------------------------------------------------------------------------------------------------') + t_stamp = time_count() + print('building symbolic and discrete domain...') + + int_x, int_y = domain + if isinstance(ncells, int): + domain = build_multipatch_domain(domain_name=domain_name) + + elif domain_name == 'refined_square' or domain_name == 'square_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='identity') + + elif domain_name == 'curved_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='polar') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(ncells, int): + ncells = [ncells, ncells] + elif ncells.ndim == 1: + ncells = {patch.name: [ncells[i], ncells[i]] + for (i, patch) in enumerate(domain.interior)} + elif ncells.ndim == 2: + ncells = {patch.name: [ncells[int(patch.name[2])][int(patch.name[4])], + ncells[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + domain_h = discretize(domain, ncells=ncells) # Vh space + + print('building symbolic and discrete derham sequences...') + t_stamp = time_count() + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + t_stamp = time_count(t_stamp) + print(' .. discrete derham sequence...') + derham_h = discretize(derham, domain_h, degree=degree) + + V0h, V1h, V2h = derham_h.spaces + print('dim(V0h) = {}'.format(V0h.nbasis)) + print('dim(V1h) = {}'.format(V1h.nbasis)) + print('dim(V2h) = {}'.format(V2h.nbasis)) + diags['ndofs_V0'] = V0h.nbasis + diags['ndofs_V1'] = V1h.nbasis + diags['ndofs_V2'] = V2h.nbasis + + t_stamp = time_count(t_stamp) + print('building the discrete operators:') + print('commuting projection operators...') + + I1 = IdentityOperator(V1h.coeff_space) + + 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) + 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...') + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + 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.derivatives(kind='linop') + + + print('computing the full operator matrix...') + + # Conga (projection-based) stiffness matrices + if mu != 0: + # curl curl: + t_stamp = time_count(t_stamp) + print('mu = {}'.format(mu)) + print('curl-curl stiffness matrix...') + + CC = cP1.T @ bD1.T @ H2 @ bD1 @ cP1 # Conga stiffness matrix + A = mu * CC + + if nu != 0: + 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...') + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + A += gamma_h * JS + + if generalized_pbm: + print('adding jump stabilization to RHS of generalized eigenproblem...') + B = cP1.T @ H1 @ cP1 + JS + else: + B = H1 + + t_stamp = time_count(t_stamp) + print('solving matrix eigenproblem...') + 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...') + zero_eigenvalues = [] + if skip_eigs_threshold is not None: + eigenvalues = [] + eigenvectors = [] + for val, vect in zip(all_eigenvalues, all_eigenvectors_transp.T): + if abs(val) < skip_eigs_threshold: + zero_eigenvalues.append(val) + # we skip the eigenvector + else: + eigenvalues.append(val) + eigenvectors.append(vect) + else: + eigenvalues = all_eigenvalues + eigenvectors = all_eigenvectors_transp.T + + for k, val in enumerate(eigenvalues): + diags['eigenvalue_{}'.format(k)] = val # eigenvalues[k] + + for k, val in enumerate(zero_eigenvalues): + diags['skipped eigenvalue_{}'.format(k)] = val + + t_stamp = time_count(t_stamp) + print('plotting the eigenmodes...') + + 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) + + return diags, eigenvalues + + +def get_eigenvalues(nb_eigs, sigma, A_m, M_m): + """ + Compute the eigenvalues of the matrix A close to sigma and right-hand-side M + + Parameters + ---------- + nb_eigs : int + Number of eigenvalues to compute + sigma : float + Value close to which the eigenvalues are computed + A_m : sparse matrix + Matrix A + M_m : sparse matrix + Matrix M + """ + + print('----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ') + print( + 'computing {0} eigenvalues (and eigenvectors) close to sigma={1} with scipy.sparse.eigsh...'.format( + nb_eigs, + sigma)) + mode = 'normal' + which = 'LM' + # from eigsh docstring: + # ncv = number of Lanczos vectors generated ncv must be greater than k and smaller than n; + # it is recommended that ncv > 2*k. Default: min(n, max(2*k + 1, 20)) + ncv = 4 * nb_eigs + print('A_m.shape = ', A_m.shape) + try_lgmres = True + max_shape_splu = 24000 # OK for nc=20, deg=6 on pretzel_f + if A_m.shape[0] < max_shape_splu: + print('(via sparse LU decomposition)') + OPinv = None + tol_eigsh = 0 + else: + + OP_m = A_m - sigma * M_m + tol_eigsh = 1e-7 + if try_lgmres: + print( + '(via SPILU-preconditioned LGMRES iterative solver for A_m - sigma*M1_m)') + OP_spilu = spilu(OP_m, fill_factor=15, drop_tol=5e-5) + preconditioner = LinearOperator( + OP_m.shape, lambda x: OP_spilu.solve(x)) + tol = tol_eigsh + OPinv = LinearOperator( + matvec=lambda v: lgmres(OP_m, v, x0=None, tol=tol, atol=tol, M=preconditioner, + callback=lambda x: print( + 'cg -- residual = ', norm(OP_m.dot(x) - v)) + )[0], + shape=M_m.shape, + dtype=M_m.dtype + ) + + else: + # from https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.eigsh.html: + # the user can supply the matrix or operator OPinv, which gives x = OPinv @ b = [A - sigma * M]^-1 @ b. + # > here, minres: MINimum RESidual iteration to solve Ax=b + # suggested in https://github.com/scipy/scipy/issues/4170 + print('(with minres iterative solver for A_m - sigma*M1_m)') + OPinv = LinearOperator( + matvec=lambda v: minres( + OP_m, + v, + tol=1e-10)[0], + shape=M_m.shape, + dtype=M_m.dtype) + + eigenvalues, eigenvectors = eigsh( + A_m, k=nb_eigs, M=M_m, sigma=sigma, mode=mode, which=which, ncv=ncv, tol=tol_eigsh, OPinv=OPinv) + + print("done: eigenvalues found: " + repr(eigenvalues)) + return eigenvalues, eigenvectors diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py new file mode 100644 index 000000000..0235dd72d --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_eigen_pbms_dg_2d.py @@ -0,0 +1,307 @@ +""" + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization, following + A. Buffa and I. Perugia, “Discontinuous Galerkin Approximation of the Maxwell Eigenproblem” + SIAM Journal on Numerical Analysis 44 (2006) +""" +import os +from mpi4py import MPI +from collections import OrderedDict + +import numpy as np +import matplotlib.pyplot + +from scipy.sparse.linalg import LinearOperator, eigsh, minres + +from sympde.calculus import grad, dot, curl, cross +from sympde.calculus import minus, plus +from sympde.topology import VectorFunctionSpace +from sympde.topology import elements_of +from sympde.topology import NormalVector +from sympde.topology import Square +from sympde.topology import IdentityMapping, PolarMapping +from sympde.expr.expr import LinearForm, BilinearForm +from sympde.expr.expr import integral +from sympde.expr.expr import Norm +from sympde.expr.equation import find, EssentialBC + +from psydac.linalg.utilities import array_to_psydac +from psydac.fem.basic import FemField +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 +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 + + +def hcurl_solve_eigen_pbm_dg(ncells=np.array([[8, 4], [4, 4]]), degree=(3, 3), domain=([0, np.pi], [0, np.pi]), domain_name='refined_square', backend_language='pyccel-gcc', mu=1, nu=0, + sigma=5, nb_eigs_solve=8, nb_eigs_plot=5, skip_eigs_threshold=1e-7, + plot_dir=None,): + """ + Solve the eigenvalue problem for the curl-curl operator in 2D with DG discretization + + Parameters + ---------- + ncells : array + Number of cells in each direction + degree : tuple + Degree of the basis functions + domain : list + Interval in x- and y-direction + domain_name : str + Name of the domain + backend_language : str + Language used for the backend + mu : float + Coefficient in the curl-curl operator + nu : float + Coefficient in the curl-curl operator + sigma : float + Calculate eigenvalues close to sigma + nb_eigs_solve : int + Number of eigenvalues to solve + nb_eigs_plot : int + Number of eigenvalues to plot + skip_eigs_threshold : float + Threshold for the eigenvalues to skip + plot_dir : str + Directory for the plots + """ + + diags = {} + + if sigma is None: + raise ValueError('please specify a value for sigma') + + print('---------------------------------------------------------------------------------------------------------') + print('Starting hcurl_solve_eigen_pbm function with: ') + print(' ncells = {}'.format(ncells)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' backend_language = {}'.format(backend_language)) + print('---------------------------------------------------------------------------------------------------------') + t_stamp = time_count() + print('building symbolic and discrete domain...') + + int_x, int_y = domain + if isinstance(ncells, int): + domain = build_multipatch_domain(domain_name=domain_name) + + elif domain_name == 'refined_square' or domain_name == 'square_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='identity') + + elif domain_name == 'curved_L_shape': + domain = build_cartesian_multipatch_domain(ncells, int_x, int_y, mapping='polar') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(ncells, int): + ncells = [ncells, ncells] + elif ncells.ndim == 1: + ncells = {patch.name: [ncells[i], ncells[i]] + for (i, patch) in enumerate(domain.interior)} + elif ncells.ndim == 2: + ncells = {patch.name: [ncells[int(patch.name[2])][int(patch.name[4])], + ncells[int(patch.name[2])][int(patch.name[4])]] for patch in domain.interior} + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) + mappings_list = list(mappings.values()) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + + V = VectorFunctionSpace('V', domain, kind='hcurl') + + u, v, F = elements_of(V, names='u, v, F') + nn = NormalVector('nn') + + I = domain.interfaces + boundary = domain.boundary + + kappa = 10 + k = 1 + + def jump(w): return plus(w) - minus(w) + def avr(w): return 0.5 * plus(w) + 0.5 * minus(w) + + expr1_I = cross(nn, jump(v)) * curl(avr(u))\ + + k * cross(nn, jump(u)) * curl(avr(v))\ + + kappa * cross(nn, jump(u)) * cross(nn, jump(v)) + + expr1 = curl(u) * curl(v) + expr1_b = -cross(nn, v) * curl(u) - k * cross(nn, u) * \ + curl(v) + kappa * cross(nn, u) * cross(nn, v) + # curl curl u = - omega**2 u + + expr2 = dot(u, v) + # expr2_I = kappa*cross(nn, jump(u))*cross(nn, jump(v)) + # expr2_b = -k*cross(nn, u)*curl(v) + kappa * cross(nn, u) * cross(nn, v) + + # Bilinear form a: V x V --> R + a = BilinearForm((u, v), integral(domain, expr1) + + integral(I, expr1_I) + integral(boundary, expr1_b)) + + # Linear form l: V --> R + # + integral(I, expr2_I) + integral(boundary, expr2_b)) + b = BilinearForm((u, v), integral(domain, expr2)) + + # +++++++++++++++++++++++++++++++ + # 2. Discretization + # +++++++++++++++++++++++++++++++ + + domain_h = discretize(domain, ncells=ncells) + Vh = discretize(V, domain_h, degree=degree) + + ah = discretize(a, domain_h, [Vh, Vh]) + Ah_m = ah.assemble().tosparse() + + bh = discretize(b, domain_h, [Vh, Vh]) + Bh_m = bh.assemble().tosparse() + + all_eigenvalues_2, all_eigenvectors_transp_2 = get_eigenvalues( + nb_eigs_solve, sigma, Ah_m, Bh_m) + + # Eigenvalue processing + t_stamp = time_count(t_stamp) + print('sorting out eigenvalues...') + zero_eigenvalues = [] + if skip_eigs_threshold is not None: + eigenvalues = [] + eigenvectors = [] + for val, vect in zip(all_eigenvalues_2, all_eigenvectors_transp_2.T): + if abs(val) < skip_eigs_threshold: + zero_eigenvalues.append(val) + # we skip the eigenvector + else: + eigenvalues.append(val) + eigenvectors.append(vect) + else: + eigenvalues = all_eigenvalues_2 + eigenvectors = all_eigenvectors_transp_2.T + diags['DG'] = True + for k, val in enumerate(eigenvalues): + diags['eigenvalue2_{}'.format(k)] = val # eigenvalues[k] + + for k, val in enumerate(zero_eigenvalues): + diags['skipped eigenvalue2_{}'.format(k)] = val + + t_stamp = time_count(t_stamp) + print('plotting the eigenmodes...') + + if 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) + + return diags, eigenvalues + + +def get_eigenvalues(nb_eigs, sigma, A_m, M_m): + """ + Compute the eigenvalues of the matrix A close to sigma and right-hand-side M + + Parameters + ---------- + nb_eigs : int + Number of eigenvalues to compute + sigma : float + Value close to which the eigenvalues are computed + A_m : sparse matrix + Matrix A + M_m : sparse matrix + Matrix M + """ + + print('----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ') + print( + 'computing {0} eigenvalues (and eigenvectors) close to sigma={1} with scipy.sparse.eigsh...'.format( + nb_eigs, + sigma)) + mode = 'normal' + which = 'LM' + # from eigsh docstring: + # ncv = number of Lanczos vectors generated ncv must be greater than k and smaller than n; + # it is recommended that ncv > 2*k. Default: min(n, max(2*k + 1, 20)) + ncv = 4 * nb_eigs + print('A_m.shape = ', A_m.shape) + try_lgmres = True + max_shape_splu = 24000 # OK for nc=20, deg=6 on pretzel_f + if A_m.shape[0] < max_shape_splu: + print('(via sparse LU decomposition)') + OPinv = None + tol_eigsh = 0 + else: + + OP_m = A_m - sigma * M_m + tol_eigsh = 1e-7 + if try_lgmres: + print( + '(via SPILU-preconditioned LGMRES iterative solver for A_m - sigma*M1_m)') + OP_spilu = spilu(OP_m, fill_factor=15, drop_tol=5e-5) + preconditioner = LinearOperator( + OP_m.shape, lambda x: OP_spilu.solve(x)) + tol = tol_eigsh + OPinv = LinearOperator( + matvec=lambda v: lgmres(OP_m, v, x0=None, tol=tol, atol=tol, M=preconditioner, + callback=lambda x: print( + 'cg -- residual = ', norm(OP_m.dot(x) - v)) + )[0], + shape=M_m.shape, + dtype=M_m.dtype + ) + + else: + # from https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.eigsh.html: + # the user can supply the matrix or operator OPinv, which gives x = OPinv @ b = [A - sigma * M]^-1 @ b. + # > here, minres: MINimum RESidual iteration to solve Ax=b + # suggested in https://github.com/scipy/scipy/issues/4170 + print('(with minres iterative solver for A_m - sigma*M1_m)') + OPinv = LinearOperator( + matvec=lambda v: minres( + OP_m, + v, + tol=1e-10)[0], + shape=M_m.shape, + dtype=M_m.dtype) + + eigenvalues, eigenvectors = eigsh( + A_m, k=nb_eigs, M=M_m, sigma=sigma, mode=mode, which=which, ncv=ncv, tol=tol_eigsh, OPinv=OPinv) + + print("done: eigenvalues found: " + repr(eigenvalues)) + return eigenvalues, eigenvectors diff --git a/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py new file mode 100644 index 000000000..5e887beda --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_eigen_testcases.py @@ -0,0 +1,290 @@ +""" + Runner script for solving the eigenvalue problem for the H(curl) operator for different discretizations. +""" + +import os +import numpy as np + +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_conga_2d import hcurl_solve_eigen_pbm +from psydac.feec.multipatch.examples.hcurl_eigen_pbms_dg_2d import hcurl_solve_eigen_pbm_dg +from psydac.feec.multipatch.utilities import time_count, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file +from psydac.api.postprocessing import OutputManager, PostProcessManager + +t_stamp_full = time_count() + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# +# test-case and numerical parameters: +method = 'feec' +# method = 'dg' + +operator = 'curl-curl' +degree = [3, 3] # shared across all patches + +# pretzel_f (18 patches) +# domain_name = 'pretzel_f' +# ncells = np.array([8, 8, 16, 16, 8, 4, 4, 4, 4, 4, 2, 2, 4, 16, 16, 8, 2, 2, 2]) +# ncells = np.array([4 for _ in range(18)]) + +# domain onlyneeded for square like domains +# domain = [[0, np.pi], [0, np.pi]] # interval in x- and y-direction + +# refined square domain +# domain_name = 'refined_square' +# the shape of ncells gives the shape of the domain, +# while the entries describe the isometric number of cells in each patch +# 2x2 = 4 patches +# ncells = np.array([[8, 4], +# [4, 4]]) +# 3x3= 9 patches +# ncells = np.array([[4, 2, 4], +# [2, 4, 2], +# [4, 2, 4]]) + +# L-shaped domain +# domain_name = 'square_L_shape' +# domain=[[-1, 1],[-1, 1]] # interval in x- and y-direction + +# The None indicates the patches to leave out +# 2x2 = 4 patches +# ncells = np.array([[None, 2], +# [2, 2]]) +# 4x4 = 16 patches +# ncells = np.array([[None, None, 4, 2], +# [None, None, 8, 4], +# [4, 8, 8, 4], +# [2, 4, 4, 2]]) +# 8x8 = 64 patches +# ncells = np.array([[None, None, None, None, 2, 2, 2,1 2], +# [None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 4, 4, 2, 2], +# [2, 2, 2, 4, 8, 4, 2, 2], +# [2, 2, 2, 4, 4, 4, 2, 2], +# [2, 2, 2, 2, 2, 2, 2, 2], +# [2, 2, 2, 2, 2, 2, 2, 2]]) + +# Curved L-shape domain +domain_name = 'curved_L_shape' +domain = [[1, 3], [0, np.pi / 4]] # interval in x- and y-direction + + +ncells = np.array([[None, 5], + [5, 10]]) +# ncells = 5 + +# ncells = np.array([[None, None, 2, 2], +# [None, None, 4, 2], +# [ 2, 4, 8, 4], +# [ 2, 2, 4, 4]]) + +# ncells = np.array([[None, None, None, 2, 2, 2], +# [None, None, None, 4, 4, 2], +# [None, None, None, 8, 4, 2], +# [2, 4, 8, 8, 4, 2], +# [2, 4, 4, 4, 4, 2], +# [2, 2, 2, 2, 2, 2]]) + +# ncells = np.array([[None, None, None, None, 2, 2, 2, 2], +# [None, None, None, None, 4, 4, 4, 2], +# [None, None, None, None, 8, 8, 4, 2], +# [None, None, None, None, 16, 8, 4, 2], +# [2, 4, 8, 16, 16, 8, 4, 2], +# [2, 4, 8, 8, 8, 8, 4, 2], +# [2, 4, 4, 4, 4, 4, 4, 2], +# [2, 2, 2, 2, 2, 2, 2, 2]]) + +# all kinds of different square refinements and constructions are possible, eg +# doubly connected domains +# ncells = np.array([[4, 2, 2, 4], +# [2, None, None, 2], +# [2, None, None, 2], +# [4, 2, 2, 4]]) + +gamma_h = 0 +# solves generalized eigenvalue problem with: B(v,w) = + +# <(I-P)v,(I-P)w> in rhs +generalized_pbm = True + +if operator == 'curl-curl': + nu = 0 + mu = 1 +else: + raise ValueError(operator) + +case_dir = 'eigenpbm_' + operator + '_' + method +ref_case_dir = case_dir + +ref_sigmas = None +sigma = None +nb_eigs_solve = None +nb_eigs_plot = None +skip_eigs_threshold = None +diags = None +eigenvalues = None + +if domain_name == 'refined_square': + assert domain == [[0, np.pi], [0, np.pi]] + ref_sigmas = [ + 1, 1, + 2, + 4, 4, + 5, 5, + 8, + 9, 9, + ] + sigma = 5 + nb_eigs_solve = 10 + nb_eigs_plot = 10 + skip_eigs_threshold = 1e-7 + +elif domain_name == 'square_L_shape': + assert domain == [[-1, 1], [-1, 1]] + ref_sigmas = [ + 1.47562182408, + 3.53403136678, + 9.86960440109, + 9.86960440109, + 11.3894793979, + ] + sigma = 6 + nb_eigs_solve = 5 + nb_eigs_plot = 5 + skip_eigs_threshold = 1e-7 + +elif domain_name == 'curved_L_shape': + # ref eigenvalues from Monique Dauge benchmark page + assert domain == [[1, 3], [0, np.pi / 4]] + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + +elif domain_name in ['pretzel_f']: + if operator == 'curl-curl': + # ref sigmas computed with nc=20 and deg=6 and gamma = 0 (and + # generalized ev-pbm) + ref_sigmas = [ + 0.1795339843, + 0.1992261261, + 0.6992717244, + 0.8709410438, + 1.1945106937, + 1.2546992683, + ] + + sigma = .8 + nb_eigs_solve = 10 + nb_eigs_plot = 5 + skip_eigs_threshold = 1e-7 + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +common_diag_filename = './' + case_dir + '_diags.txt' + + +params = { + 'domain_name': domain_name, + 'domain': domain, + 'operator': operator, + 'mu': mu, + 'nu': nu, + 'ncells': ncells, + 'degree': degree, + 'gamma_h': gamma_h, + 'generalized_pbm': generalized_pbm, + 'nb_eigs_solve': nb_eigs_solve, + 'skip_eigs_threshold': skip_eigs_threshold +} + +print(params) + +# backend_language = 'numba' +backend_language = 'pyccel-gcc' + +dims = 1 if isinstance(ncells, int) else ncells.shape +sz = 1 if isinstance(ncells, int) else ncells[ncells != None].sum() + +# get_run_dir(domain_name, nc, deg) +run_dir = domain_name + str(dims) + 'patches_' + 'size_{}'.format(sz) +plot_dir = get_plot_dir(case_dir, run_dir) +diag_filename = plot_dir + '/' + diag_fn() +common_diag_filename = './' + case_dir + '_diags.txt' + + +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') +print(' Calling hcurl_solve_eigen_pbm() with params = {}'.format(params)) +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# calling eigenpbm solver for: +# +# find lambda in R and u in H0(curl), such that +# A u = lambda * u on \Omega +# with +# +# A u := mu * curl curl u - nu * grad div u +# +# note: +# - we look for nb_eigs_solve eigenvalues close to sigma (skip zero eigenvalues if skip_zero_eigs==True) +# - we plot nb_eigs_plot eigenvectors +if method == 'feec': + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=gamma_h, + generalized_pbm=generalized_pbm, + nu=nu, + mu=mu, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language=backend_language, + plot_dir=plot_dir, + ) + +elif method == 'dg': + diags, eigenvalues = hcurl_solve_eigen_pbm_dg( + ncells=ncells, degree=degree, + nu=nu, + mu=mu, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language=backend_language, + plot_dir=plot_dir, + ) + +if ref_sigmas is not None: + errors = [] + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + diags['error_{}'.format(k)] = abs(eigenvalues[k] - ref_sigmas[k]) +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=diag_filename, + params=params) +write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=common_diag_filename, + params=params) + +# PM = PostProcessManager(geometry_file=, ) +time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py new file mode 100644 index 000000000..e2252a0e2 --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_source_pbms_conga_2d.py @@ -0,0 +1,296 @@ +""" + solver for the problem: find u in H(curl), such that + + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega + + where the operator + + A u := eta * u + mu * curl curl u - nu * grad div u + + is discretized as Ah: V1h -> V1h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h +""" + +import os +import numpy as np + +from sympde.topology import Derham + + +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 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.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='tilde_Pi', source_type='manu_J', + eta=-10., mu=1., nu=1., gamma_h=10., + project_sol=True, plot_dir=None): + """ + solver for the problem: find u in H(curl), such that + + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega + + where the operator + + A u := eta * u + mu * curl curl u - nu * grad div u + + is discretized as Ah: V1h -> V1h in a broken-FEEC approach involving a discrete sequence on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + + Examples: + + - time-harmonic maxwell equation with + eta = -omega**2 + mu = 1 + nu = 0 + + - Hodge-Laplacian operator L = A with + eta = 0 + mu = 1 + nu = 1 + + :param nc: nb of cells per dimension, in each patch + :param deg: coordinate degree in each patch + :param gamma_h: jump penalization parameter + :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() + """ + diags = {} + + degree = [deg, deg] + + + print('---------------------------------------------------------------------------------------------------------') + print('Starting solve_hcurl_source_pbm function with: ') + print(' ncells = {}'.format(nc)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' source_proj = {}'.format(source_proj)) + print(' backend_language = {}'.format(backend_language)) + print('---------------------------------------------------------------------------------------------------------') + + print() + print(' -- building discrete spaces and operators --') + + t_stamp = time_count() + print(' .. 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()) + + if isinstance(nc, int): + ncells = [nc, nc] + else: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + + + t_stamp = time_count(t_stamp) + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + domain_h = discretize(domain, ncells=ncells) + + t_stamp = time_count(t_stamp) + print(' .. discrete derham sequence...') + derham_h = discretize(derham, domain_h, degree=degree) + + t_stamp = time_count(t_stamp) + print(' .. commuting projection operators...') + nquads = [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, 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)) + diags['ndofs_V0'] = V0h.nbasis + diags['ndofs_V1'] = V1h.nbasis + diags['ndofs_V2'] = V2h.nbasis + + t_stamp = time_count(t_stamp) + print(' .. Id operator and matrix...') + I1 = 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, 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...') + # conforming Projections (should take into account the boundary conditions + # of the continuous deRham sequence) + 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.derivatives(kind='linop') + + # Conga (projection-based) stiffness matrices + # curl curl: + t_stamp = time_count(t_stamp) + print(' .. curl-curl stiffness matrix...') + pre_CC = bD1.T @ H2 @ bD1 + + # grad div: + t_stamp = time_count(t_stamp) + print(' .. grad-div 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...') + JS = (I1 - cP1).T @ H1 @ (I1 - cP1) + + + t_stamp = time_count(t_stamp) + print(' .. full operator matrix...') + print('eta = {}'.format(eta)) + print('mu = {}'.format(mu)) + print('nu = {}'.format(nu)) + print('STABILIZATION: gamma_h = {}'.format(gamma_h)) + # useful for the boundary condition (if present) + 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,) + + # 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 = 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 P1.T ...') + tilde_f = cP1.T @ tilde_f + + def lift_u_bc(u_bc): + if u_bc is not None: + ubc = P1_phys(u_bc, P1, domain).coeffs + ubc -= cP1.dot(ubc) + + else: + ubc = None + + return ubc + + + ubc = lift_u_bc(u_bc) + + 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 -= pre_A.dot(ubc) + + # direct solve with scipy spsolve + t_stamp = time_count(t_stamp) + 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: + print(' .. projecting the homogeneous solution on the conforming problem space...') + u = cP1.dot(u) + + if ubc is not None: + # adding the lifted boundary condition + t_stamp = time_count(t_stamp) + print(' .. adding the lifted boundary condition...') + u += ubc + + uh = FemField(V1h, coeffs=u) + #need cp1 here? + 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() + OM.export_fields(vh=uh) + OM.export_fields(jh=jh) + OM.export_space_info() + OM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + + '/spaces.yml', + fields_file=plot_dir + + '/fields.h5') + PM.export_to_vtk( + plot_dir + "/sol", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='vh') + PM.export_to_vtk( + plot_dir + "/source", + grid=None, + npts_per_cell=[6] * 2, + snapshots='all', + fields='jh') + + PM.close() + + time_count(t_stamp) + + if u_ex: + u_ex_p = P1_phys(u_ex, P1, domain).coeffs + + err = u_ex_p - u + print(err.inner(H1.dot(err))) + 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 + + return diags diff --git a/psydac/feec/multipatch/examples/hcurl_source_testcase.py b/psydac/feec/multipatch/examples/hcurl_source_testcase.py new file mode 100644 index 000000000..720fb75b5 --- /dev/null +++ b/psydac/feec/multipatch/examples/hcurl_source_testcase.py @@ -0,0 +1,140 @@ +""" + Runner script for solving the H(curl) source problem. +""" + +import os +import numpy as np +from psydac.feec.multipatch.examples.hcurl_source_pbms_conga_2d import solve_hcurl_source_pbm + +from psydac.feec.multipatch.utilities import time_count, FEM_sol_fn, get_run_dir, get_plot_dir, get_mat_dir, get_sol_dir, diag_fn +from psydac.feec.multipatch.utils_conga_2d import write_diags_to_file + +t_stamp_full = time_count() + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# +# main test-cases used for the ppc paper: + +# test_case = 'maxwell_hom_eta=50' # used in paper +#test_case = 'maxwell_hom_eta=170' # used in paper +test_case = 'maxwell_inhom' # used in paper + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +# numerical parameters: +domain_name = 'pretzel_f' +# domain_name = 'curved_L_shape' + +# currently only 'tilde_Pi' is implemented +source_proj = 'tilde_Pi' + +# nc_s = [np.array([16 for _ in range(18)])] + +# corners in pretzel [2, 2, 2*,2*, 2, 1, 1, 1, 1, 1, 0, 0, 1, 2*, 2*, 2, 0, 0 ] +nc_s = [np.array([16, 16, 16, 16, 16, 8, 8, 8, 8, + 8, 8, 8, 8, 16, 16, 16, 8, 8])] +# nc_s = [10] +# refine handles only +# nc_s = [np.array([16, 16, 16, 16, 16, 8, 8, 8, 8, 4, 2, 2, 4, 16, 16, 16, 2, 2])] + +# refine source +# nc_s = [np.array([32, 8, 8, 32, 32, 32, 32, 8, 8, 8, 8, 8, 8, 32, 8, 8, 8, 8])] + +deg_s = [3] + +if test_case == 'maxwell_hom_eta=50': + homogeneous = True + source_type = 'elliptic_J' + omega = np.sqrt(50) # source time pulsation + +elif test_case == 'maxwell_hom_eta=170': + homogeneous = True + source_type = 'elliptic_J' + omega = np.sqrt(170) # source time pulsation + +elif test_case == 'maxwell_inhom': + homogeneous = False + source_type = 'manu_maxwell_inhom' + omega = np.pi + +else: + raise ValueError(test_case) + +case_dir = test_case + +eta = int(-omega**2 * roundoff) / roundoff + +project_sol = True # True # (use conf proj of solution for visualization) +gamma_h = 10 + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +common_diag_filename = './' + case_dir + '_diags.txt' + +for nc in nc_s: + for deg in deg_s: + + params = { + 'domain_name': domain_name, + 'nc': nc, + 'deg': deg, + 'homogeneous': homogeneous, + 'source_type': source_type, + 'source_proj': source_proj, + 'project_sol': project_sol, + 'omega': omega, + 'gamma_h': gamma_h, + } + # backend_language = 'numba' + backend_language = 'pyccel-gcc' + + run_dir = get_run_dir(domain_name, nc, deg, source_type=source_type) + plot_dir = get_plot_dir(case_dir, run_dir) + diag_filename = plot_dir + '/' + \ + diag_fn(source_type=source_type, source_proj=source_proj) + + + + print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + print(' Calling solve_hcurl_source_pbm() with params = {}'.format(params)) + print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + # calling solver for: + # + # find u in H(curl), s.t. + # A u = f on \Omega + # n x u = n x u_bc on \partial \Omega + # with + # A u := eta * u + mu * curl curl u - nu * grad div u + + diags = solve_hcurl_source_pbm( + nc=nc, deg=deg, + eta=eta, + nu=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + source_proj=source_proj, + backend_language=backend_language, + project_sol=project_sol, + gamma_h=gamma_h, + plot_dir=plot_dir, + ) + + # + # ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + + write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=diag_filename, + params=params) + write_diags_to_file( + diags, + script_filename=__file__, + diag_filename=common_diag_filename, + params=params) + +time_count(t_stamp_full, msg='full program') diff --git a/psydac/feec/multipatch/examples/ppc_test_cases.py b/psydac/feec/multipatch/examples/ppc_test_cases.py new file mode 100644 index 000000000..f0f7d0f8c --- /dev/null +++ b/psydac/feec/multipatch/examples/ppc_test_cases.py @@ -0,0 +1,408 @@ +# coding: utf-8 +import os +import numpy as np + +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' + +def get_phi_pulse(x_0, y_0, domain=None): + x, y = domain.coordinates + ds2_0 = (0.02)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + + return phi_0 + + +def get_div_free_pulse(x_0, y_0, domain=None): + x, y = domain.coordinates + ds2_0 = (0.02)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 + dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + f_x = dy_phi_0 + f_y = - dx_phi_0 + f_vect = Tuple(f_x, f_y) + + return f_vect + + +def get_curl_free_pulse(x_0, y_0, domain=None, pp=False): + # return -grad phi_0 + x, y = domain.coordinates + if pp: + # psi=phi + ds2_0 = (0.02)**2 + else: + ds2_0 = (0.1)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 + dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + f_x = -dx_phi_0 + f_y = -dy_phi_0 + f_vect = Tuple(f_x, f_y) + + return f_vect + + +def get_Delta_phi_pulse(x_0, y_0, domain=None, pp=False): + # return -Delta phi_0, with same phi_0 as in get_curl_free_pulse() + x, y = domain.coordinates + if pp: + # psi=phi + ds2_0 = (0.02)**2 + else: + ds2_0 = (0.1)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dxx_sig_0 = 2 + dyy_sig_0 = 2 + dxx_phi_0 = ((dx_sig_0 * sigma_0 / ds2_0)**2 - + ((dx_sig_0)**2 + dxx_sig_0 * sigma_0) / ds2_0) * phi_0 + dyy_phi_0 = ((dy_sig_0 * sigma_0 / ds2_0)**2 - + ((dy_sig_0)**2 + dyy_sig_0 * sigma_0) / ds2_0) * phi_0 + f = - dxx_phi_0 - dyy_phi_0 + + return f + + +def get_Gaussian_beam_old(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = (10, 0) + nk = np.sqrt(k[0]**2 + k[1]**2) + + v = (k[0] / nk, k[1] / nk) + + sigma = 0.05 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k[1] * x + k[0] * y) * ef + B = (-v[1] * x + v[0] * y) / (sigma**2) * E + + return Tuple(v[0] * E, v[1] * E), B + + +def get_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + + x = x - x_0 + y = y - y_0 + + sigma = 0.1 + + xy = x**2 + y**2 + ef = 1 / (sigma**2) * exp(- xy / (2 * sigma**2)) + + # E = curl exp + E = Tuple(y * ef, -x * ef) + + # B = curl E + B = (xy / (sigma**2) - 2) * ef + + return E, B + + +def get_diag_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = (np.pi, np.pi) + nk = np.sqrt(k[0]**2 + k[1]**2) + + v = (k[0] / nk, k[1] / nk) + + sigma = 0.25 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k[1] * x + k[0] * y) * ef + B = (-v[1] * x + v[0] * y) / (sigma**2) * E + + return Tuple(v[0] * E, v[1] * E), B + + +def get_easy_Gaussian_beam(x_0, y_0, domain=None): + # return E = cos(k*x) exp( - x^2 + y^2 / 2 sigma^2) v + x, y = domain.coordinates + x = x - x_0 + y = y - y_0 + + k = pi + sigma = 0.5 + + xy = x**2 + y**2 + ef = exp(- xy / (2 * sigma**2)) + + E = cos(k * y) * ef + B = -y / (sigma**2) * E + + return Tuple(E, 0), B + + +def get_Gaussian_beam2(x_0, y_0, domain=None): + """ + Gaussian beam + Beam inciding from the left, centered and normal to wall: + x: axial normalized distance to the beam's focus + y: radial normalized distance to the center axis of the beam + """ + x, y = domain.coordinates + + x0 = x_0 + y0 = y_0 + theta = pi / 2 + w0 = 1 + + t = [(x - x0) * cos(theta) - (y - y0) * sin(theta), + (x - x0) * sin(theta) + (y - y0) * cos(theta)] + + EW0 = 1.0 # amplitude at the waist + k0 = 2 * pi # free-space wavenumber + + x_ray = pi * w0 ** 2 # Rayleigh range + + w = w0 * (1 + t[0]**2 / x_ray**2)**0.5 # width + curv = t[0] / (t[0]**2 + x_ray**2) # curvature + + # corresponds to atan(x / x_ray), which is the Gouy phase + gouy_psi = -0.5 * atan2(t[0] / x_ray, 1.) + + EW_mod = EW0 * (w0 / w)**0.5 * exp(-(t[1] ** 2) / (w ** 2)) # Amplitude + phase = k0 * t[0] + 0.5 * k0 * curv * t[1] ** 2 + gouy_psi # Phase + + EW_r = EW_mod * cos(phase) # Real part + EW_i = EW_mod * sin(phase) # Imaginary part + + B = 0 # t[1]/(w**2) * EW_r + + return Tuple(0, EW_r), B + + +def get_source_and_sol_for_magnetostatic_pbm( + source_type=None, + domain=None, domain_name=None, + refsol_params=None +): + """ + provide source, and exact solutions when available, for: + + Find u=B in H(curl) such that + + div B = 0 + curl B = j + + written as a mixed problem, see solve_magnetostatic_pbm() + """ + u_ex = None # exact solution + x, y = domain.coordinates + if source_type == 'dipole_J': + # we compute two possible source terms: + # . a dipole current j_scal = phi_0 - phi_1 (two blobs) + # . and f_vect = curl j_scal + x_0 = 1.0 + y_0 = 1.0 + ds2_0 = (0.02)**2 + sigma_0 = (x - x_0)**2 + (y - y_0)**2 + phi_0 = exp(-sigma_0**2 / (2 * ds2_0)) + dx_sig_0 = 2 * (x - x_0) + dy_sig_0 = 2 * (y - y_0) + dx_phi_0 = - dx_sig_0 * sigma_0 / ds2_0 * phi_0 + dy_phi_0 = - dy_sig_0 * sigma_0 / ds2_0 * phi_0 + + x_1 = 2.0 + y_1 = 2.0 + ds2_1 = (0.02)**2 + sigma_1 = (x - x_1)**2 + (y - y_1)**2 + phi_1 = exp(-sigma_1**2 / (2 * ds2_1)) + dx_sig_1 = 2 * (x - x_1) + dy_sig_1 = 2 * (y - y_1) + dx_phi_1 = - dx_sig_1 * sigma_1 / ds2_1 * phi_1 + dy_phi_1 = - dy_sig_1 * sigma_1 / ds2_1 * phi_1 + + f_scal = None + j_scal = phi_0 - phi_1 + f_x = dy_phi_0 - dy_phi_1 + f_y = - dx_phi_0 + dx_phi_1 + f_vect = Tuple(f_x, f_y) + + else: + raise ValueError(source_type) + + return f_scal, f_vect, j_scal, u_ex + + +def get_source_and_solution_hcurl( + source_type=None, eta=0, mu=0, nu=0, + domain=None, domain_name=None): + """ + provide source, and exact solutions when available, for: + + Find u in H(curl) such that + + A u = f on \\Omega + n x u = n x u_bc on \\partial \\Omega + + with + + A u := eta * u + mu * curl curl u - nu * grad div u + + see solve_hcurl_source_pbm() + """ + + # exact solutions (if available) + u_ex = None + curl_u_ex = None + div_u_ex = None + + # bc solution: describe the bc on boundary. Inside domain, values should + # not matter. Homogeneous bc will be used if None + u_bc = None + + # source terms + f_vect = None + + # auxiliary term (for more diagnostics) + grad_phi = None + phi = None + + x, y = domain.coordinates + + if source_type == 'manu_maxwell_inhom': + # used for Maxwell equation with manufactured solution + f_vect = Tuple(eta * sin(pi * y) - pi**2 * sin(pi * y) * cos(pi * x) + pi**2 * sin(pi * y), + eta * sin(pi * x) * cos(pi * y) + pi**2 * sin(pi * x) * cos(pi * y)) + if nu == 0: + u_ex = Tuple(sin(pi * y), sin(pi * x) * cos(pi * y)) + curl_u_ex = pi * (cos(pi * x) * cos(pi * y) - cos(pi * y)) + div_u_ex = -pi * sin(pi * x) * sin(pi * y) + else: + raise NotImplementedError + u_bc = u_ex + + elif source_type == 'elliptic_J': + # no manufactured solution for Maxwell pbm + x0 = 1.5 + y0 = 1.5 + s = (x - x0) - (y - y0) + t = (x - x0) + (y - y0) + a = (1 / 1.9)**2 + b = (1 / 1.2)**2 + sigma2 = 0.0121 + tau = a * s**2 + b * t**2 - 1 + phi = exp(-tau**2 / (2 * sigma2)) + dx_tau = 2 * (a * s + b * t) + dy_tau = 2 * (-a * s + b * t) + + f_x = dy_tau * phi + f_y = - dx_tau * phi + f_vect = Tuple(f_x, f_y) + + else: + raise ValueError(source_type) + + # u_ex = Tuple(0, 1) # DEBUG + return f_vect, u_bc, u_ex, curl_u_ex, div_u_ex # , phi, grad_phi + + +def get_source_and_solution_h1(source_type=None, eta=0, mu=0, + domain=None, domain_name=None): + """ + provide source, and exact solutions when available, for: + + Find u in H^1, such that + + A u = f on \\Omega + u = u_bc on \\partial \\Omega + + with + + A u := eta * u - mu * div grad u + + see solve_h1_source_pbm() + """ + + # exact solutions (if available) + u_ex = None + + # bc solution: describe the bc on boundary. Inside domain, values should + # not matter. Homogeneous bc will be used if None + u_bc = None + + # source terms + f_scal = None + + # auxiliary term (for more diagnostics) + grad_phi = None + phi = None + + x, y = domain.coordinates + + if source_type in ['manu_poisson_elliptic']: + x0 = 1.5 + y0 = 1.5 + s = (x - x0) - (y - y0) + t = (x - x0) + (y - y0) + a = (1 / 1.9)**2 + b = (1 / 1.2)**2 + sigma2 = 0.0121 + tau = a * s**2 + b * t**2 - 1 + phi = exp(-tau**2 / (2 * sigma2)) + dx_tau = 2 * (a * s + b * t) + dy_tau = 2 * (-a * s + b * t) + dxx_tau = 2 * (a + b) + dyy_tau = 2 * (a + b) + + dx_phi = (-tau * dx_tau / sigma2) * phi + dy_phi = (-tau * dy_tau / sigma2) * phi + grad_phi = Tuple(dx_phi, dy_phi) + + f_scal = -((tau * dx_tau / sigma2)**2 - (tau * dxx_tau + dx_tau**2) / sigma2 + + (tau * dy_tau / sigma2)**2 - (tau * dyy_tau + dy_tau**2) / sigma2) * phi + + # exact solution of -p'' = f with hom. bc's on pretzel domain + if mu == 1 and eta == 0: + u_ex = phi + else: + print('WARNING (54375385643): exact solution not available in this case!') + + if not domain_name in ['pretzel', 'pretzel_f']: + # we may have non-hom bc's + u_bc = u_ex + + elif source_type == 'manu_poisson_2': + f_scal = -4 + if mu == 1 and eta == 0: + u_ex = x**2 + y**2 + else: + raise NotImplementedError + u_bc = u_ex + + elif source_type == 'manu_poisson_sincos': + u_ex = sin(pi * x) * cos(pi * y) + f_scal = (eta + 2 * mu * pi**2) * u_ex + u_bc = u_ex + + else: + raise ValueError(source_type) + + return f_scal, u_bc, u_ex diff --git a/psydac/feec/multipatch/examples/timedomain_maxwell.py b/psydac/feec/multipatch/examples/timedomain_maxwell.py new file mode 100644 index 000000000..f382f0111 --- /dev/null +++ b/psydac/feec/multipatch/examples/timedomain_maxwell.py @@ -0,0 +1,704 @@ +""" + solver for the TD Maxwell problem: find E(t) in H(curl), B in L2, such that + + dt E - curl B = -J on \\Omega + dt B + curl E = 0 on \\Omega + n x E = n x E_bc on \\partial \\Omega + + with Ampere discretized weakly and Faraday discretized strongly, in a broken-FEEC approach on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + (Eh) (Bh) +""" + +from pytest import param +from mpi4py import MPI + +import os +import numpy as np +import scipy as sp +from collections import OrderedDict +import matplotlib.pyplot as plt + +from sympy import lambdify, Matrix + +from scipy.sparse.linalg import spsolve +from scipy import special + +from sympde.calculus import dot +from sympde.topology import element_of +from sympde.expr.expr import LinearForm +from sympde.expr.expr import integral, Norm +from sympde.topology import Derham +from psydac.linalg.basic import IdentityOperator + +from psydac.api.settings import PSYDAC_BACKENDS +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 + +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.fem.basic import FemField +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(*, + nc=4, + deg=4, + final_time=20, + cfl_max=0.8, + dt_max=None, + domain_name='pretzel_f', + backend='pyccel-gcc', + source_type='zero', + source_omega=None, + source_proj='P_L2', + project_sol=False, + filter_source=True, + E0_type='pulse_2', + E0_proj='P_L2', + plot_dir=None, + plot_time_ranges=None, + domain_lims=None + ): + """ + solver for the TD Maxwell problem: find E(t) in H(curl), B in L2, such that + + dt E - curl B = -J on \\Omega + dt B + curl E = 0 on \\Omega + n x E = n x E_bc on \\partial \\Omega + + with Ampere discretized weakly and Faraday discretized strongly, in a broken-FEEC approach on a 2D multipatch domain \\Omega, + + V0h --grad-> V1h -—curl-> V2h + (Eh) (Bh) + + Parameters + ---------- + nc : int + Number of cells (same along each direction) in every patch. + + deg : int + Polynomial degree (same along each direction) in every patch, for the + spline space V0 in H1. + + final_time : float + Final simulation time. Given that the speed of light is set to c=1, + this can be easily chosen based on the wave transit time in the domain. + + cfl_max : float + Maximum Courant parameter in the simulation domain, used to determine + the time step size. + + dt_max : float + Maximum time step size, which has to be met together with cfl_max. This + additional constraint is useful to resolve a time-dependent source. + + domain_name : str + Name of the multipatch geometry used in the simulation, to be chosen + among those available in the function `build_multipatch_domain`. + + backend : str + Name of the backend used for acceleration of the computational kernels, + to be chosen among the available keys of the PSYDAC_BACKENDS dict. + + source_type : str {'zero' | 'pulse' | 'cf_pulse' | 'Il_pulse'} + Name that identifies the space-time profile of the current source, to be + chosen among those available in the function get_source_and_solution(). + Available options: + - 'zero' : no current source + - 'pulse' : div-free current source, time-harmonic + - 'cf_pulse': curl-free current source, time-harmonic + - 'Il_pulse': Issautier-like pulse, with both a div-free and a + curl-free component, not time-harmonic. + + source_omega : float + Pulsation of the time-harmonic component (if any) of a time-dependent + current source. + + source_proj : str {'P_geom' | 'P_L2'} + Name of the approximation operator for the current source: 'P_geom' is + a geometric projector (based on inter/histopolation) which yields the + primal degrees of freedom; 'P_L2' is an L2 projector which yields the + dual degrees of freedom. Change of basis from primal to dual (and vice + versa) is obtained through multiplication with the proper Hodge matrix. + + project_sol : bool + Whether the solution fields should be projected onto the corresponding + conforming spaces before plotting them. + + filter_source : bool + If True, the current source will be filtered with the conforming + projector operator (or its dual, depending on which basis is used). + + 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. + + plot_dir : str + Path to the directory where the figures will be saved. + + plot_time_ranges : list + List of lists, of the form `[[start, end], dtp]`, where `[start, end]` + is a time interval and `dtp` is the time between two successive plots. + + 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]]`. + + """ + degree = [deg, deg] + + if source_omega is not None: + period_time = 2 * np.pi / source_omega + Nt_pp = period_time // dt_max + + if plot_time_ranges is None: + plot_time_ranges = [ + [[0, final_time], final_time] + ] + + + print('---------------------------------------------------------------------------------------------------------') + print('Starting solve_td_maxwell_pbm function with: ') + print(' ncells = {}'.format(nc)) + print(' degree = {}'.format(degree)) + print(' domain_name = {}'.format(domain_name)) + print(' E0_type = {}'.format(E0_type)) + print(' E0_proj = {}'.format(E0_proj)) + print(' source_type = {}'.format(source_type)) + print(' source_proj = {}'.format(source_proj)) + print(' backend = {}'.format(backend)) + print('---------------------------------------------------------------------------------------------------------') + + + print() + print(' -- building discrete spaces and operators --') + + t_stamp = time_count() + print(' .. multi-patch domain...') + if domain_name == 'refined_square' or domain_name == 'square_L_shape': + int_x, int_y = domain_lims + domain = build_cartesian_multipatch_domain(nc, int_x, int_y, mapping='identity') + + else: + domain = build_multipatch_domain(domain_name=domain_name) + + if isinstance(nc, int): + ncells = [nc, nc] + elif nc.ndim == 1: + ncells = {patch.name: [nc[i], nc[i]] + for (i, patch) in enumerate(domain.interior)} + 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} + + mappings = OrderedDict([(P.logical_domain, P.mapping) + for P in domain.interior]) + mappings_list = list(mappings.values()) + + + t_stamp = time_count(t_stamp) + print(' .. derham sequence...') + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + + t_stamp = time_count(t_stamp) + print(' .. discrete domain...') + domain_h = discretize(domain, ncells=ncells) + + t_stamp = time_count(t_stamp) + print(' .. discrete derham sequence...') + + derham_h = discretize(derham, domain_h, degree=degree) + + t_stamp = time_count(t_stamp) + print(' .. commuting projection operators...') + nquads = [4 * (d + 1) for d in degree] + P0, P1, P2 = derham_h.projectors(nquads=nquads) + + t_stamp = time_count(t_stamp) + print(' .. multi-patch spaces...') + V0h, V1h, V2h = derham_h.spaces + + t_stamp = time_count(t_stamp) + print(' .. Id operator and matrix...') + I1 = IdentityOperator(V1h.coeff_space) + + t_stamp = time_count(t_stamp) + print(' .. Hodge operators...') + 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(' .. conforming Projection operators...') + 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...') + bD0, bD1 = derham_h.derivatives(kind='linop') + + + if plot_dir is not None and not os.path.exists(plot_dir): + os.makedirs(plot_dir) + + print(' .. matrix of the primal curl (in primal bases)...') + 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 + from sympde.topology import elements_of + + u, v = elements_of(derham.V1, names='u, v') + nn = NormalVector('nn') + boundary = domain.boundary + expr_b = cross(nn, u) * cross(nn, v) + + a = BilinearForm((u, v), integral(boundary, expr_b)) + ah = discretize(a, domain_h, [V1h, V1h], backend=PSYDAC_BACKENDS[backend],) + A_eps = ah.assemble() + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Compute stable time step size based on max CFL and max dt + dt = compute_stable_dt(C=C, dC=dC, cfl_max=cfl_max, dt_max=dt_max) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + # Absorbing dC + 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.sparse 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)...') + D = dH0 @ cP0.T @ bD0.T @ H1 + + + print(" Reduce time step to match the simulation final time:") + Nt = int(np.ceil(final_time / dt)) + dt = final_time / Nt + print(f" . Time step size : dt = {dt}") + print(f" . Nb of time steps: Nt = {Nt}") + + # ... + def is_plotting_time(nt, *, dt=dt, Nt=Nt, plot_time_ranges=plot_time_ranges): + if nt in [0, Nt]: + return True + for [start, end], dt_plots in plot_time_ranges: + # number of time steps between two successive plots + ds = max(dt_plots // dt, 1) + if (start <= nt * dt <= end) and (nt % ds == 0): + return True + return False + # ... + + + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' total nb of time steps: Nt = {}, final time: T = {:5.4f}'.format(Nt, final_time)) + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + print(' ------ ------ ------ ------ ------ ------ ------ ------ ') + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # source + + t_stamp = time_count(t_stamp) + print() + print(' -- getting source --') + f0_h = None + f0_harmonic_h = None + rho0_h = None + + if source_type == 'zero': + + f0 = None + f0_harmonic = None + + elif source_type == 'pulse': + + 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=np.pi/2, y_0=np.pi/2, domain=domain) + + elif source_type == 'Il_pulse': # Issautier-like pulse + # source will be + # J = curl A + cos(om*t) * grad phi + # so that + # dt rho = - div J = - cos(om*t) Delta phi + # for instance, with rho(t=0) = 0 this gives + # rho = - sin(om*t)/om * Delta phi + # and Gauss' law reads + # div E = rho = - sin(om*t)/om * Delta phi + f0 = get_div_free_pulse(x_0=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) + assert u_bc is None # only homogeneous BC's for now + + + if source_omega is not None: + f0_harmonic = f0 + f0 = None + + def source_enveloppe(tau): + return 1 + + t_stamp = time_count(t_stamp) + 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).coeffs + tilde_f0_h = H1.dot(f0_h) + + if f0_harmonic is not None: + f0_harmonic_h = P1_phys(f0_harmonic, P1, domain).coeffs + tilde_f0_harmonic_h = H1.dot(f0_harmonic_h) + + elif source_proj == 'P_L2': + + if f0 is not None: + if source_type == 'Il_pulse': + source_name = 'Il_pulse_f0' + else: + source_name = source_type + + 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 + + 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) + + t_stamp = time_count(t_stamp) + if filter_source: + print(' .. filtering the source...') + if tilde_f0_h is not None: + tilde_f0_h = cP1.T @ tilde_f0_h + + if tilde_f0_harmonic_h is not None: + tilde_f0_harmonic_h = cP1.T @ tilde_f0_harmonic_h + + 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) + + + if f0_h is None: + f0_h = V1h.coeff_space.zeros() + + t_stamp = time_count(t_stamp) + + # diags arrays + E_norm2_diag = np.zeros(Nt + 1) + B_norm2_diag = np.zeros(Nt + 1) + divE_norm2_diag = np.zeros(Nt + 1) + time_diag = np.zeros(Nt + 1) + PE_norm2_diag = np.zeros(Nt + 1) + I_PE_norm2_diag = np.zeros(Nt + 1) + J_norm2_diag = np.zeros(Nt + 1) + if source_type == 'Il_pulse': + GaussErr_norm2_diag = np.zeros(Nt + 1) + GaussErrP_norm2_diag = np.zeros(Nt + 1) + else: + GaussErr_norm2_diag = None + GaussErrP_norm2_diag = None + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # initial solution + + print(' .. initial solution ..') + + # initial B sol + B_h = V2h.coeff_space.zeros() + E_h = V1h.coeff_space.zeros() + + # initial E sol + if E0_type == 'zero': + E_h = V1h.coeff_space.zeros() + + elif E0_type == 'pulse': + + 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) + E_h = E0_h.coeffs + + elif E0_proj == 'P_L2': + + 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) + + elif E0_type == 'pulse_2': + + 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) + E_h = E0_h.coeffs + + B0_h = P2_phys(B0, P2, domain) + B_h = B0_h.coeffs + + elif E0_proj == 'P_L2': + + 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_B0_h = get_dual_dofs(Vh=V2h, f=B0, domain_h=domain_h, backend_language=backend) + B_h = dH2.dot(tilde_B0_h) + + else: + raise ValueError(E0_type) + + # ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- + # time loop + + def compute_diags(E_h, B_h, J_h, nt): + time_diag[nt] = (nt) * dt + 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 - D @ 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') + OM1.add_spaces(V1h=V1h) + OM1.export_space_info() + + OM2 = OutputManager(plot_dir + '/spaces2.yml', plot_dir + '/fields2.h5') + OM2.add_spaces(V2h=V2h) + OM2.export_space_info() + + Eh = FemField(V1h, coeffs=cP1 @ E_h) + OM1.add_snapshot(t=0, ts=0) + OM1.export_fields(Eh=Eh) + + Bh = FemField(V2h, coeffs=B_h) + OM2.add_snapshot(t=0, ts=0) + OM2.export_fields(Bh=Bh) + + + 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_h -= (dt / 2) * C @ E_h + + # ampere: En -> En+1 + 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 + + E_h = dCH1 @ E_h + dt * (dC @ B_h - f_h) + + # 1/2 faraday: Bn+1/2 -> Bn+1 + B_h -= (dt / 2) * C @ E_h + + # diags: + compute_diags(E_h, B_h, f_h, nt=nt + 1) + + + + if is_plotting_time(nt + 1) and plot_dir: + print("Plot fields") + + Eh = FemField(V1h, coeffs=cP1 @ E_h) + OM1.add_snapshot(t=nt * dt, ts=nt) + OM1.export_fields(Eh=Eh) + + Bh = FemField(V2h, coeffs=B_h) + OM2.add_snapshot(t=nt * dt, ts=nt) + OM2.export_fields(Bh=Bh) + + + if plot_dir: + OM1.close() + + print("Post process fields") + 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=4, + snapshots='all', + fields='Eh') + PM.close() + + PM = PostProcessManager( + domain=domain, + space_file=plot_dir + '/spaces2.yml', + fields_file=plot_dir + '/fields2.h5') + PM.export_to_vtk( + plot_dir + "/Bh", + grid=None, + npts_per_cell=4, + snapshots='all', + fields='Bh') + PM.close() + + + +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 @ 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 + scheme exactly at its stability limit, which is not safe because of the + unavoidable round-off errors. Hence we require `0 < cfl_max < 1`. + + Optionally the user can provide a maximum time step size in order to + properly resolve some time scales of interest (e.g. a time-dependent + current source). + + Parameters + ---------- + C : LinearOperator + Matrix of the Curl operator. + + dC : LinearOperator + Matrix of the dual Curl operator. + + cfl_max : float + Maximum Courant parameter in the domain, intended as a stability + parameter (=1 at the stability limit). Must be `0 < cfl_max < 1`. + + dt_max : float, optional + If not None, restrict the computed dt by this value in order to + properly resolve time scales of interest. Must be > 0. + + Returns + ------- + dt : float + Largest stable dt which satisfies the provided constraints. + + """ + + print(" .. compute_stable_dt by estimating the operator norm of ") + print(" .. dC_m @ C_m: V1h -> V1h ") + print(" .. with dim(V1h) = {} ...".format(C.domain.dimension)) + + if not (0 < cfl_max < 1): + print(' ****** ****** ****** ****** ****** ****** ') + print(' WARNING !!! cfl = {} '.format(cfl)) + print(' ****** ****** ****** ****** ****** ****** ') + + t_stamp = time_count() + 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 = dC @ C + + while not (conv or ncfl > max_ncfl): + + vv *= (1. / norm_vv) + ncfl += 1 + CC.dot(vv, out=vv) + + norm_vv = np.sqrt(vv.inner(vv)) + 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 @ C ) ~= {}".format(spectral_rho)) + t_stamp = time_count(t_stamp) + + norm_op = np.sqrt(spectral_rho) + c_dt_max = 2. / norm_op + + light_c = 1 + dt = cfl_max * c_dt_max / light_c + + if dt_max is not None: + dt = min(dt, dt_max) + + print(" Time step dt computed for Maxwell solver:") + print(f" Based on cfl_max = {cfl_max} and dt_max = {dt_max}, we set dt = {dt}") + print(f" -- note that c*Dt = {light_c*dt} and c_dt_max = {c_dt_max}, thus c * dt / c_dt_max = {light_c*dt/c_dt_max}") + print(f" -- and spectral_radius((c*dt)**2* dC @ 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 new file mode 100644 index 000000000..e17c70b4a --- /dev/null +++ b/psydac/feec/multipatch/examples/timedomain_maxwell_testcase.py @@ -0,0 +1,154 @@ +""" + Runner script for solving the time-domain Maxwell problem. +""" + +import numpy as np + +from psydac.feec.multipatch.examples.timedomain_maxwell import solve_td_maxwell_pbm +from psydac.feec.multipatch.utilities import get_run_dir, get_plot_dir + +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- +# + +test_case = 'E0_pulse_no_source' +# test_case = 'Issautier_like_source' +# J_proj_case = 'P_geom' +J_proj_case = 'P_L2' + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +# Parameters to be changed in the batch run +deg = 3 + +# Common simulation parameters +# domain_name = 'square_6' +# ncells = [4,4,4,4,4,4] +# domain_name = 'pretzel_f' + +# non-conf domains +domain = [[0, 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([[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_L2' +backend = 'pyccel-gcc' +project_sol = True # whether cP1 E_h is plotted instead of E_h + +# Parameters that depend on test case +if test_case == 'E0_pulse_no_source': + + E0_type = 'pulse_2' # non-zero initial conditions + source_type = 'zero' # no current source + source_omega = None + final_time = 2 # wave transit time in domain is > 4 + dt_max = None + + plot_a_lot = True + if plot_a_lot: + plot_time_ranges = [[[0, final_time], 0.1]] + else: + plot_time_ranges = [ + [[0, 2], 0.1], + [[final_time - 1, final_time], 0.1], + ] + +# TODO: check +elif test_case == 'Issautier_like_source': + + E0_type = 'zero' # zero initial conditions + source_type = 'Il_pulse' + source_omega = None + final_time = 20 + dt_max = None + + if deg == 3 and final_time == 20: + + plot_time_ranges = [ + [[1.9, 2], 0.1], + [[4.9, 5], 0.1], + [[9.9, 10], 0.1], + [[19.9, 20], 0.1], + ] + +else: + raise ValueError(test_case) + + +# projection used for the source J +if J_proj_case == 'P_geom': + source_proj = 'P_geom' + filter_source = False + +elif J_proj_case == 'P_L2': + source_proj = 'P_L2' + filter_source = False + +elif J_proj_case == 'tilde Pi_1': + source_proj = 'P_L2' + filter_source = True + +else: + raise ValueError(J_proj_case) + +case_dir = 'tdmaxwell_' + test_case + '_J_proj=' + J_proj_case + +if filter_source: + case_dir += '_Jfilter' +else: + case_dir += '_Jnofilter' +if not project_sol: + case_dir += '_E_noproj' + +if source_omega is not None: + case_dir += f'_omega={source_omega}' + +case_dir += f'_tend={final_time}' + +# +# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + +run_dir = get_run_dir( + domain_name, + sum(ncells), + deg, + source_type=source_type, + conf_proj="") + +plot_dir = get_plot_dir(case_dir, run_dir) + + +# +params = { + 'nc': ncells, + 'deg': deg, + 'final_time': final_time, + 'cfl_max': cfl_max, + 'dt_max': dt_max, + 'domain_name': domain_name, + 'backend': backend, + 'source_type': source_type, + 'source_omega': source_omega, + 'source_proj': source_proj, + 'project_sol': project_sol, + 'filter_source': filter_source, + 'E0_type': E0_type, + 'E0_proj': E0_proj, + 'plot_dir': plot_dir, + 'plot_time_ranges': plot_time_ranges, + 'domain_lims': domain +} + +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') +print(' Calling solve_td_maxwell_pbm() with params = {}'.format(params)) +print('\n --- --- --- --- --- --- --- --- --- --- --- --- --- --- \n') + +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 new file mode 100644 index 000000000..3986aabb2 --- /dev/null +++ b/psydac/feec/multipatch/tests/test_feec_maxwell_multipatch_2d.py @@ -0,0 +1,196 @@ +# 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 + +def test_time_harmonic_maxwell_pretzel_f(): + nc = 4 + deg = 2 + + source_type = 'manu_maxwell_inhom' + domain_name = 'pretzel_f' + source_proj = 'tilde_Pi' + + omega = np.pi + eta = -omega**2 # source + + diags = solve_hcurl_source_pbm( + nc=nc, deg=deg, + eta=eta, + nu=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + source_proj=source_proj, + backend_language='pyccel-gcc') + + assert abs(diags["err"] - 0.0072015081402929445) < 1e-10 + +def test_time_harmonic_maxwell_pretzel_f_nc(): + deg = 2 + nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, + 4, 4, 4, 4, 8, 8, 8, 4, 4]) + + source_type = 'manu_maxwell_inhom' + domain_name = 'pretzel_f' + source_proj = 'tilde_Pi' + + omega = np.pi + eta = -omega**2 # source + + diags = solve_hcurl_source_pbm( + nc=nc, deg=deg, + eta=eta, + nu=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + source_proj=source_proj, + backend_language='pyccel-gcc') + + assert abs(diags["err"] - 0.004849225522124346) < 5e-7 + +def test_maxwell_eigen_curved_L_shape(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = 4 + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=0, + generalized_pbm=True, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.012915398994855902) < 1e-10 + +def test_maxwell_eigen_curved_L_shape_nc(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = np.array([[None, 4], + [4, 8]]) + + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm( + ncells=ncells, degree=degree, + gamma_h=0, + generalized_pbm=True, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.010504876643886937) < 1e-10 + +def test_maxwell_eigen_curved_L_shape_dg(): + domain_name = 'curved_L_shape' + domain = [[1, 3], [0, np.pi / 4]] + + ncells = np.array([[None, 4], + [4, 8]]) + + degree = [2, 2] + + ref_sigmas = [ + 0.181857115231E+01, + 0.349057623279E+01, + 0.100656015004E+02, + 0.101118862307E+02, + 0.124355372484E+02, + ] + sigma = 7 + nb_eigs_solve = 7 + nb_eigs_plot = 7 + skip_eigs_threshold = 1e-7 + + diags, eigenvalues = hcurl_solve_eigen_pbm_dg( + ncells=ncells, degree=degree, + nu=0, + mu=1, + sigma=sigma, + skip_eigs_threshold=skip_eigs_threshold, + nb_eigs_solve=nb_eigs_solve, + nb_eigs_plot=nb_eigs_plot, + domain_name=domain_name, domain=domain, + backend_language='pyccel-gcc', + ) + + error = 0 + n_errs = min(len(ref_sigmas), len(eigenvalues)) + for k in range(n_errs): + error += (eigenvalues[k] - ref_sigmas[k])**2 + error = np.sqrt(error) + + assert abs(error - 0.035139029534592255) < 1e-10 + +def test_maxwell_timedomain(): + solve_td_maxwell_pbm(nc = 4, deg = 2, final_time = 2, domain_name = 'square_2') + +# ============================================================================== +# CLEAN UP SYMPY NAMESPACE +# ============================================================================== +def teardown_module(): + from sympy.core import cache + cache.clear_cache() + + +def teardown_function(): + from sympy.core import cache + cache.clear_cache() diff --git a/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py new file mode 100644 index 000000000..2804fba2d --- /dev/null +++ b/psydac/feec/multipatch/tests/test_feec_poisson_multipatch_2d.py @@ -0,0 +1,54 @@ +import numpy as np + +from psydac.feec.multipatch.examples.h1_source_pbms_conga_2d import solve_h1_source_pbm + +def test_poisson_pretzel_f(): + + source_type = 'manu_poisson_2' + domain_name = 'pretzel_f' + nc = 4 + deg = 2 + + l2_error = solve_h1_source_pbm( + nc=nc, deg=deg, + eta=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + backend_language='pyccel-gcc', + plot_dir=None) + + assert abs(l2_error - 1.1016888403643595e-05) < 5e-8 + + +def test_poisson_pretzel_f_nc(): + + source_type = 'manu_poisson_2' + domain_name = 'pretzel_f' + nc = np.array([8, 8, 8, 8, 8, 4, 4, 4, 4, + 4, 4, 4, 4, 8, 8, 8, 4, 4]) + deg = 2 + + l2_error = solve_h1_source_pbm( + nc=nc, deg=deg, + eta=0, + mu=1, + domain_name=domain_name, + source_type=source_type, + backend_language='pyccel-gcc', + plot_dir=None) + + assert abs(l2_error - 7.079666478120528e-06) < 5e-8 + + +# ============================================================================== +# CLEAN UP SYMPY NAMESPACE +# ============================================================================== +def teardown_module(): + from sympy.core import cache + cache.clear_cache() + + +def teardown_function(): + from sympy.core import cache + cache.clear_cache() diff --git a/psydac/feec/multipatch/utils_conga_2d.py b/psydac/feec/multipatch/utils_conga_2d.py new file mode 100644 index 000000000..b457f0232 --- /dev/null +++ b/psydac/feec/multipatch/utils_conga_2d.py @@ -0,0 +1,387 @@ +import os +import datetime + +import numpy as np + +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 + +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 +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) +def P0_phys(f_phys, P0, domain): + f = lambdify(domain.coordinates, f_phys) + + return P0(f) + + +def P1_phys(f_phys, P1, domain): + f_x = lambdify(domain.coordinates, f_phys[0]) + f_y = lambdify(domain.coordinates, f_phys[1]) + + return P1([f_x, f_y]) + + +def P2_phys(f_phys, P2, domain): + f = lambdify(domain.coordinates, f_phys) + + return P2(f) + + +def get_kind(space='V*'): + # temp helper + if space == 'V0': + kind = 'h1' + elif space == 'V1': + kind = 'hcurl' + elif space == 'V2': + kind = 'l2' + else: + raise ValueError(space) + return kind + +# =============================================================================== +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(): + """ + Class storing: + - a diagnostic cell-centered grid + - writing / quadrature utilities + - a ref solution + + to compare solutions from different FEM spaces on same domain + """ + + def __init__(self, mappings=None, N_diag=None): + + mappings_list = list(mappings.values()) + etas, xx, yy, patch_logvols = get_plotting_grid( + mappings, N=N_diag, centered_nodes=True, return_patch_logvols=True) + quad_weights = get_grid_quad_weights( + etas, patch_logvols, mappings_list) + + self.etas = etas + self.xx = xx + self.yy = yy + self.patch_logvols = patch_logvols + self.quad_weights = quad_weights + self.mappings_list = mappings_list + + self.sol_ref = {} # Fem fields + self.sol_vals = {} # values on diag grid + self.sol_ref_vals = {} # values on diag grid + + def grid_vals_h1(self, v): + return get_grid_vals(v, self.etas, self.mappings_list, space_kind='h1') + + def grid_vals_hcurl(self, v): + return get_grid_vals( + v, + self.etas, + self.mappings_list, + space_kind='hcurl') + + def create_ref_fem_spaces(self, domain=None, ref_nc=None, ref_deg=None): + print('[DiagGrid] Discretizing the ref FEM space...') + degree = [ref_deg, ref_deg] + derham = Derham(domain, ["H1", "Hcurl", "L2"]) + ref_nc = {patch.name: [ref_nc, ref_nc] for patch in domain.interior} + + domain_h = discretize(domain, ncells=ref_nc) + # , backend=PSYDAC_BACKENDS[backend_language]) + derham_h = discretize(derham, domain_h, degree=degree) + self.V0h = derham_h.V0 + self.V1h = derham_h.V1 + + def import_ref_sol_from_coeffs(self, sol_ref_filename=None, space='V*'): + print('[DiagGrid] loading coeffs of ref_sol from {}...'.format( + sol_ref_filename)) + if space == 'V0': + Vh = self.V0h + elif space == 'V1': + Vh = self.V1h + else: + raise ValueError(space) + try: + coeffs = np.load(sol_ref_filename) + except OSError: + print("-- WARNING: file not found, setting sol_ref = 0") + coeffs = np.zeros(Vh.nbasis) + if space in self.sol_ref: + print( + 'WARNING !! sol_ref[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + self.sol_ref[space] = FemField( + Vh, coeffs=array_to_psydac( + coeffs, Vh.coeff_space)) + + def write_sol_values(self, v, space='V*'): + """ + v: FEM field + """ + if space in self.sol_vals: + print( + 'WARNING !! sol_vals[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + self.sol_vals[space] = get_grid_vals( + v, self.etas, self.mappings_list, space_kind=get_kind(space)) + + def write_sol_ref_values(self, v=None, space='V*'): + """ + if no FemField v is provided, then use the self.sol_ref (must have been imported) + """ + if space in self.sol_vals: + print( + 'WARNING !! sol_ref_vals[{}] exists -- will be overwritten !! '.format(space)) + print('use refined labels if several solutions are needed in the same space') + if v is None: + # then sol_ref must have been imported + v = self.sol_ref[space] + self.sol_ref_vals[space] = get_grid_vals( + v, self.etas, self.mappings_list, space_kind=get_kind(space)) + + def compute_l2_error(self, space='V*'): + if space in ['V0', 'V2']: + u = self.sol_ref_vals[space] + uh = self.sol_vals[space] + abs_u = [np.abs(p) for p in u] + abs_uh = [np.abs(p) for p in uh] + errors = [np.abs(p - q) for p, q in zip(u, uh)] + elif space == 'V1': + u_x, u_y = self.sol_ref_vals[space] + uh_x, uh_y = self.sol_vals[space] + abs_u = [np.sqrt((u1)**2 + (u2)**2) for u1, u2 in zip(u_x, u_y)] + abs_uh = [np.sqrt((u1)**2 + (u2)**2) for u1, u2 in zip(uh_x, uh_y)] + errors = [np.sqrt((u1 - v1)**2 + (u2 - v2)**2) + for u1, v1, u2, v2 in zip(u_x, uh_x, u_y, uh_y)] + else: + raise ValueError(space) + + l2_norm_uh = ( + np.sum([J_F * v**2 for v, J_F in zip(abs_uh, self.quad_weights)]))**0.5 + l2_norm_u = ( + np.sum([J_F * v**2 for v, J_F in zip(abs_u, self.quad_weights)]))**0.5 + l2_error = ( + np.sum([J_F * v**2 for v, J_F in zip(errors, self.quad_weights)]))**0.5 + + return l2_norm_uh, l2_norm_u, l2_error + + def get_diags_for(self, v, space='V*', print_diags=True): + self.write_sol_values(v, space) + sol_norm, sol_ref_norm, l2_error = self.compute_l2_error(space) + rel_l2_error = l2_error / (max(sol_norm, sol_ref_norm)) + diags = { + 'sol_norm': sol_norm, + 'sol_ref_norm': sol_ref_norm, + 'rel_l2_error': rel_l2_error, + } + if print_diags: + print(' .. l2 norms (computed via quadratures on diag_grid): ') + print(diags) + + return diags + + +def get_Vh_diags_for( + v=None, + v_ref=None, + M_m=None, + print_diags=True, + msg='error between ?? and ?? in Vh'): + """ + v, v_ref: FemField + M_m: mass matrix in scipy format + """ + uh_c = v.coeffs.toarray() + uh_ref_c = v_ref.coeffs.toarray() + err_c = uh_c - uh_ref_c + l2_error = np.dot(err_c, M_m.dot(err_c))**0.5 + sol_norm = np.dot(uh_c, M_m.dot(uh_c))**0.5 + sol_ref_norm = np.dot(uh_ref_c, M_m.dot(uh_ref_c))**0.5 + rel_l2_error = l2_error / (max(sol_norm, sol_ref_norm)) + diags = { + 'sol_norm': sol_norm, + 'sol_ref_norm': sol_ref_norm, + 'rel_l2_error': rel_l2_error, + } + if print_diags: + print(' .. l2 norms ({}): '.format(msg)) + print(diags) + + return diags + + +def write_diags_to_file(diags, script_filename, diag_filename, params=None): + """ write diagnostics to file """ + print(' -- writing diags to file {} --'.format(diag_filename)) + if not os.path.exists(diag_filename): + open(diag_filename, 'w') + + with open(diag_filename, 'a') as a_writer: + a_writer.write('\n') + a_writer.write( + ' ---------- ---------- ---------- ---------- ---------- ---------- \n') + a_writer.write(' run script: \n {}\n'.format(script_filename)) + a_writer.write( + ' executed on: \n {}\n\n'.format( + datetime.datetime.now())) + a_writer.write(' params: \n') + for key, value in params.items(): + a_writer.write(' {}: {} \n'.format(key, value)) + a_writer.write('\n') + a_writer.write(' diags: \n') + for key, value in diags.items(): + a_writer.write(' {}: {} \n'.format(key, value)) + a_writer.write( + ' ---------- ---------- ---------- ---------- ---------- ---------- \n') + a_writer.write('\n') diff --git a/psydac/feec/tests/test_axis_projection.py b/psydac/feec/tests/test_axis_projection.py new file mode 100644 index 000000000..7a81f0953 --- /dev/null +++ b/psydac/feec/tests/test_axis_projection.py @@ -0,0 +1,23 @@ +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(): + domain=Square('OmegaLog', bounds1=(0,1), bounds2 = (0,1)) + derham = Derham(domain, ["H1", "Hdiv", "L2"]) + domain_h = discretize(domain, ncells=[4,4], periodic=[True,True]) + derham_h = discretize(derham, domain_h, degree=(2,2)) + V1h = derham_h.V1 + V2h = derham_h.V2 + u = element_of(V1h.symbolic_space, name='u') + f = element_of(V2h.symbolic_space, name='f') + expr = u[0]*f + Pei = BilinearForm((u,f), integral(domain, expr)) + pei = discretize(Pei, domain_h, (V1h,V2h), backend=PSYDAC_BACKENDS['python']) + Peih = pei.assemble() + uh = V1h.coeff_space.zeros() + test = Peih.dot(uh) + +if __name__ == '__main__': + test_axis_projection() diff --git a/psydac/feec/tests/test_commuting_projections.py b/psydac/feec/tests/test_commuting_projections.py new file mode 100644 index 000000000..b038fbc25 --- /dev/null +++ b/psydac/feec/tests/test_commuting_projections.py @@ -0,0 +1,668 @@ +# -*- coding: UTF-8 -*- +from mpi4py import MPI +import numpy as np +import pytest + +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 +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 + +#============================================================================== +# 3D tests +#============================================================================== +@pytest.mark.parametrize('m', [1, 2]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('Nq', [5]) +@pytest.mark.parametrize('Nel', [5, 6]) +def test_3d_commuting_pro_1(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.sin(xi2)*np.sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : np.cos(xi1)*np.sin(xi2)*np.sin(xi3) + D2fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.cos(xi2)*np.sin(xi3) + D3fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.sin(xi2)*np.cos(xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[0], basis='M'), + H1.reduce_degree(axes=[1], basis='M'), + H1.reduce_degree(axes=[2], basis='M')] + + Hcurl = VectorFemSpace(*spaces) + + # create an instance of the H1 projector class + P0 = GlobalGeometricProjectorH1(H1) + + # Build linear operators on stencil arrays + grad = Gradient3D(H1, Hcurl) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u0 = P0(fun1) + u1 = P1((D1fun1, D2fun1, D3fun1)) + Dfun_h = grad(u0) + Dfun_proj = u1 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + Id_0 = IdentityOperator(H1.coeff_space) + Err_0 = P0.solver @ P0.imat_kronecker - Id_0 + e0 = Err_0 @ u0.coeffs # random vector could be used as well + norm2_e0 = np.sqrt(e0.inner(e0)) + assert norm2_e0 < 1e-12 + + Id_1 = IdentityOperator(Hcurl.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs # random vector could be used as well + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e1 < 1e-12 + +@pytest.mark.parametrize('m', [1, 2]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('Nq', [7]) +@pytest.mark.parametrize('Nel', [5, 6]) +def test_3d_commuting_pro_2(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.sin(xi2)*np.sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : np.cos(xi1)*np.sin(xi2)*np.sin(xi3) + D2fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.cos(xi2)*np.sin(xi3) + D3fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.sin(xi2)*np.cos(xi3) + + fun2 = lambda xi1, xi2, xi3 : np.sin(2*xi1)*np.sin(2*xi2)*np.sin(2*xi3) + D1fun2 = lambda xi1, xi2, xi3 : 2*np.cos(2*xi1)*np.sin(2*xi2)*np.sin(2*xi3) + D2fun2 = lambda xi1, xi2, xi3 : 2*np.sin(2*xi1)*np.cos(2*xi2)*np.sin(2*xi3) + D3fun2 = lambda xi1, xi2, xi3 : 2*np.sin(2*xi1)*np.sin(2*xi2)*np.cos(2*xi3) + + fun3 = lambda xi1, xi2, xi3 : np.sin(3*xi1)*np.sin(3*xi2)*np.sin(3*xi3) + D1fun3 = lambda xi1, xi2, xi3 : 3*np.cos(3*xi1)*np.sin(3*xi2)*np.sin(3*xi3) + D2fun3 = lambda xi1, xi2, xi3 : 3*np.sin(3*xi1)*np.cos(3*xi2)*np.sin(3*xi3) + D3fun3 = lambda xi1, xi2, xi3 : 3*np.sin(3*xi1)*np.sin(3*xi2)*np.cos(3*xi3) + + cf1 = lambda xi1, xi2, xi3 : D2fun3(xi1, xi2, xi3) - D3fun2(xi1, xi2, xi3) + cf2 = lambda xi1, xi2, xi3 : D3fun1(xi1, xi2, xi3) - D1fun3(xi1, xi2, xi3) + cf3 = lambda xi1, xi2, xi3 : D1fun2(xi1, xi2, xi3) - D2fun1(xi1, xi2, xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[0], basis='M'), + H1.reduce_degree(axes=[1], basis='M'), + H1.reduce_degree(axes=[2], basis='M')] + + Hcurl = VectorFemSpace(*spaces) + + spaces = [H1.reduce_degree(axes=[1,2], basis='M'), + H1.reduce_degree(axes=[0,2], basis='M'), + H1.reduce_degree(axes=[0,1], basis='M')] + + Hdiv = VectorFemSpace(*spaces) + + # Build linear operators on stencil arrays + curl = Curl3D(Hcurl, Hdiv) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + u1 = P1((fun1, fun2, fun3)) + u2 = P2((cf1, cf2, cf3)) + Dfun_h = curl(u1) + Dfun_proj = u2 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_1 = IdentityOperator(Hcurl.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs # random vector could be used as well + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e1 < 1e-12 + + Id_2 = IdentityOperator(Hdiv.coeff_space) + Err_2 = P2.solver @ P2.imat_kronecker - Id_2 + e2 = Err_2 @ u2.coeffs # random vector could be used as well + norm2_e2 = np.sqrt(e2.inner(e2)) + assert norm2_e2 < 1e-12 + +@pytest.mark.parametrize('m', [1, 2]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('Nq', [7]) +@pytest.mark.parametrize('Nel', [5, 6]) +def test_3d_commuting_pro_3(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2, xi3 : np.sin(xi1)*np.sin(xi2)*np.sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : np.cos(xi1)*np.sin(xi2)*np.sin(xi3) + + fun2 = lambda xi1, xi2, xi3 : np.sin(2*xi1)*np.sin(2*xi2)*np.sin(2*xi3) + D2fun2 = lambda xi1, xi2, xi3 : 2*np.sin(2*xi1)*np.cos(2*xi2)*np.sin(2*xi3) + + fun3 = lambda xi1, xi2, xi3 : np.sin(3*xi1)*np.sin(3*xi2)*np.sin(3*xi3) + D3fun3 = lambda xi1, xi2, xi3 : 3*np.sin(3*xi1)*np.sin(3*xi2)*np.cos(3*xi3) + + difun = lambda xi1, xi2, xi3 : D1fun1(xi1, xi2, xi3)+ D2fun2(xi1, xi2, xi3) + D3fun3(xi1, xi2, xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[1,2], basis='M'), + H1.reduce_degree(axes=[0,2], basis='M'), + H1.reduce_degree(axes=[0,1], basis='M')] + + Hdiv = VectorFemSpace(*spaces) + + L2 = H1.reduce_degree(axes=[0,1,2], basis='M') + + # create an instance of the H1 projector class + + # Build linear operators on stencil arrays + div = Divergence3D(Hdiv, L2) + + # create an instance of the projector class + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) + + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u2 = P2((fun1, fun2, fun3)) + u3 = P3(difun) + Dfun_h = div(u2) + Dfun_proj = u3 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_2 = IdentityOperator(Hdiv.coeff_space) + Err_2 = P2.solver @ P2.imat_kronecker - Id_2 + e2 = Err_2 @ u2.coeffs # random vector could be used as well + norm2_e2 = np.sqrt(e2.inner(e2)) + assert norm2_e2 < 1e-12 + + Id_3 = IdentityOperator(L2.coeff_space) + Err_3 = P3.solver @ P3.imat_kronecker - Id_3 + e3 = Err_3 @ u3.coeffs # random vector could be used as well + norm2_e3 = np.sqrt(e3.inner(e3)) + assert norm2_e3 < 1e-12 + +#============================================================================== +# 2D tests +#============================================================================== +@pytest.mark.parallel +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [5]) +@pytest.mark.parametrize('p', [2,3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_2d_commuting_pro_1(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2 : np.sin(xi1)*np.sin(xi2) + D1fun1 = lambda xi1, xi2 : np.cos(xi1)*np.sin(xi2) + D2fun1 = lambda xi1, xi2 : np.sin(xi1)*np.cos(xi2) + + Nel = [Nel]*2 + Nq = [Nq]*2 + p = [p]*2 + bc = [bc]*2 + m = [m]*2 + + # Side lengths of logical cube [0, L]^2 + L = [2*np.pi, 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[0], basis='M'), + H1.reduce_degree(axes=[1], basis='M')] + + Hcurl = VectorFemSpace(*spaces) + + # create an instance of the H1 projector class + P0 = GlobalGeometricProjectorH1(H1) + + # Build linear operators on stencil arrays + grad = Gradient2D(H1, Hcurl) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u0 = P0(fun1) + u1 = P1((D1fun1, D2fun1)) + Dfun_h = grad(u0) + Dfun_proj = u1 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_0 = IdentityOperator(H1.coeff_space) + Err_0 = P0.solver @ P0.imat_kronecker - Id_0 + e0 = Err_0 @ u0.coeffs # random vector could be used as well + norm2_e0 = np.sqrt(e0.inner(e0)) + assert norm2_e0 < 1e-12 + + Id_1 = IdentityOperator(Hcurl.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs # random vector could be used as well + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e1 < 1e-12 + +@pytest.mark.parallel +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [5]) +@pytest.mark.parametrize('p', [2,3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_2d_commuting_pro_2(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2 : np.sin(xi1)*np.sin(xi2) + D2fun1 = lambda xi1, xi2 : np.sin(xi1)*np.cos(xi2) + D1fun1 = lambda xi1, xi2 : -np.cos(xi1)*np.sin(xi2) + + Nel = [Nel]*2 + Nq = [Nq]*2 + p = [p]*2 + bc = [bc]*2 + m = [m]*2 + + # Side lengths of logical cube [0, L]^2 + L = [2*np.pi, 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[1], basis='M'), + H1.reduce_degree(axes=[0], basis='M')] + + Hdiv = VectorFemSpace(*spaces) + + # create an instance of the H1 projector class + P0 = GlobalGeometricProjectorH1(H1) + + # Linear operator: 2D vector curl + curl = VectorCurl2D(H1, Hdiv) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u0 = P0(fun1) + u1 = P1((D2fun1, D1fun1)) + Dfun_h = curl(u0) + Dfun_proj = u1 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_0 = IdentityOperator(H1.coeff_space) + Err_0 = P0.solver @ P0.imat_kronecker - Id_0 + e0 = Err_0 @ u0.coeffs # random vector could be used as well + norm2_e0 = np.sqrt(e0.inner(e0)) + assert norm2_e0 < 1e-12 + + Id_1 = IdentityOperator(Hdiv.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs # random vector could be used as well + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e0 < 1e-12 + +@pytest.mark.parallel +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [8]) +@pytest.mark.parametrize('p', [2,3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_2d_commuting_pro_3(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2 : np.sin(xi1)*np.sin(xi2) + D1fun1 = lambda xi1, xi2 : np.cos(xi1)*np.sin(xi2) + + fun2 = lambda xi1, xi2 : np.sin(2*xi1)*np.sin(2*xi2) + D2fun2 = lambda xi1, xi2 : 2*np.sin(2*xi1)*np.cos(2*xi2) + + difun = lambda xi1, xi2 : D1fun1(xi1, xi2)+ D2fun2(xi1, xi2) + + Nel = [Nel]*2 + Nq = [Nq]*2 + p = [p]*2 + bc = [bc]*2 + m = [m]*2 + + # Side lengths of logical cube [0, L]^2 + L = [2*np.pi, 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[1], basis='M'), + H1.reduce_degree(axes=[0], basis='M')] + + Hdiv = VectorFemSpace(*spaces) + + L2 = H1.reduce_degree(axes=[0,1], basis='M') + + # create an instance of the H1 projector class + + # Build linear operators on stencil arrays + div = Divergence2D(Hdiv, L2) + + # create an instance of the projector class + P2 = GlobalGeometricProjectorHdiv(Hdiv, Nq) + P3 = GlobalGeometricProjectorL2(L2, Nq) + + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u2 = P2((fun1, fun2)) + u3 = P3(difun) + Dfun_h = div(u2) + Dfun_proj = u3 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_2 = IdentityOperator(Hdiv.coeff_space) + Err_2 = P2.solver @ P2.imat_kronecker - Id_2 + e2 = Err_2 @ u2.coeffs + norm2_e2 = np.sqrt(e2.inner(e2)) + assert norm2_e2 < 1e-12 + + Id_3 = IdentityOperator(L2.coeff_space) + Err_3 = P3.solver @ P3.imat_kronecker - Id_3 + e3 = Err_3 @ u3.coeffs + norm2_e3 = np.sqrt(e3.inner(e3)) + assert norm2_e3 < 1e-12 + +@pytest.mark.parallel +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [8]) +@pytest.mark.parametrize('p', [2,3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_2d_commuting_pro_4(Nel, Nq, p, bc, m): + + fun1 = lambda xi1, xi2 : np.sin(xi1)*np.sin(xi2) + D2fun1 = lambda xi1, xi2 : np.sin(xi1)*np.cos(xi2) + + fun2 = lambda xi1, xi2 : np.sin(2*xi1)*np.sin(2*xi2) + D1fun2 = lambda xi1, xi2 : 2*np.cos(2*xi1)*np.sin(2*xi2) + + difun = lambda xi1, xi2 : D1fun2(xi1, xi2) - D2fun1(xi1, xi2) + + Nel = [Nel]*2 + Nq = [Nq]*2 + p = [p]*2 + bc = [bc]*2 + m = [m]*2 + + # Side lengths of logical cube [0, L]^2 + L = [2*np.pi, 2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + + spaces = [H1.reduce_degree(axes=[0], basis='M'), + H1.reduce_degree(axes=[1], basis='M')] + + Hcurl = VectorFemSpace(*spaces) + + L2 = H1.reduce_degree(axes=[0,1], basis='M') + + # create an instance of the H1 projector class + + # Build linear operators on stencil arrays + curl = ScalarCurl2D(Hcurl, L2) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorHcurl(Hcurl, Nq) + P2 = GlobalGeometricProjectorL2(L2, Nq) + + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u1 = P1((fun1, fun2)) + u2 = P2(difun) + Dfun_h = curl(u1) + Dfun_proj = u2 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_1 = IdentityOperator(Hcurl.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e1 < 1e-12 + + Id_2 = IdentityOperator(L2.coeff_space) + Err_2 = P2.solver @ P2.imat_kronecker - Id_2 + e2 = Err_2 @ u2.coeffs + norm2_e2 = np.sqrt(e2.inner(e2)) + assert norm2_e2 < 1e-12 + +#============================================================================== +# 1D tests +#============================================================================== +@pytest.mark.parametrize('Nel', [16, 20]) +@pytest.mark.parametrize('Nq', [5]) +@pytest.mark.parametrize('p', [2,3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_1d_commuting_pro_1(Nel, Nq, p, bc, m): + + fun1 = lambda xi1 : np.sin(xi1) + Dfun1 = lambda xi1 : np.cos(xi1) + + Nel = [Nel] + Nq = [Nq] + p = [p] + bc = [bc] + m = [m] + + # Side lengths of logical cube [0, L] + L = [2*np.pi] + + # element boundaries + el_b = [np.linspace(0., L_i, Nel_i + 1) for L_i, Nel_i in zip(L, Nel)] + + # knot sequences + knots = [make_knots(el_b_i, p_i, bc_i, m_i) for el_b_i, p_i, bc_i, m_i in zip(el_b, p, bc, m)] + + Vs = [SplineSpace(pi, knots=Ti, periodic=periodic, basis='B') for pi, Ti, periodic in zip(p, knots, bc)] + + domain_decomposition = DomainDecomposition(Nel, bc, comm=MPI.COMM_WORLD) + H1 = TensorFemSpace(domain_decomposition, *Vs) + L2 = H1.reduce_degree(axes=[0], basis='M') + + # create an instance of the H1 projector class + P0 = GlobalGeometricProjectorH1(H1) + + # Build linear operators on stencil arrays + grad = Derivative1D(H1, L2) + + # create an instance of the projector class + P1 = GlobalGeometricProjectorL2(L2, Nq) + #------------------------------------- + # Projections and discrete derivatives + #------------------------------------- + + u0 = P0(fun1) + u1 = P1(Dfun1) + Dfun_h = grad(u0) + Dfun_proj = u1 + + error = abs((Dfun_proj.coeffs-Dfun_h.coeffs).toarray()).max() + assert error < 1e-9 + + #-------------------------- + # check BlockLinearOperator + #-------------------------- + + Id_0 = IdentityOperator(H1.coeff_space) + Err_0 = P0.solver @ P0.imat_kronecker - Id_0 + e0 = Err_0 @ u0.coeffs + norm2_e0 = np.sqrt(e0.inner(e0)) + assert norm2_e0 < 1e-12 + + Id_1 = IdentityOperator(L2.coeff_space) + Err_1 = P1.solver @ P1.imat_kronecker - Id_1 + e1 = Err_1 @ u1.coeffs + norm2_e1 = np.sqrt(e1.inner(e1)) + assert norm2_e1 < 1e-12 + +#============================================================================== +if __name__ == '__main__': + + Nel = 8 + Nq = 8 + p = 2 + bc = True + m = 2 + + test_3d_commuting_pro_1(Nel, Nq, p, bc, m) + test_3d_commuting_pro_2(Nel, Nq, p, bc, m) + test_3d_commuting_pro_3(Nel, Nq, p, bc, m) + test_2d_commuting_pro_1(Nel, Nq, p, bc, m) + test_2d_commuting_pro_2(Nel, Nq, p, bc, m) + test_2d_commuting_pro_3(Nel, Nq, p, bc, m) + test_2d_commuting_pro_4(Nel, Nq, p, bc, m) + test_1d_commuting_pro_1(Nel, Nq, p, bc, m) diff --git a/psydac/feec/tests/test_commuting_projections_dual.py b/psydac/feec/tests/test_commuting_projections_dual.py new file mode 100644 index 000000000..9057ba1ff --- /dev/null +++ b/psydac/feec/tests/test_commuting_projections_dual.py @@ -0,0 +1,172 @@ +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 +from psydac.api.settings import PSYDAC_BACKENDS +from sympy import sin, cos +import numpy as np +import pytest + + +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [5]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_transpose_div_3d(Nel, Nq, p, bc, m): + # Test transpose div + + fun1 = lambda xi1, xi2, xi3 : sin(xi1)*sin(xi2)*sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : cos(xi1)*sin(xi2)*sin(xi3) + D2fun1 = lambda xi1, xi2, xi3 : sin(xi1)*cos(xi2)*sin(xi3) + D3fun1 = lambda xi1, xi2, xi3 : sin(xi1)*sin(xi2)*cos(xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + domain = Cube('domain', bounds1=(0, L[0]), bounds2=(0,L[1]), bounds3=(0,L[2])) + derham = Derham(domain) + domain_h = discretize(domain, ncells=Nel, periodic=bc) + derham_h = discretize(derham, domain_h, degree=p, multiplicity=m) + + v2 = element_of(derham.V2, name='v2') + v3 = element_of(derham.V3, name='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)) + + 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.linop.T.dot(u3) + + error = abs((u2-divT_u3).toarray()).max() + assert error < 2e-10 + + +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [6]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_transpose_curl_3d(Nel, Nq, p, bc, m): + # Test transpose curl + + fun1 = lambda xi1, xi2, xi3 : sin(xi1)*sin(xi2)*sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : cos(xi1)*sin(xi2)*sin(xi3) + D2fun1 = lambda xi1, xi2, xi3 : sin(xi1)*cos(xi2)*sin(xi3) + D3fun1 = lambda xi1, xi2, xi3 : sin(xi1)*sin(xi2)*cos(xi3) + + fun2 = lambda xi1, xi2, xi3 : sin(2*xi1)*sin(2*xi2)*sin(2*xi3) + D1fun2 = lambda xi1, xi2, xi3 : 2*cos(2*xi1)*sin(2*xi2)*sin(2*xi3) + D2fun2 = lambda xi1, xi2, xi3 : 2*sin(2*xi1)*cos(2*xi2)*sin(2*xi3) + D3fun2 = lambda xi1, xi2, xi3 : 2*sin(2*xi1)*sin(2*xi2)*cos(2*xi3) + + fun3 = lambda xi1, xi2, xi3 : sin(3*xi1)*sin(3*xi2)*sin(3*xi3) + D1fun3 = lambda xi1, xi2, xi3 : 3*cos(3*xi1)*sin(3*xi2)*sin(3*xi3) + D2fun3 = lambda xi1, xi2, xi3 : 3*sin(3*xi1)*cos(3*xi2)*sin(3*xi3) + D3fun3 = lambda xi1, xi2, xi3 : 3*sin(3*xi1)*sin(3*xi2)*cos(3*xi3) + + #curl + cf1 = lambda xi1, xi2, xi3 : D2fun3(xi1, xi2, xi3) - D3fun2(xi1, xi2, xi3) + cf2 = lambda xi1, xi2, xi3 : D3fun1(xi1, xi2, xi3) - D1fun3(xi1, xi2, xi3) + cf3 = lambda xi1, xi2, xi3 : D1fun2(xi1, xi2, xi3) - D2fun1(xi1, xi2, xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + domain = Cube('domain', bounds1=(0, L[0]), bounds2=(0,L[1]), bounds3=(0,L[2])) + derham = Derham(domain) + domain_h = discretize(domain, ncells=Nel, periodic=bc) + derham_h = discretize(derham, domain_h, degree=p, multiplicity=m) + + v1 = element_of(derham.V1, name='v1') + v2 = element_of(derham.V2, name='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])) + + 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.linop.T.dot(u2) + + error = abs((u1-curlT_u2).toarray()).max() + assert error < 2e-9 + + +@pytest.mark.parametrize('Nel', [8, 12]) +@pytest.mark.parametrize('Nq', [6]) +@pytest.mark.parametrize('p', [2, 3]) +@pytest.mark.parametrize('bc', [True, False]) +@pytest.mark.parametrize('m', [1,2]) +def test_transpose_grad_3d(Nel, Nq, p, bc, m): + # Test transpose grad + + fun1 = lambda xi1, xi2, xi3 : sin(xi1)*sin(xi2)*sin(xi3) + D1fun1 = lambda xi1, xi2, xi3 : cos(xi1)*sin(xi2)*sin(xi3) + + fun2 = lambda xi1, xi2, xi3 : sin(2*xi1)*sin(2*xi2)*sin(2*xi3) + D2fun2 = lambda xi1, xi2, xi3 : 2*sin(2*xi1)*cos(2*xi2)*sin(2*xi3) + + fun3 = lambda xi1, xi2, xi3 : sin(3*xi1)*sin(3*xi2)*sin(3*xi3) + D3fun3 = lambda xi1, xi2, xi3 : 3*sin(3*xi1)*sin(3*xi2)*cos(3*xi3) + + Nel = [Nel]*3 + Nq = [Nq]*3 + p = [p]*3 + bc = [bc]*3 + m = [m]*3 + + # Side lengths of logical cube [0, L]^3 + L = [2*np.pi, 2*np.pi , 2*np.pi] + + domain = Cube('domain', bounds1=(0, L[0]), bounds2=(0,L[1]), bounds3=(0,L[2])) + derham = Derham(domain) + domain_h = discretize(domain, ncells=Nel, periodic=bc) + derham_h = discretize(derham, domain_h, degree=p, multiplicity=m) + + v0 = element_of(derham.V0, name='v0') + v1 = element_of(derham.V1, name='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])) + + 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.linop.T.dot(u1) + + error = abs((u0-gradT_u1).toarray()).max() + assert error < 5e-10 + + +#============================================================================== +if __name__ == '__main__': + + Nel = 8 + Nq = 8 + p = 2 + bc = True + m = 2 + + test_transpose_div_3d (Nel, Nq, p, bc, m) + test_transpose_curl_3d(Nel, Nq, p, bc, m) + test_transpose_grad_3d(Nel, Nq, p, bc, m) diff --git a/psydac/feec/tests/test_differentiation_matrices.py b/psydac/feec/tests/test_differentiation_matrices.py new file mode 100644 index 000000000..609799dd3 --- /dev/null +++ b/psydac/feec/tests/test_differentiation_matrices.py @@ -0,0 +1,974 @@ +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.fem.vector import VectorFemSpace + +from psydac.feec.derivatives import DirectionalDerivativeOperator +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 mpi4py import MPI + +#============================================================================== + +# these tests test the DirectionalDerivativeOperator structurally. +# They do not check, if it really computes the derivatives +# (this is done in the gradient etc. tests below already) +def run_directional_derivative_operator(comm, domain, ncells, degree, periodic, direction, negative, transposed, seed, matrix_assembly=False): + + if not all([not periodic[i] or (ncells[i] >= degree[i]) for i in range(len(degree)) ]): + return + + # assemble matrix when 1 cell in each direction + matrix_assembly = (ncells == (1,1,1)) + + # determinize tests + np.random.seed(seed) + + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + Ns = [SplineSpace(degree=d, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + + domain_decomposition = DomainDecomposition(ncells, periodic, comm=comm) + # original space + V0 = TensorFemSpace(domain_decomposition, *Ns) + + # reduced space + V1 = V0.reduce_degree(axes=[direction], basis='M') + + diffop = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=negative, transposed=transposed) + + # some boundary, and transposed handling + vpads = np.array(V0.coeff_space.pads, dtype=int) + pads = np.array(V1.coeff_space.pads, dtype=int) + + # transpose, if needed + if transposed: + V0, V1 = V1, V0 + + counts = np.array(V1.coeff_space.ends, dtype=int) - np.array(V1.coeff_space.starts, dtype=int) + 1 + diffadd = np.zeros((len(ncells),), dtype=int) + diffadd[direction] = 1 + + localslice = tuple([slice(p,-p) for p in V1.coeff_space.pads]) + + # random vector, scaled-up data (with fixed seed) + v = V0.coeff_space.zeros() + v._data[:] = np.random.random(v._data.shape) * 100 + v.update_ghost_regions() + + # compute reference solution (do it element-wise for now...) + # (but we only test small domains here) + ref = V1.coeff_space.zeros() + + outslice = tuple([slice(s, s+c) for s,c in zip(pads, counts)]) + idslice = tuple([slice(s, s+c) for s,c in zip(vpads, counts)]) + diffslice = tuple([slice(s+d, s+c+d) for s,c,d in zip(vpads, counts, diffadd)]) + if transposed: + ref._data[idslice] -= v._data[outslice] + ref._data[diffslice] += v._data[outslice] + + # we need to account for the ghost region write which diffslice does, + # i.e. the part which might be sent to another process, or even swapped to the other side + # since the ghost layers of v are updated, we update the data on the other side + # (also update_ghost_regions won't preserve the data we wrote there) + ref_restslice = [c for c in idslice] + ref_restslice[direction] = slice(vpads[direction], vpads[direction] + 1) + v_restslice = [c for c in outslice] + v_restslice[direction] = slice(pads[direction] - 1, pads[direction]) + ref._data[tuple(ref_restslice)] += v._data[tuple(v_restslice)] + else: + ref._data[outslice] = v._data[diffslice] - v._data[idslice] + if negative: + ref._data[localslice] = -ref._data[localslice] + ref.update_ghost_regions() + + # compute and compare + + # case one: dot(v, out=None) + res1 = diffop.dot(v) + assert np.allclose(ref._data[localslice], res1._data[localslice]) + + # case two: dot(v, out=w) + out = V1.coeff_space.zeros() + res2 = diffop.dot(v, out=out) + + assert res2 is out + assert np.allclose(ref._data[localslice], res2._data[localslice]) + + # flag to skip matrix assembly if it takes too long or fails + if matrix_assembly: + # case three: tokronstencil().tostencil().dot(v) + # (i.e. test matrix conversion) + matrix = diffop.tokronstencil().tostencil() + res3 = matrix.dot(v) + assert np.allclose(ref._data[localslice], res3._data[localslice]) + + # compare matrix assembly (in non-parallel case at least) + if not diffop.domain.parallel: + assert np.array_equal(diffop.toarray(), matrix.toarray()) + + # case four: tosparse().dot(v._data) + res4 = diffop.tosparse(with_pads=True).dot(v._data.flatten()) + assert np.allclose(ref._data[localslice], res4.reshape(ref._data.shape)[localslice]) + +def compare_diff_operators_by_matrixassembly(lo1, lo2): + m1 = lo1.tokronstencil().tostencil() + m2 = lo2.tokronstencil().tostencil() + m1.update_ghost_regions() + m2.update_ghost_regions() + assert np.allclose(m1._data, m2._data) + +def test_directional_derivative_operator_invalid_wrongsized1(): + # test if we detect incorrectly-sized spaces + # i.e. V0.coeff_space.npts != V1.coeff_space.npts + # (NOTE: if periodic was [True,True], this test would most likely pass) + + periodic = [False, False] + domain = [(0,1),(0,1)] + ncells = [8, 8] + degree = [3, 3] + direction = 0 + negative = False + + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + Ns = [SplineSpace(degree=d, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + + domain_decomposition = DomainDecomposition(ncells, periodic) + # original space + V0 = TensorFemSpace(domain_decomposition, *Ns) + + # reduced space + V1 = V0.reduce_degree(axes=[1], basis='M') + + with pytest.raises(AssertionError): + _ = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=negative) + +def test_directional_derivative_operator_invalid_wrongspace2(): + # test, if it fails when the pads are not the same + periodic = [False, False] + domain = [(0,1),(0,1)] + ncells = [8, 8] + degree = [3, 3] + direction = 0 + negative = False + + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + Ns = [SplineSpace(degree=d, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + Ms = [SplineSpace(degree=d-1, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + + domain_decomposition = DomainDecomposition(ncells, periodic) + # original space + V0 = TensorFemSpace(domain_decomposition, *Ns) + + # reduced space + V1 = TensorFemSpace(domain_decomposition, *Ms) + + with pytest.raises(AssertionError): + _ = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=negative) + +def test_directional_derivative_operator_transposition_correctness(): + # interface tests, to see if negation and transposition work as their methods suggest + + periodic = [False, False] + domain = [(0,1),(0,1)] + ncells = [8, 8] + degree = [3, 3] + direction = 0 + + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + Ns = [SplineSpace(degree=d, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + + domain_decomposition = DomainDecomposition(ncells, periodic) + # original space + V0 = TensorFemSpace(domain_decomposition, *Ns) + + # reduced space + V1 = V0.reduce_degree(axes=[0], basis='M') + + diff = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=False, transposed=False) + + # compare, if the transpose is actually correct + M = diff.tokronstencil().tostencil() + MT = diff.T.tokronstencil().tostencil() + assert np.allclose(M.T._data, MT._data) + assert np.allclose(M._data, MT.T._data) + + sparseM = diff.tosparse().tocoo() + sparseMT = diff.T.tosparse().tocoo() + + sparseM_T = sparseM.T.tocoo() + sparseMT_T = sparseMT.T.tocoo() + + assert np.array_equal( sparseMT.col , sparseM_T.col ) + assert np.array_equal( sparseMT.row , sparseM_T.row ) + assert np.array_equal( sparseMT.data, sparseM_T.data ) + assert np.array_equal( sparseM.col , sparseMT_T.col ) + assert np.array_equal( sparseM.row , sparseMT_T.row ) + assert np.array_equal( sparseM.data, sparseMT_T.data ) + +def test_directional_derivative_operator_interface(): + # interface tests, to see if negation and transposition work as their methods suggest + + periodic = [False, False] + domain = [(0,1),(0,1)] + ncells = [8, 8] + degree = [3, 3] + direction = 0 + + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + Ns = [SplineSpace(degree=d, grid=g, periodic=p, basis='B') \ + for d, g, p in zip(degree, breaks, periodic)] + + domain_decomposition = DomainDecomposition(ncells, periodic) + + # original space + V0 = TensorFemSpace(domain_decomposition, *Ns) + + # reduced space + V1 = V0.reduce_degree(axes=[0], basis='M') + + diff = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=False, transposed=False) + diffT = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=False, transposed=True) + diffN = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=True, transposed=False) + diffNT = DirectionalDerivativeOperator(V0.coeff_space, V1.coeff_space, direction, negative=True, transposed=True) + + # compare all with all by assembling matrices + compare_diff_operators_by_matrixassembly(diff.T, diffT) + compare_diff_operators_by_matrixassembly(-diff, diffN) + compare_diff_operators_by_matrixassembly(-diff.T, diffNT) + + compare_diff_operators_by_matrixassembly(diffT.T, diff) + compare_diff_operators_by_matrixassembly(-diffT, diffNT) + compare_diff_operators_by_matrixassembly(-diffT.T, diffN) + + compare_diff_operators_by_matrixassembly(diffN.T, diffNT) + compare_diff_operators_by_matrixassembly(-diffN, diff) + compare_diff_operators_by_matrixassembly(-diffN.T, diffT) + + compare_diff_operators_by_matrixassembly(diffNT.T, diffN) + compare_diff_operators_by_matrixassembly(-diffNT, diffT) + compare_diff_operators_by_matrixassembly(-diffNT.T, diff) + +@pytest.mark.parametrize('domain', [(0, 1), (-2, 3)]) +@pytest.mark.parametrize('ncells', [11, 37]) +@pytest.mark.parametrize('degree', [2, 3, 4, 5]) +@pytest.mark.parametrize('periodic', [True, False]) +@pytest.mark.parametrize('direction', [0]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +def test_directional_derivative_operator_1d_ser(domain, ncells, degree, periodic, direction, negative, transposed, seed): + run_directional_derivative_operator(None, [domain], [ncells], [degree], [periodic], direction, negative, transposed, seed, True) + +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8])]) +@pytest.mark.parametrize('ncells', [(10, 9), (27, 15)]) +@pytest.mark.parametrize('degree', [(3, 2), (4, 5)]) +@pytest.mark.parametrize('periodic', [(True, False), (False, True)]) +@pytest.mark.parametrize('direction', [0,1]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +def test_directional_derivative_operator_2d_ser(domain, ncells, degree, periodic, direction, negative, transposed, seed): + run_directional_derivative_operator(None, domain, ncells, degree, periodic, direction, negative, transposed, seed, True) + +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8], [-0.5, 0.5])]) +@pytest.mark.parametrize('ncells', [(4, 5, 7), (1, 1, 1)]) +@pytest.mark.parametrize('degree', [(3, 2, 5), (2, 4, 7), (1, 1, 1)]) +@pytest.mark.parametrize('periodic', [( True, False, False), + (False, True, False), + (False, False, True)]) +@pytest.mark.parametrize('direction', [0,1,2]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +def test_directional_derivative_operator_3d_ser(domain, ncells, degree, periodic, direction, negative, transposed, seed): + run_directional_derivative_operator(None, domain, ncells, degree, periodic, direction, negative, transposed, seed) + +@pytest.mark.parametrize('domain', [(0, 1), (-2, 3)]) +@pytest.mark.parametrize('ncells', [29, 37]) +@pytest.mark.parametrize('degree', [2, 3, 4, 5]) +@pytest.mark.parametrize('periodic', [True, False]) +@pytest.mark.parametrize('direction', [0]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +@pytest.mark.parallel +def test_directional_derivative_operator_1d_par(domain, ncells, degree, periodic, direction, negative, transposed, seed): + # TODO: re-enable KroneckerStencilMatrix assembly here (fails right now sometimes when transposing) + run_directional_derivative_operator(MPI.COMM_WORLD, [domain], [ncells], [degree], [periodic], direction, negative, transposed, seed) + +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8])]) +@pytest.mark.parametrize('ncells', [(17, 25), (27, 39)]) +@pytest.mark.parametrize('degree', [(3, 2), (4, 5)]) +@pytest.mark.parametrize('periodic', [(True, False), (False, True)]) +@pytest.mark.parametrize('direction', [0,1]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +@pytest.mark.parallel +def test_directional_derivative_operator_2d_par(domain, ncells, degree, periodic, direction, negative, transposed, seed): + # TODO: re-enable KroneckerStencilMatrix assembly here (fails right now sometimes when transposing) + run_directional_derivative_operator(MPI.COMM_WORLD, domain, ncells, degree, periodic, direction, negative, transposed, seed) + +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8], [-0.5, 0.5])]) +@pytest.mark.parametrize('ncells', [(10, 10, 13)]) +@pytest.mark.parametrize('degree', [(2, 2, 3)]) +@pytest.mark.parametrize('periodic', [( True, False, False), + (False, True, False), + (False, False, True)]) +@pytest.mark.parametrize('direction', [0,1,2]) +@pytest.mark.parametrize('negative', [True, False]) +@pytest.mark.parametrize('transposed', [True, False]) +@pytest.mark.parametrize('seed', [3]) +@pytest.mark.parallel +def test_directional_derivative_operator_3d_par(domain, ncells, degree, periodic, direction, negative, transposed, seed): + run_directional_derivative_operator(MPI.COMM_WORLD, domain, ncells, degree, periodic, direction, negative, transposed, seed) + +# (higher dimensions are not tested here for now) + +#============================================================================== +@pytest.mark.parametrize('domain', [(0, 1), (-2, 3)]) +@pytest.mark.parametrize('ncells', [11, 37]) +@pytest.mark.parametrize('degree', [2, 3, 4, 5]) +@pytest.mark.parametrize('periodic', [True, False]) +@pytest.mark.parametrize('seed', [1,3]) +@pytest.mark.parametrize('multiplicity', [1,2]) + +def test_Derivative1D(domain, ncells, degree, periodic, seed, multiplicity): + # determinize tests + np.random.seed(seed) + + breaks = np.linspace(*domain, num=ncells+1) + knots = make_knots(breaks, degree, periodic, multiplicity=multiplicity) + + # H1 space (0-forms) + N = SplineSpace(degree=degree, knots=knots, periodic=periodic, basis='B') + + domain_decomposition = DomainDecomposition([ncells], [periodic]) + V0 = TensorFemSpace(domain_decomposition, N) + + # L2 space (1-forms) + V1 = V0.reduce_degree(axes=[0], basis='M') + + # Create random field in V0 + u0 = FemField(V0) + + # Linear operator: 1D derivative + grad = Derivative1D(V0, V1) + + # Create random field in V0 + s, = V0.coeff_space.starts + e, = V0.coeff_space.ends + + u0.coeffs[s:e+1] = np.random.random(e-s+1) + + # Compute gradient (=derivative) of u0 + u1 = grad(u0) + + # Create evaluation grid, and check if ∂/∂x u0(x) == u1(x) + xgrid = np.linspace(*N.domain, num=11) + vals_grad_u0 = np.array([u0.gradient(x)[0] for x in xgrid]) + vals_u1 = np.array([u1(x) for x in xgrid]) + + # Test if relative max-norm of error is <= TOL + maxnorm_field = abs(vals_u1).max() + maxnorm_error = abs(vals_u1 - vals_grad_u0).max() + assert maxnorm_error / maxnorm_field <= 1e-14 + +#============================================================================== +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8])]) # 1 case +@pytest.mark.parametrize('ncells', [(10, 9), (27, 15)]) # 2 cases +@pytest.mark.parametrize('degree', [(3, 2), (4, 5)]) # 2 cases +@pytest.mark.parametrize('periodic', [(True, False), (False, True)]) # 2 cases +@pytest.mark.parametrize('seed', [1,3]) +@pytest.mark.parametrize('multiplicity', [(1, 1), (1, 2), (2, 2)]) + +def test_Gradient2D(domain, ncells, degree, periodic, seed, multiplicity): + # determinize tests + np.random.seed(seed) + + # Compute breakpoints along each direction + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + # H1 space (0-forms) + Nx, Ny = [SplineSpace(degree=d, grid=g, periodic=p, basis='B', multiplicity=m) \ + for d, g, p, m in zip(degree, breaks, periodic, multiplicity)] + + domain_decomposition = DomainDecomposition(ncells, periodic) + V0 = TensorFemSpace(domain_decomposition, Nx, Ny) + + # H-curl space (1-forms) + DxNy = V0.reduce_degree(axes=[0], basis='M') + NxDy = V0.reduce_degree(axes=[1], basis='M') + V1 = VectorFemSpace(DxNy, NxDy) + + # Linear operator: 2D gradient + grad = Gradient2D(V0, V1) + + # Create random field in V0 + u0 = FemField(V0) + + s1, s2 = V0.coeff_space.starts + e1, e2 = V0.coeff_space.ends + + u0.coeffs[s1:e1+1, s2:e2+1] = np.random.random((e1-s1+1, e2-s2+1)) + + # Compute gradient of u0 + u1 = grad(u0) + + # x and y components of u1 vector field + u1x = u1.fields[0] + u1y = u1.fields[1] + + # Create evaluation grid, and check if + # ∂/∂x u0(x, y) == u1x(x, y) + # ∂/∂y u0(x, y) == u1y(x, y) + + xgrid = np.linspace(*domain[0], num=11) + ygrid = np.linspace(*domain[1], num=11) + + vals_grad_u0 = np.array([[u0.gradient(x, y) for x in xgrid] for y in ygrid]) + vals_u1 = np.array([[[u1x(x, y), u1y(x, y)] for x in xgrid] for y in ygrid]) + + # Test if relative max-norm of error is <= TOL + maxnorm_field = abs(vals_u1).max() + maxnorm_error = abs(vals_u1 - vals_grad_u0).max() + assert maxnorm_error / maxnorm_field <= 1e-14 + +#============================================================================== +@pytest.mark.parametrize('domain', [([-2, 3], [6, 8], [-0.5, 0.5])]) # 1 case +@pytest.mark.parametrize('ncells', [(1, 8, 3), (7, 1, 2), (2, 2, 1), (4, 5, 7)]) # 4 cases +@pytest.mark.parametrize('degree', [(1, 3, 1), (3, 1, 5), (2, 4, 7)]) # 3 cases +@pytest.mark.parametrize('periodic', [( True, False, False), # 3 cases + (False, True, False), + (False, False, True)]) +@pytest.mark.parametrize('seed', [1,3]) +@pytest.mark.parametrize('multiplicity', [(1, 1, 1), (1, 2, 2), (2, 2, 2)]) + +def test_Gradient3D(domain, ncells, degree, periodic, seed, multiplicity): + if any([ncells[d] <= degree[d] and periodic[d] for d in range(3)]): + return + + # determinize tests + np.random.seed(seed) + + # Compute breakpoints along each direction + breaks = [np.linspace(*lims, num=n+1) for lims, n in zip(domain, ncells)] + + #change multiplicity if higher than degree to avoid problems (case p 1 + g0_x = x * (x - 1) * (x - 1.554)**(degree[0] - 2) + else: + # if degree[0] > 1: + # g0_x = (x-0.543)**2 * (x-1.554)**(degree[0]-2) + # else: + g0_x = (x - 0.25)**degree[0] + + if hom_bc_axes[1]: + assert degree[1] > 1 + g0_y = y * (y - 1) * (y - 0.324)**(degree[1] - 2) + else: + # if degree[1] > 1: + # g0_y = (y-1.675)**2 * (y-0.324)**(degree[1]-2) + + # else: + g0_y = (y - 0.75)**degree[1] + + expr = g0_x * g0_y + callable_function = lambdify(domain.coordinates, expr) + + return expr, callable_function + + +# ============================================================================== +@pytest.mark.parametrize('V1_type', ["Hcurl"]) +@pytest.mark.parametrize('degree', [[3, 3]]) +@pytest.mark.parametrize('nc', [5]) +@pytest.mark.parametrize('reg', [0]) +@pytest.mark.parametrize('hom_bc', [False, True]) +@pytest.mark.parametrize('domain_name', ["1patch", "4patch_nc", "2patch_nc"]) +@pytest.mark.parametrize("nonconforming, full_mom_pres", + [(True, True), (False, True)]) + +def test_conf_projectors_2d( + V1_type, + degree, + nc, + reg, + hom_bc, + full_mom_pres, + domain_name, + nonconforming +): + if domain_name == '1patch': + log_domain = Square('Omega', bounds1=(0, 1), bounds2=(0, 1)) + mapping = IdentityMapping('M1', dim=2) + domain = mapping(log_domain) + + 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)) + M1 = IdentityMapping('M1', dim=2) + M2 = IdentityMapping('M2', dim=2) + A = M1(A) + B = M2(B) + + domain = Domain.join(patches=[A, B], + connectivity=[((0, 0, 1), (1, 0, -1), 1)], + name='domain') + + elif domain_name == '4patch_nc': + + A = Square('A', bounds1=(0, 0.5), bounds2=(0, 0.5)) + B = Square('B', bounds1=(0.5, 1.), bounds2=(0, 0.5)) + C = Square('C', bounds1=(0, 0.5), bounds2=(0.5, 1)) + D = Square('D', bounds1=(0.5, 1.), bounds2=(0.5, 1)) + M1 = IdentityMapping('M1', dim=2) + M2 = IdentityMapping('M2', dim=2) + M3 = IdentityMapping('M3', dim=2) + M4 = IdentityMapping('M4', dim=2) + A = M1(A) + B = M2(B) + C = M3(C) + D = M4(D) + + domain = Domain.join(patches=[A, B, C, D], + connectivity=[((0, 0, 1), (1, 0, -1), 1), + ((2, 0, 1), (3, 0, -1), 1), + ((0, 1, 1), (2, 1, -1), 1), + ((1, 1, 1), (3, 1, -1), 1)], + name='domain') + if domain_name == '1patch': + ncells_h = {domain.name: [nc, nc]} + + elif nonconforming: + if len(domain) == 2: + ncells_h = { + 'M1(A)': [nc, nc], + 'M2(B)': [2 * nc, 2 * nc], + } + elif len(domain) == 4: + ncells_h = { + 'M1(A)': [nc, nc], + 'M2(B)': [nc, nc], + 'M3(C)': [2 * nc, 2 * nc], + 'M4(D)': [4 * nc, 4 * nc], + } + + else: + ncells_h = {} + for k, D in enumerate(domain.interior): + ncells_h[D.name] = [nc, nc] + + + domain_h = discretize(domain, ncells=ncells_h) # Vh space + derham = Derham(domain, ["H1", V1_type, "L2"]) + + nquads = [(d + 1) for d in degree] + 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 + # patch (<=> enough cells) + if full_mom_pres and (nc >= degree[0] + 1): + mom_pres = degree[0] + else: + raise ValueError(f'nc = {nc} too small for moment preservation') + mom_pres = -1 + # NOTE: if mom_pres but not full_mom_pres we could test reduced order + # moment preservation... + + # geometric projections (operators) + geomP0, geomP1, geomP2 = derham_h.projectors(nquads=nquads) + + # 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)) + + M0, M1, M2 = derham_h.hodge_operators() + M0, M1, M2 = (m.tosparse().tocsc() for m in (M0, M1, M2)) + + 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)) + + D0 = bD0 @ cP0 # Conga grad + D1 = bD1 @ cP1 # Conga curl or div + + assert np.allclose(sp_norm(cP0 - cP0 @ cP0), 0, 1e-12, 1e-12) # cP0 is a projection + assert np.allclose(sp_norm(cP1 - cP1 @ cP1), 0, 1e-12, 1e-12) # cP1 is a projection + assert np.allclose(sp_norm(cP2 - cP2 @ cP2), 0, 1e-12, 1e-12) # cP2 is a projection + + # D0 maps in the conforming V1 space (where cP1 coincides with Id) + assert np.allclose(sp_norm(D0 - cP1 @ D0), 0, 1e-12, 1e-12) + # D1 maps in the conforming V2 space (where cP2 coincides with Id) + assert np.allclose(sp_norm(D1 - cP2 @ D1), 0, 1e-12, 1e-12) + + # comparing projections of polynomials which should be exact + + # tests on cP0: + g0, 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=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 + assert np.allclose(g0_c, g0_L2_c, 1e-12, 1e-12) + # (P0_geom - confP0 @ P0_L2) polynomial= 0 + assert np.allclose(g0_c, cP0 @ g0_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # testing that polynomial moments are preserved: + # the following projection should be exact for polynomials of proper degree (no bc) + # conf_P0* : L2 -> V0 defined by := + # for all phi in V0 + g0, 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=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] + + 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_L2_c = M1_inv @ tilde_G1_c + + assert np.allclose(G1_c, G1_L2_c, 1e-12, 1e-12) + # (P1_geom - confP1 @ P1_L2) polynomial= 0 + assert np.allclose(G1_c, cP1 @ G1_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # as above + G1_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, 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=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 + assert np.allclose(g2_c, g2_L2_c, 1e-12, 1e-12) + # (P2_geom - confP2 @ P2_L2) polynomial = 0 + assert np.allclose(g2_c, cP2 @ g2_L2_c, 1e-12, 1e-12) + + if full_mom_pres: + # as above, here with same degree and bc as + g2_star_c = M2_inv @ cP2.transpose() @ tilde_g2_c + # (P2_geom - P2_star) polynomial = 0 + assert np.allclose(g2_c, g2_star_c, 1e-12, 1e-12) diff --git a/psydac/feec/tests/test_global_projectors.py b/psydac/feec/tests/test_global_projectors.py new file mode 100644 index 000000000..c1389f17f --- /dev/null +++ b/psydac/feec/tests/test_global_projectors.py @@ -0,0 +1,339 @@ +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_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 +from sympde.topology import element_of, Derham + + +#============================================================================== +@pytest.mark.parametrize('domain', [(0, 2*np.pi)]) +@pytest.mark.parametrize('ncells', [500]) +@pytest.mark.parametrize('degree', [1, 2, 3, 4, 5, 6, 7]) +@pytest.mark.parametrize('periodic', [False, True]) +@pytest.mark.parametrize('multiplicity', [1, 2]) + +def test_H1_projector_1d(domain, ncells, degree, periodic, multiplicity): + + #change mulitplicity if higher than degree to avoid problems (case p + Linear Operator. + + """ + + 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) + + 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 + + @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 + + def toarray(self): + return self._linop.toarray() + + def tosparse(self): + return self._linop.tosparse() + + #-------------------------------------------------------------------------- + def __call__(self, u, *, out=None): + assert isinstance(u, FemField) + assert u.space == self.fem_domain + + if self._linop is not None: + coeffs = self._linop.dot(u.coeffs) + else: + raise NotImplementedError('Class does not provide a __call__ method without a 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: + f = FemField(self.fem_domain, coeffs=f_coeffs) + return self(f).coeffs + else: + raise NotImplementedError('Class does not provide a dot method without a linear operator') 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 diff --git a/psydac/fem/tensor.py b/psydac/fem/tensor.py index 70f849c69..60f4e7796 100644 --- a/psydac/fem/tensor.py +++ b/psydac/fem/tensor.py @@ -1217,24 +1217,77 @@ def set_refined_space(self, ncells, new_space): self._refined_space[tuple(ncells)] = new_space # ... - def plot_2d_decomposition(self, mapping=None, refine=10): + def plot_2d_decomposition(self, mapping=None, *, refine=10, fig=None, ax=None, mpi_root=0): + """ + Plot decomposition of 2D TensorFemSpace w/ mapping to 2D physical space + + Plot the domain decomposition across MPI processes of a 2D + TensorFemSpace with a mapping between 2D logical and 2D physical spaces. + This function must be called collectively, and only the root process will make + the plot. On non-root processes the arguments `fig` and `ax` must be None. + + Parameters + ---------- + mapping : BasicCallableMapping + Mapping from (eta1, eta2) to (x1, x2). + + refine : int, default=10 + Cell refinement along the logical dimensions eta1 and eta2. + + fig : plt.Figure, optional + Figure where the plot should be made. Must be None on non-root processes. + ax : plt.Axes, optional + Axes where the plot should be made. Must be None on non-root processes. + + mpi_root: int, default=0 + The rank of the MPI root process which should create the plot. + + Returns + ------- + plt.Figure + Figure where the plot was made. Coincides with `fig` if provided. + """ import matplotlib.pyplot as plt from matplotlib.patches import Polygon, Patch + from sympde.topology.mapping import BasicCallableMapping from psydac.utilities.utils import refine_array_1d + # Sanity check + assert self.ldim == 2, "Function only works in 2D" + + # Check mapping if mapping is None: mapping = lambda eta: eta else: - assert mapping.ldim == self.ldim == 2 - assert mapping.pdim == self.ldim == 2 + assert isinstance(mapping, BasicCallableMapping) + assert mapping.ldim == 2, "Domain of argument `mapping` must be 2D" + assert mapping.pdim == 2, "Codomain of argument `mapping` must be 2D" - assert refine >= 1 - N = int(refine) - V1, V2 = self.spaces + # Check refine argument + assert isinstance(refine, int), f"Argument `refine` must be int, got {type(refine)} instead" + assert refine >= 1, f"Argument `refine` must be >= 1, got {refine} instead" + # Extract information about MPI communicator mpi_comm = self.coeff_space.cart.comm mpi_rank = mpi_comm.rank + mpi_size = mpi_comm.size + + # Check mpi_root argument + assert isinstance(mpi_root, int), f"Argument `mpi_root` must be int, got {type(mpi_root)} instead" + assert mpi_root >= 0, f"Argument `mpi_root` must be >= 0, got {mpi_root} instead" + assert mpi_root < mpi_size, f"Argument `mpi_root` must be smaller than communicator size ({mpi_size}), got {mpi_root} instead" + + # Check fig and ax arguments + if mpi_rank == mpi_root: + assert isinstance(fig, plt.Figure) or fig is None, f"Argument `fig` must be matplotlib Figure, got {type(fig)} instead" + assert isinstance(ax, plt.Axes) or ax is None, f"Argument `ax` must be matplotlib Axes, got {type(ax)} instead" + else: + assert fig is None, f"Argument `fig` must be None on non-root process with rank {mpi_rank}" + assert ax is None, f"Argument `ax` must be None on non-root process with rank {mpi_rank}" + + N = refine + V1, V2 = self.spaces # Local grid, refined [sk1, sk2], [ek1, ek2] = self.local_domain @@ -1251,23 +1304,62 @@ def plot_2d_decomposition(self, mapping=None, refine=10): poly = Polygon(xy, edgecolor='None') # Gather polygons on master process - polys = mpi_comm.gather(poly) + polys = mpi_comm.gather(poly, root=mpi_root) + + # Gather (s1, s2, e1, e2) on root + if mpi_rank == mpi_root: + s1_all = np.empty(mpi_size, dtype=int) + s2_all = np.empty(mpi_size, dtype=int) + e1_all = np.empty(mpi_size, dtype=int) + e2_all = np.empty(mpi_size, dtype=int) + else: + s1_all = None + s2_all = None + e1_all = None + e2_all = None + + mpi_comm.Gather(sk1 * N, s1_all, root=mpi_root) + mpi_comm.Gather(sk2 * N, s2_all, root=mpi_root) + mpi_comm.Gather((ek1 + 1) * N, e1_all, root=mpi_root) + mpi_comm.Gather((ek2 + 1) * N, e2_all, root=mpi_root) + + # Gather pcoords on root + # TODO: use Gatherv, and NumPy arrays as buffers + gathered_pcoords = mpi_comm.gather(pcoords, root=mpi_root) #------------------------------- # Non-master processes stop here - if mpi_rank != 0: + if mpi_rank != mpi_root: return #------------------------------- - # Global grid, refined - eta1 = refine_array_1d(V1.breaks, N) - eta2 = refine_array_1d(V2.breaks, N) - pcoords = np.array([[mapping(e1, e2) for e2 in eta2] for e1 in eta1]) - xx = pcoords[:, :, 0] - yy = pcoords[:, :, 1] + # Reconstruct global grid (refined) on root process + global_shape = ((V1.breaks.size - 1) * N + 1, + (V2.breaks.size - 1) * N + 1, + 2) + pcoords_global = np.empty(global_shape) + + for rank in range(mpi_comm.size): + s1 = s1_all[rank] + e1 = e1_all[rank] + s2 = s2_all[rank] + e2 = e2_all[rank] + pcoords_global[s1:e1+1, s2:e2+1, :] = gathered_pcoords[rank] + + xx = pcoords_global[:, :, 0] + yy = pcoords_global[:, :, 1] + + # If fig or ax are given, get one from the other. Otherwise create new ones + if fig and ax: + assert ax in fig.axes, "Argument `ax` must be in `fig.axes`" + elif fig: + ax = fig.gca() + elif ax: + fig = ax.figure + else: + fig, ax = plt.subplots(1, 1) # Plot decomposed domain - fig, ax = plt.subplots(1, 1) colors = itertools.cycle(plt.rcParams['axes.prop_cycle'].by_key()['color']) handles = [] for i, (poly, color) in enumerate(zip(polys, colors)): @@ -1286,7 +1378,8 @@ def plot_2d_decomposition(self, mapping=None, refine=10): ax.set_aspect('equal') ax.legend(handles=handles, bbox_to_anchor=(1.05, 1), loc=2) fig.tight_layout() - fig.show() + + return fig # ... def __str__(self): diff --git a/psydac/fem/tests/data/decomp_analytical_1_procs.png b/psydac/fem/tests/data/decomp_analytical_1_procs.png new file mode 100644 index 000000000..f4066700f Binary files /dev/null and b/psydac/fem/tests/data/decomp_analytical_1_procs.png differ diff --git a/psydac/fem/tests/data/decomp_analytical_4_procs.png b/psydac/fem/tests/data/decomp_analytical_4_procs.png new file mode 100644 index 000000000..9eedf7529 Binary files /dev/null and b/psydac/fem/tests/data/decomp_analytical_4_procs.png differ diff --git a/psydac/fem/tests/data/decomp_spline_1_procs.png b/psydac/fem/tests/data/decomp_spline_1_procs.png new file mode 100644 index 000000000..5e4ffe257 Binary files /dev/null and b/psydac/fem/tests/data/decomp_spline_1_procs.png differ diff --git a/psydac/fem/tests/data/decomp_spline_4_procs.png b/psydac/fem/tests/data/decomp_spline_4_procs.png new file mode 100644 index 000000000..6160ff220 Binary files /dev/null and b/psydac/fem/tests/data/decomp_spline_4_procs.png differ diff --git a/psydac/fem/tests/test_tensor.py b/psydac/fem/tests/test_tensor.py new file mode 100644 index 000000000..cc0dc8200 --- /dev/null +++ b/psydac/fem/tests/test_tensor.py @@ -0,0 +1,220 @@ +import os +import contextlib +from pathlib import Path + +import pytest +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from PIL import Image +from mpi4py import MPI + +from sympde.topology.domain import Square +from sympde.topology.space import ScalarFunctionSpace +from sympde.topology.analytical_mapping import TargetMapping + +from psydac.mapping.discrete_gallery import discrete_mapping +from psydac.api.discretization import discretize + + +#============================================================================== +# Machinery for comparing a PNG image with a reference on the root MPI process +#============================================================================== + +def similar_images(file1, file2, tolerance=0.01): + """ + Compare two PNG images and check if they are similar within tolerance. + + Parameters + ---------- + file1 : str + Path to first PNG file. + file2 : str + Path to second PNG file. + tolerance: float + Maximum allowed average difference between pixel values (0-1). + + Returns + ------- + bool : + True if images are similar enough, False otherwise. + """ + # Load images and convert to numpy arrays + img1 = np.array(Image.open(file1)).astype(float) + img2 = np.array(Image.open(file2)).astype(float) + + # Check dimensions match + if img1.shape != img2.shape: + return False + + # Normalize pixel values to 0-1 + img1 = img1 / 255.0 + img2 = img2 / 255.0 + + # Calculate mean absolute difference + diff = np.mean(np.abs(img1 - img2)) + + return diff <= tolerance + + +@contextlib.contextmanager +def consistent_png_rendering(): + """ + Context manager for consistent Matplotlib rendering across platforms. + """ + # Store original settings + orig_backend = mpl.get_backend() + orig_settings = { + 'text.usetex': mpl.rcParams['text.usetex'], + 'font.family': mpl.rcParams['font.family'], + } + + try: + # Use Agg backend (pure python, no GUI) + mpl.use('Agg') + # Configure settings for consistent rendering + mpl.rcParams.update({ + 'text.usetex': False, + 'font.family': 'DejaVu Sans', + }) + yield # Control returns to the with block + finally: + # Restore original settings + mpl.use(orig_backend) + mpl.rcParams.update(orig_settings) + + +def compare_figure_to_reference(fig, filename, *, dpi, tol, folder, comm, root): + """ + Compare a matplotlib figure to a reference PNG file on the root MPI process. + + Parameters + ---------- + fig : matplotlib.figure.Figure + The figure to compare with the reference image. + filename : str + Name of the PNG file to save and compare. + dpi : int + Dots per inch resolution for saving the figure. + tol : float + Tolerance for image comparison (between 0 and 1). + folder : str + Name of the folder containing reference images. + comm : mpi4py.MPI.Comm + MPI communicator object. + root : int + Rank of the MPI process that should perform the comparison. + + Returns + ------- + bool + True if the images are similar enough within the tolerance, + False otherwise. The result is broadcast to all MPI processes. + + Notes + ----- + The function saves the figure to a temporary file, compares it with + the reference image, and then removes the temporary file. Only the + root process performs the actual comparison, but the result is + broadcast to all processes. + """ + if comm.rank == root: + test_dir = Path(__file__).parent.absolute() + file1 = test_dir / filename + file2 = test_dir / folder / filename + with consistent_png_rendering(): + fig.savefig(file1, dpi=dpi) + close_enough = similar_images(file1, file2, tol) + # Clean up the temporary file + os.remove(file1) + else: + close_enough = None + + # Broadcast the boolean result from the root process to all others + close_enough = comm.bcast(close_enough, root=root) + + # All MPI processes return the same result + return close_enough + +#============================================================================== +# Unit tests +#============================================================================== +@pytest.mark.parallel +@pytest.mark.parametrize('root', ['first', 'last']) +@pytest.mark.parametrize('kind', ['spline', 'analytical']) +def test_plot_2d_decomposition(kind, root): + + # MPI communicator + mpi_comm = MPI.COMM_WORLD + mpi_size = mpi_comm.size + mpi_rank = mpi_comm.rank + + # MPI rank which should make the plot + if root == 'first': + mpi_root = 0 + elif root == 'last': + mpi_root = mpi_size - 1 + else: + raise ValueError(f'root argument has wrong value {root}') + + # Parameters of tensor-product 2D spline space + ncells = (6, 9) + degree = (2, 2) + + if kind == 'spline': + # 2D spline mapping and tensor FEM space (distributed) + F, Vh = discrete_mapping('target', ncells=ncells, degree=degree, + comm=mpi_comm, return_space=True) + elif kind == 'analytical': + Omega = Square('Omega', bounds1=(0, 1), bounds2=(0, 2 * np.pi)) + params = dict(c1=0, c2=0, k=0.3, D=0.2) + M = TargetMapping('M', dim=2, **params) + domain = M(Omega) + V = ScalarFunctionSpace('V', domain) + + # 2D Geometry object + domain_h = discretize(domain, ncells=ncells, periodic=(False, True), + comm=mpi_comm) + + # 2D spline tensor FEM space (distributed) + Vh = discretize(V, domain_h, degree=degree) + + # 2D callable mapping (analytical) + F = M.get_callable_mapping() + else: + raise ValueError(f'kind argument has wrong value {kind}') + + # Name of temporary image file to be compared with reference one + filename = f'decomp_{kind}_{mpi_size}_procs.png' + + # Relative tolerance for image comparison + RTOL = 0.02 + + # Plot 2D decomposition + # [1] Run without passing (fig, ax) + fig = Vh.plot_2d_decomposition(F, refine=5, mpi_root=mpi_root) + assert compare_figure_to_reference(fig, filename, folder='data', dpi=100, + tol=RTOL, comm=mpi_comm, root=mpi_root) + + # [2] Run with given (fig, ax), compatible + fig2, ax2 = plt.subplots(1, 1) if mpi_rank == mpi_root else (None, None) + Vh.plot_2d_decomposition(F, refine=5, fig=fig2, ax=ax2, mpi_root=mpi_root) + assert compare_figure_to_reference(fig2, filename, folder='data', dpi=100, + tol=RTOL, comm=mpi_comm, root=mpi_root) + + # [3] Run with given (fig, ax), incompatible + if mpi_rank == mpi_root: + fig3, ax3 = plt.subplots(1, 1) + with pytest.raises(AssertionError) as excinfo: + Vh.plot_2d_decomposition(F, refine=5, fig=fig2, ax=ax3, mpi_root=mpi_root) + assert "Argument `ax` must be in `fig.axes`" in str(excinfo.value) + plt.close(fig3) + else: + Vh.plot_2d_decomposition(F, refine=5, fig=None, ax=None, mpi_root=mpi_root) + +#============================================================================== +if __name__ == '__main__': + + test_plot_2d_decomposition('spline', 'first') + test_plot_2d_decomposition('analytical', 'last') + plt.show() diff --git a/psydac/linalg/basic.py b/psydac/linalg/basic.py index c298d687b..7b688074c 100644 --- a/psydac/linalg/basic.py +++ b/psydac/linalg/basic.py @@ -435,15 +435,71 @@ def H(self): def idot(self, v, out): """ - Implements out += self @ v with a temporary. - Subclasses should provide an implementation without a temporary. + Implements `out += self @ v` without a temporary, using a work array. + + This default implementation uses a local work array to store the result + of `self @ v`, and then sums it to the vector `out`. This doubles the + amount of read/write operations from/to local memory. If possible, + subclasses should provide a more efficient implementation which does + not use work arrays. + + Parameters + ---------- + v : Vector + The vector to which the linear operator `self` is applied. It must + belong to the domain of `self`. + + out : Vector + The vector to be incremented by `self @ v`. It must belong to the + codomain of `self`. """ - assert isinstance(v, Vector) - assert v.space == self.domain + assert isinstance( v, Vector) assert isinstance(out, Vector) - assert out.space == self.codomain - out += self.dot(v) + assert v.space is self.domain + assert out.space is self.codomain + + if not hasattr(self, '_work'): + self._work = self.codomain.zeros() + + self.dot(v, out=self._work) + out += self._work + + def dot_inner(self, v, w): + """ + Compute the inner product of (self @ v) with w, without a temporary. + + This is equivalent to self.dot(v).inner(w), but avoids the creation of + a temporary vector because the result of self.dot(v) is stored in a + local work array. If self is a positive-definite operator, this + operation is a (weighted) inner product. + + Parameters + ---------- + v : Vector + The vector to which the linear operator (self) is applied. It must + belong to the domain of self. + + w : Vector + The second vector in the inner product. It must belong to the + codomain of self. + + Returns + ------- + float | complex + The result of the inner product between (self @ v) and w. If the + field of self is real, this is a real number. If the field of self + is complex, this is a complex number. + """ + assert isinstance(v, Vector) + assert isinstance(w, Vector) + assert v.space is self.domain + assert w.space is self.codomain + + if not hasattr(self, '_work'): + self._work = self.codomain.zeros() + + return self.dot(v, out=self._work).inner(w) def dot_inner(self, v, w): """ @@ -695,12 +751,15 @@ def operator(self): def dtype(self): return None + def set_scalar(self, c): + """ Modifies the scalar with which this LinearOperator is multiplied. E.g. for updating the stepsize.""" + self._scalar = c + 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.toarray()) + 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)) @@ -758,7 +817,11 @@ def __init__(self, domain, codomain, *args): self._domain = domain self._codomain = codomain self._addends = addends + self._out = codomain.zeros() + #------------------------------------- + # Abstract interface + #------------------------------------- @property def domain(self): """ The domain of the linear operator, element of class ``VectorSpace``. """ @@ -769,26 +832,39 @@ def codomain(self): """ The codomain of the linear operator, element of class ``VectorSpace``. """ return self._codomain - @property - def addends(self): - """ A tuple containing the addends of the linear operator, elements of class ``LinearOperator``. """ - return self._addends - @property def dtype(self): return None + def tosparse(self): + from scipy.sparse import csr_matrix + out = csr_matrix(self.shape, dtype=self.dtype) + for a in self._addends: + out += a.tosparse() + return out + def toarray(self): out = np.zeros(self.shape, dtype=self.dtype) for a in self._addends: out += a.toarray() return out - def tosparse(self): - from scipy.sparse import csr_matrix - out = csr_matrix(self.shape, dtype=self.dtype) - for a in self._addends: - out += a.tosparse() + def dot(self, v, out=None): + """ Evaluates SumLinearOperator object at a vector v element of domain. """ + + assert isinstance(v, Vector) + assert v.space is self.domain + + if out is not None: + assert isinstance(out, Vector) + assert out.space is self.codomain + out *= 0 + else: + out = self.codomain.zeros() + + for A in self._addends: + A.idot(v, out) + return out def transpose(self, conjugate=False): @@ -797,6 +873,14 @@ def transpose(self, conjugate=False): t_addends = (*t_addends, a.transpose(conjugate=conjugate)) return SumLinearOperator(self.codomain, self.domain, *t_addends) + #-------------------------------------- + # Other properties/methods + #-------------------------------------- + @property + def addends(self): + """ A tuple containing the addends of the linear operator, elements of class ``LinearOperator``. """ + return self._addends + @staticmethod def simplify(addends): """ Simplifies a sum of linear operators by combining addends of the same class. """ @@ -819,23 +903,6 @@ def simplify(addends): out = (*out, A) return out - def dot(self, v, out=None): - """ Evaluates SumLinearOperator object at a vector v element of domain. """ - 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 - for a in self._addends: - a.idot(v, out) - return out - else: - out = self.codomain.zeros() - for a in self._addends: - a.idot(v, out=out) - return out - #=============================================================================== class ComposedLinearOperator(LinearOperator): r""" @@ -1294,7 +1361,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 diff --git a/psydac/linalg/sparse.py b/psydac/linalg/sparse.py new file mode 100644 index 000000000..71fa29fed --- /dev/null +++ b/psydac/linalg/sparse.py @@ -0,0 +1,114 @@ +# coding: utf-8 + +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 +from psydac.linalg.stencil import StencilVector +from psydac.linalg.block import BlockVector + +__all__ = ( + 'SparseMatrixLinearOperator', +) + +class SparseMatrixLinearOperator(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.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, (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.') + + 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 is self.domain + + if out is not None: + assert isinstance(out, Vector) + assert out.space is 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 00badb580..65894f7b1 100644 --- a/psydac/linalg/tests/test_block.py +++ b/psydac/linalg/tests/test_block.py @@ -11,6 +11,7 @@ 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.sparse import SparseMatrixLinearOperator from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.ddm.cart import DomainDecomposition, CartDecomposition @@ -728,6 +729,87 @@ def test_block_linear_operator_serial_dot( dtype, n1, n2, p1, p2, P1, P2 ): @pytest.mark.parametrize( 'dtype', [float] ) @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 57a9a6b86..fb13a2e6d 100644 --- a/psydac/linalg/utilities.py +++ b/psydac/linalg/utilities.py @@ -4,14 +4,14 @@ from math import sqrt from psydac.linalg.basic import Vector -from psydac.linalg.stencil import StencilVectorSpace, StencilVector +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 __all__ = ( 'array_to_psydac', 'petsc_to_psydac', - '_sym_ortho' + '_sym_ortho', ) #============================================================================== diff --git a/psydac/polar/dense.py b/psydac/polar/dense.py new file mode 100644 index 000000000..302e0a529 --- /dev/null +++ b/psydac/polar/dense.py @@ -0,0 +1,457 @@ +# coding: utf-8 +# +# Copyright 2018 Yaman Güçlü + +import numpy as np +from scipy.sparse import coo_matrix + +from psydac.linalg.basic import VectorSpace, Vector, LinearOperator + +__all__ = ('DenseVectorSpace', 'DenseVector', 'DenseMatrix') + +#============================================================================== +class DenseVectorSpace(VectorSpace): + """ + Space of one-dimensional global arrays, NOT distributed across processes. + + Typical examples are the right-hand side vector b and solution vector x + of a linear system A*x=b. + + Parameters + ---------- + n : int + Number of vector components. + + dtype : data-type, optional + Desired data-type for the arrays (default is numpy.float64). + + cart : psydac.ddm.cart.CartDecomposition, optional + N-dimensional Cartesian communicator with N >= 2 (default is None). + + radial_dim : int, optional + Dimension index for radial variable (default is 0). + + angle_dim : int, optional + Dimension index for angle variable (default is 1). + + Notes + ----- + + - The current implementation is tailored to the algorithm for imposing C^1 + continuity of a field on a domain with a polar singularity (O-point). + + - Given an N-dimensional Cartesian communicator (N=2+M), each process + belongs to 3 different subcommunicators: + + 1. An 'angular' 1D subcommunicator, where all processes share the same + identical (M+1)-dimensional array; + + 2. A 'radial' 1D subcommunicator, where only the 'master' process has + access to the data array (other processes store a 0-length array); + + 3. A 'tensor' M-dimensional subcommunicator, where the data array + is distributed among processes and requires the usual StencilVector + communication pattern. + + - When computing the dot product between two vectors, the following + operations will be performed in sequence: + + 1. Processes with radial coordinate = 0 compute a local dot product; + + 2. Processes with radial coordinate = 0 perform an MPI_ALLREDUCE + operation on the 'tensor' subcommunicator; + + 3. All processes perform an MPI_BCAST operation on the 'radial' + subcommunicator (process with radial coordinate = 0 is the root); + + """ + def __init__(self, n, *, dtype=np.float64, cart=None, radial_dim=0, angle_dim=1): + + self._n = n + self._dtype = dtype + self._cart = cart + + if cart is not None: + + # TODO: perform checks on input arguments + + # Angle sub-communicator (1D) + angle_comm = cart.subcomm[angle_dim] + + # Radial sub-communicator (1D) + radial_comm = cart.subcomm[radial_dim] + radial_master = (cart.coords[radial_dim] == 0) + radial_root = radial_comm.allreduce(radial_comm.rank if radial_master else 0) + + # Tensor sub-communicator (M-dimensional) + remain_dims = [d not in (radial_dim, angle_dim) for d in range(cart.ndim)] + tensor_comm = cart.comm_cart.Sub(remain_dims) + + # Calculate dimension of linear space + tensor_shape = [cart.npts[i] for i, d in enumerate(remain_dims) if d] + dimension = n * np.prod(tensor_shape, dtype=int) + + # Store info + self._radial_dim = radial_dim + self._radial_comm = radial_comm + self._radial_root = radial_root + self._angle_dim = angle_dim + self._angle_comm = angle_comm + self._tensor_comm = tensor_comm + self._dimension = dimension + + else: + + # TODO: remove inconsistency between serial and parallel cases + + # For now, in the serial case we assume that the dimension of the + # linear space is equal to the number of components + self._dimension = n + + #------------------------------------- + # Abstract interface + #------------------------------------- + @property + def dimension(self): + """ The dimension of a vector space V is the cardinality + (i.e. the number of vectors) of a basis of V over its base field. + """ + return self._dimension + + # ... + @property + def dtype(self): + return self._dtype + + # ... + def zeros(self): + """ + Get a copy of the null element of the DenseVectorSpace V. + + Returns + ------- + null : DenseVector + A new vector object with all components equal to zero. + + """ + data = np.zeros(self.ncoeff, dtype=self.dtype) + return DenseVector(self, data) + + # ... + def inner(self, x, y): + """ + Evaluate the inner vector product between two vectors of this space V. + + If the field of V is real, compute the classical scalar product. + If the field of V is complex, compute the classical sesquilinear + product with linearity on the second vector. + + TODO [YG 01.05.2025]: Currently, the first vector is conjugated. We + want to reverse this behavior in order to align with the convention + of FEniCS. + + TODO [MCP 07.05.2025]: Actually, there is currently no conjugation, + since `numpy.dot` is being called. + + Parameters + ---------- + x : Vector + The first vector in the scalar product. In the case of a complex + field, the inner product is antilinear w.r.t. this vector (hence + this vector is conjugated). + NOTE [MCP 07.05.2025]: currently without conjugate, see np.dot + + y : Vector + The second vector in the scalar product. The inner product is + linear w.r.t. this vector. + + Returns + ------- + float | complex + The scalar product of the two vectors. Note that inner(x, x) is + a non-negative real number which is zero if and only if x = 0. + + """ + assert isinstance(x, DenseVector) + assert isinstance(y, DenseVector) + assert x.space is self + assert y.space is self + + res = np.dot(x._data, y._data) + + V = self + if V.parallel: + if V.radial_comm.rank == V.radial_root: + res = V.tensor_comm.allreduce(res) + res = V.radial_comm.bcast(res, root=V.radial_root) + + return res + + # ... + def axpy(self, a, x, y): + + assert isinstance(a, (int, float, complex)) + assert isinstance(x, DenseVector) + assert isinstance(y, DenseVector) + assert x.space is self + assert y.space is self + + y += a * x + + #------------------------------------- + # Other properties/methods + #------------------------------------- + @property + def parallel(self): + return (self._cart is not None) + + # ... + @property + def ncoeff(self): + """ Local number of coefficients. """ + # TODO: maybe keep this number global, and add local 'dshape' property + if self.parallel: + return self._n if self._radial_comm.rank == self._radial_root else 0 + else: + return self._n + + # ... + @property + def tensor_comm(self): + return self._tensor_comm + + # ... + @property + def angle_comm(self): + return self._angle_comm + + # ... + @property + def radial_comm(self): + return self._radial_comm + + # ... + @property + def radial_root(self): + return self._radial_root + +#============================================================================== +class DenseVector(Vector): + + def __init__(self, V, data): + + assert isinstance(V, DenseVectorSpace) + + data = np.asarray(data) + assert data.ndim == 1 + assert data.shape ==(V.ncoeff,) + assert data.dtype == V.dtype + + self._space = V + self._data = data + + #-------------------------------------- + # Abstract interface + #-------------------------------------- + @property + def space(self): + return self._space + + # ... + def toarray(self, **kwargs): + return self._data.copy() + + # ... + def copy(self, out=None): + if self is out: + return self + if out is not None: + assert isinstance(out, DenseVector) + assert self.space is out.space + np.copyto(out._data, self._data, casting='no') + return out + else: + return DenseVector(self._space, self._data.copy()) + + # ... + def conjugate(self, out=None): + if out is not None: + assert isinstance(out, DenseVector) + assert out.space is self.space + else: + out = DenseVector(self.space) + np.conjugate(self._data, out=out._data, casting='no') + return out + + # ... + def __neg__(self): + return DenseVector(self._space, -self._data) + + # ... + def __mul__(self, a): + return DenseVector(self._space, self._data * a) + + # ... + def __add__(self, v): + assert isinstance(v, DenseVector) + assert v._space is self._space + return DenseVector(self._space, self._data + v._data) + + # ... + def __sub__(self, v): + assert isinstance(v, DenseVector) + assert v._space is self._space + return DenseVector(self._space, self._data - v._data) + + # ... + def __imul__(self, a): + self._data *= a + return self + + # ... + def __iadd__(self, v): + assert isinstance(v, DenseVector) + assert v._space is self._space + self._data += v._data + return self + + # ... + def __isub__(self, v): + assert isinstance(v, DenseVector) + assert v._space is self._space + self._data -= v._data + return self + + #------------------------------------- + # Other properties/methods + #------------------------------------- + def update_ghost_regions(self, *, direction=None): + pass + + # ... + @property + def ghost_regions_in_sync(self): + return True + + # ... + @ghost_regions_in_sync.setter + def ghost_regions_in_sync(self, value): + pass + +#============================================================================== +class DenseMatrix(LinearOperator): + + def __init__(self, V, W, data): + + assert isinstance(V, DenseVectorSpace) + assert isinstance(W, DenseVectorSpace) + + data = np.asarray(data) + assert data.ndim == 2 + assert data.shape == (W.ncoeff, V.ncoeff) +# assert data.dfype == #??? + + self._domain = V + self._codomain = W + self._data = data + + #-------------------------------------- + # Abstract interface + #-------------------------------------- + @property + def domain(self): + return self._domain + + # ... + @property + def codomain(self): + return self._codomain + + def transpose(self, conjugate=False): + raise NotImplementedError() + + # ... + @property + def dtype(self): + return self.domain.dtype + + def __truediv__(self, a): + """ Divide by scalar. """ + return self * (1.0 / a) + + def __itruediv__(self, a): + """ Divide by scalar, in place. """ + self *= 1.0 / a + return self + + # ... + def dot(self, v, out=None): + + assert isinstance(v, DenseVector) + assert v.space is self._domain + + if out: + assert isinstance(out, DenseVector) + assert out.space is self._codomain + np.dot(self._data, v._data, out=out._data) + else: + W = self._codomain + data = np.dot(self._data, v._data) + out = DenseVector(W, data) + + return out + + # ... + def toarray(self , **kwargs): + return self._data.copy() + + # ... + def tosparse(self , **kwargs): + return coo_matrix(self._data) + + # ... + def copy(self): + return DenseMatrix(self.domain, self.codomain, self._data.copy()) + + # ... + def __neg__(self): + return DenseMatrix(self.domain, self.codomain, -self._data) + + # ... + def __mul__(self, a): + return DenseMatrix(self.domain, self.codomain, self._data * a) + + # ... + def __add__(self, m): + assert isinstance(m, DenseMatrix) + assert self. domain == m. domain + assert self.codomain == m.codomain + return DenseMatrix(self.domain, self.codomain, self._data + m._data) + + # ... + def __sub__(self, m): + assert isinstance(m, DenseMatrix) + assert self. domain == m. domain + assert self.codomain == m.codomain + return DenseMatrix(self.domain, self.codomain, self._data - m._data) + + # ... + def __imul__(self, a): + self._data *= a + return self + + # ... + def __iadd__(self, m): + assert isinstance(m, DenseMatrix) + assert self. domain == m. domain + assert self.codomain == m.codomain + self._data += m._data + return self + + # ... + def __isub__(self, m): + assert isinstance(m, DenseMatrix) + assert self. domain == m. domain + assert self.codomain == m.codomain + self._data -= m._data + return self diff --git a/pyproject.toml b/pyproject.toml index e7717ce09..5b460fb16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ test = [ "pytest-cov >= 5.0.0", 'pytest >= 4.5', 'pytest-xdist >= 1.16', + 'Pillow', # Python Imaging Library (PIL) fork ] mpi = [ 'mpi4py >= 4',