"""Trajectory instrument field of view (FOV) module."""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PathCollection
from matplotlib.colors import Normalize
from matplotlib.path import Path
from ..misc import cached_property, logger
from ..spice import check_kernels
from ..spice.toolbox import fov_pts, rlonlat
log_fovs, debug_fovs = logger('Trajectory FOVs')
[docs]class FovsCollection:
"""Instrument field of views collection.
Parameters
----------
traj: InstrumentTrajectory
Input instrument trajectory.
npts: int, optional
Number of points in the FOV contour.
limb: bool, optional
Compute the intersection on the limb impact parameter
if no intersection with the surface was found.
"""
def __init__(self, traj, npts=25, limb=True):
self.traj = traj
self.npts = npts
self.limb = limb
def __repr__(self):
return (
f'<{self.__class__.__name__}> '
f'Observer: {self.observer} | '
f'Target: {self.target} | '
f'Nb of pts: {len(self.traj)} | '
f'Contour pts: {self.npts} | '
f'Limb: {self.limb}'
)
def __call__(self, *args, **kwargs):
return self.collection(*args, **kwargs)
def __hash__(self):
"""Kernels hash."""
return hash(self.traj)
@property
def kernels(self):
"""Trajectory required kernels."""
return self.traj.kernels
@property
def observer(self):
"""Trajectory observer."""
return self.traj.observer
@property
def target(self):
"""Trajectory target."""
return self.traj.target
@property
def npts(self):
"""Number of points in the FOV contour."""
return self.__npts
@npts.setter
def npts(self, n):
"""FOV contour number of points setter."""
del self.pts
self.__npts = n
@cached_property
@check_kernels
def pts(self):
"""Instrument FOV points.
Note
----
Currently the intersection method is fixed internally
as ``method='ELLIPSOID'``.
"""
log_fovs.debug('Compute FOV intersection points.')
res = fov_pts(
self.traj.ets,
self.observer,
self.target,
limb=self.limb,
npt=self.npts - 1,
abcorr=self.traj.abcorr,
method='ELLIPSOID')
# Close the polygon
res = np.dstack([res, res[..., 0]])
log_fovs.debug('Result: %r', res)
return res
@cached_property(parent='pts')
def rlonlat(self):
"""Instrument FOV intersect coordinates.
Returns
-------
np.ndarray
Boresight surface intersect planetocentric coordinates:
radii, east longitudes and latitudes.
See Also
--------
.pts
"""
log_fovs.debug('Compute FOV intersects planetocentric coordinates.')
return rlonlat(self.pts)
@cached_property(parent='rlonlat')
def paths(self):
"""Instrument FOV surface paths.
Note
----
If all the points are above the limb the path is set to ``None``.
See Also
--------
.rlonlat
"""
log_fovs.debug('Compute FOV paths.')
r_max = max(self.target.radii)
return [
Path(rlonlat[..., 1:]) if np.min(rlonlat[..., 0]) < r_max else None
for rlonlat in np.moveaxis(self.rlonlat, 0, -1)
]
[docs] def get_colors(self, attr, cmap='turbo_r', vmin=None, vmax=None):
"""Get colors for a given attribute and an optional range.
Parameters
----------
attr: str
Attribute to color.
vmin: int or float, optional
Color scaling min value. If ``None`` is provided (default)
the data are scaled to the lowest (not-NaN) value.
vmax: int or float, optional
Color scaling max value. If ``None`` is provided (default)
the data are scaled to the lowest (not-NaN) value.
cmap: str, optional
Matplotlib colormap name (default: ``turbo_r``)
Returns
-------
str
If the attribute is not part of the trajectory (e.g. pure color string).
numpy.ndarray
Normalized RGB color array.
Raises
------
ValueError
If the data to represent is not a 1D array.
"""
if not isinstance(attr, str) or not hasattr(self.traj, attr):
return attr
data = getattr(self.traj, attr)
if np.ndim(data) != 1:
raise ValueError('The data need to be a 1D array.')
if vmin is None:
vmin = np.nanmin(data)
if vmax is None:
vmax = np.nanmax(data)
cmap = plt.get_cmap(cmap)
norm = Normalize(vmin, vmax)
return cmap(norm(data))
[docs] def isort(self, attr, reverse=None):
"""Get sorting indexes for a given attribute."""
if not isinstance(attr, str) or not hasattr(self.traj, attr):
raise KeyError(f'Unknown attribute `{attr}` in Trajectory.')
order = np.argsort(getattr(self.traj, attr))
if reverse is None:
# The order is reversed by default for `inc`, `dist` and `alt`
reverse = attr in ['inc', 'dist', 'alt']
return order[::-1] if reverse else order
[docs] def collection(self, edgecolors=None, facecolors='none',
vmin=None, vmax=None, cmap='turbo_r',
label=None, sort=None, reverse=None,
**kwargs) -> PathCollection:
"""Instrument field of view paths collection.
Parameters
----------
edgecolors: str, optional
Color of the patch contours.
This could be a :class:`.Trajectory` property.
facecolors: str, optional
Color of the patch face.
This could be a :class:`.Trajectory` property.
vmin: int or float
Color scaling min value. If ``None`` is provided (default)
the data are scaled to the lowest (not-NaN) value.
vmax: int or float
Color scaling max value. If ``None`` is provided (default)
the data are scaled to the lowest (not-NaN) value.
cmap: str, optional
Matplotlib colormap name (default: ``turbo_r``)
label: str, optional
Collection legend label (default: observer name).
sort: str, optional
Patches sorting on display (default: ``utc``).
reverse: bool, optional
Reverse patches sorting (default: ``False``).
**kwargs:
Keyword attributes for :class:`matplotlib.collections.PathCollection`.
"""
paths = self.paths
colors = {'cmap': cmap, 'vmin': vmin, 'vmax': vmax}
facecolors = self.get_colors(facecolors, **colors)
edgecolors = self.get_colors(edgecolors, **colors)
if sort:
ind = self.isort(sort, reverse=reverse)
paths = np.array(paths)[ind]
if isinstance(facecolors, np.ndarray):
facecolors = facecolors[ind, ...]
if isinstance(edgecolors, np.ndarray):
edgecolors = edgecolors[ind, ...]
if label is None:
label = str(self.observer).replace('_', ' ')
kwargs.update({
'paths': paths,
'facecolors': facecolors,
'edgecolors': edgecolors,
'label': label,
})
return PathCollection(**kwargs)
[docs] def get_paths(self):
"""Collection paths."""
return self.paths
[docs] @staticmethod
def get_alpha(default=None):
"""Default transparencies."""
return default
[docs] @staticmethod
def get_facecolor(default='tab:orange'):
"""Default facecolors."""
return default
[docs] @staticmethod
def get_edgecolor(default=None):
"""Default edgecolors."""
return default
[docs] @staticmethod
def get_linewidth(default=1.5):
"""Default linewidth."""
return default
[docs] @staticmethod
def get_linestyle(default='solid'):
"""Default linestyle."""
return default
[docs] @staticmethod
def get_zorder(default=1):
"""Default zorder."""
return default
[docs] @staticmethod
def get_label(default=''):
"""Default label."""
return default
[docs]class MaskedFovsCollection(FovsCollection):
"""Masked field of views collection."""
def __init__(self, fovs, mask):
super().__init__(fovs.traj, npts=fovs.npts, limb=fovs.limb)
self.mask = mask
@cached_property
def paths(self):
"""Masked instrument FOV surface paths.
Note
----
If all the points are above the limb the path is set to ``None``.
"""
return [
None if masked else path
for path, masked in zip(self.traj.fovs.paths, self.mask)
]
[docs]class SegmentedFovsCollection(FovsCollection):
"""Segmented field of views collection."""