from typing import List, Sequence, Tuple, Union, Any, Dict, Callable
import numpy as np
import math
import logging
# import sasktran as sk
from datetime import datetime
from sktimeutils import ut_to_datetime64
from ..geodesy import Geodetic
# from ..platform import Platform
from ..platform.rotationmatrix import UnitVectors
from .standard_orientation_techniques import \
_technique_set_limb_look_vectors_from_unit_xyz, \
_technique_set_boresight_look_at_location_llh, \
_technique_set_boresight_pointing_from_unitvectors, \
_technique_set_boresight_look_at_location_xyz, \
_technique_set_look_vectors_from_tangent_altitude, \
_technique_set_platform_position_from_llh, \
_technique_set_platform_position_from_observer_looking_at_llh, \
_technique_set_platform_position_from_platform, \
_technique_set_platform_position_from_xyz, \
_technique_set_observer_to_look_in_azi_elev, \
_technique_set_platform_pointing_from_platform, \
_technique_set_boresight_look_at_location_orbitangle, \
_technique_set_instrument_internal_orientation, \
_technique_set_icf_look_from_xyz, \
_technique_set_icf_orientation_from_azi_elev, \
_technique_set_icf_orientation_no_operation
# ------------------------------------------------------------------------------
# class PointingAlgorithms:
# ------------------------------------------------------------------------------
class PointingAlgorithms:
def __init__(self, platform: 'Platform'):
self._geo = Geodetic()
self.platform: 'Platform' = platform
# ------------------------------------------------------------------------------
# geo
# ------------------------------------------------------------------------------
@property
def geo(self):
return self._geo
# -----------------------------------------------------------------------------
# set_limb_boresight_to_look_at_tangent_altitude
# -----------------------------------------------------------------------------
def set_limb_boresight_to_look_at_tangent_altitude(self,
observer: np.ndarray,
target_tangent_altitude: float,
target_geographic_bearing: float,
roll_control: str,
roll_angle_degrees: float) -> bool:
"""
Sets the rotation matrix of the system so the boresight of the instrument is looking at a tangent point
at the given tangent altitude and bearing. The tangent altitude must be below the observer and above 5 km below
the ground.
Parameters:
observer: np.ndarray[3,]
The x,y,z location of the observer in meters in the :ref:`ecef` reference frame.
target_tangent_altitude : float
The tangent altitude of the target location in meteres above sea-level. The tangent altitude must be below the
observer and above 5 km below the ground otherwise it is not used.
target_geographic_bearing: float
The geographic bearing in degrres of the tangent altitude measured the current platform location. N=0, E=90, S=180, W=270.
roll_control: str
The string describing roll control, see: :ref:`rollcontrol`
roll_angle_degrees: float
The roll angle in diegrees in the clockwise direction from the zero point set by `roll_control`
Returns:
bool: True if successful.
"""
llh = self._geo.llh_from_xyz(observer)
north, east, down = self._geo.xyz_north_east_down(llh=llh)
maxaltitude = llh[2]
ok = (target_tangent_altitude < maxaltitude) and (target_tangent_altitude > -5000.0) # make sure the target tangent altitude is below the observer and no more than 5 km below the surface.
if (ok):
bearing = math.radians(target_geographic_bearing)
horiz = math.cos(bearing) * north + math.sin(bearing) * east
look = self._geo.xyz_lookvector_from_tangent_altitude(target_tangent_altitude, observer, horiz)
target = self._geo.xyz_tangent_point_location(observer, look)
G = self.apply_roll_control(look, target, observer, roll_control, roll_angle_degrees, horizontalunitvector=horiz)
self.platform.platform_pointing.force_pcf_rotation_matrix(G)
else:
logging.warning("PointingAlgorithms.set_limb_boresight_to_look_at_tangent_altitude, the target altitude {} is not between the observer altitude {} and 5 km below the ground".format(target_tangent_altitude, maxaltitude))
return ok
# -----------------------------------------------------------------------------
# set_limb_boresight_from_lookvector
# -----------------------------------------------------------------------------
def set_limb_boresight_from_lookvector(self,
observer: np.ndarray,
lookvector: np.ndarray,
roll_control: str,
roll_angle_degrees: float) -> bool:
"""
Sets the platform rotation matrices so the boresight of the instrument is looking in the direction given by lookvector. The
tangent point, implied by the look vector, must be in front of the observer and above 5 km below the ground.
Parameters:
observer: np.ndarray[3,]
The x,y,z location of the observer in meters in the :ref:`ecef` reference frame.
lookvector: np.ndarray[3]
The x,y,z look unit vector expressed in the :ref:`ecef` reference frame, looking from the platform towards the limb
roll_control: str
The string describing roll control, see: :ref:`rollcontrol`
roll_angle_degrees: float
The roll angle in diegrees in the clockwise direction from the zero point set by `roll_control`
Returns:
bool: True if successful.
"""
targetlocation = self._geo.xyz_tangent_point_location(observer, lookvector)
llh = self._geo.llh_from_xyz(targetlocation)
altitude = llh[2]
delta = targetlocation - observer
ok = (np.dot(delta, lookvector) > 0.0) and (altitude > -5000.0)
if (ok):
G = self.apply_roll_control(lookvector, targetlocation, observer, roll_control, roll_angle_degrees)
self.platform.platform_pointing.force_pcf_rotation_matrix(G)
else:
logging.warning("PointingAlgorithms.set_limb_boresight_from_lookvector, the tangent point is not in front of the observer or is more than 5000m below the ground [{}]. It has been discarded".format(altitude))
return ok
# ------------------------------------------------------------------------------
# get_unitvectors_at_target_location
# ------------------------------------------------------------------------------
def set_boresight_to_look_at_geocentric_location(self,
observer_location: np.ndarray,
targetlocation: np.ndarray,
roll_control: str,
roll_angle_degrees: float) -> bool:
"""
Sets the rotation matrix of the platform so the boresight of the instrument will look at the given geocentric
location. The target location must be more than 10 cm from the observer's location.
Parameters:
observer_location: np.ndarray[3,]
The x,y,z location of the observer in meters in the :ref:`ecef` reference frame.
targetlocation: np.ndarray[3,]
The x,y,z location of the target point in meters in the :ref:`ecef` reference frame
roll_control: str
The string describing roll control, see: :ref:`rollcontrol`
roll_angle_degrees: float
The roll angle in diegrees in the clockwise direction from the zero point set by `roll_control`
Returns:
bool: True if successful.
"""
lookvector = targetlocation - observer_location # Get the look vector from current location to the target location
dist = np.linalg.norm(lookvector) # Get the distance of the target from the observer
ok = (dist > 0.01) # make sure its more than 10 cm
if (ok): # and if we are good
lookvector /= dist # the normalize the lookvector
G = self.apply_roll_control(lookvector, targetlocation, observer_location, roll_control, roll_angle_degrees) # get the the desired unit vectors
self.platform.platform_pointing.force_pcf_rotation_matrix(G) # and calculate the new rotation matrix from the platform control frame.
else:
logging.warning("PointingAlgorithms.set_boresight_to_look_at_geocentric_location, the target location is located within 10 cm of the observer. It is too close. It has been discarded")
return ok
# ------------------------------------------------------------------------------
# get_unitvectors_at_target_location
# ------------------------------------------------------------------------------
def set_boresight_pointing_from_unitvectors(self, G: UnitVectors) -> bool:
"""
Sets the rotation matrix of the platform to the given UnitVector array. The array defines the required x, y and
z unit vectors in the :ref:`ecef`
Parameters:
G: UnitVectors
The class that represents a (3x3) matrix. Each column of the matrix is the desired x,y,z unit vector
in the :ref:`ecef` reference frame.
Returns:
bool: True if successful.
"""
self.platform.platform_pointing.force_pcf_rotation_matrix(G)
return True
# ------------------------------------------------------------------------------
# set_observer_to_look_in_azi_elev
# ------------------------------------------------------------------------------
def set_observer_to_look_in_azi_elev(self,
observer: np.ndarray,
elevation: float,
azimuth: np.ndarray,
roll_control: str,
roll_angle_degrees: float) -> bool:
"""
Sets the rotation matrix of the platform so the boresight of the instrument will look at the elevation
and azimuth defined in the horizontal plane at the observers location.
Parameters:
observer: np.ndarray[3,]
The x,y,z location of the observer in meters in the :ref:`ecef` reference frame.
elevation: float
The elevation in degrees of the look vector angle from the local horizontal plane at the observer.
Horizontal is 0 degrees. vertical up is 90, vertical down is -90. It is a left handed rotation around the
local west unit vector
azimuth: float
The azimuth in degrees of the look vector in the local horizontal plane of the observer. Azimuth is measured
clockwise, like a compass, with 0 in the due North direction. 0=N, 90=E, 180=S, 270=W. It is a left handed rotation
around the local up vector
roll_control: str
The string describing roll control, see: :ref:`rollcontrol`
roll_angle_degrees: float
The roll angle in degrees in the clockwise direction from the zero point set by `roll_control`
Returns:
bool: True if successful.
"""
llh = self._geo.llh_from_xyz(observer)
west, south, up = self._geo.xyz_west_south_up(llh=llh)
up = up
north = -south
east = -west
bearing = math.radians(azimuth)
theta = math.radians(elevation)
horiz = math.cos(bearing) * north + math.sin(bearing) * east
look = math.cos(theta) * horiz + math.sin(theta) * up
G = self.apply_roll_control(look, observer, observer, roll_control, roll_angle_degrees, horizontalunitvector=horiz)
self.platform.platform_pointing.force_pcf_rotation_matrix(G)
return True
# ------------------------------------------------------------------------------
# config_roll_control
# ------------------------------------------------------------------------------
def apply_roll_control(self,
lookvector: np.ndarray,
target_location: np.ndarray,
observer_location: np.ndarray,
roll_cntl: str,
roll_angle_degrees: float,
horizontalunitvector: np.ndarray = None) -> UnitVectors:
"""
Applys roll control to the look vector and returns the (3x3) array of Unit Vectors in the final :ref:`ecef` reference
frame.
Parameters:
lookvector : np.ndarray(3,)
Three element look unit vector away from the observer's position specified in the :ref:`ecef` coordinate system
target_location : np.ndarray(3,)
Three element position vector of the target location specified in meters in the :ref:`ecef` coordinate system
observer_location: np.ndarray(3,)
Three element position vector of the observer's location specified in meters in the :ref:`ecef` coordinate system
roll_cntl : str
The class of required roll control. 'limb', 'nadir' and 'standard' are currently supported.
roll_angle_degrees: float
The required roll angle required using the current control frame.
horizontalunitvector: np.ndarray(3,)
Optional. This is the horizontal unit vector in the horizontal plane of the look vector. It is only used in standard
roll control and allows the roll of the instrument unit vectors to stay well defined even when the looking straight up or down.
Returns:
UnitVectors
Returns the 3x3 array of unit vectors required in the final :ref:`ecef` reference frame. The platform rotation
matrix should be modified so that unit vectors in the :ref:`icf` will map to these unit vectors after rotation.
"""
if (roll_cntl == 'limb'): # roll control is limb
llh = self._geo.llh_from_xyz(target_location) # so zero roll occurs when the Instrument Z axis, is as parallel as possible to the local up unit vector
west, south, up = self._geo.xyz_west_south_up(llh=llh)
zunit = up # We want to choose a z unit vector. Get local up at the tangent point
ythreshold = math.sin(math.radians(0.5))
xunit = lookvector # Get the look vector unit vector as the X direction
yunit = np.cross(zunit, xunit) # Get the y unit vector which points towards the left as seen from the oobserver.
ymag = np.linalg.norm(yunit) # Get the magnitude of the cross product. This becomes
if ymag < ythreshold: # if we are in an unstable region where the first choice of Z is too parallel to the look vector then use the second option (ie local Z axis and lookvector are with 0.57 degrees of each other)
zunit2 = -south # so arbitrarily try local North
yunit = np.cross(zunit2, xunit) # Get the y unit vector which points towards the left as seen from the oobserver.
ymag = np.linalg.norm(yunit) # Get the magnitude of the cross product. This becomes
yunit /= ymag # get the magnitude of the Y unit vector
zunit = np.cross(xunit, yunit) # now get the z unit vector perpendicular to the look vector and the y unit vector
elif (roll_cntl == 'nadir'):
llh = self._geo.llh_from_xyz(target_location) # so zero roll occurs when the Instrument Z axis, is as parallel as possible to the local north
west, south, up = self._geo.xyz_west_south_up(llh=llh)
zunit = -south # We want to choose a z unit vector. Get local north at the target location
ythreshold = math.sin(math.radians(0.5))
xunit = lookvector # Get the look vector unit vector as the X direction
yunit = np.cross(zunit, xunit) # Get the y unit vector which points towards the left as seen from the oobserver.
ymag = np.linalg.norm(yunit) # Get the magnitude of the cross product. This becomes
if ymag < ythreshold: # if we are in an unstable region where the first choice of Z is too parallel to the look vector then use the second option (ie local Z axis and lookvector are with 0.57 degrees of each other)
zunit2 = -up # so arbitrarily use local north.
yunit = np.cross(zunit2, xunit) # Get the y unit vector which points towards the left as seen from the oobserver.
ymag = np.linalg.norm(yunit) # Get the magnitude of the cross product. This becomes
yunit /= ymag # get the magnitude of the Y unit vector
zunit = np.cross(xunit, yunit) # now get the z unit vector perpendicular to the look vector and the y unit vector
elif (roll_cntl == 'standard'):
llh = self._geo.llh_from_xyz(observer_location) # so zero roll occurs when the Instrument Z axis, is as parallel as possible to the local up unit vector
west, south, up = self._geo.xyz_west_south_up(llh=llh)
xunit = lookvector # get the instrument x unit vector
if (horizontalunitvector is not None): # If the caller has passed in the horizontal unit vector of the line of sight
zunit = up # then we can use it with good stability, so get the z unit vector
yunit = np.cross(zunit, horizontalunitvector) # the zero point for the instrument y unit vector is in the horizontal plane perpendicular to the look vector. This always works even when the look vector is straight up.
zunit = np.cross(xunit, yunit) # The zero point for the instrument z unit vector is the cross product of the x and y instrument unit vectors
else: # if the user cannot provide a horizontal bearing
zunit = up # then we try our best. We want to choose a z unit vector. Get local up at the tangent point
z = np.dot(lookvector, zunit) # Get the vertical component of the look vector
horiz = lookvector - z * zunit # And subtract it to get the horizontal component
if (math.fabs(z) > 0.9999619): # If we have almost no horizontal component( within 0.5 degrees of straight up/down)
horiz = -south # then arbitrarily use the north direction
horiz = horiz / np.linalg.norm(horiz) # get the horizontal unit vector parallel to the horizontal component of the look vector
yunit = np.cross(zunit, horiz) # now get the instrument y unit vector
zunit = np.cross(xunit, yunit) # and the instrument z unit vector.
else:
raise ValueError('Unsupported value {} of roll control'.format(roll_cntl))
if (roll_angle_degrees != 0.0): # Positive roll angle is clockwise as seen from theobserver looking along the line of sight
theta = math.radians(-roll_angle_degrees) # Apply the positive roll if the user has set it to non zero value
costheta = math.cos(theta)
sintheta = math.sin(theta)
zprime = zunit * costheta + yunit * sintheta
yprime = zunit * (-sintheta) + yunit * costheta
zunit = zprime
yunit = yprime
return UnitVectors(vectors=(xunit, yunit, zunit))
# -----------------------------------------------------------------------------
# OrientationTechniques
# -----------------------------------------------------------------------------
[docs]
class OrientationTechniques:
"""
A helper class that allows users to specify position and orientations using a set of position techniques
and orientation techniques.
"""
def __init__(self):
"""
Creates a new empty instance.
"""
self._position_convertors: Dict[str, Tuple[Callable[['Platform', np.datetime64, np.ndarray], bool], List[int]]] = {} # A dictionary of positioning tecniques. Each entry of the form [name, function]
self._platform_orientation_convertors: Dict[str, Tuple[Callable[[PointingAlgorithms, np.ndarray, str], bool], List[int]]] = {} # A dictionary of orientation techniques. Each entry of the form [name, function]
self._icf_look_vector_convertors: Dict[str, Tuple[Callable[['Platform', np.datetime64, np.ndarray], bool], List[int]]] = {} # A dictionary of ICF look vector techniques. Each entry of the form [name, function]
self._position_definitions: List[Tuple[str, Union[np.ndarray, int]]] = [] # A list of various representations of position specifications. Each entry of the form [technique, data_array]
self._platform_orientation_definitions: List[Tuple[str, Union[np.ndarray, int], str]] = [] # A list of various representations of platform orientation techniques and data . Each entry of the form [technique, data_array, roll_control]
self._icf_lookvector_definitions: List[Tuple[str, Union[np.ndarray, int], str]] = [] # A list of various representations of ICF look vector specifications.
self._utc: List[np.ndarray] = [] # A list of arrays of measurement times of numpy.datetime64['us']
self._isdirty = False
self.add_position_convertor('xyz', _technique_set_platform_position_from_xyz, [3])
self.add_position_convertor('llh', _technique_set_platform_position_from_llh, [3])
self.add_position_convertor('from_platform', _technique_set_platform_position_from_platform, [0])
self.add_position_convertor('looking_at_llh', _technique_set_platform_position_from_observer_looking_at_llh, [5])
self.add_platform_orientation_convertor('tangent_xyz_look', _technique_set_limb_look_vectors_from_unit_xyz, [3, 4]) # Set look vectors using ITRF/ecef xyz location vectors
self.add_platform_orientation_convertor('tangent_altitude', _technique_set_look_vectors_from_tangent_altitude, [2, 3]) # specifies
self.add_platform_orientation_convertor('tangent_from_orbitplane', _technique_set_boresight_look_at_location_orbitangle, [2, 3])
self.add_platform_orientation_convertor('location_xyz', _technique_set_boresight_look_at_location_xyz, [3, 4])
self.add_platform_orientation_convertor('location_llh', _technique_set_boresight_look_at_location_llh, [3, 4])
self.add_platform_orientation_convertor('unit_vectors', _technique_set_boresight_pointing_from_unitvectors, [6])
self.add_platform_orientation_convertor('azi_elev', _technique_set_observer_to_look_in_azi_elev, [2, 3])
self.add_platform_orientation_convertor('yaw_pitch_roll', _technique_set_observer_to_look_in_azi_elev, [2, 3])
self.add_platform_orientation_convertor('from_platform', _technique_set_platform_pointing_from_platform, [0])
self.add_icf_look_vector_convertor('azi_elev', _technique_set_icf_orientation_from_azi_elev, [2])
self.add_icf_look_vector_convertor('xyz', _technique_set_icf_look_from_xyz, [1, 3])
self.add_icf_look_vector_convertor('nop', _technique_set_icf_orientation_no_operation, [0])
# ------------------------------------------------------------------------------
# add_platform_orientation_convertor
# ------------------------------------------------------------------------------
def add_platform_orientation_convertor(self, orientationtype: str, convertor_function: Callable[[PointingAlgorithms, np.ndarray, str], bool], num_user_params: List[int]):
key = orientationtype.lower()
self._platform_orientation_convertors[key] = (convertor_function, num_user_params)
# ------------------------------------------------------------------------------
# add_position_convertor
# ------------------------------------------------------------------------------
def add_position_convertor(self, positiontype: str, convertor_function: Callable[['Platform', np.datetime64, np.ndarray], bool], num_user_params: List[int]):
key = positiontype.lower()
self._position_convertors[key] = (convertor_function, num_user_params)
# ------------------------------------------------------------------------------
# add_position_convertor
# ------------------------------------------------------------------------------
def add_icf_look_vector_convertor(self, lookvectorype: str, convertor_function: Callable[['Platform', np.datetime64, np.ndarray], bool], num_user_params: List[int]):
key = lookvectorype.lower()
self._icf_look_vector_convertors[key] = (convertor_function, num_user_params)
# -----------------------------------------------------------------------------
# numsamples
# -----------------------------------------------------------------------------
[docs]
def num_measurements(self) -> int:
"""
Returns the number of samples in this observation policy
Returns:
int
The number of samples in the observation policy.
"""
n = 0
for entry in self._utc:
n += entry.size
return n
# -----------------------------------------------------------------------------
# clear
# -----------------------------------------------------------------------------
[docs]
def clear(self):
"""
Clears the current list of platform states inside the observation policy. This method should be called
before starting a new observation policy
"""
self._position_definitions.clear()
self._platform_orientation_definitions.clear()
self._icf_lookvector_definitions.clear()
self._utc.clear()
# ------------------------------------------------------------------------------
# check_position_converter_key
# ------------------------------------------------------------------------------
def _check_position_converter_key(self, key: str) -> bool:
ok = self._position_convertors.get(key) is not None
if not ok:
raise ValueError('The position converter keyword <{:s}> is not in the position converter dictionary. You will need to manually add this function with method OrientationTechniques.add_position_convertor'.format(key))
return ok
# ------------------------------------------------------------------------------
# _check_platform_orientation_converter_key
# ------------------------------------------------------------------------------
def _check_platform_orientation_converter_key(self, key: str) -> bool:
ok = self._platform_orientation_convertors.get(key) is not None
if not ok:
raise ValueError('The platform orientation converter keyword <{:s}> is not in the platform orientation converter dictionary. You will need to manually add this function with method OrientationTechniques.add_platform_orientation_convertor'.format(key))
return ok
# ------------------------------------------------------------------------------
# _check_icf_look_vector_converter_key
# ------------------------------------------------------------------------------
def _check_icf_look_vector_converter_key(self, key: str) -> bool:
ok = self._icf_look_vector_convertors.get(key) is not None
if not ok:
raise ValueError('The ICF look vector converter keyword <{}> is not in the platform orientation converter dictionary. You will need to manually add this function with method OrientationTechniques.add_icf_look_vector_convertor'.format(key))
return ok
# ------------------------------------------------------------------------------
# _coerce_to_vector
# ------------------------------------------------------------------------------
def _coerce_to_vector(self, data: Union[np.ndarray, Sequence[float]], acceptable_n: List[int]) -> np.ndarray:
if isinstance(data, Sequence):
data = np.array(data, dtype=np.float64)
if type(data) is np.ndarray:
if data.dtype != np.float64:
data = np.array(data, dtype=np.float64)
s = data.shape
N = s[len(s) - 1]
ok = N in acceptable_n
if not ok:
raise ValueError('Incoming data must be an array or sequence with a trailing dimension being one of {}. Your array shape was {}'.format(acceptable_n, s))
answer = np.copy(np.reshape(data, [data.size // N, N]))
else:
answer = None
if (data is not None):
raise ValueError('Incoming data must be a numpy array, sequence or None')
return answer
# ------------------------------------------------------------------------------
# _decode_position_entry
# ------------------------------------------------------------------------------
def _decode_position_entry(self, observer_positions):
if isinstance(observer_positions, str): # if we have just a string
key = observer_positions # then it is the technique key. It should be 'from_platform'
data = None # and there is no position data.
else: # otherwise it must be a sequence
key = observer_positions[0] # where the technique key is the first element
data = observer_positions[1] if len(observer_positions) > 1 else None # and the data sequence is the second element, or None if there is no second element
key = key.lower() # Convert the technique key to lower case
if (key == 'from_platform'):
data = None
self._check_position_converter_key(key) # check that the technique key is supported
num_params = self._position_convertors[key][1]
positiondata = self._coerce_to_vector(data, num_params) # coerce the vector data into array[N,3]
numpositions = positiondata.shape[0] if (positiondata is not None) else 0
return numpositions, positiondata, key
# ------------------------------------------------------------------------------
# _decode_platform_orientation_entry
# ------------------------------------------------------------------------------
def _decode_platform_orientation_entry(self, platform_orientation_data):
roll_control = 'undefined' # default value of roll if no value passed in
data = None # default value of data is no data are passed in
if type(platform_orientation_data) is str:
key = platform_orientation_data
else: # otherwise a dictionary has been passed
key = platform_orientation_data[0] # get the technique key from the first element
if (len(platform_orientation_data) > 1):
roll_control = platform_orientation_data[1]
data = platform_orientation_data[2]
else:
data = None # get the technique parameter data from the second element. None if there is no second element.
roll_control = "undefined"
key = key.lower() # convert the technique key to lower case
if (key == 'from_platform'):
data = None
roll_control = 'undefined'
self._check_platform_orientation_converter_key(key) # check that the look-vector technique is supported
num_params = self._platform_orientation_convertors[key][1] # get the number of parameters expected from this look vector technique
lookdata = self._coerce_to_vector(data, num_params) # coerce the technique parameter data into a 2d-array of [num_params,N]
numlooks = lookdata.shape[0] if (lookdata is not None) else 0
return numlooks, lookdata, key, roll_control
# ------------------------------------------------------------------------------
# _decode_instrument_internal_rotation
# ------------------------------------------------------------------------------
def _decode_icf_look_vector_entry(self, icf_look_vectors):
data = None # default value of data is no data are passed in
if type(icf_look_vectors) is str:
key = icf_look_vectors
else: # otherwise a dictionary has been passed
if (icf_look_vectors is not None):
key = icf_look_vectors[0] # get the technique key from the first element
if (len(icf_look_vectors) > 1):
data = icf_look_vectors[1]
else:
data = None # get the technique parameter data from the second element. None if there is no second element.
key = 'nop'
self._check_icf_look_vector_converter_key(key) # check that the look-vector technique is supported
num_params = self._icf_look_vector_convertors[key][1] # get the number of parameters expected from this look vector technique
lookdata = self._coerce_to_vector(data, num_params) # coerce the technique parameter data into a 2d-array of [num_params,N]
numlooks = lookdata.shape[0] if (lookdata is not None) else 1
return numlooks, lookdata, key
# ------------------------------------------------------------------------------
# _decode_instrument_internal_rotation
# ------------------------------------------------------------------------------
def _decode_instrument_internal_rotation(self, instrument_internal_rotation):
turntabledata = self._coerce_to_vector(instrument_internal_rotation, [2, 3]) # coerce the technique parameter data into a 2d-array of [num_params,N]
if (turntabledata is None): # If no turntable data are passed in
turntabledata = np.zeros([1, 3]) # then create a 1 measurement array full of zeros
numturntable = turntabledata.shape[0] # get the number of measurements from the first dimension
numv = turntabledata.shape[1] # get the number of parameters
if (numv != 3): # if its not 3
values = np.zeros([numturntable, 3]) # then resize the array to 3 with the last element equal to zero
values[:, 0] = turntabledata[:, 0] # copy over the first set of parameters
values[:, 1] = turntabledata[:, 1] # and the second set
turntabledata = values # and assign to the turntabledata
return numturntable, turntabledata # and we are done.
# ------------------------------------------------------------------------------
# add_measurement_set
# ------------------------------------------------------------------------------
[docs]
def add_measurement_set(self,
utc: Union[float, str, np.datetime64, datetime, np.ndarray, Sequence[Union[float, str, np.datetime64, datetime, np.ndarray]]],
platform_position: Tuple[str, Union[Sequence, np.ndarray]],
platform_orientation: Tuple[str, str, Union[Sequence, np.ndarray]],
icf_orientation: Tuple[str, str, Union[Sequence, np.ndarray]] = None
):
"""
Adds a set of measurements to the parent :class:~.Instrument`. The call adds **N** distinct measurements with
**N* distinct settings of *time*, *position* and *orientation*. For convenience, settings for one variable can be represented
with one value and it will be automatically expanded to the number of measurements represented in the other variables, i.e.
borrowing the idea of **broadcasting** from *numpy*. Multiple measurement sets can be added to the :class:`~.Instrument`
Parameters:
utc: Array, sequence or scalar of string, float, datetime or datetime64
The universal times of the set of measurements. It can be an array, sequence or scalar and each element
should be a representation of universal time. If it is a scalar value then the same time value is assigned to every
position given in `observer_positions`. All ali_elements of the array should be of the same type and should represent
universal time.
roll_control: str
The technique used to specify the location of zero roll in the :ref:`ecef` system. A description of values can
be found in :ref:`rollcontrol`.
platform_position: Tuple[str, sequence]
A two element tuple. The first element of the tuple is a string that specifies the positioning technique. The second element
of the tuple is a sequence or array (or None) that contains parameter data for the selected technique. The array should be of size (Npts, NParam)
where *Npts* is the number of points and *Nparam* is the number of parameters for the technique, e.g. (N,3), in the measurement set. The
second element may be None if the technique is `from_platform`.
platform_orientation:
A three element tuple. The first element of the tuple is a string that specifies the orientation technique. The second element
of the tuple is a sequence or array (or None) that contains parameter data for the selected technique. The array should be of size (Npts, NParam)
where *Npts* is the number of points and *Nparam* is the number of parameters for the technique, e.g. (N,3), in the measurement set. The
second element may be None if the technique is `from_platform`.
icf_orientation:
Optional. A two element tuuple. This option allows used to specify an array of instantaneous look vectors in the :ref:`icf`.
The first element of the tuple is a string that specifies the ref:`icf` look vector technique. The second element
of the tuple is a sequence or array (or None) that contains parameter data for the selected technique. The array should be of size (Numlookvector, NParam)
where *Numlookvector* is the number of instantaneous look vectors points and *Nparam* is the number of parameters for the look vector technique, e.g. (N,3),
"""
utdata = ut_to_datetime64(utc) # Convert the incoming array of times to datetime64 for numpy
if isinstance(utdata, np.datetime64): # If a scalar value comes back then we resize the times to math the positions
utdata = np.array([utdata]) # make it into an array
numtimes = utdata.size # Get the number of time
numpositions, positiondata, positionkey = self._decode_position_entry(platform_position) # Get the number of positions, the key
numorientations, orientationdata, orientkey, roll_control = self._decode_platform_orientation_entry(platform_orientation)
numicflook, icflookdata, icfkey = self._decode_icf_look_vector_entry(icf_orientation)
maxpts = np.max((numtimes, numpositions, numorientations, numicflook))
if (numtimes == 1):
utdata = np.resize(utdata, [maxpts]) # then resize the times array by simply resizing the current array
numtimes = maxpts # and reset the number of times
else:
if (numtimes != maxpts):
raise ValueError('The number of time parameters given {} does not equal 1 or the number of looks {} and and orientations given {}'.format(numtimes, numpositions, numorientations))
if (positionkey == 'from_platform'): # if the technique parameter array returns as None
positiondata = maxpts # set the technique parameter data as the number of times, used when passed to the technique algorithm
numpositions = maxpts # and add the number of times to the number of positions
elif (numpositions == 1): # otherwise we have valied technique parameter data
positiondata = np.tile(positiondata, [maxpts, 1])
numpositions = maxpts # so get the number of points as the first element of the shape
else:
if (numpositions != maxpts):
raise ValueError('The number of position parameters given {} does not equal 1 or the number of times {} and orientations given {}'.format(numpositions, numtimes, numorientations))
if (orientkey == 'from_platform'): # if the technique parameter array returns as None
orientationdata = maxpts # set the technique parameter data as the number of times, used when passed to the technique algorithm
elif (numorientations == 1): # otherwise we have valied technique parameter data
orientationdata = np.tile(orientationdata, [maxpts, 1])
else:
if (numorientations != maxpts):
raise ValueError('The number of orientation parameters given {} does not equal 1 or the number of times {} and positions {} given'.format(numorientations, numtimes, numpositions))
if (numicflook == 1) and (icflookdata is not None):
icflookdata = np.tile(icflookdata, [maxpts, 1])
self._utc.append(utdata) # Add the UTC times now that they are finalized in size
self._position_definitions.append((positionkey, positiondata)) # if it is then append the definitions to the internal measurement set within this object
self._platform_orientation_definitions.append((orientkey, orientationdata, roll_control)) # append these look vectors definitions to the internal measurement set within thgis object.
self._icf_lookvector_definitions.append((icfkey, icflookdata))
self._isdirty = True
# ------------------------------------------------------------------------------
# make_observation_set
# ------------------------------------------------------------------------------
[docs]
def make_observation_set(self, platform: 'Platform'):
"""
Converts the internal list of measurements from previous calls to :meth:`~.add_measurement_set` into an
an internally cached :class:`~.PositionAndOrientationArray`. This method is normally called by the :class:`~.Platform` class.
The internal list of measurement sets has only been cached up to this point. This code goes through the measurements
and generates all the necessary platform positions and rotation matrices using all the specified techniques.
Parameters:
platform : Platform
The :class:`~.Platform` object that will be updated with the measurements defined in this object.
"""
pointingalgorithms = PointingAlgorithms(platform)
postype = '----'
orientationtype = '----'
ilookseg = -1
numlook = -1
ilook = 0
iposseg = -1
numpos = -1
ipos = 0
iutc = 0
iturn = 0
numturn = -1
iturnseg = -1
allok = True
for iseg in range(len(self._utc)): # Loop through all of the time segements. There is usually just one
currentutc = self._utc[iseg] # Get the current array of universal times
numutc = currentutc.size # Get the number of universal times in this segment
for it in range(numutc): # Loop over all of the time values, we have already validated that the total number of positions and look arrays are the same size as the time array
iutc += 1 # increase the time index to the next time measurement in the set of segments
utc = currentutc[it] # get the time. This should be np.datetime64
iturn += 1 # Step to the next position entry in the current segment of internal turntable orientation entries
if (iturn >= numturn): # if we have exceeded the bounds of the current segment
iturnseg += 1 # then step to the next segment (it whould work as we have already validated data sets)
icflooktype, currentturn = self._icf_lookvector_definitions[ilookseg] # get the techniquename to calculate ICF look vectors and the parameter data for this setting
icflookfunc = self._icf_look_vector_convertors[icflooktype][0] # get the ICF look vector technique function
iturn = 0 # Reset our position in the current segment
assert ((currentturn is None) or (type(currentturn) is np.ndarray)) # We must have an array. There is no "from_platform" option
numturn = currentturn.shape[0] if currentturn is not None else 1 # then get the number of positions in this segment
thisturn = currentturn[iturn, :] if currentturn is not None else None # get the technique parameter data for this measurement point.
ok3 = icflookfunc(platform, thisturn) # Call the positioning technique function. Its sets the Platform position State. It returns false if it fails.
ipos += 1 # Step to the next position entry in the current segment of position entries
if (ipos >= numpos): # if we have exceeded the bounds of the current segment
iposseg += 1 # then step to the next segment (it whould work as we have already validated data sets)
postype, currentpos = self._position_definitions[iposseg] # get the name of the position technique and the current position technique data for this segment
posfunc = self._position_convertors[postype][0] # Get the function we should call fo rthis technique
ipos = 0 # Reset our position in the current segment
if type(currentpos) is np.ndarray: # If the position technique parameter is an array
numpos = currentpos.shape[0] # then get the number of positions ion this segment
else: # otherwise its the special case, e.g. 'from_platform'
numpos = currentpos # and the number of points is internally stored as the technique parameter data
currentpos = None # and the real parameter data is None
thispos = currentpos[ipos, :] if currentpos is not None else None # get the technique parameter data for this measurement point.
ok1 = posfunc(platform, utc, thispos) # Call the positioning technique function. Its sets the Platform position State. It returns false if it fails.
ilook += 1 # Step to the next position entry in the current list of orientation technique entries
if (ilook >= numlook): # if we have exceeded the bounds of the current segment
ilookseg += 1 # then step to the next segment. It should always work
orientationtype, currentlook, roll_control = self._platform_orientation_definitions[ilookseg] # get the technique, parameter data and roll control for this settings
lookfunc = self._platform_orientation_convertors[orientationtype][0] # get the look technique function
ilook = 0
if type(currentlook) is np.ndarray:
numlook = currentlook.shape[0]
else:
numlook = currentlook
currentlook = None
thislook = currentlook[ilook, :] if currentlook is not None else None
ok2 = lookfunc(pointingalgorithms, thislook, roll_control)
if (ok1 and ok2 and ok3):
platform.add_current_platform_state()
else:
logging.warning('Rejecting point {} from the observation policy. Positioning technique={} status = {}, Pointing Technique={} status = {}, Internal Rotation status = {}'.format(iutc, postype, ok1, orientationtype, ok2, ok3))
allok = False
if not allok:
raise RuntimeError("Platform.make_observation_set failed to create a valid observation set. Warning messages have been written to logging.warning")
self._isdirty = False