Skip to content

Commit 7334c92

Browse files
committed
move LUDecomposition to legacy.py
1 parent 2207b6c commit 7334c92

File tree

7 files changed

+212
-203
lines changed

7 files changed

+212
-203
lines changed

profiling/banded_matrix_solver.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
Matrix,
99
BandedMatrixLU,
1010
banded_matrix,
11-
LUDecomposition,
11+
NumpySolver,
1212
)
1313

14+
1415
CWD = pathlib.Path("~/Desktop/Outbox").expanduser()
1516
if not CWD.exists():
1617
CWD = pathlib.Path(".")
@@ -28,10 +29,10 @@ def random_matrix(shape, m1: int, m2: int):
2829
return m
2930

3031

31-
def profile_LU_matrix_solver(count: int, A: Matrix, B: Matrix):
32+
def profile_numpy_matrix_solver(count: int, A: Matrix, B: Matrix):
3233
for _ in range(count):
33-
lu = LUDecomposition(A)
34-
lu.solve_matrix(B)
34+
lu = NumpySolver(A.matrix)
35+
lu.solve_matrix(B.matrix)
3536

3637

3738
def profile_banded_matrix_solver(count, A: Matrix, B: Matrix):
@@ -76,11 +77,11 @@ def profile(func, *args):
7677
)
7778
)
7879
)
79-
t0 = profile(profile_LU_matrix_solver, REPEAT, A, B)
80+
t0 = profile(profile_numpy_matrix_solver, REPEAT, A, B)
8081
t1 = profile(profile_banded_matrix_solver, REPEAT, A, B)
8182
factor = t0 / t1
8283
print(
83-
f"Matrix {size}x{size}, m1={m1}, m2={m2}, {REPEAT}x: Standard LU {t0:0.3f}s Banded LU {t1:0.3}s factor: x{factor:.1f}"
84+
f"Matrix {size}x{size}, m1={m1}, m2={m2}, {REPEAT}x: Numpy {t0:0.3f}s Banded LU {t1:0.3}s factor: x{factor:.1f}"
8485
)
8586
writer.writerow(
8687
[

profiling/linalg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from ezdxf.math.linalg import (
77
Matrix,
8-
LUDecomposition,
98
numpy_matrix_solver,
109
numpy_vector_solver,
1110
)
@@ -14,6 +13,7 @@
1413
gauss_matrix_solver,
1514
gauss_jordan_solver,
1615
gauss_jordan_inverse,
16+
LUDecomposition,
1717
)
1818

1919

src/ezdxf/math/bspline.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
Iterator,
2323
Sequence,
2424
TYPE_CHECKING,
25-
Union,
2625
Optional,
2726
)
2827
import math
@@ -41,7 +40,7 @@
4140
)
4241
from ezdxf.math import linalg
4342
from ezdxf.lldxf.const import DXFValueError
44-
from ezdxf import PYPY
43+
4544

4645
if TYPE_CHECKING:
4746
from ezdxf.math import (
@@ -51,11 +50,6 @@
5150
Bezier4P,
5251
)
5352

54-
# Acceleration of banded diagonal matrix solver kicks in at:
55-
# N=15 for CPython on Windows and Linux
56-
# N=60 for pypy3 on Windows and Linux
57-
USE_BANDED_MATRIX_SOLVER_CPYTHON_LIMIT = 15
58-
USE_BANDED_MATRIX_SOLVER_PYPY_LIMIT = 60
5953

