Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 106 additions & 41 deletions pysp2/util/normalized_derivative_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,84 @@ def compute_sigma_moteki_kondo(
)
return out

def compute_normalized_incident_irradiance_moteki_kondo(
sigma_out: xr.Dataset,
*,
t_vals: Optional[Union[np.ndarray, xr.DataArray]] = None,
sample_dim: str = "time",
) -> xr.DataArray:
"""
Compute the normalized incident irradiance I(t)/I0 using the Moteki & Kondo
Gaussian beam model:

I(t) / I0 = exp(-(t - tau_best)^2 / (2 * sigma_hat^2)) [Eq. (1)]

Parameters
----------
sigma_out : xr.Dataset
Output from compute_sigma_moteki_kondo(...). Must contain at least:
- sigma_hat
- tau_best
and ideally fit_start / fit_stop.
h : float, optional
Sampling interval. Required if `t` is not provided.
t : array-like, optional
Explicit time axis to evaluate on. If provided, this is used directly.
n_samples : int, optional
If `t` is not provided, use this many samples starting at 0 with spacing `h`.
If omitted, and `fit_start` / `fit_stop` are present in sigma_out, the function
evaluates only over the fitted window [fit_start, fit_stop).
sample_dim : str, default "time"
Name of the returned sample dimension.

Returns
-------
xr.DataArray
Normalized incident irradiance I/I0 evaluated on the chosen time axis.
"""
if "sigma_hat" not in sigma_out:
raise ValueError("sigma_out must contain 'sigma_hat'.")
if "tau_best" not in sigma_out:
raise ValueError("sigma_out must contain 'tau_best'.")

sigma_hat = float(np.asarray(sigma_out["sigma_hat"].values))
tau_best = float(np.asarray(sigma_out["tau_best"].values))

if not np.isfinite(sigma_hat) or sigma_hat <= 0:
raise ValueError(f"Invalid sigma_hat={sigma_hat}.")
if not np.isfinite(tau_best):
raise ValueError(f"Invalid tau_best={tau_best}.")

# Default SP2 waveform time axis:
# 100 samples spanning 0–39.6 µs at 0.4 µs spacing.
if t_vals is None:
t_vals = np.arange(0.0, 40.0, 0.4)
else:
t_vals = np.asarray(
t_vals.data if isinstance(t_vals, xr.DataArray) else t_vals,
dtype=float,
)

if t_vals.ndim != 1:
raise ValueError("t_vals must be one-dimensional.")

# Moteki & Kondo Eq. (1): normalized irradiance profile.
i_norm = np.exp(-((t_vals - tau_best) ** 2) / (2.0 * sigma_hat ** 2))

out = xr.DataArray(
i_norm,
dims=(sample_dim,),
coords={sample_dim: t_vals},
name="I_over_I0",
attrs={
"long_name": "Normalized incident irradiance I/I0",
"description": "Gaussian incident irradiance normalized by its peak I0",
"tau_best": tau_best,
"sigma_hat": sigma_hat,
},
)
return out

