codexprojected-area-radiance.py
projected-area-radiance.py
"""Generate projected-area-radiance.png in CWD; tested with Python 3.14.4.
Requires the repository's matplotlib and numpy environment. The two geometric
panels are cross-sections of a rectangle with unchanged depth into the page.
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False, "figure.facecolor": "white"})
radiance, area = 10.0, 0.020
angles = np.deg2rad([0, 60])
intensities = radiance * area * np.cos(angles)
assert np.allclose(intensities, [0.20, 0.10], atol=1e-15, rtol=0)
fig = plt.figure(figsize=(11.2, 4.7), dpi=100)
grid = fig.add_gridspec(1, 3, width_ratios=[1, 1, 1.35],
left=0.03, right=0.98, bottom=0.23, top=0.78, wspace=0.29)
patch_color, projection_color, ray_color = "#2166ac", "#d87911", "#8c2341"
for column, theta in enumerate(angles):
ax = fig.add_subplot(grid[0, column])
direction = np.array([np.sin(theta), np.cos(theta)])
endpoints = np.array([[-1.0, 0.0], [1.0, 0.0]])
height = 1.5
projected = endpoints + (height - endpoints @ direction)[:, None] * direction
assert np.isclose(np.linalg.norm(projected[1] - projected[0]), 2 * np.cos(theta))
ax.plot(endpoints[:, 0], endpoints[:, 1], lw=5, color=patch_color,
solid_capstyle="butt")
ax.plot(projected[:, 0], projected[:, 1], lw=5, color=projection_color,
solid_capstyle="butt")
for point, shadow in zip(endpoints, projected):
ax.plot([point[0], shadow[0]], [point[1], shadow[1]], "--",
color="0.55", lw=1.2)
ax.annotate("", xy=(0, 1.0), xytext=(0, 0),
arrowprops={"arrowstyle": "->", "color": "0.3", "lw": 1.7})
ax.text(-0.12, 1.01, "normal", ha="right", color="0.3", fontsize=10)
if column:
ax.annotate("", xy=1.35 * direction, xytext=(0, 0),
arrowprops={"arrowstyle": "->", "color": ray_color, "lw": 2})
ax.add_patch(Arc((0, 0), 0.88, 0.88, theta1=30, theta2=90,
color=ray_color, lw=1.3))
ax.text(0.27, 0.53, r"$60^\circ$", color=ray_color)
ax.text(1.78, 1.26, "view", ha="center", color=ray_color)
ax.text(1.65, 0.31, r"$A_\perp=A/2$", ha="left", color=projection_color)
else:
ax.text(0.18, 0.66, "view along normal", color=ray_color, fontsize=10)
ax.text(0, 1.67, r"$A_\perp=A$", ha="center", color=projection_color)
ax.text(0, -0.31, "same physical patch, area A", ha="center",
color=patch_color, fontsize=10)
ax.set(xlim=(-1.5, 2.75), ylim=(-0.6, 2.0), aspect="equal")
ax.set_title(rf"View at ${round(np.rad2deg(theta))}^\circ$", pad=15, fontsize=12)
ax.axis("off")
ax = fig.add_subplot(grid[0, 2])
degrees = np.linspace(0, 90, 361)
ax.plot(degrees, radiance * area * np.cos(np.deg2rad(degrees)),
color=patch_color, lw=2.5)
ax.scatter([0, 60], intensities, color=projection_color, zorder=4, s=50)
ax.plot([60, 60, 0], [0, 0.1, 0.1], "--", color="0.6", lw=1)
ax.annotate("0.20 W/sr", xy=(0, .20), xytext=(12, .209), fontsize=10)
ax.annotate("0.10 W/sr", xy=(60, .10), xytext=(29, .065), fontsize=10,
arrowprops={"arrowstyle": "->", "color": "0.35"})
ax.set(xlim=(-3, 93), ylim=(0, .23), xticks=[0, 30, 60, 90],
yticks=[0, .05, .1, .15, .2], xlabel=r"View angle $\theta$ (degrees)",
ylabel=r"Radiant intensity $I$ (W/sr)")
ax.set_title(r"$I(\theta)=LA\cos\theta$", fontsize=13, pad=15)
ax.grid(alpha=.2)
fig.suptitle("Projected area changes; radiance stays the same", fontsize=16, y=.97)
fig.text(.5, .88, r"Uniform outgoing $L=10$ W/(m$^2$ sr), physical $A=0.020$ m$^2$",
ha="center", fontsize=12)
fig.text(.5, .07, "Parallel projection onto a plane perpendicular to the view.\n"
"The rectangle's depth into the page stays fixed; only its apparent width contracts.",
ha="center", fontsize=11, linespacing=1.5)
fig.savefig("projected-area-radiance.png", facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()