Skip to content

Commit 77b5ea1

Browse files
authored
Merge pull request #91 from jtgasparik/calculate_irradiance
Calculate irradiance
2 parents 477d3b0 + 143f5f1 commit 77b5ea1

4 files changed

Lines changed: 141 additions & 32 deletions

File tree

pysp2/util/normalized_derivative_method.py

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
9691041
def 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
9.47 KB
Loading

tests/test_ndm.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
np.set_printoptions(threshold=np.inf)
44

55
from pysp2.util.normalized_derivative_method import MLEConfig, mle_tau_moteki_kondo, compute_d2_moteki_kondo
6-
from pysp2.util.normalized_derivative_method import compute_sigma_moteki_kondo
6+
from pysp2.util.normalized_derivative_method import compute_sigma_moteki_kondo, compute_normalized_incident_irradiance_moteki_kondo
77
event=152
88
my_sp2b = pysp2.io.read_sp2(pysp2.testing.EXAMPLE_SP2B_PSL, arm_convention=False)
99
my_ini = pysp2.io.read_config(pysp2.testing.EXAMPLE_INI_PSL)
@@ -77,7 +77,7 @@ def test_ndm_moteki_kondo():
7777
np.testing.assert_allclose(
7878
tau_best,
7979
tau_val_true,
80-
atol=0.3, # absolute tolerance = 5e-7
80+
atol=0.3, # absolute tolerance = 0.3 microseconds
8181
)
8282

8383
sigma_ds = compute_sigma_moteki_kondo(
@@ -94,11 +94,29 @@ def test_ndm_moteki_kondo():
9494
)
9595

9696
# example: use the best sigma value from your analysis, divided by 4.29193 to convert FWTM value of
97-
# 18.51*np.sqrt(np.log(10)/np.log(2)) to sigma where 18.51 is the average FWHM in us
97+
# 18.51*np.sqrt(np.log(10)/np.log(2)) to sigma where 33.7366 is the average FWHM in us
9898
sigma_best = (33.7366*0.4)/4.29193
9999

100100
np.testing.assert_allclose(
101101
sigma_ds['sigma_hat'].values,
102102
sigma_best,
103103
atol=0.12, # absolute tolerance = 1.5 microseconds
104-
)
104+
)
105+
106+
# Test the normalized irradiance function
107+
I_norm = compute_normalized_incident_irradiance_moteki_kondo(
108+
sigma_out=sigma_ds,
109+
)
110+
111+
y_scatter_background_shifted = (
112+
my_binary['Data_ch0'].isel(event_index=event).values -
113+
np.nanmin(my_binary['Data_ch0'].isel(event_index=event).values)
114+
)
115+
116+
# test for peak area only
117+
for i in range(15,75):
118+
np.testing.assert_allclose(
119+
(I_norm * np.nanmax(y_scatter_background_shifted))[i],
120+
y_scatter_background_shifted[i],
121+
atol=4500, # absolute tolerance ~ 10% of the max scattering signal value
122+
)

tests/test_vis.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def test_plot_incident_irradiance():
100100
sigma_ds=sigma_ds,
101101
time_units="us",
102102
)
103+
103104
fig = ax.figure
104105

105106
return fig

0 commit comments

Comments
 (0)