"""Module for the discretization class."""
from __future__ import annotations
import abc
from typing import Callable, Type
import numpy as np
import scipy.sparse as sps
import pygeon as pg
[docs]
class Discretization(abc.ABC):
"""
Abstract class for PyGeoN discretization methods.
"""
poly_order: int
"""Polynomial degree of the basis functions"""
tensor_order: int
"""Tensor order of the basis functions"""
[docs]
def __init__(self, keyword: str = pg.UNITARY_DATA) -> None:
"""
Initialize the Discretization object.
Args:
keyword (str): The keyword used to identify the discretization method.
Default is pg.UNITARY_DATA.
Returns:
None
"""
self.keyword = keyword
[docs]
def __repr__(self) -> str:
"""
Return a string representation of the Discretization object.
The string includes the type of the discretization and, if available,
the keyword associated with it.
Returns:
str: A string representation of the Discretization object.
"""
s = f"Discretization of type {self.__class__.__name__}"
if hasattr(self, "keyword"):
s += f" with keyword {self.keyword}"
return s
[docs]
@abc.abstractmethod
def ndof(self, sd: pg.Grid) -> int:
"""
Returns the number of degrees of freedom associated to the method.
Args:
sd: Grid, or a subclass.
Returns:
ndof: the number of degrees of freedom.
"""
[docs]
@abc.abstractmethod
def assemble_mass_matrix(
self, sd: pg.Grid, data: dict | None = None
) -> sps.csc_array:
"""
Assembles the mass matrix
Args:
sd (pg.Grid): Grid object or a subclass.
data (dict | None): Dictionary with physical parameters for scaling.
Returns:
sps.csc_array: The mass matrix.
"""
[docs]
def assemble_lumped_matrix(
self, sd: pg.Grid, data: dict | None = None
) -> sps.csc_array:
"""
Assembles the lumped mass matrix given by the row sums on the diagonal.
Args:
sd (pg.Grid): Grid object or a subclass.
data (dict | None): Dictionary with physical parameters for scaling.
Returns:
sps.csc_array: The lumped mass matrix.
"""
diag_mass = self.assemble_mass_matrix(sd, data).sum(axis=0)
return sps.diags_array(np.asarray(diag_mass).flatten()).tocsc()
[docs]
@abc.abstractmethod
def assemble_diff_matrix(self, sd: pg.Grid) -> sps.csc_array:
"""
Assembles the matrix corresponding to the differential operator.
Args:
sd (pg.Grid): Grid object or a subclass.
Returns:
sps.csc_array: The differential matrix.
"""
[docs]
def assemble_stiff_matrix(
self, sd: pg.Grid, data: dict | None = None
) -> sps.csc_array:
"""
Assembles the stiffness matrix.
This method takes a grid object `sd` and an optional data dictionary `data` as
input. It first calls the `assemble_diff_matrix` method to obtain the
differential matrix `B`.
Then, it creates an instance of the range discretization class using the `dim`
attribute of `sd`. Next, it calls the `assemble_mass_matrix` method of the range
discretization class to obtain the mass matrix `A`.
Finally, it returns the product of the transpose of `B`, `A`, and `B`.
Args:
sd (pg.Grid): Grid object or a subclass.
data (dict | None): Optional data dictionary. Defaults to None.
Returns:
sps.csc_array: The stiffness matrix.
"""
B = self.assemble_diff_matrix(sd)
discr = self.get_range_discr_class(sd.dim)(self.keyword)
A = discr.assemble_mass_matrix(sd, data)
return (B.T @ A @ B).tocsc()
[docs]
@abc.abstractmethod
def interpolate(
self, sd: pg.Grid, func: Callable[[np.ndarray], np.ndarray]
) -> np.ndarray:
"""
Interpolates a function onto the finite element space
Args:
sd (pg.Grid): Grid, or a subclass.
func (Callable): A function that returns the function values at coordinates.
Returns:
np.ndarray: The values of the degrees of freedom
"""
[docs]
def eval_at_cell_centers(self, sd: pg.Grid) -> sps.csc_array:
"""
Assembles the matrix for evaluating the discretization at the cell centers.
Args:
sd (pg.Grid): Grid object or a subclass.
Returns:
sps.csc_array: The evaluation matrix.
"""
Pi = self.proj_to_PwPolynomials(sd)
Poly_space = pg.get_PwPolynomials(self.poly_order, self.tensor_order)(
self.keyword
)
return Poly_space.eval_at_cell_centers(sd) @ Pi
[docs]
def source_term(
self, sd: pg.Grid, func: Callable[[np.ndarray], np.ndarray]
) -> np.ndarray:
"""
Assembles the source term by interpolating the given function
and multiplying by the mass matrix
Args:
sd (pg.Grid): Grid object or a subclass.
func (Callable): A function that returns the function values at coordinates.
Returns:
np.ndarray: The evaluation matrix.
"""
return self.assemble_mass_matrix(sd) @ self.interpolate(sd, func)
[docs]
@abc.abstractmethod
def assemble_nat_bc(
self, sd: pg.Grid, func: Callable[[np.ndarray], np.ndarray], b_faces: np.ndarray
) -> np.ndarray:
"""
Assembles the natural boundary condition term
(Tr q, p)_Gamma
Args:
sd (pg.Grid): The grid object.
func (Callable): The function representing the natural boundary condition.
b_faces (np.ndarray): The array of boundary faces.
Returns:
np.ndarray: The assembled natural boundary condition term.
"""
[docs]
@abc.abstractmethod
def get_range_discr_class(self, dim: int) -> Type[pg.Discretization]:
"""
Returns the discretization class that contains the range of the differential
Args:
dim (int): The dimension of the range.
Returns:
pg.Discretization: The discretization class containing the range of the
differential
"""
[docs]
@abc.abstractmethod
def proj_to_PwPolynomials(self, sd: pg.Grid) -> sps.csc_array:
"""
Returns the inclusion matrix that projects the finite element space onto
the lowest order piecewise polynomial space without loss of information.
Args:
sd (pg.Grid): The grid.
Returns:
sps.csc_array: The inclusion matrix.
"""
[docs]
def error_l2(
self,
sd: pg.Grid,
num_sol: np.ndarray,
ana_sol: Callable[[np.ndarray], np.ndarray],
relative: bool = True,
poly_order: int | None = None,
data: dict | None = None,
) -> float:
"""
Returns the l2 error computed against an analytical solution given as a
function. NOTE: The default implementation uses interpolation which may result
in unexpected superconvergence. In that case, we advise specifying poly_order.
Args:
sd (pg.Grid): Grid, or a subclass.
num_sol (np.ndarray): Vector of the numerical solution.
ana_sol (Callable): Function that represents the analytical solution.
relative (bool, optional): Compute the relative error or not. Defaults to
True.
poly_order (int, optional): The default is to compare the numerical solution
to the interpolation of the given solution. If poly_order is specified,
the error is evaluated in a piecewise polynomial space of that order.
Returns:
float: The computed error.
"""
# Default case in which we interpolate the solution and compare
if poly_order is None:
int_sol = self.interpolate(sd, ana_sol)
mass = self.assemble_mass_matrix(sd, data)
norm = (int_sol @ mass @ int_sol.T) if relative else 1
diff = num_sol - int_sol
return np.sqrt(diff @ mass @ diff.T / norm)
# If poly_order is specified, we compare the solutions in a piecewise polynomial
# space
else:
poly_space = pg.get_PwPolynomials(poly_order, self.tensor_order)(
self.keyword
)
proj_sol = pg.proj_to_PwPolynomials(self, sd, poly_order) @ num_sol
return poly_space.error_l2(sd, proj_sol, ana_sol, relative, data=data)