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/__init__.py +1 -0
- PyMVP/main.py +1776 -0
- PyMVP/mvp_routines.py +996 -0
- pymvp-0.1.0.dist-info/METADATA +8 -0
- pymvp-0.1.0.dist-info/RECORD +7 -0
- pymvp-0.1.0.dist-info/WHEEL +5 -0
- pymvp-0.1.0.dist-info/top_level.txt +1 -0
PyMVP/main.py
ADDED
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
##########################################################################
|
|
2
|
+
# PyMVP/main.py
|
|
3
|
+
# Author: Maximilien Wemaere (LMD/CNRS)
|
|
4
|
+
# Date: March 2026
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
# Simple routines to load, analyze and correct data from a Moving Vessel Profiler (MVP) 300
|
|
8
|
+
# Requires numpy, matplotlib, gsw, seabird, tqdm, cartopy
|
|
9
|
+
#
|
|
10
|
+
#
|
|
11
|
+
# Routines to read mvp data are adapted from routines provided by Pierre l'Hegaret (UBO)
|
|
12
|
+
# (mvp_routines.py, temporal_lag_correction.py, thermal_mass_correction.py)
|
|
13
|
+
#
|
|
14
|
+
#
|
|
15
|
+
# STILL IN DEVELOPMENT !
|
|
16
|
+
#
|
|
17
|
+
#
|
|
18
|
+
##########################################################################
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from math import e
|
|
24
|
+
import numpy as np
|
|
25
|
+
import glob
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
import matplotlib.pyplot as plt
|
|
28
|
+
import os
|
|
29
|
+
import gsw
|
|
30
|
+
from seabird.cnv import fCNV
|
|
31
|
+
from tqdm import tqdm
|
|
32
|
+
import cartopy.crs as ccrs
|
|
33
|
+
import cartopy.feature as cfeature
|
|
34
|
+
import xarray as xr
|
|
35
|
+
from . import mvp_routines as mvp
|
|
36
|
+
from scipy.ndimage import median_filter
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Analyzer:
|
|
40
|
+
def __init__(self, data_path, output_path=None, subdirs=False, Yorig=1950):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the analyzer with the data path and reference year.
|
|
43
|
+
Args:
|
|
44
|
+
data_path (str): Path to the folder containing MVP files.
|
|
45
|
+
subdirs (bool): Whether to search in subdirectories for MVP files (default False).
|
|
46
|
+
Yorig (int): Reference year for dates (default 1950).
|
|
47
|
+
"""
|
|
48
|
+
self.Yorig = Yorig
|
|
49
|
+
self.date_ref = datetime(Yorig, 1, 1)
|
|
50
|
+
self.data_path = data_path
|
|
51
|
+
self.output_path = output_path if output_path is not None else data_path
|
|
52
|
+
self.subdirs = subdirs
|
|
53
|
+
self.mvp = False
|
|
54
|
+
self.ctd = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_mvp_data(self,delp=[],data_path=None,format='raw',only_new=False):
|
|
58
|
+
"""
|
|
59
|
+
Load MVP data from .raw and .log files in the data_path folder.
|
|
60
|
+
Fills the object attributes with data matrices and associated metadata.
|
|
61
|
+
Args:
|
|
62
|
+
delp (list): Indices of profiles to remove from the list (optional).
|
|
63
|
+
data_path (str): Path to the folder containing MVP files (optional).
|
|
64
|
+
"""
|
|
65
|
+
if data_path is not None:
|
|
66
|
+
self.data_path = data_path
|
|
67
|
+
|
|
68
|
+
if format=='raw':
|
|
69
|
+
if self.subdirs:
|
|
70
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + '**/*.raw', recursive=True)))
|
|
71
|
+
else:
|
|
72
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + '*.raw', recursive=self.subdirs)))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if only_new:
|
|
76
|
+
list_output = [f for f in os.listdir(self.output_path) if f.endswith(".nc")]
|
|
77
|
+
files = [f for f in files if not "MVP_"+os.path.basename(f).replace('.raw', '.nc') in list_output]
|
|
78
|
+
|
|
79
|
+
elif format=='ncdf':
|
|
80
|
+
if self.subdirs:
|
|
81
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + '**/MVP*.nc', recursive=True)))
|
|
82
|
+
else:
|
|
83
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + 'MVP*.nc', recursive=self.subdirs)))
|
|
84
|
+
|
|
85
|
+
if only_new:
|
|
86
|
+
list_output = [f for f in os.listdir(self.output_path) if f.endswith(".nc")]
|
|
87
|
+
files = [f for f in files if not "MVP_"+os.path.basename(f) in list_output]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
print('Found ' + str(len(files)) + ' MVP files in the directory: ' + self.data_path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if format=='ncdf':
|
|
95
|
+
|
|
96
|
+
for f in files:
|
|
97
|
+
nc = xr.open_dataset(f)
|
|
98
|
+
self.PRES_mvp = nc['PRES'].values
|
|
99
|
+
self.TEMP_mvp = nc['TEMP'].values
|
|
100
|
+
self.COND_mvp = nc['COND'].values
|
|
101
|
+
self.SOUNDVEL_mvp = nc['SOUNDVEL'].values
|
|
102
|
+
self.DO_mvp = nc['DO'].values
|
|
103
|
+
self.TEMP2_mvp = nc['TEMP2'].values
|
|
104
|
+
self.SUNA_mvp = nc['SUNA'].values
|
|
105
|
+
self.FLUO_mvp = nc['FLUO'].values
|
|
106
|
+
self.TURB_mvp = nc['TURB'].values
|
|
107
|
+
self.PH_mvp = nc['PH'].values
|
|
108
|
+
self.SALT_mvp = nc['SAL'].values
|
|
109
|
+
self.TIME_mvp = nc['TIME_s'].values
|
|
110
|
+
self.LAT_mvp = nc['LATITUDE'].values
|
|
111
|
+
self.LON_mvp = nc['LONGITUDE'].values
|
|
112
|
+
self.DATETIME_mvp = nc['profile_time'].values
|
|
113
|
+
self.DIR = nc['direction'].values
|
|
114
|
+
self.label_mvp = nc['profile'].values
|
|
115
|
+
self.freq_echant = nc.attrs['sampling frequency_hz']
|
|
116
|
+
|
|
117
|
+
nc.close()
|
|
118
|
+
print('MVP data loaded successfully.')
|
|
119
|
+
self.mvp = True
|
|
120
|
+
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
PRES_temp = []
|
|
124
|
+
TEMP_temp = []
|
|
125
|
+
COND_temp = []
|
|
126
|
+
SOUNDVEL_temp = []
|
|
127
|
+
DO_temp = []
|
|
128
|
+
TEMP2_temp = [] # temp from DO sensor
|
|
129
|
+
SUNA_temp = []
|
|
130
|
+
FLUO_temp = []
|
|
131
|
+
TURB_temp = []
|
|
132
|
+
PH_temp = []
|
|
133
|
+
SALT_temp = []
|
|
134
|
+
TIME_mvp_temp = []
|
|
135
|
+
LAT_temp = []
|
|
136
|
+
LON_temp= []
|
|
137
|
+
DATETIME_mvp = []
|
|
138
|
+
DIR = []
|
|
139
|
+
Label_mvp = []
|
|
140
|
+
|
|
141
|
+
delp.sort(reverse=True)
|
|
142
|
+
for i in delp:
|
|
143
|
+
del files[i]
|
|
144
|
+
|
|
145
|
+
for mvp_dat_name in files[0:]:
|
|
146
|
+
|
|
147
|
+
mvp_log_name=mvp_dat_name[:-4]+'.log'
|
|
148
|
+
|
|
149
|
+
# Get start and end time of the cycle
|
|
150
|
+
|
|
151
|
+
if format=='raw':
|
|
152
|
+
(mvp_tstart,mvp_tend,cycle_dur, lat, lon, dt_station) = mvp.get_log(mvp_log_name,self.Yorig)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if cycle_dur>1:
|
|
156
|
+
|
|
157
|
+
# Read one cycle MVP data
|
|
158
|
+
(pres,soundvel,cond,temp,do_raw,temp2_raw,suna_raw,fluo_raw,turb_raw,ph_raw) = mvp.read_mvp_cycle_raw(mvp_dat_name)
|
|
159
|
+
(pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph) = mvp.raw_data_conversion(pres,soundvel,cond,temp,do_raw,temp2_raw,suna_raw,fluo_raw,turb_raw,ph_raw)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
freq_echant = float(len(pres)/cycle_dur)
|
|
163
|
+
|
|
164
|
+
DATETIME_mvp.append(dt_station)
|
|
165
|
+
|
|
166
|
+
if np.nanmax(pres)-np.nanmin(pres)>2:
|
|
167
|
+
|
|
168
|
+
# Allocate time to samples and select the ascending part
|
|
169
|
+
(pres_up,soundvel_up,cond_up,temp_up,do_up,temp2_up,suna_up,fluo_up,turb_up,ph_up,time_up) = mvp.time_mvp_cycle_up([pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph],mvp_tstart,mvp_tend)
|
|
170
|
+
(pres_down,soundvel_down,cond_down,temp_down,do_down,temp2_down,suna_down,fluo_down,turb_down,ph_down,time_down) = mvp.time_mvp_cycle_down([pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph],mvp_tstart,mvp_tend)
|
|
171
|
+
|
|
172
|
+
if len(pres_down)>0:
|
|
173
|
+
if np.nanmax(pres_down)-np.nanmin(pres_down)>2:
|
|
174
|
+
PRES_temp.append(pres_down)
|
|
175
|
+
SOUNDVEL_temp.append(soundvel_down)
|
|
176
|
+
COND_temp.append(cond_down)
|
|
177
|
+
TEMP_temp.append(temp_down)
|
|
178
|
+
DO_temp.append(do_down)
|
|
179
|
+
TEMP2_temp.append(temp2_down)
|
|
180
|
+
SUNA_temp.append(suna_down)
|
|
181
|
+
FLUO_temp.append(fluo_down)
|
|
182
|
+
TURB_temp.append(turb_down)
|
|
183
|
+
PH_temp.append(ph_down)
|
|
184
|
+
SALT_temp.append(gsw.SP_from_C(cond_down, temp_down,pres_down))
|
|
185
|
+
TIME_mvp_temp.append(time_down)
|
|
186
|
+
LAT_temp.append(lat)
|
|
187
|
+
LON_temp.append(lon)
|
|
188
|
+
|
|
189
|
+
DIR.append('down')
|
|
190
|
+
Label_mvp.append(mvp_dat_name.replace('\\','/').split('/')[-2])
|
|
191
|
+
|
|
192
|
+
else:
|
|
193
|
+
print('ohohoh no down profile found for file: ' + mvp_dat_name)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if len(pres_up)>0:
|
|
197
|
+
if np.nanmax(pres_up)-np.nanmin(pres_up)>2:
|
|
198
|
+
PRES_temp.append(pres_up)
|
|
199
|
+
SOUNDVEL_temp.append(soundvel_up)
|
|
200
|
+
COND_temp.append(cond_up)
|
|
201
|
+
TEMP_temp.append(temp_up)
|
|
202
|
+
DO_temp.append(do_up)
|
|
203
|
+
TEMP2_temp.append(temp2_up)
|
|
204
|
+
SUNA_temp.append(suna_up)
|
|
205
|
+
FLUO_temp.append(fluo_up)
|
|
206
|
+
TURB_temp.append(turb_up)
|
|
207
|
+
PH_temp.append(ph_up)
|
|
208
|
+
SALT_temp.append(gsw.SP_from_C(cond_up, temp_up,pres_up))
|
|
209
|
+
TIME_mvp_temp.append(time_up)
|
|
210
|
+
LAT_temp.append(lat)
|
|
211
|
+
LON_temp.append(lon)
|
|
212
|
+
DIR.append('up')
|
|
213
|
+
Label_mvp.append(mvp_dat_name.replace('\\','/').split('/')[-2])
|
|
214
|
+
|
|
215
|
+
else:
|
|
216
|
+
print('ohohoh no up profile found for file: ' + mvp_dat_name)
|
|
217
|
+
|
|
218
|
+
else:
|
|
219
|
+
print('ohohoh no profile found for file: ' + mvp_dat_name)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Re-arange files into matrices
|
|
225
|
+
M_size = 0
|
|
226
|
+
for i in range(len(PRES_temp)):
|
|
227
|
+
M_size = max(M_size, len(PRES_temp[i]))
|
|
228
|
+
|
|
229
|
+
PRES_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
230
|
+
SOUNDVEL_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
231
|
+
COND_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
232
|
+
TEMP_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
233
|
+
DO_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
234
|
+
TEMP_mvp2 = np.zeros(( len(PRES_temp), M_size))
|
|
235
|
+
SUNA_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
236
|
+
FLUO_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
237
|
+
TURB_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
238
|
+
PH_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
239
|
+
SALT_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
240
|
+
TIME_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
241
|
+
LAT_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
242
|
+
LON_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
243
|
+
PRES_mvp[:] = np.nan
|
|
244
|
+
SOUNDVEL_mvp[:] = np.nan
|
|
245
|
+
COND_mvp[:] = np.nan
|
|
246
|
+
TEMP_mvp[:] = np.nan
|
|
247
|
+
DO_mvp[:] = np.nan
|
|
248
|
+
TEMP_mvp2[:] = np.nan
|
|
249
|
+
SUNA_mvp[:] = np.nan
|
|
250
|
+
FLUO_mvp[:] = np.nan
|
|
251
|
+
TURB_mvp[:] = np.nan
|
|
252
|
+
PH_mvp[:] = np.nan
|
|
253
|
+
SALT_mvp[:] = np.nan
|
|
254
|
+
TIME_mvp[:] = np.nan
|
|
255
|
+
LAT_mvp[:] = np.nan
|
|
256
|
+
LON_mvp[:] = np.nan
|
|
257
|
+
|
|
258
|
+
del M_size
|
|
259
|
+
|
|
260
|
+
for i in range(len(PRES_temp)):
|
|
261
|
+
PRES_mvp[i,0:len(PRES_temp[i])] = PRES_temp[i]
|
|
262
|
+
SOUNDVEL_mvp[i,0:len(SOUNDVEL_temp[i])] = SOUNDVEL_temp[i]
|
|
263
|
+
COND_mvp[i,0:len(COND_temp[i])] = COND_temp[i]
|
|
264
|
+
TEMP_mvp[i,0:len(TEMP_temp[i])] = TEMP_temp[i]
|
|
265
|
+
DO_mvp[i,0:len(DO_temp[i])] = DO_temp[i]
|
|
266
|
+
TEMP_mvp2[i,0:len(TEMP2_temp[i])] = TEMP2_temp[i]
|
|
267
|
+
SUNA_mvp[i,0:len(SUNA_temp[i])] = SUNA_temp[i]
|
|
268
|
+
FLUO_mvp[i,0:len(FLUO_temp[i])] = FLUO_temp[i]
|
|
269
|
+
TURB_mvp[i,0:len(TURB_temp[i])] = TURB_temp[i]
|
|
270
|
+
PH_mvp[i,0:len(PH_temp[i])] = PH_temp[i]
|
|
271
|
+
SALT_mvp[i,0:len(SALT_temp[i])] = SALT_temp[i]
|
|
272
|
+
TIME_mvp[i,0:len(TIME_mvp_temp[i])] = TIME_mvp_temp[i]
|
|
273
|
+
LAT_mvp[i,0:len(PRES_temp[i])] = LAT_temp[i]
|
|
274
|
+
LON_mvp[i,0:len(PRES_temp[i])] = LON_temp[i]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
self.PRES_mvp = PRES_mvp
|
|
278
|
+
self.SOUNDVEL_mvp = SOUNDVEL_mvp
|
|
279
|
+
self.COND_mvp = COND_mvp
|
|
280
|
+
self.TEMP_mvp = TEMP_mvp
|
|
281
|
+
self.DO_mvp = DO_mvp
|
|
282
|
+
self.TEMP2_mvp = TEMP_mvp2
|
|
283
|
+
self.SUNA_mvp = SUNA_mvp
|
|
284
|
+
self.FLUO_mvp = FLUO_mvp
|
|
285
|
+
self.TURB_mvp = TURB_mvp
|
|
286
|
+
self.PH_mvp = PH_mvp
|
|
287
|
+
self.SALT_mvp = SALT_mvp
|
|
288
|
+
self.TIME_mvp = TIME_mvp
|
|
289
|
+
self.LAT_mvp = LAT_mvp
|
|
290
|
+
self.LON_mvp = LON_mvp
|
|
291
|
+
self.DATETIME_mvp = DATETIME_mvp
|
|
292
|
+
self.DIR = DIR
|
|
293
|
+
self.label_mvp = Label_mvp
|
|
294
|
+
self.freq_echant = freq_echant
|
|
295
|
+
|
|
296
|
+
del PRES_temp, SOUNDVEL_temp, DO_temp, TEMP2_temp, SUNA_temp, FLUO_temp, TURB_temp, PH_temp, COND_temp, TEMP_temp, SALT_temp, TIME_mvp_temp, LAT_temp, LON_temp
|
|
297
|
+
|
|
298
|
+
print('MVP data loaded successfully.')
|
|
299
|
+
self.mvp = True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def load_mvp_data_again(self,data_path=None,format='raw',delp=[]):
|
|
305
|
+
"""
|
|
306
|
+
Load MVP data from .raw and .log files in the data_path folder.
|
|
307
|
+
Fills the object attributes with data matrices and associated metadata.
|
|
308
|
+
Args:
|
|
309
|
+
data_path (str): Path to the folder containing MVP files.
|
|
310
|
+
delp (list): Indices of profiles to remove from the list (optional).
|
|
311
|
+
"""
|
|
312
|
+
if data_path is not None:
|
|
313
|
+
self.data_path = data_path
|
|
314
|
+
|
|
315
|
+
if format=='raw':
|
|
316
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + '*.raw', recursive=True)))
|
|
317
|
+
elif format=='ncdf':
|
|
318
|
+
files = sorted(filter(os.path.isfile,glob.glob(self.data_path + '**/MVP*.nc', recursive=True)))
|
|
319
|
+
print('Found ' + str(len(files)) + ' MVP files in the directory: ' + self.data_path)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
if format=='ncdf':
|
|
324
|
+
for f in files:
|
|
325
|
+
nc = xr.open_dataset(f)
|
|
326
|
+
self.PRES_mvp = nc['PRES'].values
|
|
327
|
+
self.TEMP_mvp = nc['TEMP'].values
|
|
328
|
+
self.COND_mvp = nc['COND'].values
|
|
329
|
+
self.SOUNDVEL_mvp = nc['SOUNDVEL'].values
|
|
330
|
+
self.DO_mvp = nc['DO'].values
|
|
331
|
+
self.TEMP2_mvp = nc['TEMP2'].values
|
|
332
|
+
self.SUNA_mvp = nc['SUNA'].values
|
|
333
|
+
self.FLUO_mvp = nc['FLUO'].values
|
|
334
|
+
self.TURB_mvp = nc['TURB'].values
|
|
335
|
+
self.PH_mvp = nc['PH'].values
|
|
336
|
+
self.SALT_mvp = nc['SAL'].values
|
|
337
|
+
self.TIME_mvp = nc['TIME'].values
|
|
338
|
+
self.LAT_mvp = nc['LATITUDE'].values
|
|
339
|
+
self.LON_mvp = nc['LONGITUDE'].values
|
|
340
|
+
self.DATETIME_mvp = nc['profile_time'].values
|
|
341
|
+
self.DIR = nc['direction'].values
|
|
342
|
+
self.Label_mvp = nc['profile'].values
|
|
343
|
+
self.freq_echant = nc.attrs['sampling frequency_hz']
|
|
344
|
+
|
|
345
|
+
nc.close()
|
|
346
|
+
print('MVP data loaded successfully.')
|
|
347
|
+
self.mvp = True
|
|
348
|
+
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
PRES_temp = []
|
|
355
|
+
TEMP_temp = []
|
|
356
|
+
COND_temp = []
|
|
357
|
+
SOUNDVEL_temp = []
|
|
358
|
+
DO_temp = []
|
|
359
|
+
TEMP2_temp = [] # temp from DO sensor
|
|
360
|
+
SUNA_temp = []
|
|
361
|
+
FLUO_temp = []
|
|
362
|
+
TURB_temp = []
|
|
363
|
+
PH_temp = []
|
|
364
|
+
SALT_temp = []
|
|
365
|
+
TIME_mvp_temp = []
|
|
366
|
+
LAT_temp = []
|
|
367
|
+
LON_temp= []
|
|
368
|
+
DATETIME_mvp = []
|
|
369
|
+
DIR = []
|
|
370
|
+
Label_mvp = []
|
|
371
|
+
|
|
372
|
+
delp.sort(reverse=True)
|
|
373
|
+
for i in delp:
|
|
374
|
+
del files[i]
|
|
375
|
+
|
|
376
|
+
for mvp_dat_name in files[0:]:
|
|
377
|
+
|
|
378
|
+
mvp_log_name=mvp_dat_name[:-4]+'.log'
|
|
379
|
+
|
|
380
|
+
# Get start and end time of the cycle
|
|
381
|
+
(mvp_tstart,mvp_tend,cycle_dur, lat, lon, dt_station) = mvp.get_log(mvp_log_name,self.Yorig)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if cycle_dur>1:
|
|
385
|
+
|
|
386
|
+
# Read one cycle MVP data
|
|
387
|
+
|
|
388
|
+
(pres,soundvel,cond,temp,do_raw,temp2_raw,suna_raw,fluo_raw,turb_raw,ph_raw) = mvp.read_mvp_cycle_raw(mvp_dat_name)
|
|
389
|
+
(pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph) = mvp.raw_data_conversion(pres,soundvel,cond,temp,do_raw,temp2_raw,suna_raw,fluo_raw,turb_raw,ph_raw)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
freq_echant = float(len(pres)/cycle_dur)
|
|
393
|
+
|
|
394
|
+
DATETIME_mvp.append(dt_station)
|
|
395
|
+
|
|
396
|
+
if np.nanmax(pres)-np.nanmin(pres)>2:
|
|
397
|
+
|
|
398
|
+
# Allocate time to samples and select the ascending part
|
|
399
|
+
(pres_up,soundvel_up,cond_up,temp_up,do_up,temp2_up,suna_up,fluo_up,turb_up,ph_up,time_up) = mvp.time_mvp_cycle_up([pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph],mvp_tstart,mvp_tend)
|
|
400
|
+
(pres_down,soundvel_down,cond_down,temp_down,do_down,temp2_down,suna_down,fluo_down,turb_down,ph_down,time_down) = mvp.time_mvp_cycle_down([pres,soundvel,cond,temp,do,temp2,suna,fluo,turb,ph],mvp_tstart,mvp_tend)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if len(pres_down)>0:
|
|
404
|
+
if np.nanmax(pres_down)-np.nanmin(pres_down)>2:
|
|
405
|
+
PRES_temp.append(pres_down)
|
|
406
|
+
SOUNDVEL_temp.append(soundvel_down)
|
|
407
|
+
COND_temp.append(cond_down)
|
|
408
|
+
TEMP_temp.append(temp_down)
|
|
409
|
+
DO_temp.append(do_down)
|
|
410
|
+
TEMP2_temp.append(temp2_down)
|
|
411
|
+
SUNA_temp.append(suna_down)
|
|
412
|
+
FLUO_temp.append(fluo_down)
|
|
413
|
+
TURB_temp.append(turb_down)
|
|
414
|
+
PH_temp.append(ph_down)
|
|
415
|
+
SALT_temp.append(gsw.SP_from_C(cond_down, temp_down,pres_down))
|
|
416
|
+
TIME_mvp_temp.append(time_down)
|
|
417
|
+
LAT_temp.append(lat)
|
|
418
|
+
LON_temp.append(lon)
|
|
419
|
+
|
|
420
|
+
DIR.append('down')
|
|
421
|
+
Label_mvp.append(mvp_dat_name.replace('\\','/').split('/')[-2])
|
|
422
|
+
|
|
423
|
+
else:
|
|
424
|
+
print('ohohoh no down profile found for file: ' + mvp_dat_name)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if len(pres_up)>0:
|
|
428
|
+
if np.nanmax(pres_up)-np.nanmin(pres_up)>2:
|
|
429
|
+
PRES_temp.append(pres_up)
|
|
430
|
+
SOUNDVEL_temp.append(soundvel_up)
|
|
431
|
+
COND_temp.append(cond_up)
|
|
432
|
+
TEMP_temp.append(temp_up)
|
|
433
|
+
DO_temp.append(do_up)
|
|
434
|
+
TEMP2_temp.append(temp2_up)
|
|
435
|
+
SUNA_temp.append(suna_up)
|
|
436
|
+
FLUO_temp.append(fluo_up)
|
|
437
|
+
TURB_temp.append(turb_up)
|
|
438
|
+
PH_temp.append(ph_up)
|
|
439
|
+
SALT_temp.append(gsw.SP_from_C(cond_up, temp_up,pres_up))
|
|
440
|
+
TIME_mvp_temp.append(time_up)
|
|
441
|
+
LAT_temp.append(lat)
|
|
442
|
+
LON_temp.append(lon)
|
|
443
|
+
|
|
444
|
+
DIR.append('up')
|
|
445
|
+
Label_mvp.append(mvp_dat_name.replace('\\','/').split('/')[-2])
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
else:
|
|
449
|
+
print('ohohoh no up profile found for file: ' + mvp_dat_name)
|
|
450
|
+
|
|
451
|
+
else:
|
|
452
|
+
print('ohohoh no profile found for file: ' + mvp_dat_name)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# Re-arange files into matrices
|
|
458
|
+
M_size = 0
|
|
459
|
+
for i in range(len(PRES_temp)):
|
|
460
|
+
M_size = max(M_size, len(PRES_temp[i]))
|
|
461
|
+
|
|
462
|
+
if M_size < self.PRES_mvp.shape[1]:
|
|
463
|
+
M_size = self.PRES_mvp.shape[1]
|
|
464
|
+
else:
|
|
465
|
+
nan_cols = np.full((self.PRES_mvp.shape[0], M_size - self.PRES_mvp.shape[1]), np.nan)
|
|
466
|
+
self.PRES_mvp = np.hstack((self.PRES_mvp, nan_cols))
|
|
467
|
+
self.SOUNDVEL_mvp = np.hstack((self.SOUNDVEL_mvp, nan_cols))
|
|
468
|
+
self.COND_mvp = np.hstack((self.COND_mvp, nan_cols))
|
|
469
|
+
self.TEMP_mvp = np.hstack((self.TEMP_mvp, nan_cols))
|
|
470
|
+
self.DO_mvp = np.hstack((self.DO_mvp, nan_cols))
|
|
471
|
+
self.TEMP2_mvp = np.hstack((self.TEMP2_mvp, nan_cols))
|
|
472
|
+
self.SUNA_mvp = np.hstack((self.SUNA_mvp, nan_cols))
|
|
473
|
+
self.FLUO_mvp = np.hstack((self.FLUO_mvp, nan_cols))
|
|
474
|
+
self.TURB_mvp = np.hstack((self.TURB_mvp, nan_cols))
|
|
475
|
+
self.PH_mvp = np.hstack((self.PH_mvp, nan_cols))
|
|
476
|
+
self.SALT_mvp = np.hstack((self.SALT_mvp, nan_cols))
|
|
477
|
+
self.TIME_mvp = np.hstack((self.TIME_mvp, nan_cols))
|
|
478
|
+
self.LAT_mvp = np.hstack((self.LAT_mvp, nan_cols))
|
|
479
|
+
self.LON_mvp = np.hstack((self.LON_mvp, nan_cols))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
PRES_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
485
|
+
SOUNDVEL_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
486
|
+
COND_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
487
|
+
TEMP_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
488
|
+
DO_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
489
|
+
TEMP_mvp2 = np.zeros(( len(PRES_temp), M_size))
|
|
490
|
+
SUNA_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
491
|
+
FLUO_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
492
|
+
TURB_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
493
|
+
PH_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
494
|
+
SALT_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
495
|
+
TIME_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
496
|
+
LAT_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
497
|
+
LON_mvp = np.zeros(( len(PRES_temp), M_size))
|
|
498
|
+
PRES_mvp[:] = np.nan
|
|
499
|
+
SOUNDVEL_mvp[:] = np.nan
|
|
500
|
+
COND_mvp[:] = np.nan
|
|
501
|
+
TEMP_mvp[:] = np.nan
|
|
502
|
+
DO_mvp[:] = np.nan
|
|
503
|
+
TEMP_mvp2[:] = np.nan
|
|
504
|
+
SUNA_mvp[:] = np.nan
|
|
505
|
+
FLUO_mvp[:] = np.nan
|
|
506
|
+
TURB_mvp[:] = np.nan
|
|
507
|
+
PH_mvp[:] = np.nan
|
|
508
|
+
SALT_mvp[:] = np.nan
|
|
509
|
+
TIME_mvp[:] = np.nan
|
|
510
|
+
LAT_mvp[:] = np.nan
|
|
511
|
+
LON_mvp[:] = np.nan
|
|
512
|
+
|
|
513
|
+
del M_size
|
|
514
|
+
|
|
515
|
+
for i in range(len(PRES_temp)):
|
|
516
|
+
PRES_mvp[i,0:len(PRES_temp[i])] = PRES_temp[i]
|
|
517
|
+
SOUNDVEL_mvp[i,0:len(SOUNDVEL_temp[i])] = SOUNDVEL_temp[i]
|
|
518
|
+
COND_mvp[i,0:len(COND_temp[i])] = COND_temp[i]
|
|
519
|
+
TEMP_mvp[i,0:len(TEMP_temp[i])] = TEMP_temp[i]
|
|
520
|
+
DO_mvp[i,0:len(DO_temp[i])] = DO_temp[i]
|
|
521
|
+
TEMP_mvp2[i,0:len(TEMP2_temp[i])] = TEMP2_temp[i]
|
|
522
|
+
SUNA_mvp[i,0:len(SUNA_temp[i])] = SUNA_temp[i]
|
|
523
|
+
FLUO_mvp[i,0:len(FLUO_temp[i])] = FLUO_temp[i]
|
|
524
|
+
TURB_mvp[i,0:len(TURB_temp[i])] = TURB_temp[i]
|
|
525
|
+
PH_mvp[i,0:len(PH_temp[i])] = PH_temp[i]
|
|
526
|
+
SALT_mvp[i,0:len(SALT_temp[i])] = SALT_temp[i]
|
|
527
|
+
TIME_mvp[i,0:len(TIME_mvp_temp[i])] = TIME_mvp_temp[i]
|
|
528
|
+
LAT_mvp[i,0:len(PRES_temp[i])] = LAT_temp[i]
|
|
529
|
+
LON_mvp[i,0:len(PRES_temp[i])] = LON_temp[i]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
self.PRES_mvp = np.concatenate((self.PRES_mvp, PRES_mvp), axis=0)
|
|
533
|
+
self.SOUNDVEL_mvp = np.concatenate((self.SOUNDVEL_mvp, SOUNDVEL_mvp), axis=0)
|
|
534
|
+
self.COND_mvp = np.concatenate((self.COND_mvp, COND_mvp), axis=0)
|
|
535
|
+
self.TEMP_mvp = np.concatenate((self.TEMP_mvp, TEMP_mvp), axis=0)
|
|
536
|
+
self.DO_mvp = np.concatenate((self.DO_mvp, DO_mvp), axis=0)
|
|
537
|
+
self.TEMP2_mvp = np.concatenate((self.TEMP2_mvp, TEMP_mvp2), axis=0)
|
|
538
|
+
self.SUNA_mvp = np.concatenate((self.SUNA_mvp, SUNA_mvp), axis=0)
|
|
539
|
+
self.FLUO_mvp = np.concatenate((self.FLUO_mvp, FLUO_mvp), axis=0)
|
|
540
|
+
self.TURB_mvp = np.concatenate((self.TURB_mvp, TURB_mvp), axis=0)
|
|
541
|
+
self.PH_mvp = np.concatenate((self.PH_mvp, PH_mvp), axis=0)
|
|
542
|
+
self.SALT_mvp = np.concatenate((self.SALT_mvp, SALT_mvp), axis=0)
|
|
543
|
+
self.TIME_mvp = np.concatenate((self.TIME_mvp, TIME_mvp), axis=0)
|
|
544
|
+
self.LAT_mvp = np.concatenate((self.LAT_mvp, LAT_mvp), axis=0)
|
|
545
|
+
self.LON_mvp = np.concatenate((self.LON_mvp, LON_mvp), axis=0)
|
|
546
|
+
|
|
547
|
+
self.DATETIME_mvp.extend(DATETIME_mvp)
|
|
548
|
+
self.DIR.extend(DIR)
|
|
549
|
+
self.label_mvp.extend(Label_mvp)
|
|
550
|
+
|
|
551
|
+
del PRES_temp, SOUNDVEL_temp, DO_temp, TEMP2_temp, SUNA_temp, FLUO_temp, TURB_temp, PH_temp, COND_temp, TEMP_temp, SALT_temp, TIME_mvp_temp, LAT_temp, LON_temp
|
|
552
|
+
|
|
553
|
+
print('MVP data loaded successfully.')
|
|
554
|
+
self.mvp = True
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def load_ctd_data(self,data_path_ctd,format='cnv'):
|
|
558
|
+
"""
|
|
559
|
+
Load CTD data from .cnv files in the data_path_ctd folder.
|
|
560
|
+
Fills the object attributes with data matrices and associated metadata.
|
|
561
|
+
Args:
|
|
562
|
+
data_path_ctd (str): Path to the folder containing CTD files.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
if format=='cnv':
|
|
567
|
+
list_of_ctd_files = sorted(filter(os.path.isfile,\
|
|
568
|
+
glob.glob(data_path_ctd + '*.cnv')))
|
|
569
|
+
elif format=='ncdf':
|
|
570
|
+
list_of_ctd_files = sorted(filter(os.path.isfile,\
|
|
571
|
+
glob.glob(data_path_ctd + 'CTD'+'*.nc')))
|
|
572
|
+
print('Found ' + str(len(list_of_ctd_files)) + ' CTD files in the directory: ' + data_path_ctd)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# keys: ['scan', 'timeJ', 'timeQ', 'LATITUDE', 'LONGITUDE', 'PRES', 'TEMP', 'CNDC', 'descentrate', 'flECO-AFL', 'v1', 'wetCDOM', 'v0', 'turbWETntu0', 'v5', 'CStarTr0', 'CStarAt0', 'oxygen_ml_L', 'oxsolML/L', 'v2', 'flag', 'timeS']
|
|
579
|
+
LAT_ctd_temp = []
|
|
580
|
+
LON_ctd_temp = []
|
|
581
|
+
PRES_ctd_temp = []
|
|
582
|
+
TEMP_ctd_temp = []
|
|
583
|
+
COND_ctd_temp = []
|
|
584
|
+
TURB_ctd_temp = []
|
|
585
|
+
OXY_ctd_temp = []
|
|
586
|
+
FLUO_ctd_temp = []
|
|
587
|
+
CDOM_ctd_temp = []
|
|
588
|
+
DATETIME_ctd = []
|
|
589
|
+
SALT_ctd_temp = []
|
|
590
|
+
|
|
591
|
+
if format=='ncdf':
|
|
592
|
+
for f in list_of_ctd_files:
|
|
593
|
+
nc = xr.open_dataset(f)
|
|
594
|
+
PRES_ctd_temp.append(nc['PRES'].values[0])
|
|
595
|
+
PRES_ctd_temp.append(nc['PRES'].values[1])
|
|
596
|
+
TEMP_ctd_temp.append(nc['TEMP'].values[0])
|
|
597
|
+
TEMP_ctd_temp.append(nc['TEMP'].values[1])
|
|
598
|
+
COND_ctd_temp.append(nc['COND'].values[0])
|
|
599
|
+
COND_ctd_temp.append(nc['COND'].values[1])
|
|
600
|
+
SALT_ctd_temp.append(nc['SAL'].values[0])
|
|
601
|
+
SALT_ctd_temp.append(nc['SAL'].values[1])
|
|
602
|
+
TURB_ctd_temp.append(nc['TURB'].values[0])
|
|
603
|
+
TURB_ctd_temp.append(nc['TURB'].values[1])
|
|
604
|
+
OXY_ctd_temp.append(nc['OXY'].values[0])
|
|
605
|
+
OXY_ctd_temp.append(nc['OXY'].values[1])
|
|
606
|
+
FLUO_ctd_temp.append(nc['FLUO'].values[0])
|
|
607
|
+
FLUO_ctd_temp.append(nc['FLUO'].values[1])
|
|
608
|
+
CDOM_ctd_temp.append(nc['CDOM'].values[0])
|
|
609
|
+
CDOM_ctd_temp.append(nc['CDOM'].values[1])
|
|
610
|
+
LAT_ctd_temp.append(nc['LATITUDE'].values[0])
|
|
611
|
+
LAT_ctd_temp.append(nc['LATITUDE'].values[1])
|
|
612
|
+
LON_ctd_temp.append(nc['LONGITUDE'].values[0])
|
|
613
|
+
LON_ctd_temp.append(nc['LONGITUDE'].values[1])
|
|
614
|
+
DATETIME_ctd.append(nc['profile_time'].values[0])
|
|
615
|
+
|
|
616
|
+
nc.close()
|
|
617
|
+
|
|
618
|
+
self.PRES_ctd = np.array(PRES_ctd_temp)
|
|
619
|
+
self.TEMP_ctd = np.array(TEMP_ctd_temp)
|
|
620
|
+
self.COND_ctd = np.array(COND_ctd_temp)
|
|
621
|
+
self.SALT_ctd = np.array(SALT_ctd_temp)
|
|
622
|
+
self.TURB_ctd = np.array(TURB_ctd_temp)
|
|
623
|
+
self.OXY_ctd = np.array(OXY_ctd_temp)
|
|
624
|
+
self.FLUO_ctd = np.array(FLUO_ctd_temp)
|
|
625
|
+
self.CDOM_ctd = np.array(CDOM_ctd_temp)
|
|
626
|
+
self.LAT_ctd = np.array(LAT_ctd_temp)
|
|
627
|
+
self.LON_ctd = np.array(LON_ctd_temp)
|
|
628
|
+
self.DATETIME_ctd = np.array(DATETIME_ctd)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
print('CTD data loaded successfully.')
|
|
632
|
+
self.ctd = True
|
|
633
|
+
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
for ctd_dat_name in tqdm(list_of_ctd_files[0:]):
|
|
642
|
+
ctd_files = ctd_dat_name
|
|
643
|
+
|
|
644
|
+
cnv = fCNV(ctd_files)
|
|
645
|
+
|
|
646
|
+
Lat_up,Lat_down = split_ctd(cnv['PRES'], cnv['LATITUDE'])
|
|
647
|
+
Lon_up,Lon_down = split_ctd(cnv['PRES'], cnv['LONGITUDE'])
|
|
648
|
+
Pres_up,Pres_down = split_ctd(cnv['PRES'], cnv['PRES'])
|
|
649
|
+
Temp_up,Temp_down = split_ctd(cnv['PRES'], cnv['TEMP'])
|
|
650
|
+
Cond_up,Cond_down = split_ctd(cnv['PRES'], cnv['CNDC']*10)
|
|
651
|
+
Turb_up,Turb_down = split_ctd(cnv['PRES'], cnv['turbWETntu0'])
|
|
652
|
+
Oxy_up,Oxy_down = split_ctd(cnv['PRES'],np.array([a/b*100 for a,b in zip(cnv['oxygen_ml_L'], cnv['oxsolML/L'])]))
|
|
653
|
+
Fluo_up,Fluo_down = split_ctd(cnv['PRES'], cnv['flECO-AFL'])
|
|
654
|
+
Cdom_up,Cdom_down = split_ctd(cnv['PRES'], cnv['wetCDOM'])
|
|
655
|
+
Salt_up,Salt_down = split_ctd(cnv['PRES'], gsw.SP_from_C(cnv['CNDC']*10, cnv['TEMP'], cnv['PRES']))
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
LAT_ctd_temp.append(Lat_down)
|
|
662
|
+
LAT_ctd_temp.append(Lat_up)
|
|
663
|
+
LON_ctd_temp.append(Lon_down)
|
|
664
|
+
LON_ctd_temp.append(Lon_up)
|
|
665
|
+
PRES_ctd_temp.append(Pres_down)
|
|
666
|
+
PRES_ctd_temp.append(Pres_up)
|
|
667
|
+
TEMP_ctd_temp.append(Temp_down)
|
|
668
|
+
TEMP_ctd_temp.append(Temp_up)
|
|
669
|
+
COND_ctd_temp.append(Cond_down)
|
|
670
|
+
COND_ctd_temp.append(Cond_up)
|
|
671
|
+
TURB_ctd_temp.append(Turb_down)
|
|
672
|
+
TURB_ctd_temp.append(Turb_up)
|
|
673
|
+
OXY_ctd_temp.append(Oxy_down)
|
|
674
|
+
OXY_ctd_temp.append(Oxy_up)
|
|
675
|
+
FLUO_ctd_temp.append(Fluo_down)
|
|
676
|
+
FLUO_ctd_temp.append(Fluo_up)
|
|
677
|
+
CDOM_ctd_temp.append(Cdom_down)
|
|
678
|
+
CDOM_ctd_temp.append(Cdom_up)
|
|
679
|
+
SALT_ctd_temp.append(Salt_down)
|
|
680
|
+
SALT_ctd_temp.append(Salt_up)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
with open(ctd_dat_name, 'r') as f:
|
|
686
|
+
header_lines = []
|
|
687
|
+
for _ in range(10):
|
|
688
|
+
header_lines.append(f.readline().strip())
|
|
689
|
+
|
|
690
|
+
line = header_lines[9]
|
|
691
|
+
date_str = line.split('=')[1].strip()
|
|
692
|
+
dt = datetime.strptime(date_str, "%b %d %Y %H:%M:%S")
|
|
693
|
+
DATETIME_ctd.append(dt)
|
|
694
|
+
|
|
695
|
+
# Re-arange files into matrices
|
|
696
|
+
M_size = 0
|
|
697
|
+
for i in range(len(PRES_ctd_temp)):
|
|
698
|
+
M_size = max(M_size, len(PRES_ctd_temp[i]))
|
|
699
|
+
|
|
700
|
+
PRES_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
701
|
+
COND_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
702
|
+
SALT_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
703
|
+
TEMP_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
704
|
+
TURB_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
705
|
+
OXY_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
706
|
+
FLUO_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
707
|
+
CDOM_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
708
|
+
LAT_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
709
|
+
LON_ctd = np.zeros(( len(PRES_ctd_temp), M_size))
|
|
710
|
+
PRES_ctd[:] = np.nan
|
|
711
|
+
COND_ctd[:] = np.nan
|
|
712
|
+
SALT_ctd[:] = np.nan
|
|
713
|
+
TEMP_ctd[:] = np.nan
|
|
714
|
+
TURB_ctd[:] = np.nan
|
|
715
|
+
OXY_ctd[:] = np.nan
|
|
716
|
+
FLUO_ctd[:] = np.nan
|
|
717
|
+
CDOM_ctd[:] = np.nan
|
|
718
|
+
LAT_ctd[:] = np.nan
|
|
719
|
+
LON_ctd[:] = np.nan
|
|
720
|
+
del M_size
|
|
721
|
+
for i in range(len(PRES_ctd_temp)):
|
|
722
|
+
LAT_ctd[i,0:len(PRES_ctd_temp[i])] = LAT_ctd_temp[i]
|
|
723
|
+
LON_ctd[i,0:len(PRES_ctd_temp[i])] = LON_ctd_temp[i]
|
|
724
|
+
PRES_ctd[i,0:len(PRES_ctd_temp[i])] = PRES_ctd_temp[i]
|
|
725
|
+
TEMP_ctd[i,0:len(PRES_ctd_temp[i])] = TEMP_ctd_temp[i]
|
|
726
|
+
COND_ctd[i,0:len(PRES_ctd_temp[i])] = COND_ctd_temp[i]
|
|
727
|
+
SALT_ctd[i,0:len(PRES_ctd_temp[i])] = SALT_ctd_temp[i]
|
|
728
|
+
TURB_ctd[i,0:len(PRES_ctd_temp[i])] = TURB_ctd_temp[i]
|
|
729
|
+
OXY_ctd[i,0:len(PRES_ctd_temp[i])] = OXY_ctd_temp[i]
|
|
730
|
+
FLUO_ctd[i,0:len(PRES_ctd_temp[i])] = FLUO_ctd_temp[i]
|
|
731
|
+
CDOM_ctd[i,0:len(PRES_ctd_temp[i])] = CDOM_ctd_temp[i]
|
|
732
|
+
del PRES_ctd_temp, TEMP_ctd_temp, COND_ctd_temp, SALT_ctd_temp, TURB_ctd_temp, OXY_ctd_temp, FLUO_ctd_temp, CDOM_ctd_temp, LAT_ctd_temp, LON_ctd_temp
|
|
733
|
+
|
|
734
|
+
self.PRES_ctd = PRES_ctd
|
|
735
|
+
self.TEMP_ctd = TEMP_ctd
|
|
736
|
+
self.COND_ctd = COND_ctd
|
|
737
|
+
self.SALT_ctd = SALT_ctd
|
|
738
|
+
self.TURB_ctd = TURB_ctd
|
|
739
|
+
self.OXY_ctd = OXY_ctd
|
|
740
|
+
self.FLUO_ctd = FLUO_ctd
|
|
741
|
+
self.CDOM_ctd = CDOM_ctd
|
|
742
|
+
self.LAT_ctd = LAT_ctd
|
|
743
|
+
self.LON_ctd = LON_ctd
|
|
744
|
+
self.DATETIME_ctd = DATETIME_ctd
|
|
745
|
+
|
|
746
|
+
print('CTD data loaded successfully.')
|
|
747
|
+
self.ctd = True
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def compute_waterflow(self,horizontal_speed=2,corr=False):
|
|
751
|
+
"""
|
|
752
|
+
Compute the water flow speed (u,v) from the horizontal speed and the direction of the profiles.
|
|
753
|
+
Args:
|
|
754
|
+
horizontal_speed (float): Horizontal speed of the boat in m/s.
|
|
755
|
+
"""
|
|
756
|
+
|
|
757
|
+
if corr:
|
|
758
|
+
SPEED_MVP = []
|
|
759
|
+
for i in range(len(self.PRES_mvp_corr)):
|
|
760
|
+
SPEED_MVP.append(np.sqrt(np.gradient(self.PRES_mvp_corr[i], 1/self.freq_echant)**2+ horizontal_speed**2))
|
|
761
|
+
self.SPEED_mvp_corr = {i: SPEED_MVP[i] for i in range(len(SPEED_MVP))}
|
|
762
|
+
else:
|
|
763
|
+
SPEED_MVP = np.zeros((self.PRES_mvp.shape[0], self.PRES_mvp.shape[1]))
|
|
764
|
+
for i in range(self.PRES_mvp.shape[0]):
|
|
765
|
+
SPEED_MVP[i,:] = np.sqrt(np.gradient(self.PRES_mvp[i,:], 1/self.freq_echant)**2+ horizontal_speed**2)
|
|
766
|
+
|
|
767
|
+
self.SPEED_mvp = SPEED_MVP
|
|
768
|
+
print('Water flow speed computed successfully.')
|
|
769
|
+
|
|
770
|
+
def print_profile_metadata(self):
|
|
771
|
+
"""
|
|
772
|
+
Print main metadata (date, position, number of samples) for each loaded MVP and CTD profile.
|
|
773
|
+
"""
|
|
774
|
+
|
|
775
|
+
if self.mvp:
|
|
776
|
+
print('MVP data:')
|
|
777
|
+
print('Number of profiles: ' + str(len(self.DATETIME_mvp)))
|
|
778
|
+
for i in range(0,len(self.DATETIME_mvp)):
|
|
779
|
+
print(f" Profil down {2*i} - Profil up {2*i+1} - Latitude: {self.LAT_mvp[2*i,0]:.5f}, Longitude: {self.LON_mvp[2*i,0]:.5f}, Date/Heure: {self.DATETIME_mvp[i]}")
|
|
780
|
+
|
|
781
|
+
if self.ctd:
|
|
782
|
+
print('CTD data:')
|
|
783
|
+
print('Number of profiles: ' + str(len(self.DATETIME_ctd)))
|
|
784
|
+
for i in range(0,len(self.DATETIME_ctd)):
|
|
785
|
+
print(f" Profil down {2*i} - Profil up {2*i+1} - Latitude: {self.LAT_ctd[2*i,0]:.5f}, Longitude: {self.LON_ctd[2*i,0]:.5f}, Date/Heure: {self.DATETIME_ctd[i]}")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def keep_selected_profiles(self, id_mvp, id_ctd=None):
|
|
789
|
+
"""
|
|
790
|
+
Keep only the selected MVP and CTD profiles in the object attributes.
|
|
791
|
+
Args:
|
|
792
|
+
id_mvp (list): Indices of MVP profiles to keep.
|
|
793
|
+
id_ctd (list): Indices of CTD profiles to keep (optional).
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# Make a list of all id to keep for MVP profiles
|
|
798
|
+
l_id = []
|
|
799
|
+
l_id2 = []
|
|
800
|
+
for i in id_mvp:
|
|
801
|
+
l_id.append(i)
|
|
802
|
+
l_id.append(i+1) # Add the next profile for the up profile
|
|
803
|
+
l_id2.append(i//2)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
# Keep only the selected profiles
|
|
808
|
+
|
|
809
|
+
if self.mvp:
|
|
810
|
+
|
|
811
|
+
self.PRES_mvp = self.PRES_mvp[l_id,:]
|
|
812
|
+
self.SOUNDVEL_mvp = self.SOUNDVEL_mvp[l_id,:]
|
|
813
|
+
self.COND_mvp = self.COND_mvp[l_id,:]
|
|
814
|
+
self.TEMP_mvp = self.TEMP_mvp[l_id,:]
|
|
815
|
+
self.DO_mvp = self.DO_mvp[l_id,:]
|
|
816
|
+
self.TEMP2_mvp = self.TEMP2_mvp[l_id,:]
|
|
817
|
+
self.SUNA_mvp = self.SUNA_mvp[l_id,:]
|
|
818
|
+
self.FLUO_mvp = self.FLUO_mvp[l_id,:]
|
|
819
|
+
self.TURB_mvp = self.TURB_mvp[l_id,:]
|
|
820
|
+
self.PH_mvp = self.PH_mvp[l_id,:]
|
|
821
|
+
self.SALT_mvp = self.SALT_mvp[l_id,:]
|
|
822
|
+
self.TIME_mvp = self.TIME_mvp[l_id,:]
|
|
823
|
+
self.LAT_mvp = self.LAT_mvp[l_id,:]
|
|
824
|
+
self.LON_mvp = self.LON_mvp[l_id,:]
|
|
825
|
+
self.DATETIME_mvp = np.array(self.DATETIME_mvp)[l_id2]
|
|
826
|
+
self.DIR = np.array(self.DIR)[l_id]
|
|
827
|
+
self.label_mvp = np.array(self.label_mvp)[l_id]
|
|
828
|
+
|
|
829
|
+
if self.ctd and id_ctd != None:
|
|
830
|
+
|
|
831
|
+
l_id = []
|
|
832
|
+
l_id2 = []
|
|
833
|
+
for i in id_ctd:
|
|
834
|
+
l_id.append(i)
|
|
835
|
+
l_id.append(i+1) # Add the next profile for the up profile
|
|
836
|
+
l_id2.append(i//2)
|
|
837
|
+
|
|
838
|
+
self.PRES_ctd = self.PRES_ctd[l_id,:]
|
|
839
|
+
self.TEMP_ctd = self.TEMP_ctd[l_id,:]
|
|
840
|
+
self.SALT_ctd = self.SALT_ctd[l_id,:]
|
|
841
|
+
self.COND_ctd = self.COND_ctd[l_id,:]
|
|
842
|
+
self.TURB_ctd = self.TURB_ctd[l_id,:]
|
|
843
|
+
self.OXY_ctd = self.OXY_ctd[l_id,:]
|
|
844
|
+
self.FLUO_ctd = self.FLUO_ctd[l_id,:]
|
|
845
|
+
self.CDOM_ctd = self.CDOM_ctd[l_id,:]
|
|
846
|
+
self.LAT_ctd = self.LAT_ctd[l_id,:]
|
|
847
|
+
self.LON_ctd = self.LON_ctd[l_id,:]
|
|
848
|
+
self.DATETIME_ctd = np.array(self.DATETIME_ctd)[l_id2]
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def plot_vertical_speed(self,id,mean=False,window=20):
|
|
852
|
+
|
|
853
|
+
if self.mvp==False:
|
|
854
|
+
print('No MVP data loaded.')
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
if mean:
|
|
858
|
+
v_z_down = np.gradient(self.PRES_mvp[0::2], 1/self.freq_echant,axis=1)
|
|
859
|
+
v_z_up = np.gradient(self.PRES_mvp[1::2], 1/self.freq_echant,axis=1)
|
|
860
|
+
|
|
861
|
+
# smooth speed
|
|
862
|
+
for i in range(v_z_down.shape[0]):
|
|
863
|
+
v_z_down[i,:] = np.convolve(v_z_down[i,:], np.ones(2*window+1)/(2*window+1), mode='same')
|
|
864
|
+
v_z_up[i,:] = np.convolve(v_z_up[i,:], np.ones(2*window+1)/(2*window+1), mode='same')
|
|
865
|
+
|
|
866
|
+
# take mean profile
|
|
867
|
+
v_z_down = np.nanmean(v_z_down,axis=0)
|
|
868
|
+
v_z_up = np.nanmean(v_z_up,axis=0)
|
|
869
|
+
|
|
870
|
+
self.v_z_down = v_z_down
|
|
871
|
+
self.v_z_up = v_z_up
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
else:
|
|
875
|
+
|
|
876
|
+
v_z_down = np.gradient(self.PRES_mvp[id,:], 1/self.freq_echant)
|
|
877
|
+
v_z_up = np.gradient(self.PRES_mvp[id+1,:], 1/self.freq_echant)
|
|
878
|
+
|
|
879
|
+
# smooth speed
|
|
880
|
+
self.v_z_down = np.convolve(v_z_down, np.ones(2*window+1)/(2*window+1), mode='same')
|
|
881
|
+
self.v_z_up = np.convolve(v_z_up, np.ones(2*window+1)/(2*window+1), mode='same')
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
plt.figure()
|
|
887
|
+
|
|
888
|
+
plt.plot(v_z_down,self.PRES_mvp[id], label='down')
|
|
889
|
+
plt.plot(v_z_up,self.PRES_mvp[id+1], label='up')
|
|
890
|
+
|
|
891
|
+
plt.gca().invert_yaxis()
|
|
892
|
+
plt.legend()
|
|
893
|
+
plt.grid()
|
|
894
|
+
plt.xlabel('Vertical speed, m/s')
|
|
895
|
+
plt.ylabel('Pressure, dbar')
|
|
896
|
+
plt.title('Vertical speed profiles')
|
|
897
|
+
plt.legend()
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def plot_profile_map(self):
|
|
901
|
+
"""
|
|
902
|
+
Plot a map of the start locations of each profile (MVP and CTD),
|
|
903
|
+
with a land/ocean background and coastlines using cartopy.
|
|
904
|
+
The map is automatically zoomed to the profile area (no excessive margin).
|
|
905
|
+
Requires the cartopy module (pip install cartopy).
|
|
906
|
+
"""
|
|
907
|
+
|
|
908
|
+
fig = plt.figure(figsize=(8, 8))
|
|
909
|
+
ax = plt.axes(projection=ccrs.PlateCarree())
|
|
910
|
+
ax.set_title('Carte des profils (début de plongée)')
|
|
911
|
+
ax.set_aspect('equal', adjustable='datalim')
|
|
912
|
+
ax.add_feature(cfeature.LAND, zorder=0, edgecolor='black', facecolor='lightgray')
|
|
913
|
+
ax.add_feature(cfeature.OCEAN, zorder=0, facecolor='lightblue')
|
|
914
|
+
ax.add_feature(cfeature.COASTLINE, linewidth=1.2)
|
|
915
|
+
ax.add_feature(cfeature.BORDERS, linestyle=':', linewidth=0.8)
|
|
916
|
+
gl = ax.gridlines(draw_labels=True, linewidth=0.5, color='gray', alpha=0.5, linestyle='--')
|
|
917
|
+
gl.top_labels = False
|
|
918
|
+
gl.right_labels = False
|
|
919
|
+
colors = plt.cm.tab10.colors
|
|
920
|
+
|
|
921
|
+
# MVP
|
|
922
|
+
if hasattr(self, 'LAT_mvp') and hasattr(self, 'LON_mvp'):
|
|
923
|
+
|
|
924
|
+
put_label = True
|
|
925
|
+
c = 0
|
|
926
|
+
for i in range(0,self.LAT_mvp.shape[0],2):
|
|
927
|
+
if i>0:
|
|
928
|
+
if self.label_mvp[i] == self.label_mvp[i-1]:
|
|
929
|
+
put_label = False
|
|
930
|
+
else:
|
|
931
|
+
put_label = True
|
|
932
|
+
c+=1
|
|
933
|
+
|
|
934
|
+
lat = self.LAT_mvp[i,0] if self.LAT_mvp.ndim == 2 else self.LAT_mvp[i]
|
|
935
|
+
lon = self.LON_mvp[i,0] if self.LON_mvp.ndim == 2 else self.LON_mvp[i]
|
|
936
|
+
ax.scatter(lon, lat, color=colors[c], marker='o', label='MVP '+self.label_mvp[i] if put_label else "", transform=ccrs.PlateCarree())
|
|
937
|
+
|
|
938
|
+
# CTD
|
|
939
|
+
if hasattr(self, 'LAT_ctd') and hasattr(self, 'LON_ctd'):
|
|
940
|
+
for i in range(0,self.LAT_ctd.shape[0],2):
|
|
941
|
+
lat = self.LAT_ctd[i,0] if self.LAT_ctd.ndim == 2 else self.LAT_ctd[i]
|
|
942
|
+
lon = self.LON_ctd[i,0] if self.LON_ctd.ndim == 2 else self.LON_ctd[i]
|
|
943
|
+
ax.scatter(lon, lat, color='red', marker='^', label='CTD' if i==0 else "", transform=ccrs.PlateCarree())
|
|
944
|
+
|
|
945
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
946
|
+
by_label = dict(zip(labels, handles))
|
|
947
|
+
ax.legend(by_label.values(), by_label.keys())
|
|
948
|
+
plt.show()
|
|
949
|
+
|
|
950
|
+
def plot_TSprofile(self, id_mvp,id_ctd=None,correction=False):
|
|
951
|
+
"""
|
|
952
|
+
Plot temperature and salinity profiles versus pressure for a given profile (MVP and CTD).
|
|
953
|
+
Args:
|
|
954
|
+
id_mvp (int): Index of the MVP profile to plot.
|
|
955
|
+
id_ctd (int, optional): Index of the CTD profile to plot (default: same as id_mvp).
|
|
956
|
+
correction (bool): If True, plot corrected profiles.
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
if id_ctd is None:
|
|
960
|
+
id_ctd = id_mvp
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
plt.figure()
|
|
965
|
+
if self.mvp:
|
|
966
|
+
if correction:
|
|
967
|
+
plt.plot(self.TEMP_mvp_corr[id_mvp],self.PRES_mvp_corr[id_mvp],label='MVP down corrected')
|
|
968
|
+
plt.plot(self.TEMP_mvp_corr[id_mvp+1],self.PRES_mvp_corr[id_mvp+1],label='MVP up corrected')
|
|
969
|
+
else:
|
|
970
|
+
plt.plot(self.TEMP_mvp[id_mvp],self.PRES_mvp[id_mvp],label='MVP down')
|
|
971
|
+
plt.plot(self.TEMP_mvp[id_mvp+1],self.PRES_mvp[id_mvp+1],label='MVP up')
|
|
972
|
+
if self.ctd:
|
|
973
|
+
plt.plot(self.TEMP_ctd[id_ctd],self.PRES_ctd[id_ctd],label='CTD down')
|
|
974
|
+
plt.plot(self.TEMP_ctd[id_ctd+1],self.PRES_ctd[id_ctd+1],label='CTD up')
|
|
975
|
+
plt.legend()
|
|
976
|
+
plt.gca().invert_yaxis()
|
|
977
|
+
plt.grid()
|
|
978
|
+
plt.xlabel('Temperature, C')
|
|
979
|
+
plt.ylabel('Pressure, dbar')
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
plt.figure()
|
|
983
|
+
if self.mvp:
|
|
984
|
+
if correction:
|
|
985
|
+
plt.plot(self.SALT_mvp_corr[id_mvp],self.PRES_mvp_corr[id_mvp],label='MVP down corrected')
|
|
986
|
+
plt.plot(self.SALT_mvp_corr[id_mvp+1],self.PRES_mvp_corr[id_mvp+1],label='MVP up corrected')
|
|
987
|
+
else:
|
|
988
|
+
plt.plot(self.SALT_mvp[id_mvp],self.PRES_mvp[id_mvp],label='MVP down')
|
|
989
|
+
plt.plot(self.SALT_mvp[id_mvp+1],self.PRES_mvp[id_mvp+1],label='MVP up')
|
|
990
|
+
if self.ctd:
|
|
991
|
+
plt.plot(self.SALT_ctd[id_ctd],self.PRES_ctd[id_ctd],label='CTD down')
|
|
992
|
+
plt.plot(self.SALT_ctd[id_ctd+1],self.PRES_ctd[id_ctd+1],label='CTD up')
|
|
993
|
+
plt.legend()
|
|
994
|
+
plt.gca().invert_yaxis()
|
|
995
|
+
plt.grid()
|
|
996
|
+
plt.xlabel('Salinity, psu')
|
|
997
|
+
plt.ylabel('Pressure, dbar')
|
|
998
|
+
|
|
999
|
+
def plot_BGCprofile(self, id_mvp,id_ctd=None,):
|
|
1000
|
+
"""
|
|
1001
|
+
Plot raw biogeochemical profiles (O2, turbidity, fluorescence) for a given profile (MVP and CTD).
|
|
1002
|
+
Args:
|
|
1003
|
+
id_mvp (int): Index of the MVP profile to plot.
|
|
1004
|
+
id_ctd (int, optional): Index of the CTD profile to plot (default: same as id_mvp).
|
|
1005
|
+
"""
|
|
1006
|
+
|
|
1007
|
+
if id_ctd is None:
|
|
1008
|
+
id_ctd = id_mvp
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
plt.figure()
|
|
1013
|
+
if self.mvp:
|
|
1014
|
+
plt.plot(self.DO_mvp[id_mvp],self.PRES_mvp[id_mvp],label='MVP down')
|
|
1015
|
+
plt.plot(self.DO_mvp[id_mvp+1],self.PRES_mvp[id_mvp+1],label='MVP up')
|
|
1016
|
+
if self.ctd:
|
|
1017
|
+
plt.plot(self.OXY_ctd[id_ctd],self.PRES_ctd[id_ctd],label='CTD down')
|
|
1018
|
+
plt.plot(self.OXY_ctd[id_ctd+1],self.PRES_ctd[id_ctd+1],label='CTD up')
|
|
1019
|
+
plt.legend()
|
|
1020
|
+
plt.gca().invert_yaxis()
|
|
1021
|
+
plt.grid()
|
|
1022
|
+
plt.xlabel('Dissolved Oxygen, %')
|
|
1023
|
+
plt.ylabel('Pressure, dbar')
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
plt.figure()
|
|
1027
|
+
if self.mvp:
|
|
1028
|
+
plt.plot(self.TURB_mvp[id_mvp],self.PRES_mvp[id_mvp],label='MVP down')
|
|
1029
|
+
plt.plot(self.TURB_mvp[id_mvp+1],self.PRES_mvp[id_mvp+1],label='MVP up')
|
|
1030
|
+
if self.ctd:
|
|
1031
|
+
plt.plot(self.TURB_ctd[id_ctd],self.PRES_ctd[id_ctd],label='CTD down')
|
|
1032
|
+
plt.plot(self.TURB_ctd[id_ctd+1],self.PRES_ctd[id_ctd+1],label='CTD up')
|
|
1033
|
+
plt.legend()
|
|
1034
|
+
plt.gca().invert_yaxis()
|
|
1035
|
+
plt.grid()
|
|
1036
|
+
plt.xlabel('Turbidity, NTU')
|
|
1037
|
+
plt.ylabel('Pressure, dbar')
|
|
1038
|
+
|
|
1039
|
+
plt.figure()
|
|
1040
|
+
if self.mvp:
|
|
1041
|
+
plt.plot(self.FLUO_mvp[id_mvp],self.PRES_mvp[id_mvp],label='MVP down')
|
|
1042
|
+
plt.plot(self.FLUO_mvp[id_mvp+1],self.PRES_mvp[id_mvp+1],label='MVP up')
|
|
1043
|
+
if self.ctd:
|
|
1044
|
+
plt.plot(self.FLUO_ctd[id_ctd],self.PRES_ctd[id_ctd],label='CTD down')
|
|
1045
|
+
plt.plot(self.FLUO_ctd[id_ctd+1],self.PRES_ctd[id_ctd+1],label='CTD up')
|
|
1046
|
+
plt.legend()
|
|
1047
|
+
plt.gca().invert_yaxis()
|
|
1048
|
+
plt.grid()
|
|
1049
|
+
plt.xlabel('Fluorescence, ug/L')
|
|
1050
|
+
plt.ylabel('Pressure, dbar')
|
|
1051
|
+
|
|
1052
|
+
def plot_diagramTS_raw(self,id_mvp=None,id_ctd=None,correction=False):
|
|
1053
|
+
"""
|
|
1054
|
+
Plot the TS diagram (Salinity vs Temperature) for one or more profiles, with isopycnals.
|
|
1055
|
+
Args:
|
|
1056
|
+
id_mvp (int, optional): Index of the MVP profile to plot, or None for all profiles.
|
|
1057
|
+
id_ctd (int, optional): Index of the CTD profile to plot, or None for all profiles.
|
|
1058
|
+
correction (bool): If True, plot corrected profiles.
|
|
1059
|
+
"""
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
plt.figure()
|
|
1065
|
+
if id_mvp != None:
|
|
1066
|
+
if id_ctd == None:
|
|
1067
|
+
id_ctd = id_mvp
|
|
1068
|
+
|
|
1069
|
+
if self.mvp:
|
|
1070
|
+
if correction:
|
|
1071
|
+
plt.plot(self.SALT_mvp_corr[id_mvp],self.TEMP_mvp_corr[id_mvp],label='MVP down corrected',linestyle='', marker='.')
|
|
1072
|
+
plt.plot(self.SALT_mvp_corr[id_mvp+1],self.TEMP_mvp_corr[id_mvp+1],label='MVP up corrected',linestyle='', marker='.')
|
|
1073
|
+
else:
|
|
1074
|
+
plt.plot(self.SALT_mvp[id_mvp],self.TEMP_mvp[id_mvp],label='MVP down',linestyle='', marker='.')
|
|
1075
|
+
plt.plot(self.SALT_mvp[id_mvp+1],self.TEMP_mvp[id_mvp+1],label='MVP up',linestyle='', marker='.')
|
|
1076
|
+
if self.ctd:
|
|
1077
|
+
plt.plot(self.SALT_ctd[id_ctd],self.TEMP_ctd[id_ctd],label='CTD down', linestyle='', marker='.')
|
|
1078
|
+
plt.plot(self.SALT_ctd[id_ctd+1],self.TEMP_ctd[id_ctd+1],label='CTD up', linestyle='', marker='.')
|
|
1079
|
+
|
|
1080
|
+
else:
|
|
1081
|
+
if self.mvp:
|
|
1082
|
+
if correction:
|
|
1083
|
+
plt.plot(self.SALT_mvp_corr[0],self.TEMP_mvp_corr[0],linestyle='',color='red', marker='.',label='MVP down corrected')
|
|
1084
|
+
plt.plot(self.SALT_mvp_corr[1],self.TEMP_mvp_corr[1],linestyle='',color='blue', marker='.',label='MVP up corrected')
|
|
1085
|
+
for i in range(2,len(self.PRES_mvp),2):
|
|
1086
|
+
plt.plot(self.SALT_mvp_corr[i],self.TEMP_mvp_corr[i],linestyle='',color='red', marker='.')
|
|
1087
|
+
plt.plot(self.SALT_mvp_corr[i+1],self.TEMP_mvp_corr[i+1],linestyle='',color='blue', marker='.')
|
|
1088
|
+
else:
|
|
1089
|
+
plt.plot(self.SALT_mvp[0],self.TEMP_mvp[0],linestyle='',color='red', marker='.',label='MVP down')
|
|
1090
|
+
plt.plot(self.SALT_mvp[1],self.TEMP_mvp[1],linestyle='',color='blue', marker='.',label='MVP up')
|
|
1091
|
+
for i in range(2,len(self.PRES_mvp),2):
|
|
1092
|
+
plt.plot(self.SALT_mvp[i],self.TEMP_mvp[i],linestyle='',color='red', marker='.')
|
|
1093
|
+
plt.plot(self.SALT_mvp[i+1],self.TEMP_mvp[i+1],linestyle='',color='blue', marker='.')
|
|
1094
|
+
if self.ctd:
|
|
1095
|
+
plt.plot(self.SALT_ctd[0],self.TEMP_ctd[0],color='green', linestyle='', marker='.',label='CTD down')
|
|
1096
|
+
plt.plot(self.SALT_ctd[1],self.TEMP_ctd[1],color='orange', linestyle='', marker='.',label='CTD up')
|
|
1097
|
+
for i in range(2,len(self.PRES_ctd),2):
|
|
1098
|
+
plt.plot(self.SALT_ctd[i],self.TEMP_ctd[i],color='green', linestyle='', marker='.')
|
|
1099
|
+
plt.plot(self.SALT_ctd[i+1],self.TEMP_ctd[i+1],color='orange', linestyle='', marker='.')
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
s_lim = plt.xlim()
|
|
1103
|
+
t_lim = plt.ylim()
|
|
1104
|
+
SA = np.linspace(s_lim[0], s_lim[1], 100) # Absolute Salinity [g/kg]
|
|
1105
|
+
CT = np.linspace(t_lim[0], t_lim[1], 100)
|
|
1106
|
+
SA_grid, CT_grid = np.meshgrid(SA, CT)
|
|
1107
|
+
# Calcul de la densité potentielle sigma0 (kg/m³ - 1000)
|
|
1108
|
+
sigma0 = gsw.sigma0(SA_grid, CT_grid)
|
|
1109
|
+
# Dessiner les contours (les isopycnes)
|
|
1110
|
+
contour_plot = plt.contour(SA_grid, CT_grid, sigma0, colors='k', linestyles='dotted')
|
|
1111
|
+
# Ajouter les étiquettes (les chiffres) le long des contours
|
|
1112
|
+
plt.clabel(contour_plot, inline=True, fontsize=10, fmt='%1.1f')
|
|
1113
|
+
|
|
1114
|
+
plt.legend()
|
|
1115
|
+
plt.xlabel('Salinity, psu')
|
|
1116
|
+
plt.ylabel('Temperature, C')
|
|
1117
|
+
|
|
1118
|
+
def stat_compar(self,id_mvp=[],id_ctd=None,num_sample=5000,cond=False,speed=False,correction=False):
|
|
1119
|
+
"""
|
|
1120
|
+
Statistically compare MVP and CTD profiles (temperature and salinity),
|
|
1121
|
+
print statistics and interpolated differences.
|
|
1122
|
+
Args:
|
|
1123
|
+
id (list): Indices of profiles to compare (all if empty).
|
|
1124
|
+
num_sample (int): Number of pressure levels for interpolation.
|
|
1125
|
+
"""
|
|
1126
|
+
|
|
1127
|
+
if not self.mvp or not self.ctd:
|
|
1128
|
+
raise ValueError("MVP or CTD data not loaded.")
|
|
1129
|
+
|
|
1130
|
+
if id_mvp == []:
|
|
1131
|
+
id_mvp = list(range(0, self.PRES_mvp.shape[0]))
|
|
1132
|
+
if id_ctd is None:
|
|
1133
|
+
id_ctd = id_mvp
|
|
1134
|
+
|
|
1135
|
+
if len(id_mvp) != len(id_ctd):
|
|
1136
|
+
raise ValueError("id_mvp and id_ctd must have the same length.")
|
|
1137
|
+
|
|
1138
|
+
if correction:
|
|
1139
|
+
Pres = self.PRES_mvp_corr
|
|
1140
|
+
Temp = self.TEMP_mvp_corr
|
|
1141
|
+
Salt = self.SALT_mvp_corr
|
|
1142
|
+
Cond = self.COND_mvp_corr
|
|
1143
|
+
else:
|
|
1144
|
+
Pres = self.PRES_mvp
|
|
1145
|
+
Temp = self.TEMP_mvp
|
|
1146
|
+
Salt = self.SALT_mvp
|
|
1147
|
+
Cond = self.COND_mvp
|
|
1148
|
+
Do = self.DO_mvp
|
|
1149
|
+
|
|
1150
|
+
# Interpolate MVP and CTD data to match pressure levels
|
|
1151
|
+
pmin = np.nanmin(Pres)
|
|
1152
|
+
pmax = np.nanmax(Pres)
|
|
1153
|
+
pressure_grid = np.linspace(pmin, pmax, num_sample)
|
|
1154
|
+
|
|
1155
|
+
TEMP_mvp_interp = mvp.vertical_interp(Pres[id_mvp,:],Temp[id_mvp,:], pressure_grid)
|
|
1156
|
+
SALT_mvp_interp = mvp.vertical_interp(Pres[id_mvp,:], Salt[id_mvp,:], pressure_grid)
|
|
1157
|
+
DO_mvp_interp = mvp.vertical_interp(Pres[id_mvp,:], Do[id_mvp,:], pressure_grid)
|
|
1158
|
+
COND_mvp_interp = mvp.vertical_interp(Pres[id_mvp,:], Cond[id_mvp,:], pressure_grid)
|
|
1159
|
+
|
|
1160
|
+
# keep only down profiles
|
|
1161
|
+
id_ctd1 = [id_ctd[i] for i in range(len(id_ctd)) if id_ctd[i]%2 == 0]
|
|
1162
|
+
|
|
1163
|
+
TEMP_ctd_interp = mvp.vertical_interp(self.PRES_ctd[id_ctd1,:],self.TEMP_ctd[id_ctd1,:], pressure_grid)
|
|
1164
|
+
SALT_ctd_interp = mvp.vertical_interp(self.PRES_ctd[id_ctd1,:],self.SALT_ctd[id_ctd1,:], pressure_grid)
|
|
1165
|
+
DO_ctd_interp = mvp.vertical_interp(self.PRES_ctd[id_ctd1,:],self.OXY_ctd[id_ctd1,:], pressure_grid)
|
|
1166
|
+
COND_ctd_interp = mvp.vertical_interp(self.PRES_ctd[id_ctd1,:],self.COND_ctd[id_ctd1,:], pressure_grid)
|
|
1167
|
+
|
|
1168
|
+
# differences study between MVP down and CTD profiles
|
|
1169
|
+
|
|
1170
|
+
# Calcul des différences entre les profils interpolés (MVP - CTD)
|
|
1171
|
+
diff_temp_down = TEMP_mvp_interp[0::2] - TEMP_ctd_interp
|
|
1172
|
+
diff_temp_up = TEMP_mvp_interp[1::2] - TEMP_ctd_interp
|
|
1173
|
+
diff_salt_down = SALT_mvp_interp[0::2] - SALT_ctd_interp
|
|
1174
|
+
diff_salt_up = SALT_mvp_interp[1::2] - SALT_ctd_interp
|
|
1175
|
+
diff_do_down = DO_mvp_interp[0::2] - DO_ctd_interp
|
|
1176
|
+
diff_do_up = DO_mvp_interp[1::2] - DO_ctd_interp
|
|
1177
|
+
diff_cond_down = COND_mvp_interp[0::2] - COND_ctd_interp
|
|
1178
|
+
diff_cond_up = COND_mvp_interp[1::2] - COND_ctd_interp
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
# Plot mean error vs depth for each variable (down/up)
|
|
1182
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
1183
|
+
|
|
1184
|
+
# Compute mean error along profiles (axis=0: profiles, axis=1: depth)
|
|
1185
|
+
mean_temp_down = np.absolute(np.nanmean(diff_temp_down, axis=0))
|
|
1186
|
+
mean_temp_up = np.absolute(np.nanmean(diff_temp_up, axis=0))
|
|
1187
|
+
mean_salt_down = np.absolute(np.nanmean(diff_salt_down, axis=0))
|
|
1188
|
+
mean_salt_up = np.absolute(np.nanmean(diff_salt_up, axis=0))
|
|
1189
|
+
mean_do_down = np.absolute(np.nanmean(diff_do_down, axis=0))
|
|
1190
|
+
mean_do_up = np.absolute(np.nanmean(diff_do_up, axis=0))
|
|
1191
|
+
mean_cond_down = np.absolute(np.nanmean(diff_cond_down, axis=0))
|
|
1192
|
+
mean_cond_up = np.absolute(np.nanmean(diff_cond_up, axis=0))
|
|
1193
|
+
|
|
1194
|
+
axes[0].plot(mean_temp_down, pressure_grid, label='Down')
|
|
1195
|
+
axes[0].plot(mean_temp_up, pressure_grid, label='Up')
|
|
1196
|
+
axes[0].invert_yaxis()
|
|
1197
|
+
axes[0].set_xlabel('Absolute Mean Error (°C)')
|
|
1198
|
+
axes[0].set_ylabel('Pressure (dbar)')
|
|
1199
|
+
axes[0].set_title('Temperature Error')
|
|
1200
|
+
axes[0].legend()
|
|
1201
|
+
axes[0].grid()
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
if cond:
|
|
1205
|
+
|
|
1206
|
+
axes[1].plot(mean_cond_down, pressure_grid, label='Down')
|
|
1207
|
+
axes[1].plot(mean_cond_up, pressure_grid, label='Up')
|
|
1208
|
+
axes[1].invert_yaxis()
|
|
1209
|
+
axes[1].set_xlabel('Absolute Mean Error (S/m)')
|
|
1210
|
+
axes[1].set_ylabel('Pressure (dbar)')
|
|
1211
|
+
axes[1].set_title('Conductivity Error')
|
|
1212
|
+
axes[1].legend()
|
|
1213
|
+
axes[1].grid()
|
|
1214
|
+
|
|
1215
|
+
else:
|
|
1216
|
+
|
|
1217
|
+
axes[1].plot(mean_salt_down, pressure_grid, label='Down')
|
|
1218
|
+
axes[1].plot(mean_salt_up, pressure_grid, label='Up')
|
|
1219
|
+
axes[1].invert_yaxis()
|
|
1220
|
+
axes[1].set_xlabel('Absolute Mean Error (psu)')
|
|
1221
|
+
axes[1].set_ylabel('Pressure (dbar)')
|
|
1222
|
+
axes[1].set_title('Salinity Error')
|
|
1223
|
+
axes[1].legend()
|
|
1224
|
+
axes[1].grid()
|
|
1225
|
+
|
|
1226
|
+
if speed:
|
|
1227
|
+
|
|
1228
|
+
axes[2].plot(self.v_z_down, self.PRES_mvp[0], label='Down')
|
|
1229
|
+
axes[2].plot(self.v_z_up, self.PRES_mvp[0], label='Up')
|
|
1230
|
+
axes[2].invert_yaxis()
|
|
1231
|
+
axes[2].set_xlabel('Vertical Speed (m/s)')
|
|
1232
|
+
axes[2].set_ylabel('Pressure (dbar)')
|
|
1233
|
+
axes[2].set_title('Vertical Speed')
|
|
1234
|
+
axes[2].legend()
|
|
1235
|
+
axes[2].grid()
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
else:
|
|
1239
|
+
|
|
1240
|
+
axes[2].plot(mean_do_down, pressure_grid, label='Down')
|
|
1241
|
+
axes[2].plot(mean_do_up, pressure_grid, label='Up')
|
|
1242
|
+
axes[2].invert_yaxis()
|
|
1243
|
+
axes[2].set_xlabel('Absolute Mean Error (%)')
|
|
1244
|
+
axes[2].set_ylabel('Pressure (dbar)')
|
|
1245
|
+
axes[2].set_title('Oxygen Error')
|
|
1246
|
+
axes[2].legend()
|
|
1247
|
+
axes[2].grid()
|
|
1248
|
+
|
|
1249
|
+
fig.suptitle('Absolute Mean Error (MVP - CTD) vs Depth')
|
|
1250
|
+
fig.tight_layout()
|
|
1251
|
+
plt.show()
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
# Compute RMSE
|
|
1255
|
+
|
|
1256
|
+
rmse_temp_down = np.mean(np.sqrt(np.nanmean(diff_temp_down**2, axis=1)))
|
|
1257
|
+
rmse_temp_up = np.mean(np.sqrt(np.nanmean(diff_temp_up**2, axis=1)))
|
|
1258
|
+
rmse_salt_down = np.mean(np.sqrt(np.nanmean(diff_salt_down**2, axis=1)))
|
|
1259
|
+
rmse_salt_up = np.mean(np.sqrt(np.nanmean(diff_salt_up**2, axis=1)))
|
|
1260
|
+
rmse_do_down = np.mean(np.sqrt(np.nanmean(diff_do_down**2, axis=1)))
|
|
1261
|
+
rmse_do_up = np.mean(np.sqrt(np.nanmean(diff_do_up**2, axis=1)))
|
|
1262
|
+
rmse_cond_down = np.mean(np.sqrt(np.nanmean(diff_cond_down**2, axis=1)))
|
|
1263
|
+
rmse_cond_up = np.mean(np.sqrt(np.nanmean(diff_cond_up**2, axis=1)))
|
|
1264
|
+
|
|
1265
|
+
# Find index where depth >= 200 dbar; fallback to 0 if not found
|
|
1266
|
+
i_200 = 0
|
|
1267
|
+
for i in range(len(pressure_grid)):
|
|
1268
|
+
if pressure_grid[i] >= 200:
|
|
1269
|
+
i_200 = i
|
|
1270
|
+
break
|
|
1271
|
+
|
|
1272
|
+
# Slice along depth axis (columns) to keep depths >= 200 dbar
|
|
1273
|
+
rmse_temp_down_deep = np.mean(np.sqrt(np.nanmean(diff_temp_down[:, i_200:]**2, axis=1)))
|
|
1274
|
+
rmse_temp_up_deep = np.mean(np.sqrt(np.nanmean(diff_temp_up[:, i_200:]**2, axis=1)))
|
|
1275
|
+
rmse_salt_down_deep = np.mean(np.sqrt(np.nanmean(diff_salt_down[:, i_200:]**2, axis=1)))
|
|
1276
|
+
rmse_salt_up_deep = np.mean(np.sqrt(np.nanmean(diff_salt_up[:, i_200:]**2, axis=1)))
|
|
1277
|
+
rmse_do_down_deep = np.mean(np.sqrt(np.nanmean(diff_do_down[:, i_200:]**2, axis=1)))
|
|
1278
|
+
rmse_do_up_deep = np.mean(np.sqrt(np.nanmean(diff_do_up[:, i_200:]**2, axis=1)))
|
|
1279
|
+
rmse_cond_down_deep = np.mean(np.sqrt(np.nanmean(diff_cond_down[:, i_200:]**2, axis=1)))
|
|
1280
|
+
rmse_cond_up_deep = np.mean(np.sqrt(np.nanmean(diff_cond_up[:, i_200:]**2, axis=1)))
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
# Print statistics + grouped deep RMSE
|
|
1284
|
+
|
|
1285
|
+
temp_rmse = [rmse_temp_down, rmse_temp_up]
|
|
1286
|
+
salt_rmse = [rmse_salt_down, rmse_salt_up]
|
|
1287
|
+
do_rmse = [rmse_do_down, rmse_do_up]
|
|
1288
|
+
|
|
1289
|
+
temp_rmse_deep = [rmse_temp_down_deep, rmse_temp_up_deep]
|
|
1290
|
+
salt_rmse_deep = [rmse_salt_down_deep, rmse_salt_up_deep]
|
|
1291
|
+
do_rmse_deep = [rmse_do_down_deep, rmse_do_up_deep]
|
|
1292
|
+
|
|
1293
|
+
labels = ['MVP down', 'MVP up']
|
|
1294
|
+
colors = ['blue', 'orange']
|
|
1295
|
+
|
|
1296
|
+
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
|
|
1297
|
+
|
|
1298
|
+
for idx, (ax, data, data_deep, title, ylabel) in enumerate(zip(
|
|
1299
|
+
axes,
|
|
1300
|
+
[temp_rmse, salt_rmse, do_rmse],
|
|
1301
|
+
[temp_rmse_deep, salt_rmse_deep, do_rmse_deep],
|
|
1302
|
+
['Temperature', 'Salinity', 'Oxygen'],
|
|
1303
|
+
['RMSE (°C)', 'RMSE (psu)', 'RMSE (%)']
|
|
1304
|
+
)):
|
|
1305
|
+
x = np.arange(len(labels))
|
|
1306
|
+
width = 0.35
|
|
1307
|
+
# Side-by-side grouped bars: left = All depths, right = Deep
|
|
1308
|
+
label_all = 'All depths' if idx == 0 else None
|
|
1309
|
+
label_deep = 'Deep (≥200 dbar)' if idx == 0 else None
|
|
1310
|
+
bars_all = ax.bar(x - width/2, data, width=width, color=colors, edgecolor='k', label=label_all)
|
|
1311
|
+
bars_deep = ax.bar(x + width/2, data_deep, width=width, color=colors, edgecolor='k', alpha=0.6, label=label_deep)
|
|
1312
|
+
|
|
1313
|
+
ax.set_xticks(x)
|
|
1314
|
+
ax.set_xticklabels(labels, rotation=20)
|
|
1315
|
+
ax.set_title(title)
|
|
1316
|
+
ax.set_ylabel(ylabel)
|
|
1317
|
+
ax.grid(axis='y', linestyle=':', alpha=0.5)
|
|
1318
|
+
ymax = max(max(data), max(data_deep)) * 1.25 # 25% margin above highest
|
|
1319
|
+
ax.set_ylim(0, ymax)
|
|
1320
|
+
|
|
1321
|
+
# Annotations
|
|
1322
|
+
for b in bars_all:
|
|
1323
|
+
h = b.get_height()
|
|
1324
|
+
if np.isfinite(h):
|
|
1325
|
+
ax.annotate(f'{h:.3f}', (b.get_x() + b.get_width()/2, h),
|
|
1326
|
+
xytext=(0, 3), textcoords='offset points',
|
|
1327
|
+
ha='center', va='bottom', fontsize=10, fontweight='bold')
|
|
1328
|
+
for b in bars_deep:
|
|
1329
|
+
h = b.get_height()
|
|
1330
|
+
if np.isfinite(h):
|
|
1331
|
+
ax.annotate(f'{h:.3f}', (b.get_x() + b.get_width()/2, h),
|
|
1332
|
+
xytext=(0, 3), textcoords='offset points',
|
|
1333
|
+
ha='center', va='bottom', fontsize=9)
|
|
1334
|
+
|
|
1335
|
+
if idx == 0:
|
|
1336
|
+
ax.legend()
|
|
1337
|
+
|
|
1338
|
+
fig.suptitle('RMSE MVP vs CTD')
|
|
1339
|
+
fig.tight_layout()
|
|
1340
|
+
plt.show()
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
if cond:
|
|
1344
|
+
print("Conductivity RMSE (MVP - CTD):")
|
|
1345
|
+
print(f" MVP down: {rmse_cond_down:.4f} S/m (deep: {rmse_cond_down_deep:.4f} S/m)")
|
|
1346
|
+
print(f" MVP up: {rmse_cond_up:.4f} S/m (deep: {rmse_cond_up_deep:.4f} S/m)")
|
|
1347
|
+
|
|
1348
|
+
def correct_oxygen(self,id_mvp=None,id_ctd=None,num_sample=500,plotting=False,correction=False):
|
|
1349
|
+
"""
|
|
1350
|
+
Apply oxygen correction to MVP dissolved oxygen profiles thanks to CTD data.
|
|
1351
|
+
Args:
|
|
1352
|
+
id_mvp (int): Index of the MVP profile to use for correction.
|
|
1353
|
+
id_ctd (int): Index of the CTD profile to use for correction.
|
|
1354
|
+
num_sample (int): Number of pressure levels for interpolation.
|
|
1355
|
+
plotting (bool): If True, plot the correction results.
|
|
1356
|
+
correction (bool): If True, update corrected attributes.
|
|
1357
|
+
"""
|
|
1358
|
+
|
|
1359
|
+
if not self.mvp or not self.ctd:
|
|
1360
|
+
raise ValueError("MVP or CTD data not loaded.")
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
if id_mvp is None:
|
|
1364
|
+
id_mvp,id_ctd = 0,0
|
|
1365
|
+
print(f"No profile index provided, using first profiles: MVP {id_mvp} and CTD {id_ctd}.")
|
|
1366
|
+
elif id_ctd is None:
|
|
1367
|
+
id_ctd = id_mvp
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
# Interpolate MVP and CTD data to match pressure levels
|
|
1371
|
+
pmin = np.nanmin(self.PRES_mvp)
|
|
1372
|
+
pmax = np.nanmax(self.PRES_mvp)
|
|
1373
|
+
pressure_grid = np.linspace(pmin, pmax, num_sample)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
DO_mvp_interp = mvp.vertical_interp(self.PRES_mvp[id_mvp,:], self.DO_mvp[id_mvp,:], pressure_grid)
|
|
1377
|
+
DO_ctd_interp = mvp.vertical_interp(self.PRES_ctd[id_ctd,:],self.OXY_ctd[id_ctd,:], pressure_grid)
|
|
1378
|
+
|
|
1379
|
+
mask = ~np.isnan(DO_mvp_interp) & ~np.isnan(DO_ctd_interp)
|
|
1380
|
+
pressure_grid = pressure_grid[mask[0]]
|
|
1381
|
+
DO_mvp_interp = DO_mvp_interp[mask]
|
|
1382
|
+
DO_ctd_interp = DO_ctd_interp[mask]
|
|
1383
|
+
|
|
1384
|
+
diff = DO_mvp_interp-DO_ctd_interp
|
|
1385
|
+
|
|
1386
|
+
A = np.vstack([pressure_grid, np.ones_like(pressure_grid)]).T
|
|
1387
|
+
print(A.shape, diff.shape)
|
|
1388
|
+
diff = diff.flatten()
|
|
1389
|
+
a_estime, b_estime = np.linalg.lstsq(A, diff, rcond=None)[0]
|
|
1390
|
+
|
|
1391
|
+
print(f"Pente estimée (a) : {a_estime:.6f} ")
|
|
1392
|
+
print(f"Biais estimé (b) : {b_estime:.6f} ")
|
|
1393
|
+
|
|
1394
|
+
DO_mvp_corr = DO_mvp_interp - (a_estime*pressure_grid + b_estime)
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
rmse_before = np.sqrt(np.nanmean((DO_mvp_interp - DO_ctd_interp)**2))
|
|
1399
|
+
rmse_after = np.sqrt(np.nanmean((DO_mvp_corr - DO_ctd_interp)**2))
|
|
1400
|
+
print(f"RMSE before correction: {rmse_before:.4f}")
|
|
1401
|
+
print(f"RMSE after correction: {rmse_after:.4f}")
|
|
1402
|
+
|
|
1403
|
+
DO_mvp_corr_full = self.DO_mvp - (a_estime*self.PRES_mvp + b_estime)
|
|
1404
|
+
|
|
1405
|
+
DO_mvp_corr_full_interp = mvp.vertical_interp(self.PRES_mvp, DO_mvp_corr_full, pressure_grid)
|
|
1406
|
+
rmse_after_full = np.mean(np.sqrt(np.nanmean((DO_mvp_corr_full_interp - DO_ctd_interp)**2,axis=1)))
|
|
1407
|
+
print(f"RMSE after correction (full profile): {rmse_after_full:.4f}")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
if correction:
|
|
1411
|
+
self.DO_mvp = DO_mvp_corr_full
|
|
1412
|
+
|
|
1413
|
+
if plotting:
|
|
1414
|
+
|
|
1415
|
+
plt.figure()
|
|
1416
|
+
plt.plot(DO_mvp_interp,pressure_grid,label='MVP')
|
|
1417
|
+
plt.plot(DO_ctd_interp,pressure_grid,label='CTD')
|
|
1418
|
+
plt.plot(DO_mvp_corr,pressure_grid,label='MVP corrected')
|
|
1419
|
+
plt.gca().invert_yaxis()
|
|
1420
|
+
plt.xlabel('Dissolved Oxygen, %')
|
|
1421
|
+
plt.ylabel('Pressure, dbar')
|
|
1422
|
+
plt.title('Oxygen correction')
|
|
1423
|
+
plt.legend()
|
|
1424
|
+
plt.grid()
|
|
1425
|
+
plt.show()
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
|
|
1429
|
+
def mvp_correction(self,high_cutoff=1,dp=0.1):
|
|
1430
|
+
|
|
1431
|
+
T_MVP_corr = []
|
|
1432
|
+
P_MVP_corr = []
|
|
1433
|
+
C_MVP_corr = []
|
|
1434
|
+
S_MVP_corr = []
|
|
1435
|
+
Time_MVP_corr = []
|
|
1436
|
+
|
|
1437
|
+
print("Applying corrections to MVP profiles...")
|
|
1438
|
+
|
|
1439
|
+
for id in tqdm(range(0,self.PRES_mvp.shape[0])):
|
|
1440
|
+
|
|
1441
|
+
T = self.TEMP_mvp[id]
|
|
1442
|
+
C = self.COND_mvp[id]
|
|
1443
|
+
P = self.PRES_mvp[id]
|
|
1444
|
+
S = self.SALT_mvp[id]
|
|
1445
|
+
Time = np.linspace(0,len(P)/self.freq_echant,len(P))
|
|
1446
|
+
|
|
1447
|
+
mask = ~np.isnan(C) & ~np.isnan(T)
|
|
1448
|
+
|
|
1449
|
+
C = C[mask]
|
|
1450
|
+
T = T[mask]
|
|
1451
|
+
Time = Time[mask]
|
|
1452
|
+
P = P[mask]
|
|
1453
|
+
S = S[mask]
|
|
1454
|
+
|
|
1455
|
+
T,C = mvp.filtering_tc(T,C,self.freq_echant,high_cutoff)
|
|
1456
|
+
T_corr,S_corr = mvp.temporal_lag(T,C,P,self.freq_echant)
|
|
1457
|
+
|
|
1458
|
+
if dp != None:
|
|
1459
|
+
P_ba,T_corr_ba,C_ba,S_corr_ba,Time_ba = mvp.bin_average_v2(P,T_corr,C,S_corr,Time,dp=0.1)
|
|
1460
|
+
|
|
1461
|
+
S_corr_medfilt = median_filter(S_corr_ba, size=5)
|
|
1462
|
+
|
|
1463
|
+
else:
|
|
1464
|
+
|
|
1465
|
+
T_corr_ba = T_corr
|
|
1466
|
+
C_ba = C
|
|
1467
|
+
S_corr_medfilt = S_corr
|
|
1468
|
+
P_ba = P
|
|
1469
|
+
Time_ba = Time
|
|
1470
|
+
|
|
1471
|
+
T_MVP_corr.append(T_corr_ba)
|
|
1472
|
+
P_MVP_corr.append(P_ba)
|
|
1473
|
+
C_MVP_corr.append(C_ba)
|
|
1474
|
+
S_MVP_corr.append(S_corr_medfilt)
|
|
1475
|
+
Time_MVP_corr.append(Time_ba)
|
|
1476
|
+
|
|
1477
|
+
self.TEMP_mvp_corr = {i: sublist for i, sublist in enumerate(T_MVP_corr)}
|
|
1478
|
+
self.PRES_mvp_corr = {i: sublist for i, sublist in enumerate(P_MVP_corr)}
|
|
1479
|
+
self.COND_mvp_corr = {i: sublist for i, sublist in enumerate(C_MVP_corr)}
|
|
1480
|
+
self.SALT_mvp_corr = {i: sublist for i, sublist in enumerate(S_MVP_corr)}
|
|
1481
|
+
self.TIME_mvp_corr = {i: sublist for i, sublist in enumerate(Time_MVP_corr)}
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
print("MVP profiles corrected.")
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def interpolate_CTD_and_MVPcorrected(self,length):
|
|
1488
|
+
|
|
1489
|
+
"""
|
|
1490
|
+
Interpolate CTD data onto the corrected MVP pressure levels.
|
|
1491
|
+
"""
|
|
1492
|
+
if not self.ctd:
|
|
1493
|
+
raise ValueError("CTD data not loaded.")
|
|
1494
|
+
|
|
1495
|
+
if not hasattr(self, 'PRES_mvp_corr'):
|
|
1496
|
+
raise ValueError("Corrected MVP data not available. Apply corrections first.")
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
max_lenpres = max([len(p) for p in self.PRES_mvp_corr.values()])
|
|
1500
|
+
PRES_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lenpres - len(row)) for row in self.PRES_mvp_corr.values()])
|
|
1501
|
+
|
|
1502
|
+
max_lentemp = max([len(p) for p in self.TEMP_mvp_corr.values()])
|
|
1503
|
+
TEMP_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lentemp - len(row)) for row in self.TEMP_mvp_corr.values()])
|
|
1504
|
+
|
|
1505
|
+
max_lencond = max([len(p) for p in self.COND_mvp_corr.values()])
|
|
1506
|
+
COND_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lencond - len(row)) for row in self.COND_mvp_corr.values()])
|
|
1507
|
+
|
|
1508
|
+
max_lensalt = max([len(p) for p in self.SALT_mvp_corr.values()])
|
|
1509
|
+
SALT_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lensalt - len(row)) for row in self.SALT_mvp_corr.values()])
|
|
1510
|
+
|
|
1511
|
+
max_lenvspd = max([len(p) for p in self.SPEED_mvp_corr.values()])
|
|
1512
|
+
SPEED_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lenvspd - len(row)) for row in self.SPEED_mvp_corr.values()])
|
|
1513
|
+
|
|
1514
|
+
max_lentime = max([len(p) for p in self.TIME_mvp_corr.values()])
|
|
1515
|
+
TIME_mvp_corr_mat = np.array([list(row) + [np.nan] * (max_lentime - len(row)) for row in self.TIME_mvp_corr.values()])
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
pressure_grid = np.linspace(np.nanmin(PRES_mvp_corr_mat), np.nanmax(PRES_mvp_corr_mat), length)
|
|
1519
|
+
|
|
1520
|
+
self.TEMP_ctd_on_mvp = mvp.vertical_interp(self.PRES_ctd, self.TEMP_ctd, pressure_grid)
|
|
1521
|
+
self.PRES_ctd_on_mvp = mvp.vertical_interp(self.PRES_ctd, self.PRES_ctd, pressure_grid)
|
|
1522
|
+
self.COND_ctd_on_mvp = mvp.vertical_interp(self.PRES_ctd, self.COND_ctd, pressure_grid)
|
|
1523
|
+
self.SALT_ctd_on_mvp = mvp.vertical_interp(self.PRES_ctd, self.SALT_ctd, pressure_grid)
|
|
1524
|
+
self.OXY_ctd_on_mvp = mvp.vertical_interp(self.PRES_ctd, self.OXY_ctd, pressure_grid)
|
|
1525
|
+
self.TEMP_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, TEMP_mvp_corr_mat, pressure_grid)
|
|
1526
|
+
self.PRES_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, PRES_mvp_corr_mat, pressure_grid)
|
|
1527
|
+
self.COND_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, COND_mvp_corr_mat, pressure_grid)
|
|
1528
|
+
self.SALT_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, SALT_mvp_corr_mat, pressure_grid)
|
|
1529
|
+
self.SPEED_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, SPEED_mvp_corr_mat, pressure_grid)
|
|
1530
|
+
self.TIME_mvp_corr_interp = mvp.vertical_interp(PRES_mvp_corr_mat, TIME_mvp_corr_mat, pressure_grid)
|
|
1531
|
+
|
|
1532
|
+
print('CTD data interpolated onto corrected MVP pressure levels.')
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
def to_netcdf(self, filepath=None, corrected=False, compression=True, engine=None, per_profile_files=False):
|
|
1536
|
+
"""
|
|
1537
|
+
Export MVP data to a NetCDF file using xarray.
|
|
1538
|
+
|
|
1539
|
+
Args:
|
|
1540
|
+
filepath (str): Output NetCDF file path.
|
|
1541
|
+
corrected (bool): Also write corrected arrays if present (*_mvp_corr). Default False.
|
|
1542
|
+
compression (bool): Enable compression (engine dependent). Default True.
|
|
1543
|
+
engine (str|None): One of 'netcdf4', 'h5netcdf', 'scipy'. If None, choose netcdf4.
|
|
1544
|
+
per_profile_files (bool): If True, write one .nc per MVP cycle (two rows: down and up).
|
|
1545
|
+
"""
|
|
1546
|
+
if not getattr(self, 'mvp', False):
|
|
1547
|
+
raise RuntimeError("No MVP data loaded. Call load_mvp_data() first.")
|
|
1548
|
+
|
|
1549
|
+
engine = 'netcdf4' if engine is None else engine
|
|
1550
|
+
if engine == 'scipy' and compression:
|
|
1551
|
+
print('Warning: scipy backend does not support compression; writing without compression.')
|
|
1552
|
+
compression = False
|
|
1553
|
+
|
|
1554
|
+
# Dimensions
|
|
1555
|
+
n_prof, n_samp = self.PRES_mvp.shape
|
|
1556
|
+
|
|
1557
|
+
# Coordinates
|
|
1558
|
+
profile_idx = np.arange(n_prof, dtype=np.int32)
|
|
1559
|
+
sample_idx = np.arange(n_samp, dtype=np.int32)
|
|
1560
|
+
|
|
1561
|
+
# Direction per profile (down/up)
|
|
1562
|
+
direction = None
|
|
1563
|
+
if hasattr(self, 'DIR') and len(self.DIR) == n_prof:
|
|
1564
|
+
direction = np.array(self.DIR, dtype=object)
|
|
1565
|
+
else:
|
|
1566
|
+
# Fallback based on even/odd
|
|
1567
|
+
direction = np.array(['down' if i % 2 == 0 else 'up' for i in range(n_prof)], dtype=object)
|
|
1568
|
+
|
|
1569
|
+
# Per-sample time as seconds since reference origin
|
|
1570
|
+
# TIME_mvp is in days relative to self.date_ref
|
|
1571
|
+
time_seconds = None
|
|
1572
|
+
if hasattr(self, 'TIME_mvp'):
|
|
1573
|
+
time_seconds = self.TIME_mvp * 24.0 * 3600.0
|
|
1574
|
+
else:
|
|
1575
|
+
time_seconds = np.full((n_prof, n_samp), np.nan)
|
|
1576
|
+
|
|
1577
|
+
# Per-profile datetime (one timestamp per cast pair); map using i//2
|
|
1578
|
+
profile_time = None
|
|
1579
|
+
if hasattr(self, 'DATETIME_mvp') and len(getattr(self, 'DATETIME_mvp', [])) > 0:
|
|
1580
|
+
prof_times = []
|
|
1581
|
+
for i in range(n_prof):
|
|
1582
|
+
j = i // 2
|
|
1583
|
+
if j < len(self.DATETIME_mvp) and self.DATETIME_mvp[j] is not None:
|
|
1584
|
+
prof_times.append(np.datetime64(self.DATETIME_mvp[j]))
|
|
1585
|
+
else:
|
|
1586
|
+
prof_times.append(np.datetime64('NaT'))
|
|
1587
|
+
profile_time = np.array(prof_times, dtype='datetime64[ns]')
|
|
1588
|
+
else:
|
|
1589
|
+
profile_time = np.array([np.datetime64('NaT')] * n_prof, dtype='datetime64[ns]')
|
|
1590
|
+
|
|
1591
|
+
# Build dataset variables safely
|
|
1592
|
+
data_vars = {}
|
|
1593
|
+
|
|
1594
|
+
def add_var(var_name, arr, units=None, long_name=None):
|
|
1595
|
+
if arr is None:
|
|
1596
|
+
return
|
|
1597
|
+
data_vars[var_name] = (
|
|
1598
|
+
('profile', 'sample'), arr,
|
|
1599
|
+
{k: v for k, v in [('units', units), ('long_name', long_name)] if v is not None}
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
add_var('PRES', getattr(self, 'PRES_mvp', None), units='dbar', long_name='Sea water pressure')
|
|
1603
|
+
add_var('TEMP', getattr(self, 'TEMP_mvp', None), units='degC', long_name='In-situ temperature')
|
|
1604
|
+
add_var('COND', getattr(self, 'COND_mvp', None), units='mS/cm', long_name='Conductivity')
|
|
1605
|
+
add_var('SAL', getattr(self, 'SALT_mvp', None), units='psu', long_name='Practical salinity')
|
|
1606
|
+
add_var('SOUNDVEL', getattr(self, 'SOUNDVEL_mvp', None), units='m s-1', long_name='Sound speed')
|
|
1607
|
+
add_var('DO', getattr(self, 'DO_mvp', None), units='ml/L', long_name='Dissolved oxygen')
|
|
1608
|
+
add_var('TEMP2', getattr(self, 'TEMP2_mvp', None), units='degC', long_name='Oxygen sensor temperature')
|
|
1609
|
+
add_var('SUNA', getattr(self, 'SUNA_mvp', None), long_name='SUNA raw/derived')
|
|
1610
|
+
add_var('FLUO', getattr(self, 'FLUO_mvp', None), units='ug/L', long_name='Chl fluorescence')
|
|
1611
|
+
add_var('TURB', getattr(self, 'TURB_mvp', None), units='NTU', long_name='Turbidity')
|
|
1612
|
+
add_var('PH', getattr(self, 'PH_mvp', None), units='1', long_name='pH')
|
|
1613
|
+
|
|
1614
|
+
# Position and time arrays (2D)
|
|
1615
|
+
if hasattr(self, 'LAT_mvp'):
|
|
1616
|
+
add_var('LATITUDE', self.LAT_mvp, units='degrees_north', long_name='Latitude at sample')
|
|
1617
|
+
if hasattr(self, 'LON_mvp'):
|
|
1618
|
+
add_var('LONGITUDE', self.LON_mvp, units='degrees_east', long_name='Longitude at sample')
|
|
1619
|
+
# Time seconds since reference
|
|
1620
|
+
data_vars['TIME'] = (
|
|
1621
|
+
('profile', 'sample'), time_seconds,
|
|
1622
|
+
{
|
|
1623
|
+
'units': f'seconds since {self.date_ref.strftime("%Y-%m-%d %H:%M:%S")}',
|
|
1624
|
+
'long_name': 'Time at sample'
|
|
1625
|
+
}
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Include corrected arrays if requested and present
|
|
1629
|
+
if corrected:
|
|
1630
|
+
def add_corr(name, attr, units=None, long_name=None):
|
|
1631
|
+
if hasattr(self, attr):
|
|
1632
|
+
data_vars[name] = (
|
|
1633
|
+
('profile', 'sample'), getattr(self, attr),
|
|
1634
|
+
{k: v for k, v in [('units', units), ('long_name', long_name)] if v is not None}
|
|
1635
|
+
)
|
|
1636
|
+
add_corr('pressure_corrected', 'PRES_mvp_corr', units='dbar', long_name='Corrected pressure')
|
|
1637
|
+
add_corr('temperature_corrected', 'TEMP_mvp_corr', units='degC', long_name='Corrected temperature')
|
|
1638
|
+
add_corr('conductivity_corrected', 'COND_mvp_corr', units='mS/cm', long_name='Corrected conductivity')
|
|
1639
|
+
add_corr('salinity_corrected', 'SALT_mvp_corr', units='psu', long_name='Corrected salinity')
|
|
1640
|
+
if hasattr(self, 'TIME_mvp_corr'):
|
|
1641
|
+
data_vars['time_corrected'] = (
|
|
1642
|
+
('profile', 'sample'), self.TIME_mvp_corr * 24.0 * 3600.0,
|
|
1643
|
+
{
|
|
1644
|
+
'units': f'seconds since {self.date_ref.strftime("%Y-%m-%d %H:%M:%S")}',
|
|
1645
|
+
'long_name': 'Corrected time at sample'
|
|
1646
|
+
}
|
|
1647
|
+
)
|
|
1648
|
+
if hasattr(self, 'LAT_mvp_corr'):
|
|
1649
|
+
add_corr('latitude_corrected', 'LAT_mvp_corr', units='degrees_north', long_name='Corrected latitude at sample')
|
|
1650
|
+
if hasattr(self, 'LON_mvp_corr'):
|
|
1651
|
+
add_corr('longitude_corrected', 'LON_mvp_corr', units='degrees_east', long_name='Corrected longitude at sample')
|
|
1652
|
+
|
|
1653
|
+
# Coordinates and auxiliary per-profile variables
|
|
1654
|
+
coords = {
|
|
1655
|
+
'profile': ('profile', profile_idx),
|
|
1656
|
+
'sample': ('sample', sample_idx)
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
# Encode direction/time according to engine capabilities
|
|
1660
|
+
if engine in ('netcdf4', 'h5netcdf'):
|
|
1661
|
+
coords['direction'] = ('profile', direction.astype('U'), {'long_name': 'Profile direction'})
|
|
1662
|
+
coords['profile_time'] = ('profile', profile_time, {'long_name': 'Profile nominal time'})
|
|
1663
|
+
else:
|
|
1664
|
+
# scipy backend: avoid object strings and datetime; use numeric fallbacks
|
|
1665
|
+
dir_flag = np.where(direction.astype('U') == 'down', 0, 1).astype('int8')
|
|
1666
|
+
coords['direction_flag'] = (
|
|
1667
|
+
'profile', dir_flag, {'long_name': 'Profile direction (0=down,1=up)'}
|
|
1668
|
+
)
|
|
1669
|
+
ref = np.datetime64(self.date_ref)
|
|
1670
|
+
pt = profile_time.astype('datetime64[s]')
|
|
1671
|
+
mask = (pt == np.datetime64('NaT'))
|
|
1672
|
+
secs = (pt - ref).astype('timedelta64[s]').astype('float64')
|
|
1673
|
+
secs[mask] = np.nan
|
|
1674
|
+
coords['profile_time_sec'] = (
|
|
1675
|
+
'profile', secs,
|
|
1676
|
+
{'units': f'seconds since {self.date_ref.strftime("%Y-%m-%d %H:%M:%S")}',
|
|
1677
|
+
'long_name': 'Profile nominal time'}
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
# Optional per-profile lat/lon (first valid sample)
|
|
1681
|
+
def first_valid(vec):
|
|
1682
|
+
# vec shape (n_prof, n_samp)
|
|
1683
|
+
out = np.full((vec.shape[0],), np.nan)
|
|
1684
|
+
for i in range(vec.shape[0]):
|
|
1685
|
+
row = vec[i]
|
|
1686
|
+
j = np.where(~np.isnan(row))[0]
|
|
1687
|
+
if j.size:
|
|
1688
|
+
out[i] = row[j[0]]
|
|
1689
|
+
return out
|
|
1690
|
+
|
|
1691
|
+
if hasattr(self, 'LAT_mvp'):
|
|
1692
|
+
coords['profile_lat'] = (
|
|
1693
|
+
'profile', first_valid(self.LAT_mvp), {'units': 'degrees_north', 'long_name': 'Profile latitude'}
|
|
1694
|
+
)
|
|
1695
|
+
if hasattr(self, 'LON_mvp'):
|
|
1696
|
+
coords['profile_lon'] = (
|
|
1697
|
+
'profile', first_valid(self.LON_mvp), {'units': 'degrees_east', 'long_name': 'Profile longitude'}
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
# Global attributes
|
|
1701
|
+
attrs = {
|
|
1702
|
+
'title': 'MVP profile data',
|
|
1703
|
+
'Conventions': 'CF-1.8',
|
|
1704
|
+
'institution': 'LMD/CNRS',
|
|
1705
|
+
'source': 'MVPAnalyzer',
|
|
1706
|
+
'history': f"Created on {datetime.now().isoformat()}",
|
|
1707
|
+
'mvp_Yorig': int(self.Yorig)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
ds = xr.Dataset(data_vars=data_vars, coords=coords, attrs=attrs)
|
|
1711
|
+
|
|
1712
|
+
# Compression encoding per engine
|
|
1713
|
+
encoding = None
|
|
1714
|
+
if compression:
|
|
1715
|
+
if engine == 'netcdf4':
|
|
1716
|
+
encoding = {name: {'zlib': True, 'complevel': 4} for name in data_vars.keys()}
|
|
1717
|
+
elif engine == 'h5netcdf':
|
|
1718
|
+
encoding = {name: {'compression': 'gzip', 'compression_opts': 4} for name in data_vars.keys()}
|
|
1719
|
+
|
|
1720
|
+
# Determine output base directory
|
|
1721
|
+
if filepath is None:
|
|
1722
|
+
base_dir = self.output_path if hasattr(self, 'output_path') else os.getcwd() + os.sep
|
|
1723
|
+
else:
|
|
1724
|
+
# If a full file path was provided and not per_profile_files, honor it
|
|
1725
|
+
if (not per_profile_files) and filepath.lower().endswith('.nc'):
|
|
1726
|
+
out_path = filepath
|
|
1727
|
+
ds.to_netcdf(out_path, encoding=encoding, engine=engine)
|
|
1728
|
+
print(f"NetCDF written: {out_path} using engine={engine}")
|
|
1729
|
+
return
|
|
1730
|
+
base_dir = filepath
|
|
1731
|
+
|
|
1732
|
+
if not base_dir.endswith(os.sep):
|
|
1733
|
+
base_dir = base_dir + os.sep
|
|
1734
|
+
|
|
1735
|
+
base_name = "MVP_" + os.path.basename(self.data_path).rstrip(os.sep)
|
|
1736
|
+
if per_profile_files:
|
|
1737
|
+
# Write one file per pair (down/up)
|
|
1738
|
+
total_pairs = (n_prof + 1) // 2
|
|
1739
|
+
for i in range(total_pairs):
|
|
1740
|
+
idxs = [k for k in (2*i, 2*i+1) if k < n_prof]
|
|
1741
|
+
if not idxs:
|
|
1742
|
+
continue
|
|
1743
|
+
ds_i = ds.isel(profile=idxs)
|
|
1744
|
+
|
|
1745
|
+
#add i to filename
|
|
1746
|
+
fname = f"{base_name}_profile_{i:03d}.nc"
|
|
1747
|
+
out_path = os.path.join(base_dir, fname)
|
|
1748
|
+
ds_i.to_netcdf(out_path, encoding=encoding, engine=engine)
|
|
1749
|
+
print(f"NetCDF written per profile into: {base_dir} using engine={engine}")
|
|
1750
|
+
else:
|
|
1751
|
+
file_name = f"{base_name}.nc"
|
|
1752
|
+
out_path = os.path.join(base_dir, file_name)
|
|
1753
|
+
ds.to_netcdf(out_path, encoding=encoding, engine=engine)
|
|
1754
|
+
print(f"NetCDF written: {out_path} using engine={engine}")
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def help(self):
|
|
1758
|
+
"""
|
|
1759
|
+
Print all methods of the class with their docstring (header).
|
|
1760
|
+
"""
|
|
1761
|
+
for attr in dir(self):
|
|
1762
|
+
if callable(getattr(self, attr)) and not attr.startswith("__"):
|
|
1763
|
+
method = getattr(self, attr)
|
|
1764
|
+
doc = method.__doc__
|
|
1765
|
+
print(f"{attr}:\n{doc}\n{'-'*40}")
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
|
|
1769
|
+
def split_ctd(pres, array):
|
|
1770
|
+
|
|
1771
|
+
ibot = np.min(np.where(pres == pres.max()))
|
|
1772
|
+
|
|
1773
|
+
array_down = array[:ibot]
|
|
1774
|
+
array_up = array[ibot:]
|
|
1775
|
+
|
|
1776
|
+
return array_down, array_up
|