""" Functions to plot :class:`pyarts.arts.Ppath` in different ways """
import pyarts
import numpy as np
import matplotlib.pyplot as plt
__all__ = [
'polar_ppath',
'polar_ppath_list',
]
[docs]
def polar_ppath_helper(rad, tht, planetary_radius, rscale, ax=None):
"""Just the polar plots required by :func:`polar_ppath`.
Parameters
----------
rad : np.array
List of radiuses [in meters].
tht : np.array
List of angles [in radian].
planetary_radius : float
A planetary radius in same unit as rad.
rscale : float
This will rescale values. As rad is in meters, rscale=1000 means that
the scale is now in kilometers. See :func:`polar_ppath_rad_unit` for
good options
ax : Axes, optional
The axis to draw at. The default is None, which generates a default
polar coordinate.
Returns
-------
ax : As input
As input.
"""
if ax is None:
ax = plt.subplot(111, polar=True)
st = '-' if len(rad) > 1 else 'x'
ax.plot(tht, rad / rscale + planetary_radius / rscale, st)
ax.set_rmin(planetary_radius / rscale)
ax.set_theta_zero_location("E")
ax.set_thetalim(-np.pi, np.pi)
ax.set_thetagrids(np.arange(-180, 179, 30))
return ax
[docs]
def polar_ppath_rad_unit(rscale):
"""Returns the radial unit
Parameters
----------
rscale : float
1.0 for "m", 1000 for "km", 1e6 for "Mm.
Otherwise returns "???"
Returns
-------
str
The unit or "???".
"""
if rscale == 1.0:
return "m"
elif rscale == 1000.0:
return "km"
elif rscale == 1e6:
return "Mm"
else:
return "???"
[docs]
def polar_ppath_lat(rad, lat, planetary_radius, rscale, ax=None):
"""Basic plot for ppath latitudes
Parameters
----------
rad : np.array
List of radiuses [in meters]
lat : np.array
List of latitudes [in radian].
planetary_radius : float
A planetary radius in same unit as rad.
rscale : A rescaler for the radius
This will rescale values. If rad is in meters, rscale=1000
means that the scale is now in kilometers
ax : Axes, optional
The axis to draw at. The default is None, which generates a
default polar coordinate.
Returns
-------
ax : As input.
As input
"""
if ax is None:
ax = plt.subplot(111, polar=True)
ax = polar_ppath_helper(rad, lat, planetary_radius, rscale, ax)
ax.set_frame_on(False)
ax.set_title(
"Latitude vs " f"{'Altitude' if planetary_radius==0 else 'Radius'}"
)
ax.set_thetalim(-np.pi / 2, np.pi / 2)
ax.set_thetagrids(np.arange(-90, 91, 45))
return ax
[docs]
def polar_ppath_lon(rad, lon, planetary_radius, rscale, ax=None):
"""Basic plot for ppath longitudes
Parameters
----------
rad : np.array
List of radiuses [in meters]
lon : np.array
List of longitudes [in radian].
planetary_radius : float
A planetary radius in same unit as rad.
rscale : A rescaler for the radius
This will rescale values. If rad is in meters, rscale=1000
means that the scale is now in kilometers
ax : Axes, optional
The axis to draw at. The default is None, which generates a
default polar coordinate.
Returns
-------
ax : As input.
As input
"""
if ax is None:
ax = plt.subplot(111, polar=True)
ax = polar_ppath_helper(rad, lon, planetary_radius, rscale, ax)
ax.set_frame_on(False)
ax.set_title(
"Longitude vs " f"{'Altitude' if planetary_radius==0 else 'Radius'}"
)
ax.set_theta_zero_location("S")
ax.set_thetagrids(np.arange(-180, 179, 45))
ax.set_yticklabels([]) # Disable r-ticks by bad name
return ax
[docs]
def polar_ppath_map(lat, lon, ax=None):
"""Basic plot for ppath longitudes
Parameters
----------
lat : np.array
List of latitudes [in degrees]
lon : np.array
List of longitudes [in degrees].
ax : Axes, optional
The axis to draw at. The default is None, which generates a
default polar coordinate.
Returns
-------
ax : As input.
As input
"""
if ax is None:
ax = plt.subplot(111, polar=False)
st = '-' if len(lat) > 1 else 'x'
plot_data = None
for [londeg, latdeg] in unwrap_lon(lon, lat):
if plot_data is None:
(plot_data,) = ax.plot(londeg, latdeg, st)
else:
(plot_data,) = ax.plot(londeg, latdeg, st,
color=plot_data.get_color())
ax.set_ylim(-90, 90)
ax.set_xlim(-180, 180)
ax.set_title("Latitude vs Longitude")
ax.set_ylabel("Latitude [deg]")
ax.set_xlabel("Longitude [deg]")
ax.set_xticks(np.linspace(-180, 180, 7))
ax.set_yticks(np.linspace(-90, 90, 7))
return ax
[docs]
def polar_ppath_za(za, ax=None):
"""Basic plot for ppath zeniths
Parameters
----------
za : np.array
List of Zenith angles [in radians]
ax : Axes, optional
The axis to draw at. The default is None, which generates a
default polar coordinate.
Returns
-------
ax : As input.
As input
"""
if ax is None:
ax = plt.subplot(111, polar=True)
ax = polar_ppath_helper(np.ones_like(za), za, 0.0, 1.0, ax)
ax.set_frame_on(False)
ax.set_title("Zenith Angle")
ax.set_thetalim(0, np.pi)
ax.set_thetagrids(np.arange(0, 181, 45))
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
return ax
[docs]
def polar_ppath_aa(aa, ax=None):
"""Basic plot for ppath azimuths
Parameters
----------
aa : np.array
List of Azimuth angles [in radians]
ax : Axes, optional
The axis to draw at. The default is None, which generates a
default polar coordinate.
Returns
-------
ax : As input.
As input
"""
if ax is None:
ax = plt.subplot(111, polar=True)
ax = polar_ppath_helper(np.ones_like(aa), aa, 0.0, 1.0, ax)
ax.set_frame_on(False)
ax.set_title("Azimuth Angle")
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.set_thetagrids(np.arange(-180, 179, 45))
ax.set_yticklabels([]) # Disable r-ticks by bad name
return ax
[docs]
def unwrap_lon(lon, lat):
"""Unwraps the lat and lon for plotting purposes. This is a helper func
Parameters
----------
lon : np.array
A list of latitudes.
lat : np.array
A list of longitudes.
Returns
-------
list
one or more pairs of [lon, lat] that when plotted does not allow
wrapping.
"""
jumps = np.nonzero(np.abs(np.diff(lon)) > 180)[0]
if len(jumps) == 0:
return [[lon, lat]]
out = []
i = 0
for ind in jumps:
ind = ind + 1
out.append([lon[i:ind], lat[i:ind]])
i = ind
out.append([lon[i:-1], lat[i:-1]])
return out
[docs]
def polar_ppath_default_axes(fig, draw_lat_lon, draw_map, draw_za_aa):
"""Get the default axes
Parameters
----------
fig : Figure
A figure
draw_lat_lon : bool, optional
Whether or not latitude and longitude vs radius angles are drawn.
Def: True
draw_map : bool, optional
Whether or not latitude and longitude map is drawn. Def: True
draw_za_aa : bool, optional
Whether or not Zenith and Azimuth angles are drawn. Def: False
Returns
-------
A list of five Axes
A tuple of five axis.
The order is [lat, lon, map, za, aa]
"""
R = draw_map + (draw_za_aa or draw_lat_lon)
Z = 2 * draw_lat_lon
C = 2 * draw_za_aa + Z
ax_lat = fig.add_subplot(R, C, 1, polar=True) if draw_lat_lon else None
ax_lon = fig.add_subplot(R, C, 2, polar=True) if draw_lat_lon else None
ax_za = fig.add_subplot(R, C, 1 + Z, polar=True) if draw_za_aa else None
ax_aa = fig.add_subplot(R, C, 2 + Z, polar=True) if draw_za_aa else None
ax_map = (
fig.add_subplot(R, 1, R, polar=False, aspect=0.5) if draw_map else None
)
if draw_za_aa and draw_lat_lon:
ax_lat.set_position([0.0, 1.0, 0.2, 0.2])
ax_lon.set_position([0.3, 1.0, 0.2, 0.2])
ax_za.set_position([0.6, 1.0, 0.2, 0.2])
ax_aa.set_position([0.9, 1.0, 0.2, 0.2])
if draw_map:
ax_map.set_position([0.1, 0.4, 1.0, 0.5])
return [ax_lat, ax_lon, ax_map, ax_za, ax_aa]
[docs]
def polar_ppath(
ppath,
planetary_radius=0.0,
rscale=1000,
figure_kwargs={"dpi": 300},
draw_lat_lon=True,
draw_map=True,
draw_za_aa=False,
select="all",
fig=None,
axes=None,
):
"""Plots a single observation in a polar coordinate system
Use the draw_* variables to select which plots are done
The polar plots' central point is at the surface of the planet, i.e., at
planetary_radius/rscale. The radius of these plots are the scaled down
radiuses of the input ppath.pos[0, :] / rscale + planetary_radius/rscale.
The default radius value is thus just the altitude in kilometers. If you
put, e.g., 6371e3 as the planetary_radius, the radius values will be the
radius from the surface to the highest altitude
Note also that longitudes are unwrapped, e.g. a step longer than 180
degrees between Ppath points will wrap around, or rather, create separate
entries of the lat-lons.
Parameters
----------
ppath : pyarts.arts.Ppath
A single propagation path object
planetary_radius : float, optional
See :func:`polar_ppath_helper`
rscale : float, optional
See :func:`polar_ppath_helper`
figure_kwargs : dict, optional
Arguments to put into plt.figure(). The default is {"dpi": 300}.
draw_lat_lon : bool, optional
Whether or not latitude and longitude vs radius angles are drawn.
Def: True
draw_map : bool, optional
Whether or not latitude and longitude map is drawn. Def: True
draw_za_aa : bool, optional
Whether or not Zenith and Azimuth angles are drawn. Def: False
select : str, optional
Choose to use "all", "start", or "end" data from Ppath
fig : Figure, optional
A figure. The default is None, which generates a new figure.
axes : A list of five Axes, optional
A tuple of five axis. The default is None, which generates new axes.
The order is [lat, lon, map, za, aa]
Returns
-------
fig : As input
As input.
axes : As input
As input.
"""
if fig is None:
fig = polar_ppath_default_figure(figure_kwargs)
if axes is None:
axes = polar_ppath_default_axes(
fig, draw_lat_lon, draw_map, draw_za_aa
)
# Set radius and convert degrees
e = np.array([])
if "all" == select:
rad = ppath.pos[:, 0] if ppath.pos.shape[1] > 0 else e
latdeg = ppath.pos[:, 1] if ppath.pos.shape[1] > 1 else e
londeg = ppath.pos[:, 2] if ppath.pos.shape[1] > 2 else e
za = np.deg2rad(ppath.los[:, 0]) if ppath.los.shape[1] > 0 else e
aa = np.deg2rad(ppath.los[:, 1]) if ppath.los.shape[1] > 1 else e
elif "end" == select:
ps = ppath.end_pos.shape[0]
ls = ppath.end_los.shape[0]
rad = np.array([ppath.end_pos[0]]) if ps > 0 else e
latdeg = np.array([ppath.end_pos[1]]) if ps > 1 else e
londeg = np.array([ppath.end_pos[2]]) if ps > 2 else e
za = np.deg2rad([ppath.end_los[0]]) if ls > 0 else e
aa = np.deg2rad([ppath.end_los[1]]) if ls > 1 else e
elif "start" == select:
ps = ppath.start_pos.shape[0]
ls = ppath.start_los.shape[0]
rad = np.array([ppath.start_pos[0]]) if ps > 0 else e
latdeg = np.array([ppath.start_pos[1]]) if ps > 1 else e
londeg = np.array([ppath.start_pos[2]]) if ps > 2 else e
za = np.deg2rad([ppath.start_los[0]]) if ls > 0 else e
aa = np.deg2rad([ppath.start_los[1]]) if ls > 1 else e
elif "low" == select:
p = ppath.r[:].min() == ppath.r[:]
rad = ppath.pos[p, 0] if ppath.pos.shape[1] > 0 else e
latdeg = ppath.pos[p, 1] if ppath.pos.shape[1] > 1 else e
londeg = ppath.pos[p, 2] if ppath.pos.shape[1] > 2 else e
za = np.deg2rad(ppath.los[p, 0]) if ppath.los.shape[1] > 0 else e
aa = np.deg2rad(ppath.los[p, 1]) if ppath.los.shape[1] > 1 else e
else:
assert False, f"Bad selection: {select}"
lat = np.deg2rad(latdeg)
lon = np.deg2rad(londeg)
if draw_lat_lon:
axes[0] = polar_ppath_lat(rad, lat, planetary_radius, rscale, axes[0])
axes[0].set_ylabel(
f"{'Altitude' if planetary_radius==0 else 'Radius'}"
f" [{polar_ppath_rad_unit(rscale)}]"
)
axes[1] = polar_ppath_lon(rad, lon, planetary_radius, rscale, axes[1])
if draw_map:
axes[2] = polar_ppath_map(latdeg, londeg, axes[2])
if draw_za_aa:
axes[3] = polar_ppath_za(za, axes[3])
axes[3].set_ylabel("Arbitrary unit [-]")
axes[4] = polar_ppath_aa(aa, axes[4])
# Return incase people want to modify more
return fig, axes
[docs]
def polar_ppath_list(
ppaths,
planetary_radius=0.0,
rscale=1000,
figure_kwargs={"dpi": 300},
fig=None,
axes=None,
select="end",
show="poslos",
):
"""Wraps :func:`polar_ppath` for a list of ppaths
This function takes several ppath objects in a list and manipulates them
based on the option input to form a new ppath object that only has a valid
pos field. This new ppath object is the passed directly to
:func:`polar_ppath` to plot the polar coordinate and mapping information
about pathing
For example, by default, the select argument is "end", which means that
this call would plot the sensor position for each Ppath
Parameters
----------
ppaths : list of pyarts.arts.Ppath
A list of :class:`Ppath` (or pyarts.arts.ArrayOfPpath).
planetary_radius : float, optional
See :func:`polar_ppath`
rscale : float, optional
See :func:`polar_ppath`
figure_kwargs : dict, optional
See :func:`polar_ppath`
fig : Figure, optional
See :func:`polar_ppath`
axes : Axes, optional
See :func:`polar_ppath`
select : str, optional
The selection criteria for the positions and line of sights in the
ppath list. Default is "end".
Options are:
- "end" - end_pos and end_los for each :class:`Ppath`
- "start" - start_pos and start_los for each :class:`Ppath`
- "low" - the lowest r's pos and los for each :class:`Ppath`
- "all" - all pos and los for each :class:`Ppath`
show : str or list, optional
Selects what to show. Default is "poslos" for showing everything.
Options are:
- "pos" - show the position
- "los" - show the line of sight
- "no_map" - don't show the map
The python "in" operator is used to determine what to show, and the
triggers are: "pos" to show ppath pos, "los" to show ppath los, and
"no_map" to not show the map. Note that for all intents and purposes,
"poslos", "lospos", ["pos", "los"], and "lost purpose" will give the
exact same result because they all contain "los" and "pos" in a way
that "in" will find
Returns
-------
As :func:`polar_ppath`.
"""
draw_map = "no_map" not in show
draw_lat_lon = "pos" in show
draw_za_aa = "los" in show
my_path = pyarts.arts.Ppath()
if "end" == select:
my_path.pos = [ppath.end_pos for ppath in ppaths]
my_path.los = [ppath.end_los for ppath in ppaths]
elif "start" == select:
my_path.pos = [ppath.start_pos for ppath in ppaths]
my_path.los = [ppath.start_los for ppath in ppaths]
elif "low" == select:
my_path.pos = np.concatenate(
[ppath.pos[ppath.r[:].min() == ppath.r[:]] for ppath in ppaths]
)
my_path.los = np.concatenate(
[ppath.los[ppath.r[:].min() == ppath.r[:]] for ppath in ppaths]
)
elif "all" == select:
my_path.pos = np.concatenate([ppath.pos for ppath in ppaths])
my_path.los = np.concatenate([ppath.los for ppath in ppaths])
else:
assert False, f"Unknown selection: '{select}'"
return polar_ppath(
ppath=my_path,
planetary_radius=planetary_radius,
rscale=rscale,
figure_kwargs=figure_kwargs,
draw_lat_lon=draw_lat_lon,
draw_map=draw_map,
draw_za_aa=draw_za_aa,
select='all',
fig=fig,
axes=axes,
)