Time: about 20 minutes. Read page.md first, at least as far as the worked
example for B2.
Two routes run through this activity: table-based verification and Python with exact rationals. They cover the same ground and end at the same checkpoints. Pick whichever suits how you work. If you want to do both, do the paper working first — the code route is more useful as a check on your own arithmetic than as a substitute for it.
Keep your working. You will refer back to it in Unit 5, and it belongs in your evidence pack for Unit 8.
Task 1 — Work out B4 by hand (about 8 minutes)
This is the central task of the unit. Do not look it up, do not compute it with a library, and do not ask an AI system. The value of this exercise is entirely in having produced the number yourself, because in the next unit you will need a reference that did not come from a machine.
The recurrence, again:
sum_{j=0}^{n} C(n+1, j) * B_j = 0
Take n = 4.
Step 1. Write out the identity with all five terms, from j = 0 to j = 4.
Every binomial coefficient uses n + 1 = 5 on top.
C(5,0)·B0 + C(5,1)·B1 + C(5,2)·B2 + C(5,3)·B3 + C(5,4)·B4 = 0
Step 2. Fill in the five binomial coefficients. Row 5 of Pascal's triangle is 1, 5, 10, 10, 5, 1 — you need the first five of those. Write them into your copy of the identity.
Step 3. Substitute the values you already have for B0, B1, B2 and B3.
All four are in the table on the student page, and B2 is the one you watched
being derived. B3 is odd and above 1, so you know what it is without looking.
Step 4. Add up everything that does not contain B4. Keep it as a single
fraction — do not convert to decimals at any point. You should end up with one
small fraction plus a whole-number multiple of B4, and the two together equal
zero.
Step 5. Solve for B4. Watch the sign: the whole sum is zero, so the term
containing B4 must be the negative of everything else.
Write the result as a fully reduced fraction. Keep every line of working — the intermediate steps are what you will check, not just the final number.
If you are taking the Python route
Do steps 1 to 5 on paper first. Then use Python to check your own arithmetic line by line, rather than to produce the answer.
from fractions import Fraction
from math import comb
# Step 2 — the coefficients you wrote down
print([comb(5, j) for j in range(5)])
# Step 3 and 4 — the part of the sum that does not contain B4.
# Fill in the values you substituted; do not import the oracle.
b0 = Fraction(1)
b1 = Fraction(-1, 2)
b2 = Fraction(1, 6)
b3 = Fraction(0)
known = comb(5, 0) * b0 + comb(5, 1) * b1 + comb(5, 2) * b2 + comb(5, 3) * b3
print(known) # compare against your Step 4 fraction
If the printed value matches the fraction you got by hand, your substitution and addition are sound and any remaining error is in Step 5. If it does not match, you have found your slip without being told the answer, which is the more useful outcome.
Do not finish this task by calling reference_bernoulli.bernoulli(4). That
routine is the thing you are learning to be able to check. Using it here would
invert the whole exercise.
If you are taking the table-based route
Your working on paper is the deliverable. Before moving on, do one extra pass: re-derive Step 4 by putting all four known terms over a common denominator of 6, independently of how you did it the first time. Two routes to the same intermediate value is a much stronger check than reading your own arithmetic twice.
Continue after attempting Task 1
Task 2 — Diagnose a disagreement (about 5 minutes)
A colleague sends you a Python file. Its output begins:
B0 = 1
B1 = 1/2
B2 = 1/6
B3 = 0
B4 = -1/30
B5 = 0
B6 = 1/42
B10 = 5/66
B12 = -691/2730
Compare it against the table in page.md, index by index, and write
down:
- Every index where it disagrees with this module's table.
- Whether that pattern of disagreement looks like a mistake in the code, or like something else.
- The one sentence you would add to a specification so that this disagreement could not arise again. Write it as a requirement, not as a complaint — something a reader could test against.
Keep sentence 3. You will compare it with the clause that appears in Unit 6's precise prompt, and with the failure-mode name it corresponds to in Unit 5.
Task 3 — Read an index from Note G (about 4 minutes)
Lovelace labels only the Bernoulli numbers that are not zero. The zeros never receive a label at all.
Fill in this table from that rule alone. Work out the modern index for each of her labels rather than copying the mapping from the student page, then check your answers against it.
| Ada's label | modern index | value |
|---|---|---|
| B1 | ||
| B3 | ||
| B5 | ||
| B7 |
Then answer in one sentence each:
- What is the general rule connecting one of Lovelace's labels to its modern index?
- Note G's worked example computes the number Lovelace calls B7. If a modern
routine were asked to reproduce Note G's result and returned its own
B7instead, what would go wrong, and would the returned value be a genuine Bernoulli number?
Question 2 is the one to sit with. A routine can return real Bernoulli numbers, correctly computed, and still be wrong — because the values are filed under the wrong labels. That is a failure that survives being read carefully, and it is why Unit 5 compares index by index against a reference instead of asking whether output "looks like" Bernoulli numbers.
Task 4 — Optional CS extension (about 5 minutes, outside the core budget)
Clearly optional. Nothing later in the module depends on it.
Write a short function that takes a list of claimed values and reports the first index at which it disagrees with the table on the student page — not every disagreement, just the first, with the index, the claimed value and the expected one.
from fractions import Fraction
TABLE = {
0: Fraction(1),
1: Fraction(-1, 2),
2: Fraction(1, 6),
3: Fraction(0),
# ... complete this from the student page, as fractions, not floats
}
def first_divergence(claimed: dict[int, Fraction]) -> str:
"""Return a description of the first index where claimed differs from TABLE."""
...
Two constraints worth honouring, because they are the same ones the module's own checker works under:
- Compare
FractionagainstFraction. Do not compare againstfloat, and do not use a tolerance. These values are exact and an approximate comparison would hide precisely the errors you are looking for. - Report the first divergence rather than all of them. A single specific index is a reproducible debugging boundary; do not assume it causes every later mismatch.
You have now written, in miniature, the tool that Unit 5 hands you.
Now check yourself
This unit has three checkpoints. Attempt them from your own working, not from the student page.
cp03-b4-by-hand— the value you derived in Task 1cp03-convention— the disagreement you diagnosed in Task 2cp03-ada-index— the mapping you worked out in Task 3
Either route:
python3 selfcheck.py run cp03-b4-by-hand # or use the browser self-check
Run this from the course/lab folder (setup: course/lab/README.md).
To attempt all three at once:
python3 selfcheck.py run --unit 3
The browser route at build/site/selfcheck.html has the same three checkpoints, the
same answers and the same feedback, and needs nothing installed.
A wrong answer on cp03-b4-by-hand will not reveal the correct value. It will
name the specific slip your answer resembles and point you at the step to redo.
Deriving the number is the objective, so being given it would defeat the
exercise. If you are stuck, ask for the hint and go back to Step 2.
Marking guidance — open this once you have done the activity, to check your own work
For learners after attempting the activity, and for instructors reviewing it. Working through this page before attempting Task 1 removes the only part of the module that has to happen before an AI system is involved.
Checkpoint answers are not restated here. Checkpoints are defined once in
course/lab/checkpoints.py, and both self-check routes grade against that
file. This page explains the reasoning; the checkpoints do the marking.
Task 1 — B4 by hand
The full working
With n = 4, the identity has five terms and every binomial coefficient is taken
from row 5:
C(5,0)·B0 + C(5,1)·B1 + C(5,2)·B2 + C(5,3)·B3 + C(5,4)·B4 = 0
The coefficients are 1, 5, 10, 10, 5. Substituting B0 = 1, B1 = -1/2,
B2 = 1/6 and B3 = 0:
(1)(1) + (5)(-1/2) + (10)(1/6) + (10)(0) + (5)(B4) = 0
Term by term:
1 - 5/2 + 5/3 + 0 + 5·B4 = 0
Putting the known part over a denominator of 6:
6/6 - 15/6 + 10/6 = 1/6
So:
1/6 + 5·B4 = 0
5·B4 = -1/6
B4 = -1/30
Which agrees with the table on the student page, and with
course/lab/reference_bernoulli.py.
Where the working usually goes wrong
Four slips account for nearly all incorrect answers, and each of them is worth recognising rather than merely correcting.
- Sign lost at the last step. The learner reaches
1/6 + 5·B4 = 0and reads off1/30instead of-1/30. The sum equals zero, so the term containingB4is the negative of everything else. This is the single most common error, which is worth saying aloud: the same slip, in code, produces a routine that disagrees with the reference at every even index. - Stopping at n = 2. Answering
1/6means the recurrence was run forB2rather thanB4. Usually a misread of whichnto take, rather than an arithmetic error. - Wrong binomial row. Using row 4 (1, 4, 6, 4, 1) instead of row 5. The
identity uses
n + 1on top, notn, and it is easy to substitutenreflexively. - Decimals. Converting to decimals partway through gives
-0.0333…, which is not exact and cannot be compared for equality with-1/30. This is the same error that the module later namesnot-fractionwhen it appears in generated code, and it is worth pointing out that it began here, on paper.
Why this task exists
The value matters less than its provenance. In Unit 4 an AI system will produce a Bernoulli routine, and you will need a value that did not come from that system to judge it by. A number you derived is such a value. A number you looked up in the same conversation is not.
The course site withholds the numerical value in the initial reading and animations, then places the later tasks, this guidance, and the revealing mapping table behind labelled disclosures. This is an instructional boundary, not an access-control claim: a learner can still open the source or disclosure. The direction remains to attempt the derivation before doing so.
Task 2 — Diagnosing the disagreement
1. Where it disagrees. At exactly one index: B1. The file gives +1/2
where this module's table gives -1/2. Every other value shown — B0, B2,
B3, B4, B5, B6, B10, B12 — agrees exactly.
2. What that pattern means. A single-index disagreement, with everything
around it untouched, is not the signature of a bug. Bugs in a recurrence
propagate: an error at B1 would corrupt B2, which would corrupt B4, and so
on down the sequence. Here nothing propagates, which tells you the two programs
are computing different things on purpose.
They are. The file uses the second Bernoulli convention, B1 = +1/2; this
module uses the first, B1 = -1/2. Both are standard, both are published,
and they differ in exactly this one value. The colleague's file is not broken.
The specification that both of you were working to was incomplete.
This is the failure mode the checker names b1-sign. It is worth noticing that
the name describes what the output looks like, not a diagnosis of intent — the
same output would result from a genuine sign error, and the checker cannot tell
those apart. You can, by looking at whether anything else moved.
3. The specification sentence. Any wording is acceptable provided it is testable and unambiguous. A reasonable version:
Use the first Bernoulli convention, in which
B1 = -1/2.
The properties that make it a good requirement:
- It names the convention, so a reader can look it up.
- It states the value, so a reader who has never heard of the convention can still comply.
- It is checkable against a single output, which means a test can be written for it directly.
A version such as "use the correct Bernoulli numbers" fails all three: there is no such thing as the correct one here, and nothing can be tested against it. Compare your sentence with the corresponding clause in Unit 6's precise prompt. If yours is close, you have independently reinvented a piece of requirements engineering, which is what specification work mostly is.
Task 3 — Reading an index from Note G
The completed table, which is the locked mapping used throughout the module:
| Ada's label | modern index | value |
|---|---|---|
| B1 | B2 | 1/6 |
| B3 | B4 | -1/30 |
| B5 | B6 | 1/42 |
| B7 | B8 | -1/30 |
1. The general rule. Lovelace labels only the non-zero Bernoulli numbers.
The modern scheme spends an index on every term, including the odd ones that are
zero, so her label numbers are one smaller: her Bk is the modern B(k+1), for
odd k. Her B7 is the modern B8.
Two ways of getting this wrong are worth naming. Treating her labels as modern
labels gives B7 = 0, since the modern B7 is an odd index above 1. Doubling —
guessing that her B7 is the modern B14 — comes from noticing that only even
indices are involved, but the rule is a shift by one, not a scaling. Writing out
all four pairs makes the pattern visible immediately, which is why the task asks
for the whole table rather than a single row.
2. Note G's B7. Note G's worked example computes Ada's B7, which is the
modern B8. Its value is -1/30.
A modern routine that returned its own B7 would return 0, since every odd
modern index above 1 is exactly zero — a plainly visible mismatch. The more
dangerous version of this error is the one where nothing looks wrong at all: a
routine that renumbers the non-zero terms consecutively returns 1/6 for B1,
-1/30 for B2, 1/42 for B3 — its bernoulli(k) is the modern B(2k). It
skips the zeros as Note G does, though these are not Lovelace's labels either
(hers run B1, B3, B5, B7 = modern B2, B4, B6, B8). Every value it returns is a
genuine Bernoulli number, correctly computed, exactly represented. Every one is
filed under the wrong index.
That combination — real values, wrong labels — is why reading the code carefully
does not catch it. There is nothing in the code to catch. The mismatch only
exists relative to a specification, and it only becomes visible when you compare
against a reference index by index. The module names this failure mode
ada-indexing, and you will meet a working example of it in Unit 5.
A note on the history: Note G's published table contains an error — established
from a facsimile as an inverted division in operation 4 — while its attribution
(who introduced it) remains contested. This unit does not depend on the error at
all, and Unit 7 handles it properly. See
course/historical-notes.md before repeating any account of it.
Task 4 — Optional CS extension
No single correct implementation. A reasonable one:
from fractions import Fraction
def first_divergence(claimed: dict[int, Fraction]) -> str:
for n in sorted(TABLE):
if n not in claimed:
return f"B{n}: no value supplied"
if claimed[n] != TABLE[n]:
return f"B{n}: claimed {claimed[n]}, expected {TABLE[n]}"
return f"no divergence up to B{max(TABLE)}"
Points worth drawing out in review:
- Iterating in index order is what makes "first" meaningful. Iterating over the claimed dictionary in insertion order would report whichever divergence happened to be inserted first, which is not the same thing.
- Exact comparison.
!=between twoFractionobjects is exact. If a learner has writtenabs(a - b) < 1e-9, that tolerance will pass a float-based implementation for small indices and fail it atB30, which is the worst possible behaviour: it hides the bug until the bug is expensive. Comparing aFractionagainst afloatin Python does work, and that is part of the hazard — it will silently succeed forFraction(1, 4)and fail forFraction(1, 6). - A missing value is a finding, not a crash. A routine that has no answer at
an index has failed the specification just as surely as one with a wrong
answer. The module's own checker treats this as its
raisesfailure mode.
If a learner wants to go further, ask them to add the property check that every
odd index above 1 is exactly zero. That check needs no table at all — it tests a
property rather than a value — and it catches the ada-indexing failure mode
immediately, because a routine using Lovelace's numbering has no zeros in it
anywhere.
Checkpoints
Attempt these from your own working:
cp03-b4-by-handcp03-conventioncp03-ada-index
python3 selfcheck.py run cp03-b4-by-hand # or use the browser self-check
Both routes ask the same questions and give the same feedback. Answers and
diagnostics live in course/lab/checkpoints.py.
Your private activity record
Browser storage is not a permanent copy
Progress is kept only in this browser, profile and device. Private browsing, clearing site data, removing the profile, a browser reset, storage eviction or device loss can erase it. Keep important answers and contributions separately.
These notes stay in this browser unless you download a backup or activity log.