Exemplo n.º 1
0
    def __init__(self, **kwargs):
        self._log = logging.getLogger(self.__class__.__name__)

        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self._displacement = None
        self._displacement_px_var = None
        self._phi = None
        self._theta = None
        self._los_factors = None
        self.cols = 0
        self.rows = 0
        self.los = LOSUnitVectors(scene=self)

        self._elevation = {}

        frame_config = kwargs.pop('frame_config', FrameConfig())

        for fattr in ('llLat', 'llLon', 'dLat', 'dLon'):
            coord = kwargs.pop(fattr, None)
            if coord is not None:
                frame_config.__setattr__(fattr, coord)
        self.frame = Frame(scene=self, config=frame_config)

        for attr in ('displacement', 'displacement_px_var', 'theta', 'phi'):
            data = kwargs.pop(attr, None)
            if data is not None:
                self.__setattr__(attr, data)
Exemplo n.º 2
0
class SandboxSource(Object):

    lat = Float.T(default=0.0, help="Latitude in [deg]")
    lon = Float.T(default=0.0, help="Longitude in [deg]")
    easting = Float.T(default=0.0, help="Easting in [m]")
    northing = Float.T(default=0.0, help="Northing in [m]")
    depth = Float.T(default=1.0 * km, help="Depth in [m]")

    def __init__(self, *args, **kwargs):
        Object.__init__(self, *args, **kwargs)
        self._cached_result = None
        self.evParametersChanged = Subject()
        self._sandbox = None

    def parametersUpdated(self):
        self._cached_result = None
        self.evParametersChanged.notify()

    def getSandboxOffset(self):
        if not self._sandbox or (self.lat == 0.0 and self.lon == 0.0):
            return 0.0, 0.0
        return od.latlon_to_ne_numpy(
            self._sandbox.frame.llLat, self._sandbox.frame.llLon, self.lat, self.lon
        )

    def getParametersArray(self):
        raise NotImplementedError
Exemplo n.º 3
0
    def __init__(self, config=SceneConfig(), **kwargs):
        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self.config = config
        self.meta = self.config.meta

        BaseScene.__init__(self, frame_config=self.config.frame, **kwargs)

        # wiring special methods
        self.import_data = self._import_data
        self.load = self._load
Exemplo n.º 4
0
    def __init__(self, scene, **kwargs):
        self.evPlotChanged = Subject()
        self._scene = scene
        self._data = None

        self.fig = None
        self.ax = None
        self._show_plt = False
        self._colormap_symmetric = True

        self.title = 'unnamed'

        self._log = logging.getLogger(self.__class__.__name__)
Exemplo n.º 5
0
    def __init__(self, scene, config=None):
        self.evChanged = Subject()
        self._scene = scene
        self._log = scene._log.getChild('Frame')

        self.N = None
        self.E = None

        self.llEutm = None
        self.llNutm = None
        self.utm_zone = None
        self.utm_zone_letter = None
        self._meter_grid = None

        self._updateConfig(config or FrameConfig())
        self._scene.evConfigChanged.subscribe(self._updateConfig)
        self._scene.evChanged.subscribe(self.updateExtent)
Exemplo n.º 6
0
class SandboxSource(Object):

    easting = Float.T(help='Easting in [m]')
    northing = Float.T(help='Northing in [m]')
    depth = Float.T(help='Depth in [m]')

    def __init__(self, *args, **kwargs):
        Object.__init__(self, *args, **kwargs)
        self._cached_result = None
        self.evParametersChanged = Subject()

    def parametersUpdated(self):
        self._cached_result = None
        self.evParametersChanged.notify()

    def getParametersArray(self):
        raise NotImplemented
Exemplo n.º 7
0
    def __init__(self, scene, config=CovarianceConfig()):
        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self.frame = scene.frame
        self.quadtree = scene.quadtree
        self.scene = scene
        self.nthreads = 0
        self._noise_data = None
        self._powerspec1d_cached = None
        self._powerspec2d_cached = None
        self._powerspec3d_cached = None
        self._noise_data_grid = None
        self._initialized = False
        self._log = scene._log.getChild("Covariance")

        self.setConfig(config)
        self.quadtree.evChanged.subscribe(self._clear)
        self.scene.evConfigChanged.subscribe(self.setConfig)
Exemplo n.º 8
0
    def __init__(self, config=None, **kwargs):
        self.evChanged = Subject()
        self.evModelUpdated = Subject()
        self.evConfigChanged = Subject()

        self.config = config if config else SandboxSceneConfig()
        BaseScene.__init__(self, frame_config=self.config.frame, **kwargs)

        self.reference = None
        self._los_factors = None

        for attr in ['theta', 'phi']:
            data = kwargs.pop(attr, None)
            if data is not None:
                self.__setattr__(attr, data)

        self.setExtent(self.config.extent_east, self.config.extent_north)

        if self.config.reference_scene is not None:
            self.loadReferenceScene(self.config.reference_scene)
Exemplo n.º 9
0
class Frame(object):
    """ Frame holding geographical references for :class:`kite.scene.Scene`

    The pixel spacing is given by ``dE`` and ``dN`` which can meters or degree.
    """
    def __init__(self, scene, config=None):
        self.evChanged = Subject()
        self._scene = scene
        self._log = scene._log.getChild('Frame')

        self.N = None
        self.E = None

        self.llEutm = None
        self.llNutm = None
        self.utm_zone = None
        self.utm_zone_letter = None
        self._meter_grid = None

        self._updateConfig(config or FrameConfig())
        self._scene.evConfigChanged.subscribe(self._updateConfig)
        self._scene.evChanged.subscribe(self.updateExtent)

    def _updateConfig(self, config=None):
        if config is not None:
            self.config = config
        elif self.config != self._scene.config.frame:
            self.config = self._scene.config.frame
        else:
            return

        if self.config.old_import:
            self._log.warning('Importing an old kite format...')
            self._log.warning('Please check your pixel spacing - dE, dN!')
        self.updateExtent()

    def updateExtent(self):
        if self._scene.cols == 0 or self._scene.rows == 0:
            return

        self.cols = self._scene.cols
        self.rows = self._scene.rows

        (self.llEutm, self.llNutm, self.utm_zone,
         self.utm_zone_letter) = utm.from_latlon(self.llLat, self.llLon)

        self.E = None
        self.N = None

        self.gridE = None
        self.gridN = None
        self._meter_grid = None
        self.coordinates = None

        self.config.regularize()
        self.evChanged.notify()

    @property
    def llLat(self):
        return self.config.llLat

    @llLat.setter
    def llLat(self, llLat):
        self.config.llLat = llLat
        self.updateExtent()

    @property
    def llLon(self):
        return self.config.llLon

    @llLon.setter
    def llLon(self, llLon):
        self.config.llLon = llLon
        self.updateExtent()

    @property
    def dN(self):
        return self.config.dN

    @dN.setter
    def dN(self, dN):
        self.config.dN = dN
        self.updateExtent()

    @property
    def dE(self):
        return self.config.dE

    @dE.setter
    def dE(self, dE):
        self.config.dE = dE
        self.updateExtent()

    @property
    def dEmeter(self):
        if self.isMeter():
            return self.dE

        _, dEmeter = latlon_to_ne(self.llLat, self.llLon, self.llLat,
                                  self.llLon + self.dE * self.cols)
        return dEmeter / self.cols

    @property
    def dNmeter(self):
        if self.isMeter():
            return self.dN
        dNmeter, _ = latlon_to_ne(self.llLat, self.llLon,
                                  self.llLat + self.dN * self.rows, self.llLon)
        return dNmeter / self.rows

    @property
    def dEdegree(self):
        if self.isDegree():
            return self.dE

        lat, lon = ne_to_latlon(self.llLat, self.llLon, 0.,
                                self.dE * self.cols)
        distLon = lon - self.llLon
        return distLon / self.cols

    @property
    def dNdegree(self):
        if self.isDegree():
            return self.dE

        lat, lon = ne_to_latlon(self.llLat, self.llLon, self.dN * self.rows,
                                0.)
        distLat = lat - self.llLat
        return distLat / self.rows

    @property
    def spacing(self):
        return self.config.spacing

    @spacing.setter
    def spacing(self, unit):
        self.config.spacing = unit

    @property_cached
    def E(self):
        return num.arange(self.cols) * self.dE

    @property_cached
    def Emeter(self):
        return num.arange(self.cols) * self.dEmeter

    @property_cached
    def N(self):
        return num.arange(self.rows) * self.dN

    @property
    def lengthE(self):
        return self.cols * self.dE

    @property
    def lengthN(self):
        return self.rows * self.dN

    @property_cached
    def Nmeter(self):
        return num.arange(self.rows) * self.dNmeter

    @property_cached
    def gridE(self):
        """ Grid holding local east coordinates of all pixels in ``NxM`` matrix
            of :attr:`~kite.Scene.displacement`.

        :type: :class:`numpy.ndarray`, size ``NxM``
        """
        valid_data = num.isnan(self._scene.displacement)
        gridE = num.repeat(self.E[num.newaxis, :], self.rows, axis=0)
        return num.ma.masked_array(gridE, valid_data, fill_value=num.nan)

    @property_cached
    def gridN(self):
        """ Grid holding local north coordinates of all pixels in ``NxM`` matrix
            of :attr:`~kite.Scene.displacement`.

        :type: :class:`numpy.ndarray`, size ``NxM``
        """
        valid_data = num.isnan(self._scene.displacement)
        gridN = num.repeat(self.N[:, num.newaxis], self.cols, axis=1)
        return num.ma.masked_array(gridN, valid_data, fill_value=num.nan)

    def _calculateMeterGrid(self):
        if self.isMeter():
            raise ValueError('Frame is defined in meter! '
                             'Use gridE and gridN for meter grids')

        if self._meter_grid is None:
            self._log.debug('Transforming latlon grid to meters...')
            gridN, gridE = latlon_to_ne_numpy(
                self.llLat, self.llLon, self.llLat + self.gridN.data.ravel(),
                self.llLon + self.gridE.data.ravel())

            valid_data = num.isnan(self._scene.displacement)
            gridE = num.ma.masked_array(gridE.reshape(self.gridE.shape),
                                        valid_data,
                                        fill_value=num.nan)
            gridN = num.ma.masked_array(gridN.reshape(self.gridN.shape),
                                        valid_data,
                                        fill_value=num.nan)
            self._meter_grid = (gridE, gridN)

        return self._meter_grid

    @property_cached
    def gridEmeter(self):
        if self.isMeter():
            return self.gridE

        return self._calculateMeterGrid()[0]

    @property_cached
    def gridNmeter(self):
        if self.isMeter():
            return self.gridN
        return self._calculateMeterGrid()[1]

    @property_cached
    def coordinates(self):
        """ Local east and north coordinates of all pixels in
           ``Nx2`` matrix.

        :type: :class:`numpy.ndarray`, size ``Nx2``
        """
        coords = num.empty((self.rows * self.cols, 2))
        coords[:, 0] = num.repeat(self.E[num.newaxis, :], self.rows,
                                  axis=0).flatten()
        coords[:, 1] = num.repeat(self.N[:, num.newaxis], self.cols,
                                  axis=1).flatten()

        if self.isMeter():
            coords = ne_to_latlon(self.llLat, self.llLon, *coords.T)
            coords = num.array(coords).T

        else:
            coords[:, 0] += self.llLon
            coords[:, 1] += self.llLat

        return coords

    @property_cached
    def coordinatesMeter(self):
        """ Local east and north coordinates [m] of all pixels in
           ``NxM`` matrix.

        :type: :class:`numpy.ndarray`, size ``NxM``
        """
        coords = num.empty((self.rows * self.cols, 2))
        coords[:, 0] = num.repeat(self.Emeter[num.newaxis, :],
                                  self.rows,
                                  axis=0).flatten()
        coords[:, 1] = num.repeat(self.Nmeter[:, num.newaxis],
                                  self.cols,
                                  axis=1).flatten()
        return coords

    def mapENMatrix(self, E, N):
        """ Local map coordinates in east and north to matrix
            row and column

        :param E: Easting in local coordinates
        :type E: float
        :param N: Northing in local coordinates
        :type N: float
        :returns: Row and column
        :rtype: tuple (int), (row, column)
        """
        row = round(E / self.dE) if E > 0 else 0
        col = round(N / self.dN) if N > 0 else 0
        return int(row), int(col)

    @property
    def shape(self):
        return self._scene.shape

    def isMeter(self):
        return self.config.spacing == 'meter'

    def isDegree(self):
        return self.config.spacing == 'degree'

    @property
    def npixel(self):
        return self.cols * self.rows

    def __eq__(self, other):
        return self.llLat == other.llLat and\
            self.llLon == other.llLon and\
            self.dE == other.dE and\
            self.dN == other.dN and\
            self.rows == other.rows and\
            self.cols == other.cols
