Skip to content

← Final exam model papers

Final Exam Model Paper 6

Theory paper: 50 marksTime: 2 hours

Practical exam: 20 marksTime: 1 hour 30 minutes

All questions are compulsory.

A full practice run of the Grade 12 final exam: the theory paper (50 marks, 2 hours) and, below it, the practical exam (20 marks, 1 hour 30 minutes, at a computer, marked by the school). In the theory paper, work out on paper what each program does, as in the exam hall: its programs have no Run button. In the practical exam, the programs run on this page.

A practice paper by Science Playground, made to its own course’s grid; not an official exam paper.

Write your answers on paper first. Then open each answer and its marking scheme, and give yourself one mark for each point you got. In multiple choice, your first choice counts.

Section A

10 × 1 = 10

1. Choose the correct option for each of the following questions.

a.

What is the space complexity of an algorithm?

Pick an answer to see why.

b.

A trekking agency in Solukhumbu plans a guide’s round of 12 lodges. Its manager says: “The traveling salesperson problem is NP-complete, so no computer can find the shortest round trip, even through 12 lodges. Our guide’s own order is as good as anything a program could give.” Which judgment is right?

Pick an answer to see why.

c.

What is a pull request?

Pick an answer to see why.

d.

The Manifesto for Agile Software Development values “working software over comprehensive documentation”. What does this mean?

Pick an answer to see why.

e.

In a big service, what is a circuit breaker?

Pick an answer to see why.

f.

A hospital in Makwanpur keeps its patient records on a leader in Hetauda, with a follower copy in Kathmandu. It must never lose a record that a nurse was told was saved, even if the Hetauda building is destroyed a second later. Which choice does this need?

Pick an answer to see why.

g.

A data center’s computers use 2 MW, all day and night. Its WUE is 1.5 L/kWh. About how much water does it use on site in one day?

Pick an answer to see why.

h.

What does Kerckhoffs’s principle say?

Pick an answer to see why.

i.

A farmers’ group in Nawalparasi keeps its records in a hash chain. At each month’s meeting, the last hash is read out and every member writes it in a notebook. Months later the treasurer changes an old record and rebuilds every block after it, so that every link checks again. How can the members find out?

Pick an answer to see why.

j.

Sujan, an engineer at a company that runs a cable car for tourists, finds that the software that stops the cars in strong wind was switched off last month “to keep the cars running in the busy season”. His manager tells him to keep quiet. Which action best follows a professional code of ethics?

Pick an answer to see why.

Section B

8 × 3 = 24

Write short answers to the following questions.

2.
[3]

A program that makes RSA keys picks a random 300-digit number and tests it with 10 rounds of the Miller–Rabin test.

  • i.If the test says “not prime”, can it be wrong? Give a reason.
  • ii.If it says “prime” after 10 rounds, at most what is the chance that it is wrong?
  • iii.Why doesn’t the program simply divide the number by every whole number up to its square root?
Answer
  • i.No. The test says “not prime” only when it has found a witness: a number a for which the calculation comes out a way it never does for a prime. That is proof.
  • ii.Each round that finds no witness cuts the chance of being fooled to a quarter or less, so after 10 rounds it is at most 1 in 4¹⁰ = 1 in 1,048,576, about one in a million. (20 rounds would make it less than one in a million million.)
  • iii.The square root of a 300-digit number has about 150 digits, so dividing would take about 10¹⁵⁰ tries: no computer could ever finish. Each Miller–Rabin round takes only a few hundred multiplications, even for a 300-digit number.
Marking scheme
  • i. No: “not prime” comes with a witness, which is proof.
  • ii. At most 1 in 4¹⁰, about one in a million.
  • iii. Dividing up to the square root would take about 10¹⁵⁰ tries; Miller–Rabin takes a few hundred multiplications a round.
3.
[3]

A trekking app for Manang writes what it knows as facts, for the facts-and-rules engine of Unit 3:

facts = {    ("trail", "Chame", "Pisang"), ("trail", "Pisang", "Ngawal"), ("trail", "Pisang", "Manang"),    ("trail", "Ngawal", "Manang"), ("trail", "Manang", "Yak Kharka"), ("trail", "Yak Kharka", "Thorong Phedi"),    ("height", "Chame", 2670), ("height", "Pisang", 3200), ("height", "Ngawal", 3660),    ("height", "Manang", 3540), ("height", "Yak Kharka", 4050), ("height", "Thorong Phedi", 4450),}

Each trail is written once, going up the valley; heights are in meters, rounded. Write rules, in the engine’s form, for:

  • i.reach: b can be reached from a by one trail;
  • ii.reach: c can be reached from a by any number of trails;
  • iii.high: b can be reached from a and is higher than 3,500 m.
