Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

# -*- coding: utf-8 -*- 

""" 

Module for handling ObsPy RtTrace objects. 

 

:copyright: 

    The ObsPy Development Team (devs@obspy.org) & Anthony Lomax 

:license: 

    GNU Lesser General Public License, Version 3 

    (http://www.gnu.org/copyleft/lesser.html) 

""" 

 

from obspy import Trace 

from obspy.core import Stats 

from obspy.realtime import signal 

from obspy.realtime.rtmemory import RtMemory 

import copy 

import numpy as np 

import warnings 

 

 

# dictionary to map given type-strings to processing functions keys must be all 

# lower case - values are tuples: (function name, number of RtMemory objects) 

REALTIME_PROCESS_FUNCTIONS = { 

    'scale': (signal.scale, 0), 

    'integrate': (signal.integrate, 1), 

    'differentiate': (signal.differentiate, 1), 

    'boxcar': (signal.boxcar, 1), 

    'tauc': (signal.tauc, 2), 

    'mwpintegral': (signal.mwpIntegral, 1), 

} 

 

 

class RtTrace(Trace): 

    """ 

    An object containing data of a continuous series constructed dynamically 

    from sequential data packets. 

 

    New data packets may be periodically appended. Registered time-domain 

    processes can be applied to the new data and the resulting trace will be 

    left trimmed to maintain a specified maximum trace length. 

 

    :type max_length: int, optional 

    :param max_length: maximum trace length in seconds 

 

    .. rubric:: Example 

 

    RtTrace has been built to handle real time processing of periodically 

    append data packets, such as adding and processing data requested from an 

    SeedLink server. See :mod:`obspy.seedlink` for further information. 

 

    For the sake of simplicity we will just split data of an existing example 

    file into multiple chucks (Trace objects) of about equal size (step 1 + 2) 

    and append those chunks in a simple loop (step 4) into an RtTrace object. 

    Additionally there are two real time processing functions registered to the 

    RtTrace object (step 3) which will automatically process any appended data 

    chunks. 

 

    1. Read first trace of example SAC data file and extract contained time 

       offset and epicentral distance of an earthquake:: 

 

        >>> import numpy as np 

        >>> from obspy.realtime import RtTrace 

        >>> from obspy import read 

        >>> from obspy.realtime.signal import calculateMwpMag 

        >>> data_trace = read('/path/to/II.TLY.BHZ.SAC')[0] 

        >>> len(data_trace) 

        12684 

        >>> ref_time_offset = data_trace.stats.sac.a 

        >>> print(ref_time_offset) 

        301.506 

        >>> epicentral_distance = data_trace.stats.sac.gcarc 

        >>> print(epicentral_distance) 

        30.0855 

 

    2. Split given trace into a list of three sub-traces:: 

 

        >>> traces = data_trace / 3 

        >>> [len(tr) for tr in traces] 

        [4228, 4228, 4228] 

 

    3. Assemble real time trace and register two processes:: 

 

        >>> rt_trace = RtTrace() 

        >>> rt_trace.registerRtProcess('integrate') 

        1 

        >>> rt_trace.registerRtProcess('mwpIntegral', mem_time=240, 

        ...     ref_time=(data_trace.stats.starttime + ref_time_offset), 

        ...     max_time=120, gain=1.610210e+09) 

        2 

 

    4. Append and auto-process packet data into RtTrace:: 

 

        >>> for tr in traces: 

        ...     processed_trace = rt_trace.append(tr, gap_overlap_check=True) 

        ... 

        >>> len(rt_trace) 

        12684 

 

    5. Some post processing to get Mwp:: 

 

        >>> peak = np.amax(np.abs(rt_trace.data)) 

        >>> print(peak) 

        0.136404 

        >>> mwp = calculateMwpMag(peak, epicentral_distance) 

        >>> print(mwp) 

        8.78902911791 

    """ 

    have_appended_data = False 

 

    @classmethod 

    def rtProcessFunctionsToString(cls): 

        """ 

        Return doc string for all predefined real-time processing functions. 

 

        :rtype: str 

        :return: String containing doc for all real-time processing functions. 

        """ 

        string = 'Real-time processing functions (use as: ' + \ 

            'RtTrace.registerRtProcess(process_name, [parameter values])):\n' 

        for key in REALTIME_PROCESS_FUNCTIONS: 

            string += '\n' 

            string += '  ' + (str(key) + ' ' + 80 * '-')[:80] 

            string += str(REALTIME_PROCESS_FUNCTIONS[key][0].__doc__) 

        return(string) 

 

    def __init__(self, max_length=None, *args, **kwargs):  # @UnusedVariable 

        """ 

        Initializes an RtTrace. 

 

        See :class:`obspy.core.trace.Trace` for all parameters. 

        """ 

        # set window length attribute 

        if max_length is not None and max_length <= 0: 

            raise ValueError("Input max_length out of bounds: %s" % max_length) 

        self.max_length = max_length 

 

        # initialize processing list 

        self.processing = [] 

 

        # initialize parent Trace with no data or header - all data must be 

        # added using append 

        super(RtTrace, self).__init__(data=np.array([]), header=None) 

 

    def __eq__(self, other): 

        """ 

        Implements rich comparison of RtTrace objects for "==" operator. 

 

        Traces are the same, if both their data and stats are the same. 

        """ 

        # check if other object is a RtTrace 

        if not isinstance(other, RtTrace): 

            return False 

        return super(RtTrace, self).__eq__(other) 

 

    def __add__(self, **kwargs):  # @UnusedVariable 

        """ 

        Too ambiguous, throw an Error. 

 

        .. seealso:: :meth:`obspy.realtime.RtTrace.append`. 

        """ 

        msg = "Too ambiguous for realtime trace data. Try: RtTrace.append()" 

        raise NotImplementedError(msg) 

 

    def append(self, trace, gap_overlap_check=False, verbose=False): 

        """ 

        Appends a Trace object to this RtTrace. 

 

        Registered real-time processing will be applied to copy of appended 

        Trace object before it is appended.  This RtTrace will be truncated 

        from the beginning to RtTrace.max_length, if specified. 

        Sampling rate, data type and trace.id of both traces must match. 

 

        :type trace: :class:`~obspy.core.trace.Trace` 

        :param trace:  :class:`~obspy.core.trace.Trace` object to append to 

            this RtTrace 

        :type gap_overlap_check: bool, optional 

        :param gap_overlap_check: Action to take when there is a gap or overlap 

            between the end of this RtTrace and start of appended Trace: 

                If True, raise TypeError. 

                If False, all trace processing memory will be re-initialized to 

                    prevent false signal in processed trace. 

            (default is ``True``). 

        :type verbose: bool, optional 

        :param verbose: Print additional information to stdout 

        :return: NumPy :class:`np.ndarray` object containing processed trace 

            data from appended Trace object. 

        """ 

        if not isinstance(trace, Trace): 

            # only add Trace objects 

            raise TypeError("Only obspy.core.trace.Trace objects are allowed") 

 

        # sanity checks 

        if self.have_appended_data: 

            #  check id 

            if self.getId() != trace.getId(): 

                raise TypeError("Trace ID differs:", self.getId(), 

                                trace.getId()) 

            #  check sample rate 

            if self.stats.sampling_rate != trace.stats.sampling_rate: 

                raise TypeError("Sampling rate differs:", 

                                self.stats.sampling_rate, 

                                trace.stats.sampling_rate) 

            #  check calibration factor 

            if self.stats.calib != trace.stats.calib: 

                raise TypeError("Calibration factor differs:", 

                                self.stats.calib, trace.stats.calib) 

            # check data type 

            if self.data.dtype != trace.data.dtype: 

                raise TypeError("Data type differs:", 

                                self.data.dtype, trace.data.dtype) 

        # TODO: IMPORTANT? Should improve check for gaps and overlaps 

        # and handle more elegantly 

        # check times 

        gap_or_overlap = False 

        if self.have_appended_data: 

            #delta = int(math.floor(\ 

            #    round((rt.stats.starttime - lt.stats.endtime) * sr, 5) )) - 1 

            diff = trace.stats.starttime - self.stats.endtime 

            delta = diff * self.stats.sampling_rate - 1.0 

            if verbose: 

                msg = "%s: Overlap/gap of (%g) samples in data: (%s) (%s) " + \ 

                    "diff=%gs  dt=%gs" 

                print msg % (self.__class__.__name__, 

                             delta, self.stats.endtime, trace.stats.starttime, 

                             diff, self.stats.delta) 

            if delta < -0.1: 

                msg = "Overlap of (%g) samples in data: (%s) (%s) diff=%gs" + \ 

                    "  dt=%gs" 

                msg = msg % (-delta, self.stats.endtime, trace.stats.starttime, 

                             diff, self.stats.delta) 

                if gap_overlap_check: 

                    raise TypeError(msg) 

                gap_or_overlap = True 

            if delta > 0.1: 

                msg = "Gap of (%g) samples in data: (%s) (%s) diff=%gs" + \ 

                    "  dt=%gs" 

                msg = msg % (delta, self.stats.endtime, trace.stats.starttime, 

                             diff, self.stats.delta) 

                if gap_overlap_check: 

                    raise TypeError(msg) 

                gap_or_overlap = True 

            if gap_or_overlap: 

                msg += " - Trace processing memory will be re-initialized." 

                warnings.warn(msg, UserWarning) 

            else: 

                # correct start time to pin absolute trace timing to start of 

                # appended trace, this prevents slow drift of nominal trace 

                # timing from absolute time when nominal sample rate differs 

                # from true sample rate 

                self.stats.starttime = \ 

                    self.stats.starttime + diff - self.stats.delta 

                if verbose: 

                    print "%s: self.stats.starttime adjusted by: %gs" \ 

                        % (self.__class__.__name__, diff - self.stats.delta) 

        # first apply all registered processing to Trace 

        for proc in self.processing: 

            process_name, options, rtmemory_list = proc 

            # if gap or overlap, clear memory 

            if gap_or_overlap and rtmemory_list != None: 

                for n in range(len(rtmemory_list)): 

                    rtmemory_list[n] = RtMemory() 

            # apply processing 

            trace = trace.copy() 

            dtype = trace.data.dtype 

            if hasattr(process_name, '__call__'): 

                # check if direct function call 

                trace.data = process_name(trace.data, **options) 

            else: 

                # got predefined function 

                func = REALTIME_PROCESS_FUNCTIONS[process_name.lower()][0] 

                options['rtmemory_list'] = rtmemory_list 

                trace.data = func(trace, **options) 

            # assure dtype is not changed 

            trace.data = np.require(trace.data, dtype=dtype) 

        # if first data, set stats 

        if not self.have_appended_data: 

            self.data = np.array(trace.data) 

            self.stats = Stats(header=trace.stats) 

            self.have_appended_data = True 

            return trace 

        # handle all following data sets 

        # fix Trace.__add__ parameters 

        # TODO: IMPORTANT? Should check for gaps and overlaps and handle 

        # more elegantly 

        sum_trace = Trace.__add__(self, trace, method=0, 

            interpolation_samples=0, fill_value='latest', sanity_checks=True) 

        # Trace.__add__ returns new Trace, so update to this RtTrace 

        self.data = sum_trace.data 

        # left trim if data length exceeds max_length 

        if self.max_length != None: 

            max_samples = int(self.max_length * self.stats.sampling_rate + 0.5) 

            if np.size(self.data) > max_samples: 

                starttime = self.stats.starttime + (np.size(self.data) - \ 

                    max_samples) / self.stats.sampling_rate 

                self._ltrim(starttime, pad=False, nearest_sample=True, 

                            fill_value=None) 

        return trace 

 

    def registerRtProcess(self, process, **options): 

        """ 

        Adds real-time processing algorithm to processing list of this RtTrace. 

 

        Processing function must be one of: 

            %s. % REALTIME_PROCESS_FUNCTIONS.keys() 

            or a non-recursive, time-domain np or obspy function which takes 

            a single array as an argument and returns an array 

 

        :type process: str or function 

        :param process: Specifies which processing function is added, 

            e.g. ``"boxcar"`` or ``np.abs``` (functions without brackets). 

            See :mod:`obspy.realtime.signal` for all predefined processing 

            functions. 

        :type options: dict, optional 

        :param options: Required keyword arguments to be passed the respective 

            processing function, e.g. ``width=100`` for ``'boxcar'`` process. 

            See :mod:`obspy.realtime.signal` for all options. 

        :rtype: int 

        :return: Length of processing list after registering new processing 

            function. 

        """ 

        # create process_name either from string or function name 

        process_name = ("%s" % process).lower() 

 

        # set processing entry for this process 

        entry = False 

        rtmemory_list = None 

        if hasattr(process, '__call__'): 

            # direct function call 

            entry = (process, options, None) 

        elif process_name in REALTIME_PROCESS_FUNCTIONS: 

            # predefined function 

            num = REALTIME_PROCESS_FUNCTIONS[process_name][1] 

            if num: 

                # make sure we have num new RtMemory instances 

                rtmemory_list = [RtMemory() for _i in range(num)] 

            entry = (process_name, options, rtmemory_list) 

        else: 

            # check if process name is contained within a predefined function, 

            # e.g. 'int' for 'integrate' 

            for key in REALTIME_PROCESS_FUNCTIONS: 

                if not key.startswith(process_name): 

                    continue 

                process_name = key 

                num = REALTIME_PROCESS_FUNCTIONS[process_name][1] 

                if num: 

                    # make sure we have num new RtMemory instances 

                    rtmemory_list = [RtMemory() for _i in range(num)] 

                entry = (process_name, options, rtmemory_list) 

                break 

 

        if not entry: 

            raise NotImplementedError("Can't register process %s" % (process)) 

 

        # add process entry 

        self.processing.append(entry) 

 

        # add processing information to the stats dictionary 

        proc_info = "realtime_process:%s:%s" % (process_name, options) 

        self._addProcessingInfo(proc_info) 

 

        return len(self.processing) 

 

    def copy(self, *args, **kwargs): 

        """ 

        Returns a deepcopy of this RtTrace. 

        """ 

        # XXX: ugly hack to allow deepcopy of an RtTrace object containing 

        # registered NumPy function (numpy.ufunc) calls 

        temp = copy.copy(self.processing) 

        self.processing = [] 

        new = copy.deepcopy(self, *args, **kwargs) 

        new.processing = temp 

        return new 

 

 

if __name__ == '__main__': 

    import doctest 

    doctest.testmod(exclude_empty=True)