""" A node (or *tile*) in held by :class:`~kite.Quadtree`. Each node in the tree hold a back reference to the quadtree and scene to access
:param llr: Lower left corner row in :attr:`kite.Scene.displacement` matrix. :type llr: int :param llc: Lower left corner column in :attr:`kite.Scene.displacement` matrix. :type llc: int :param length: Length of node in from ``llr, llc`` in both dimensions :type length: int :param id: Unique id of node :type id: str :param children: Node's children :type children: List of :class:`~kite.quadtree.QuadNode` """
def nan_fraction(self): """ Fraction of NaN values within the tile :type: float """ self.displacement.size
def npixel(self):
def mean(self): """ Mean displacement :type: float """ return float(num.nanmean(self.displacement))
def median(self): """ Median displacement :type: float """
def std(self): """ Standard deviation of displacement :type: float """
def var(self): """ Variance of displacement :type: float """ return float(num.nanvar(self.displacement))
def mean_px_var(self): """ Variance of displacement :type: float """ if self.displacement_px_var is not None: return float(num.nanmean(self.displacement_px_var)) return None
def corr_median(self): """ Standard deviation of node's displacement corrected for median :type: float """
def corr_mean(self): """ Standard deviation of node's displacement corrected for mean :type: float """ return float(num.nanstd(self.displacement - self.mean))
def corr_bilinear(self): """ Standard deviation of node's displacement corrected for bilinear trend (2D) :type: float """ return float(num.nanstd(derampMatrix(self.displacement)))
def weight(self): """ :getter: Absolute weight derived from :class:`kite.Covariance` - works on tree leaves only. :type: float """ return float(self.scene.covariance.getLeafWeight(self))
def focal_point(self): """ Node focal point in local coordinates respecting NaN values :type: tuple, float - (easting, northing) """
def focal_point_meter(self): """ Node focal point in local coordinates respecting NaN values :type: tuple, float - (easting, northing) """ E = float(num.mean(self.gridEmeter.compressed() + self.frame.dEmeter/2)) N = float(num.mean(self.gridNmeter.compressed() + self.frame.dNmeter/2)) return E, N
def displacement(self): """ Displacement array, slice from :attr:`kite.Scene.displacement` :type: :class:`numpy.ndarray` """
def displacement_masked(self): """ Masked displacement, see :attr:`~kite.quadtree.QuadNode.displacement` :type: :class:`numpy.ndarray` """ return num.ma.masked_array(self.displacement, self.displacement_mask, fill_value=num.nan)
def displacement_mask(self): """ Displacement nan mask of :attr:`~kite.quadtree.QuadNode.displacement` :type: :class:`numpy.ndarray`, dtype :class:`numpy.bool`
.. todo ::
Faster to slice Scene.displacement_mask? """
def displacement_px_var(self): """ Displacement array, slice from :attr:`kite.Scene.displacement` :type: :class:`numpy.ndarray` """ if self.scene.displacement_px_var is not None: return self.scene.displacement_px_var[ self._slice_rows, self._slice_cols] return None
def phi(self): """ Median Phi angle, see :class:`~kite.Scene`. :type: float """
def theta(self): """ Median Theta angle, see :class:`~kite.Scene`. :type: float """
def unitE(self): unitE = self.scene.los_rotation_factors[ self._slice_rows, self._slice_cols, 1] return num.nanmedian(unitE[~self.displacement_mask])
def unitN(self): unitN = self.scene.los_rotation_factors[ self._slice_rows, self._slice_cols, 2] return num.nanmedian(unitN[~self.displacement_mask])
def unitU(self): unitU = self.scene.los_rotation_factors[ self._slice_rows, self._slice_cols, 0] return num.nanmedian(unitU[~self.displacement_mask])
def gridE(self): """ Grid holding local east coordinates, see :attr:`kite.scene.Frame.gridE`. :type: :class:`numpy.ndarray` """
def gridEmeter(self): """ Grid holding local east coordinates, see :attr:`kite.scene.Frame.gridEmeter`. :type: :class:`numpy.ndarray` """ return self.scene.frame.gridEmeter[self._slice_rows, self._slice_cols]
def gridN(self): """ Grid holding local north coordinates, see :attr:`kite.scene.Frame.gridN`. :type: :class:`numpy.ndarray` """
def gridNmeter(self): """ Grid holding local north coordinates, see :attr:`kite.scene.Frame.gridNmeter`. :type: :class:`numpy.ndarray` """ return self.scene.frame.gridNmeter[self._slice_rows, self._slice_cols]
def llE(self): """ :getter: Lower left east coordinate in local coordinates (*meters* or *degree*). :type: float """
def llN(self): """ :getter: Lower left north coordinate in local coordinates (*meter* or *degree*). :type: float """
def urN(self): return self.llN + self.sizeN
def urE(self): return self.llE + self.sizeE
def sizeE(self): """ :getter: Size in eastern direction in *meters* or *degree*. :type: float """
def sizeN(self): """ :getter: Size in northern direction in *meters* or *degree*. :type: float """
""" Iterator over the all children.
:yields: Children of it's own. :type: :class:`~kite.quadtree.QuadNode` """
""" Iterator over the leaves, evaluating parameters from :class:`~kite.Quadtree` instance.
:yields: Leafs fullfilling the tree's parameters. :type: :class:`~kite.quadtree.QuadNode` """ not self.length > self.quadtree._tile_size_lim_px[1])\ or self.children is None \ or (self.children[0].length < self.quadtree._tile_size_lim_px[0]): else:
yield None self._displacement, self.llr + self.length / 2 * nr, self.llc + self.length / 2 * nc, self.length / 2)
""" Create the tree from a set of basenodes, ignited by :class:`~kite.Quadtree` instance. Evaluates :class:`~kite.Quadtree` correction method and :attr:`~kite.Quadtree.epsilon_min`. """ or self.length >= 64)\ and not self.length < self.MIN_PIXEL_LENGTH_NODE: # self.length > .1 * max(self.quadtree._data.shape): !! Expensive self.children = None else: else:
""" Quadtree configuration object holding essential parameters used to reconstruct a particular tree """ choices=('mean', 'median', 'bilinear', 'std'), default='median', help='Node correction for splitting, available methods ' ' ``[\'mean\', \'median\', \'bilinear\', \'std\']``') optional=True, help='Variance threshold when a node is split') default=0.9, help='Allowed NaN fraction per tile') optional=True, help='Minimum allowed tile size in *meters* or *degree*') optional=True, help='Maximum allowed tile size in *meters* or *degree*') optional=True, default=[], help='Blacklist of excluded leaves')
"""Quadtree for irregular subsampling InSAR displacement data held in :py:class:`kite.scene.Scene`
InSAR displacement scenes can hold a vast amount of data points, which is often highly redundant and unsuitably large for the use in inverse modeling. By subsampling and therefore decimating the data points systematically through a parametrized quadtree we can reduce the dataset without significant loss of displacement information. Quadtree subsampling keeps a high spatial resolution where displacement gradients are high and efficiently reduces data point density in regions with small displacement variations. The product is a managable dataset size with good representation of the original data.
The standard deviation from :attr:`kite.quadtree.QuadNode.displacement` is evaluated against different corrections:
* ``mean``: Mean is substracted * ``median``: Median is substracted * ``bilinear``: A 2D detrend is applied to the node * ``std``: Pure standard deviation without correction
set through :func:`~kite.Quadtree.setCorrection`. If the standard deviation exceeds :attr:`~kite.Quadtree.epsilon` the node is split.
The leaves can also be exported in a *CSV* format by :func:`~kite.Quadtree.export_csv`, or *GeoJSON* by :func:`~kite.Quadtree.export_geojson`.
Controlling attributes are:
* :attr:`~kite.Quadtree.epsilon`, RMS threshold * :attr:`~kite.Quadtree.nan_fraction`, allowed :attr:`numpy.nan` in node * :attr:`~kite.Quadtree.tile_size_max`, maximum node size in *meters* or *degree* * :attr:`~kite.Quadtree.tile_size_min`, minimum node size in *meter* or *degree*
:attr:`~kite.Quadtree.leaves` hold the current tree's :class:`~kite.quadtree.QuadNode` 's. """
'mean': ('Standard deviation around mean', lambda n: n.corr_mean), 'median': ('Standard deviation around median', lambda n: n.corr_median), 'bilinear': ('Standard deviation around bilinear detrended node', lambda n: n.corr_bilinear), 'std': ('Standard deviation (std)', lambda n: n.std), }
'mean': lambda n: n.mean, 'median': lambda n: n.median, 'weight': lambda n: n.weight, }
# Cached matrices
# self.scene.evChanged.subscribe(self.reinitializeTree)
""" 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.QuadtreeConfig`, optional """ config = self.scene.config.quadtree
frame = self.scene.config.frame
from pyrocko import orthodrome as od self._log.warning('Old format - converting quadtree configuration')
dLat, dLon = od.ne_to_latlon( frame.llLat, frame.llLon, config.tile_size_max, config.tile_size_min)
config.tile_size_min = dLon - frame.llLon config.tile_size_max = dLat - frame.llLat
""" Set correction method calculating the standard deviation of instances :class:`~kite.quadtree.QuadNode` s
The standard deviation from :attr:`kite.quadtree.QuadNode.displacement` is evaluated against different corrections:
* ``mean``: Mean is substracted * ``median``: Median is substracted * ``bilinear``: A 2D detrend is applied to the node * ``std``: Pure standard deviation without correction
:param correction: Choose from methods ``mean_std, median_std, bilinear_std, std`` :type correction: str :raises: AttributeError """ raise AttributeError('Method %s not in %s', correction, self._displacement_corrections)
if self._scene_state != self.scene.get_plugin_state_hash(): self.reinitializeTree()
# Clearing cached properties through None
self._log.warning('No leaves in default quadtree,' ' setting allowed_nan=1.') self.nan_allowed = 1.
"""Clear cached leafs and properties"""
def min_node_length_px(self):
self.min_node_length_px
self.nnodes, time.time() - t0)
def epsilon(self): """ Threshold for quadtree splitting its ``QuadNode``.
The threshold is the maximum standard deviation of leaf mean, median or simply its values (see ''SetSplitMethod'') allowed to not further split a "QuadNode".
:setter: Sets the epsilon/RMS threshold :getter: Returns the current epsilon :type: float """
def epsilon(self, value): self._log.warning( 'Epsilon is out of bounds [%0.6f], epsilon_min %0.6f', value, self.epsilon_min) return
def _epsilon_init(self): """ Initial epsilon for virgin tree creation """
def epsilon_min(self): """ Lowest allowed epsilon :type: float """
def nan_allowed(self): """Fraction of allowed ``NaN`` values in quadtree leaves. If value is exceeded the leaf is kicked out entirely.
:setter: Fraction ``0. <= fraction <= 1``. :type: float """
def nan_allowed(self, value): if (value > 1. or value <= 0.): self._log.warning('NaN fraction must be 0. < nan_allowed <= 1.') return
self.clearLeaves() self.clearLeafBlacklist() self.config.nan_allowed = value self.evChanged.notify()
def tile_size_min(self): """ Minimum allowed tile size in *meter*. Measured along long edge ``(max(dE, dN))``. Minimum tile size defaults to 1/20th of the largest dimension
:getter: Returns the minimum allowed tile size :setter: Sets the minimum threshold :type: float """
def tile_size_min(self, value): if value > self.tile_size_max: self._log.warning('tile_size_min > tile_size_max is required!') return self.config.tile_size_min = value self._tileSizeChanged()
def tile_size_max(self): """ Maximum allowed tile size in *meter*. Measured along long edge ``(max(dE, dN))`` Maximum tile size defaults to 1/5th of the largest dimension
:getter: Returns the maximum allowed tile size :setter: Sets the maximum threshold :type: float """
def tile_size_max(self, value): if value < self.tile_size_min: self._log.warning('tile_size_min > tile_size_max is required') return self.config.tile_size_max = value self._tileSizeChanged()
self._tile_size_lim_px = None self.clearLeaves() self.clearLeafBlacklist() self.evChanged.notify()
def _tile_size_lim_px(self): round(self.tile_size_max / dpx))
def nodes(self): """ All nodes of the tree
:getter: Get the list of nodes :type: list """
def nnodes(self): """ :getter: Number of nodes of the built tree. :type: int """
""" Blacklist a leaf and exclude it from the tree
:param leaves: Leaf instances :type leaves: list """ self.config.leaf_blacklist.extend(leaves) self._log.debug('Blacklisted leaves: %s' % ', '.join(self.config.leaf_blacklist)) self.clearLeaves() self.evChanged.notify()
def leaves(self): """:getter: List of leaves for current configuration. :type: (list or :class:`~kite.quadtree.QuadNode` s) """ [lf for lf in b.iterLeaves() if lf.nan_fraction < self.nan_allowed and lf.id not in self.config.leaf_blacklist]) 'Gathering leaves for epsilon %.4f (nleaves=%d) [%0.4f s]' % (self.epsilon, len(leaves), time.time() - t0))
def nleaves(self): """ :getter: Number of leaves for current parametrisation. :type: int """
def leaf_mean_px_var(self): """ :getter: Mean pixel variance in each quadtree, if :attr:`kite.Scene.displacement_px_var` is set. :type: :class:`numpy.ndarray`, size ``N``. """ return num.array([lf.mean_px_var for lf in self.leaves])
def leaf_means(self): """ :getter: Leaf mean displacements from :attr:`kite.quadtree.QuadNode.mean`. :type: :class:`numpy.ndarray`, size ``N``. """ return num.array([lf.mean for lf in self.leaves])
def leaf_medians(self): """ :getter: Leaf median displacements from :attr:`kite.quadtree.QuadNode.median`. :type: :class:`numpy.ndarray`, size ``N``. """
def _leaf_focal_points(self): return num.array([lf._focal_point for lf in self.leaves])
def leaf_focal_points(self): """ :getter: Leaf focal points in local coordinates. :type: :class:`numpy.ndarray`, size ``(N, 2)`` """
def leaf_focal_points_meter(self): """ :getter: Leaf focal points in meter. :type: :class:`numpy.ndarray`, size ``(N, 2)`` """ return num.array([lf.focal_point_meter for lf in self.leaves])
def leaf_coordinates(self): """Synonym for :func:`Quadtree.leaf_focal_points` in easting/northing"""
def leaf_center_distance(self): """ :getter: Leaf distance to center point of the quadtree :type: :class:`numpy.ndarray`, size ``(N, 3)`` """
def leaf_eastings(self): return self.leaf_coordinates[:, 0]
def leaf_northings(self): return self.leaf_coordinates[:, 1]
def leaf_phis(self): """ :getter: Median leaf LOS phi angle. :attr:`kite.Scene.phi` :type: :class:`numpy.ndarray`, size ``(N)`` """
def leaf_thetas(self): """ :getter: Median leaf LOS theta angle. :attr:`kite.Scene.theta` :type: :class:`numpy.ndarray`, size ``(N)`` """
def leaf_los_rotation_factors(self): """ :getter: Trigonometric factors for rotating displacement matrices towards LOS. See :attr:`kite.BaseScene.los_rotation_factors` :type: :class:`numpy.ndarray`, Nx3 """ los_factors = num.empty((self.nleaves, 3)) los_factors[:, 0] = num.sin(self.leaf_thetas) los_factors[:, 1] = num.cos(self.leaf_thetas)\ * num.cos(self.leaf_phis) los_factors[:, 2] = num.cos(self.leaf_thetas)\ * num.sin(self.leaf_phis) return los_factors
def leaf_matrix_means(self): """ :getter: Leaf mean displacements casted to :attr:`kite.Scene.displacement`. :type: :class:`numpy.ndarray`, size ``(N, M)`` """ return self._getLeafsNormMatrix(self._leaf_matrix_means, method='mean')
def leaf_matrix_medians(self): """ :getter: Leaf median displacements casted to :attr:`kite.Scene.displacement`. :type: :class:`numpy.ndarray`, size ``(N, M)`` """ return self._getLeafsNormMatrix(self._leaf_matrix_medians, method='median')
def leaf_matrix_weights(self): """ :getter: Leaf weights casted to :attr:`kite.Scene.displacement`. :type: :class:`numpy.ndarray`, size ``(N, M)`` """ return self._getLeafsNormMatrix(self._leaf_matrix_weights, method='weight')
if method not in self._norm_methods.keys(): raise AttributeError( 'Method %s is not in %s' % (method, list(self._norm_methods.keys())))
array.fill(num.nan) for lf in self.leaves: array[lf._slice_rows, lf._slice_cols] = \ self._norm_methods[method](lf) array[self.scene.displacement_mask] = num.nan return array
def center_point(self):
def reduction_efficiency(self): """ This is measure for the reduction of the scene's full resolution over the quadtree.
:getter: Quadtree efficiency as :math:`N_{full} / N_{leaves}` :type: float """ return (self.scene.rows * self.scene.cols) / \ (self.nleaves if self.nleaves else 1)
def reduction_rms(self): """ The RMS error is defined between :attr:`~kite.Quadtree.leaf_matrix_means` and :attr:`kite.Scene.displacement`.
:getter: The reduction RMS error :type: float """ if num.all(num.isnan(self.leaf_matrix_means)): return num.inf return num.sqrt(num.nanmean((self.scene.displacement - self.leaf_matrix_means)**2))
def _base_nodes(self): 2, num.ceil(num.log(num.min(self.displacement.shape)) / num.log(2)))
raise AssertionError('Could not init base nodes.')
def plot(self): """ Simple `matplotlib` illustration of the quadtree
:type: :attr:`Quadtree.leaf_matrix_means`. """ from kite.plot2d import QuadtreePlot return QuadtreePlot(self)
"""Not Implemented """ raise NotImplementedError
""" Get the quadtree as a list of matplotlib rectangles.
:returns: Rectangles for plotting :rtype: list of :class:`matplotlib.patcjes.Rectangle` """
""" Exports the current quadtree leaves to ``filename`` in a *CSV* format
The formatting is::
# node_id, focal_point_E, focal_point_N, theta, phi, \ mean_displacement, median_displacement, absolute_weight
:param filename: export_csv to path :type filename: string """ self._log.debug('Exporting Quadtree as to %s', filename) with open(filename, mode='w') as f: f.write( '# node_id, focal_point_E, focal_point_N, theta, phi,' ' unitE, unitN, unitU,' ' mean_displacement, median_displacement, absolute_weight\n') for lf in self.leaves: f.write( '{lf.id}, {lf.focal_point[0]}, {lf.focal_point[1]}, ' '{lf.theta}, {lf.phi}, {lf.unitE}, {lf.unitN}, {lf.unitU},' ' {lf.mean}, {lf.median}, {lf.weight}\n'.format(lf=lf))
import geojson self._log.debug('Exporting GeoJSON Quadtree to %s', filename) features = []
for lf in self.leaves: llN, llE, urN, urE = (lf.llN, lf.llE, lf.urN, lf.urE)
if self.frame.isDegree(): llN += self.frame.llLat llE += self.frame.llLon urN += self.frame.llLat urE += self.frame.llLon
coords = num.array([ (llN, llE), (llN, urE), (urN, urE), (urN, llE), (llN, llE)])
if self.frame.isMeter(): coords = od.ne_to_latlon( self.frame.llLat, self.frame.llLon, *coords.T) coords = num.array(coords).T
coords = coords[:, [1, 0]].tolist()
feature = geojson.Feature( geometry=geojson.Polygon(coordinates=[coords]), id=lf.id, properties={ 'mean': float(lf.mean), 'median': float(lf.median), 'std': float(lf.std), 'var': float(lf.var),
'phi': float(lf.phi), 'theta': float(lf.theta), 'unitE': float(lf.unitE), 'unitN': float(lf.unitN), 'unitU': float(lf.unitU), }) features.append(feature)
collection = geojson.FeatureCollection( features) with open(filename, 'w') as f: geojson.dump(collection, f)
sha = sha1() sha.update(str(self.config).encode()) return sha.digest().hex()
if __name__ == '__main__': from kite.scene import SceneSynTest sc = SceneSynTest.createGauss(2000, 2000)
for e in num.linspace(0.1, .00005, num=30): sc.quadtree.epsilon = e # qp = Plot2DQuadTree(qt, cmap='spectral') # qp.plot() |