crawtools.compliance

crawtools.compliance.compliance_functions

Python compliance functions: - gravd - raydep - zp_to_norm_compliance - frf_to_compliance - calc_norm_compliance

Authors: A. Doran, W Crawford

calc_norm_compliance

calc_norm_compliance(depth, freq, model)

Calculate normalized compliance for a model and water depth

Parameters:
  • depth (float) –

    water depth (m)

  • freq (

    class:numpy.nparray): frequencies (1/s)

  • model (

    class:EarthModel1D): 1D earth model

depth = 2000 freqs = np.array([0.001, 0.003, 0.005, 0.01, 0.03]) model = EarthModel1D([[1000, 3000, 3000, 1600], ... [1000, 3000, 4000, 2300], ... [1000, 3000, 5000, 2800], ... [3000, 3000, 7500, 4300], ... [3000, 3000, 8200, 4700]]) np.set_printoptions(precision=1) calc_norm_compliance(depth, freqs, model) array([1.4e-11, 2.0e-11, 2.6e-11, 4.2e-11, 9.0e-11])

Source code in crawtools/compliance/compliance_functions.py
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
def calc_norm_compliance(depth, freq, model):
    """
    Calculate normalized compliance for a model and water depth

    Args:
        depth (float): water depth (m)
        freq (:class:`numpy.nparray`): frequencies (1/s)
        model (:class:`EarthModel1D`): 1D earth model

    >>> depth = 2000
    >>> freqs = np.array([0.001, 0.003, 0.005, 0.01, 0.03])
    >>> model = EarthModel1D([[1000, 3000, 3000, 1600],
    ...                      [1000, 3000, 4000, 2300],
    ...                      [1000, 3000, 5000, 2800],
    ...                      [3000, 3000, 7500, 4300],
    ...                      [3000, 3000, 8200, 4700]])
    >>> np.set_printoptions(precision=1)
    >>> calc_norm_compliance(depth, freqs, model)
    array([1.4e-11, 2.0e-11, 2.6e-11, 4.2e-11, 9.0e-11])
    """
    if np.any(freq<=0):
        raise ValueError('At least one freq <= 0: cannot calculate compliance')
    vpsq = model.vps * model.vps
    vssq = model.vss * model.vss
    omega = 2 * np.pi * freq
    k = gravd(omega, depth)
    ps = k / omega

    ncomp = np.zeros((len(ps)))
    for i in np.arange((len(ps))):
        v, u, sigzz, sigzx = raydep(ps[i], omega[i], model.thicks, model.rhos,
                                    vpsq, vssq)
        ncomp[i] = -k[i] * v[1-1] / (omega[i] * sigzz[1-1])
    # # The following should give the same answer
    # vs, us, sigzzs, sigzxs = [raydep(p, w, thick, rho, vpsq, vssq)
    #                           for p, w in zip(ps, omega)]
    # ncomp = zp_to_norm_compliance(freq, vs/sigzzs, depth)
    return ncomp

frf_to_compliance

frf_to_compliance(xf, wdepth, z_units='M/S')

Changes the response for each out_channel from z_units/Pa to 1/Pa (normalized compliance)

Parameters:
  • xf (

    class:tiskit.TransferFunction): z/p transfer function(s)

  • wdepth (float) –

    water depth (m)

  • z_units (str, default: 'M/S' ) –

    z units, one of 'M', 'M/S' or 'M/S^2'

Source code in crawtools/compliance/compliance_functions.py
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
def frf_to_compliance(xf, wdepth, z_units='M/S'):
    """
    Changes the response for each out_channel from z_units/Pa
    to 1/Pa (normalized compliance)

    Args:
        xf (:class:`tiskit.TransferFunction`): z/p transfer function(s)
        wdepth (float): water depth (m)
        z_units (str): z units, one of 'M', 'M/S' or 'M/S^2'
    """
    compl = deepcopy(xf)
    for oc in compl.output_channels:
        if not compl.output_units(oc).upper() == z_units:
            raise ValueError('output_units({}) ({}) != "{}"'.format(
                oc, compl.output_units(oc), z_units))
        if not compl.input_units.upper() == 'PA':
            raise ValueError(f'input_units ({compl.input_units}) != "PA"')
        orig_resp = compl.response(oc)
        new_resp = zp_to_norm_compliance(compl.freqs, orig_resp,
                                         wdepth, z_units)
        print(f'{new_resp/orig_resp=}')
        print(f'BEFORE {compl.response(oc)=}')
        compl.put_response(new_resp, oc)
        print(f'AFTER {compl.response(oc)=}')
        # compl._ds["response"].loc[dict(input=compl.input_channel,
        #                             output=oc)] = new_resp
    return compl

gravd

gravd(W, h)

Linear ocean surface gravity wave dispersion

Parameters:
  • W (

    class:numpy.ndarray): angular frequencies (rad/s)

  • h (float) –

    water depth (m)

Returns:
  • K( :class:`numpy.ndarray` ) –

    wavenumbers (rad/m)

f = np.array([0.0001, 0.001, 0.01, 0.1, 1, 10]) K = gravd(2 * np.pi * f, 2000) wlen = 2 * np.pi * np.power(K, -1) np.set_printoptions(precision=1) print(f'{K=} rad/m') K=array([4.5e-06, 4.5e-05, 5.2e-04, 4.0e-02, 4.0e+00, 4.0e+02]) rad/m print(f'{wlen=} m') wlen=array([1.4e+06, 1.4e+05, 1.2e+04, 1.6e+02, 1.6e+00, 1.6e-02]) m print(f'c={f*wlen} m/s') c=[140. 139.8 121.1 15.6 1.6 0.2] m/s

Source code in crawtools/compliance/compliance_functions.py
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
def gravd(W, h):
    """
    Linear ocean surface gravity wave dispersion

    Args:
        W (:class:`numpy.ndarray`): angular frequencies (rad/s)
        h (float): water depth (m)

    Returns:
        K (:class:`numpy.ndarray`): wavenumbers (rad/m)

    >>> f = np.array([0.0001, 0.001, 0.01, 0.1, 1, 10])
    >>> K = gravd(2 * np.pi * f, 2000)
    >>> wlen = 2 * np.pi * np.power(K, -1)
    >>> np.set_printoptions(precision=1)
    >>> print(f'{K=} rad/m')
    K=array([4.5e-06, 4.5e-05, 5.2e-04, 4.0e-02, 4.0e+00, 4.0e+02]) rad/m
    >>> print(f'{wlen=} m')
    wlen=array([1.4e+06, 1.4e+05, 1.2e+04, 1.6e+02, 1.6e+00, 1.6e-02]) m
    >>> print(f'c={f*wlen} m/s')
    c=[140.  139.8 121.1  15.6   1.6   0.2] m/s
    """
    # W must be array
    if not isinstance(W, np.ndarray):
        W = np.array([W])
    if np.any(W < 0):
        raise ValueError('there are omegas <= 0')
    G = 9.79329
    N = len(W)
    W2 = W*W
    kDEEP = W2/G
    kSHAL = W/(np.sqrt(G*h))
    erDEEP = np.ones(np.shape(W)) - G*kDEEP*_dtanh(kDEEP*h)/W2
    one = np.ones(np.shape(W))
    d = np.copy(one)
    done = np.zeros(np.shape(W))
    done[W==0] = 1   # if W==0, k is also zero
    nd = np.where(done == 0)

    k1 = np.copy(kDEEP)
    k2 = np.copy(kSHAL)
    e1 = np.copy(erDEEP)
    ktemp = np.copy(done)
    e2 = np.copy(done)
    e2[W==0] = 0

    while True:
        e2[nd] = one[nd] - G*k2[nd] * _dtanh(k2[nd]*h)/W2[nd]
        d = e2*e2
        done = d < 1e-20
        if done.all():
            K = k2
            break
        nd = np.where(done == 0)
        ktemp[nd] = k1[nd]-e1[nd]*(k2[nd]-k1[nd])/(e2[nd]-e1[nd])
        k1[nd] = k2[nd]
        k2[nd] = ktemp[nd]
        e1[nd] = e2[nd]
    return K

raydep

raydep(P, om, d, ro, vp2, vs2)

Propagator matrix solutionn for P-SV waves, minor vector method

