codexlegendre-factorial-nested-multiples-ledger.py
legendre-factorial-nested-multiples-ledger.py
"""Original contribution ledger; Python 3.14.4 / Matplotlib 3.10.7.
Run from _media; writes the matching basename.png to the working directory.
"""
from math import factorial
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
def valuation(value, prime):
count = 0
while value % prime == 0:
count += 1
value //= prime
return count
def checked_counts():
multiples = list(range(3, 51, 3))
rows = [[m % power == 0 for m in multiples] for power in (3, 9, 27)]
counts = [sum(row) for row in rows]
assert counts == [16, 5, 1]
columns = [sum(row[i] for row in rows) for i in range(len(multiples))]
assert columns == [valuation(m, 3) for m in multiples]
assert [columns.count(i) for i in (1, 2, 3)] == [11, 4, 1]
exact = factorial(50)
assert exact % (3**22) == 0 and exact % (3**23) != 0
for prime in (2, 3, 5, 7, 11):
for n in range(101):
quotient, total = n, 0
while quotient:
quotient //= prime
total += quotient
assert total == valuation(factorial(n), prime)
return multiples, rows, counts, columns
def main():
multiples, rows, counts, columns = checked_counts()
colors = ['#246a9c', '#169286', '#b86428']
plt.rcParams.update({'font.size': 12, 'figure.facecolor': 'white',
'savefig.facecolor': 'white'})
fig, ax = plt.subplots(figsize=(10.8, 4.8))
fig.subplots_adjust(left=0.025, right=0.99, top=0.81, bottom=0.13)
fig.suptitle('Count every factor of three in 50!', fontsize=19, y=0.96)
fig.text(0.5, 0.86, 'Each filled cell contributes one; nested multiples contribute again.',
ha='center', fontsize=12)
ax.set(xlim=(-4.5, 19.1), ylim=(-1.25, 4.0))
ax.axis('off')
for i, number in enumerate(multiples):
ax.text(i, 3.65, str(number), ha='center', va='center')
ax.text(-1.0, 3.65, 'factor m', ha='right', fontsize=11)
ax.text(17.3, 3.65, 'row count', ha='center', fontsize=11)
labels = [r'$3\mid m$', r'$9\mid m$', r'$27\mid m$']
for row_index, (row, color, label, count) in enumerate(zip(rows, colors, labels, counts)):
y = 2.75 - row_index
ax.text(-1.0, y, label, ha='right', va='center', fontsize=15)
for i, filled in enumerate(row):
ax.add_patch(Rectangle((i - 0.35, y - 0.32), 0.7, 0.64,
facecolor=color if filled else '#f1f3f4',
edgecolor='white', linewidth=1))
if filled:
ax.text(i, y, '1', color='white', ha='center', va='center')
ax.text(17.3, y, str(count), color=color, fontsize=18, ha='center', va='center')
ax.plot([-3.6, 18.1], [0.12, 0.12], color='#a5afb5', lw=1)
ax.text(-1.0, -0.43, r'$v_3(m)$', ha='right', va='center', fontsize=15)
for i, count in enumerate(columns):
ax.text(i, -0.43, str(count), ha='center', va='center',
fontweight='bold' if count > 1 else 'normal', fontsize=14)
ax.text(17.3, -0.43, '22', fontsize=20, ha='center', va='center', fontweight='bold')
fig.text(0.5, 0.06, r'By rows: $16+5+1=22$ | By columns: $11\cdot1+4\cdot2+1\cdot3=22$',
ha='center', fontsize=14)
fig.savefig(Path(__file__).with_suffix('.png').name, dpi=100, transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()