Skip to content

Commit c4d82ce

Browse files
authored
Implementing the choice of the matrix inversion algorithm for the user (#51)
* Added a block inversion algorithm for numerical case * Matrix inversion algorithm is now configurable * Preparing for v1.0.6 beta release
1 parent 1ed4583 commit c4d82ce

6 files changed

Lines changed: 117 additions & 47 deletions

File tree

documentation/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
project = 'LayerCake'
2020
copyright = '2025-2026, Jonathan Demaeyer and Oisín Hamilton'
2121
author = 'Jonathan Demaeyer and Oisín Hamilton'
22-
release = 'v1.0.5-beta'
22+
release = 'v1.0.6-beta'
2323
version = release
2424

2525
# -- General configuration ---------------------------------------------------

layercake/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
'OperatorTerm', 'ProductOfTerms', 'AdditionOfTerms', 'LinearTerm', 'ConstantTerm', 'Equation', 'Laplacian', 'D',
1414
'Layer', 'Cake']
1515

16-
__version__ = '1.0.5b0'
16+
__version__ = '1.0.6b0'

layercake/bakery/cake.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self):
5555
self.layers = list()
5656
self._lhs_inversion_in_layer = True
5757
self._simplify_after_LHS_inversion = True
58+
self._matrix_inv = np.linalg.inv
5859

5960
def add_layer(self, layer):
6061
"""Add a layer object to the cake.
@@ -65,6 +66,7 @@ def add_layer(self, layer):
6566
Layer object to add to the cake.
6667
"""
6768
layer._cake_order = len(self.layers)
69+
layer._matrix_inv = self._matrix_inv
6870
self.layers.append(layer)
6971
layer._cake = self
7072
for equation in layer.equations:
@@ -262,7 +264,7 @@ def tensor(self):
262264
if not self._lhs_inversion_in_layer:
263265
try:
264266
lhs_mat_inverted = np.zeros((self.ndim + 1, self.ndim + 1))
265-
lhs_mat_inverted[1:, 1:] = np.linalg.inv(lhs_mat.todense()[1:, 1:])
267+
lhs_mat_inverted[1:, 1:] = self._matrix_inv(lhs_mat.todense()[1:, 1:])
266268
tensor = sp.COO(np.tensordot(lhs_mat_inverted, tensor.to_coo(), 1))
267269
except LinAlgError:
268270
raise LinAlgError(f'The left-hand side of the cake is not invertible with the provided basis.')

layercake/bakery/layers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(self, name=''):
6363
self._lhs_inverted = False
6464
self._lhs_mat = None
6565
self._simplify_after_LHS_inversion = True
66+
self._matrix_inv = np.linalg.inv
6667