Parameters:
  • P (float) –

    surface wave slowness (s/m)

  • om (float) –

    surface wave angular frequency (radians/sec)

  • d (

    class:numpy.ndarray): layer thicknesses (meters?)

  • rho (

    class:numpy.ndarray): layer densities (kg/m^3) (gm/cc * 1000)

  • vp2 (

    class:numpy.ndarray): layer P velocities squared (m/s)^2

  • vs2 (

    class:numpy.ndarray): layer shear velocities squared (m/s)^2

Returns:
  • list

    Parameters, each value is at layer top v (:class:numpy.ndarray): vertical velocity (m/s?) u (:class:numpy.ndarray): horizontal velocity (m/s?) zz (:class:numpy.ndarray): vertical stress (Pa?) zx (:class:numpy.ndarray): horizontal stress (Pa?)

Notes

d, rho, vp2 and vs2 have one value for each layer (top to bottom), must be same length (Normalized compliance = -kv/(omegasigzz) )

P = 1/140 # Corresponds to 2000m depth, low freqs om = 2 * np.pi * 0.005 d = np.array([1000, 1000, 1000, 3000, 3000]) rho = np.array([3000, 3000, 3000, 3000, 3000]) vp2 = np.array([30002, 40002, 50002, 75002, 82002]) vs2 = np.array([16002, 23002, 28002, 43002, 47002]) np.set_printoptions(precision=1) raydep(P, om, d, rho, vp2, vs2) (array([1. , 0.7, 0.5, 0.4, 0.3]), array([ 1.8e-01, 6.0e-02, 1.2e-02, 5.2e-05, -5.8e-02]), array([-2.8e+08, -2.8e+08, -2.7e+08, -2.6e+08, -1.9e+08]), array([-0.0e+00, 3.1e+07, 5.4e+07, 7.6e+07, 1.0e+08]))

Source code in crawtools/compliance/compliance_functions.py
 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
def raydep(P, om, d, ro, vp2, vs2):
    """
    Propagator matrix solutionn for P-SV waves, minor vector method

    Args:
        P (float): surface wave slowness (s/m)
        om (float): surface wave angular frequency (radians/sec)
        d (:class:`numpy.ndarray`): layer thicknesses (meters?)
        rho (:class:`numpy.ndarray`): layer densities (kg/m^3) (gm/cc * 1000)
        vp2 (:class:`numpy.ndarray`): layer P velocities squared (m/s)^2
        vs2 (:class:`numpy.ndarray`): layer shear velocities squared (m/s)^2

    Returns:
        (list): Parameters, each value is at layer top
            v (:class:`numpy.ndarray`): vertical velocity (m/s?)
            u (:class:`numpy.ndarray`): horizontal velocity (m/s?)
            zz (:class:`numpy.ndarray`): vertical stress (Pa?)
            zx (:class:`numpy.ndarray`): horizontal stress (Pa?)

    Notes:
        d, rho, vp2 and vs2 have one value for each layer (top to bottom),
            must be same length
        (Normalized compliance = -k*v/(omega*sigzz) )

    >>> P = 1/140    # Corresponds to 2000m depth, low freqs
    >>> om = 2 * np.pi * 0.005
    >>> d = np.array([1000, 1000, 1000, 3000, 3000])
    >>> rho = np.array([3000, 3000, 3000, 3000, 3000])
    >>> vp2 = np.array([3000**2, 4000**2, 5000**2, 7500**2, 8200**2])
    >>> vs2 = np.array([1600**2, 2300**2, 2800**2, 4300**2, 4700**2])
    >>> np.set_printoptions(precision=1)
    >>> raydep(P, om, d, rho, vp2, vs2)
    (array([1. , 0.7, 0.5, 0.4, 0.3]), array([ 1.8e-01,  6.0e-02,  1.2e-02,  5.2e-05, -5.8e-02]), array([-2.8e+08, -2.8e+08, -2.7e+08, -2.6e+08, -1.9e+08]), array([-0.0e+00,  3.1e+07,  5.4e+07,  7.6e+07,  1.0e+08]))
    """
    mu = ro * vs2
    n = len(d)
    ist = n-1
    ysav = 0
    psq = P*P
    r2 = 2 * mu[ist] * P
    # R and S are the "Wavenumbers" of compress and shear waves in botlayer
    # RoW and SoW are divided by ang freq
    RoW = np.sqrt(psq - 1/vp2[ist])
    SoW = np.sqrt(psq - 1/vs2[ist])
    ym = np.zeros((ist+1, 5))
    i = ist
    y = np.zeros((5, ))     # Minor vector matrix
    # Stress-displacement vector: (vert vel, hor vel, vert stress, hor stress)
    x = np.zeros((i+1, 4))

    y[2] =  RoW
    y[3] = -SoW
    y[0] = (RoW*SoW - psq) / ro[i]
    y[1] = r2*y[0] + P
    y[4] = ro[i] - r2*(P + y[1])
    ym[i, :] = y
    # *****PROPAGATE UP LAYERS*********
    while i > 0:
        i = i-1
        ha = psq - 1/vp2[i]
        ca, sa = _argdtray(om*d[i], ha)
        hb = psq - 1/vs2[i]
        cb, sb = _argdtray(om*d[i], hb)
        hbs = hb*sb
        has = ha*sa
        r1 = 1 / ro[i]
        r2 = 2 * mu[i] * P
        b1 = r2*y[0] - y[1]
        g3 = (y[4] + r2*(y[1]-b1)) * r1
        g1 = b1 + P*g3
        g2 = ro[i]*y[0] - P*(g1+b1)
        e1 = cb*g2 - hbs*y[2]
        e2 = -sb*g2 + cb*y[2]
        e3 = cb*y[3] + hbs*g3
        e4 = sb*y[3] + cb*g3
        y[2] = ca*e2 - has*e4
        y[3] = sa*e1 + ca*e3
        g3 = ca*e4 - sa*e2
        b1 = g1 - P*g3
        y[0] = (ca*e1 + has*e3 + P*(g1+b1))*r1
        y[1] = r2*y[0] - b1
        y[4] = ro[i]*g3 - r2*(y[1] - b1)
        ym[i, :] = y

    de = y[4]/np.sqrt(y[0]*y[0] + y[1]*y[1])
    ynorm = 1/y[2]
    y[0: 4] = np.array([0, -ynorm,  0,  0])
    # *****PROPAGATE BACK DOWN LAYERS*********
    while i <= ist:
        x[i, 0] = -ym[i, 1]*y[0] - ym[i, 2]*y[1] + ym[i, 0]*y[3]
        x[i, 1] = -ym[i, 3]*y[0] + ym[i, 1]*y[1] - ym[i, 0]*y[2]
        x[i, 2] = -ym[i, 4]*y[1] - ym[i, 1]*y[2] - ym[i, 3]*y[3]
        x[i, 3] =  ym[i, 4]*y[0] - ym[i, 2]*y[2] + ym[i, 1]*y[3]
        ls = i
        if i >= 1:
            sum = abs(x[i, 0] + i*x[i, 1])
            pbsq = 1 / vs2[i]
            if sum < 1e-4:
                break

        ha = psq - 1/vp2[i]
        ca, sa = _argdtray(om*d[i], ha)
        hb = psq-1/vs2[i]
        cb, sb = _argdtray(om*d[i], hb)
        hbs = hb*sb
        has = ha*sa
        r2 = 2*P*mu[i]
        e2 = r2*y[1] - y[2]
        e3 = ro[i]*y[1] - P*e2
        e4 = r2*y[0] - y[3]
        e1 = ro[i]*y[0] - P*e4
        e6 = ca*e2 - sa*e1
        e8 = cb*e4 - sb*e3
        y[0] = (ca*e1 - has*e2+P*e8) / ro[i]
        y[1] = (cb*e3 - hbs*e4+P*e6) / ro[i]
        y[2] = r2*y[1] - e6
        y[3] = r2*y[0] - e8
        i = i+1;
    #
    #if x(1,3) == 0
    #  error('vertical surface stress = 0 in DETRAY');
    #end
    ist = ls

    return x[:,0], x[:,1], x[:,2], x[:,3]

zp_to_norm_compliance

