ray_geodesics.py

Visualization of geodesics.


Requires Python packages/modules:

class gme.plot.ray_geodesics.RayGeodesics(gmes: gme.ode.time_invariant.TimeInvariantSolution, gmeq: gme.core.equations_extended.EquationsGeodesic, n_points: int, do_recompute: bool = False)

Visualization of geodesics.

Extends gme.plot.base.Graphing.

Parameters
  • gmes – instance of single-ray solution class defined in gme.ode.single_ray

  • gmeq

    GME model equations class instance defined in

    gme.core.equations or similar

  • n_points – optional sample rate along each curve

  • do_recompute – optionally redo (slow) computation of metric tensors?

__init__(gmes: gme.ode.time_invariant.TimeInvariantSolution, gmeq: gme.core.equations_extended.EquationsGeodesic, n_points: int, do_recompute: bool = False)None

Initialize: constructor method.

profile_g_properties(gmes: gme.ode.time_invariant.TimeInvariantSolution, gmeq: gme.core.equations_extended.EquationsGeodesic, sub: Dict, name: str, fig_size: Optional[Tuple[float, float]] = None, dpi: Optional[int] = None, y_limits: Optional[Tuple[Optional[float], Optional[float]]] = None, do_gstar: bool = False, do_det: bool = False, do_eigenvectors: bool = False, eta_label_xy: Optional[Tuple[float, float]] = None, do_etaxi_label: bool = True, legend_loc: str = 'lower left', do_pv: bool = False)None

Plot velocity \(\dot{r}\) along a ray.

Parameters
  • gmes – instance of single ray solution class defined in gme.ode.single_ray

  • gmeq – GME model equations class instance defined in gme.core.equations

  • n_points – sample rate along each curve

Code

"""
Visualization of geodesics.

---------------------------------------------------------------------

Requires Python packages/modules:
  -  :mod:`NumPy <numpy>`
  -  :mod:`SciPy <scipy>`
  -  :mod:`SymPy <sympy>`
  -  :mod:`MatPlotLib <matplotlib>`
  -  `GME`_

.. _GMPLib: https://github.com/geomorphysics/GMPLib
.. _GME: https://github.com/geomorphysics/GME
.. _Matrix:
    https://docs.sympy.org/latest/modules/matrices/immutablematrices.html

---------------------------------------------------------------------
"""
# Library
import warnings
from typing import Tuple, Dict, List, Optional

# NumPy
import numpy as np

# Scipy utils
from scipy.linalg import eigh, det

# SymPy
from sympy import deg, re, Matrix

# MatPlotLib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

# GME
from gme.core.symbols import Ci, rx, rdotx, rdotz
from gme.core.equations_extended import EquationsGeodesic
from gme.ode.time_invariant import TimeInvariantSolution
from gme.plot.base import Graphing

warnings.filterwarnings("ignore")

__all__ = ["RayGeodesics"]