def plot_incident_irradiance(
S: xr.Dataset,
ds: xr.Dataset,
Expand All @@ -981,38 +1059,8 @@ def plot_incident_irradiance(
):
"""
Plot normalized derivative S'(t)/S(t), expected I'(t)/I(t), and optionally
the scattering signal, all against the same bins-based time axis.

Parameters
----------
S : xr.Dataset
Original scattering signal dataset.
ds : xr.Dataset
Dataset containing the normalized derivative.
record_no : int
Event index to plot.
chn : int
Channel number (0 or 4).
plot_scattering_signal : bool
If True, overlay the scattering signal on a secondary y-axis.
sigma_ds : xr.Dataset, optional
Output of compute_sigma_moteki_kondo(). If provided, tau/sigma are
taken from sigma_ds["tau_best"] and sigma_ds["sigma_hat"].
tau : float, optional
Beam-center time in seconds.
sigma : float, optional
Gaussian width in seconds.
h : float
Sampling interval in seconds.
time_units : {"us", "s"}
Units for the x-axis.
show_fit_window : bool
If True, shade the fitted leading-edge window when available.

Returns
-------
ax : matplotlib Axes
Primary axes object.
the scattering signal and normalized incident irradiance, all against the
same bins-based time axis.
"""
if chn not in [0, 4]:
raise ValueError("Channel number must be 0 or 4.")
Expand Down Expand Up @@ -1068,15 +1116,18 @@ def plot_incident_irradiance(
# Expected I'/I line from Moteki & Kondo.
i_ratio_expected = -(t_plot - tau_plot) / (sigma_plot ** 2)

# Normalized incident irradiance I(t)/I0 from Eq. (1).
i_norm = np.exp(-((t_plot - tau_plot) ** 2) / (2.0 * sigma_plot ** 2))

plt.rcParams["font.family"] = "Times New Roman"
plt.rcParams["mathtext.fontset"] = "stix"
plt.rcParams["mathtext.fontset"] = "stix"
fig, ax = plt.subplots(figsize=(10, 6))

# Normalized derivative.
line1, = ax.plot(
t_plot,
y_norm, # Scale for visibility
'o',
y_norm,
"o",
color="blue",
label=f"{ch_name} (Normalized dS/dt)",
linewidth=1.2,
Expand All @@ -1095,8 +1146,10 @@ def plot_incident_irradiance(
ax.set_xlabel(x_label)
ax.set_ylim(-1.0, 1.0)
ax.set_xlim(t_plot[10], t_plot[-30])
ax.set_ylabel(r"Normalized Derivative ($\rm \mu s^{-1}$)",
color="blue")
ax.set_ylabel(
r"Normalized Derivative ($\rm \mu s^{-1}$)",
color="blue",
)
ax.grid(True, alpha=0.3)
ax.tick_params(axis="y", colors="blue")

Expand All @@ -1116,7 +1169,7 @@ def plot_incident_irradiance(
label="Fit window",
)

# Optional scattering signal overlay.
# Optional scattering signal overlay + normalized incident irradiance on the right axis.
if plot_scattering_signal:
ax2 = ax.twinx()
y_scatter_shifted = y_scatter - np.nanmin(y_scatter)
Expand All @@ -1129,9 +1182,21 @@ def plot_incident_irradiance(
linewidth=1.2,
label=f"{ch_name} (Scattering Signal)",
)
ax2.set_ylabel("Scattering Signal (baseline shifted)")

lines = [line1,line2, line3]
# TODO multiplying by the max of the scattering signal will not work
# for evaporative particles
line4, = ax2.plot(
t_plot,
i_norm * np.nanmax(y_scatter_shifted),
color="red",
linestyle="--",
linewidth=2.0,
label=r"Normalized incident irradiance $I(t)/I_0$",
)

ax2.set_ylabel("Scattering Signal (baseline shifted) / $I/I_0$")

lines = [line1, line2, line3, line4]
labels = [l.get_label() for l in lines]
ax.legend(lines, labels, loc="best", fontsize=10)
else:
Expand All @@ -1140,7 +1205,7 @@ def plot_incident_irradiance(
ax.set_title(
f"Normalized Derivative, Expected I'(t)/I(t), and Scattering Signal - "
f"Channel {chn} Record {record_no}",
pad=20, # increase space between title and plot
pad=20,
)

return ax
Binary file modified tests/baseline/test_plot_incident_irradiance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 22 additions & 4 deletions tests/test_ndm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
np.set_printoptions(threshold=np.inf)

from pysp2.util.normalized_derivative_method import MLEConfig, mle_tau_moteki_kondo, compute_d2_moteki_kondo
from pysp2.util.normalized_derivative_method import compute_sigma_moteki_kondo
from pysp2.util.normalized_derivative_method import compute_sigma_moteki_kondo, compute_normalized_incident_irradiance_moteki_kondo
event=152
my_sp2b = pysp2.io.read_sp2(pysp2.testing.EXAMPLE_SP2B_PSL, arm_convention=False)
my_ini = pysp2.io.read_config(pysp2.testing.EXAMPLE_INI_PSL)
Expand Down Expand Up @@ -77,7 +77,7 @@ def test_ndm_moteki_kondo():
np.testing.assert_allclose(
tau_best,
tau_val_true,
atol=0.3, # absolute tolerance = 5e-7
atol=0.3, # absolute tolerance = 0.3 microseconds
)

sigma_ds = compute_sigma_moteki_kondo(
Expand All @@ -94,11 +94,29 @@ def test_ndm_moteki_kondo():
)

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

np.testing.assert_allclose(
sigma_ds['sigma_hat'].values,
sigma_best,
atol=0.12, # absolute tolerance = 1.5 microseconds
)
)

# Test the normalized irradiance function
I_norm = compute_normalized_incident_irradiance_moteki_kondo(
sigma_out=sigma_ds,
)

y_scatter_background_shifted = (
my_binary['Data_ch0'].isel(event_index=event).values -
np.nanmin(my_binary['Data_ch0'].isel(event_index=event).values)
)

# test for peak area only
for i in range(15,75):
np.testing.assert_allclose(
(I_norm * np.nanmax(y_scatter_background_shifted))[i],
y_scatter_background_shifted[i],
atol=4500, # absolute tolerance ~ 10% of the max scattering signal value
)
2 changes: 2 additions & 0 deletions tests/test_vis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

my_sp2b = pysp2.io.read_sp2(pysp2.testing.EXAMPLE_SP2B_PSL, arm_convention=False)
my_ini = pysp2.io.read_config(pysp2.testing.EXAMPLE_INI_PSL)
#event = 213
event = 152

@pytest.mark.mpl_image_compare(tolerance=10)
Expand Down Expand Up @@ -100,6 +101,7 @@ def test_plot_incident_irradiance():
sigma_ds=sigma_ds,
time_units="us",
)

fig = ax.figure

return fig
Loading