Back to courses

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: 3e9590ad7ecf169502a11049bc3d2de1f4836177483f387956afa00ed71db203
Source 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>
Original notebook output
<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>
Original notebook output
<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>
Original notebook output
<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>
Original notebook output
<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>
Original notebook output
<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 times
l = 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 correct

Notes — 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 marks

Notes — 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 l3

Notes — 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 marks

Notes — 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 discretion

Notes — 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]