@@ -966,6 +966,78 @@ def compute_sigma_moteki_kondo(
966966 )
967967 return out
968968
969+ def compute_normalized_incident_irradiance_moteki_kondo (
970+ sigma_out : xr .Dataset ,
971+ t_vals : Optional [Union [np .ndarray , xr .DataArray ]] = None ,
972+ sample_dim : str = "time" ,
973+ ) -> xr .DataArray :
974+ """
975+ Compute the normalized incident irradiance I(t)/I0 using the Moteki & Kondo
976+ Gaussian beam model:
977+
978+ I(t) / I0 = exp(-(t - tau_best)^2 / (2 * sigma_hat^2)) [Eq. (1)]
979+
980+ Parameters
981+ ----------
982+ sigma_out : xr.Dataset
983+ Output from compute_sigma_moteki_kondo(...). Must contain at least:
984+ - sigma_hat
985+ - tau_best
986+ and ideally fit_start / fit_stop.
987+ t_vals : 1D array-like, optional
988+ Explicit time axis (same units as tau_best and sigma_hat). If None,
989+ defaults to 0–39.6 µs at 0.4 µs spacing.
990+ sample_dim : str, default "time"
991+ Name of the returned sample dimension.
992+
993+ Returns
994+ -------
995+ xr.DataArray
996+ Normalized incident irradiance I/I0 evaluated on the chosen time axis.
997+ """
998+ if "sigma_hat" not in sigma_out :
999+ raise ValueError ("sigma_out must contain 'sigma_hat'." )
1000+ if "tau_best" not in sigma_out :
1001+ raise ValueError ("sigma_out must contain 'tau_best'." )
1002+
1003+ sigma_hat = float (np .asarray (sigma_out ["sigma_hat" ].item ()))
1004+ tau_best = float (np .asarray (sigma_out ["tau_best" ].item ()))
1005+
1006+ if not np .isfinite (sigma_hat ) or sigma_hat <= 0 :
1007+ raise ValueError (f"Invalid sigma_hat={ sigma_hat } ." )
1008+ if not np .isfinite (tau_best ):
1009+ raise ValueError (f"Invalid tau_best={ tau_best } ." )
1010+
1011+ # Default SP2 waveform time axis:
1012+ # 100 samples spanning 0–39.6 µs at 0.4 µs spacing.
1013+ if t_vals is None :
1014+ t_vals = np .arange (0.0 , 40.0 , 0.4 )
1015+ else :
1016+ t_vals = np .asarray (
1017+ t_vals .data if isinstance (t_vals , xr .DataArray ) else t_vals ,
1018+ dtype = float ,
1019+ )
1020+
1021+ if t_vals .ndim != 1 :
1022+ raise ValueError ("t_vals must be one-dimensional." )
1023+
1024+ # Moteki & Kondo Eq. (1): normalized irradiance profile.
1025+ i_norm = np .exp (- ((t_vals - tau_best ) ** 2 ) / (2.0 * sigma_hat ** 2 ))
1026+
1027+ out = xr .DataArray (
1028+ i_norm ,
1029+ dims = (sample_dim ,),
1030+ coords = {sample_dim : t_vals },
1031+ name = "I_over_I0" ,
1032+ attrs = {
1033+ "long_name" : "Normalized incident irradiance I/I0" ,
1034+ "description" : "Gaussian incident irradiance normalized by its peak I0" ,
1035+ "tau_best" : tau_best ,
1036+ "sigma_hat" : sigma_hat ,
1037+ },
1038+ )
1039+ return out
1040+
9691041def plot_incident_irradiance (
9701042 S : xr .Dataset ,
9711043 ds : xr .Dataset ,
@@ -981,38 +1053,39 @@ def plot_incident_irradiance(
9811053):
9821054 """
9831055 Plot normalized derivative S'(t)/S(t), expected I'(t)/I(t), and optionally
984- the scattering signal, all against the same bins-based time axis.
1056+ the scattering signal and normalized incident irradiance, all against the
1057+ same bins-based time axis.
9851058
9861059 Parameters
9871060 ----------
9881061 S : xr.Dataset
989- Original scattering signal dataset .
1062+ Dataset containing the normalized derivative S'(t)/S(t) .
9901063 ds : xr.Dataset
991- Dataset containing the normalized derivative .
1064+ Dataset containing the scattering signal S(t) .
9921065 record_no : int
9931066 Event index to plot.
994- chn : int
995- Channel number (0 or 4).
996- plot_scattering_signal : bool
997- If True, overlay the scattering signal on a secondary y-axis .
1067+ chn : int, default 0
1068+ Channel number (0 or 4) to select the appropriate data variable .
1069+ plot_scattering_signal : bool, default True
1070+ If True, overlay the scattering signal and normalized incident irradiance .
9981071 sigma_ds : xr.Dataset, optional
999- Output of compute_sigma_moteki_kondo() . If provided, tau/sigma are
1000- taken from sigma_ds["tau_best"] and sigma_ds["sigma_hat"] .
1072+ Dataset containing sigma_hat and tau_best for the event . If provided,
1073+ these values will be used for plotting the expected I'/I line .
10011074 tau : float, optional
1002- Beam-center time in seconds .
1075+ If sigma_ds is not provided, tau must be supplied for plotting the expected I'/I line .
10031076 sigma : float, optional
1004- Gaussian width in seconds .
1005- h : float
1006- Sampling interval in seconds .
1007- time_units : {"us", "s"}
1008- Units for the x-axis.
1009- show_fit_window : bool
1010- If True, shade the fitted leading-edge window when available .
1077+ If sigma_ds is not provided, sigma must be supplied for plotting the expected I'/I line .
1078+ h : float, default 0.4
1079+ Time bin width in microseconds .
1080+ time_units : str, default "us"
1081+ Time units for the x-axis. Must be either "us" (microseconds) or "s" (seconds) .
1082+ show_fit_window : bool, default True
1083+ If True and sigma_ds is provided , shade the fit window region on the plot .
10111084
10121085 Returns
10131086 -------
1014- ax : matplotlib Axes
1015- Primary axes object.
1087+ axes : matplotlib.axes. Axes
1088+ The axes object containing the plot .
10161089 """
10171090 if chn not in [0 , 4 ]:
10181091 raise ValueError ("Channel number must be 0 or 4." )
@@ -1068,15 +1141,18 @@ def plot_incident_irradiance(
10681141 # Expected I'/I line from Moteki & Kondo.
10691142 i_ratio_expected = - (t_plot - tau_plot ) / (sigma_plot ** 2 )
10701143
1144+ # Normalized incident irradiance I(t)/I0 from Eq. (1).
1145+ i_norm = np .exp (- ((t_plot - tau_plot ) ** 2 ) / (2.0 * sigma_plot ** 2 ))
1146+
10711147 plt .rcParams ["font.family" ] = "Times New Roman"
1072- plt .rcParams ["mathtext.fontset" ] = "stix"
1148+ plt .rcParams ["mathtext.fontset" ] = "stix"
10731149 fig , ax = plt .subplots (figsize = (10 , 6 ))
10741150
10751151 # Normalized derivative.
10761152 line1 , = ax .plot (
10771153 t_plot ,
1078- y_norm , # Scale for visibility
1079- 'o' ,
1154+ y_norm ,
1155+ "o" ,
10801156 color = "blue" ,
10811157 label = f"{ ch_name } (Normalized dS/dt)" ,
10821158 linewidth = 1.2 ,
@@ -1095,8 +1171,10 @@ def plot_incident_irradiance(
10951171 ax .set_xlabel (x_label )
10961172 ax .set_ylim (- 1.0 , 1.0 )
10971173 ax .set_xlim (t_plot [10 ], t_plot [- 30 ])
1098- ax .set_ylabel (r"Normalized Derivative ($\rm \mu s^{-1}$)" ,
1099- color = "blue" )
1174+ ax .set_ylabel (
1175+ r"Normalized Derivative ($\rm \mu s^{-1}$)" ,
1176+ color = "blue" ,
1177+ )
11001178 ax .grid (True , alpha = 0.3 )
11011179 ax .tick_params (axis = "y" , colors = "blue" )
11021180
@@ -1116,7 +1194,7 @@ def plot_incident_irradiance(
11161194 label = "Fit window" ,
11171195 )
11181196
1119- # Optional scattering signal overlay.
1197+ # Optional scattering signal overlay + normalized incident irradiance on the right axis .
11201198 if plot_scattering_signal :
11211199 ax2 = ax .twinx ()
11221200 y_scatter_shifted = y_scatter - np .nanmin (y_scatter )
@@ -1129,9 +1207,21 @@ def plot_incident_irradiance(
11291207 linewidth = 1.2 ,
11301208 label = f"{ ch_name } (Scattering Signal)" ,
11311209 )
1132- ax2 .set_ylabel ("Scattering Signal (baseline shifted)" )
11331210
1134- lines = [line1 ,line2 , line3 ]
1211+ # TODO multiplying by the max of the scattering signal will not work
1212+ # for evaporative particles
1213+ line4 , = ax2 .plot (
1214+ t_plot ,
1215+ i_norm * np .nanmax (y_scatter_shifted ),
1216+ color = "red" ,
1217+ linestyle = "--" ,
1218+ linewidth = 2.0 ,
1219+ label = r"Normalized incident irradiance $I(t)/I_0$" ,
1220+ )
1221+
1222+ ax2 .set_ylabel ("Scattering Signal (baseline shifted) / $I/I_0$" )
1223+
1224+ lines = [line1 , line2 , line3 , line4 ]
11351225 labels = [l .get_label () for l in lines ]
11361226 ax .legend (lines , labels , loc = "best" , fontsize = 10 )
11371227 else :
@@ -1140,7 +1230,7 @@ def plot_incident_irradiance(
11401230 ax .set_title (
11411231 f"Normalized Derivative, Expected I'(t)/I(t), and Scattering Signal - "
11421232 f"Channel { chn } Record { record_no } " ,
1143- pad = 20 , # increase space between title and plot
1233+ pad = 20 ,
11441234 )
11451235
11461236 return ax
0 commit comments