Source code for geogen.coverage

"""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 derive_footprints(self, split=False): """Derive coverage observations footprints. Args: split (bool): Whether or not (default) coverage observations footprints are split when crossing the target body's antimeridian. """ self.antimeridian_split = split self.update_basename() for observation in self.observations: if observation.is_valid(): observation.derive_footprint(split=split) else: print('derive_footprints: Invalid Observation - '+observation.product.product_id)
[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 derive_footprint(self, split=False): """Derive observation footprint. Derive `GeoJSON Feature`_ geometry and properties from computed geometry parameters for each frustum boresight and pointing vectors. Boundary box coordinates and whether or not the footprint geometry is crossing the target body's antimeridian, North Pole and South Pole, is performed using :func:`geogen.tamn.get_bbox` function. Splitting of footprint geometry is performed using the :func:`geogen.tamn.getSplitFeature` function. Args: split: Whether or not (default) coverage observations footprints are split when crossing the target body's antimeridian. """ # Set default props props = { "product_id": self.product.product_id } # Append optional properties if hasattr(self.product, 'dataset_id'): void = props.update({"dataset_id": self.product.dataset_id}) if hasattr(self.product, 'browse_path'): void = props.update({"browse_path": self.product.browse_path}) # Init GeoJSON Geometry object geojson_geometry = None # default for IN-SITU detector type if self.detector.type == 'POINT': coordTuples = [] for geometry in self.geometries: lon = geometry.target_point_longitudes[0] lat = geometry.target_point_latitudes[0] coordTuples.append((lon, lat)) # Set GeoJSON Geometry object geojson_geometry = LineString(coordTuples) elif self.detector.type == 'LINE': coordTuples = [] for geometry in self.geometries: for index, (lon, lat) in enumerate(zip(geometry.target_point_longitudes, geometry.target_point_latitudes)): if index > 0: # exclude boresight coordTuples.append((lon, lat)) # Sort coordinates tuples try: coordTuples = [ coordTuples[0:2*self.t_steps:2] + coordTuples[2*self.t_steps-1:0:-2] + [coordTuples[0]] ] except Exception as e: # report error and continue logger.error(e) logger.error('Unable to derive footprint for observation {}'.format(self.product.product_id)) # Set GeoJSON Geometry object geojson_geometry = Polygon(coordTuples) elif self.detector.type == 'FRAME': # Derive and sort footprint points # Set input variables target_point_longitudes = np.asarray(self.geometries[0].target_point_longitudes[1:-1]) # no boresight target_point_latitudes = np.asarray(self.geometries[0].target_point_latitudes[1:-1]) # no boresight tangent_altitudes = np.asarray(self.geometries[0].tangent_altitudes[1:-1]) # no boresight limb_points = np.asarray(self.geometries[0].limb_points) limb_dirs = np.asarray(self.geometries[0].limb_dirs) pvecs = np.asarray(self.detector.pvecs) # Filter out pointing vectors not intersecting the surface (surf_pvecs) indices = np.where(tangent_altitudes == 0.0) surf_pvecs = pvecs[indices] surf_point_longitudes = target_point_longitudes[indices] surf_point_latitudes = target_point_latitudes[indices] # Filter out limb direction vectors not in the FOV, and corresponding limb points infov_limb_dirs = [] infov_limb_points = [] for limb_dir, limb_point in zip(limb_dirs, limb_points): if self.detector.in_fov(limb_dir): infov_limb_dirs.append(limb_dir) infov_limb_points.append(limb_point) # footprint point vectors: concatenate limb and surface pointing vectors (in detector frame) footprint_pvecs = [] # list of 3d-numpy arrays for infov_limb_dir in infov_limb_dirs: footprint_pvecs.append(infov_limb_dir) for surf_pvec in surf_pvecs: footprint_pvecs.append(surf_pvec) # set footprint points longitudes/latitudes footprint_longitudes = [] footprint_latitudes = [] # print(len(footprint_pvecs)) if len(footprint_pvecs) > 2: # target in FOV for infov_limb_point in infov_limb_points: r, infov_limb_point_lon, infov_limb_point_lat = spice.reclat(infov_limb_point) footprint_longitudes.append(infov_limb_point_lon*spice.dpr()) footprint_latitudes.append(infov_limb_point_lat*spice.dpr()) for surf_point_lon, surf_point_lat in zip(surf_point_longitudes, surf_point_latitudes): footprint_longitudes.append(surf_point_lon) footprint_latitudes.append(surf_point_lat) # Sort footprint pointing vectors and corresponding surface/limb points # (hyp: all pointing vectors in footprint_pvecs lie in a detector plane) # # compute mean footprint pointing vector centroid and reference plane centroid_vec = np.zeros(3) for footprint_pvec in footprint_pvecs: centroid_vec += spice.vhat(footprint_pvec) centroid_vec = spice.vhat(centroid_vec) # intersects all pointing vectors with reference/centroid plane centroid_plane = spice.nvc2pl(centroid_vec, 1.0) plane_pvec_dirs = [] for footprint_pvec in footprint_pvecs: nxpts, xpt = spice.inrypl([0,0,0], footprint_pvec, centroid_plane) plane_pvec_dirs.append(spice.vhat(xpt-centroid_vec)) pvec_thetas = [] for plane_pvec_dir in plane_pvec_dirs: theta = spice.vsep(plane_pvec_dirs[0], plane_pvec_dir) crossp_norm = spice.vdot(centroid_vec, spice.vcrss(plane_pvec_dirs[0], plane_pvec_dir)) if crossp_norm < 0: theta = -theta pvec_thetas.append(theta) indices = np.argsort(np.array(pvec_thetas)) # increasing theta angle footprint_longitudes = np.array(footprint_longitudes)[indices] footprint_latitudes = np.array(footprint_latitudes)[indices] else: # target not in FOV print('Target not in FOV for <{}> product. Null footprint geometry'.format(self.product.product_id)) logger.info('Target not in FOV for <{}> product. Null footprint geometry'.format(self.product.product_id)) # Set coordinates tuples coordTuples = [] for (lon, lat) in zip(footprint_longitudes, footprint_latitudes): coordTuples.append((lon, lat)) if len(coordTuples) > 0: coordTuples = [coordTuples + [coordTuples[0]]] # Set GeoJSON Geometry object geojson_geometry = Polygon(coordTuples) # Derive boundary box coordinates and antimeridian/poles crossing flags bbox, antimeridian_crossing, npole_crossing, spole_crossing = tamn.get_bbox(geojson_geometry) # Update corresponding geometry parameters if bbox: self.geometry_parameters.westernmost_longitude = bbox[0] self.geometry_parameters.minimum_latitude = bbox[1] self.geometry_parameters.easternmost_longitude = bbox[2] self.geometry_parameters.maximum_latitude = bbox[3] self.geometry_parameters.antimeridian_crossing = antimeridian_crossing self.geometry_parameters.npole_crossing = npole_crossing self.geometry_parameters.spole_crossing = spole_crossing # Append geometry_parameters dictionary to default and optional properties void = props.update(self.geometry_parameters.__dict__) # Set GeoJSON Feature as observation footprint self.footprint = Feature(geometry=geojson_geometry, properties=props, bbox=bbox) # Split footprint GeoJSON geometry if requested if split: logger.info('TAMN splitting footprint GeoJSON Geometry.') self.footprint = tamn.getSplitFeature(self.footprint)
[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))