weighted_spectral_effectiveness.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import pandas as pd
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. # -----------------------------------------------------
  5. # 1) Read and prepare data
  6. # -----------------------------------------------------
  7. file_path = "./uv_responsivity_and_spectral_effectiveness_data.csv"
  8. df = pd.read_csv(file_path)
  9. w = df["Wavelength (nm)"].values
  10. uvc = df["UVC"].values
  11. uvb = df["UVB"].values
  12. uva = df["UVA"].values
  13. # eff = df["Relative Spectral Effectiveness"].values
  14. eff = df["Relative Spectral Effectiveness (eyes protected)"].values
  15. # We have two cutoffs (rounded to data points):
  16. cutoff_uvc_uvb = 280 # exact: 280.7
  17. cutoff_uvb_uva = 315 # exact: 315.5
  18. # Calculate "effective" curves = Responsivity * Spectral Effectiveness
  19. uvc_eff = uvc * eff
  20. uvb_eff = uvb * eff
  21. uva_eff = uva * eff
  22. # -----------------------------------------------------
  23. # 2) Weighted spectral effectiveness (in-band only)
  24. # -----------------------------------------------------
  25. def compute_weighted_eff_in_band(wl, resp, sp_eff, band_min, band_max):
  26. """
  27. Weighted spectral effectiveness = sum(resp * sp_eff) / sum(resp),
  28. but only for band_min <= wl < band_max (and resp>0).
  29. (using Responsivity as the weight)
  30. """
  31. mask = (wl >= band_min) & (wl < band_max) & (resp > 0)
  32. if mask.sum() == 0:
  33. return 0.0
  34. return (resp[mask] * sp_eff[mask]).sum() / resp[mask].sum()
  35. wl_min = w[0]
  36. wl_max = w[-1]
  37. uvc_w_eff = compute_weighted_eff_in_band(w, uvc, eff, wl_min, cutoff_uvc_uvb)
  38. uvb_w_eff = compute_weighted_eff_in_band(w, uvb, eff, cutoff_uvc_uvb, cutoff_uvb_uva)
  39. uva_w_eff = compute_weighted_eff_in_band(w, uva, eff, cutoff_uvb_uva, wl_max)
  40. # -----------------------------------------------------
  41. # 2.5) Compute Area Ratios and Correct Weighted Spectral Effectiveness
  42. # -----------------------------------------------------
  43. def compute_area_ratio(wl, curve, band_min, band_max):
  44. """
  45. Compute the ratio of the area under the curve in the band [band_min, band_max)
  46. to the total area under the curve.
  47. """
  48. total_area = np.trapezoid(curve, wl)
  49. band_mask = (wl >= band_min) & (wl <= band_max)
  50. band_area = np.trapezoid(curve[band_mask], wl[band_mask])
  51. return band_area / total_area if total_area > 0 else 0.0
  52. # Compute area ratios for the Responsivity curves
  53. uvc_area_ratio = compute_area_ratio(w, uvc, wl_min, cutoff_uvc_uvb)
  54. uvb_area_ratio = compute_area_ratio(w, uvb, cutoff_uvc_uvb, cutoff_uvb_uva)
  55. uva_area_ratio = compute_area_ratio(w, uva, cutoff_uvb_uva, wl_max)
  56. # Correct Weighted Spectral Effectiveness
  57. uvc_w_eff = uvc_w_eff * uvc_area_ratio
  58. uvb_w_eff = uvb_w_eff * uvb_area_ratio
  59. uva_w_eff = uva_w_eff * uva_area_ratio
  60. # -----------------------------------------------------
  61. # 3) In-band peaks
  62. # -----------------------------------------------------
  63. def find_in_band_peak(wl, y, band_min, band_max):
  64. """
  65. Return (peak_wl, peak_val) for the maximum of y where band_min <= wl <= band_max.
  66. """
  67. mask = (wl >= band_min) & (wl < band_max)
  68. if not np.any(mask):
  69. return None, None
  70. idx_max = np.argmax(y[mask])
  71. peak_val = y[mask][idx_max]
  72. peak_wl = wl[mask][idx_max]
  73. return peak_wl, peak_val
  74. uvc_peak_wl, uvc_peak_val = find_in_band_peak(w, uvc_eff, wl_min, cutoff_uvc_uvb)
  75. uvb_peak_wl, uvb_peak_val = find_in_band_peak(
  76. w, uvb_eff, cutoff_uvc_uvb, cutoff_uvb_uva
  77. )
  78. uva_peak_wl, uva_peak_val = find_in_band_peak(w, uva_eff, cutoff_uvb_uva, wl_max)
  79. # -----------------------------------------------------
  80. # 4) Plotting Helpers: Split in-band vs. out-of-band
  81. # We do it by creating arrays with NaN outside the band
  82. # so matplotlib doesn't draw lines across boundaries.
  83. # -----------------------------------------------------
  84. def band_split(wl, arr, band_min, band_max):
  85. """
  86. Return two arrays: in-band (NaN outside) and out-of-band (NaN inside),
  87. so we can plot them separately and avoid bridging lines across cutoff edges.
  88. """
  89. in_mask = (wl >= band_min) & (wl <= band_max)
  90. out_mask = (wl <= band_min) | (wl >= band_max) # ~in_mask
  91. in_array = np.where(in_mask, arr, np.nan)
  92. out_array = np.where(out_mask, arr, np.nan)
  93. return in_array, out_array
  94. # We'll also define a small function to plot the peak marker
  95. def add_peak_marker(ax, peak_wl, peak_val, color):
  96. if peak_wl is not None and peak_val is not None:
  97. ax.scatter(peak_wl, peak_val, color=color, zorder=5)
  98. ax.text(
  99. peak_wl + 2,
  100. peak_val,
  101. f"{peak_val:.4g} @{peak_wl:.0f}nm",
  102. color=color,
  103. fontsize=9,
  104. ha="left",
  105. va="bottom",
  106. )
  107. # -----------------------------------------------------
  108. # 6) Create figure with 3 subplots
  109. # -----------------------------------------------------
  110. colors = {
  111. "UVC": "#4d4d4d", # darker/grey
  112. "UVB": "#8a2be2", # violet/purple
  113. "UVA": "#0b66f2", # pleasing blue
  114. }
  115. fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12), sharex=True)
  116. plt.subplots_adjust(hspace=0.1)
  117. # A helper to plot everything for a given subplot
  118. def plot_subplot(
  119. ax,
  120. w,
  121. resp,
  122. eff,
  123. eff_curve,
  124. band_min,
  125. band_max,
  126. color,
  127. label,
  128. weighted_eff,
  129. peak_wl,
  130. peak_val,
  131. ):
  132. """
  133. Plots:
  134. - background (responsivity in linear scale, spectral eff in log scale)
  135. - in-band and out-of-band lines for 'eff_curve' (which is resp*eff)
  136. - vertical cutoffs
  137. - weighted efficiency text
  138. - peak marker
  139. """
  140. # Split in-band vs out-of-band
  141. main_in, main_out = band_split(w, eff_curve, band_min, band_max)
  142. p1 = ax.plot(
  143. w, main_out, color=color, alpha=0.4, linewidth=1, label=f"{label} - out"
  144. )
  145. p2 = ax.plot(w, main_in, color=color, alpha=1.0, linewidth=2, label=f"{label} - in")
  146. ax.set_ylim(0, 1.1 * np.max(eff_curve))
  147. # Background plots
  148. ax_resp = ax.twinx()
  149. ax_eff = ax.twinx()
  150. ax_eff.spines.right.set_position(("axes", 1.1))
  151. ax_eff.set_yscale("log", nonpositive="clip")
  152. p3 = ax_resp.plot(w, resp, color="grey", linestyle="-", alpha=0.5, label="Resp")
  153. p4 = ax_eff.plot(
  154. w,
  155. eff,
  156. color="grey",
  157. linestyle="--",
  158. alpha=0.6,
  159. label="SpecEff",
  160. )
  161. ax_resp.set_ylabel("Normalized Sensor Responsivity")
  162. ax_eff.set_ylabel("Relative Spectral Effectiveness")
  163. # Add cutoff lines
  164. ax.axvline(x=cutoff_uvc_uvb, color="black", linestyle="dotted", alpha=0.5)
  165. ax.axvline(x=cutoff_uvb_uva, color="black", linestyle="dotted", alpha=0.5)
  166. # Weighted Spectral Effectiveness text annotation
  167. text_str = f"Weighted Spectral Effectiveness (in band): {weighted_eff:.4g}"
  168. ax.text(
  169. 0.00,
  170. 1.0055,
  171. text_str,
  172. transform=ax.transAxes,
  173. color=color,
  174. fontsize=10,
  175. ha="left",
  176. va="bottom",
  177. )
  178. # Mark the peak
  179. add_peak_marker(ax, peak_wl, peak_val, color)
  180. # Final styling
  181. ax.legend(loc="upper right", handles=[p1[0], p2[0], p3[0], p4[0]])
  182. ax.set_ylabel(f"{label}")
  183. # --------------------- Subplot for UVC --------------------------
  184. plot_subplot(
  185. ax1,
  186. w,
  187. uvc,
  188. eff,
  189. uvc_eff,
  190. band_min=w[0],
  191. band_max=cutoff_uvc_uvb,
  192. color=colors["UVC"],
  193. label="Resp (UVC) * SpecEff",
  194. weighted_eff=uvc_w_eff,
  195. peak_wl=uvc_peak_wl,
  196. peak_val=uvc_peak_val,
  197. )
  198. # --------------------- Subplot for UVB --------------------------
  199. plot_subplot(
  200. ax2,
  201. w,
  202. uvb,
  203. eff,
  204. uvb_eff,
  205. band_min=cutoff_uvc_uvb,
  206. band_max=cutoff_uvb_uva,
  207. color=colors["UVB"],
  208. label="Resp (UVB) * SpecEff",
  209. weighted_eff=uvb_w_eff,
  210. peak_wl=uvb_peak_wl,
  211. peak_val=uvb_peak_val,
  212. )
  213. # --------------------- Subplot for UVA --------------------------
  214. plot_subplot(
  215. ax3,
  216. w,
  217. uva,
  218. eff,
  219. uva_eff,
  220. band_min=cutoff_uvb_uva,
  221. band_max=w[-1],
  222. color=colors["UVA"],
  223. label="Resp (UVA) * SpecEff",
  224. weighted_eff=uva_w_eff,
  225. peak_wl=uva_peak_wl,
  226. peak_val=uva_peak_val,
  227. )
  228. ax3.set_xlabel("Wavelength (nm)")
  229. plt.tight_layout()
  230. plt.show()