Coverage for /usr/local/lib/python3.11/dist-packages/grond/analysers/noise_analyser/analyser.py: 19%

147 statements  

« 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 

4 

5from pyrocko.client import catalog 

6 

7import logging 

8import numpy as num 

9from pyrocko.guts import Int, Bool, Float, String, StringChoice 

10from pyrocko.gf.meta import OutOfBounds 

11from ..base import Analyser, AnalyserConfig, AnalyserResult 

12from grond.dataset import NotFound 

13from grond.meta import store_t 

14 

15logger = logging.getLogger('grond.analysers.NoiseAnalyser') 

16 

17 

18guts_prefix = 'grond' 

19 

20 

21def get_phase_arrival_time(engine, source, target, wavename): 

22 """ 

23 Get arrival time from Green's Function store for respective 

24 :class:`pyrocko.gf.seismosizer.Target`, 

25 :class:`pyrocko.gf.meta.Location` pair. 

26 

27 Parameters 

28 ---------- 

29 engine : :class:`pyrocko.gf.seismosizer.LocalEngine` 

30 source : :class:`pyrocko.gf.meta.Location` 

31 can be therefore :class:`pyrocko.gf.seismosizer.Source` or 

32 :class:`pyrocko.model.Event` 

33 target : :class:`pyrocko.gf.seismosizer.Target` 

34 wavename : string 

35 of the tabulated phase_def that determines the phase arrival 

36 

37 Returns 

38 ------- 

39 scalar, float of the arrival time of the wave 

40 """ 

41 store = engine.get_store(target.store_id) 

42 return store_t(store, wavename, source, target) + source.time 

43 

44 

45def seismic_noise_variance(traces, engine, source, targets, 

46 nwindows, pre_event_noise_duration, 

47 check_events, phase_def): 

48 """ 

49 Calculate variance of noise in a given time before P-Phase onset. 

50 

51 Optionally check the gCMT earthquake catalogue for M>5 events interfering. 

52 

53 Parameters 

54 ---------- 

55 data_traces : list 

56 of :class:`pyrocko.trace.Trace` containing observed data 

57 engine : :class:`pyrocko.gf.seismosizer.LocalEngine` 

58 processing object for synthetics calculation 

59 source : :class:`pyrocko.gf.Source` 

60 reference source 

61 targets : list 

62 of :class:`pyrocko.gf.seismosizer.Targets` 

63 nwindows : integer 

64 number of windows in which the noise trace is split. If not 1, the 

65 variance is calculated for each window separately and a mean 

66 variance is returned. Else, the variance is calculated on the 

67 entire pre-event noise window. 

68 pre_event_noise_duration : Time before the first arrival to include in the 

69 noise analysis 

70 phase_def : :class:'pyrocko.gf.Timing' 

71 arrivals : list 

72 of :class'pyrocko.gf.Timing' arrivals of waveforms 

73 at station 

74 

75 Returns 

76 ------- 

77 :class:`numpy.ndarray` 

78 """ 

79 

80 var_ds = [] 

81 global_cmt_catalog = catalog.GlobalCMT() 

82 var_ds = [] 

83 ev_ws = [] 

84 for tr, target in zip(traces, targets): 

85 stat_w = 1. 

86 

87 if tr is None: 

88 var_ds.append(num.nan) 

89 ev_ws.append(num.nan) 

90 else: 

91 

92 arrival_time = get_phase_arrival_time( 

93 engine=engine, source=source, 

94 target=target, wavename=phase_def) 

95 if check_events: 

96 events = global_cmt_catalog.get_events( 

97 time_range=( 

98 arrival_time-pre_event_noise_duration-50.*60., 

99 arrival_time), 

100 magmin=5.,) 

101 for ev in events: 

102 try: 

103 arrival_time_pre = get_phase_arrival_time( 

104 engine=engine, 

105 source=ev, 

106 target=target, 

107 wavename=phase_def) 

108 

109 if arrival_time_pre > arrival_time \ 

110 - pre_event_noise_duration \ 

111 and arrival_time_pre < arrival_time: 

112 

113 stat_w = 0. 

114 logger.info( 

115 'Noise analyser found event "%s" phase onset ' 

116 'of "%s" for target "%s".' % ( 

117 ev.name, phase_def, target.name)) 

118 

119 if arrival_time_pre > arrival_time-30.*60.\ 

120 and arrival_time_pre < arrival_time - \ 

121 pre_event_noise_duration: 

122 stat_w *= 1. 

123 logger.info( 

124 'Noise analyser found event "%s" possibly ' 

125 'contaminating the noise.' % ev.name) 

126 

127 # this should be magnitude dependent 

128 except Exception: 

129 pass 

130 ev_ws.append(stat_w) 

131 

132 if nwindows == 1: 

133 vtrace_var = num.nanvar(tr.ydata) 

134 var_ds.append(vtrace_var) 

135 else: 

136 win = arrival_time - (arrival_time - 

137 pre_event_noise_duration) 

