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.
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/METADATA +7 -8
- pyMOTO-1.5.0.dist-info/RECORD +29 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +17 -11
- pymoto/common/domain.py +61 -5
- pymoto/common/dyadcarrier.py +87 -29
- pymoto/common/mma.py +142 -129
- pymoto/core_objects.py +129 -117
- pymoto/modules/aggregation.py +209 -0
- pymoto/modules/assembly.py +250 -10
- pymoto/modules/complex.py +3 -3
- pymoto/modules/filter.py +171 -24
- pymoto/modules/generic.py +12 -1
- pymoto/modules/io.py +85 -12
- pymoto/modules/linalg.py +92 -120
- pymoto/modules/scaling.py +5 -4
- pymoto/routines.py +34 -9
- pymoto/solvers/__init__.py +14 -0
- pymoto/solvers/auto_determine.py +108 -0
- pymoto/{common/solvers_dense.py → solvers/dense.py} +90 -70
- pymoto/solvers/iterative.py +361 -0
- pymoto/solvers/matrix_checks.py +60 -0
- pymoto/solvers/solvers.py +253 -0
- pymoto/{common/solvers_sparse.py → solvers/sparse.py} +42 -29
- pyMOTO-1.3.0.dist-info/RECORD +0 -24
- pymoto/common/solvers.py +0 -236
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/LICENSE +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.3.0.dist-info → pyMOTO-1.5.0.dist-info}/zip-safe +0 -0
@@ -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 .
|
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 /
|
23
|
+
return rhs / d
|
22
24
|
else:
|
23
|
-
return rhs /
|
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
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
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
|
-
|
79
|
-
:math:`\mathbf{
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
91
|
-
|
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.
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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.
|
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
|
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
|
200
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|