示例#1
0
class splinify:
    def __init__(self, z, l0, dL=None, d2L=None):
        self.z_max = z[-1]
        self.z = z
        self.dL = dL
        self.d2L = d2L
        self.l0 = l0
        if dL is not None:
            self._dLz = UnivariateSpline(self.z, self.dL, k=3, s=0, ext=1)
            self._d2Lz = self._dLz.derivative()
            self._Lz = self._dLz.antiderivative()
        elif d2L is not None:
            self._d2Lz = UnivariateSpline(self.z, self.d2L, k=3, s=0, ext=1)
            self._dLz = self._d2Lz.antiderivative()
            self._Lz = self._dLz.antiderivative()
        else:
            raise BaseException("No data to interpolate")

    def dLz(self):
        return vectorize(lambda x: -self._dLz(-x) if x > 0 else self._dLz(x))

    def d2Lz(self):
        return vectorize(lambda x: self._d2Lz(-x) if x > 0 else self._d2Lz(x))

    def Lz(self):
        return vectorize(lambda x: self._Lz(-x) + self.l0 if x > 0 else self._Lz(x) + self.l0)
示例#2
0
def integral(x, y, I, k=10):
    """
    Integrate y = f(x) for x = 0 to a such that the integral = I
    I can be an array
    """
    I = np.atleast_1d(I)

    f = UnivariateSpline(x, y, s=k)

    # Integrate as a function of x
    F = f.antiderivative()
    Y = F(x)

    a = []
    for intval in I:
        F2 = UnivariateSpline(x, Y/Y[-1] - intval, s=0)
        a.append(F2.roots())

    return np.hstack(a)
