codexradiant-flux-synthetic-spectral-integrals.py
radiant-flux-synthetic-spectral-integrals.py
"""Two synthetic equal-power spectra and an illustrative detector response.
Run from _media with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
The PNG is written to the current directory with this script's basename.
All wavelength integrals here use nm; powers are converted to W for currents.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
wave_a = np.linspace(400, 600, 2001)
wave_b = np.linspace(600, 800, 2001)
density_a = 0.020 * (1 - np.abs(wave_a - 500) / 100) # mW/nm
density_b = np.full_like(wave_b, 0.010) # mW/nm
def response(wavelength_nm):
return 0.2 + 0.001 * (wavelength_nm - 400) # A/W
totals_mw = [np.trapezoid(density_a, wave_a), np.trapezoid(density_b, wave_b)]
currents_ma = [np.trapezoid(density_a * response(wave_a), wave_a),
np.trapezoid(density_b * response(wave_b), wave_b)]
np.testing.assert_allclose(totals_mw, [2, 2], atol=1e-12)
np.testing.assert_allclose(currents_ma, [0.6, 1.0], atol=1e-12)
for wave, density, bounds, expected in [
(wave_a, density_a, (450, 550), 1.5),
(wave_b, density_b, (650, 750), 1.0),
]:
mask = (wave >= bounds[0]) & (wave <= bounds[1])
np.testing.assert_allclose(np.trapezoid(density[mask], wave[mask]), expected, atol=1e-12)
plt.rcParams.update({"font.size": 11, "axes.titlesize": 12})
fig = plt.figure(figsize=(10, 6.6), layout="constrained")
fig.patch.set_facecolor("white")
grid = fig.add_gridspec(2, 2, height_ratios=[1.25, 1])
axes = [fig.add_subplot(grid[0, 0]), fig.add_subplot(grid[0, 1])]
fig.text(0.5, 0.985, "Two synthetic spectra: equal total radiant flux, different signals",
fontsize=15, ha="center", va="top")
colors = ["#17649c", "#bd570c"]
for ax, wave, density, label, band, band_power, color in zip(
axes, [wave_a, wave_b], [density_a, density_b], ["A: triangular", "B: flat band"],
[(450, 550), (650, 750)], [1.5, 1.0], colors,
):
ax.plot([350, wave[0], *wave, wave[-1], 850], [0, 0, *density, 0, 0], color=color, lw=2)
ax.fill_between(wave, density, color=color, alpha=0.14)
mask = (wave >= band[0]) & (wave <= band[1])
ax.fill_between(wave[mask], density[mask], color=color, alpha=0.4)
ax.set(title=label, xlim=(350, 850), ylim=(0, 0.026), xlabel="Wavelength λ (nm)",
ylabel="Spectral radiant flux (mW/nm)")
ax.text(0.5, 0.91, "Total area = 2.00 mW", transform=ax.transAxes, ha="center")
ax.text(0.5, 0.73, f"{band[0]}–{band[1]} nm:\n{band_power:.2f} mW", transform=ax.transAxes,
ha="center", color=color)
ax.grid(alpha=0.18)
ax.set_xticks([400, 500, 600, 700, 800])
ax = fig.add_subplot(grid[1, :])
wavelengths = np.linspace(400, 800, 401)
ax.plot(wavelengths, response(wavelengths), color="#333333", lw=2)
ax.scatter([500, 700], [0.3, 0.5], c=colors, s=60, zorder=4)
ax.text(510, 0.26, "A: mean R = 0.30 A/W\nCurrent = 0.60 mA", color=colors[0], va="top")
ax.text(690, 0.54, "B: mean R = 0.50 A/W\nCurrent = 1.00 mA", color=colors[1], ha="right", va="bottom")
ax.set(xlim=(380, 820), ylim=(0.13, 0.68), xlabel="Wavelength λ (nm)",
ylabel="Responsivity R (A/W)", title="Illustrative linear response: current = ∫ R(λ) Φₑ,λ(λ) dλ")
ax.grid(alpha=0.18)
fig.get_layout_engine().set(rect=(0, 0.035, 1, 0.90))
fig.text(0.5, 0.009, "All curves are stipulated models • No measured spectra or real detector calibration",
ha="center", fontsize=10, color="#444444")
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()