1313
1414from .bopdmd import BOPDMD
1515from .hankeldmd import HankelDMD
16+ from .havok import HAVOK
17+ from .preprocessing import PrePostProcessingDMD
1618
1719mpl .rcParams ["figure.max_open_warning" ] = 0
1820
@@ -532,30 +534,51 @@ def plot_snapshots_2D(
532534
533535def plot_summary (
534536 dmd ,
537+ * ,
538+ continuous = False ,
535539 snapshots_shape = None ,
536540 index_modes = None ,
537541 filename = None ,
538542 order = "C" ,
539543 figsize = (12 , 8 ),
540- mode_colors = None ,
544+ dpi = 200 ,
545+ tight_layout_kwargs = None ,
546+ main_colors = ("r" , "b" , "g" , "gray" ),
547+ mode_color = "k" ,
548+ mode_cmap = "bwr" ,
549+ dynamics_color = "tab:blue" ,
550+ sval_ms = 8 ,
551+ max_eig_ms = 10 ,
552+ max_sval_plot = 50 ,
553+ title_fontsize = 14 ,
554+ label_fontsize = 12 ,
555+ plot_semilogy = False ,
556+ remove_cmap_ticks = False ,
541557):
542558 """
543- Generate a 3x3 summarizing plot that contains the following components:
559+ Generate a 3 x 3 summarizing plot that contains the following components:
544560 - the singular value spectrum of the data
545- - the discrete-time and continuous-time dmd eigenvalues
546- - the three dmd modes specified by the index_modes parameter
561+ - the discrete-time and continuous-time DMD eigenvalues
562+ - the three DMD modes specified by the ` index_modes` parameter
547563 - the dynamics corresponding with each plotted mode
548564 Eigenvalues, modes, and dynamics are ordered according to the magnitude of
549565 their corresponding amplitude value. Singular values and eigenvalues that
550566 are associated with plotted modes and dynamics are also highlighted.
551567
552568 :param dmd: DMD instance.
553569 :type dmd: pydmd.DMDBase
554- :param snapshots_shape: Shape of the snapshots. If not provided, snapshots
555- and modes are assumed to be 1D and the data snapshot length is used.
556- :type snapshots_shape: int or tuple(int,int)
557- :param index_modes: The indices of the modes to plot. By default, the first
558- three leading modes are plotted.
570+ :param continuous: Whether or not the eigenvalues of the given DMD instance
571+ are continuous-time. If `False`, the eigenvalues are assumed to be the
572+ discrete-time eigenvalues. If `True`, the eigenvalues are taken to be
573+ the continuous-time eigenvalues. Note that `continuous` is
574+ automatically assumed to be `True` if a `BOPDMD` model is given.
575+ :type continuous: bool
576+ :param snapshots_shape: Shape of the snapshots. If not provided, the shape
577+ of the snapshots and modes is assumed to be the flattened space dim of
578+ the snapshot data.
579+ :type snapshots_shape: tuple(int, int)
580+ :param index_modes: A list of the indices of the modes to plot. By default,
581+ the first three leading modes are plotted.
559582 :type index_modes: list
560583 :param filename: If specified, the plot is saved at `filename`.
561584 :type filename: str
@@ -573,55 +596,124 @@ def plot_summary(
573596 "C" is used by default.
574597 :type order: {"C", "F", "A"}
575598 :param figsize: Tuple in inches defining the figure size.
576- Deafult is (12,8).
577- :type figsize: tuple(int,int)
578- :param mode_colors: List of strings defining the colors used to denote
599+ :type figsize: tuple(int, int)
600+ :param dpi: Figure resolution.
601+ :type dpi: int
602+ :param tight_layout_kwargs: Optional dictionary of
603+ `matplotlib.pyplot.tight_layout()` parameters.
604+ :type tight_layout_kwargs: dict
605+ :param main_colors: Tuple of strings defining the colors used to denote
579606 eigenvalue, mode, dynamics associations. The first three colors are
580607 used to highlight the singular values and eigenvalues associated with
581608 the plotted modes and dynamics, while the fourth color is used to
582- denote all other singular values and eigenvalues. Default colors are
583- ["r","b","g","gray"].
584- :type mode_colors: list(str,str,str,str)
609+ denote all other values.
610+ :type main_colors: tuple(str, str, str, str)
611+ :param mode_color: Color used to plot the modes, if modes are 1D.
612+ :type mode_color: str
613+ :param mode_cmap: Colormap used to plot the modes, if modes are 2D.
614+ :type mode_cmap: str
615+ :param dynamics_color: Color used to plot the dynamics.
616+ :type dynamics_color: str
617+ :param sval_ms: Marker size of all singular values.
618+ :type sval_ms: int
619+ :param max_eig_ms: Marker size of the most prominent eigenvalue. The marker
620+ sizes of all other eigenvalues are then scaled according to eigenvalue
621+ prominence.
622+ :type max_eig_ms: int
623+ :param max_sval_plot: Maximum number of singular values to plot.
624+ :type max_sval_plot: int
625+ :param title_fontsize: Fontsize used for subplot titles.
626+ :type title_fontsize: int
627+ :param label_fontsize: Fontsize used for axis labels.
628+ :type label_fontsize: int
629+ :param plot_semilogy: Whether or not to plot the singular values on a
630+ semilogy plot. If `True`, a semilogy plot is used.
631+ :type plot_semilogy: bool
632+ :param remove_cmap_ticks: Whether or not to include the ticks on 2D mode
633+ plots. If `True`, ticks are removed from all 2D mode plots.
634+ :type remove_cmap_ticks: bool
585635 """
636+
637+ # This plotting method is inappropriate for plotting HAVOK results.
638+ if isinstance (dmd , HAVOK ):
639+ raise ValueError ("You should use HAVOK.plot_summary() instead." )
640+
641+ # Check that the DMD instance has been fitted.
586642 if dmd .modes is None :
587643 raise ValueError (
588644 "The modes have not been computed."
589- "You have to perform the fit method ."
645+ "You need to perform fit() first ."
590646 )
591647
648+ # By default, snapshots_shape is the flattened space dimension.
592649 if snapshots_shape is None :
593650 snapshots_shape = (len (dmd .snapshots ),)
594- elif isinstance (snapshots_shape , int ):
595- snapshots_shape = (snapshots_shape ,)
651+ # Only 2D tuples are admissible for snapshots_shape.
596652 elif not isinstance (snapshots_shape , tuple ) or len (snapshots_shape ) != 2 :
597- raise ValueError ("snapshots_shape must be an int or a 2D tuple." )
653+ raise ValueError ("snapshots_shape must be None or a 2D tuple." )
598654
655+ # Override index_modes if there are less than 3 modes available.
599656 if len (dmd .eigs ) < 3 :
600- # Even if index_modes is provided, override it if there are fewer than
601- # three modes available. Alert the user of this plot alteration.
602657 warnings .warn (
603658 "Provided dmd model has less than 3 modes."
604659 "Plotting all available modes."
605660 )
606661 index_modes = list (range (len (dmd .eigs )))
662+ # By default, we plot the 3 leading modes and their dynamics.
607663 elif index_modes is None :
608664 index_modes = list (range (3 ))
665+ # index_modes was provided - check its type and its length.
609666 elif not isinstance (index_modes , list ) or len (index_modes ) > 3 :
610667 raise ValueError ("index_modes must be a list of length at most 3." )
611- elif np . any ( np . array ( index_modes ) >= 50 ):
612- raise ValueError ( "Cannot view past the 50th mode." )
613-
614- if mode_colors is None :
615- mode_colors = [ "r" , "b" , "g" , "gray" ]
668+ # Indices cannot go past the total number of available or plottable modes.
669+ elif np . any ( np . array ( index_modes ) >= min ( len ( dmd . eigs ), max_sval_plot )):
670+ raise ValueError (
671+ f"Cannot view past mode { min ( len ( dmd . eigs ), max_sval_plot ) } ."
672+ )
616673
617- # Order the DMD eigenvalues, modes, and dynamics according to amplitude.
674+ # Sort eigenvalues, modes, and dynamics according to amplitude magnitude .
618675 mode_order = np .argsort (- np .abs (dmd .amplitudes ))
619676 lead_eigs = dmd .eigs [mode_order ]
620677 lead_modes = dmd .modes [:, mode_order ]
621678 lead_dynamics = dmd .dynamics [mode_order ]
622- if isinstance (dmd , BOPDMD ):
623- # BOPDMD computes continuous-time eigenvalues.
624- lead_eigs = np .exp (lead_eigs )
679+
680+ # Get time information for eigenvalue conversions.
681+ if isinstance (dmd , BOPDMD ) or (
682+ isinstance (dmd , PrePostProcessingDMD )
683+ and isinstance (dmd .pre_post_processed_dmd , BOPDMD )
684+ ):
685+ # BOPDMD models store time in the time attribute.
686+ # BOPDMD models also always compute continuous-time eigenvalues.
687+ cont_eigs = lead_eigs
688+ time = dmd .time
689+ dt = dmd .time [1 ] - dmd .time [0 ]
690+ if not np .allclose (dmd .time [1 :] - dmd .time [:- 1 ], dt ):
691+ warnings .warn (
692+ "Time step is not uniform. "
693+ "No discrete-time eigenvalues to plot..."
694+ )
695+ disc_eigs = None
696+ else :
697+ disc_eigs = np .exp (lead_eigs * dt )
698+ else :
699+ # For all other dmd models, go to the TimeDict for time information.
700+ try :
701+ time = dmd .dmd_timesteps
702+ dt = dmd .original_time ["dt" ]
703+ except AttributeError :
704+ warnings .warn (
705+ "No time step information available. "
706+ "Using dt = 1 and t0 = 0."
707+ )
708+ time = np .arange (dmd .snapshots .shape [- 1 ])
709+ dt = 1.0
710+
711+ if continuous :
712+ cont_eigs = lead_eigs
713+ disc_eigs = np .exp (lead_eigs * dt )
714+ else :
715+ disc_eigs = lead_eigs
716+ cont_eigs = np .log (lead_eigs ) / dt
625717
626718 # Compute the singular values of the data matrix.
627719 if isinstance (dmd , HankelDMD ):
@@ -636,81 +728,97 @@ def plot_summary(
636728
637729 # Generate the summarizing plot.
638730 fig , (eig_axes , mode_axes , dynamics_axes ) = plt .subplots (
639- 3 , 3 , figsize = figsize , dpi = 200
731+ 3 , 3 , figsize = figsize , dpi = dpi
640732 )
641733
642- # Plot 1: Plot the singular value spectrum (plot at most 50 values).
643- s_var_plot = s_var [:50 ]
644- eig_axes [0 ].set_title ("Singular Values" )
645- eig_axes [0 ].set_ylabel ("% variance" )
646- t = np .arange (len (s_var_plot )) + 1
647- eig_axes [0 ].plot (t , s_var_plot , "o" , c = mode_colors [- 1 ], ms = 8 , mec = "k" )
734+ # PLOT 1: Plot the singular value spectrum.
735+ s_var_plot = s_var [:max_sval_plot ]
736+ eig_axes [0 ].set_title ("Singular Values" , fontsize = title_fontsize )
737+ eig_axes [0 ].set_ylabel ("% variance" , fontsize = label_fontsize )
738+ s_t = np .arange (len (s_var_plot )) + 1
739+ eig_axes [0 ].plot (
740+ s_t , s_var_plot , "o" , c = main_colors [- 1 ], ms = sval_ms , mec = "k"
741+ )
648742 for i , idx in enumerate (index_modes ):
649743 eig_axes [0 ].plot (
650- t [idx ], s_var_plot [idx ], "o" , c = mode_colors [i ], ms = 8 , mec = "k"
744+ s_t [idx ],
745+ s_var_plot [idx ],
746+ "o" ,
747+ c = main_colors [i ],
748+ ms = sval_ms ,
749+ mec = "k" ,
651750 )
751+ if plot_semilogy :
752+ eig_axes [0 ].semilogy ()
652753
653- # Plots 2-3: Plot the eigenvalues (discrete-time and continuous-time).
754+ # PLOTS 2-3: Plot the eigenvalues (discrete-time and continuous-time).
654755 # Scale marker sizes to reflect the amount of variance captured.
655- max_marker_size = 10
656- ms_vals = max_marker_size * np .sqrt (s_var / s_var [0 ])
657- for i , ax in enumerate (eig_axes [1 :]):
756+ ms_vals = max_eig_ms * np .sqrt (s_var / s_var [0 ])
757+ for i , (ax , eigs ) in enumerate (zip (eig_axes [1 :], [disc_eigs , cont_eigs ])):
658758 # Plot the complex plane axes.
659759 ax .axvline (x = 0 , c = "k" , lw = 1 )
660760 ax .axhline (y = 0 , c = "k" , lw = 1 )
661761 ax .axis ("equal" )
662- # Plot 2: Plot the discrete-time eigenvalues with the unit circle.
762+ # PLOT 2: Plot the discrete-time eigenvalues on the unit circle.
663763 if i == 0 :
664- ax .set_title ("Discrete-time Eigenvalues" )
665- eigs = lead_eigs
764+ ax .set_title ("Discrete-time Eigenvalues" , fontsize = title_fontsize )
666765 t = np .linspace (0 , 2 * np .pi , 100 )
667766 ax .plot (np .cos (t ), np .sin (t ), c = "tab:blue" , ls = "--" )
668- ax .set_xlabel ("Real" )
669- ax .set_ylabel ("Imag" )
670- # Plot 3: Plot the continuous-time eigenvalues
767+ ax .set_xlabel (r"$Re(\lambda)$" , fontsize = label_fontsize )
768+ ax .set_ylabel (r"$Im(\lambda)$" , fontsize = label_fontsize )
769+ # PLOT 3: Plot the continuous-time eigenvalues.
671770 else :
672- ax .set_title ("Continuous-time Eigenvalues" )
673- eigs = np .log (lead_eigs )
674- ax .set_xlabel ("Imag" )
675- ax .set_ylabel ("Real" )
676- # Plot the eigenvalues.
677- for idx , eig in enumerate (eigs ):
678- if idx in index_modes :
679- color = mode_colors [index_modes .index (idx )]
680- else :
681- color = mode_colors [- 1 ]
682- if i == 0 :
683- ax .plot (eig .real , eig .imag , "o" , c = color , ms = ms_vals [idx ])
684- else :
685- ax .plot (eig .imag , eig .real , "o" , c = color , ms = ms_vals [idx ])
686-
687- # Plots 4-6: Plot the DMD modes.
688- for i , idx in enumerate (index_modes ):
689- ax = mode_axes [i ]
690- ax .set_title (f"Mode { idx + 1 } " , c = mode_colors [i ], fontsize = 15 )
771+ ax .set_title ("Continuous-time Eigenvalues" , fontsize = title_fontsize )
772+ ax .set_xlabel (r"$Im(\omega)$" , fontsize = label_fontsize )
773+ ax .set_ylabel (r"$Re(\omega)$" , fontsize = label_fontsize )
774+ # Plot the eigenvalues (discrete or continuous).
775+ if eigs is not None :
776+ for idx , eig in enumerate (eigs ):
777+ if idx in index_modes :
778+ color = main_colors [index_modes .index (idx )]
779+ else :
780+ color = main_colors [- 1 ]
781+ if i == 0 :
782+ ax .plot (eig .real , eig .imag , "o" , c = color , ms = ms_vals [idx ])
783+ else :
784+ ax .plot (eig .imag , eig .real , "o" , c = color , ms = ms_vals [idx ])
785+
786+ # PLOTS 4-6: Plot the DMD modes.
787+ for i , (ax , idx ) in enumerate (zip (mode_axes , index_modes )):
788+ ax .set_title (
789+ f"Mode { idx + 1 } " , c = main_colors [i ], fontsize = title_fontsize
790+ )
691791 # Plot modes in 1D.
692792 if len (snapshots_shape ) == 1 :
693- ax .plot (lead_modes [:, idx ].real , c = "tab:orange" )
793+ ax .plot (lead_modes [:, idx ].real , c = mode_color )
694794 # Plot modes in 2D.
695795 else :
696796 mode = lead_modes [:, idx ].reshape (* snapshots_shape , order = order )
697- # Multiply by factor of 0.9 to intensify the plotted image.
698- vmax = 0.9 * np .abs (mode .real ).max ()
699- im = ax .imshow (mode .real , vmax = vmax , vmin = - vmax , cmap = "bwr" )
797+ vmax = np .abs (mode .real ).max ()
798+ im = ax .imshow (mode .real , vmax = vmax , vmin = - vmax , cmap = mode_cmap )
700799 # Align the colorbar with the plotted image.
701800 divider = make_axes_locatable (ax )
702801 cax = divider .append_axes ("right" , size = "3%" , pad = 0.05 )
703802 fig .colorbar (im , cax = cax )
704-
705- # Plots 7-9: Plot the DMD mode dynamics.
706- for i , idx in enumerate (index_modes ):
707- ax = dynamics_axes [i ]
708- ax .set_title ("Mode Dynamics" , c = mode_colors [i ], fontsize = 12 )
709- ax .plot (lead_dynamics [idx ].real )
710- ax .set_xlabel ("Time" )
803+ if remove_cmap_ticks :
804+ ax .set_xticks ([])
805+ ax .set_yticks ([])
806+
807+ # PLOTS 7-9: Plot the DMD mode dynamics.
808+ for i , (ax , idx ) in enumerate (zip (dynamics_axes , index_modes )):
809+ dynamics_data = lead_dynamics [idx ].real
810+ ax .set_title ("Mode Dynamics" , c = main_colors [i ], fontsize = title_fontsize )
811+ ax .plot (time , dynamics_data , c = dynamics_color )
812+ ax .set_xlabel ("Time" , fontsize = label_fontsize )
813+ dynamics_range = dynamics_data .max () - dynamics_data .min ()
814+ # Re-adjust ylim if dynamics oscillations are extremely small.
815+ if dynamics_range / np .abs (np .average (dynamics_data )) < 1e-4 :
816+ ax .set_ylim (np .sort ([0.0 , 2 * np .average (dynamics_data )]))
711817
712818 # Padding between elements.
713- plt .tight_layout ()
819+ if tight_layout_kwargs is None :
820+ tight_layout_kwargs = {}
821+ plt .tight_layout (** tight_layout_kwargs )
714822
715823 # Save plot if filename is provided.
716824 if filename :
0 commit comments