示例#3
0
class TablePSF(object):
    r"""Radially-symmetric table PSF.

    This PSF represents a :math:`PSF(r)=dP / d\Omega(r)`
    spline interpolation curve for a given set of offset :math:`r`
    and :math:`PSF` points.

    Uses `scipy.interpolate.UnivariateSpline`.

    Parameters
    ----------
    rad : `~astropy.units.Quantity` with angle units
        Offset wrt source position
    dp_domega : `~astropy.units.Quantity` with sr^-1 units
        PSF value array
    spline_kwargs : dict
        Keyword arguments passed to `~scipy.interpolate.UnivariateSpline`

    Notes
    -----
    * This PSF class works well for model PSFs of arbitrary shape (represented by a table),
      but might give unstable results if the PSF has noise.
      E.g. if ``dp_domega`` was estimated from histograms of real or simulated event data
      with finite statistics, it will have noise and it is your responsibility
      to check that the interpolating spline is reasonable.
    * To customize the spline, pass keyword arguments to `~scipy.interpolate.UnivariateSpline`
      in ``spline_kwargs``. E.g. passing ``dict(k=1)`` changes from the default cubic to
      linear interpolation.
    * TODO: evaluate spline for ``(log(rad), log(PSF))`` for numerical stability?
    * TODO: merge morphology.theta class functionality with this class.
    * TODO: add FITS I/O methods
    * TODO: add ``normalize`` argument to ``__init__`` with default ``True``?
    * TODO: ``__call__`` doesn't show up in the html API docs, but it should:
      https://github.com/astropy/astropy/pull/2135
    """
    def __init__(self,
                 rad,
                 dp_domega,
                 spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):

        self._rad = Angle(rad).to("radian")
        self._dp_domega = Quantity(dp_domega).to("sr^-1")

        assert self._rad.ndim == self._dp_domega.ndim == 1
        assert self._rad.shape == self._dp_domega.shape

        # Store input arrays as quantities in default internal units
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to("radian^-1")
        self._spline_kwargs = spline_kwargs

        self._compute_splines(spline_kwargs)

    @classmethod
    def from_shape(cls, shape, width, rad):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.units.Quantity` with angle units
            PSF width angle (radius for disk, sigma for Gauss).
        rad : `~astropy.units.Quantity` with angle units
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import TablePSF
        >>> TablePSF.from_shape(shape='gauss', width='0.2 deg',
        ...                     rad=Angle(np.linspace(0, 0.7, 100), 'deg'))
        """
        width = Angle(width)
        rad = Angle(rad)

        if shape == "disk":
            amplitude = 1 / (np.pi * width.radian**2)
            psf_value = np.where(rad < width, amplitude, 0)
        elif shape == "gauss":
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            psf_value = gauss2d_pdf(rad.radian)
        else:
            raise ValueError("Invalid shape: {}".format(shape))

        psf_value = Quantity(psf_value, "sr^-1")

        return cls(rad, psf_value)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self._rad.degree, "offset")
        ss += "integral = {}\n".format(self.integral())

        for containment in [50, 68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += "containment radius {} deg for {}%\n".format(
                radius.degree, containment)

        return ss

    # TODO: remove because it's not flexible enough?
    def __call__(self, lon, lat):
        """Evaluate PSF at a 2D position.

        The PSF is centered on ``(0, 0)``.

        Parameters
        ----------
        lon, lat : `~astropy.coordinates.Angle`
            Longitude / latitude position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        center = SkyCoord(0, 0, unit="radian")
        point = SkyCoord(lon, lat)
        rad = center.separation(point)
        return self.evaluate(rad)

    def evaluate(self, rad, quantity="dp_domega"):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}

        * 'dp_dr': PDF per 1-dim offset :math:`r` in radian^-1

            .. math:: \frac{dP}{dr} = 2 \pi r \frac{dP}{d\Omega}

        Parameters
        ----------
        rad : `~astropy.coordinates.Angle`
            Offset wrt source position
        quantity : {'dp_domega', 'dp_dr'}
            Which PSF quantity?

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        rad = Angle(rad)

        shape = rad.shape
        x = np.array(rad.radian).flat

        if quantity == "dp_domega":
            y = self._dp_domega_spline(x)
            unit = "sr^-1"
        elif quantity == "dp_dr":
            y = self._dp_dr_spline(x)
            unit = "radian^-1"
        else:
            ss = "Invalid quantity: {}\n".format(quantity)
            ss += "Choose one of: 'dp_domega', 'dp_dr'"
            raise ValueError(ss)

        y = np.clip(a=y, a_min=0, a_max=None)
        return Quantity(y, unit).reshape(shape)

    def integral(self, rad_min=None, rad_max=None):
        """Compute PSF integral, aka containment fraction.

        Parameters
        ----------
        rad_min, rad_max : `~astropy.units.Quantity` with angle units
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        if rad_min is None:
            rad_min = self._rad[0]
        else:
            rad_min = Angle(rad_min)

        if rad_max is None:
            rad_max = self._rad[-1]
        else:
            rad_max = Angle(rad_max)

        rad_min = self._rad_clip(rad_min)
        rad_max = self._rad_clip(rad_max)

        cdf_min = self._cdf_spline(rad_min)
        cdf_max = self._cdf_spline(rad_max)

        return cdf_max - cdf_min

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        rad : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        rad = self._ppf_spline(fraction)
        return Angle(rad, "radian").to("deg")

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / dr` spline
        and then divides the :math:`dP / dr` array.
        """
        integral = self.integral()

        self._dp_dr /= integral

        # Clip to small positive number to avoid divide by 0
        rad = np.clip(self._rad.radian, 1e-6, None)

        rad = Quantity(rad, "radian")
        self._dp_domega = self._dp_dr / (2 * np.pi * rad)
        self._compute_splines(self._spline_kwargs)

    def broaden(self, factor, normalize=True):
        r"""Broaden PSF by scaling the offset array.

        For a broadening factor :math:`f` and the offset
        array :math:`r`, the offset array scaled
        in the following way:

        .. math::
            r_{new} = f \times r_{old}
            \frac{dP}{dr}(r_{new}) = \frac{dP}{dr}(r_{old})

        Parameters
        ----------
        factor : float
            Broadening factor
        normalize : bool
            Normalize PSF after broadening
        """
        self._rad *= factor
        # We define broadening such that self._dp_domega remains the same
        # so we only have to re-compute self._dp_dr and the slines here.
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to("radian^-1")
        self._compute_splines(self._spline_kwargs)

        if normalize:
            self.normalize()

    def plot_psf_vs_rad(self, ax=None, quantity="dp_domega", **kwargs):
        """Plot PSF vs radius.

        TODO: describe PSF ``quantity`` argument in a central place and link to it from here.
        """
        import matplotlib.pyplot as plt

        ax = plt.gca() if ax is None else ax

        x = self._rad.to("deg")
        y = self.evaluate(self._rad, quantity)

        ax.plot(x.value, y.value, **kwargs)
        ax.loglog()
        ax.set_xlabel("Radius ({})".format(x.unit))
        ax.set_ylabel("PSF ({})".format(y.unit))

    def _compute_splines(self, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):
        """Compute two splines representing the PSF.

        * `_dp_domega_spline` is used to evaluate the 2D PSF.
        * `_dp_dr_spline` is not really needed for most applications,
          but is available via `eval`.
        * `_cdf_spline` is used to compute integral and for normalisation.
        * `_ppf_spline` is used to compute containment radii.
        """
        # Compute spline and normalize.
        x, y = self._rad.value, self._dp_domega.value
        self._dp_domega_spline = UnivariateSpline(x, y, **spline_kwargs)

        x, y = self._rad.value, self._dp_dr.value
        self._dp_dr_spline = UnivariateSpline(x, y, **spline_kwargs)

        # We use the terminology for scipy.stats distributions
        # http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html#common-methods

        # cdf = "cumulative distribution function"
        self._cdf_spline = self._dp_dr_spline.antiderivative()

        # ppf = "percent point function" (inverse of cdf)
        # Here's a discussion on methods to compute the ppf
        # http://mail.scipy.org/pipermail/scipy-user/2010-May/025237.html
        y = self._rad.value
        x = self.integral(Angle(0, "rad"), self._rad)

        # Since scipy 1.0 the UnivariateSpline requires that x is strictly increasing
        # So only keep nodes where this is the case (and always keep the first one):
        x, idx = np.unique(x, return_index=True)
        y = y[idx]

        # Dummy values, for cases where one really doesn't have a valid PSF.
        if len(x) < 4:
            x = [0, 1, 2, 3]
            y = [0, 0, 0, 0]

        self._ppf_spline = UnivariateSpline(x, y, **spline_kwargs)

    def _rad_clip(self, rad):
        """Clip to radius support range, because spline extrapolation is unstable."""
        rad = Angle(rad, "radian").radian
        rad = np.clip(rad, 0, self._rad[-1].radian)
        return rad
示例#4
0
class TablePSF(object):
    r"""Radially-symmetric table PSF.

    This PSF represents a :math:`PSF(r)=dP / d\Omega(r)`
    spline interpolation curve for a given set of offset :math:`r`
    and :math:`PSF` points.

    Uses `scipy.interpolate.UnivariateSpline`.

    Parameters
    ----------
    rad : `~astropy.units.Quantity` with angle units
        Offset wrt source position
    dp_domega : `~astropy.units.Quantity` with sr^-1 units
        PSF value array
    spline_kwargs : dict
        Keyword arguments passed to `~scipy.interpolate.UnivariateSpline`

    Notes
    -----
    * This PSF class works well for model PSFs of arbitrary shape (represented by a table),
      but might give unstable results if the PSF has noise.
      E.g. if ``dp_domega`` was estimated from histograms of real or simulated event data
      with finite statistics, it will have noise and it is your responsibility
      to check that the interpolating spline is reasonable.
    * To customize the spline, pass keyword arguments to `~scipy.interpolate.UnivariateSpline`
      in ``spline_kwargs``. E.g. passing ``dict(k=1)`` changes from the default cubic to
      linear interpolation.
    * TODO: evaluate spline for ``(log(rad), log(PSF))`` for numerical stability?
    * TODO: merge morphology.theta class functionality with this class.
    * TODO: add FITS I/O methods
    * TODO: add ``normalize`` argument to ``__init__`` with default ``True``?
    * TODO: ``__call__`` doesn't show up in the html API docs, but it should:
      https://github.com/astropy/astropy/pull/2135
    """

    def __init__(self, rad, dp_domega, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):

        self._rad = Angle(rad).to('radian')
        self._dp_domega = Quantity(dp_domega).to('sr^-1')

        assert self._rad.ndim == self._dp_domega.ndim == 1
        assert self._rad.shape == self._dp_domega.shape

        # Store input arrays as quantities in default internal units
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to('radian^-1')
        self._spline_kwargs = spline_kwargs

        self._compute_splines(spline_kwargs)

    @classmethod
    def from_shape(cls, shape, width, rad):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.units.Quantity` with angle units
            PSF width angle (radius for disk, sigma for Gauss).
        rad : `~astropy.units.Quantity` with angle units
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import TablePSF
        >>> TablePSF.from_shape(shape='gauss', width='0.2 deg',
        ...                     rad=Angle(np.linspace(0, 0.7, 100), 'deg'))
        """
        width = Angle(width)
        rad = Angle(rad)

        if shape == 'disk':
            amplitude = 1 / (np.pi * width.radian ** 2)
            psf_value = np.where(rad < width, amplitude, 0)
        elif shape == 'gauss':
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            psf_value = gauss2d_pdf(rad.radian)
        else:
            raise ValueError('Invalid shape: {}'.format(shape))

        psf_value = Quantity(psf_value, 'sr^-1')

        return cls(rad, psf_value)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self._rad.degree, 'offset')
        ss += 'integral = {}\n'.format(self.integral())

        for containment in [50, 68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += ('containment radius {} deg for {}%\n'
                   .format(radius.degree, containment))

        return ss

    # TODO: remove because it's not flexible enough?
    def __call__(self, lon, lat):
        """Evaluate PSF at a 2D position.

        The PSF is centered on ``(0, 0)``.

        Parameters
        ----------
        lon, lat : `~astropy.coordinates.Angle`
            Longitude / latitude position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        center = SkyCoord(0, 0, unit='radian')
        point = SkyCoord(lon, lat)
        rad = center.separation(point)
        return self.evaluate(rad)

    def kernel(self, reference, rad_max, normalize=True,
               discretize_model_kwargs=dict(factor=10)):
        """
        Make a 2-dimensional kernel image.

        The kernel image is evaluated on a cartesian grid defined by the
        reference sky image.

        Parameters
        ----------
        reference : `~gammapy.image.SkyImage` or `~gammapy.cube.SkyCube`
            Reference sky image or sky cube defining the spatial grid.
        rad_max : `~astropy.coordinates.Angle`
            Radial size of the kernel
        normalize : bool
            Whether to normalize the kernel.

        Returns
        -------
        kernel : `~astropy.units.Quantity`
            Kernel 2D image of Quantities
        """
        from ..cube import SkyCube
        rad_max = Angle(rad_max)

        if isinstance(reference, SkyCube):
            reference = reference.sky_image_ref

        pixel_size = reference.wcs_pixel_scale()[0]

        def _model(x, y):
            """Model in the appropriate format for discretize_model."""
            rad = np.sqrt(x * x + y * y) * pixel_size
            return self.evaluate(rad)

        npix = int(rad_max.radian / pixel_size.radian)
        pix_range = (-npix, npix + 1)

        kernel = discretize_oversample_2D(_model, x_range=pix_range, y_range=pix_range,
                                          **discretize_model_kwargs)
        if normalize:
            kernel = kernel / kernel.sum()

        return kernel

    def evaluate(self, rad, quantity='dp_domega'):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}

        * 'dp_dr': PDF per 1-dim offset :math:`r` in radian^-1

            .. math:: \frac{dP}{dr} = 2 \pi r \frac{dP}{d\Omega}

        Parameters
        ----------
        rad : `~astropy.coordinates.Angle`
            Offset wrt source position
        quantity : {'dp_domega', 'dp_dr'}
            Which PSF quantity?

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        rad = Angle(rad)

        shape = rad.shape
        x = np.array(rad.radian).flat

        if quantity == 'dp_domega':
            y = self._dp_domega_spline(x)
            unit = 'sr^-1'
        elif quantity == 'dp_dr':
            y = self._dp_dr_spline(x)
            unit = 'radian^-1'
        else:
            ss = 'Invalid quantity: {}\n'.format(quantity)
            ss += "Choose one of: 'dp_domega', 'dp_dr'"
            raise ValueError(ss)

        y = np.clip(a=y, a_min=0, a_max=None)
        return Quantity(y, unit).reshape(shape)

    def integral(self, rad_min=None, rad_max=None):
        """Compute PSF integral, aka containment fraction.

        Parameters
        ----------
        rad_min, rad_max : `~astropy.units.Quantity` with angle units
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        if rad_min is None:
            rad_min = self._rad[0]
        else:
            rad_min = Angle(rad_min)

        if rad_max is None:
            rad_max = self._rad[-1]
        else:
            rad_max = Angle(rad_max)

        rad_min = self._rad_clip(rad_min)
        rad_max = self._rad_clip(rad_max)

        cdf_min = self._cdf_spline(rad_min)
        cdf_max = self._cdf_spline(rad_max)

        return cdf_max - cdf_min

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        rad : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        rad = self._ppf_spline(fraction)
        return Angle(rad, 'radian').to('deg')

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / dr` spline
        and then divides the :math:`dP / dr` array.
        """
        integral = self.integral()

        self._dp_dr /= integral

        # Don't divide by 0
        EPS = 1e-6
        rad = np.clip(self._rad.radian, EPS, None)
        rad = Quantity(rad, 'radian')
        self._dp_domega = self._dp_dr / (2 * np.pi * rad)
        self._compute_splines(self._spline_kwargs)

    def broaden(self, factor, normalize=True):
        r"""Broaden PSF by scaling the offset array.

        For a broadening factor :math:`f` and the offset
        array :math:`r`, the offset array scaled
        in the following way:

        .. math::
            r_{new} = f \times r_{old}
            \frac{dP}{dr}(r_{new}) = \frac{dP}{dr}(r_{old})

        Parameters
        ----------
        factor : float
            Broadening factor
        normalize : bool
            Normalize PSF after broadening
        """
        self._rad *= factor
        # We define broadening such that self._dp_domega remains the same
        # so we only have to re-compute self._dp_dr and the slines here.
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to('radian^-1')
        self._compute_splines(self._spline_kwargs)

        if normalize:
            self.normalize()

    def plot_psf_vs_rad(self, ax=None, quantity='dp_domega', **kwargs):
        """Plot PSF vs radius.

        TODO: describe PSF ``quantity`` argument in a central place and link to it from here.
        """
        import matplotlib.pyplot as plt
        ax = plt.gca() if ax is None else ax

        x = self._rad.to('deg')
        y = self.evaluate(self._rad, quantity)

        ax.plot(x.value, y.value, **kwargs)
        ax.loglog()
        ax.set_xlabel('Radius ({})'.format(x.unit))
        ax.set_ylabel('PSF ({})'.format(y.unit))

    def _compute_splines(self, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):
        """Compute two splines representing the PSF.

        * `_dp_domega_spline` is used to evaluate the 2D PSF.
        * `_dp_dr_spline` is not really needed for most applications,
          but is available via `eval`.
        * `_cdf_spline` is used to compute integral and for normalisation.
        * `_ppf_spline` is used to compute containment radii.
        """
        from scipy.interpolate import UnivariateSpline

        # Compute spline and normalize.
        x, y = self._rad.value, self._dp_domega.value
        self._dp_domega_spline = UnivariateSpline(x, y, **spline_kwargs)

        x, y = self._rad.value, self._dp_dr.value
        self._dp_dr_spline = UnivariateSpline(x, y, **spline_kwargs)

        # We use the terminology for scipy.stats distributions
        # http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html#common-methods

        # cdf = "cumulative distribution function"
        self._cdf_spline = self._dp_dr_spline.antiderivative()

        # ppf = "percent point function" (inverse of cdf)
        # Here's a discussion on methods to compute the ppf
        # http://mail.scipy.org/pipermail/scipy-user/2010-May/025237.html
        y = self._rad.value
        x = self.integral(Angle(0, 'rad'), self._rad)

        # Since scipy 1.0 the UnivariateSpline requires that x is strictly increasing
        # So only keep nodes where this is the case (and always keep the first one):
        x, idx = np.unique(x, return_index=True)
        y = y[idx]

        # Dummy values, for cases where one really doesn't have a valid PSF.
        if len(x) < 4:
            x = [0, 1, 2, 3]
            y = [0, 0, 0, 0]

        self._ppf_spline = UnivariateSpline(x, y, **spline_kwargs)

    def _rad_clip(self, rad):
        """Clip to radius support range, because spline extrapolation is unstable."""
        rad = Angle(rad, 'radian').radian
        rad = np.clip(rad, 0, self._rad[-1].radian)
        return rad
示例#5
0
class TablePSF(object):
    r"""Radially-symmetric table PSF.

    This PSF represents a :math:`PSF(\theta)=dP / d\Omega(\theta)`
    spline interpolation curve for a given set of offset :math:`\theta`
    and :math:`PSF` points.

    Uses `scipy.interpolate.UnivariateSpline`.

    Parameters
    ----------
    offset : `~astropy.coordinates.Angle`
        Offset angle array
    dp_domega : `~astropy.units.Quantity`
        PSF value array
    spline_kwargs : dict
        Keyword arguments passed to `~scipy.interpolate.UnivariateSpline`

    Notes
    -----
    * This PSF class works well for model PSFs of arbitrary shape (represented by a table),
      but might give unstable results if the PSF has noise.
      E.g. if ``dp_domega`` was estimated from histograms of real or simulated event data
      with finite statistics, it will have noise and it is your responsibility
      to check that the interpolating spline is reasonable.
    * To customize the spline, pass keyword arguments to `~scipy.interpolate.UnivariateSpline`
      in ``spline_kwargs``. E.g. passing ``dict(k=1)`` changes from the default cubic to
      linear interpolation.
    * TODO: evaluate spline for ``(log(offset), log(PSF))`` for numerical stability?
    * TODO: merge morphology.theta class functionality with this class.
    * TODO: add FITS I/O methods
    * TODO: add ``normalize`` argument to ``__init__`` with default ``True``?
    * TODO: ``__call__`` doesn't show up in the html API docs, but it should:
      https://github.com/astropy/astropy/pull/2135
    """

    def __init__(self, offset, dp_domega, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):

        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")
        if not isinstance(dp_domega, Quantity):
            raise ValueError("dp_domega must be a Quantity object.")

        assert offset.ndim == dp_domega.ndim == 1
        assert offset.shape == dp_domega.shape

        # Store input arrays as quantities in default internal units
        self._offset = offset.to('radian')
        self._dp_domega = dp_domega.to('sr^-1')
        self._dp_dtheta = (2 * np.pi * self._offset * self._dp_domega).to('radian^-1')
        self._spline_kwargs = spline_kwargs

        self._compute_splines(spline_kwargs)

    @classmethod
    def from_shape(cls, shape, width, offset):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.coordinates.Angle`
            PSF width angle (radius for disk, sigma for Gauss).
        offset : `~astropy.coordinates.Angle`
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import make_table_psf
        >>> make_table_psf(shape='gauss', width=Angle(0.2, 'deg'),
        ...                offset=Angle(np.linspace(0, 0.7, 100), 'deg'))
        """
        if not isinstance(width, Angle):
            raise ValueError("width must be an Angle object.")
        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")

        if shape == 'disk':
            amplitude = 1 / (np.pi * width.radian ** 2)
            psf_value = np.where(offset < width, amplitude, 0)
        elif shape == 'gauss':
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            psf_value = gauss2d_pdf(offset.radian)

        psf_value = Quantity(psf_value, 'sr^-1')

        return cls(offset, psf_value)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self._offset.degree, 'offset')
        ss += 'integral = {0}\n'.format(self.integral())

        for containment in [50, 68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += ('containment radius {0} deg for {1}%\n'
                   .format(radius.degree, containment))

        return ss

    # TODO: remove because it's not flexible enough?
    def __call__(self, lon, lat):
        """Evaluate PSF at a 2D position.

        The PSF is centered on ``(0, 0)``.

        Parameters
        ----------
        lon, lat : `~astropy.coordinates.Angle`
            Longitude / latitude position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        center = SkyCoord(0, 0, unit='radian')
        point = SkyCoord(lon, lat)
        offset = center.separation(point)
        return self.evaluate(offset)

    def kernel(self, pixel_size, offset_max=None, normalize=True,
               discretize_model_kwargs=dict(factor=10)):
        """Make a 2-dimensional kernel image.

        The kernel image is evaluated on a cartesian
        grid with ``pixel_size`` spacing, not on the sphere.

        Calls `astropy.convolution.discretize_model`,
        allowing for accurate discretization.

        Parameters
        ----------
        pixel_size : `~astropy.coordinates.Angle`
            Kernel pixel size
        discretize_model_kwargs : dict
            Keyword arguments passed to
            `astropy.convolution.discretize_model`

        Returns
        -------
        kernel : `~astropy.units.Quantity`
            Kernel 2D image of Quantities

        Notes
        -----
        * In the future, `astropy.modeling.Fittable2DModel` and
          `astropy.convolution.Model2DKernel` could be used to construct
          the kernel.
        """
        if not isinstance(pixel_size, Angle):
            raise ValueError("pixel_size must be an Angle object.")

        if offset_max is None:
            offset_max = self._offset.max()

        def _model(x, y):
            """Model in the appropriate format for discretize_model."""
            offset = np.sqrt(x * x + y * y) * pixel_size
            return self.evaluate(offset)

        npix = int(offset_max.radian / pixel_size.radian)
        pix_range = (-npix, npix + 1)

        # FIXME: Using `discretize_model` is currently very cumbersome due to these issue:
        # https://github.com/astropy/astropy/issues/2274
        # https://github.com/astropy/astropy/issues/1763#issuecomment-39552900
        # from astropy.modeling import Fittable2DModel
        #
        # class TempModel(Fittable2DModel):
        #    @staticmethod
        #    def evaluate(x, y):
        #        return 42 temp_model_function(x, y)
        #
        # temp_model = TempModel()

        array = discretize_oversample_2D(_model,
                                         x_range=pix_range, y_range=pix_range,
                                         **discretize_model_kwargs)
        if normalize:
            return array / array.value.sum()
        else:
            return array

    def evaluate(self, offset, quantity='dp_domega'):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}

        * 'dp_dtheta': PDF per 1-dim offset :math:`\theta` in radian^-1

            .. math:: \frac{dP}{d\theta} = 2 \pi \theta \frac{dP}{d\Omega}

        Parameters
        ----------
        offset : `~astropy.coordinates.Angle`
            Offset angle
        quantity : {'dp_domega', 'dp_dtheta'}
            Which PSF quantity?

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")

        shape = offset.shape
        x = np.array(offset.radian).flat

        if quantity == 'dp_domega':
            y = self._dp_domega_spline(x)
            return Quantity(y, 'sr^-1').reshape(shape)
        elif quantity == 'dp_dtheta':
            y = self._dp_dtheta_spline(x)
            return Quantity(y, 'radian^-1').reshape(shape)
        else:
            ss = 'Invalid quantity: {0}\n'.format(quantity)
            ss += "Choose one of: 'dp_domega', 'dp_dtheta'"
            raise ValueError(ss)

    def integral(self, offset_min=None, offset_max=None):
        """Compute PSF integral, aka containment fraction.

        Parameters
        ----------
        offset_min, offset_max : `~astropy.coordinates.Angle`
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        if offset_min is None:
            offset_min = self._offset[0]
        else:
            if not isinstance(offset_min, Angle):
                raise ValueError("offset_min must be an Angle object.")

        if offset_max is None:
            offset_max = self._offset[-1]
        else:
            if not isinstance(offset_max, Angle):
                raise ValueError("offset_max must be an Angle object.")

        offset_min = self._offset_clip(offset_min)
        offset_max = self._offset_clip(offset_max)

        cdf_min = self._cdf_spline(offset_min)
        cdf_max = self._cdf_spline(offset_max)

        return cdf_max - cdf_min

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        radius : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        radius = self._ppf_spline(fraction)
        return Angle(radius, 'radian').to('deg')

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / d\theta` spline
        and then divides the :math:`dP / d\theta` array.
        """
        integral = self.integral()

        self._dp_dtheta /= integral

        # Don't divide by 0
        EPS = 1e-6
        offset = np.clip(self._offset.radian, EPS, None)
        offset = Quantity(offset, 'radian')
        self._dp_domega = self._dp_dtheta / (2 * np.pi * offset)
        self._compute_splines(self._spline_kwargs)

    def broaden(self, factor, normalize=True):
        r"""Broaden PSF by scaling the offset array.

        For a broadening factor :math:`f` and the offset
        array :math:`\theta`, the offset array scaled
        in the following way:

        .. math::
            \theta_{new} = f \times \theta_{old}
            \frac{dP}{d\theta}(\theta_{new}) = \frac{dP}{d\theta}(\theta_{old})

        Parameters
        ----------
        factor : float
            Broadening factor
        normalize : bool
            Normalize PSF after broadening
        """
        self._offset *= factor
        # We define broadening such that self._dp_domega remains the same
        # so we only have to re-compute self._dp_dtheta and the slines here.
        self._dp_dtheta = (2 * np.pi * self._offset * self._dp_domega).to('radian^-1')
        self._compute_splines(self._spline_kwargs)

        if normalize:
            self.normalize()

    def plot_psf_vs_theta(self, quantity='dp_domega'):
        """Plot PSF vs offset.

        TODO: describe PSF ``quantity`` argument in a central place and link to it from here.
        """
        import matplotlib.pyplot as plt

        x = self._offset.to('deg')
        y = self.evaluate(self._offset, quantity)

        plt.plot(x.value, y.value, lw=2)
        plt.semilogy()
        plt.loglog()
        plt.xlabel('Offset ({0})'.format(x.unit))
        plt.ylabel('PSF ({0})'.format(y.unit))

    def _compute_splines(self, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):
        """Compute two splines representing the PSF.

        * `_dp_domega_spline` is used to evaluate the 2D PSF.
        * `_dp_dtheta_spline` is not really needed for most applications,
          but is available via `eval`.
        * `_cdf_spline` is used to compute integral and for normalisation.
        * `_ppf_spline` is used to compute containment radii.
        """
        from scipy.interpolate import UnivariateSpline

        # Compute spline and normalize.
        x, y = self._offset.value, self._dp_domega.value
        self._dp_domega_spline = UnivariateSpline(x, y, **spline_kwargs)

        x, y = self._offset.value, self._dp_dtheta.value
        self._dp_dtheta_spline = UnivariateSpline(x, y, **spline_kwargs)

        # We use the terminology for scipy.stats distributions
        # http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html#common-methods

        # cdf = "cumulative distribution function"
        self._cdf_spline = self._dp_dtheta_spline.antiderivative()

        # ppf = "percent point function" (inverse of cdf)
        # Here's a discussion on methods to compute the ppf
        # http://mail.scipy.org/pipermail/scipy-user/2010-May/025237.html
        x = self._offset.value
        y = self._cdf_spline(x)
        self._ppf_spline = UnivariateSpline(y, x, **spline_kwargs)

    def _offset_clip(self, offset):
        """Clip to offset support range, because spline extrapolation is unstable."""
        offset = Angle(offset, 'radian').radian
        offset = np.clip(offset, 0, self._offset[-1].radian)
        return offset
