Skip to content

Commit a8d1079

Browse files
committed
More plot_summary updates
1 parent 0e0a0c5 commit a8d1079

File tree

1 file changed

+82
-54
lines changed

1 file changed

+82
-54
lines changed

pydmd/plotter.py

Lines changed: 82 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .bopdmd import BOPDMD
1515
from .hankeldmd import HankelDMD
1616
from .havok import HAVOK
17+
from .preprocessing import PrePostProcessingDMD
1718

1819
mpl.rcParams["figure.max_open_warning"] = 0
1920

@@ -545,6 +546,11 @@ def plot_summary(
545546
mode_color="k",
546547
mode_cmap="bwr",
547548
dynamics_color="tab:blue",
549+
sval_ms=8,
550+
max_eig_ms=10,
551+
max_sval_plot=50,
552+
title_fontsize=14,
553+
label_fontsize=12,
548554
plot_semilogy=False,
549555
remove_cmap_ticks=False,
550556
):
@@ -564,7 +570,7 @@ def plot_summary(
564570
are continuous-time. If `False`, the eigenvalues are assumed to be the
565571
discrete-time eigenvalues. If `True`, the eigenvalues are taken to be
566572
the continuous-time eigenvalues. Note that `continuous` is
567-
automatically assumed to be true if a `BOPDMD` model is given.
573+
automatically assumed to be `True` if a `BOPDMD` model is given.
568574
:type continuous: bool
569575
:param snapshots_shape: Shape of the snapshots. If not provided, the shape
570576
of the snapshots and modes is assumed to be the flattened space dim of
@@ -607,24 +613,25 @@ def plot_summary(
607613
:type mode_cmap: str
608614
:param dynamics_color: Color used to plot the dynamics.
609615
:type dynamics_color: str
616+
:param sval_ms: Marker size of all singular values.
617+
:type sval_ms: int
618+
:param max_eig_ms: Marker size of the most prominent eigenvalue.
619+
:type max_eig_ms: int
620+
:param max_sval_plot: Maximum number of singular values to plot.
621+
:type max_sval_plot: int
622+
:param title_fontsize: Fontsize used for subplot titles.
623+
:type title_fontsize: int
624+
:param label_fontsize: Fontsize used for axis labels.
625+
:type label_fontsize: int
610626
:param plot_semilogy: Whether or not to plot the singular values on a
611627
semilogy plot. If `True`, a semilogy plot is used.
612628
:type plot_semilogy: bool
613629
:param remove_cmap_ticks: Whether or not to include the ticks on 2D mode
614630
plots. If `True`, ticks are removed from all 2D mode plots.
615631
:type remove_cmap_ticks: bool
616632
"""
617-
# Other potential parameters to consider:
618-
# - ylims on the dynamics plots
619-
# - fontsizes
620-
621-
# All other potentially customizable values.
622-
# For now, we take them to be constants.
623-
max_plot = 50
624-
sval_ms = 8 # singular value marker size
625-
max_eig_ms = 10 # marker size of the most prominent eigenvalue
626-
vmax_scale = 0.9
627633

634+
# This plotting method is inappropriate for plotting HAVOK results.
628635
if isinstance(dmd, HAVOK):
629636
raise ValueError("You should use HAVOK.plot_summary() instead.")
630637

@@ -656,9 +663,9 @@ def plot_summary(
656663
elif not isinstance(index_modes, list) or len(index_modes) > 3:
657664
raise ValueError("index_modes must be a list of length at most 3.")
658665
# Indices cannot go past the total number of available or plottable modes.
659-
elif np.any(np.array(index_modes) >= min(len(dmd.eigs), max_plot)):
666+
elif np.any(np.array(index_modes) >= min(len(dmd.eigs), max_sval_plot)):
660667
raise ValueError(
661-
f"Cannot view past mode {min(len(dmd.eigs), max_plot)}."
668+
f"Cannot view past mode {min(len(dmd.eigs), max_sval_plot)}."
662669
)
663670

664671
# Sort eigenvalues, modes, and dynamics according to amplitude magnitude.
@@ -667,26 +674,34 @@ def plot_summary(
667674
lead_modes = dmd.modes[:, mode_order]
668675
lead_dynamics = dmd.dynamics[mode_order]
669676

670-
# Get time step information for eigenvalue conversions.
671-
if isinstance(dmd, BOPDMD):
677+
# Get time information for eigenvalue conversions.
678+
if isinstance(dmd, BOPDMD) or (
679+
isinstance(dmd, PrePostProcessingDMD)
680+
and isinstance(dmd.pre_post_processed_dmd, BOPDMD)
681+
):
672682
# BOPDMD models store time in the time attribute.
683+
# BOPDMD models also always compute continuous-time eigenvalues.
684+
cont_eigs = lead_eigs
673685
time = dmd.time
674686
dt = dmd.time[1] - dmd.time[0]
675687
if not np.allclose(dmd.time[1:] - dmd.time[:-1], dt):
676-
print(
688+
warnings.warn(
677689
"Time step is not uniform. "
678690
"No discrete-time eigenvalues to plot..."
679691
)
680692
disc_eigs = None
681693
else:
682694
disc_eigs = np.exp(lead_eigs * dt)
683-
cont_eigs = lead_eigs
684695
else:
696+
# For all other dmd models, go to the TimeDict for time information.
685697
try:
686-
time = dmd.original_timesteps
698+
time = dmd.dmd_timesteps
687699
dt = dmd.original_time["dt"]
688700
except AttributeError:
689-
warnings.warn("No time step information available. Using dt = 1.")
701+
warnings.warn(
702+
"No time step information available. "
703+
"Using dt = 1 and t0 = 0."
704+
)
690705
time = np.arange(dmd.snapshots.shape[-1])
691706
dt = 1.0
692707

@@ -714,14 +729,21 @@ def plot_summary(
714729
)
715730

716731
# PLOT 1: Plot the singular value spectrum.
717-
s_var_plot = s_var[:max_plot]
718-
eig_axes[0].set_title("Singular Values")
719-
eig_axes[0].set_ylabel("% variance")
720-
t = np.arange(len(s_var_plot)) + 1
721-
eig_axes[0].plot(t, s_var_plot, "o", c=main_colors[-1], ms=sval_ms, mec="k")
732+
s_var_plot = s_var[:max_sval_plot]
733+
eig_axes[0].set_title("Singular Values", fontsize=title_fontsize)
734+
eig_axes[0].set_ylabel("% variance", fontsize=label_fontsize)
735+
s_t = np.arange(len(s_var_plot)) + 1
736+
eig_axes[0].plot(
737+
s_t, s_var_plot, "o", c=main_colors[-1], ms=sval_ms, mec="k"
738+
)
722739
for i, idx in enumerate(index_modes):
723740
eig_axes[0].plot(
724-
t[idx], s_var_plot[idx], "o", c=main_colors[i], ms=sval_ms, mec="k"
741+
s_t[idx],
742+
s_var_plot[idx],
743+
"o",
744+
c=main_colors[i],
745+
ms=sval_ms,
746+
mec="k",
725747
)
726748
if plot_semilogy:
727749
eig_axes[0].semilogy()
@@ -731,44 +753,46 @@ def plot_summary(
731753
ms_vals = max_eig_ms * np.sqrt(s_var / s_var[0])
732754
for i, (ax, eigs) in enumerate(zip(eig_axes[1:], [disc_eigs, cont_eigs])):
733755
# Plot the complex plane axes.
734-
ax.axvline(x=0, c="k")
735-
ax.axhline(y=0, c="k")
756+
ax.axvline(x=0, c="k", lw=1)
757+
ax.axhline(y=0, c="k", lw=1)
736758
ax.axis("equal")
737-
# PLOT 2: Plot the discrete-time eigenvalues with the unit circle.
759+
# PLOT 2: Plot the discrete-time eigenvalues on the unit circle.
738760
if i == 0:
739-
ax.set_title("Discrete-time Eigenvalues")
761+
ax.set_title("Discrete-time Eigenvalues", fontsize=title_fontsize)
740762
t = np.linspace(0, 2 * np.pi, 100)
741763
ax.plot(np.cos(t), np.sin(t), c="tab:blue", ls="--")
742-
ax.set_xlabel("Real")
743-
ax.set_ylabel("Imag")
764+
ax.set_xlabel(r"$Re(\lambda)$", fontsize=label_fontsize)
765+
ax.set_ylabel(r"$Im(\lambda)$", fontsize=label_fontsize)
744766
# PLOT 3: Plot the continuous-time eigenvalues.
745767
else:
746-
ax.set_title("Continuous-time Eigenvalues")
747-
ax.set_xlabel("Imag")
748-
ax.set_ylabel("Real")
749-
# Plot the eigenvalues.
750-
for idx, eig in enumerate(eigs):
751-
if idx in index_modes:
752-
color = main_colors[index_modes.index(idx)]
753-
else:
754-
color = main_colors[-1]
755-
if i == 0:
756-
ax.plot(eig.real, eig.imag, "o", c=color, ms=ms_vals[idx])
757-
else:
758-
ax.plot(eig.imag, eig.real, "o", c=color, ms=ms_vals[idx])
768+
ax.set_title("Continuous-time Eigenvalues", fontsize=title_fontsize)
769+
ax.set_xlabel(r"$Im(\omega)$", fontsize=label_fontsize)
770+
ax.set_ylabel(r"$Re(\omega)$", fontsize=label_fontsize)
771+
# Plot the eigenvalues (discrete or continuous).
772+
if eigs is not None:
773+
for idx, eig in enumerate(eigs):
774+
if idx in index_modes:
775+
color = main_colors[index_modes.index(idx)]
776+
else:
777+
color = main_colors[-1]
778+
if i == 0:
779+
ax.plot(eig.real, eig.imag, "o", c=color, ms=ms_vals[idx])
780+
else:
781+
ax.plot(eig.imag, eig.real, "o", c=color, ms=ms_vals[idx])
759782

760783
# PLOTS 4-6: Plot the DMD modes.
761-
for i, idx in enumerate(index_modes):
762-
ax = mode_axes[i]
763-
ax.set_title(f"Mode {idx + 1}", c=main_colors[i])
784+
for i, (ax, idx) in enumerate(zip(mode_axes, index_modes)):
785+
ax.set_title(
786+
f"Mode {idx + 1}", c=main_colors[i], fontsize=title_fontsize
787+
)
764788
# Plot modes in 1D.
765789
if len(snapshots_shape) == 1:
766790
ax.plot(lead_modes[:, idx].real, c=mode_color)
767791
# Plot modes in 2D.
768792
else:
769793
mode = lead_modes[:, idx].reshape(*snapshots_shape, order=order)
770-
# Multiply by factor of vmax_scale to intensify the plotted image.
771-
vmax = vmax_scale * np.abs(mode.real).max()
794+
# Multiply by factor of 0.9 to intensify the plotted image.
795+
vmax = 0.9 * np.abs(mode.real).max()
772796
im = ax.imshow(mode.real, vmax=vmax, vmin=-vmax, cmap=mode_cmap)
773797
# Align the colorbar with the plotted image.
774798
divider = make_axes_locatable(ax)
@@ -779,11 +803,15 @@ def plot_summary(
779803
ax.set_yticks([])
780804

781805
# PLOTS 7-9: Plot the DMD mode dynamics.
782-
for i, idx in enumerate(index_modes):
783-
ax = dynamics_axes[i]
784-
ax.set_title("Mode Dynamics", c=main_colors[i])
785-
ax.plot(time, lead_dynamics[idx].real, c=dynamics_color)
786-
ax.set_xlabel("Time")
806+
for i, (ax, idx) in enumerate(zip(dynamics_axes, index_modes)):
807+
dynamics_data = lead_dynamics[idx].real
808+
ax.set_title("Mode Dynamics", c=main_colors[i], fontsize=title_fontsize)
809+
ax.plot(time, dynamics_data, c=dynamics_color)
810+
ax.set_xlabel("Time", fontsize=label_fontsize)
811+
dynamics_range = dynamics_data.max() - dynamics_data.min()
812+
# Re-adjust ylim if dynamics oscillations are extremely small.
813+
if dynamics_range / np.abs(np.average(dynamics_data)) < 1e-4:
814+
ax.set_ylim([0.0, 2 * np.average(dynamics_data)])
787815

788816
# Padding between elements.
789817
if tight_layout_kwargs is None:

0 commit comments

Comments
 (0)