| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- '''
- This module provides some basic linear algebra procedures.
- Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
- Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
- Python translation by Nickolai Belakovski.
- '''
- import numpy as np
- from .consts import DEBUGGING, EPS, REALMAX, REALMIN
- from .present import present
- # We use naive implementations of matrix multiplication and other routines for two
- # reasons:
- # 1. When Fortran is compiled in debug mode, and Python is using these routines, we
- # can get bit for bit identical results as compared to Fortran. This is helpful
- # for comparing the two implementations. It will be particularly helpful when porting
- # the other implementations like LINCOA, etc.
- # 2. On some problems this algorithm is very sensitive to errors in finite precision
- # arithmetic. Switching to naive implementation will slow down the algorithm, but
- # may be more stable.
- USE_NAIVE_MATH = False
- def inprod(x, y):
- if not USE_NAIVE_MATH:
- return np.dot(x, y)
- result = 0
- for i in range(len(x)):
- result += x[i] * y[i]
- return result
- def matprod12(x, y):
- result = np.zeros(y.shape[1])
- for i in range(y.shape[1]):
- result[i] = inprod(x, y[:, i])
- return result
- def matprod21(x, y):
- result = np.zeros(x.shape[0])
- for i in range(x.shape[1]):
- result += x[:, i] * y[i]
- return result
- def matprod22(x, y):
- result = np.zeros((x.shape[0], y.shape[1]))
- for i in range(y.shape[1]):
- for j in range(x.shape[1]):
- result[:, j] += x[:, i] * y[i, j]
- return result
- def matprod(x, y):
- if not USE_NAIVE_MATH:
- return x@y
- if len(x.shape) == 1 and len(y.shape) == 1:
- return inprod(x, y)
- elif len(x.shape) == 1 and len(y.shape) == 2:
- return matprod12(x, y)
- elif len(x.shape) == 2 and len(y.shape) == 1:
- return matprod21(x, y)
- elif len(x.shape) == 2 and len(y.shape) == 2:
- return matprod22(x, y)
- else:
- raise ValueError(f'Invalid shapes for x and y: {x.shape} and {y.shape}')
- def outprod(x, y):
- if not USE_NAIVE_MATH:
- return np.outer(x, y)
- result = np.zeros((len(x), len(y)))
- for i in range(len(x)):
- result[:, i] = x * y[i]
- return result
- def lsqr(A, b, Q, Rdiag):
- if not USE_NAIVE_MATH:
- return np.linalg.lstsq(A, b, rcond=None)[0]
- m = A.shape[0]
- n = A.shape[1]
- rank = min(m, n)
- x = np.zeros(n)
- y = b.copy()
- for i in range(rank - 1, -1, -1):
- yq = inprod(y, Q[:, i])
- yqa = inprod(np.abs(y), np.abs(Q[:, i]))
- if isminor(yq, yqa):
- x[i] = 0
- else:
- x[i] = yq / Rdiag[i]
- y = y - x[i] * A[:, i]
- return x
- def hypot(x1, x2):
- if not USE_NAIVE_MATH:
- return np.hypot(x1, x2)
- if not np.isfinite(x1):
- r = abs(x1)
- elif not np.isfinite(x2):
- r = abs(x2)
- else:
- y = abs(np.array([x1, x2]))
- y = np.array([min(y), max(y)])
- if y[0] > np.sqrt(REALMIN) and y[1] < np.sqrt(REALMAX/2.1):
- r = np.sqrt(sum(y*y))
- elif y[1] > 0:
- r = y[1] * np.sqrt((y[0]/y[1])*(y[0]/y[1]) + 1)
- else:
- r = 0
- return r
- def norm(x):
- if not USE_NAIVE_MATH:
- return np.linalg.norm(x)
- # NOTE: Avoid np.pow! And exponentiation in general!
- # It appears that in Fortran, x*x and x**2 are the same, but in Python they are not!
- # Try it with x = 5 - 1e-15
- result = np.sqrt(sum([xi*xi for xi in x]))
- return result
- def istril(A, tol=0):
- return primasum(abs(A) - np.tril(abs(A))) <= tol
- def istriu(A, tol=0):
- return primasum(abs(A) - np.triu(abs(A))) <= tol
- def inv(A):
- if not USE_NAIVE_MATH:
- return np.linalg.inv(A)
- A = A.copy()
- n = A.shape[0]
- if istril(A):
- # This case is invoked in COBYLA.
- R = A.T
- B = np.zeros((n, n))
- for i in range(n):
- B[i, i] = 1 / R[i, i]
- B[:i, i] = -matprod(B[:i, :i], R[:i, i]) / R[i, i]
- return B.T
- elif istriu(A):
- B = np.zeros((n, n))
- for i in range(n):
- B[i, i] = 1 / A[i, i]
- B[:i, i] = -matprod(B[:i, :i], A[:i, i]) / A[i, i]
- else:
- # This is NOT the best algorithm for the inverse, but since the QR subroutine is available ...
- Q, R, P = qr(A)
- R = R.T
- B = np.zeros((n, n))
- for i in range(n - 1, -1, -1):
- B[:, i] = (Q[:, i] - matprod(B[:, i + 1:n], R[i + 1:n, i])) / R[i, i]
- InvP = np.zeros(n, dtype=int)
- InvP[P] = np.linspace(0, n-1, n)
- B = B[:, InvP].T
- return B
- def qr(A):
- m = A.shape[0]
- n = A.shape[1]
- Q = np.eye(m)
- T = A.T
- P = np.linspace(0, n-1, n, dtype=int)
- for j in range(n):
- k = np.argmax(primasum(primapow2(T[j:n+1, j:m+1]), axis=1), axis=0)
- if k > 0 and k <= n - j - 1:
- k += j
- P[j], P[k] = P[k], P[j]
- T[[j, k], :] = T[[k, j], :]
- for i in range(m-1, j, -1):
- G = planerot(T[j, [j, i]]).T
- T[j, [j, i]] = np.append(hypot(T[j, j], T[j, i]), 0)
- T[j + 1:n + 1, [j, i]] = matprod(T[j + 1:n + 1, [j, i]], G)
- Q[:, [j, i]] = matprod(Q[:, [j, i]], G)
- R = T.T
- return Q, R, P
- def primasum(x, axis=None):
- '''
- According to its documentation, np.sum will sometimes do partial pairwise summation.
- For our purposes, when comparing, we want don't want to do anything fancy, and we
- just want to add things up one at a time.
- '''
- if not USE_NAIVE_MATH:
- return np.sum(x, axis=axis)
- if axis is None:
- if x.ndim == 2:
- # Sum columns first, then sum the result
- return sum(primasum(x, axis=0))
- else:
- return sum(x)
- elif axis == 0:
- result = np.zeros(x.shape[1])
- for i in range(x.shape[1]):
- result[i] = sum(x[:, i])
- return result
- elif axis == 1:
- result = np.zeros(x.shape[0])
- for i in range(x.shape[0]):
- result[i] = sum(x[i, :])
- return result
- def primapow2(x):
- '''
- Believe it or now, x**2 is not always the same as x*x in Python. In Fortran they
- appear to be identical. Here's a quick one-line to find an example on your system
- (well, two liner after importing numpy):
- list(filter(lambda x: x[1], [(x:=np.random.random(), x**2 - x*x != 0) for _ in range(10000)]))
- '''
- return x*x
- def planerot(x):
- '''
- As in MATLAB, planerot(x) returns a 2x2 Givens matrix G for x in R2 so that Y=G@x has Y[1] = 0.
- Roughly speaking, G = np.array([[x[0]/R, x[1]/R], [-x[1]/R, x[0]/R]]), where R = np.linalg.norm(x).
- 0. We need to take care of the possibilities of R=0, Inf, NaN, and over/underflow.
- 1. The G defined above is continuous with respect to X except at 0. Following this definition,
- G = np.array([[np.sign(x[0]), 0], [0, np.sign(x[0])]]) if x[1] == 0,
- G = np.array([[0, np.sign(x[1])], [np.sign(x[1]), 0]]) if x[0] == 0
- Yet some implementations ignore the signs, leading to discontinuity and numerical instability.
- 2. Difference from MATLAB: if x contains NaN of consists of only Inf, MATLAB returns a NaN matrix,
- but we return an identity matrix or a matrix of +/-np.sqrt(2). We intend to keep G always orthogonal.
- '''
- # Preconditions
- if DEBUGGING:
- assert len(x) == 2, "x must be a 2-vector"
- # ==================
- # Calculation starts
- # ==================
- # Define C = X(1) / R and S = X(2) / R with R = HYPOT(X(1), X(2)). Handle Inf/NaN, over/underflow.
- if (any(np.isnan(x))):
- # In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
- c = 1
- s = 0
- elif (all(np.isinf(x))):
- # In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
- c = 1 / np.sqrt(2) * np.sign(x[0])
- s = 1 / np.sqrt(2) * np.sign(x[1])
- elif (abs(x[0]) <= 0 and abs(x[1]) <= 0): # X(1) == 0 == X(2).
- c = 1
- s = 0
- elif (abs(x[1]) <= EPS * abs(x[0])):
- # N.B.:
- # 0. With <= instead of <, this case covers X(1) == 0 == X(2), which is treated above separately
- # to avoid the confusing SIGN(., 0) (see 1).
- # 1. SIGN(A, 0) = ABS(A) in Fortran but sign(0) = 0 in MATLAB, Python, Julia, and R#
- # 2. Taking SIGN(X(1)) into account ensures the continuity of G with respect to X except at 0.
- c = np.sign(x[0])
- s = 0
- elif (abs(x[0]) <= EPS * abs(x[1])):
- # N.B.: SIGN(A, X) = ABS(A) * sign of X /= A * sign of X # Therefore, it is WRONG to define G
- # as SIGN(RESHAPE([ZERO, -ONE, ONE, ZERO], [2, 2]), X(2)). This mistake was committed on
- # 20211206 and took a whole day to debug! NEVER use SIGN on arrays unless you are really sure.
- c = 0
- s = np.sign(x[1])
- else:
- # Here is the normal case. It implements the Givens rotation in a stable & continuous way as in:
- # Bindel, D., Demmel, J., Kahan, W., and Marques, O. (2002). On computing Givens rotations
- # reliably and efficiently. ACM Transactions on Mathematical Software (TOMS), 28(2), 206-238.
- # N.B.: 1. Modern compilers compute SQRT(REALMIN) and SQRT(REALMAX/2.1) at compilation time.
- # 2. The direct calculation without involving T and U seems to work better; use it if possible.
- if (all(np.logical_and(np.sqrt(REALMIN) < np.abs(x), np.abs(x) < np.sqrt(REALMAX / 2.1)))):
- # Do NOT use HYPOTENUSE here; the best implementation for one may be suboptimal for the other
- r = norm(x)
- c = x[0] / r
- s = x[1] / r
- elif (abs(x[0]) > abs(x[1])):
- t = x[1] / x[0]
- u = max(1, abs(t), np.sqrt(1 + t*t)) # MAXVAL: precaution against rounding error.
- u *= np.sign(x[0]) ##MATLAB: u = sign(x(1))*sqrt(1 + t**2)
- c = 1 / u
- s = t / u
- else:
- t = x[0] / x[1]
- u = max([1, abs(t), np.sqrt(1 + t*t)]) # MAXVAL: precaution against rounding error.
- u *= np.sign(x[1]) ##MATLAB: u = sign(x(2))*sqrt(1 + t**2)
- c = t / u
- s = 1 / u
- G = np.array([[c, s], [-s, c]]) # MATLAB: G = [c, s; -s, c]
- #====================#
- # Calculation ends #
- #====================#
- # Postconditions
- if DEBUGGING:
- assert G.shape == (2,2)
- assert np.all(np.isfinite(G))
- assert abs(G[0, 0] - G[1, 1]) + abs(G[0, 1] + G[1, 0]) <= 0
- tol = np.maximum(1.0E-10, np.minimum(1.0E-1, 1.0E6 * EPS))
- assert isorth(G, tol)
- if all(np.logical_and(np.isfinite(x), np.abs(x) < np.sqrt(REALMAX / 2.1))):
- r = np.linalg.norm(x)
- assert max(abs(G@x - [r, 0])) <= max(tol, tol * r), 'G @ X = [||X||, 0]'
- return G
- def isminor(x, ref):
- '''
- This function tests whether x is minor compared to ref. It is used by Powell, e.g., in COBYLA.
- In precise arithmetic, isminor(x, ref) is true if and only if x == 0; in floating point
- arithmetic, isminor(x, ref) is true if x is 0 or its nonzero value can be attributed to
- computer rounding errors according to ref.
- Larger sensitivity means the function is more strict/precise, the value 0.1 being due to Powell.
- For example:
- isminor(1e-20, 1e300) -> True, because in floating point arithmetic 1e-20 cannot be added to
- 1e300 without being rounded to 1e300.
- isminor(1e300, 1e-20) -> False, because in floating point arithmetic adding 1e300 to 1e-20
- dominates the latter number.
- isminor(3, 4) -> False, because 3 can be added to 4 without being rounded off
- '''
- sensitivity = 0.1
- refa = abs(ref) + sensitivity * abs(x)
- refb = abs(ref) + 2 * sensitivity * abs(x)
- return np.logical_or(abs(ref) >= refa, refa >= refb)
- def isinv(A, B, tol=None):
- '''
- This procedure tests whether A = B^{-1} up to the tolerance TOL.
- '''
- # Sizes
- n = np.size(A, 0)
- # Preconditions
- if DEBUGGING:
- assert np.size(A, 0) == np.size(A, 1)
- assert np.size(B, 0) == np.size(B, 1)
- assert np.size(A, 0) == np.size(B, 0)
- if present(tol):
- assert tol >= 0
- #====================#
- # Calculation starts #
- #====================#
- tol = tol if present(tol) else np.minimum(1e-3, 1e2 * EPS * np.maximum(np.size(A, 0), np.size(A, 1)))
- tol = np.max([tol, tol * np.max(abs(A)), tol * np.max(abs(B))])
- is_inv = ((abs(matprod(A, B)) - np.eye(n)) <= tol).all() or ((abs(matprod(B, A) - np.eye(n))) <= tol).all()
- #===================#
- # Calculation ends #
- #===================#
- return is_inv
- def isorth(A, tol=None):
- '''
- This function tests whether the matrix A has orthonormal columns up to the tolerance TOL.
- '''
- # Preconditions
- if DEBUGGING:
- if present(tol):
- assert tol >= 0
- #====================#
- # Calculation starts #
- #====================#
- num_vars = np.size(A, 1)
- if num_vars > np.size(A, 0):
- is_orth = False
- elif (np.isnan(primasum(abs(A)))):
- is_orth = False
- else:
- if present(tol):
- is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= np.maximum(tol, tol * np.max(abs(A)))).all()
- else:
- is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= 0).all()
- #====================#
- # Calculation ends #
- #====================#
- return is_orth
- def get_arrays_tol(*arrays):
- """
- Get a relative tolerance for a set of arrays. Borrowed from COBYQA
- Parameters
- ----------
- *arrays: tuple
- Set of `numpy.ndarray` to get the tolerance for.
- Returns
- -------
- float
- Relative tolerance for the set of arrays.
- Raises
- ------
- ValueError
- If no array is provided.
- """
- if len(arrays) == 0:
- raise ValueError("At least one array must be provided.")
- size = max(array.size for array in arrays)
- weight = max(
- np.max(np.abs(array[np.isfinite(array)]), initial=1.0)
- for array in arrays
- )
- return 10.0 * EPS * max(size, 1.0) * weight
|