示例#6
0
class TablePSF(object):
    r"""Radially-symmetric table PSF.

    This PSF represents a :math:`PSF(\theta)=dP / d\Omega(\theta)`
    spline interpolation curve for a given set of offset :math:`\theta`
    and :math:`PSF` points.

    Uses `scipy.interpolate.UnivariateSpline`.

    Parameters
    ----------
    offset : `~astropy.coordinates.Angle`
        Offset angle array
    dp_domega : `~astropy.units.Quantity`
        PSF value array
    spline_kwargs : dict
        Keyword arguments passed to `~scipy.interpolate.UnivariateSpline`

    Notes
    -----
    * This PSF class works well for model PSFs of arbitrary shape (represented by a table),
      but might give unstable results if the PSF has noise.
      E.g. if ``dp_domega`` was estimated from histograms of real or simulated event data
      with finite statistics, it will have noise and it is your responsibility
      to check that the interpolating spline is reasonable.
    * To customize the spline, pass keyword arguments to `~scipy.interpolate.UnivariateSpline`
      in ``spline_kwargs``. E.g. passing ``dict(k=1)`` changes from the default cubic to
      linear interpolation.
    * TODO: evaluate spline for ``(log(offset), log(PSF))`` for numerical stability?
    * TODO: merge morphology.theta class functionality with this class.
    * TODO: add FITS I/O methods
    * TODO: add ``normalize`` argument to ``__init__`` with default ``True``?
    * TODO: ``__call__`` doesn't show up in the html API docs, but it should:
      https://github.com/astropy/astropy/pull/2135
    """
    def __init__(self,
                 offset,
                 dp_domega,
                 spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):

        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")
        if not isinstance(dp_domega, Quantity):
            raise ValueError("dp_domega must be a Quantity object.")

        assert offset.ndim == dp_domega.ndim == 1
        assert offset.shape == dp_domega.shape

        # Store input arrays as quantities in default internal units
        self._offset = offset.to('radian')
        self._dp_domega = dp_domega.to('sr^-1')
        self._dp_dtheta = (2 * np.pi * self._offset *
                           self._dp_domega).to('radian^-1')
        self._spline_kwargs = spline_kwargs

        self._compute_splines(spline_kwargs)

    @classmethod
    def from_shape(cls, shape, width, offset):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.coordinates.Angle`
            PSF width angle (radius for disk, sigma for Gauss).
        offset : `~astropy.coordinates.Angle`
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import make_table_psf
        >>> make_table_psf(shape='gauss', width=Angle(0.2, 'deg'),
        ...                offset=Angle(np.linspace(0, 0.7, 100), 'deg'))
        """
        if not isinstance(width, Angle):
            raise ValueError("width must be an Angle object.")
        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")

        if shape == 'disk':
            amplitude = 1 / (np.pi * width.radian**2)
            psf_value = np.where(offset < width, amplitude, 0)
        elif shape == 'gauss':
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            psf_value = gauss2d_pdf(offset.radian)

        psf_value = Quantity(psf_value, 'sr^-1')

        return cls(offset, psf_value)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self._offset.degree, 'offset')
        ss += 'integral = {0}\n'.format(self.integral())

        for containment in [50, 68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += ('containment radius {0} deg for {1}%\n'.format(
                radius.degree, containment))

        return ss

    # TODO: remove because it's not flexible enough?
    def __call__(self, lon, lat):
        """Evaluate PSF at a 2D position.

        The PSF is centered on ``(0, 0)``.

        Parameters
        ----------
        lon, lat : `~astropy.coordinates.Angle`
            Longitude / latitude position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        center = SkyCoord(0, 0, unit='radian')
        point = SkyCoord(lon, lat)
        offset = center.separation(point)
        return self.evaluate(offset)

    def kernel(self,
               pixel_size,
               offset_max=None,
               normalize=True,
               discretize_model_kwargs=dict(factor=10)):
        """Make a 2-dimensional kernel image.

        The kernel image is evaluated on a cartesian
        grid with ``pixel_size`` spacing, not on the sphere.

        Calls `astropy.convolution.discretize_model`,
        allowing for accurate discretization.

        Parameters
        ----------
        pixel_size : `~astropy.coordinates.Angle`
            Kernel pixel size
        discretize_model_kwargs : dict
            Keyword arguments passed to
            `astropy.convolution.discretize_model`

        Returns
        -------
        kernel : `~astropy.units.Quantity`
            Kernel 2D image of Quantities

        Notes
        -----
        * In the future, `astropy.modeling.Fittable2DModel` and
          `astropy.convolution.Model2DKernel` could be used to construct
          the kernel.
        """
        if not isinstance(pixel_size, Angle):
            raise ValueError("pixel_size must be an Angle object.")

        if offset_max is None:
            offset_max = self._offset.max()

        def _model(x, y):
            """Model in the appropriate format for discretize_model."""
            offset = np.sqrt(x * x + y * y) * pixel_size
            return self.evaluate(offset)

        npix = int(offset_max.radian / pixel_size.radian)
        pix_range = (-npix, npix + 1)

        # FIXME: Using `discretize_model` is currently very cumbersome due to these issue:
        # https://github.com/astropy/astropy/issues/2274
        # https://github.com/astropy/astropy/issues/1763#issuecomment-39552900
        # from astropy.modeling import Fittable2DModel
        #
        # class TempModel(Fittable2DModel):
        #    @staticmethod
        #    def evaluate(x, y):
        #        return 42 temp_model_function(x, y)
        #
        # temp_model = TempModel()

        array = discretize_oversample_2D(_model,
                                         x_range=pix_range,
                                         y_range=pix_range,
                                         **discretize_model_kwargs)
        if normalize:
            return array / array.value.sum()
        else:
            return array

    def evaluate(self, offset, quantity='dp_domega'):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}

        * 'dp_dtheta': PDF per 1-dim offset :math:`\theta` in radian^-1

            .. math:: \frac{dP}{d\theta} = 2 \pi \theta \frac{dP}{d\Omega}

        Parameters
        ----------
        offset : `~astropy.coordinates.Angle`
            Offset angle
        quantity : {'dp_domega', 'dp_dtheta'}
            Which PSF quantity?

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        if not isinstance(offset, Angle):
            raise ValueError("offset must be an Angle object.")

        shape = offset.shape
        x = np.array(offset.radian).flat

        if quantity == 'dp_domega':
            y = self._dp_domega_spline(x)
            unit = 'sr^-1'
        elif quantity == 'dp_dtheta':
            y = self._dp_dtheta_spline(x)
            unit = 'radian^-1'
        else:
            ss = 'Invalid quantity: {0}\n'.format(quantity)
            ss += "Choose one of: 'dp_domega', 'dp_dtheta'"
            raise ValueError(ss)

        y = np.clip(a=y, a_min=0, a_max=None)
        return Quantity(y, unit).reshape(shape)

    def integral(self, offset_min=None, offset_max=None):
        """Compute PSF integral, aka containment fraction.

        Parameters
        ----------
        offset_min, offset_max : `~astropy.coordinates.Angle`
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        if offset_min is None:
            offset_min = self._offset[0]
        else:
            if not isinstance(offset_min, Angle):
                raise ValueError("offset_min must be an Angle object.")

        if offset_max is None:
            offset_max = self._offset[-1]
        else:
            if not isinstance(offset_max, Angle):
                raise ValueError("offset_max must be an Angle object.")

        offset_min = self._offset_clip(offset_min)
        offset_max = self._offset_clip(offset_max)

        cdf_min = self._cdf_spline(offset_min)
        cdf_max = self._cdf_spline(offset_max)

        return cdf_max - cdf_min

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        radius : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        radius = self._ppf_spline(fraction)
        return Angle(radius, 'radian').to('deg')

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / d\theta` spline
        and then divides the :math:`dP / d\theta` array.
        """
        integral = self.integral()

        self._dp_dtheta /= integral

        # Don't divide by 0
        EPS = 1e-6
        offset = np.clip(self._offset.radian, EPS, None)
        offset = Quantity(offset, 'radian')
        self._dp_domega = self._dp_dtheta / (2 * np.pi * offset)
        self._compute_splines(self._spline_kwargs)

    def broaden(self, factor, normalize=True):
        r"""Broaden PSF by scaling the offset array.

        For a broadening factor :math:`f` and the offset
        array :math:`\theta`, the offset array scaled
        in the following way:

        .. math::
            \theta_{new} = f \times \theta_{old}
            \frac{dP}{d\theta}(\theta_{new}) = \frac{dP}{d\theta}(\theta_{old})

        Parameters
        ----------
        factor : float
            Broadening factor
        normalize : bool
            Normalize PSF after broadening
        """
        self._offset *= factor
        # We define broadening such that self._dp_domega remains the same
        # so we only have to re-compute self._dp_dtheta and the slines here.
        self._dp_dtheta = (2 * np.pi * self._offset *
                           self._dp_domega).to('radian^-1')
        self._compute_splines(self._spline_kwargs)

        if normalize:
            self.normalize()

    def plot_psf_vs_theta(self, quantity='dp_domega'):
        """Plot PSF vs offset.

        TODO: describe PSF ``quantity`` argument in a central place and link to it from here.
        """
        import matplotlib.pyplot as plt

        x = self._offset.to('deg')
        y = self.evaluate(self._offset, quantity)

        plt.plot(x.value, y.value, lw=2)
        plt.semilogy()
        plt.loglog()
        plt.xlabel('Offset ({0})'.format(x.unit))
        plt.ylabel('PSF ({0})'.format(y.unit))

    def _compute_splines(self, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):
        """Compute two splines representing the PSF.

        * `_dp_domega_spline` is used to evaluate the 2D PSF.
        * `_dp_dtheta_spline` is not really needed for most applications,
          but is available via `eval`.
        * `_cdf_spline` is used to compute integral and for normalisation.
        * `_ppf_spline` is used to compute containment radii.
        """
        from scipy.interpolate import UnivariateSpline

        # Compute spline and normalize.
        x, y = self._offset.value, self._dp_domega.value
        self._dp_domega_spline = UnivariateSpline(x, y, **spline_kwargs)

        x, y = self._offset.value, self._dp_dtheta.value
        self._dp_dtheta_spline = UnivariateSpline(x, y, **spline_kwargs)

        # We use the terminology for scipy.stats distributions
        # http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html#common-methods

        # cdf = "cumulative distribution function"
        self._cdf_spline = self._dp_dtheta_spline.antiderivative()

        # ppf = "percent point function" (inverse of cdf)
        # Here's a discussion on methods to compute the ppf
        # http://mail.scipy.org/pipermail/scipy-user/2010-May/025237.html
        x = self._offset.value
        y = self.integral(Angle(0, 'rad'), self._offset)

        # This is a hack to stabilize the univariate spline. Only use the first
        # i entries, where the integral is srictly increasing, to build the spline.
        i = (np.diff(y) <= 0).argmax()
        i = len(y) if i == 0 else i
        self._ppf_spline = UnivariateSpline(y[:i], x[:i], **spline_kwargs)

    def _offset_clip(self, offset):
        """Clip to offset support range, because spline extrapolation is unstable."""
        offset = Angle(offset, 'radian').radian
        offset = np.clip(offset, 0, self._offset[-1].radian)
        return offset
示例#7
0
class TablePSF(object):
    r"""Radially-symmetric table PSF.

    This PSF represents a :math:`PSF(r)=dP / d\Omega(r)`
    spline interpolation curve for a given set of offset :math:`r`
    and :math:`PSF` points.

    Uses `scipy.interpolate.UnivariateSpline`.

    Parameters
    ----------
    rad : `~astropy.units.Quantity` with angle units
        Offset wrt source position
    dp_domega : `~astropy.units.Quantity` with sr^-1 units
        PSF value array
    spline_kwargs : dict
        Keyword arguments passed to `~scipy.interpolate.UnivariateSpline`

    Notes
    -----
    * This PSF class works well for model PSFs of arbitrary shape (represented by a table),
      but might give unstable results if the PSF has noise.
      E.g. if ``dp_domega`` was estimated from histograms of real or simulated event data
      with finite statistics, it will have noise and it is your responsibility
      to check that the interpolating spline is reasonable.
    * To customize the spline, pass keyword arguments to `~scipy.interpolate.UnivariateSpline`
      in ``spline_kwargs``. E.g. passing ``dict(k=1)`` changes from the default cubic to
      linear interpolation.
    * TODO: evaluate spline for ``(log(rad), log(PSF))`` for numerical stability?
    * TODO: merge morphology.theta class functionality with this class.
    * TODO: add FITS I/O methods
    * TODO: add ``normalize`` argument to ``__init__`` with default ``True``?
    * TODO: ``__call__`` doesn't show up in the html API docs, but it should:
      https://github.com/astropy/astropy/pull/2135
    """

    def __init__(self, rad, dp_domega, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):

        self._rad = Angle(rad).to('radian')
        self._dp_domega = Quantity(dp_domega).to('sr^-1')

        assert self._rad.ndim == self._dp_domega.ndim == 1
        assert self._rad.shape == self._dp_domega.shape

        # Store input arrays as quantities in default internal units
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to('radian^-1')
        self._spline_kwargs = spline_kwargs

        self._compute_splines(spline_kwargs)

    @classmethod
    def from_shape(cls, shape, width, rad):
        """Make TablePSF objects with commonly used shapes.

        This function is mostly useful for examples and testing.

        Parameters
        ----------
        shape : {'disk', 'gauss'}
            PSF shape.
        width : `~astropy.units.Quantity` with angle units
            PSF width angle (radius for disk, sigma for Gauss).
        rad : `~astropy.units.Quantity` with angle units
            Offset angle

        Returns
        -------
        psf : `TablePSF`
            Table PSF

        Examples
        --------
        >>> import numpy as np
        >>> from astropy.coordinates import Angle
        >>> from gammapy.irf import TablePSF
        >>> TablePSF.from_shape(shape='gauss', width='0.2 deg',
        ...                     rad=Angle(np.linspace(0, 0.7, 100), 'deg'))
        """
        width = Angle(width)
        rad = Angle(rad)

        if shape == 'disk':
            amplitude = 1 / (np.pi * width.radian ** 2)
            psf_value = np.where(rad < width, amplitude, 0)
        elif shape == 'gauss':
            gauss2d_pdf = Gauss2DPDF(sigma=width.radian)
            psf_value = gauss2d_pdf(rad.radian)
        else:
            raise ValueError('Invalid shape: {}'.format(shape))

        psf_value = Quantity(psf_value, 'sr^-1')

        return cls(rad, psf_value)

    def info(self):
        """Print basic info."""
        ss = array_stats_str(self._rad.degree, 'offset')
        ss += 'integral = {}\n'.format(self.integral())

        for containment in [50, 68, 80, 95]:
            radius = self.containment_radius(0.01 * containment)
            ss += ('containment radius {} deg for {}%\n'
                   .format(radius.degree, containment))

        return ss

    # TODO: remove because it's not flexible enough?
    def __call__(self, lon, lat):
        """Evaluate PSF at a 2D position.

        The PSF is centered on ``(0, 0)``.

        Parameters
        ----------
        lon, lat : `~astropy.coordinates.Angle`
            Longitude / latitude position

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        center = SkyCoord(0, 0, unit='radian')
        point = SkyCoord(lon, lat)
        rad = center.separation(point)
        return self.evaluate(rad)

    def kernel(self, reference, containment=0.99, normalize=True,
               discretize_model_kwargs=dict(factor=10)):
        """
        Make a 2-dimensional kernel image.

        The kernel image is evaluated on a cartesian grid defined by the
        reference sky image.

        Parameters
        ----------
        reference : `~gammapy.image.SkyImage` or `~gammapy.cube.SkyCube`
            Reference sky image or sky cube defining the spatial grid.
        containment : float
            Minimal containment fraction of the kernel image.
        normalize : bool
            Whether to normalize the kernel.

        Returns
        -------
        kernel : `~astropy.units.Quantity`
            Kernel 2D image of Quantities
        """
        from ..cube import SkyCube
        rad_max = self.containment_radius(containment)

        if isinstance(reference, SkyCube):
            reference = reference.sky_image_ref

        pixel_size = reference.wcs_pixel_scale()[0]

        def _model(x, y):
            """Model in the appropriate format for discretize_model."""
            rad = np.sqrt(x * x + y * y) * pixel_size
            return self.evaluate(rad)

        npix = int(rad_max.radian / pixel_size.radian)
        pix_range = (-npix, npix + 1)

        kernel = discretize_oversample_2D(_model, x_range=pix_range, y_range=pix_range,
                                          **discretize_model_kwargs)
        if normalize:
            kernel = kernel / kernel.sum()

        return kernel

    def evaluate(self, rad, quantity='dp_domega'):
        r"""Evaluate PSF.

        The following PSF quantities are available:

        * 'dp_domega': PDF per 2-dim solid angle :math:`\Omega` in sr^-1

            .. math:: \frac{dP}{d\Omega}

        * 'dp_dr': PDF per 1-dim offset :math:`r` in radian^-1

            .. math:: \frac{dP}{dr} = 2 \pi r \frac{dP}{d\Omega}

        Parameters
        ----------
        rad : `~astropy.coordinates.Angle`
            Offset wrt source position
        quantity : {'dp_domega', 'dp_dr'}
            Which PSF quantity?

        Returns
        -------
        psf_value : `~astropy.units.Quantity`
            PSF value
        """
        rad = Angle(rad)

        shape = rad.shape
        x = np.array(rad.radian).flat

        if quantity == 'dp_domega':
            y = self._dp_domega_spline(x)
            unit = 'sr^-1'
        elif quantity == 'dp_dr':
            y = self._dp_dr_spline(x)
            unit = 'radian^-1'
        else:
            ss = 'Invalid quantity: {}\n'.format(quantity)
            ss += "Choose one of: 'dp_domega', 'dp_dr'"
            raise ValueError(ss)

        y = np.clip(a=y, a_min=0, a_max=None)
        return Quantity(y, unit).reshape(shape)

    def integral(self, rad_min=None, rad_max=None):
        """Compute PSF integral, aka containment fraction.

        Parameters
        ----------
        rad_min, rad_max : `~astropy.units.Quantity` with angle units
            Offset angle range

        Returns
        -------
        integral : float
            PSF integral
        """
        if rad_min is None:
            rad_min = self._rad[0]
        else:
            rad_min = Angle(rad_min)

        if rad_max is None:
            rad_max = self._rad[-1]
        else:
            rad_max = Angle(rad_max)

        rad_min = self._rad_clip(rad_min)
        rad_max = self._rad_clip(rad_max)

        cdf_min = self._cdf_spline(rad_min)
        cdf_max = self._cdf_spline(rad_max)

        return cdf_max - cdf_min

    def containment_radius(self, fraction):
        """Containment radius.

        Parameters
        ----------
        fraction : array_like
            Containment fraction (range 0 .. 1)

        Returns
        -------
        rad : `~astropy.coordinates.Angle`
            Containment radius angle
        """
        rad = self._ppf_spline(fraction)
        return Angle(rad, 'radian').to('deg')

    def normalize(self):
        """Normalize PSF to unit integral.

        Computes the total PSF integral via the :math:`dP / dr` spline
        and then divides the :math:`dP / dr` array.
        """
        integral = self.integral()

        self._dp_dr /= integral

        # Don't divide by 0
        EPS = 1e-6
        rad = np.clip(self._rad.radian, EPS, None)
        rad = Quantity(rad, 'radian')
        self._dp_domega = self._dp_dr / (2 * np.pi * rad)
        self._compute_splines(self._spline_kwargs)

    def broaden(self, factor, normalize=True):
        r"""Broaden PSF by scaling the offset array.

        For a broadening factor :math:`f` and the offset
        array :math:`r`, the offset array scaled
        in the following way:

        .. math::
            r_{new} = f \times r_{old}
            \frac{dP}{dr}(r_{new}) = \frac{dP}{dr}(r_{old})

        Parameters
        ----------
        factor : float
            Broadening factor
        normalize : bool
            Normalize PSF after broadening
        """
        self._rad *= factor
        # We define broadening such that self._dp_domega remains the same
        # so we only have to re-compute self._dp_dr and the slines here.
        self._dp_dr = (2 * np.pi * self._rad * self._dp_domega).to('radian^-1')
        self._compute_splines(self._spline_kwargs)

        if normalize:
            self.normalize()

    def plot_psf_vs_rad(self, ax=None, quantity='dp_domega', **kwargs):
        """Plot PSF vs radius.

        TODO: describe PSF ``quantity`` argument in a central place and link to it from here.
        """
        import matplotlib.pyplot as plt
        ax = plt.gca() if ax is None else ax

        x = self._rad.to('deg')
        y = self.evaluate(self._rad, quantity)

        ax.plot(x.value, y.value, **kwargs)
        ax.loglog()
        ax.set_xlabel('Radius ({})'.format(x.unit))
        ax.set_ylabel('PSF ({})'.format(y.unit))

    def _compute_splines(self, spline_kwargs=DEFAULT_PSF_SPLINE_KWARGS):
        """Compute two splines representing the PSF.

        * `_dp_domega_spline` is used to evaluate the 2D PSF.
        * `_dp_dr_spline` is not really needed for most applications,
          but is available via `eval`.
        * `_cdf_spline` is used to compute integral and for normalisation.
        * `_ppf_spline` is used to compute containment radii.
        """
        from scipy.interpolate import UnivariateSpline

        # Compute spline and normalize.
        x, y = self._rad.value, self._dp_domega.value
        self._dp_domega_spline = UnivariateSpline(x, y, **spline_kwargs)

        x, y = self._rad.value, self._dp_dr.value
        self._dp_dr_spline = UnivariateSpline(x, y, **spline_kwargs)

        # We use the terminology for scipy.stats distributions
        # http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html#common-methods

        # cdf = "cumulative distribution function"
        self._cdf_spline = self._dp_dr_spline.antiderivative()

        # ppf = "percent point function" (inverse of cdf)
        # Here's a discussion on methods to compute the ppf
        # http://mail.scipy.org/pipermail/scipy-user/2010-May/025237.html
        x = self._rad.value
        y = self.integral(Angle(0, 'rad'), self._rad)

        # This is a hack to stabilize the univariate spline. Only use the first
        # i entries, where the integral is srictly increasing, to build the spline.
        i = (np.diff(y) <= 0).argmax()
        i = len(y) if i == 0 else i
        self._ppf_spline = UnivariateSpline(y[:i], x[:i], **spline_kwargs)

    def _rad_clip(self, rad):
        """Clip to radius support range, because spline extrapolation is unstable."""
        rad = Angle(rad, 'radian').radian
        rad = np.clip(rad, 0, self._rad[-1].radian)
        return rad
