capytaine 2.3.1__cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- capytaine/__about__.py +16 -0
- capytaine/__init__.py +36 -0
- capytaine/bem/__init__.py +0 -0
- capytaine/bem/airy_waves.py +111 -0
- capytaine/bem/engines.py +441 -0
- capytaine/bem/problems_and_results.py +600 -0
- capytaine/bem/solver.py +594 -0
- capytaine/bodies/__init__.py +4 -0
- capytaine/bodies/bodies.py +1221 -0
- capytaine/bodies/dofs.py +19 -0
- capytaine/bodies/predefined/__init__.py +6 -0
- capytaine/bodies/predefined/cylinders.py +151 -0
- capytaine/bodies/predefined/rectangles.py +111 -0
- capytaine/bodies/predefined/spheres.py +70 -0
- capytaine/green_functions/FinGreen3D/.gitignore +1 -0
- capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
- capytaine/green_functions/FinGreen3D/LICENSE +165 -0
- capytaine/green_functions/FinGreen3D/Makefile +16 -0
- capytaine/green_functions/FinGreen3D/README.md +24 -0
- capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
- capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
- capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
- capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
- capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
- capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
- capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
- capytaine/green_functions/__init__.py +2 -0
- capytaine/green_functions/abstract_green_function.py +64 -0
- capytaine/green_functions/delhommeau.py +507 -0
- capytaine/green_functions/hams.py +204 -0
- capytaine/green_functions/libs/Delhommeau_float32.cpython-313-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/Delhommeau_float64.cpython-313-x86_64-linux-gnu.so +0 -0
- capytaine/green_functions/libs/__init__.py +0 -0
- capytaine/io/__init__.py +0 -0
- capytaine/io/bemio.py +153 -0
- capytaine/io/legacy.py +328 -0
- capytaine/io/mesh_loaders.py +1086 -0
- capytaine/io/mesh_writers.py +692 -0
- capytaine/io/meshio.py +38 -0
- capytaine/io/wamit.py +479 -0
- capytaine/io/xarray.py +668 -0
- capytaine/matrices/__init__.py +16 -0
- capytaine/matrices/block.py +592 -0
- capytaine/matrices/block_toeplitz.py +325 -0
- capytaine/matrices/builders.py +89 -0
- capytaine/matrices/linear_solvers.py +232 -0
- capytaine/matrices/low_rank.py +395 -0
- capytaine/meshes/__init__.py +6 -0
- capytaine/meshes/clipper.py +465 -0
- capytaine/meshes/collections.py +342 -0
- capytaine/meshes/geometry.py +409 -0
- capytaine/meshes/mesh_like_protocol.py +37 -0
- capytaine/meshes/meshes.py +890 -0
- capytaine/meshes/predefined/__init__.py +6 -0
- capytaine/meshes/predefined/cylinders.py +314 -0
- capytaine/meshes/predefined/rectangles.py +261 -0
- capytaine/meshes/predefined/spheres.py +62 -0
- capytaine/meshes/properties.py +276 -0
- capytaine/meshes/quadratures.py +80 -0
- capytaine/meshes/quality.py +448 -0
- capytaine/meshes/surface_integrals.py +63 -0
- capytaine/meshes/symmetric.py +462 -0
- capytaine/post_pro/__init__.py +6 -0
- capytaine/post_pro/free_surfaces.py +88 -0
- capytaine/post_pro/impedance.py +92 -0
- capytaine/post_pro/kochin.py +54 -0
- capytaine/post_pro/rao.py +60 -0
- capytaine/tools/__init__.py +0 -0
- capytaine/tools/cache_on_disk.py +26 -0
- capytaine/tools/deprecation_handling.py +18 -0
- capytaine/tools/lists_of_points.py +52 -0
- capytaine/tools/lru_cache.py +49 -0
- capytaine/tools/optional_imports.py +27 -0
- capytaine/tools/prony_decomposition.py +150 -0
- capytaine/tools/symbolic_multiplication.py +149 -0
- capytaine/tools/timer.py +66 -0
- capytaine/ui/__init__.py +0 -0
- capytaine/ui/cli.py +28 -0
- capytaine/ui/rich.py +5 -0
- capytaine/ui/vtk/__init__.py +3 -0
- capytaine/ui/vtk/animation.py +329 -0
- capytaine/ui/vtk/body_viewer.py +28 -0
- capytaine/ui/vtk/helpers.py +82 -0
- capytaine/ui/vtk/mesh_viewer.py +461 -0
- capytaine-2.3.1.dist-info/LICENSE +674 -0
- capytaine-2.3.1.dist-info/METADATA +750 -0
- capytaine-2.3.1.dist-info/RECORD +93 -0
- capytaine-2.3.1.dist-info/WHEEL +6 -0
- capytaine-2.3.1.dist-info/entry_points.txt +3 -0
- capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
- capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
- capytaine.libs/libmvec-2-583a17db.28.so +0 -0
- capytaine.libs/libquadmath-2284e583.so.0.0.0 +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"""This module implements block matrices to be used in Hierarchical Toeplitz matrices.
|
|
2
|
+
|
|
3
|
+
It takes inspiration from the following works:
|
|
4
|
+
|
|
5
|
+
* `openHmx module from Gypsilab by Matthieu Aussal (GPL licensed) <https://github.com/matthieuaussal/gypsilab>`_
|
|
6
|
+
* `HierarchicalMatrices by Markus Neumann (GPL licensed) <https://github.com/maekke97/HierarchicalMatrices>`_
|
|
7
|
+
"""
|
|
8
|
+
# Copyright (C) 2017-2019 Matthieu Ancellin
|
|
9
|
+
# See LICENSE file at <https://github.com/mancellin/capytaine>
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from numbers import Number
|
|
14
|
+
from typing import Tuple, List, Callable, Union, Iterable
|
|
15
|
+
from itertools import cycle, accumulate, chain, product
|
|
16
|
+
from collections.abc import Iterator
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from capytaine.matrices.low_rank import LowRankMatrix
|
|
21
|
+
from capytaine.tools.optional_imports import import_optional_dependency
|
|
22
|
+
|
|
23
|
+
LOG = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BlockMatrix:
|
|
27
|
+
"""A (2D) matrix, stored as a set of submatrices (or blocks).
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
blocks: list of list of matrices
|
|
32
|
+
The blocks of the block matrix.
|
|
33
|
+
check: bool, optional
|
|
34
|
+
Should the code perform sanity checks on the inputs? (default: True)
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
shape: pair of ints
|
|
39
|
+
shape of the full matrix
|
|
40
|
+
nb_blocks: pair of ints
|
|
41
|
+
number of blocks in each directions.
|
|
42
|
+
Example::
|
|
43
|
+
|
|
44
|
+
AAAABB
|
|
45
|
+
AAAABB -> nb_blocks = (1, 2)
|
|
46
|
+
AAAABB
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
ndim = 2 # Other dimensions have not been implemented.
|
|
50
|
+
|
|
51
|
+
def __init__(self, blocks, _stored_block_shapes=None, check=True):
|
|
52
|
+
|
|
53
|
+
self._stored_nb_blocks = (len(blocks), len(blocks[0]))
|
|
54
|
+
|
|
55
|
+
self._stored_blocks = np.empty(self._stored_nb_blocks, dtype=object)
|
|
56
|
+
for i in range(len(blocks)):
|
|
57
|
+
for j in range(len(blocks[i])):
|
|
58
|
+
self._stored_blocks[i, j] = blocks[i][j]
|
|
59
|
+
|
|
60
|
+
if _stored_block_shapes is None:
|
|
61
|
+
self._stored_block_shapes = ([block.shape[0] for block in self._stored_blocks[:, 0]],
|
|
62
|
+
[block.shape[1] for block in self._stored_blocks[0, :]])
|
|
63
|
+
else:
|
|
64
|
+
# To avoid going down the tree if it is already known.
|
|
65
|
+
self._stored_block_shapes = _stored_block_shapes
|
|
66
|
+
|
|
67
|
+
# Block shape of the full matrix
|
|
68
|
+
self.nb_blocks = self._compute_nb_blocks()
|
|
69
|
+
|
|
70
|
+
# Total shape of the full matrix
|
|
71
|
+
self.shape = self._compute_shape()
|
|
72
|
+
|
|
73
|
+
LOG.debug(f"New block matrix: %s", self)
|
|
74
|
+
|
|
75
|
+
if check:
|
|
76
|
+
assert self._check_dimensions_of_blocks()
|
|
77
|
+
assert self._check_dtype()
|
|
78
|
+
|
|
79
|
+
def _compute_shape(self):
|
|
80
|
+
# In a dedicated routine because it will be overloaded by subclasses.
|
|
81
|
+
return sum(self._stored_block_shapes[0]), sum(self._stored_block_shapes[1])
|
|
82
|
+
|
|
83
|
+
def _compute_nb_blocks(self):
|
|
84
|
+
return self._stored_nb_blocks
|
|
85
|
+
|
|
86
|
+
def _check_dimensions_of_blocks(self) -> bool:
|
|
87
|
+
"""Check that the dimensions of the blocks are consistent."""
|
|
88
|
+
if not all(block.ndim == self.ndim for line in self._stored_blocks for block in line):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
for line in self.all_blocks:
|
|
92
|
+
block_height = line[0].shape[0]
|
|
93
|
+
for block in line[1:]:
|
|
94
|
+
if not block.shape[0] == block_height: # Same height on a given line
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
for col in np.moveaxis(self.all_blocks, 1, 0):
|
|
98
|
+
block_width = col[0].shape[1]
|
|
99
|
+
for block in col[1:]:
|
|
100
|
+
if not block.shape[1] == block_width: # Same width on a given column
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _check_dtype(self) -> bool:
|
|
105
|
+
"""Check that the type of the blocks are consistent."""
|
|
106
|
+
for line in self._stored_blocks:
|
|
107
|
+
for block in line:
|
|
108
|
+
if block.dtype != self.dtype:
|
|
109
|
+
return False
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
# ACCESSING DATA
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def dtype(self):
|
|
116
|
+
"""The type of data of all of the subblocks."""
|
|
117
|
+
try:
|
|
118
|
+
return self._stored_blocks[0][0].dtype
|
|
119
|
+
except AttributeError:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def all_blocks(self) -> np.ndarray:
|
|
124
|
+
"""The matrix of matrices. For a full block matrix, all the blocks are stored in memory."""
|
|
125
|
+
return self._stored_blocks
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def block_shapes(self) -> Tuple[List[int], List[int]]:
|
|
129
|
+
"""The shapes of the blocks composing the BlockMatrix.
|
|
130
|
+
|
|
131
|
+
Example::
|
|
132
|
+
|
|
133
|
+
AAAABB
|
|
134
|
+
AAAABB -> block_shapes = ([3], [4, 2])
|
|
135
|
+
AAAABB
|
|
136
|
+
"""
|
|
137
|
+
return self._stored_block_shapes
|
|
138
|
+
|
|
139
|
+
def _stored_block_positions(self, global_frame=(0, 0)) -> Iterable[List[Tuple[int, int]]]:
|
|
140
|
+
"""The position of each blocks in the matrix as a generator.
|
|
141
|
+
The list is used by subclasses where the same block may appear several times in different positions.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
global_frame: Tuple[int], optional
|
|
146
|
+
the coordinate of the top right corner. Default: 0, 0.
|
|
147
|
+
|
|
148
|
+
Example::
|
|
149
|
+
|
|
150
|
+
AAAABB
|
|
151
|
+
AAAABB -> list(matrix._stored_block_positions) = [[(0,0)], [(0, 4)], [(2, 0)], [(2, 4)]]
|
|
152
|
+
CCCCDD
|
|
153
|
+
"""
|
|
154
|
+
x_acc = accumulate([0] + self.block_shapes[0][:-1])
|
|
155
|
+
y_acc = accumulate([0] + self.block_shapes[1][:-1])
|
|
156
|
+
return ([(global_frame[0] + x, global_frame[1] + y)] for x, y in product(x_acc, y_acc))
|
|
157
|
+
|
|
158
|
+
def _put_in_full_matrix(self, full_matrix: np.ndarray, where=(0, 0)) -> None:
|
|
159
|
+
"""Copy the content of the block matrix in a matrix, which is modified in place."""
|
|
160
|
+
all_blocks_in_flat_iterator = (block for line in self._stored_blocks for block in line)
|
|
161
|
+
positions_of_all_blocks = self._stored_block_positions(global_frame=where)
|
|
162
|
+
for block, positions_of_the_block in zip(all_blocks_in_flat_iterator, positions_of_all_blocks):
|
|
163
|
+
if isinstance(block, BlockMatrix):
|
|
164
|
+
position_of_first_appearance = positions_of_the_block[0]
|
|
165
|
+
frame_of_first_appearance = (slice(position_of_first_appearance[0], position_of_first_appearance[0]+block.shape[0]),
|
|
166
|
+
slice(position_of_first_appearance[1], position_of_first_appearance[1]+block.shape[1]))
|
|
167
|
+
block._put_in_full_matrix(full_matrix, where=position_of_first_appearance)
|
|
168
|
+
|
|
169
|
+
for position in positions_of_the_block[1:]: # For the other appearances, only copy the first appearance
|
|
170
|
+
block_frame = (slice(position[0], position[0]+block.shape[0]),
|
|
171
|
+
slice(position[1], position[1]+block.shape[1]))
|
|
172
|
+
full_matrix[block_frame] = full_matrix[frame_of_first_appearance]
|
|
173
|
+
|
|
174
|
+
else:
|
|
175
|
+
full_block = block if isinstance(block, np.ndarray) else block.full_matrix()
|
|
176
|
+
for position in positions_of_the_block:
|
|
177
|
+
block_frame = (slice(position[0], position[0]+block.shape[0]),
|
|
178
|
+
slice(position[1], position[1]+block.shape[1]))
|
|
179
|
+
full_matrix[block_frame] = full_block
|
|
180
|
+
|
|
181
|
+
def full_matrix(self, dtype=None) -> np.ndarray:
|
|
182
|
+
"""Flatten the block structure and return a full matrix."""
|
|
183
|
+
if dtype is None: dtype = self.dtype
|
|
184
|
+
full_matrix = np.empty(self.shape, dtype=dtype)
|
|
185
|
+
self._put_in_full_matrix(full_matrix)
|
|
186
|
+
return full_matrix
|
|
187
|
+
|
|
188
|
+
def __array__(self, dtype=None, copy=True):
|
|
189
|
+
if not copy:
|
|
190
|
+
raise ValueError("Making an ndarray out of a BlockMatrix requires copy")
|
|
191
|
+
return self.full_matrix(dtype=dtype)
|
|
192
|
+
|
|
193
|
+
def no_toeplitz(self):
|
|
194
|
+
"""Recursively replace the block toeplitz matrices by usual block matrices.
|
|
195
|
+
WARNING: the block matrices may still contain several references to the same block."""
|
|
196
|
+
blocks = [[block.no_toeplitz() if isinstance(block, BlockMatrix) else block for block in line] for line in self.all_blocks]
|
|
197
|
+
return BlockMatrix(blocks)
|
|
198
|
+
|
|
199
|
+
def __deepcopy__(self, memo):
|
|
200
|
+
from copy import deepcopy
|
|
201
|
+
blocks = [[deepcopy(block) for block in line] for line in self._stored_blocks]
|
|
202
|
+
# The call to deepcopy does not use the memo on purpose:
|
|
203
|
+
# the goal is to replace references to the same block by references to different copies of the block.
|
|
204
|
+
return self.__class__(blocks)
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def stored_data_size(self):
|
|
208
|
+
"""Return the number of entries actually stored in memory."""
|
|
209
|
+
size = 0
|
|
210
|
+
for line in self._stored_blocks:
|
|
211
|
+
for block in line:
|
|
212
|
+
if isinstance(block, np.ndarray):
|
|
213
|
+
size += np.prod(block.shape)
|
|
214
|
+
else:
|
|
215
|
+
size += block.stored_data_size
|
|
216
|
+
return size
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def density(self):
|
|
220
|
+
return self.stored_data_size/np.prod(self.shape)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def sparcity(self):
|
|
224
|
+
return 1 - self.density
|
|
225
|
+
|
|
226
|
+
def __hash__(self):
|
|
227
|
+
# Temporary
|
|
228
|
+
return id(self)
|
|
229
|
+
|
|
230
|
+
# TRANSFORMING DATA
|
|
231
|
+
|
|
232
|
+
def _apply_unary_op(self, op: Callable) -> 'BlockMatrix':
|
|
233
|
+
"""Helper function applying a function recursively on all submatrices."""
|
|
234
|
+
LOG.debug(f"Apply op {op.__name__} to {self}")
|
|
235
|
+
result = [[op(block) for block in line] for line in self._stored_blocks]
|
|
236
|
+
return self.__class__(result, _stored_block_shapes=self._stored_block_shapes, check=False)
|
|
237
|
+
|
|
238
|
+
def _apply_binary_op(self, op: Callable, other: 'BlockMatrix') -> 'BlockMatrix':
|
|
239
|
+
"""Helper function applying a binary operator recursively on all submatrices."""
|
|
240
|
+
if isinstance(other, self.__class__) and self.nb_blocks == other.nb_blocks:
|
|
241
|
+
LOG.debug(f"Apply op {op.__name__} to {self} and {other}")
|
|
242
|
+
result = [
|
|
243
|
+
[op(block, other_block) for block, other_block in zip(line, other_line)]
|
|
244
|
+
for line, other_line in zip(self._stored_blocks, other._stored_blocks)
|
|
245
|
+
]
|
|
246
|
+
return self.__class__(result, _stored_block_shapes=self._stored_block_shapes, check=False)
|
|
247
|
+
else:
|
|
248
|
+
return NotImplemented
|
|
249
|
+
|
|
250
|
+
def __add__(self, other: 'BlockMatrix') -> 'BlockMatrix':
|
|
251
|
+
from operator import add
|
|
252
|
+
return self._apply_binary_op(add, other)
|
|
253
|
+
|
|
254
|
+
def __radd__(self, other: 'BlockMatrix') -> 'BlockMatrix':
|
|
255
|
+
return self + other
|
|
256
|
+
|
|
257
|
+
def __neg__(self) -> 'BlockMatrix':
|
|
258
|
+
from operator import neg
|
|
259
|
+
return self._apply_unary_op(neg)
|
|
260
|
+
|
|
261
|
+
def __sub__(self, other: 'BlockMatrix') -> 'BlockMatrix':
|
|
262
|
+
from operator import sub
|
|
263
|
+
return self._apply_binary_op(sub, other)
|
|
264
|
+
|
|
265
|
+
def __rsub__(self, other: 'BlockMatrix') -> 'BlockMatrix':
|
|
266
|
+
from operator import sub
|
|
267
|
+
return other._apply_binary_op(sub, self)
|
|
268
|
+
|
|
269
|
+
def __mul__(self, other: Union['BlockMatrix', Number]) -> 'BlockMatrix':
|
|
270
|
+
if isinstance(other, Number):
|
|
271
|
+
return self._apply_unary_op(lambda x: other*x)
|
|
272
|
+
else:
|
|
273
|
+
from operator import mul
|
|
274
|
+
return self._apply_binary_op(mul, other)
|
|
275
|
+
|
|
276
|
+
def __rmul__(self, other: Union['BlockMatrix', Number]) -> 'BlockMatrix':
|
|
277
|
+
return self * other
|
|
278
|
+
|
|
279
|
+
def __truediv__(self, other: Union['BlockMatrix', Number]) -> 'BlockMatrix':
|
|
280
|
+
from numbers import Number
|
|
281
|
+
if isinstance(other, Number):
|
|
282
|
+
return self._apply_unary_op(lambda x: x/other)
|
|
283
|
+
else:
|
|
284
|
+
from operator import truediv
|
|
285
|
+
return self._apply_binary_op(truediv, other)
|
|
286
|
+
|
|
287
|
+
def __rtruediv__(self, other: Union['BlockMatrix', Number]) -> 'BlockMatrix':
|
|
288
|
+
from numbers import Number
|
|
289
|
+
if isinstance(other, Number):
|
|
290
|
+
return self._apply_unary_op(lambda x: other/x)
|
|
291
|
+
else:
|
|
292
|
+
return self._apply_binary_op(lambda x, y: y/x, other)
|
|
293
|
+
|
|
294
|
+
def matvec(self, other):
|
|
295
|
+
"""Matrix vector product.
|
|
296
|
+
Named as such to be used as scipy LinearOperator."""
|
|
297
|
+
LOG.debug(f"Multiplication of {self} with a full vector of size {other.shape}.")
|
|
298
|
+
result = np.zeros(self.shape[0], dtype=other.dtype)
|
|
299
|
+
line_heights = self.block_shapes[0]
|
|
300
|
+
line_positions = list(accumulate(chain([0], line_heights)))
|
|
301
|
+
col_widths = self.block_shapes[1]
|
|
302
|
+
col_positions = list(accumulate(chain([0], col_widths)))
|
|
303
|
+
for line, line_position, line_height in zip(self.all_blocks, line_positions, line_heights):
|
|
304
|
+
line_slice = slice(line_position, line_position+line_height)
|
|
305
|
+
for block, col_position, col_width in zip(line, col_positions, col_widths):
|
|
306
|
+
col_slice = slice(col_position, col_position+col_width)
|
|
307
|
+
result[line_slice] += block @ other[col_slice]
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
def rmatvec(self, other):
|
|
311
|
+
"""Vector matrix product.
|
|
312
|
+
Named as such to be used as scipy LinearOperator."""
|
|
313
|
+
LOG.debug(f"Multiplication of a full vector of size {other.shape} with {self}.")
|
|
314
|
+
result = np.zeros(self.shape[1], dtype=other.dtype)
|
|
315
|
+
line_heights = self.block_shapes[0]
|
|
316
|
+
line_positions = list(accumulate(chain([0], line_heights)))
|
|
317
|
+
col_widths = self.block_shapes[1]
|
|
318
|
+
col_positions = list(accumulate(chain([0], col_widths)))
|
|
319
|
+
for col, col_position, col_width in zip(self.all_blocks.T, col_positions, col_widths):
|
|
320
|
+
col_slice = slice(col_position, col_position+col_width)
|
|
321
|
+
for block, line_position, line_height in zip(col, line_positions, line_heights):
|
|
322
|
+
line_slice = slice(line_position, line_position+line_height)
|
|
323
|
+
if isinstance(block, BlockMatrix):
|
|
324
|
+
result[col_slice] += block.rmatvec(other[line_slice])
|
|
325
|
+
else:
|
|
326
|
+
result[col_slice] += other[line_slice] @ block
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
def matmat(self, other):
|
|
330
|
+
"""Matrix-matrix product."""
|
|
331
|
+
if isinstance(other, BlockMatrix) and self.block_shapes[1] == other.block_shapes[0]:
|
|
332
|
+
LOG.debug(f"Multiplication of %s with %s", self, other)
|
|
333
|
+
own_blocks = self.all_blocks
|
|
334
|
+
other_blocks = np.moveaxis(other.all_blocks, 1, 0)
|
|
335
|
+
new_matrix = []
|
|
336
|
+
for own_line in own_blocks:
|
|
337
|
+
new_line = []
|
|
338
|
+
for other_col in other_blocks:
|
|
339
|
+
new_line.append(sum(own_block @ other_block for own_block, other_block in zip(own_line, other_col)))
|
|
340
|
+
new_matrix.append(new_line)
|
|
341
|
+
return BlockMatrix(new_matrix, check=False)
|
|
342
|
+
|
|
343
|
+
elif isinstance(other, np.ndarray) and self.shape[1] == other.shape[0]:
|
|
344
|
+
LOG.debug(f"Multiplication of {self} with a full matrix of shape {other.shape}.")
|
|
345
|
+
# Cut the matrix and recursively call itself to use the code above.
|
|
346
|
+
from capytaine.matrices.builders import cut_matrix
|
|
347
|
+
cut_other = cut_matrix(other, self.block_shapes[1], [other.shape[1]], check=False)
|
|
348
|
+
return (self @ cut_other).full_matrix()
|
|
349
|
+
|
|
350
|
+
def __matmul__(self, other: Union['BlockMatrix', np.ndarray]) -> Union['BlockMatrix', np.ndarray]:
|
|
351
|
+
if not (isinstance(other, BlockMatrix) or isinstance(other, np.ndarray)):
|
|
352
|
+
return NotImplemented
|
|
353
|
+
elif other.ndim == 2: # Other is a matrix
|
|
354
|
+
if other.shape[1] == 1: # Actually a column vector
|
|
355
|
+
return self.matvec(other.flatten())
|
|
356
|
+
else:
|
|
357
|
+
return self.matmat(other)
|
|
358
|
+
elif other.ndim == 1: # Other is a vector
|
|
359
|
+
return self.matvec(other)
|
|
360
|
+
else:
|
|
361
|
+
return NotImplemented
|
|
362
|
+
|
|
363
|
+
def __rmatmul__(self, other: Union['BlockMatrix', np.ndarray]) -> Union['BlockMatrix', np.ndarray]:
|
|
364
|
+
if not (isinstance(other, BlockMatrix) or isinstance(other, np.ndarray)):
|
|
365
|
+
return NotImplemented
|
|
366
|
+
elif other.ndim == 2: # Other is a matrix
|
|
367
|
+
if other.shape[1] == 1: # Actually a column vector
|
|
368
|
+
return self.rmatvec(other.flatten())
|
|
369
|
+
else:
|
|
370
|
+
return NotImplemented
|
|
371
|
+
elif other.ndim == 1: # Other is a vector
|
|
372
|
+
return self.rmatvec(other)
|
|
373
|
+
else:
|
|
374
|
+
return NotImplemented
|
|
375
|
+
|
|
376
|
+
def astype(self, dtype: np.dtype) -> 'BlockMatrix':
|
|
377
|
+
return self._apply_unary_op(lambda x: x.astype(dtype))
|
|
378
|
+
|
|
379
|
+
def fft_of_list(*block_matrices, check=True):
|
|
380
|
+
"""Compute the fft of a list of block matrices of the same type and shape.
|
|
381
|
+
The output is a list of block matrices of the same shape as the input ones.
|
|
382
|
+
The fft is computed element-wise, so the block structure does not cause any mathematical difficulty.
|
|
383
|
+
Returns an array of BlockMatrices.
|
|
384
|
+
"""
|
|
385
|
+
class_of_matrices = type(block_matrices[0])
|
|
386
|
+
nb_blocks = block_matrices[0]._stored_nb_blocks
|
|
387
|
+
|
|
388
|
+
LOG.debug(f"FFT of {len(block_matrices)} {class_of_matrices.__name__} (stored blocks = {nb_blocks})")
|
|
389
|
+
|
|
390
|
+
if check:
|
|
391
|
+
# Check the validity of the shapes of the matrices given as input
|
|
392
|
+
shape = block_matrices[0].shape
|
|
393
|
+
assert [nb_blocks == matrix._stored_nb_blocks for matrix in block_matrices[1:]]
|
|
394
|
+
assert [shape == matrix.shape for matrix in block_matrices[1:]]
|
|
395
|
+
assert [class_of_matrices == type(matrix) for matrix in block_matrices[1:]]
|
|
396
|
+
|
|
397
|
+
# Initialize a vector of block matrices without values in the blocks.
|
|
398
|
+
result = np.empty(len(block_matrices), dtype=object)
|
|
399
|
+
for i in range(len(block_matrices)):
|
|
400
|
+
result[i] = class_of_matrices(np.empty(nb_blocks, dtype=object),
|
|
401
|
+
_stored_block_shapes=block_matrices[0]._stored_block_shapes,
|
|
402
|
+
check=False)
|
|
403
|
+
|
|
404
|
+
for i_block, j_block in product(range(nb_blocks[0]), range(nb_blocks[1])):
|
|
405
|
+
list_of_i_j_blocks = [block_matrices[i_matrix]._stored_blocks[i_block, j_block]
|
|
406
|
+
for i_matrix in range(len(block_matrices))]
|
|
407
|
+
|
|
408
|
+
if any(isinstance(block, np.ndarray) or isinstance(block, LowRankMatrix) for block in list_of_i_j_blocks):
|
|
409
|
+
list_of_i_j_blocks = [block if isinstance(block, np.ndarray) else block.full_matrix() for block in list_of_i_j_blocks]
|
|
410
|
+
fft_of_blocks = np.fft.fft(list_of_i_j_blocks, axis=0)
|
|
411
|
+
else:
|
|
412
|
+
fft_of_blocks = BlockMatrix.fft_of_list(*list_of_i_j_blocks, check=False)
|
|
413
|
+
|
|
414
|
+
for matrix, computed_block in zip(result, fft_of_blocks):
|
|
415
|
+
matrix._stored_blocks[i_block, j_block] = computed_block
|
|
416
|
+
|
|
417
|
+
return result
|
|
418
|
+
|
|
419
|
+
# COMPARISON AND REDUCTION
|
|
420
|
+
|
|
421
|
+
def __eq__(self, other: 'BlockMatrix') -> 'BlockMatrix[bool]':
|
|
422
|
+
from operator import eq
|
|
423
|
+
return self._apply_binary_op(eq, other)
|
|
424
|
+
|
|
425
|
+
def __invert__(self) -> 'BlockMatrix':
|
|
426
|
+
"""Boolean not (~)"""
|
|
427
|
+
from operator import invert
|
|
428
|
+
return self._apply_unary_op(invert)
|
|
429
|
+
|
|
430
|
+
def __ne__(self, other: 'BlockMatrix') -> 'BlockMatrix[bool]':
|
|
431
|
+
return ~(self == other)
|
|
432
|
+
|
|
433
|
+
def all(self) -> bool:
|
|
434
|
+
for line in self._stored_blocks:
|
|
435
|
+
for block in line:
|
|
436
|
+
if not block.all():
|
|
437
|
+
return False
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
def any(self) -> bool:
|
|
441
|
+
for line in self._stored_blocks:
|
|
442
|
+
for block in line:
|
|
443
|
+
if block.any():
|
|
444
|
+
return True
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
def min(self) -> Number:
|
|
448
|
+
return min(block.min() for line in self._stored_blocks for block in line)
|
|
449
|
+
|
|
450
|
+
def max(self) -> Number:
|
|
451
|
+
return max(block.max() for line in self._stored_blocks for block in line)
|
|
452
|
+
|
|
453
|
+
# DISPLAYING DATA
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
def str_shape(self):
|
|
457
|
+
blocks_str = []
|
|
458
|
+
for line in self.all_blocks:
|
|
459
|
+
for block in line:
|
|
460
|
+
if isinstance(block, BlockMatrix):
|
|
461
|
+
blocks_str.append(block.str_shape)
|
|
462
|
+
elif isinstance(block, np.ndarray) or isinstance(block, LowRankMatrix):
|
|
463
|
+
blocks_str.append("{}×{}".format(*block.shape))
|
|
464
|
+
else:
|
|
465
|
+
blocks_str.append("?×?")
|
|
466
|
+
|
|
467
|
+
if len(set(blocks_str)) == 1:
|
|
468
|
+
return "{}×{}×[".format(*self.nb_blocks) + blocks_str[0] + "]"
|
|
469
|
+
else:
|
|
470
|
+
blocks_str = np.array(blocks_str).reshape(self.nb_blocks).tolist()
|
|
471
|
+
return str(blocks_str).replace("'", "")
|
|
472
|
+
|
|
473
|
+
def __str__(self):
|
|
474
|
+
if not hasattr(self, '_str'):
|
|
475
|
+
args = [self.str_shape]
|
|
476
|
+
if self.dtype not in [np.float64, float]:
|
|
477
|
+
args.append(f"dtype={self.dtype}")
|
|
478
|
+
self._str = f"{self.__class__.__name__}(" + ", ".join(args) + ")"
|
|
479
|
+
return self._str
|
|
480
|
+
|
|
481
|
+
def _repr_pretty_(self, p, cycle):
|
|
482
|
+
p.text(self.__str__())
|
|
483
|
+
|
|
484
|
+
display_color = cycle([f'C{i}' for i in range(10)])
|
|
485
|
+
|
|
486
|
+
def _patches(self,
|
|
487
|
+
global_frame: Union[Tuple[int, int], np.ndarray]
|
|
488
|
+
):
|
|
489
|
+
"""Helper function for displaying the shape of the matrix.
|
|
490
|
+
Recursively returns a list of rectangles representing the sub-blocks of the matrix.
|
|
491
|
+
|
|
492
|
+
Uses BlockMatrix.display_color to assign color to the blocks.
|
|
493
|
+
By default, it cycles through matplotlib default colors.
|
|
494
|
+
But if display_color is redefined as a callable, it is called with the block as argument.
|
|
495
|
+
|
|
496
|
+
Parameters
|
|
497
|
+
----------
|
|
498
|
+
global_frame: tuple of ints
|
|
499
|
+
coordinates of the origin in the top left corner.
|
|
500
|
+
|
|
501
|
+
Returns
|
|
502
|
+
-------
|
|
503
|
+
list of matplotlib.patches.Rectangle
|
|
504
|
+
"""
|
|
505
|
+
matplotlib_patches = import_optional_dependency("matplotlib.patches", "matplotlib")
|
|
506
|
+
Rectangle = matplotlib_patches.Rectangle
|
|
507
|
+
|
|
508
|
+
all_blocks_in_flat_iterator = (block for line in self._stored_blocks for block in line)
|
|
509
|
+
positions_of_all_blocks = self._stored_block_positions(global_frame=global_frame)
|
|
510
|
+
patches = []
|
|
511
|
+
for block, positions_of_the_block in zip(all_blocks_in_flat_iterator, positions_of_all_blocks):
|
|
512
|
+
position_of_first_appearance = positions_of_the_block[0]
|
|
513
|
+
# Exchange coordinates: row index i -> y, column index j -> x
|
|
514
|
+
position_of_first_appearance = np.array((position_of_first_appearance[1], position_of_first_appearance[0]))
|
|
515
|
+
|
|
516
|
+
if isinstance(block, BlockMatrix):
|
|
517
|
+
patches_of_this_block = block._patches(np.array((position_of_first_appearance[1], position_of_first_appearance[0])))
|
|
518
|
+
elif isinstance(block, np.ndarray):
|
|
519
|
+
|
|
520
|
+
if isinstance(self.display_color, Iterator):
|
|
521
|
+
color = next(self.display_color)
|
|
522
|
+
elif callable(self.display_color):
|
|
523
|
+
color = self.display_color(block)
|
|
524
|
+
else:
|
|
525
|
+
color = np.random.rand(3)
|
|
526
|
+
|
|
527
|
+
patches_of_this_block = [Rectangle(position_of_first_appearance,
|
|
528
|
+
block.shape[1], block.shape[0],
|
|
529
|
+
edgecolor='k', facecolor=color)]
|
|
530
|
+
elif isinstance(block, LowRankMatrix):
|
|
531
|
+
|
|
532
|
+
if isinstance(self.display_color, Iterator):
|
|
533
|
+
color = next(self.display_color)
|
|
534
|
+
elif callable(self.display_color):
|
|
535
|
+
color = self.display_color(block)
|
|
536
|
+
else:
|
|
537
|
+
color = np.random.rand(3)
|
|
538
|
+
|
|
539
|
+
patches_of_this_block = [
|
|
540
|
+
# Left block
|
|
541
|
+
Rectangle(position_of_first_appearance,
|
|
542
|
+
block.left_matrix.shape[1], block.left_matrix.shape[0],
|
|
543
|
+
edgecolor='k', facecolor=color),
|
|
544
|
+
# Top block
|
|
545
|
+
Rectangle(position_of_first_appearance,
|
|
546
|
+
block.right_matrix.shape[1], block.right_matrix.shape[0],
|
|
547
|
+
edgecolor='k', facecolor=color),
|
|
548
|
+
# Rest of the matrix
|
|
549
|
+
Rectangle(position_of_first_appearance,
|
|
550
|
+
block.right_matrix.shape[1], block.left_matrix.shape[0],
|
|
551
|
+
facecolor=color, alpha=0.2),
|
|
552
|
+
]
|
|
553
|
+
else:
|
|
554
|
+
raise NotImplementedError()
|
|
555
|
+
|
|
556
|
+
patches.extend(patches_of_this_block)
|
|
557
|
+
|
|
558
|
+
# For the other appearances, copy the patches of the first appearance
|
|
559
|
+
for block_position in positions_of_the_block[1:]:
|
|
560
|
+
block_position = np.array((block_position[1], block_position[0]))
|
|
561
|
+
for patch in patches_of_this_block: # A block can be made of several patches.
|
|
562
|
+
shift = block_position - position_of_first_appearance
|
|
563
|
+
patch_position = np.array(patch.get_xy()) + shift
|
|
564
|
+
patches.append(Rectangle(patch_position, patch.get_width(), patch.get_height(),
|
|
565
|
+
facecolor=patch.get_facecolor(), alpha=0.2))
|
|
566
|
+
|
|
567
|
+
return patches
|
|
568
|
+
|
|
569
|
+
def plot_shape(self):
|
|
570
|
+
"""Plot the structure of the matrix using matplotlib."""
|
|
571
|
+
matplotlib = import_optional_dependency("matplotlib")
|
|
572
|
+
plt = matplotlib.pyplot
|
|
573
|
+
|
|
574
|
+
plt.figure()
|
|
575
|
+
for patch in self._patches((0, 0)):
|
|
576
|
+
plt.gca().add_patch(patch)
|
|
577
|
+
plt.axis('equal')
|
|
578
|
+
plt.xlim(0, self.shape[1])
|
|
579
|
+
plt.ylim(0, self.shape[0])
|
|
580
|
+
plt.gca().invert_yaxis()
|
|
581
|
+
# plt.show()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def access_block_by_path(self, path):
|
|
585
|
+
"""
|
|
586
|
+
Access a diagonal block in a block matrix from the path of the
|
|
587
|
+
corresponding leaf
|
|
588
|
+
"""
|
|
589
|
+
this_block = self
|
|
590
|
+
for index in path:
|
|
591
|
+
this_block = this_block.all_blocks[index, index]
|
|
592
|
+
return this_block
|