Answer
rules = [    (("reach", "?a", "?b"), [("trail", "?a", "?b")]),    (("reach", "?a", "?c"), [("trail", "?a", "?b"), ("reach", "?b", "?c")]),    (("high", "?a", "?b"), [("reach", "?a", "?b"), ("height", "?b", "?h"), ("<", 3500, "?h")]),]

The second rule is recursive, like the book’s ancestor: one trail, then anywhere that can be reached from there. With the engine of Unit 3:

# Facts: trails up the valley (each written once, going up) and heights in meters, roundedfacts = {    ("trail", "Chame", "Pisang"), ("trail", "Pisang", "Ngawal"), ("trail", "Pisang", "Manang"),    ("trail", "Ngawal", "Manang"), ("trail", "Manang", "Yak Kharka"), ("trail", "Yak Kharka", "Thorong Phedi"),    ("height", "Chame", 2670), ("height", "Pisang", 3200), ("height", "Ngawal", 3660),    ("height", "Manang", 3540), ("height", "Yak Kharka", 4050), ("height", "Thorong Phedi", 4450),} rules = [    (("reach", "?a", "?b"), [("trail", "?a", "?b")]),    (("reach", "?a", "?c"), [("trail", "?a", "?b"), ("reach", "?b", "?c")]),    (("high", "?a", "?b"), [("reach", "?a", "?b"), ("height", "?b", "?h"), ("<", 3500, "?h")]),]  def match(pattern, fact, found):    if len(pattern) != len(fact):        return None    found = dict(found)    for p, f in zip(pattern, fact):        if isinstance(p, str) and p.startswith("?"):            if found.setdefault(p, f) != f:                return None        elif p != f:            return None    return found  def solve(body, found):    if not body:        yield found        return    first, rest = body[0], body[1:]    if first[0] in ("!=", "<"):        a, b = (found.get(x, x) for x in first[1:])        if (a != b) if first[0] == "!=" else (a < b):            yield from solve(rest, found)        return    for fact in list(facts):        more = match(first, fact, found)        if more is not None:            yield from solve(rest, more)  while True:    new = set()    for head, body in rules:        for found in solve(body, {}):            new.add(tuple(found.get(x, x) for x in head))    if new <= facts:        break    facts |= new  def ask(*pattern):    return sorted(m["?who"] for f in facts if (m := match(pattern, f, {})))  print("From Chame you can reach:", ask("reach", "Chame", "?who"))print("Places that lead to Manang:", ask("reach", "?who", "Manang"))print("Above 3,500 m from Pisang:", ask("high", "Pisang", "?who"))

Output

From Chame you can reach: ['Manang', 'Ngawal', 'Pisang', 'Thorong Phedi', 'Yak Kharka']Places that lead to Manang: ['Chame', 'Ngawal', 'Pisang']Above 3,500 m from Pisang: ['Manang', 'Ngawal', 'Thorong Phedi', 'Yak Kharka']
Marking scheme
  • i. reach(?a, ?b) from one trail(?a, ?b).
  • ii. A recursive reach: a trail from ?a to ?b, then reach(?b, ?c).
  • iii. high from reach, the height, and < with 3500 (in either order that makes the height the bigger).
4.
[3]

Students in Lamjung build an app for their village’s homestays, and plan to share it free with other villages, source code and all. They used a map library under the MIT license, a calendar library under the GPL, photos of the village they found on a website, and a function that an AI chatbot wrote.

  • i.What must they do about the MIT library?
  • ii.What does the GPL ask of them, now that they will share the app?
  • iii.What must they do about the photos and the AI’s function?
Answer
  • i.Keep the library’s copyright line and license with it, and list it, with its version and license (in a file such as requirements.txt and the README). A permissive license asks little more.
  • ii.The GPL is copyleft: whoever shares a program built from it must share that program’s source code too, under the same license. Since they will share the source anyway, they can use it, but their app must then be licensed under the GPL.
  • iii.Photos: someone else’s photos may not be used unless their license allows it: find the license, credit the photographer and source, or ask permission, or take their own photos. AI function: test it with normal, boundary and erroneous data and review every line like a teammate’s code, then record that an AI wrote the first draft (in the commit message and the README).
Marking scheme
  • i. Keep its copyright and license, and list it with version and license.
  • ii. Share the app’s source under the same license (GPL).
  • iii. Photos only with a license or permission, credited; the AI’s function tested and reviewed, and its use recorded.
5.
[3]

An airline that flies to Lukla keeps its seat bookings in five copies that vote: two in Kathmandu, one each in Pokhara, Biratnagar and Nepalgunj. A booking counts only when a majority of the copies has it.

  • i.How many copies must have a booking for it to count?
  • ii.A network partition cuts Biratnagar and Nepalgunj off from the other three. What happens to bookings on each side, and why is that right for seats?
  • iii.Why does the airline keep five copies, and not four?