示例#8
0
 def get_full_distance_curve(self, resolution=1000):
     vel_curve, timeline = self.get_full_velocity_curve(
         resolution=resolution)
     spl = UnivariateSpline(timeline, vel_curve, s=0)
     ispl = spl.antiderivative()
     return ispl(timeline), timeline
    if(len(curr_data) <= 3):
        curr_data = np.concatenate([curr_data, np.zeros((3,num_params))])

    time = np.arange(0, len(curr_data), 1) # the sample 'times' (0 to number of samples)

    acc_X = curr_data[:,0]
    acc_Y = curr_data[:,1]
    acc_Z = curr_data[:,2]

    # fit 2nd the antiderivative

    # the interpolation representation
    tck_X = UnivariateSpline(time, acc_X, s=0)

    # integrals
    tck_X.integral = tck_X.antiderivative()
    tck_X.integral_2 = tck_X.antiderivative(2)

    # the interpolation representation
    tck_Y = UnivariateSpline(time, acc_Y, s=0)

    # integrals
    tck_Y.integral = tck_Y.antiderivative()
    tck_Y.integral_2 = tck_Y.antiderivative(2)

    # the interpolation representation
    tck_Z = UnivariateSpline(time, acc_Z, s=0)

    # integrals
    tck_Z.integral = tck_Z.antiderivative()
    tck_Z.integral_2 = tck_Z.antiderivative(2)
