Plagiarism

Integrity and originality are paramount in the educational process. In this course, all code in your assessed Jupyter notebooks should be exclusively your own work. Sharing or borrowing code from your peers is strictly prohibited and is in violation of the academic integrity policy. Do not engage in 'copy-pasting' code from online repositories, forums, or platforms such as Stack Overflow. While these sources can be instructive for learning, directly copying code from them undermines the assessment objectives, jeopardizes your academic standing, and can result in disciplinary action. Additionally, refrain from using generative AI tools, such as code-generating algorithms, to complete your assignments. Utilizing artificial intelligence to write your code defeats the purpose of the assessment, which is to gauge your understanding and skill in the subject matter. Failure to abide by these guidelines will be considered academic dishonesty and will be subject to severe penalties.

FAQ

Can I Define a Function Inside a Loop?

How to combine arrays

What is tuple unpacking?

How do I get my feedback?

How to Create an Infinite Loop

How Can I Be Sure My Submission Went In

Why Can I Not Save My Notebook

How Does the Marking System Work

Why is my curve not shown in my plot?

What is the difference between np.empty and np.zeros?

How to create a list of values spaced equally on a logarithmic scale

How to Loop Over Elements of an Array

How to Construct a List

How can I edit the cell with the assert() code?

Are the assignments summative?

Do I have to validate before submitting?

What is the raise NotImplementedError() for?

Can I Define a Function Inside a Loop?

Yes it is possible, and often it is useful. Say we have the function

def evaluateAFunctionAt2(f):
    print ("The value of this function at x=2 is ", f(2))

Which takes a function and evaluates it at 2. If we want to use it for a family of functions $f(x)=x^i$ we can use this loop:

for exponent in range(1,10):
    def theFunction(x):
        return x**exponent
    evaluateAFunctionAt2(theFunction)    
The value of this function at x=2 is  2
The value of this function at x=2 is  4
The value of this function at x=2 is  8
The value of this function at x=2 is  16
The value of this function at x=2 is  32
The value of this function at x=2 is  64
The value of this function at x=2 is  128
The value of this function at x=2 is  256
The value of this function at x=2 is  512

How to combine arrays

If we have two arrays:

import numpy 

a = numpy.array([1.0, 2.0, 3.0])
b = numpy.array([4.0 ,5.0 ,6.0])

and we want to combine them into a single array, we have (at least) two options. The first is to create an array to contain the combination and fill it using indexing:

combined = numpy.empty(6)
combined[:3] = a
combined[3:] = b
print(combined)
[1. 2. 3. 4. 5. 6.]

Another option is to use numpy.concatenate:

combined = numpy.concatenate([a,b])
print(combined)
[1. 2. 3. 4. 5. 6.]

numpy.concatenate has more options to combine more structured arrays.

What is tuple unpacking?

Tuple unpacking refers to the ability of assigning several values in one assignment. For example in

a, b, c = 1, 2, 3

a, b, c = ['a', 'b', 'c']

we can set a, b, c at once. Tuple unpacking is especially useful when looping over lists. Say we have a list of restaurant orders:

orders = [
    ('Adam', 'Pizza funghi', 7.99),
    ('Sarah', 'Gnocchi al pesto', 8.59),
    ('Zoe', 'Spaghetti al arabiata', 6.55)
]

We can loop over the orders and index into each order to access the name, meal and price

total = 0

for order in orders:
    total += order[2]
    print ("{} ordered {}".format(order[0], order[1]) )

but we can make the code a lot more explicit using tuple unpacking:

total = 0

for name, meal, price in orders:
    total += price
    print ("{} ordered {}".format(name, meal) )

print("Total bill: {}".format(total))
Adam ordered Pizza funghi
Sarah ordered Gnocchi al pesto
Zoe ordered Spaghetti al arabiata
Total bill: 23.13

How do I get my feedback?

To get feedback go to the assignment tab, the assignment for which feedback is available should have a “fetch feedback” button. Pressing this will copy the feedback to your server. Once this is done you can view the feedback by clicking on the “view” link.

How to Create an Infinite Loop

To create a loop where one is not sure how many iterations will be necessary one can use a while loop.

We can use While True to repeat indefinitely, but we have to escape the loop according to some criteria. There are two ways of escaping: break or return if inside a function.

Using break:

i = 0
while True:
    if i == 17:
        break 
    i += 1
        
i
17

Using return in a function body:

def doForAWhile():
    i = 0
    while True:
        if i == 17:
            return i 
        i += 1
doForAWhile()
17

The criterium can also be in the while statement:

i = 0 
while i < 234:
    i += 1
i
234

With infinite loops there is a danger: they can actually go infinite, that is if we miss the termination condition the computer will never finish executing the code. If this happens in a notebook the execution can be stopped using the square icon in the notebook toolbar, or use the “Kernel-interrupt” menu.

Sometimes the infinite loop uses more and more resources, for example if elements are added to an array in the loop and the size of the array gets out of control. For example here:

aList = []
i = 0

while True:
    aList.append(i)
    i += 1

You might get a “Kernel restarting” error, saying “The kernel appears to have died. It will restart automatically.” THis happens because the memory available to your server has been exhausted. In accute cases the notebook might become unresponsive. The best way to solve the problem is to close the tab and to go to the “Running” tab in the home page and click the “shutdown” button.

With all these issues it is really worth being preventive with infinite loops. One way is to stop the loop after a large number of iteration, when it becomes clear something went wrong:

def runCodeRun():
    x = 0.0
    nIterations = 0

    while nIterations < 100000 :
        x += 0.1
        if x == 123.6:
            return "Victory!"
        nIterations += 1
    
    # if I get there it means the loop did not end after 100000 iterations
    return "Oops, the loop never ended..."

runCodeRun()
'Oops, the loop never ended...'

This code should work but we got into trouble with numerical accuracy, we can fix it by using inequalities rather than strict equalities. Having the safeguard with nIteration allowed us to diagnose the problem immediately and not have to stare at the notebook for ages while we wonder why the code never finished.

def runCodeRun():
    x = 0.0
    nIterations = 0

    while nIterations < 100000 :
        x += 0.1
        if x >= 123.6:
            return "Victory!"
        nIterations += 1
    
    # if I get there it means the loop did not end after 100000 iterations
    return "Oops, the loop never ended..."

runCodeRun()
'Victory!'

If we are not within a function we can use while, break, else to achieve the same effect.

x = 0.0
nIterations = 0

while nIterations < 100000 :
    x += 0.1
    if x == 123.6:
        print ("Victory!")
        break
    nIterations += 1
else:
    print("Oops, the loop never ended...") 
Oops, the loop never ended...

This can be fixed again using inequalities:

x = 0.0
nIterations = 0

while nIterations < 100000 :
    x += 0.1
    if x >= 123.6:
        print ("Victory!")
        break
    nIterations += 1
else:
    print("Oops, the loop never ended...") 
Victory!

How Can I Be Sure My Submission Went In

When you press the “submit” button a new line should appear in the list of your submissions with the time of the submission (the time is in UTC and might be off by one hour depending on the timezone).

If you went to the submission page and left it for more than an hour it can happen that your srever was stopped and the page you see in your browser is not linking to it any longer. The symptom of that is that when you submit no new line is added and you might get an error message. If this happens you have to log back in and submit. Clicking on the “Files” tab should restart your server and get you ready to submit.

Why Can I Not Save My Notebook

If your notebook has too much output (typically if you use print statements in a large loop) your notebook will refuse to save. You would get a message saying “request entity too large” or you might see “autosave failed!” messages. Remove the offending print function calls and rerun the cells to be able to save the notebook. You could also use “Restart and clear all output” for the Kernel menu to remove all output. You will still need to remove the print calls but this should allow you to save your work first.

How Does the Marking System Work

The marking works in the following way:

  • the marking system retrieves your last submitted notebook
  • it removes all output
  • it runs all cells in order

This means that you have to make sure your notebook produces all plots you want to have in your assignment. You can see what would be the result of running your notebook like the marking system does by selecting “Restart & Run all” in the “Kernel menu”.

Why is my curve not shown in my plot?

If you plotted some curve and you cannot see it there could be a few problems:

  • if your plot is a log plot and your curve has negative values they are not shown. Print the values you want the plot to check they are not negative!

  • if you have some hard-coded limits through plt.xlim or plt.ylim your curve might be outside of that range. It can help to print the values you want to plot, or to temporarily disable the limits to check where your curves are.

  • if you have several curves but only see one: they might be on top of each other!

What is the difference between np.empty and np.zeros?

With np.zeros you create an array and wet all of its elements to 0. The time spent setting all elements to zero can be a waste of time if you never use the 0 values and fill the array with different values later:

a = numpy.zeros(10)
for i in range(10):
    a[i] = 1.0 + i

There was no point spending time setting all elements to 0, and then set them to something else. This is why np.empty is for: it creates the array but does not initialise the elements. This means the what the actual values are are quite random, if you use them you will get unpredictable results. It is perfectly safe to use if you are initialising all values of the array before using them. THe code below will have the same effect as the code above but will be quicker since we are not writing 0s in hte array before writing the values we really want.

a = numpy.empty(10)
for i in range(10):
    a[i] = 1.0 + i

How to create a list of values spaced equally on a logarithmic scale

You can use list comprehentions:

[2**n for n in range(20)]
[1,
 2,
 4,
 8,
 16,
 32,
 64,
 128,
 256,
 512,
 1024,
 2048,
 4096,
 8192,
 16384,
 32768,
 65536,
 131072,
 262144,
 524288]
