import logging import numpy as num from matplotlib import cm, gridspec from grond.plot.config import PlotConfig from grond.plot.collection import PlotItem from matplotlib import pyplot as plt from matplotlib.ticker import MaxNLocator from matplotlib import patches from pyrocko.guts import Tuple, Float, String, Int, Bool, StringChoice logger = logging.getLogger('grond.targets.satellite.plot') km = 1e3 d2r = num.pi/180. guts_prefix = 'grond' def scale_axes(axis, scale, offset=0., suffix=''): from matplotlib.ticker import ScalarFormatter class FormatScaled(ScalarFormatter): @staticmethod def __call__(value, pos): return '{:,.1f}{:}'.format((offset + value) * scale, suffix)\ .replace(',', ' ') axis.set_major_formatter(FormatScaled()) class SatelliteTargetDisplacement(PlotConfig): ''' Maps showing surface displacements from satellite and modelled data ''' name = 'satellite' dpi = Int.T( default=250) size_cm = Tuple.T( 2, Float.T(), default=(22., 12.)) colormap = String.T( default='RdBu', help='Colormap for the surface displacements') relative_coordinates = Bool.T( default=False, help='Show relative coordinates, initial location centered at 0N, 0E') fit = StringChoice.T( default='best', choices=['best', 'mean'], help='Show the \'best\' or \'mean\' fits and source model from the' ' ensamble.') def make(self, environ): cm = environ.get_plot_collection_manager() history = environ.get_history(subset='harvest') optimiser = environ.get_optimiser() ds = environ.get_dataset() environ.setup_modelling() cm.create_group_mpl( self, self.draw_static_fits(ds, history, optimiser), title=u'InSAR Displacements', section='fits', feather_icon='navigation', description=u''' Maps showing subsampled surface displacements as observed, modelled and the residual (observed minus modelled). The displacement values predicted by the orbit-ambiguity ramps are added to the modelled displacements (middle panels). The color shows the LOS displacement values associated with, and the extent of, every quadtree box. The light grey dots show the focal point of pixels combined in the quadtree box. This point corresponds to the position of the modelled data point. The large dark grey dot shows the reference source position. The grey filled box shows the surface projection of the modelled source, with the thick-lined edge marking the upper fault edge. Complete data extent is shown. ''') def draw_static_fits(self, ds, history, optimiser, closeup=False): from pyrocko.orthodrome import latlon_to_ne_numpy problem = history.problem sat_targets = problem.satellite_targets for target in sat_targets: target.set_dataset(ds) if self.fit == 'best': source = history.get_best_source() model = history.get_best_model() elif self.fit == 'mean': source = history.get_mean_source() model = history.get_mean_model() results = problem.evaluate(model, targets=sat_targets) def initAxes(ax, scene, title, last_axes=False): ax.set_title(title) ax.tick_params(length=2) if scene.frame.isMeter(): ax.set_xlabel('Easting [km]') scale_x = dict(scale=1./km) scale_y = dict(scale=1./km) if not self.relative_coordinates: import utm utm_E, utm_N, utm_zone, utm_zone_letter =\ utm.from_latlon(source.effective_lat, source.effective_lon) scale_x['offset'] = utm_E scale_y['offset'] = utm_N if last_axes: ax.text(0.975, 0.025, 'UTM Zone %d%s' % (utm_zone, utm_zone_letter), va='bottom', ha='right', fontsize=8, alpha=.7, transform=ax.transAxes) ax.set_aspect('equal') elif scene.frame.isDegree(): scale_x = dict(scale=1., suffix='°') scale_y = dict(scale=1., suffix='°') if not self.relative_coordinates: scale_x['offset'] = source.effective_lon scale_y['offset'] = source.effective_lat ax.set_aspect(1./num.cos(source.effective_lat*d2r)) nticks_lon = 4 if abs(scene.frame.llLon) >= 100 else 5 ax.xaxis.set_major_locator(MaxNLocator(nticks_lon)) ax.yaxis.set_major_locator(MaxNLocator(5)) scale_axes(ax.get_xaxis(), **scale_x) scale_axes(ax.get_yaxis(), **scale_y) def drawSource(ax, scene): if scene.frame.isMeter(): fn, fe = source.outline(cs='xy').T fn -= fn.mean() fe -= fe.mean() elif scene.frame.isDegree(): fn, fe = source.outline(cs='latlon').T fn -= source.effective_lat fe -= source.effective_lon # source is centered ax.scatter(0., 0., color='black', s=3, alpha=.5, marker='o') ax.fill(fe, fn, edgecolor=(0., 0., 0.), facecolor=(.5, .5, .5), alpha=0.7) ax.plot(fe[0:2], fn[0:2], 'k', linewidth=1.3) def mapDisplacementGrid(displacements, scene): arr = num.full_like(scene.displacement, fill_value=num.nan) qt = scene.quadtree for syn_v, l in zip(displacements, qt.leaves): arr[l._slice_rows, l._slice_cols] = syn_v arr[scene.displacement_mask] = num.nan return arr def drawLeaves(ax, scene, offset_e=0., offset_n=0.): rects = scene.quadtree.getMPLRectangles() for r in rects: r.set_edgecolor((.4, .4, .4)) r.set_linewidth(.5) r.set_facecolor('none') r.set_x(r.get_x() - offset_e) r.set_y(r.get_y() - offset_n) map(ax.add_artist, rects) ax.scatter(scene.quadtree.leaf_coordinates[:, 0] - offset_e, scene.quadtree.leaf_coordinates[:, 1] - offset_n, s=.25, c='black', alpha=.1) def addArrow(ax, scene): phi = num.nanmean(scene.phi) los_dx = num.cos(phi + num.pi) * .0625 los_dy = num.sin(phi + num.pi) * .0625 az_dx = num.cos(phi - num.pi/2) * .125 az_dy = num.sin(phi - num.pi/2) * .125 anchor_x = .9 if los_dx < 0 else .1 anchor_y = .85 if los_dx < 0 else .975 az_arrow = patches.FancyArrow( x=anchor_x-az_dx, y=anchor_y-az_dy, dx=az_dx, dy=az_dy, head_width=.025, alpha=.5, fc='k', head_starts_at_zero=False, length_includes_head=True, transform=ax.transAxes) los_arrow = patches.FancyArrow( x=anchor_x-az_dx/2, y=anchor_y-az_dy/2, dx=los_dx, dy=los_dy, head_width=.02, alpha=.5, fc='k', head_starts_at_zero=False, length_includes_head=True, transform=ax.transAxes) ax.add_artist(az_arrow) ax.add_artist(los_arrow) urE, urN, llE, llN = (0., 0., 0., 0.) for target in sat_targets: if target.scene.frame.isMeter(): off_n, off_e = map(float, latlon_to_ne_numpy( target.scene.frame.llLat, target.scene.frame.llLon, source.effective_lat, source.effective_lon)) if target.scene.frame.isDegree(): off_n = source.effective_lat - target.scene.frame.llLat off_e = source.effective_lon - target.scene.frame.llLon turE, turN, tllE, tllN = zip( *[(l.gridE.max()-off_e, l.gridN.max()-off_n, l.gridE.min()-off_e, l.gridN.min()-off_n) for l in target.scene.quadtree.leaves]) turE, turN = map(max, (turE, turN)) tllE, tllN = map(min, (tllE, tllN)) urE, urN = map(max, ((turE, urE), (urN, turN))) llE, llN = map(min, ((tllE, llE), (llN, tllN))) def generate_plot(sat_target, result, ifig): scene = sat_target.scene fig = plt.figure() fig.set_size_inches(*self.size_inch) gs = gridspec.GridSpec( 2, 3, wspace=.15, hspace=.2, left=.1, right=.975, top=.95, height_ratios=[12, 1]) item = PlotItem( name='fig_%i' % ifig, attributes={'targets': [sat_target.path]}, title=u'Satellite Surface Displacements - %s' % scene.meta.scene_title, description=u''' Surface displacements derived from satellite data. (Left) the input data, (center) the modelled data and (right) the model residual. '''.format(meta=scene.meta)) stat_obs = result.statics_obs stat_syn = result.statics_syn['displacement.los'] res = stat_obs - stat_syn if scene.frame.isMeter(): offset_n, offset_e = map(float, latlon_to_ne_numpy( scene.frame.llLat, scene.frame.llLon, source.effective_lat, source.effective_lon)) elif scene.frame.isDegree(): offset_n = source.effective_lat - scene.frame.llLat offset_e = source.effective_lon - scene.frame.llLon im_extent = (scene.frame.E.min() - offset_e, scene.frame.E.max() - offset_e, scene.frame.N.min() - offset_n, scene.frame.N.max() - offset_n) abs_displ = num.abs([stat_obs.min(), stat_obs.max(), stat_syn.min(), stat_syn.max(), res.min(), res.max()]).max() cmw = cm.ScalarMappable(cmap=self.colormap) cmw.set_clim(vmin=-abs_displ, vmax=abs_displ) cmw.set_array(stat_obs) axes = [fig.add_subplot(gs[0, 0]), fig.add_subplot(gs[0, 1]), fig.add_subplot(gs[0, 2])] ax = axes[0] ax.imshow(mapDisplacementGrid(stat_obs, scene), extent=im_extent, cmap=self.colormap, vmin=-abs_displ, vmax=abs_displ, origin='lower') drawLeaves(ax, scene, offset_e, offset_n) drawSource(ax, scene) addArrow(ax, scene) initAxes(ax, scene, 'Observed') ax.text(.025, .025, 'Scene ID: %s' % scene.meta.scene_id, fontsize=8, alpha=.7, va='bottom', transform=ax.transAxes) if scene.frame.isMeter(): ax.set_ylabel('Northing [km]') ax = axes[1] ax.imshow(mapDisplacementGrid(stat_syn, scene), extent=im_extent, cmap=self.colormap, vmin=-abs_displ, vmax=abs_displ, origin='lower') drawLeaves(ax, scene, offset_e, offset_n) drawSource(ax, scene) addArrow(ax, scene) initAxes(ax, scene, 'Model') ax.get_yaxis().set_visible(False) ax = axes[2] ax.imshow(mapDisplacementGrid(res, scene), extent=im_extent, cmap=self.colormap, vmin=-abs_displ, vmax=abs_displ, origin='lower') drawLeaves(ax, scene, offset_e, offset_n) drawSource(ax, scene) addArrow(ax, scene) initAxes(ax, scene, 'Residual', last_axes=True) ax.get_yaxis().set_visible(False) for ax in axes: ax.set_xlim(llE, urE) ax.set_ylim(llN, urN) if closeup: if scene.frame.isMeter(): fn, fe = source.outline(cs='xy').T elif scene.frame.isDegree(): fn, fe = source.outline(cs='latlon').T fn -= source.effective_lat fe -= source.effective_lon if fn.size > 1: off_n = (fn[0] + fn[1]) / 2 off_e = (fe[0] + fe[1]) / 2 else: off_n = fn[0] off_e = fe[0] fault_size = 2*num.sqrt(max(abs(fn-off_n))**2 + max(abs(fe-off_e))**2) fault_size *= self.map_scale if fault_size == 0.0: extent = (scene.frame.N[-1] + scene.frame.E[-1]) / 2 fault_size = extent * .25 for ax in axes: ax.set_xlim(-fault_size/2 + off_e, fault_size/2 + off_e) ax.set_ylim(-fault_size/2 + off_n, fault_size/2 + off_n) cax = fig.add_subplot(gs[1, :]) cbar = fig.colorbar(cmw, cax=cax, orientation='horizontal', use_gridspec=True) cbar.set_label('LOS Displacement [m]') return (item, fig) for ifig, (sat_target, result) in enumerate(zip(sat_targets, results)): yield generate_plot(sat_target, result, ifig) class SatelliteTargetDisplacementCloseup(SatelliteTargetDisplacement): ''' Close-up of satellite surface displacements and modelled data. ''' name = 'satellite_closeup' map_scale = Float.T( default=2., help='Scale the map surroundings, larger value zooms out.') def make(self, environ): cm = environ.get_plot_collection_manager() history = environ.get_history(subset='harvest') optimiser = environ.get_optimiser() ds = environ.get_dataset() environ.setup_modelling() cm.create_group_mpl( self, self.draw_static_fits(ds, history, optimiser, closeup=True), title=u'InSAR Displacements (Closeup)', section='fits', feather_icon='zoom-in', description=u''' Maps showing subsampled surface displacements as observed, modelled and the residual (observed minus modelled). The displacement values predicted by the orbit-ambiguity ramps are added to the modelled displacements (middle panels). The color shows the LOS displacement values associated with, and the extent of, every quadtree box. The light grey dots show the focal point of pixels combined in the quadtree box. This point corresponds to the position of the modelled data point. The large dark grey dot shows the reference source position. The grey filled box shows the surface projection of the modelled source, with the thick-lined edge marking the upper fault edge. Map is focused around the fault's extent. ''') def get_plot_classes(): return [SatelliteTargetDisplacement, SatelliteTargetDisplacementCloseup]