zp_to_norm_compliance(freqs, zp, wdepth, z_units='M/S')

Calculate normalized compliance from the z/p ratio, freqs and water depth

normalized compliance is defined as k*Z/P, with Z in m and P in Pa. Its units are therefore 1/Pa

Parameters:
  • freqs (

    class:numpy.nparray): frequencies (1/s)

  • zp (

    class:numpy.nparray): vertical motion / pressure. Pressure units are Pa, z_units are specified by the paramter z_units

  • wdepth (float) –

    water depth (m)

  • z_units (str, default: 'M/S' ) –

    z units, one of 'M', 'M/S' or 'M/S^2'

Source code in crawtools/compliance/compliance_functions.py
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
def zp_to_norm_compliance(freqs, zp, wdepth, z_units='M/S'):
    """
    Calculate normalized compliance from the z/p ratio, freqs and water depth

    normalized compliance is defined as k*Z/P, with Z in m and P in Pa.
    Its units are therefore 1/Pa

    Args:
        freqs (:class:`numpy.nparray`): frequencies (1/s)
        zp (:class:`numpy.nparray`): vertical motion / pressure.  Pressure
            units are Pa, z_units are specified by the paramter z_units
        wdepth (float): water depth (m)
        z_units (str): z units, one of 'M', 'M/S' or 'M/S^2'
    """
    omega = 2 * np.pi * freqs
    k = gravd(omega, wdepth)
    if z_units.upper() == 'M':
        omega_term = np.ones(omega.shape())
    elif z_units.upper() == 'M/S':
        omega_term = omega**(-1)
    elif z_units.upper() == 'M/S^2':
        omega_term = omega**(-2)
    else:
        raise ValueError(f'Z_units ({z_units}) is not in ("M", "M/S", "M/S^2")')
    return zp * k * omega_term

crawtools.compliance.compliance_noise

ComplianceNoise

Bases: object

Generate synthetic seismological data based on environmental and noise factors

Source code in crawtools/compliance/compliance_noise.py
 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
