assessment2 2025 2026 markscheme
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: 3e9590ad7ecf169502a11049bc3d2de1f4836177483f387956afa00ed71db203Source date: 2026-07-31
Notes — cell 1
# MATH40006 In-Course Assessment # 18 February 2026, 09.10-10.10 am ## One Hour ### 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 others: - from the `numpy` module, the constant `pi` and the functions `cos`, `sin`, `linspace`, `arange`, `meshgrid`, `tile`, `flatnonzero`,`array`, `zeros` and `append`; - from the `pyplot` submodule of `matplotlib`, the functions `imshow`, `contour` and `quiver`; - from the `Axes3D` submodule of `mpl_toolkits`, the function `plot_wireframe`; - from `sympy`, the function `Rational`. We recommend that you execute the following cell:
Code — cell 3
import numpy as np import sympy as sp import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D
Notes — cell 4
<b>Important</b>: this test is open-book, in the sense that you have access to all the materials on Blackboard, and to documentation websites for Python, NumPy, SymPy and matplotlib. The use of other websites, and of AI assistance, is strictly prohibited. If your code should hang: - As a first instance try to interrupt the kernel. - If that fails, try to restart the kernel. - If that fails, close all Python and Jupyter windows and restart Python. The notebook will be autosaved, but it's a good idea to save manually too, especially before executing a `while` loop or a recursion.
Notes — cell 5
## Question 1 (20 marks) (a) Using `linspace` or `arange`, define the variable `x_values1` to be a one-dimensional array of 121 elements, equally spaced between $-\pi$ and $\pi$ inclusive. Also, define `y_values1` to be a one-dimensional array of 121 elements, equally spaced between $-3$ and $3$ inclusive.
Code — cell 6
x_values1, y_values1 = np.linspace(-np.pi, np.pi, 121), np.linspace(-3, 3, 121) # 2 marks
Notes — cell 7
(b) Using `meshgrid`, define `x1` and `y1` to be $121\times121$ arrays consisting, respectively, of the $x$- and $y$-coordinates of the points of the lattice formed from `x_values1` and `y_values1`.
Code — cell 8
x1, y1 = np.meshgrid(x_values1, y_values1) # 2 marks
Notes — cell 9
(c) Using `imshow` with appropriate settings for the keyword arguments `origin` and `extent`, generate a colour-map visualisation of the function
$$u(x,\,y) = \frac{y^2}{2}-\cos(x).$$
Your values for $x$ and $y$ should come from `x1` and `y1` respectively.Code — cell 10
plt.imshow(-np.cos(x1) + y1**2/2, origin='lower', extent=[-np.pi,np.pi, -3, 3]) # 3 marks # (1 mark each for: correct use of imshow; correct use of origin arg; correct use of extent arg)
<matplotlib.image.AxesImage at 0x16ae5ec30e0>
<Figure size 640x480 with 1 Axes>
Notes — cell 11
(d) Again using `x1` and `y1`, create a contour plot of the function $u(x,\,y)$ for $-\pi\le x\le\pi$, $-3\le y\le 3$.
Code — cell 12
plt.contour(x1, y1, -np.cos(x1) + y1**2/2) # 2 marks
<matplotlib.contour.QuadContourSet at 0x16ae5ff97f0>
<Figure size 640x480 with 1 Axes>
Notes — cell 13
(e) Using `linspace` or `arange`, define the variable `x_values2` to be a one-dimensional array of 21 elements, equally spaced between $-\pi$ and $\pi$ inclusive. Also, define `y_values2` to be a one-dimensional array of 21 elements, equally spaced between $-3$ and $3$ inclusive.
Code — cell 14
x_values2, y_values2 = np.linspace(-np.pi, np.pi, 21), np.linspace(-3, 3, 21) # 2 marks
Notes — cell 15
(f) Using `meshgrid`, define `x2` and `y2` to be $21\times21$ arrays consisting, respectively, of the $x$- and $y$-coordinates of the points of the lattice formed from `x_values2` and `y_values2`.
Code — cell 16
x2, y2 = np.meshgrid(x_values2, y_values2) # 2 marks
Notes — cell 17
(g) The partial derivatives of $u(x,\,y)$ are
$$\begin{align}
\frac{\partial u}{\partial x} &= \sin x,\\
\frac{\partial u}{\partial y} &= y.
\end{align}$$
Using `quiver`, with coordinates drawn from `x2` and `y2`, create a vector field plot of the field
$$\left(\begin{array}{c}
\partial u/\partial y\\
-\partial u/\partial x
\end{array}\right)$$
for $-\pi\le x \le \pi$, $-3\le y \le 3$.Code — cell 18
plt.quiver(x2, y2, y2, -np.sin(x2)) # 2 marks
<matplotlib.quiver.Quiver at 0x16ae689fe90>
<Figure size 640x480 with 1 Axes>
Notes — cell 19
(h) Create an image consisting of your vector field plot, in blue, superimposed on your contour plot, in red.
Code — cell 20
plt.contour(x1, y1, -np.cos(x1) + y1**2/2, colors='red') plt.quiver(x2, y2, y2, -np.sin(x2), color='blue') # 2 marks
<matplotlib.quiver.Quiver at 0x16ae607df10>
<Figure size 640x480 with 1 Axes>
Notes — cell 21
(i) Using `plot_wireframe`, create a 3D plot of the surface $$z=u(x,\,y).$$ Use coordinates drawn from `x1` and `y1`.
Code — cell 22
ax = plt.axes(projection='3d') ax.plot_wireframe(x1, y1, -np.cos(x1) + y1**2/2) # 3 marks
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x16ae60645c0>
<Figure size 640x480 with 1 Axes>
Notes — cell 23
## Question 2 (20 marks) This question involves two NumPy functions that you may not have studied: `tile` and `flatnonzero`. (a) Try the code ```python np.tile([1, 0, 7, 0, 0, 2], 3) ``` Experiment further. Write a short description of what `tile` does.
Code — cell 24
# space for code
Code — cell 25
# space for code
Code — cell 26
# space for code
Notes — cell 27
```python np.tile(arr, n) ``` returns an array consisting of `n` copies of `arr`, connected head to tail.
Notes — cell 28
<b>1 mark; accept anything sensible</b>
Notes — cell 29
(b) Try the code ```python np.flatnonzero([1, 0, 7, 0, 0, 2]) ``` Experiment further. Write a short description of what `flatnonzero` does.
Code — cell 30
# space for code
Code — cell 31
# space for code
Code — cell 32
# space for code
Notes — cell 33
```python np.flatnonzero(arr) ``` returns an array consisting of the positions in `arr` at which a non-zero element is to be found.
Notes — cell 34
<b>1 mark; accept anything sensible</b>
Notes — cell 35
The rest of this question is about a method called the <b>Sieve of Pritchard</b>, which is a little like the Sieve of Eratosthenes but different. We'll begin by using it to sieve the integers between $1$ and $29$. (c) Set the variable `n` equal to 29, and set `sieving_array` equal to a NumPy Boolean array of length $1$, consisting of a single `True`.
Code — cell 36
n = 29 sieving_array = np.array([True]) # 2 marks
Notes — cell 37
(d) Using the NumPy `zeros` function, with `dtype` set to `'bool'`, or otherwise, set `discards` equal to a NumPy Boolean array all of whose elements are `False`. The length of this array should be `n + 1`; that is, it should have length $30$.
Code — cell 38
discards = np.zeros(n+1, dtype='bool') # 1 mark
Notes — cell 39
(e) Initialize the variables `l` and `p`, giving them the values $1$ and $2$ respectively.
Code — cell 40
l, p = 1, 2 # 1 mark
Notes — cell 41
(f) Repeat the following steps for as long as $\min(p^2,l+1)\le n$. You may do this either as a `while` loop or simply by repeated evaluation (there won't be many iterations!) - Set `discards[p]` equal to `True`. - If $l<n$: - set `sieving_array` equal to `np.tile(sieving_array, p)[:n]` (that is, the elements of `np.tile(sieving_array, p)`, up to a maximum of `n` elements); - set `l` equal to `l * p`; - print the new value of `l`. - If $p\le l$, set `sieving_array[p-1]` to `False`. - Set every $p$th element of `sieving_array`, beginning with the element with index `p**2-1`, to `False`. (You should be able to do this with a single line, without using a loop or comprehension.) - Set `nonzeropositions` equal to `np.flatnonzero(sieving_array)`. - If `nonzeropositions` has length $1$, then set `p` equal to `l + 1`. Otherwise, set `p` equal to element $1$ of `nonzeropositions`, plus $1$. - Print the value of `p`; if the algorithm is working, this should always be a prime. Remember to stop when $\min(p^2,l+1) > n$.
Code — cell 42
discards[p] = True
if l < n:
sieving_array = np.tile(sieving_array, p)[:n]
l *= p
print(f'l = {l}')
if p <= l:
sieving_array[p-1] = False
sieving_array[p**2-1::p] = False
nonzeropositions = np.flatnonzero(sieving_array)
if len(nonzeropositions) == 1:
p = l + 1
else:
p = nonzeropositions[1] + 1
print(f'p = {p}')l = 2 p = 3
Code — cell 43
discards[p] = True
if l < n:
sieving_array = np.tile(sieving_array, p)[:n]
l *= p
print(f'l = {l}')
if p <= l:
sieving_array[p-1] = False
sieving_array[p**2-1::p] = False
nonzeropositions = np.flatnonzero(sieving_array)
if len(nonzeropositions) == 1:
p = l + 1
else:
p = nonzeropositions[1] + 1
print(f'p = {p}')l = 6 p = 5
Code — cell 44
discards[p] = True
if l < n:
sieving_array = np.tile(sieving_array, p)[:n]
l *= p
print(f'l = {l}')
if p <= l:
sieving_array[p-1] = False
sieving_array[p**2-1::p] = False
nonzeropositions = np.flatnonzero(sieving_array)
if len(nonzeropositions) == 1:
p = l + 1
else:
p = nonzeropositions[1] + 1
print(f'p = {p}')
# 5 marks
# Award part marks at your discretion, making sure candidates receive due credit for things they do correctly
# Deduct 1 mark if the stopping condition is applied incorrectly
# Accept either: a while loop; repeated blocks of code, as here; one block of code, evaluated several timesl = 30 p = 7
Notes — cell 45
(g) Print the final values of `np.flatnonzero(discards)` and `np.flatnonzero(sieving_array[1:]) + 2`.
Code — cell 46
print(np.flatnonzero(discards)) print(np.flatnonzero(sieving_array[1:]) + 2) # 2 marks
Notes — cell 47
(h) Write a function called `pritchard_sieve`, which takes an argument `n`, assumed to be a positive integer, and uses the above algorithm to calculate and return an array of the primes between $2$ and $n$ inclusive. Your code need not include any `print` statements.
Code — cell 48
def pritchard_sieve(n):
sieving_array = np.array([True])
discards = np.zeros(n+2, dtype='bool')
l, p = 1, 2
while min(p**2, l+1) <= n:
discards[p] = True
if l < n:
sieving_array = np.tile(sieving_array, p)[:n]
l *= p
if p <= l:
sieving_array[p-1] = False
sieving_array[p**2-1::p] = False
nonzeropositions = np.flatnonzero(sieving_array)
if len(nonzeropositions) == 1:
p = l + 1
else:
p = nonzeropositions[1] + 1
sieved = np.append(np.flatnonzero(discards), np.flatnonzero(sieving_array[1:]) + 2)
return sieved
# 5 marks
# Award part marks at your discretion, making sure candidates receive due credit for things they do correctly
# Deduct 1 mark if the stopping condition is applied incorrectly
# Deduct 1 mark if no value, or the wrong value, is returned, but the code is otherwise correctNotes — cell 49
(i) Test your function, including typical cases and edge cases. Include at least one value of `n` with five or more digits.
Code — cell 50
Code — cell 51
Code — cell 52
Code — cell 53
Code — cell 54
# 2 marks # If testing has been done, but falling short of the range of cases required, award 1 mark
Notes — cell 55
## Question 3 (10 marks)
(a) Write a function `neighbor_sums`, which takes a single argument `lis`, assumed to be a list of the form $[a_0, a_1, a_2, a_3, \dots, a_{n-1}, a_n]$, and returns the list
$$[a_0 + a_1,\, a_1 + a_2,\, a_2 + a_3,\, \dots,\, a_{n-1} + a_n].$$Code — cell 56
def neighbor_sums(lis):
return [lis[i]+lis[i+1] for i in range(len(lis) - 1)]
# 2 marksNotes — cell 57
(b) Test your function: ```python neighbor_sums([1, 3, 5, 7, 9]) ``` should return ``` [4, 8, 12, 16] ```
Code — cell 58
neighbor_sums([1, 3, 5, 7, 9])
[4, 8, 12, 16]
Notes — cell 59
For part (c) of this question, you may wish to use the function `riffle`, listed below, which takes arguments `l1` and `l2`, assumed to be lists which are either of the same length, or such that `l1` contains one more element than `l2`, and generates a list consisting of the elements of the two lists, alternating with one another.
Code — cell 60
def riffle(l1, l2):
l3 = l1 + l2
l3[::2] = l1
l3[1::2] = l2
return l3Notes — cell 61
Execute the following cell to see what `riffle` does.
Code — cell 62
print(riffle([0,2,4],[1,3,5])) print(riffle([0,2,4],[1,3]))
[0, 1, 2, 3, 4, 5] [0, 1, 2, 3, 4]
Notes — cell 63
(c) Write a function `riffled_neighbor_sums`, which takes a single argument `lis`, assumed to be a list of the form $[a_0, a_1, a_2, a_3, \dots, a_{n-1}, a_n]$, and returns the "riffled" list
$$[a_0,\, a_0 + a_1,\, a_1,\, a_1 + a_2,\, a_2,\, a_2 + a_3,\, a_3,\, \dots,\, a_{n-1},\, a_{n-1} + a_n].$$Notes — cell 64
<b>Note for markers</b>: this should have read
$$[a_0,\, a_0 + a_1,\, a_1,\, a_1 + a_2,\, a_2,\, a_2 + a_3,\, a_3,\, \dots,\, a_{n-1},\, a_{n-1} + a_n, a_n].$$
An announcement was made, but please accept either solution, and allow follow marks below.Code — cell 65
def riffled_neighbor_sums(lis):
return riffle(lis, neighbor_sums(lis))
# 2 marksNotes — cell 66
(d) Test your function: ```python riffled_neighbor_sums([1, 3, 5, 7, 9]) ``` should return ``` [1, 4, 3, 8, 5, 12, 7, 16, 9] ```
Code — cell 67
riffled_neighbor_sums([1, 3, 5, 7, 9]) # 1 mark
[1, 4, 3, 8, 5, 12, 7, 16, 9]
Notes — cell 68
The <b>Stern-Brocot tree</b> is an infinite tree of fractions that is known to contain every positive rational number exactly once. The $n$th <b>generation</b> of the Stern-Brocot tree may be generated as follows. Starting with the list `lis = [np.array([0, 1]), np.array([1, 1]), np.array([1, 0])]`, iterate ```python lis = riffled_neighbor_sums(lis) ``` $n$ times. Then, using the final value of `lis`, take the odd-indexed elements, with indexes $1$, $3$, $5$ etc, of `lis`, and convert each element `el` into a SymPy Rational whose numerator is `el[0]` and whose denominator is `el[1]`. Return the resulting list of Rationals. (e) Write a function `sb_generation`, which takes a non-negative int `n`, and returns the $n$th Stern-Brocot generation as a list of SymPy Rationals.
Code — cell 69
def sb_generation(n):
lis = [np.array([0, 1]), np.array([1, 1]), np.array([1, 0])]
for _ in range(n):
lis = riffled_neighbor_sums(lis)
return [sp.Rational(el[0],el[1]) for el in lis[1::2]]
# 3 marks
# Award part marks at your discretionNotes — cell 70
(f) Test it: ```python sb_generation(3) ``` should return ``` [1/4, 2/5, 3/5, 3/4, 4/3, 5/3, 5/2, 4] ```
Code — cell 71
sb_generation(3) # 1 mark
[1/4, 2/5, 3/5, 3/4, 4/3, 5/3, 5/2, 4]