ticoi 0.0.1__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.

Potentially problematic release.


This version of ticoi might be problematic. Click here for more details.

ticoi/pixel_class.py ADDED
@@ -0,0 +1,1830 @@
1
+ import copy
2
+ from typing import List
3
+
4
+ import matplotlib
5
+ import matplotlib.colors as mcolors
6
+ import matplotlib.lines as malines
7
+ import matplotlib.pyplot as plt
8
+ import numpy as np
9
+ import pandas as pd
10
+ import scipy.fft as fft
11
+ import scipy.signal as signal
12
+ import seaborn as sns
13
+ from scipy.optimize import curve_fit
14
+ from sklearn.metrics import mean_squared_error
15
+
16
+ # %%========================================================================= #
17
+ # DATAFRAME_DATA OBJECT #
18
+ # =========================================================================%% #
19
+
20
+
21
+ class DataframeData:
22
+ """Object to define a pd.Dataframe storing velocity observations"""
23
+
24
+ def __init__(self, dataf: pd.DataFrame = pd.DataFrame()):
25
+ self.dataf = dataf
26
+
27
+ def set_temporal_baseline_central_date_offset_bar(self):
28
+ """Set temporal baselines ('temporal_baseline'), centrale date (date_cori), and offset bar ('offset_bar'), used for plotting"""
29
+
30
+ delta = self.dataf["date2"] - self.dataf["date1"] # temporal baseline of the observations
31
+ self.dataf["date_cori"] = np.asarray(self.dataf["date1"] + delta // 2).astype("datetime64[D]") # central date
32
+ try:
33
+ self.dataf["temporal_baseline"] = np.asarray((delta).dt.days).astype(
34
+ "int"
35
+ ) # temporal baseline as an integer
36
+ except TypeError:
37
+ self.dataf["temporal_baseline"] = np.array([delta[i].days for i in range(delta.shape[0])])
38
+ self.dataf["offset_bar"] = delta // 2 # to plot the temporal baseline of the plots
39
+
40
+ def set_vx_vy_invert(self, type_data: str = "invert", conversion: int = 365):
41
+ """
42
+ Convert displacements into velocity
43
+
44
+ :param type_data: [str] [default is "invert"] --- Type of the data to be converted to velocities (generally "invert" or "obs_filt")
45
+ :param conversion: [int] [default is 365] --- Conversion factor: 365 is the unit of the velocity is m/y and 1 if it is m/d
46
+ """
47
+
48
+ if "result_dx" in self.dataf.columns:
49
+ self.dataf = self.dataf.rename(columns={"result_dx": "vx", "result_dy": "vy"})
50
+ self.dataf["vx"] = self.dataf["vx"] / self.dataf["temporal_baseline"] * conversion
51
+ self.dataf["vy"] = self.dataf["vy"] / self.dataf["temporal_baseline"] * conversion
52
+
53
+ def set_vx_vy_my(self, type_data: str = "obs_filt", conversion: int = 365):
54
+ if "result_dx" in self.dataf.columns:
55
+ self.dataf = self.dataf.rename(columns={"result_dx": "vx", "result_dy": "vy"})
56
+ self.dataf["vx"] = self.dataf["vx"] * conversion
57
+ self.dataf["vy"] = self.dataf["vy"] * conversion
58
+
59
+ def set_vv(self):
60
+ """Set velocity magnitude variable (here vv) in the dataframe"""
61
+
62
+ self.dataf["vv"] = np.round(
63
+ np.sqrt((self.dataf["vx"] ** 2 + self.dataf["vy"] ** 2).astype("float")), 2
64
+ ) # Compute the magnitude of the velocity
65
+
66
+ def set_minmax(self):
67
+ """Set the attribute minimum and maximum fir vx, vy, and possibly vv"""
68
+
69
+ self.vxymin = int(self.dataf["vx"].min())
70
+ self.vxymax = int(self.dataf["vx"].max())
71
+ self.vyymin = int(self.dataf["vy"].min())
72
+ self.vyymax = int(self.dataf["vy"].max())
73
+ if "vv" in self.dataf.columns:
74
+ self.vvymin = int(self.dataf["vv"].min())
75
+ self.vvymax = int(self.dataf["vv"].max())
76
+
77
+
78
+ # %%========================================================================= #
79
+ # PIXEL_CLASS OBJECT #
80
+ # =========================================================================%% #
81
+
82
+
83
+ class PixelClass:
84
+ """Object class to store the data on a given pixel"""
85
+
86
+ def __init__(
87
+ self,
88
+ save: bool = False,
89
+ path_save: str = "",
90
+ show: bool = True,
91
+ figsize: tuple[int, int] = (10, 6),
92
+ unit: str = "m/y",
93
+ A: np.ndarray | None = None,
94
+ dataobs: pd.DataFrame | None = None,
95
+ ):
96
+ """
97
+ Initialize the pixel_class object with general plotting parameters, or set them to default values if no parameters are given.
98
+
99
+ :param save: [bool] [default is False] --- Save the figures to path_save
100
+ :param path_save: [str] [default is ""] --- Path where to save the figures if save is True
101
+ :param show: [bool] [default is True] --- Plot the figures
102
+ :param figsize: [tuple<int, int>] [default is (10, 6)] --- Size of the figures
103
+ :param unit: [str] [default is "m/y"] --- Unit of the velocities ("m/y" or "m/d")
104
+ :param A: [np.array | None] [default is None] --- Design matrix
105
+ :param dataobs: [pd.DataFrame | None] [default is None] --- Observation data
106
+ """
107
+
108
+ self.dataobs = dataobs
109
+ self.datainvert = None
110
+ self.datainterp = None
111
+ self.dataobsfilt = None
112
+ self.save = save
113
+ self.show = show
114
+ self.path_save = path_save
115
+ self.figsize = figsize
116
+ self.unit = unit
117
+ self.A = A
118
+
119
+ def set_data_from_pandas_df(
120
+ self, dataf_ilf: pd.DataFrame, type_data: str = "invert", conversion: int = 365, variables: List[str] = ["vv"]
121
+ ):
122
+ """
123
+ Set the data as a pandas DataFrame (using methods from the dataframe_data object).
124
+
125
+ :param dataf_ilf: [pd.DataFrame] --- Data
126
+ :param type_data: [str] [default is "invert"] --- Type of the data (raw data, results of TICO, TICOI...)
127
+ :param conversion: [int] [default is 365] --- Conversion factor: 365 is the unit of the velocity is m/y and 1 if it is m/d
128
+ :param variables: [List<str>] [default is ['vv']] --- List of variable to plot
129
+ """
130
+
131
+ if type_data == "invert":
132
+ self.datainvert = DataframeData(dataf_ilf)
133
+ self.datainvert.dataf = self.datainvert.dataf.rename(columns={"error_x": "errorx", "error_y": "errory"})
134
+ datatemp = self.datainvert
135
+ elif type_data == "interp":
136
+ self.datainterp = DataframeData(dataf_ilf)
137
+ datatemp = self.datainterp
138
+ elif type_data == "obs":
139
+ self.dataobs = DataframeData(dataf_ilf)
140
+ datatemp = self.dataobs
141
+ elif type_data == "obs_filt":
142
+ self.dataobsfilt = DataframeData(dataf_ilf)
143
+ datatemp = self.dataobsfilt
144
+ else:
145
+ raise ValueError(
146
+ "Please enter 'invert' for inverted results, 'interp' for ineterpolated results, 'obs' for observation or 'obs_filt' for filtered observations"
147
+ )
148
+
149
+ datatemp.set_temporal_baseline_central_date_offset_bar() # Set the temporal baseline,
150
+ if type_data == "invert":
151
+ datatemp.set_vx_vy_invert(type_data=type_data, conversion=conversion) # Convert displacement in vx and vy
152
+ elif type_data == "obs_filt":
153
+ datatemp.set_vx_vy_my(type_data=type_data, conversion=conversion)
154
+ if "vv" in variables:
155
+ datatemp.set_vv() # set velocity magnitude
156
+ datatemp.set_minmax() # set min and max, for figures plots
157
+
158
+ def load(
159
+ self,
160
+ dataf: pd.DataFrame | List[pd.DataFrame],
161
+ type_data: str = "obs",
162
+ dataformat: str = "df",
163
+ save: bool = False,
164
+ show: bool = False,
165
+ figsize: tuple[int, int] = (10, 6),
166
+ unit: str = "m/y",
167
+ path_save: str = "",
168
+ variables: List[str] | None = ["vv", "vx", "vy"],
169
+ A: np.ndarray | None = None,
170
+ ):
171
+ """
172
+ Load the data from dataf and format it in a dataframe_data object using the set_data_from_pandas_df method, depending on the type of data (type_data).
173
+ Initialize the object with general plotting parameters.
174
+
175
+ :param dataf: [pd.DataFrame | List[pd.DataFrame]] --- observations orresults from the inversion
176
+ :param type_data: [str] [default is 'obs'] --- of 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
177
+ :param dataformat: [str] [default is 'df'] --- id 'df' dataf is a pd.DataFrame
178
+ :param save: [bool] [default is False] --- if True, save the figures
179
+ :param show: [bool] [default is True] --- if True, show the figures
180
+ :param figsize: tuple[int, int] --- size of the figure
181
+ :param unit: [str] --- unit wanted for plotting
182
+ :param filt: [List[bool] | None] [default is None] --- Are dataf data filtered ? Put True if dataf data are displacemenst, None if all data are not filtered
183
+ :param path_save:[str] --- path where to store the data
184
+ :param variables: [List[str]] [default is ['vv']] --- list of variable to plot
185
+ :param A: [np.array] --- design matrix
186
+ """
187
+
188
+ self.__init__(save=save, show=show, figsize=figsize, unit=unit, path_save=path_save, A=A)
189
+
190
+ conversion = self.get_conversion() # Conversion factor
191
+ if isinstance(dataf, list) and len(dataf) > 1:
192
+ assert isinstance(type_data, list) and (len(dataf) == len(type_data)), (
193
+ "If 'dataf' is a list, 'type_data' must be a list of the same length"
194
+ )
195
+
196
+ for i in range(len(dataf)):
197
+ if dataformat == "df":
198
+ self.set_data_from_pandas_df(
199
+ dataf[i], type_data=type_data[i], conversion=conversion, variables=variables
200
+ )
201
+ elif (isinstance(dataf, list) and len(dataf) == 1) or isinstance(dataf, pd.DataFrame):
202
+ assert (isinstance(type_data, list) and len(type_data) == 1) or isinstance(type_data, str), (
203
+ "If 'dataf' is a dataframe or list of a single dataframe, 'type_data' must either be a list of a single string element, or a string"
204
+ )
205
+
206
+ if dataformat == "df":
207
+ self.set_data_from_pandas_df(
208
+ dataf[0] if isinstance(dataf, list) else dataf,
209
+ type_data=type_data[0] if isinstance(type_data, list) else type_data,
210
+ conversion=conversion,
211
+ variables=variables,
212
+ )
213
+ else:
214
+ raise ValueError(f"'dataf' must be a list or a pandas dataframe, not {type(dataf)}")
215
+
216
+ def get_dataf_invert_or_obs_or_interp(self, type_data: str = "obs") -> (pd.DataFrame, str): # type: ignore
217
+ """
218
+ Get dataframe either obs or invert
219
+
220
+ :param type_data: [str] [default is 'obs'] --- If 'obs', dataf corresponds to obsevations. If 'invert', it corresponds to the inverted velocities
221
+
222
+ :return [pd.DataFrame] --- Dataframe from obs, invert or interp
223
+ :return [str] --- Label used in the legend of the figures
224
+ """
225
+
226
+ # Get data when there is only dataframe loaded
227
+ if self.dataobs is None and self.datainterp is None and self.dataobsfilt is None:
228
+ return self.datainvert, "Results from the inversion"
229
+ elif self.datainvert is None and self.datainterp is None and self.dataobsfilt is None:
230
+ return self.dataobs, "Observations"
231
+ elif self.datainvert is None and self.dataobs is None and self.dataobsfilt is None:
232
+ return self.datainterp, "Results from TICOI"
233
+ elif self.datainvert is None and self.dataobs is None and self.datainter is None:
234
+ return self.dataobsfilt, "Observations filtered"
235
+ elif self.datainvert is None and self.dataobs is None and self.datainterp is None and self.dataobsfilt is None:
236
+ raise ValueError("Please load at least one dataframe")
237
+ else: # else
238
+ if type_data == "invert":
239
+ return self.datainvert, "Results from the inversion"
240
+ elif type_data == "obs":
241
+ return self.dataobs, "Observations"
242
+ elif type_data == "obs_filt":
243
+ return self.dataobsfilt, "Observations filtered"
244
+ else:
245
+ return self.datainterp, "Results from TICOI"
246
+
247
+ def get_conversion(self):
248
+ """
249
+ Get conversion factor
250
+
251
+ :return: [int] --- conversion factor
252
+ """
253
+
254
+ conversion = 365 if self.unit == "m/y" else 1
255
+ return conversion
256
+
257
+ def get_direction(self, data: "PixelClass.DataframeData") -> (np.array, np.array): # type: ignore
258
+ """
259
+ Get the direction of the provided data
260
+
261
+ :param data: [ticoi.pixel_class.dataframe_data] --- Dataframe from obs, invert or interp
262
+
263
+ :return directionm: [np.array] --- Directions of the data
264
+ :return directionm_mean: [np.array] --- Averaged direction of the data
265
+ """
266
+
267
+ directionm = np.arctan2(data.dataf["vy"].astype("float32"), data.dataf["vx"].astype("float32"))
268
+ directionm[directionm < 0] += 2 * np.pi
269
+ directionm_mean = np.arctan2(np.mean(data.dataf["vy"]), np.mean(data.dataf["vx"]))
270
+ if directionm_mean < 0:
271
+ directionm_mean += 2 * np.pi
272
+
273
+ # Convert to degrees
274
+ directionm *= 360 / (2 * np.pi)
275
+ directionm_mean *= 360 / (2 * np.pi)
276
+ return directionm, directionm_mean
277
+
278
+ def get_filtered_results(self, filt: str | None = None):
279
+ """
280
+ Filter TICOI results using a given filter.
281
+
282
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
283
+
284
+ :return vv_filt: [np array] --- Filtered velocities (magnitude)
285
+ :return vv_c: [np array] --- Centered velocities (magnitude)
286
+ :return dates_c: [np array] --- Central dates of the data
287
+ :return dates: [np array] --- For each data, the number of days between its central date and a reference (first date of the data)
288
+ """
289
+
290
+ # Get dates and velocities from TICOI results
291
+ dates_c = (
292
+ self.datainterp.dataf["date1"] + (self.datainterp.dataf["date2"] - self.datainterp.dataf["date1"]) // 2
293
+ ) # Central dates
294
+ dates = (
295
+ dates_c - self.datainterp.dataf["date1"].min()
296
+ ).dt.days.to_numpy() # Number of days to the reference day (first day of acquisition at the point)
297
+
298
+ vv = self.datainterp.dataf["vv"] # Velocity magnitude
299
+ vv_c = vv - np.mean(vv) # Centered velocities
300
+
301
+ Ts = dates[1] - dates[0]
302
+
303
+ # Filter the results...
304
+ if filt == "highpass": # ...to remove low frequencies (general trend over several years)
305
+ b, a = signal.butter(4, [1 / (1.5 * 365), 1 / (2.001 * Ts)], "bandpass", fs=1 / Ts, output="ba")
306
+ vv_filt = signal.filtfilt(b, a, vv_c)
307
+ elif filt == "lowpass": # ...to ensure Shanon critrion
308
+ sos = signal.butter(4, 1 / (2.001 * Ts), "lowpass", fs=1 / Ts, output="sos")
309
+ vv_filt = signal.sosfilt(sos, vv_c)
310
+ else: # Don't filter
311
+ vv_filt = vv_c
312
+
313
+ return vv_filt, vv_c, dates_c, dates
314
+
315
+ def get_TF(
316
+ self,
317
+ filtered_results: list = None,
318
+ filt: str | None = None,
319
+ verbose: bool = False,
320
+ ):
321
+ """
322
+ Compute the Fourier Transform (TF) of the interpolated results after applying a Hanning window.
323
+
324
+ :param filtered_results: [list | None] [default is None] --- Results of the filtering (get_filtered_results method) if previously processed. If None, it is processed here
325
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
326
+ :param verbose: [bool] [default is False] --- If True, print the maximum and the amplitude of the TF
327
+
328
+ :return vv_tf: [np array] --- TF of the interpolated velocities without windowing
329
+ :return vv_win_tf: [np array] --- TF of the interpolated velocities after windowing
330
+ :return freq: [np array] --- Frequencies of the TF
331
+ :return N: [np array] --- Number of dates
332
+ """
333
+
334
+ if filtered_results is not None:
335
+ vv_filt, vv_c, dates_c, dates = filtered_results
336
+ else:
337
+ vv_filt, vv_c, dates_c, dates = self.get_filtered_results(filt)
338
+ vv_filt = np.array(vv_filt)
339
+
340
+ N = len(dates)
341
+ Ts = dates[1] - dates[0]
342
+
343
+ # Hanning window
344
+ window = signal.windows.hann(N)
345
+
346
+ # TFD
347
+ n = 64 * N
348
+ vv_tf = fft.rfft(vv_filt, n=n)
349
+ vv_win_tf = fft.rfft(vv_filt * window, n=n)
350
+ freq = fft.rfftfreq(n, d=Ts)
351
+
352
+ if verbose:
353
+ f = freq[np.argmax(np.abs(vv_win_tf))]
354
+ print(f"TF maximum for f = {round(f, 5)} day-1 (period of {round(1 / f, 2)} days)")
355
+ print(
356
+ f"Amplitude of the TF at this frequency : {round(2 / N * np.abs(vv_tf[np.argmax(np.abs(vv_win_tf))]), 2)} m/y"
357
+ )
358
+
359
+ return vv_tf, vv_win_tf, freq, N
360
+
361
+ def get_best_matching_sinus(
362
+ self,
363
+ filt: str | None = None,
364
+ impose_frequency: bool = True,
365
+ raw_seasonality: bool = False,
366
+ several_freq: int = 1,
367
+ verbose: bool = False,
368
+ ):
369
+ """
370
+ Match a sinus (with fixed frequency or not) or a composition of several sinus (fundamental and harmonics) to the resulting TICOI data (and raw data)
371
+ to measure its amplitude, the position of its maximum, the RMSE with the original data...
372
+
373
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
374
+ :param impose_frequency: [bool] [default is True] --- If True, impose the frequency to 1/365.25 days-1 (one year seasonality). If False, look for the best matching frequency too, using the Fourier Transform in the first place
375
+ :param raw_seasonality: [bool] [default is False] --- Also look for the best matching sinus directly on the raw data
376
+ :param several_freq: [int] [default is 1] --- Number of harmonics to be computed (combination of sinus at frequencies 1/365.25, 2/365.25, etc...). If 1, only compute the fundamental.
377
+ :param verbose: [bool] [default is False] --- If True, print the amplitude, the position of the maximum and the RMSE between the best matching sinus and the original data (TICOI results and raw data), and the best matching frequency if impose_frequency is False
378
+
379
+ :return sine_f: [function] --- The function used for the optimization (can be used like sine = sine_f(dates[0], *popt, freqs=several_freq))
380
+ :return popt: [list] --- Parameters of the best matching sinus to TICOI results
381
+ :return popt_raw: [list] --- Parameters of the best matching sinus to raw data
382
+ :return [dates, dates_c, dates_raw]: [list] --- dates, dates_raw: number of days between the central dates and the first central dates, dates_c: central dates of the data
383
+ :return vv_filt: [np array] --- Filtered velocities (magnitude)
384
+ :return stats: [list] --- Statistics about the best matching sinus to TICOI results [first maximum (date), day of the year of the maximum, amplitude, RMSE]
385
+ :return stats_raw: [list] --- Statistics about the best matching sinus to raw data
386
+ """
387
+
388
+ # sine_fconst if impose_frequency else sine_fvar, popt, popt_raw, [dates, dates_c, dates_raw], vv_filt, stats, stats_raw
389
+
390
+ vv_filt, vv_c, dates_c, dates = self.get_filtered_results(filt=filt)
391
+
392
+ N = len(dates)
393
+
394
+ if impose_frequency:
395
+ # Sinus function (can add harmonics)
396
+ def sine_fconst(t, *args, freqs=1, f=1 / 365.25):
397
+ sine = args[0] * np.sin(2 * np.pi * f * t + args[1])
398
+ for freq in range(1, freqs):
399
+ sine += args[2 * freq] * np.sin(2 * np.pi * (freq + 1) * f * t + args[2 * freq + 1])
400
+ return sine + args[-1]
401
+
402
+ f = 1 / 365.25
403
+
404
+ # Find the best matching sinus to TICOI results
405
+ guess = np.concatenate(
406
+ [np.concatenate([[np.max(vv_filt) - np.min(vv_filt), 0] for _ in range(several_freq)]), [0]]
407
+ )
408
+ popt, pcov = curve_fit(lambda t, *args: sine_fconst(t, *args, freqs=several_freq), dates, vv_filt, p0=guess)
409
+
410
+ # Parameters
411
+ sine = sine_fconst(dates, *popt, freqs=several_freq)
412
+ sine_year = sine_fconst(np.linspace(1, 365, 365), *popt, freqs=several_freq)
413
+
414
+ first_max_day = pd.Timedelta(np.argmax(sine_year), "D") + self.datainterp.dataf["date1"].min()
415
+ max_day = first_max_day - pd.Timestamp(year=first_max_day.year, month=1, day=1)
416
+ max_value = np.max(sine_year) - popt[-1]
417
+ RMSE = np.sqrt(mean_squared_error(sine, vv_filt))
418
+
419
+ del sine_year
420
+
421
+ if verbose:
422
+ print(
423
+ f"Amplitude of the best matching sinus (with period 365.25 days) to TICOI results: {round(max_value, 2)} m/y"
424
+ )
425
+ print(f"Maximum at day {max_day.days}")
426
+ print(f"RMSE : {round(RMSE, 2)} m/y")
427
+
428
+ if raw_seasonality:
429
+ # Find the best matching sinus to raw data
430
+ dates_raw = (self.dataobs.dataf.index - self.datainterp.dataf["date1"].min()).days.to_numpy()
431
+ raw_c = self.dataobs.dataf["vv"] - self.dataobs.dataf["vv"].mean()
432
+ guess_raw = np.concatenate(
433
+ [np.concatenate([[np.max(raw_c) - np.min(raw_c), 0] for _ in range(several_freq)]), [0]]
434
+ )
435
+ popt_raw, pcov_raw = curve_fit(
436
+ lambda t, *args: sine_fconst(t, *args, freqs=several_freq), dates_raw, raw_c, p0=guess_raw
437
+ )
438
+
439
+ # Parameters
440
+ sine_raw = sine_fconst(dates_raw, *popt_raw, freqs=several_freq)
441
+ sine_year_raw = sine_fconst(np.linspace(1, 365, 365), *popt_raw, freqs=several_freq)
442
+
443
+ first_max_day_raw = pd.Timedelta(np.argmax(sine_year_raw), "D") + self.datainterp.dataf["date1"].min()
444
+ max_day_raw = first_max_day_raw - pd.Timestamp(year=first_max_day_raw.year, month=1, day=1)
445
+ max_value_raw = np.max(sine_year_raw) - popt_raw[-1]
446
+ RMSE_raw = np.sqrt(mean_squared_error(sine_raw, raw_c))
447
+
448
+ stats_raw = [first_max_day_raw, max_day_raw, max_value_raw, RMSE_raw]
449
+ del sine_year_raw
450
+
451
+ if verbose:
452
+ print(
453
+ f"Amplitude of the best matching sinus (with period 365.25 days) to raw data: {round(max_value_raw, 2)} m/y"
454
+ )
455
+ print(f"Maximum at day {max_day_raw.days}")
456
+ print(f"RMSE : {round(RMSE_raw, 2)} m/y")
457
+
458
+ else:
459
+ vv_tf, vv_win_tf, freq, _ = self.get_TF(vv_filt, vv_c, dates_c, dates, filt=filt, verbose=False)
460
+
461
+ # Sinus function
462
+ def sine_fvar(t, A, f, phi, off, freqs=None):
463
+ return A * np.sin(2 * np.pi * f * t + phi) + off
464
+
465
+ # Initial guess from the TF
466
+ guess = np.array(
467
+ [
468
+ np.max(2 / N * np.abs(vv_win_tf)),
469
+ freq[np.argmax(np.abs(vv_win_tf))],
470
+ np.angle(vv_win_tf)[np.argmax(np.abs(vv_win_tf))],
471
+ np.mean(vv_win_tf),
472
+ ],
473
+ dtype="float",
474
+ )
475
+
476
+ popt, pcov = curve_fit(sine_fvar, dates, vv_filt, p0=guess)
477
+ A, f, phi, off = popt
478
+ sine = sine_fvar(dates, A, f, phi, off)
479
+ sine_year = sine_fvar(np.linspace(1, 365, 365), A, f, phi, off)
480
+
481
+ first_max_day = pd.Timedelta(np.argmax(sine_year), "D") + self.datainterp.dataf["date1"].min()
482
+ max_day = first_max_day - pd.Timestamp(year=first_max_day.year, month=1, day=1)
483
+ max_value = np.max(sine_year) - off
484
+ RMSE = np.sqrt(mean_squared_error(mean_squared_error(sine, vv_filt)))
485
+
486
+ del sine_year
487
+
488
+ if verbose:
489
+ print(f"Period of the best matching sinus : {round(1 / f, 2)} days")
490
+ print(f"Amplitude : {round(max_value, 2)} m/y")
491
+ print(f"Maximum at day {max_day.days}")
492
+ print(f"RMSE : {round(RMSE, 2)} m/y")
493
+
494
+ stats = [first_max_day, max_day, max_value, RMSE]
495
+ if not (impose_frequency and raw_seasonality):
496
+ popt_raw, dates_raw, stats_raw = None, None, None
497
+
498
+ return (
499
+ sine_fconst if impose_frequency else sine_fvar,
500
+ popt,
501
+ popt_raw,
502
+ [dates, dates_c, dates_raw],
503
+ vv_filt,
504
+ stats,
505
+ stats_raw,
506
+ )
507
+
508
+ # %%========================================================================= #
509
+ # PLOTS ABOUT RAW DATA / INTERPOLATION RESULTS #
510
+ # =========================================================================%% #
511
+
512
+ def plot_vx_vy(self, color: str = "orange", type_data: str = "invert", block_plot: bool = True):
513
+ """
514
+ Plot vx and vy in two plots of the same figure.
515
+
516
+ :param color: [str] [default is 'orange'] --- Color used for the plot
517
+ :param type_data: [str] [default is 'obs'] --- If 'obs' dataf corresponds to observations, if 'invert', it corresponds to inverted velocity
518
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
519
+
520
+ :return fig, ax: Axis and Figures of the plot
521
+ """
522
+
523
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
524
+
525
+ # Display the vx components
526
+ fig, ax = plt.subplots(2, 1, figsize=self.figsize)
527
+ ax[0].set_ylim(data.vxymin, data.vxymax)
528
+ ax[0].plot(data.dataf["date_cori"], data.dataf["vx"], linestyle="", marker="o", markersize=2, color=color)
529
+ ax[0].errorbar(
530
+ data.dataf["date_cori"],
531
+ data.dataf["vx"],
532
+ xerr=data.dataf["offset_bar"],
533
+ color=color,
534
+ alpha=0.2,
535
+ fmt=",",
536
+ zorder=1,
537
+ )
538
+ ax[0].set_ylabel(f"Vx [{self.unit}]", fontsize=14)
539
+
540
+ # Display the vy components
541
+ ax[1].set_ylim(data.vyymin, data.vyymax)
542
+ ax[1].plot(
543
+ data.dataf["date_cori"], data.dataf["vy"], linestyle="", marker="o", markersize=2, color=color, label=label
544
+ )
545
+ ax[1].errorbar(
546
+ data.dataf["date_cori"],
547
+ data.dataf["vy"],
548
+ xerr=data.dataf["offset_bar"],
549
+ color=color,
550
+ alpha=0.2,
551
+ fmt=",",
552
+ zorder=1,
553
+ )
554
+ ax[1].set_ylabel(f"Vy [{self.unit}]", fontsize=14)
555
+ ax[1].set_xlabel("Central dates", fontsize=14)
556
+ plt.subplots_adjust(bottom=0.2)
557
+ ax[1].legend(loc="lower left", bbox_to_anchor=(0.02, -0.4), fontsize=14)
558
+
559
+ fig.suptitle("X and Y components of raw data velocities", y=0.95, fontsize=16)
560
+
561
+ if self.show:
562
+ plt.show(block=block_plot)
563
+ if self.save:
564
+ fig.savefig(f"{self.path_save}/vx_vy_{type_data}.png")
565
+
566
+ return fig, ax
567
+
568
+ def plot_vx_vy_overlaid(
569
+ self,
570
+ colors: List[str] = ["orange", "blue"],
571
+ type_data: str = "invert",
572
+ zoom_on_results: bool = False,
573
+ block_plot: bool = True,
574
+ ):
575
+ """
576
+ Plot vx and vy in two plots of the same figure where inverted/interpolated results overlay the observations (raw data).
577
+
578
+ :param colors: [List[str]] [default is ['orange', 'blue']] --- List of the colors used for the plot (first : raw data, second : overlaying data)
579
+ :param type_data: [str] [default is 'obs'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
580
+ :param zoom_on_results: [bool] [default is False] --- If True set the limits of the axis according to the results min and max
581
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
582
+
583
+ :return fig, ax: Axis and Figures of the plot
584
+ """
585
+
586
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
587
+
588
+ show = copy.copy(self.show)
589
+ save = copy.copy(self.save)
590
+ self.show, self.save = False, False
591
+ fig, ax = self.plot_vx_vy(color=colors[0], type_data="obs")
592
+
593
+ self.show, self.save = show, save
594
+
595
+ if zoom_on_results:
596
+ ax[0].set_ylim(data.vxymin, data.vxymax)
597
+ ax[0].plot(
598
+ data.dataf["date_cori"], data.dataf["vx"], linestyle="", marker="o", markersize=2, color=colors[1]
599
+ ) # Display the vx components
600
+ ax[0].errorbar(
601
+ data.dataf["date_cori"],
602
+ data.dataf["vx"],
603
+ xerr=data.dataf["offset_bar"],
604
+ color=colors[1],
605
+ alpha=0.5,
606
+ fmt=",",
607
+ zorder=1,
608
+ )
609
+ if zoom_on_results:
610
+ ax[1].set_ylim(data.vyymin, data.vyymax)
611
+ ax[1].plot(
612
+ data.dataf["date_cori"],
613
+ data.dataf["vy"],
614
+ linestyle="",
615
+ marker="o",
616
+ markersize=2,
617
+ color=colors[1],
618
+ label=label,
619
+ ) # Display the vy components
620
+ ax[1].errorbar(
621
+ data.dataf["date_cori"],
622
+ data.dataf["vy"],
623
+ xerr=data.dataf["offset_bar"],
624
+ color="b",
625
+ alpha=0.2,
626
+ fmt=",",
627
+ zorder=1,
628
+ )
629
+ ax[1].legend(loc="lower left", bbox_to_anchor=(0.0, -0.65), fontsize=14)
630
+ fig.suptitle(
631
+ f"X and Y components of {'interpolated' if type_data == 'interp' else 'inverted'} results, along with raw data",
632
+ y=0.95,
633
+ fontsize=16,
634
+ )
635
+
636
+ if self.show:
637
+ plt.show(block=block_plot)
638
+ if self.save:
639
+ if zoom_on_results:
640
+ fig.savefig(f"{self.path_save}/vx_vy_overlaid_zoom_on_results_{type_data}.png")
641
+ else:
642
+ fig.savefig(f"{self.path_save}/vx_vy_overlaid_{type_data}.png")
643
+
644
+ return fig, ax
645
+
646
+ def plot_vv(
647
+ self, color: str = "orange", type_data: str = "invert", block_plot: bool = True, vminmax: list | None = None
648
+ ):
649
+ """
650
+ Plot the velocity magnitude.
651
+
652
+ :param color: [str] [default is 'orange'] --- Color used for the plot
653
+ :param type_data: [str] [default is 'invert'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
654
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
655
+ :param vminmax: List[int] [default is None] --- Min and max values for the y-axis of the plots
656
+
657
+ :return fig, ax: Axis and Figure of the plot
658
+ """
659
+
660
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
661
+
662
+ fig, ax = plt.subplots(figsize=self.figsize)
663
+ if vminmax is None:
664
+ ax.set_ylim(data.vvymin, data.vvymax)
665
+ else:
666
+ ax.set_ylim(vminmax[0], vminmax[1])
667
+ ax.set_ylabel(f"Velocity magnitude [{self.unit}]", fontsize=14)
668
+ ax.plot(
669
+ data.dataf["date_cori"],
670
+ data.dataf["vv"],
671
+ linestyle="",
672
+ zorder=1,
673
+ marker="o",
674
+ lw=0.7,
675
+ markersize=2,
676
+ color=color,
677
+ label=label,
678
+ )
679
+ ax.errorbar(
680
+ data.dataf["date_cori"],
681
+ data.dataf["vv"],
682
+ xerr=data.dataf["offset_bar"],
683
+ color=color,
684
+ alpha=0.2,
685
+ fmt=",",
686
+ zorder=1,
687
+ )
688
+ plt.subplots_adjust(bottom=0.2)
689
+ ax.legend(loc="lower left", bbox_to_anchor=(0.02, -0.25), fontsize=14)
690
+ ax.set_xlabel("Central dates", fontsize=14)
691
+
692
+ if type_data == "obs":
693
+ fig.suptitle("Magnitude of raw data velocities", y=0.95, fontsize=16)
694
+ elif type_data == "invert":
695
+ fig.suptitle("Magnitude of inverted velocities", y=0.95, fontsize=16)
696
+ elif type_data == "interp":
697
+ fig.suptitle("Magnitude of interpolated velocities", y=0.95, fontsize=16)
698
+
699
+ if self.show:
700
+ plt.show(block=block_plot)
701
+ if self.save:
702
+ fig.savefig(f"{self.path_save}/vv_{type_data}.png")
703
+
704
+ return fig, ax
705
+
706
+ def plot_vv_overlaid(
707
+ self,
708
+ colors: List[str] = ["orange", "blue"],
709
+ type_data: str = "invert",
710
+ zoom_on_results: bool = False,
711
+ block_plot: bool = True,
712
+ vminmax: list | None = None,
713
+ ):
714
+ """
715
+ Plot the velocity magnitude of inverted/interpolated results, overlaying the velocity magnitude of the observations (raw data).
716
+
717
+ :param colors: [List[str]] [default is ['orange', 'blue']] --- List of the colors used for the plot (first : raw data, second : overlaying data)
718
+ :param type_data: [str] [default is 'invert'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
719
+ :param zoom_on_results: [bool] [default is False] --- Set the limites of the axis according to the results min and max
720
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
721
+ :param vminmax: List[int] [default is None] --- Min and max values for the y-axis of the plots
722
+
723
+ :return fig, ax: Axis and Figure of the plots
724
+ """
725
+
726
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
727
+
728
+ show = copy.copy(self.show)
729
+ save = copy.copy(self.save)
730
+ self.show, self.save = False, False
731
+ fig, ax = self.plot_vv(color=colors[0], type_data="obs", vminmax=vminmax)
732
+ self.show, self.save = show, save
733
+
734
+ if zoom_on_results:
735
+ ax.set_ylim(data.vvymin, data.vvymax)
736
+ ax.plot(
737
+ data.dataf["date_cori"],
738
+ data.dataf["vv"],
739
+ linestyle="",
740
+ zorder=1,
741
+ marker="o",
742
+ lw=0.7,
743
+ markersize=2,
744
+ color=colors[1],
745
+ label="Results from the inversion",
746
+ )
747
+ ax.errorbar(
748
+ data.dataf["date_cori"],
749
+ data.dataf["vv"],
750
+ xerr=data.dataf["offset_bar"],
751
+ color=colors[1],
752
+ alpha=0.2,
753
+ fmt=",",
754
+ zorder=1,
755
+ )
756
+ ax.legend(loc="lower left", bbox_to_anchor=(0, -0.3), fontsize=14)
757
+ fig.suptitle(
758
+ f"Magnitude of {'interpolated' if type_data == 'interp' else 'inverted'} results, along with raw data magnitude",
759
+ y=0.95,
760
+ fontsize=16,
761
+ )
762
+
763
+ if self.show:
764
+ plt.show(block=block_plot)
765
+ if self.save:
766
+ if zoom_on_results:
767
+ fig.savefig(f"{self.path_save}/vv_overlaid_zoom_on_results_{type_data}.png")
768
+ else:
769
+ fig.savefig(f"{self.path_save}/vv_overlaid_{type_data}.png")
770
+
771
+ return fig, ax
772
+
773
+ def plot_vv_quality(self, cmap: str = "viridis", type_data: str = "obs", block_plot: bool = True):
774
+ """
775
+ Plot error on top of velocity vx and vy.
776
+
777
+ :param cmap: [str] [default is 'viridis''] --- Color map used to mark the errors in the plots
778
+ :param type_data: [str] [default is 'obs'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
779
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting
780
+ :param vminmax: List[int] [default is None] --- Min and max values for the y-axis of the plots
781
+
782
+ :return fig, ax: Axis and Figure of the plots
783
+ """
784
+
785
+ assert "errorx" in self.dataobs.dataf.columns and "errory" in self.dataobs.dataf.columns, (
786
+ "'errorx' and/or 'errory' values are missing in the data, impossible to plot the errors"
787
+ )
788
+
789
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
790
+
791
+ qualityx = data.dataf["errorx"]
792
+ qualityy = data.dataf["errory"]
793
+ qualityv = np.sqrt(
794
+ (qualityx / data.dataf["vx"] * qualityx) ** 2 + (qualityy / data.dataf["vy"] * qualityy) ** 2
795
+ )
796
+
797
+ fig, ax = plt.subplots(figsize=self.figsize)
798
+ # First subplot
799
+ ax.set_ylabel(f"Vx [{self.unit}]", fontsize=14)
800
+ scat = ax.scatter(data.dataf["date_cori"], data.dataf["vv"], c=qualityv, s=5, cmap=cmap)
801
+ cbar = fig.colorbar(scat, ax=ax, orientation="horizontal", pad=0.2) # Increased pad for spacing
802
+ cbar.set_label("Errors [m/y]", fontsize=14)
803
+ # Adjustments
804
+ plt.subplots_adjust(hspace=0.5, bottom=0.3) # Increase hspace and bottom padding
805
+ fig.suptitle("Error associated to the velocity data", y=0.98, fontsize=16) # Adjusted title position
806
+ # Use tight layout
807
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
808
+ if self.show:
809
+ plt.show(block=block_plot)
810
+ if self.save:
811
+ fig.savefig(f"{self.path_save}/vxvy_quality_bas_{type_data}.png")
812
+
813
+ return fig, ax
814
+
815
+ def plot_vx_vy_quality(self, cmap: str = "viridis", type_data: str = "obs", block_plot: bool = True):
816
+ """
817
+ Plot error on top of velocity magnitude vv
818
+
819
+ :param cmap: [str] [default is 'viridis''] --- Color map used to mark the errors in the plots
820
+ :param type_data: [str] [default is 'obs'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
821
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting
822
+
823
+ :return fig, ax: Axis and Figure of the plots
824
+ """
825
+
826
+ assert "errorx" in self.dataobs.dataf.columns and "errory" in self.dataobs.dataf.columns, (
827
+ "'errorx' and/or 'errory' values are missing in the data, impossible to plot the errors"
828
+ )
829
+
830
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
831
+
832
+ qualityx = data.dataf["errorx"]
833
+ qualityy = data.dataf["errory"]
834
+
835
+ fig, ax = plt.subplots(2, 1, figsize=self.figsize)
836
+ # First subplot
837
+ ax[0].set_ylabel(f"Vx [{self.unit}]", fontsize=14)
838
+ scat = ax[0].scatter(data.dataf["date_cori"], data.dataf["vx"], c=qualityx, s=5, cmap=cmap)
839
+ cbar = fig.colorbar(scat, ax=ax[0], orientation="horizontal", pad=0.2) # Increased pad for spacing
840
+ cbar.set_label("Errors [m/y]", fontsize=14)
841
+
842
+ # Second subplot
843
+ ax[1].set_ylabel(f"Vy [{self.unit}]", fontsize=14)
844
+ scat = ax[1].scatter(data.dataf["date_cori"], data.dataf["vy"], c=qualityy, s=5, cmap=cmap)
845
+ cbar = fig.colorbar(scat, ax=ax[1], orientation="horizontal", pad=0.2) # Increased pad for spacing
846
+ cbar.set_label("Errors [m/y]", fontsize=14)
847
+
848
+ # Adjustments
849
+ plt.subplots_adjust(hspace=0.5, bottom=0.3) # Increase hspace and bottom padding
850
+ fig.suptitle("Error associated to the velocity data", y=0.98, fontsize=16) # Adjusted title position
851
+
852
+ # Use tight layout
853
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
854
+
855
+ if self.show:
856
+ plt.show(block=block_plot)
857
+ if self.save:
858
+ fig.savefig(f"{self.path_save}/vxvy_quality_bas_{type_data}.png")
859
+
860
+ return fig, ax
861
+
862
+ def plot_direction(
863
+ self, color: str = "orange", type_data: str = "obs", block_plot: bool = True, plot_mean: bool = True
864
+ ):
865
+ """
866
+ Plot the direction of the velocities for each of the data at this point.
867
+
868
+ :param color: [str] [default is 'orange'] --- Color used for the plot
869
+ :param type_data: [str] [default is 'obs'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
870
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
871
+ :param plot_mean: [bool] [default is True] --- If True, plot the mean velocity direction
872
+ :return fig, ax: Axis and Figure of the plot
873
+ """
874
+
875
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
876
+
877
+ directionm, directionm_mean = self.get_direction(data)
878
+ fig, ax = plt.subplots(figsize=self.figsize)
879
+ ax.plot(data.dataf["date_cori"], directionm, linestyle="", marker="o", markersize=2, color=color, label=label)
880
+ if plot_mean:
881
+ ax.hlines(
882
+ directionm_mean,
883
+ np.min(data.dataf["date_cori"]),
884
+ np.max(data.dataf["date_cori"]),
885
+ label=f"Mean direction of {label}",
886
+ )
887
+ ax.set_ylim(0, 360)
888
+ ax.set_ylabel("Direction [°]")
889
+ ax.set_xlabel("Central Dates")
890
+ plt.subplots_adjust(bottom=0.25)
891
+ ax.legend(loc="lower left", bbox_to_anchor=(0, -0.4), ncol=2, fontsize=14)
892
+ fig.suptitle("Direction of the observations", y=0.95, fontsize=16)
893
+
894
+ if self.show:
895
+ plt.show(block=block_plot)
896
+ if self.save:
897
+ fig.savefig(f"{self.path_save}/direction_{type_data}.png")
898
+
899
+ return fig, ax
900
+
901
+ def plot_direction_overlaid(
902
+ self,
903
+ colors: List[str] = ["orange", "blue"],
904
+ type_data: str = "interp",
905
+ block_plot: bool = True,
906
+ plot_mean: bool = True,
907
+ ):
908
+ """
909
+ Plot the velocity direction of inverted/interpolated results, overlaying the velocity direction of the observations (raw data).
910
+
911
+ :param colors: [List[str]] [default is ['orange', 'blue']] --- List of the colors used for the plot (first : raw data, second : overlaying data)
912
+ :param type_data: [str] [default is 'invert'] --- If 'obs' dataf corresponds to obsevations, if 'invert', it corresponds to inverted velocity
913
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
914
+ :param plot_mean: [bool] [default is True] --- If True, plot the mean velocity direction
915
+
916
+ :return fig, ax: Axis and Figure of the plot
917
+ """
918
+
919
+ data, label = self.get_dataf_invert_or_obs_or_interp(type_data)
920
+
921
+ show = copy.copy(self.show)
922
+ save = copy.copy(self.save)
923
+ self.show, self.save = False, False
924
+ fig, ax = self.plot_direction(color=colors[0], type_data="obs", plot_mean=plot_mean)
925
+ self.show, self.save = show, save
926
+
927
+ directionm, directionm_mean = self.get_direction(data)
928
+
929
+ ax.plot(
930
+ data.dataf["date_cori"], directionm, linestyle="", marker="o", markersize=2, color=colors[1], label=label
931
+ )
932
+ ax.set_ylim(0, 360)
933
+ ax.set_ylabel("Direction [°]", fontsize=14)
934
+ ax.set_xlabel("Central Dates", fontsize=14)
935
+ ax.legend(loc="lower left", bbox_to_anchor=(0, -0.4), ncol=2, fontsize=14)
936
+ fig.suptitle(
937
+ f"Direction of the {'interpolated' if type_data == 'interp' else 'inverted'} results, along with raw data direction",
938
+ y=0.95,
939
+ fontsize=16,
940
+ )
941
+
942
+ if self.show:
943
+ plt.show(block=block_plot)
944
+ if self.save:
945
+ fig.savefig(f"{self.path_save}/direction_overlaid_{type_data}.png")
946
+
947
+ return fig, ax
948
+
949
+ return fig, ax
950
+
951
+ def plot_quality_metrics(self, color: str = "orange"):
952
+ """
953
+ Plot quality metrics on top of velocity magnitude. It can be the number of observations used for each estimation, and/or the confidence intervals.
954
+ :param color: [str] [default is 'orange'] --- Color used for the plot
955
+ :return:
956
+ """
957
+
958
+ dataf, label = self.get_dataf_invert_or_obs_or_interp(type_data="interp")
959
+ data = dataf.dataf.dropna(subset=["vx", "vy"]) # drop rows where with no velocity values
960
+
961
+ assert "error_x" and "x_count" not in data.columns, (
962
+ "No quality metrics to display, please re run ticoi using the options Error_propagation or X_contribution"
963
+ )
964
+
965
+ if "error_x" in data.columns:
966
+ data["error_x"] = np.sqrt(data["error_x"])
967
+ data["error_y"] = np.sqrt(data["error_y"])
968
+ data["error_v"] = np.sqrt(
969
+ (data["vx"] / data["vv"] * data["error_x"]) ** 2 + (data["vy"] / data["vv"] * data["error_y"]) ** 2
970
+ )
971
+
972
+ data["confidence_x"] = data["sigma0"].iloc[2] * data["error_x"]
973
+ data["confidence_y"] = data["sigma0"].iloc[3] * data["error_y"]
974
+ data["confidence_v"] = np.nanmean(data["sigma0"].iloc[2:4]) * data["error_v"]
975
+
976
+ if "xcount_x" in data.columns:
977
+ xcount_mean = np.nanmean([data["xcount_x"], data["xcount_y"]], axis=0) # Mean of xcount_x and xcount_y
978
+ max_xcount = int(np.max(xcount_mean))
979
+ if max_xcount > 100:
980
+ bounds = [0, 100, 1000, max_xcount]
981
+ cmap = mcolors.ListedColormap(["lightcoral", "red", "darkred"]) # Light red, red, dark red
982
+ # Boundaries for color ranges
983
+ else:
984
+ bounds = [0, 100, max_xcount]
985
+ cmap = mcolors.ListedColormap(["lightcoral", "red"]) # Light red, red, dark red
986
+
987
+ norm = mcolors.BoundaryNorm(bounds, cmap.N) # Apply the custom colormap to the scatter plot based on xcount
988
+
989
+ fig, ax = plt.subplots(figsize=(10, 6))
990
+ if "error_x" in data.columns:
991
+ if "xcount_x" not in data.columns:
992
+ ax.plot(
993
+ data["date_cori"],
994
+ data["vv"],
995
+ linestyle="",
996
+ zorder=1,
997
+ marker="o",
998
+ lw=0.7,
999
+ markersize=2,
1000
+ color=color,
1001
+ label=label,
1002
+ )
1003
+ # Plot confidence interval using fill_between
1004
+ ax.fill_between(
1005
+ data["date_cori"],
1006
+ data["vv"] - data["confidence_v"],
1007
+ data["vv"] + data["confidence_v"],
1008
+ color="purple",
1009
+ alpha=0.4,
1010
+ )
1011
+ # Create custom legend entries for confidence interval
1012
+ conf_legend = malines.Line2D([], [], color="purple", alpha=0.4, lw=6, label="95% confidence interval")
1013
+ if "xcount_x" in data.columns:
1014
+ plt.subplots_adjust(bottom=-0.01)
1015
+ # Add the legends for confidence interval and GPS
1016
+ ax.legend(
1017
+ [conf_legend],
1018
+ ["95% confidence interval"],
1019
+ loc="upper center",
1020
+ bbox_to_anchor=(0.5, -0.05),
1021
+ fontsize=15,
1022
+ ncol=3,
1023
+ markerscale=1.5,
1024
+ )
1025
+
1026
+ if "xcount_x" in data.columns:
1027
+ scat = ax.scatter(data["date_cori"], data["vv"], c=xcount_mean, cmap=cmap, norm=norm, s=7)
1028
+ # Add the colorbar for xcount
1029
+ cbar = fig.colorbar(scat, ax=ax, boundaries=bounds, orientation="horizontal", pad=0.15, shrink=0.7)
1030
+ cbar.set_label("Number of image-pair velocities used", fontsize=14)
1031
+
1032
+ ax.set_ylabel("Velocity magnitude [m/y]", fontsize=18)
1033
+ # Show plot if specified
1034
+ if self.show:
1035
+ plt.show(block=False)
1036
+
1037
+ # Save the figure
1038
+ if self.save:
1039
+ fig.savefig(f"{self.path_save}/confidence_intervals_and_quality.png")
1040
+
1041
+ return fig, ax
1042
+
1043
+ # %%========================================================================= #
1044
+ # PLOTS ABOUT INVERSION RESULTS #
1045
+ # =========================================================================%% #
1046
+
1047
+ def plot_xcount_vx_vy(self, cmap: str = "viridis", block_plot: bool = True):
1048
+ """
1049
+ Plot the observation contribution to the inversion on top of velocities x and y components.
1050
+
1051
+ :param cmap: [str] [default is 'rainbow] --- Color map used to mark the xcount values in the plots.
1052
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
1053
+
1054
+ :return fig, ax: Axis and Figure of the plot
1055
+ """
1056
+
1057
+ assert self.datainvert is not None, (
1058
+ "No inverted data found, think of loading the results of an inversion to this pixel_class before calling plot_xcount_vx_vy()"
1059
+ )
1060
+ assert "xcount_x" in self.datainvert.dataf.columns and "xcount_y" in self.datainvert.dataf.columns, (
1061
+ "'xcount_x' and/or 'xount_y' values are missing in the data, impossible to plot the xcount values"
1062
+ )
1063
+
1064
+ fig, ax = plt.subplots(2, 1, figsize=self.figsize)
1065
+ ax[0].set_ylabel(f"Vx [{self.unit}]", fontsize=14)
1066
+ ax[0].scatter(
1067
+ self.datainvert.dataf["date_cori"],
1068
+ self.datainvert.dataf["vx"],
1069
+ c=self.datainvert.dataf["xcount_x"],
1070
+ s=8,
1071
+ cmap=cmap,
1072
+ label="Y_contribution",
1073
+ )
1074
+ ax[1].set_ylabel(f"Vy [{self.unit}]", fontsize=14)
1075
+ ax[1].set_xlabel("Central dates", fontsize=14)
1076
+ scat = ax[1].scatter(
1077
+ self.datainvert.dataf["date_cori"],
1078
+ self.datainvert.dataf["vy"],
1079
+ c=self.datainvert.dataf["xcount_y"],
1080
+ s=8,
1081
+ cmap=cmap,
1082
+ )
1083
+ plt.subplots_adjust(bottom=0.1)
1084
+ cbar = fig.colorbar(scat, ax=ax.ravel().tolist(), orientation="horizontal", pad=0.15)
1085
+ cbar.set_label("Amount of contributing observations", fontsize=14)
1086
+ fig.suptitle(
1087
+ "Contribution of the observations to the resulting inverted velocity x and y components",
1088
+ y=0.95,
1089
+ fontsize=16,
1090
+ )
1091
+
1092
+ if self.show:
1093
+ plt.show(block=block_plot)
1094
+ if self.save:
1095
+ fig.savefig(f"{self.path_save}/X_dates_contribution_vx_vy.png")
1096
+
1097
+ return fig, ax
1098
+
1099
+ def plot_xcount_vv(self, cmap: str = "viridis", block_plot: bool = True):
1100
+ """
1101
+ Plot the observation contribution to the inversion on top of the velocity magnitude.
1102
+
1103
+ :param cmap: [str] [default is 'rainbow''] --- Color map used in the plots
1104
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
1105
+
1106
+ :return fig, ax: Axis and Figure of the plot
1107
+ """
1108
+
1109
+ assert self.datainvert is not None, (
1110
+ "No inverted data found, think of loading the results of an inversion to this pixel_class before calling plot_xcount_vv()"
1111
+ )
1112
+ assert "xcount_x" in self.datainvert.dataf.columns and "xcount_y" in self.datainvert.dataf.columns, (
1113
+ "'xcount_x' and/or 'xount_y' values are missing in the data, impossible to plot the xcount values"
1114
+ )
1115
+
1116
+ fig, ax = plt.subplots(figsize=self.figsize)
1117
+ ax.set_ylabel(f"Velocity magnitude [{self.unit}]", fontsize=14)
1118
+ ax.set_xlabel("Central dates", fontsize=14)
1119
+ scat = ax.scatter(
1120
+ self.datainvert.dataf["date_cori"],
1121
+ self.datainvert.dataf["vv"],
1122
+ c=(self.datainvert.dataf["xcount_x"] + self.datainvert.dataf["xcount_y"]) / 2,
1123
+ s=8,
1124
+ cmap=cmap,
1125
+ )
1126
+ # Adding a colorbar for the scatter plot
1127
+ cbar = plt.colorbar(scat, ax=ax, pad=0.02)
1128
+ cbar.set_label("Amount of contributing observations", fontsize=14)
1129
+ plt.subplots_adjust(bottom=0.2)
1130
+ fig.suptitle("Contribution of the observations to the resulting inverted velocities", y=0.95, fontsize=16)
1131
+
1132
+ if self.show:
1133
+ plt.show(block=block_plot)
1134
+ if self.save:
1135
+ fig.savefig(f"{self.path_save}/X_dates_contribution_vv.png")
1136
+
1137
+ return fig, ax
1138
+
1139
+ def plot_weights_inversion(self, cmap: str = "plasma_r", block_plot: bool = True):
1140
+ """
1141
+ Plot initial and final weights used in the inversion.
1142
+
1143
+ :param cmap: [str] [default is 'plasma_r'] --- Color map used in the plots
1144
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
1145
+
1146
+ :return ax_f, fig_f, ax_l, fig_l: Axis and Figure of the plots (weights from f: the first inversion, l: the last inversion)
1147
+ """
1148
+
1149
+ assert self.datainvert is not None, (
1150
+ "No inverted data found, think of loading the results of an inversion to this pixel_class before calling plot_xcount_vv()"
1151
+ )
1152
+
1153
+ ## ----------------------- Weights used during the first inversion ------------------------- ##
1154
+ fig_f, ax_f = plt.subplots(2, 1, figsize=(8, 4))
1155
+ ax_f[0].set_ylabel(f"Vx [{self.unit}]", fontsize=14)
1156
+ ax_f[0].set_xticklabels([])
1157
+ scat1 = ax_f[0].scatter(
1158
+ self.dataobs.dataf["date_cori"],
1159
+ self.dataobs.dataf["vx"],
1160
+ c=abs(self.dataobs.dataf["weightinix"]),
1161
+ s=5,
1162
+ cmap=cmap,
1163
+ edgecolors="k",
1164
+ linewidth=0.1,
1165
+ )
1166
+ ax_f[1].set_ylabel(f"Vy [{self.unit}]", fontsize=14)
1167
+ ax_f[1].set_xlabel("Central dates", fontsize=14)
1168
+ scat2 = ax_f[1].scatter(
1169
+ self.dataobs.dataf["date_cori"],
1170
+ self.dataobs.dataf["vx"],
1171
+ c=abs(self.dataobs.dataf["weightiniy"]),
1172
+ s=5,
1173
+ cmap=cmap,
1174
+ edgecolors="k",
1175
+ linewidth=0.1,
1176
+ )
1177
+ plt.subplots_adjust(bottom=0.32)
1178
+ legend1 = ax_f[1].legend(
1179
+ *scat1.legend_elements(num=5),
1180
+ loc="lower left",
1181
+ bbox_to_anchor=(0.05, -1.25),
1182
+ ncol=3,
1183
+ title="Initial weights for Vx",
1184
+ )
1185
+ legend2 = ax_f[1].legend(
1186
+ *scat2.legend_elements(num=5),
1187
+ loc="lower right",
1188
+ bbox_to_anchor=(0.95, -1.25),
1189
+ ncol=3,
1190
+ title="Initial weights for Vy",
1191
+ )
1192
+ ax_f[1].add_artist(legend1)
1193
+ ax_f[1].add_artist(legend2)
1194
+ fig_f.suptitle("Initial weights before the inversion", y=0.95, fontsize=16)
1195
+
1196
+ if self.show:
1197
+ plt.show(block=block_plot)
1198
+ if self.save:
1199
+ fig_f.savefig(f"{self.path_save}/weightini_vx_vy.png")
1200
+
1201
+ ## ------------------------ Weights used during the last inversion ------------------------- ##
1202
+ fig_l, ax_l = plt.subplots(2, 1, figsize=(8, 4))
1203
+ ax_l[0].set_ylabel(f"Vx [{self.unit}]", fontsize=14)
1204
+ ax_l[0].set_xticklabels([])
1205
+ scat1 = ax_l[0].scatter(
1206
+ self.dataobs.dataf["date_cori"],
1207
+ self.dataobs.dataf["vx"],
1208
+ c=abs(self.dataobs.dataf["weightlastx"]),
1209
+ s=5,
1210
+ cmap=cmap,
1211
+ edgecolors="k",
1212
+ linewidth=0.1,
1213
+ )
1214
+ ax_l[1].set_ylabel(f"Vy [{self.unit}]", fontsize=14)
1215
+ ax_l[1].set_xlabel("Central dates", fontsize=14)
1216
+ scat2 = ax_l[1].scatter(
1217
+ self.dataobs.dataf["date_cori"],
1218
+ self.dataobs.dataf["vx"],
1219
+ c=abs(self.dataobs.dataf["weightlasty"]),
1220
+ s=5,
1221
+ cmap=cmap,
1222
+ edgecolors="k",
1223
+ linewidth=0.1,
1224
+ )
1225
+ plt.subplots_adjust(bottom=0.32)
1226
+ legend1 = ax_l[1].legend(
1227
+ *scat1.legend_elements(num=5),
1228
+ loc="lower left",
1229
+ bbox_to_anchor=(0.05, -1.25),
1230
+ ncol=3,
1231
+ title="Final weights for Vx",
1232
+ )
1233
+ legend2 = ax_l[1].legend(
1234
+ *scat2.legend_elements(num=5),
1235
+ loc="lower right",
1236
+ bbox_to_anchor=(0.95, -1.25),
1237
+ ncol=3,
1238
+ title="Final weights for Vy",
1239
+ )
1240
+ ax_l[1].add_artist(legend1)
1241
+ ax_l[1].add_artist(legend2)
1242
+ fig_l.suptitle("Final weights after the inversion", y=0.95, fontsize=16)
1243
+
1244
+ if self.show:
1245
+ plt.show(block=block_plot)
1246
+ if self.save:
1247
+ fig_l.savefig(f"{self.path_save}/weightlast_vx_vy.png")
1248
+
1249
+ return ax_f, fig_f, ax_l, fig_l
1250
+
1251
+ def plot_residuals(self, log_scale: bool = False, block_plot: bool = True):
1252
+ """
1253
+ Statistics about the residuals from the inversion:
1254
+ - Plot of the final residuals overlaid in colors on vx and vy measurements ('residuals_vx_vy_final_residual.png').
1255
+ - Plot of the reconstructed velocity observations (from AX) overlaid on the original velocity observations ('residuals_vx_vy_mismatch.png').
1256
+ - Comparison of residuals according to the temporal baseline (residuals_tempbaseline.png),
1257
+ - the type of sensor and authors (residuals_author_abs.png,residuals_vy_author.png,residuals_vx_author_abs.png),
1258
+ - and the quality indicators (residuals_quality.png).
1259
+
1260
+ :param log_scale: [bool] [default is False] --- if True, plot the figure in a log scale
1261
+ :param block_plot: [bool] [default is True] --- If True, the plot persists on the screen until the user manually closes it. If False, it disappears instantly after plotting.
1262
+ """
1263
+
1264
+ assert self.datainvert is not None, (
1265
+ "No inverted data found, think of loading the results of an inversion to this pixel_class before calling plot_xcount_vv()"
1266
+ )
1267
+ assert self.A is not None, "Please provide A (design matrix) when loading the pixel_class"
1268
+
1269
+ dataf = self.dataobs.dataf.replace("L. Charrier, J. Mouginot, R.Millan, A.Derkacheva", "IGE")
1270
+ dataf = dataf.replace("S. Leinss, L. Charrier", "Leinss")
1271
+
1272
+ dataf["abs_residux"] = abs(dataf["residux"])
1273
+ dataf["abs_residuy"] = abs(dataf["residuy"])
1274
+
1275
+ dataf = dataf.rename(columns={"author": "Author"})
1276
+
1277
+ conversion = self.get_conversion()
1278
+
1279
+ ###RECONSTRUCTION PLOT : reconstruct the observation from AX
1280
+ Y_reconstruct_x = (
1281
+ np.dot(self.A, self.datainvert.dataf["vx"] * self.datainvert.dataf["temporal_baseline"] / conversion)
1282
+ / self.dataobs.dataf["temporal_baseline"]
1283
+ * conversion
1284
+ )
1285
+ Y_reconstruct_y = (
1286
+ np.dot(self.A, self.datainvert.dataf["vy"] * self.datainvert.dataf["temporal_baseline"] / conversion)
1287
+ / self.dataobs.dataf["temporal_baseline"]
1288
+ * conversion
1289
+ )
1290
+
1291
+ show = copy.copy(self.show)
1292
+ save = copy.copy(self.save)
1293
+ self.show, self.save = False, False
1294
+ fig, ax = self.plot_vx_vy(type_data="obs")
1295
+ self.show, self.save = show, save
1296
+
1297
+ # fig, ax = plt.subplots(2, 1, figsize=(8, 4))
1298
+ ax[0].plot(
1299
+ self.dataobs.dataf["date_cori"],
1300
+ Y_reconstruct_x,
1301
+ linestyle="",
1302
+ marker="o",
1303
+ color="r",
1304
+ markersize=3,
1305
+ alpha=0.2,
1306
+ ) # Display the vx components
1307
+ ax[0].errorbar(
1308
+ self.dataobs.dataf["date_cori"],
1309
+ Y_reconstruct_x,
1310
+ xerr=self.dataobs.dataf["offset_bar"],
1311
+ color="r",
1312
+ alpha=0.2,
1313
+ fmt=",",
1314
+ zorder=1,
1315
+ )
1316
+ ax[0].set_ylabel(f"Vx [{self.unit}]", fontsize=18)
1317
+ ax[1].plot(
1318
+ self.dataobs.dataf["date_cori"],
1319
+ Y_reconstruct_y,
1320
+ linestyle="",
1321
+ marker="o",
1322
+ color="r",
1323
+ markersize=3,
1324
+ alpha=0.2,
1325
+ label="Reconstructed Data",
1326
+ ) # Display the vy components
1327
+ ax[1].errorbar(
1328
+ self.dataobs.dataf["date_cori"],
1329
+ Y_reconstruct_y,
1330
+ xerr=self.dataobs.dataf["offset_bar"],
1331
+ color="r",
1332
+ alpha=0.3,
1333
+ fmt=",",
1334
+ zorder=1,
1335
+ )
1336
+ ax[1].legend(bbox_to_anchor=(0.55, -0.3), ncol=3, fontsize=15)
1337
+ if self.show:
1338
+ plt.show()
1339
+ if self.save:
1340
+ fig.savefig(f"{self.path_save}/residuals_vx_vy_mismatch.png")
1341
+
1342
+ ###RESIDUALS FROM THE LAST INVERSION
1343
+ fig, ax = plt.subplots(2, 1, figsize=(8, 4))
1344
+ ax[0].set_ylabel(f"Vx [{self.unit}]")
1345
+ scat1 = ax[0].scatter(
1346
+ self.dataobs.dataf["date_cori"],
1347
+ self.dataobs.dataf["vx"],
1348
+ c=abs(self.dataobs.dataf["residux"]),
1349
+ s=5,
1350
+ cmap="plasma_r",
1351
+ edgecolors="k",
1352
+ linewidth=0.1,
1353
+ )
1354
+ ax[1].set_ylabel(f"Vy [{self.unit}]")
1355
+ scat2 = ax[1].scatter(
1356
+ self.dataobs.dataf["date_cori"],
1357
+ self.dataobs.dataf["vy"],
1358
+ c=abs(self.dataobs.dataf["residuy"]),
1359
+ s=5,
1360
+ cmap="plasma_r",
1361
+ edgecolors="k",
1362
+ linewidth=0.1,
1363
+ )
1364
+ plt.subplots_adjust(bottom=0.3)
1365
+ legend1 = ax[1].legend(
1366
+ *scat1.legend_elements(num=5),
1367
+ loc="lower left",
1368
+ bbox_to_anchor=(0.05, 0),
1369
+ bbox_transform=fig.transFigure,
1370
+ ncol=3,
1371
+ title="Absolute residual Vx",
1372
+ )
1373
+ legend2 = ax[1].legend(
1374
+ *scat2.legend_elements(num=5),
1375
+ loc="lower right",
1376
+ bbox_to_anchor=(0.95, 0),
1377
+ bbox_transform=fig.transFigure,
1378
+ ncol=3,
1379
+ title="Absolute residual Vy",
1380
+ )
1381
+ ax[1].add_artist(legend1)
1382
+ ax[1].add_artist(legend2)
1383
+ if self.show:
1384
+ plt.show(block=False)
1385
+ if self.save:
1386
+ fig.savefig(f"{self.path_save}/residuals_vx_vy_final_residual.png")
1387
+
1388
+ ###RESIDUALS FOR VX AND VY, ACCORDING TO THE SENSOR
1389
+ ax = sns.catplot(data=dataf, x="sensor", y="abs_residux", hue="Author", kind="box")
1390
+ ax.set(xlabel="Sensor", ylabel="Absolute residual vx [m/y]")
1391
+ if self.save:
1392
+ plt.savefig(f"{self.path_save}/residuals_vx_author_abs.png")
1393
+ if self.show:
1394
+ plt.show()
1395
+
1396
+ ax = sns.catplot(data=dataf, x="sensor", y="abs_residuy", hue="Author", kind="box")
1397
+ ax.set(xlabel="Sensor", ylabel="Absolute residual vy [m/y]")
1398
+ if self.save:
1399
+ plt.savefig(f"{self.path_save}/residuals_author_abs.png")
1400
+ if self.show:
1401
+ plt.show()
1402
+
1403
+ ###RESIDUALS FROM VX AND VY, ACCORDING TO THE AUTHOR
1404
+ ax = sns.catplot(data=dataf, x="sensor", y="residux", hue="Author", kind="box")
1405
+ ax.set(xlabel="Sensor", ylabel="Residual vx [m/y]")
1406
+ if self.save:
1407
+ plt.savefig(f"{self.path_save}/residuals_vx_author.png")
1408
+ if self.show:
1409
+ plt.show()
1410
+
1411
+ ax = sns.catplot(data=dataf, x="sensor", y="residuy", hue="Author", kind="box")
1412
+ ax.set(xlabel="Sensor", ylabel="Residual vy [m/y]")
1413
+ if self.save:
1414
+ plt.savefig(f"{self.path_save}/residuals_vy_author.png")
1415
+ if self.show:
1416
+ plt.show()
1417
+
1418
+ ###RESIDUALS FROM VX AND VY, ACCORDING TO THE QUALITY INDICATOR
1419
+ fig, ax = plt.subplots(2, 1, figsize=self.figsize)
1420
+ color_list = ["b", "m", "k", "g", "m"]
1421
+ for i, auth in enumerate(dataf["Author"].unique()):
1422
+ ax[0].plot(
1423
+ dataf[dataf["Author"] == auth]["weightinix"],
1424
+ dataf[dataf["Author"] == auth]["residux"],
1425
+ linestyle="",
1426
+ marker="o",
1427
+ color=color_list[i],
1428
+ markersize=3,
1429
+ )
1430
+ ax[1].plot(
1431
+ dataf[dataf["Author"] == auth]["weightiniy"],
1432
+ dataf[dataf["Author"] == auth]["residuy"],
1433
+ linestyle="",
1434
+ marker="o",
1435
+ color=color_list[i],
1436
+ markersize=3,
1437
+ label=auth,
1438
+ )
1439
+ if log_scale:
1440
+ ax[0].set_yscale("log")
1441
+ ax[1].set_yscale("log")
1442
+ ax[0].set_ylabel(f"Residual vx [{self.unit}]", fontsize=16)
1443
+ ax[1].set_ylabel(f"Residual vy [{self.unit}]", fontsize=16)
1444
+ ax[1].set_xlabel("Quality indicator", fontsize=16)
1445
+ plt.subplots_adjust(bottom=0.2)
1446
+ ax[1].legend(loc="lower left", bbox_to_anchor=(0.12, 0), bbox_transform=fig.transFigure, fontsize=12, ncol=5)
1447
+ if self.show:
1448
+ plt.show()
1449
+ if self.save:
1450
+ if log_scale:
1451
+ fig.savefig(f"{self.path_save}/residu_qualitylog.png")
1452
+ else:
1453
+ fig.savefig(f"{self.path_save}/residuals_quality.png")
1454
+
1455
+ ###RESIDUALS FROM VX AND VY, ACCORDING TO THE TEMPORAL BASELINE
1456
+ fig, ax = plt.subplots(2, 1, figsize=self.figsize)
1457
+ color_list = ["b", "m", "k", "g", "m"]
1458
+ for i, auth in enumerate(dataf["Author"].unique()):
1459
+ ax[0].plot(
1460
+ np.array(dataf["temporal_baseline"])[dataf["Author"] == auth] * 2,
1461
+ dataf[dataf["Author"] == auth]["residux"],
1462
+ linestyle="",
1463
+ marker="o",
1464
+ color=color_list[i],
1465
+ markersize=3,
1466
+ )
1467
+ ax[1].plot(
1468
+ np.array(dataf["temporal_baseline"])[dataf["Author"] == auth] * 2,
1469
+ dataf[dataf["Author"] == auth]["residuy"],
1470
+ linestyle="",
1471
+ marker="o",
1472
+ color=color_list[i],
1473
+ markersize=3,
1474
+ label=auth,
1475
+ )
1476
+ if log_scale:
1477
+ ax[0].set_yscale("log")
1478
+ ax[1].set_yscale("log")
1479
+ ax[0].set_ylabel(f"Residual vx [{self.unit}]", fontsize=16)
1480
+ ax[1].set_ylabel(f"Residual vy [{self.unit}]", fontsize=16)
1481
+ ax[1].set_xlabel("Temporal baseline [days]", fontsize=16)
1482
+ plt.subplots_adjust(bottom=0.2)
1483
+ ax[1].legend(loc="lower left", bbox_to_anchor=(0.12, 0), bbox_transform=fig.transFigure, fontsize=12, ncol=5)
1484
+ if self.show:
1485
+ plt.show()
1486
+ if self.save:
1487
+ if log_scale:
1488
+ fig.savefig(f"{self.path_save}/residu_tempbaseline_log.png")
1489
+ else:
1490
+ fig.savefig(f"{self.path_save}/residuals_tempbaseline.png")
1491
+
1492
+ # %%========================================================================= #
1493
+ # PLOTS ABOUT THE SEASONALITY #
1494
+ # =========================================================================%% #
1495
+
1496
+ def plot_filtered_results(self, filt: str | None = None, impose_frequency: bool = True):
1497
+ """
1498
+ Plot the filtered TICOI results, with a given filter.
1499
+
1500
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
1501
+ :param impose_frequency: [bool] [default is True] --- If True, impose the frequency to 1/365.25 days-1 (one year seasonality). If False, look for the best matching frequency too, using the Fourier Transform in the first place
1502
+
1503
+ :return fig, ax: Axis and Figure of the plot
1504
+ """
1505
+
1506
+ vv_filt, vv_c, dates_c, dates = self.get_filtered_results(filt=filt)
1507
+
1508
+ if impose_frequency:
1509
+ fig, ax = plt.subplots(nrows=1, ncols=1, figsize=self.figsize)
1510
+ axe = ax
1511
+ else:
1512
+ fig, ax = plt.subplots(nrows=2, ncols=1, figsize=self.figsize)
1513
+ axe = ax[0]
1514
+
1515
+ axe.plot(dates_c, vv_c, "blue", label="Before filtering")
1516
+ axe.plot(dates_c, vv_filt, "red", label="After filtering")
1517
+ axe.set_xlabel("Centered velocity [m/y]", fontsize=16)
1518
+ axe.set_ylabel("Central date", fontsize=16)
1519
+ axe.set_title("Effect of filtering", fontsize=16)
1520
+ axe.legend(loc="lower left")
1521
+
1522
+ if not impose_frequency:
1523
+ ax[1].plot(dates_c, vv_filt * signal.windows.hann(len(dates)), "blue", label="With Hanning windowing")
1524
+ ax[1].plot(dates_c, vv_filt, "black", label="Without windowing")
1525
+ ax[1].set_xlabel("Centered velocity [m/y]", fontsize=16)
1526
+ ax[1].set_ylabel("Central date", fontsize=16)
1527
+ ax[1].set_title("Effect of Hanning windowing", fontsize=16)
1528
+ ax[1].legend(loc="best")
1529
+
1530
+ fig.tight_layout()
1531
+
1532
+ if self.show:
1533
+ plt.show()
1534
+ if self.save:
1535
+ fig.savefig(f"{self.path_save}/filtered_results.png")
1536
+
1537
+ return fig, ax
1538
+
1539
+ def plot_TF(self, filt=None, verbose=False):
1540
+ """
1541
+ Plot the Fourier Transform (TF) of the TICOI results after filtering with a given filter.
1542
+
1543
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
1544
+ :param verbose:[bool] [default is False] --- If True, print the maximum and the amplitude of the TF
1545
+
1546
+ :return fig, ax: Axis and Figure of the plot
1547
+ """
1548
+
1549
+ vv_tf, vv_win_tf, freq, N = self.get_TF(filt=filt, verbose=verbose)
1550
+
1551
+ fig, ax = plt.subplots(nrows=1, ncols=1, figsize=self.figsize)
1552
+ ax.plot(freq, 2 / N * np.abs(vv_tf), "blue", label="TF without windowing")
1553
+ ax.plot(freq, 2 / N * np.abs(vv_win_tf), "red", label="TF after Hanning windowing")
1554
+ ax.vlines(
1555
+ [i / 365 for i in range(1, 4)],
1556
+ 0,
1557
+ 1.1 * 2 / N * max(np.max(np.abs(vv_tf)), np.max(np.abs(vv_win_tf))),
1558
+ color="black",
1559
+ label="365d periodicity",
1560
+ )
1561
+ ax.set_xlim([0, 0.01])
1562
+ ax.set_ylim([0, 1.1 * 2 / N * max(np.max(np.abs(vv_tf)), np.max(np.abs(vv_win_tf)))])
1563
+ ax.set_xlabel("Frequency [day-1]", fontsize=16)
1564
+ ax.set_ylabel("Amplitude [m/y]", fontsize=16)
1565
+ ax.legend(loc="best")
1566
+ ax.set_title("Fourier Transform of the TICOI-resulting velocities", fontsize=16)
1567
+
1568
+ if self.show:
1569
+ plt.show()
1570
+ if self.save:
1571
+ fig.savefig(f"{self.path_save}TF.png")
1572
+
1573
+ return fig, ax
1574
+
1575
+ def plot_best_matching_sinus(
1576
+ self,
1577
+ filt: str | None = None,
1578
+ impose_frequency: bool = True,
1579
+ raw_seasonality: bool = False,
1580
+ several_freq: int = 1,
1581
+ verbose: bool = False,
1582
+ ):
1583
+ """
1584
+ Plot the best matching sinus to the TICOI results (and to the raw data if required), by fixing the frequency to 1/365.25 days-1 or looking for the best matching one.
1585
+
1586
+ :param filt: [str | None] [default is None] --- Filter to be used ('highpass' for a highpass filtering removing the trend over several years, 'lowpass' to just respect Shannon criterium, or None to don't apply any filter)
1587
+ :param impose_frequency: [bool] [default is True] --- If True, impose the frequency to 1/365.25 days-1 (one year seasonality). If False, look for the best matching frequency too, using the Fourier Transform in the first place
1588
+ :param raw_seasonality: [bool] [default is False] --- Also look for the best matching sinus directly on the raw data
1589
+ :param several_freq: [int] [default is 1] --- Number of harmonics to be computed (combination of sinus at frequencies 1/365.25, 2/365.25, etc...). If 1, only compute the fundamental
1590
+ :param verbose: [bool] [default is False] --- If True, print the amplitude, the position of the maximum and the RMSE between the best matching sinus and the original data (TICOI results and raw data), and the best matching frequency if impose_frequency is False
1591
+
1592
+ :return fig, ax: Axis and Figure of the plots
1593
+ """
1594
+
1595
+ sine_f, popt, popt_raw, dates, vv_filt, stats, stats_raw = self.get_best_matching_sinus(
1596
+ filt=filt,
1597
+ impose_frequency=impose_frequency,
1598
+ raw_seasonality=raw_seasonality,
1599
+ several_freq=several_freq,
1600
+ verbose=verbose,
1601
+ )
1602
+
1603
+ sine = sine_f(dates[0], *popt, freqs=several_freq)
1604
+ f = popt[1] if not impose_frequency else 1 / 365.25
1605
+
1606
+ fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(12, 6))
1607
+ ax.plot(
1608
+ self.dataobs.dataf.index,
1609
+ self.dataobs.dataf["vv"],
1610
+ linestyle="",
1611
+ marker="x",
1612
+ markersize=2,
1613
+ color="orange",
1614
+ label="Raw data",
1615
+ )
1616
+ ax.plot(dates[1], self.datainterp.dataf["vv"], "black", alpha=0.6, label="TICOI velocities")
1617
+ if filt is not None:
1618
+ ax.plot(
1619
+ dates[1],
1620
+ vv_filt + np.mean(self.datainterp.dataf["vv"]),
1621
+ "red",
1622
+ alpha=0.6,
1623
+ label="Filtered TICOI velocities",
1624
+ )
1625
+ if impose_frequency and raw_seasonality:
1626
+ sine_raw = sine_f(dates[2], *popt_raw, freqs=several_freq) if impose_frequency else sine_f(dates[2], *popt)
1627
+ ax.plot(
1628
+ self.dataobs.dataf.index,
1629
+ sine_raw + self.dataobs.dataf["vv"].mean(),
1630
+ linewidth=3,
1631
+ color="forestgreen",
1632
+ label="Best matching sinus to raw data",
1633
+ )
1634
+ ax.plot(
1635
+ dates[1],
1636
+ sine + np.mean(self.datainterp.dataf["vv"]),
1637
+ color="deepskyblue",
1638
+ linewidth=3,
1639
+ label="Best matching sinus to TICOI results",
1640
+ )
1641
+ ax.vlines(
1642
+ pd.date_range(start=stats[0], end=self.datainterp.dataf["date2"].max(), freq=f"{int(1 / f)}D"),
1643
+ np.min(self.datainterp.dataf["vv"]),
1644
+ np.max(self.datainterp.dataf["vv"]),
1645
+ "black",
1646
+ label="Maximum (TICOI)",
1647
+ )
1648
+ ax.set_xlabel("Central dates", fontsize=16)
1649
+ ax.set_ylabel("Velocity", fontsize=16)
1650
+ ax.legend(loc="best")
1651
+ ax.set_title("Best matching sinus around an annual seasonality")
1652
+
1653
+ if self.show:
1654
+ plt.show()
1655
+ if self.save:
1656
+ fig.savefig(f"{self.path_save}matching_sine.png")
1657
+
1658
+ return fig, ax
1659
+
1660
+ def plot_annual_curves(
1661
+ self,
1662
+ normalize: bool = False,
1663
+ statistics: List[str] = [
1664
+ "min",
1665
+ "max",
1666
+ "mean",
1667
+ "median",
1668
+ "std",
1669
+ "amplitude",
1670
+ "max_day",
1671
+ "nb_peaks",
1672
+ "relative_max",
1673
+ ],
1674
+ cmap: str = "hsv",
1675
+ markers: List[str] = [".", "p", "s", "v", "D", "*", "x", "1", "+"],
1676
+ markers_size: List[int] = [5, 4, 3, 4, 3, 4, 4, 7, 4],
1677
+ verbose: bool = True,
1678
+ ):
1679
+ """
1680
+ Plot the velocity curves of each year on top of ones another and compute some statistics about it ().
1681
+
1682
+ :param normalize: [bool] [default is False] --- Normalize the curves to [0-1] before plotting
1683
+ :param statistics: [List[str]] [default is everything] --- List of the statistics to compute and return (in ['min_max', 'mean', 'median', 'std', 'amplitude', 'max_day', 'nb_peaks', 'relative_max'])
1684
+ :param cmap: [str] [default is 'hsv'] --- Color map among which the colors for plotting the annual curves are picked
1685
+ :param markers: [List[str]] [default is ['.', 'p', 's', 'v', 'D', '*', 'x', '1', '+']] --- Symbols of the markers for the plot
1686
+ :param markers_size: [List[int]] [default is [5, 4, 3, 4, 3, 4, 4, 6, 4]] --- Marker size to use for each marker
1687
+ :param verbose: [bool] [default is False] --- Print a recap of the year statistics for each year
1688
+
1689
+ :return fig, ax: Axis and Figure of the plots
1690
+ :return stats: [dict] --- dictionary of the statistics (each key is associated to a list with every year's value of the statistic related to the key)
1691
+ """
1692
+
1693
+ dates_c = (
1694
+ self.datainterp.dataf["date1"] + (self.datainterp.dataf["date2"] - self.datainterp.dataf["date1"]) // 2
1695
+ ) # Central dates
1696
+ vv = np.sqrt(
1697
+ self.datainterp.dataf["vx"] ** 2 + self.datainterp.dataf["vy"] ** 2
1698
+ ).to_numpy() # Velocity magnitude
1699
+
1700
+ years = np.unique(np.array([dates_c.iloc[i].year for i in range(dates_c.size)]))
1701
+ months_start = {
1702
+ "January": 1,
1703
+ "February": 32,
1704
+ "March": 60,
1705
+ "April": 91,
1706
+ "May": 121,
1707
+ "June": 152,
1708
+ "July": 182,
1709
+ "August": 213,
1710
+ "September": 244,
1711
+ "October": 274,
1712
+ "November": 305,
1713
+ "December": 335,
1714
+ }
1715
+
1716
+ stats = {
1717
+ "min": [],
1718
+ "max": [],
1719
+ "mean": [],
1720
+ "median": [],
1721
+ "std": [],
1722
+ "amplitude": [],
1723
+ "max_day": [],
1724
+ "nb_peaks": [],
1725
+ "relative_max": [],
1726
+ }
1727
+
1728
+ cmap = matplotlib.cm.get_cmap(cmap)
1729
+ colors = [cmap(i) for i in np.linspace(0, 1, len(years))]
1730
+ fig, ax = plt.subplots(figsize=(12, 4))
1731
+ for y in range(len(years)):
1732
+ dates = dates_c[[dates_c.iloc[i].year == years[y] for i in range(dates_c.size)]] - pd.Timestamp(
1733
+ year=years[y], month=1, day=1
1734
+ )
1735
+ dates = np.array([dates.iloc[i].days for i in range(dates.size)])
1736
+ vv_y = vv[[dates_c.iloc[i].year == years[y] for i in range(dates_c.size)]]
1737
+
1738
+ if verbose:
1739
+ print(f"Year {years[y]} :")
1740
+
1741
+ if "min" in statistics: # Min value of the velocities over the year
1742
+ stats["min"].append(np.min(vv_y))
1743
+ if verbose:
1744
+ print(" Min = {:.1f} m/y".format(stats["min"][y]))
1745
+ if "max" in statistics: # Max value of the velocities over the year
1746
+ stats["max"].append(np.max(vv_y))
1747
+ if verbose:
1748
+ print(" Max = {:.1f} m/y".format(stats["max"][y]))
1749
+ if "mean" in statistics:
1750
+ stats["mean"].append(np.mean(vv_y)) # Mean value of the velocities over the year
1751
+ if verbose:
1752
+ print(" Mean = {:.1f} m/y".format(stats["mean"][y]))
1753
+ if "median" in statistics:
1754
+ stats["median"].append(np.median(vv_y)) # Median value of the velocities over the year
1755
+ if verbose:
1756
+ print(" Median = {:.1f} m/y".format(stats["median"][y]))
1757
+ if "std" in statistics:
1758
+ stats["std"].append(np.std(vv_y, ddof=0)) # Standard deviation of the velocities over the year
1759
+ if verbose:
1760
+ print(" Standard deviation = {:.1f} m/y".format(stats["std"][y]))
1761
+ if "amplitude" in statistics:
1762
+ stats["amplitude"].append(
1763
+ (np.max(vv_y) - np.min(vv_y)) / 2
1764
+ ) # Amplitude of the velocity variations (computed as (max - min)/2)
1765
+ if verbose:
1766
+ print(" Amplitude = {:.1f} m/y".format(stats["amplitude"][y]))
1767
+ if "max_day" in statistics:
1768
+ stats["max_day"].append(dates[np.argmax(vv_y)]) # Position of the maximum (in day)
1769
+ if verbose:
1770
+ diff_month = stats["max_day"][y] - np.array(list(months_start.values()))
1771
+ month = list(months_start.keys())[np.argmin(diff_month[diff_month > 0])]
1772
+ day = np.min(diff_month[diff_month > 0]) + 1
1773
+ print(f" Day of the maximum = {stats['max_day'][y]}th day of the year ({month}, {day})")
1774
+
1775
+ if "nb_peaks" in statistics or "relative_max" in statistics or "start_accel" in statistics:
1776
+ deriv = np.diff(vv_y) / np.diff(dates) # Compute the derivative of the velocities
1777
+ peak_pos = (
1778
+ [False]
1779
+ + [(np.sign(deriv[i + 1]) == -1 and np.sign(deriv[i]) == 1) for i in range(len(deriv) - 1)]
1780
+ + [False]
1781
+ )
1782
+ peak_dates = dates[peak_pos]
1783
+ peak_amplitudes = vv_y[peak_pos] - np.mean(vv_y) # This time, the amplitudes are compute as max - mean
1784
+
1785
+ if "nb_peaks" in statistics:
1786
+ stats["nb_peaks"].append(len(peak_dates)) # Number of velocitiy peaks during the year
1787
+ if verbose:
1788
+ print(" Number of maximum = {}".format(stats["nb_peaks"][y]))
1789
+ if (
1790
+ "relative_max" in statistics
1791
+ ): # Amplitude of the second maximum divided by the amplitude of the first maximum
1792
+ if len(peak_dates) == 0:
1793
+ stats["relative_max"].append(None)
1794
+ else:
1795
+ stats["relative_max"].append(
1796
+ np.max(peak_amplitudes[np.arange(len(peak_amplitudes)) != np.argmax(peak_amplitudes)])
1797
+ / np.max(peak_amplitudes)
1798
+ )
1799
+ if verbose:
1800
+ print(" Relative maximum value = {:.2f}".format(stats["relative_max"][y]))
1801
+ if "start_accel" in statistics:
1802
+ pass
1803
+
1804
+ if normalize:
1805
+ vv_y = (vv_y - np.min(vv_y)) / (np.max(vv_y) - np.min(vv_y))
1806
+
1807
+ ax.plot(
1808
+ dates,
1809
+ vv_y,
1810
+ linestyle="",
1811
+ marker=markers[y],
1812
+ markersize=markers_size[y],
1813
+ label=str(years[y]),
1814
+ color=colors[y],
1815
+ )
1816
+
1817
+ ax.set_xticks(list(months_start.values()), list(months_start.keys()))
1818
+ plt.setp(ax.get_xticklabels(), rotation=20, ha="right", rotation_mode="anchor")
1819
+ ax.set_xlabel("Day of the year", fontsize=14)
1820
+ ax.set_ylabel("Velocity magnitude [m/y]", fontsize=14)
1821
+ ax.legend(loc="best")
1822
+ ax.set_title("Superposed annual TICOI resulting velocities", fontsize=16)
1823
+ plt.subplots_adjust(bottom=0.2)
1824
+
1825
+ if self.show:
1826
+ plt.show()
1827
+ if self.save:
1828
+ fig.savefig(f"{self.path_save}annual_curves.png")
1829
+
1830
+ return fig, ax, {key: stats[key] for key in statistics}