PyMVP 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PyMVP/mvp_routines.py ADDED
@@ -0,0 +1,996 @@
1
+
2
+ ################################################################################
3
+ #
4
+ # Collection of routines for MVP treatment
5
+ #
6
+ #
7
+ # Function get_log: Read MPV log file to get starting and ending times of the cycle
8
+ #
9
+ #
10
+ # Function read_mvp_cycle: Read one MVP cycle from a mvp_2022XXX.m1 file
11
+ #
12
+ #
13
+ # Function time_mvp_cycle_up:
14
+ # Allocate time to each sample for a MVP cycle
15
+ # Select the ascending MVP cycle (less noise)
16
+ #
17
+ # Function time_mvp_cycle_down:
18
+ # Allocate time to each sample for a MVP cycle
19
+ # Select the descending MVP cycle
20
+ #
21
+ #
22
+ #
23
+ #
24
+ #
25
+ #
26
+ #
27
+ #
28
+ ################################################################################
29
+
30
+ #
31
+ # Import libraries
32
+ #
33
+
34
+ import numpy as np
35
+ import scipy.stats as st
36
+ from datetime import date
37
+ from datetime import datetime
38
+ from scipy import interpolate
39
+ from scipy.signal import butter, freqz
40
+ from scipy import signal
41
+ import gsw
42
+ from scipy.interpolate import pchip_interpolate
43
+ import similaritymeasures
44
+ from netCDF4 import Dataset
45
+ from scipy.signal import butter, filtfilt, correlate, correlation_lags
46
+
47
+ #
48
+ ################################################################################
49
+ #
50
+ # Function get_log: Read MPV log file to get starting and ending times of the cycle, latitude, longitude, and datetime
51
+ #
52
+ # input:
53
+ # mvp_log_name : ASCII MVP Log file (MVP_2022xxx.log)
54
+ # Yorig : time is counted in days since Yorig/1/1 (here 1950/1/1)
55
+ #
56
+ # output:
57
+ # mvp_tstart : Start of the dive in days since Yorig/1/1
58
+ # mvp_tend : End of the dive in days since Yorig/1/1
59
+ # cycle_dur : Duration of the cycle in seconds
60
+ # lat : Latitude (float, if available)
61
+ # lon : Longitude (float, if available)
62
+ # dt_station : Datetime object (datetime.datetime) of the station (if available)
63
+ #
64
+ ################################################################################
65
+
66
+ def get_log(mvp_log_name,Yorig):
67
+
68
+ #print('Reading '+mvp_log_name)
69
+
70
+ # Get start time and end time of the cycle
71
+ flog = open(mvp_log_name, 'r', encoding = "ISO-8859-1")
72
+
73
+ # --- Read header and extract date, lat, lon, datetime ---
74
+ lat = None
75
+ lon = None
76
+ dt_station = None
77
+ mvptime = None
78
+ mvpdate = None
79
+ header_lines = []
80
+ # Read first 14 lines (header)
81
+ for i in range(14):
82
+ line = flog.readline()
83
+ header_lines.append(line)
84
+ # Parse LAT (line 9, index 8)
85
+ try:
86
+ lat_line = header_lines[8]
87
+ lat_str = lat_line.split(':')[1].strip().split(',')[0] # e.g. '4253.6113800'
88
+ lat_dir = lat_line.strip().split(',')[-1].replace(':','').strip() # e.g. 'N'
89
+ lat_deg = float(lat_str[:2])
90
+ lat_min = float(lat_str[2:])
91
+ lat = lat_deg + lat_min/60.0
92
+ if lat_dir.upper() == 'S':
93
+ lat = -lat
94
+ except Exception:
95
+ lat = None
96
+ # Parse LON (line 10, index 9)
97
+ try:
98
+ lon_line = header_lines[9]
99
+ lon_str = lon_line.split(':')[1].strip().split(',')[0] # e.g. '00614.5387900'
100
+ lon_dir = lon_line.strip().split(',')[-1].replace(':','').strip() # e.g. 'E'
101
+ lon_deg = float(lon_str[:3])
102
+ lon_min = float(lon_str[3:])
103
+ lon = lon_deg + lon_min/60.0
104
+ if lon_dir.upper() == 'W':
105
+ lon = -lon
106
+ except Exception:
107
+ lon = None
108
+ # Parse Time (line 12, index 11)
109
+ try:
110
+ time_line = header_lines[11]
111
+ time_str = time_line.split(':',1)[1].strip() # e.g. '10:28:58.6'
112
+ except Exception:
113
+ time_str = None
114
+ # Parse Date (line 13, index 12)
115
+ try:
116
+ date_line = header_lines[12]
117
+ date_str = date_line.split(':',1)[1].strip() # e.g. '09/08/2025'
118
+ mvpdate = datetime.strptime(date_str, "%d/%m/%Y").date()
119
+ mvptime = mvpdate.toordinal() - date.toordinal(date(Yorig, 1, 1))
120
+ except Exception:
121
+ mvpdate = None
122
+ mvptime = None
123
+ # Compose datetime object
124
+ try:
125
+ if time_str is not None and mvpdate is not None:
126
+ dt_station = datetime.strptime(date_str + ' ' + time_str, "%d/%m/%Y %H:%M:%S.%f")
127
+ except ValueError:
128
+ try:
129
+ dt_station = datetime.strptime(date_str + ' ' + time_str, "%d/%m/%Y %H:%M:%S")
130
+ except Exception:
131
+ dt_station = None
132
+
133
+ # Read 3 more lines (as before)
134
+ for i in range(3):
135
+ line = flog.readline()
136
+
137
+ # --- Read data for start/stop times ---
138
+ mvp_tstart = None
139
+ mvp_tend = None
140
+ hh1 = mn1 = sc1 = None
141
+ while True:
142
+ line = flog.readline()
143
+ if line == '':
144
+ break
145
+ words = line.split()
146
+ if len(words) < 3:
147
+ continue
148
+ if words[1] == 'EVENT:':
149
+ if words[2][:5] == 'START':
150
+ hh1 = float(words[2][6:8])
151
+ mn1 = float(words[2][9:11])
152
+ sc1 = float(words[2][12:16])
153
+ mvp_tstart = mvptime + (hh1 + (mn1 + sc1 / 60.) / 60.) / 24.
154
+ if words[2][:5] == 'STOP_':
155
+ hh2 = float(words[2][6:8])
156
+ mn2 = float(words[2][9:11])
157
+ sc2 = float(words[2][12:16])
158
+ if hh2 < hh1:
159
+ hh2 = hh2 + 24.
160
+ mvp_tend = mvptime + (hh2 + (mn2 + sc2 / 60.) / 60.) / 24.
161
+
162
+ cycle_dur = (mvp_tend - mvp_tstart) * (24 * 60 * 60)
163
+
164
+ flog.close()
165
+
166
+ return mvp_tstart, mvp_tend, cycle_dur, lat, lon, dt_station
167
+
168
+
169
+
170
+
171
+
172
+
173
+ #
174
+ ################################################################################
175
+ #
176
+ # Function read_mvp_cycle_raw: Read one MVP cycle from a mvp_2022XXX.raw file
177
+ #
178
+ # input:
179
+ # mvp_dat_name : ASCII MVP .raw file (mvp_2022xxx.raw)
180
+ #
181
+ #
182
+ # output:
183
+ #
184
+ # pres : pressure [dbar]
185
+ # soundvel : Sound velocity [m/s]
186
+ # do : dissolved oxygen [umol/kg]
187
+ # temp2 : Temperature from DO sensor [oC]
188
+ # suna : SUNA data [umol/kg]
189
+ # fluo : Fluorometer data [ug/l]
190
+ # turb : Turbidity data [NTU]
191
+ # ph : pH data [pH units]
192
+ #
193
+ #
194
+ ################################################################################
195
+ #
196
+
197
+ def read_mvp_cycle_raw(mvp_dat_name):
198
+
199
+ #print('Reading '+mvp_dat_name)
200
+
201
+ # Open the file (there was a problem with encoding)
202
+ fdat = open(mvp_dat_name, 'r', encoding = "ISO-8859-1")
203
+
204
+ # Préparer les listes pour chaque variable utile
205
+ pres = [] # Pressure
206
+ cond = [] # Conductivity
207
+ temp = [] # Temperature
208
+ soundvel = [] # Sound velocity
209
+ dox = [] # Dissolved oxygen
210
+ temp2 = [] # Temperature from DO sensor
211
+ suna = [] # SUNA data
212
+ fluo = [] # Fluorometer data
213
+ turb = [] # Turbidity data
214
+ ph = [] # pH data
215
+
216
+
217
+ # Sauter le header : lire jusqu'à la première ligne commençant par 'M'
218
+ while True:
219
+ pos = fdat.tell()
220
+ line = fdat.readline()
221
+ if line == '':
222
+ break
223
+ line = line.strip()
224
+ if len(line) == 0:
225
+ continue
226
+ if line[0] == 'M':
227
+ # On revient en arrière pour traiter cette ligne dans la boucle principale
228
+ fdat.seek(pos)
229
+ break
230
+
231
+ # Lecture des données à partir de la première ligne 'M'
232
+ while True:
233
+ line = fdat.readline()
234
+ if line == '':
235
+ break
236
+ line = line.strip()
237
+ if len(line) == 0:
238
+ continue
239
+ if line[0] == 'Z':
240
+ continue # ignorer les lignes Z
241
+ if line[0] == 'M':
242
+ words = line.split()
243
+ if len(words) < 11:
244
+ continue
245
+ try:
246
+ pres.append(float(words[1]))
247
+ soundvel.append(float(words[2]))
248
+ cond.append(float(words[3]))
249
+ temp.append(float(words[4]))
250
+ dox.append(float(words[5]))
251
+ temp2.append(float(words[6]))
252
+ suna.append(float(words[7]))
253
+ fluo.append(float(words[8]))
254
+ turb.append(float(words[9]))
255
+ ph.append(float(words[10]))
256
+ except Exception:
257
+ continue
258
+
259
+ fdat.close()
260
+
261
+ # Convertir en numpy arrays
262
+ pres = np.array(pres)
263
+ soundvel = np.array(soundvel)
264
+ cond = np.array(cond)
265
+ temp = np.array(temp)
266
+ do = np.array(dox)
267
+ temp2 = np.array(temp2)
268
+ suna = np.array(suna)
269
+ fluo = np.array(fluo)
270
+ turb = np.array(turb)
271
+ ph = np.array(ph)
272
+
273
+ return pres, soundvel, cond, temp, do, temp2, suna, fluo, turb, ph
274
+
275
+
276
+
277
+ #
278
+ ################################################################################
279
+ #
280
+ # Function read_mvp_cycle_ncdf: Read one MVP cycle from a mvp_2022XXX.ncdf file
281
+ #
282
+ # input:
283
+ # mvp_dat_name : NetCDF MVP .ncdf file (mvp_2022xxx.ncdf)
284
+ #
285
+ #
286
+ # output:
287
+ #
288
+ # pres : pressure [dbar]
289
+ # soundvel : Sound velocity [m/s]
290
+ # do : dissolved oxygen [umol/kg]
291
+ # temp2 : Temperature from DO sensor [oC]
292
+ # suna : SUNA data [umol/kg]
293
+ # fluo : Fluorometer data [ug/l]
294
+ # turb : Turbidity data [NTU]
295
+ # ph : pH data [pH units]
296
+ #
297
+ #
298
+ ################################################################################
299
+ #
300
+
301
+ def read_mvp_cycle_ncdf(mvp_dat_name):
302
+
303
+
304
+ #print('Reading '+mvp_dat_name)
305
+
306
+ # Open the file
307
+ nc = Dataset(mvp_dat_name, 'r')
308
+
309
+ # Read variables
310
+ pres = nc.variables['PRES'][:]
311
+ soundvel = nc.variables['SOUNDVEL'][:]
312
+ cond = nc.variables['COND'][:]
313
+ temp = nc.variables['TEMP'][:]
314
+ do = nc.variables['DO'][:]
315
+ temp2 = nc.variables['TEMP2'][:]
316
+ suna = nc.variables['SUNA'][:] if 'SUNA' in nc.variables else None
317
+ fluo = nc.variables['FLUO'][:] if 'FLUO' in nc.variables else None
318
+ turb = nc.variables['TURB'][:] if 'TURB' in nc.variables else None
319
+ ph = nc.variables['PH'][:] if 'PH' in nc.variables else None
320
+
321
+ nc.close()
322
+
323
+ return pres, soundvel, cond, temp, do, temp2, suna, fluo, turb, ph
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+ # ################################################################################
335
+ #
336
+ # Function time_mvp_cycle_up_bgc:
337
+ # Allocate time to each sample for a MVP cycle (BGC version)
338
+ # Select the ascending MVP cycle (less noise)
339
+ #
340
+ # input:
341
+ # pres : pressure [dbar]
342
+ # soundvel : sound velocity [m/s]
343
+ # do : dissolved oxygen [umol/kg]
344
+ # temp2 : temperature from DO sensor [°C]
345
+ # suna : SUNA data [umol/kg]
346
+ # fluo : fluorometer data [ug/l]
347
+ # turb : turbidity data [NTU]
348
+ # ph : pH data [pH units]
349
+ # mvp_tstart: start of the dive in days since Yorig/1/1
350
+ # mvp_tend : end of the dive in days since Yorig/1/1
351
+ #
352
+ # output:
353
+ # pres_up, soundvel_up, do_up, temp2_up, suna_up, fluo_up, turb_up, ph_up
354
+ # (données ascendantes pour chaque variable)
355
+ # time_cycle : Time of each sample in days since Yorig/1/1
356
+ #
357
+ # ################################################################################
358
+ #
359
+
360
+ def time_mvp_cycle_up(args,mvp_tstart,mvp_tend):
361
+
362
+ # Allocate time to each data point
363
+ N = np.size(args[0])
364
+ time_cycle = np.linspace(mvp_tstart, mvp_tend, N)
365
+
366
+ # Get only the ascending lines
367
+ ibot = np.min(np.where(args[0] == args[0].max()))
368
+ for i, arg in enumerate(args):
369
+ args[i] = arg[ibot:]
370
+
371
+
372
+ time_cycle_up = time_cycle[ibot:]
373
+
374
+ return args + [time_cycle_up]
375
+
376
+
377
+ # ################################################################################
378
+ #
379
+ # Function time_mvp_cycle_down_bgc:
380
+ # Allocate time to each sample for a MVP cycle (BGC version)
381
+ # Select the descending MVP cycle
382
+ #
383
+ # input:
384
+ # pres : pressure [dbar]
385
+ # soundvel : sound velocity [m/s]
386
+ # do : dissolved oxygen [umol/kg]
387
+ # temp2 : temperature from DO sensor [°C]
388
+ # suna : SUNA data [umol/kg]
389
+ # fluo : fluorometer data [ug/l]
390
+ # turb : turbidity data [NTU]
391
+ # ph : pH data [pH units]
392
+ # mvp_tstart: start of the dive in days since Yorig/1/1
393
+ # mvp_tend : end of the dive in days since Yorig/1/1
394
+ #
395
+ # output:
396
+ # pres_down, soundvel_down, do_down, temp2_down, suna_down, fluo_down, turb_down, ph_down
397
+ # (données descendantes pour chaque variable)
398
+ # time_cycle_down: Time of each sample in days since Yorig/1/1
399
+ #
400
+ # ################################################################################
401
+ def time_mvp_cycle_down(args, mvp_tstart, mvp_tend):
402
+
403
+ N = np.size(args[0])
404
+ time_cycle = np.linspace(mvp_tstart, mvp_tend, N)
405
+
406
+ # Trouver les indices pour la partie descendante
407
+ ibot = np.min(np.where(args[0] == args[0].max()))
408
+
409
+ for i, arg in enumerate(args):
410
+ args[i] = arg[:ibot]
411
+
412
+ time_cycle_down = time_cycle[:ibot]
413
+
414
+ return args + [time_cycle_down]
415
+
416
+
417
+
418
+ # ################################################################################
419
+ #
420
+ # Function raw_data_conversion:
421
+ # Converts raw BGC sensor data (in V or mV) to physical units.
422
+ # Not very efficien because of np.vectorize, if too long, recode the conversion functions to work with numpy arrays directly.
423
+ #
424
+ # input:
425
+ # pres, soundvel : already in physical units
426
+ # do_raw, temp2_raw, suna_raw, fluo_raw, turb_raw, ph_raw : raw sensor data to convert
427
+ #
428
+ # output:
429
+ # pres, soundvel, do, temp2, suna, fluo, turb, ph : all variables in physical units (currently identical to input)
430
+ #
431
+ # ################################################################################
432
+ def raw_data_conversion(pres, soundvel, cond, temp, do_raw, temp2_raw, suna_raw, fluo_raw, turb_raw, ph_raw):
433
+ """
434
+ Converts raw BGC sensor data (in V or mV) to physical units.
435
+
436
+ """
437
+
438
+ temp2 = np.vectorize(TEMP2_conversion)(temp2_raw)
439
+ do = np.vectorize(DO_conversion)(do_raw, temp, pres)
440
+ suna = np.vectorize(SUNA_conversion)(suna_raw)
441
+ fluo = np.vectorize(FLUO_conversion)(fluo_raw)
442
+ turb = np.vectorize(TURBIDITY_conversion)(turb_raw)
443
+ ph = np.vectorize(PH_conversion)(ph_raw, temp)
444
+ return pres, soundvel, cond, temp, do, temp2, suna, fluo, turb, ph
445
+
446
+ def TEMP2_conversion(temp_raw2):
447
+ """
448
+ Converts raw temperature data from the DO sensor to physical units.
449
+ """
450
+
451
+ A = -1.191875e1
452
+ B = 2.145289e1
453
+ C = -3.611291
454
+ D = 6.788267e-1
455
+
456
+ # Assuming temp_raw2 is in V
457
+ temp2 = A + B * temp_raw2 + C * temp_raw2**2 + D * temp_raw2**3
458
+
459
+ return temp2
460
+
461
+
462
+ def DO_conversion(do_raw, temp, pres):
463
+ """
464
+ Converts raw dissolved oxygen data to physical units.
465
+
466
+ """
467
+
468
+ A = -4.329108e1
469
+ B = 1.326965e2
470
+ C = -3.459148e-1
471
+ D = 1.011300e-2
472
+ E = 3.9e-3
473
+ F = 4.03e-5
474
+ G = 0
475
+ H = 1
476
+
477
+ P_ = A/(1+D*(temp-25)+F*(temp-25)**2) + B/(do_raw*(1+D*(temp-25)+F*(temp-25)**2)+C)
478
+
479
+ do = G + H*P_
480
+ do = do*(1+E*(pres+10.1325)/100) # Apply pressure correction in MPa (air+water column)
481
+
482
+ return do
483
+
484
+ def SUNA_conversion(suna_raw):
485
+ """
486
+ Converts raw SUNA data to physical units.
487
+ """
488
+ # Not sur at all about these coefficients TO CHECK
489
+ Vmax = 1.8621
490
+ Vmin = 0.3666
491
+ DACmax = 39.56
492
+ DACmin = -0.2919
493
+
494
+ A1 = (DACmax - DACmin)/(Vmax-Vmin)
495
+ A0 = DACmin - A1 * Vmin
496
+
497
+ # Assuming suna_raw is in mV
498
+ # suna is Cnitrate in umol/L
499
+ suna = A0 + A1 * suna_raw*1e-3
500
+
501
+ suna = suna_raw*1e-3
502
+ return suna
503
+
504
+ def TURBIDITY_conversion(turb_raw):
505
+
506
+ """
507
+ Converts raw turbidity data to physical units.
508
+ """
509
+
510
+ # for chlorophyll concentration
511
+ # Scale_factor =6
512
+ # Darkcounts = 0.091
513
+
514
+ # for turbidity in NTU
515
+ Scale_factor = 2
516
+ Dark_counts = 0.098
517
+
518
+ # Assuming turb_raw is in mV
519
+ turb = Scale_factor * (turb_raw*1e-3 - Dark_counts)
520
+
521
+ return turb
522
+
523
+ def FLUO_conversion(fluo_raw):
524
+ """
525
+ Converts raw fluorometer data to physical units.
526
+ """
527
+
528
+ # for chlorophyll concentration in ug/l
529
+ Scale_factor = 6
530
+ Dark_counts = 0.091
531
+ fluo = Scale_factor * (fluo_raw*1e-3 - Dark_counts)
532
+
533
+ return fluo
534
+
535
+ def PH_conversion(ph_raw, temp):
536
+ """
537
+ Converts raw pH data to physical units.
538
+ """
539
+
540
+ pHslope = 4.6630
541
+ pHoffset = 2.5330
542
+ # Assuming ph_raw is in m
543
+ pH = 7.0 + (ph_raw*1e-3 - pHoffset) / (pHslope * (temp+273.15) * 1.98416e-4)
544
+
545
+ return pH
546
+
547
+
548
+ #
549
+ ################################################################################
550
+ #
551
+ #
552
+ #
553
+ # Function remove surface waves : apply a butterworth filter to the profiles
554
+ # to remove the surface waves, set here to 0.2 Hz
555
+ #
556
+ # input:
557
+ # data : variable to filter
558
+ # TIME : time is counted in days since Yorig/1/1 (here 1950/1/1)
559
+ #
560
+ # output:
561
+ # data_final : filtered data
562
+ # fluo_chla : fluorometer intensity
563
+ #
564
+ ################################################################################
565
+ #
566
+
567
+ def remove_surface_waves(data,TIME,f_s,f_c,order):
568
+ data_final = np.zeros((data.shape[0],data.shape[1]))
569
+ data_final[:] = np.nan
570
+
571
+ for i_profile in range(data.shape[0]):
572
+ if np.isnan(np.nanmean(TIME[i_profile,:]))==0:
573
+ time_sampling=np.arange(np.nanmin(TIME[i_profile,:]),np.nanmax(TIME[i_profile,:]),1/f_s)
574
+ time_sampling = time_sampling[np.where(time_sampling<=np.nanmax(TIME[i_profile,:]))[0]]
575
+
576
+ ind = np.where((np.isnan(TIME[i_profile,:])==0) & (np.isnan(data[i_profile,:])==0))[0]
577
+ if len(ind)>6:
578
+ f1 = interpolate.interp1d(TIME[i_profile,ind], data[i_profile,ind],'linear',fill_value="extrapolate")
579
+ W_sampling = f1(time_sampling)
580
+
581
+ # Fourier transform
582
+ #f_s = 10*f_s
583
+ #f_s = 25*10
584
+ #f_c = 1 # Cut-off frequency in Hz
585
+ #order = 10 # Order of the butterworth filter
586
+
587
+ omega_c = f_c # Cut-off angular frequency
588
+ omega_c_d = omega_c / (f_s) # Normalized cut-off frequency (digital)
589
+
590
+ # Design the digital Butterworth filter
591
+ b, a = butter(order, omega_c_d)
592
+
593
+ W_lisse = signal.filtfilt(b, a, signal.detrend(W_sampling))
594
+
595
+ W_lisse = W_lisse + W_sampling - signal.detrend(W_sampling)
596
+ ind = np.where(TIME[i_profile,:]<=np.nanmax(time_sampling))[0]
597
+ f2 = interpolate.interp1d(time_sampling, W_lisse,'linear',fill_value="extrapolate")
598
+ data_final[i_profile,ind] = f2(TIME[i_profile,ind])
599
+ del ind, time_sampling, b, a, f1, f2, W_lisse, W_sampling, omega_c, omega_c_d
600
+
601
+ return data_final
602
+
603
+
604
+ #
605
+ ################################################################################
606
+ #
607
+ #
608
+ #
609
+ # Correct thermistor viscous heating
610
+ #
611
+ # input:
612
+ # TEMP0 : In-situ Temperature
613
+ # SAL_PRA0 : Practical Salinity
614
+ # PRES0 : Pressure
615
+ # LON0 : Longitude
616
+ # LAT0 : Latitude
617
+ # TIME : time is counted in days since Yorig/1/1 (here 1950/1/1)
618
+ #
619
+ # output:
620
+ # TEMP1 : Corrected temperature
621
+ #
622
+ ################################################################################
623
+ #
624
+
625
+ def viscous_heating(TEMP0, SAL_PRA0, PRES0, LON0, LAT0, TIME):
626
+ TEMP1 = np.zeros((TEMP0.shape[0], TEMP0.shape[1]))
627
+ TEMP1[:] = np.nan
628
+ for i in range(TEMP0.shape[0]):
629
+
630
+ T = np.zeros(TEMP0.shape[1])
631
+ S = np.zeros(TEMP0.shape[1])
632
+ P = np.zeros(PRES0.shape[1])
633
+ S = gsw.SA_from_SP(SAL_PRA0[i,:], PRES0[i,:], LON0[i,:], LAT0[i,:])
634
+ T = TEMP0[i,:]
635
+
636
+ T = T + 273.15;
637
+
638
+ a = [-5.8002206e+03, 1.3914993e00, -4.8640239e-02, 4.1764768e-05, -1.4452093e-08, 6.5459673e+00]
639
+ Pv_w = np.exp((a[0]/T) + a[1] + a[2]*T + a[3]*T**2 + a[4]*T**3 + a[5]*np.log(T))
640
+
641
+ b = [-4.5818e-4,-2.0443e-6]
642
+ P0 = Pv_w*np.exp(b[0]*S+b[1]*S**2)/1e6
643
+
644
+ T = TEMP0[i,:]
645
+ P0[np.where(T<100)[0]] = 0.101325
646
+ T68 = 1.00024*(T+273.15)
647
+
648
+ S = SAL_PRA0[i,:]
649
+ S_gkg=S
650
+
651
+ P = PRES0[i,:]
652
+
653
+ A = 5.328 - 9.76e-2*S + 4.04e-4*S**2
654
+ B = -6.913e-3 + 7.351e-4*S - 3.15e-6*S**2
655
+ C = 9.6e-6 - 1.927e-6*S + 8.23e-9*S**2
656
+ D = 2.5e-9 + 1.666e-9*S - 7.125e-12*S**2
657
+ cp_sw_P0 = 1000*(A + B*T68 + C*(T68**2) + D*(T68**3))
658
+
659
+ c1 = -3.1118
660
+ c2 = 0.0157
661
+ c3 = 5.1014e-5
662
+ c4 = -1.0302e-6
663
+ c5 = 0.0107
664
+ c6 = -3.9716e-5
665
+ c7 = 3.2088e-8
666
+ c8 = 1.0119e-9
667
+
668
+ cp_sw_P = (P - P0)*(c1 + c2*T + c3*(T**2) + c4*(T**3) + S_gkg*(c5 + c6*T + c7*(T**2) + c8*(T**3)))
669
+
670
+ cp = cp_sw_P0 + cp_sw_P
671
+ del cp_sw_P0, cp_sw_P, P, P0, c1, c2, c3, c4, c5, c6, c7, c8, T, S, T68, S_gkg
672
+
673
+ T = np.zeros(TEMP0.shape[1])
674
+ S = np.zeros(TEMP0.shape[1])
675
+ S = SAL_PRA0[i,:]
676
+ T = TEMP0[i,:]
677
+ S = S/1000;
678
+
679
+ a = [1.5700386464e-01, 6.4992620050e01, -9.1296496657e+01, 4.2844324477e-05, 1.5409136040e+00, 1.9981117208e-02, -9.5203865864e-05, 7.9739318223e+00, -7.5614568881e-02, 4.7237011074e-04]
680
+
681
+ mu_w = a[3] + 1/(a[0]*(T+a[1])**2+a[2])
682
+
683
+ A = a[4] + a[5]*T + a[6]*T**2
684
+ B = a[7] + a[8]*T + a[9]*T**2
685
+ mu = mu_w*(1 + A*S + B*S**2)
686
+ del mu_w, A, S, B, T, a
687
+
688
+ T = np.zeros(TEMP0.shape[1])
689
+ S = np.zeros(TEMP0.shape[1])
690
+ S = SAL_PRA0[i,:]
691
+ T = TEMP0[i,:]
692
+ T68 = 1.00024*T
693
+ S = S / 1.00472
694
+ k = 10**(np.log10(240+0.0002*S)+0.434*(2.3-(343.5+0.037*S)/(T+273.15))*(1-(T+273.15)/(647.3+0.03*S))**(1/3)-3)
695
+ del T, S, T68
696
+
697
+ W = np.zeros(TEMP0.shape[1])
698
+ W[1:-1] = (PRES0[i,2::]-PRES0[i,0:-2])/(TIME[i,2::]*24*3600-TIME[i,0:-2]*24*3600)
699
+ Pr = cp*mu/k
700
+
701
+ dT=0.80e-4*(W**2)*(Pr**(1/2))
702
+ TEMP1[i,:] = TEMP0[i,:] - dT
703
+ del dT, Pr, cp, mu, k, W
704
+ return(TEMP1)
705
+
706
+ #
707
+ ################################################################################
708
+ #
709
+ # Function vertical_interp: Interpolate each profile on a required variable
710
+ #
711
+ # input:
712
+ # Depth_mat : original variable acquisition
713
+ # Mat : field to be interpolated
714
+ # Depth_interp : variable on which the field is interpolated (regularly sampled)
715
+ #
716
+ # output:
717
+ #
718
+ # Mat_Z_interp : interpolated field
719
+ #
720
+ #
721
+ ################################################################################
722
+ #
723
+
724
+ def vertical_interp(Depth_mat,Mat,Depth_interp):
725
+
726
+ Mat_Z_interp = np.zeros((Mat.shape[0],len(Depth_interp)))
727
+ Mat_Z_interp[:] = np.nan
728
+
729
+ for i in range(Mat_Z_interp.shape[0]):
730
+ Depth_temp, ind = np.unique(Depth_mat[i,:],return_index=True)
731
+ Mat_temp = Mat[i,ind]
732
+ del ind
733
+ Mat_temp = Mat_temp[np.where(np.isnan(Depth_temp)==0)[0]]
734
+ Depth_temp = Depth_temp[np.where(np.isnan(Depth_temp)==0)[0]]
735
+ Depth_temp = Depth_temp[np.where(np.isnan(Mat_temp)==0)[0]]
736
+ Mat_temp = Mat_temp[np.where(np.isnan(Mat_temp)==0)[0]]
737
+ if (len(Mat_temp)>2) & (len(Depth_temp)>2):
738
+ ind = np.arange(np.where(Depth_interp>=np.nanmin(Depth_temp))[0][0], np.where(Depth_interp<=np.nanmax(Depth_temp))[0][-1])
739
+ f1 = interpolate.interp1d(Depth_temp, Mat_temp,'linear')
740
+ Mat_Z_interp[i,ind] = f1(Depth_interp[ind])
741
+ #Mat_Z_interp[i,ind] = pchip_interpolate(Depth_temp, Mat_temp, Depth_interp[ind])
742
+ del ind
743
+ del Depth_temp, Mat_temp
744
+ del i
745
+ return Mat_Z_interp
746
+
747
+
748
+ #
749
+ ################################################################################
750
+ #
751
+ #
752
+ #
753
+ # Function mvp_bin(pres,salt,temp,time_cycle,Pbin,bin_method)
754
+ #
755
+ # Do a binning of MVP data on regulat pressure levels
756
+ #
757
+ # input:
758
+ # pres : pressure [dbar]
759
+ # fluo : fluo data [mg m-3]
760
+ # salt : Salinity [PSS-78]
761
+ # temp : Temperature [oC]
762
+ # time_cycle : Time of each sample in days since Yorig/1/1
763
+ # Pbin : pressure bins [dbar]
764
+ # bin_method : binning method ('mean', 'median',...)
765
+ # add_fluo : Flag to process fluorometer
766
+ #
767
+ # output:
768
+ # pres : pressure [dbar]
769
+ # fluo : fluo data [mg m-3]
770
+ # salt : Salinity [PSS-78]
771
+ # temp : Temperature [oC]
772
+ # time_cycle : Time in days since Yorig/1/1
773
+ #
774
+ #
775
+ ################################################################################
776
+ #
777
+
778
+ def median(Depth_mat,Mat,Depth_interp):
779
+
780
+ Mat_Z_interp = np.zeros((Mat.shape[0],len(Depth_interp)))
781
+ Mat_Z_interp[:] = np.nan
782
+
783
+ for i in range(Mat_Z_interp.shape[0]):
784
+ Depth_temp, ind = np.unique(Depth_mat[i,:],return_index=True)
785
+ Mat_temp = Mat[i,ind]
786
+ del ind
787
+ ind = np.argsort(Depth_temp, axis=0)
788
+ Mat_temp=np.take_along_axis(Mat_temp, ind, axis=0)
789
+ Depth_temp=np.take_along_axis(Depth_temp, ind, axis=0)
790
+ del ind
791
+ Mat_temp = Mat_temp[np.where(np.isnan(Depth_temp)==0)[0]]
792
+ Depth_temp = Depth_temp[np.where(np.isnan(Depth_temp)==0)[0]]
793
+ Depth_temp = Depth_temp[np.where(np.isnan(Mat_temp)==0)[0]]
794
+ Mat_temp = Mat_temp[np.where(np.isnan(Mat_temp)==0)[0]]
795
+ if (len(Mat_temp)>2) & (len(np.where(Depth_interp>=np.nanmin(Depth_temp))[0])>2) & (len(np.where(Depth_interp<=np.nanmax(Depth_temp))[0])>2):
796
+ ind = np.arange(np.where(Depth_interp>=np.nanmin(Depth_temp))[0][0], np.where(Depth_interp<=np.nanmax(Depth_temp))[0][-1])
797
+ if len(ind)>0:
798
+ vout = st.binned_statistic(Depth_temp,Mat_temp,statistic='median', bins=Depth_interp[ind])
799
+ Mat_Z_interp[i,ind[0:-1]] = vout.statistic
800
+ del ind, vout
801
+ del Depth_temp, Mat_temp
802
+ del i
803
+ return Mat_Z_interp
804
+
805
+ def Calc_dist_time(TIME1, LON1, LAT1, TIME2, LON2, LAT2):
806
+ Dist = np.zeros((len(TIME1), len(TIME2)))
807
+ Time = np.zeros((len(TIME1), len(TIME2)))
808
+ R = 6373.0
809
+
810
+ for i in range(len(TIME1)):
811
+ lat1 = np.radians(LAT1[i])
812
+ lon1 = np.radians(LON1[i])
813
+ lat2 = np.radians(LAT2[:])
814
+ lon2 = np.radians(LON2[:])
815
+
816
+ dlon = lon2 - lon1
817
+ dlat = lat2 - lat1
818
+
819
+ a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2
820
+ c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
821
+
822
+ Dist[i,:] = R * c * 1e3
823
+ Time[i,:] = np.abs(TIME2[:]-TIME1[i])
824
+
825
+ return Dist, Time
826
+
827
+
828
+
829
+
830
+ def filtering_tc(T,C,freq_echant,high_cutoff=1):
831
+
832
+
833
+ sampling_frequency = freq_echant
834
+ order = 2
835
+
836
+ nyquist = sampling_frequency / 2.0
837
+ normalized_cutoff = high_cutoff / nyquist
838
+
839
+ b_bp, a_bp = butter(order,normalized_cutoff, btype='lowpass')
840
+
841
+
842
+ # Filter only the valid (non-NaN) leading segment
843
+ valid_idx = np.where(np.isfinite(T) & np.isfinite(C))[0]
844
+ T_low = np.full_like(T, np.nan)
845
+ C_low = np.full_like(C, np.nan)
846
+
847
+ if valid_idx.size > 0:
848
+ n_valid = valid_idx[-1] + 1
849
+ T_low[:n_valid] = filtfilt(b_bp, a_bp, T[:n_valid])
850
+ C_low[:n_valid] = filtfilt(b_bp, a_bp, C[:n_valid])
851
+ return T_low,C_low
852
+
853
+
854
+ def temporal_lag(T,C,P,freq_echant):
855
+
856
+
857
+ # Band-pass filter to keep frequencies between 0.1 Hz and 9 Hz
858
+ low_cutoff = 0.1
859
+ high_cutoff = 5
860
+ sampling_frequency = freq_echant
861
+ order = 1
862
+
863
+ nyquist = sampling_frequency / 2.0
864
+ normalized_low = low_cutoff / nyquist
865
+ normalized_high = high_cutoff / nyquist
866
+
867
+ b_bp, a_bp = butter(order, [normalized_low, normalized_high], btype='band')
868
+
869
+ # Filter only the valid (non-NaN) leading segment
870
+ valid_idx = np.where(np.isfinite(T) & np.isfinite(C))[0]
871
+ T_high = np.full_like(T, np.nan)
872
+ C_high = np.full_like(C, np.nan)
873
+
874
+ if valid_idx.size > 0:
875
+ n_valid = valid_idx[-1] + 1
876
+ T_high[:n_valid] = filtfilt(b_bp, a_bp, T[:n_valid])
877
+ C_high[:n_valid] = filtfilt(b_bp, a_bp, C[:n_valid])
878
+
879
+ C_high = C_high - np.nanmean(C_high)
880
+ T_high = T_high - np.nanmean(T_high)
881
+
882
+
883
+
884
+ corr = correlate(T_high,
885
+ C_high,
886
+ mode='full')
887
+
888
+ lags = correlation_lags(len(T_high), len(C_high), mode='full')
889
+
890
+ corr = corr / (np.std(T_high) * np.std(C_high) * len(T_high))
891
+
892
+ # lag optimal
893
+ lag_samples = lags[np.argmax(np.abs(corr))]
894
+ lag_time = lag_samples / 20 # 20 Hz
895
+
896
+ # print("Lag (samples) =", lag_samples)
897
+ # print("Lag (sec) =", lag_time)
898
+
899
+ if lag_samples == 0:
900
+ return T, gsw.SP_from_C(C,T,P)
901
+
902
+ T_corr = T.copy()
903
+ T_corr[:-lag_samples] = T[lag_samples:]
904
+ T_corr[-lag_samples:] = T_corr[-lag_samples-1]
905
+ S_corr = gsw.SP_from_C(C,T_corr,P)
906
+
907
+
908
+ # t_shifted = Time + lag_time_sub
909
+ # T_corr2 = np.interp(t_shifted, Time, T, left=np.nan, right=np.nan)
910
+ # S_corr2 = gsw.SP_from_C(C,T_corr2,P)
911
+
912
+
913
+
914
+ normalized_cutoff = 0.5 / freq_echant / 2.0
915
+ b_bp, a_bp = butter(4,normalized_cutoff, btype='lowpass')
916
+ n_valid = np.isfinite(S_corr)
917
+ S_corr[n_valid] = filtfilt(b_bp, a_bp, S_corr[n_valid])
918
+
919
+ return T_corr,S_corr
920
+
921
+
922
+
923
+
924
+ def bin_average(P,T,C,time,dp=0.05):
925
+ if np.sum(np.isnan(P)) > 0:
926
+ print('nan in pressure, cannot bin')
927
+ if np.sum(np.isnan(T)) > 0:
928
+ print('nan in temperature, cannot bin')
929
+ if np.sum(np.isnan(C)) > 0:
930
+ print('nan in conductivity, cannot bin')
931
+
932
+ idx = np.argsort(P)
933
+ P, T, C, time = P[idx], T[idx], C[idx], time[idx]
934
+
935
+
936
+ bins = np.arange(P.min(), P.max(), dp)
937
+ digitized = np.digitize(P, bins)
938
+
939
+ P_bin = []
940
+ T_bin = []
941
+ C_bin = []
942
+ time_bin = []
943
+
944
+ for i in range(1, len(bins)):
945
+ mask_bin = digitized == i
946
+ if np.any(mask_bin):
947
+ P_bin.append(P[mask_bin].mean())
948
+ T_bin.append(T[mask_bin].mean())
949
+ C_bin.append(C[mask_bin].mean())
950
+ time_bin.append(time[mask_bin].mean())
951
+
952
+ return (np.array(P_bin),
953
+ np.array(T_bin),
954
+ np.array(C_bin),
955
+ np.array(time_bin))
956
+
957
+
958
+
959
+ def bin_average_v2(P,T,C,S,time,dp=0.05):
960
+ if np.sum(np.isnan(P)) > 0:
961
+ print('nan in pressure, cannot bin')
962
+ if np.sum(np.isnan(T)) > 0:
963
+ print('nan in temperature, cannot bin')
964
+ if np.sum(np.isnan(C)) > 0:
965
+ print('nan in conductivity, cannot bin')
966
+ if np.sum(np.isnan(S)) > 0:
967
+ print('nan in salinity, cannot bin')
968
+
969
+
970
+ idx = np.argsort(P)
971
+ P, T, C, S, time = P[idx], T[idx], C[idx], S[idx], time[idx]
972
+
973
+
974
+ bins = np.arange(P.min(), P.max(), dp)
975
+ digitized = np.digitize(P, bins)
976
+
977
+ P_bin = []
978
+ T_bin = []
979
+ C_bin = []
980
+ S_bin = []
981
+ time_bin = []
982
+
983
+ for i in range(1, len(bins)):
984
+ mask_bin = digitized == i
985
+ if np.any(mask_bin):
986
+ P_bin.append(P[mask_bin].mean())
987
+ T_bin.append(T[mask_bin].mean())
988
+ C_bin.append(C[mask_bin].mean())
989
+ S_bin.append(S[mask_bin].mean())
990
+ time_bin.append(time[mask_bin].mean())
991
+
992
+ return (np.array(P_bin),
993
+ np.array(T_bin),
994
+ np.array(C_bin),
995
+ np.array(S_bin),
996
+ np.array(time_bin))