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/__about__.py +1 -0
- ticoi/__init__.py +0 -0
- ticoi/core.py +1500 -0
- ticoi/cube_data_classxr.py +2204 -0
- ticoi/cube_writer.py +741 -0
- ticoi/example.py +81 -0
- ticoi/filtering_functions.py +676 -0
- ticoi/interpolation_functions.py +236 -0
- ticoi/inversion_functions.py +1015 -0
- ticoi/mjd2date.py +31 -0
- ticoi/optimize_coefficient_functions.py +264 -0
- ticoi/pixel_class.py +1830 -0
- ticoi/seasonality_functions.py +209 -0
- ticoi/utils.py +725 -0
- ticoi-0.0.1.dist-info/METADATA +152 -0
- ticoi-0.0.1.dist-info/RECORD +18 -0
- ticoi-0.0.1.dist-info/WHEEL +4 -0
- ticoi-0.0.1.dist-info/licenses/LICENSE +165 -0
ticoi/cube_writer.py
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import xarray as xr
|
|
7
|
+
|
|
8
|
+
from ticoi.cube_data_classxr import CubeDataClass
|
|
9
|
+
from ticoi.interpolation_functions import smooth_results
|
|
10
|
+
|
|
11
|
+
# %% ======================================================================== #
|
|
12
|
+
# Hardcoded configs #
|
|
13
|
+
# =========================================================================%% #
|
|
14
|
+
|
|
15
|
+
BASE_CONFIGS = {
|
|
16
|
+
"velocity": {
|
|
17
|
+
"suffixes": ["x", "y", "z", "h"],
|
|
18
|
+
"directions": ["East/West", "North/South", "Up/Down", "nSPF"],
|
|
19
|
+
"unit": "m year-1",
|
|
20
|
+
"var_prefix": "v",
|
|
21
|
+
"final_var_tpl": "v{dim}",
|
|
22
|
+
"long_name_tpl": "velocity in the {direction} direction",
|
|
23
|
+
},
|
|
24
|
+
"displacement": {
|
|
25
|
+
"suffixes": ["x", "y", "z", "h"],
|
|
26
|
+
"directions": ["East/West", "North/South", "Up/Down", "nSPF"],
|
|
27
|
+
"unit": "m",
|
|
28
|
+
"var_prefix": "result_d",
|
|
29
|
+
"final_var_tpl": "d{dim}",
|
|
30
|
+
"long_name_tpl": "cumulative displacement in the {direction} direction",
|
|
31
|
+
},
|
|
32
|
+
"contribution": {
|
|
33
|
+
"flag": "X_contribution",
|
|
34
|
+
"suffixes": ["x", "y", "z", "h"],
|
|
35
|
+
"unit": "count",
|
|
36
|
+
"var_prefix": "xcount_",
|
|
37
|
+
"final_var_tpl": "xcount_{dim}",
|
|
38
|
+
"long_name_tpl": "number of Y observations contributing to X estimation ({dim_upper})",
|
|
39
|
+
},
|
|
40
|
+
"error": {
|
|
41
|
+
"flag": "Error_propagation",
|
|
42
|
+
"suffixes": ["x", "y", "z", "h"],
|
|
43
|
+
"unit": "m year-1",
|
|
44
|
+
"var_prefix": "error_",
|
|
45
|
+
"final_var_tpl": "error_{dim}",
|
|
46
|
+
"long_name_tpl": "Error propagated for the displacement in {dim_upper} direction",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
QUALITY_METRIC_CONFIGS = {
|
|
51
|
+
"Norm_residual": {
|
|
52
|
+
"vars": ["ResidualAXY_dx", "ResidualRegu_dx", "ResidualAXY_dy", "ResidualRegu_dy"],
|
|
53
|
+
"source_col": "NormR",
|
|
54
|
+
"long_names": [
|
|
55
|
+
"Residual from the inversion AX=Y, where Y is the displacement in the direction Est/West",
|
|
56
|
+
"Residual from the regularisation term for the displacement in the direction Est/West",
|
|
57
|
+
"Residual from the inversion AX=Y, where Y is the displacement in the direction North/South",
|
|
58
|
+
"Residual from the regularisation term for the displacement in the direction North/South",
|
|
59
|
+
],
|
|
60
|
+
"unit": "m",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# %% ======================================================================== #
|
|
66
|
+
# WRITING RESULTS AS NETCDF #
|
|
67
|
+
# =========================================================================%% #
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CubeResultsWriter:
|
|
71
|
+
def __init__(self, cube: CubeDataClass):
|
|
72
|
+
self.ds = cube.ds
|
|
73
|
+
self.nx = cube.nx
|
|
74
|
+
self.ny = cube.ny
|
|
75
|
+
self.proj4 = cube.ds.proj4
|
|
76
|
+
self.variable_configs = {}
|
|
77
|
+
|
|
78
|
+
def write_result_ticoi(
|
|
79
|
+
self,
|
|
80
|
+
result: list,
|
|
81
|
+
source: str,
|
|
82
|
+
sensor: str,
|
|
83
|
+
filename: str = "Time_series",
|
|
84
|
+
savepath: Optional[str] = None,
|
|
85
|
+
result_quality: Optional[List[str]] = None,
|
|
86
|
+
smooth_res: bool = False,
|
|
87
|
+
smooth_window_size: int = 3,
|
|
88
|
+
return_result: bool = False,
|
|
89
|
+
verbose: bool = False,
|
|
90
|
+
) -> Union["CubeDataClass", str, Tuple["CubeDataClass", list]]:
|
|
91
|
+
"""
|
|
92
|
+
Write the result from TICOI, stored in result, in a xarray dataset matching the conventions CF-1.11
|
|
93
|
+
http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.pdf
|
|
94
|
+
|
|
95
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
96
|
+
:param source: [str] --- Name of the source
|
|
97
|
+
:param sensor: [str] --- Sensors which have been used
|
|
98
|
+
:param filename: [str] [default is Time_series] --- Filename of file to saved
|
|
99
|
+
:param savepath: [Optional[str]] [default is None] --- Path to save file
|
|
100
|
+
:param result_quality: [list | str | None] [default is None] --- Which can contain 'Norm_residual' to determine the L2 norm of the residuals from the last inversion, 'X_contribution' to determine the number of Y observations which have contributed to estimate each value in X (it corresponds to A.dot(weight)):param savepath: string, path where to save the file
|
|
101
|
+
:param smooth_res: [bool] [default is False] --- Smooth the residuals before saving
|
|
102
|
+
:param smooth_window_size:[int] [default is 3] --- Size of the smoothing kernel
|
|
103
|
+
:param return_result: [bool] [default is False] --- If True, return result
|
|
104
|
+
:param verbose: [bool] [default is False] --- Print information throughout the process
|
|
105
|
+
|
|
106
|
+
:return cubenew: [cube_data_class] --- New cube where the results are saved
|
|
107
|
+
"""
|
|
108
|
+
if not self._validate_input(result):
|
|
109
|
+
return "No results to write or save."
|
|
110
|
+
|
|
111
|
+
dimensions = self._detect_dimensions(result) # detect needed dimension (x,y and possibly z and h)
|
|
112
|
+
if verbose:
|
|
113
|
+
print(f"[Writing results] Detected dimensions: {dimensions}")
|
|
114
|
+
|
|
115
|
+
self.variable_configs = self._generate_variable_configs(
|
|
116
|
+
dimensions
|
|
117
|
+
) # set variable long_names,short_names, and unit
|
|
118
|
+
|
|
119
|
+
time_base, non_null_el = self._get_time_base(result)
|
|
120
|
+
|
|
121
|
+
cubenew = self._initialize_cube(time_variable=time_base, add_date_vars=True, non_null_el=non_null_el)
|
|
122
|
+
|
|
123
|
+
available_vars = self._detect_available_variables(non_null_el, result_quality)
|
|
124
|
+
self._process_velocity_variables(cubenew, result, available_vars, time_base, smooth_res, smooth_window_size)
|
|
125
|
+
|
|
126
|
+
if result_quality: # if there are quality metrics
|
|
127
|
+
self._process_2d_quality_metrics(cubenew, result, result_quality)
|
|
128
|
+
|
|
129
|
+
self._set_metadata(cubenew, source, sensor, dimensions)
|
|
130
|
+
|
|
131
|
+
if savepath:
|
|
132
|
+
self._save_cube(cubenew, savepath, filename, verbose)
|
|
133
|
+
|
|
134
|
+
return (cubenew, result) if return_result else cubenew
|
|
135
|
+
|
|
136
|
+
def write_result_tico(
|
|
137
|
+
self,
|
|
138
|
+
result: list,
|
|
139
|
+
source: str,
|
|
140
|
+
sensor: str,
|
|
141
|
+
filename: str = "Time_series_invert",
|
|
142
|
+
savepath: Optional[str] = None,
|
|
143
|
+
result_quality: Optional[List[str]] = None,
|
|
144
|
+
return_result: bool = False,
|
|
145
|
+
verbose: bool = False,
|
|
146
|
+
) -> Union["CubeDataClass", str]:
|
|
147
|
+
"""
|
|
148
|
+
Write the result from TICOI, stored in result, in a xarray dataset matching the conventions CF-1.11
|
|
149
|
+
http://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.pdf
|
|
150
|
+
|
|
151
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
152
|
+
:param source: [str] --- Name of the source
|
|
153
|
+
:param sensor: [str] --- Sensors which have been used
|
|
154
|
+
:param filename: [str] [default is Time_series] --- Filename of file to saved
|
|
155
|
+
:param savepath: [Optional[str]] [default is None] --- Path to save file
|
|
156
|
+
:param result_quality: [list | str | None] [default is None] --- Which can contain 'Norm_residual' to determine the L2 norm of the residuals from the last inversion, 'X_contribution' to determine the number of Y observations which have contributed to estimate each value in X (it corresponds to A.dot(weight))
|
|
157
|
+
:param return_result: [bool] [default is False] --- If True, return result
|
|
158
|
+
:param verbose: [bool] [default is False] --- Print information throughout the process
|
|
159
|
+
|
|
160
|
+
:return cubenew: [cube_data_class] --- New cube where the results are saved
|
|
161
|
+
"""
|
|
162
|
+
if not self._validate_input(result):
|
|
163
|
+
return "No results to write or save."
|
|
164
|
+
|
|
165
|
+
dimensions = self._detect_dimensions(result)
|
|
166
|
+
if verbose:
|
|
167
|
+
print(f"[Writing results] Detected dimensions: {dimensions}")
|
|
168
|
+
|
|
169
|
+
self.variable_configs = self._generate_variable_configs(dimensions)
|
|
170
|
+
|
|
171
|
+
sample = next((r for r in result if not r.empty), None) # first results not empty
|
|
172
|
+
available_vars = self._detect_available_variables(sample, result_quality)
|
|
173
|
+
|
|
174
|
+
reconstructed_data, time_base, ref_dates = self._vectorized_reconstruct(
|
|
175
|
+
result, available_vars
|
|
176
|
+
) # reconstruct cumulative displacement time series
|
|
177
|
+
|
|
178
|
+
cubenew = self._initialize_cube(time_variable=time_base)
|
|
179
|
+
self._set_reference_date(
|
|
180
|
+
cubenew, ref_dates
|
|
181
|
+
) # set reference date (i.e. the first date of the cumulative displacement time series
|
|
182
|
+
|
|
183
|
+
final_var_map = self._build_final_var_map()
|
|
184
|
+
|
|
185
|
+
for var_name, data_array in reconstructed_data.items():
|
|
186
|
+
if var_name in final_var_map:
|
|
187
|
+
config, idx = final_var_map[var_name]
|
|
188
|
+
self._add_variable_to_cube(
|
|
189
|
+
cubenew,
|
|
190
|
+
var_name,
|
|
191
|
+
data_array,
|
|
192
|
+
config["long_names"][idx],
|
|
193
|
+
config["unit"],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif verbose:
|
|
197
|
+
print(f"Warning: Configuration for variable '{var_name}' not found. Skipping.")
|
|
198
|
+
|
|
199
|
+
self._set_metadata(cubenew, source, sensor, dimensions)
|
|
200
|
+
|
|
201
|
+
if savepath:
|
|
202
|
+
self._save_cube(cubenew, savepath, filename, verbose)
|
|
203
|
+
|
|
204
|
+
return (cubenew, result) if return_result else cubenew
|
|
205
|
+
|
|
206
|
+
def _process_2d_quality_metrics(self, cube: "CubeDataClass", result: list, result_quality: List[str]):
|
|
207
|
+
"""
|
|
208
|
+
Processes and adds 2D quality metrics to the data cube.
|
|
209
|
+
:param cube: [CubeDataClass] --- Cube data class
|
|
210
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
211
|
+
:param result_quality: [list | str | None] [default is None] --- Which can contain 'Norm_residual' to determine the L2 norm of the residuals from the last inversion, 'X_contribution' to determine the number of Y observations which have contributed to estimate each value in X (it corresponds to A.dot(weight))
|
|
212
|
+
:return:
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
if "Norm_residual" not in result_quality:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
config = QUALITY_METRIC_CONFIGS.get("Norm_residual")
|
|
219
|
+
if not config:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
source_col = config["source_col"]
|
|
223
|
+
sample = next((r for r in result if not r.empty and source_col in r), None)
|
|
224
|
+
if sample is None:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
for i, var_name in enumerate(config["vars"]):
|
|
228
|
+
data_arr = np.full((self.nx, self.ny), np.nan, dtype=np.float32)
|
|
229
|
+
|
|
230
|
+
for p_idx, df in enumerate(result):
|
|
231
|
+
if not df.empty and source_col in df and df[source_col].shape[0] > i:
|
|
232
|
+
x = p_idx // self.ny
|
|
233
|
+
y = p_idx % self.ny
|
|
234
|
+
data_arr[x, y] = df[source_col][i]
|
|
235
|
+
|
|
236
|
+
cube.ds[var_name] = xr.DataArray(data_arr, dims=["x", "y"], coords={"x": cube.ds["x"], "y": cube.ds["y"]})
|
|
237
|
+
cube.ds[var_name] = cube.ds[var_name].transpose("y", "x")
|
|
238
|
+
cube.ds[var_name].attrs = {
|
|
239
|
+
"short_name": var_name,
|
|
240
|
+
"unit": config["unit"],
|
|
241
|
+
"long_name": config["long_names"][i],
|
|
242
|
+
"grid_mapping": "grid_mapping",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def _build_final_var_map(self) -> Dict[str, Tuple[Dict, int]]:
|
|
246
|
+
"""
|
|
247
|
+
Builds a mapping from a final variable name (e.g., 'dx') to its config and index.
|
|
248
|
+
:return:
|
|
249
|
+
"""
|
|
250
|
+
final_var_map = {}
|
|
251
|
+
for config in self.variable_configs.values():
|
|
252
|
+
for i, final_var in enumerate(config.get("final_vars", [])):
|
|
253
|
+
final_var_map[final_var] = (config, i)
|
|
254
|
+
return final_var_map
|
|
255
|
+
|
|
256
|
+
def _vectorized_reconstruct(
|
|
257
|
+
self, result: list, available_vars: Dict
|
|
258
|
+
) -> Tuple[Dict[str, np.ndarray], pd.Series, np.ndarray]:
|
|
259
|
+
"""
|
|
260
|
+
A fully vectorized replacement for the original `reconstruct_common_ref` loop.
|
|
261
|
+
:param result: [list] --- List of pd xarray, result from the TICOI method
|
|
262
|
+
:param available_vars:[Dict] --- dictionary of available variables
|
|
263
|
+
:return:
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
all_dates = sorted(list({date for df in result if not df.empty for date in df["date2"]}))
|
|
267
|
+
time_axis = pd.Series(all_dates, dtype="datetime64[ns]")
|
|
268
|
+
time_len = len(time_axis)
|
|
269
|
+
|
|
270
|
+
vars_to_process = []
|
|
271
|
+
for var_type, final_var_list in available_vars.items():
|
|
272
|
+
if var_type in ["displacement", "contribution", "error"]:
|
|
273
|
+
config = self.variable_configs[var_type]
|
|
274
|
+
for final_var in final_var_list:
|
|
275
|
+
if final_var in config["final_vars"]:
|
|
276
|
+
idx = config["final_vars"].index(final_var)
|
|
277
|
+
vars_to_process.append(config["vars"][idx])
|
|
278
|
+
|
|
279
|
+
if not vars_to_process:
|
|
280
|
+
return {}, time_axis, np.full((self.nx, self.ny), np.nan, dtype="datetime64[ns]")
|
|
281
|
+
|
|
282
|
+
final_var_names = {v: v.replace("result_d", "d") for v in vars_to_process}
|
|
283
|
+
|
|
284
|
+
reconstructed_data = {
|
|
285
|
+
final_name: np.full((self.nx, self.ny, time_len), np.nan, dtype=np.float32)
|
|
286
|
+
for final_name in final_var_names.values()
|
|
287
|
+
} # initialize the reconstructed array as 3D array
|
|
288
|
+
ref_dates_array = np.full((self.nx, self.ny), np.nan, dtype="datetime64[ns]")
|
|
289
|
+
|
|
290
|
+
max_pixel_len = 0
|
|
291
|
+
if result and any(not df.empty for df in result):
|
|
292
|
+
max_pixel_len = max(len(df) for df in result if not df.empty) # maximal temporal length of each pixel
|
|
293
|
+
|
|
294
|
+
if max_pixel_len == 0: # empty cube
|
|
295
|
+
return {}, time_axis, ref_dates_array
|
|
296
|
+
|
|
297
|
+
packed_data = {
|
|
298
|
+
v: np.full((self.nx * self.ny, max_pixel_len), np.nan, dtype=np.float32) for v in vars_to_process
|
|
299
|
+
} # flatten spatial dimensions
|
|
300
|
+
packed_dates = np.full(
|
|
301
|
+
(self.nx * self.ny, max_pixel_len), np.nan, dtype="datetime64[ns]"
|
|
302
|
+
) # flatten spatial dimensions
|
|
303
|
+
pixel_lengths = np.zeros(self.nx * self.ny, dtype=int)
|
|
304
|
+
|
|
305
|
+
for i, df in enumerate(result):
|
|
306
|
+
if not df.empty:
|
|
307
|
+
n = len(df)
|
|
308
|
+
pixel_lengths[i] = n
|
|
309
|
+
ref_dates_array[i // self.ny, i % self.ny] = df["date1"].iloc[0]
|
|
310
|
+
packed_dates[i, :n] = df["date2"].values
|
|
311
|
+
for v in vars_to_process:
|
|
312
|
+
if v in df:
|
|
313
|
+
packed_data[v][i, :n] = df[v].values
|
|
314
|
+
|
|
315
|
+
cumulative_data = {
|
|
316
|
+
v: np.nancumsum(arr, axis=1) for v, arr in packed_data.items()
|
|
317
|
+
} # cumulative summation of displacement along time
|
|
318
|
+
|
|
319
|
+
# put the results in a 3D array
|
|
320
|
+
for i in range(self.nx * self.ny):
|
|
321
|
+
n = pixel_lengths[i]
|
|
322
|
+
if n > 0:
|
|
323
|
+
pixel_dates = packed_dates[i, :n]
|
|
324
|
+
insert_indices = np.searchsorted(time_axis, pixel_dates)
|
|
325
|
+
|
|
326
|
+
x, y = i // self.ny, i % self.ny
|
|
327
|
+
for v_orig, v_cum in cumulative_data.items():
|
|
328
|
+
v_new = final_var_names[v_orig]
|
|
329
|
+
reconstructed_data[v_new][x, y, insert_indices] = v_cum[i, :n]
|
|
330
|
+
|
|
331
|
+
return reconstructed_data, time_axis, ref_dates_array
|
|
332
|
+
|
|
333
|
+
def _prepare_variable_array(self, result: list, var: str, time_len: int) -> np.ndarray:
|
|
334
|
+
"""
|
|
335
|
+
Efficiently prepares a 3D numpy array for a given variable from the result list.
|
|
336
|
+
:param result: [list] --- list with results from ticoi or tico
|
|
337
|
+
:param var : [str] --- variable name
|
|
338
|
+
:param time_len : [int] --- length of the time axis
|
|
339
|
+
:return: 3D numpy array
|
|
340
|
+
"""
|
|
341
|
+
final_array = np.full((self.nx, self.ny, time_len), np.nan, dtype=np.float32)
|
|
342
|
+
for i in range(self.nx):
|
|
343
|
+
for j in range(self.ny):
|
|
344
|
+
idx = i * self.ny + j
|
|
345
|
+
if idx < len(result) and not result[idx].empty and var in result[idx]:
|
|
346
|
+
data_slice = result[idx][var].values
|
|
347
|
+
if data_slice.shape[0] == time_len:
|
|
348
|
+
final_array[i, j, :] = data_slice
|
|
349
|
+
return final_array
|
|
350
|
+
|
|
351
|
+
def _process_velocity_variables(
|
|
352
|
+
self,
|
|
353
|
+
cube: "CubeDataClass",
|
|
354
|
+
result: list,
|
|
355
|
+
available_vars: Dict,
|
|
356
|
+
time_variable: pd.Series,
|
|
357
|
+
smooth_res: bool,
|
|
358
|
+
smooth_window_size: int,
|
|
359
|
+
):
|
|
360
|
+
"""
|
|
361
|
+
Process and add all detected velocity-related variables to the data cube.
|
|
362
|
+
:param cube : [CubeDataClass] --- cube we are saving
|
|
363
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
364
|
+
:param available_vars:[Dict] --- dictionary of available variables
|
|
365
|
+
:param time_variable : [pd.Series] --- centered dates for each estimation
|
|
366
|
+
:param smooth_res: [bool] [default is False] --- Smooth the residuals before saving
|
|
367
|
+
:param smooth_window_size:[int] [default is 3] --- Size of the smoothing kernel
|
|
368
|
+
|
|
369
|
+
:return:
|
|
370
|
+
"""
|
|
371
|
+
time_len = len(time_variable)
|
|
372
|
+
|
|
373
|
+
for var_type, var_list in available_vars.items():
|
|
374
|
+
if var_type not in self.variable_configs:
|
|
375
|
+
continue
|
|
376
|
+
config = self.variable_configs[var_type]
|
|
377
|
+
|
|
378
|
+
for i, final_var in enumerate(config.get("final_vars", [])):
|
|
379
|
+
if final_var not in var_list:
|
|
380
|
+
continue
|
|
381
|
+
original_var_name = config["vars"][i]
|
|
382
|
+
result_arr = self._prepare_variable_array(result, original_var_name, time_len) # create a 3D np array
|
|
383
|
+
|
|
384
|
+
if smooth_res and var_type == "velocity":
|
|
385
|
+
result_arr = self._smooth_array(
|
|
386
|
+
result_arr, smooth_window_size
|
|
387
|
+
) # smooth the result by applying a spatial smoothing
|
|
388
|
+
self._update_result_list(result, original_var_name, result_arr)
|
|
389
|
+
|
|
390
|
+
self._add_variable_to_cube(
|
|
391
|
+
cube,
|
|
392
|
+
final_var,
|
|
393
|
+
result_arr,
|
|
394
|
+
config["long_names"][i],
|
|
395
|
+
config["unit"],
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _initialize_cube(
|
|
399
|
+
self, time_variable: pd.Series, add_date_vars: bool = False, non_null_el: Optional[pd.DataFrame] = None
|
|
400
|
+
) -> "CubeDataClass":
|
|
401
|
+
"""
|
|
402
|
+
Initialize a data cube with basic coordinates and time variables.
|
|
403
|
+
:param time_variable [pd.Series]: centered dates for each estimation
|
|
404
|
+
:param add_date_vars [bool]: If yes, add also the two dates between each the velocity have been estimated
|
|
405
|
+
:param non_null_el [Optional[pd.DataFrame]]: results which are not null
|
|
406
|
+
:return:
|
|
407
|
+
"""
|
|
408
|
+
cubenew = CubeDataClass()
|
|
409
|
+
cubenew.nx = self.nx
|
|
410
|
+
cubenew.ny = self.ny
|
|
411
|
+
cubenew.proj4 = self.proj4
|
|
412
|
+
|
|
413
|
+
x_attrs = {"standard_name": "projection_x_coordinate", "units": "m", "long_name": "x coordinate of projection"}
|
|
414
|
+
y_attrs = {"standard_name": "projection_y_coordinate", "units": "m", "long_name": "y coordinate of projection"}
|
|
415
|
+
|
|
416
|
+
epoch = pd.Timestamp("1970-01-01")
|
|
417
|
+
time_values = (time_variable - epoch).dt.total_seconds() / (24 * 3600)
|
|
418
|
+
time_attrs = {
|
|
419
|
+
"standard_name": "time",
|
|
420
|
+
"long_name": "center date of the velocity estimation",
|
|
421
|
+
"units": "days since 1970-01-01 00:00:00",
|
|
422
|
+
"calendar": "gregorian",
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
cubenew.ds = xr.Dataset(
|
|
426
|
+
coords={
|
|
427
|
+
"x": ("x", self.ds["x"].values, x_attrs),
|
|
428
|
+
"y": ("y", self.ds["y"].values, y_attrs),
|
|
429
|
+
"time": ("time", time_values.values, time_attrs),
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Set grid mapping variable
|
|
434
|
+
cubenew.ds.rio.write_crs(self.proj4, inplace=True)
|
|
435
|
+
grid_mapping_attrs = cubenew.ds.coords["spatial_ref"].attrs
|
|
436
|
+
cubenew.ds = cubenew.ds.drop_vars("spatial_ref")
|
|
437
|
+
cubenew.ds["grid_mapping"] = xr.DataArray(0, attrs=grid_mapping_attrs)
|
|
438
|
+
|
|
439
|
+
if add_date_vars and non_null_el is not None:
|
|
440
|
+
date1_values = (non_null_el["date1"] - epoch).dt.total_seconds() / (24 * 3600)
|
|
441
|
+
date2_values = (non_null_el["date2"] - epoch).dt.total_seconds() / (24 * 3600)
|
|
442
|
+
time_bnds_data = np.vstack([date1_values, date2_values]).T
|
|
443
|
+
cubenew.ds["time_bnds"] = (("time", "bnds"), time_bnds_data)
|
|
444
|
+
cubenew.ds["time"].attrs["bounds"] = "time_bnds"
|
|
445
|
+
|
|
446
|
+
return cubenew
|
|
447
|
+
|
|
448
|
+
def _add_variable_to_cube(
|
|
449
|
+
self,
|
|
450
|
+
cube: "CubeDataClass",
|
|
451
|
+
var: str,
|
|
452
|
+
data: np.ndarray,
|
|
453
|
+
long_name: str,
|
|
454
|
+
unit: str,
|
|
455
|
+
):
|
|
456
|
+
"""
|
|
457
|
+
Add a variable as a DataArray to the data cube.
|
|
458
|
+
:param cube: [CubeDataClass] --- Cube data class
|
|
459
|
+
:param var: [str] --- variable name
|
|
460
|
+
:param data: [np.ndarray] --- data array to add as variable
|
|
461
|
+
:param long_name: [str] --- long name of the variable
|
|
462
|
+
:param unit: [str] --- unit of the variable
|
|
463
|
+
:return:
|
|
464
|
+
"""
|
|
465
|
+
data_array = xr.DataArray(
|
|
466
|
+
data, dims=["x", "y", "time"], coords={"x": cube.ds["x"], "y": cube.ds["y"], "time": cube.ds["time"]}
|
|
467
|
+
)
|
|
468
|
+
cube.ds[var] = data_array.transpose("time", "y", "x")
|
|
469
|
+
attrs = {"units": unit, "long_name": long_name, "grid_mapping": "grid_mapping"}
|
|
470
|
+
|
|
471
|
+
attrs["short_name"] = var # no standard_name exist for our variables
|
|
472
|
+
cube.ds[var].attrs = attrs
|
|
473
|
+
|
|
474
|
+
def _set_reference_date(self, cube: "CubeDataClass", ref_dates: np.ndarray):
|
|
475
|
+
"""
|
|
476
|
+
Set the reference date for displacement time series.
|
|
477
|
+
:param cube: [CubeDataClass] --- Cube data class
|
|
478
|
+
:param ref_dates: [np.ndarray] --- reference dates
|
|
479
|
+
:return:
|
|
480
|
+
"""
|
|
481
|
+
epoch = pd.Timestamp("1970-01-01")
|
|
482
|
+
# This handles NaT (Not a Time) values, which will become NaN after conversion.
|
|
483
|
+
numerical_dates = (pd.to_datetime(ref_dates.flatten()) - epoch).total_seconds() / (24 * 3600)
|
|
484
|
+
numerical_dates_arr = numerical_dates.values.reshape(ref_dates.shape)
|
|
485
|
+
|
|
486
|
+
cube.ds["reference_date"] = xr.DataArray(
|
|
487
|
+
numerical_dates_arr, dims=["x", "y"], coords={"x": cube.ds["x"], "y": cube.ds["y"]}
|
|
488
|
+
)
|
|
489
|
+
cube.ds["reference_date"].attrs = {
|
|
490
|
+
"long_name": "First date of the cumulative displacement time series",
|
|
491
|
+
"units": "days since 1970-01-01 00:00:00",
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
def _validate_input(self, result: list) -> bool:
|
|
495
|
+
"""
|
|
496
|
+
Check if the inputs are valid
|
|
497
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
498
|
+
:return:
|
|
499
|
+
"""
|
|
500
|
+
return bool(result) and any(not r.empty for r in result)
|
|
501
|
+
|
|
502
|
+
def _get_time_base(self, result: list) -> Tuple[pd.Series, pd.DataFrame]:
|
|
503
|
+
"""
|
|
504
|
+
Get the centered date (time_variable) and the results which are not null
|
|
505
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
506
|
+
:return: entered date (time_variable) and the results which are not null
|
|
507
|
+
"""
|
|
508
|
+
non_null_el = next((r for r in result if not r.empty), None)
|
|
509
|
+
if non_null_el is None:
|
|
510
|
+
return pd.Series([], dtype="datetime64[ns]"), None
|
|
511
|
+
time_variable = non_null_el["date1"] + (non_null_el["date2"] - non_null_el["date1"]) / 2
|
|
512
|
+
return time_variable, non_null_el
|
|
513
|
+
|
|
514
|
+
def _detect_dimensions(self, result: list) -> List[str]:
|
|
515
|
+
"""
|
|
516
|
+
Detect the dimension in cube result
|
|
517
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
518
|
+
:return: list of dimensions
|
|
519
|
+
"""
|
|
520
|
+
sample = next((r for r in result if not r.empty), None)
|
|
521
|
+
if sample is None:
|
|
522
|
+
return []
|
|
523
|
+
dim_map = {
|
|
524
|
+
"vx": "x",
|
|
525
|
+
"vy": "y",
|
|
526
|
+
"vz": "z",
|
|
527
|
+
"vh": "h",
|
|
528
|
+
"result_dx": "x",
|
|
529
|
+
"result_dy": "y",
|
|
530
|
+
"result_dz": "z",
|
|
531
|
+
"result_dh": "h",
|
|
532
|
+
}
|
|
533
|
+
return sorted(list({dim_map[col] for col in sample.columns if col in dim_map}))
|
|
534
|
+
|
|
535
|
+
def _generate_variable_configs(self, dimensions: List[str]) -> Dict[str, Dict]:
|
|
536
|
+
"""
|
|
537
|
+
Generate config files
|
|
538
|
+
:param dimensions [List[str]]:
|
|
539
|
+
:return: dict o configs
|
|
540
|
+
"""
|
|
541
|
+
configs = {}
|
|
542
|
+
for var_type, base_config in BASE_CONFIGS.items():
|
|
543
|
+
vars_list, long_names, final_vars = [], [], []
|
|
544
|
+
for dim in dimensions:
|
|
545
|
+
if dim not in base_config["suffixes"]:
|
|
546
|
+
continue # if the dimension is not defined
|
|
547
|
+
vars_list.append(base_config["var_prefix"] + dim)
|
|
548
|
+
final_vars.append(base_config["final_var_tpl"].format(dim=dim))
|
|
549
|
+
direction = base_config.get("directions", [""] * len(dimensions))[base_config["suffixes"].index(dim)]
|
|
550
|
+
long_names.append(base_config["long_name_tpl"].format(direction=direction, dim_upper=dim.upper()))
|
|
551
|
+
|
|
552
|
+
if vars_list:
|
|
553
|
+
configs[var_type] = {
|
|
554
|
+
"vars": vars_list,
|
|
555
|
+
"long_names": long_names,
|
|
556
|
+
"unit": base_config["unit"],
|
|
557
|
+
"final_vars": final_vars,
|
|
558
|
+
"flag": base_config.get("flag"),
|
|
559
|
+
}
|
|
560
|
+
return configs
|
|
561
|
+
|
|
562
|
+
def _detect_available_variables(
|
|
563
|
+
self, sample_result: pd.DataFrame, result_quality: Optional[List[str]]
|
|
564
|
+
) -> Dict[str, List[str]]:
|
|
565
|
+
"""
|
|
566
|
+
Detect variable names inside the cube result
|
|
567
|
+
:param sample_result [pd.DataFrame]: result for one particular date
|
|
568
|
+
:param result_quality: [list | str | None] [default is None] --- Which can contain 'Norm_residual' to determine the L2 norm of the residuals from the last inversion, 'X_contribution' to determine the number of Y observations which have contributed to estimate each value in X (it corresponds to A.dot(weight))
|
|
569
|
+
:return:
|
|
570
|
+
"""
|
|
571
|
+
if sample_result is None:
|
|
572
|
+
return {}
|
|
573
|
+
|
|
574
|
+
# Get available variable
|
|
575
|
+
available = {}
|
|
576
|
+
for var_type, config in self.variable_configs.items():
|
|
577
|
+
# Always include base types if they exist
|
|
578
|
+
if var_type in ["velocity", "displacement"]:
|
|
579
|
+
if any(var in sample_result for var in config["vars"]):
|
|
580
|
+
available[var_type] = config.get("final_vars", [])
|
|
581
|
+
continue
|
|
582
|
+
|
|
583
|
+
# For quality metrics, check if the flag is set
|
|
584
|
+
if result_quality and config.get("flag") in result_quality:
|
|
585
|
+
if any(var in sample_result for var in config["vars"]):
|
|
586
|
+
available[var_type] = config.get("final_vars", [])
|
|
587
|
+
|
|
588
|
+
return available
|
|
589
|
+
|
|
590
|
+
def _set_metadata(self, cube: "CubeDataClass", source: str, sensor: str, dimensions: List[str]):
|
|
591
|
+
"""
|
|
592
|
+
Set the global attributes of the cube
|
|
593
|
+
:param cube: [CubeDataClass] --- Cube data class
|
|
594
|
+
:param source: [str] --- processing steps that have been applied
|
|
595
|
+
:param sensor: [str] --- satellite sensors used to compute the original displacements
|
|
596
|
+
:param dimensions: List[str] -- dimensions of the cube
|
|
597
|
+
"""
|
|
598
|
+
cube.ds.attrs = {
|
|
599
|
+
"Conventions": "CF-1.11",
|
|
600
|
+
"title": "Ice velocity and displacement time series",
|
|
601
|
+
"institution": "Université Grenoble Alpes",
|
|
602
|
+
"source": source,
|
|
603
|
+
"sensor": sensor,
|
|
604
|
+
"proj4": self.ds.proj4,
|
|
605
|
+
"author": "L. Charrier",
|
|
606
|
+
"history": f"Created on {datetime.date.today()}",
|
|
607
|
+
"dimensions": f"{len(dimensions)}D ({', '.join(dimensions)})",
|
|
608
|
+
"references": "Charrier, L., et al. (2025)",
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
def _save_cube(self, cube: "CubeDataClass", savepath: str, filename: str, verbose: bool):
|
|
612
|
+
"""
|
|
613
|
+
Saves the data cube to a NetCDF file with appropriate encoding.
|
|
614
|
+
:param cube: [CubeDataClass] --- Cube data class
|
|
615
|
+
:param savepath: [Optional[str]] [default is None] --- Path to save file
|
|
616
|
+
:param filename: [str] [default is Time_series] --- Filename of file to saved
|
|
617
|
+
:param verbose: [bool] [default is False] --- Print information throughout the process
|
|
618
|
+
:return:
|
|
619
|
+
"""
|
|
620
|
+
encoding = {}
|
|
621
|
+
for var in cube.ds.data_vars:
|
|
622
|
+
if var in cube.ds.coords or var == "grid_mapping":
|
|
623
|
+
continue
|
|
624
|
+
encoding[var] = {"zlib": True, "complevel": 5, "dtype": "int16" if var.startswith("xcount") else "float32"}
|
|
625
|
+
|
|
626
|
+
if "time_bnds" in cube.ds:
|
|
627
|
+
encoding["time_bnds"] = {"_FillValue": None}
|
|
628
|
+
|
|
629
|
+
filepath = f"{savepath}/{filename}.nc"
|
|
630
|
+
cube.ds.to_netcdf(filepath, engine="h5netcdf", encoding=encoding)
|
|
631
|
+
if verbose:
|
|
632
|
+
print(f"[Writing results] Saved to {filepath}")
|
|
633
|
+
|
|
634
|
+
def _parse_proj4_to_cf_attrs(self) -> dict:
|
|
635
|
+
"""convert proj4 string to CF attributes."""
|
|
636
|
+
attrs = {}
|
|
637
|
+
proj_map = {
|
|
638
|
+
"proj": "grid_mapping_name",
|
|
639
|
+
"lat_0": "latitude_of_projection_origin",
|
|
640
|
+
"lon_0": "longitude_of_projection_origin",
|
|
641
|
+
"lat_ts": "standard_parallel",
|
|
642
|
+
"x_0": "false_easting",
|
|
643
|
+
"y_0": "false_northing",
|
|
644
|
+
"datum": "datum",
|
|
645
|
+
}
|
|
646
|
+
value_map = {"stere": "polar_stereographic"}
|
|
647
|
+
|
|
648
|
+
# BUG FIX: Robustly parse proj4 string to handle flags without values
|
|
649
|
+
params = {}
|
|
650
|
+
for item in self.proj4.replace("+", "").strip().split():
|
|
651
|
+
if "=" in item:
|
|
652
|
+
key, value = item.split("=", 1)
|
|
653
|
+
params[key] = value
|
|
654
|
+
else:
|
|
655
|
+
params[item] = True # Treat flags like 'no_defs' as boolean
|
|
656
|
+
|
|
657
|
+
for key, value in params.items():
|
|
658
|
+
if key in proj_map:
|
|
659
|
+
try:
|
|
660
|
+
# Attempt to convert to float, otherwise use string value
|
|
661
|
+
cf_value = float(value_map.get(value, value))
|
|
662
|
+
except (ValueError, TypeError):
|
|
663
|
+
cf_value = value_map.get(value, value)
|
|
664
|
+
attrs[proj_map[key]] = cf_value
|
|
665
|
+
|
|
666
|
+
if attrs.get("datum") == "WGS84":
|
|
667
|
+
attrs.update({"semi_major_axis": 6378137.0, "inverse_flattening": 298.257223563})
|
|
668
|
+
|
|
669
|
+
attrs["crs_wkt"] = self.proj4
|
|
670
|
+
return attrs
|
|
671
|
+
|
|
672
|
+
def _smooth_array(self, array: np.ndarray, smooth_window_size: int) -> np.ndarray:
|
|
673
|
+
"""
|
|
674
|
+
|
|
675
|
+
:param array: [np.ndarray] --- array to be smoothed
|
|
676
|
+
:param smooth_window_size:[int] [default is 3] --- size of the smoothing kernel
|
|
677
|
+
:return:
|
|
678
|
+
"""
|
|
679
|
+
return smooth_results(array, window_size=smooth_window_size)
|
|
680
|
+
|
|
681
|
+
def _update_result_list(self, result: list, var: str, smoothed_array: np.ndarray):
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
:param result: [list] --- list of pd xarray, results from the TICOI method
|
|
685
|
+
:param var: [str] --- name of the variable
|
|
686
|
+
:param smoothed_array: [np.ndarray] --- smoothed array
|
|
687
|
+
:return:
|
|
688
|
+
"""
|
|
689
|
+
for x in range(self.nx):
|
|
690
|
+
for y in range(self.ny):
|
|
691
|
+
idx = x * self.ny + y
|
|
692
|
+
if idx < len(result) and not result[idx].empty:
|
|
693
|
+
result[idx][var] = smoothed_array[x, y, :]
|
|
694
|
+
|
|
695
|
+
def write_results_ticoi_or_tico(
|
|
696
|
+
self,
|
|
697
|
+
result: list,
|
|
698
|
+
source: str,
|
|
699
|
+
sensor: str,
|
|
700
|
+
filename: str = "Time_series",
|
|
701
|
+
savepath: str | None = None,
|
|
702
|
+
result_quality: list | None = None,
|
|
703
|
+
verbose: bool = False,
|
|
704
|
+
) -> Union["CubeDataClass", str]:
|
|
705
|
+
"""
|
|
706
|
+
Write the result from TICOI or TICO, stored in result, in a xarray dataset matching the conventions CF-1.10
|
|
707
|
+
It recognizes whether the results are irregular or regular and uses the appropriate saving method
|
|
708
|
+
http://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.pdf
|
|
709
|
+
units has been changed to unit, since it was producing an error while wirtting the netcdf file
|
|
710
|
+
|
|
711
|
+
:param result: [list] --- List of pd xarray, results from the TICOI method
|
|
712
|
+
:param source: [str] --- Name of the source
|
|
713
|
+
:param sensor: [str] --- Sensors which have been used
|
|
714
|
+
:param filename: [str] [default is Time_series] --- Filename of file to saved
|
|
715
|
+
:param result_quality: [list | str | None] [default is None] --- Which can contain 'Norm_residual' to determine the L2 norm of the residuals from the last inversion, 'X_contribution' to determine the number of Y observations which have contributed to estimate each value in X (it corresponds to A.dot(weight)):param savepath: string, path where to save the file
|
|
716
|
+
:param verbose: [bool] [default is None] --- Print information throughout the process (default is False)
|
|
717
|
+
|
|
718
|
+
:return cubenew: [cube_data_class] --- New cube where the results are saved
|
|
719
|
+
"""
|
|
720
|
+
|
|
721
|
+
if result[0].columns[0] == "date1":
|
|
722
|
+
self.write_result_ticoi(
|
|
723
|
+
result=result,
|
|
724
|
+
source=source,
|
|
725
|
+
sensor=sensor,
|
|
726
|
+
filename=filename,
|
|
727
|
+
savepath=savepath,
|
|
728
|
+
result_quality=result_quality,
|
|
729
|
+
verbose=verbose,
|
|
730
|
+
)
|
|
731
|
+
else:
|
|
732
|
+
self.write_result_tico(
|
|
733
|
+
result=result,
|
|
734
|
+
source=source,
|
|
735
|
+
sensor=sensor,
|
|
736
|
+
filename=filename,
|
|
737
|
+
savepath=savepath,
|
|
738
|
+
result_quality=result_quality,
|
|
739
|
+
verbose=verbose,
|
|
740
|
+
)
|
|
741
|
+
return self
|