Exemplo n.º 10
0
class BaseScene(object):
    def __init__(self, **kwargs):
        self._log = logging.getLogger(self.__class__.__name__)

        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self._displacement = None
        self._displacement_px_var = None
        self._phi = None
        self._theta = None
        self._los_factors = None
        self.cols = 0
        self.rows = 0
        self.los = LOSUnitVectors(scene=self)

        self._elevation = {}

        frame_config = kwargs.pop('frame_config', FrameConfig())

        for fattr in ('llLat', 'llLon', 'dLat', 'dLon'):
            coord = kwargs.pop(fattr, None)
            if coord is not None:
                frame_config.__setattr__(fattr, coord)
        self.frame = Frame(scene=self, config=frame_config)

        for attr in ('displacement', 'displacement_px_var', 'theta', 'phi'):
            data = kwargs.pop(attr, None)
            if data is not None:
                self.__setattr__(attr, data)

    @property
    def displacement(self):
        """Surface displacement in meter on a regular grid.

        :setter: Set the unwrapped InSAR displacement.
        :getter: Return the displacement matrix.
        :type: :class:`numpy.ndarray`, ``NxM``
        """
        return self._displacement

    @displacement.setter
    def displacement(self, value):
        _setDataNumpy(self, '_displacement', value)
        self.rows, self.cols = self._displacement.shape
        self.evChanged.notify()

    @property
    def displacement_px_var(self):
        """ Variance of the surface displacement per pixel.
        Same dimension as displacement.

        :setter: Set standard deviation of of the displacement.
        :getter: Return the standard deviation matrix.
        :type: :class:`numpy.ndarray`, ``NxM``
        """
        return self._displacement_px_var

    @displacement_px_var.setter
    def displacement_px_var(self, value):
        self._displacement_px_var = value

    @property
    def displacement_mask(self):
        """ Displacement :attr:`numpy.nan` mask

        :type: :class:`numpy.ndarray`, dtype :class:`numpy.bool`
        """
        return ~num.isfinite(self.displacement)

    @property
    def shape(self):
        return self.displacement.shape

    @property
    def phi(self):
        """ Horizontal angle towards satellite :abbr:`line of sight (LOS)`
        in radians counter-clockwise from East.

        .. important ::

            Kite's convention is:

            * :math:`0` is **East**
            * :math:`\\frac{\\pi}{2}` is **North**!

        :setter: Set the phi matrix for scene's displacement, can be ``int``
                 for static look vector.
        :type: :class:`numpy.ndarray`, size same as
               :attr:`~kite.Scene.displacement` or int
        """
        return self._phi

    @phi.setter
    def phi(self, value):
        if isinstance(value, float):
            self._phi = value
        else:
            _setDataNumpy(self, '_phi', value)
        self.phiDeg = None
        self.los_rotation_factors = None
        self.evChanged.notify()

    @property
    def theta(self):
        """ Theta is the look vector elevation angle towards satellite from
        the horizon in radians. Matrix of theta towards satellite's
        :abbr:`line of sight (LOS)`.

        .. important ::

            Kite convention!

            * :math:`-\\frac{\\pi}{2}` is **Down**
            * :math:`\\frac{\\pi}{2}` is **Up**

        :setter: Set the theta matrix for scene's displacement, can be ``int``
                 for static look vector.
        :type: :class:`numpy.ndarray`, size same as
               :attr:`~kite.Scene.displacement` or int
        """
        return self._theta

    @theta.setter
    def theta(self, value):
        if isinstance(value, float):
            self._theta = value
        else:
            _setDataNumpy(self, '_theta', value)
        self.thetaDeg = None
        self.los_rotation_factors = None
        self.evChanged.notify()

    @property_cached
    def thetaDeg(self):
        """ LOS elevation angle in degree, ``NxM`` matrix like
            :class:`kite.Scene.theta`

        :type: :class:`numpy.ndarray`
        """
        return num.rad2deg(self.theta)

    @property_cached
    def phiDeg(self):
        """ LOS horizontal orientation angle in degree,
            counter-clockwise from East,``NxM`` matrix like
            :class:`kite.Scene.phi`

        :type: :class:`numpy.ndarray`
        """
        return num.rad2deg(self.phi)

    @property_cached
    def los_rotation_factors(self):
        """ Trigonometric factors to rotate displacement matrices towards LOS

        Rotation is as follows:

        ..
            displacement_los =\
                (los_rotation_factors[:, :, 0] * -down +
                 los_rotation_factors[:, :, 1] * east +
                 los_rotation_factors[:, :, 2] * north)

        :returns: Factors for rotation
        :rtype: :class:`numpy.ndarray`, ``NxMx3``
        :raises: AttributeError
        """
        if (self.theta.size != self.phi.size):
            raise AttributeError('LOS angles inconsistent with provided'
                                 ' coordinate shape.')
        if self._los_factors is None:
            self._los_factors = num.empty(
                (self.theta.shape[0], self.theta.shape[1], 3))
            self._los_factors[:, :, 0] = num.sin(self.theta)
            self._los_factors[:, :, 1] = num.cos(self.theta)\
                * num.cos(self.phi)
            self._los_factors[:, :, 2] = num.cos(self.theta)\
                * num.sin(self.phi)
        return self._los_factors

    def get_elevation(self, interpolation='nearest_neighbor'):
        assert interpolation in ('nearest_neighbor', 'bivariate')

        if self._elevation.get(interpolation, None) is None:
            self._log.debug('Getting elevation...')
            # region = llLon, urLon, llLat, urLon
            coords = self.frame.coordinates
            lons = coords[:, 0]
            lats = coords[:, 1]

            region = (lons.min(), lons.max(), lats.min(), lats.max())
            if not srtmgl3.covers(region):
                raise AssertionError(
                    'Region is outside of SRTMGL3 topo dataset')

            tile = srtmgl3.get(region)
            if not tile:
                raise AssertionError('Cannot get SRTMGL3 topo dataset')

            if interpolation == 'nearest_neighbor':
                iy = num.rint((lats - tile.ymin) / tile.dy).astype(num.intp)
                ix = num.rint((lons - tile.xmin) / tile.dx).astype(num.intp)

                elevation = tile.data[(iy, ix)]

            elif interpolation == 'bivariate':
                interp = interpolate.RectBivariateSpline(
                    tile.y(), tile.x(), tile.data)
                elevation = interp(lats, lons, grid=False)

            elevation = elevation.reshape(self.rows, self.cols)
            self._elevation[interpolation] = elevation

        return self._elevation[interpolation]

    def __neg__(self):
        ret = copy.deepcopy(self)
        ret.displacement *= -1
        return ret

    def __add__(self, other, copy_obj=True):
        if copy_obj:
            ret = copy.deepcopy(self)
        else:
            ret = self

        if not ret.frame == other.frame:
            raise ValueError('Scene frames do not align!')
        ret.displacement += other.displacement

        tmin = ret.meta.time_master \
            if ret.meta.time_master < other.meta.time_master \
            else other.meta.time_master

        tmax = ret.meta.time_slave \
            if ret.meta.time_slave > other.meta.time_slave \
            else other.meta.time_slave

        ret.meta.time_master = tmin
        ret.meta.time_slave = tmax
        return ret

    def __sub__(self, other):
        return self.__add__(-other)

    def __isub__(self, scene):
        return self.__add__(-scene, copy_obj=False)

    def __iadd__(self, scene):
        return self.__add__(scene, copy_obj=False)
Exemplo n.º 11
0
class Scene(BaseScene):
    """Scene of unwrapped InSAR ground displacements measurements

    :param config: Configuration object
    :type config: :class:`~kite.scene.SceneConfig`, optional

    Optional parameters

    :param displacement: Displacement in [m]
    :type displacement: :class:`numpy.ndarray`, NxM, optional
    :param theta: Theta look angle, see :attr:`BaseScene.theta`
    :type theta: :class:`numpy.ndarray`, NxM, optional
    :param phi: Phi look angle, see :attr:`BaseScene.phi`
    :type phi: :class:`numpy.ndarray`, NxM, optional

    :param llLat: Lower left latitude in [deg]
    :type llLat: float, optional
    :param llLon: Lower left longitude in [deg]
    :type llLon: float, optional
    :param dLat: Pixel spacing in latitude [deg]
    :type dLat: float, optional
    :param dLon: Pixel spacing in longitude [deg]
    :type dLon: float, optional
    """
    def __init__(self, config=SceneConfig(), **kwargs):
        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self.config = config
        self.meta = self.config.meta

        BaseScene.__init__(self, frame_config=self.config.frame, **kwargs)

        # wiring special methods
        self.import_data = self._import_data
        self.load = self._load

    @property_cached
    def quadtree(self):
        """ Instantiates the scene's quadtree.

        :type: :class:`kite.quadtree.Quadtree`
        """
        self._log.debug('Creating kite.Quadtree instance')
        from kite.quadtree import Quadtree
        return Quadtree(scene=self, config=self.config.quadtree)

    @property_cached
    def covariance(self):
        """ Instantiates the scene's covariance attribute.

        :type: :class:`kite.covariance.Covariance`
        """
        self._log.debug('Creating kite.Covariance instance')
        from kite.covariance import Covariance
        return Covariance(scene=self, config=self.config.covariance)

    @property_cached
    def plot(self):
        """ Shows a simple plot of the scene's displacement
        """
        self._log.debug('Creating kite.ScenePlot instance')
        from kite.plot2d import ScenePlot
        return ScenePlot(self)

    def spool(self):
        """ Start the spool user interface :class:`~kite.spool.Spool` to inspect
        the scene.
        """
        if self.displacement is None:
            raise SceneError('Can not display an empty scene.')

        from kite.spool import spool
        spool(scene=self)

    def _testImport(self):
        try:
            self.frame.E
            self.frame.N
            self.frame.gridE
            self.frame.gridN
            self.frame.dE
            self.frame.dN
            self.displacement
            self.theta
            self.phi
        except Exception as e:
            print(e)
            raise ImportError('Something went wrong during import - '
                              'see Exception!')

    def save(self, filename=None):
        """ Save kite scene to kite file structure

        Saves the current scene meta information and UTM frame to a YAML
        (``.yml``) file. Numerical data (:attr:`~kite.Scene.displacement`,
        :attr:`~kite.Scene.theta` and :attr:`~kite.Scene.phi`)
        are saved as binary files from :class:`numpy.ndarray`.

        :param filename: Filenames to save scene to, defaults to
            ' :attr:`~kite.Scene.meta.scene_id` ``_``
            :attr:`~kite.Scene.meta.scene_view`
        :type filename: str, optional
        """
        filename = filename or '%s_%s' % (self.meta.scene_id,
                                          self.meta.scene_view)
        _file, ext = op.splitext(filename)
        filename = _file if ext in ['.yml', '.npz'] else filename

        components = ['displacement', 'theta', 'phi']
        self._log.debug('Saving scene data to %s.npz' % filename)

        num.savez('%s.npz' % (filename),
                  *[getattr(self, arr) for arr in components])
        self.saveConfig('%s.yml' % filename)

    def saveConfig(self, filename):
        _file, ext = op.splitext(filename)
        filename = filename if ext in ['.yml'] else filename + '.yml'
        self._log.debug('Saving scene config to %s' % filename)
        self.config.regularize()
        self.config.dump(filename='%s' % filename,
                         header='kite.Scene YAML Config')

    @dynamicmethod
    def _load(self, filename):
        """ Load a kite scene from file ``filename.[npz,yml]``
        structure.

        :param filename: Filenames the scene data is saved under
        :type filename: str
        :returns: Scene object from data resources
        :rtype: :class:`~kite.Scene`
        """
        scene = self
        components = ['displacement', 'theta', 'phi']

        basename = op.splitext(filename)[0]
        scene._log.debug('Loading from %s[.npz,.yml]' % basename)
        try:
            data = num.load('%s.npz' % basename)
            for i, comp in enumerate(components):
                scene.__setattr__(comp, data['arr_%d' % i])
        except IOError:
            raise UserIOWarning('Could not load data from %s.npz' % basename)

        try:
            scene.load_config('%s.yml' % basename)
        except IOError:
            raise UserIOWarning('Could not load %s.yml' % basename)

        scene.meta.filename = op.basename(filename)
        scene._testImport()
        return scene

    load = staticmethod(_load)

    def load_config(self, filename):
        self._log.debug('Loading config from %s' % filename)
        self.config = guts.load(filename=filename)
        self.meta = self.config.meta

        self.evConfigChanged.notify()

    @dynamicmethod
    def _import_data(self, path, **kwargs):
        """ Import displacement data from foreign file format.

        :param path: Filename of resource to import
        :type path: str
        :param kwargs: keyword arguments passed to import function
        :type kwargs: dict
        :returns: Scene from path
        :rtype: :class:`~kite.Scene`
        :raises: TypeError
        """
        scene = self
        if not op.isfile(path) or op.isdir(path):
            raise ImportError('File %s does not exist!' % path)
        data = None

        for mod in scene_io.__all__:
            module = eval('scene_io.%s(scene)' % mod)
            if module.validate(path, **kwargs):
                scene._log.debug('Importing %s using %s module' % (path, mod))
                data = module.read(path, **kwargs)
                break
        if data is None:
            raise ImportError('Could not recognize format for %s' % path)

        scene.meta.filename = op.basename(path)
        return scene._import_from_dict(scene, data)

    _import_data.__doc__ += \
        '\nSupported import modules are **%s**.\n'\
        % (', ').join(scene_io.__all__)
    for mod in scene_io.__all__:
        _import_data.__doc__ += '\n**%s**\n\n' % mod
        _import_data.__doc__ += eval('scene_io.%s.__doc__' % mod)
    import_data = staticmethod(_import_data)

    @staticmethod
    def _import_from_dict(scene, data):
        for sk in ['theta', 'phi', 'displacement']:
            setattr(scene, sk, data[sk])

        for fk, fv in data['frame'].items():
            setattr(scene.frame, fk, fv)

        for mk, mv in data['meta'].items():
            if mv is not None:
                setattr(scene.meta, mk, mv)
        scene.meta.extra.update(data['extra'])
        scene.frame.updateExtent()

        scene._testImport()
        return scene

    def __str__(self):
        return self.config.__str__()
