Skip to content

Solving Linear Systems

Solve systems of linear equations \(Ax = b\) using np.linalg.

np.linalg.solve

1. Basic Usage

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4]])
    b = np.array([5, 6])

    x = np.linalg.solve(A, b)

    print(f"x = {x}")
    print(f"Verify A @ x = {A @ x}")

if __name__ == "__main__":
    main()

Output:

x = [-4.   4.5]
Verify A @ x = [5. 6.]

2. Mathematical Form

Solve:

\[\begin{cases} 1x_1 + 2x_2 = 5 \\ 3x_1 + 4x_2 = 6 \end{cases}\]

3. Multiple Right-Hand Sides

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4]])
    B = np.array([[5, 1],
                  [6, 2]])  # Two RHS vectors as columns

    X = np.linalg.solve(A, B)

    print("X =")
    print(X)
    print()
    print("Verify A @ X =")
    print(A @ X)

if __name__ == "__main__":
    main()

Why Not Inverse?

1. Numerical Stability

import numpy as np

def main():
    np.random.seed(42)
    n = 100

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

    # Method 1: Using inverse (less stable)
    x1 = np.linalg.inv(A) @ b

    # Method 2: Using solve (more stable)
    x2 = np.linalg.solve(A, b)

    # Both give similar results for well-conditioned A
    print(f"Max difference: {np.max(np.abs(x1 - x2)):.2e}")

if __name__ == "__main__":
    main()

2. Performance

import numpy as np
import time

def main():
    n = 1000
    A = np.random.randn(n, n)
    b = np.random.randn(n)

    # Using inverse
    start = time.perf_counter()
    x1 = np.linalg.inv(A) @ b
    inv_time = time.perf_counter() - start

    # Using solve
    start = time.perf_counter()
    x2 = np.linalg.solve(A, b)
    solve_time = time.perf_counter() - start

    print(f"Inverse time: {inv_time:.4f} sec")
    print(f"Solve time:   {solve_time:.4f} sec")

if __name__ == "__main__":
    main()

3. Rule of Thumb

Always use solve instead of computing inverse explicitly.

Solution Cases

Linear systems \(Ax = b\) have three possible outcomes depending on the matrix \(A\).

1. Unique Solution

Full rank square matrix has exactly one solution.

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4]])
    b = np.array([[5],
                  [11]])

    # Method 1: inverse (not recommended)
    x1 = np.linalg.inv(A) @ b
    print("Using inv:")
    print(x1)
    print()

    # Method 2: solve (not recommended for general case)
    x2 = np.linalg.solve(A, b)
    print("Using solve:")
    print(x2)
    print()

    # Method 3: lstsq (recommended - handles all cases)
    x3, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
    print("Using lstsq:")
    print(x3)
    print(f"Rank: {rank}")

if __name__ == "__main__":
    main()

2. Infinitely Many Solutions

Dependent rows (rank < n) with consistent equations.

import numpy as np

def main():
    # Rows are linearly dependent (row2 = row1)
    A = np.array([[1, 2],
                  [1, 2]])
    b = np.array([[5],
                  [5]])

    # inv and solve will fail
    # lstsq returns minimum norm solution
    x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)

    print("Minimum norm solution:")
    print(x)
    print(f"Rank: {rank}")
    print()

    # Verify: any x where x1 + 2*x2 = 5 is valid
    print(f"A @ x = {(A @ x).flatten()}")

if __name__ == "__main__":
    main()

3. No Solution

Inconsistent equations (overdetermined or contradictory).

import numpy as np

def main():
    # Inconsistent: row2 = 2*row1 but b2 != 2*b1
    A = np.array([[1, 2],
                  [2, 4]])
    b = np.array([[5],
                  [11]])  # Should be 10 for consistency

    # lstsq returns least squares solution
    x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)

    print("Least squares solution:")
    print(x)
    print(f"Rank: {rank}")
    print()

    # Check residual (non-zero means no exact solution)
    actual = A @ x
    print(f"A @ x = {actual.flatten()}")
    print(f"b = {b.flatten()}")
    print(f"Residual norm: {np.linalg.norm(actual - b):.4f}")

if __name__ == "__main__":
    main()

Overdetermined Systems

1. More Equations Than Unknowns

import numpy as np

def main():
    # 3 equations, 2 unknowns
    A = np.array([[1, 2],
                  [3, 4],
                  [4, 6]])
    b = np.array([[5],
                  [11],
                  [15]])

    # lstsq finds best fit
    x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)

    print("Least squares solution:")
    print(x)
    print(f"Rank: {rank}")
    print()
    print(f"A @ x =")
    print(A @ x)
    print()
    print(f"b =")
    print(b)

if __name__ == "__main__":
    main()

2. Method Comparison

Method Unique Many None Overdetermined
inv(A) @ b
solve(A, b)
lstsq(A, b)

3. Recommendation

Use np.linalg.lstsq for robustness across all cases.

1. Error Handling

import numpy as np

def main():
    # Singular matrix
    A = np.array([[1, 2],
                  [2, 4]])
    b = np.array([3, 6])

    try:
        x = np.linalg.solve(A, b)
    except np.linalg.LinAlgError as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()

2. Check Before Solving

import numpy as np

