MODFLOW 6  version 6.7.0.dev2
USGS Modular Hydrologic Model
SingularValueDecomposition.f90
Go to the documentation of this file.
1 module svdmodule
2  use kindmodule, only: dp, lgp, i4b
3  use constantsmodule, only: done, dprec, dsame, dem6
5 
6  implicit none
7  private
8 
9  public :: svd
10  public :: bidiagonal_decomposition
12 
13 contains
14 
15  !!> @brief Constructs a Householder reflection matrix for a given vector.
16  !!
17  !<
18  pure function house_holder_matrix(x) result(Q)
19  ! -- dummy
20  real(dp), intent(in) :: x(:)
21  real(dp), allocatable, dimension(:, :) :: q
22  ! -- locals
23  real(dp) :: x_norm, y_norm
24  real(dp), allocatable :: v(:)
25  real(dp), allocatable :: w(:)
26 
27  x_norm = norm2(x)
28  y_norm = norm2(x(2:))
29 
30  q = eye(size(x))
31 
32  if (dabs(y_norm) < dsame) then
33  return
34  end if
35 
36  v = x
37  if (x(1) > 0) then
38  ! Parlett (1971) suggested this modification to avoid cancellation
39  v(1) = -(y_norm**2) / (x(1) + x_norm)
40  else
41  v(1) = v(1) - x_norm
42  end if
43 
44  w = v / norm2(v)
45 
46  q = q - 2.0_dp * outer_product(w, w)
47 
48  end function house_holder_matrix
49 
50  !!> @brief Reduces a matrix to bidiagonal form using Householder transformations.
51  !!
52  !! This subroutine transforms the input matrix `A` into a bidiagonal matrix by applying a sequence of
53  !! Householder reflections from the left and right. The orthogonal transformations are accumulated in
54  !! the matrices `P` (left transformations) and `Qt` (right transformations).
55  !!
56  !! The result is:
57  !! P^T * A * Qt = B
58  !! where `B` is bidiagonal, `P` and `Qt` are orthogonal matrices.
59  !!
60  !! This step is the first phase of the Golub-Reinsch SVD algorithm and prepares the matrix for further
61  !! diagonalization using the QR algorithm.
62  !!
63  !<
64  pure subroutine bidiagonal_decomposition(A, P, Qt)
65  ! -- dummy
66  real(dp), intent(inout), dimension(:, :) :: a
67  real(dp), intent(out), dimension(:, :), allocatable :: p, qt
68  ! -- locals
69  integer(I4B) :: m, n
70  integer(I4B) :: i
71  real(dp), allocatable, dimension(:, :) :: qi, pi
72  real(dp), allocatable, dimension(:) :: h
73 
74  m = size(a, dim=1) ! Number of rows
75  n = size(a, dim=2) ! Number of columns
76 
77  qt = eye(n)
78  p = eye(m)
79 
80  do i = 1, min(m, n)
81  ! columns
82  h = a(i:m, i)
83  pi = house_holder_matrix(h)
84  a(i:m, :) = matmul(pi, a(i:m, :)) ! Apply householder transformation from left
85  p(:, i:m) = matmul(p(:, i:m), pi)
86 
87  ! rows
88  if (i < n) then
89  h = a(i, i + 1:n)
90  qi = transpose(house_holder_matrix(h))
91  a(:, i + 1:n) = matmul(a(:, i + 1:n), qi) ! Apply householder transformation from right
92  qt(i + 1:n, :) = matmul(qi, qt(i + 1:n, :))
93  end if
94 
95  end do
96 
97  end subroutine bidiagonal_decomposition
98 
99  !!> @brief Constructs a Givens rotation matrix.
100  !!
101  !<
102  pure function givens_rotation(a, b) result(G)
103  ! -- dummy
104  real(dp), intent(in) :: a, b
105  real(dp), dimension(2, 2) :: g
106  ! -- locals
107  real(dp) :: c, s, h, d
108 
109  if (abs(b) < dprec) then
110  g = eye(2)
111  return
112  end if
113 
114  h = hypot(a, b)
115  d = 1.0 / h
116  c = abs(a) * d
117  s = sign(d, a) * b
118 
119  g(1, 1) = c
120  g(1, 2) = s
121  g(2, 1) = -s
122  g(2, 2) = c
123 
124  end function givens_rotation
125 
126  !!> @brief Computes the Wilkinson shift for the lower-right 2x2 block of a bidiagonal matrix.
127  !!
128  !! This function calculates the Wilkinson shift, which is used to accelerate convergence in the implicit QR algorithm
129  !! for bidiagonal matrices during SVD. The shift is computed from the lower-right 2x2 block of the input matrix `A`
130  !! and is chosen to be the eigenvalue of that block which is closer to the bottom-right element.
131  !!
132  !! The shift is used to improve numerical stability and speed up the annihilation of off-diagonal elements.
133  !!
134  !<
135  pure function compute_shift(A) result(mu)
136  ! -- dummy
137  real(dp), intent(in), dimension(:, :) :: a
138  real(dp) :: mu
139  ! -- locals
140  integer(I4B) :: m, n, min_mn
141  real(dp) t11, t12, t21, t22
142  real(dp) dm, fmmin, fm, dn
143  real(dp) :: mean, product, mean_product, mu1, mu2
144 
145  m = size(a, dim=1) ! Number of rows
146  n = size(a, dim=2) ! Number of columns
147 
148  min_mn = min(m, n)
149 
150  dn = a(min_mn, min_mn)
151  dm = a(min_mn - 1, min_mn - 1)
152  fm = a(min_mn - 1, min_mn)
153  if (min_mn > 2) then
154  fmmin = a(min_mn - 2, min_mn - 1)
155  else
156  fmmin = 0.0_dp
157  end if
158 
159  t11 = dm**2 + fmmin**2
160  t12 = dm * fm
161  t21 = t12
162  t22 = fm**2 + dn**2
163 
164  mean = (t11 + t22) / 2.0_dp
165  product = t11 * t22 - t12 * t21
166  mean_product = mean**2 - product
167  if (abs(mean_product) < dem6) then
168  mean_product = 0.0_dp
169  end if
170  mu1 = mean - sqrt(mean_product)
171  mu2 = mean + sqrt(mean_product)
172  if (abs(t22 - mu1) < abs(t22 - mu2)) then
173  mu = mu1
174  else
175  mu = mu2
176  end if
177 
178  end function compute_shift
179 
180  !!> @brief Performs QR iterations on a bidiagonal matrix as part of the SVD algorithm.
181  !!
182  !<
183  pure subroutine bidiagonal_qr_decomposition(A, U, VT)
184  ! -- dummy
185  real(dp), intent(inout), dimension(:, :) :: a
186  real(dp), intent(inout), dimension(:, :) :: u, vt
187  ! -- locals
188  integer(I4B) :: m, n, i, j
189  real(dp), dimension(2, 2) :: g
190  real(dp) :: t11, t12, mu
191 
192  m = size(a, dim=1) ! Number of rows
193  n = size(a, dim=2) ! Number of columns
194 
195  DO i = 1, n - 1
196  j = i + 1
197  if (i == 1) then
198  ! For the first iteration use the implicit shift
199  mu = compute_shift(a)
200  ! Apply the shift to the full tri-diagonal matrix.
201  t11 = a(1, 1)**2 - mu
202  t12 = a(1, 1) * a(1, 2)
203 
204  g = givens_rotation(t11, t12)
205  else
206  g = givens_rotation(a(i - 1, i), a(i - 1, j))
207  end if
208 
209  a(:, i:j) = matmul(a(:, i:j), transpose(g))
210  vt(i:j, :) = matmul(g, vt(i:j, :))
211 
212  if (j > m) cycle
213  g = givens_rotation(a(i, i), a(j, i))
214  a(i:j, :) = matmul(g, a(i:j, :))
215  u(:, i:j) = matmul(u(:, i:j), transpose(g))
216  END DO
217 
218  end subroutine bidiagonal_qr_decomposition
219 
220  !!> @brief Handles zero (or near-zero) diagonal elements in a bidiagonal matrix during SVD.
221  !!
222  !! This subroutine detects and processes zero (or near-zero) diagonal elements in the bidiagonal matrix `A`.
223  !! If a zero is found on the diagonal, it applies a sequence of Givens rotations to either zero out the
224  !! corresponding column (if the zero is at the end of the diagonal) or the corresponding row (otherwise).
225  !! The orthogonal transformations are accumulated in the `U` and `VT` matrices as appropriate.
226  !! This step is essential for splitting the matrix into smaller blocks and ensuring numerical stability
227  !! during the iterative QR step of the SVD algorithm.
228  !!
229  !<
230  pure subroutine handle_zero_diagonal(A, U, VT)
231  ! -- dummy
232  real(dp), intent(inout), dimension(:, :) :: a
233  real(dp), intent(inout), dimension(:, :) :: u, vt
234  ! -- locals
235  integer(I4B) :: m, n, i
236  real(dp), dimension(2, 2) :: g
237  integer(I4B) :: zero_index
238 
239  m = size(a, dim=1) ! Number of rows
240  n = size(a, dim=2) ! Number of columns
241 
242  do i = 1, min(m, n)
243  if (abs(a(i, i)) < dprec) then
244  zero_index = i
245  exit
246  end if
247  end do
248 
249  if (zero_index == min(n, m)) then
250  ! If the zero index is the last element of the diagonal then zero out the column
251  do i = zero_index - 1, 1, -1
252  g = givens_rotation(a(i, i), a(i, zero_index))
253  a(:, [i, zero_index]) = matmul(a(:, [i, zero_index]), transpose(g))
254  vt([i, zero_index], :) = matmul(g, vt([i, zero_index], :))
255  end do
256  else
257  ! Else zero out the row
258  do i = zero_index + 1, n
259  g = givens_rotation(a(i, i), a(zero_index, i))
260  a([zero_index, i], :) = matmul(transpose(g), a([zero_index, i], :))
261  u(:, [zero_index, i]) = matmul(u(:, [zero_index, i]), g)
262  end do
263  end if
264  end subroutine handle_zero_diagonal
265 
266  !!> @brief Computes the infinity norm of the superdiagonal elements of a matrix.
267  !!
268  !<
269  pure function superdiagonal_norm(A) result(norm)
270  ! -- dummy
271  real(dp), intent(in) :: a(:, :)
272  real(dp) :: norm
273  ! -- locals
274  integer(I4B) :: m, n, i
275 
276  m = size(a, dim=1) ! Number of rows
277  n = size(a, dim=2) ! Number of columns
278 
279  norm = 0.0_dp
280  do i = 1, min(m, n) - 1
281  norm = max(norm, abs(a(i, i + 1)))
282  end do
283 
284  end function superdiagonal_norm
285 
286  !!> @brief Finds the range of nonzero superdiagonal elements in a bidiagonal matrix.
287  !!
288  !! This subroutine scans the superdiagonal of the matrix `A` and determines the indices `p` and `q`
289  !! such that all superdiagonal elements outside the range `A(p:q, p:q)` are (near-)zero, while
290  !! those within the range may be nonzero. This is useful for identifying active submatrices during
291  !! the iterative QR step of the SVD algorithm.
292  !!
293  !! The search starts from the lower-right corner and moves upward to find the last nonzero
294  !! superdiagonal (sets `q`), then moves upward again to find the first zero (sets `p`).
295  !<
296  pure subroutine find_nonzero_superdiagonal(A, p, q)
297  ! -- dummy
298  real(dp), intent(in), dimension(:, :) :: a
299  integer(I4B), intent(out) :: p ! Index of the first nonzero superdiagonal (start of active block)
300  integer(I4B), intent(out) :: q ! Index of the last nonzero superdiagonal (end of active block)
301  ! -- locals
302  integer(I4B) :: m, n, j, min_mn
303 
304  m = size(a, dim=1) ! Number of rows
305  n = size(a, dim=2) ! Number of columns
306 
307  min_mn = min(m, n)
308  p = 1
309  q = min_mn
310 
311  do j = min_mn, 2, -1
312  if (abs(a(j - 1, j)) > dprec) then
313  q = j
314  exit
315  end if
316  end do
317 
318  do j = q - 1, 2, -1
319  if (abs(a(j - 1, j)) < dprec) then
320  p = j
321  exit
322  end if
323  end do
324  end subroutine find_nonzero_superdiagonal
325 
326  !!> @brief Checks if a matrix has a (near-)zero diagonal element.
327  !!
328  !<
329  pure function has_zero_diagonal(A) result(has_zero)
330  ! -- dummy
331  real(dp), intent(in) :: a(:, :)
332  logical(LGP) :: has_zero
333  ! -- locals
334  integer(I4B) :: m, n, i
335 
336  m = size(a, dim=1) ! Number of rows
337  n = size(a, dim=2) ! Number of columns
338 
339  has_zero = .false.
340  do i = 1, min(m, n)
341  if (abs(a(i, i)) < dprec) then
342  has_zero = .true.
343  exit
344  end if
345  end do
346 
347  end function has_zero_diagonal
348 
349  !!> @brief Reduces a rectangular matrix to square form using Givens rotations.
350  !!
351  !! This subroutine transforms a rectangular matrix `A` into a square matrix by applying Givens rotations
352  !! to eliminate extra columns (when the number of columns exceeds the number of rows).
353  !! The corresponding orthogonal transformations are accumulated in the matrix `Qt`.
354  !<
355  pure subroutine make_matrix_square(A, Qt)
356  ! -- dummy
357  real(dp), intent(inout), dimension(:, :) :: a
358  real(dp), intent(inout), dimension(:, :), allocatable :: qt
359  ! -- locals
360  real(dp), dimension(2, 2) :: g
361  integer(I4B) :: m, n, i
362 
363  m = size(a, dim=1) ! Number of rows
364  n = size(a, dim=2) ! Number of columns
365 
366  do i = m, 1, -1
367  g = givens_rotation(a(i, i), a(i, m + 1))
368  a(:, [i, m + 1]) = matmul(a(:, [i, m + 1]), transpose(g))
369  qt([i, m + 1], :) = matmul(g, qt([i, m + 1], :))
370  end do
371 
372  end subroutine make_matrix_square
373 
374  !!> @brief Sets small superdiagonal elements to zero for numerical stability.
375  !!
376  !! This subroutine scans the superdiagonal elements of the matrix `A` and sets to zero any element
377  !! whose absolute value is less than or equal to a tolerance (relative to the neighboring diagonal elements).
378  !! This helps to maintain numerical stability and ensures that the matrix is properly treated as bidiagonal
379  !! in subsequent SVD steps.
380  !!
381  !! The criterion used is:
382  !! |A(j, j+1)| <= DPREC * (|A(j, j)| + |A(j+1, j+1)|)
383  !! In such cases, A(j, j+1) is set to zero.
384  !<
385  pure subroutine clean_superdiagonal(A)
386  ! -- dummy
387  real(dp), intent(inout), dimension(:, :) :: a
388  ! -- locals
389  integer(I4B) :: n, m, j
390 
391  m = size(a, dim=1) ! Number of rows
392  n = size(a, dim=2) ! Number of columns
393 
394  do j = 1, min(n, m) - 1
395  if (abs(a(j, j + 1)) <= dprec * (abs(a(j, j)) + abs(a(j + 1, j + 1)))) then
396  a(j, j + 1) = 0.0_dp
397  end if
398  end do
399 
400  end subroutine clean_superdiagonal
401 
402  !> @brief Singular Value Decomposition
403  !!
404  !! This method decomposes the matrix A into U, S and VT.
405  !! It follows the algorithm as described by Golub and Reinsch.
406  !!
407  !! The first step is to decompose the matrix A into a bidiagonal matrix.
408  !! This is done using Householder transformations.
409  !! Then second step is to decompose the bidiagonal matrix into U, S and VT
410  !! by repetitively applying the QR algorithm.
411  !! If there is a zero on the diagonal or superdiagonal the matrix can be split
412  !! into two smaller matrices and the QR algorithm can be applied to the smaller
413  !! matrices.
414  !!
415  !! The matrix U is the eigenvectors of A*A^T
416  !! The matrix VT is the eigenvectors of A^T*A
417  !! The matrix S is the square root of the eigenvalues of A*A^T or A^T*A
418  !!
419  !<
420  pure subroutine svd(A, U, S, VT)
421  ! -- dummy
422  real(dp), intent(in), dimension(:, :) :: a
423  real(dp), intent(out), dimension(:, :), allocatable :: u
424  real(dp), intent(out), dimension(:, :), allocatable :: s
425  real(dp), intent(out), dimension(:, :), allocatable :: vt
426  ! -- locals
427  integer(I4B) :: i ! Loop counter for QR iterations
428  integer(I4B) :: max_itr ! Maximum number of QR iterations allowed for convergence
429  real(dp) :: error ! Convergence criterion (superdiagonal norm)
430 
431  integer(I4B) :: m ! Number of rows in input matrix A
432  integer(I4B) :: n ! Number of columns in input matrix A
433 
434  integer(I4B) :: r ! Start index of active submatrix (for QR step)
435  integer(I4B) :: q ! End index of active submatrix (for QR step)
436 
437  max_itr = 100
438 
439  m = size(a, dim=1) ! Number of rows
440  n = size(a, dim=2) ! Number of columns
441  s = a
442 
443  call bidiagonal_decomposition(s, u, vt)
444 
445  if (n > m) then
446  call make_matrix_square(s, vt)
447  end if
448 
449  do i = 1, max_itr
450  call find_nonzero_superdiagonal(s, r, q)
451 
452  ! find zero diagonal
453  if (has_zero_diagonal(s(r:q, r:q))) then
454  ! write(*,*) 'Iteration: ', i, ' handle zero diagonal element'
455  call handle_zero_diagonal(s(r:q, r:q), u(:, r:q), vt(r:q, :))
456  else
457  call bidiagonal_qr_decomposition(s(r:q, r:q), u(:, r:q), vt(r:q, :))
458  call clean_superdiagonal(s(r:q, r:q))
459 
460  error = superdiagonal_norm(s)
461  ! write(*,*) 'Iteration: ', i, ' Error: ', error
462  if (error < dprec) exit
463  end if
464  end do
465 
466  end subroutine svd
467 
468 end module
This module contains simulation constants.
Definition: Constants.f90:9
real(dp), parameter dsame
real constant for values that are considered the same based on machine precision
Definition: Constants.f90:122
real(dp), parameter dem6
real constant 1e-6
Definition: Constants.f90:109
real(dp), parameter dprec
real constant machine precision
Definition: Constants.f90:120
real(dp), parameter done
real constant 1
Definition: Constants.f90:76
This module defines variable data types.
Definition: kind.f90:8
pure real(dp) function, dimension(size(a), size(b)), public outer_product(A, B)
pure real(dp) function, dimension(:, :), allocatable, public eye(n)
pure real(dp) function compute_shift(A)
pure subroutine, public bidiagonal_decomposition(A, P, Qt)
pure subroutine find_nonzero_superdiagonal(A, p, q)
pure subroutine clean_superdiagonal(A)
pure logical(lgp) function has_zero_diagonal(A)
pure real(dp) function, dimension(:, :), allocatable house_holder_matrix(x)
pure subroutine make_matrix_square(A, Qt)
pure real(dp) function superdiagonal_norm(A)
pure subroutine, public svd(A, U, S, VT)
Singular Value Decomposition.
pure subroutine handle_zero_diagonal(A, U, VT)
pure real(dp) function, dimension(2, 2) givens_rotation(a, b)
pure subroutine, public bidiagonal_qr_decomposition(A, U, VT)