# Householder Similarity Transforms¶

In [2]:
import numpy as np
import numpy.linalg as la

np.set_printoptions(precision=2, linewidth=150)

In [3]:
n = 8

e1 = np.zeros(n); e1[0] = 1
e2 = np.zeros(n); e2[1] = 1

A = np.random.randn(n, n)
A

Out[3]:
array([[-0.76, -0.2 , -0.93, -1.1 , -0.64, -0.83, -1.09, -3.14],
[-0.66,  1.32,  0.04,  1.15, -0.99,  0.31,  0.49, -1.12],
[-0.51,  0.74, -1.66, -1.4 , -1.19, -1.02, -1.76,  0.62],
[-0.27,  0.09,  0.37, -0.74,  1.1 ,  2.35, -0.35,  1.16],
[ 0.15, -0.88,  1.  ,  0.48,  0.54,  0.93,  0.94,  0.04],
[-0.07,  0.37, -0.16,  1.86,  0.52, -1.52,  0.16,  1.25],
[-2.22, -0.6 ,  0.76, -0.09, -0.07, -0.39,  0.75,  1.77],
[-0.99,  1.09, -0.56,  0.04,  2.26, -1.18, -0.82,  2.87]])

Now try to zero the first column with a similarity transform.

## Starting with the first row¶

Lets first try to proceed as in Householder QR, creating a transformation $$H=I-2\frac{vv^T}{v^Tv}$$ where $v = a_1 - ||a_1||_2e_1$ with $a_1$ being the first column of $A$.

In [4]:
a = A[:, 0].copy()
v = a-la.norm(a)*e1

H1 = np.eye(n) - 2*np.outer(v, v)/(v@v)


We can apply the transformation from the left as in QR to reduce the first column to a multiple of the first elementary vector.

In [5]:
(H1@A).round(4)

Out[5]:
array([[ 2.7 , -0.37,  0.16,  0.41, -0.21,  0.95,  0.28, -1.6 ],
[-0.  ,  1.28,  0.25,  1.44, -0.91,  0.65,  0.75, -0.83],
[-0.  ,  0.72, -1.5 , -1.18, -1.13, -0.75, -1.56,  0.84],
[-0.  ,  0.08,  0.45, -0.62,  1.14,  2.49, -0.25,  1.28],
[ 0.  , -0.87,  0.95,  0.41,  0.52,  0.85,  0.89, -0.03],
[-0.  ,  0.36, -0.14,  1.89,  0.52, -1.49,  0.19,  1.28],
[-0.  , -0.72,  1.46,  0.88,  0.21,  0.76,  1.63,  2.75],
[-0.  ,  1.05, -0.25,  0.47,  2.38, -0.67, -0.43,  3.31]])

However, to ensure we do not perturb the eigenvalues of $A$, we must also apply the matrix from the right, resulting in a similarity transformation.

In [6]:
(H1@A@H1.T).round(4)

Out[6]:
array([[-4.23e-01, -9.69e-01, -2.94e-01,  1.70e-01, -8.20e-02,  8.94e-01, -1.73e+00, -2.49e+00],
[-8.88e-01,  1.11e+00,  1.18e-01,  1.37e+00, -8.73e-01,  6.31e-01,  1.82e-01, -1.08e+00],
[ 1.16e+00,  9.36e-01, -1.33e+00, -1.09e+00, -1.18e+00, -7.32e-01, -8.14e-01,  1.17e+00],
[-3.04e-01,  2.31e-02,  4.10e-01, -6.47e-01,  1.15e+00,  2.48e+00, -4.43e-01,  1.19e+00],
[-7.19e-01, -1.01e+00,  8.44e-01,  3.55e-01,  5.50e-01,  8.40e-01,  4.24e-01, -2.33e-01],
[-8.12e-01,  2.10e-01, -2.56e-01,  1.82e+00,  5.59e-01, -1.50e+00, -3.32e-01,  1.05e+00],
[-2.54e+00, -1.20e+00,  1.09e+00,  6.80e-01,  3.18e-01,  7.12e-01, -2.20e-03,  2.03e+00],
[-9.66e-01,  8.61e-01, -3.87e-01,  3.93e-01,  2.42e+00, -6.90e-01, -1.05e+00,  3.04e+00]])