Answer
  • i.A majority of five: 3 copies.
  • ii.Kathmandu (two copies) and Pokhara are 3, a majority, so they go on booking. Biratnagar and Nepalgunj are only 2, so they refuse (“please try again later”) until the link is back, then catch up. That is right for seats: if both sides sold seats, the same seat could be sold twice, and two passengers would turn up for it. The airline chooses consistency over availability.
  • iii.With four copies, a break can split them two and two: neither side is a majority, so nobody can book anywhere. With an odd number, one side of any split always keeps a majority; five copies can lose any two and still carry on.
Marking scheme
  • i. 3 copies.
  • ii. The side with 3 copies books; the side with 2 refuses until the link is back, so no seat is sold twice.
  • iii. Four can split 2–2 with no majority on either side; an odd number always leaves a majority on one side.
6.
[3]

The agriculture office in Rupandehi measured the paddy harvest on 40 farms chosen at random: on average 4.1 tonnes per hectare. It then resampled the 40 farms 1,000 times, and the middle 95% of the resampled averages ran from 3.7 to 4.5 tonnes per hectare. (Made-up numbers of a realistic size.)

  • i.What does one resample do?
  • ii.Write the result as an honest one-sentence report.
  • iii.Suppose the 40 farms had all been beside the highway. Would the interval still tell the office how far 4.1 could be from the district’s true average? Give a reason.
Answer
  • i.It picks 40 farms’ harvests from the sample, with replacement, so some farms come in twice and some not at all, and works out their average: a stand-in for another random sample of 40.
  • ii.“The farms’ paddy harvest was about 4.1 tonnes per hectare (95% interval 3.7 to 4.5), from 40 farms chosen at random.”
  • iii.No. The interval measures only chance: how much another random sample of the same size would vary. Farms beside the highway may differ from the rest (better roads to fertilizer and markets, irrigation along the road), and that bias misses in one direction; the interval would be just as narrow, round a possibly wrong answer.
Marking scheme
  • i. 40 picks from the sample with replacement, and their average.
  • ii. The estimate with its 95% interval and how the sample was chosen.
  • iii. No: the interval measures chance only, not the bias of farms beside the highway.
7.
[3]

Rojina, an agriculture student, tested a new organic spray against aphids on mustard in Lalitpur. Of six plots, three were chosen at random and sprayed. The sprayed plots lost 18% of their yield to aphids on average, the others 25%. A shuffle test gave p = 0.4. She writes: “The spray does not work.” Judge her conclusion, and say what she should do next.

Answer

The conclusion goes beyond the evidence. p = 0.4 means that with only six plots, chance alone gives a difference this big about four times in ten, so the test can’t tell whether the spray made the difference. That is not the same as showing no difference: a test this small has very low power, and can find only a very big difference. The 7 points may be real; the test was simply too small to tell. “We found no difference” from a small test is not evidence that there is none.
She should plan a bigger trial in advance: decide the measure (yield lost) and how many plots are needed to find a difference of the size that would matter to farmers (aiming for about 80% power), choose the sprayed plots at random again, and report the difference with its 95% interval, not just “works” or “doesn’t”.

Marking scheme
  • p = 0.4 shows chance alone could easily give this difference with six plots: the test can’t tell.
  • A small test has low power: no significant difference is not evidence of no effect.
  • A fair next step: a bigger, randomized trial planned in advance (measure and size, for enough power), reported with the size of the difference and its interval.
8.
[3]

For a computer club in Palpa, Bibek makes a tiny RSA key pair with p = 7 and q = 13.

  • i.Work out n and φ(n).
  • ii.He chooses e = 5. Find the private power d by trying 1 + φ(n), 1 + 2φ(n), … until one can be divided exactly by 5.
  • iii.Why could he not have chosen e = 3?
Answer
  • i.n = 7 × 13 = 91; φ(n) = (7 − 1)(13 − 1) = 6 × 12 = 72.
  • ii.1 + 72 = 73: not a multiple of 5. 1 + 2 × 72 = 145 = 5 × 29. So d = 29 (check: 5 × 29 = 145 = 2 × 72 + 1).
  • iii.3 shares a factor, 3, with φ(n) = 72. Then 3 × d is always a multiple of 3, but 1 + a multiple of 72 never is (it leaves 1 when divided by 3), so no private power exists: e must share no factor with φ(n).
Marking scheme
  • i. n = 91 and φ(n) = 72.
  • ii. d = 29, found from 145 = 5 × 29.
  • iii. 3 shares a factor with 72, so no d with 3 × d = 1 + a multiple of 72 exists.
