Reverse-engineering the "Irish logarithm"
Source: MJD’s blog post on “The Irish logarithm” (cites Wikipedia: Irish logarithm).
Apparently, Percy Ludgate (1883–1922) was an Irish amateur (was “Boy Copyist” in 1901, and “Commercial Clerk (Corn Merchant)” in 1911, later accountant), who in 1909—unaware of the work of Charles Babbage (1791–1871) and his analytical engine—designed a similar general-purpose (Turing-complete) computing machine.
As part of this machine, he needed to implement multiplication of one-digit numbers, and it appears that a two-dimensional lookup in a
- First we look up
and in a table of logarithms, to get their respective logarithms and , - We add these logarithms, getting the value
. - Then we look up this value in a table of anti-logarithms (for logarithms, we can reuse the same table, as it is sorted…) to get the value of the product
.
Ludgate could do something similar (he had mechanisms for adding and looking up a one-dimensional table), except not use logarithms exactly, as they would require fractional values rather than integers (and integer arithmetic is simpler). To be sure, he could have integer values if he would only need to multiply, say, powers of
1 2 4 8 16 32 64
0 1 2 3 4 5 6
with which to multiply, say,
Or can it?
Could we make it work, for some appropriate values in the “logarithm” and “anti-logarithm” tables (even if they are not true logarithms)? Let’s say the “logarithm” table is some
What can we say about such an
- In the first place, as multiplying by
does not change the value—we need —we are forced to set . - Let’s say we set
for some . This forces some values: and , and (conceptually, though we don’t need it in the table) and and . - Next if
, this forces not only and and , but also things like and . Basically this determines the values of at all values that are the product of two one-digit numbers, and have only and as prime factors. - By similar reasoning, picking the values
and completely determine the values of on all integers of interest (one-digit numbers, and products of any two of them).
Can we pick arbitrary values for
In particular, suppose we pick the value
1 2 4 8 16 32 64
0 1 2 3 4 5 6
In Python:
f = {1: 0}
g = {0: 1}
for i in range(1, 6):
f[2 ** i] = i
g[i] = 2 ** i
Then after this, picking the value of
1 2 3 4 6 8 9 12 16 18 24 27 32 36 48 54 64 72 81
0 1 7 2 8 3 14 9 4 15 10 21 5 16 11 22 6 17 28
which will work for multiplying together any two one-digit numbers other than
In Python (append to the earlier code):
f[3] = 7
for a in [3, 6, 9]:
for b in [1, 2, 3, 4, 6, 8, 9]:
f[a * b] = f[a] + f[b]
g[f[a] + f[b]] = a * b
More interesting is the choice of
In Python, for verifying:
x = 0
while True:
x += 1
if all(v not in f.values() for v in [x, x+1, x+7, x+2, 2*x, x+8, x+3, x+14, 2*x+1]):
print(x)
break
(This exercise reminds me of the “packed trie” idea, mentioned in F. M. Liang’s thesis—see page 15—and used by Knuth in TeX and in his program for Bentley’s word-counting problem… more on that some other time, but it’s a very similar idea of having arrays with “holes”, which can be “moved around” or “shifted” with the constraint that their values only fall in the “holes” of other arrays, and never collide.)
Once
f[5] = 23
for a in [5]:
for b in [1, 2, 3, 4, 5, 6, 8, 9]:
f[a * b] = f[a] + f[b]
g[f[a] + f[b]] = a * b
print(f'{a * b} -> {f[a*b]}')
i.e. the following values are added:
5 10 15 20 25 30 40 45
23 24 30 25 46 31 26 37
Finally,
x = 0
while True:
x += 1
if all(x + f[b] not in f.values() for b in [1, 2, 3, 4, 5, 6, 8, 9]) and 2*x not in f.values():
print(x)
break
This gives
f[7] = 33
for a in [7]:
for b in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
f[a * b] = f[a] + f[b]
g[f[a] + f[b]] = a * b
print(f'{a * b} -> {f[a*b]}')
i.e. the following values are added:
7 14 21 28 35 42 49 56 63
33 34 40 35 56 41 66 36 47
There’s one final wrinkle: what if one of the two numbers we want to multiply is
x = 0
while True:
x += 1
if all(x + f[b] not in f.values() for b in range(1, 10)) and 2*x not in f.values():
print(x)
break
turns out to be the nice round number
f[0] = 50
for a in [0]:
for b in range(10):
# f[a * b] = f[a] + f[b]
g[f[a] + f[b]] = a * b
print(f'{a * b} -> {f[a] + f[b]}')
or rather, tells us that the inverse table should contain
So with all this, we have the following “forward” table:
for x in range(10):
print(x, f[x])
Output:
0 50
1 0
2 1
3 7
4 2
5 23
6 8
7 33
8 3
9 14
and the following “inverse” table:
for x in sorted(g.keys()):
print(x, g[x])
Output:
0 1
1 2
2 4
3 8
4 16
5 32
7 3
8 6
9 12
10 24
11 48
14 9
15 18
16 36
17 72
21 27
22 54
23 5
24 10
25 20
26 40
28 81
30 15
31 30
33 7
34 14
35 28
36 56
37 45
40 21
41 42
46 25
47 63
50 0
51 0
52 0
53 0
56 35
57 0
58 0
64 0
66 49
73 0
83 0
100 0
These are the two tables that were computed by Ludgate, and used as one-dimensional arrays (and are reproduced on Wikipedia).
See also “Percy Ludgate’s Logarithmic Indexes” by Brian Coghlan (and earlier 2019 slides).