def preprocess(filename, num_resamplings=25):

    # read data
    #filename = "../data/MarieTherese_jul31_and_Aug07_all.pkl"

    pkl_file = open(filename, 'rb')
    data1 = cPickle.load(pkl_file)
    num_strokes = len(data1)

    # get the unique stroke labels, map to class labels (ints) for later using dictionary
    stroke_dict = dict()
    value_index = 0
    for i in range(0, num_strokes):
        current_key = data1[i][0]
        if current_key not in stroke_dict:
            stroke_dict[current_key] = value_index
            value_index = value_index + 1

# save the dictionary to file, for later use
    dict_filename = "../data/stroke_label_mapping.pkl"
    dict_file = open(dict_filename, 'wb')
    pickle.dump(stroke_dict, dict_file)

    # - smooth data
    # 	for each stroke, get the vector of data, smooth/interpolate it over time, store sampling from smoothed signal in vector
    # - sample at regular intervals (1/30 of total time, etc.) -> input vector X

    num_params = len(data1[0][1][0])  #accelx, accely, etc.
    #num_params = 16 #accelx, accely, etc.

    # re-sample the interpolated spline this many times (25 or so seems ok, since most letters have this many points)

    # build an output array large enough to hold the vectors for each stroke and the (unicode -> int) stroke value (1 elts)
    #        output_array = np.zeros((num_strokes, (num_resamplings_2 + num_resamplings) * num_params + 1))
    output_array = np.zeros(
        (num_strokes, (5 * num_resamplings) * num_params + 1))
    print output_array.size

    print filename
    print num_params
    print num_resamplings_2
    print

    for i in range(0, num_strokes):

        # how far?
        if (i % 100 == 0):
            print float(i) / num_strokes

        X_matrix = np.zeros(
            (num_params, num_resamplings * 5)
        )  # the array to store in (using original data and 2 derivs, 2 integrals)

        # the array to store reshaped resampled vector in
        X_2_vector_scaled = np.zeros((num_params, num_resamplings_2))

        # the array to store the above 2 concatenated
        #		concatenated_X_X_2 = np.zeros((num_params, num_resamplings_2 + num_resamplings))
        concatenated_X_X_2 = np.zeros(
            (num_params, num_resamplings * 5)
        )  # the array to store in (using original data and 2 derivs, 2 integrals)

        # for each parameter (accelX, accelY, ...)

        # map the unicode character to int
        curr_stroke_val = stroke_dict[data1[i][0]]

        #print(len(curr_stroke))
        #print(curr_stroke[0])
        #print(curr_stroke[1])

        curr_data = data1[i][1]

        # fix if too short for interpolation - pad current data with 3 zeros
        if (len(curr_data) <= 3):
            curr_data = np.concatenate([curr_data, np.zeros((3, num_params))])

        time = np.arange(0, len(curr_data),
                         1)  # the sample 'times' (0 to number of samples)
        time_new = np.arange(0, len(curr_data),
                             float(len(curr_data)) /
                             num_resamplings)  # the resampled time points

        for j in range(0, num_params):  # iterate through parameters

            signal = curr_data[:,
                               j]  # one signal (accelx, etc.) to interpolate
            # interpolate the signal using a spline or so, so that arbitrary points can be used
            # (~30 seems reasonable based on data, for example)

            #tck = interpolate.splrep(time, signal, s=0)  # the interpolation represenation
            tck = UnivariateSpline(time, signal, s=0)

            # sample the interpolation num_resamplings times to get values
            # resampled_data = interpolate.splev(time_new, tck, der=0) # the resampled data
            resampled_data = tck(time_new)

            # scale data (center, norm)
            resampled_data = preprocessing.scale(resampled_data)

            # first integral
            tck.integral = tck.antiderivative()
            resampled_data_integral = tck.integral(time_new)

            # scale data (center, norm)
            resampled_data_integral = preprocessing.scale(
                resampled_data_integral)

            # 2nd integral
            tck.integral_2 = tck.antiderivative(2)
            resampled_data_integral_2 = tck.integral_2(time_new)

            # scale data (center, norm)
            resampled_data_integral_2 = preprocessing.scale(
                resampled_data_integral_2)

            # first deriv
            tck.deriv = tck.derivative()
            resampled_data_deriv = tck.deriv(time_new)

            # scale
            resampled_data_deriv = preprocessing.scale(resampled_data_deriv)

            # second deriv
            tck.deriv_2 = tck.derivative(2)
            resampled_data_deriv_2 = tck.deriv_2(time_new)

            #scale
            resampled_data_deriv_2 = preprocessing.scale(
                resampled_data_deriv_2)

            # concatenate into one vector
            concatenated_resampled_data = np.concatenate(
                (resampled_data, resampled_data_integral,
                 resampled_data_integral_2, resampled_data_deriv,
                 resampled_data_deriv_2))

            # store for the correct parameter, to be used later as part of inputs to SVM
            X_matrix[j] = concatenated_resampled_data

            # while we're at it, square vector of resampled data to get a matrix, vectorize the matrix, and store
            #  for each X in list, multiply X by itself -> X_2
            #- vectorize X^2 (e.g. 10 x 10 -> 100 dimensions)
            #			X_2_matrix = np.outer(concatenated_resampled_data, concatenated_resampled_data) # temp matrix for outer product
            #			X_2_vector = np.reshape(X_2_matrix, -1) # reshape into a vector

            #- center and normalize X^2 by mean and standard deviation
            #			X_2_vector_scaled[j] = preprocessing.scale(X_2_vector)

            #- concatenate with input X -> 110 dimensions
            #			concatenated_X_X_2[j] = np.concatenate([X_matrix[j], X_2_vector_scaled[j]])

            # FOR NOW, ONLY USE X, NOT OUTER PRODUCT
            concatenated_X_X_2[j] = X_matrix[j]

        # NOTE, THIS SHOULD REALLY JUST BE A BIG VECTOR FOR EACH STROKE, SO RESHAPE BEFORE ADDING TO OUTPUT LIST
        # ALSO, THE STROKE VALUE SHOULD BE ADDED
        this_sample = np.concatenate(
            (np.reshape(concatenated_X_X_2, -1), np.array([curr_stroke_val])))
        concatenated_samples = np.reshape(this_sample, -1)

        # ADD TO OUTPUT ARRAY
        output_array[i] = concatenated_samples

    print(output_array.size)

    return (output_array)