9.
[3]

The European Union and India govern AI in two different ways. Describe each in a sentence, and name one idea Nepal could borrow from each.

Answer
  • The European Union: one law, by risk. The AI Act covers all AI: some uses are banned, and “high-risk” uses (jobs, education, credit, public services) must be tested, use good data, keep records and have human oversight, checked by regulators with heavy fines.
  • India: guidelines and the laws it already has. Principles that companies follow on their own, its existing laws on data, consumers, copyright and crime applied to AI, and a new law only where a gap is found.
  • For Nepal: from the EU, the idea of levels of risk, with strict rules only for uses that decide about people’s lives; from India, a channel for reporting harm from AI, records of AI incidents, or labels on AI-made content, without a heavy new office.
Marking scheme
  • The EU: one law for all AI, with rules by level of risk (bans, strict duties for high-risk uses).
  • India: voluntary guidelines and existing laws, with new law only for gaps.
  • One idea from each for Nepal (levels of risk; a complaint channel, incident records or labels).

Section C

2 × 8 = 16

Write long answers to the following questions.

10.
[8]

A farmer in Marpha, Mustang, takes goods to sell at Jomsom by mule. The mule can carry at most 60 kg. He has one sack of each (made-up weights and prices):

Sack Weight (kg) Price (Rs)
Dried apples 10 8,000
Walnuts 15 9,000
Kidney beans 40 12,000
Buckwheat flour 10 2,000
  • i.A greedy method takes the sacks in order of price per kg, highest first, each one that still fits. Work out the price per kg of each sack, and the load it chooses, with its weight and price. (2)
  • ii.Another greedy method takes the highest-priced sack first, then the next, each one that still fits. Which load does it choose, and what is it worth? (1)
  • iii.Try every packing: list every set of sacks that fits on the mule, with its weight and price, and find the best. (3)
  • iv.A trader with 50 kinds of goods, one sack of each, tries every packing on a computer that checks a billion packings a second. About how long does it take? Name a better method, and the idea behind it. (2)
Answer

i. Price per kg: dried apples 8,000 ÷ 10 = 800; walnuts 9,000 ÷ 15 = 600; kidney beans 12,000 ÷ 40 = 300; flour 2,000 ÷ 10 = 200.
Take dried apples (10 kg), then walnuts (25 kg so far); kidney beans would make 65 kg, too heavy, so skip them; flour fits (35 kg). The load: dried apples, walnuts and flour, 35 kg, Rs 19,000, with 25 kg of the mule’s load unused.

ii. Kidney beans (Rs 12,000, 40 kg), then walnuts (55 kg); dried apples or flour would make 65 kg, too heavy. Kidney beans and walnuts, 55 kg, Rs 21,000.

iii. With four sacks there are 2⁴ = 16 sets. The ones that fit (D dried apples, W walnuts, B beans, F flour):

Sacks kg Rs
none 0 0
D 10 8,000
W 15 9,000
B 40 12,000
F 10 2,000
D + W 25 17,000
D + B 50 20,000
D + F 20 10,000
W + B 55 21,000
W + F 25 11,000
B + F 50 14,000
D + W + F 35 19,000
D + B + F 60 22,000

The other three (D + W + B 65 kg, W + B + F 65 kg, all four 75 kg) are too heavy. The best is dried apples, kidney beans and flour: exactly 60 kg, Rs 22,000, more than either greedy method found. Taking the walnuts, which looked good on both rules, left no room for the beans with the dried apples.

iv. Each kind is either taken or not, so there are 2⁵⁰ packings, about 1.1 × 10¹⁵ (a thousand million million). At a billion a second that is about 1.1 million seconds, about 13 days, and each kind more doubles it. Dynamic programming does far better (the book’s snack-packing problem in “Which family?”): the best load from the first k kinds with w kg of room left is the better of “take kind k” and “leave it”, each built from the best for the first k − 1 kinds; the same smaller questions come up again and again (they overlap), so each is worked out once and remembered. With whole kilograms, that is only 50 × 61 small questions.

Marking scheme
  • i. The prices per kg (800, 600, 300, 200) and the order they give.
  • i. Dried apples, walnuts and flour: 35 kg, Rs 19,000 (beans skipped as too heavy).
  • ii. Kidney beans and walnuts: 55 kg, Rs 21,000.
  • iii. The sets tried systematically (16 in all), with the too-heavy ones ruled out.
  • iii. Weights and prices of the sets that fit worked out correctly.
  • iii. The best: dried apples, beans and flour, 60 kg, Rs 22,000.
  • iv. 2⁵⁰ ≈ 1.1 × 10¹⁵ packings, about 13 days, doubling with each kind more.
  • iv. Dynamic programming: take it or leave it, overlapping smaller problems each solved once (or: a heuristic, measured against the best on small cases).
