Linear Algebra in Go: Matrix Fundamentals

Linear Algebra in Go Part 2

Posted by Craig Johnston on Tuesday, December 15, 2020

This article covers matrix fundamentals in Go using the gonum library: matrix creation, basic arithmetic operations, and common matrix manipulations.

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

Previous articles in this series:

  1. Linear Algebra in Go: Vectors and Basic Operations

This continues from Part 1: Vectors and Basic Operations.

Creating Matrices

Gonum’s mat.Dense is the primary matrix type:

package main

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

func main() {
    // Create a 3x4 matrix from row-major data
    data := []float64{
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12,
    }
    A := mat.NewDense(3, 4, data)

    fmt.Println("Matrix A (3x4):")
    fmt.Printf("%v\n", mat.Formatted(A, mat.Prefix("  ")))

    // Get dimensions
    rows, cols := A.Dims()
    fmt.Printf("Dimensions: %d x %d\n", rows, cols)

    // Access elements (0-indexed)
    fmt.Printf("A[1,2] = %.2f\n", A.At(1, 2))
}

Output:

Matrix A (3x4):
  [1  2  3  4]
  [5  6  7  8]
  [9  10  11  12]
Dimensions: 3 x 4
A[1,2] = 7.00

Special Matrices

Identity Matrix

func identityMatrix() {
    // Create 4x4 identity matrix
    I := mat.NewDiagDense(4, []float64{1, 1, 1, 1})
    fmt.Println("Identity matrix:")
    fmt.Printf("%v\n", mat.Formatted(I))
}

Zero Matrix

func zeroMatrix() {
    // Create 3x3 zero matrix
    Z := mat.NewDense(3, 3, nil)  // nil initializes to zeros
    fmt.Println("Zero matrix:")
    fmt.Printf("%v\n", mat.Formatted(Z))
}

Diagonal Matrix

func diagonalMatrix() {
    // Create diagonal matrix
    diag := []float64{2, 3, 5, 7}
    D := mat.NewDiagDense(4, diag)
    fmt.Println("Diagonal matrix:")
    fmt.Printf("%v\n", mat.Formatted(D))
}

Matrix Arithmetic

Addition and Subtraction

func matrixAddition() {
    A := mat.NewDense(2, 2, []float64{1, 2, 3, 4})
    B := mat.NewDense(2, 2, []float64{5, 6, 7, 8})

    // Addition
    sum := mat.NewDense(2, 2, nil)
    sum.Add(A, B)
    fmt.Println("A + B:")
    fmt.Printf("%v\n", mat.Formatted(sum))

    // Subtraction
    diff := mat.NewDense(2, 2, nil)
    diff.Sub(A, B)
    fmt.Println("A - B:")
    fmt.Printf("%v\n", mat.Formatted(diff))
}

Output:

A + B:
[6  8]
[10  12]
A - B:
[-4  -4]
[-4  -4]

Scalar Multiplication

func scalarMultiplication() {
    A := mat.NewDense(2, 3, []float64{1, 2, 3, 4, 5, 6})
    scalar := 2.5

    result := mat.NewDense(2, 3, nil)
    result.Scale(scalar, A)

    fmt.Printf("%.1f * A:\n", scalar)
    fmt.Printf("%v\n", mat.Formatted(result))
}

Matrix Multiplication

func matrixMultiplication() {
    // A: 2x3
    A := mat.NewDense(2, 3, []float64{
        1, 2, 3,
        4, 5, 6,
    })

    // B: 3x2
    B := mat.NewDense(3, 2, []float64{
        7, 8,
        9, 10,
        11, 12,
    })

    // C = A * B (2x2)
    var C mat.Dense
    C.Mul(A, B)

    fmt.Println("A (2x3):")
    fmt.Printf("%v\n", mat.Formatted(A))
    fmt.Println("B (3x2):")
    fmt.Printf("%v\n", mat.Formatted(B))
    fmt.Println("A * B (2x2):")
    fmt.Printf("%v\n", mat.Formatted(&C))
}