Exemplo n.º 12
0
class Frame(object):
    ''' UTM frame holding geographical references for :class:`kite.scene.Scene`
    '''
    evChanged = Subject()

    def __init__(self, scene, config=FrameConfig()):
        self._scene = scene
        self._log = scene._log.getChild('Frame')

        self.extentE = 0.
        self.extentN = 0.
        self.spherical_distortion = 0.
        self.urE = 0.
        self.urN = 0.
        self.llEutm = None
        self.llNutm = None
        self.utm_zone = None
        self.llN = None
        self.llE = None
        self.N = None
        self.E = None

        self.offsetE = 0.
        self.offsetN = 0.

        self._updateConfig(config)
        self._scene.evConfigChanged.subscribe(self._updateConfig)
        self._scene.evChanged.subscribe(self.updateExtent)

    def _updateConfig(self, config=None):
        if config is not None:
            self.config = config
        elif self.config != self._scene.config.frame:
            self.config = self._scene.config.frame
        else:
            return

        self.updateExtent()
        self.evChanged.notify()

    def updateExtent(self):
        if self._scene.cols == 0 or self._scene.rows == 0:
            return

        self.llEutm, self.llNutm, self.utm_zone, self.utm_zone_letter = \
            utm.from_latlon(self.llLat, self.llLon)

        self.cols = self._scene.cols
        self.rows = self._scene.rows

        urlat = self.llLat + self.dLat * self.rows
        urlon = self.llLon + self.dLon * self.cols
        self.urEutm, self.urNutm, _, _ = utm.from_latlon(
            urlat, urlon, self.utm_zone)

        # Width at the bottom of the scene
        self.extentE = greatCircleDistance(self.llLat, self.llLon, self.llLat,
                                           urlon)
        self.extentN = greatCircleDistance(self.llLat, self.llLon, urlat,
                                           self.llLon)

        # Width at the N' top of the scene
        extentE_top = greatCircleDistance(urlat, self.llLon, urlat, urlon)
        self.spherical_distortion = num.abs(self.extentE - extentE_top)

        self.dE = (self.extentE + extentE_top) / (2 * self.cols)
        self.dN = self.extentN / self.rows

        self.E = num.arange(self.cols) * self.dE + self.offsetE
        self.N = num.arange(self.rows) * self.dN + self.offsetN

        self.llE = 0
        self.llN = 0
        self.urE = self.E.max()
        self.urN = self.N.max()

        self.gridE = None
        self.gridN = None
        self.coordinates = None

        self.config.regularize()
        return

    @property
    def llLat(self):
        return self.config.llLat

    @llLat.setter
    def llLat(self, llLat):
        self.config.llLat = llLat
        self._llLat = llLat

    @property
    def llLon(self):
        return self.config.llLon

    @llLon.setter
    def llLon(self, llLon):
        self.config.llLon = llLon
        self._llLon = llLon

    @property
    def dLat(self):
        return self.config.dLat

    @dLat.setter
    def dLat(self, dLat):
        self.config.dLat = dLat

    @property
    def dLon(self):
        return self.config.dLon

    @dLon.setter
    def dLon(self, dLon):
        self.config.dLon = dLon

    @property
    def dN(self):
        return self.config.dN

    @dN.setter
    def dN(self, dN):
        self.config.dN = dN

    @property
    def dE(self):
        return self.config.dE

    @dE.setter
    def dE(self, dE):
        self.config.dE = dE

    @property_cached
    def gridE(self):
        ''' UTM grid holding eastings of all pixels in ``NxM`` matrix
            of :attr:`~kite.Scene.displacement`.

        :type: :class:`numpy.ndarray`, size ``NxM``
        '''
        valid_data = num.isnan(self._scene.displacement)
        gridE = num.repeat(self.E[num.newaxis, :], self.rows, axis=0)
        return num.ma.masked_array(gridE, valid_data, fill_value=num.nan)

    @property_cached
    def gridN(self):
        ''' UTM grid holding northings of all pixels in ``NxM`` matrix
            of :attr:`~kite.Scene.displacement`.

        :type: :class:`numpy.ndarray`, size ``NxM``
        '''
        valid_data = num.isnan(self._scene.displacement)
        gridN = num.repeat(self.N[:, num.newaxis], self.cols, axis=1)
        return num.ma.masked_array(gridN, valid_data, fill_value=num.nan)

    @property_cached
    def coordinates(self):
        coords = num.empty((self.rows * self.cols, 2))
        coords[:, 0] = num.repeat(self.E[num.newaxis, :], self.rows,
                                  axis=0).flatten()
        coords[:, 1] = num.repeat(self.N[:, num.newaxis], self.cols,
                                  axis=1).flatten()
        return coords

    def setENOffset(self, east, north):
        '''Set scene offsets in local cartesian coordinates.

        :param east: East offset in [m]
        :type east: float, :class:`numpy.ndarray` or None
        :param north: North offset in [m]
        :type north: float, :class:`numpy.ndarray` or None
        '''
        self.offsetE = east
        self.offsetN = north
        self.updateExtent()

    def setLatLonReference(self, lat, lon):
        pass

    def mapMatrixEN(self, row, col):
        ''' Maps matrix row, column to local easting and northing.

        :param row: Matrix row number
        :type row: int
        :param col: Matrix column number
        :type col: int
        :returns: Easting and northing in local coordinates
        :rtype: tuple (float), (easting, northing)
        '''
        return row * self.dE, col * self.dN

    def mapENMatrix(self, E, N):
        ''' Maps local coordinates (easting and northing) to matrix
            row and column

        :param E: Easting in local coordinates
        :type E: float
        :param N: Northing in local coordinates
        :type N: float
        :returns: Row and column
        :rtype: tuple (int), (row, column)
        '''
        row = int(E / self.dE) if E > 0 else 0
        col = int(N / self.dN) if N > 0 else 0
        return row, col

    def __eq__(self, other):
        return self.llLat == other.llLat and\
            self.llLon == other.llLon and\
            self.dE == other.dE and\
            self.dN == other.dN and\
            self.rows == other.rows and\
            self.cols == other.cols

    def __str__(self):
        return (
            'Lower right latitude:  {frame.llLat:.4f} N\n'
            'Lower right longitude: {frame.llLon:.4f} E\n'
            '\n\n'
            'UTM Zone:              {frame.utm_zone}{frame.utm_zone_letter}\n'
            'Lower right easting:   {frame.llE:.4f} m\n'
            'Lower right northing:  {frame.llN:.4f} m'
            '\n\n'
            'Pixel spacing east:    {frame.dE:.4f} m\n'
            'Pixel spacing north:   {frame.dN:.4f} m\n'
            'Extent east:           {frame.extentE:.4f} m\n'
            'Extent north:          {frame.extentN:.4f} m\n'
            'Dimensions:            {frame.cols} x {frame.rows} px\n'
            'Spherical distortion:  {frame.spherical_distortion:.4f} m\n'
        ).format(frame=self)