11.
[8]

A company offers tomato farmers an app: photograph a leaf, and it says either “Late blight: spray fungicide today” or “Healthy”. The agriculture knowledge center in Palpa is asked to recommend it to 3,000 tomato farmers. Its plant protection officer tested the app on 500 leaf photos taken by farmers, and checked each one himself: 100 leaves really had late blight. (The app and its results are made up.)

App says blight App says healthy
Really blight 85 15
Really healthy 60 340

The same photos, by the phone that took them:

Blight, said blight Blight, said healthy Healthy, said blight Healthy, said healthy
Newer phones (300 photos) 57 3 24 216
Older, cheaper phones (200 photos) 28 12 36 124
  • i.Work out the app’s precision, recall and F1 over all 500 photos. (3)
  • ii.Work out its precision and recall for each kind of phone. What do they mean for the farmers who own older phones? (2)
  • iii.Should the center recommend the app? Argue your case from the cost of each kind of mistake, and say what must be done first, or how it must be used. (3)
Answer

i. TP = 85, FN = 15, FP = 60, TN = 340.

  • Precision = 85 ÷ (85 + 60) = 85 ÷ 145 ≈ 59%: of the leaves it calls blighted, only about 3 in 5 are.
  • Recall = 85 ÷ (85 + 15) = 85 ÷ 100 = 85%: it finds 85 of every 100 blighted leaves.
  • F1 = 2 × 0.586 × 0.85 ÷ (0.586 + 0.85) = 0.997 ÷ 1.436 ≈ 69%.

ii. Newer phones: precision 57 ÷ (57 + 24) = 57 ÷ 81 ≈ 70%, recall 57 ÷ 60 = 95%. Older phones: precision 28 ÷ (28 + 36) = 28 ÷ 64 ≈ 44%, recall 28 ÷ 40 = 70%. Farmers with older phones get worse answers both ways: it misses 3 in 10 of their blighted plants, and more than half of its “spray today” warnings to them are false. They are often the poorer farmers, who can least afford either a lost crop or a wasted spray. The overall numbers hid this.

iii. One well-argued case (other conclusions earn marks with good reasons): Not as it is; perhaps later, as advice checked by people.

  • The cost of each mistake. A miss is costly: late blight can spread through a field in days, and a missed leaf can cost the harvest, so recall matters a great deal. But a false alarm is not free here, as it might be for a warning: it tells a farmer to spray fungicide, which costs money, exposes the person spraying (often without protective clothing), leaves residues on tomatoes sold in the market, and harms soil and water. With precision 59%, and 44% on older phones, many farmers would spray for nothing, and the app tells them to act, not just to look.
  • First: collect and label many more photos from older phones, retrain, and test again for each kind of phone (and each season and variety) before any recommendation; a lower threshold for older phones would catch more blight but bring still more false alarms, so it isn’t the fix.
  • How to use it: the app should say “may be blight: check these signs, or send the photo to the officer” rather than “spray today”, with the officer or a trained farmer leader confirming before spraying (human review); farmers told what it was tested on and how often it is wrong; results checked each season by phone type after release (drift), with a way to report mistakes and a way to stop recommending it. A model card from the company, with these numbers, should come first.
Marking scheme
  • i. Precision about 59%.
  • i. Recall 85%.
  • i. F1 about 69%.
  • ii. Newer phones: precision about 70%, recall 95%; older phones: precision about 44%, recall 70%.
  • ii. Owners of older phones get more misses and more false alarms, and the overall numbers hid it.
  • iii. A clear conclusion argued from the cost of each mistake: a missed blight costs the crop; a false alarm costs a needless spray (money, health, residues, environment).
  • iii. The gap dealt with at its cause: more photos from older phones, retraining, testing by group again (a threshold change is not the fix).
  • iii. Safeguards for its use: advice rather than an order, a person confirms before spraying, farmers told its limits, checks after release by group, a way to stop.

Practical exam

20 marks

Marked by the school. Work alone at a computer, with no internet and no AI, in front of the teacher, for 1 hour 30 minutes. Write and run each program, show it working, and answer the teacher’s questions. On this page, press Edit on a program to type in it, and Run to run it.

P1.
[7]

The ambulance’s route. An ambulance service in Rupandehi and Palpa keeps the map below (made up: the positions and road lengths are invented, not the real ones). POS gives each place’s position in km east and north; the straight line between two positions is never longer than the road. The road to Jhumsa is closed by a landslide. Write find_route(start, goal, estimate), the A* search, which returns the length of the shortest route, the route, and the number of places visited, or None if there is no route. Write automatic tests for it, run by a small test runner: at least the start being the goal, a place that can’t be reached, a route you worked out by hand, and a test that A* with the straight-line estimate gives the same length as Dijkstra’s algorithm between every two places. Then print the route from Ridi to Bhairahawa with both, and the places each visited.

