codex/microscope-objective-eyepiece-conjugates.py

microscope-objective-eyepiece-conjugates.py
"""Draw an original finite-conjugate thin-lens microscope; Python 3.14.

Run from _media; output is the generator basename with a .png suffix.
Heights are enlarged by the same factor for both image and object.
"""
from pathlib import Path

import matplotlib.pyplot as plt


def main():
    plt.rcParams.update({'font.size': 11, 'font.family': 'DejaVu Sans'})
    fig, ax = plt.subplots(figsize=(11, 5.6), dpi=100, facecolor='white')
    ax.set_facecolor('white')
    ax.set_xlim(-24, 118)
    ax.set_ylim(-12, 13)
    ax.axis('off')
    fig.suptitle('A real intermediate image, then relaxed-eye viewing', fontsize=16, y=0.96)
    ax.axhline(0, color='#737373', linewidth=1)
    ax.text(113, 0.5, 'Optical\naxis', ha='center', fontsize=10)
    for x, label, focal in [(0, 'Objective', 10), (85, 'Eyepiece', 25)]:
        ax.annotate('', xy=(x, 8.6), xytext=(x, -8.6),
                    arrowprops={'arrowstyle': '<->', 'lw': 2, 'color': '#34495e'})
        ax.text(x, 10.6, label, ha='center', fontsize=12, fontweight='bold')
        ax.text(x, 9.1, f'f = {focal} mm', ha='center')
    for x in [-10, 10, 60]:
        ax.plot(x, 0, 'o', color='#34495e', markersize=4)
    ax.annotate('', xy=(-12, 1), xytext=(-12, 0),
                arrowprops={'arrowstyle': '-|>', 'lw': 2.5, 'color': '#207a47'})
    ax.text(-15, 3.5, 'Object', ha='center', color='#207a47', fontweight='bold')
    ax.axvline(60, ymin=0.14, ymax=0.64, color='#207a47', linestyle=':', lw=1.5)
    ax.annotate('', xy=(60, -5), xytext=(60, 0),
                arrowprops={'arrowstyle': '-|>', 'lw': 2.5, 'color': '#207a47'})
    ax.text(59, 5.9, 'Real intermediate image\n5 times taller, inverted',
            ha='center', color='#207a47', fontsize=11)
    # Two independent paraxial rays from the same object point.
    # Thin-lens slope update is t_out = t_in - y/f.
    for intercept, color in [(1.0, '#c34737'), (0.0, '#2868ac')]:
        slope_in = (intercept - 1) / 12
        slope_obj = slope_in - intercept / 10
        y_image = intercept + slope_obj * 60
        y_eye = intercept + slope_obj * 85
        slope_eye = slope_obj - y_eye / 25
        ax.plot([-12, 0, 60, 85, 112],
                [1, intercept, y_image, y_eye, y_eye + slope_eye * 27],
                color=color, linewidth=2)
        ax.annotate('', xy=(108, y_eye + slope_eye * 23),
                    xytext=(101, y_eye + slope_eye * 16),
                    arrowprops={'arrowstyle': '->', 'color': color, 'lw': 2})
    ax.text(103, -9.2, 'Parallel rays from\none image point', ha='center', fontsize=10)
    for left, right, y, label in [(-12, 0, -10.6, '12 mm'),
                                  (0, 60, -10.6, '60 mm'),
                                  (60, 85, -10.6, '25 mm')]:
        ax.annotate('', xy=(left, y), xytext=(right, y),
                    arrowprops={'arrowstyle': '<->', 'color': '#555555', 'lw': 1})
        ax.text((left + right) / 2, y - 0.7, label, ha='center', va='top')
    fig.text(0.5, 0.095, 'Detector at the intermediate image: m = −5.  Eyepiece viewing: Mₑ = 250/25 = 10.',
             ha='center', fontsize=11)
    fig.text(0.5, 0.049, 'Total signed angular magnification: −50.  Toy geometry; vertical heights enlarged.',
             ha='center', fontsize=11)
    fig.subplots_adjust(left=0.035, right=0.97, bottom=0.19, top=0.86)
    fig.savefig(Path(__file__).stem + '.png', facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()