class RayGeodesics(Graphing):
    """
    Visualization of geodesics.

    Extends :class:`gme.plot.base.Graphing`.

    Args:
        gmes:
            instance of single-ray solution class
            defined in :mod:`gme.ode.single_ray`
        gmeq:
            GME model equations class instance defined in
                :mod:`gme.core.equations` or similar
        n_points:
            optional sample rate along each curve
        do_recompute:
            optionally redo (slow) computation of metric tensors?
    """

    # Definitions
    x_array: np.ndarray
    t_array: np.ndarray
    rz_array: np.ndarray
    vx_array: np.ndarray
    vz_array: np.ndarray
    gstar_matrices_list: List
    gstar_matrices_array: List
    g_matrices_list: List
    g_matrices_array: List

    def __init__(
        self,
        gmes: TimeInvariantSolution,
        gmeq: EquationsGeodesic,
        n_points: int,
        do_recompute: bool = False,
    ) -> None:
        r"""Initialize: constructor method."""
        super().__init__()
        # if not hasattr(self,'x_array') or n_points!=len(self.x_array):
        # HACK!!!
        do_recompute = True
        print("(Re)computing g matrices")

        rx_array = gmes.rx_array
        x_min, x_max = rx_array[0], rx_array[-1]
        # HACK!!!
        self.x_array = np.linspace(x_min, x_max, n_points)  # \
        # if do_recompute or not hasattr(self,'x_array') else self.x_array
        self.t_array = gmes.t_interp_x(self.x_array)  # \
        # if do_recompute or not hasattr(self,'t_array') else self.t_array
        self.rz_array = gmes.rz_interp(self.x_array)  # \
        # if do_recompute or not hasattr(self,'rz_array') else self.rz_array
        self.vx_array = gmes.rdotx_interp(self.x_array)  # \
        # if do_recompute or not hasattr(self,'vx_array') else self.vx_array
        self.vz_array = gmes.rdotz_interp(self.x_array)  # \
        # if do_recompute or not hasattr(self,'vz_array') else self.vz_array
        x_array = self.x_array
        # t_array  = self.t_array
        # rz_array = self.rz_array
        vx_array = self.vx_array
        vz_array = self.vz_array

        if do_recompute:
            self.gstar_matrices_list: List[Matrix] = []
            self.gstar_matrices_array: List[np.ndarray] = []
            self.g_matrices_list: List[Matrix] = []
            self.g_matrices_array: List[np.ndarray] = []
        if not hasattr(gmeq, "gstar_ij_mat"):
            return
        try:
            self.gstar_matrices_list = [
                gmeq.gstar_ij_mat.subs({rx: x_, rdotx: vx_, rdotz: vz_})
                for x_, vx_, vz_ in zip(x_array, vx_array, vz_array)
            ]
        except ValueError as e:
            print(f'Failed to (re)generate gstar_matrices_list: "{e}"')

        try:
            self.gstar_matrices_array = [
                np.array([float(re(elem_)) for elem_ in g_]).reshape(2, 2)
                for g_ in self.gstar_matrices_list
            ]
        except ValueError as e:
            print(f'Failed to (re)generate gstar_matrices_array: "{e}"')

        try:
            self.g_matrices_list = [
                gmeq.g_ij_mat.subs({rx: x_, rdotx: vx_, rdotz: vz_})
                for x_, vx_, vz_ in zip(x_array, vx_array, vz_array)
            ]
        except ValueError as e:
            print(f'Failed to (re)generate g_matrices_list: "{e}"')

        try:
            self.g_matrices_array = [
                np.array([float(re(elem_)) for elem_ in g_]).reshape(2, 2)
                for g_ in self.g_matrices_list
            ]
        except ValueError as e:
            print(f'Failed to (re)generate g_matrices_array: "{e}"')

    def profile_g_properties(
        self,
        gmes: TimeInvariantSolution,
        gmeq: EquationsGeodesic,
        sub: Dict,
        name: str,
        fig_size: Optional[Tuple[float, float]] = None,
        dpi: Optional[int] = None,
        y_limits: Optional[Tuple[Optional[float], Optional[float]]] = None,
        # n_points=121,
        # do_pub_label=False, pub_label='',
        do_gstar: bool = False,
        do_det: bool = False,
        do_eigenvectors: bool = False,
        eta_label_xy: Optional[Tuple[float, float]] = None,
        do_etaxi_label: bool = True,
        legend_loc: str = "lower left",
        # do_mod_v=False,
        # do_recompute=False
        do_pv: bool = False,
    ) -> None:
        r"""
        Plot velocity :math:`\dot{r}` along a ray.

        Args:
            gmes:
                instance of single ray solution class defined in
                :mod:`gme.ode.single_ray`
            gmeq:
                GME model equations class instance defined in
                :mod:`gme.core.equations`
            n_points: sample rate along each curve
        """
        _ = self.create_figure(name, fig_size=fig_size, dpi=dpi)
        y_limits_: Tuple[Optional[float], Optional[float]] = (
            (None, None) if y_limits is None else y_limits
        )
        axes = plt.gca()

        # HACK
        # self.prep_g_arrays(gmes, gmeq, n_points, do_recompute)

        if do_gstar:
            g_matrices_array = self.gstar_matrices_array
        else:
            g_matrices_array = self.g_matrices_array
        x_array = self.x_array
        # t_array  = self.t_array
        rz_array = self.rz_array
        vx_array = self.vx_array
        vz_array = self.vz_array

        if do_gstar:
            # Use of lambdified g matrix here fails for eta=1/4, sin(beta)
            #    for some reason
            # g_matrices_list = [gmeq.gstar_ij_mat_lambdified(x_,vx_,vz_)
            #                for x_,vx_,vz_ in zip(x_array,vx_array,vz_array)]
            g_label = "{g^*}"
            m_label = "co-metric"
            h_label = "H"
            eta_label_xy_: Tuple[float, float] = (
                (0.5, 0.2) if eta_label_xy is None else eta_label_xy
            )
        else:
            # Use of lambdified g* matrix here fails for eta=1/4, sin(beta)
            #   for some reason
            # g_matrices_list = [gmeq.g_ij_mat_lambdified(x_,vx_,vz_)
            #               for x_,vx_,vz_ in zip(x_array,vx_array,vz_array)]
            g_label = "{g}"
            m_label = "metric"
            h_label = "L"
            eta_label_xy_ = (
                (0.5, 0.85) if eta_label_xy is None else eta_label_xy
            )
        # g_eigenvalues_array
        #  = np.array([np.real(eig(g_)[0]) for g_ in g_matrices_array])
        # The metric tensor matrices are symmetric therefore Hermitian
        #  so we can use 'eigh'
        # print(f'g_matrices_array = {g_matrices_array}')
        if g_matrices_array is not None:
            g_eigh_array: Optional[List] = [eigh(g_) for g_ in g_matrices_array]
            g_det_array = np.array([det(g_) for g_ in g_matrices_array])
        else:
            g_eigh_array = None
            g_det_array = None
        if g_eigh_array is not None:
            g_eigenvalues_array = np.real(
                np.array([g_eigh_[0] for g_eigh_ in g_eigh_array])
            )
            g_eigenvectors_array = np.real(
                np.array([g_eigh_[1] for g_eigh_ in g_eigh_array])
            )
        else:
            g_eigenvalues_array = None
            g_eigenvectors_array = None
        if do_eigenvectors and g_eigenvectors_array is not None:
            plt.plot(x_array, rz_array, "0.6", ls="-", lw=3, label=r"ray")
            plt.ylabel(r"Eigenvectors of $" + g_label + "$", fontsize=14)
            arrow_sf = 0.5
            my_arrow_style = mpatches.ArrowStyle.Fancy(
                head_length=0.99 * arrow_sf,
                head_width=0.6 * arrow_sf,
                tail_width=0.01 * arrow_sf,
            )
            step = 8
            off = 0 * step // 2
            ev_sf = 0.04
            zipped_arrays = zip(
                x_array[off::step],
                rz_array[off::step],
                g_eigenvectors_array[off::step],
            )
            for x_, rz_, evs_ in zipped_arrays:
                xy_ = np.array([x_, rz_])
                for pm in (-1, +1):
                    axes.annotate(
                        "",
                        xy=xy_ + pm * evs_[0] * ev_sf,
                        xytext=xy_,
                        arrowprops={
                            "arrowstyle": my_arrow_style,
                            "color": "magenta",
                        },
                    )
                    axes.annotate(
                        "",
                        xy=xy_ + pm * evs_[1] * ev_sf,
                        xytext=xy_,
                        arrowprops={
                            "arrowstyle": my_arrow_style,
                            "color": "DarkGreen",
                        },
                    )
            plt.plot(0, 0, "DarkGreen", ls="-", lw=1.5, label="eigenvector 0")
            plt.plot(0, 0, "magenta", ls="-", lw=1.5, label=r"eigenvector 1")
            axes.set_aspect(1)
        elif do_det and g_det_array is not None:
            plt.plot(
                x_array,
                g_det_array,
                "DarkBlue",
                ls="-",
                lw=1.5,
                label=r"$\det(" + g_label + ")$",
            )
            plt.ylabel(
                r"Det of $" + g_label + "$ (Hessian of $" + h_label + "$)",
                fontsize=14,
            )
        elif do_pv:
            px_array = gmes.px_interp(x_array)
            pz_array = gmes.pz_interp(x_array)
            pv_array = px_array * vx_array + pz_array * vz_array
            plt.plot(x_array, pv_array, "r", ls="-", lw=2, label=r"$p_i v^i$")
            if self.gstar_matrices_array is not None:
                gstarpp_array = [
                    np.dot(
                        np.dot(gstar_, np.array([px_, pz_])),
                        np.array([px_, pz_]),
                    )
                    for gstar_, px_, pz_ in zip(
                        self.gstar_matrices_array, px_array, pz_array
                    )
                ]
                plt.plot(
                    x_array,
                    gstarpp_array,
                    "0.5",
                    ls="--",
                    lw=3,
                    label=r"$g^j p_j p_j$",
                )
            if self.g_matrices_array is not None:
                gvv_array = [
                    np.dot(
                        np.dot(g_, np.array([vx_, vz_])), np.array([vx_, vz_])
                    )
                    for g_, vx_, vz_ in zip(
                        self.g_matrices_array, vx_array, vz_array
                    )
                ]
                plt.plot(
                    x_array, gvv_array, "k", ls=":", lw=4, label=r"$g_i v^iv^i$"
                )
            plt.ylabel(
                r"Inner product of $\mathbf{\widetilde{p}}$"
                + r" and $\mathbf{{v}}$",
                fontsize=14,
            )
            legend_loc = "upper left"
        elif g_eigenvalues_array is not None:
            (sign_ev0, label_ev0) = (
                (-1, "negative  ")
                if g_eigenvalues_array[0, 0] < 0
                else (1, "positive  ")
            )
            (sign_ev1, label_ev1) = (
                (-1, "negative  ")
                if g_eigenvalues_array[0, 1] < 0
                else (1, "positive  ")
            )
            plt.yscale("log")
            plt.plot(
                x_array,
                sign_ev1 * (g_eigenvalues_array[:, 1]),
                "DarkGreen",
                ls="-",
                lw=1.5,
                label=f"{label_ev1}" + rf"$\lambda_{g_label}(1)$",
            )
            plt.plot(
                x_array,
                sign_ev0 * (g_eigenvalues_array[:, 0]),
                "magenta",
                ls="-",
                lw=1.5,
                label=f"{label_ev0}" + rf"$\lambda_{g_label}(0)$",
            )
            plt.ylabel(
                f"Eigenvalues of {m_label} tensor " + rf"${g_label}$",
                fontsize=12,
            )
        else:
            return

        if do_eigenvectors:
            axes.set_ylim(*y_limits_)
        elif do_det:
            ylim = plt.ylim()
            if ylim[1] < 0:
                axes.set_ylim(ylim[0], 0)
            if ylim[0] > 0:
                axes.set_ylim(0, ylim[1])
            # axes.set_ylim( -(ylim[1]-ylim[0])/20,ylim[1] )
        elif do_pv:
            axes.set_ylim(0, 2)
        plt.grid(True, ls=":")
        plt.xlabel(r"Distance, $x/L_{\mathrm{c}}$  [-]", fontsize=14)
        # axes.set_ylim(ylim[0]*1.1,-0)
        plt.legend(loc=legend_loc, fontsize=12, framealpha=0.95)
        if do_etaxi_label:
            plt.text(
                *eta_label_xy_,
                rf"$\eta={gmeq.eta_}$"
                + r"$\quad\mathsf{Ci}=$"
                + rf"${round(float(deg(Ci.subs(sub))))}\degree$",
                transform=axes.transAxes,
                horizontalalignment="center",
                verticalalignment="center",
                fontsize=14,
                color="k",
            )


#