1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import numpy as num 

7import logging 

8from os import path as op 

9 

10from datetime import datetime 

11from pyrocko import gf, util 

12from pyrocko import orthodrome as od 

13from pyrocko.guts import Float, Timestamp, Tuple, StringChoice, Bool, Object,\ 

14 String 

15from pyrocko.util import num_full 

16 

17from .base import TargetGenerator, NoiseGenerator 

18from ..base import get_gsshg 

19 

20DEFAULT_STORE_ID = 'ak135_static' 

21 

22km = 1e3 

23d2r = num.pi/180. 

24 

25logger = logging.getLogger('pyrocko.scenario.targets.insar') 

26guts_prefix = 'pf.scenario' 

27 

28 

29class ScenePatch(Object): 

30 center_lat = Float.T( 

31 help='Center latitude anchor.') 

32 center_lon = Float.T( 

33 help='center longitude anchor.') 

34 time_master = Timestamp.T( 

35 help='Timestamp of the master.') 

36 time_slave = Timestamp.T( 

37 help='Timestamp of the slave.') 

38 inclination = Float.T( 

39 help='Orbital inclination towards the equatorial plane [deg].') 

40 apogee = Float.T( 

41 help='Apogee of the satellite in [m].') 

42 swath_width = Float.T( 

43 help='Swath width in [m].') 

44 track_length = Float.T( 

45 help='Track length in [m].') 

46 incident_angle = Float.T( 

47 help='Ground incident angle in [deg].') 

48 resolution = Tuple.T( 

49 help='Resolution of raster in east x north [px].') 

50 orbital_node = StringChoice.T( 

51 ['Ascending', 'Descending'], 

52 help='Orbit heading.') 

53 mask_water = Bool.T( 

54 default=True, 

55 help='Mask water bodies.') 

56 

57 class SatelliteGeneratorTarget(gf.SatelliteTarget): 

58 

59 def __init__(self, scene_patch, *args, **kwargs): 

60 gf.SatelliteTarget.__init__(self, *args, **kwargs) 

61 

62 self.scene_patch = scene_patch 

63 

64 def post_process(self, *args, **kwargs): 

65 resp = gf.SatelliteTarget.post_process(self, *args, **kwargs) 

66 

67 from kite import Scene 

68 from kite.scene import SceneConfig, FrameConfig, Meta 

69 

70 patch = self.scene_patch 

71 

72 grid, _ = patch.get_grid() 

73 

74 displacement = num.empty_like(grid) 

75 displacement.fill(num.nan) 

76 displacement[patch.get_mask()] = resp.result['displacement.los'] 

77 

78 theta, phi = patch.get_incident_angles() 

79 

80 llLat, llLon = patch.get_ll_anchor() 

81 urLat, urLon = patch.get_ur_anchor() 

82 dLon = num.abs(llLon - urLon) / patch.resolution[0] 

83 dLat = num.abs(llLat - urLat) / patch.resolution[1] 

84 

85 scene_config = SceneConfig( 

86 meta=Meta( 

87 scene_title='Pyrocko Scenario Generator - {orbit} ({time})' 

88 .format(orbit=self.scene_patch.orbital_node, 

89 time=datetime.now()), 

90 orbital_node=patch.orbital_node, 

91 scene_id='pyrocko_scenario_%s' 

92 % self.scene_patch.orbital_node, 

93 satellite_name='Sentinel-1 (pyrocko-scenario)'), 

94 frame=FrameConfig( 

95 llLat=float(llLat), 

96 llLon=float(llLon), 

97 dN=float(dLat), 

98 dE=float(dLon), 

99 spacing='degree')) 

100 

101 scene = Scene( 

102 displacement=displacement, 

103 theta=theta, 

104 phi=phi, 

105 config=scene_config) 

106 

107 resp.scene = scene 

108 

109 return resp 

110 

111 def __init__(self, *args, **kwargs): 

112 Object.__init__(self, *args, **kwargs) 

113 self._mask_water = None 

114 

115 @property 

116 def width(self): 

117 track_shift = num.abs( 

118 num.cos(self.inclination*d2r) * self.track_length) 

119 

120 return self.swath_width + track_shift 

121 

122 def get_ll_anchor(self): 

123 return od.ne_to_latlon(self.center_lat, self.center_lon, 

124 -self.track_length/2, -self.width/2) 

125 

126 def get_ur_anchor(self): 

127 return od.ne_to_latlon(self.center_lat, self.center_lon, 

128 self.track_length/2, self.width/2) 

129 

130 def get_ul_anchor(self): 

131 return od.ne_to_latlon(self.center_lat, self.center_lon, 

132 self.track_length/2, -self.width/2) 

