Linear Algebra in Go: Solving Linear Systems

Linear Algebra in Go Part 3

Posted by Craig Johnston on Saturday, February 20, 2021

This article covers solving linear systems in Go using the gonum library, including direct methods with mat.Solve, LU decomposition, and Cholesky decomposition for positive-definite matrices.

Linear Algebra: Golang Series - View all articles in this series.

Previous articles in this series:

  1. Linear Algebra in Go: Vectors and Basic Operations
  2. Linear Algebra in Go: Matrix Fundamentals

This continues from Part 2: Matrix Fundamentals.

The Linear System Problem

The goal is to solve $\boldsymbol{A}\vec{x} = \vec{b}$ for $\vec{x}$, where $\boldsymbol{A}$ is an $n \times n$ matrix.

Using mat.Solve

The simplest approach in gonum:

package main

import (
    "fmt"
    "gonum.org/v1/gonum/mat"
)

func main() {
    // System: 2x + 3y = 8
    //         x - y = 1
    A := mat.NewDense(2, 2, []float64{
        2, 3,
        1, -1,
    })
    b := mat.NewVecDense(2, []float64{8, 1})

    // Solve Ax = b
    var x mat.VecDense
    err := x.SolveVec(A, b)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Println("Solution:")
    fmt.Printf("x = %.4f\n", x.AtVec(0))
    fmt.Printf("y = %.4f\n", x.AtVec(1))

    // Verify: A*x should equal b
    var result mat.VecDense
    result.MulVec(A, &x)
    fmt.Printf("\nVerification (A*x): [%.2f, %.2f]\n",
        result.AtVec(0), result.AtVec(1))
}

Output:

Solution:
x = 2.2000
y = 1.2000

Verification (A*x): [8.00, 1.00]

Visualizing the Solution

A system of two linear equations represents two lines. The solution is their intersection:

package main

import (
    "image/color"

    "gonum.org/v1/plot"
    "gonum.org/v1/plot/plotter"
    "gonum.org/v1/plot/vg"
    "gonum.org/v1/plot/vg/draw"
)

func main() {
    p := plot.New()
    p.Title.Text = "Linear System: Two Lines Intersecting"
    p.X.Label.Text = "x"
    p.Y.Label.Text = "y"
    p.X.Min, p.X.Max = -1, 5
    p.Y.Min, p.Y.Max = -1, 4

    // Line 1: 2x + 3y = 8  =>  y = (8 - 2x) / 3
    line1Pts := make(plotter.XYs, 100)
    for i := range line1Pts {
        x := -1.0 + float64(i)*0.06
        line1Pts[i] = plotter.XY{X: x, Y: (8 - 2*x) / 3}
    }
    l1, _ := plotter.NewLine(line1Pts)
    l1.Color = color.RGBA{R: 66, G: 133, B: 244, A: 255}
    l1.Width = vg.Points(2)

    // Line 2: x - y = 1  =>  y = x - 1
    line2Pts := make(plotter.XYs, 100)
    for i := range line2Pts {
        x := -1.0 + float64(i)*0.06
        line2Pts[i] = plotter.XY{X: x, Y: x - 1}
    }
    l2, _ := plotter.NewLine(line2Pts)
    l2.Color = color.RGBA{R: 234, G: 67, B: 53, A: 255}
    l2.Width = vg.Points(2)

    // Solution point (2.2, 1.2)
    solPts := plotter.XYs{{X: 2.2, Y: 1.2}}
    sol, _ := plotter.NewScatter(solPts)
    sol.GlyphStyle.Color = color.RGBA{R: 52, G: 168, B: 83, A: 255}
    sol.GlyphStyle.Radius = vg.Points(6)
    sol.GlyphStyle.Shape = draw.CircleGlyph{}

    p.Add(l1, l2, sol)
    p.Save(6*vg.Inch, 5*vg.Inch, "systems.png")
}

Two lines intersecting at the solution point

The blue line represents 2x + 3y = 8, the red line represents x - y = 1, and the green point marks the solution (2.2, 1.2).

LU Decomposition

LU decomposition factors a matrix as $\boldsymbol{A} = \boldsymbol{L}\boldsymbol{U}$ where $\boldsymbol{L}$ is lower triangular and $\boldsymbol{U}$ is upper triangular:

import "gonum.org/v1/gonum/mat"

func luDecomposition() {
    A := mat.NewDense(3, 3, []float64{
        2, -1, 0,
        -1, 2, -1,
        0, -1, 2,
    })

    // Compute LU decomposition
    var lu mat.LU
    lu.Factorize(A)

    // Extract L and U
    var L, U mat.TriDense
    lu.LTo(&L)
    lu.UTo(&U)

    fmt.Println("L (lower triangular):")
    fmt.Printf("%v\n", mat.Formatted(&L))

    fmt.Println("\nU (upper triangular):")
    fmt.Printf("%v\n", mat.Formatted(&U))

    // Solve using LU
    b := mat.NewVecDense(3, []float64{1, 0, 1})
    var x mat.VecDense
    err := lu.SolveVecTo(&x, false, b)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Println("\nSolution x:")
    fmt.Printf("%v\n", mat.Formatted(&x))
}