138 win_len = win/nwindows 

139 v_traces_w = [] 

140 for i in range(0, nwindows): 

141 vtrace_w = tr.chop( 

142 tmin=win+win_len*i, 

143 tmax=arrival_time+win_len*i+1, 

144 inplace=False) 

145 v_traces_w.append(vtrace_w.ydata) 

146 v_traces_w = num.nanmean(v_traces_w) 

147 var_ds.append(v_traces_w) 

148 var_ds = num.array(var_ds, dtype=float) 

149 ev_ws = num.array(ev_ws, dtype=float) 

150 return var_ds, ev_ws 

151 

152 

153class NoiseAnalyser(Analyser): 

154 ''' 

155 From the pre-event station noise variance-based trace weights are formed. 

156 

157 By default, the trace weights are the inverse of the noise variance. The 

158 correlation of the noise is neglected. Optionally, using a the gCMT global 

159 earthquake catalogue, the station data are checked for theoretical phase 

160 arrivals of M>5 earthquakes. In case of a very probable contamination the 

161 trace weights are set to zero. In case global earthquake phase arrivals are 

162 within a 30 min time window before the start of the set pre-event noise 

163 window, only a warning is thrown. 

164 

165 It is further possible to disregard data with a noise level exceeding the 

166 median by a given ``cutoff`` factor. These weights are set to 0. This can 

167 be done exclusively (``mode='weeding'``) such that noise weights are either 

168 1 or 0, or in combination with weighting below the median-times-cutoff 

169 noise level (``mode='weighting'``). 

170 ''' 

171 

172 def __init__(self, nwindows, pre_event_noise_duration, 

173 check_events, phase_def, statistic, mode, cutoff, 

174 cutoff_exception_on_high_snr): 

175 

176 Analyser.__init__(self) 

177 self.nwindows = nwindows 

178 self.pre_event_noise_duration = pre_event_noise_duration 

179 self.check_events = check_events 

180 self.phase_def = phase_def 

181 self.statistic = statistic 

182 self.mode = mode 

183 self.cutoff = cutoff 

184 self.cutoff_exception_on_high_snr = cutoff_exception_on_high_snr 

185 

186 def analyse(self, problem, ds): 

187 

188 tdur = self.pre_event_noise_duration 

189 

190 if tdur == 0: 

191 return 

192 

193 if not problem.has_waveforms: 

194 return 

195 

196 engine = problem.get_engine() 

197 source = problem.base_source 

198 

199 paths = sorted(set(t.path for t in problem.waveform_targets)) 

200 

201 for path in paths: 

202 targets = [t for t in problem.waveform_targets if t.path == path] 

203 

204 deltats = set() 

205 for target in targets: # deltat diff check? 

206 store = engine.get_store(target.store_id) 

207 deltats.add(float(store.config.deltat)) 

208 

209 if len(deltats) > 1: 

210 logger.warning( 

211 'Differing sampling rates in stores used. Using highest.') 

212 

213 deltat = min(deltats) 

214 

215 data = [] 

216 for target in targets: 

217 try: 

218 freqlimits = list(target.get_freqlimits()) 

219 freqlimits = tuple(freqlimits) 

220 

221 source = problem.base_source 

222 

223 arrival_time = get_phase_arrival_time( 

224 engine=engine, 

225 source=source, 

226 target=target, 

227 wavename=self.phase_def) 

228 

229 tmin_fit, tmax_fit, tfade, tfade_taper = \ 

230 target.get_taper_params(engine, source) 

231 

232 data.append([ 

233 tmin_fit, 

234 tmax_fit, 

235 tfade_taper, 

236 ds.get_waveform( 

237 target.codes, 

238 tmin=arrival_time-tdur-tfade, 

239 tmax=tmax_fit+tfade, 

240 tfade=tfade, 

241 freqlimits=freqlimits, 

242 deltat=deltat, 

243 backazimuth=target.get_backazimuth_for_waveform(), 

244 tinc_cache=1./freqlimits[0], 

245 debug=False)]) 

246 

247 except (NotFound, OutOfBounds) as e: 

248 logger.debug(str(e)) 

249 data.append([None, None, None, None]) 

250 

251 traces_noise = [] 

252 traces_signal = [] 

253 for tmin_fit, tmax_fit, tfade_taper, tr in data: 

254 if tr: 

255 traces_noise.append( 

256 tr.chop(tr.tmin, tr.tmin + tdur, inplace=False)) 

257 traces_signal.append( 

258 tr.chop( 

259 tmin_fit-tfade_taper, 

260 tmax_fit+tfade_taper, 

261 inplace=False)) 

262 else: 

263 traces_noise.append(None) 

264 traces_signal.append(None) 

265 

266 var_ds, ev_ws = seismic_noise_variance( 

267 traces_noise, engine, source, targets, 

268 self.nwindows, tdur, 

269 self.check_events, self.phase_def) 

270 