133 

134 def get_lr_anchor(self): 

135 return od.ne_to_latlon(self.center_lat, self.center_lon, 

136 -self.track_length/2, self.width/2) 

137 

138 def get_corner_coordinates(self): 

139 inc = self.inclination 

140 

141 llLat, llLon = self.get_ll_anchor() 

142 urLat, urLon = self.get_ur_anchor() 

143 

144 if self.orbital_node == 'Ascending': 

145 

146 ulLat, ulLon = od.ne_to_latlon( 

147 self.center_lat, self.center_lon, 

148 self.track_length/2, 

149 -num.tan(inc*d2r) * self.width/2) 

150 lrLat, lrLon = od.ne_to_latlon( 

151 self.center_lat, self.center_lon, 

152 -self.track_length/2, 

153 num.tan(inc*d2r) * self.width/2) 

154 

155 elif self.orbital_node == 'Descending': 

156 urLat, urLon = od.ne_to_latlon( 

157 self.center_lat, self.center_lon, 

158 self.track_length/2, 

159 num.tan(inc*d2r) * self.width/2) 

160 llLat, llLon = od.ne_to_latlon( 

161 self.center_lat, self.center_lon, 

162 -self.track_length/2, 

163 -num.tan(inc*d2r) * self.width/2) 

164 

165 return ((llLat, llLon), (ulLat, ulLon), 

166 (urLat, urLon), (lrLat, lrLon)) 

167 

168 def get_grid(self): 

169 ''' 

170 Return relative positions of scatterer. 

171 

172 :param track: Acquisition track, from `'asc'` or `'dsc'`. 

173 :type track: string 

174 ''' 

175 easts = num.linspace(0, self.width, 

176 self.resolution[0]) 

177 norths = num.linspace(0, self.track_length, 

178 self.resolution[1]) 

179 

180 return num.meshgrid(easts, norths) 

181 

182 def get_mask_track(self): 

183 east_shifts, north_shifts = self.get_grid() 

184 norths = north_shifts[:, 0] 

185 track = num.abs(num.cos(self.inclination*d2r)) * norths 

186 

187 track_mask = num.logical_and( 

188 east_shifts > track[:, num.newaxis], 

189 east_shifts < (track + self.swath_width)[:, num.newaxis]) 

190 

191 if self.orbital_node == 'Ascending': 

192 track_mask = num.fliplr(track_mask) 

193 

194 return track_mask 

195 

196 def get_mask_water(self): 

197 if self._mask_water is None: 

198 east_shifts, north_shifts = self.get_grid() 

199 

200 east_shifts -= east_shifts[0, -1]/2 

201 north_shifts -= north_shifts[-1, -1]/2 

202 

203 latlon = od.ne_to_latlon(self.center_lat, self.center_lon, 

204 north_shifts.ravel(), east_shifts.ravel()) 

205 points = num.array(latlon).T 

206 self._mask_water = get_gsshg().get_land_mask(points)\ 

207 .reshape(*east_shifts.shape) 

208 

209 return self._mask_water 

210 

211 def get_mask(self): 

212 mask_track = self.get_mask_track() 

213 if self.mask_water: 

214 mask_water = self.get_mask_water() 

215 return num.logical_and(mask_track, mask_water) 

216 return mask_track 

217 

218 def get_incident_angles(self): 

219 # theta: elevation angle towards satellite from horizon in radians. 

220 # phi: Horizontal angle towards satellite' :abbr:`line of sight (LOS)` 

221 # in [rad] from East. 

222 east_shifts, _ = self.get_grid() 

223 

224 phi = num.empty_like(east_shifts) 

225 theta = num.empty_like(east_shifts) 

226 

227 east_shifts += num.tan(self.incident_angle*d2r) * self.apogee 

228 theta = num.arctan(east_shifts/self.apogee) 

229 

230 if self.orbital_node == 'Ascending': 

231 phi.fill(self.inclination*d2r + num.pi/2) 

232 elif self.orbital_node == 'Descending': 

233 phi.fill(2*num.pi-(self.inclination*d2r + 3/2*num.pi)) 

234 theta = num.fliplr(theta) 

235 else: 

236 raise AttributeError( 

237 'Orbital node %s not defined!' % self.orbital_node) 

238 

239 theta[~self.get_mask()] = num.nan 

240 phi[~self.get_mask()] = num.nan 

241 

242 return theta, phi 

243 

244 def get_target(self): 

245 gE, gN = self.get_grid() 

246 mask = self.get_mask() 

247 

248 east_shifts = gE[mask].ravel() 

249 north_shifts = gN[mask].ravel() 