class ComplianceNoise(object):
    """
    Generate synthetic seismological data based on environmental and noise factors
    """
    def __init__(self, water_depth, Z_offset_angles=(1, 10),
                 IG_m_seasurface=None, noise_pressure=None, noise_seismo=None,
                 noise_tilt_max=None, noise_tilt_variance=60, earth_model=None):
        """
        Return seismo and DPG time series corresponding to compliance plus noise

        Args:
            water_depth (numeric): water depth in meters
            Z_offset_angles (list): Seismometer's Z offset [angle, azimuth]
                from vertical, in degrees: (angle is the most important)
            IG_m_seasurface (:class:`PSDVals`, None): representation of
                infragravity wave PSD levels. Values are wave heights in
                dB ref m^2/Hz.
                If None, uses ComplianceNoise().default_IG_m_seasurface
            noise_pressure (:class:`PSDVals`, None): representation of DPG noise levels.
                Values are in dB ref Pa^2/Hz
                If None, uses ComplianceNoise().default_noise_pressure
            noise_seismo (:class:`PSDVals`, None): representation of seismometer noise levels.
                Values are in dB ref (m/s^2)^2/Hz
                If None, uses ComplianceNoise().default_noise_seismo
            noise_tilt_max (:class: `PSDVals`, None): maximum tilt noise levels (dB ref
                1 rad^2/Hz)
                If None, uses ComplianceNoise().default_tilt_max
            noise_tilt_variance (float): variance in dB of tilt noise levels
            earth_model (:class:`EarthModel1D`, None): 1D Earth model (last row
                treated as a half-space)
                If None, uses ComplianceNoise().default_earth_model
        """
        self.water_depth = water_depth
        self.Z_offset_angles = Z_offset_angles
        self.IG_m_seasurface = IG_m_seasurface
        self.noise_pressure = noise_pressure
        self.noise_seismo = noise_seismo
        self.noise_tilt_max = noise_tilt_max
        self.noise_tilt_variance = noise_tilt_variance
        self.earth_model = earth_model

        # Fill empty parameters with default values
        if self.IG_m_seasurface is None:
            self.IG_m_seasurface = self.default_IG_m_seasurface
        if self.noise_pressure is None:
            self.noise_pressure = self.default_noise_pressure
        if self.noise_seismo is None:
            self.noise_seismo = self.default_noise_seismo
        if self.noise_tilt_max is None:
            self.noise_tilt_max = self.default_tilt_max
        if self.earth_model is None:
            self.earth_model = self.default_earth_model

        # Validate variables
        assert isinstance(self.water_depth, (int, float))
        assert isinstance(self.IG_m_seasurface, PSDVals)
        assert isinstance(self.noise_pressure, PSDVals)
        assert isinstance(self.noise_seismo, PSDVals)
        assert isinstance(self.noise_tilt_max, PSDVals)
        assert isinstance(self.noise_tilt_variance, (int, float))
        assert isinstance(self.earth_model, EarthModel1D)

        self.IG_Pa_seafloor = self._calc_IG_Pa_seafloor()

    def __str__(self):
        s = '<ComplianceNoise>:'
        s += f'      water_depth={self.water_depth}'
        s += f'      Z_offset_angles={self.Z_offset_angles}\n'
        s += f'      IG_m_seasurface={self.IG_m_seasurface}\n'
        s += f'      noise_pressure={self.noise_pressure}\n'
        s += f'      noise_seismo={self.noise_seismo}\n'
        s += f'      noise_tilt_max={self.noise_tilt_max}\n'
        s += f'      tilt_min = noise_tilt_max - {self.noise_tilt_variance} dB\n'
        s += f'      earth_model={self.earth_model}'
        return s

    def _calc_IG_Pa_seafloor(self, min_f_spacing=0.003):
        psd = deepcopy(self.IG_m_seasurface)
        if np.any(np.diff(psd.freqs) > min_f_spacing):
            psd.resample(np.arange(psd.freqs[0],
                         psd.freqs[-1] + min_f_spacing * .999,
                         min_f_spacing))
        k = gravd(2 * np.pi * psd.freqs, self.water_depth)
        psd.values += 100 - self._cosh_dBs(k * self.water_depth)
        psd.value_units = 'dB ref 1 Pa^2/Hz'
        return psd

    def _cosh_dBs(self, x, max_input=700):
        # protect against values that are too big
        x[x > max_input] = max_input
        x[x < -max_input] = -max_input
        return to_DBs(np.cosh(x))

    @property
    def velPSD_compliance(self):
        om, k, ncompl = self._calc_ncompl()
        psd = deepcopy(self.IG_Pa_seafloor)
        psd.values += to_DBs(om * ncompl / k)
        return psd

    @property
    def default_IG_m_seasurface(self):
        """Default IG wave levels"""
        return PSDVals([0.001, 1], to_DBs([.002, .002]), "dB ref 1 m^2/Hz")

    @property
    def default_noise_pressure(self):
        """Default DPG noise levels"""
        return PSDVals([0.001, 0.003, 0.006, 0.01, 0.02, 0.05, 0.1],
                       [60, 30, -0, -10, -10, -10, -10],
                       'dB ref Pa^2/Hz')

    @property
    def default_noise_seismo(self):
        """Default seismometer noise levels"""
        return PSDVals([0.001, 0.003, 0.006, 0.01, 0.02, 0.05, 0.1],
                       [-130, -160, -170, -175, -175, -180, -180],
                       'dB ref (m/s^2)^2/Hz')

    @property
    def default_tilt_max(self):
        """Default noise_tilt_max values"""
        f = np.arange(0.001, 0.1, 0.001)
        hi = to_DBs(np.power(10., -6.5)*np.power(f, -1.5))
        return PSDVals(f.tolist(), hi.tolist(), 'dB ref 1 rad^2/Hz')

    @property
    def default_earth_model(self):
        """Default earth model"""
        return EarthModel1D([[1000, 3000, 3000, 1600],
                             [1000, 3000, 4000, 2300],
                             [1000, 3000, 5000, 2800],
                             [3000, 3000, 7500, 4300],
                             [3000, 3000, 8200, 4700]])

    def _default_tilt_ts(self, ref_trace, noise_tilt_variance):
        """
        returns default earth tides normalized to [0,1]
        and angle oscillating from 90 to 110

        Args:
            ref_trace (:class:`obspy.Trace`): a Trace used for sampling rate
                and time limits
            noise_tilt_variance (float): maximum dB variance.  Used to change tilt
                range from [0, 1] to [10^-[dB/20], 1]
        """
        assert isinstance(ref_trace, Trace)
        amp_trace, angle_trace = self.make_tilt_ts(ref_trace, TideCoefficients(), (90, 110))
        min_val = 10**(-noise_tilt_variance / 20)
        amp_trace.data[amp_trace.data < min_val] = min_val
        return amp_trace, angle_trace

    @property
    def accelPSD_compliance(self):
        om, k, ncompl = self._calc_ncompl()
        psd = deepcopy(self.IG_Pa_seafloor)
        psd.values = psd.values + to_DBs(om * om * ncompl / k)
        return psd

    def _calc_ncompl(self):
        f = self.IG_Pa_seafloor.freqs
        om = 2 * np.pi * f
        k = gravd(om, self.water_depth)
        ncompl = calc_norm_compliance(self.water_depth, f, self.earth_model)
        return om, k, ncompl

    def save_compliance(self, max_freq=None):
        """
        Saves self.earth_model's compliance to a BRUIT-FM CSV file
        """
        oms, ks, ncompls = self._calc_ncompl()
        fname = "model_compliance_Pa-1.csv"
        freqs = oms/(2*np.pi)
        if max_freq is not None:
            ncompls = ncompls[freqs <= max_freq]
            freqs = freqs[freqs <= max_freq]
        with open(fname, "w") as fid:
            fid.write('frequencies;compliance;uncertainty;phase\n')
            for freq, ncompl in zip(freqs, ncompls):
                fid.write('{:.5g};{:.5g};{:.5g};{:.5g}\n'
                          .format(freq, np.abs(ncompl), 0.000,
                                  np.angle(ncompl, deg=True)))


    def plot(self, fmin=0.001, fmax=0.1, fstep=0.001, outfile=None):
        """
        Plot the spectral representation of the noise sources in dB
        """
        f = np.arange(fmin, fmax + fstep / 2, fstep)
        # Plot
        fig, axs = plt.subplots(2, 1, sharex='col')
        # Plot the pressure signal
        axs[0].semilogx(f, self.IG_Pa_seafloor.resample_values(f), 'r', label='s_IG')
        axs[0].semilogx(f, self.noise_pressure.resample_values(f), 'b', label='n_press')
        axs[0].set_ylabel('dB ref 1 Pa^2/Hz')
        axs[0].legend()
        axs[0].set_ylim(-20, 60)
        # Plot the accel
        axs[1].semilogx(f, self.accelPSD_compliance.resample_values(f),
                        'r', label='s_IG')
        axs[1].semilogx(f, self.noise_seismo.resample_values(f),
                        'b', label='n_seismo')
        Z_angle_factor = to_DBs(np.sin(np.radians(self.Z_offset_angles[0])))
        axs[1].semilogx(f, self.noise_tilt_max.resample_values(f) + Z_angle_factor,
                        'g--', label='n_tilt_Z_max')
        axs[1].semilogx(f, self.noise_tilt_max.resample_values(f) + Z_angle_factor - self.noise_tilt_variance,
                        'g-.', label='n_tilt_Z_min')
        axs[1].set_ylabel('dB ref 1 (m/s^2)^2/Hz')
        axs[1].set_xlabel('Frequency (Hz)')
        axs[1].set_ylim(-200, -100)
        axs[1].legend()
        if outfile is not None:
            plt.savefig(outfile)
        plt.show()

    def streams(self, ref_trace, tilt_ts=None, s_sensitivity=3774870000,
                p_sensitivity=495, network='XX', station='SSSSS', plotit=False,
                forceInt32=False):
        """
        Return streams generated from to the noise and signal levels

        Args:
            ref_trace (:class: `obspy.Trace`): trace with time base to use
            tilt_ts (:class: `obspy.stream.Stream`): a tilt time series between the minimum
                and maximum values.  Must have the same sample rate and time span as
                the values defined by start_date, end_date and sps.
                The first trace is the tilt level modifier, which
                should vary between 0 and 1 (otherwise it will be shifted/compressed to
                do so).  Second trace is the direction of the tilt, in degrees clockwise
                from the seismometer's N channel.  Values must be between 0 and 360.
            s_sensitivity (float): Desired seismometer sensitivity (counts/m/s)
            p_sensitivity (float): Desired pressure gauge sensitivity (counts/Pa)

        Returns:
            streams (list):
                data (:class:`obspy.Stream): synthetic seafloor BB 4C data
                sources (:class:`obspy.Stream`): individual noises and signals
        """        
        trace_pts = ref_trace.stats.npts
        npts = 2**int(np.ceil(np.log2(trace_pts)))
        f = np.linspace(0, ref_trace.stats.sampling_rate / 2, npts)

        tr = []
        ref_trace = ref_trace.copy()    # Avoid overwriting original
        ref_trace.stats.station = station
        ref_trace.stats.network = network

        # IG wave pressure signal
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "PIG"
        p_fft = self.IG_Pa_seafloor.as_fft(f, plotit=False)
        tr[-1].data = irfft(p_fft)[:trace_pts]

        # DPG noise model
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "PNO"
        fft = self.noise_pressure.as_fft(f)
        tr[-1].data = irfft(fft)[:trace_pts]

        # Vertical compliance signal 
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "ZIG"
        z_fft = self.velPSD_compliance.as_fft(f, phases=np.angle(p_fft))
        tr[-1].data = irfft(z_fft)[:trace_pts]

        # Seismometer noise model
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "SNO"
        fft = self.noise_seismo.as_fft(f)
        tr[-1].data = irfft(fft)[:trace_pts]

        # Tilt noise model
        fft = self.noise_tilt_max.as_fft(f)
        noise_max = irfft(fft)[:trace_pts]
        dyntilt_amp, dyntilt_angle = self._default_tilt_ts(ref_trace, self.noise_tilt_variance)
        noise = noise_max * dyntilt_amp.data
        N_noise = np.cos(np.radians(dyntilt_amp)) * noise
        E_noise = np.sin(np.radians(dyntilt_amp)) * noise
        angfact = np.sin(np.radians(self.Z_offset_angles[0]))
        azefact_1 = np.sin(np.radians(self.Z_offset_angles[1]))
        azefact_2 = np.cos(np.radians(self.Z_offset_angles[1]))
        Z_noise = angfact * (azefact_1 * N_noise + azefact_2 * E_noise)
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "1NO"
        tr[-1].data = N_noise
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "2NO"
        tr[-1].data = E_noise
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "ZNO"
        tr[-1].data = Z_noise

        sources = Stream(traces=tr)
        if plotit is True:
            sources.plot(equal_scales=False)

        # Add signal and noise time series to make synthetic BBOBS channels
        tr = []
        # LDG: Differential pressure gauge
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "LDG"
        tr[-1].data = ((sources.select(channel='PIG')[0].data
                        + sources.select(channel='PNO')[0].data)
                       * p_sensitivity)
        # LH1: N-equivalent horizontal seismometer channel
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "LH1"
        tr[-1].data = ((sources.select(channel='SNO')[0].data
                        + sources.select(channel='1NO')[0].data)
                       * s_sensitivity)
        # LH2: E-equivalent horizontal seismometer channel
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "LH2"
        tr[-1].data = ((sources.select(channel='SNO')[0].data
                        + sources.select(channel='2NO')[0].data)
                       * s_sensitivity)
        # LHA: Vertical seismometer channel
        tr.append(ref_trace.copy())
        tr[-1].stats.channel = "LHZ"
        tr[-1].data = ((sources.select(channel='SNO')[0].data
                        + sources.select(channel='ZIG')[0].data
                        + sources.select(channel='ZNO')[0].data)
                       * s_sensitivity)

        data = Stream(traces=tr)
        if forceInt32 is True:
            for tr in data:
                tr.data  = np.require(tr.data, dtype=np.int32)
        if plotit is True:
            data.plot(equal_scales=False)

        return data, sources

    @staticmethod
    def make_tilt_ts(ref_trace, coefficients, angle_limits=(70, 100)):
        """
        make a simple tilt time series summing signals of given periods,
        amplitudes and starting phases

        Args:
            ref_trace (:class: `obspy.core.Trace`): A trace covering the
                desired period and with the desired sample rate
            coefficients (TideCoefficients): the tidal coefficients
            angle_limits (list): current direction angle limits ([min, max]
                in degrees).  Will be modulated by tide levels
        """
        tide_trace = coefficients.make_trace(ref_trace)
        # normalize between 0 and 1
        tide_trace.data -= np.min(tide_trace.data)
        tide_trace.data /= np.max(tide_trace.data)
        angles_trace = tide_trace.copy()
        angle_range = abs(angle_limits[1] - angle_limits[0])
        angles_trace.data = (angles_trace.data * angle_range) + min(angle_limits)
        return tide_trace, angles_trace

