Example #1
0
class YDirectionType(Serializable):
    """The Y direction of the collect"""
    _fields = ('UVectECF', 'SampleSpacing', 'NumSamples', 'FirstSample')
    _required = _fields
    _numeric_format = {
        'SampleSpacing': FLOAT_FORMAT,
    }
    # descriptors
    UVectECF = UnitVectorDescriptor(
        'UVectECF',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring='The unit vector in the Y direction.')  # type: XYZType
    SampleSpacing = FloatDescriptor(
        'SampleSpacing',
        _required,
        strict=DEFAULT_STRICT,
        docstring='The collection sample spacing in the Y direction in meters.'
    )  # type: float
    NumSamples = IntegerDescriptor(
        'NumSamples',
        _required,
        strict=DEFAULT_STRICT,
        docstring='The number of samples in the Y direction.')  # type: int
    FirstSample = IntegerDescriptor(
        'FirstSample',
        _required,
        strict=DEFAULT_STRICT,
        docstring='The first sample index.')  # type: int

    def __init__(self,
                 UVectECF=None,
                 SampleSpacing=None,
                 NumSamples=None,
                 FirstSample=None,
                 **kwargs):
        """

        Parameters
        ----------
        UVectECF : XYZType|numpy.ndarray|list|tuple
        SampleSpacing : float
        NumSamples : int
        FirstSample : int
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.UVectECF = UVectECF
        self.SampleSpacing = SampleSpacing
        self.NumSamples = NumSamples
        self.FirstSample = FirstSample
        super(YDirectionType, self).__init__(**kwargs)
Example #2
0
class ProductPlaneType(Serializable):
    """
    Plane definition for the product.
    """

    _fields = ('RowUnitVector', 'ColUnitVector')
    _required = _fields
    # Descriptor
    RowUnitVector = UnitVectorDescriptor(
        'RowUnitVector',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Unit vector of the plane defined to be aligned in the increasing row direction '
        'of the product. (Defined as Rpgd in Design and Exploitation document)'
    )  # type: XYZType
    ColUnitVector = UnitVectorDescriptor(
        'ColUnitVector',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Unit vector of the plane defined to be aligned in the increasing column direction '
        'of the product. (Defined as Cpgd in Design and Exploitation document)'
    )  # type: XYZType

    def __init__(self, RowUnitVector=None, ColUnitVector=None, **kwargs):
        """

        Parameters
        ----------
        RowUnitVector : XYZType|numpy.ndarray|list|tuple
        ColUnitVector : XYZType|numpy.ndarray|list|tuple
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.RowUnitVector = RowUnitVector
        self.ColUnitVector = ColUnitVector
        super(ProductPlaneType, self).__init__(**kwargs)
Example #3
0
class ECFPlanarType(Serializable):
    """
    Parameters for a planar surface defined in ECF coordinates. The reference
    surface is a plane that contains the IARP.
    """

    _fields = ('uIAX', 'uIAY')
    _required = _fields
    # descriptors
    uIAX = UnitVectorDescriptor(
        'uIAX',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Image Area X-coordinate (IAX) unit vector in ECF coordinates. '
        'For stripmap collections, uIAX ALWAYS points in the direction '
        'of the scanning footprint.')  # type: XYZType
    uIAY = UnitVectorDescriptor(
        'uIAY',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring='Image Area Y-coordinate (IAY) unit vector in ECF '
        'coordinates. This should be perpendicular to '
        'uIAX.')  # type: XYZType

    def __init__(self, uIAX=None, uIAY=None, **kwargs):
        """

        Parameters
        ----------
        uIAX : XYZType|numpy.ndarray|list|tuple
        uIAY : XYZType|numpy.ndarray|list|tuple
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.uIAX = uIAX
        self.uIAY = uIAY
        super(ECFPlanarType, self).__init__(**kwargs)
Example #4
0
class XDirectionType(Serializable):
    """The X direction of the collect"""
    _fields = ('UVectECF', 'LineSpacing', 'NumLines', 'FirstLine')
    _required = _fields
    _numeric_format = {'LineSpacing': '0.16G', }
    # descriptors
    UVectECF = UnitVectorDescriptor(
        'UVectECF', XYZType, _required, strict=DEFAULT_STRICT,
        docstring='The unit vector in the X direction.')  # type: XYZType
    LineSpacing = FloatDescriptor(
        'LineSpacing', _required, strict=DEFAULT_STRICT,
        docstring='The collection line spacing in the X direction in meters.')  # type: float
    NumLines = IntegerDescriptor(
        'NumLines', _required, strict=DEFAULT_STRICT,
        docstring='The number of lines in the X direction.')  # type: int
    FirstLine = IntegerDescriptor(
        'FirstLine', _required, strict=DEFAULT_STRICT,
        docstring='The first line index.')  # type: int

    def __init__(self, UVectECF=None, LineSpacing=None, NumLines=None, FirstLine=None, **kwargs):
        """

        Parameters
        ----------
        UVectECF : XYZType|numpy.ndarray|list|tuple
        LineSpacing : float
        NumLines : int
        FirstLine : int
        kwargs : dict
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.UVectECF = UVectECF
        self.LineSpacing = LineSpacing
        self.NumLines = NumLines
        self.FirstLine = FirstLine
        super(XDirectionType, self).__init__(**kwargs)