250 llLat, llLon = self.get_ll_anchor() 

251 

252 ncoords = east_shifts.size 

253 

254 theta, phi = self.get_incident_angles() 

255 

256 theta = theta[mask].ravel() 

257 phi = phi[mask].ravel() 

258 

259 if ncoords == 0: 

260 logger.warning('InSAR taget has no valid points,' 

261 ' maybe it\'s all water?') 

262 

263 return self.SatelliteGeneratorTarget( 

264 scene_patch=self, 

265 lats=num_full(ncoords, fill_value=llLat), 

266 lons=num_full(ncoords, fill_value=llLon), 

267 east_shifts=east_shifts, 

268 north_shifts=north_shifts, 

269 theta=theta, 

270 phi=phi) 

271 

272 

273class AtmosphericNoiseGenerator(NoiseGenerator): 

274 

275 amplitude = Float.T( 

276 default=1., 

277 help='Amplitude of the atmospheric noise.') 

278 

279 beta = [5./3, 8./3, 2./3] 

280 regimes = [.15, .99, 1.] 

281 

282 def get_noise(self, scene): 

283 nE = scene.frame.rows 

284 nN = scene.frame.cols 

285 

286 if (nE+nN) % 2 != 0: 

287 raise ArithmeticError('Dimensions of synthetic scene must ' 

288 'both be even!') 

289 

290 dE = scene.frame.dE 

291 dN = scene.frame.dN 

292 

293 rfield = num.random.rand(nE, nN) 

294 spec = num.fft.fft2(rfield) 

295 

296 kE = num.fft.fftfreq(nE, dE) 

297 kN = num.fft.fftfreq(nN, dN) 

298 k_rad = num.sqrt(kN[:, num.newaxis]**2 + kE[num.newaxis, :]**2) 

299 

300 regimes = num.array(self.regimes) 

301 

302 k0 = 0. 

303 k1 = regimes[0] * k_rad.max() 

304 k2 = regimes[1] * k_rad.max() 

305 

306 r0 = num.logical_and(k_rad > k0, k_rad < k1) 

307 r1 = num.logical_and(k_rad >= k1, k_rad < k2) 

308 r2 = k_rad >= k2 

309 

310 beta = num.array(self.beta) 

311 # From Hanssen (2001) 

312 # beta+1 is used as beta, since, the power exponent 

313 # is defined for a 1D slice of the 2D spectrum: 

314 # austin94: "Adler, 1981, shows that the surface profile 

315 # created by the intersection of a plane and a 

316 # 2-D fractal surface is itself fractal with 

317 # a fractal dimension equal to that of the 2D 

318 # surface decreased by one." 

319 beta += 1. 

320 # From Hanssen (2001) 

321 # The power beta/2 is used because the power spectral 

322 # density is proportional to the amplitude squared 

323 # Here we work with the amplitude, instead of the power 

324 # so we should take sqrt( k.^beta) = k.^(beta/2) RH 

325 # beta /= 2. 

326 

327 amp = num.zeros_like(k_rad) 

328 amp[r0] = k_rad[r0] ** -beta[0] 

329 amp[r0] /= amp[r0].max() 

330 

331 amp[r1] = k_rad[r1] ** -beta[1] 

332 amp[r1] /= amp[r1].max() / amp[r0].min() 

333 

334 amp[r2] = k_rad[r2] ** -beta[2] 

335 amp[r2] /= amp[r2].max() / amp[r1].min() 

336 

337 amp[k_rad == 0.] = amp.max() 

338 

339 spec *= self.amplitude * num.sqrt(amp) 

340 noise = num.abs(num.fft.ifft2(spec)) 

341 noise -= num.mean(noise) 

342 

343 return noise 

344 

345 

346class InSARGenerator(TargetGenerator): 

347 # https://sentinel.esa.int/web/sentinel/user-guides/sentinel-1-sar/acquisition-modes/interferometric-wide-swath 

348 store_id = String.T( 

349 default=DEFAULT_STORE_ID, 

350 help='Store ID for these stations.') 

351 

352 inclination = Float.T( 

353 default=98.2, 

354 help='Inclination of the satellite orbit towards equatorial plane' 

355 ' in [deg]. Defaults to Sentinel-1 (98.1 deg).') 

356 apogee = Float.T( 

357 default=693.*km, 

358 help='Apogee of the satellite in [m]. ' 

359 'Defaults to Sentinel-1 (693 km).') 

360 swath_width = Float.T( 

361 default=250*km, 

362 help='Swath width in [m]. ' 

363 'Defaults to Sentinel-1 Interfeometric Wide Swath Mode (IW).' 

364 ' (IW; 250 km).') 

