codex/similar-triangle-scaling.py

similar-triangle-scaling.py
"""Compare length and area scaling of similar right triangles; output in cwd.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Both triangles share the same plotting scale; the grid has unit spacing.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Arc, Polygon
import numpy as np


def main():
    original = np.array([[0, 0], [4, 0], [0, 3]])
    scaled = 2 * original
    lengths = np.linalg.norm(np.roll(original, -1, axis=0) - original, axis=1)
    scaled_lengths = np.linalg.norm(np.roll(scaled, -1, axis=0) - scaled, axis=1)
    assert sorted(lengths) == [3, 4, 5]
    assert sorted(scaled_lengths) == [6, 8, 10]
    assert np.all(scaled_lengths / lengths == 2)
    areas = [abs(np.linalg.det((v[1:] - v[0]).T)) / 2
             for v in (original, scaled)]
    assert np.allclose(areas, [6, 24])
    acute = np.degrees(np.arctan2(3, 4))
    assert np.isclose(acute, 36.86989764584402)
    assert np.isclose(np.degrees(np.arctan2(6, 8)), acute)

    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(9.2, 5.7), facecolor="white")
    fig.subplots_adjust(left=0.045, right=0.98, bottom=0.24, top=0.81)
    ax.set_facecolor("white")
    ax.set_aspect("equal")
    for scale, shift, face, color, area in [
        (1, 0, "#e5eefb", "#245b9b", 6),
        (2, 7, "#e1f2e9", "#287551", 24),
    ]:
        v = scale * original + np.array([shift, 0])
        ax.add_patch(Polygon(v, facecolor=face, edgecolor=color, linewidth=2.2))
        ax.plot([shift + 0.35, shift + 0.35, shift], [0, 0.35, 0.35],
                color=color, linewidth=1.5)
        ax.text(shift + 0.48, 0.28, "90°", fontsize=10, color=color)
        ax.text(shift - 0.25, 1.5 * scale, str(3 * scale),
                ha="right", va="center", color=color, fontsize=16)
        ax.text(shift + 2 * scale, -0.35, str(4 * scale),
                ha="center", va="top", color=color, fontsize=16)
        ax.text(shift + 2 * scale + 0.28, 1.5 * scale + 0.33,
                str(5 * scale), ha="center", color=color, fontsize=16)
        ax.text(shift + 1.1 * scale, 1.05 * scale, f"Area = {area}",
                ha="center", fontsize=13, color=color)
        # Equal angle arcs retain the same visual radius in both triangles.
        ax.add_patch(Arc((shift + 4 * scale, 0), 1.6, 1.6,
                         theta1=180 - acute, theta2=180, edgecolor=color, lw=1.5))
        ax.text(shift + 4 * scale + 0.05, -0.3, "36.87°",
                ha="center", color=color, fontsize=10)
        ax.text(shift, 3 * scale + 0.25, "53.13°",
                ha="center", va="bottom", color=color, fontsize=10)

    ax.set_xlim(-0.8, 15.7)
    ax.set_ylim(-0.8, 6.6)
    ax.set_xticks(np.arange(0, 16))
    ax.set_yticks(np.arange(0, 7))
    ax.grid(color="#e6e6e6", linewidth=0.6)
    ax.set_axisbelow(True)
    ax.tick_params(length=0, labelbottom=False, labelleft=False)
    for spine in ax.spines.values():
        spine.set_visible(False)
    fig.suptitle("Similar triangles: lengths ×2, area ×4", fontsize=20, y=0.97)
    fig.text(0.5, 0.89, "Same angles • Same plotting scale • Grid spacing = 1 length unit",
             ha="center", fontsize=12)
    fig.text(0.5, 0.155, "Corresponding lengths: 6/3 = 8/4 = 10/5 = 2",
             ha="center", fontsize=14)
    fig.text(0.5, 0.085, "Areas: 24/6 = 4 = 2²", ha="center", fontsize=15,
             fontweight="bold")
    output = Path.cwd() / (Path(__file__).stem + ".png")
    fig.savefig(output, dpi=140, facecolor="white", transparent=False)
    plt.close(fig)
    print(f"Saved {output.name}; lengths ×2, areas 6 → 24 and angles checked.")


if __name__ == "__main__":
    main()