Exemplo n.º 13
0
class Covariance(object):
    """Construct the variance-covariance matrix of quadtree subsampled data.

    Variance and covariance estimates are used to construct the weighting
    matrix to be used later in an optimization.

    Two different methods exist to propagate full-resolution data variances
    and covariances of :class:`kite.Scene.displacement` to the
    covariance matrix of the subsampled dataset:

    1. The distance between :py:class:`kite.quadtree.QuadNode`
       leaf focal points, :py:class:`kite.covariance.Covariance.matrix_focal`
       defines the approximate covariance of the quadtree leaf pair.
    2. The _accurate_ propagation of covariances by taking the mean of
       every node pair pixel covariances. This process is computational
       very expensive and can take a few minutes.
       :py:class:`kite.covariance.Covariance.matrix_focal`

    :param quadtree: Quadtree to work on
    :type quadtree: :class:`~kite.Quadtree`
    :param config: Config object
    :type config: :class:`~kite.covariance.CovarianceConfig`
    """
    def __init__(self, scene, config=CovarianceConfig()):
        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self.frame = scene.frame
        self.quadtree = scene.quadtree
        self.scene = scene
        self.nthreads = 0
        self._noise_data = None
        self._powerspec1d_cached = None
        self._powerspec2d_cached = None
        self._powerspec3d_cached = None
        self._noise_data_grid = None
        self._initialized = False
        self._log = scene._log.getChild("Covariance")

        self.setConfig(config)
        self.quadtree.evChanged.subscribe(self._clear)
        self.scene.evConfigChanged.subscribe(self.setConfig)

    def __call__(self, *args, **kwargs):
        return self.getLeafCovariance(*args, **kwargs)

    def setConfig(self, config=None):
        """Sets and updated the config of the instance

        :param config: New config instance, defaults to configuration provided
                       by parent :class:`~kite.Scene`
        :type config: :class:`~kite.covariance.CovarianceConfig`, optional
        """
        if config is None:
            config = self.scene.config.covariance

        if self.scene.config.old_import:
            self._log.warning("Old format - resetting noise patch coordinates")
            config.covariance_matrix = None
            config.noise_coord = None

        self.config = config
        if config.noise_coord is None and (config.model_coefficients
                                           is not None
                                           or config.variance is not None):
            self.noise_data  # init data array
            self.config.model_coefficients = config.model_coefficients
            self.config.variance = config.variance

        self._clear(config=False)
        self.evConfigChanged.notify()

    def _clear(self, config=True, spectrum=True):
        if config:
            self.config.model_coefficients = None
            self.config.variance = None
            self.config.covariance_matrix = None

        if spectrum:
            self.structure_spectral = None
            self._powerspec1d_cached = None
            self._powerspec2d_cached = None

        self._noise_data_grid = None
        self.covariance_matrix = None
        self.covariance_matrix_focal = None
        self.covariance_spectral = None
        self.covariance_spatial = None
        self.structure_spatial = None
        self.weight_matrix = None
        self.weight_matrix_focal = None
        self._initialized = False
        self.evChanged.notify()

    @property
    def finished_combinations(self):
        return covariance_ext.get_finished_combinations()

    @property
    def noise_coord(self):
        """Coordinates of the noise patch in local coordinates.

        :setter: Set the noise coordinates
        :getter: Get the noise coordinates
        :type: :class:`numpy.ndarray`, ``[llE, llN, sizeE, sizeN]``
        """
        if self.config.noise_coord is None:
            self.noise_data
        return self.config.noise_coord

    @noise_coord.setter
    def noise_coord(self, values):
        self.config.noise_coord = num.array(values)

    @property
    def noise_patch_size_km2(self):
        """
        :getter: Noise patch size in :math:`km^2`.
        :type: float
        """
        if self.noise_coord is None:
            return 0.0
        size = (self.noise_coord[2] * self.noise_coord[3]) * 1e-6
        if self.noise_data.size < self.NOISE_PATCH_MIN_PX:
            self._log.warning("Defined noise patch is instably small")
        return size

    @property
    def noise_data(self, data):
        """Noise data we process to estimate the covariance

        :setter: Set the noise patch to analyze the covariance.
        :getter: If the noise data has not been set manually, we grab data
                 through :func:`~kite.Covariance.selectNoiseNode`.
        :type: :class:`numpy.ndarray`
        """
        return self._noise_data

    @noise_data.getter
    def noise_data(self):
        if self._noise_data is not None:
            return self._noise_data
        elif self.config.noise_coord is not None:
            self._log.debug("Selecting noise_data from config...")
            llE, llN = self.scene.frame.mapENMatrix(
                *self.config.noise_coord[:2])
            sE, sN = self.scene.frame.mapENMatrix(*self.config.noise_coord[2:])
            slice_E = slice(llE, llE + sE)
            slice_N = slice(llN, llN + sN)

            covariance_matrix = self.config.covariance_matrix
            self.noise_data = self.scene.displacement[slice_N, slice_E]
            self.config.covariance_matrix = covariance_matrix
        else:
            self._log.debug("Selecting noise_data from Quadtree...")
            node = self.selectNoiseNode()
            self.noise_data = node.displacement
            self.noise_coord = [node.llE, node.llN, node.sizeE, node.sizeN]

        return self._noise_data

    @noise_data.setter
    def noise_data(self, data):
        data = data.copy()
        data = derampMatrix(trimMatrix(data))
        data[num.isnan(data)] = 0.0
        self._noise_data = data
        self._clear()

    @property
    def noise_data_gridE(self):
        return self._get_noise_data_grid()[0]

    @property
    def noise_data_gridN(self):
        return self._get_noise_data_grid()[1]

    def _get_noise_data_grid(self):
        if self._noise_data_grid is None:
            scene = self.scene

            llE, llN = scene.frame.mapENMatrix(*self.noise_coord[:2])
            sE, sN = scene.frame.mapENMatrix(*self.noise_coord[2:])
            slice_E = slice(llE, llE + sE + 1)
            slice_N = slice(llN, llN + sN + 1)

            gridE = scene.frame.gridEmeter[slice_N, slice_E]
            gridN = scene.frame.gridNmeter[slice_N, slice_E]

            gridE = trimMatrix(self.noise_data, data=gridE)
            gridN = trimMatrix(self.noise_data, data=gridN)

            self._noise_data_grid = (gridE, gridN)

        return self._noise_data_grid

    def selectNoiseNode(self):
        """Choose noise node from quadtree
        the biggest :class:`~kite.quadtree.QuadNode` from
        :class:`~kite.Quadtree`.

        :returns: A quadnode with the least signal.
        :rtype: :class:`~kite.quadtree.QuadNode`
        """
        t0 = time.time()

        node_selection = [
            n for n in self.quadtree.nodes if n.npixel > NOISE_PATCH_MIN_PX
            and n.nan_fraction < NOISE_PATCH_MAX_NAN
        ]
        if not node_selection:
            node_selection = self.quadtree.leaves

        stdmax = max([n.std for n in node_selection])
        lmax = max([n.std for n in node_selection])

        def costFunction(n):
            nl = num.log2(n.length) / num.log2(lmax)
            ns = n.std / stdmax
            return nl * (1.0 - ns) * (1.0 - n.nan_fraction)

        fitness = num.array([costFunction(n) for n in node_selection])

        self._log.debug("Fetched noise from Quadtree.nodes [%0.4f s]" %
                        (time.time() - t0))
        node = node_selection[num.argmin(fitness)]
        return node

    def _mapLeaves(self, nx, ny):
        """Helper function returning appropriate
            :class:`~kite.quadtree.QuadNode` and for maintaining
            the internal mapping with the matrices.

        :param nx: matrix x position
        :type nx: int
        :param ny: matrix y position
        :type ny: int
        :returns: tuple of :class:`~kite.quadtree.QuadNode` s for ``nx``
            and ``ny``
        :rtype: tuple
        """
        leaf1 = self.quadtree.leaves[nx]
        leaf2 = self.quadtree.leaves[ny]

        self._leaf_mapping[leaf1.id] = nx
        self._leaf_mapping[leaf2.id] = ny

        return leaf1, leaf2

    def isFullCovarianceCalculated(self):
        if self.config.covariance_matrix is None:
            return False
        return True

    @property_cached
    def covariance_matrix(self):
        """Covariance matrix calculated from mean of all pixel pairs
            inside the node pairs (full and accurate propagation).

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        """
        if not isinstance(self.config.covariance_matrix, num.ndarray):
            self.config.covariance_matrix = self._calcCovarianceMatrix(
                method="full")
            self.evChanged.notify()
        elif self.config.covariance_matrix.ndim == 1:
            try:
                nl = self.quadtree.nleaves
                self.config.covariance_matrix = self.config.covariance_matrix.reshape(
                    nl, nl)
            except ValueError:
                self.config.covariance_matrix = None
                return self.covariance_matrix
        return self.config.covariance_matrix

    @property_cached
    def covariance_matrix_focal(self):
        """Approximate Covariance matrix from quadtree leaf pair
            distance only. Fast, use for intermediate steps only and
            finallly use approach :attr:`~kite.Covariance.covariance_matrix`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        """
        return self._calcCovarianceMatrix(method="focal")

    @property_cached
    def weight_matrix(self):
        """Weight matrix from full covariance :math:`cov^{-1}`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        """
        return num.linalg.inv(self.covariance_matrix)

    @property_cached
    def weight_matrix_L2(self):
        """Weight matrix from full covariance :math:`\\sqrt{cov^{-1}}`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        """
        incov = num.linalg.inv(self.covariance_matrix)
        return sp.linalg.sqrtm(incov)

    @property_cached
    def weight_matrix_focal(self):
        """Approximated weight matrix from fast focal method
            :math:`cov_{focal}^{-1}`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        """
        try:
            return num.linalg.inv(self.covariance_matrix_focal)
        except num.linalg.LinAlgError as e:
            self._log.exception(e)
            return num.eye(self.covariance_matrix_focal.shape[0])

    @property_cached
    def weight_vector(self):
        """Weight vector from full covariance :math:`cov^{-1}`.
        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves`)
        """
        return num.sum(self.weight_matrix, axis=1)

    @property_cached
    def weight_vector_focal(self):
        """Weight vector from fast focal method
            :math:`\\sqrt{cov_{focal}^{-1}}`.
        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves`)
        """
        return num.sum(self.weight_matrix_focal, axis=1)

    def _calcCovarianceMatrix(self, method="focal", nthreads=None):
        """Constructs the covariance matrix.

        :param method: Either ``focal`` point distances are used - this is
            quick but only an approximation.
            Or ``full``, where the full quadtree pixel distances matrices are
            calculated , defaults to ``focal``
        :type method: str, optional
        :returns: Covariance matrix
        :rtype: thon:numpy.ndarray
        """
        self._initialized = True
        nthreads = nthreads or self.nthreads

        nl = len(self.quadtree.leaves)
        self._leaf_mapping = {}

        t0 = time.time()

        if method == "focal":
            model = self.getModelFunction()

            coords = self.quadtree.leaf_focal_points_meter
            dist_matrix = num.sqrt(
                (coords[:, 0] - coords[:, 0, num.newaxis])**2 +
                (coords[:, 1] - coords[:, 1, num.newaxis])**2)
            cov_matrix = model(dist_matrix, *self.covariance_model)

            # adding variance
            if self.variance < cov_matrix.max():
                variance = cov_matrix.max()
            else:
                variance = self.variance
            if self.quadtree.leaf_mean_px_var is not None:
                self._log.debug(
                    "Adding variance from scene.displacement_px_var")
                variance += self.quadtree.leaf_mean_px_var
            num.fill_diagonal(cov_matrix, variance)

            for nx, ny in num.nditer(num.triu_indices_from(dist_matrix)):
                self._mapLeaves(nx, ny)

        elif method == "full":
            leaf_map = num.empty((len(self.quadtree.leaves), 4),
                                 dtype=num.uint32)
            for nl, leaf in enumerate(self.quadtree.leaves):
                leaf, _ = self._mapLeaves(nl, nl)
                leaf_map[nl, 0], leaf_map[nl, 1] = (
                    leaf._slice_rows.start,
                    leaf._slice_rows.stop,
                )
                leaf_map[nl, 2], leaf_map[nl, 3] = (
                    leaf._slice_cols.start,
                    leaf._slice_cols.stop,
                )

            nleaves = self.quadtree.nleaves
            cov_matrix = covariance_ext.covariance_matrix(
                self.scene.frame.gridEmeter.filled(),
                self.scene.frame.gridNmeter.filled(),
                leaf_map,
                self.covariance_model,
                self.variance,
                nthreads,
                self.config.adaptive_subsampling,
            ).reshape(nleaves, nleaves)

            if self.quadtree.leaf_mean_px_var is not None:
                self._log.debug(
                    "Adding variance from scene.displacement_px_var")
                cov_matrix[num.diag_indices_from(
                    cov_matrix)] += self.quadtree.leaf_mean_px_var

        else:
            raise TypeError("Covariance calculation %s method not defined!" %
                            method)

        self._log.debug("Created covariance matrix - %s mode [%0.4f s]" %
                        (method, time.time() - t0))
        return cov_matrix

    def isMatrixPosDefinite(self, full=False):
        self._log.debug("Checking whether matrix is positive-definite")
        if full:
            matrix = self.covariance_matrix
        else:
            matrix = self.covariance_matrix_focal

        try:
            chol_decomp = num.linalg.cholesky(matrix)
        except num.linalg.linalg.LinAlgError:
            pos_def = False
        else:
            pos_def = ~num.all(num.iscomplex(chol_decomp))
        finally:
            if not pos_def:
                self._log.warning("Covariance matrix is not positiv definite!")
            return pos_def

    @staticmethod
    def _leafFocalDistance(leaf1, leaf2):
        return num.sqrt((leaf1.focal_point[0] - leaf2.focal_point[0])**2 +
                        (leaf1.focal_point[1] - leaf2.focal_point[1])**2)

    def _leafMapping(self, leaf1, leaf2):
        if not isinstance(leaf1, str):
            leaf1 = leaf1.id
        if not isinstance(leaf2, str):
            leaf2 = leaf2.id
        if not self._initialized:
            self.covariance_matrix_focal
        try:
            return self._leaf_mapping[leaf1], self._leaf_mapping[leaf2]
        except KeyError as e:
            raise KeyError("Unknown quadtree leaf with id %s" % e)

    def getLeafCovariance(self, leaf1, leaf2):
        """Get the covariance between ``leaf1`` and ``leaf2`` from
            distances.

        :param leaf1: Leaf one
        :type leaf1: str of `leaf.id` or :class:`~kite.quadtree.QuadNode`
        :param leaf2: Leaf two
        :type leaf2: str of `leaf.id` or :class:`~kite.quadtree.QuadNode`
        :returns: Covariance between ``leaf1`` and ``leaf2``
        :rtype: float
        """
        return self.covariance_matrix[self._leafMapping(leaf1, leaf2)]

    def getLeafWeight(self, leaf, model="focal"):
        """Get the total weight of ``leaf``, which is the summation of
            all single pair weights of :attr:`kite.Covariance.weight_matrix`.

        .. math ::

            w_{x} = \\sum_i W_{x,i}

        :param model: ``Focal`` or ``full``, default ``focal``
        :type model: str
        :param leaf: A leaf from :class:`~kite.Quadtree`
        :type leaf: :class:`~kite.quadtree.QuadNode`

        :returns: Weight of the leaf
        :rtype: float
        """
        (nl, _) = self._leafMapping(leaf, leaf)
        weight_mat = self.weight_matrix_focal
        return num.mean(weight_mat, axis=0)[nl]

    def syntheticNoise(self,
                       shape=(1024, 1024),
                       dEdN=None,
                       anisotropic=False,
                       rstate=None):
        """Create random synthetic noise from data noise power spectrum.

        This function uses the power spectrum of the data noise
        (:attr:`noise_data`) (:func:`powerspecNoise`) to create synthetic
        noise, e.g. to use it for data pertubation in optinmizations.
        The default sampling distances are taken from
        :attr:`kite.scene.Frame.dE` and :attr:`kite.scene.Frame.dN`. They can
        be overwritten.

        :param shape: shape of the desired noise patch.
            Pixels in northing and easting (`nE`, `nN`),
            defaults to `(1024, 1024)`.
        :type shape: tuple, optional
        :param dEdN: The sampling distance in east and north [m], defaults to
            (:attr:`kite.scene.Frame.dEmeter`,
             :attr:`kite.scene.Frame.dNmeter`).
        :type dEdN: tuple, floats
        :returns: synthetic noise patch
        :rtype: :class:`numpy.ndarray`
        """
        if (shape[0] + shape[1]) % 2 != 0:
            # self._log.warning('Patch dimensions must be even, '
            #                   'ceiling dimensions!')
            pass
        nE = shape[1] + (shape[1] % 2)
        nN = shape[0] + (shape[0] % 2)

        if rstate is None:
            rstate = num.random.RandomState()

        rfield = rstate.rand(nN, nE)
        spec = num.fft.fft2(rfield)

        if not dEdN:
            dE, dN = (self.scene.frame.dEmeter, self.scene.frame.dNmeter)
        kE = num.fft.fftfreq(nE, dE)
        kN = num.fft.fftfreq(nN, dN)
        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)

        amp = num.zeros_like(k_rad)

        if not anisotropic:
            noise_pspec, k, _, _, _, _ = self.powerspecNoise2D()
            k_bin = num.insert(k + k[0] / 2, 0, 0)

            for i in range(k.size):
                k_min = k_bin[i]
                k_max = k_bin[i + 1]
                r = num.logical_and(k_rad > k_min, k_rad <= k_max)
                if i == (k.size - 1):
                    r = k_rad > k_min
                if not num.any(r):
                    continue
                amp[r] = noise_pspec[i]
            amp[k_rad == 0.0] = self.variance
            amp[k_rad > k.max()] = noise_pspec[num.argmax(k)]
            amp = num.sqrt(amp * self.noise_data.size * num.pi * 4)

        elif anisotropic:
            interp_pspec, _, _, _, skE, skN = self.powerspecNoise3D()
            kE = num.fft.fftshift(kE)
            kN = num.fft.fftshift(kN)
            mkE = num.logical_and(kE >= skE.min(), kE <= skE.max())
            mkN = num.logical_and(kN >= skN.min(), kN <= skN.max())
            mkRad = num.where(  # noqa
                k_rad < num.sqrt(kN[mkN].max()**2 + kE[mkE].max()**2))
            res = interp_pspec(kN[mkN, num.newaxis],
                               kE[num.newaxis, mkE],
                               grid=True)
            amp = res
            amp = num.fft.fftshift(amp)

        spec *= amp
        noise = num.abs(num.fft.ifft2(spec))
        noise -= num.mean(noise)

        # remove shape % 2 padding
        return noise[:shape[0], :shape[1]]

    def getQuadtreeNoise(self, rstate=None, gather=num.nanmedian):
        """Create noise for a :class:`~kite.quadtree.Quadtree`

        Use :meth:`~kite.covariance.Covariance.getSyntheticNoise` to create
        data-driven noise on each quadtree leaf, summarized by

        :param gather: Function gathering leaf's noise realisation,
                       defaults to num.median.
        :type normalisation: callable, optional
        :returns: Array of noise level at each quadtree leaf.
        :rtype: :class:`numpy.ndarray`
        """
        qt = self.quadtree

        syn_noise = self.syntheticNoise(shape=self.scene.displacement.shape,
                                        rstate=rstate)
        syn_noise[self.scene.displacement_mask] = num.nan
        noise_quadtree_arr = num.full(qt.nleaves, num.nan)

        for il, lv in enumerate(qt.leaves):
            noise_quadtree_arr[il] = gather(syn_noise[lv._slice_rows,
                                                      lv._slice_cols])
        return noise_quadtree_arr

    def powerspecNoise1D(self, data=None, ndeg=512, nk=512):
        if self._powerspec1d_cached is None:
            self._powerspec1d_cached = self._powerspecNoise(data,
                                                            norm="1d",
                                                            ndeg=ndeg,
                                                            nk=nk)
        return self._powerspec1d_cached

    def powerspecNoise2D(self, data=None, ndeg=512, nk=512):
        if self._powerspec2d_cached is None:
            self._powerspec2d_cached = self._powerspecNoise(data,
                                                            norm="2d",
                                                            ndeg=ndeg,
                                                            nk=nk)
        return self._powerspec2d_cached

    def powerspecNoise3D(self, data=None):
        if self._powerspec3d_cached is None:
            self._powerspec3d_cached = self._powerspecNoise(data, norm="3d")
        return self._powerspec3d_cached

    def _powerspecNoise(self, data=None, norm="1d", ndeg=512, nk=512):
        """Get the noise power spectrum from
            :attr:`kite.Covariance.noise_data`.

        :param data: Overwrite Covariance.noise_data, defaults to `None`
        :type data: :class:`numpy.ndarray`, optional
        :returns: `(power_spec, k, f_spectrum, kN, kE)`
        :rtype: tuple
        """
        if data is None:
            noise = self.noise_data
        else:
            noise = data.copy()
        if norm not in ("1d", "2d", "3d"):
            raise AttributeError("norm must be 1d, 2d or 3d")

        # noise = squareMatrix(noise)
        shift = num.fft.fftshift

        spectrum = shift(num.fft.fft2(noise, axes=(0, 1), norm=None))
        power_spec = (num.abs(spectrum) / spectrum.size)**2

        kE = shift(
            num.fft.fftfreq(power_spec.shape[1],
                            d=self.quadtree.frame.dEmeter))
        kN = shift(
            num.fft.fftfreq(power_spec.shape[0],
                            d=self.quadtree.frame.dNmeter))
        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)
        power_spec[k_rad == 0.0] = 0.0

        power_interp = sp.interpolate.RectBivariateSpline(kN, kE, power_spec)

        # def power1d(k):
        #     theta = num.linspace(-num.pi, num.pi, ndeg, False)
        #     power = num.empty_like(k)
        #     for i in range(k.size):
        #         kE = num.cos(theta) * k[i]
        #         kN = num.sin(theta) * k[i]
        #         power[i] = num.median(power_interp.ev(kN, kE)) * k[i]\
        #             * num.pi * 4
        #     return power

        def power1d(k):
            theta = num.linspace(-num.pi, num.pi, ndeg, False)
            power = num.empty_like(k)

            cos_theta = num.cos(theta)
            sin_theta = num.sin(theta)
            for i in range(k.size):
                kE = cos_theta * k[i]
                kN = sin_theta * k[i]
                power[i] = num.mean(power_interp.ev(kN, kE))

            power *= 2 * num.pi
            return power

        def power2d(k):
            """Mean 2D Power works!"""
            theta = num.linspace(-num.pi, num.pi, ndeg, False)
            power = num.empty_like(k)

            cos_theta = num.cos(theta)
            sin_theta = num.sin(theta)
            for i in range(k.size):
                kE = sin_theta * k[i]
                kN = cos_theta * k[i]
                power[i] = num.median(power_interp.ev(kN, kE))
                # Median is more stable than the mean here

            return power

        def power3d(k):
            return power_interp

        power = power1d
        if norm == "2d":
            power = power2d
        elif norm == "3d":
            power = power3d

        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)
        k = num.linspace(k_rad[k_rad > 0].min(), k_rad.max(), nk)
        dk = 1.0 / (k[1] - k[0]) / (2 * nk)
        return power(k), k, dk, spectrum, kE, kN

    def _powerCosineTransform(self, p_spec):
        """Calculating the cosine transform of the power spectrum.

        The cosine transform of the power spectrum is an estimate
        of the data covariance (see Hanssen, 2001)."""
        cos = fft.idct(p_spec, type=3)
        return cos

    def setSamplingMethod(self, method):
        """Set the sampling method"""
        assert method in CovarianceConfig.sampling_method.choices

        self.config.sampling_method = method
        self._clear(config=True, spectrum=False)
        self.evChanged.notify()
        self._log.debug("Changed sampling method to %s" % method)

    def setSpatialBins(self, nbins):
        """Set number of spatial bins"""
        self.config.spatial_bins = nbins
        self._clear(config=True, spectrum=False)
        self.evChanged.notify()
        self._log.debug("Changed spatial distance bins to %s" % nbins)

    def setSpatialPairs(self, npairs):
        """Set number of random spatial pairs"""
        self.config.spatial_pairs = npairs
        self._clear(config=True, spectrum=False)
        self.evChanged.notify()
        self._log.debug("Changed random pairs to %s" % npairs)

    def setModelFunction(self, model):
        assert model in CovarianceConfig.model_function.choices
        self.config.model_function = model
        self._clear(config=True, spectrum=True)
        self.evChanged.notify()
        self._log.debug("Changed model function to %s" % model)

    def getModelFunction(self):
        if self.config.model_function == "exponential":
            return modelCovarianceExponential
        if self.config.model_function == "exponential_cosine":
            return modelCovarianceExponentialCosine

    @property_cached
    def covariance_spectral(self):
        """Covariance function estimated directly from the power spectrum of
            displacement noise patch using the cosine transform.

        :type: tuple, :class:`numpy.ndarray` (covariance, distance)"""
        power_spec, k, dk, _, _, _ = self.powerspecNoise1D()
        # power_spec -= self.variance

        d = num.arange(1, power_spec.size + 1) * dk
        cov = self._powerCosineTransform(power_spec)

        return cov, d

    @property_cached
    def covariance_spatial(self):
        self._log.debug("Estimating covariance (spatial)...")

        nbins = self.config.spatial_bins
        npairs = self.config.spatial_pairs
        noise_data = self.noise_data.ravel()
        noise_data -= noise_data.mean()

        grdE = self.noise_data_gridE
        grdN = self.noise_data_gridN

        max_distance = min(abs(grdE.min() - grdE.max()),
                           abs(grdN.min() - grdN.max()))
        dist_bins = num.linspace(0, max_distance, nbins + 1)

        grdE = grdE.ravel()
        grdN = grdN.ravel()

        # Select random coordinates
        rstate = num.random.RandomState(noise_data.size)
        rand_idx = rstate.randint(0, noise_data.size, (2, npairs))
        idx0 = rand_idx[0, :]
        idx1 = rand_idx[1, :]

        distances = num.sqrt((grdN[idx0] - grdN[idx1])**2 +
                             (grdE[idx0] - grdE[idx1])**2)

        cov_all = noise_data[idx0] * noise_data[idx1]
        vario_all = (noise_data[idx0] - noise_data[idx1])**2

        bins = num.digitize(distances, dist_bins, right=True)
        bin_distances = dist_bins[1:] - dist_bins[1] / 2

        covariance = num.full_like(bin_distances, fill_value=num.nan)
        variance = num.full_like(bin_distances, fill_value=num.nan)

        for ib in range(nbins):
            selection = bins == ib
            if selection.sum() != 0:
                covariance[ib] = num.nanmean(cov_all[selection])
                variance[ib] = num.nanmean(vario_all[selection]) / 2

        self._structure_spatial = (
            variance[~num.isnan(variance)],
            bin_distances[~num.isnan(variance)],
        )
        covariance[0] = num.nan
        return (
            covariance[~num.isnan(covariance)],
            bin_distances[~num.isnan(covariance)],
        )

    def getCovariance(self):
        """Calculate the covariance function

        :return: The covariance and distance
        :rtype: tuple
        """
        if self.config.sampling_method == "spatial":
            return self.covariance_spatial
        elif self.config.sampling_method == "spectral":
            return self.covariance_spectral

    @property
    def covariance_model(self, regime=0):
        """Covariance model parameters for
            :func:`~kite.covariance.modelCovariance` retrieved
            from :attr:`~kite.Covariance.getCovarianceFunction`.

        .. note:: using this function implies several model
            fits: (1) fit of the spectrum and (2) the cosine transform.
            Not sure about the consequences, if this is useful and/or
            meaningful.

        :getter: Get the parameters.
        :type: tuple, ``a`` and ``b``
        """
        if self.config.model_coefficients is None:
            covariance, distance = self.getCovariance()
            model = self.getModelFunction()

            if self.config.model_function == "exponential":
                coeff = (num.mean(covariance), num.mean(distance))

            elif self.config.model_function == "exponential_cosine":
                coeff = (
                    num.mean(covariance),
                    num.mean(distance),
                    num.mean(distance) * -0.1,
                    0.1,
                )

                func = self.getModelFunction()

                # Testing penalty function
                def model(*args):
                    distance, a, b, c, d = args
                    res = func(*args)

                    penalty = 0.0
                    if distance[-1] / b > (distance[-1] + c) / d:
                        penalty = (b - d) * coeff[0]
                        self._log.warning("Penalty %f" % penalty)

                    return res + penalty

                # Overwrite with pure model function
                model = self.getModelFunction()  # noqa

            try:
                coeff, _ = sp.optimize.curve_fit(model,
                                                 distance,
                                                 covariance,
                                                 p0=coeff)
            except (RuntimeError, TypeError) as e:
                self._log.exception(e)
                self._log.warning("Could not fit the %s covariance model",
                                  self.config.model_function)
            finally:
                self.config.model_coefficients = tuple(map(float, coeff))

        return self.config.model_coefficients

    @property
    def covariance_model_rms(self):
        """
        :getter: RMS missfit between :class:`~kite.Covariance.covariance_model`
            and :class:`~kite.Covariance.getCovarianceFunction`
        :type: float
        """
        cov, d = self.getCovariance()
        model = self.getModelFunction()
        cov_mod = model(d, *self.covariance_model)

        return num.sqrt(num.mean((cov - cov_mod)**2))

    @property_cached
    def structure_spatial(self):
        self.covariance_spatial
        return self._structure_spatial

    @property_cached
    def structure_spectral(self):
        """Structure function derived from ``noise_patch``
            :type: tuple, :class:`numpy.ndarray` (structure_spectral, distance)

        Adapted from
        http://clouds.eos.ubc.ca/~phil/courses/atsc500/docs/strfun.pdf
        """
        power_spec, k, dk, _, _, _ = self.powerspecNoise1D()
        d = num.arange(1, power_spec.size + 1) * dk

        def structure_spectral(power_spec, d, k):
            struc_func = num.zeros_like(k)
            for i, d in enumerate(d):
                for ik, tk in enumerate(k):
                    # struc_func[i] += (1. - num.cos(tk*d))*power_spec[ik]
                    struc_func[i] += (1.0 -
                                      sp.special.j0(tk * d)) * power_spec[ik]
            struc_func *= 2.0 / 1
            return struc_func

        struc_func = structure_spectral(power_spec, d, k)
        return struc_func, d

    def getStructure(self, method=None):
        """Get the structure function

        :param method: Either `spatial` or `spectral`, if `None`
            the method is taken from config
        :type method: str (optional)

        :return: (variance, distance)
        :rtype: tuple
        """
        if method is None:
            method = self.config.sampling_method
        if method == "spatial":
            return self.structure_spatial
        elif method == "spectral":
            return self.structure_spectral

    @property
    def variance(self):
        """Variance of data noise estimated from the
            high-frequency end of power spectrum.

        :setter: Set the variance manually
        :getter: Retrieve the variance
        :type: float
        """
        return self.config.variance

    @variance.setter
    def variance(self, value):
        self.config.variance = float(value)
        # self._clear(config=False, spectrum=False, spatial=False)
        self.evChanged.notify()

    @variance.getter
    def variance(self):

        if self.config.variance is None and self.config.sampling_method == "spatial":
            structure_spatial, dist = self.structure_spatial

            last_20p = -int(structure_spatial.size * 0.2)
            self.config.variance = float(
                num.mean(structure_spatial[(last_20p):]))

        elif self.config.variance is None and self.config.sampling_method == "spectral":
            power_spec, k, dk, spectrum, _, _ = self.powerspecNoise1D()
            cov, _ = self.covariance_spectral
            ma = self.covariance_model[0]
            # print(cov[1])
            ps = power_spec * spectrum.size
            # print(spectrum.size)
            # print(num.mean(ps[-int(ps.size/9.):-1]))
            var = num.median(ps[-int(ps.size / 9.0):]) + ma
            self.config.variance = float(var)

        return self.config.variance

    def export_weight_matrix(self, filename):
        """Export the full :attr:`~kite.Covariance.weight_matrix` to an ASCII
            file. The data can be loaded through :func:`numpy.loadtxt`.

        :param filename: path to export to
        :type filename: str
        """
        self._log.debug("Exporting Covariance.weight_matrix to %s" % filename)
        header = ("Exported kite.Covariance.weight_matrix, "
                  "for more information visit https://pyrocko.org\n"
                  "\nThe matrix is symmetric and ordered by QuadNode.id:\n")
        header += ", ".join([l.id for l in self.quadtree.leaves])
        num.savetxt(filename, self.weight_matrix, header=header)

    def get_state_hash(self):
        sha = sha1()
        sha.update(str(self.config).encode())
        return sha.digest().hex()

    @property_cached
    def plot(self):
        """Simple overview plot to summarize the covariance estimations."""
        from kite.plot2d import CovariancePlot

        return CovariancePlot(self)

    @property_cached
    def plot_syntheticNoise(self):
        """Simple overview plot to summarize the covariance estimations."""
        from kite.plot2d import SyntheticNoisePlot

        return SyntheticNoisePlot(self)