def preprocess(filename, num_resamplings = 25):

	# read data
	#filename = "../data/MarieTherese_jul31_and_Aug07_all.pkl"

	pkl_file = open(filename, 'rb')
        data1 = cPickle.load(pkl_file)
        num_strokes = len(data1)

        # get the unique stroke labels, map to class labels (ints) for later using dictionary
        stroke_dict = dict()
        value_index = 0
        for i in range(0,num_strokes):
                current_key = data1[i][0]
                if current_key not in stroke_dict:
                        stroke_dict[current_key] = value_index
                        value_index = value_index + 1

        # save the dictionary to file, for later use
        dict_filename = "../data/stroke_label_mapping.pkl"
        dict_file = open(dict_filename, 'wb')
        pickle.dump(stroke_dict, dict_file)

	# - smooth data
	# 	for each stroke, get the vector of data, smooth/interpolate it over time, store sampling from smoothed signal in vector
	# - sample at regular intervals (1/30 of total time, etc.) -> input vector X


	num_params = len(data1[0][1][0]) #accelx, accely, etc.
	#num_params = 16 #accelx, accely, etc.

        # re-sample the interpolated spline this many times (25 or so seems ok, since most letters have this many points)


        # build an output array large enough to hold the vectors for each stroke and the (unicode -> int) stroke value (1 elts)