Output:

A (2x3):
[1  2  3]
[4  5  6]
B (3x2):
[7   8]
[9   10]
[11  12]
A * B (2x2):
[58   64]
[139  154]

Element-wise Operations

func elementWiseOperations() {
    A := mat.NewDense(2, 2, []float64{1, 2, 3, 4})
    B := mat.NewDense(2, 2, []float64{5, 6, 7, 8})

    // Element-wise multiplication (Hadamard product)
    result := mat.NewDense(2, 2, nil)
    result.MulElem(A, B)

    fmt.Println("A ⊙ B (element-wise):")
    fmt.Printf("%v\n", mat.Formatted(result))

    // Element-wise division
    result.DivElem(A, B)
    fmt.Println("A ⊘ B (element-wise division):")
    fmt.Printf("%v\n", mat.Formatted(result))
}

Visualizing Matrix Transformations

Matrices represent linear transformations. Here’s how a matrix transforms the unit square:

package main

import (
    "image/color"

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

func main() {
    p := plot.New()
    p.Title.Text = "Matrix Transformation"
    p.X.Min, p.X.Max = -1, 5
    p.Y.Min, p.Y.Max = -1, 4

    // Unit square vertices
    square := [][]float64{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}

    // Transformation matrix
    A := mat.NewDense(2, 2, []float64{2, 1, 0.5, 1.5})

    // Original square (blue)
    origPts := make(plotter.XYs, len(square))
    for i, pt := range square {
        origPts[i] = plotter.XY{X: pt[0], Y: pt[1]}
    }
    origLine, _ := plotter.NewLine(origPts)
    origLine.Color = color.RGBA{R: 66, G: 133, B: 244, A: 255}
    origLine.Width = vg.Points(2)

    // Transformed square (red)
    transPts := make(plotter.XYs, len(square))
    for i, pt := range square {
        v := mat.NewVecDense(2, pt)
        var result mat.VecDense
        result.MulVec(A, v)
        transPts[i] = plotter.XY{X: result.AtVec(0), Y: result.AtVec(1)}
    }
    transLine, _ := plotter.NewLine(transPts)
    transLine.Color = color.RGBA{R: 234, G: 67, B: 53, A: 255}
    transLine.Width = vg.Points(2)

    p.Add(origLine, transLine)
    p.Save(6*vg.Inch, 5*vg.Inch, "matrix-transform.png")
}

Matrix transformation of unit square

The blue unit square is transformed by matrix A into the red parallelogram. This visualizes how matrices stretch, shear, and rotate space.

Matrix Transposition

func matrixTranspose() {
    A := mat.NewDense(2, 3, []float64{
        1, 2, 3,
        4, 5, 6,
    })

    // Transpose
    var AT mat.Dense
    AT.CloneFrom(A.T())

    fmt.Println("A:")
    fmt.Printf("%v\n", mat.Formatted(A))
    fmt.Println("A^T:")
    fmt.Printf("%v\n", mat.Formatted(&AT))

    // Note: A.T() returns a view, not a copy
    // Use CloneFrom for a separate matrix
}

Output:

A:
[1  2  3]
[4  5  6]
A^T:
[1  4]
[2  5]
[3  6]

Matrix Slicing and Submatrices

func matrixSlicing() {
    A := mat.NewDense(4, 4, []float64{
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12,
        13, 14, 15, 16,
    })

    fmt.Println("Original matrix A:")
    fmt.Printf("%v\n", mat.Formatted(A))

    // Extract a row
    row := A.RowView(1)  // Second row
    fmt.Printf("Row 1: %v\n", mat.Formatted(row.T()))

    // Extract a column
    col := A.ColView(2)  // Third column
    fmt.Printf("Column 2: %v\n", mat.Formatted(col))

    // Extract a submatrix (rows 1-2, cols 1-2)
    sub := A.Slice(1, 3, 1, 3).(*mat.Dense)
    fmt.Println("Submatrix [1:3, 1:3]:")
    fmt.Printf("%v\n", mat.Formatted(sub))
}

Modifying Matrices

func modifyMatrix() {
    A := mat.NewDense(3, 3, nil)

    // Set individual elements
    A.Set(0, 0, 1)
    A.Set(1, 1, 2)
    A.Set(2, 2, 3)

    fmt.Println("After setting diagonal:")
    fmt.Printf("%v\n", mat.Formatted(A))

    // Set a row
    A.SetRow(0, []float64{7, 8, 9})
    fmt.Println("After setting row 0:")
    fmt.Printf("%v\n", mat.Formatted(A))

    // Set a column
    A.SetCol(2, []float64{10, 20, 30})
    fmt.Println("After setting column 2:")
    fmt.Printf("%v\n", mat.Formatted(A))
}

Matrix Properties

func matrixProperties() {
    A := mat.NewDense(3, 3, []float64{
        1, 2, 3,
        4, 5, 6,
        7, 8, 9,
    })

    // Trace (sum of diagonal)
    trace := mat.Trace(A)
    fmt.Printf("Trace: %.2f\n", trace)

    // Sum of all elements
    sum := mat.Sum(A)
    fmt.Printf("Sum of all elements: %.2f\n", sum)

    // Frobenius norm
    frobNorm := mat.Norm(A, 2)
    fmt.Printf("Frobenius norm: %.4f\n", frobNorm)
}

Output:

Trace: 15.00
Sum of all elements: 45.00
Frobenius norm: 16.8819

Sparse Matrices

For matrices with many zeros, use sparse representations:

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

func sparseMatrixExample() {
    // Create a sparse matrix using DOK (Dictionary of Keys)
    // then convert to dense if needed
    data := map[int]map[int]float64{
        0: {0: 1, 2: 2},
        1: {1: 3},
        2: {0: 4, 2: 5},
    }

    // Create dense from sparse data
    A := mat.NewDense(3, 3, nil)
    for i, row := range data {
        for j, val := range row {
            A.Set(i, j, val)
        }
    }

    fmt.Println("Sparse matrix as dense:")
    fmt.Printf("%v\n", mat.Formatted(A))
}

Complete Working Example

package main

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

func main() {
    fmt.Println("=== Matrix Fundamentals in Go ===\n")

    // Create matrices
    A := mat.NewDense(2, 3, []float64{1, 2, 3, 4, 5, 6})
    B := mat.NewDense(3, 2, []float64{7, 8, 9, 10, 11, 12})

    fmt.Println("Matrix A:")
    fmt.Printf("%v\n\n", mat.Formatted(A, mat.Prefix("  ")))

    fmt.Println("Matrix B:")
    fmt.Printf("%v\n\n", mat.Formatted(B, mat.Prefix("  ")))

    // Multiplication
    var C mat.Dense
    C.Mul(A, B)
    fmt.Println("A * B:")
    fmt.Printf("%v\n\n", mat.Formatted(&C, mat.Prefix("  ")))

    // Transpose
    var AT mat.Dense
    AT.CloneFrom(A.T())
    fmt.Println("A^T:")
    fmt.Printf("%v\n", mat.Formatted(&AT, mat.Prefix("  ")))
}

Summary

This article covered:

  • Creating matrices with mat.Dense
  • Special matrices: identity, zero, diagonal
  • Matrix arithmetic: addition, subtraction, multiplication
  • Element-wise operations: Hadamard product
  • Transposition: A.T()
  • Slicing and submatrices
  • Matrix properties: trace, sum, norm

Resources


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: Matrix Fundamentals: Linear Algebra in Go Part 2" by Craig Johnston, is licensed under a Creative Commons Attribution 4.0 International License. Creative Commons License