Exemplo n.º 14
0
class SandboxScene(BaseScene):

    def __init__(self, config=None, **kwargs):
        self.evChanged = Subject()
        self.evModelUpdated = Subject()
        self.evConfigChanged = Subject()
        self._initialised = False

        self.config = config if config else SandboxSceneConfig()
        BaseScene.__init__(self, frame_config=self.config.frame, **kwargs)

        self.reference = None
        self._los_factors = None

        for attr in ['theta', 'phi']:
            data = kwargs.pop(attr, None)
            if data is not None:
                self.__setattr__(attr, data)

        self.setExtent(self.config.extent_east, self.config.extent_north)

        if self.config.reference_scene is not None:
            self.loadReferenceScene(self.config.reference_scene)

    @property
    def sources(self):
        """
        :returns: List of sources attached sandbox
        :rtype: list
        """
        return self.config.sources

    def setExtent(self, east, north):
        """Set the sandbox's extent in pixels

        :param east: Pixels in East
        :type east: int
        :param north: Pixels in North
        :type north: int
        """
        if self.reference is not None:
            self._log.warning('Cannot change a referenced model!')
            return

        self._log.debug('Changing model extent to %d px by %d px'
                        % (east, north))

        self.cols = east
        self.rows = north

        self._north = num.zeros((self.rows, self.cols))
        self._east = num.zeros_like(self._north)
        self._down = num.zeros_like(self._north)

        self.theta = num.zeros_like(self._north)
        self.phi = num.zeros_like(self._north)
        self.theta.fill(num.pi/2)
        self.phi.fill(0.)

        self.config.extent_east = east
        self.config.extent_north = north

        self.frame.updateExtent()
        self._clearModel()
        self.processSources()

        self.evChanged.notify()

    @property
    def north(self):
        if not self._initialised:
            self.processSources()
        return self._north

    @property
    def east(self):
        if not self._initialised:
            self.processSources()
        return self._east

    @property
    def down(self):
        if not self._initialised:
            self.processSources()
        return self._down

    @property_cached
    def displacement(self):
        """ Displacement projected to LOS """
        self.processSources()
        los_factors = self.los_rotation_factors

        self._displacement =\
            (los_factors[:, :, 0] * -self._down +
             los_factors[:, :, 1] * self._east +
             los_factors[:, :, 2] * self._north)
        return self._displacement

    @property_cached
    def max_horizontal_displacement(self):
        """ Maximum horizontal displacement """
        return num.sqrt(self._north**2 + self._east**2).max()

    def addSource(self, source):
        """Add displacement source to sandbox

        :param source: Displacement Source
        :type source: :class:`kite.sources.meta.SandboxSource`
        """
        if source not in self.sources:
            self.sources.append(source)
        source.evParametersChanged.subscribe(self._clearModel)
        self._clearModel()

        self._log.debug('Source %s added' % source.__class__.__name__)

    def removeSource(self, source):
        """Remove displacement source from sandbox

        :param source: Displacement Source
        :type source: :class:`kite.sources.meta.SandboxSource`
        """
        source.evParametersChanged.unsubscribe(self._clearModel)
        self.sources.remove(source)
        self._log.debug('Source %s removed' % source.__class__.__name__)
        del source

        self._clearModel()

    def processSources(self):
        """ Process displacement sources and update displacements """
        result = self._process(
            self.frame.coordinates,
            self.sources)

        self._north += result['north'].reshape(self.rows, self.cols)
        self._east += result['east'].reshape(self.rows, self.cols)
        self._down += result['down'].reshape(self.rows, self.cols)
        self._initialised = True

    def processCustom(self, coordinates, sources, result_dict=None):
        return self._process(coordinates, sources, result_dict)

    def _process(self, coordinates, sources, result=None):
        if result is None:
            result = num.zeros(
                coordinates.shape[0],
                dtype=[('north', num.float64),
                       ('east', num.float64),
                       ('down', num.float64)])

        avail_processors = {}
        for proc in __processors__:
            avail_processors[proc.__implements__] = proc

        for impl in set([src.__implements__ for src in sources]):
            proc_sources = [src for src in sources
                            if src.__implements__ == impl
                            and src._cached_result is None]

            if not proc_sources:
                continue

            processor = avail_processors.get(impl, None)

            if processor is None:
                self._log.warning(
                    'Could not find source processor for %s' % impl)
                continue

            t0 = time.time()

            proc_result = processor(self).process(
                proc_sources,
                coordinates,
                nthreads=0)

            src_type = proc_sources[0].__class__.__name__
            self._log.debug('Processed %s (nsources:%d) using %s [%.4f s]'
                            % (src_type, len(proc_sources),
                               processor.__name__, time.time() - t0))

            result['north'] += proc_result['displacement.n']
            result['east'] += proc_result['displacement.e']
            result['down'] += proc_result['displacement.d']

        return result

    def loadReferenceScene(self, filename):
        """Load a reference kite scene container into the sandbox

        A reference scene could be actually measured InSAR displacements.

        :param filename: filename of the scene container to load [.npy, .yml]
        :type filename: str
        """
        from .scene import Scene
        self._log.debug('Loading reference scene from %s' % filename)
        scene = Scene.load(filename)
        self.setReferenceScene(scene)
        self.config.reference_scene = filename

    def setReferenceScene(self, scene):
        """Set a reference scene.

        A reference scene could be actually measured InSAR displacements.

        :param scene: Kite scene
        :type scene: :class:`kite.Scene`
        """
        self.frame._updateConfig(scene.frame.config)
        self.setExtent(scene.cols, scene.rows)

        self.phi = scene.phi
        self.theta = scene.theta
        self.reference = Reference(self, scene)
        self._log.debug('Reference scene set to scene.id:%s'
                        % scene.meta.scene_id)

        self._clearModel()

    def getKiteScene(self):
        """Return a :class:`kite.Scene` from current model.

        :returns: Scene
        :rtype: :class:`Scene`
        """
        from .scene import Scene, SceneConfig
        self._log.debug('Creating kite.Scene from SandboxScene')

        config = SceneConfig()
        config.frame = self.frame.config
        config.meta.scene_id = 'Exported SandboxScene'

        return Scene(
            displacement=self.displacement,
            theta=self.theta,
            phi=self.phi,
            config=config)

    def _clearModel(self):
        for arr in [self._north, self._east, self._down]:
            arr.fill(0.)
        self.displacement = None
        self._los_factors = None
        self._initialised = False

        self.max_horizontal_displacement = None

        self.evModelUpdated.notify()

    def save(self, filename):
        """Save the sandbox as kite scene container

        :param filename: filename to save under
        :type filename: str
        """
        _file, ext = op.splitext(filename)
        filename = filename if ext in ['.yml'] else filename + '.yml'
        self._log.debug('Saving model scene to %s' % filename)
        for source in self.sources:
            source.regularize()
        self.config.dump(filename='%s' % filename,
                         header='kite.SandboxScene YAML Config')

    @classmethod
    def load(cls, filename):
        """Load a :class:`kite.SandboxScene`

        :param filename: Config file to load [.yml]
        :type filename: str
        :returns: A sandbox from config file
        :rtype: :class:`kite.SandboxScene`
        """
        config = guts.load(filename=filename)
        sandbox_scene = cls(config=config)
        sandbox_scene._log.debug('Loading config from %s' % filename)
        for source in sandbox_scene.sources:
            sandbox_scene.addSource(source)
        return sandbox_scene