#        output_array = np.zeros((num_strokes, (num_resamplings_2 + num_resamplings) * num_params + 1))
        output_array = np.zeros((num_strokes, (5 * num_resamplings) * num_params + 1))
        print output_array.size

        print filename
        print num_params
        print num_resamplings_2
        print

	for i in range(0, num_strokes):

                # how far?
                if (i % 100 == 0):
                        print float(i)/num_strokes
	
		X_matrix = np.zeros((num_params, num_resamplings * 5)) # the array to store in (using original data and 2 derivs, 2 integrals)

                # the array to store reshaped resampled vector in
		X_2_vector_scaled = np.zeros((num_params, num_resamplings_2)) 

                # the array to store the above 2 concatenated
#		concatenated_X_X_2 = np.zeros((num_params, num_resamplings_2 + num_resamplings)) 
		concatenated_X_X_2 = np.zeros((num_params, num_resamplings * 5)) # the array to store in (using original data and 2 derivs, 2 integrals)

		# for each parameter (accelX, accelY, ...)

                # map the unicode character to int
                curr_stroke_val = stroke_dict[data1[i][0]]
                                        
                #print(len(curr_stroke))
                #print(curr_stroke[0])
                #print(curr_stroke[1])

		curr_data = data1[i][1]

                # fix if too short for interpolation - pad current data with 3 zeros
                if(len(curr_data) <= 3):
                        curr_data = np.concatenate([curr_data, np.zeros((3,num_params))])

		time = np.arange(0, len(curr_data), 1) # the sample 'times' (0 to number of samples)
		time_new = np.arange(0, len(curr_data), float(len(curr_data))/num_resamplings) # the resampled time points

		for j in range(0, num_params): # iterate through parameters

			signal = curr_data[:,j] # one signal (accelx, etc.) to interpolate
			# interpolate the signal using a spline or so, so that arbitrary points can be used 
			# (~30 seems reasonable based on data, for example)
                        
			#tck = interpolate.splrep(time, signal, s=0)  # the interpolation represenation
                        tck = UnivariateSpline(time, signal, s=0)

			# sample the interpolation num_resamplings times to get values
                        # resampled_data = interpolate.splev(time_new, tck, der=0) # the resampled data
                        resampled_data = tck(time_new)

                        # scale data (center, norm)
                        resampled_data = preprocessing.scale(resampled_data)
                        
                        # first integral
                        tck.integral = tck.antiderivative()
                        resampled_data_integral = tck.integral(time_new)

                        # scale data (center, norm)
                        resampled_data_integral = preprocessing.scale(resampled_data_integral)

                        # 2nd integral
                        tck.integral_2 = tck.antiderivative(2)
                        resampled_data_integral_2 = tck.integral_2(time_new)

                        # scale data (center, norm)
                        resampled_data_integral_2 = preprocessing.scale(resampled_data_integral_2)

                        # first deriv
                        tck.deriv = tck.derivative()
                        resampled_data_deriv = tck.deriv(time_new)

                        # scale
                        resampled_data_deriv = preprocessing.scale(resampled_data_deriv)

                        # second deriv
                        tck.deriv_2 = tck.derivative(2)
                        resampled_data_deriv_2 = tck.deriv_2(time_new)

                        #scale
                        resampled_data_deriv_2 = preprocessing.scale(resampled_data_deriv_2)


                        # concatenate into one vector
                        concatenated_resampled_data = np.concatenate((resampled_data, 
                                                                      resampled_data_integral, 
                                                                      resampled_data_integral_2, 
                                                                      resampled_data_deriv, 
                                                                      resampled_data_deriv_2))
                        
                        # store for the correct parameter, to be used later as part of inputs to SVM
 			X_matrix[j] = concatenated_resampled_data

			# while we're at it, square vector of resampled data to get a matrix, vectorize the matrix, and store
			#  for each X in list, multiply X by itself -> X_2
			#- vectorize X^2 (e.g. 10 x 10 -> 100 dimensions)