Cholesky Decomposition

For symmetric positive-definite matrices, Cholesky decomposition is more efficient: $\boldsymbol{A} = \boldsymbol{L}\boldsymbol{L}^T$

func choleskyDecomposition() {
    // Symmetric positive-definite matrix
    A := mat.NewSymDense(3, []float64{
        4, 2, 2,
        2, 5, 1,
        2, 1, 6,
    })

    // Compute Cholesky decomposition
    var chol mat.Cholesky
    ok := chol.Factorize(A)
    if !ok {
        fmt.Println("Matrix is not positive-definite")
        return
    }

    // Extract L
    var L mat.TriDense
    chol.LTo(&L)

    fmt.Println("Cholesky factor L:")
    fmt.Printf("%v\n", mat.Formatted(&L))

    // Solve using Cholesky
    b := mat.NewVecDense(3, []float64{1, 2, 3})
    var x mat.VecDense
    err := chol.SolveVecTo(&x, b)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Println("\nSolution:")
    fmt.Printf("%v\n", mat.Formatted(&x))
}

QR Decomposition for Least Squares

For overdetermined systems (more equations than unknowns), use QR decomposition:

func qrLeastSquares() {
    // Overdetermined system: 4 equations, 2 unknowns
    A := mat.NewDense(4, 2, []float64{
        1, 1,
        1, 2,
        1, 3,
        1, 4,
    })
    b := mat.NewVecDense(4, []float64{2.1, 3.9, 6.2, 7.8})

    // QR decomposition
    var qr mat.QR
    qr.Factorize(A)

    // Solve least squares
    var x mat.VecDense
    err := qr.SolveVecTo(&x, false, b)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Println("Least squares solution:")
    fmt.Printf("Intercept: %.4f\n", x.AtVec(0))
    fmt.Printf("Slope: %.4f\n", x.AtVec(1))

    // Compute residuals
    var residuals mat.VecDense
    residuals.MulVec(A, &x)
    residuals.SubVec(&residuals, b)

    residualNorm := mat.Norm(&residuals, 2)
    fmt.Printf("\nResidual norm: %.4f\n", residualNorm)
}

Benchmarking: Go vs Python

Go typically offers significant performance advantages:

import (
    "fmt"
    "time"
    "gonum.org/v1/gonum/mat"
)

func benchmarkSolve() {
    sizes := []int{100, 500, 1000}

    for _, n := range sizes {
        // Generate random matrix and vector
        data := make([]float64, n*n)
        for i := range data {
            data[i] = float64(i%17) - 8
        }
        A := mat.NewDense(n, n, data)

        bData := make([]float64, n)
        for i := range bData {
            bData[i] = float64(i % 11)
        }
        b := mat.NewVecDense(n, bData)

        // Time the solve
        start := time.Now()
        var x mat.VecDense
        x.SolveVec(A, b)
        elapsed := time.Since(start)

        fmt.Printf("Size %4d x %4d: %v\n", n, n, elapsed)
    }
}

Condition Number

Check numerical stability by computing the condition number:

func conditionNumber() {
    A := mat.NewDense(3, 3, []float64{
        1, 2, 3,
        4, 5, 6,
        7, 8, 9.0001,  // Nearly singular
    })

    // SVD to compute condition number
    var svd mat.SVD
    svd.Factorize(A, mat.SVDThin)

    values := svd.Values(nil)
    cond := values[0] / values[len(values)-1]

    fmt.Printf("Singular values: %v\n", values)
    fmt.Printf("Condition number: %.2e\n", cond)
}

Summary

This article covered:

  • Direct solving with mat.Solve
  • LU decomposition for general square matrices
  • Cholesky decomposition for symmetric positive-definite matrices
  • QR decomposition for least squares
  • Performance benchmarking
  • Condition number for numerical stability

The next article covers eigenvalue problems in Go.

Resources

Next: Eigenvalue Problems

Check out the next article in this series, Linear Algebra in Go: Eigenvalue Problems.


Linear Algebra: Golang Series - View all articles in this series.

Note: This blog is a collection of personal notes. Making them public encourages me to think beyond the limited scope of the current problem I'm trying to solve or concept I'm implementing, and hopefully provides something useful to my team and others.

This blog post, titled: "Linear Algebra in Go: Solving Linear Systems: Linear Algebra in Go Part 3" by Craig Johnston, is licensed under a Creative Commons Attribution 4.0 International License. Creative Commons License