Exemplo n.º 15
0
class Covariance(object):
    '''Construct the variance-covariance matrix of quadtree subsampled data.

    Variance and covariance estimates are used to construct the weighting
    matrix to be used later in an optimization.

    Two different methods exist to propagate full-resolution data variances
    and covariances of :class:`kite.Scene.displacement` to the
    covariance matrix of the subsampled dataset:

    1. The distance between :py:class:`kite.quadtree.QuadNode`
       leaf focal points, :py:class:`kite.covariance.Covariance.matrix_focal`
       defines the approximate covariance of the quadtree leaf pair.
    2. The _accurate_ propagation of covariances by taking the mean of
       every node pair pixel covariances. This process is computational
       very expensive and can take a few minutes.
       :py:class:`kite.covariance.Covariance.matrix_focal`

    :param quadtree: Quadtree to work on
    :type quadtree: :class:`~kite.Quadtree`
    :param config: Config object
    :type config: :class:`~kite.covariance.CovarianceConfig`
    '''
    evChanged = Subject()
    evConfigChanged = Subject()

    def __init__(self, scene, config=CovarianceConfig()):
        self.frame = scene.frame
        self.quadtree = scene.quadtree
        self.scene = scene
        self._noise_data = None
        self._powerspec1d_cached = None
        self._powerspec2d_cached = None
        self._powerspec3d_cached = None
        self._initialized = False
        self._nthreads = 0
        self._log = scene._log.getChild('Covariance')

        self.setConfig(config)
        self.quadtree.evChanged.subscribe(self._clear)
        self.scene.evConfigChanged.subscribe(self.setConfig)

    def __call__(self, *args, **kwargs):
        return self.getLeafCovariance(*args, **kwargs)

    def setConfig(self, config=None):
        ''' Sets and updated the config of the instance

        :param config: New config instance, defaults to configuration provided
                       by parent :class:`~kite.Scene`
        :type config: :class:`~kite.covariance.CovarianceConfig`, optional
        '''
        if config is None:
            config = self.scene.config.covariance

        self.config = config

        if config.noise_coord is None\
           and (config.a is not None or
                config.b is not None or
                config.variance is not None):
            self.noise_data  # init data array
            self.config.a = config.a
            self.config.b = config.b
            self.config.variance = config.variance

        self._clear(config=False)
        self.evConfigChanged.notify()

    def _clear(self, config=True, spectrum=True):
        if config:
            self.config.a = None
            self.config.b = None
            self.config.variance = None
            self.config.covariance_matrix = None

        if spectrum:
            self.structure_func = None
            self._powerspec1d_cached = None
            self._powerspec2d_cached = None

        self.covariance_matrix = None
        self.covariance_matrix_focal = None
        self.covariance_func = None
        self.weight_matrix = None
        self.weight_matrix_focal = None
        self._initialized = False
        self.evChanged.notify()

    @property
    def nthreads(self):
        ''' Number of threads (CPU cores) to use for full covariance
            calculation

        Setting ``nthreads`` to ``0`` uses all available cores (default).

        :setter: Sets the number of threads
        :type: int
        '''
        return self._nthreads

    @nthreads.setter
    def nthreads(self, value):
        self._nthreads = int(value)

    @property
    def noise_coord(self):
        ''' Coordinates of the noise patch in local coordinates.

        :setter: Set the noise coordinates
        :getter: Get the noise coordinates
        :type: :class:`numpy.ndarray`, ``[llE, llN, sizeE, sizeN]``
        '''
        if self.config.noise_coord is None:
            self.noise_data
        return self.config.noise_coord

    @noise_coord.setter
    def noise_coord(self, values):
        self.config.noise_coord = num.array(values)

    @property
    def noise_patch_size_km2(self):
        '''
        :getter: Noise patch size in :math:`km^2`.
        :type: float
        '''
        if self.noise_coord is None:
            return 0.
        size = (self.noise_coord[2] * self.noise_coord[3]) * 1e-6
        if size < 75:
            self._log.warning('Defined noise patch is instably small')
        return size

    @property
    def noise_data(self, data):
        ''' Noise data we process to estimate the covariance

        :setter: Set the noise patch to analyze the covariance.
        :getter: If the noise data has not been set manually, we grab data
                 through :func:`~kite.Covariance.selectNoiseNode`.
        :type: :class:`numpy.ndarray`
        '''
        return self._noise_data

    @noise_data.getter
    def noise_data(self):
        if self._noise_data is not None:
            return self._noise_data
        elif self.config.noise_coord is not None:
            self._log.debug('Selecting noise_data from config...')
            llE, llN = self.scene.frame.mapENMatrix(
                *self.config.noise_coord[:2])
            sE, sN = self.scene.frame.mapENMatrix(*self.config.noise_coord[2:])
            slice_E = slice(llE, llE + sE)
            slice_N = slice(llN, llN + sN)
            self.noise_data = self.scene.displacement[slice_N, slice_E]
        else:
            self._log.debug('Selecting noise_data from Quadtree...')
            node = self.selectNoiseNode()
            self.noise_data = node.displacement
            self.noise_coord = [node.llE, node.llN, node.sizeE, node.sizeN]
        return self.noise_data

    @noise_data.setter
    def noise_data(self, data):
        data = data.copy()
        data = derampMatrix(trimMatrix(data))
        data = trimMatrix(data)
        data[num.isnan(data)] = 0.
        self._noise_data = data
        self._clear()

    def selectNoiseNode(self):
        ''' Choose noise node from quadtree
        the biggest :class:`~kite.quadtree.QuadNode` from
        :class:`~kite.Quadtree`.

        :returns: A quadnode with the least signal.
        :rtype: :class:`~kite.quadtree.QuadNode`
        '''
        t0 = time.time()

        stdmax = max([n.std for n in self.quadtree.nodes])  # noqa
        lmax = max([n.std for n in self.quadtree.nodes])  # noqa

        def costFunction(n):
            nl = num.log2(n.length) / num.log2(lmax)
            ns = n.std / stdmax
            return nl * (1. - ns) * (1. - n.nan_fraction)

        nodes = sorted(self.quadtree.nodes, key=costFunction)

        self._log.debug('Fetched noise from Quadtree.nodes [%0.4f s]' %
                        (time.time() - t0))
        return nodes[0]

    def _mapLeaves(self, nx, ny):
        ''' Helper function returning appropriate
            :class:`~kite.quadtree.QuadNode` and for maintaining
            the internal mapping with the matrices.

        :param nx: matrix x position
        :type nx: int
        :param ny: matrix y position
        :type ny: int
        :returns: tuple of :class:`~kite.quadtree.QuadNode` s for ``nx``
            and ``ny``
        :rtype: tuple
        '''
        leaf1 = self.quadtree.leaves[nx]
        leaf2 = self.quadtree.leaves[ny]

        self._leaf_mapping[leaf1.id] = nx
        self._leaf_mapping[leaf2.id] = ny

        return leaf1, leaf2

    @property_cached
    def covariance_matrix(self):
        ''' Covariance matrix calculated from mean of all pixel pairs
            inside the node pairs (full and accurate propagation).

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        '''
        if not isinstance(self.config.covariance_matrix, num.ndarray):
            self.config.covariance_matrix =\
                self._calcCovarianceMatrix(method='full')
        elif self.config.covariance_matrix.ndim == 1:
            try:
                nl = self.quadtree.nleaves
                self.config.covariance_matrix =\
                    self.config.covariance_matrix.reshape(nl, nl)
            except ValueError:
                self.config.covariance = None
                return self.covariance_matrix
        return self.config.covariance_matrix

    @property_cached
    def covariance_matrix_focal(self):
        ''' Approximate Covariance matrix from quadtree leaf pair
            distance only. Fast, use for intermediate steps only and
            finallly use approach :attr:`~kite.Covariance.covariance_matrix`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        '''
        return self._calcCovarianceMatrix(method='focal')

    @property_cached
    def weight_matrix(self):
        ''' Weight matrix from full covariance :math:`\\sqrt{cov^{-1}}`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        '''
        return num.linalg.inv(self.covariance_matrix)

    @property_cached
    def weight_matrix_focal(self):
        ''' Approximated weight matrix from fast focal method
            :math:`\\sqrt{cov_{focal}^{-1}}`.

        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves` x
            :class:`~kite.Quadtree.nleaves`)
        '''
        return num.linalg.inv(self.covariance_matrix_focal)

    @property_cached
    def weight_vector(self):
        ''' Weight vector from full covariance :math:`\\sqrt{cov^{-1}}`.
        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves`)
        '''
        return num.sum(self.weight_matrix, axis=1)

    @property_cached
    def weight_vector_focal(self):
        ''' Weight vector from fast focal method
            :math:`\\sqrt{cov_{focal}^{-1}}`.
        :type: :class:`numpy.ndarray`,
            size (:class:`~kite.Quadtree.nleaves`)
        '''
        return num.sum(self.weight_matrix_focal, axis=1)

    def _calcCovarianceMatrix(self, method='focal', nthreads=None):
        '''Constructs the covariance matrix.

        :param method: Either ``focal`` point distances are used - this is
            quick but only an approximation.
            Or ``full``, where the full quadtree pixel distances matrices are
            calculated , defaults to ``focal``
        :type method: str, optional
        :returns: Covariance matrix
        :rtype: thon:numpy.ndarray
        '''
        self._initialized = True
        if nthreads is None:
            nthreads = self.nthreads

        nl = len(self.quadtree.leaves)
        self._leaf_mapping = {}

        t0 = time.time()
        ma, mb = self.covariance_model
        if method == 'focal':
            dist_matrix = num.zeros((nl, nl))
            dist_iter = num.nditer(num.triu_indices_from(dist_matrix))

            for nx, ny in dist_iter:
                leaf1, leaf2 = self._mapLeaves(nx, ny)
                dist = self._leafFocalDistance(leaf1, leaf2)
                dist_matrix[(nx, ny), (ny, nx)] = dist
            cov_matrix = modelCovariance(dist_matrix, ma, mb)
            num.fill_diagonal(cov_matrix, self.variance)

        elif method == 'full':
            leaf_map = num.empty((len(self.quadtree.leaves), 4),
                                 dtype=num.uint32)
            for nl, leaf in enumerate(self.quadtree.leaves):
                leaf, _ = self._mapLeaves(nl, nl)
                leaf_map[nl, 0], leaf_map[nl, 1] = (leaf._slice_rows.start,
                                                    leaf._slice_rows.stop)
                leaf_map[nl, 2], leaf_map[nl, 3] = (leaf._slice_cols.start,
                                                    leaf._slice_cols.stop)

            nleaves = self.quadtree.nleaves
            cov_matrix = covariance_ext.covariance_matrix(
                            self.scene.frame.gridE.filled(),
                            self.scene.frame.gridN.filled(),
                            leaf_map, ma, mb, self.variance, nthreads,
                            self.config.adaptive_subsampling)\
                .reshape(nleaves, nleaves)
        else:
            raise TypeError('Covariance calculation %s method not defined!' %
                            method)

        self._log.debug('Created covariance matrix - %s mode [%0.4f s]' %
                        (method, time.time() - t0))
        return cov_matrix

    @staticmethod
    def _leafFocalDistance(leaf1, leaf2):
        return num.sqrt((leaf1.focal_point[0] - leaf2.focal_point[0])**2 +
                        (leaf1.focal_point[1] - leaf2.focal_point[1])**2)

    def _leafMapping(self, leaf1, leaf2):
        if not isinstance(leaf1, str):
            leaf1 = leaf1.id
        if not isinstance(leaf2, str):
            leaf2 = leaf2.id
        if not self._initialized:
            self.covariance_matrix_focal
        try:
            return self._leaf_mapping[leaf1], self._leaf_mapping[leaf2]
        except KeyError as e:
            raise KeyError('Unknown quadtree leaf with id %s' % e)

    def getLeafCovariance(self, leaf1, leaf2):
        '''Get the covariance between ``leaf1`` and ``leaf2`` from
            distances.

        :param leaf1: Leaf one
        :type leaf1: str of `leaf.id` or :class:`~kite.quadtree.QuadNode`
        :param leaf2: Leaf two
        :type leaf2: str of `leaf.id` or :class:`~kite.quadtree.QuadNode`
        :returns: Covariance between ``leaf1`` and ``leaf2``
        :rtype: float
        '''
        return self.covariance_matrix[self._leafMapping(leaf1, leaf2)]

    def getLeafWeight(self, leaf, model='focal'):
        ''' Get the total weight of ``leaf``, which is the summation of
            all single pair weights of :attr:`kite.Covariance.weight_matrix`.

        .. math ::

            w_{x} = \\sum_i W_{x,i}

        :param model: ``Focal`` or ``full``, default ``focal``
        :type model: str
        :param leaf: A leaf from :class:`~kite.Quadtree`
        :type leaf: :class:`~kite.quadtree.QuadNode`

        :returns: Weight of the leaf
        :rtype: float
        '''
        (nl, _) = self._leafMapping(leaf, leaf)
        weight_mat = self.weight_matrix_focal
        return num.mean(weight_mat, axis=0)[nl]

    def syntheticNoise(self, shape=(1024, 1024), dEdN=None, anisotropic=False):
        '''Create random synthetic noise from data noise power spectrum.

        This function uses the power spectrum of the data noise
        (:attr:`noise_data`) (:func:`powerspecNoise`) to create synthetic
        noise, e.g. to use it for data pertubation in optinmizations.
        The default sampling distances are taken from
        :attr:`kite.scene.Frame.dE` and :attr:`kite.scene.Frame.dN`. They can
        be overwritten.

        :param shape: shape of the desired noise patch.
            Pixels in northing and easting (`nE`, `nN`),
            defaults to `(1024, 1024)`.
        :type shape: tuple, optional
        :param dEdN: The sampling distance in easting, defaults to
            (:attr:`kite.scene.Frame.dE`, :attr:`kite.scene.Frame.dN`).
        :type dE: tuple, floats
        :returns: synthetic noise patch
        :rtype: :class:`numpy.ndarray`
        '''
        if (shape[0] + shape[1]) % 2 != 0:
            # self._log.warning('Patch dimensions must be even, '
            #                   'ceiling dimensions!')
            pass
        nE = shape[1] + (shape[1] % 2)
        nN = shape[0] + (shape[0] % 2)

        rfield = num.random.rand(nN, nE)
        spec = num.fft.fft2(rfield)

        if not dEdN:
            dE, dN = (self.scene.frame.dE, self.scene.frame.dN)
        kE = num.fft.fftfreq(nE, dE)
        kN = num.fft.fftfreq(nN, dN)
        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)

        amp = num.zeros_like(k_rad)

        if not anisotropic:
            noise_pspec, k, _, _, _, _ = self.powerspecNoise2D()
            k_bin = num.insert(k + k[0] / 2, 0, 0)

            for i in xrange(k.size):
                k_min = k_bin[i]
                k_max = k_bin[i + 1]
                r = num.logical_and(k_rad > k_min, k_rad <= k_max)
                if i == (k.size - 1):
                    r = k_rad > k_min
                if r.sum() == 0:
                    continue
                amp[r] = noise_pspec[i]
            amp[k_rad == 0.] = self.variance
            amp[k_rad > k.max()] = noise_pspec[num.argmax(k)]
            amp = num.sqrt(amp * self.noise_data.size * num.pi * 4)

        elif anisotropic:
            interp_pspec, _, _, _, skE, skN = self.powerspecNoise3D()
            kE = num.fft.fftshift(kE)
            kN = num.fft.fftshift(kN)
            mkE = num.logical_and(kE >= skE.min(), kE <= skE.max())
            mkN = num.logical_and(kN >= skN.min(), kN <= skN.max())
            mkRad = num.where(  # noqa
                k_rad < num.sqrt(kN[mkN].max()**2 + kE[mkE].max()**2))
            res = interp_pspec(kN[mkN, num.newaxis],
                               kE[num.newaxis, mkE],
                               grid=True)
            print amp.shape, res.shape
            print kN.size, kE.size
            amp = res
            amp = num.fft.fftshift(amp)
            print amp.min(), amp.max()

        spec *= amp
        noise = num.abs(num.fft.ifft2(spec))
        noise -= num.mean(noise)
        return noise

    def powerspecNoise1D(self, data=None, ndeg=512, nk=512):
        if self._powerspec1d_cached is None:
            self._powerspec1d_cached = self._powerspecNoise(data,
                                                            norm='1d',
                                                            ndeg=ndeg,
                                                            nk=nk)
        return self._powerspec1d_cached

    def powerspecNoise2D(self, data=None, ndeg=512, nk=512):
        if self._powerspec2d_cached is None:
            self._powerspec2d_cached = self._powerspecNoise(data,
                                                            norm='2d',
                                                            ndeg=ndeg,
                                                            nk=nk)
        return self._powerspec2d_cached

    def powerspecNoise3D(self, data=None):
        if self._powerspec3d_cached is None:
            self._powerspec3d_cached = self._powerspecNoise(data, norm='3d')
        return self._powerspec3d_cached

    def _powerspecNoise(self, data=None, norm='1d', ndeg=512, nk=512):
        '''Get the noise power spectrum from
            :attr:`kite.Covariance.noise_data`.

        :param data: Overwrite Covariance.noise_data, defaults to `None`
        :type data: :class:`numpy.ndarray`, optional
        :returns: `(power_spec, k, f_spectrum, kN, kE)`
        :rtype: tuple
        '''
        if data is None:
            noise = self.noise_data
        else:
            noise = data.copy()
        if norm not in ['1d', '2d', '3d']:
            raise AttributeError('norm must be either 1d, 2d or 3d')

        # noise = squareMatrix(noise)
        shift = num.fft.fftshift

        spectrum = shift(num.fft.fft2(noise, axes=(0, 1), norm=None))
        power_spec = (num.abs(spectrum) / spectrum.size)**2

        kE = shift(
            num.fft.fftfreq(power_spec.shape[1], d=self.quadtree.frame.dE))
        kN = shift(
            num.fft.fftfreq(power_spec.shape[0], d=self.quadtree.frame.dN))
        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)
        power_spec[k_rad == 0.] = 0.

        power_interp = sp.interpolate.RectBivariateSpline(kN, kE, power_spec)

        # def power1d(k):
        #     theta = num.linspace(-num.pi, num.pi, ndeg, False)
        #     power = num.empty_like(k)
        #     for i in xrange(k.size):
        #         kE = num.cos(theta) * k[i]
        #         kN = num.sin(theta) * k[i]
        #         power[i] = num.median(power_interp.ev(kN, kE)) * k[i]\
        #             * num.pi * 4
        #     return power

        def power1d(k):
            theta = num.linspace(-num.pi, num.pi, ndeg, False)
            power = num.empty_like(k)
            for i in xrange(k.size):
                kE = num.cos(theta) * k[i]
                kN = num.sin(theta) * k[i]
                power[i] = num.median(power_interp.ev(kN, kE))
            return power

        def power2d(k):
            ''' Mean 2D Power works! '''
            theta = num.linspace(-num.pi, num.pi, ndeg, False)
            power = num.empty_like(k)
            for i in xrange(k.size):
                kE = num.sin(theta) * k[i]
                kN = num.cos(theta) * k[i]
                power[i] = num.median(power_interp.ev(kN, kE))
                # Median is more stable than the mean here
            return power

        def power3d(k):
            return power_interp

        power = power1d
        if norm == '2d':
            power = power2d
        elif norm == '3d':
            power = power3d

        k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2)
        k = num.linspace(k_rad[k_rad > 0].min(), k_rad.max(), nk)
        dk = 1. / k.min() / (2. * nk)
        return power(k), k, dk, spectrum, kE, kN

        # def power1Ddisc():
        #     self._log.info('Using discrete summation')
        #     ps = power_spec
        #     d = num.abs(num.arange(-ps.shape[0]/2,
        #                            ps.shape[0]/2))
        #     rm = num.sqrt(d[:, num.newaxis]**2 + d[num.newaxis, :]**2)

        #     axis = num.argmax(ps.shape)
        #     k_ref = kN if axis == 0 else kE
        #     p = num.empty(ps.shape[axis]/2)
        #     k = num.empty(ps.shape[axis]/2)
        #     for r in xrange(ps.shape[axis]/2):
        #         mask = num.logical_and(rm >= r-.5, rm < r+.5)
        #         k[r] = k_ref[(k_ref.size/2)+r]
        #         p[r] = num.median(ps[mask]) * 4 * num.pi
        #     return p, k

        # power, k = power1Ddisc()
        # dk = k[1] - k[0]
        # return power, k, dk, spectrum, kE, kN

    def _powerspecFit(self, regime=3):
        '''Fitting a function to data noise power spectrum.
        '''
        power_spec, k, _, _, _, _ = self.powerspecNoise1D()

        def selectRegime(k, k1, k2):
            return num.logical_and(k > k1, k < k2)

        regime = selectRegime(k, *noise_regimes[regime])

        try:
            return sp.optimize.curve_fit(modelPowerspec,
                                         k[regime],
                                         power_spec[regime],
                                         p0=(0.1, 2000))
        except RuntimeError as e:
            self._log.warning('Could not fit the powerspectrum model. <%s>' %
                              e)
            return (0., 0.), 0.

    @property
    def powerspec_model(self):
        '''Fit function to power spectrum based on the spectral model parameters
            :func:`~kite.covariance.modelPowerspec`

        :returns: Model parameters ``a`` and ``b``
        :rtype: tuple, floats
        '''
        p, _ = self._powerspecFit()
        return p

    @property
    def powerspec_model_rms(self):
        '''
        :getter: RMS missfit between :class:`~kite.Covariance.powerspecNoise1D`
            and :class:`~kite.Covariance.powerspec_model``
        :type: float
        '''
        power_spec, k, _, _, _, _ = self.powerspecNoise1D()
        power_spec_mod = self.powerspecModel(k)
        return num.sqrt(num.mean((power_spec - power_spec_mod)**2))

    def powerspecModel(self, k):
        ''' Calculates the model power spectrum based on the fit of
            :func:`~kite.covariance.powerspec_model`.

        :param k: Wavenumber(s)
        :type k: float or :class:`numpy.ndarray`
        :returns: Power at wavenumber ``k``
        :rtype: float or :class:`numpy.ndarray`
        '''
        p = self.powerspec_model
        return modelPowerspec(k, *p)

    def _powerCosineTransform(self, p_spec):
        '''Calculating the cosine transform of the power spectrum.

            The cosine transform of the power spectrum is an estimate
            of the data covariance (see Hanssen, 2001).'''
        cos = sp.fftpack.idct(p_spec, type=3)
        return cos

    @property_cached
    def covariance_func(self):
        ''' Covariance function estimated directly from the power spectrum of
            displacement noise patch using the cosine transform.

        :type: tuple, :class:`numpy.ndarray` (covariance, distance) '''
        power_spec, k, dk, _, _, _ = self.powerspecNoise1D()
        # power_spec -= self.variance

        d = num.arange(1, power_spec.size + 1) * dk
        cov = self._powerCosineTransform(power_spec)

        return cov, d

    def covarianceFromModel(self, regime=0):
        '''Caluclate exponential analytical covariance

        Empirical Covariance function based on the power spectral model fit
        and not directly on the power spectrum as in
        :func:`~kite.covariance.covariance_func`
        from :func:`~kite.covariance.modelPowerspec`

        .. warning:: Deprecated!

        :return: Covariance and corresponding distances.
        :rtype: tuple, :class:`numpy.ndarray` (covariance_analytical, distance)
        '''
        _, k, dk, _, kN, kE = self.powerspecNoise1D()
        (a, b) = self.powerspec_model

        spec = modelPowerspec(k, a, b)
        d = num.arange(1, spec.size + 1) * dk

        cos = self._powerCosineTransform(spec)
        return cos, d

    @property
    def covariance_model(self, regime=0):
        ''' Covariance model parameters for
            :func:`~kite.covariance.modelCovariance` retrieved
            from :attr:`~kite.Covariance.covarianceFromModel`.

        .. note:: using this function implies several model
            fits: (1) fit of the spectrum and (2) the cosine transform.
            Not sure about the consequences, if this is useful and/or
            meaningful.

        :getter: Get the parameters.
        :type: tuple, ``a`` and ``b``
        '''
        if self.config.a is None or self.config.b is None:
            cov, d = self.covarianceFromModel(regime)
            cov, d = self.covariance_func
            try:
                (a, b), _ =\
                    sp.optimize.curve_fit(modelCovariance, d, cov,
                                          p0=(.001, 500.))
                self.config.a, self.config.b = (float(a), float(b))
            except RuntimeError:
                self._log.warning('Could not fit the covariance model')
                self.config.a, self.config.b = (1., 1000.)
        return self.config.a, self.config.b

    @property
    def covariance_model_rms(self):
        '''
        :getter: RMS missfit between :class:`~kite.Covariance.covariance_model`
            and :class:`~kite.Covariance.covariance_func`
        :type: float
        '''
        cov, d = self.covariance_func
        cov_mod = modelCovariance(d, *self.covariance_model)

        return num.sqrt(num.mean((cov - cov_mod)**2))

    @property_cached
    def structure_func(self):
        ''' Structure function derived from ``noise_patch``
            :type: tuple, :class:`numpy.ndarray` (structure_func, distance)

        Adapted from
        http://clouds.eos.ubc.ca/~phil/courses/atsc500/docs/strfun.pdf
        '''
        power_spec, k, dk, _, _, _ = self.powerspecNoise1D()
        d = num.arange(1, power_spec.size + 1) * dk

        def structure_func(power_spec, d, k):
            struc_func = num.zeros_like(k)
            for i, d in enumerate(d):
                for ik, tk in enumerate(k):
                    # struc_func[i] += (1. - num.cos(tk*d))*power_spec[ik]
                    struc_func[i] += (1. -
                                      sp.special.j0(tk * d)) * power_spec[ik]
            struc_func *= 2. / 1
            return struc_func

        struc_func = structure_func(power_spec, d, k)
        return struc_func, d

    @property
    def variance(self):
        ''' Variance of data noise estimated from the
            high-frequency end of power spectrum.

        :setter: Set the variance manually
        :getter: Retrieve the variance
        :type: float
        '''
        return self.config.variance

    @variance.setter
    def variance(self, value):
        self.config.variance = float(value)
        self._clear(config=False, spectrum=False)
        self.evChanged.notify()

    @variance.getter
    def variance(self):

        if self.config.variance is None:
            power_spec, k, dk, spectrum, _, _ = self.powerspecNoise1D()
            cov, _ = self.covariance_func
            ma, mb = self.covariance_model
            # print cov[1]
            ps = power_spec * spectrum.size
            # print spectrum.size
            # print num.mean(ps[-int(ps.size/9.):-1])
            var = num.median(ps[-int(ps.size / 9.):]) + ma
            self.config.variance = float(var)
        return self.config.variance

    def export_weight_matrix(self, filename):
        ''' Export the full :attr:`~kite.Covariance.weight_matrix` to an ASCII
            file. The data can be loaded through :func:`numpy.loadtxt`.

        :param filename: path to export to
        :type filename: str
        '''
        self._log.debug('Exporting Covariance.weight_matrix to %s' % filename)
        header = 'Exported kite.Covariance.weight_matrix, '\
                 'for more information visit http://pyrocko.com\n'\
                 '\nThe matrix is symmetric and ordered by QuadNode.id:\n'
        header += ', '.join([l.id for l in self.quadtree.leaves])
        num.savetxt(filename, self.weight_matrix, header=header)

    @property_cached
    def plot(self):
        ''' Simple overview plot to summarize the covariance estimations.
        '''
        from kite.plot2d import CovariancePlot
        return CovariancePlot(self)
