Source code for gfinder.geometry

"""Geometry module."""

from gfinder.target import Target
from gfinder.detector import Detector
import gfinder.simulator as simulator

import numpy as np
import spiceypy as spice
import spiceypy.utils.support_types as stypes

import geojson
from geojson import Polygon, Point, Feature, MultiPolygon
import inspect
import copy
import re

# Constants
IFOV = 0.00015 # radians
MAJIS_SCAN_ANGLE_MAX = 4.0 # degrees
ABCORR = 'LT+S'
OBSRVR = 'JUICE'
JUPITER_RADIUS = 71492.0  # Jupiter equatorial radius
RINGS_RADIUS_MIN = JUPITER_RADIUS  # 1.8 * JUPITER_RADIUS  # main ring radius
RINGS_RADIUS_MAX = 100*JUPITER_RADIUS  # 3.5 * JUPITER_RADIUS  # Gossamer ring radius

# Turning SpiceyPy found check off
spice.found_check_off()


[docs]class Geometry: """Abstract class that represents any types of geometrical data associated to an instant acquisition (measurement), an observation, or a sequence. Args: name (str): name of the Geometry class geoevt (GeometryEvent): associated GeometryEvent object (passed as optional argument to the constructor) searchable (boolean): whether or not this class of Geometry object is searchable required_parameters_sets (list): list of required parameters parameters (dict): parameters required to compute geometry data dimension (int): data array dimension (default is 0, scalar) units (float): data units prefix (float): prefix to be used for naming each data array element. For example, SubSC_Point, which is 2-element vector data, prefixes are defined to be ['lon', 'lat'] (see :class:`SubSC_Point`). data (NoneType): data value, which can be a scalar, vector, or multidimentional array. valid (boolean): whether or not Geometry object is valid """ def __init__(self, parameters, geoevt=None, silent=None): """Constructor class. Args: parameters (dict): parameters to be used for computation geoevt(GeometryEvent): associated GeometryEvent object (None by default) Returns: Geometry: Geometry object """ self.name = self.__class__.__name__ self.geoevt = geoevt self.searchable = False # TODO: check if it is used self.required_parameters_sets = [[]] self.parameters_set_index = -1 self.parameters = {} self.condition = None self.dimension = 0 self.units = '' self.data = None self.prefix = '' self.valid = False self.silent = silent def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.getDict())
[docs] def get_parent_class(self): return self.__class__.__bases__[0].__name__
[docs] def check_validity(self): """Check overall validity of Geometry object. Checked when not abstract objects are created. """ if self.valid: if self.dimension == 0: # both prefix and units must be of type str. if not isinstance(self.prefix, str) or not isinstance(self.units, str): print('WARNING: `prefix` and `units` must be of type str.') self.valid = False elif self.dimension == 1: if self.data is None: print('WARNING: Default `data` not set.') self.valid = False if isinstance(self.prefix, str): # set default prefixes self.prefix = [] for i in range(len(self.data)): self.prefix.append(str(i)) if isinstance(self.units, list): if len(self.prefix) != len(self.units): print(f'WARNING: Discrepancy between the number of prefixes and units, respectively: {len(self.prefix)} and {len(self.units)}.') self.valid = False elif isinstance(self.units, str): # set default prefix for all list element units = self.units self.units = [] for i in range(len(self.prefix)): self.units.append(units)
# else: # print(self.name, self.dimension)
[docs] def check_parameters_set(self, parameters, required_parameters_set): """Check input parameters against a set of required parameters. Args: parameters (dict): input parameters to be checked. required_parameters_set (list): Returns: valid (bool), missing_parameters (list), extra_parameters (list) """ # Init variables missing_parameters = [] extra_parameters = [] valid = True # check that each required parameter is available in input parameters for required_parameter_name in required_parameters_set: if required_parameter_name not in parameters.keys(): missing_parameters.append(required_parameter_name) valid = False # check that there are no extra parameters for parameter in parameters: if parameter not in required_parameters_set: extra_parameters.append(parameter) valid = False return valid, missing_parameters, extra_parameters
[docs] def set_parameters(self, parameters): """Check that required input parameters are provided, and set input parameters. Args: parameters (dict): input parameters to be used for computation. """ valid = False self.parameters_set_index = -1 missing_parameters_sets = [] extra_parameters_sets = [] # check against each required parameters set for i, required_parameters_set in enumerate(self.required_parameters_sets): valid, missing_parameters, extra_parameters = self.check_parameters_set(parameters, required_parameters_set) missing_parameters_sets.append(missing_parameters) extra_parameters_sets.append(extra_parameters) if valid: self.parameters_set_index = i valid = True break # Set parameters if valid if valid: self.parameters = parameters self.valid = True else: if not self.silent: print(f'WARNING: Invalid input {self.name} parameters: {parameters}') print() for i in range(len(self.required_parameters_sets)): print(f'Required parameters set #{i}: {self.required_parameters_sets[i]}') if missing_parameters_sets[i]: print(f' - missing parameters: {missing_parameters_sets[i]}') if extra_parameters_sets[i]: print(f' - extra parameters: {extra_parameters_sets[i]}') print() self.valid = False # check overall validity of Geoemetry object self.check_validity()
[docs] def getDict(self): """Returns Geometry object. Returns: dict: Geometry object time_inputs_dict """ geometry_dict = { 'name': self.name, 'parameters': self.parameters, 'value': self.data } return geometry_dict
[docs] def get_csv_header(self): header = '' class_name = self.__class__.__name__.lower() if self.dimension == 0: header = f'{class_name}' if self.units: header += f' ({self.units})' elif self.dimension == 1: headers = [] for i, (prefix, unit) in enumerate(zip(self.prefix, self.units)): header = f'{class_name}:{prefix}' if unit: header += f' ({unit})' headers.append(header) header = ','.join(headers) elif self.dimension >= 2: header = None return header
[docs] def get_csv_line(self): line = '' if self.dimension == 0: if self.data is None: line = '' else: line = str(self.data) elif self.dimension == 1: values = [] data = self.data if isinstance(self.data[0], list): # workaround when forcing dimension=1 for single-vector Director and Geolocation children classes data = self.data[0] for value in data: if value is None: values.append('') else: values.append(str(value)) line = ','.join(values) elif self.dimension >= 2: line = None return line
[docs] def get_time_step(self): time_step = 1.0 # default if no associated GeometryEvent object if self.geoevt: time_step = self.geoevt.parent_event.time_step return time_step
[docs] def compute(self): """Compute this Geometry object data at ephemeris time `et`. """ pass
[docs] def getData(self): return self.data
[docs] def getGeoJSON(self): return None
[docs] def search(self): pass
[docs] def isSearchable(self): return self.searchable
[docs] def getName(self): return self.name
[docs] def getCondition(self): return self.condition
[docs] def is_valid(self): return self.valid
[docs]class Condition: def __init__(self, condition): self.relate = condition['relationalCondition'] self.refval = condition['referenceValue'] self.adjust = condition['adjustmentValue'] def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.__dict__)
[docs]class GeometryFactory: """Class responsible for creating Geometry objects from input geometry definition. Args: classes_index (dict): index of all Geometry classes available. excluded_class_names (List(str)): lowlevel_class_names (List(str)): highlevel_class_names (List(str)): """ def __init__(self): """Constructor method. """ # Make a copy of the current global symbol table (data structure maintained by a compiler which contains all # necessary information about the program. globals_copy = copy.copy(globals()) # Set excluded, low-level and high-level Geometry class names self.excluded_class_names = [ 'Geometry', 'Condition', 'GeometryFactory', 'Scalar', 'Vector', 'Multidimentional', 'SimulatedSCFrameError' ] self.lowlevel_class_names = ['Scalar', 'Vector', 'Multidimentional'] self.highlevel_class_names = ['Direction', 'Geolocation'] self.classes_index = {} for this_class_name in globals_copy.keys(): if this_class_name not in self.excluded_class_names: this_class = globals_copy[this_class_name] if re.search("^(<class 'gfinder.geometry.*'>)", str(this_class)): self.classes_index.update({this_class_name: this_class}) def __repr__(self): return '<%s %r>' % (self.__class__.__name__, self.__dict__)
[docs] def get_geometries(self, name_filter='', class_filter='', searchable=False): """Returns Geometry class names matching input query. Args: searchable: name_filter: class_filter: Returns: """ geometry_class_names = [] for geometry_class_name in self.classes_index: if searchable: # print(f'is {geometry_class_name} searchable?') print(self.get_geometry_info(geometry_class_name)['is_searchable']) if self.get_geometry_info(geometry_class_name)['is_searchable']: geometry_class_names.append(geometry_class_name) elif class_filter: if class_filter == self.get_geometry_info(geometry_class_name)['parent_class']: geometry_class_names.append(geometry_class_name) else: geometry_class_names.append(geometry_class_name) if name_filter: geometry_class_names_copy = copy.copy(geometry_class_names) for geometry_class_name in geometry_class_names_copy: if name_filter.lower() not in geometry_class_name.lower(): geometry_class_names.remove(geometry_class_name) return geometry_class_names
[docs] def get_geometry_info(self, class_name): """Return information about a Geometry class. Args: class_name: Returns: geometry_class_dict """ geometry_class_dict = {} if class_name in self.classes_index.keys(): # Create Geometry object corresponding to input class name geometry = self.classes_index[class_name]({}, silent=True) # could use the create() method, but geometry_def is required. short_description = '' if geometry.__doc__: short_description = geometry.__doc__.split('\n')[0] geometry_class_dict = dict( parent_class=geometry.__class__.__bases__[0].__name__, # 'Scalar', 'Vector' or 'Multidimensional' is_searchable=geometry.isSearchable(), required_parameters_sets=geometry.required_parameters_sets, prefixes=geometry.prefix, units=geometry.units, short_description=short_description, source=inspect.getsource(self.classes_index[class_name].compute) ) return geometry_class_dict
[docs] def get_geometry_required_parameters(self, class_name, set_index=0): """Return a Geometry class required parameters set dict.""" if class_name in self.classes_index.keys(): # Create Geometry object corresponding to input class name geometry = self.classes_index[class_name]({}, silent=True) # could use the create() method, but geometry_def is required. if set_index < len(geometry.required_parameters_sets): required_parameters_set = geometry.required_parameters_sets[set_index] return dict.fromkeys(required_parameters_set, None) # TODO: Below is an example of what would be a much better definition for required parameters sets. # self.required_parameters_sets = [ # {'start_angle': 0.0, 'start_scan_rate': 0.0, 'stop_scan_rate': 0.0, 'method': ''}, # {'start_angle': 0.0, 'stop_angle': 0.0, 'start_scan_rate': 0.0, 'stop_scan_rate': 0.0, 'method':''}, # {'start_angle': 0.0, 'scan_rate': 0.0} # ] else: raise IndexError(f'Input set index exceeds number of required parameters sets: {len(geometry.required_parameters_sets)}') else: raise AttributeError(f'Input `{class_name}` Geometry class name does not exist.')
[docs] def create(self, geometry_def, geoevt=None): name = geometry_def['name'] parameters = geometry_def['parameters'] condition = None if 'condition' in geometry_def.keys(): condition = Condition(geometry_def['condition']) if name in self.classes_index.keys(): geometry_class = self.classes_index[name] geometry = geometry_class(parameters, geoevt=geoevt) # Workaround to assign condition to Geometry object. # TODO: Geometry classes must be re-designed/implemented to take into account for condition. # (this should be done in concert with opportunity./sequence.search() methods) geometry.condition = condition return geometry else: geometry = Geometry({}) geometry.valid = False print(f'[WARNING] {name} Geometry class name does not exist. Add to geometry.py modules or check for valid input name.') return geometry
[docs]class Scalar(Geometry): """Abstract Geometry class that holds scalar data. Args: type (str): scalar type: ``numerical`` (default) or ``boolean`` """ def __init__(self, parameters, geoevt=None, silent=None): """Scalar constructor class. Returns: Scalar: Scalar object """ super().__init__(parameters, geoevt=geoevt, silent=silent) self.type = 'numerical' # numerical or boolean self.dimension = 0 self.valid = True def __format__(self, geometry_fmt): if self.data is not None: str = '{:f}'.format(self.data) else: str = '{:f}'.format(0) return str
[docs] def getData(self): return self.data
[docs] def search(self, condition, cnfine, step): # cnfine is search_window """Search and return times when input geometric condition is met. Args: condition: input condition that relates to the scalar quantity. cnfine (SpiceCell): SPICE window to which the search is restricted. step (float): Step size used for locating extrema and roots. Returns: SpiceCell: SPICE window containing result """ relate = condition.relate refval = condition.refval adjust = condition.adjust nintvls = 2000 # 2*n + ( m / step ) @spice.utils.callbacks.SpiceUDFUNS def gfq(et): value = self.compute(et) return value @spice.utils.callbacks.SpiceUDFUNB def gfdecrx(udfuns, et): return spice.uddc(udfuns, et, 1.0) @spice.utils.callbacks.SpiceUDFUNS def dummy_udf(et): return et @spice.utils.callbacks.SpiceUDFUNB def gfq_boolean(udfuns, et): bool_value = self.compute(et) if refval: # true return bool_value else: return not (bool_value) result = stypes.SPICEDOUBLE_CELL(100000) if self.type == 'boolean': spice.gfudb(dummy_udf, gfq_boolean, step, cnfine, result) else: spice.gfuds(gfq, gfdecrx, relate, refval, adjust, step, nintvls, cnfine, result) # spice.gfuds(udfuns, udqdec, relate, refval, adjust, step, nintvls, cnfine, result) return result
[docs]class Vector(Geometry): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 self.valid = True
[docs] def getData(self, prefix=None): """Returns Geometry data as a list or list element (given its prefix) Args: prefix: Name of the prefix to get data element from. """ prefixes = self.prefix if prefix in prefixes: data_element = self.data[prefixes.index(prefix)] return data_element else: return self.data
[docs]class Multidimentional(Geometry): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 2 self.valid = True
################################################################################ ################################################################################ ## ## GEOMETRY CLASSES ## ################################################################################ ################################################################################
[docs]class Direction(Multidimentional): """Direction abstract class - Any Direction children classes/objects can be used as input vector for the simulated SC attitude (when not pointing-dependent !!) - It can be used as input to the Directions_Separation_Angle class. - It can be represented in the "pointing"/"NOA" coordinates space (PointingViewer) """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.data = [None, None, None] self.prefix = ['dx', 'dy', 'dz'] self.units = '' self.n_vectors = 0 self.reference_frame = 'J2000' # default
[docs] def setDirection(self, frame, vectors): # 3-element list (vector), or list of 3-element list (vector) # set reference frame self.reference_frame = frame # check inputs vectors n_valid_vectors = 0 n_total_vectors = 1 if isinstance(vectors, np.ndarray): vectors = vectors.tolist() for vec in vectors: if isinstance(vec, list): n_total_vectors = len(vectors) if len(vec) == 3: n_valid_vectors += 1 else: if len(vectors) == 3: n_valid_vectors = 1 vectors = [vectors] break if n_valid_vectors == n_total_vectors: self.n_vectors = n_valid_vectors self.data = vectors else: self.n_vectors == 0 print('WARNING: Unable to set direction vectors.')
# print(vectors)
[docs] def get_pointing_angles(self, et, observer, target, frame='NOA'): """Computes the directions pointing angles in a two-dimensional coordinate system based on NOA (default) or NPO SPICE dynamic frames. """ # Set direction vectors to be transformed to NOA angles dir_vectors = self.data pointing_frame = f'{observer}_{target}_{frame}' rotmat = spice.pxform(self.reference_frame, pointing_frame, et) dir_noa_angles = [] for dir_vec in dir_vectors: # transform direction vector into NOA frame dir_noa_vec = spice.mxv(rotmat, dir_vec) # derive "pointing" angles from NOA direction vector yz_plane = spice.nvp2pl([1.0, 0.0, 0.0], [0.0, 0.0, 0.0]) xz_plane = spice.nvp2pl([0.0, 1.0, 0.0], [0.0, 0.0, 0.0]) p_yz = spice.vprjp(dir_noa_vec, yz_plane) # projection of dir_noa_vec onto YZ_NOA plane p_xz = spice.vprjp(dir_noa_vec, xz_plane) # projection of dir_noa_vec onto YZ_NOA plane theta_x = np.sign(p_xz[0]) * spice.vsep([0.0, 0.0, 1.0], p_xz) * spice.dpr() # rotation angle between Z_NOA and p_xz, in degrees theta_y = np.sign(p_yz[1]) * spice.vsep([0.0, 0.0, 1.0], p_yz) * spice.dpr() # rotation angle between Z_NOA and p_xz, in degrees dir_noa_angles.append([theta_x, -theta_y]) # TODO: "minused" theta_y to fix issue, valid ? return dir_noa_angles
[docs] def toJ2000(self, et, reference_frame='', vectors=[]): # Transform a vector defined in given a reference frame into J2000 frame, taking into account for SC and MAJIS # scanner pointing simulator. # # - Vectors defined in SC or SC-fixed frames are transformed using SC pointing simulator (when enabled/on): # # SC frame = 'JUICE_SPACECRAFT' -> Simulated_SC_Frame # SC-fixed frames = [ 'JUICE_MAJIS_BASE', 'JUICE_MAJIS' ] # # - Vectors defined in Scan (SC moving structure) or Scan-fixed frames are transformed using Scan pointing # simulator (when enabled/on): # # Scan frame = 'JUICE_MAJIS_SCAN' -> Simulated_Scan_Frame # Scan-fixed frames = [ 'JUICE_MAJIS_VISNIR', 'JUICE_MAJIS_IR'] # # - Vectors defined in any other frames are transformed using SPICE Frames sub-system pxform() function. # # Set input vectors and reference_frame external_call = False if reference_frame and vectors: external_call = True frame = reference_frame vectors = vectors else: frame = self.reference_frame vectors = self.data # Init output J2000 vectors list j2000_vectors = [] # Transform input vector in J2000 using SPICE Frames sub-system pxform() function, when: # - input frame can not be simulated (eg: IAU_CALLISTO), or # - simulator is disabled. if frame not in simulator.frames() or simulator.is_off(): frame_j2000_rotmat = spice.pxform(frame, 'J2000', et) for vec in vectors: j2000_vectors.append(spice.mxv(frame_j2000_rotmat, vec).tolist()) if not external_call: self.reference_frame = 'J2000' self.data = j2000_vectors return j2000_vectors # Check that required simulated frame Geometry objects are accessible if simulator.is_on(): if self.geoevt is None: print( 'WARNING: Unable to simulate pointing because Direction object is not associated to any GeometryEvent object.') return None else: # (the following could be easily scripted: for frame in simulator.simulated_frames ...) if simulator.is_on(frame='JUICE_SPACECRAFT'): if not self.geoevt.has_geometry('Simulated_SC_Frame'): print( 'WARNING: Unable to simulate SC pointing because Simulated_SC_Frame not found in associated GeometryEvent.') return None if simulator.is_on(frame='JUICE_MAJIS_SCAN'): if not self.geoevt.has_geometry('Simulated_Scan_Frame'): print( 'WARNING: Unable to simulate Scan pointing because Simulated_Scan_Frame not found in associated GeometryEvent.') return None # (the following would be way more difficult to script) if frame in simulator.frames(frame='JUICE_MAJIS_SCAN'): # input vector defined in a Scan/Scan-fixed frame if simulator.is_on(frame='JUICE_MAJIS_SCAN'): # compute transformation matrix from input frame to SC frame, using Scan frame simulator frame_scan_rotmat = spice.pxform(frame, 'JUICE_MAJIS_SCAN', et) # fixed #simscan_base_rotmat = self.geoevt.get_geometry_data('Simulated_Scan_Frame') if et == self.geoevt.reference_time: # retrieve pre-computed simulated frame at epoch et simscan_base_rotmat = self.geoevt.get_geometry_data('Simulated_Scan_Frame') else: # compute simulated frame at epoch et. eg: Detector_Boresight_Ground_Drift parameters = self.geoevt.get_geometry_parameters('Simulated_Scan_Frame') simscan_base_rotmat = Simulated_Scan_Frame(parameters, geoevt=self.geoevt).compute(et) frame_base_rotmat = spice.mxm(frame_scan_rotmat, simscan_base_rotmat) # hyp: scan = simscan base_sc_rotmat = spice.pxform('JUICE_MAJIS_BASE', 'JUICE_SPACECRAFT', et) # fixed frame_sc_rotmat = spice.mxm(frame_base_rotmat, base_sc_rotmat) else: # using SPICE pxform() function frame_sc_rotmat = spice.pxform(frame, 'JUICE_SPACECRAFT', et) # compute input vectors coordinates in SC frame. sc_vectors = [] for vec in vectors: sc_vectors.append(spice.mxv(frame_sc_rotmat, vec).tolist()) else: # input vector defined in a SC/SC-fixed frame # compute input vector coordinates in SC frame, using SPICE pxform() function. frame_sc_rotmat = spice.pxform(frame, 'JUICE_SPACECRAFT', et) # fixed sc_vectors = [] for vec in vectors: sc_vectors.append(spice.mxv(frame_sc_rotmat, vec).tolist()) # transform SC vector (computed in above step) into J2000 if simulator.is_on(frame='JUICE_SPACECRAFT'): # using SC frame simulator if et == self.geoevt.reference_time: # retrieve pre-computed simulated frame at epoch et simsc_j2000_rotmat = self.geoevt.get_geometry_data('Simulated_SC_Frame') else: # compute simulated frame at epoch et. eg: Detector_Boresight_Ground_Drift parameters = self.geoevt.get_geometry_parameters('Simulated_SC_Frame') simsc_j2000_rotmat = Simulated_SC_Frame(parameters, geoevt=self.geoevt).compute(et) for sc_vec in sc_vectors: j2000_vectors.append(spice.mxv(simsc_j2000_rotmat, sc_vec).tolist()) else: # using SPICE pxform() function sc_j2000_rotmat = spice.pxform('JUICE_SPACECRAFT', 'J2000', et) for sc_vec in sc_vectors: j2000_vectors.append(spice.mxv(sc_j2000_rotmat, sc_vec).tolist()) if not external_call: self.reference_frame = 'J2000' self.data = j2000_vectors return j2000_vectors
# TODO: Properly implement a Ring_Location class and subsequent design. # class Ring_Location(Vector): # def __init__(self, parameters, geoevt=None, silent=None): # super().__init__(parameters, geoevt=geoevt, silent=silent) # # self.n_points = 0 # self.data = [] # self.target = None from gfinder.utils import split_polygon
[docs]class Geolocation(Multidimentional): """Geolocation abstract class - MapViewer """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.data = [None, None, None] self.prefix = ['lon', 'lat', 'alt'] self.units = 'deg' self.n_points = 0 self.target = None
[docs] def setGeolocation(self, points): # 3-element list (point), or list of 3-element list (points) # check input points n_valid_points = 0 n_total_points = 1 if isinstance(points, np.ndarray): points = points.tolist() for point in points: if isinstance(point, list): n_total_points = len(points) if len(point) == 3: n_valid_points += 1 else: if len(points) == 3: n_valid_points = 1 points = [points] break if n_valid_points == n_total_points: self.n_points = n_valid_points else: self.n_points = 0 print('WARNING: Unable to set geolocation point coordinates.') # print(points) points = [] #print('>> setGeolocation: n_points', self.n_points) # print(self.n_points) # print(type(points)) # print(points) # convert from rectangular to planetocentric coordinates geopoints = [] for point in points: # compute nearest point on the surface to input observered point, which can be above (alt > 0), on (alt = 0), # or below the surface (alt < 0) srfpt, alt = spice.nearpt(point, self.target.radii[0], self.target.radii[1], self.target.radii[2]) if np.isclose(alt, 0.0): alt = 0.0 # derive planetocentric coordinates. r, lon, lat = spice.reclat(srfpt) # add to the list of points geopoints.append([lon * spice.dpr(), lat * spice.dpr(), alt]) self.data = geopoints
[docs] def getGeoJSON(self, surface_only=False, split=False): self.n_points = len(self.data) # needed for when loaded opportunity data (only self.data state is preserved) if self.n_points < 1: feature = Feature() elif self.n_points == 1: point = self.data[0] feature = Feature(geometry=Point((point[0], point[1], point[2])), properties={}) else: coordTuples = [] for point in self.data: if surface_only: # only add points that lie onto the surface if point[2] <= 0.0: # coordTuples.append((point[0], point[1], point[2])) coordTuples.append((point[0], point[1])) else: coordTuples.append((point[0], point[1])) #, point[2])) # lon, lat, alt # TODO: unpatch -> add point[2] above # ~/workspace/git-repos/gfinder-python-cli/gfinder/utils.py in split_polygon(geojson, output_format, validate) # 229 crossings = 0 # 230 # --> 231 for coord_index, (lon, _) in enumerate(ring[1:], start=1): # 232 lon_prev = ring[coord_index - 1][0] # [0] corresponds to longitude coordinate # 233 if check_crossing(lon, lon_prev, validate=validate): # # ValueError: too many values to unpack (expected 2) if len(coordTuples) > 2: coordTuples = [coordTuples + [coordTuples[0]]] # complete coordinates tuples to close Polygon feature = Feature(geometry=Polygon(coordTuples), properties={}) if split: if len(feature.geometry.coordinates) > 0: try: split_polygons = split_polygon(feature.geometry) except: print(f'[WARNING] Could not split polygon: {feature.geometry}') split_polygons = [] if len(split_polygons) > 1: # assuming polygon crosses antimeridian multi_polygon = [] for polygon in split_polygons: multi_polygon.append(geojson.loads(polygon).coordinates) feature = Feature(geometry=MultiPolygon(multi_polygon), properties={}) #feature = tamn.getSplitFeature(feature) return feature
[docs] def to_rectangular(self): """ Returns the rectangular coordinates of the Geolocation points. """ recpoints = [] for geopoint in self.data: recpoints.append(spice.georec(geopoint[0]*spice.rpd(), geopoint[1]*spice.rpd(), geopoint[2], self.target.re, self.target.f)) return recpoints
[docs] def angular_sep(self, geolocation): """ Returns ground angular separation between two geolocations (surface points). Args: geolocation: Returns: """ # print('> self.n_points = {}, geolocation.n_points = {}'.format(self.n_points, geolocation.n_points)) # print('> len(self.data) = {}, len(geolocation.data) = {}'.format(len(self.data), len(geolocation.data))) spoint1 = self.to_rectangular()[0] if self.n_points > 0 else [0.0, 0.0, 0.0] spoint2 = geolocation.to_rectangular()[0] if geolocation.n_points > 0 else [0.0, 0.0, 0.0] # print('> angular_sep: spoint1={}, spoint2={}'.format(spoint1, spoint2)) angular_sep = spice.vsep(spoint1, spoint2) return angular_sep
[docs]class Surface_Point_Direction(Direction): # Direction vector from SC to a given target surface point in J2000 frame """Direction vector from SC to a given target surface point in J2000 frame. Args: required_parameters_sets (list): ['target', 'longitude', 'latitude'] data (list): Example of ODF definition:: { "name": "Surface_Point_Direction", "parameters": { "target": "JUPITER", "longitude": 0.0, "latitude": 0.0 } } """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target', 'longitude', 'latitude']] self.set_parameters(parameters) #self.reference_frame = Target(self.parameters['target']).frame
[docs] def compute(self, et): # set default parameters target = Target(self.parameters['target']) lons = self.parameters['longitude'] lats = self.parameters['latitude'] if not isinstance(lons, list): lons = [ lons ] if not isinstance(lats, list): lats = [ lats ] alt = 0.0 spoint_dir_vectors = [] target_pos, ltime = spice.spkpos(target.name, et, target.frame, 'NONE', OBSRVR) for lon, lat in zip(lons, lats): lon = lon * spice.rpd() lat = lat * spice.rpd() # compute surface point direction from spacecraft in target's body-fixed frame spoint = spice.georec(lon, lat, alt, target.re, target.f) spoint_dir_vectors.append((spoint+target_pos).tolist()) self.setDirection(target.frame, spoint_dir_vectors) self.toJ2000(et) return self.data
[docs]class North_Pole_Direction(Direction): # SC to north-pole direction in J2000 frame """North_Pole_Direction. Args: required_parameters_sets (list): ['target'] data (list): """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) longitude = 0.0 latitude = 90.0 self.data = Surface_Point_Direction( {'target': target.name, 'longitude': longitude, 'latitude': latitude}).compute(et) return self.data
[docs]class SC_Slew_Angles(Vector): """SC_Slew_Angles. Attributes: required_parameters_sets (list): [['x_start','x_stop', 'y_start', 'y_stop'], ['x_start', 'x_rate', 'y_start', 'y_rate']] data (list): """ # TODO: Rename to SC_Offset_Angles and return PTR-convention X-offset and Y-offset angles. def __init__(self, parameters, geoevt=None, silent=None, et_start=None, et_stop=None): """Constructs SC_Slew_Angles object. Args: parameters: geoevt: silent: et_start (float, optional): start ephemeris time. et_stop: """ super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['x_rot', 'y_rot'] self.data = [None, None] self.units = 'rad' self.required_parameters_sets = [ ['x_start', 'x_stop', 'y_start', 'y_stop'], # x_rate/y_rate relaxed ['x_start', 'x_rate', 'y_start', 'y_rate'], # x_stop/y_stop relaxed ['x_start', 'x_stop', 'x_rate'], ['x_start', 'y_start'], # constant x-y offset ['y_start', 'x_start', 'x_stop', 'x_rate'], # to be able to map with PTR parameters # ['x_start', 'y_start', 'scan_delta', 'scan_speed'], # PTR approach and convention ...later. ] self.et_start = None self.et_stop = None if et_start is not None: self.et_start = et_start if et_stop is not None: self.et_stop = et_stop self.set_parameters(parameters)
[docs] def compute(self, et): # Retrieve start and stop time of the observation associated to this measurement's Geometry object if self.geoevt: et_start = self.geoevt.parent_event.get_start_time() et_stop = self.geoevt.parent_event.get_stop_time() elif (self.et_start is not None) and (self.et_stop is not None): et_start = self.et_start et_stop = self.et_stop else: print('Missing associated GeometryEvent object or `et_start` and `et_stop` optional argument.') return [None, None] if self.parameters_set_index == 0: # ['x_start', 'x_stop', 'y_start', 'y_stop'] # x_rot a = (self.parameters['x_stop'] - self.parameters['x_start']) / (et_stop-et_start) b = self.parameters['x_start'] - a * et_start x_rot = (a*et + b) * spice.rpd() # y_rot a = (self.parameters['y_stop'] - self.parameters['y_start']) / (et_stop-et_start) b = self.parameters['y_start'] - a * et_start y_rot = (a*et + b) * spice.rpd() elif self.parameters_set_index == 1: # ['x_start', 'x_rate', 'y_start', 'y_rate'] # x_rot angle at time et b = self.parameters['x_start'] - self.parameters['x_rate'] * et_start x_rot = (self.parameters['x_rate']*et + b) * spice.rpd() # y_rot angle at time et b = self.parameters['y_start'] - self.parameters['y_rate'] * et_start y_rot = (self.parameters['y_rate']*et + b) * spice.rpd() elif self.parameters_set_index == 2: # ['x_start', 'x_stop', 'x_rate'] # x_rot angle at time et b = self.parameters['x_start'] - self.parameters['x_rate'] * et_start x_rot = (self.parameters['x_rate'] * et + b) if self.parameters['x_stop'] >= 0: # x_rot can't be higher than x_rot(et_stop) x_rot = min(x_rot, self.parameters['x_stop']) * spice.rpd() else: # x_rot can't be lower than x_rot(et_stop) x_rot = max(x_rot, self.parameters['x_stop']) * spice.rpd() # set fixed no rotation around Y y_rot = 0.0 elif self.parameters_set_index == 3: # ['x_start', 'y_start'] x_rot = self.parameters['x_start'] * spice.rpd() y_rot = self.parameters['y_start'] * spice.rpd() elif self.parameters_set_index == 4: # ['y_start', 'x_start', 'x_stop', 'x_rate'] # x_rot angle at time et b = self.parameters['x_start'] - self.parameters['x_rate'] * et_start x_rot = (self.parameters['x_rate'] * et + b) if self.parameters['x_stop'] >= 0: # x_rot can't be higher than x_rot(et_stop) x_rot = min(x_rot, self.parameters['x_stop']) * spice.rpd() else: # x_rot can't be lower than x_rot(et_stop) x_rot = max(x_rot, self.parameters['x_stop']) * spice.rpd() # set constant rotation around Y y_rot = self.parameters['y_start'] * spice.rpd() else: return [None, None] self.data = [x_rot, y_rot] return self.data
# class MAJIS_Scan_Rate_Angle(Scalar): # """MAJIS_Scan_Rate_Angle. # # Args: # required_parameters_sets (list): ['x_start','x_rate'] # data (list): # """ # # def __init__(self, parameters, geoevt=None, silent=None): # super().__init__(parameters, geoevt=geoevt, silent=silent) # # self.prefix = ['x_rot', 'y_rot'] # self.data = 0.0 # self.units = 'radians' # self.required_parameters_sets = [['x_start','x_rate']] # rates in iFOV (0.00015 radians) ! # # self.set_parameters(parameters) # # def compute(self, et): # # set default parameters # x_start = self.parameters['x_start'] # x_rate = self.parameters['x_rate'] # # # Retrieve start and stop time of the observation associated to this measurement's Geometry object # et_start = self.geoevt.parent_event.get_start_time() # et_stop = self.geoevt.parent_event.get_stop_time() # # # x_rot angle at time et # a = x_rate * IFOV # (x_stop - x_start) / (et_stop - et_start) # b = x_start*spice.rpd() - a * et_start # x_rot = (a * et + b) # * spice.rpd() # # self.data = x_rot # # return self.data
[docs]class MAJIS_Scan_Angle(Scalar): """MAJIS_Scan_Angle. Args: required_parameters_sets (list): ['start_angle','start_scan_rate', 'stop_scan_rate', 'method'] data (list): """ def __init__(self, parameters, geoevt=None, silent=None, scan_duration=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # self.data = 0.0 self.units = 'rad' self.required_parameters_sets = [ ['start_angle', 'start_scan_rate', 'stop_scan_rate', 'method'], ['start_angle', 'stop_angle', 'start_scan_rate', 'stop_scan_rate', 'method'], ['start_angle', 'scan_rate'] ] # TODO: Below is an example of what would be a much better definition for required parameters sets. # self.required_parameters_sets = [ # {'start_angle': 0.0, 'start_scan_rate': 0.0, 'stop_scan_rate': 0.0, 'method': ''}, # {'start_angle': 0.0, 'stop_angle': 0.0, 'start_scan_rate': 0.0, 'stop_scan_rate': 0.0, 'method':''}, # {'start_angle': 0.0, 'scan_rate': 0.0} # ] self.scan_duration = None if scan_duration: self.scan_duration = scan_duration # Set MAJIS scanner parameters self.scanner_angular_step = 360 / 8192 / 28 scanner_start_position = 2821 # "min" scanner_stop_position = 5369 # "max" n_scanner_total_steps = scanner_stop_position-scanner_start_position # 2548 self.scanner_angular_range = self.scanner_angular_step * n_scanner_total_steps self.valid_scanner_angles = np.linspace(-self.scanner_angular_range/2, self.scanner_angular_range/2, n_scanner_total_steps+1) self.valid_scanner_positions = np.arange(scanner_start_position, scanner_stop_position+1) self.set_parameters(parameters) if self.valid: if self.parameters_set_index == 2: self.method = "discrete" # default else: self.set_scan_parameters() self.method = self.parameters['method'] # 'MC', for testing
[docs] def compute(self, et): # temporary code for testing purpose only if self.method == 'MC': mc_angle = self.geoevt.parent_event.get_sub_events_geometry_data('Motion_Compensation_Angle')[self.geoevt.index] majis_scan_angle_obj = MAJIS_Scan_Angle({}, silent=True) valid_scanner_angle, scanner_position = majis_scan_angle_obj.get_valid_scanner_angle(mc_angle*spice.dpr()/2) self.data = valid_scanner_angle*spice.rpd()*2 return self.data et_start = 0.0 if self.geoevt: et_start = self.geoevt.parent_event.get_start_time() time = et-et_start if self.parameters_set_index == 2: # ['start_angle', 'scan_rate'] (deg, deg/s) start_angle = self.parameters['start_angle'] scan_rate = self.parameters['scan_rate'] scan_angle = (scan_rate*time + start_angle) * 2.0 * spice.rpd() else: scan_angle = self.get_scanner_angle(time) * 2.0 * spice.rpd() #print('>>', scan_angle) self.data = scan_angle # optical, in radians return self.data
[docs] def set_scan_parameters(self): """ Set scan parameters from input commanding parameters. start_angle: stop_angle: n_steps: positions: times: t_scan: t_scan_yl: """ start_angle = self.parameters['start_angle'] start_scan_rate = self.parameters['start_scan_rate'] stop_scan_rate = self.parameters['stop_scan_rate'] self.start_angle = start_angle if 'stop_angle' in self.parameters.keys(): self.stop_angle = self.parameters['stop_angle'] # asymmetrical scan else: self.stop_angle = -start_angle # symmetrical scan # print('>>>', self.stop_angle) valid_start_angle, self.start_position = self.get_valid_scanner_angle(self.start_angle) valid_stop_angle, self.stop_position = self.get_valid_scanner_angle(self.stop_angle) self.n_steps = np.abs(self.stop_position-self.start_position) # always 2548 for a full-range scan self.positions = np.linspace(self.start_position, self.stop_position, self.n_steps+1, dtype=int) self.angles = np.linspace(self.start_angle, self.stop_angle, self.n_steps+1) self.delta_scan_rate = 0.0 if self.n_steps != 0: self.delta_scan_rate = (stop_scan_rate - start_scan_rate) / self.n_steps delta_times = [] for n, position in enumerate(self.positions): scan_rate = start_scan_rate + self.delta_scan_rate * n delta_time = 0.0 if scan_rate != 0.0: delta_time = abs(self.scanner_angular_step / scan_rate) delta_times.append(delta_time) delta_times = np.array(delta_times) self.times = np.zeros(self.n_steps+1) for n, delta_time in enumerate(delta_times): if n < self.n_steps: self.times[n+1] = self.times[n] + delta_time self.t_scan = np.sum(delta_times[:-1]) self.t_scan_yl = 0.0 if stop_scan_rate != start_scan_rate: if stop_scan_rate != 0.0: self.t_scan_yl = np.abs(self.scanner_angular_range / (stop_scan_rate-start_scan_rate)*np.log(stop_scan_rate/start_scan_rate))
[docs] def get_scanner_angle(self, time): """ Returns scanner angle corresponding to a given time since scan start. Args: time: """ if self.method == 'discrete': position = round(np.interp(time, self.times, self.positions)) scanner_angle = self.position_to_angle(position) elif self.method == 'continuous': scanner_angle = np.interp(time, self.times, self.angles) else: print('[WARNING] Unknown MAJIS_Scan_Angle method.') scanner_angle = 0.0 return scanner_angle
[docs] def position_to_angle(self, position): """ Returns scanner angle corresponding to a given scanner position. Args: position: """ scanner_angle = None indexes = np.where(self.valid_scanner_positions == np.round(position)) if indexes[0].size != 0: scanner_angle = self.valid_scanner_angles[indexes][0] return scanner_angle
[docs] def get_valid_scanner_angle(self, angle): """ Returns valid scanner angle corresponding to an input angle. Args: angle: input (mechanical) scanner angle, in degrees. """ index = np.abs(self.valid_scanner_angles - angle).argmin() valid_scanner_angle = self.valid_scanner_angles[index] scanner_position = self.valid_scanner_positions[index] return valid_scanner_angle, scanner_position
[docs] def is_valid_scanner_angle(self, angle): """ Returns whether or not an input angle is valid. Args: angle: input (mechanical) scanner angle, in degrees. Returns: """ scanner_position = None index = np.abs(self.valid_scanner_angles - angle).argmin() is_valid_scanner_angle = abs(self.valid_scanner_angles[index]-angle) < 1e-12 if is_valid_scanner_angle: scanner_position = self.valid_scanner_positions[index] return is_valid_scanner_angle, scanner_position
class SimulatedSCFrameError(Exception): """Error raised when computing Simulated_SC_Frame data (rotation matrix) is not possible due to no primary or secondary vector solution.""" pass
[docs]class Simulated_SC_Frame(Multidimentional): """Simulated_SC_Frame. Args: required_parameters_sets (list): ['target'] data (list): [[],[],[]] """ # Example of output data: # # [[-8.78687303e-10 -3.10418898e-05 -3.85203489e-05] # [-5.93810778e-10 3.62125190e-05 -2.75750771e-05] # [-3.17808890e-10 1.35058517e-05 -1.45997027e-05]] # # Examples of input parameters # # { # "name": "Simulated_SC_Frame", # "parameters": { # "primary_axis": "+Z", # "primary_vector": { # "name": "Target_Position", # "parameters": { # "target": "JUPITER" # } # }, # "secondary_axis": "+Y", # "secondary_vector": { # "name": "SC_Velocity", # "parameters": { # "target": "JUPITER" # } # }, # "offset_rotations": "SC_Slew_Angles" # } # } def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # self.prefix = ['x', 'y', 'z'] self.data = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # self.units = 'km' self.required_parameters_sets = [ ['primary_axis', 'primary_vector', 'secondary_axis', 'secondary_vector', 'offset_rotations'], ['primary_axis', 'primary_vector', 'secondary_axis', 'secondary_vector', 'power_optimised', 'offset_rotations'], ['naif_sc_frame', 'offset_rotations'] ] self.set_parameters(parameters)
[docs] def compute(self, et): power_optimised = None # default: no power-optimisation if self.parameters_set_index == 1: # 'power_optimised' is provided power_optimised = self.parameters['power_optimised'] elif self.parameters_set_index == 2: # 'naif_sc_frame' is provided # Get input NAIF SC reference frame to J200 transformation matrix naif_sc_frame = self.parameters['naif_sc_frame'] simsc_j2000_rotmat = spice.pxform(naif_sc_frame, 'J2000', et) # Apply offset rotation angles offset_rot_def = self.parameters['offset_rotations'] simsc_j2000_rotmat = self.apply_offset_rotations(simsc_j2000_rotmat, offset_rot_def) self.data = simsc_j2000_rotmat.tolist() return self.data # set default parameters, from parameters sets 0 and 1. pri_vector_def = self.parameters['primary_vector'] sec_vector_def = self.parameters['secondary_vector'] pri_axis_def = self.parameters['primary_axis'] sec_axis_def = self.parameters['secondary_axis'] offset_rot_def = self.parameters['offset_rotations'] # compute primary vector pri_vector_data = GeometryFactory().create(pri_vector_def, geoevt=self.geoevt).compute(et) if pri_vector_data: pri_vector = pri_vector_data[0] else: if simulator.is_on(frame='JUICE_SPACECRAFT'): raise SimulatedSCFrameError(f'Could not compute {pri_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}. ' f'Try again with a different set of inputs parameters: {pri_vector_def["parameters"]}.') else: print(f'[WARNING] Could not compute {pri_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}.') return np.identity(3).tolist() # compute secondary vector sec_vector_data = GeometryFactory().create(sec_vector_def, geoevt=self.geoevt).compute(et) if sec_vector_data: sec_vector = sec_vector_data[0] else: if simulator.is_on(frame='JUICE_SPACECRAFT'): raise SimulatedSCFrameError( f'Could not compute {sec_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}. ' f'Try again with a different set of inputs parameters: {sec_vector_def["parameters"]}.') else: print(f'[WARNING] Could not compute {sec_vector_def["name"]} as Simulated_SC_Frame secondary vector at ephemeris time {et}.') return np.identity(3).tolist() indexa, signa, framea = self.axis_str(pri_axis_def) indexp, signp, framep = self.axis_str(sec_axis_def) axdef = signa * np.array(pri_vector) # vector defining the primary axis plndef = signp * np.array(sec_vector) # vector defining the secondary axis # print('>>', axdef, indexa, signa, framea) # print('>>', plndef, indexp, signp, framep) # print() # j2000_simsc_rotmat = spice.twovec(axdef, indexa, plndef, indexp) # compute J2000 to simulated SC basic pointing frame transformation matrix based on input axis frame definition, # either 'JUICE_SPACECRAFT' or 'JUICE_MAJIS_BASE'. if framea == 'JUICE_SPACECRAFT': j2000_simsc_rotmat = spice.twovec(axdef, indexa, plndef, indexp) # J2000 to simulated SC frame if framep != framea: raise NotImplementedError(f'Secondary axis must be defined wrt primary axis frame: {framea}.') elif framea == 'JUICE_MAJIS_BASE': j2000_simbase_rotmat = spice.twovec(axdef, indexa, plndef, indexp) simbase_simsc_rotmat = spice.pxform(framea, 'JUICE_SPACECRAFT', 0.0) j2000_simsc_rotmat = spice.xpose(spice.mxm(spice.xpose(j2000_simbase_rotmat), spice.xpose(simbase_simsc_rotmat))) # print(f'{pri_axis_def} = {simbase_simsc_rotmat[:,2]}') # MAJIS +Z axis in SC frame if framep == 'JUICE_SPACECRAFT': # NPO pointing case raise NotImplementedError(f'Secondary axis must be defined wrt primary axis frame: {framea}.') else: raise Exception(f'Invalid primary axis frame: {framea}.') # Power-optimised pointing: changing the sign of secondary axis depending on whether or not defined solar-array # normal vector (-Xsc) is pointing to the Sun for power-optimisation. # # TODO: "Power-optimisation" method must be updated to accommodate for new primary and secondary axis definition. if power_optimised is not None: # print('>> "power-optimising"') solar_array_norm_axis = -1 # (-Xsc); defines the orientation of solar panels in SC ref fram; allowed values: -3, -2, -1, 1, 2, 3 solar_array_norm = np.sign(solar_array_norm_axis) * j2000_simsc_rotmat[abs(solar_array_norm_axis) - 1] # in J2000 sunpos, ltime = spice.spkpos('SUN', et, 'J2000', ABCORR, OBSRVR) solar_array_inc_angle = spice.vsep(solar_array_norm, sunpos) if power_optimised: if solar_array_inc_angle > spice.halfpi(): j2000_simsc_rotmat = spice.twovec(axdef, indexa, -plndef, indexp) else: if solar_array_inc_angle < spice.halfpi(): j2000_simsc_rotmat = spice.twovec(axdef, indexa, -plndef, indexp) # Transpose to get simulated SC frame to J2000 simsc_j2000_rotmat = spice.xpose(j2000_simsc_rotmat) # Apply offset rotation angles simsc_j2000_rotmat = self.apply_offset_rotations(simsc_j2000_rotmat, offset_rot_def) self.data = simsc_j2000_rotmat.tolist() return self.data
[docs] def apply_offset_rotations(self, simsc_j2000_rotmat, offset_rot_def): """Returns the SC basic pointing frame rotated around X and Y by angles derived from input "offset_rotation" geometry definition (eg: 'SC_Slew_Angles', which must be pre-computed.) """ # TODO: Implement similar 'offsetRefAxis' (rotation axis) parameter to be aligned with PTR definition/capability. # Currently, rotations are always applied to input rotation matrix reference frame (basic pointing SC frame). if offset_rot_def == '': return simsc_j2000_rotmat else: offset_angles = self.geoevt.get_geometry_data(offset_rot_def) # !! again, must placed before Simulated_SC_Frame def in ODF # parameters = self.geoevt.get_geometry_parameters(offset_rot_def) # offset_angles = GeometryFactory().create({'name': offset_rot_def, parameters': parameters}).compute(et) # before: not working because offset rotations geometry definition was not in `measurement_geometries` list. xrotmat = spice.rotate(offset_angles[0], 1) # rotation around axis X; simsc to x_rot_simsc simsc_j2000_rotmat = spice.mxm(simsc_j2000_rotmat, spice.xpose(xrotmat)) # x_rot_simsc yrotmat = spice.rotate(offset_angles[1], 2) # rotation around axis Y; x_rot_simsc to y_rot_simsc simsc_j2000_rotmat = spice.mxm(simsc_j2000_rotmat, spice.xpose(yrotmat)) # y_rot_simsc return simsc_j2000_rotmat
[docs] def axis_str(self, axis_def): """Parse input axis definition string and returns axis parameters. Definition string schema: `[+|-][<frame_key>_][X|Y|Z]axis`, where `<frame_key>` can be 'SC' (default) or 'MAJIS'. """ sign = 1 if axis_def[0] == '-': sign = -1 axis_def = axis_def[1:] # remove sign elif axis_def[0] == '+': axis_def = axis_def[1:] # remove sign valid_frames = { 'SC': 'JUICE_SPACECRAFT', 'MAJIS': 'JUICE_MAJIS_BASE' } # find axis definition frame and name def_splits = axis_def.split('_') if len(def_splits) == 1: # default is SC frame frame = valid_frames['SC'] name = axis_def elif len(def_splits) == 2: if def_splits[0] in valid_frames.keys(): frame = valid_frames[def_splits[0]] name = def_splits[1] else: raise Exception(f'Invalid axis frame definition: {def_splits[0]}.') else: raise Exception(f'Invalid axis definition: {axis_def}.') # find axis index number axes = {'X': 1, 'Y': 2, 'Z': 3} if name[0] in axes.keys(): axis_number = axes[name[0]] else: raise Exception(f'Invalid axis definition: {axis_def}.') return axis_number, sign, frame
# class Simulated_SC_Frame(Multidimentional): # """Simulated_SC_Frame. # # Args: # required_parameters_sets (list): ['target'] # data (list): [[],[],[]] # """ # # # Example of output data: # # # # [[-8.78687303e-10 -3.10418898e-05 -3.85203489e-05] # # [-5.93810778e-10 3.62125190e-05 -2.75750771e-05] # # [-3.17808890e-10 1.35058517e-05 -1.45997027e-05]] # # # # Examples of input parameters # # # # { # # "name": "Simulated_SC_Frame", # # "parameters": { # # "primary_axis": "+Z", # # "primary_vector": { # # "name": "Target_Position", # # "parameters": { # # "target": "JUPITER" # # } # # }, # # "secondary_axis": "+Y", # # "secondary_vector": { # # "name": "SC_Velocity", # # "parameters": { # # "target": "JUPITER" # # } # # }, # # "offset_rotations": "SC_Slew_Angles" # # } # # } # # def __init__(self, parameters, geoevt=None, silent=None): # super().__init__(parameters, geoevt=geoevt, silent=silent) # # # self.prefix = ['x', 'y', 'z'] # self.data = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # # self.units = 'km' # self.required_parameters_sets = [ # ['primary_axis', 'primary_vector', 'secondary_axis', 'secondary_vector', 'offset_rotations'], # ['primary_axis', 'primary_vector', 'secondary_axis', 'secondary_vector', 'power_optimised', 'offset_rotations'], # ['naif_sc_frame', 'offset_rotations'] # ] # # self.set_parameters(parameters) # # def compute(self, et): # # power_optimised = None # default: no power-optimisation # if self.parameters_set_index == 1: # 'power_optimised' is provided # power_optimised = self.parameters['power_optimised'] # elif self.parameters_set_index == 2: # 'naif_sc_frame' is provided # # Get input NAIF SC reference frame to J200 transformation matrix # naif_sc_frame = self.parameters['naif_sc_frame'] # simsc_j2000_rotmat = spice.pxform(naif_sc_frame, 'J2000', et) # # # Apply offset rotation angles # offset_rot_def = self.parameters['offset_rotations'] # simsc_j2000_rotmat = self.apply_offset_rotations(simsc_j2000_rotmat, offset_rot_def) # # self.data = simsc_j2000_rotmat.tolist() # return self.data # # # set default parameters, from parameters sets 0 and 1. # pri_vector_def = self.parameters['primary_vector'] # sec_vector_def = self.parameters['secondary_vector'] # pri_axis_def = self.parameters['primary_axis'] # sec_axis_def = self.parameters['secondary_axis'] # offset_rot_def = self.parameters['offset_rotations'] # # # compute primary vector # pri_vector_data = GeometryFactory().create(pri_vector_def, geoevt=self.geoevt).compute(et) # if pri_vector_data: # pri_vector = pri_vector_data[0] # else: # if simulator.is_on(frame='JUICE_SPACECRAFT'): # raise SimulatedSCFrameError(f'Could not compute {pri_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}. ' # f'Try again with a different set of inputs parameters: {pri_vector_def["parameters"]}.') # else: # print(f'[WARNING] Could not compute {pri_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}.') # return np.identity(3).tolist() # # # compute secondary vector # sec_vector_data = GeometryFactory().create(sec_vector_def, geoevt=self.geoevt).compute(et) # if sec_vector_data: # sec_vector = sec_vector_data[0] # else: # if simulator.is_on(frame='JUICE_SPACECRAFT'): # raise SimulatedSCFrameError( # f'Could not compute {sec_vector_def["name"]} as Simulated_SC_Frame primary vector at ephemeris time {et}. ' # f'Try again with a different set of inputs parameters: {sec_vector_def["parameters"]}.') # else: # print(f'[WARNING] Could not compute {sec_vector_def["name"]} as Simulated_SC_Frame secondary vector at ephemeris time {et}.') # return np.identity(3).tolist() # # indexa, signa = self.axis_str(pri_axis_def) # indexp, signp = self.axis_str(sec_axis_def) # # axdef = signa * np.array(pri_vector) # vector defining the primary axis # plndef = signp * np.array(sec_vector) # vector defining the secondary axis # # #print('>>', axdef, indexa, signa, plndef, indexp, signp) # j2000_simsc_rotmat = spice.twovec(axdef, indexa, plndef, indexp) # J2000 to simulated SC frame # # # Power-optimised pointing: changing the sign of secondary axis depending on whether or not defined solar-array # # normal vector (-Xsc) is pointing to the Sun for power-optimisation. # # # if power_optimised is not None: # #print('>> "power-optimising"') # solar_array_norm_axis = -1 # (-Xsc); defines the orientation of solar panels in SC ref fram; allowed values: -3, -2, -1, 1, 2, 3 # solar_array_norm = np.sign(solar_array_norm_axis) * j2000_simsc_rotmat[abs(solar_array_norm_axis) - 1] # in J2000 # sunpos, ltime = spice.spkpos('SUN', et, 'J2000', ABCORR, OBSRVR) # solar_array_inc_angle = spice.vsep(solar_array_norm, sunpos) # if power_optimised: # if solar_array_inc_angle > spice.halfpi(): # j2000_simsc_rotmat = spice.twovec(axdef, indexa, -plndef, indexp) # else: # if solar_array_inc_angle < spice.halfpi(): # j2000_simsc_rotmat = spice.twovec(axdef, indexa, -plndef, indexp) # # # Transpose to get simulated SC frame to J2000 # simsc_j2000_rotmat = spice.xpose(j2000_simsc_rotmat) # # # Apply offset rotation angles # simsc_j2000_rotmat = self.apply_offset_rotations(simsc_j2000_rotmat, offset_rot_def) # # self.data = simsc_j2000_rotmat.tolist() # return self.data # # def apply_offset_rotations(self, simsc_j2000_rotmat, offset_rot_def): # # if offset_rot_def == '': # return simsc_j2000_rotmat # else: # offset_angles = self.geoevt.get_geometry_data(offset_rot_def) # !! again, must placed before Simulated_SC_Frame def in ODF # # parameters = self.geoevt.get_geometry_parameters(offset_rot_def) # # offset_angles = GeometryFactory().create({'name': offset_rot_def, parameters': parameters}).compute(et) # # before: not working because offset rotations geometry definition was not in `measurement_geometries` list. # # xrotmat = spice.rotate(offset_angles[0], 1) # rotation around axis X; simsc to x_rot_simsc # simsc_j2000_rotmat = spice.mxm(simsc_j2000_rotmat, spice.xpose(xrotmat)) # x_rot_simsc # yrotmat = spice.rotate(offset_angles[1], 2) # rotation around axis Y; x_rot_simsc to y_rot_simsc # simsc_j2000_rotmat = spice.mxm(simsc_j2000_rotmat, spice.xpose(yrotmat)) # y_rot_simsc # # return simsc_j2000_rotmat # # # # def axis_str(self, axis_def): # sign = 1 # if axis_def[0] == '-': # sign = -1 # axes = {'X': 1, '-X': 1, 'Y': 2, '-Y': 2, 'Z': 3, '-Z': 3} # axis_number = axes[axis_def] # # return axis_number, sign
[docs]class Simulated_Scan_Frame(Multidimentional): """Simulated_Scan_Frame. Defined wrt to JUICE_MAJIS_BASE frame. scan_zero (list): list of Euler rotation angles defining the orientation of the JUICE_MAJIS_SCAN wrt the JUICE_MAJIS_BASE when the scanner is in "zero" position (scanner rotation axis). scan_rotation_angle: Args: required_parameters_sets (list): ['scan_zero', 'scan_rotation_angle'] data (list): [[],[],[]] """ # Example of output data: # # [[-8.78687303e-10 -3.10418898e-05 -3.85203489e-05] # [-5.93810778e-10 3.62125190e-05 -2.75750771e-05] # [-3.17808890e-10 1.35058517e-05 -1.45997027e-05]] # # Examples of input parameters # # { # "name": "Simulated_Scan_Frame", # "parameters": { # "scan_zero": [0.0, 0.0, 0.0], # Eg. Euler rotation angles wrt to JUICE_MAJIS_BASE # "scan_rotation_angle": "MAJIS_Scanner_Rate_Angles" # Function returning the scanner offset rotation angle # } # at each acquisition time. # def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # self.prefix = ['x', 'y', 'z'] self.data = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] # self.units = 'km' self.required_parameters_sets = [[ 'scan_zero', 'scan_rotation_angle' ]] self.set_parameters(parameters)
[docs] def compute(self, et): # primary vector is +X that is intended to be co-aligned with +Xmajis_base # We use Euler angles for now to defined the scan mirror axis # TODO: take into accounts for maximum MAJIS scan rotation angle limit, using MAJIS_SCAN_ANGLE_MAX # set default parameters scan_zero_angles = self.parameters['scan_zero'] scan_rotation_angle_def = self.parameters['scan_rotation_angle'] # Compute rotation matrix from "zero"-position Scan frame to JUICE_MAJIS_BASE. # Hypothesis on order of rotation ('Z' 'X' 'Y') # angle3, angle2, angle1, axis3, axis2, axis1 base_simscan_rotmat = spice.eul2m(scan_zero_angles[0], scan_zero_angles[1], scan_zero_angles[2], 3, 1, 2) # JUICE_MAJIS_BASE to "zero" Scan frame simscan_base_rotmat = spice.xpose(base_simscan_rotmat) # "zero" Scan frame to JUICE_MAJIS_BASE # print(simscan_base_rotmat) # Apply mirror rotation angle scan_rotation_angle_geometry_name = scan_rotation_angle_def # Geometry class name #scan_rotation_angle = self.geoevt.get_geometry_data(scan_rotation_angle_geometry) if et == self.geoevt.reference_time: # retrieve pre-computed scan rotation angle at epoch et scan_rotation_angle = self.geoevt.get_geometry_data(scan_rotation_angle_geometry_name) #print(f'line={self.geoevt.index+1}, time={et}, {scan_rotation_angle_geometry_name}={scan_rotation_angle*spice.dpr()}') else: # compute scan rotation angle at epoch et. eg: Detector_Boresight_Ground_Drift if scan_rotation_angle_def == 'Motion_Compensation_Angle': #print('[WARNING] MAJIS scanner motion can not be simulated at requested epoch (using reference time instead).') scan_rotation_angle = self.geoevt.get_geometry_data(scan_rotation_angle_geometry_name) else: parameters = self.geoevt.get_geometry_parameters(scan_rotation_angle_def) scan_rotation_angle_geometry_def = {'name': scan_rotation_angle_geometry_name, 'parameters':parameters} scan_rotation_angle = GeometryFactory().create(scan_rotation_angle_geometry_def, geoevt=self.geoevt).compute(et) #print(scan_rotation_angle) # IMPORTANT: again, Geometry definition must placed before Simulated_Scan_Frame def in ODF xrotmat = spice.rotate(scan_rotation_angle, 1) # apply rotation around axis X; simscan to x_rot_simscan simscan_base_rotmat = spice.mxm(simscan_base_rotmat, spice.xpose(xrotmat)) # x_rot_simscan self.data = simscan_base_rotmat.tolist() return self.data
[docs]class Target_Position(Direction): """Target center geometric position vector with respect to spacecraft, expressed in J2000. Args: required_parameters_sets (list): ['target'] data (list): """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): ref_frame = 'J2000' target = Target(self.parameters['target']) target_state, ltime = spice.spkezr(target.name, et, ref_frame, 'NONE', OBSRVR) self.setDirection(ref_frame, list(target_state[0:3])) return self.data
[docs]class SubSC_Point_Direction(Direction): """SubSC_Point_Direction. Args: required_parameters_sets (list): ['target'] data (list): """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): # set default parameters (could be passed as required parameters if needed) observer = OBSRVR # JUICE abcorr = 'NONE' # ABCORR = 'LT+S' target = Target(self.parameters['target']) spoint, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', target.name, et, target.frame, abcorr, OBSRVR) self.setDirection(target.frame, list(srfvec)) self.toJ2000(et) return self.data
[docs]class SC_Velocity(Direction): """Target geometric velocity vector with respect to target, expressed in J2000 frame. Args: required_parameters_sets (list): ['target'] data (list): [x, y, z] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. # self.prefix = ['vx', 'vy', 'vz'] # self.data = [0.0, 0.0, 0.0] # self.units = 'km/s' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): ref_frame = 'J2000' target = Target(self.parameters['target']) target_state, ltime = spice.spkezr(OBSRVR, et, ref_frame, 'NONE', target.name) self.setDirection(ref_frame, list(target_state[3:])) return self.data
[docs]class Ground_Track_Across_Direction(Direction): """NOT AVAILABLE (WIP). Ground_Track_Across_Direction. Args: required_parameters_sets (list): ['target'] data (list): """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): # set default parameters (could be passed as required parameters if needed) observer = OBSRVR # JUICE abcorr = 'NONE' # ABCORR = 'LT+S' target = Target(self.parameters['target']) spoint1, trgepc1, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', target.name, et, target.frame, abcorr, OBSRVR) spoint2, trgepc2, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', target.name, et+1.0, target.frame, abcorr, OBSRVR) spoint1 = spice.mxv(spice.pxform(target.frame, 'J2000', et), spoint1) spoint2 = spice.mxv(spice.pxform(target.frame, 'J2000', et), spoint2) dir = spice.vcrss(spoint1, spoint2) self.setDirection('J2000', list(dir)) # self.toJ2000(et) return self.data
[docs]class Ring_Ansae(Multidimentional): """Multidimensional-type data container for pointing-independent Jovian ring's ansae properties. Geometry classes that users ``Ring_Ansae``: - :class:`gfinder.geometry.Ring_Illumination_Angle` - :class:`gfinder.geometry.Ring_Horizontal_Angular_Size` - :class:`gfinder.geometry.Ring_Vertical_Angular_Size` - :class:`gfinder.geometry.Ring_West_Ansa_Phase_Angle` - :class:`gfinder.geometry.Ring_East_Ansa_Phase_Angle` - :class:`gfinder.geometry.Ring_West_Ansa_Best_Resolution` - :class:`gfinder.geometry.Ring_East_Ansa_Best_Resolution` - :class:`gfinder.geometry.Ring_Ansae_Phase_Angle_In_Range` Args: required_parameters_sets (list): ['radius'] data (list): [[[sun_elev, h_ang_size, v_ang_size]],[[west_phase, west_resol]],[[east_phase, east_resol]]] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # set required parameters sets self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): radius = self.parameters['radius'] target = Target('JUPITER') # vector from Jupiter to the observer vec_jup_sc, ltime = spice.spkpos(OBSRVR, et, 'J2000', 'NONE', target.name) # vector from Sun to Jupiter vec_sun_jup, ltime = spice.spkpos(target.name, et, 'J2000', 'NONE', 'SUN') # vector from Jupiter to Sun in Jupiter Inertial Frame ("ji") at J2000 vec_jup_sun = -vec_sun_jup rotmat = spice.pxform('J2000', 'JUICE_JUPITER_IF_J2000', et) vec_jup_sun_ji = spice.mxv(rotmat, vec_jup_sun) ang_jupsun_jupaxis = spice.vsep(vec_jup_sun_ji, np.array([0., 0., 1.]))*spice.dpr() sun_elev = 90.0-ang_jupsun_jupaxis # vector from Jupiter to the observer in Jupiter Inertial Frame vec_jup_sc_ji = spice.mxv(rotmat, vec_jup_sc) # vector from Sun to Jupiter in Jupiter Inertial Frame vec_sun_jup_ji = spice.mxv(rotmat, vec_sun_jup) # vector from jupiter center to ansa, perpendicular to jupiter-sun line pa = spice.vcrss(vec_sun_jup_ji, np.array([0., 0., 1.])) pa = spice.vhat(pa)*radius*target.re vec_sun_ansa1 = vec_sun_jup_ji + pa vec_sun_ansa2 = vec_sun_jup_ji - pa vec_sc_ansa1 = pa-vec_jup_sc_ji vec_sc_ansa2 = -pa-vec_jup_sc_ji # determine which one is the west or east from the observer point of view ansae_orient = spice.vdot(spice.vcrss(vec_sc_ansa1, vec_sc_ansa2), np.array([0., 0., 1.])) if ansae_orient < 0.0: vec_sc_west_ansa = vec_sc_ansa1 vec_sun_west_ansa = vec_sun_ansa1 vec_sc_east_ansa = vec_sc_ansa2 vec_sun_east_ansa = vec_sun_ansa2 else: vec_sc_west_ansa = vec_sc_ansa2 vec_sun_west_ansa = vec_sun_ansa2 vec_sc_east_ansa = vec_sc_ansa1 vec_sun_east_ansa = vec_sun_ansa1 # phase angle at each ansa west_phase = spice.vsep(vec_sun_west_ansa, vec_sc_west_ansa)*spice.dpr() east_phase = spice.vsep(vec_sun_east_ansa, vec_sc_east_ansa)*spice.dpr() # best spatial resolution at both ansae west_resol = spice.vnorm(vec_sc_west_ansa)*IFOV east_resol = spice.vnorm(vec_sc_east_ansa)*IFOV # alternative computations of horizontal and vertical angular size of the ring ## find direction vectors tangent to ring ansa. n_points = 100 azimuths = np.linspace(0.0, 2 * spice.pi(), num=n_points) xvec = radius*target.re * np.cos(azimuths) yvec = radius*target.re * np.sin(azimuths) rpoint_dir_vecs = [] rpoints = [] sep_angles = [] for x, y in zip(xvec, yvec): rpoint = np.array([x, y, 0.0]) rpoint_dir_vec = rpoint-vec_jup_sc_ji sep_angle = spice.vsep(rpoint, rpoint_dir_vec) rpoints.append(rpoint) rpoint_dir_vecs.append(rpoint_dir_vec) sep_angles.append(sep_angle*spice.dpr()) tangent_dirs = [] # ring ansa tangent points for i in range(len(sep_angles)-1): if sep_angles[i+1] >= 90.0 and sep_angles[i] < 90.0: r = (90.0-sep_angles[i]) / (sep_angles[i+1] - sep_angles[i]) rpoint0 = r*rpoints[i] + (1-r)*rpoints[i+1] # print(i, azimuths[i]*spice.dpr(), rpoint_dir_vecs[i]) # print(spice.vsep(rpoint_dir_vecs[i], (rpoint0 - vec_jup_sc_ji)) * spice.dpr()) tangent_dirs.append(rpoint0-vec_jup_sc_ji) if sep_angles[i+1] < 90.0 and sep_angles[i] >= 90.0: r = (90.0-sep_angles[i+1]) / (sep_angles[i] - sep_angles[i+1]) rpoint0 = (1-r)*rpoints[i] + r*rpoints[i+1] # print(i, azimuths[i]*spice.dpr(), rpoint_dir_vecs[i]) # print(spice.vsep(rpoint_dir_vecs[i],(rpoint0-vec_jup_sc_ji))*spice.dpr()) tangent_dirs.append(rpoint0-vec_jup_sc_ji) h_ang_size0 = None if len(tangent_dirs) == 2: # print(tangent_dirs[0]) # print(tangent_dirs[1]) h_ang_size0 = spice.vsep(tangent_dirs[0], tangent_dirs[1])*spice.dpr() # print(f'Angular size = {h_ang_size} deg') ## vertical angular size eq_plane = spice.nvc2pl([0.0, 0.0, 1.0], 0.0) # set equatorial plane pa = spice.vprjp(vec_jup_sc_ji,eq_plane) # projection of sc position in equatorial plane pa = spice.vhat(pa)*radius*target.re # one of the most vertical ansa vec_sc_ansa1 = pa-vec_jup_sc_ji vec_sc_ansa2 = -pa-vec_jup_sc_ji v_ang_size = spice.vsep(vec_sc_ansa1, vec_sc_ansa2)*spice.dpr() # print(f'v_vec_sc_ansa1 = {vec_sc_ansa1}') # print(f'v_vec_sc_ansa2 = {vec_sc_ansa2}') ## horizontal angular size if h_ang_size0: h_ang_size = h_ang_size0 else: pa = spice.vcrss(vec_jup_sc_ji, np.array([0., 0., 1.])) # normal vector to plane defined by sc position and north pole pa = spice.vhat(pa)*radius*target.re # one of the most horizontal ansa vec_sc_ansa1 = pa-vec_jup_sc_ji vec_sc_ansa2 = -pa-vec_jup_sc_ji h_ang_size = spice.vsep(vec_sc_ansa1, vec_sc_ansa2) * spice.dpr() # ### radial (horizonal) angular size # dist = spice.vnorm(vec_jup_sc_ji) # min_radius = target.re # max_radius = radius*target.re # h_ang_size0 = (np.arcsin(max_radius/dist) - np.arcsin(min_radius/dist)) * spice.dpr() # # ### ring opening angle (vertical angular size) # rho = np.sqrt(vec_jup_sc_ji[0]*vec_jup_sc_ji[0] + vec_jup_sc_ji[1]*vec_jup_sc_ji[1]) # v_ang_size0 = abs(np.arctan2(vec_jup_sc_ji[2], rho)) * spice.dpr() # # print('Vertical angular size:') # print(f'- v_ang_size0 = {v_ang_size0:.4} deg') # print(f'- v_ang_size = {v_ang_size:.4} deg') # print() # print('Horizontal angular size:') # print(f'- h_ang_size0 = {h_ang_size0:.4} deg') # print(f'- h_ang_size = {h_ang_size:.4} deg') # print() ring_values = [sun_elev, h_ang_size, v_ang_size] west_ansa_values = [west_phase, west_resol] east_ansa_values = [east_phase, east_resol] self.data = [ ring_values, west_ansa_values, east_ansa_values ] return self.data
[docs]class Ring_Illumination_Angle(Scalar): """Jovian ring solar illumination angle (deg). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[0][0] return self.data
[docs]class Ring_Horizontal_Angular_Size(Scalar): """Jovian ring horizontal angular size (deg). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[0][1] return self.data
[docs]class Ring_Vertical_Angular_Size(Scalar): """Jovian ring vertical angular size (deg). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[0][2] return self.data
[docs]class Ring_West_Ansa_Phase_Angle(Scalar): """Jovian ring West ansa phase angle (deg). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[2][0] return self.data
[docs]class Ring_East_Ansa_Phase_Angle(Scalar): """Jovian ring East ansa phase angle (deg). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[1][0] return self.data
[docs]class Ring_West_Ansa_Best_Resolution(Scalar): """Jovian ring West ansa best resolution (km). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[2][1] return self.data
[docs]class Ring_East_Ansa_Best_Resolution(Scalar): """Jovian ring East ansa best resolution (km). Args: required_parameters_sets (list): ['radius'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['radius']] self.set_parameters(parameters)
[docs] def compute(self, et): ring_ansae_data = self.geoevt.get_geometry_data('Ring_Ansae') self.data = ring_ansae_data[1][1] return self.data
[docs]class Ring_Ansae_Phase_Angle_In_Range(Scalar): """Whether or not at least one Jovian ring ansa phase angle is within a given range. Args: required_parameters_sets (list): ['radius', 'min_phase_angle', 'max_phase_angle'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.type = 'boolean' self.required_parameters_sets = [['radius', 'min_phase_angle', 'max_phase_angle']] self.set_parameters(parameters)
[docs] def compute(self, et): min_phase_angle = self.parameters['min_phase_angle'] max_phase_angle = self.parameters['max_phase_angle'] # wansa_phase_angle = self.geoevt.get_geometry_data('Ring_West_Ansa_Phase_Angle') # eansa_phase_angle = self.geoevt.get_geometry_data('Ring_East_Ansa_Phase_Angle') # ring_ansae_data = GeometryFactory().create(pri_vector_def, geoevt=self.geoevt).compute(et) ring_ansae_data = Ring_Ansae({'radius': self.parameters['radius']}).compute(et) wansa_phase_angle = ring_ansae_data[1][0] eansa_phase_angle = ring_ansae_data[2][0] self.data = False # method: “at least one ansa test for phase conditions” if ((wansa_phase_angle > min_phase_angle) and (wansa_phase_angle <= max_phase_angle) or (eansa_phase_angle > min_phase_angle) and (eansa_phase_angle <= max_phase_angle)): self.data = True return self.data
# Coordinates and illumination angles at intersection point between LOS and ring/equatorial plane. #
[docs]class Observed_Ring_Point(Vector): """Vector-type data container for detector's boresight observed Jovian ring's point coordinates and associated properties. Geometry classes that users ``Observed_Ring_Point``: - :class:`gfinder.geometry.Observed_Ring_Point_Radius` - :class:`gfinder.geometry.Observed_Ring_Point_Azimuth` - :class:`gfinder.geometry.Observed_Ring_Point_Spatial_Resolution` - :class:`gfinder.geometry.Observed_Ring_Point_Phase` - :class:`gfinder.geometry.Observed_Ring_Point_Incidence` - :class:`gfinder.geometry.Observed_Ring_Point_Emergence` Args: required_parameters_sets (list): ['detector'] data (list): [radius, azimuth, slant, phase, incdnc, emissn] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['radius', 'azimuth', 'resol', 'phase', 'incdnc', 'emissn'] self.data = [None, None, None, None, None, None] self.units = ['RJ', 'deg', 'km', 'deg', 'deg', 'deg'] self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target('JUPITER') ref_frame = 'JUICE_JUPITER_IF_J2000' detector = Detector(self.parameters['detector']) scpos, ltime = spice.spkpos(OBSRVR, et, ref_frame, ABCORR, target.name) # retrieve and transform detector boresight in JUICE_JUPITER_IF_J2000 if self.geoevt.has_geometry('Detector_Boresight'): dvec = self.geoevt.get_geometry_data('Detector_Boresight')[0] else: dvec = Detector_Boresight(self.parameters).compute(et)[0] rotmat = spice.pxform('J2000', ref_frame, et) dvec = spice.mxv(rotmat, dvec) ring_normal = [0.0, 0.0, 1.0] plane = spice.nvp2pl(ring_normal, [0.0, 0.0, 0.0]) nxpts, ring_point = spice.inrypl(scpos, dvec, plane) # https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/inrypl_c.html if nxpts < 1: return self.data radius, azimuth, elevation = spice.reclat(ring_point) # elevation should be equal to zero if (radius >= RINGS_RADIUS_MIN) and (radius <= RINGS_RADIUS_MAX): # compute illumination angles at ring-point sunpos, ltime = spice.spkpos('SUN', et - ltime, ref_frame, ABCORR, target.name) sundir = sunpos - ring_point scdir = scpos - ring_point phase = spice.vsep(sundir, scdir) incdnc = spice.vsep(sundir, ring_normal) emissn = spice.vsep(scdir, ring_normal) resol = spice.vnorm(scdir) * IFOV self.data = [radius / JUPITER_RADIUS, azimuth * spice.dpr(), resol, phase * spice.dpr(), incdnc * spice.dpr(), emissn * spice.dpr()] return self.data
[docs]class Observed_Ring_Point_Radius(Scalar): """Detector boresight observed Jovian ring point radius (RJ). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'RJ' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[0] return self.data
[docs]class Observed_Ring_Point_Azimuth(Scalar): """Detector boresight observed Jovian ring point azimuth (deg). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[1] return self.data
[docs]class Observed_Ring_Point_Spatial_Resolution(Scalar): """Detector boresight observed Jovian ring point spatial resolution (km). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # data = Observed_Ring_Point(self.parameters).compute(et) data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[2] return self.data
[docs]class Observed_Ring_Point_Phase(Scalar): """Detector boresight observed Jovian ring point phase angle (deg). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # data = Observed_Ring_Point(self.parameters).compute(et) data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[3] return self.data
[docs]class Observed_Ring_Point_Incidence(Scalar): """Detector boresight observed Jovian ring point incidence angle (deg). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # data = Observed_Ring_Point(self.parameters).compute(et) data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[4] return self.data
[docs]class Observed_Ring_Point_Emergence(Scalar): """Detector boresight observed Jovian ring point emergence angle (deg). Args: required_parameters_sets (list): ['detector'] """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # data = Observed_Ring_Point(self.parameters).compute(et) data = self.geoevt.get_geometry_data('Observed_Ring_Point') self.data = data[5] return self.data
[docs]class Target_Point(Vector): """Detector boresight target point coordinates and its associated illumination and viewing angles. TODO: Shall become deprecated and replaced by Detector_Boresight_Target_Point. """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['lon', 'lat', 'alt', 'slant', 'phase', 'incdnc', 'emissn'] self.data = [None, None, None, None, None, None, None] self.units = ['deg', 'deg', 'km', 'km', 'deg', 'deg', 'deg'] self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) detector = Detector(self.parameters['detector']) # retrieve detector boresight vector in J2000 dref = 'J2000' if self.geoevt.has_geometry('Detector_Boresight'): dvec = self.geoevt.get_geometry_data('Detector_Boresight')[0] else: dvec = Detector_Boresight(self.parameters).compute(et)[0] rotmat = spice.pxform('J2000', target.frame, et) dvec = spice.mxv(rotmat, dvec) spoint, trgepc, srfvec, found = spice.sincpt('ELLIPSOID', target.name, et, target.frame, ABCORR, OBSRVR, dref, dvec) if found: # observed target point is on the target body surface r, lon, lat = spice.reclat(spoint) alt = 0.0 slant = spice.vnorm(srfvec) else: targpos, ltime = spice.spkpos(target.name, et, target.frame, ABCORR, OBSRVR) scpos = -targpos trg_pnear, dist = spice.npedln(target.radii[0], target.radii[1], target.radii[2], scpos, dvec) r, lon, lat = spice.reclat(trg_pnear) spoint = trg_pnear # assign as surface point for illumination angles computation los_pnear, alt = spice.nplnpt(scpos, dvec, trg_pnear) slant = spice.vnorm(los_pnear - scpos) # compute illumination angles at surface point (intercept or nearest) trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target.name, et, target.frame, ABCORR, OBSRVR, spoint) self.data = [lon * spice.dpr(), lat * spice.dpr(), alt, slant, phase * spice.dpr(), incdnc * spice.dpr(), emissn * spice.dpr()] return self.data
[docs]class Target_Point_Longitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # data = Target_Point(self.parameters).compute(et) data = self.geoevt.get_geometry_data('Target_Point') self.data = data[0] return self.data
[docs]class Target_Point_Latitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[1] return self.data
[docs]class Target_Point_Altitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[2] return self.data
[docs]class Target_Point_Slant(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[3] return self.data
[docs]class Target_Point_Phase(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[4] return self.data
[docs]class Target_Point_Incidence(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[5] return self.data
[docs]class Target_Point_Emission(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): data = self.geoevt.get_geometry_data('Target_Point') self.data = data[6] return self.data
[docs]class SC_Elevation(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'reference_frame']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) ref_frame = self.parameters['reference_frame'] sc_pos = SC_Position(self.parameters).compute(et) # sc_pos, ltime = spice.spkpos('JUICE', et, ref_frame, 'NONE', target.name) r, lon, lat = spice.reclat(sc_pos) self.data = lat * spice.dpr() return self.data
[docs]class SC_Position(Vector): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['x', 'y', 'z'] self.data = [None, None, None] self.units = 'km' self.required_parameters_sets = [['target', 'reference_frame']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) ref_frame = self.parameters['reference_frame'] sc_pos, ltime = spice.spkpos('JUICE', et, ref_frame, 'NONE', target.name) self.data = [sc_pos[0], sc_pos[1], sc_pos[2]] return self.data
[docs]class Ring_Ansae_Directions(Direction): """Direction vector(s) in J2000 from the observer to a given Jovian ring point or set of points. TODO: Rename Ring_Ansae_Directions into Ring_Directions and create a new Ring_Point_Direction class. Args: required_parameters_sets (list): [['radius', 'azimuth'], ['radius', 'n_points']] data (list): Example of ODF definition:: { "name": "Ring_Ansae_Directions", "parameters": { "radius": "@ring_radius", "n_points": 100 } } """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['radius', 'azimuth'], ['radius', 'n_points']] self.set_parameters(parameters) # self.reference_frame = Target(self.parameters['target']).frame
[docs] def compute(self, et): # set default parameters target = Target('JUPITER') if self.parameters_set_index == 0: # TODO: when single 'radius', 'azimuth' ring point. lons = self.parameters['radius'] lats = self.parameters['latitude'] if not isinstance(lons, list): lons = [lons] if not isinstance(lats, list): lats = [lats] alt = 0.0 spoint_dir_vectors = [] target_pos, ltime = spice.spkpos(target.name, et, target.frame, ABCORR, OBSRVR) for lon, lat in zip(lons, lats): lon = lon * spice.rpd() lat = lat * spice.rpd() # compute surface point direction from spacecraft in target's body-fixed frame spoint = spice.georec(lon, lat, alt, target.re, target.f) spoint_dir_vectors.append((spoint + target_pos).tolist()) self.setDirection(target.frame, spoint_dir_vectors) self.toJ2000(et) elif self.parameters_set_index == 1: # set radius, n_points radius = self.parameters['radius']*JUPITER_RADIUS n_points = self.parameters['n_points'] # calculate latitudinal coordinates of ring points azimuths = np.linspace(0.0,2*spice.pi(), num=n_points) xvec = radius*np.cos(azimuths) yvec = radius*np.sin(azimuths) rpoint_dir_vectors = [] target_pos, ltime = spice.spkpos(target.name, et, target.frame, ABCORR, OBSRVR) for x, y in zip(xvec, yvec): rpoint= np.array([x, y, 0.0]) rpoint_dir_vectors.append((rpoint + target_pos).tolist()) self.setDirection(target.frame, rpoint_dir_vectors) self.toJ2000(et) return self.data
# Return the terminator points, times associated with terminator points, and terminator vectors emanating from the observer. #
[docs]class Terminator(Multidimentional): # for the sake of a better Geometry parent class for now. def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # self.prefix = [] self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameter target = Target(self.parameters['target']) # set spice.limbpt input parameters method = 'UMBRAL/TANGENT/ELLIPSOID' corloc = 'CENTER' refvec = [0., 0., 1.0] ncuts = 100 rolstp = spice.twopi() / ncuts schstp = 0 soltol = 0 maxn = 10000 npts, points, epochs, tangts = spice.termpt(method, 'SUN', target.name, et, target.frame, ABCORR, corloc, OBSRVR, refvec, rolstp, ncuts, schstp, soltol, maxn); self.data = [points, epochs, tangts] return self.data
# Return the limb points of tangency on the target of rays emanating from the observer, in target body-fixed reference frame. #
[docs]class Limb(Multidimentional): # for the sake of a better Geometry parent class for now. def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) # self.prefix = [] self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameter target = Target(self.parameters['target']) # set spice.limbpt input parameters method = 'TANGENT/ELLIPSOID' corloc = 'CENTER' refvec = [0., 0., 1.0] ncuts = 100 rolstp = spice.twopi() / ncuts schstp = 0 soltol = 0 maxn = 10000 npts, points, epochs, tangts = spice.limbpt(method, target.name, et, target.frame, ABCORR, corloc, OBSRVR, refvec, rolstp, ncuts, schstp, soltol, maxn) self.data = [points, epochs, tangts] return self.data
# TODO: A generic Geometry.get[Sibling]GeometryData(name) should be implemented to easily retrieve # "sibling" Geometry objects, introducing the concept of GeometryEvent "buffer" Geometry objects # for pre-computed data. Eg. for Limb: self.get[Sibling]GeometryData('Limb') would return sibling 'Limb' geometry # if computed (in ODF, placed before 'Limb_Disk' definition; or in buffer), or compute it (once) and # store it in the buffer geometries list.
[docs]class Limb_Directions(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameter target = Target(self.parameters['target']) # compute limb tangent points in body-fixed frame (directions from observer) limb_data = Limb(self.parameters).compute(et) self.setDirection(target.frame, limb_data[2]) self.toJ2000(et) return self.data
[docs]class Limb_Point_Direction(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target', 'latitude', 'max_incidence_angle']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) lat0 = self.parameters['latitude'] max_incidence_angle = self.parameters['max_incidence_angle'] limb_data = Limb({'target':target.name}).compute(et) # [points, epochs, tangts] points = limb_data[0] epochs = limb_data[1] tangts = limb_data[2] # Find desired limb point, eg: could be the brightest point on the diurnal limb, # or a specific latitude if visible. Implementation below is the latter. limb_lats = [] limb_points = [] limb_tangts = [] limb_epochs = [] # max_incidence_angle = 90.0 for point, tangt, epoch in zip(points, tangts, epochs): r, lon, lat = spice.reclat(point) trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target.name, et, target.frame, ABCORR, OBSRVR, point) if incdnc * spice.dpr() < max_incidence_angle: limb_lats.append(lat * spice.dpr()) limb_points.append(point) limb_tangts.append(tangt) limb_epochs.append(epoch) n_points = len(limb_lats) if n_points < 2: return None ## convert to np arrays limb_lats = np.array(limb_lats) limb_points = np.array(limb_points) limb_tangts = np.array(limb_tangts) limb_epochs = np.array(limb_epochs) ## sort arrays based on latitudes array sorted in ascending order sort_idxs = limb_lats.argsort() limb_lats = limb_lats[sort_idxs] limb_points = limb_points[sort_idxs] limb_tangts = limb_tangts[sort_idxs] limb_epochs = limb_epochs[sort_idxs] # search and interpolate found = False for i in range(n_points - 1): if limb_lats[i] <= lat0 and limb_lats[i + 1] > lat0: r = (lat0 - limb_lats[i]) / (limb_lats[i + 1] - limb_lats[i]) limb_tangt0 = (1-r) * limb_tangts[i] + r*limb_tangts[i+1] epoch0 = (1-r) * limb_epochs[i] + r*limb_epochs[i+1] found = True #limb_r0, limb_lon0, limb_lat0 = spice.reclat(limb_point0) #print('r = {}, bracket latitudes:({},{}), limb_lat0={}'.format(r,limb_lats[i], limb_lats[i+1], limb_lat0*spice.dpr())) if not found: return None # set and transform direction vectors in J2000 frame self.setDirection(target.frame, limb_tangt0) self.toJ2000(et) return self.data
[docs]class Limb_Min_Latitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'max_incidence_angle']] self.set_parameters(parameters)
[docs] def compute(self, et): self.data = None limb_lats = Limb_Latitudes(self.parameters).compute(et) if limb_lats: self.data = min(limb_lats) return self.data
[docs]class Limb_Max_Latitude(Scalar): # DOING... def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'max_incidence_angle']] self.set_parameters(parameters)
[docs] def compute(self, et): self.data = None limb_lats = Limb_Latitudes(self.parameters).compute(et) if limb_lats: self.data = max(limb_lats) return self.data
[docs]class Limb_Latitudes(Vector): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.data = [None, None, None, None] self.units = 'deg' self.required_parameters_sets = [['target', 'max_incidence_angle']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) max_incidence_angle = self.parameters['max_incidence_angle'] # set spice.limbpt input parameters method = 'TANGENT/ELLIPSOID' corloc = 'CENTER' refvec = [0., 0., 1.0] ncuts = 100 rolstp = spice.twopi() / ncuts schstp = 0 soltol = 0 maxn = 10000 npts, points, epochs, tangts = spice.limbpt(method, target.name, et, target.frame, ABCORR, corloc, OBSRVR, refvec, rolstp, ncuts, schstp, soltol, maxn) limb_lats = [] for point in points: r, lon, lat = spice.reclat(point) trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target.name, et, target.frame, ABCORR, OBSRVR, point) if incdnc * spice.dpr() < max_incidence_angle: # diurnal limb limb_lats.append(lat * spice.dpr()) self.data = limb_lats return self.data
[docs]class Limb_Point_Tangent_Direction(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target', 'latitude', 'max_incidence_angle', 'power_optimised']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) lat0 = self.parameters['latitude'] max_incidence_angle = self.parameters['max_incidence_angle'] power_optimised = self.parameters['power_optimised'] limb_data = Limb({'target': target.name}).compute(et) # [points, epochs, tangts] points = limb_data[0] epochs = limb_data[1] tangts = limb_data[2] # Find desired limb point, eg: could be the brightest point on the diurnal limb, or a specific latitude if visible. Herebelow the latter. limb_lats = [] limb_points = [] limb_tangts = [] limb_epochs = [] # max_incidence_angle = 90.0 for point, tangt, epoch in zip(points, tangts, epochs): r, lon, lat = spice.reclat(point) trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target.name, et, target.frame, ABCORR, OBSRVR, point) if incdnc * spice.dpr() < max_incidence_angle: limb_lats.append(lat * spice.dpr()) limb_points.append(point) limb_tangts.append(tangt) limb_epochs.append(epoch) n_points = len(limb_lats) if n_points < 2: return None ## convert to np arrays limb_lats = np.array(limb_lats) limb_points = np.array(limb_points) limb_tangts = np.array(limb_tangts) limb_epochs = np.array(limb_epochs) ## sort arrays based on latitudes array sorted in ascending order sort_idxs = limb_lats.argsort() limb_lats = limb_lats[sort_idxs] limb_points = limb_points[sort_idxs] limb_tangts = limb_tangts[sort_idxs] limb_epochs = limb_epochs[sort_idxs] # search and interpolate found = False for i in range(n_points - 1): if limb_lats[i] <= lat0 and limb_lats[i + 1] > lat0: r = (lat0 - limb_lats[i]) / (limb_lats[i + 1] - limb_lats[i]) limb_point0 = (1-r) * limb_points[i] + r * limb_points[i+1] limb_tangt0 = (1-r) * limb_tangts[i] + r * limb_tangts[i+1] epoch0 = (1-r) * limb_epochs[i] + r * limb_epochs[i+1] found = True if not found: return None tangent_dir = spice.vcrss(limb_point0, limb_tangt0) sunpos, ltime = spice.spkpos('SUN', et, target.frame, ABCORR, OBSRVR) if power_optimised: # Sun direction angle must be minimal if spice.vsep(tangent_dir, sunpos) > spice.halfpi(): tangent_dir = -tangent_dir else: # Sun direction angle must be maximal if spice.vsep(tangent_dir, sunpos) < spice.halfpi(): tangent_dir = -tangent_dir # set and transform direction vectors in J2000 frame self.setDirection(target.frame, tangent_dir) self.toJ2000(et) return self.data
[docs]class SC_In_Equatorial_Plane(Scalar): """SC_In_Equatorial_Plane. """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.type = 'boolean' self.required_parameters_sets = [['target', 'margin']] self.set_parameters(parameters)
[docs] def compute(self, et): target = Target(self.parameters['target']) margin = self.parameters['margin'] sc_pos, ltime = spice.spkpos('JUICE', et, target.frame, 'NONE', target.name) r, lon, lat = spice.reclat(sc_pos) lat = lat * spice.dpr() sc_in_equatorial_plane = False if (lat <= margin) and (lat >= -margin): sc_in_equatorial_plane = True self.data = sc_in_equatorial_plane return self.data
from trianglesolver import solve
[docs]class Equatorial_Coverage(Scalar): """Geometry class representing the maximum MAJIS scanner latitudinal coverage on a given target, as seen from its equatorial plane. Initially implemented to enable the definition of MAJIS Jupiter Disk Nadir Scan observation opportunity. """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target']] self.set_parameters(parameters) self.units = 'deg'
[docs] def compute(self, et): target = Target(self.parameters['target']) # retrieve target body mean radius radius = np.max(target.radii) # Compute distance from target centre to spacecraft targpos, ltime = spice.spkpos(target.name, et, target.frame, ABCORR, OBSRVR) distance = spice.vnorm(targpos) # Compute latitudinal coverage A, using trianglesolver. try: a, b, c, A, B, C = solve(b=distance, c=radius, C=MAJIS_SCAN_ANGLE_MAX, ssa_flag='obtuse') except: if distance <= radius: A = 0 else: A = 0.5 * spice.pi() - np.arcsin(radius / distance) self.data = A * spice.dpr() return self.data
[docs]class ROI_In_FOV(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.type = 'boolean' self.required_parameters_sets = [['roi', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): roi = ROI(self.parameters['roi']) detector = Detector(self.parameters['detector']) # Determine ray pointing from ROI center location on the target body surface to the spacecraft/detector roi_spoint = roi.getSurfacePoint() target_pos, ltime = spice.spkpos(roi.target.name, et, roi.target.frame, ABCORR, OBSRVR) raydir = -(target_pos + roi_spoint) # Determine of ROI is occulted by the target (found = True) spoint, trgepc, srfvec, found = spice.sincpt('ELLIPSOID', roi.target.name, et, roi.target.frame, ABCORR, OBSRVR, roi.target.frame, raydir) # Determine if a ray is within the FOV of a specified detector if found: self.data = False else: self.data = spice.fovray(detector.name, -raydir, roi.target.frame, 'NONE', OBSRVR, et) return self.data
[docs]class ROI_Boresight_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['roi', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): roi = ROI(self.parameters['roi']) detector = Detector(self.parameters['detector']) # Determine ROI position/direction in detector frame at epoch et roi_spoint = roi.getSurfacePoint() target_pos, ltime = spice.spkpos(roi.target.name, et, roi.target.frame, ABCORR, OBSRVR) roi_dir = target_pos + roi_spoint rotmat = spice.pxform(roi.target.frame, detector.frame, et) # from body-fixed to detector frame at epoch et roi_dir = spice.mxv(rotmat, roi_dir) # in detector frame # Determine separation angle between ROI position vector and detector boresight roi_boresight_angle = spice.vsep([0., 0., 1.], roi_dir) * spice.dpr() self.data = roi_boresight_angle return self.data
from gfinder.roi import ROI
[docs]class ROI_X_Depointing_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target', 'detector', 'roi']] self.set_parameters(parameters)
[docs] def compute(self, et): roi_def = { "id": "NA", "target": self.parameters['target'], "center_lon": self.parameters['roi']['parameters']['lon'], "center_lat": self.parameters['roi']['parameters']['lat'], "diameter": 1.0 } roi = ROI(roi_def) detector = Detector(self.parameters['detector']) # Determine ROI position in detector frame at epoch et roi_spoint = roi.getSurfacePoint() target_pos, ltime = spice.spkpos(roi.target.name, et, roi.target.frame, ABCORR, OBSRVR) roi_dir = target_pos + roi_spoint rotmat = spice.pxform(roi.target.frame, detector.frame, et) # from body-fixed to detector frame at epoch et roi_dir = spice.mxv(rotmat, roi_dir) # in detector frame # Determine separation angle between ROI position vector and XZ plane of detector frame plane = spice.nvp2pl([0., 1., 0.], [0., 0., 0.]) roi_x_depointing_angle = spice.vsep(spice.vprjp(roi_dir, plane), roi_dir) * spice.dpr() self.data = roi_x_depointing_angle return self.data
[docs]class ROI_Y_Depointing_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['roi', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): roi = ROI(self.parameters['roi']) detector = Detector(self.parameters['detector']) # Determine ROI position in detector frame at epoch et roi_spoint = roi.getSurfacePoint() target_pos, ltime = spice.spkpos(roi.target.name, et, roi.target.frame, ABCORR, OBSRVR) roi_dir = target_pos + roi_spoint rotmat = spice.pxform(roi.target.frame, detector.frame, et) # from body-fixed to detector frame at epoch et roi_dir = spice.mxv(rotmat, roi_dir) # in detector frame # Determine separation angle between ROI position vector and YZ plane of detector frame plane = spice.nvp2pl([1., 0., 0.], [0., 0., 0.]) roi_y_depointing_angle = spice.vsep(spice.vprjp(roi_dir, plane), roi_dir) * spice.dpr() self.data = roi_y_depointing_angle return self.data
[docs]class Angular_Separation(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target1', 'target2']] self.set_parameters(parameters)
[docs] def compute(self, et): target1 = Target(self.parameters['target1']) target2 = Target(self.parameters['target2']) ref_frame = 'J2000' # retrieve position vector of target1 as seen from observer in ref frame target1_pos, ltime = spice.spkpos(target1.name, et, ref_frame, ABCORR, OBSRVR) # retrieve position vector of target2 as seen from observer in ref frame target2_pos, ltime = spice.spkpos(target2.name, et, ref_frame, ABCORR, OBSRVR) # compute separation with taking account for target angular size angular_sep = spice.vsep(target1_pos, target2_pos) * spice.dpr() target1_ang_size = Target_Angular_Size({'target': target1.name}).compute(et) target2_ang_size = Target_Angular_Size({'target': target2.name}).compute(et) angular_sep = angular_sep - (target1_ang_size + target2_ang_size) / 2 self.data = angular_sep * spice.dpr() return self.data
[docs]class Target_Solar_Eclipse(Scalar): """Target_Solar_Eclipse. https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/occult_c.html """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target', 'occult_type']] self.set_parameters(parameters) self.type = 'boolean'
[docs] def compute(self, et): target = Target(self.parameters['target']) occult_type = self.parameters['occult_type'] occult_code = spice.occult('SUN', 'ELLIPSOID', 'IAU_SUN', target.name, 'ELLIPSOID', target.frame, ABCORR, OBSRVR, et) occult_types = { 'TOTAL': -3, 'ANNULAR': -2, 'PARTIAL': -1 } self.data = False if occult_code < 0: self.data = (occult_code == occult_types[occult_type]) return self.data
[docs]class No_Compensation_Required(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['margin']] self.set_parameters(parameters) self.type = 'boolean'
[docs] def compute(self, et): margin = self.parameters['margin'] # Retrieve dwell time from sibling Dwell_Time geometry data. # or compute using Dwell_Time class (TODO) dwell_time = self.geoevt.parent_event.get_sub_events_geometry_data('Dwell_Time')[self.geoevt.index] time_step = self.geoevt.parent_event.time_step # observation measurements time step # !!Geometries depending of parent GeometryEvent object (Geometry objects container) are NOT SEARCHABLE !! # dwell_time = time_step = 1.0 # Compensation is required when dwell time is significantly different from the repetition time step. # If time_step - margin (%) < Dwell Time < time_step + margin (%), then no compensation is required. # no_compensation_required = False if (dwell_time <= time_step * (1 + margin)) and dwell_time >= time_step * (1 - margin): no_compensation_required = True self.data = no_compensation_required return self.data
[docs]class Motion_Compensation_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'rad' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): if et == self.geoevt.reference_time: # TODO: check that parameters match Motion_Compensation parameters. mc_data = self.geoevt.parent_event.get_sub_events_geometry_data('Motion_Compensation')[self.geoevt.index] self.data = mc_data[0] else: self.data = None # if self.data > MAJIS_SCAN_ANGLE_MAX*spice.rpd() or self.data < -MAJIS_SCAN_ANGLE_MAX*spice.rpd(): # print('Invalid scanner angle: line number = {}, angle = {}'.format(self.geoevt.index, self.data*spice.dpr()/2)) return self.data
[docs]class Motion_Compensation(Multidimentional): """Geometry class that computes the detector boresight rotation angle required for two consecutive detector FOV (slit) footprints to be contiguous (avoiding overlapping or drifting). Motion compensation is required when dwell time is greater or lower than the repetition time between two detector acquisitions. It holds the coordinates of the rotated (compensated) forward- and backward-boresights surface intersect points in the target body-fixed frame. These coordinates are used to derive the required rotation angle of the next detector acquisition, and scan mirror position. In our computation approach, we currently make the following hypothesis: - Boresight points in the "nadir" direction (vector from spacecraft to sub-spacecraft point or target center) - Slit (detector FOV) is perpendicular to orbital plane (velocity vector) - Spacecraft is in forward motion (velocity vector co-aligned with +Ysc axis) Note for later: Direction-class-based method, by which only the SC pointing would be simulated (disabling the MAJIS Scan frame simulator). This would allow to determine the scanner motion required by any SC pointings (off-track pointing for example). simulator.turn_off(frame='JUICE_MAJIS_SCAN') detector_bsight = Direction() detector_bsight.setDirection(self.detector.frame, self.detector.get_forward_bsight()) #detector_bsight.toJ2000(et) Q: why would I transform to J2000 ? forward_bsight_pvec = detector_bsight.data simulator.turn_on(frame='JUICE_MAJIS_SCAN') """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target', 'detector', 'start_scan_mode']] self.set_parameters(parameters)
[docs] def compute(self, et): # Set parameters self.target = Target(self.parameters['target']) self.detector = Detector(self.parameters['detector']) self.start_scan_mode = self.parameters['start_scan_mode'] # set start scanner angle depending of input start_scan_mode: "zero" or "max_range" at the moment. start_angle = 0.0 if self.start_scan_mode == 'max_range': start_angle = MAJIS_Scan_Angle({}, silent=True).scanner_angular_range * spice.rpd() # set number of detector binning (used to derive forward- and backward-boresight directions) binning = float(self.detector.binning) # print(f'> binning = {binning}') # Compute detector boresight / nadir direction, spacecraft position and scan mirror rotation axis in body-fixed frame spoint, trgepc, srfvec = spice.subpnt('INTERCEPT/ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR) bsight = spice.vhat(srfvec) # ~ NADIR. unit vector from observer to sub-observer point at epoch et, in body-fixed frame. target_state, ltime = spice.spkezr(self.target.name, et, self.target.frame, 'NONE', OBSRVR) sc_position = -target_state[0:3] sc_velocity = -target_state[3:] rot_axis = spice.vcrss(sc_velocity, bsight) # hypothesis for now: scan mirror rotation axis. # Adjust start angle and rot_axis depending on whether +Ysc (simulated or not) points forward or backward w.r.t # spacecraft motion. if SC_Y_Axis_Velocity_Angle({'target': self.target.name}, geoevt=self.geoevt).compute(et) < 90.0: # forward motion start_angle = -start_angle # print(f'> SC forward motion') else: # backward motion rot_axis = -rot_axis # print(f'> SC backward motion') # print(f'> sc_position = {sc_position}') # print(f'> rot_axis = {rot_axis}') # print(f'> start_angle = {start_angle}') # Compute rotation matrix from target body-fixed to J2000 frame rot_mat = spice.pxform(self.target.frame, 'J2000', et-ltime) # Compute the first acquisition forward-boresight surface intersect point # if self.geoevt.index == 0: # Note applicable anymore: # # Adjust start angle depending of whether we are in the "too-fast" (dwell time < Trep) or "too-slow" # # (dwell time < Trep) scenario. # if self.start_scan_mode == 'max_range': # otherwise not needed because start scanner angle is zero. # t_rep = self.geoevt.parent_event.time_step # dwell_time = Dwell_Time({'target':self.target.name, 'detector': self.detector.name}).compute(et) # if dwell_time > t_rep: # start_angle = -start_angle # Set initial required scan angle to start angle req_scanner_angle = start_angle # Simulate initial rotated boresight, taking into account for start scanner angle # bsight = spice.vrotv(bsight, rot_axis, start_angle) forward_bsight_spoint = [0.0, 0.0, 0.0] backward_bsight_spoint = [0.0, 0.0, 0.0] # Compute forward-boresight pointing vector at epoch et. # forward_bsight_pvec = spice.vrotv(bsight, rot_axis, -0.5*IFOV*binning) # body-fixed forward_bsight_pvec = spice.vrotv(bsight, rot_axis, -0.5*IFOV*binning + req_scanner_angle) # body-fixed forward_bsight_pvec = spice.mxv(rot_mat, forward_bsight_pvec) # J2000 # compute forward-boresight surface intersect point spoint, trgepc, srfvec, found = spice.sincpt('ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR, 'J2000', forward_bsight_pvec) if found: forward_bsight_spoint = spoint else: print('[WARNING] Motion_Compensation #1 : No surface intercept point found.') # DEBUG # # compute backward-boresight point vector at epoch et, in body-fixed frame. # backward_bsight_pvec = spice.vrotv(bsight, rot_axis, 0.5 * IFOV * binning) # backward_bsight_pvec = spice.mxv(rot_mat, backward_bsight_pvec) # J2000 # print(f'> start_angle = {start_angle}') # print(f'> backward_bsight_pvec = {backward_bsight_pvec}') # print(f'> req_scanner_angle = {req_scanner_angle}') else: # Retrieve previous forward-boresight surface intersect point prev_motion_comp_data = self.geoevt.parent_event.get_sub_events_geometry_data('Motion_Compensation')[self.geoevt.index - 1] prev_forward_bsight_spoint = prev_motion_comp_data[2] # Compute the required backward-boresight pointing vector, at epoch et, to intersect with previous # forward-boresight pointing vector surface intersection point (leading to acquiring contiguous iFOV # footprints); defined by the difference between the latter and the detector/SC position at epoch et. # sp_ltime = ltime * spice.vnorm(prev_forward_bsight_spoint - sc_position) / spice.vnorm(sc_position) target_pos, ltime = spice.spkpos(self.target.name, et-sp_ltime, self.target.frame, 'NONE', OBSRVR) #rot_mat = spice.pxfrm2(self.target.frame, 'J2000', et-sp_ltime, et) ...not needed at the end. rot_mat = spice.pxform(self.target.frame, 'J2000', et) req_backward_bsight_pvec = spice.vhat(prev_forward_bsight_spoint + target_pos) # body-fixed req_backward_bsight_pvec = spice.mxv(rot_mat, req_backward_bsight_pvec) # J2000 # Compute required scanner mirror rotation at epoch et, defined as the angular separation between the # required backward-boresight pointing vector and the backward-boresight point vector. # # compute backward-boresight point vector at epoch et, in body-fixed frame. # backward_bsight_pvec = spice.vrotv(bsight, rot_axis, 0.5*IFOV*binning) backward_bsight_pvec = spice.vrotv(bsight, rot_axis, 0.5 * IFOV * binning) backward_bsight_pvec = spice.mxv(rot_mat, backward_bsight_pvec) # J2000 # print(f'> backward_bsight_pvec = {backward_bsight_pvec}') # print(f'> req_backward_bsight_pvec = {req_backward_bsight_pvec}') req_scanner_angle = spice.vsep(backward_bsight_pvec, req_backward_bsight_pvec) # Get oriented angle (invert angle if backward to required-backward pointing vector rotation axis is # opposite to scanner rotation axis). # print(f'> vdot_rotaxis = {spice.vdot(spice.vcrss(backward_bsight_pvec, req_backward_bsight_pvec), rot_axis)}') if spice.vdot(spice.vcrss(backward_bsight_pvec, req_backward_bsight_pvec), rot_axis) < 0: req_scanner_angle = -req_scanner_angle # print('> inverted') # print(f'> req_scanner_angle = {req_scanner_angle}') # Compute corresponding (required) forward-boresight surface intercept point, from forward-boresight # pointing vector and required scanner rotation angle. # req_forward_bsight_pvec = spice.vrotv(bsight, rot_axis, -0.5*IFOV*binning + req_scanner_angle) req_forward_bsight_pvec = spice.vrotv(bsight, rot_axis, -0.5 * IFOV * binning + req_scanner_angle) req_forward_bsight_pvec = spice.mxv(rot_mat, req_forward_bsight_pvec) # J2000 # compute forward-boresight surface intersect point spoint, trgepc, srfvec, found = spice.sincpt('ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR, 'J2000', req_forward_bsight_pvec) if found: forward_bsight_spoint = spoint else: forward_bsight_spoint = [0.0, 0.0, 0.0] print('[WARNING] Motion_Compensation #2 : No surface intercept point found.') backward_bsight_spoint = prev_forward_bsight_spoint self.data = [req_scanner_angle, list(backward_bsight_spoint), list(forward_bsight_spoint)] # print('>') return self.data
[docs]class Target_Angular_Size(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): target = self.parameters['target'] fixref = 'IAU_' + target # retrieve target body mean radius dim, radii = spice.bodvrd(target, 'RADII', 3) radius = np.max(radii) # Compute distance from target centre to spacecraft targpos, ltime = spice.spkpos(target, et, fixref, ABCORR, OBSRVR) distance = spice.vnorm(targpos) self.data = 2.0 * np.arcsin(radius / distance) * spice.dpr() return self.data
[docs]class SC_Pointing_Offsets(Vector): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['x_sc_angle','y_sc_angle', 'z_sc_angle'] self.data = [None]*3 self.units = 'deg' self.required_parameters_sets = [['reference_frame']] self.set_parameters(parameters)
[docs] def compute(self, et): reference_frame = self.parameters['reference_frame'] # Compute SC +Z axis/vector in reference frame rotmat = spice.pxform('JUICE_SPACECRAFT', reference_frame, et) # could the reference frame be the Simulated_SC_Frame ? # It problably should! # z_axis = spice.mxv(rotmat, [0., 0., 1.]) # self.data = [z_axis[0], z_axis[1]] rotmat = spice.pxform('JUICE_SPACECRAFT', reference_frame, et) x_sc = spice.vsep([1., 0., 0.], spice.mxv(rotmat, [1., 0., 0.])) * spice.dpr() y_sc = spice.vsep([0., 1., 0.], spice.mxv(rotmat, [0., 1., 0.])) * spice.dpr() z_sc = spice.vsep([0., 0., 1.], spice.mxv(rotmat, [0., 0., 1.])) * spice.dpr() self.data = [x_sc, y_sc, z_sc] return self.data
# Direction vector in J2000 specified by a vector in the spacecraft reference frame (JUICE_SPACECRAFT) #
[docs]class SC_Vector(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['vector']] self.set_parameters(parameters)
[docs] def compute(self, et): vector = self.parameters['vector'] self.setDirection('JUICE_SPACECRAFT', vector) self.toJ2000(et) return self.data
# Boresight direction vector in J2000 for a given input detector name. #
[docs]class Detector_Boresight(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters detector = Detector(self.parameters['detector']) # set and transform direction vector in J2000 frame self.setDirection(detector.frame, detector.get_bsight()) self.toJ2000(et) return self.data
[docs]class Detector_Forward_Boresight(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters detector = Detector(self.parameters['detector']) # set and transform direction vector in J2000 frame self.setDirection(detector.frame, detector.get_forward_bsight()) self.toJ2000(et) return self.data
[docs]class Detector_Backward_Boresight(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters detector = Detector(self.parameters['detector']) # set and transform direction vector in J2000 frame self.setDirection(detector.frame, detector.get_backward_bsight()) self.toJ2000(et) return self.data
# FOV boundary direction vectors in J2000 for a given input detector name. #
[docs]class Detector_FOV(Direction): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters detector = Detector(self.parameters['detector']) # set and transform direction vectors in J2000 frame self.setDirection(detector.frame, detector.get_bounds()) self.toJ2000(et) # print('Detector_FOV.compute(et) direction vectors:') # print(self.data) return self.data
[docs]class Detector_Boresight_Altitude(Scalar): """Altitude of the detector boresight tangent point above the target surface.""" def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters #self.detector = Detector(self.parameters['detector']) bsight_target_point_geometry = \ self.geoevt.parent_event.get_sub_events_geometries('Detector_Boresight_Target_Point')[self.geoevt.index] # print(bsight_target_point_geometry.data) self.data = bsight_target_point_geometry.data[0][2] # altitude # print(self.data) # print() return self.data
[docs]class Detector_Boresight_Ground_Drift(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'IFOV' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # set required parameters self.detector = Detector(self.parameters['detector']) # # Compute detector boresight ground angular drift between two computation time step # time_step = self.get_time_step() # geoloc1 = Detector_Boresight_Target_Point(self.parameters, geoevt=self.geoevt) # geoloc1.compute(et) # geoloc2 = Detector_Boresight_Target_Point(self.parameters, geoevt=self.geoevt) # geoloc2.compute(et+time_step) # bsight_drift = geoloc1.angular_sep(geoloc2) # # print('> geoloc1.data={}, geoloc2.data={}'.format(geoloc1.data, geoloc2.data)) if self.geoevt.index == 0: self.data = None return self.data else: # Retrieve current and previous Detector_Boresight_Target_Point. # Rule: Detector_Boresight_Target_Point must be placed before Detector_Boresight_Ground_Drift in ODF file. bsight_target_point_geometry = \ self.geoevt.parent_event.get_sub_events_geometries('Detector_Boresight_Target_Point')[self.geoevt.index] prev_bsight_target_point_geometry = \ self.geoevt.parent_event.get_sub_events_geometries('Detector_Boresight_Target_Point')[self.geoevt.index - 1] # Compute angular separation between both geolocations bsight_drift = bsight_target_point_geometry.angular_sep(prev_bsight_target_point_geometry) # Compute dir_def = {'name': 'Detector_Forward_Boresight', 'parameters': {'detector': self.parameters['detector'] }} forward_bsight_geoloc = Observed_Target_Points({'target': self.parameters['target'], 'direction': dir_def}, geoevt=self.geoevt) forward_bsight_geoloc.compute(et) dir_def = {'name': 'Detector_Backward_Boresight', 'parameters': {'detector': self.parameters['detector']}} backward_bsight_geoloc = Observed_Target_Points({'target': self.parameters['target'], 'direction': dir_def}, geoevt=self.geoevt) backward_bsight_geoloc.compute(et) ifov_ground_res = forward_bsight_geoloc.angular_sep(backward_bsight_geoloc) #print('> time_step={}, bsight_drift={}, ifov_ground_res={}, ratio={}'.format(time_step, bsight_drift, ifov_ground_res, bsight_drift / ifov_ground_res)) if ifov_ground_res != 0.0: self.data = bsight_drift / ifov_ground_res else: self.data = 0.0 #print('This?! > ', bsight_drift, ifov_ground_res) return self.data
[docs]class Directions_Angular_Separation(Scalar): """Compute separation angle between two direction vectors. Example of usage in ODF file:: "name": "Directions_Angular_Separation", "parameters": { "direction1": { "name": "SC_Vector", "parameters": { "vector": [0.0, 1.0, 0.0] } }, "direction2": { "name": "Observer_Target_Velocity", "parameters": { "target": "JUPITER" } }, } """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['direction1', 'direction2']] self.set_parameters(parameters)
[docs] def compute(self, et): dir_vec1 = GeometryFactory().create(self.parameters['direction1'], geoevt=self.geoevt).compute(et)[0] dir_vec2 = GeometryFactory().create(self.parameters['direction2'], geoevt=self.geoevt).compute(et)[0] self.data = spice.vsep(dir_vec1, dir_vec2) * spice.dpr() return self.data
[docs]class SC_X_Axis_Sun_Angle(Scalar): # Useful to check solar array and radiator illumination. # Ideally, one could use it to through warnings, eg: "Solar Array not in light!", "Radiator not in shadow!" # def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) #self.required_parameters_sets = [['target']] self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'SC_Vector', 'parameters': {'vector': [1.0, 0.0, 0.0]}} dir2_def = {'name': 'Target_Position', 'parameters': {'target': 'SUN'}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class SC_Solar_Array_Sun_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'SC_Vector', 'parameters': {'vector': [-1.0, 0.0, 0.0]}} dir2_def = {'name': 'Target_Position', 'parameters': {'target': 'SUN'}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class SC_Y_Axis_Velocity_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'SC_Vector', 'parameters': {'vector': [0.0, 1.0, 0.0]}} dir2_def = {'name': 'SC_Velocity', 'parameters': {'target': self.parameters['target']}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class SC_Z_Axis_Sun_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'SC_Vector', 'parameters': {'vector': [0.0, 0.0, 1.0]}} dir2_def = {'name': 'Target_Position', 'parameters': {'target': 'SUN'}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class SC_Z_Axis_Target_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'SC_Vector', 'parameters': {'vector': [0.0, 0.0, 1.0]}} dir2_def = {'name': 'Target_Position', 'parameters': {'target': self.parameters['target']}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class Boresight_Target_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['detector', 'target']] self.set_parameters(parameters)
[docs] def compute(self, et): dir1_def = {'name': 'Detector_Boresight', 'parameters': {'detector': self.parameters['detector']}} dir2_def = {'name': 'Target_Position', 'parameters': {'target': self.parameters['target']}} self.data = Directions_Angular_Separation({'direction1': dir1_def, 'direction2': dir2_def}, geoevt=self.geoevt).compute(et) return self.data
[docs]class Observed_Target_Points(Geolocation): # Compute the geodetic coordinates of the point(s) observed by one or several direction vectors, from observer (SC). # Used by Detector_FOV_Footprint, Detector_Boresight_Target_Point. # # "name": "Observed_Target_Points", # "parameters": { # "target": "CALLISTO", # "direction": { # "name": "Detector_Boresight", # "parameters": { # "detector": "JUICE_MAJIS_VISNIR" # } # }, # } def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target', 'direction']] self.set_parameters(parameters) self.data = []
[docs] def compute(self, et): # Set/initialise required parameters self.target = Target(self.parameters['target']) obs_points = [] # geodetic coordinates of observed target points wrt the target's surface [lon, lat, altitude] # set parameters dref = 'J2000' method = 'ELLIPSOID' # should later become a parameter taking into account for digital shape model (if required) # Retrieve or compute direction vectors in J2000 frame if self.geoevt: #print('>>> retrieving <{}> direction vectors.'.format(self.parameters['direction']['name'])) dir_vectors = self.geoevt.get_geometry_data(self.parameters['direction']['name']) if dir_vectors is None: #print('>>>>> Direction object not pre-computed (because not in ODF)') #print('>>>>> computing <{}> direction vectors.'.format(self.parameters['direction']['name'])) dir_vectors = GeometryFactory().create(self.parameters['direction'], geoevt=self.geoevt).compute(et) else: # print('>>> computing <{}> direction vectors.'.format(self.parameters['direction']['name'])) dir_vectors = GeometryFactory().create(self.parameters['direction'], geoevt=self.geoevt).compute(et) # compute observed target point for each direction vector (whether surface intersect or nearest point) for dir_vector in dir_vectors: dvec = np.array(dir_vector) spoint, trgepc, srfvec, found = spice.sincpt(method, self.target.name, et, self.target.frame, ABCORR, OBSRVR, dref, dvec) obspt = spoint if not found: # compute tangent point, using the new SPICE `tangpt` routine, CSPICE Version 1.0.0, 28-OCT-2021 (MCS) tanpt, alt1, range, srfpt, trgepc, srfvec = spice.tangpt(method, self.target.name, et, self.target.frame, ABCORR, 'TANGENT POINT', OBSRVR, dref, dvec) obspt = tanpt obs_points.append(obspt.tolist()) # Set geolocation points from observation points self.setGeolocation(obs_points) return self.data
[docs]class Detector_Boresight_Target_Point(Geolocation): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 # force dimension to one; returned data is a list. self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # Set/initialise required parameters self.target = Target(self.parameters['target']) dir_def = {'name': 'Detector_Boresight', 'parameters': {'detector': self.parameters['detector'] }} self.data = Observed_Target_Points({'target': self.parameters['target'], 'direction': dir_def}, geoevt=self.geoevt).compute(et) self.n_points = len(self.data) # update Geolocation number of points return self.data
[docs]class Detector_FOV_Footprint(Geolocation): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # Set/initialise required parameters self.target = Target(self.parameters['target']) dir_def = {'name': 'Detector_FOV', 'parameters': {'detector': self.parameters['detector'] }} self.data = Observed_Target_Points({'target': self.parameters['target'], 'direction': dir_def}, geoevt=self.geoevt).compute(et) #self.n_points = len(self.data) # update Geolocation number of points -> to be checked. return self.data
[docs]class Vertical_Resolution(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # compute distance to limb limb_distance = Limb_Distance({'target': self.parameters['target']}).compute(et) # compute corresponding spatial resolution self.data = limb_distance * IFOV * Detector(self.parameters['detector']).binning # .ifov attribute could be used instead return self.data
[docs]class Limb_Distance(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): self.target = Target(self.parameters['target']) # compute spacecraft altitude sc_altitude = SC_Altitude({'target': self.target.name}).compute(et) # retrieve target radius radius = self.target.re # compute to distance to limb: radius^2 + limb_distance^2 = (radius+ sc_altitude)^2 self.data = np.sqrt(np.square(radius+sc_altitude)-np.square(radius)) return self.data
[docs]class Spatial_Resolution(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): # compute spacecraft altitude sc_altitude = SC_Altitude({'target': self.parameters['target']}).compute(et) # compute corresponding spatial resolution self.data = sc_altitude * IFOV * Detector(self.parameters['detector']).binning # .ifov attribute could be used instead return self.data
[docs]class Ground_Speed(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km/s' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): self.target = Target(self.parameters['target']) # Simple method spoint1, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR) spoint2, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', self.target.name, et + 1.0, self.target.frame, ABCORR, OBSRVR) self.data = spice.vnorm(spoint1 - spoint2) return self.data
[docs]class Dwell_Time(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 's' self.required_parameters_sets = [['target', 'detector']] self.set_parameters(parameters)
[docs] def compute(self, et): ifov_ground_resolution = Spatial_Resolution(self.parameters).compute(et) ground_speed = Ground_Speed({'target': self.parameters['target']}).compute(et) self.data = (ifov_ground_resolution / ground_speed) #/ self.get_time_step() return self.data
[docs]class SC_Altitude(Scalar): # apparent sub-observer point altitude def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): self.target = Target(self.parameters['target']) spoint, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR) self.data = spice.vnorm(srfvec) return self.data
[docs]class Obs_Min_SC_Altitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: sc_altitudes = self.geoevt.get_sub_events_geometry_data('SC_Altitude') self.data = min(sc_altitudes) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Obs_Max_SC_Altitude(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: sc_altitudes = self.geoevt.get_sub_events_geometry_data('SC_Altitude') self.data = max(sc_altitudes) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Distance_To_Target(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): target = self.parameters['target'] fixref = 'IAU_' + target # Compute vector from target centre to spacecraft targpos, ltime = spice.spkpos(target, et, fixref, ABCORR, OBSRVR) self.data = spice.vnorm(targpos) return self.data
[docs]class Obs_Min_Spatial_Resolution(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: spatial_resolutions = self.geoevt.get_sub_events_geometry_data('Spatial_Resolution') self.data = min(spatial_resolutions) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Obs_Max_Spatial_Resolution(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'km' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: spatial_resolutions = self.geoevt.get_sub_events_geometry_data('Spatial_Resolution') self.data = max(spatial_resolutions) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Phase_Angle(Scalar): # parameters = {'target':'GANYMEDE', 'detector': 'JUICE_MAJIS_VISNIR'} def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.required_parameters_sets = [['target']] self.set_parameters(parameters)
[docs] def compute(self, et): target = self.parameters['target'] fixref = 'IAU_' + target spoint, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', target, et, fixref, ABCORR, OBSRVR) trgepc, srfvec, phase, incdnc, emissn = spice.ilumin('ELLIPSOID', target, et, fixref, ABCORR, OBSRVR, spoint) self.data = phase * spice.dpr() return self.data
[docs]class SubSC_Point(Geolocation): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.dimension = 1 self.required_parameters_sets = [['target']] self.set_parameters(parameters) # def __format__(self, geometry_fmt): # if self.data is not None: # str = '[{:f},{:f}]'.format(self.data[0],self.data[1]) # else: # str = '[{:f},{:f}]'.format(0,0) # return str
[docs] def compute(self, et): # set target self.target = Target(self.parameters['target']) # compute sub-spacecraft point rectangular coordinates spoint, trgepc, srfvec = spice.subpnt('NEAR POINT/ELLIPSOID', self.target.name, et, self.target.frame, ABCORR, OBSRVR) self.setGeolocation(spoint) return self.data
[docs]class Obs_SC_Slew_Parameters(Vector): """Obs_SC_Slew_Parameters. Requires Target_Angular_Size data. """ def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.data = [None] * 5 self.prefix = ['x_start', 'x_stop', 'x_rate', 'slew_duration', 'n_lines'] self.units = ['deg', 'deg', 'deg/s', 's', ''] self.required_parameters_sets = [['detector']] self.set_parameters(parameters)
[docs] def compute(self, et): target_angular_sizes = self.geoevt.get_measurements_geometry_data('Target_Angular_Size') # x_start = -0.5*target_angular_sizes[0] # x_stop = 0.5*target_angular_sizes[-1] # angular_range = x_stop-x_start # detector_ifov = Detector(self.parameters['detector']).ifov[1] * spice.dpr() # slew_duration = self.geoevt.time_step * (angular_range/detector_ifov) # x_rate = angular_range/slew_duration # n_lines = slew_duration/self.geoevt.time_step x_start = -0.5 * target_angular_sizes[0] x_stop = 0.5 *target_angular_sizes[-1] angular_range = x_stop - x_start detector_ifov = Detector(self.parameters['detector']).ifov[1] * spice.dpr() time_step = self.geoevt.time_step x_rate = detector_ifov / time_step n_lines = int(angular_range / detector_ifov) slew_duration = n_lines * time_step self.data = [x_start, x_stop, x_rate, slew_duration, n_lines] return self.data
from numpy.polynomial import Polynomial
[docs]class Obs_Scanner_Motion_Parameters(Vector): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.data = [None] * 5 self.prefix = ['start_angle', 'start_scan_rate', 'stop_scan_rate', 'scan_duration', 'scan_lines'] self.units = ['deg', 'deg/s', 'deg/s', 's', ''] self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: # Retrieve pre-computed scanner motion compensation "optimised" optical angles and acquisition times mc_angles = np.array(self.geoevt.get_measurements_geometry_data('Motion_Compensation_Angle')) * spice.dpr() / 2 etimes = np.array(self.geoevt.get_measurements_times(format='et')) times = etimes - etimes[0] # Determine if and when MC angle goes beyond scanner angular range (+/- 2.0 degrees) majis_scan_angle_obj = MAJIS_Scan_Angle({}, silent=True) # check that start scanner angle is +/- 2.0 degrees start_angle = mc_angles[0] if abs(start_angle) - majis_scan_angle_obj.scanner_angular_range / 2 != 0.0: print(f'[WARNING] Invalid motion compensation start angle: {start_angle}. It must correspond to +/-2.0 degrees.') return self.data # Symmetrical scan stop_angle = -start_angle last_valid_line = -1 for i in range(len(mc_angles) - 1): if stop_angle < 0: if (mc_angles[i] > stop_angle) and (mc_angles[i+1] < stop_angle): last_valid_line = i else: if (mc_angles[i] < stop_angle) and (mc_angles[i+1] > stop_angle): last_valid_line = i # No symmetrical scan is possible if last_valid_line < 0: return self.data # Compute valid scanner angles and positions corresponding to input optimal motion compensation (MC) angles valid_mc_scanner_angles = [] valid_mc_scanner_positions = [] for mc_angle in mc_angles[0:last_valid_line + 2]: valid_angle, valid_position = majis_scan_angle_obj.get_valid_scanner_angle(mc_angle) valid_mc_scanner_angles.append(valid_angle) valid_mc_scanner_positions.append(valid_position) valid_mc_scanner_angles = np.array(valid_mc_scanner_angles) valid_mc_scanner_positions = np.array(valid_mc_scanner_positions) # Estimate optimal scan duration and update last acquisition time scan_duration = np.interp(stop_angle, mc_angles, times, period=360) mc_scanner_times = times[0:last_valid_line + 2].copy() mc_scanner_times[-1] = scan_duration # Compute duration of each optimal acquisition delta_mc_acq_times = mc_scanner_times[1:] - mc_scanner_times[0:-1] # Compute angular variation between each acquisition valid_delta_mc_scanner_angles = valid_mc_scanner_angles[1:] - valid_mc_scanner_angles[0:-1] # Compute optimal scanner angular velocity valid_mc_scanner_speeds = valid_delta_mc_scanner_angles / delta_mc_acq_times # Compute start/stop scanner angular velocities based on a first order polynomial fit coefs = Polynomial.fit(valid_mc_scanner_positions[0:-1], valid_mc_scanner_speeds, 1).convert().coef fit_scan_rates = coefs[0] + coefs[1] * valid_mc_scanner_positions start_scan_rate = fit_scan_rates[0] stop_scan_rate = fit_scan_rates[-1] # Compute number of acquisitions fitting in the scan duration t_int = mc_scanner_times[1]-mc_scanner_times[0] scan_lines = int(np.floor(scan_duration / t_int)) self.data = [start_angle, start_scan_rate, stop_scan_rate, scan_duration, scan_lines] else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Obs_Limb_Latitudinal_Range(Vector): """Observable limb latitudinal range during an observation opportunity. """ def __init__(self, parameters, geoevt=None, silent=None): """Constructor. Args: parameters: geoevt: silent: """ super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['min', 'max'] self.data = [None, None] self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): min_lat = max(self.geoevt.get_sub_events_geometry_data('Limb_Min_Latitude')) max_lat = min(self.geoevt.get_sub_events_geometry_data('Limb_Max_Latitude')) if min_lat < max_lat: self.data = [min_lat, max_lat] return self.data
[docs]class Obs_Min_Phase_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: phase_angles = self.geoevt.get_sub_events_geometry_data('Phase_Angle') self.data = min(phase_angles) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Obs_Max_Phase_Angle(Scalar): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.units = 'deg' self.set_parameters(parameters)
[docs] def compute(self, et): if self.geoevt is not None: phase_angles = self.geoevt.get_sub_events_geometry_data('Phase_Angle') self.data = max(phase_angles) else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data
[docs]class Obs_Ref_SubSC_Position(Vector): def __init__(self, parameters, geoevt=None, silent=None): super().__init__(parameters, geoevt=geoevt, silent=silent) self.prefix = ['x', 'y', 'z'] self.data = [None] * 3 self.units = 'km' self.set_parameters(parameters)
[docs] def compute(self, et): ref = 0 # temporary sub_sub_sc_positions = [] if self.geoevt != None: sub_sub_sc_positions = self.geoevt.get_sub_events_geometry_data('SubSC_Point') self.data = sub_sub_sc_positions[ref] else: self.data = None self.valid = False print('Missing GeometryEvent object containing this geometry.') return self.data