6054
__all__ = [
6155
# High level functions:
@@ -551,24 +545,26 @@ def double_knots(n: int, p: int, t: Sequence[float]) -> list[float]:
551545
return u
552546

553547

554-
def _get_best_solver(matrix: Union[list, linalg.Matrix], degree: int):
555-
"""Returns best suited linear equation solver depending on matrix
556-
configuration and python interpreter.
548+
def _get_best_solver(matrix: list| linalg.Matrix, degree: int) -> linalg.Solver:
549+
"""Returns best suited linear equation solver depending on matrix configuration and
550+
python interpreter.
557551
"""
558-
A = matrix if isinstance(matrix, linalg.Matrix) else linalg.Matrix(matrix=matrix)
559-
if PYPY:
560-
limit = USE_BANDED_MATRIX_SOLVER_PYPY_LIMIT
561-
else:
562-
limit = USE_BANDED_MATRIX_SOLVER_CPYTHON_LIMIT
563-
if A.nrows < limit: # use default equation solver
564-
return linalg.LUDecomposition(A.matrix)
552+
# v1.2: added NumpySolver
553+
# Acceleration of banded diagonal matrix solver is still a thing but only for
554+
# really big matrices N > 30
555+
# PyPy has no advantages when using the NumpySolver
556+
if not isinstance(matrix, linalg.Matrix):
557+
matrix =linalg.Matrix(matrix)
558+
559+
if matrix.nrows < 30: # use default equation solver
560+
return linalg.NumpySolver(matrix.matrix)
565561
else:
566562
# Theory: band parameters m1, m2 are at maximum degree-1, for
567563
# B-spline interpolation and approximation:
568564
# m1 = m2 = degree-1
569565
# But the speed gain is not that big and just to be sure:
570-
m1, m2 = linalg.detect_banded_matrix(A, check_all=False)
571-
A = linalg.compact_banded_matrix(A, m1, m2)
566+
m1, m2 = linalg.detect_banded_matrix(matrix, check_all=False)
567+
A = linalg.compact_banded_matrix(matrix, m1, m2)
572568
return linalg.BandedMatrixLU(A, m1, m2)
573569

574570

@@ -606,7 +602,8 @@ def unconstrained_global_bspline_interpolation(
606602
)
607603
N = Basis(knots=knots, order=degree + 1, count=len(fit_points))
608604
solver = _get_best_solver([N.basis_vector(t) for t in t_vector], degree)
609-
control_points = solver.solve_matrix(fit_points)
605+
mat_B = np.array(fit_points, dtype=np.float64)
606+
control_points = solver.solve_matrix(mat_B)
610607
return Vec3.list(control_points.rows()), knots
611608

612609

src/ezdxf/math/legacy.py

Lines changed: 159 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,18 @@
22
# License: MIT License
33
# legacy.py code is replaced by numpy - exists just for benchmarking, testing and nostalgia
44
from __future__ import annotations
5-
from typing import Iterable
5+
from typing import Iterable, cast
66
from itertools import repeat
7-
from .linalg import Matrix, MatrixData, NDArray
7+
import reprlib
8+
9+
from .linalg import Matrix, MatrixData, NDArray, Solver
810

911
__all__ = [
1012
"gauss_vector_solver",
1113
"gauss_matrix_solver",
1214
"gauss_jordan_solver",
1315
"gauss_jordan_inverse",
1416
"LUDecomposition",
15-
"tridiagonal_vector_solver",
16-
"tridiagonal_matrix_solver",
17-
"detect_banded_matrix",
18-
"compact_banded_matrix",
19-
"BandedMatrixLU",
20-
"banded_matrix",
2117
]
2218

2319

@@ -264,4 +260,158 @@ def gauss_jordan_inverse(A: MatrixData) -> Matrix:
264260
else:
265261
matrix_a = list(A)
266262
nrows = len(matrix_a)
267-
return gauss_jordan_solver(matrix_a, list(repeat([0.0], nrows)))[0]
263+
return gauss_jordan_solver(matrix_a, list(repeat([0.0], nrows)))[0]
264+
265+
266+
class LUDecomposition(Solver):
267+
"""Represents a `LU decomposition`_ matrix of A, raise :class:`ZeroDivisionError`
268+
for a singular matrix.
269+
270+
This algorithm is a little bit faster than the `Gauss-Elimination`_
271+
algorithm using CPython and much faster when using pypy.
272+
273+
The :attr:`LUDecomposition.matrix` attribute gives access to the matrix data
274+
as list of rows like in the :class:`Matrix` class, and the :attr:`LUDecomposition.index`
275+
attribute gives access to the swapped row indices.
276+
277+
Args:
278+
A: matrix [[a11, a12, ..., a1n], [a21, a22, ..., a2n], [a21, a22, ..., a2n],
279+
... [an1, an2, ..., ann]]
280+
281+
raises:
282+
ZeroDivisionError: singular matrix
283+
284+
"""
285+
286+
__slots__ = ("matrix", "index", "_det")
287+
288+
def __init__(self, A: MatrixData | NDArray):
289+
lu: MatrixData = copy_float_matrix(A)
290+
n: int = len(lu)
291+
det: float = 1.0
292+
index: list[int] = []
293+
294+
# find max value for each row, raises ZeroDivisionError for singular matrix!
295+
scaling: list[float] = [1.0 / max(abs(v) for v in row) for row in lu]
296+
297+
for k in range(n):
298+
big: float = 0.0
299+
imax: int = k
300+
for i in range(k, n):
301+
temp: float = scaling[i] * abs(lu[i][k])
302+
if temp > big:
303+
big = temp
304+
imax = i
305+
306+
if k != imax:
307+
for col in range(n):
308+
temp = lu[imax][col]
309+
lu[imax][col] = lu[k][col]
310+
lu[k][col] = temp
311+
312+
det = -det
313+
scaling[imax] = scaling[k]
314+
315+
index.append(imax)
316+
for i in range(k + 1, n):
317+
temp = lu[i][k] / lu[k][k]
318+
lu[i][k] = temp
319+
for j in range(k + 1, n):
320+
lu[i][j] -= temp * lu[k][j]
321+
322+
self.index: list[int] = index
323+
self.matrix: MatrixData = lu
324+
self._det: float = det
325+
326+
def __str__(self) -> str:
327+
return str(self.matrix)
328+
329+
def __repr__(self) -> str:
330+
return f"{self.__class__} {reprlib.repr(self.matrix)}"
331+
332+
@property
333+
def nrows(self) -> int:
334+
"""Count of matrix rows (and cols)."""
335+
return len(self.matrix)
336+
337+
def solve_vector(self, B: Iterable[float]) -> list[float]:
338+
"""Solves the linear equation system given by the nxn Matrix A . x = B,
339+
right-hand side quantities as vector B with n elements.
340+
341+
Args:
342+
B: vector [b1, b2, ..., bn]
343+
344+
Returns:
345+
vector as list of floats
346+
347+
"""
348+
X: list[float] = [float(v) for v in B]
349+
lu: MatrixData = self.matrix
350+
index: list[int] = self.index
351+
n: int = self.nrows
352+
ii: int = 0
353+
354+
if len(X) != n:
355+
raise ValueError(
356+
"Item count of vector B has to be equal to matrix row count."
357+
)
358+
359+
for i in range(n):
360+
ip: int = index[i]
361+
sum_: float = X[ip]
362+
X[ip] = X[i]
363+
if ii != 0:
364+
for j in range(ii - 1, i):
365+
sum_ -= lu[i][j] * X[j]
366+
elif sum_ != 0.0:
367+
ii = i + 1
368+
X[i] = sum_
369+
370+
for row in range(n - 1, -1, -1):
371+
sum_ = X[row]
372+
for col in range(row + 1, n):
373+
sum_ -= lu[row][col] * X[col]
374+
X[row] = sum_ / lu[row][row]
375+
return X
376+
377+
def solve_matrix(self, B: MatrixData | NDArray) -> Matrix:
378+
"""Solves the linear equation system given by the nxn Matrix A . x = B,
379+
right-hand side quantities as nxm Matrix B.
380+
381+
Args:
382+
B: matrix [[b11, b12, ..., b1m], [b21, b22, ..., b2m],
383+
... [bn1, bn2, ..., bnm]]
384+
385+
Returns:
386+
matrix as :class:`Matrix` object
387+
388+
"""
389+
if not isinstance(B, Matrix):
390+
matrix_b = Matrix(matrix=[list(row) for row in B])
391+
else:
392+
matrix_b = cast(Matrix, B)
393+
394+
if matrix_b.nrows != self.nrows:
395+
raise ValueError("Row count of self and matrix B has to match.")
396+
397+
return Matrix(
398+
matrix=[self.solve_vector(col) for col in matrix_b.cols()]
399+
).transpose()
400+
401+
def inverse(self) -> Matrix:
402+
"""Returns the inverse of matrix as :class:`Matrix` object,
403+
raise :class:`ZeroDivisionError` for a singular matrix.
404+
405+
"""
406+
return self.solve_matrix(Matrix.identity(shape=(self.nrows, self.nrows)).matrix)
407+
408+
def determinant(self) -> float:
409+
"""Returns the determinant of matrix, raises :class:`ZeroDivisionError`
410+
if matrix is singular.
411+
412+
"""
413+
det: float = self._det
414+
lu: MatrixData = self.matrix
415+
for i in range(self.nrows):
416+
det *= lu[i][i]
417+
return det

0 commit comments

Comments
 (0)