Note that applying the Householder transformation from the right filled in the elements annihilated by applying it from the left.

## Starting in the second row¶

To avoid this, we define the Householder transformation to annihilate elements below the first subdiagonal. That way, the first transformation does not affect the first row when applied from the left, and consequently does not affect the first column when applied for the right, preserving the zeros we've annihilated.

In [7]:
a = A[:, 0].copy()
a[0] = 0
v = a-la.norm(a)*e2

H2 = np.eye(n) - 2*np.outer(v, v)/(v@v)

In [8]:
(H2 @ A).round(4)

Out[8]:
array([[-0.76, -0.2 , -0.93, -1.1 , -0.64, -0.83, -1.09, -3.14],
[ 2.59, -0.45, -0.1 ,  0.11, -0.41,  0.75, -0.03, -2.6 ],
[ 0.  ,  0.47, -1.69, -1.56, -1.1 , -0.95, -1.84,  0.39],
[ 0.  , -0.05,  0.36, -0.83,  1.15,  2.39, -0.4 ,  1.04],
[-0.  , -0.8 ,  1.  ,  0.52,  0.51,  0.91,  0.97,  0.1 ],
[ 0.  ,  0.33, -0.16,  1.84,  0.53, -1.51,  0.15,  1.22],
[ 0.  , -1.81,  0.66, -0.81,  0.33, -0.09,  0.4 ,  0.76],
[ 0.  ,  0.56, -0.6 , -0.28,  2.43, -1.05, -0.98,  2.43]])
In [9]:
(H2 @ A @ H2.T).round(4)

Out[9]:
array([[-0.76,  2.46, -0.51, -0.88, -0.76, -0.78,  0.73, -2.33],
[ 2.59,  1.09,  0.14,  0.24, -0.48,  0.78,  1.03, -2.13],
[ 0.  ,  1.77, -1.48, -1.45, -1.16, -0.92, -0.95,  0.78],
[ 0.  , -0.02,  0.36, -0.83,  1.15,  2.39, -0.37,  1.05],
[-0.  , -0.91,  0.98,  0.51,  0.52,  0.91,  0.89,  0.07],
[ 0.  , -0.77, -0.33,  1.75,  0.58, -1.53, -0.6 ,  0.89],
[ 0.  , -0.19,  0.92, -0.67,  0.26, -0.05,  1.51,  1.25],
[ 0.  ,  0.08, -0.67, -0.32,  2.45, -1.06, -1.3 ,  2.28]])

To generalize this process, we continue to eliminate everything below the subdiagonal in the next column and applying the two-sided transformations, finally resulting in an upper-Hessenberg matrix.

Why does post-multiplying with H2.T not destroy the zeros?

In [10]:
H2.T.round(4)

Out[10]:
array([[ 1.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ],
[ 0.  , -0.25, -0.2 , -0.1 ,  0.06, -0.03, -0.86, -0.38],
[ 0.  , -0.2 ,  0.97, -0.02,  0.01, -0.  , -0.13, -0.06],
[ 0.  , -0.1 , -0.02,  0.99,  0.  , -0.  , -0.07, -0.03],
[ 0.  ,  0.06,  0.01,  0.  ,  1.  ,  0.  ,  0.04,  0.02],
[ 0.  , -0.03, -0.  , -0.  ,  0.  ,  1.  , -0.02, -0.01],
[ 0.  , -0.86, -0.13, -0.07,  0.04, -0.02,  0.41, -0.26],
[ 0.  , -0.38, -0.06, -0.03,  0.02, -0.01, -0.26,  0.88]])
In [ ]: