codexlegendre-base-three-digits-and-carry.py
legendre-base-three-digits-and-carry.py
"""Original digit/carry illustration; Python 3.14.4 / Matplotlib 3.10.7.
Run from _media; writes the matching basename.png to the working directory.
"""
from math import comb, factorial
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Rectangle
def digits(n, base):
result = []
while n:
n, digit = divmod(n, base)
result.append(digit)
return result or [0]
def valuation(n, p):
exponent = 0
while n % p == 0:
n //= p
exponent += 1
return exponent
def carry_count(a, b, p):
carry = total = 0
while a or b or carry:
a, da = divmod(a, p)
b, db = divmod(b, p)
carry = (da + db + carry) // p
total += carry
return total
def check():
assert digits(50, 3) == [2, 1, 2, 1]
assert sum(digits(50, 3)) == 6
assert digits(17, 3) == [2, 2, 1]
assert digits(33, 3) == [0, 2, 0, 1]
assert carry_count(17, 33, 3) == 1
number = comb(50, 17)
assert number % 3 == 0 and number % 9 != 0
assert [valuation(factorial(n), 3) for n in (50, 17, 33)] == [22, 6, 15]
for p in (2, 3, 5, 7):
for n in range(51):
assert valuation(factorial(n), p) == (n - sum(digits(n, p))) // (p - 1)
for k in range(n + 1):
count = carry_count(k, n - k, p)
deficit = sum(digits(k, p)) + sum(digits(n - k, p)) - sum(digits(n, p))
assert deficit == (p - 1) * count
assert valuation(comb(n, k), p) == count
def main():
check()
plt.rcParams.update({'font.size': 12, 'figure.facecolor': 'white',
'savefig.facecolor': 'white'})
fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.1))
fig.subplots_adjust(left=0.02, right=0.98, bottom=0.12, top=0.82, wspace=0.12)
fig.suptitle('Base-three digits connect factorial factors and carries', fontsize=18, y=0.96)
left, right = axes
for ax in axes:
ax.axis('off')
ax.set(xlim=(-1.85, 3.7), ylim=(-1.5, 4.1))
left.set_title(r'$50=(1212)_3$', fontsize=17, pad=13)
left.text(-0.6, 3.6, '27', ha='center', color='#52616a')
for x, label in zip((0.4, 1.4, 2.4), ('9', '3', '1')):
left.text(x, 3.6, label, ha='center', color='#52616a')
for x, d in zip((-0.6, 0.4, 1.4, 2.4), reversed(digits(50, 3))):
left.add_patch(Rectangle((x - 0.4, 2.4), 0.8, 0.85,
facecolor='#e5f0f7', edgecolor='white'))
left.text(x, 2.82, str(d), ha='center', va='center', fontsize=24, color='#246a9c')
left.text(0.9, 1.86, r'$d_3(50)=1+2+1+2=6$', ha='center', fontsize=15)
left.text(0.9, 0.85, r'$v_3(50!)=\frac{50-6}{3-1}=22$', ha='center', fontsize=21)
left.text(0.9, -0.25, 'Digit contributions to the valuation:', ha='center', fontsize=11)
left.text(0.9, -0.85, r'$13+8+1+0=22$', ha='center', fontsize=16)
right.set_title(r'$17+33=50$', fontsize=17, pad=13)
xs = (-0.6, 0.4, 1.4, 2.4)
for x, label in zip(xs, ('27', '9', '3', '1')):
right.text(x, 3.6, label, ha='center', color='#52616a')
for y, value, label in ((2.6, 17, '17'), (1.65, 33, '+ 33'), (0.5, 50, '= 50')):
row = list(reversed(digits(value, 3) + [0] * (4 - len(digits(value, 3)))))
right.text(-1.3, y, label, ha='right', va='center', fontsize=13)
for x, d in zip(xs, row):
right.text(x, y, str(d), ha='center', va='center', fontsize=24,
color='#246a9c' if value == 50 else '#26343d')
right.plot([-0.95, 2.8], [1.05, 1.05], color='#88959e')
right.add_patch(FancyArrowPatch((1.4, 2.98), (0.4, 2.98),
connectionstyle='arc3,rad=0.5',
arrowstyle='-|>', mutation_scale=15,
color='#b86428', lw=2))
right.text(0.4, 2.98, '1', ha='center', va='bottom', color='#b86428', fontsize=14)
right.text(0.9, -0.28, 'One carry: 2 + 2 = 1 + 3', ha='center', fontsize=12, color='#9c4c18')
right.text(0.9, -0.98, r'$v_3\binom{50}{17}=\frac{5+3-6}{2}=1$', ha='center', fontsize=19)
fig.text(0.5, 0.045, 'Each base-three carry lowers the digit sum by 2.', ha='center', fontsize=13)
fig.savefig(Path(__file__).with_suffix('.png').name, dpi=100, transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()