import heapqimport math POS = {"Bhairahawa": (0, 0), "Lumbini": (-20, -3), "Butwal": (8, 22), "Sunwal": (-6, 19),       "Devdaha": (20, 20), "Tansen": (6, 40), "Rampur": (26, 52), "Ridi": (-14, 55),       "Bartung": (-4, 46), "Jhumsa": (14, 60)}ROADS = [("Bhairahawa", "Lumbini", 22), ("Bhairahawa", "Butwal", 25), ("Bhairahawa", "Sunwal", 24),         ("Lumbini", "Sunwal", 30), ("Butwal", "Sunwal", 16), ("Butwal", "Devdaha", 14),         ("Butwal", "Tansen", 28), ("Sunwal", "Bartung", 38), ("Tansen", "Bartung", 15),         ("Tansen", "Rampur", 35), ("Bartung", "Ridi", 20), ("Tansen", "Ridi", 34),         ("Devdaha", "Rampur", 45)] graph = {place: {} for place in POS}for a, b, km in ROADS:    graph[a][b] = km    graph[b][a] = km  def find_route(start, goal, estimate):    return None # Your estimates and tests here
Answer
import heapqimport math POS = {"Bhairahawa": (0, 0), "Lumbini": (-20, -3), "Butwal": (8, 22), "Sunwal": (-6, 19),       "Devdaha": (20, 20), "Tansen": (6, 40), "Rampur": (26, 52), "Ridi": (-14, 55),       "Bartung": (-4, 46), "Jhumsa": (14, 60)}ROADS = [("Bhairahawa", "Lumbini", 22), ("Bhairahawa", "Butwal", 25), ("Bhairahawa", "Sunwal", 24),         ("Lumbini", "Sunwal", 30), ("Butwal", "Sunwal", 16), ("Butwal", "Devdaha", 14),         ("Butwal", "Tansen", 28), ("Sunwal", "Bartung", 38), ("Tansen", "Bartung", 15),         ("Tansen", "Rampur", 35), ("Bartung", "Ridi", 20), ("Tansen", "Ridi", 34),         ("Devdaha", "Rampur", 45)] graph = {place: {} for place in POS}for a, b, km in ROADS:    graph[a][b] = km    graph[b][a] = km  def find_route(start, goal, estimate):    """(km, route, places visited) for the shortest route, or None if there is none."""    best = {start: 0}    came_from = {start: None}    done = set()    waiting = [(estimate(start, goal), 0, start)]    while waiting:        guess, km, place = heapq.heappop(waiting)        if place in done:            continue        done.add(place)        if place == goal:            route = []            while place is not None:                route.append(place)                place = came_from[place]            return km, route[::-1], len(done)        for nxt, road_km in graph[place].items():            new_km = km + road_km            if nxt not in best or new_km < best[nxt]:                best[nxt] = new_km                came_from[nxt] = place                heapq.heappush(waiting, (new_km + estimate(nxt, goal), new_km, nxt))    return None  def no_estimate(place, goal):    return 0  def straight_line(place, goal):    return math.dist(POS[place], POS[goal])  def test_start_is_goal():    assert find_route("Tansen", "Tansen", straight_line) == (0, ["Tansen"], 1)  def test_no_road():    assert find_route("Butwal", "Jhumsa", straight_line) is None  def test_worked_by_hand():    # by hand: through Sunwal and Butwal is 30 + 16 + 28 = 74; through Bhairahawa 75    km, route, visited = find_route("Lumbini", "Tansen", straight_line)    assert km == 74 and route == ["Lumbini", "Sunwal", "Butwal", "Tansen"], f"{km} {route}"  def test_same_as_dijkstra_everywhere():    for a in POS:        for b in POS:            fast = find_route(a, b, straight_line)            sure = find_route(a, b, no_estimate)            if sure is None:                assert fast is None, f"{a} to {b}"            else:                assert fast[0] == sure[0], f"{a} to {b}: {fast[0]} km, not {sure[0]}"  passed = failed = 0for name, test in list(globals().items()):    if name.startswith("test_"):        try:            test()            print("PASS", name)            passed += 1        except AssertionError as problem:            print("FAIL", name, problem)            failed += 1print(passed, "passed,", failed, "failed") for name, estimate in [("Dijkstra", no_estimate), ("A*", straight_line)]:    km, route, visited = find_route("Ridi", "Bhairahawa", estimate)    print(f"{name}: {km} km, {visited} places visited: {' → '.join(route)}")

Output