#			X_2_matrix = np.outer(concatenated_resampled_data, concatenated_resampled_data) # temp matrix for outer product
#			X_2_vector = np.reshape(X_2_matrix, -1) # reshape into a vector

			#- center and normalize X^2 by mean and standard deviation
#			X_2_vector_scaled[j] = preprocessing.scale(X_2_vector) 

			#- concatenate with input X -> 110 dimensions
#			concatenated_X_X_2[j] = np.concatenate([X_matrix[j], X_2_vector_scaled[j]])

# FOR NOW, ONLY USE X, NOT OUTER PRODUCT
			concatenated_X_X_2[j] = X_matrix[j]

                # NOTE, THIS SHOULD REALLY JUST BE A BIG VECTOR FOR EACH STROKE, SO RESHAPE BEFORE ADDING TO OUTPUT LIST
                # ALSO, THE STROKE VALUE SHOULD BE ADDED
                this_sample = np.concatenate((np.reshape(concatenated_X_X_2, -1), np.array([curr_stroke_val])))
                concatenated_samples = np.reshape(this_sample, -1)

                # ADD TO OUTPUT ARRAY
                output_array[i] = concatenated_samples
        
        print(output_array.size)
        
	return(output_array)
def get_distance_from_vel_curve(vel_profile, timeline):
    spl = UnivariateSpline(timeline, vel_profile, s=0)
    ispl = spl.antiderivative()
    return ispl(timeline)
        curr_data = np.concatenate([curr_data, np.zeros((3, num_params))])

    time = np.arange(0, len(curr_data),
                     1)  # the sample 'times' (0 to number of samples)

    acc_X = curr_data[:, 0]
    acc_Y = curr_data[:, 1]
    acc_Z = curr_data[:, 2]

    # fit 2nd the antiderivative

    # the interpolation representation
    tck_X = UnivariateSpline(time, acc_X, s=0)

    # integrals
    tck_X.integral = tck_X.antiderivative()
    tck_X.integral_2 = tck_X.antiderivative(2)

    # the interpolation representation
    tck_Y = UnivariateSpline(time, acc_Y, s=0)

    # integrals
    tck_Y.integral = tck_Y.antiderivative()
    tck_Y.integral_2 = tck_Y.antiderivative(2)

    # the interpolation representation
    tck_Z = UnivariateSpline(time, acc_Z, s=0)

    # integrals
    tck_Z.integral = tck_Z.antiderivative()
    tck_Z.integral_2 = tck_Z.antiderivative(2)