diff --git a/requirements-dev.txt b/requirements-dev.txt
index 385dbdab..8dac3c99 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,8 +1,8 @@
-black == 23.*
+black == 24.*
deepdiff
flake8
isort
-mypy >= 0.981
+mypy
pytest >= 4.6
pytest-cov
pytest-runner
diff --git a/src/pygeon/__init__.py b/src/pygeon/__init__.py
index caca007a..8347e6cc 100644
--- a/src/pygeon/__init__.py
+++ b/src/pygeon/__init__.py
@@ -18,9 +18,14 @@
from pygeon.discretizations.vec_discretization import VecDiscretization
from pygeon.discretizations.fem.hcurl import Nedelec0, Nedelec1
-from pygeon.discretizations.fem.hdiv import RT0, BDM1, VecBDM1
+from pygeon.discretizations.fem.hdiv import RT0, BDM1, VecBDM1, VecRT0
from pygeon.discretizations.fem.h1 import Lagrange1, VecLagrange1
-from pygeon.discretizations.fem.l2 import PwConstants, PwLinears, VecPwConstants
+from pygeon.discretizations.fem.l2 import (
+ PwConstants,
+ PwLinears,
+ VecPwConstants,
+ VecPwLinears,
+)
from pygeon.discretizations.vem.hdiv import VRT0, VBDM1
from pygeon.discretizations.vem.h1 import VLagrange1, VecVLagrange1
@@ -44,6 +49,7 @@
SpanningTree,
SpanningWeightedTrees,
SpanningTreeElasticity,
+ SpanningTreeCosserat,
)
import pygeon.utils.bmat as bmat
diff --git a/src/pygeon/discretizations/discretization.py b/src/pygeon/discretizations/discretization.py
index 1fb87d1e..2da02f22 100644
--- a/src/pygeon/discretizations/discretization.py
+++ b/src/pygeon/discretizations/discretization.py
@@ -265,6 +265,7 @@ def error_l2(
ana_sol: Callable[[np.ndarray], np.ndarray],
relative: Optional[bool] = True,
etype: Optional[str] = "standard",
+ data: dict = None,
) -> float:
"""
Returns the l2 error computed against an analytical solution given as a function.
@@ -281,7 +282,7 @@ def error_l2(
float: The computed error.
"""
int_sol = self.interpolate(sd, ana_sol)
- mass = self.assemble_mass_matrix(sd)
+ mass = self.assemble_mass_matrix(sd, data)
norm = (int_sol @ mass @ int_sol.T) if relative else 1
diff --git a/src/pygeon/discretizations/fem/hdiv.py b/src/pygeon/discretizations/fem/hdiv.py
index fe6ca7d7..7d5c2bf9 100644
--- a/src/pygeon/discretizations/fem/hdiv.py
+++ b/src/pygeon/discretizations/fem/hdiv.py
@@ -41,7 +41,7 @@ class RT0(pg.Discretization, pp.RT0):
assemble_nat_bc(sd: pg.Grid, func: Callable[[np.ndarray], np.ndarray],
b_faces: np.ndarray) -> np.ndarray:
- Assembles the natural boundary condition term (n dot q, func)_\Gamma
+ Assembles the natural boundary condition term (n dot q, func)_Gamma
get_range_discr_class(dim: int) -> pg.Discretization:
Returns the range discretization class for the given dimension.
@@ -121,8 +121,9 @@ def assemble_mass_matrix(
Returns:
sps.csc_matrix: The mass matrix.
"""
-
+ # create dummy data, unitary permeability, in case not present
data = self.create_dummy_data(sd, data)
+ # perform the rt0 discretization
pp.RT0.discretize(self, sd, data)
return data[pp.DISCRETIZATION_MATRICES][self.keyword][
self.mass_matrix_key
@@ -211,7 +212,7 @@ def assemble_nat_bc(
) -> np.ndarray:
"""
Assembles the natural boundary condition term
- (n dot q, func)_\Gamma
+ (n dot q, func)_Gamma
Args:
sd (pg.Grid): The grid object representing the computational domain.
@@ -584,7 +585,7 @@ def assemble_nat_bc(
) -> np.ndarray:
"""
Assembles the natural boundary condition term
- (n dot q, func)_\Gamma
+ (n dot q, func)_Gamma
Args:
sd (pg.Grid): The grid object representing the computational domain.
@@ -606,7 +607,7 @@ def assemble_nat_bc(
sign = np.sum(sd.cell_faces.tocsr()[face, :])
loc_vals = np.array(
[func(sd.nodes[:, node]) for node in sd.face_nodes[:, face].indices]
- )
+ ).ravel()
vals[face + np.arange(sd.dim) * sd.num_faces] = sign * local_mass @ loc_vals
@@ -812,7 +813,7 @@ def assemble_mass_matrix(self, sd: pg.Grid, data: dict) -> sps.csc_matrix:
# Assemble the trace part
B = self.assemble_trace_matrix(sd)
- # Assemble the piecewise linear mass mastrix, to assemble the term
+ # Assemble the piecewise linear mass matrix, to assemble the term
# (Trace(sigma), Trace(tau))
discr = pg.PwLinears(self.keyword)
M = discr.assemble_mass_matrix(sd, data_for_PwL)
@@ -820,6 +821,46 @@ def assemble_mass_matrix(self, sd: pg.Grid, data: dict) -> sps.csc_matrix:
# Compose all the parts and return them
return D - B.T @ M @ B
+ def assemble_mass_matrix_cosserat(self, sd: pg.Grid, data: dict) -> sps.csc_matrix:
+ """
+ Assembles and returns the mass matrix for vector BDM1 discretizing the Cosserat inner
+ product, which is given by (A sigma, tau) where
+ A sigma = (sym(sigma) - coeff * Trace(sigma) * I) / (2 mu) + skw(sigma) / (2 mu_c)
+ with mu and lambda the Lamé constants, coeff = lambda / (2*mu + dim*lambda), and
+ mu_c the coupling Lamé modulus.
+
+ Args:
+ sd (pg.Grid): The grid.
+ data (dict): Data for the assembly.
+
+ Returns:
+ sps.csc_matrix: The mass matrix obtained from the discretization.
+ """
+ M = self.assemble_mass_matrix(sd, data)
+
+ # Extract the data
+ mu = data[pp.PARAMETERS][self.keyword]["mu"]
+ mu_c = data[pp.PARAMETERS][self.keyword]["mu_c"]
+
+ coeff = 0.25 * (1 / mu_c - 1 / mu)
+
+ # If coeff is a scalar, replace it by a vector so that it can be accessed per cell
+ if isinstance(coeff, np.ScalarType):
+ coeff = np.full(sd.num_cells, coeff)
+
+ data_for_R = pp.initialize_data(sd, {}, self.keyword, {"weight": coeff})
+
+ if sd.dim == 2:
+ R_space = pg.PwLinears(self.keyword)
+ elif sd.dim == 3:
+ R_space = pg.VecPwLinears(self.keyword)
+
+ R_mass = R_space.assemble_mass_matrix(sd, data_for_R)
+
+ asym = self.assemble_asym_matrix(sd, False)
+
+ return M + asym.T @ R_mass @ asym
+
def assemble_trace_matrix(self, sd: pg.Grid) -> sps.csc_matrix:
"""
Assembles and returns the trace matrix for the vector BDM1.
@@ -876,69 +917,200 @@ def assemble_trace_matrix(self, sd: pg.Grid) -> sps.csc_matrix:
# Construct the global matrices
return sps.csc_matrix((data_IJ[:idx], (rows_I[:idx], cols_J[:idx])))
- def assemble_asym_matrix(self, sd: pg.Grid) -> sps.csc_matrix:
+ def assemble_asym_matrix(self, sd: pg.Grid, as_pwconstant=True) -> sps.csc_matrix:
"""
Assembles and returns the asymmetric matrix for the vector BDM1.
The asymmetric operator `as' for a tensor is a scalar and it is defined in 2d as
- as(tau) = tau_xy - tau_yx
+ as(tau) = tau_yx - tau_xy
while for a tensor in 3d it is a vector and given by
as(tau) = [tau_zy - tau_yz, tau_xz - tau_zx, tau_yx - tau_xy]^T
- Note: We assume that the as(tau) is a cell variable.
+ Note: We assume that the as(tau) is a piecewise linear.
Args:
sd (pg.Grid): The grid.
+ as_pwconstant (bool): Compute the operator with the range on the piece-wise
+ constant (default), otherwise the mapping is on the piece-wise linears.
Returns:
sps.csc_matrix: The asymmetric matrix obtained from the discretization.
"""
- # We need to map the BDM1 to the cell center, thus we construct the proper operator
- P = self.eval_at_cell_centers(sd)
- nc, cv = sd.num_cells, sd.cell_volumes
- # We consider a different approach if sd is 2d or 3d
- if sd.dim == 2:
- # Every cell has two entries, one for the t_xy and one for the t_yx
- rows_I = np.tile(np.arange(nc), 2)
- # Given a 2d tensor, represented as a vector, the t_xy component is the second
- # and the t_yx is the third
- cols_J = np.hstack(
- (
- np.arange(nc, 2 * nc),
- np.arange(3 * nc, 4 * nc),
- )
+
+ # overestimate the size
+ size = np.square((sd.dim + 1) * sd.dim) * sd.num_cells
+ rows_I = np.empty(size, dtype=int)
+ cols_J = np.empty(size, dtype=int)
+ data_IJ = np.empty(size)
+ idx = 0
+
+ # Helper functions for inside the loop
+ negate_col = [2, 0, 1]
+ zeroed_col = [0, 1, 2]
+
+ if sd.dim == 3:
+ ind_list = np.arange(3)
+ shift = ind_list
+ rot_space = pg.VecPwLinears(self.keyword)
+ scaling = sps.diags(np.tile(sd.cell_volumes, 3))
+ elif sd.dim == 2:
+ ind_list = [2]
+ shift = [0, 0, 0]
+ rot_space = pg.PwLinears(self.keyword)
+ scaling = sps.diags(sd.cell_volumes)
+ else:
+ raise ValueError("The grid should be either two or three-dimensional")
+
+ cell_nodes = sd.cell_nodes()
+ for c in np.arange(sd.num_cells):
+ # For the current cell retrieve its faces and
+ # determine the location of the dof
+ loc = slice(sd.cell_faces.indptr[c], sd.cell_faces.indptr[c + 1])
+ faces_loc = sd.cell_faces.indices[loc]
+ dof_loc = np.reshape(
+ sd.face_nodes[:, faces_loc].indices, (sd.dim, -1), order="F"
)
- # t_xy gets a +1 and t_yx a -1, represented as p0 dofs so with the cell volume
- data_IJ = np.hstack((cv, -cv))
- # Assemble the matrix with a given shape, t_xx and t_yy are not considered
- T = sps.csc_matrix((data_IJ, (rows_I, cols_J)), shape=(nc, P.shape[0]))
- return T @ P
- elif sd.dim == 3:
- # Define an help function
- enum = lambda i: np.arange(i * nc, (i + 1) * nc)
- # Since as(tau) is a piecewise vector we need to arrange the entries accordingly
- # to the second component of the tensor
- # as(tau)_x = tau_zy - tau_yz -> z and y with -1 and +1
- # as(tau)_y = tau_xz - tau_zx -> z and x with +1 and -1
- # as(tau)_z = tau_yx - tau_xy -> x and y with -1 and +1
- rows_I = np.hstack((enum(2), enum(1), enum(2), enum(0), enum(1), enum(0)))
- # Given a 3d tensor, represented as a vector, the involved components are
- # (t_xy, t_xz, t_yx, t_yz, t_zx, t_zy) which are in positions (1, 2, 3) and
- # (5, 6, 7)
- cols_J = np.hstack(
- (
- np.arange(nc, 4 * nc),
- np.arange(5 * nc, 8 * nc),
- )
+
+ cell_nodes_loc = cell_nodes[:, c].toarray()
+ Psi = self.scalar_discr.eval_basis_at_node(
+ sd, dof_loc, cell_nodes_loc, faces_loc
)
- # Assording to the ordering of rows and cols given abose we have the following
- # signs (-1, +1, +1, -1, -1, +1), weighted by the cell volumes since it is
- # represented as a p0 vector element
- data_IJ = np.hstack((-cv, cv, cv, -cv, -cv, cv))
- T = sps.csc_matrix((data_IJ, (rows_I, cols_J)), shape=(3 * nc, P.shape[0]))
- return T @ P
+
+ # Get all the components of the basis at node
+ Psi_i, Psi_j, Psi_v = sps.find(Psi)
+
+ for ind in ind_list:
+ Psi_v_copy = Psi_v.copy()
+ Psi_v_copy[np.mod(Psi_j, 3) == negate_col[ind]] *= -1
+ Psi_v_copy[np.mod(Psi_j, 3) == zeroed_col[ind]] *= 0
+
+ loc_ind = np.hstack([faces_loc] * sd.dim)
+ loc_ind += np.repeat(np.arange(sd.dim), sd.dim + 1) * sd.num_faces
+
+ cols = np.tile(loc_ind, (3, 1))
+ cols[0, :] += np.mod(-ind, 3) * self.scalar_discr.ndof(sd)
+ cols[1, :] += np.mod(-ind - 1, 3) * self.scalar_discr.ndof(sd)
+ cols[2, :] += np.mod(-ind - 2, 3) * self.scalar_discr.ndof(sd)
+
+ cols = np.tile(cols, (sd.dim + 1, 1)).T
+
+ cols = cols[Psi_i, Psi_j]
+
+ nodes_loc = np.arange((sd.dim + 1) * c, (sd.dim + 1) * (c + 1))
+ rows = np.repeat(nodes_loc, 3)[Psi_j]
+
+ # Save values of the local matrix in the global structure
+ loc_idx = slice(idx, idx + cols.size)
+ rows_I[loc_idx] = rows + shift[ind] * (sd.dim + 1) * sd.num_cells
+ cols_J[loc_idx] = cols
+ data_IJ[loc_idx] = Psi_v_copy
+ idx += cols.size
+
+ # Construct the global matrices
+ asym = sps.csc_matrix((data_IJ[:idx], (rows_I[:idx], cols_J[:idx])))
+
+ # Return the operator that maps to the piece-wise constant
+ if as_pwconstant:
+ return scaling @ rot_space.eval_at_cell_centers(sd) @ asym
else:
- raise ValueError("The grid should be either bi or three-dimensional")
+ return asym
+
+ def assemble_lumped_matrix(
+ self, sd: pg.Grid, data: Optional[dict] = None
+ ) -> sps.csc_matrix:
+ """
+ Assembles the lumped matrix for the given grid.
+
+ Args:
+ sd (pg.Grid): The grid object.
+ data (Optional[dict]): Optional data dictionary.
+
+ Returns:
+ sps.csc_matrix: The assembled lumped matrix.
+ """
+ # Assemble the block diagonal mass matrix for the base discretization class
+ D = super().assemble_lumped_matrix(sd)
+
+ # Assemble the trace part
+ B = self.assemble_trace_matrix(sd)
+
+ # Assemble the piecewise linear mass matrix, to assemble the term
+ # (Trace(sigma), Trace(tau))
+ discr = pg.PwLinears(self.keyword)
+ M = discr.assemble_lumped_matrix(sd)
+
+ # Extract the data and compute the coefficient for the trace part
+ mu = data[pp.PARAMETERS][self.keyword]["mu"]
+ lambda_ = data[pp.PARAMETERS][self.keyword]["lambda"]
+ coeff = lambda_ / (2 * mu + sd.dim * lambda_)
+
+ # Compose all the parts and return them
+ return (D - coeff * B.T @ M @ B) / (2 * mu)
+
+ def assemble_lumped_matrix_cosserat(
+ self, sd: pg.Grid, data: dict
+ ) -> sps.csc_matrix:
+ """
+ Assembles the lumped matrix with cosserat terms for the given grid.
+
+ Args:
+ sd (pg.Grid): The grid object.
+ data (Optional[dict]): Optional data dictionary.
+
+ Returns:
+ sps.csc_matrix: The assembled lumped matrix.
+ """
+
+ M = self.assemble_lumped_matrix(sd, data)
+
+ # Extract the data
+ mu = data[pp.PARAMETERS][self.keyword]["mu"]
+ mu_c = data[pp.PARAMETERS][self.keyword]["mu_c"]
+
+ coeff = 0.25 * (1 / mu_c - 1 / mu)
+
+ # If coeff is a scalar, replace it by a vector so that it can be accessed per cell
+ if isinstance(coeff, np.ScalarType):
+ coeff = np.full(sd.num_cells, coeff)
+
+ data_for_R = pp.initialize_data(sd, {}, self.keyword, {"weight": coeff})
+
+ if sd.dim == 2:
+ R_space = pg.PwLinears(self.keyword)
+ elif sd.dim == 3:
+ R_space = pg.VecPwLinears(self.keyword)
+
+ R_mass = R_space.assemble_lumped_matrix(sd, data_for_R)
+
+ asym = self.assemble_asym_matrix(sd, False)
+
+ return M + asym.T @ R_mass @ asym
+
+ def proj_to_RT0(self, sd: pg.Grid) -> sps.csc_matrix:
+ """
+ Project the function space to the lowest order Raviart-Thomas (RT0) space.
+
+ Args:
+ sd (pg.Grid): The grid object representing the computational domain.
+
+ Returns:
+ sps.csc_matrix: The projection matrix to the RT0 space.
+ """
+ proj = self.scalar_discr.proj_to_RT0(sd)
+ return sps.block_diag([proj] * sd.dim, format="csc")
+
+ def proj_from_RT0(self, sd: pg.Grid) -> sps.csc_matrix:
+ """
+ Project the RT0 finite element space onto the faces of the given grid.
+
+ Args:
+ sd (pg.Grid): The grid on which the projection is performed.
+
+ Returns:
+ sps.csc_matrix: The projection matrix.
+ """
+ proj = self.scalar_discr.proj_from_RT0(sd)
+ return sps.block_diag([proj] * sd.dim, format="csc")
def get_range_discr_class(self, dim: int) -> object:
"""
@@ -952,3 +1124,163 @@ def get_range_discr_class(self, dim: int) -> object:
differential
"""
return pg.VecPwConstants
+
+
+class VecRT0(pg.VecDiscretization):
+ def __init__(self, keyword: str) -> None:
+ """
+ Initialize the vector RT0 discretization class.
+ The scalar discretization class is pg.RT0.
+
+ We are considering the following structure of the stress tensor in 2d
+
+ sigma = [[sigma_xx, sigma_xy],
+ [sigma_yx, sigma_yy]]
+
+ which is represented in the code unrolled row-wise as a vector of length 4
+
+ sigma = [sigma_xx, sigma_xy,
+ sigma_yx, sigma_yy]
+
+ While in 3d the stress tensor can be written as
+
+ sigma = [[sigma_xx, sigma_xy, sigma_xz],
+ [sigma_yx, sigma_yy, sigma_yz],
+ [sigma_zx, sigma_zy, sigma_zz]]
+
+ where its vectorized structure of length 9 is given by
+
+ sigma = [sigma_xx, sigma_xy, sigma_xz,
+ sigma_yx, sigma_yy, sigma_yz,
+ sigma_zx, sigma_zy, sigma_zz]
+
+ Args:
+ keyword (str): The keyword for the vector discretization class.
+
+ Returns:
+ None
+ """
+ super().__init__(keyword, pg.RT0)
+
+ def assemble_mass_matrix(self, sd: pg.Grid, data: dict) -> sps.csc_matrix:
+ """
+ Assembles and returns the mass matrix for vector RT0, which is given by
+ (A sigma, tau) where A sigma = (sigma - coeff * Trace(sigma) * I) / (2 mu)
+ with mu and lambda the Lamé constants and coeff = lambda / (2*mu + dim*lambda)
+
+ Args:
+ sd (pg.Grid): The grid.
+ data (dict): Data for the assembly.
+
+ Returns:
+ sps.csc_matrix: The mass matrix obtained from the discretization.
+ """
+ # Assemble the block diagonal mass matrix for the base discretization class,
+ # with unitary data. The data are handled afterwards
+ D = super().assemble_mass_matrix(sd)
+
+ # Assemble the trace part
+ B = self.assemble_trace_matrix(sd)
+
+ # Assemble the piecewise linear mass matrix, to assemble the term
+ # (Trace(sigma), Trace(tau))
+ discr = pg.PwLinears(self.keyword)
+ M = discr.assemble_mass_matrix(sd)
+
+ # Extract the data and compute the coefficient for the trace part
+ mu = data[pp.PARAMETERS][self.keyword]["mu"]
+ lambda_ = data[pp.PARAMETERS][self.keyword]["lambda"]
+ coeff = lambda_ / (2 * mu + sd.dim * lambda_)
+
+ # Compose all the parts and return them
+ return (D - coeff * B.T @ M @ B) / (2 * mu)
+
+ def assemble_mass_matrix_cosserat(self, sd: pg.Grid, data: dict) -> sps.csc_matrix:
+ """
+ Assembles and returns the mass matrix for vector BDM1 discretizing the Cosserat inner
+ product, which is given by (A sigma, tau) where
+ A sigma = (sym(sigma) - coeff * Trace(sigma) * I) / (2 mu) + skw(sigma) / (2 mu_c)
+ with mu and lambda the Lamé constants, coeff = lambda / (2*mu + dim*lambda), and
+ mu_c the coupling Lamé modulus.
+
+ Args:
+ sd (pg.Grid): The grid.
+ data (dict): Data for the assembly.
+
+ Returns:
+ sps.csc_matrix: The mass matrix obtained from the discretization.
+
+ TODO: Consider using inheritance from VecBDM1.assemble_mass_matrix_cosserat
+ """
+
+ M = self.assemble_mass_matrix(sd, data)
+
+ # Extract the data
+ mu = data[pp.PARAMETERS][self.keyword]["mu"]
+ mu_c = data[pp.PARAMETERS][self.keyword]["mu_c"]
+
+ coeff = 0.25 * (1 / mu_c - 1 / mu)
+
+ # If coeff is a scalar, replace it by a vector so that it can be accessed per cell
+ if isinstance(coeff, np.ScalarType):
+ coeff = np.full(sd.num_cells, coeff)
+
+ data_for_R = pp.initialize_data(sd, {}, self.keyword, {"weight": coeff})
+
+ if sd.dim == 2:
+ R_space = pg.PwLinears(self.keyword)
+ elif sd.dim == 3:
+ R_space = pg.VecPwLinears(self.keyword)
+
+ R_mass = R_space.assemble_mass_matrix(sd, data_for_R)
+
+ asym = self.assemble_asym_matrix(sd, False)
+
+ return M + asym.T @ R_mass @ asym
+
+ def assemble_trace_matrix(self, sd: pg.Grid) -> sps.csc_matrix:
+ """
+ Assembles and returns the trace matrix for the vector BDM1.
+
+ Args:
+ sd (pg.Grid): The grid.
+
+ Returns:
+ sps.csc_matrix: The trace matrix obtained from the discretization.
+ """
+ vec_bdm1 = VecBDM1(self.keyword)
+ proj = vec_bdm1.proj_from_RT0(sd)
+ return vec_bdm1.assemble_trace_matrix(sd) @ proj
+
+ def assemble_asym_matrix(self, sd: pg.Grid, as_pwconstant=True) -> sps.csc_matrix:
+ """
+ Assembles and returns the asymmetric matrix for the vector RT0.
+
+ The asymmetric operator `as' for a tensor is a scalar and it is defined in 2d as
+ as(tau) = tau_xy - tau_yx
+ while for a tensor in 3d it is a vector and given by
+ as(tau) = [tau_zy - tau_yz, tau_xz - tau_zx, tau_yx - tau_xy]^T
+
+ Note: We assume that the as(tau) is a cell variable.
+
+ Args:
+ sd (pg.Grid): The grid.
+
+ Returns:
+ sps.csc_matrix: The asymmetric matrix obtained from the discretization.
+ """
+ vec_bdm1 = VecBDM1(self.keyword)
+ proj = vec_bdm1.proj_from_RT0(sd)
+ return vec_bdm1.assemble_asym_matrix(sd, as_pwconstant) @ proj
+
+ def get_range_discr_class(self, dim: int) -> object:
+ """
+ Returns the range discretization class for the given dimension.
+
+ Args:
+ dim (int): The dimension of the range space.
+
+ Returns:
+ pg.Discretization: The range discretization class.
+ """
+ return pg.VecPwConstants
diff --git a/src/pygeon/discretizations/fem/l2.py b/src/pygeon/discretizations/fem/l2.py
index bf851c9f..ced9a6f2 100644
--- a/src/pygeon/discretizations/fem/l2.py
+++ b/src/pygeon/discretizations/fem/l2.py
@@ -341,6 +341,27 @@ def assemble_mass_matrix(
# Construct the global matrix
return sps.csc_matrix((data_IJ, (rows_I, cols_J)))
+ def assemble_lumped_matrix(
+ self, sd: pg.Grid, data: Optional[dict] = None
+ ) -> sps.csc_matrix:
+ """
+ Assembles the lumped matrix for the given grid.
+
+ Args:
+ sd (pg.Grid): The grid object.
+ data (Optional[dict]): Optional data dictionary.
+
+ Returns:
+ sps.csc_matrix: The assembled lumped matrix.
+ """
+ try:
+ weight = data[pp.PARAMETERS][self.keyword]["weight"]
+ except Exception:
+ weight = np.ones(sd.num_cells)
+
+ diag = np.repeat(weight * sd.cell_volumes, sd.dim + 1) / (sd.dim + 1)
+ return sps.diags(diag, format="csc")
+
def assemble_diff_matrix(self, sd: pg.Grid) -> sps.csc_matrix:
"""
Assembles the matrix corresponding to the differential operator.
@@ -393,7 +414,12 @@ def eval_at_cell_centers(self, sd: pg.Grid) -> np.ndarray:
Returns:
np.ndarray: The evaluation matrix.
"""
- raise NotImplementedError
+
+ rows = np.repeat(np.arange(sd.num_cells), sd.dim + 1)
+ cols = np.arange(self.ndof(sd))
+ data = np.ones(self.ndof(sd)) / (sd.dim + 1)
+
+ return sps.csc_matrix((data, (rows, cols)))
def interpolate(
self, sd: pg.Grid, func: Callable[[np.ndarray], np.ndarray]
@@ -468,3 +494,95 @@ def assemble_nat_bc(
np.ndarray: The assembled natural boundary condition vector.
"""
return np.zeros(self.ndof(sd))
+
+ def error_l2(
+ self,
+ sd: pg.Grid,
+ num_sol: np.ndarray,
+ ana_sol: Callable[[np.ndarray], np.ndarray],
+ relative: Optional[bool] = True,
+ etype: Optional[str] = "specific",
+ ) -> float:
+ """
+ Returns the l2 error computed against an analytical solution given as a function.
+
+ Args:
+ sd (pg.Grid): Grid, or a subclass.
+ num_sol (np.ndarray): Vector of the numerical solution.
+ ana_sol (Callable[[np.ndarray], np.ndarray]): Function that represents the
+ analytical solution.
+ relative (Optional[bool], optional): Compute the relative error or not.
+ Defaults to True.
+ etype (Optional[str], optional): Type of error computed. Defaults to "specific".
+
+ Returns:
+ float: The computed error.
+ """
+
+ err2 = 0
+ num_sol = num_sol.reshape((sd.dim, -1))
+ for d in np.arange(sd.dim):
+ ana_sol_dim = lambda x: ana_sol(x)[d]
+ num_sol_dim = num_sol[d]
+
+ err2_dim = self.scalar_discr.error_l2(
+ sd, num_sol_dim, ana_sol_dim, relative, etype
+ )
+ err2 += err2_dim**2
+ return np.sqrt(err2)
+
+
+class VecPwLinears(pg.VecDiscretization):
+ """
+ A class representing the discretization using vector piecewise linear functions.
+
+ Attributes:
+ keyword (str): The keyword for the vector discretization class.
+
+ Methods:
+ get_range_discr_class(self, dim: int) -> pg.Discretization:
+ Returns the discretization class for the range of the differential.
+
+ """
+
+ def __init__(self, keyword: str) -> None:
+ """
+ Initialize the vector discretization class.
+ The scalar discretization class is pg.PwLinears.
+
+ Args:
+ keyword (str): The keyword for the vector discretization class.
+
+ Returns:
+ None
+ """
+ super().__init__(keyword, pg.PwLinears)
+
+ def get_range_discr_class(self, dim: int) -> pg.Discretization:
+ """
+ Returns the discretization class for the range of the differential.
+
+ Args:
+ dim (int): The dimension of the range space.
+
+ Returns:
+ pg.Discretization: The discretization class for the range of the differential.
+ """
+ return self.scalar_discr.get_range_discr_class(dim)
+
+ 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 vector, equal to zero.
+
+ Args:
+ sd (pg.Grid): The grid object.
+ func (Callable[[np.ndarray], np.ndarray]): The function defining the
+ natural boundary condition.
+ b_faces (np.ndarray): The array of boundary faces.
+
+ Returns:
+ np.ndarray: The assembled natural boundary condition vector.
+ """
+ return np.zeros(self.ndof(sd))
diff --git a/src/pygeon/discretizations/vem/h1.py b/src/pygeon/discretizations/vem/h1.py
index a5561b9a..955f8679 100644
--- a/src/pygeon/discretizations/vem/h1.py
+++ b/src/pygeon/discretizations/vem/h1.py
@@ -150,7 +150,7 @@ def assemble_loc_proj_to_mon(
) -> np.ndarray:
"""
Computes the local projection onto the monomials
- Returns the coefficients {a_i} in a_0 + [a_1, a_2] \dot (x - c) / d
+ Returns the coefficients {a_i} in a_0 + [a_1, a_2] dot (x - c) / d
for each VL1 basis function.
Args:
diff --git a/src/pygeon/discretizations/vem/hdiv.py b/src/pygeon/discretizations/vem/hdiv.py
index 645e3dc6..846ebb9c 100644
--- a/src/pygeon/discretizations/vem/hdiv.py
+++ b/src/pygeon/discretizations/vem/hdiv.py
@@ -167,7 +167,7 @@ def assemble_nat_bc(
) -> np.ndarray:
"""
Assembles the natural boundary condition term
- (n dot q, func)_\Gamma.
+ (n dot q, func)_Gamma.
Args:
sd (pg.Grid): The grid object representing the computational domain.
@@ -408,7 +408,7 @@ def assemble_nat_bc(
) -> np.ndarray:
"""
Assembles the natural boundary condition term
- (n dot q, func)_\Gamma
+ (n dot q, func)_Gamma
Args:
sd (pg.Grid): The grid object representing the computational domain.
diff --git a/src/pygeon/grids/create_grid.py b/src/pygeon/grids/create_grid.py
index 5d9e66b2..f75a5254 100644
--- a/src/pygeon/grids/create_grid.py
+++ b/src/pygeon/grids/create_grid.py
@@ -25,12 +25,14 @@ def grid_from_domain(
Returns:
Either a pg.MixedDimensionalGrid or a pg.Grid, depending on the value of as_mdg.
"""
+ as_mdg = kwargs.get("as_mdg", True)
mesh_size_min = kwargs.get("mesh_size_min", mesh_size / 10)
+
mesh_kwargs = {"mesh_size_frac": mesh_size, "mesh_size_min": mesh_size_min}
- mdg = pp.create_fracture_network(domain=domain).mesh(mesh_kwargs)
+ mdg = pp.create_fracture_network(domain=domain).mesh(mesh_kwargs, **kwargs)
pg.convert_from_pp(mdg)
- if kwargs.get("as_mdg", True):
+ if as_mdg:
return mdg
else:
return mdg.subdomains(dim=mdg.dim_max())[0]
diff --git a/src/pygeon/grids/graph.py b/src/pygeon/grids/graph.py
index e61e6d70..b9c0c122 100644
--- a/src/pygeon/grids/graph.py
+++ b/src/pygeon/grids/graph.py
@@ -12,7 +12,7 @@ class Graph(pp.Grid):
def __init__(self, graph, dim=2):
"""Construct a pp.Grid like representation of a graph.
- The following informations are stored
+ The following information are stored
dim: the dimension of the graph, always
nodes: the coordinate of the nodes
cell_faces: the map from the cells (graph nodes) to the faces (graph edges)
@@ -89,7 +89,7 @@ def compute_ridges(self):
This function create mapping between the faces and the ridges in the graph,
the following data are created
- num_ridges: the number of rideges (graph cicles)
+ num_ridges: the number of ridges (graph cycles)
num_peaks: the number of peaks (always 0)
face_ridges: the map from the faces to the ridges
ridge_peaks: an empty matrix that maps the ridges to peaks
@@ -182,7 +182,7 @@ def edges_of_nodes(self, nodes):
def collapse(self, dim):
"""Collapse the graph removing the nodes with a given dimension.
- From a bipartite graph it is possible to create a dim = 2 graph (only fracures)
+ From a bipartite graph it is possible to create a dim = 2 graph (only fractures)
or a dim = 1 (only fracture intersections) graph.
"""
@@ -224,7 +224,7 @@ def shortest_paths(self, start, end):
return np.array(list(sp), dtype=np.object)
def not_shortest_paths(self, start, end, sp=None, cutoff=None):
- """Compyte all the not shortest paths between two graph nodes.
+ """Compute all the not shortest paths between two graph nodes.
It is possible to set a cutoff to make the computation faster leaving out long paths.
"""
@@ -291,8 +291,8 @@ def to_file(self, file_name):
"""
# make sure that an edge is sorted by dimension
- sort = (
- lambda e: e
+ sort = lambda e: (
+ e
if self.graph.nodes[e[0]]["dim"] > self.graph.nodes[e[1]]["dim"]
else np.flip(e)
)
diff --git a/src/pygeon/grids/md_grid.py b/src/pygeon/grids/md_grid.py
index 565af939..91bba4e4 100644
--- a/src/pygeon/grids/md_grid.py
+++ b/src/pygeon/grids/md_grid.py
@@ -89,26 +89,27 @@ def initialize_data(self) -> None:
Returns:
None
"""
+ unit_keyword = "unit"
for sd, data in self.subdomains(return_data=True):
perm = pp.SecondOrderTensor(np.ones(sd.num_cells))
data[pp.PARAMETERS] = pp.Parameters(
sd,
- ["unit"],
+ [unit_keyword],
[
{"second_order_tensor": perm},
],
)
- data[pp.DISCRETIZATION_MATRICES] = {}
+ data[pp.DISCRETIZATION_MATRICES] = {unit_keyword: {}}
for _, data in self.interfaces(return_data=True):
data[pp.PARAMETERS] = pp.Parameters(
sd,
- ["unit"],
+ [unit_keyword],
[
{"normal_diffusivity": 1.0},
],
)
- data[pp.DISCRETIZATION_MATRICES] = {}
+ data[pp.DISCRETIZATION_MATRICES] = {unit_keyword: {}}
def num_subdomain_faces(
self, cond: Optional[Callable[[pg.Grid], bool]] = None
diff --git a/src/pygeon/numerics/projections.py b/src/pygeon/numerics/projections.py
index e739725f..fd4e4247 100644
--- a/src/pygeon/numerics/projections.py
+++ b/src/pygeon/numerics/projections.py
@@ -72,7 +72,7 @@ def proj_faces_to_cells(
)
if discr is None:
- discr = pg.RT0("flow")
+ discr = pg.RT0("unit")
# Local mass matrices
for nn_sd, (sd, d_sd) in enumerate(mdg.subdomains(return_data=True)):
diff --git a/src/pygeon/numerics/spanningtree.py b/src/pygeon/numerics/spanningtree.py
index 7899e4b5..99dcb364 100644
--- a/src/pygeon/numerics/spanningtree.py
+++ b/src/pygeon/numerics/spanningtree.py
@@ -497,6 +497,70 @@ def compute_system(self, sd: pg.Grid) -> sps.csc_matrix:
return sps.vstack((-div, -asym)) @ self.expand
+class SpanningTreeCosserat(SpanningTreeElasticity):
+ """
+ Represents a class for computing the spanning tree for the Cosserat problem.
+
+ Attributes:
+ expand (sps.csc_matrix): The expanded matrix for spanning tree computation.
+ system (sps.csc_matrix): The computed system matrix.
+
+ Methods:
+ setup_system(self, mdg: pg.MixedDimensionalGrid, flagged_faces: np.ndarray) -> None:
+ Set up the system for the spanning tree algorithm.
+
+ compute_expand(self, sd: pg.Grid, flagged_faces: np.ndarray) -> sps.csc_matrix:
+ Compute the expanded matrix for spanning tree computation.
+
+ compute_system(self, sd: pg.Grid) -> sps.csc_matrix:
+ Computes the system matrix for the given grid.
+ """
+
+ def compute_expand(self, sd: pg.Grid, flagged_faces: np.ndarray) -> sps.csc_matrix:
+ """
+ Compute the expanded matrix for spanning tree computation.
+
+ Args:
+ sd (pg.Grid): The grid object.
+ flagged_faces (np.ndarray): Array of flagged faces.
+
+ Returns:
+ sps.csc_matrix: The expanded matrix for spanning tree computation.
+ """
+ dim_sig_omega = sd.dim * (sd.dim + 1) // 2
+
+ expand = pg.numerics.linear_system.create_restriction(flagged_faces).T.tocsc()
+
+ return sps.block_diag([expand] * dim_sig_omega, format="csc")
+
+ def compute_system(self, sd: pg.Grid) -> sps.csc_matrix:
+ """
+ Computes the system matrix for the given grid.
+
+ Args:
+ sd (pg.Grid): The grid object representing the domain.
+
+ Returns:
+ sps.csc_matrix: The computed system matrix.
+ """
+ assert sd.dim == 3, "Only implemented the 3D Raviart-Thomas version."
+
+ # first we assemble the B matrix
+ key = "tree"
+ vec_rt0 = pg.VecRT0(key)
+ vec_p0 = pg.VecPwConstants(key)
+
+ M = vec_p0.assemble_mass_matrix(sd)
+
+ div = M @ vec_rt0.assemble_diff_matrix(sd)
+ asym = M @ vec_rt0.assemble_asym_matrix(sd)
+
+ B = sps.bmat([[-div, None], [-asym, -div]], format="csc")
+
+ # create the solution operator
+ return B @ self.expand
+
+
class SpanningWeightedTrees:
"""
Class that can perform a spanning weighted trees solve, based on one of
diff --git a/tests/integration/test_elasticity.py b/tests/integration/test_elasticity.py
index 8c8dfc26..bf8d135d 100644
--- a/tests/integration/test_elasticity.py
+++ b/tests/integration/test_elasticity.py
@@ -180,9 +180,9 @@ def run_elasticity_2d(self, u_boundary, N):
asym = Mr @ vec_bdm1.assemble_asym_matrix(sd)
# fmt: off
- spp = sps.bmat([[ Ms, div.T, asym.T],
- [ -div, None, None],
- [-asym, None, None]], format = "csc")
+ spp = sps.bmat([[ Ms, div.T, -asym.T],
+ [-div, None, None],
+ [asym, None, None]], format = "csc")
# fmt: on
b_faces = sd.tags["domain_boundary_faces"]
diff --git a/tests/unit/test_projections.py b/tests/unit/test_projections.py
index 5763f5bd..83cce460 100644
--- a/tests/unit/test_projections.py
+++ b/tests/unit/test_projections.py
@@ -294,5 +294,4 @@ def test3(self):
if __name__ == "__main__":
- # ProjectionsUnitTest().test3()
unittest.main()
diff --git a/tests/unit/test_pwlinear.py b/tests/unit/test_pwlinear.py
index db457936..f63056b5 100644
--- a/tests/unit/test_pwlinear.py
+++ b/tests/unit/test_pwlinear.py
@@ -137,7 +137,11 @@ def test_eval_at_cell_centers(self):
discr = pg.PwLinears("P1b")
- self.assertRaises(NotImplementedError, discr.eval_at_cell_centers, sd)
+ known_func = np.ones(discr.ndof(sd))
+
+ P = discr.eval_at_cell_centers(sd)
+
+ self.assertTrue(np.allclose(P @ known_func, np.ones(sd.num_cells)))
def test_assemble_nat_bc(self):
dim = 2
diff --git a/tests/unit/test_spanningtree.py b/tests/unit/test_spanningtree.py
index 4208c8bd..a52fe34f 100644
--- a/tests/unit/test_spanningtree.py
+++ b/tests/unit/test_spanningtree.py
@@ -230,5 +230,57 @@ def test_for_errors(self):
self.assertRaises(NotImplementedError, pg.SpanningTreeElasticity, mdg)
+class SpanningTreeCosseratTest(unittest.TestCase):
+ def check(self, sd):
+ mdg = pg.as_mdg(sd)
+ pg.convert_from_pp(mdg)
+ mdg.compute_geometry()
+
+ B = self.assemble_B(mdg)
+ f = np.random.rand(B.shape[0])
+
+ sptr = pg.SpanningTreeCosserat(mdg)
+
+ s_f = sptr.solve(f)
+ self.assertTrue(np.allclose(B @ s_f, f))
+
+ def assemble_B(self, mdg):
+ sd = mdg.subdomains(dim=mdg.dim_max())[0]
+
+ key = "tree"
+ vec_rt0 = pg.VecRT0(key)
+ vec_p0 = pg.VecPwConstants(key)
+
+ M = vec_p0.assemble_mass_matrix(sd)
+
+ div = M @ vec_rt0.assemble_diff_matrix(sd)
+ asym = M @ vec_rt0.assemble_asym_matrix(sd)
+
+ return sps.bmat([[-div, None], [-asym, -div]])
+
+ def test_elasticity_struct_tet_grid(self):
+ sd = pp.StructuredTetrahedralGrid([1] * 3)
+ self.check(sd)
+
+ def test_elasticity_unstruct_tet_grid(self):
+ sd = pg.unit_grid(3, 1.0)
+ self.check(sd)
+
+ def test_assemble_SI(self):
+ N, dim = 3, 3
+ sd = pp.StructuredTetrahedralGrid([N] * dim, [1] * dim)
+ mdg = pg.as_mdg(sd)
+ pg.convert_from_pp(mdg)
+ mdg.compute_geometry()
+
+ sptr = pg.SpanningTreeCosserat(mdg)
+
+ SI = sptr.assemble_SI()
+ B = self.assemble_B(mdg)
+ check = sps.eye_array(B.shape[0]) - B @ SI
+
+ self.assertTrue(np.allclose(check.data, 0))
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/unit/test_vecbdm1.py b/tests/unit/test_vecbdm1.py
index 07e36705..c20b8802 100644
--- a/tests/unit/test_vecbdm1.py
+++ b/tests/unit/test_vecbdm1.py
@@ -6,6 +6,15 @@
class VecBDM1Test(unittest.TestCase):
+
+ def test_1d(self):
+ sd = pp.CartGrid([1])
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ vec_bdm1 = pg.VecBDM1("vecbdm1")
+ self.assertRaises(ValueError, vec_bdm1.assemble_asym_matrix, sd)
+
def test_trace_2d(self):
sd = pp.StructuredTriangleGrid([1] * 2, [1] * 2)
pg.convert_from_pp(sd)
@@ -52,6 +61,61 @@ def test_assemble_mass_matrix_2d(self):
M = vec_bdm1.assemble_mass_matrix(sd, data)
self.assertAlmostEqual(u.T @ M @ u, 30)
+ def test_assemble_mass_matrix_cosserat_2d(self):
+ N = 10
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_bdm1.assemble_mass_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 28)
+
+ def test_assemble_lumped_matrix_2d(self):
+ N = 10
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5}}}
+ M = vec_bdm1.assemble_lumped_matrix(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 26)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0}}}
+ M = vec_bdm1.assemble_lumped_matrix(sd, data)
+ self.assertAlmostEqual(u.T @ M @ u, 30)
+
+ def test_assemble_lumped_matrix_cosserat_2d(self):
+ N = 10
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_bdm1.assemble_lumped_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 28)
+
def test_eval_at_cell_centers_2d(self):
N = 1
sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
@@ -72,6 +136,24 @@ def linear(x):
self.assertAlmostEqual(np.linalg.norm(eval - known), 0)
+ def test_proj_to_and_from_rt0_2d(self):
+ N = 1
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ def linear(x):
+ return np.array([x, 2 * x])
+
+ interp = vec_bdm1.interpolate(sd, linear)
+ interp_to_rt0 = vec_bdm1.proj_to_RT0(sd) @ interp
+ interp_from_rt0 = vec_bdm1.proj_from_RT0(sd) @ interp_to_rt0
+
+ self.assertAlmostEqual(np.linalg.norm(interp - interp_from_rt0), 0)
+
def test_range(self):
key = "vecbdm1"
vec_bdm1 = pg.VecBDM1(key)
@@ -88,19 +170,19 @@ def test_assemble_asym_matrix_2d(self):
fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
u = vec_bdm1.interpolate(sd, fun)
- asym = vec_bdm1.assemble_asym_matrix(sd)
+ asym = vec_bdm1.assemble_asym_matrix(sd, False)
- p0 = pg.PwConstants("p0")
- cell_asym_u = p0.eval_at_cell_centers(sd) @ (asym @ u)
+ p1 = pg.PwLinears("p1")
+ cell_asym_u = p1.eval_at_cell_centers(sd) @ (asym @ u)
- self.assertTrue(np.allclose(cell_asym_u, -2))
+ self.assertTrue(np.allclose(cell_asym_u, 2))
def test_trace_3d(self):
sd = pp.StructuredTetrahedralGrid([1] * 3, [1] * 3)
pg.convert_from_pp(sd)
sd.compute_geometry()
- vec_bdm1 = pg.VecBDM1("vec_bdm1")
+ vec_bdm1 = pg.VecBDM1("vecbdm1")
B = vec_bdm1.assemble_trace_matrix(sd)
@@ -118,7 +200,7 @@ def test_ndof_3d(self):
pg.convert_from_pp(sd)
sd.compute_geometry()
- vec_bdm1 = pg.VecBDM1("vec_bdm1")
+ vec_bdm1 = pg.VecBDM1("vecbdm1")
self.assertEqual(vec_bdm1.ndof(sd), 162)
@@ -128,7 +210,7 @@ def test_assemble_mass_matrix_3d(self):
pg.convert_from_pp(sd)
sd.compute_geometry()
- key = "vec_bdm1"
+ key = "vecbdm1"
vec_bdm1 = pg.VecBDM1(key)
data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5}}}
@@ -143,13 +225,72 @@ def test_assemble_mass_matrix_3d(self):
M = vec_bdm1.assemble_mass_matrix(sd, data)
self.assertAlmostEqual(u.T @ M @ u, 32)
+ def test_assemble_mass_matrix_cosserat_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_bdm1.assemble_mass_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0], [0, 1, 1]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 29.5)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0, "mu_c": 0.25}}}
+ M = vec_bdm1.assemble_mass_matrix_cosserat(sd, data)
+ self.assertAlmostEqual(u.T @ M @ u, 34.5)
+
+ def test_assemble_lumped_matrix_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5}}}
+ M = vec_bdm1.assemble_lumped_matrix(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0], [0, 1, 1]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 27)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0}}}
+ M = vec_bdm1.assemble_lumped_matrix(sd, data)
+ self.assertAlmostEqual(u.T @ M @ u, 32)
+
+ def test_assemble_lumped_matrix_cosserat_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_bdm1.assemble_lumped_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0], [0, 1, 1]])
+ u = vec_bdm1.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 29.5)
+
def test_eval_at_cell_centers_3d(self):
N = 1
sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
pg.convert_from_pp(sd)
sd.compute_geometry()
- key = "vec_bdm1"
+ key = "vecbdm1"
vec_bdm1 = pg.VecBDM1(key)
def linear(x):
@@ -168,21 +309,39 @@ def test_assemble_asym_matrix_3d(self):
pg.convert_from_pp(sd)
sd.compute_geometry()
- key = "vec_bdm1"
+ key = "vecbdm1"
vec_bdm1 = pg.VecBDM1(key)
fun = lambda _: np.array([[1, 2, -1], [4, 3, 2], [1, 1, 1]])
u = vec_bdm1.interpolate(sd, fun)
- asym = vec_bdm1.assemble_asym_matrix(sd)
+ asym = vec_bdm1.assemble_asym_matrix(sd, False)
- p0 = pg.VecPwConstants("p0")
- cell_asym_u = p0.eval_at_cell_centers(sd) @ (asym @ u)
+ p1 = pg.VecPwLinears("p1")
+ cell_asym_u = p1.eval_at_cell_centers(sd) @ (asym @ u)
cell_asym_u = cell_asym_u.reshape((3, -1))
self.assertTrue(np.allclose(cell_asym_u[0], -1))
self.assertTrue(np.allclose(cell_asym_u[1], -2))
self.assertTrue(np.allclose(cell_asym_u[2], 2))
+ def test_proj_to_and_from_rt0_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecbdm1"
+ vec_bdm1 = pg.VecBDM1(key)
+
+ def linear(x):
+ return np.array([x, 2 * x, 3 * x])
+
+ interp = vec_bdm1.interpolate(sd, linear)
+ interp_to_rt0 = vec_bdm1.proj_to_RT0(sd) @ interp
+ interp_from_rt0 = vec_bdm1.proj_from_RT0(sd) @ interp_to_rt0
+
+ self.assertAlmostEqual(np.linalg.norm(interp - interp_from_rt0), 0)
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/unit/test_vecpwconstant.py b/tests/unit/test_vecpwconstant.py
index ee4b0f73..58a74714 100644
--- a/tests/unit/test_vecpwconstant.py
+++ b/tests/unit/test_vecpwconstant.py
@@ -13,7 +13,7 @@ def test_ndof(self):
sd.compute_geometry()
discr = pg.VecPwConstants("P0")
- assert discr.ndof(sd) == sd.num_cells * dim
+ self.assertTrue(discr.ndof(sd) == sd.num_cells * dim)
def test_assemble_mass_matrix(self):
dim = 2
@@ -179,8 +179,13 @@ def test_error_l2(self):
# fmt: on
ana_sol = lambda x: np.sin(x)
+ err = discr.error_l2(sd, num_sol, ana_sol, etype="standard")
+ err_known = 7.951651025069553e-08
+
+ self.assertTrue(np.allclose(err, err_known))
+
err = discr.error_l2(sd, num_sol, ana_sol)
- err_known = 5.622666361455642e-08
+ err_known = 0.5254595621486393
self.assertTrue(np.allclose(err, err_known))
diff --git a/tests/unit/test_vecpwlinear.py b/tests/unit/test_vecpwlinear.py
new file mode 100644
index 00000000..90cdbc1d
--- /dev/null
+++ b/tests/unit/test_vecpwlinear.py
@@ -0,0 +1,167 @@
+import unittest
+import numpy as np
+import scipy.sparse as sps
+
+import pygeon as pg
+import porepy as pp
+
+
+class VecPwLinearsTest(unittest.TestCase):
+ def test_ndof(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([2] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+ self.assertTrue(discr.ndof(sd) == sd.num_cells * dim * (dim + 1))
+
+ def test_assemble_mass_matrix(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+ M = discr.assemble_mass_matrix(sd)
+
+ # fmt: off
+ M_known_data = np.array(
+ [0.08333333, 0.04166667, 0.04166667, 0.04166667, 0.08333333,
+ 0.04166667, 0.04166667, 0.04166667, 0.08333333, 0.08333333,
+ 0.04166667, 0.04166667, 0.04166667, 0.08333333, 0.04166667,
+ 0.04166667, 0.04166667, 0.08333333, 0.08333333, 0.04166667,
+ 0.04166667, 0.04166667, 0.08333333, 0.04166667, 0.04166667,
+ 0.04166667, 0.08333333, 0.08333333, 0.04166667, 0.04166667,
+ 0.04166667, 0.08333333, 0.04166667, 0.04166667, 0.04166667,
+ 0.08333333]
+ )
+
+ M_known_indices = np.array(
+ [ 0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 4, 5, 3, 4, 5, 3, 4,
+ 5, 6, 7, 8, 6, 7, 8, 6, 7, 8, 9, 10, 11, 9, 10, 11, 9,
+ 10, 11]
+ )
+
+ M_known_indptr = np.array(
+ [ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36]
+ )
+ # fmt: on
+
+ self.assertTrue(np.allclose(M.data, M_known_data))
+ self.assertTrue(np.allclose(M.indptr, M_known_indptr))
+ self.assertTrue(np.allclose(M.indices, M_known_indices))
+
+ def test_assemble_lumped_matrix(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ M = discr.assemble_lumped_matrix(sd)
+
+ # fmt: off
+ M_known_data = np.array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) / 6
+
+ M_known_indices = np.array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
+
+ M_known_indptr = np.array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+ # fmt: on
+
+ self.assertTrue(np.allclose(M.data, M_known_data))
+ self.assertTrue(np.allclose(M.indptr, M_known_indptr))
+ self.assertTrue(np.allclose(M.indices, M_known_indices))
+
+ def test_assemble_diff_matrix(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ self.assertRaises(NotImplementedError, discr.assemble_diff_matrix, sd)
+
+ def test_assemble_stiff_matrix(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ self.assertRaises(NotImplementedError, discr.assemble_stiff_matrix, sd)
+
+ def test_interpolate(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ func = lambda x: np.sin(x) # Example function
+ self.assertRaises(NotImplementedError, discr.interpolate, sd, func)
+
+ def test_eval_at_cell_centers(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ P = discr.eval_at_cell_centers(sd)
+
+ # fmt: off
+ P_known_data = np.ones(discr.ndof(sd)) / (sd.dim + 1)
+
+ P_known_indices = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3])
+
+ P_known_indptr = np.arange(discr.ndof(sd)+1)
+ # fmt: on
+
+ self.assertTrue(np.allclose(P.data, P_known_data))
+ self.assertTrue(np.allclose(P.indptr, P_known_indptr))
+ self.assertTrue(np.allclose(P.indices, P_known_indices))
+
+ def test_assemble_nat_bc(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ func = lambda x: np.sin(x[0]) # Example function
+
+ b_faces = np.array([0, 1, 3]) # Example boundary faces
+ vals = discr.assemble_nat_bc(sd, func, b_faces)
+
+ vals_known = np.zeros(discr.ndof(sd))
+
+ self.assertTrue(np.allclose(vals, vals_known))
+
+ def test_get_range_discr_class(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+
+ self.assertRaises(
+ NotImplementedError,
+ discr.get_range_discr_class,
+ dim,
+ )
+
+ def test_error_l2(self):
+ dim = 2
+ sd = pp.StructuredTriangleGrid([1] * dim, [1] * dim)
+ sd.compute_geometry()
+
+ discr = pg.VecPwLinears("P1")
+ # fmt: off
+ num_sol = np.empty(0)
+ # fmt: on
+ ana_sol = lambda x: np.sin(x)
+
+ self.assertRaises(NotImplementedError, discr.error_l2, sd, num_sol, ana_sol)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/unit/test_vecrt0.py b/tests/unit/test_vecrt0.py
new file mode 100644
index 00000000..9b8bbabe
--- /dev/null
+++ b/tests/unit/test_vecrt0.py
@@ -0,0 +1,222 @@
+import unittest
+import numpy as np
+
+import porepy as pp
+import pygeon as pg
+
+
+class VecRT0Test(unittest.TestCase):
+ def test_trace_2d(self):
+ sd = pp.StructuredTriangleGrid([1] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ vec_rt0 = pg.VecRT0("vecrt0")
+
+ B = vec_rt0.assemble_trace_matrix(sd)
+
+ fun = lambda _: np.array([[1, 2, 0], [3, 4, 0]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ trace = B @ u
+
+ self.assertTrue(np.allclose(trace, 5))
+
+ def test_ndof_2d(self):
+ sd = pp.StructuredTriangleGrid([1] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ vec_rt0 = pg.VecRT0("vecrt0")
+
+ self.assertEqual(vec_rt0.ndof(sd), 10)
+
+ def test_assemble_mass_matrix_2d(self):
+ N = 10
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5}}}
+ M = vec_rt0.assemble_mass_matrix(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 26)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0}}}
+ M = vec_rt0.assemble_mass_matrix(sd, data)
+ self.assertAlmostEqual(u.T @ M @ u, 30)
+
+ def test_assemble_mass_matrix_cosserat_2d(self):
+ N = 10
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_rt0.assemble_mass_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 28)
+
+ def test_eval_at_cell_centers_2d(self):
+ N = 1
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ def linear(x):
+ return np.array([x, 2 * x])
+
+ interp = vec_rt0.interpolate(sd, linear)
+ eval = vec_rt0.eval_at_cell_centers(sd) @ interp
+ eval = np.reshape(eval, (3, -1), order="F")
+ eval = np.vstack(np.split(eval, sd.dim, axis=1))
+
+ known = np.array([linear(x).ravel() for x in sd.cell_centers.T]).T
+
+ self.assertAlmostEqual(np.linalg.norm(eval - known), 0)
+
+ def test_range(self):
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+ self.assertTrue(vec_rt0.get_range_discr_class(2) is pg.VecPwConstants)
+
+ def test_assemble_asym_matrix_2d(self):
+ N = 1
+ sd = pp.StructuredTriangleGrid([N] * 2, [1] * 2)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0]])
+ u = vec_rt0.interpolate(sd, fun)
+ asym = vec_rt0.assemble_asym_matrix(sd)
+
+ p0 = pg.PwConstants("p0")
+ cell_asym_u = p0.eval_at_cell_centers(sd) @ (asym @ u)
+
+ self.assertTrue(np.allclose(cell_asym_u, 2))
+
+ def test_trace_3d(self):
+ sd = pp.StructuredTetrahedralGrid([1] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ vec_rt0 = pg.VecRT0("vecrt0")
+
+ B = vec_rt0.assemble_trace_matrix(sd)
+
+ fun = lambda _: np.array([[1, 2, 3], [4, 5, 6], [0, 0, 7]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ trace = B @ u
+
+ self.assertTrue(np.allclose(trace, 13))
+
+ def test_ndof_3d(self):
+ sd = pp.StructuredTetrahedralGrid([1] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ vec_rt0 = pg.VecRT0("vecrt0")
+
+ self.assertEqual(vec_rt0.ndof(sd), 54)
+
+ def test_assemble_mass_matrix_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5}}}
+ M = vec_rt0.assemble_mass_matrix(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0], [0, 1, 1]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 27)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0}}}
+ M = vec_rt0.assemble_mass_matrix(sd, data)
+ self.assertAlmostEqual(u.T @ M @ u, 32)
+
+ def test_assemble_mass_matrix_cosserat_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ data = {pp.PARAMETERS: {key: {"mu": 0.5, "lambda": 0.5, "mu_c": 0.25}}}
+ M = vec_rt0.assemble_mass_matrix_cosserat(sd, data)
+
+ fun = lambda _: np.array([[1, 2, 0], [4, 3, 0], [0, 1, 1]])
+ u = vec_rt0.interpolate(sd, fun)
+
+ self.assertAlmostEqual(u.T @ M @ u, 29.5)
+
+ def test_eval_at_cell_centers_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ def linear(x):
+ return np.array([x, 2 * x, -x])
+
+ interp = vec_rt0.interpolate(sd, linear)
+ eval = vec_rt0.eval_at_cell_centers(sd) @ interp
+ eval = np.reshape(eval, (3, -1), order="F")
+ eval = np.vstack(np.split(eval, sd.dim, axis=1))
+
+ known = np.array([linear(x).ravel() for x in sd.cell_centers.T]).T
+ self.assertAlmostEqual(np.linalg.norm(eval - known), 0)
+
+ def test_assemble_asym_matrix_3d(self):
+ N = 1
+ sd = pp.StructuredTetrahedralGrid([N] * 3, [1] * 3)
+ pg.convert_from_pp(sd)
+ sd.compute_geometry()
+
+ key = "vecrt0"
+ vec_rt0 = pg.VecRT0(key)
+
+ fun = lambda _: np.array([[1, 2, -1], [4, 3, 2], [1, 1, 1]])
+ u = vec_rt0.interpolate(sd, fun)
+ asym = vec_rt0.assemble_asym_matrix(sd)
+
+ p0 = pg.VecPwConstants("p0")
+ cell_asym_u = p0.eval_at_cell_centers(sd) @ (asym @ u)
+ cell_asym_u = cell_asym_u.reshape((3, -1))
+
+ self.assertTrue(np.allclose(cell_asym_u[0], -1))
+ self.assertTrue(np.allclose(cell_asym_u[1], -2))
+ self.assertTrue(np.allclose(cell_asym_u[2], 2))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tutorials/cosserat.ipynb b/tutorials/cosserat.ipynb
new file mode 100644
index 00000000..dff6017f
--- /dev/null
+++ b/tutorials/cosserat.ipynb
@@ -0,0 +1,310 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "essential-american",
+ "metadata": {},
+ "source": [
+ "# Cosserat equation in mixed form\n",
+ "\n",
+ "In this tutorial we present how to solve the Cosserat equation with [PyGeoN](https://github.com/compgeo-mox/pygeon) in mixed form. The unknowns are the stress $\\sigma$, the micro-stress $\\omega$, the displacement $u$, and the rotation $r$.\n",
+ "\n",
+ "Let $\\Omega=(0,1)^2$ with boundary $\\partial \\Omega$ and outward unit normal ${\\nu}$. Given the macroscopic \n",
+ "$\\lambda_\\sigma$ Lamé constant and $\\mu_\\sigma$ the Kirchhoff modulus and their microscopic version $\\lambda_\\omega$ and $\\mu_\\omega$, \n",
+ "we want to solve the following problem: find $(\\sigma, \\omega, u, r)$ such that\n",
+ "\\begin{align*}\n",
+ "&A_\\sigma \\sigma - \\epsilon(u) + {\\rm asym}^* r = 0\\\\\n",
+ "&A_\\omega \\omega - \\nabla r = 0\\\\\n",
+ "&\\nabla \\cdot \\sigma = - b\\\\\n",
+ "&{\\rm asym}\\, \\sigma - \\nabla \\cdot \\omega = 0\n",
+ "\\end{align*}\n",
+ "with $\\epsilon$ the symmetric gradient and $b$ a body force, which is set to $0$ in this example.\n",
+ "The operator $A_\\sigma$ is defined as\n",
+ "$$\n",
+ "A_\\sigma \\sigma = \\frac1{2\\mu_\\sigma} \\left( \\mathrm{sym} \\sigma - \\frac{\\lambda_\\sigma}{2\\mu_\\sigma + d \\lambda_\\sigma} \\mathrm{Tr} (\\sigma) I\\right) + \\frac1{2\\mu_\\sigma^c} \\mathrm{skw} \\sigma\n",
+ "$$\n",
+ "with $d$ the dimension of the domain, here $d=2$. While the operator $A_\\omega$ depends on the dimension \n",
+ "and is defined as \n",
+ "$$\n",
+ "A_\\omega \\omega = \\frac1{2\\mu_\\omega} \\left( \\mathrm{sym} \\omega - \\frac{\\lambda_\\omega}{2\\mu_\\omega + d \\lambda_\\omega} \\mathrm{Tr} (\\omega) I\\right) + \\frac1{2\\mu_\\omega^c} \\mathrm{skw} \\omega\n",
+ " \\qquad d = 3\\\\\n",
+ "A_\\omega \\omega = \\frac{1}{2\\mu_\\omega}\\omega \\qquad d = 2\n",
+ "$$\n",
+ "Finally, ${\\rm asym}$ is the asymmetric operator, which is defined as followed\n",
+ "$$\n",
+ " {\\rm asym }\\, \\sigma = \\begin{bmatrix}\n",
+ " \\sigma_{32} - \\sigma_{23} \\\\\n",
+ " \\sigma_{13} - \\sigma_{31} \\\\\n",
+ " \\sigma_{21} - \\sigma_{12}\n",
+ " \\end{bmatrix} \n",
+ " \\qquad\n",
+ " {\\rm asym}^* r =\n",
+ " \\begin{bmatrix}\n",
+ " 0 & -r_3 & r_2 \\\\\n",
+ " r_3 & 0 & -r_1 \\\\\n",
+ " -r_2 & r_1 & 0\n",
+ " \\end{bmatrix}\n",
+ " \\qquad\n",
+ " d = 3\\\\\n",
+ " {\\rm asym} \\, \\sigma = \\sigma_{21} - \\sigma_{12} \\qquad\n",
+ " {\\rm asym}^* r =\n",
+ " \\begin{bmatrix}\n",
+ " 0 & -r \\\\\n",
+ " r & 0\n",
+ " \\end{bmatrix} \\qquad\n",
+ " d = 2.\n",
+ "$$\n",
+ "For this test case we set the following boundary conditions related to the so-called footstep problem:\n",
+ "$$ u = 0 \\text{ on } \\partial_{bottom} \\Omega \\qquad \\nu \\cdot \\sigma = [0, 0]^\\top \\text{ on } \\partial_{left} \\Omega \\cup \\partial_{right} \\Omega \\qquad \\nu \\cdot \\sigma = [0, -1]^\\top \\text{ on } \\partial_{top} \\Omega$$\n",
+ "\n",
+ "We present *step-by-step* how to create the grid, declare the problem data, and finally solve the problem."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "planned-danger",
+ "metadata": {},
+ "source": [
+ "First we import some of the standard modules, like `numpy` and `scipy.sparse`. Since PyGeoN is based on [PorePy](https://github.com/pmgbergen/porepy) we import both modules."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "dietary-perth",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import scipy.sparse as sps\n",
+ "\n",
+ "import porepy as pp\n",
+ "import pygeon as pg"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "roman-glossary",
+ "metadata": {},
+ "source": [
+ "We create now the grid, since we use vector RT0 for ${\\sigma}$, RT0 for $\\omega$, vector piece-wise constant for $u$ and piece-wise constant for $r$, we are restricted to simplices. In this example we consider a 2-dimensional structured grid, but the presented code will work also in 3d with the only attention that in 3d the micro-stress is a vector RT0 and the rotations is a vector piece-wise constant."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "spectacular-saturn",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "mesh_size = 0.05\n",
+ "dim = 2\n",
+ "\n",
+ "sd = pg.unit_grid(dim, mesh_size, as_mdg=False)\n",
+ "sd.compute_geometry()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d625fca2",
+ "metadata": {},
+ "source": [
+ "We define now the finite element spaces for all the variables."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "52400a88",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "key = \"cosserat\"\n",
+ "\n",
+ "# finite element spaces\n",
+ "vec_rt0 = pg.VecRT0(key)\n",
+ "rt0 = pg.RT0(key)\n",
+ "vec_p0 = pg.VecPwConstants(key)\n",
+ "p0 = pg.PwConstants(key)\n",
+ "\n",
+ "# build the degrees of freedom\n",
+ "dofs = np.array([vec_rt0.ndof(sd), rt0.ndof(sd), vec_p0.ndof(sd), p0.ndof(sd)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "precious-belle",
+ "metadata": {},
+ "source": [
+ "With the following code we set the data, in particular the Lamé and the Kirchhoff modulus, and the boundary conditions. Since we need to identify each side of $\\partial \\Omega$ we need few steps."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "spare-person",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# data for sigma\n",
+ "data_sigma = {pp.PARAMETERS: {key: {\"lambda\": 1, \"mu\": 0.5, \"mu_c\": 0.25}}}\n",
+ "\n",
+ "# data for omega\n",
+ "sot = pp.SecondOrderTensor(np.ones(sd.num_cells))\n",
+ "data_omega = {pp.PARAMETERS: {key: {\"second_order_tensor\": sot}}}\n",
+ "\n",
+ "# select the portions of the boundary where we want to apply the boundary conditions\n",
+ "bottom = np.isclose(sd.face_centers[1, :], 0)\n",
+ "top = np.isclose(sd.face_centers[1, :], 1)\n",
+ "left = np.isclose(sd.face_centers[0, :], 0)\n",
+ "right = np.isclose(sd.face_centers[0, :], 1)\n",
+ "\n",
+ "# select the part of the boundary where we apply the essential boundary conditions\n",
+ "b_faces = np.logical_or.reduce((top, left, right))\n",
+ "ess_dof = np.tile(b_faces, sd.dim)\n",
+ "\n",
+ "# function for the essential boundary conditions\n",
+ "val = np.array([[0, 0, 0], [0, -1, 0]])\n",
+ "fct = lambda pt: val if np.isclose(pt[1], 1) else 0 * val\n",
+ "\n",
+ "# interpolate the essential boundary conditions\n",
+ "ess_val = vec_rt0.interpolate(sd, fct)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "secure-flesh",
+ "metadata": {},
+ "source": [
+ "Once the data are assigned to the grid, we construct the matrices. Once the latter is created, we also construct the right-hand side containing the boundary conditions."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "romance-findings",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# assemble mass matrices\n",
+ "Ms = vec_rt0.assemble_mass_matrix_cosserat(sd, data_sigma)\n",
+ "Mw = rt0.assemble_mass_matrix(sd, data_omega)\n",
+ "Mu = vec_p0.assemble_mass_matrix(sd)\n",
+ "Mr = p0.assemble_mass_matrix(sd)\n",
+ "\n",
+ "# assemble matrices associated with the divergence and asymmetry operators\n",
+ "div_s = Mu @ vec_rt0.assemble_diff_matrix(sd)\n",
+ "asym = Mr @ vec_rt0.assemble_asym_matrix(sd)\n",
+ "div_w = Mr @ rt0.assemble_diff_matrix(sd)\n",
+ "\n",
+ "# assemble the saddle point problem matrix\n",
+ "# fmt: off\n",
+ "A = sps.block_diag([Ms, Mw])\n",
+ "B = sps.bmat([[-div_s, None],\n",
+ " [ asym, -div_w]])\n",
+ "spp = sps.bmat([[A, -B.T],\n",
+ " [B, None]], format=\"csc\")\n",
+ "# fmt: on"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mobile-nirvana",
+ "metadata": {},
+ "source": [
+ "We need to solve the linear system, PyGeoN provides a framework for that. The actual imposition of essential boundary conditions (stress boundary conditions) might change the symmetry of the global system, the class `pg.LinearSystem` preserves this structure by internally eliminating these degrees of freedom."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "subtle-wonder",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# solve the linear system\n",
+ "ls = pg.LinearSystem(spp, np.zeros(spp.shape[0]))\n",
+ "ls.flag_ess_bc(np.where(ess_dof)[0], ess_val)\n",
+ "x = ls.solve()\n",
+ "\n",
+ "# split the solution into the components\n",
+ "idx = np.cumsum(dofs[:-1])\n",
+ "sigma, omega, u, r = np.split(x, idx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "pacific-alpha",
+ "metadata": {},
+ "source": [
+ "We finally export the solution to be visualized by [ParaView](https://www.paraview.org/)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "satisfactory-jerusalem",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# post process of the displacement\n",
+ "proj_u = vec_p0.eval_at_cell_centers(sd)\n",
+ "cell_u = (proj_u @ u).reshape((sd.dim, -1), order=\"C\")\n",
+ "# since we are in 2d we need to add the z component for the exporting\n",
+ "cell_u = np.vstack((cell_u, np.zeros(cell_u.shape[1])))\n",
+ "\n",
+ "# post process of the rotation\n",
+ "proj_r = p0.eval_at_cell_centers(sd)\n",
+ "cell_r = proj_r @ r\n",
+ "\n",
+ "save = pp.Exporter(sd, \"sol\", folder_name=key)\n",
+ "save.write_vtu([(\"cell_u\", cell_u), (\"cell_r\", cell_r)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "developing-mobile",
+ "metadata": {},
+ "source": [
+ "A representation of the computed solution is given below.
\n",
+ "![](fig/cosserat.png)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.4"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "e4cc1db98167c7fd7d55a1da8057731abc6cd6fe154328a2ae319df8aab4e24d"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/elasticity_mixed.ipynb b/tutorials/elasticity_mixed.ipynb
new file mode 100644
index 00000000..40ec2f36
--- /dev/null
+++ b/tutorials/elasticity_mixed.ipynb
@@ -0,0 +1,294 @@
+{
+ "cells": [
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "essential-american",
+ "metadata": {},
+ "source": [
+ "# Elasticity equation in mixed form\n",
+ "\n",
+ "In this tutorial we present how to solve the elasticity equation with [PyGeoN](https://github.com/compgeo-mox/pygeon) in mixed form. The unknowns are the stress $\\sigma$, the displacement $u$, and the rotation $r$.\n",
+ "\n",
+ "Let $\\Omega=(0,1)^2$ with boundary $\\partial \\Omega$ and outward unit normal ${\\nu}$. Given \n",
+ "$\\lambda$ Lamé constant and $\\mu$ the Kirchhoff modulus, we want to solve the following problem: find $(\\sigma, u, r)$ such that\n",
+ "\\begin{align*}\n",
+ "&A \\sigma - \\epsilon(u) - {\\rm asym}^* r = 0\\\\\n",
+ "&\\nabla \\cdot \\sigma = - b\\\\\n",
+ "&{\\rm asym}\\, \\sigma = 0\n",
+ "\\end{align*}\n",
+ "with $\\epsilon$ the symmetric gradient and $b$ a body force, which is set to $0$ in this example.\n",
+ "The operator $A$ is defined as\n",
+ "$$\n",
+ "A \\sigma = \\frac{1}{2\\mu}\\left( \\sigma -\\frac{\\lambda}{2\\mu + d \\lambda} {\\rm Tr}(\\sigma) I\\right)\n",
+ "$$\n",
+ "with $d$ the dimension of the domain, here $d=2$.\n",
+ "Finally, ${\\rm asym}$ is the asymmetric operator, which is defined as followed\n",
+ "$$\n",
+ " {\\rm asym }\\, \\sigma = \\begin{bmatrix}\n",
+ " \\sigma_{32} - \\sigma_{23} \\\\\n",
+ " \\sigma_{13} - \\sigma_{31} \\\\\n",
+ " \\sigma_{21} - \\sigma_{12}\n",
+ " \\end{bmatrix} \n",
+ " \\qquad\n",
+ " {\\rm asym}^* r =\n",
+ " \\begin{bmatrix}\n",
+ " 0 & -r_3 & r_2 \\\\\n",
+ " r_3 & 0 & -r_1 \\\\\n",
+ " -r_2 & r_1 & 0\n",
+ " \\end{bmatrix}\n",
+ " \\qquad\n",
+ " d = 3\\\\\n",
+ " {\\rm asym} \\, \\sigma = \\sigma_{21} - \\sigma_{12} \\qquad\n",
+ " {\\rm asym}^* r =\n",
+ " \\begin{bmatrix}\n",
+ " 0 & -r \\\\\n",
+ " r & 0\n",
+ " \\end{bmatrix} \\qquad\n",
+ " d = 2.\n",
+ "$$\n",
+ "For this test case we set the following boundary conditions related to the so-called footstep problem:\n",
+ "$$ u = 0 \\text{ on } \\partial_{bottom} \\Omega \\qquad \\nu \\cdot \\sigma = [0, 0]^\\top \\text{ on } \\partial_{left} \\Omega \\cup \\partial_{right} \\Omega \\qquad \\nu \\cdot \\sigma = [0, -1]^\\top \\text{ on } \\partial_{top} \\Omega$$\n",
+ "\n",
+ "We present *step-by-step* how to create the grid, declare the problem data, and finally solve the problem."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "planned-danger",
+ "metadata": {},
+ "source": [
+ "First we import some of the standard modules, like `numpy` and `scipy.sparse`. Since PyGeoN is based on [PorePy](https://github.com/pmgbergen/porepy) we import both modules."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "dietary-perth",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import scipy.sparse as sps\n",
+ "\n",
+ "import porepy as pp\n",
+ "import pygeon as pg"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "roman-glossary",
+ "metadata": {},
+ "source": [
+ "We create now the grid, since we use vector BDM1 for ${\\sigma}$, vector piece-wise constant for $u$ and piece-wise constant for $r$, we are restricted to simplices. In this example we consider a 2-dimensional structured grid, but the presented code will work also in 3d with the only attention that in 3d the rotations should be discretized with vector piece-wise constant finite elements."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "spectacular-saturn",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "mesh_size = 0.05\n",
+ "dim = 2\n",
+ "\n",
+ "sd = pg.unit_grid(dim, mesh_size, as_mdg=False)\n",
+ "sd.compute_geometry()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d625fca2",
+ "metadata": {},
+ "source": [
+ "We define now the finite element spaces for all the variables."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "52400a88",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "key = \"elasticity\"\n",
+ "\n",
+ "# finite element spaces\n",
+ "vec_bdm1 = pg.VecBDM1(key)\n",
+ "vec_p0 = pg.VecPwConstants(key)\n",
+ "p0 = pg.PwConstants(key)\n",
+ "\n",
+ "# build the degrees of freedom\n",
+ "dofs = np.array([vec_bdm1.ndof(sd), vec_p0.ndof(sd), p0.ndof(sd)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "precious-belle",
+ "metadata": {},
+ "source": [
+ "With the following code we set the data, in particular the Lamé and the Kirchhoff modulus, and the boundary conditions. Since we need to identify each side of $\\partial \\Omega$ we need few steps."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "spare-person",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data = {pp.PARAMETERS: {key: {\"lambda\": 1, \"mu\": 0.5}}}\n",
+ "\n",
+ "# select the portions of the boundary where we want to apply the boundary conditions\n",
+ "bottom = np.isclose(sd.face_centers[1, :], 0)\n",
+ "top = np.isclose(sd.face_centers[1, :], 1)\n",
+ "left = np.isclose(sd.face_centers[0, :], 0)\n",
+ "right = np.isclose(sd.face_centers[0, :], 1)\n",
+ "\n",
+ "# select the part of the boundary where we apply the essential boundary conditions\n",
+ "b_faces = np.logical_or.reduce((top, left, right))\n",
+ "ess_dof = np.tile(b_faces, sd.dim**2)\n",
+ "\n",
+ "# function for the essential boundary conditions\n",
+ "val = np.array([[0, 0, 0], [0, -1, 0]])\n",
+ "fct = lambda pt: val if np.isclose(pt[1], 1) else 0 * val\n",
+ "\n",
+ "# interpolate the essential boundary conditions\n",
+ "ess_val = vec_bdm1.interpolate(sd, fct)"
+ ]
+ },
+ {
+ "attachments": {},
+ "cell_type": "markdown",
+ "id": "secure-flesh",
+ "metadata": {},
+ "source": [
+ "Once the data are assigned to the grid, we construct the matrices. Once the latter is created, we also construct the right-hand side containing the boundary conditions."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "romance-findings",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# assemble mass matrices\n",
+ "Ms = vec_bdm1.assemble_mass_matrix(sd, data)\n",
+ "Mu = vec_p0.assemble_mass_matrix(sd)\n",
+ "Mr = p0.assemble_mass_matrix(sd)\n",
+ "\n",
+ "# assemble matrices associated with the divergence and asymmetry operators\n",
+ "div = Mu @ vec_bdm1.assemble_diff_matrix(sd)\n",
+ "asym = Mr @ vec_bdm1.assemble_asym_matrix(sd)\n",
+ "\n",
+ "# assemble the saddle point problem matrix\n",
+ "# fmt: off\n",
+ "spp = sps.bmat([[ Ms, div.T, -asym.T],\n",
+ " [-div, None, None],\n",
+ " [asym, None, None]], format = \"csc\")\n",
+ "# fmt: on"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "mobile-nirvana",
+ "metadata": {},
+ "source": [
+ "We need to solve the linear system, PyGeoN provides a framework for that. The actual imposition of essential boundary conditions (stress boundary conditions) might change the symmetry of the global system, the class `pg.LinearSystem` preserves this structure by internally eliminating these degrees of freedom."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "subtle-wonder",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# solve the linear system\n",
+ "ls = pg.LinearSystem(spp, np.zeros(spp.shape[0]))\n",
+ "ls.flag_ess_bc(np.where(ess_dof)[0], ess_val)\n",
+ "x = ls.solve()\n",
+ "\n",
+ "# split the solution into the components\n",
+ "idx = np.cumsum(dofs[:-1])\n",
+ "sigma, u, r = np.split(x, idx)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "pacific-alpha",
+ "metadata": {},
+ "source": [
+ "We finally export the solution to be visualized by [ParaView](https://www.paraview.org/)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "satisfactory-jerusalem",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# post process of the displacement\n",
+ "proj_u = vec_p0.eval_at_cell_centers(sd)\n",
+ "cell_u = (proj_u @ u).reshape((sd.dim, -1), order=\"C\")\n",
+ "# since we are in 2d we need to add the z component for the exporting\n",
+ "cell_u = np.vstack((cell_u, np.zeros(cell_u.shape[1])))\n",
+ "\n",
+ "# post process of the rotation\n",
+ "proj_r = p0.eval_at_cell_centers(sd)\n",
+ "cell_r = proj_r @ r\n",
+ "\n",
+ "save = pp.Exporter(sd, \"sol\")\n",
+ "save.write_vtu([(\"cell_u\", cell_u), (\"cell_r\", cell_r)])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "developing-mobile",
+ "metadata": {},
+ "source": [
+ "A representation of the computed solution is given below.
\n",
+ "![](fig/elasticity_mixed.png)\n",
+ "
\n",
+ "![](fig/elasticity_mixed1.png)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.1"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "e4cc1db98167c7fd7d55a1da8057731abc6cd6fe154328a2ae319df8aab4e24d"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/tutorials/fig/cosserat.png b/tutorials/fig/cosserat.png
new file mode 100644
index 00000000..1d764e3a
Binary files /dev/null and b/tutorials/fig/cosserat.png differ