PASS test_start_is_goalPASS test_no_roadPASS test_worked_by_handPASS test_same_as_dijkstra_everywhere4 passed, 0 failedDijkstra: 82 km, 8 places visited: Ridi → Bartung → Sunwal → BhairahawaA*: 82 km, 5 places visited: Ridi → Bartung → Sunwal → Bhairahawa

Both find the same 82 km route; A* visits 5 places instead of 8, because places away from Bhairahawa get bigger totals and wait.

Marking scheme
  • A priority queue (heap) ordered by the distance so far plus the estimate, with each place taken once.
  • Stops when the goal is taken from the heap (not when first reached), and reads the route back from the came-from notes.
  • Returns None when the heap empties without reaching the goal.
  • An estimate passed in as a function: the straight line (math.dist), and 0 for Dijkstra’s algorithm.
  • Tests as test_ functions with assert: the start as the goal, the closed road, a route worked out by hand.
  • A test that compares A* with Dijkstra’s algorithm between every two places; a runner reports passes and failures, and all pass.
  • Prints the 82 km route from Ridi with both, and the places visited (8 and 5).
P2.
[4]

The heavy-rain warning. A weather station in Sindhupalchok sends the district’s disaster office its rain gauge readings. The requirement says: “The warning lists every hour in which 30 mm of rain or more fell, in the order they came. With no heavy hour, or no readings, it lists nothing.” Run the tests, find why some fail, fix the code, and run them again. (The readings are made up.)

def heavy_hours(readings):    """The hours with 30 mm of rain or more, in the order they came."""    heavy = []    for hour, mm in readings:        if mm >= 30:            heavy.append(hour)        return heavy  def test_dry_night():    assert heavy_hours([("01:00", 4), ("02:00", 0), ("03:00", 12)]) == []  def test_one_burst():    assert heavy_hours([("05:00", 41), ("06:00", 9)]) == ["05:00"]  def test_storm():    night = [("21:00", 12), ("22:00", 35), ("23:00", 48), ("00:00", 30), ("01:00", 6)]    assert heavy_hours(night) == ["22:00", "23:00", "00:00"], f"gave {heavy_hours(night)}"  def test_no_readings():    assert heavy_hours([]) == [], f"gave {heavy_hours([])}"  passed = failed = 0for name, test in list(globals().items()):    if name.startswith("test_"):        try:            test()            print("PASS", name)            passed += 1        except AssertionError as problem:            print("FAIL", name, problem)            failed += 1print(passed, "passed,", failed, "failed")
Answer

The first run:

Output

PASS test_dry_nightPASS test_one_burstFAIL test_storm gave []FAIL test_no_readings gave None2 passed, 2 failed

Line 7, return heavy, is indented inside the loop, so the function returns after looking at the first reading only. The storm’s first hour had 12 mm, so it returned []; with no readings the loop never runs, the return is never reached, and the function gives None. The two tests that passed happened to need only the first reading. The tests match the requirement, so the code is wrong: move return heavy out of the loop, level with for:

def heavy_hours(readings):    """The hours with 30 mm of rain or more, in the order they came."""    heavy = []    for hour, mm in readings:        if mm >= 30:            heavy.append(hour)    return heavy  def test_dry_night():    assert heavy_hours([("01:00", 4), ("02:00", 0), ("03:00", 12)]) == []  def test_one_burst():    assert heavy_hours([("05:00", 41), ("06:00", 9)]) == ["05:00"]  def test_storm():    night = [("21:00", 12), ("22:00", 35), ("23:00", 48), ("00:00", 30), ("01:00", 6)]    assert heavy_hours(night) == ["22:00", "23:00", "00:00"], f"gave {heavy_hours(night)}"  def test_no_readings():    assert heavy_hours([]) == [], f"gave {heavy_hours([])}"  passed = failed = 0for name, test in list(globals().items()):    if name.startswith("test_"):        try:            test()            print("PASS", name)            passed += 1        except AssertionError as problem:            print("FAIL", name, problem)            failed += 1print(passed, "passed,", failed, "failed")

Output

PASS test_dry_nightPASS test_one_burstPASS test_stormPASS test_no_readings4 passed, 0 failed
Marking scheme
  • Runs the tests and names the failing ones: the storm gives [], no readings gives None.
  • Finds the cause: return heavy on line 7 is inside the loop, so only the first reading is looked at (and nothing is returned for an empty list).
  • Fixes the code (moves the return out of the loop), not the tests, which match the requirement.
  • Runs all the tests again, and all pass.
P3.
[4]

Rain tomorrow? A cooperative in Syangja tried a model that forecasts whether it will rain the next day, so that farmers know when to dry their harvest in the sun. It was tested on 60 days from across a year: truth has R where it really rained the next day, said has R where the model forecast rain, and season is M for a monsoon day and D for a dry-season day. (The days and results are made up.)

