Skip to content

Commit 8220bef

Browse files
authored
Merge pull request #1346 from keileg/matrix_slicer
Introduce matrix slicer objects
2 parents 74e2c7c + f0d3caf commit 8220bef

File tree

17 files changed

+1324
-270
lines changed

17 files changed

+1324
-270
lines changed

src/porepy/applications/test_utils/arrays.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import numpy as np
44
import scipy.sparse as sps
55

6+
import porepy as pp
7+
68

79
def compare_arrays(
810
a: np.ndarray, b: np.ndarray, tol: float = 1e-4, sort: bool = True
@@ -68,3 +70,37 @@ def compare_matrices(m1: sps.spmatrix, m2: sps.spmatrix, tol: float = 1e-10) ->
6870
if np.max(np.abs(d.data)) > tol:
6971
return False
7072
return True
73+
74+
75+
def projection_matrix_from_array_slicers(
76+
slicers: pp.matrix_operations.ArraySlicer | list[pp.matrix_operations.ArraySlicer],
77+
dim: int,
78+
) -> sps.coo_matrix:
79+
"""Recover a projection matrix from a one or multiple matrix slicers.
80+
81+
For a set of slicers {P_1, P_2, ..., P_n}, obtain the matrix P that corresponds to
82+
83+
P = P_1 + P_2 + ... + P_n.
84+
85+
Parameters:
86+
slicers: One or more ArraySlicers.
87+
dim: Dimension of the domain space of the slicer.
88+
89+
Returns:
90+
Matrix representation of the basis vector.
91+
92+
"""
93+
# Always deal with a list of slicers.
94+
if isinstance(slicers, pp.matrix_operations.ArraySlicer):
95+
slicers = [slicers]
96+
97+
# Initialize result.
98+
result = None
99+
100+
for slicer in slicers:
101+
if result is None:
102+
result = slicer @ np.eye(dim)
103+
else:
104+
result += slicer @ np.eye(dim)
105+
106+
return sps.coo_matrix(result)

src/porepy/fracs/split_grid.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def _update_face_cells(
445445
if j == i:
446446
# We hit the target neighbor.
447447
# Pick out the part of f_c to be used with this neighbor.
448-
f_c_sliced = pp.matrix_operations.slice_mat(f_c, face_id)
448+
f_c_sliced = pp.matrix_operations.slice_sparse_matrix(f_c, face_id)
449449
# The new face-cell relations are added to the end of the matrix (since
450450
# the faces were added to the end of the face arrays in the
451451
# higher-dimensional grid). Columns (face-indices in the higher
@@ -1075,14 +1075,10 @@ def _find_cell_color(sd: pp.Grid, cells: np.ndarray) -> np.ndarray:
10751075
c = np.sort(cells)
10761076
# Local cell-face and face-node maps.
10771077
assert sd.cell_faces.getformat() == "csc"
1078-
cell_faces = pp.matrix_operations.slice_mat(sd.cell_faces, c)
1078+
cell_faces = pp.matrix_operations.slice_sparse_matrix(sd.cell_faces, c)
10791079
child_cell_ind = -np.ones(sd.num_cells, dtype=int)
10801080
child_cell_ind[c] = np.arange(cell_faces.shape[1])
10811081

1082-
# Create a copy of the cell-face relation, so that we can modify it at will.
1083-
# RB: I don't think this is necessary as slice_mat creates a copy cell_faces =
1084-
# cf_sub.copy()
1085-
10861082
# Direction of normal vector does not matter here, only 0s and 1s
10871083
cell_faces.data = np.abs(cell_faces.data)
10881084

src/porepy/grids/partition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def _extract_submatrix(
525525
"""
526526
if mat.format != "csc":
527527
raise ValueError("To extract columns from a matrix, it must be csc")
528-
sub_mat = pp.matrix_operations.slice_mat(mat, ind)
528+
sub_mat = pp.matrix_operations.slice_sparse_matrix(mat, ind)
529529
cols = sub_mat.indptr
530530
data = sub_mat.data
531531
unique_rows, rows_sub = np.unique(sub_mat.indices, return_inverse=True)

src/porepy/models/constitutive_laws.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def elastic_displacement_jump(self, subdomains: list[pp.Grid]) -> pp.ad.Operator
9696
# respectively, to nd dimensions.
9797
basis = self.basis(subdomains, dim=self.nd)
9898
local_basis = self.basis(subdomains, dim=self.nd - 1)
99-
tangential_to_nd = pp.ad.sum_operator_list(
99+
tangential_to_nd = pp.ad.sum_projection_list(
100100
[e_nd @ e_f.T for e_nd, e_f in zip(basis[:-1], local_basis)]
101101
)
102102
normal_to_nd = basis[-1]
@@ -1117,7 +1117,7 @@ def interface_vector_source_darcy_flux(
11171117
# composed by summation).
11181118
normals_times_source = normals * vector_source
11191119
# Then sum over the nd dimensions. The result will in effect be a matrix.
1120-
nd_to_scalar_sum = pp.ad.sum_operator_list(
1120+
nd_to_scalar_sum = pp.ad.sum_projection_list(
11211121
[e.T for e in self.basis(interfaces, dim=self.nd)]
11221122
)
11231123
# Finally, the dot product between normal vectors and the vector source. This
@@ -3104,9 +3104,7 @@ def fracture_pressure_stress(
31043104

31053105
# Expands from cell-wise scalar to vector. Equivalent to the :math:`\mathbf{I}p`
31063106
# operation.
3107-
scalar_to_nd = pp.ad.sum_operator_list(
3108-
[e_i for e_i in self.basis(interfaces, dim=self.nd)]
3109-
)
3107+
scalar_to_nd = pp.ad.sum_projection_list(self.basis(interfaces, dim=self.nd))
31103108
# Spelled out, from the right: Project the pressure from the fracture to the
31113109
# mortar, expand to an nd-vector, and multiply with the outwards normal vector.
31123110
stress = outwards_normal * (

src/porepy/models/contact_mechanics.py

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,12 @@ def tangential_fracture_deformation_equation(
181181
tangential_basis = self.basis(subdomains, dim=self.nd - 1)
182182

183183
# To map a scalar to the tangential plane, we need to sum the basis vectors. The
184-
# individual basis functions have shape (Nc * (self.nd - 1), Nc), where Nc is
185-
# the total number of cells in the subdomain. The sum will have the same shape,
186-
# but the row corresponding to each cell will be non-zero in all rows
187-
# corresponding to the tangential basis vectors of this cell. EK: mypy insists
188-
# that the argument to sum should be a list of booleans. Ignore this error.
189-
scalar_to_tangential = pp.ad.sum_operator_list(
190-
[e_i for e_i in tangential_basis]
191-
)
184+
# individual basis vectors can be represented as projection matrices of shape
185+
# (Nc * (self.nd - 1), Nc), where Nc is the total number of cells in the
186+
# subdomain. The matrix representation of the sum has the same shape, but the
187+
# row corresponding to each cell will be non-zero in all rows corresponding to
188+
# the tangential basis vectors of this cell.
189+
scalar_to_tangential = pp.ad.sum_projection_list(tangential_basis)
192190

193191
# Variables: The tangential component of the contact traction and the plastic
194192
# displacement jump.
@@ -207,39 +205,28 @@ def tangential_fracture_deformation_equation(
207205
f_norm = pp.ad.Function(partial(pp.ad.l2_norm, self.nd - 1), "norm_function")
208206

209207
# The numerical constant is used to loosen the sensitivity in the transition
210-
# between sticking and sliding.
211-
# Expanding using only left multiplication to with scalar_to_tangential does not
212-
# work for an array, unlike the operators below. Arrays need right
213-
# multiplication as well.
208+
# between sticking and sliding. Expanding using only left multiplication to with
209+
# scalar_to_tangential does not work for an array, unlike the operators below.
210+
# Arrays need right multiplication as well.
214211
c_num_as_scalar = self.contact_mechanics_numerical_constant(subdomains)
215212

216-
# The numerical parameter is a cell-wise scalar which must be extended to a
217-
# vector quantity to be used in the equation (multiplied from the right).
218-
# Spelled out, from the right: Restrict the vector quantity to one dimension in
219-
# the tangential plane (e_i.T), multiply with the numerical parameter, prolong
220-
# to the full vector quantity (e_i), and sum over all all directions in the
221-
# tangential plane.
222-
c_num = pp.ad.sum_operator_list(
223-
[e_i * c_num_as_scalar * e_i.T for e_i in tangential_basis]
224-
)
225-
226-
# Combine the above into expressions that enter the equation. c_num will
227-
# effectively be a sum of SparseArrays, thus we use a matrix-vector product @
228-
tangential_sum = t_t + c_num @ u_t_increment
213+
# The numerical parameter is a cell-wise scalar, or a single scalar common for
214+
# all cells. In both cases, it must be extended to a vector quantity to be used
215+
# in the equation (multiplied from the right). Do this by multiplying with the
216+
# sum of the tangential basis vectors. Then take a Hadamard product with the
217+
# tangential displacement jump and add to the tangential component of the
218+
# contact traction to arrive at the expression that enters the equation.
219+
basis_sum = pp.ad.sum_projection_list(tangential_basis)
220+
tangential_sum = t_t + (basis_sum @ c_num_as_scalar) * u_t_increment
229221

230222
norm_tangential_sum = f_norm(tangential_sum)
231223
norm_tangential_sum.set_name("norm_tangential")
232224

233225
b_p = f_max(self.friction_bound(subdomains), zeros_frac)
234226
b_p.set_name("bp")
235227

236-
# Remove parentheses to make the equation more readable if possible. The product
237-
# between (the SparseArray) scalar_to_tangential and b_p is of matrix-vector
238-
# type (thus @), and the result is then multiplied elementwise with
239-
# tangential_sum.
240228
bp_tang = (scalar_to_tangential @ b_p) * tangential_sum
241229

242-
# For the use of @, see previous comment.
243230
maxbp_abs = scalar_to_tangential @ f_max(b_p, norm_tangential_sum)
244231

245232
# The characteristic function below reads "1 if (abs(b_p) < tol) else 0".
@@ -538,13 +525,12 @@ def contact_mechanics_open_state_characteristic(
538525
tangential_basis = self.basis(subdomains, dim=self.nd - 1)
539526

540527
# To map a scalar to the tangential plane, we need to sum the basis vectors. The
541-
# individual basis functions have shape (Nc * (self.nd - 1), Nc), where Nc is
542-
# the total number of cells in the subdomain. The sum will have the same shape,
543-
# but the row corresponding to each cell will be non-zero in all rows
544-
# corresponding to the tangential basis vectors of this cell.
545-
scalar_to_tangential = pp.ad.sum_operator_list(
546-
[e_i for e_i in tangential_basis]
547-
)
528+
# individual basis functions can be represented as projection matrices of size
529+
# (Nc * (self.nd - 1), Nc), where Nc is # the total number of cells in the
530+
# subdomain. The sum of basis vectors can likewise be represented as a matrix of
531+
# the same shape, but the row corresponding to each cell will be non-zero in all
532+
# rows corresponding to the tangential basis vectors of this cell.
533+
scalar_to_tangential = pp.ad.sum_projection_list(tangential_basis)
548534

549535
# With the active set method, the performance of the Newton solver is sensitive
550536
# to changes in state between sticking and sliding. To reduce the sensitivity to

src/porepy/models/geometry.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,13 @@ def wrap_grid_attribute(
278278
array.set_name(f"Array wrapping attribute {attr} on {len(grids)} grids.")
279279
return array
280280

281-
def basis(self, grids: Sequence[pp.GridLike], dim: int) -> list[pp.ad.SparseArray]:
281+
def basis(self, grids: Sequence[pp.GridLike], dim: int) -> list[pp.ad.Projection]:
282282
"""Return a cell-wise basis for all subdomains.
283283
284-
The basis is represented as a list of matrices, each of which represents a
285-
basis function. The individual matrices have shape ``Nc * dim, Nc`` where
286-
``Nc`` is the total number of cells in the subdomains.
284+
The basis is represented as a list of projections, each of which represents a
285+
basis function. The individiual basis functions can be represented as a
286+
projection matrix, of shape ``Nc * dim, Nc`` where ``Nc`` is the total number of
287+
cells in the subdomains.
287288
288289
Examples:
289290
To extend a cell-wise scalar to a vector field, use
@@ -307,32 +308,33 @@ def basis(self, grids: Sequence[pp.GridLike], dim: int) -> list[pp.ad.SparseArra
307308
function.
308309
309310
"""
310-
# Collect the basis functions for each dimension
311-
basis: list[pp.ad.SparseArray] = []
311+
# Collect the basis functions for each dimension.
312+
basis: list[pp.ad.Projection] = []
312313
for i in range(dim):
313314
basis.append(self.e_i(grids, i=i, dim=dim))
314-
# Stack the basis functions horizontally
315+
# Stack the basis functions horizontally.
315316
return basis
316317

317318
def e_i(
318319
self, grids: Sequence[pp.GridLike], *, i: int, dim: int
319-
) -> pp.ad.SparseArray:
320+
) -> pp.ad.Projection:
320321
"""Return a cell-wise basis function in a specified dimension.
321322
322323
It is assumed that the grids are embedded in a space of dimension dim and
323324
aligned with the coordinate axes, that is, the reference space of the grid.
324325
Moreover, the grid is assumed to be planar.
325326
326327
Example:
327-
For a grid with two cells, and with `i=1` and `dim=3`, the returned
328-
basis will be (after conversion to a numpy array)
328+
For a grid with two cells, and with `i=1` and `dim=3`, the returned basis
329+
will be a Projection that is equivalent to applying the following projection
330+
matrix:
329331
.. code-block:: python
330332
array([[0., 0.],
331-
[1., 0.],
332-
[0., 0.],
333-
[0., 0.],
334-
[0., 1.],
335-
[0., 0.]])
333+
[1., 0.],
334+
[0., 0.],
335+
[0., 0.],
336+
[0., 1.],
337+
[0., 0.]])
336338
337339
See also:
338340
:meth:`basis` for the construction of a full basis.
@@ -343,16 +345,12 @@ def e_i(
343345
dim: Dimension of the functions.
344346
345347
Returns:
346-
pp.ad.SparseArray: Ad representation of a matrix with the basis
347-
functions as columns.
348+
Ad projection that represents a basis function.
348349
349350
Raises:
350351
ValueError: If i is larger than dim - 1.
351352
352353
"""
353-
# TODO: Should we expand this to grids not aligned with the coordinate axes, and
354-
# possibly unify with ``porepy.utils.projections.TangentialNormalProjection``?
355-
# This is not a priority for the moment, though.
356354

357355
if dim is None:
358356
dim = self.nd
@@ -361,15 +359,18 @@ def e_i(
361359
if i >= dim:
362360
raise ValueError("Basis function index out of range")
363361

364-
# Construct a single vector, and later stack it to a matrix
365-
# Collect the basis functions for each dimension
366-
e_i = np.zeros((dim, 1))
367-
e_i[i] = 1
368362
# Expand to cell-wise column vectors.
369363
num_cells = sum([g.num_cells for g in grids])
370-
# Expand to a matrix.
371-
mat = sps.kron(sps.eye(num_cells), e_i)
372-
return pp.ad.SparseArray(mat)
364+
range_ind = np.arange(i, dim * num_cells, dim)
365+
366+
slicer = pp.ad.Projection(
367+
domain_indices=np.arange(num_cells),
368+
range_indices=range_ind,
369+
range_size=num_cells * dim,
370+
domain_size=num_cells,
371+
)
372+
373+
return slicer
373374

374375
# Local basis related methods
375376
def tangential_component(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
@@ -394,7 +395,7 @@ def tangential_component(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
394395
# it in the tangential basis. The two operations are combined in a single
395396
# operator composed right to left: v will be hit by first e_i.T (row vector) and
396397
# secondly t_i (column vector).
397-
op: pp.ad.Operator = pp.ad.sum_operator_list(
398+
op: pp.ad.Operator = pp.ad.sum_projection_list(
398399
[
399400
self.e_i(subdomains, i=i, dim=self.nd - 1)
400401
@ self.e_i(subdomains, i=i, dim=self.nd).T
@@ -404,7 +405,7 @@ def tangential_component(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
404405
op.set_name("tangential_component")
405406
return op
406407

407-
def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.SparseArray:
408+
def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.Projection:
408409
"""Compute the normal component of a vector field.
409410
410411
The normal space is defined according to the local coordinates of the
@@ -421,8 +422,8 @@ def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.SparseArray:
421422
subdomains: List of grids on which the vector field is defined.
422423
423424
Returns:
424-
Matrix extracting normal component of the vector field and expressing it
425-
in normal basis. The size of the matrix is `(Nc, Nc * self.nd)`, where
425+
Projection extracting normal component of the vector field and expressing it
426+
in normal basis. The size of the projection is `(Nc, Nc * self.nd)`, where
426427
`Nc` is the total number of cells in the subdomains.
427428
428429
"""

src/porepy/models/protocol.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def wrap_grid_attribute(
198198

199199
def basis(
200200
self, grids: Sequence[pp.GridLike], dim: int
201-
) -> list[pp.ad.SparseArray]:
201+
) -> list[pp.ad.Projection]:
202202
"""Return a cell-wise basis for all subdomains.
203203
204204
The basis is represented as a list of matrices, each of which represents a
@@ -230,7 +230,7 @@ def basis(
230230

231231
def e_i(
232232
self, grids: Sequence[pp.GridLike], *, i: int, dim: int
233-
) -> pp.ad.SparseArray:
233+
) -> pp.ad.Projection:
234234
"""Return a cell-wise basis function in a specified dimension.
235235
236236
It is assumed that the grids are embedded in a space of dimension dim and
@@ -257,8 +257,7 @@ def e_i(
257257
dim: Dimension of the functions.
258258
259259
Returns:
260-
pp.ad.SparseArray: Ad representation of a matrix with the basis
261-
functions as columns.
260+
Ad projection that represents a basis function.
262261
263262
Raises:
264263
ValueError: If i is larger than dim - 1.
@@ -283,7 +282,7 @@ def tangential_component(self, subdomains: list[pp.Grid]) -> pp.ad.Operator:
283282
284283
"""
285284

286-
def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.SparseArray:
285+
def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.Projection:
287286
"""Compute the normal component of a vector field.
288287
289288
The normal space is defined according to the local coordinates of the
@@ -300,9 +299,9 @@ def normal_component(self, subdomains: list[pp.Grid]) -> pp.ad.SparseArray:
300299
subdomains: List of grids on which the vector field is defined.
301300
302301
Returns:
303-
Matrix extracting normal component of the vector field and expressing it
304-
in normal basis. The size of the matrix is `(Nc, Nc * self.nd)`, where
305-
`Nc` is the total number of cells in the subdomains.
302+
Projection extracting normal component of the vector field and
303+
expressing it in normal basis. The size of the projection is `(Nc, Nc *
304+
self.nd)`, where `Nc` is the total number of cells in the subdomains.
306305
307306
"""
308307

0 commit comments

Comments
 (0)