Exemplo n.º 16
0
class Plot2D(object):
    """Base class for matplotlib 2D plots
    """
    def __init__(self, scene, **kwargs):
        self.evPlotChanged = Subject()
        self._scene = scene
        self._data = None

        self.fig = None
        self.ax = None
        self._show_plt = False
        self._colormap_symmetric = True

        self.title = 'unnamed'

        self._log = logging.getLogger(self.__class__.__name__)

    def __call__(self, *args, **kwargs):
        return self.plot(*args, **kwargs)

    def setCanvas(self, **kwargs):
        """Set canvas to plot in

        :param figure: Matplotlib figure to plot in
        :type figure: :py:class:`matplotlib.Figure`
        :param axes: Matplotlib axes to plot in
        :type axes: :py:class:`matplotlib.Axes`
        :raises: TypeError
        """
        axes = kwargs.get('axes', None)
        figure = kwargs.get('figure', None)

        if isinstance(axes, plt.Axes):
            self.fig, self.ax = axes.get_figure(), axes
            self._show_plt = False
        elif isinstance(figure, plt.Figure):
            self.fig, self.ax = figure, figure.gca()
            self._show_plt = False
        elif axes is None and figure is None and self.fig is None:
            self.fig, self.ax = plt.subplots(1, 1)
            self._show_plt = True
        else:
            raise TypeError('axes has to be of type matplotlib.Axes. '
                            'figure has to be of type matplotlib.Figure')
        self.image = AxesImage(self.ax)
        self.ax.add_artist(self.image)

    @property
    def data(self):
        """ Data passed to matplotlib.image.AxesImage """
        return self._data

    @data.setter
    def data(self, value):
        self._data = value
        self.image.set_data(self.data)
        self.colormapAdjust()

    @data.getter
    def data(self):
        if self._data is None:
            return num.empty((50, 50))
        return self._data

    def _initImagePlot(self, **kwargs):
        """ Initiate the plot

        :param figure: Matplotlib figure to plot in
        :type figure: :py:class:`matplotlib.Figure`
        :param axes: Matplotlib axes to plot in
        :type axes: :py:class:`matplotlib.Axes`
        """
        self.setCanvas(**kwargs)

        self.setColormap(kwargs.get('cmap', 'RdBu'))
        self.colormapAdjust()

        self.ax.set_xlim((0, self._scene.frame.E.size))
        self.ax.set_ylim((0, self._scene.frame.N.size))
        self.ax.set_aspect('equal')
        self.ax.invert_yaxis()

        self.ax.set_title(self.title)

        def close_figure(ev):
            self.fig = None
            self.ax = None

        try:
            self.fig.canvas.mpl_connect('close_event', close_figure)
        except Exception:
            pass

    def plot(self, **kwargs):
        """Placeholder in prototype class

        :param figure: Matplotlib figure to plot in
        :type figure: :py:class:`matplotlib.Figure`
        :param axes: Matplotlib axes to plot in
        :type axes: :py:class:`matplotlib.Axes`
        :param **kwargs: kwargs are passed into `plt.imshow`
        :type **kwargs: dict
        :raises: NotImplemented
        """
        raise NotImplementedError()
        self._initImagePlot(**kwargs)
        if self._show_plt:
            plt.show()

    def _updateImage(self):
        self.image.set_data(self.data)

    def setColormap(self, cmap='RdBu'):
        """Set matplotlib colormap

        :param cmap: matplotlib colormap name, defaults to 'RdBu'
        :type cmap: str, optional
        """
        self.image.set_cmap(cmap)
        self.evPlotChanged.notify()

    def colormapAdjust(self):
        """Set colormap limits automatically

        :param symmetric: symmetric colormap around 0, defaults to True
        :type symmetric: bool, optional
        """
        vmax = num.nanmax(self.data)
        vmin = num.nanmin(self.data)
        self.colormap_limits = (vmin, vmax)

    @property
    def colormap_symmetric(self):
        return self._colormap_symmetric

    @colormap_symmetric.setter
    def colormap_symmetric(self, value):
        self._colormap_symmetric = value
        self.colormapAdjust()

    @property
    def colormap_limits(self):
        return self.image.get_clim()

    @colormap_limits.setter
    def colormap_limits(self, limits):
        if not isinstance(limits, tuple):
            raise AttributeError('Limits have to be a tuple (vmin, vmax)')
        vmin, vmax = limits

        if self.colormap_symmetric:
            _max = max(abs(vmin), abs(vmax))
            vmin, vmax = -_max, _max
        self.image.set_clim(vmin, vmax)

        self.evPlotChanged.notify()

    @staticmethod
    def _colormapsAvailable():
        return [  # ('Perceptually Uniform Sequential',
            #  ['viridis', 'inferno', 'plasma', 'magma']),
            # ('Sequential', ['Blues', 'BuGn', 'BuPu',
            #                 'GnBu', 'Greens', 'Greys', 'Oranges', 'OrRd',
            #                 'PuBu', 'PuBuGn', 'PuRd', 'Purples', 'RdPu',
            #               'Reds', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd']),
            # ('Sequential (2)', ['afmhot', 'autumn', 'bone', 'cool',
            #                     'copper', 'gist_heat', 'gray', 'hot',
            #                     'pink', 'spring', 'summer', 'winter']),
            ('Diverging', [
                'BrBG', 'bwr', 'coolwarm', 'PiYG', 'PRGn', 'RdBu', 'RdGy',
                'RdYlBu', 'RdYlGn', 'Spectral', 'seismic', 'PuOr'
            ]),
            ('Qualitative', [
                'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1',
                'Set2', 'Set3'
            ]),
            # ('Miscellaneous', ['gist_earth', 'terrain', 'ocean',
            #                  'brg', 'CMRmap', 'cubehelix', 'gist_stern',
            #                    'gnuplot', 'gnuplot2', 'gist_ncar',
            #                    'nipy_spectral', 'jet', 'rainbow',
            #                    'gist_rainbow', 'hsv', 'flag', 'prism'])
        ]
