5.4.13 · D4Scientific Computing (Python)

Exercises — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

3,370 words15 min readBack to topic

Level 1 — Recognition

These test whether you can read the three CSR/CSC arrays and turn them back into a matrix.

L1.1 — Read a CSR triple back into a matrix

Given a matrix in CSR:

data    = [5, 8, 3, 7]
indices = [0, 2, 1, 2]
indptr  = [0, 2, 3, 4]

Reconstruct the full matrix.

Recall Solution

WHAT we do: slice data/indices per row using indptr. Recall row lives in data[indptr[i]:indptr[i+1]].

  • Row 0: indptr[0]:indptr[1] [0:2]data[0:2]=[5,8] at columns indices[0:2]=[0,2]. So .
  • Row 1: [2:3]data=[3] at column indices=[1]. So .
  • Row 2: [3:4]data=[7] at column indices=[2]. So .

Sanity check: indptr has length ✓, and its last value ✓ (there are 4 stored numbers).

L1.2 — Spot the illegal CSR

One of these indptr arrays cannot be valid CSR for a matrix with and rows. Which, and why?

  • (a) [0, 1, 2, 5, 6]
  • (b) [0, 2, 2, 4, 6]
  • (c) [0, 3, 1, 4, 6]
Recall Solution

The rule: indptr must be non-decreasing (each row's start is the previous row's start — you can't un-count nonzeros), start at 0, have length , and end at .

  • (a) [0,1,2,5,6]: increasing, ends at 6 ✓ valid (row counts ).
  • (b) [0,2,2,4,6]: non-decreasing (a 2,2 means row 1 is empty — allowed), ends at 6 ✓ valid (counts ).
  • (c) [0,3,1,4,6]: goes , which decreases. That would say row 1 has nonzeros — impossible. ❌ This is the invalid one.

Level 2 — Application

Now you produce the arrays yourself and use them.

L2.1 — Derive CSR by hand

Build the CSR arrays for

Recall Solution

Step 1 — scan each row left→right, list (column, value):

  • Row 0:
  • Row 1:
  • Row 2: — empty —
  • Row 3:

So data = [4, 1, 2, 5, 6], indices = [1, 0, 2, 0, 2].

Step 2 — count nonzeros per row: .

Step 3 — cumulative sum starting at 0: Note the repeated 3,3 — that empty row 2 (see L1 mistake). Last value ✓, length ✓.

See the scan happen. The figure below shows the row-by-row sweep over : a magenta arrow slides left→right through each row, dropping each nonzero into data/indices; the empty row 2 produces the tell-tale flat step in indptr.

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

L2.2 — CSC of the same matrix

Now build CSC arrays for the same above. (CSC scans column by column; indices holds row indices; indptr has length .)

Recall Solution

Scan each column top→bottom, list (row, value):

  • Col 0:
  • Col 1:
  • Col 2:

So data = [1, 5, 4, 2, 6], indices = [1, 3, 0, 1, 3].

Per-column counts: → cumulative from 0: indptr = [0, 2, 3, 5]. Length ✓, ends at ✓. Same 5 numbers as CSR, reordered by column.

See the difference. In the recap figure, CSR swept rows; here the sweep runs down each column. Same coloured cells, a different traversal order — that reordering is the entire distinction between CSR and CSC.

L2.3 — Compute using the CSR SpMV formula

Using CSR of from L2.1 and , compute with the row-slice formula

Recall Solution

Recall data=[4,1,2,5,6], indices=[1,0,2,0,2], indptr=[0,1,3,3,5].

  • : .
  • : .
  • : → empty sum .
  • : .

Only 5 multiply–adds were needed (one per nonzero) — cost , not .

See the routing. The figure below draws the SpMV as a wiring diagram: each nonzero of picks the entry of named by its indices value, multiplies, and drops the product into the slot named by its row. Row 2 has no wires → .

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

Level 3 — Analysis

Reason about memory, cost, and structure.

L3.1 — Memory crossover

For an CSR matrix, a dense array stores floats. CSR stores numbers (nonzeros data + indices + indptr). Take . Below what density does CSR use fewer numbers than dense? Ignore the small term for the estimate, then refine.

Recall Solution

Estimate (drop ): CSR cheaper when Refine (keep ): solve : so , i.e. . Interpretation: by a raw count of stored numbers, CSR wins below ~50% density. But indices/indptr are integers and each nonzero also needs its integer column — in bytes, and because sparse SpMV has worse cache behaviour per nonzero, the practical rule of thumb from the parent note () is much stricter. The crossover in numbers is generous; the crossover in wall-clock speed is far tighter. See Big-O complexity.

See the crossover. The plot below draws both storage costs against density for : a flat dense line at and a rising CSR line . Where they cross () is the count-crossover; the shaded band near marks where sparse actually pays off in speed.

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

L3.2 — Row access vs column access cost

In CSR, getting all of row 7 is to locate plus to read. What is the cost of getting all of column 7 in CSR, and why? What format fixes it?

Recall Solution

Column access in CSR is slow: the nonzeros of column 7 are scattered arbitrarily across data — CSR groups by row, so to find every entry with indices[k]==7 you must scan all entries. Cost , and it also costs a search inside each row. Fix: use CSC, which groups by column, making column 7 a single contiguous slice data[indptr[7]:indptr[8]] to locate. This is exactly why direct solvers (LU decomposition, Cholesky decomposition) prefer CSC: they eliminate column by column.

L3.3 — Fill-in

You factor a sparse LU of an arrow matrix: nonzeros only on the diagonal, the entire first row, and the entire first column (). Sketch (in words) why eliminating the first column before reordering creates a dense trailing block (fill-in), and how reversing the arrow avoids it.

Recall Solution

The arrow (point at top-left): Read the figure left→right. The left panel shows the original arrow: violet cells are the only stored nonzeros (*) — the full first row, full first column, and the diagonal. The right panel shows what happens after eliminating column 0 first: to zero each (rows 1–4) we subtract a multiple of the full row 0, which injects a new nonzero into every column 1–4 of those rows. Those brand-new entries are drawn in orange and marked + — the previously-empty trailing block has become completely dense. The magenta arrow points straight at that catastrophic block.

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

Reverse the arrow (permute so the dense row/column is last): Now eliminating columns 0–3 each touches only its own diagonal and the last row — no new fill until the final pivot. A good reordering (AMD, RCM) chases exactly this. Takeaway: fill-in depends on ordering, not just on the matrix.


Level 4 — Synthesis

Combine format choice, solver choice, and code.

L4.1 — Choose the solver

For each system, pick the best solver from {spsolve, cg, gmres, lsqr} and justify:

  • (a) is , symmetric, all eigenvalues positive, sparse.
  • (b) is , sparse, non-symmetric, you need an exact answer and it fits in memory.
  • (c) is (tall, non-square), solve in the least-squares sense.
  • (d) is huge, non-symmetric, memory-limited.
Recall Solution
  • (a) cg — symmetric positive-definite (SPD) is exactly CG's home turf; it only needs matrix–vector products and no factorization, so it's cheap and memory-light.
  • (b) spsolve — non-symmetric but you want an exact, robust answer and it fits: direct sparse LU (feed it CSC).
  • (c) lsqr — non-square, so "solve" means minimise ; lsqr/lsmr are built for that.
  • (d) gmres (or bicgstab) — general non-symmetric, low memory: a Krylov iterative method, optionally with a preconditioner to speed convergence.

L4.2 — Build a 1-D Poisson matrix and count its nonzeros

The 1-D Poisson discretization gives a tridiagonal matrix with stencil : value on the diagonal, on the sub- and super-diagonal, for size . (i) How many nonzeros does it have? (ii) What is its density for ? (iii) Write the scipy.sparse one-liner to build it in CSC.

Recall Solution

(i) Count the nonzeros. The diagonal has entries. The sub-diagonal (just below) has entries, and the super-diagonal (just above) also has . There is no overlap between these three bands, so (ii) Density for . Plug in: . The full matrix has cells, so Extremely sparse — a dense store would waste 99.7% of its memory on zeros. (iii) One-liner in CSC.

from scipy.sparse import diags
A = diags([-1, 2, -1], [-1, 0, 1], shape=(1000, 1000), format='csc')

The list [-1, 0, 1] gives the three diagonal offsets (below main, main, above main); format='csc' builds it directly in CSC so spsolve won't need an internal conversion.

L4.3 — Predict indptr for the tridiagonal (small case)

For the tridiagonal matrix with , write out data, indices, indptr in CSR by hand.

Recall Solution

Scan rows left→right:

  • Row 0:
  • Row 1:
  • Row 2:
  • Row 3:

data = [2, -1, -1, 2, -1, -1, 2, -1, -1, 2] indices = [0, 1, 0, 1, 2, 1, 2, 3, 2, 3] Per-row counts indptr = [0, 2, 5, 8, 10]. Check: = last indptr value ✓.

See the mapping. The figure below colour-codes the tridiagonal and draws, for each stored nonzero, an arrow into its exact slot of data/indices, with the indptr bookmarks marking each row boundary. This is the whole "matrix → three arrays" translation made literal.

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

Level 5 — Mastery

Derive and design.

L5.1 — Derive the SpMV cost from the CSR structure

Prove that computing via CSR costs exactly multiplications and additions (ignoring loop overhead), and explain why this beats dense .

Recall Solution

Setup. For each output entry, The inner loop for row runs over terms — that's the number of nonzeros in row . Each term is one multiply () and one add into the running sum.

Sum over all rows. Total multiplies a telescoping sum — the internal terms cancel, leaving last minus first. Adds are the same count. ∎

See the cancellation. The figure below draws the telescoping sum as a chain of indptr values: each term subtracts the previous bookmark, so every interior value is added once and subtracted once (they annihilate in pairs), leaving only .

Figure — scipy.sparse — sparse matrix formats (CSR, CSC), sparse solvers

Why it wins: dense multiply touches every one of the entries, doing multiplies. When (e.g. 5 per row from a stencil), CSR does work — a factor faster. For that's a million-fold saving.

L5.2 — Design: symmetric storage question

You have a symmetric sparse matrix (). A colleague says "store only the upper triangle to halve memory." What breaks in a generic CSR SpMV if you do this naively, and how do you fix the SpMV to still compute the full ?

Recall Solution

What breaks: if you store only entries with , then the plain row-slice SpMV computes only the contribution of the upper triangle. Each off-diagonal nonzero () has a mirror that is no longer stored, so its contribution to is silently dropped. Result: wrong for every row that had a mirror entry below the diagonal.

Fix (symmetric SpMV): for each stored nonzero apply it twice by hand: The diagonal () is applied once. This restores the full product using half the storage — the standard trick in FEM and Cholesky codes, where matrices are symmetric by construction.

L5.3 — Design a preconditioner rationale

You solve with CG on an SPD matrix whose condition number is ; convergence crawls. Explain, in terms of the CG convergence bound, why a preconditioner helps, and what (no preconditioning) vs (perfect) represent as extremes.

Recall Solution

The bound. CG's error after iterations shrinks roughly like Large pushes that ratio near 1, so each step barely reduces the error → slow.

Preconditioning replaces with (via a symmetric split), whose condition number we want small. Since appears in the bound, cutting from to, say, changes from to — dramatically faster convergence.

Extremes:

  • : , no change — plain CG, , crawls.
  • : , condition number — CG converges in one step, but forming is the original solve, so it's useless as work.

The art is choosing cheap to invert yet close to (e.g. incomplete Cholesky) — a compromise between these two extremes.


Final active recall

Recall One-line answers (cover them)
  • Invalid indptr symptom? ::: It decreases somewhere (or wrong length / wrong final value).
  • Empty row in CSR looks like? ::: Two equal consecutive indptr values.
  • CSC indices holds? ::: row indices.
  • Column access in CSR costs? ::: — use CSC instead.
  • SpMV cost, derived how? ::: Telescoping sum of per-row counts .
  • tridiagonal nnz? ::: .
  • CG requires? ::: symmetric positive-definite.
  • Preconditioner goal? ::: shrink while keeping cheap to invert.