Write a program that counts the four outcomes and works out the model’s precision and recall, for all 60 days and for each season apart. Then write one limit of the model, in a sentence for the farmers, with the numbers that show it.

truth  = "-R---RR--RR--R---R-R-RRR--R---RR-RR--RR--R-RRR-R-RR-------R-"said   = "-----RR--RR-R------RR-RR--R---RR-RR-RR---RRRRR-R-R------R-R-"season = "DDDDDMMDMDMDMDDDDMDMMMMMMDMDMDMMMMMDDMDMDDMMMMDMMMDDDDDDMDMD" # Your program here
Answer
truth  = "-R---RR--RR--R---R-R-RRR--R---RR-RR--RR--R-RRR-R-RR-------R-"said   = "-----RR--RR-R------RR-RR--R---RR-RR-RR---RRRRR-R-R------R-R-"season = "DDDDDMMDMDMDMDDDDMDMMMMMMDMDMDMMMMMDDMDMDDMMMMDMMMDDDDDDMDMD"  def measure(name, keep):    """Count the outcomes on the days whose season is in keep."""    tp = fp = fn = tn = 0    for real, forecast, s in zip(truth, said, season):        if s not in keep:            continue        if real == "R" and forecast == "R":            tp += 1        elif real == "R":            fn += 1        elif forecast == "R":            fp += 1        else:            tn += 1    print(f"{name:8} TP {tp:2}  FP {fp}  FN {fn}  TN {tn:2}  precision {tp / (tp + fp):.0%}  recall {tp / (tp + fn):.0%}")  measure("All", "MD")measure("Monsoon", "M")measure("Dry", "D")

Output

All      TP 20  FP 5  FN 6  TN 29  precision 80%  recall 77%Monsoon  TP 18  FP 4  FN 2  TN  6  precision 82%  recall 90%Dry      TP  2  FP 1  FN 4  TN 23  precision 67%  recall 33%

One limit: “In the dry season the model forecast only 2 of the 6 rainy days, so on most days when rain came it had said the harvest was safe to dry; and with only 6 dry-season rainy days in the test, even that number is uncertain.” The dry season is when the harvest is dried, so this is where the model matters most.

Marking scheme
  • A loop over the three strings together that counts true positives, false positives, false negatives and true negatives.
  • Precision and recall from the counts: 80% and 77% for all 60 days.
  • The seasons measured apart (recall 90% in the monsoon, 33% in the dry season).
  • One limit stated in a sentence with its numbers (the dry-season recall, or only 6 dry-season rainy days).
P4.
[5]

Viva. The teacher asks each student about their own work, for example:

  • i.Why must the estimate in find_route never be more than the real distance by road? What did your test against Dijkstra’s algorithm check? (2)
  • ii.In the rain warning, why did two tests pass, though the code was wrong? (1)
  • iii.The rain model is right on 49 of the 60 days, 82%. Why is that not enough to tell a farmer it is safe to dry paddy in the dry season? What would you do? (2)
Answer
  • i.A* stops when the goal is taken from the heap. If an estimate were too high, a place on the real shortest route could look worse than it is and wait, while the goal was reached and taken along a longer route. The straight line is the shortest possible path between two places, so it is never more than the road. The test ran A* and Dijkstra’s algorithm (estimate 0, sure to be right) between every two places and checked that the lengths were the same: evidence that the estimate never misled the search on this map.
  • ii.The return inside the loop looks only at the first reading. In the two tests that passed, the first reading alone gave the right answer (a dry first hour, or a heavy first hour and no other), so the bug didn’t show.
  • iii.Most days in the dry season are dry, so a model that always said “no rain” would still be right on most of them: accuracy is mostly about the common kind. What matters to a farmer is the rainy days, and in the dry season it missed 4 of 6. I would test it on many more dry-season days, with rain, retrain it with more of them, and measure its recall in each season apart before anyone relies on it.
Marking scheme
  • i. An estimate never too high keeps a place on the shortest route from being passed over, so the first time the goal is taken its route is the shortest.
  • i. The test compared A* with Dijkstra’s algorithm between every two places: the same lengths.
  • ii. Those tests needed only the first reading, which was the only one the buggy loop looked at.
  • iii. Rain is rare in the dry season, so accuracy hides the missed rainy days (4 of 6).
  • iii. More dry-season rainy days to train and test on, measured season by season.

Your score

0 / 50 theory

0 / 20 practical

24 questions not marked yet

Section A–Full marks 10
Section B–Full marks 24
Section C–Full marks 16
Practical exam–Full marks 20

Marks lost, by unit

Mark a question to see your score here.

Score 0 of 0 0 of 24 marked Time left: 2:00:00