default_IG_m_seasurface property

default_IG_m_seasurface

Default IG wave levels

default_earth_model property

default_earth_model

Default earth model

default_noise_pressure property

default_noise_pressure

Default DPG noise levels

default_noise_seismo property

default_noise_seismo

Default seismometer noise levels

default_tilt_max property

default_tilt_max

Default noise_tilt_max values

__init__

__init__(water_depth, Z_offset_angles=(1, 10), IG_m_seasurface=None, noise_pressure=None, noise_seismo=None, noise_tilt_max=None, noise_tilt_variance=60, earth_model=None)

Return seismo and DPG time series corresponding to compliance plus noise

Parameters:
  • water_depth (numeric) –

    water depth in meters

  • Z_offset_angles (list, default: (1, 10) ) –

    Seismometer's Z offset [angle, azimuth] from vertical, in degrees: (angle is the most important)

  • IG_m_seasurface (

    class:PSDVals, None): representation of infragravity wave PSD levels. Values are wave heights in dB ref m^2/Hz. If None, uses ComplianceNoise().default_IG_m_seasurface

  • noise_pressure (

    class:PSDVals, None): representation of DPG noise levels. Values are in dB ref Pa^2/Hz If None, uses ComplianceNoise().default_noise_pressure

  • noise_seismo (

    class:PSDVals, None): representation of seismometer noise levels. Values are in dB ref (m/s^2)^2/Hz If None, uses ComplianceNoise().default_noise_seismo

  • noise_tilt_max (

    class: PSDVals, None): maximum tilt noise levels (dB ref 1 rad^2/Hz) If None, uses ComplianceNoise().default_tilt_max

  • noise_tilt_variance (float, default: 60 ) –

    variance in dB of tilt noise levels

  • earth_model (

    class:EarthModel1D, None): 1D Earth model (last row treated as a half-space) If None, uses ComplianceNoise().default_earth_model

Source code in crawtools/compliance/compliance_noise.py
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
def __init__(self, water_depth, Z_offset_angles=(1, 10),
             IG_m_seasurface=None, noise_pressure=None, noise_seismo=None,
             noise_tilt_max=None, noise_tilt_variance=60, earth_model=None):
    """
    Return seismo and DPG time series corresponding to compliance plus noise

    Args:
        water_depth (numeric): water depth in meters
        Z_offset_angles (list): Seismometer's Z offset [angle, azimuth]
            from vertical, in degrees: (angle is the most important)
        IG_m_seasurface (:class:`PSDVals`, None): representation of
            infragravity wave PSD levels. Values are wave heights in
            dB ref m^2/Hz.
            If None, uses ComplianceNoise().default_IG_m_seasurface
        noise_pressure (:class:`PSDVals`, None): representation of DPG noise levels.
            Values are in dB ref Pa^2/Hz
            If None, uses ComplianceNoise().default_noise_pressure
        noise_seismo (:class:`PSDVals`, None): representation of seismometer noise levels.
            Values are in dB ref (m/s^2)^2/Hz
            If None, uses ComplianceNoise().default_noise_seismo
        noise_tilt_max (:class: `PSDVals`, None): maximum tilt noise levels (dB ref
            1 rad^2/Hz)
            If None, uses ComplianceNoise().default_tilt_max
        noise_tilt_variance (float): variance in dB of tilt noise levels
        earth_model (:class:`EarthModel1D`, None): 1D Earth model (last row
            treated as a half-space)
            If None, uses ComplianceNoise().default_earth_model
    """
    self.water_depth = water_depth
    self.Z_offset_angles = Z_offset_angles
    self.IG_m_seasurface = IG_m_seasurface
    self.noise_pressure = noise_pressure
    self.noise_seismo = noise_seismo
    self.noise_tilt_max = noise_tilt_max
    self.noise_tilt_variance = noise_tilt_variance
    self.earth_model = earth_model

    # Fill empty parameters with default values
    if self.IG_m_seasurface is None:
        self.IG_m_seasurface = self.default_IG_m_seasurface
    if self.noise_pressure is None:
        self.noise_pressure = self.default_noise_pressure
    if self.noise_seismo is None:
        self.noise_seismo = self.default_noise_seismo
    if self.noise_tilt_max is None:
        self.noise_tilt_max = self.default_tilt_max
    if self.earth_model is None:
        self.earth_model = self.default_earth_model

    # Validate variables
    assert isinstance(self.water_depth, (int, float))
    assert isinstance(self.IG_m_seasurface, PSDVals)
    assert isinstance(self.noise_pressure, PSDVals)
    assert isinstance(self.noise_seismo, PSDVals)
    assert isinstance(self.noise_tilt_max, PSDVals)
    assert isinstance(self.noise_tilt_variance, (int, float))
    assert isinstance(self.earth_model, EarthModel1D)

    self.IG_Pa_seafloor = self._calc_IG_Pa_seafloor()

make_tilt_ts staticmethod

make_tilt_ts(ref_trace, coefficients, angle_limits=(70, 100))

make a simple tilt time series summing signals of given periods, amplitudes and starting phases

Parameters:
  • ref_trace (

    class: obspy.core.Trace): A trace covering the desired period and with the desired sample rate

  • coefficients (TideCoefficients) –

    the tidal coefficients

  • angle_limits (list, default: (70, 100) ) –

    current direction angle limits ([min, max] in degrees). Will be modulated by tide levels

Source code in crawtools/compliance/compliance_noise.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
@staticmethod
def make_tilt_ts(ref_trace, coefficients, angle_limits=(70, 100)):
    """
    make a simple tilt time series summing signals of given periods,
    amplitudes and starting phases

    Args:
        ref_trace (:class: `obspy.core.Trace`): A trace covering the
            desired period and with the desired sample rate
        coefficients (TideCoefficients): the tidal coefficients
        angle_limits (list): current direction angle limits ([min, max]
            in degrees).  Will be modulated by tide levels
    """
    tide_trace = coefficients.make_trace(ref_trace)
    # normalize between 0 and 1
    tide_trace.data -= np.min(tide_trace.data)
    tide_trace.data /= np.max(tide_trace.data)
    angles_trace = tide_trace.copy()
    angle_range = abs(angle_limits[1] - angle_limits[0])
    angles_trace.data = (angles_trace.data * angle_range) + min(angle_limits)
    return tide_trace, angles_trace

plot

plot(fmin=0.001, fmax=0.1, fstep=0.001, outfile=None)

Plot the spectral representation of the noise sources in dB

Source code in crawtools/compliance/compliance_noise.py
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
def plot(self, fmin=0.001, fmax=0.1, fstep=0.001, outfile=None):
    """
    Plot the spectral representation of the noise sources in dB
    """
    f = np.arange(fmin, fmax + fstep / 2, fstep)
    # Plot
    fig, axs = plt.subplots(2, 1, sharex='col')
    # Plot the pressure signal
    axs[0].semilogx(f, self.IG_Pa_seafloor.resample_values(f), 'r', label='s_IG')
    axs[0].semilogx(f, self.noise_pressure.resample_values(f), 'b', label='n_press')
    axs[0].set_ylabel('dB ref 1 Pa^2/Hz')
    axs[0].legend()
    axs[0].set_ylim(-20, 60)
    # Plot the accel
    axs[1].semilogx(f, self.accelPSD_compliance.resample_values(f),
                    'r', label='s_IG')
    axs[1].semilogx(f, self.noise_seismo.resample_values(f),
                    'b', label='n_seismo')
    Z_angle_factor = to_DBs(np.sin(np.radians(self.Z_offset_angles[0])))
    axs[1].semilogx(f, self.noise_tilt_max.resample_values(f) + Z_angle_factor,
                    'g--', label='n_tilt_Z_max')
    axs[1].semilogx(f, self.noise_tilt_max.resample_values(f) + Z_angle_factor - self.noise_tilt_variance,
                    'g-.', label='n_tilt_Z_min')
    axs[1].set_ylabel('dB ref 1 (m/s^2)^2/Hz')
    axs[1].set_xlabel('Frequency (Hz)')
    axs[1].set_ylim(-200, -100)
    axs[1].legend()
    if outfile is not None:
        plt.savefig(outfile)
    plt.show()

