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)
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)
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
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
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
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
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
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)