6768
@property
6869
def _cake_first_index(self):
@@ -298,7 +299,7 @@ def compute_tensor(self, numerical=True, compute_inner_products=False, compute_i
298299
self._lhs_mat[lhs_order:lhs_order + ndim, ofield_order:ofield_order + ondim] + lhs_term.inner_products.todense()
299300
else:
300301
try:
301-
lhs_mat_inverted[lhs_order:lhs_order + ndim, lhs_order:lhs_order + ndim] = np.linalg.inv(eq.lhs_inner_products_addition.todense())
302+
lhs_mat_inverted[lhs_order:lhs_order + ndim, lhs_order:lhs_order + ndim] = self._matrix_inv(eq.lhs_inner_products_addition.todense())
302303
self._lhs_inverted = True
303304
except LinAlgError:
304305
raise LinAlgError(f'The left-hand side of the equation {eq} is not invertible with the provided basis.')
@@ -345,7 +346,7 @@ def compute_tensor(self, numerical=True, compute_inner_products=False, compute_i
345346
lhs_order += ndim
346347
if self._lhs_inversion and not self._lhs_inverted:
347348
try:
348-
lhs_mat_inverted[1:, 1:] = np.linalg.inv(self._lhs_mat.todense()[1:, 1:])
349+
lhs_mat_inverted[1:, 1:] = self._matrix_inv(self._lhs_mat.todense()[1:, 1:])
349350
self._lhs_inverted = True
350351
except LinAlgError:
351352
raise LinAlgError(f'The left-hand side of the layer {self} is not invertible with the provided basis.')

layercake/utils/matrix.py

Lines changed: 108 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,105 @@
99
"""
1010
import warnings
1111

12+
import numpy as np
13+
from numpy.linalg import LinAlgError
14+
1215
from sympy import MutableSparseMatrix
1316
from sympy.matrices.exceptions import NonInvertibleMatrixError
1417

1518
def block_matrix_inverse(P, blocks_extent, simplify=True):
16-
"""Function to invert a symbolic matrix :math:`P` divided by blocks.
19+
"""Function to invert a symbolic or numeric matrix :math:`P` divided by blocks.
1720
1821
Parameters
1922
----------
20-
P: ~sympy.matrices.immutable.ImmutableSparseMatrix or ~sympy.matrices.mutable.MutableSparseMatrix
23+
P: ~sympy.matrices.immutable.ImmutableSparseMatrix or ~sympy.matrices.mutable.MutableSparseMatrix or ~numpy.ndarray
2124
The block matrix to invert.
2225
blocks_extent: list(tuple)
2326
The extent of each block, as a list of 2-tuple.
2427
simplify: bool, optional
2528
Try to simplify the inverse.
29+
Only applies to symbolic matrices.
2630
Default to `True`.
2731
2832
Warnings
2933
--------
30-
If the `simplify` argument is set to `False`, this function will not check if the
34+
In the symbolic case, if the `simplify` argument is set to `False`, this function will not check if the
3135
matrix :math:`P` is invertible !
3236
3337
"""
34-
if simplify:
35-
if P.det().simplify() == 0:
36-
raise NonInvertibleMatrixError('block_matrix_inverse: The provided matrix is not invertible.')
37-
else:
38-
warnings.warn(f'Inverting a symbolic matrix without checking that it is invertible. '
39-
'Be cautious about the result.')
40-
be = blocks_extent.copy()
41-
PP = P
42-
B_list = list()
43-
C_list = list()
44-
Am1_list = list()
45-
while len(be) >= 2:
46-
rest_block_extent = (be[1][0], be[-1][1])
47-
Ainv, B, C, D, PPP = _block_matrix_inverse_2x2(PP, (be[0], rest_block_extent))
48-
49-
B_list.append(B)
50-
C_list.append(C)
51-
Am1_list.append(Ainv)
52-
53-
PP = PPP
54-
ret = be[0][1]
55-
be = [(p[0]-ret, p[1]-ret) for p in be[1:]]
56-
57-
PP = PP.adjugate() / PP.det()
58-
for Am1, B, C in zip(Am1_list[::-1], B_list[::-1], C_list[::-1]):
59-
Ashape = Am1.shape[0]
60-
Pshape = PP.shape[0]
61-
mat_shape = Ashape + Pshape
62-
Pp1 = MutableSparseMatrix(mat_shape, mat_shape, {})
63-
Pp1[slice(0, Ashape), slice(0, Ashape)] = Am1 + Am1 @ B @ PP @ C @ Am1
64-
Pp1[slice(0, Ashape), slice(Ashape, mat_shape)] = - Am1 @ B @ PP
65-
Pp1[slice(Ashape, mat_shape), slice(0, Ashape)] = - PP @ C @ Am1
66-
Pp1[slice(Ashape, mat_shape), slice(Ashape, mat_shape)] = PP
67-
PP = Pp1
68-
69-
if simplify:
70-
return PP.simplify()
71-
else:
38+
if isinstance(P, np.ndarray):
39+
if np.linalg.det(P) == 0:
40+
raise LinAlgError('block_matrix_inverse: The provided matrix is not invertible.')
41+
be = blocks_extent.copy()
42+
PP = P
43+
B_list = list()
44+
C_list = list()
45+
Am1_list = list()
46+
while len(be) >= 2:
47+
rest_block_extent = (be[1][0], be[-1][1])
48+
Ainv, B, C, D, PPP = _block_matrix_inverse_2x2_num(PP, (be[0], rest_block_extent))
49+
50+
B_list.append(B)
51+
C_list.append(C)
52+
Am1_list.append(Ainv)
53+
54+
PP = PPP
55+
ret = be[0][1]
56+
be = [(p[0] - ret, p[1] - ret) for p in be[1:]]
57+
58+
PP = np.linalg.inv(PP)
59+
for Am1, B, C in zip(Am1_list[::-1], B_list[::-1], C_list[::-1]):
60+
Ashape = Am1.shape[0]
61+
Pshape = PP.shape[0]
62+
mat_shape = Ashape + Pshape
63+
Pp1 = np.zeros((mat_shape, mat_shape))
64+
Pp1[slice(0, Ashape), slice(0, Ashape)] = Am1 + Am1 @ B @ PP @ C @ Am1
65+
Pp1[slice(0, Ashape), slice(Ashape, mat_shape)] = - Am1 @ B @ PP
66+
Pp1[slice(Ashape, mat_shape), slice(0, Ashape)] = - PP @ C @ Am1
67+
Pp1[slice(Ashape, mat_shape), slice(Ashape, mat_shape)] = PP
68+
PP = Pp1
69+
7270
return PP
71+
else:
72+
if simplify:
73+
if P.det().simplify() == 0:
74+
raise NonInvertibleMatrixError('block_matrix_inverse: The provided matrix is not invertible.')
75+
else:
76+
warnings.warn(f'Inverting a symbolic matrix without checking that it is invertible. '
77+
'Be cautious about the result.')
78+
be = blocks_extent.copy()
79+
PP = P
80+
B_list = list()
81+
C_list = list()
82+
Am1_list = list()
83+
while len(be) >= 2:
84+
rest_block_extent = (be[1][0], be[-1][1])
85+
Ainv, B, C, D, PPP = _block_matrix_inverse_2x2(PP, (be[0], rest_block_extent))
86+
87+
B_list.append(B)
88+
C_list.append(C)
89+
Am1_list.append(Ainv)
90+
91+
PP = PPP
92+
ret = be[0][1]
93+
be = [(p[0]-ret, p[1]-ret) for p in be[1:]]
94+
95+
PP = PP.adjugate() / PP.det()
96+
for Am1, B, C in zip(Am1_list[::-1], B_list[::-1], C_list[::-1]):
97+
Ashape = Am1.shape[0]
98+
Pshape = PP.shape[0]
99+
mat_shape = Ashape + Pshape
100+
Pp1 = MutableSparseMatrix(mat_shape, mat_shape, {})
101+
Pp1[slice(0, Ashape), slice(0, Ashape)] = Am1 + Am1 @ B @ PP @ C @ Am1
102+
Pp1[slice(0, Ashape), slice(Ashape, mat_shape)] = - Am1 @ B @ PP
103+
Pp1[slice(Ashape, mat_shape), slice(0, Ashape)] = - PP @ C @ Am1
104+
Pp1[slice(Ashape, mat_shape), slice(Ashape, mat_shape)] = PP
105+
PP = Pp1
106+
107+
if simplify:
108+
return PP.simplify()
109+
else:
110+
return PP
73111

74112
def _block_matrix_inverse_2x2(P, blocks_extent):
75113
be = blocks_extent
@@ -81,6 +119,16 @@ def _block_matrix_inverse_2x2(P, blocks_extent):
81119

82120
return Ainv, B, C, D, D - C @ Ainv @ B
83121

122+
def _block_matrix_inverse_2x2_num(P, blocks_extent):
123+
be = blocks_extent
124+
A = P[slice(*be[0]), slice(*be[0])]
125+
B = P[slice(*be[0]), slice(*be[1])]
126+
C = P[slice(*be[1]), slice(*be[0])]
127+
D = P[slice(*be[1]), slice(*be[1])]
128+
Ainv = np.linalg.inv(A)
129+
130+
return Ainv, B, C, D, D - C @ Ainv @ B
131+
84132

85133
if __name__ == '__main__':
86134
A = MutableSparseMatrix(3, 3, {})
@@ -102,3 +150,22 @@ def _block_matrix_inverse_2x2(P, blocks_extent):
102150
bl = [(0, 3), (3, 6), (6, 9)]
103151
Ginv = block_matrix_inverse(G, bl)
104152

153+
A = np.zeros((3, 3))
154+
A[0,0] = 1
155+
A[1,1] = 3
156+
A[2,2] = 2
157+
C = A /2
158+
D = 3 * A
159+
G = np.zeros((9, 9))
160+
G[:3, :3] = A
161+
G[:3, 3:6] = -C
162+
G[:3, 6:] = D
163+
G[3:6, :3] = C
164+
G[3:6, 3:6] = C
165+
G[3:6, 6:] = - D
166+
G[6:, :3] = - C
167+
G[6:, 3:6] = A
168+
G[6:, 6:] = D
169+
bl = [(0, 3), (3, 6), (6, 9)]
170+
Ginv_num = block_matrix_inverse(G, bl)
171+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires = ["setuptools", "wheel"]
77
[project]
88
requires-python = ">=3.10"
99
name = "layercake_model"
10-
version = "1.0.5-beta"
10+
version = "1.0.6-beta"
1111
description = "A framework to design systems of partial differential equations (PDEs), and convert them to ordinary differential equations (ODEs) via Galerkin-type expansions. "
1212
readme = "README.md"
1313
authors = [

0 commit comments

Comments
 (0)