365 track_length = Float.T( 

366 default=250*km, 

367 help='Track length in [m]. Defaults to 200 km.') 

368 incident_angle = Float.T( 

369 default=29.1, 

370 help='Near range incident angle in [deg]. Defaults to 29.1 deg;' 

371 ' Sentinel IW mode (29.1 - 46.0 deg).') 

372 resolution = Tuple.T( 

373 default=(250, 250), 

374 help='Resolution of raster in east x north [px].') 

375 mask_water = Bool.T( 

376 default=True, 

377 help='Mask out water bodies.') 

378 noise_generator = NoiseGenerator.T( 

379 default=AtmosphericNoiseGenerator.D(), 

380 help='Add atmospheric noise model after Hansen, 2001.') 

381 

382 def get_scene_patches(self): 

383 center_lat, center_lon = self.get_center_latlon() 

384 

385 scene_patches = [] 

386 for direction in ('Ascending', 'Descending'): 

387 patch = ScenePatch( 

388 center_lat=center_lat, 

389 center_lon=center_lon, 

390 time_master=0, 

391 time_slave=0, 

392 inclination=self.inclination, 

393 apogee=self.apogee, 

394 swath_width=self.swath_width, 

395 track_length=self.track_length, 

396 incident_angle=self.incident_angle, 

397 resolution=self.resolution, 

398 orbital_node=direction, 

399 mask_water=self.mask_water) 

400 scene_patches.append(patch) 

401 

402 return scene_patches 

403 

404 def get_targets(self): 

405 targets = [s.get_target() for s in self.get_scene_patches()] 

406 

407 for t in targets: 

408 t.store_id = self.store_id 

409 

410 return targets 

411 

412 def get_insar_scenes(self, engine, sources, tmin=None, tmax=None): 

413 logger.debug('Forward modelling InSAR displacement...') 

414 

415 scenario_tmin, scenario_tmax = self.get_time_range(sources) 

416 

417 try: 

418 resp = engine.process( 

419 sources, 

420 self.get_targets(), 

421 nthreads=0) 

422 except gf.meta.OutOfBounds: 

423 logger.warning('Could not calculate InSAR displacements' 

424 ' - the GF store\'s extend is too small!') 

425 return [] 

426 

427 scenes = [res.scene for res in resp.static_results()] 

428 

429 tmin, tmax = self.get_time_range(sources) 

430 for sc in scenes: 

431 sc.meta.time_master = util.to_time_float(tmin) 

432 sc.meta.time_slave = util.to_time_float(tmax) 

433 

434 scenes_asc = [sc for sc in scenes 

435 if sc.config.meta.orbital_node == 'Ascending'] 

436 scenes_dsc = [sc for sc in scenes 

437 if sc.config.meta.orbital_node == 'Descending'] 

438 

439 def stack_scenes(scenes): 

440 base = scenes[0] 

441 for sc in scenes[1:]: 

442 base += sc 

443 return base 

444 

445 scene_asc = stack_scenes(scenes_asc) 

446 scene_dsc = stack_scenes(scenes_dsc) 

447 

448 if self.noise_generator: 

449 scene_asc.displacement += self.noise_generator.get_noise(scene_asc) 

450 scene_dsc.displacement += self.noise_generator.get_noise(scene_dsc) 

451 

452 return scene_asc, scene_dsc 

453 

454 def ensure_data(self, engine, sources, path, tmin=None, tmax=None): 

455 

456 path_insar = op.join(path, 'insar') 

457 util.ensuredir(path_insar) 

458 

459 tmin, tmax = self.get_time_range(sources) 

460 tts = util.time_to_str 

461 

462 fn_tpl = op.join(path_insar, 'colosseo-scene-{orbital_node}_%s_%s' 

463 % (tts(tmin, '%Y-%m-%d'), tts(tmax, '%Y-%m-%d'))) 

464 

465 def scene_fn(track): 

466 return fn_tpl.format(orbital_node=track.lower()) 

467 

468 for track in ('ascending', 'descending'): 

469 fn = '%s.yml' % scene_fn(track) 

470 

471 if op.exists(fn): 

472 logger.debug('Scene exists: %s' % fn) 

473 continue 

474 

475 scenes = self.get_insar_scenes(engine, sources, tmin, tmax) 

476 for sc in scenes: 

477 fn = scene_fn(sc.config.meta.orbital_node) 

478 logger.debug('Writing %s' % fn) 

479 sc.save('%s.npz' % fn) 

480 

481 def add_map_artists(self, engine, sources, automap): 

482 logger.warning('InSAR mapping is not implemented!') 

483 return None