Exemplo n.º 17
0
 def __init__(self, *args, **kwargs):
     Object.__init__(self, *args, **kwargs)
     self._cached_result = None
     self.evParametersChanged = Subject()
Exemplo n.º 18
0
Arquivo: scene.py Projeto: wsja/kite
class BaseScene(object):

    def __init__(self, **kwargs):
        self._initLogging()
        self.evChanged = Subject()
        self.evConfigChanged = Subject()

        self._displacement = None
        self._displacement_px_var = None
        self._phi = None
        self._theta = None
        self._los_factors = None
        self.cols = 0
        self.rows = 0
        self.los = LOSUnitVectors(scene=self)

        frame_config = kwargs.pop('frame_config', FrameConfig())

        for fattr in ('llLat', 'llLon', 'dLat', 'dLon'):
            coord = kwargs.pop(fattr, None)
            if coord is not None:
                frame_config.__setattr__(fattr, coord)
        self.frame = Frame(scene=self, config=frame_config)

        for attr in ('displacement', 'displacement_px_var', 'theta', 'phi'):
            data = kwargs.pop(attr, None)
            if data is not None:
                self.__setattr__(attr, data)

    def _initLogging(self):
        self._log = logging.getLogger(self.__class__.__name__)

    @property
    def displacement(self):
        """Surface displacement in meter on a regular grid.

        :setter: Set the unwrapped InSAR displacement.
        :getter: Return the displacement matrix.
        :type: :class:`numpy.ndarray`, ``NxM``
        """
        return self._displacement

    @displacement.setter
    def displacement(self, value):
        _setDataNumpy(self, '_displacement', value)
        self.rows, self.cols = self._displacement.shape
        self.displacement_mask = None
        self.evChanged.notify()

    @property
    def displacement_px_var(self):
        """ Variance of the surface displacement per pixel.
        Same dimension as displacement.

        :setter: Set standard deviation of of the displacement.
        :getter: Return the standard deviation matrix.
        :type: :class:`numpy.ndarray`, ``NxM``
        """
        return self._displacement_px_var

    @displacement_px_var.setter
    def displacement_px_var(self, value):
        self._displacement_px_var = value

    @property_cached
    def displacement_mask(self):
        """ Displacement :attr:`numpy.nan` mask

        :type: :class:`numpy.ndarray`, dtype :class:`numpy.bool`
        """
        return ~num.isfinite(self.displacement)

    @property
    def shape(self):
        return self._displacement.shape

    @property
    def phi(self):
        """ Horizontal angle towards satellite :abbr:`line of sight (LOS)`
        in radians counter-clockwise from East.

        .. important ::

            Kite's convention is:

            * :math:`0` is **East**
            * :math:`\\frac{\\pi}{2}` is **North**!

        :setter: Set the phi matrix for scene's displacement, can be ``int``
                 for static look vector.
        :type: :class:`numpy.ndarray`, size same as
               :attr:`~kite.Scene.displacement` or int
        """
        return self._phi

    @phi.setter
    def phi(self, value):
        if isinstance(value, float):
            self._phi = value
        else:
            _setDataNumpy(self, '_phi', value)
        self.phiDeg = None
        self.los_rotation_factors = None
        self.evChanged.notify()

    @property
    def theta(self):
        """ Theta is the look vector elevation angle towards satellite from
        the horizon in radians. Matrix of theta towards satellite's
        :abbr:`line of sight (LOS)`.

        .. important ::

            Kite convention!

            * :math:`-\\frac{\\pi}{2}` is **Down**
            * :math:`\\frac{\\pi}{2}` is **Up**

        :setter: Set the theta matrix for scene's displacement, can be ``int``
                 for static look vector.
        :type: :class:`numpy.ndarray`, size same as
               :attr:`~kite.Scene.displacement` or int
        """
        return self._theta

    @theta.setter
    def theta(self, value):
        if isinstance(value, float):
            self._theta = value
        else:
            _setDataNumpy(self, '_theta', value)
        self.thetaDeg = None
        self.los_rotation_factors = None
        self.evChanged.notify()

    @property_cached
    def thetaDeg(self):
        """ LOS elevation angle in degree, ``NxM`` matrix like
            :class:`kite.Scene.theta`

        :type: :class:`numpy.ndarray`
        """
        return num.rad2deg(self.theta)

    @property_cached
    def phiDeg(self):
        """ LOS horizontal orientation angle in degree,
            counter-clockwise from East,``NxM`` matrix like
            :class:`kite.Scene.phi`

        :type: :class:`numpy.ndarray`
        """
        return num.rad2deg(self.phi)

    @property_cached
    def los_rotation_factors(self):
        """ Trigonometric factors to rotate displacement matrices towards LOS

        Rotation is as follows:

        ..
            displacement_los =\
                (los_rotation_factors[:, :, 0] * -down +
                 los_rotation_factors[:, :, 1] * east +
                 los_rotation_factors[:, :, 2] * north)

        :returns: Factors for rotation
        :rtype: :class:`numpy.ndarray`, ``NxMx3``
        :raises: AttributeError
        """
        if (self.theta.size != self.phi.size):
            raise AttributeError('LOS angles inconsistent with provided'
                                 ' coordinate shape.')
        if self._los_factors is None:
            self._los_factors = num.empty((self.theta.shape[0],
                                           self.theta.shape[1],
                                           3))
            self._los_factors[:, :, 0] = num.sin(self.theta)
            self._los_factors[:, :, 1] = num.cos(self.theta)\
                * num.cos(self.phi)
            self._los_factors[:, :, 2] = num.cos(self.theta)\
                * num.sin(self.phi)
        return self._los_factors

    def get_ramp_coefficients(self):
        '''Fit plane through the displacement data.

        :returns: Mean of the displacement and slopes in easting coefficients
            of the fitted plane. The array hold
            ``[offset_e, offset_n, slope_e, slope_n]``.
        :rtype: :class:`numpy.ndarray`
        '''
        msk = ~self.displacement_mask
        displacement = self.displacement[msk]

        coords = self.frame.coordinates[msk.flatten()]

        # Add ones for the offset
        coords = num.hstack((
            num.ones_like(coords),
            coords))

        coeffs, res, _, _ = num.linalg.lstsq(
            coords, displacement, rcond=None)

        return coeffs

    def displacement_deramp(self, demean=True, inplace=True):
        '''Fit a plane onto the displacement data and substract it

        :param demean: Demean the displacement
        :type demean: bool
        :param inplace: Replace data of the scene (default: True)
        :type inplace: bool

        :return: ``None`` if ``inplace=True`` else a new Scene
        :rtype: ``None`` or :class:`~kite.Scene`
        '''
        self._log.debug('De-ramping scene...')
        coeffs = self.get_ramp_coefficients()
        msk = self.displacement_mask
        coords = self.frame.coordinates

        ramp = coeffs[2:] * coords
        if demean:
            ramp += coeffs[:2]

        ramp = ramp.sum(axis=1).reshape(self.shape)
        ramp[msk] = num.nan

        if inplace:
            self.displacement -= ramp
            self.evChanged.notify()

        else:
            return self.__class__(
                config=self.config,
                theta=self.theta,
                phi=self.phi,
                displacement=self.displacement - ramp)

    def __neg__(self):
        ret = copy.deepcopy(self)
        ret.displacement *= -1
        return ret

    def __add__(self, other, copy_obj=True):
        if copy_obj:
            ret = copy.deepcopy(self)
        else:
            ret = self

        if not ret.frame == other.frame:
            raise ValueError('Scene frames do not align!')
        ret.displacement += other.displacement

        tmin = ret.meta.time_master \
            if ret.meta.time_master < other.meta.time_master \
            else other.meta.time_master

        tmax = ret.meta.time_slave \
            if ret.meta.time_slave > other.meta.time_slave \
            else other.meta.time_slave

        ret.meta.time_master = tmin
        ret.meta.time_slave = tmax
        return ret

    def __sub__(self, other):
        return self.__add__(-other)

    def __isub__(self, scene):
        return self.__add__(-scene, copy_obj=False)

    def __iadd__(self, scene):
        return self.__add__(scene, copy_obj=False)