Example #5
0
class DirParamType(Serializable):
    """The direction parameters container"""
    _fields = ('UVectECF', 'SS', 'ImpRespWid', 'Sgn', 'ImpRespBW', 'KCtr',
               'DeltaK1', 'DeltaK2', 'DeltaKCOAPoly', 'WgtType', 'WgtFunct')
    _required = ('UVectECF', 'SS', 'ImpRespWid', 'Sgn', 'ImpRespBW', 'KCtr',
                 'DeltaK1', 'DeltaK2')
    _numeric_format = {
        'SS': FLOAT_FORMAT,
        'ImpRespWid': FLOAT_FORMAT,
        'Sgn': '+d',
        'ImpRespBW': FLOAT_FORMAT,
        'KCtr': FLOAT_FORMAT,
        'DeltaK1': FLOAT_FORMAT,
        'DeltaK2': FLOAT_FORMAT,
        'WgtFunct': FLOAT_FORMAT
    }
    _collections_tags = {'WgtFunct': {'array': True, 'child_tag': 'Wgt'}}
    # descriptors
    UVectECF = UnitVectorDescriptor(
        'UVectECF',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Unit vector in the increasing ``(row/col)`` direction *(ECF)* at '
        'the SCP pixel.')  # type: XYZType
    SS = FloatDescriptor(
        'SS',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Sample spacing in the increasing ``(row/col)`` direction. Precise spacing '
        'at the SCP.')  # type: float
    ImpRespWid = FloatDescriptor(
        'ImpRespWid',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Half power impulse response width in the increasing ``(row/col)`` direction. '
        'Measured at the scene center point.')  # type: float
    Sgn = IntegerEnumDescriptor(
        'Sgn', (1, -1),
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Sign for exponent in the DFT to transform the ``(row/col)`` dimension to '
        'spatial frequency dimension.')  # type: int
    ImpRespBW = FloatDescriptor(
        'ImpRespBW',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Spatial bandwidth in ``(row/col)`` used to form the impulse response in '
        'the ``(row/col)`` direction. Measured at the center of '
        'support for the SCP.')  # type: float
    KCtr = FloatDescriptor(
        'KCtr',
        _required,
        strict=DEFAULT_STRICT,
        docstring='Center spatial frequency in the given dimension. '
        'Corresponds to the zero frequency of the DFT in the given ``(row/col)`` '
        'direction.')  # type: float
    DeltaK1 = FloatDescriptor(
        'DeltaK1',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Minimum ``(row/col)`` offset from KCtr of the spatial frequency support '
        'for the image.')  # type: float
    DeltaK2 = FloatDescriptor(
        'DeltaK2',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Maximum ``(row/col)`` offset from KCtr of the spatial frequency '
        'support for the image.')  # type: float
    DeltaKCOAPoly = SerializableDescriptor(
        'DeltaKCOAPoly',
        Poly2DType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Offset from KCtr of the center of support in the given ``(row/col)`` spatial frequency. '
        'The polynomial is a function of image given ``(row/col)`` coordinate ``(variable 1)`` and '
        'column coordinate ``(variable 2)``.')  # type: Poly2DType
    WgtType = SerializableDescriptor(
        'WgtType',
        WgtTypeType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Parameters describing aperture weighting type applied in the spatial frequency domain '
        'to yield the impulse response in the given ``(row/col)`` direction.'
    )  # type: WgtTypeType
    WgtFunct = FloatArrayDescriptor(
        'WgtFunct',
        _collections_tags,
        _required,
        strict=DEFAULT_STRICT,
        minimum_length=2,
        docstring=
        'Sampled aperture amplitude weighting function (array) applied to form the SCP impulse '
        'response in the given ``(row/col)`` direction.'
    )  # type: numpy.ndarray

    def __init__(self,
                 UVectECF=None,
                 SS=None,
                 ImpRespWid=None,
                 Sgn=None,
                 ImpRespBW=None,
                 KCtr=None,
                 DeltaK1=None,
                 DeltaK2=None,
                 DeltaKCOAPoly=None,
                 WgtType=None,
                 WgtFunct=None,
                 **kwargs):
        """

        Parameters
        ----------
        UVectECF : XYZType|numpy.ndarray|list|tuple
        SS : float
        ImpRespWid : float
        Sgn : int
        ImpRespBW : float
        KCtr : float
        DeltaK1 : float
        DeltaK2 : float
        DeltaKCOAPoly : Poly2DType|numpy.ndarray|list|tuple
        WgtType : WgtTypeType
        WgtFunct : None|numpy.ndarray|list|tuple
        kwargs : dict
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.UVectECF = UVectECF
        self.SS = SS
        self.ImpRespWid, self.ImpRespBW = ImpRespWid, ImpRespBW
        self.Sgn = Sgn
        self.KCtr, self.DeltaK1, self.DeltaK2 = KCtr, DeltaK1, DeltaK2
        self.DeltaKCOAPoly = DeltaKCOAPoly
        self.WgtType = WgtType
        self.WgtFunct = WgtFunct
        super(DirParamType, self).__init__(**kwargs)

    def define_weight_function(self,
                               weight_size=DEFAULT_WEIGHT_SIZE,
                               populate=False):
        """
        Try to derive WgtFunct from WgtType, if necessary. This should likely be called from the `GridType` parent.

        Parameters
        ----------
        weight_size : int
            the size of the `WgtFunct` to generate.
        populate : bool
            Overwrite any populated WgtFunct value?

        Returns
        -------
        None|numpy.ndarray
        """

        if self.WgtType is None or self.WgtType.WindowName is None:
            return  # nothing to be done

        value = None
        window_name = self.WgtType.WindowName.upper()
        if window_name == 'HAMMING':
            # A Hamming window is defined in many places as a raised cosine of weight .54, so this is the default.
            # Some data use a generalized raised cosine and call it HAMMING, so we allow for both uses.
            try:
                # noinspection PyTypeChecker
                coef = float(self.WgtType.get_parameter_value(
                    None, 0.54))  # just get first parameter - name?
            except ValueError:
                coef = 0.54
            value = general_hamming(weight_size, coef, sym=True)
        elif window_name == 'HANNING':
            value = general_hamming(weight_size, 0.5, sym=True)
        elif window_name == 'KAISER':
            beta = 14.0  # suggested default in literature/documentation
            try:
                # noinspection PyTypeChecker
                beta = float(self.WgtType.get_parameter_value(
                    None, beta))  # just get first parameter - name?
            except ValueError:
                pass
            value = kaiser(weight_size, beta, sym=True)
        elif window_name == 'TAYLOR':
            # noinspection PyTypeChecker
            sidelobes = int(self.WgtType.get_parameter_value(
                'NBAR', 4))  # apparently the matlab argument name
            # noinspection PyTypeChecker
            max_sidelobe_level = float(
                self.WgtType.get_parameter_value('SLL', -30))  # same
            value = taylor(weight_size,
                           nbar=sidelobes,
                           sll=max_sidelobe_level,
                           norm=True,
                           sym=True)
        elif window_name == 'UNIFORM':
            value = numpy.ones((32, ), dtype='float64')

        if self.WgtFunct is None or (populate and value is not None):
            self.WgtFunct = value
        return value

    def get_oversample_rate(self):
        """
        Gets the oversample rate. *Added in version 1.2.35.*

        Returns
        -------
        float
        """

        if self.SS is None or self.ImpRespBW is None:
            raise AttributeError('Both SS and ImpRespBW must be populated.')

        return max(1., 1. / (self.SS * self.ImpRespBW))

    def _get_broadening_factor(self):
        """
        Gets the *broadening factor*, assuming that `WgtFunct` has been properly populated.

        Returns
        -------
        float
            the broadening factor
        """

        if self.WgtType is not None and self.WgtType.WindowName is not None:
            window_name = self.WgtType.WindowName.upper()
            coef = None
            if window_name == 'UNIFORM':
                coef = 1.0
            elif window_name == 'HAMMING':
                try:
                    # noinspection PyTypeChecker
                    coef = float(self.WgtType.get_parameter_value(
                        None, 0.54))  # just get first parameter - name?
                except ValueError:
                    coef = 0.54
            elif window_name == 'HANNING':
                coef = 0.5

            if coef is not None:
                return get_hamming_broadening_factor(coef)

        return find_half_power(self.WgtFunct, oversample=1024)

    def define_response_widths(self, populate=False):
        """
        Assuming that `WgtFunct` has been properly populated, define the response widths.
        This should likely be called by `GridType` parent.

        Parameters
        ----------
        populate : bool
            Overwrite populated ImpRespWid and/or ImpRespBW?

        Returns
        -------
        None|(float, float)
            None or (ImpRespBw, ImpRespWid)
        """

        broadening_factor = self._get_broadening_factor()
        if broadening_factor is None:
            return None

        if self.ImpRespBW is not None:
            resp_width = broadening_factor / self.ImpRespBW
            if populate or self.ImpRespWid is None:
                self.ImpRespWid = resp_width
            return self.ImpRespBW, resp_width
        elif self.ImpRespWid is not None:
            resp_bw = broadening_factor / self.ImpRespWid
            if populate or self.ImpRespBW is None:
                self.ImpRespBW = resp_bw
            return resp_bw, self.ImpRespWid
        return None

    def estimate_deltak(self, x_coords, y_coords, populate=False):
        """
        The `DeltaK1` and `DeltaK2` parameters can be estimated from `DeltaKCOAPoly`, if necessary.
        This should likely be called by the `GridType` parent.

        Parameters
        ----------
        x_coords : None|numpy.ndarray
            The physical vertex coordinates to evaluate DeltaKCOAPoly
        y_coords : None|numpy.ndarray
            The physical vertex coordinates to evaluate DeltaKCOAPoly
        populate : bool
            Overwite any populated DeltaK1 and DeltaK2?

        Returns
        -------
        (float, float)
        """

        if self.ImpRespBW is None or self.SS is None:
            return  # nothing can be done

        if self.DeltaKCOAPoly is not None and x_coords is not None:
            deltaks = self.DeltaKCOAPoly(x_coords, y_coords)
            min_deltak = numpy.amin(deltaks) - 0.5 * self.ImpRespBW
            max_deltak = numpy.amax(deltaks) + 0.5 * self.ImpRespBW
        else:
            min_deltak = -0.5 * self.ImpRespBW
            max_deltak = 0.5 * self.ImpRespBW

        if (min_deltak < -0.5 / abs(self.SS)) or (max_deltak >
                                                  0.5 / abs(self.SS)):
            min_deltak = -0.5 / abs(self.SS)
            max_deltak = -min_deltak

        if populate or (self.DeltaK1 is None or self.DeltaK2 is None):
            self.DeltaK1 = min_deltak
            self.DeltaK2 = max_deltak
        return min_deltak, max_deltak

    def check_deltak(self, x_coords, y_coords):
        """
        Checks the DeltaK values for validity.

        Parameters
        ----------
        x_coords : None|numpy.ndarray
            The physical vertex coordinates to evaluate DeltaKCOAPoly
        y_coords : None|numpy.ndarray
            The physical vertex coordinates to evaluate DeltaKCOAPoly

        Returns
        -------
        bool
        """

        out = True
        try:
            if self.DeltaK2 <= self.DeltaK1 + 1e-10:
                self.log_validity_error(
                    'DeltaK2 ({}) must be greater than DeltaK1 ({})'.format(
                        self.DeltaK2, self.DeltaK1))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass

        try:
            if self.DeltaK2 > 1. / (2 * self.SS) + 1e-10:
                self.log_validity_error(
                    'DeltaK2 ({}) must be <= 1/(2*SS) ({})'.format(
                        self.DeltaK2, 1. / (2 * self.SS)))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass

        try:
            if self.DeltaK1 < -1. / (2 * self.SS) - 1e-10:
                self.log_validity_error(
                    'DeltaK1 ({}) must be >= -1/(2*SS) ({})'.format(
                        self.DeltaK1, -1. / (2 * self.SS)))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass

        min_deltak, max_deltak = self.estimate_deltak(x_coords,
                                                      y_coords,
                                                      populate=False)
        try:
            if abs(self.DeltaK1 / min_deltak - 1) > 1e-2:
                self.log_validity_error(
                    'The DeltaK1 value is populated as {}, but estimated to be {}'
                    .format(self.DeltaK1, min_deltak))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass

        try:
            if abs(self.DeltaK2 / max_deltak - 1) > 1e-2:
                self.log_validity_error(
                    'The DeltaK2 value is populated as {}, but estimated to be {}'
                    .format(self.DeltaK2, max_deltak))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass
        return out

    def _check_bw(self):
        out = True
        try:
            if self.ImpRespBW > (self.DeltaK2 - self.DeltaK1) + 1e-10:
                self.log_validity_error(
                    'ImpRespBW ({}) must be <= DeltaK2 - DeltaK1 '
                    '({})'.format(self.ImpRespBW, self.DeltaK2 - self.DeltaK1))
                out = False
        except (AttributeError, TypeError, ValueError):
            pass
        return out

    def _check_wgt(self):
        cond = True
        if self.WgtType is None:
            return cond

        wgt_size = self.WgtFunct.size if self.WgtFunct is not None else None
        if self.WgtType.WindowName not in [
                'UNIFORM', 'UNKNOWN'
        ] and (wgt_size is None or wgt_size < 2):
            self.log_validity_error(
                'Non-uniform weighting indicated, but WgtFunct not properly defined'
            )
            return False

        if wgt_size is not None and wgt_size > 1024:
            self.log_validity_warning(
                'WgtFunct with {} elements is provided.\n'
                'The recommended number of elements is 512, '
                'and many more is likely needlessly excessive.'.format(
                    wgt_size))

        result = self.define_response_widths(populate=False)
        if result is None:
            return cond
        resp_bw, resp_wid = result
        if abs(resp_bw / self.ImpRespBW - 1) > 1e-2:
            self.log_validity_error(
                'ImpRespBW expected as {} from weighting,\n'
                'but populated as {}'.format(resp_bw, self.ImpRespBW))
            cond = False
        if abs(resp_wid / self.ImpRespWid - 1) > 1e-2:
            self.log_validity_error(
                'ImpRespWid expected as {} from weighting,\n'
                'but populated as {}'.format(resp_wid, self.ImpRespWid))
            cond = False
        return cond

    def _basic_validity_check(self):
        condition = super(DirParamType, self)._basic_validity_check()
        if (self.WgtFunct is not None) and (self.WgtFunct.size < 2):
            self.log_validity_error(
                'The WgtFunct array has been defined in DirParamType, '
                'but there are fewer than 2 entries.')
            condition = False
        for attribute in ['SS', 'ImpRespBW', 'ImpRespWid']:
            value = getattr(self, attribute)
            if value is not None and value <= 0:
                self.log_validity_error(
                    'attribute {} is populated as {}, '
                    'but should be strictly positive.'.format(
                        attribute, value))
                condition = False
        condition &= self._check_bw()
        condition &= self._check_wgt()
        return condition
Example #6
0
class PFAType(Serializable):
    """Parameters for the Polar Formation Algorithm."""
    _fields = ('FPN', 'IPN', 'PolarAngRefTime', 'PolarAngPoly',
               'SpatialFreqSFPoly', 'Krg1', 'Krg2', 'Kaz1', 'Kaz2', 'STDeskew')
    _required = ('FPN', 'IPN', 'PolarAngRefTime', 'PolarAngPoly',
                 'SpatialFreqSFPoly', 'Krg1', 'Krg2', 'Kaz1', 'Kaz2')
    _numeric_format = {
        'PolarAngRefTime': FLOAT_FORMAT,
        'Krg1': FLOAT_FORMAT,
        'Krg2': FLOAT_FORMAT,
        'Kaz1': FLOAT_FORMAT,
        'Kaz2': FLOAT_FORMAT
    }
    # descriptors
    FPN = UnitVectorDescriptor(
        'FPN',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Focus Plane unit normal in ECF coordinates. Unit vector FPN points away from the center of '
        'the Earth.')  # type: XYZType
    IPN = UnitVectorDescriptor(
        'IPN',
        XYZType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Image Formation Plane unit normal in ECF coordinates. Unit vector IPN points away from the '
        'center of the Earth.')  # type: XYZType
    PolarAngRefTime = FloatDescriptor(
        'PolarAngRefTime',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Polar image formation reference time *(in seconds)*. Polar Angle = 0 at the reference time. '
        'Measured relative to collection start. *Note: Reference time is typically set equal to the SCP '
        'COA time but may be different.*')  # type: float
    PolarAngPoly = SerializableDescriptor(
        'PolarAngPoly',
        Poly1DType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Polynomial function that yields Polar Angle *(in radians)* as function of time '
        'relative to Collection Start.')  # type: Poly1DType
    SpatialFreqSFPoly = SerializableDescriptor(
        'SpatialFreqSFPoly',
        Poly1DType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Polynomial that yields the *Spatial Frequency Scale Factor (KSF)* as a function of Polar '
        r'Angle. That is, :math:`Polar Angle[radians] \to KSF[dimensionless]`. Used to scale RF '
        'frequency *(fx, Hz)* to aperture spatial frequency *(Kap, cycles/m)*. Where,'
        r':math:`Kap = fx\cdot (2/c)\cdot KSF`, and `Kap` is the effective spatial '
        'frequency in the polar aperture.')  # type: Poly1DType
    Krg1 = FloatDescriptor(
        'Krg1',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Minimum *range spatial frequency (Krg)* output from the polar to rectangular '
        'resampling.')  # type: float
    Krg2 = FloatDescriptor(
        'Krg2',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Maximum *range spatial frequency (Krg)* output from the polar to rectangular '
        'resampling.')  # type: float
    Kaz1 = FloatDescriptor(
        'Kaz1',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Minimum *azimuth spatial frequency (Kaz)* output from the polar to rectangular '
        'resampling.')  # type: float
    Kaz2 = FloatDescriptor(
        'Kaz2',
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Maximum *azimuth spatial frequency (Kaz)* output from the polar to rectangular '
        'resampling.')  # type: float
    STDeskew = SerializableDescriptor(
        'STDeskew',
        STDeskewType,
        _required,
        strict=DEFAULT_STRICT,
        docstring=
        'Parameters to describe image domain slow time *(ST)* Deskew processing.'
    )  # type: STDeskewType

    def __init__(self,
                 FPN=None,
                 IPN=None,
                 PolarAngRefTime=None,
                 PolarAngPoly=None,
                 SpatialFreqSFPoly=None,
                 Krg1=None,
                 Krg2=None,
                 Kaz1=None,
                 Kaz2=None,
                 STDeskew=None,
                 **kwargs):
        """

        Parameters
        ----------
        FPN : XYZType|numpy.ndarray|list|tuple
        IPN : XYZType|numpy.ndarray|list|tuple
        PolarAngRefTime : float
        PolarAngPoly : Poly1DType|numpy.ndarray|list|tuple
        SpatialFreqSFPoly : Poly1DType|numpy.ndarray|list|tuple
        Krg1 : float
        Krg2 : float
        Kaz1 : float
        Kaz2 : float
        STDeskew : STDeskewType
        kwargs
        """

        if '_xml_ns' in kwargs:
            self._xml_ns = kwargs['_xml_ns']
        if '_xml_ns_key' in kwargs:
            self._xml_ns_key = kwargs['_xml_ns_key']
        self.FPN = FPN
        self.IPN = IPN
        self.PolarAngRefTime = PolarAngRefTime
        self.PolarAngPoly = PolarAngPoly
        self.SpatialFreqSFPoly = SpatialFreqSFPoly
        self.Krg1, self.Krg2 = Krg1, Krg2
        self.Kaz1, self.Kaz2 = Kaz1, Kaz2
        self.STDeskew = STDeskew
        super(PFAType, self).__init__(**kwargs)

    def pfa_polar_coords(self, Position, SCP, times):
        """
        Calculate the PFA parameters necessary for mapping phase history to polar coordinates.

        Parameters
        ----------
        Position : sarpy.io.complex.sicd_elements.Position.PositionType
        SCP : numpy.ndarray
        times : numpy.ndarray|float|int

        Returns
        -------
        (numpy.ndarray, numpy.ndarray)|(float, float)
            `(k_a, k_sf)`, where `k_a` is polar angle, and `k_sf` is spatial
            frequency scale factor. The shape of the output array (or scalar) will
            match the shape of the `times` array (or scalar).
        """
        def project_to_image_plane(points):
            # type: (numpy.ndarray) -> numpy.ndarray
            # project into the image plane along line normal to the focus plane
            offset = (SCP - points).dot(ipn) / fpn.dot(ipn)
            if offset.ndim == 0:
                return points + offset * fpn
            else:
                return points + numpy.outer(offset, fpn)

        if self.IPN is None or self.FPN is None:
            return None, None

        ipn = self.IPN.get_array(dtype='float64')
        fpn = self.FPN.get_array(dtype='float64')
        if isinstance(times, (float, int)) or times.ndim == 0:
            o_shape = None
            times = numpy.array([
                times,
            ], dtype='float64')
        else:
            o_shape = times.shape
            times = numpy.reshape(times, (-1, ))
        positions = Position.ARPPoly(times)
        reference_position = Position.ARPPoly(self.PolarAngRefTime)
        image_plane_positions = project_to_image_plane(positions)
        image_plane_coa = project_to_image_plane(reference_position)

        # establish image plane coordinate system
        ip_x = image_plane_coa - SCP
        ip_x /= numpy.linalg.norm(ip_x)
        ip_y = numpy.cross(ip_x, ipn)

        # compute polar angle of sensor position in image plane
        ip_range = image_plane_positions - SCP
        ip_range /= numpy.linalg.norm(ip_range, axis=1)[:, numpy.newaxis]
        k_a = -numpy.arctan2(ip_range.dot(ip_y), ip_range.dot(ip_x))

        # compute the spatial frequency scale factor
        range_vectors = positions - SCP
        range_vectors /= numpy.linalg.norm(range_vectors,
                                           axis=1)[:, numpy.newaxis]
        sin_graze = range_vectors.dot(fpn)
        sin_graze_ip = ip_range.dot(fpn)
        k_sf = numpy.sqrt(
            (1 - sin_graze * sin_graze) / (1 - sin_graze_ip * sin_graze_ip))
        if o_shape is None:
            return k_a[0], k_sf[0]
        elif len(o_shape) > 1:
            return numpy.reshape(k_a, o_shape), numpy.reshape(k_sf, o_shape)
        else:
            return k_a, k_sf

    def _derive_parameters(self, Grid, SCPCOA, GeoData, Position, Timeline):
        """
        Expected to be called from SICD parent.

        Parameters
        ----------
        Grid : sarpy.io.complex.sicd_elements.Grid.GridType
        SCPCOA : sarpy.io.complex.sicd_elements.SCPCOA.SCPCOAType
        GeoData : sarpy.io.complex.sicd_elements.GeoData.GeoDataType
        Position : sarpy.io.complex.sicd_elements.Position.PositionType
        Timeline : sarpy.io.complex.sicd_elements.Timeline.TimelineType

        Returns
        -------
        None
        """

        if self.PolarAngRefTime is None and SCPCOA.SCPTime is not None:
            self.PolarAngRefTime = SCPCOA.SCPTime

        if GeoData is None or GeoData.SCP is None or GeoData.SCP.ECF is None:
            return

        scp = GeoData.SCP.ECF.get_array()

        if SCPCOA.ARPPos is not None and SCPCOA.ARPVel is not None:
            scp = GeoData.SCP.ECF.get_array()
            etp = geocoords.wgs_84_norm(scp)

            arp = SCPCOA.ARPPos.get_array()
            los = (scp - arp)
            ulos = los / norm(los)

            look = SCPCOA.look
            arp_vel = SCPCOA.ARPVel.get_array()
            uspz = look * numpy.cross(arp_vel, ulos)
            uspz /= norm(uspz)
            if Grid is not None and Grid.ImagePlane is not None:
                if self.IPN is None:
                    if Grid.ImagePlane == 'SLANT':
                        self.IPN = XYZType.from_array(uspz)
                    elif Grid.ImagePlane == 'GROUND':
                        self.IPN = XYZType.from_array(etp)
            elif self.IPN is None:
                self.IPN = XYZType.from_array(
                    uspz)  # assuming slant -> most common

            if self.FPN is None:
                self.FPN = XYZType.from_array(etp)

        if Position is not None and \
                Timeline is not None and Timeline.CollectDuration is not None and \
                (self.PolarAngPoly is None or self.SpatialFreqSFPoly is None):
            pol_ref_pos = Position.ARPPoly(self.PolarAngRefTime)
            # fit the PFA polynomials
            times = numpy.linspace(0, Timeline.CollectDuration, 15)
            k_a, k_sf = self.pfa_polar_coords(Position, scp, times)

            self.PolarAngPoly = Poly1DType(
                Coefs=polynomial.polyfit(times, k_a, 5, full=False))
            self.SpatialFreqSFPoly = Poly1DType(
                Coefs=polynomial.polyfit(k_a, k_sf, 5, full=False))

        if Grid is not None and Grid.Row is not None and \
                Grid.Row.KCtr is not None and Grid.Row.ImpRespBW is not None:
            if self.Krg1 is None:
                self.Krg1 = Grid.Row.KCtr - 0.5 * Grid.Row.ImpRespBW
            if self.Krg2 is None:
                self.Krg2 = Grid.Row.KCtr + 0.5 * Grid.Row.ImpRespBW
        if Grid is not None and Grid.Col is not None and \
                Grid.Col.KCtr is not None and Grid.Col.ImpRespBW is not None:
            if self.Kaz1 is None:
                self.Kaz1 = Grid.Col.KCtr - 0.5 * Grid.Col.ImpRespBW
            if self.Kaz2 is None:
                self.Kaz2 = Grid.Col.KCtr + 0.5 * Grid.Col.ImpRespBW

    def _check_polar_ang_ref(self):
        """
        Checks the polar angle origin makes sense.

        Returns
        -------
        bool
        """

        if self.PolarAngPoly is None or self.PolarAngRefTime is None:
            return True

        cond = True
        polar_angle_ref = self.PolarAngPoly(self.PolarAngRefTime)
        if abs(polar_angle_ref) > 1e-4:
            self.log_validity_error(
                'The PolarAngPoly evaluated at PolarAngRefTime yields {}, which should be 0'
                .format(polar_angle_ref))
            cond = False
        return cond

    def _basic_validity_check(self):
        condition = super(PFAType, self)._basic_validity_check()
        condition &= self._check_polar_ang_ref()
        return condition