pyMOTO 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.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.
@@ -0,0 +1,108 @@
1
+ import warnings
2
+ import scipy.sparse as sps
3
+ from inspect import currentframe, getframeinfo
4
+
5
+ from .dense import *
6
+ from .sparse import *
7
+ from .matrix_checks import *
8
+
9
+
10
+ # flake8: noqa: C901
11
+ def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertriangular=None,
12
+ ishermitian=None, issymmetric=None, ispositivedefinite=None):
13
+ """
14
+ Uses parts of Matlab's scheme https://nl.mathworks.com/help/matlab/ref/mldivide.html
15
+ :param A: The matrix
16
+ :param isdiagonal: Manual override for diagonal matrix
17
+ :param islowertriangular: Override for lower triangular matrix
18
+ :param isuppertriangular: Override for upper triangular matrix
19
+ :param ishermitian: Override for hermitian matrix (prevents check)
20
+ :param issymmetric: Override for symmetric matrix (prevents check). Is the same as hermitian for a real matrix
21
+ :param ispositivedefinite: Manual override for positive definiteness
22
+ :return: LinearSolver which should be 'best' for the matrix
23
+ """
24
+ issparse = matrix_is_sparse(A) # Check if the matrix is sparse
25
+ issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
26
+
27
+ if not issquare:
28
+ if issparse:
29
+ sps.SparseEfficiencyWarning("Only a dense version of QR solver is available") # TODO
30
+ return SolverDenseQR()
31
+
32
+ # l_bw, u_bw = spla.bandwidth(A) # TODO Get bandwidth (implemented in scipy version > 1.8.0)
33
+
34
+ if isdiagonal is None: # Check if matrix is diagonal
35
+ # TODO: This could be improved to check other sparse matrix types as well
36
+ isdiagonal = matrix_is_diagonal(A)
37
+ if isdiagonal:
38
+ return SolverDiagonal()
39
+
40
+ # Check if the matrix is triangular
41
+ # TODO Currently only for dense matrices
42
+ if islowertriangular is None: # Check if matrix is lower triangular
43
+ islowertriangular = False if issparse else np.allclose(A, np.tril(A))
44
+ if islowertriangular:
45
+ warnings.WarningMessage("Lower triangular solver not implemented",
46
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
47
+
48
+ if isuppertriangular is None: # Check if matrix is upper triangular
49
+ isuppertriangular = False if issparse else np.allclose(A, np.triu(A))
50
+ if isuppertriangular:
51
+ warnings.WarningMessage("Upper triangular solver not implemented",
52
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
53
+
54
+ ispermutedtriangular = False
55
+ if ispermutedtriangular:
56
+ warnings.WarningMessage("Permuted triangular solver not implemented",
57
+ UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
58
+
59
+ # Check if the matrix is complex-valued
60
+ iscomplex = np.iscomplexobj(A)
61
+ if iscomplex:
62
+ # Detect if the matrix is hermitian and/or symmetric
63
+ if ishermitian is None:
64
+ ishermitian = matrix_is_hermitian(A)
65
+ if issymmetric is None:
66
+ issymmetric = matrix_is_symmetric(A)
67
+ else:
68
+ if ishermitian is None and issymmetric is None:
69
+ # Detect if the matrix is symmetric
70
+ issymmetric = matrix_is_symmetric(A)
71
+ ishermitian = issymmetric
72
+ elif ishermitian is not None and issymmetric is not None:
73
+ assert ishermitian == issymmetric, "For real-valued matrices, symmetry and hermitian must be equal"
74
+ elif ishermitian is None:
75
+ ishermitian = issymmetric
76
+ elif issymmetric is None:
77
+ issymmetric = ishermitian
78
+
79
+ if issparse:
80
+ # Prefer Intel Pardiso solver as it can solve any matrix TODO: Check for complex matrix
81
+ if SolverSparsePardiso.defined and not iscomplex:
82
+ # TODO check for positive definiteness? np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
83
+ return SolverSparsePardiso(symmetric=issymmetric, hermitian=ishermitian, positive_definite=ispositivedefinite)
84
+
85
+ if ishermitian:
86
+ # Check if diagonal is all positive or all negative -> Cholesky
87
+ if ispositivedefinite is None:
88
+ ispositivedefinite = np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
89
+ if ispositivedefinite: # TODO what about the complex case?
90
+ if SolverSparseCholeskyScikit.defined:
91
+ return SolverSparseCholeskyScikit()
92
+ if SolverSparseCholeskyCVXOPT.defined:
93
+ return SolverSparseCholeskyCVXOPT()
94
+
95
+ return SolverSparseLU() # Default to LU, which should be possible for any non-singular square matrix
96
+
97
+ else: # Dense branch
98
+ if ishermitian:
99
+ # Check if diagonal is all positive or all negative
100
+ if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0):
101
+ return SolverDenseCholesky()
102
+ else:
103
+ return SolverDenseLDL(hermitian=ishermitian)
104
+ elif issymmetric:
105
+ return SolverDenseLDL(hermitian=ishermitian)
106
+ else:
107
+ # TODO: Detect if the matrix is Hessenberg
108
+ return SolverDenseLU()
@@ -1,7 +1,8 @@
1
1
  import warnings
2
2
  import numpy as np
3
3
  import scipy.linalg as spla # Dense matrix solvers
4
- from .solvers import matrix_is_hermitian, matrix_is_diagonal, LinearSolver
4
+ from .matrix_checks import matrix_is_hermitian, matrix_is_diagonal
5
+ from .solvers import LinearSolver
5
6
 
6
7
 
7
8
  class SolverDiagonal(LinearSolver):
@@ -11,24 +12,17 @@ class SolverDiagonal(LinearSolver):
11
12
  self.diag = A.diagonal()
12
13
  return self
13
14
 
14
- def solve(self, rhs):
15
+ def solve(self, rhs, x0=None, trans='N'):
15
16
  r""" Solve using the diagonal only, by :math:`x_i = b_i / A_{ii}`
16
17
 
17
18
  The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
18
19
  :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
19
20
  """
21
+ d = self.diag.conj() if trans == 'H' else self.diag
20
22
  if rhs.ndim == 1:
21
- return rhs / self.diag
23
+ return rhs / d
22
24
  else:
23
- return rhs / self.diag[..., None]
24
-
25
- def adjoint(self, rhs):
26
- r""" Solve using the diagonal only, by :math:`x_i = b_i / A_{ii}^*`
27
-
28
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
29
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
30
- """
31
- return self.solve(rhs.conj()).conj()
25
+ return rhs / d[..., None]
32
26
 
33
27
 
34
28
  # Dense QR solver
@@ -42,23 +36,31 @@ class SolverDenseQR(LinearSolver):
42
36
  self.q, self.r = spla.qr(A)
43
37
  return self
44
38
 
45
- def solve(self, rhs):
46
- r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by backward substitution of
47
- :math:`\mathbf{x} = \mathbf{R}^{-1}\mathbf{Q}^\text{H}\mathbf{b}`.
48
-
49
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
50
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
51
- """
52
- return spla.solve_triangular(self.r, self.q.T.conj()@rhs)
39
+ def solve(self, rhs, x0=None, trans='N'):
40
+ r""" Solves the linear system of equations using the QR factorization.
53
41
 
54
- def adjoint(self, rhs):
55
- r""" Solves the linear system of equations :math:`\mathbf{A}^\text{H}\mathbf{x} = \mathbf{b}` by
56
- forward substitution of :math:`\mathbf{x} = \mathbf{Q}\mathbf{R}^{-H}\mathbf{b}`.
42
+ ======= ================= =====================
43
+ `trans` Equation Solution of :math:`x`
44
+ ------- ----------------- ---------------------
45
+ `N` :math:`A x = b` :math:`R^{-1} Q^H b`
46
+ `T` :math:`A^T x = b` :math:`Q^* R^{-T} b`
47
+ `H` :math:`A^H x = b` :math:`Q R^{-H} b`
48
+ ======= ================= =====================
57
49
 
58
50
  The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
59
51
  :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
60
52
  """
61
- return self.q@spla.solve_triangular(self.r, rhs, trans='C')
53
+ if trans == 'N':
54
+ # A = Q R -> inv(A) = inv(R) inv(Q) = inv(R) Q^H
55
+ return spla.solve_triangular(self.r, self.q.T.conj() @ rhs)
56
+ elif trans == 'T':
57
+ # A^T = R^T Q^T -> inv(A^T) = inv(Q^T) inv(R^T) = conj(Q) inv(R^T)
58
+ return self.q.conj() @ spla.solve_triangular(self.r, rhs, trans='T')
59
+ elif trans == 'H':
60
+ # A^H = R^H Q^H -> inv(A^H) = inv(Q^H) inv(R^H) = Q inv(R^H)
61
+ return self.q @ spla.solve_triangular(self.r, rhs, trans='C')
62
+ else:
63
+ raise TypeError("Only N, T, and H transposition is possible")
62
64
 
63
65
 
64
66
  # Dense LU solver
@@ -71,24 +73,34 @@ class SolverDenseLU(LinearSolver):
71
73
  self.p, self.l, self.u = spla.lu(A)
72
74
  return self
73
75
 
74
- def solve(self, rhs):
75
- r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
76
- substitution of :math:`\mathbf{x} = \mathbf{U}^{-1}\mathbf{L}^{-1}\mathbf{b}`.
76
+ def solve(self, rhs, x0=None, trans='N'):
77
+ r""" Solves the linear system of equations using the LU factorization.
77
78
 
78
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
79
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
80
- """
81
- return spla.solve_triangular(self.u, spla.solve_triangular(self.l, self.p.T@rhs, lower=True))
79
+ :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
80
+ substitution of :math:`\mathbf{x} = \mathbf{U}^{-1}\mathbf{L}^{-1}\mathbf{b}`.
82
81
 
83
- def adjoint(self, rhs):
84
- r""" Solves the linear system of equations :math:`\mathbf{A}^\text{H}\mathbf{x} = \mathbf{b}` by forward and
85
- backward substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{U}^{-\text{H}}\mathbf{b}`.
82
+ ======= ================= =========================
83
+ `trans` Equation Solution of :math:`x`
84
+ ------- ----------------- -------------------------
85
+ `N` :math:`A x = b` :math:`x = U^{-1} L^{-1}`
86
+ `T` :math:`A^T x = b` :math:`x = L^{-1} U^{-1}`
87
+ `H` :math:`A^H x = b` :math:`x = L^{-*} U^{-*}`
88
+ ======= ================= =========================
86
89
 
87
90
  The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
88
91
  :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
89
92
  """
90
- return self.p@spla.solve_triangular(self.l, spla.solve_triangular(self.u, rhs, trans='C'),
91
- lower=True, trans='C') # TODO permutation
93
+ if trans == 'N':
94
+ # A = P L U -> x = U^-1 L^-1 P^T b
95
+ return spla.solve_triangular(self.u, spla.solve_triangular(self.l, self.p.T@rhs, lower=True))
96
+ elif trans == 'T':
97
+ return self.p @ spla.solve_triangular(self.l, spla.solve_triangular(self.u, rhs, trans='T'),
98
+ lower=True, trans='T')
99
+ elif trans == 'H':
100
+ return self.p @ spla.solve_triangular(self.l, spla.solve_triangular(self.u, rhs, trans='C'),
101
+ lower=True, trans='C')
102
+ else:
103
+ raise TypeError("Only N, T, and H transposition is possible")
92
104
 
93
105
 
94
106
  # Dense Cholesky solver
@@ -106,7 +118,7 @@ class SolverDenseCholesky(LinearSolver):
106
118
  upper triangular matrix.
107
119
  """
108
120
  try:
109
- self.u = spla.cholesky(A)
121
+ self.U = spla.cholesky(A)
110
122
  self.success = True
111
123
  except np.linalg.LinAlgError as err:
112
124
  warnings.warn(f"{type(self).__name__}: {err} -- using {type(self.backup_solver).__name__} instead")
@@ -114,7 +126,7 @@ class SolverDenseCholesky(LinearSolver):
114
126
  self.success = False
115
127
  return self
116
128
 
117
- def solve(self, rhs):
129
+ def solve(self, rhs, x0=None, trans='N'):
118
130
  r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
119
131
  substitution of :math:`\mathbf{x} = \mathbf{U}^{-1}\mathbf{U}^{-\text{H}}\mathbf{b}`.
120
132
 
@@ -124,21 +136,16 @@ class SolverDenseCholesky(LinearSolver):
124
136
  # TODO When Cholesky factorization A = U^T U is used, symmetric complex matrices can also be solved, but this is
125
137
  # not implemented in scipy
126
138
  if self.success:
127
- return spla.solve_triangular(self.u, spla.solve_triangular(self.u, rhs, trans='C'))
128
- else:
129
- return self.backup_solver.solve(rhs)
130
-
131
- def adjoint(self, rhs):
132
- r""" A Hermitian matrix is self-adjoint (:math:`\mathbf{A}=\mathbf{A}^\text{H}`), so this is equal to the
133
- regular solution.
134
-
135
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
136
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
137
- """
138
- if self.success:
139
- return self.solve(rhs)
139
+ if trans == 'N' or trans == 'H':
140
+ # A = U^H U -> A^-1 = U^-1 U^-H
141
+ return spla.solve_triangular(self.U, spla.solve_triangular(self.U, rhs, trans='C'))
142
+ elif trans == 'T':
143
+ # A^T = U^T conj(U) -> A^-T = conj(U^-1) U^-T
144
+ return spla.solve_triangular(self.U, spla.solve_triangular(self.U, rhs, trans='T').conj()).conj()
145
+ else:
146
+ raise TypeError("Only N, T, and H transposition is possible")
140
147
  else:
141
- return self.backup_solver.adjoint(rhs)
148
+ return self.backup_solver.solve(rhs, trans=trans)
142
149
 
143
150
 
144
151
  # Dense LDL solver
@@ -171,23 +178,13 @@ class SolverDenseLDL(LinearSolver):
171
178
  self.lp = self.l[self.p, :]
172
179
  return self
173
180
 
174
- def solve(self, rhs):
181
+ def solve(self, rhs, x0=None, trans='N'):
175
182
  r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
176
183
  substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{D}^{-1}\mathbf{L}^{-1}\mathbf{b}` in the
177
184
  Hermitian case or as :math:`\mathbf{x} = \mathbf{L}^{-\text{T}}\mathbf{D}^{-1}\mathbf{L}^{-1}\mathbf{b}` in the
178
185
  symmetric case.
179
186
 
180
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
181
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
182
- """
183
- u1 = spla.solve_triangular(self.lp, rhs[self.p], lower=True, unit_diagonal=True)
184
- u2 = self.dinv(u1)
185
- u = np.zeros_like(rhs, dtype=u2.dtype)
186
- u[self.p] = spla.solve_triangular(self.lp, u2, trans='C' if self.hermitian else 'T', lower=True, unit_diagonal=True)
187
- return u
188
-
189
- def adjoint(self, rhs):
190
- r""" Solves the linear system of equations :math:`\mathbf{A}^\text{H} \mathbf{x} = \mathbf{b}` by forward and
187
+ The adjoint system of equations :math:`\mathbf{A}^\text{H} \mathbf{x} = \mathbf{b}` is solved by forward and
191
188
  backward substitution of
192
189
  :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{D}^{-\text{H}}\mathbf{L}^{-1}\mathbf{b}` in the Hermitian
193
190
  case or as :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{D}^{-\text{H}}\mathbf{L}^{-*}\mathbf{b}`
@@ -196,11 +193,34 @@ class SolverDenseLDL(LinearSolver):
196
193
  The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
197
194
  :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
198
195
  """
199
- if not self.hermitian:
200
- u1 = spla.solve_triangular(self.lp, rhs[self.p].conj(), lower=True, unit_diagonal=True).conj()
201
- else:
196
+ if trans == 'N':
197
+ # Hermitian matrix A: A = L D L^H -> inv(A) = inv(L^H) inv(D) inv(L)
198
+ # Symmetric matrix A: A = L D L^T -> inv(A) = inv(L^T) inv(D) inv(L)
202
199
  u1 = spla.solve_triangular(self.lp, rhs[self.p], lower=True, unit_diagonal=True)
203
- u2 = self.dinvH(u1)
204
- u = np.zeros_like(rhs, dtype=u2.dtype)
205
- u[self.p] = spla.solve_triangular(self.lp, u2, trans='C', lower=True, unit_diagonal=True)
200
+ u2 = self.dinv(u1)
201
+ u = np.zeros_like(rhs, dtype=u2.dtype)
202
+ u[self.p] = spla.solve_triangular(self.lp, u2, trans='C' if self.hermitian else 'T', lower=True, unit_diagonal=True)
203
+ elif trans == 'T':
204
+ # Hermitian matrix A^T: A = conj(L) D^T L^T -> inv(A) = inv(L^T) inv(D^T) inv(L^*)
205
+ # Symmetric matrix A^T: A = L D^T L^T -> inv(A) = inv(L^T) inv(D^T) inv(L)
206
+ if self.hermitian:
207
+ u1 = spla.solve_triangular(self.lp, rhs[self.p].conj(), lower=True, unit_diagonal=True).conj()
208
+ else:
209
+ u1 = spla.solve_triangular(self.lp, rhs[self.p], lower=True, unit_diagonal=True)
210
+
211
+ u2 = self.dinvH(u1.conj()).conj()
212
+ u = np.zeros_like(rhs, dtype=u2.dtype)
213
+ u[self.p] = spla.solve_triangular(self.lp, u2, trans='T', lower=True, unit_diagonal=True)
214
+ elif trans == 'H':
215
+ # Hermitian matrix A: inv(A^H) = inv(L^H) inv(D^H) inv(L)
216
+ # Symmetric matrix A: inv(A^H) = inv(L^H) inv(D^H) inv(L^*)
217
+ if not self.hermitian:
218
+ u1 = spla.solve_triangular(self.lp, rhs[self.p].conj(), lower=True, unit_diagonal=True).conj()
219
+ else:
220
+ u1 = spla.solve_triangular(self.lp, rhs[self.p], lower=True, unit_diagonal=True)
221
+ u2 = self.dinvH(u1)
222
+ u = np.zeros_like(rhs, dtype=u2.dtype)
223
+ u[self.p] = spla.solve_triangular(self.lp, u2, trans='C', lower=True, unit_diagonal=True)
224
+ else:
225
+ raise TypeError("Only N, T, and H transposition is possible")
206
226
  return u