Gram-Schmidt and Modified Gram-Schmidt¶

In [3]:
import numpy as np
import numpy.linalg as la
In [4]:
A = np.random.randn(3, 3)
In [5]:
def test_orthogonality(Q):
    print("Q:")
    print(Q.round(9))
    
    print("Q^T Q:")
    QtQ = np.dot(Q.T, Q)
    QtQ[np.abs(QtQ) < 1e-15] = 0
    print(QtQ.round(9))
In [6]:
Q = np.zeros(A.shape)

Now let us generalize the process we used for three vectors earlier:

In [7]:
for k in range(A.shape[1]):
    avec = A[:, k]
    q = avec
    for j in range(k):
        coeff = avec @ Q[:,j]
        q = q - coeff*Q[:,j]
    
    Q[:, k] = q/la.norm(q)

This procedure is called Gram-Schmidt Orthonormalization.

In [8]:
test_orthogonality(Q)
Q:
[[-0.58228521  0.04686309 -0.81163279]
 [-0.70728599  0.4630539   0.53416067]
 [ 0.40086215  0.88509035 -0.23648384]]
Q^T Q:
[[ 1.  0. -0.]
 [ 0.  1.  0.]
 [-0.  0.  1.]]

Now let us try a different example (Source):

In [15]:
np.set_printoptions(precision=13)

eps = 1e-8

A = np.array([
    [1,  1,  1],
    [eps,eps,0],
    [eps,0,  eps]
    ])

A
Out[15]:
array([[1.e+00, 1.e+00, 1.e+00],
       [1.e-08, 1.e-08, 0.e+00],
       [1.e-08, 0.e+00, 1.e-08]])
In [16]:
Q = np.zeros(A.shape)
In [22]:
for k in range(A.shape[1]):
    avec = A[:, k]
    q = avec
    for j in range(k):
        coeff = avec @ Q[:, j]
        print(q, coeff)
        q = q - coeff * Q[:,j]
    
    print(q)
    Q[:, k] = q/la.norm(q)
    print("norm -->", Q[:, k])
    print("-------")
[1.e+00 1.e-08 1.e-08]
norm --> [1.e+00 1.e-08 1.e-08]
-------
[1.e+00 1.e-08 0.e+00] 1.0
[ 0.e+00  0.e+00 -1.e-08]
norm --> [ 0.  0. -1.]
-------
[1.e+00 0.e+00 1.e-08] 1.0
[ 0.e+00 -1.e-08  0.e+00] -1e-08
[ 0.e+00 -1.e-08 -1.e-08]
norm --> [ 0.              -0.7071067811865 -0.7071067811865]
-------
In [18]:
test_orthogonality(Q)
Q:
[[ 1.00000000e+00  0.00000000e+00  0.00000000e+00]
 [ 1.00000000e-08  0.00000000e+00 -7.07106781e-01]
 [ 1.00000000e-08 -1.00000000e+00 -7.07106781e-01]]
Q^T Q:
[[ 1.00000000e+00 -1.00000000e-08 -1.40000000e-08]
 [-1.00000000e-08  1.00000000e+00  7.07106781e-01]
 [-1.40000000e-08  7.07106781e-01  1.00000000e+00]]

Questions:

  • What happened?
  • How do we fix it?
In [19]:
Q = np.zeros(A.shape)
In [21]:
for k in range(A.shape[1]):
    q = A[:, k]
    for j in range(k):
        coeff = q @ Q[:,j]
        print(q, coeff)
        q = q - np.dot(q, Q[:,j])*Q[:,j]

    print(q)
    Q[:, k] = q/la.norm(q)
    print("norm -->", Q[:,k])
    print("-------")    
[1.e+00 1.e-08 1.e-08]
norm --> [1.e+00 1.e-08 1.e-08]
-------
[1.e+00 1.e-08 0.e+00] 1.0
[ 0.e+00  0.e+00 -1.e-08]
norm --> [ 0.  0. -1.]
-------
[1.e+00 0.e+00 1.e-08] 1.0
[ 0.e+00 -1.e-08  0.e+00] 0.0
[ 0.e+00 -1.e-08  0.e+00]
norm --> [ 0. -1.  0.]
-------
In [48]:
test_orthogonality(Q)
Q:
[[  1.0000000000000e+00   0.0000000000000e+00   0.0000000000000e+00]
 [  1.0000000000000e-08   0.0000000000000e+00  -1.0000000000000e+00]
 [  1.0000000000000e-08  -1.0000000000000e+00   0.0000000000000e+00]]
Q^T Q:
[[  1.0000000000000e+00  -1.0000000000000e-08  -1.0000000000000e-08]
 [ -1.0000000000000e-08   1.0000000000000e+00   0.0000000000000e+00]
 [ -1.0000000000000e-08   0.0000000000000e+00   1.0000000000000e+00]]

This procedure is called Modified Gram-Schmidt Orthogonalization.

Questions:

  • Is there a difference mathematically between modified and unmodified?
  • Why are there $10^{-8}$ values left in $Q^TQ$?