Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Prerequisites: Matrix multiplication (ch154), identity matrix (ch156) You will learn:

  • What makes a matrix invertible

  • How to compute the inverse (via cofactors, Gauss-Jordan, numpy)

  • When NOT to compute the inverse (numerical stability)

  • The pseudoinverse for non-square matrices

Environment: Python 3.x, numpy

# --- Matrix Inverse: Definition, Computation, Pitfalls ---
import numpy as np

def inverse_gauss_jordan(A):
    """
    Compute matrix inverse via Gauss-Jordan elimination.
    Augments [A | I] and row-reduces to [I | A⁻¹].

    Args:
        A: square 2D numpy array, shape (n,n)

    Returns:
        A_inv: 2D numpy array, shape (n,n)

    Raises:
        ValueError if A is singular.
    """
    n = A.shape[0]
    assert A.shape == (n, n), "Must be square"
    # Augmented matrix [A | I]
    aug = np.hstack([A.astype(float).copy(), np.eye(n)])
    for col in range(n):
        # Find pivot
        pivot_row = col + np.argmax(np.abs(aug[col:, col]))
        if abs(aug[pivot_row, col]) < 1e-12:
            raise ValueError("Matrix is singular or nearly singular")
        aug[[col, pivot_row]] = aug[[pivot_row, col]]   # swap rows
        # Normalize pivot row
        aug[col] /= aug[col, col]
        # Eliminate in all other rows
        for row in range(n):
            if row != col:
                aug[row] -= aug[row, col] * aug[col]
    return aug[:, n:]   # right half is A⁻¹

# Test
np.random.seed(42)
A = np.array([[2., 1., 0.],
              [1., 3., 1.],
              [0., 1., 2.]])

A_inv_scratch = inverse_gauss_jordan(A)
A_inv_numpy   = np.linalg.inv(A)

print("Inverse via Gauss-Jordan:")
print(np.round(A_inv_scratch, 6))
print("\nMax diff from numpy: ", np.max(np.abs(A_inv_scratch - A_inv_numpy)))
print("\nVerify AA⁻¹ = I:", np.allclose(A @ A_inv_scratch, np.eye(3)))

# When NOT to invert: solve instead
b = np.array([1., 2., 3.])
# Bad: x = A⁻¹ @ b  (computes full inverse, then multiplies)
x_inv = np.linalg.inv(A) @ b
# Good: x = np.linalg.solve(A, b)  (uses LU decomposition, faster and more stable)
x_solve = np.linalg.solve(A, b)
print(f"\nSolve via inv: {x_inv}")
print(f"Solve via solve: {x_solve}")
print(f"Equal: {np.allclose(x_inv, x_solve)}")
print("\nRule: Never compute A⁻¹ explicitly if you only need A⁻¹b.")
Inverse via Gauss-Jordan:
[[ 0.625 -0.25   0.125]
 [-0.25   0.5   -0.25 ]
 [ 0.125 -0.25   0.625]]

Max diff from numpy:  0.0

Verify AA⁻¹ = I: True

Solve via inv: [0.5 0.  1.5]
Solve via solve: [5.0000000e-01 8.8817842e-17 1.5000000e+00]
Equal: True

Rule: Never compute A⁻¹ explicitly if you only need A⁻¹b.

4. Mathematical Formulation

A⁻¹ exists iff A is square AND det(A) ≠ 0 (equivalently, full rank).

AA⁻¹ = A⁻¹A = I

Properties:
  (A⁻¹)⁻¹ = A
  (AB)⁻¹  = B⁻¹A⁻¹         [reversal, same as transpose]
  (Aᵀ)⁻¹  = (A⁻¹)ᵀ
  det(A⁻¹) = 1/det(A)

For 2×2:  A = [[a,b],[c,d]]
  A⁻¹ = (1/(ad-bc)) * [[d,-b],[-c,a]]

Pseudoinverse (Moore-Penrose) A⁺:
  For any A (not necessarily square):
  A⁺ = Vᵀ Σ⁺ Uᵀ  (via SVD — see ch173)
  When A is full rank: A⁺ = (AᵀA)⁻¹Aᵀ (left inverse)
# --- Pseudoinverse for non-square systems ---
import numpy as np

# Overdetermined system: more equations than unknowns
# A is (5,3) — 5 equations, 3 unknowns
A = np.array([[1,0,0],[0,1,0],[0,0,1],[1,1,0],[0,1,1]], dtype=float)
b = np.array([1., 2., 3., 3.5, 4.5])   # inconsistent — no exact solution

# Pseudoinverse gives least-squares solution
A_pinv = np.linalg.pinv(A)
x_lstsq = A_pinv @ b

print(f"A shape: {A.shape}, A⁺ shape: {A_pinv.shape}")
print(f"Least-squares solution: {x_lstsq.round(4)}")
print(f"Residual ||Ax - b||: {np.linalg.norm(A @ x_lstsq - b):.4f}")

# Underdetermined: fewer equations than unknowns (infinitely many solutions)
# pinv gives minimum-norm solution
A_under = np.array([[1,1,0],[0,1,1]], dtype=float)  # 2 eq, 3 unknowns
b_under = np.array([3., 2.])
x_min_norm = np.linalg.pinv(A_under) @ b_under
print(f"\nUnderdetermined min-norm solution: {x_min_norm.round(4)}")
print(f"||x||: {np.linalg.norm(x_min_norm):.4f}")
A shape: (5, 3), A⁺ shape: (3, 5)
Least-squares solution: [1.25 2.   2.75]
Residual ||Ax - b||: 0.5000

Underdetermined min-norm solution: [1.3333 1.6667 0.3333]
||x||: 2.1602

7. Exercises

Easy 1. Compute by hand the inverse of [[2,1],[1,1]] using the 2×2 formula. Verify with NumPy.

Easy 2. Why does [[1,2],[2,4]] have no inverse? What is its rank?

Medium 1. Implement solve_without_inv(A, b) that solves Ax=b using np.linalg.solve and compare its accuracy and speed to np.linalg.inv(A) @ b for a 500×500 system.

Medium 2. For what values of k is [[1,k],[k,4]] invertible? Find the condition algebraically, then verify by testing k from -3 to 3.

Hard. Implement inverse_2x2(A) using cofactors, then implement inverse_nxn(A) using the general cofactor expansion. Compare numerical accuracy to Gauss-Jordan for ill-conditioned matrices.


9. Chapter Summary & Connections

  • A⁻¹ satisfies AA⁻¹ = I. Exists iff det(A) ≠ 0 (equivalently, A is full rank).

  • (AB)⁻¹ = B⁻¹A⁻¹ — reversal, same pattern as transpose.

  • To solve Ax=b, use np.linalg.solve, not explicit inversion — faster and more numerically stable.

  • The pseudoinverse A⁺ generalizes inverse to non-square matrices (fully explained in ch173 — SVD).

Forward connections:

  • In ch159 (Determinants Computation), we formalize the condition det(A)≠0 for invertibility.

  • In ch161 (Gaussian Elimination), the Gauss-Jordan procedure here is generalized to solve Ax=b directly.