271 amp_maxs = num.array([ 

272 (tr.absmax()[1] if tr else num.nan) for tr in traces_signal]) 

273 

274 if self.statistic == 'var': 

275 noise = var_ds 

276 elif self.statistic == 'std': 

277 noise = num.sqrt(var_ds) 

278 else: 

279 assert False, 'invalid statistic argument' 

280 

281 ok = num.isfinite(noise) 

282 

283 if num.sum(ok) == 0: 

284 norm_noise = 0.0 

285 else: 

286 norm_noise = num.median(noise[ok]) 

287 

288 if norm_noise == 0.0: 

289 logger.info( 

290 'Noise Analyser returned a weight of 0 for all stations.') 

291 

292 assert num.all(noise[ok] >= 0.0) 

293 

294 ce_factor = self.cutoff_exception_on_high_snr 

295 high_snr = num.zeros(ok.size, dtype=bool) 

296 if ce_factor is not None: 

297 high_snr[ok] = amp_maxs[ok] > ce_factor * num.sqrt(var_ds)[ok] 

298 

299 weights = num.zeros(noise.size) 

300 if self.mode == 'weighting': 

301 weights[ok] = norm_noise / noise[ok] 

302 elif self.mode == 'weeding': 

303 weights[ok] = 1.0 

304 else: 

305 assert False, 'invalid mode argument' 

306 

307 if self.cutoff is not None: 

308 weights[ok] = num.where( 

309 num.logical_or( 

310 noise[ok] <= norm_noise * self.cutoff, 

311 high_snr[ok]), 

312 weights[ok], 0.0) 

313 

314 if self.check_events: 

315 weights = weights*ev_ws 

316 

317 for itarget, target in enumerate(targets): 

318 logger.info(( 

319 'Noise analysis for target "%s":\n' 

320 ' var: %g\n' 

321 ' std: %g\n' 

322 ' max/std: %g\n' 

323 ' %s/median(%s): %g\n' 

324 ' contamination_weight: %g\n' 

325 ' weight: %g') % ( 

326 target.string_id(), 

327 var_ds[itarget], 

328 num.sqrt(var_ds[itarget]), 

329 amp_maxs[itarget] / num.sqrt(var_ds[itarget]), 

330 self.statistic, self.statistic, 

331 noise[itarget] / norm_noise, 

332 ev_ws[itarget], 

333 weights[itarget])) 

334 

335 for weight, target in zip(weights, targets): 

336 target.analyser_results['noise'] = \ 

337 NoiseAnalyserResult(weight=float(weight)) 

338 

339 

340class NoiseAnalyserResult(AnalyserResult): 

341 weight = Float.T( 

342 help='The inverse of the pre-event data variance or standard ' 

343 'deviation. If traces were checked for other event phase ' 

344 'arrivals, the weight can be zero for contaminated traces.') 

345 

346 

347class NoiseAnalyserConfig(AnalyserConfig): 

348 """Configuration parameters for the pre-event noise analysis.""" 

349 

350 nwindows = Int.T( 

351 default=1, 

352 help='number of windows for trace splitting') 

353 

354 pre_event_noise_duration = Float.T( 

355 default=0., 

356 help='Total length of noise trace in the analysis') 

357 

358 phase_def = String.T( 

359 default='P', 

360 help='Onset of phase_def used for upper limit of window') 

361 

362 check_events = Bool.T( 

363 default=False, 

364 help='check the GlobalCMT for M>5 earthquakes' 

365 ' that produce phase arrivals' 

366 ' contaminating and affecting the noise analysis') 

367 

368 statistic = StringChoice.T( 

369 choices=('var', 'std'), 

370 default='var', 

371 help='Set weight to inverse of noise variance (var) or standard ' 

372 'deviation (std).') 

373 

374 mode = StringChoice.T( 

375 choices=('weighting', 'weeding'), 

376 default='weighting', 

377 help='Generate weights based on inverse of noise measure (weighting), ' 

378 'or discrete on/off style in combination with cutoff value ' 

379 '(weeding).') 

380 

381 cutoff = Float.T( 

382 optional=True, 

383 help='Set weight to zero, when noise level exceeds median by the ' 

384 'given cutoff factor.') 

385 

386 cutoff_exception_on_high_snr = Float.T( 

387 optional=True, 

388 help='Exclude from cutoff when max amplitude exceeds standard ' 

389 'deviation times this factor.') 

390 

391 def get_analyser(self): 

392 return NoiseAnalyser( 

393 nwindows=self.nwindows, 

394 pre_event_noise_duration=self.pre_event_noise_duration, 

395 check_events=self.check_events, phase_def=self.phase_def, 

396 statistic=self.statistic, mode=self.mode, cutoff=self.cutoff, 

397 cutoff_exception_on_high_snr=self.cutoff_exception_on_high_snr) 

398 

399 

400__all__ = ''' 

401 NoiseAnalyser 

402 NoiseAnalyserConfig 

403'''.split()