Coverage for /usr/local/lib/python3.11/dist-packages/grond/targets/satellite/plot.py: 13%
298 statements
« prev ^ index » next coverage.py v6.5.0, created at 2024-11-27 15:15 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2024-11-27 15:15 +0000
1# https://pyrocko.org/grond - GPLv3
2#
3# The Grond Developers, 21st Century
4import logging
5import numpy as num
6from matplotlib import cm, gridspec
8from grond.plot.config import PlotConfig
9from grond.plot.collection import PlotItem
11from matplotlib import pyplot as plt
12from matplotlib.ticker import MaxNLocator, FuncFormatter
13from matplotlib import patches
14from pyrocko.guts import Tuple, Float, String, Int, Bool, StringChoice
16logger = logging.getLogger('grond.targets.satellite.plot')
18km = 1e3
19d2r = num.pi/180.
20guts_prefix = 'grond'
23def drape_displacements(
24 displacement, shad_data, mappable,
25 shad_lim=(.4, .98), contrast=1., mask=None):
26 '''Map color data (displacement) on shaded relief.'''
28 from scipy.ndimage import convolve as im_conv
29 # Light source from somewhere above - psychologically the best choice
30 # from upper left
31 ramp = num.array([[1, 0], [0, -1.]]) * contrast
33 # convolution of two 2D arrays
34 shad = im_conv(shad_data*km, ramp.T)
35 shad *= -1.
37 # if there are strong artifical edges in the data, shades get
38 # dominated by them. Cutting off the largest and smallest 2% of
39 # shades helps
40 percentile2 = num.quantile(shad, 0.02)
41 percentile98 = num.quantile(shad, 0.98)
42 shad[shad > percentile98] = percentile98
43 shad[shad < percentile2] = percentile2
45 # normalize shading
46 shad -= num.nanmin(shad)
47 shad /= num.nanmax(shad)
49 if mask is not None:
50 shad[mask] = num.nan
52 # reduce range to balance gray color
53 shad *= shad_lim[1] - shad_lim[0]
54 shad += shad_lim[0]
56 rgb_map = mappable.to_rgba(displacement)
57 rgb_map[num.isnan(displacement)] = 1.
58 rgb_map[:, :, :3] *= shad[:, :, num.newaxis]
60 return rgb_map
63def displ2rad(displ, wavelength):
64 return (displ % wavelength) / wavelength * num.pi
67def scale_axes(axis, scale, offset=0., suffix=''):
68 from matplotlib.ticker import ScalarFormatter
70 class FormatScaled(ScalarFormatter):
72 @staticmethod
73 def __call__(value, pos):
74 return '{:,.1f}{:}'.format((offset + value) * scale, suffix)\
75 .replace(',', ' ')
77 axis.set_major_formatter(FormatScaled())
80class SatelliteTargetDisplacement(PlotConfig):
81 ''' Maps showing surface displacements from satellite and modelled data '''
83 name = 'satellite'
84 dpi = Int.T(
85 default=250)
86 size_cm = Tuple.T(
87 2, Float.T(),
88 default=(22., 12.))
89 colormap = String.T(
90 default='RdBu',
91 help='Colormap for the surface displacements')
92 relative_coordinates = Bool.T(
93 default=False,
94 help='Show relative coordinates, initial location centered at 0N, 0E')
95 fit = StringChoice.T(
96 default='best', choices=['best', 'mean'],
97 help='Show the \'best\' or \'mean\' fits and source model from the'
98 ' ensamble.')
100 show_topo = Bool.T(
101 default=True,
102 help='Drape displacements over the topography.')
103 displacement_unit = StringChoice.T(
104 default='m',
105 choices=['m', 'mm', 'cm', 'rad'],
106 help="Show results in 'm', 'cm', 'mm' or 'rad' for radians.")
107 show_leaf_centres = Bool.T(
108 default=True,
109 help='show the center points of Quadtree leaves')
110 source_outline_color = String.T(
111 default='grey',
112 help='Choose color of source outline from named matplotlib Colors')
113 common_color_scale = Bool.T(
114 default=True,
115 help='Results shown with common color scale for all satellite '
116 'data sets (based on the data)')
117 map_limits = Tuple.T(
118 4, Float.T(),
119 optional=True,
120 help='Overwrite map limits in native coordinates. '
121 'Use (xmin, xmax, ymin, ymax)')
122 nticks_x = Int.T(
123 optional=True,
124 help='Number of ticks on the x-axis.')
126 def make(self, environ):
127 cm = environ.get_plot_collection_manager()
128 history = environ.get_history(subset='harvest')
129 optimiser = environ.get_optimiser()
130 ds = environ.get_dataset()
132 environ.setup_modelling()
134 cm.create_group_mpl(
135 self,
136 self.draw_static_fits(ds, history, optimiser),
137 title=u'InSAR Displacements',
138 section='fits',
139 feather_icon='navigation',
140 description=u'''
141Maps showing subsampled surface displacements as observed, modelled and the
142residual (observed minus modelled).
144The displacement values predicted by the orbit-ambiguity ramps are added to the
145modelled displacements (middle panels). The color shows the LOS displacement
146values associated with, and the extent of, every quadtree box. The light grey
147dots show the focal point of pixels combined in the quadtree box. This point
148corresponds to the position of the modelled data point.
150The large dark grey dot shows the reference source position. The grey filled
151box shows the surface projection of the modelled source, with the thick-lined
152edge marking the upper fault edge. Complete data extent is shown.
153''')
155 def draw_static_fits(self, ds, history, optimiser, closeup=False):
156 from pyrocko.orthodrome import latlon_to_ne_numpy
157 problem = history.problem
159 sat_targets = problem.satellite_targets
160 for target in sat_targets:
161 target.set_dataset(ds)
163 if self.fit == 'best':
164 source = history.get_best_source()
165 model = history.get_best_model()
166 elif self.fit == 'mean':
167 source = history.get_mean_source()
168 model = history.get_mean_model()
170 results = problem.evaluate(model, targets=sat_targets)
172 def init_axes(ax, scene, title, last_axes=False):
173 ax.set_title(title, fontsize=self.font_size)
174 ax.tick_params(length=2)
176 if scene.frame.isMeter():
177 import utm
178 ax.set_xlabel('Easting [km]', fontsize=self.font_size)
179 scale_x = dict(scale=1./km)
180 scale_y = dict(scale=1./km)
181 utm_E, utm_N, utm_zone, utm_zone_letter =\
182 utm.from_latlon(source.effective_lat,
183 source.effective_lon)
184 scale_x['offset'] = utm_E
185 scale_y['offset'] = utm_N
187 if last_axes:
188 ax.text(0.975, 0.025,
189 'UTM Zone %d%s' % (utm_zone, utm_zone_letter),
190 va='bottom', ha='right',
191 fontsize=8, alpha=.7,
192 transform=ax.transAxes)
193 ax.set_aspect('equal')
195 elif scene.frame.isDegree():
196 scale_x = dict(scale=1., suffix='°')
197 scale_y = dict(scale=1., suffix='°')
198 scale_x['offset'] = source.effective_lon
199 scale_y['offset'] = source.effective_lat
201 ax.set_aspect(1./num.cos(source.effective_lat*d2r))
203 if self.relative_coordinates:
204 scale_x['offset'] = 0.
205 scale_y['offset'] = 0.
207 nticks_x = 4 if abs(scene.frame.llLon) >= 100 else 5
209 ax.xaxis.set_major_locator(MaxNLocator(self.nticks_x or nticks_x))
210 ax.yaxis.set_major_locator(MaxNLocator(5))
212 ax.scale_x = scale_x
213 ax.scale_y = scale_y
215 scale_axes(ax.get_xaxis(), **scale_x)
216 scale_axes(ax.get_yaxis(), **scale_y)
218 def draw_source(ax, scene):
219 if scene.frame.isMeter():
220 fn, fe = source.outline(cs='xy').T
221 fn -= fn.mean()
222 fe -= fe.mean()
223 elif scene.frame.isDegree():
224 fn, fe = source.outline(cs='latlon').T
225 fn -= source.effective_lat
226 fe -= source.effective_lon
228 # source is centered
229 ax.scatter(0., 0., color='black', s=3, alpha=.5, marker='o')
230 ax.fill(fe, fn,
231 edgecolor=(0., 0., 0.),
232 facecolor=self.source_outline_color,
233 alpha=0.7)
234 ax.plot(fe[0:2], fn[0:2], 'k', linewidth=1.3)
236 def get_displacement_rgba(displacements, scene, mappable):
237 arr = num.full_like(scene.displacement, fill_value=num.nan)
238 qt = scene.quadtree
240 for syn_v, leaf in zip(displacements, qt.leaves):
241 arr[leaf._slice_rows, leaf._slice_cols] = syn_v
243 arr[scene.displacement_mask] = num.nan
245 if not self.common_color_scale \
246 and not self.displacement_unit == 'rad':
247 abs_displ = num.abs(displacements).max()
248 mappable.set_clim(-abs_displ, abs_displ)
250 if self.show_topo:
251 try:
252 elevation = scene.get_elevation()
253 return drape_displacements(arr, elevation, mappable)
254 except Exception as e:
255 logger.warning('could not plot hillshaded topo')
256 logger.exception(e)
257 rgb_arr = mappable.to_rgba(arr)
258 rgb_arr[num.isnan(arr)] = 1.
259 rgb_arr[scene.displacement_mask] = 1.
261 return rgb_arr
263 def draw_leaves(ax, scene, offset_e=0., offset_n=0.):
264 rects = scene.quadtree.getMPLRectangles()
265 for r in rects:
266 r.set_edgecolor((.4, .4, .4))
267 r.set_linewidth(.5)
268 r.set_facecolor('none')
269 r.set_x(r.get_x() - offset_e)
270 r.set_y(r.get_y() - offset_n)
271 map(ax.add_artist, rects)
273 if self.show_leaf_centres:
274 ax.scatter(scene.quadtree.leaf_coordinates[:, 0] - offset_e,
275 scene.quadtree.leaf_coordinates[:, 1] - offset_n,
276 s=.25, c='black', alpha=.1)
278 def add_arrow(ax, scene):
279 phi = num.nanmean(scene.phi)
280 los_dx = num.cos(phi + num.pi) * .0625
281 los_dy = num.sin(phi + num.pi) * .0625
283 az_dx = num.cos(phi - num.pi/2) * .125
284 az_dy = num.sin(phi - num.pi/2) * .125
286 anchor_x = .9 if los_dx < 0 else .1
287 anchor_y = .85 if los_dx < 0 else .975
289 az_arrow = patches.FancyArrow(
290 x=anchor_x-az_dx, y=anchor_y-az_dy,
291 dx=az_dx, dy=az_dy,
292 head_width=.025,
293 alpha=.5, fc='k',
294 head_starts_at_zero=False,
295 length_includes_head=True,
296 transform=ax.transAxes)
298 los_arrow = patches.FancyArrow(
299 x=anchor_x-az_dx/2, y=anchor_y-az_dy/2,
300 dx=los_dx, dy=los_dy,
301 head_width=.02,
302 alpha=.5, fc='k',
303 head_starts_at_zero=False,
304 length_includes_head=True,
305 transform=ax.transAxes)
307 ax.add_artist(az_arrow)
308 ax.add_artist(los_arrow)
310 urE, urN, llE, llN = (0., 0., 0., 0.)
311 for target in sat_targets:
313 if target.scene.frame.isMeter():
314 off_n, off_e = map(float, latlon_to_ne_numpy(
315 target.scene.frame.llLat, target.scene.frame.llLon,
316 source.effective_lat, source.effective_lon))
317 if target.scene.frame.isDegree():
318 off_n = source.effective_lat - target.scene.frame.llLat
319 off_e = source.effective_lon - target.scene.frame.llLon
321 turE, turN, tllE, tllN = zip(
322 *[(leaf.gridE.max()-off_e,
323 leaf.gridN.max()-off_n,
324 leaf.gridE.min()-off_e,
325 leaf.gridN.min()-off_n)
326 for leaf in target.scene.quadtree.leaves])
328 turE, turN = map(max, (turE, turN))
329 tllE, tllN = map(min, (tllE, tllN))
330 urE, urN = map(max, ((turE, urE), (urN, turN)))
331 llE, llN = map(min, ((tllE, llE), (llN, tllN)))
333 def generate_plot(sat_target, result, ifig):
335 scene = sat_target.scene
337 fig = plt.figure()
338 fig.set_size_inches(*self.size_inch)
339 gs = gridspec.GridSpec(
340 2, 3,
341 wspace=.15, hspace=.2,
342 left=.1, right=.975, top=.95,
343 height_ratios=[12, 1])
345 item = PlotItem(
346 name='fig_%i' % ifig,
347 attributes={'targets': [sat_target.path]},
348 title=u'Satellite Surface Displacements - %s'
349 % scene.meta.scene_title,
350 description=u'''
351Surface displacements derived from satellite data.
352(Left) the input data, (center) the modelled
353data and (right) the model residual.
354''')
356 stat_obs = result.statics_obs['displacement.los']
357 stat_syn = result.statics_syn['displacement.los']
358 res = stat_obs - stat_syn
360 if scene.frame.isMeter():
361 offset_n, offset_e = map(float, latlon_to_ne_numpy(
362 scene.frame.llLat, scene.frame.llLon,
363 source.effective_lat, source.effective_lon))
364 elif scene.frame.isDegree():
365 offset_n = source.effective_lat - scene.frame.llLat
366 offset_e = source.effective_lon - scene.frame.llLon
368 im_extent = (
369 scene.frame.E.min() - offset_e,
370 scene.frame.E.max() - offset_e,
371 scene.frame.N.min() - offset_n,
372 scene.frame.N.max() - offset_n)
374 if self.displacement_unit == 'rad':
375 wavelength = scene.meta.wavelength
376 if wavelength is None:
377 raise AttributeError(
378 'The satellite\'s wavelength is not set')
380 stat_obs = displ2rad(stat_obs, wavelength)
381 stat_syn = displ2rad(stat_syn, wavelength)
382 res = displ2rad(res, wavelength)
384 self.colormap = 'hsv'
385 data_range = (0., num.pi)
387 else:
388 abs_displ = num.abs([stat_obs.min(), stat_obs.max(),
389 stat_syn.min(), stat_syn.max(),
390 res.min(), res.max()]).max()
391 data_range = (-abs_displ, abs_displ)
393 cmw = cm.ScalarMappable(cmap=self.colormap)
394 cmw.set_clim(*data_range)
395 cmw.set_array(stat_obs)
397 axes = [fig.add_subplot(gs[0, 0]),
398 fig.add_subplot(gs[0, 1]),
399 fig.add_subplot(gs[0, 2])]
401 ax = axes[0]
402 ax.imshow(
403 get_displacement_rgba(stat_obs, scene, cmw),
404 extent=im_extent, origin='lower')
405 draw_leaves(ax, scene, offset_e, offset_n)
406 draw_source(ax, scene)
407 add_arrow(ax, scene)
408 init_axes(ax, scene, 'Observed')
410 ax.text(.025, .025, 'Scene ID: %s' % scene.meta.scene_id,
411 fontsize=8, alpha=.7,
412 va='bottom', transform=ax.transAxes)
413 if scene.frame.isMeter():
414 ax.set_ylabel('Northing [km]', fontsize=self.font_size)
416 ax = axes[1]
417 ax.imshow(
418 get_displacement_rgba(stat_syn, scene, cmw),
419 extent=im_extent, origin='lower')
420 draw_leaves(ax, scene, offset_e, offset_n)
421 draw_source(ax, scene)
422 add_arrow(ax, scene)
423 init_axes(ax, scene, 'Model')
424 ax.get_yaxis().set_visible(False)
426 ax = axes[2]
427 ax.imshow(
428 get_displacement_rgba(res, scene, cmw),
429 extent=im_extent, origin='lower')
431 draw_leaves(ax, scene, offset_e, offset_n)
432 draw_source(ax, scene)
433 add_arrow(ax, scene)
434 init_axes(ax, scene, 'Residual', last_axes=True)
435 ax.get_yaxis().set_visible(False)
437 for ax in axes:
438 ax.set_xlim(*im_extent[:2])
439 ax.set_ylim(*im_extent[2:])
441 if closeup:
442 if scene.frame.isMeter():
443 fn, fe = source.outline(cs='xy').T
444 elif scene.frame.isDegree():
445 fn, fe = source.outline(cs='latlon').T
446 fn -= source.effective_lat
447 fe -= source.effective_lon
449 if fn.size > 1:
450 off_n = (fn[0] + fn[1]) / 2
451 off_e = (fe[0] + fe[1]) / 2
452 else:
453 off_n = fn[0]
454 off_e = fe[0]
456 fault_size = 2*num.sqrt(max(abs(fn-off_n))**2
457 + max(abs(fe-off_e))**2)
458 fault_size *= self.map_scale
459 if fault_size == 0.0:
460 extent = (scene.frame.N[-1] + scene.frame.E[-1]) / 2
461 fault_size = extent * .25
463 for ax in axes:
464 ax.set_xlim(-fault_size/2 + off_e, fault_size/2 + off_e)
465 ax.set_ylim(-fault_size/2 + off_n, fault_size/2 + off_n)
467 if self.map_limits is not None:
468 xmin, xmax, ymin, ymax = self.map_limits
469 assert xmin < xmax, 'bad map_limits xmin > xmax'
470 assert ymin < ymax, 'bad map_limits ymin > ymax'
472 for ax in axes:
473 ax.set_xlim(
474 xmin/ax.scale_x['scale'] - ax.scale_x['offset'],
475 xmax/ax.scale_x['scale'] - ax.scale_x['offset'],)
476 ax.set_ylim(
477 ymin/ax.scale_y['scale'] - ax.scale_y['offset'],
478 ymax/ax.scale_y['scale'] - ax.scale_y['offset'])
480 if self.displacement_unit == 'm':
481 def cfmt(x, p):
482 return '%.2f' % x
483 elif self.displacement_unit == 'cm':
484 def cfmt(x, p):
485 return '%.1f' % (x * 1e2)
486 elif self.displacement_unit == 'mm':
487 def cfmt(x, p):
488 return '%.0f' % (x * 1e3)
489 elif self.displacement_unit == 'rad':
490 def cfmt(x, p):
491 return '%.2f' % x
492 else:
493 raise AttributeError(
494 'unknown displacement unit %s' % self.displacement_unit)
496 cbar_args = dict(
497 orientation='horizontal',
498 format=FuncFormatter(cfmt),
499 use_gridspec=True)
500 cbar_label = 'LOS Displacement [%s]' % self.displacement_unit
502 if self.common_color_scale:
503 cax = fig.add_subplot(gs[1, 1])
504 cax.set_aspect(.05)
505 cbar = fig.colorbar(cmw, cax=cax, **cbar_args)
506 cbar.set_label(cbar_label)
507 else:
508 for idata, data in enumerate((stat_syn, stat_obs, res)):
509 cax = fig.add_subplot(gs[1, idata])
510 cax.set_aspect(.05)
512 if not self.displacement_unit == 'rad':
513 abs_displ = num.abs(data).max()
514 cmw.set_clim(-abs_displ, abs_displ)
516 cbar = fig.colorbar(cmw, cax=cax, **cbar_args)
517 cbar.set_label(cbar_label)
519 return (item, fig)
521 for ifig, (sat_target, result) in enumerate(zip(sat_targets, results)):
522 yield generate_plot(sat_target, result, ifig)
525class SatelliteTargetDisplacementCloseup(SatelliteTargetDisplacement):
526 ''' Close-up of satellite surface displacements and modelled data. '''
527 name = 'satellite_closeup'
529 map_scale = Float.T(
530 default=2.,
531 help='Scale the map surroundings, larger value zooms out.')
533 def make(self, environ):
534 cm = environ.get_plot_collection_manager()
535 history = environ.get_history(subset='harvest')
536 optimiser = environ.get_optimiser()
537 ds = environ.get_dataset()
539 environ.setup_modelling()
541 cm.create_group_mpl(
542 self,
543 self.draw_static_fits(ds, history, optimiser, closeup=True),
544 title=u'InSAR Displacements (Closeup)',
545 section='fits',
546 feather_icon='zoom-in',
547 description=u'''
548Maps showing subsampled surface displacements as observed, modelled and the
549residual (observed minus modelled).
551The displacement values predicted by the orbit-ambiguity ramps are added to the
552modelled displacements (middle panels). The color shows the LOS displacement
553values associated with, and the extent of, every quadtree box. The light grey
554dots show the focal point of pixels combined in the quadtree box. This point
555corresponds to the position of the modelled data point.
557The large dark grey dot shows the reference source position. The grey filled
558box shows the surface projection of the modelled source, with the thick-lined
559edge marking the upper fault edge. Map is focused around the fault's extent.
560''')
563def get_plot_classes():
564 return [SatelliteTargetDisplacement, SatelliteTargetDisplacementCloseup]