save_compliance

save_compliance(max_freq=None)

Saves self.earth_model's compliance to a BRUIT-FM CSV file

Source code in crawtools/compliance/compliance_noise.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def save_compliance(self, max_freq=None):
    """
    Saves self.earth_model's compliance to a BRUIT-FM CSV file
    """
    oms, ks, ncompls = self._calc_ncompl()
    fname = "model_compliance_Pa-1.csv"
    freqs = oms/(2*np.pi)
    if max_freq is not None:
        ncompls = ncompls[freqs <= max_freq]
        freqs = freqs[freqs <= max_freq]
    with open(fname, "w") as fid:
        fid.write('frequencies;compliance;uncertainty;phase\n')
        for freq, ncompl in zip(freqs, ncompls):
            fid.write('{:.5g};{:.5g};{:.5g};{:.5g}\n'
                      .format(freq, np.abs(ncompl), 0.000,
                              np.angle(ncompl, deg=True)))

streams

streams(ref_trace, tilt_ts=None, s_sensitivity=3774870000, p_sensitivity=495, network='XX', station='SSSSS', plotit=False, forceInt32=False)

Return streams generated from to the noise and signal levels

Parameters:
  • ref_trace (

    class: obspy.Trace): trace with time base to use

  • tilt_ts (

    class: obspy.stream.Stream): a tilt time series between the minimum and maximum values. Must have the same sample rate and time span as the values defined by start_date, end_date and sps. The first trace is the tilt level modifier, which should vary between 0 and 1 (otherwise it will be shifted/compressed to do so). Second trace is the direction of the tilt, in degrees clockwise from the seismometer's N channel. Values must be between 0 and 360.

  • s_sensitivity (float, default: 3774870000 ) –

    Desired seismometer sensitivity (counts/m/s)

  • p_sensitivity (float, default: 495 ) –

    Desired pressure gauge sensitivity (counts/Pa)

