alt assessment 2026
English review edition prepared on 4 October 2026 from a preserved source copy. It is a later presentation, not the historical study interface. Source checks and difficulty judgements describe the original material author's own process; they do not indicate Imperial College London endorsement.
Source SHA-256: 8e08e885fbac0aa2bc025e9e1147d9c110ffcec943f2d93443ef223be9db007bSource date: 2026-07-31
Notes — cell 1
# MATH40006 Alternative In-Course Assessment # 11 May 2026 ## Ninety minutes ### Answer all questions, submitting your answers as a single Jupyter notebook.
Notes — cell 2
To carry out this assignment, you may find useful, possibly amongst other things: - from the `numpy` module, the functions `array`, `arange`, `argmax` and `abs`; - from the `random` submodule of `numpy`, the function `random`; - from the `linalg` submodule of `numpy`, the function `det`; - from the `sympy` module, the functions `symbols`, `simplify` and `expand`; - from the `pandas` module, the function `DataFrame`; - from the `pickle` module, the function `dump`; - from the `copy` module, the function `copy`; - from the `pickle` module, the function `dump`; We recommend that you execute the following cell:
Code — cell 3
import numpy as np import numpy.random as nrnd import numpy.linalg as la import sympy as sp import pandas as pd import pickle from copy import copy from time import time
Notes — cell 4
## Question 1 (30 marks)
Many tasks related to linear systems can be accomplished using <b>elementary row operations on matrices</b>, of which there are three main kinds to consider.
1. <b>Interchanging two rows</b>; for example, making $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\3&-5&4\\8&9&-7\end{array}\right)}$ into $\displaystyle{\left(\begin{array}{ccc}8&9&-7\\3&-5&4\\2&0&-1\end{array}\right)}$ by swapping rows $0$ and $2$;
2. <b>Multiplying a row by a scalar</b>; for example, making $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\3&-5&4\\8&9&-7\end{array}\right)}$ into $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\-9&15&-12\\8&9&-7\end{array}\right)}$ by multiplying row $1$ by $-3$;
3. <b>Adding a multiple of one row from another</b>: for example, making $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\3&-5&4\\8&9&-7\end{array}\right)}$ into $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\3&-5&4\\0&9&-3\end{array}\right)}$ by adding $-4$ times row $0$ to row $2$.
### Part (i)
This question part uses the matrix $\displaystyle{\left(\begin{array}{ccc}2&0&-1\\3&-5&4\\8&9&-7\end{array}\right)}$, defined below as `mat0`.Notes — cell 5
The following function takes as its arguments: - `mat`, assumed to be a matrix implemented as a 2D NumPy array; - `row_index` and `pivot_index`, assumed to be non-negative ints; - `multiple`, which will typically be either a number or a SymPy expression representing a number. It then adds row number `pivot_index`, multiplied by `multiple`, to row number `row_index`, changing the value of `mat` in place (that is, as a side-effect). (<b>Note</b>: we've avoided using augmented assignment, which can be a bit tricky to handle with arrays.)
Code — cell 6
def row_add(mat, row_index, pivot_index, multiple):
mat[row_index, :] = mat[row_index, :] + multiple * mat[pivot_index, :]Notes — cell 7
(a) Test this function: by using appropriate arguments, you should be able to duplicate example 3 above.
Code — cell 8
mat0 = np.array([[2, 0, -1], [3, -5, 4], [8, 9, -7]]) # YOUR CODE HERE raise NotImplementedError()
Notes — cell 9
(b) Write a function called `row_multiply`, which takes as its argument a matrix `mat`, an int `row_index` and a `multiple`, and replaces row number `row_index` with `multiple` times this value; the value of `mat` should be changed in place, as a side-effect.
Code — cell 10
def row_multiply(mat, row_index, multiple):
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 11
(c) Test this function, by duplicating example 2 above.
Code — cell 12
mat0 = np.array([[2, 0, -1], [3, -5, 4], [8, 9, -7]]) # YOUR CODE HERE raise NotImplementedError()
Notes — cell 13
(e) Write a function called `row_swap`, which takes as its arguments a matrix `mat`, and ints `row_a_index` and `row_b_index`, and interchanges the corresponding rows in `mat`; the value of `mat` should be changed in place, as a side-effect.
Code — cell 14
def row_swap(mat, row_a_index, row_b_index):
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 15
(f) Test this function, by duplicating example 1 above.
Code — cell 16
mat0 = np.array([[2, 0, -1], [3, -5, 4], [8, 9, -7]]) # YOUR CODE HERE raise NotImplementedError()
Notes — cell 17
### Part (ii)
A useful application of row operations is in reducing a matrix to <b>row echelon form</b>: that is, a form for which every element below the leading diagonal is zero. This is done by repeated `row_add` operations, as follows. Given the matrix
$$\left(\begin{array}{ccccc}
a_{0,0}&a_{0,1}&a_{0,2}&\cdots&a_{0,n-1}\\
a_{1,0}&a_{1,1}&a_{1,2}&\cdots&a_{1,n-1}\\
a_{2,0}&a_{2,1}&a_{2,2}&\cdots&a_{2,n-1}\\
\vdots&\vdots&\vdots&\ddots&\vdots\\
a_{m-1,0}&a_{m-1,1}&a_{m-1,2}&\cdots&a_{m-1,n-1}
\end{array}\right),$$
first designate row $0$ the so-called <b>pivot row</b> and $a_{0,0}$ the <b>pivot element</b>, and:
- subtract $(a_{1,0}/a_{0,0})\times$ (row $0$) from row $1$, so that the new value of $a_{1,0}$ becomes zero;
- subtract $(a_{2,0}/a_{0,0})\times$ (row $0$) from row $2$, so that the new value of $a_{2,0}$ becomes zero;
- $\dots$
- subtract $(a_{m-1,0}/a_{0,0})\times$ (row $0$) from row $m-1$, so that the new value of $a_{m-1,0}$ becomes zero.
This makes every entry in column $0$ <em>below the leading diagonal</em> into zero. The method proceeds by moving on to column $1$, and then column $2$, and so on, in each case making every element below the leading diagonal into zero by repeated `row_add` operations.
Here's a function for zeroing the sub-diagonal elements in a given column of a matrix:Code — cell 18
def zero_column(mat, pivot_index):
for row_index in range(pivot_index+1,len(mat)):
multiple = -mat[row_index, pivot_index]/mat[pivot_index, pivot_index]
row_add(mat, row_index, pivot_index, multiple)Notes — cell 19
(a) Test this function on the matrix
$\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$, with `pivot_index` set to 0.Code — cell 20
mat1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError() mat1
Notes — cell 21
(b) Using this latest value of `mat`, apply `zero_column` with `pivot_index` set to `1` and then again with `2`; you should end up with a matrix with zeros below the leading diagonal, which in the case of a square matrix is the same as an <b>upper triangular</b> matrix.
Code — cell 22
# YOUR CODE HERE raise NotImplementedError() mat1
Notes — cell 23
(c) In the case of a square matrix, the row operation represented by `row_add` always preserves the value of the determinant. State the value of
$$\det\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right),$$
giving reasons for your answer.Notes — cell 24
YOUR ANSWER HERE
Notes — cell 25
(d) Use `zero_column` repeatedly, with increasing values of `pivot_index` each time, to place the $3\times4$ matrix $$\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\end{array}\right)}$$ in row echelon form. (Remember: this simply means that all elements below the leading diagonal must be zero.)Code — cell 26
mat2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13]], dtype='float') # YOUR CODE HERE raise NotImplementedError() mat2
Notes — cell 27
(e) Use `zero_column` repeatedly, with increasing values of `pivot_index` each time, to place the $4\times3$ matrix $$\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0\\5.0&6.0&7.0\\9.0&10.0&12.0\\14.0&16.0&17.0\end{array}\right)}$$ in row echelon form.Code — cell 28
mat3 = np.array([[1, 2, 3], [5, 6, 7], [9, 10, 12], [14, 16, 17]], dtype='float') # YOUR CODE HERE raise NotImplementedError() mat3
Notes — cell 29
(f) How many applications of `zero_column` are needed in order to reduce an $m\times n$ matrix to row echelon form, assuming all are effective and none may be skipped? Give reasons for your answer.
Notes — cell 30
YOUR ANSWER HERE
Notes — cell 31
(g) Write a function called `row_echelon_form`, which takes a single argument, `mat`, assumed to be a matrix implemented as a 2D NumPy array. Your function should use the `copy` function to make a copy `mat_copy` of `mat`, and then apply `zero_column` repeatedly, with the value of `pivot_index` increasing, to `mat_copy`, until `mat_copy` is in row echelon form. It should then <em>return</em> the value of `mat_copy`. (<b>Note</b>: your function should return a value rather than operating on `mat` in place; using `copy` like this is one way to achieve this.)
Code — cell 32
def row_echelon_form(mat):
# YOUR CODE HERE
raise NotImplementedError()
return mat_copyNotes — cell 33
(h) Test your function on the matrices $\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$, $\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\end{array}\right)}$ and $\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0\\5.0&6.0&7.0\\9.0&10.0&12.0\\14.0&16.0&17.0\end{array}\right)}$, given below.Code — cell 34
mat1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError()
Code — cell 35
mat2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13]], dtype='float') # YOUR CODE HERE raise NotImplementedError()
Code — cell 36
mat3 = np.array([[1, 2, 3], [5, 6, 7], [9, 10, 12], [14, 16, 17]], dtype='float') # YOUR CODE HERE raise NotImplementedError()
Notes — cell 37
## Question 2 (35 marks) There are many uses of reduction to row echelon form; one is in the calculation of <b>determinants</b>. This uses the facts that the `row_add` operation preserves determinant, and that the determinant of an upper triangular square matrix is simply the product of the entries along the leading diagonal. ### Part (i) (a) Write a function called `my_det`, which should take a single argument, `mat`, which may be assumed to be a <b>square</b> matrix implemented as a 2D NumPy array. It should then calculate the determinant of `mat` by reducing `mat` to row echelon form, and multiplying the entries along the leading diagonal. Give your function an appropriate dosctring.
Code — cell 38
def my_det(mat):
"<Insert docstring here>"
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 39
(b) Test your function on the matrix $\displaystyle{\left(\begin{array}{ccc}1.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$, making sure its output is consistent (up to reasonable rounding error) with that of the `det` function from `numpy.linalg`.Code — cell 40
mat1 = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError()
Notes — cell 41
The following listing is a recursive implementation of a more "traditional" way of calculating determinants, using <b>cofactor reduction</b>:
Code — cell 42
def my_det2(mat):
"Calculates det(mat) recursively using cofactor reduction"
dim = len(mat)
if dim == 1:
return mat[0,0]
else:
total = 0
sign = 1
for i in range(dim):
total += sign*mat[0,i]*my_det2(mat[1:, (np.arange(dim) != i)])
sign = -sign
return totalNotes — cell 43
(b) Compare the output of your `my_det` function and this `my_det2` function on the matrix `mat_sp_sym`, given below. Show, using suitable algebraic manipulations in SymPy, that the calculated determinants are the same.
Code — cell 44
mat_sp_sym = np.array(
[[sp.symbols(el) for el in row] for row in
[["a11", "a12", "a13"], ["a21", "a22", "a23"], ["a31", "a32", "a33"]]])Code — cell 45
# YOUR CODE HERE raise NotImplementedError()
Code — cell 46
# YOUR CODE HERE raise NotImplementedError()
Notes — cell 47
(c) The `row_add` function is $\Theta(n)$, in the sense that each application of `row_add` to two rows of length $n$ requires $n$ separate additions. Suppose `my_det` is called on an $n\times n$ matrix; give a "big Theta" description of the dependence of its time-complexity on $n$, giving reasons for your answer.
Notes — cell 48
YOUR ANSWER HERE
Notes — cell 49
(d) When `my_det2` is called on an $n\times n$ matrix, it spawns $n$ recursive calls, each on an $(n-1)\times(n-1)$ matrix; the base case, in which the matrix is $1\times1$, is $\Theta(1)$. Give a "big Theta" description of the dependence of the time-complexity of `my_det2` on $n$, giving reasons for your answer.
Notes — cell 50
YOUR ANSWER HERE
Notes — cell 51
### Part (ii)
In order to work, the `zero_column` function depends on dividing by the pivot element ($a_{0,0}$, or $a_{1,1}$, or whichever it is). This can't be done if this element is zero; and even if it's close to zero but not actually zero, this can create damaging numerical instabilities.
We can prevent these difficulties by, if necessary, performing a row swap before we call `zero_column`, so that the pivot element is as large, numerically, as it can possibly be.
(a) Execute the following code:Code — cell 52
data = np.array([2.3, 1.0, -5.7, 5.5, -2.9, -3.1, 1.9, -10.9, 4.4]) print(np.argmax(data)) print(np.argmax(np.abs(data)))
Notes — cell 53
Explore further if you like. Write down a short description of what the NumPy function `argmax` does.
Notes — cell 54
YOUR ANSWER HERE
Notes — cell 55
(b) Amend the coding for `zero_column` as follows. The amended function should still take as its arguments `mat` and `pivot_index`, but it should then inspect all the entries in column number `pivot_index`, starting with the entry on the leading diagonal and working downwards, looking for the position of the element with the greatest absolute value (you may find the `argmax` function helpful for this). If this greatest absolute value is zero, the function should terminate immediately without doing anything; no column-zeroing is necessary or possible. If the greatest absolute value lies below the leading diagonal (in row $r$, say), then it should swap row $r$ with row number `pivot_index`; you can use the `row_swap` function you wrote earlier for this. It should then proceed exactly as before, looping over `row_index` and repeatedly applying `row_add` (this part of the code has been left intact).
Code — cell 56
def zero_column(mat, pivot_index):
# YOUR CODE HERE
raise NotImplementedError()
for row_index in range(pivot_index+1,len(mat)):
multiple = -mat[row_index, pivot_index]/mat[pivot_index, pivot_index]
row_add(mat, row_index, pivot_index, multiple)Notes — cell 57
(c) Test this amended function on the matrix
$\displaystyle{\left(\begin{array}{ccc}0.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$, with `pivot_index` set to 0.Code — cell 58
mat4 = np.array([[0, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError() mat4
Notes — cell 59
Unlike `row_add`, `row_swap` doesn't preserve determinants; instead, each row swap changes the sign of the determinant from positive to negative, or vice versa. (d) Further amend `zero_column` so that as well as changing the value of `mat`, it returns the value `True` if a row swap has been necessary, and `False` otherwise. (Note that it should return `False` even if it terminates immediately because the greatest absolute value is zero.)
Code — cell 60
def zero_column(mat, pivot_index):
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 61
(e) Test this amended function on the matrix
$\displaystyle{\left(\begin{array}{ccc}0.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$, with `pivot_index` set to 0; you should find that a row-swap has been necessary.Code — cell 62
mat4 = np.array([[0, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError() mat4
Notes — cell 63
(f) Amend your code for `row_echelon_form` so that it returns a tuple whose first element is the row echelon matrix as before, but whose second element is either $1$ or $-1$; it should be $1$ if the total number of row swaps carried out is even, and $-1$ if it is odd.
Code — cell 64
def row_echelon_form(mat):
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 65
(g) Amend your `my_det` function so that it works with this new version of `row_echelon_form`. Your function should adjust the sign of the determinant according to the number of row swaps carried out.
Code — cell 66
def my_det(mat):
# YOUR CODE HERE
raise NotImplementedError()Notes — cell 67
(h) Test your function against the function `det` from `numpy.linalg`, using the matrix $\displaystyle{\left(\begin{array}{ccc}0.0&2.0&3.0&4.0\\5.0&6.0&7.0&8.0\\9.0&10.0&12.0&13.0\\14.0&16.0&17.0&18.0\end{array}\right)}$.Code — cell 68
mat4 = np.array([[0, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 13], [14, 16, 17, 18]], dtype='float') # YOUR CODE HERE raise NotImplementedError()
Notes — cell 69
## Question 3 (10 marks) The following function takes as its arguments a matrix `mat` and a function `func`, which for our purposes may be assumed to be one of `my_det`, `my_det2` or `la.det`, and returns the execution time for calculating `func(mat)`.
Code — cell 70
def execution_time(mat, func):
start = time()
func(mat)
return time() - startNotes — cell 71
So for example, the following code times the process of running `my_det2` on a random real $4\times4$ matrix:
Code — cell 72
execution_time(nrnd.random([4,4]), my_det2)
Notes — cell 73
(a) Create a dictionary, `det_time_dict`, whose keys should be the strings `"la.det"`, `"my_det"` and `"my_det2"`. Each of the corresponding values should itself be a dictionary, whose keys should be the ints $n$ from $4$ to $9$ inclusive and whose values should be the execution times, using the appropriate function, for calculating the determinant of a random real $n \times n$ square matrix. <b>Notes</b>: - This may take a few seconds to execute, as one of these functions is pretty inefficient. - You may use either version of `my_det`: your original one, without row swaps, or your amended one, incorporating row swaps. - If you haven't been able to get `my_det` to work at all, you may omit this dictionary item.
Code — cell 74
# YOUR CODE HERE raise NotImplementedError() det_time_dict
Notes — cell 75
(b) Export this dictionary to the file `"det_times_pickle"` using the `dump` function from the `pickle` module.
Code — cell 76
# YOUR CODE HERE raise NotImplementedError()
Notes — cell 77
(c) Create a `pandas` DataFrame containing this data whose index consists of the ints from $4$ to $9$ inclusive and whose column headings are the strings `"la.det"`, `"my_det"` and `"my_det2"`. <b>Note</b>: if you haven't been able to get `my_det` to work, you may omit this column.
Code — cell 78
# YOUR CODE HERE raise NotImplementedError()
Notes — cell 79
(d) Export this DataFrame as the CSV file `"det_times_csv"` using the `to_csv` method from `pandas`.
Code — cell 80
# YOUR CODE HERE raise NotImplementedError()