[10**(expo/10) for expo in range(100)  ]
[1.0,
 1.2589254117941673,
 1.5848931924611136,
 1.9952623149688795,
 2.51188643150958,
 3.1622776601683795,
 3.9810717055349722,
 5.011872336272722,
 6.309573444801933,
 7.943282347242816,
 10.0,
 12.589254117941675,
 ...
 794328234.7242821,
 1000000000.0,
 1258925411.794166,
 1584893192.4611108,
 1995262314.9688828,
 2511886431.509582,
 3162277660.1683793,
 3981071705.5349693,
 5011872336.272715,
 6309573444.801943,
 7943282347.242822]

The numpy.logspace function can be used for this. Be careful, the arguments are the minimum and maximum exponent!

numpy.logspace(1,6)
array([1.00000000e+01, 1.26485522e+01, 1.59985872e+01, 2.02358965e+01,
       2.55954792e+01, 3.23745754e+01, 4.09491506e+01, 5.17947468e+01,
       6.55128557e+01, 8.28642773e+01, 1.04811313e+02, 1.32571137e+02,
       1.67683294e+02, 2.12095089e+02, 2.68269580e+02, 3.39322177e+02,
       4.29193426e+02, 5.42867544e+02, 6.86648845e+02, 8.68511374e+02,
       1.09854114e+03, 1.38949549e+03, 1.75751062e+03, 2.22299648e+03,
       2.81176870e+03, 3.55648031e+03, 4.49843267e+03, 5.68986603e+03,
       7.19685673e+03, 9.10298178e+03, 1.15139540e+04, 1.45634848e+04,
       1.84206997e+04, 2.32995181e+04, 2.94705170e+04, 3.72759372e+04,
       4.71486636e+04, 5.96362332e+04, 7.54312006e+04, 9.54095476e+04,
       1.20679264e+05, 1.52641797e+05, 1.93069773e+05, 2.44205309e+05,
       3.08884360e+05, 3.90693994e+05, 4.94171336e+05, 6.25055193e+05,
       7.90604321e+05, 1.00000000e+06])

How to Loop Over Elements of an Array

To loop over the elements of an array we can loop over an index that goes from 0 to the lenght of the array:

allElements = [1,2,3,7,99]

for i in range(len(allElements)):
    print(allElements[i])
1
2
3
7
99

This is useful if you need i for other purposes, such as accessing values of the array at different places. If you do not need i but are only interested in the element itself you can use the following Python syntax:

allElements = [1,2,3,7,99]

for oneElement in allElements:
    print(oneElement)
1
2
3
7
99

How to Construct a List

There are many ways to construct a list in python, I list here a few. In each example we will construct the set of the ten first odd numbers.

For loop

You can create a new list and fill it in the for loop.

l = []
for i in range(10):
    o = 2 * i +1
    l.append(o)
print(l)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

alternatively you can fill an existing list with the elements:

l = [0]*10
for i in range(10):
    o = 2 * i +1
    l[i] = o
print(l)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

List comprehension

Because creating lists is so common python has a dedicated syntax for it called list comprehension, here is an example.

l = [ 2*i + 1 for i in range(10) ]
print(l)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

Examples using numpy

This is the same as the for loop above, but with a numpy array instead of a list. Note that the default for numpy arrays is floats, so if you want integers you have to specify dtype=int when you create the array.

import numpy
l = numpy.zeros(10, dtype=int)
for i in range(10):
    l[i] = 2*i + 1
print(l)
[ 1  3  5  7  9 11 13 15 17 19]

For simple intervals you can use the numpy.linspace function:

l = numpy.linspace(1, 19, 10, dtype=int)
print(l)
array([ 1,  3,  5,  7,  9, 11, 13, 15, 17, 19])

For simple operations you can manipulate numpy arrays to obtain new ones:

all_i = numpy.array(range(10))
l = 2 * all_i +1
print(l)
[ 1  3  5  7  9 11 13 15 17 19]

This is the same as above, but using a function instead.

all_i = numpy.array(range(10))
def odd(i):
    return 2*i + 1
l = odd(all_i)
print(l)
[ 1  3  5  7  9 11 13 15 17 19]

The two method above require the function to be simple enough for numpy to understand it, that means, among other that mathematical functions such as sin or sqrt are imported from the numpy library and not from the math library.

How can I edit the cell with the assert() code?

You can’t… This is by design: the assert cells are there to test your work and are part of the assessment. If you want to add more tests to help you debug you can create a new cell using the “+” button in the toolbar at the top of the notebook. The cells you create can be edited any way you want.

Are the assignments summative?

Yes, the assignments are summative. They account for 17% of the module mark.

Do I have to validate before submitting?

No, there is no need to validate before submitting, but it can be useful to check that your notebook executes properly.

What is the raise NotImplementedError() for?

In a coding exercise the place you have to insert code in is marked by the following lines.

# YOUR CODE HERE
raise NotImplementedError()

Raising an exception everywhere you are supposed to contribute some code makes sure that you will not miss one. When you use “validate” you will be warned that you still need to do something there. Once you have written the code you can remove the raise statement. If you return before the raise statement there is no need to remove the line for the test to run without error.