Returns:
  • streams( list ) –

    data (:class:obspy.Stream): synthetic seafloor BB 4C data sources (:class:obspy.Stream`): individual noises and signals

Source code in crawtools/compliance/compliance_noise.py
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
def streams(self, ref_trace, tilt_ts=None, s_sensitivity=3774870000,
            p_sensitivity=495, network='XX', station='SSSSS', plotit=False,
            forceInt32=False):
    """
    Return streams generated from to the noise and signal levels

    Args:
        ref_trace (:class: `obspy.Trace`): trace with time base to use
        tilt_ts (:class: `obspy.stream.Stream`): a tilt time series between the minimum
            and maximum values.  Must have the same sample rate and time span as
            the values defined by start_date, end_date and sps.
            The first trace is the tilt level modifier, which
            should vary between 0 and 1 (otherwise it will be shifted/compressed to
            do so).  Second trace is the direction of the tilt, in degrees clockwise
            from the seismometer's N channel.  Values must be between 0 and 360.
        s_sensitivity (float): Desired seismometer sensitivity (counts/m/s)
        p_sensitivity (float): Desired pressure gauge sensitivity (counts/Pa)

    Returns:
        streams (list):
            data (:class:`obspy.Stream): synthetic seafloor BB 4C data
            sources (:class:`obspy.Stream`): individual noises and signals
    """        
    trace_pts = ref_trace.stats.npts
    npts = 2**int(np.ceil(np.log2(trace_pts)))
    f = np.linspace(0, ref_trace.stats.sampling_rate / 2, npts)

    tr = []
    ref_trace = ref_trace.copy()    # Avoid overwriting original
    ref_trace.stats.station = station
    ref_trace.stats.network = network

    # IG wave pressure signal
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "PIG"
    p_fft = self.IG_Pa_seafloor.as_fft(f, plotit=False)
    tr[-1].data = irfft(p_fft)[:trace_pts]

    # DPG noise model
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "PNO"
    fft = self.noise_pressure.as_fft(f)
    tr[-1].data = irfft(fft)[:trace_pts]

    # Vertical compliance signal 
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "ZIG"
    z_fft = self.velPSD_compliance.as_fft(f, phases=np.angle(p_fft))
    tr[-1].data = irfft(z_fft)[:trace_pts]

    # Seismometer noise model
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "SNO"
    fft = self.noise_seismo.as_fft(f)
    tr[-1].data = irfft(fft)[:trace_pts]

    # Tilt noise model
    fft = self.noise_tilt_max.as_fft(f)
    noise_max = irfft(fft)[:trace_pts]
    dyntilt_amp, dyntilt_angle = self._default_tilt_ts(ref_trace, self.noise_tilt_variance)
    noise = noise_max * dyntilt_amp.data
    N_noise = np.cos(np.radians(dyntilt_amp)) * noise
    E_noise = np.sin(np.radians(dyntilt_amp)) * noise
    angfact = np.sin(np.radians(self.Z_offset_angles[0]))
    azefact_1 = np.sin(np.radians(self.Z_offset_angles[1]))
    azefact_2 = np.cos(np.radians(self.Z_offset_angles[1]))
    Z_noise = angfact * (azefact_1 * N_noise + azefact_2 * E_noise)
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "1NO"
    tr[-1].data = N_noise
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "2NO"
    tr[-1].data = E_noise
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "ZNO"
    tr[-1].data = Z_noise

    sources = Stream(traces=tr)
    if plotit is True:
        sources.plot(equal_scales=False)

    # Add signal and noise time series to make synthetic BBOBS channels
    tr = []
    # LDG: Differential pressure gauge
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "LDG"
    tr[-1].data = ((sources.select(channel='PIG')[0].data
                    + sources.select(channel='PNO')[0].data)
                   * p_sensitivity)
    # LH1: N-equivalent horizontal seismometer channel
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "LH1"
    tr[-1].data = ((sources.select(channel='SNO')[0].data
                    + sources.select(channel='1NO')[0].data)
                   * s_sensitivity)
    # LH2: E-equivalent horizontal seismometer channel
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "LH2"
    tr[-1].data = ((sources.select(channel='SNO')[0].data
                    + sources.select(channel='2NO')[0].data)
                   * s_sensitivity)
    # LHA: Vertical seismometer channel
    tr.append(ref_trace.copy())
    tr[-1].stats.channel = "LHZ"
    tr[-1].data = ((sources.select(channel='SNO')[0].data
                    + sources.select(channel='ZIG')[0].data
                    + sources.select(channel='ZNO')[0].data)
                   * s_sensitivity)

    data = Stream(traces=tr)
    if forceInt32 is True:
        for tr in data:
            tr.data  = np.require(tr.data, dtype=np.int32)
    if plotit is True:
        data.plot(equal_scales=False)

    return data, sources

from_DBs

from_DBs(inp)

Converts values from dBs ref 1

Parameters:
  • inp ((float, list or ndarray)) –

    values to convert

Source code in crawtools/compliance/compliance_noise.py
384
385
386
387
388
389
390
391
392
393
def from_DBs(inp):
    """
    Converts values from dBs ref 1

    Args:
        inp (float, list or np.ndarray): values to convert
    """
    if isinstance(inp, (list, tuple)):
        inp = np.array(inp)
    return np.power(10., inp / 20)

to_DBs

to_DBs(inp)

Converts values to dBs ref 1

Parameters:
  • inp (float, list, or np.ndarray) –

    values to convert

Source code in crawtools/compliance/compliance_noise.py
372
373
374
375
376
377
378
379
380
381
def to_DBs(inp):
    """
    Converts values to dBs ref 1

    Args:
        inp (float, list, or np.ndarray): values to convert
    """
    if isinstance(inp, (list, tuple)):
        inp = np.array(inp)
    return 20 * np.log10(inp)

crawtools.compliance.earth_model

1D Earth model Author: W Crawford

EarthModel1D

Source code in crawtools/compliance/earth_model.py
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
class EarthModel1D():
    def __init__(self, prop_list):
        """
        Args:
            prop_list (list of lists): 1D model, each row is a
                layer with values [thick(m), rho(kg/m^3), vp(m/s), vs(m/s)]
        """
        if not isinstance(prop_list, (list, tuple)):
            raise ValueError(f'prop_list is not a list or tuple')
        for row in prop_list:
            if not isinstance(row, (list, tuple)):
                raise ValueError(f'{row=} of prop_list is not a list or tuple')
            if not len(row) == 4:
                raise ValueError(f'{row=} of prop_list does not have for elements')
            for c in row:
                if not isinstance(c, (float, int)):
                    raise ValueError(f'{element=} of {row=} of prop_list is not a number')
                if c < 0:
                    raise ValueError(f'{element=} of {row=} of prop_list is less than zero')
                elif c > 10000:
                    raise ValueError(f'{element=} of {row=} of prop_list is > 10000')
        self.thicks = np.array([x[0] for x in prop_list])
        self.rhos = np.array([x[1] for x in prop_list])
        self.vps = np.array([x[2] for x in prop_list])
        self.vss = np.array([x[3] for x in prop_list])
        for vs, vp in zip(self.vss, self.vps):
            if vs*np.sqrt(3) > vp:
                warnings.warn(f'{vs=} > {vp=} / sqrt(3)')

    def __str__(self):
        s = 'EarthModel1D: thickness (m) | rho (kg/m^3) | Vp (m/s) | Vs (m/s)\n'
        s +='              ------------- | ------------ | -------- | ----------\n'
        for t, r, vp, vs in zip(self.thicks.tolist(), self.rhos.tolist(),
                              self.vps.tolist(), self.vss.tolist()):
            s += f"               {t:12.0f} | {r:12.0f} | {vp:8.0f} | {vs:8.0f}\n"
        return s

__init__

__init__(prop_list)
Parameters:
  • prop_list (list of lists) –

    1D model, each row is a layer with values [thick(m), rho(kg/m^3), vp(m/s), vs(m/s)]

Source code in crawtools/compliance/earth_model.py
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
def __init__(self, prop_list):
    """
    Args:
        prop_list (list of lists): 1D model, each row is a
            layer with values [thick(m), rho(kg/m^3), vp(m/s), vs(m/s)]
    """
    if not isinstance(prop_list, (list, tuple)):
        raise ValueError(f'prop_list is not a list or tuple')
    for row in prop_list:
        if not isinstance(row, (list, tuple)):
            raise ValueError(f'{row=} of prop_list is not a list or tuple')
        if not len(row) == 4:
            raise ValueError(f'{row=} of prop_list does not have for elements')
        for c in row:
            if not isinstance(c, (float, int)):
                raise ValueError(f'{element=} of {row=} of prop_list is not a number')
            if c < 0:
                raise ValueError(f'{element=} of {row=} of prop_list is less than zero')
            elif c > 10000:
                raise ValueError(f'{element=} of {row=} of prop_list is > 10000')
    self.thicks = np.array([x[0] for x in prop_list])
    self.rhos = np.array([x[1] for x in prop_list])
    self.vps = np.array([x[2] for x in prop_list])
    self.vss = np.array([x[3] for x in prop_list])
    for vs, vp in zip(self.vss, self.vps):
        if vs*np.sqrt(3) > vp:
            warnings.warn(f'{vs=} > {vp=} / sqrt(3)')

crawtools.compliance.psd_vals

PSDVals

Holds PSD frequencies and values

Source code in crawtools/compliance/psd_vals.py
  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
class PSDVals():
    """
    Holds PSD frequencies and values
    """
    def __init__(self, freqs, values, value_units="unknown"):
        """
        Args:
            freqs (list or :class:`np.array`): Frequencies (Hz)
            values (list or :class:`np.array`): PSD values in dB
            value_unites (str): Units of values
        """
        if isinstance(freqs, (list, tuple)):
            freqs = np.array(freqs)
        else:
            assert isinstance(freqs, np.ndarray)
        assert np.all(np.diff(freqs) > 0), 'freqs are not monotonically increasing'
        if isinstance(values, (list, tuple)):
            values = np.array(values)
        else:
            assert isinstance(values, np.ndarray)
        assert len(freqs) == len(values)
        assert isinstance(value_units, str)
        self.freqs = freqs
        self.values = values
        self.value_units = value_units

    def __str__(self):
        s = f"PSDVals (units={self.value_units}):\n"
        s += 20*" " + "     freq  |   value  \n"
        s += 20*" " + "---------- | ---------\n"
        for f, v in zip(self.freqs, self.values):
            s += 20*" " + f"{f:9.4g}  | {v:9.4g}\n"
        return s

    def resample(self, new_f):
        self.values = np.interp(new_f, self.freqs, self.values)
        self.freqs = new_f

    def plot(self):
        f, a = plt.subplots()
        a.semilogx(self.freqs, self.values)
        a.set_ylabel('Amplitude (dB)')
        a.set_title(self.value_units)        
        plt.show()

    @staticmethod
    def _random_phases(n_values):
        rng = np.random.default_rng()
        return 360.*rng.random(n_values)

    def resample_values(self, freqs):
        """
        Resample values at the given frequencies
        """
        return np.interp(freqs, self.freqs, self.values)

    def as_fft(self, freqs, left='taper', right='taper', phases=None,
               plotit=False):
        """
        Return an fft "equivalent" to the given Power Spectral Density
        Args:
            freqs (list, np.array or None): Resample at the given freqs
            left (None, float or 'taper'): how to handle values below the
                lowest self.freq:
                    - None: use value at lowest input frequency
                    - float: set to the given value
                    - 'taper': taper using Kaiser function
            right (None, float, or 'taper'): how to handle values above the
                highest self.freq
            phases (np.array or None): force phases to be the given values
                (must be same length as frequencies)
        """
        if not freqs[0] == 0:
            raise ValueError(f"Cannot create an fft without f[0] == 0")
        fdiffs = np.diff(freqs)
        if not np.all(np.abs(fdiffs-fdiffs[0]) < fdiffs[0]/1e6):
            raise ValueError(f"freqs are not evenly spaced")

        fft = np.power(10., np.interp(freqs, self.freqs, self.values)/20)

        fft = np.nan_to_num(fft)        
        if left == 'taper':
            fft = self._add_left_taper(fft, freqs, self.freqs[0])
        elif left is not None:
            fft[freqs<self.freqs[0]] = left
        if right == 'taper':
            fft = self._add_right_taper(fft, freqs, self.freqs[-1])
        elif right is not None:
            fft[freqs>self.freqs[-1]] = right

        if plotit is True:
            f,a = plt.subplots()
            a.semilogx(freqs, fft, 'b')
            a.axvline(self.freqs[0], color='g', ls='--')
            a.axvline(self.freqs[-1], color='g', ls='--')
            a.semilogx(freqs, fft, 'r')
            plt.show()
        fft[0] = 0. # Make sure the zero-frequency value is zero
        # Scale for sample rate and window length
        sampling_rate = 1/(2*freqs[-1])
        # Bendata&Piersol 1986 eqs 11.100 & 11.102
        mul_factor = np.sqrt(len(freqs) * sampling_rate)  / 2    
        # mul_factor = (len(freqs)) / 2 
        # mul_factor = (sampling_rate) / 2 
        # div_factor = 2 / (sampling_rate) 
        # div_factor = 1    
        fft *= mul_factor
        if phases is None:
            phases = np.radians(self._random_phases(len(fft)))
        return fft * np.exp(1j * phases)


    def _add_left_taper(self, fft, freqs, freq_lim, max_taper_len=100):
        n_zeros = len(fft[freqs<freq_lim])
        if n_zeros == 0:
            return(fft)
        fft[:n_zeros] = 0.
        if n_zeros > max_taper_len:
            taper_len = max_taper_len
        else:
            taper_len = n_zeros
        i_left = n_zeros-taper_len
        fft[i_left: n_zeros] = fft[n_zeros+1]*self._taper_left(taper_len)
        return fft

    def _add_right_taper(self, fft, freqs, freq_lim, max_taper_len=100):
        n_zeros = len(fft[freqs > freq_lim])
        if n_zeros == 0:
            return(fft)
        fft[-n_zeros:] = 0.
        if n_zeros > max_taper_len:
            taper_len = max_taper_len
        else:
            taper_len = n_zeros
        i_right = -(n_zeros-taper_len+1)
        # print(f'{taper_len=}, {i_right=}, {-n_zeros=}')
        fft[-(n_zeros+1): i_right] = fft[-(n_zeros+1)]*self._taper_right(taper_len)
        return fft

    @staticmethod
    def _taper_right(npts):
        return np.kaiser(2*npts, 14)[npts:]

    @staticmethod
    def _taper_left(npts):
        return np.kaiser(2*npts, 14)[:npts]

    @classmethod
    def from_amp_var_period(cls, amplitude, variance, period,
                            value_units="unknown"):
        """
        Generate a class object from an amplitude, a variance and a central period
        """
        raise ValueError('from_amp_var_period() not yet written')

__init__

__init__(freqs, values, value_units='unknown')
Parameters:
  • freqs (list or

    class:np.array): Frequencies (Hz)

  • values (list or

    class:np.array): PSD values in dB

  • value_unites (str) –

    Units of values

Source code in crawtools/compliance/psd_vals.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, freqs, values, value_units="unknown"):
    """
    Args:
        freqs (list or :class:`np.array`): Frequencies (Hz)
        values (list or :class:`np.array`): PSD values in dB
        value_unites (str): Units of values
    """
    if isinstance(freqs, (list, tuple)):
        freqs = np.array(freqs)
    else:
        assert isinstance(freqs, np.ndarray)
    assert np.all(np.diff(freqs) > 0), 'freqs are not monotonically increasing'
    if isinstance(values, (list, tuple)):
        values = np.array(values)
    else:
        assert isinstance(values, np.ndarray)
    assert len(freqs) == len(values)
    assert isinstance(value_units, str)
    self.freqs = freqs
    self.values = values
    self.value_units = value_units

as_fft

as_fft(freqs, left='taper', right='taper', phases=None, plotit=False)

Return an fft "equivalent" to the given Power Spectral Density Args: freqs (list, np.array or None): Resample at the given freqs left (None, float or 'taper'): how to handle values below the lowest self.freq: - None: use value at lowest input frequency - float: set to the given value - 'taper': taper using Kaiser function right (None, float, or 'taper'): how to handle values above the highest self.freq phases (np.array or None): force phases to be the given values (must be same length as frequencies)

Source code in crawtools/compliance/psd_vals.py
 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
def as_fft(self, freqs, left='taper', right='taper', phases=None,
           plotit=False):
    """
    Return an fft "equivalent" to the given Power Spectral Density
    Args:
        freqs (list, np.array or None): Resample at the given freqs
        left (None, float or 'taper'): how to handle values below the
            lowest self.freq:
                - None: use value at lowest input frequency
                - float: set to the given value
                - 'taper': taper using Kaiser function
        right (None, float, or 'taper'): how to handle values above the
            highest self.freq
        phases (np.array or None): force phases to be the given values
            (must be same length as frequencies)
    """
    if not freqs[0] == 0:
        raise ValueError(f"Cannot create an fft without f[0] == 0")
    fdiffs = np.diff(freqs)
    if not np.all(np.abs(fdiffs-fdiffs[0]) < fdiffs[0]/1e6):
        raise ValueError(f"freqs are not evenly spaced")

    fft = np.power(10., np.interp(freqs, self.freqs, self.values)/20)

    fft = np.nan_to_num(fft)        
    if left == 'taper':
        fft = self._add_left_taper(fft, freqs, self.freqs[0])
    elif left is not None:
        fft[freqs<self.freqs[0]] = left
    if right == 'taper':
        fft = self._add_right_taper(fft, freqs, self.freqs[-1])
    elif right is not None:
        fft[freqs>self.freqs[-1]] = right

    if plotit is True:
        f,a = plt.subplots()
        a.semilogx(freqs, fft, 'b')
        a.axvline(self.freqs[0], color='g', ls='--')
        a.axvline(self.freqs[-1], color='g', ls='--')
        a.semilogx(freqs, fft, 'r')
        plt.show()
    fft[0] = 0. # Make sure the zero-frequency value is zero
    # Scale for sample rate and window length
    sampling_rate = 1/(2*freqs[-1])
    # Bendata&Piersol 1986 eqs 11.100 & 11.102
    mul_factor = np.sqrt(len(freqs) * sampling_rate)  / 2    
    # mul_factor = (len(freqs)) / 2 
    # mul_factor = (sampling_rate) / 2 
    # div_factor = 2 / (sampling_rate) 
    # div_factor = 1    
    fft *= mul_factor
    if phases is None:
        phases = np.radians(self._random_phases(len(fft)))
    return fft * np.exp(1j * phases)

from_amp_var_period classmethod

from_amp_var_period(amplitude, variance, period, value_units='unknown')

Generate a class object from an amplitude, a variance and a central period

Source code in crawtools/compliance/psd_vals.py
154
155
156
157
158
159
160
@classmethod
def from_amp_var_period(cls, amplitude, variance, period,
                        value_units="unknown"):
    """
    Generate a class object from an amplitude, a variance and a central period
    """
    raise ValueError('from_amp_var_period() not yet written')

resample_values

resample_values(freqs)

Resample values at the given frequencies

Source code in crawtools/compliance/psd_vals.py
57
58
59
60
61
def resample_values(self, freqs):
    """
    Resample values at the given frequencies
    """
    return np.interp(freqs, self.freqs, self.values)

crawtools.compliance.tide_coefficients

TideCoefficients

Source code in crawtools/compliance/tide_coefficients.py
 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
class TideCoefficients():
    def __init__(self, coeffs_dict=None):
        """
        Args:
            coeffs_dict (dict): keys = component names, values =
                (period (hours), amplitude (?), phase).  If phase is None,
                will randomly generate for each trace.
        """
        if coeffs_dict is None:
            coeffs_dict = self._earth_tides_default()
        assert isinstance(coeffs_dict, dict)
        for v in coeffs_dict.values():
            assert isinstance(v, list)
            assert len(v) == 3
        self.coeffs_dict = coeffs_dict

    @staticmethod
    def _earth_tides_default():
        return {"M2": [12.421, 384.83, None],
                "K1": [23.934, 191.78, None],
                "S2": [12, 179.05, None],
                "O1": [25.819, 158.11, None],
                "N2": [12.658, 73.69, None],
                "P1": [24.066, 70.88, None],
                "K2": [11.967, 48.72, None],
                "Mf": [13.661*12, 40.36, None]}

    def make_trace(self, ref_trace):
        tide_trace = ref_trace.copy()
        tide_trace.data = np.zeros(len(tide_trace.data))
        for k, v in self.coeffs_dict.items():
            period = v[0]
            amp = v[1]
            phase = v[2]
            if phase is None:
                phase = 360 * random.random()
            tide_trace.data += np.sin(2 * np.pi * tide_trace.times() / (3600 * period) + np.radians(phase)) * amp
        return tide_trace

__init__

__init__(coeffs_dict=None)
Parameters:
  • coeffs_dict (dict, default: None ) –

    keys = component names, values = (period (hours), amplitude (?), phase). If phase is None, will randomly generate for each trace.

Source code in crawtools/compliance/tide_coefficients.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def __init__(self, coeffs_dict=None):
    """
    Args:
        coeffs_dict (dict): keys = component names, values =
            (period (hours), amplitude (?), phase).  If phase is None,
            will randomly generate for each trace.
    """
    if coeffs_dict is None:
        coeffs_dict = self._earth_tides_default()
    assert isinstance(coeffs_dict, dict)
    for v in coeffs_dict.values():
        assert isinstance(v, list)
        assert len(v) == 3
    self.coeffs_dict = coeffs_dict