def main():
    A = np.array([[1, 2],
                  [2, 4]])
    b = np.array([3, 6])

    det = np.linalg.det(A)

    if np.abs(det) < 1e-10:
        print("Matrix is singular or near-singular")
        print("Using least squares instead")
        x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)
        print(f"x = {x}")
    else:
        x = np.linalg.solve(A, b)

if __name__ == "__main__":
    main()

3. Condition Number

import numpy as np

def main():
    A = np.array([[1, 2],
                  [2, 4.0001]])  # Nearly singular
    b = np.array([3, 6])

    cond = np.linalg.cond(A)
    print(f"Condition number: {cond:.2e}")

    if cond > 1e10:
        print("Warning: Ill-conditioned system")

    x = np.linalg.solve(A, b)
    print(f"x = {x}")

if __name__ == "__main__":
    main()

np.linalg.lstsq

1. Overdetermined Systems

More equations than unknowns.

import numpy as np

def main():
    # 3 equations, 2 unknowns
    A = np.array([[1, 1],
                  [1, 2],
                  [1, 3]])
    b = np.array([1, 2, 2])

    x, residuals, rank, s = np.linalg.lstsq(A, b, rcond=None)

    print(f"Least squares solution: x = {x}")
    print(f"Residual sum of squares: {residuals}")
    print(f"Matrix rank: {rank}")

if __name__ == "__main__":
    main()

2. Linear Regression

import numpy as np
import matplotlib.pyplot as plt

def main():
    # Data points
    x_data = np.array([0, 1, 2, 3, 4])
    y_data = np.array([1, 2.1, 2.9, 4.2, 4.8])

    # Design matrix [1, x]
    A = np.column_stack([np.ones_like(x_data), x_data])

    # Least squares fit
    coeffs, residuals, rank, s = np.linalg.lstsq(A, y_data, rcond=None)

    intercept, slope = coeffs
    print(f"y = {slope:.3f}x + {intercept:.3f}")

    # Plot
    fig, ax = plt.subplots()
    ax.scatter(x_data, y_data, label='Data')
    ax.plot(x_data, A @ coeffs, 'r-', label='Fit')
    ax.legend()
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    plt.show()

if __name__ == "__main__":
    main()

3. Polynomial Fitting

import numpy as np

def main():
    # Data
    x = np.array([0, 1, 2, 3, 4])
    y = np.array([1, 1, 2, 4, 7])

    # Quadratic fit: y = a + bx + cx^2
    A = np.column_stack([np.ones_like(x), x, x**2])

    coeffs, residuals, rank, s = np.linalg.lstsq(A, y, rcond=None)

    a, b, c = coeffs
    print(f"y = {c:.3f}x² + {b:.3f}x + {a:.3f}")

if __name__ == "__main__":
    main()

Structured Matrices

1. Triangular Systems

import numpy as np
from scipy import linalg

def main():
    # Upper triangular
    U = np.array([[2, 1, 3],
                  [0, 4, 2],
                  [0, 0, 5]])
    b = np.array([10, 14, 15])

    # Specialized solver (faster)
    x = linalg.solve_triangular(U, b)

    print(f"x = {x}")
    print(f"Verify U @ x = {U @ x}")

if __name__ == "__main__":
    main()

2. Symmetric Positive Definite

import numpy as np
from scipy import linalg

def main():
    # Symmetric positive definite
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])
    b = np.array([1, 2, 3])

    # Cholesky-based solve
    x = linalg.solve(A, b, assume_a='pos')

    print(f"x = {x}")
    print(f"Verify A @ x = {A @ x}")

if __name__ == "__main__":
    main()

3. Banded Matrices

import numpy as np
from scipy import linalg

def main():
    # Tridiagonal matrix
    n = 5
    diag = 4 * np.ones(n)
    off_diag = -np.ones(n - 1)

    A = np.diag(diag) + np.diag(off_diag, 1) + np.diag(off_diag, -1)
    b = np.ones(n)

    x = np.linalg.solve(A, b)

    print("A =")
    print(A)
    print(f"x = {x}")

if __name__ == "__main__":
    main()

Applications

1. Circuit Analysis

import numpy as np

def main():
    # Kirchhoff's laws: RI = V
    R = np.array([[10, -5, 0],
                  [-5, 15, -10],
                  [0, -10, 20]])
    V = np.array([10, 0, 0])

    I = np.linalg.solve(R, V)

    print(f"Currents: {I}")

if __name__ == "__main__":
    main()

2. Equilibrium Prices

import numpy as np

def main():
    # Supply-demand equilibrium
    A = np.array([[2, -1],
                  [1, 1]])
    b = np.array([1, 3])

    prices = np.linalg.solve(A, b)

    print(f"Equilibrium prices: {prices}")

if __name__ == "__main__":
    main()

3. Heat Distribution

import numpy as np

def main():
    # Steady-state heat equation (finite differences)
    n = 5

    # Laplacian matrix
    A = -2 * np.eye(n) + np.eye(n, k=1) + np.eye(n, k=-1)

    # Boundary conditions
    b = np.zeros(n)
    b[0] = -100   # Left boundary at 100
    b[-1] = -50   # Right boundary at 50

    T = np.linalg.solve(A, b)

    print(f"Temperature profile: {T}")

if __name__ == "__main__":
    main()