ocstrack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ocstrack/Collocation/__init__.py +1 -0
- ocstrack/Collocation/collocate.py +429 -0
- ocstrack/Collocation/output.py +63 -0
- ocstrack/Collocation/spatial.py +175 -0
- ocstrack/Collocation/temporal.py +112 -0
- ocstrack/Model/__init__.py +1 -0
- ocstrack/Model/model.py +236 -0
- ocstrack/Satellite/__init__.py +1 -0
- ocstrack/Satellite/get_sat.py +369 -0
- ocstrack/Satellite/satellite.py +103 -0
- ocstrack/Satellite/urls.py +11 -0
- ocstrack/__init__.py +1 -0
- ocstrack/utils.py +47 -0
- ocstrack-0.1.0.dist-info/METADATA +134 -0
- ocstrack-0.1.0.dist-info/RECORD +18 -0
- ocstrack-0.1.0.dist-info/WHEEL +5 -0
- ocstrack-0.1.0.dist-info/licenses/LICENSE.txt +121 -0
- ocstrack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import xarray as xr
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Union, Optional, List
|
|
11
|
+
from tqdm import tqdm
|
|
12
|
+
|
|
13
|
+
from .urls import URL_TEMPLATES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.WARNING,
|
|
18
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
19
|
+
handlers=[
|
|
20
|
+
logging.StreamHandler(), # Print logs to the console
|
|
21
|
+
# logging.FileHandler('get_sat.log', mode='w')
|
|
22
|
+
]
|
|
23
|
+
)
|
|
24
|
+
_logger = logging.getLogger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_daily_dates(start_date_str: str,
|
|
28
|
+
end_date_str: str) -> List[str]:
|
|
29
|
+
"""
|
|
30
|
+
This function generates a list of formated
|
|
31
|
+
dates between the start and end dates.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
start_date_str: String with dates as 'YYYY-MM-DD'
|
|
35
|
+
end_date_str: String with dates as 'YYYY-MM-DD'
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of formated dates (daily).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
start_date = datetime.strptime(start_date_str,
|
|
42
|
+
'%Y-%m-%d')
|
|
43
|
+
end_date = datetime.strptime(end_date_str,
|
|
44
|
+
'%Y-%m-%d')
|
|
45
|
+
|
|
46
|
+
return [(start_date + timedelta(days=i)).strftime('%Y%m%d')
|
|
47
|
+
for i in range((end_date - start_date).days + 1)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download_sat_data(dates_str: List[str],
|
|
51
|
+
url_template: str,
|
|
52
|
+
raw_dir: str,
|
|
53
|
+
sat: str,
|
|
54
|
+
retries: int = 1,
|
|
55
|
+
delay: int = 5) -> List[str]:
|
|
56
|
+
"""
|
|
57
|
+
This function downloads the satellite data
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
dates_str: List with all the dates data will be downloaded for
|
|
62
|
+
url_template: from urls.py
|
|
63
|
+
raw_dir: path to where the raw sat data will be saved
|
|
64
|
+
retries: how many times will it try to download the data
|
|
65
|
+
delay: how long will it wait to try the download again
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
List of paths to the downlaoded satellite files.
|
|
70
|
+
"""
|
|
71
|
+
os.makedirs(raw_dir, exist_ok=True)
|
|
72
|
+
raw_files = []
|
|
73
|
+
|
|
74
|
+
for date_str in tqdm(dates_str, desc=f"Downloading {sat} data..."):
|
|
75
|
+
url = f"{url_template}{date_str}.nc"
|
|
76
|
+
filename = os.path.basename(url)
|
|
77
|
+
raw_path = os.path.join(raw_dir, filename)
|
|
78
|
+
|
|
79
|
+
if not os.path.exists(raw_path):
|
|
80
|
+
for attempt in range(retries):
|
|
81
|
+
try:
|
|
82
|
+
with requests.get(url, stream=True) as r:
|
|
83
|
+
r.raise_for_status()
|
|
84
|
+
with open(raw_path, 'wb') as f:
|
|
85
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
86
|
+
f.write(chunk)
|
|
87
|
+
_logger.info(f"Downloaded {filename}")
|
|
88
|
+
break
|
|
89
|
+
except requests.RequestException as e:
|
|
90
|
+
_logger.warning(f"Attempt {attempt+1}/{retries} \
|
|
91
|
+
failed for {url}: {e}")
|
|
92
|
+
time.sleep(delay)
|
|
93
|
+
else:
|
|
94
|
+
_logger.error(f"Failed to download {filename} after \
|
|
95
|
+
{retries} attempts.")
|
|
96
|
+
else:
|
|
97
|
+
_logger.info(f"File already exists: {filename}")
|
|
98
|
+
raw_files.append(raw_path)
|
|
99
|
+
|
|
100
|
+
return raw_files
|
|
101
|
+
|
|
102
|
+
def crop_by_box(dataset: xr.Dataset,
|
|
103
|
+
lat_min: float,
|
|
104
|
+
lat_max: float,
|
|
105
|
+
lon_min: float,
|
|
106
|
+
lon_max: float) -> xr.Dataset:
|
|
107
|
+
"""
|
|
108
|
+
Crops xarray data based on lats and lons
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
lat_min: float/int of mininum latitude
|
|
113
|
+
lat_max: float/int of maximum latitude
|
|
114
|
+
lon_min: float/int of mininum longitude
|
|
115
|
+
lon_max: float/int of maximum latitude
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
xarray object of the cropped data
|
|
119
|
+
|
|
120
|
+
Notes
|
|
121
|
+
-----
|
|
122
|
+
Satellite data uses the -180 to 180 standard
|
|
123
|
+
If you want to cross the meridian, then pass a lon_min > lon_max
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
# Check if latitude and longitude coordinates are in the dataset
|
|
127
|
+
if 'lat' not in dataset or 'lon' not in dataset:
|
|
128
|
+
raise ValueError("Dataset does not contain lat or lon dimensions")
|
|
129
|
+
|
|
130
|
+
if lon_min < lon_max:
|
|
131
|
+
lon_mask = (dataset.lon >= lon_min) & (dataset.lon <= lon_max)
|
|
132
|
+
else:
|
|
133
|
+
lon_mask = (dataset.lon >= lon_min) | (dataset.lon <= lon_max)
|
|
134
|
+
|
|
135
|
+
lat_mask = (dataset.lat >= lat_min) & (dataset.lat <= lat_max)
|
|
136
|
+
cropped = dataset.where(lat_mask & lon_mask, drop=True)
|
|
137
|
+
|
|
138
|
+
return cropped
|
|
139
|
+
|
|
140
|
+
def crop_by_shape():
|
|
141
|
+
"""
|
|
142
|
+
To be implemented
|
|
143
|
+
"""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def crop_sat_data(file_paths: List[str],
|
|
147
|
+
cropped_dir: str,
|
|
148
|
+
lat_min: float,
|
|
149
|
+
lat_max: float,
|
|
150
|
+
lon_min: float,
|
|
151
|
+
lon_max: float) -> List[xr.Dataset]:
|
|
152
|
+
"""
|
|
153
|
+
Handles the satellite data cropping
|
|
154
|
+
Crops and saves all the satellite data
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
lat_min: float/int of mininum latitude
|
|
159
|
+
lat_max: float/int of maximum latitude
|
|
160
|
+
lon_min: float/int of mininum longitude
|
|
161
|
+
lon_max: float/int of maximum latitude
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
xarray object of the cropped data
|
|
166
|
+
|
|
167
|
+
Notes
|
|
168
|
+
-----
|
|
169
|
+
Satellite data uses the -180 to 180 standard
|
|
170
|
+
If you want to change the longitudes, use util.convert_longitude(sat_data.lon,mode=?)
|
|
171
|
+
"""
|
|
172
|
+
os.makedirs(cropped_dir, exist_ok=True)
|
|
173
|
+
cropped_datasets = []
|
|
174
|
+
|
|
175
|
+
for file_path in tqdm(file_paths, desc="Cropping"):
|
|
176
|
+
try:
|
|
177
|
+
with xr.open_dataset(file_path) as ds:
|
|
178
|
+
ds.load()
|
|
179
|
+
cropped = crop_by_box(ds, lat_min, lat_max, lon_min, lon_max)
|
|
180
|
+
if cropped.dims and all(size > 0 for size in cropped.sizes.values()):
|
|
181
|
+
out_path = os.path.join(cropped_dir,
|
|
182
|
+
f"cropped_\
|
|
183
|
+
{os.path.basename(file_path)}")
|
|
184
|
+
cropped.to_netcdf(out_path)
|
|
185
|
+
_logger.info(f"Saved {out_path}")
|
|
186
|
+
cropped_datasets.append(cropped)
|
|
187
|
+
else:
|
|
188
|
+
_logger.warning(f"Skipping empty cropped dataset: \
|
|
189
|
+
{file_path}")
|
|
190
|
+
except Exception as e:
|
|
191
|
+
_logger.warning(f"Failed to crop \
|
|
192
|
+
{file_path}: {type(e).__name__} - {e}")
|
|
193
|
+
|
|
194
|
+
return cropped_datasets
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def concat_sat_data(datasets: List[xr.Dataset],
|
|
198
|
+
output_path: str,
|
|
199
|
+
sat: str) -> Optional[xr.Dataset]:
|
|
200
|
+
"""
|
|
201
|
+
Handles the satellite data concatenation
|
|
202
|
+
Concat and saves all the satellite data on the datasets list
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
datasets: List xr satellite data to be concatenated
|
|
207
|
+
output_path: path to where the concatenated data will be saved
|
|
208
|
+
sat: name of the satellite
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
----------
|
|
212
|
+
xarray object of the concatenated data
|
|
213
|
+
"""
|
|
214
|
+
if not datasets:
|
|
215
|
+
_logger.warning("No datasets to concatenate.")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
concat_ds = xr.concat(datasets, dim='time')
|
|
220
|
+
concat_ds = concat_ds.assign_coords(source=sat)
|
|
221
|
+
concat_ds.to_netcdf(output_path)
|
|
222
|
+
_logger.info(f"Concatenated dataset saved to {output_path}")
|
|
223
|
+
return concat_ds
|
|
224
|
+
except Exception as e:
|
|
225
|
+
_logger.warning(f"Failed to concatenate datasets: \
|
|
226
|
+
{type(e).__name__} - {e}")
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_per_sat(start_date: str,
|
|
231
|
+
end_date: str,
|
|
232
|
+
sat: str,
|
|
233
|
+
output_dir: Union[str, os.PathLike],
|
|
234
|
+
lat_min: Optional[float] = None,
|
|
235
|
+
lat_max: Optional[float] = None,
|
|
236
|
+
lon_min: Optional[float] = None,
|
|
237
|
+
lon_max: Optional[float] = None,
|
|
238
|
+
concat: bool = True,
|
|
239
|
+
clean_raw: bool = False,
|
|
240
|
+
clean_cropped: bool = False) -> Optional[xr.Dataset]:
|
|
241
|
+
"""
|
|
242
|
+
Download, crop, and optionally concatenate satellite data.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
start_date: Start date in 'YYYY-MM-DD'
|
|
247
|
+
end_date: End date in 'YYYY-MM-DD'
|
|
248
|
+
sat: Satellite key for URL_TEMPLATES
|
|
249
|
+
output_dir: Directory to save files
|
|
250
|
+
lat_min, lat_max, lon_min, lon_max: Optional cropping bounds
|
|
251
|
+
concat: Save a single concatenated output
|
|
252
|
+
clean_raw: Delete raw files after processing
|
|
253
|
+
clean_cropped: Delete cropped files after processing
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
----------
|
|
257
|
+
xarray.Dataset if concatenated, otherwise None
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
url_template = URL_TEMPLATES[sat]
|
|
261
|
+
except KeyError:
|
|
262
|
+
raise ValueError(f"Unknown satellite key: {sat}")
|
|
263
|
+
|
|
264
|
+
output_dir = os.path.join(output_dir, sat)
|
|
265
|
+
raw_dir = os.path.join(output_dir, "raw")
|
|
266
|
+
cropped_dir = os.path.join(output_dir, "cropped")
|
|
267
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
268
|
+
|
|
269
|
+
cropping_enabled = None not in (lat_min, lat_max, lon_min, lon_max)
|
|
270
|
+
dates_str = generate_daily_dates(start_date, end_date)
|
|
271
|
+
|
|
272
|
+
raw_files = download_sat_data(dates_str, url_template, raw_dir, sat)
|
|
273
|
+
|
|
274
|
+
datasets_to_concat = []
|
|
275
|
+
if cropping_enabled:
|
|
276
|
+
datasets_to_concat = crop_sat_data(raw_files,
|
|
277
|
+
cropped_dir,
|
|
278
|
+
lat_min,
|
|
279
|
+
lat_max,
|
|
280
|
+
lon_min,
|
|
281
|
+
lon_max)
|
|
282
|
+
else:
|
|
283
|
+
for file_path in raw_files:
|
|
284
|
+
try:
|
|
285
|
+
with xr.open_dataset(file_path) as ds:
|
|
286
|
+
ds.load()
|
|
287
|
+
datasets_to_concat.append(ds)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
_logger.warning(f"Failed to load \
|
|
290
|
+
{file_path}: {type(e).__name__} - {e}")
|
|
291
|
+
|
|
292
|
+
final_dataset = None
|
|
293
|
+
if concat and datasets_to_concat:
|
|
294
|
+
concat_filename = f"concat_{'cropped_' if cropping_enabled else ''}{sat}_{start_date}_{end_date}.nc"
|
|
295
|
+
concat_path = os.path.join(output_dir, concat_filename)
|
|
296
|
+
final_dataset = concat_sat_data(datasets_to_concat, concat_path, sat)
|
|
297
|
+
|
|
298
|
+
if clean_raw and os.path.exists(raw_dir):
|
|
299
|
+
shutil.rmtree(raw_dir)
|
|
300
|
+
_logger.info("Raw files removed.")
|
|
301
|
+
|
|
302
|
+
if clean_cropped and os.path.exists(cropped_dir):
|
|
303
|
+
shutil.rmtree(cropped_dir)
|
|
304
|
+
_logger.info("Cropped files removed.")
|
|
305
|
+
|
|
306
|
+
return final_dataset
|
|
307
|
+
|
|
308
|
+
def get_multi_sat(start_date: str,
|
|
309
|
+
end_date: str,
|
|
310
|
+
sat_list: List,
|
|
311
|
+
output_dir: Union[str, os.PathLike],
|
|
312
|
+
lat_min: Optional[float] = None,
|
|
313
|
+
lat_max: Optional[float] = None,
|
|
314
|
+
lon_min: Optional[float] = None,
|
|
315
|
+
lon_max: Optional[float] = None,
|
|
316
|
+
concat: bool = True,
|
|
317
|
+
clean_raw: bool = True,
|
|
318
|
+
clean_cropped: bool = True) -> Optional[xr.Dataset]:
|
|
319
|
+
"""
|
|
320
|
+
Run download and processing for multiple satellites.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
start_date: Start date in 'YYYY-MM-DD'
|
|
325
|
+
end_date: End date in 'YYYY-MM-DD'
|
|
326
|
+
sat: Satellite key for URL_TEMPLATES
|
|
327
|
+
output_dir: Directory to save files
|
|
328
|
+
lat_min, lat_max, lon_min, lon_max: Optional cropping bounds
|
|
329
|
+
concat: Save a single concatenated output
|
|
330
|
+
clean_raw: Delete raw files after processing
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
----------
|
|
334
|
+
xarray.Dataset if concatenated, otherwise None
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
all_sat = []
|
|
338
|
+
for sat in sat_list:
|
|
339
|
+
concat_ds = get_per_sat(start_date,
|
|
340
|
+
end_date,
|
|
341
|
+
sat,
|
|
342
|
+
output_dir,
|
|
343
|
+
lat_min,
|
|
344
|
+
lat_max,
|
|
345
|
+
lon_min,
|
|
346
|
+
lon_max,
|
|
347
|
+
concat=concat,
|
|
348
|
+
clean_raw=clean_raw,
|
|
349
|
+
clean_cropped=clean_cropped)
|
|
350
|
+
if concat_ds is not None:
|
|
351
|
+
all_sat.append(concat_ds)
|
|
352
|
+
|
|
353
|
+
if all_sat:
|
|
354
|
+
try:
|
|
355
|
+
multisat_filename = f"multisat_{'cropped_' if lat_min is not None else ''}{start_date}_{end_date}.nc"
|
|
356
|
+
multisat_path = os.path.join(output_dir, multisat_filename)
|
|
357
|
+
all_sat_ds = xr.concat(all_sat, dim='time')
|
|
358
|
+
all_sat_ds.to_netcdf(multisat_path)
|
|
359
|
+
_logger.info(f"Concatenated multisat dataset saved to \
|
|
360
|
+
{multisat_path}")
|
|
361
|
+
return all_sat_ds
|
|
362
|
+
except Exception as e:
|
|
363
|
+
_logger.warning(f"Failed to concatenate all satellite datasets: \
|
|
364
|
+
{type(e).__name__} - {e}")
|
|
365
|
+
else:
|
|
366
|
+
_logger.warning("No satellite datasets were successfully processed.")
|
|
367
|
+
|
|
368
|
+
return None
|
|
369
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
class SatelliteData:
|
|
7
|
+
"""
|
|
8
|
+
Satellite altimetry data handler.
|
|
9
|
+
|
|
10
|
+
Loads a NetCDF file containing satellite-derived variables such as
|
|
11
|
+
significant wave height (SWH), sea level anomaly (SLA), and metadata.
|
|
12
|
+
|
|
13
|
+
Provides accessor properties and time filtering functionality.
|
|
14
|
+
|
|
15
|
+
Methods
|
|
16
|
+
-------
|
|
17
|
+
filter_by_time(start_date, end_date)
|
|
18
|
+
Restrict the dataset to a specific time range
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, filepath: str):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the SatelliteData object by loading a NetCDF dataset.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
filepath : str
|
|
27
|
+
Path to the satellite NetCDF file
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
ValueError
|
|
32
|
+
If required variables are missing from the dataset
|
|
33
|
+
"""
|
|
34
|
+
self.ds = xr.open_dataset(filepath)
|
|
35
|
+
|
|
36
|
+
# Check essential variables exist
|
|
37
|
+
required_vars = ['time', 'lon', 'lat', 'swh', 'sla', 'source']
|
|
38
|
+
missing = [v for v in required_vars if v not in self.ds.variables]
|
|
39
|
+
if missing:
|
|
40
|
+
raise ValueError(f"Missing required variables in dataset: {missing}")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def time(self):
|
|
44
|
+
return self.ds.time.values
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def lon(self):
|
|
48
|
+
return self.ds.lon.values
|
|
49
|
+
@lon.setter
|
|
50
|
+
def lon(self, new_lon: Union[np.ndarray, list]):
|
|
51
|
+
if len(new_lon) != len(self.ds.lon):
|
|
52
|
+
raise ValueError("New longitude array must match existing size.")
|
|
53
|
+
self.ds['lon'] = ('time', np.array(new_lon))
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def lat(self):
|
|
57
|
+
return self.ds.lat.values
|
|
58
|
+
@lat.setter
|
|
59
|
+
def lat(self, new_lat: Union[np.ndarray, list]):
|
|
60
|
+
if len(new_lat) != len(self.ds.lat):
|
|
61
|
+
raise ValueError("New latitude array must match existing size.")
|
|
62
|
+
self.ds['lat'] = ('time', np.array(new_lat))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def swh(self):
|
|
66
|
+
return self.ds.swh.values
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def sla(self):
|
|
70
|
+
return self.ds.sla.values
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def source(self):
|
|
74
|
+
return self.ds.source.values
|
|
75
|
+
|
|
76
|
+
def filter_by_time(self,
|
|
77
|
+
start_date: str,
|
|
78
|
+
end_date: str) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Filter the dataset by time range.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
start_date : str
|
|
85
|
+
ISO 8601 string representing the start date
|
|
86
|
+
end_date : str
|
|
87
|
+
ISO 8601 string representing the end date
|
|
88
|
+
|
|
89
|
+
Notes
|
|
90
|
+
-----
|
|
91
|
+
The method converts time variables to datetime64 and ensures the time
|
|
92
|
+
dimension is sorted before filtering.
|
|
93
|
+
"""
|
|
94
|
+
# Convert to datetime for safety
|
|
95
|
+
start = np.datetime64(start_date)
|
|
96
|
+
end = np.datetime64(end_date)
|
|
97
|
+
|
|
98
|
+
# Ensure time is datetime64 and sorted
|
|
99
|
+
if not np.issubdtype(self.ds['time'].dtype, np.datetime64):
|
|
100
|
+
self.ds['time'] = xr.decode_cf(self.ds).time
|
|
101
|
+
|
|
102
|
+
self.ds = self.ds.sortby('time') # Ensure sorted time axis
|
|
103
|
+
self.ds = self.ds.sel(time=slice(start, end))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
URL_TEMPLATES = {
|
|
3
|
+
'sentinel3a': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/3a/3a_',
|
|
4
|
+
'sentinel3b': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/3b/3b_',
|
|
5
|
+
'sentinel6a': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/6a/6a_',
|
|
6
|
+
'jason2': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/j2/j2_',
|
|
7
|
+
'jason3': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/j3/j3_',
|
|
8
|
+
'cryosat2': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/c2/c2_',
|
|
9
|
+
'saral': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/sa/sa_',
|
|
10
|
+
'swot': 'https://www.star.nesdis.noaa.gov/data/pub0010/lsa/johnk/coastwatch/sw/sw_',
|
|
11
|
+
}
|
ocstrack/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
ocstrack/utils.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
# Set up logging
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
level=logging.WARNING,
|
|
9
|
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
10
|
+
handlers=[
|
|
11
|
+
logging.StreamHandler(),
|
|
12
|
+
]
|
|
13
|
+
)
|
|
14
|
+
_logger = logging.getLogger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def convert_longitude(lon: Union[float, np.ndarray],
|
|
18
|
+
mode: int = 1) -> np.ndarray:
|
|
19
|
+
"""
|
|
20
|
+
Convert longitudes between common geographic conventions.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
lon: array-like of longitudes
|
|
24
|
+
mode: conversion mode:
|
|
25
|
+
- 1: Convert from [-180, 180] (Greenwich at 0°) → [0, 360] (Greenwich at 0°)
|
|
26
|
+
- 2: Convert from [0, 360] (Greenwich at 0°) → [-180, 180] (Greenwich at 0°)
|
|
27
|
+
- 3: Convert from [-180, 180] (Greenwich at 0°) → [0, 360] (Greenwich at 180°)
|
|
28
|
+
- 4: Convert from [0, 360] (Greenwich at 0°) → [0, 360] (Greenwich at 180°)
|
|
29
|
+
- 5: Convert from [0, 360] (Greenwich at 180°) → [0, 360] (Greenwich at 0°)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
np.ndarray of converted longitudes
|
|
33
|
+
"""
|
|
34
|
+
_logger.debug("Converting longitude with mode %d", mode)
|
|
35
|
+
lon = np.asarray(lon)
|
|
36
|
+
if mode == 1:
|
|
37
|
+
return lon % 360
|
|
38
|
+
elif mode == 2:
|
|
39
|
+
return np.where(lon > 180, lon - 360, lon)
|
|
40
|
+
elif mode == 3:
|
|
41
|
+
return lon + 180
|
|
42
|
+
elif mode == 4:
|
|
43
|
+
return (lon + 180) % 360
|
|
44
|
+
elif mode == 5:
|
|
45
|
+
return (lon - 180) % 360
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Invalid mode {mode}. Supported modes: 1, 2, 3, 4, 5")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ocstrack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Satellite data download, crop, and collocation with model outputs
|
|
5
|
+
Author-email: Felicio Cassalho <felicio.cassalho@noaa.gov>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/noaa-ocs-modeling/OCSTrack
|
|
8
|
+
Project-URL: Repository, https://github.com/noaa-ocs-modeling/OCSTrack
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: xarray
|
|
14
|
+
Requires-Dist: scipy
|
|
15
|
+
Requires-Dist: tqdm
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: netcdf4
|
|
18
|
+
Requires-Dist: h5netcdf
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# OCSTrack
|
|
22
|
+
OCSTrack is an object-oriented Python package for the along-track collocation of satellite data with ocean circulation and wave model outputs.
|
|
23
|
+
It simplifies the process of aligning diverse datasets, making it easier to compare and analyze satellite observations against model simulations.
|
|
24
|
+
|
|
25
|
+
## Key Features
|
|
26
|
+
### Satellite Altimetry Data Support
|
|
27
|
+
|
|
28
|
+
Seamlessly integrates with NOAA [CoastalWatch](https://coastwatch.noaa.gov/cwn/products/along-track-significant-wave-height-wind-speed-and-sea-level-anomaly-multiple-altimeters.html) altimetry data, providing access to a wide range of missions:
|
|
29
|
+
* Jason-2
|
|
30
|
+
* Jason-3
|
|
31
|
+
* Sentinel-3A
|
|
32
|
+
* Sentinel-3B
|
|
33
|
+
* Sentinel-6A
|
|
34
|
+
* CryoSat-2
|
|
35
|
+
* SARAL
|
|
36
|
+
* SWOT
|
|
37
|
+
|
|
38
|
+
### Ocean Model Data Support
|
|
39
|
+
Supports outputs from various ocean circulation and wave models:
|
|
40
|
+
* [SCHISM+WWM](https://github.com/schism-dev/schism)
|
|
41
|
+
* WaveWatch3 (to be implemented)
|
|
42
|
+
* ADCIRC+SWAN (to be implemented)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
1. **Create and activate a new conda environment:**
|
|
48
|
+
This command creates an environment named `ocstrack` and installs all dependencies from `conda-forge`.
|
|
49
|
+
```bash
|
|
50
|
+
conda create -n ocstrack -c conda-forge python=3.10 numpy xarray scipy tqdm requests netcdf4 h5netcdf
|
|
51
|
+
conda activate ocstrack
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. **Install `ocstrack` from GitHub:**
|
|
55
|
+
Finally, install this package using `pip`.
|
|
56
|
+
```bash
|
|
57
|
+
pip install "git+[https://github.com/noaa-ocs-modeling/OCSTrack.git](https://github.com/noaa-ocs-modeling/OCSTrack.git)"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
Here's a typical workflow demonstrating how to use OCSTrack to download satellite data, load model outputs, and perform collocation.
|
|
62
|
+
```
|
|
63
|
+
import numpy as np
|
|
64
|
+
import xarray as xr
|
|
65
|
+
# Assuming ocstrack is installed and available in your environment
|
|
66
|
+
from ocstrack.Model.model import SCHISM
|
|
67
|
+
from ocstrack.Satellite.satellite import SatelliteData
|
|
68
|
+
from ocstrack.Satellite import get_sat
|
|
69
|
+
from ocstrack.Collocation.collocate import Collocate
|
|
70
|
+
from ocstrack.utils import convert_longitude
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# 1. Download Satellite Data
|
|
74
|
+
# Specify your desired date range, list of satellites, output directory, and geographical bounding box.
|
|
75
|
+
get_sat.get_multi_sat(start_date="2019-07-30",
|
|
76
|
+
end_date="2019-08-04",
|
|
77
|
+
sat_list=['sentinel3a','sentinel3b','jason2','jason3','cryosat2','saral'],
|
|
78
|
+
output_dir=r"Your/Path/Here/",
|
|
79
|
+
lat_min=49.109,
|
|
80
|
+
lat_max=66.304309,
|
|
81
|
+
lon_min=156.6854,
|
|
82
|
+
lon_max=-156.864,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 2. Define File Paths
|
|
86
|
+
# Set the paths for your downloaded satellite data, model run, and where you want to save the collocated output.
|
|
87
|
+
sat_path = "/path/to/your/multisat_cropped_2019-07-30_2019-08-04.nc"
|
|
88
|
+
model_path = "/path/to/your/model/run/"
|
|
89
|
+
output_path = "/path/to/your/collocated_output.nc"
|
|
90
|
+
s_time,e_time = "2019-08-01", "2019-08-03"
|
|
91
|
+
|
|
92
|
+
# 3. Load Satellite Data
|
|
93
|
+
# Initialize the SatelliteData object with your satellite data file.
|
|
94
|
+
sat_data = SatelliteData(sat_path)
|
|
95
|
+
# It's crucial to ensure longitude conventions match between satellite and model data.
|
|
96
|
+
# Use convert_longitude if needed (mode=1 for converting to 0-360 degrees).
|
|
97
|
+
sat_data.lon = convert_longitude(sat_data.lon, mode=1)
|
|
98
|
+
|
|
99
|
+
# 4. Load Model Data
|
|
100
|
+
# Instantiate the SCHISM model object, specifying the run directory and model variable details.
|
|
101
|
+
model_run = SCHISM(
|
|
102
|
+
rundir=model_path,
|
|
103
|
+
model_dict={'var': 'sigWaveHeight',
|
|
104
|
+
'startswith': 'out2d_', # File name prefix for 2D outputs
|
|
105
|
+
'var_type': '2D',
|
|
106
|
+
'model': 'SCHISM'},
|
|
107
|
+
start_date=np.datetime64(s_time),
|
|
108
|
+
end_date=np.datetime64(e_time)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# 5. Perform Collocation
|
|
112
|
+
# Create a Collocate object, providing the loaded model and satellite data.
|
|
113
|
+
coll = Collocate(
|
|
114
|
+
model_run=model_run,
|
|
115
|
+
satellite=sat_data,
|
|
116
|
+
# dist_coast=dist_coast,
|
|
117
|
+
n_nearest=3,
|
|
118
|
+
# search_radius = 3000,
|
|
119
|
+
temporal_interp=True
|
|
120
|
+
)
|
|
121
|
+
ds_coll = coll.run(output_path=output_path) # Execute the collocation and save the results
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Contributing
|
|
125
|
+
We welcome contributions to OCSTrack! If you have ideas for improvements, new features, or find a bug, please don't hesitate to open an issue or submit a pull request on our GitHub repository. Your input helps make OCSTrack better for everyone.
|
|
126
|
+
|
|
127
|
+
### Contact
|
|
128
|
+
<sub><sup>Contact: felicio.cassalho@noaa.gov </sup></sub>
|
|
129
|
+
|
|
130
|
+

|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
#### Acknowledgements:
|
|
134
|
+
*OCSTrack was inspired by the MATLAB-based [WW3-tools](https://github.com/NOAA-EMC/WW3-tools) and [wave-tools](https://github.com/NOAA-EMC/WW3-tools) collocation tools developed for WaveWatch3.*
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ocstrack/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
2
|
+
ocstrack/utils.py,sha256=js_J3m2CqHDpryNNRRWkROrqiY4PqC86hH7jHVXHy5s,1543
|
|
3
|
+
ocstrack/Collocation/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
4
|
+
ocstrack/Collocation/collocate.py,sha256=B_niVPz_zB1cUXJcZyoIbxoH0TEOkOevjoogctAP1Y4,17648
|
|
5
|
+
ocstrack/Collocation/output.py,sha256=NT1orNapFx7WCOkqouSiylL_Qz_BRCjPi1xIYB1SloI,2730
|
|
6
|
+
ocstrack/Collocation/spatial.py,sha256=OH6m7QlkI-FnkegZsX6tkNXxvwXMRCDco41SyS12pGE,6657
|
|
7
|
+
ocstrack/Collocation/temporal.py,sha256=BPjXJqxMiBFNSbLV2gGUSSl39HpZDIZiNByk1-j4eAs,4010
|
|
8
|
+
ocstrack/Model/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
9
|
+
ocstrack/Model/model.py,sha256=vxkxYMbxFfiV_0T-JDqT2D2q3LZmpL_CFMH6vRLwhBM,7887
|
|
10
|
+
ocstrack/Satellite/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
11
|
+
ocstrack/Satellite/get_sat.py,sha256=2Ut5K5ca_Pf_Pl9GUv-5Tg7OaujseQ2xG8_qsRfUCBA,12830
|
|
12
|
+
ocstrack/Satellite/satellite.py,sha256=O0vcbRrotp3xaTpVdBct-OnEiI5zG2evSB8siEIIxY8,3135
|
|
13
|
+
ocstrack/Satellite/urls.py,sha256=Q1MSanZwTnsjghRO6j9yH8ZGWVo6Y0EWraxOCN4dR6E,790
|
|
14
|
+
ocstrack-0.1.0.dist-info/licenses/LICENSE.txt,sha256=9Ofzc7m5lpUDN-jUGkopOcLZC3cl6brz1QhKInF60yg,7169
|
|
15
|
+
ocstrack-0.1.0.dist-info/METADATA,sha256=Zfp3T6kMOPg1Na7RwHDJ34MCJ-j34L0nWUqBGCyndy4,5657
|
|
16
|
+
ocstrack-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
ocstrack-0.1.0.dist-info/top_level.txt,sha256=II1d5yXek4m6ld-nK6MGUXQT8dpvgrgPRJuYgyIfJhA,9
|
|
18
|
+
ocstrack-0.1.0.dist-info/RECORD,,
|