"""The ``coverage`` module holds the classes that support the modelling, computation and writting of observations
geometry metadata: :class:`GeometryParameters <geogen.coverage.GeometryParameters>` and footprint (`GeoJSON Feature`_).
.. figure:: ../../images/coverage_module.png
:width: 99%
:align: center
"""
import numpy as np
import spiceypy as spice
import geogen.tamn as tamn
import json
import csv
from geojson import LineString, Polygon, Feature, FeatureCollection
import os
from loguru import logger
# Turning SpiceyPy found check off
#
spice.found_check_off()
T_STEPS_MAX = 100
"""Maximum number of computation steps per observation.
"""
[docs]class CoverageList:
"""Class that represents a collection of observation coverages.
Attributes:
coverages (List[Coverage]): List of Coverage objects.
n_coverages (int): Number of Coverage objects.
current (int): Iterator index of current coverage.
"""
def __init__(self):
"""Constructs CoverageList object.
"""
self.coverages = []
self.n_coverages = 0
self.current = 0
def __str__(self):
return '<CoverageList: {} coverages>'.format(self.n_coverages)
def __iter__(self):
return self
def __next__(self):
if self.current >= self.n_coverages:
raise StopIteration
else:
self.current += 1
return self.coverages[self.current - 1]
[docs] def get_coverage(self, observation):
"""Returns the coverage in each a given observation is contained.
Args:
observation (Observation): Input Observation object.
Returns:
Coverage:
"""
for coverage in self.coverages:
if ( (coverage.target_name == observation.target.name) and \
(coverage.instrument_host_id == observation.product.instrument_host_id) and \
(coverage.instrument_id == observation.product.instrument_id) and \
(coverage.product_type == observation.product.product_type)):
return coverage
[docs] def add_coverage(self, coverage):
"""Add a coverage to the coverage list.
Args:
coverage (Coverage): Coverage object to be added.
"""
self.coverages.append(coverage)
self.n_coverages += 1
[docs] def get_targets(self):
"""Returns the list of unique targets within this CoverageList.
Returns:
unique_targets (List[str]): List of unique target names.
"""
unique_targets = []
for coverage in self.coverages:
if coverage.target_name not in unique_targets:
unique_targets.append(coverage.target_name)
return unique_targets
[docs]class Coverage:
"""Class that represents a collection of observation, grouped by target, instrument host and data product types.
Attributes:
target_name (str): Target name related to observations within this coverage.
instrument_host_id (str): Instrument host ID to observations within this coverage.
instrument_id (str): Instrument ID related to observations within this coverage.
product_type (str): Product type related to observations within this coverage.
surface_model (str): Target surface model to be used for computation of observations geometry metadata, which
can be 'ELLIPSOID' (default) or 'DSK/UNPRIORITIZED'.
antimeridian_split (bool): Whether or not (default) coverage observations footprints are split when crossing the
target body's antimeridian.
basename (str): Output files basename associated to this coverage.
observations (List[Observation]): List of Observation objects within this coverage.
n_observations (int): Number of observations within this coverage.
current (int): Iterator index of current observation.
"""
def __init__(self, observation):
"""Constructs Coverage object.
Args:
observation (Observation): Input Observation object.
"""
self.target_name = observation.target.name
self.instrument_host_id = observation.product.instrument_host_id
self.instrument_id = observation.product.instrument_id
self.product_type = observation.product.product_type
self.surface_model = 'ELLIPSOID'
self.antimeridian_split = False
self.basename = ''
self.update_basename()
self.observations = []
self.n_observations = 0
self.current = 0
def __str__(self):
return '<Coverage: {}, {} observations>'.format(self.basename, self.n_observations)
def __iter__(self):
return self
def __next__(self):
if self.current >= self.n_observations:
raise StopIteration
else:
self.current += 1
return self.observations[self.current - 1]
[docs] def update_basename(self):
"""Update coverage basename.
"""
self.basename = (self.target_name.strip().replace('/', '-').replace(' ', '_') + '_' +
self.instrument_host_id + '_' + self.instrument_id + '_' + self.product_type).lower()
if self.surface_model == 'ELLIPSOID':
self.basename += '_ell'
elif self.surface_model == 'DSK/UNPRIORITIZED':
self.basename += '_dsk'
if self.antimeridian_split:
self.basename += '_split'
[docs] def get_name(self):
"""Get coverage name (coverage name without the surface-model and antimeridian-split related suffixes).
Returns:
str:
"""
return '_'.join(self.basename.split('_')[0:4])
[docs] def add_observation(self, observation):
"""Add an observation to the coverage.
Args:
observation (Observation): Input Observation object to be added.
"""
self.observations.append(observation)
self.n_observations += 1
[docs] def compute_geometry(self, surface_model='ELLIPSOID'):
"""Compute coverage observations geometry metadata.
Args:
surface_model (str, optional): Surface model to be used for computation of observations geometry metadata,
which can be 'ELLIPSOID' (default) or 'DSK/UNPRIORITIZED'.
"""
for observation in self.observations:
if observation.is_valid():
try:
observation.compute_geometries(surface_model=surface_model)
observation.derive_geometry_parameters()
except Exception as e:
logger.error(e)
logger.error('Observation <{}> could not be computed.'.format(observation.product.product_id))
observation.valid = False
else:
print('compute_geometry: Invalid Observation.')
# Assess surface model actually used for this coverage and update basename accordingly.
n_ell = 0 # number of observations for which ellipsoidal model was used
n_dsk = 0 # number of observations for which digital shape model was used
for observation in self.observations:
if observation.is_valid():
if observation.surf_model == 'ell': n_ell += 1
if observation.surf_model == 'dsk': n_dsk += 1
if n_ell > n_dsk:
self.surface_model = 'ELLIPSOID'
if surface_model == 'DSK/UNPRIORITIZED':
print('[WARNING] SPICE(DSKDATANOTFOUND) for ' + self.target_name)
else:
self.surface_model = 'DSK/UNPRIORITIZED'
self.update_basename()
[docs] def write_B3F(self, output_dir):
"""Write output coverage B3F file.
Args:
output_dir (str): Output directory path of B3F file.
"""
with open(output_dir + self.basename + '.b3f', 'w') as b3f_file:
fieldnames = ['PRODUCT_ID', 'TIME', 'SC_X', 'SC_Y', 'SC_Z', \
'SUN_X', 'SUN_Y', 'SUN_Z', 'BSIGHT_X', 'BSIGHT_Y', 'BSIGHT_Z', \
'PVEC1_X', 'PVEC1_Y', 'PVEC1_Z', 'PVEC2_X', 'PVEC2_Y', 'PVEC2_Z', \
'PVEC3_X', 'PVEC3_Y', 'PVEC3_Z', 'PVEC4_X', 'PVEC4_Y', 'PVEC4_Z' ]
b3fwriter = csv.writer(b3f_file)
b3fwriter.writerow(fieldnames)
for observation in self.observations:
if observation.is_valid():
productID = observation.product.product_id
for index, geometry in enumerate(observation.geometries):
frustum = geometry.frustum
n_pvec = len(frustum) - 1
if n_pvec == 0:
b3fwriter.writerow([ productID, observation.times[index], \
geometry.scpos[0], geometry.scpos[1], geometry.scpos[2], \
geometry.sunpos[0], geometry.sunpos[1], geometry.sunpos[2], \
frustum[0][0], frustum[0][1], frustum[0][2] ] )
elif n_pvec == 2:
b3fwriter.writerow([ productID, observation.times[index], \
geometry.scpos[0], geometry.scpos[1], geometry.scpos[2], \
geometry.sunpos[0], geometry.sunpos[1], geometry.sunpos[2], \
frustum[0][0], frustum[0][1], frustum[0][2], \
frustum[1][0], frustum[1][1], frustum[1][2], \
frustum[2][0], frustum[2][1], frustum[2][2] ] )
elif n_pvec == 4:
b3fwriter.writerow([ productID, observation.times[index], \
geometry.scpos[0], geometry.scpos[1], geometry.scpos[2], \
geometry.sunpos[0], geometry.sunpos[1], geometry.sunpos[2], \
frustum[0][0], frustum[0][1], frustum[0][2], \
frustum[1][0], frustum[1][1], frustum[1][2], \
frustum[2][0], frustum[2][1], frustum[2][2], \
frustum[3][0], frustum[3][1], frustum[3][2], \
frustum[4][0], frustum[4][1], frustum[4][2] ] )
[docs] def write_GeoJSON(self, output_dir, dsk_files=[]):
"""Write output :ref:`coverage_geojson_file_spec`.
Args:
output_dir (str): Output directory path of coverage GeoJSON file.
dsk_files (List[str]): List of DSK files to be included in coverage metadata.
"""
with open(output_dir + self.basename + '.json', 'w') as f:
collection = []
for observation in self.observations:
if observation.is_valid():
collection.append(observation.footprint)
# Add common observations coverage metadata
surface_model_dict = {}
if self.surface_model == 'ELLIPSOID':
surface_model_dict = {
'type': self.surface_model,
'a_axis_radius': self.observations[0].target.radii[0],
'b_axis_radius': self.observations[0].target.radii[1],
'c_axis_radius': self.observations[0].target.radii[2]
}
elif self.surface_model == 'DSK/UNPRIORITIZED':
dsk_fbasenames = []
for dsk_file in dsk_files:
dsk_fbasenames.append(os.path.basename(dsk_file))
if len(dsk_fbasenames) == 1:
dsk_fbasenames = dsk_fbasenames[0]
surface_model_dict = {
'type': self.surface_model,
'dsk_file': dsk_fbasenames
}
coverage_geojson_dict = {'coverage': {
'target_name': self.target_name,
'target_id': self.observations[0].target.id,
'target_frame': self.observations[0].target.frame,
'surface_model': surface_model_dict,
'antimeridian_split': self.antimeridian_split,
'instrument_host_id': self.instrument_host_id,
'instrument_id': self.instrument_id,
'product_type': self.product_type }
}
void = coverage_geojson_dict.update(FeatureCollection(collection))
json.dump(coverage_geojson_dict, f)
[docs]class Observation:
"""Class that represents an observation associated to a PDS observational data product.
Attributes:
product (Product): Associated PDS data product.
target (Target): Observation target corresponding to associated PDS data product.
detector (Detector): Observation detector corresponding to associated PDS data product.
geometry_parameters (GeometryParameters): Observation geometry parameters.
footprint (`GeoJSON Feature`_): Observation footprint.
t_steps (int): Number of computation steps (and Geometry objects).
times (list): Ephemeris times at which Geometry objects are computed.
geometries (List[Geometry]): List of Geometry objects.
surf_model (str): Target surface model used for computation, either 'ell' or 'dsk'.
valid (bool): Observation validity flag.
"""
def __init__(self, product, forced_target='', force_detector=False):
"""Constructs Observation object.
Args:
product (Product): Input Product object.
forced_target (str, optional): Name of the target to use instead of data product label target name property.
force_detector (bool, optional): TODO
"""
# Set associated Product object
self.product = product
# Set default validity for Observation class constructor
self.valid = True
# Check that stop time is greater than start time
et1 = spice.str2et(self.product.start_time.rstrip('Z'))
et2 = spice.str2et(self.product.stop_time.rstrip('Z'))
if et2 < et1:
logger.warning('stop_time < start_time')
self.valid = False
return
# Set target object holding its properties.
self.target = Target(self.product.target_name, self.product.config, self.product.instrument_host_id, forced_target=forced_target)
if not self.target.is_valid():
logger.warning('Invalid <{}> target name related to <{}> product, for <{}> mission.'
.format(self.product.target_name, self.product.product_id, self.product.instrument_host_id))
self.valid = False
return
# Initiate Detector object
self.detector = Detector(
self.product.detector_name,
self.product.detector_mode,
self.product.detector_fov_ref_ratio,
self.product.detector_fov_cross_ratio,
force_detector=force_detector
)
self.t_steps = min([int(product.n_data_records), T_STEPS_MAX]) if product.n_data_records != 0 else T_STEPS_MAX
self.times = []
self.geometries = []
self.footprint = None
self.geometry_parameters = GeometryParameters()
self.surf_model = ''
# Set validity taking into account for detector validity
self.valid = self.detector.isValid() if self.valid else self.valid
def __str__(self):
return '<Observation: product_id={} valid={}>'.format(self.product.product_id, self.valid)
[docs] def set_times(self):
"""Set ephemeris times at which Geometry objects are computed.
"""
# Remove trailing Z if any
start_time = self.product.start_time.rstrip('Z')
stop_time = self.product.stop_time.rstrip('Z')
et1 = spice.str2et(start_time) + self.product.time_offset
et2 = spice.str2et(stop_time) + self.product.time_offset
if self.detector.type != 'FRAME':
self.times = [ x*(et2-et1)/(self.t_steps-1) + et1 for x in range(self.t_steps)]
else:
self.times = [ (et1+et2)/2 ]
[docs] def compute_geometries(self, surface_model='ELLIPSOID'):
"""Compute Observation Geometry objects.
Args:
surface_model: Target surface model to be used for computation, which can be 'ELLIPSOID' (default) or
'DSK/UNPRIORITIZED'.
"""
self.set_times()
self.geometries = []
# computation parameters
target = self.target.name
body = self.target.id
fixref = self.target.frame
abcorr = 'LT+S'
obsrvr = self.product.instrument_host_id
trg_radii = self.target.radii
method = surface_model # ELLIPSOID or DSK/UNPRIORITIZED
self.surf_model = (surface_model[0:3]).lower() # ell or dsk
for et in self.times:
geometry = Geometry()
if (fixref != '67P/C-G_CK') and (fixref != 'IAU_SUN'):
geometry.solar_longitude = spice.lspcn(target, et, abcorr)*spice.dpr()
else:
geometry.solar_longitude = None
# Compute vector from target centre to spacecraft
targpos, ltime = spice.spkpos(target, et, fixref, abcorr, self.product.instrument_host_id)
scpos = -targpos
geometry.scpos = scpos
# Compute spacecraft to target center distance
geometry.target_center_distance = spice.vnorm(scpos)
# Compute vector from target centre to the Sun
sunpos, ltime = spice.spkpos('SUN', et-ltime, fixref, abcorr, target)
geometry.sunpos = sunpos
# Compute spacecraft to sun distance
geometry.sc_sun_distance = spice.vnorm(sunpos-scpos)
# Compute sub-solar longitude and latitude
if (fixref != 'IAU_SUN'):
try:
spoint, trgepc, srfvec = spice.subslr('INTERCEPT/'+method, target, et, fixref, abcorr, obsrvr)
except:
spoint, trgepc, srfvec = spice.subslr('INTERCEPT/ELLIPSOID', target, et, fixref, abcorr, obsrvr)
self.surf_model = 'ell'
r, lon, lat = spice.reclat(spoint)
geometry.sub_solar_longitude = lon*spice.dpr()
geometry.sub_solar_latitude = lat*spice.dpr()
else:
geometry.sub_solar_longitude = None
geometry.sub_solar_latitude = None
# Compute detector boresight vector in target body-fixed frame
if self.detector.type != 'IN-SITU':
rotmat = spice.pxform(self.detector.frame, fixref, et) # from Detector Frame to Body-fixed frame
mat_detec_j2000 = spice.pxform(self.detector.frame, 'J2000', et) # from Detector Frame to J2000 frame
bvec = spice.mxv(rotmat, self.detector.bsight)
geometry.frustum.append(bvec)
# Compute detector pointing vectors in target body-fixed frame
if self.detector.type == 'LINE' or self.detector.type == 'FRAME':
for dpvec in self.detector.pvecs:
pvec = spice.mxv(rotmat, dpvec)
geometry.frustum.append(pvec)
# Compute solar_distance
geometry.solar_distance = spice.vnorm(geometry.sunpos)
# Compute sub-spacecraft point
try:
spoint, trgepc, srfvec = spice.subpnt('NADIR/'+method, target, et, fixref, abcorr, obsrvr)
except:
spoint, trgepc, srfvec = spice.subpnt('NADIR/ELLIPSOID', target, et, fixref, abcorr, obsrvr)
self.surf_model = 'ell'
geometry.sc_altitude = spice.vnorm(srfvec)
r, lon, lat = spice.reclat(spoint)
geometry.sub_sc_longitude = lon*spice.dpr()
geometry.sub_sc_latitude = lat*spice.dpr()
# Compute solar zenith angle at sub-spacecraft point
try:
trgepc, srfvec, phase, incdnc, emissn = spice.ilumin(method, target, et, fixref, abcorr, obsrvr, spoint)
except:
trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target, et, fixref, abcorr, obsrvr, spoint)
self.surf_model = 'ell'
geometry.sub_sc_sza = incdnc*spice.dpr()
# Compute Sun and target states as seen from spacecraft in J2000, and
# derived right ascension and declination coordinates
#
geometry.sc_sun, ltime = spice.spkezr('SUN', et, 'J2000', abcorr, obsrvr)
geometry.sc_target, ltime = spice.spkezr(target, et, 'J2000', abcorr, obsrvr)
r, ra, dec = spice.recrad(geometry.sc_sun[0:3])
geometry.sun_right_ascension = ra*spice.dpr()
geometry.sun_declination = dec*spice.dpr()
r, ra, dec = spice.recrad(geometry.sc_target[0:3])
geometry.target_right_ascension = ra*spice.dpr()
geometry.target_declination = dec*spice.dpr()
# Compute observed target point for each pointing vector in frustum (whether intersect or nearest point)
dref = fixref
for dvec in geometry.frustum:
try:
spoint, trgepc, srfvec, found = spice.sincpt(method, target, et, fixref, abcorr, obsrvr, dref, dvec)
except:
# assuming that SPICE error is caused by "No DSK segments were found matching the body ID"
spoint, trgepc, srfvec, found = spice.sincpt('ELLIPSOID', target, et, fixref, abcorr, obsrvr, dref, dvec)
self.surf_model = 'ell'
if found:
r, lon, lat = spice.reclat(spoint)
geometry.target_point_longitudes.append(lon*spice.dpr())
geometry.target_point_latitudes.append(lat*spice.dpr())
dist = 0.0
slant = spice.vnorm(srfvec)
else:
# Compute nearest point on a triaxial ellipsoid to LOS (trg_pnear),
# the distance from the ellipsoid to LOS (dist), and the nearest point
# on the LOS to the ellipsoid (los_pnear).
trg_pnear, dist = spice.npedln(trg_radii[0], trg_radii[1], trg_radii[2], scpos, dvec)
los_pnear, dist = spice.nplnpt(scpos, dvec, trg_pnear)
spoint = trg_pnear # assign as surface point for illumination angles computation
r, lon, lat = spice.reclat(spoint)
if method == 'DSK/UNPRIORITIZED':
lonlat = np.array([lon,lat])
try:
srfpts = spice.latsrf(method, target, 0.0, fixref, lonlat)
dsk_spoint = srfpts[0]
los_pnear, dist = spice.nplnpt(scpos, dvec, dsk_spoint)
spoint = dsk_spoint
except:
pass
geometry.target_point_longitudes.append(lon*spice.dpr())
geometry.target_point_latitudes.append(lat*spice.dpr())
slant = spice.vnorm(los_pnear-scpos)
# Compute illumination angles
try:
trgepc, srfvec, phase, incdnc, emissn = spice.ilumin(method, target, et, fixref, abcorr, obsrvr, spoint)
except:
trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target, et, fixref, abcorr, obsrvr, spoint)
self.surf_model = 'ell'
geometry.incidence_angles.append(incdnc*spice.dpr())
geometry.emergence_angles.append(emissn*spice.dpr())
geometry.phase_angles.append(phase*spice.dpr())
geometry.slant_distances.append(slant)
geometry.tangent_altitudes.append(dist)
if self.detector.ifov:
spatial_resolution = slant*self.detector.ifov
geometry.spatial_resolutions.append(spatial_resolution)
# Compute local solar time at the observed target point planetocentric longitude
if self.target.frame != '67P/C-G_CK':
hr, mn, sc, time, ampm = spice.et2lst(et, body, lon, 'PLANETOCENTRIC')
else:
time = None
geometry.local_times.append(time)
# Compute right ascension and declination of pointing vector
pvec = spice.mxv(mat_detec_j2000, dvec)
r, ra, dec = spice.recrad(geometry.sc_target[0:3])
geometry.pvec_ras.append(ra*spice.dpr())
geometry.pvec_decs.append(dec*spice.dpr())
# Compute angle between S/C to target and pointing vector
geometry.pvec_target_angles.append(spice.vsep(-geometry.scpos, dvec)*spice.dpr())
# Compute angle between S/C to Sun and pointing vector
geometry.pvec_sun_angles.append(spice.vsep(geometry.sunpos-geometry.scpos, dvec)*spice.dpr())
# Compute limb points if FRAME detector limb observation.
if (self.detector.type == 'FRAME') and (np.where(np.array(geometry.tangent_altitudes) > 0.0)[0].size > 0): # number of non-surface points not null.
method = 'TANGENT/ELLIPSOID'
corloc = 'CENTER'
refvec = [0., 0., 1.0] # north pole
NCUTS = 200
rolstp = spice.twopi() / NCUTS
schstp = 0
soltol = 0
maxn = 10000
npts, limb_points, epochs, limb_tangts = spice.limbpt(method, target, et, fixref, abcorr, corloc, obsrvr, refvec, rolstp, NCUTS, schstp, soltol, maxn)
# Compute limb tangent points in detector frame and normalise to detector plane
limb_dirs = []
rotmat = spice.pxform(fixref, self.detector.frame, et) # TODO: use returned `epochs` for better accuracy when far way from target
detector_plane = spice.nvp2pl(self.detector.bsight, self.detector.bsight)
for limb_tangt, epoch in zip(limb_tangts, epochs):
# rotmat = spice.pxform(fixref, self.detector.frame, epoch)
limb_dir = spice.mxv(rotmat, limb_tangt)
# intersect with image plane
nxpts, xpt = spice.inrypl([0,0,0], limb_dir, detector_plane)
if nxpts > 0:
limb_dirs.append(xpt)
geometry.limb_points = limb_points
geometry.limb_dirs = limb_dirs
# Append this geometry to observation geometries
self.geometries.append(geometry)
[docs] def derive_geometry_parameters(self):
"""Derive geometry parameters based on observation ephemeris times and Geometriy objects.
Handled by the :meth:`geogen.coverage.GeometryParameters.derive` method.
"""
self.geometry_parameters.derive(self.times, self.geometries)
[docs] def is_valid(self):
"""Returns whether this observation is valid.
Returns:
bool:
"""
return self.valid
[docs]class Detector:
"""Class that represents the detector associated to an Observation.
Attributes:
name (str): SPICE detector name.
id (int): SPICE detector code.
type (str): Detector type, 'IN-SITU' (default), 'POINT', 'LINE', or 'FRAME'.
mode (int): Detector mode used for a given observation.
subframe (bool): Whether a FRAME detector can have sub-frames.
fov_ref_ratio (float): FRAME detector sub-frame FOV reference-axis ratio for a given observation.
fov_cross_ratio (float): FRAME detector sub-frame FOV cross-axis ratio for a given observation.
shape (str): SPICE detector shape, eg: 'RECTANGLE'.
frame (str): SPICE detector frame name.
bsight (list): Detector boresight, defined wrt to SPICE detector frame.
bounds (List[list]): Detector FOV bound vectors, defined wrt to SPICE detector frame.
pvecs (list): Detector FOV pointing vectors, defined wrt to SPICE detector frame.
ifov (float): Across-track instantaneous field-of-view (iFOV), in radians.
valid (bool): Detector validity flag.
"""
def __init__(self, detector_name, detector_mode=1, detector_fov_ref_ratio=1.0, detector_fov_cross_ratio=1.0, force_detector=False):
"""Constructs Detector object.
Args:
detector_name (str): SPICE detector name.
detector_mode (int): Detector mode used for a given observation (default is 1).
detector_fov_ref_ratio (float): Detector sub-frame FOV reference-axis ratio for a given observation.
detector_fov_cross_ratio (float): Detector sub-frame FOV reference-axis ratio for a given observation.
"""
self.name = detector_name
self.type = 'IN-SITU' # default
self.mode = detector_mode
self.subframe = False
self.fov_ref_ratio = detector_fov_ref_ratio
self.fov_cross_ratio = detector_fov_cross_ratio
self.shape = ''
self.frame = ''
self.bsight = None
self.bounds = None
self.pvecs = None
self.ifov = None
self.valid = True
self.id, found = spice.bodn2c(detector_name)
if found:
# Get and set detector type
self.type = self.get_type()
if self.type != 'IN-SITU':
# Retrieve SPICE detector FOV
ROOM = 100
try:
shape, frame, bsight, n, bounds = spice.getfov(self.id, ROOM)
except:
print('Unable to retrieve FOV parameters for SPICE instrument code: ' + str(self.id) + ' (' + self.name + ')')
frame = ''
bsight = None
bounds = None
self.valid = False
self.shape = shape
self.frame = frame
self.bsight = bsight
self.bounds = bounds
# Get detector geometry model parameters: detector type and pointing vectors.
self.pvecs, self.ifov = self.get_geometry()
else:
if force_detector:
logger.warning('Unable to retrieve SPICE instrument code for detector name: ' + self.name)
logger.warning('This detector is assumed to be of IN-SITU type.')
else:
logger.error('Unable to retrieve SPICE instrument code for detector name: ' + self.name)
logger.error('Use --force-detector option to force computation for this detector, assuming it is of IN-SITU type.')
self.valid = False
[docs] def get_type(self):
"""Returns the detector type: 'IN-SITU' (default), 'POINT', 'LINE', or 'FRAME'.
Returns:
str:
"""
kvarprefix = 'INS' + str(self.id)
cvals, found = spice.gcpool(kvarprefix + '_GG_DETECTOR_TYPE', 0, 1, 6)
detector_type = cvals[0] if found else 'IN-SITU' # default if not defined in addendum IK
return detector_type
[docs] def get_geometry(self):
"""Returns the detector pointing vectors and across-track instantaneous field-of-view (iFOV).
- IN-SITU and POINT detectors have no pointing vectors.
- LINE detectors have two pointing vectors defining an angle-of-view (AOV).
- FRAME detectors have multiple interpolated pointing vectors defintion a fied-of-view (FOV).
Returns:
Tuple[list, float]:
"""
kvarprefix = 'INS' + str(self.id)
# Retrieve IFOV if exists.
ifovs, found = spice.gdpool(kvarprefix + '_IFOV', 0, 2)
ifov = None
if found:
ifov = ifovs[0]
else:
ifovs, found = spice.gdpool(kvarprefix + '_GG_IFOV', 0, 2)
if found:
ifov = ifovs[0]
if self.type == 'LINE':
dmodes, found = spice.gipool(kvarprefix + '_GG_DETECTOR_MODES', 0, 10)
aov_rot_vec, found = spice.gdpool(kvarprefix + '_GG_AOV_ROT_VECTOR', 0, 3)
aov_angles, found = spice.gdpool(kvarprefix + '_GG_AOV_ANGLES', 0, 10)
cvals, found = spice.gcpool(kvarprefix + '_GG_AOV_ANGLE_UNITS', 0, 1, 10)
aov_angles_units = cvals[0] if found else ''
try:
aov_angle = spice.convrt(aov_angles[np.where(dmodes == self.mode)][0], aov_angles_units, 'RADIANS')
pvec1 = spice.vrotv(self.bsight, aov_rot_vec, aov_angle/2)
pvec2 = spice.vrotv(self.bsight, aov_rot_vec, -aov_angle/2)
return [pvec1, pvec2], ifov
except:
self.valid = False
logger.warning('Unable to derive AOV corresponding to a product detector mode for ' + self.name )
return None, ifov
elif self.type == 'FRAME':
# Retrieve from kernel pool whether or not the FRAME detector implement a sub-frame
subframe, found = spice.gcpool(kvarprefix + '_GG_DETECTOR_SUBFRAME', 0, 1, 5)
if found:
self.subframe = True if subframe[0] == 'TRUE' else False
else:
self.subframe = False
# Derive sub-frame bound vectors, when applicable.
if self.subframe == True and self.shape == 'RECTANGLE':
# Retrieve FOV reference vector
fov_ref_vector, found = spice.gdpool(kvarprefix + '_FOV_REF_VECTOR', 0, 3)
# Retrieve FOV reference and cross angles, and units
fov_ref_angle, found = spice.gdpool(kvarprefix + '_FOV_REF_ANGLE', 0, 1)
fov_cross_angle, found = spice.gdpool(kvarprefix + '_FOV_CROSS_ANGLE', 0, 1)
fov_angle_units, found = spice.gcpool(kvarprefix + '_FOV_ANGLE_UNITS', 0, 1, 10)
# Scale FOV reference and cross angles according input ratios.
fov_ref_angle = spice.convrt(fov_ref_angle[0] * self.fov_ref_ratio, fov_angle_units[0], 'RADIANS')
fov_cross_angle = spice.convrt(fov_cross_angle[0] * self.fov_cross_ratio, fov_angle_units[0], 'RADIANS')
# Compute cross rotation vector
fov_cross_vector = spice.vcrss(fov_ref_vector, self.bsight)
# Compute sub-frame FOV bound vectors
subfrm_bounds = []
fov_plane = spice.nvc2pl(self.bsight, 1.0)
cross_rot_signs = [-1.0, 1.0, 1.0, -1.0]
ref_rot_signs = [-1.0, -1.0, 1.0, 1.0]
for i in range(4):
bound = spice.vrotv(self.bsight, fov_cross_vector, cross_rot_signs[i] * fov_ref_angle)
bound = spice.vrotv(bound, fov_ref_vector, ref_rot_signs[i] * fov_cross_angle)
nxpts, bound = spice.inrypl([0.0, 0.0, 0.0], bound, fov_plane)
subfrm_bounds.append(bound)
# Overwrite bound vectors
self.bounds = subfrm_bounds
# Bound vectors interpolation (initially for limb observations handling)
# Hypothesis:
# - Rectangular FOV (bounds=4)
# - Bounds in the same plane
# set number of interpolated pointing vectors for the long and short FOV facet (n_min, n_max)
N_PVECS = 100 # x2 actually
bounds = self.bounds
n_bounds = len(bounds)
facet_sizes = []
for j in range(n_bounds):
facet_sizes.append(spice.vnorm(bounds[(j+1) % n_bounds]-bounds[j]))
min_facet_size = min(facet_sizes)
max_facet_size = max(facet_sizes)
n_max = int(N_PVECS / (1 + (min_facet_size / max_facet_size)))
n_min = N_PVECS - n_max
pvecs = []
for j in range(n_bounds):
# set number of facet pointing vectors
if facet_sizes[j] == min_facet_size:
n_facet_pvecs = n_min
else:
n_facet_pvecs = n_max
# create and append facet pointing vectors
for i in range(n_facet_pvecs):
pvec = bounds[j] + i * (bounds[(j+1) % n_bounds]-bounds[j]) / n_facet_pvecs
pvecs.append(pvec)
return pvecs, ifov
else:
return None, ifov
[docs] def get_pointing_vectors(self, interpolated=True):
"""Returns detector pointing vectors, whether interpolated or not.
Note that LINE detector AOV pointing vectors are not interpolated.
Args:
interpolated (bool): Set to True (default) to return interpolated pointing vectors. Otherwise, bound vectors
are returned.
Returns:
list:
"""
if interpolated:
return self.pvecs
else:
return self.bounds
[docs] def in_fov(self, vec):
"""Returns whether or not a given vectors is contained within the detector FOV.
Used for FRAME detector limb observations. We make the hypothesis that bound vectors defining FOV are already
sorted.
Args:
vec (list): Input vector, defined wrt to SPICE detector frame.
Returns:
bool:
"""
inside = True
n_bounds = len(self.bounds)
for j in range(n_bounds):
v1 = np.asarray(self.bounds[j])
v2 = np.asarray(self.bounds[(j+1) % n_bounds])
vnorm = spice.vcrss(v1, v2)
vsep = spice.vsep(vnorm, vec)
if vsep >= spice.halfpi():
inside = False
return inside
[docs] def isValid(self):
return self.valid
[docs]class Target:
"""Class that represents the target body associated to an observation.
Attributes:
name (str): SPICE body name.
id (int): SPICE body code.
frame (str): SPICE body-fixed reference frame name.
radii (list): Target body ellipsoid radii.
valid (bool): Target validity flag.
"""
def __init__(self, name, config, mission, forced_target=''):
""" Constructs Target objects.
Args:
name (str): Input target name.
config (Config): Config object.
mission (str): Mission/instrument host ID to be used for the retrieval of the primary target name.
forced_target (str): Target name to be used instead of input target name.
"""
self.name = name
self.id = 0
self.frame = ''
self.radii = None
self.valid = False
# set target name to forced_target
if forced_target:
self.name = forced_target
# check if input target name is a valid/recognized SPICE target
body_id, found = spice.bodn2c(self.name)
if not found:
# use primary target instead, if input target name is applicable
if self.name in config.get_applicable_targets():
primary_target_name = config.get_primary_target(mission)
body_id, found = spice.bodn2c(primary_target_name)
if not found:
# unlikely to happen unless primary_target is wrongly defined in config file.
return
else:
logger.warning('Input target name <{}> not applicable.'.format(name))
return
# set target SPICE body ID
self.id = body_id
# Set body name to common SPICE body name
self.name, found = spice.bodc2n(self.id)
# Set geodetic reference frame for SPICE body ID
#
# get target body reference frame as defined in config file
target_frame = config.get_target_frame(self.name)
# set target body reference frame as specified in config file, or set as 'IAU_<target_name>' where
# <target_name> is the SPICE common name of the target body.
if target_frame:
self.frame = target_frame
else:
self.frame = 'IAU_' + self.name
# retrieve body ellipsoidal model radii
try:
dim, radii = spice.bodvrd(self.name, 'RADII', 3)
self.radii = radii
except Exception as e:
logger.warning('Input SPICE body <{}> is not a natural Solar System body that can be used as reference target.'.format(self.name))
return
self.valid = True
[docs] def is_valid(self):
"""Returns validity flag for this target.
Returns:
bool:
"""
return self.valid
[docs]class Geometry:
""" Class that represents the geometrical state of an observation at a given time/computation step.
A collection of Geometry objects is used to derive the :class:`GeometryParameters <geogen.coverage.GeometryParameters>`
and ``footprint`` associated to a given :class:`Observation <geogen.coverage.Observation>`.
See Geometry class `source code <../_modules/geogen/coverage.html#Geometry>`_ for the list of attributes.
"""
def __init__(self):
self.scpos = [] # 3d-vector
self.sunpos = [] # # 3d-vector
self.frustum = [] # boresight and pointing vectors [[3]x[N]] (N=1,3,4, or more)
self.solar_distance = 0.0
self.solar_longitude = 0.0
self.target_center_distance = 0.0
self.sc_sun = [] # J2000
self.sc_target = [] # J2000
self.target_right_ascension = 0.0
self.target_declination = 0.0
self.sun_right_ascension = 0.0
self.sun_declination = 0.0
# Pointing-dependent quantities
self.pvec_sun_angles = []
self.pvec_target_angles = []
self.pvec_ras = []
self.pvec_decs = []
# Body-fixed-surface-model-dependent quantities
self.sub_solar_longitude = 0.0
self.sub_solar_latitude = 0.0
self.sc_sun_distance = 0.0
self.sc_altitude = 0.0
self.sub_sc_longitude = 0.0
self.sub_sc_latitude = 0.0
self.sub_sc_sza = 0.0
self.target_point_longitudes = [] # one 3d-vector per boresight/pointing vector (frustum element)
self.target_point_latitudes = [] # one 3d-vector per boresight/pointing vector (frustum element)
self.incidence_angles = [] # incidence angles at surface-OTP for each frustum element
self.emergence_angles = []
self.phase_angles = []
self.slant_distances = []
self.tangent_altitudes = []
self.spatial_resolutions = []
self.local_times = []
# Specific to FRAME detector limb observations
self.limb_points = []
self.limb_dirs = []
[docs]class GeometryParameters:
"""Class that holds and derive the geometry parameters of an observation.
Each of the following attribute is a property of an observation footprint within output
:ref:`Coverage GeoJSON <coverage_geojson_file_spec>` files, meant for ingestion into the PSA DB and for
visualisation on the PSA map-based UI.
Geometry parameters are derived from individual measurement geometries computed by the
:meth:`geogen.coverage.Observation.compute_geometries` method.
- Longitudes and latitudes are expressed in planetocentric coordinate system, in degrees.
- All angles are expressed in degrees.
- All distance are expressed in kilometers.
Attributes:
start_time (str): Earliest UTC time corresponding to the observation footprint.
stop_time (str): Latest UTC time corresponding to the observation footprint.
reference_time (str): Reference UTC time at which most geometry parameters are computed (min_*/max_* parameters
are computed taking into account for all observation geometry computation times. The reference time is
defined as the time between the start and stop time).
solar_longitude (float): Planetocentric longitude (Ls) of the sun for the target body at the reference time.
The planetocentric longitude is the angle between the body-sun vector at the time of interest and the
body-sun vector at the vernal equinox.
sub_solar_latitude (float): Latitude of the sub-solar point on the target body at the reference time. The
sub-solar point is the point on a body's reference surface where a line from the body center to the
sun center intersects that surface.
sub_solar_longitude (float): Longitude of the sub-solar point on the target body at the reference time.
The sub-solar point is the point on a body's reference surface where a line from the body center to the
sun center intersects that surface.
solar_distance (float): Distance from the center of the sun to the center of the target body at the
reference time.
spacecraft_solar_distance (float): Distance from the spacecraft to the center of the sun at the
reference time.
spacecraft_altitude (float): Distance from the spacecraft to the sub-spacecraft point on the target body at the
reference time.
target_center_distance (float): Distance from the spacecraft to the center of the target body at the
treference time.
sub_spacecraft_latitude (float): Latitude of the sub-spacecraft point on the target body at the reference time.
sub_spacecraft_longitude (float): Longitude of the sub-spacecraft point on the target body at the reference time.
sub_spacecraft_solar_zenith_angle (float): Solar zenith angle at the sub-spacecraft point on the target body
surface at the reference time. The solar zenith angle is the angle subtended between the direction towards
the Sun and the local normal at the surface.
target_right_ascension (float): Right ascension of the position vector of the target body center as seen from
the spacecraft in the Earth mean equator and equinox frame (J2000).
target_declination (float): Declination of the position vector of the target body center as seen from the
spacecraft in the Earth mean equator and equinox frame (J2000).
sun_right_ascension (float): Right ascension of the position vector of the Sun as seen from the spacecraft in
the Earth mean equator and equinox frame (J2000).
sun_declination (float): Declination of the position vector of the Sun as seen from the spacecraft in the Earth
mean equator and equinox frame (J2000).
x_sc_sun_position (float): X component of the position vector from spacecraft to Sun, expressed in J2000
coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
y_sc_sun_position (float): Y component of the position vector from spacecraft to Sun, expressed in J2000
coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
z_sc_sun_position (float): Z component of the position vector from spacecraft to Sun, expressed in J2000
coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
x_sc_sun_velocity (float): X component of the velocity vector of Sun relative to the spacecraft, expressed
in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
y_sc_sun_velocity (float): Y component of the velocity vector of Sun relative to the spacecraft, expressed
in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
z_sc_sun_velocity (float): Z component of the velocity vector of Sun relative to the spacecraft, expressed
in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the reference time.
x_sc_target_position (float): X component of the position vector from the spacecraft to target body center,
expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the
reference time.
y_sc_target_position (float): Y component of the position vector from the spacecraft to target body center,
expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the
reference time.
z_sc_target_position (float): Z component of the position vector from the spacecraft to target body center,
expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated at the
reference time.
x_sc_target_velocity (float): X component of the velocity vector of the target body center relative to the
spacecraft, expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated
at the reference time.
y_sc_target_velocity (float): Y component of the velocity vector of the target body center relative to the
spacecraft, expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated
at the reference time.
z_sc_target_velocity (float): Z component of the velocity vector of the target body center relative to the
spacecraft, expressed in J2000 coordinates, and corrected for light time and stellar aberration, evaluated
at the reference time.
boresight_right_ascension (float): Right ascension of the detector boresight vector, in the Earth mean equator
and equinox frame (J2000), at the reference time.
boresight_declination (float): Declination of the detector boresight vector, in the Earth mean equator and
equinox frame (J2000), at the reference time.
boresight_target_angle (float): The separation angle between the detector line-of-sight (boresight) and the
target body center as seen from the spacecraft, at the reference time.
boresight_solar_elongation (float): Separation angle between the detector line-of-sight and the position vector
of the Sun as seen from the spacecraft, at the reference time.
center_latitude (float): Latitude of the observation footprint center point.
center_longitude (float): Longitude of the observation footprint center point.
antimeridian_crossing (bool): Whether or not observation footprint is crossing the target body antimeridian.
npole_crossing (bool): Whether or not observation footprint is crossing the target body North Pole.
spole_crossing (bool): Whether or not observation footprint is crossing the target body South Pole.
westernmost_longitude (float): Westernmost observation longitude of the footprint.
easternmost_longitude (float): Easternmost observation longitude of the footprint.
minimum_latitude (float): Minimum observation latitude of the footprint.
maximum_latitude (float): Maximum observation latitude of the footprint.
local_true_solar_time (str): Local solar time for the surface point, evaluated at the reference time. The local
solar time is the angle between the planetocentric longitude of the Sun, as viewed from the center of the
target body, and the planetocentric longitude of the surface point, expressed on a “24 hour” clock.
min_incidence_angle (float): Minimum incidence angle. The incidence angle is the angle between the local
vertical at a given surface point and the vector from the surface point to the sun.
max_incidence_angle (float): Maximum incidence angle. The incidence angle is the angle between the local
vertical at a given surface point and the vector from that the surface point to the sun.
min_emergence_angle (float): Minimum emission angle. The emission angle is the angle between the surface normal
at a given surface point and the vector from the surface point to the spacecraft.
max_emergence_angle (float): Maximum emission angle. The emission angle is the angle between the surface normal
at a given surface point and the vector from the surface point to the spacecraft.
min_phase_angle (float): Minimum phase angle. The phase angle is the angle between the vectors from the surface
point to the spacecraft and from the surface point to the Sun.
max_phase_angle (float): Maximum phase angle. The phase angle is the angle between the vectors from the surface
point to the spacecraft and from the surface point to the Sun.
min_slant_distance (float): Minimum slant distance. The slant distance is the distance from the spacecraft to
the nearest point on the detector line-of-sight to the target body surface.
max_slant_distance (float): Maximum slant distance. The slant distance is the distance from the spacecraft to
the nearest point on the detector line-of-sight to the target body surface.
min_tangent_altitude (float): Minimum tangent altitude. The tangent altitude is the distance from the target
body surface nearest point to the detector line-of-sight.
max_tangent_altitude (float): Maximum tangent altitude. The tangent altitude is the distance from the target
body surface nearest point to the detector line-of-sight.
min_spatial_resolution (float): Minimum across-track instantaneous field-of-view (iFOV) target point
spatial resolution.
max_spatial_resolution (float): Maximum across-track instantaneous field-of-view (iFOV) target point
spatial resolution.
"""
def __init__(self):
"""Constructs GeometryParameters object.
"""
self.start_time = None
self.stop_time = None
self.reference_time = None
self.center_latitude = None
self.center_longitude = None
self.antimeridian_crossing = None
self.npole_crossing = None
self.spole_crossing = None
self.westernmost_longitude = None
self.easternmost_longitude = None
self.minimum_latitude = None
self.maximum_latitude = None
self.local_true_solar_time = None
self.solar_longitude = None
self.sub_solar_latitude = None
self.sub_solar_longitude = None
self.solar_distance = None
self.spacecraft_solar_distance = None
self.spacecraft_altitude = None
self.target_center_distance = None
self.sub_spacecraft_latitude = None
self.sub_spacecraft_longitude = None
self.sub_spacecraft_solar_zenith_angle = None
self.target_right_ascension = None
self.target_declination = None
self.sun_right_ascension = None
self.sun_declination = None
self.x_sc_sun_position = None
self.y_sc_sun_position = None
self.z_sc_sun_position = None
self.x_sc_sun_velocity = None
self.y_sc_sun_velocity = None
self.z_sc_sun_velocity = None
self.x_sc_target_position = None
self.y_sc_target_position = None
self.z_sc_target_position = None
self.x_sc_target_velocity = None
self.y_sc_target_velocity = None
self.z_sc_target_velocity = None
self.min_incidence_angle = None
self.max_incidence_angle = None
self.min_emergence_angle = None
self.max_emergence_angle = None
self.min_phase_angle = None
self.max_phase_angle = None
self.min_slant_distance = None
self.max_slant_distance = None
self.min_tangent_altitude = None
self.max_tangent_altitude = None
self.min_spatial_resolution = None
self.max_spatial_resolution = None
self.boresight_right_ascension = None
self.boresight_declination = None
self.boresight_target_angle = None
self.boresight_solar_elongation = None
[docs] def derive(self, times, geometries):
"""Derive observation geometry parameters from inputs ephemeris times and Geometry objects list.
Args:
times (list): list of ephemeris times.
geometries (list): list of Geometry objects.
Returns:
"""
# Set reference index
ref_idx = int(len(geometries)/2)
ref_geom = geometries[ref_idx]
# start/stop/reference local_times
self.start_time = spice.et2utc(times[0], 'ISOC', 3, 35)
self.stop_time = spice.et2utc(times[-1], 'ISOC', 3, 35)
self.reference_time = spice.et2utc(times[ref_idx], 'ISOC', 3, 35)
# footprint center latitude/longitude
if ref_geom.target_point_latitudes:
self.center_latitude = ref_geom.target_point_latitudes[0] # boresight
if ref_geom.target_point_longitudes:
self.center_longitude = ref_geom.target_point_longitudes[0] # boresight
# Determination of whether or not input geometry is crossing the antimeridian and/or the pole(s) is performed
# by tamn.get_bbox() method. Boundary box coordinates below is a first estimation not taking into account for
# antimeridian and/or the pole(s) crossing, and for off-surface target points in the case of FRAME detectors.
# westernmost/easternmost_longitude
if geometries[0].target_point_longitudes:
values = []
for geometry in geometries:
values += geometry.target_point_longitudes
values = np.asarray(values)
self.westernmost_longitude = np.min(values)
self.easternmost_longitude = np.max(values)
# minimum/maximum_latitude
if geometries[0].target_point_latitudes:
values = []
for geometry in geometries:
values += geometry.target_point_latitudes
self.minimum_latitude = min(values)
self.maximum_latitude = max(values)
# local_true_solar_time
if ref_geom.local_times:
self.local_true_solar_time = ref_geom.local_times[0] # boresight
# solar_longitude
self.solar_longitude = ref_geom.solar_longitude
# sub-sub_solar_longitude/latitude
self.sub_solar_longitude = ref_geom.sub_solar_longitude
self.sub_solar_latitude = ref_geom.sub_solar_latitude
# solar_distance
self.solar_distance = ref_geom.solar_distance
# spacecraft_solar_distance
self.spacecraft_solar_distance = ref_geom.sc_sun_distance
# spacecraft_altitude
self.spacecraft_altitude = ref_geom.sc_altitude
# target_center_distance
self.target_center_distance = ref_geom.target_center_distance
# sub_spacecraft_longitude/latitude
self.sub_spacecraft_latitude = ref_geom.sub_sc_latitude
self.sub_spacecraft_longitude = ref_geom.sub_sc_longitude
# solar_zenith_angle
self.sub_spacecraft_solar_zenith_angle = ref_geom.sub_sc_sza
# Target right ascension and declination
self.target_right_ascension = ref_geom.target_right_ascension
self.target_declination = ref_geom.target_declination
# Sun right ascension and declination
self.sun_right_ascension = ref_geom.sun_right_ascension
self.sun_declination = ref_geom.sun_declination
# Spacecraft-Sun position vector and velocity (J2000)
self.x_sc_sun_position = ref_geom.sc_sun[0]
self.y_sc_sun_position = ref_geom.sc_sun[1]
self.z_sc_sun_position = ref_geom.sc_sun[2]
self.x_sc_sun_velocity = ref_geom.sc_sun[3]
self.y_sc_sun_velocity = ref_geom.sc_sun[4]
self.z_sc_sun_velocity = ref_geom.sc_sun[5]
# Spacecraft-Target position vector and velocity (J2000)
self.x_sc_target_position = ref_geom.sc_target[0]
self.y_sc_target_position = ref_geom.sc_target[1]
self.z_sc_target_position = ref_geom.sc_target[2]
self.x_sc_target_velocity = ref_geom.sc_target[3]
self.y_sc_target_velocity = ref_geom.sc_target[4]
self.z_sc_target_velocity = ref_geom.sc_target[5]
# min/max_incidence_angle
if geometries[0].incidence_angles:
values = []
for geometry in geometries:
values += geometry.incidence_angles
self.min_incidence_angle = min(values)
self.max_incidence_angle = max(values)
# min/max_emergence_angle
if geometries[0].emergence_angles:
values = []
for geometry in geometries:
values += geometry.emergence_angles
self.min_emergence_angle = min(values)
self.max_emergence_angle = max(values)
# min/max_phase_angle
if geometries[0].phase_angles:
values = []
for geometry in geometries:
values += geometry.phase_angles
self.min_phase_angle = min(values)
self.max_phase_angle = max(values)
# min/max_slant_distance
if geometries[0].slant_distances:
values = []
for geometry in geometries:
values += geometry.slant_distances
self.min_slant_distance = min(values)
self.max_slant_distance = max(values)
# min/max_tangent_altitude
if geometries[0].tangent_altitudes:
values = []
for geometry in geometries:
values += geometry.tangent_altitudes
self.min_tangent_altitude = min(values)
self.max_tangent_altitude = max(values)
# min/max_spatial_resolution
if geometries[0].spatial_resolutions:
values = []
for geometry in geometries:
values += geometry.spatial_resolutions
self.min_spatial_resolution = min(values)
self.max_spatial_resolution = max(values)
# Right ascension and declination of reference geometry boresight (J2000)
if ref_geom.pvec_ras and ref_geom.pvec_decs:
self.boresight_right_ascension = ref_geom.pvec_ras[0]
self.boresight_declination = ref_geom.pvec_decs[0]
# Boresight_target_angle
if ref_geom.pvec_target_angles:
self.boresight_target_angle = ref_geom.pvec_target_angles[0] # boresight
# boresight_solar_elongation
if ref_geom.pvec_sun_angles:
self.boresight_solar_elongation = ref_geom.pvec_sun_angles[0] # boresight
# Report on "un-derived" parameters (None values)
for key in self.__dict__.keys():
if self.__dict__[key] == None:
pass
#print('[WARNING] `{}` geometry parameter not derived, mostly likely because not applicable.'.format(key))